1461 lines
59 KiB
TypeScript
1461 lines
59 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useEffect } from "react"
|
||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Badge } from "@/components/ui/badge"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Label } from "@/components/ui/label"
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||
import { Switch } from "@/components/ui/switch"
|
||
import { Textarea } from "@/components/ui/textarea"
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from "@/components/ui/dialog"
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/components/ui/select"
|
||
import {
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableHead,
|
||
TableHeader,
|
||
TableRow,
|
||
} from "@/components/ui/table"
|
||
import {
|
||
Server,
|
||
Copy,
|
||
CheckCircle2,
|
||
Key,
|
||
RefreshCw,
|
||
ExternalLink,
|
||
Code,
|
||
Zap,
|
||
Users,
|
||
Search,
|
||
Tags,
|
||
Brain,
|
||
Database,
|
||
FileText,
|
||
Package,
|
||
Activity,
|
||
Plus,
|
||
Settings,
|
||
ArrowDownToLine,
|
||
ArrowUpFromLine,
|
||
Shield,
|
||
Clock,
|
||
Eye,
|
||
EyeOff,
|
||
Trash2,
|
||
Edit,
|
||
BarChart3,
|
||
AlertCircle,
|
||
CheckCircle,
|
||
XCircle,
|
||
Play,
|
||
} from "lucide-react"
|
||
|
||
// 接入方类型
|
||
interface Partner {
|
||
id: string
|
||
name: string
|
||
type: 'external' | 'internal' // 外部系统/内部系统
|
||
description: string
|
||
apiKey: string
|
||
apiSecret: string
|
||
status: 'active' | 'inactive' | 'pending'
|
||
permissions: {
|
||
dataIngest: boolean // 数据流入权限(传入数据)
|
||
dataQuery: boolean // 数据查询权限(获取标签)
|
||
tagEnrich: boolean // 标签完善权限
|
||
batchProcess: boolean // 批量处理权限
|
||
}
|
||
quotaConfig: {
|
||
dailyLimit: number // 每日限额
|
||
monthlyLimit: number // 每月限额
|
||
rateLimit: number // 速率限制(次/秒)
|
||
}
|
||
statistics: {
|
||
totalCalls: number
|
||
todayCalls: number
|
||
monthCalls: number
|
||
lastCallTime: string
|
||
}
|
||
createdAt: string
|
||
updatedAt: string
|
||
}
|
||
|
||
// 调用记录类型
|
||
interface CallLog {
|
||
id: string
|
||
partnerId: string
|
||
partnerName: string
|
||
endpoint: string
|
||
method: string
|
||
status: 'success' | 'error' | 'pending'
|
||
responseTime: number
|
||
requestSize: number
|
||
responseSize: number
|
||
timestamp: string
|
||
errorMessage?: string
|
||
}
|
||
|
||
// 开放API端点定义
|
||
interface OpenAPIEndpoint {
|
||
id: string
|
||
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||
path: string
|
||
name: string
|
||
description: string
|
||
category: 'ingest' | 'query' | 'tag' | 'batch'
|
||
params: { name: string; type: string; required: boolean; desc: string }[]
|
||
response: string
|
||
example: {
|
||
request: string
|
||
response: string
|
||
}
|
||
}
|
||
|
||
// 预定义接入方
|
||
const DEFAULT_PARTNERS: Partner[] = [
|
||
{
|
||
id: 'ckb_001',
|
||
name: '存客宝',
|
||
type: 'external',
|
||
description: '私域流量管理系统,提供流量词上报和用户画像获取',
|
||
apiKey: 'sk-ckb-xxxxxxxxxx',
|
||
apiSecret: 'sec-ckb-xxxxxxxxxx',
|
||
status: 'active',
|
||
permissions: {
|
||
dataIngest: true,
|
||
dataQuery: true,
|
||
tagEnrich: true,
|
||
batchProcess: true,
|
||
},
|
||
quotaConfig: {
|
||
dailyLimit: 10000,
|
||
monthlyLimit: 300000,
|
||
rateLimit: 100,
|
||
},
|
||
statistics: {
|
||
totalCalls: 158420,
|
||
todayCalls: 1520,
|
||
monthCalls: 45800,
|
||
lastCallTime: '2026-01-31 18:45:22',
|
||
},
|
||
createdAt: '2025-06-15',
|
||
updatedAt: '2026-01-31',
|
||
},
|
||
{
|
||
id: 'dlm_001',
|
||
name: '点了码',
|
||
type: 'external',
|
||
description: '线下扫码营销系统,同步用户行为数据',
|
||
apiKey: 'sk-dlm-xxxxxxxxxx',
|
||
apiSecret: 'sec-dlm-xxxxxxxxxx',
|
||
status: 'active',
|
||
permissions: {
|
||
dataIngest: true,
|
||
dataQuery: true,
|
||
tagEnrich: false,
|
||
batchProcess: false,
|
||
},
|
||
quotaConfig: {
|
||
dailyLimit: 5000,
|
||
monthlyLimit: 150000,
|
||
rateLimit: 50,
|
||
},
|
||
statistics: {
|
||
totalCalls: 89200,
|
||
todayCalls: 820,
|
||
monthCalls: 28500,
|
||
lastCallTime: '2026-01-31 18:30:15',
|
||
},
|
||
createdAt: '2025-08-20',
|
||
updatedAt: '2026-01-31',
|
||
},
|
||
{
|
||
id: 'feishu_001',
|
||
name: '飞书机器人',
|
||
type: 'internal',
|
||
description: '企业内部飞书机器人,用于用户查询和报告推送',
|
||
apiKey: 'sk-fs-xxxxxxxxxx',
|
||
apiSecret: 'sec-fs-xxxxxxxxxx',
|
||
status: 'active',
|
||
permissions: {
|
||
dataIngest: false,
|
||
dataQuery: true,
|
||
tagEnrich: false,
|
||
batchProcess: false,
|
||
},
|
||
quotaConfig: {
|
||
dailyLimit: 1000,
|
||
monthlyLimit: 30000,
|
||
rateLimit: 10,
|
||
},
|
||
statistics: {
|
||
totalCalls: 12500,
|
||
todayCalls: 156,
|
||
monthCalls: 4200,
|
||
lastCallTime: '2026-01-31 18:50:00',
|
||
},
|
||
createdAt: '2025-10-10',
|
||
updatedAt: '2026-01-31',
|
||
},
|
||
]
|
||
|
||
// 开放API端点列表
|
||
const OPEN_API_ENDPOINTS: OpenAPIEndpoint[] = [
|
||
// 数据流入接口
|
||
{
|
||
id: 'ingest_1',
|
||
method: 'POST',
|
||
path: '/api/open-api/ingest/user',
|
||
name: '用户数据流入',
|
||
description: '第三方系统传入用户数据,神射手自动完善标签',
|
||
category: 'ingest',
|
||
params: [
|
||
{ name: 'phone', type: 'string', required: true, desc: '手机号(11位)' },
|
||
{ name: 'name', type: 'string', required: false, desc: '用户姓名' },
|
||
{ name: 'source', type: 'string', required: true, desc: '数据来源标识' },
|
||
{ name: 'tags', type: 'array', required: false, desc: '原始标签数组' },
|
||
{ name: 'extra', type: 'object', required: false, desc: '扩展字段' },
|
||
],
|
||
response: '{ "success": true, "data": { "userId": "xxx", "enriched": true, "tags": [...] } }',
|
||
example: {
|
||
request: `{
|
||
"phone": "13800138000",
|
||
"name": "张三",
|
||
"source": "cunkebao",
|
||
"tags": ["高意向", "电商用户"],
|
||
"extra": { "wechatId": "wxid_xxx" }
|
||
}`,
|
||
response: `{
|
||
"success": true,
|
||
"data": {
|
||
"userId": "usr_xxx",
|
||
"enriched": true,
|
||
"tags": ["高意向", "电商用户", "A级用户", "一线城市"],
|
||
"rfm_score": 85,
|
||
"user_level": "A"
|
||
}
|
||
}`,
|
||
},
|
||
},
|
||
{
|
||
id: 'ingest_2',
|
||
method: 'POST',
|
||
path: '/api/open-api/ingest/batch',
|
||
name: '批量数据流入',
|
||
description: '批量传入用户数据,支持最多1000条/次',
|
||
category: 'ingest',
|
||
params: [
|
||
{ name: 'users', type: 'array', required: true, desc: '用户数组' },
|
||
{ name: 'source', type: 'string', required: true, desc: '数据来源标识' },
|
||
{ name: 'async', type: 'boolean', required: false, desc: '是否异步处理' },
|
||
],
|
||
response: '{ "success": true, "data": { "taskId": "xxx", "total": 1000, "processed": 1000 } }',
|
||
example: {
|
||
request: `{
|
||
"source": "cunkebao",
|
||
"async": true,
|
||
"users": [
|
||
{ "phone": "13800138001", "name": "张三" },
|
||
{ "phone": "13800138002", "name": "李四" }
|
||
]
|
||
}`,
|
||
response: `{
|
||
"success": true,
|
||
"data": {
|
||
"taskId": "task_xxx",
|
||
"total": 2,
|
||
"processed": 0,
|
||
"status": "processing"
|
||
}
|
||
}`,
|
||
},
|
||
},
|
||
// 数据查询接口
|
||
{
|
||
id: 'query_1',
|
||
method: 'GET',
|
||
path: '/api/open-api/query/user',
|
||
name: '用户画像查询',
|
||
description: '根据手机号/QQ获取完整用户画像和标签',
|
||
category: 'query',
|
||
params: [
|
||
{ name: 'phone', type: 'string', required: false, desc: '手机号' },
|
||
{ name: 'qq', type: 'string', required: false, desc: 'QQ号' },
|
||
{ name: 'fields', type: 'string', required: false, desc: '返回字段(逗号分隔)' },
|
||
],
|
||
response: '{ "success": true, "data": { "phone": "xxx", "tags": [...], "rfm": {...} } }',
|
||
example: {
|
||
request: `GET /api/open-api/query/user?phone=13800138000&fields=tags,rfm,level`,
|
||
response: `{
|
||
"success": true,
|
||
"data": {
|
||
"phone": "138****8000",
|
||
"name": "张*",
|
||
"tags": ["高价值用户", "电商活跃", "一线城市"],
|
||
"rfm": { "r": 5, "f": 4, "m": 5, "score": 93 },
|
||
"level": "S",
|
||
"traffic_pool": "钻石池"
|
||
}
|
||
}`,
|
||
},
|
||
},
|
||
{
|
||
id: 'query_2',
|
||
method: 'POST',
|
||
path: '/api/open-api/query/batch',
|
||
name: '批量用户查询',
|
||
description: '批量查询用户画像,支持最多500个/次',
|
||
category: 'query',
|
||
params: [
|
||
{ name: 'phones', type: 'array', required: false, desc: '手机号数组' },
|
||
{ name: 'qqs', type: 'array', required: false, desc: 'QQ号数组' },
|
||
{ name: 'fields', type: 'array', required: false, desc: '返回字段数组' },
|
||
],
|
||
response: '{ "success": true, "data": { "users": [...], "notFound": [...] } }',
|
||
example: {
|
||
request: `{
|
||
"phones": ["13800138001", "13800138002"],
|
||
"fields": ["tags", "level"]
|
||
}`,
|
||
response: `{
|
||
"success": true,
|
||
"data": {
|
||
"users": [
|
||
{ "phone": "138****8001", "tags": [...], "level": "A" },
|
||
{ "phone": "138****8002", "tags": [...], "level": "B" }
|
||
],
|
||
"notFound": []
|
||
}
|
||
}`,
|
||
},
|
||
},
|
||
// 标签服务接口
|
||
{
|
||
id: 'tag_1',
|
||
method: 'POST',
|
||
path: '/api/open-api/tag/enrich',
|
||
name: '标签完善',
|
||
description: '对用户进行AI标签完善,返回新增标签',
|
||
category: 'tag',
|
||
params: [
|
||
{ name: 'phone', type: 'string', required: true, desc: '手机号' },
|
||
{ name: 'strategy', type: 'string', required: false, desc: '打标策略:rfm/behavior/preference' },
|
||
{ name: 'force', type: 'boolean', required: false, desc: '是否强制刷新' },
|
||
],
|
||
response: '{ "success": true, "data": { "newTags": [...], "allTags": [...] } }',
|
||
example: {
|
||
request: `{
|
||
"phone": "13800138000",
|
||
"strategy": "rfm",
|
||
"force": false
|
||
}`,
|
||
response: `{
|
||
"success": true,
|
||
"data": {
|
||
"newTags": ["高价值用户", "活跃用户"],
|
||
"allTags": ["高价值用户", "活跃用户", "电商用户", "一线城市"],
|
||
"rfm_score": 89
|
||
}
|
||
}`,
|
||
},
|
||
},
|
||
{
|
||
id: 'tag_2',
|
||
method: 'GET',
|
||
path: '/api/open-api/tag/list',
|
||
name: '标签列表',
|
||
description: '获取所有可用标签及其统计',
|
||
category: 'tag',
|
||
params: [
|
||
{ name: 'category', type: 'string', required: false, desc: '标签分类' },
|
||
{ name: 'source', type: 'string', required: false, desc: '来源筛选' },
|
||
],
|
||
response: '{ "success": true, "data": { "tags": [...], "total": 100 } }',
|
||
example: {
|
||
request: `GET /api/open-api/tag/list?category=value`,
|
||
response: `{
|
||
"success": true,
|
||
"data": {
|
||
"tags": [
|
||
{ "id": "tag_001", "name": "高价值用户", "count": 125000 },
|
||
{ "id": "tag_002", "name": "优质用户", "count": 850000 }
|
||
],
|
||
"total": 45
|
||
}
|
||
}`,
|
||
},
|
||
},
|
||
// 批量处理接口
|
||
{
|
||
id: 'batch_1',
|
||
method: 'POST',
|
||
path: '/api/open-api/batch/task',
|
||
name: '创建批量任务',
|
||
description: '创建异步批量处理任务',
|
||
category: 'batch',
|
||
params: [
|
||
{ name: 'type', type: 'string', required: true, desc: '任务类型:enrich/export/sync' },
|
||
{ name: 'config', type: 'object', required: true, desc: '任务配置' },
|
||
{ name: 'callback', type: 'string', required: false, desc: '完成回调URL' },
|
||
],
|
||
response: '{ "success": true, "data": { "taskId": "xxx", "status": "pending" } }',
|
||
example: {
|
||
request: `{
|
||
"type": "enrich",
|
||
"config": {
|
||
"source": "cunkebao",
|
||
"strategy": "rfm",
|
||
"dateRange": "2026-01"
|
||
},
|
||
"callback": "https://your-domain.com/webhook"
|
||
}`,
|
||
response: `{
|
||
"success": true,
|
||
"data": {
|
||
"taskId": "task_xxx",
|
||
"status": "pending",
|
||
"estimatedTime": "10分钟"
|
||
}
|
||
}`,
|
||
},
|
||
},
|
||
{
|
||
id: 'batch_2',
|
||
method: 'GET',
|
||
path: '/api/open-api/batch/status',
|
||
name: '查询任务状态',
|
||
description: '查询批量任务执行状态',
|
||
category: 'batch',
|
||
params: [
|
||
{ name: 'taskId', type: 'string', required: true, desc: '任务ID' },
|
||
],
|
||
response: '{ "success": true, "data": { "status": "completed", "progress": 100 } }',
|
||
example: {
|
||
request: `GET /api/open-api/batch/status?taskId=task_xxx`,
|
||
response: `{
|
||
"success": true,
|
||
"data": {
|
||
"taskId": "task_xxx",
|
||
"status": "completed",
|
||
"progress": 100,
|
||
"total": 10000,
|
||
"processed": 10000,
|
||
"failed": 5,
|
||
"completedAt": "2026-01-31T18:45:00Z"
|
||
}
|
||
}`,
|
||
},
|
||
},
|
||
]
|
||
|
||
// 调用日志数据
|
||
const DEFAULT_CALL_LOGS: CallLog[] = [
|
||
{ id: 'log_1', partnerId: 'ckb_001', partnerName: '存客宝', endpoint: '/api/open-api/ingest/user', method: 'POST', status: 'success', responseTime: 45, requestSize: 256, responseSize: 512, timestamp: '2026-01-31 18:50:22' },
|
||
{ id: 'log_2', partnerId: 'ckb_001', partnerName: '存客宝', endpoint: '/api/open-api/query/user', method: 'GET', status: 'success', responseTime: 32, requestSize: 64, responseSize: 1024, timestamp: '2026-01-31 18:49:55' },
|
||
{ id: 'log_3', partnerId: 'dlm_001', partnerName: '点了码', endpoint: '/api/open-api/ingest/batch', method: 'POST', status: 'success', responseTime: 1250, requestSize: 45000, responseSize: 2048, timestamp: '2026-01-31 18:48:30' },
|
||
{ id: 'log_4', partnerId: 'ckb_001', partnerName: '存客宝', endpoint: '/api/open-api/tag/enrich', method: 'POST', status: 'success', responseTime: 89, requestSize: 128, responseSize: 768, timestamp: '2026-01-31 18:47:10' },
|
||
{ id: 'log_5', partnerId: 'feishu_001', partnerName: '飞书机器人', endpoint: '/api/open-api/query/user', method: 'GET', status: 'error', responseTime: 15, requestSize: 32, responseSize: 128, timestamp: '2026-01-31 18:46:00', errorMessage: '用户不存在' },
|
||
]
|
||
|
||
export default function OpenAPIPage() {
|
||
const [partners, setPartners] = useState<Partner[]>(DEFAULT_PARTNERS)
|
||
const [callLogs, setCallLogs] = useState<CallLog[]>(DEFAULT_CALL_LOGS)
|
||
const [activeTab, setActiveTab] = useState('guide')
|
||
const [selectedCategory, setSelectedCategory] = useState('all')
|
||
const [showAddDialog, setShowAddDialog] = useState(false)
|
||
const [showKeyDialog, setShowKeyDialog] = useState(false)
|
||
const [showDetailDialog, setShowDetailDialog] = useState(false)
|
||
const [selectedPartner, setSelectedPartner] = useState<Partner | null>(null)
|
||
const [selectedEndpoint, setSelectedEndpoint] = useState<OpenAPIEndpoint | null>(null)
|
||
const [copiedId, setCopiedId] = useState<string | null>(null)
|
||
const [apiBaseUrl, setApiBaseUrl] = useState('')
|
||
const [newPartner, setNewPartner] = useState({
|
||
name: '',
|
||
type: 'external' as 'external' | 'internal',
|
||
description: '',
|
||
permissions: {
|
||
dataIngest: true,
|
||
dataQuery: true,
|
||
tagEnrich: false,
|
||
batchProcess: false,
|
||
},
|
||
quotaConfig: {
|
||
dailyLimit: 5000,
|
||
monthlyLimit: 150000,
|
||
rateLimit: 50,
|
||
},
|
||
})
|
||
|
||
useEffect(() => {
|
||
const host = typeof window !== 'undefined' ? window.location.origin : ''
|
||
setApiBaseUrl(host)
|
||
}, [])
|
||
|
||
const copyToClipboard = (text: string, id: string) => {
|
||
navigator.clipboard.writeText(text)
|
||
setCopiedId(id)
|
||
setTimeout(() => setCopiedId(null), 2000)
|
||
}
|
||
|
||
const getMethodColor = (method: string) => {
|
||
switch (method) {
|
||
case 'GET': return 'bg-green-100 text-green-700'
|
||
case 'POST': return 'bg-blue-100 text-blue-700'
|
||
case 'PUT': return 'bg-yellow-100 text-yellow-700'
|
||
case 'DELETE': return 'bg-red-100 text-red-700'
|
||
default: return 'bg-gray-100 text-gray-700'
|
||
}
|
||
}
|
||
|
||
const getCategoryIcon = (category: string) => {
|
||
switch (category) {
|
||
case 'ingest': return <ArrowDownToLine className="h-4 w-4" />
|
||
case 'query': return <Search className="h-4 w-4" />
|
||
case 'tag': return <Tags className="h-4 w-4" />
|
||
case 'batch': return <Package className="h-4 w-4" />
|
||
default: return <Code className="h-4 w-4" />
|
||
}
|
||
}
|
||
|
||
const getCategoryName = (category: string) => {
|
||
switch (category) {
|
||
case 'ingest': return '数据流入'
|
||
case 'query': return '数据查询'
|
||
case 'tag': return '标签服务'
|
||
case 'batch': return '批量处理'
|
||
default: return category
|
||
}
|
||
}
|
||
|
||
const getStatusBadge = (status: string) => {
|
||
switch (status) {
|
||
case 'active':
|
||
return <Badge className="bg-green-100 text-green-700">已启用</Badge>
|
||
case 'inactive':
|
||
return <Badge className="bg-gray-100 text-gray-700">已停用</Badge>
|
||
case 'pending':
|
||
return <Badge className="bg-yellow-100 text-yellow-700">待审核</Badge>
|
||
case 'success':
|
||
return <Badge className="bg-green-100 text-green-700">成功</Badge>
|
||
case 'error':
|
||
return <Badge className="bg-red-100 text-red-700">失败</Badge>
|
||
default:
|
||
return <Badge>{status}</Badge>
|
||
}
|
||
}
|
||
|
||
const filteredEndpoints = selectedCategory === 'all'
|
||
? OPEN_API_ENDPOINTS
|
||
: OPEN_API_ENDPOINTS.filter(e => e.category === selectedCategory)
|
||
|
||
const handleAddPartner = () => {
|
||
const newId = `partner_${Date.now()}`
|
||
const apiKey = `sk-${newPartner.type.slice(0, 3)}-${Math.random().toString(36).slice(2, 12)}`
|
||
const apiSecret = `sec-${newPartner.type.slice(0, 3)}-${Math.random().toString(36).slice(2, 12)}`
|
||
|
||
const partner: Partner = {
|
||
id: newId,
|
||
name: newPartner.name,
|
||
type: newPartner.type,
|
||
description: newPartner.description,
|
||
apiKey,
|
||
apiSecret,
|
||
status: 'pending',
|
||
permissions: newPartner.permissions,
|
||
quotaConfig: newPartner.quotaConfig,
|
||
statistics: {
|
||
totalCalls: 0,
|
||
todayCalls: 0,
|
||
monthCalls: 0,
|
||
lastCallTime: '-',
|
||
},
|
||
createdAt: new Date().toISOString().split('T')[0],
|
||
updatedAt: new Date().toISOString().split('T')[0],
|
||
}
|
||
|
||
setPartners([...partners, partner])
|
||
setShowAddDialog(false)
|
||
setNewPartner({
|
||
name: '',
|
||
type: 'external',
|
||
description: '',
|
||
permissions: {
|
||
dataIngest: true,
|
||
dataQuery: true,
|
||
tagEnrich: false,
|
||
batchProcess: false,
|
||
},
|
||
quotaConfig: {
|
||
dailyLimit: 5000,
|
||
monthlyLimit: 150000,
|
||
rateLimit: 50,
|
||
},
|
||
})
|
||
}
|
||
|
||
const togglePartnerStatus = (partnerId: string) => {
|
||
setPartners(partners.map(p => {
|
||
if (p.id === partnerId) {
|
||
return { ...p, status: p.status === 'active' ? 'inactive' : 'active' }
|
||
}
|
||
return p
|
||
}))
|
||
}
|
||
|
||
const togglePermission = (partnerId: string, permission: keyof Partner['permissions']) => {
|
||
setPartners(partners.map(p => {
|
||
if (p.id === partnerId) {
|
||
return {
|
||
...p,
|
||
permissions: {
|
||
...p.permissions,
|
||
[permission]: !p.permissions[permission],
|
||
},
|
||
}
|
||
}
|
||
return p
|
||
}))
|
||
}
|
||
|
||
// 计算统计数据
|
||
const totalCalls = partners.reduce((sum, p) => sum + p.statistics.todayCalls, 0)
|
||
const activePartners = partners.filter(p => p.status === 'active').length
|
||
const totalQuota = partners.reduce((sum, p) => sum + p.quotaConfig.dailyLimit, 0)
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/20">
|
||
<div className="p-6 space-y-6">
|
||
{/* 顶部标题 */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-gray-900">开放API管理</h1>
|
||
<p className="text-sm text-gray-500 mt-1">第三方系统接入管理,数据流入流出控制</p>
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
<Button variant="outline" onClick={() => setShowAddDialog(true)}>
|
||
<Plus className="h-4 w-4 mr-2" />
|
||
添加接入方
|
||
</Button>
|
||
<Button>
|
||
<ExternalLink className="h-4 w-4 mr-2" />
|
||
API文档
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 统计卡片 */}
|
||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||
<Card className="border-0 shadow-sm bg-white/80">
|
||
<CardContent className="p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-gray-500">接入方数量</p>
|
||
<p className="text-2xl font-bold text-gray-900">{partners.length}</p>
|
||
<p className="text-xs text-green-600">{activePartners} 个已启用</p>
|
||
</div>
|
||
<div className="h-12 w-12 rounded-xl bg-purple-100 flex items-center justify-center">
|
||
<Users className="h-6 w-6 text-purple-600" />
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="border-0 shadow-sm bg-white/80">
|
||
<CardContent className="p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-gray-500">今日调用</p>
|
||
<p className="text-2xl font-bold text-gray-900">{totalCalls.toLocaleString()}</p>
|
||
<p className="text-xs text-gray-500">总配额 {totalQuota.toLocaleString()}</p>
|
||
</div>
|
||
<div className="h-12 w-12 rounded-xl bg-blue-100 flex items-center justify-center">
|
||
<Activity className="h-6 w-6 text-blue-600" />
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="border-0 shadow-sm bg-white/80">
|
||
<CardContent className="p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-gray-500">数据流入</p>
|
||
<p className="text-2xl font-bold text-gray-900">1,520</p>
|
||
<p className="text-xs text-green-600">+12.5% 较昨日</p>
|
||
</div>
|
||
<div className="h-12 w-12 rounded-xl bg-green-100 flex items-center justify-center">
|
||
<ArrowDownToLine className="h-6 w-6 text-green-600" />
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="border-0 shadow-sm bg-white/80">
|
||
<CardContent className="p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-gray-500">标签完善</p>
|
||
<p className="text-2xl font-bold text-gray-900">856</p>
|
||
<p className="text-xs text-purple-600">98.5% 成功率</p>
|
||
</div>
|
||
<div className="h-12 w-12 rounded-xl bg-orange-100 flex items-center justify-center">
|
||
<Tags className="h-6 w-6 text-orange-600" />
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* 快速开始指南 */}
|
||
<Card className="border-0 shadow-sm bg-gradient-to-r from-blue-50 to-purple-50">
|
||
<CardContent className="p-5">
|
||
<div className="flex items-start gap-6">
|
||
<div className="flex-1">
|
||
<h3 className="font-semibold text-gray-900 mb-3">快速开始</h3>
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
||
<div className="flex items-start gap-2">
|
||
<div className="h-6 w-6 rounded-full bg-blue-500 text-white flex items-center justify-center text-xs font-bold shrink-0">1</div>
|
||
<div>
|
||
<p className="font-medium text-gray-900">获取密钥</p>
|
||
<p className="text-gray-500">点击接入方的"密钥"按钮获取 API Key 和 Secret</p>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-start gap-2">
|
||
<div className="h-6 w-6 rounded-full bg-purple-500 text-white flex items-center justify-center text-xs font-bold shrink-0">2</div>
|
||
<div>
|
||
<p className="font-medium text-gray-900">配置权限</p>
|
||
<p className="text-gray-500">在"配置"中开启数据流入、查询、标签完善等权限</p>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-start gap-2">
|
||
<div className="h-6 w-6 rounded-full bg-green-500 text-white flex items-center justify-center text-xs font-bold shrink-0">3</div>
|
||
<div>
|
||
<p className="font-medium text-gray-900">调用接口</p>
|
||
<p className="text-gray-500">在请求头中携带 Authorization: Bearer API_KEY 即可调用</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="hidden lg:block p-3 rounded-lg bg-gray-900 text-xs font-mono">
|
||
<p className="text-gray-400 mb-1"># 示例:数据流入</p>
|
||
<p className="text-green-400">curl -X POST "{apiBaseUrl}/api/open-api?endpoint=ingest/user" \</p>
|
||
<p className="text-blue-400 pl-4">-H "Authorization: Bearer sk-xxx" \</p>
|
||
<p className="text-yellow-400 pl-4">-d '{`{"phone":"138xxx","source":"ckb"}`}'</p>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 主体内容 */}
|
||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||
<TabsList className="bg-white/80">
|
||
<TabsTrigger value="guide">
|
||
<FileText className="h-4 w-4 mr-2" />
|
||
使用说明
|
||
</TabsTrigger>
|
||
<TabsTrigger value="partners">
|
||
<Users className="h-4 w-4 mr-2" />
|
||
接入方管理
|
||
</TabsTrigger>
|
||
<TabsTrigger value="endpoints">
|
||
<Code className="h-4 w-4 mr-2" />
|
||
API端点
|
||
</TabsTrigger>
|
||
<TabsTrigger value="logs">
|
||
<Clock className="h-4 w-4 mr-2" />
|
||
调用日志
|
||
</TabsTrigger>
|
||
</TabsList>
|
||
|
||
{/* 使用说明 */}
|
||
<TabsContent value="guide" className="mt-4">
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||
{/* 接口概述 */}
|
||
<Card className="border-0 shadow-sm bg-white/80">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="text-base">接口概述</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-3 text-sm">
|
||
<p className="text-gray-600">
|
||
神射手开放API为第三方系统提供用户数据对接能力,支持存客宝、点了码等系统接入。
|
||
</p>
|
||
<div className="space-y-2">
|
||
<div className="flex items-center gap-2">
|
||
<Badge className="bg-green-100 text-green-700">数据流入</Badge>
|
||
<span className="text-gray-600">传入用户数据,自动完善标签</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Badge className="bg-blue-100 text-blue-700">数据查询</Badge>
|
||
<span className="text-gray-600">查询用户画像、标签、RFM等</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Badge className="bg-purple-100 text-purple-700">标签完善</Badge>
|
||
<span className="text-gray-600">AI智能标签完善</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Badge className="bg-orange-100 text-orange-700">批量处理</Badge>
|
||
<span className="text-gray-600">大批量异步任务处理</span>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 认证方式 */}
|
||
<Card className="border-0 shadow-sm bg-white/80">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="text-base">认证方式</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-3 text-sm">
|
||
<p className="text-gray-600">所有API请求需在Header中携带API密钥:</p>
|
||
<div className="p-3 rounded-lg bg-gray-900 text-xs font-mono space-y-1">
|
||
<p className="text-blue-400">Authorization: Bearer YOUR_API_KEY</p>
|
||
<p className="text-gray-500"># 可选:增强安全性</p>
|
||
<p className="text-purple-400">X-API-Secret: YOUR_API_SECRET</p>
|
||
</div>
|
||
<p className="text-gray-500 text-xs">API Key 和 Secret 在接入方管理中获取</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 数据流入示例 */}
|
||
<Card className="border-0 shadow-sm bg-white/80">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="text-base">数据流入示例</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="text-sm">
|
||
<div className="p-3 rounded-lg bg-gray-900 text-xs font-mono space-y-1">
|
||
<p className="text-gray-400"># 存客宝传入用户数据</p>
|
||
<p className="text-green-400">POST /api/open-api?endpoint=ingest/user</p>
|
||
<p className="text-yellow-400">{`{`}</p>
|
||
<p className="text-white pl-4">"phone": "13800138000",</p>
|
||
<p className="text-white pl-4">"name": "张三",</p>
|
||
<p className="text-white pl-4">"source": "cunkebao",</p>
|
||
<p className="text-white pl-4">"tags": ["高意向", "电商用户"]</p>
|
||
<p className="text-yellow-400">{`}`}</p>
|
||
</div>
|
||
<p className="text-gray-500 text-xs mt-2">返回完善后的标签:高意向、电商用户、A级用户、一线城市...</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 数据查询示例 */}
|
||
<Card className="border-0 shadow-sm bg-white/80">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="text-base">数据查询示例</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="text-sm">
|
||
<div className="p-3 rounded-lg bg-gray-900 text-xs font-mono space-y-1">
|
||
<p className="text-gray-400"># 查询用户画像</p>
|
||
<p className="text-green-400">GET /api/open-api?endpoint=query/user&phone=13800138000</p>
|
||
<p className="text-gray-400 mt-2"># 响应</p>
|
||
<p className="text-yellow-400">{`{`}</p>
|
||
<p className="text-white pl-4">"phone": "138****8000",</p>
|
||
<p className="text-white pl-4">"tags": ["高价值用户", "电商活跃"],</p>
|
||
<p className="text-white pl-4">"level": "A",</p>
|
||
<p className="text-white pl-4">"rfm": {`{ "r": 5, "f": 4, "m": 5, "score": 93 }`}</p>
|
||
<p className="text-yellow-400">{`}`}</p>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 常用端点 */}
|
||
<Card className="border-0 shadow-sm bg-white/80 lg:col-span-2">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="text-base">常用端点</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
|
||
<div className="p-3 rounded-lg bg-gray-50">
|
||
<Badge className="bg-blue-100 text-blue-700 mb-2">POST</Badge>
|
||
<p className="font-mono text-xs text-gray-700">ingest/user</p>
|
||
<p className="text-xs text-gray-500 mt-1">单用户数据流入</p>
|
||
</div>
|
||
<div className="p-3 rounded-lg bg-gray-50">
|
||
<Badge className="bg-blue-100 text-blue-700 mb-2">POST</Badge>
|
||
<p className="font-mono text-xs text-gray-700">ingest/batch</p>
|
||
<p className="text-xs text-gray-500 mt-1">批量数据流入</p>
|
||
</div>
|
||
<div className="p-3 rounded-lg bg-gray-50">
|
||
<Badge className="bg-green-100 text-green-700 mb-2">GET</Badge>
|
||
<p className="font-mono text-xs text-gray-700">query/user</p>
|
||
<p className="text-xs text-gray-500 mt-1">用户画像查询</p>
|
||
</div>
|
||
<div className="p-3 rounded-lg bg-gray-50">
|
||
<Badge className="bg-blue-100 text-blue-700 mb-2">POST</Badge>
|
||
<p className="font-mono text-xs text-gray-700">tag/enrich</p>
|
||
<p className="text-xs text-gray-500 mt-1">AI标签完善</p>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</TabsContent>
|
||
|
||
{/* 接入方管理 */}
|
||
<TabsContent value="partners" className="mt-4">
|
||
<div className="space-y-4">
|
||
{partners.map(partner => (
|
||
<Card key={partner.id} className="border-0 shadow-sm bg-white/80">
|
||
<CardContent className="p-4">
|
||
<div className="flex items-start justify-between">
|
||
<div className="flex items-start gap-4">
|
||
<div className={`h-12 w-12 rounded-xl flex items-center justify-center ${
|
||
partner.type === 'external' ? 'bg-purple-100' : 'bg-blue-100'
|
||
}`}>
|
||
{partner.type === 'external' ? (
|
||
<Server className={`h-6 w-6 ${partner.type === 'external' ? 'text-purple-600' : 'text-blue-600'}`} />
|
||
) : (
|
||
<Shield className="h-6 w-6 text-blue-600" />
|
||
)}
|
||
</div>
|
||
<div>
|
||
<div className="flex items-center gap-2">
|
||
<h3 className="font-semibold text-gray-900">{partner.name}</h3>
|
||
{getStatusBadge(partner.status)}
|
||
<Badge variant="outline" className="text-xs">
|
||
{partner.type === 'external' ? '外部系统' : '内部系统'}
|
||
</Badge>
|
||
</div>
|
||
<p className="text-sm text-gray-500 mt-1">{partner.description}</p>
|
||
<div className="flex items-center gap-4 mt-2 text-xs text-gray-400">
|
||
<span>创建于 {partner.createdAt}</span>
|
||
<span>最后调用 {partner.statistics.lastCallTime}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => {
|
||
setSelectedPartner(partner)
|
||
setShowKeyDialog(true)
|
||
}}
|
||
>
|
||
<Key className="h-4 w-4 mr-1" />
|
||
密钥
|
||
</Button>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => {
|
||
setSelectedPartner(partner)
|
||
setShowDetailDialog(true)
|
||
}}
|
||
>
|
||
<Settings className="h-4 w-4 mr-1" />
|
||
配置
|
||
</Button>
|
||
<Switch
|
||
checked={partner.status === 'active'}
|
||
onCheckedChange={() => togglePartnerStatus(partner.id)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 权限和配额 */}
|
||
<div className="mt-4 pt-4 border-t grid grid-cols-2 gap-4">
|
||
{/* 权限 */}
|
||
<div>
|
||
<h4 className="text-xs font-medium text-gray-500 mb-2">接口权限</h4>
|
||
<div className="flex flex-wrap gap-2">
|
||
<Badge
|
||
className={`cursor-pointer ${partner.permissions.dataIngest ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'}`}
|
||
onClick={() => togglePermission(partner.id, 'dataIngest')}
|
||
>
|
||
<ArrowDownToLine className="h-3 w-3 mr-1" />
|
||
数据流入
|
||
</Badge>
|
||
<Badge
|
||
className={`cursor-pointer ${partner.permissions.dataQuery ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-500'}`}
|
||
onClick={() => togglePermission(partner.id, 'dataQuery')}
|
||
>
|
||
<Search className="h-3 w-3 mr-1" />
|
||
数据查询
|
||
</Badge>
|
||
<Badge
|
||
className={`cursor-pointer ${partner.permissions.tagEnrich ? 'bg-purple-100 text-purple-700' : 'bg-gray-100 text-gray-500'}`}
|
||
onClick={() => togglePermission(partner.id, 'tagEnrich')}
|
||
>
|
||
<Tags className="h-3 w-3 mr-1" />
|
||
标签完善
|
||
</Badge>
|
||
<Badge
|
||
className={`cursor-pointer ${partner.permissions.batchProcess ? 'bg-orange-100 text-orange-700' : 'bg-gray-100 text-gray-500'}`}
|
||
onClick={() => togglePermission(partner.id, 'batchProcess')}
|
||
>
|
||
<Package className="h-3 w-3 mr-1" />
|
||
批量处理
|
||
</Badge>
|
||
</div>
|
||
</div>
|
||
{/* 统计 */}
|
||
<div>
|
||
<h4 className="text-xs font-medium text-gray-500 mb-2">调用统计</h4>
|
||
<div className="flex items-center gap-4 text-sm">
|
||
<div>
|
||
<span className="text-gray-500">今日</span>
|
||
<span className="ml-1 font-medium">{partner.statistics.todayCalls.toLocaleString()}</span>
|
||
<span className="text-gray-400 text-xs">/{partner.quotaConfig.dailyLimit.toLocaleString()}</span>
|
||
</div>
|
||
<div>
|
||
<span className="text-gray-500">本月</span>
|
||
<span className="ml-1 font-medium">{partner.statistics.monthCalls.toLocaleString()}</span>
|
||
</div>
|
||
<div>
|
||
<span className="text-gray-500">累计</span>
|
||
<span className="ml-1 font-medium">{partner.statistics.totalCalls.toLocaleString()}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
</TabsContent>
|
||
|
||
{/* API端点 */}
|
||
<TabsContent value="endpoints" className="mt-4">
|
||
{/* 分类筛选 */}
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<Button
|
||
variant={selectedCategory === 'all' ? 'default' : 'outline'}
|
||
size="sm"
|
||
onClick={() => setSelectedCategory('all')}
|
||
>
|
||
全部 ({OPEN_API_ENDPOINTS.length})
|
||
</Button>
|
||
{['ingest', 'query', 'tag', 'batch'].map(cat => {
|
||
const count = OPEN_API_ENDPOINTS.filter(e => e.category === cat).length
|
||
return (
|
||
<Button
|
||
key={cat}
|
||
variant={selectedCategory === cat ? 'default' : 'outline'}
|
||
size="sm"
|
||
onClick={() => setSelectedCategory(cat)}
|
||
>
|
||
{getCategoryIcon(cat)}
|
||
<span className="ml-1">{getCategoryName(cat)} ({count})</span>
|
||
</Button>
|
||
)
|
||
})}
|
||
</div>
|
||
|
||
{/* 端点列表 */}
|
||
<div className="space-y-3">
|
||
{filteredEndpoints.map(endpoint => (
|
||
<Card key={endpoint.id} className="border-0 shadow-sm bg-white/80">
|
||
<CardContent className="p-4">
|
||
<div className="flex items-start justify-between mb-3">
|
||
<div className="flex items-center gap-3">
|
||
<Badge className={`${getMethodColor(endpoint.method)} font-mono`}>
|
||
{endpoint.method}
|
||
</Badge>
|
||
<code className="text-sm font-mono text-gray-700">{endpoint.path}</code>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => copyToClipboard(`${apiBaseUrl}${endpoint.path}`, endpoint.id)}
|
||
>
|
||
{copiedId === endpoint.id ? (
|
||
<CheckCircle2 className="h-3 w-3 text-green-500" />
|
||
) : (
|
||
<Copy className="h-3 w-3" />
|
||
)}
|
||
</Button>
|
||
</div>
|
||
<Badge variant="outline" className="text-xs">
|
||
{getCategoryIcon(endpoint.category)}
|
||
<span className="ml-1">{getCategoryName(endpoint.category)}</span>
|
||
</Badge>
|
||
</div>
|
||
|
||
<h3 className="font-semibold text-gray-900 mb-1">{endpoint.name}</h3>
|
||
<p className="text-sm text-gray-500 mb-3">{endpoint.description}</p>
|
||
|
||
{/* 参数 */}
|
||
{endpoint.params && endpoint.params.length > 0 && (
|
||
<div className="mb-3">
|
||
<h4 className="text-xs font-medium text-gray-500 mb-2">参数</h4>
|
||
<div className="space-y-1">
|
||
{endpoint.params.map((param, i) => (
|
||
<div key={i} className="flex items-center gap-2 text-sm">
|
||
<code className="bg-gray-100 px-2 py-0.5 rounded text-purple-600">{param.name}</code>
|
||
<Badge variant="outline" className="text-xs">{param.type}</Badge>
|
||
{param.required && <Badge className="bg-red-100 text-red-700 text-xs">必填</Badge>}
|
||
<span className="text-gray-500">{param.desc}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 示例展开 */}
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="w-full justify-start text-gray-500"
|
||
onClick={() => setSelectedEndpoint(selectedEndpoint?.id === endpoint.id ? null : endpoint)}
|
||
>
|
||
<Code className="h-4 w-4 mr-2" />
|
||
{selectedEndpoint?.id === endpoint.id ? '收起示例' : '查看请求/响应示例'}
|
||
</Button>
|
||
|
||
{selectedEndpoint?.id === endpoint.id && (
|
||
<div className="mt-3 space-y-3">
|
||
<div>
|
||
<h4 className="text-xs font-medium text-gray-500 mb-2">请求示例</h4>
|
||
<pre className="bg-gray-900 text-green-400 p-3 rounded-lg text-xs overflow-x-auto">
|
||
{endpoint.example.request}
|
||
</pre>
|
||
</div>
|
||
<div>
|
||
<h4 className="text-xs font-medium text-gray-500 mb-2">响应示例</h4>
|
||
<pre className="bg-gray-900 text-blue-400 p-3 rounded-lg text-xs overflow-x-auto">
|
||
{endpoint.example.response}
|
||
</pre>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
</TabsContent>
|
||
|
||
{/* 调用日志 */}
|
||
<TabsContent value="logs" className="mt-4">
|
||
<Card className="border-0 shadow-sm bg-white/80">
|
||
<CardHeader className="pb-2">
|
||
<div className="flex items-center justify-between">
|
||
<CardTitle className="text-sm font-medium">最近调用记录</CardTitle>
|
||
<Button variant="outline" size="sm">
|
||
<RefreshCw className="h-4 w-4 mr-2" />
|
||
刷新
|
||
</Button>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead>时间</TableHead>
|
||
<TableHead>接入方</TableHead>
|
||
<TableHead>端点</TableHead>
|
||
<TableHead>方法</TableHead>
|
||
<TableHead>状态</TableHead>
|
||
<TableHead>耗时</TableHead>
|
||
<TableHead>大小</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{callLogs.map(log => (
|
||
<TableRow key={log.id}>
|
||
<TableCell className="text-sm text-gray-500">{log.timestamp}</TableCell>
|
||
<TableCell className="font-medium">{log.partnerName}</TableCell>
|
||
<TableCell className="font-mono text-xs">{log.endpoint}</TableCell>
|
||
<TableCell>
|
||
<Badge className={`${getMethodColor(log.method)} font-mono text-xs`}>
|
||
{log.method}
|
||
</Badge>
|
||
</TableCell>
|
||
<TableCell>{getStatusBadge(log.status)}</TableCell>
|
||
<TableCell className="text-sm">{log.responseTime}ms</TableCell>
|
||
<TableCell className="text-sm text-gray-500">
|
||
{(log.requestSize / 1024).toFixed(1)}KB / {(log.responseSize / 1024).toFixed(1)}KB
|
||
</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
</Tabs>
|
||
|
||
{/* 添加接入方弹窗 */}
|
||
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
|
||
<DialogContent className="max-w-lg">
|
||
<DialogHeader>
|
||
<DialogTitle>添加接入方</DialogTitle>
|
||
<DialogDescription>配置第三方系统接入神射手数据中台</DialogDescription>
|
||
</DialogHeader>
|
||
<div className="space-y-4 py-4">
|
||
<div className="space-y-2">
|
||
<Label>接入方名称 *</Label>
|
||
<Input
|
||
placeholder="例如:存客宝、点了码"
|
||
value={newPartner.name}
|
||
onChange={e => setNewPartner({ ...newPartner, name: e.target.value })}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>接入方类型</Label>
|
||
<Select
|
||
value={newPartner.type}
|
||
onValueChange={(v: 'external' | 'internal') => setNewPartner({ ...newPartner, type: v })}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="external">外部系统</SelectItem>
|
||
<SelectItem value="internal">内部系统</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>描述</Label>
|
||
<Textarea
|
||
placeholder="描述接入方用途和数据来源"
|
||
value={newPartner.description}
|
||
onChange={e => setNewPartner({ ...newPartner, description: e.target.value })}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>接口权限</Label>
|
||
<div className="flex flex-wrap gap-2">
|
||
{[
|
||
{ key: 'dataIngest', label: '数据流入', icon: ArrowDownToLine },
|
||
{ key: 'dataQuery', label: '数据查询', icon: Search },
|
||
{ key: 'tagEnrich', label: '标签完善', icon: Tags },
|
||
{ key: 'batchProcess', label: '批量处理', icon: Package },
|
||
].map(({ key, label, icon: Icon }) => (
|
||
<Badge
|
||
key={key}
|
||
className={`cursor-pointer ${
|
||
newPartner.permissions[key as keyof typeof newPartner.permissions]
|
||
? 'bg-purple-100 text-purple-700'
|
||
: 'bg-gray-100 text-gray-500'
|
||
}`}
|
||
onClick={() => setNewPartner({
|
||
...newPartner,
|
||
permissions: {
|
||
...newPartner.permissions,
|
||
[key]: !newPartner.permissions[key as keyof typeof newPartner.permissions],
|
||
},
|
||
})}
|
||
>
|
||
<Icon className="h-3 w-3 mr-1" />
|
||
{label}
|
||
</Badge>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div className="grid grid-cols-3 gap-4">
|
||
<div className="space-y-2">
|
||
<Label>每日限额</Label>
|
||
<Input
|
||
type="number"
|
||
value={newPartner.quotaConfig.dailyLimit}
|
||
onChange={e => setNewPartner({
|
||
...newPartner,
|
||
quotaConfig: { ...newPartner.quotaConfig, dailyLimit: parseInt(e.target.value) },
|
||
})}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>每月限额</Label>
|
||
<Input
|
||
type="number"
|
||
value={newPartner.quotaConfig.monthlyLimit}
|
||
onChange={e => setNewPartner({
|
||
...newPartner,
|
||
quotaConfig: { ...newPartner.quotaConfig, monthlyLimit: parseInt(e.target.value) },
|
||
})}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>速率限制(次/秒)</Label>
|
||
<Input
|
||
type="number"
|
||
value={newPartner.quotaConfig.rateLimit}
|
||
onChange={e => setNewPartner({
|
||
...newPartner,
|
||
quotaConfig: { ...newPartner.quotaConfig, rateLimit: parseInt(e.target.value) },
|
||
})}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<DialogFooter>
|
||
<Button variant="outline" onClick={() => setShowAddDialog(false)}>取消</Button>
|
||
<Button onClick={handleAddPartner} disabled={!newPartner.name}>创建</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* API密钥弹窗 */}
|
||
<Dialog open={showKeyDialog} onOpenChange={setShowKeyDialog}>
|
||
<DialogContent>
|
||
<DialogHeader>
|
||
<DialogTitle>API密钥 - {selectedPartner?.name}</DialogTitle>
|
||
<DialogDescription>用于接口认证,请妥善保管</DialogDescription>
|
||
</DialogHeader>
|
||
{selectedPartner && (
|
||
<div className="space-y-4 py-4">
|
||
<div className="space-y-2">
|
||
<Label>API Key</Label>
|
||
<div className="flex items-center gap-2">
|
||
<Input value={selectedPartner.apiKey} readOnly className="font-mono" />
|
||
<Button
|
||
variant="outline"
|
||
size="icon"
|
||
onClick={() => copyToClipboard(selectedPartner.apiKey, 'apiKey')}
|
||
>
|
||
{copiedId === 'apiKey' ? <CheckCircle2 className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>API Secret</Label>
|
||
<div className="flex items-center gap-2">
|
||
<Input value={selectedPartner.apiSecret} readOnly className="font-mono" type="password" />
|
||
<Button
|
||
variant="outline"
|
||
size="icon"
|
||
onClick={() => copyToClipboard(selectedPartner.apiSecret, 'apiSecret')}
|
||
>
|
||
{copiedId === 'apiSecret' ? <CheckCircle2 className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
<div className="p-3 rounded-lg bg-yellow-50 text-sm text-yellow-700">
|
||
<AlertCircle className="h-4 w-4 inline mr-2" />
|
||
请妥善保管密钥,不要在客户端代码中暴露
|
||
</div>
|
||
<div className="p-4 rounded-lg bg-gray-50">
|
||
<h4 className="text-sm font-medium mb-2">调用示例</h4>
|
||
<pre className="bg-gray-900 text-green-400 p-3 rounded-lg text-xs overflow-x-auto">
|
||
{`curl -X POST "${apiBaseUrl}/api/open-api/ingest/user" \\
|
||
-H "Authorization: Bearer ${selectedPartner.apiKey}" \\
|
||
-H "X-API-Secret: ${selectedPartner.apiSecret}" \\
|
||
-H "Content-Type: application/json" \\
|
||
-d '{"phone": "13800138000", "source": "${selectedPartner.name.toLowerCase()}"}'`}
|
||
</pre>
|
||
</div>
|
||
</div>
|
||
)}
|
||
<DialogFooter>
|
||
<Button variant="outline" onClick={() => setShowKeyDialog(false)}>关闭</Button>
|
||
<Button variant="destructive">
|
||
<RefreshCw className="h-4 w-4 mr-2" />
|
||
重新生成
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* 详细配置弹窗 */}
|
||
<Dialog open={showDetailDialog} onOpenChange={setShowDetailDialog}>
|
||
<DialogContent className="max-w-2xl">
|
||
<DialogHeader>
|
||
<DialogTitle>接入方配置 - {selectedPartner?.name}</DialogTitle>
|
||
<DialogDescription>管理接入方的权限和配额</DialogDescription>
|
||
</DialogHeader>
|
||
{selectedPartner && (
|
||
<div className="space-y-6 py-4">
|
||
{/* 基本信息 */}
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label>接入方名称</Label>
|
||
<Input value={selectedPartner.name} readOnly />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>类型</Label>
|
||
<Input value={selectedPartner.type === 'external' ? '外部系统' : '内部系统'} readOnly />
|
||
</div>
|
||
</div>
|
||
|
||
{/* 权限配置 */}
|
||
<div className="space-y-3">
|
||
<Label>接口权限</Label>
|
||
<div className="space-y-2">
|
||
{[
|
||
{ key: 'dataIngest', label: '数据流入', desc: '允许向神射手传入用户数据', icon: ArrowDownToLine },
|
||
{ key: 'dataQuery', label: '数据查询', desc: '允许查询用户画像和标签', icon: Search },
|
||
{ key: 'tagEnrich', label: '标签完善', desc: '允许触发AI标签完善', icon: Tags },
|
||
{ key: 'batchProcess', label: '批量处理', desc: '允许创建批量处理任务', icon: Package },
|
||
].map(({ key, label, desc, icon: Icon }) => (
|
||
<div key={key} className="flex items-center justify-between p-3 rounded-lg bg-gray-50">
|
||
<div className="flex items-center gap-3">
|
||
<div className="h-8 w-8 rounded-lg bg-white flex items-center justify-center">
|
||
<Icon className="h-4 w-4 text-gray-600" />
|
||
</div>
|
||
<div>
|
||
<p className="font-medium text-sm">{label}</p>
|
||
<p className="text-xs text-gray-500">{desc}</p>
|
||
</div>
|
||
</div>
|
||
<Switch
|
||
checked={selectedPartner.permissions[key as keyof Partner['permissions']]}
|
||
onCheckedChange={() => togglePermission(selectedPartner.id, key as keyof Partner['permissions'])}
|
||
/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 配额配置 */}
|
||
<div className="grid grid-cols-3 gap-4">
|
||
<div className="space-y-2">
|
||
<Label>每日限额</Label>
|
||
<Input type="number" defaultValue={selectedPartner.quotaConfig.dailyLimit} />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>每月限额</Label>
|
||
<Input type="number" defaultValue={selectedPartner.quotaConfig.monthlyLimit} />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label>速率限制(次/秒)</Label>
|
||
<Input type="number" defaultValue={selectedPartner.quotaConfig.rateLimit} />
|
||
</div>
|
||
</div>
|
||
|
||
{/* 调用统计 */}
|
||
<div className="p-4 rounded-lg bg-gray-50">
|
||
<h4 className="text-sm font-medium mb-3">调用统计</h4>
|
||
<div className="grid grid-cols-4 gap-4 text-center">
|
||
<div>
|
||
<p className="text-2xl font-bold text-gray-900">{selectedPartner.statistics.todayCalls.toLocaleString()}</p>
|
||
<p className="text-xs text-gray-500">今日调用</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-2xl font-bold text-gray-900">{selectedPartner.statistics.monthCalls.toLocaleString()}</p>
|
||
<p className="text-xs text-gray-500">本月调用</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-2xl font-bold text-gray-900">{selectedPartner.statistics.totalCalls.toLocaleString()}</p>
|
||
<p className="text-xs text-gray-500">累计调用</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm font-medium text-gray-900">{selectedPartner.statistics.lastCallTime}</p>
|
||
<p className="text-xs text-gray-500">最后调用</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
<DialogFooter>
|
||
<Button variant="outline" onClick={() => setShowDetailDialog(false)}>关闭</Button>
|
||
<Button>保存配置</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|