diff --git a/src/app/(main)/ad-revenue-report/page.tsx b/src/app/(main)/ad-revenue-report/page.tsx index 1fe077b..1ed0091 100644 --- a/src/app/(main)/ad-revenue-report/page.tsx +++ b/src/app/(main)/ad-revenue-report/page.tsx @@ -66,21 +66,89 @@ const APP_TAG: Record = { test: { color: 'default', label: '测试应用' }, }; -// 发奖状态 → 颜色 + 中文(含「不发金币的原因」:提前关闭/未满10秒/缺eCPM/次数超限) -const STATUS_TAG: Record = { - granted: { color: 'green', label: '已发' }, - capped: { color: 'orange', label: '次数超限' }, - ecpm_missing: { color: 'red', label: '缺 eCPM' }, +const REWARD_STATUS_HINT: Record = { + granted: '已完成金币发放', + capped: '次数超限,未发金币', + ecpm_missing: '缺少有效 eCPM,未发金币', + too_short: '播放时长未达到发奖条件,未发金币', + closed_early: '用户提前关闭广告,未发金币', +}; + +const PLAYBACK_STATUS_TAG: Record = { + completed: { color: 'green', label: '已完成' }, too_short: { color: 'gold', label: '未满10秒' }, closed_early: { color: 'default', label: '提前关闭' }, + unknown: { color: 'default', label: '未知' }, }; -const STATUS_HINT: Record = { - granted: '已满足当前客户端发奖条件并完成金币发放。', - too_short: '旧版 Draw 信息流观看不足 10 秒,不发金币;新版按观看比例发放时会记为已发。', - closed_early: '用户在达到发奖条件前主动关闭,不发金币。', - capped: '已达到次数上限,不再发金币。', - ecpm_missing: '缺少有效 eCPM,无法计算金币。', -}; + +function rewardStatuses(row: AdRevenueRow): string[] { + if (row.sub_rewards?.length) return row.sub_rewards.map((reward) => reward.status); + return row.status ? [row.status] : []; +} + +function rewardStatusTag(row: AdRevenueRow) { + const statuses = rewardStatuses(row); + if (!row.has_reward || statuses.length === 0) { + return { color: 'default', label: '无记录', hint: '仅记录到广告展示,没有对应发奖记录。' }; + } + + const grantedCount = statuses.filter((status) => status === 'granted').length; + const reasonSummary = Object.entries( + statuses + .filter((status) => status !== 'granted') + .reduce>((counts, status) => { + counts[status] = (counts[status] ?? 0) + 1; + return counts; + }, {}), + ) + .map(([status, count]) => `${REWARD_STATUS_HINT[status] ?? status} ${count} 条`) + .join(';'); + + if (grantedCount === statuses.length) { + return { color: 'green', label: '已发', hint: `已完成金币发放${statuses.length > 1 ? ` ${statuses.length} 条` : ''}。` }; + } + if (grantedCount > 0) { + return { + color: 'blue', + label: '部分已发', + hint: `已发 ${grantedCount} 条,未发 ${statuses.length - grantedCount} 条${reasonSummary ? `;${reasonSummary}` : ''}。`, + }; + } + return { color: 'default', label: '未发', hint: reasonSummary ? `${reasonSummary}。` : '未发放金币。' }; +} + +function playbackStatusTag(row: AdRevenueRow) { + const statuses = rewardStatuses(row); + if (statuses.length === 0) { + return row.has_impression + ? { color: 'blue', label: '仅展示', hint: '记录到广告展示,但没有对应的播放结果。' } + : { color: 'default', label: '无记录', hint: '没有可用的广告播放记录。' }; + } + + const playbackCounts = statuses.reduce>((counts, status) => { + const playbackStatus = + status === 'too_short' || status === 'closed_early' + ? status + : status === 'granted' || status === 'capped' || status === 'ecpm_missing' + ? 'completed' + : 'unknown'; + counts[playbackStatus] = (counts[playbackStatus] ?? 0) + 1; + return counts; + }, {}); + const entries = Object.entries(playbackCounts); + if (entries.length === 1) { + const [status, count] = entries[0]; + const tag = PLAYBACK_STATUS_TAG[status]; + return { + ...tag, + hint: `${tag.label}${count > 1 ? ` ${count} 条` : ''}。`, + }; + } + const hint = entries + .map(([status, count]) => `${PLAYBACK_STATUS_TAG[status].label} ${count} 条`) + .join(';'); + return { color: 'purple', label: '混合状态', hint: `${hint}。` }; +} const fmtFactorRange = (a: number | null, b: number | null) => { if (a == null) return '-'; @@ -485,12 +553,20 @@ export default function AdRevenueReportPage() { }, { title: '发奖状态', - dataIndex: 'status', + key: 'reward_status', width: 100, - render: (s: string | null) => { - if (!s) return 仅展示; - const t = STATUS_TAG[s] ?? { color: 'default', label: s }; - return {t.label}; + render: (_: unknown, row: AdRevenueRow) => { + const tag = rewardStatusTag(row); + return {tag.label}; + }, + }, + { + title: '广告播放状态', + key: 'playback_status', + width: 120, + render: (_: unknown, row: AdRevenueRow) => { + const tag = playbackStatusTag(row); + return {tag.label}; }, }, { @@ -532,7 +608,8 @@ export default function AdRevenueReportPage() { 'ecpm_yuan', 'revenue_yuan', 'actual_coin', - 'status', + 'reward_status', + 'playback_status', 'ad_type', 'app_env', 'our_code_id',