diff --git a/ops-center/src/api.js b/ops-center/src/api.js index 7ef47d5..1c3be33 100644 --- a/ops-center/src/api.js +++ b/ops-center/src/api.js @@ -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), diff --git a/ops-center/src/components/OverviewView.jsx b/ops-center/src/components/OverviewView.jsx index ed64516..5ee4c7d 100644 --- a/ops-center/src/components/OverviewView.jsx +++ b/ops-center/src/components/OverviewView.jsx @@ -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 (