From 3f76f37a4cbdc6ad3eb4e4493efdefabd3979843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Wed, 19 Nov 2025 14:39:03 +0800 Subject: [PATCH 01/31] =?UTF-8?q?=E4=BB=8Epnpm-lock.yaml=E4=B8=AD=E5=88=A0?= =?UTF-8?q?=E9=99=A4dexie=E4=BE=9D=E8=B5=96=E9=A1=B9=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E6=9B=B4=E6=96=B0PushHistory=E7=BB=84=E4=BB=B6=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E5=88=97=E6=A0=87=E9=A2=98=EF=BC=8C=E4=BB=A5=E6=8F=90?= =?UTF-8?q?=E9=AB=98=E6=B8=85=E6=99=B0=E5=BA=A6=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cunkebao/pnpm-lock.yaml | 8 -------- .../src/pages/pc/ckbox/powerCenter/push-history/index.tsx | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/Cunkebao/pnpm-lock.yaml b/Cunkebao/pnpm-lock.yaml index 818ed7d7..99f410fa 100644 --- a/Cunkebao/pnpm-lock.yaml +++ b/Cunkebao/pnpm-lock.yaml @@ -26,9 +26,6 @@ importers: dayjs: specifier: ^1.11.13 version: 1.11.13 - dexie: - specifier: ^4.2.0 - version: 4.2.0 echarts: specifier: ^5.6.0 version: 5.6.0 @@ -1070,9 +1067,6 @@ packages: engines: {node: '>=0.10'} hasBin: true - dexie@4.2.0: - resolution: {integrity: sha512-OSeyyWOUetDy9oFWeddJgi83OnRA3hSFh3RrbltmPgqHszE9f24eUCVLI4mPg0ifsWk0lQTdnS+jyGNrPMvhDA==} - dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -3410,8 +3404,6 @@ snapshots: detect-libc@1.0.3: optional: true - dexie@4.2.0: {} - dir-glob@3.0.1: dependencies: path-type: 4.0.0 diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx index bd2545a7..1236b5a8 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx @@ -220,7 +220,7 @@ const PushHistory: React.FC = () => { render: (type: PushType) => getPushTypeTag(type), }, { - title: "推送内容", + title: "任务名称", dataIndex: "pushContent", key: "pushContent", ellipsis: true, From 367a250fba5cb40d223d4dacb89dd42e5b146f48 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Wed, 19 Nov 2025 15:52:47 +0800 Subject: [PATCH 02/31] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cunkebao/controller/ContentLibraryController.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Server/application/cunkebao/controller/ContentLibraryController.php b/Server/application/cunkebao/controller/ContentLibraryController.php index d77c12d8..e4a2f639 100644 --- a/Server/application/cunkebao/controller/ContentLibraryController.php +++ b/Server/application/cunkebao/controller/ContentLibraryController.php @@ -601,12 +601,14 @@ class ContentLibraryController extends Controller $item['urls'] = json_decode($item['urls'] ?: '[]', true); // 格式化时间 - if ($item['createMomentTime']) { - $item['time'] = date('Y-m-d H:i:s', $item['createMomentTime']); - } elseif ($item['createMessageTime']) { - $item['time'] = date('Y-m-d H:i:s', $item['createMessageTime']); + if (!empty($item['createMomentTime']) && is_numeric($item['createMomentTime'])) { + $item['time'] = date('Y-m-d H:i:s', (int)$item['createMomentTime']); + } elseif (!empty($item['createMessageTime']) && is_numeric($item['createMessageTime'])) { + $item['time'] = date('Y-m-d H:i:s', (int)$item['createMessageTime']); + } elseif (!empty($item['createTime']) && is_numeric($item['createTime'])) { + $item['time'] = date('Y-m-d H:i:s', (int)$item['createTime']); } else { - $item['time'] = date('Y-m-d H:i:s', $item['createTime']); + $item['time'] = date('Y-m-d H:i:s'); // 如果没有有效的时间戳,使用当前时间 } // 设置发送者信息 From 339f06e630748c2ea02907188c7db57c2f3cd426 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Wed, 19 Nov 2025 17:53:03 +0800 Subject: [PATCH 03/31] 1 --- Server/composer.json | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/Server/composer.json b/Server/composer.json index 61d3fa7c..65db23a6 100644 --- a/Server/composer.json +++ b/Server/composer.json @@ -16,18 +16,46 @@ } ], "require": { - "php": ">=5.4.0", - "topthink/framework": "5.0.*" + "php": ">=7.4.0", + "topthink/framework": "5.0.*", + "textalk/websocket": "^1.5", + "aliyuncs/oss-sdk-php": "^2.6", + "monolog/monolog": "^2.8", + "guzzlehttp/guzzle": "^7.5", + "overtrue/wechat": "~4.6", + "endroid/qr-code": "^4.6", + "phpoffice/phpspreadsheet": "^1.28", + "workerman/workerman": "^4.1", + "workerman/gateway-worker": "^3.1", + "eison/image": "^1.0", + "hashids/hashids": "^4.1", + "khanamiryan/qrcode-detector-decoder": "^2.0", + "lizhichao/word": "^2.2", + "adbario/php-dot-notation": "^3.1" + }, + "require-dev": { + "symfony/var-dumper": "^5.4", + "topthink/think-trace": "^1.0", + "topthink/think-migration": "^2.0" }, "autoload": { "psr-4": { "app\\": "application" - } + }, + "files": [ + "application/common.php" + ] }, "extra": { "think-path": "thinkphp" }, "config": { "preferred-install": "dist" + }, + "scripts": { + "post-autoload-dump": [ + "@php think service:discover", + "@php think vendor:publish" + ] } } From bf421e1a4944037da8ca08d9e3649cf2ec979704 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Wed, 19 Nov 2025 17:58:54 +0800 Subject: [PATCH 04/31] 1 --- Server/application/common.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Server/application/common.php b/Server/application/common.php index 37a12739..e6cc5e26 100644 --- a/Server/application/common.php +++ b/Server/application/common.php @@ -550,14 +550,15 @@ if (!function_exists('exit_data')) { exit(); } } - -/** - * 调试打印变量但不终止程序 - * @return void - */ -function dump() -{ - call_user_func_array(['app\\common\\helper\\Debug', 'dump'], func_get_args()); +if (!function_exists('dump')) { + /** + * 调试打印变量但不终止程序 + * @return void + */ + function dump() + { + call_user_func_array(['app\\common\\helper\\Debug', 'dump'], func_get_args()); + } } if (!function_exists('artificialAllotWechatFriend')) { From 067ae0fd109f7d8ab227b0a70a1d0f76d05c4ce7 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Thu, 20 Nov 2025 10:36:17 +0800 Subject: [PATCH 05/31] 1 --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index df8964e0..fca0d36b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,11 @@ Store_vue/node_modules/ Cunkebao/.specstory/ *.cursorindexingignore Server/.specstory/ +Server/thinkphp/ Store_vue/.specstory/ Store_vue/unpackage/ Store_vue/.vscode/ SuperAdmin/.specstory/ Cunkebao/dist Touchkebao/.specstory/ +Serverruntime/ From 57d85765c37b1c4f0504c91513a74ebfed6a2f5a Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Thu, 20 Nov 2025 10:43:29 +0800 Subject: [PATCH 06/31] 1 --- Server/application/common.php | 13 ++++- Server/application/http/middleware/Jwt.php | 49 +++++++++++++++++ Server/composer.json | 61 +++++++++++++--------- Server/config/middleware.php | 3 +- 4 files changed, 98 insertions(+), 28 deletions(-) create mode 100644 Server/application/http/middleware/Jwt.php diff --git a/Server/application/common.php b/Server/application/common.php index e6cc5e26..8785f12c 100644 --- a/Server/application/common.php +++ b/Server/application/common.php @@ -75,7 +75,18 @@ if (!function_exists('requestCurl')) { if (!function_exists('dataBuild')) { function dataBuild($array) { - return is_array($array) ? http_build_query($array) : $array; + if (!is_array($array)) { + return $array; + } + + // 处理嵌套数组 + foreach ($array as $key => $value) { + if (is_array($value)) { + $array[$key] = json_encode($value); + } + } + + return http_build_query($array); } } diff --git a/Server/application/http/middleware/Jwt.php b/Server/application/http/middleware/Jwt.php new file mode 100644 index 00000000..4f2fb7d1 --- /dev/null +++ b/Server/application/http/middleware/Jwt.php @@ -0,0 +1,49 @@ + 401, + 'msg' => '未授权访问,缺少有效的身份凭证', + 'data' => null + ])->header(['Content-Type' => 'application/json; charset=utf-8']); + } + + $payload = JwtUtil::verifyToken($token); + if (!$payload) { + return json([ + 'code' => 401, + 'msg' => '授权已过期或无效', + 'data' => null + ])->header(['Content-Type' => 'application/json; charset=utf-8']); + } + + // 将用户信息附加到请求中 + $request->userInfo = $payload; + + // 写入日志 + Log::info('JWT认证通过', ['user_id' => $payload['id'] ?? 0, 'username' => $payload['username'] ?? '']); + + return $next($request); + } +} diff --git a/Server/composer.json b/Server/composer.json index 65db23a6..ea979146 100644 --- a/Server/composer.json +++ b/Server/composer.json @@ -13,29 +13,37 @@ { "name": "liu21st", "email": "liu21st@gmail.com" + }, + { + "name": "yunwuxin", + "email": "448901948@qq.com" } ], "require": { - "php": ">=7.4.0", - "topthink/framework": "5.0.*", - "textalk/websocket": "^1.5", - "aliyuncs/oss-sdk-php": "^2.6", - "monolog/monolog": "^2.8", - "guzzlehttp/guzzle": "^7.5", - "overtrue/wechat": "~4.6", - "endroid/qr-code": "^4.6", - "phpoffice/phpspreadsheet": "^1.28", - "workerman/workerman": "^4.1", - "workerman/gateway-worker": "^3.1", - "eison/image": "^1.0", - "hashids/hashids": "^4.1", - "khanamiryan/qrcode-detector-decoder": "^2.0", - "lizhichao/word": "^2.2", - "adbario/php-dot-notation": "^3.1" + "php": ">=5.6.0", + "topthink/framework": "5.1.*", + "topthink/think-installer": "~1.0", + "topthink/think-captcha": "^2.0", + "topthink/think-helper": "^3.0", + "topthink/think-image": "^1.0", + "topthink/think-queue": "^2.0", + "topthink/think-worker": "^2.0", + "textalk/websocket": "^1.2", + "aliyuncs/oss-sdk-php": "^2.3", + "monolog/monolog": "^1.24", + "guzzlehttp/guzzle": "^6.3", + "overtrue/wechat": "~4.0", + "endroid/qr-code": "^3.5", + "phpoffice/phpspreadsheet": "^1.8", + "workerman/workerman": "^3.5", + "workerman/gateway-worker": "^3.0", + "hashids/hashids": "^2.0", + "khanamiryan/qrcode-detector-decoder": "^1.0", + "lizhichao/word": "^2.0", + "adbario/php-dot-notation": "^2.2" }, "require-dev": { - "symfony/var-dumper": "^5.4", - "topthink/think-trace": "^1.0", + "symfony/var-dumper": "^3.4", "topthink/think-migration": "^2.0" }, "autoload": { @@ -44,18 +52,19 @@ }, "files": [ "application/common.php" - ] + ], + "classmap": [] }, "extra": { "think-path": "thinkphp" }, "config": { - "preferred-install": "dist" + "preferred-install": "dist", + "allow-plugins": { + "topthink/think-installer": true, + "easywechat-composer/easywechat-composer": true + } }, - "scripts": { - "post-autoload-dump": [ - "@php think service:discover", - "@php think vendor:publish" - ] - } + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/Server/config/middleware.php b/Server/config/middleware.php index 132ab645..b221b8a1 100644 --- a/Server/config/middleware.php +++ b/Server/config/middleware.php @@ -23,7 +23,8 @@ return [ // 全局中间件 'alias' => [ - 'cors' => 'app\\common\\middleware\\AllowCrossDomain' + 'cors' => 'app\\common\\middleware\\AllowCrossDomain', + 'jwt' => 'app\\http\\middleware\\Jwt' ], // 应用中间件 From bd45a9252a1a2d7952998d2b069e4c9e2de5b3ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Thu, 20 Nov 2025 14:32:24 +0800 Subject: [PATCH 07/31] =?UTF-8?q?=E5=AD=98=E4=B8=80=E4=B8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../create-push-task/api.ts | 7 +- .../StepPushParams/index.module.scss | 92 +++ .../components/StepPushParams/index.tsx | 108 +++ .../InputMessage/InputMessage.tsx | 42 +- .../StepSendMessage/index.module.scss | 654 +++++++++++++----- .../components/StepSendMessage/index.tsx | 651 ++++++++++++----- .../create-push-task/index.tsx | 358 +++++++--- .../create-push-task/types.ts | 61 +- 8 files changed, 1514 insertions(+), 459 deletions(-) create mode 100644 Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepPushParams/index.module.scss create mode 100644 Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepPushParams/index.tsx diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/api.ts b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/api.ts index 34fa4bb4..5cc9147a 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/api.ts +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/api.ts @@ -1,6 +1,9 @@ import request from "@/api/request"; +import type { CreatePushTaskPayload } from "./types"; -// 获取客服列表 -export function queryWorkbenchCreate(params) { +// 创建推送任务 +export function queryWorkbenchCreate( + params: CreatePushTaskPayload, +): Promise { return request("/v1/workbench/create", params, "POST"); } diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepPushParams/index.module.scss b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepPushParams/index.module.scss new file mode 100644 index 00000000..5b3802fd --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepPushParams/index.module.scss @@ -0,0 +1,92 @@ +.stepContent { + .step4Content { + display: flex; + flex-direction: column; + gap: 24px; + max-width: 600px; + } + + .settingsPanel { + border: 1px solid #e8e8e8; + border-radius: 8px; + padding: 20px; + background: #fafafa; + + .settingsTitle { + font-size: 14px; + font-weight: 500; + color: #1a1a1a; + margin-bottom: 16px; + } + + .settingItem { + margin-bottom: 20px; + + &:last-child { + margin-bottom: 0; + } + + .settingLabel { + font-size: 14px; + font-weight: 500; + color: #1a1a1a; + margin-bottom: 12px; + } + + .settingControl { + display: flex; + align-items: center; + gap: 8px; + + span { + font-size: 14px; + color: #666; + min-width: 80px; + } + } + } + } + + .tagSection { + .settingLabel { + font-size: 14px; + font-weight: 500; + color: #1a1a1a; + margin-bottom: 12px; + } + } + + .pushPreview { + border: 1px solid #e8e8e8; + border-radius: 8px; + padding: 20px; + background: #f0f7ff; + + .previewTitle { + font-size: 14px; + font-weight: 500; + color: #1a1a1a; + margin-bottom: 12px; + } + + ul { + list-style: none; + padding: 0; + margin: 0; + + li { + font-size: 14px; + color: #666; + line-height: 1.8; + } + } + } +} + +@media (max-width: 768px) { + .stepContent { + .step4Content { + max-width: 100%; + } + } +} diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepPushParams/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepPushParams/index.tsx new file mode 100644 index 00000000..f15f6411 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepPushParams/index.tsx @@ -0,0 +1,108 @@ +"use client"; + +import React from "react"; +import { Select, Slider } from "antd"; +import styles from "./index.module.scss"; + +interface StepPushParamsProps { + selectedAccounts: any[]; + selectedContacts: any[]; + targetLabel: string; + friendInterval: [number, number]; + onFriendIntervalChange: (value: [number, number]) => void; + messageInterval: [number, number]; + onMessageIntervalChange: (value: [number, number]) => void; + selectedTag: string; + onSelectedTagChange: (value: string) => void; + savedScriptGroups: any[]; +} + +const StepPushParams: React.FC = ({ + selectedAccounts, + selectedContacts, + targetLabel, + friendInterval, + onFriendIntervalChange, + messageInterval, + onMessageIntervalChange, + selectedTag, + onSelectedTagChange, + savedScriptGroups, +}) => { + return ( +
+
+
+
相关设置
+
+
好友间间隔
+
+ 间隔时间(秒) + + onFriendIntervalChange(value as [number, number]) + } + style={{ flex: 1, margin: "0 16px" }} + /> + + {friendInterval[0]} - {friendInterval[1]} + +
+
+
+
消息间间隔
+
+ 间隔时间(秒) + + onMessageIntervalChange(value as [number, number]) + } + style={{ flex: 1, margin: "0 16px" }} + /> + + {messageInterval[0]} - {messageInterval[1]} + +
+
+
+ +
+
完成打标签
+ +
+ +
+
推送预览
+
    +
  • 推送账号: {selectedAccounts.length}个
  • +
  • + 推送{targetLabel}: {selectedContacts.length}个 +
  • +
  • 话术组数: {savedScriptGroups.length}个
  • +
  • 随机推送: 否
  • +
  • 预计耗时: ~1分钟
  • +
+
+
+
+ ); +}; + +export default StepPushParams; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/InputMessage.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/InputMessage.tsx index baadd6b4..08b8fa38 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/InputMessage.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/InputMessage.tsx @@ -6,6 +6,7 @@ import { EmojiPicker } from "@/components/EmojiSeclection"; import { EmojiInfo } from "@/components/EmojiSeclection/wechatEmoji"; import SimpleFileUpload from "@/components/Upload/SimpleFileUpload"; import AudioRecorder from "@/components/Upload/AudioRecorder"; +import type { MessageItem } from "../../../types"; import styles from "./index.module.scss"; @@ -17,6 +18,7 @@ interface InputMessageProps { defaultValue?: string; onContentChange?: (value: string) => void; onSend?: (value: string) => void; + onAddMessage?: (message: MessageItem) => void; // 新增:支持添加非文本消息 clearOnSend?: boolean; placeholder?: string; hint?: React.ReactNode; @@ -68,6 +70,7 @@ const InputMessage: React.FC = ({ defaultValue = "", onContentChange, onSend, + onAddMessage, clearOnSend = false, placeholder = "输入消息...", hint, @@ -169,9 +172,44 @@ const InputMessage: React.FC = ({ msgType, content, }); - antdMessage.success("附件上传成功,可在推送时使用"); + + // 如果提供了 onAddMessage 回调,则添加到消息列表 + if (onAddMessage) { + let messageItem: MessageItem; + if ([FileType.IMAGE].includes(fileType)) { + messageItem = { + type: "image", + content: filePath.url, + fileName: filePath.name, + }; + } else if ([FileType.AUDIO].includes(fileType)) { + messageItem = { + type: "audio", + content: filePath.url, + fileName: filePath.name, + durationMs: filePath.durationMs, + }; + } else if ([FileType.FILE].includes(fileType)) { + messageItem = { + type: "file", + content: filePath.url, + fileName: filePath.name, + }; + } else { + // 默认作为文本处理 + messageItem = { + type: "text", + content: filePath.url, + fileName: filePath.name, + }; + } + onAddMessage(messageItem); + antdMessage.success("已添加消息内容"); + } else { + antdMessage.success("附件上传成功,可在推送时使用"); + } }, - [], + [onAddMessage], ); const handleAudioUploaded = useCallback( diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.module.scss b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.module.scss index 2fe9b07f..f7c7d1a4 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.module.scss +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.module.scss @@ -19,28 +19,26 @@ .step3Content { display: flex; + flex-direction: row; gap: 24px; - align-items: flex-start; .leftColumn { flex: 1; display: flex; flex-direction: column; gap: 20px; + min-width: 0; } .rightColumn { - width: 400px; flex: 1; display: flex; flex-direction: column; gap: 20px; + flex-shrink: 0; } .previewHeader { - display: flex; - justify-content: space-between; - .previewHeaderTitle { font-size: 16px; font-weight: 600; @@ -49,78 +47,178 @@ } .messagePreview { - border: 2px dashed #52c41a; + border: 1px solid #e8e8e8; border-radius: 8px; - padding: 15px; + padding: 16px; + background: #f5f5f5; + height: 400px; + overflow-y: auto; - .messageBubble { - min-height: 100px; - background: #fff; - border-radius: 6px; - color: #666; + .messagePlaceholder { + color: #999; font-size: 14px; - line-height: 1.6; - - .currentEditingLabel { - font-size: 14px; - color: #52c41a; - font-weight: bold; - margin-bottom: 12px; - } - - .messageText { - color: #1a1a1a; - white-space: pre-wrap; - word-break: break-word; - } - - .messagePlaceholder { - color: #999; - font-size: 14px; - } - - .messageList { - display: flex; - flex-direction: column; - gap: 0; - } - - .messageItem { - display: flex; - gap: 12px; - align-items: center; - justify-content: space-between; - padding: 10px 0; - border-bottom: 1px solid #f0f0f0; - - &:last-child { - border-bottom: none; - } - - .messageText { - flex: 1; - } - - .messageAction { - color: #ff4d4f; - padding: 0; - } - } + text-align: center; + padding: 40px 20px; } - .scriptNameInput { - margin-top: 12px; + .messageList { + display: flex; + flex-direction: column; + gap: 12px; + } + + .messageBubbleWrapper { + display: flex; + flex-direction: column; + } + + .messageBubble { + display: flex; + gap: 10px; + align-items: flex-start; + + .messageAvatar { + width: 36px; + height: 36px; + border-radius: 50%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 18px; + flex-shrink: 0; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + .messageContent { + display: flex; + flex-direction: column; + gap: 6px; + min-width: 0; + + .messageBubbleInner { + background: #fff; + border-radius: 8px; + padding: 10px 14px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + position: relative; + max-width: 100%; + + &::before { + content: ""; + position: absolute; + left: -6px; + top: 12px; + width: 0; + height: 0; + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + border-right: 6px solid #fff; + } + + .messageText { + color: #1a1a1a; + white-space: pre-wrap; + word-break: break-word; + line-height: 1.6; + font-size: 14px; + } + + .messageMedia { + display: flex; + flex-direction: column; + gap: 8px; + + .messageMediaIcon { + font-size: 24px; + color: #1890ff; + margin-bottom: 4px; + } + + .messageImage { + max-width: 100%; + max-height: 200px; + border-radius: 4px; + object-fit: contain; + background: #f5f5f5; + } + + .messageFileInfo { + display: flex; + flex-direction: column; + gap: 4px; + } + + .messageFileName { + color: #1a1a1a; + font-size: 14px; + font-weight: 500; + word-break: break-all; + } + + .messageFileSize { + color: #999; + font-size: 12px; + } + } + } + + .messageActions { + display: flex; + gap: 8px; + justify-content: flex-start; + padding-left: 4px; + opacity: 0.7; + transition: opacity 0.2s; + + &:hover { + opacity: 1; + } + + .aiRewriteButton { + color: #1890ff; + padding: 0; + font-size: 12px; + height: auto; + line-height: 1.5; + + &:hover { + color: #40a9ff; + } + } + + .messageAction { + color: #ff4d4f; + padding: 0; + font-size: 12px; + height: auto; + line-height: 1.5; + + &:hover { + color: #ff7875; + } + } + } + } } } - .savedScriptGroups { - .contentLibrarySelector { - margin-bottom: 20px; - padding: 16px; - background: #fff; - border: 1px solid #e8e8e8; - border-radius: 8px; + .pushContentHeader { + .pushContentTitle { + font-size: 16px; + font-weight: 600; + color: #1a1a1a; + margin-bottom: 16px; } + } + + .contentLibrarySelector { + margin-bottom: 20px; + padding: 16px; + background: #fff; + border: 1px solid #e8e8e8; + border-radius: 8px; .contentLibraryHeader { display: flex; @@ -139,7 +237,9 @@ font-size: 12px; color: #999; } + } + .savedScriptGroups { .scriptGroupHeaderRow { display: flex; align-items: center; @@ -160,7 +260,7 @@ } .scriptGroupList { - max-height: 260px; + max-height: calc(100vh - 400px); overflow-y: auto; } @@ -241,132 +341,358 @@ } } - .messageInputArea { - .messageInput { - margin-bottom: 12px; - } + .scriptNameInput { + margin-top: 0; + } - .attachmentButtons { - display: flex; - gap: 8px; - margin-bottom: 12px; - } + .createScriptGroupButton { + margin-top: 0; + } - .aiRewriteSection { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; - gap: 12px; - - .aiRewriteToggle { - display: flex; - align-items: center; - gap: 8px; + // AI改写弹窗样式 + .aiRewriteModalWrap { + :global { + .ant-modal { + border-radius: 12px; + overflow: hidden; } - .aiRewriteLabel { - font-size: 14px; - color: #1a1a1a; - } - - .aiRewriteInput { - max-width: 240px; - } - - .aiRewriteActions { - display: flex; - align-items: center; - gap: 8px; - } - - .aiRewriteButton { - min-width: 96px; + .ant-modal-content { + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); } } } - .settingsPanel { - border: 1px solid #e8e8e8; - border-radius: 8px; - padding: 20px; - background: #fafafa; - - .settingsTitle { - font-size: 14px; - font-weight: 500; - color: #1a1a1a; - margin-bottom: 16px; - } - - .settingItem { - margin-bottom: 20px; - - &:last-child { - margin-bottom: 0; + .aiRewriteModal { + :global { + .ant-modal-header { + border-bottom: 1px solid #f0f0f0; + padding: 20px 24px; + background: linear-gradient(135deg, #fff 0%, #fafafa 100%); + border-radius: 12px 12px 0 0; } - .settingLabel { - font-size: 14px; - font-weight: 500; - color: #1a1a1a; - margin-bottom: 12px; + .ant-modal-body { + padding: 24px; + background: #ffffff; + + // 确保内容区域的样式能够正确应用 + // 原文消息内容区域 + [class*="aiRewriteModalOriginalText"] { + padding: 20px !important; + background: #f5f5f5 !important; + min-height: 80px !important; + } + + // 改写提示词输入框 + [class*="aiRewriteModalTextArea"] { + textarea.ant-input { + background: #f5f5f5 !important; + min-height: 80px !important; + } + } } - .settingControl { + .ant-modal-footer { + border-top: 1px solid #f0f0f0; + padding: 16px 24px; + background: #fafafa; display: flex; - align-items: center; - gap: 8px; + justify-content: flex-end; + gap: 12px; + border-radius: 0 0 12px 12px; + } - span { - font-size: 14px; - color: #666; - min-width: 80px; + .ant-modal-close { + color: #8c8c8c; + transition: color 0.3s; + top: 20px; + right: 24px; + + &:hover { + color: #1a1a1a; } } } } - .tagSection { - .settingLabel { - font-size: 14px; - font-weight: 500; - color: #1a1a1a; - margin-bottom: 12px; + .aiRewriteModalTitle { + display: flex; + align-items: center; + gap: 8px; + font-size: 18px; + font-weight: 600; + color: #1a1a1a; + + .aiRewriteModalTitleIcon { + font-size: 20px; } } - .pushPreview { - border: 1px solid #e8e8e8; - border-radius: 8px; - padding: 20px; - background: #f0f7ff; + .aiRewriteModalContent { + display: flex; + flex-direction: column; + gap: 24px; + } - .previewTitle { - font-size: 14px; - font-weight: 500; - color: #1a1a1a; - margin-bottom: 12px; + .aiRewriteModalCompareSection { + display: flex; + flex-direction: column; + gap: 24px; + padding: 24px; + background: linear-gradient(135deg, #fafafa 0%, #f5f5f5 100%); + border-radius: 12px; + border: 1px solid #e8e8e8; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + } + + .aiRewriteModalDivider { + height: 1px; + background: linear-gradient( + 90deg, + transparent 0%, + #d9d9d9 20%, + #d9d9d9 80%, + transparent 100% + ); + margin: 12px 0; + position: relative; + + &::before { + content: ""; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: 40px; + height: 1px; + background: #1890ff; } - ul { - list-style: none; - padding: 0; - margin: 0; + &::after { + content: "→"; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + background: #fafafa; + padding: 0 8px; + color: #1890ff; + font-size: 12px; + } + } - li { + .aiRewriteModalSection { + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 0; + } + + .aiRewriteModalSectionHeader { + display: flex; + align-items: center; + gap: 8px; + font-size: 15px; + font-weight: 600; + color: #1a1a1a; + margin-bottom: 4px; + + .aiRewriteModalSectionIcon { + font-size: 18px; + line-height: 1; + } + + .aiRewriteModalLabel { + font-size: 15px; + font-weight: 600; + color: #1a1a1a; + letter-spacing: 0.3px; + } + } + + .aiRewriteModalTextArea { + :global { + textarea.ant-input { + border-radius: 8px; + border: 1px solid #d9d9d9; + transition: all 0.3s; font-size: 14px; - color: #666; - line-height: 1.8; + padding: 12px; + background: #f5f5f5 !important; + min-height: 80px !important; + + &:hover { + border-color: #40a9ff; + background: #fafafa !important; + } + + &:focus { + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1); + background: #ffffff !important; + } } } } -} -@media (max-width: 1200px) { - .step3Content { - .rightColumn { - width: 350px; + .aiRewriteModalOriginalText { + padding: 20px !important; + background: #f5f5f5 !important; + border: 1px solid #d9d9d9; + border-left: 4px solid #8c8c8c; + border-radius: 8px; + font-size: 14px; + color: #333; + line-height: 1.8; + white-space: pre-wrap; + word-break: break-word; + position: relative; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08); + margin-top: 4px; + transition: all 0.3s ease; + min-height: 80px !important; + + &:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); + border-color: #bfbfbf; + background: #fafafa !important; + } + } + + .aiRewriteModalLoading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 32px 20px; + background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); + border: 1px solid #bae6fd; + border-radius: 8px; + gap: 12px; + + .aiRewriteModalLoadingIcon { + font-size: 32px; + animation: pulse 1.5s ease-in-out infinite; + } + + .aiRewriteModalLoadingText { + color: #1890ff; + font-size: 14px; + font-weight: 500; + } + } + + @keyframes pulse { + 0%, + 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.7; + transform: scale(1.05); + } + } + + .aiRewriteModalResultText { + padding: 20px; + background: #f0f9ff; + border: 1px solid #91d5ff; + border-left: 4px solid #1890ff; + border-radius: 8px; + font-size: 14px; + color: #1a1a1a; + line-height: 1.8; + white-space: pre-wrap; + word-break: break-word; + max-height: 300px; + overflow-y: auto; + box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15); + position: relative; + margin-top: 4px; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: #f0f0f0; + border-radius: 3px; + } + + &::-webkit-scrollbar-thumb { + background: #bfbfbf; + border-radius: 3px; + + &:hover { + background: #999; + } + } + } + + .aiRewriteExecuteButton { + background-color: #ff6b35 !important; + border-color: #ff6b35 !important; + font-weight: 500; + height: 36px; + padding: 0 20px; + border-radius: 6px; + box-shadow: 0 2px 4px rgba(255, 107, 53, 0.2); + transition: all 0.3s ease; + + &:hover:not(:disabled) { + background-color: #ff5722 !important; + border-color: #ff5722 !important; + box-shadow: 0 4px 8px rgba(255, 107, 53, 0.3); + transform: translateY(-1px); + } + + &:active:not(:disabled) { + background-color: #e64a19 !important; + border-color: #e64a19 !important; + transform: translateY(0); + box-shadow: 0 2px 4px rgba(255, 107, 53, 0.2); + } + + &:disabled { + background-color: #d9d9d9 !important; + border-color: #d9d9d9 !important; + color: #bfbfbf !important; + box-shadow: none; + } + } + + .confirmButton { + background-color: #07c160 !important; + border-color: #07c160 !important; + font-weight: 500; + height: 36px; + padding: 0 20px; + border-radius: 6px; + box-shadow: 0 2px 4px rgba(7, 193, 96, 0.2); + transition: all 0.3s ease; + + &:hover:not(:disabled) { + background-color: #06ad56 !important; + border-color: #06ad56 !important; + box-shadow: 0 4px 8px rgba(7, 193, 96, 0.3); + transform: translateY(-1px); + } + + &:active:not(:disabled) { + background-color: #059c4d !important; + border-color: #059c4d !important; + transform: translateY(0); + box-shadow: 0 2px 4px rgba(7, 193, 96, 0.2); + } + + &:disabled { + background-color: #d9d9d9 !important; + border-color: #d9d9d9 !important; + color: #bfbfbf !important; + box-shadow: none; } } } diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.tsx index 991f3a2a..ab0eefe1 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.tsx @@ -5,8 +5,7 @@ import { Button, Checkbox, Input, - Select, - Slider, + Modal, Switch, message as antdMessage, } from "antd"; @@ -15,11 +14,15 @@ import { DeleteOutlined, PlusOutlined, ReloadOutlined, + UserOutlined, + PictureOutlined, + FileOutlined, + SoundOutlined, } from "@ant-design/icons"; import type { CheckboxChangeEvent } from "antd/es/checkbox"; import styles from "./index.module.scss"; -import { ContactItem, ScriptGroup } from "../../types"; +import { ContactItem, ScriptGroup, MessageItem } from "../../types"; import InputMessage from "./InputMessage/InputMessage"; import ContentLibrarySelector from "./ContentLibrarySelector"; import type { ContentItem } from "@/components/ContentSelection/data"; @@ -36,12 +39,6 @@ interface StepSendMessageProps { targetLabel: string; messageContent: string; onMessageContentChange: (value: string) => void; - friendInterval: [number, number]; - onFriendIntervalChange: (value: [number, number]) => void; - messageInterval: [number, number]; - onMessageIntervalChange: (value: [number, number]) => void; - selectedTag: string; - onSelectedTagChange: (value: string) => void; aiRewriteEnabled: boolean; onAiRewriteToggle: (value: boolean) => void; aiPrompt: string; @@ -64,12 +61,6 @@ const StepSendMessage: React.FC = ({ targetLabel, messageContent, onMessageContentChange, - friendInterval, - onFriendIntervalChange, - messageInterval, - onMessageIntervalChange, - selectedTag, - onSelectedTagChange, aiRewriteEnabled, onAiRewriteToggle, aiPrompt, @@ -88,47 +79,111 @@ const StepSendMessage: React.FC = ({ const [savingScriptGroup, setSavingScriptGroup] = useState(false); const [aiRewriting, setAiRewriting] = useState(false); const [deletingGroupIds, setDeletingGroupIds] = useState([]); + const [aiRewriteModalVisible, setAiRewriteModalVisible] = useState(false); + const [aiRewriteModalIndex, setAiRewriteModalIndex] = useState( + null, + ); + const [aiRewriteModalPrompt, setAiRewriteModalPrompt] = useState(""); + const [aiRewritingMessage, setAiRewritingMessage] = useState(false); + const [aiRewriteResult, setAiRewriteResult] = useState(null); + + // 将 string[] 转换为 MessageItem[] + const messagesToItems = useCallback((messages: string[]): MessageItem[] => { + return messages.map(msg => { + try { + // 尝试解析为 JSON(新格式) + const parsed = JSON.parse(msg); + if (parsed && typeof parsed === "object" && "type" in parsed) { + return parsed as MessageItem; + } + } catch { + // 解析失败,作为文本消息处理 + } + // 旧格式:纯文本 + return { type: "text", content: msg }; + }); + }, []); + + // 将 MessageItem[] 转换为 string[] + const itemsToMessages = useCallback((items: MessageItem[]): string[] => { + return items.map(item => { + // 如果是纯文本消息,直接返回内容(保持向后兼容) + if (item.type === "text" && !item.fileName) { + return item.content; + } + // 其他类型序列化为 JSON + return JSON.stringify(item); + }); + }, []); + + // 内部维护的 MessageItem[] 状态 + const [messageItems, setMessageItems] = useState(() => + messagesToItems(currentScriptMessages), + ); + + // 当 currentScriptMessages 变化时,同步更新 messageItems + React.useEffect(() => { + setMessageItems(messagesToItems(currentScriptMessages)); + }, [currentScriptMessages, messagesToItems]); const handleAddMessage = useCallback( - (content?: string, showSuccess?: boolean) => { - const finalContent = (content ?? messageContent).trim(); - if (!finalContent) { - antdMessage.warning("请输入消息内容"); - return; + (content?: string | MessageItem, showSuccess?: boolean) => { + let newItem: MessageItem; + if (typeof content === "string") { + const finalContent = (content || messageContent).trim(); + if (!finalContent) { + antdMessage.warning("请输入消息内容"); + return; + } + newItem = { type: "text", content: finalContent }; + } else if (content && typeof content === "object") { + newItem = content; + } else { + const finalContent = messageContent.trim(); + if (!finalContent) { + antdMessage.warning("请输入消息内容"); + return; + } + newItem = { type: "text", content: finalContent }; } - onCurrentScriptMessagesChange([...currentScriptMessages, finalContent]); + + const newItems = [...messageItems, newItem]; + setMessageItems(newItems); + onCurrentScriptMessagesChange(itemsToMessages(newItems)); onMessageContentChange(""); if (showSuccess) { antdMessage.success("已添加消息内容"); } }, [ - currentScriptMessages, messageContent, + messageItems, onCurrentScriptMessagesChange, onMessageContentChange, + itemsToMessages, ], ); const handleRemoveMessage = useCallback( (index: number) => { - const next = currentScriptMessages.filter((_, idx) => idx !== index); - onCurrentScriptMessagesChange(next); + const next = messageItems.filter((_, idx) => idx !== index); + setMessageItems(next); + onCurrentScriptMessagesChange(itemsToMessages(next)); }, - [currentScriptMessages, onCurrentScriptMessagesChange], + [messageItems, onCurrentScriptMessagesChange, itemsToMessages], ); const handleSaveScriptGroup = useCallback(async () => { if (savingScriptGroup) { return; } - if (currentScriptMessages.length === 0) { + if (messageItems.length === 0) { antdMessage.warning("请先添加消息内容"); return; } const groupName = currentScriptName.trim() || `话术组${savedScriptGroups.length + 1}`; - const messages = [...currentScriptMessages]; + const messages = itemsToMessages(messageItems); const params: CreateContentLibraryParams = { name: groupName, sourceType: 1, @@ -155,6 +210,7 @@ const StepSendMessage: React.FC = ({ messages, }; onSavedScriptGroupsChange([...savedScriptGroups, newGroup]); + setMessageItems([]); onCurrentScriptMessagesChange([]); onCurrentScriptNameChange(""); onMessageContentChange(""); @@ -169,7 +225,7 @@ const StepSendMessage: React.FC = ({ }, [ aiPrompt, aiRewriteEnabled, - currentScriptMessages, + messageItems, currentScriptName, onCurrentScriptMessagesChange, onCurrentScriptNameChange, @@ -177,6 +233,7 @@ const StepSendMessage: React.FC = ({ onSavedScriptGroupsChange, savedScriptGroups, savingScriptGroup, + itemsToMessages, ]); const handleAiRewrite = useCallback(async () => { @@ -272,6 +329,138 @@ const StepSendMessage: React.FC = ({ onMessageContentChange, ]); + const handleOpenAiRewriteModal = useCallback((index: number) => { + setAiRewriteModalIndex(index); + setAiRewriteModalPrompt(""); + setAiRewriteModalVisible(true); + }, []); + + const handleCloseAiRewriteModal = useCallback(() => { + setAiRewriteModalVisible(false); + setAiRewriteModalIndex(null); + setAiRewriteModalPrompt(""); + setAiRewriteResult(null); + }, []); + + // 执行 AI 改写,获取结果但不立即应用 + const handleAiRewriteExecute = useCallback(async () => { + if (aiRewriteModalIndex === null) { + return; + } + const trimmedPrompt = aiRewriteModalPrompt.trim(); + if (!trimmedPrompt) { + antdMessage.warning("请输入改写提示词"); + return; + } + const messageToRewrite = messageItems[aiRewriteModalIndex]; + if (!messageToRewrite) { + antdMessage.error("消息不存在"); + return; + } + // AI改写只支持文本消息 + if (messageToRewrite.type !== "text") { + antdMessage.warning("AI改写仅支持文本消息"); + return; + } + if (aiRewritingMessage) { + return; + } + try { + setAiRewritingMessage(true); + const response = await aiEditContent({ + aiPrompt: trimmedPrompt, + content: messageToRewrite.content, + }); + const normalizedResponse = response as { + content?: string; + contentAfter?: string; + contentFront?: string; + data?: + | string + | { + content?: string; + contentAfter?: string; + contentFront?: string; + }; + result?: string; + }; + const dataField = normalizedResponse?.data; + const dataContent = + typeof dataField === "string" + ? dataField + : (dataField?.content ?? undefined); + const dataContentAfter = + typeof dataField === "string" ? undefined : dataField?.contentAfter; + const dataContentFront = + typeof dataField === "string" ? undefined : dataField?.contentFront; + + const primaryAfter = + normalizedResponse?.contentAfter ?? dataContentAfter ?? undefined; + const primaryFront = + normalizedResponse?.contentFront ?? dataContentFront ?? undefined; + + let rewrittenContent = ""; + if (typeof response === "string") { + rewrittenContent = response; + } else if (primaryAfter) { + rewrittenContent = primaryFront + ? `${primaryFront}\n${primaryAfter}` + : primaryAfter; + } else if (typeof normalizedResponse?.content === "string") { + rewrittenContent = normalizedResponse.content; + } else if (typeof dataContent === "string") { + rewrittenContent = dataContent; + } else if (typeof normalizedResponse?.result === "string") { + rewrittenContent = normalizedResponse.result; + } else if (primaryFront) { + rewrittenContent = primaryFront; + } + if (!rewrittenContent || typeof rewrittenContent !== "string") { + antdMessage.error("AI改写失败,请稍后重试"); + return; + } + setAiRewriteResult(rewrittenContent.trim()); + } catch (error) { + console.error("AI改写失败:", error); + antdMessage.error("AI改写失败,请稍后重试"); + } finally { + setAiRewritingMessage(false); + } + }, [ + aiRewriteModalIndex, + aiRewriteModalPrompt, + messageItems, + aiRewritingMessage, + ]); + + // 确认并应用 AI 改写结果 + const handleConfirmAiRewrite = useCallback(() => { + if (aiRewriteModalIndex === null || !aiRewriteResult) { + return; + } + const messageToRewrite = messageItems[aiRewriteModalIndex]; + if (!messageToRewrite) { + antdMessage.error("消息不存在"); + return; + } + const newItems = [...messageItems]; + newItems[aiRewriteModalIndex] = { + ...messageToRewrite, + content: aiRewriteResult, + }; + setMessageItems(newItems); + onCurrentScriptMessagesChange(itemsToMessages(newItems)); + handleCloseAiRewriteModal(); + antdMessage.success("AI改写完成"); + }, [ + aiRewriteModalIndex, + aiRewriteResult, + messageItems, + onCurrentScriptMessagesChange, + itemsToMessages, + handleCloseAiRewriteModal, + ]); + const handleApplyGroup = useCallback( (group: ScriptGroup) => { onCurrentScriptMessagesChange(group.messages); @@ -352,61 +541,167 @@ const StepSendMessage: React.FC = ({
+ {/* 1. 模拟推送内容 */}
模拟推送内容
+
+ + {/* 2. 消息列表 */} +
+ {messageItems.length === 0 ? ( +
+ 开始添加消息内容... +
+ ) : ( +
+ {messageItems.map((msgItem, index) => ( +
+
+
+ +
+
+
+ {msgItem.type === "text" && ( +
+ {msgItem.content} +
+ )} + {msgItem.type === "image" && ( +
+
+ +
+ {msgItem.fileName { + const target = e.target as HTMLImageElement; + target.style.display = "none"; + }} + /> + {msgItem.fileName && ( +
+ {msgItem.fileName} +
+ )} +
+ )} + {msgItem.type === "file" && ( +
+
+ +
+
+
+ {msgItem.fileName || "文件"} +
+ {msgItem.fileSize && ( +
+ {msgItem.fileSize >= 1024 * 1024 + ? `${(msgItem.fileSize / 1024 / 1024).toFixed(2)} MB` + : `${(msgItem.fileSize / 1024).toFixed(2)} KB`} +
+ )} +
+
+ )} + {msgItem.type === "audio" && ( +
+
+ +
+
+
+ {msgItem.fileName || "语音消息"} +
+ {msgItem.durationMs && ( +
+ {Math.floor(msgItem.durationMs / 1000)}秒 +
+ )} +
+
+ )} +
+
+ {msgItem.type === "text" && ( + + )} +
+
+
+
+ ))} +
+ )} +
+ + {/* 3. 消息输入组件 */} +
+ handleAddMessage(value)} + onAddMessage={message => handleAddMessage(message)} + clearOnSend + placeholder="请输入内容" + hint={`按ENTER发送,按住CTRL+ENTER换行,已配置${savedScriptGroups.length}个话术组,已选择${selectedScriptGroupIds.length}个进行推送,已选${selectedContentLibraries.length}个内容库`} + /> +
+ + {/* 4. 话术组标题 */} +
+ onCurrentScriptNameChange(event.target.value)} + /> +
+ + {/* 5. 创建话术组按钮 */} +
-
-
-
当前编辑话术
- {currentScriptMessages.length === 0 ? ( -
- 开始添加消息内容... -
- ) : ( -
- {currentScriptMessages.map((msg, index) => ( -
-
{msg}
-
- ))} -
- )} -
-
- - onCurrentScriptNameChange(event.target.value) - } - /> -
-
+
-
- {/* 内容库选择组件 */} +
+
+
推送内容
+
+
已保存话术组 ({savedScriptGroups.length}) @@ -460,128 +755,114 @@ const StepSendMessage: React.FC = ({ )}
- -
- handleAddMessage(value)} - clearOnSend - placeholder="请输入内容" - hint={`按住CTRL+ENTER换行,已配置${savedScriptGroups.length}个话术组,已选择${selectedScriptGroupIds.length}个进行推送,已选${selectedContentLibraries.length}个内容库`} - /> -
-
- -
AI智能话术改写
-
- {aiRewriteEnabled && ( - onAiPromptChange(event.target.value)} - className={styles.aiRewriteInput} - /> - )} -
-
- -
- - -
-
-
-
- -
-
-
相关设置
-
-
好友间间隔
-
- 间隔时间(秒) - - onFriendIntervalChange(value as [number, number]) - } - style={{ flex: 1, margin: "0 16px" }} - /> - - {friendInterval[0]} - {friendInterval[1]} - -
-
-
-
消息间间隔
-
- 间隔时间(秒) - - onMessageIntervalChange(value as [number, number]) - } - style={{ flex: 1, margin: "0 16px" }} - /> - - {messageInterval[0]} - {messageInterval[1]} - -
-
-
- -
-
完成打标签
- -
- -
-
推送预览
-
    -
  • 推送账号: {selectedAccounts.length}个
  • -
  • - 推送{targetLabel}: {selectedContacts.length}个 -
  • -
  • 话术组数: {savedScriptGroups.length}个
  • -
  • 随机推送: 否
  • -
  • 预计耗时: ~1分钟
  • -
-
+ + {/* AI改写弹窗 */} + + + AI智能改写 +
+ } + open={aiRewriteModalVisible} + onCancel={handleCloseAiRewriteModal} + width={680} + footer={[ + , + , + , + ]} + className={styles.aiRewriteModal} + wrapClassName={styles.aiRewriteModalWrap} + > +
+ {/* 原文和结果对比区域 */} +
+ {/* 原消息内容区域 */} + {aiRewriteModalIndex !== null && ( +
+
+ 📝 + + 1原消息内容 + +
+
+ {messageItems[aiRewriteModalIndex]?.type === "text" + ? messageItems[aiRewriteModalIndex].content + : "非文本消息不支持AI改写"} +
+
+ )} + + {/* Loading 状态 */} + {aiRewritingMessage && ( +
+
+
+ AI正在改写中,请稍候... +
+
+ )} + + {/* 分隔线 */} + {aiRewriteModalIndex !== null && aiRewriteResult && ( +
+ )} + + {/* 改写结果区域 */} + {aiRewriteResult && ( +
+
+ + 改写结果 +
+
+ {aiRewriteResult} +
+
+ )} +
+ + {/* 提示词输入区域 - 放在最下面 */} +
+
+ 💡 + 改写提示词 +
+ setAiRewriteModalPrompt(event.target.value)} + rows={3} + autoFocus + disabled={aiRewritingMessage} + className={styles.aiRewriteModalTextArea} + /> +
+
+
); }; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.tsx index 5ee5b4bd..6789c6a6 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/index.tsx @@ -14,7 +14,13 @@ import { getCustomerList } from "@/pages/pc/ckbox/weChat/api"; import StepSelectAccount from "./components/StepSelectAccount"; import StepSelectContacts from "./components/StepSelectContacts"; import StepSendMessage from "./components/StepSendMessage"; -import { ContactItem, PushType, ScriptGroup } from "./types"; +import StepPushParams from "./components/StepPushParams"; +import { + ContactItem, + PushType, + ScriptGroup, + CreatePushTaskPayload, +} from "./types"; import StepIndicator from "@/components/StepIndicator"; import type { ContentItem } from "@/components/ContentSelection/data"; import type { PoolSelectionItem } from "@/components/PoolSelection/data"; @@ -163,6 +169,20 @@ const CreatePushTask: React.FC = () => { return; } setCurrentStep(3); + return; + } + + if (currentStep === 3) { + // 验证推送内容 + if ( + currentScriptMessages.length === 0 && + selectedScriptGroupIds.length === 0 && + selectedContentLibraries.length === 0 + ) { + message.warning("请至少添加一条消息、选择一个话术组或内容库"); + return; + } + setCurrentStep(4); } }; @@ -184,9 +204,12 @@ const CreatePushTask: React.FC = () => { if (creatingTask) { return; } + + // ========== 1. 数据验证和准备 ========== const selectedGroups = savedScriptGroups.filter(group => selectedScriptGroupIds.includes(group.id), ); + if ( currentScriptMessages.length === 0 && selectedGroups.length === 0 && @@ -195,19 +218,27 @@ const CreatePushTask: React.FC = () => { message.warning("请添加话术内容、选择话术组或内容库"); return; } + + // 手动消息处理 const manualMessages = currentScriptMessages .map(item => item.trim()) .filter(Boolean); + if (validPushType === "group-announcement" && manualMessages.length === 0) { message.warning("请先填写公告内容"); return; } - const toNumberId = (value: unknown) => { + + // ID 转换工具函数 + const toNumberId = (value: unknown): number | null => { + if (value === null || value === undefined) return null; const numeric = Number(value); return Number.isFinite(numeric) && !Number.isNaN(numeric) ? numeric : null; }; + + // ========== 2. 内容库ID处理 ========== const contentGroupIds = Array.from( new Set( [ @@ -220,6 +251,7 @@ const CreatePushTask: React.FC = () => { ].filter((id): id is number => id !== null), ), ); + if ( manualMessages.length === 0 && selectedGroups.length === 0 && @@ -228,6 +260,8 @@ const CreatePushTask: React.FC = () => { message.warning("缺少有效的话术内容,请重新检查"); return; } + + // ========== 3. 账号ID处理 ========== const ownerWechatIds = Array.from( new Set( selectedAccounts @@ -235,47 +269,25 @@ const CreatePushTask: React.FC = () => { .filter((id): id is number => id !== null), ), ); + if (ownerWechatIds.length === 0) { message.error("缺少有效的推送账号信息"); return; } + + // ========== 4. 联系人ID处理 ========== const selectedContactIds = Array.from( new Set( selectedContacts.map(contact => contact?.id).filter(isValidNumber), ), ); + if (selectedContactIds.length === 0) { message.error("缺少有效的推送对象"); return; } - const friendIntervalMin = friendInterval[0]; - const friendIntervalMax = friendInterval[1]; - const messageIntervalMin = messageInterval[0]; - const messageIntervalMax = messageInterval[1]; - const trafficPoolIds = selectedTrafficPools - .map(pool => pool.id) - .filter( - id => id !== undefined && id !== null && String(id).trim() !== "", - ); - const { startTime, endTime } = DEFAULT_TIME_RANGE[validPushType]; - const maxPerDay = - selectedContacts.length > 0 - ? selectedContacts.length - : DEFAULT_MAX_PER_DAY[validPushType]; - const pushOrder = DEFAULT_PUSH_ORDER[validPushType]; - const normalizedPostPushTags = - selectedTag.trim().length > 0 - ? [ - toNumberId(selectedTag) !== null - ? (toNumberId(selectedTag) as number) - : selectedTag, - ] - : []; - const taskName = - currentScriptName.trim() || - selectedGroups[0]?.name || - (manualMessages[0] ? manualMessages[0].slice(0, 20) : "") || - `推送任务-${Date.now()}`; + + // ========== 5. 设备分组ID处理(好友推送必填) ========== const deviceGroupIds = Array.from( new Set( selectedAccounts @@ -283,84 +295,191 @@ const CreatePushTask: React.FC = () => { .filter((id): id is number => id !== null), ), ); + if (validPushType === "friend-message" && deviceGroupIds.length === 0) { message.error("缺少有效的推送设备分组"); return; } - const basePayload: Record = { - name: taskName, - type: 3, - autoStart: DEFAULT_AUTO_START[validPushType], - status: 1, - pushType: DEFAULT_PUSH_TYPE[validPushType], + // ========== 6. 流量池ID处理 ========== + const trafficPoolIds = selectedTrafficPools + .map(pool => { + const id = pool.id; + if (id === undefined || id === null) return null; + const strId = String(id).trim(); + return strId !== "" ? strId : null; + }) + .filter((id): id is string => id !== null); + + // ========== 7. 时间范围 ========== + const { startTime, endTime } = DEFAULT_TIME_RANGE[validPushType]; + + // ========== 8. 每日最大推送数 ========== + const maxPerDay = + selectedContacts.length > 0 + ? selectedContacts.length + : DEFAULT_MAX_PER_DAY[validPushType]; + + // ========== 9. 推送顺序 ========== + const pushOrder = DEFAULT_PUSH_ORDER[validPushType]; + + // ========== 10. 推送后标签处理 ========== + const postPushTags = + selectedTag.trim().length > 0 + ? (() => { + const tagId = toNumberId(selectedTag); + return tagId !== null ? [tagId] : []; + })() + : []; + + // ========== 11. 任务名称 ========== + const taskName = + currentScriptName.trim() || + selectedGroups[0]?.name || + (manualMessages[0] ? manualMessages[0].slice(0, 20) : "") || + `推送任务-${Date.now()}`; + + // ========== 12. 构建基础载荷 ========== + const basePayload: CreatePushTaskPayload = { + name: String(taskName).trim(), + type: 3, // 固定值:工作台类型 + autoStart: DEFAULT_AUTO_START[validPushType] ? 1 : 0, + status: 1, // 固定值:启用 + pushType: DEFAULT_PUSH_TYPE[validPushType] ? 1 : 0, targetType: validPushType === "friend-message" ? 2 : 1, groupPushSubType: validPushType === "group-announcement" ? 2 : 1, - startTime, - endTime, - maxPerDay, - pushOrder, - friendIntervalMin, - friendIntervalMax, - messageIntervalMin, - messageIntervalMax, + startTime: String(startTime), + endTime: String(endTime), + maxPerDay: Number(maxPerDay), + pushOrder: Number(pushOrder), + friendIntervalMin: Number(friendInterval[0]), + friendIntervalMax: Number(friendInterval[1]), + messageIntervalMin: Number(messageInterval[0]), + messageIntervalMax: Number(messageInterval[1]), isRandomTemplate: selectedScriptGroupIds.length > 1 ? 1 : 0, - contentGroups: contentGroupIds, - postPushTags: normalizedPostPushTags, - ownerWechatIds, - enableAiRewrite: aiRewriteEnabled ? 1 : 0, + contentGroups: contentGroupIds.length > 0 ? contentGroupIds : [], + postPushTags: postPushTags, + ownerWechatIds: ownerWechatIds, }; - if (trafficPoolIds.length > 0) { - basePayload.trafficPools = trafficPoolIds; - } - if (validPushType === "friend-message") { - basePayload.isLoop = 0; - basePayload.deviceGroups = deviceGroupIds; - } - if (manualMessages.length > 0) { - basePayload.manualMessages = manualMessages; - if (currentScriptName.trim()) { - basePayload.manualScriptName = currentScriptName.trim(); - } - } - if (selectedScriptGroupIds.length > 0) { - basePayload.selectedScriptGroupIds = selectedScriptGroupIds; - } - if (aiRewriteEnabled && aiPrompt.trim()) { - basePayload.aiRewritePrompt = aiPrompt.trim(); - } - if (selectedGroups.length > 0) { - basePayload.scriptGroups = selectedGroups.map(group => ({ - id: group.id, - name: group.name, - messages: group.messages, - })); - } + + // ========== 13. 根据推送类型添加特定字段 ========== if (validPushType === "friend-message") { + // 好友推送特有字段 + // 注意:wechatFriends 必须是字符串数组,不是数字数组 basePayload.wechatFriends = Array.from( new Set( selectedContacts - .map(contact => toNumberId(contact?.id)) - .filter((id): id is number => id !== null), + .map(contact => { + const id = toNumberId(contact?.id); + return id !== null ? String(id) : null; + }) + .filter((id): id is string => id !== null), ), ); - basePayload.targetType = 2; + basePayload.deviceGroups = deviceGroupIds; // 必填,数字数组 + basePayload.isLoop = 0; // 固定值 + basePayload.targetType = 2; // 确保是好友类型 + basePayload.groupPushSubType = 1; // 固定为群群发 } else { + // 群推送特有字段 const groupIds = Array.from( new Set( selectedContacts - .map(contact => toNumberId(contact.groupId ?? contact.id)) + .map(contact => { + // 优先使用 groupId,如果没有则使用 id + const id = contact.groupId ?? contact.id; + return toNumberId(id); + }) .filter((id): id is number => id !== null), ), ); - basePayload.wechatGroups = groupIds; + + basePayload.wechatGroups = groupIds; // 数字数组 + basePayload.targetType = 1; // 群类型 basePayload.groupPushSubType = validPushType === "group-announcement" ? 2 : 1; - basePayload.targetType = 1; + + // 群公告特有字段 if (validPushType === "group-announcement") { basePayload.announcementContent = manualMessages.join("\n"); } } + + // ========== 14. 可选字段处理 ========== + // 流量池(如果存在) + if (trafficPoolIds.length > 0) { + basePayload.trafficPools = trafficPoolIds; // 字符串数组 + } + + // 手动消息(如果存在) + if (manualMessages.length > 0) { + basePayload.manualMessages = manualMessages; + if (currentScriptName.trim()) { + basePayload.manualScriptName = String(currentScriptName.trim()); + } + } + + // 选中的话术组ID(如果存在) + if (selectedScriptGroupIds.length > 0) { + basePayload.selectedScriptGroupIds = selectedScriptGroupIds.map(id => + String(id), + ); + } + + // AI改写相关(如果启用) + if (aiRewriteEnabled) { + basePayload.enableAiRewrite = 1; + if (aiPrompt.trim()) { + basePayload.aiRewritePrompt = String(aiPrompt.trim()); + } + } else { + basePayload.enableAiRewrite = 0; + } + + // 话术组对象(如果存在) + if (selectedGroups.length > 0) { + basePayload.scriptGroups = selectedGroups.map(group => ({ + id: String(group.id), + name: String(group.name || ""), + messages: Array.isArray(group.messages) + ? group.messages.map(msg => String(msg)) + : [], + })); + } + + // ========== 15. 数据验证和提交 ========== + // 最终验证:确保必填字段存在 + if (validPushType === "friend-message") { + if ( + !Array.isArray(basePayload.deviceGroups) || + basePayload.deviceGroups.length === 0 + ) { + message.error("好友推送必须选择设备分组"); + return; + } + if ( + !Array.isArray(basePayload.wechatFriends) || + basePayload.wechatFriends.length === 0 + ) { + message.error("好友推送必须选择好友"); + return; + } + } else { + if ( + !Array.isArray(basePayload.wechatGroups) || + basePayload.wechatGroups.length === 0 + ) { + message.error("群推送必须选择群"); + return; + } + } + + // 提交前打印日志(开发环境) + if (process.env.NODE_ENV === "development") { + console.log("提交数据:", JSON.stringify(basePayload, null, 2)); + } + + // ========== 16. 提交请求 ========== let hideLoading: ReturnType | undefined; try { setCreatingTask(true); @@ -386,7 +505,7 @@ const CreatePushTask: React.FC = () => { -
+
{ onBackClick={handleClose} />
- +
+ +
} footer={ @@ -434,6 +560,12 @@ const CreatePushTask: React.FC = () => { {selectedContacts.length}个 )} + {currentStep === 4 && ( + + 推送账号: {selectedAccounts.length}个, 推送{step2Title}:{" "} + {selectedContacts.length}个 + + )}
{currentStep === 1 && ( @@ -458,6 +590,14 @@ const CreatePushTask: React.FC = () => { )} {currentStep === 3 && ( + <> + + + + )} + {currentStep === 4 && ( <>
diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/types.ts b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/types.ts index bd5eb0f1..f51d5ed4 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/types.ts +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/types.ts @@ -20,8 +20,67 @@ export interface ContactItem { extendFields?: Record; } +// 消息类型定义 +export type MessageType = "text" | "image" | "file" | "audio"; + +export interface MessageItem { + type: MessageType; + content: string; // 文本内容或文件URL + // 文件相关字段 + fileName?: string; // 文件名 + fileSize?: number; // 文件大小(字节) + // 语音相关字段 + durationMs?: number; // 语音时长(毫秒) +} + export interface ScriptGroup { id: string; name: string; - messages: string[]; + messages: string[]; // 保持向后兼容,但实际应该使用 MessageItem[] +} + +// 接口请求载荷类型定义 +export interface CreatePushTaskPayload { + // 基础字段 + name: string; + type: 3; // 固定值:工作台类型 + autoStart: 0 | 1; + status: 1; // 固定值:启用 + pushType: 0 | 1; // 0=定时,1=立即 + targetType: 1 | 2; // 1=群推送,2=好友推送 + groupPushSubType: 1 | 2; // 1=群群发,2=群公告 + startTime: string; // "HH:mm" 格式 + endTime: string; // "HH:mm" 格式 + maxPerDay: number; + pushOrder: 1 | 2; // 1=最早优先,2=最新优先 + friendIntervalMin: number; + friendIntervalMax: number; + messageIntervalMin: number; + messageIntervalMax: number; + isRandomTemplate: 0 | 1; + contentGroups: number[]; // 内容库ID数组 + postPushTags: number[]; // 推送后标签ID数组 + ownerWechatIds: number[]; // 客服ID数组 + + // 好友推送特有字段 + wechatFriends?: string[]; // 好友ID列表(字符串数组) + deviceGroups?: number[]; // 设备分组ID数组(好友推送时必填) + isLoop?: 0; // 固定值(好友推送时) + + // 群推送特有字段 + wechatGroups?: number[]; // 微信群ID数组 + announcementContent?: string; // 群公告内容(群公告时必填) + + // 可选字段 + trafficPools?: string[]; // 流量池ID数组(字符串数组) + manualMessages?: string[]; // 手动消息数组 + manualScriptName?: string; // 手动话术名称 + selectedScriptGroupIds?: string[]; // 选中的话术组ID数组 + enableAiRewrite?: 0 | 1; // 是否启用AI改写 + aiRewritePrompt?: string; // AI改写提示词 + scriptGroups?: Array<{ + id: string; + name: string; + messages: string[]; + }>; // 话术组对象数组 } From 0e56f0808cbf84ba0817642cc487467ff5e94122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Thu, 20 Nov 2025 14:57:18 +0800 Subject: [PATCH 08/31] =?UTF-8?q?=E5=9C=A8NavCommon=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E4=B8=AD=E5=AE=9E=E7=8E=B0=E7=BC=93=E5=AD=98=E6=B8=85=E9=99=A4?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=8C=85=E6=8B=AClocalStorage?= =?UTF-8?q?=E3=80=81sessionStorage=E5=92=8CIndexedDB=E3=80=82=E5=9C=A8?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E6=B8=85=E9=99=A4=E6=9C=9F=E9=97=B4=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=8A=A0=E8=BD=BD=E6=B6=88=E6=81=AF=E5=B9=B6=E7=9B=B8?= =?UTF-8?q?=E5=BA=94=E5=9C=B0=E6=9B=B4=E6=96=B0=E8=8F=9C=E5=8D=95=E9=80=89?= =?UTF-8?q?=E9=A1=B9=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pc/ckbox/components/NavCommon/index.tsx | 109 +++++++++++++++++- 1 file changed, 103 insertions(+), 6 deletions(-) diff --git a/Touchkebao/src/pages/pc/ckbox/components/NavCommon/index.tsx b/Touchkebao/src/pages/pc/ckbox/components/NavCommon/index.tsx index 4e3fabcd..52d9e5df 100644 --- a/Touchkebao/src/pages/pc/ckbox/components/NavCommon/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/components/NavCommon/index.tsx @@ -8,6 +8,7 @@ import { Badge, Dropdown, Empty, + message, } from "antd"; import { BarChartOutlined, @@ -17,7 +18,7 @@ import { ThunderboltOutlined, SettingOutlined, CalendarOutlined, - RetweetOutlined, + ClearOutlined, } from "@ant-design/icons"; import { noticeList, readMessage, readAll } from "./api"; import { useUserStore } from "@/store/module/user"; @@ -54,6 +55,7 @@ const NavCommon: React.FC = ({ title = "触客宝" }) => { const [messageList, setMessageList] = useState([]); const [messageCount, setMessageCount] = useState(0); const [loading, setLoading] = useState(false); + const [clearingCache, setClearingCache] = useState(false); const navigate = useNavigate(); const location = useLocation(); const { user, logout } = useUserStore(); @@ -72,11 +74,6 @@ const NavCommon: React.FC = ({ title = "触客宝" }) => { navigate("/pc/weChat"); } }; - - const isWeChat = () => { - return location.pathname.startsWith("/pc/weChat"); - }; - // 定时器获取消息条数 const IntervalMessageCount = async () => { try { @@ -125,6 +122,99 @@ const NavCommon: React.FC = ({ title = "触客宝" }) => { navigate("/login"); // 跳转到登录页面 }; + // 清除所有 IndexedDB 数据库 + const clearAllIndexedDB = async (): Promise => { + return new Promise((resolve, reject) => { + if (!window.indexedDB) { + resolve(); + return; + } + + // 获取所有数据库名称 + const databases: string[] = []; + const request = indexedDB.databases(); + + request + .then(dbs => { + dbs.forEach(db => { + if (db.name) { + databases.push(db.name); + } + }); + + // 删除所有数据库 + const deletePromises = databases.map(dbName => { + return new Promise((resolveDelete, rejectDelete) => { + const deleteRequest = indexedDB.deleteDatabase(dbName); + deleteRequest.onsuccess = () => { + resolveDelete(); + }; + deleteRequest.onerror = () => { + rejectDelete(new Error(`删除数据库 ${dbName} 失败`)); + }; + deleteRequest.onblocked = () => { + // 如果数据库被阻塞,等待一下再重试 + setTimeout(() => { + const retryRequest = indexedDB.deleteDatabase(dbName); + retryRequest.onsuccess = () => resolveDelete(); + retryRequest.onerror = () => + rejectDelete(new Error(`删除数据库 ${dbName} 失败`)); + }, 100); + }; + }); + }); + + Promise.all(deletePromises) + .then(() => resolve()) + .catch(reject); + }) + .catch(reject); + }); + }; + + // 处理清除缓存 + const handleClearCache = async () => { + try { + setClearingCache(true); + const hideLoading = message.loading("正在清除缓存...", 0); + + // 1. 清除所有 localStorage + try { + localStorage.clear(); + } catch (error) { + console.warn("清除 localStorage 失败:", error); + } + + // 2. 清除所有 sessionStorage + try { + sessionStorage.clear(); + } catch (error) { + console.warn("清除 sessionStorage 失败:", error); + } + + // 3. 清除所有 IndexedDB 数据库 + try { + await clearAllIndexedDB(); + } catch (error) { + console.warn("清除 IndexedDB 失败:", error); + } + + hideLoading(); + message.success("缓存清除成功"); + + // 清除成功后跳转到登录页面 + setTimeout(() => { + logout(); + navigate("/login"); + }, 500); + } catch (error) { + console.error("清除缓存失败:", error); + message.error("清除缓存失败,请稍后重试"); + } finally { + setClearingCache(false); + } + }; + // 处理消息已读 const handleReadMessage = async (messageId: number) => { try { @@ -199,6 +289,13 @@ const NavCommon: React.FC = ({ title = "触客宝" }) => { navigate("/pc/commonConfig"); }, }, + { + key: "clearCache", + icon: , + label: clearingCache ? "清除缓存中..." : "清除缓存", + onClick: handleClearCache, + disabled: clearingCache, + }, { key: "logout", icon: , From 16a8656b490dae559104dcd8f878a455d4ca89b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Thu, 20 Nov 2025 15:52:05 +0800 Subject: [PATCH 09/31] =?UTF-8?q?=E4=BB=8E=E6=B6=88=E6=81=AF=E6=8E=A8?= =?UTF-8?q?=E9=80=81=E5=8A=A9=E6=89=8B=E4=BB=BB=E5=8A=A1=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E4=B8=AD=E5=88=A0=E9=99=A4=E5=BC=83=E7=94=A8=E7=9A=84=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E7=A4=BA=E4=BE=8B=EF=BC=8C=E6=9B=B4=E6=96=B0=E6=8E=A8?= =?UTF-8?q?=E9=80=81=E5=8E=86=E5=8F=B2API=E4=BB=A5=E4=BD=BF=E7=94=A8pushTy?= =?UTF-8?q?peCode=E5=92=8C=E7=8A=B6=E6=80=81=EF=BC=8C=E5=B9=B6=E9=80=9A?= =?UTF-8?q?=E8=BF=87=E6=94=B9=E8=BF=9B=E7=9A=84=E7=B1=BB=E5=9E=8B=E5=A4=84?= =?UTF-8?q?=E7=90=86=E5=92=8CUI=E8=B0=83=E6=95=B4=E6=9D=A5=E5=A2=9E?= =?UTF-8?q?=E5=BC=BAPushHistory=E7=BB=84=E4=BB=B6=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StepSendMessage/index.module.scss | 1 + .../components/StepSendMessage/index.tsx | 4 - .../create-push-task/提示词.txt | 79 --------- .../pc/ckbox/powerCenter/push-history/api.ts | 33 +++- .../ckbox/powerCenter/push-history/index.tsx | 159 +++++++++++++----- 5 files changed, 149 insertions(+), 127 deletions(-) delete mode 100644 Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/提示词.txt diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.module.scss b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.module.scss index f7c7d1a4..0896f840 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.module.scss +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.module.scss @@ -32,6 +32,7 @@ .rightColumn { flex: 1; + max-width: 500px; display: flex; flex-direction: column; gap: 20px; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.tsx index ab0eefe1..82f109e7 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.tsx @@ -746,10 +746,6 @@ const StepSendMessage: React.FC = ({ />
-
- {group.messages[0]} - {group.messages.length > 1 && " ..."} -
)) )} diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/提示词.txt b/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/提示词.txt deleted file mode 100644 index df1db7a1..00000000 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/提示词.txt +++ /dev/null @@ -1,79 +0,0 @@ -帮我对接数据,以下是传参实例,三种模式都是同一界面的。 - -群发助手传参实例 -{ - "name": "群群发-新品宣传", // 任务名称 - "type": 3, // 工作台类型:3=群消息推送 - "autoStart": 1, // 保存后自动启动 - "status": 1, // 是否启用 - "pushType": 0, // 推送方式:0=定时,1=立即 - "targetType": 1, // 目标类型:1=群推送 - "groupPushSubType": 1, // 群推送子类型:1=群群发,2=群公告 - "startTime": "09:00", // 推送起始时间 - "endTime": "20:00", // 推送结束时间 - "maxPerDay": 200, // 每日最大推送群数 - "pushOrder": 1, // 推送顺序:1=最早优先,2=最新优先 - "wechatGroups": [102, 205, 318], // 选择的微信群 ID 列表 - "contentGroups": [11, 12], // 关联内容库 ID 列表 - "friendIntervalMin": 10, // 群间最小间隔(秒) - "friendIntervalMax": 25, // 群间最大间隔(秒) - "messageIntervalMin": 2, // 同一群消息间最小间隔(秒) - "messageIntervalMax": 6, // 同一群消息间最大间隔(秒) - "isRandomTemplate": 1, // 是否随机选择话术模板 - "postPushTags": [301, 302], // 推送完成后打的标签 - ownerWechatIds:[123123,1231231] //客服id -} - -//群公告传参实例 -{ - "name": "群公告-双11活动", // 任务名称 - "type": 3, // 群消息推送 - "autoStart": 0, // 不自动启动 - "status": 1, // 启用 - "pushType": 1, // 立即推送 - "targetType": 1, // 群推送 - "groupPushSubType": 2, // 群公告 - "startTime": "08:30", // 开始时间 - "endTime": "18:30", // 结束时间 - "maxPerDay": 80, // 每日最大公告数 - "pushOrder": 2, // 最新优先 - "wechatGroups": [5021, 5026], // 公告目标群 - "announcementContent": "…", // 公告正文 - "enableAiRewrite": 1, // 启用 AI 改写 - "aiRewritePrompt": "保持活泼口吻…", // AI 改写提示词 - "contentGroups": [21], // 关联内容库 - "friendIntervalMin": 15, // 群间最小间隔 - "friendIntervalMax": 30, // 群间最大间隔 - "messageIntervalMin": 3, // 消息间最小间隔 - "messageIntervalMax": 9, // 消息间最大间隔 - "isRandomTemplate": 0, // 不随机模板 - "postPushTags": [], // 推送后标签 - ownerWechatIds:[123123,1231231] //客服id -} - -//好友传参实例 -{ - "name": "好友私聊-新客转化", // 任务名称 - "type": 3, // 群消息推送 - "autoStart": 1, // 自动启动 - "status": 1, // 启用 - "pushType": 0, // 定时推送 - "targetType": 2, // 目标类型:2=好友推送 - "groupPushSubType": 1, // 固定为群群发(好友推送不支持公告) - "startTime": "10:00", // 开始时间 - "endTime": "22:00", // 结束时间 - "maxPerDay": 150, // 每日最大推送好友数 - "pushOrder": 1, // 最早优先 - "wechatFriends": ["12312"], // 指定好友列表(可为空数组) - "deviceGroups": [9001, 9002], // 必选:推送设备分组 ID - "contentGroups": [41, 42], // 话术内容库 - "friendIntervalMin": 12, // 好友间最小间隔 - "friendIntervalMax": 28, // 好友间最大间隔 - "messageIntervalMin": 4, // 消息间最小间隔 - "messageIntervalMax": 10, // 消息间最大间隔 - "isRandomTemplate": 1, // 随机话术 - "postPushTags": [501], // 推送后标签 - ownerWechatIds:[123123,1231231] //客服id -} - -请求接口是 queryWorkbenchCreate diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/api.ts b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/api.ts index a3627033..9b4a4abe 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/api.ts +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/api.ts @@ -6,8 +6,10 @@ export interface GetPushHistoryParams { page?: number; pageSize?: number; keyword?: string; - pushType?: string; - status?: string; + pushTypeCode?: string; // 推送类型代码:friend, group, announcement + status?: string; // 状态:pending, completed, failed + workbenchId?: string; + [property: string]: any; } // 获取推送历史接口响应 @@ -27,11 +29,30 @@ export interface GetPushHistoryResponse { */ export interface GetGroupPushHistoryParams { keyword?: string; - limit: string; - page: string; + limit?: string | number; + page?: string | number; + pageSize?: string | number; + pushTypeCode?: string; + status?: string; workbenchId?: string; [property: string]: any; } -export const getPushHistory = async (params: GetGroupPushHistoryParams) => { - return request("/v1/workbench/group-push-history", params, "GET"); + +export const getPushHistory = async ( + params: GetGroupPushHistoryParams, +): Promise => { + // 转换参数格式,确保 limit 和 page 是字符串 + const requestParams: Record = { + ...params, + }; + + if (params.page !== undefined) { + requestParams.page = String(params.page); + } + + if (params.pageSize !== undefined) { + requestParams.limit = String(params.pageSize); + } + + return request("/v1/workbench/group-push-history", requestParams, "GET"); }; diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx index 1236b5a8..8b71fa3c 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/push-history/index.tsx @@ -15,30 +15,33 @@ import styles from "./index.module.scss"; const { Option } = Select; -// 推送类型枚举 -export enum PushType { - FRIEND_MESSAGE = "friend-message", // 好友消息 - GROUP_MESSAGE = "group-message", // 群消息 - GROUP_ANNOUNCEMENT = "group-announcement", // 群公告 +// 推送类型代码枚举 +export enum PushTypeCode { + FRIEND = "friend", // 好友消息 + GROUP = "group", // 群消息 + ANNOUNCEMENT = "announcement", // 群公告 } // 推送状态枚举 export enum PushStatus { + PENDING = "pending", // 进行中 COMPLETED = "completed", // 已完成 - IN_PROGRESS = "in-progress", // 进行中 FAILED = "failed", // 失败 } // 推送历史记录接口 export interface PushHistoryRecord { - id: string; - pushType: PushType; - pushContent: string; + workbenchId: number; + taskName: string; + pushType: string; // 推送类型中文名称,如 "好友消息" + pushTypeCode: string; // 推送类型代码,如 "friend" targetCount: number; successCount: number; - failureCount: number; - status: PushStatus; + failCount: number; + status: string; // 状态代码,如 "pending" + statusText: string; // 状态中文名称,如 "进行中" createTime: string; + contentLibraryName: string; // 内容库名称 } const PushHistory: React.FC = () => { @@ -59,8 +62,8 @@ const PushHistory: React.FC = () => { try { setLoading(true); const params: any = { - page, - pageSize: pagination.pageSize, + page: String(page), + limit: String(pagination.pageSize), }; if (searchValue.trim()) { @@ -68,7 +71,7 @@ const PushHistory: React.FC = () => { } if (typeFilter !== "all") { - params.pushType = typeFilter; + params.pushTypeCode = typeFilter; } if (statusFilter !== "all") { @@ -157,13 +160,33 @@ const PushHistory: React.FC = () => { }; // 获取推送类型标签 - const getPushTypeTag = (type: PushType) => { - const typeMap = { - [PushType.FRIEND_MESSAGE]: { text: "好友消息", color: "#666" }, - [PushType.GROUP_MESSAGE]: { text: "群消息", color: "#666" }, - [PushType.GROUP_ANNOUNCEMENT]: { text: "群公告", color: "#666" }, + const getPushTypeTag = (pushType: string, pushTypeCode?: string) => { + // 优先使用中文名称,如果没有则根据代码映射 + if (pushType) { + const colorMap: Record = { + 好友消息: "#1890ff", + 群消息: "#52c41a", + 群公告: "#722ed1", + }; + return ( + + {pushType} + + ); + } + // 如果没有中文名称,根据代码映射 + const codeMap: Record = { + [PushTypeCode.FRIEND]: { text: "好友消息", color: "#1890ff" }, + [PushTypeCode.GROUP]: { text: "群消息", color: "#52c41a" }, + [PushTypeCode.ANNOUNCEMENT]: { text: "群公告", color: "#722ed1" }, }; - const config = typeMap[type] || { text: "未知", color: "#666" }; + const config = + pushTypeCode && codeMap[pushTypeCode] + ? codeMap[pushTypeCode] + : { text: pushType || "未知", color: "#666" }; return ( {config.text} @@ -172,14 +195,31 @@ const PushHistory: React.FC = () => { }; // 获取状态标签 - const getStatusTag = (status: PushStatus) => { - const statusMap = { + const getStatusTag = (status: string, statusText?: string) => { + // 优先使用中文状态文本 + const displayText = statusText || status; + + // 根据状态代码或文本匹配 + const statusMap: Record< + string, + { text: string; color: string; icon: React.ReactNode } + > = { [PushStatus.COMPLETED]: { text: "已完成", color: "#52c41a", icon: , }, - [PushStatus.IN_PROGRESS]: { + completed: { + text: "已完成", + color: "#52c41a", + icon: , + }, + [PushStatus.PENDING]: { + text: "进行中", + color: "#1890ff", + icon: , + }, + pending: { text: "进行中", color: "#1890ff", icon: , @@ -189,12 +229,43 @@ const PushHistory: React.FC = () => { color: "#ff4d4f", icon: , }, + failed: { + text: "失败", + color: "#ff4d4f", + icon: , + }, }; - const config = statusMap[status] || { - text: "未知", - color: "#666", - icon: null, + + // 根据状态文本匹配 + const textMap: Record< + string, + { text: string; color: string; icon: React.ReactNode } + > = { + 已完成: { + text: "已完成", + color: "#52c41a", + icon: , + }, + 进行中: { + text: "进行中", + color: "#1890ff", + icon: , + }, + 失败: { + text: "失败", + color: "#ff4d4f", + icon: , + }, }; + + const config = textMap[displayText] || + statusMap[status] || + statusMap[status.toLowerCase()] || { + text: displayText, + color: "#666", + icon: null, + }; + return ( { dataIndex: "pushType", key: "pushType", width: 120, - render: (type: PushType) => getPushTypeTag(type), + render: (pushType: string, record: PushHistoryRecord) => + getPushTypeTag(pushType, record.pushTypeCode), }, { title: "任务名称", - dataIndex: "pushContent", - key: "pushContent", + dataIndex: "taskName", + key: "taskName", ellipsis: true, render: (text: string) => {text}, }, + { + title: "内容库", + dataIndex: "contentLibraryName", + key: "contentLibraryName", + width: 150, + ellipsis: true, + render: (text: string) => ( + {text || "-"} + ), + }, { title: "目标数量", dataIndex: "targetCount", @@ -246,8 +328,8 @@ const PushHistory: React.FC = () => { }, { title: "失败数", - dataIndex: "failureCount", - key: "failureCount", + dataIndex: "failCount", + key: "failCount", width: 100, align: "center" as const, render: (count: number) => ( @@ -260,7 +342,8 @@ const PushHistory: React.FC = () => { key: "status", width: 120, align: "center" as const, - render: (status: PushStatus) => getStatusTag(status), + render: (status: string, record: PushHistoryRecord) => + getStatusTag(status, record.statusText), }, { title: "创建时间", @@ -329,9 +412,9 @@ const PushHistory: React.FC = () => { suffixIcon={} > - - - + + + @@ -353,7 +436,7 @@ const PushHistory: React.FC = () => { columns={columns} dataSource={dataSource} loading={loading} - rowKey="id" + rowKey="workbenchId" pagination={false} className={styles.dataTable} /> From b0a60e428c249e02729a0862952351eb8616cb6a Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Thu, 20 Nov 2025 16:07:57 +0800 Subject: [PATCH 10/31] =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E8=AF=84=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/model/WechatAccountModel.php | 24 +- .../CheckAndFixWechatAccountCommand.php | 159 ++ .../WechatAccountHealthScoreService.php | 378 +++ .../controller/WorkbenchController.php | 244 +- Server/sql.sql | 2301 +++++++++++++++++ Server/微信健康分规则v2.md | 58 + 6 files changed, 3116 insertions(+), 48 deletions(-) create mode 100644 Server/application/command/CheckAndFixWechatAccountCommand.php create mode 100644 Server/application/common/service/WechatAccountHealthScoreService.php create mode 100644 Server/sql.sql create mode 100644 Server/微信健康分规则v2.md diff --git a/Server/application/api/model/WechatAccountModel.php b/Server/application/api/model/WechatAccountModel.php index 2f72b908..919da478 100644 --- a/Server/application/api/model/WechatAccountModel.php +++ b/Server/application/api/model/WechatAccountModel.php @@ -7,5 +7,27 @@ use think\Model; class WechatAccountModel extends Model { // 设置表名 - protected $table = 's2_wechat_account'; + protected $table = 's2_wechat_account'; + + // 定义字段类型 + protected $type = [ + 'healthScore' => 'integer', + 'baseScore' => 'integer', + 'dynamicScore' => 'integer', + 'isModifiedAlias' => 'integer', + 'frequentCount' => 'integer', + 'consecutiveNoFrequentDays' => 'integer', + 'lastFrequentTime' => 'integer', + 'lastNoFrequentTime' => 'integer', + 'scoreUpdateTime' => 'integer', + ]; + + // 允许批量赋值的字段 + protected $field = [ + 'id', 'wechatId', 'alias', 'nickname', 'avatar', 'gender', 'region', 'signature', + 'healthScore', 'baseScore', 'dynamicScore', 'isModifiedAlias', + 'lastFrequentTime', 'frequentCount', 'lastNoFrequentTime', + 'consecutiveNoFrequentDays', 'scoreUpdateTime', + 'createTime', 'updateTime', 'status', 'isDeleted' + ]; } \ No newline at end of file diff --git a/Server/application/command/CheckAndFixWechatAccountCommand.php b/Server/application/command/CheckAndFixWechatAccountCommand.php new file mode 100644 index 00000000..e498f77b --- /dev/null +++ b/Server/application/command/CheckAndFixWechatAccountCommand.php @@ -0,0 +1,159 @@ +setName('wechat:check-and-fix') + ->setDescription('检测和修复s2_wechat_account表中wechatId和alias不一致的问题,并更新健康分'); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln("开始检测和修复s2_wechat_account表..."); + + try { + // 1. 检测wechatId和alias不一致的记录 + $output->writeln("步骤1: 检测wechatId和alias不一致的记录..."); + $inconsistentAccounts = $this->findInconsistentAccounts(); + $output->writeln("发现 " . count($inconsistentAccounts) . " 条不一致记录"); + + if (empty($inconsistentAccounts)) { + $output->writeln("没有发现不一致的记录,任务完成!"); + return; + } + + // 2. 修复不一致的记录 + $output->writeln("步骤2: 修复不一致的记录..."); + $fixedCount = $this->fixInconsistentAccounts($inconsistentAccounts); + $output->writeln("已修复 " . $fixedCount . " 条记录"); + + // 3. 更新isModifiedAlias字段 + $output->writeln("步骤3: 更新isModifiedAlias字段..."); + $updatedCount = $this->updateModifiedAliasFlag(); + $output->writeln("已更新 " . $updatedCount . " 条记录的isModifiedAlias字段"); + + // 4. 重新计算健康分 + $output->writeln("步骤4: 重新计算健康分..."); + $healthScoreService = new WechatAccountHealthScoreService(); + $accountIds = array_column($inconsistentAccounts, 'id'); + $stats = $healthScoreService->batchCalculateAndUpdate($accountIds, 100); + $output->writeln("健康分计算完成:成功 " . $stats['success'] . " 条,失败 " . $stats['failed'] . " 条"); + + if (!empty($stats['errors'])) { + $output->writeln("错误详情:"); + foreach ($stats['errors'] as $error) { + $output->writeln(" 账号ID {$error['accountId']}: {$error['error']}"); + } + } + + $output->writeln("任务完成!"); + + } catch (\Exception $e) { + $output->writeln("错误: " . $e->getMessage()); + $output->writeln($e->getTraceAsString()); + } + } + + /** + * 查找wechatId和alias不一致的记录 + * + * @return array + */ + private function findInconsistentAccounts() + { + // 查找wechatId和alias不一致的记录 + // 条件:wechatId不为空,alias不为空,且wechatId != alias + $accounts = Db::table('s2_wechat_account') + ->where('isDeleted', 0) + ->where('wechatId', '<>', '') + ->where('alias', '<>', '') + ->whereRaw('wechatId != alias') + ->field('id, wechatId, alias, nickname, isModifiedAlias') + ->select(); + + return $accounts ?: []; + } + + /** + * 修复不一致的记录 + * 策略:从s2_wechat_friend表中查找最新的alias值来更新 + * + * @param array $accounts 不一致的账号列表 + * @return int 修复数量 + */ + private function fixInconsistentAccounts($accounts) + { + $fixedCount = 0; + + foreach ($accounts as $account) { + $wechatId = $account['wechatId']; + + // 从s2_wechat_friend表中查找最新的alias值 + $latestAlias = Db::table('s2_wechat_friend') + ->where('wechatId', $wechatId) + ->where('alias', '<>', '') + ->order('updateTime', 'desc') + ->value('alias'); + + // 如果找到了最新的alias,则更新 + if (!empty($latestAlias) && $latestAlias !== $account['alias']) { + Db::table('s2_wechat_account') + ->where('id', $account['id']) + ->update([ + 'alias' => $latestAlias, + 'updateTime' => time() + ]); + $fixedCount++; + } + } + + return $fixedCount; + } + + /** + * 更新isModifiedAlias字段 + * 如果wechatId和alias不一致,则标记为已修改 + * + * @return int 更新数量 + */ + private function updateModifiedAliasFlag() + { + // 更新isModifiedAlias字段:wechatId != alias 的记录标记为1 + $updatedCount = Db::table('s2_wechat_account') + ->where('isDeleted', 0) + ->where('wechatId', '<>', '') + ->where('alias', '<>', '') + ->whereRaw('wechatId != alias') + ->update([ + 'isModifiedAlias' => 1, + 'updateTime' => time() + ]); + + // 更新isModifiedAlias字段:wechatId == alias 的记录标记为0 + Db::table('s2_wechat_account') + ->where('isDeleted', 0) + ->where('wechatId', '<>', '') + ->where('alias', '<>', '') + ->whereRaw('wechatId = alias') + ->update([ + 'isModifiedAlias' => 0, + 'updateTime' => time() + ]); + + return $updatedCount; + } +} + diff --git a/Server/application/common/service/WechatAccountHealthScoreService.php b/Server/application/common/service/WechatAccountHealthScoreService.php new file mode 100644 index 00000000..cda09cfd --- /dev/null +++ b/Server/application/common/service/WechatAccountHealthScoreService.php @@ -0,0 +1,378 @@ +where('id', $accountId) + ->find(); + } + + if (empty($accountData)) { + throw new Exception("账号不存在:{$accountId}"); + } + + // 计算基础分 + $baseScore = $this->calculateBaseScore($accountData); + + // 计算动态分 + $dynamicScore = $this->calculateDynamicScore($accountData); + + // 计算总分 + $healthScore = $baseScore + $dynamicScore; + + // 确保健康分在合理范围内(0-100) + $healthScore = max(0, min(100, $healthScore)); + + // 更新数据库 + $updateData = [ + 'healthScore' => $healthScore, + 'baseScore' => $baseScore, + 'dynamicScore' => $dynamicScore, + 'scoreUpdateTime' => time() + ]; + + Db::table('s2_wechat_account') + ->where('id', $accountId) + ->update($updateData); + + return [ + 'accountId' => $accountId, + 'wechatId' => $accountData['wechatId'] ?? '', + 'healthScore' => $healthScore, + 'baseScore' => $baseScore, + 'dynamicScore' => $dynamicScore, + 'baseInfoScore' => $this->getBaseInfoScore($accountData), + 'friendCountScore' => $this->getFriendCountScore($accountData['totalFriend'] ?? 0), + 'maxAddFriendPerDay' => $this->getMaxAddFriendPerDay($healthScore) + ]; + + } catch (Exception $e) { + throw new Exception("计算健康分失败:" . $e->getMessage()); + } + } + + /** + * 计算基础分 + * 基础分 = 默认60分 + 基础信息分(10分) + 好友数量分(30分) + * + * @param array $accountData 账号数据 + * @return int 基础分 + */ + private function calculateBaseScore($accountData) + { + $baseScore = self::DEFAULT_BASE_SCORE; + + // 基础信息分(已修改微信号得10分) + $baseScore += $this->getBaseInfoScore($accountData); + + // 好友数量分(最高30分) + $totalFriend = $accountData['totalFriend'] ?? 0; + $baseScore += $this->getFriendCountScore($totalFriend); + + return $baseScore; + } + + /** + * 获取基础信息分 + * 已修改微信号:10分 + * + * @param array $accountData 账号数据 + * @return int 基础信息分 + */ + private function getBaseInfoScore($accountData) + { + // 检查是否已修改微信号 + // 如果isModifiedAlias字段为1,或者wechatId和alias不一致,则认为已修改 + $isModifiedAlias = isset($accountData['isModifiedAlias']) ? (int)$accountData['isModifiedAlias'] : 0; + $wechatId = trim($accountData['wechatId'] ?? ''); + $alias = trim($accountData['alias'] ?? ''); + + // 如果字段标记为已修改,或者wechatId和alias不一致,则得分 + if ($isModifiedAlias == 1 || (!empty($wechatId) && !empty($alias) && $wechatId !== $alias)) { + return self::BASE_INFO_SCORE; + } + + return 0; + } + + /** + * 获取好友数量分 + * 根据好友数量区间得分(最高30分) + * + * @param int $totalFriend 总好友数 + * @return int 好友数量分 + */ + private function getFriendCountScore($totalFriend) + { + if ($totalFriend <= 50) { + return 3; // 0-50: 3分 + } elseif ($totalFriend <= 500) { + return 6; // 51-500: 6分 + } elseif ($totalFriend <= 3000) { + return 8; // 501-3000: 8分 + } else { + return 12; // 3001以上: 12分 + } + } + + /** + * 计算动态分 + * 动态分 = 扣分 + 加分 + * + * @param array $accountData 账号数据 + * @return int 动态分 + */ + private function calculateDynamicScore($accountData) + { + $dynamicScore = 0; + + // 处理扣分 + $dynamicScore += $this->calculatePenalty($accountData); + + // 处理加分 + $dynamicScore += $this->calculateBonus($accountData); + + return $dynamicScore; + } + + /** + * 计算扣分 + * 首次频繁:-15分 + * 再次频繁:-25分 + * 封号:-60分 + * + * @param array $accountData 账号数据 + * @return int 扣分数 + */ + private function calculatePenalty($accountData) + { + $penalty = 0; + + // 检查是否有频繁记录 + $lastFrequentTime = $accountData['lastFrequentTime'] ?? null; + $frequentCount = $accountData['frequentCount'] ?? 0; + + if (!empty($lastFrequentTime)) { + // 判断是首次频繁还是再次频繁 + if ($frequentCount == 1) { + $penalty += self::PENALTY_FIRST_FREQUENT; // 首次频繁-15分 + } elseif ($frequentCount >= 2) { + $penalty += self::PENALTY_SECOND_FREQUENT; // 再次频繁-25分 + } + } + + // 检查是否封号(这里需要根据实际业务逻辑判断,比如status字段或其他标识) + // 假设status=0表示封号 + $status = $accountData['status'] ?? 1; + if ($status == 0) { + $penalty += self::PENALTY_BANNED; // 封号-60分 + } + + return $penalty; + } + + /** + * 计算加分 + * 连续3天不触发频繁:每天+5分 + * + * @param array $accountData 账号数据 + * @return int 加分数 + */ + private function calculateBonus($accountData) + { + $bonus = 0; + + $lastNoFrequentTime = $accountData['lastNoFrequentTime'] ?? null; + $consecutiveNoFrequentDays = $accountData['consecutiveNoFrequentDays'] ?? 0; + + // 如果连续不频繁天数>=3,则每天+5分 + if ($consecutiveNoFrequentDays >= 3) { + $bonus = $consecutiveNoFrequentDays * self::BONUS_NO_FREQUENT_PER_DAY; + } + + return $bonus; + } + + /** + * 根据健康分计算每日最大加人次数 + * 公式:每日最大加人次数 = 健康分 * 0.2 + * + * @param int $healthScore 健康分 + * @return int 每日最大加人次数 + */ + public function getMaxAddFriendPerDay($healthScore) + { + return (int)floor($healthScore * 0.2); + } + + /** + * 批量计算并更新多个账号的健康分 + * + * @param array $accountIds 账号ID数组 + * @param int $batchSize 每批处理数量 + * @return array 处理结果统计 + */ + public function batchCalculateAndUpdate($accountIds = [], $batchSize = 100) + { + $stats = [ + 'total' => 0, + 'success' => 0, + 'failed' => 0, + 'errors' => [] + ]; + + // 如果没有指定账号ID,则处理所有账号 + if (empty($accountIds)) { + $accountIds = Db::table('s2_wechat_account') + ->where('isDeleted', 0) + ->column('id'); + } + + $stats['total'] = count($accountIds); + + // 分批处理 + $batches = array_chunk($accountIds, $batchSize); + + foreach ($batches as $batch) { + foreach ($batch as $accountId) { + try { + $this->calculateAndUpdate($accountId); + $stats['success']++; + } catch (Exception $e) { + $stats['failed']++; + $stats['errors'][] = [ + 'accountId' => $accountId, + 'error' => $e->getMessage() + ]; + } + } + } + + return $stats; + } + + /** + * 记录频繁事件 + * + * @param int $accountId 账号ID + * @return bool + */ + public function recordFrequent($accountId) + { + $accountData = Db::table('s2_wechat_account') + ->where('id', $accountId) + ->find(); + + if (empty($accountData)) { + return false; + } + + $frequentCount = ($accountData['frequentCount'] ?? 0) + 1; + + $updateData = [ + 'lastFrequentTime' => time(), + 'frequentCount' => $frequentCount, + 'consecutiveNoFrequentDays' => 0, // 重置连续不频繁天数 + 'lastNoFrequentTime' => null + ]; + + Db::table('s2_wechat_account') + ->where('id', $accountId) + ->update($updateData); + + // 重新计算健康分 + $this->calculateAndUpdate($accountId); + + return true; + } + + /** + * 记录不频繁事件(用于加分) + * + * @param int $accountId 账号ID + * @return bool + */ + public function recordNoFrequent($accountId) + { + $accountData = Db::table('s2_wechat_account') + ->where('id', $accountId) + ->find(); + + if (empty($accountData)) { + return false; + } + + $lastNoFrequentTime = $accountData['lastNoFrequentTime'] ?? null; + $consecutiveNoFrequentDays = $accountData['consecutiveNoFrequentDays'] ?? 0; + $currentTime = time(); + + // 如果上次不频繁时间是昨天或更早,则增加连续天数 + if (empty($lastNoFrequentTime) || ($currentTime - $lastNoFrequentTime) >= 86400) { + // 如果间隔超过1天,重置为1天 + if (!empty($lastNoFrequentTime) && ($currentTime - $lastNoFrequentTime) > 86400 * 2) { + $consecutiveNoFrequentDays = 1; + } else { + $consecutiveNoFrequentDays++; + } + } + + $updateData = [ + 'lastNoFrequentTime' => $currentTime, + 'consecutiveNoFrequentDays' => $consecutiveNoFrequentDays + ]; + + Db::table('s2_wechat_account') + ->where('id', $accountId) + ->update($updateData); + + // 重新计算健康分 + $this->calculateAndUpdate($accountId); + + return true; + } +} + diff --git a/Server/application/cunkebao/controller/WorkbenchController.php b/Server/application/cunkebao/controller/WorkbenchController.php index 62e17116..11681d9d 100644 --- a/Server/application/cunkebao/controller/WorkbenchController.php +++ b/Server/application/cunkebao/controller/WorkbenchController.php @@ -2816,6 +2816,8 @@ class WorkbenchController extends Controller $limit = $this->request->param('limit', 10); $workbenchId = $this->request->param('workbenchId', 0); $keyword = $this->request->param('keyword', ''); + $pushType = $this->request->param('pushType', ''); // 推送类型筛选:''=全部, 'friend'=好友消息, 'group'=群消息, 'announcement'=群公告 + $status = $this->request->param('status', ''); // 状态筛选:''=全部, 'success'=已完成, 'progress'=进行中, 'failed'=失败 $userId = $this->request->userInfo['id']; // 构建工作台查询条件 @@ -2840,10 +2842,11 @@ class WorkbenchController extends Controller $workbenchWhere[] = ['w.id', '=', $workbenchId]; } - // 按内容ID、工作台ID和时间分组,统计每次推送 - $query = Db::name('workbench_group_push_item') + // 1. 先查询所有已执行的推送记录(按推送时间分组) + $pushHistoryQuery = Db::name('workbench_group_push_item') ->alias('wgpi') ->join('workbench w', 'w.id = wgpi.workbenchId', 'left') + ->join('workbench_group_push wgp', 'wgp.workbenchId = wgpi.workbenchId', 'left') ->join('content_item ci', 'ci.id = wgpi.contentId', 'left') ->join('content_library cl', 'cl.id = ci.libraryId', 'left') ->where($workbenchWhere) @@ -2853,52 +2856,57 @@ class WorkbenchController extends Controller 'wgpi.contentId', 'FROM_UNIXTIME(wgpi.createTime, "%Y-%m-%d %H:00:00") as pushTime', 'wgpi.targetType', + 'wgp.groupPushSubType', 'MIN(wgpi.createTime) as createTime', 'COUNT(DISTINCT wgpi.id) as totalCount', 'cl.name as contentLibraryName' ]) - ->group('wgpi.workbenchId, wgpi.contentId, pushTime, wgpi.targetType'); + ->group('wgpi.workbenchId, wgpi.contentId, pushTime, wgpi.targetType, wgp.groupPushSubType'); if (!empty($keyword)) { - $query->where('w.name|cl.name|ci.content', 'like', '%' . $keyword . '%'); + $pushHistoryQuery->where('w.name|cl.name|ci.content', 'like', '%' . $keyword . '%'); } - // 获取分页数据 - $list = $query->order('createTime', 'desc') - ->page($page, $limit) - ->select(); - - // 对于有 group by 的查询,统计总数需要重新查询 - $totalQuery = Db::name('workbench_group_push_item') - ->alias('wgpi') - ->join('workbench w', 'w.id = wgpi.workbenchId', 'left') - ->join('content_item ci', 'ci.id = wgpi.contentId', 'left') - ->join('content_library cl', 'cl.id = ci.libraryId', 'left') - ->where($workbenchWhere); - - if (!empty($keyword)) { - $totalQuery->where('w.name|cl.name|ci.content', 'like', '%' . $keyword . '%'); - } - - // 统计分组后的记录数(使用子查询) - $subQuery = $totalQuery + $pushHistoryList = $pushHistoryQuery->order('createTime', 'desc')->select(); + + // 2. 查询所有任务(包括未执行的) + $allTasksQuery = Db::name('workbench') + ->alias('w') + ->join('workbench_group_push wgp', 'wgp.workbenchId = w.id', 'left') + ->where($workbenchWhere) ->field([ - 'wgpi.workbenchId', - 'wgpi.contentId', - 'FROM_UNIXTIME(wgpi.createTime, "%Y-%m-%d %H:00:00") as pushTime', - 'wgpi.targetType' - ]) - ->group('wgpi.workbenchId, wgpi.contentId, pushTime, wgpi.targetType') - ->buildSql(); - - $total = Db::table('(' . $subQuery . ') as temp')->count(); + 'w.id as workbenchId', + 'w.name as workbenchName', + 'w.createTime', + 'wgp.targetType', + 'wgp.groupPushSubType', + 'wgp.groups', + 'wgp.friends', + 'wgp.trafficPools' + ]); - // 处理每条记录 - foreach ($list as &$item) { + if (!empty($keyword)) { + $allTasksQuery->where('w.name', 'like', '%' . $keyword . '%'); + } + + $allTasks = $allTasksQuery->select(); + + // 3. 合并数据:已执行的推送记录 + 未执行的任务 + $resultList = []; + $executedWorkbenchIds = []; + + // 处理已执行的推送记录 + foreach ($pushHistoryList as $item) { $itemWorkbenchId = $item['workbenchId']; $contentId = $item['contentId']; $pushTime = $item['pushTime']; $targetType = intval($item['targetType']); + $groupPushSubType = isset($item['groupPushSubType']) ? intval($item['groupPushSubType']) : 1; + + // 标记该工作台已有执行记录 + if (!in_array($itemWorkbenchId, $executedWorkbenchIds)) { + $executedWorkbenchIds[] = $itemWorkbenchId; + } // 将时间字符串转换为时间戳范围(小时级别) $pushTimeStart = strtotime($pushTime); @@ -2937,23 +2945,149 @@ class WorkbenchController extends Controller $failCount = 0; // 简化处理,实际需要从发送状态获取 // 状态判断 - $status = $successCount > 0 ? 'success' : 'failed'; + $itemStatus = $successCount > 0 ? 'success' : 'failed'; if ($failCount > 0 && $successCount > 0) { - $status = 'partial'; + $itemStatus = 'partial'; } - $item['pushType'] = $targetType == 1 ? '群推送' : '好友推送'; - $item['pushTypeCode'] = $targetType; - $item['targetCount'] = $targetCount; - $item['successCount'] = $successCount; - $item['failCount'] = $failCount; - $item['status'] = $status; - $item['statusText'] = $status == 'success' ? '成功' : ($status == 'partial' ? '部分成功' : '失败'); - $item['createTime'] = date('Y-m-d H:i:s', $item['createTime']); - // 任务名称(工作台名称) - $item['taskName'] = $item['workbenchName'] ?? ''; + // 推送类型判断 + $pushTypeText = ''; + $pushTypeCode = ''; + if ($targetType == 1) { + // 群推送 + if ($groupPushSubType == 2) { + $pushTypeText = '群公告'; + $pushTypeCode = 'announcement'; + } else { + $pushTypeText = '群消息'; + $pushTypeCode = 'group'; + } + } else { + // 好友推送 + $pushTypeText = '好友消息'; + $pushTypeCode = 'friend'; + } + + $resultList[] = [ + 'workbenchId' => $itemWorkbenchId, + 'taskName' => $item['workbenchName'] ?? '', + 'pushType' => $pushTypeText, + 'pushTypeCode' => $pushTypeCode, + 'targetCount' => $targetCount, + 'successCount' => $successCount, + 'failCount' => $failCount, + 'status' => $itemStatus, + 'statusText' => $this->getStatusText($itemStatus), + 'createTime' => date('Y-m-d H:i:s', $item['createTime']), + 'contentLibraryName' => $item['contentLibraryName'] ?? '' + ]; } - unset($item); + + // 处理未执行的任务 + foreach ($allTasks as $task) { + $taskWorkbenchId = $task['workbenchId']; + + // 如果该任务已有执行记录,跳过(避免重复) + if (in_array($taskWorkbenchId, $executedWorkbenchIds)) { + continue; + } + + $targetType = isset($task['targetType']) ? intval($task['targetType']) : 1; + $groupPushSubType = isset($task['groupPushSubType']) ? intval($task['groupPushSubType']) : 1; + + // 计算目标数量(从配置中获取) + $targetCount = 0; + if ($targetType == 1) { + // 群推送:统计配置的群数量 + $groups = json_decode($task['groups'] ?? '[]', true); + $targetCount = is_array($groups) ? count($groups) : 0; + } else { + // 好友推送:统计配置的好友数量或流量池数量 + $friends = json_decode($task['friends'] ?? '[]', true); + $trafficPools = json_decode($task['trafficPools'] ?? '[]', true); + $friendCount = is_array($friends) ? count($friends) : 0; + $poolCount = is_array($trafficPools) ? count($trafficPools) : 0; + // 如果配置了流量池,目标数量暂时显示为流量池数量(实际数量需要从流量池中统计) + $targetCount = $friendCount > 0 ? $friendCount : $poolCount; + } + + // 推送类型判断 + $pushTypeText = ''; + $pushTypeCode = ''; + if ($targetType == 1) { + // 群推送 + if ($groupPushSubType == 2) { + $pushTypeText = '群公告'; + $pushTypeCode = 'announcement'; + } else { + $pushTypeText = '群消息'; + $pushTypeCode = 'group'; + } + } else { + // 好友推送 + $pushTypeText = '好友消息'; + $pushTypeCode = 'friend'; + } + + $resultList[] = [ + 'workbenchId' => $taskWorkbenchId, + 'taskName' => $task['workbenchName'] ?? '', + 'pushType' => $pushTypeText, + 'pushTypeCode' => $pushTypeCode, + 'targetCount' => $targetCount, + 'successCount' => 0, + 'failCount' => 0, + 'status' => 'pending', + 'statusText' => '进行中', + 'createTime' => date('Y-m-d H:i:s', $task['createTime']), + 'contentLibraryName' => '' + ]; + } + + // 应用筛选条件 + $filteredList = []; + foreach ($resultList as $item) { + // 推送类型筛选 + if (!empty($pushType)) { + if ($pushType === 'friend' && $item['pushTypeCode'] !== 'friend') { + continue; + } + if ($pushType === 'group' && $item['pushTypeCode'] !== 'group') { + continue; + } + if ($pushType === 'announcement' && $item['pushTypeCode'] !== 'announcement') { + continue; + } + } + + // 状态筛选 + if (!empty($status)) { + if ($status === 'success' && $item['status'] !== 'success') { + continue; + } + if ($status === 'progress') { + // 进行中:包括 partial 和 pending + if ($item['status'] !== 'partial' && $item['status'] !== 'pending') { + continue; + } + } + if ($status === 'failed' && $item['status'] !== 'failed') { + continue; + } + } + + $filteredList[] = $item; + } + + // 按创建时间倒序排序 + usort($filteredList, function($a, $b) { + return strtotime($b['createTime']) - strtotime($a['createTime']); + }); + + // 分页处理 + $total = count($filteredList); + $offset = ($page - 1) * $limit; + $list = array_slice($filteredList, $offset, $limit); return json([ 'code' => 200, @@ -2967,5 +3101,21 @@ class WorkbenchController extends Controller ]); } + /** + * 获取状态文本 + * @param string $status 状态码 + * @return string 状态文本 + */ + private function getStatusText($status) + { + $statusMap = [ + 'success' => '已完成', + 'partial' => '进行中', + 'pending' => '进行中', + 'failed' => '失败' + ]; + return $statusMap[$status] ?? '未知'; + } + } \ No newline at end of file diff --git a/Server/sql.sql b/Server/sql.sql new file mode 100644 index 00000000..b49716bc --- /dev/null +++ b/Server/sql.sql @@ -0,0 +1,2301 @@ +/* + Navicat Premium Data Transfer + + Source Server : kr_存客宝 + Source Server Type : MySQL + Source Server Version : 50736 + Source Host : 56b4c23f6853c.gz.cdb.myqcloud.com:14413 + Source Schema : cunkebao_v3 + + Target Server Type : MySQL + Target Server Version : 50736 + File Encoding : 65001 + + Date: 12/11/2025 11:05:39 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for ck_administrator_permissions +-- ---------------------------- +DROP TABLE IF EXISTS `ck_administrator_permissions`; +CREATE TABLE `ck_administrator_permissions` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自动ID', + `adminId` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '超管用户ID', + `permissions` json NULL COMMENT '权限对象', + `createTime` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '更新时间', + `deleteTime` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '超级管理员权限配置表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_administrators +-- ---------------------------- +DROP TABLE IF EXISTS `ck_administrators`; +CREATE TABLE `ck_administrators` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `username` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '管理员名字', + `account` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '登录账号', + `password` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '登录密码', + `status` tinyint(3) UNSIGNED NULL DEFAULT 1 COMMENT '1->可用,0->禁用', + `lastLoginTime` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '最近登录时间', + `lastLoginIp` char(18) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '最近登录ip', + `authId` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '权限id', + `createTime` int(10) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(10) NULL DEFAULT NULL COMMENT '更新时间', + `deleteTime` int(11) NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '超级管理员表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_ai_knowledge_base +-- ---------------------------- +DROP TABLE IF EXISTS `ck_ai_knowledge_base`; +CREATE TABLE `ck_ai_knowledge_base` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `typeId` int(11) NULL DEFAULT 1 COMMENT '类型id', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称', + `label` json NULL COMMENT '标签', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + `isDel` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(11) NOT NULL DEFAULT 0 COMMENT '删除时间', + `documentId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '知识库文件id', + `fileUrl` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件地址', + `size` int(10) NULL DEFAULT NULL COMMENT '文件大小', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ai知识库' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_ai_knowledge_base_type +-- ---------------------------- +DROP TABLE IF EXISTS `ck_ai_knowledge_base_type`; +CREATE TABLE `ck_ai_knowledge_base_type` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `type` tinyint(2) NULL DEFAULT 1 COMMENT '类型 0系统 1用户创建', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述', + `label` json NULL COMMENT '标签', + `prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '提示词', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + `isDel` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(11) NOT NULL DEFAULT 0 COMMENT '删除时间', + `status` tinyint(2) NULL DEFAULT 1 COMMENT '状态 1启用 0禁用', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ai知识库类型' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_ai_settings +-- ---------------------------- +DROP TABLE IF EXISTS `ck_ai_settings`; +CREATE TABLE `ck_ai_settings` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `config` json NULL COMMENT '配置信息', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + `isRelease` tinyint(2) NULL DEFAULT 0 COMMENT '是否发布 0未发布 1已发布', + `releaseTime` int(11) NULL DEFAULT NULL COMMENT '发布时间', + `botId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '智能体id', + `datasetId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '知识库id', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'AI配置' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_app_version +-- ---------------------------- +DROP TABLE IF EXISTS `ck_app_version`; +CREATE TABLE `ck_app_version` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `forceUpdate` tinyint(2) NULL DEFAULT 0 COMMENT '是否强制更新', + `version` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `downloadUrl` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `updateContent` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `createTime` int(11) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_attachments +-- ---------------------------- +DROP TABLE IF EXISTS `ck_attachments`; +CREATE TABLE `ck_attachments` ( + `id` int(10) NOT NULL AUTO_INCREMENT COMMENT '自增长ID', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '资源名', + `hash_key` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '资源hash校验值', + `server` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '存储服务商', + `source` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '资源地址', + `dl_count` int(10) NULL DEFAULT 0 COMMENT '下载次数', + `size` int(10) NULL DEFAULT 0 COMMENT '资源大小', + `suffix` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '资源类型', + `scene` tinyint(3) NOT NULL COMMENT '引用场景,获客海报1', + `create_at` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', + `update_at` timestamp(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_at` timestamp(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_hash_key`(`hash_key`) USING BTREE, + INDEX `idx_server`(`server`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 481 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '附件表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_call_recording +-- ---------------------------- +DROP TABLE IF EXISTS `ck_call_recording`; +CREATE TABLE `ck_call_recording` ( + `id` int(11) NOT NULL, + `phone` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号', + `isCallOut` tinyint(2) NULL DEFAULT NULL COMMENT '是否外呼', + `companyId` int(11) NULL DEFAULT NULL, + `callType` tinyint(2) NULL DEFAULT NULL, + `beginTime` int(11) NULL DEFAULT NULL, + `endTime` int(11) NULL DEFAULT NULL, + `createTime` int(11) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_id_phone_isCallOut_companyId`(`id`, `phone`, `isCallOut`, `companyId`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_company +-- ---------------------------- +DROP TABLE IF EXISTS `ck_company`; +CREATE TABLE `ck_company` ( + `id` int(11) UNSIGNED NOT NULL COMMENT '项目真实ID,非自增', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '项目名称', + `status` tinyint(1) UNSIGNED NULL DEFAULT 1 COMMENT '状态', + `tenantId` int(11) UNSIGNED NULL DEFAULT 242 COMMENT '触客宝租户ID', + `companyId` int(11) UNSIGNED NOT NULL COMMENT '触客宝部门ID', + `memo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `createTime` int(11) NULL DEFAULT NULL, + `updateTime` int(11) NULL DEFAULT NULL, + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '部门表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_content_item +-- ---------------------------- +DROP TABLE IF EXISTS `ck_content_item`; +CREATE TABLE `ck_content_item` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `libraryId` int(11) NOT NULL COMMENT '所属内容库ID', + `type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'moment' COMMENT '内容类型(moment:朋友圈)', + `contentType` tinyint(1) NULL DEFAULT 0 COMMENT '0:未知 1:图片 2:链接 3:视频 4:文本 5:小程序 ', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '内容标题', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '文本内容', + `contentAi` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '文本内容_Ai版', + `contentData` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '完整内容数据(JSON格式)', + `snsId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '朋友圈唯一标识', + `msgId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群消息唯一标识', + `wechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信ID', + `friendId` int(11) NULL DEFAULT NULL COMMENT '微信好友ID', + `createMomentTime` bigint(20) NULL DEFAULT 0 COMMENT '朋友圈创建时间', + `createTime` int(11) NULL DEFAULT NULL COMMENT '记录创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '记录更新时间', + `coverImage` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '封面图片URL', + `resUrls` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '资源URL列表(JSON格式)', + `urls` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '相对路径URL列表(JSON格式)', + `location` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '地理位置名称', + `lat` decimal(10, 6) NULL DEFAULT 0.000000 COMMENT '纬度', + `lng` decimal(10, 6) NULL DEFAULT 0.000000 COMMENT '经度', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态(0:禁用,1:启用)', + `isDel` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除(0:否,1:是)', + `delTime` int(11) NULL DEFAULT 0 COMMENT '删除时间', + `wechatChatroomId` int(11) NULL DEFAULT NULL, + `senderNickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `createMessageTime` int(11) NULL DEFAULT NULL, + `comment` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '评论', + `sendTime` int(11) NULL DEFAULT 0 COMMENT '预计发布时间', + `sendTimes` int(11) NULL DEFAULT 0 COMMENT '实际发布时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_library`(`libraryId`) USING BTREE, + INDEX `idx_snsid`(`snsId`) USING BTREE, + INDEX `idx_wechatid`(`wechatId`) USING BTREE, + INDEX `idx_friendid`(`friendId`) USING BTREE, + INDEX `idx_create_time`(`createTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5876 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '内容项目表-存储朋友圈采集数据' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_content_library +-- ---------------------------- +DROP TABLE IF EXISTS `ck_content_library`; +CREATE TABLE `ck_content_library` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `sourceType` tinyint(2) NOT NULL DEFAULT 1 COMMENT '类型 1好友 2群 3自定义', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '内容库名称', + `devices` json NULL COMMENT '设备列表,JSON格式:[{\"id\":1,\"name\":\"设备1\"},{\"id\":2,\"name\":\"设备2\"}]', + `catchType` json NULL COMMENT '采集类型', + `sourceFriends` json NULL COMMENT '选择的微信好友', + `sourceGroups` json NULL COMMENT '选择的微信群', + `groupMembers` json NULL COMMENT '选择的微信群的群成员', + `keywordInclude` json NULL COMMENT '包含的关键词', + `keywordExclude` json NULL COMMENT '排除的关键词', + `aiEnabled` tinyint(1) NULL DEFAULT 0 COMMENT '是否启用AI:0=禁用,1=启用', + `aiPrompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT 'AI提示词', + `timeEnabled` tinyint(1) NULL DEFAULT 0 COMMENT '是否启用时间限制:0=禁用,1=启用', + `timeStart` int(11) NULL DEFAULT NULL COMMENT '开始时间', + `timeEnd` int(11) NULL DEFAULT NULL COMMENT '结束时间', + `status` tinyint(1) NULL DEFAULT 0 COMMENT '状态:0=禁用,1=启用', + `userId` int(11) NOT NULL COMMENT '用户ID', + `companyId` int(11) NOT NULL COMMENT '公司ID', + `createTime` int(11) NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT 0 COMMENT '更新时间', + `isDel` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 87 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '内容库表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_coze_conversation +-- ---------------------------- +DROP TABLE IF EXISTS `ck_coze_conversation`; +CREATE TABLE `ck_coze_conversation` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户id', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `conversation_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '对话ID', + `bot_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '机器人ID', + `created_at` int(11) NOT NULL DEFAULT 0 COMMENT '会话创建时间戳', + `meta_data` json NULL COMMENT '元数据', + `create_time` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间戳', + `update_time` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间戳', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `idx_conversation_id`(`conversation_id`) USING BTREE, + INDEX `idx_bot_id`(`bot_id`) USING BTREE, + INDEX `idx_create_time`(`create_time`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 39 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'Coze AI 会话表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_coze_message +-- ---------------------------- +DROP TABLE IF EXISTS `ck_coze_message`; +CREATE TABLE `ck_coze_message` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `chat_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '消息ID', + `conversation_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '会话ID', + `bot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '机器人ID', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '消息内容', + `content_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'text' COMMENT '内容类型', + `role` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色', + `type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '消息类型', + `created_at` int(11) NOT NULL COMMENT '消息创建时间', + `updated_at` int(11) NOT NULL COMMENT '消息更新时间', + `create_time` int(11) NOT NULL COMMENT '记录创建时间', + `update_time` int(11) NOT NULL COMMENT '记录更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_chat_id`(`chat_id`) USING BTREE, + INDEX `idx_conversation_id`(`conversation_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 184 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '消息记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_coze_workspace +-- ---------------------------- +DROP TABLE IF EXISTS `ck_coze_workspace`; +CREATE TABLE `ck_coze_workspace` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `workspace_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '工作区ID', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '工作区名称', + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '工作区描述', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `workspace_id`(`workspace_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'Coze空间表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_customer_acquisition_task +-- ---------------------------- +DROP TABLE IF EXISTS `ck_customer_acquisition_task`; +CREATE TABLE `ck_customer_acquisition_task` ( + `id` int(10) NOT NULL AUTO_INCREMENT COMMENT '自增长ID', + `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '计划名称', + `sceneId` int(11) NULL DEFAULT 1 COMMENT '场景ID', + `sceneConf` json NULL COMMENT '场景具体配置信息', + `reqConf` json NULL COMMENT '好友申请设置', + `msgConf` json NULL COMMENT '消息设置', + `tagConf` json NULL COMMENT '标签设置', + `userId` int(11) NULL DEFAULT 0 COMMENT '创建者', + `companyId` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '公司ID', + `status` tinyint(3) NOT NULL DEFAULT 0 COMMENT '状态 0禁用 1启用', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '修改时间', + `deleteTime` int(11) NULL DEFAULT 0 COMMENT '删除时间', + `apiKey` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 162 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '获客计划表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_device +-- ---------------------------- +DROP TABLE IF EXISTS `ck_device`; +CREATE TABLE `ck_device` ( + `id` int(11) UNSIGNED NOT NULL COMMENT '设备真实ID,非自增', + `memo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '设备名称', + `imei` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '设备IMEI', + `deviceImei` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '设备本地IMEI', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号', + `operatingSystem` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作系统版本', + `model` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '型号', + `brand` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '品牌', + `rooted` tinyint(1) NULL DEFAULT 0 COMMENT '是否root', + `xPosed` tinyint(1) NULL DEFAULT 0 COMMENT '是否安装xposed', + `softwareVersion` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '软件版本', + `extra` json NULL COMMENT '额外信息JSON', + `alive` tinyint(1) NULL DEFAULT 0 COMMENT '是否在线', + `companyId` int(11) NOT NULL COMMENT '公司ID', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_id_imei`(`imei`, `id`) USING BTREE, + INDEX `idx_group`(`companyId`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_device_handle_log +-- ---------------------------- +DROP TABLE IF EXISTS `ck_device_handle_log`; +CREATE TABLE `ck_device_handle_log` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `content` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作说明', + `deviceId` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '设备id', + `userId` int(11) NULL DEFAULT NULL COMMENT '用户id', + `companyId` int(11) NULL DEFAULT NULL COMMENT '租户id', + `createTime` int(11) NULL DEFAULT NULL COMMENT '操作时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 304 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_device_taskconf +-- ---------------------------- +DROP TABLE IF EXISTS `ck_device_taskconf`; +CREATE TABLE `ck_device_taskconf` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `deviceId` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '设备ID', + `autoLike` tinyint(3) NULL DEFAULT 0 COMMENT '自动点赞', + `momentsSync` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT '朋友圈同步', + `autoCustomerDev` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT '自动开发客户', + `groupMessageDeliver` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT '群消息推送', + `autoGroup` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT '自动建群', + `autoAddFriend` tinyint(3) NULL DEFAULT 0 COMMENT '自动加好友', + `contentSync` tinyint(255) UNSIGNED NULL DEFAULT 0 COMMENT '朋友圈同步', + `aiChat` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT 'AI 会话', + `autoReply` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT '自动回复', + `companyId` int(10) NULL DEFAULT NULL COMMENT '公司ID', + `createTime` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '更新时间', + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 29 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备任务配置表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_device_user +-- ---------------------------- +DROP TABLE IF EXISTS `ck_device_user`; +CREATE TABLE `ck_device_user` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `companyId` int(11) UNSIGNED NOT NULL COMMENT '公司id', + `userId` int(11) UNSIGNED NOT NULL COMMENT '用户id', + `deviceId` int(11) UNSIGNED NOT NULL COMMENT '设备id', + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 22 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备跟操盘手的关联关系' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_device_wechat_login +-- ---------------------------- +DROP TABLE IF EXISTS `ck_device_wechat_login`; +CREATE TABLE `ck_device_wechat_login` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `deviceId` int(11) NULL DEFAULT NULL COMMENT '设备ID', + `wechatId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信ID', + `alive` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT '微信在线否', + `companyId` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '租户ID', + `createTime` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '更新时间', + `isTips` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否提示迁移', + PRIMARY KEY (`id`) USING BTREE, + INDEX `wechatId`(`wechatId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 309 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备登录微信记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_flow_package +-- ---------------------------- +DROP TABLE IF EXISTS `ck_flow_package`; +CREATE TABLE `ck_flow_package` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '套餐名称', + `tag` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '套餐标签', + `originalPrice` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '原价', + `price` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '售价', + `monthlyFlow` int(11) NOT NULL DEFAULT 0 COMMENT '每月流量(人/月)', + `duration` int(11) NOT NULL DEFAULT 1 COMMENT '套餐时长(月)', + `privileges` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '套餐特权,多行文本存储', + `sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序', + `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态: 0=禁用, 1=启用', + `isDel` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除: 0=否, 1=是', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_name`(`name`) USING BTREE, + INDEX `idx_tag`(`tag`) USING BTREE, + INDEX `idx_is_del`(`isDel`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量套餐表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_flow_package_order +-- ---------------------------- +DROP TABLE IF EXISTS `ck_flow_package_order`; +CREATE TABLE `ck_flow_package_order` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `orderNo` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '订单编号', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `packageId` int(11) NOT NULL DEFAULT 0 COMMENT '套餐ID', + `packageName` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '套餐名称', + `amount` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '订单金额', + `duration` int(11) NOT NULL DEFAULT 1 COMMENT '购买时长(月)', + `payStatus` tinyint(1) NOT NULL DEFAULT 0 COMMENT '支付状态: 0=未支付, 1=已支付,10=无需支付', + `payTime` int(11) NOT NULL DEFAULT 0 COMMENT '支付时间', + `payType` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '支付方式: wechat=微信, alipay=支付宝,nopay=无需支付', + `transactionId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '第三方支付交易号', + `status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '订单状态: 0=待支付, 1=已支付, 2=已取消, 3=已退款', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '备注', + `isDel` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除: 0=否, 1=是', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_order_no`(`orderNo`) USING BTREE, + INDEX `idx_user_id`(`userId`) USING BTREE, + INDEX `idx_package_id`(`packageId`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_pay_status`(`payStatus`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 37 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '套餐订单表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_flow_usage_record +-- ---------------------------- +DROP TABLE IF EXISTS `ck_flow_usage_record`; +CREATE TABLE `ck_flow_usage_record` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `packageId` int(11) NOT NULL DEFAULT 0 COMMENT '套餐ID', + `userPackageId` int(11) NOT NULL DEFAULT 0 COMMENT '用户套餐ID', + `taskId` int(11) NOT NULL DEFAULT 0 COMMENT '关联任务ID', + `phone` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '微信号', + `usageAmount` int(11) NOT NULL DEFAULT 0 COMMENT '使用量(人)', + `usageType` tinyint(1) NOT NULL DEFAULT 1 COMMENT '使用类型: 1=添加好友, 2=群发消息, 3=其他', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '备注', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_user_id`(`userId`) USING BTREE, + INDEX `idx_package_id`(`packageId`) USING BTREE, + INDEX `idx_user_package_id`(`userPackageId`) USING BTREE, + INDEX `idx_task_id`(`taskId`) USING BTREE, + INDEX `idx_phone`(`phone`) USING BTREE, + INDEX `idx_create_time`(`createTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量使用记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_jd_promotion_site +-- ---------------------------- +DROP TABLE IF EXISTS `ck_jd_promotion_site`; +CREATE TABLE `ck_jd_promotion_site` ( + `id` bigint(11) NOT NULL, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `jdSocialMediaId` bigint(11) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_jd_social_media +-- ---------------------------- +DROP TABLE IF EXISTS `ck_jd_social_media`; +CREATE TABLE `ck_jd_social_media` ( + `id` bigint(11) NOT NULL, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `appkey` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `secretkey` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_ai_push +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_ai_push`; +CREATE TABLE `ck_kf_ai_push` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '推送名称', + `tags` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '目标用户标签(JSON数组格式)', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '推送内容(支持变量:{客户名称}{产品功能}{核心价值}等)', + `pushTiming` tinyint(4) NOT NULL DEFAULT 1 COMMENT '推送时机:1=立即推送,2=最佳时机(AI决定),3=定时推送', + `scheduledTime` int(11) NOT NULL DEFAULT 0 COMMENT '定时推送时间(时间戳,仅当pushTiming=3时有效)', + `status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '启用状态:0=禁用,1=启用', + `successRate` decimal(5, 2) NOT NULL DEFAULT 0.00 COMMENT '成功率(百分比,保留两位小数)', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司ID', + `isDel` tinyint(4) NOT NULL DEFAULT 0 COMMENT '删除标记:0=未删除,1=已删除', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间(时间戳)', + `updateTime` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间(时间戳)', + `delTime` int(11) NOT NULL DEFAULT 0 COMMENT '删除时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_company_user`(`companyId`, `userId`) USING BTREE, + INDEX `idx_pushTiming`(`pushTiming`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_isDel`(`isDel`) USING BTREE, + INDEX `idx_scheduledTime`(`scheduledTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'AI智能推送表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_ai_push_record +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_ai_push_record`; +CREATE TABLE `ck_kf_ai_push_record` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `pushId` int(11) NOT NULL DEFAULT 0 COMMENT '推送ID', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司ID', + `wechatAccountId` int(11) NOT NULL DEFAULT 0 COMMENT '微信账号ID', + `friendIdOrGroupId` int(11) NOT NULL DEFAULT 0 COMMENT '好友ID或群ID', + `isSend` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否发送:0=未发送,1=已发送', + `sendTime` int(11) NOT NULL DEFAULT 0 COMMENT '发送时间(时间戳)', + `receiveTime` int(11) NOT NULL DEFAULT 0 COMMENT '接收时间(时间戳)', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_pushId`(`pushId`) USING BTREE, + INDEX `idx_company`(`companyId`) USING BTREE, + INDEX `idx_user`(`userId`) USING BTREE, + INDEX `idx_createTime`(`createTime`) USING BTREE, + INDEX `idx_isSend`(`isSend`) USING BTREE, + INDEX `idx_wechatAccount`(`wechatAccountId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'AI智能推送记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_auto_greetings +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_auto_greetings`; +CREATE TABLE `ck_kf_auto_greetings` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '规则名称', + `trigger` tinyint(4) NOT NULL DEFAULT 0 COMMENT '触发类型:1=好友首次添加,2=首次发消息,3=时间触发,4=关键词触发,5=生日触发,6=自定义', + `condition` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '具体条件(JSON格式)', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '问候内容', + `level` int(11) NOT NULL DEFAULT 0 COMMENT '优先级(数字越小优先级越高)', + `status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '启用状态:0=禁用,1=启用', + `usageCount` int(11) NOT NULL DEFAULT 0 COMMENT '使用次数', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司ID', + `is_template` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否模板:0=否,1=是', + `isDel` tinyint(4) NOT NULL DEFAULT 0 COMMENT '删除标记:0=未删除,1=已删除', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间(时间戳)', + `updateTime` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间(时间戳)', + `delTime` int(11) NOT NULL DEFAULT 0 COMMENT '删除时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_company_user`(`companyId`, `userId`) USING BTREE, + INDEX `idx_trigger`(`trigger`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_isDel`(`isDel`) USING BTREE, + INDEX `idx_level`(`level`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '问候规则表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_auto_greetings_record +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_auto_greetings_record`; +CREATE TABLE `ck_kf_auto_greetings_record` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `autoId` int(11) NOT NULL DEFAULT 0 COMMENT '规则ID', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司ID', + `wechatAccountId` int(11) NOT NULL DEFAULT 0 COMMENT '微信账号ID', + `friendIdOrGroupId` int(11) NOT NULL DEFAULT 0 COMMENT '好友ID或群ID', + `isSend` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否发送:0=未发送,1=已发送', + `sendTime` int(11) NOT NULL DEFAULT 0 COMMENT '发送时间(时间戳)', + `receiveTime` int(11) NOT NULL DEFAULT 0 COMMENT '接收时间(时间戳)', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_autoId`(`autoId`) USING BTREE, + INDEX `idx_company`(`companyId`) USING BTREE, + INDEX `idx_user`(`userId`) USING BTREE, + INDEX `idx_createTime`(`createTime`) USING BTREE, + INDEX `idx_isSend`(`isSend`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '问候规则使用记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_follow_up +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_follow_up`; +CREATE TABLE `ck_kf_follow_up` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标题', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述', + `friendId` int(12) NULL DEFAULT NULL COMMENT '好友id', + `type` tinyint(2) NULL DEFAULT 0 COMMENT '类型 0其他 1电话回访 2发送消息 3安排会议 4发送邮件', + `reminderTime` int(12) NULL DEFAULT NULL COMMENT '提醒时间', + `isRemind` tinyint(2) NULL DEFAULT 0 COMMENT '是否提醒', + `isProcess` tinyint(2) NULL DEFAULT 0 COMMENT '是否处理', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_companyId`(`companyId`) USING BTREE, + INDEX `idx_userId`(`userId`) USING BTREE, + INDEX `idx_level`(`type`) USING BTREE, + INDEX `idx_isRemind`(`isRemind`) USING BTREE, + INDEX `idx_isProcess`(`isProcess`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '跟进提醒' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_friend_settings +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_friend_settings`; +CREATE TABLE `ck_kf_friend_settings` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `type` tinyint(2) NULL DEFAULT 0 COMMENT '匹配类型 0人工接待 1AI辅助 2AI接管', + `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id', + `friendId` int(11) NULL DEFAULT NULL COMMENT '好友id', + `conversationId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '会话id', + `conversationTime` int(11) NULL DEFAULT NULL COMMENT '会话创建时间', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_companyId`(`companyId`) USING BTREE, + INDEX `idx_userId`(`userId`) USING BTREE, + INDEX `idx_wechatAccountId`(`wechatAccountId`) USING BTREE, + INDEX `idx_friendId`(`friendId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 37 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '好友AI配置' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_keywords +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_keywords`; +CREATE TABLE `ck_kf_keywords` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标题', + `keywords` json NULL COMMENT '关键词', + `type` tinyint(2) NULL DEFAULT NULL COMMENT '匹配类型 0模糊 1精确', + `replyType` tinyint(2) NULL DEFAULT NULL COMMENT '回复类型 0素材回复 1自定义', + `content` json NULL COMMENT '自定义内容', + `metailGroups` json NULL COMMENT '素材id', + `status` tinyint(2) NULL DEFAULT NULL COMMENT '状态 0停用 1启用', + `level` tinyint(2) NULL DEFAULT 0 COMMENT '等级 0低优先级 1中优先级 2高优先级', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '关键词管理' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_material +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_material`; +CREATE TABLE `ck_kf_material` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `title` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标题', + `content` json NULL COMMENT '内容', + `cover` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '封面', + `status` tinyint(2) NULL DEFAULT NULL COMMENT '状态 0停用 1启用', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 19 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '素材库管理' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_moments +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_moments`; +CREATE TABLE `ck_kf_moments` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `sendData` json NULL COMMENT '发送的具体信息', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `isSend` tinyint(2) NULL DEFAULT 0 COMMENT '是否发送 0否 1是', + `sendTime` int(11) NULL DEFAULT NULL COMMENT '发送时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '客服端发布朋友圈记录' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_moments_settings +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_moments_settings`; +CREATE TABLE `ck_kf_moments_settings` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `wechatId` int(12) NULL DEFAULT NULL COMMENT '微信客服id', + `max` int(11) NULL DEFAULT 5 COMMENT '每日上限', + `sendNum` int(11) NULL DEFAULT 0 COMMENT '今日发送次数', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '客服朋友圈配置信息' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_notice +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_notice`; +CREATE TABLE `ck_kf_notice` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `type` tinyint(2) NULL DEFAULT NULL COMMENT '通知类型 1代办事项 2跟进提醒 ', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `bindId` int(11) NULL DEFAULT NULL COMMENT '绑定的id', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标题', + `message` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '通知消息', + `isRead` tinyint(2) NULL DEFAULT 0 COMMENT '是否读取', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `readTime` int(12) NULL DEFAULT NULL COMMENT '读取时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 246 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '通知消息' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_questions +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_questions`; +CREATE TABLE `ck_kf_questions` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `type` tinyint(2) NULL DEFAULT 0 COMMENT '匹配类型 0模糊 1精确', + `questions` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '问题', + `answers` json NULL COMMENT '答案', + `status` tinyint(2) NULL DEFAULT 1 COMMENT '状态 0禁用 1启用', + `isDel` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除', + `deleteTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_companyId`(`companyId`) USING BTREE, + INDEX `idx_userId`(`userId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'AI问答' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_reply +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_reply`; +CREATE TABLE `ck_kf_reply` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `groupId` int(11) NULL DEFAULT NULL, + `userId` int(11) NULL DEFAULT NULL, + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `msgType` tinyint(2) NULL DEFAULT NULL, + `content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `createTime` int(11) NULL DEFAULT NULL, + `lastUpdateTime` int(11) NULL DEFAULT NULL, + `sortIndex` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 130746 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '快捷回复' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_reply_group +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_reply_group`; +CREATE TABLE `ck_kf_reply_group` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `userId` int(11) NULL DEFAULT 0, + `companyId` int(11) NULL DEFAULT 0, + `groupName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `sortIndex` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `parentId` int(11) NULL DEFAULT NULL, + `replyType` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `replys` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 21898 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '快捷回复分组' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_sensitive_word +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_sensitive_word`; +CREATE TABLE `ck_kf_sensitive_word` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标题', + `keywords` json NULL COMMENT '关键词', + `content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '替换内容/警告内容', + `operation` tinyint(2) NULL DEFAULT NULL COMMENT '操作 0不操作 1替换 2删除 3警告 4禁止发送', + `status` tinyint(2) NULL DEFAULT NULL COMMENT '状态 0停用 1启用', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '敏感词管理' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_to_do +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_to_do`; +CREATE TABLE `ck_kf_to_do` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标题', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述', + `friendId` int(12) NULL DEFAULT NULL COMMENT '好友id', + `level` tinyint(2) NULL DEFAULT 0 COMMENT '提示等级 0低优先级 1中优先级 2高优先级 3紧急', + `reminderTime` int(12) NULL DEFAULT NULL COMMENT '提醒时间', + `isRemind` tinyint(2) NULL DEFAULT 0 COMMENT '是否提醒', + `isProcess` tinyint(2) NULL DEFAULT 0 COMMENT '是否处理', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_companyId`(`companyId`) USING BTREE, + INDEX `idx_userId`(`userId`) USING BTREE, + INDEX `idx_level`(`level`) USING BTREE, + INDEX `idx_isRemind`(`isRemind`) USING BTREE, + INDEX `idx_isProcess`(`isProcess`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '待办事项' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_menus +-- ---------------------------- +DROP TABLE IF EXISTS `ck_menus`; +CREATE TABLE `ck_menus` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '菜单ID', + `title` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '菜单名称', + `path` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '路由路径', + `icon` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '图标名称', + `parentId` int(11) NOT NULL DEFAULT 0 COMMENT '父菜单ID,0表示顶级菜单', + `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态:1启用,0禁用', + `sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序,数值越小越靠前', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_parent_id`(`parentId`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '系统菜单表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_order +-- ---------------------------- +DROP TABLE IF EXISTS `ck_order`; +CREATE TABLE `ck_order` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `mchId` int(11) NULL DEFAULT NULL COMMENT '门店号', + `companyId` int(11) UNSIGNED NOT NULL, + `userId` int(11) NULL DEFAULT NULL, + `orderType` tinyint(2) NULL DEFAULT NULL COMMENT '订单类型 1购买算力', + `status` tinyint(1) UNSIGNED NULL DEFAULT 0 COMMENT '支付状态 0待支付 1已付款 2已退款 3付款失败', + `goodsId` int(11) NULL DEFAULT 0 COMMENT '商品id', + `goodsName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品名称', + `goodsSpecs` json NULL COMMENT '商品规格', + `money` int(11) NULL DEFAULT 0 COMMENT '金额 单位分', + `orderNo` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单号', + `ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `nonceStr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '随机字符串', + `createTime` int(11) NULL DEFAULT NULL, + `payType` tinyint(2) NULL DEFAULT NULL COMMENT '支付类型 1微信 2支付宝', + `payTime` int(11) NULL DEFAULT NULL, + `payInfo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '错误信息', + `deleteTime` int(11) UNSIGNED NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 79 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_plan_scene +-- ---------------------------- +DROP TABLE IF EXISTS `ck_plan_scene`; +CREATE TABLE `ck_plan_scene` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `name` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '场景名称', + `description` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述', + `image` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '图片icon', + `status` tinyint(3) NULL DEFAULT NULL COMMENT '状态', + `sort` tinyint(3) NULL DEFAULT NULL, + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '修改时间', + `deleteTime` int(11) NULL DEFAULT 0 COMMENT '删除时间', + `scenarioTags` json NULL COMMENT '标签', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '获客场景' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_plan_tags +-- ---------------------------- +DROP TABLE IF EXISTS `ck_plan_tags`; +CREATE TABLE `ck_plan_tags` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `tagName` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签名', + `companyId` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '部门ID', + `createTime` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量标签表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_task_customer +-- ---------------------------- +DROP TABLE IF EXISTS `ck_task_customer`; +CREATE TABLE `ck_task_customer` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `task_id` int(11) NOT NULL, + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户姓名', + `source` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '来源', + `phone` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `remark` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `tags` json NULL, + `siteTags` json NULL COMMENT '站内标签', + `processed_wechat_ids` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '0-未处理,1-已处理/添加中,2-~~添加成功~~ ~~已添加~~添加任务成功 3-添加失败 4-已通过-已发消息', + `fail_reason` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `addTime` int(11) NOT NULL DEFAULT 0 COMMENT '添加时间', + `passTime` int(11) NOT NULL DEFAULT 0 COMMENT '通过时间', + `createTime` int(11) NOT NULL DEFAULT 0, + `updateTime` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE, + INDEX `task_id`(`task_id`) USING BTREE, + INDEX `addTime`(`addTime`) USING BTREE, + INDEX `passTime`(`passTime`) USING BTREE, + INDEX `updateTime`(`updateTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 24192 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_tokens_company +-- ---------------------------- +DROP TABLE IF EXISTS `ck_tokens_company`; +CREATE TABLE `ck_tokens_company` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `tokens` bigint(100) NULL DEFAULT NULL, + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '公司算力账户' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_tokens_package +-- ---------------------------- +DROP TABLE IF EXISTS `ck_tokens_package`; +CREATE TABLE `ck_tokens_package` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称', + `tokens` int(12) NULL DEFAULT NULL, + `price` int(12) NULL DEFAULT NULL COMMENT '售价 单位分', + `originalPrice` int(12) NULL DEFAULT NULL COMMENT '原价 单位分', + `description` json NULL COMMENT '描述', + `sort` int(12) NULL DEFAULT 50 COMMENT '排序', + `isTrial` tinyint(2) NULL DEFAULT 0 COMMENT '是否试用', + `isRecommend` tinyint(2) NULL DEFAULT 0 COMMENT '是否推荐', + `isHot` tinyint(2) NULL DEFAULT 0 COMMENT '是否热门', + `isVip` tinyint(2) NULL DEFAULT 0 COMMENT '是否VIP', + `status` tinyint(2) NULL DEFAULT 0 COMMENT '状态 0停用 1启用', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'token套餐' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_tokens_record +-- ---------------------------- +DROP TABLE IF EXISTS `ck_tokens_record`; +CREATE TABLE `ck_tokens_record` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id', + `friendIdOrGroupId` int(11) NULL DEFAULT NULL COMMENT '好友id或者群id', + `form` tinyint(2) NULL DEFAULT 0 COMMENT '来源 0未知 1好友聊天 2群聊天 3群公告 4商家 5充值', + `type` tinyint(2) NULL DEFAULT 0 COMMENT '类型 0减少 1增加', + `tokens` int(11) NULL DEFAULT NULL COMMENT '消耗tokens', + `balanceTokens` int(11) NULL DEFAULT NULL COMMENT '剩余tokens', + `remarks` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 236 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '算力明细记录' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_traffic_order +-- ---------------------------- +DROP TABLE IF EXISTS `ck_traffic_order`; +CREATE TABLE `ck_traffic_order` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(10) UNSIGNED NULL DEFAULT NULL, + `identifier` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '流量池用户', + `createTime` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', + `isDel` tinyint(2) NULL DEFAULT 0, + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', + `orderno` varchar(0) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单编号', + `userId` int(11) NULL DEFAULT NULL, + `storeId` int(11) NULL DEFAULT NULL COMMENT '门店id', + `goddsName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品价格', + `price` int(10) NULL DEFAULT NULL COMMENT '商品价格', + `actualPay` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '实际支付', + `ownerWechatId` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_traffic_pool +-- ---------------------------- +DROP TABLE IF EXISTS `ck_traffic_pool`; +CREATE TABLE `ck_traffic_pool` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `identifier` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '流量标识,可以是手机号、微信号', + `mobile` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号', + `wechatId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信ID', + `createTime` int(10) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(10) NULL DEFAULT NULL COMMENT '修改时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_identifier`(`identifier`) USING BTREE, + INDEX `idx_wechatId`(`wechatId`) USING BTREE, + INDEX `idx_mobile`(`mobile`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 959687 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量池' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_traffic_profile +-- ---------------------------- +DROP TABLE IF EXISTS `ck_traffic_profile`; +CREATE TABLE `ck_traffic_profile` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `identifier` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '流量标识,可以是手机号、微信号', + `nickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '平台昵称', + `avatar` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '平台头像', + `gender` tinyint(3) NULL DEFAULT 0 COMMENT '平台性别', + `phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '平台手机号', + `platformId` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '平台Id', + `createTime` int(10) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(10) NULL DEFAULT NULL COMMENT '修改时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_identifier`(`identifier`) USING BTREE, + INDEX `idx_mobile`(`phone`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 196606 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量池用户个人信息' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_traffic_source +-- ---------------------------- +DROP TABLE IF EXISTS `ck_traffic_source`; +CREATE TABLE `ck_traffic_source` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `type` tinyint(2) NULL DEFAULT 1 COMMENT '流量来源 0其他 1好友 2群 3场景', + `identifier` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '流量标识', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称', + `status` tinyint(3) NULL DEFAULT 1 COMMENT '1待处理,2处理中,3已通过,4已拒绝,5已过期,6已取消 -3已删除(同步 tk_friend_task 表的 status)', + `sourceId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '来源id(微信id或群id)', + `fromd` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '流量来源(群聊名称)', + `sceneId` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '场景ID', + `companyId` int(11) NULL DEFAULT 0 COMMENT '账号所属项目id', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '修改时间', + `R` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0', + `F` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0', + `M` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_identifier_sourceId_sceneId`(`identifier`, `sourceId`, `sceneId`) USING BTREE, + INDEX `idx_identifier`(`identifier`) USING BTREE, + INDEX `idx_companyId`(`companyId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 564508 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量来源' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_traffic_source_package +-- ---------------------------- +DROP TABLE IF EXISTS `ck_traffic_source_package`; +CREATE TABLE `ck_traffic_source_package` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `userId` int(10) NULL DEFAULT NULL COMMENT '用户id', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述', + `pic` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '图标', + `companyId` int(11) NULL DEFAULT NULL COMMENT '账号所属项目id', + `matchingRules` json NULL COMMENT '匹配规则', + `isSys` tinyint(2) NULL DEFAULT 0 COMMENT '是否系统只有', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `updateTime` int(11) NULL DEFAULT NULL, + `createTime` int(11) NULL DEFAULT 0 COMMENT '创建时间', + `deleteTime` int(11) NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `companyId`(`companyId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量池包' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_traffic_source_package_item +-- ---------------------------- +DROP TABLE IF EXISTS `ck_traffic_source_package_item`; +CREATE TABLE `ck_traffic_source_package_item` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `packageId` int(10) NULL DEFAULT NULL COMMENT '流量包id', + `companyId` int(11) NULL DEFAULT NULL COMMENT '账号所属项目id', + `identifier` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '流量标识,可以是手机号、微信号', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `createTime` int(11) NULL DEFAULT 0 COMMENT '创建时间', + `deleteTime` int(10) NULL DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_packageId_companyId_identifier_isDel`(`packageId`, `companyId`, `identifier`, `isDel`) USING BTREE, + INDEX `packageId`(`packageId`) USING BTREE, + INDEX `companyId`(`companyId`) USING BTREE, + INDEX `identifier`(`identifier`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 34 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_traffic_tag +-- ---------------------------- +DROP TABLE IF EXISTS `ck_traffic_tag`; +CREATE TABLE `ck_traffic_tag` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `tagId` int(11) NULL DEFAULT NULL, + `tagName` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签名', + `tagType` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签值', + `tagValue` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签值', + `companyId` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '部门ID', + `trafficPoolId` int(10) NULL DEFAULT NULL COMMENT '流量池用户id traffic_pool的主键', + `createTime` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', + `isDel` tinyint(2) NULL DEFAULT NULL, + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量标签表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_user_flow_package +-- ---------------------------- +DROP TABLE IF EXISTS `ck_user_flow_package`; +CREATE TABLE `ck_user_flow_package` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `packageId` int(11) NOT NULL DEFAULT 0 COMMENT '套餐ID', + `orderId` int(11) NOT NULL DEFAULT 0 COMMENT '关联订单ID', + `duration` int(11) NOT NULL DEFAULT 1 COMMENT '套餐时长(月)', + `totalFlow` int(11) NOT NULL DEFAULT 0 COMMENT '总流量(人)', + `usedFlow` int(11) NOT NULL DEFAULT 0 COMMENT '已使用流量(人)', + `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态: 0=无效, 1=有效', + `startTime` int(11) NOT NULL DEFAULT 0 COMMENT '开始时间', + `expireTime` int(11) NOT NULL DEFAULT 0 COMMENT '到期时间', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_user_id`(`userId`) USING BTREE, + INDEX `idx_package_id`(`packageId`) USING BTREE, + INDEX `idx_order_id`(`orderId`) USING BTREE, + INDEX `idx_expire_time`(`expireTime`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户流量套餐表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_user_log +-- ---------------------------- +DROP TABLE IF EXISTS `ck_user_log`; +CREATE TABLE `ck_user_log` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `userName` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户名', + `action` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '操作类型', + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '操作描述', + `ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT 'IP地址', + `userAgent` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户设备信息', + `requestMethod` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '请求方法', + `requestUrl` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '请求URL', + `requestData` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '请求数据', + `responseCode` int(11) NULL DEFAULT 0 COMMENT '响应状态码', + `responseMsg` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '响应消息', + `createTime` int(11) NULL DEFAULT 0 COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_user_id`(`userId`) USING BTREE, + INDEX `idx_user_name`(`userName`) USING BTREE, + INDEX `idx_action`(`action`) USING BTREE, + INDEX `idx_create_time`(`createTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 45 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户操作日志表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_user_portrait +-- ---------------------------- +DROP TABLE IF EXISTS `ck_user_portrait`; +CREATE TABLE `ck_user_portrait` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `type` tinyint(2) NULL DEFAULT 0 COMMENT '类型 0浏览 1点击 2下单/购买 3注册 4互动', + `companyId` int(11) NULL DEFAULT 0, + `trafficPoolId` int(10) NULL DEFAULT NULL COMMENT '流量池用户id traffic_pool的主键', + `source` tinyint(2) NULL DEFAULT 0 COMMENT '来源 0本站 1老油条 2老坑爹', + `uniqueId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '来源网站唯一id', + `sourceData` json NULL COMMENT '来源网站数据', + `remark` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `count` int(10) NULL DEFAULT 1 COMMENT '统计次数(半小时内)', + `createTime` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '修改时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 17718 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户画像' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_users +-- ---------------------------- +DROP TABLE IF EXISTS `ck_users`; +CREATE TABLE `ck_users` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID', + `account` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号', + `username` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', + `phone` char(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '登录手机号', + `passwordMd5` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码', + `passwordLocal` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '本地密码', + `avatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'https://img.icons8.com/color/512/circled-user-male-skin-type-7.png' COMMENT '头像', + `isAdmin` tinyint(3) NULL DEFAULT 0 COMMENT '是否管理身份 1->是 0->否', + `companyId` int(10) UNSIGNED NOT NULL COMMENT '账号所属项目id', + `typeId` tinyint(3) NOT NULL DEFAULT -1 COMMENT '类型:运营后台/操盘手 传1 、 门店传2', + `status` tinyint(3) NULL DEFAULT 0 COMMENT '1->可用,0->禁用', + `s2_accountId` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'S2的用户账号id', + `balance` int(11) NULL DEFAULT 0 COMMENT '余额', + `tokens` int(11) NULL DEFAULT 0 COMMENT '算力余额', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '修改时间', + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1652 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_vendor_order +-- ---------------------------- +DROP TABLE IF EXISTS `ck_vendor_order`; +CREATE TABLE `ck_vendor_order` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '订单ID', + `orderNo` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '订单编号', + `userId` int(10) UNSIGNED NOT NULL COMMENT '用户ID', + `packageId` int(10) UNSIGNED NOT NULL COMMENT '套餐ID', + `packageName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '套餐名称', + `totalAmount` decimal(10, 2) NOT NULL COMMENT '订单总额', + `payAmount` decimal(10, 2) NOT NULL COMMENT '支付金额', + `advancePayment` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '预付款', + `status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '状态:0=待支付,1=已支付,2=已完成,3=已取消', + `payTime` int(11) NULL DEFAULT 0 COMMENT '支付时间', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `createTime` int(11) NOT NULL COMMENT '创建时间', + `updateTime` int(11) NOT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `orderNo`(`orderNo`) USING BTREE, + INDEX `userId`(`userId`) USING BTREE, + INDEX `packageId`(`packageId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '供应商订单表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_vendor_package +-- ---------------------------- +DROP TABLE IF EXISTS `ck_vendor_package`; +CREATE TABLE `ck_vendor_package` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '套餐ID', + `userId` int(11) NULL DEFAULT NULL COMMENT '用户id', + `companyId` int(11) NULL DEFAULT NULL COMMENT '公司id', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '套餐名称', + `originalPrice` decimal(10, 2) NOT NULL COMMENT '原价', + `price` decimal(10, 2) NOT NULL COMMENT '售价', + `discount` decimal(4, 2) NULL DEFAULT 0.00 COMMENT '折扣', + `advancePayment` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '预付款', + `tags` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签', + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '套餐描述', + `cover` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '封面图片', + `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态:0=下架,1=上架', + `createTime` int(11) NOT NULL COMMENT '创建时间', + `updateTime` int(11) NOT NULL COMMENT '更新时间', + `isDel` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '供应商套餐表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_vendor_project +-- ---------------------------- +DROP TABLE IF EXISTS `ck_vendor_project`; +CREATE TABLE `ck_vendor_project` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '项目ID', + `packageId` int(10) UNSIGNED NOT NULL COMMENT '套餐ID', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '项目名称', + `originalPrice` decimal(10, 2) NOT NULL COMMENT '原价', + `price` decimal(10, 2) NOT NULL COMMENT '售价', + `duration` int(11) NULL DEFAULT 0 COMMENT '项目时长(分钟)', + `image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '项目图片', + `detail` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '项目详情', + `createTime` int(11) NOT NULL COMMENT '创建时间', + `updateTime` int(11) NOT NULL COMMENT '更新时间', + `isDel` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE, + INDEX `packageId`(`packageId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '供应商套餐项目表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_wechat_account +-- ---------------------------- +DROP TABLE IF EXISTS `ck_wechat_account`; +CREATE TABLE `ck_wechat_account` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `s2_wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '微信账号id', + `wechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信ID', + `alias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信号', + `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', + `pyInitial` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '拼音首字母', + `quanPin` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '全拼', + `avatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像URL', + `gender` tinyint(1) NULL DEFAULT 0 COMMENT '性别 0->保密;1->男;2->女', + `region` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '地区', + `signature` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '个性签名', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '电话', + `country` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '国家', + `privince` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '省份', + `city` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '城市', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_wechatId`(`wechatId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 3097959 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_wechat_customer +-- ---------------------------- +DROP TABLE IF EXISTS `ck_wechat_customer`; +CREATE TABLE `ck_wechat_customer` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增id', + `wechatId` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信id', + `basic` json NULL COMMENT '保存基础信息', + `weight` json NULL COMMENT '保存权重信息', + `activity` json NULL COMMENT '保存账号活跃信息', + `friendShip` json NULL COMMENT '保存朋友关系信息', + `companyId` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '公司id', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_wechatId`(`wechatId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 153 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信客服信息' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_wechat_friendship +-- ---------------------------- +DROP TABLE IF EXISTS `ck_wechat_friendship`; +CREATE TABLE `ck_wechat_friendship` ( + `id` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '好友id', + `wechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '微信ID', + `tags` json NULL COMMENT '好友标签', + `memo` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '好友备注', + `ownerWechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '所有者微信ID', + `companyId` int(11) NULL DEFAULT NULL COMMENT '公司ID', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + UNIQUE INDEX `uk_owner_wechat_account`(`ownerWechatId`, `wechatId`) USING BTREE, + INDEX `idx_wechat_id`(`wechatId`) USING BTREE, + INDEX `idx_owner_wechat_id`(`ownerWechatId`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信好友表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_wechat_group +-- ---------------------------- +DROP TABLE IF EXISTS `ck_wechat_group`; +CREATE TABLE `ck_wechat_group` ( + `id` int(11) UNSIGNED NOT NULL COMMENT 'S2微信群id', + `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '微信账号ID', + `chatroomId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信群聊id', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群名称', + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群头像', + `companyId` int(11) NULL DEFAULT NULL COMMENT '项目id', + `ownerWechatId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '所有者微信ID', + `identifier` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群主(流量标识,可以是手机号、微信号)', + `createTime` int(11) UNSIGNED NULL DEFAULT NULL, + `updateTime` int(11) UNSIGNED NULL DEFAULT NULL, + `deleteTime` int(11) UNSIGNED NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_owner_chatroomId`(`chatroomId`, `ownerWechatId`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_wechat_group_member +-- ---------------------------- +DROP TABLE IF EXISTS `ck_wechat_group_member`; +CREATE TABLE `ck_wechat_group_member` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `identifier` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群成员(流量标识,可以是手机号、微信号)', + `chatroomId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群真实id', + `customerIs` tinyint(3) NULL DEFAULT 0 COMMENT '是否客服', + `companyId` int(11) NULL DEFAULT NULL COMMENT '项目id', + `groupId` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '所属群ID', + `createTime` int(11) UNSIGNED NULL DEFAULT 0, + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_identifier_chatroomId_groupId`(`identifier`, `chatroomId`, `groupId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 549847 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群成员' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_wechat_restricts +-- ---------------------------- +DROP TABLE IF EXISTS `ck_wechat_restricts`; +CREATE TABLE `ck_wechat_restricts` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `taskId` int(11) NULL DEFAULT NULL COMMENT '任务id', + `level` tinyint(3) UNSIGNED NULL DEFAULT 1 COMMENT '风险类型 1 普通 2 警告 3 错误', + `reason` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '风险原因', + `memo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '记录更详细的风险信息', + `wechatId` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信id', + `companyId` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '项目id', + `restrictTime` int(11) NULL DEFAULT NULL COMMENT '限制日期', + `recoveryTime` int(11) NULL DEFAULT NULL COMMENT '恢复日期', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1302 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信风险受限记录' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_wechat_tag +-- ---------------------------- +DROP TABLE IF EXISTS `ck_wechat_tag`; +CREATE TABLE `ck_wechat_tag` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `tags` json NULL COMMENT '标签JSON', + `wechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '微信ID', + `companyId` int(11) NULL DEFAULT NULL COMMENT '公司ID', + `createTime` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_wechatId`(`wechatId`) USING BTREE, + INDEX `idx_companyId`(`companyId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 123366 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench`; +CREATE TABLE `ck_workbench` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `userId` int(11) NOT NULL COMMENT '创建用户ID', + `companyId` int(11) NULL DEFAULT 0 COMMENT '公司id', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '工作台名称', + `type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '工作台类型:1=自动点赞,2=朋友圈同步,3=群消息推送,4=自动建群,5=流量分发,6=通讯录导入', + `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态:0=禁用,1=启用', + `autoStart` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否自动启动:0=否,1=是', + `createTime` int(11) NOT NULL COMMENT '创建时间', + `updateTime` int(11) NOT NULL COMMENT '更新时间', + `isDel` tinyint(1) NULL DEFAULT 0, + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_user_id`(`userId`) USING BTREE, + INDEX `idx_type`(`type`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 275 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '工作台主表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_auto_like +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_auto_like`; +CREATE TABLE `ck_workbench_auto_like` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `interval` int(11) NOT NULL DEFAULT 60 COMMENT '点赞间隔(秒)', + `maxLikes` int(11) NOT NULL DEFAULT 100 COMMENT '最大点赞数', + `startTime` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '00:00:00' COMMENT '开始时间', + `endTime` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '23:59:59' COMMENT '结束时间', + `contentTypes` json NULL COMMENT '内容类型', + `devices` json NULL COMMENT '设备列表,JSON格式:[{\"id\":1,\"name\":\"设备1\"},{\"id\":2,\"name\":\"设备2\"}]', + `friends` json NULL COMMENT '用户列表', + `createTime` int(11) NOT NULL COMMENT '创建时间', + `updateTime` int(11) NOT NULL COMMENT '更新时间', + `targetGroups` json NULL COMMENT '目标用户组列表,JSON格式:[{\"id\":1,\"name\":\"用户组1\"},{\"id\":2,\"name\":\"用户组2\"}] 废除', + `tagOperator` tinyint(1) NULL DEFAULT 2 COMMENT '标签匹配规则 1:and 2:or 废除', + `friendMaxLikes` int(10) NULL DEFAULT NULL COMMENT '好友最大点赞数', + `friendTags` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '好友标签', + `enableFriendTags` tinyint(1) NULL DEFAULT 0 COMMENT '启用好友标签', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_workbench_id`(`workbenchId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 54 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '自动点赞配置表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_auto_like_item +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_auto_like_item`; +CREATE TABLE `ck_workbench_auto_like_item` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `deviceId` int(11) NULL DEFAULT 0 COMMENT '设备id', + `snsId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '自动点赞id', + `wechatFriendId` int(11) NULL DEFAULT NULL COMMENT '好友id', + `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id', + `momentsId` int(11) NULL DEFAULT NULL COMMENT '朋友圈id', + `createTime` int(11) NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `workbenchId`(`workbenchId`) USING BTREE, + INDEX `wechatFriendId`(`wechatFriendId`) USING BTREE, + INDEX `wechatAccountId`(`wechatAccountId`) USING BTREE, + INDEX `momentsId`(`momentsId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4639 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '工作台-自动点赞记录' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_group_create +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_group_create`; +CREATE TABLE `ck_workbench_group_create` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `workbenchId` int(11) NOT NULL COMMENT '计划ID', + `devices` json NULL COMMENT '目标设备/客服(JSON数组)', + `poolGroups` json NULL COMMENT '流量池JSON', + `wechatGroups` json NULL COMMENT '微信客服JSON', + `startTime` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '开始时间', + `endTime` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '结束时间', + `groupSizeMin` int(10) NULL DEFAULT NULL COMMENT '群好友最小人数', + `groupSizeMax` int(10) NULL DEFAULT NULL COMMENT '群好友最大人数', + `maxGroupsPerDay` int(10) NULL DEFAULT NULL COMMENT '每日建群最大数量', + `groupNameTemplate` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群模板信息', + `groupDescription` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群描述', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 26 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_group_create_item +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_group_create_item`; +CREATE TABLE `ck_workbench_group_create_item` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `friendId` int(11) NULL DEFAULT NULL, + `wechatId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '微信id', + `groupId` int(10) NULL DEFAULT NULL COMMENT '群id', + `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id', + `createTime` int(11) NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 46 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_group_push +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_group_push`; +CREATE TABLE `ck_workbench_group_push` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `pushType` tinyint(1) NOT NULL DEFAULT 1 COMMENT '推送方式 0定时 1立即', + `targetType` tinyint(1) NOT NULL DEFAULT 1 COMMENT '推送目标类型:1=群推送,2=好友推送', + `startTime` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '推送开始时间', + `endTime` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '推送结束时间', + `maxPerDay` int(11) NULL DEFAULT 0 COMMENT '每日推送条数', + `pushOrder` tinyint(1) NULL DEFAULT 1 COMMENT '推送顺序 1最早 2最新', + `isLoop` tinyint(1) NULL DEFAULT 0 COMMENT '是否循环推送 0否 1是', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '是否启用 0否 1是', + `groups` json NULL COMMENT '推送微信群组(JSON)', + `friends` json NULL COMMENT '推送好友列表(JSON)', + `ownerWechatIds` json NULL COMMENT '所属微信id', + `contentLibraries` json NULL COMMENT '内容库(JSON)', + `friendIntervalMin` int(11) NOT NULL DEFAULT 10 COMMENT '好友间最小间隔时间(秒)', + `friendIntervalMax` int(11) NOT NULL DEFAULT 20 COMMENT '好友间最大间隔时间(秒)', + `messageIntervalMin` int(11) NOT NULL DEFAULT 1 COMMENT '消息间最小间隔时间(秒)', + `messageIntervalMax` int(11) NOT NULL DEFAULT 12 COMMENT '消息间最大间隔时间(秒)', + `isRandomTemplate` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否随机选择话术组(0=否,1=是)', + `postPushTags` json NOT NULL COMMENT '推送完成后打标签', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + `socialMediaId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '京东导购媒体', + `promotionSiteId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '京东广告位', + `trafficPools` json NULL COMMENT '流量池', + `devices` json NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_workbench_id`(`workbenchId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 32 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '群消息推送扩展表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_group_push_item +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_group_push_item`; +CREATE TABLE `ck_workbench_group_push_item` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `targetType` tinyint(1) NOT NULL DEFAULT 1 COMMENT '推送目标类型:1=群,2=好友', + `contentId` int(11) NULL DEFAULT 0 COMMENT '内容库is', + `groupId` int(10) NULL DEFAULT NULL COMMENT '群id', + `friendId` int(11) NULL DEFAULT NULL COMMENT '好友ID(当targetType=2时使用)', + `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id', + `isLoop` tinyint(2) NULL DEFAULT 0 COMMENT '是否循环完成', + `createTime` int(11) NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 302 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_import_contact +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_import_contact`; +CREATE TABLE `ck_workbench_import_contact` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `devices` json NULL COMMENT '设备id', + `pools` json NULL COMMENT '流量池', + `num` int(11) NULL DEFAULT NULL COMMENT '分配数量', + `clearContact` tinyint(2) NULL DEFAULT 0 COMMENT '是否清除现有联系人', + `remarkType` tinyint(2) NOT NULL DEFAULT 0 COMMENT '备注类型 0不备注 1年月日 2月日 3自定义', + `remark` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `startTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '开始时间', + `endTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '结束时间', + `createTime` int(11) NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_import_contact_item +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_import_contact_item`; +CREATE TABLE `ck_workbench_import_contact_item` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `deviceId` int(11) NULL DEFAULT NULL COMMENT '设备id', + `packageId` int(11) NULL DEFAULT 0 COMMENT '流量包id', + `poolId` int(11) NULL DEFAULT NULL COMMENT '流量id', + `createTime` int(11) NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 140 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_moments_sync +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_moments_sync`; +CREATE TABLE `ck_workbench_moments_sync` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `syncInterval` int(11) NOT NULL DEFAULT 1 COMMENT '同步间隔(小时)', + `syncCount` int(11) NOT NULL DEFAULT 5 COMMENT '每日同步数量', + `syncType` tinyint(1) NOT NULL DEFAULT 1 COMMENT '同步类型:1=文本,2=图片,3=视频,4=链接', + `startTime` time(0) NULL DEFAULT '06:00:00' COMMENT '发布开始时间', + `endTime` time(0) NULL DEFAULT '23:59:00' COMMENT '发布结束时间', + `accountType` tinyint(1) NOT NULL DEFAULT 1 COMMENT '账号类型:1=业务号,2=个人号', + `devices` json NOT NULL COMMENT '设备列表,JSON格式', + `contentLibraries` json NULL COMMENT '内容库ID列表,JSON格式', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_workbench_id`(`workbenchId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 47 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '朋友圈同步配置' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_moments_sync_item +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_moments_sync_item`; +CREATE TABLE `ck_workbench_moments_sync_item` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `deviceId` int(11) NULL DEFAULT 0 COMMENT '设备id', + `contentId` int(10) NULL DEFAULT NULL COMMENT '内容库id', + `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id', + `createTime` int(11) NOT NULL COMMENT '创建时间', + `isLoop` tinyint(2) NULL DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1650 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '朋友圈同步配置' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_traffic_config +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_traffic_config`; +CREATE TABLE `ck_workbench_traffic_config` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '流量分发计划ID', + `distributeType` tinyint(1) NOT NULL DEFAULT 1 COMMENT '分配方式 1均分 2优先级 3比例', + `maxPerDay` int(11) NOT NULL DEFAULT 0 COMMENT '每日最大分配量', + `timeType` tinyint(1) NOT NULL DEFAULT 1 COMMENT '时间限制 1全天 2自定义', + `startTime` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '开始时间', + `endTime` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '结束时间', + `account` json NULL COMMENT '分发的账号', + `devices` json NULL COMMENT '目标设备/客服(JSON数组)', + `pools` json NULL COMMENT '流量池(JSON数组)', + `exp` int(10) NULL DEFAULT 30 COMMENT '有效期 单位天', + `createTime` int(11) NOT NULL, + `updateTime` int(11) NOT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uniq_workbench`(`workbenchId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 31 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量分发计划扩展表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_traffic_config_item +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_traffic_config_item`; +CREATE TABLE `ck_workbench_traffic_config_item` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL DEFAULT 0 COMMENT '工作台ID', + `deviceId` int(11) NULL DEFAULT 0 COMMENT '设备id', + `wechatFriendId` int(10) NULL DEFAULT NULL COMMENT '好友id', + `wechatAccountId` int(11) NULL DEFAULT 0 COMMENT '客服id', + `expTime` int(11) NULL DEFAULT 0 COMMENT '有效时间', + `exp` int(11) NULL DEFAULT 0 COMMENT '有效时间 天', + `isRecycle` tinyint(2) NULL DEFAULT 0 COMMENT '是否回收', + `recycleTime` int(11) NULL DEFAULT 0 COMMENT '回收时间', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `workbenchId`(`workbenchId`) USING BTREE, + INDEX `deviceId`(`deviceId`) USING BTREE, + INDEX `wechatFriendId`(`wechatFriendId`) USING BTREE, + INDEX `wechatAccountId`(`wechatAccountId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 49898 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量分发计划扩展表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_allot_rule +-- ---------------------------- +DROP TABLE IF EXISTS `s2_allot_rule`; +CREATE TABLE `s2_allot_rule` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '规则ID', + `departmentId` int(11) NULL DEFAULT 0 COMMENT '部门id', + `tenantId` int(11) NOT NULL DEFAULT 0 COMMENT '租户ID', + `allotType` tinyint(4) NOT NULL DEFAULT 0 COMMENT '分配类型', + `allotOnline` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否在线分配', + `kefuRange` tinyint(4) NOT NULL DEFAULT 0 COMMENT '客服范围', + `wechatRange` tinyint(4) NOT NULL DEFAULT 0 COMMENT '微信范围', + `kefuData` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '客服数据JSON', + `wechatData` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '微信ID列表JSON', + `labels` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '标签JSON', + `priorityStrategy` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '优先级策略JSON', + `sortIndex` int(11) NOT NULL DEFAULT 0 COMMENT '排序索引', + `creatorAccountId` int(11) NOT NULL DEFAULT 0 COMMENT '创建者账号ID', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `ruleName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '规则名称', + `isDel` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_tenant`(`tenantId`) USING BTREE, + INDEX `idx_sort`(`sortIndex`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2176 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '分配规则表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_call_recording +-- ---------------------------- +DROP TABLE IF EXISTS `s2_call_recording`; +CREATE TABLE `s2_call_recording` ( + `id` bigint(20) NOT NULL COMMENT '主键ID', + `tenantId` bigint(20) NOT NULL DEFAULT 0 COMMENT '租户ID', + `deviceOwnerId` bigint(20) NOT NULL DEFAULT 0 COMMENT '设备所有者ID', + `userName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户名', + `nickname` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '昵称', + `realName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '真实姓名', + `deviceMemo` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '设备备注', + `fileName` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '文件名', + `imei` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '设备IMEI', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '电话号码', + `isCallOut` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否为呼出电话(0:呼入,1:呼出)', + `beginTime` int(11) NOT NULL DEFAULT 0 COMMENT '通话开始时间戳', + `endTime` int(11) NOT NULL DEFAULT 0 COMMENT '通话结束时间戳', + `audioUrl` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '录音文件URL', + `mp3AudioUrl` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'MP3录音文件URL', + `callBeginTime` int(11) NOT NULL DEFAULT 0 COMMENT '呼叫开始时间戳', + `callLogId` bigint(20) NOT NULL DEFAULT 0 COMMENT '通话日志ID', + `callType` int(11) NOT NULL DEFAULT 0 COMMENT '通话类型', + `duration` int(11) NOT NULL DEFAULT 0 COMMENT '通话时长(秒)', + `skipReason` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '跳过原因', + `skipUpload` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否跳过上传', + `isDeleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已删除', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间戳', + `lastUpdateTime` int(11) NOT NULL DEFAULT 0 COMMENT '最后更新时间戳', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_tenant_id`(`tenantId`) USING BTREE, + INDEX `idx_device_owner_id`(`deviceOwnerId`) USING BTREE, + INDEX `idx_user_name`(`userName`) USING BTREE, + INDEX `idx_phone`(`phone`) USING BTREE, + INDEX `idx_begin_time`(`beginTime`) USING BTREE, + INDEX `idx_end_time`(`endTime`) USING BTREE, + INDEX `idx_call_begin_time`(`callBeginTime`) USING BTREE, + INDEX `idx_imei`(`imei`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '通话记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_company_account +-- ---------------------------- +DROP TABLE IF EXISTS `s2_company_account`; +CREATE TABLE `s2_company_account` ( + `id` int(11) NULL DEFAULT NULL COMMENT 'id', + `tenantId` int(11) NULL DEFAULT NULL, + `userName` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户名', + `realName` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '真实姓名', + `nickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '昵称', + `memo` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '备注', + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '头像', + `secret` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '密钥', + `accountType` int(11) NULL DEFAULT 0 COMMENT '账户类型', + `departmentId` int(11) NULL DEFAULT 0 COMMENT '部门ID', + `departmentName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门名称', + `useGoogleSecretKey` tinyint(1) NULL DEFAULT 0 COMMENT '是否使用谷歌密钥', + `hasVerifyGoogleSecret` tinyint(1) NULL DEFAULT 0 COMMENT '是否验证谷歌密钥', + `passwordMd5` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT 'MD5加密密码', + `passwordLocal` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '本地加密密码', + `lastLoginIp` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '最后登录IP', + `lastLoginTime` int(11) NULL DEFAULT 0 COMMENT '最后登录时间', + `createTime` int(11) NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT 0 COMMENT '更新时间', + `privilegeIds` json NULL COMMENT '权限', + `alive` tinyint(1) NULL DEFAULT NULL, + `creator` int(10) NULL DEFAULT NULL COMMENT '创建者', + `creatorRealName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建者真实姓名', + `creatorUserName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建者用户名', + `status` tinyint(1) NULL DEFAULT 0 COMMENT '状态 0正常 1禁用', + UNIQUE INDEX `idx_username`(`userName`) USING BTREE, + INDEX `idx_create_time`(`createTime`) USING BTREE, + INDEX `idx_update_time`(`updateTime`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '公司账户表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_department +-- ---------------------------- +DROP TABLE IF EXISTS `s2_department`; +CREATE TABLE `s2_department` ( + `id` int(11) NOT NULL, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称', + `memo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `tenantId` int(11) NULL DEFAULT NULL, + `isTop` tinyint(1) NULL DEFAULT 0, + `level` int(10) NULL DEFAULT 0, + `parentId` int(10) NULL DEFAULT 0, + `privileges` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, + `createTime` int(11) NULL DEFAULT NULL, + `lastUpdateTime` int(11) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '部门表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_device +-- ---------------------------- +DROP TABLE IF EXISTS `s2_device`; +CREATE TABLE `s2_device` ( + `id` int(11) NULL DEFAULT NULL COMMENT '设备真实ID', + `userName` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户名', + `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', + `realName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '真实姓名', + `groupName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分组名称', + `wechatAccounts` json NULL COMMENT '微信账号列表JSON', + `alive` tinyint(1) NULL DEFAULT 0 COMMENT '是否在线', + `lastAliveTime` int(11) NULL DEFAULT NULL COMMENT '最后在线时间', + `tenantId` int(11) NULL DEFAULT NULL COMMENT '租户ID', + `groupId` int(11) NULL DEFAULT NULL COMMENT '分组ID', + `currentAccountId` int(11) NULL DEFAULT NULL COMMENT '当前账号ID', + `imei` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '设备IMEI', + `deviceImei` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '设备本地IMEI', + `memo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `isDeleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除', + `deletedAndStop` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除并停止', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + `rooted` tinyint(1) NULL DEFAULT 0 COMMENT '是否root', + `xPosed` tinyint(1) NULL DEFAULT 0 COMMENT '是否安装xposed', + `brand` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '品牌', + `model` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '型号', + `operatingSystem` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作系统版本', + `softwareVersion` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '软件版本', + `extra` json NULL COMMENT '额外信息JSON', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号', + `lastUpdateTime` int(11) NULL DEFAULT NULL COMMENT '最后更新时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + `taskConfig` json NULL COMMENT '自动化任务开关 \r\nautoLike:自动点赞\r\nmomentsSync:朋友圈同步\r\nautoCustomerDev:自动开发客户\r\ngroupMessageDeliver:群消息推送\r\nautoGroup:自动建群', + UNIQUE INDEX `uk_imei`(`imei`) USING BTREE, + INDEX `idx_tenant`(`tenantId`) USING BTREE, + INDEX `idx_group`(`groupId`) USING BTREE, + INDEX `idx_current_account`(`currentAccountId`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_device_group +-- ---------------------------- +DROP TABLE IF EXISTS `s2_device_group`; +CREATE TABLE `s2_device_group` ( + `id` int(11) NOT NULL, + `tenantId` int(11) NOT NULL COMMENT '租户ID', + `groupName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '分组名称', + `groupMemo` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分组备注', + `count` int(11) NULL DEFAULT 0 COMMENT '设备数量', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + INDEX `idx_tenant`(`tenantId`) USING BTREE, + INDEX `idx_group_name`(`groupName`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备分组表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_friend_task +-- ---------------------------- +DROP TABLE IF EXISTS `s2_friend_task`; +CREATE TABLE `s2_friend_task` ( + `id` int(11) NOT NULL COMMENT '任务ID', + `tenantId` int(11) NULL DEFAULT 0 COMMENT '租户ID', + `operatorAccountId` int(11) NULL DEFAULT 0 COMMENT '操作账号ID', + `status` int(11) NULL DEFAULT 1 COMMENT '状态:0执行中,1执行成功,2执行失败', + `phone` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号/微信号', + `msgContent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '验证消息', + `wechatAccountId` int(11) NULL DEFAULT 0 COMMENT '微信账号ID', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间戳', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `extra` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '额外数据JSON', + `labels` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签,逗号分隔', + `from` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '来源', + `alias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信账号别名', + `wechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信ID', + `wechatAvatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信头像', + `wechatNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信昵称', + `accountNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号昵称', + `accountRealName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号真实姓名', + `accountUsername` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号用户名', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间戳', + UNIQUE INDEX `uk_task_id`(`id`) USING BTREE, + INDEX `idx_tenant_id`(`tenantId`) USING BTREE, + INDEX `idx_operator_account_id`(`operatorAccountId`) USING BTREE, + INDEX `idx_wechat_account_id`(`wechatAccountId`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_phone`(`phone`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '添加好友任务记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_moments_item +-- ---------------------------- +DROP TABLE IF EXISTS `s2_moments_item`; +CREATE TABLE `s2_moments_item` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `task_id` int(11) NOT NULL COMMENT '朋友圈任务ID', + `temp_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '临时ID', + `wechat_account_id` int(11) NULL DEFAULT NULL COMMENT '微信账号ID', + `execute_count` int(11) NULL DEFAULT 0 COMMENT '执行次数', + `executed` tinyint(1) NULL DEFAULT 0 COMMENT '是否已执行', + `status` tinyint(1) NULL DEFAULT 0 COMMENT '状态', + `extra` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '额外信息', + `execute_time` int(11) NULL DEFAULT NULL COMMENT '执行时间', + `finished_time` int(11) NULL DEFAULT NULL COMMENT '完成时间', + `labels` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '标签', + `alt_list` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '替代列表', + `comments` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '评论', + `moment_content_type` tinyint(1) NULL DEFAULT 0 COMMENT '朋友圈内容类型', + `text` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '文本内容', + `pic_url_list` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '图片URL列表', + `video_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '视频URL', + `link` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '链接信息', + `is_use_location` tinyint(1) NULL DEFAULT 0 COMMENT '是否使用位置', + `lat` decimal(10, 6) NULL DEFAULT 0.000000 COMMENT '纬度', + `lng` decimal(10, 6) NULL DEFAULT 0.000000 COMMENT '经度', + `poi_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '位置名称', + `poi_address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '位置地址', + `video_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '视频编号', + `created_at` int(11) NULL DEFAULT NULL COMMENT '记录创建时间', + `updated_at` int(11) NULL DEFAULT NULL COMMENT '记录更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `idx_task_temp`(`task_id`, `temp_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 184 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '朋友圈任务项表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_moments_task +-- ---------------------------- +DROP TABLE IF EXISTS `s2_moments_task`; +CREATE TABLE `s2_moments_task` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `task_id` int(11) NOT NULL COMMENT '朋友圈任务ID', + `tenant_id` int(11) NULL DEFAULT NULL COMMENT '租户ID', + `operator_account_id` int(11) NULL DEFAULT NULL COMMENT '操作人账号ID', + `account_username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号用户名', + `account_nickname` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号昵称', + `account_real_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号真实姓名', + `public_mode` tinyint(1) NULL DEFAULT 0 COMMENT '发布模式', + `moment_content_type` tinyint(1) NULL DEFAULT 1 COMMENT '朋友圈内容类型:1纯文本,2图片,3视频,4链接', + `text` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '文本内容', + `pic_url_list` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '图片URL列表', + `video_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '视频URL', + `link` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '链接信息', + `job_status` tinyint(1) NULL DEFAULT 0 COMMENT '任务状态', + `job_origin_status` tinyint(1) NULL DEFAULT 0 COMMENT '任务原始状态', + `job_group` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '任务组', + `job_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '任务名称', + `begin_time` int(11) NULL DEFAULT NULL COMMENT '开始时间', + `end_time` int(11) NULL DEFAULT NULL COMMENT '结束时间', + `timing_time` int(11) NULL DEFAULT NULL COMMENT '定时发布时间', + `create_time` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `immediately` tinyint(1) NULL DEFAULT 1 COMMENT '是否立即发布', + `created_at` int(11) NULL DEFAULT NULL COMMENT '记录创建时间', + `updated_at` int(11) NULL DEFAULT NULL COMMENT '记录更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `idx_task_id`(`task_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 88 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '朋友圈任务表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_reply +-- ---------------------------- +DROP TABLE IF EXISTS `s2_reply`; +CREATE TABLE `s2_reply` ( + `id` int(11) NOT NULL, + `tenantId` int(255) NULL DEFAULT NULL, + `groupId` int(11) NULL DEFAULT NULL, + `accountId` int(11) NULL DEFAULT NULL, + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `msgType` tinyint(2) NULL DEFAULT NULL, + `content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `createTime` int(11) NULL DEFAULT NULL, + `lastUpdateTime` int(11) NULL DEFAULT NULL, + `sortIndex` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '快捷回复' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_reply_group +-- ---------------------------- +DROP TABLE IF EXISTS `s2_reply_group`; +CREATE TABLE `s2_reply_group` ( + `id` int(11) NOT NULL, + `groupName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `sortIndex` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `parentId` int(11) NULL DEFAULT NULL, + `replyType` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `replys` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `departmentId` int(11) NULL DEFAULT 2130, + `accountId` int(11) NULL DEFAULT 5150, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '快捷回复分组' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_account +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_account`; +CREATE TABLE `s2_wechat_account` ( + `id` int(11) NOT NULL COMMENT '微信账号ID', + `wechatId` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '微信ID', + `deviceAccountId` int(11) NULL DEFAULT 0 COMMENT '设备账号ID', + `imei` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'IMEI', + `deviceMemo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '设备备注', + `accountUserName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号用户名', + `accountRealName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号真实姓名', + `accountNickname` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号昵称', + `keFuAlive` tinyint(1) NULL DEFAULT 0 COMMENT '客服是否在线', + `deviceAlive` tinyint(1) NULL DEFAULT 0 COMMENT '设备是否在线', + `wechatAlive` tinyint(1) NULL DEFAULT 0 COMMENT '微信是否在线', + `yesterdayMsgCount` int(11) NULL DEFAULT 0 COMMENT '昨日消息数', + `sevenDayMsgCount` int(11) NULL DEFAULT 0 COMMENT '7天消息数', + `thirtyDayMsgCount` int(11) NULL DEFAULT 0 COMMENT '30天消息数', + `totalFriend` int(11) NULL DEFAULT 0 COMMENT '总好友数', + `maleFriend` int(11) NULL DEFAULT 0 COMMENT '男性好友数', + `unknowFriend` int(11) NULL DEFAULT NULL COMMENT '未知好友数', + `femaleFriend` int(11) NULL DEFAULT 0 COMMENT '女性好友数', + `wechatGroupName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信群组名称', + `tenantId` int(11) NULL DEFAULT NULL COMMENT '租户ID', + `nickname` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', + `alias` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '别名', + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像', + `gender` tinyint(1) NULL DEFAULT 0 COMMENT '性别', + `region` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '地区', + `signature` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '签名', + `bindQQ` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '绑定QQ', + `bindEmail` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '绑定邮箱', + `bindMobile` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '绑定手机', + `currentDeviceId` int(11) NULL DEFAULT 0 COMMENT '当前设备ID', + `isDeleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + `groupId` int(11) NULL DEFAULT 0 COMMENT '分组ID', + `memo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `wechatVersion` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信版本', + `labels` json NULL COMMENT '标签', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + `status` tinyint(3) NULL DEFAULT 1 COMMENT '状态值', + INDEX `idx_wechat_id`(`wechatId`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_chatroom +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_chatroom`; +CREATE TABLE `s2_wechat_chatroom` ( + `id` int(11) NOT NULL, + `wechatAccountId` int(11) NOT NULL COMMENT '微信账号ID', + `wechatAccountAlias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信账号别名', + `wechatAccountWechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信账号微信ID', + `wechatAccountAvatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信账号头像', + `wechatAccountNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信账号昵称', + `chatroomId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '群聊ID', + `hasMe` tinyint(1) NULL DEFAULT 0 COMMENT '是否包含自己', + `chatroomOwnerNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群主昵称', + `chatroomOwnerAvatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群主头像', + `conRemark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群聊名称', + `pyInitial` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '拼音首字母', + `quanPin` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '全拼', + `chatroomAvatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群头像', + `isDeleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `accountId` int(11) NULL DEFAULT 0 COMMENT '账号ID', + `accountUserName` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号用户名', + `accountRealName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号真实姓名', + `accountNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号昵称', + `groupId` int(11) NULL DEFAULT 0 COMMENT '分组ID', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + UNIQUE INDEX `uk_chatroom_account`(`chatroomId`, `wechatAccountId`) USING BTREE, + INDEX `wechatAccountId`(`wechatAccountId`) USING BTREE, + INDEX `chatroomId`(`chatroomId`) USING BTREE, + INDEX `wechatAccountWechatId`(`wechatAccountWechatId`) USING BTREE, + INDEX `idx_account_deleted`(`accountId`, `isDeleted`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_chatroom_member +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_chatroom_member`; +CREATE TABLE `s2_wechat_chatroom_member` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `chatroomId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '群聊ID', + `wechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '微信ID', + `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', + `avatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像', + `conRemark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `alias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '别名', + `friendType` tinyint(11) NULL DEFAULT 0 COMMENT '好友类型', + `createTime` int(10) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(10) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_chatroom_wechat`(`chatroomId`, `wechatId`) USING BTREE, + INDEX `chatroomId`(`chatroomId`) USING BTREE, + INDEX `wechatId`(`wechatId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 495043 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群成员表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_friend +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_friend`; +CREATE TABLE `s2_wechat_friend` ( + `id` int(11) NULL DEFAULT NULL COMMENT '好友id', + `wechatAccountId` int(11) NOT NULL COMMENT '所有者微信账号ID', + `alias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '好友微信号', + `wechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '好友微信ID', + `conRemark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注名', + `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', + `pyInitial` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '拼音首字母', + `quanPin` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '全拼', + `avatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像URL', + `gender` tinyint(1) NULL DEFAULT 0 COMMENT '性别', + `region` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '地区', + `addFrom` int(11) NULL DEFAULT NULL COMMENT '添加来源', + `labels` json NULL COMMENT '标签JSON', + `siteLabels` json NULL COMMENT '站内标签JSON', + `signature` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '个性签名', + `isDeleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除', + `isPassed` tinyint(1) NULL DEFAULT 1 COMMENT '是否通过', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + `accountId` int(11) NULL DEFAULT 0 COMMENT '账号ID', + `extendFields` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '扩展字段JSON', + `accountUserName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号用户名', + `accountRealName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号真实姓名', + `accountNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号昵称', + `ownerAlias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '所有者别名', + `ownerWechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '所有者微信ID', + `ownerNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '所有者昵称', + `ownerAvatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '所有者头像', + `phone` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '电话', + `thirdParty` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '第三方数据JSON', + `groupId` int(11) NULL DEFAULT 0 COMMENT '分组ID', + `passTime` int(11) NULL DEFAULT NULL COMMENT '通过时间', + `additionalPicture` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '附加图片', + `desc` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '描述', + `country` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '国家', + `privince` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '省份', + `city` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '城市', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + `R` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0', + `F` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0', + `M` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0', + UNIQUE INDEX `uk_owner_wechat_account`(`ownerWechatId`, `wechatId`, `wechatAccountId`) USING BTREE, + INDEX `idx_wechat_account_id`(`wechatAccountId`) USING BTREE, + INDEX `idx_wechat_id`(`wechatId`) USING BTREE, + INDEX `idx_owner_wechat_id`(`ownerWechatId`) USING BTREE, + INDEX `idx_id`(`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信好友表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_group +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_group`; +CREATE TABLE `s2_wechat_group` ( + `id` int(11) NOT NULL, + `tenantId` int(11) NULL DEFAULT NULL, + `groupName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `groupMemo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `groupType` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `sortIndex` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `groupOwnerType` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `departmentId` int(11) NULL DEFAULT NULL, + `accountId` int(11) NULL DEFAULT NULL, + `createTime` int(11) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_message +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_message`; +CREATE TABLE `s2_wechat_message` ( + `id` bigint(20) NOT NULL COMMENT '消息ID', + `type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '消息类型 1好友 2群', + `wechatFriendId` bigint(20) NULL DEFAULT NULL COMMENT '微信好友ID', + `wechatChatroomId` bigint(20) NOT NULL COMMENT '微信群聊ID', + `senderNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '发送者昵称', + `senderWechatId` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '发送者微信ID', + `senderIsAdmin` tinyint(1) NULL DEFAULT 0 COMMENT '发送者是否管理员', + `senderIsDeleted` tinyint(1) NULL DEFAULT 0 COMMENT '发送者是否已删除', + `senderChatroomNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '发送者群昵称', + `senderWechatAccountId` bigint(20) NULL DEFAULT NULL COMMENT '发送者微信账号ID', + `wechatAccountId` bigint(20) NULL DEFAULT NULL COMMENT '微信账号ID', + `tenantId` bigint(20) NULL DEFAULT NULL COMMENT '租户ID', + `accountId` bigint(20) NULL DEFAULT NULL COMMENT '账号ID', + `synergyAccountId` bigint(20) NULL DEFAULT 0 COMMENT '协同账号ID', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '消息内容', + `originalContent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '消息内容(原版)', + `msgType` int(11) NULL DEFAULT NULL COMMENT '消息类型 1 文字 3图片 47动态图片 34语言 43视频 42名片 40/20链接 49文件 419430449转账 436207665红包', + `msgSubType` int(11) NULL DEFAULT 0 COMMENT '消息子类型', + `msgSvrId` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '消息服务器ID', + `isSend` tinyint(1) NULL DEFAULT 1 COMMENT '是否发送', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `isDeleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否已删除', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + `sendStatus` int(11) NULL DEFAULT 0 COMMENT '发送状态', + `wechatTime` int(11) NULL DEFAULT NULL COMMENT '微信时间', + `origin` int(11) NULL DEFAULT 0 COMMENT '来源', + `msgId` bigint(20) NULL DEFAULT NULL COMMENT '消息ID', + `recallId` tinyint(1) NULL DEFAULT 0 COMMENT '撤回ID', + `isRead` tinyint(1) NULL DEFAULT 0 COMMENT '是否读取', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_wechatChatroomId`(`wechatChatroomId`) USING BTREE, + INDEX `idx_wechatAccountId`(`wechatAccountId`) USING BTREE, + INDEX `idx_msgSvrId`(`msgSvrId`) USING BTREE, + INDEX `idx_type`(`type`) USING BTREE, + INDEX `idx_type_wechatTime`(`type`, `wechatTime`, `id`) USING BTREE, + INDEX `idx_friend_time`(`wechatFriendId`, `wechatTime`, `id`) USING BTREE, + INDEX `idx_chatroom_time`(`wechatChatroomId`, `wechatTime`, `id`) USING BTREE, + INDEX `idx_account_type`(`accountId`, `type`, `wechatTime`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群聊消息记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_moments +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_moments`; +CREATE TABLE `s2_wechat_moments` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `wechatAccountId` int(11) NOT NULL COMMENT '微信账号ID', + `wechatFriendId` int(11) NULL DEFAULT NULL COMMENT '微信好友ID', + `snsId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '朋友圈消息ID', + `commentList` json NULL COMMENT '评论列表JSON', + `createTime` bigint(20) NULL DEFAULT 0 COMMENT '创建时间戳', + `likeList` json NULL COMMENT '点赞列表JSON', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '朋友圈内容', + `lat` decimal(10, 6) NULL DEFAULT 0.000000 COMMENT '纬度', + `lng` decimal(10, 6) NULL DEFAULT 0.000000 COMMENT '经度', + `location` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '位置信息', + `picSize` int(11) NULL DEFAULT 0 COMMENT '图片大小', + `resUrls` json NULL COMMENT '资源URL列表', + `userName` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户名', + `type` int(11) NULL DEFAULT 0 COMMENT '朋友圈类型', + `create_time` int(11) NULL DEFAULT NULL COMMENT '数据创建时间', + `update_time` int(11) NULL DEFAULT NULL COMMENT '数据更新时间', + `coverImage` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `urls` json NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `idx_sns_account`(`snsId`, `wechatAccountId`) USING BTREE, + INDEX `idx_account_friend`(`wechatAccountId`, `wechatFriendId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 39669 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信朋友圈数据表' ROW_FORMAT = Dynamic; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/Server/微信健康分规则v2.md b/Server/微信健康分规则v2.md new file mode 100644 index 00000000..90c63f07 --- /dev/null +++ b/Server/微信健康分规则v2.md @@ -0,0 +1,58 @@ +# 微信健康分规则 v2 + +## 一、定义 + +当客户收到手机设备后,登录了微信号,我们将对其微信号进行健康分的评估。 + +**健康分 = 基础分 + 动态分** + +健康分只与系统中的"每日自动添加好友次数"这个功能相关联。\ +通过健康分体系来定义一个微信号每日**最佳、最稳定的添加次数**。\ +后期还可将健康分作为标签属性,用于快速筛选微信号。 + +**公式:每日最大加人次数 = 健康分 × 0.2** + +## 二、基础分 + +基础分为 **60--100 分**。 + +由 `60 + 40(基础加成分)` 四个维度参数组成,每个参数具有不同权重。 + +### 基础分组成 + + 类型 权重 分数 + ------------ ------ ------ + 基础信息 0.2 10 + 好友数量 0.3 30 + 默认基础分 --- 60 + +### 1. 基础信息(权重 0.2,满分 10) + + 类型 权重 分数 + -------------- ------ ------ + 已修改微信号 1 10 + +### 2. 好友数量(权重 0.3,满分 30) + + 好友数量范围 权重 分数 + -------------- ------ ------ + 0--50 0.1 3 + 51--500 0.2 6 + 501--3000 0.3 8 + 3001 以上 0.4 12 + +## 三、动态分规则 + +### 扣分规则 + + 场景 扣分 处罚 + ---------- ------ -------------- + 首次频繁 15 暂停 24 小时 + 再次频繁 25 暂停 24 小时 + 封号 60 暂停 72 小时 + +### 加分规则 + + 场景 加分 + --------------------- ------ + 连续 3 天不触发频繁 5/日 From 719ed989964afebaa9f77a24189f18f4fd525f6f Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Thu, 20 Nov 2025 16:11:27 +0800 Subject: [PATCH 11/31] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Server/composer.json | 3 +- .../extend/Eison/Utils/Helper/ArrHelper.php | 125 ++++++++++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 Server/extend/Eison/Utils/Helper/ArrHelper.php diff --git a/Server/composer.json b/Server/composer.json index ea979146..5bb827df 100644 --- a/Server/composer.json +++ b/Server/composer.json @@ -48,7 +48,8 @@ }, "autoload": { "psr-4": { - "app\\": "application" + "app\\": "application", + "Eison\\": "extend/Eison" }, "files": [ "application/common.php" diff --git a/Server/extend/Eison/Utils/Helper/ArrHelper.php b/Server/extend/Eison/Utils/Helper/ArrHelper.php new file mode 100644 index 00000000..a659c82d --- /dev/null +++ b/Server/extend/Eison/Utils/Helper/ArrHelper.php @@ -0,0 +1,125 @@ + Date: Thu, 20 Nov 2025 16:13:57 +0800 Subject: [PATCH 12/31] =?UTF-8?q?=E4=BC=98=E5=8C=96ProfileModules=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E4=B8=AD=E7=9A=84=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86?= =?UTF-8?q?=EF=BC=8C=E4=BD=BF=E7=94=A8=E9=80=89=E6=8B=A9=E5=99=A8=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E8=AE=A2=E9=98=85=E5=8C=B9=E9=85=8D=E7=9A=84=E5=AE=A2?= =?UTF-8?q?=E6=9C=8D=E5=AF=B9=E8=B1=A1=EF=BC=8C=E9=81=BF=E5=85=8D=E4=B8=8D?= =?UTF-8?q?=E5=BF=85=E8=A6=81=E7=9A=84=E9=87=8D=E6=96=B0=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E3=80=82=E5=90=8C=E6=97=B6=E5=9C=A8DetailValue=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E4=B8=AD=E5=BC=95=E5=85=A5useRef=E8=BF=9B=E8=A1=8C?= =?UTF-8?q?=E6=B7=B1=E5=BA=A6=E6=AF=94=E8=BE=83=EF=BC=8C=E7=A1=AE=E4=BF=9D?= =?UTF-8?q?=E5=8F=AA=E6=9C=89=E5=9C=A8=E5=80=BC=E7=9C=9F=E6=AD=A3=E5=8F=98?= =?UTF-8?q?=E5=8C=96=E6=97=B6=E6=89=8D=E6=9B=B4=E6=96=B0=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProfileModules/components/detailValue.tsx | 31 ++++++++++++++-- .../components/ProfileModules/index.tsx | 36 ++++++++++++++----- 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/components/detailValue.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/components/detailValue.tsx index 4966a863..a986f5cb 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/components/detailValue.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/components/detailValue.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState, useEffect } from "react"; +import React, { useCallback, useState, useEffect, useRef } from "react"; import { Input, message } from "antd"; import { Button } from "antd-mobile"; import { EditOutlined } from "@ant-design/icons"; @@ -56,8 +56,32 @@ const DetailValue: React.FC = ({ useState>(value); const [changedKeys, setChangedKeys] = useState([]); + // 使用 useRef 存储上一次的 value,用于深度比较 + const prevValueRef = useRef>(value); + + // 深度比较函数:比较两个对象的值是否真的变化了 + const isValueChanged = useCallback( + (prev: Record, next: Record) => { + const allKeys = new Set([...Object.keys(prev), ...Object.keys(next)]); + for (const key of allKeys) { + if (prev[key] !== next[key]) { + return true; + } + } + return false; + }, + [], + ); + // 当外部value变化时,更新内部状态 + // 优化:只有当值真正变化时才重置编辑状态,避免因对象引用变化导致编辑状态丢失 useEffect(() => { + // 深度比较,只有当值真正变化时才更新 + if (!isValueChanged(prevValueRef.current, value)) { + return; + } + + // 只有在值真正变化时才更新状态 setFieldValues(value); setOriginalValues(value); setChangedKeys([]); @@ -67,7 +91,10 @@ const DetailValue: React.FC = ({ newEditingFields[field.key] = false; }); setEditingFields(newEditingFields); - }, [value, fields]); + + // 更新 ref + prevValueRef.current = value; + }, [value, fields, isValueChanged]); const handleFieldChange = useCallback( (fieldKey: string, nextVal: string) => { diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx index 6f40e175..951b5bcc 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx @@ -210,14 +210,34 @@ const Person: React.FC = ({ contract }) => { // 构建联系人或群聊详细信息 - const customerList = useCustomerStore(state => state.customerList); - const kfSelectedUser = useMemo(() => { - if (!contract.wechatAccountId) return null; - const matchedCustomer = customerList.find( - customer => customer.id === contract.wechatAccountId, - ); - return matchedCustomer || null; - }, [customerList, contract.wechatAccountId]); + // 优化:使用选择器函数直接订阅匹配的客服对象,避免订阅整个 customerList + // 添加相等性比较,只有当匹配的客服对象或其 labels 真正变化时才触发重新渲染 + const kfSelectedUser = useCustomerStore( + state => { + if (!contract.wechatAccountId) return null; + return ( + state.customerList.find( + customer => customer.id === contract.wechatAccountId, + ) || null + ); + }, + (prev, next) => { + // 如果都是 null,认为相等 + if (!prev && !next) return true; + // 如果一个是 null 另一个不是,认为不相等 + if (!prev || !next) return false; + // 比较关键字段:id 和 labels(因为 useEffect 中使用了 labels) + if (prev.id !== next.id) return false; + // 比较 labels 数组是否真的变化了 + const prevLabels = prev.labels || []; + const nextLabels = next.labels || []; + if (prevLabels.length !== nextLabels.length) return false; + // 深度比较 labels 数组内容(先复制再排序,避免修改原数组) + const prevLabelsStr = JSON.stringify([...prevLabels].sort()); + const nextLabelsStr = JSON.stringify([...nextLabels].sort()); + return prevLabelsStr === nextLabelsStr; + }, + ); // 不再需要从useContactStore获取getContactsByCustomer From 235e3ed994e5e68201f05aab3169e201d76a6b04 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Thu, 20 Nov 2025 16:18:16 +0800 Subject: [PATCH 13/31] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Server/composer.json | 137 ++++++++++++++++++++++--------------------- 1 file changed, 69 insertions(+), 68 deletions(-) diff --git a/Server/composer.json b/Server/composer.json index 5bb827df..26ba92b7 100644 --- a/Server/composer.json +++ b/Server/composer.json @@ -1,71 +1,72 @@ { - "name": "topthink/think", - "description": "the new thinkphp framework", - "type": "project", - "keywords": [ - "framework", - "thinkphp", - "ORM" + "name": "topthink/think", + "description": "the new thinkphp framework", + "type": "project", + "keywords": [ + "framework", + "thinkphp", + "ORM" + ], + "homepage": "http://thinkphp.cn/", + "license": "Apache-2.0", + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + }, + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "require": { + "php": ">=5.6.0", + "topthink/framework": "5.1.41", + "topthink/think-installer": "2.*", + "topthink/think-captcha": "^2.0", + "topthink/think-helper": "^3.0", + "topthink/think-image": "^1.0", + "topthink/think-queue": "^2.0", + "topthink/think-worker": "^2.0", + "textalk/websocket": "^1.5", + "aliyuncs/oss-sdk-php": "^2.6", + "monolog/monolog": "^1.27", + "guzzlehttp/guzzle": "^6.5", + "overtrue/wechat": "~4.6", + "endroid/qr-code": "^3.9", + "phpoffice/phpspreadsheet": "^1.29", + "workerman/workerman": "^3.5", + "workerman/gateway-worker": "^3.0", + "hashids/hashids": "^2.0", + "khanamiryan/qrcode-detector-decoder": "^1.0", + "lizhichao/word": "^2.0", + "adbario/php-dot-notation": "^2.2" + }, + "require-dev": { + "symfony/var-dumper": "^3.4|^4.4", + "topthink/think-migration": "^2.0", + "phpunit/phpunit": "^5.0|^6.0" + }, + "autoload": { + "psr-4": { + "app\\": "application", + "Eison\\": "extend/Eison" + }, + "files": [ + "application/common.php" ], - "homepage": "http://thinkphp.cn/", - "license": "Apache-2.0", - "authors": [ - { - "name": "liu21st", - "email": "liu21st@gmail.com" - }, - { - "name": "yunwuxin", - "email": "448901948@qq.com" - } - ], - "require": { - "php": ">=5.6.0", - "topthink/framework": "5.1.*", - "topthink/think-installer": "~1.0", - "topthink/think-captcha": "^2.0", - "topthink/think-helper": "^3.0", - "topthink/think-image": "^1.0", - "topthink/think-queue": "^2.0", - "topthink/think-worker": "^2.0", - "textalk/websocket": "^1.2", - "aliyuncs/oss-sdk-php": "^2.3", - "monolog/monolog": "^1.24", - "guzzlehttp/guzzle": "^6.3", - "overtrue/wechat": "~4.0", - "endroid/qr-code": "^3.5", - "phpoffice/phpspreadsheet": "^1.8", - "workerman/workerman": "^3.5", - "workerman/gateway-worker": "^3.0", - "hashids/hashids": "^2.0", - "khanamiryan/qrcode-detector-decoder": "^1.0", - "lizhichao/word": "^2.0", - "adbario/php-dot-notation": "^2.2" - }, - "require-dev": { - "symfony/var-dumper": "^3.4", - "topthink/think-migration": "^2.0" - }, - "autoload": { - "psr-4": { - "app\\": "application", - "Eison\\": "extend/Eison" - }, - "files": [ - "application/common.php" - ], - "classmap": [] - }, - "extra": { - "think-path": "thinkphp" - }, - "config": { - "preferred-install": "dist", - "allow-plugins": { - "topthink/think-installer": true, - "easywechat-composer/easywechat-composer": true - } - }, - "minimum-stability": "dev", - "prefer-stable": true + "classmap": [] + }, + "extra": { + "think-path": "thinkphp" + }, + "config": { + "preferred-install": "dist", + "allow-plugins": { + "topthink/think-installer": true, + "easywechat-composer/easywechat-composer": true + } + }, + "minimum-stability": "dev", + "prefer-stable": true } From a2b0041490a5eb5875de44a730784d8e00df9d21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Thu, 20 Nov 2025 16:50:20 +0800 Subject: [PATCH 14/31] =?UTF-8?q?=E6=9B=B4=E6=96=B0NavCommon=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E4=B8=AD=E7=9A=84=E6=8C=89=E9=92=AE=E5=9B=BE=E6=A0=87?= =?UTF-8?q?=E5=92=8C=E6=96=87=E6=9C=AC=EF=BC=8C=E5=B0=86=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=8C=89=E9=92=AE=E7=9A=84=E5=9B=BE=E6=A0=87?= =?UTF-8?q?=E6=9B=B4=E6=94=B9=E4=B8=BASendOutlined=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E5=B0=86=E5=85=B6=E6=96=87=E6=9C=AC=E6=9B=B4=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E2=80=9C=E5=8F=91=E6=9C=8B=E5=8F=8B=E5=9C=88=E2=80=9D=E3=80=82?= =?UTF-8?q?=E5=90=8C=E6=97=B6=EF=BC=8C=E8=B0=83=E6=95=B4powerCenter?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E4=B8=AD=E7=9A=84=E6=95=B0=E6=8D=AE=E7=BB=93?= =?UTF-8?q?=E6=9E=84=EF=BC=8C=E6=B3=A8=E9=87=8A=E6=8E=89=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E5=BA=93=E9=85=8D=E7=BD=AE=E9=83=A8=E5=88=86=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=A0=B7=E5=BC=8F=E4=BB=A3=E7=A0=81=E3=80=82?= =?UTF-8?q?=E6=9B=B4=E6=96=B0ContentManagement=E7=BB=84=E4=BB=B6=E7=9A=84?= =?UTF-8?q?=E6=A0=87=E9=A2=98=E4=B8=BA=E2=80=9C=E5=8F=91=E6=9C=8B=E5=8F=8B?= =?UTF-8?q?=E5=9C=88=E2=80=9D=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pc/ckbox/components/NavCommon/index.tsx | 6 +-- .../powerCenter/content-management/index.tsx | 2 +- .../pages/pc/ckbox/powerCenter/index.data.tsx | 37 +++++++++++-------- .../src/pages/pc/ckbox/powerCenter/index.tsx | 30 ++++++--------- 4 files changed, 36 insertions(+), 39 deletions(-) diff --git a/Touchkebao/src/pages/pc/ckbox/components/NavCommon/index.tsx b/Touchkebao/src/pages/pc/ckbox/components/NavCommon/index.tsx index 52d9e5df..fcd3f7a4 100644 --- a/Touchkebao/src/pages/pc/ckbox/components/NavCommon/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/components/NavCommon/index.tsx @@ -17,7 +17,7 @@ import { LogoutOutlined, ThunderboltOutlined, SettingOutlined, - CalendarOutlined, + SendOutlined, ClearOutlined, } from "@ant-design/icons"; import { noticeList, readMessage, readAll } from "./api"; @@ -317,10 +317,10 @@ const NavCommon: React.FC = ({ title = "触客宝" }) => { > {title} diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/index.tsx index fed9b453..97ea2452 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/content-management/index.tsx @@ -20,7 +20,7 @@ const ContentManagement: React.FC = () => { return (
, - color: "#52c41a", - tag: "内容管理", - features: [ - "多库管理与分类", - "AI调用权限配置", - "内容检索规则设置", - "手动内容上传", - ], - path: "/pc/powerCenter/content-library", - }, + // { + // id: "content-library", + // title: "AI内容库配置", + // description: "管理AI内容库,配置调用权限,优化AI推送效果和内容质量", + // icon: , + // color: "#52c41a", + // tag: "内容管理", + // features: [ + // "多库管理与分类", + // "AI调用权限配置", + // "内容检索规则设置", + // "手动内容上传", + // ], + // path: "/pc/powerCenter/content-library", + // }, { id: "message-push-assistant", title: "消息推送助手", diff --git a/Touchkebao/src/pages/pc/ckbox/powerCenter/index.tsx b/Touchkebao/src/pages/pc/ckbox/powerCenter/index.tsx index 51652b1e..87090063 100644 --- a/Touchkebao/src/pages/pc/ckbox/powerCenter/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/powerCenter/index.tsx @@ -39,18 +39,6 @@ const PowerCenter: React.FC = () => { return (
{/* 页面标题区域 */} -
-
-
-
-

功能中心

-
-

- AI智能营销·一站式客户管理·高效业务增长 -

-
-
- {/* KPI统计区域(置顶,按图展示) */}
@@ -157,9 +145,11 @@ const PowerCenter: React.FC = () => { {card.features.map((feature, index) => (
  • {feature}
  • @@ -186,7 +176,7 @@ const PowerCenter: React.FC = () => { className={styles.cardIcon} style={{ backgroundColor: getIconBgColor( - featureCategories[3].color + featureCategories[3].color, ), }} > @@ -212,9 +202,11 @@ const PowerCenter: React.FC = () => { {featureCategories[3].features.map((feature, index) => (
  • {feature}
  • From fe12f78c1d5765110a51e400b844cdee9c2540b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Fri, 21 Nov 2025 10:34:55 +0800 Subject: [PATCH 15/31] =?UTF-8?q?=E4=BC=98=E5=8C=96MessageRecord=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E7=9A=84=E8=87=AA=E5=8A=A8=E6=BB=9A=E5=8A=A8=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E7=A1=AE=E4=BF=9D=E5=9C=A8=E6=96=B0=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=88=B0=E8=BE=BE=E4=B8=94=E6=B2=A1=E6=9C=89=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E7=8A=B6=E6=80=81=E5=8F=98=E5=8C=96=E6=97=B6=E6=89=8D?= =?UTF-8?q?=E6=BB=9A=E5=8A=A8=E5=88=B0=E5=BA=95=E9=83=A8=EF=BC=8C=E5=90=8C?= =?UTF-8?q?=E6=97=B6=E4=BF=AE=E5=A4=8D=E4=BA=86=E5=AF=B9=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E9=95=BF=E5=BA=A6=E7=9A=84=E5=BC=95=E7=94=A8=E4=BB=A5=E6=8F=90?= =?UTF-8?q?=E9=AB=98=E6=80=A7=E8=83=BD=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ChatWindow/components/MessageRecord/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx index 4bfe7c51..a3de4b84 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx @@ -347,6 +347,7 @@ const MessageRecord: React.FC = ({ contract }) => { useEffect(() => { const prevMessages = prevMessagesRef.current; + const prevLength = prevMessages.length; const hasVideoStateChange = currentMessages.some((msg, index) => { // 首先检查消息对象本身是否为null或undefined @@ -384,8 +385,9 @@ const MessageRecord: React.FC = ({ contract }) => { } }); - // 只有在没有视频状态变化时才自动滚动到底部 - if (!hasVideoStateChange && isLoadingData) { + if (currentMessages.length > prevLength && !hasVideoStateChange) { + scrollToBottom(); + } else if (isLoadingData && !hasVideoStateChange) { scrollToBottom(); } From 3faee0d8be1ac6034d5bc69978727b53b7f1ec8a Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Fri, 21 Nov 2025 14:38:29 +0800 Subject: [PATCH 16/31] =?UTF-8?q?=E5=AE=A2=E6=9C=8D=E5=81=A5=E5=BA=B7?= =?UTF-8?q?=E5=88=86=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Server/application/command.php | 3 + .../CalculateWechatAccountScoreCommand.php | 311 +++++++++ .../CheckAndFixWechatAccountCommand.php | 159 ----- .../UpdateWechatAccountScoreCommand.php | 168 +++++ .../common/model/WechatAccountScore.php | 44 ++ .../WechatAccountHealthScoreService.php | 649 ++++++++++++++---- Server/crontab_tasks.md | 8 + .../Adapters/ChuKeBao/Adapter.php | 85 ++- 8 files changed, 1129 insertions(+), 298 deletions(-) create mode 100644 Server/application/command/CalculateWechatAccountScoreCommand.php delete mode 100644 Server/application/command/CheckAndFixWechatAccountCommand.php create mode 100644 Server/application/command/UpdateWechatAccountScoreCommand.php create mode 100644 Server/application/common/model/WechatAccountScore.php diff --git a/Server/application/command.php b/Server/application/command.php index f1d4d72b..b5c4d0e5 100644 --- a/Server/application/command.php +++ b/Server/application/command.php @@ -39,4 +39,7 @@ return [ 'workbench:groupCreate' => 'app\command\WorkbenchGroupCreateCommand', // 工作台群创建任务 'workbench:import-contact' => 'app\command\WorkbenchImportContactCommand', // 工作台通讯录导入任务 'kf:notice' => 'app\command\KfNoticeCommand', // 客服端消息通知 + + 'wechat:calculate-score' => 'app\command\CalculateWechatAccountScoreCommand', // 统一计算微信账号健康分 + 'wechat:update-score' => 'app\command\UpdateWechatAccountScoreCommand', // 更新微信账号评分记录 ]; diff --git a/Server/application/command/CalculateWechatAccountScoreCommand.php b/Server/application/command/CalculateWechatAccountScoreCommand.php new file mode 100644 index 00000000..dbc7ec5b --- /dev/null +++ b/Server/application/command/CalculateWechatAccountScoreCommand.php @@ -0,0 +1,311 @@ +setName('wechat:calculate-score') + ->setDescription('统一计算微信账号健康分(包含初始化、更新评分记录、批量计算)'); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln("=========================================="); + $output->writeln("开始统一计算微信账号健康分..."); + $output->writeln("=========================================="); + + $startTime = time(); + $service = new WechatAccountHealthScoreService(); + + try { + // 步骤1: 初始化未计算基础分的账号 + $output->writeln("\n[步骤1] 初始化未计算基础分的账号..."); + $initStats = $this->initUncalculatedAccounts($service, $output); + $output->writeln("初始化完成:成功 {$initStats['success']} 条,失败 {$initStats['failed']} 条"); + + // 步骤2: 更新评分记录(根据wechatId和alias不一致情况) + $output->writeln("\n[步骤2] 更新评分记录(根据wechatId和alias不一致情况)..."); + $updateStats = $this->updateScoreRecords($service, $output); + $output->writeln("更新完成:处理了 {$updateStats['total']} 条记录"); + + // 步骤3: 批量更新健康分(只更新动态分,不重新计算基础分) + $output->writeln("\n[步骤3] 批量更新健康分(只更新动态分)..."); + $batchStats = $this->batchUpdateHealthScore($service, $output); + $output->writeln("批量更新完成:成功 {$batchStats['success']} 条,失败 {$batchStats['failed']} 条"); + + // 统计信息 + $endTime = time(); + $duration = $endTime - $startTime; + + $output->writeln("\n=========================================="); + $output->writeln("任务完成!"); + $output->writeln("=========================================="); + $output->writeln("总耗时: {$duration} 秒"); + $output->writeln("初始化: 成功 {$initStats['success']} 条,失败 {$initStats['failed']} 条"); + $output->writeln("更新评分记录: {$updateStats['total']} 条"); + $output->writeln("批量更新: 成功 {$batchStats['success']} 条,失败 {$batchStats['failed']} 条"); + + if (!empty($initStats['errors'])) { + $output->writeln("\n初始化错误详情:"); + foreach (array_slice($initStats['errors'], 0, 10) as $error) { + $output->writeln(" 账号ID {$error['accountId']}: {$error['error']}"); + } + if (count($initStats['errors']) > 10) { + $output->writeln(" ... 还有 " . (count($initStats['errors']) - 10) . " 个错误"); + } + } + + if (!empty($batchStats['errors'])) { + $output->writeln("\n批量更新错误详情:"); + foreach (array_slice($batchStats['errors'], 0, 10) as $error) { + $output->writeln(" 账号ID {$error['accountId']}: {$error['error']}"); + } + if (count($batchStats['errors']) > 10) { + $output->writeln(" ... 还有 " . (count($batchStats['errors']) - 10) . " 个错误"); + } + } + + } catch (\Exception $e) { + $output->writeln("\n错误: " . $e->getMessage()); + $output->writeln($e->getTraceAsString()); + } + } + + /** + * 初始化未计算基础分的账号 + * + * @param WechatAccountHealthScoreService $service + * @param Output $output + * @return array + */ + private function initUncalculatedAccounts($service, $output) + { + $stats = [ + 'total' => 0, + 'success' => 0, + 'failed' => 0, + 'errors' => [] + ]; + + // 获取所有未计算基础分的账号 + $accounts = Db::table('s2_wechat_account') + ->alias('a') + ->leftJoin(['s2_wechat_account_score' => 's'], 's.accountId = a.id') + ->where('a.isDeleted', 0) + ->where(function($query) { + $query->whereNull('s.id') + ->whereOr('s.baseScoreCalculated', 0); + }) + ->field('a.id, a.wechatId') + ->select(); + + $stats['total'] = count($accounts); + + if ($stats['total'] == 0) { + $output->writeln("没有需要初始化的账号"); + return $stats; + } + + $output->writeln("找到 {$stats['total']} 个需要初始化的账号"); + + $batchSize = 100; + $batches = array_chunk($accounts, $batchSize); + + foreach ($batches as $batchIndex => $batch) { + foreach ($batch as $account) { + try { + $service->calculateAndUpdate($account['id']); + $stats['success']++; + + if ($stats['success'] % 100 == 0) { + $output->write("."); + } + } catch (\Exception $e) { + $stats['failed']++; + $stats['errors'][] = [ + 'accountId' => $account['id'], + 'error' => $e->getMessage() + ]; + } + } + + if (($batchIndex + 1) % 10 == 0) { + $output->writeln(" 已处理 " . ($batchIndex + 1) * $batchSize . " 条"); + } + } + + return $stats; + } + + /** + * 更新评分记录(根据wechatId和alias不一致情况) + * + * @param WechatAccountHealthScoreService $service + * @param Output $output + * @return array + */ + private function updateScoreRecords($service, $output) + { + $stats = ['total' => 0]; + + // 查找wechatId和alias不一致的账号 + $inconsistentAccounts = Db::table('s2_wechat_account') + ->where('isDeleted', 0) + ->where('wechatId', '<>', '') + ->where('alias', '<>', '') + ->whereRaw('wechatId != alias') + ->field('id, wechatId, alias') + ->select(); + + // 查找wechatId和alias一致的账号 + $consistentAccounts = Db::table('s2_wechat_account') + ->where('isDeleted', 0) + ->where('wechatId', '<>', '') + ->where('alias', '<>', '') + ->whereRaw('wechatId = alias') + ->field('id, wechatId, alias') + ->select(); + + $allAccounts = array_merge($inconsistentAccounts, $consistentAccounts); + $stats['total'] = count($allAccounts); + + if ($stats['total'] == 0) { + $output->writeln("没有需要更新的账号"); + return $stats; + } + + $output->writeln("找到 {$stats['total']} 个需要更新的账号(不一致: " . count($inconsistentAccounts) . ",一致: " . count($consistentAccounts) . ")"); + + $updatedCount = 0; + + foreach ($allAccounts as $account) { + $isModifiedAlias = in_array($account['id'], array_column($inconsistentAccounts, 'id')); + $this->updateScoreRecord($account['id'], $isModifiedAlias, $service); + $updatedCount++; + + if ($updatedCount % 100 == 0) { + $output->write("."); + } + } + + if ($updatedCount > 0 && $updatedCount % 100 == 0) { + $output->writeln(""); + } + + return $stats; + } + + /** + * 批量更新健康分(只更新动态分) + * + * @param WechatAccountHealthScoreService $service + * @param Output $output + * @return array + */ + private function batchUpdateHealthScore($service, $output) + { + // 获取所有已计算基础分的账号 + $accountIds = Db::table('s2_wechat_account_score') + ->where('baseScoreCalculated', 1) + ->column('accountId'); + + $total = count($accountIds); + + if ($total == 0) { + $output->writeln("没有需要更新的账号"); + return ['success' => 0, 'failed' => 0, 'errors' => []]; + } + + $output->writeln("找到 {$total} 个需要更新动态分的账号"); + + $stats = $service->batchCalculateAndUpdate($accountIds, 100, false); + + return $stats; + } + + /** + * 更新评分记录 + * + * @param int $accountId 账号ID + * @param bool $isModifiedAlias 是否已修改微信号 + * @param WechatAccountHealthScoreService $service 评分服务 + */ + private function updateScoreRecord($accountId, $isModifiedAlias, $service) + { + // 获取账号数据 + $accountData = Db::table('s2_wechat_account') + ->where('id', $accountId) + ->find(); + + if (empty($accountData)) { + return; + } + + // 确保评分记录存在 + $scoreRecord = Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) + ->find(); + + if (empty($scoreRecord)) { + // 如果记录不存在,创建并计算基础分 + $service->calculateAndUpdate($accountId); + $scoreRecord = Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) + ->find(); + } + + if (empty($scoreRecord)) { + return; + } + + // 更新isModifiedAlias字段 + $updateData = [ + 'isModifiedAlias' => $isModifiedAlias ? 1 : 0, + 'updateTime' => time() + ]; + + // 如果基础分已计算,需要更新基础信息分和基础分 + if ($scoreRecord['baseScoreCalculated']) { + $oldBaseInfoScore = $scoreRecord['baseInfoScore'] ?? 0; + $newBaseInfoScore = $isModifiedAlias ? 10 : 0; // 已修改微信号得10分 + + if ($oldBaseInfoScore != $newBaseInfoScore) { + $oldBaseScore = $scoreRecord['baseScore'] ?? 60; + $newBaseScore = $oldBaseScore - $oldBaseInfoScore + $newBaseInfoScore; + + $updateData['baseInfoScore'] = $newBaseInfoScore; + $updateData['baseScore'] = $newBaseScore; + + // 重新计算健康分 + $dynamicScore = $scoreRecord['dynamicScore'] ?? 0; + $healthScore = $newBaseScore + $dynamicScore; + $healthScore = max(0, min(100, $healthScore)); + $updateData['healthScore'] = $healthScore; + $updateData['maxAddFriendPerDay'] = (int)floor($healthScore * 0.2); + } + } else { + // 基础分未计算,只更新标记和基础信息分 + $updateData['baseInfoScore'] = $isModifiedAlias ? 10 : 0; + } + + Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) + ->update($updateData); + } +} + diff --git a/Server/application/command/CheckAndFixWechatAccountCommand.php b/Server/application/command/CheckAndFixWechatAccountCommand.php deleted file mode 100644 index e498f77b..00000000 --- a/Server/application/command/CheckAndFixWechatAccountCommand.php +++ /dev/null @@ -1,159 +0,0 @@ -setName('wechat:check-and-fix') - ->setDescription('检测和修复s2_wechat_account表中wechatId和alias不一致的问题,并更新健康分'); - } - - protected function execute(Input $input, Output $output) - { - $output->writeln("开始检测和修复s2_wechat_account表..."); - - try { - // 1. 检测wechatId和alias不一致的记录 - $output->writeln("步骤1: 检测wechatId和alias不一致的记录..."); - $inconsistentAccounts = $this->findInconsistentAccounts(); - $output->writeln("发现 " . count($inconsistentAccounts) . " 条不一致记录"); - - if (empty($inconsistentAccounts)) { - $output->writeln("没有发现不一致的记录,任务完成!"); - return; - } - - // 2. 修复不一致的记录 - $output->writeln("步骤2: 修复不一致的记录..."); - $fixedCount = $this->fixInconsistentAccounts($inconsistentAccounts); - $output->writeln("已修复 " . $fixedCount . " 条记录"); - - // 3. 更新isModifiedAlias字段 - $output->writeln("步骤3: 更新isModifiedAlias字段..."); - $updatedCount = $this->updateModifiedAliasFlag(); - $output->writeln("已更新 " . $updatedCount . " 条记录的isModifiedAlias字段"); - - // 4. 重新计算健康分 - $output->writeln("步骤4: 重新计算健康分..."); - $healthScoreService = new WechatAccountHealthScoreService(); - $accountIds = array_column($inconsistentAccounts, 'id'); - $stats = $healthScoreService->batchCalculateAndUpdate($accountIds, 100); - $output->writeln("健康分计算完成:成功 " . $stats['success'] . " 条,失败 " . $stats['failed'] . " 条"); - - if (!empty($stats['errors'])) { - $output->writeln("错误详情:"); - foreach ($stats['errors'] as $error) { - $output->writeln(" 账号ID {$error['accountId']}: {$error['error']}"); - } - } - - $output->writeln("任务完成!"); - - } catch (\Exception $e) { - $output->writeln("错误: " . $e->getMessage()); - $output->writeln($e->getTraceAsString()); - } - } - - /** - * 查找wechatId和alias不一致的记录 - * - * @return array - */ - private function findInconsistentAccounts() - { - // 查找wechatId和alias不一致的记录 - // 条件:wechatId不为空,alias不为空,且wechatId != alias - $accounts = Db::table('s2_wechat_account') - ->where('isDeleted', 0) - ->where('wechatId', '<>', '') - ->where('alias', '<>', '') - ->whereRaw('wechatId != alias') - ->field('id, wechatId, alias, nickname, isModifiedAlias') - ->select(); - - return $accounts ?: []; - } - - /** - * 修复不一致的记录 - * 策略:从s2_wechat_friend表中查找最新的alias值来更新 - * - * @param array $accounts 不一致的账号列表 - * @return int 修复数量 - */ - private function fixInconsistentAccounts($accounts) - { - $fixedCount = 0; - - foreach ($accounts as $account) { - $wechatId = $account['wechatId']; - - // 从s2_wechat_friend表中查找最新的alias值 - $latestAlias = Db::table('s2_wechat_friend') - ->where('wechatId', $wechatId) - ->where('alias', '<>', '') - ->order('updateTime', 'desc') - ->value('alias'); - - // 如果找到了最新的alias,则更新 - if (!empty($latestAlias) && $latestAlias !== $account['alias']) { - Db::table('s2_wechat_account') - ->where('id', $account['id']) - ->update([ - 'alias' => $latestAlias, - 'updateTime' => time() - ]); - $fixedCount++; - } - } - - return $fixedCount; - } - - /** - * 更新isModifiedAlias字段 - * 如果wechatId和alias不一致,则标记为已修改 - * - * @return int 更新数量 - */ - private function updateModifiedAliasFlag() - { - // 更新isModifiedAlias字段:wechatId != alias 的记录标记为1 - $updatedCount = Db::table('s2_wechat_account') - ->where('isDeleted', 0) - ->where('wechatId', '<>', '') - ->where('alias', '<>', '') - ->whereRaw('wechatId != alias') - ->update([ - 'isModifiedAlias' => 1, - 'updateTime' => time() - ]); - - // 更新isModifiedAlias字段:wechatId == alias 的记录标记为0 - Db::table('s2_wechat_account') - ->where('isDeleted', 0) - ->where('wechatId', '<>', '') - ->where('alias', '<>', '') - ->whereRaw('wechatId = alias') - ->update([ - 'isModifiedAlias' => 0, - 'updateTime' => time() - ]); - - return $updatedCount; - } -} - diff --git a/Server/application/command/UpdateWechatAccountScoreCommand.php b/Server/application/command/UpdateWechatAccountScoreCommand.php new file mode 100644 index 00000000..13ded8d4 --- /dev/null +++ b/Server/application/command/UpdateWechatAccountScoreCommand.php @@ -0,0 +1,168 @@ +setName('wechat:update-score') + ->setDescription('更新微信账号评分记录,根据wechatId和alias不一致情况更新isModifiedAlias字段(仅用于评分)'); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln("开始更新微信账号评分记录..."); + + try { + // 1. 查找所有需要更新的账号 + $output->writeln("步骤1: 查找需要更新的账号..."); + + // 查找wechatId和alias不一致的账号 + $inconsistentAccounts = Db::table('s2_wechat_account') + ->where('isDeleted', 0) + ->where('wechatId', '<>', '') + ->where('alias', '<>', '') + ->whereRaw('wechatId != alias') + ->field('id, wechatId, alias') + ->select(); + + // 查找wechatId和alias一致的账号 + $consistentAccounts = Db::table('s2_wechat_account') + ->where('isDeleted', 0) + ->where('wechatId', '<>', '') + ->where('alias', '<>', '') + ->whereRaw('wechatId = alias') + ->field('id, wechatId, alias') + ->select(); + + $output->writeln("发现 " . count($inconsistentAccounts) . " 条不一致记录(已修改微信号)"); + $output->writeln("发现 " . count($consistentAccounts) . " 条一致记录(未修改微信号)"); + + // 2. 更新评分记录表中的isModifiedAlias字段 + $output->writeln("步骤2: 更新评分记录表..."); + $updatedCount = 0; + $healthScoreService = new WechatAccountHealthScoreService(); + + // 更新不一致的记录 + foreach ($inconsistentAccounts as $account) { + $this->updateScoreRecord($account['id'], true, $healthScoreService); + $updatedCount++; + } + + // 更新一致的记录 + foreach ($consistentAccounts as $account) { + $this->updateScoreRecord($account['id'], false, $healthScoreService); + $updatedCount++; + } + + $output->writeln("已更新 " . $updatedCount . " 条评分记录"); + + // 3. 重新计算健康分(只更新基础信息分,不重新计算基础分) + $output->writeln("步骤3: 重新计算健康分..."); + $allAccountIds = array_merge( + array_column($inconsistentAccounts, 'id'), + array_column($consistentAccounts, 'id') + ); + + if (!empty($allAccountIds)) { + $stats = $healthScoreService->batchCalculateAndUpdate($allAccountIds, 100, false); + $output->writeln("健康分计算完成:成功 " . $stats['success'] . " 条,失败 " . $stats['failed'] . " 条"); + + if (!empty($stats['errors'])) { + $output->writeln("错误详情:"); + foreach ($stats['errors'] as $error) { + $output->writeln(" 账号ID {$error['accountId']}: {$error['error']}"); + } + } + } + + $output->writeln("任务完成!"); + + } catch (\Exception $e) { + $output->writeln("错误: " . $e->getMessage()); + $output->writeln($e->getTraceAsString()); + } + } + + /** + * 更新评分记录 + * + * @param int $accountId 账号ID + * @param bool $isModifiedAlias 是否已修改微信号 + * @param WechatAccountHealthScoreService $service 评分服务 + */ + private function updateScoreRecord($accountId, $isModifiedAlias, $service) + { + // 获取或创建评分记录 + $accountData = Db::table('s2_wechat_account') + ->where('id', $accountId) + ->find(); + + if (empty($accountData)) { + return; + } + + // 确保评分记录存在 + $scoreRecord = Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) + ->find(); + + if (empty($scoreRecord)) { + // 如果记录不存在,创建并计算基础分 + $service->calculateAndUpdate($accountId); + $scoreRecord = Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) + ->find(); + } + + if (empty($scoreRecord)) { + return; + } + + // 更新isModifiedAlias字段 + $updateData = [ + 'isModifiedAlias' => $isModifiedAlias ? 1 : 0, + 'updateTime' => time() + ]; + + // 如果基础分已计算,需要更新基础信息分和基础分 + if ($scoreRecord['baseScoreCalculated']) { + $oldBaseInfoScore = $scoreRecord['baseInfoScore'] ?? 0; + $newBaseInfoScore = $isModifiedAlias ? 10 : 0; // 已修改微信号得10分 + + if ($oldBaseInfoScore != $newBaseInfoScore) { + $oldBaseScore = $scoreRecord['baseScore'] ?? 60; + $newBaseScore = $oldBaseScore - $oldBaseInfoScore + $newBaseInfoScore; + + $updateData['baseInfoScore'] = $newBaseInfoScore; + $updateData['baseScore'] = $newBaseScore; + + // 重新计算健康分 + $dynamicScore = $scoreRecord['dynamicScore'] ?? 0; + $healthScore = $newBaseScore + $dynamicScore; + $healthScore = max(0, min(100, $healthScore)); + $updateData['healthScore'] = $healthScore; + $updateData['maxAddFriendPerDay'] = (int)floor($healthScore * 0.2); + } + } else { + // 基础分未计算,只更新标记和基础信息分 + $updateData['baseInfoScore'] = $isModifiedAlias ? 10 : 0; + } + + Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) + ->update($updateData); + } +} + diff --git a/Server/application/common/model/WechatAccountScore.php b/Server/application/common/model/WechatAccountScore.php new file mode 100644 index 00000000..43170f78 --- /dev/null +++ b/Server/application/common/model/WechatAccountScore.php @@ -0,0 +1,44 @@ + 'integer', + 'baseScore' => 'integer', + 'baseScoreCalculated' => 'integer', + 'baseInfoScore' => 'integer', + 'friendCountScore' => 'integer', + 'friendCount' => 'integer', + 'dynamicScore' => 'integer', + 'frequentCount' => 'integer', + 'frequentPenalty' => 'integer', + 'consecutiveNoFrequentDays' => 'integer', + 'noFrequentBonus' => 'integer', + 'banPenalty' => 'integer', + 'healthScore' => 'integer', + 'maxAddFriendPerDay' => 'integer', + 'isModifiedAlias' => 'integer', + 'isBanned' => 'integer', + 'lastFrequentTime' => 'integer', + 'lastNoFrequentTime' => 'integer', + 'baseScoreCalcTime' => 'integer', + 'createTime' => 'integer', + 'updateTime' => 'integer', + ]; +} + diff --git a/Server/application/common/service/WechatAccountHealthScoreService.php b/Server/application/common/service/WechatAccountHealthScoreService.php index cda09cfd..02a89a9b 100644 --- a/Server/application/common/service/WechatAccountHealthScoreService.php +++ b/Server/application/common/service/WechatAccountHealthScoreService.php @@ -6,9 +6,15 @@ use think\Db; use think\Exception; /** - * 微信账号健康分评分服务 + * 微信账号健康分评分服务(优化版) * 基于《微信健康分规则v2.md》实现 * + * 优化点: + * 1. 基础分只计算一次 + * 2. 各个评分维度独立存储 + * 3. 使用独立的评分记录表 + * 4. 好友数量评分特殊处理(避免同步问题) + * * 健康分 = 基础分 + 动态分 * 基础分:60-100分(默认60分 + 基础信息10分 + 好友数量30分) * 动态分:扣分和加分规则 @@ -18,13 +24,14 @@ class WechatAccountHealthScoreService // 默认基础分 const DEFAULT_BASE_SCORE = 60; - // 基础信息权重和分数 - const BASE_INFO_WEIGHT = 0.2; + // 基础信息分数 const BASE_INFO_SCORE = 10; - // 好友数量权重和分数 - const FRIEND_COUNT_WEIGHT = 0.3; - const FRIEND_COUNT_MAX_SCORE = 30; + // 好友数量分数区间 + const FRIEND_COUNT_SCORE_0_50 = 3; + const FRIEND_COUNT_SCORE_51_500 = 6; + const FRIEND_COUNT_SCORE_501_3000 = 8; + const FRIEND_COUNT_SCORE_3001_PLUS = 12; // 动态分扣分规则 const PENALTY_FIRST_FREQUENT = -15; // 首次频繁扣15分 @@ -39,9 +46,10 @@ class WechatAccountHealthScoreService * * @param int $accountId 账号ID(s2_wechat_account表的id) * @param array $accountData 账号数据(可选,如果不传则从数据库查询) + * @param bool $forceRecalculateBase 是否强制重新计算基础分(默认false) * @return array 返回评分结果 */ - public function calculateAndUpdate($accountId, $accountData = null) + public function calculateAndUpdate($accountId, $accountData = null, $forceRecalculateBase = false) { try { // 获取账号数据 @@ -55,39 +63,68 @@ class WechatAccountHealthScoreService throw new Exception("账号不存在:{$accountId}"); } - // 计算基础分 - $baseScore = $this->calculateBaseScore($accountData); + $wechatId = $accountData['wechatId'] ?? ''; + if (empty($wechatId)) { + throw new Exception("账号wechatId为空:{$accountId}"); + } - // 计算动态分 - $dynamicScore = $this->calculateDynamicScore($accountData); + // 获取或创建评分记录 + $scoreRecord = $this->getOrCreateScoreRecord($accountId, $wechatId); + + // 计算基础分(只计算一次,除非强制重新计算) + if (!$scoreRecord['baseScoreCalculated'] || $forceRecalculateBase) { + $baseScoreData = $this->calculateBaseScore($accountData, $scoreRecord); + $this->updateBaseScore($accountId, $baseScoreData); + // 重新获取记录以获取最新数据 + $scoreRecord = $this->getScoreRecord($accountId); + } + + // 计算动态分(每次都要重新计算) + $dynamicScoreData = $this->calculateDynamicScore($accountData, $scoreRecord); // 计算总分 + $baseScore = $scoreRecord['baseScore']; + $dynamicScore = $dynamicScoreData['total']; $healthScore = $baseScore + $dynamicScore; // 确保健康分在合理范围内(0-100) $healthScore = max(0, min(100, $healthScore)); - // 更新数据库 + // 计算每日最大加人次数 + $maxAddFriendPerDay = $this->getMaxAddFriendPerDay($healthScore); + + // 更新评分记录 $updateData = [ - 'healthScore' => $healthScore, - 'baseScore' => $baseScore, 'dynamicScore' => $dynamicScore, - 'scoreUpdateTime' => time() + 'frequentPenalty' => $dynamicScoreData['frequentPenalty'], + 'noFrequentBonus' => $dynamicScoreData['noFrequentBonus'], + 'banPenalty' => $dynamicScoreData['banPenalty'], + 'lastFrequentTime' => $dynamicScoreData['lastFrequentTime'], + 'frequentCount' => $dynamicScoreData['frequentCount'], + 'lastNoFrequentTime' => $dynamicScoreData['lastNoFrequentTime'], + 'consecutiveNoFrequentDays' => $dynamicScoreData['consecutiveNoFrequentDays'], + 'isBanned' => $dynamicScoreData['isBanned'], + 'healthScore' => $healthScore, + 'maxAddFriendPerDay' => $maxAddFriendPerDay, + 'updateTime' => time() ]; - Db::table('s2_wechat_account') - ->where('id', $accountId) + Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) ->update($updateData); return [ 'accountId' => $accountId, - 'wechatId' => $accountData['wechatId'] ?? '', + 'wechatId' => $wechatId, 'healthScore' => $healthScore, 'baseScore' => $baseScore, + 'baseInfoScore' => $scoreRecord['baseInfoScore'], + 'friendCountScore' => $scoreRecord['friendCountScore'], 'dynamicScore' => $dynamicScore, - 'baseInfoScore' => $this->getBaseInfoScore($accountData), - 'friendCountScore' => $this->getFriendCountScore($accountData['totalFriend'] ?? 0), - 'maxAddFriendPerDay' => $this->getMaxAddFriendPerDay($healthScore) + 'frequentPenalty' => $dynamicScoreData['frequentPenalty'], + 'noFrequentBonus' => $dynamicScoreData['noFrequentBonus'], + 'banPenalty' => $dynamicScoreData['banPenalty'], + 'maxAddFriendPerDay' => $maxAddFriendPerDay ]; } catch (Exception $e) { @@ -96,24 +133,119 @@ class WechatAccountHealthScoreService } /** - * 计算基础分 - * 基础分 = 默认60分 + 基础信息分(10分) + 好友数量分(30分) + * 获取或创建评分记录 + * + * @param int $accountId 账号ID + * @param string $wechatId 微信ID + * @return array 评分记录 + */ + private function getOrCreateScoreRecord($accountId, $wechatId) + { + $record = Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) + ->find(); + + if (empty($record)) { + // 创建新记录 + $data = [ + 'accountId' => $accountId, + 'wechatId' => $wechatId, + 'baseScore' => 0, + 'baseScoreCalculated' => 0, + 'baseInfoScore' => 0, + 'friendCountScore' => 0, + 'dynamicScore' => 0, + 'frequentCount' => 0, + 'consecutiveNoFrequentDays' => 0, + 'healthScore' => 0, + 'maxAddFriendPerDay' => 0, + 'createTime' => time(), + 'updateTime' => time() + ]; + + Db::table('s2_wechat_account_score')->insert($data); + + return $data; + } + + return $record; + } + + /** + * 获取评分记录 + * + * @param int $accountId 账号ID + * @return array + */ + private function getScoreRecord($accountId) + { + return Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) + ->find() ?: []; + } + + /** + * 计算基础分(只计算一次) + * 基础分 = 默认60分 + 基础信息分(10分) + 好友数量分(3-12分) * * @param array $accountData 账号数据 - * @return int 基础分 + * @param array $scoreRecord 现有评分记录 + * @return array 基础分数据 */ - private function calculateBaseScore($accountData) + private function calculateBaseScore($accountData, $scoreRecord = []) { $baseScore = self::DEFAULT_BASE_SCORE; // 基础信息分(已修改微信号得10分) - $baseScore += $this->getBaseInfoScore($accountData); + $baseInfoScore = $this->getBaseInfoScore($accountData); + $baseScore += $baseInfoScore; - // 好友数量分(最高30分) - $totalFriend = $accountData['totalFriend'] ?? 0; - $baseScore += $this->getFriendCountScore($totalFriend); + // 好友数量分(特殊处理:使用快照值,避免同步问题) + $friendCountScore = 0; + $friendCount = 0; + $friendCountSource = 'manual'; - return $baseScore; + // 如果已有评分记录且好友数量分已计算,使用历史值 + if (!empty($scoreRecord['friendCountScore']) && $scoreRecord['friendCountScore'] > 0) { + $friendCountScore = $scoreRecord['friendCountScore']; + $friendCount = $scoreRecord['friendCount'] ?? 0; + $friendCountSource = $scoreRecord['friendCountSource'] ?? 'manual'; + } else { + // 首次计算:使用当前好友数量,但标记为手动计算 + $totalFriend = $accountData['totalFriend'] ?? 0; + $friendCountScore = $this->getFriendCountScore($totalFriend); + $friendCount = $totalFriend; + $friendCountSource = 'manual'; + } + + $baseScore += $friendCountScore; + + // 检查是否已修改微信号 + $isModifiedAlias = $this->checkIsModifiedAlias($accountData); + + return [ + 'baseScore' => $baseScore, + 'baseInfoScore' => $baseInfoScore, + 'friendCountScore' => $friendCountScore, + 'friendCount' => $friendCount, + 'friendCountSource' => $friendCountSource, + 'isModifiedAlias' => $isModifiedAlias ? 1 : 0, + 'baseScoreCalculated' => 1, + 'baseScoreCalcTime' => time() + ]; + } + + /** + * 更新基础分 + * + * @param int $accountId 账号ID + * @param array $baseScoreData 基础分数据 + */ + private function updateBaseScore($accountId, $baseScoreData) + { + Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) + ->update($baseScoreData); } /** @@ -125,23 +257,36 @@ class WechatAccountHealthScoreService */ private function getBaseInfoScore($accountData) { - // 检查是否已修改微信号 - // 如果isModifiedAlias字段为1,或者wechatId和alias不一致,则认为已修改 - $isModifiedAlias = isset($accountData['isModifiedAlias']) ? (int)$accountData['isModifiedAlias'] : 0; - $wechatId = trim($accountData['wechatId'] ?? ''); - $alias = trim($accountData['alias'] ?? ''); - - // 如果字段标记为已修改,或者wechatId和alias不一致,则得分 - if ($isModifiedAlias == 1 || (!empty($wechatId) && !empty($alias) && $wechatId !== $alias)) { + if ($this->checkIsModifiedAlias($accountData)) { return self::BASE_INFO_SCORE; } - return 0; } + /** + * 检查是否已修改微信号 + * 判断标准:wechatId和alias不一致且都不为空,则认为已修改微信号 + * 注意:这里只用于评分,不修复数据 + * + * @param array $accountData 账号数据 + * @return bool + */ + private function checkIsModifiedAlias($accountData) + { + $wechatId = trim($accountData['wechatId'] ?? ''); + $alias = trim($accountData['alias'] ?? ''); + + // 如果wechatId和alias不一致且都不为空,则认为已修改微信号(用于评分) + if (!empty($wechatId) && !empty($alias) && $wechatId !== $alias) { + return true; + } + + return false; + } + /** * 获取好友数量分 - * 根据好友数量区间得分(最高30分) + * 根据好友数量区间得分(最高12分) * * @param int $totalFriend 总好友数 * @return int 好友数量分 @@ -149,92 +294,292 @@ class WechatAccountHealthScoreService private function getFriendCountScore($totalFriend) { if ($totalFriend <= 50) { - return 3; // 0-50: 3分 + return self::FRIEND_COUNT_SCORE_0_50; } elseif ($totalFriend <= 500) { - return 6; // 51-500: 6分 + return self::FRIEND_COUNT_SCORE_51_500; } elseif ($totalFriend <= 3000) { - return 8; // 501-3000: 8分 + return self::FRIEND_COUNT_SCORE_501_3000; } else { - return 12; // 3001以上: 12分 + return self::FRIEND_COUNT_SCORE_3001_PLUS; } } + /** + * 手动更新好友数量分(用于处理同步问题) + * + * @param int $accountId 账号ID + * @param int $friendCount 好友数量 + * @param string $source 来源(manual=手动,sync=同步) + * @return bool + */ + public function updateFriendCountScore($accountId, $friendCount, $source = 'manual') + { + $scoreRecord = $this->getScoreRecord($accountId); + + // 如果基础分已计算,不允许修改好友数量分(除非是手动更新) + if (!empty($scoreRecord['baseScoreCalculated']) && $source === 'sync') { + // 同步数据不允许修改已计算的基础分 + return false; + } + + $friendCountScore = $this->getFriendCountScore($friendCount); + + // 重新计算基础分 + $oldBaseScore = $scoreRecord['baseScore'] ?? self::DEFAULT_BASE_SCORE; + $oldFriendCountScore = $scoreRecord['friendCountScore'] ?? 0; + $baseInfoScore = $scoreRecord['baseInfoScore'] ?? 0; + + $newBaseScore = self::DEFAULT_BASE_SCORE + $baseInfoScore + $friendCountScore; + + $updateData = [ + 'friendCountScore' => $friendCountScore, + 'friendCount' => $friendCount, + 'friendCountSource' => $source, + 'baseScore' => $newBaseScore, + 'updateTime' => time() + ]; + + // 如果基础分已计算,需要更新总分 + if (!empty($scoreRecord['baseScoreCalculated'])) { + $dynamicScore = $scoreRecord['dynamicScore'] ?? 0; + $healthScore = $newBaseScore + $dynamicScore; + $healthScore = max(0, min(100, $healthScore)); + $updateData['healthScore'] = $healthScore; + $updateData['maxAddFriendPerDay'] = $this->getMaxAddFriendPerDay($healthScore); + } + + Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) + ->update($updateData); + + return true; + } + /** * 计算动态分 * 动态分 = 扣分 + 加分 * * @param array $accountData 账号数据 - * @return int 动态分 + * @param array $scoreRecord 现有评分记录 + * @return array 动态分数据 */ - private function calculateDynamicScore($accountData) + private function calculateDynamicScore($accountData, $scoreRecord) { - $dynamicScore = 0; + $result = [ + 'total' => 0, + 'frequentPenalty' => 0, + 'noFrequentBonus' => 0, + 'banPenalty' => 0, + 'lastFrequentTime' => null, + 'frequentCount' => 0, + 'lastNoFrequentTime' => null, + 'consecutiveNoFrequentDays' => 0, + 'isBanned' => 0 + ]; - // 处理扣分 - $dynamicScore += $this->calculatePenalty($accountData); + $accountId = $accountData['id'] ?? 0; + $wechatId = $accountData['wechatId'] ?? ''; - // 处理加分 - $dynamicScore += $this->calculateBonus($accountData); + if (empty($accountId) || empty($wechatId)) { + return $result; + } - return $dynamicScore; + // 继承现有数据 + if (!empty($scoreRecord)) { + $result['lastFrequentTime'] = $scoreRecord['lastFrequentTime'] ?? null; + $result['frequentCount'] = $scoreRecord['frequentCount'] ?? 0; + $result['lastNoFrequentTime'] = $scoreRecord['lastNoFrequentTime'] ?? null; + $result['consecutiveNoFrequentDays'] = $scoreRecord['consecutiveNoFrequentDays'] ?? 0; + $result['frequentPenalty'] = $scoreRecord['frequentPenalty'] ?? 0; + $result['noFrequentBonus'] = $scoreRecord['noFrequentBonus'] ?? 0; + $result['banPenalty'] = $scoreRecord['banPenalty'] ?? 0; + } + + // 1. 检查频繁记录(从s2_friend_task表查询,只统计近30天) + $frequentData = $this->checkFrequentFromFriendTask($accountId, $wechatId, $scoreRecord); + $result['lastFrequentTime'] = $frequentData['lastFrequentTime'] ?? null; + $result['frequentCount'] = $frequentData['frequentCount'] ?? 0; + $result['frequentPenalty'] = $frequentData['frequentPenalty'] ?? 0; + + // 2. 检查封号记录(从s2_wechat_message表查询) + $banData = $this->checkBannedFromMessage($accountId, $wechatId); + if (!empty($banData)) { + $result['isBanned'] = $banData['isBanned']; + $result['banPenalty'] = $banData['banPenalty']; + } + + // 3. 计算不频繁加分(基于近30天的频繁记录,反向参考频繁规则) + $noFrequentData = $this->calculateNoFrequentBonus($accountId, $wechatId, $frequentData); + $result['noFrequentBonus'] = $noFrequentData['bonus'] ?? 0; + $result['consecutiveNoFrequentDays'] = $noFrequentData['consecutiveDays'] ?? 0; + $result['lastNoFrequentTime'] = $noFrequentData['lastNoFrequentTime'] ?? null; + + // 计算总分 + $result['total'] = $result['frequentPenalty'] + $result['noFrequentBonus'] + $result['banPenalty']; + + return $result; } /** - * 计算扣分 - * 首次频繁:-15分 - * 再次频繁:-25分 - * 封号:-60分 + * 从s2_friend_task表检查频繁记录 + * extra字段包含"操作过于频繁"即需要扣分 + * 只统计近30天的数据 * - * @param array $accountData 账号数据 - * @return int 扣分数 + * @param int $accountId 账号ID + * @param string $wechatId 微信ID + * @param array $scoreRecord 现有评分记录 + * @return array|null */ - private function calculatePenalty($accountData) + private function checkFrequentFromFriendTask($accountId, $wechatId, $scoreRecord) { + // 计算30天前的时间戳 + $thirtyDaysAgo = time() - (30 * 24 * 3600); + + // 查询包含"操作过于频繁"的记录(只统计近30天) + // extra字段可能是文本或JSON格式,使用LIKE查询 + $frequentTasks = Db::table('s2_friend_task') + ->where('wechatAccountId', $accountId) + ->where('createTime', '>=', $thirtyDaysAgo) + ->where(function($query) use ($wechatId) { + if (!empty($wechatId)) { + $query->where('wechatId', $wechatId); + } + }) + ->where(function($query) { + // 检查extra字段是否包含"操作过于频繁"(可能是文本或JSON) + $query->where('extra', 'like', '%操作过于频繁%') + ->whereOr('extra', 'like', '%"操作过于频繁"%'); + }) + ->order('createTime', 'desc') + ->field('id, createTime, extra') + ->select(); + + // 获取最新的频繁时间 + $latestFrequentTime = !empty($frequentTasks) ? $frequentTasks[0]['createTime'] : null; + + // 计算频繁次数(统计近30天内包含"操作过于频繁"的记录) + $frequentCount = count($frequentTasks); + + // 如果30天内没有频繁记录,清除扣分 + if (empty($frequentTasks)) { + return [ + 'lastFrequentTime' => null, + 'frequentCount' => 0, + 'frequentPenalty' => 0 + ]; + } + + // 根据30天内的频繁次数计算扣分 $penalty = 0; + if ($frequentCount == 1) { + $penalty = self::PENALTY_FIRST_FREQUENT; // 首次频繁-15分 + } elseif ($frequentCount >= 2) { + $penalty = self::PENALTY_SECOND_FREQUENT; // 再次频繁-25分 + } - // 检查是否有频繁记录 - $lastFrequentTime = $accountData['lastFrequentTime'] ?? null; - $frequentCount = $accountData['frequentCount'] ?? 0; + return [ + 'lastFrequentTime' => $latestFrequentTime, + 'frequentCount' => $frequentCount, + 'frequentPenalty' => $penalty + ]; + } + + /** + * 从s2_wechat_message表检查封号记录 + * content包含"你的账号被限制"且msgType为10000 + * 只统计近30天的数据 + * + * @param int $accountId 账号ID + * @param string $wechatId 微信ID + * @return array|null + */ + private function checkBannedFromMessage($accountId, $wechatId) + { + // 计算30天前的时间戳 + $thirtyDaysAgo = time() - (30 * 24 * 3600); - if (!empty($lastFrequentTime)) { - // 判断是首次频繁还是再次频繁 - if ($frequentCount == 1) { - $penalty += self::PENALTY_FIRST_FREQUENT; // 首次频繁-15分 - } elseif ($frequentCount >= 2) { - $penalty += self::PENALTY_SECOND_FREQUENT; // 再次频繁-25分 + // 查询封号消息(只统计近30天) + $banMessage = Db::table('s2_wechat_message') + ->where('wechatAccountId', $accountId) + ->where('msgType', 10000) + ->where('content', 'like', '%你的账号被限制%') + ->where('isDeleted', 0) + ->where('createTime', '>=', $thirtyDaysAgo) + ->order('createTime', 'desc') + ->find(); + + if (!empty($banMessage)) { + return [ + 'isBanned' => 1, + 'banPenalty' => self::PENALTY_BANNED // 封号-60分 + ]; + } + + return [ + 'isBanned' => 0, + 'banPenalty' => 0 + ]; + } + + /** + * 计算不频繁加分 + * 反向参考频繁规则:查询近30天的频繁记录,计算连续不频繁天数 + * 规则:30天内连续不频繁的,只要有一次频繁就得重新计算(重置连续不频繁天数) + * 如果连续3天没有频繁,则每天+5分 + * + * @param int $accountId 账号ID + * @param string $wechatId 微信ID + * @param array $frequentData 频繁数据(包含lastFrequentTime和frequentCount) + * @return array 包含bonus、consecutiveDays、lastNoFrequentTime + */ + private function calculateNoFrequentBonus($accountId, $wechatId, $frequentData) + { + $result = [ + 'bonus' => 0, + 'consecutiveDays' => 0, + 'lastNoFrequentTime' => null + ]; + + if (empty($accountId) || empty($wechatId)) { + return $result; + } + + // 计算30天前的时间戳 + $thirtyDaysAgo = time() - (30 * 24 * 3600); + $currentTime = time(); + + // 获取最后一次频繁时间(30天内最后一次频繁的时间) + $lastFrequentTime = $frequentData['lastFrequentTime'] ?? null; + + // 规则:30天内连续不频繁的,只要有一次频繁就得重新计算(重置连续不频繁天数) + if (empty($lastFrequentTime)) { + // 情况1:30天内没有频繁记录,说明30天内连续不频繁 + // 计算从30天前到现在的连续不频繁天数(最多30天) + $consecutiveDays = min(30, floor(($currentTime - $thirtyDaysAgo) / 86400)); + } else { + // 情况2:30天内有频繁记录,从最后一次频繁时间开始重新计算连续不频繁天数 + // 只要有一次频繁,连续不频繁天数就从最后一次频繁时间开始重新计算 + // 计算从最后一次频繁时间到现在,连续多少天没有频繁 + $timeDiff = $currentTime - $lastFrequentTime; + $consecutiveDays = floor($timeDiff / 86400); // 向下取整,得到完整的天数 + + // 边界情况:如果最后一次频繁时间在30天前(理论上不应该发生,因为查询已经限制了30天),则按30天处理 + if ($lastFrequentTime < $thirtyDaysAgo) { + $consecutiveDays = min(30, floor(($currentTime - $thirtyDaysAgo) / 86400)); } } - // 检查是否封号(这里需要根据实际业务逻辑判断,比如status字段或其他标识) - // 假设status=0表示封号 - $status = $accountData['status'] ?? 1; - if ($status == 0) { - $penalty += self::PENALTY_BANNED; // 封号-60分 + // 如果连续3天或以上没有频繁,则每天+5分 + if ($consecutiveDays >= 3) { + $bonus = $consecutiveDays * self::BONUS_NO_FREQUENT_PER_DAY; + $result['bonus'] = $bonus; + $result['consecutiveDays'] = $consecutiveDays; + $result['lastNoFrequentTime'] = $currentTime; + } else { + $result['consecutiveDays'] = $consecutiveDays; } - return $penalty; - } - - /** - * 计算加分 - * 连续3天不触发频繁:每天+5分 - * - * @param array $accountData 账号数据 - * @return int 加分数 - */ - private function calculateBonus($accountData) - { - $bonus = 0; - - $lastNoFrequentTime = $accountData['lastNoFrequentTime'] ?? null; - $consecutiveNoFrequentDays = $accountData['consecutiveNoFrequentDays'] ?? 0; - - // 如果连续不频繁天数>=3,则每天+5分 - if ($consecutiveNoFrequentDays >= 3) { - $bonus = $consecutiveNoFrequentDays * self::BONUS_NO_FREQUENT_PER_DAY; - } - - return $bonus; + return $result; } /** @@ -252,11 +597,12 @@ class WechatAccountHealthScoreService /** * 批量计算并更新多个账号的健康分 * - * @param array $accountIds 账号ID数组 + * @param array $accountIds 账号ID数组(为空则处理所有账号) * @param int $batchSize 每批处理数量 + * @param bool $forceRecalculateBase 是否强制重新计算基础分 * @return array 处理结果统计 */ - public function batchCalculateAndUpdate($accountIds = [], $batchSize = 100) + public function batchCalculateAndUpdate($accountIds = [], $batchSize = 100, $forceRecalculateBase = false) { $stats = [ 'total' => 0, @@ -280,7 +626,7 @@ class WechatAccountHealthScoreService foreach ($batches as $batch) { foreach ($batch as $accountId) { try { - $this->calculateAndUpdate($accountId); + $this->calculateAndUpdate($accountId, null, $forceRecalculateBase); $stats['success']++; } catch (Exception $e) { $stats['failed']++; @@ -296,38 +642,22 @@ class WechatAccountHealthScoreService } /** - * 记录频繁事件 + * 记录频繁事件(已废弃,改为从s2_friend_task表自动检测) + * 保留此方法以兼容旧代码,实际频繁检测在calculateDynamicScore中完成 * * @param int $accountId 账号ID * @return bool */ public function recordFrequent($accountId) { - $accountData = Db::table('s2_wechat_account') - ->where('id', $accountId) - ->find(); - - if (empty($accountData)) { + // 频繁检测已改为从s2_friend_task表自动检测 + // 直接重新计算健康分即可 + try { + $this->calculateAndUpdate($accountId); + return true; + } catch (\Exception $e) { return false; } - - $frequentCount = ($accountData['frequentCount'] ?? 0) + 1; - - $updateData = [ - 'lastFrequentTime' => time(), - 'frequentCount' => $frequentCount, - 'consecutiveNoFrequentDays' => 0, // 重置连续不频繁天数 - 'lastNoFrequentTime' => null - ]; - - Db::table('s2_wechat_account') - ->where('id', $accountId) - ->update($updateData); - - // 重新计算健康分 - $this->calculateAndUpdate($accountId); - - return true; } /** @@ -338,21 +668,29 @@ class WechatAccountHealthScoreService */ public function recordNoFrequent($accountId) { - $accountData = Db::table('s2_wechat_account') - ->where('id', $accountId) - ->find(); + $scoreRecord = $this->getScoreRecord($accountId); - if (empty($accountData)) { - return false; + if (empty($scoreRecord)) { + // 如果记录不存在,先创建 + $accountData = Db::table('s2_wechat_account') + ->where('id', $accountId) + ->find(); + + if (empty($accountData)) { + return false; + } + + $this->getOrCreateScoreRecord($accountId, $accountData['wechatId']); + $scoreRecord = $this->getScoreRecord($accountId); } - $lastNoFrequentTime = $accountData['lastNoFrequentTime'] ?? null; - $consecutiveNoFrequentDays = $accountData['consecutiveNoFrequentDays'] ?? 0; + $lastNoFrequentTime = $scoreRecord['lastNoFrequentTime'] ?? null; + $consecutiveNoFrequentDays = $scoreRecord['consecutiveNoFrequentDays'] ?? 0; $currentTime = time(); // 如果上次不频繁时间是昨天或更早,则增加连续天数 if (empty($lastNoFrequentTime) || ($currentTime - $lastNoFrequentTime) >= 86400) { - // 如果间隔超过1天,重置为1天 + // 如果间隔超过2天,重置为1天 if (!empty($lastNoFrequentTime) && ($currentTime - $lastNoFrequentTime) > 86400 * 2) { $consecutiveNoFrequentDays = 1; } else { @@ -360,13 +698,21 @@ class WechatAccountHealthScoreService } } + // 计算加分(连续3天及以上才加分) + $bonus = 0; + if ($consecutiveNoFrequentDays >= 3) { + $bonus = $consecutiveNoFrequentDays * self::BONUS_NO_FREQUENT_PER_DAY; + } + $updateData = [ 'lastNoFrequentTime' => $currentTime, - 'consecutiveNoFrequentDays' => $consecutiveNoFrequentDays + 'consecutiveNoFrequentDays' => $consecutiveNoFrequentDays, + 'noFrequentBonus' => $bonus, + 'updateTime' => $currentTime ]; - Db::table('s2_wechat_account') - ->where('id', $accountId) + Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) ->update($updateData); // 重新计算健康分 @@ -374,5 +720,38 @@ class WechatAccountHealthScoreService return true; } + + /** + * 获取账号健康分信息 + * + * @param int $accountId 账号ID + * @return array|null + */ + public function getHealthScore($accountId) + { + $scoreRecord = $this->getScoreRecord($accountId); + + if (empty($scoreRecord)) { + return null; + } + + return [ + 'accountId' => $scoreRecord['accountId'], + 'wechatId' => $scoreRecord['wechatId'], + 'healthScore' => $scoreRecord['healthScore'] ?? 0, + 'baseScore' => $scoreRecord['baseScore'] ?? 0, + 'baseInfoScore' => $scoreRecord['baseInfoScore'] ?? 0, + 'friendCountScore' => $scoreRecord['friendCountScore'] ?? 0, + 'friendCount' => $scoreRecord['friendCount'] ?? 0, + 'dynamicScore' => $scoreRecord['dynamicScore'] ?? 0, + 'frequentPenalty' => $scoreRecord['frequentPenalty'] ?? 0, + 'noFrequentBonus' => $scoreRecord['noFrequentBonus'] ?? 0, + 'banPenalty' => $scoreRecord['banPenalty'] ?? 0, + 'maxAddFriendPerDay' => $scoreRecord['maxAddFriendPerDay'] ?? 0, + 'baseScoreCalculated' => $scoreRecord['baseScoreCalculated'] ?? 0, + 'lastFrequentTime' => $scoreRecord['lastFrequentTime'] ?? null, + 'frequentCount' => $scoreRecord['frequentCount'] ?? 0, + 'isBanned' => $scoreRecord['isBanned'] ?? 0 + ]; + } } - diff --git a/Server/crontab_tasks.md b/Server/crontab_tasks.md index f5849517..f7d1dfc8 100644 --- a/Server/crontab_tasks.md +++ b/Server/crontab_tasks.md @@ -81,6 +81,8 @@ # 消息提醒 */1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think kf:notice >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/kf_notice.log 2>&1 +# 客服评分 +0 2 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechat:calculate-score >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/calculate_score.log 2>&1 @@ -107,4 +109,10 @@ ```bash crontab -l +``` + +```bash +- 本地: php think worker:server +- 线上: php think worker:server -d (自带守护进程,无需搭配Supervisor 之类的工具) +- php think worker:server stop php think worker:server status ``` \ No newline at end of file diff --git a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php index adafc321..247684bc 100644 --- a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php +++ b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php @@ -18,6 +18,7 @@ use think\facade\Config; use think\facade\Log; use app\api\controller\FriendTaskController; use app\common\service\AuthService; +use app\common\service\WechatAccountHealthScoreService; use app\api\controller\WebSocketController; use Workerman\Lib\Timer; @@ -180,13 +181,11 @@ class Adapter implements WeChatServiceInterface ->select(); $taskData = array_merge($taskData, $tasks); } - if ($taskData) { foreach ($taskData as $task) { $task_id = $task['task_id']; $task_info = $this->getCustomerAcquisitionTask($task_id); - if (empty($task_info['status']) || empty($task_info['reqConf']) || empty($task_info['reqConf']['device'])) { continue; } @@ -213,9 +212,86 @@ class Adapter implements WeChatServiceInterface continue; } - // 判断24h内加的好友数量,friend_task 先固定10个人 getLast24hAddedFriendsCount + // 根据健康分判断24h内加的好友数量限制 + $healthScoreService = new WechatAccountHealthScoreService(); + $healthScoreInfo = $healthScoreService->getHealthScore($accountId); + + // 如果健康分记录不存在,先计算一次 + if (empty($healthScoreInfo)) { + try { + $healthScoreService->calculateAndUpdate($accountId); + $healthScoreInfo = $healthScoreService->getHealthScore($accountId); + } catch (\Exception $e) { + Log::error("计算健康分失败 (accountId: {$accountId}): " . $e->getMessage()); + // 如果计算失败,使用默认值5作为兜底 + $maxAddFriendPerDay = 5; + } + } + + // 获取每日最大加人次数(基于健康分) + $maxAddFriendPerDay = $healthScoreInfo['maxAddFriendPerDay'] ?? 5; + + // 如果健康分为0或很低,不允许添加好友 + if ($maxAddFriendPerDay <= 0) { + Log::info("账号健康分过低,不允许添加好友 (accountId: {$accountId}, wechatId: {$wechatId}, healthScore: " . ($healthScoreInfo['healthScore'] ?? 0) . ")"); + continue; + } + + // 检查频繁暂停限制:首次频繁或再次频繁,暂停24小时 + $lastFrequentTime = $healthScoreInfo['lastFrequentTime'] ?? null; + $frequentCount = $healthScoreInfo['frequentCount'] ?? 0; + if (!empty($lastFrequentTime) && $frequentCount > 0) { + $frequentPauseHours = 24; // 频繁暂停24小时 + $frequentPauseTime = $lastFrequentTime + ($frequentPauseHours * 3600); + $currentTime = time(); + + if ($currentTime < $frequentPauseTime) { + $remainingHours = ceil(($frequentPauseTime - $currentTime) / 3600); + Log::info("账号频繁,暂停添加好友 (accountId: {$accountId}, wechatId: {$wechatId}, frequentCount: {$frequentCount}, 剩余暂停时间: {$remainingHours}小时)"); + continue; + } + } + + // 检查封号暂停限制:封号暂停72小时 + $isBanned = $healthScoreInfo['isBanned'] ?? 0; + if ($isBanned == 1) { + // 查询封号时间(从s2_wechat_message表查询最近一次封号消息) + $banMessage = Db::table('s2_wechat_message') + ->where('wechatAccountId', $accountId) + ->where('msgType', 10000) + ->where('content', 'like', '%你的账号被限制%') + ->where('isDeleted', 0) + ->order('createTime', 'desc') + ->find(); + + if (!empty($banMessage)) { + $banTime = $banMessage['createTime'] ?? 0; + $banPauseHours = 72; // 封号暂停72小时 + $banPauseTime = $banTime + ($banPauseHours * 3600); + $currentTime = time(); + + if ($currentTime < $banPauseTime) { + $remainingHours = ceil(($banPauseTime - $currentTime) / 3600); + Log::info("账号封号,暂停添加好友 (accountId: {$accountId}, wechatId: {$wechatId}, 剩余暂停时间: {$remainingHours}小时)"); + continue; + } + } + } + + // 判断今天添加的好友数量,使用健康分计算的每日最大加人次数 + // 优先使用今天添加的好友数量(更符合"每日"限制) + $todayAddedFriendsCount = $this->getTodayAddedFriendsCount($wechatId); + if ($todayAddedFriendsCount >= $maxAddFriendPerDay) { + Log::info("今天添加好友数量已达上限 (accountId: {$accountId}, wechatId: {$wechatId}, count: {$todayAddedFriendsCount}, max: {$maxAddFriendPerDay}, healthScore: " . ($healthScoreInfo['healthScore'] ?? 0) . ")"); + continue; + } + + // 如果今天添加数量未达上限,再检查24小时内的数量(作为额外保护) $last24hAddedFriendsCount = $this->getLast24hAddedFriendsCount($wechatId); - if ($last24hAddedFriendsCount >= 20) { + // 24小时内的限制可以稍微宽松一些,设置为每日限制的1.2倍(防止跨天累积) + $max24hLimit = (int)ceil($maxAddFriendPerDay * 1.2); + if ($last24hAddedFriendsCount >= $max24hLimit) { + Log::info("24小时内添加好友数量已达上限 (accountId: {$accountId}, wechatId: {$wechatId}, count: {$last24hAddedFriendsCount}, max24h: {$max24hLimit}, maxDaily: {$maxAddFriendPerDay})"); continue; } @@ -828,6 +904,7 @@ class Adapter implements WeChatServiceInterface if (empty($deviceIds)) { return []; } + $records = Db::table('s2_wechat_account') ->where('deviceAlive', 1) ->where('wechatAlive', 1) From d296d019e54190d2c74623236dd48bc3918858a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Fri, 21 Nov 2025 14:55:56 +0800 Subject: [PATCH 17/31] Enhance MessageEnter component by adding a map selection feature. Introduced a new button for map visibility and integrated SelectMap component for improved user interaction. --- .../InputMessage/InputMessage.tsx | 0 .../InputMessage/index.module.scss | 147 --- .../StepSendMessage/index.module.scss | 265 ------ .../components/StepSendMessage/index.tsx | 6 - Moncter/src/pages/pc/ckbox/weChat/api.ts | 0 .../components/ProfileModules/index.tsx | 0 .../MessageEnter/components/selectMap.tsx | 900 ++++++++++++++++++ .../components/MessageEnter/index.tsx | 31 +- 8 files changed, 928 insertions(+), 421 deletions(-) delete mode 100644 Moncter/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/InputMessage.tsx delete mode 100644 Moncter/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/index.module.scss delete mode 100644 Moncter/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.module.scss delete mode 100644 Moncter/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.tsx delete mode 100644 Moncter/src/pages/pc/ckbox/weChat/api.ts delete mode 100644 Moncter/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx create mode 100644 Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx diff --git a/Moncter/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/InputMessage.tsx b/Moncter/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/InputMessage.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/Moncter/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/index.module.scss b/Moncter/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/index.module.scss deleted file mode 100644 index b8085ce9..00000000 --- a/Moncter/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/InputMessage/index.module.scss +++ /dev/null @@ -1,147 +0,0 @@ -.chatFooter { - background: #f7f7f7; - border-top: 1px solid #e1e1e1; - padding: 0; - height: auto; - border-radius: 8px; -} - -.inputContainer { - padding: 8px 12px; - display: flex; - flex-direction: column; - gap: 6px; -} - -.inputToolbar { - display: flex; - align-items: center; - padding: 4px 0; -} - -.leftTool { - display: flex; - gap: 4px; - align-items: center; -} - -.toolbarButton { - width: 28px; - height: 28px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - color: #666; - font-size: 16px; - transition: all 0.15s; - border: none; - background: transparent; - - &:hover { - background: #e6e6e6; - color: #333; - } - - &:active { - background: #d9d9d9; - } -} - -.inputArea { - display: flex; - flex-direction: column; - padding: 4px 0; -} - -.inputWrapper { - border: 1px solid #d1d1d1; - border-radius: 4px; - background: #fff; - overflow: hidden; - - &:focus-within { - border-color: #07c160; - } -} - -.messageInput { - width: 100%; - border: none; - resize: none; - font-size: 13px; - line-height: 1.4; - padding: 8px 10px; - background: transparent; - - &:focus { - box-shadow: none; - outline: none; - } - - &::placeholder { - color: #b3b3b3; - } -} - -.sendButtonArea { - padding: 8px 10px; - display: flex; - justify-content: flex-end; - gap: 8px; -} - -.sendButton { - height: 32px; - border-radius: 4px; - font-weight: normal; - min-width: 60px; - font-size: 13px; - background: #07c160; - border-color: #07c160; - - &:hover { - background: #06ad56; - border-color: #06ad56; - } - - &:active { - background: #059748; - border-color: #059748; - } - - &:disabled { - background: #b3b3b3; - border-color: #b3b3b3; - opacity: 1; - } -} - -.hintButton { - border: none; - background: transparent; - color: #666; - font-size: 12px; - - &:hover { - color: #333; - } -} - -.inputHint { - font-size: 11px; - color: #999; - text-align: right; - margin-top: 2px; -} - -@media (max-width: 768px) { - .inputToolbar { - flex-wrap: wrap; - gap: 8px; - } - - .sendButtonArea { - justify-content: space-between; - } -} diff --git a/Moncter/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.module.scss b/Moncter/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.module.scss deleted file mode 100644 index fee50c0a..00000000 --- a/Moncter/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.module.scss +++ /dev/null @@ -1,265 +0,0 @@ -.stepContent { - .stepHeader { - margin-bottom: 20px; - - h3 { - font-size: 18px; - font-weight: 600; - color: #1a1a1a; - margin: 0 0 8px 0; - } - - p { - font-size: 14px; - color: #666; - margin: 0; - } - } -} - -.step3Content { - display: flex; - gap: 24px; - align-items: flex-start; - - .leftColumn { - flex: 1; - display: flex; - flex-direction: column; - gap: 20px; - } - - .rightColumn { - width: 400px; - flex: 1; - display: flex; - flex-direction: column; - gap: 20px; - } - - .messagePreview { - border: 2px dashed #52c41a; - border-radius: 8px; - padding: 20px; - background: #f6ffed; - - .previewTitle { - font-size: 14px; - color: #52c41a; - font-weight: 500; - margin-bottom: 12px; - } - - .messageBubble { - min-height: 60px; - padding: 12px; - background: #fff; - border-radius: 6px; - color: #666; - font-size: 14px; - line-height: 1.6; - - .currentEditingLabel { - font-size: 12px; - color: #999; - margin-bottom: 8px; - } - - .messageText { - color: #333; - white-space: pre-wrap; - word-break: break-word; - } - } - } - - .savedScriptGroups { - .scriptGroupTitle { - font-size: 14px; - font-weight: 500; - color: #333; - margin-bottom: 12px; - } - - .scriptGroupItem { - border: 1px solid #e8e8e8; - border-radius: 8px; - padding: 12px; - margin-bottom: 12px; - background: #fff; - - .scriptGroupHeader { - display: flex; - justify-content: space-between; - align-items: center; - - .scriptGroupLeft { - display: flex; - align-items: center; - gap: 8px; - flex: 1; - - :global(.ant-radio) { - margin-right: 4px; - } - - .scriptGroupName { - font-size: 14px; - font-weight: 500; - color: #333; - } - - .messageCount { - font-size: 12px; - color: #999; - margin-left: 8px; - } - } - - .scriptGroupActions { - display: flex; - gap: 4px; - - .actionButton { - padding: 4px; - color: #666; - - &:hover { - color: #1890ff; - } - } - } - } - - .scriptGroupContent { - margin-top: 8px; - padding-top: 8px; - border-top: 1px solid #f0f0f0; - font-size: 13px; - color: #666; - } - } - } - - .messageInputArea { - .messageInput { - margin-bottom: 12px; - } - - .attachmentButtons { - display: flex; - gap: 8px; - margin-bottom: 12px; - } - - .aiRewriteSection { - display: flex; - align-items: center; - margin-bottom: 8px; - } - - .messageHint { - font-size: 12px; - color: #999; - } - } - - .settingsPanel { - border: 1px solid #e8e8e8; - border-radius: 8px; - padding: 20px; - background: #fafafa; - - .settingsTitle { - font-size: 14px; - font-weight: 500; - color: #1a1a1a; - margin-bottom: 16px; - } - - .settingItem { - margin-bottom: 20px; - - &:last-child { - margin-bottom: 0; - } - - .settingLabel { - font-size: 14px; - font-weight: 500; - color: #1a1a1a; - margin-bottom: 12px; - } - - .settingControl { - display: flex; - align-items: center; - gap: 8px; - - span { - font-size: 14px; - color: #666; - min-width: 80px; - } - } - } - } - - .tagSection { - .settingLabel { - font-size: 14px; - font-weight: 500; - color: #1a1a1a; - margin-bottom: 12px; - } - } - - .pushPreview { - border: 1px solid #e8e8e8; - border-radius: 8px; - padding: 20px; - background: #f0f7ff; - - .previewTitle { - font-size: 14px; - font-weight: 500; - color: #1a1a1a; - margin-bottom: 12px; - } - - ul { - list-style: none; - padding: 0; - margin: 0; - - li { - font-size: 14px; - color: #666; - line-height: 1.8; - } - } - } -} - -@media (max-width: 1200px) { - .step3Content { - .rightColumn { - width: 350px; - } - } -} - -@media (max-width: 768px) { - .step3Content { - flex-direction: column; - - .leftColumn { - width: 100%; - } - - .rightColumn { - width: 100%; - } - } -} - diff --git a/Moncter/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.tsx b/Moncter/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.tsx deleted file mode 100644 index 082831d5..00000000 --- a/Moncter/src/pages/pc/ckbox/powerCenter/message-push-assistant/create-push-task/components/StepSendMessage/index.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import ContentSelection from "@/components/ContentSelection"; -import { ContentItem } from "@/components/ContentSelection/data"; -import InputMessage from "./InputMessage/InputMessage"; -import styles from "./index.module.scss"; - -interface StepSendMessageProps { diff --git a/Moncter/src/pages/pc/ckbox/weChat/api.ts b/Moncter/src/pages/pc/ckbox/weChat/api.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/Moncter/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx b/Moncter/src/pages/pc/ckbox/weChat/components/ChatWindow/components/ProfileCard/components/ProfileModules/index.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx new file mode 100644 index 00000000..7393c08c --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx @@ -0,0 +1,900 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { + AutoComplete, + Input as AntInput, + message, + Modal, + Spin, + Button, +} from "antd"; +import { ContractData, weChatGroup, ChatRecord } from "@/pages/pc/ckbox/data"; +import { useWebSocketStore } from "@/store/module/websocket/websocket"; + +declare const AMap: any; + +interface SelectMapProps { + visible: boolean; + onClose: () => void; + contract: ContractData | weChatGroup; + addMessage: (message: ChatRecord) => void; +} + +const SelectMap: React.FC = ({ + visible, + onClose, + contract, + addMessage, +}) => { + const [selectedLocation, setSelectedLocation] = useState(null); + const mapRef = useRef(null); + const markersRef = useRef([]); // 使用数组保存所有标记点 + const geocoderRef = useRef(null); // 保存 Geocoder 实例 + const placeSearchRef = useRef(null); // 保存 PlaceSearch 实例 + const geolocationRef = useRef(null); // 保存 Geolocation 实例 + const pendingClickRef = useRef<{ + lat: number; + lng: number; + lnglat: any; + } | null>(null); // 保存待处理的点击坐标 + const [options, setOptions] = useState([]); + const [mapLoading, setMapLoading] = useState(true); + + const sendCommand = useWebSocketStore(state => state.sendCommand); + + // 清除所有标记点 + const clearAllMarkers = useCallback(() => { + if (markersRef.current && markersRef.current.length > 0) { + console.log(`清除 ${markersRef.current.length} 个标记点`); + markersRef.current.forEach(marker => { + if (marker) { + marker.setMap(null); + marker = null; + } + }); + markersRef.current = []; + } + }, []); + + const addMarker = useCallback( + (lnglat: any) => { + console.log("addMarker 调用,坐标:", lnglat); + + // 先清除所有现有的标记点 + clearAllMarkers(); + + // 创建红色图标,保持默认倒水滴形状 + const redIcon = new AMap.Icon({ + size: new AMap.Size(25, 34), // 默认标记点尺寸 + image: "https://webapi.amap.com/theme/v1.3/markers/n/mark_r.png", // 红色标记点图片 + imageOffset: new AMap.Pixel(0, 0), + imageSize: new AMap.Size(25, 34), + }); + + // 创建新的标记点 + const newMarker = new AMap.Marker({ + position: lnglat, + map: mapRef.current, + icon: redIcon, + }); + + // 将新标记点添加到数组中 + markersRef.current.push(newMarker); + + mapRef.current.setCenter(lnglat); + mapRef.current.setZoom(16); // 确保缩放到合适级别 + console.log("新 marker 已添加并居中"); + }, + [clearAllMarkers], + ); + + // 通用的地址获取函数 + const getAddressForLocation = useCallback( + (lat: number, lng: number, lnglat: any) => { + console.log("=== getAddressForLocation 调用 ==="); + console.log("坐标:", { lat, lng }); + console.log( + "Geocoder ref 状态:", + geocoderRef.current ? "存在" : "不存在", + ); + + // 检查 Geocoder 是否已初始化 + if (!geocoderRef.current) { + console.warn("Geocoder 未初始化,无法获取地址"); + return false; + } + + // 使用更精确的坐标进行查询 + // 高德地图 getAddress 支持 LngLat 对象或 [lng, lat] 数组 + let queryLnglat: any; + if (lnglat && typeof lnglat.getLat === "function") { + // 如果传入的是 LngLat 对象,直接使用 + queryLnglat = lnglat; + } else { + // 否则创建新的 LngLat 对象或使用数组格式 + try { + queryLnglat = new AMap.LngLat(lng, lat); + } catch (error) { + console.error("创建 LngLat 对象失败,使用数组格式:", error); + queryLnglat = [lng, lat]; + } + } + console.log("调用 geocoder.getAddress,坐标:", queryLnglat); + + try { + geocoderRef.current.getAddress( + queryLnglat, + (status: string, result: any) => { + console.log("=== Geocoder 回调触发(通用函数) ==="); + console.log("Status:", status); + console.log( + "Result:", + result ? JSON.stringify(result, null, 2) : "null", + ); + + if ( + status === "complete" && + result && + result.info === "OK" && + result.regeocode + ) { + const regeocode = result.regeocode; + const formattedAddress = regeocode.formattedAddress || ""; + const addressComponent = regeocode.addressComponent || {}; + + // 构建详细地址信息 + let addressLabel = formattedAddress; + let poiName = "点击位置"; + + // 优先级1: 如果有POI信息,优先使用POI名称 + if (regeocode.pois && regeocode.pois.length > 0) { + const poi = regeocode.pois[0]; + poiName = poi.name || poiName; + const poiAddress = poi.address || ""; + addressLabel = poiAddress + ? `${poiName} ${poiAddress}` + : `${poiName} ${formattedAddress}`; + } + // 优先级2: 如果有建筑物信息 + else if (regeocode.buildings && regeocode.buildings.length > 0) { + const building = regeocode.buildings[0]; + poiName = building.name || addressComponent.building || poiName; + addressLabel = `${poiName} ${formattedAddress}`; + } + // 优先级3: 如果有AOI(兴趣区域)信息 + else if (regeocode.aois && regeocode.aois.length > 0) { + const aoi = regeocode.aois[0]; + poiName = aoi.name || poiName; + addressLabel = `${poiName} ${formattedAddress}`; + } + // 优先级4: 使用地址组件构建详细地址 + else if (addressComponent.building) { + poiName = addressComponent.building; + addressLabel = `${poiName} ${formattedAddress}`; + } + // 优先级5: 组合地址组件 + else { + const parts = []; + if (addressComponent.province) + parts.push(addressComponent.province); + if (addressComponent.city) parts.push(addressComponent.city); + if (addressComponent.district) + parts.push(addressComponent.district); + if (addressComponent.township) + parts.push(addressComponent.township); + if (addressComponent.street) + parts.push(addressComponent.street); + if (addressComponent.streetNumber) + parts.push(addressComponent.streetNumber); + + if (parts.length > 0) { + const fullAddress = parts.join(""); + poiName = + addressComponent.street || + addressComponent.district || + "点击位置"; + addressLabel = fullAddress || formattedAddress; + } else { + addressLabel = + formattedAddress || `纬度: ${lat}, 经度: ${lng}`; + } + } + + setSelectedLocation({ + lat: lat, + lng: lng, + label: addressLabel, + poiname: poiName, + }); + + message.success("地址信息获取成功"); + } else { + console.warn("=== Geocoder 返回异常,尝试 PlaceSearch ==="); + console.warn("Status:", status); + console.warn("Result:", result); + // Geocoder 失败,使用 PlaceSearch 作为备用 + if (placeSearchRef.current) { + try { + const searchLnglat = lnglat || [lng, lat]; + placeSearchRef.current.searchNearBy( + "", + searchLnglat, + 1000, + (searchStatus: string, searchResult: any) => { + if ( + searchStatus === "complete" && + searchResult && + searchResult.info === "OK" && + searchResult.poiList?.pois?.length > 0 + ) { + const poi = searchResult.poiList.pois[0]; + const poiLabel = poi.address + ? `${poi.name} ${poi.address}` + : poi.name; + setSelectedLocation({ + lat: lat, + lng: lng, + label: poiLabel, + poiname: poi.name, + }); + message.success("通过附近搜索获取到地址信息"); + } else { + console.warn("PlaceSearch 返回异常:", { + status: searchStatus, + result: searchResult, + }); + const coordLabel = `纬度: ${lat}, 经度: ${lng}`; + setSelectedLocation({ + lat: lat, + lng: lng, + label: coordLabel, + poiname: "点击位置", + }); + message.warning("无法获取详细地址信息,但坐标已记录"); + } + }, + ); + } catch (placeSearchError) { + console.error( + "PlaceSearch.searchNearBy 调用异常:", + placeSearchError, + ); + const coordLabel = `纬度: ${lat}, 经度: ${lng}`; + setSelectedLocation({ + lat: lat, + lng: lng, + label: coordLabel, + poiname: "点击位置", + }); + message.warning("无法获取详细地址信息,但坐标已记录"); + } + } else { + const coordLabel = `纬度: ${lat}, 经度: ${lng}`; + setSelectedLocation({ + lat: lat, + lng: lng, + label: coordLabel, + poiname: "点击位置", + }); + message.warning("无法获取详细地址信息,但坐标已记录"); + } + } + }, + ); + } catch (error) { + console.error("=== Geocoder.getAddress 调用异常 ===", error); + // 如果 geocoder 调用失败,尝试使用 PlaceSearch + if (placeSearchRef.current) { + console.log("尝试使用 PlaceSearch 作为备用方案"); + try { + placeSearchRef.current.searchNearBy( + "", + lnglat || [lng, lat], + 1000, + (status: string, result: any) => { + if ( + status === "complete" && + result && + result.info === "OK" && + result.poiList?.pois?.length > 0 + ) { + const poi = result.poiList.pois[0]; + const poiLabel = poi.address + ? `${poi.name} ${poi.address}` + : poi.name; + setSelectedLocation({ + lat: lat, + lng: lng, + label: poiLabel, + poiname: poi.name, + }); + message.success("通过附近搜索获取到地址信息"); + } else { + const coordLabel = `纬度: ${lat}, 经度: ${lng}`; + setSelectedLocation({ + lat: lat, + lng: lng, + label: coordLabel, + poiname: "点击位置", + }); + message.warning("无法获取详细地址信息,但坐标已记录"); + } + }, + ); + } catch (placeSearchError) { + console.error("PlaceSearch 调用也失败:", placeSearchError); + const coordLabel = `纬度: ${lat}, 经度: ${lng}`; + setSelectedLocation({ + lat: lat, + lng: lng, + label: coordLabel, + poiname: "点击位置", + }); + message.warning("无法获取详细地址信息,但坐标已记录"); + } + } else { + const coordLabel = `纬度: ${lat}, 经度: ${lng}`; + setSelectedLocation({ + lat: lat, + lng: lng, + label: coordLabel, + poiname: "点击位置", + }); + message.warning("无法获取详细地址信息,但坐标已记录"); + } + return false; + } + + return true; + }, + [], + ); + + const initMap = useCallback(() => { + console.log("initMap 执行中"); + setMapLoading(true); + + // 确保容器存在 + const container = document.getElementById("amap-container"); + if (!container) { + console.error("地图容器不存在"); + setMapLoading(false); + return; + } + + // 确保容器样式正确 + container.style.pointerEvents = "auto"; + container.style.cursor = "crosshair"; + container.style.position = "relative"; + container.style.zIndex = "1"; + + const map = new AMap.Map("amap-container", { + zoom: 16, + center: [118.113653, 24.470164], // 默认中心 + viewMode: "2D", // 明确指定视图模式 + }); + mapRef.current = map; + + // 添加超时机制,防止 loading 一直显示 + const loadingTimeout = setTimeout(() => { + console.warn("地图加载超时,强制关闭 loading"); + setMapLoading(false); + }, 10000); // 10秒超时 + + // 立即加载插件,不等待地图 complete 事件 + console.log("=== 立即开始加载 AMap 插件(地图创建后) ==="); + AMap.plugin( + [ + "AMap.AutoComplete", + "AMap.PlaceSearch", + "AMap.Geocoder", + "AMap.Geolocation", + ], + error => { + if (error) { + console.error("=== AMap 插件加载失败 ===", error); + message.error("地图插件加载失败,部分功能可能不可用"); + clearTimeout(loadingTimeout); + setMapLoading(false); + return; + } + console.log("=== AMap 插件加载成功 ==="); + + // 立即创建 PlaceSearch 和 Geocoder 实例 + const placeSearch = new AMap.PlaceSearch({ + city: "全国", + map: map, + }); + placeSearchRef.current = placeSearch; + console.log("PlaceSearch 实例已创建"); + + const geocoder = new AMap.Geocoder({ + city: "全国", + radius: 1000, // 搜索半径,单位米 + extensions: "all", // 返回详细信息,包括POI、建筑物等 + }); + geocoderRef.current = geocoder; + console.log("Geocoder 实例已创建并保存到 ref,现在可以立即使用"); + + // 创建 Geolocation 实例用于获取当前位置 + const geolocation = new AMap.Geolocation({ + enableHighAccuracy: true, // 是否使用高精度定位,默认:true + timeout: 10000, // 超过10秒后停止定位,默认:无穷大 + maximumAge: 0, // 定位结果缓存0毫秒,默认:0 + convert: true, // 自动偏移坐标,偏移后的坐标为高德坐标,默认:true + showButton: false, // 显示定位按钮,默认:true + buttonPosition: "RB", // 定位按钮停靠位置,默认:'LB',左下角 + showMarker: false, // 定位成功后在定位到的位置显示点标记,默认:true + showCircle: false, // 定位成功后用圆圈表示定位精度范围,默认:true + panToLocation: false, // 定位成功后将定位到的位置作为地图中心点,默认:true + zoomToAccuracy: false, // 定位成功后调整地图视野范围使定位位置及精度范围视野内可见,默认:false + }); + geolocationRef.current = geolocation; + + // 获取当前位置 + console.log("=== 开始获取当前位置 ==="); + geolocation.getCurrentPosition((status: string, result: any) => { + console.log("=== 定位回调触发 ==="); + console.log("定位状态:", status); + console.log("定位结果:", result); + + if (status === "complete") { + const { position, formattedAddress, addressComponent } = result; + const lat = position.lat; + const lng = position.lng; + const lnglat = new AMap.LngLat(lng, lat); + + console.log("定位成功,当前位置:", { lat, lng }); + console.log("定位地址:", formattedAddress); + + // 将地图中心设置为当前位置 + map.setCenter(lnglat); + map.setZoom(16); + + // 添加当前位置标记 + addMarker(lnglat); + + // 设置选中位置信息 + let addressLabel = formattedAddress || `纬度: ${lat}, 经度: ${lng}`; + let poiName = "当前位置"; + + // 尝试从地址组件中获取更详细的信息 + if (addressComponent) { + const parts = []; + if (addressComponent.province) + parts.push(addressComponent.province); + if (addressComponent.city) parts.push(addressComponent.city); + if (addressComponent.district) + parts.push(addressComponent.district); + if (addressComponent.street) parts.push(addressComponent.street); + if (addressComponent.streetNumber) + parts.push(addressComponent.streetNumber); + + if (parts.length > 0) { + addressLabel = parts.join(""); + } + + poiName = + addressComponent.street || + addressComponent.district || + "当前位置"; + } + + setSelectedLocation({ + lat: lat, + lng: lng, + label: addressLabel, + poiname: poiName, + }); + + message.success("已获取当前位置"); + } else { + console.warn("定位失败:", result); + message.warning( + "无法获取当前位置,请手动点击地图选择位置。原因: " + + (result.message || "定位服务不可用"), + ); + // 定位失败时,使用默认中心点 + console.log("使用默认中心点"); + } + }); + + // 如果有待处理的点击坐标,立即处理它 + if (pendingClickRef.current) { + console.log("=== 检测到待处理的点击坐标,立即处理 ==="); + const { lat, lng, lnglat } = pendingClickRef.current; + console.log("待处理坐标:", { lat, lng }); + setTimeout(() => { + const success = getAddressForLocation(lat, lng, lnglat); + if (success) { + console.log("✓ 待处理的坐标已成功获取地址"); + pendingClickRef.current = null; // 清除待处理坐标 + } else { + console.warn("待处理的坐标获取地址失败"); + } + }, 100); + } + }, + ); + + // 立即绑定点击事件,插件可能已加载或正在加载 + console.log("立即绑定点击事件(插件可能已初始化)"); + map.on("click", (e: any) => { + console.log("=== 地图点击事件触发 ==="); + console.log("点击事件对象:", e); + console.log("点击位置对象:", e.lnglat); + console.log( + "Geocoder ref 状态:", + geocoderRef.current ? "已初始化" : "未初始化", + ); + + if (!e || !e.lnglat) { + console.error("点击事件无效,缺少 lnglat"); + return; + } + + const lnglat = e.lnglat; + const lat = lnglat.getLat(); + const lng = lnglat.getLng(); + console.log(`点击坐标 - 纬度: ${lat}, 经度: ${lng}`); + + // 立即添加标记和居中 + addMarker(lnglat); + + // 设置基本 selectedLocation(至少有坐标) + setSelectedLocation({ + lat: lat, + lng: lng, + label: "正在获取地址信息...", + poiname: "点击位置", + }); + + // 如果 Geocoder 已初始化,立即使用它 + if (geocoderRef.current) { + console.log("Geocoder 已初始化,立即获取地址"); + getAddressForLocation(lat, lng, lnglat); + } else { + console.log("Geocoder 未初始化,保存坐标待插件加载完成后处理"); + // 保存待处理的坐标 + pendingClickRef.current = { lat, lng, lnglat }; + } + }); + + // 等待地图完全加载后关闭 loading + map.on("complete", () => { + console.log("地图加载完成"); + // 清除超时定时器 + clearTimeout(loadingTimeout); + // 关闭 loading + setMapLoading(false); + console.log("地图加载完成,loading 已关闭"); + + // 确保地图容器可点击 + const mapContainer = map.getContainer(); + if (mapContainer) { + mapContainer.style.pointerEvents = "auto"; + mapContainer.style.cursor = "crosshair"; + console.log("地图容器指针事件已启用"); + } + }); + + // 如果地图加载失败,也设置加载完成 + map.on("error", (error: any) => { + console.error("地图加载错误:", error); + clearTimeout(loadingTimeout); + setMapLoading(false); + message.error("地图加载失败"); + }); + }, [addMarker, getAddressForLocation]); + + const handleSearch = value => { + if (value) { + AMap.plugin("AMap.AutoComplete", () => { + const auto = new AMap.AutoComplete({ city: "全国" }); + auto.search(value, (status, result) => { + if (status === "complete") { + setOptions( + result.tips.map(tip => ({ + value: tip.name, + data: tip, + })), + ); + } + }); + }); + } else { + setOptions([]); + } + }; + + const onSelect = (value, option) => { + const { district, address, name, location } = option.data; + const lnglat = location; + setSelectedLocation({ + lat: lnglat.lat, + lng: lnglat.lng, + label: `${name} ${address || district}`, + poiname: name, + }); + addMarker(lnglat); + mapRef.current.setCenter(lnglat); + }; + + const handleModalChange = useCallback( + (visible: boolean) => { + if (visible) { + console.log("模态打开:开始加载地图脚本"); + setMapLoading(true); + setSelectedLocation(null); + const script = document.createElement("script"); + script.src = + "https://webapi.amap.com/maps?v=1.4.15&key=79370028f5763e46742125ed2e900c76&plugin=AMap.PlaceSearch,AMap.AutoComplete,AMap.Geocoder,AMap.Geolocation"; + script.async = true; + script.onload = () => { + console.log("脚本加载成功:开始初始化地图"); + setTimeout(() => initMap(), 100); // 添加延迟确保 DOM 就绪 + }; + script.onerror = () => { + console.error("脚本加载失败"); + message.error("地图加载失败,请检查网络或API密钥"); + setMapLoading(false); + }; + document.body.appendChild(script); + } else { + console.log("模态关闭:清理地图和脚本"); + // 清除所有标记点 + clearAllMarkers(); + // 重置 ref + geocoderRef.current = null; + placeSearchRef.current = null; + geolocationRef.current = null; + // Cleanup on close + const scripts = document.querySelectorAll( + 'script[src*="webapi.amap.com"]', + ); + scripts.forEach(s => { + if (document.body.contains(s)) { + document.body.removeChild(s); + } + }); + if (mapRef.current) { + mapRef.current.destroy(); + mapRef.current = null; + } + setMapLoading(false); + setOptions([]); // 重置搜索选项 + } + }, + [initMap, clearAllMarkers], + ); + + // 手动获取当前位置 + const handleGetCurrentLocation = useCallback(() => { + if (!geolocationRef.current) { + message.warning("定位服务未初始化,请稍候再试"); + return; + } + + console.log("=== 手动触发获取当前位置 ==="); + message.loading({ content: "正在获取当前位置...", key: "location" }); + + geolocationRef.current.getCurrentPosition((status: string, result: any) => { + message.destroy("location"); + console.log("=== 手动定位回调触发 ==="); + console.log("定位状态:", status); + console.log("定位结果:", result); + + if (status === "complete" && result && result.position) { + const { position, formattedAddress, addressComponent } = result; + const lat = position.lat; + const lng = position.lng; + const lnglat = new AMap.LngLat(lng, lat); + + console.log("定位成功,当前位置:", { lat, lng }); + + // 将地图中心设置为当前位置 + if (mapRef.current) { + mapRef.current.setCenter(lnglat); + mapRef.current.setZoom(16); + } + + // 添加当前位置标记 + addMarker(lnglat); + + // 设置选中位置信息 + let addressLabel = formattedAddress || `纬度: ${lat}, 经度: ${lng}`; + let poiName = "当前位置"; + + // 尝试从地址组件中获取更详细的信息 + if (addressComponent) { + const parts = []; + if (addressComponent.province) parts.push(addressComponent.province); + if (addressComponent.city) parts.push(addressComponent.city); + if (addressComponent.district) parts.push(addressComponent.district); + if (addressComponent.street) parts.push(addressComponent.street); + if (addressComponent.streetNumber) + parts.push(addressComponent.streetNumber); + + if (parts.length > 0) { + addressLabel = parts.join(""); + } + + poiName = + addressComponent.street || addressComponent.district || "当前位置"; + } + + setSelectedLocation({ + lat: lat, + lng: lng, + label: addressLabel, + poiname: poiName, + }); + + message.success("已获取当前位置"); + } else { + console.warn("定位失败:", result); + message.error( + "获取当前位置失败: " + (result?.message || "定位服务不可用"), + ); + } + }); + }, [addMarker]); + + const handleSendLocation = () => { + if (!selectedLocation || !selectedLocation.lat || !selectedLocation.lng) { + message.warning("请选择有效位置"); + return; + } + + const { lat, lng, label, poiname } = selectedLocation; + const content = ``; + + const messageId = +Date.now(); + const params = { + wechatAccountId: contract.wechatAccountId, + wechatChatroomId: contract?.chatroomId ? contract.id : 0, + wechatFriendId: contract?.chatroomId ? 0 : contract.id, + msgSubType: 0, + msgType: 48, + content: content, + seq: messageId, + }; + + // 构造本地消息 + const localMessage: ChatRecord = { + id: messageId, + wechatAccountId: contract.wechatAccountId, + wechatFriendId: contract?.chatroomId ? 0 : contract.id, + wechatChatroomId: contract?.chatroomId ? contract.id : 0, + tenantId: 0, + accountId: 0, + synergyAccountId: 0, + content: content, + msgType: 48, + msgSubType: 0, + msgSvrId: "", + isSend: true, + createTime: new Date().toISOString(), + isDeleted: false, + deleteTime: "", + sendStatus: 1, + wechatTime: Date.now(), + origin: 0, + msgId: 0, + recalled: false, + seq: messageId, + }; + addMessage(localMessage); + + sendCommand("CmdSendMessage", params); + + onClose(); + setSelectedLocation(null); + }; + + useEffect(() => { + if (visible) { + handleModalChange(true); + // 确保容器在模态框打开后可以接收事件 + setTimeout(() => { + const container = document.getElementById("amap-container"); + if (container) { + container.style.pointerEvents = "auto"; + container.style.cursor = "crosshair"; + console.log( + "容器样式已设置,pointerEvents:", + container.style.pointerEvents, + ); + } + }, 200); + } else { + handleModalChange(false); + } + }, [visible, handleModalChange]); + + return ( + +
    + + + + +
    + + {/* 显示点击位置的信息 */} + {selectedLocation && ( +
    +
    + 位置信息: +
    +
    + 地址: {selectedLocation.label || "加载中..."} +
    +
    + 坐标: {selectedLocation.lat},{" "} + {selectedLocation.lng} +
    +
    + )} + +
    + {mapLoading && ( +
    + +
    + )} +
    { + // 添加原生点击事件监听作为备用 + console.log("容器原生点击事件触发", e); + }} + >
    +
    +
    + ); +}; + +export default SelectMap; diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx index 864ccb6e..9135624f 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/index.tsx @@ -1,5 +1,15 @@ -import React, { useEffect, useState } from "react"; -import { Layout, Input, Button, Modal, message, Tooltip } from "antd"; +import React, { useEffect, useState, useRef } from "react"; +import { + Layout, + Input, + Button, + Modal, + message, + Tooltip, + AutoComplete, + Input as AntInput, + Spin, +} from "antd"; import { SendOutlined, FolderOutlined, @@ -8,6 +18,7 @@ import { CloseOutlined, MessageOutlined, ReloadOutlined, + EnvironmentOutlined, } from "@ant-design/icons"; import { ContractData, weChatGroup, ChatRecord } from "@/pages/pc/ckbox/data"; import { useWebSocketStore } from "@/store/module/websocket/websocket"; @@ -23,6 +34,7 @@ import { manualTriggerAi, } from "@/store/module/weChat/weChat"; import { useContactStore } from "@/store/module/weChat/contacts"; +import SelectMap from "./components/selectMap"; const { Footer } = Layout; const { TextArea } = Input; @@ -326,6 +338,8 @@ const MessageEnter: React.FC = ({ contract }) => { updateShowChatRecordModel(!showChatRecordModel); }; + const [mapVisible, setMapVisible] = useState(false); + return ( <> {/* 聊天输入 */} @@ -423,6 +437,12 @@ const MessageEnter: React.FC = ({ contract }) => { } className={styles.toolbarButton} /> + @@ -877,7 +652,7 @@ const SelectMap: React.FC = ({
    )}
    { try { - console.log("getContactCount 调用参数:", { - userId, - type, - customerId, - groupIds, - exclude, - }); + // console.log("getContactCount 调用参数:", { + // userId, + // type, + // customerId, + // groupIds, + // exclude, + // }); const conditions: any[] = [ { field: "userId", operator: "equals", value: userId }, @@ -394,14 +394,14 @@ export class ContactManager { } } - console.log("查询条件:", conditions); + // console.log("查询条件:", conditions); const contacts = await contactUnifiedService.findWhereMultiple(conditions); - console.log( - `查询结果数量: ${contacts.length}, type: ${type}, groupIds: ${groupIds}`, - ); + // console.log( + // `查询结果数量: ${contacts.length}, type: ${type}, groupIds: ${groupIds}`, + // ); return contacts.length; } catch (error) { From 7dcbcfacc1f0b5899719e72a15449053f86d1f27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Fri, 21 Nov 2025 16:33:02 +0800 Subject: [PATCH 19/31] =?UTF-8?q?=E9=87=8D=E7=BD=AE=E4=B8=80=E4=B8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MessageEnter/components/selectMap.tsx | 773 +++++++++++------- 1 file changed, 499 insertions(+), 274 deletions(-) diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx index 7492be75..7393c08c 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx @@ -10,7 +10,7 @@ import { import { ContractData, weChatGroup, ChatRecord } from "@/pages/pc/ckbox/data"; import { useWebSocketStore } from "@/store/module/websocket/websocket"; -declare const qq: any; +declare const AMap: any; interface SelectMapProps { visible: boolean; @@ -34,7 +34,7 @@ const SelectMap: React.FC = ({ const pendingClickRef = useRef<{ lat: number; lng: number; - latlng: any; + lnglat: any; } | null>(null); // 保存待处理的点击坐标 const [options, setOptions] = useState([]); const [mapLoading, setMapLoading] = useState(true); @@ -56,103 +56,282 @@ const SelectMap: React.FC = ({ }, []); const addMarker = useCallback( - (latlng: any) => { - console.log("addMarker 调用,坐标:", latlng); - - const qq = (window as any).qq; - if (!qq || !qq.maps || !mapRef.current) { - console.error("地图API未加载或地图未初始化"); - return; - } + (lnglat: any) => { + console.log("addMarker 调用,坐标:", lnglat); // 先清除所有现有的标记点 clearAllMarkers(); - // 创建新的标记点(腾讯地图默认红色标记) - const newMarker = new qq.maps.Marker({ - position: latlng, + // 创建红色图标,保持默认倒水滴形状 + const redIcon = new AMap.Icon({ + size: new AMap.Size(25, 34), // 默认标记点尺寸 + image: "https://webapi.amap.com/theme/v1.3/markers/n/mark_r.png", // 红色标记点图片 + imageOffset: new AMap.Pixel(0, 0), + imageSize: new AMap.Size(25, 34), + }); + + // 创建新的标记点 + const newMarker = new AMap.Marker({ + position: lnglat, map: mapRef.current, + icon: redIcon, }); // 将新标记点添加到数组中 markersRef.current.push(newMarker); - mapRef.current.setCenter(latlng); + mapRef.current.setCenter(lnglat); mapRef.current.setZoom(16); // 确保缩放到合适级别 console.log("新 marker 已添加并居中"); }, [clearAllMarkers], ); - // 通用的地址获取函数(使用腾讯地图逆地理编码API) - const getAddressForLocation = useCallback((lat: number, lng: number) => { - console.log("=== getAddressForLocation 调用 ==="); - console.log("坐标:", { lat, lng }); + // 通用的地址获取函数 + const getAddressForLocation = useCallback( + (lat: number, lng: number, lnglat: any) => { + console.log("=== getAddressForLocation 调用 ==="); + console.log("坐标:", { lat, lng }); + console.log( + "Geocoder ref 状态:", + geocoderRef.current ? "存在" : "不存在", + ); - // 使用腾讯地图逆地理编码API - const key = "5ZSBZ-23ICU-XDKVU-4ZQ7Z-O35AJ-XUF6S"; - const url = `https://apis.map.qq.com/ws/geocoder/v1/?location=${lat},${lng}&key=${key}&get_poi=1`; + // 检查 Geocoder 是否已初始化 + if (!geocoderRef.current) { + console.warn("Geocoder 未初始化,无法获取地址"); + return false; + } - fetch(url) - .then(response => response.json()) - .then(result => { - console.log("=== 腾讯地图逆地理编码回调 ==="); - console.log("Result:", result); + // 使用更精确的坐标进行查询 + // 高德地图 getAddress 支持 LngLat 对象或 [lng, lat] 数组 + let queryLnglat: any; + if (lnglat && typeof lnglat.getLat === "function") { + // 如果传入的是 LngLat 对象,直接使用 + queryLnglat = lnglat; + } else { + // 否则创建新的 LngLat 对象或使用数组格式 + try { + queryLnglat = new AMap.LngLat(lng, lat); + } catch (error) { + console.error("创建 LngLat 对象失败,使用数组格式:", error); + queryLnglat = [lng, lat]; + } + } + console.log("调用 geocoder.getAddress,坐标:", queryLnglat); - if (result.status === 0 && result.result) { - const data = result.result; - const formattedAddress = - data.formatted_addresses?.recommend || data.address || ""; - const addressComponent = data.address_component || {}; - const pois = data.pois || []; + try { + geocoderRef.current.getAddress( + queryLnglat, + (status: string, result: any) => { + console.log("=== Geocoder 回调触发(通用函数) ==="); + console.log("Status:", status); + console.log( + "Result:", + result ? JSON.stringify(result, null, 2) : "null", + ); - // 构建详细地址信息 - let addressLabel = formattedAddress; - let poiName = "点击位置"; + if ( + status === "complete" && + result && + result.info === "OK" && + result.regeocode + ) { + const regeocode = result.regeocode; + const formattedAddress = regeocode.formattedAddress || ""; + const addressComponent = regeocode.addressComponent || {}; - // 优先级1: 如果有POI信息,优先使用POI名称 - if (pois.length > 0) { - const poi = pois[0]; - poiName = poi.title || poiName; - const poiAddress = poi.address || ""; - addressLabel = poiAddress - ? `${poiName} ${poiAddress}` - : `${poiName} ${formattedAddress}`; - } - // 优先级2: 组合地址组件 - else { - const parts = []; - if (addressComponent.province) - parts.push(addressComponent.province); - if (addressComponent.city) parts.push(addressComponent.city); - if (addressComponent.district) - parts.push(addressComponent.district); - if (addressComponent.street) parts.push(addressComponent.street); - if (addressComponent.street_number) - parts.push(addressComponent.street_number); + // 构建详细地址信息 + let addressLabel = formattedAddress; + let poiName = "点击位置"; - if (parts.length > 0) { - const fullAddress = parts.join(""); - poiName = - addressComponent.street || - addressComponent.district || - "点击位置"; - addressLabel = fullAddress || formattedAddress; + // 优先级1: 如果有POI信息,优先使用POI名称 + if (regeocode.pois && regeocode.pois.length > 0) { + const poi = regeocode.pois[0]; + poiName = poi.name || poiName; + const poiAddress = poi.address || ""; + addressLabel = poiAddress + ? `${poiName} ${poiAddress}` + : `${poiName} ${formattedAddress}`; + } + // 优先级2: 如果有建筑物信息 + else if (regeocode.buildings && regeocode.buildings.length > 0) { + const building = regeocode.buildings[0]; + poiName = building.name || addressComponent.building || poiName; + addressLabel = `${poiName} ${formattedAddress}`; + } + // 优先级3: 如果有AOI(兴趣区域)信息 + else if (regeocode.aois && regeocode.aois.length > 0) { + const aoi = regeocode.aois[0]; + poiName = aoi.name || poiName; + addressLabel = `${poiName} ${formattedAddress}`; + } + // 优先级4: 使用地址组件构建详细地址 + else if (addressComponent.building) { + poiName = addressComponent.building; + addressLabel = `${poiName} ${formattedAddress}`; + } + // 优先级5: 组合地址组件 + else { + const parts = []; + if (addressComponent.province) + parts.push(addressComponent.province); + if (addressComponent.city) parts.push(addressComponent.city); + if (addressComponent.district) + parts.push(addressComponent.district); + if (addressComponent.township) + parts.push(addressComponent.township); + if (addressComponent.street) + parts.push(addressComponent.street); + if (addressComponent.streetNumber) + parts.push(addressComponent.streetNumber); + + if (parts.length > 0) { + const fullAddress = parts.join(""); + poiName = + addressComponent.street || + addressComponent.district || + "点击位置"; + addressLabel = fullAddress || formattedAddress; + } else { + addressLabel = + formattedAddress || `纬度: ${lat}, 经度: ${lng}`; + } + } + + setSelectedLocation({ + lat: lat, + lng: lng, + label: addressLabel, + poiname: poiName, + }); + + message.success("地址信息获取成功"); } else { - addressLabel = formattedAddress || `纬度: ${lat}, 经度: ${lng}`; + console.warn("=== Geocoder 返回异常,尝试 PlaceSearch ==="); + console.warn("Status:", status); + console.warn("Result:", result); + // Geocoder 失败,使用 PlaceSearch 作为备用 + if (placeSearchRef.current) { + try { + const searchLnglat = lnglat || [lng, lat]; + placeSearchRef.current.searchNearBy( + "", + searchLnglat, + 1000, + (searchStatus: string, searchResult: any) => { + if ( + searchStatus === "complete" && + searchResult && + searchResult.info === "OK" && + searchResult.poiList?.pois?.length > 0 + ) { + const poi = searchResult.poiList.pois[0]; + const poiLabel = poi.address + ? `${poi.name} ${poi.address}` + : poi.name; + setSelectedLocation({ + lat: lat, + lng: lng, + label: poiLabel, + poiname: poi.name, + }); + message.success("通过附近搜索获取到地址信息"); + } else { + console.warn("PlaceSearch 返回异常:", { + status: searchStatus, + result: searchResult, + }); + const coordLabel = `纬度: ${lat}, 经度: ${lng}`; + setSelectedLocation({ + lat: lat, + lng: lng, + label: coordLabel, + poiname: "点击位置", + }); + message.warning("无法获取详细地址信息,但坐标已记录"); + } + }, + ); + } catch (placeSearchError) { + console.error( + "PlaceSearch.searchNearBy 调用异常:", + placeSearchError, + ); + const coordLabel = `纬度: ${lat}, 经度: ${lng}`; + setSelectedLocation({ + lat: lat, + lng: lng, + label: coordLabel, + poiname: "点击位置", + }); + message.warning("无法获取详细地址信息,但坐标已记录"); + } + } else { + const coordLabel = `纬度: ${lat}, 经度: ${lng}`; + setSelectedLocation({ + lat: lat, + lng: lng, + label: coordLabel, + poiname: "点击位置", + }); + message.warning("无法获取详细地址信息,但坐标已记录"); + } } + }, + ); + } catch (error) { + console.error("=== Geocoder.getAddress 调用异常 ===", error); + // 如果 geocoder 调用失败,尝试使用 PlaceSearch + if (placeSearchRef.current) { + console.log("尝试使用 PlaceSearch 作为备用方案"); + try { + placeSearchRef.current.searchNearBy( + "", + lnglat || [lng, lat], + 1000, + (status: string, result: any) => { + if ( + status === "complete" && + result && + result.info === "OK" && + result.poiList?.pois?.length > 0 + ) { + const poi = result.poiList.pois[0]; + const poiLabel = poi.address + ? `${poi.name} ${poi.address}` + : poi.name; + setSelectedLocation({ + lat: lat, + lng: lng, + label: poiLabel, + poiname: poi.name, + }); + message.success("通过附近搜索获取到地址信息"); + } else { + const coordLabel = `纬度: ${lat}, 经度: ${lng}`; + setSelectedLocation({ + lat: lat, + lng: lng, + label: coordLabel, + poiname: "点击位置", + }); + message.warning("无法获取详细地址信息,但坐标已记录"); + } + }, + ); + } catch (placeSearchError) { + console.error("PlaceSearch 调用也失败:", placeSearchError); + const coordLabel = `纬度: ${lat}, 经度: ${lng}`; + setSelectedLocation({ + lat: lat, + lng: lng, + label: coordLabel, + poiname: "点击位置", + }); + message.warning("无法获取详细地址信息,但坐标已记录"); } - - setSelectedLocation({ - lat: lat, - lng: lng, - label: addressLabel, - poiname: poiName, - }); - - message.success("地址信息获取成功"); } else { - console.warn("逆地理编码返回异常:", result); const coordLabel = `纬度: ${lat}, 经度: ${lng}`; setSelectedLocation({ lat: lat, @@ -162,48 +341,20 @@ const SelectMap: React.FC = ({ }); message.warning("无法获取详细地址信息,但坐标已记录"); } - }) - .catch(error => { - console.error("=== 逆地理编码API调用异常 ===", error); - const coordLabel = `纬度: ${lat}, 经度: ${lng}`; - setSelectedLocation({ - lat: lat, - lng: lng, - label: coordLabel, - poiname: "点击位置", - }); - message.warning("无法获取详细地址信息,但坐标已记录"); - }); - }, []); + return false; + } + + return true; + }, + [], + ); const initMap = useCallback(() => { console.log("initMap 执行中"); setMapLoading(true); - // 检查 qq 对象是否已加载 - if (!(window as any).qq || !(window as any).qq.maps) { - console.warn("腾讯地图API尚未加载,等待中..."); - // 等待最多5秒,每100ms检查一次 - let attempts = 0; - const maxAttempts = 50; - const checkInterval = setInterval(() => { - attempts++; - if ((window as any).qq && (window as any).qq.maps) { - clearInterval(checkInterval); - console.log("腾讯地图API已加载,继续初始化"); - initMap(); - } else if (attempts >= maxAttempts) { - clearInterval(checkInterval); - console.error("腾讯地图API加载超时"); - message.error("地图加载失败,请刷新页面重试"); - setMapLoading(false); - } - }, 100); - return; - } - // 确保容器存在 - const container = document.getElementById("tencent-map-container"); + const container = document.getElementById("amap-container"); if (!container) { console.error("地图容器不存在"); setMapLoading(false); @@ -216,13 +367,10 @@ const SelectMap: React.FC = ({ container.style.position = "relative"; container.style.zIndex = "1"; - const qq = (window as any).qq; - - // 腾讯地图初始化(注意:坐标顺序是 lat, lng) - const center = new qq.maps.LatLng(24.470164, 118.113653); // 默认中心 - const map = new qq.maps.Map("tencent-map-container", { + const map = new AMap.Map("amap-container", { zoom: 16, - center: center, + center: [118.113653, 24.470164], // 默认中心 + viewMode: "2D", // 明确指定视图模式 }); mapRef.current = map; @@ -232,91 +380,165 @@ const SelectMap: React.FC = ({ setMapLoading(false); }, 10000); // 10秒超时 - // 标记服务已初始化 - geocoderRef.current = true; // 使用HTTP API,不需要实例 - placeSearchRef.current = true; // 使用HTTP API,不需要实例 - - // 获取当前位置(使用浏览器原生定位) - console.log("=== 开始获取当前位置 ==="); - if (navigator.geolocation) { - navigator.geolocation.getCurrentPosition( - position => { - const lat = position.coords.latitude; - const lng = position.coords.longitude; - const qq = (window as any).qq; - if (!qq || !qq.maps) { - console.error("腾讯地图API未加载"); - return; - } - const latlng = new qq.maps.LatLng(lat, lng); - - console.log("定位成功,当前位置:", { lat, lng }); - - // 将地图中心设置为当前位置 - map.setCenter(latlng); - map.setZoom(16); - - // 添加当前位置标记 - addMarker(latlng); - - // 获取地址信息 - getAddressForLocation(lat, lng); - }, - error => { - console.warn("定位失败:", error); - message.warning( - "无法获取当前位置,请手动点击地图选择位置。原因: " + - (error.message || "定位服务不可用"), - ); - // 定位失败时,使用默认中心点 - console.log("使用默认中心点"); + // 立即加载插件,不等待地图 complete 事件 + console.log("=== 立即开始加载 AMap 插件(地图创建后) ==="); + AMap.plugin( + [ + "AMap.AutoComplete", + "AMap.PlaceSearch", + "AMap.Geocoder", + "AMap.Geolocation", + ], + error => { + if (error) { + console.error("=== AMap 插件加载失败 ===", error); + message.error("地图插件加载失败,部分功能可能不可用"); clearTimeout(loadingTimeout); setMapLoading(false); - }, - { - enableHighAccuracy: true, - timeout: 10000, - maximumAge: 0, - }, - ); - } else { - console.warn("浏览器不支持定位"); - message.warning("浏览器不支持定位功能"); - clearTimeout(loadingTimeout); - setMapLoading(false); - } + return; + } + console.log("=== AMap 插件加载成功 ==="); - // 如果有待处理的点击坐标,立即处理它 - if (pendingClickRef.current) { - console.log("=== 检测到待处理的点击坐标,立即处理 ==="); - const { lat, lng } = pendingClickRef.current; - console.log("待处理坐标:", { lat, lng }); - setTimeout(() => { - getAddressForLocation(lat, lng); - console.log("✓ 待处理的坐标已成功获取地址"); - pendingClickRef.current = null; // 清除待处理坐标 - }, 100); - } + // 立即创建 PlaceSearch 和 Geocoder 实例 + const placeSearch = new AMap.PlaceSearch({ + city: "全国", + map: map, + }); + placeSearchRef.current = placeSearch; + console.log("PlaceSearch 实例已创建"); - // 绑定点击事件 - console.log("立即绑定点击事件"); - qq.maps.event.addListener(map, "click", (e: any) => { + const geocoder = new AMap.Geocoder({ + city: "全国", + radius: 1000, // 搜索半径,单位米 + extensions: "all", // 返回详细信息,包括POI、建筑物等 + }); + geocoderRef.current = geocoder; + console.log("Geocoder 实例已创建并保存到 ref,现在可以立即使用"); + + // 创建 Geolocation 实例用于获取当前位置 + const geolocation = new AMap.Geolocation({ + enableHighAccuracy: true, // 是否使用高精度定位,默认:true + timeout: 10000, // 超过10秒后停止定位,默认:无穷大 + maximumAge: 0, // 定位结果缓存0毫秒,默认:0 + convert: true, // 自动偏移坐标,偏移后的坐标为高德坐标,默认:true + showButton: false, // 显示定位按钮,默认:true + buttonPosition: "RB", // 定位按钮停靠位置,默认:'LB',左下角 + showMarker: false, // 定位成功后在定位到的位置显示点标记,默认:true + showCircle: false, // 定位成功后用圆圈表示定位精度范围,默认:true + panToLocation: false, // 定位成功后将定位到的位置作为地图中心点,默认:true + zoomToAccuracy: false, // 定位成功后调整地图视野范围使定位位置及精度范围视野内可见,默认:false + }); + geolocationRef.current = geolocation; + + // 获取当前位置 + console.log("=== 开始获取当前位置 ==="); + geolocation.getCurrentPosition((status: string, result: any) => { + console.log("=== 定位回调触发 ==="); + console.log("定位状态:", status); + console.log("定位结果:", result); + + if (status === "complete") { + const { position, formattedAddress, addressComponent } = result; + const lat = position.lat; + const lng = position.lng; + const lnglat = new AMap.LngLat(lng, lat); + + console.log("定位成功,当前位置:", { lat, lng }); + console.log("定位地址:", formattedAddress); + + // 将地图中心设置为当前位置 + map.setCenter(lnglat); + map.setZoom(16); + + // 添加当前位置标记 + addMarker(lnglat); + + // 设置选中位置信息 + let addressLabel = formattedAddress || `纬度: ${lat}, 经度: ${lng}`; + let poiName = "当前位置"; + + // 尝试从地址组件中获取更详细的信息 + if (addressComponent) { + const parts = []; + if (addressComponent.province) + parts.push(addressComponent.province); + if (addressComponent.city) parts.push(addressComponent.city); + if (addressComponent.district) + parts.push(addressComponent.district); + if (addressComponent.street) parts.push(addressComponent.street); + if (addressComponent.streetNumber) + parts.push(addressComponent.streetNumber); + + if (parts.length > 0) { + addressLabel = parts.join(""); + } + + poiName = + addressComponent.street || + addressComponent.district || + "当前位置"; + } + + setSelectedLocation({ + lat: lat, + lng: lng, + label: addressLabel, + poiname: poiName, + }); + + message.success("已获取当前位置"); + } else { + console.warn("定位失败:", result); + message.warning( + "无法获取当前位置,请手动点击地图选择位置。原因: " + + (result.message || "定位服务不可用"), + ); + // 定位失败时,使用默认中心点 + console.log("使用默认中心点"); + } + }); + + // 如果有待处理的点击坐标,立即处理它 + if (pendingClickRef.current) { + console.log("=== 检测到待处理的点击坐标,立即处理 ==="); + const { lat, lng, lnglat } = pendingClickRef.current; + console.log("待处理坐标:", { lat, lng }); + setTimeout(() => { + const success = getAddressForLocation(lat, lng, lnglat); + if (success) { + console.log("✓ 待处理的坐标已成功获取地址"); + pendingClickRef.current = null; // 清除待处理坐标 + } else { + console.warn("待处理的坐标获取地址失败"); + } + }, 100); + } + }, + ); + + // 立即绑定点击事件,插件可能已加载或正在加载 + console.log("立即绑定点击事件(插件可能已初始化)"); + map.on("click", (e: any) => { console.log("=== 地图点击事件触发 ==="); console.log("点击事件对象:", e); - console.log("点击位置对象:", e.latLng); + console.log("点击位置对象:", e.lnglat); + console.log( + "Geocoder ref 状态:", + geocoderRef.current ? "已初始化" : "未初始化", + ); - if (!e || !e.latLng) { - console.error("点击事件无效,缺少 latLng"); + if (!e || !e.lnglat) { + console.error("点击事件无效,缺少 lnglat"); return; } - const latlng = e.latLng; - const lat = latlng.getLat(); - const lng = latlng.getLng(); + const lnglat = e.lnglat; + const lat = lnglat.getLat(); + const lng = lnglat.getLng(); console.log(`点击坐标 - 纬度: ${lat}, 经度: ${lng}`); // 立即添加标记和居中 - addMarker(latlng); + addMarker(lnglat); // 设置基本 selectedLocation(至少有坐标) setSelectedLocation({ @@ -326,12 +548,19 @@ const SelectMap: React.FC = ({ poiname: "点击位置", }); - // 立即获取地址 - getAddressForLocation(lat, lng); + // 如果 Geocoder 已初始化,立即使用它 + if (geocoderRef.current) { + console.log("Geocoder 已初始化,立即获取地址"); + getAddressForLocation(lat, lng, lnglat); + } else { + console.log("Geocoder 未初始化,保存坐标待插件加载完成后处理"); + // 保存待处理的坐标 + pendingClickRef.current = { lat, lng, lnglat }; + } }); // 等待地图完全加载后关闭 loading - qq.maps.event.addListener(map, "tilesloaded", () => { + map.on("complete", () => { console.log("地图加载完成"); // 清除超时定时器 clearTimeout(loadingTimeout); @@ -347,63 +576,47 @@ const SelectMap: React.FC = ({ console.log("地图容器指针事件已启用"); } }); + + // 如果地图加载失败,也设置加载完成 + map.on("error", (error: any) => { + console.error("地图加载错误:", error); + clearTimeout(loadingTimeout); + setMapLoading(false); + message.error("地图加载失败"); + }); }, [addMarker, getAddressForLocation]); const handleSearch = value => { if (value) { - // 使用腾讯地图搜索建议API - const key = "5ZSBZ-23ICU-XDKVU-4ZQ7Z-O35AJ-XUF6S"; - const url = `https://apis.map.qq.com/ws/place/v1/suggestion?keyword=${encodeURIComponent(value)}&key=${key}®ion=全国`; - - fetch(url) - .then(response => response.json()) - .then(result => { - if (result.status === 0 && result.data) { + AMap.plugin("AMap.AutoComplete", () => { + const auto = new AMap.AutoComplete({ city: "全国" }); + auto.search(value, (status, result) => { + if (status === "complete") { setOptions( - result.data.map((item: any) => ({ - value: item.title, - data: { - name: item.title, - address: item.address, - district: item.adname, - location: { - lat: item.location.lat, - lng: item.location.lng, - }, - }, + result.tips.map(tip => ({ + value: tip.name, + data: tip, })), ); - } else { - setOptions([]); } - }) - .catch(error => { - console.error("搜索建议API调用失败:", error); - setOptions([]); }); + }); } else { setOptions([]); } }; const onSelect = (value, option) => { - const qq = (window as any).qq; - if (!qq || !qq.maps) { - message.error("地图API未加载,请稍后再试"); - return; - } const { district, address, name, location } = option.data; - const latlng = new qq.maps.LatLng(location.lat, location.lng); + const lnglat = location; setSelectedLocation({ - lat: location.lat, - lng: location.lng, - label: `${name} ${address || district || ""}`, + lat: lnglat.lat, + lng: lnglat.lng, + label: `${name} ${address || district}`, poiname: name, }); - addMarker(latlng); - if (mapRef.current) { - mapRef.current.setCenter(latlng); - } + addMarker(lnglat); + mapRef.current.setCenter(lnglat); }; const handleModalChange = useCallback( @@ -412,17 +625,9 @@ const SelectMap: React.FC = ({ console.log("模态打开:开始加载地图脚本"); setMapLoading(true); setSelectedLocation(null); - - // 检查是否已加载腾讯地图脚本 - if ((window as any).qq && (window as any).qq.maps) { - console.log("腾讯地图脚本已加载,直接初始化"); - setTimeout(() => initMap(), 100); - return; - } - const script = document.createElement("script"); script.src = - "https://map.qq.com/api/gljs?v=1.exp&key=5ZSBZ-23ICU-XDKVU-4ZQ7Z-O35AJ-XUF6S"; + "https://webapi.amap.com/maps?v=1.4.15&key=79370028f5763e46742125ed2e900c76&plugin=AMap.PlaceSearch,AMap.AutoComplete,AMap.Geocoder,AMap.Geolocation"; script.async = true; script.onload = () => { console.log("脚本加载成功:开始初始化地图"); @@ -443,14 +648,16 @@ const SelectMap: React.FC = ({ placeSearchRef.current = null; geolocationRef.current = null; // Cleanup on close - const scripts = document.querySelectorAll('script[src*="map.qq.com"]'); + const scripts = document.querySelectorAll( + 'script[src*="webapi.amap.com"]', + ); scripts.forEach(s => { if (document.body.contains(s)) { document.body.removeChild(s); } }); if (mapRef.current) { - // 腾讯地图没有destroy方法,设置为null即可 + mapRef.current.destroy(); mapRef.current = null; } setMapLoading(false); @@ -462,57 +669,75 @@ const SelectMap: React.FC = ({ // 手动获取当前位置 const handleGetCurrentLocation = useCallback(() => { - if (!navigator.geolocation) { - message.warning("浏览器不支持定位服务"); - return; - } - - const qq = (window as any).qq; - if (!qq || !qq.maps) { - message.error("地图API未加载,请稍后再试"); + if (!geolocationRef.current) { + message.warning("定位服务未初始化,请稍候再试"); return; } console.log("=== 手动触发获取当前位置 ==="); message.loading({ content: "正在获取当前位置...", key: "location" }); - navigator.geolocation.getCurrentPosition( - position => { - message.destroy("location"); - console.log("=== 手动定位回调触发 ==="); - const lat = position.coords.latitude; - const lng = position.coords.longitude; - const latlng = new qq.maps.LatLng(lat, lng); + geolocationRef.current.getCurrentPosition((status: string, result: any) => { + message.destroy("location"); + console.log("=== 手动定位回调触发 ==="); + console.log("定位状态:", status); + console.log("定位结果:", result); + + if (status === "complete" && result && result.position) { + const { position, formattedAddress, addressComponent } = result; + const lat = position.lat; + const lng = position.lng; + const lnglat = new AMap.LngLat(lng, lat); console.log("定位成功,当前位置:", { lat, lng }); // 将地图中心设置为当前位置 if (mapRef.current) { - mapRef.current.setCenter(latlng); + mapRef.current.setCenter(lnglat); mapRef.current.setZoom(16); } // 添加当前位置标记 - addMarker(latlng); + addMarker(lnglat); + + // 设置选中位置信息 + let addressLabel = formattedAddress || `纬度: ${lat}, 经度: ${lng}`; + let poiName = "当前位置"; + + // 尝试从地址组件中获取更详细的信息 + if (addressComponent) { + const parts = []; + if (addressComponent.province) parts.push(addressComponent.province); + if (addressComponent.city) parts.push(addressComponent.city); + if (addressComponent.district) parts.push(addressComponent.district); + if (addressComponent.street) parts.push(addressComponent.street); + if (addressComponent.streetNumber) + parts.push(addressComponent.streetNumber); + + if (parts.length > 0) { + addressLabel = parts.join(""); + } + + poiName = + addressComponent.street || addressComponent.district || "当前位置"; + } + + setSelectedLocation({ + lat: lat, + lng: lng, + label: addressLabel, + poiname: poiName, + }); - // 获取地址信息 - getAddressForLocation(lat, lng); message.success("已获取当前位置"); - }, - error => { - message.destroy("location"); - console.warn("定位失败:", error); + } else { + console.warn("定位失败:", result); message.error( - "获取当前位置失败: " + (error.message || "定位服务不可用"), + "获取当前位置失败: " + (result?.message || "定位服务不可用"), ); - }, - { - enableHighAccuracy: true, - timeout: 10000, - maximumAge: 0, - }, - ); - }, [addMarker, getAddressForLocation]); + } + }); + }, [addMarker]); const handleSendLocation = () => { if (!selectedLocation || !selectedLocation.lat || !selectedLocation.lng) { @@ -571,7 +796,7 @@ const SelectMap: React.FC = ({ handleModalChange(true); // 确保容器在模态框打开后可以接收事件 setTimeout(() => { - const container = document.getElementById("tencent-map-container"); + const container = document.getElementById("amap-container"); if (container) { container.style.pointerEvents = "auto"; container.style.cursor = "crosshair"; @@ -607,7 +832,7 @@ const SelectMap: React.FC = ({ @@ -652,7 +877,7 @@ const SelectMap: React.FC = ({
    )}
    Date: Fri, 21 Nov 2025 17:22:33 +0800 Subject: [PATCH 20/31] =?UTF-8?q?=E5=AE=A2=E6=9C=8D=E8=AF=84=E5=88=86?= =?UTF-8?q?=E4=BD=93=E7=B3=BB=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CalculateWechatAccountScoreCommand.php | 413 ++++++++++++---- .../WechatAccountHealthScoreService.php | 455 +++++++++++++++--- 2 files changed, 711 insertions(+), 157 deletions(-) diff --git a/Server/application/command/CalculateWechatAccountScoreCommand.php b/Server/application/command/CalculateWechatAccountScoreCommand.php index dbc7ec5b..aeb73784 100644 --- a/Server/application/command/CalculateWechatAccountScoreCommand.php +++ b/Server/application/command/CalculateWechatAccountScoreCommand.php @@ -6,6 +6,7 @@ use think\console\Command; use think\console\Input; use think\console\Output; use think\Db; +use think\facade\Log; use app\common\service\WechatAccountHealthScoreService; /** @@ -17,36 +18,122 @@ use app\common\service\WechatAccountHealthScoreService; */ class CalculateWechatAccountScoreCommand extends Command { + /** + * 数据库表名 + */ + const TABLE_WECHAT_ACCOUNT = 's2_wechat_account'; + const TABLE_WECHAT_ACCOUNT_SCORE = 's2_wechat_account_score'; protected function configure() { $this->setName('wechat:calculate-score') - ->setDescription('统一计算微信账号健康分(包含初始化、更新评分记录、批量计算)'); + ->setDescription('统一计算微信账号健康分(包含初始化、更新评分记录、批量计算)') + ->addOption('only-init', null, \think\console\input\Option::VALUE_NONE, '仅执行初始化步骤') + ->addOption('only-update', null, \think\console\input\Option::VALUE_NONE, '仅执行更新评分记录步骤') + ->addOption('only-batch', null, \think\console\input\Option::VALUE_NONE, '仅执行批量更新健康分步骤') + ->addOption('account-id', 'a', \think\console\input\Option::VALUE_OPTIONAL, '指定账号ID,仅处理该账号') + ->addOption('batch-size', 'b', \think\console\input\Option::VALUE_OPTIONAL, '批处理大小', 50) + ->addOption('force-recalculate', 'f', \think\console\input\Option::VALUE_NONE, '强制重新计算基础分'); } + /** + * 执行命令 + * + * @param Input $input 输入对象 + * @param Output $output 输出对象 + * @return int 命令执行状态码(0表示成功) + */ protected function execute(Input $input, Output $output) { + // 解析命令行参数 + $onlyInit = $input->getOption('only-init'); + $onlyUpdate = $input->getOption('only-update'); + $onlyBatch = $input->getOption('only-batch'); + $accountId = $input->getOption('account-id'); + $batchSize = (int)$input->getOption('batch-size'); + $forceRecalculate = $input->getOption('force-recalculate'); + + // 参数验证 + if ($batchSize <= 0) { + $batchSize = 50; // 默认批处理大小 + } + + // 显示执行参数 $output->writeln("=========================================="); $output->writeln("开始统一计算微信账号健康分..."); $output->writeln("=========================================="); + if ($accountId) { + $output->writeln("指定账号ID: {$accountId}"); + } + + if ($onlyInit) { + $output->writeln("仅执行初始化步骤"); + } elseif ($onlyUpdate) { + $output->writeln("仅执行更新评分记录步骤"); + } elseif ($onlyBatch) { + $output->writeln("仅执行批量更新健康分步骤"); + } + + if ($forceRecalculate) { + $output->writeln("强制重新计算基础分"); + } + + $output->writeln("批处理大小: {$batchSize}"); + + // 记录命令开始执行的日志 + Log::info('开始执行微信账号健康分计算命令', [ + 'accountId' => $accountId, + 'onlyInit' => $onlyInit ? 'true' : 'false', + 'onlyUpdate' => $onlyUpdate ? 'true' : 'false', + 'onlyBatch' => $onlyBatch ? 'true' : 'false', + 'batchSize' => $batchSize, + 'forceRecalculate' => $forceRecalculate ? 'true' : 'false' + ]); + $startTime = time(); - $service = new WechatAccountHealthScoreService(); + + try { + // 实例化服务 + $service = new WechatAccountHealthScoreService(); + } catch (\Exception $e) { + $errorMsg = "实例化WechatAccountHealthScoreService失败: " . $e->getMessage(); + $output->writeln("{$errorMsg}"); + Log::error($errorMsg); + return 1; // 返回非零状态码表示失败 + } + + // 初始化统计数据 + $initStats = ['success' => 0, 'failed' => 0, 'errors' => []]; + $updateStats = ['total' => 0]; + $batchStats = ['success' => 0, 'failed' => 0, 'errors' => []]; try { // 步骤1: 初始化未计算基础分的账号 - $output->writeln("\n[步骤1] 初始化未计算基础分的账号..."); - $initStats = $this->initUncalculatedAccounts($service, $output); - $output->writeln("初始化完成:成功 {$initStats['success']} 条,失败 {$initStats['failed']} 条"); + if (!$onlyUpdate && !$onlyBatch) { + $output->writeln("\n[步骤1] 初始化未计算基础分的账号..."); + Log::info('[步骤1] 开始初始化未计算基础分的账号'); + $initStats = $this->initUncalculatedAccounts($service, $output, $accountId, $batchSize); + $output->writeln("初始化完成:成功 {$initStats['success']} 条,失败 {$initStats['failed']} 条"); + Log::info("初始化完成:成功 {$initStats['success']} 条,失败 {$initStats['failed']} 条"); + } // 步骤2: 更新评分记录(根据wechatId和alias不一致情况) - $output->writeln("\n[步骤2] 更新评分记录(根据wechatId和alias不一致情况)..."); - $updateStats = $this->updateScoreRecords($service, $output); - $output->writeln("更新完成:处理了 {$updateStats['total']} 条记录"); + if (!$onlyInit && !$onlyBatch) { + $output->writeln("\n[步骤2] 更新评分记录(根据wechatId和alias不一致情况)..."); + Log::info('[步骤2] 开始更新评分记录(根据wechatId和alias不一致情况)'); + $updateStats = $this->updateScoreRecords($service, $output, $accountId, $batchSize); + $output->writeln("更新完成:处理了 {$updateStats['total']} 条记录"); + Log::info("更新评分记录完成:处理了 {$updateStats['total']} 条记录"); + } // 步骤3: 批量更新健康分(只更新动态分,不重新计算基础分) - $output->writeln("\n[步骤3] 批量更新健康分(只更新动态分)..."); - $batchStats = $this->batchUpdateHealthScore($service, $output); - $output->writeln("批量更新完成:成功 {$batchStats['success']} 条,失败 {$batchStats['failed']} 条"); + if (!$onlyInit && !$onlyUpdate) { + $output->writeln("\n[步骤3] 批量更新健康分(只更新动态分)..."); + Log::info('[步骤3] 开始批量更新健康分(只更新动态分)'); + $batchStats = $this->batchUpdateHealthScore($service, $output, $accountId, $batchSize, $forceRecalculate); + $output->writeln("批量更新完成:成功 {$batchStats['success']} 条,失败 {$batchStats['failed']} 条"); + Log::info("批量更新健康分完成:成功 {$batchStats['success']} 条,失败 {$batchStats['failed']} 条"); + } // 统计信息 $endTime = time(); @@ -60,40 +147,89 @@ class CalculateWechatAccountScoreCommand extends Command $output->writeln("更新评分记录: {$updateStats['total']} 条"); $output->writeln("批量更新: 成功 {$batchStats['success']} 条,失败 {$batchStats['failed']} 条"); + // 记录命令执行完成的日志 + Log::info("微信账号健康分计算命令执行完成,总耗时: {$duration} 秒," . + "初始化: 成功 {$initStats['success']} 条,失败 {$initStats['failed']} 条," . + "更新评分记录: {$updateStats['total']} 条," . + "批量更新: 成功 {$batchStats['success']} 条,失败 {$batchStats['failed']} 条"); + if (!empty($initStats['errors'])) { $output->writeln("\n初始化错误详情:"); + Log::warning("初始化阶段出现 " . count($initStats['errors']) . " 个错误"); + foreach (array_slice($initStats['errors'], 0, 10) as $error) { $output->writeln(" 账号ID {$error['accountId']}: {$error['error']}"); + Log::error("初始化错误 - 账号ID {$error['accountId']}: {$error['error']}"); } + if (count($initStats['errors']) > 10) { $output->writeln(" ... 还有 " . (count($initStats['errors']) - 10) . " 个错误"); + Log::warning("初始化错误过多,只记录前10个,还有 " . (count($initStats['errors']) - 10) . " 个错误未显示"); } } if (!empty($batchStats['errors'])) { $output->writeln("\n批量更新错误详情:"); + Log::warning("批量更新阶段出现 " . count($batchStats['errors']) . " 个错误"); + foreach (array_slice($batchStats['errors'], 0, 10) as $error) { $output->writeln(" 账号ID {$error['accountId']}: {$error['error']}"); + Log::error("批量更新错误 - 账号ID {$error['accountId']}: {$error['error']}"); } + if (count($batchStats['errors']) > 10) { $output->writeln(" ... 还有 " . (count($batchStats['errors']) - 10) . " 个错误"); + Log::warning("批量更新错误过多,只记录前10个,还有 " . (count($batchStats['errors']) - 10) . " 个错误未显示"); } } - } catch (\Exception $e) { - $output->writeln("\n错误: " . $e->getMessage()); + } catch (\PDOException $e) { + // 数据库异常 + $errorMsg = "数据库操作失败: " . $e->getMessage(); + $output->writeln("\n数据库错误: " . $errorMsg . ""); $output->writeln($e->getTraceAsString()); + + // 记录数据库错误 + Log::error("数据库错误: " . $errorMsg); + Log::error("错误堆栈: " . $e->getTraceAsString()); + + return 2; // 数据库错误状态码 + } catch (\Exception $e) { + // 一般异常 + $errorMsg = "命令执行失败: " . $e->getMessage(); + $output->writeln("\n错误: " . $errorMsg . ""); + $output->writeln($e->getTraceAsString()); + + // 记录严重错误 + Log::error($errorMsg); + Log::error("错误堆栈: " . $e->getTraceAsString()); + + return 1; // 一般错误状态码 + } catch (\Throwable $e) { + // 其他所有错误 + $errorMsg = "严重错误: " . $e->getMessage(); + $output->writeln("\n严重错误: " . $errorMsg . ""); + $output->writeln($e->getTraceAsString()); + + // 记录严重错误 + Log::critical($errorMsg); + Log::critical("错误堆栈: " . $e->getTraceAsString()); + + return 3; // 严重错误状态码 } + + return 0; // 成功执行 } /** * 初始化未计算基础分的账号 * - * @param WechatAccountHealthScoreService $service - * @param Output $output - * @return array + * @param WechatAccountHealthScoreService $service 健康分服务实例 + * @param Output $output 输出对象 + * @return array 处理结果统计 + * @throws \Exception 如果查询或处理过程中出现错误 */ - private function initUncalculatedAccounts($service, $output) + private function initUncalculatedAccounts($service, $output, $accountId = null, $batchSize = 50) { $stats = [ 'total' => 0, @@ -102,41 +238,67 @@ class CalculateWechatAccountScoreCommand extends Command 'errors' => [] ]; - // 获取所有未计算基础分的账号 - $accounts = Db::table('s2_wechat_account') - ->alias('a') - ->leftJoin(['s2_wechat_account_score' => 's'], 's.accountId = a.id') - ->where('a.isDeleted', 0) - ->where(function($query) { - $query->whereNull('s.id') - ->whereOr('s.baseScoreCalculated', 0); - }) - ->field('a.id, a.wechatId') - ->select(); + try { + // 获取所有未计算基础分的账号 + // 优化查询:使用索引字段,只查询必要的字段 + $query = Db::table(self::TABLE_WECHAT_ACCOUNT) + ->alias('a') + ->leftJoin([self::TABLE_WECHAT_ACCOUNT_SCORE => 's'], 's.accountId = a.id') + ->where('a.isDeleted', 0) + ->where(function($query) { + $query->whereNull('s.id') + ->whereOr('s.baseScoreCalculated', 0); + }); + + // 如果指定了账号ID,则只处理该账号 + if ($accountId) { + $query->where('a.id', $accountId); + } + + $accounts = $query->field('a.id, a.wechatId') // 只查询必要的字段 + ->select(); + } catch (\Exception $e) { + Log::error("查询未计算基础分的账号失败: " . $e->getMessage()); + throw new \Exception("查询未计算基础分的账号失败: " . $e->getMessage(), 0, $e); + } $stats['total'] = count($accounts); if ($stats['total'] == 0) { $output->writeln("没有需要初始化的账号"); + Log::info("没有需要初始化的账号"); return $stats; } $output->writeln("找到 {$stats['total']} 个需要初始化的账号"); + Log::info("找到 {$stats['total']} 个需要初始化的账号"); - $batchSize = 100; + // 优化批处理:使用传入的批处理大小 $batches = array_chunk($accounts, $batchSize); + $batchCount = count($batches); + + Log::info("将分 {$batchCount} 批处理,每批 {$batchSize} 个账号"); foreach ($batches as $batchIndex => $batch) { + $batchStartTime = microtime(true); + $batchSuccessCount = 0; + $batchFailedCount = 0; + foreach ($batch as $account) { try { $service->calculateAndUpdate($account['id']); $stats['success']++; + $batchSuccessCount++; - if ($stats['success'] % 100 == 0) { + if ($stats['success'] % 20 == 0) { // 更频繁地显示进度 $output->write("."); + Log::debug("已成功初始化 {$stats['success']} 个账号"); } } catch (\Exception $e) { $stats['failed']++; + $batchFailedCount++; + $errorMsg = "初始化账号 {$account['id']} 失败: " . $e->getMessage(); + Log::error($errorMsg); $stats['errors'][] = [ 'accountId' => $account['id'], 'error' => $e->getMessage() @@ -144,9 +306,12 @@ class CalculateWechatAccountScoreCommand extends Command } } - if (($batchIndex + 1) % 10 == 0) { - $output->writeln(" 已处理 " . ($batchIndex + 1) * $batchSize . " 条"); - } + $batchEndTime = microtime(true); + $batchDuration = round($batchEndTime - $batchStartTime, 2); + + // 每批次完成后输出进度信息 + $output->writeln(" 批次 " . ($batchIndex + 1) . "/{$batchCount} 完成,耗时 {$batchDuration} 秒,成功 {$batchSuccessCount},失败 {$batchFailedCount}"); + Log::info("初始化批次 " . ($batchIndex + 1) . "/{$batchCount} 完成,耗时 {$batchDuration} 秒,成功 {$batchSuccessCount},失败 {$batchFailedCount}"); } return $stats; @@ -155,52 +320,90 @@ class CalculateWechatAccountScoreCommand extends Command /** * 更新评分记录(根据wechatId和alias不一致情况) * - * @param WechatAccountHealthScoreService $service - * @param Output $output - * @return array + * @param WechatAccountHealthScoreService $service 健康分服务实例 + * @param Output $output 输出对象 + * @return array 处理结果统计 + * @throws \Exception 如果查询或处理过程中出现错误 */ - private function updateScoreRecords($service, $output) + private function updateScoreRecords($service, $output, $accountId = null, $batchSize = 50) { $stats = ['total' => 0]; - // 查找wechatId和alias不一致的账号 - $inconsistentAccounts = Db::table('s2_wechat_account') - ->where('isDeleted', 0) - ->where('wechatId', '<>', '') - ->where('alias', '<>', '') - ->whereRaw('wechatId != alias') - ->field('id, wechatId, alias') - ->select(); - - // 查找wechatId和alias一致的账号 - $consistentAccounts = Db::table('s2_wechat_account') - ->where('isDeleted', 0) - ->where('wechatId', '<>', '') - ->where('alias', '<>', '') - ->whereRaw('wechatId = alias') - ->field('id, wechatId, alias') - ->select(); + try { + // 优化查询:合并两次查询为一次,减少数据库访问次数 + $query = Db::table(self::TABLE_WECHAT_ACCOUNT) + ->where('isDeleted', 0) + ->where('wechatId', '<>', '') + ->where('alias', '<>', ''); + + // 如果指定了账号ID,则只处理该账号 + if ($accountId) { + $query->where('id', $accountId); + } + + $accounts = $query->field('id, wechatId, alias, IF(wechatId = alias, 0, 1) as isModifiedAlias') + ->select(); + + // 分类处理查询结果 + $inconsistentAccounts = []; + $consistentAccounts = []; + + foreach ($accounts as $account) { + if ($account['isModifiedAlias'] == 1) { + $inconsistentAccounts[] = $account; + } else { + $consistentAccounts[] = $account; + } + } + } catch (\Exception $e) { + Log::error("查询需要更新评分记录的账号失败: " . $e->getMessage()); + throw new \Exception("查询需要更新评分记录的账号失败: " . $e->getMessage(), 0, $e); + } $allAccounts = array_merge($inconsistentAccounts, $consistentAccounts); $stats['total'] = count($allAccounts); if ($stats['total'] == 0) { $output->writeln("没有需要更新的账号"); + Log::info("没有需要更新的评分记录"); return $stats; } $output->writeln("找到 {$stats['total']} 个需要更新的账号(不一致: " . count($inconsistentAccounts) . ",一致: " . count($consistentAccounts) . ")"); + Log::info("找到 {$stats['total']} 个需要更新的账号(不一致: " . count($inconsistentAccounts) . ",一致: " . count($consistentAccounts) . ")"); $updatedCount = 0; - foreach ($allAccounts as $account) { - $isModifiedAlias = in_array($account['id'], array_column($inconsistentAccounts, 'id')); - $this->updateScoreRecord($account['id'], $isModifiedAlias, $service); - $updatedCount++; + // 优化批处理:使用传入的批处理大小 + $batches = array_chunk($allAccounts, $batchSize); + $batchCount = count($batches); + + Log::info("将分 {$batchCount} 批更新评分记录,每批 {$batchSize} 个账号"); + + foreach ($batches as $batchIndex => $batch) { + $batchStartTime = microtime(true); + $batchUpdatedCount = 0; - if ($updatedCount % 100 == 0) { - $output->write("."); + foreach ($batch as $account) { + $isModifiedAlias = isset($account['isModifiedAlias']) ? + ($account['isModifiedAlias'] == 1) : + in_array($account['id'], array_column($inconsistentAccounts, 'id')); + + $this->updateScoreRecord($account['id'], $isModifiedAlias, $service); + $updatedCount++; + $batchUpdatedCount++; + + if ($batchUpdatedCount % 20 == 0) { + $output->write("."); + } } + + $batchEndTime = microtime(true); + $batchDuration = round($batchEndTime - $batchStartTime, 2); + + // 每批次完成后输出进度信息 + $output->writeln(" 批次 " . ($batchIndex + 1) . "/{$batchCount} 完成,耗时 {$batchDuration} 秒,更新 {$batchUpdatedCount} 条记录"); + Log::info("更新评分记录批次 " . ($batchIndex + 1) . "/{$batchCount} 完成,耗时 {$batchDuration} 秒,更新 {$batchUpdatedCount} 条记录"); } if ($updatedCount > 0 && $updatedCount % 100 == 0) { @@ -213,27 +416,44 @@ class CalculateWechatAccountScoreCommand extends Command /** * 批量更新健康分(只更新动态分) * - * @param WechatAccountHealthScoreService $service - * @param Output $output - * @return array + * @param WechatAccountHealthScoreService $service 健康分服务实例 + * @param Output $output 输出对象 + * @return array 处理结果统计 + * @throws \Exception 如果查询或处理过程中出现错误 */ - private function batchUpdateHealthScore($service, $output) + private function batchUpdateHealthScore($service, $output, $accountId = null, $batchSize = 50, $forceRecalculate = false) { - // 获取所有已计算基础分的账号 - $accountIds = Db::table('s2_wechat_account_score') - ->where('baseScoreCalculated', 1) - ->column('accountId'); + try { + // 获取所有已计算基础分的账号 + // 优化查询:只查询必要的字段,使用索引字段 + $query = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE) + ->where('baseScoreCalculated', 1); + + // 如果指定了账号ID,则只处理该账号 + if ($accountId) { + $query->where('accountId', $accountId); + } + + $accountIds = $query->column('accountId'); + } catch (\Exception $e) { + Log::error("查询需要批量更新健康分的账号失败: " . $e->getMessage()); + throw new \Exception("查询需要批量更新健康分的账号失败: " . $e->getMessage(), 0, $e); + } $total = count($accountIds); if ($total == 0) { $output->writeln("没有需要更新的账号"); + Log::info("没有需要批量更新健康分的账号"); return ['success' => 0, 'failed' => 0, 'errors' => []]; } $output->writeln("找到 {$total} 个需要更新动态分的账号"); + Log::info("找到 {$total} 个需要更新动态分的账号"); - $stats = $service->batchCalculateAndUpdate($accountIds, 100, false); + // 使用传入的批处理大小和强制重新计算标志 + Log::info("使用批量大小 {$batchSize} 进行批量更新健康分,强制重新计算基础分: " . ($forceRecalculate ? 'true' : 'false')); + $stats = $service->batchCalculateAndUpdate($accountIds, $batchSize, $forceRecalculate); return $stats; } @@ -245,31 +465,47 @@ class CalculateWechatAccountScoreCommand extends Command * @param bool $isModifiedAlias 是否已修改微信号 * @param WechatAccountHealthScoreService $service 评分服务 */ + /** + * 更新评分记录 + * + * @param int $accountId 账号ID + * @param bool $isModifiedAlias 是否已修改微信号 + * @param WechatAccountHealthScoreService $service 评分服务 + * @return bool 是否成功更新 + */ private function updateScoreRecord($accountId, $isModifiedAlias, $service) { - // 获取账号数据 - $accountData = Db::table('s2_wechat_account') - ->where('id', $accountId) - ->find(); + Log::debug("开始更新账号 {$accountId} 的评分记录,isModifiedAlias: " . ($isModifiedAlias ? 'true' : 'false')); - if (empty($accountData)) { - return; - } - - // 确保评分记录存在 - $scoreRecord = Db::table('s2_wechat_account_score') - ->where('accountId', $accountId) - ->find(); + try { + // 获取账号数据 - 只查询必要的字段 + $accountData = Db::table(self::TABLE_WECHAT_ACCOUNT) + ->where('id', $accountId) + ->field('id, wechatId, alias') // 只查询必要的字段 + ->find(); + + if (empty($accountData)) { + Log::warning("账号 {$accountId} 不存在,跳过更新评分记录"); + return false; + } + + // 确保评分记录存在 - 只查询必要的字段 + $scoreRecord = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE) + ->where('accountId', $accountId) + ->field('accountId, baseScore, baseScoreCalculated, baseInfoScore, dynamicScore') // 只查询必要的字段 + ->find(); if (empty($scoreRecord)) { // 如果记录不存在,创建并计算基础分 + Log::info("账号 {$accountId} 的评分记录不存在,创建并计算基础分"); $service->calculateAndUpdate($accountId); - $scoreRecord = Db::table('s2_wechat_account_score') + $scoreRecord = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE) ->where('accountId', $accountId) ->find(); } if (empty($scoreRecord)) { + Log::warning("账号 {$accountId} 的评分记录创建失败,跳过更新"); return; } @@ -297,15 +533,26 @@ class CalculateWechatAccountScoreCommand extends Command $healthScore = max(0, min(100, $healthScore)); $updateData['healthScore'] = $healthScore; $updateData['maxAddFriendPerDay'] = (int)floor($healthScore * 0.2); + + Log::info("账号 {$accountId} 的基础信息分从 {$oldBaseInfoScore} 更新为 {$newBaseInfoScore}," . + "基础分从 {$oldBaseScore} 更新为 {$newBaseScore},健康分更新为 {$healthScore}"); } } else { // 基础分未计算,只更新标记和基础信息分 $updateData['baseInfoScore'] = $isModifiedAlias ? 10 : 0; } - Db::table('s2_wechat_account_score') + $result = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE) ->where('accountId', $accountId) ->update($updateData); + + Log::debug("账号 {$accountId} 的评分记录更新" . ($result !== false ? "成功" : "失败")); + + return $result !== false; + } catch (\Exception $e) { + Log::error("更新账号 {$accountId} 的评分记录失败: " . $e->getMessage()); + return false; + } } } diff --git a/Server/application/common/service/WechatAccountHealthScoreService.php b/Server/application/common/service/WechatAccountHealthScoreService.php index 02a89a9b..d6246a26 100644 --- a/Server/application/common/service/WechatAccountHealthScoreService.php +++ b/Server/application/common/service/WechatAccountHealthScoreService.php @@ -4,6 +4,8 @@ namespace app\common\service; use think\Db; use think\Exception; +use think\facade\Log; +use think\facade\Cache; /** * 微信账号健康分评分服务(优化版) @@ -14,33 +16,63 @@ use think\Exception; * 2. 各个评分维度独立存储 * 3. 使用独立的评分记录表 * 4. 好友数量评分特殊处理(避免同步问题) + * 5. 动态分仅统计近30天数据 + * 6. 优化数据库查询,减少重复计算 + * 7. 添加完善的日志记录,便于问题排查 * * 健康分 = 基础分 + 动态分 * 基础分:60-100分(默认60分 + 基础信息10分 + 好友数量30分) * 动态分:扣分和加分规则 + * + * @author Your Name + * @version 2.0.0 */ class WechatAccountHealthScoreService { - // 默认基础分 + /** + * 缓存相关配置 + */ + const CACHE_PREFIX = 'wechat_health_score:'; // 缓存前缀 + const CACHE_TTL = 3600; // 缓存有效期(秒) + + /** + * 默认基础分 + */ const DEFAULT_BASE_SCORE = 60; - // 基础信息分数 + /** + * 基础信息分数 + */ const BASE_INFO_SCORE = 10; - // 好友数量分数区间 - const FRIEND_COUNT_SCORE_0_50 = 3; - const FRIEND_COUNT_SCORE_51_500 = 6; - const FRIEND_COUNT_SCORE_501_3000 = 8; - const FRIEND_COUNT_SCORE_3001_PLUS = 12; + /** + * 好友数量分数区间 + */ + const FRIEND_COUNT_SCORE_0_50 = 3; // 0-50个好友 + const FRIEND_COUNT_SCORE_51_500 = 6; // 51-500个好友 + const FRIEND_COUNT_SCORE_501_3000 = 8; // 501-3000个好友 + const FRIEND_COUNT_SCORE_3001_PLUS = 12; // 3001+个好友 - // 动态分扣分规则 - const PENALTY_FIRST_FREQUENT = -15; // 首次频繁扣15分 + /** + * 动态分扣分规则 + */ + const PENALTY_FIRST_FREQUENT = -15; // 首次频繁扣15分 const PENALTY_SECOND_FREQUENT = -25; // 再次频繁扣25分 const PENALTY_BANNED = -60; // 封号扣60分 - // 动态分加分规则 + /** + * 动态分加分规则 + */ const BONUS_NO_FREQUENT_PER_DAY = 5; // 连续3天不触发频繁,每天+5分 + /** + * 数据库表名 + */ + const TABLE_WECHAT_ACCOUNT = 's2_wechat_account'; + const TABLE_WECHAT_ACCOUNT_SCORE = 's2_wechat_account_score'; + const TABLE_FRIEND_TASK = 's2_friend_task'; + const TABLE_WECHAT_MESSAGE = 's2_wechat_message'; + /** * 计算并更新账号健康分 * @@ -48,38 +80,65 @@ class WechatAccountHealthScoreService * @param array $accountData 账号数据(可选,如果不传则从数据库查询) * @param bool $forceRecalculateBase 是否强制重新计算基础分(默认false) * @return array 返回评分结果 + * @throws Exception 如果计算过程中出现错误 */ public function calculateAndUpdate($accountId, $accountData = null, $forceRecalculateBase = false) { + // 参数验证 + if (empty($accountId) || !is_numeric($accountId)) { + $errorMsg = "无效的账号ID: " . (is_scalar($accountId) ? $accountId : gettype($accountId)); + Log::error($errorMsg); + throw new Exception($errorMsg); + } + try { + Log::info("开始计算账号健康分,accountId: {$accountId}, forceRecalculateBase: " . ($forceRecalculateBase ? 'true' : 'false')); + // 获取账号数据 if (empty($accountData)) { - $accountData = Db::table('s2_wechat_account') + $accountData = Db::table(self::TABLE_WECHAT_ACCOUNT) ->where('id', $accountId) ->find(); + + Log::debug("查询账号数据: " . ($accountData ? "成功" : "失败")); } if (empty($accountData)) { - throw new Exception("账号不存在:{$accountId}"); + $errorMsg = "账号不存在:{$accountId}"; + Log::error($errorMsg); + throw new Exception($errorMsg); } $wechatId = $accountData['wechatId'] ?? ''; if (empty($wechatId)) { - throw new Exception("账号wechatId为空:{$accountId}"); + $errorMsg = "账号wechatId为空:{$accountId}"; + Log::error($errorMsg); + throw new Exception($errorMsg); } + Log::debug("账号数据: accountId={$accountId}, wechatId={$wechatId}"); + // 获取或创建评分记录 $scoreRecord = $this->getOrCreateScoreRecord($accountId, $wechatId); + Log::debug("获取评分记录: " . ($scoreRecord ? "成功" : "失败")); // 计算基础分(只计算一次,除非强制重新计算) if (!$scoreRecord['baseScoreCalculated'] || $forceRecalculateBase) { + Log::info("计算基础分,accountId: {$accountId}, baseScoreCalculated: " . + ($scoreRecord['baseScoreCalculated'] ? 'true' : 'false') . + ", forceRecalculateBase: " . ($forceRecalculateBase ? 'true' : 'false')); + $baseScoreData = $this->calculateBaseScore($accountData, $scoreRecord); $this->updateBaseScore($accountId, $baseScoreData); + + Log::debug("基础分计算结果: " . json_encode($baseScoreData)); + // 重新获取记录以获取最新数据 $scoreRecord = $this->getScoreRecord($accountId); } // 计算动态分(每次都要重新计算) + Log::info("计算动态分,accountId: {$accountId}"); $dynamicScoreData = $this->calculateDynamicScore($accountData, $scoreRecord); // 计算总分 @@ -93,6 +152,9 @@ class WechatAccountHealthScoreService // 计算每日最大加人次数 $maxAddFriendPerDay = $this->getMaxAddFriendPerDay($healthScore); + Log::info("健康分计算结果,accountId: {$accountId}, baseScore: {$baseScore}, dynamicScore: {$dynamicScore}, " . + "healthScore: {$healthScore}, maxAddFriendPerDay: {$maxAddFriendPerDay}"); + // 更新评分记录 $updateData = [ 'dynamicScore' => $dynamicScore, @@ -109,11 +171,16 @@ class WechatAccountHealthScoreService 'updateTime' => time() ]; - Db::table('s2_wechat_account_score') + $updateResult = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE) ->where('accountId', $accountId) ->update($updateData); + + // 更新成功后,清除缓存 + if ($updateResult !== false) { + $this->clearScoreCache($accountId); + } - return [ + $result = [ 'accountId' => $accountId, 'wechatId' => $wechatId, 'healthScore' => $healthScore, @@ -127,8 +194,19 @@ class WechatAccountHealthScoreService 'maxAddFriendPerDay' => $maxAddFriendPerDay ]; - } catch (Exception $e) { - throw new Exception("计算健康分失败:" . $e->getMessage()); + Log::debug("健康分计算完成,返回结果: " . json_encode($result)); + return $result; + + } catch (\PDOException $e) { + // 数据库异常 + $errorMsg = "数据库操作失败,accountId: {$accountId}, 错误: " . $e->getMessage(); + Log::error($errorMsg); + throw new Exception($errorMsg, $e->getCode(), $e); + } catch (\Throwable $e) { + // 其他所有异常 + $errorMsg = "计算健康分失败,accountId: {$accountId}, 错误: " . $e->getMessage(); + Log::error($errorMsg); + throw new Exception($errorMsg, $e->getCode(), $e); } } @@ -141,11 +219,13 @@ class WechatAccountHealthScoreService */ private function getOrCreateScoreRecord($accountId, $wechatId) { - $record = Db::table('s2_wechat_account_score') - ->where('accountId', $accountId) - ->find(); + // 尝试获取现有记录 + $record = $this->getScoreRecord($accountId); + // 如果记录不存在,创建新记录 if (empty($record)) { + Log::info("为账号 {$accountId} 创建新的评分记录"); + // 创建新记录 $data = [ 'accountId' => $accountId, @@ -163,7 +243,7 @@ class WechatAccountHealthScoreService 'updateTime' => time() ]; - Db::table('s2_wechat_account_score')->insert($data); + Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)->insert($data); return $data; } @@ -175,13 +255,33 @@ class WechatAccountHealthScoreService * 获取评分记录 * * @param int $accountId 账号ID - * @return array + * @param bool $useCache 是否使用缓存(默认true) + * @return array 评分记录,如果不存在则返回空数组 */ - private function getScoreRecord($accountId) + private function getScoreRecord($accountId, $useCache = true) { - return Db::table('s2_wechat_account_score') + // 生成缓存键 + $cacheKey = self::CACHE_PREFIX . 'score:' . $accountId; + + // 如果使用缓存且缓存存在,则直接返回缓存数据 + if ($useCache && Cache::has($cacheKey)) { + $cachedData = Cache::get($cacheKey); + Log::debug("从缓存获取评分记录,accountId: {$accountId}"); + return $cachedData ?: []; + } + + // 从数据库获取记录 + $record = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE) ->where('accountId', $accountId) - ->find() ?: []; + ->find(); + + // 如果记录存在且使用缓存,则缓存记录 + if ($record && $useCache) { + Cache::set($cacheKey, $record, self::CACHE_TTL); + Log::debug("缓存评分记录,accountId: {$accountId}"); + } + + return $record ?: []; } /** @@ -240,12 +340,41 @@ class WechatAccountHealthScoreService * * @param int $accountId 账号ID * @param array $baseScoreData 基础分数据 + * @return bool 更新是否成功 */ private function updateBaseScore($accountId, $baseScoreData) { - Db::table('s2_wechat_account_score') - ->where('accountId', $accountId) - ->update($baseScoreData); + try { + $result = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE) + ->where('accountId', $accountId) + ->update($baseScoreData); + + Log::debug("更新基础分,accountId: {$accountId}, 结果: " . ($result ? "成功" : "失败")); + + // 更新成功后,清除缓存 + if ($result !== false) { + $this->clearScoreCache($accountId); + } + + return $result !== false; + } catch (Exception $e) { + Log::error("更新基础分失败,accountId: {$accountId}, 错误: " . $e->getMessage()); + return false; + } + } + + /** + * 清除评分记录缓存 + * + * @param int $accountId 账号ID + * @return bool 是否成功清除缓存 + */ + private function clearScoreCache($accountId) + { + $cacheKey = self::CACHE_PREFIX . 'score:' . $accountId; + $result = Cache::rm($cacheKey); + Log::debug("清除评分记录缓存,accountId: {$accountId}, 结果: " . ($result ? "成功" : "失败")); + return $result; } /** @@ -310,16 +439,44 @@ class WechatAccountHealthScoreService * @param int $accountId 账号ID * @param int $friendCount 好友数量 * @param string $source 来源(manual=手动,sync=同步) - * @return bool + * @return bool 更新是否成功 + * @throws Exception 如果参数无效或更新过程中出现错误 */ public function updateFriendCountScore($accountId, $friendCount, $source = 'manual') { - $scoreRecord = $this->getScoreRecord($accountId); + // 参数验证 + if (empty($accountId) || !is_numeric($accountId)) { + $errorMsg = "无效的账号ID: " . (is_scalar($accountId) ? $accountId : gettype($accountId)); + Log::error($errorMsg); + throw new Exception($errorMsg); + } - // 如果基础分已计算,不允许修改好友数量分(除非是手动更新) - if (!empty($scoreRecord['baseScoreCalculated']) && $source === 'sync') { - // 同步数据不允许修改已计算的基础分 - return false; + if (!is_numeric($friendCount) || $friendCount < 0) { + $errorMsg = "无效的好友数量: {$friendCount}"; + Log::error($errorMsg); + throw new Exception($errorMsg); + } + + if (!in_array($source, ['manual', 'sync'])) { + $errorMsg = "无效的来源: {$source},必须是 'manual' 或 'sync'"; + Log::error($errorMsg); + throw new Exception($errorMsg); + } + + try { + $scoreRecord = $this->getScoreRecord($accountId); + + // 如果基础分已计算,不允许修改好友数量分(除非是手动更新) + if (!empty($scoreRecord['baseScoreCalculated']) && $source === 'sync') { + // 同步数据不允许修改已计算的基础分 + Log::warning("同步数据不允许修改已计算的基础分,accountId: {$accountId}"); + return false; + } + } + catch (\Exception $e) { + $errorMsg = "获取评分记录失败,accountId: {$accountId}, 错误: " . $e->getMessage(); + Log::error($errorMsg); + throw new Exception($errorMsg, $e->getCode(), $e); } $friendCountScore = $this->getFriendCountScore($friendCount); @@ -348,16 +505,36 @@ class WechatAccountHealthScoreService $updateData['maxAddFriendPerDay'] = $this->getMaxAddFriendPerDay($healthScore); } - Db::table('s2_wechat_account_score') - ->where('accountId', $accountId) - ->update($updateData); - - return true; + try { + $result = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE) + ->where('accountId', $accountId) + ->update($updateData); + + // 更新成功后,清除缓存 + if ($result !== false) { + $this->clearScoreCache($accountId); + $this->clearHealthScoreCache($accountId); + Log::info("更新好友数量分成功,accountId: {$accountId}, friendCount: {$friendCount}, source: {$source}"); + } else { + Log::warning("更新好友数量分失败,accountId: {$accountId}, friendCount: {$friendCount}, source: {$source}"); + } + + return $result !== false; + } catch (\PDOException $e) { + $errorMsg = "数据库操作失败,accountId: {$accountId}, 错误: " . $e->getMessage(); + Log::error($errorMsg); + throw new Exception($errorMsg, $e->getCode(), $e); + } catch (\Throwable $e) { + $errorMsg = "更新好友数量分失败,accountId: {$accountId}, 错误: " . $e->getMessage(); + Log::error($errorMsg); + throw new Exception($errorMsg, $e->getCode(), $e); + } } /** * 计算动态分 * 动态分 = 扣分 + 加分 + * 如果添加好友记录表没有记录,则动态分为0 * * @param array $accountData 账号数据 * @param array $scoreRecord 现有评分记录 @@ -365,6 +542,11 @@ class WechatAccountHealthScoreService */ private function calculateDynamicScore($accountData, $scoreRecord) { + $accountId = $accountData['id'] ?? 0; + $wechatId = $accountData['wechatId'] ?? ''; + + Log::debug("开始计算动态分,accountId: {$accountId}, wechatId: {$wechatId}"); + $result = [ 'total' => 0, 'frequentPenalty' => 0, @@ -377,13 +559,33 @@ class WechatAccountHealthScoreService 'isBanned' => 0 ]; - $accountId = $accountData['id'] ?? 0; - $wechatId = $accountData['wechatId'] ?? ''; - if (empty($accountId) || empty($wechatId)) { + Log::warning("计算动态分失败: accountId或wechatId为空"); return $result; } + // 计算30天前的时间戳(在多个方法中使用) + $thirtyDaysAgo = time() - (30 * 24 * 3600); + + // 检查添加好友记录表是否有记录,如果没有记录则动态分为0 + // 使用EXISTS子查询优化性能,只检查是否存在记录,不需要计数 + $hasFriendTask = Db::table(self::TABLE_FRIEND_TASK) + ->where('wechatAccountId', $accountId) + ->where(function($query) use ($wechatId) { + if (!empty($wechatId)) { + $query->where('wechatId', $wechatId); + } + }) + ->value('id'); // 只获取ID,比count()更高效 + + // 如果添加好友记录表没有记录,则动态分为0 + if (empty($hasFriendTask)) { + Log::info("账号没有添加好友记录,动态分为0,accountId: {$accountId}"); + return $result; + } + + Log::debug("账号有添加好友记录,继续计算动态分,accountId: {$accountId}"); + // 继承现有数据 if (!empty($scoreRecord)) { $result['lastFrequentTime'] = $scoreRecord['lastFrequentTime'] ?? null; @@ -396,20 +598,20 @@ class WechatAccountHealthScoreService } // 1. 检查频繁记录(从s2_friend_task表查询,只统计近30天) - $frequentData = $this->checkFrequentFromFriendTask($accountId, $wechatId, $scoreRecord); + $frequentData = $this->checkFrequentFromFriendTask($accountId, $wechatId, $scoreRecord, $thirtyDaysAgo); $result['lastFrequentTime'] = $frequentData['lastFrequentTime'] ?? null; $result['frequentCount'] = $frequentData['frequentCount'] ?? 0; $result['frequentPenalty'] = $frequentData['frequentPenalty'] ?? 0; // 2. 检查封号记录(从s2_wechat_message表查询) - $banData = $this->checkBannedFromMessage($accountId, $wechatId); + $banData = $this->checkBannedFromMessage($accountId, $wechatId, $thirtyDaysAgo); if (!empty($banData)) { $result['isBanned'] = $banData['isBanned']; $result['banPenalty'] = $banData['banPenalty']; } // 3. 计算不频繁加分(基于近30天的频繁记录,反向参考频繁规则) - $noFrequentData = $this->calculateNoFrequentBonus($accountId, $wechatId, $frequentData); + $noFrequentData = $this->calculateNoFrequentBonus($accountId, $wechatId, $frequentData, $thirtyDaysAgo); $result['noFrequentBonus'] = $noFrequentData['bonus'] ?? 0; $result['consecutiveNoFrequentDays'] = $noFrequentData['consecutiveDays'] ?? 0; $result['lastNoFrequentTime'] = $noFrequentData['lastNoFrequentTime'] ?? null; @@ -417,6 +619,10 @@ class WechatAccountHealthScoreService // 计算总分 $result['total'] = $result['frequentPenalty'] + $result['noFrequentBonus'] + $result['banPenalty']; + Log::debug("动态分计算结果,accountId: {$accountId}, frequentPenalty: {$result['frequentPenalty']}, " . + "noFrequentBonus: {$result['noFrequentBonus']}, banPenalty: {$result['banPenalty']}, " . + "total: {$result['total']}"); + return $result; } @@ -428,16 +634,20 @@ class WechatAccountHealthScoreService * @param int $accountId 账号ID * @param string $wechatId 微信ID * @param array $scoreRecord 现有评分记录 + * @param int $thirtyDaysAgo 30天前的时间戳(可选,如果已计算则传入以避免重复计算) * @return array|null */ - private function checkFrequentFromFriendTask($accountId, $wechatId, $scoreRecord) + private function checkFrequentFromFriendTask($accountId, $wechatId, $scoreRecord, $thirtyDaysAgo = null) { - // 计算30天前的时间戳 - $thirtyDaysAgo = time() - (30 * 24 * 3600); + // 如果没有传入30天前的时间戳,则计算 + if ($thirtyDaysAgo === null) { + $thirtyDaysAgo = time() - (30 * 24 * 3600); + } // 查询包含"操作过于频繁"的记录(只统计近30天) // extra字段可能是文本或JSON格式,使用LIKE查询 - $frequentTasks = Db::table('s2_friend_task') + // 优化查询:只查询必要的字段,减少数据传输量 + $frequentTasks = Db::table(self::TABLE_FRIEND_TASK) ->where('wechatAccountId', $accountId) ->where('createTime', '>=', $thirtyDaysAgo) ->where(function($query) use ($wechatId) { @@ -448,7 +658,7 @@ class WechatAccountHealthScoreService ->where(function($query) { // 检查extra字段是否包含"操作过于频繁"(可能是文本或JSON) $query->where('extra', 'like', '%操作过于频繁%') - ->whereOr('extra', 'like', '%"操作过于频繁"%'); + ->whereOr('extra', 'like', '%"当前账号存在安全风险"%'); }) ->order('createTime', 'desc') ->field('id, createTime, extra') @@ -491,20 +701,25 @@ class WechatAccountHealthScoreService * * @param int $accountId 账号ID * @param string $wechatId 微信ID + * @param int $thirtyDaysAgo 30天前的时间戳(可选,如果已计算则传入以避免重复计算) * @return array|null */ - private function checkBannedFromMessage($accountId, $wechatId) + private function checkBannedFromMessage($accountId, $wechatId, $thirtyDaysAgo = null) { - // 计算30天前的时间戳 - $thirtyDaysAgo = time() - (30 * 24 * 3600); + // 如果没有传入30天前的时间戳,则计算 + if ($thirtyDaysAgo === null) { + $thirtyDaysAgo = time() - (30 * 24 * 3600); + } // 查询封号消息(只统计近30天) - $banMessage = Db::table('s2_wechat_message') + // 优化查询:只查询必要的字段,减少数据传输量 + $banMessage = Db::table(self::TABLE_WECHAT_MESSAGE) ->where('wechatAccountId', $accountId) ->where('msgType', 10000) ->where('content', 'like', '%你的账号被限制%') ->where('isDeleted', 0) ->where('createTime', '>=', $thirtyDaysAgo) + ->field('id, createTime') // 只查询必要的字段 ->order('createTime', 'desc') ->find(); @@ -530,9 +745,10 @@ class WechatAccountHealthScoreService * @param int $accountId 账号ID * @param string $wechatId 微信ID * @param array $frequentData 频繁数据(包含lastFrequentTime和frequentCount) + * @param int $thirtyDaysAgo 30天前的时间戳(可选,如果已计算则传入以避免重复计算) * @return array 包含bonus、consecutiveDays、lastNoFrequentTime */ - private function calculateNoFrequentBonus($accountId, $wechatId, $frequentData) + private function calculateNoFrequentBonus($accountId, $wechatId, $frequentData, $thirtyDaysAgo = null) { $result = [ 'bonus' => 0, @@ -544,8 +760,10 @@ class WechatAccountHealthScoreService return $result; } - // 计算30天前的时间戳 - $thirtyDaysAgo = time() - (30 * 24 * 3600); + // 如果没有传入30天前的时间戳,则计算 + if ($thirtyDaysAgo === null) { + $thirtyDaysAgo = time() - (30 * 24 * 3600); + } $currentTime = time(); // 获取最后一次频繁时间(30天内最后一次频繁的时间) @@ -601,29 +819,54 @@ class WechatAccountHealthScoreService * @param int $batchSize 每批处理数量 * @param bool $forceRecalculateBase 是否强制重新计算基础分 * @return array 处理结果统计 + * @throws Exception 如果参数无效或批量处理过程中出现严重错误 */ public function batchCalculateAndUpdate($accountIds = [], $batchSize = 100, $forceRecalculateBase = false) { - $stats = [ - 'total' => 0, - 'success' => 0, - 'failed' => 0, - 'errors' => [] - ]; - - // 如果没有指定账号ID,则处理所有账号 - if (empty($accountIds)) { - $accountIds = Db::table('s2_wechat_account') - ->where('isDeleted', 0) - ->column('id'); + // 参数验证 + if (!is_array($accountIds)) { + $errorMsg = "无效的账号ID数组: " . gettype($accountIds); + Log::error($errorMsg); + throw new Exception($errorMsg); } + if (!is_numeric($batchSize) || $batchSize <= 0) { + $errorMsg = "无效的批处理大小: {$batchSize}"; + Log::error($errorMsg); + throw new Exception($errorMsg); + } + + try { + $startTime = microtime(true); + Log::info("开始批量计算健康分,batchSize: {$batchSize}, forceRecalculateBase: " . ($forceRecalculateBase ? 'true' : 'false')); + + $stats = [ + 'total' => 0, + 'success' => 0, + 'failed' => 0, + 'errors' => [] + ]; + + // 如果没有指定账号ID,则处理所有账号 + if (empty($accountIds)) { + Log::info("未指定账号ID,处理所有未删除账号"); + $accountIds = Db::table(self::TABLE_WECHAT_ACCOUNT) + ->where('isDeleted', 0) + ->column('id'); + } + $stats['total'] = count($accountIds); + Log::info("需要处理的账号总数: {$stats['total']}"); // 分批处理 $batches = array_chunk($accountIds, $batchSize); + $batchCount = count($batches); + Log::info("分批处理,共 {$batchCount} 批"); - foreach ($batches as $batch) { + foreach ($batches as $batchIndex => $batch) { + $batchStartTime = microtime(true); + Log::info("开始处理第 " . ($batchIndex + 1) . " 批,共 " . count($batch) . " 个账号"); + foreach ($batch as $accountId) { try { $this->calculateAndUpdate($accountId, null, $forceRecalculateBase); @@ -634,11 +877,30 @@ class WechatAccountHealthScoreService 'accountId' => $accountId, 'error' => $e->getMessage() ]; + Log::error("账号 {$accountId} 计算失败: " . $e->getMessage()); } } + + $batchEndTime = microtime(true); + $batchDuration = round($batchEndTime - $batchStartTime, 2); + Log::info("第 " . ($batchIndex + 1) . " 批处理完成,耗时: {$batchDuration}秒," . + "成功: {$stats['success']},失败: {$stats['failed']}"); } + $endTime = microtime(true); + $totalDuration = round($endTime - $startTime, 2); + Log::info("批量计算健康分完成,总耗时: {$totalDuration}秒,成功: {$stats['success']},失败: {$stats['failed']}"); + return $stats; + } catch (\PDOException $e) { + $errorMsg = "批量计算健康分过程中数据库操作失败: " . $e->getMessage(); + Log::error($errorMsg); + throw new Exception($errorMsg, $e->getCode(), $e); + } catch (\Throwable $e) { + $errorMsg = "批量计算健康分过程中发生严重错误: " . $e->getMessage(); + Log::error($errorMsg); + throw new Exception($errorMsg, $e->getCode(), $e); + } } /** @@ -672,7 +934,7 @@ class WechatAccountHealthScoreService if (empty($scoreRecord)) { // 如果记录不存在,先创建 - $accountData = Db::table('s2_wechat_account') + $accountData = Db::table(self::TABLE_WECHAT_ACCOUNT) ->where('id', $accountId) ->find(); @@ -725,17 +987,36 @@ class WechatAccountHealthScoreService * 获取账号健康分信息 * * @param int $accountId 账号ID + * @param bool $useCache 是否使用缓存(默认true) + * @param bool $forceRecalculate 是否强制重新计算(默认false) * @return array|null */ - public function getHealthScore($accountId) + public function getHealthScore($accountId, $useCache = true, $forceRecalculate = false) { - $scoreRecord = $this->getScoreRecord($accountId); + // 如果强制重新计算,则不使用缓存 + if ($forceRecalculate) { + Log::info("强制重新计算健康分,accountId: {$accountId}"); + return $this->calculateAndUpdate($accountId, null, false); + } + + // 生成缓存键 + $cacheKey = self::CACHE_PREFIX . 'health:' . $accountId; + + // 如果使用缓存且缓存存在,则直接返回缓存数据 + if ($useCache && !$forceRecalculate && Cache::has($cacheKey)) { + $cachedData = Cache::get($cacheKey); + Log::debug("从缓存获取健康分信息,accountId: {$accountId}"); + return $cachedData; + } + + // 从数据库获取记录 + $scoreRecord = $this->getScoreRecord($accountId, $useCache); if (empty($scoreRecord)) { return null; } - return [ + $healthScoreInfo = [ 'accountId' => $scoreRecord['accountId'], 'wechatId' => $scoreRecord['wechatId'], 'healthScore' => $scoreRecord['healthScore'] ?? 0, @@ -753,5 +1034,31 @@ class WechatAccountHealthScoreService 'frequentCount' => $scoreRecord['frequentCount'] ?? 0, 'isBanned' => $scoreRecord['isBanned'] ?? 0 ]; + + // 如果使用缓存,则缓存健康分信息 + if ($useCache) { + Cache::set($cacheKey, $healthScoreInfo, self::CACHE_TTL); + Log::debug("缓存健康分信息,accountId: {$accountId}"); + } + + return $healthScoreInfo; + } + + /** + * 清除健康分信息缓存 + * + * @param int $accountId 账号ID + * @return bool 是否成功清除缓存 + */ + public function clearHealthScoreCache($accountId) + { + $cacheKey = self::CACHE_PREFIX . 'health:' . $accountId; + $result = Cache::rm($cacheKey); + + // 同时清除评分记录缓存 + $this->clearScoreCache($accountId); + + Log::debug("清除健康分信息缓存,accountId: {$accountId}, 结果: " . ($result ? "成功" : "失败")); + return $result; } } From 6c9551be05dbed1024fa76223d570de459c8fab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Fri, 21 Nov 2025 18:30:12 +0800 Subject: [PATCH 21/31] =?UTF-8?q?=E8=85=BE=E8=AE=AF=E5=9C=B0=E5=9B=BE?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E9=94=81=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Touchkebao/index.html | 4 + .../components/selectMap.module.scss | 111 ++ .../MessageEnter/components/selectMap.tsx | 1235 +++++------------ 3 files changed, 487 insertions(+), 863 deletions(-) create mode 100644 Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.module.scss diff --git a/Touchkebao/index.html b/Touchkebao/index.html index 92656ef9..d0dcd4e9 100644 --- a/Touchkebao/index.html +++ b/Touchkebao/index.html @@ -11,6 +11,10 @@ +
    diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.module.scss b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.module.scss new file mode 100644 index 00000000..f0a77bb3 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.module.scss @@ -0,0 +1,111 @@ +.selectMapContainer { + display: flex; + flex-direction: column; + height: 600px; + gap: 16px; +} + +.searchArea { + flex-shrink: 0; + position: relative; +} + +.searchInput { + width: 100%; +} + +.searchResults { + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 1000; + background: #fff; + border: 1px solid #e8e8e8; + border-radius: 4px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + margin-top: 4px; + max-height: 300px; + overflow-y: auto; + + :global(.ant-list-item) { + cursor: pointer; + padding: 12px 16px; + transition: background-color 0.2s; + + &:hover { + background-color: #f5f5f5; + } + } +} + +.mapArea { + flex: 1; + position: relative; + border: 1px solid #e8e8e8; + border-radius: 4px; + overflow: hidden; +} + +.mapContainer { + width: 100%; + height: 100%; + min-height: 400px; +} + +.loadingOverlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; +} + +.locationInfo { + flex-shrink: 0; + padding: 12px 16px; + background: #f5f5f5; + border-radius: 4px; + border: 1px solid #e8e8e8; +} + +.locationLabel { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 500; + color: #1890ff; + margin-bottom: 8px; +} + +.locationText { + font-size: 14px; + color: #333; + margin-bottom: 4px; + word-break: break-all; +} + +.locationCoords { + font-size: 12px; + color: #999; + font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace; +} + +.resultItem { + :global(.ant-list-item-meta-title) { + font-size: 14px; + color: #333; + margin-bottom: 4px; + } + + :global(.ant-list-item-meta-description) { + font-size: 12px; + color: #999; + } +} diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx index 7393c08c..d2658da6 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx @@ -1,22 +1,45 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; -import { - AutoComplete, - Input as AntInput, - message, - Modal, - Spin, - Button, -} from "antd"; -import { ContractData, weChatGroup, ChatRecord } from "@/pages/pc/ckbox/data"; +import React, { useState, useEffect, useRef } from "react"; +import { Modal, Input, Button, List, message, Spin } from "antd"; +import { SearchOutlined, EnvironmentOutlined } from "@ant-design/icons"; import { useWebSocketStore } from "@/store/module/websocket/websocket"; +import styles from "./selectMap.module.scss"; -declare const AMap: any; +// 声明腾讯地图类型 +declare global { + interface Window { + qq: any; + } +} interface SelectMapProps { visible: boolean; onClose: () => void; - contract: ContractData | weChatGroup; - addMessage: (message: ChatRecord) => void; + contract?: any; + addMessage?: (message: any) => void; + onConfirm?: (locationXml: string) => void; +} + +interface SearchResult { + id: string; + title: string; + address: string; + location: { + lat: number; + lng: number; + }; + adcode?: string; + city?: string; + district?: string; +} + +interface LocationData { + x: string; // 经度 + y: string; // 纬度 + scale: string; // 缩放级别 + label: string; // 地址标签 + poiname: string; // POI名称 + maptype: string; // 地图类型 + poiid: string; // POI ID } const SelectMap: React.FC = ({ @@ -24,874 +47,360 @@ const SelectMap: React.FC = ({ onClose, contract, addMessage, + onConfirm, }) => { - const [selectedLocation, setSelectedLocation] = useState(null); - const mapRef = useRef(null); - const markersRef = useRef([]); // 使用数组保存所有标记点 - const geocoderRef = useRef(null); // 保存 Geocoder 实例 - const placeSearchRef = useRef(null); // 保存 PlaceSearch 实例 - const geolocationRef = useRef(null); // 保存 Geolocation 实例 - const pendingClickRef = useRef<{ - lat: number; - lng: number; - lnglat: any; - } | null>(null); // 保存待处理的点击坐标 - const [options, setOptions] = useState([]); - const [mapLoading, setMapLoading] = useState(true); - - const sendCommand = useWebSocketStore(state => state.sendCommand); - - // 清除所有标记点 - const clearAllMarkers = useCallback(() => { - if (markersRef.current && markersRef.current.length > 0) { - console.log(`清除 ${markersRef.current.length} 个标记点`); - markersRef.current.forEach(marker => { - if (marker) { - marker.setMap(null); - marker = null; - } - }); - markersRef.current = []; - } - }, []); - - const addMarker = useCallback( - (lnglat: any) => { - console.log("addMarker 调用,坐标:", lnglat); - - // 先清除所有现有的标记点 - clearAllMarkers(); - - // 创建红色图标,保持默认倒水滴形状 - const redIcon = new AMap.Icon({ - size: new AMap.Size(25, 34), // 默认标记点尺寸 - image: "https://webapi.amap.com/theme/v1.3/markers/n/mark_r.png", // 红色标记点图片 - imageOffset: new AMap.Pixel(0, 0), - imageSize: new AMap.Size(25, 34), - }); - - // 创建新的标记点 - const newMarker = new AMap.Marker({ - position: lnglat, - map: mapRef.current, - icon: redIcon, - }); - - // 将新标记点添加到数组中 - markersRef.current.push(newMarker); - - mapRef.current.setCenter(lnglat); - mapRef.current.setZoom(16); // 确保缩放到合适级别 - console.log("新 marker 已添加并居中"); - }, - [clearAllMarkers], + const [searchValue, setSearchValue] = useState(""); + const [searchResults, setSearchResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); + const [selectedLocation, setSelectedLocation] = useState( + null, ); + const [map, setMap] = useState(null); + const [isReverseGeocoding, setIsReverseGeocoding] = useState(false); + const mapContainerRef = useRef(null); + const geocoderRef = useRef(null); + const searchServiceRef = useRef(null); + const markerRef = useRef(null); + const { sendCommand } = useWebSocketStore.getState(); - // 通用的地址获取函数 - const getAddressForLocation = useCallback( - (lat: number, lng: number, lnglat: any) => { - console.log("=== getAddressForLocation 调用 ==="); - console.log("坐标:", { lat, lng }); - console.log( - "Geocoder ref 状态:", - geocoderRef.current ? "存在" : "不存在", - ); - - // 检查 Geocoder 是否已初始化 - if (!geocoderRef.current) { - console.warn("Geocoder 未初始化,无法获取地址"); - return false; - } - - // 使用更精确的坐标进行查询 - // 高德地图 getAddress 支持 LngLat 对象或 [lng, lat] 数组 - let queryLnglat: any; - if (lnglat && typeof lnglat.getLat === "function") { - // 如果传入的是 LngLat 对象,直接使用 - queryLnglat = lnglat; - } else { - // 否则创建新的 LngLat 对象或使用数组格式 - try { - queryLnglat = new AMap.LngLat(lng, lat); - } catch (error) { - console.error("创建 LngLat 对象失败,使用数组格式:", error); - queryLnglat = [lng, lat]; - } - } - console.log("调用 geocoder.getAddress,坐标:", queryLnglat); - - try { - geocoderRef.current.getAddress( - queryLnglat, - (status: string, result: any) => { - console.log("=== Geocoder 回调触发(通用函数) ==="); - console.log("Status:", status); - console.log( - "Result:", - result ? JSON.stringify(result, null, 2) : "null", - ); - - if ( - status === "complete" && - result && - result.info === "OK" && - result.regeocode - ) { - const regeocode = result.regeocode; - const formattedAddress = regeocode.formattedAddress || ""; - const addressComponent = regeocode.addressComponent || {}; - - // 构建详细地址信息 - let addressLabel = formattedAddress; - let poiName = "点击位置"; - - // 优先级1: 如果有POI信息,优先使用POI名称 - if (regeocode.pois && regeocode.pois.length > 0) { - const poi = regeocode.pois[0]; - poiName = poi.name || poiName; - const poiAddress = poi.address || ""; - addressLabel = poiAddress - ? `${poiName} ${poiAddress}` - : `${poiName} ${formattedAddress}`; - } - // 优先级2: 如果有建筑物信息 - else if (regeocode.buildings && regeocode.buildings.length > 0) { - const building = regeocode.buildings[0]; - poiName = building.name || addressComponent.building || poiName; - addressLabel = `${poiName} ${formattedAddress}`; - } - // 优先级3: 如果有AOI(兴趣区域)信息 - else if (regeocode.aois && regeocode.aois.length > 0) { - const aoi = regeocode.aois[0]; - poiName = aoi.name || poiName; - addressLabel = `${poiName} ${formattedAddress}`; - } - // 优先级4: 使用地址组件构建详细地址 - else if (addressComponent.building) { - poiName = addressComponent.building; - addressLabel = `${poiName} ${formattedAddress}`; - } - // 优先级5: 组合地址组件 - else { - const parts = []; - if (addressComponent.province) - parts.push(addressComponent.province); - if (addressComponent.city) parts.push(addressComponent.city); - if (addressComponent.district) - parts.push(addressComponent.district); - if (addressComponent.township) - parts.push(addressComponent.township); - if (addressComponent.street) - parts.push(addressComponent.street); - if (addressComponent.streetNumber) - parts.push(addressComponent.streetNumber); - - if (parts.length > 0) { - const fullAddress = parts.join(""); - poiName = - addressComponent.street || - addressComponent.district || - "点击位置"; - addressLabel = fullAddress || formattedAddress; - } else { - addressLabel = - formattedAddress || `纬度: ${lat}, 经度: ${lng}`; - } - } - - setSelectedLocation({ - lat: lat, - lng: lng, - label: addressLabel, - poiname: poiName, - }); - - message.success("地址信息获取成功"); - } else { - console.warn("=== Geocoder 返回异常,尝试 PlaceSearch ==="); - console.warn("Status:", status); - console.warn("Result:", result); - // Geocoder 失败,使用 PlaceSearch 作为备用 - if (placeSearchRef.current) { - try { - const searchLnglat = lnglat || [lng, lat]; - placeSearchRef.current.searchNearBy( - "", - searchLnglat, - 1000, - (searchStatus: string, searchResult: any) => { - if ( - searchStatus === "complete" && - searchResult && - searchResult.info === "OK" && - searchResult.poiList?.pois?.length > 0 - ) { - const poi = searchResult.poiList.pois[0]; - const poiLabel = poi.address - ? `${poi.name} ${poi.address}` - : poi.name; - setSelectedLocation({ - lat: lat, - lng: lng, - label: poiLabel, - poiname: poi.name, - }); - message.success("通过附近搜索获取到地址信息"); - } else { - console.warn("PlaceSearch 返回异常:", { - status: searchStatus, - result: searchResult, - }); - const coordLabel = `纬度: ${lat}, 经度: ${lng}`; - setSelectedLocation({ - lat: lat, - lng: lng, - label: coordLabel, - poiname: "点击位置", - }); - message.warning("无法获取详细地址信息,但坐标已记录"); - } - }, - ); - } catch (placeSearchError) { - console.error( - "PlaceSearch.searchNearBy 调用异常:", - placeSearchError, - ); - const coordLabel = `纬度: ${lat}, 经度: ${lng}`; - setSelectedLocation({ - lat: lat, - lng: lng, - label: coordLabel, - poiname: "点击位置", - }); - message.warning("无法获取详细地址信息,但坐标已记录"); - } - } else { - const coordLabel = `纬度: ${lat}, 经度: ${lng}`; - setSelectedLocation({ - lat: lat, - lng: lng, - label: coordLabel, - poiname: "点击位置", - }); - message.warning("无法获取详细地址信息,但坐标已记录"); - } - } - }, - ); - } catch (error) { - console.error("=== Geocoder.getAddress 调用异常 ===", error); - // 如果 geocoder 调用失败,尝试使用 PlaceSearch - if (placeSearchRef.current) { - console.log("尝试使用 PlaceSearch 作为备用方案"); - try { - placeSearchRef.current.searchNearBy( - "", - lnglat || [lng, lat], - 1000, - (status: string, result: any) => { - if ( - status === "complete" && - result && - result.info === "OK" && - result.poiList?.pois?.length > 0 - ) { - const poi = result.poiList.pois[0]; - const poiLabel = poi.address - ? `${poi.name} ${poi.address}` - : poi.name; - setSelectedLocation({ - lat: lat, - lng: lng, - label: poiLabel, - poiname: poi.name, - }); - message.success("通过附近搜索获取到地址信息"); - } else { - const coordLabel = `纬度: ${lat}, 经度: ${lng}`; - setSelectedLocation({ - lat: lat, - lng: lng, - label: coordLabel, - poiname: "点击位置", - }); - message.warning("无法获取详细地址信息,但坐标已记录"); - } - }, - ); - } catch (placeSearchError) { - console.error("PlaceSearch 调用也失败:", placeSearchError); - const coordLabel = `纬度: ${lat}, 经度: ${lng}`; - setSelectedLocation({ - lat: lat, - lng: lng, - label: coordLabel, - poiname: "点击位置", - }); - message.warning("无法获取详细地址信息,但坐标已记录"); - } - } else { - const coordLabel = `纬度: ${lat}, 经度: ${lng}`; - setSelectedLocation({ - lat: lat, - lng: lng, - label: coordLabel, - poiname: "点击位置", - }); - message.warning("无法获取详细地址信息,但坐标已记录"); - } - return false; - } - - return true; - }, - [], - ); - - const initMap = useCallback(() => { - console.log("initMap 执行中"); - setMapLoading(true); - - // 确保容器存在 - const container = document.getElementById("amap-container"); - if (!container) { - console.error("地图容器不存在"); - setMapLoading(false); - return; - } - - // 确保容器样式正确 - container.style.pointerEvents = "auto"; - container.style.cursor = "crosshair"; - container.style.position = "relative"; - container.style.zIndex = "1"; - - const map = new AMap.Map("amap-container", { - zoom: 16, - center: [118.113653, 24.470164], // 默认中心 - viewMode: "2D", // 明确指定视图模式 - }); - mapRef.current = map; - - // 添加超时机制,防止 loading 一直显示 - const loadingTimeout = setTimeout(() => { - console.warn("地图加载超时,强制关闭 loading"); - setMapLoading(false); - }, 10000); // 10秒超时 - - // 立即加载插件,不等待地图 complete 事件 - console.log("=== 立即开始加载 AMap 插件(地图创建后) ==="); - AMap.plugin( - [ - "AMap.AutoComplete", - "AMap.PlaceSearch", - "AMap.Geocoder", - "AMap.Geolocation", - ], - error => { - if (error) { - console.error("=== AMap 插件加载失败 ===", error); - message.error("地图插件加载失败,部分功能可能不可用"); - clearTimeout(loadingTimeout); - setMapLoading(false); - return; - } - console.log("=== AMap 插件加载成功 ==="); - - // 立即创建 PlaceSearch 和 Geocoder 实例 - const placeSearch = new AMap.PlaceSearch({ - city: "全国", - map: map, - }); - placeSearchRef.current = placeSearch; - console.log("PlaceSearch 实例已创建"); - - const geocoder = new AMap.Geocoder({ - city: "全国", - radius: 1000, // 搜索半径,单位米 - extensions: "all", // 返回详细信息,包括POI、建筑物等 - }); - geocoderRef.current = geocoder; - console.log("Geocoder 实例已创建并保存到 ref,现在可以立即使用"); - - // 创建 Geolocation 实例用于获取当前位置 - const geolocation = new AMap.Geolocation({ - enableHighAccuracy: true, // 是否使用高精度定位,默认:true - timeout: 10000, // 超过10秒后停止定位,默认:无穷大 - maximumAge: 0, // 定位结果缓存0毫秒,默认:0 - convert: true, // 自动偏移坐标,偏移后的坐标为高德坐标,默认:true - showButton: false, // 显示定位按钮,默认:true - buttonPosition: "RB", // 定位按钮停靠位置,默认:'LB',左下角 - showMarker: false, // 定位成功后在定位到的位置显示点标记,默认:true - showCircle: false, // 定位成功后用圆圈表示定位精度范围,默认:true - panToLocation: false, // 定位成功后将定位到的位置作为地图中心点,默认:true - zoomToAccuracy: false, // 定位成功后调整地图视野范围使定位位置及精度范围视野内可见,默认:false - }); - geolocationRef.current = geolocation; - - // 获取当前位置 - console.log("=== 开始获取当前位置 ==="); - geolocation.getCurrentPosition((status: string, result: any) => { - console.log("=== 定位回调触发 ==="); - console.log("定位状态:", status); - console.log("定位结果:", result); - - if (status === "complete") { - const { position, formattedAddress, addressComponent } = result; - const lat = position.lat; - const lng = position.lng; - const lnglat = new AMap.LngLat(lng, lat); - - console.log("定位成功,当前位置:", { lat, lng }); - console.log("定位地址:", formattedAddress); - - // 将地图中心设置为当前位置 - map.setCenter(lnglat); - map.setZoom(16); - - // 添加当前位置标记 - addMarker(lnglat); - - // 设置选中位置信息 - let addressLabel = formattedAddress || `纬度: ${lat}, 经度: ${lng}`; - let poiName = "当前位置"; - - // 尝试从地址组件中获取更详细的信息 - if (addressComponent) { - const parts = []; - if (addressComponent.province) - parts.push(addressComponent.province); - if (addressComponent.city) parts.push(addressComponent.city); - if (addressComponent.district) - parts.push(addressComponent.district); - if (addressComponent.street) parts.push(addressComponent.street); - if (addressComponent.streetNumber) - parts.push(addressComponent.streetNumber); - - if (parts.length > 0) { - addressLabel = parts.join(""); - } - - poiName = - addressComponent.street || - addressComponent.district || - "当前位置"; - } - - setSelectedLocation({ - lat: lat, - lng: lng, - label: addressLabel, - poiname: poiName, - }); - - message.success("已获取当前位置"); - } else { - console.warn("定位失败:", result); - message.warning( - "无法获取当前位置,请手动点击地图选择位置。原因: " + - (result.message || "定位服务不可用"), - ); - // 定位失败时,使用默认中心点 - console.log("使用默认中心点"); - } - }); - - // 如果有待处理的点击坐标,立即处理它 - if (pendingClickRef.current) { - console.log("=== 检测到待处理的点击坐标,立即处理 ==="); - const { lat, lng, lnglat } = pendingClickRef.current; - console.log("待处理坐标:", { lat, lng }); - setTimeout(() => { - const success = getAddressForLocation(lat, lng, lnglat); - if (success) { - console.log("✓ 待处理的坐标已成功获取地址"); - pendingClickRef.current = null; // 清除待处理坐标 - } else { - console.warn("待处理的坐标获取地址失败"); - } - }, 100); - } - }, - ); - - // 立即绑定点击事件,插件可能已加载或正在加载 - console.log("立即绑定点击事件(插件可能已初始化)"); - map.on("click", (e: any) => { - console.log("=== 地图点击事件触发 ==="); - console.log("点击事件对象:", e); - console.log("点击位置对象:", e.lnglat); - console.log( - "Geocoder ref 状态:", - geocoderRef.current ? "已初始化" : "未初始化", - ); - - if (!e || !e.lnglat) { - console.error("点击事件无效,缺少 lnglat"); - return; - } - - const lnglat = e.lnglat; - const lat = lnglat.getLat(); - const lng = lnglat.getLng(); - console.log(`点击坐标 - 纬度: ${lat}, 经度: ${lng}`); - - // 立即添加标记和居中 - addMarker(lnglat); - - // 设置基本 selectedLocation(至少有坐标) - setSelectedLocation({ - lat: lat, - lng: lng, - label: "正在获取地址信息...", - poiname: "点击位置", - }); - - // 如果 Geocoder 已初始化,立即使用它 - if (geocoderRef.current) { - console.log("Geocoder 已初始化,立即获取地址"); - getAddressForLocation(lat, lng, lnglat); - } else { - console.log("Geocoder 未初始化,保存坐标待插件加载完成后处理"); - // 保存待处理的坐标 - pendingClickRef.current = { lat, lng, lnglat }; - } - }); - - // 等待地图完全加载后关闭 loading - map.on("complete", () => { - console.log("地图加载完成"); - // 清除超时定时器 - clearTimeout(loadingTimeout); - // 关闭 loading - setMapLoading(false); - console.log("地图加载完成,loading 已关闭"); - - // 确保地图容器可点击 - const mapContainer = map.getContainer(); - if (mapContainer) { - mapContainer.style.pointerEvents = "auto"; - mapContainer.style.cursor = "crosshair"; - console.log("地图容器指针事件已启用"); - } - }); - - // 如果地图加载失败,也设置加载完成 - map.on("error", (error: any) => { - console.error("地图加载错误:", error); - clearTimeout(loadingTimeout); - setMapLoading(false); - message.error("地图加载失败"); - }); - }, [addMarker, getAddressForLocation]); - - const handleSearch = value => { - if (value) { - AMap.plugin("AMap.AutoComplete", () => { - const auto = new AMap.AutoComplete({ city: "全国" }); - auto.search(value, (status, result) => { - if (status === "complete") { - setOptions( - result.tips.map(tip => ({ - value: tip.name, - data: tip, - })), - ); - } - }); - }); - } else { - setOptions([]); - } - }; - - const onSelect = (value, option) => { - const { district, address, name, location } = option.data; - const lnglat = location; - setSelectedLocation({ - lat: lnglat.lat, - lng: lnglat.lng, - label: `${name} ${address || district}`, - poiname: name, - }); - addMarker(lnglat); - mapRef.current.setCenter(lnglat); - }; - - const handleModalChange = useCallback( - (visible: boolean) => { - if (visible) { - console.log("模态打开:开始加载地图脚本"); - setMapLoading(true); - setSelectedLocation(null); - const script = document.createElement("script"); - script.src = - "https://webapi.amap.com/maps?v=1.4.15&key=79370028f5763e46742125ed2e900c76&plugin=AMap.PlaceSearch,AMap.AutoComplete,AMap.Geocoder,AMap.Geolocation"; - script.async = true; - script.onload = () => { - console.log("脚本加载成功:开始初始化地图"); - setTimeout(() => initMap(), 100); // 添加延迟确保 DOM 就绪 - }; - script.onerror = () => { - console.error("脚本加载失败"); - message.error("地图加载失败,请检查网络或API密钥"); - setMapLoading(false); - }; - document.body.appendChild(script); - } else { - console.log("模态关闭:清理地图和脚本"); - // 清除所有标记点 - clearAllMarkers(); - // 重置 ref - geocoderRef.current = null; - placeSearchRef.current = null; - geolocationRef.current = null; - // Cleanup on close - const scripts = document.querySelectorAll( - 'script[src*="webapi.amap.com"]', - ); - scripts.forEach(s => { - if (document.body.contains(s)) { - document.body.removeChild(s); - } - }); - if (mapRef.current) { - mapRef.current.destroy(); - mapRef.current = null; - } - setMapLoading(false); - setOptions([]); // 重置搜索选项 - } - }, - [initMap, clearAllMarkers], - ); - - // 手动获取当前位置 - const handleGetCurrentLocation = useCallback(() => { - if (!geolocationRef.current) { - message.warning("定位服务未初始化,请稍候再试"); - return; - } - - console.log("=== 手动触发获取当前位置 ==="); - message.loading({ content: "正在获取当前位置...", key: "location" }); - - geolocationRef.current.getCurrentPosition((status: string, result: any) => { - message.destroy("location"); - console.log("=== 手动定位回调触发 ==="); - console.log("定位状态:", status); - console.log("定位结果:", result); - - if (status === "complete" && result && result.position) { - const { position, formattedAddress, addressComponent } = result; - const lat = position.lat; - const lng = position.lng; - const lnglat = new AMap.LngLat(lng, lat); - - console.log("定位成功,当前位置:", { lat, lng }); - - // 将地图中心设置为当前位置 - if (mapRef.current) { - mapRef.current.setCenter(lnglat); - mapRef.current.setZoom(16); - } - - // 添加当前位置标记 - addMarker(lnglat); - - // 设置选中位置信息 - let addressLabel = formattedAddress || `纬度: ${lat}, 经度: ${lng}`; - let poiName = "当前位置"; - - // 尝试从地址组件中获取更详细的信息 - if (addressComponent) { - const parts = []; - if (addressComponent.province) parts.push(addressComponent.province); - if (addressComponent.city) parts.push(addressComponent.city); - if (addressComponent.district) parts.push(addressComponent.district); - if (addressComponent.street) parts.push(addressComponent.street); - if (addressComponent.streetNumber) - parts.push(addressComponent.streetNumber); - - if (parts.length > 0) { - addressLabel = parts.join(""); - } - - poiName = - addressComponent.street || addressComponent.district || "当前位置"; - } - - setSelectedLocation({ - lat: lat, - lng: lng, - label: addressLabel, - poiname: poiName, - }); - - message.success("已获取当前位置"); - } else { - console.warn("定位失败:", result); - message.error( - "获取当前位置失败: " + (result?.message || "定位服务不可用"), - ); - } - }); - }, [addMarker]); - - const handleSendLocation = () => { - if (!selectedLocation || !selectedLocation.lat || !selectedLocation.lng) { - message.warning("请选择有效位置"); - return; - } - - const { lat, lng, label, poiname } = selectedLocation; - const content = ``; - - const messageId = +Date.now(); - const params = { - wechatAccountId: contract.wechatAccountId, - wechatChatroomId: contract?.chatroomId ? contract.id : 0, - wechatFriendId: contract?.chatroomId ? 0 : contract.id, - msgSubType: 0, - msgType: 48, - content: content, - seq: messageId, - }; - - // 构造本地消息 - const localMessage: ChatRecord = { - id: messageId, - wechatAccountId: contract.wechatAccountId, - wechatFriendId: contract?.chatroomId ? 0 : contract.id, - wechatChatroomId: contract?.chatroomId ? contract.id : 0, - tenantId: 0, - accountId: 0, - synergyAccountId: 0, - content: content, - msgType: 48, - msgSubType: 0, - msgSvrId: "", - isSend: true, - createTime: new Date().toISOString(), - isDeleted: false, - deleteTime: "", - sendStatus: 1, - wechatTime: Date.now(), - origin: 0, - msgId: 0, - recalled: false, - seq: messageId, - }; - addMessage(localMessage); - - sendCommand("CmdSendMessage", params); - - onClose(); - setSelectedLocation(null); - }; - + // 初始化地图 useEffect(() => { - if (visible) { - handleModalChange(true); - // 确保容器在模态框打开后可以接收事件 - setTimeout(() => { - const container = document.getElementById("amap-container"); - if (container) { - container.style.pointerEvents = "auto"; - container.style.cursor = "crosshair"; - console.log( - "容器样式已设置,pointerEvents:", - container.style.pointerEvents, - ); + if (visible && mapContainerRef.current && window.qq && window.qq.maps) { + // 创建地图实例 + const center = new window.qq.maps.LatLng(39.908823, 116.39747); // 默认北京 + const mapInstance = new window.qq.maps.Map(mapContainerRef.current, { + center: center, + zoom: 13, + }); + + setMap(mapInstance); + + // 创建地理编码服务 + geocoderRef.current = new window.qq.maps.Geocoder({ + complete: (result: any) => { + setIsReverseGeocoding(false); + if (result && result.detail) { + const detail = result.detail; + const location = detail.location || detail.latLng; + if (location) { + setSelectedLocation({ + x: location.lng?.toString() || location.getLng().toString(), + y: location.lat?.toString() || location.getLat().toString(), + scale: "16", + label: + detail.address || detail.formatted_addresses?.recommend || "", + poiname: + detail.addressComponents?.street || detail.address || "", + maptype: "0", + poiid: detail.poiid || "", + }); + } + } + }, + error: () => { + setIsReverseGeocoding(false); + message.error("获取地址信息失败"); + }, + }); + + // 创建搜索服务 + searchServiceRef.current = new window.qq.maps.SearchService({ + complete: (result: any) => { + setIsSearching(false); + if (result && result.detail) { + const pois = result.detail.pois || []; + if (pois.length > 0) { + const searchResults = pois.map((poi: any) => { + const location = poi.location || poi.latLng; + return { + id: + poi.id || + `${location.lat || location.getLat()},${ + location.lng || location.getLng() + }`, + title: poi.title || poi.name || "", + address: poi.address || poi.ad_info?.adcode || "", + location: { + lat: location.lat || location.getLat(), + lng: location.lng || location.getLng(), + }, + adcode: poi.adcode || poi.ad_info?.adcode || "", + city: poi.city || poi.ad_info?.city || "", + district: poi.district || poi.ad_info?.district || "", + }; + }); + setSearchResults(searchResults); + } else { + setSearchResults([]); + message.info("未找到相关地址"); + } + } else { + setSearchResults([]); + message.info("未找到相关地址"); + } + }, + error: () => { + setIsSearching(false); + message.error("搜索失败,请重试"); + }, + }); + + // 地图点击事件 + window.qq.maps.event.addListener(mapInstance, "click", (event: any) => { + const lat = event.latLng.lat; + const lng = event.latLng.lng; + + // 更新标记点 + if (markerRef.current) { + markerRef.current.setMap(null); } - }, 200); - } else { - handleModalChange(false); + + const newMarker = new window.qq.maps.Marker({ + position: new window.qq.maps.LatLng(lat, lng), + map: mapInstance, + }); + + markerRef.current = newMarker; + + // 反向地理编码 + setIsReverseGeocoding(true); + geocoderRef.current.getAddress(new window.qq.maps.LatLng(lat, lng)); + }); + + return () => { + if (mapInstance) { + window.qq.maps.event.clearListeners(mapInstance, "click"); + } + }; } - }, [visible, handleModalChange]); + }, [visible]); + + // 搜索地址 + const handleSearch = () => { + if (!searchValue.trim()) { + message.warning("请输入搜索关键词"); + return; + } + + if (!searchServiceRef.current) { + message.error("搜索服务未初始化"); + return; + } + + setIsSearching(true); + searchServiceRef.current.search(searchValue); + }; + + // 选择搜索结果 + const handleSelectResult = (result: SearchResult) => { + if (!map) return; + + const lat = result.location.lat; + const lng = result.location.lng; + + // 移动地图中心 + map.setCenter(new window.qq.maps.LatLng(lat, lng)); + map.setZoom(16); + + // 更新标记点 + if (markerRef.current) { + markerRef.current.setMap(null); + } + + const newMarker = new window.qq.maps.Marker({ + position: new window.qq.maps.LatLng(lat, lng), + map: map, + }); + + markerRef.current = newMarker; + + // 设置选中的位置信息 + setSelectedLocation({ + x: lng.toString(), + y: lat.toString(), + scale: "16", + label: result.address || result.title, + poiname: result.title || "", + maptype: "0", + poiid: result.id || "", + }); + + // 清空搜索结果 + setSearchResults([]); + setSearchValue(""); + }; + + // 确认选择 + const handleConfirm = () => { + if (!selectedLocation) { + message.warning("请先选择位置"); + return; + } + + // 生成XML格式的位置信息 + const locationXml = ``; + + // 如果有onConfirm回调,调用它 + if (onConfirm) { + onConfirm(locationXml); + } + + // 如果有addMessage和contract,发送位置消息 + if (addMessage && contract) { + const messageId = +Date.now(); + const localMessage = { + id: messageId, + wechatAccountId: contract.wechatAccountId, + wechatFriendId: contract?.chatroomId ? 0 : contract.id, + wechatChatroomId: contract?.chatroomId ? contract.id : 0, + tenantId: 0, + accountId: 0, + synergyAccountId: 0, + content: locationXml, + msgType: 48, // 位置消息类型 + msgSubType: 0, + msgSvrId: "", + isSend: true, + createTime: new Date().toISOString(), + isDeleted: false, + deleteTime: "", + sendStatus: 1, + wechatTime: Date.now(), + origin: 0, + msgId: 0, + recalled: false, + seq: messageId, + }; + + addMessage(localMessage); + + // 发送消息到服务器 + sendCommand("CmdSendMessage", { + wechatAccountId: contract.wechatAccountId, + wechatChatroomId: contract?.chatroomId ? contract.id : 0, + wechatFriendId: contract?.chatroomId ? 0 : contract.id, + msgSubType: 0, + msgType: 48, + content: locationXml, + seq: messageId, + }); + } + + // 关闭弹窗并重置状态 + handleClose(); + }; + + // 关闭弹窗 + const handleClose = () => { + setSearchValue(""); + setSearchResults([]); + setSelectedLocation(null); + if (markerRef.current) { + markerRef.current.setMap(null); + markerRef.current = null; + } + setIsSearching(false); + setIsReverseGeocoding(false); + onClose(); + }; return ( -
    - - - + open={visible} + onCancel={handleClose} + width={900} + centered + footer={[ + , -
    + {isReverseGeocoding ? "正在获取地址信息..." : "确认"} + , + ]} + > +
    + {/* 搜索区域 */} +
    + setSearchValue(e.target.value)} + onPressEnter={handleSearch} + prefix={} + suffix={ + + } + className={styles.searchInput} + /> - {/* 显示点击位置的信息 */} - {selectedLocation && ( -
    -
    - 位置信息: -
    -
    - 地址: {selectedLocation.label || "加载中..."} -
    -
    - 坐标: {selectedLocation.lat},{" "} - {selectedLocation.lng} -
    + {/* 搜索结果列表 */} + {searchResults.length > 0 && ( +
    + ( + handleSelectResult(item)} + > + } + title={item.title} + description={item.address} + /> + + )} + /> +
    + )}
    - )} -
    - {mapLoading && ( -
    - + {/* 地图区域 */} +
    +
    + {isReverseGeocoding && ( +
    + +
    + )} +
    + + {/* 选中位置信息 */} + {selectedLocation && ( +
    +
    + 已选择位置 +
    +
    + {selectedLocation.label || selectedLocation.poiname} +
    +
    + 经度: {selectedLocation.x}, 纬度: {selectedLocation.y} +
    )} -
    { - // 添加原生点击事件监听作为备用 - console.log("容器原生点击事件触发", e); - }} - >
    ); From 782031cbc6c451a76fc705a0f9e5eb3b351853b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Fri, 21 Nov 2025 19:02:41 +0800 Subject: [PATCH 22/31] =?UTF-8?q?=E5=A2=9E=E5=BC=BASelectMap=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=EF=BC=8C=E6=B7=BB=E5=8A=A0=E7=94=A8=E6=88=B7=E5=BD=93?= =?UTF-8?q?=E5=89=8D=E4=BD=8D=E7=BD=AE=E8=8E=B7=E5=8F=96=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E5=9C=B0=E5=9D=80=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E5=92=8C=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86?= =?UTF-8?q?=EF=BC=8C=E6=94=B9=E8=BF=9B=E5=8A=A0=E8=BD=BD=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=EF=BC=8C=E6=8F=90=E5=8D=87=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BD=93=E9=AA=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MessageEnter/components/selectMap.tsx | 145 +++++++++++++++--- 1 file changed, 120 insertions(+), 25 deletions(-) diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx index d2658da6..bc75ddc5 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx @@ -57,6 +57,7 @@ const SelectMap: React.FC = ({ ); const [map, setMap] = useState(null); const [isReverseGeocoding, setIsReverseGeocoding] = useState(false); + const [isLocating, setIsLocating] = useState(false); const mapContainerRef = useRef(null); const geocoderRef = useRef(null); const searchServiceRef = useRef(null); @@ -79,27 +80,70 @@ const SelectMap: React.FC = ({ geocoderRef.current = new window.qq.maps.Geocoder({ complete: (result: any) => { setIsReverseGeocoding(false); - if (result && result.detail) { - const detail = result.detail; - const location = detail.location || detail.latLng; - if (location) { - setSelectedLocation({ - x: location.lng?.toString() || location.getLng().toString(), - y: location.lat?.toString() || location.getLat().toString(), - scale: "16", - label: - detail.address || detail.formatted_addresses?.recommend || "", - poiname: - detail.addressComponents?.street || detail.address || "", - maptype: "0", - poiid: detail.poiid || "", - }); + try { + if (result && result.detail) { + const detail = result.detail; + const location = detail.location || detail.latLng; + if (location) { + const lat = + location.lat || (location.getLat ? location.getLat() : null); + const lng = + location.lng || (location.getLng ? location.getLng() : null); + + if (lat && lng) { + // 构建地址标签 + let addressLabel = ""; + if (detail.formatted_addresses) { + addressLabel = + detail.formatted_addresses.recommend || + detail.formatted_addresses.rough || + ""; + } + if (!addressLabel && detail.address) { + addressLabel = detail.address; + } + if (!addressLabel && detail.addressComponents) { + const addr = detail.addressComponents; + const parts = []; + if (addr.province) parts.push(addr.province); + if (addr.city) parts.push(addr.city); + if (addr.district) parts.push(addr.district); + if (addr.street) parts.push(addr.street); + if (addr.street_number) parts.push(addr.street_number); + addressLabel = parts.join(""); + } + if (!addressLabel) { + addressLabel = `${lat.toFixed(6)}, ${lng.toFixed(6)}`; + } + + setSelectedLocation({ + x: lng.toString(), + y: lat.toString(), + scale: "16", + label: addressLabel, + poiname: + detail.addressComponents?.street || detail.poiid || "", + maptype: "0", + poiid: detail.poiid || "", + }); + } else { + message.warning("无法解析位置信息"); + } + } else { + message.warning("未找到位置信息"); + } + } else { + message.warning("获取地址信息失败:返回数据为空"); } + } catch (error) { + console.error("解析地址信息错误:", error); + message.error("解析地址信息失败"); } }, - error: () => { + error: (error: any) => { setIsReverseGeocoding(false); - message.error("获取地址信息失败"); + console.error("反向地理编码错误:", error); + message.error("获取地址信息失败,请稍后重试"); }, }); @@ -163,10 +207,60 @@ const SelectMap: React.FC = ({ markerRef.current = newMarker; // 反向地理编码 - setIsReverseGeocoding(true); - geocoderRef.current.getAddress(new window.qq.maps.LatLng(lat, lng)); + if (geocoderRef.current) { + setIsReverseGeocoding(true); + geocoderRef.current.getAddress(new window.qq.maps.LatLng(lat, lng)); + } }); + // 获取用户当前位置 + if (navigator.geolocation) { + setIsLocating(true); + navigator.geolocation.getCurrentPosition( + position => { + setIsLocating(false); + const userLat = position.coords.latitude; + const userLng = position.coords.longitude; + + // 移动地图中心到用户位置 + const userLocation = new window.qq.maps.LatLng(userLat, userLng); + mapInstance.setCenter(userLocation); + mapInstance.setZoom(16); + + // 添加标记点 + if (markerRef.current) { + markerRef.current.setMap(null); + } + + const newMarker = new window.qq.maps.Marker({ + position: userLocation, + map: mapInstance, + }); + + markerRef.current = newMarker; + + // 获取用户位置的地址信息 + if (geocoderRef.current) { + setIsReverseGeocoding(true); + geocoderRef.current.getAddress(userLocation); + } + }, + error => { + setIsLocating(false); + console.error("获取位置失败:", error); + // 如果获取位置失败,使用默认位置(北京) + message.info("无法获取您的位置,已定位到默认位置"); + }, + { + enableHighAccuracy: true, + timeout: 10000, + maximumAge: 0, + }, + ); + } else { + message.info("您的浏览器不支持地理定位功能"); + } + return () => { if (mapInstance) { window.qq.maps.event.clearListeners(mapInstance, "click"); @@ -309,6 +403,7 @@ const SelectMap: React.FC = ({ } setIsSearching(false); setIsReverseGeocoding(false); + setIsLocating(false); onClose(); }; @@ -379,12 +474,12 @@ const SelectMap: React.FC = ({ {/* 地图区域 */}
    -
    - {isReverseGeocoding && ( -
    - -
    - )} + +
    +
    {/* 选中位置信息 */} From 21de87ca32efd51f3125d2eaf259f2376e3839bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Sat, 22 Nov 2025 11:45:53 +0800 Subject: [PATCH 23/31] =?UTF-8?q?=E6=9B=B4=E6=96=B0SelectMap=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E4=BB=A5=E4=BD=BF=E7=94=A8=E6=96=B0=E7=89=88=E8=85=BE?= =?UTF-8?q?=E8=AE=AF=E5=9C=B0=E5=9B=BEAPI=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=9C=B0=E5=9B=BE=E5=8A=A0=E8=BD=BD=E5=92=8C=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE=E8=8E=B7=E5=8F=96=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86=E5=92=8C?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Touchkebao/index.html | 2 +- .../MessageEnter/components/selectMap.tsx | 742 +++++++++++------- Touchkebao/腾讯地图定位服务修复说明.md | 73 ++ 3 files changed, 534 insertions(+), 283 deletions(-) create mode 100644 Touchkebao/腾讯地图定位服务修复说明.md diff --git a/Touchkebao/index.html b/Touchkebao/index.html index d0dcd4e9..f1c04fe6 100644 --- a/Touchkebao/index.html +++ b/Touchkebao/index.html @@ -13,7 +13,7 @@ diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx index bc75ddc5..d09316ce 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx @@ -4,10 +4,11 @@ import { SearchOutlined, EnvironmentOutlined } from "@ant-design/icons"; import { useWebSocketStore } from "@/store/module/websocket/websocket"; import styles from "./selectMap.module.scss"; -// 声明腾讯地图类型 +// 声明腾讯地图类型(新版TMap API) declare global { interface Window { - qq: any; + TMap: any; + geolocationRef: any; // 全局IP定位服务引用 } } @@ -58,281 +59,454 @@ const SelectMap: React.FC = ({ const [map, setMap] = useState(null); const [isReverseGeocoding, setIsReverseGeocoding] = useState(false); const [isLocating, setIsLocating] = useState(false); + const [tmapLoaded, setTmapLoaded] = useState(false); const mapContainerRef = useRef(null); const geocoderRef = useRef(null); - const searchServiceRef = useRef(null); + const suggestServiceRef = useRef(null); const markerRef = useRef(null); const { sendCommand } = useWebSocketStore.getState(); + // 加载腾讯地图SDK + useEffect(() => { + // 检查TMap是否已经加载 + if (window.TMap) { + setTmapLoaded(true); + return; + } + + // 动态加载腾讯地图SDK(使用与index.html相同的密钥) + const script = document.createElement("script"); + script.src = + "https://map.qq.com/api/gljs?v=1.exp&libraries=service&key=7DZBZ-ZSRK3-QJN3W-O5VTV-4E2P6-7GFYX"; + script.async = true; + script.onload = () => { + console.log("腾讯地图SDK加载成功"); + setTmapLoaded(true); + }; + script.onerror = () => { + console.error("腾讯地图SDK加载失败"); + message.error("地图加载失败,请刷新页面重试"); + }; + document.head.appendChild(script); + + return () => { + // 清理script标签 + if (document.head.contains(script)) { + document.head.removeChild(script); + } + }; + }, []); + // 初始化地图 useEffect(() => { - if (visible && mapContainerRef.current && window.qq && window.qq.maps) { - // 创建地图实例 - const center = new window.qq.maps.LatLng(39.908823, 116.39747); // 默认北京 - const mapInstance = new window.qq.maps.Map(mapContainerRef.current, { - center: center, - zoom: 13, - }); + if (visible && mapContainerRef.current && tmapLoaded && window.TMap) { + console.log("开始初始化地图"); - setMap(mapInstance); - - // 创建地理编码服务 - geocoderRef.current = new window.qq.maps.Geocoder({ - complete: (result: any) => { - setIsReverseGeocoding(false); - try { - if (result && result.detail) { - const detail = result.detail; - const location = detail.location || detail.latLng; - if (location) { - const lat = - location.lat || (location.getLat ? location.getLat() : null); - const lng = - location.lng || (location.getLng ? location.getLng() : null); - - if (lat && lng) { - // 构建地址标签 - let addressLabel = ""; - if (detail.formatted_addresses) { - addressLabel = - detail.formatted_addresses.recommend || - detail.formatted_addresses.rough || - ""; - } - if (!addressLabel && detail.address) { - addressLabel = detail.address; - } - if (!addressLabel && detail.addressComponents) { - const addr = detail.addressComponents; - const parts = []; - if (addr.province) parts.push(addr.province); - if (addr.city) parts.push(addr.city); - if (addr.district) parts.push(addr.district); - if (addr.street) parts.push(addr.street); - if (addr.street_number) parts.push(addr.street_number); - addressLabel = parts.join(""); - } - if (!addressLabel) { - addressLabel = `${lat.toFixed(6)}, ${lng.toFixed(6)}`; - } - - setSelectedLocation({ - x: lng.toString(), - y: lat.toString(), - scale: "16", - label: addressLabel, - poiname: - detail.addressComponents?.street || detail.poiid || "", - maptype: "0", - poiid: detail.poiid || "", - }); - } else { - message.warning("无法解析位置信息"); - } - } else { - message.warning("未找到位置信息"); - } - } else { - message.warning("获取地址信息失败:返回数据为空"); - } - } catch (error) { - console.error("解析地址信息错误:", error); - message.error("解析地址信息失败"); - } - }, - error: (error: any) => { - setIsReverseGeocoding(false); - console.error("反向地理编码错误:", error); - message.error("获取地址信息失败,请稍后重试"); - }, - }); - - // 创建搜索服务 - searchServiceRef.current = new window.qq.maps.SearchService({ - complete: (result: any) => { - setIsSearching(false); - if (result && result.detail) { - const pois = result.detail.pois || []; - if (pois.length > 0) { - const searchResults = pois.map((poi: any) => { - const location = poi.location || poi.latLng; - return { - id: - poi.id || - `${location.lat || location.getLat()},${ - location.lng || location.getLng() - }`, - title: poi.title || poi.name || "", - address: poi.address || poi.ad_info?.adcode || "", - location: { - lat: location.lat || location.getLat(), - lng: location.lng || location.getLng(), - }, - adcode: poi.adcode || poi.ad_info?.adcode || "", - city: poi.city || poi.ad_info?.city || "", - district: poi.district || poi.ad_info?.district || "", - }; - }); - setSearchResults(searchResults); - } else { - setSearchResults([]); - message.info("未找到相关地址"); - } - } else { - setSearchResults([]); - message.info("未找到相关地址"); - } - }, - error: () => { - setIsSearching(false); - message.error("搜索失败,请重试"); - }, - }); - - // 地图点击事件 - window.qq.maps.event.addListener(mapInstance, "click", (event: any) => { - const lat = event.latLng.lat; - const lng = event.latLng.lng; - - // 更新标记点 - if (markerRef.current) { - markerRef.current.setMap(null); - } - - const newMarker = new window.qq.maps.Marker({ - position: new window.qq.maps.LatLng(lat, lng), - map: mapInstance, + try { + // 创建地图实例 + const center = new window.TMap.LatLng(39.908823, 116.39747); // 默认北京 + const mapInstance = new window.TMap.Map(mapContainerRef.current, { + center: center, + zoom: 13, + rotation: 0, + pitch: 0, }); - markerRef.current = newMarker; + setMap(mapInstance); - // 反向地理编码 - if (geocoderRef.current) { - setIsReverseGeocoding(true); - geocoderRef.current.getAddress(new window.qq.maps.LatLng(lat, lng)); - } - }); + // 创建地理编码服务(用于反向地理编码) + geocoderRef.current = new window.TMap.service.Geocoder(); - // 获取用户当前位置 - if (navigator.geolocation) { - setIsLocating(true); - navigator.geolocation.getCurrentPosition( - position => { - setIsLocating(false); - const userLat = position.coords.latitude; - const userLng = position.coords.longitude; + // 使用腾讯地图内置的定位服务 + window.geolocationRef = window.TMap.service.Geolocation; - // 移动地图中心到用户位置 - const userLocation = new window.qq.maps.LatLng(userLat, userLng); - mapInstance.setCenter(userLocation); - mapInstance.setZoom(16); + // 创建搜索建议服务 + suggestServiceRef.current = new window.TMap.service.Suggestion({ + pageSize: 10, + autoExtend: true, + }); - // 添加标记点 + // 地图点击事件处理函数 + const handleMapClick = (evt: any) => { + try { + const lat = evt.latLng.getLat(); + const lng = evt.latLng.getLng(); + + console.log("地图点击:", lat, lng); + + // 更新标记点 if (markerRef.current) { markerRef.current.setMap(null); + markerRef.current = null; } - const newMarker = new window.qq.maps.Marker({ - position: userLocation, + // 创建新标记 + const newMarker = new window.TMap.MultiMarker({ + id: "marker-layer", map: mapInstance, + geometries: [ + { + id: "selected-marker", + styleId: "marker", + position: new window.TMap.LatLng(lat, lng), + properties: { + title: "选中位置", + }, + }, + ], }); markerRef.current = newMarker; - // 获取用户位置的地址信息 - if (geocoderRef.current) { - setIsReverseGeocoding(true); - geocoderRef.current.getAddress(userLocation); - } - }, - error => { + // 设置基本位置信息(防止白屏) + setSelectedLocation({ + x: lng.toString(), + y: lat.toString(), + scale: "16", + label: `${lat.toFixed(6)}, ${lng.toFixed(6)}`, + poiname: "选中位置", + maptype: "0", + poiid: "", + }); + + // 反向地理编码获取地址 + setIsReverseGeocoding(true); + geocoderRef.current + .getAddress({ location: new window.TMap.LatLng(lat, lng) }) + .then((result: any) => { + setIsReverseGeocoding(false); + console.log("反向地理编码结果:", result); + + try { + if (result && result.result) { + const resultData = result.result; + const address = resultData.address || ""; + const addressComponent = resultData.address_component || {}; + const formattedAddresses = + resultData.formatted_addresses || {}; + + // 构建地址标签 + let addressLabel = + formattedAddresses.recommend || + formattedAddresses.rough || + address; + + if (!addressLabel) { + const parts = []; + if (addressComponent.province) + parts.push(addressComponent.province); + if (addressComponent.city) + parts.push(addressComponent.city); + if (addressComponent.district) + parts.push(addressComponent.district); + if (addressComponent.street) + parts.push(addressComponent.street); + if (addressComponent.street_number) + parts.push(addressComponent.street_number); + addressLabel = parts.join(""); + } + + if (!addressLabel) { + addressLabel = `${lat.toFixed(6)}, ${lng.toFixed(6)}`; + } + + setSelectedLocation({ + x: lng.toString(), + y: lat.toString(), + scale: "16", + label: addressLabel, + poiname: addressComponent.street || "未知位置", + maptype: "0", + poiid: resultData.poi_id || "", + }); + } else { + message.warning("获取详细地址信息失败,将使用坐标显示"); + } + } catch (error) { + console.error("解析地址信息错误:", error); + message.warning("解析地址信息失败,将使用坐标显示"); + } + }) + .catch((error: any) => { + setIsReverseGeocoding(false); + console.error("反向地理编码错误:", error); + message.warning("获取详细地址信息失败,将使用坐标显示"); + }); + } catch (error) { + console.error("地图点击处理错误:", error); + message.error("处理地图点击时出错,请重试"); + } + }; + + // 绑定地图点击事件 + mapInstance.on("click", handleMapClick); + + // 使用腾讯地图API初始化用户位置 + const initializeUserLocation = ( + lat: number, + lng: number, + isDefault: boolean = false, + ) => { + console.log(isDefault ? "使用默认位置:" : "用户位置:", lat, lng); + + // 移动地图中心到位置 + const userLocation = new window.TMap.LatLng(lat, lng); + mapInstance.setCenter(userLocation); + mapInstance.setZoom(16); + + // 添加标记点 + if (markerRef.current) { + markerRef.current.setMap(null); + markerRef.current = null; + } + + const newMarker = new window.TMap.MultiMarker({ + id: "marker-layer", + map: mapInstance, + geometries: [ + { + id: "user-location", + styleId: "marker", + position: userLocation, + properties: { + title: isDefault ? "默认位置" : "当前位置", + }, + }, + ], + }); + + markerRef.current = newMarker; + + // 使用腾讯地图服务获取该位置的地址信息 + setIsReverseGeocoding(true); + geocoderRef.current + .getAddress({ location: userLocation }) + .then((result: any) => { + setIsReverseGeocoding(false); + if (result && result.result) { + const resultData = result.result; + const formattedAddresses = resultData.formatted_addresses || {}; + const addressComponent = resultData.address_component || {}; + + const addressLabel = + formattedAddresses.recommend || + formattedAddresses.rough || + resultData.address || + `${lat.toFixed(6)}, ${lng.toFixed(6)}`; + + setSelectedLocation({ + x: lng.toString(), + y: lat.toString(), + scale: "16", + label: addressLabel, + poiname: + addressComponent.street || + (isDefault ? "默认位置" : "当前位置"), + maptype: "0", + poiid: resultData.poi_id || "", + }); + } + }) + .catch((error: any) => { + setIsReverseGeocoding(false); + console.error("获取地址信息失败:", error); + // 即使获取地址失败,也设置基本的位置信息 + setSelectedLocation({ + x: lng.toString(), + y: lat.toString(), + scale: "16", + label: `${lat.toFixed(6)}, ${lng.toFixed(6)}`, + poiname: isDefault ? "默认位置" : "当前位置", + maptype: "0", + poiid: "", + }); + }); + }; + + // 使用腾讯地图IP定位获取用户位置 + setIsLocating(true); + try { + if (window.geolocationRef) { + window.geolocationRef.getLocation({ + timeout: 10000, + convert: true, + success: function (result: any) { + setIsLocating(false); + if (result && result.location) { + const { lat, lng } = result.location; + message.info("已定位到您的大致位置"); + initializeUserLocation(lat, lng, false); + } else { + // IP定位失败:使用默认位置 + message.info("无法获取您的位置,已定位到北京"); + // 使用默认位置(北京市) + initializeUserLocation(39.908823, 116.39747, true); + } + }, + error: function () { + setIsLocating(false); + message.info("无法获取您的位置,已定位到北京"); + // 使用默认位置(北京市) + initializeUserLocation(39.908823, 116.39747, true); + }, + }); + } else { + // 地理编码服务未初始化:使用默认位置 setIsLocating(false); - console.error("获取位置失败:", error); - // 如果获取位置失败,使用默认位置(北京) - message.info("无法获取您的位置,已定位到默认位置"); - }, - { - enableHighAccuracy: true, - timeout: 10000, - maximumAge: 0, - }, - ); - } else { - message.info("您的浏览器不支持地理定位功能"); + message.info("无法获取您的位置,已定位到北京"); + // 使用默认位置(北京市) + initializeUserLocation(39.908823, 116.39747, true); + } + } catch (error) { + // 捕获任何可能的错误,防止白屏 + console.error("定位过程中发生错误:", error); + setIsLocating(false); + message.error("定位服务出现异常,已定位到北京"); + // 使用默认位置(北京市) + initializeUserLocation(39.908823, 116.39747, true); + } + + return () => { + // 清理地图事件监听 + if (mapInstance) { + mapInstance.off("click", handleMapClick); + } + }; + } catch (error) { + console.error("初始化地图时出错:", error); + message.error("地图加载失败,请刷新页面重试"); + setIsLocating(false); + } + } + }, [visible, tmapLoaded]); + + // 搜索地址(获取搜索建议) + const handleSearch = () => { + try { + if (!searchValue.trim()) { + message.warning("请输入搜索关键词"); + return; } - return () => { - if (mapInstance) { - window.qq.maps.event.clearListeners(mapInstance, "click"); - } - }; - } - }, [visible]); + if (!suggestServiceRef.current) { + message.error("搜索服务未初始化,请刷新页面重试"); + return; + } - // 搜索地址 - const handleSearch = () => { - if (!searchValue.trim()) { - message.warning("请输入搜索关键词"); - return; - } + setIsSearching(true); + suggestServiceRef.current + .getSuggestions({ + keyword: searchValue, + location: map ? map.getCenter() : undefined, + }) + .then((result: any) => { + setIsSearching(false); + console.log("搜索建议结果:", result); - if (!searchServiceRef.current) { - message.error("搜索服务未初始化"); - return; + if (result && result.data && result.data.length > 0) { + const searchResults = result.data.map((item: any) => ({ + id: item.id, + title: item.title || item.name || "", + address: item.address || "", + location: { + lat: item.location.lat, + lng: item.location.lng, + }, + adcode: item.adcode || "", + city: item.city || "", + district: item.district || "", + })); + setSearchResults(searchResults); + } else { + setSearchResults([]); + message.info("未找到相关地址"); + } + }) + .catch((error: any) => { + setIsSearching(false); + console.error("搜索失败:", error); + message.error("搜索失败,请重试"); + // 确保搜索状态被重置 + setSearchResults([]); + }); + } catch (error) { + setIsSearching(false); + console.error("搜索处理错误:", error); + message.error("搜索过程中出错,请重试"); + setSearchResults([]); } - - setIsSearching(true); - searchServiceRef.current.search(searchValue); }; // 选择搜索结果 const handleSelectResult = (result: SearchResult) => { - if (!map) return; + try { + if (!map) { + message.error("地图未初始化,请刷新页面重试"); + return; + } - const lat = result.location.lat; - const lng = result.location.lng; + const lat = result.location.lat; + const lng = result.location.lng; - // 移动地图中心 - map.setCenter(new window.qq.maps.LatLng(lat, lng)); - map.setZoom(16); + console.log("选择搜索结果:", result); - // 更新标记点 - if (markerRef.current) { - markerRef.current.setMap(null); + // 移动地图中心 + map.setCenter(new window.TMap.LatLng(lat, lng)); + map.setZoom(16); + + // 更新标记点 + if (markerRef.current) { + markerRef.current.setMap(null); + markerRef.current = null; + } + + const newMarker = new window.TMap.MultiMarker({ + id: "marker-layer", + map: map, + geometries: [ + { + id: "selected-poi", + styleId: "marker", + position: new window.TMap.LatLng(lat, lng), + properties: { + title: result.title, + }, + }, + ], + }); + + markerRef.current = newMarker; + + // 设置选中的位置信息 + setSelectedLocation({ + x: lng.toString(), + y: lat.toString(), + scale: "16", + label: result.address || result.title, + poiname: result.title || "", + maptype: "0", + poiid: result.id || "", + }); + + // 清空搜索结果 + setSearchResults([]); + setSearchValue(""); + } catch (error) { + console.error("选择搜索结果错误:", error); + message.error("选择位置时出错,请重试"); } - - const newMarker = new window.qq.maps.Marker({ - position: new window.qq.maps.LatLng(lat, lng), - map: map, - }); - - markerRef.current = newMarker; - - // 设置选中的位置信息 - setSelectedLocation({ - x: lng.toString(), - y: lat.toString(), - scale: "16", - label: result.address || result.title, - poiname: result.title || "", - maptype: "0", - poiid: result.id || "", - }); - - // 清空搜索结果 - setSearchResults([]); - setSearchValue(""); }; // 确认选择 const handleConfirm = () => { - if (!selectedLocation) { - message.warning("请先选择位置"); - return; - } + try { + if (!selectedLocation) { + message.warning("请先选择位置"); + return; + } - // 生成XML格式的位置信息 - const locationXml = ` = ({ maptype="${selectedLocation.maptype}" poiid="${selectedLocation.poiid}" />`; - // 如果有onConfirm回调,调用它 - if (onConfirm) { - onConfirm(locationXml); + // 如果有onConfirm回调,调用它 + if (onConfirm) { + onConfirm(locationXml); + } + + // 如果有addMessage和contract,发送位置消息 + if (addMessage && contract) { + const messageId = +Date.now(); + const localMessage = { + id: messageId, + wechatAccountId: contract.wechatAccountId, + wechatFriendId: contract?.chatroomId ? 0 : contract.id, + wechatChatroomId: contract?.chatroomId ? contract.id : 0, + tenantId: 0, + accountId: 0, + synergyAccountId: 0, + content: locationXml, + msgType: 48, // 位置消息类型 + msgSubType: 0, + msgSvrId: "", + isSend: true, + createTime: new Date().toISOString(), + isDeleted: false, + deleteTime: "", + sendStatus: 1, + wechatTime: Date.now(), + origin: 0, + msgId: 0, + recalled: false, + seq: messageId, + }; + + addMessage(localMessage); + + // 发送消息到服务器 + sendCommand("CmdSendMessage", { + wechatAccountId: contract.wechatAccountId, + wechatChatroomId: contract?.chatroomId ? contract.id : 0, + wechatFriendId: contract?.chatroomId ? 0 : contract.id, + msgSubType: 0, + msgType: 48, + content: locationXml, + seq: messageId, + }); + } + + // 关闭弹窗并重置状态 + handleClose(); + } catch (error) { + console.error("确认位置时出错:", error); + message.error("发送位置信息时出错,请重试"); } - - // 如果有addMessage和contract,发送位置消息 - if (addMessage && contract) { - const messageId = +Date.now(); - const localMessage = { - id: messageId, - wechatAccountId: contract.wechatAccountId, - wechatFriendId: contract?.chatroomId ? 0 : contract.id, - wechatChatroomId: contract?.chatroomId ? contract.id : 0, - tenantId: 0, - accountId: 0, - synergyAccountId: 0, - content: locationXml, - msgType: 48, // 位置消息类型 - msgSubType: 0, - msgSvrId: "", - isSend: true, - createTime: new Date().toISOString(), - isDeleted: false, - deleteTime: "", - sendStatus: 1, - wechatTime: Date.now(), - origin: 0, - msgId: 0, - recalled: false, - seq: messageId, - }; - - addMessage(localMessage); - - // 发送消息到服务器 - sendCommand("CmdSendMessage", { - wechatAccountId: contract.wechatAccountId, - wechatChatroomId: contract?.chatroomId ? contract.id : 0, - wechatFriendId: contract?.chatroomId ? 0 : contract.id, - msgSubType: 0, - msgType: 48, - content: locationXml, - seq: messageId, - }); - } - - // 关闭弹窗并重置状态 - handleClose(); }; // 关闭弹窗 diff --git a/Touchkebao/腾讯地图定位服务修复说明.md b/Touchkebao/腾讯地图定位服务修复说明.md new file mode 100644 index 00000000..601db72f --- /dev/null +++ b/Touchkebao/腾讯地图定位服务修复说明.md @@ -0,0 +1,73 @@ +# 腾讯地图定位服务修复说明 + +## 问题描述 + +在 `selectMap.tsx` 文件中使用腾讯地图定位服务时出现以下错误: + +``` +TypeError: window.TMap.service.Location is not a constructor + at selectMap.tsx:121:33 +``` + +## 原因分析 + +错误原因是尝试将 `TMap.service.Location` 作为构造函数使用,但在腾讯地图 GL API 中,定位服务不是通过构造函数方式创建的。 + +## 修复方法 + +### 1. 修改定位服务的初始化方式 + +将原来的代码: + +```typescript +// 创建IP定位服务 +window.geolocationRef = new window.TMap.service.Location({ + timeout: 10000, + convert: true, +}); +``` + +修改为: + +```typescript +// 使用腾讯地图内置的定位服务 +window.geolocationRef = window.TMap.service.Geolocation; +``` + +### 2. 修改定位服务的调用方式 + +在调用定位服务时,将配置参数直接传入 `getLocation` 方法: + +```typescript +window.geolocationRef.getLocation({ + timeout: 10000, + convert: true, + success: function (result: any) { + // 处理成功回调 + }, + error: function () { + // 处理错误回调 + }, +}); +``` + +## 技术说明 + +1. **腾讯地图 GL API 中的定位服务**: + - 正确的服务名称是 `TMap.service.Geolocation`,而非 `TMap.service.Location` + - 它是一个对象,不需要使用 `new` 关键字实例化 + - 配置参数应该直接传递给 `getLocation` 方法 + +2. **定位服务参数**: + - `timeout`:定位超时时间,单位毫秒 + - `convert`:是否将坐标转换为腾讯地图坐标系 + +3. **回调处理**: + - `success`:定位成功回调函数,返回位置信息 + - `error`:定位失败回调函数 + +## 注意事项 + +1. 确保腾讯地图 SDK 已正确加载 +2. 确保 API 密钥有定位服务的权限 +3. 定位精度可能受网络环境影响 From ab2ee050def8d64474a52d351f7b601970c0e570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Sat, 22 Nov 2025 11:53:54 +0800 Subject: [PATCH 24/31] =?UTF-8?q?=E4=BC=98=E5=8C=96SelectMap=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=EF=BC=8C=E6=9B=B4=E6=96=B0IP=E5=AE=9A=E4=BD=8D?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E7=9A=84=E5=AE=9E=E7=8E=B0=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E7=A1=AE=E4=BF=9D=E7=94=A8=E6=88=B7=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E6=9B=B4=E5=8A=A0=E5=87=86=E7=A1=AE=E5=92=8C?= =?UTF-8?q?=E7=A8=B3=E5=AE=9A=EF=BC=8C=E5=90=8C=E6=97=B6=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E4=BB=A5=E6=8F=90=E5=8D=87=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BD=93=E9=AA=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/selectMap.module.scss | 6 +++- .../MessageEnter/components/selectMap.tsx | 28 +++++++++---------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.module.scss b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.module.scss index f0a77bb3..b8f65724 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.module.scss +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.module.scss @@ -8,10 +8,13 @@ .searchArea { flex-shrink: 0; position: relative; + z-index: 10000; } .searchInput { width: 100%; + position: relative; + z-index: 10000; } .searchResults { @@ -19,7 +22,7 @@ top: 100%; left: 0; right: 0; - z-index: 1000; + z-index: 10001; background: #fff; border: 1px solid #e8e8e8; border-radius: 4px; @@ -27,6 +30,7 @@ margin-top: 4px; max-height: 300px; overflow-y: auto; + pointer-events: auto; :global(.ant-list-item) { cursor: pointer; diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx index d09316ce..7df7fdfc 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx @@ -8,7 +8,7 @@ import styles from "./selectMap.module.scss"; declare global { interface Window { TMap: any; - geolocationRef: any; // 全局IP定位服务引用 + geolocationRef: any; // 全局IP定位服务引用(TMap.service.IPLocation实例) } } @@ -117,8 +117,8 @@ const SelectMap: React.FC = ({ // 创建地理编码服务(用于反向地理编码) geocoderRef.current = new window.TMap.service.Geocoder(); - // 使用腾讯地图内置的定位服务 - window.geolocationRef = window.TMap.service.Geolocation; + // 创建IP定位服务 + window.geolocationRef = new window.TMap.service.IPLocation(); // 创建搜索建议服务 suggestServiceRef.current = new window.TMap.service.Suggestion({ @@ -327,13 +327,13 @@ const SelectMap: React.FC = ({ setIsLocating(true); try { if (window.geolocationRef) { - window.geolocationRef.getLocation({ - timeout: 10000, - convert: true, - success: function (result: any) { + window.geolocationRef + .locate() + .then((result: any) => { setIsLocating(false); - if (result && result.location) { - const { lat, lng } = result.location; + console.log("IP定位结果:", result); + if (result && result.result && result.result.location) { + const { lat, lng } = result.result.location; message.info("已定位到您的大致位置"); initializeUserLocation(lat, lng, false); } else { @@ -342,16 +342,16 @@ const SelectMap: React.FC = ({ // 使用默认位置(北京市) initializeUserLocation(39.908823, 116.39747, true); } - }, - error: function () { + }) + .catch((error: any) => { setIsLocating(false); + console.error("IP定位失败:", error); message.info("无法获取您的位置,已定位到北京"); // 使用默认位置(北京市) initializeUserLocation(39.908823, 116.39747, true); - }, - }); + }); } else { - // 地理编码服务未初始化:使用默认位置 + // IP定位服务未初始化:使用默认位置 setIsLocating(false); message.info("无法获取您的位置,已定位到北京"); // 使用默认位置(北京市) From 43b60a0049696bb9b3ed3ddd25d74d561be273f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Sat, 22 Nov 2025 14:16:40 +0800 Subject: [PATCH 25/31] =?UTF-8?q?=E4=BC=98=E5=8C=96SelectMap=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=EF=BC=8C=E5=A2=9E=E5=BC=BATMap=20API=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E5=92=8C=E5=88=9D=E5=A7=8B=E5=8C=96=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0API=E5=8F=AF=E7=94=A8=E6=80=A7?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=EF=BC=8C=E6=94=B9=E8=BF=9B=E6=A0=87=E8=AE=B0?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E5=88=9B=E5=BB=BA=E5=92=8C=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86=EF=BC=8C=E6=8F=90=E5=8D=87=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BD=93=E9=AA=8C=E5=92=8C=E7=A8=B3=E5=AE=9A=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MessageEnter/components/selectMap.tsx | 793 ++++++++++++------ 1 file changed, 547 insertions(+), 246 deletions(-) diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx index 7df7fdfc..71de2273 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx @@ -70,7 +70,17 @@ const SelectMap: React.FC = ({ useEffect(() => { // 检查TMap是否已经加载 if (window.TMap) { - setTmapLoaded(true); + // 等待 API 完全初始化 + const checkAPIReady = () => { + if (window.TMap && window.TMap.Map) { + console.log("腾讯地图SDK已加载,API 可用"); + setTmapLoaded(true); + } else { + // 如果 API 还未完全初始化,等待一段时间后重试 + setTimeout(checkAPIReady, 100); + } + }; + checkAPIReady(); return; } @@ -80,8 +90,19 @@ const SelectMap: React.FC = ({ "https://map.qq.com/api/gljs?v=1.exp&libraries=service&key=7DZBZ-ZSRK3-QJN3W-O5VTV-4E2P6-7GFYX"; script.async = true; script.onload = () => { - console.log("腾讯地图SDK加载成功"); - setTmapLoaded(true); + console.log("腾讯地图SDK脚本加载成功,等待 API 初始化..."); + // 等待 API 完全初始化 + const checkAPIReady = () => { + if (window.TMap && window.TMap.Map) { + console.log("腾讯地图SDK API 初始化完成"); + setTmapLoaded(true); + } else { + // 如果 API 还未完全初始化,等待一段时间后重试(最多等待 5 秒) + setTimeout(checkAPIReady, 100); + } + }; + // 延迟检查,给 API 一些初始化时间 + setTimeout(checkAPIReady, 200); }; script.onerror = () => { console.error("腾讯地图SDK加载失败"); @@ -97,220 +118,359 @@ const SelectMap: React.FC = ({ }; }, []); + // 检查 TMap API 是否可用(辅助函数) + const checkTMapAPI = () => { + if (!window.TMap) { + console.error("TMap 未加载"); + return false; + } + + // 检查 MultiMarker 是否可用 + if (!window.TMap.MultiMarker) { + console.error("TMap.MultiMarker 不可用", { + TMap: window.TMap, + keys: Object.keys(window.TMap || {}), + }); + return false; + } + + // 检查 Style 是否存在(可能是构造函数、对象或命名空间) + // 注意:Style 可能不是构造函数,而是配置对象或命名空间 + const hasStyle = + window.TMap.MultiMarker.Style !== undefined || + window.TMap.MarkerStyle !== undefined; + + if (!hasStyle) { + console.warn("TMap Style API 不可用,将使用配置对象方式", { + MultiMarker: window.TMap.MultiMarker, + MultiMarkerKeys: Object.keys(window.TMap.MultiMarker || {}), + MarkerStyle: window.TMap.MarkerStyle, + }); + // 不返回 false,因为 MultiMarker 可能接受配置对象 + } + + return true; + }; + + // 创建标记样式(兼容不同的 API 版本) + const createMarkerStyle = (options: any) => { + // 检查 MultiMarker.Style 是否存在 + if (window.TMap.MultiMarker?.Style) { + // 如果 Style 是函数(构造函数),使用 new + if (typeof window.TMap.MultiMarker.Style === "function") { + try { + return new window.TMap.MultiMarker.Style(options); + } catch (error) { + console.warn( + "使用 new MultiMarker.Style 失败,尝试直接返回配置对象:", + error, + ); + // 如果构造函数调用失败,直接返回配置对象 + return options; + } + } else { + // 如果 Style 不是函数,可能是对象或命名空间,直接返回配置对象 + // MultiMarker 可能接受配置对象而不是 Style 实例 + console.log("MultiMarker.Style 不是构造函数,直接使用配置对象"); + return options; + } + } + // 尝试 MarkerStyle + if (window.TMap.MarkerStyle) { + if (typeof window.TMap.MarkerStyle === "function") { + try { + return new window.TMap.MarkerStyle(options); + } catch (error) { + console.warn( + "使用 new MarkerStyle 失败,尝试直接返回配置对象:", + error, + ); + return options; + } + } else { + return options; + } + } + // 如果都不存在,直接返回配置对象(让 MultiMarker 自己处理) + console.warn("未找到 Style API,直接使用配置对象"); + return options; + }; + // 初始化地图 useEffect(() => { if (visible && mapContainerRef.current && tmapLoaded && window.TMap) { console.log("开始初始化地图"); + console.log("TMap API 检查:", { + TMap: !!window.TMap, + MultiMarker: !!window.TMap.MultiMarker, + MultiMarkerStyle: !!window.TMap.MultiMarker?.Style, + MarkerStyle: !!window.TMap.MarkerStyle, + }); - try { - // 创建地图实例 - const center = new window.TMap.LatLng(39.908823, 116.39747); // 默认北京 - const mapInstance = new window.TMap.Map(mapContainerRef.current, { - center: center, - zoom: 13, - rotation: 0, - pitch: 0, - }); + // 检查容器尺寸,确保容器有有效的宽高 + const checkContainerSize = () => { + if (!mapContainerRef.current) return false; + const rect = mapContainerRef.current.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + }; - setMap(mapInstance); + let mapInstance: any = null; + let handleMapClickFn: ((evt: any) => void) | null = null; + let delayTimer: NodeJS.Timeout | null = null; + let isMounted = true; // 标记弹窗是否仍然打开 - // 创建地理编码服务(用于反向地理编码) - geocoderRef.current = new window.TMap.service.Geocoder(); + // 初始化地图函数(使用箭头函数避免函数声明位置问题) + const initializeMap = () => { + if (!mapContainerRef.current) return; - // 创建IP定位服务 - window.geolocationRef = new window.TMap.service.IPLocation(); - - // 创建搜索建议服务 - suggestServiceRef.current = new window.TMap.service.Suggestion({ - pageSize: 10, - autoExtend: true, - }); - - // 地图点击事件处理函数 - const handleMapClick = (evt: any) => { - try { - const lat = evt.latLng.getLat(); - const lng = evt.latLng.getLng(); - - console.log("地图点击:", lat, lng); - - // 更新标记点 - if (markerRef.current) { - markerRef.current.setMap(null); - markerRef.current = null; - } - - // 创建新标记 - const newMarker = new window.TMap.MultiMarker({ - id: "marker-layer", - map: mapInstance, - geometries: [ - { - id: "selected-marker", - styleId: "marker", - position: new window.TMap.LatLng(lat, lng), - properties: { - title: "选中位置", - }, - }, - ], - }); - - markerRef.current = newMarker; - - // 设置基本位置信息(防止白屏) - setSelectedLocation({ - x: lng.toString(), - y: lat.toString(), - scale: "16", - label: `${lat.toFixed(6)}, ${lng.toFixed(6)}`, - poiname: "选中位置", - maptype: "0", - poiid: "", - }); - - // 反向地理编码获取地址 - setIsReverseGeocoding(true); - geocoderRef.current - .getAddress({ location: new window.TMap.LatLng(lat, lng) }) - .then((result: any) => { - setIsReverseGeocoding(false); - console.log("反向地理编码结果:", result); - - try { - if (result && result.result) { - const resultData = result.result; - const address = resultData.address || ""; - const addressComponent = resultData.address_component || {}; - const formattedAddresses = - resultData.formatted_addresses || {}; - - // 构建地址标签 - let addressLabel = - formattedAddresses.recommend || - formattedAddresses.rough || - address; - - if (!addressLabel) { - const parts = []; - if (addressComponent.province) - parts.push(addressComponent.province); - if (addressComponent.city) - parts.push(addressComponent.city); - if (addressComponent.district) - parts.push(addressComponent.district); - if (addressComponent.street) - parts.push(addressComponent.street); - if (addressComponent.street_number) - parts.push(addressComponent.street_number); - addressLabel = parts.join(""); - } - - if (!addressLabel) { - addressLabel = `${lat.toFixed(6)}, ${lng.toFixed(6)}`; - } - - setSelectedLocation({ - x: lng.toString(), - y: lat.toString(), - scale: "16", - label: addressLabel, - poiname: addressComponent.street || "未知位置", - maptype: "0", - poiid: resultData.poi_id || "", - }); - } else { - message.warning("获取详细地址信息失败,将使用坐标显示"); - } - } catch (error) { - console.error("解析地址信息错误:", error); - message.warning("解析地址信息失败,将使用坐标显示"); - } - }) - .catch((error: any) => { - setIsReverseGeocoding(false); - console.error("反向地理编码错误:", error); - message.warning("获取详细地址信息失败,将使用坐标显示"); - }); - } catch (error) { - console.error("地图点击处理错误:", error); - message.error("处理地图点击时出错,请重试"); - } - }; - - // 绑定地图点击事件 - mapInstance.on("click", handleMapClick); - - // 使用腾讯地图API初始化用户位置 - const initializeUserLocation = ( - lat: number, - lng: number, - isDefault: boolean = false, - ) => { - console.log(isDefault ? "使用默认位置:" : "用户位置:", lat, lng); - - // 移动地图中心到位置 - const userLocation = new window.TMap.LatLng(lat, lng); - mapInstance.setCenter(userLocation); - mapInstance.setZoom(16); - - // 添加标记点 - if (markerRef.current) { - markerRef.current.setMap(null); - markerRef.current = null; + try { + // 再次检查容器尺寸 + const rect = mapContainerRef.current.getBoundingClientRect(); + if (rect.width <= 0 || rect.height <= 0) { + console.error("地图容器尺寸无效:", rect); + message.error("地图容器尺寸无效,请刷新页面重试"); + return; } - const newMarker = new window.TMap.MultiMarker({ - id: "marker-layer", - map: mapInstance, - geometries: [ - { - id: "user-location", - styleId: "marker", - position: userLocation, - properties: { - title: isDefault ? "默认位置" : "当前位置", - }, - }, - ], + // 创建地图实例 + const center = new window.TMap.LatLng(39.908823, 116.39747); // 默认北京 + mapInstance = new window.TMap.Map(mapContainerRef.current, { + center: center, + zoom: 13, + rotation: 0, + pitch: 0, }); - markerRef.current = newMarker; + setMap(mapInstance); - // 使用腾讯地图服务获取该位置的地址信息 - setIsReverseGeocoding(true); - geocoderRef.current - .getAddress({ location: userLocation }) - .then((result: any) => { - setIsReverseGeocoding(false); - if (result && result.result) { - const resultData = result.result; - const formattedAddresses = resultData.formatted_addresses || {}; - const addressComponent = resultData.address_component || {}; + // 创建地理编码服务(用于反向地理编码) + geocoderRef.current = new window.TMap.service.Geocoder(); - const addressLabel = - formattedAddresses.recommend || - formattedAddresses.rough || - resultData.address || - `${lat.toFixed(6)}, ${lng.toFixed(6)}`; + // 创建IP定位服务 + window.geolocationRef = new window.TMap.service.IPLocation(); - setSelectedLocation({ - x: lng.toString(), - y: lat.toString(), - scale: "16", - label: addressLabel, - poiname: - addressComponent.street || - (isDefault ? "默认位置" : "当前位置"), - maptype: "0", - poiid: resultData.poi_id || "", - }); + // 创建搜索建议服务 + suggestServiceRef.current = new window.TMap.service.Suggestion({ + pageSize: 10, + autoExtend: true, + }); + + // 地图点击事件处理函数 + handleMapClickFn = (evt: any) => { + try { + // 检查弹窗是否仍然打开,以及必要的API是否可用 + if (!isMounted || !mapInstance || !mapContainerRef.current) { + return; } - }) - .catch((error: any) => { - setIsReverseGeocoding(false); - console.error("获取地址信息失败:", error); - // 即使获取地址失败,也设置基本的位置信息 + + // 检查 TMap API 是否可用 + if (!checkTMapAPI()) { + console.error("TMap API 不可用,无法创建标记点"); + message.warning("地图标记功能不可用,请刷新页面重试"); + return; + } + + const lat = evt.latLng.getLat(); + const lng = evt.latLng.getLng(); + + console.log("地图点击:", lat, lng); + + // 更新标记点 + if (markerRef.current) { + markerRef.current.setMap(null); + markerRef.current = null; + } + + // 创建标记样式 + const markerStyle = createMarkerStyle({ + width: 25, + height: 35, + anchor: { x: 12, y: 35 }, + src: "https://mapapi.qq.com/web/lbs/javascriptGL/demo/img/markerDefault.png", + }); + + // 创建新标记 + const newMarker = new window.TMap.MultiMarker({ + id: "marker-layer", + map: mapInstance, + styles: { + marker: markerStyle, + }, + geometries: [ + { + id: "selected-marker", + styleId: "marker", + position: new window.TMap.LatLng(lat, lng), + properties: { + title: "选中位置", + }, + }, + ], + }); + + markerRef.current = newMarker; + + // 设置基本位置信息(防止白屏) + setSelectedLocation({ + x: lng.toString(), + y: lat.toString(), + scale: "16", + label: `${lat.toFixed(6)}, ${lng.toFixed(6)}`, + poiname: "选中位置", + maptype: "0", + poiid: "", + }); + + // 反向地理编码获取地址 + if (!isMounted || !geocoderRef.current) { + return; + } + + setIsReverseGeocoding(true); + geocoderRef.current + .getAddress({ location: new window.TMap.LatLng(lat, lng) }) + .then((result: any) => { + // 检查弹窗是否仍然打开 + if (!isMounted) { + return; + } + setIsReverseGeocoding(false); + console.log("反向地理编码结果:", result); + + try { + if (result && result.result) { + const resultData = result.result; + const address = resultData.address || ""; + const addressComponent = + resultData.address_component || {}; + const formattedAddresses = + resultData.formatted_addresses || {}; + + // 构建地址标签 + let addressLabel = + formattedAddresses.recommend || + formattedAddresses.rough || + address; + + if (!addressLabel) { + const parts = []; + if (addressComponent.province) + parts.push(addressComponent.province); + if (addressComponent.city) + parts.push(addressComponent.city); + if (addressComponent.district) + parts.push(addressComponent.district); + if (addressComponent.street) + parts.push(addressComponent.street); + if (addressComponent.street_number) + parts.push(addressComponent.street_number); + addressLabel = parts.join(""); + } + + if (!addressLabel) { + addressLabel = `${lat.toFixed(6)}, ${lng.toFixed(6)}`; + } + + setSelectedLocation({ + x: lng.toString(), + y: lat.toString(), + scale: "16", + label: addressLabel, + poiname: addressComponent.street || "未知位置", + maptype: "0", + poiid: resultData.poi_id || "", + }); + } else { + message.warning("获取详细地址信息失败,将使用坐标显示"); + } + } catch (error) { + console.error("解析地址信息错误:", error); + message.warning("解析地址信息失败,将使用坐标显示"); + } + }) + .catch((error: any) => { + // 检查弹窗是否仍然打开 + if (!isMounted) { + return; + } + setIsReverseGeocoding(false); + console.error("反向地理编码错误:", error); + message.warning("获取详细地址信息失败,将使用坐标显示"); + }); + } catch (error) { + console.error("地图点击处理错误:", error); + message.error("处理地图点击时出错,请重试"); + } + }; + + // 绑定地图点击事件 + mapInstance.on("click", handleMapClickFn); + + // 使用腾讯地图API初始化用户位置 + const initializeUserLocation = ( + lat: number, + lng: number, + isDefault: boolean = false, + ) => { + // 检查弹窗是否仍然打开,以及必要的API是否可用 + if (!isMounted || !mapInstance || !mapContainerRef.current) { + console.log("弹窗已关闭或地图实例无效,跳过初始化位置"); + return; + } + + // 检查 TMap API 是否可用 + if (!checkTMapAPI()) { + console.error("TMap API 不可用,无法创建标记点"); + message.warning("地图标记功能不可用,请刷新页面重试"); + return; + } + + // 创建位置对象 + let userLocation: any = null; + try { + console.log(isDefault ? "使用默认位置:" : "用户位置:", lat, lng); + + // 移动地图中心到位置 + userLocation = new window.TMap.LatLng(lat, lng); + mapInstance.setCenter(userLocation); + mapInstance.setZoom(16); + + // 添加标记点 + if (markerRef.current) { + markerRef.current.setMap(null); + markerRef.current = null; + } + + // 创建标记样式 + const markerStyle = createMarkerStyle({ + width: 25, + height: 35, + anchor: { x: 12, y: 35 }, + src: "https://mapapi.qq.com/web/lbs/javascriptGL/demo/img/markerDefault.png", + }); + + const newMarker = new window.TMap.MultiMarker({ + id: "marker-layer", + map: mapInstance, + styles: { + marker: markerStyle, + }, + geometries: [ + { + id: "user-location", + styleId: "marker", + position: userLocation, + properties: { + title: isDefault ? "默认位置" : "当前位置", + }, + }, + ], + }); + + markerRef.current = newMarker; + } catch (error) { + console.error("创建标记点失败:", error); + // 即使创建标记失败,也设置基本的位置信息 setSelectedLocation({ x: lng.toString(), y: lat.toString(), @@ -320,63 +480,186 @@ const SelectMap: React.FC = ({ maptype: "0", poiid: "", }); - }); - }; + return; + } - // 使用腾讯地图IP定位获取用户位置 - setIsLocating(true); - try { - if (window.geolocationRef) { - window.geolocationRef - .locate() + // 使用腾讯地图服务获取该位置的地址信息 + if (!isMounted || !geocoderRef.current || !userLocation) { + return; + } + + setIsReverseGeocoding(true); + geocoderRef.current + .getAddress({ location: userLocation }) .then((result: any) => { - setIsLocating(false); - console.log("IP定位结果:", result); - if (result && result.result && result.result.location) { - const { lat, lng } = result.result.location; - message.info("已定位到您的大致位置"); - initializeUserLocation(lat, lng, false); - } else { - // IP定位失败:使用默认位置 - message.info("无法获取您的位置,已定位到北京"); - // 使用默认位置(北京市) - initializeUserLocation(39.908823, 116.39747, true); + // 检查弹窗是否仍然打开 + if (!isMounted) { + return; + } + setIsReverseGeocoding(false); + if (result && result.result) { + const resultData = result.result; + const formattedAddresses = + resultData.formatted_addresses || {}; + const addressComponent = resultData.address_component || {}; + + const addressLabel = + formattedAddresses.recommend || + formattedAddresses.rough || + resultData.address || + `${lat.toFixed(6)}, ${lng.toFixed(6)}`; + + setSelectedLocation({ + x: lng.toString(), + y: lat.toString(), + scale: "16", + label: addressLabel, + poiname: + addressComponent.street || + (isDefault ? "默认位置" : "当前位置"), + maptype: "0", + poiid: resultData.poi_id || "", + }); } }) .catch((error: any) => { - setIsLocating(false); - console.error("IP定位失败:", error); - message.info("无法获取您的位置,已定位到北京"); - // 使用默认位置(北京市) - initializeUserLocation(39.908823, 116.39747, true); + // 检查弹窗是否仍然打开 + if (!isMounted) { + return; + } + setIsReverseGeocoding(false); + console.error("获取地址信息失败:", error); + // 即使获取地址失败,也设置基本的位置信息 + setSelectedLocation({ + x: lng.toString(), + y: lat.toString(), + scale: "16", + label: `${lat.toFixed(6)}, ${lng.toFixed(6)}`, + poiname: isDefault ? "默认位置" : "当前位置", + maptype: "0", + poiid: "", + }); }); - } else { - // IP定位服务未初始化:使用默认位置 - setIsLocating(false); - message.info("无法获取您的位置,已定位到北京"); - // 使用默认位置(北京市) - initializeUserLocation(39.908823, 116.39747, true); + }; + + // 使用腾讯地图IP定位获取用户位置 + setIsLocating(true); + try { + if (window.geolocationRef) { + window.geolocationRef + .locate() + .then((result: any) => { + // 检查弹窗是否仍然打开 + if (!isMounted) { + return; + } + setIsLocating(false); + console.log("IP定位结果:", result); + if (result && result.result && result.result.location) { + const { lat, lng } = result.result.location; + // message.info("已定位到您的大致位置"); + initializeUserLocation(lat, lng, false); + } else { + // IP定位失败:使用默认位置 + message.info("无法获取您的位置,已定位到北京"); + // 使用默认位置(北京市) + initializeUserLocation(39.908823, 116.39747, true); + } + }) + .catch((error: any) => { + // 检查弹窗是否仍然打开 + if (!isMounted) { + return; + } + setIsLocating(false); + console.error("IP定位失败:", error); + message.info("无法获取您的位置,已定位到北京"); + // 使用默认位置(北京市) + initializeUserLocation(39.908823, 116.39747, true); + }); + } else { + // IP定位服务未初始化:使用默认位置 + setIsLocating(false); + message.info("无法获取您的位置,已定位到北京"); + // 使用默认位置(北京市) + initializeUserLocation(39.908823, 116.39747, true); + } + } catch (error) { + // 捕获任何可能的错误,防止白屏 + console.error("定位过程中发生错误:", error); + if (isMounted) { + setIsLocating(false); + message.error("定位服务出现异常,已定位到北京"); + // 使用默认位置(北京市) + initializeUserLocation(39.908823, 116.39747, true); + } } } catch (error) { - // 捕获任何可能的错误,防止白屏 - console.error("定位过程中发生错误:", error); + console.error("初始化地图时出错:", error); + message.error("地图加载失败,请刷新页面重试"); setIsLocating(false); - message.error("定位服务出现异常,已定位到北京"); - // 使用默认位置(北京市) - initializeUserLocation(39.908823, 116.39747, true); + } + }; + + // 使用 requestAnimationFrame 确保容器尺寸正确后再初始化 + const initTimer = requestAnimationFrame(() => { + // 再次检查容器尺寸 + if (!checkContainerSize()) { + console.log("容器尺寸无效,延迟初始化地图"); + delayTimer = setTimeout(() => { + if (checkContainerSize() && mapContainerRef.current) { + initializeMap(); + } else { + console.error("地图容器尺寸仍然无效"); + message.error("地图容器初始化失败,请刷新页面重试"); + } + }, 100); + return; } - return () => { - // 清理地图事件监听 - if (mapInstance) { - mapInstance.off("click", handleMapClick); + // 容器尺寸有效,立即初始化 + initializeMap(); + }); + + // 清理函数 + return () => { + // 标记弹窗已关闭 + isMounted = false; + // 取消 requestAnimationFrame + cancelAnimationFrame(initTimer); + // 清理延迟定时器 + if (delayTimer) { + clearTimeout(delayTimer); + } + // 清理地图事件监听 + if (mapInstance && handleMapClickFn) { + try { + mapInstance.off("click", handleMapClickFn); + } catch (error) { + console.error("清理地图事件监听失败:", error); } - }; - } catch (error) { - console.error("初始化地图时出错:", error); - message.error("地图加载失败,请刷新页面重试"); - setIsLocating(false); - } + } + // 清理地图实例 + if (mapInstance) { + try { + mapInstance.destroy(); + } catch (error) { + console.error("销毁地图实例失败:", error); + } + mapInstance = null; + } + // 清理标记点 + if (markerRef.current) { + try { + markerRef.current.setMap(null); + } catch (error) { + console.error("清理标记点失败:", error); + } + markerRef.current = null; + } + // 重置地图状态 + setMap(null); + }; } }, [visible, tmapLoaded]); @@ -445,6 +728,13 @@ const SelectMap: React.FC = ({ return; } + // 检查 TMap API 是否可用 + if (!checkTMapAPI()) { + console.error("TMap API 不可用,无法创建标记点"); + message.error("地图API不可用,请刷新页面重试"); + return; + } + const lat = result.location.lat; const lng = result.location.lng; @@ -460,9 +750,20 @@ const SelectMap: React.FC = ({ markerRef.current = null; } + // 创建标记样式 + const markerStyle = createMarkerStyle({ + width: 25, + height: 35, + anchor: { x: 12, y: 35 }, + src: "https://mapapi.qq.com/web/lbs/javascriptGL/demo/img/markerDefault.png", + }); + const newMarker = new window.TMap.MultiMarker({ id: "marker-layer", map: map, + styles: { + marker: markerStyle, + }, geometries: [ { id: "selected-poi", From deac306f699eda2bd5e008cfa1c2e1eac81f446b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Sat, 22 Nov 2025 15:57:16 +0800 Subject: [PATCH 26/31] =?UTF-8?q?=E4=BF=AE=E6=AD=A3SelectMap=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E4=B8=AD=E7=9A=84=E7=BB=8F=E7=BA=AC=E5=BA=A6=E6=A0=87?= =?UTF-8?q?=E8=AE=B0=E9=A1=BA=E5=BA=8F=EF=BC=8C=E6=9B=B4=E6=96=B0XML?= =?UTF-8?q?=E7=94=9F=E6=88=90=E9=80=BB=E8=BE=91=E4=BB=A5=E7=A1=AE=E4=BF=9D?= =?UTF-8?q?=E7=89=B9=E6=AE=8A=E5=AD=97=E7=AC=A6=E8=BD=AC=E4=B9=89=EF=BC=8C?= =?UTF-8?q?=E6=8F=90=E5=8D=87=E5=9C=B0=E5=9B=BE=E4=BD=8D=E7=BD=AE=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E7=9A=84=E5=87=86=E7=A1=AE=E6=80=A7=E5=92=8C=E7=A8=B3?= =?UTF-8?q?=E5=AE=9A=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MessageEnter/components/selectMap.tsx | 93 +++++++++++++------ 1 file changed, 63 insertions(+), 30 deletions(-) diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx index 71de2273..093b3067 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageEnter/components/selectMap.tsx @@ -34,8 +34,8 @@ interface SearchResult { } interface LocationData { - x: string; // 经度 - y: string; // 纬度 + x: string; // 纬度 + y: string; // 经度 scale: string; // 缩放级别 label: string; // 地址标签 poiname: string; // POI名称 @@ -66,6 +66,17 @@ const SelectMap: React.FC = ({ const markerRef = useRef(null); const { sendCommand } = useWebSocketStore.getState(); + // XML转义函数,防止特殊字符破坏XML格式 + const escapeXml = (str: string | undefined | null): string => { + if (!str) return ""; + return String(str) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + }; + // 加载腾讯地图SDK useEffect(() => { // 检查TMap是否已经加载 @@ -311,11 +322,12 @@ const SelectMap: React.FC = ({ markerRef.current = newMarker; // 设置基本位置信息(防止白屏) + // 经纬度格式化为6位小数(微信位置消息标准格式) setSelectedLocation({ - x: lng.toString(), - y: lat.toString(), + x: lat.toString(), + y: lng.toString(), scale: "16", - label: `${lat.toFixed(6)}, ${lng.toFixed(6)}`, + label: `${lat}, ${lng}`, poiname: "选中位置", maptype: "0", poiid: "", @@ -368,12 +380,13 @@ const SelectMap: React.FC = ({ } if (!addressLabel) { - addressLabel = `${lat.toFixed(6)}, ${lng.toFixed(6)}`; + addressLabel = `${lat}, ${lng}`; } + // 经纬度格式化为6位小数(微信位置消息标准格式) setSelectedLocation({ - x: lng.toString(), - y: lat.toString(), + x: lat.toString(), + y: lng.toString(), scale: "16", label: addressLabel, poiname: addressComponent.street || "未知位置", @@ -471,11 +484,12 @@ const SelectMap: React.FC = ({ } catch (error) { console.error("创建标记点失败:", error); // 即使创建标记失败,也设置基本的位置信息 + // 经纬度格式化为6位小数(微信位置消息标准格式) setSelectedLocation({ - x: lng.toString(), - y: lat.toString(), + x: lat.toString(), + y: lng.toString(), scale: "16", - label: `${lat.toFixed(6)}, ${lng.toFixed(6)}`, + label: `${lat}, ${lng}`, poiname: isDefault ? "默认位置" : "当前位置", maptype: "0", poiid: "", @@ -507,11 +521,12 @@ const SelectMap: React.FC = ({ formattedAddresses.recommend || formattedAddresses.rough || resultData.address || - `${lat.toFixed(6)}, ${lng.toFixed(6)}`; + `${lat}, ${lng}`; + // 经纬度格式化为6位小数(微信位置消息标准格式) setSelectedLocation({ - x: lng.toString(), - y: lat.toString(), + x: lat.toString(), + y: lng.toString(), scale: "16", label: addressLabel, poiname: @@ -530,11 +545,12 @@ const SelectMap: React.FC = ({ setIsReverseGeocoding(false); console.error("获取地址信息失败:", error); // 即使获取地址失败,也设置基本的位置信息 + // 经纬度格式化为6位小数(微信位置消息标准格式) setSelectedLocation({ - x: lng.toString(), - y: lat.toString(), + x: lat.toString(), + y: lng.toString(), scale: "16", - label: `${lat.toFixed(6)}, ${lng.toFixed(6)}`, + label: `${lat}, ${lng}`, poiname: isDefault ? "默认位置" : "当前位置", maptype: "0", poiid: "", @@ -779,9 +795,10 @@ const SelectMap: React.FC = ({ markerRef.current = newMarker; // 设置选中的位置信息 + // 经纬度格式化为6位小数(微信位置消息标准格式) setSelectedLocation({ - x: lng.toString(), - y: lat.toString(), + x: lat.toString(), + y: lng.toString(), scale: "16", label: result.address || result.title, poiname: result.title || "", @@ -806,16 +823,31 @@ const SelectMap: React.FC = ({ return; } - // 生成XML格式的位置信息 - const locationXml = ``; + // 转义XML特殊字符,确保格式正确 + // 注意:经纬度在存储时已经格式化为6位小数,直接使用即可 + const escapedLabel = escapeXml(selectedLocation.label); + const escapedPoiname = escapeXml(selectedLocation.poiname); + const scale = selectedLocation.scale || "16"; + const maptype = selectedLocation.maptype || "0"; + const poiid = escapeXml(selectedLocation.poiid || ""); + + // 生成XML格式的位置信息(格式与正确示例保持一致) + const locationXml = + ''; // 如果有onConfirm回调,调用它 if (onConfirm) { @@ -850,6 +882,7 @@ const SelectMap: React.FC = ({ }; addMessage(localMessage); + console.log(locationXml); // 发送消息到服务器 sendCommand("CmdSendMessage", { @@ -971,7 +1004,7 @@ const SelectMap: React.FC = ({ {selectedLocation.label || selectedLocation.poiname}
    - 经度: {selectedLocation.x}, 纬度: {selectedLocation.y} + 经度: {selectedLocation.y}, 纬度: {selectedLocation.x}
    )} From bda57a84e862ff4572f84222e8ba1d533798d3fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Sat, 22 Nov 2025 16:15:33 +0800 Subject: [PATCH 27/31] =?UTF-8?q?=E4=BC=98=E5=8C=96LocationMessage?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=EF=BC=8C=E6=B7=BB=E5=8A=A0=E7=BB=8F=E7=BA=AC?= =?UTF-8?q?=E5=BA=A6=E6=A0=BC=E5=BC=8F=E5=8C=96=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E7=94=9F=E6=88=90=E9=9D=99=E6=80=81=E5=9C=B0=E5=9B=BE=E9=A2=84?= =?UTF-8?q?=E8=A7=88=E5=9B=BEURL=EF=BC=8C=E6=94=B9=E8=BF=9B=E4=BD=8D?= =?UTF-8?q?=E7=BD=AE=E4=BF=A1=E6=81=AF=E5=B1=95=E7=A4=BA=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C=E5=92=8C=E8=A7=86?= =?UTF-8?q?=E8=A7=89=E6=95=88=E6=9E=9C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LocationMessage.module.scss | 76 ++++++++++++++- .../components/LocationMessage/index.tsx | 92 +++++++++++++------ 2 files changed, 137 insertions(+), 31 deletions(-) diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/LocationMessage/LocationMessage.module.scss b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/LocationMessage/LocationMessage.module.scss index 61ef4774..99bdc3e4 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/LocationMessage/LocationMessage.module.scss +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/LocationMessage/LocationMessage.module.scss @@ -9,7 +9,7 @@ // 位置消息基础样式 .locationMessage { - max-width: 420px; + width: 420px; margin: 4px 0; } @@ -21,6 +21,8 @@ cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); overflow: hidden; + display: flex; + flex-direction: column; &:hover { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); @@ -33,6 +35,45 @@ } } +// 地图预览区域 +.mapPreview { + position: relative; + width: 100%; + height: 200px; + overflow: hidden; + background: #f5f5f5; + display: flex; + align-items: center; + justify-content: center; +} + +.mapImage { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.mapPlaceholder { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: none; + flex-direction: column; + align-items: center; + justify-content: center; + background: #f5f5f5; + color: #999; + font-size: 14px; + gap: 8px; + + span:first-child { + font-size: 32px; + } +} + // 位置消息头部 .locationHeader { display: flex; @@ -70,6 +111,21 @@ // 位置消息内容 .locationContent { padding: 12px 16px; + display: flex; + flex-direction: column; + gap: 4px; +} + +.roadName { + font-size: 16px; + font-weight: 600; + color: #1a1a1a; + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; } .poiName { @@ -89,9 +145,8 @@ font-size: 13px; color: #666; line-height: 1.5; - margin-bottom: 12px; display: -webkit-box; - -webkit-line-clamp: 3; + -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; @@ -175,13 +230,17 @@ // 响应式设计 @media (max-width: 768px) { .locationMessage { - max-width: 280px; + width: 280px; } .locationCard { border-radius: 10px; } + .mapPreview { + height: 150px; + } + .locationHeader { padding: 10px 14px 6px; } @@ -253,6 +312,15 @@ } } + .mapPreview { + background: #2a2a2a; + } + + .mapPlaceholder { + background: #2a2a2a; + color: #999; + } + .locationHeader { background: linear-gradient(135deg, #2a2a2a 0%, #1a1a1a 100%); border-bottom-color: #333; diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/LocationMessage/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/LocationMessage/index.tsx index c2d6d8a4..4456f3e8 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/LocationMessage/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/components/LocationMessage/index.tsx @@ -85,10 +85,36 @@ const LocationMessage: React.FC = ({ content }) => { return renderErrorMessage("[位置消息 - 解析失败]"); } - // 生成地图链接 + // 格式化经纬度为6位小数 + const formatCoordinate = (coord: string): string => { + const num = parseFloat(coord); + if (isNaN(num)) { + return coord; // 如果无法解析,返回原值 + } + return num.toFixed(6); + }; + + // 生成地图链接(用于点击跳转) const generateMapUrl = (lat: string, lng: string, label: string) => { + const formattedLat = formatCoordinate(lat); + const formattedLng = formatCoordinate(lng); // 使用腾讯地图链接 - return `https://apis.map.qq.com/uri/v1/marker?marker=coord:${lat},${lng};title:${encodeURIComponent(label)}&referer=wechat`; + return `https://apis.map.qq.com/uri/v1/marker?marker=coord:${formattedLng},${formattedLat};title:${encodeURIComponent(label)}&referer=wechat`; + }; + + // 生成静态地图预览图URL + const generateStaticMapUrl = ( + lat: string, + lng: string, + width: number = 420, + height: number = 200, + ) => { + const formattedLat = formatCoordinate(lat); + const formattedLng = formatCoordinate(lng); + const key = "7DZBZ-ZSRK3-QJN3W-O5VTV-4E2P6-7GFYX"; + const zoom = locationData.scale || "15"; + // 腾讯地图静态地图API + return `https://apis.map.qq.com/ws/staticmap/v2/?center=${formattedLng},${formattedLat}&zoom=${zoom}&size=${width}x${height}&markers=${formattedLng},${formattedLat}&key=${key}`; }; const mapUrl = generateMapUrl( @@ -97,12 +123,18 @@ const LocationMessage: React.FC = ({ content }) => { locationData.label, ); + const staticMapUrl = generateStaticMapUrl( + locationData.y, + locationData.x, + 420, + 200, + ); + // 处理POI信息 - const poiName = locationData.poiname || locationData.label; - const poiCategory = locationData.poiCategoryTips - ? locationData.poiCategoryTips.split(":")[0] - : ""; - const poiPhone = locationData.poiPhone || ""; + // 提取道路名称(如果有的话,从label中提取) + const roadName = + locationData.poiname.split(/[((]/)[0] || locationData.label; + const detailAddress = locationData.label; return (
    @@ -110,29 +142,35 @@ const LocationMessage: React.FC = ({ content }) => { className={styles.locationCard} onClick={() => window.open(mapUrl, "_blank")} > - {/* 位置详情 */} + {/* 地图预览图 */} +
    + {locationData.label} { + // 如果图片加载失败,显示占位符 + const target = e.target as HTMLImageElement; + target.style.display = "none"; + const placeholder = target.nextElementSibling as HTMLElement; + if (placeholder) { + placeholder.style.display = "flex"; + } + }} + /> +
    + 📍 + 地图加载中... +
    +
    + + {/* 位置信息 */}
    - {/* POI名称 */} - {poiName &&
    {poiName}
    } + {/* 道路名称 */} + {roadName &&
    {roadName}
    } {/* 详细地址 */} -
    {locationData.label}
    - - {/* POI分类和电话 */} -
    - {poiCategory && ( -
    - 🏷️ - {poiCategory} -
    - )} - {poiPhone && ( -
    - 📞 - {poiPhone} -
    - )} -
    +
    {detailAddress}
    From 1786dec900ec91a28719fc7ade31e1c45745da67 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Mon, 24 Nov 2025 10:13:30 +0800 Subject: [PATCH 28/31] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/controller/WebSocketController.php | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Server/application/api/controller/WebSocketController.php b/Server/application/api/controller/WebSocketController.php index 773cbb3d..5b714997 100644 --- a/Server/application/api/controller/WebSocketController.php +++ b/Server/application/api/controller/WebSocketController.php @@ -457,7 +457,7 @@ class WebSocketController extends BaseController // 构建请求参数 $params = [ - "cmdType" => 'CmdDownloadMomentImagesResult', + "cmdType" => 'CmdDownloadMomentImages', "snsId" => $data['snsId'], "urls" => $data['snsUrls'], "wechatAccountId" => $data['wechatAccountId'], @@ -558,15 +558,15 @@ class WebSocketController extends BaseController $dataToSave['create_time'] = time(); $res = WechatMoments::create($dataToSave); } - // // 获取资源链接 - // if(empty($momentEntity['resUrls']) && !empty($momentEntity['urls'])){ - // $snsData = [ - // 'snsId' => $moment['snsId'], - // 'snsUrls' => $momentEntity['urls'], - // 'wechatAccountId' => $wechatAccountId, - // ]; - // $this->getMomentSourceRealUrl($snsData); - // } + // 获取资源链接 + if(empty($momentEntity['resUrls']) && !empty($momentEntity['urls'])){ + $snsData = [ + 'snsId' => $moment['snsId'], + 'snsUrls' => $momentEntity['urls'], + 'wechatAccountId' => $wechatAccountId, + ]; + $this->getMomentSourceRealUrl($snsData); + } } //Log::write('朋友圈数据已存入数据库,共' . count($momentList) . '条'); From 30b33ec974059ede3ee8f6dcd4fef64ef2f5f994 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Mon, 24 Nov 2025 11:03:31 +0800 Subject: [PATCH 29/31] =?UTF-8?q?=E6=9C=8B=E5=8F=8B=E5=9C=88=E9=87=87?= =?UTF-8?q?=E9=9B=86=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/controller/WebSocketController.php | 12 ++++-------- .../cunkebao/controller/ContentLibraryController.php | 8 ++++---- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/Server/application/api/controller/WebSocketController.php b/Server/application/api/controller/WebSocketController.php index 5b714997..b9b8a860 100644 --- a/Server/application/api/controller/WebSocketController.php +++ b/Server/application/api/controller/WebSocketController.php @@ -463,16 +463,10 @@ class WebSocketController extends BaseController "wechatAccountId" => $data['wechatAccountId'], "seq" => time(), ]; - // 记录请求日志 Log::info('获取朋友圈资源链接请求:' . json_encode($params, 256)); - // 发送请求 - $this->client->send(json_encode($params)); - - // 接收响应 - $response = $this->client->receive(); - $message = json_decode($response, true); + $message = $this->sendMessage($params); if (empty($message)) { return json_encode(['code' => 500, 'msg' => '获取朋友圈资源链接失败']); @@ -558,8 +552,10 @@ class WebSocketController extends BaseController $dataToSave['create_time'] = time(); $res = WechatMoments::create($dataToSave); } + + // 获取资源链接 - if(empty($momentEntity['resUrls']) && !empty($momentEntity['urls'])){ + if(empty($momentEntity['resUrls']) && !empty($momentEntity['urls']) && $moment['type'] == 1) { $snsData = [ 'snsId' => $moment['snsId'], 'snsUrls' => $momentEntity['urls'], diff --git a/Server/application/cunkebao/controller/ContentLibraryController.php b/Server/application/cunkebao/controller/ContentLibraryController.php index e4a2f639..69291123 100644 --- a/Server/application/cunkebao/controller/ContentLibraryController.php +++ b/Server/application/cunkebao/controller/ContentLibraryController.php @@ -1070,7 +1070,7 @@ class ContentLibraryController extends Controller ->select()->toArray(); if (empty($libraries)) { - return json(['code' => 200, 'msg' => '没有可用的内容库配置']); + return json_encode(['code' => 200, 'msg' => '没有可用的内容库配置'],256); } $successCount = 0; @@ -1159,7 +1159,7 @@ class ContentLibraryController extends Controller } // 返回采集结果 - return json([ + return json_encode([ 'code' => 200, 'msg' => '采集任务执行完成', 'data' => [ @@ -1169,7 +1169,7 @@ class ContentLibraryController extends Controller 'skipped' => $totalLibraries - $successCount - $failCount, 'results' => $results ] - ]); + ],256); } /** @@ -1206,7 +1206,7 @@ class ContentLibraryController extends Controller ->whereIn('id', $friendIds) ->where('isDeleted', 0) ->select(); - + if (empty($friends)) { return [ 'status' => 'failed', From c801490c2f5bf2350b1b510e3c3db09542b00fa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Mon, 24 Nov 2025 15:37:57 +0800 Subject: [PATCH 30/31] =?UTF-8?q?=E5=88=A0=E9=99=A4=E8=85=BE=E8=AE=AF?= =?UTF-8?q?=E5=9C=B0=E5=9B=BE=E5=AE=9A=E4=BD=8D=E6=9C=8D=E5=8A=A1=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E8=AF=B4=E6=98=8E=E6=96=87=E6=A1=A3=EF=BC=8C=E6=9B=B4?= =?UTF-8?q?=E6=96=B0CustomerList=E7=BB=84=E4=BB=B6=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=E4=BB=A5=E6=94=AF=E6=8C=81=E7=A6=BB=E7=BA=BF=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=8A=B6=E6=80=81=EF=BC=8C=E8=B0=83=E6=95=B4MessageList?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E7=9A=84=E5=8A=A0=E8=BD=BD=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E7=AE=A1=E7=90=86=EF=BC=8C=E5=A2=9E=E5=BC=BAfilter=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E4=BB=A5=E6=94=AF=E6=8C=81XML=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE=E6=B6=88=E6=81=AF=E7=9A=84=E8=AF=86=E5=88=AB?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/CustomerList/com.module.scss | 5 ++ .../weChat/components/CustomerList/index.tsx | 6 +- .../SidebarMenu/MessageList/index.tsx | 4 +- Touchkebao/src/utils/filter.ts | 5 ++ Touchkebao/消息功能规划.md | 0 Touchkebao/腾讯地图定位服务修复说明.md | 73 ------------------- 6 files changed, 13 insertions(+), 80 deletions(-) create mode 100644 Touchkebao/消息功能规划.md delete mode 100644 Touchkebao/腾讯地图定位服务修复说明.md diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/CustomerList/com.module.scss b/Touchkebao/src/pages/pc/ckbox/weChat/components/CustomerList/com.module.scss index 9cc2941e..29e92bf8 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/CustomerList/com.module.scss +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/CustomerList/com.module.scss @@ -47,6 +47,11 @@ .active & { border-color: #1890ff; } + + &.offline { + filter: grayscale(100%); + opacity: 0.6; + } } } .allUser { diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/CustomerList/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/CustomerList/index.tsx index 9ce88a1f..55ba297f 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/CustomerList/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/CustomerList/index.tsx @@ -89,7 +89,6 @@ const CustomerList: React.FC = () => { >
    全部
    -
    {customerList.map(customer => (
    { { {!customer.avatar && customer.name.charAt(0)} -
    ))} diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx index 2625b581..0e39a94b 100644 --- a/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/SidebarMenu/MessageList/index.tsx @@ -383,7 +383,7 @@ const MessageList: React.FC = () => { const requestId = ++loadRequestRef.current; const initializeSessions = async () => { - setLoading(true); + // setLoading(true); try { const cachedSessions = @@ -416,7 +416,7 @@ const MessageList: React.FC = () => { } } finally { if (!isCancelled && loadRequestRef.current === requestId) { - setLoading(false); + // setLoading(false); } } }; diff --git a/Touchkebao/src/utils/filter.ts b/Touchkebao/src/utils/filter.ts index 5e2d3754..22887a8b 100644 --- a/Touchkebao/src/utils/filter.ts +++ b/Touchkebao/src/utils/filter.ts @@ -58,6 +58,11 @@ export const messageFilter = (message: string) => { return "[图片]"; } + // XML 格式的位置消息:包含 ]/i.test(message)) { + return "[位置]"; + } + // 其他情况直接返回原始消息 return message; } diff --git a/Touchkebao/消息功能规划.md b/Touchkebao/消息功能规划.md new file mode 100644 index 00000000..e69de29b diff --git a/Touchkebao/腾讯地图定位服务修复说明.md b/Touchkebao/腾讯地图定位服务修复说明.md deleted file mode 100644 index 601db72f..00000000 --- a/Touchkebao/腾讯地图定位服务修复说明.md +++ /dev/null @@ -1,73 +0,0 @@ -# 腾讯地图定位服务修复说明 - -## 问题描述 - -在 `selectMap.tsx` 文件中使用腾讯地图定位服务时出现以下错误: - -``` -TypeError: window.TMap.service.Location is not a constructor - at selectMap.tsx:121:33 -``` - -## 原因分析 - -错误原因是尝试将 `TMap.service.Location` 作为构造函数使用,但在腾讯地图 GL API 中,定位服务不是通过构造函数方式创建的。 - -## 修复方法 - -### 1. 修改定位服务的初始化方式 - -将原来的代码: - -```typescript -// 创建IP定位服务 -window.geolocationRef = new window.TMap.service.Location({ - timeout: 10000, - convert: true, -}); -``` - -修改为: - -```typescript -// 使用腾讯地图内置的定位服务 -window.geolocationRef = window.TMap.service.Geolocation; -``` - -### 2. 修改定位服务的调用方式 - -在调用定位服务时,将配置参数直接传入 `getLocation` 方法: - -```typescript -window.geolocationRef.getLocation({ - timeout: 10000, - convert: true, - success: function (result: any) { - // 处理成功回调 - }, - error: function () { - // 处理错误回调 - }, -}); -``` - -## 技术说明 - -1. **腾讯地图 GL API 中的定位服务**: - - 正确的服务名称是 `TMap.service.Geolocation`,而非 `TMap.service.Location` - - 它是一个对象,不需要使用 `new` 关键字实例化 - - 配置参数应该直接传递给 `getLocation` 方法 - -2. **定位服务参数**: - - `timeout`:定位超时时间,单位毫秒 - - `convert`:是否将坐标转换为腾讯地图坐标系 - -3. **回调处理**: - - `success`:定位成功回调函数,返回位置信息 - - `error`:定位失败回调函数 - -## 注意事项 - -1. 确保腾讯地图 SDK 已正确加载 -2. 确保 API 密钥有定位服务的权限 -3. 定位精度可能受网络环境影响 From 78de6c92b7debbd5473915d45a9b9492488f4b40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Tue, 25 Nov 2025 15:36:43 +0800 Subject: [PATCH 31/31] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=A5=BD=E5=8F=8B?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BB=BB=E5=8A=A1=E5=88=97=E8=A1=A8API?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96NavCommon=E7=BB=84=E4=BB=B6=EF=BC=8C?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E7=AE=80?= =?UTF-8?q?=E5=8C=96=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84=EF=BC=8C=E6=8F=90?= =?UTF-8?q?=E5=8D=87=E5=8F=AF=E8=AF=BB=E6=80=A7=E5=92=8C=E7=BB=B4=E6=8A=A4?= =?UTF-8?q?=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pc/ckbox/components/NavCommon/Notice.tsx | 431 ++++++++++++++++++ .../pc/ckbox/components/NavCommon/api.ts | 5 + .../pc/ckbox/components/NavCommon/index.tsx | 211 +-------- 3 files changed, 440 insertions(+), 207 deletions(-) create mode 100644 Touchkebao/src/pages/pc/ckbox/components/NavCommon/Notice.tsx diff --git a/Touchkebao/src/pages/pc/ckbox/components/NavCommon/Notice.tsx b/Touchkebao/src/pages/pc/ckbox/components/NavCommon/Notice.tsx new file mode 100644 index 00000000..1249a6d7 --- /dev/null +++ b/Touchkebao/src/pages/pc/ckbox/components/NavCommon/Notice.tsx @@ -0,0 +1,431 @@ +import React, { useEffect, useState } from "react"; +import { Drawer, Avatar, Space, Button, Badge, Empty, Tabs, Tag } from "antd"; +import { BellOutlined } from "@ant-design/icons"; +import { + noticeList, + readMessage, + readAll, + friendRequestList as fetchFriendRequestListApi, +} from "./api"; +import styles from "./index.module.scss"; + +interface MessageItem { + id: number; + type: number; + companyId: number; + userId: number; + bindId: number; + title: string; + message: string; + isRead: number; + createTime: string; + readTime: string; + friendData: { + nickname: string; + avatar: string; + }; +} + +interface FriendRequestItem { + taskId: number; + phone: string; + wechatId: string; + adder?: { + avatar?: string; + nickname?: string; + username?: string; + accountNickname?: string; + accountRealName?: string; + }; + status?: { + code?: number; + text?: string; + }; + time?: { + addTime?: string; + addTimeStamp?: number; + updateTime?: string; + updateTimeStamp?: number; + passTime?: string; + passTimeStamp?: number; + }; + friend?: { + nickname?: string; + isPassed?: boolean; + }; + other?: { + msgContent?: string; + remark?: string; + from?: string; + labels?: string[]; + }; +} + +const DEFAULT_QUERY = { page: 1, limit: 20 }; + +const Notice: React.FC = () => { + const [messageDrawerVisible, setMessageDrawerVisible] = useState(false); + const [activeTab, setActiveTab] = useState("messages"); + const [messageList, setMessageList] = useState([]); + const [messageCount, setMessageCount] = useState(0); + const [loading, setLoading] = useState(false); + const [friendRequestList, setFriendRequestList] = useState< + FriendRequestItem[] + >([]); + const [friendRequestLoading, setFriendRequestLoading] = useState(false); + + const fetchMessageList = async () => { + try { + setLoading(true); + const response = await noticeList(DEFAULT_QUERY); + if (response?.list) { + setMessageList(response.list); + const unreadCount = response.list.filter( + (item: MessageItem) => item.isRead === 0, + ).length; + setMessageCount(unreadCount); + } + } catch (error) { + console.error("获取消息列表失败:", error); + } finally { + setLoading(false); + } + }; + + const refreshUnreadCount = async () => { + try { + const response = await noticeList(DEFAULT_QUERY); + if (response && typeof response.noRead === "number") { + setMessageCount(response.noRead); + } + } catch (error) { + console.error("获取未读消息数失败:", error); + } + }; + + useEffect(() => { + fetchMessageList(); + const timer = window.setInterval(refreshUnreadCount, 30 * 1000); + return () => { + window.clearInterval(timer); + }; + }, []); + + const handleMessageClick = () => { + setMessageDrawerVisible(true); + fetchMessageList(); + fetchFriendRequestList(); + }; + + const handleTabChange = (key: string) => { + setActiveTab(key); + if (key === "friendRequests") { + fetchFriendRequestList(); + } + }; + + const handleMessageDrawerClose = () => { + setMessageDrawerVisible(false); + }; + + const handleReadMessage = async (messageId: number) => { + try { + await readMessage({ id: messageId }); + setMessageList(prev => { + const updated = prev.map(item => + item.id === messageId ? { ...item, isRead: 1 } : item, + ); + const unreadCount = updated.filter(item => item.isRead === 0).length; + setMessageCount(unreadCount); + return updated; + }); + } catch (error) { + console.error("标记消息已读失败:", error); + } + }; + + const handleReadAll = async () => { + try { + await readAll(); + setMessageList(prev => prev.map(item => ({ ...item, isRead: 1 }))); + setMessageCount(0); + } catch (error) { + console.error("全部已读失败:", error); + } + }; + + const fetchFriendRequestList = async () => { + try { + setFriendRequestLoading(true); + const response = await fetchFriendRequestListApi(DEFAULT_QUERY); + if (response?.list) { + setFriendRequestList(response.list); + } + } catch (error) { + console.error("获取好友添加记录失败:", error); + } finally { + setFriendRequestLoading(false); + } + }; + + const formatTime = (timeStr?: string) => { + if (!timeStr) { + return "-"; + } + const date = new Date(timeStr); + const now = new Date(); + const diff = now.getTime() - date.getTime(); + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + + if (days === 0) { + return date.toLocaleTimeString("zh-CN", { + hour: "2-digit", + minute: "2-digit", + }); + } else if (days === 1) { + return "昨天"; + } else if (days < 7) { + return `${days}天前`; + } else { + return date.toLocaleDateString("zh-CN", { + month: "2-digit", + day: "2-digit", + }); + } + }; + + const getStatusText = (statusCode?: number, statusText?: string) => { + if (statusText) { + return statusText; + } + switch (statusCode) { + case 0: + return "待处理"; + case 1: + return "已同意"; + case 2: + return "已拒绝"; + default: + return "未知"; + } + }; + + const getStatusColor = (statusCode?: number) => { + switch (statusCode) { + case 0: + return "#1890ff"; + case 1: + return "#52c41a"; + case 2: + return "#ff4d4f"; + default: + return "#999"; + } + }; + + const getFriendRequestKey = (item: FriendRequestItem) => { + return ( + item.taskId?.toString() || + item.wechatId || + item.phone || + `${item.adder?.username || "unknown"}-${item.time?.addTime || "time"}` + ); + }; + + const getAddedUserName = (item: FriendRequestItem) => { + return ( + item.friend?.nickname || + item.phone || + item.wechatId || + item.adder?.nickname || + "未知好友" + ); + }; + + const getAdderName = (item: FriendRequestItem) => { + return ( + item.adder?.nickname || + item.adder?.username || + item.adder?.accountNickname || + item.adder?.accountRealName || + "未知添加人" + ); + }; + + return ( + <> +
    + + + +
    + + + + + ) + } + > +
    + + {loading ? ( +
    + 加载中... +
    + ) : messageList.length === 0 ? ( + + ) : ( + messageList.map(item => ( +
    handleReadMessage(item.id)} + > +
    + + {item.friendData?.nickname?.charAt(0) || "U"} + +
    +
    +
    + + {item.title} + + {item.isRead === 0 && ( +
    + )} +
    +
    + {item.message} +
    + {item.isRead === 0 && ( +
    + {formatTime(item.createTime)} + +
    + )} +
    +
    + )) + )} +
    + ), + }, + { + key: "friendRequests", + label: "好友添加记录", + children: ( +
    + {friendRequestLoading ? ( +
    + 加载中... +
    + ) : friendRequestList.length === 0 ? ( + + ) : ( + friendRequestList.map(item => ( +
    +
    + + {item.adder?.nickname?.charAt(0) || "U"} + +
    +
    +
    + + 添加好友: + {getAddedUserName(item)} + + + {getStatusText( + item.status?.code, + item.status?.text, + )} + +
    +
    + 申请人:{getAdderName(item)} +
    +
    + 验证信息:{item.other?.msgContent || "无"} +
    + +
    + {item.other?.remark && ( + + 备注:{item.other.remark} + + )} +
    +
    + {formatTime(item.time?.addTime)} +
    +
    +
    + )) + )} +
    + ), + }, + ]} + /> +
    + + + ); +}; + +export default Notice; diff --git a/Touchkebao/src/pages/pc/ckbox/components/NavCommon/api.ts b/Touchkebao/src/pages/pc/ckbox/components/NavCommon/api.ts index f814b6b9..dd665a02 100644 --- a/Touchkebao/src/pages/pc/ckbox/components/NavCommon/api.ts +++ b/Touchkebao/src/pages/pc/ckbox/components/NavCommon/api.ts @@ -14,3 +14,8 @@ export const readMessage = (params: { id: number }) => { export const readAll = () => { return request(`/v1/kefu/notice/readAll`, undefined, "PUT"); }; + +// 好友添加任务列表 +export const friendRequestList = (params: { page: number; limit: number }) => { + return request(`/v1/kefu/wechatFriend/addTaskList`, params, "GET"); +}; diff --git a/Touchkebao/src/pages/pc/ckbox/components/NavCommon/index.tsx b/Touchkebao/src/pages/pc/ckbox/components/NavCommon/index.tsx index fcd3f7a4..dce087d4 100644 --- a/Touchkebao/src/pages/pc/ckbox/components/NavCommon/index.tsx +++ b/Touchkebao/src/pages/pc/ckbox/components/NavCommon/index.tsx @@ -1,29 +1,18 @@ -import React, { useState, useEffect } from "react"; -import { - Layout, - Drawer, - Avatar, - Space, - Button, - Badge, - Dropdown, - Empty, - message, -} from "antd"; +import React, { useState } from "react"; +import { Layout, Avatar, Space, Button, Dropdown, message } from "antd"; import { BarChartOutlined, UserOutlined, - BellOutlined, LogoutOutlined, ThunderboltOutlined, SettingOutlined, SendOutlined, ClearOutlined, } from "@ant-design/icons"; -import { noticeList, readMessage, readAll } from "./api"; import { useUserStore } from "@/store/module/user"; import { useNavigate, useLocation } from "react-router-dom"; import styles from "./index.module.scss"; +import Notice from "./Notice"; const { Header } = Layout; @@ -32,40 +21,12 @@ interface NavCommonProps { onMenuClick?: () => void; } -// 消息数据类型 -interface MessageItem { - id: number; - type: number; - companyId: number; - userId: number; - bindId: number; - title: string; - message: string; - isRead: number; - createTime: string; - readTime: string; - friendData: { - nickname: string; - avatar: string; - }; -} - const NavCommon: React.FC = ({ title = "触客宝" }) => { - const [messageDrawerVisible, setMessageDrawerVisible] = useState(false); - const [messageList, setMessageList] = useState([]); - const [messageCount, setMessageCount] = useState(0); - const [loading, setLoading] = useState(false); const [clearingCache, setClearingCache] = useState(false); const navigate = useNavigate(); const location = useLocation(); const { user, logout } = useUserStore(); - // 初始化时获取消息列表 - useEffect(() => { - fetchMessageList(); - setInterval(IntervalMessageCount, 30 * 1000); - }, []); - // 处理菜单图标点击:在两个路由之间切换 const handleMenuClick = () => { if (!location.pathname.startsWith("/pc/powerCenter")) { @@ -74,48 +35,6 @@ const NavCommon: React.FC = ({ title = "触客宝" }) => { navigate("/pc/weChat"); } }; - // 定时器获取消息条数 - const IntervalMessageCount = async () => { - try { - const response = await noticeList({ page: 1, limit: 20 }); - if (response && response.noRead) { - setMessageCount(response.noRead); - } - } catch (error) { - console.error("获取消息列表失败:", error); - } - }; - // 获取消息列表 - const fetchMessageList = async () => { - try { - setLoading(true); - const response = await noticeList({ page: 1, limit: 20 }); - if (response && response.list) { - setMessageList(response.list); - // 计算未读消息数量 - const unreadCount = response.list.filter( - (item: MessageItem) => item.isRead === 0, - ).length; - setMessageCount(unreadCount); - } - } catch (error) { - console.error("获取消息列表失败:", error); - } finally { - setLoading(false); - } - }; - - // 处理消息中心点击 - const handleMessageClick = () => { - setMessageDrawerVisible(true); - fetchMessageList(); - }; - - // 处理消息抽屉关闭 - const handleMessageDrawerClose = () => { - setMessageDrawerVisible(false); - }; - // 处理退出登录 const handleLogout = () => { logout(); // 清除localStorage中的token和用户状态 @@ -215,61 +134,6 @@ const NavCommon: React.FC = ({ title = "触客宝" }) => { } }; - // 处理消息已读 - const handleReadMessage = async (messageId: number) => { - try { - await readMessage({ id: messageId }); // 这里需要根据实际API调整参数 - // 更新本地状态 - setMessageList(prev => - prev.map(item => - item.id === messageId ? { ...item, isRead: 1 } : item, - ), - ); - // 重新计算未读数量 - const unreadCount = - messageList.filter(item => item.isRead === 0).length - 1; - setMessageCount(Math.max(0, unreadCount)); - } catch (error) { - console.error("标记消息已读失败:", error); - } - }; - - // 处理全部已读 - const handleReadAll = async () => { - try { - await readAll(); // 这里需要根据实际API调整参数 - // 更新本地状态 - setMessageList(prev => prev.map(item => ({ ...item, isRead: 1 }))); - setMessageCount(0); - } catch (error) { - console.error("全部已读失败:", error); - } - }; - - // 格式化时间 - const formatTime = (timeStr: string) => { - const date = new Date(timeStr); - const now = new Date(); - const diff = now.getTime() - date.getTime(); - const days = Math.floor(diff / (1000 * 60 * 60 * 24)); - - if (days === 0) { - return date.toLocaleTimeString("zh-CN", { - hour: "2-digit", - minute: "2-digit", - }); - } else if (days === 1) { - return "昨天"; - } else if (days < 7) { - return `${days}天前`; - } else { - return date.toLocaleDateString("zh-CN", { - month: "2-digit", - day: "2-digit", - }); - } - }; - // 用户菜单项 const userMenuItems = [ { @@ -333,11 +197,7 @@ const NavCommon: React.FC = ({ title = "触客宝" }) => { {user?.tokens} -
    - - - -
    + = ({ title = "触客宝" }) => {
    - - - - - } - > -
    - {loading ? ( -
    - 加载中... -
    - ) : messageList.length === 0 ? ( - - ) : ( - messageList.map(item => ( -
    handleReadMessage(item.id)} - > -
    - - {item.friendData?.nickname?.charAt(0) || "U"} - -
    -
    -
    - {item.title} - {item.isRead === 0 && ( -
    - )} -
    -
    {item.message}
    - {item.isRead === 0 && ( -
    - {formatTime(item.createTime)} - -
    - )} -
    -
    - )) - )} -
    -
    ); };