diff --git a/src/app/(main)/ad-revenue-report/page.tsx b/src/app/(main)/ad-revenue-report/page.tsx index 86a992c..e46de3c 100644 --- a/src/app/(main)/ad-revenue-report/page.tsx +++ b/src/app/(main)/ad-revenue-report/page.tsx @@ -9,7 +9,6 @@ import { Card, Col, DatePicker, - Descriptions, Divider, InputNumber, Modal, @@ -43,6 +42,8 @@ import type { import UserAdRevenueDrawer from './UserAdRevenueDrawer'; const { RangePicker } = DatePicker; +const DEFAULT_PAGE_SIZE = 50; +const ALL_FILTER_VALUE = '__all__'; // 广告类型标签 const TYPE_TAG: Record = { @@ -190,11 +191,76 @@ const LT_FACTOR_ROWS = [ { key: '5', lt: '第 11 条及以后', factor: 1.0 }, ]; -type AdRevenueDetailRow = AdRevenueRecord & { source_adn?: string | null }; +type AdRevenueDetailRow = AdRevenueRecord & { + source_adn?: string | null; + is_impression_only?: boolean; +}; + +const FORMULA_EMPTY_STATUS_LABEL: Record = { + capped: '次数超限', + ecpm_missing: 'eCPM 缺失', + too_short: '观看时长不足', + closed_early: '提前关闭', + impression_only: '仅展示', +}; + +// 未发奖记录用可读状态代替空值,避免误认为数据丢失;悬停时说明为何没有计算因子。 +function renderFormulaValue(value: string | number, row: AdRevenueDetailRow) { + if (value !== '-') return value; + const isGrantedButMissing = row.status === 'granted'; + const reason = isGrantedButMissing + ? '该记录标记为已发奖,但计算因子缺失,需要进一步排查' + : row.is_impression_only + ? '该记录只有广告展示,没有对应发奖记录' + : REWARD_STATUS_HINT[row.status] ?? `发奖状态为 ${row.status || '未知'}`; + const statusLabel = isGrantedButMissing + ? '计算数据缺失' + : (FORMULA_EMPTY_STATUS_LABEL[row.status] ?? row.status) || '未进入计算'; + return ( + + {statusLabel} + + ); +} + +function impressionOnlyDetail(row: AdRevenueRow): AdRevenueDetailRow { + return { + record_id: 0, + created_at: row.created_at, + status: 'impression_only', + ecpm: row.ecpm, + ecpm_factor: null, + units: 0, + lt_index_start: null, + lt_index_end: null, + lt_factor_start: null, + lt_factor_end: null, + expected_coin: 0, + actual_coin: 0, + matched: true, + adn: row.adn, + slot_id: row.slot_id, + source_adn: row.adn, + is_impression_only: true, + }; +} // 展开行(逐条事件展开)- 该条的发奖复算明细(还原金币审计的 eCPM/因子1/份数/LT/因子2 等列) const DETAIL_COLUMNS: ColumnsType = [ - { title: '来源广告网络', dataIndex: 'source_adn', width: 110, render: (value: string | null) => value || '-' }, + { + title: '来源广告网络', + dataIndex: 'source_adn', + width: 110, + render: (value: string | null) => value?.trim() || ( + + 未上报 + + ), + }, { title: 'eCPM(元)', dataIndex: 'ecpm', @@ -208,18 +274,25 @@ const DETAIL_COLUMNS: ColumnsType = [ render: (v: string | null) => (v == null || Number.isNaN(Number(v)) ? '-' : (Number(v) / 100 / 1000).toFixed(4)), }, { title: '实发金币数', dataIndex: 'actual_coin', width: 100 }, - { title: '因子1', dataIndex: 'ecpm_factor', width: 70, render: (v: number | null) => v ?? '-' }, + { + title: '因子1', + dataIndex: 'ecpm_factor', + width: 70, + render: (v: number | null, row: AdRevenueDetailRow) => renderFormulaValue(v ?? '-', row), + }, { title: '因子2', key: 'lt_factor', width: 90, - render: (_: unknown, r: AdRevenueDetailRow) => fmtFactorRange(r.lt_factor_start, r.lt_factor_end), + render: (_: unknown, row: AdRevenueDetailRow) => + renderFormulaValue(fmtFactorRange(row.lt_factor_start, row.lt_factor_end), row), }, { title: 'LT累计条数', key: 'lt_index', width: 100, - render: (_: unknown, r: AdRevenueDetailRow) => fmtIndexRange(r.lt_index_start, r.lt_index_end), + render: (_: unknown, row: AdRevenueDetailRow) => + renderFormulaValue(fmtIndexRange(row.lt_index_start, row.lt_index_end), row), }, ]; @@ -387,13 +460,13 @@ export default function AdRevenueReportPage() { const [scene, setScene] = useState(); const [granularity, setGranularity] = useState<'day' | 'hour'>('day'); const [sortBy, setSortBy] = useState<'time' | 'ecpm'>('time'); // 明细排序:time=时间倒序 / ecpm=eCPM 倒序 - const [limit, setLimit] = useState(1000); // 默认拉满接口单页上限,供单次流程广告数分位统计 + const [limit, setLimit] = useState(DEFAULT_PAGE_SIZE); const [page, setPage] = useState(1); // 当前页码(后端分页;1 起) const [queriedGranularity, setQueriedGranularity] = useState<'day' | 'hour'>('day'); // 本次结果对应的粒度,决定是否显示「小时」列 const [queriedMultiDay, setQueriedMultiDay] = useState(false); // 本次结果是否跨多天,决定显示「日期」列 + 按天/按小时图 const [data, setData] = useState(null); const [allFlowItems, setAllFlowItems] = useState([]); - const [queriedLimit, setQueriedLimit] = useState(1000); + const [queriedLimit, setQueriedLimit] = useState(DEFAULT_PAGE_SIZE); const [loading, setLoading] = useState(false); const [formulaOpen, setFormulaOpen] = useState(false); // 点用户手机号弹出的「用户广告收益详情」半屏抽屉(userId + 手机号;null=关闭) @@ -661,15 +734,19 @@ export default function AdRevenueReportPage() { : data.date_from : `${data.date_from} ~ ${data.date_to}`; - // eCPM 优先使用后端按真实展示加权的经营分类口径;兼容旧后端时才用收益÷展示数回退。 + // 第二行大盘「分广告类型」:看视频包含福利视频与提现视频; + // eCPM 优先使用后端按真实展示加权的经营分类口径;兼容旧后端时才用合并收益÷展示数回退。 const drawStat = data?.type_stats?.draw; - const rvStat = data?.type_stats?.reward_video; + const rewardVideoStat = data?.type_stats?.reward_video; const withdrawalVideoStat = data?.type_stats?.withdrawal_video; - const drawEcpmStat = data?.category_stats?.draw ?? drawStat; - const videoEcpmStat = data?.category_stats?.video ?? { - impressions: (rvStat?.impressions ?? 0) + (withdrawalVideoStat?.impressions ?? 0), - revenue_yuan: (rvStat?.revenue_yuan ?? 0) + (withdrawalVideoStat?.revenue_yuan ?? 0), + const videoStat: AdRevenueTypeStat = { + impressions: + (rewardVideoStat?.impressions ?? 0) + (withdrawalVideoStat?.impressions ?? 0), + revenue_yuan: + (rewardVideoStat?.revenue_yuan ?? 0) + (withdrawalVideoStat?.revenue_yuan ?? 0), }; + const drawEcpmStat = data?.category_stats?.draw ?? drawStat; + const videoEcpmStat = data?.category_stats?.video ?? videoStat; const ecpmOf = (s?: AdRevenueTypeStat) => s?.ecpm_yuan ?? (s && s.impressions > 0 ? (s.revenue_yuan / s.impressions) * 1000 : 0); @@ -781,12 +858,11 @@ export default function AdRevenueReportPage() { 类型 setScene(value === ALL_FILTER_VALUE ? undefined : value)} style={{ width: 130 }} options={[ + { value: ALL_FILTER_VALUE, label: '全部' }, { value: 'comparison', label: '比价' }, { value: 'coupon', label: '领券' }, { value: 'welfare', label: '福利' }, @@ -993,13 +1068,13 @@ export default function AdRevenueReportPage() { - + - + @@ -1108,7 +1183,11 @@ export default function AdRevenueReportPage() { style={{ marginTop: 8 }} rowKey="record_id" columns={DETAIL_COLUMNS} - dataSource={r.sub_rewards.map((detail) => ({ ...detail, source_adn: r.adn }))} + dataSource={r.sub_rewards.map((detail) => ({ + ...detail, + // 一次比价/领券可能由不同 ADN 连续填充,优先显示本条发奖记录的来源。 + source_adn: detail.adn ?? r.adn, + }))} pagination={false} size="small" scroll={{ x: 900 }} @@ -1124,7 +1203,10 @@ export default function AdRevenueReportPage() { style={{ marginTop: 8 }} rowKey="record_id" columns={DETAIL_COLUMNS} - dataSource={[{ ...r.reward_detail, source_adn: r.adn }]} + dataSource={[{ + ...r.reward_detail, + source_adn: r.reward_detail.adn ?? r.adn, + }]} pagination={false} size="small" scroll={{ x: 900 }} @@ -1132,24 +1214,19 @@ export default function AdRevenueReportPage() { ) : (
- 展示明细{' '} - (纯展示事件,未单独发奖,无金币复算) - - {formatUtcTime(r.created_at)} - {r.ecpm ?? '-'} - - {r.ecpm != null && !Number.isNaN(Number(r.ecpm)) - ? (Number(r.ecpm) / 100).toFixed(2) - : '-'} - - - {r.has_impression ? r.revenue_yuan.toFixed(4) : '-'} - - {r.adn ?? '-'} - {r.slot_id ?? '-'} - {r.our_code_id ?? '-'} - {r.app_env ?? '-'} - + 本次广告逐条明细{' '} + + (共 1 条 · 应发 0 / 实发 0;仅展示,未单独发奖) + + + style={{ marginTop: 8 }} + rowKey="record_id" + columns={DETAIL_COLUMNS} + dataSource={[impressionOnlyDetail(r)]} + pagination={false} + size="small" + scroll={{ x: 900 }} + />
), }} diff --git a/src/app/(main)/comparison-records/page.tsx b/src/app/(main)/comparison-records/page.tsx index ae9cf14..be8cd7f 100644 --- a/src/app/(main)/comparison-records/page.tsx +++ b/src/app/(main)/comparison-records/page.tsx @@ -237,9 +237,9 @@ export default function ComparisonRecordsPage() { key: 'user', width: 140, render: (_, r) => ( -
+
{r.phone || `#${r.user_id}`}
- {r.nickname &&
{r.nickname}
} + {r.nickname &&
{r.nickname}
}
), }, diff --git a/src/app/(main)/feedbacks/page.tsx b/src/app/(main)/feedbacks/page.tsx index b37008d..4bb89ce 100644 --- a/src/app/(main)/feedbacks/page.tsx +++ b/src/app/(main)/feedbacks/page.tsx @@ -122,13 +122,31 @@ const renderReview = (f: Feedback) => { // 机型 / OS版本(提交反馈时的端环境;改版前历史反馈无 → -) const renderDeviceOs = (f: Feedback) => { - const os = [f.android_version ? `Android ${f.android_version}` : null, f.rom_name] + const modelName = f.device_model_name || f.device_model; + const rawModel = + f.device_model_name && f.device_model_name !== f.device_model ? f.device_model : null; + const romHasVersion = /\d/.test(f.rom_name || ''); + const vendorOs = [ + f.rom_name, + f.rom_version && !romHasVersion ? f.rom_version : null, + ] + .filter(Boolean) + .join(' '); + const os = [ + vendorOs || null, + f.android_version ? `Android ${f.android_version}` : null, + ] .filter(Boolean) .join(' · '); - if (!f.device_model && !os) return -; + if (!modelName && !os) return -; return ( - {f.device_model || '-'} + {modelName || '-'} + {rawModel ? ( + + {[f.device_manufacturer, rawModel].filter(Boolean).join(' · ')} + + ) : null} {os ? ( {os} @@ -352,7 +370,7 @@ export default function FeedbacksPage() { { title: '机型 / OS版本', key: 'device', - width: 150, + width: 180, render: (_: unknown, f: Feedback) => renderDeviceOs(f), }, { diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index b63fdc5..389f232 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -91,6 +91,7 @@ const NAV_GROUPS: NavGroup[] = [ { key: '/users', icon: , label: '用户管理' }, ], }, + { key: '/admins', icon: , label: '权限管理' }, { key: 'monitoring-audit', icon: , @@ -103,7 +104,6 @@ const NAV_GROUPS: NavGroup[] = [ { key: '/audit-logs', icon: , label: '审计日志' }, ], }, - { key: '/admins', icon: , label: '权限管理' }, ]; // 导航项 key(/dashboard)→ 权限页 key(dashboard),与后端 permissions.py 目录对齐 @@ -223,8 +223,7 @@ export default function MainLayout({ children }: { children: React.ReactNode }) position: 'sticky', top: 0, height: '100vh', - overflowY: 'auto', - overflowX: 'hidden', + overflow: 'hidden', }} >