merge: current and historical lucky pool view

This commit is contained in:
zhx 2026-07-15 17:46:21 +08:00
commit 917a68b9d8
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 = [] } = {}) {
const luckyGifts = perApp.flatMap((entry) => entry.luckyGifts || []);
const poolBalances = perApp
const normalizedPoolBalances = perApp
.flatMap((entry) => (entry.poolBalances || []).map((item) => normalizePoolBalance(item, entry.appCode)))
.filter((item) => item.app_code);
const appSummaries = perApp
@ -157,8 +157,32 @@ export function normalizeDashboard({ apps = [], perApp = [] } = {}) {
.filter((summary) => summary.app_code);
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 = {
activeApps: apps.length,
configuredPools: publishedConfigs.length,
@ -166,9 +190,9 @@ export function normalizeDashboard({ apps = [], perApp = [] } = {}) {
failedDraws: sumBy(appSummaries, "failed_draws"),
grantedDraws: sumBy(appSummaries, "granted_draws"),
pendingDraws: sumBy(appSummaries, "pending_draws"),
poolAvailableTotal: sumBy(poolBalances, "available_balance", "availableBalance"),
poolBalanceTotal: sumBy(poolBalances, "balance", "Balance"),
poolReserveTotal: sumBy(poolBalances, "reserve_floor", "reserveFloor"),
poolAvailableTotal: sumBy(currentPoolBalances, "available_balance", "availableBalance"),
poolBalanceTotal: sumBy(currentPoolBalances, "balance", "Balance"),
poolReserveTotal: sumBy(currentPoolBalances, "reserve_floor", "reserveFloor"),
totalDraws: sumBy(appSummaries, "total_draws"),
totalRewardCoins: sumBy(appSummaries, "total_reward_coins"),
totalSpentCoins: sumBy(appSummaries, "total_spent_coins"),
@ -208,6 +232,14 @@ export function luckyGiftPoolKey(item = {}) {
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 = "") {
return {
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 CasinoOutlined from "@mui/icons-material/CasinoOutlined";
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 { KpiCard } from "@/shared/ui/KpiCard.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 }) {
const { appSummaries, poolBalances, summary } = data;
const [poolScope, setPoolScope] = useState("current");
const poolBalanceColumns = useMemo(
() => buildPoolBalanceColumns({ 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 (
<div className="ops-view">
@ -43,21 +63,35 @@ export function OverviewView({ canCredit, canDebit, data, disabled, onCredit, on
/>
<KpiCard
icon={SavingsOutlined}
label="奖池金币"
label="当前奖池金币"
sub={`可用 ${formatNumber(summary.poolAvailableTotal)} · 保底 ${formatNumber(summary.poolReserveTotal)}`}
tone="success"
value={formatNumber(summary.poolBalanceTotal)}
/>
</div>
<AdminListToolbar
filters={
<AdminFilterSelect
label="奖池范围"
options={[
["current", `当前奖池 (${poolCounts.current})`],
["history", `历史奖池 (${poolCounts.history})`],
["all", `全部奖池 (${poolCounts.all})`],
]}
value={poolScope}
onChange={setPoolScope}
/>
}
/>
<DataTable
columns={poolBalanceColumns}
emptyLabel="暂无奖池余额"
items={poolBalances}
emptyLabel={poolScope === "history" ? "暂无历史奖池" : "暂无奖池余额"}
items={visiblePoolBalances}
minWidth="1480px"
rowKey={luckyGiftPoolKey}
title="奖池实时水位"
total={poolBalances.length}
total={visiblePoolBalances.length}
/>
<DataTable
@ -82,12 +116,16 @@ function buildPoolBalanceColumns({ canCredit, canDebit, disabled, onCredit, onDe
key: "materialized",
label: "水位状态",
// materialized=false owner owner
render: (item) =>
item.materialized === false ? (
render: (item) => {
if (item.is_historical) {
return <OpsStatusBadge label="历史奖池" tone="stopped" />;
}
return item.materialized === false ? (
<OpsStatusBadge label="默认水位" />
) : (
<OpsStatusBadge label="已入账" tone="succeeded" />
),
);
},
width: "minmax(132px, 0.9fr)",
},
{ key: "balance", label: "当前余额", render: (item) => formatNumber(item.balance) },