import { CoinIcon, ActiveUsersIcon, CrownIcon, StarUserIcon, TrendIcon, UserPlusIcon } from "../components/MetricIcons.jsx"; import { countryFlag, resolveCountryMeta, stripCountryFlagPrefix } from "./countryMeta.js"; import { formatCoin, formatMoney, formatMoneyFull, formatNumber, formatPercent, formatWholeMoney } from "../utils/format.js"; import { numberValue, ratio } from "../utils/number.js"; import { formatDateTime, sameRange, todayRange } from "../utils/time.js"; export function createDashboardModel(overview, { appCode, countryId, range, timeZone }) { const source = overview || {}; const comparisonCaption = isTodayRange(range, timeZone) ? "与昨日相比" : "近 7 日"; const recharge = effectiveRechargeUSDMinor(source); const userRecharge = effectiveUserRechargeUSDMinor(source); const newUserRecharge = readNumber(source, "new_user_recharge_usd_minor", "newUserRechargeUsdMinor"); const newPaidUsers = readNumber(source, "new_paid_users", "newPaidUsers"); const activeUsers = readNumber(source, "active_users", "activeUsers"); const paidUsers = readNumber(source, "paid_users", "paidUsers"); const rechargeUsers = readNumber(source, "recharge_users", "rechargeUsers", "paid_users", "paidUsers"); const newUsers = readNumber(source, "new_users", "newUsers", "registrations", "registered_users", "registeredUsers"); const arpu = readNumber(source, "arpu_usd_minor", "arpuUsdMinor"); const arppu = readNumber(source, "arppu_usd_minor", "arppuUsdMinor"); const giftCoinSpent = readNumber(source, "gift_coin_spent", "giftCoinSpent"); const coinSellerTransferCoin = readNumber(source, "coin_seller_transfer_coin", "coinSellerTransferCoin"); const realRoomRobotNormalGiftCoin = readNumber(source, "real_room_robot_normal_gift_coin", "realRoomRobotNormalGiftCoin"); const realRoomRobotLuckyGiftCoin = readNumber(source, "real_room_robot_lucky_gift_coin", "realRoomRobotLuckyGiftCoin"); const realRoomRobotSuperGiftCoin = readNumber(source, "real_room_robot_super_gift_coin", "realRoomRobotSuperGiftCoin"); const realRoomRobotTotalGiftCoin = readNumber(source, "real_room_robot_total_gift_coin", "realRoomRobotTotalGiftCoin") || realRoomRobotNormalGiftCoin + realRoomRobotLuckyGiftCoin + realRoomRobotSuperGiftCoin; const realRoomRobotGiftRoomCount = readNumber(source, "real_room_robot_gift_room_count", "realRoomRobotGiftRoomCount"); const realRoomRobotAvgRoomGiftCoin = readNumber(source, "real_room_robot_avg_room_gift_coin", "realRoomRobotAvgRoomGiftCoin") || Math.round(ratio(realRoomRobotTotalGiftCoin, realRoomRobotGiftRoomCount)); const luckyGiftPools = normalizeLuckyGiftPools(source); const luckyGiftPoolTotals = summarizeLuckyGiftPools(luckyGiftPools); const luckyGiftTurnover = luckyGiftPools.length ? luckyGiftPoolTotals.turnover : readNumber(source, "room_lucky_gift_turnover", "roomLuckyGiftTurnover", "lucky_gift_turnover", "luckyGiftTurnover"); const luckyGiftPayout = luckyGiftPools.length ? luckyGiftPoolTotals.payout : readNumber(source, "lucky_gift_payout", "luckyGiftPayout"); const luckyGiftProfit = luckyGiftPools.length ? luckyGiftPoolTotals.profit : readNumber(source, "lucky_gift_profit", "luckyGiftProfit"); const gameTurnover = readNumber(source, "game_turnover", "gameTurnover"); const gameProfit = readNumber(source, "game_profit", "gameProfit"); const countryBreakdown = normalizeCountries(source, countryId); const dailyCountryBreakdown = normalizeDailyCountryBreakdown(source); const dailySeries = normalizeDailySeries(source); const revenueSeries = dailySeries.length ? dailySeries : normalizeRevenueSeries(source); const payoutDistribution = normalizeDistribution(source); const gameRanking = normalizeGameRanking(source); const reportCountryRows = buildReportCountryRows({ countryBreakdown, dailyCountryBreakdown, dailySeries, range, source }); return { appCode, businessKpis: [ coinMetric("礼物消费", giftCoinSpent, readDelta(source, "gift_coin_spent_delta_rate", "giftCoinSpentDeltaRate"), comparisonCaption, { trend: metricTrend(dailySeries, "gift_coin_spent", "礼物消费", "coin") }), coinMetric("币商出货金币", coinSellerTransferCoin, readDelta(source, "coin_seller_transfer_coin_delta_rate", "coinSellerTransferCoinDeltaRate"), comparisonCaption, { trend: metricTrend(dailySeries, "coin_seller_transfer_coin", "币商出货金币", "coin") }), coinMetric("幸运礼物流水", luckyGiftTurnover, readDelta(source, "room_lucky_gift_turnover_delta_rate", "lucky_gift_turnover_delta_rate", "roomLuckyGiftTurnoverDeltaRate", "luckyGiftTurnoverDeltaRate"), comparisonCaption, { detailKey: "luckyGiftPools", trend: metricTrend(dailySeries, "lucky_gift_turnover", "幸运礼物流水", "coin") }), coinMetric("幸运礼物返奖", luckyGiftPayout, readDelta(source, "lucky_gift_payout_delta_rate", "luckyGiftPayoutDeltaRate"), comparisonCaption, { detailKey: "luckyGiftPools", trend: metricTrend(dailySeries, "lucky_gift_payout", "幸运礼物返奖", "coin") }), coinMetric("幸运礼物利润", luckyGiftProfit, readDelta(source, "lucky_gift_profit_delta_rate", "luckyGiftProfitDeltaRate"), comparisonCaption, { detailKey: "luckyGiftPools", trend: metricTrend(dailySeries, "lucky_gift_profit", "幸运礼物利润", "coin") }), coinMetric("游戏流水", gameTurnover, readDelta(source, "game_turnover_delta_rate", "gameTurnoverDeltaRate"), comparisonCaption, { trend: metricTrend(dailySeries, "game_turnover", "游戏流水", "coin") }), coinMetric("游戏利润", gameProfit, readDelta(source, "game_profit_delta_rate", "gameProfitDeltaRate"), comparisonCaption, { trend: metricTrend(dailySeries, "game_profit", "游戏利润", "coin") }) ], countryBreakdown, dailyCountryBreakdown, funnel: [ { name: "活跃用户", rate: 1, value: activeUsers }, { name: "付费用户", rate: ratio(paidUsers, activeUsers), value: paidUsers }, { name: "充值用户", rate: ratio(rechargeUsers, activeUsers), value: rechargeUsers } ], gameMetrics: [ { caption: captionWithDelta(comparisonCaption, readDelta(source, "game_turnover_delta_rate", "gameTurnoverDeltaRate")), label: "流水", value: formatWholeMoney(source.game_turnover) }, { caption: captionWithDelta(comparisonCaption, readDelta(source, "game_payout_delta_rate", "gamePayoutDeltaRate")), label: "派奖", value: formatWholeMoney(source.game_payout) }, { caption: captionWithDelta(comparisonCaption, readDelta(source, "game_refund_delta_rate", "gameRefundDeltaRate")), label: "退款", value: formatWholeMoney(source.game_refund) }, { caption: captionWithDelta(comparisonCaption, readDelta(source, "game_profit_delta_rate", "gameProfitDeltaRate")), label: "利润", value: formatWholeMoney(source.game_profit) } ], gameRanking, giftRanking: normalizeGiftRanking(source), kpis: [ { caption: comparisonCaption, delta: readDelta(source, "recharge_delta_rate", "rechargeDeltaRate", "total_recharge_delta_rate", "totalRechargeDeltaRate"), icon: CoinIcon, label: "总充值", trend: metricTrend(dailySeries, "recharge_usd_minor", "总充值", "money"), trendFormat: "money", value: formatMoneyFull(recharge) }, { icon: UserPlusIcon, label: "用户充值 / 新用户充值", subMetrics: [ { caption: comparisonCaption, delta: readDelta(source, "user_recharge_delta_rate", "userRechargeDeltaRate", "recharge_delta_rate", "rechargeDeltaRate"), label: "用户充值", value: formatMoneyFull(userRecharge) }, { caption: comparisonCaption, delta: readDelta(source, "new_user_recharge_delta_rate", "newUserRechargeDeltaRate"), label: "新用户充值", value: formatMoneyFull(newUserRecharge) } ], trend: [ ...metricTrend(dailySeries, "user_recharge_usd_minor", "用户充值", "money"), ...metricTrend(dailySeries, "new_user_recharge_usd_minor", "新用户充值", "money") ], trendFormat: "money" }, { caption: comparisonCaption, delta: readDelta(source, "new_users_delta_rate", "newUsersDeltaRate"), icon: UserPlusIcon, label: "新增用户数", trend: metricTrend(dailySeries, "new_users", "新增用户数", "number"), trendFormat: "number", value: formatNumber(newUsers) }, { caption: comparisonCaption, delta: readDelta(source, "active_users_delta_rate", "activeUsersDeltaRate"), icon: ActiveUsersIcon, label: "活跃用户", trend: metricTrend(dailySeries, "active_users", "活跃用户", "number"), trendFormat: "number", value: formatNumber(activeUsers) }, { icon: CrownIcon, label: "付费用户 / 新增付费用户", subMetrics: [ { caption: comparisonCaption, delta: readDelta(source, "paid_users_delta_rate", "paidUsersDeltaRate"), label: "付费用户", value: formatNumber(paidUsers) }, { caption: comparisonCaption, delta: readDelta(source, "new_paid_users_delta_rate", "newPaidUsersDeltaRate"), label: "新增付费用户", value: formatNumber(newPaidUsers) } ], trend: [ ...metricTrend(dailySeries, "paid_users", "付费用户", "number"), ...metricTrend(dailySeries, "new_paid_users", "新增付费用户", "number") ], trendFormat: "number" }, { caption: comparisonCaption, delta: readDelta(source, "arpu_delta_rate", "arpuDeltaRate"), deltaTone: deltaTone(source, "arpu_delta_rate", "arpuDeltaRate"), icon: TrendIcon, label: "ARPU", trend: metricTrend(dailySeries, "arpu_usd_minor", "ARPU", "money"), trendFormat: "money", value: formatMoney(arpu) }, { caption: comparisonCaption, delta: readDelta(source, "arppu_delta_rate", "arppuDeltaRate"), icon: StarUserIcon, label: "ARPPU", trend: metricTrend(dailySeries, "arppu_usd_minor", "ARPPU", "money"), trendFormat: "money", value: formatMoney(arppu) } ], luckyMetrics: [ { caption: captionWithDelta(comparisonCaption, readDelta(source, "lucky_gift_turnover_delta_rate", "luckyGiftTurnoverDeltaRate")), label: "流水", value: formatWholeMoney(luckyGiftTurnover) }, { caption: captionWithDelta(comparisonCaption, readDelta(source, "lucky_gift_payout_delta_rate", "luckyGiftPayoutDeltaRate")), label: "返奖", value: formatWholeMoney(luckyGiftPayout) }, { caption: captionWithDelta(comparisonCaption, readDelta(source, "lucky_gift_profit_delta_rate", "luckyGiftProfitDeltaRate")), label: "利润", value: formatWholeMoney(luckyGiftProfit) }, { caption: captionWithDelta(comparisonCaption, readDelta(source, "lucky_gift_payout_rate_delta_pp", "luckyGiftPayoutRateDeltaPp"), "pp"), label: "返奖率", value: formatPercent(source.lucky_gift_payout_rate) }, { caption: captionWithDelta(comparisonCaption, readDelta(source, "lucky_gift_profit_rate_delta_pp", "luckyGiftProfitRateDeltaPp"), "pp"), label: "利润率", value: formatPercent(source.lucky_gift_profit_rate) } ], luckyGiftPools, payoutDistribution, reportCountryRows, reportMetricSources: Array.isArray(source.report_metric_sources) ? source.report_metric_sources : [], revenueSeries, robotGiftKpis: [ coinMetric("真人房机器人普通礼物", realRoomRobotNormalGiftCoin, "", "当前筛选", { trend: metricTrend(dailySeries, "real_room_robot_normal_gift_coin", "真人房机器人普通礼物", "coin") }), coinMetric("真人房机器人幸运礼物", realRoomRobotLuckyGiftCoin, "", "当前筛选", { trend: metricTrend(dailySeries, "real_room_robot_lucky_gift_coin", "真人房机器人幸运礼物", "coin") }), coinMetric("真人房机器人超级幸运礼物", realRoomRobotSuperGiftCoin, "", "当前筛选", { trend: metricTrend(dailySeries, "real_room_robot_super_gift_coin", "真人房机器人超级幸运礼物", "coin") }), coinMetric("真人房机器人房均礼物", realRoomRobotAvgRoomGiftCoin, "", `${formatNumber(realRoomRobotGiftRoomCount)} 个房间`, { trend: metricTrend(dailySeries, "real_room_robot_avg_room_gift_coin", "真人房机器人房均礼物", "coin") }) ], roomMetrics: [ { caption: captionWithDelta(comparisonCaption, readDelta(source, "gift_coin_spent_delta_rate", "giftCoinSpentDeltaRate")), label: "礼物消费(充值)", value: formatWholeMoney(source.gift_coin_spent) }, { caption: captionWithDelta(comparisonCaption, readDelta(source, "room_lucky_gift_turnover_delta_rate", "lucky_gift_turnover_delta_rate", "roomLuckyGiftTurnoverDeltaRate", "luckyGiftTurnoverDeltaRate")), label: "幸运礼物流水", value: formatWholeMoney(source.room_lucky_gift_turnover ?? source.lucky_gift_turnover) } ], sideMetrics: [ { caption: captionWithDelta(comparisonCaption, readDelta(source, "arpu_delta_rate", "arpuDeltaRate")), deltaTone: deltaTone(source, "arpu_delta_rate", "arpuDeltaRate"), label: "ARPU", value: formatMoney(arpu) }, { caption: captionWithDelta(comparisonCaption, readDelta(source, "payer_rate_delta_pp", "payerRateDeltaPp"), "pp"), label: "付费转化率", value: formatPercent(ratio(paidUsers, activeUsers)) }, { caption: captionWithDelta(comparisonCaption, readDelta(source, "arppu_delta_rate", "arppuDeltaRate")), label: "ARPPU", value: formatMoney(arppu) } ], topCountries: countryBreakdown.slice(0, 4), updatedAt: formatDateTime(source.updated_at_ms || source.updatedAtMs || Date.now(), timeZone) }; } function coinMetric(label, value, delta, caption, extra = {}) { return { caption, delta, label, unit: "金币", unitIcon: CoinIcon, value: formatCoin(value), ...extra }; } function captionWithDelta(label, delta) { return `${label} ${delta}`; } function readNumber(source, ...keys) { for (const key of keys) { if (source?.[key] !== undefined && source[key] !== null) { return numberValue(source[key]); } } return 0; } function readOptionalNumber(source, ...keys) { for (const key of keys) { if (source?.[key] !== undefined && source[key] !== null) { return numberValue(source[key]); } } return null; } function readDelta(source, ...keys) { const unit = keys[keys.length - 1] === "pp" ? keys.pop() : "%"; for (const key of keys) { if (source?.[key] !== undefined && source[key] !== null && source[key] !== "") { return formatDelta(source[key], unit); } } return ""; } function deltaTone(source, ...keys) { for (const key of keys) { if (source?.[key] !== undefined && source[key] !== null && source[key] !== "") { return numberValue(source[key]) < 0 ? "down" : "up"; } } return "up"; } function formatDelta(value, unit) { const numeric = numberValue(value); const normalized = unit === "%" && Math.abs(numeric) <= 1 ? numeric * 100 : numeric; const prefix = normalized > 0 ? "+" : ""; return `${prefix}${normalized.toFixed(2)}${unit}`; } function isTodayRange(range, timeZone) { return sameRange(range, todayRange(timeZone)); } function normalizeRevenueSeries(source) { if (!source || !Object.keys(source).length) { return []; } if (Array.isArray(source.daily_series) && source.daily_series.length) { return source.daily_series.map((item) => { const coinSeller = numberValue(item.coin_seller ?? item.coin_seller_recharge_usd_minor ?? item.coinSellerRechargeUsdMinor); const google = numberValue(item.google ?? item.google_recharge_usd_minor ?? item.googleRechargeUsdMinor); const mifapay = numberValue(item.mifapay ?? item.mifa_pay ?? item.mifapay_recharge_usd_minor ?? item.mifaPayRechargeUsdMinor); const channelTotal = google + mifapay + coinSeller; const total = Math.max(numberValue(item.total ?? item.recharge_usd_minor), channelTotal); return { coin_seller: coinSeller, google, label: localizeSeriesLabel(item.label || item.stat_day || item.day || "-"), lineTotal: Math.max(numberValue(item.line_total ?? item.lineTotal ?? item.trend_total ?? item.trendTotal ?? total), channelTotal), mifapay, total }; }); } const coinSeller = readNumber(source, "coin_seller_recharge_usd_minor", "coinSellerRechargeUsdMinor"); const google = numberValue(source.google_recharge_usd_minor ?? source.googleRechargeUsdMinor); const mifapay = numberValue(source.mifapay_recharge_usd_minor ?? source.mifaPayRechargeUsdMinor); const total = effectiveRechargeUSDMinor(source); return [ { coin_seller: coinSeller, google, label: "当前", lineTotal: total, mifapay, total } ]; } function normalizeCountries(source, countryId) { const raw = Array.isArray(source.country_breakdown) && source.country_breakdown.length ? source.country_breakdown : null; const rows = raw || (countryId ? [{ active_users: source.active_users, arppu_usd_minor: source.arppu_usd_minor, country: `国家 ${countryId}`, paid_users: source.paid_users, recharge_usd_minor: source.recharge_usd_minor, x: 56, y: 52 }] : []); const totalRecharge = rows.reduce((sum, item) => sum + effectiveRechargeUSDMinor(item), 0) || 1; return rows .map((item, index) => ({ ...normalizeCountryRow(item, index, totalRecharge) })) .sort((left, right) => right.recharge_usd_minor - left.recharge_usd_minor); } function normalizeDailyCountryBreakdown(source) { const rows = firstArray(source?.daily_country_breakdown, source?.dailyCountryBreakdown); if (!rows.length) { return []; } const totalRecharge = rows.reduce((sum, item) => sum + effectiveRechargeUSDMinor(item), 0) || 1; return rows.map((item, index) => ({ ...normalizeCountryRow(item, index, totalRecharge), date_label: item.label || item.stat_day || item.statDay || item.day || "", stat_day: item.stat_day || item.statDay || item.day || "" })); } function buildReportCountryRows({ countryBreakdown, dailyCountryBreakdown, dailySeries, range, source }) { const totalRow = createReportRow(normalizeCountryRow({ ...source, country: "总计" }, 0, effectiveRechargeUSDMinor(source) || 1), { country: "总计", date_label: "全部", row_type: "total" }); if (!isMultiDayRange(range)) { return [ totalRow, ...countryBreakdown.map((row) => createReportRow(row, { date_label: range?.start || row.stat_day || "", row_type: "country" })) ]; } const dailyRowsByDay = groupRowsByDay(dailyCountryBreakdown); const dailySeriesByDay = new Map(dailySeries.map((item) => [item.stat_day || item.label, item])); const dayKeys = Array.from(new Set([ ...dailySeries.map((item) => item.stat_day || item.label).filter(Boolean), ...dailyCountryBreakdown.map((item) => item.stat_day).filter(Boolean) ])).sort(); if (!dayKeys.length) { return [ totalRow, ...countryBreakdown.map((row) => createReportRow(row, { date_label: "汇总", row_type: "country" })) ]; } const rows = [totalRow]; for (const day of dayKeys) { const dayCountries = dailyRowsByDay.get(day) || []; const dayTotalSource = { ...summarizeReportRows(dayCountries), ...(dailySeriesByDay.get(day) || {}) }; const childRows = dayCountries.map((row) => createReportRow(row, { date_label: day, row_type: "country", stat_day: day })); rows.push(createReportRow(normalizeCountryRow({ ...dayTotalSource, country: "总计" }, rows.length, effectiveRechargeUSDMinor(dayTotalSource) || 1), { children: childRows, country: "总计", date_label: day, row_type: "date_total", stat_day: day })); } return rows; } function createReportRow(row, extra) { return { ...row, ...extra, flag: extra.row_type === "country" ? row.flag : "", row_id: [ extra.row_type, extra.stat_day || extra.date_label || "", row.country_id || row.country || "", row.region_id || "" ].join(":") }; } function groupRowsByDay(rows) { return rows.reduce((groups, row) => { const day = row.stat_day || row.date_label || ""; if (!day) { return groups; } if (!groups.has(day)) { groups.set(day, []); } groups.get(day).push(row); return groups; }, new Map()); } function summarizeReportRows(rows) { const fields = [ "active_users", "coin_seller_recharge_usd_minor", "coin_seller_stock_coin", "coin_seller_transfer_coin", "game_payout", "game_refund", "game_turnover", "gift_coin_spent", "google_recharge_usd_minor", "lucky_gift_payout", "lucky_gift_turnover", "mifapay_recharge_usd_minor", "new_user_recharge_usd_minor", "paid_users", "recharge_usd_minor", "recharge_users", "registrations", "salary_transfer_coin", "super_lucky_gift_payout", "super_lucky_gift_turnover" ]; return fields.reduce((summary, key) => { summary[key] = rows.reduce((sum, row) => sum + numberValue(row[key]), 0); return summary; }, {}); } function isMultiDayRange(range) { return Boolean(range?.start && range?.end && range.start !== range.end); } function normalizeCountryRow(item, index, totalRecharge) { const code = item.country_code || item.countryCode || item.iso_code || item.isoCode; const countryId = numberValue(item.country_id ?? item.countryId); const country = stripCountryFlagPrefix(item.country || item.country_name || code || (countryId > 0 ? `国家 ${countryId || index + 1}` : "未知国家")); const meta = resolveCountryMeta({ code, name: country }); const recharge = effectiveRechargeUSDMinor(item); const activeUsers = numberValue(item.active_users); const paidUsers = numberValue(item.paid_users); const rechargeUsers = numberValue(item.recharge_users ?? item.rechargeUsers ?? item.paid_users ?? item.paidUsers); const gameTurnover = readNumber(item, "game_turnover", "gameTurnover"); const gamePayout = readNumber(item, "game_payout", "gamePayout"); const gameRefund = readNumber(item, "game_refund", "gameRefund"); const gameProfit = readOptionalNumber(item, "game_profit", "gameProfit") ?? (gameTurnover - gamePayout - gameRefund); const luckyGiftTurnover = readNumber(item, "lucky_gift_turnover", "luckyGiftTurnover", "room_lucky_gift_turnover", "roomLuckyGiftTurnover"); const luckyGiftPayout = readNumber(item, "lucky_gift_payout", "luckyGiftPayout"); const luckyGiftProfit = readOptionalNumber(item, "lucky_gift_profit", "luckyGiftProfit") ?? (luckyGiftTurnover - luckyGiftPayout); const superLuckyGiftTurnover = readOptionalNumber(item, "super_lucky_gift_turnover", "superLuckyGiftTurnover"); const superLuckyGiftPayout = readOptionalNumber(item, "super_lucky_gift_payout", "superLuckyGiftPayout"); const superLuckyGiftProfit = readOptionalNumber(item, "super_lucky_gift_profit", "superLuckyGiftProfit") ?? (superLuckyGiftTurnover === null || superLuckyGiftPayout === null ? null : superLuckyGiftTurnover - superLuckyGiftPayout); return { active_users: activeUsers, arpu_usd_minor: numberValue(item.arpu_usd_minor ?? item.arpuUsdMinor), arppu_usd_minor: numberValue(item.arppu_usd_minor), avg_recharge_usd_minor: numberValue(item.avg_recharge_usd_minor ?? item.avgRechargeUsdMinor), avg_mic_online_ms: readOptionalNumber(item, "avg_mic_online_ms", "avgMicOnlineMs"), coin_seller_recharge_usd_minor: readNumber(item, "coin_seller_recharge_usd_minor", "coinSellerRechargeUsdMinor"), coin_seller_stock_coin: readOptionalNumber(item, "coin_seller_stock_coin", "coinSellerStockCoin"), coin_seller_transfer_coin: numberValue(item.coin_seller_transfer_coin ?? item.coinSellerTransferCoin), coin_total: readOptionalNumber(item, "coin_total", "coinTotal"), consumed_coin: readOptionalNumber(item, "consumed_coin", "consumedCoin"), country: meta?.label || country, country_id: countryId, country_code: code || meta?.code || "", flag: countryFlag(code || meta?.code), game_payout: gamePayout, game_profit: gameProfit, game_profit_rate: readOptionalNumber(item, "game_profit_rate", "gameProfitRate") ?? ratio(gameProfit, gameTurnover), game_refund: gameRefund, game_turnover: gameTurnover, geo_point: Array.isArray(item.geo_point) ? item.geo_point : meta?.point, gift_coin_spent: readNumber(item, "gift_coin_spent", "giftCoinSpent"), google_recharge_usd_minor: readNumber(item, "google_recharge_usd_minor", "googleRechargeUsdMinor"), lucky_gift_payout: luckyGiftPayout, lucky_gift_profit: luckyGiftProfit, lucky_gift_profit_rate: readOptionalNumber(item, "lucky_gift_profit_rate", "luckyGiftProfitRate") ?? ratio(luckyGiftProfit, luckyGiftTurnover), lucky_gift_turnover: luckyGiftTurnover, manual_grant_coin: readOptionalNumber(item, "manual_grant_coin", "manualGrantCoin"), mic_online_ms: readOptionalNumber(item, "mic_online_ms", "micOnlineMs"), mifapay_recharge_usd_minor: readNumber(item, "mifapay_recharge_usd_minor", "mifapayRechargeUsdMinor", "mifa_pay_recharge_usd_minor", "mifaPayRechargeUsdMinor"), new_user_recharge_usd_minor: numberValue(item.new_user_recharge_usd_minor ?? item.newUserRechargeUsdMinor), paid_users: paidUsers, payer_rate: ratio(paidUsers, activeUsers), platform_grant_coin: readOptionalNumber(item, "platform_grant_coin", "platformGrantCoin"), recharge_usd_minor: recharge, recharge_conversion_rate: item.recharge_conversion_rate !== undefined || item.rechargeConversionRate !== undefined ? numberValue(item.recharge_conversion_rate ?? item.rechargeConversionRate) : ratio(rechargeUsers, activeUsers), recharge_users: rechargeUsers, salary_usd_minor: readOptionalNumber(item, "salary_usd_minor", "salaryUsdMinor"), salary_transfer_coin: readOptionalNumber(item, "salary_transfer_coin", "salaryTransferCoin"), share: recharge / totalRecharge, registrations: numberValue(item.new_users ?? item.newUsers ?? item.registrations ?? item.registered_users ?? item.registeredUsers), super_lucky_gift_payout: superLuckyGiftPayout, super_lucky_gift_profit: superLuckyGiftProfit, super_lucky_gift_profit_rate: readOptionalNumber(item, "super_lucky_gift_profit_rate", "superLuckyGiftProfitRate") ?? ratio(superLuckyGiftProfit, superLuckyGiftTurnover), super_lucky_gift_turnover: superLuckyGiftTurnover, trend_rate: numberValue(item.trend_rate ?? item.trendRate) }; } function effectiveRechargeUSDMinor(source) { const direct = readNumber(source, "recharge_usd_minor", "rechargeUsdMinor", "total_recharge_usd_minor", "totalRechargeUsdMinor"); const channelTotal = readNumber(source, "mifapay_recharge_usd_minor", "mifapayRechargeUsdMinor", "mifa_pay_recharge_usd_minor", "mifaPayRechargeUsdMinor") + readNumber(source, "google_recharge_usd_minor", "googleRechargeUsdMinor") + readNumber(source, "coin_seller_recharge_usd_minor", "coinSellerRechargeUsdMinor"); return Math.max(direct, channelTotal); } function effectiveUserRechargeUSDMinor(source) { const google = readNumber(source, "google_recharge_usd_minor", "googleRechargeUsdMinor"); const mifapay = readNumber(source, "mifapay_recharge_usd_minor", "mifapayRechargeUsdMinor", "mifa_pay_recharge_usd_minor", "mifaPayRechargeUsdMinor"); const channelUserRecharge = google + mifapay; const direct = readNumber(source, "user_recharge_usd_minor", "userRechargeUsdMinor", "recharge_usd_minor", "rechargeUsdMinor"); const coinSeller = readNumber(source, "coin_seller_recharge_usd_minor", "coinSellerRechargeUsdMinor"); return Math.max(channelUserRecharge, Math.max(0, direct - coinSeller)); } function normalizeDailySeries(source) { if (!Array.isArray(source?.daily_series) || !source.daily_series.length) { return []; } return source.daily_series.map((item) => { const google = readNumber(item, "google_recharge_usd_minor", "googleRechargeUsdMinor", "google"); const mifapay = readNumber(item, "mifapay_recharge_usd_minor", "mifapayRechargeUsdMinor", "mifa_pay_recharge_usd_minor", "mifaPayRechargeUsdMinor", "mifapay"); const coinSeller = readNumber(item, "coin_seller_recharge_usd_minor", "coinSellerRechargeUsdMinor", "coin_seller"); const channelTotal = google + mifapay + coinSeller; const recharge = Math.max(effectiveRechargeUSDMinor(item), channelTotal); const userRecharge = Math.max(google + mifapay, Math.max(0, readNumber(item, "user_recharge_usd_minor", "userRechargeUsdMinor", "recharge_usd_minor", "rechargeUsdMinor") - coinSeller)); const gameTurnover = readNumber(item, "game_turnover", "gameTurnover"); const gameProfit = readNumber(item, "game_profit", "gameProfit"); const luckyGiftTurnover = readNumber(item, "lucky_gift_turnover", "luckyGiftTurnover", "room_lucky_gift_turnover", "roomLuckyGiftTurnover"); const luckyGiftPayout = readNumber(item, "lucky_gift_payout", "luckyGiftPayout"); return { active_users: readNumber(item, "active_users", "activeUsers"), arppu_usd_minor: readNumber(item, "arppu_usd_minor", "arppuUsdMinor"), arpu_usd_minor: readNumber(item, "arpu_usd_minor", "arpuUsdMinor"), coin_seller: coinSeller, coin_seller_recharge_usd_minor: coinSeller, coin_seller_stock_coin: readOptionalNumber(item, "coin_seller_stock_coin", "coinSellerStockCoin"), coin_seller_transfer_coin: readNumber(item, "coin_seller_transfer_coin", "coinSellerTransferCoin"), game_profit: gameProfit, game_turnover: gameTurnover, gift_coin_spent: readNumber(item, "gift_coin_spent", "giftCoinSpent"), google, label: localizeSeriesLabel(item.label || item.stat_day || item.day || "-"), lineTotal: Math.max(readNumber(item, "line_total", "lineTotal", "trend_total", "trendTotal", "total"), recharge, channelTotal), lucky_gift_payout: luckyGiftPayout, lucky_gift_profit: readNumber(item, "lucky_gift_profit", "luckyGiftProfit") || (luckyGiftTurnover - luckyGiftPayout), lucky_gift_turnover: luckyGiftTurnover, mifapay, new_paid_users: readNumber(item, "new_paid_users", "newPaidUsers"), new_user_recharge_usd_minor: readNumber(item, "new_user_recharge_usd_minor", "newUserRechargeUsdMinor"), new_users: readNumber(item, "new_users", "newUsers"), paid_users: readNumber(item, "paid_users", "paidUsers"), recharge_usd_minor: recharge, recharge_users: readNumber(item, "recharge_users", "rechargeUsers", "paid_users", "paidUsers"), salary_transfer_coin: readOptionalNumber(item, "salary_transfer_coin", "salaryTransferCoin"), stat_day: item.stat_day || item.statDay || item.day || item.label || "", total: recharge, user_recharge_usd_minor: userRecharge }; }); } function metricTrend(series, key, name, format = "number") { const data = series.map((item) => ({ label: item.label || item.stat_day || "-", value: format === "money" ? numberValue(item[key]) / 100 : numberValue(item[key]) })); return data.length ? [{ data, name }] : []; } function normalizeGiftRanking(source) { if (Array.isArray(source.gift_ranking) && source.gift_ranking.length) { return source.gift_ranking.map((item) => ({ name: item.name || item.gift_name || item.gift_id || "-", trend_rate: numberValue(item.trend_rate ?? item.trendRate), value: numberValue(item.value ?? item.amount ?? item.coin) })); } return []; } function normalizeDistribution(source) { if (Array.isArray(source.payout_distribution) && source.payout_distribution.length) { return source.payout_distribution.map((item) => ({ name: item.name || "-", value: numberValue(item.value) })); } if (!source || !Object.keys(source).length) { return []; } return [ { name: "幸运礼物返奖", value: numberValue(source.lucky_gift_payout) }, { name: "幸运礼物利润", value: Math.max(numberValue(source.lucky_gift_profit), 0) } ]; } function normalizeLuckyGiftPools(source) { const raw = firstArray(source?.lucky_gift_pools, source?.luckyGiftPools, source?.lucky_gift_pool_stats, source?.luckyGiftPoolStats, source?.pool_breakdown); return raw .map((item) => { const turnover = readNumber(item, "turnover_coin", "turnoverCoin", "turnover", "total_spent_coins", "totalSpentCoins", "lucky_gift_turnover", "luckyGiftTurnover"); const payout = readNumber(item, "payout_coin", "payoutCoin", "payout", "total_reward_coins", "totalRewardCoins", "lucky_gift_payout", "luckyGiftPayout"); const rawProfit = item.profit_coin ?? item.profitCoin ?? item.profit ?? item.lucky_gift_profit ?? item.luckyGiftProfit; const profit = rawProfit === undefined || rawProfit === null ? turnover - payout : numberValue(rawProfit); return { payout, payout_rate: numberValue(item.payout_rate ?? item.payoutRate ?? ratio(payout, turnover)), pool_id: String(item.pool_id ?? item.poolId ?? item.id ?? "-").trim() || "-", profit, profit_rate: numberValue(item.profit_rate ?? item.profitRate ?? ratio(profit, turnover)), turnover }; }) .sort((left, right) => right.turnover - left.turnover || left.pool_id.localeCompare(right.pool_id)); } function summarizeLuckyGiftPools(items) { return items.reduce((summary, item) => ({ payout: summary.payout + item.payout, profit: summary.profit + item.profit, turnover: summary.turnover + item.turnover }), { payout: 0, profit: 0, turnover: 0 }); } function firstArray(...values) { return values.find((value) => Array.isArray(value)) || []; } function normalizeGameRanking(source) { if (Array.isArray(source.game_ranking) && source.game_ranking.length) { return source.game_ranking.map((item) => ({ game_id: item.game_id || item.gameId || "-", game_label: gameDisplayLabel(item), game_name: item.game_name || item.gameName || item.name || "", platform_code: item.platform_code || item.platformCode || "", profit_rate: numberValue(item.profit_rate ?? item.profitRate), turnover_coin: numberValue(item.turnover_coin ?? item.turnoverCoin) })); } return []; } function gameDisplayLabel(item) { const id = String(item.game_id || item.gameId || "").trim(); const name = String(item.game_name || item.gameName || item.name || "").trim(); if (name && id && name !== id) { return `${name}(${id})`; } return name || id || "-"; } function localizeSeriesLabel(label) { const match = String(label).match(/^May\s+(\d{1,2})$/i); if (match) { return `5月${match[1]}日`; } return label; }