游戏和幸运礼物配置
This commit is contained in:
parent
90bf486385
commit
841480d89e
@ -134,6 +134,20 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/admin/activity/user-leaderboards": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "listUserLeaderboards",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/components/responses/EmptyResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-permission": "user-leaderboard:view",
|
||||||
|
"x-permissions": [
|
||||||
|
"user-leaderboard:view"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/admin/activity/lucky-gifts/config": {
|
"/admin/activity/lucky-gifts/config": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getLuckyGiftConfig",
|
"operationId": "getLuckyGiftConfig",
|
||||||
@ -160,6 +174,20 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/admin/activity/lucky-gifts/configs": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "listLuckyGiftConfigs",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/components/responses/EmptyResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-permission": "lucky-gift:view",
|
||||||
|
"x-permissions": [
|
||||||
|
"lucky-gift:view"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/admin/activity/lucky-gifts/draws": {
|
"/admin/activity/lucky-gifts/draws": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "listLuckyGiftDraws",
|
"operationId": "listLuckyGiftDraws",
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import HistoryOutlined from "@mui/icons-material/HistoryOutlined";
|
|||||||
import HubOutlined from "@mui/icons-material/HubOutlined";
|
import HubOutlined from "@mui/icons-material/HubOutlined";
|
||||||
import ImageOutlined from "@mui/icons-material/ImageOutlined";
|
import ImageOutlined from "@mui/icons-material/ImageOutlined";
|
||||||
import Inventory2Outlined from "@mui/icons-material/Inventory2Outlined";
|
import Inventory2Outlined from "@mui/icons-material/Inventory2Outlined";
|
||||||
|
import LeaderboardOutlined from "@mui/icons-material/LeaderboardOutlined";
|
||||||
import LoginOutlined from "@mui/icons-material/LoginOutlined";
|
import LoginOutlined from "@mui/icons-material/LoginOutlined";
|
||||||
import ManageAccountsOutlined from "@mui/icons-material/ManageAccountsOutlined";
|
import ManageAccountsOutlined from "@mui/icons-material/ManageAccountsOutlined";
|
||||||
import MapOutlined from "@mui/icons-material/MapOutlined";
|
import MapOutlined from "@mui/icons-material/MapOutlined";
|
||||||
@ -42,6 +43,7 @@ const iconMap = {
|
|||||||
event_available: EventAvailableOutlined,
|
event_available: EventAvailableOutlined,
|
||||||
gift: CardGiftcardOutlined,
|
gift: CardGiftcardOutlined,
|
||||||
inventory: Inventory2Outlined,
|
inventory: Inventory2Outlined,
|
||||||
|
leaderboard: LeaderboardOutlined,
|
||||||
network: HubOutlined,
|
network: HubOutlined,
|
||||||
team: GroupsOutlined,
|
team: GroupsOutlined,
|
||||||
history: HistoryOutlined,
|
history: HistoryOutlined,
|
||||||
@ -147,6 +149,7 @@ export const fallbackNavigation = [
|
|||||||
routeNavItem("daily-task-list", { icon: TaskAltOutlined }),
|
routeNavItem("daily-task-list", { icon: TaskAltOutlined }),
|
||||||
routeNavItem("registration-reward", { icon: CardGiftcardOutlined }),
|
routeNavItem("registration-reward", { icon: CardGiftcardOutlined }),
|
||||||
routeNavItem("seven-day-checkin", { icon: EventAvailableOutlined }),
|
routeNavItem("seven-day-checkin", { icon: EventAvailableOutlined }),
|
||||||
|
routeNavItem("user-leaderboard", { icon: LeaderboardOutlined }),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -108,6 +108,7 @@ export const PERMISSIONS = {
|
|||||||
luckyGiftUpdate: "lucky-gift:update",
|
luckyGiftUpdate: "lucky-gift:update",
|
||||||
roomTreasureView: "room-treasure:view",
|
roomTreasureView: "room-treasure:view",
|
||||||
roomTreasureUpdate: "room-treasure:update",
|
roomTreasureUpdate: "room-treasure:update",
|
||||||
|
userLeaderboardView: "user-leaderboard:view",
|
||||||
uploadCreate: "upload:create",
|
uploadCreate: "upload:create",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@ -154,6 +155,7 @@ export const MENU_CODES = {
|
|||||||
achievementConfig: "achievement-config",
|
achievementConfig: "achievement-config",
|
||||||
sevenDayCheckIn: "seven-day-checkin",
|
sevenDayCheckIn: "seven-day-checkin",
|
||||||
roomTreasure: "room-treasure",
|
roomTreasure: "room-treasure",
|
||||||
|
userLeaderboard: "user-leaderboard",
|
||||||
geo: "geo",
|
geo: "geo",
|
||||||
hostOrg: "host-org",
|
hostOrg: "host-org",
|
||||||
hostOrgCountries: "host-org-countries",
|
hostOrgCountries: "host-org-countries",
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import { rolesRoutes } from "@/features/roles/routes.js";
|
|||||||
import { roomRoutes } from "@/features/rooms/routes.js";
|
import { roomRoutes } from "@/features/rooms/routes.js";
|
||||||
import { roomTreasureRoutes } from "@/features/room-treasure/routes.js";
|
import { roomTreasureRoutes } from "@/features/room-treasure/routes.js";
|
||||||
import { sevenDayCheckInRoutes } from "@/features/seven-day-checkin/routes.js";
|
import { sevenDayCheckInRoutes } from "@/features/seven-day-checkin/routes.js";
|
||||||
|
import { userLeaderboardRoutes } from "@/features/user-leaderboard/routes.js";
|
||||||
import { usersRoutes } from "@/features/users/routes.js";
|
import { usersRoutes } from "@/features/users/routes.js";
|
||||||
import type { MenuCode } from "@/app/permissions";
|
import type { MenuCode } from "@/app/permissions";
|
||||||
import type { AdminRoute, PublicRoute } from "./types";
|
import type { AdminRoute, PublicRoute } from "./types";
|
||||||
@ -37,6 +38,7 @@ export const adminRoutes: AdminRoute[] = [
|
|||||||
...registrationRewardRoutes,
|
...registrationRewardRoutes,
|
||||||
...sevenDayCheckInRoutes,
|
...sevenDayCheckInRoutes,
|
||||||
...roomTreasureRoutes,
|
...roomTreasureRoutes,
|
||||||
|
...userLeaderboardRoutes,
|
||||||
...resourceRoutes,
|
...resourceRoutes,
|
||||||
...operationsRoutes,
|
...operationsRoutes,
|
||||||
...luckyGiftRoutes,
|
...luckyGiftRoutes,
|
||||||
|
|||||||
@ -8,6 +8,14 @@ export interface GamePlatformDto {
|
|||||||
platformName: string;
|
platformName: string;
|
||||||
status: string;
|
status: string;
|
||||||
apiBaseUrl: string;
|
apiBaseUrl: string;
|
||||||
|
// 服务端按 adapterType 选择厂商协议:demo/yomi_v4/leadercc_v1。
|
||||||
|
adapterType: string;
|
||||||
|
// callbackSecret 只用于提交更新;列表返回不会携带明文。
|
||||||
|
callbackSecret?: string;
|
||||||
|
callbackSecretSet: boolean;
|
||||||
|
callbackIpWhitelist: string[];
|
||||||
|
// 厂商扩展配置保留 JSON 字符串,后台前端不绑定具体字段结构。
|
||||||
|
adapterConfigJson: string;
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
createdAtMs?: number;
|
createdAtMs?: number;
|
||||||
updatedAtMs?: number;
|
updatedAtMs?: number;
|
||||||
@ -44,7 +52,10 @@ export interface GamePlatformPage {
|
|||||||
serverTimeMs?: number;
|
serverTimeMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GamePlatformPayload = Omit<GamePlatformDto, "appCode" | "createdAtMs" | "updatedAtMs">;
|
export type GamePlatformPayload = Omit<
|
||||||
|
GamePlatformDto,
|
||||||
|
"appCode" | "createdAtMs" | "updatedAtMs" | "callbackSecretSet"
|
||||||
|
>;
|
||||||
export type GameCatalogPayload = Omit<GameCatalogDto, "appCode" | "createdAtMs" | "updatedAtMs">;
|
export type GameCatalogPayload = Omit<GameCatalogDto, "appCode" | "createdAtMs" | "updatedAtMs">;
|
||||||
|
|
||||||
export interface GameCatalogQuery {
|
export interface GameCatalogQuery {
|
||||||
@ -118,6 +129,12 @@ function normalizePlatform(platform: Partial<GamePlatformDto>): GamePlatformDto
|
|||||||
platformName: stringValue(platform.platformName),
|
platformName: stringValue(platform.platformName),
|
||||||
status: stringValue(platform.status || "active"),
|
status: stringValue(platform.status || "active"),
|
||||||
apiBaseUrl: stringValue(platform.apiBaseUrl),
|
apiBaseUrl: stringValue(platform.apiBaseUrl),
|
||||||
|
adapterType: stringValue(platform.adapterType || "demo"),
|
||||||
|
callbackSecretSet: Boolean(platform.callbackSecretSet),
|
||||||
|
callbackIpWhitelist: Array.isArray(platform.callbackIpWhitelist)
|
||||||
|
? platform.callbackIpWhitelist.map(String).filter(Boolean)
|
||||||
|
: [],
|
||||||
|
adapterConfigJson: stringValue(platform.adapterConfigJson || "{}"),
|
||||||
sortOrder: numberValue(platform.sortOrder),
|
sortOrder: numberValue(platform.sortOrder),
|
||||||
createdAtMs: numberValue(platform.createdAtMs),
|
createdAtMs: numberValue(platform.createdAtMs),
|
||||||
updatedAtMs: numberValue(platform.updatedAtMs),
|
updatedAtMs: numberValue(platform.updatedAtMs),
|
||||||
|
|||||||
@ -55,3 +55,25 @@
|
|||||||
.fullField {
|
.fullField {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.platformList {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platformRow {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.platformActions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|||||||
@ -31,6 +31,14 @@ const defaultPlatformForm = {
|
|||||||
platformName: "",
|
platformName: "",
|
||||||
status: "active",
|
status: "active",
|
||||||
apiBaseUrl: "",
|
apiBaseUrl: "",
|
||||||
|
// 默认 demo 兼容本地调试;真实厂商在后台切到 yomi_v4/leadercc_v1。
|
||||||
|
adapterType: "demo",
|
||||||
|
// 密钥只写不读,打开编辑弹窗时始终为空,避免明文停留在浏览器状态里。
|
||||||
|
callbackSecret: "",
|
||||||
|
callbackSecretSet: false,
|
||||||
|
callbackIpWhitelistText: "",
|
||||||
|
// 厂商扩展配置以 JSON 透传,服务端按 adapter_type 自己解析需要的字段。
|
||||||
|
adapterConfigJson: "{}",
|
||||||
sortOrder: 0,
|
sortOrder: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -150,6 +158,10 @@ export function useGamesPage() {
|
|||||||
setActiveAction("create-platform");
|
setActiveAction("create-platform");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openManagePlatforms = () => {
|
||||||
|
setActiveAction("manage-platforms");
|
||||||
|
};
|
||||||
|
|
||||||
const openEditPlatform = (platform) => {
|
const openEditPlatform = (platform) => {
|
||||||
setEditingPlatformCode(platform.platformCode);
|
setEditingPlatformCode(platform.platformCode);
|
||||||
setPlatformForm(platformToForm(platform));
|
setPlatformForm(platformToForm(platform));
|
||||||
@ -224,6 +236,7 @@ export function useGamesPage() {
|
|||||||
openCreatePlatform,
|
openCreatePlatform,
|
||||||
openEditGame,
|
openEditGame,
|
||||||
openEditPlatform,
|
openEditPlatform,
|
||||||
|
openManagePlatforms,
|
||||||
pageSize,
|
pageSize,
|
||||||
platformCode,
|
platformCode,
|
||||||
platformData,
|
platformData,
|
||||||
@ -267,6 +280,12 @@ function platformToForm(platform) {
|
|||||||
platformName: platform.platformName || "",
|
platformName: platform.platformName || "",
|
||||||
status: platform.status || "active",
|
status: platform.status || "active",
|
||||||
apiBaseUrl: platform.apiBaseUrl || "",
|
apiBaseUrl: platform.apiBaseUrl || "",
|
||||||
|
adapterType: platform.adapterType || "demo",
|
||||||
|
// 后端不会返回 callbackSecret 明文;这里保留空字符串代表“不更新密钥”。
|
||||||
|
callbackSecret: "",
|
||||||
|
callbackSecretSet: Boolean(platform.callbackSecretSet),
|
||||||
|
callbackIpWhitelistText: (platform.callbackIpWhitelist || []).join("\n"),
|
||||||
|
adapterConfigJson: platform.adapterConfigJson || "{}",
|
||||||
sortOrder: Number(platform.sortOrder || 0),
|
sortOrder: Number(platform.sortOrder || 0),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -298,6 +317,16 @@ function platformPayload(form) {
|
|||||||
platformName: form.platformName.trim(),
|
platformName: form.platformName.trim(),
|
||||||
status: form.status.trim() || "active",
|
status: form.status.trim() || "active",
|
||||||
apiBaseUrl: form.apiBaseUrl.trim(),
|
apiBaseUrl: form.apiBaseUrl.trim(),
|
||||||
|
adapterType: form.adapterType.trim() || "demo",
|
||||||
|
// 空密钥提交给后端后会保留旧值;非空才覆盖,用于安全轮换 key/AppSecret。
|
||||||
|
callbackSecret: form.callbackSecret.trim(),
|
||||||
|
// 支持换行或逗号分隔,方便直接粘贴厂商给的多 IP 白名单。
|
||||||
|
callbackIpWhitelist: form.callbackIpWhitelistText
|
||||||
|
.split(/[\n,]/)
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
// JSON 不在前端强解析,避免前端版本限制后端新增厂商字段。
|
||||||
|
adapterConfigJson: form.adapterConfigJson.trim() || "{}",
|
||||||
sortOrder: Number(form.sortOrder || 0),
|
sortOrder: Number(form.sortOrder || 0),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import Add from "@mui/icons-material/Add";
|
import Add from "@mui/icons-material/Add";
|
||||||
import EditOutlined from "@mui/icons-material/EditOutlined";
|
import EditOutlined from "@mui/icons-material/EditOutlined";
|
||||||
|
import SettingsOutlined from "@mui/icons-material/SettingsOutlined";
|
||||||
import SportsEsportsOutlined from "@mui/icons-material/SportsEsportsOutlined";
|
import SportsEsportsOutlined from "@mui/icons-material/SportsEsportsOutlined";
|
||||||
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
|
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
@ -32,6 +33,12 @@ const orientationOptions = [
|
|||||||
["landscape", "Landscape"],
|
["landscape", "Landscape"],
|
||||||
];
|
];
|
||||||
const launchModeOptions = [["h5_popup", "H5 弹窗"]];
|
const launchModeOptions = [["h5_popup", "H5 弹窗"]];
|
||||||
|
// 这里的值必须和 game-service 的 adapter_type 白名单一致;新增厂商先后端登记,再放到后台可选项。
|
||||||
|
const adapterTypeOptions = [
|
||||||
|
["demo", "Demo"],
|
||||||
|
["yomi_v4", "小游 Yomi V4"],
|
||||||
|
["leadercc_v1", "灵仙 LeaderCC V1"],
|
||||||
|
];
|
||||||
|
|
||||||
const baseColumns = [
|
const baseColumns = [
|
||||||
{
|
{
|
||||||
@ -128,6 +135,11 @@ export function GameListPage() {
|
|||||||
<AdminListToolbar
|
<AdminListToolbar
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
|
{page.abilities.canUpdate ? (
|
||||||
|
<AdminActionIconButton label="平台配置" onClick={page.openManagePlatforms}>
|
||||||
|
<SettingsOutlined fontSize="small" />
|
||||||
|
</AdminActionIconButton>
|
||||||
|
) : null}
|
||||||
{page.abilities.canUpdate ? (
|
{page.abilities.canUpdate ? (
|
||||||
<AdminActionIconButton label="添加平台" onClick={page.openCreatePlatform}>
|
<AdminActionIconButton label="添加平台" onClick={page.openCreatePlatform}>
|
||||||
<SportsEsportsOutlined fontSize="small" />
|
<SportsEsportsOutlined fontSize="small" />
|
||||||
@ -178,6 +190,11 @@ export function GameListPage() {
|
|||||||
onClose={page.closeAction}
|
onClose={page.closeAction}
|
||||||
onSubmit={page.submitPlatform}
|
onSubmit={page.submitPlatform}
|
||||||
/>
|
/>
|
||||||
|
<PlatformManageDialog
|
||||||
|
open={page.activeAction === "manage-platforms"}
|
||||||
|
page={page}
|
||||||
|
onClose={page.closeAction}
|
||||||
|
/>
|
||||||
</AdminListPage>
|
</AdminListPage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -319,15 +336,15 @@ function GameFormDialog({ form, loading, mode, open, page, onClose, onSubmit })
|
|||||||
value={form.sortOrder}
|
value={form.sortOrder}
|
||||||
onChange={(event) => page.setGameForm({ ...form, sortOrder: event.target.value })}
|
onChange={(event) => page.setGameForm({ ...form, sortOrder: event.target.value })}
|
||||||
/>
|
/>
|
||||||
<SwitchOnlyField
|
<SwitchOnlyField
|
||||||
checked={form.status === "active"}
|
checked={form.status === "active"}
|
||||||
checkedLabel="启用"
|
checkedLabel="启用"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label="游戏启用状态"
|
label="游戏启用状态"
|
||||||
uncheckedLabel="维护"
|
uncheckedLabel="维护"
|
||||||
onChange={(checked) =>
|
onChange={(checked) =>
|
||||||
page.setGameForm({
|
page.setGameForm({
|
||||||
...form,
|
...form,
|
||||||
status: checked ? "active" : form.status === "disabled" ? "disabled" : "maintenance",
|
status: checked ? "active" : form.status === "disabled" ? "disabled" : "maintenance",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -386,15 +403,15 @@ function PlatformFormDialog({ form, loading, mode, open, page, onClose, onSubmit
|
|||||||
value={form.platformName}
|
value={form.platformName}
|
||||||
onChange={(event) => page.setPlatformForm({ ...form, platformName: event.target.value })}
|
onChange={(event) => page.setPlatformForm({ ...form, platformName: event.target.value })}
|
||||||
/>
|
/>
|
||||||
<SwitchOnlyField
|
<SwitchOnlyField
|
||||||
checked={form.status === "active"}
|
checked={form.status === "active"}
|
||||||
checkedLabel="启用"
|
checkedLabel="启用"
|
||||||
disabled={!page.abilities.canUpdate}
|
disabled={!page.abilities.canUpdate}
|
||||||
label="游戏平台启用状态"
|
label="游戏平台启用状态"
|
||||||
uncheckedLabel="维护"
|
uncheckedLabel="维护"
|
||||||
onChange={(checked) =>
|
onChange={(checked) =>
|
||||||
page.setPlatformForm({
|
page.setPlatformForm({
|
||||||
...form,
|
...form,
|
||||||
status: checked ? "active" : form.status === "disabled" ? "disabled" : "maintenance",
|
status: checked ? "active" : form.status === "disabled" ? "disabled" : "maintenance",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -408,18 +425,101 @@ function PlatformFormDialog({ form, loading, mode, open, page, onClose, onSubmit
|
|||||||
</AdminFormFieldGrid>
|
</AdminFormFieldGrid>
|
||||||
</AdminFormSection>
|
</AdminFormSection>
|
||||||
<AdminFormSection title="接入配置">
|
<AdminFormSection title="接入配置">
|
||||||
<TextField
|
<AdminFormFieldGrid columns="repeat(2, minmax(0, 1fr))">
|
||||||
className={styles.fullField}
|
{/* 适配器决定服务端启动参数、签名/解密和错误码映射。 */}
|
||||||
label="H5 Base URL"
|
<TextField
|
||||||
required
|
label="适配器"
|
||||||
value={form.apiBaseUrl}
|
select
|
||||||
onChange={(event) => page.setPlatformForm({ ...form, apiBaseUrl: event.target.value })}
|
value={form.adapterType}
|
||||||
/>
|
onChange={(event) => page.setPlatformForm({ ...form, adapterType: event.target.value })}
|
||||||
|
>
|
||||||
|
{adapterTypeOptions.map(([value, label]) => (
|
||||||
|
<MenuItem key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
label="H5/Auth URL"
|
||||||
|
required
|
||||||
|
value={form.apiBaseUrl}
|
||||||
|
onChange={(event) => page.setPlatformForm({ ...form, apiBaseUrl: event.target.value })}
|
||||||
|
/>
|
||||||
|
{/* 密钥不回显;编辑平台时留空表示沿用旧密钥,输入新值才覆盖。 */}
|
||||||
|
<TextField
|
||||||
|
label={form.callbackSecretSet ? "回调密钥(已配置)" : "回调密钥"}
|
||||||
|
type="password"
|
||||||
|
value={form.callbackSecret}
|
||||||
|
onChange={(event) => page.setPlatformForm({ ...form, callbackSecret: event.target.value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="IP 白名单"
|
||||||
|
minRows={3}
|
||||||
|
multiline
|
||||||
|
value={form.callbackIpWhitelistText}
|
||||||
|
onChange={(event) =>
|
||||||
|
page.setPlatformForm({ ...form, callbackIpWhitelistText: event.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{/* 厂商差异字段放 JSON:如 uid_mode、default_lang、token_ttl_seconds、game_level_uid。 */}
|
||||||
|
<TextField
|
||||||
|
className={styles.fullField}
|
||||||
|
label="适配器配置 JSON"
|
||||||
|
minRows={5}
|
||||||
|
multiline
|
||||||
|
value={form.adapterConfigJson}
|
||||||
|
onChange={(event) => page.setPlatformForm({ ...form, adapterConfigJson: event.target.value })}
|
||||||
|
/>
|
||||||
|
</AdminFormFieldGrid>
|
||||||
</AdminFormSection>
|
</AdminFormSection>
|
||||||
</AdminFormDialog>
|
</AdminFormDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PlatformManageDialog({ open, page, onClose }) {
|
||||||
|
const platforms = page.platformData.items || [];
|
||||||
|
return (
|
||||||
|
<AdminFormDialog
|
||||||
|
disabled={false}
|
||||||
|
loading={false}
|
||||||
|
open={open}
|
||||||
|
submitLabel="关闭"
|
||||||
|
title="平台配置"
|
||||||
|
onClose={onClose}
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={styles.platformList}>
|
||||||
|
{platforms.map((platform) => (
|
||||||
|
<div className={styles.platformRow} key={platform.platformCode}>
|
||||||
|
<div className={styles.stack}>
|
||||||
|
<span className={styles.name}>{platform.platformName || platform.platformCode}</span>
|
||||||
|
<span className={styles.meta}>
|
||||||
|
{platform.platformCode} / {platform.adapterType || "demo"}
|
||||||
|
</span>
|
||||||
|
<span className={styles.meta}>{platform.apiBaseUrl}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.platformActions}>
|
||||||
|
<span className={styles.meta}>
|
||||||
|
{platform.callbackSecretSet ? "密钥已配置" : "未配置密钥"}
|
||||||
|
</span>
|
||||||
|
<AdminActionIconButton
|
||||||
|
disabled={!page.abilities.canUpdate}
|
||||||
|
label="编辑平台"
|
||||||
|
onClick={() => page.openEditPlatform(platform)}
|
||||||
|
>
|
||||||
|
<EditOutlined fontSize="small" />
|
||||||
|
</AdminActionIconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</AdminFormDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function SwitchOnlyField({ checked, checkedLabel, disabled, label, onChange, uncheckedLabel }) {
|
function SwitchOnlyField({ checked, checkedLabel, disabled, label, onChange, uncheckedLabel }) {
|
||||||
return (
|
return (
|
||||||
<span className={styles.switchOnlyField}>
|
<span className={styles.switchOnlyField}>
|
||||||
|
|||||||
@ -6,6 +6,7 @@ export interface LuckyGiftTierDto {
|
|||||||
pool: string;
|
pool: string;
|
||||||
tierId: string;
|
tierId: string;
|
||||||
rewardCoins: number;
|
rewardCoins: number;
|
||||||
|
multiplierPPM: number;
|
||||||
weight: number;
|
weight: number;
|
||||||
highWaterOnly: boolean;
|
highWaterOnly: boolean;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -13,6 +14,7 @@ export interface LuckyGiftTierDto {
|
|||||||
|
|
||||||
export interface LuckyGiftConfigDto {
|
export interface LuckyGiftConfigDto {
|
||||||
appCode?: string;
|
appCode?: string;
|
||||||
|
poolId: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
ruleVersion: number;
|
ruleVersion: number;
|
||||||
giftPrice: number;
|
giftPrice: number;
|
||||||
@ -45,6 +47,7 @@ export interface LuckyGiftConfigDto {
|
|||||||
activityBudget: number;
|
activityBudget: number;
|
||||||
activityDailyLimit: number;
|
activityDailyLimit: number;
|
||||||
largeTierEnabled: boolean;
|
largeTierEnabled: boolean;
|
||||||
|
multiplierPPMs: number[];
|
||||||
tiers: LuckyGiftTierDto[];
|
tiers: LuckyGiftTierDto[];
|
||||||
updatedByAdminId?: number;
|
updatedByAdminId?: number;
|
||||||
createdAtMs?: number;
|
createdAtMs?: number;
|
||||||
@ -52,6 +55,7 @@ export interface LuckyGiftConfigDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface LuckyGiftConfigPayload {
|
export interface LuckyGiftConfigPayload {
|
||||||
|
pool_id: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
gift_price: number;
|
gift_price: number;
|
||||||
target_rtp_ppm: number;
|
target_rtp_ppm: number;
|
||||||
@ -83,10 +87,12 @@ export interface LuckyGiftConfigPayload {
|
|||||||
activity_budget: number;
|
activity_budget: number;
|
||||||
activity_daily_limit: number;
|
activity_daily_limit: number;
|
||||||
large_tier_enabled: boolean;
|
large_tier_enabled: boolean;
|
||||||
|
multiplier_ppms: number[];
|
||||||
tiers: Array<{
|
tiers: Array<{
|
||||||
pool: string;
|
pool: string;
|
||||||
tier_id: string;
|
tier_id: string;
|
||||||
reward_coins: number;
|
reward_coins: number;
|
||||||
|
multiplier_ppm: number;
|
||||||
weight: number;
|
weight: number;
|
||||||
high_water_only: boolean;
|
high_water_only: boolean;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -96,6 +102,7 @@ export interface LuckyGiftConfigPayload {
|
|||||||
export interface LuckyGiftDrawDto {
|
export interface LuckyGiftDrawDto {
|
||||||
drawId: string;
|
drawId: string;
|
||||||
commandId?: string;
|
commandId?: string;
|
||||||
|
poolId: string;
|
||||||
giftId: string;
|
giftId: string;
|
||||||
ruleVersion: number;
|
ruleVersion: number;
|
||||||
experiencePool: string;
|
experiencePool: string;
|
||||||
@ -119,13 +126,21 @@ type RawConfig = LuckyGiftConfigDto & Record<string, unknown>;
|
|||||||
type RawTier = LuckyGiftTierDto & Record<string, unknown>;
|
type RawTier = LuckyGiftTierDto & Record<string, unknown>;
|
||||||
type RawDraw = LuckyGiftDrawDto & Record<string, unknown>;
|
type RawDraw = LuckyGiftDrawDto & Record<string, unknown>;
|
||||||
|
|
||||||
export function getLuckyGiftConfig(): Promise<LuckyGiftConfigDto> {
|
export function getLuckyGiftConfig(poolId: string): Promise<LuckyGiftConfigDto> {
|
||||||
const endpoint = API_ENDPOINTS.getLuckyGiftConfig;
|
const endpoint = API_ENDPOINTS.getLuckyGiftConfig;
|
||||||
return apiRequest<RawConfig>(apiEndpointPath(API_OPERATIONS.getLuckyGiftConfig), {
|
return apiRequest<RawConfig>(apiEndpointPath(API_OPERATIONS.getLuckyGiftConfig), {
|
||||||
method: endpoint.method,
|
method: endpoint.method,
|
||||||
|
query: { pool_id: poolId },
|
||||||
}).then(normalizeConfig);
|
}).then(normalizeConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listLuckyGiftConfigs(): Promise<LuckyGiftConfigDto[]> {
|
||||||
|
const endpoint = API_ENDPOINTS.listLuckyGiftConfigs;
|
||||||
|
return apiRequest<RawConfig[]>(apiEndpointPath(API_OPERATIONS.listLuckyGiftConfigs), {
|
||||||
|
method: endpoint.method,
|
||||||
|
}).then((items) => (items || []).map(normalizeConfig));
|
||||||
|
}
|
||||||
|
|
||||||
export function updateLuckyGiftConfig(payload: LuckyGiftConfigPayload): Promise<LuckyGiftConfigDto> {
|
export function updateLuckyGiftConfig(payload: LuckyGiftConfigPayload): Promise<LuckyGiftConfigDto> {
|
||||||
const endpoint = API_ENDPOINTS.upsertLuckyGiftConfig;
|
const endpoint = API_ENDPOINTS.upsertLuckyGiftConfig;
|
||||||
return apiRequest<RawConfig, LuckyGiftConfigPayload>(
|
return apiRequest<RawConfig, LuckyGiftConfigPayload>(
|
||||||
@ -133,6 +148,7 @@ export function updateLuckyGiftConfig(payload: LuckyGiftConfigPayload): Promise<
|
|||||||
{
|
{
|
||||||
body: payload,
|
body: payload,
|
||||||
method: endpoint.method,
|
method: endpoint.method,
|
||||||
|
query: { pool_id: payload.pool_id },
|
||||||
},
|
},
|
||||||
).then(normalizeConfig);
|
).then(normalizeConfig);
|
||||||
}
|
}
|
||||||
@ -151,6 +167,7 @@ export function listLuckyGiftDraws(query: PageQuery = {}): Promise<ApiPage<Lucky
|
|||||||
function normalizeConfig(item: RawConfig): LuckyGiftConfigDto {
|
function normalizeConfig(item: RawConfig): LuckyGiftConfigDto {
|
||||||
return {
|
return {
|
||||||
appCode: stringValue(item.appCode ?? item.app_code),
|
appCode: stringValue(item.appCode ?? item.app_code),
|
||||||
|
poolId: stringValue(item.poolId ?? item.pool_id ?? item.gift_id),
|
||||||
enabled: booleanValue(item.enabled),
|
enabled: booleanValue(item.enabled),
|
||||||
ruleVersion: numberValue(item.ruleVersion ?? item.rule_version),
|
ruleVersion: numberValue(item.ruleVersion ?? item.rule_version),
|
||||||
giftPrice: numberValue(item.giftPrice ?? item.gift_price),
|
giftPrice: numberValue(item.giftPrice ?? item.gift_price),
|
||||||
@ -183,6 +200,7 @@ function normalizeConfig(item: RawConfig): LuckyGiftConfigDto {
|
|||||||
activityBudget: numberValue(item.activityBudget ?? item.activity_budget),
|
activityBudget: numberValue(item.activityBudget ?? item.activity_budget),
|
||||||
activityDailyLimit: numberValue(item.activityDailyLimit ?? item.activity_daily_limit),
|
activityDailyLimit: numberValue(item.activityDailyLimit ?? item.activity_daily_limit),
|
||||||
largeTierEnabled: booleanValue(item.largeTierEnabled ?? item.large_tier_enabled),
|
largeTierEnabled: booleanValue(item.largeTierEnabled ?? item.large_tier_enabled),
|
||||||
|
multiplierPPMs: arrayValue(item.multiplierPPMs ?? item.multiplier_ppms).map(numberValue),
|
||||||
tiers: arrayValue(item.tiers).map(normalizeTier),
|
tiers: arrayValue(item.tiers).map(normalizeTier),
|
||||||
updatedByAdminId: numberValue(item.updatedByAdminId ?? item.updated_by_admin_id),
|
updatedByAdminId: numberValue(item.updatedByAdminId ?? item.updated_by_admin_id),
|
||||||
createdAtMs: numberValue(item.createdAtMs ?? item.created_at_ms),
|
createdAtMs: numberValue(item.createdAtMs ?? item.created_at_ms),
|
||||||
@ -196,6 +214,7 @@ function normalizeTier(raw: unknown): LuckyGiftTierDto {
|
|||||||
pool: stringValue(item.pool),
|
pool: stringValue(item.pool),
|
||||||
tierId: stringValue(item.tierId ?? item.tier_id),
|
tierId: stringValue(item.tierId ?? item.tier_id),
|
||||||
rewardCoins: numberValue(item.rewardCoins ?? item.reward_coins),
|
rewardCoins: numberValue(item.rewardCoins ?? item.reward_coins),
|
||||||
|
multiplierPPM: numberValue(item.multiplierPPM ?? item.multiplier_ppm),
|
||||||
weight: numberValue(item.weight),
|
weight: numberValue(item.weight),
|
||||||
highWaterOnly: booleanValue(item.highWaterOnly ?? item.high_water_only),
|
highWaterOnly: booleanValue(item.highWaterOnly ?? item.high_water_only),
|
||||||
enabled: booleanValue(item.enabled),
|
enabled: booleanValue(item.enabled),
|
||||||
@ -206,6 +225,7 @@ function normalizeDraw(item: RawDraw): LuckyGiftDrawDto {
|
|||||||
return {
|
return {
|
||||||
drawId: stringValue(item.drawId ?? item.draw_id),
|
drawId: stringValue(item.drawId ?? item.draw_id),
|
||||||
commandId: stringValue(item.commandId ?? item.command_id),
|
commandId: stringValue(item.commandId ?? item.command_id),
|
||||||
|
poolId: stringValue(item.poolId ?? item.pool_id),
|
||||||
giftId: stringValue(item.giftId ?? item.gift_id),
|
giftId: stringValue(item.giftId ?? item.gift_id),
|
||||||
ruleVersion: numberValue(item.ruleVersion ?? item.rule_version),
|
ruleVersion: numberValue(item.ruleVersion ?? item.rule_version),
|
||||||
experiencePool: stringValue(item.experiencePool ?? item.experience_pool),
|
experiencePool: stringValue(item.experiencePool ?? item.experience_pool),
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
|
import AddOutlined from "@mui/icons-material/AddOutlined";
|
||||||
|
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
|
||||||
import SaveOutlined from "@mui/icons-material/SaveOutlined";
|
import SaveOutlined from "@mui/icons-material/SaveOutlined";
|
||||||
import Drawer from "@mui/material/Drawer";
|
import Drawer from "@mui/material/Drawer";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
|
import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx";
|
||||||
@ -10,7 +13,6 @@ import {
|
|||||||
controlStrengthOptions,
|
controlStrengthOptions,
|
||||||
jackpotStrategyOptions,
|
jackpotStrategyOptions,
|
||||||
noviceStrengthOptions,
|
noviceStrengthOptions,
|
||||||
payoutStyleOptions,
|
|
||||||
poolModeOptions,
|
poolModeOptions,
|
||||||
riskLevelOptions,
|
riskLevelOptions,
|
||||||
} from "@/features/lucky-gift/constants.js";
|
} from "@/features/lucky-gift/constants.js";
|
||||||
@ -18,11 +20,12 @@ import styles from "@/features/lucky-gift/lucky-gift.module.css";
|
|||||||
|
|
||||||
const helpText = {
|
const helpText = {
|
||||||
enabled: "控制全站幸运礼物抽奖是否生效,关闭后用户仍可送礼但不触发幸运抽奖。",
|
enabled: "控制全站幸运礼物抽奖是否生效,关闭后用户仍可送礼但不触发幸运抽奖。",
|
||||||
|
poolId: "奖池 ID 是请求接口传入的隔离键,不同奖池的 RTP、预算、风控和用户阶段互不影响。",
|
||||||
targetRtpPercent: "基础返奖成本比例,95 表示长期每消耗 100 金币,基础奖池目标返还 95 金币。",
|
targetRtpPercent: "基础返奖成本比例,95 表示长期每消耗 100 金币,基础奖池目标返还 95 金币。",
|
||||||
controlStrength: "控制 RTP 收敛窗口大小,越精准越快贴近目标,越自然短期波动越大。",
|
controlStrength: "控制 RTP 收敛窗口大小,越精准越快贴近目标,越自然短期波动越大。",
|
||||||
poolMode: "决定平台池、房间池、整体幸运礼物池分别承担多少基础返奖责任。",
|
poolMode: "决定平台池、房间池、整体幸运礼物池分别承担多少基础返奖责任。",
|
||||||
noviceStrength: "控制新手池持续抽数,越强表示用户前期更长时间处于低波动体验。",
|
noviceStrength: "控制新手池持续抽数,越强表示用户前期更长时间处于低波动体验。",
|
||||||
payoutStyle: "控制小奖和大奖的权重分布,稳定偏小奖,刺激偏高倍爆点。",
|
multiplierValues: "配置用户可能命中的倍率,例如 0、0.5、1、2、10、100;每个倍率的概率由后端按 RTP 窗口自动生成。",
|
||||||
jackpotStrategy: "控制高倍奖档开放强度,保守需要更高水位,关闭则不出高倍大奖。",
|
jackpotStrategy: "控制高倍奖档开放强度,保守需要更高水位,关闭则不出高倍大奖。",
|
||||||
riskLevel: "控制单用户、设备、房间和主播关联的奖励上限,越严格越防套利。",
|
riskLevel: "控制单用户、设备、房间和主播关联的奖励上限,越严格越防套利。",
|
||||||
atmosphereLevel: "控制房间气氛池入账强度,用于房间共享爆点,不计入基础 RTP。",
|
atmosphereLevel: "控制房间气氛池入账强度,用于房间共享爆点,不计入基础 RTP。",
|
||||||
@ -50,6 +53,12 @@ export function LuckyGiftConfigDrawer({
|
|||||||
<section className="form-drawer__section">
|
<section className="form-drawer__section">
|
||||||
<div className="form-drawer__section-title">基础控制</div>
|
<div className="form-drawer__section-title">基础控制</div>
|
||||||
<div className={styles.configGrid}>
|
<div className={styles.configGrid}>
|
||||||
|
<TextField
|
||||||
|
required
|
||||||
|
disabled
|
||||||
|
label={<FieldLabel help={helpText.poolId} text="奖池 ID" />}
|
||||||
|
value={form.poolId}
|
||||||
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -97,13 +106,11 @@ export function LuckyGiftConfigDrawer({
|
|||||||
value={form.noviceStrength}
|
value={form.noviceStrength}
|
||||||
onChange={(value) => updateForm(setForm, "noviceStrength", value)}
|
onChange={(value) => updateForm(setForm, "noviceStrength", value)}
|
||||||
/>
|
/>
|
||||||
<SelectField
|
<MultiplierField
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
help={helpText.payoutStyle}
|
help={helpText.multiplierValues}
|
||||||
label="中奖体感"
|
setForm={setForm}
|
||||||
options={payoutStyleOptions}
|
values={form.multiplierValues || []}
|
||||||
value={form.payoutStyle}
|
|
||||||
onChange={(value) => updateForm(setForm, "payoutStyle", value)}
|
|
||||||
/>
|
/>
|
||||||
<SelectField
|
<SelectField
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -181,6 +188,47 @@ export function LuckyGiftConfigDrawer({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function MultiplierField({ disabled, help, setForm, values }) {
|
||||||
|
const list = values.length > 0 ? values : ["0"];
|
||||||
|
return (
|
||||||
|
<div className={[styles.multiplierBlock, styles.configWide].join(" ")}>
|
||||||
|
<div className={styles.multiplierHeader}>
|
||||||
|
<FieldLabel help={help} text="倍率配置" />
|
||||||
|
<Button
|
||||||
|
disabled={disabled}
|
||||||
|
startIcon={<AddOutlined fontSize="small" />}
|
||||||
|
type="button"
|
||||||
|
onClick={() => addMultiplier(setForm)}
|
||||||
|
>
|
||||||
|
添加倍率
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className={styles.multiplierList}>
|
||||||
|
{list.map((value, index) => (
|
||||||
|
<div key={index} className={styles.multiplierRow}>
|
||||||
|
<TextField
|
||||||
|
disabled={disabled}
|
||||||
|
inputProps={{ min: 0, step: 0.01 }}
|
||||||
|
label={`倍率 ${index + 1}`}
|
||||||
|
type="number"
|
||||||
|
value={value}
|
||||||
|
onChange={(event) => updateMultiplier(setForm, index, event.target.value)}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
aria-label="删除倍率"
|
||||||
|
disabled={disabled || list.length <= 1}
|
||||||
|
size="small"
|
||||||
|
onClick={() => removeMultiplier(setForm, index)}
|
||||||
|
>
|
||||||
|
<DeleteOutlineOutlined fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function SelectField({ disabled, help, label, onChange, options, value }) {
|
function SelectField({ disabled, help, label, onChange, options, value }) {
|
||||||
return (
|
return (
|
||||||
<TextField
|
<TextField
|
||||||
@ -218,3 +266,25 @@ function SwitchField({ checked, className, disabled, help, label, onChange }) {
|
|||||||
function updateForm(setForm, key, value) {
|
function updateForm(setForm, key, value) {
|
||||||
setForm((current) => ({ ...current, [key]: value }));
|
setForm((current) => ({ ...current, [key]: value }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateMultiplier(setForm, index, value) {
|
||||||
|
setForm((current) => {
|
||||||
|
const nextValues = [...(current.multiplierValues || [])];
|
||||||
|
nextValues[index] = value;
|
||||||
|
return { ...current, multiplierValues: nextValues };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMultiplier(setForm) {
|
||||||
|
setForm((current) => ({
|
||||||
|
...current,
|
||||||
|
multiplierValues: [...(current.multiplierValues || []), ""],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeMultiplier(setForm, index) {
|
||||||
|
setForm((current) => ({
|
||||||
|
...current,
|
||||||
|
multiplierValues: (current.multiplierValues || []).filter((_, itemIndex) => itemIndex !== index),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
|
import AddOutlined from "@mui/icons-material/AddOutlined";
|
||||||
import EditOutlined from "@mui/icons-material/EditOutlined";
|
import EditOutlined from "@mui/icons-material/EditOutlined";
|
||||||
import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
|
import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
import { AdminActionIconButton } from "@/shared/ui/AdminListLayout.jsx";
|
import { AdminActionIconButton } from "@/shared/ui/AdminListLayout.jsx";
|
||||||
import { Button } from "@/shared/ui/Button.jsx";
|
import { Button } from "@/shared/ui/Button.jsx";
|
||||||
import { formatMillis } from "@/shared/utils/time.js";
|
import { formatMillis } from "@/shared/utils/time.js";
|
||||||
@ -7,16 +10,24 @@ import {
|
|||||||
atmosphereLevelOptions,
|
atmosphereLevelOptions,
|
||||||
controlStrengthOptions,
|
controlStrengthOptions,
|
||||||
jackpotStrategyOptions,
|
jackpotStrategyOptions,
|
||||||
luckyGiftConfigScopeLabel,
|
|
||||||
noviceStrengthOptions,
|
noviceStrengthOptions,
|
||||||
optionLabel,
|
optionLabel,
|
||||||
payoutStyleOptions,
|
|
||||||
poolModeOptions,
|
poolModeOptions,
|
||||||
} from "@/features/lucky-gift/constants.js";
|
} from "@/features/lucky-gift/constants.js";
|
||||||
import { formFromConfig } from "@/features/lucky-gift/configModel.js";
|
import { formFromConfig } from "@/features/lucky-gift/configModel.js";
|
||||||
import styles from "@/features/lucky-gift/lucky-gift.module.css";
|
import styles from "@/features/lucky-gift/lucky-gift.module.css";
|
||||||
|
|
||||||
export function LuckyGiftConfigSummary({ canUpdate, config, configLoading, onEdit, onRefresh }) {
|
export function LuckyGiftConfigSummary({
|
||||||
|
canUpdate,
|
||||||
|
config,
|
||||||
|
configLoading,
|
||||||
|
onAddPool,
|
||||||
|
onEdit,
|
||||||
|
onPoolChange,
|
||||||
|
onRefresh,
|
||||||
|
poolId,
|
||||||
|
poolOptions,
|
||||||
|
}) {
|
||||||
const form = formFromConfig(config);
|
const form = formFromConfig(config);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -25,7 +36,7 @@ export function LuckyGiftConfigSummary({ canUpdate, config, configLoading, onEdi
|
|||||||
<div className={styles.summaryContent}>
|
<div className={styles.summaryContent}>
|
||||||
<div className={styles.summaryHeader}>
|
<div className={styles.summaryHeader}>
|
||||||
<span className={styles.title}>幸运礼物配置</span>
|
<span className={styles.title}>幸运礼物配置</span>
|
||||||
<span className={styles.meta}>{luckyGiftConfigScopeLabel}</span>
|
<span className={styles.meta}>奖池 {poolId}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.summaryItems}>
|
<div className={styles.summaryItems}>
|
||||||
<ConfigStatus config={config} loading={configLoading} />
|
<ConfigStatus config={config} loading={configLoading} />
|
||||||
@ -37,7 +48,7 @@ export function LuckyGiftConfigSummary({ canUpdate, config, configLoading, onEdi
|
|||||||
<SummaryItem label="新手保护">
|
<SummaryItem label="新手保护">
|
||||||
{optionLabel(noviceStrengthOptions, form.noviceStrength)}
|
{optionLabel(noviceStrengthOptions, form.noviceStrength)}
|
||||||
</SummaryItem>
|
</SummaryItem>
|
||||||
<SummaryItem label="中奖体感">{optionLabel(payoutStyleOptions, form.payoutStyle)}</SummaryItem>
|
<SummaryItem label="倍率">{formatMultipliers(form.multiplierValues)}</SummaryItem>
|
||||||
<SummaryItem label="大奖策略">
|
<SummaryItem label="大奖策略">
|
||||||
{optionLabel(jackpotStrategyOptions, form.jackpotStrategy)}
|
{optionLabel(jackpotStrategyOptions, form.jackpotStrategy)}
|
||||||
</SummaryItem>
|
</SummaryItem>
|
||||||
@ -51,6 +62,23 @@ export function LuckyGiftConfigSummary({ canUpdate, config, configLoading, onEdi
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.summaryActions}>
|
<div className={styles.summaryActions}>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
className={styles.poolSelector}
|
||||||
|
disabled={configLoading}
|
||||||
|
label="奖池 ID"
|
||||||
|
value={poolId}
|
||||||
|
onChange={(event) => onPoolChange(event.target.value)}
|
||||||
|
>
|
||||||
|
{(poolOptions || [poolId]).map((option) => (
|
||||||
|
<MenuItem key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
<AdminActionIconButton disabled={configLoading || !canUpdate} label="添加奖池" type="button" onClick={onAddPool}>
|
||||||
|
<AddOutlined fontSize="small" />
|
||||||
|
</AdminActionIconButton>
|
||||||
<AdminActionIconButton disabled={configLoading} label="刷新配置" type="button" onClick={onRefresh}>
|
<AdminActionIconButton disabled={configLoading} label="刷新配置" type="button" onClick={onRefresh}>
|
||||||
<RefreshOutlined fontSize="small" />
|
<RefreshOutlined fontSize="small" />
|
||||||
</AdminActionIconButton>
|
</AdminActionIconButton>
|
||||||
@ -68,6 +96,13 @@ export function LuckyGiftConfigSummary({ canUpdate, config, configLoading, onEdi
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatMultipliers(values) {
|
||||||
|
if (!Array.isArray(values) || values.length === 0) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
return values.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
function SummaryItem({ children, label }) {
|
function SummaryItem({ children, label }) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.summaryItem}>
|
<div className={styles.summaryItem}>
|
||||||
|
|||||||
@ -21,8 +21,9 @@ export function emptyLuckyGiftForm() {
|
|||||||
controlStrength: "standard",
|
controlStrength: "standard",
|
||||||
enabled: false,
|
enabled: false,
|
||||||
jackpotStrategy: "standard",
|
jackpotStrategy: "standard",
|
||||||
|
multiplierValues: ["0", "0.2", "0.5", "1", "2", "5", "10", "20", "50", "100", "500"],
|
||||||
noviceStrength: "standard",
|
noviceStrength: "standard",
|
||||||
payoutStyle: "balanced",
|
poolId: "default",
|
||||||
poolMode: "standard",
|
poolMode: "standard",
|
||||||
riskLevel: "standard",
|
riskLevel: "standard",
|
||||||
targetRtpPercent: "95",
|
targetRtpPercent: "95",
|
||||||
@ -43,8 +44,9 @@ export function formFromConfig(config) {
|
|||||||
controlStrength: inferControlStrength(config.globalWindowDraws, config.giftWindowDraws),
|
controlStrength: inferControlStrength(config.globalWindowDraws, config.giftWindowDraws),
|
||||||
enabled: Boolean(config.enabled),
|
enabled: Boolean(config.enabled),
|
||||||
jackpotStrategy: inferJackpotStrategy(config.largeTierEnabled, config.highWaterPoolMultiple),
|
jackpotStrategy: inferJackpotStrategy(config.largeTierEnabled, config.highWaterPoolMultiple),
|
||||||
|
multiplierValues: formatMultiplierValues(config.multiplierPPMs, config.tiers || []),
|
||||||
noviceStrength: inferNoviceStrength(config.noviceDrawLimit, config.intermediateDrawLimit),
|
noviceStrength: inferNoviceStrength(config.noviceDrawLimit, config.intermediateDrawLimit),
|
||||||
payoutStyle: inferPayoutStyle(config.tiers || []),
|
poolId: config.poolId || "default",
|
||||||
poolMode: inferPoolMode(config),
|
poolMode: inferPoolMode(config),
|
||||||
riskLevel: inferRiskLevel(config),
|
riskLevel: inferRiskLevel(config),
|
||||||
targetRtpPercent: formatPercentInput(config.targetRTPPPM || defaultTargetRTPPPM),
|
targetRtpPercent: formatPercentInput(config.targetRTPPPM || defaultTargetRTPPPM),
|
||||||
@ -61,6 +63,9 @@ export function payloadFromLuckyGiftForm(form, currentConfig) {
|
|||||||
const atmosphere = atmosphereLevelConfig[form.atmosphereLevel] || atmosphereLevelConfig.medium;
|
const atmosphere = atmosphereLevelConfig[form.atmosphereLevel] || atmosphereLevelConfig.medium;
|
||||||
const risk = riskCapsForLevel(form.riskLevel, giftPrice, targetRTPPPM);
|
const risk = riskCapsForLevel(form.riskLevel, giftPrice, targetRTPPPM);
|
||||||
const base = currentConfig || {};
|
const base = currentConfig || {};
|
||||||
|
const multiplierPPMs = parseMultiplierValues(form.multiplierValues);
|
||||||
|
const maxMultiplierPPM = Math.max(...multiplierPPMs);
|
||||||
|
const highMultiplier = positiveNumber(base.highMultiplier, 100);
|
||||||
const activityBudget = form.activitySubsidyEnabled ? nonNegativeNumber(form.activityBudget) : 0;
|
const activityBudget = form.activitySubsidyEnabled ? nonNegativeNumber(form.activityBudget) : 0;
|
||||||
const activityDailyLimit = form.activitySubsidyEnabled ? nonNegativeNumber(form.activityDailyLimit) : 0;
|
const activityDailyLimit = form.activitySubsidyEnabled ? nonNegativeNumber(form.activityDailyLimit) : 0;
|
||||||
|
|
||||||
@ -75,7 +80,7 @@ export function payloadFromLuckyGiftForm(form, currentConfig) {
|
|||||||
gift_reserve: positiveNumber(base.giftReserve, 1_000_000),
|
gift_reserve: positiveNumber(base.giftReserve, 1_000_000),
|
||||||
global_window_draws: control.globalWindowDraws,
|
global_window_draws: control.globalWindowDraws,
|
||||||
gift_window_draws: control.giftWindowDraws,
|
gift_window_draws: control.giftWindowDraws,
|
||||||
high_multiplier: positiveNumber(base.highMultiplier, 100),
|
high_multiplier: highMultiplier,
|
||||||
high_water_pool_multiple: jackpot.highWaterPoolMultiple,
|
high_water_pool_multiple: jackpot.highWaterPoolMultiple,
|
||||||
initial_gift_pool: positiveNumber(base.initialGiftPool, expectedPayout(giftPrice, targetRTPPPM, control.giftWindowDraws) / 2),
|
initial_gift_pool: positiveNumber(base.initialGiftPool, expectedPayout(giftPrice, targetRTPPPM, control.giftWindowDraws) / 2),
|
||||||
initial_platform_pool: positiveNumber(
|
initial_platform_pool: positiveNumber(
|
||||||
@ -85,10 +90,12 @@ export function payloadFromLuckyGiftForm(form, currentConfig) {
|
|||||||
initial_room_pool: positiveNumber(base.initialRoomPool, giftPrice * 10_000),
|
initial_room_pool: positiveNumber(base.initialRoomPool, giftPrice * 10_000),
|
||||||
intermediate_draw_limit: novice.intermediateDrawLimit,
|
intermediate_draw_limit: novice.intermediateDrawLimit,
|
||||||
large_tier_enabled: jackpot.largeTierEnabled,
|
large_tier_enabled: jackpot.largeTierEnabled,
|
||||||
max_single_payout: risk.maxSinglePayout,
|
max_single_payout: Math.max(risk.maxSinglePayout, Math.ceil((giftPrice * maxMultiplierPPM) / ppmScale)),
|
||||||
|
multiplier_ppms: multiplierPPMs,
|
||||||
novice_draw_limit: novice.noviceDrawLimit,
|
novice_draw_limit: novice.noviceDrawLimit,
|
||||||
platform_pool_weight_ppm: poolMode.platformPoolWeightPPM,
|
platform_pool_weight_ppm: poolMode.platformPoolWeightPPM,
|
||||||
platform_reserve: positiveNumber(base.platformReserve, 1_000_000),
|
platform_reserve: positiveNumber(base.platformReserve, 1_000_000),
|
||||||
|
pool_id: String(form.poolId || base.poolId || "default").trim() || "default",
|
||||||
pool_rate_ppm: Math.max(targetRTPPPM, positiveNumber(base.poolRatePPM, targetRTPPPM)),
|
pool_rate_ppm: Math.max(targetRTPPPM, positiveNumber(base.poolRatePPM, targetRTPPPM)),
|
||||||
room_atmosphere_initial: positiveNumber(base.roomAtmosphereInitial, giftPrice * 200),
|
room_atmosphere_initial: positiveNumber(base.roomAtmosphereInitial, giftPrice * 200),
|
||||||
room_atmosphere_rate_ppm: atmosphere.roomAtmosphereRatePPM,
|
room_atmosphere_rate_ppm: atmosphere.roomAtmosphereRatePPM,
|
||||||
@ -97,7 +104,7 @@ export function payloadFromLuckyGiftForm(form, currentConfig) {
|
|||||||
room_pool_weight_ppm: poolMode.roomPoolWeightPPM,
|
room_pool_weight_ppm: poolMode.roomPoolWeightPPM,
|
||||||
room_reserve: positiveNumber(base.roomReserve, giftPrice * 200),
|
room_reserve: positiveNumber(base.roomReserve, giftPrice * 200),
|
||||||
target_rtp_ppm: targetRTPPPM,
|
target_rtp_ppm: targetRTPPPM,
|
||||||
tiers: buildTiers(giftPrice, form.payoutStyle),
|
tiers: buildTiers(giftPrice, multiplierPPMs, highMultiplier),
|
||||||
user_daily_payout_cap: risk.userDailyPayoutCap,
|
user_daily_payout_cap: risk.userDailyPayoutCap,
|
||||||
user_hourly_payout_cap: risk.userHourlyPayoutCap,
|
user_hourly_payout_cap: risk.userHourlyPayoutCap,
|
||||||
};
|
};
|
||||||
@ -121,50 +128,69 @@ export function previewForForm(form) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTiers(cost, style) {
|
function buildTiers(cost, multiplierPPMs, highMultiplier) {
|
||||||
const scale = style === "stable" ? 0 : style === "exciting" ? 2 : 1;
|
return ["novice", "intermediate", "advanced"].flatMap((pool) =>
|
||||||
const noviceHigh = scale === 0 ? 200 : scale === 2 ? 900 : 500;
|
multiplierPPMs.map((multiplierPPM) =>
|
||||||
const intermediateHigh = scale === 0 ? 10_000 : scale === 2 ? 36_000 : 25_000;
|
tier(
|
||||||
const advancedHigh = scale === 0 ? 15_000 : scale === 2 ? 58_000 : 40_000;
|
pool,
|
||||||
|
`${pool}_${multiplierLabel(multiplierPPM)}`,
|
||||||
return [
|
Math.floor((cost * multiplierPPM) / ppmScale),
|
||||||
tier("novice", "none", 0, 720_000),
|
multiplierPPM,
|
||||||
tier("novice", "novice_feedback_0_2x", Math.floor(cost / 5), 90_000),
|
highMultiplier > 0 && multiplierPPM >= highMultiplier * ppmScale,
|
||||||
tier("novice", "novice_rebate_0_5x", Math.floor(cost / 2), 70_000),
|
),
|
||||||
tier("novice", "novice_rebate_1x", cost, 70_000),
|
),
|
||||||
tier("novice", "novice_small_2x", cost * 2, 35_000),
|
);
|
||||||
tier("novice", "novice_small_5x", cost * 5, 12_000),
|
|
||||||
tier("novice", "novice_medium_10x", cost * 10, 2_800),
|
|
||||||
tier("novice", "novice_cap_20x", cost * 20, noviceHigh, true),
|
|
||||||
|
|
||||||
tier("intermediate", "none", 0, 830_000),
|
|
||||||
tier("intermediate", "inter_rebate_0_5x", Math.floor(cost / 2), 25_000),
|
|
||||||
tier("intermediate", "inter_rebate_1x", cost, 40_000),
|
|
||||||
tier("intermediate", "inter_small_2x", cost * 2, 45_000),
|
|
||||||
tier("intermediate", "inter_medium_5x", cost * 5, 35_000),
|
|
||||||
tier("intermediate", "inter_large_20x", cost * 20, Math.floor(intermediateHigh * 0.8), true),
|
|
||||||
tier("intermediate", "inter_large_50x", cost * 50, Math.floor(intermediateHigh * 0.2), true),
|
|
||||||
|
|
||||||
tier("advanced", "none", 0, 900_000),
|
|
||||||
tier("advanced", "adv_small_2x", cost * 2, 30_000),
|
|
||||||
tier("advanced", "adv_medium_5x", cost * 5, 30_000),
|
|
||||||
tier("advanced", "adv_large_20x", cost * 20, Math.floor(advancedHigh * 0.75), true),
|
|
||||||
tier("advanced", "adv_large_100x", cost * 100, Math.floor(advancedHigh * 0.225), true),
|
|
||||||
tier("advanced", "adv_jackpot_500x", cost * 500, Math.max(100, Math.floor(advancedHigh * 0.025)), true),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function tier(pool, tierId, rewardCoins, weight, highWaterOnly = false) {
|
function tier(pool, tierId, rewardCoins, multiplierPPM, highWaterOnly = false) {
|
||||||
return {
|
return {
|
||||||
enabled: weight > 0,
|
enabled: true,
|
||||||
high_water_only: Boolean(highWaterOnly),
|
high_water_only: Boolean(highWaterOnly),
|
||||||
|
multiplier_ppm: multiplierPPM,
|
||||||
pool,
|
pool,
|
||||||
reward_coins: rewardCoins,
|
reward_coins: rewardCoins,
|
||||||
tier_id: tierId,
|
tier_id: tierId,
|
||||||
weight,
|
weight: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseMultiplierList(value) {
|
||||||
|
const numbers = String(value || "")
|
||||||
|
.split(/[,,\s]+/)
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((item) => Number(item.replace(/x$/i, "")))
|
||||||
|
.filter((item) => Number.isFinite(item) && item >= 0)
|
||||||
|
.map((item) => Math.round(item * ppmScale));
|
||||||
|
const unique = Array.from(new Set([0, ...numbers])).sort((a, b) => a - b);
|
||||||
|
return unique.length > 1 ? unique : [0, 1_000_000];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseMultiplierValues(values) {
|
||||||
|
return parseMultiplierList((values || []).join(","));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMultiplierValues(multiplierPPMs, tiers) {
|
||||||
|
const values = Array.isArray(multiplierPPMs) && multiplierPPMs.length > 0 ? multiplierPPMs : multipliersFromTiers(tiers);
|
||||||
|
return values.map(multiplierText);
|
||||||
|
}
|
||||||
|
|
||||||
|
function multipliersFromTiers(tiers) {
|
||||||
|
const values = (tiers || [])
|
||||||
|
.map((tierItem) => Number(tierItem.multiplierPPM || 0))
|
||||||
|
.filter((item) => item >= 0);
|
||||||
|
return values.length > 0 ? Array.from(new Set(values)).sort((a, b) => a - b) : parseMultiplierList("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function multiplierLabel(multiplierPPM) {
|
||||||
|
return multiplierPPM === 0 ? "none" : `${multiplierText(multiplierPPM).replace(".", "_")}x`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function multiplierText(multiplierPPM) {
|
||||||
|
const value = multiplierPPM / ppmScale;
|
||||||
|
return Number.isInteger(value) ? String(value) : value.toFixed(6).replace(/\.?0+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
function riskCapsForLevel(level, giftPrice, targetRTPPPM) {
|
function riskCapsForLevel(level, giftPrice, targetRTPPPM) {
|
||||||
const expectedHour = expectedPayout(giftPrice, targetRTPPPM, defaultDrawsPerHour);
|
const expectedHour = expectedPayout(giftPrice, targetRTPPPM, defaultDrawsPerHour);
|
||||||
const expectedDay = expectedHour * 24;
|
const expectedDay = expectedHour * 24;
|
||||||
@ -223,19 +249,6 @@ function inferAtmosphereLevel(ratePPM) {
|
|||||||
return matchConfig(atmosphereLevelConfig, (item) => item.roomAtmosphereRatePPM === ratePPM);
|
return matchConfig(atmosphereLevelConfig, (item) => item.roomAtmosphereRatePPM === ratePPM);
|
||||||
}
|
}
|
||||||
|
|
||||||
function inferPayoutStyle(tiers) {
|
|
||||||
const jackpotWeight = tiers
|
|
||||||
.filter((tierItem) => tierItem.pool === "advanced" && tierItem.highWaterOnly)
|
|
||||||
.reduce((sum, tierItem) => sum + Number(tierItem.weight || 0), 0);
|
|
||||||
if (jackpotWeight <= 20_000) {
|
|
||||||
return "stable";
|
|
||||||
}
|
|
||||||
if (jackpotWeight >= 50_000) {
|
|
||||||
return "exciting";
|
|
||||||
}
|
|
||||||
return "balanced";
|
|
||||||
}
|
|
||||||
|
|
||||||
function inferRiskLevel() {
|
function inferRiskLevel() {
|
||||||
return "standard";
|
return "standard";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
import { emptyLuckyGiftForm, payloadFromLuckyGiftForm } from "@/features/lucky-gift/configModel.js";
|
import { emptyLuckyGiftForm, parseMultiplierList, payloadFromLuckyGiftForm } from "@/features/lucky-gift/configModel.js";
|
||||||
|
|
||||||
describe("lucky gift config payload", () => {
|
describe("lucky gift config payload", () => {
|
||||||
test("submits both RTP control windows for backend validation", () => {
|
test("submits both RTP control windows for backend validation", () => {
|
||||||
@ -7,5 +7,12 @@ describe("lucky gift config payload", () => {
|
|||||||
|
|
||||||
expect(payload.global_window_draws).toBeGreaterThan(0);
|
expect(payload.global_window_draws).toBeGreaterThan(0);
|
||||||
expect(payload.gift_window_draws).toBeGreaterThan(0);
|
expect(payload.gift_window_draws).toBeGreaterThan(0);
|
||||||
|
expect(payload.pool_id).toBe("default");
|
||||||
|
expect(payload.multiplier_ppms).toContain(500_000_000);
|
||||||
|
expect(payload.tiers.every((tier) => tier.weight === 0)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses custom multipliers without configured probabilities", () => {
|
||||||
|
expect(parseMultiplierList("0, 0.5x, 1, 2")).toEqual([0, 500_000, 1_000_000, 2_000_000]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
export const luckyGiftConfigScopeLabel = "全局配置";
|
|
||||||
|
|
||||||
export const controlStrengthOptions = [
|
export const controlStrengthOptions = [
|
||||||
["precise", "精准"],
|
["precise", "精准"],
|
||||||
["standard", "标准"],
|
["standard", "标准"],
|
||||||
@ -19,12 +17,6 @@ export const noviceStrengthOptions = [
|
|||||||
["strong", "强"],
|
["strong", "强"],
|
||||||
];
|
];
|
||||||
|
|
||||||
export const payoutStyleOptions = [
|
|
||||||
["stable", "稳定小奖"],
|
|
||||||
["balanced", "均衡"],
|
|
||||||
["exciting", "刺激大奖"],
|
|
||||||
];
|
|
||||||
|
|
||||||
export const jackpotStrategyOptions = [
|
export const jackpotStrategyOptions = [
|
||||||
["off", "关闭"],
|
["off", "关闭"],
|
||||||
["conservative", "保守"],
|
["conservative", "保守"],
|
||||||
|
|||||||
@ -2,22 +2,27 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|||||||
import { parseForm } from "@/shared/forms/validation";
|
import { parseForm } from "@/shared/forms/validation";
|
||||||
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
import { usePaginatedQuery } from "@/shared/hooks/usePaginatedQuery.js";
|
||||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||||
import { getLuckyGiftConfig, listLuckyGiftDraws, updateLuckyGiftConfig } from "@/features/lucky-gift/api";
|
import { getLuckyGiftConfig, listLuckyGiftConfigs, listLuckyGiftDraws, updateLuckyGiftConfig } from "@/features/lucky-gift/api";
|
||||||
import { emptyLuckyGiftForm, formFromConfig, payloadFromLuckyGiftForm } from "@/features/lucky-gift/configModel.js";
|
import { emptyLuckyGiftForm, formFromConfig, payloadFromLuckyGiftForm } from "@/features/lucky-gift/configModel.js";
|
||||||
import { useLuckyGiftAbilities } from "@/features/lucky-gift/permissions.js";
|
import { useLuckyGiftAbilities } from "@/features/lucky-gift/permissions.js";
|
||||||
import { luckyGiftConfigFormSchema } from "@/features/lucky-gift/schema";
|
import { luckyGiftConfigFormSchema } from "@/features/lucky-gift/schema";
|
||||||
|
|
||||||
const pageSize = 10;
|
const pageSize = 10;
|
||||||
const emptyDraws = { items: [], page: 1, pageSize, total: 0 };
|
const emptyDraws = { items: [], page: 1, pageSize, total: 0 };
|
||||||
|
const defaultPoolId = "default";
|
||||||
|
|
||||||
export function useLuckyGiftPage() {
|
export function useLuckyGiftPage() {
|
||||||
const abilities = useLuckyGiftAbilities();
|
const abilities = useLuckyGiftAbilities();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const [config, setConfig] = useState(null);
|
const [config, setConfig] = useState(null);
|
||||||
|
const [configs, setConfigs] = useState([]);
|
||||||
const [configDrawerOpen, setConfigDrawerOpen] = useState(false);
|
const [configDrawerOpen, setConfigDrawerOpen] = useState(false);
|
||||||
|
const [addPoolOpen, setAddPoolOpen] = useState(false);
|
||||||
|
const [addPoolId, setAddPoolId] = useState("");
|
||||||
const [form, setForm] = useState(emptyLuckyGiftForm());
|
const [form, setForm] = useState(emptyLuckyGiftForm());
|
||||||
const [configLoading, setConfigLoading] = useState(false);
|
const [configLoading, setConfigLoading] = useState(false);
|
||||||
const [configSaving, setConfigSaving] = useState(false);
|
const [configSaving, setConfigSaving] = useState(false);
|
||||||
|
const [poolId, setPoolId] = useState(defaultPoolId);
|
||||||
const [giftId, setGiftId] = useState("");
|
const [giftId, setGiftId] = useState("");
|
||||||
const [userId, setUserId] = useState("");
|
const [userId, setUserId] = useState("");
|
||||||
const [roomId, setRoomId] = useState("");
|
const [roomId, setRoomId] = useState("");
|
||||||
@ -25,8 +30,8 @@ export function useLuckyGiftPage() {
|
|||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
const drawFilters = useMemo(
|
const drawFilters = useMemo(
|
||||||
() => ({ gift_id: giftId, room_id: roomId, status, user_id: userId }),
|
() => ({ gift_id: giftId, pool_id: poolId, room_id: roomId, status, user_id: userId }),
|
||||||
[giftId, roomId, status, userId],
|
[giftId, poolId, roomId, status, userId],
|
||||||
);
|
);
|
||||||
const {
|
const {
|
||||||
data: draws = emptyDraws,
|
data: draws = emptyDraws,
|
||||||
@ -41,11 +46,29 @@ export function useLuckyGiftPage() {
|
|||||||
pageSize,
|
pageSize,
|
||||||
queryKey: ["lucky-gift-draws", drawFilters, page],
|
queryKey: ["lucky-gift-draws", drawFilters, page],
|
||||||
});
|
});
|
||||||
|
const poolOptions = useMemo(() => {
|
||||||
|
const ids = new Set([poolId]);
|
||||||
|
for (const item of configs) {
|
||||||
|
if (item.poolId) {
|
||||||
|
ids.add(item.poolId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(ids);
|
||||||
|
}, [configs, poolId]);
|
||||||
|
|
||||||
|
const reloadConfigs = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const nextConfigs = await listLuckyGiftConfigs();
|
||||||
|
setConfigs(nextConfigs);
|
||||||
|
} catch (err) {
|
||||||
|
showToast(err.message || "加载幸运礼物奖池失败", "error");
|
||||||
|
}
|
||||||
|
}, [showToast]);
|
||||||
|
|
||||||
const reloadConfig = useCallback(async () => {
|
const reloadConfig = useCallback(async () => {
|
||||||
setConfigLoading(true);
|
setConfigLoading(true);
|
||||||
try {
|
try {
|
||||||
const nextConfig = await getLuckyGiftConfig();
|
const nextConfig = await getLuckyGiftConfig(poolId);
|
||||||
setConfig(nextConfig);
|
setConfig(nextConfig);
|
||||||
setForm(formFromConfig(nextConfig));
|
setForm(formFromConfig(nextConfig));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -53,21 +76,25 @@ export function useLuckyGiftPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setConfigLoading(false);
|
setConfigLoading(false);
|
||||||
}
|
}
|
||||||
}, [showToast]);
|
}, [poolId, showToast]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void reloadConfig();
|
void reloadConfig();
|
||||||
}, [reloadConfig]);
|
}, [reloadConfig]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void reloadConfigs();
|
||||||
|
}, [reloadConfigs]);
|
||||||
|
|
||||||
const openConfigDrawer = useCallback(() => {
|
const openConfigDrawer = useCallback(() => {
|
||||||
setForm(config ? formFromConfig(config) : emptyLuckyGiftForm());
|
setForm(config ? formFromConfig(config) : { ...emptyLuckyGiftForm(), poolId });
|
||||||
setConfigDrawerOpen(true);
|
setConfigDrawerOpen(true);
|
||||||
}, [config]);
|
}, [config, poolId]);
|
||||||
|
|
||||||
const closeConfigDrawer = useCallback(() => {
|
const closeConfigDrawer = useCallback(() => {
|
||||||
setConfigDrawerOpen(false);
|
setConfigDrawerOpen(false);
|
||||||
setForm(config ? formFromConfig(config) : emptyLuckyGiftForm());
|
setForm(config ? formFromConfig(config) : { ...emptyLuckyGiftForm(), poolId });
|
||||||
}, [config]);
|
}, [config, poolId]);
|
||||||
|
|
||||||
const submitConfig = async (event) => {
|
const submitConfig = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -79,7 +106,9 @@ export function useLuckyGiftPage() {
|
|||||||
const parsed = parseForm(luckyGiftConfigFormSchema, form);
|
const parsed = parseForm(luckyGiftConfigFormSchema, form);
|
||||||
const saved = await updateLuckyGiftConfig(payloadFromLuckyGiftForm(parsed, config));
|
const saved = await updateLuckyGiftConfig(payloadFromLuckyGiftForm(parsed, config));
|
||||||
setConfig(saved);
|
setConfig(saved);
|
||||||
|
setConfigs((current) => mergeConfigList(current, saved));
|
||||||
setForm(formFromConfig(saved));
|
setForm(formFromConfig(saved));
|
||||||
|
setPoolId(saved.poolId || poolId);
|
||||||
setConfigDrawerOpen(false);
|
setConfigDrawerOpen(false);
|
||||||
showToast("幸运礼物配置已保存", "success");
|
showToast("幸运礼物配置已保存", "success");
|
||||||
await reloadDraws();
|
await reloadDraws();
|
||||||
@ -94,6 +123,33 @@ export function useLuckyGiftPage() {
|
|||||||
setGiftId(value);
|
setGiftId(value);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
};
|
};
|
||||||
|
const changePoolId = (value) => {
|
||||||
|
setPoolId(value.trim() || defaultPoolId);
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
const openAddPool = () => {
|
||||||
|
setAddPoolId("");
|
||||||
|
setAddPoolOpen(true);
|
||||||
|
};
|
||||||
|
const closeAddPool = () => {
|
||||||
|
setAddPoolOpen(false);
|
||||||
|
setAddPoolId("");
|
||||||
|
};
|
||||||
|
const submitAddPool = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const nextPoolID = addPoolId.trim();
|
||||||
|
if (!nextPoolID) {
|
||||||
|
showToast("奖池 ID 不能为空", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPoolId(nextPoolID);
|
||||||
|
setConfig(null);
|
||||||
|
setForm({ ...emptyLuckyGiftForm(), poolId: nextPoolID });
|
||||||
|
setAddPoolOpen(false);
|
||||||
|
setAddPoolId("");
|
||||||
|
setConfigDrawerOpen(true);
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
const changeUserId = (value) => {
|
const changeUserId = (value) => {
|
||||||
setUserId(value);
|
setUserId(value);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
@ -116,11 +172,15 @@ export function useLuckyGiftPage() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
abilities,
|
abilities,
|
||||||
|
addPoolId,
|
||||||
|
addPoolOpen,
|
||||||
changeGiftId,
|
changeGiftId,
|
||||||
|
changePoolId,
|
||||||
changeRoomId,
|
changeRoomId,
|
||||||
changeStatus,
|
changeStatus,
|
||||||
changeUserId,
|
changeUserId,
|
||||||
closeConfigDrawer,
|
closeConfigDrawer,
|
||||||
|
closeAddPool,
|
||||||
config,
|
config,
|
||||||
configDrawerOpen,
|
configDrawerOpen,
|
||||||
configLoading,
|
configLoading,
|
||||||
@ -131,15 +191,38 @@ export function useLuckyGiftPage() {
|
|||||||
form,
|
form,
|
||||||
giftId,
|
giftId,
|
||||||
openConfigDrawer,
|
openConfigDrawer,
|
||||||
|
openAddPool,
|
||||||
page,
|
page,
|
||||||
|
poolId,
|
||||||
|
poolOptions,
|
||||||
reloadConfig,
|
reloadConfig,
|
||||||
|
reloadConfigs,
|
||||||
reloadDraws,
|
reloadDraws,
|
||||||
resetDrawFilters,
|
resetDrawFilters,
|
||||||
roomId,
|
roomId,
|
||||||
setForm,
|
setForm,
|
||||||
|
setAddPoolId,
|
||||||
setPage,
|
setPage,
|
||||||
status,
|
status,
|
||||||
submitConfig,
|
submitConfig,
|
||||||
|
submitAddPool,
|
||||||
userId,
|
userId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergeConfigList(configs, saved) {
|
||||||
|
const out = [];
|
||||||
|
let replaced = false;
|
||||||
|
for (const item of configs) {
|
||||||
|
if (item.poolId === saved.poolId) {
|
||||||
|
out.push(saved);
|
||||||
|
replaced = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push(item);
|
||||||
|
}
|
||||||
|
if (!replaced) {
|
||||||
|
out.unshift(saved);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|||||||
@ -83,6 +83,10 @@
|
|||||||
width: 220px;
|
width: 220px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.poolSelector {
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
.configDrawer {
|
.configDrawer {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: min(520px, 100vw);
|
width: min(520px, 100vw);
|
||||||
@ -166,6 +170,39 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.multiplierBlock {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-3);
|
||||||
|
border: 1px solid var(--border-soft);
|
||||||
|
border-radius: var(--radius-control);
|
||||||
|
background: var(--bg-card-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiplierHeader {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiplierList {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiplierRow {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
.simulationPanel {
|
.simulationPanel {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
@ -270,7 +307,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.simulationGrid,
|
.simulationGrid,
|
||||||
.simulationOverview {
|
.simulationOverview,
|
||||||
|
.multiplierList {
|
||||||
grid-template-columns: repeat(2, minmax(140px, 1fr));
|
grid-template-columns: repeat(2, minmax(140px, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,7 @@
|
|||||||
|
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 TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import { LuckyGiftConfigDrawer } from "@/features/lucky-gift/components/LuckyGiftConfigDrawer.jsx";
|
import { LuckyGiftConfigDrawer } from "@/features/lucky-gift/components/LuckyGiftConfigDrawer.jsx";
|
||||||
import { LuckyGiftConfigSummary } from "@/features/lucky-gift/components/LuckyGiftConfigSummary.jsx";
|
import { LuckyGiftConfigSummary } from "@/features/lucky-gift/components/LuckyGiftConfigSummary.jsx";
|
||||||
@ -12,6 +16,7 @@ import {
|
|||||||
AdminListPage,
|
AdminListPage,
|
||||||
AdminListToolbar,
|
AdminListToolbar,
|
||||||
} from "@/shared/ui/AdminListLayout.jsx";
|
} from "@/shared/ui/AdminListLayout.jsx";
|
||||||
|
import { Button } from "@/shared/ui/Button.jsx";
|
||||||
import { DataState } from "@/shared/ui/DataState.jsx";
|
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||||
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||||
import { formatMillis } from "@/shared/utils/time.js";
|
import { formatMillis } from "@/shared/utils/time.js";
|
||||||
@ -24,7 +29,10 @@ const columnsBase = [
|
|||||||
render: (draw) => (
|
render: (draw) => (
|
||||||
<div className={styles.stack}>
|
<div className={styles.stack}>
|
||||||
<span>{draw.drawId}</span>
|
<span>{draw.drawId}</span>
|
||||||
<span className={styles.meta}>{draw.giftId || draw.commandId || "-"}</span>
|
<span className={styles.meta}>
|
||||||
|
{draw.poolId ? `${draw.poolId} / ` : ""}
|
||||||
|
{draw.giftId || draw.commandId || "-"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -92,13 +100,23 @@ export function LuckyGiftConfigPage() {
|
|||||||
canUpdate={page.abilities.canUpdate}
|
canUpdate={page.abilities.canUpdate}
|
||||||
config={page.config}
|
config={page.config}
|
||||||
configLoading={page.configLoading}
|
configLoading={page.configLoading}
|
||||||
|
poolId={page.poolId}
|
||||||
|
poolOptions={page.poolOptions}
|
||||||
|
onAddPool={page.openAddPool}
|
||||||
onEdit={page.openConfigDrawer}
|
onEdit={page.openConfigDrawer}
|
||||||
|
onPoolChange={page.changePoolId}
|
||||||
onRefresh={page.reloadConfig}
|
onRefresh={page.reloadConfig}
|
||||||
/>
|
/>
|
||||||
<LuckyGiftSimulationPanel config={page.config} />
|
<LuckyGiftSimulationPanel config={page.config} />
|
||||||
<AdminListToolbar
|
<AdminListToolbar
|
||||||
filters={
|
filters={
|
||||||
<>
|
<>
|
||||||
|
<TextField
|
||||||
|
className={styles.filterInput}
|
||||||
|
disabled
|
||||||
|
label="当前奖池"
|
||||||
|
value={page.poolId}
|
||||||
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
className={styles.filterInput}
|
className={styles.filterInput}
|
||||||
label="礼物 ID"
|
label="礼物 ID"
|
||||||
@ -160,10 +178,48 @@ export function LuckyGiftConfigPage() {
|
|||||||
onClose={page.closeConfigDrawer}
|
onClose={page.closeConfigDrawer}
|
||||||
onSubmit={page.submitConfig}
|
onSubmit={page.submitConfig}
|
||||||
/>
|
/>
|
||||||
|
<AddPoolDialog
|
||||||
|
disabled={page.configSaving}
|
||||||
|
open={page.addPoolOpen}
|
||||||
|
value={page.addPoolId}
|
||||||
|
onChange={page.setAddPoolId}
|
||||||
|
onClose={page.closeAddPool}
|
||||||
|
onSubmit={page.submitAddPool}
|
||||||
|
/>
|
||||||
</AdminListPage>
|
</AdminListPage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AddPoolDialog({ disabled, onChange, onClose, onSubmit, open, value }) {
|
||||||
|
return (
|
||||||
|
<Dialog fullWidth maxWidth="xs" open={open} onClose={disabled ? undefined : onClose}>
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<DialogTitle>添加奖池</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
disabled={disabled}
|
||||||
|
label="奖池 ID"
|
||||||
|
margin="dense"
|
||||||
|
value={value}
|
||||||
|
onChange={(event) => onChange(event.target.value)}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button disabled={disabled} type="button" onClick={onClose}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button disabled={disabled} type="submit" variant="primary">
|
||||||
|
添加并编辑
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</form>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function DrawStatus({ status }) {
|
function DrawStatus({ status }) {
|
||||||
const tone = status === "granted" ? "running" : status === "failed" ? "danger" : "warning";
|
const tone = status === "granted" ? "running" : status === "failed" ? "danger" : "warning";
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -9,25 +9,34 @@ export const luckyGiftConfigFormSchema = z
|
|||||||
controlStrength: z.enum(["precise", "standard", "natural"]),
|
controlStrength: z.enum(["precise", "standard", "natural"]),
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
jackpotStrategy: z.enum(["off", "conservative", "standard", "aggressive"]),
|
jackpotStrategy: z.enum(["off", "conservative", "standard", "aggressive"]),
|
||||||
|
multiplierValues: z.array(z.coerce.number().min(0, "倍率不能小于 0")).min(1, "倍率配置不能为空"),
|
||||||
noviceStrength: z.enum(["weak", "standard", "strong"]),
|
noviceStrength: z.enum(["weak", "standard", "strong"]),
|
||||||
payoutStyle: z.enum(["stable", "balanced", "exciting"]),
|
poolId: z.string().trim().min(1, "奖池 ID 不能为空").max(96, "奖池 ID 不能超过 96 个字符"),
|
||||||
poolMode: z.enum(["platform_guard", "standard", "room_burst", "gift_isolated"]),
|
poolMode: z.enum(["platform_guard", "standard", "room_burst", "gift_isolated"]),
|
||||||
riskLevel: z.enum(["strict", "standard", "loose"]),
|
riskLevel: z.enum(["strict", "standard", "loose"]),
|
||||||
targetRtpPercent: z.coerce.number().min(1, "RTP 必须大于 1%").max(100, "RTP 不能超过 100%"),
|
targetRtpPercent: z.coerce.number().min(1, "RTP 必须大于 1%").max(100, "RTP 不能超过 100%"),
|
||||||
})
|
})
|
||||||
.superRefine((value, context) => {
|
.superRefine((value, context) => {
|
||||||
if (!value.activitySubsidyEnabled) {
|
if (value.activitySubsidyEnabled && value.activityBudget <= 0) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (value.activityBudget <= 0) {
|
|
||||||
context.addIssue({ code: "custom", message: "开启活动补贴后必须配置总预算", path: ["activityBudget"] });
|
context.addIssue({ code: "custom", message: "开启活动补贴后必须配置总预算", path: ["activityBudget"] });
|
||||||
}
|
}
|
||||||
if (value.activityBudget > 0 && value.activityDailyLimit <= 0) {
|
if (value.activitySubsidyEnabled && value.activityBudget > 0 && value.activityDailyLimit <= 0) {
|
||||||
context.addIssue({ code: "custom", message: "配置活动总预算时必须配置每日预算", path: ["activityDailyLimit"] });
|
context.addIssue({ code: "custom", message: "配置活动总预算时必须配置每日预算", path: ["activityDailyLimit"] });
|
||||||
}
|
}
|
||||||
if (value.activityDailyLimit > value.activityBudget && value.activityBudget > 0) {
|
if (value.activitySubsidyEnabled && value.activityDailyLimit > value.activityBudget && value.activityBudget > 0) {
|
||||||
context.addIssue({ code: "custom", message: "每日预算不能大于活动总预算", path: ["activityDailyLimit"] });
|
context.addIssue({ code: "custom", message: "每日预算不能大于活动总预算", path: ["activityDailyLimit"] });
|
||||||
}
|
}
|
||||||
|
const multipliers = value.multiplierValues
|
||||||
|
.map((item) => Number(item))
|
||||||
|
.filter((item) => Number.isFinite(item) && item >= 0);
|
||||||
|
if (multipliers.length === 0 || Math.max(...multipliers) <= 0) {
|
||||||
|
context.addIssue({ code: "custom", message: "至少配置一个大于 0 的中奖倍率", path: ["multiplierValues"] });
|
||||||
|
}
|
||||||
|
const maxMultiplier = Math.max(...multipliers, 0);
|
||||||
|
const targetMultiplier = Number(value.targetRtpPercent || 0) / 100;
|
||||||
|
if (maxMultiplier < targetMultiplier) {
|
||||||
|
context.addIssue({ code: "custom", message: "最大倍率不能低于 RTP 对应返还倍数", path: ["multiplierValues"] });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export type LuckyGiftConfigForm = z.infer<typeof luckyGiftConfigFormSchema>;
|
export type LuckyGiftConfigForm = z.infer<typeof luckyGiftConfigFormSchema>;
|
||||||
|
|||||||
155
src/features/user-leaderboard/api.ts
Normal file
155
src/features/user-leaderboard/api.ts
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import { API_ENDPOINTS, API_OPERATIONS, apiEndpointPath } from "@/shared/api/generated/endpoints";
|
||||||
|
import { apiRequest } from "@/shared/api/request";
|
||||||
|
import type { PageQuery } from "@/shared/api/types";
|
||||||
|
|
||||||
|
export type UserLeaderboardBoardType = "sent" | "received" | "room";
|
||||||
|
export type UserLeaderboardPeriod = "today" | "week" | "month";
|
||||||
|
|
||||||
|
export interface UserLeaderboardUserDto {
|
||||||
|
userId: string;
|
||||||
|
displayUserId?: string;
|
||||||
|
username?: string;
|
||||||
|
avatar?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserLeaderboardRoomDto {
|
||||||
|
roomId: string;
|
||||||
|
roomShortId?: string;
|
||||||
|
title?: string;
|
||||||
|
coverUrl?: string;
|
||||||
|
ownerUserId?: string;
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserLeaderboardItemDto {
|
||||||
|
rank: number;
|
||||||
|
userId?: string;
|
||||||
|
roomId?: string;
|
||||||
|
giftValue: number;
|
||||||
|
giftCount: number;
|
||||||
|
transactionCount: number;
|
||||||
|
lastGiftAtMs: number;
|
||||||
|
user?: UserLeaderboardUserDto;
|
||||||
|
room?: UserLeaderboardRoomDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserLeaderboardResponseDto {
|
||||||
|
items: UserLeaderboardItemDto[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
boardType: UserLeaderboardBoardType;
|
||||||
|
period: UserLeaderboardPeriod;
|
||||||
|
startAtMs: number;
|
||||||
|
endAtMs: number;
|
||||||
|
serverTimeMs: number;
|
||||||
|
myRank?: UserLeaderboardItemDto | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserLeaderboardQuery extends PageQuery {
|
||||||
|
board_type?: UserLeaderboardBoardType;
|
||||||
|
period?: UserLeaderboardPeriod;
|
||||||
|
viewer_user_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RawUser = UserLeaderboardUserDto & Record<string, unknown>;
|
||||||
|
type RawRoom = UserLeaderboardRoomDto & Record<string, unknown>;
|
||||||
|
type RawItem = Omit<UserLeaderboardItemDto, "room" | "user"> & {
|
||||||
|
last_gift_at_ms?: number | string;
|
||||||
|
transaction_count?: number | string;
|
||||||
|
gift_count?: number | string;
|
||||||
|
gift_value?: number | string;
|
||||||
|
user_id?: string | number;
|
||||||
|
room_id?: string;
|
||||||
|
user?: RawUser;
|
||||||
|
room?: RawRoom;
|
||||||
|
} & Record<string, unknown>;
|
||||||
|
type RawResponse = Omit<UserLeaderboardResponseDto, "items" | "myRank"> & {
|
||||||
|
board_type?: UserLeaderboardBoardType;
|
||||||
|
end_at_ms?: number | string;
|
||||||
|
items?: RawItem[];
|
||||||
|
my_rank?: RawItem | null;
|
||||||
|
myRank?: RawItem | null;
|
||||||
|
page_size?: number | string;
|
||||||
|
server_time_ms?: number | string;
|
||||||
|
start_at_ms?: number | string;
|
||||||
|
} & Record<string, unknown>;
|
||||||
|
|
||||||
|
export function listUserLeaderboards(query: UserLeaderboardQuery = {}): Promise<UserLeaderboardResponseDto> {
|
||||||
|
const endpoint = API_ENDPOINTS.listUserLeaderboards;
|
||||||
|
return apiRequest<RawResponse>(apiEndpointPath(API_OPERATIONS.listUserLeaderboards), {
|
||||||
|
method: endpoint.method,
|
||||||
|
query,
|
||||||
|
}).then(normalizeResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeResponse(response: RawResponse): UserLeaderboardResponseDto {
|
||||||
|
return {
|
||||||
|
items: (response.items || []).map(normalizeItem),
|
||||||
|
total: numberValue(response.total),
|
||||||
|
page: numberValue(response.page) || 1,
|
||||||
|
pageSize: numberValue(response.pageSize ?? response.page_size) || 20,
|
||||||
|
boardType: normalizeBoardType(response.boardType ?? response.board_type),
|
||||||
|
period: normalizePeriod(response.period),
|
||||||
|
startAtMs: numberValue(response.startAtMs ?? response.start_at_ms),
|
||||||
|
endAtMs: numberValue(response.endAtMs ?? response.end_at_ms),
|
||||||
|
serverTimeMs: numberValue(response.serverTimeMs ?? response.server_time_ms),
|
||||||
|
myRank: response.myRank || response.my_rank ? normalizeItem((response.myRank || response.my_rank) as RawItem) : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeItem(item: RawItem): UserLeaderboardItemDto {
|
||||||
|
return {
|
||||||
|
rank: numberValue(item.rank),
|
||||||
|
userId: optionalString(item.userId ?? item.user_id),
|
||||||
|
roomId: optionalString(item.roomId ?? item.room_id),
|
||||||
|
giftValue: numberValue(item.giftValue ?? item.gift_value),
|
||||||
|
giftCount: numberValue(item.giftCount ?? item.gift_count),
|
||||||
|
transactionCount: numberValue(item.transactionCount ?? item.transaction_count),
|
||||||
|
lastGiftAtMs: numberValue(item.lastGiftAtMs ?? item.last_gift_at_ms),
|
||||||
|
user: item.user ? normalizeUser(item.user) : undefined,
|
||||||
|
room: item.room ? normalizeRoom(item.room) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeUser(user: RawUser): UserLeaderboardUserDto {
|
||||||
|
return {
|
||||||
|
userId: stringValue(user.userId ?? user.user_id),
|
||||||
|
displayUserId: optionalString(user.displayUserId ?? user.display_user_id),
|
||||||
|
username: optionalString(user.username),
|
||||||
|
avatar: optionalString(user.avatar),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRoom(room: RawRoom): UserLeaderboardRoomDto {
|
||||||
|
return {
|
||||||
|
roomId: stringValue(room.roomId ?? room.room_id),
|
||||||
|
roomShortId: optionalString(room.roomShortId ?? room.room_short_id),
|
||||||
|
title: optionalString(room.title),
|
||||||
|
coverUrl: optionalString(room.coverUrl ?? room.cover_url),
|
||||||
|
ownerUserId: optionalString(room.ownerUserId ?? room.owner_user_id),
|
||||||
|
status: optionalString(room.status),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeBoardType(value: unknown): UserLeaderboardBoardType {
|
||||||
|
return value === "received" || value === "room" ? value : "sent";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePeriod(value: unknown): UserLeaderboardPeriod {
|
||||||
|
return value === "week" || value === "month" ? value : "today";
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringValue(value: unknown) {
|
||||||
|
return value === null || value === undefined ? "" : String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function optionalString(value: unknown) {
|
||||||
|
const text = stringValue(value).trim();
|
||||||
|
return text || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function numberValue(value: unknown) {
|
||||||
|
const next = Number(value ?? 0);
|
||||||
|
return Number.isFinite(next) ? next : 0;
|
||||||
|
}
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||||
|
import { listUserLeaderboards } from "@/features/user-leaderboard/api";
|
||||||
|
import { useUserLeaderboardAbilities } from "@/features/user-leaderboard/permissions.js";
|
||||||
|
|
||||||
|
const pageSize = 20;
|
||||||
|
|
||||||
|
export function useUserLeaderboardPage() {
|
||||||
|
const abilities = useUserLeaderboardAbilities();
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const [boardType, setBoardType] = useState("sent");
|
||||||
|
const [period, setPeriod] = useState("today");
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [data, setData] = useState(emptyData());
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const reload = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
try {
|
||||||
|
const nextData = await listUserLeaderboards({
|
||||||
|
board_type: boardType,
|
||||||
|
page,
|
||||||
|
page_size: pageSize,
|
||||||
|
period,
|
||||||
|
});
|
||||||
|
setData(nextData);
|
||||||
|
} catch (err) {
|
||||||
|
const message = err.message || "加载用户榜单失败";
|
||||||
|
setError(message);
|
||||||
|
showToast(message, "error");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [boardType, page, period, showToast]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void reload();
|
||||||
|
}, [reload]);
|
||||||
|
|
||||||
|
const changeBoardType = useCallback((nextValue) => {
|
||||||
|
setBoardType(nextValue || "sent");
|
||||||
|
setPage(1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const changePeriod = useCallback((nextValue) => {
|
||||||
|
setPeriod(nextValue || "today");
|
||||||
|
setPage(1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resetFilters = useCallback(() => {
|
||||||
|
setBoardType("sent");
|
||||||
|
setPeriod("today");
|
||||||
|
setPage(1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
abilities,
|
||||||
|
boardType,
|
||||||
|
changeBoardType,
|
||||||
|
changePeriod,
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
page,
|
||||||
|
period,
|
||||||
|
reload,
|
||||||
|
resetFilters,
|
||||||
|
setPage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyData() {
|
||||||
|
return {
|
||||||
|
boardType: "sent",
|
||||||
|
endAtMs: 0,
|
||||||
|
items: [],
|
||||||
|
myRank: null,
|
||||||
|
page: 1,
|
||||||
|
pageSize,
|
||||||
|
period: "today",
|
||||||
|
serverTimeMs: 0,
|
||||||
|
startAtMs: 0,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
162
src/features/user-leaderboard/pages/UserLeaderboardPage.jsx
Normal file
162
src/features/user-leaderboard/pages/UserLeaderboardPage.jsx
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import { DataState } from "@/shared/ui/DataState.jsx";
|
||||||
|
import { DataTable } from "@/shared/ui/DataTable.jsx";
|
||||||
|
import {
|
||||||
|
AdminFilterResetButton,
|
||||||
|
AdminFilterSelect,
|
||||||
|
AdminListBody,
|
||||||
|
AdminListPage,
|
||||||
|
AdminListToolbar,
|
||||||
|
} from "@/shared/ui/AdminListLayout.jsx";
|
||||||
|
import { formatMillis } from "@/shared/utils/time.js";
|
||||||
|
import { useUserLeaderboardPage } from "@/features/user-leaderboard/hooks/useUserLeaderboardPage.js";
|
||||||
|
import styles from "@/features/user-leaderboard/user-leaderboard.module.css";
|
||||||
|
|
||||||
|
const boardTypeOptions = [
|
||||||
|
["sent", "用户送礼榜"],
|
||||||
|
["received", "收礼榜"],
|
||||||
|
["room", "房间礼物值榜"],
|
||||||
|
];
|
||||||
|
|
||||||
|
const periodOptions = [
|
||||||
|
["today", "今天"],
|
||||||
|
["week", "一周"],
|
||||||
|
["month", "一月"],
|
||||||
|
];
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
key: "rank",
|
||||||
|
label: "排名",
|
||||||
|
width: "minmax(96px, 0.45fr)",
|
||||||
|
render: (item) => <span className={styles.rank}>#{item.rank}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "subject",
|
||||||
|
label: "对象",
|
||||||
|
width: "minmax(280px, 1.25fr)",
|
||||||
|
render: (item, _index, context) => <LeaderboardSubject item={item} type={context.boardType} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "giftValue",
|
||||||
|
label: "礼物值",
|
||||||
|
width: "minmax(150px, 0.75fr)",
|
||||||
|
render: (item) => formatNumber(item.giftValue),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "giftCount",
|
||||||
|
label: "礼物数量",
|
||||||
|
width: "minmax(130px, 0.65fr)",
|
||||||
|
render: (item) => formatNumber(item.giftCount),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "transactionCount",
|
||||||
|
label: "交易数",
|
||||||
|
width: "minmax(120px, 0.6fr)",
|
||||||
|
render: (item) => formatNumber(item.transactionCount),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "lastGiftAtMs",
|
||||||
|
label: "最后送礼时间",
|
||||||
|
width: "minmax(180px, 0.85fr)",
|
||||||
|
render: (item) => `${formatMillis(item.lastGiftAtMs, "UTC")} UTC`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function UserLeaderboardPage() {
|
||||||
|
const page = useUserLeaderboardPage();
|
||||||
|
const items = page.data.items || [];
|
||||||
|
const total = page.data.total || 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdminListPage>
|
||||||
|
<AdminListToolbar
|
||||||
|
filters={
|
||||||
|
<>
|
||||||
|
<AdminFilterSelect
|
||||||
|
label="榜单"
|
||||||
|
options={boardTypeOptions}
|
||||||
|
value={page.boardType}
|
||||||
|
onChange={page.changeBoardType}
|
||||||
|
/>
|
||||||
|
<AdminFilterSelect
|
||||||
|
label="时间维度"
|
||||||
|
options={periodOptions}
|
||||||
|
value={page.period}
|
||||||
|
onChange={page.changePeriod}
|
||||||
|
/>
|
||||||
|
<AdminFilterResetButton
|
||||||
|
disabled={page.boardType === "sent" && page.period === "today"}
|
||||||
|
onClick={page.resetFilters}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className={styles.summary}>
|
||||||
|
<div>
|
||||||
|
<span className={styles.summaryLabel}>UTC 时间范围</span>
|
||||||
|
<span className={styles.summaryValue}>
|
||||||
|
{formatMillis(page.data.startAtMs, "UTC")} - {formatMillis(page.data.endAtMs, "UTC")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{page.data.myRank ? (
|
||||||
|
<div>
|
||||||
|
<span className={styles.summaryLabel}>当前用户排名</span>
|
||||||
|
<span className={styles.summaryValue}>
|
||||||
|
#{page.data.myRank.rank} · {formatNumber(page.data.myRank.giftValue)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
|
||||||
|
<AdminListBody>
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
context={{ boardType: page.boardType }}
|
||||||
|
items={items}
|
||||||
|
minWidth="960px"
|
||||||
|
pagination={
|
||||||
|
total > 0
|
||||||
|
? {
|
||||||
|
page: page.page,
|
||||||
|
pageSize: page.data.pageSize || 20,
|
||||||
|
total,
|
||||||
|
onPageChange: page.setPage,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
rowKey={(item) => `${page.boardType}-${item.userId || item.roomId || item.rank}`}
|
||||||
|
/>
|
||||||
|
</AdminListBody>
|
||||||
|
</DataState>
|
||||||
|
</AdminListPage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LeaderboardSubject({ item, type }) {
|
||||||
|
if (type === "room") {
|
||||||
|
const room = item.room || {};
|
||||||
|
return (
|
||||||
|
<div className={styles.identity}>
|
||||||
|
<span className={styles.name}>{room.title || item.roomId || "-"}</span>
|
||||||
|
<span className={styles.meta}>
|
||||||
|
{room.roomShortId ? `短号 ${room.roomShortId}` : item.roomId || "-"}
|
||||||
|
{room.ownerUserId ? ` · 房主 ${room.ownerUserId}` : ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const user = item.user || {};
|
||||||
|
return (
|
||||||
|
<div className={styles.identity}>
|
||||||
|
<span className={styles.name}>{user.username || user.displayUserId || item.userId || "-"}</span>
|
||||||
|
<span className={styles.meta}>
|
||||||
|
{user.displayUserId ? `短号 ${user.displayUserId}` : ""}
|
||||||
|
{item.userId ? `${user.displayUserId ? " · " : ""}ID ${item.userId}` : ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNumber(value) {
|
||||||
|
return Number(value || 0).toLocaleString("zh-CN");
|
||||||
|
}
|
||||||
9
src/features/user-leaderboard/permissions.js
Normal file
9
src/features/user-leaderboard/permissions.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { useAuth } from "@/app/auth/AuthProvider.jsx";
|
||||||
|
import { PERMISSIONS } from "@/app/permissions";
|
||||||
|
|
||||||
|
export function useUserLeaderboardAbilities() {
|
||||||
|
const { can } = useAuth();
|
||||||
|
return {
|
||||||
|
canView: can(PERMISSIONS.userLeaderboardView),
|
||||||
|
};
|
||||||
|
}
|
||||||
12
src/features/user-leaderboard/routes.js
Normal file
12
src/features/user-leaderboard/routes.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { MENU_CODES, PERMISSIONS } from "@/app/permissions";
|
||||||
|
|
||||||
|
export const userLeaderboardRoutes = [
|
||||||
|
{
|
||||||
|
label: "用户榜单",
|
||||||
|
loader: () => import("./pages/UserLeaderboardPage.jsx").then((module) => module.UserLeaderboardPage),
|
||||||
|
menuCode: MENU_CODES.userLeaderboard,
|
||||||
|
pageKey: "user-leaderboard",
|
||||||
|
path: "/activities/user-leaderboards",
|
||||||
|
permission: PERMISSIONS.userLeaderboardView,
|
||||||
|
},
|
||||||
|
];
|
||||||
50
src/features/user-leaderboard/user-leaderboard.module.css
Normal file
50
src/features/user-leaderboard/user-leaderboard.module.css
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
.summary {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryLabel {
|
||||||
|
display: block;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryValue {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.identity {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: 12px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
@ -107,6 +107,7 @@ export const API_OPERATIONS = {
|
|||||||
listHosts: "listHosts",
|
listHosts: "listHosts",
|
||||||
listLevelConfig: "listLevelConfig",
|
listLevelConfig: "listLevelConfig",
|
||||||
listLoginLogs: "listLoginLogs",
|
listLoginLogs: "listLoginLogs",
|
||||||
|
listLuckyGiftConfigs: "listLuckyGiftConfigs",
|
||||||
listLuckyGiftDraws: "listLuckyGiftDraws",
|
listLuckyGiftDraws: "listLuckyGiftDraws",
|
||||||
listOperationLogs: "listOperationLogs",
|
listOperationLogs: "listOperationLogs",
|
||||||
listPermissions: "listPermissions",
|
listPermissions: "listPermissions",
|
||||||
@ -125,6 +126,7 @@ export const API_OPERATIONS = {
|
|||||||
listSevenDayCheckInClaims: "listSevenDayCheckInClaims",
|
listSevenDayCheckInClaims: "listSevenDayCheckInClaims",
|
||||||
listSystemMenus: "listSystemMenus",
|
listSystemMenus: "listSystemMenus",
|
||||||
listTaskDefinitions: "listTaskDefinitions",
|
listTaskDefinitions: "listTaskDefinitions",
|
||||||
|
listUserLeaderboards: "listUserLeaderboards",
|
||||||
listUsers: "listUsers",
|
listUsers: "listUsers",
|
||||||
login: "login",
|
login: "login",
|
||||||
logout: "logout",
|
logout: "logout",
|
||||||
@ -837,6 +839,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
|||||||
permission: "log:view",
|
permission: "log:view",
|
||||||
permissions: ["log:view"]
|
permissions: ["log:view"]
|
||||||
},
|
},
|
||||||
|
listLuckyGiftConfigs: {
|
||||||
|
method: "GET",
|
||||||
|
operationId: API_OPERATIONS.listLuckyGiftConfigs,
|
||||||
|
path: "/v1/admin/activity/lucky-gifts/configs",
|
||||||
|
permission: "lucky-gift:view",
|
||||||
|
permissions: ["lucky-gift:view"]
|
||||||
|
},
|
||||||
listLuckyGiftDraws: {
|
listLuckyGiftDraws: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listLuckyGiftDraws,
|
operationId: API_OPERATIONS.listLuckyGiftDraws,
|
||||||
@ -963,6 +972,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
|||||||
permission: "daily-task:view",
|
permission: "daily-task:view",
|
||||||
permissions: ["daily-task:view"]
|
permissions: ["daily-task:view"]
|
||||||
},
|
},
|
||||||
|
listUserLeaderboards: {
|
||||||
|
method: "GET",
|
||||||
|
operationId: API_OPERATIONS.listUserLeaderboards,
|
||||||
|
path: "/v1/admin/activity/user-leaderboards",
|
||||||
|
permission: "user-leaderboard:view",
|
||||||
|
permissions: ["user-leaderboard:view"]
|
||||||
|
},
|
||||||
listUsers: {
|
listUsers: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listUsers,
|
operationId: API_OPERATIONS.listUsers,
|
||||||
|
|||||||
56
src/shared/api/generated/schema.d.ts
vendored
56
src/shared/api/generated/schema.d.ts
vendored
@ -84,6 +84,22 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/admin/activity/user-leaderboards": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get: operations["listUserLeaderboards"];
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/admin/activity/lucky-gifts/config": {
|
"/admin/activity/lucky-gifts/config": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@ -100,6 +116,22 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/admin/activity/lucky-gifts/configs": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get: operations["listLuckyGiftConfigs"];
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/admin/activity/lucky-gifts/draws": {
|
"/admin/activity/lucky-gifts/draws": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@ -2432,6 +2464,18 @@ export interface operations {
|
|||||||
200: components["responses"]["EmptyResponse"];
|
200: components["responses"]["EmptyResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
listUserLeaderboards: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
200: components["responses"]["EmptyResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
getLuckyGiftConfig: {
|
getLuckyGiftConfig: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@ -2456,6 +2500,18 @@ export interface operations {
|
|||||||
200: components["responses"]["EmptyResponse"];
|
200: components["responses"]["EmptyResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
listLuckyGiftConfigs: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
200: components["responses"]["EmptyResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
listLuckyGiftDraws: {
|
listLuckyGiftDraws: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user