baseUrl = Env::get('api.wechat_url'); $this->authorization = AuthService::getSystemAuthorization(); } private function resolveConversationWechatAccountId( int $accountId, int $wechatFriendId, int $wechatChatroomId ): int { $companyId = (int)$this->getUserInfo('companyId'); if ($wechatChatroomId > 0) { $row = Db::table('s2_wechat_chatroom') ->where('id', $wechatChatroomId) ->where('isDeleted', 0) ->field('wechatAccountId,accountId') ->find(); if (empty($row)) { return 0; } $wechatAccountId = (int)($row['wechatAccountId'] ?? 0); if ($wechatAccountId <= 0) { return 0; } if ((int)($row['accountId'] ?? 0) === $accountId) { return $wechatAccountId; } return $this->isWechatAccountInKefuScope($companyId, $accountId, $wechatAccountId) ? $wechatAccountId : 0; } if ($wechatFriendId > 0) { $friend = Db::table('s2_wechat_friend') ->where('id', $wechatFriendId) ->where('isDeleted', 0) ->field('wechatAccountId,accountId') ->find(); if (empty($friend)) { return 0; } $wechatAccountId = (int)($friend['wechatAccountId'] ?? 0); if ($wechatAccountId <= 0) { return 0; } if ((int)($friend['accountId'] ?? 0) === $accountId) { return $wechatAccountId; } return $this->isWechatAccountInKefuScope($companyId, $accountId, $wechatAccountId) ? $wechatAccountId : 0; } return 0; } /** 与 ensure-session 侧栏口径一致:本项目设备链微信号可跨 accountId 代发 */ private function isWechatAccountInKefuScope(int $companyId, int $accountId, int $wechatAccountId): bool { if ($wechatAccountId <= 0 || $companyId <= 0) { return false; } $scopeIds = CompanyWechatScopeService::resolveKefuSidebarAccountIds($companyId, $accountId); return !empty($scopeIds) && in_array($wechatAccountId, $scopeIds, true); } /** * POST /v1/kefu/message/ensure-session(CKB-141 · 141.4 进入聊天链路) * * 好友管理「进入聊天」前置:权限校验 → 会话补建 → 返回定位上下文。 * body: { friendId, wechatAccountId? } * 失败原因明确返回:NOT_FOUND / NOT_IN_COMPANY / OWNED_BY_OTHER。 */ public function ensureSession() { try { $accountId = (int)$this->getUserInfo('s2_accountId'); $companyId = (int)$this->getUserInfo('companyId'); if ($accountId <= 0) { return ResponseHelper::error('请先登录', 401); } $friendId = (int)$this->request->param('friendId', 0); if ($friendId <= 0) { return ResponseHelper::error('缺少 friendId', 400); } $friend = Db::table('s2_wechat_friend') ->where('id', $friendId) ->where('isDeleted', 0) ->find(); if (!$friend) { return ResponseHelper::error('好友不存在或已删除(NOT_FOUND)', 404); } $friendWechatAccountId = (int)($friend['wechatAccountId'] ?? 0); // 权限校验:与好友管理页同一口径(项目设备链微信号,无则个人 IM 号兜底)(141.4 步骤 2) $scopeAccountIds = CompanyWechatScopeService::resolveKefuSidebarAccountIds($companyId, $accountId); if (!empty($scopeAccountIds) && !in_array($friendWechatAccountId, $scopeAccountIds, true)) { return ResponseHelper::error('好友不属于本项目微信号,无聊天权限(NOT_IN_COMPANY)', 403); } // 归属判定(141.4 步骤 6:失败原因明确;141.3 管理员可触达全部) $isAdmin = (int)$this->getUserInfo('isAdmin') === 1; $ownerAccountId = (int)($friend['accountId'] ?? 0); $ownedByOther = $ownerAccountId > 0 && $ownerAccountId !== $accountId; $ownerName = ''; if ($ownedByOther) { try { $owner = Db::name('users') ->where('s2_accountId', $ownerAccountId) ->where('deleteTime', 0) ->field('account,username') ->find(); $ownerName = $owner ? (string)($owner['username'] ?: $owner['account']) : ''; } catch (\Throwable $e) { } } // 初始分配兜底:未分配好友领取到当前客服(141.2 初始分配 / V5) $assigned = false; if ($ownerAccountId <= 0) { Db::table('s2_wechat_friend')->where('id', $friendId)->update(['accountId' => $accountId]); $friend['accountId'] = $accountId; $assigned = true; Log::info('[ensure-session] friend auto-assigned', [ 'friendId' => $friendId, 'toAccountId' => $accountId, 'companyId' => $companyId, ]); } $blocked = $ownedByOther && !$isAdmin; // 会话补建:无任何消息时插入一条系统消息,使 message/list 聚合出该会话(141.4 步骤 3 / V6) $sessionCreated = false; if (!$blocked) { $hasMsg = Db::table('s2_wechat_message') ->where('type', 1) ->where('wechatFriendId', $friendId) ->limit(1) ->value('id'); if (!$hasMsg) { $now = time(); // s2_wechat_message.id 为同步主键(非自增),手工取 MAX+1 $nextId = (int)Db::table('s2_wechat_message')->max('id') + 1; Db::table('s2_wechat_message')->insert([ 'id' => $nextId, 'type' => 1, 'wechatFriendId' => $friendId, 'wechatChatroomId' => 0, 'wechatAccountId' => $friendWechatAccountId, 'accountId' => (int)$friend['accountId'], 'tenantId' => (int)($friend['tenantId'] ?? 0), 'content' => '[系统] 会话已建立,可直接发起聊天', 'originalContent' => '[系统] 会话已建立,可直接发起聊天', 'msgType' => 1, 'msgSubType' => 0, 'isSend' => 1, 'isRead' => 1, 'createTime' => $now, 'wechatTime' => $now, 'sendStatus' => 1, ]); $sessionCreated = true; } } return ResponseHelper::success([ 'friendId' => $friendId, 'wechatAccountId' => $friendWechatAccountId, 'wechatId' => (string)($friend['wechatId'] ?? ''), 'nickname' => (string)($friend['nickname'] ?? ''), 'conRemark' => (string)($friend['conRemark'] ?? ''), 'avatar' => (string)($friend['avatar'] ?? ''), 'canChat' => !$blocked, 'ownedByOther' => $ownedByOther, 'ownerAccountId' => $ownerAccountId, 'ownerName' => $ownerName, 'assigned' => $assigned, 'sessionCreated' => $sessionCreated, 'reason' => $blocked ? 'OWNED_BY_OTHER' : '', ]); } catch (\Throwable $e) { return ResponseHelper::error('进入聊天预检失败:' . $e->getMessage(), 500); } } /** * POST /v1/kefu/message/send * * 触客宝专用发送适配层:只做权限、对象解析、SDK 调用与本地消息落库。 * 工作手机能力仍由 WorkPhoneSDK / /v1/workphone/* 负责,避免业务域耦合。 */ public function send() { try { $accountId = (int)$this->getUserInfo('s2_accountId'); if ($accountId <= 0) { return ResponseHelper::error('请先登录', 401); } $wechatFriendId = (int)$this->request->param('wechatFriendId/d', 0); $wechatChatroomId = (int)$this->request->param('wechatChatroomId/d', 0); $content = trim((string)$this->request->param('content', '')); $msgType = (int)$this->request->param('msgType/d', 1); $seq = (int)$this->request->param('seq/d', 0); $channel = trim((string)$this->request->param('channel', '')); if ($wechatFriendId <= 0 && $wechatChatroomId <= 0) { return ResponseHelper::error('wechatFriendId 或 wechatChatroomId 至少传一个', 400); } if ($content === '') { return ResponseHelper::error('消息内容不能为空', 400); } $wechatAccountId = $this->resolveConversationWechatAccountId($accountId, $wechatFriendId, $wechatChatroomId); if ($wechatAccountId <= 0) { return ResponseHelper::error('会话不存在或无权限', 404); } $target = $this->resolveSendTarget($accountId, $wechatAccountId, $wechatFriendId, $wechatChatroomId); if (empty($target['toId'])) { return ResponseHelper::error('未找到接收方微信标识', 404); } $companyId = (int)$this->getUserInfo('companyId'); $resolved = WorkphoneMessageDeviceResolver::resolveForWechatAccount($companyId, $wechatAccountId); $deviceId = (string)($resolved['deviceId'] ?? ''); if ($deviceId === '') { $hint = trim((string)($resolved['reason'] ?? '')); return ResponseHelper::error( $hint !== '' ? $hint : '未找到工作手机设备标识,请先在设备管理绑定该微信号', 422 ); } // 安全门禁:禁止客户端 device_id 绕过 Resolver 跨设备代发 $requestedDeviceId = trim((string)$this->request->param('device_id', $this->request->param('deviceId', ''))); if ($requestedDeviceId !== '' && strcasecmp($requestedDeviceId, $deviceId) !== 0) { Log::warning('[TouchkebaoMessageSend] device_id mismatch blocked', [ 'companyId' => $companyId, 'wechatAccountId' => $wechatAccountId, 'requested' => $requestedDeviceId, 'resolved' => $deviceId, 'accountId' => $accountId, ]); return ResponseHelper::error('设备标识与项目解析结果不一致,禁止跨设备代发', 403); } // 142-P1:发消息前校验会话微信号 == 工作手机当前 Hook 登录微信 $isAdmin = (int)$this->getUserInfo('isAdmin') === 1; $skipAccountCheck = $isAdmin && (int)$this->request->param('skipAccountCheck/d', 0) === 1; if (!$skipAccountCheck) { $check = $this->verifyWechatAccountOnDevice($wechatAccountId, $deviceId); if ($check['checked'] && !$check['matched']) { return ResponseHelper::error( '工作手机当前登录微信为「' . $check['hookNickname'] . '」,与会话归属微信「' . $check['expectNickname'] . '」不一致,' . '请先在真机切换对应微信后再发送', 409 ); } } $messageId = $seq > 0 ? $seq : (int)floor(microtime(true) * 1000); $sdkResp = WorkPhoneSDK::getInstance()->sendMessage( $deviceId, 'wechat', (string)$target['toId'], $content, $this->mapWorkPhoneMsgType($msgType), $channel !== '' ? $channel : null ); $sdkOk = $this->isWorkphoneSendSuccessful($sdkResp); $sendStatus = $sdkOk ? 1 : 2; $this->upsertLocalOutgoingMessage([ 'id' => $messageId, 'type' => $wechatChatroomId > 0 ? 2 : 1, 'accountId' => $accountId, 'wechatAccountId' => $wechatAccountId, 'wechatFriendId' => $wechatFriendId, 'wechatChatroomId' => $wechatChatroomId, 'content' => $content, 'msgType' => $msgType, 'sendStatus' => $sendStatus, ]); return ResponseHelper::success([ 'messageId' => $messageId, 'seq' => $messageId, 'sendStatus' => $sendStatus, 'deviceId' => $deviceId, 'toId' => $target['toId'], 'targetName' => $target['name'], 'wechatAccountId' => $wechatAccountId, 'sdk' => $sdkResp, ], $sdkOk ? '消息已提交工作手机,等待微信回执' : '工作手机返回失败,已记录失败消息'); } catch (\Throwable $e) { Log::error('[TouchkebaoMessageSend] ' . $e->getMessage()); return ResponseHelper::error($e->getMessage(), 500); } } /** * 142-P1:校验工作手机当前 Hook 登录微信是否等于会话归属微信。 * Hook 探测失败/超时不阻断发送(checked=false);命中缓存 60s 降低真机 RPC 频次。 * * @return array{checked:bool,matched:bool,hookNickname:string,expectNickname:string} */ private function verifyWechatAccountOnDevice(int $wechatAccountId, string $deviceId): array { $result = ['checked' => false, 'matched' => true, 'hookNickname' => '', 'expectNickname' => '']; try { $expect = Db::table('s2_wechat_account') ->where('id', $wechatAccountId) ->field('wechatId,nickname,alias') ->find(); if (empty($expect) || empty($expect['wechatId'])) { return $result; } $result['expectNickname'] = (string)($expect['nickname'] ?: $expect['wechatId']); $cacheKey = 'tkb:hook_wxid:' . strtolower($deviceId); $hook = \think\facade\Cache::get($cacheKey); if (!is_array($hook)) { $resp = WorkPhoneSDK::getInstance()->getProfile($deviceId, 'wechat'); $profile = $resp['data']['profile'] ?? $resp['data']['data']['profile'] ?? $resp['profile'] ?? []; if (!is_array($profile)) { $profile = []; } $hook = [ 'wxid' => trim((string)($profile['wxid'] ?? ($profile['wechat_id'] ?? ''))), 'alias' => trim((string)($profile['alias'] ?? '')), 'nickname' => trim((string)($profile['nickname'] ?? '')), ]; \think\facade\Cache::set($cacheKey, $hook, 60); } if ($hook['wxid'] === '' && $hook['alias'] === '') { // Hook 未返回当前微信(未登录/通道不可用)→ 不阻断 return $result; } $result['checked'] = true; $result['hookNickname'] = $hook['nickname'] !== '' ? $hook['nickname'] : ($hook['wxid'] ?: $hook['alias']); $expectIds = array_filter([ strtolower((string)$expect['wechatId']), strtolower((string)($expect['alias'] ?? '')), ]); $hookIds = array_filter([strtolower($hook['wxid']), strtolower($hook['alias'])]); $result['matched'] = count(array_intersect($expectIds, $hookIds)) > 0; } catch (\Throwable $e) { // 探测异常不阻断发送 } return $result; } private function resolveSendTarget(int $accountId, int $wechatAccountId, int $wechatFriendId, int $wechatChatroomId): array { if ($wechatChatroomId > 0) { $row = Db::table('s2_wechat_chatroom') ->where([ 'id' => $wechatChatroomId, 'accountId' => $accountId, 'wechatAccountId' => $wechatAccountId, 'isDeleted' => 0, ]) ->field('id,nickname,chatroomId') ->find(); return [ 'toId' => $row['chatroomId'] ?? '', 'name' => $row['nickname'] ?? '', ]; } $row = Db::table('s2_wechat_friend') ->where([ 'id' => $wechatFriendId, 'wechatAccountId' => $wechatAccountId, 'isDeleted' => 0, ]) ->field('id,nickname,conRemark,wechatId,alias') ->find(); $displayName = trim((string)($row['conRemark'] ?? '')); if ($displayName === '') { $displayName = trim((string)($row['nickname'] ?? '')); } return [ 'toId' => $row['wechatId'] ?? ($row['alias'] ?? ''), 'name' => $displayName, ]; } private function resolveWorkPhoneDeviceId(int $wechatAccountId): string { $account = Db::table('s2_wechat_account') ->where('id', $wechatAccountId) ->field('id,wechatId,currentDeviceId,imei,deviceAccountId') ->find(); if (empty($account)) { return ''; } $candidates = []; foreach (['currentDeviceId', 'deviceAccountId', 'imei'] as $key) { if (!empty($account[$key])) { $candidates[] = (string)$account[$key]; } } if (!empty($account['wechatId'])) { $login = Db::name('device_wechat_login')->alias('l') ->join('device d', 'd.id = l.deviceId', 'left') ->where('l.wechatId', $account['wechatId']) ->where('d.deleteTime', 0) ->field('l.deviceId,d.imei,d.deviceImei,d.extra') ->order('l.alive desc,l.updateTime desc') ->find(); if (!empty($login)) { foreach (['deviceId', 'imei', 'deviceImei'] as $key) { if (!empty($login[$key])) { $candidates[] = (string)$login[$key]; } } $candidates = array_merge($candidates, $this->extractDeviceIdentifiers($login['extra'] ?? '')); } } $device = Db::name('device') ->where(function ($q) use ($account) { if (!empty($account['imei'])) { $q->where('imei', $account['imei'])->whereOr('deviceImei', $account['imei']); } else { $q->where('id', (int)($account['currentDeviceId'] ?? 0)); } }) ->where('deleteTime', 0) ->field('id,imei,deviceImei,extra') ->find(); if (!empty($device)) { foreach (['id', 'imei', 'deviceImei'] as $key) { if (!empty($device[$key])) { $candidates[] = (string)$device[$key]; } } $candidates = array_merge($candidates, $this->extractDeviceIdentifiers($device['extra'] ?? '')); } $resolved = $this->matchSdkDeviceIdentifier($candidates); return $resolved !== '' ? $resolved : (string)($candidates[0] ?? ''); } private function extractDeviceIdentifiers($extra): array { $decoded = is_array($extra) ? $extra : json_decode((string)$extra, true); if (!is_array($decoded)) { return []; } $ids = []; foreach (['device_id', 'deviceId', 'device_id_md5', 'deviceIdMd5', 'serial', 'adb_serial', 'adbSerial', 'ws_id'] as $key) { if (!empty($decoded[$key])) { $ids[] = (string)$decoded[$key]; } } return $ids; } private function matchSdkDeviceIdentifier(array $candidates): string { $candidateMap = []; foreach ($candidates as $candidate) { $key = strtolower(trim((string)$candidate)); if ($key !== '') { $candidateMap[$key] = (string)$candidate; } } if (empty($candidateMap)) { return ''; } try { $resp = WorkPhoneSDK::getInstance()->getDevices(); $devices = is_array($resp['data'] ?? null) ? $resp['data'] : []; foreach ($devices as $device) { if (!is_array($device)) { continue; } $status = strtolower((string)($device['status'] ?? '')); foreach (['device_id', 'id', 'deviceId', 'device_id_md5', 'deviceIdMd5', 'serial', 'adb_serial', 'imei'] as $key) { $value = strtolower(trim((string)($device[$key] ?? ''))); if ($value !== '' && isset($candidateMap[$value])) { return (string)($device['device_id'] ?? $device['id'] ?? $candidateMap[$value]); } } } } catch (\Throwable $e) { return ''; } return ''; } /** * 判定工作手机 SDK 是否真正发送成功(禁止仅凭 HTTP 200 误判)。 */ private function isWorkphoneSendSuccessful(array $sdkResp): bool { if (!empty($sdkResp['success']) && empty($sdkResp['data']['success']) && empty($sdkResp['data']['error'])) { return true; } $payload = $sdkResp['data'] ?? []; if (is_array($payload)) { if (array_key_exists('success', $payload)) { return (bool)$payload['success']; } $inner = $payload['data'] ?? null; if (is_array($inner) && array_key_exists('success', $inner)) { return (bool)$inner['success']; } } return false; } private function mapWorkPhoneMsgType(int $msgType): string { if ($msgType === 3) { return 'image'; } if ($msgType === 43) { return 'video'; } if ($msgType === 49) { return 'file'; } return 'text'; } private function upsertLocalOutgoingMessage(array $data): void { $now = time(); $row = [ 'id' => (int)$data['id'], 'type' => (int)$data['type'], 'wechatFriendId' => (int)$data['wechatFriendId'], 'wechatChatroomId' => (int)$data['wechatChatroomId'], 'wechatAccountId' => (int)$data['wechatAccountId'], 'tenantId' => 0, 'accountId' => (int)$data['accountId'], 'synergyAccountId' => 0, 'content' => (string)$data['content'], 'originalContent' => (string)$data['content'], 'msgType' => (int)$data['msgType'], 'msgSubType' => 0, 'msgSvrId' => '', 'isSend' => 1, 'createTime' => $now, 'isDeleted' => 0, 'deleteTime' => 0, 'sendStatus' => (int)$data['sendStatus'], 'wechatTime' => $now, 'origin' => 0, 'msgId' => 0, 'recallId' => 0, 'isRead' => 1, ]; $exists = Db::table('s2_wechat_message') ->where('id', $row['id']) ->where('type', $row['type']) ->find(); if ($exists) { Db::table('s2_wechat_message') ->where('id', $row['id']) ->where('type', $row['type']) ->update($row); return; } Db::table('s2_wechat_message')->insert($row); } public function getList() { $page = $this->request->param('page', 1); $limit = $this->request->param('limit', 10); $wechatAccountId = (int)$this->request->param('wechatAccountId/d', 0); $accountId = (int)$this->getUserInfo('s2_accountId'); $companyId = (int)$this->getUserInfo('companyId'); if ($accountId <= 0) { return ResponseHelper::error('请先登录'); } if ($wechatAccountId > 0) { $scopeIds = CompanyWechatScopeService::resolveKefuSidebarAccountIds($companyId, $accountId); if (!empty($scopeIds) && !in_array($wechatAccountId, $scopeIds, true)) { return ResponseHelper::error('会话不属于本项目微信号(NOT_IN_COMPANY)', 403); } $friendWhere = ['wechatAccountId' => $wechatAccountId, 'isDeleted' => 0]; $chatroomWhere = ['wechatAccountId' => $wechatAccountId, 'isDeleted' => 0]; } else { $friendWhere = ['accountId' => $accountId, 'isDeleted' => 0]; $chatroomWhere = ['accountId' => $accountId, 'isDeleted' => 0]; } // 直接查询好友ID列表 $ids = Db::table('s2_wechat_friend') ->where($friendWhere) ->column('id'); $friendIds = empty($ids) ? [0] : $ids; // 避免 IN 查询为空 // 直接查询好友信息 $friends = Db::table('s2_wechat_friend') ->where($friendWhere) ->column('id,nickname,avatar,conRemark,labels,groupId,wechatAccountId,wechatId,extendFields,phone,region,isTop'); // 直接查询群聊信息 $chatrooms = Db::table('s2_wechat_chatroom') ->where($chatroomWhere) ->column('id,nickname,chatroomAvatar,chatroomId,isTop'); // 获取群聊ID列表 $chatroomIds = array_keys($chatrooms); if (empty($chatroomIds)) { $chatroomIds = [0]; } // 1. 查询群聊最新消息 $chatroomMessages = []; if (!empty($chatroomIds) && $chatroomIds[0] != 0) { $chatroomIdsStr = implode(',', array_map('intval', $chatroomIds)); $chatroomLatestQuery = " SELECT wc.id as chatroomId, m.id, m.content, m.wechatChatroomId, m.createTime, m.wechatTime, m.wechatAccountId, wc.nickname, wc.chatroomAvatar as avatar, wc.chatroomId, wc.isTop, 2 as msgType FROM s2_wechat_chatroom wc INNER JOIN ( SELECT wechatChatroomId, MAX(wechatTime) as maxTime, MAX(id) as maxId FROM s2_wechat_message WHERE type = 2 AND wechatChatroomId IN ({$chatroomIdsStr}) GROUP BY wechatChatroomId ) latest ON wc.id = latest.wechatChatroomId INNER JOIN s2_wechat_message m ON m.wechatChatroomId = latest.wechatChatroomId AND m.wechatTime = latest.maxTime AND m.id = latest.maxId WHERE wc.accountId = {$accountId} AND wc.isDeleted = 0 "; $chatroomMessages = Db::query($chatroomLatestQuery); } // 2. 查询好友最新消息 $friendMessages = []; if (!empty($friendIds) && $friendIds[0] != 0) { $friendIdsStr = implode(',', array_map('intval', $friendIds)); $friendLatestQuery = " SELECT m.wechatFriendId, m.id, m.content, m.createTime, m.wechatTime, f.wechatAccountId, 1 as msgType, 0 as isTop FROM s2_wechat_message m INNER JOIN ( SELECT wechatFriendId, MAX(wechatTime) as maxTime, MAX(id) as maxId FROM s2_wechat_message WHERE type = 1 AND wechatFriendId IN ({$friendIdsStr}) GROUP BY wechatFriendId ) latest ON m.wechatFriendId = latest.wechatFriendId AND m.wechatTime = latest.maxTime AND m.id = latest.maxId INNER JOIN s2_wechat_friend f ON f.id = m.wechatFriendId WHERE m.type = 1 AND m.wechatFriendId IN ({$friendIdsStr}) "; $friendMessages = Db::query($friendLatestQuery); } // 合并结果并排序 $allMessages = array_merge($chatroomMessages, $friendMessages); usort($allMessages, function ($a, $b) { return $b['wechatTime'] <=> $a['wechatTime']; }); // 计算总数 $totalCount = count($allMessages); // 分页处理 $list = array_slice($allMessages, ($page - 1) * $limit, $limit); // 收集需要查询的ID $queryFriendIds = []; $queryChatroomIds = []; foreach ($list as $row) { if (!empty($row['wechatFriendId'])) { $queryFriendIds[] = $row['wechatFriendId']; } if (!empty($row['wechatChatroomId'])) { $queryChatroomIds[] = $row['wechatChatroomId']; } } $queryFriendIds = array_unique($queryFriendIds); $queryChatroomIds = array_unique($queryChatroomIds); // 批量查询未读数量(优化:合并查询) $unreadMap = []; if (!empty($queryFriendIds)) { $friendUnreads = Db::table('s2_wechat_message') ->where(['isRead' => 0, 'type' => 1]) ->whereIn('wechatFriendId', $queryFriendIds) ->field('wechatFriendId, COUNT(*) as cnt') ->group('wechatFriendId') ->select(); foreach ($friendUnreads as $item) { $unreadMap['friend_' . $item['wechatFriendId']] = (int)$item['cnt']; } } if (!empty($queryChatroomIds)) { $chatroomUnreads = Db::table('s2_wechat_message') ->where(['isRead' => 0, 'type' => 2]) ->whereIn('wechatChatroomId', $queryChatroomIds) ->field('wechatChatroomId, COUNT(*) as cnt') ->group('wechatChatroomId') ->select(); foreach ($chatroomUnreads as $item) { $unreadMap['chatroom_' . $item['wechatChatroomId']] = (int)$item['cnt']; } } // 批量查询AI类型 $aiTypeData = []; if (!empty($queryFriendIds)) { $aiTypeData = FriendSettings::where('friendId', 'in', $queryFriendIds)->column('friendId,type'); } // 格式化数据 foreach ($list as $k => &$v) { $createTime = !empty($v['createTime']) ? date('Y-m-d H:i:s', $v['createTime']) : ''; $wechatTime = !empty($v['wechatTime']) ? date('Y-m-d H:i:s', $v['wechatTime']) : ''; $unreadCount = 0; $v['aiType'] = 0; if (!empty($v['wechatFriendId'])) { // 好友消息 $friendId = $v['wechatFriendId']; $friend = $friends[$friendId] ?? null; $v['nickname'] = $friend['nickname'] ?? ''; $v['avatar'] = $friend['avatar'] ?? ''; $v['conRemark'] = $friend['conRemark'] ?? ''; $v['groupId'] = $friend['groupId'] ?? ''; $v['wechatAccountId'] = $friend['wechatAccountId'] ?? ''; $v['wechatId'] = $friend['wechatId'] ?? ''; $v['extendFields'] = $friend['extendFields'] ?? []; $v['region'] = $friend['region'] ?? ''; $v['phone'] = $friend['phone'] ?? ''; $v['isTop'] = $friend['isTop'] ?? 0; $v['labels'] = !empty($friend['labels']) ? json_decode($friend['labels'], true) : []; $unreadCount = $unreadMap['friend_' . $friendId] ?? 0; $v['aiType'] = $aiTypeData[$friendId] ?? 0; $v['id'] = $friendId; unset($v['chatroomId']); } elseif (!empty($v['wechatChatroomId'])) { // 群聊消息 $chatroomId = $v['wechatChatroomId']; $chatroom = $chatrooms[$chatroomId] ?? null; $v['nickname'] = $chatroom['nickname'] ?? ''; $v['avatar'] = $chatroom['chatroomAvatar'] ?? ''; $v['conRemark'] = ''; $v['isTop'] = $chatroom['isTop'] ?? 0; $v['chatroomId'] = $chatroom['chatroomId'] ?? ''; $unreadCount = $unreadMap['chatroom_' . $chatroomId] ?? 0; $v['id'] = $chatroomId; unset($v['wechatFriendId']); } $v['config'] = [ 'top' => !empty($v['isTop']) ? true : false, 'unreadCount' => $unreadCount, 'chat' => true, 'msgTime' => $wechatTime, ]; $v['createTime'] = $createTime; $v['lastUpdateTime'] = $wechatTime; $v['latestMessage'] = [ 'content' => $v['content'] ?? '', 'wechatTime' => $wechatTime ]; unset($v['wechatChatroomId'], $v['isTop'], $v['msgType']); } unset($v); return ResponseHelper::success(['list' => $list, 'total' => $totalCount]); } public function readMessage() { $wechatFriendId = $this->request->param('wechatFriendId', ''); $wechatChatroomId = $this->request->param('wechatChatroomId', ''); $accountId = $this->getUserInfo('s2_accountId'); if (empty($accountId)) { return ResponseHelper::error('请先登录'); } if (empty($wechatChatroomId) && empty($wechatFriendId)) { return ResponseHelper::error('参数缺失'); } $conversationAccountId = $this->resolveConversationWechatAccountId( (int)$accountId, (int)$wechatFriendId, (int)$wechatChatroomId ); if ($conversationAccountId <= 0) { return ResponseHelper::error('会话不存在或无权限', 404); } $where = []; if (!empty($wechatChatroomId)) { $where[] = ['wechatChatroomId', '=', $wechatChatroomId]; } if (!empty($wechatFriendId)) { $where[] = ['wechatFriendId', '=', $wechatFriendId]; } Db::table('s2_wechat_message')->where($where)->update(['isRead' => 1]); return ResponseHelper::success([]); } /** * 获取单条消息发送状态(带轮询功能) * @return \think\response\Json */ public function getMessageStatus() { $messageId = $this->request->param('messageId', 0); $wechatAccountId = $this->request->param('wechatAccountId', ''); $accountId = $this->getUserInfo('s2_accountId'); $wechatFriendId = $this->request->param('wechatFriendId', ''); $wechatChatroomId = $this->request->param('wechatChatroomId', ''); if (empty($accountId)) { return ResponseHelper::error('请先登录'); } if (empty($messageId)) { return ResponseHelper::error('消息ID不能为空'); } if(empty($wechatFriendId) && empty($wechatChatroomId)) { return ResponseHelper::error('消息类型不能为空'); } $conversationAccountId = $this->resolveConversationWechatAccountId( (int)$accountId, (int)$wechatFriendId, (int)$wechatChatroomId ); if ($conversationAccountId <= 0) { return ResponseHelper::error('会话不存在或无权限', 404); } $wechatAccountId = $conversationAccountId; // 查询单条消息的基本信息(只需要发送状态相关字段) $messageQuery = Db::table('s2_wechat_message')->where('id', $messageId); if (!empty($wechatChatroomId)) { $messageQuery->where('wechatChatroomId', $wechatChatroomId); } else { $messageQuery->where('wechatFriendId', $wechatFriendId); } $message = $messageQuery ->field('id,wechatAccountId,wechatFriendId,wechatChatroomId,sendStatus') ->find(); if (empty($message)) { $message = [ 'id' => $messageId, 'wechatAccountId' => $wechatAccountId, 'wechatFriendId' => $wechatFriendId, 'wechatChatroomId' => $wechatChatroomId, 'sendStatus' => 0, ]; } $sendStatus = isset($message['sendStatus']) ? (int)$message['sendStatus'] : 0; $isUpdated = false; $pollCount = 0; $maxPollCount = 10; // 最多轮询10次 // 如果sendStatus不为0,开始轮询 if ($sendStatus != 0) { $messageRequest = [ 'id' => $message['id'], 'wechatAccountId' => !empty($wechatAccountId) ? $wechatAccountId : $message['wechatAccountId'], 'wechatFriendId' => !empty($message['wechatFriendId']) ? $message['wechatFriendId'] : '', 'wechatChatroomId' => !empty($message['wechatChatroomId']) ? $message['wechatChatroomId'] : '', 'from' => '', 'to' => '', ]; // 轮询逻辑:最多10次 while ($pollCount < $maxPollCount && $sendStatus != 0) { $pollCount++; // 请求线上接口获取最新状态 $newData = $this->fetchLatestMessageFromApi($messageRequest); if (!empty($newData)) { // 重新查询消息状态(可能已更新) $updatedMessage = Db::table('s2_wechat_message') ->where('id', $messageId) ->field('sendStatus') ->find(); if (!empty($updatedMessage)) { $newSendStatus = isset($updatedMessage['sendStatus']) ? (int)$updatedMessage['sendStatus'] : 0; // 如果状态已更新为0(已发送),停止轮询 if ($newSendStatus == 0) { $sendStatus = 0; $isUpdated = true; break; } // 如果状态仍然是1,继续轮询(但需要等待一下,避免请求过快) if ($newSendStatus != 0 && $pollCount < $maxPollCount) { // 每次轮询间隔500毫秒(0.5秒) usleep(500000); } } } else { // 如果请求失败,等待后继续尝试 if ($pollCount < $maxPollCount) { usleep(500000); } } } } // 返回发送状态信息 return ResponseHelper::success([ 'messageId' => $messageId, 'sendStatus' => $sendStatus, 'statusText' => $sendStatus == 0 ? '已发送' : '发送中' ]); } public function details() { $wechatFriendId = $this->request->param('wechatFriendId', ''); $wechatChatroomId = $this->request->param('wechatChatroomId', ''); $wechatAccountId = $this->request->param('wechatAccountId', ''); $page = $this->request->param('page', 1); $limit = $this->request->param('limit', 10); $from = $this->request->param('From', $this->request->param('from', '')); $to = $this->request->param('To', $this->request->param('to', '')); $olderData = $this->request->param('olderData', false); $accountId = $this->getUserInfo('s2_accountId'); if (empty($accountId)) { return ResponseHelper::error('请先登录'); } if (empty($wechatChatroomId) && empty($wechatFriendId)) { return ResponseHelper::error('参数缺失'); } $conversationAccountId = $this->resolveConversationWechatAccountId( (int)$accountId, (int)$wechatFriendId, (int)$wechatChatroomId ); if ($conversationAccountId <= 0) { return ResponseHelper::error('会话不存在或无权限', 404); } $wechatAccountId = $conversationAccountId; $where = []; if (!empty($wechatChatroomId)) { $where[] = ['wechatChatroomId', '=', $wechatChatroomId]; } if (!empty($wechatFriendId)) { $where[] = ['wechatFriendId', '=', $wechatFriendId]; } if ($from !== '' && $to !== '' && is_numeric($from) && is_numeric($to)) { $where[] = ['wechatTime', 'between', [(int)$from, (int)$to]]; } $total = Db::table('s2_wechat_message')->where($where)->count(); $list = Db::table('s2_wechat_message')->where($where)->page($page, $limit)->order('id DESC')->select(); // 检查消息是否有sendStatus字段,如果有且不为0,则请求线上最新接口 foreach ($list as $k => &$item) { // 检查是否存在sendStatus字段且不为0(0表示已发送成功) if (isset($item['sendStatus']) && $item['sendStatus'] != 0) { // 需要请求新的数据 $messageRequest = [ 'id' => $item['id'], 'wechatAccountId' => $wechatAccountId, 'wechatFriendId' => $wechatFriendId, 'wechatChatroomId' => $wechatChatroomId, 'from' => '', 'to' => '', ]; $newData = $this->fetchLatestMessageFromApi($messageRequest); if (!empty($newData)){ $item['sendStatus'] = 0; } } // 格式化时间 $item['wechatTime'] = !empty($item['wechatTime']) ? date('Y-m-d H:i:s', $item['wechatTime']) : ''; } unset($item); return ResponseHelper::success(['total' => $total, 'list' => $list]); } /** * 从线上接口获取最新消息 * @param array $messageRequest 消息项(包含wechatAccountId、wechatFriendId或wechatChatroomId、id等) * @return array|null 最新消息数据,失败返回null */ private function fetchLatestMessageFromApi($messageRequest) { if (empty($this->baseUrl) || empty($this->authorization)) { return null; } try { // 设置请求头 $headerData = ['client:system']; $header = setHeader($headerData, $this->authorization, 'json'); // 判断是好友消息还是群聊消息 if (!empty($messageRequest['wechatFriendId'])) { // 好友消息接口 $params = [ 'keyword' => '', 'msgType' => '', 'accountId' => '', 'count' => 20, // 获取多条消息以便找到对应的消息 'messageId' => isset($messageRequest['id']) ? $messageRequest['id'] : '', 'olderData' => true, 'wechatAccountId' => $messageRequest['wechatAccountId'], 'wechatFriendId' => $messageRequest['wechatFriendId'], 'from' => $messageRequest['from'], 'to' => $messageRequest['to'], 'searchFrom' => 'admin' ]; $result = requestCurl($this->baseUrl . 'api/FriendMessage/searchMessage', $params, 'GET', $header, 'json'); $response = handleApiResponse($result); // 查找对应的消息 if (!empty($response) && is_array($response)) { $data = $response[0]; if ($data['sendStatus'] == 0){ WechatMessageModel::where(['id' => $data['id']])->update(['sendStatus' => 0]); return true; } } return false; } elseif (!empty($messageRequest['wechatChatroomId'])) { // 群聊消息接口 $params = [ 'keyword' => '', 'msgType' => '', 'accountId' => '', 'count' => 20, // 获取多条消息以便找到对应的消息 'messageId' => isset($messageRequest['id']) ? $messageRequest['id'] : '', 'olderData' => true, 'wechatId' => '', 'wechatAccountId' => $messageRequest['wechatAccountId'], 'wechatChatroomId' => $messageRequest['wechatChatroomId'], 'from' => $messageRequest['from'], 'to' => $messageRequest['to'], 'searchFrom' => 'admin' ]; $result = requestCurl($this->baseUrl . 'api/ChatroomMessage/searchMessage', $params, 'GET', $header, 'json'); $response = handleApiResponse($result); // 查找对应的消息 if (!empty($response) && is_array($response)) { $data = $response[0]; if ($data['sendStatus'] == 0){ WechatMessageModel::where(['id' => $data['id']])->update(['sendStatus' => 0]); return true; } } return false; } } catch (\Exception $e) { // 记录错误日志,但不影响主流程 \think\facade\Log::error('获取线上最新消息失败:' . $e->getMessage()); } return null; } /** * 更新数据库中的消息 * @param array $latestMessage 线上获取的最新消息 * @param array $oldMessage 旧消息数据 */ private function updateMessageInDatabase($latestMessage, $oldMessage) { try { // 使用API模块的MessageController来保存消息 $apiMessageController = new \app\api\controller\MessageController(); // 判断是好友消息还是群聊消息 if (!empty($oldMessage['wechatFriendId'])) { // 保存好友消息 $apiMessageController->saveMessage($latestMessage); } elseif (!empty($oldMessage['wechatChatroomId'])) { // 保存群聊消息 $apiMessageController->saveChatroomMessage($latestMessage); } } catch (\Exception $e) { // 记录错误日志,但不影响主流程 \think\facade\Log::error('更新数据库消息失败:' . $e->getMessage()); } } }