5 Commits

Author SHA1 Message Date
wong
1a28c9ae75 代码优化 2026-04-29 16:33:23 +08:00
wong
edfc21c373 111111 2026-04-15 17:21:12 +08:00
wong
b1bfcec35b feat(media): 微信消息媒体 OSS 归档与下载地址回写
- 新增 MediaArchiveJob、MediaOssArchiveService、WechatMediaArchiveService 与回填命令
- Message/DataProcessing 等支持归档调度与持久化下载 URL
- WebSocket 控制器整理;朋友圈与文档/定时任务说明更新

Made-with: Cursor
2026-04-13 17:23:02 +08:00
wong
3bf1b2aee9 11111 2026-04-09 12:30:38 +08:00
wong
29413f57c7 1111 2026-03-24 10:38:29 +08:00
20 changed files with 1221 additions and 157 deletions

1
.gitignore vendored
View File

@@ -17,3 +17,4 @@ nginx.htaccess
thinkphp/ thinkphp/
public/static/ public/static/
*.log *.log
Server.code-workspace

View File

@@ -44,7 +44,7 @@
```bash ```bash
# 每分钟执行一次调度器(调度器内部会根据 cron 表达式判断哪些任务需要执行) # 每分钟执行一次调度器(调度器内部会根据 cron 表达式判断哪些任务需要执行)
* * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think scheduler:run >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/scheduler.log 2>&1 * * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think scheduler:run >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/scheduler.log 2>&1
``` ```
### 4. 系统要求 ### 4. 系统要求

View File

@@ -4,7 +4,10 @@ namespace app\api\controller;
use app\api\model\WechatMessageModel; use app\api\model\WechatMessageModel;
use app\common\service\FriendTransferService; use app\common\service\FriendTransferService;
use app\common\service\WechatMediaArchiveService;
use app\job\MediaArchiveJob;
use think\Db; use think\Db;
use think\facade\Log;
use think\facade\Request; use think\facade\Request;
class MessageController extends BaseController class MessageController extends BaseController
@@ -418,6 +421,7 @@ class MessageController extends BaseController
'type' => 1, 'type' => 1,
'accountId' => $item['accountId'], 'accountId' => $item['accountId'],
'content' => $item['content'], 'content' => $item['content'],
'originalContent' => $item['content'],
'createTime' => $createTime, 'createTime' => $createTime,
'deleteTime' => $deleteTime, 'deleteTime' => $deleteTime,
'isDeleted' => $item['isDeleted'] ?? false, 'isDeleted' => $item['isDeleted'] ?? false,
@@ -463,6 +467,9 @@ class MessageController extends BaseController
}else{ }else{
$id = $data['id']; $id = $data['id'];
unset($data['id']); unset($data['id']);
if (!empty($exists['originalContent'])) {
unset($data['originalContent']);
}
$res = $exists->save($data); $res = $exists->save($data);
} }
@@ -496,6 +503,9 @@ class MessageController extends BaseController
} }
} }
} }
if (in_array((int)($item['msgType'] ?? 0), [3, 34, 43, 47, 49], true) && !empty($id)) {
MediaArchiveJob::dispatch('message', $id, ['source' => 'saveMessage']);
}
return true; return true;
} }
@@ -577,6 +587,9 @@ class MessageController extends BaseController
throw new \Exception('更新群聊消息记录失败'); throw new \Exception('更新群聊消息记录失败');
} }
} }
if (in_array((int)($item['msgType'] ?? 0), [3, 34, 43, 47, 49], true)) {
MediaArchiveJob::dispatch('message', $item['id'], ['source' => 'saveChatroomMessage']);
}
return true; return true;
} catch (\Exception $e) { } catch (\Exception $e) {
// 记录错误日志,便于调试 // 记录错误日志,便于调试
@@ -588,6 +601,29 @@ class MessageController extends BaseController
} }
} }
/**
* 持久化视频/文件下载后的真实地址,并重新触发 OSS 归档
* @param array $data
* @return bool
*/
public function updateDownloadedMessageMedia($data)
{
$messageId = (int)($data['friendMessageId'] ?? $data['chatroomMessageId'] ?? 0);
$downloadUrl = trim((string)($data['url'] ?? ''));
if ($messageId <= 0 || empty($downloadUrl)) {
return false;
}
$updated = WechatMediaArchiveService::updateDownloadedMessageMedia($messageId, $downloadUrl);
if (!$updated) {
return false;
}
MediaArchiveJob::dispatch('message', $messageId, ['source' => $data['type'] ?? 'download_result']);
return true;
}
/** /**
* 处理消息内容提取发送者ID和消息内容 * 处理消息内容提取发送者ID和消息内容
* @param string $content 原始消息内容 * @param string $content 原始消息内容

View File

@@ -12,7 +12,7 @@ use think\facade\Env;
use app\api\model\WechatFriendModel as WechatFriend; use app\api\model\WechatFriendModel as WechatFriend;
use app\api\model\WechatMomentsModel as WechatMoments; use app\api\model\WechatMomentsModel as WechatMoments;
use think\facade\Cache; use think\facade\Cache;
use app\common\util\AliyunOSS; use app\common\service\MediaOssArchiveService;
class WebSocketController extends BaseController class WebSocketController extends BaseController
@@ -468,7 +468,7 @@ class WebSocketController extends BaseController
// 更新数据库保存原始URL和OSS URL并标记已上传 // 更新数据库保存原始URL和OSS URL并标记已上传
$updateData = [ $updateData = [
'resUrls' => $urls, 'resUrls' => $urls,
'isOssUploaded' => 1, // 标识已上传到OSS 'isOssUploaded' => !empty($ossUrls) ? 1 : 0,
'update_time' => time() 'update_time' => time()
]; ];
@@ -513,7 +513,7 @@ class WebSocketController extends BaseController
// 更新数据库保存原始URL和OSS URL并标记已上传 // 更新数据库保存原始URL和OSS URL并标记已上传
$updateData = [ $updateData = [
'resUrls' => $urls, 'resUrls' => $urls,
'isOssUploaded' => 1, // 标识已上传到OSS 'isOssUploaded' => !empty($ossUrls) ? 1 : 0,
'update_time' => time() 'update_time' => time()
]; ];
@@ -558,75 +558,22 @@ class WebSocketController extends BaseController
return $ossUrls; return $ossUrls;
} }
try { foreach ($urls as $url) {
// 创建临时目录(兼容无 runtime_path() 辅助函数的环境) if (!MediaOssArchiveService::isRemoteHttpUrl($url)) {
if (function_exists('runtime_path')) { continue;
$baseRuntimePath = rtrim(runtime_path(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
} elseif (defined('RUNTIME_PATH')) {
$baseRuntimePath = rtrim(RUNTIME_PATH, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
} else {
// 兜底:使用项目根目录下的 runtime 目录
$baseRuntimePath = rtrim(ROOT_PATH, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'runtime' . DIRECTORY_SEPARATOR;
} }
$tempDir = $baseRuntimePath . 'temp' . DIRECTORY_SEPARATOR . 'moments' . DIRECTORY_SEPARATOR . date('Y' . DIRECTORY_SEPARATOR . 'm' . DIRECTORY_SEPARATOR . 'd') . DIRECTORY_SEPARATOR; $resourceType = preg_match('/\.(mp4|mov|avi|webm|mkv)(\?.*)?$/i', $url) ? 'video' : 'image';
$result = MediaOssArchiveService::archiveRemoteUrl($url, 'moments', $resourceType, (string)$snsId);
if (!is_dir($tempDir)) { if (!empty($result['success']) && !empty($result['url'])) {
mkdir($tempDir, 0755, true); $ossUrls[] = $result['url'];
continue;
} }
foreach ($urls as $index => $url) { Log::error('朋友圈媒体上传OSS失败' . ($result['error'] ?? '未知错误'), [
if (empty($url)) { 'snsId' => $snsId,
continue; 'url' => $url,
} ]);
try {
// 下载图片到临时文件
$tempFile = $tempDir . md5($url . $snsId . $index) . '.jpg';
// 使用curl下载图片
$ch = curl_init($url);
$fp = fopen($tempFile, 'wb');
curl_setopt($ch, CURLOPT_FILE, $fp);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
fclose($fp);
if ($httpCode != 200 || !file_exists($tempFile) || filesize($tempFile) == 0) {
Log::warning('下载朋友圈图片失败:' . $url . ', HTTP Code: ' . $httpCode);
@unlink($tempFile);
continue;
}
// 生成OSS对象名称
$objectName = 'moments/' . date('Y/m/d/') . md5($snsId . $index . time()) . '.jpg';
// 上传到OSS
$result = AliyunOSS::uploadFile($tempFile, $objectName);
if ($result['success']) {
$ossUrls[] = $result['url'];
} else {
Log::error('朋友圈图片上传OSS失败' . $url . ', 错误:' . ($result['error'] ?? '未知错误'));
}
// 删除临时文件
@unlink($tempFile);
} catch (\Exception $e) {
Log::error('上传朋友圈图片到OSS异常' . $e->getMessage() . ', URL: ' . $url);
if (isset($tempFile) && file_exists($tempFile)) {
@unlink($tempFile);
}
}
}
} catch (\Exception $e) {
Log::error('上传朋友圈图片到OSS异常' . $e->getMessage());
} }
return $ossUrls; return $ossUrls;
@@ -696,8 +643,8 @@ class WebSocketController extends BaseController
} }
// 获取资源链接检查是否已上传到OSS如果已上传则跳过 // 获取资源链接检查是否已上传到OSS如果已上传则跳过
if(empty($momentEntity['urls']) || $moment['type'] != 1) { if(empty($momentEntity['urls'])) {
// 如果没有urls或类型不是1,跳过 // 如果没有urls跳过
} elseif ($isOssUploaded == 1) { } elseif ($isOssUploaded == 1) {
// 如果已上传到OSS跳过采集 // 如果已上传到OSS跳过采集
} else { } else {

View File

@@ -37,6 +37,8 @@ class DataProcessing extends BaseController
'CmdChatroomOperate', //修改群信息 {chatroomName群名、announce公告、extra公告、wechatAccountId、wechatChatroomId} 'CmdChatroomOperate', //修改群信息 {chatroomName群名、announce公告、extra公告、wechatAccountId、wechatChatroomId}
'CmdNewMessage', //接收消息 'CmdNewMessage', //接收消息
'CmdSendMessageResult', //更新消息状态 'CmdSendMessageResult', //更新消息状态
'CmdDownloadVideoResult', //视频下载结果回写
'CmdDownloadFileResult', //文件下载结果回写
'CmdPinToTop', //置顶 'CmdPinToTop', //置顶
]; ];
@@ -168,6 +170,34 @@ class DataProcessing extends BaseController
$msg = '更新消息状态成功'; $msg = '更新消息状态成功';
break; break;
case 'CmdDownloadVideoResult':
case 'CmdDownloadFileResult':
$friendMessageId = $this->request->param('friendMessageId', 0);
$chatroomMessageId = $this->request->param('chatroomMessageId', 0);
$url = trim((string)$this->request->param('url', ''));
if (empty($friendMessageId) && empty($chatroomMessageId)) {
return ResponseHelper::error('friendMessageId或chatroomMessageId至少提供一个');
}
if (empty($url)) {
return ResponseHelper::error('url不能为空');
}
$messageController = new MessageController();
$updated = $messageController->updateDownloadedMessageMedia([
'friendMessageId' => $friendMessageId,
'chatroomMessageId' => $chatroomMessageId,
'url' => $url,
'type' => $type,
]);
if (!$updated) {
return ResponseHelper::error('媒体地址回写失败');
}
$msg = '媒体地址回写成功';
break;
case 'CmdPinToTop': //置顶 case 'CmdPinToTop': //置顶
$wechatFriendId = $this->request->param('wechatFriendId', 0); $wechatFriendId = $this->request->param('wechatFriendId', 0);
$wechatChatroomId = $this->request->param('wechatChatroomId', 0); $wechatChatroomId = $this->request->param('wechatChatroomId', 0);

View File

@@ -53,4 +53,5 @@ return [
// V2 流量池数据迁移 // V2 流量池数据迁移
'migrate:trafficPoolV2' => 'app\command\MigrateTrafficPoolV2Command', // 迁移数据到 V2 流量池系统 'migrate:trafficPoolV2' => 'app\command\MigrateTrafficPoolV2Command', // 迁移数据到 V2 流量池系统
'media:archive' => 'app\command\BackfillMediaOssCommand', // 历史媒体资源归档到OSS
]; ];

View File

@@ -0,0 +1,121 @@
<?php
namespace app\command;
use app\common\service\WechatMediaArchiveService;
use app\job\MediaArchiveJob;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\console\input\Option;
use think\Db;
class BackfillMediaOssCommand extends Command
{
protected function configure()
{
$this->setName('media:archive')
->setDescription('扫描聊天记录和朋友圈媒体资源并加入 OSS 归档队列')
->addOption('scope', null, Option::VALUE_OPTIONAL, '归档范围chat|moments|all', 'all')
->addOption('limit', null, Option::VALUE_OPTIONAL, '单次扫描数量', 100)
->addOption('startId', null, Option::VALUE_OPTIONAL, '仅扫描大于该ID的记录', 0)
->addOption('messageId', null, Option::VALUE_OPTIONAL, '仅同步归档单条聊天记录(本地调试用,不走队列)', 0);
}
protected function execute(Input $input, Output $output)
{
$singleMessageId = (int)$input->getOption('messageId');
if ($singleMessageId > 0) {
return $this->archiveOneChatMessage($singleMessageId, $output);
}
$scope = strtolower((string)$input->getOption('scope'));
$limit = max(1, (int)$input->getOption('limit'));
$startId = max(0, (int)$input->getOption('startId'));
$messageCount = 0;
$momentCount = 0;
if (in_array($scope, ['all', 'chat'])) {
$messageIds = Db::table('s2_wechat_message')
->where('id', '>', $startId)
->whereIn('msgType', [3, 34, 43, 47, 49])
->where(function ($query) {
$query->whereLike('content', 'http%')
->whereOrLike('content', '%"url"%')
->whereOrLike('content', '%"previewImage"%')
->whereOrLike('content', '%"tencentUrl"%');
})
->order('id', 'asc')
->limit($limit)
->column('id');
foreach ($messageIds as $id) {
MediaArchiveJob::dispatch('message', $id, ['source' => 'backfill_command']);
$messageCount++;
}
}
if (in_array($scope, ['all', 'moments'])) {
$momentIds = Db::table('s2_wechat_moments')
->where('id', '>', $startId)
->where(function ($query) {
$query->where('isOssUploaded', 0)
->whereOr('ossUrls', 'null')
->whereOr('ossUrls', '')
->whereOr('ossUrls', '[]');
})
->where(function ($query) {
$query->where('resUrls', '<>', '')
->whereOr('urls', '<>', '');
})
->order('id', 'asc')
->limit($limit)
->column('id');
foreach ($momentIds as $id) {
MediaArchiveJob::dispatch('moment', $id, ['source' => 'backfill_command']);
$momentCount++;
}
}
$output->writeln(sprintf(
'已加入 OSS 归档队列:聊天记录 %d 条,朋友圈 %d 条。',
$messageCount,
$momentCount
));
return 0;
}
/**
* 单条聊天消息同步归档(用于本地验证 AliyunOSS 与下载链路)
*/
protected function archiveOneChatMessage($messageId, Output $output)
{
$row = Db::table('s2_wechat_message')->where('id', $messageId)->find();
if (empty($row)) {
$output->writeln('错误:未找到消息 id=' . $messageId);
return 1;
}
$preview = function ($s, $len) {
$s = (string)$s;
return function_exists('mb_substr') ? mb_substr($s, 0, $len) : substr($s, 0, $len);
};
$output->writeln('msgType=' . ($row['msgType'] ?? '') . ' content(前200字): ' . $preview($row['content'] ?? '', 200));
$ok = WechatMediaArchiveService::archiveMessageById($messageId);
if (!$ok) {
$output->writeln('失败:归档未执行或无需更新(请确认 msgType 为 3/34/43/47/49 且 content 含可下载 http 资源)');
return 1;
}
$after = Db::table('s2_wechat_message')->where('id', $messageId)->find();
$output->writeln('成功:归档完成。更新后 content(前300字):');
$output->writeln($preview($after['content'] ?? '', 300));
return 0;
}
}

View File

@@ -9,7 +9,7 @@ use think\console\Command;
use think\facade\App; use think\facade\App;
use WeChatDeviceApi\Adapters\ChuKeBao\Adapter as ChuKeBaoAdapter; use WeChatDeviceApi\Adapters\ChuKeBao\Adapter as ChuKeBaoAdapter;
// */7 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think sync:wechatData >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/sync_wechat_data.log 2>&1 // */7 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think sync:wechatData >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/sync_wechat_data.log 2>&1
class SyncWechatDataToCkbTask extends Command class SyncWechatDataToCkbTask extends Command
{ {
protected $lockFile; protected $lockFile;

View File

@@ -73,6 +73,43 @@ class TrafficPoolSource extends Model
return $value ? json_encode($value, JSON_UNESCAPED_UNICODE) : null; return $value ? json_encode($value, JSON_UNESCAPED_UNICODE) : null;
} }
/**
* 兼容历史库s2_wechat_friend 早期可能不存在 headImgUrl 列
* @param string $wechatId
* @return string
*/
protected static function getFriendHeadImgUrl(string $wechatId): string
{
$wechatId = trim($wechatId);
if ($wechatId === '') {
return '';
}
static $hasHeadImgUrl = null;
if ($hasHeadImgUrl === null) {
try {
$cols = Db::query("SHOW COLUMNS FROM s2_wechat_friend LIKE 'headImgUrl'");
$hasHeadImgUrl = !empty($cols);
} catch (\Throwable $e) {
$hasHeadImgUrl = false;
}
}
if (!$hasHeadImgUrl) {
return '';
}
try {
$val = Db::table('s2_wechat_friend')
->where('wechatId', $wechatId)
->value('headImgUrl');
return $val ? (string)$val : '';
} catch (\Throwable $e) {
// 兜底:避免因字段缺失导致接口直接 1054
return '';
}
}
/** /**
* 获取来源类型名称 * 获取来源类型名称
* @return string * @return string
@@ -224,11 +261,10 @@ class TrafficPoolSource extends Model
// 尝试获取好友头像 // 尝试获取好友头像
if (!empty($source['sourceWechatId'])) { if (!empty($source['sourceWechatId'])) {
$sourceData['sourceAvatar'] = Db::table('ck_traffic_pool') $avatar = Db::table('ck_traffic_pool')
->where('wechatId', $source['sourceWechatId']) ->where('wechatId', $source['sourceWechatId'])
->value('avatar') ?: Db::table('s2_wechat_friend') ->value('avatar');
->where('wechatId', $source['sourceWechatId']) $sourceData['sourceAvatar'] = $avatar ?: self::getFriendHeadImgUrl((string)$source['sourceWechatId']);
->value('headImgUrl') ?: '';
} }
} else { } else {
$sourceData['chatroomOwners'] = []; $sourceData['chatroomOwners'] = [];
@@ -448,11 +484,10 @@ class TrafficPoolSource extends Model
$sourceData['chatroomInfo'] = null; $sourceData['chatroomInfo'] = null;
// 尝试获取好友头像 // 尝试获取好友头像
if (!empty($source['sourceWechatId'])) { if (!empty($source['sourceWechatId'])) {
$sourceData['sourceAvatar'] = Db::table('ck_traffic_pool') $avatar = Db::table('ck_traffic_pool')
->where('wechatId', $source['sourceWechatId']) ->where('wechatId', $source['sourceWechatId'])
->value('avatar') ?: Db::table('s2_wechat_friend') ->value('avatar');
->where('wechatId', $source['sourceWechatId']) $sourceData['sourceAvatar'] = $avatar ?: self::getFriendHeadImgUrl((string)$source['sourceWechatId']);
->value('headImgUrl') ?: '';
} }
} else { } else {
$sourceData['chatroomOwners'] = []; $sourceData['chatroomOwners'] = [];

View File

@@ -0,0 +1,261 @@
<?php
namespace app\common\service;
use app\common\util\AliyunOSS;
use think\facade\Log;
class MediaOssArchiveService
{
protected static $mimeExtensionMap = [
'image/jpeg' => 'jpg',
'image/jpg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
'image/bmp' => 'bmp',
'image/svg+xml' => 'svg',
'video/mp4' => 'mp4',
'video/quicktime' => 'mov',
'video/x-msvideo' => 'avi',
'video/webm' => 'webm',
'audio/mpeg' => 'mp3',
'audio/mp3' => 'mp3',
'audio/wav' => 'wav',
'audio/x-wav' => 'wav',
'audio/amr' => 'amr',
'audio/aac' => 'aac',
'audio/mp4' => 'm4a',
'audio/ogg' => 'ogg',
'application/pdf' => 'pdf',
'application/msword' => 'doc',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
'application/vnd.ms-excel' => 'xls',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
'application/vnd.ms-powerpoint' => 'ppt',
'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
'application/zip' => 'zip',
'application/x-rar-compressed' => 'rar',
'application/x-7z-compressed' => '7z',
'text/plain' => 'txt',
];
public static function isRemoteHttpUrl($url)
{
if (!is_string($url)) {
return false;
}
return (bool)preg_match('/^https?:\/\//i', trim($url));
}
public static function isOwnOssUrl($url)
{
if (!self::isRemoteHttpUrl($url)) {
return false;
}
$host = strtolower((string)parse_url($url, PHP_URL_HOST));
if (empty($host)) {
return false;
}
$customHost = strtolower((string)parse_url(AliyunOSS::ossUrl, PHP_URL_HOST));
$bucketHost = strtolower(AliyunOSS::BUCKET . '.' . AliyunOSS::ENDPOINT);
return $host === $customHost || $host === $bucketHost || strpos($host, strtolower(AliyunOSS::BUCKET . '.')) === 0;
}
public static function archiveRemoteUrl($url, $bizType, $resourceType, $bizKey, array $options = [])
{
$url = trim((string)$url);
if (!self::isRemoteHttpUrl($url)) {
return [
'success' => false,
'error' => 'URL不是有效的http(s)地址',
'url' => '',
];
}
if (self::isOwnOssUrl($url)) {
return [
'success' => true,
'alreadyArchived' => true,
'url' => self::normalizeOssUrl($url),
'originalUrl' => $url,
'object_name' => '',
];
}
$tempFile = '';
try {
$tempDir = self::ensureTempDirectory();
$tempFile = $tempDir . md5($url . microtime(true) . mt_rand()) . '.tmp';
$downloadResult = self::downloadToLocal($url, $tempFile, (int)($options['timeout'] ?? 60));
if (!$downloadResult['success']) {
return $downloadResult;
}
$extension = self::detectExtension(
$url,
$tempFile,
$downloadResult['contentType'] ?? '',
$options['extension'] ?? ''
);
$objectName = self::buildObjectName($bizType, $resourceType, $bizKey, $extension);
$result = AliyunOSS::uploadFile($tempFile, $objectName);
if (!$result['success']) {
return [
'success' => false,
'error' => $result['error'] ?? '上传OSS失败',
'url' => '',
];
}
return [
'success' => true,
'alreadyArchived' => false,
'url' => self::normalizeOssUrl($result['url'] ?? ''),
'originalUrl' => $url,
'object_name' => $objectName,
'extension' => $extension,
'mime_type' => $result['mime_type'] ?? ($downloadResult['contentType'] ?? ''),
'size' => $result['size'] ?? 0,
];
} catch (\Exception $e) {
Log::error('媒体资源上传OSS失败' . $e->getMessage(), [
'url' => $url,
'bizType' => $bizType,
'resourceType' => $resourceType,
'bizKey' => $bizKey,
]);
return [
'success' => false,
'error' => $e->getMessage(),
'url' => '',
];
} finally {
if (!empty($tempFile) && file_exists($tempFile)) {
@unlink($tempFile);
}
}
}
protected static function ensureTempDirectory()
{
if (function_exists('runtime_path')) {
$baseRuntimePath = rtrim(runtime_path(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
} elseif (defined('RUNTIME_PATH')) {
$baseRuntimePath = rtrim(RUNTIME_PATH, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
} else {
$baseRuntimePath = rtrim(ROOT_PATH, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'runtime' . DIRECTORY_SEPARATOR;
}
$tempDir = $baseRuntimePath . 'temp' . DIRECTORY_SEPARATOR . 'media_archive' . DIRECTORY_SEPARATOR . date('Y' . DIRECTORY_SEPARATOR . 'm' . DIRECTORY_SEPARATOR . 'd') . DIRECTORY_SEPARATOR;
if (!is_dir($tempDir)) {
mkdir($tempDir, 0755, true);
}
return $tempDir;
}
protected static function downloadToLocal($url, $tempFile, $timeout)
{
$ch = curl_init($url);
$fp = fopen($tempFile, 'wb');
curl_setopt($ch, CURLOPT_FILE, $fp);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_USERAGENT, 'CKB-MediaArchive/1.0');
curl_exec($ch);
$curlError = curl_error($ch);
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
$contentType = (string)curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
curl_close($ch);
fclose($fp);
if (!empty($curlError)) {
@unlink($tempFile);
return [
'success' => false,
'error' => '下载资源失败:' . $curlError,
'url' => '',
];
}
if ($httpCode < 200 || $httpCode >= 300 || !file_exists($tempFile) || filesize($tempFile) <= 0) {
@unlink($tempFile);
return [
'success' => false,
'error' => '下载资源失败HTTP状态码' . $httpCode,
'url' => '',
];
}
return [
'success' => true,
'contentType' => $contentType,
'httpCode' => $httpCode,
];
}
protected static function detectExtension($url, $tempFile, $contentType = '', $forcedExtension = '')
{
$forcedExtension = strtolower(trim((string)$forcedExtension, '. '));
if (!empty($forcedExtension)) {
return $forcedExtension;
}
$pathExtension = strtolower((string)pathinfo((string)parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION));
if (preg_match('/^[a-z0-9]{1,8}$/i', $pathExtension)) {
return $pathExtension;
}
$contentType = strtolower(trim((string)$contentType));
if (!empty($contentType)) {
$contentType = trim(explode(';', $contentType)[0]);
if (!empty(self::$mimeExtensionMap[$contentType])) {
return self::$mimeExtensionMap[$contentType];
}
}
$mimeType = '';
if (function_exists('mime_content_type')) {
$mimeType = strtolower((string)mime_content_type($tempFile));
}
if (!empty($mimeType) && !empty(self::$mimeExtensionMap[$mimeType])) {
return self::$mimeExtensionMap[$mimeType];
}
return 'bin';
}
protected static function buildObjectName($bizType, $resourceType, $bizKey, $extension)
{
$bizType = trim((string)$bizType, '/');
$resourceType = trim((string)$resourceType, '/');
$bizKey = preg_replace('/[^a-zA-Z0-9_\-]/', '_', (string)$bizKey);
$extension = trim((string)$extension, '.');
return $bizType . '/' . $resourceType . '/' . date('Y/m/d/') . $bizKey . '_' . substr(md5(uniqid('', true)), 0, 16) . '.' . $extension;
}
protected static function normalizeOssUrl($url)
{
$url = trim((string)$url);
if (strpos($url, 'http://') === 0) {
return 'https://' . substr($url, 7);
}
return $url;
}
}

View File

@@ -0,0 +1,372 @@
<?php
namespace app\common\service;
use think\Db;
use think\facade\Log;
class WechatMediaArchiveService
{
public static function archiveMessageById($messageId)
{
$messageId = (int)$messageId;
if ($messageId <= 0) {
return false;
}
$row = Db::table('s2_wechat_message')->where('id', $messageId)->find();
if (empty($row)) {
return false;
}
$update = self::buildMessageArchiveUpdate($row);
if (empty($update)) {
return false;
}
Db::table('s2_wechat_message')->where('id', $messageId)->update($update);
return true;
}
public static function archiveMomentById($momentId)
{
$momentId = (int)$momentId;
if ($momentId <= 0) {
return false;
}
$row = Db::table('s2_wechat_moments')->where('id', $momentId)->find();
if (empty($row)) {
return false;
}
$displayUrls = self::decodeJsonArray($row['resUrls'] ?? '');
$rawUrls = self::decodeJsonArray($row['urls'] ?? '');
$candidateUrls = !empty($displayUrls) ? $displayUrls : $rawUrls;
if (empty($candidateUrls)) {
return false;
}
$ossUrls = [];
foreach ($candidateUrls as $index => $url) {
if (!MediaOssArchiveService::isRemoteHttpUrl($url)) {
continue;
}
$resourceType = self::isVideoUrl($url) ? 'video' : 'image';
$result = MediaOssArchiveService::archiveRemoteUrl($url, 'moments', $resourceType, (string)($row['snsId'] ?? $momentId), [
'index' => $index,
]);
if (!empty($result['success']) && !empty($result['url'])) {
$ossUrls[] = $result['url'];
}
}
if (empty($ossUrls)) {
return false;
}
Db::table('s2_wechat_moments')->where('id', $momentId)->update([
'ossUrls' => json_encode($ossUrls, JSON_UNESCAPED_UNICODE),
'isOssUploaded' => 1,
'update_time' => time(),
]);
return true;
}
public static function updateDownloadedMessageMedia($messageId, $downloadUrl)
{
$messageId = (int)$messageId;
$downloadUrl = trim((string)$downloadUrl);
if ($messageId <= 0 || empty($downloadUrl)) {
return false;
}
$row = Db::table('s2_wechat_message')->where('id', $messageId)->find();
if (empty($row)) {
return false;
}
$content = (string)($row['content'] ?? '');
$originalContent = (string)($row['originalContent'] ?? '');
$msgType = (int)($row['msgType'] ?? 0);
$update = [];
if (empty($originalContent)) {
$update['originalContent'] = $content;
}
if ($msgType === 43) {
$payload = self::decodeJsonObject($content);
if (empty($payload)) {
$payload = [];
}
$payload['videoUrl'] = $downloadUrl;
$payload['isLoading'] = false;
$update['content'] = self::encodeJson($payload);
} elseif ($msgType === 49) {
$payload = self::decodeJsonObject($content);
if (empty($payload) || strtolower((string)($payload['type'] ?? '')) !== 'file') {
$payload = [
'type' => 'file',
'title' => self::extractFileTitle($content),
];
}
$payload['url'] = $downloadUrl;
$payload['isDownloading'] = false;
$update['content'] = self::encodeJson($payload);
} else {
return false;
}
Db::table('s2_wechat_message')->where('id', $messageId)->update($update);
return true;
}
protected static function buildMessageArchiveUpdate(array $row)
{
$msgType = (int)($row['msgType'] ?? 0);
$content = (string)($row['content'] ?? '');
$originalContent = (string)($row['originalContent'] ?? '');
$sourceOriginalContent = $originalContent !== '' ? $originalContent : $content;
$update = [];
switch ($msgType) {
case 3:
case 47:
$imageUpdate = self::archiveImageLikeContent($content, $msgType, $row['id']);
if (!empty($imageUpdate)) {
$update = array_merge($update, $imageUpdate);
}
break;
case 34:
$audioUpdate = self::archiveAudioContent($content, $row['id']);
if (!empty($audioUpdate)) {
$update = array_merge($update, $audioUpdate);
}
break;
case 43:
$videoUpdate = self::archiveVideoContent($content, $row['id']);
if (!empty($videoUpdate)) {
$update = array_merge($update, $videoUpdate);
}
break;
case 49:
$fileUpdate = self::archiveFileContent($content, $row['id']);
if (!empty($fileUpdate)) {
$update = array_merge($update, $fileUpdate);
}
break;
default:
break;
}
if (!empty($update) && empty($originalContent) && !empty($sourceOriginalContent)) {
$update['originalContent'] = $sourceOriginalContent;
}
return $update;
}
protected static function archiveImageLikeContent($content, $msgType, $messageId)
{
$payload = self::decodeJsonObject($content);
if (!empty($payload) && !empty($payload['url'])) {
$sourceUrl = (string)($payload['originUrl'] ?? $payload['url']);
$result = MediaOssArchiveService::archiveRemoteUrl(
$sourceUrl,
'messages',
$msgType == 47 ? 'emoji' : 'image',
(string)$messageId
);
if (empty($result['success']) || empty($result['url'])) {
return [];
}
$payload['originUrl'] = $payload['originUrl'] ?? $sourceUrl;
$payload['ossUrl'] = $result['url'];
$payload['url'] = $result['url'];
return ['content' => self::encodeJson($payload)];
}
if (!MediaOssArchiveService::isRemoteHttpUrl($content)) {
return [];
}
$result = MediaOssArchiveService::archiveRemoteUrl(
$content,
'messages',
$msgType == 47 ? 'emoji' : 'image',
(string)$messageId
);
if (empty($result['success']) || empty($result['url'])) {
return [];
}
return ['content' => $result['url']];
}
protected static function archiveAudioContent($content, $messageId)
{
$payload = self::decodeJsonObject($content);
if (!empty($payload) && !empty($payload['url'])) {
$sourceUrl = (string)($payload['originUrl'] ?? $payload['url']);
$result = MediaOssArchiveService::archiveRemoteUrl($sourceUrl, 'messages', 'audio', (string)$messageId);
if (empty($result['success']) || empty($result['url'])) {
return [];
}
$payload['originUrl'] = $payload['originUrl'] ?? $sourceUrl;
$payload['ossUrl'] = $result['url'];
$payload['url'] = $result['url'];
return ['content' => self::encodeJson($payload)];
}
if (!MediaOssArchiveService::isRemoteHttpUrl($content)) {
return [];
}
$result = MediaOssArchiveService::archiveRemoteUrl($content, 'messages', 'audio', (string)$messageId);
if (empty($result['success']) || empty($result['url'])) {
return [];
}
return ['content' => $result['url']];
}
protected static function archiveVideoContent($content, $messageId)
{
$payload = self::decodeJsonObject($content);
if (empty($payload)) {
if (!MediaOssArchiveService::isRemoteHttpUrl($content)) {
return [];
}
$result = MediaOssArchiveService::archiveRemoteUrl($content, 'messages', 'video', (string)$messageId);
if (empty($result['success']) || empty($result['url'])) {
return [];
}
return ['content' => $result['url']];
}
$changed = false;
$previewSource = (string)($payload['previewImageOriginUrl'] ?? $payload['previewImage'] ?? '');
if (MediaOssArchiveService::isRemoteHttpUrl($previewSource)) {
$previewResult = MediaOssArchiveService::archiveRemoteUrl($previewSource, 'messages', 'video_cover', (string)$messageId);
if (!empty($previewResult['success']) && !empty($previewResult['url'])) {
$payload['previewImageOriginUrl'] = $payload['previewImageOriginUrl'] ?? $previewSource;
$payload['previewImageOssUrl'] = $previewResult['url'];
$payload['previewImage'] = $previewResult['url'];
$changed = true;
}
}
$videoSource = (string)($payload['originUrl'] ?? $payload['videoUrl'] ?? $payload['ossUrl'] ?? $payload['tencentUrl'] ?? $payload['url'] ?? '');
if (MediaOssArchiveService::isRemoteHttpUrl($videoSource)) {
$videoResult = MediaOssArchiveService::archiveRemoteUrl($videoSource, 'messages', 'video', (string)$messageId);
if (!empty($videoResult['success']) && !empty($videoResult['url'])) {
$payload['originUrl'] = $payload['originUrl'] ?? $videoSource;
$payload['ossUrl'] = $videoResult['url'];
$payload['videoUrl'] = $videoResult['url'];
$payload['url'] = $videoResult['url'];
$payload['isLoading'] = false;
$changed = true;
}
}
if (!$changed) {
return [];
}
return ['content' => self::encodeJson($payload)];
}
protected static function archiveFileContent($content, $messageId)
{
$payload = self::decodeJsonObject($content);
if (empty($payload) || strtolower((string)($payload['type'] ?? '')) !== 'file') {
return [];
}
$sourceUrl = (string)($payload['originUrl'] ?? $payload['url'] ?? '');
if (!MediaOssArchiveService::isRemoteHttpUrl($sourceUrl)) {
return [];
}
$result = MediaOssArchiveService::archiveRemoteUrl(
$sourceUrl,
'messages',
'file',
(string)$messageId,
[
'extension' => $payload['fileext'] ?? '',
]
);
if (empty($result['success']) || empty($result['url'])) {
return [];
}
$payload['originUrl'] = $payload['originUrl'] ?? $sourceUrl;
$payload['ossUrl'] = $result['url'];
$payload['url'] = $result['url'];
$payload['isDownloading'] = false;
return ['content' => self::encodeJson($payload)];
}
protected static function decodeJsonObject($value)
{
if (!is_string($value)) {
return [];
}
$decoded = json_decode(trim($value), true);
return is_array($decoded) ? $decoded : [];
}
protected static function decodeJsonArray($value)
{
if (is_array($value)) {
return $value;
}
if (!is_string($value) || trim($value) === '') {
return [];
}
$decoded = json_decode($value, true);
return is_array($decoded) ? $decoded : [];
}
protected static function encodeJson(array $payload)
{
return json_encode($payload, JSON_UNESCAPED_UNICODE);
}
protected static function extractFileTitle($rawContent)
{
if (!is_string($rawContent) || trim($rawContent) === '') {
return '文件';
}
if (preg_match('/<title><!\[CDATA\[(.*?)\]\]><\/title>/i', $rawContent, $matches) && !empty($matches[1])) {
return trim($matches[1]);
}
if (preg_match('/<title>([^<]+)<\/title>/i', $rawContent, $matches) && !empty($matches[1])) {
return trim($matches[1]);
}
return '文件';
}
protected static function isVideoUrl($url)
{
if (!is_string($url)) {
return false;
}
return (bool)preg_match('/\.(mp4|mov|avi|webm|mkv)(\?.*)?$/i', $url);
}
}

View File

@@ -283,10 +283,20 @@ Route::group('v1/', function () {
// 旧版场景获客对外接口(计划级 apiKey保持兼容
Route::group('v1/api/scenarios', function () { Route::group('v1/api/scenarios', function () {
Route::any('', 'app\cunkebao\controller\plan\PostExternalApiV1Controller@index'); Route::any('', 'app\cunkebao\controller\plan\PostExternalApiV1Controller@index');
}); });
// 新版开放接口(账号级 apiKey + JWT
// ① 公开:用 apiKey + sign 换取 JWT Token
Route::post('v1/open/auth/token', 'app\common\controller\OpenAuthController@getToken');
// ② 需要 JWT所有业务接口
Route::group('v1/open', function () {
Route::post('scenarios', 'app\common\controller\OpenScenariosController@submit'); // 场景获客线索上报
})->middleware(['jwt']);
//小程序 //小程序
Route::group('v1/frontend', function () { Route::group('v1/frontend', function () {

View File

@@ -21,8 +21,8 @@ class PostCreateAddFriendPlanV1Controller extends BaseController
*/ */
public function generateApiKey() public function generateApiKey()
{ {
// 生成5组随机字符串每组5个字符 // 生成5组随机字符串每组5个字符(包含大小写字母和数字)
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$apiKey = ''; $apiKey = '';
for ($i = 0; $i < 5; $i++) { for ($i = 0; $i < 5; $i++) {

View File

@@ -201,7 +201,7 @@ class GetWechatMomentsV1Controller extends BaseController
// 格式化数据 // 格式化数据
$rows = []; $rows = [];
foreach ($moments as $moment) { foreach ($moments as $moment) {
$resUrls = $this->decodeJson($moment['resUrls'] ?? null); $resUrls = $this->preferredMediaUrls($moment);
$imageUrls = is_array($resUrls) ? $resUrls : []; $imageUrls = is_array($resUrls) ? $resUrls : [];
// 格式化日期和时间 // 格式化日期和时间
@@ -300,7 +300,8 @@ class GetWechatMomentsV1Controller extends BaseController
'content' => $row['content'] ?? '', 'content' => $row['content'] ?? '',
'commentList' => $this->decodeJson($row['commentList'] ?? null), 'commentList' => $this->decodeJson($row['commentList'] ?? null),
'likeList' => $this->decodeJson($row['likeList'] ?? null), 'likeList' => $this->decodeJson($row['likeList'] ?? null),
'resUrls' => $this->decodeJson($row['resUrls'] ?? null), 'resUrls' => $this->preferredMediaUrls($row),
'ossUrls' => $this->decodeJson($row['ossUrls'] ?? null),
'createTime' => $formatTime($row['createTime'] ?? null), 'createTime' => $formatTime($row['createTime'] ?? null),
'momentEntity' => [ 'momentEntity' => [
'lat' => $row['lat'] ?? 0, 'lat' => $row['lat'] ?? 0,
@@ -312,6 +313,16 @@ class GetWechatMomentsV1Controller extends BaseController
]; ];
} }
protected function preferredMediaUrls(array $row): array
{
$ossUrls = $this->decodeJson($row['ossUrls'] ?? null);
if (!empty($ossUrls)) {
return $ossUrls;
}
return $this->decodeJson($row['resUrls'] ?? null);
}
/** /**
* JSON字段解析 * JSON字段解析
* *

View File

@@ -0,0 +1,57 @@
<?php
namespace app\job;
use app\common\service\WechatMediaArchiveService;
use think\facade\Config;
use think\facade\Log;
use think\queue\Job;
use think\Queue;
class MediaArchiveJob
{
const QUEUE_NAME = 'media_archive';
public function fire(Job $job, $data)
{
try {
$scope = $data['scope'] ?? '';
$id = (int)($data['id'] ?? 0);
if (empty($scope) || $id <= 0) {
$job->delete();
return;
}
$success = false;
if ($scope === 'message') {
$success = WechatMediaArchiveService::archiveMessageById($id);
} elseif ($scope === 'moment') {
$success = WechatMediaArchiveService::archiveMomentById($id);
}
if ($success || $job->attempts() >= 3) {
$job->delete();
return;
}
$job->release(Config::get('queue.failed_delay', 10));
} catch (\Exception $e) {
Log::error('媒体资源归档任务失败:' . $e->getMessage(), $data ?: []);
if ($job->attempts() >= 3) {
$job->delete();
} else {
$job->release(Config::get('queue.failed_delay', 10));
}
}
}
public static function dispatch($scope, $id, array $extra = [])
{
$payload = array_merge($extra, [
'scope' => $scope,
'id' => (int)$id,
]);
return Queue::push(self::class, $payload, self::QUEUE_NAME);
}
}

View File

@@ -5,12 +5,75 @@ use app\chukebao\model\Reply;
use app\chukebao\model\ReplyGroup; use app\chukebao\model\ReplyGroup;
use think\Db; use think\Db;
use think\queue\Job; use think\queue\Job;
use app\api\controller\WebSocketController;
use think\facade\Env;
class SyncContentJob class SyncContentJob
{ {
public function fire(Job $job, $data) public function fire(Job $job, $data)
{ {
$toAccountId = '';
$username = Env::get('api.username', '');
$password = Env::get('api.password', '');
if (!empty($username) || !empty($password)) {
$toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId');
}
// 建立WebSocket
$wsController = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]);
// 为了便于改写 appattach这里用 heredoc 先生成 contentXml再 json_encode 成 msgType=49 的 content
$contentXml = <<<'XML'
wxid_480es52qsj2812:
<?xml version="1.0"?>
<msg>
<appmsg appid="" sdkver="0">
<title>性格测试 - 快来测测你的MBTI性格类型</title>
<des>性格测试 - 快来测测你的MBTI性格类型</des>
<type>33</type>
<showtype>0</showtype>
<soundtype>0</soundtype>
<contentattr>0</contentattr>
<sourceusername>gh_5c672bbbc96f@app</sourceusername>
<weappinfo>
<username><![CDATA[gh_5c672bbbc96f@app]]></username>
<appid><![CDATA[]]></appid>
<type>2</type>
<version>50</version>
<weappiconurl><![CDATA[]]></weappiconurl>
<pagepath><![CDATA[pages/index/index.html?uid=1]]></pagepath>
<pkginfo>
<type>0</type>
<md5><![CDATA[]]></md5>
</pkginfo>
<wadynamicpageinfo>
<shouldUseDynamicPage>0</shouldUseDynamicPage>
<cacheKey><![CDATA[]]></cacheKey>
</wadynamicpageinfo>
<appservicetype>0</appservicetype>
</weappinfo>
</appmsg>
</msg>
XML;
$contentPayload = [
'contentXml' => $contentXml,
'previewImage' => 'https://ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com/weremote/chat-logs/5E2C38F5A275450D935F3ECEC076124E/57fbafda482b09e1ac93f39fe4f34d47/3/192c0acf93b4b106e7d2eafdd278b0cc14e6693b',
'type' => 'miniprogram',
];
$ddd = $wsController->sendPersonal([
'wechatFriendId' => 17453058,
'wechatAccountId' => 300745,
'msgType' => 49,
'content' => json_encode($contentPayload, JSON_UNESCAPED_UNICODE),
]);
exit_data($ddd);
$ddd= Db::table('s2_wechat_friend')->where('ownerWechatId','wxid_h7nsh7vxseyn29')->select(); $ddd= Db::table('s2_wechat_friend')->where('ownerWechatId','wxid_h7nsh7vxseyn29')->select();
foreach ($ddd as $v) { foreach ($ddd as $v) {
$d = Db::table('ck_task_customer')->where('task_id','167')->where('phone',$v['wechatId'])->find(); $d = Db::table('ck_task_customer')->where('task_id','167')->where('phone',$v['wechatId'])->find();

View File

@@ -333,8 +333,9 @@ class WorkbenchGroupPushJob
{ {
$sendData = []; $sendData = [];
// 内容处理 // 内容处理(小程序素材 content 为 JSON不能按文本推送
if (!empty($content['content'])) { $contentTypeNum = (int)($content['contentType'] ?? 0);
if (!empty($content['content']) && $contentTypeNum !== 5) {
// 京东转链 // 京东转链
if (!empty($config['promotionSiteId'])) { if (!empty($config['promotionSiteId'])) {
$WorkbenchController = new WorkbenchController(); $WorkbenchController = new WorkbenchController();
@@ -430,6 +431,49 @@ class WorkbenchGroupPushJob
]; ];
} }
break; break;
case 5:
// 小程序content 为 JSON与触客宝快捷语 / 存客宝表单一致(可含 body 内容消息)
$mini = json_decode($content['content'] ?? '', true);
if (is_array($mini) && ($mini['type'] ?? '') === 'miniprogram') {
$body = trim((string)($mini['body'] ?? $mini['contentMessage'] ?? $mini['message'] ?? ''));
if ($body !== '') {
if ($type == 'group') {
$sendData[] = [
'content' => $body,
'msgType' => 1,
'wechatAccountId' => $wechatAccountId,
'wechatChatroomId' => $targetId,
];
} else {
$sendData[] = [
'content' => $body,
'msgType' => 1,
];
}
}
$miniPayload = [
'type' => 'miniprogram',
'title' => $mini['title'] ?? '',
'des' => $mini['des'] ?? '',
'gh' => $mini['gh'] ?? '',
'pagepath' => $mini['pagepath'] ?? '',
'previewImage' => $mini['previewImage'] ?? '',
];
if ($type == 'group') {
$sendData[] = [
'content' => $miniPayload,
'msgType' => 49,
'wechatAccountId' => $wechatAccountId,
'wechatChatroomId' => $targetId,
];
} else {
$sendData[] = [
'content' => $miniPayload,
'msgType' => 49,
];
}
}
break;
} }
return $sendData; return $sendData;

View File

@@ -81,6 +81,17 @@ return [
// 中频任务(每 2-5 分钟) // 中频任务(每 2-5 分钟)
// =========================== // ===========================
// 内容库同步
'content_collect' => [
'name' => '同步内容库',
'command' => 'content:collect',
'schedule' => '*/5 * * * *', // 每5分钟
'options' => [],
'enabled' => true,
'max_concurrent' => 1,
'log_file' => 'content_collect.log',
],
// 同步微信设备列表(未删除设备),用于设备管理与监控 // 同步微信设备列表(未删除设备),用于设备管理与监控
'device_active' => [ 'device_active' => [
'name' => '同步微信设备列表(未删除)', 'name' => '同步微信设备列表(未删除)',
@@ -354,16 +365,6 @@ return [
'log_file' => 'crontab_department.log', 'log_file' => 'crontab_department.log',
], ],
// 同步内容库,将外部内容同步到系统内容库
'content_sync' => [
'name' => '同步内容库',
'command' => 'content:sync',
'schedule' => '0 2 * * *', // 每天2点
'options' => [],
'enabled' => true,
'max_concurrent' => 1,
'log_file' => 'crontab_content_sync.log',
],
// 朋友圈采集任务,采集好友朋友圈内容 // 朋友圈采集任务,采集好友朋友圈内容
'moments_collect' => [ 'moments_collect' => [

View File

@@ -4,85 +4,85 @@
```bash ```bash
# 设备列表 # 设备列表
*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think device:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/device_list.log 2>&1 */30 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think device:list >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/device_list.log 2>&1
# 微信好友列表 # 微信好友列表
*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatFriends:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/wechat_friends_list.log 2>&1 */5 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think wechatFriends:list >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/wechat_friends_list.log 2>&1
# 微信群列表 # 微信群列表
*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatChatroom:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/wechat_chatroom_list.log 2>&1 */30 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think wechatChatroom:list >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/wechat_chatroom_list.log 2>&1
# 添加好友任务列表 # 添加好友任务列表
*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think friendTask:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/friend_task_list.log 2>&1 */30 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think friendTask:list >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/friend_task_list.log 2>&1
# 微信客服列表 # 微信客服列表
*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatList:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/wechat_list.log 2>&1 */30 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think wechatList:list >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/wechat_list.log 2>&1
# 公司账号列表 # 公司账号列表
*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think account:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/account_list.log 2>&1 */30 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think account:list >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/account_list.log 2>&1
# 微信好友消息列表 # 微信好友消息列表
*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think message:friendsList >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/message_friends_list.log 2>&1 */30 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think message:friendsList >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/message_friends_list.log 2>&1
# 微信群聊消息列表 # 微信群聊消息列表
*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think message:chatroomList >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/message_chatroom_list.log 2>&1 */30 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think message:chatroomList >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/message_chatroom_list.log 2>&1
# 部门列表 # 部门列表
*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think department:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/department_list.log 2>&1 */30 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think department:list >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/department_list.log 2>&1
# 同步内容库 # 同步内容库
0 2 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think content:sync >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/content_sync.log 2>&1 0 2 * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think content:sync >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/content_sync.log 2>&1
# 微信群好友列表 # 微信群好友列表
*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think groupFriends:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/group_friends_list.log 2>&1 */30 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think groupFriends:list >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/group_friends_list.log 2>&1
# 获取通话记录 # 获取通话记录
*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think call-recording:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/call_recording.log 2>&1 */30 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think call-recording:list >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/call_recording.log 2>&1
# 分配规则列表 # 分配规则列表
0 3 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think allotrule:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/allot_rule_list.log 2>&1 0 3 * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think allotrule:list >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/allot_rule_list.log 2>&1
# 自动创建分配规则 # 自动创建分配规则
0 4 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think allotrule:autocreate >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/allot_rule_autocreate.log 2>&1 0 4 * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think allotrule:autocreate >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/allot_rule_autocreate.log 2>&1
# 内容采集任务 # 内容采集任务
0 5 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think content:collect >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/content_collect.log 2>&1 0 5 * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think content:collect >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/content_collect.log 2>&1
# 朋友圈采集任务 # 朋友圈采集任务
0 6 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think moments:collect >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/moments_collect.log 2>&1 0 6 * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think moments:collect >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/moments_collect.log 2>&1
# 工作台自动点赞任务 # 工作台自动点赞任务
0 7 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:autoLike >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_auto_like.log 2>&1 0 7 * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think workbench:autoLike >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/workbench_auto_like.log 2>&1
# 工作台朋友圈同步任务 # 工作台朋友圈同步任务
0 8 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:moments >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_moments.log 2>&1 0 8 * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think workbench:moments >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/workbench_moments.log 2>&1
# 同步微信数据到存客宝 # 同步微信数据到存客宝
0 9 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think sync:wechatData >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/sync_wechat_data.log 2>&1 0 9 * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think sync:wechatData >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/sync_wechat_data.log 2>&1
# 工作台群发消息 # 工作台群发消息
*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:groupPush >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_groupPush.log 2>&1 */2 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think workbench:groupPush >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/workbench_groupPush.log 2>&1
# 工作台建群 # 工作台建群
*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:groupCreate >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_groupCreate.log 2>&1 */5 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think workbench:groupCreate >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/workbench_groupCreate.log 2>&1
# 工作台通讯录导入 # 工作台通讯录导入
*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:import-contact >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/import_contact.log 2>&1 */5 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think workbench:import-contact >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/import_contact.log 2>&1
# 工作台流量分发 # 工作台流量分发
0 9 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:trafficDistribute >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/traffic_distribute.log 2>&1 0 9 * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think workbench:trafficDistribute >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/traffic_distribute.log 2>&1
# 预防性切换好友 # 预防性切换好友
*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think switch:friends >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/switch_friends.log 2>&1 */2 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think switch:friends >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/switch_friends.log 2>&1
# 消息提醒 # 消息提醒
*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think kf:notice >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/kf_notice.log 2>&1 */1 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think kf:notice >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/kf_notice.log 2>&1
# 客服评分 # 客服评分
0 2 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechat:calculate-score >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/calculate_score.log 2>&1 0 2 * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think wechat:calculate-score >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/calculate_score.log 2>&1
@@ -92,12 +92,12 @@
## 说明 ## 说明
- 所有命令都在 `/www/wwwroot/mckb_quwanzhi_com/Server` 目录下执行 - 所有命令都在 `/www/wwwroot/ckbapi.quwanzhi.com` 目录下执行
- 默认只获取未删除(活跃)的设备、微信好友和群聊 - 默认只获取未删除(活跃)的设备、微信好友和群聊
- 已注释的命令(以#开头)是获取已删除或已停用数据的任务,可根据需要取消注释启用 - 已注释的命令(以#开头)是获取已删除或已停用数据的任务,可根据需要取消注释启用
- 每个命令的执行结果都会记录到对应的日志文件中 - 每个命令的执行结果都会记录到对应的日志文件中
- 日志文件名格式包含了数据状态(如 `_active`, `_deleted`, `_stopped` - 日志文件名格式包含了数据状态(如 `_active`, `_deleted`, `_stopped`
- 日志文件位于 `/www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/` 目录下 - 日志文件位于 `/www/wwwroot/ckbapi.quwanzhi.com/runtime/log/` 目录下
- 大部分任务每5分钟执行一次`*/5 * * * *` 表示每小时的第0,5,10,15...55分钟执行) - 大部分任务每5分钟执行一次`*/5 * * * *` 表示每小时的第0,5,10,15...55分钟执行)
- 设备列表的未删除设备任务每天凌晨1点执行一次`0 1 * * *` - 设备列表的未删除设备任务每天凌晨1点执行一次`0 1 * * *`
- 自动创建分配规则每小时整点执行一次(`0 * * * *` - 自动创建分配规则每小时整点执行一次(`0 * * * *`
@@ -120,64 +120,64 @@ crontab -l
# 设备列表 - 未删除设备(每半小时执行) # 设备列表 - 未删除设备(每半小时执行)
*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think device:list --isDel=0 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_device_active.log 2>&1 */5 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think device:list --isDel=0 >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/crontab_device_active.log 2>&1
# 设备列表 - 已删除设备每天1点执行 # 设备列表 - 已删除设备每天1点执行
0 1 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think device:list --isDel=1 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_device_deleted.log 2>&1 0 1 * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think device:list --isDel=1 >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/crontab_device_deleted.log 2>&1
# 设备列表 - 已停用设备每天1:10执行 # 设备列表 - 已停用设备每天1:10执行
10 1 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think device:list --isDel=2 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_device_stopped.log 2>&1 10 1 * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think device:list --isDel=2 >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/crontab_device_stopped.log 2>&1
# 微信好友列表 - 未删除好友每1分钟执行 # 微信好友列表 - 未删除好友每1分钟执行
*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatFriends:list --isDel=0 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_wechatFriends_active.log 2>&1 */1 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think wechatFriends:list --isDel=0 >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/crontab_wechatFriends_active.log 2>&1
# 微信好友列表 - 已删除好友每天1:30分执行 # 微信好友列表 - 已删除好友每天1:30分执行
30 1 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatFriends:list --isDel=1 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_wechatFriends_deleted.log 2>&1 30 1 * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think wechatFriends:list --isDel=1 >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/crontab_wechatFriends_deleted.log 2>&1
# 微信群列表 - 未删除群每5分钟执行 # 微信群列表 - 未删除群每5分钟执行
*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatChatroom:list --isDel=0 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_wechatChatroom_active.log 2>&1 */5 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think wechatChatroom:list --isDel=0 >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/crontab_wechatChatroom_active.log 2>&1
# 微信群列表 - 已删除群每天1:30分执行 # 微信群列表 - 已删除群每天1:30分执行
30 1 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatChatroom:list --isDel=1 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_wechatChatroom_deleted.log 2>&1 30 1 * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think wechatChatroom:list --isDel=1 >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/crontab_wechatChatroom_deleted.log 2>&1
# 微信群好友列表没5分钟执行 # 微信群好友列表没5分钟执行
*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think groupFriends:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_groupFriends.log 2>&1 */5 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think groupFriends:list >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/crontab_groupFriends.log 2>&1
# 添加好友任务列表(每1分钟执行) # 添加好友任务列表(每1分钟执行)
*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think friendTask:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_friendTask.log 2>&1 */1 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think friendTask:list >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/crontab_friendTask.log 2>&1
# 微信客服列表每5分钟执行 # 微信客服列表每5分钟执行
*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatList:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_wechatList.log 2>&1 */5 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think wechatList:list >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/crontab_wechatList.log 2>&1
# 公司账号列表每5分钟执行 # 公司账号列表每5分钟执行
*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think account:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_account.log 2>&1 */5 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think account:list >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/crontab_account.log 2>&1
# 微信好友消息列表每30分钟执行 # 微信好友消息列表每30分钟执行
*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think message:friendsList >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_messageFriends.log 2>&1 */1 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think message:friendsList >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/crontab_messageFriends.log 2>&1
# 微信群聊消息列表每30分钟执行 # 微信群聊消息列表每30分钟执行
*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think message:chatroomList >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_messageChatroom.log 2>&1 */1 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think message:chatroomList >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/crontab_messageChatroom.log 2>&1
# 获取通话记录 # 获取通话记录
*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think call-recording:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/call_recording.log 2>&1 */30 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think call-recording:list >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/call_recording.log 2>&1
# 清洗微信数据 # 清洗微信数据
*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think sync:wechatData >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/sync_wechat_data.log 2>&1 */2 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think sync:wechatData >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/sync_wechat_data.log 2>&1
# 内容采集任务每5分钟执行 # 内容采集任务每5分钟执行
*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think content:collect >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_contentCollect.log 2>&1 */5 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think content:collect >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/crontab_contentCollect.log 2>&1
# 工作台任务_自动点赞每10分钟执行 # 工作台任务_自动点赞每10分钟执行
*/6 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:autoLike >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_workbench_autoLike.log 2>&1 */6 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think workbench:autoLike >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/crontab_workbench_autoLike.log 2>&1
# 每3天的3点同步所有好友 # 每3天的3点同步所有好友
0 3 */3 * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think sync:allFriends >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/all_friends.log 2>&1 0 3 */3 * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think sync:allFriends >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/all_friends.log 2>&1
# 工作台流量分发 # 工作台流量分发
*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:trafficDistribute >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/traffic_distribute.log 2>&1 */2 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think workbench:trafficDistribute >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/traffic_distribute.log 2>&1
# 工作台朋友圈同步任务 # 工作台朋友圈同步任务
*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:moments >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_moments.log 2>&1 */2 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think workbench:moments >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/workbench_moments.log 2>&1
# 工作台群发消息 # 工作台群发消息
#*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:groupPush >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_groupPush.log 2>&1 #*/2 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think workbench:groupPush >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/workbench_groupPush.log 2>&1
# 预防性切换好友 # 预防性切换好友
*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think switch:friends >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/switch_friends.log 2>&1 */2 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think switch:friends >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/switch_friends.log 2>&1
# 工作台建群 # 工作台建群
*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:groupCreate >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_groupCreate.log 2>&1 */5 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think workbench:groupCreate >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/workbench_groupCreate.log 2>&1
# 工作台通讯录导入 # 工作台通讯录导入
*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:import-contact >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/import_contact.log 2>&1 */5 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think workbench:import-contact >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/import_contact.log 2>&1
# 工作台入群欢迎语 # 工作台入群欢迎语
*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:groupWelcome >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_groupWelcome.log 2>&1 */1 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think workbench:groupWelcome >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/workbench_groupWelcome.log 2>&1
# 消息提醒 # 消息提醒
*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think kf:notice >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/kf_notice.log 2>&1 */1 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think kf:notice >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/kf_notice.log 2>&1
# 客服评分 # 客服评分
0 2 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechat:calculate-score >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/calculate_score.log 2>&1 0 2 * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think wechat:calculate-score >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/calculate_score.log 2>&1
# 采集客服自己的朋友圈 # 采集客服自己的朋友圈
*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think own:moments:collect >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/own_moments_collect.log 2>&1 */30 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think own:moments:collect >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/own_moments_collect.log 2>&1
# 检查未读/未回复消息并自动迁移好友 # 检查未读/未回复消息并自动迁移好友
*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think check:unread-message --minutes=30 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/check_unread_message.log 2>&1 */5 * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think check:unread-message --minutes=30 >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/check_unread_message.log 2>&1
# 每分钟执行一次调度器(调度器内部会自动判断哪些任务需要执行) # 每分钟执行一次调度器(调度器内部会自动判断哪些任务需要执行)
* * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think scheduler:run >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/scheduler.log 2>&1 * * * * * cd /www/wwwroot/ckbapi.quwanzhi.com && php think scheduler:run >> /www/wwwroot/ckbapi.quwanzhi.com/runtime/log/scheduler.log 2>&1

View File

@@ -592,6 +592,14 @@ class Adapter implements WeChatServiceInterface
// 建立WebSocket // 建立WebSocket
$wsController = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]); $wsController = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]);
// 获取发送方 wechatId例如 wxid_xxx用于拼接 miniprogram 的 contentXml
$senderWxid = '';
if (!empty($wechatAccountId)) {
$senderWxid = Db::table('s2_wechat_account')
->where('id', $wechatAccountId)
->value('wechatId') ?? '';
}
$gap = 0; $gap = 0;
foreach ($msgConf as $messages) { foreach ($msgConf as $messages) {
@@ -626,7 +634,73 @@ class Adapter implements WeChatServiceInterface
case 'miniprogram': case 'miniprogram':
$msgType = 49; $msgType = 49;
$detail = ''; $title = $content['title'] ?? '';
$des = $content['des'] ?? ($content['description'] ?? '');
$gh = $content['gh'] ?? ($content['miniProgramId'] ?? ($content['nativeId'] ?? ''));
$pagepath = $content['pagepath'] ?? ($content['pagePath'] ?? '');
$previewImage = $content['previewImage'] ?? ($content['cover'] ?? ($content['thumbPath'] ?? ''));
$senderWxidLocal = $content['wxid'] ?? ($content['senderWxid'] ?? $senderWxid);
// 兼容:如果外部已经直接传了 contentXml就不再按字段拼 xml
if (!empty($content['contentXml']) && !empty($previewImage) && !empty($senderWxidLocal)) {
$detailArray = [
'contentXml' => $content['contentXml'],
'previewImage' => $previewImage,
'type' => 'miniprogram',
];
$detail = json_encode($detailArray, JSON_UNESCAPED_UNICODE);
break;
}
if (empty($senderWxidLocal) || empty($title) || empty($des) || empty($gh) || empty($pagepath) || empty($previewImage)) {
$detail = '';
break;
}
$ghUsername = (string)$gh;
if (strpos($ghUsername, '@app') === false) {
$ghUsername .= '@app';
}
$contentXml = $senderWxidLocal . ":\n" . <<<XML
<?xml version="1.0"?>
<msg>
<appmsg appid="" sdkver="0">
<title>{$title}</title>
<des>{$des}</des>
<type>33</type>
<showtype>0</showtype>
<soundtype>0</soundtype>
<contentattr>0</contentattr>
<sourceusername>{$ghUsername}</sourceusername>
<weappinfo>
<username><![CDATA[{$ghUsername}]]></username>
<appid><![CDATA[]]></appid>
<type>2</type>
<version>50</version>
<weappiconurl><![CDATA[]]></weappiconurl>
<pagepath><![CDATA[{$pagepath}]]></pagepath>
<pkginfo>
<type>0</type>
<md5><![CDATA[]]></md5>
</pkginfo>
<wadynamicpageinfo>
<shouldUseDynamicPage>0</shouldUseDynamicPage>
<cacheKey><![CDATA[]]></cacheKey>
</wadynamicpageinfo>
<appservicetype>0</appservicetype>
</weappinfo>
</appmsg>
</msg>
XML;
$detailArray = [
'contentXml' => $contentXml,
'previewImage' => $previewImage,
'type' => 'miniprogram',
];
$detail = json_encode($detailArray, JSON_UNESCAPED_UNICODE);
break; break;
case 'link': case 'link':