From 9faab51394cbe62177d951fba1720425c0085ac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A1=E8=8B=A5?= Date: Sun, 8 Mar 2026 10:24:41 +0800 Subject: [PATCH] =?UTF-8?q?sync:=20soul-admin=20=E9=A1=B5=E9=9D=A2=20|=20?= =?UTF-8?q?=E5=8E=9F=E5=9B=A0:=20=E5=89=8D=E7=AB=AF=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/find-partner/tabs/CKBStatsTab.tsx | 378 ++++++++++++------ 1 file changed, 262 insertions(+), 116 deletions(-) diff --git a/soul-admin/src/pages/find-partner/tabs/CKBStatsTab.tsx b/soul-admin/src/pages/find-partner/tabs/CKBStatsTab.tsx index 0b26c3fd..7892089c 100644 --- a/soul-admin/src/pages/find-partner/tabs/CKBStatsTab.tsx +++ b/soul-admin/src/pages/find-partner/tabs/CKBStatsTab.tsx @@ -1,8 +1,13 @@ import { useState, useEffect, useCallback } from 'react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' -import { RefreshCw, Users, UserCheck, TrendingUp, Zap, Link2, CheckCircle2, XCircle } from 'lucide-react' +import { + RefreshCw, Users, UserCheck, TrendingUp, Zap, Link2, + CheckCircle2, XCircle, DollarSign, Activity, Smartphone, +} from 'lucide-react' import { get, post } from '@/api/client' interface MatchStats { @@ -10,29 +15,71 @@ interface MatchStats { todayMatches: number byType: { matchType: string; count: number }[] uniqueUsers: number + matchRevenue?: number + paidMatchCount?: number } interface CKBTestResult { endpoint: string label: string + description: string + method: 'GET' | 'POST' status: 'idle' | 'testing' | 'success' | 'error' message?: string responseTime?: number + ckbResponse?: string +} + +const typeLabels: Record = { + partner: '超级个体', investor: '资源对接', mentor: '导师顾问', team: '团队招募', +} +const typeIcons: Record = { + partner: '⭐', investor: '👥', mentor: '❤️', team: '🎮', } export function CKBStatsTab() { const [stats, setStats] = useState(null) const [isLoading, setIsLoading] = useState(true) - const [ckbTests, setCkbTests] = useState([ - { endpoint: '/api/ckb/join', label: 'CKB 加入(ckb/join)', status: 'idle' }, - { endpoint: '/api/ckb/match', label: 'CKB 匹配上报(ckb/match)', status: 'idle' }, - { endpoint: '/api/miniprogram/ckb/lead', label: 'CKB 链接卡若(ckb/lead)', status: 'idle' }, - { endpoint: '/api/match/config', label: '匹配配置(match/config)', status: 'idle' }, - ]) + const [testPhone, setTestPhone] = useState('13800000000') + const [testWechat, setTestWechat] = useState('') - const typeLabels: Record = { - partner: '超级个体', investor: '资源对接', mentor: '导师顾问', team: '团队招募', - } + const [ckbTests, setCkbTests] = useState([ + { + endpoint: '/api/ckb/join', label: '场景获客 — 加入(partner)', + description: '用测试手机号添加到存客宝「创业合伙」计划', + method: 'POST', status: 'idle', + }, + { + endpoint: '/api/ckb/join', label: '场景获客 — 加入(investor)', + description: '用测试手机号添加到存客宝「资源对接」计划', + method: 'POST', status: 'idle', + }, + { + endpoint: '/api/ckb/join', label: '场景获客 — 加入(mentor)', + description: '用测试手机号添加到存客宝「导师顾问」计划', + method: 'POST', status: 'idle', + }, + { + endpoint: '/api/ckb/join', label: '场景获客 — 加入(team)', + description: '用测试手机号添加到存客宝「团队招募」计划', + method: 'POST', status: 'idle', + }, + { + endpoint: '/api/ckb/match', label: '匹配上报', + description: '上报匹配行为到存客宝', + method: 'POST', status: 'idle', + }, + { + endpoint: '/api/miniprogram/ckb/lead', label: '链接卡若', + description: '首页「链接卡若」留资到存客宝', + method: 'POST', status: 'idle', + }, + { + endpoint: '/api/match/config', label: '匹配配置', + description: '获取匹配类型、价格等配置', + method: 'GET', status: 'idle', + }, + ]) const loadStats = useCallback(async () => { setIsLoading(true) @@ -41,14 +88,9 @@ export function CKBStatsTab() { if (data?.success && data.data) { setStats(data.data) } else { - const fallback = await get<{ success?: boolean; records?: unknown[]; total?: number }>('/api/db/match-records?page=1&pageSize=1') + const fallback = await get<{ success?: boolean; total?: number }>('/api/db/match-records?page=1&pageSize=1') if (fallback?.success) { - setStats({ - totalMatches: fallback.total ?? 0, - todayMatches: 0, - byType: [], - uniqueUsers: 0, - }) + setStats({ totalMatches: fallback.total ?? 0, todayMatches: 0, byType: [], uniqueUsers: 0 }) } } } catch (e) { console.error('加载统计失败:', e) } @@ -57,43 +99,68 @@ export function CKBStatsTab() { useEffect(() => { loadStats() }, [loadStats]) + const getTestBody = (index: number) => { + const phone = testPhone.trim() + const wechat = testWechat.trim() + const typeMap = ['partner', 'investor', 'mentor', 'team'] + if (index <= 3) { + return { + type: typeMap[index], + phone: phone || undefined, + wechat: wechat || undefined, + userId: 'admin_test', + name: '后台测试', + canHelp: index === 1 ? '测试-我能帮到你' : '', + needHelp: index === 1 ? '测试-我需要什么帮助' : '', + } + } + if (index === 4) { + return { + matchType: 'partner', phone: phone || undefined, wechat: wechat || undefined, + userId: 'admin_test', nickname: '后台测试', + matchedUser: { id: 'test_matched', nickname: '测试匹配用户', matchScore: 88 }, + } + } + if (index === 5) { + return { phone: phone || undefined, wechatId: wechat || undefined, userId: 'admin_test', name: '后台测试' } + } + return {} + } + const testEndpoint = async (index: number) => { const test = ckbTests[index] + if (test.method === 'POST' && !testPhone.trim() && !testWechat.trim()) { + alert('请至少填写测试手机号或微信号') + return + } const updated = [...ckbTests] - updated[index] = { ...test, status: 'testing', message: undefined, responseTime: undefined } + updated[index] = { ...test, status: 'testing', message: undefined, responseTime: undefined, ckbResponse: undefined } setCkbTests(updated) const start = performance.now() try { - let res: { success?: boolean; message?: string; code?: number } - if (test.endpoint.includes('match/config')) { - res = await get<{ success?: boolean }>(test.endpoint) + let res: { success?: boolean; message?: string; code?: number; data?: unknown } + if (test.method === 'GET') { + res = await get(test.endpoint) } else { - res = await post<{ success?: boolean; message?: string }>(test.endpoint, { - type: 'partner', - phone: '00000000000', - wechat: 'test_ping', - userId: 'test_admin_ping', - matchType: 'partner', - name: '接口测试', - }) + res = await post(test.endpoint, getTestBody(index)) } const elapsed = Math.round(performance.now() - start) const next = [...ckbTests] - const ok = res?.success !== undefined || res?.code === 200 || res?.code === 400 + const ok = res?.success === true || res?.code === 200 next[index] = { ...test, - status: ok ? 'success' : 'error', - message: res?.message || (ok ? '接口可用' : '响应异常'), + status: ok ? 'success' : (res?.success === false ? 'error' : 'success'), + message: res?.message || (ok ? '接口正常' : '返回异常'), responseTime: elapsed, + ckbResponse: res?.data ? JSON.stringify(res.data).slice(0, 100) : undefined, } setCkbTests(next) } catch (e: unknown) { const elapsed = Math.round(performance.now() - start) const next = [...ckbTests] next[index] = { - ...test, - status: 'error', + ...test, status: 'error', message: e instanceof Error ? e.message : '请求失败', responseTime: elapsed, } @@ -102,133 +169,178 @@ export function CKBStatsTab() { } const testAll = async () => { + if (!testPhone.trim() && !testWechat.trim()) { + alert('请至少填写测试手机号或微信号') + return + } for (let i = 0; i < ckbTests.length; i++) { await testEndpoint(i) } } + const v = (n: number | undefined) => isLoading ? '-' : (n ?? 0) + return (
- {/* 统计卡片 */} -
+ {/* 核心数据 */} +
- -
-
-

总匹配次数

-

{isLoading ? '-' : (stats?.totalMatches ?? 0)}

-
- + +
+
+

总匹配次数

{v(stats?.totalMatches)}

- -
-
-

今日匹配

-

{isLoading ? '-' : (stats?.todayMatches ?? 0)}

-
- + +
+
+

今日匹配

{v(stats?.todayMatches)}

- -
-
-

独立用户数

-

{isLoading ? '-' : (stats?.uniqueUsers ?? 0)}

-
- + +
+
+

匹配用户数

{v(stats?.uniqueUsers)}

- -
-
-

人均匹配

-

- {isLoading ? '-' : (stats?.uniqueUsers ? (stats.totalMatches / stats.uniqueUsers).toFixed(1) : '0')} -

-
- + +
+
+

人均匹配

{isLoading ? '-' : (stats?.uniqueUsers ? (stats.totalMatches / stats.uniqueUsers).toFixed(1) : '0')}

+
+
+ + + +
+
+

匹配收益

¥{v(stats?.matchRevenue)}

+
+
+
+ + +
+
+

付费匹配

{v(stats?.paidMatchCount)}

{/* 按类型分布 */} - {stats?.byType && stats.byType.length > 0 && ( - - - 按类型分布 - - -
- {stats.byType.map(item => ( -
-

{typeLabels[item.matchType] || item.matchType}

-

{item.count}

-

- {stats.totalMatches > 0 ? ((item.count / stats.totalMatches) * 100).toFixed(1) : 0}% -

-
- ))} -
-
-
- )} - - {/* CKB 接口测试 */} - -
- - - 存客宝接口连通性测试 - -

- 测试所有找伙伴相关的 CKB 接口是否正常可用 -

-
-
- - + + + {stats?.byType && stats.byType.length > 0 ? ( +
+ {stats.byType.map(item => { + const pct = stats.totalMatches > 0 ? ((item.count / stats.totalMatches) * 100) : 0 + return ( +
+
+ {typeIcons[item.matchType] || '📊'} + {typeLabels[item.matchType] || item.matchType} +
+

{item.count}

+
+
+
+
+

{pct.toFixed(1)}% 占比

+
+
+ ) + })} +
+ ) : ( +

{isLoading ? '加载中...' : '暂无匹配数据'}

+ )} + + + + {/* CKB 接口联通测试 */} + + +
+
+ + + 存客宝接口联通测试 + +

+ 点击测试会用下方手机号/微信号真实添加到存客宝对应计划中 +

+
+
+
+
+ +
+ + setTestPhone(e.target.value)} + /> +
+
+
+ 💬 +
+ + setTestWechat(e.target.value)} + /> +
+
+
-
+
{ckbTests.map((test, idx) => ( -
-
- {test.status === 'idle' &&
} - {test.status === 'testing' && } - {test.status === 'success' && } - {test.status === 'error' && } -
+
+
+ {test.status === 'idle' &&
} + {test.status === 'testing' && } + {test.status === 'success' && } + {test.status === 'error' && } +

{test.label}

-

{test.endpoint}

+

{test.description}

-
+
{test.message && ( - + {test.message} )} {test.responseTime !== undefined && ( - {test.responseTime}ms + {test.responseTime}ms )}
@@ -237,6 +349,40 @@ export function CKBStatsTab() {
+ + {/* 协作需求登记 */} + + + + 📋 存客宝协作需求(待发给存客宝团队) + + + +
+
+ 待开发 +
+

场景获客接口回馈 — 添加好友成功率反馈

+

当前 scenarios API 只返回「新增成功/已存在」,需要新增字段返回该线索的微信添加状态(已添加/待添加/添加失败)和添加时间

+
+
+
+ 待开发 +
+

线索查询接口 — 按手机号/微信号查询添加结果

+

需要一个 GET 接口,传入 phone/wechatId 可查询该线索在存客宝中的状态(是否已添加好友、所属计划、标签等)

+
+
+
+ 待确认 +
+

批量线索统计接口 — 查询某时间段内的添加成功率

+

需要一个统计接口,返回指定时间段内上报的线索总数、已添加好友数、添加成功率、各计划分布

+
+
+
+
+
) }