更新数据大屏金币统计列

This commit is contained in:
zhx 2026-06-30 14:41:06 +08:00
parent f5e1a20dd9
commit a952b4262e
4 changed files with 48 additions and 3 deletions

View File

@ -33,8 +33,10 @@ const countryColumns = [
{ key: "lucky_gift_turnover", label: "幸运礼物流水/利润率", render: (row) => <FlowRateCell rate={row.lucky_gift_profit_rate} value={row.lucky_gift_turnover} />, sortable: false }, { key: "lucky_gift_turnover", label: "幸运礼物流水/利润率", render: (row) => <FlowRateCell rate={row.lucky_gift_profit_rate} value={row.lucky_gift_turnover} />, sortable: false },
{ key: "super_lucky_gift_turnover", label: "超级幸运礼物流水/利润率", render: (row) => <FlowRateCell rate={row.super_lucky_gift_profit_rate} value={row.super_lucky_gift_turnover} />, sortable: false }, { key: "super_lucky_gift_turnover", label: "超级幸运礼物流水/利润率", render: (row) => <FlowRateCell rate={row.super_lucky_gift_profit_rate} value={row.super_lucky_gift_turnover} />, sortable: false },
{ key: "gift_coin_spent", label: "礼物流水", render: (row) => formatOptionalCoin(row.gift_coin_spent), sortable: false }, { key: "gift_coin_spent", label: "礼物流水", render: (row) => formatOptionalCoin(row.gift_coin_spent), sortable: false },
{ key: "coin_total", label: "金币数量(总和)", render: (row) => formatOptionalCoin(row.coin_total), sortable: false }, { key: "coin_total", label: "用户金币数", render: (row) => formatOptionalCoin(row.coin_total), sortable: false },
{ key: "consumed_coin", label: "消耗金币", render: (row) => formatOptionalCoin(row.consumed_coin), sortable: false }, { key: "consumed_coin", label: "消耗金币", render: (row) => formatOptionalCoin(row.consumed_coin), sortable: false },
{ key: "output_coin", label: "产出金币", render: (row) => formatOptionalCoin(row.output_coin), sortable: false },
{ key: "consume_output_ratio", label: "消耗产出比", render: (row) => <ConsumeOutputCell row={row} />, sortable: false },
{ key: "platform_grant_coin", label: "平台发放金币", render: (row, context) => <PlatformGrantCoinCell context={context} row={row} />, sortable: false }, { key: "platform_grant_coin", label: "平台发放金币", render: (row, context) => <PlatformGrantCoinCell context={context} row={row} />, sortable: false },
{ key: "manual_grant_coin", label: "人工发放金币", render: (row) => formatOptionalCoin(row.manual_grant_coin), sortable: false }, { key: "manual_grant_coin", label: "人工发放金币", render: (row) => formatOptionalCoin(row.manual_grant_coin), sortable: false },
{ key: "salary_usd_minor", label: "工资总和(主播/代理)", render: (row) => formatOptionalMoney(row.salary_usd_minor), sortable: false }, { key: "salary_usd_minor", label: "工资总和(主播/代理)", render: (row) => formatOptionalMoney(row.salary_usd_minor), sortable: false },
@ -736,6 +738,18 @@ function FlowRateCell({ rate, value }) {
); );
} }
function ConsumeOutputCell({ row }) {
if (isMissing(row.consume_output_ratio) && isMissing(row.consume_output_delta)) {
return <span className="report-placeholder">--</span>;
}
return (
<span className="report-pair-cell">
<b>{formatOptionalPercent(row.consume_output_ratio)}</b>
<small>差额 {formatOptionalCoin(row.consume_output_delta)}</small>
</span>
);
}
function TrendValue({ value }) { function TrendValue({ value }) {
return <span className={Number(value) >= 0 ? "report-trend-up" : "report-trend-down"}>{formatSignedPercent(value)}</span>; return <span className={Number(value) >= 0 ? "report-trend-up" : "report-trend-down"}>{formatSignedPercent(value)}</span>;
} }

View File

@ -59,6 +59,10 @@ test("shows daily totals first and expands daily country details on demand", asy
const stickyDateHeader = container.querySelector(".report-table--wide th.is-sticky-left"); const stickyDateHeader = container.querySelector(".report-table--wide th.is-sticky-left");
expect(stickyDateHeader).toHaveTextContent("日期"); expect(stickyDateHeader).toHaveTextContent("日期");
expect(screen.getByRole("columnheader", { name: "用户金币数" })).toBeInTheDocument();
expect(screen.getByRole("columnheader", { name: "产出金币" })).toBeInTheDocument();
expect(screen.getByRole("columnheader", { name: "消耗产出比" })).toBeInTheDocument();
expect(screen.queryByRole("columnheader", { name: "金币数量(总和)" })).not.toBeInTheDocument();
expect(container.querySelector(".report-table--wide td.is-sticky-left .report-date-cell")).toBeTruthy(); expect(container.querySelector(".report-table--wide td.is-sticky-left .report-date-cell")).toBeTruthy();
const june5Row = screen.getByRole("button", { name: "展开 2026-06-05 国家明细" }); const june5Row = screen.getByRole("button", { name: "展开 2026-06-05 国家明细" });

View File

@ -339,6 +339,7 @@ function summarizeReportRows(rows) {
"coin_seller_transfer_coin", "coin_seller_transfer_coin",
"coin_total", "coin_total",
"consumed_coin", "consumed_coin",
"output_coin",
"game_payout", "game_payout",
"game_refund", "game_refund",
"game_turnover", "game_turnover",
@ -398,6 +399,12 @@ function normalizeCountryRow(item, index, totalRecharge) {
const micOnlineUsers = readOptionalNumber(item, "mic_online_users", "micOnlineUsers"); const micOnlineUsers = readOptionalNumber(item, "mic_online_users", "micOnlineUsers");
const avgMicOnlineMS = readOptionalNumber(item, "avg_mic_online_ms", "avgMicOnlineMs") ?? const avgMicOnlineMS = readOptionalNumber(item, "avg_mic_online_ms", "avgMicOnlineMs") ??
(micOnlineMS === null || micOnlineUsers === null ? null : Math.round(ratio(micOnlineMS, micOnlineUsers))); (micOnlineMS === null || micOnlineUsers === null ? null : Math.round(ratio(micOnlineMS, micOnlineUsers)));
const consumedCoin = readOptionalNumber(item, "consumed_coin", "consumedCoin");
const outputCoin = readOptionalNumber(item, "output_coin", "outputCoin");
const consumeOutputRatio = readOptionalNumber(item, "consume_output_ratio", "consumeOutputRatio") ??
(consumedCoin === null || outputCoin === null ? null : ratio(outputCoin, consumedCoin));
const consumeOutputDelta = readOptionalNumber(item, "consume_output_delta", "consumeOutputDelta") ??
(consumedCoin === null || outputCoin === null ? null : consumedCoin - outputCoin);
return { return {
active_users: activeUsers, active_users: activeUsers,
arpu_usd_minor: numberValue(item.arpu_usd_minor ?? item.arpuUsdMinor), arpu_usd_minor: numberValue(item.arpu_usd_minor ?? item.arpuUsdMinor),
@ -408,7 +415,9 @@ function normalizeCountryRow(item, index, totalRecharge) {
coin_seller_stock_coin: readOptionalNumber(item, "coin_seller_stock_coin", "coinSellerStockCoin"), coin_seller_stock_coin: readOptionalNumber(item, "coin_seller_stock_coin", "coinSellerStockCoin"),
coin_seller_transfer_coin: numberValue(item.coin_seller_transfer_coin ?? item.coinSellerTransferCoin), coin_seller_transfer_coin: numberValue(item.coin_seller_transfer_coin ?? item.coinSellerTransferCoin),
coin_total: readOptionalNumber(item, "coin_total", "coinTotal"), coin_total: readOptionalNumber(item, "coin_total", "coinTotal"),
consumed_coin: readOptionalNumber(item, "consumed_coin", "consumedCoin"), consumed_coin: consumedCoin,
consume_output_delta: consumeOutputDelta,
consume_output_ratio: consumeOutputRatio,
country: meta?.label || country, country: meta?.label || country,
country_id: countryId, country_id: countryId,
country_code: code || meta?.code || "", country_code: code || meta?.code || "",
@ -432,6 +441,7 @@ function normalizeCountryRow(item, index, totalRecharge) {
new_user_recharge_usd_minor: numberValue(item.new_user_recharge_usd_minor ?? item.newUserRechargeUsdMinor), new_user_recharge_usd_minor: numberValue(item.new_user_recharge_usd_minor ?? item.newUserRechargeUsdMinor),
paid_users: paidUsers, paid_users: paidUsers,
payer_rate: ratio(paidUsers, activeUsers), payer_rate: ratio(paidUsers, activeUsers),
output_coin: outputCoin,
platform_grant_coin: readOptionalNumber(item, "platform_grant_coin", "platformGrantCoin"), platform_grant_coin: readOptionalNumber(item, "platform_grant_coin", "platformGrantCoin"),
recharge_usd_minor: recharge, recharge_usd_minor: recharge,
recharge_conversion_rate: item.recharge_conversion_rate !== undefined || item.rechargeConversionRate !== undefined recharge_conversion_rate: item.recharge_conversion_rate !== undefined || item.rechargeConversionRate !== undefined
@ -487,6 +497,12 @@ function normalizeDailySeries(source) {
const micOnlineUsers = readOptionalNumber(item, "mic_online_users", "micOnlineUsers"); const micOnlineUsers = readOptionalNumber(item, "mic_online_users", "micOnlineUsers");
const avgMicOnlineMS = readOptionalNumber(item, "avg_mic_online_ms", "avgMicOnlineMs") ?? const avgMicOnlineMS = readOptionalNumber(item, "avg_mic_online_ms", "avgMicOnlineMs") ??
(micOnlineMS === null || micOnlineUsers === null ? null : Math.round(ratio(micOnlineMS, micOnlineUsers))); (micOnlineMS === null || micOnlineUsers === null ? null : Math.round(ratio(micOnlineMS, micOnlineUsers)));
const consumedCoin = readOptionalNumber(item, "consumed_coin", "consumedCoin");
const outputCoin = readOptionalNumber(item, "output_coin", "outputCoin");
const consumeOutputRatio = readOptionalNumber(item, "consume_output_ratio", "consumeOutputRatio") ??
(consumedCoin === null || outputCoin === null ? null : ratio(outputCoin, consumedCoin));
const consumeOutputDelta = readOptionalNumber(item, "consume_output_delta", "consumeOutputDelta") ??
(consumedCoin === null || outputCoin === null ? null : consumedCoin - outputCoin);
return { return {
active_users: readNumber(item, "active_users", "activeUsers"), active_users: readNumber(item, "active_users", "activeUsers"),
arppu_usd_minor: readNumber(item, "arppu_usd_minor", "arppuUsdMinor"), arppu_usd_minor: readNumber(item, "arppu_usd_minor", "arppuUsdMinor"),
@ -497,7 +513,9 @@ function normalizeDailySeries(source) {
coin_seller_stock_coin: readOptionalNumber(item, "coin_seller_stock_coin", "coinSellerStockCoin"), coin_seller_stock_coin: readOptionalNumber(item, "coin_seller_stock_coin", "coinSellerStockCoin"),
coin_seller_transfer_coin: readNumber(item, "coin_seller_transfer_coin", "coinSellerTransferCoin"), coin_seller_transfer_coin: readNumber(item, "coin_seller_transfer_coin", "coinSellerTransferCoin"),
coin_total: readOptionalNumber(item, "coin_total", "coinTotal"), coin_total: readOptionalNumber(item, "coin_total", "coinTotal"),
consumed_coin: readOptionalNumber(item, "consumed_coin", "consumedCoin"), consumed_coin: consumedCoin,
consume_output_delta: consumeOutputDelta,
consume_output_ratio: consumeOutputRatio,
game_profit: gameProfit, game_profit: gameProfit,
game_turnover: gameTurnover, game_turnover: gameTurnover,
gift_coin_spent: readNumber(item, "gift_coin_spent", "giftCoinSpent"), gift_coin_spent: readNumber(item, "gift_coin_spent", "giftCoinSpent"),
@ -514,6 +532,7 @@ function normalizeDailySeries(source) {
new_paid_users: readNumber(item, "new_paid_users", "newPaidUsers"), new_paid_users: readNumber(item, "new_paid_users", "newPaidUsers"),
new_user_recharge_usd_minor: readNumber(item, "new_user_recharge_usd_minor", "newUserRechargeUsdMinor"), new_user_recharge_usd_minor: readNumber(item, "new_user_recharge_usd_minor", "newUserRechargeUsdMinor"),
new_users: readNumber(item, "new_users", "newUsers"), new_users: readNumber(item, "new_users", "newUsers"),
output_coin: outputCoin,
paid_users: readNumber(item, "paid_users", "paidUsers"), paid_users: readNumber(item, "paid_users", "paidUsers"),
platform_grant_coin: readOptionalNumber(item, "platform_grant_coin", "platformGrantCoin"), platform_grant_coin: readOptionalNumber(item, "platform_grant_coin", "platformGrantCoin"),
recharge_usd_minor: recharge, recharge_usd_minor: recharge,

View File

@ -147,6 +147,7 @@ test("builds report rows with range total, daily totals, and daily country rows"
manual_grant_coin: 20, manual_grant_coin: 20,
mic_online_ms: 6_000, mic_online_ms: 6_000,
mic_online_users: 3, mic_online_users: 3,
output_coin: 120,
platform_grant_coin: 150, platform_grant_coin: 150,
recharge_usd_minor: 100, recharge_usd_minor: 100,
salary_usd_minor: 900, salary_usd_minor: 900,
@ -172,6 +173,7 @@ test("builds report rows with range total, daily totals, and daily country rows"
manual_grant_coin: 20, manual_grant_coin: 20,
mic_online_ms: 6_000, mic_online_ms: 6_000,
mic_online_users: 3, mic_online_users: 3,
output_coin: 120,
platform_grant_coin: 150, platform_grant_coin: 150,
recharge_usd_minor: 100, recharge_usd_minor: 100,
salary_usd_minor: 900, salary_usd_minor: 900,
@ -188,10 +190,13 @@ test("builds report rows with range total, daily totals, and daily country rows"
avg_mic_online_ms: 2_000, avg_mic_online_ms: 2_000,
coin_total: 1_200, coin_total: 1_200,
consumed_coin: 300, consumed_coin: 300,
consume_output_delta: 180,
consume_output_ratio: 0.4,
country: "总计", country: "总计",
date_label: "2026-06-05", date_label: "2026-06-05",
manual_grant_coin: 20, manual_grant_coin: 20,
mic_online_ms: 6_000, mic_online_ms: 6_000,
output_coin: 120,
platform_grant_coin: 150, platform_grant_coin: 150,
salary_usd_minor: 900 salary_usd_minor: 900
})); }));
@ -202,11 +207,14 @@ test("builds report rows with range total, daily totals, and daily country rows"
coin_seller_transfer_coin: 500, coin_seller_transfer_coin: 500,
coin_total: 1_200, coin_total: 1_200,
consumed_coin: 300, consumed_coin: 300,
consume_output_delta: 180,
consume_output_ratio: 0.4,
country: "巴西", country: "巴西",
game_turnover: 300, game_turnover: 300,
lucky_gift_turnover: 200, lucky_gift_turnover: 200,
manual_grant_coin: 20, manual_grant_coin: 20,
mic_online_ms: 6_000, mic_online_ms: 6_000,
output_coin: 120,
platform_grant_coin: 150, platform_grant_coin: 150,
salary_usd_minor: 900, salary_usd_minor: 900,
salary_transfer_coin: 700, salary_transfer_coin: 700,