diff --git a/Server/application/api/controller/AccountController.php b/Server/application/api/controller/AccountController.php index aeadb752..30345292 100644 --- a/Server/application/api/controller/AccountController.php +++ b/Server/application/api/controller/AccountController.php @@ -23,9 +23,15 @@ class AccountController extends BaseController * @param bool $isInner 是否为定时任务调用 * @return \think\response\Json */ - public function getlist($pageIndex = '', $pageSize = '', $isInner = false) + public function getlist($data = [], $isInner = false) { + $pageIndex = !empty($data['pageIndex']) ? $data['pageIndex'] : 0; + $pageSize = !empty($data['pageSize']) ? $data['pageSize'] : 20; + $showNormalAccount = !empty($data['showNormalAccount']) ? $data['showNormalAccount'] : ''; + $keyword = !empty($data['keyword']) ? $data['keyword'] : ''; + $departmentId = !empty($data['departmentId']) ? $data['departmentId'] : ''; + // 获取授权token $authorization = trim($this->request->header('authorization', $this->authorization)); if (empty($authorization)) { @@ -39,11 +45,11 @@ class AccountController extends BaseController try { // 构建请求参数 $params = [ - 'showNormalAccount' => $this->request->param('showNormalAccount', ''), - 'keyword' => $this->request->param('keyword', ''), - 'departmentId' => $this->request->param('departmentId', ''), - 'pageIndex' => !empty($pageIndex) ? $pageIndex : $this->request->param('pageIndex', 0), - 'pageSize' => !empty($pageSize) ? $pageSize : $this->request->param('pageSize', 20) + 'showNormalAccount' => $showNormalAccount, + 'keyword' => $keyword, + 'departmentId' => $departmentId, + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize ]; // 设置请求头 diff --git a/Server/application/api/controller/AutomaticAssign.php b/Server/application/api/controller/AutomaticAssign.php index 88e0048f..17bc03cc 100644 --- a/Server/application/api/controller/AutomaticAssign.php +++ b/Server/application/api/controller/AutomaticAssign.php @@ -171,7 +171,7 @@ class AutomaticAssign extends BaseController * @param int $optFrom 操作来源 * @return \think\response\Json */ - public function allotWechatFriend($data = [],$isInner = false) + public function allotWechatFriend($data = [],$isInner = false,$errorNum = 0) { // 获取授权token $authorization = trim($this->request->header('authorization', $this->authorization)); @@ -188,23 +188,24 @@ class AutomaticAssign extends BaseController $wechatFriendId = !empty($data['wechatFriendId']) ? $data['wechatFriendId'] : input('wechatFriendId', 0); $toAccountId = !empty($data['toAccountId']) ? $data['toAccountId'] : input('toAccountId', 0); $comment = !empty($data['comment']) ? $data['comment'] : input('comment', ''); - $notifyReceiver = !empty($data['notifyReceiver']) ? $data['notifyReceiver'] : input('notifyReceiver', false); + $notifyReceiver = !empty($data['notifyReceiver']) ? $data['notifyReceiver'] : input('notifyReceiver', 'false'); $optFrom = !empty($data['optFrom']) ? $data['optFrom'] : input('optFrom', 4); // 默认操作来源为4 // 参数验证 if (empty($wechatFriendId)) { - return errorJson('微信好友ID不能为空'); + return json_encode(['code'=>500,'msg'=>'微信好友ID不能为空']); + } if (empty($toAccountId)) { - return errorJson('目标账号ID不能为空'); + return json_encode(['code'=>500,'msg'=>'目标账号ID不能为空']); } // 设置请求头 $headerData = ['client:system']; $header = setHeader($headerData, $authorization, 'json'); - + // 发送请求 $url = $this->baseUrl . 'api/WechatFriend/allot?wechatFriendId='.$wechatFriendId.'¬ifyReceiver='.$notifyReceiver.'&comment='.$comment.'&toAccountId='.$toAccountId.'&optFrom='.$optFrom; $result = requestCurl($url, [], 'PUT', $header, 'json'); @@ -226,8 +227,59 @@ class AutomaticAssign extends BaseController if($isInner){ return json_encode(['code'=>500,'msg'=>'微信好友分配失败:' . $e->getMessage()]); }else{ + Cache::rm('system_authorization_token'); + Cache::rm('system_refresh_token'); + $errorNum ++; + if ($errorNum <= 3) { + $this->allotWechatFriend($data,$isInner,$errorNum); + } + return json_encode(['code'=>500,'msg'=> $result]); return errorJson('微信好友分配失败:' . $e->getMessage()); } } } + + + public function multiAllotFriendToAccount($data = [],$errorNum = 0){ + // 获取授权token + $authorization = $this->authorization; + if (empty($authorization)) { + return json_encode(['code'=>500,'msg'=>'缺少授权信息']); + } + + $wechatFriendIds = !empty($data['wechatFriendIds']) ? $data['wechatFriendIds'] : input('wechatFriendIds', []); + $toAccountId = !empty($data['toAccountId']) ? $data['toAccountId'] : input('toAccountId', 0); + $notifyReceiver = !empty($data['notifyReceiver']) ? $data['notifyReceiver'] : input('notifyReceiver', 'false'); + // 参数验证 + if (empty($wechatFriendIds)) { + return json_encode(['code'=>500,'msg'=>'微信好友ID不能为空']); + } + + if (empty($toAccountId)) { + return json_encode(['code'=>500,'msg'=>'目标账号ID不能为空']); + } + + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求 + $url = $this->baseUrl . 'api/WechatFriend/multiAllotFriendToAccount?wechatFriendIds='.$wechatFriendIds.'&toAccountId='.$toAccountId.'¬ifyReceiver='.$notifyReceiver; + $result = requestCurl($url, [], 'PUT', $header, 'json'); + if (empty($result)) { + return json_encode(['code'=>200,'msg'=>'微信好友分配成功']); + } else { + Cache::rm('system_authorization_token'); + Cache::rm('system_refresh_token'); + $errorNum ++; + if ($errorNum <= 3) { + $this->multiAllotFriendToAccount($data,$errorNum); + } + return json_encode(['code'=>500,'msg'=> $result]); + } + + } + + } \ No newline at end of file diff --git a/Server/application/api/controller/MessageController.php b/Server/application/api/controller/MessageController.php index 2cb39682..da033741 100644 --- a/Server/application/api/controller/MessageController.php +++ b/Server/application/api/controller/MessageController.php @@ -3,6 +3,7 @@ namespace app\api\controller; use app\api\model\WechatMessageModel; +use think\Db; use think\facade\Request; class MessageController extends BaseController @@ -386,6 +387,51 @@ class MessageController extends BaseController 'wechatTime' => $wechatTime ]; + //已被删除 + if ($item['msgType'] == 10000 && strpos($item['content'],'开启了朋友验证') !== false) { + Db::table('s2_wechat_friend')->where('id',$item['wechatFriendId'])->update(['isDeleted'=> 1,'deleteTime' => $wechatTime]); + }else{ + //优先分配在线客服 + $friend = Db::table('s2_wechat_friend')->where('id',$item['wechatFriendId'])->find(); + if (!empty($friend)){ + $accountId = $item['accountId']; + $accountData = Db::table('s2_company_account')->where('id',$accountId)->find(); + if (!empty($accountData)){ + $account = new AccountController(); + $account->getlist(['pageIndex' => 0,'pageSize' => 100,'departmentId' => $accountData['departmentId']]); + $accountIds = Db::table('s2_company_account')->where(['id' => $accountId,'alive' => 1])->column('id'); + if (!empty($accountIds)){ + if (!in_array($friend['accountId'],$accountData)){ + // 执行切换好友命令 + $randomKey = array_rand($accountIds, 1); + $toAccountId = $accountIds[$randomKey]; + $toAccountData = Db::table('s2_company_account')->where('id',$toAccountId)->find(); + $automaticAssign = new AutomaticAssign(); + $automaticAssign->allotWechatFriend([ + 'wechatFriendId' => $friend['id'], + 'toAccountId' => $toAccountId + ], true); + Db::table('s2_wechat_friend') + ->where('id',$friend['id']) + ->update([ + 'accountId' => $toAccountId, + 'accountUserName' => $toAccountData['userName'], + 'accountRealName' => $toAccountData['realName'], + 'accountNickname' => $toAccountData['nickname'], + ]); + } + } + + } + } + } + + + + + + + // 创建新记录 WechatMessageModel::create($data); } diff --git a/Server/application/api/controller/WebSocketController.php b/Server/application/api/controller/WebSocketController.php index 15f5fdfa..8400183d 100644 --- a/Server/application/api/controller/WebSocketController.php +++ b/Server/application/api/controller/WebSocketController.php @@ -62,11 +62,11 @@ class WebSocketController extends BaseController 'username' => $userData['userName'], 'password' => $userData['password'] ]; - + // 调用登录接口获取token - $headerData = ['client:kefu-client']; + $headerData = ['client:kefu-client']; $header = setHeader($headerData, '', 'plain'); - $result = requestCurl('https://kf.quwanzhi.com:9991/token', $params, 'POST', $header); + $result = requestCurl('https://kf.quwanzhi.com:9991/token', $params, 'POST', $header); $result_array = handleApiResponse($result); if (isset($result_array['access_token']) && !empty($result_array['access_token'])) { diff --git a/Server/application/api/controller/WechatChatroomController.php b/Server/application/api/controller/WechatChatroomController.php index 43a33654..1fb4938d 100644 --- a/Server/application/api/controller/WechatChatroomController.php +++ b/Server/application/api/controller/WechatChatroomController.php @@ -72,7 +72,11 @@ class WechatChatroomController extends BaseController return successJson($response); } } catch (\Exception $e) { - return errorJson('获取微信群聊列表失败:' . $e->getMessage()); + if($isInner){ + return json_encode(['code'=>200,'msg'=>'获取微信群聊列表失败' . $e->getMessage()]); + }else{ + return errorJson('获取微信群聊列表失败:' . $e->getMessage()); + } } } diff --git a/Server/application/command/SwitchFriendsCommand.php b/Server/application/command/SwitchFriendsCommand.php new file mode 100644 index 00000000..d153f372 --- /dev/null +++ b/Server/application/command/SwitchFriendsCommand.php @@ -0,0 +1,225 @@ +setName('switch:friends') + ->setDescription('切换好友命令'); + } + + protected function execute(Input $input, Output $output) + { + //处理流量分过期数据 + $expUserData = Db::name('workbench_traffic_config_item') + ->where('expTime','<=',time()) + ->where('isRecycle',0) + ->select(); + + // 根据accountId对数组进行归类 + $groupedByAccount = []; + foreach ($expUserData as $friend) { + $accountId = $friend['wechatAccountId']; + if (!isset($groupedByAccount[$accountId])) { + $groupedByAccount[$accountId] = []; + } + $friendId = $friend['wechatFriendId']; + $groupedByAccount[$accountId][] = $friendId; + } + + // 对每个账号的好友进行20个为一组的分组 + foreach ($groupedByAccount as $accountId => $accountFriends) { + //检索主账号 + $account = Db::name('users')->where('s2_accountId',$accountId)->find(); + if (empty($account)) { + continue; + } + $account2 = Db::name('users') + ->where('s2_accountId','>',0) + ->where('companyId',$account['companyId']) + ->order('s2_accountId ASC') + ->find(); + if (empty($account2)) { + continue; + } + $newaAccountId = $account2['s2_accountId']; + + $chunks = array_chunk($accountFriends, 20); + $output->writeln('账号 ' . $newaAccountId . ' 共有 ' . count($accountFriends) . ' 个好友,分为 ' . count($chunks) . ' 组'); + + $automaticAssign = new AutomaticAssign(); + foreach ($chunks as $chunkIndex => $chunk) { + $output->writeln('处理账号 ' . $newaAccountId . ' 第 ' . ($chunkIndex + 1) . ' 组,共 ' . count($chunk) . ' 个好友'); + try { + $friendIds = implode(',', $chunk); + $res = $automaticAssign->multiAllotFriendToAccount([ + 'wechatFriendIds' => $friendIds, + 'toAccountId' => $newaAccountId, + ]); + $res = json_decode($res, true); + if ($res['code'] == 200){ + //修改数据库 + Db::table('s2_wechat_friend') + ->where('id',$friendIds) + ->update([ + 'accountId' => $account2['s2_accountId'], + 'accountUserName' => $account2['account'], + 'accountRealName' => $account2['username'], + 'accountNickname' => $account2['username'], + ]); + + Db::name('workbench_traffic_config_item') + ->whereIn('wechatFriendId',$friendIds) + ->where('wechatAccountId',$accountId) + ->update([ + 'isRecycle' => 1, + 'recycleTime' => time(), + ]); + $output->writeln('✓ 成功切换好友:' . $friendIds . ' 到账号:' . $newaAccountId); + } else { + $output->writeln('✗ 切换失败 - 好友:' . $friendIds . ' 到账号:' . $newaAccountId . ' 结果:' . $res['msg']); + } + } catch (\Exception $e) { + $output->writeln('✗ 切换异常 - 好友:' . implode(',', $chunk) . ' 到账号:' . $newaAccountId . ' 错误:' . $e->getMessage()); + } + + // 每组处理完后稍作延迟,避免请求过于频繁 + if ($chunkIndex < count($chunks) - 1) { + sleep(1); + } + } + } + + + + + + $cacheKey = 'allotWechatFriend'; + $now = time(); + $maxRetry = 5; + $retry = 0; + $switchedIds = []; + $totalProcessed = 0; + $totalSuccess = 0; + $totalFailed = 0; + + $output->writeln('开始执行好友切换任务...'); + + do { + $friends = Cache::get($cacheKey, []); + $toSwitch = []; + foreach ($friends as $friend) { + if (isset($friend['time']) && $friend['time'] < $now) { + $toSwitch[] = $friend; + } + } + + if (empty($toSwitch)) { + $output->writeln('没有需要切换的好友'); + return; + } + + $output->writeln('找到 ' . count($toSwitch) . ' 个需要切换的好友'); + + $automaticAssign = new AutomaticAssign(); + + // 根据accountId对数组进行归类 + $groupedByAccount = []; + foreach ($toSwitch as $friend) { + $accountId = $friend['accountId']; + if (!isset($groupedByAccount[$accountId])) { + $groupedByAccount[$accountId] = []; + } + $friendId = !empty($friend['friendId']) ? $friend['friendId'] : $friend['id']; + $groupedByAccount[$accountId][] = $friendId; + } + + + // 对每个账号的好友进行20个为一组的分组 + foreach ($groupedByAccount as $accountId => $accountFriends) { + $chunks = array_chunk($accountFriends, 20); + $output->writeln('账号 ' . $accountId . ' 共有 ' . count($accountFriends) . ' 个好友,分为 ' . count($chunks) . ' 组'); + $accountSuccess = 0; + $accountFailed = 0; + + foreach ($chunks as $chunkIndex => $chunk) { + $output->writeln('处理账号 ' . $accountId . ' 第 ' . ($chunkIndex + 1) . ' 组,共 ' . count($chunk) . ' 个好友'); + try { + $friendIds = implode(',', $chunk); + $res = $automaticAssign->multiAllotFriendToAccount([ + 'wechatFriendIds' => $friendIds, + 'toAccountId' => $accountId, + ]); + $res = json_decode($res, true); + if ($res['code'] == 200){ + $output->writeln('✓ 成功切换好友:' . $friendIds . ' 到账号:' . $accountId); + $switchedIds = array_merge($switchedIds, $chunk); + $accountSuccess += count($chunk); + $totalSuccess += count($chunk); + } else { + $output->writeln('✗ 切换失败 - 好友:' . $friendIds . ' 到账号:' . $accountId . ' 结果:' . $res['msg']); + $accountFailed += count($chunk); + $totalFailed += count($chunk); + } + } catch (\Exception $e) { + $output->writeln('✗ 切换异常 - 好友:' . implode(',', $chunk) . ' 到账号:' . $accountId . ' 错误:' . $e->getMessage()); + Log::error('切换好友异常: ' . $e->getMessage() . ' 好友IDs: ' . implode(',', $chunk) . ' 账号ID: ' . $accountId); + $accountFailed += count($chunk); + $totalFailed += count($chunk); + } + + $totalProcessed += count($chunk); + + // 每组处理完后稍作延迟,避免请求过于频繁 + if ($chunkIndex < count($chunks) - 1) { + sleep(1); + } + } + + $output->writeln('账号 ' . $accountId . ' 处理完成 - 成功:' . $accountSuccess . ',失败:' . $accountFailed); + } + + // 过滤掉已切换的,保留未切换和新进来的 + $newFriends = Cache::get($cacheKey, []); + $updated = []; + foreach ($newFriends as $friend) { + $friendId = !empty($friend['friendId']) ? $friend['friendId'] : $friend['id']; + if (!in_array($friendId, $switchedIds)) { + $updated[] = $friend; + } + } + + // 按time升序排序 + usort($updated, function($a, $b) { + return ($a['time'] ?? 0) <=> ($b['time'] ?? 0); + }); + + $success = Cache::set($cacheKey, $updated); + $retry++; + } while (!$success && $retry < $maxRetry); + + $output->writeln('=== 切换任务完成 ==='); + $output->writeln('总处理数量:' . $totalProcessed); + $output->writeln('成功切换:' . $totalSuccess); + $output->writeln('切换失败:' . $totalFailed); + $output->writeln('成功率:' . ($totalProcessed > 0 ? round(($totalSuccess / $totalProcessed) * 100, 2) : 0) . '%'); + $output->writeln('缓存已更新并排序'); + } + +} \ No newline at end of file diff --git a/Server/application/cunkebao/config/route.php b/Server/application/cunkebao/config/route.php index ff4709e6..01efb604 100644 --- a/Server/application/cunkebao/config/route.php +++ b/Server/application/cunkebao/config/route.php @@ -45,6 +45,7 @@ Route::group('v1/', function () { Route::post('updateStatus', 'app\cunkebao\controller\plan\PlanSceneV1Controller@updateStatus'); Route::get('detail', 'app\cunkebao\controller\plan\GetAddFriendPlanDetailV1Controller@index'); Route::PUT('update', 'app\cunkebao\controller\plan\PostUpdateAddFriendPlanV1Controller@index'); + Route::get('getWxMinAppCode', 'app\cunkebao\controller\plan\PlanSceneV1Controller@getWxMinAppCode'); }); // 流量池相关 @@ -100,4 +101,18 @@ Route::group('v1/', function () { }); })->middleware(['jwt']); + + +Route::group('v1/frontend', function () { + + Route::group('business/poster', function () { + Route::post('getone', 'app\cunkebao\controller\plan\PosterWeChatMiniProgram@getPosterTaskData'); + Route::post('decryptphone', 'app\cunkebao\controller\plan\PosterWeChatMiniProgram@getPhoneNumber'); + }); +}); + + + + + return []; \ No newline at end of file diff --git a/Server/application/cunkebao/controller/ContentLibraryController.php b/Server/application/cunkebao/controller/ContentLibraryController.php index 20bccc7e..6c522a6d 100644 --- a/Server/application/cunkebao/controller/ContentLibraryController.php +++ b/Server/application/cunkebao/controller/ContentLibraryController.php @@ -4,9 +4,11 @@ namespace app\cunkebao\controller; use app\cunkebao\model\ContentLibrary; use app\cunkebao\model\ContentItem; +use library\s2\titleFavicon; use think\Controller; use think\Db; use app\api\controller\WebSocketController; +use think\facade\Cache; use think\facade\Env; use app\api\controller\AutomaticAssign; @@ -329,7 +331,7 @@ class ContentLibraryController extends Controller $library->timeEnd = isset($param['endTime']) ? strtotime($param['endTime']) : 0; $library->status = isset($param['status']) ? $param['status'] : 0; $library->updateTime = time(); - + $library->save(); @@ -418,7 +420,7 @@ class ContentLibraryController extends Controller // 查询数据 $list = ContentItem::where($where) - ->order('createTime', 'desc') + ->order('createMomentTime DESC,createTime DESC') ->page($page, $limit) ->select(); @@ -751,7 +753,26 @@ class ContentLibraryController extends Controller /************************************ * 数据采集相关功能 ************************************/ - + + function getExternalPageDetails($url) + { + $html = file_get_contents($url); + $dom = new \DOMDocument(); + @$dom->loadHTML($html); + $xpath = new \DOMXPath($dom); + + // 获取标题 + $titleNode = $xpath->query('//title'); + $title = $titleNode->length > 0 ? $titleNode->item(0)->nodeValue : ''; + + // 获取图标链接 + $iconNode = $xpath->query('//link[@rel="shortcut icon"]/@href'); + $icon = $iconNode->length > 0 ? $iconNode->item(0)->nodeValue : ''; + + return ['title' => $title, 'icon' => $icon]; + } + + /** * 执行朋友圈采集任务 * @return \think\response\Json @@ -761,7 +782,7 @@ class ContentLibraryController extends Controller // 查询条件:未删除且已开启的内容库 $where = [ ['isDel', '=', 0], // 未删除 - ['status', '=', 1] // 已开启 + ['status', '=', 1], // 已开启 ]; // 查询符合条件的内容库 @@ -862,10 +883,11 @@ class ContentLibraryController extends Controller 'message' => '没有指定要采集的好友' ]; } + $friendData = []; try { $toAccountId = ''; - $username = Env::get('api.username', ''); - $password = Env::get('api.password', ''); + $username = Env::get('api.username2', ''); + $password = Env::get('api.password2', ''); if (!empty($username) || !empty($password)) { $toAccountId = Db::name('users')->where('account',$username)->value('s2_accountId'); } @@ -889,10 +911,14 @@ class ContentLibraryController extends Controller $totalMomentsCount = 0; foreach ($friends as $friend) { + $friendData = $friend; if (!empty($username) && !empty($password)) { //执行切换好友命令 $automaticAssign = new AutomaticAssign(); $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['id'],'toAccountId' => $toAccountId],true); + //存入缓存 + $friendData['friendId'] = $friend['id']; + artificialAllotWechatFriend($friendData); //执行采集朋友圈命令 $webSocket = new WebSocketController(['userName' => $username,'password' => $password,'accountId' => $toAccountId]); $webSocket->getMoments(['wechatFriendId' => $friend['id'],'wechatAccountId' => $friend['wechatAccountId']]); @@ -1389,9 +1415,56 @@ class ContentLibraryController extends Controller $coverImage = $resUrls[0]; } - // 判断内容类型 (0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序, 6=图文) - $contentType = $this->determineContentType($moment['content'] ?? '', $resUrls, $urls); - + // 判断内容类型 (0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序) + if($moment['type'] == 1) { + //图文 + $contentType = 1; + }elseif ($moment['type'] == 3){ + //链接 + $contentType = 2; + $urls = []; + $url = is_string($moment['urls']) ? json_decode($moment['urls'], true) : $moment['urls'] ?? []; + $url = $url[0]; + + // 检查是否是飞书链接 + if (strpos($url, 'feishu.cn') !== false) { + // 飞书文档需要登录,无法直接获取内容,返回默认信息 + $urls[] = [ + 'url' => $url, + 'image' => 'http://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/2025/07/09/3db2a5d7fe49011ab68175a42a5094ce.jpeg', + 'desc' => '飞书文档' + ]; + }else{ + $getUrlDetails = $this->getExternalPageDetails($url); + $icon = 'http://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/2025/07/09/ec039d96fad6eab1d960f207d3d9ca9f.jpeg'; + if (!empty($getUrlDetails['title'])) { + $urls[] = [ + 'url' => $url, + 'image' => $icon, + 'desc' => '点击查看详情' + ]; + }else{ + $urls[] = [ + 'url' => $url, + 'image' => !empty($getUrlDetails['icon']) ? $getUrlDetails['icon'] : $icon, + 'desc' => $getUrlDetails['title'] + ]; + } + } + $moment['urls'] = $urls; + }elseif ($moment['type'] == 15){ + //视频 + $contentType = 3; + }elseif ($moment['type'] == 2){ + //纯文本 + $contentType = 4; + }elseif ($moment['type'] == 30){ + //小程序 + $contentType = 5; + }else{ + $contentType = 1; + } + // 如果不存在,则创建新的内容项目 $item = new ContentItem(); $item->libraryId = $libraryId; @@ -1695,4 +1768,167 @@ class ContentLibraryController extends Controller return []; } } -} \ No newline at end of file + + + + + /** + * 解析URL获取网页信息(内部调用) + * @param string $url 要解析的URL + * @return array 包含title、icon的数组,失败返回空数组 + */ + public function parseUrl($url) + { + if (empty($url) || !filter_var($url, FILTER_VALIDATE_URL)) { + return []; + } + + try { + // 设置请求头,模拟浏览器访问 + $context = stream_context_create([ + 'http' => [ + 'method' => 'GET', + 'header' => [ + 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8', + 'Accept-Encoding: gzip, deflate', + 'Connection: keep-alive', + 'Upgrade-Insecure-Requests: 1' + ], + 'timeout' => 10, + 'follow_location' => true, + 'max_redirects' => 3 + ] + ]); + + // 获取网页内容 + $html = @file_get_contents($url, false, $context); + + if ($html === false) { + return []; + } + + // 检测编码并转换为UTF-8 + $encoding = mb_detect_encoding($html, ['UTF-8', 'GBK', 'GB2312', 'BIG5', 'ASCII']); + if ($encoding && $encoding !== 'UTF-8') { + $html = mb_convert_encoding($html, 'UTF-8', $encoding); + } + + // 解析HTML + $dom = new \DOMDocument(); + @$dom->loadHTML($html, LIBXML_NOERROR | LIBXML_NOWARNING); + $xpath = new \DOMXPath($dom); + + $result = [ + 'title' => '', + 'icon' => '', + 'url' => $url + ]; + + // 提取标题 + $titleNodes = $xpath->query('//title'); + if ($titleNodes->length > 0) { + $result['title'] = trim($titleNodes->item(0)->textContent); + } + + // 提取图标 - 优先获取favicon + $iconNodes = $xpath->query('//link[@rel="icon"]/@href | //link[@rel="shortcut icon"]/@href | //link[@rel="apple-touch-icon"]/@href'); + if ($iconNodes->length > 0) { + $iconUrl = trim($iconNodes->item(0)->value); + $result['icon'] = $this->makeAbsoluteUrl($iconUrl, $url); + } else { + // 尝试获取Open Graph图片 + $ogImageNodes = $xpath->query('//meta[@property="og:image"]/@content'); + if ($ogImageNodes->length > 0) { + $result['icon'] = trim($ogImageNodes->item(0)->value); + } else { + // 默认favicon路径 + $result['icon'] = $this->makeAbsoluteUrl('/favicon.ico', $url); + } + } + + // 清理和验证数据 + $result['title'] = $this->cleanText($result['title']); + + return $result; + + } catch (\Exception $e) { + // 记录错误日志但不抛出异常 + \think\facade\Log::error('URL解析失败: ' . $e->getMessage() . ' URL: ' . $url); + return []; + } + } + + + + + + /** + * 将相对URL转换为绝对URL + * @param string $relativeUrl 相对URL + * @param string $baseUrl 基础URL + * @return string 绝对URL + */ + private function makeAbsoluteUrl($relativeUrl, $baseUrl) + { + if (empty($relativeUrl)) { + return ''; + } + + // 如果已经是绝对URL,直接返回 + if (filter_var($relativeUrl, FILTER_VALIDATE_URL)) { + return $relativeUrl; + } + + // 解析基础URL + $baseParts = parse_url($baseUrl); + if (!$baseParts) { + return $relativeUrl; + } + + // 处理以/开头的绝对路径 + if (strpos($relativeUrl, '/') === 0) { + return $baseParts['scheme'] . '://' . $baseParts['host'] . + (isset($baseParts['port']) ? ':' . $baseParts['port'] : '') . + $relativeUrl; + } + + // 处理相对路径 + $basePath = isset($baseParts['path']) ? dirname($baseParts['path']) : '/'; + if ($basePath === '.') { + $basePath = '/'; + } + + return $baseParts['scheme'] . '://' . $baseParts['host'] . + (isset($baseParts['port']) ? ':' . $baseParts['port'] : '') . + $basePath . '/' . $relativeUrl; + } + + /** + * 清理文本内容 + * @param string $text 要清理的文本 + * @return string 清理后的文本 + */ + private function cleanText($text) + { + if (empty($text)) { + return ''; + } + + // 移除HTML实体 + $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + + // 移除多余的空白字符 + $text = preg_replace('/\s+/', ' ', $text); + + // 移除控制字符 + $text = preg_replace('/[\x00-\x1F\x7F]/', '', $text); + + return trim($text); + } + + + + +} \ No newline at end of file diff --git a/Server/application/cunkebao/controller/WorkbenchController.php b/Server/application/cunkebao/controller/WorkbenchController.php index 86d80095..5bf682a6 100644 --- a/Server/application/cunkebao/controller/WorkbenchController.php +++ b/Server/application/cunkebao/controller/WorkbenchController.php @@ -2,6 +2,8 @@ namespace app\cunkebao\controller; +use app\common\model\Device as DeviceModel; +use app\common\model\DeviceWechatLogin as DeviceWechatLoginModel; use app\cunkebao\model\Workbench; use app\cunkebao\model\WorkbenchAutoLike; use app\cunkebao\model\WorkbenchMomentsSync; @@ -236,10 +238,16 @@ class WorkbenchController extends Controller $item->config = $item->momentsSync; $item->config->devices = json_decode($item->config->devices, true); $item->config->contentLibraries = json_decode($item->config->contentLibraries, true); - + //同步记录 + $sendNum = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $item->id])->count(); + $item->syncCount = $sendNum; + $lastTime = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $item->id])->order('id DESC')->value('createTime'); + $item->lastSyncTime = !empty($lastTime) ? date('Y-m-d H:i',$lastTime) : '--'; + + // 获取内容库名称 if (!empty($item->config->contentLibraries)) { - $libraryNames = ContentLibrary::where('id', 'in', $item->config->contentLibraries) + $libraryNames = ContentLibrary::whereIn('id', $item->config->contentLibraries) ->column('name'); $item->config->contentLibraryNames = $libraryNames; } else { @@ -426,6 +434,42 @@ class WorkbenchController extends Controller $workbench->config = $workbench->momentsSync; $workbench->config->devices = json_decode($workbench->config->devices, true); $workbench->config->contentLibraries = json_decode($workbench->config->contentLibraries, true); + + //同步记录 + $sendNum = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $workbench->id])->count(); + $workbench->syncCount = $sendNum; + $lastTime = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $workbench->id])->order('id DESC')->value('createTime'); + $workbench->lastSyncTime = !empty($lastTime) ? date('Y-m-d H:i',$lastTime) : '--'; + + // 获取内容库名称 + if (!empty($workbench->config->contentLibraries)) { + $libraryNames = ContentLibrary::where('id', 'in', $workbench->config->contentLibraries) + ->select(); + $workbench->config->contentLibraries = $libraryNames; + } else { + $workbench->config->contentLibraryNames = []; + } + + if(!empty($workbench->config->devices)){ + $deviceList = DeviceModel::alias('d') + ->field([ + 'd.id', 'd.imei', 'd.memo', 'd.alive', + 'l.wechatId', + 'a.nickname', 'a.alias', 'a.avatar','a.alias' + ]) + ->leftJoin('device_wechat_login l', 'd.id = l.deviceId and l.alive =' . DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE . ' and l.companyId = d.companyId') + ->leftJoin('wechat_account a', 'l.wechatId = a.wechatId') + ->whereIn('d.id',$workbench->config->devices) + ->order('d.id desc') + ->select(); + $workbench->config->deviceList = $deviceList; + }else{ + $workbench->config->deviceList = []; + } + + + + unset($workbench->momentsSync,$workbench->moments_sync); } break; @@ -635,6 +679,18 @@ class WorkbenchController extends Controller case self::TYPE_MOMENTS_SYNC: $config = WorkbenchMomentsSync::where('workbenchId', $param['id'])->find(); if ($config) { + if (!empty($param['contentLibraries'])){ + foreach ($param['contentLibraries'] as $library){ + if(isset($library['id']) && !empty($library['id'])){ + $contentLibraries[] = $library['id']; + }else{ + $contentLibraries[] = $library; + } + } + }else{ + $contentLibraries = []; + } + $config->syncInterval = $param['syncInterval']; $config->syncCount = $param['syncCount']; $config->syncType = $param['syncType']; @@ -642,7 +698,7 @@ class WorkbenchController extends Controller $config->endTime = $param['endTime']; $config->accountType = $param['accountType']; $config->devices = json_encode($param['devices']); - $config->contentLibraries = json_encode($param['contentLibraries'] ?? []); + $config->contentLibraries = json_encode($contentLibraries); $config->updateTime = time(); $config->save(); } diff --git a/Server/application/cunkebao/controller/chatroom/GetChatroomListV1Controller.php b/Server/application/cunkebao/controller/chatroom/GetChatroomListV1Controller.php index 83e50ee3..a08a5db9 100644 --- a/Server/application/cunkebao/controller/chatroom/GetChatroomListV1Controller.php +++ b/Server/application/cunkebao/controller/chatroom/GetChatroomListV1Controller.php @@ -21,25 +21,31 @@ class GetChatroomListV1Controller extends BaseController $limit = $this->request->param('limit', 20); $keyword = $this->request->param('keyword', ''); try { + + $wechatIds = Db::name('device')->alias('d') + ->join('device_wechat_login dwl','dwl.deviceId=d.id AND dwl.companyId='.$this->getUserInfo('companyId')) + ->where(['d.companyId' => $this->getUserInfo('companyId'),'d.deleteTime' => 0]) + ->column('dwl.wechatId'); + $where = []; if ($this->getUserInfo('isAdmin') == 1) { - $where[] = ['g.companyId', '=', $this->getUserInfo('companyId')]; $where[] = ['g.deleteTime', '=', 0]; + $where[] = ['g.ownerWechatId', 'in', $wechatIds]; } else { - $where[] = ['g.companyId', '=', $this->getUserInfo('companyId')]; $where[] = ['g.deleteTime', '=', 0]; + $where[] = ['g.ownerWechatId', 'in', $wechatIds]; //$where[] = ['g.userId', '=', $this->getUserInfo('id')]; } if(!empty($keyword)){ $where[] = ['g.name', 'like', '%'.$keyword.'%']; } - + $data = WechatChatroom::alias('g') ->field(['g.id', 'g.chatroomId', 'g.name', 'g.avatar','g.ownerWechatId', 'g.identifier', 'g.createTime', 'wa.nickname as ownerNickname','wa.avatar as ownerAvatar','wa.alias as ownerAlias']) - ->Join('wechat_account wa', 'g.ownerWechatId = wa.wechatId', 'LEFT') + ->join('wechat_account wa', 'g.ownerWechatId = wa.wechatId', 'LEFT') ->where($where); $total = $data->count(); diff --git a/Server/application/cunkebao/controller/friend/GetFriendListV1Controller.php b/Server/application/cunkebao/controller/friend/GetFriendListV1Controller.php index a6d12d6e..86965d79 100644 --- a/Server/application/cunkebao/controller/friend/GetFriendListV1Controller.php +++ b/Server/application/cunkebao/controller/friend/GetFriendListV1Controller.php @@ -38,23 +38,21 @@ class GetFriendListV1Controller extends BaseController $where[] = ['isDeleted','=',0]; } - if(!empty($keyword)){ $where[] = ['nickname|alias|wechatId','like','%'.$keyword.'%']; } + $wechatIds = Db::name('device')->alias('d') + ->join('device_wechat_login dwl','dwl.deviceId=d.id AND dwl.companyId='.$this->getUserInfo('companyId')) + ->where(['d.companyId' => $this->getUserInfo('companyId'),'d.deleteTime' => 0]); - $devices = Db::name('device_wechat_login') - ->where(['companyId' => $this->getUserInfo('companyId'),'alive' => 1]) - ->order('id desc') - ->group('wechatId'); - - if(!empty($deviceIds) && is_array($deviceIds)){ - $devices = $devices->whereIn('deviceId',$deviceIds); + if (!empty($deviceIds)){ + $wechatIds = $wechatIds->where('d.id','in',$deviceIds); } - $devices = $devices->column('wechatId'); + $wechatIds = $wechatIds->column('dwl.wechatId'); - $where[] = ['ownerWechatId','in',$devices]; + + $where[] = ['ownerWechatId','in',$wechatIds]; $data = Db::table('s2_wechat_friend') ->field(['nickname','avatar','alias','id','wechatId','ownerNickname','ownerAlias','ownerWechatId','createTime']) diff --git a/Server/application/cunkebao/controller/plan/PlanSceneV1Controller.php b/Server/application/cunkebao/controller/plan/PlanSceneV1Controller.php index 3d958815..d9bc41c3 100644 --- a/Server/application/cunkebao/controller/plan/PlanSceneV1Controller.php +++ b/Server/application/cunkebao/controller/plan/PlanSceneV1Controller.php @@ -5,6 +5,8 @@ namespace app\cunkebao\controller\plan; use library\ResponseHelper; use think\Db; use app\cunkebao\controller\BaseController; +use app\cunkebao\controller\plan\PosterWeChatMiniProgram; + /** * 获取计划任务列表控制器 */ @@ -54,18 +56,18 @@ class PlanSceneV1Controller extends BaseController $val['acquiredCount'] = Db::name('task_customer')->where('task_id',$val['id'])->count(); $val['addedCount'] = Db::name('task_customer')->where('task_id',$val['id'])->whereIn('status',[1,2,3,4])->count(); $val['passCount'] = Db::name('task_customer')->where('task_id',$val['id'])->where('status',4)->count(); - $val['passRate'] = 0; if(!empty($val['passCount']) && !empty($val['addedCount'])){ - $passRate = ($val['addedCount'] / $val['passCount']) * 100; + $passRate = ($val['passCount'] / $val['addedCount']) * 100; $val['passRate'] = number_format($passRate,2); } + $lastTime = Db::name('task_customer')->where(['task_id'=>$val['id']])->max('updated_at'); + $val['lastUpdated'] = !empty($lastTime) ? date('Y-m-d H:i', $lastTime) : '--'; + + } unset($val); - - - return ResponseHelper::success([ 'total' => $total, 'list' => $list @@ -75,41 +77,6 @@ class PlanSceneV1Controller extends BaseController } } - /** - * 拷贝计划任务 - * - * @return \think\response\Json - */ - public function copy() - { - try { - $params = $this->request->param(); - $planId = isset($params['planId']) ? intval($params['planId']) : 0; - - if ($planId <= 0) { - return ResponseHelper::error('计划ID不能为空', 400); - } - - $plan = Db::name('customer_acquisition_task')->where('id', $planId)->find(); - if (!$plan) { - return ResponseHelper::error('计划不存在', 404); - } - - unset($plan['id']); - $plan['name'] = $plan['name'] . ' (拷贝)'; - $plan['createTime'] = time(); - $plan['updateTime'] = time(); - - $newPlanId = Db::name('customer_acquisition_task')->insertGetId($plan); - if (!$newPlanId) { - return ResponseHelper::error('拷贝计划失败', 500); - } - - return ResponseHelper::success(['planId' => $newPlanId], '拷贝计划任务成功'); - } catch (\Exception $e) { - return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500); - } - } /** * 删除计划任务 @@ -163,4 +130,273 @@ class PlanSceneV1Controller extends BaseController return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500); } } + + /** + * 获取获客计划设备列表 + * + * @return \think\response\Json + */ + public function getPlanDevices() + { + try { + $params = $this->request->param(); + $planId = isset($params['planId']) ? intval($params['planId']) : 0; + $page = isset($params['page']) ? intval($params['page']) : 1; + $limit = isset($params['limit']) ? intval($params['limit']) : 10; + $deviceStatus = isset($params['deviceStatus']) ? $params['deviceStatus'] : ''; + $searchKeyword = isset($params['searchKeyword']) ? trim($params['searchKeyword']) : ''; + + // 验证计划ID + if ($planId <= 0) { + return ResponseHelper::error('计划ID不能为空', 400); + } + + // 验证计划是否存在且用户有权限 + $plan = Db::name('customer_acquisition_task') + ->where([ + 'id' => $planId, + 'deleteTime' => 0, + 'companyId' => $this->getUserInfo('companyId') + ]) + ->find(); + + if (!$plan) { + return ResponseHelper::error('计划不存在或无权限访问', 404); + } + + // 如果是管理员,需要验证用户权限 + if ($this->getUserInfo('isAdmin')) { + $userPlan = Db::name('customer_acquisition_task') + ->where([ + 'id' => $planId, + 'userId' => $this->getUserInfo('id') + ]) + ->find(); + + if (!$userPlan) { + return ResponseHelper::error('您没有权限访问该计划', 403); + } + } + + // 构建查询条件 + $where = [ + 'pt.plan_id' => $planId, + 'd.deleteTime' => 0, + 'd.companyId' => $this->getUserInfo('companyId') + ]; + + // 设备状态筛选 + if (!empty($deviceStatus)) { + $where['d.alive'] = $deviceStatus; + } + + // 搜索关键词 + $searchWhere = []; + if (!empty($searchKeyword)) { + $searchWhere[] = ['d.imei', 'like', "%{$searchKeyword}%"]; + $searchWhere[] = ['d.memo', 'like', "%{$searchKeyword}%"]; + } + + // 查询设备总数 + $totalQuery = Db::name('plan_task_device')->alias('pt') + ->join('device d', 'pt.device_id = d.id') + ->where($where); + + if (!empty($searchWhere)) { + $totalQuery->where(function ($query) use ($searchWhere) { + foreach ($searchWhere as $condition) { + $query->whereOr($condition[0], $condition[1], $condition[2]); + } + }); + } + + $total = $totalQuery->count(); + + // 查询设备列表 + $listQuery = Db::name('plan_task_device')->alias('pt') + ->join('device d', 'pt.device_id = d.id') + ->field([ + 'd.id', + 'd.imei', + 'd.memo', + 'd.alive', + 'd.extra', + 'd.createTime', + 'd.updateTime', + 'pt.status as plan_device_status', + 'pt.createTime as assign_time' + ]) + ->where($where) + ->order('pt.createTime', 'desc'); + + if (!empty($searchWhere)) { + $listQuery->where(function ($query) use ($searchWhere) { + foreach ($searchWhere as $condition) { + $query->whereOr($condition[0], $condition[1], $condition[2]); + } + }); + } + + $list = $listQuery->page($page, $limit)->select(); + + // 处理设备数据 + foreach ($list as &$device) { + // 格式化时间 + $device['createTime'] = date('Y-m-d H:i:s', $device['createTime']); + $device['updateTime'] = date('Y-m-d H:i:s', $device['updateTime']); + $device['assign_time'] = date('Y-m-d H:i:s', $device['assign_time']); + + // 解析设备额外信息 + if (!empty($device['extra'])) { + $extra = json_decode($device['extra'], true); + $device['battery'] = isset($extra['battery']) ? intval($extra['battery']) : 0; + $device['device_info'] = $extra; + } else { + $device['battery'] = 0; + $device['device_info'] = []; + } + + // 设备状态文本 + $device['alive_text'] = $this->getDeviceStatusText($device['alive']); + $device['plan_device_status_text'] = $this->getPlanDeviceStatusText($device['plan_device_status']); + + // 获取设备当前微信登录信息 + $wechatLogin = Db::name('device_wechat_login') + ->where([ + 'deviceId' => $device['id'], + 'companyId' => $this->getUserInfo('companyId'), + 'alive' => 1 + ]) + ->order('createTime', 'desc') + ->find(); + + $device['current_wechat'] = $wechatLogin ? [ + 'wechatId' => $wechatLogin['wechatId'], + 'nickname' => $wechatLogin['nickname'] ?? '', + 'loginTime' => date('Y-m-d H:i:s', $wechatLogin['createTime']) + ] : null; + + // 获取设备在该计划中的任务统计 + $device['task_stats'] = $this->getDeviceTaskStats($device['id'], $planId); + + // 移除原始extra字段 + unset($device['extra']); + } + unset($device); + + return ResponseHelper::success([ + 'total' => $total, + 'list' => $list, + 'plan_info' => [ + 'id' => $plan['id'], + 'name' => $plan['name'], + 'status' => $plan['status'] + ] + ], '获取计划设备列表成功'); + + } catch (\Exception $e) { + return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500); + } + } + + /** + * 获取设备状态文本 + * + * @param int $status + * @return string + */ + private function getDeviceStatusText($status) + { + $statusMap = [ + 0 => '离线', + 1 => '在线', + 2 => '忙碌', + 3 => '故障' + ]; + return isset($statusMap[$status]) ? $statusMap[$status] : '未知'; + } + + /** + * 获取计划设备状态文本 + * + * @param int $status + * @return string + */ + private function getPlanDeviceStatusText($status) + { + $statusMap = [ + 0 => '待分配', + 1 => '已分配', + 2 => '执行中', + 3 => '已完成', + 4 => '已暂停', + 5 => '已取消' + ]; + return isset($statusMap[$status]) ? $statusMap[$status] : '未知'; + } + + /** + * 获取设备在指定计划中的任务统计 + * + * @param int $deviceId + * @param int $planId + * @return array + */ + private function getDeviceTaskStats($deviceId, $planId) + { + // 获取该设备在计划中的任务总数 + $totalTasks = Db::name('task_customer') + ->where([ + 'task_id' => $planId, + 'device_id' => $deviceId + ]) + ->count(); + + // 获取已完成的任务数 + $completedTasks = Db::name('task_customer') + ->where([ + 'task_id' => $planId, + 'device_id' => $deviceId, + 'status' => 4 + ]) + ->count(); + + // 获取进行中的任务数 + $processingTasks = Db::name('task_customer') + ->where([ + 'task_id' => $planId, + 'device_id' => $deviceId, + 'status' => ['in', [1, 2, 3]] + ]) + ->count(); + + return [ + 'total_tasks' => $totalTasks, + 'completed_tasks' => $completedTasks, + 'processing_tasks' => $processingTasks, + 'completion_rate' => $totalTasks > 0 ? round(($completedTasks / $totalTasks) * 100, 2) : 0 + ]; + } + + + + public function getWxMinAppCode() + { + $params = $this->request->param(); + $taskId = isset($params['taskId']) ? intval($params['taskId']) : 0; + + if($taskId <= 0) { + return ResponseHelper::error('任务ID或场景ID不能为空', 400); + } + + $task = Db::name('customer_acquisition_task')->where(['id' => $taskId, 'deleteTime' => 0])->find(); + if(!$task) { + return ResponseHelper::error('任务不存在', 400); + } + + $posterWeChatMiniProgram = new PosterWeChatMiniProgram(); + $result = $posterWeChatMiniProgram->generateMiniProgramCodeWithScene($taskId); + return ResponseHelper::success($result, '获取小程序码成功'); + } + } \ No newline at end of file diff --git a/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php b/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php index 3c766e47..075a5217 100644 --- a/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php +++ b/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php @@ -16,15 +16,14 @@ class PosterWeChatMiniProgram extends Controller } const MINI_PROGRAM_CONFIG = [ - 'app_id' => 'wx12345678', - 'secret' => 'your-app-secret', - + 'app_id' => 'wx789850448e26c91d', + 'secret' => 'd18f75b3a3623cb40da05648b08365a1', 'response_type' => 'array' ]; // 生成小程序码,存客宝-操盘手调用 - public function generateMiniProgramCodeWithScene() { + public function generateMiniProgramCodeWithScene($taskId = '') { $taskId = request()->param('id'); @@ -106,7 +105,7 @@ class PosterWeChatMiniProgram extends Controller } // return $result['phone_info']['phoneNumber']; return json([ - 'code' => 0, + 'code' => 200, 'message' => '获取手机号成功', 'data' => $result['phone_info']['phoneNumber'] ]); @@ -125,12 +124,45 @@ class PosterWeChatMiniProgram extends Controller // todo 获取海报获客任务的任务/海报数据 -- 表还没设计好,不急 ck_customer_acquisition_task public function getPosterTaskData() { $id = request()->param('id'); - $task = Db::name('customer_acquisition_task')->where('id', $id)->find(); + $task = Db::name('customer_acquisition_task')->where(['id' => $id,'deleteTime' => 0])->find(); + if (!$task) { + return json([ + 'code' => 400, + 'message' => '任务不存在' + ]); + } + + if($task['status'] == 0) { + return json([ + 'code' => 400, + 'message' => '任务已结束' + ]); + } + + $sceneConf = json_decode($task['sceneConf'], true); + + if(isset($sceneConf['posters'][0]['preview'])) { + $posterUrl = $sceneConf['posters'][0]['preview']; + } else { + $posterUrl = ''; + } + + + + + $data = [ + 'id' => $task['id'], + 'name' => $task['name'], + 'poster' => ['sUrl' => $posterUrl], + 'sTip' => '啦啦啦啦', + ]; + + // todo 只需 返回 poster_url success_tip return json([ - 'code' => 0, + 'code' => 200, 'message' => '获取海报获客任务数据成功', - 'data' => $task + 'data' => $data ]); } diff --git a/Server/application/job/AccountListJob.php b/Server/application/job/AccountListJob.php index 08f23142..6d76ab3d 100644 --- a/Server/application/job/AccountListJob.php +++ b/Server/application/job/AccountListJob.php @@ -73,7 +73,7 @@ class AccountListJob $request->withGet($params); // 调用公司账号列表获取方法 - $result = $accountController->getlist($pageIndex,$pageSize,true); + $result = $accountController->getlist(['pageIndex' => $pageIndex,'pageSize' => $pageSize],true); $response = json_decode($result,true); diff --git a/Server/application/job/WechatMomentsJob.php b/Server/application/job/WechatMomentsJob.php index 9042783b..2aabff4f 100644 --- a/Server/application/job/WechatMomentsJob.php +++ b/Server/application/job/WechatMomentsJob.php @@ -18,13 +18,12 @@ class WechatMomentsJob public function fire(Job $job, $data) { $toAccountId = ''; - $username = Env::get('api.username', ''); - $password = Env::get('api.password', ''); + $username = Env::get('api.username2', ''); + $password = Env::get('api.password2', ''); if (!empty($username) || !empty($password)) { $toAccountId = Db::name('users')->where('account',$username)->value('s2_accountId'); }else{ Log::error("没有账号配置"); - Cache::rm($queueLockKey); return; } @@ -47,6 +46,8 @@ class WechatMomentsJob // 执行切换好友命令 $automaticAssign = new AutomaticAssign(); $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['friendId'], 'toAccountId' => $toAccountId], true); + //存入缓存 + artificialAllotWechatFriend($friend); // 执行采集朋友圈命令 $webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]); @@ -54,6 +55,8 @@ class WechatMomentsJob // 处理完毕切换回原账号 $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['friendId'], 'toAccountId' => $friend['accountId']], true); + + } catch (\Exception $e) { // 发生异常时也要切换回原账号 $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['friendId'], 'toAccountId' => $friend['accountId']], true); diff --git a/Server/application/job/WorkbenchAutoLikeJob.php b/Server/application/job/WorkbenchAutoLikeJob.php index 351b6b6b..11c8d84c 100644 --- a/Server/application/job/WorkbenchAutoLikeJob.php +++ b/Server/application/job/WorkbenchAutoLikeJob.php @@ -209,8 +209,8 @@ class WorkbenchAutoLikeJob protected function processFriendMoments($workbench, $config, $friend) { $toAccountId = ''; - $username = Env::get('api.username', ''); - $password = Env::get('api.password', ''); + $username = Env::get('api.username2', ''); + $password = Env::get('api.password2', ''); if (!empty($username) || !empty($password)) { $toAccountId = Db::name('users')->where('account',$username)->value('s2_accountId'); } @@ -219,7 +219,8 @@ class WorkbenchAutoLikeJob // 执行切换好友命令 $automaticAssign = new AutomaticAssign(); $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['friendId'], 'toAccountId' => $toAccountId], true); - + //存入缓存 + artificialAllotWechatFriend($friend); // 创建WebSocket链接 $webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]); diff --git a/Server/application/job/WorkbenchMomentsJob.php b/Server/application/job/WorkbenchMomentsJob.php index a24baf8a..a62ca783 100644 --- a/Server/application/job/WorkbenchMomentsJob.php +++ b/Server/application/job/WorkbenchMomentsJob.php @@ -85,6 +85,7 @@ class WorkbenchMomentsJob // 获取设备 $devices = $this->getDevice($workbench, $config); + if (empty($devices)) { continue; } @@ -94,7 +95,6 @@ class WorkbenchMomentsJob if (empty($contentLibrary)) { continue; } - // 处理内容发送 $this->handleContentSend($workbench, $config, $devices, $contentLibrary); } @@ -234,27 +234,30 @@ class WorkbenchMomentsJob $newList = []; foreach ($list as $val) { - // 检查今日发送次数 + // 检查发送间隔(新逻辑:根据startTime、endTime、syncCount动态计算) + $today = date('Y-m-d'); + $startTimestamp = strtotime($today . ' ' . $config['startTime'] . ':00'); + $endTimestamp = strtotime($today . ' ' . $config['endTime'] . ':00'); + $totalSeconds = $endTimestamp - $startTimestamp; + if ($totalSeconds <= 0 || empty($config['syncCount'])) { + continue; + } + $interval = floor($totalSeconds / $config['syncCount']); + + // 查询今日已同步次数 $count = Db::name('workbench_moments_sync_item') ->where('workbenchId', $workbench->id) ->where('deviceId', $val['deviceId']) - ->whereTime('createTime', 'between', [ - strtotime(date('Y-m-d') . '00:00:00'), - strtotime(date('Y-m-d') . '23:59:59') - ])->count(); + ->whereTime('createTime', 'between', [$startTimestamp, $endTimestamp]) + ->count(); if ($count >= $config['syncCount']) { continue; } - // 检查发送间隔 - $prevSend = Db::name('workbench_moments_sync_item') - ->where('workbenchId', $workbench->id) - ->where('deviceId', $val['deviceId']) - ->order('createTime DESC') - ->find(); - - if (!empty($prevSend) && ($prevSend['createTime'] + $config['syncInterval'] * 60) > time()) { + // 计算本次同步的最早允许时间 + $nextSyncTime = $startTimestamp + $count * $interval; + if (time() < $nextSyncTime) { continue; } diff --git a/Server/application/job/WorkbenchTrafficDistributeJob.php b/Server/application/job/WorkbenchTrafficDistributeJob.php index 674a54a0..53e26d54 100644 --- a/Server/application/job/WorkbenchTrafficDistributeJob.php +++ b/Server/application/job/WorkbenchTrafficDistributeJob.php @@ -58,7 +58,7 @@ class WorkbenchTrafficDistributeJob protected function processSingleWorkbench($workbench) { $page = 1; - $pageSize = 100; + $pageSize = 20; $config = WorkbenchTrafficConfig::where('workbenchId', $workbench->id)->find(); if (!$config) { @@ -89,8 +89,6 @@ class WorkbenchTrafficDistributeJob ->group('a.id') ->having('todayCount <= ' . $config['maxPerDay']) ->select(); - - $accountNum = count($accounts); if ($accountNum < 1) { Log::info("流量分发工作台 {$workbench->id} 可分配账号少于1个"); @@ -134,27 +132,33 @@ class WorkbenchTrafficDistributeJob } // 执行切换好友命令 - $automaticAssign->allotWechatFriend([ + $res = $automaticAssign->allotWechatFriend([ 'wechatFriendId' => $friend['id'], 'toAccountId' => $account['id'] ], true); - Db::table('s2_wechat_friend') - ->where('id',$friend['id']) - ->update([ - 'accountId' => $account['id'], - 'accountUserName' => $account['userName'], - 'accountRealName' => $account['realName'], - 'accountNickname' => $account['nickname'], + + $res = json_decode($res,true); + if ($res['code'] == 200){ + Db::table('s2_wechat_friend') + ->where('id',$friend['id']) + ->update([ + 'accountId' => $account['id'], + 'accountUserName' => $account['userName'], + 'accountRealName' => $account['realName'], + 'accountNickname' => $account['nickname'], ]); - // 写入分配记录表 - Db::name('workbench_traffic_config_item')->insert([ - 'workbenchId' => $workbench->id, - 'deviceId' => $friend['deviceId'], - 'wechatFriendId' => $friend['id'], - 'wechatAccountId' => $account['id'], - 'createTime' => time() - ]); - Log::info("流量分发工作台 {$workbench->id} 好友[{$friend['id']}]分配给客服[{$account['id']}] 成功"); + // 写入分配记录表 + Db::name('workbench_traffic_config_item')->insert([ + 'workbenchId' => $workbench->id, + 'deviceId' => $friend['deviceId'], + 'wechatFriendId' => $friend['id'], + 'wechatAccountId' => $account['id'], + 'createTime' => time(), + 'exp' => $config['exp'], + 'expTime' => time() + 86400 * $config['exp'], + ]); + Log::info("流量分发工作台 {$workbench->id} 好友[{$friend['id']}]分配给客服[{$account['id']}] 成功"); + } $i++; } break; @@ -204,7 +208,7 @@ class WorkbenchTrafficDistributeJob $query = Db::table('s2_wechat_friend')->alias('wf') ->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left') ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') - ->join('workbench_traffic_config_item wtci', 'wtci.wechatFriendId = wf.id AND wtci.workbenchId = ' . $config['workbenchId'], 'left') + ->join('workbench_traffic_config_item wtci', 'wtci.isRecycle = 0 and wtci.wechatFriendId = wf.id AND wtci.workbenchId = ' . $config['workbenchId'], 'left') ->where([ ['wf.isDeleted', '=', 0], ['wf.isPassed', '=', 1], @@ -214,12 +218,6 @@ class WorkbenchTrafficDistributeJob ->whereIn('wa.currentDeviceId', $devices) ->field('wf.id,wf.wechatAccountId,wf.wechatId,wf.labels,sa.userName,wa.currentDeviceId as deviceId'); - //lllll - if($workbench->id == 65){ - $query->where('wf.accountId',1602); - } - - if(!empty($labels)){ $query->where(function ($q) use ($labels) { diff --git a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php index e1cc561e..495b7ae7 100644 --- a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php +++ b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php @@ -153,25 +153,43 @@ class Adapter implements WeChatServiceInterface public function handleCustomerTaskWithStatusIsNew(int $current_worker_id, int $process_count_for_status_0) { - $tasks = Db::name('task_customer') - ->where('status', 0) + $task = Db::name('customer_acquisition_task') + ->where(['status' => 1,'deleteTime' => 0]) ->whereRaw("id % $process_count_for_status_0 = {$current_worker_id}") - ->limit(50) - ->order('id DESC') + ->order('id desc') ->select(); + if (empty($task)) { + return false; + } - if ($tasks) { + $taskData = []; + foreach ($task as $item) { + $reqConf = json_decode($item['reqConf'], true); + $device = $reqConf['device'] ?? []; + $deviceCount = count($device); + if ($deviceCount <= 0){ + continue; + } + $tasks = Db::name('task_customer') + ->where(['status'=> 0,'task_id'=>$item['id']]) + ->order('id DESC') + ->limit($deviceCount) + ->select(); + $taskData = array_merge($taskData, $tasks); + } - foreach ($tasks as $task) { + 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; } - + //筛选出设备在线微信在线 $wechatIdAccountIdMap = $this->getWeChatIdsAccountIdsMapByDeviceIds($task_info['reqConf']['device']); if (empty($wechatIdAccountIdMap)) { @@ -235,8 +253,10 @@ class Adapter implements WeChatServiceInterface { $tasks = Db::name('task_customer') - ->whereIn('status', [1,3]) + ->whereIn('status', [1,2]) + ->where('updated_at', '>=', (time() - 86400 * 3)) ->limit(50) + ->order('updated_at DESC') ->select(); if (empty($tasks)) { @@ -272,15 +292,14 @@ class Adapter implements WeChatServiceInterface - if ($passedWeChatId && !empty($task_info['msgConf'])) { + Db::name('task_customer') ->where('id', $task['id']) ->update(['status' => 4, 'updated_at' => time()]); $wechatFriendRecord = $this->getWeChatAccoutIdAndFriendIdByWeChatIdAndFriendPhone($passedWeChatId, $task['phone']); $msgConf = is_string($task_info['msgConf']) ? json_decode($task_info['msgConf'], 1) : $task_info['msgConf']; - $wechatFriendRecord && $this->sendMsgToFriend($wechatFriendRecord['id'], $wechatFriendRecord['wechatAccountId'], $msgConf); } else { @@ -451,6 +470,7 @@ class Adapter implements WeChatServiceInterface try { $id = Db::table('s2_wechat_friend') ->where('ownerWechatId', $wxId) + ->where(['isPassed' => 1,'isDeleted' => 0]) ->where('phone|alias|wechatId', 'like', $phone . '%') ->order('createTime', 'desc') ->value('id'); @@ -598,6 +618,11 @@ class Adapter implements WeChatServiceInterface if (empty($wechatId)) { return false; } + //强制请求添加好友的列表 + $friendController = new FriendTaskController(); + $friendController->getlist(0,50); + + $record = $this->getLatestFriendTask($wechatId); if (empty($record)) { @@ -676,6 +701,7 @@ class Adapter implements WeChatServiceInterface } $records = Db::table('s2_wechat_account') ->where('deviceAlive', 1) + ->where('wechatAlive', 1) ->where('currentDeviceId', 'in', $deviceIds) ->field('id,wechatId') ->column('id,wechatId'); diff --git a/nkebao/src/api/utils.ts b/nkebao/src/api/utils.ts index 585d6395..c678120c 100644 --- a/nkebao/src/api/utils.ts +++ b/nkebao/src/api/utils.ts @@ -161,4 +161,35 @@ class RequestCancelManager { // 导出单例实例 export const requestDeduplicator = new RequestDeduplicator(); -export const requestCancelManager = new RequestCancelManager(); \ No newline at end of file +export const requestCancelManager = new RequestCancelManager(); + +/** + * 通用文件上传方法(支持图片、文件) + * @param {File} file - 要上传的文件对象 + * @param {string} [uploadUrl='/v1/attachment/upload'] - 上传接口地址 + * @returns {Promise} - 上传成功后返回文件url + */ +export async function uploadFile(file: File, uploadUrl: string = '/v1/attachment/upload'): Promise { + const formData = new FormData(); + formData.append('file', file); + const token = typeof window !== 'undefined' ? localStorage.getItem('token') : ''; + const headers: Record = {}; + if (token) headers['Authorization'] = `Bearer ${token}`; + try { + const response = await fetch(uploadUrl, { + method: 'POST', + headers, + body: formData, + }); + const res = await response.json(); + if (res?.url) { + return res.url; + } + if (res?.data?.url) { + return res.data.url; + } + throw new Error(res?.msg || '文件上传失败'); + } catch (e: any) { + throw new Error(e?.message || '文件上传失败'); + } +} \ No newline at end of file diff --git a/nkebao/src/pages/scenarios/new/steps/BasicSettings.tsx b/nkebao/src/pages/scenarios/new/steps/BasicSettings.tsx index 704af472..b62d7064 100644 --- a/nkebao/src/pages/scenarios/new/steps/BasicSettings.tsx +++ b/nkebao/src/pages/scenarios/new/steps/BasicSettings.tsx @@ -304,6 +304,7 @@ export function BasicSettings({ // 新增:用于文件选择的ref const uploadInputRef = useRef(null); + const uploadOrderInputRef = useRef(null); // 更新电话获客设置 const handlePhoneSettingsUpdate = () => { @@ -648,6 +649,9 @@ export function BasicSettings({ transition: "border 0.2s", textAlign: "center", position: "relative", + height: 180 + 12, // 图片高度180+上下padding + overflow: "hidden", + minHeight: 192, }} onClick={() => handleMaterialSelect(material)} > @@ -675,40 +679,31 @@ export function BasicSettings({ {/* 删除自定义海报按钮 */} {isCustom && ( - + × + )}
{material.name} @@ -753,7 +751,7 @@ export function BasicSettings({ flexDirection: "column", alignItems: "center", justifyContent: "center", - height: 220, + height: 190, }} onClick={() => uploadInputRef.current?.click()} > @@ -801,62 +799,75 @@ export function BasicSettings({ index={0} />
- {/* 订单导入区块 */} + {/* 订单导入区块优化 */}
- - -
- 导入订单 -
-
- +
订单表格上传
+
+ +
- {importedTags.length > 0 && ( - - )} - + + +
+ 支持 CSV、Excel 格式,上传后将文件保存到服务器 +
+ {/* 已导入数据表格可复用原有Table渲染 */} + {importedTags.length > 0 && ( +
+ )} {/* 电话获客设置区块,仅在选择电话获客场景时显示 */} - {formData.scenario === "phone" && ( + {formData.scenario === 5 && (
- - -
+
电话获客设置
-
+
-
-
+
)} {/* 微信群设置区块,仅在选择微信群场景时显示 */} - {formData.scenario === "weixinqun" && ( + {formData.scenario === 7 && (
)} -