api接口提交
This commit is contained in:
@@ -1,231 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useState } from "react"
|
|
||||||
import { ChevronLeft, Copy, Check, Info } from "lucide-react"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { useRouter } from "next/navigation"
|
|
||||||
import { useToast } from "@/components/ui/use-toast"
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
||||||
import { getApiGuideForScenario } from "@/docs/api-guide"
|
|
||||||
import { Badge } from "@/components/ui/badge"
|
|
||||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
|
|
||||||
|
|
||||||
export default function ApiDocPage({ params }: { params: { channel: string; id: string } }) {
|
|
||||||
const router = useRouter()
|
|
||||||
const { toast } = useToast()
|
|
||||||
const [copiedExample, setCopiedExample] = useState<string | null>(null)
|
|
||||||
|
|
||||||
const apiGuide = getApiGuideForScenario(params.id, params.channel)
|
|
||||||
|
|
||||||
const copyToClipboard = (text: string, exampleId: string) => {
|
|
||||||
navigator.clipboard.writeText(text)
|
|
||||||
setCopiedExample(exampleId)
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "已复制代码",
|
|
||||||
description: "代码示例已复制到剪贴板",
|
|
||||||
})
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setCopiedExample(null)
|
|
||||||
}, 2000)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50">
|
|
||||||
<header className="sticky top-0 z-10 bg-white border-b">
|
|
||||||
<div className="flex items-center justify-between h-14 px-4">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Button variant="ghost" size="icon" onClick={() => router.back()}>
|
|
||||||
<ChevronLeft className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
<h1 className="ml-2 text-lg font-medium">{apiGuide.title}</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="container mx-auto py-6 px-4 max-w-4xl">
|
|
||||||
<Card className="mb-6">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>接口说明</CardTitle>
|
|
||||||
<CardDescription>{apiGuide.description}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Info className="h-4 w-4 text-blue-500" />
|
|
||||||
<p className="text-sm text-gray-700">
|
|
||||||
此接口用于将外部系统收集的客户信息直接导入到存客宝的获客计划中。您需要使用API密钥进行身份验证。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-md bg-amber-50 p-4 border border-amber-200">
|
|
||||||
<p className="text-sm text-amber-800">
|
|
||||||
<strong>安全提示:</strong>{" "}
|
|
||||||
请妥善保管您的API密钥,不要在客户端代码中暴露它。建议在服务器端使用该接口。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Accordion type="single" collapsible className="mb-6">
|
|
||||||
{apiGuide.endpoints.map((endpoint, index) => (
|
|
||||||
<AccordionItem key={index} value={`endpoint-${index}`}>
|
|
||||||
<AccordionTrigger className="px-4 hover:bg-gray-50">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Badge className="mr-2">{endpoint.method}</Badge>
|
|
||||||
<span className="font-mono text-sm">{endpoint.url}</span>
|
|
||||||
</div>
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="px-4 pt-2">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<p className="text-sm text-gray-700">{endpoint.description}</p>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium mb-2">请求头</h4>
|
|
||||||
<div className="bg-gray-50 rounded-md p-3">
|
|
||||||
{endpoint.headers.map((header, i) => (
|
|
||||||
<div key={i} className="flex items-start mb-2 last:mb-0">
|
|
||||||
<Badge variant="outline" className="mr-2 mt-0.5 font-mono">
|
|
||||||
{header.required ? "*" : ""}
|
|
||||||
{header.name}
|
|
||||||
</Badge>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm">{header.value}</p>
|
|
||||||
<p className="text-xs text-gray-500">{header.description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium mb-2">请求参数</h4>
|
|
||||||
<div className="bg-gray-50 rounded-md p-3">
|
|
||||||
{endpoint.parameters.map((param, i) => (
|
|
||||||
<div key={i} className="flex items-start mb-3 last:mb-0">
|
|
||||||
<Badge variant="outline" className="mr-2 mt-0.5 font-mono">
|
|
||||||
{param.required ? "*" : ""}
|
|
||||||
{param.name}
|
|
||||||
</Badge>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm">
|
|
||||||
<span className="text-gray-500 font-mono">{param.type}</span>
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500">{param.description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium mb-2">响应示例</h4>
|
|
||||||
<pre className="bg-gray-50 rounded-md p-3 text-xs overflow-auto">
|
|
||||||
{JSON.stringify(endpoint.response, null, 2)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
))}
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>代码示例</CardTitle>
|
|
||||||
<CardDescription>以下是不同编程语言的接口调用示例</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Tabs defaultValue={apiGuide.examples[0].language}>
|
|
||||||
<TabsList className="mb-4">
|
|
||||||
{apiGuide.examples.map((example) => (
|
|
||||||
<TabsTrigger key={example.language} value={example.language}>
|
|
||||||
{example.title}
|
|
||||||
</TabsTrigger>
|
|
||||||
))}
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
{apiGuide.examples.map((example) => (
|
|
||||||
<TabsContent key={example.language} value={example.language}>
|
|
||||||
<div className="relative">
|
|
||||||
<pre className="bg-gray-50 p-4 rounded-md overflow-auto text-xs">{example.code}</pre>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="absolute top-2 right-2"
|
|
||||||
onClick={() => copyToClipboard(example.code, example.language)}
|
|
||||||
>
|
|
||||||
{copiedExample === example.language ? (
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<Copy className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
))}
|
|
||||||
</Tabs>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<div className="mt-8">
|
|
||||||
<h3 className="text-lg font-medium mb-4">集成指南</h3>
|
|
||||||
|
|
||||||
<Card className="mb-4">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-base">集简云平台集成</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ol className="list-decimal list-inside space-y-2 text-sm">
|
|
||||||
<li>登录集简云平台</li>
|
|
||||||
<li>导航至"应用集成" > "外部接口"</li>
|
|
||||||
<li>选择"添加新接口",输入存客宝接口信息</li>
|
|
||||||
<li>配置回调参数,将"X-API-KEY"设置为您的API密钥</li>
|
|
||||||
<li>
|
|
||||||
设置接口URL为:
|
|
||||||
<code className="bg-gray-100 px-1 py-0.5 rounded">
|
|
||||||
<code>{apiGuide.endpoints[0].url}</code>
|
|
||||||
</code>
|
|
||||||
</li>
|
|
||||||
<li>映射必要字段(name, phone等)</li>
|
|
||||||
<li>保存并启用集成</li>
|
|
||||||
</ol>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-base">问题排查</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium mb-1">接口认证失败</h4>
|
|
||||||
<p className="text-sm text-gray-700">
|
|
||||||
请确保X-API-KEY正确无误,此密钥区分大小写。如需重置密钥,请联系管理员。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium mb-1">数据格式错误</h4>
|
|
||||||
<p className="text-sm text-gray-700">
|
|
||||||
确保所有必填字段已提供,并且字段类型正确。特别是电话号码格式需符合标准。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium mb-1">请求频率限制</h4>
|
|
||||||
<p className="text-sm text-gray-700">
|
|
||||||
单个API密钥每分钟最多可发送30个请求,超过限制将被暂时限制。对于大批量数据,请使用批量接口。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
405
Cunkebao/app/api/docs/scenarios/page.tsx
Normal file
405
Cunkebao/app/api/docs/scenarios/page.tsx
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState } from "react"
|
||||||
|
import { ChevronLeft, Copy, Check, Info } from "lucide-react"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
|
import { useToast } from "@/components/ui/use-toast"
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
import { getApiGuideForScenario } from "@/docs/api-guide"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
|
||||||
|
|
||||||
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'https://yishi.com'
|
||||||
|
|
||||||
|
export default function ApiDocPage({ params }: { params: { channel: string; id: string } }) {
|
||||||
|
const router = useRouter()
|
||||||
|
const { toast } = useToast()
|
||||||
|
const [copiedExample, setCopiedExample] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const apiGuide = getApiGuideForScenario(params.id, params.channel)
|
||||||
|
|
||||||
|
// 假设 fullUrl 和 apiKey 可通过 props 或接口获取,这里用演示值
|
||||||
|
const [apiKey] = useState("naxf1-82h2f-vdwcm-rrhpm-q9hd1")
|
||||||
|
const [fullUrl] = useState("/v1/api/scenarios")
|
||||||
|
const testUrl = fullUrl.startsWith("http") ? fullUrl : `${API_BASE_URL}${fullUrl}`
|
||||||
|
|
||||||
|
const copyToClipboard = (text: string, exampleId: string) => {
|
||||||
|
navigator.clipboard.writeText(text)
|
||||||
|
setCopiedExample(exampleId)
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "已复制代码",
|
||||||
|
description: "代码示例已复制到剪贴板",
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopiedExample(null)
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<header className="sticky top-0 z-10 bg-white border-b">
|
||||||
|
<div className="flex items-center justify-between h-14 px-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Button variant="ghost" size="icon" onClick={() => router.back()}>
|
||||||
|
<ChevronLeft className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
<h1 className="ml-2 text-lg font-medium">计划接口文档</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="container mx-auto py-6 px-4 max-w-4xl">
|
||||||
|
<Card className="mb-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>接口说明</CardTitle>
|
||||||
|
<CardDescription>本接口用于将外部客户数据导入到存客宝计划。请使用 <b>apiKey</b> 进行身份认证,建议仅在服务端调用。</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Info className="h-4 w-4 text-blue-500" />
|
||||||
|
<p className="text-sm text-gray-700">
|
||||||
|
支持多种编程语言和平台集成。接口地址和参数请参考下方说明。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md bg-amber-50 p-4 border border-amber-200">
|
||||||
|
<p className="text-sm text-amber-800">
|
||||||
|
<strong>安全提示:</strong> 请妥善保管您的API密钥,不要在客户端代码中暴露它。建议在服务器端使用该接口。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="mb-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>签名规则</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2 text-sm text-gray-700">
|
||||||
|
<div>
|
||||||
|
<b>签名参数:</b> <span className="font-mono">sign</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b>签名算法:</b> 将所有请求参数(排除 sign 和 apiKey)按参数名升序排序,<b>直接拼接参数值</b>(不含等号和&),对该字符串进行 <span className="font-mono">MD5</span> 加密,得到中间串,再拼接 apiKey,最后再进行一次 <span className="font-mono">MD5</span> 加密,结果作为 sign 参数传递。
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b>签名步骤:</b>
|
||||||
|
<ol className="list-decimal list-inside ml-4">
|
||||||
|
<li>去除 sign 和 apiKey 参数,将其余所有参数(如 name、phone、timestamp、source、remark、tags)按参数名升序排序</li>
|
||||||
|
<li><b>直接拼接参数值</b>,如 value1value2value3...</li>
|
||||||
|
<li>对拼接后的字符串进行 MD5 加密,得到中间串</li>
|
||||||
|
<li>将中间串与 apiKey 直接拼接</li>
|
||||||
|
<li>对拼接后的字符串再进行一次 MD5 加密,结果即为 sign</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b>示例:</b>
|
||||||
|
<pre className="bg-gray-50 rounded p-2 text-xs overflow-auto">
|
||||||
|
{`参数:
|
||||||
|
name=张三
|
||||||
|
phone=18888888888
|
||||||
|
timestamp=1700000000
|
||||||
|
apiKey=naxf1-82h2f-vdwcm-rrhpm-q9hd1
|
||||||
|
|
||||||
|
排序后拼接(排除apiKey,直接拼接值):
|
||||||
|
张三188888888881700000000
|
||||||
|
|
||||||
|
第一步MD5:
|
||||||
|
md5(张三188888888881700000000) = 123456abcdef...
|
||||||
|
|
||||||
|
拼接apiKey:
|
||||||
|
123456abcdef...naxf1-82h2f-vdwcm-rrhpm-q9hd1
|
||||||
|
|
||||||
|
第二步MD5:
|
||||||
|
sign=md5(123456abcdef...naxf1-82h2f-vdwcm-rrhpm-q9hd1)`}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-amber-700 mt-2">注意:所有参数均需参与签名(除 sign 和 apiKey),且参数值需为原始值(不可 URL 编码)。</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="mb-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>接口地址</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="mb-2">
|
||||||
|
<span className="font-mono text-sm">{testUrl}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
<div>必要参数: <b>phone</b> (电话), <b>timestamp</b> (时间戳), <b>apiKey</b> (接口密钥)</div>
|
||||||
|
<div>可选参数: <b>name</b> (姓名), <b>source</b> (来源), <b>remark</b> (备注), <b>tags</b> (标签)</div>
|
||||||
|
<div>请求方式: <b>POST</b> 或 <b>GET</b></div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="mb-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>请求参数</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="bg-gray-50 rounded-md p-3 text-xs">
|
||||||
|
<div><b>phone</b> (string, 必填): 客户电话</div>
|
||||||
|
<div><b>timestamp</b> (int, 必填): 当前时间戳,精确到秒(如 1700000000,建议用 Math.floor(Date.now() / 1000) 获取)</div>
|
||||||
|
<div><b>apiKey</b> (string, 必填): 接口密钥</div>
|
||||||
|
<div><b>name</b> (string, 可选): 客户姓名</div>
|
||||||
|
<div><b>source</b> (string, 可选): 来源</div>
|
||||||
|
<div><b>remark</b> (string, 可选): 备注</div>
|
||||||
|
<div><b>tags</b> (string, 可选): 标签</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="mb-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>响应示例</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<pre className="bg-gray-50 rounded-md p-3 text-xs overflow-auto">
|
||||||
|
{`{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "导入成功",
|
||||||
|
"data": {
|
||||||
|
"customerId": "123456"
|
||||||
|
}
|
||||||
|
}`}
|
||||||
|
</pre>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>代码示例</CardTitle>
|
||||||
|
<CardDescription>以下是不同编程语言的接口调用示例</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Tabs defaultValue="curl">
|
||||||
|
<TabsList className="mb-4">
|
||||||
|
<TabsTrigger value="curl">cURL</TabsTrigger>
|
||||||
|
<TabsTrigger value="python">Python</TabsTrigger>
|
||||||
|
<TabsTrigger value="node">Node.js</TabsTrigger>
|
||||||
|
<TabsTrigger value="php">PHP</TabsTrigger>
|
||||||
|
<TabsTrigger value="java">Java</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="curl">
|
||||||
|
<pre className="bg-gray-50 p-4 rounded-md overflow-auto text-xs">{`curl -X POST 'http://yishi.com/v1/plan/api/scenariosz' \
|
||||||
|
-d "phone=18888888888" \
|
||||||
|
-d "timestamp=1700000000" \
|
||||||
|
-d "name=张三" \
|
||||||
|
-d "apiKey=naxf1-82h2f-vdwcm-rrhpm-q9hd1" \
|
||||||
|
-d "sign=请用签名算法生成"`}</pre>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="python">
|
||||||
|
<pre className="bg-gray-50 p-4 rounded-md overflow-auto text-xs">{`import hashlib
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
|
||||||
|
def gen_sign(params, api_key):
|
||||||
|
data = {k: v for k, v in params.items() if k not in ('sign', 'apiKey')}
|
||||||
|
s = ''.join([str(data[k]) for k in sorted(data)])
|
||||||
|
first = hashlib.md5(s.encode('utf-8')).hexdigest()
|
||||||
|
return hashlib.md5((first + api_key).encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
api_key = 'naxf1-82h2f-vdwcm-rrhpm-q9hd1'
|
||||||
|
params = {
|
||||||
|
'phone': '18888888888',
|
||||||
|
'timestamp': int(time.time()),
|
||||||
|
'name': '张三',
|
||||||
|
}
|
||||||
|
params['apiKey'] = api_key
|
||||||
|
params['sign'] = gen_sign(params, api_key)
|
||||||
|
resp = requests.post('http://yishi.com/v1/plan/api/scenariosz', data=params)
|
||||||
|
print(resp.json())`}</pre>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="node">
|
||||||
|
<pre className="bg-gray-50 p-4 rounded-md overflow-auto text-xs">{`const axios = require('axios');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
function genSign(params, apiKey) {
|
||||||
|
const data = {...params};
|
||||||
|
delete data.sign;
|
||||||
|
delete data.apiKey;
|
||||||
|
const keys = Object.keys(data).sort();
|
||||||
|
let str = '';
|
||||||
|
keys.forEach(k => { str += data[k]; });
|
||||||
|
const first = crypto.createHash('md5').update(str).digest('hex');
|
||||||
|
return crypto.createHash('md5').update(first + apiKey).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKey = 'naxf1-82h2f-vdwcm-rrhpm-q9hd1';
|
||||||
|
const params = {
|
||||||
|
phone: '18888888888',
|
||||||
|
timestamp: Math.floor(Date.now() / 1000),
|
||||||
|
name: '张三',
|
||||||
|
};
|
||||||
|
params.apiKey = apiKey;
|
||||||
|
params.sign = genSign(params, apiKey);
|
||||||
|
|
||||||
|
axios.post('http://yishi.com/v1/plan/api/scenariosz', params)
|
||||||
|
.then(res => console.log(res.data));`}</pre>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="php">
|
||||||
|
<pre className="bg-gray-50 p-4 rounded-md overflow-auto text-xs">{`<?php
|
||||||
|
function md5_sign($params, $apiKey) {
|
||||||
|
unset($params['sign']);
|
||||||
|
unset($params['apiKey']);
|
||||||
|
ksort($params);
|
||||||
|
$str = '';
|
||||||
|
foreach ($params as $v) {
|
||||||
|
$str .= $v;
|
||||||
|
}
|
||||||
|
$first = md5($str);
|
||||||
|
$final = md5($first . $apiKey);
|
||||||
|
return $final;
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
'phone' => '18888888888',
|
||||||
|
'timestamp' => time(),
|
||||||
|
'name' => '张三',
|
||||||
|
// 'source' => '', 'remark' => '', 'tags' => ''
|
||||||
|
];
|
||||||
|
$apiKey = 'naxf1-82h2f-vdwcm-rrhpm-q9hd1';
|
||||||
|
$params['apiKey'] = $apiKey;
|
||||||
|
$params['sign'] = md5_sign($params, $apiKey);
|
||||||
|
|
||||||
|
$url = '${API_BASE_URL}/v1/api/scenarios';
|
||||||
|
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $url);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, 1);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
echo $response;
|
||||||
|
?>`}</pre>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="java">
|
||||||
|
<pre className="bg-gray-50 p-4 rounded-md overflow-auto text-xs">{`import java.security.MessageDigest;
|
||||||
|
import java.util.*;
|
||||||
|
import java.net.*;
|
||||||
|
import java.io.*;
|
||||||
|
|
||||||
|
public class ApiSignDemo {
|
||||||
|
public static String md5(String s) throws Exception {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||||
|
byte[] array = md.digest(s.getBytes("UTF-8"));
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (byte b : array) sb.append(String.format("%02x", b));
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
Map<String, String> params = new HashMap<>();
|
||||||
|
params.put("phone", "18888888888");
|
||||||
|
params.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
|
||||||
|
params.put("name", "张三");
|
||||||
|
// params.put("source", ""); params.put("remark", ""); params.put("tags", "");
|
||||||
|
String apiKey = "naxf1-82h2f-vdwcm-rrhpm-q9hd1";
|
||||||
|
// 排序并拼接
|
||||||
|
List<String> keys = new ArrayList<>(params.keySet());
|
||||||
|
keys.remove("sign");
|
||||||
|
keys.remove("apiKey");
|
||||||
|
Collections.sort(keys);
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (String k : keys) {
|
||||||
|
sb.append(params.get(k));
|
||||||
|
}
|
||||||
|
String first = md5(sb.toString());
|
||||||
|
String sign = md5(first + apiKey);
|
||||||
|
params.put("apiKey", apiKey);
|
||||||
|
params.put("sign", sign);
|
||||||
|
// 发送POST请求
|
||||||
|
StringBuilder postData = new StringBuilder();
|
||||||
|
for (Map.Entry<String, String> entry : params.entrySet()) {
|
||||||
|
if (postData.length() > 0) postData.append("&");
|
||||||
|
postData.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
|
||||||
|
postData.append("=");
|
||||||
|
postData.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
|
||||||
|
}
|
||||||
|
URL url = new URL("${API_BASE_URL}/v1/api/scenarios");
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
OutputStream os = conn.getOutputStream();
|
||||||
|
os.write(postData.toString().getBytes("UTF-8"));
|
||||||
|
os.flush(); os.close();
|
||||||
|
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||||
|
String line; StringBuilder resp = new StringBuilder();
|
||||||
|
while ((line = in.readLine()) != null) resp.append(line);
|
||||||
|
in.close();
|
||||||
|
System.out.println(resp.toString());
|
||||||
|
}
|
||||||
|
}`}</pre>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<h3 className="text-lg font-medium mb-4">集成指南</h3>
|
||||||
|
|
||||||
|
<Card className="mb-4">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">集简云平台集成</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ol className="list-decimal list-inside space-y-2 text-sm">
|
||||||
|
<li>登录集简云平台</li>
|
||||||
|
<li>导航至"应用集成" > "外部接口"</li>
|
||||||
|
<li>选择"添加新接口",输入存客宝接口信息</li>
|
||||||
|
<li>配置回调参数,将"X-API-KEY"设置为您的API密钥</li>
|
||||||
|
<li>
|
||||||
|
设置接口URL为:
|
||||||
|
<code className="bg-gray-100 px-1 py-0.5 rounded">
|
||||||
|
{testUrl}
|
||||||
|
</code>
|
||||||
|
</li>
|
||||||
|
<li>映射必要字段(name, phone等)</li>
|
||||||
|
<li>保存并启用集成</li>
|
||||||
|
</ol>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">问题排查</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium mb-1">接口认证失败</h4>
|
||||||
|
<p className="text-sm text-gray-700">
|
||||||
|
请确保X-API-KEY正确无误,此密钥区分大小写。如需重置密钥,请联系管理员。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium mb-1">数据格式错误</h4>
|
||||||
|
<p className="text-sm text-gray-700">
|
||||||
|
确保所有必填字段已提供,并且字段类型正确。特别是电话号码格式需符合标准。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium mb-1">请求频率限制</h4>
|
||||||
|
<p className="text-sm text-gray-700">
|
||||||
|
单个API密钥每分钟最多可发送30个请求,超过限制将被暂时限制。对于大批量数据,请使用批量接口。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -38,6 +38,8 @@ interface DeviceStats {
|
|||||||
active: number
|
active: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://yishi.com'
|
||||||
|
|
||||||
// API文档提示组件
|
// API文档提示组件
|
||||||
function ApiDocumentationTooltip() {
|
function ApiDocumentationTooltip() {
|
||||||
return (
|
return (
|
||||||
@@ -118,6 +120,7 @@ function ApiDocumentationTooltip() {
|
|||||||
apiKey: "",
|
apiKey: "",
|
||||||
webhookUrl: "",
|
webhookUrl: "",
|
||||||
taskId: "",
|
taskId: "",
|
||||||
|
fullUrl: ""
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleEditPlan = (taskId: string) => {
|
const handleEditPlan = (taskId: string) => {
|
||||||
@@ -201,7 +204,8 @@ function ApiDocumentationTooltip() {
|
|||||||
if (res.code === 200 && res.data) {
|
if (res.code === 200 && res.data) {
|
||||||
setCurrentApiSettings({
|
setCurrentApiSettings({
|
||||||
apiKey: res.data.apiKey || '', // 使用接口返回的 API 密钥
|
apiKey: res.data.apiKey || '', // 使用接口返回的 API 密钥
|
||||||
webhookUrl: `${window.location.origin}/api/scenarios/${channel}/${taskId}/webhook`,
|
webhookUrl: `${API_BASE_URL}/v1/api/scenarios`,
|
||||||
|
fullUrl: res.data.textUrl.fullUrl || '',
|
||||||
taskId,
|
taskId,
|
||||||
})
|
})
|
||||||
setShowApiDialog(true)
|
setShowApiDialog(true)
|
||||||
@@ -404,7 +408,7 @@ function ApiDocumentationTooltip() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full flex items-center justify-center gap-2 bg-white"
|
className="w-full flex items-center justify-center gap-2 bg-white"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.open(`/api/docs/scenarios/${channel}/${currentApiSettings.taskId}`, "_blank")
|
window.open(`/api/docs/scenarios?planId=${currentApiSettings.taskId}`, "_blank")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Link className="h-4 w-4" />
|
<Link className="h-4 w-4" />
|
||||||
@@ -417,7 +421,7 @@ function ApiDocumentationTooltip() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.open(`/api/docs/scenarios/${channel}/${currentApiSettings.taskId}#examples`, "_blank")
|
window.open(`/api/docs/scenarios?planId=${currentApiSettings.taskId}#examples`, "_blank")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="text-blue-600">查看代码示例</span>
|
<span className="text-blue-600">查看代码示例</span>
|
||||||
@@ -428,7 +432,7 @@ function ApiDocumentationTooltip() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.open(`/api/docs/scenarios/${channel}/${currentApiSettings.taskId}#integration`, "_blank")
|
window.open(`/api/docs/scenarios?planId=${currentApiSettings.taskId}#integration`, "_blank")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="text-blue-600">查看集成指南</span>
|
<span className="text-blue-600">查看集成指南</span>
|
||||||
@@ -446,7 +450,7 @@ function ApiDocumentationTooltip() {
|
|||||||
<div className="bg-gray-50 p-3 rounded-md border border-gray-100">
|
<div className="bg-gray-50 p-3 rounded-md border border-gray-100">
|
||||||
<p className="text-xs text-gray-600 mb-2">使用以下URL可以快速测试接口是否正常工作:</p>
|
<p className="text-xs text-gray-600 mb-2">使用以下URL可以快速测试接口是否正常工作:</p>
|
||||||
<div className="text-xs font-mono bg-white p-2 rounded border border-gray-200 overflow-x-auto">
|
<div className="text-xs font-mono bg-white p-2 rounded border border-gray-200 overflow-x-auto">
|
||||||
{`${currentApiSettings.webhookUrl}?name=测试客户&phone=13800138000`}
|
{`${currentApiSettings.webhookUrl}?${currentApiSettings.fullUrl}`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import { Search, Users } from "lucide-react"
|
|||||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"
|
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"
|
||||||
import { api } from "@/lib/api"
|
import { api } from "@/lib/api"
|
||||||
|
|
||||||
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://yishi.com'
|
||||||
|
|
||||||
interface BasicInfoStepProps {
|
interface BasicInfoStepProps {
|
||||||
onNext: (data: any) => void
|
onNext: (data: any) => void
|
||||||
initialData?: {
|
initialData?: {
|
||||||
@@ -45,6 +47,11 @@ export default function BasicInfoStep({ onNext, initialData = {} }: BasicInfoSte
|
|||||||
const [accountTotal, setAccountTotal] = useState(0)
|
const [accountTotal, setAccountTotal] = useState(0)
|
||||||
const [accountLoading, setAccountLoading] = useState(false)
|
const [accountLoading, setAccountLoading] = useState(false)
|
||||||
|
|
||||||
|
// API配置弹窗状态
|
||||||
|
const [apiDialogOpen, setApiDialogOpen] = useState(false)
|
||||||
|
const [apiKey] = useState("naxf1-82h2f-vdwcm-rrhpm-q9hd1") // 这里可以从后端获取或生成
|
||||||
|
const [apiUrl] = useState(`${API_BASE_URL}/v1/plan/api/scenariosz`)
|
||||||
|
|
||||||
// 拉取账号列表
|
// 拉取账号列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAccountLoading(true)
|
setAccountLoading(true)
|
||||||
|
|||||||
14
Cunkebao/package-lock.json
generated
14
Cunkebao/package-lock.json
generated
@@ -39,11 +39,13 @@
|
|||||||
"@radix-ui/react-toggle-group": "^1.1.1",
|
"@radix-ui/react-toggle-group": "^1.1.1",
|
||||||
"@radix-ui/react-tooltip": "latest",
|
"@radix-ui/react-tooltip": "latest",
|
||||||
"@tanstack/react-table": "latest",
|
"@tanstack/react-table": "latest",
|
||||||
|
"@types/crypto-js": "^4.2.2",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"chart.js": "latest",
|
"chart.js": "latest",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "1.0.4",
|
"cmdk": "1.0.4",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
"date-fns": "latest",
|
"date-fns": "latest",
|
||||||
"embla-carousel-react": "8.5.1",
|
"embla-carousel-react": "8.5.1",
|
||||||
"input-otp": "1.4.1",
|
"input-otp": "1.4.1",
|
||||||
@@ -2463,6 +2465,12 @@
|
|||||||
"url": "https://github.com/sponsors/tannerlinsley"
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/crypto-js": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/crypto-js/-/crypto-js-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/d3-array": {
|
"node_modules/@types/d3-array": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
|
||||||
@@ -3125,6 +3133,12 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crypto-js": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cssesc": {
|
"node_modules/cssesc": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
|
|||||||
@@ -40,11 +40,13 @@
|
|||||||
"@radix-ui/react-toggle-group": "^1.1.1",
|
"@radix-ui/react-toggle-group": "^1.1.1",
|
||||||
"@radix-ui/react-tooltip": "latest",
|
"@radix-ui/react-tooltip": "latest",
|
||||||
"@tanstack/react-table": "latest",
|
"@tanstack/react-table": "latest",
|
||||||
|
"@types/crypto-js": "^4.2.2",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"chart.js": "latest",
|
"chart.js": "latest",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "1.0.4",
|
"cmdk": "1.0.4",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
"date-fns": "latest",
|
"date-fns": "latest",
|
||||||
"embla-carousel-react": "8.5.1",
|
"embla-carousel-react": "8.5.1",
|
||||||
"input-otp": "1.4.1",
|
"input-otp": "1.4.1",
|
||||||
|
|||||||
@@ -8,93 +8,98 @@ Route::group('v1', function () {
|
|||||||
Route::group('api', function () {
|
Route::group('api', function () {
|
||||||
// Account控制器路由
|
// Account控制器路由
|
||||||
Route::group('account', function () {
|
Route::group('account', function () {
|
||||||
Route::get('list', 'app\\api\\controller\\AccountController@getList'); // 获取账号列表 √
|
Route::get('list', 'app\api\controller\AccountController@getList'); // 获取账号列表 √
|
||||||
Route::post('create', 'app\\api\\controller\\AccountController@createAccount'); // 创建账号 √
|
Route::post('create', 'app\api\controller\AccountController@createAccount'); // 创建账号 √
|
||||||
Route::post('createNewAccount', 'app\\api\\controller\\AccountController@createNewAccount'); // 创建新账号(包含创建部门) √
|
Route::post('createNewAccount', 'app\api\controller\AccountController@createNewAccount'); // 创建新账号(包含创建部门) √
|
||||||
Route::post('department/create', 'app\\api\\controller\\AccountController@createDepartment'); // 创建部门 √
|
Route::post('department/create', 'app\api\controller\AccountController@createDepartment'); // 创建部门 √
|
||||||
Route::get('department/list', 'app\\api\\controller\\AccountController@getDepartmentList'); // 获取部门列表 √
|
Route::get('department/list', 'app\api\controller\AccountController@getDepartmentList'); // 获取部门列表 √
|
||||||
Route::post('department/update', 'app\\api\\controller\\AccountController@updateDepartment'); // 更新部门 √
|
Route::post('department/update', 'app\api\controller\AccountController@updateDepartment'); // 更新部门 √
|
||||||
Route::post('department/delete', 'app\\api\\controller\\AccountController@deleteDepartment'); // 删除部门 √
|
Route::post('department/delete', 'app\api\controller\AccountController@deleteDepartment'); // 删除部门 √
|
||||||
Route::post('department/setPrivileges', 'app\\api\\controller\\AccountController@setPrivileges'); // 设置部门权限 √
|
Route::post('department/setPrivileges', 'app\api\controller\AccountController@setPrivileges'); // 设置部门权限 √
|
||||||
});
|
});
|
||||||
|
|
||||||
// Device控制器路由
|
// Device控制器路由
|
||||||
Route::group('device', function () {
|
Route::group('device', function () {
|
||||||
Route::get('list', 'app\\api\\controller\\DeviceController@getList'); // 获取设备列表 √
|
Route::get('list', 'app\api\controller\DeviceController@getList'); // 获取设备列表 √
|
||||||
Route::post('add', 'app\\api\\controller\\DeviceController@addDevice'); // 生成设备二维码(POST方式) √
|
Route::post('add', 'app\api\controller\DeviceController@addDevice'); // 生成设备二维码(POST方式) √
|
||||||
Route::post('updateDeviceGroup', 'app\\api\\controller\\DeviceController@updateDeviceGroup'); // 更新设备分组 √
|
Route::post('updateDeviceGroup', 'app\api\controller\DeviceController@updateDeviceGroup'); // 更新设备分组 √
|
||||||
Route::post('updateaccount', 'app\\api\\controller\\DeviceController@updateaccount'); // 更新设备账号 √
|
Route::post('updateaccount', 'app\api\controller\DeviceController@updateaccount'); // 更新设备账号 √
|
||||||
Route::post('createGroup', 'app\\api\\controller\\DeviceController@createGroup'); // 创建设备分组 √
|
Route::post('createGroup', 'app\api\controller\DeviceController@createGroup'); // 创建设备分组 √
|
||||||
Route::get('groupList', 'app\\api\\controller\\DeviceController@getGroupList'); // 获取设备分组列表 √
|
Route::get('groupList', 'app\api\controller\DeviceController@getGroupList'); // 获取设备分组列表 √
|
||||||
Route::post('updateDeviceToGroup', 'app\\api\\controller\\DeviceController@updateDeviceToGroup'); // 更新设备的分组 √
|
Route::post('updateDeviceToGroup', 'app\api\controller\DeviceController@updateDeviceToGroup'); // 更新设备的分组 √
|
||||||
});
|
});
|
||||||
|
|
||||||
// FriendTask控制器路由
|
// FriendTask控制器路由
|
||||||
Route::group('friend-task', function () {
|
Route::group('friend-task', function () {
|
||||||
Route::get('list', 'app\\api\\controller\\FriendTaskController@getList'); // 获取添加好友记录列表 √
|
Route::get('list', 'app\api\controller\FriendTaskController@getList'); // 获取添加好友记录列表 √
|
||||||
Route::post('add', 'app\\api\\controller\\FriendTaskController@addFriendTask'); // 添加好友任务 √
|
Route::post('add', 'app\api\controller\FriendTaskController@addFriendTask'); // 添加好友任务 √
|
||||||
});
|
});
|
||||||
|
|
||||||
// Moments控制器路由
|
// Moments控制器路由
|
||||||
Route::group('moments', function () {
|
Route::group('moments', function () {
|
||||||
Route::post('add-job', 'app\\api\\controller\\MomentsController@addJob'); // 发布朋友圈
|
Route::post('add-job', 'app\api\controller\MomentsController@addJob'); // 发布朋友圈
|
||||||
Route::get('list', 'app\\api\\controller\\MomentsController@getList'); // 获取朋友圈任务列表 √
|
Route::get('list', 'app\api\controller\MomentsController@getList'); // 获取朋友圈任务列表 √
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stats控制器路由
|
// Stats控制器路由
|
||||||
Route::group('stats', function () {
|
Route::group('stats', function () {
|
||||||
Route::get('basic-data', 'app\\api\\controller\\StatsController@basicData'); // 账号基本信息
|
Route::get('basic-data', 'app\api\controller\StatsController@basicData'); // 账号基本信息
|
||||||
Route::get('fans-statistics', 'app\\api\\controller\\StatsController@FansStatistics'); // 好友统计
|
Route::get('fans-statistics', 'app\api\controller\StatsController@FansStatistics'); // 好友统计
|
||||||
});
|
});
|
||||||
|
|
||||||
// User控制器路由
|
// User控制器路由
|
||||||
Route::group('user', function () {
|
Route::group('user', function () {
|
||||||
Route::post('login', 'app\\api\\controller\\UserController@login'); // 登录 √
|
Route::post('login', 'app\api\controller\UserController@login'); // 登录 √
|
||||||
Route::post('token', 'app\\api\\controller\\UserController@getNewToken'); // 获取新的token √
|
Route::post('token', 'app\api\controller\UserController@getNewToken'); // 获取新的token √
|
||||||
Route::get('info', 'app\\api\\controller\\UserController@getAccountInfo'); // 获取商户基本信息 √
|
Route::get('info', 'app\api\controller\UserController@getAccountInfo'); // 获取商户基本信息 √
|
||||||
Route::post('modify-pwd', 'app\\api\\controller\\UserController@modifyPwd'); // 修改密码
|
Route::post('modify-pwd', 'app\api\controller\UserController@modifyPwd'); // 修改密码
|
||||||
Route::get('logout', 'app\\api\\controller\\UserController@logout'); // 登出 √
|
Route::get('logout', 'app\api\controller\UserController@logout'); // 登出 √
|
||||||
Route::get('verify-code', 'app\\api\\controller\\UserController@getVerifyCode'); // 获取验证码 √
|
Route::get('verify-code', 'app\api\controller\UserController@getVerifyCode'); // 获取验证码 √
|
||||||
});
|
});
|
||||||
|
|
||||||
// WebSocket控制器路由
|
// WebSocket控制器路由
|
||||||
Route::group('websocket', function () {
|
Route::group('websocket', function () {
|
||||||
Route::post('send-personal', 'app\\api\\controller\\WebSocketController@sendPersonal'); // 个人消息发送 √
|
Route::post('send-personal', 'app\api\controller\WebSocketController@sendPersonal'); // 个人消息发送 √
|
||||||
Route::post('send-community', 'app\\api\\controller\\WebSocketController@sendCommunity'); // 发送群消息 √
|
Route::post('send-community', 'app\api\controller\WebSocketController@sendCommunity'); // 发送群消息 √
|
||||||
Route::get('get-moments', 'app\\api\\controller\\WebSocketController@getMoments'); // 获取指定账号朋友圈信息 √
|
Route::get('get-moments', 'app\api\controller\WebSocketController@getMoments'); // 获取指定账号朋友圈信息 √
|
||||||
Route::get('get-moment-source', 'app\\api\\controller\\WebSocketController@getMomentSourceRealUrl'); // 获取指定账号朋友圈图片地址
|
Route::get('get-moment-source', 'app\api\controller\WebSocketController@getMomentSourceRealUrl'); // 获取指定账号朋友圈图片地址
|
||||||
});
|
});
|
||||||
|
|
||||||
// WechatChatroom控制器路由
|
// WechatChatroom控制器路由
|
||||||
Route::group('chatroom', function () {
|
Route::group('chatroom', function () {
|
||||||
Route::get('list', 'app\\api\\controller\\WechatChatroomController@getList'); // 获取微信群聊列表 √
|
Route::get('list', 'app\api\controller\WechatChatroomController@getList'); // 获取微信群聊列表 √
|
||||||
Route::get('members', 'app\\api\\controller\\WechatChatroomController@listChatroomMember'); // 获取群成员列表 √
|
Route::get('members', 'app\api\controller\WechatChatroomController@listChatroomMember'); // 获取群成员列表 √
|
||||||
// Route::get('sync', 'app\\api\\controller\\WechatChatroomController@syncChatrooms'); // 同步微信群聊数据 √
|
// Route::get('sync', 'app\api\controller\WechatChatroomController@syncChatrooms'); // 同步微信群聊数据 √
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wechat控制器路由
|
// Wechat控制器路由
|
||||||
Route::group('wechat', function () {
|
Route::group('wechat', function () {
|
||||||
Route::get('list', 'app\\api\\controller\\WechatController@getList'); // 获取微信账号列表 √
|
Route::get('list', 'app\api\controller\WechatController@getList'); // 获取微信账号列表 √
|
||||||
});
|
});
|
||||||
|
|
||||||
// WechatFriend控制器路由
|
// WechatFriend控制器路由
|
||||||
Route::group('friend', function () {
|
Route::group('friend', function () {
|
||||||
Route::get('list', 'app\\api\\controller\\WechatFriendController@getList'); // 获取微信好友列表数据 √
|
Route::get('list', 'app\api\controller\WechatFriendController@getList'); // 获取微信好友列表数据 √
|
||||||
});
|
});
|
||||||
|
|
||||||
// Message控制器路由
|
// Message控制器路由
|
||||||
Route::group('message', function () {
|
Route::group('message', function () {
|
||||||
Route::get('getFriendsList', 'app\\api\\controller\\MessageController@getFriendsList'); // 获取微信好友列表 √
|
Route::get('getFriendsList', 'app\api\controller\MessageController@getFriendsList'); // 获取微信好友列表 √
|
||||||
Route::get('getChatroomList', 'app\\api\\controller\\MessageController@getChatroomList'); // 同步群聊消息 √
|
Route::get('getChatroomList', 'app\api\controller\MessageController@getChatroomList'); // 同步群聊消息 √
|
||||||
});
|
});
|
||||||
|
|
||||||
// AllotRule控制器路由
|
// AllotRule控制器路由
|
||||||
Route::group('allot-rule', function () {
|
Route::group('allot-rule', function () {
|
||||||
Route::get('list', 'app\\api\\controller\\AllotRuleController@getAllRules'); // 获取所有分配规则 √
|
Route::get('list', 'app\api\controller\AllotRuleController@getAllRules'); // 获取所有分配规则 √
|
||||||
Route::post('create', 'app\\api\\controller\\AllotRuleController@createRule');// 创建分配规则 √
|
Route::post('create', 'app\api\controller\AllotRuleController@createRule');// 创建分配规则 √
|
||||||
Route::post('edit', 'app\\api\\controller\\AllotRuleController@updateRule');// 编辑分配规则 √
|
Route::post('edit', 'app\api\controller\AllotRuleController@updateRule');// 编辑分配规则 √
|
||||||
Route::delete('del', 'app\\api\\controller\\AllotRuleController@deleteRule');// 删除分配规则 √
|
Route::delete('del', 'app\api\controller\AllotRuleController@deleteRule');// 删除分配规则 √
|
||||||
Route::get('autoCreate', 'app\\api\\controller\\AllotRuleController@autoCreateAllotRules');// 自动创建分配规则 √
|
Route::get('autoCreate', 'app\api\controller\AllotRuleController@autoCreateAllotRules');// 自动创建分配规则 √
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::group('scenarios', function () {
|
||||||
|
Route::any('', 'app\cunkebao\controller\plan\PostExternalApiV1Controller@index');
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -11,6 +11,86 @@ use think\Db;
|
|||||||
*/
|
*/
|
||||||
class GetAddFriendPlanDetailV1Controller extends Controller
|
class GetAddFriendPlanDetailV1Controller extends Controller
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* 生成签名
|
||||||
|
*
|
||||||
|
* @param array $params 参数数组
|
||||||
|
* @param string $apiKey API密钥
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function generateSignature($params, $apiKey)
|
||||||
|
{
|
||||||
|
// 1. 移除sign和apiKey
|
||||||
|
unset($params['sign'], $params['apiKey']);
|
||||||
|
|
||||||
|
// 2. 移除空值
|
||||||
|
$params = array_filter($params, function($value) {
|
||||||
|
return !is_null($value) && $value !== '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. 参数按键名升序排序
|
||||||
|
ksort($params);
|
||||||
|
|
||||||
|
// 4. 直接拼接参数值
|
||||||
|
$stringToSign = implode('', array_values($params));
|
||||||
|
|
||||||
|
// 5. 第一次MD5加密
|
||||||
|
$firstMd5 = md5($stringToSign);
|
||||||
|
|
||||||
|
// 6. 拼接apiKey并第二次MD5加密
|
||||||
|
return md5($firstMd5 . $apiKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成测试URL
|
||||||
|
*
|
||||||
|
* @param string $apiKey API密钥
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function testUrl($apiKey)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
if (empty($apiKey)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建测试参数
|
||||||
|
$testParams = [
|
||||||
|
'name' => '测试客户',
|
||||||
|
'phone' => '18888888888',
|
||||||
|
'apiKey' => $apiKey,
|
||||||
|
'timestamp' => time()
|
||||||
|
];
|
||||||
|
|
||||||
|
// 生成签名
|
||||||
|
$sign = $this->generateSignature($testParams, $apiKey);
|
||||||
|
$testParams['sign'] = $sign;
|
||||||
|
|
||||||
|
// 构建签名过程说明
|
||||||
|
$signParams = $testParams;
|
||||||
|
unset($signParams['sign'], $signParams['apiKey']);
|
||||||
|
ksort($signParams);
|
||||||
|
$signStr = implode('', array_values($signParams));
|
||||||
|
|
||||||
|
// 构建完整URL参数,不对中文进行编码
|
||||||
|
$urlParams = [];
|
||||||
|
foreach ($testParams as $key => $value) {
|
||||||
|
$urlParams[] = $key . '=' . $value;
|
||||||
|
}
|
||||||
|
$fullUrl = implode('&', $urlParams);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'apiKey' => $apiKey,
|
||||||
|
'originalString' => $signStr,
|
||||||
|
'sign' => $sign,
|
||||||
|
'fullUrl' => $fullUrl
|
||||||
|
];
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取计划详情
|
* 获取计划详情
|
||||||
*
|
*
|
||||||
@@ -37,11 +117,14 @@ class GetAddFriendPlanDetailV1Controller extends Controller
|
|||||||
// 解析JSON字段
|
// 解析JSON字段
|
||||||
$sceneConf = json_decode($plan['sceneConf'], true) ?: [];
|
$sceneConf = json_decode($plan['sceneConf'], true) ?: [];
|
||||||
$reqConf = json_decode($plan['reqConf'], true) ?: [];
|
$reqConf = json_decode($plan['reqConf'], true) ?: [];
|
||||||
$msgConf= json_decode($plan['msgConf'], true) ?: [];
|
$msgConf = json_decode($plan['msgConf'], true) ?: [];
|
||||||
$tagConf = json_decode($plan['tagConf'], true) ?: [];
|
$tagConf = json_decode($plan['tagConf'], true) ?: [];
|
||||||
|
|
||||||
|
// 合并数据
|
||||||
$newData['messagePlans'] = $msgConf;
|
$newData['messagePlans'] = $msgConf;
|
||||||
$newData = array_merge($newData,$sceneConf,$reqConf,$tagConf,$plan);
|
$newData = array_merge($newData, $sceneConf, $reqConf, $tagConf, $plan);
|
||||||
|
|
||||||
|
// 移除不需要的字段
|
||||||
unset(
|
unset(
|
||||||
$newData['sceneConf'],
|
$newData['sceneConf'],
|
||||||
$newData['reqConf'],
|
$newData['reqConf'],
|
||||||
@@ -50,10 +133,11 @@ class GetAddFriendPlanDetailV1Controller extends Controller
|
|||||||
$newData['userInfo'],
|
$newData['userInfo'],
|
||||||
$newData['createTime'],
|
$newData['createTime'],
|
||||||
$newData['updateTime'],
|
$newData['updateTime'],
|
||||||
$newData['deleteTime'],
|
$newData['deleteTime']
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 生成测试URL
|
||||||
|
$newData['textUrl'] = $this->testUrl($newData['apiKey']);
|
||||||
|
|
||||||
return ResponseHelper::success($newData, '获取计划详情成功');
|
return ResponseHelper::success($newData, '获取计划详情成功');
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\cunkebao\controller\plan;
|
||||||
|
|
||||||
|
use library\ResponseHelper;
|
||||||
|
use think\Controller;
|
||||||
|
use think\Db;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对外API接口控制器
|
||||||
|
*/
|
||||||
|
class PostExternalApiV1Controller extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 验证签名
|
||||||
|
*
|
||||||
|
* @param array $params 请求参数
|
||||||
|
* @param string $apiKey API密钥
|
||||||
|
* @param string $sign 签名
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function validateSign($params, $apiKey, $sign)
|
||||||
|
{
|
||||||
|
// 1. 从参数中移除sign和apiKey
|
||||||
|
unset($params['sign'], $params['apiKey']);
|
||||||
|
|
||||||
|
// 2. 移除空值
|
||||||
|
$params = array_filter($params, function($value) {
|
||||||
|
return !is_null($value) && $value !== '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. 参数按键名升序排序
|
||||||
|
ksort($params);
|
||||||
|
|
||||||
|
// 4. 直接拼接参数值
|
||||||
|
$stringToSign = implode('', array_values($params));
|
||||||
|
|
||||||
|
// 5. 第一次MD5加密
|
||||||
|
$firstMd5 = md5($stringToSign);
|
||||||
|
|
||||||
|
// 6. 拼接apiKey并第二次MD5加密
|
||||||
|
$expectedSign = md5($firstMd5 . $apiKey);
|
||||||
|
|
||||||
|
// 7. 比对签名
|
||||||
|
return $expectedSign === $sign;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对外API接口入口
|
||||||
|
*
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$params = $this->request->param();
|
||||||
|
|
||||||
|
// 验证必填参数
|
||||||
|
if (empty($params['apiKey'])) {
|
||||||
|
return ResponseHelper::error('apiKey不能为空', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($params['sign'])) {
|
||||||
|
return ResponseHelper::error('sign不能为空', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($params['timestamp'])) {
|
||||||
|
return ResponseHelper::error('timestamp不能为空', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证时间戳(允许5分钟误差)
|
||||||
|
if (abs(time() - intval($params['timestamp'])) > 300) {
|
||||||
|
return ResponseHelper::error('请求已过期', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询API密钥是否存在
|
||||||
|
$plan = Db::name('customer_acquisition_task')
|
||||||
|
->where('apiKey', $params['apiKey'])
|
||||||
|
->where('status', 1)
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if (!$plan) {
|
||||||
|
return ResponseHelper::error('无效的apiKey', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证签名
|
||||||
|
if (!$this->validateSign($params,$params['apiKey'], $params['sign'])) {
|
||||||
|
return ResponseHelper::error('签名验证失败', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$identifier = !empty($params['wechatId']) ? $params['wechatId'] : $params['phone'];
|
||||||
|
|
||||||
|
|
||||||
|
$trafficPool = Db::name('traffic_pool')->where('identifier', $identifier)->find();
|
||||||
|
if (!$trafficPool) {
|
||||||
|
Db::name('traffic_pool')->insert([
|
||||||
|
'identifier' => $identifier,
|
||||||
|
'mobile' => $params['phone']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$taskCustomer = Db::name('task_customer')->where('task_id', $plan['id'])->where('phone', $identifier)->find();
|
||||||
|
if (!$taskCustomer) {
|
||||||
|
Db::name('task_customer')->insert([
|
||||||
|
'task_id' => $plan['id'],
|
||||||
|
'phone' => $identifier
|
||||||
|
]);
|
||||||
|
|
||||||
|
return json([
|
||||||
|
'code' => 200,
|
||||||
|
'message' => '新增成功',
|
||||||
|
'data' => $identifier
|
||||||
|
]);
|
||||||
|
}else{
|
||||||
|
return json([
|
||||||
|
'code' => 200,
|
||||||
|
'message' => '已存在',
|
||||||
|
'data' => $identifier
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ class SyncAllFriendsJob
|
|||||||
public function fire(Job $job, $data)
|
public function fire(Job $job, $data)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$wechatId = $data['wechatId'];
|
$wechatId = 'Lytiao1';
|
||||||
$pageIndex = $data['pageIndex'];
|
$pageIndex = $data['pageIndex'];
|
||||||
$pageSize = $data['pageSize'];
|
$pageSize = $data['pageSize'];
|
||||||
$preFriendId = isset($data['preFriendId']) ? $data['preFriendId'] : '';
|
$preFriendId = isset($data['preFriendId']) ? $data['preFriendId'] : '';
|
||||||
|
|||||||
@@ -204,6 +204,13 @@ class WorkbenchTrafficDistributeJob
|
|||||||
->whereIn('wa.currentDeviceId', $devices)
|
->whereIn('wa.currentDeviceId', $devices)
|
||||||
->field('wf.id,wf.wechatAccountId,wf.wechatId,wf.labels,sa.userName,wa.currentDeviceId as deviceId');
|
->field('wf.id,wf.wechatAccountId,wf.wechatId,wf.labels,sa.userName,wa.currentDeviceId as deviceId');
|
||||||
|
|
||||||
|
//lllll
|
||||||
|
if($workbench->id == 65){
|
||||||
|
$query->where('wf.accountId',1602);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if(!empty($labels)){
|
if(!empty($labels)){
|
||||||
$query->where(function ($q) use ($labels) {
|
$query->where(function ($q) use ($labels) {
|
||||||
foreach ($labels as $label) {
|
foreach ($labels as $label) {
|
||||||
@@ -212,7 +219,7 @@ class WorkbenchTrafficDistributeJob
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
$list = $query->page($page, $pageSize)->order('wf.id DESC')->select();
|
$list = $query->page($page, $pageSize)->order('wf.id DESC')->select();
|
||||||
|
|
||||||
return $list;
|
return $list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user