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,