feat(cps): 分发后台从仅美团扩展到美团/淘宝/京东多平台

- 群/活动支持平台多选,sid 仅美团群保留
- 活动按平台切换录入(美团 actId/pvs、淘宝淘口令、京东链接)
- 生成发群链接改批量勾选多活动(/cps/referral-links)
- 统计表加平台/复制列,淘宝/京东无对账字段显示 "-"

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 19:15:27 +08:00
parent a393b5c0e3
commit 654aca4c40
2 changed files with 228 additions and 167 deletions
+202 -150
View File
@@ -1,12 +1,13 @@
'use client'; 'use client';
// CPS 优惠券分发与对账(当前仅美团)。单页 Tabs:对账统计 / 群管理 / 活动管理 / 订单明细 // CPS 优惠券分发与对账。平台:美团(actId+sid,完整对账) / 淘宝(淘口令) / 京东(链接)
// 群管理生成带 sid 的券链接 → 发群 → 对账按 sid 拉回订单/佣金归群统计 // 淘宝/京东无 API → 只统计点击,对账字段显示 "-"。单页 Tabs:对账统计/群管理/活动管理/订单明细
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import { import {
Button, Button,
Card, Card,
Checkbox,
Form, Form,
Input, Input,
InputNumber, InputNumber,
@@ -30,7 +31,7 @@ import type {
CpsGroupStat, CpsGroupStat,
CpsOrder, CpsOrder,
CpsReconcileResult, CpsReconcileResult,
CpsReferralLink, CpsReferralLinks,
CpsStats, CpsStats,
} from '@/lib/types'; } from '@/lib/types';
@@ -42,21 +43,30 @@ const MT_STATUS: Record<string, { text: string; color: string }> = {
'6': { text: '已结算', color: 'green' }, '6': { text: '已结算', color: 'green' },
}; };
const LINK_TYPE_NAME: Record<string, string> = { const PLATFORM_LABEL: Record<string, string> = { meituan: '美团', taobao: '淘宝', jd: '京东' };
'1': 'H5 长链', const PLATFORM_COLOR: Record<string, string> = { meituan: 'gold', taobao: 'volcano', jd: 'red' };
'2': '短链(微信可发)', const PLATFORM_OPTIONS = [
'3': 'deeplink(唤起 App)', { label: '美团', value: 'meituan' },
}; { label: '淘宝', value: 'taobao' },
{ label: '京东', value: 'jd' },
];
const fmtRate = (r: string | null) => (r ? `${Number(r) / 100}%` : '-'); const fmtRate = (r: string | null) => (r ? `${Number(r) / 100}%` : '-');
const DASH = <span style={{ color: '#bbb' }}>-</span>;
const platformTags = (ps: string[] | undefined) =>
(ps || []).map((p) => (
<Tag key={p} color={PLATFORM_COLOR[p]} style={{ marginInlineEnd: 4 }}>
{PLATFORM_LABEL[p] || p}
</Tag>
));
export default function CpsPage() { export default function CpsPage() {
return ( return (
<div> <div>
<h2>CPS </h2> <h2>CPS </h2>
<Typography.Paragraph type="secondary" style={{ marginTop: -8 }}> <Typography.Paragraph type="secondary" style={{ marginTop: -8 }}>
sid: sid , sid (actId + sid,/) / () / () API,
() <b></b>,- /c/xxx
</Typography.Paragraph> </Typography.Paragraph>
<Tabs <Tabs
defaultActiveKey="stats" defaultActiveKey="stats"
@@ -113,43 +123,54 @@ function StatsTab() {
}; };
const columns: ColumnsType<CpsGroupStat> = [ const columns: ColumnsType<CpsGroupStat> = [
{ title: '群', dataIndex: 'name', width: 160, ellipsis: true }, { title: '群', dataIndex: 'name', width: 140, ellipsis: true, fixed: 'left' },
{ title: '平台', key: 'platforms', width: 130, render: (_, r) => platformTags(r.platforms) },
{ title: '人数', dataIndex: 'member_count', width: 64, render: (v) => v ?? '-' },
{ title: '点击', dataIndex: 'click_pv', width: 64 },
{ title: '独立', dataIndex: 'click_uv', width: 64 },
{ {
title: 'sid', title: '复制',
dataIndex: 'sid', dataIndex: 'copy_pv',
width: 130, width: 64,
render: (v: string | null) => (v ? <Typography.Text copyable>{v}</Typography.Text> : '-'), render: (v: number) => (v ? v : DASH),
},
{
title: '有效单',
dataIndex: 'order_count',
width: 70,
render: (v: number | null) => (v == null ? DASH : v),
}, },
{ title: '人数', dataIndex: 'member_count', width: 70, render: (v) => v ?? '-' },
{ title: '点击', dataIndex: 'click_pv', width: 70 },
{ title: '独立访客', dataIndex: 'click_uv', width: 80 },
{ title: '有效单', dataIndex: 'order_count', width: 70 },
{ {
title: '取消/风控', title: '取消/风控',
dataIndex: 'canceled_count', dataIndex: 'canceled_count',
width: 85, width: 80,
render: (v: number) => (v ? <span style={{ color: '#cf1322' }}>{v}</span> : 0), render: (v: number | null) => (v == null ? DASH : v ? <span style={{ color: '#cf1322' }}>{v}</span> : 0),
}, },
{ {
title: '点击→下单', title: '点击→下单',
key: 'rate', key: 'rate',
width: 90, width: 88,
render: (_, r) => render: (_, r) =>
r.click_uv ? `${((r.order_count / r.click_uv) * 100).toFixed(0)}%` : '-', r.order_count == null ? DASH : r.click_uv ? `${((r.order_count / r.click_uv) * 100).toFixed(0)}%` : '-',
},
{
title: '成交额',
dataIndex: 'gmv_cents',
width: 96,
render: (v: number | null) => (v == null ? DASH : yuan(v)),
}, },
{ title: '成交额', dataIndex: 'gmv_cents', width: 110, render: (v: number) => yuan(v) },
{ {
title: '预估佣金', title: '预估佣金',
dataIndex: 'est_commission_cents', dataIndex: 'est_commission_cents',
width: 110, width: 100,
sorter: (a, b) => a.est_commission_cents - b.est_commission_cents, sorter: (a, b) => (a.est_commission_cents || 0) - (b.est_commission_cents || 0),
render: (v: number) => <b>{yuan(v)}</b>, render: (v: number | null) => (v == null ? DASH : <b>{yuan(v)}</b>),
}, },
{ {
title: '已结算佣金', title: '已结算佣金',
dataIndex: 'settled_commission_cents', dataIndex: 'settled_commission_cents',
width: 110, width: 100,
render: (v: number) => <span style={{ color: '#3f8600' }}>{yuan(v)}</span>, render: (v: number | null) => (v == null ? DASH : <span style={{ color: '#3f8600' }}>{yuan(v)}</span>),
}, },
]; ];
@@ -172,14 +193,12 @@ function StatsTab() {
() ()
</Button> </Button>
)} )}
<Typography.Text type="secondary"> <Typography.Text type="secondary">/,-;</Typography.Text>
,
</Typography.Text>
</Space> </Space>
<Space size="large" style={{ marginBottom: 16 }} wrap> <Space size="large" style={{ marginBottom: 16 }} wrap>
<Card size="small"> <Card size="small">
<Statistic title="有效订单数" value={data?.total_order_count ?? 0} /> <Statistic title="有效订单数(美团)" value={data?.total_order_count ?? 0} />
</Card> </Card>
<Card size="small"> <Card size="small">
<Statistic <Statistic
@@ -206,7 +225,7 @@ function StatsTab() {
dataSource={data?.groups ?? []} dataSource={data?.groups ?? []}
loading={loading} loading={loading}
pagination={false} pagination={false}
scroll={{ x: 1120 }} scroll={{ x: 1200 }}
/> />
</div> </div>
); );
@@ -222,33 +241,33 @@ function GroupsTab() {
); );
const canManage = canDo(['operator']); const canManage = canDo(['operator']);
const [editing, setEditing] = useState<CpsGroup | null>(null); // null=不开,{} 新建用单独标志 const [editing, setEditing] = useState<CpsGroup | null>(null);
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
const [linkGroup, setLinkGroup] = useState<CpsGroup | null>(null); const [linkGroup, setLinkGroup] = useState<CpsGroup | null>(null);
const columns: ColumnsType<CpsGroup> = [ const columns: ColumnsType<CpsGroup> = [
{ title: 'ID', dataIndex: 'id', width: 70 }, { title: 'ID', dataIndex: 'id', width: 60 },
{ title: '群名', dataIndex: 'name', width: 180, ellipsis: true }, { title: '群名', dataIndex: 'name', width: 160, ellipsis: true },
{ title: '平台', key: 'platforms', width: 140, render: (_, g) => platformTags(g.platforms) },
{ {
title: 'sid', title: 'sid',
dataIndex: 'sid', dataIndex: 'sid',
width: 150, width: 130,
render: (v: string) => <Typography.Text copyable>{v}</Typography.Text>, render: (v: string | null) => (v ? <Typography.Text copyable>{v}</Typography.Text> : DASH),
}, },
{ title: '人数', dataIndex: 'member_count', width: 80, render: (v) => v ?? '-' }, { title: '人数', dataIndex: 'member_count', width: 70, render: (v) => v ?? '-' },
{ {
title: '状态', title: '状态',
dataIndex: 'status', dataIndex: 'status',
width: 90, width: 80,
render: (s: string) => <Tag color={s === 'active' ? 'green' : 'default'}>{s}</Tag>, render: (s: string) => <Tag color={s === 'active' ? 'green' : 'default'}>{s}</Tag>,
}, },
{ title: '备注', dataIndex: 'remark', width: 160, ellipsis: true, render: (v) => v || '-' },
{ title: '创建时间', dataIndex: 'created_at', width: 150, render: (v: string) => formatUtcTime(v) }, { title: '创建时间', dataIndex: 'created_at', width: 150, render: (v: string) => formatUtcTime(v) },
{ {
title: '操作', title: '操作',
key: 'op', key: 'op',
fixed: 'right', fixed: 'right',
width: 170, width: 150,
render: (_, g) => ( render: (_, g) => (
<Space onClick={(e) => e.stopPropagation()}> <Space onClick={(e) => e.stopPropagation()}>
{canManage && <a onClick={() => setLinkGroup(g)}></a>} {canManage && <a onClick={() => setLinkGroup(g)}></a>}
@@ -272,9 +291,7 @@ function GroupsTab() {
<Button type="primary" onClick={() => setApplied({ keyword: keyword || undefined })}> <Button type="primary" onClick={() => setApplied({ keyword: keyword || undefined })}>
</Button> </Button>
{canManage && ( {canManage && <Button onClick={() => setCreateOpen(true)}></Button>}
<Button onClick={() => setCreateOpen(true)}></Button>
)}
</Space> </Space>
<Table <Table
@@ -290,21 +307,11 @@ function GroupsTab() {
showTotal: (t) => `${t} 个群`, showTotal: (t) => `${t} 个群`,
onChange, onChange,
}} }}
scroll={{ x: 1050 }} scroll={{ x: 1040 }}
/> />
<GroupFormModal <GroupFormModal open={createOpen} group={null} onClose={() => setCreateOpen(false)} onDone={reload} />
open={createOpen} <GroupFormModal open={!!editing} group={editing} onClose={() => setEditing(null)} onDone={reload} />
group={null}
onClose={() => setCreateOpen(false)}
onDone={reload}
/>
<GroupFormModal
open={!!editing}
group={editing}
onClose={() => setEditing(null)}
onDone={reload}
/>
<ReferralLinkModal group={linkGroup} onClose={() => setLinkGroup(null)} /> <ReferralLinkModal group={linkGroup} onClose={() => setLinkGroup(null)} />
</div> </div>
); );
@@ -323,18 +330,22 @@ function GroupFormModal({
}) { }) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const isEdit = !!group; const isEdit = !!group;
const watchPlatforms = (Form.useWatch('platforms', form) as string[] | undefined) || [];
const hasMeituan = watchPlatforms.includes('meituan');
useEffect(() => { useEffect(() => {
if (open) { if (open) {
if (group) { if (group) {
form.setFieldsValue({ form.setFieldsValue({
name: group.name, name: group.name,
platforms: group.platforms,
member_count: group.member_count, member_count: group.member_count,
remark: group.remark, remark: group.remark,
status: group.status, status: group.status,
}); });
} else { } else {
form.resetFields(); form.resetFields();
form.setFieldsValue({ platforms: ['meituan'] });
} }
} }
}, [open, group, form]); }, [open, group, form]);
@@ -357,26 +368,32 @@ function GroupFormModal({
}; };
return ( return (
<Modal <Modal title={isEdit ? '编辑群' : '新建群'} open={open} onOk={submit} onCancel={onClose} destroyOnHidden>
title={isEdit ? '编辑群' : '新建群'}
open={open}
onOk={submit}
onCancel={onClose}
destroyOnHidden
>
<Form form={form} layout="vertical"> <Form form={form} layout="vertical">
<Form.Item name="name" label="群名" rules={[{ required: true, message: '请输入群名' }]}> <Form.Item name="name" label="群名" rules={[{ required: true, message: '请输入群名' }]}>
<Input placeholder="如:宝妈优惠群1" /> <Input placeholder="如:宝妈优惠群1" />
</Form.Item> </Form.Item>
{!isEdit && ( <Form.Item
name="platforms"
label="这个群发哪些平台的券(可多选)"
rules={[{ required: true, message: '至少选一个平台' }]}
>
<Checkbox.Group options={PLATFORM_OPTIONS} />
</Form.Item>
{hasMeituan && !isEdit && (
<Form.Item <Form.Item
name="sid" name="sid"
label="sid(渠道标识,留空自动生成 g+ID)" label="美团 sid(渠道标识,留空自动生成 g+ID)"
rules={[{ pattern: /^[A-Za-z0-9]+$/, message: '仅字母和数字' }]} rules={[{ pattern: /^[A-Za-z0-9]+$/, message: '仅字母和数字' }]}
> >
<Input placeholder="留空自动生成;或自定义如 baoma1" maxLength={64} /> <Input placeholder="留空自动生成;或自定义如 baoma1" maxLength={64} />
</Form.Item> </Form.Item>
)} )}
{hasMeituan && isEdit && (
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 12 }}>
sid:{group?.sid || '(无,保存后如缺会在生成美团链接时报错)'}
</Typography.Text>
)}
<Form.Item name="member_count" label="群人数(用于估算转化率)"> <Form.Item name="member_count" label="群人数(用于估算转化率)">
<InputNumber style={{ width: '100%' }} min={0} placeholder="如 200" /> <InputNumber style={{ width: '100%' }} min={0} placeholder="如 200" />
</Form.Item> </Form.Item>
@@ -400,13 +417,13 @@ function GroupFormModal({
function ReferralLinkModal({ group, onClose }: { group: CpsGroup | null; onClose: () => void }) { function ReferralLinkModal({ group, onClose }: { group: CpsGroup | null; onClose: () => void }) {
const [activities, setActivities] = useState<CpsActivity[]>([]); const [activities, setActivities] = useState<CpsActivity[]>([]);
const [activityId, setActivityId] = useState<number | undefined>(); const [activityIds, setActivityIds] = useState<number[]>([]);
const [result, setResult] = useState<CpsReferralLink | null>(null); const [result, setResult] = useState<CpsReferralLinks | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
useEffect(() => { useEffect(() => {
if (!group) { if (!group) {
setActivityId(undefined); setActivityIds([]);
setResult(null); setResult(null);
return; return;
} }
@@ -416,16 +433,19 @@ function ReferralLinkModal({ group, onClose }: { group: CpsGroup | null; onClose
.catch((e) => message.error(errMsg(e))); .catch((e) => message.error(errMsg(e)));
}, [group]); }, [group]);
// 只列群平台范围内的活动
const groupActivities = activities.filter((a) => (group?.platforms || []).includes(a.platform));
const generate = async () => { const generate = async () => {
if (!group || !activityId) { if (!group || !activityIds.length) {
message.warning('请选择活动'); message.warning('请勾选要生成的活动');
return; return;
} }
setLoading(true); setLoading(true);
try { try {
const resp = await api.post<CpsReferralLink>('/admin/api/cps/referral-link', { const resp = await api.post<CpsReferralLinks>('/admin/api/cps/referral-links', {
group_id: group.id, group_id: group.id,
activity_id: activityId, activity_ids: activityIds,
}); });
setResult(resp.data); setResult(resp.data);
} catch (e) { } catch (e) {
@@ -437,68 +457,68 @@ function ReferralLinkModal({ group, onClose }: { group: CpsGroup | null; onClose
return ( return (
<Modal <Modal
title={`生成链接 - ${group?.name ?? ''}(sid=${group?.sid ?? ''})`} title={`生成发群链接 - ${group?.name ?? ''}`}
open={!!group} open={!!group}
onCancel={onClose} onCancel={onClose}
onOk={generate} onOk={generate}
okText="生成" okText="生成"
confirmLoading={loading} confirmLoading={loading}
destroyOnHidden destroyOnHidden
width={640} width={660}
> >
<Form layout="vertical"> <Typography.Paragraph type="secondary" style={{ fontSize: 12 }}>
<Form.Item label="选择活动" required> ,;
<Select </Typography.Paragraph>
placeholder="选要推广的活动" {groupActivities.length === 0 ? (
value={activityId} <Typography.Text type="warning">
onChange={setActivityId} ({(group?.platforms || []).map((p) => PLATFORM_LABEL[p]).join('/')}),
options={activities.map((a) => ({ </Typography.Text>
value: a.id, ) : (
label: `${a.name}${a.act_id ? ` (actId=${a.act_id})` : ''}`, <Checkbox.Group
}))} value={activityIds}
notFoundContent="无活动,请先到「活动管理」新建" onChange={(v) => setActivityIds(v as number[])}
/> style={{ width: '100%' }}
</Form.Item> >
</Form> <Space direction="vertical" style={{ width: '100%' }}>
{groupActivities.map((a) => (
<Checkbox key={a.id} value={a.id}>
<Tag color={PLATFORM_COLOR[a.platform]}>{PLATFORM_LABEL[a.platform]}</Tag>
{a.name}
{a.act_id ? <span style={{ color: '#999' }}> (actId={a.act_id})</span> : null}
</Checkbox>
))}
</Space>
</Checkbox.Group>
)}
{result && ( {result && (
<div style={{ marginTop: 8 }}> <div style={{ marginTop: 16 }}>
<Typography.Text strong>👇 ( sid={result.sid},):</Typography.Text> <Typography.Text strong>👇 ():</Typography.Text>
<div {result.results.map((item) => (
style={{ <div
marginTop: 8, key={item.code}
padding: 12, style={{
background: '#f6ffed', marginTop: 8,
border: '1px solid #b7eb8f', padding: 10,
borderRadius: 6, background: '#f6ffed',
}} border: '1px solid #b7eb8f',
> borderRadius: 6,
<Typography.Text }}
copyable={{ text: result.redirect_url }}
style={{ fontSize: 15, wordBreak: 'break-all' }}
> >
{result.redirect_url} <div style={{ marginBottom: 4 }}>
</Typography.Text> <Tag color={PLATFORM_COLOR[item.platform]}>{PLATFORM_LABEL[item.platform]}</Tag>
</div> <span style={{ fontSize: 13 }}>{item.activity_name}</span>
{!result.redirect_url.startsWith('http') && ( </div>
<Typography.Text type="warning" style={{ fontSize: 12 }}> <Typography.Text copyable={{ text: item.redirect_url }} style={{ fontSize: 14, wordBreak: 'break-all' }}>
CPS_REDIRECT_BASE,; .env {item.redirect_url}
</Typography.Text>
)}
<Typography.Paragraph type="secondary" style={{ marginTop: 14, marginBottom: 4, fontSize: 12 }}>
(/,):
</Typography.Paragraph>
{Object.entries(result.link_map).map(([type, url]) => (
<div key={type} style={{ marginBottom: 4 }}>
<Tag>{LINK_TYPE_NAME[type] ?? type}</Tag>
<Typography.Text
copyable={{ text: url }}
style={{ fontSize: 12, color: '#999', wordBreak: 'break-all' }}
>
{url.length > 56 ? `${url.slice(0, 56)}` : url}
</Typography.Text> </Typography.Text>
</div> </div>
))} ))}
{result.results.some((i) => !i.redirect_url.startsWith('http')) && (
<Typography.Text type="warning" style={{ fontSize: 12 }}>
部分为相对路径:后端未配 CPS_REDIRECT_BASE,
</Typography.Text>
)}
</div> </div>
)} )}
</Modal> </Modal>
@@ -515,26 +535,31 @@ function ActivitiesTab() {
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
const columns: ColumnsType<CpsActivity> = [ const columns: ColumnsType<CpsActivity> = [
{ title: 'ID', dataIndex: 'id', width: 70 }, { title: 'ID', dataIndex: 'id', width: 60 },
{ title: '平台', dataIndex: 'platform', width: 90, render: (v: string) => <Tag>{v}</Tag> },
{ title: '活动名', dataIndex: 'name', width: 220, ellipsis: true },
{ {
title: 'actId', title: '平台',
dataIndex: 'platform',
width: 80,
render: (v: string) => <Tag color={PLATFORM_COLOR[v]}>{PLATFORM_LABEL[v] || v}</Tag>,
},
{ title: '活动名', dataIndex: 'name', width: 180, ellipsis: true },
{
title: 'actId(美团)',
dataIndex: 'act_id', dataIndex: 'act_id',
width: 120, width: 110,
render: (v: string | null) => v || '-', render: (v: string | null) => v || '-',
}, },
{ {
title: 'productViewSign', title: '淘口令/链接',
dataIndex: 'product_view_sign', dataIndex: 'payload',
width: 160, width: 240,
ellipsis: true, ellipsis: true,
render: (v: string | null) => v || '-', render: (v: string | null) => v || '-',
}, },
{ {
title: '状态', title: '状态',
dataIndex: 'status', dataIndex: 'status',
width: 90, width: 80,
render: (s: string) => <Tag color={s === 'active' ? 'green' : 'default'}>{s}</Tag>, render: (s: string) => <Tag color={s === 'active' ? 'green' : 'default'}>{s}</Tag>,
}, },
{ title: '创建时间', dataIndex: 'created_at', width: 150, render: (v: string) => formatUtcTime(v) }, { title: '创建时间', dataIndex: 'created_at', width: 150, render: (v: string) => formatUtcTime(v) },
@@ -579,15 +604,24 @@ function ActivityFormModal({
onDone: () => void; onDone: () => void;
}) { }) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const platform = (Form.useWatch('platform', form) as string | undefined) || 'meituan';
useEffect(() => { useEffect(() => {
if (open) form.resetFields(); if (open) {
form.resetFields();
form.setFieldsValue({ platform: 'meituan' });
}
}, [open, form]); }, [open, form]);
const submit = async () => { const submit = async () => {
const v = await form.validateFields(); const v = await form.validateFields();
if (!v.act_id && !v.product_view_sign) { if (v.platform === 'meituan') {
message.warning('actId 与 productViewSign 至少填一个'); if (!v.act_id && !v.product_view_sign) {
message.warning('美团活动:actId 与 productViewSign 至少填一个');
return;
}
} else if (!v.payload) {
message.warning(v.platform === 'taobao' ? '请填淘口令' : '请填京东链接');
return; return;
} }
try { try {
@@ -606,21 +640,39 @@ function ActivityFormModal({
<Form.Item name="name" label="活动名" rules={[{ required: true }]}> <Form.Item name="name" label="活动名" rules={[{ required: true }]}>
<Input placeholder="如:点我领取大额红包" /> <Input placeholder="如:点我领取大额红包" />
</Form.Item> </Form.Item>
<Form.Item name="platform" label="平台"> <Form.Item name="platform" label="平台" rules={[{ required: true }]}>
<Select <Select options={PLATFORM_OPTIONS} />
options={[
{ value: 'meituan', label: '美团(已支持)' },
{ value: 'taobao', label: '淘宝(待接入)', disabled: true },
{ value: 'jd', label: '京东(待接入)', disabled: true },
]}
/>
</Form.Item>
<Form.Item name="act_id" label="actId(美团活动物料 ID,推荐)">
<Input placeholder="美团联盟「我要推广-活动推广」第一列 ID,如 792" maxLength={64} />
</Form.Item>
<Form.Item name="product_view_sign" label="productViewSign(商品券,与 actId 二选一)">
<Input maxLength={128} />
</Form.Item> </Form.Item>
{platform === 'meituan' && (
<>
<Form.Item name="act_id" label="actId(美团活动物料 ID,推荐)">
<Input placeholder="美团联盟「我要推广-活动推广」第一列 ID,如 792" maxLength={64} />
</Form.Item>
<Form.Item name="product_view_sign" label="productViewSign(商品券,与 actId 二选一)">
<Input maxLength={128} />
</Form.Item>
</>
)}
{platform === 'taobao' && (
<Form.Item
name="payload"
label="淘口令(整段,运营从淘宝联盟复制,落地页原样复制给用户)"
rules={[{ required: true, message: '请填淘口令' }]}
>
<Input.TextArea rows={4} placeholder="如:8¥abcXYZ¥ https://m.tb.cn/h.xxx 长按复制本条..." maxLength={4096} />
</Form.Item>
)}
{platform === 'jd' && (
<Form.Item
name="payload"
label="京东推广链接(落地页 302 跳到它)"
rules={[{ required: true, message: '请填京东链接' }]}
>
<Input placeholder="如:https://u.jd.com/xxxxxx" maxLength={1024} />
</Form.Item>
)}
<Form.Item name="remark" label="备注"> <Form.Item name="remark" label="备注">
<Input.TextArea rows={2} maxLength={256} /> <Input.TextArea rows={2} maxLength={256} />
</Form.Item> </Form.Item>
+26 -17
View File
@@ -307,8 +307,9 @@ export interface PriceReportSummary {
// ===== CPS 分发与对账 ===== // ===== CPS 分发与对账 =====
export interface CpsGroup { export interface CpsGroup {
id: number; id: number;
sid: string; // 美团二级渠道追踪位,每群唯一 sid: string | null; // 美团群有(渠道追踪位)
name: string; name: string;
platforms: string[]; // meituan/taobao/jd 多选
member_count: number | null; member_count: number | null;
status: string; // active / archived status: string; // active / archived
remark: string | null; remark: string | null;
@@ -321,6 +322,7 @@ export interface CpsActivity {
name: string; name: string;
act_id: string | null; // 美团活动物料 ID act_id: string | null; // 美团活动物料 ID
product_view_sign: string | null; product_view_sign: string | null;
payload: string | null; // 淘宝整段淘口令 / 京东链接
status: string; status: string;
remark: string | null; remark: string | null;
created_at: string; created_at: string;
@@ -340,14 +342,17 @@ export interface CpsOrder {
pay_time: string | null; pay_time: string | null;
} }
export interface CpsReferralLink { export interface CpsReferralLinkItem {
redirect_url: string; // ★ 我们的群发短链(/c/{code}),发群用这个,点击经我们统计 activity_id: number;
code: string;
sid: string;
group_name: string;
activity_name: string; activity_name: string;
target_url: string; // 实际 302 跳转的美团短链 platform: string;
link_map: Record<string, string>; // 美团原始 1长链/2短链/3deeplink(参考) redirect_url: string; // 我们的落地页 /c/{code}(发群用)
code: string;
}
export interface CpsReferralLinks {
group_name: string;
results: CpsReferralLinkItem[];
} }
export interface CpsReconcileResult { export interface CpsReconcileResult {
@@ -358,18 +363,22 @@ export interface CpsReconcileResult {
} }
export interface CpsGroupStat { export interface CpsGroupStat {
group_id: number | null; // null = 未归群的 sid group_id: number | null;
sid: string | null; sid: string | null;
name: string; name: string;
platforms: string[];
member_count: number | null; member_count: number | null;
click_pv: number; // 点击总次数 click_pv: number;
click_uv: number; // 独立点击(ip+ua 近似去重) click_uv: number;
order_count: number; // 有效单(不含取消/风控) copy_pv: number; // 淘宝"复制口令"次数
settled_count: number; copy_uv: number;
canceled_count: number; // 对账类:淘宝/京东无法对账 → null(前端显示 "-")
gmv_cents: number; order_count: number | null;
est_commission_cents: number; settled_count: number | null;
settled_commission_cents: number; canceled_count: number | null;
gmv_cents: number | null;
est_commission_cents: number | null;
settled_commission_cents: number | null;
} }
export interface CpsStats { export interface CpsStats {