feat(ops-center): separate current and historical lucky pools

This commit is contained in:
zhx 2026-07-15 17:46:03 +08:00
parent edf1420f49
commit 662c497771
2 changed files with 83 additions and 13 deletions

View File

@ -149,7 +149,7 @@ export function buildOpsUrl(path, query = {}) {
export function normalizeDashboard({ apps = [], perApp = [] } = {}) { export function normalizeDashboard({ apps = [], perApp = [] } = {}) {
const luckyGifts = perApp.flatMap((entry) => entry.luckyGifts || []); const luckyGifts = perApp.flatMap((entry) => entry.luckyGifts || []);
const poolBalances = perApp const normalizedPoolBalances = perApp
.flatMap((entry) => (entry.poolBalances || []).map((item) => normalizePoolBalance(item, entry.appCode))) .flatMap((entry) => (entry.poolBalances || []).map((item) => normalizePoolBalance(item, entry.appCode)))
.filter((item) => item.app_code); .filter((item) => item.app_code);
const appSummaries = perApp const appSummaries = perApp
@ -157,8 +157,32 @@ export function normalizeDashboard({ apps = [], perApp = [] } = {}) {
.filter((summary) => summary.app_code); .filter((summary) => summary.app_code);
const publishedConfigs = luckyGifts.filter((config) => !config.is_default); const publishedConfigs = luckyGifts.filter((config) => !config.is_default);
const currentConfigByPool = new Map();
luckyGifts.forEach((config) => {
const familyKey = luckyGiftPoolFamilyKey(config);
const ruleVersion = numberValue(config.rule_version ?? config.ruleVersion);
const current = currentConfigByPool.get(familyKey);
// 正常接口只返回每个奖池的最新版本;这里仍按最大 rule_version 收敛,避免兼容接口返回多版本时
// 由数组顺序把旧策略误判为当前。版本相同则保留首条,确保结果不随返回顺序抖动。
if (!current || ruleVersion > current.ruleVersion) {
currentConfigByPool.set(familyKey, {
ruleVersion,
strategyVersion: normalizeStrategyVersion(config.strategy_version ?? config.strategyVersion),
});
}
});
// 账本接口会同时返回同一 app+pool 的 V2/V3 独立水位;最新规则配置才决定哪口是当前奖池。
// 没有配置可关联的孤立账本继续按当前展示,避免前端误把仍需运营处理的资金静默藏进历史筛选。
const poolBalances = normalizedPoolBalances.map((pool) => {
const currentStrategy = currentConfigByPool.get(luckyGiftPoolFamilyKey(pool))?.strategyVersion;
return {
...pool,
is_historical: Boolean(currentStrategy && currentStrategy !== pool.strategy_version),
};
});
const currentPoolBalances = poolBalances.filter((pool) => !pool.is_historical);
// KPI 一律跨应用聚合。旧版只取第一个应用的汇总,导致 lalu 有大量抽奖时首屏"抽奖次数"仍显示 0。 // KPI 一律跨应用聚合;奖池金额只汇总当前策略,避免历史 V2 与当前 V3 的独立账本重复计入运营水位
const summary = { const summary = {
activeApps: apps.length, activeApps: apps.length,
configuredPools: publishedConfigs.length, configuredPools: publishedConfigs.length,
@ -166,9 +190,9 @@ export function normalizeDashboard({ apps = [], perApp = [] } = {}) {
failedDraws: sumBy(appSummaries, "failed_draws"), failedDraws: sumBy(appSummaries, "failed_draws"),
grantedDraws: sumBy(appSummaries, "granted_draws"), grantedDraws: sumBy(appSummaries, "granted_draws"),
pendingDraws: sumBy(appSummaries, "pending_draws"), pendingDraws: sumBy(appSummaries, "pending_draws"),
poolAvailableTotal: sumBy(poolBalances, "available_balance", "availableBalance"), poolAvailableTotal: sumBy(currentPoolBalances, "available_balance", "availableBalance"),
poolBalanceTotal: sumBy(poolBalances, "balance", "Balance"), poolBalanceTotal: sumBy(currentPoolBalances, "balance", "Balance"),
poolReserveTotal: sumBy(poolBalances, "reserve_floor", "reserveFloor"), poolReserveTotal: sumBy(currentPoolBalances, "reserve_floor", "reserveFloor"),
totalDraws: sumBy(appSummaries, "total_draws"), totalDraws: sumBy(appSummaries, "total_draws"),
totalRewardCoins: sumBy(appSummaries, "total_reward_coins"), totalRewardCoins: sumBy(appSummaries, "total_reward_coins"),
totalSpentCoins: sumBy(appSummaries, "total_spent_coins"), totalSpentCoins: sumBy(appSummaries, "total_spent_coins"),
@ -208,6 +232,14 @@ export function luckyGiftPoolKey(item = {}) {
return `${appCode}:${poolID}:${strategyVersion}`; return `${appCode}:${poolID}:${strategyVersion}`;
} }
function luckyGiftPoolFamilyKey(item = {}) {
const appCode = String(item.app_code || item.appCode || "")
.trim()
.toLowerCase();
const poolID = String(item.pool_id || item.poolId || DEFAULT_POOL_ID).trim() || DEFAULT_POOL_ID;
return `${appCode}:${poolID}`;
}
function normalizeDrawSummary(summary = {}, appCode = "") { function normalizeDrawSummary(summary = {}, appCode = "") {
return { return {
actual_rtp_ppm: numberValue(summary.actual_rtp_ppm ?? summary.actualRtpPpm), actual_rtp_ppm: numberValue(summary.actual_rtp_ppm ?? summary.actualRtpPpm),

View File

@ -2,7 +2,8 @@ import AppsOutlined from "@mui/icons-material/AppsOutlined";
import CardGiftcardOutlined from "@mui/icons-material/CardGiftcardOutlined"; import CardGiftcardOutlined from "@mui/icons-material/CardGiftcardOutlined";
import CasinoOutlined from "@mui/icons-material/CasinoOutlined"; import CasinoOutlined from "@mui/icons-material/CasinoOutlined";
import SavingsOutlined from "@mui/icons-material/SavingsOutlined"; import SavingsOutlined from "@mui/icons-material/SavingsOutlined";
import { useMemo } from "react"; import { useMemo, useState } from "react";
import { AdminFilterSelect, AdminListToolbar } from "@/shared/ui/AdminListLayout.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx"; import { DataTable } from "@/shared/ui/DataTable.jsx";
import { KpiCard } from "@/shared/ui/KpiCard.jsx"; import { KpiCard } from "@/shared/ui/KpiCard.jsx";
import { TimeText } from "@/shared/ui/TimeText.jsx"; import { TimeText } from "@/shared/ui/TimeText.jsx";
@ -13,10 +14,29 @@ import { OpsStatusBadge } from "./OpsStatusBadge.jsx";
export function OverviewView({ canCredit, canDebit, data, disabled, onCredit, onDebit }) { export function OverviewView({ canCredit, canDebit, data, disabled, onCredit, onDebit }) {
const { appSummaries, poolBalances, summary } = data; const { appSummaries, poolBalances, summary } = data;
const [poolScope, setPoolScope] = useState("current");
const poolBalanceColumns = useMemo( const poolBalanceColumns = useMemo(
() => buildPoolBalanceColumns({ canCredit, canDebit, disabled, onCredit, onDebit }), () => buildPoolBalanceColumns({ canCredit, canDebit, disabled, onCredit, onDebit }),
[canCredit, canDebit, disabled, onCredit, onDebit], [canCredit, canDebit, disabled, onCredit, onDebit],
); );
const poolCounts = useMemo(
() => ({
all: poolBalances.length,
current: poolBalances.filter((pool) => !pool.is_historical).length,
history: poolBalances.filter((pool) => pool.is_historical).length,
}),
[poolBalances],
);
const visiblePoolBalances = useMemo(() => {
if (poolScope === "history") {
return poolBalances.filter((pool) => pool.is_historical);
}
if (poolScope === "all") {
return poolBalances;
}
// V2/V3
return poolBalances.filter((pool) => !pool.is_historical);
}, [poolBalances, poolScope]);
return ( return (
<div className="ops-view"> <div className="ops-view">
@ -43,21 +63,35 @@ export function OverviewView({ canCredit, canDebit, data, disabled, onCredit, on
/> />
<KpiCard <KpiCard
icon={SavingsOutlined} icon={SavingsOutlined}
label="奖池金币" label="当前奖池金币"
sub={`可用 ${formatNumber(summary.poolAvailableTotal)} · 保底 ${formatNumber(summary.poolReserveTotal)}`} sub={`可用 ${formatNumber(summary.poolAvailableTotal)} · 保底 ${formatNumber(summary.poolReserveTotal)}`}
tone="success" tone="success"
value={formatNumber(summary.poolBalanceTotal)} value={formatNumber(summary.poolBalanceTotal)}
/> />
</div> </div>
<AdminListToolbar
filters={
<AdminFilterSelect
label="奖池范围"
options={[
["current", `当前奖池 (${poolCounts.current})`],
["history", `历史奖池 (${poolCounts.history})`],
["all", `全部奖池 (${poolCounts.all})`],
]}
value={poolScope}
onChange={setPoolScope}
/>
}
/>
<DataTable <DataTable
columns={poolBalanceColumns} columns={poolBalanceColumns}
emptyLabel="暂无奖池余额" emptyLabel={poolScope === "history" ? "暂无历史奖池" : "暂无奖池余额"}
items={poolBalances} items={visiblePoolBalances}
minWidth="1480px" minWidth="1480px"
rowKey={luckyGiftPoolKey} rowKey={luckyGiftPoolKey}
title="奖池实时水位" title="奖池实时水位"
total={poolBalances.length} total={visiblePoolBalances.length}
/> />
<DataTable <DataTable
@ -82,12 +116,16 @@ function buildPoolBalanceColumns({ canCredit, canDebit, disabled, onCredit, onDe
key: "materialized", key: "materialized",
label: "水位状态", label: "水位状态",
// materialized=false owner owner // materialized=false owner owner
render: (item) => render: (item) => {
item.materialized === false ? ( if (item.is_historical) {
return <OpsStatusBadge label="历史奖池" tone="stopped" />;
}
return item.materialized === false ? (
<OpsStatusBadge label="默认水位" /> <OpsStatusBadge label="默认水位" />
) : ( ) : (
<OpsStatusBadge label="已入账" tone="succeeded" /> <OpsStatusBadge label="已入账" tone="succeeded" />
), );
},
width: "minmax(132px, 0.9fr)", width: "minmax(132px, 0.9fr)",
}, },
{ key: "balance", label: "当前余额", render: (item) => formatNumber(item.balance) }, { key: "balance", label: "当前余额", render: (item) => formatNumber(item.balance) },