From a952b4262ea4a746758541808066b96d46464c98 Mon Sep 17 00:00:00 2001 From: zhx Date: Tue, 30 Jun 2026 14:41:06 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=95=B0=E6=8D=AE=E5=A4=A7?= =?UTF-8?q?=E5=B1=8F=E9=87=91=E5=B8=81=E7=BB=9F=E8=AE=A1=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- databi/src/components/ReportOverview.jsx | 16 ++++++++++++- databi/src/components/ReportOverview.test.jsx | 4 ++++ databi/src/data/createDashboardModel.js | 23 +++++++++++++++++-- databi/src/data/createDashboardModel.test.js | 8 +++++++ 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/databi/src/components/ReportOverview.jsx b/databi/src/components/ReportOverview.jsx index a61fc10..52cf13b 100644 --- a/databi/src/components/ReportOverview.jsx +++ b/databi/src/components/ReportOverview.jsx @@ -33,8 +33,10 @@ const countryColumns = [ { key: "lucky_gift_turnover", label: "幸运礼物流水/利润率", render: (row) => , sortable: false }, { key: "super_lucky_gift_turnover", label: "超级幸运礼物流水/利润率", render: (row) => , 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: "output_coin", label: "产出金币", render: (row) => formatOptionalCoin(row.output_coin), sortable: false }, + { key: "consume_output_ratio", label: "消耗产出比", render: (row) => , sortable: false }, { key: "platform_grant_coin", label: "平台发放金币", render: (row, context) => , 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 }, @@ -736,6 +738,18 @@ function FlowRateCell({ rate, value }) { ); } +function ConsumeOutputCell({ row }) { + if (isMissing(row.consume_output_ratio) && isMissing(row.consume_output_delta)) { + return --; + } + return ( + + {formatOptionalPercent(row.consume_output_ratio)} + 差额 {formatOptionalCoin(row.consume_output_delta)} + + ); +} + function TrendValue({ value }) { return = 0 ? "report-trend-up" : "report-trend-down"}>{formatSignedPercent(value)}; } diff --git a/databi/src/components/ReportOverview.test.jsx b/databi/src/components/ReportOverview.test.jsx index 79f7ef1..87dcb84 100644 --- a/databi/src/components/ReportOverview.test.jsx +++ b/databi/src/components/ReportOverview.test.jsx @@ -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"); 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(); const june5Row = screen.getByRole("button", { name: "展开 2026-06-05 国家明细" }); diff --git a/databi/src/data/createDashboardModel.js b/databi/src/data/createDashboardModel.js index 4da8b8a..005c49b 100644 --- a/databi/src/data/createDashboardModel.js +++ b/databi/src/data/createDashboardModel.js @@ -339,6 +339,7 @@ function summarizeReportRows(rows) { "coin_seller_transfer_coin", "coin_total", "consumed_coin", + "output_coin", "game_payout", "game_refund", "game_turnover", @@ -398,6 +399,12 @@ function normalizeCountryRow(item, index, totalRecharge) { const micOnlineUsers = readOptionalNumber(item, "mic_online_users", "micOnlineUsers"); const avgMicOnlineMS = readOptionalNumber(item, "avg_mic_online_ms", "avgMicOnlineMs") ?? (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 { active_users: activeUsers, 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_transfer_coin: numberValue(item.coin_seller_transfer_coin ?? item.coinSellerTransferCoin), 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_id: countryId, 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), paid_users: paidUsers, payer_rate: ratio(paidUsers, activeUsers), + output_coin: outputCoin, platform_grant_coin: readOptionalNumber(item, "platform_grant_coin", "platformGrantCoin"), recharge_usd_minor: recharge, 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 avgMicOnlineMS = readOptionalNumber(item, "avg_mic_online_ms", "avgMicOnlineMs") ?? (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 { active_users: readNumber(item, "active_users", "activeUsers"), 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_transfer_coin: readNumber(item, "coin_seller_transfer_coin", "coinSellerTransferCoin"), 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_turnover: gameTurnover, 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_user_recharge_usd_minor: readNumber(item, "new_user_recharge_usd_minor", "newUserRechargeUsdMinor"), new_users: readNumber(item, "new_users", "newUsers"), + output_coin: outputCoin, paid_users: readNumber(item, "paid_users", "paidUsers"), platform_grant_coin: readOptionalNumber(item, "platform_grant_coin", "platformGrantCoin"), recharge_usd_minor: recharge, diff --git a/databi/src/data/createDashboardModel.test.js b/databi/src/data/createDashboardModel.test.js index adf0af8..6c04cbf 100644 --- a/databi/src/data/createDashboardModel.test.js +++ b/databi/src/data/createDashboardModel.test.js @@ -147,6 +147,7 @@ test("builds report rows with range total, daily totals, and daily country rows" manual_grant_coin: 20, mic_online_ms: 6_000, mic_online_users: 3, + output_coin: 120, platform_grant_coin: 150, recharge_usd_minor: 100, 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, mic_online_ms: 6_000, mic_online_users: 3, + output_coin: 120, platform_grant_coin: 150, recharge_usd_minor: 100, 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, coin_total: 1_200, consumed_coin: 300, + consume_output_delta: 180, + consume_output_ratio: 0.4, country: "总计", date_label: "2026-06-05", manual_grant_coin: 20, mic_online_ms: 6_000, + output_coin: 120, platform_grant_coin: 150, 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_total: 1_200, consumed_coin: 300, + consume_output_delta: 180, + consume_output_ratio: 0.4, country: "巴西", game_turnover: 300, lucky_gift_turnover: 200, manual_grant_coin: 20, mic_online_ms: 6_000, + output_coin: 120, platform_grant_coin: 150, salary_usd_minor: 900, salary_transfer_coin: 700,