Files
cunkebao_v3/Server/application/chukebao/controller/MessageController.php
Manus AI f6025724ea feat: 四端工作手机链路与触客宝发消息安全门禁
统一设备/微信解析与 BFF 发消息正规流程(Resolver、142-P1、sendStatus 判定),并补齐超管映射、触客宝乐观 UI 与验收文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-13 19:41:21 +08:00

1148 lines
47 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace app\chukebao\controller;
use app\api\model\WechatMessageModel;
use app\chukebao\model\FriendSettings;
use app\chukebao\service\CompanyWechatScopeService;
use app\common\service\workphone\WorkphoneMessageDeviceResolver;
use app\common\util\WorkPhoneSDK;
use library\ResponseHelper;
use think\Db;
use think\facade\Env;
use think\facade\Log;
use app\common\service\AuthService;
class MessageController extends BaseController
{
protected $baseUrl;
protected $authorization;
public function __construct()
{
parent::__construct();
$this->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-sessionCKB-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字段且不为00表示已发送成功
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());
}
}
}