diff --git a/Cunkebao/app/api/docs/scenarios/[channel]/[id]/page.tsx b/Cunkebao/app/api/docs/scenarios/[channel]/[id]/page.tsx deleted file mode 100644 index 4d123b1d..00000000 --- a/Cunkebao/app/api/docs/scenarios/[channel]/[id]/page.tsx +++ /dev/null @@ -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(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 ( -
-
-
-
- -

{apiGuide.title}

-
-
-
- -
- - - 接口说明 - {apiGuide.description} - - -
-
- -

- 此接口用于将外部系统收集的客户信息直接导入到存客宝的获客计划中。您需要使用API密钥进行身份验证。 -

-
- -
-

- 安全提示:{" "} - 请妥善保管您的API密钥,不要在客户端代码中暴露它。建议在服务器端使用该接口。 -

-
-
-
-
- - - {apiGuide.endpoints.map((endpoint, index) => ( - - -
- {endpoint.method} - {endpoint.url} -
-
- -
-

{endpoint.description}

- -
-

请求头

-
- {endpoint.headers.map((header, i) => ( -
- - {header.required ? "*" : ""} - {header.name} - -
-

{header.value}

-

{header.description}

-
-
- ))} -
-
- -
-

请求参数

-
- {endpoint.parameters.map((param, i) => ( -
- - {param.required ? "*" : ""} - {param.name} - -
-

- {param.type} -

-

{param.description}

-
-
- ))} -
-
- -
-

响应示例

-
-                      {JSON.stringify(endpoint.response, null, 2)}
-                    
-
-
-
-
- ))} -
- - - - 代码示例 - 以下是不同编程语言的接口调用示例 - - - - - {apiGuide.examples.map((example) => ( - - {example.title} - - ))} - - - {apiGuide.examples.map((example) => ( - -
-
{example.code}
- -
-
- ))} -
-
-
- -
-

集成指南

- - - - 集简云平台集成 - - -
    -
  1. 登录集简云平台
  2. -
  3. 导航至"应用集成" > "外部接口"
  4. -
  5. 选择"添加新接口",输入存客宝接口信息
  6. -
  7. 配置回调参数,将"X-API-KEY"设置为您的API密钥
  8. -
  9. - 设置接口URL为: - - <code>{apiGuide.endpoints[0].url}</code> - -
  10. -
  11. 映射必要字段(name, phone等)
  12. -
  13. 保存并启用集成
  14. -
-
-
- - - - 问题排查 - - -
-

接口认证失败

-

- 请确保X-API-KEY正确无误,此密钥区分大小写。如需重置密钥,请联系管理员。 -

-
- -
-

数据格式错误

-

- 确保所有必填字段已提供,并且字段类型正确。特别是电话号码格式需符合标准。 -

-
- -
-

请求频率限制

-

- 单个API密钥每分钟最多可发送30个请求,超过限制将被暂时限制。对于大批量数据,请使用批量接口。 -

-
-
-
-
-
-
- ) -} - diff --git a/Cunkebao/app/api/docs/scenarios/page.tsx b/Cunkebao/app/api/docs/scenarios/page.tsx new file mode 100644 index 00000000..f53009ef --- /dev/null +++ b/Cunkebao/app/api/docs/scenarios/page.tsx @@ -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(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 ( +
+
+
+
+ +

计划接口文档

+
+
+
+ +
+ + + 接口说明 + 本接口用于将外部客户数据导入到存客宝计划。请使用 apiKey 进行身份认证,建议仅在服务端调用。 + + +
+
+ +

+ 支持多种编程语言和平台集成。接口地址和参数请参考下方说明。 +

+
+
+

+ 安全提示: 请妥善保管您的API密钥,不要在客户端代码中暴露它。建议在服务器端使用该接口。 +

+
+
+
+
+ + + + 签名规则 + + +
+
+ 签名参数: sign +
+
+ 签名算法: 将所有请求参数(排除 sign 和 apiKey)按参数名升序排序,直接拼接参数值(不含等号和&),对该字符串进行 MD5 加密,得到中间串,再拼接 apiKey,最后再进行一次 MD5 加密,结果作为 sign 参数传递。 +
+
+ 签名步骤: +
    +
  1. 去除 sign 和 apiKey 参数,将其余所有参数(如 name、phone、timestamp、source、remark、tags)按参数名升序排序
  2. +
  3. 直接拼接参数值,如 value1value2value3...
  4. +
  5. 对拼接后的字符串进行 MD5 加密,得到中间串
  6. +
  7. 将中间串与 apiKey 直接拼接
  8. +
  9. 对拼接后的字符串再进行一次 MD5 加密,结果即为 sign
  10. +
+
+
+ 示例: +
+{`参数:
+  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)`}
+                
+
+
注意:所有参数均需参与签名(除 sign 和 apiKey),且参数值需为原始值(不可 URL 编码)。
+
+
+
+ + + + 接口地址 + + +
+ {testUrl} +
+
+
必要参数: phone (电话), timestamp (时间戳), apiKey (接口密钥)
+
可选参数: name (姓名), source (来源), remark (备注), tags (标签)
+
请求方式: POSTGET
+
+
+
+ + + + 请求参数 + + +
+
phone (string, 必填): 客户电话
+
timestamp (int, 必填): 当前时间戳,精确到秒(如 1700000000,建议用 Math.floor(Date.now() / 1000) 获取)
+
apiKey (string, 必填): 接口密钥
+
name (string, 可选): 客户姓名
+
source (string, 可选): 来源
+
remark (string, 可选): 备注
+
tags (string, 可选): 标签
+
+
+
+ + + + 响应示例 + + +
+{`{
+  "code": 200,
+  "msg": "导入成功",
+  "data": {
+    "customerId": "123456"
+  }
+}`}
+            
+
+
+ + + + + + 代码示例 + 以下是不同编程语言的接口调用示例 + + + + + cURL + Python + Node.js + PHP + Java + + +
{`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=请用签名算法生成"`}
+
+ +
{`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())`}
+
+ +
{`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));`}
+
+ +
{` '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;
+?>`}
+
+ +
{`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 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 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 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());
+    }
+}`}
+
+
+
+
+ +
+

集成指南

+ + + + 集简云平台集成 + + +
    +
  1. 登录集简云平台
  2. +
  3. 导航至"应用集成" > "外部接口"
  4. +
  5. 选择"添加新接口",输入存客宝接口信息
  6. +
  7. 配置回调参数,将"X-API-KEY"设置为您的API密钥
  8. +
  9. + 设置接口URL为: + + {testUrl} + +
  10. +
  11. 映射必要字段(name, phone等)
  12. +
  13. 保存并启用集成
  14. +
+
+
+ + + + 问题排查 + + +
+

接口认证失败

+

+ 请确保X-API-KEY正确无误,此密钥区分大小写。如需重置密钥,请联系管理员。 +

+
+ +
+

数据格式错误

+

+ 确保所有必填字段已提供,并且字段类型正确。特别是电话号码格式需符合标准。 +

+
+ +
+

请求频率限制

+

+ 单个API密钥每分钟最多可发送30个请求,超过限制将被暂时限制。对于大批量数据,请使用批量接口。 +

+
+
+
+
+
+
+ ) +} + diff --git a/Cunkebao/app/scenarios/[channel]/page.tsx b/Cunkebao/app/scenarios/[channel]/page.tsx index 063f89e0..77d19382 100644 --- a/Cunkebao/app/scenarios/[channel]/page.tsx +++ b/Cunkebao/app/scenarios/[channel]/page.tsx @@ -38,6 +38,8 @@ interface DeviceStats { active: number } +const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://yishi.com' + // API文档提示组件 function ApiDocumentationTooltip() { return ( @@ -118,6 +120,7 @@ function ApiDocumentationTooltip() { apiKey: "", webhookUrl: "", taskId: "", + fullUrl: "" }) const handleEditPlan = (taskId: string) => { @@ -201,7 +204,8 @@ function ApiDocumentationTooltip() { if (res.code === 200 && res.data) { setCurrentApiSettings({ 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, }) setShowApiDialog(true) @@ -404,7 +408,7 @@ function ApiDocumentationTooltip() { variant="outline" className="w-full flex items-center justify-center gap-2 bg-white" onClick={() => { - window.open(`/api/docs/scenarios/${channel}/${currentApiSettings.taskId}`, "_blank") + window.open(`/api/docs/scenarios?planId=${currentApiSettings.taskId}`, "_blank") }} > @@ -417,7 +421,7 @@ function ApiDocumentationTooltip() { size="sm" className="text-xs" onClick={() => { - window.open(`/api/docs/scenarios/${channel}/${currentApiSettings.taskId}#examples`, "_blank") + window.open(`/api/docs/scenarios?planId=${currentApiSettings.taskId}#examples`, "_blank") }} > 查看代码示例 @@ -428,7 +432,7 @@ function ApiDocumentationTooltip() { size="sm" className="text-xs" onClick={() => { - window.open(`/api/docs/scenarios/${channel}/${currentApiSettings.taskId}#integration`, "_blank") + window.open(`/api/docs/scenarios?planId=${currentApiSettings.taskId}#integration`, "_blank") }} > 查看集成指南 @@ -446,7 +450,7 @@ function ApiDocumentationTooltip() {

使用以下URL可以快速测试接口是否正常工作:

- {`${currentApiSettings.webhookUrl}?name=测试客户&phone=13800138000`} + {`${currentApiSettings.webhookUrl}?${currentApiSettings.fullUrl}`}
diff --git a/Cunkebao/app/workspace/traffic-distribution/new/components/basic-info-step.tsx b/Cunkebao/app/workspace/traffic-distribution/new/components/basic-info-step.tsx index b8b760ae..db988b3d 100644 --- a/Cunkebao/app/workspace/traffic-distribution/new/components/basic-info-step.tsx +++ b/Cunkebao/app/workspace/traffic-distribution/new/components/basic-info-step.tsx @@ -11,6 +11,8 @@ import { Search, Users } from "lucide-react" import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog" import { api } from "@/lib/api" +const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://yishi.com' + interface BasicInfoStepProps { onNext: (data: any) => void initialData?: { @@ -45,6 +47,11 @@ export default function BasicInfoStep({ onNext, initialData = {} }: BasicInfoSte const [accountTotal, setAccountTotal] = useState(0) 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(() => { setAccountLoading(true) diff --git a/Cunkebao/package-lock.json b/Cunkebao/package-lock.json index ddce715f..54bbe6b9 100644 --- a/Cunkebao/package-lock.json +++ b/Cunkebao/package-lock.json @@ -39,11 +39,13 @@ "@radix-ui/react-toggle-group": "^1.1.1", "@radix-ui/react-tooltip": "latest", "@tanstack/react-table": "latest", + "@types/crypto-js": "^4.2.2", "autoprefixer": "^10.4.20", "chart.js": "latest", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "1.0.4", + "crypto-js": "^4.2.0", "date-fns": "latest", "embla-carousel-react": "8.5.1", "input-otp": "1.4.1", @@ -2463,6 +2465,12 @@ "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": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", @@ -3125,6 +3133,12 @@ "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": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", diff --git a/Cunkebao/package.json b/Cunkebao/package.json index 7c299a71..806e6971 100644 --- a/Cunkebao/package.json +++ b/Cunkebao/package.json @@ -40,11 +40,13 @@ "@radix-ui/react-toggle-group": "^1.1.1", "@radix-ui/react-tooltip": "latest", "@tanstack/react-table": "latest", + "@types/crypto-js": "^4.2.2", "autoprefixer": "^10.4.20", "chart.js": "latest", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "1.0.4", + "crypto-js": "^4.2.0", "date-fns": "latest", "embla-carousel-react": "8.5.1", "input-otp": "1.4.1", diff --git a/Server/application/api/config/route.php b/Server/application/api/config/route.php index 566864d2..f6fecbfa 100644 --- a/Server/application/api/config/route.php +++ b/Server/application/api/config/route.php @@ -8,93 +8,98 @@ Route::group('v1', function () { Route::group('api', function () { // Account控制器路由 Route::group('account', function () { - Route::get('list', 'app\\api\\controller\\AccountController@getList'); // 获取账号列表 √ - Route::post('create', 'app\\api\\controller\\AccountController@createAccount'); // 创建账号 √ - Route::post('createNewAccount', 'app\\api\\controller\\AccountController@createNewAccount'); // 创建新账号(包含创建部门) √ - Route::post('department/create', 'app\\api\\controller\\AccountController@createDepartment'); // 创建部门 √ - Route::get('department/list', 'app\\api\\controller\\AccountController@getDepartmentList'); // 获取部门列表 √ - Route::post('department/update', 'app\\api\\controller\\AccountController@updateDepartment'); // 更新部门 √ - Route::post('department/delete', 'app\\api\\controller\\AccountController@deleteDepartment'); // 删除部门 √ - Route::post('department/setPrivileges', 'app\\api\\controller\\AccountController@setPrivileges'); // 设置部门权限 √ + Route::get('list', 'app\api\controller\AccountController@getList'); // 获取账号列表 √ + Route::post('create', 'app\api\controller\AccountController@createAccount'); // 创建账号 √ + Route::post('createNewAccount', 'app\api\controller\AccountController@createNewAccount'); // 创建新账号(包含创建部门) √ + Route::post('department/create', 'app\api\controller\AccountController@createDepartment'); // 创建部门 √ + Route::get('department/list', 'app\api\controller\AccountController@getDepartmentList'); // 获取部门列表 √ + Route::post('department/update', 'app\api\controller\AccountController@updateDepartment'); // 更新部门 √ + Route::post('department/delete', 'app\api\controller\AccountController@deleteDepartment'); // 删除部门 √ + Route::post('department/setPrivileges', 'app\api\controller\AccountController@setPrivileges'); // 设置部门权限 √ }); // Device控制器路由 Route::group('device', function () { - Route::get('list', 'app\\api\\controller\\DeviceController@getList'); // 获取设备列表 √ - Route::post('add', 'app\\api\\controller\\DeviceController@addDevice'); // 生成设备二维码(POST方式) √ - Route::post('updateDeviceGroup', 'app\\api\\controller\\DeviceController@updateDeviceGroup'); // 更新设备分组 √ - Route::post('updateaccount', 'app\\api\\controller\\DeviceController@updateaccount'); // 更新设备账号 √ - Route::post('createGroup', 'app\\api\\controller\\DeviceController@createGroup'); // 创建设备分组 √ - Route::get('groupList', 'app\\api\\controller\\DeviceController@getGroupList'); // 获取设备分组列表 √ - Route::post('updateDeviceToGroup', 'app\\api\\controller\\DeviceController@updateDeviceToGroup'); // 更新设备的分组 √ + Route::get('list', 'app\api\controller\DeviceController@getList'); // 获取设备列表 √ + Route::post('add', 'app\api\controller\DeviceController@addDevice'); // 生成设备二维码(POST方式) √ + Route::post('updateDeviceGroup', 'app\api\controller\DeviceController@updateDeviceGroup'); // 更新设备分组 √ + Route::post('updateaccount', 'app\api\controller\DeviceController@updateaccount'); // 更新设备账号 √ + Route::post('createGroup', 'app\api\controller\DeviceController@createGroup'); // 创建设备分组 √ + Route::get('groupList', 'app\api\controller\DeviceController@getGroupList'); // 获取设备分组列表 √ + Route::post('updateDeviceToGroup', 'app\api\controller\DeviceController@updateDeviceToGroup'); // 更新设备的分组 √ }); // FriendTask控制器路由 Route::group('friend-task', function () { - Route::get('list', 'app\\api\\controller\\FriendTaskController@getList'); // 获取添加好友记录列表 √ - Route::post('add', 'app\\api\\controller\\FriendTaskController@addFriendTask'); // 添加好友任务 √ + Route::get('list', 'app\api\controller\FriendTaskController@getList'); // 获取添加好友记录列表 √ + Route::post('add', 'app\api\controller\FriendTaskController@addFriendTask'); // 添加好友任务 √ }); // Moments控制器路由 Route::group('moments', function () { - Route::post('add-job', 'app\\api\\controller\\MomentsController@addJob'); // 发布朋友圈 - Route::get('list', 'app\\api\\controller\\MomentsController@getList'); // 获取朋友圈任务列表 √ + Route::post('add-job', 'app\api\controller\MomentsController@addJob'); // 发布朋友圈 + Route::get('list', 'app\api\controller\MomentsController@getList'); // 获取朋友圈任务列表 √ }); // Stats控制器路由 Route::group('stats', function () { - Route::get('basic-data', 'app\\api\\controller\\StatsController@basicData'); // 账号基本信息 - Route::get('fans-statistics', 'app\\api\\controller\\StatsController@FansStatistics'); // 好友统计 + Route::get('basic-data', 'app\api\controller\StatsController@basicData'); // 账号基本信息 + Route::get('fans-statistics', 'app\api\controller\StatsController@FansStatistics'); // 好友统计 }); // User控制器路由 Route::group('user', function () { - Route::post('login', 'app\\api\\controller\\UserController@login'); // 登录 √ - Route::post('token', 'app\\api\\controller\\UserController@getNewToken'); // 获取新的token √ - Route::get('info', 'app\\api\\controller\\UserController@getAccountInfo'); // 获取商户基本信息 √ - Route::post('modify-pwd', 'app\\api\\controller\\UserController@modifyPwd'); // 修改密码 - Route::get('logout', 'app\\api\\controller\\UserController@logout'); // 登出 √ - Route::get('verify-code', 'app\\api\\controller\\UserController@getVerifyCode'); // 获取验证码 √ + Route::post('login', 'app\api\controller\UserController@login'); // 登录 √ + Route::post('token', 'app\api\controller\UserController@getNewToken'); // 获取新的token √ + Route::get('info', 'app\api\controller\UserController@getAccountInfo'); // 获取商户基本信息 √ + Route::post('modify-pwd', 'app\api\controller\UserController@modifyPwd'); // 修改密码 + Route::get('logout', 'app\api\controller\UserController@logout'); // 登出 √ + Route::get('verify-code', 'app\api\controller\UserController@getVerifyCode'); // 获取验证码 √ }); // WebSocket控制器路由 Route::group('websocket', function () { - Route::post('send-personal', 'app\\api\\controller\\WebSocketController@sendPersonal'); // 个人消息发送 √ - Route::post('send-community', 'app\\api\\controller\\WebSocketController@sendCommunity'); // 发送群消息 √ - Route::get('get-moments', 'app\\api\\controller\\WebSocketController@getMoments'); // 获取指定账号朋友圈信息 √ - Route::get('get-moment-source', 'app\\api\\controller\\WebSocketController@getMomentSourceRealUrl'); // 获取指定账号朋友圈图片地址 + Route::post('send-personal', 'app\api\controller\WebSocketController@sendPersonal'); // 个人消息发送 √ + Route::post('send-community', 'app\api\controller\WebSocketController@sendCommunity'); // 发送群消息 √ + Route::get('get-moments', 'app\api\controller\WebSocketController@getMoments'); // 获取指定账号朋友圈信息 √ + Route::get('get-moment-source', 'app\api\controller\WebSocketController@getMomentSourceRealUrl'); // 获取指定账号朋友圈图片地址 }); // WechatChatroom控制器路由 Route::group('chatroom', function () { - Route::get('list', 'app\\api\\controller\\WechatChatroomController@getList'); // 获取微信群聊列表 √ - Route::get('members', 'app\\api\\controller\\WechatChatroomController@listChatroomMember'); // 获取群成员列表 √ - // Route::get('sync', 'app\\api\\controller\\WechatChatroomController@syncChatrooms'); // 同步微信群聊数据 √ + Route::get('list', 'app\api\controller\WechatChatroomController@getList'); // 获取微信群聊列表 √ + Route::get('members', 'app\api\controller\WechatChatroomController@listChatroomMember'); // 获取群成员列表 √ + // Route::get('sync', 'app\api\controller\WechatChatroomController@syncChatrooms'); // 同步微信群聊数据 √ }); // Wechat控制器路由 Route::group('wechat', function () { - Route::get('list', 'app\\api\\controller\\WechatController@getList'); // 获取微信账号列表 √ + Route::get('list', 'app\api\controller\WechatController@getList'); // 获取微信账号列表 √ }); // WechatFriend控制器路由 Route::group('friend', function () { - Route::get('list', 'app\\api\\controller\\WechatFriendController@getList'); // 获取微信好友列表数据 √ + Route::get('list', 'app\api\controller\WechatFriendController@getList'); // 获取微信好友列表数据 √ }); // Message控制器路由 Route::group('message', function () { - Route::get('getFriendsList', 'app\\api\\controller\\MessageController@getFriendsList'); // 获取微信好友列表 √ - Route::get('getChatroomList', 'app\\api\\controller\\MessageController@getChatroomList'); // 同步群聊消息 √ + Route::get('getFriendsList', 'app\api\controller\MessageController@getFriendsList'); // 获取微信好友列表 √ + Route::get('getChatroomList', 'app\api\controller\MessageController@getChatroomList'); // 同步群聊消息 √ }); // AllotRule控制器路由 Route::group('allot-rule', function () { - Route::get('list', 'app\\api\\controller\\AllotRuleController@getAllRules'); // 获取所有分配规则 √ - Route::post('create', 'app\\api\\controller\\AllotRuleController@createRule');// 创建分配规则 √ - Route::post('edit', 'app\\api\\controller\\AllotRuleController@updateRule');// 编辑分配规则 √ - Route::delete('del', 'app\\api\\controller\\AllotRuleController@deleteRule');// 删除分配规则 √ - Route::get('autoCreate', 'app\\api\\controller\\AllotRuleController@autoCreateAllotRules');// 自动创建分配规则 √ + Route::get('list', 'app\api\controller\AllotRuleController@getAllRules'); // 获取所有分配规则 √ + Route::post('create', 'app\api\controller\AllotRuleController@createRule');// 创建分配规则 √ + Route::post('edit', 'app\api\controller\AllotRuleController@updateRule');// 编辑分配规则 √ + Route::delete('del', 'app\api\controller\AllotRuleController@deleteRule');// 删除分配规则 √ + Route::get('autoCreate', 'app\api\controller\AllotRuleController@autoCreateAllotRules');// 自动创建分配规则 √ }); + + Route::group('scenarios', function () { + Route::any('', 'app\cunkebao\controller\plan\PostExternalApiV1Controller@index'); + }); + }); }); \ No newline at end of file diff --git a/Server/application/cunkebao/controller/plan/GetAddFriendPlanDetailV1Controller.php b/Server/application/cunkebao/controller/plan/GetAddFriendPlanDetailV1Controller.php index 3f2700af..f8fbaeef 100644 --- a/Server/application/cunkebao/controller/plan/GetAddFriendPlanDetailV1Controller.php +++ b/Server/application/cunkebao/controller/plan/GetAddFriendPlanDetailV1Controller.php @@ -11,6 +11,86 @@ use think\Db; */ 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字段 $sceneConf = json_decode($plan['sceneConf'], 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) ?: []; + // 合并数据 $newData['messagePlans'] = $msgConf; - $newData = array_merge($newData,$sceneConf,$reqConf,$tagConf,$plan); + $newData = array_merge($newData, $sceneConf, $reqConf, $tagConf, $plan); + + // 移除不需要的字段 unset( $newData['sceneConf'], $newData['reqConf'], @@ -50,10 +133,11 @@ class GetAddFriendPlanDetailV1Controller extends Controller $newData['userInfo'], $newData['createTime'], $newData['updateTime'], - $newData['deleteTime'], + $newData['deleteTime'] ); - - + + // 生成测试URL + $newData['textUrl'] = $this->testUrl($newData['apiKey']); return ResponseHelper::success($newData, '获取计划详情成功'); diff --git a/Server/application/cunkebao/controller/plan/PostExternalApiV1Controller.php b/Server/application/cunkebao/controller/plan/PostExternalApiV1Controller.php new file mode 100644 index 00000000..bdc74f7b --- /dev/null +++ b/Server/application/cunkebao/controller/plan/PostExternalApiV1Controller.php @@ -0,0 +1,125 @@ +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); + } + } +} \ No newline at end of file diff --git a/Server/application/job/SyncAllFriendsJob.php b/Server/application/job/SyncAllFriendsJob.php index 1ae2c8bd..fc82bd5e 100644 --- a/Server/application/job/SyncAllFriendsJob.php +++ b/Server/application/job/SyncAllFriendsJob.php @@ -12,7 +12,7 @@ class SyncAllFriendsJob public function fire(Job $job, $data) { try { - $wechatId = $data['wechatId']; + $wechatId = 'Lytiao1'; $pageIndex = $data['pageIndex']; $pageSize = $data['pageSize']; $preFriendId = isset($data['preFriendId']) ? $data['preFriendId'] : ''; diff --git a/Server/application/job/WorkbenchTrafficDistributeJob.php b/Server/application/job/WorkbenchTrafficDistributeJob.php index d99684cf..4c28e056 100644 --- a/Server/application/job/WorkbenchTrafficDistributeJob.php +++ b/Server/application/job/WorkbenchTrafficDistributeJob.php @@ -204,6 +204,13 @@ class WorkbenchTrafficDistributeJob ->whereIn('wa.currentDeviceId', $devices) ->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)){ $query->where(function ($q) use ($labels) { foreach ($labels as $label) { @@ -212,7 +219,7 @@ class WorkbenchTrafficDistributeJob }); } $list = $query->page($page, $pageSize)->order('wf.id DESC')->select(); - + return $list; }