Compare commits
5 Commits
feature/ne
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a28c9ae75 | ||
|
|
edfc21c373 | ||
|
|
b1bfcec35b | ||
|
|
3bf1b2aee9 | ||
|
|
29413f57c7 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,3 +17,4 @@ nginx.htaccess
|
||||
thinkphp/
|
||||
public/static/
|
||||
*.log
|
||||
Server.code-workspace
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
```bash
|
||||
# 每分钟执行一次调度器(调度器内部会根据 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,7 +4,10 @@ namespace app\api\controller;
|
||||
|
||||
use app\api\model\WechatMessageModel;
|
||||
use app\common\service\FriendTransferService;
|
||||
use app\common\service\WechatMediaArchiveService;
|
||||
use app\job\MediaArchiveJob;
|
||||
use think\Db;
|
||||
use think\facade\Log;
|
||||
use think\facade\Request;
|
||||
|
||||
class MessageController extends BaseController
|
||||
@@ -418,6 +421,7 @@ class MessageController extends BaseController
|
||||
'type' => 1,
|
||||
'accountId' => $item['accountId'],
|
||||
'content' => $item['content'],
|
||||
'originalContent' => $item['content'],
|
||||
'createTime' => $createTime,
|
||||
'deleteTime' => $deleteTime,
|
||||
'isDeleted' => $item['isDeleted'] ?? false,
|
||||
@@ -463,6 +467,9 @@ class MessageController extends BaseController
|
||||
}else{
|
||||
$id = $data['id'];
|
||||
unset($data['id']);
|
||||
if (!empty($exists['originalContent'])) {
|
||||
unset($data['originalContent']);
|
||||
}
|
||||
$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;
|
||||
}
|
||||
|
||||
@@ -577,6 +587,9 @@ class MessageController extends BaseController
|
||||
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;
|
||||
} 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和消息内容
|
||||
* @param string $content 原始消息内容
|
||||
|
||||
@@ -12,7 +12,7 @@ use think\facade\Env;
|
||||
use app\api\model\WechatFriendModel as WechatFriend;
|
||||
use app\api\model\WechatMomentsModel as WechatMoments;
|
||||
use think\facade\Cache;
|
||||
use app\common\util\AliyunOSS;
|
||||
use app\common\service\MediaOssArchiveService;
|
||||
|
||||
|
||||
class WebSocketController extends BaseController
|
||||
@@ -468,7 +468,7 @@ class WebSocketController extends BaseController
|
||||
// 更新数据库:保存原始URL和OSS URL,并标记已上传
|
||||
$updateData = [
|
||||
'resUrls' => $urls,
|
||||
'isOssUploaded' => 1, // 标识已上传到OSS
|
||||
'isOssUploaded' => !empty($ossUrls) ? 1 : 0,
|
||||
'update_time' => time()
|
||||
];
|
||||
|
||||
@@ -513,7 +513,7 @@ class WebSocketController extends BaseController
|
||||
// 更新数据库:保存原始URL和OSS URL,并标记已上传
|
||||
$updateData = [
|
||||
'resUrls' => $urls,
|
||||
'isOssUploaded' => 1, // 标识已上传到OSS
|
||||
'isOssUploaded' => !empty($ossUrls) ? 1 : 0,
|
||||
'update_time' => time()
|
||||
];
|
||||
|
||||
@@ -557,76 +557,23 @@ class WebSocketController extends BaseController
|
||||
if (empty($urls) || !is_array($urls)) {
|
||||
return $ossUrls;
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建临时目录(兼容无 runtime_path() 辅助函数的环境)
|
||||
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 {
|
||||
// 兜底:使用项目根目录下的 runtime 目录
|
||||
$baseRuntimePath = rtrim(ROOT_PATH, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'runtime' . DIRECTORY_SEPARATOR;
|
||||
|
||||
foreach ($urls as $url) {
|
||||
if (!MediaOssArchiveService::isRemoteHttpUrl($url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$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 (!empty($result['success']) && !empty($result['url'])) {
|
||||
$ossUrls[] = $result['url'];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_dir($tempDir)) {
|
||||
mkdir($tempDir, 0755, true);
|
||||
}
|
||||
|
||||
foreach ($urls as $index => $url) {
|
||||
if (empty($url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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());
|
||||
Log::error('朋友圈媒体上传OSS失败:' . ($result['error'] ?? '未知错误'), [
|
||||
'snsId' => $snsId,
|
||||
'url' => $url,
|
||||
]);
|
||||
}
|
||||
|
||||
return $ossUrls;
|
||||
@@ -696,8 +643,8 @@ class WebSocketController extends BaseController
|
||||
}
|
||||
|
||||
// 获取资源链接(检查是否已上传到OSS,如果已上传则跳过)
|
||||
if(empty($momentEntity['urls']) || $moment['type'] != 1) {
|
||||
// 如果没有urls或类型不是1,跳过
|
||||
if(empty($momentEntity['urls'])) {
|
||||
// 如果没有urls,跳过
|
||||
} elseif ($isOssUploaded == 1) {
|
||||
// 如果已上传到OSS,跳过采集
|
||||
} else {
|
||||
|
||||
@@ -37,6 +37,8 @@ class DataProcessing extends BaseController
|
||||
'CmdChatroomOperate', //修改群信息 {chatroomName(群名)、announce(公告)、extra(公告)、wechatAccountId、wechatChatroomId}
|
||||
'CmdNewMessage', //接收消息
|
||||
'CmdSendMessageResult', //更新消息状态
|
||||
'CmdDownloadVideoResult', //视频下载结果回写
|
||||
'CmdDownloadFileResult', //文件下载结果回写
|
||||
'CmdPinToTop', //置顶
|
||||
];
|
||||
|
||||
@@ -168,6 +170,34 @@ class DataProcessing extends BaseController
|
||||
|
||||
$msg = '更新消息状态成功';
|
||||
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': //置顶
|
||||
$wechatFriendId = $this->request->param('wechatFriendId', 0);
|
||||
$wechatChatroomId = $this->request->param('wechatChatroomId', 0);
|
||||
|
||||
@@ -53,4 +53,5 @@ return [
|
||||
|
||||
// V2 流量池数据迁移
|
||||
'migrate:trafficPoolV2' => 'app\command\MigrateTrafficPoolV2Command', // 迁移数据到 V2 流量池系统
|
||||
'media:archive' => 'app\command\BackfillMediaOssCommand', // 历史媒体资源归档到OSS
|
||||
];
|
||||
|
||||
121
application/command/BackfillMediaOssCommand.php
Normal file
121
application/command/BackfillMediaOssCommand.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ use think\console\Command;
|
||||
use think\facade\App;
|
||||
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
|
||||
{
|
||||
protected $lockFile;
|
||||
|
||||
@@ -73,6 +73,43 @@ class TrafficPoolSource extends Model
|
||||
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
|
||||
@@ -224,11 +261,10 @@ class TrafficPoolSource extends Model
|
||||
|
||||
// 尝试获取好友头像
|
||||
if (!empty($source['sourceWechatId'])) {
|
||||
$sourceData['sourceAvatar'] = Db::table('ck_traffic_pool')
|
||||
$avatar = Db::table('ck_traffic_pool')
|
||||
->where('wechatId', $source['sourceWechatId'])
|
||||
->value('avatar') ?: Db::table('s2_wechat_friend')
|
||||
->where('wechatId', $source['sourceWechatId'])
|
||||
->value('headImgUrl') ?: '';
|
||||
->value('avatar');
|
||||
$sourceData['sourceAvatar'] = $avatar ?: self::getFriendHeadImgUrl((string)$source['sourceWechatId']);
|
||||
}
|
||||
} else {
|
||||
$sourceData['chatroomOwners'] = [];
|
||||
@@ -448,11 +484,10 @@ class TrafficPoolSource extends Model
|
||||
$sourceData['chatroomInfo'] = null;
|
||||
// 尝试获取好友头像
|
||||
if (!empty($source['sourceWechatId'])) {
|
||||
$sourceData['sourceAvatar'] = Db::table('ck_traffic_pool')
|
||||
$avatar = Db::table('ck_traffic_pool')
|
||||
->where('wechatId', $source['sourceWechatId'])
|
||||
->value('avatar') ?: Db::table('s2_wechat_friend')
|
||||
->where('wechatId', $source['sourceWechatId'])
|
||||
->value('headImgUrl') ?: '';
|
||||
->value('avatar');
|
||||
$sourceData['sourceAvatar'] = $avatar ?: self::getFriendHeadImgUrl((string)$source['sourceWechatId']);
|
||||
}
|
||||
} else {
|
||||
$sourceData['chatroomOwners'] = [];
|
||||
|
||||
261
application/common/service/MediaOssArchiveService.php
Normal file
261
application/common/service/MediaOssArchiveService.php
Normal 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;
|
||||
}
|
||||
}
|
||||
372
application/common/service/WechatMediaArchiveService.php
Normal file
372
application/common/service/WechatMediaArchiveService.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -283,10 +283,20 @@ Route::group('v1/', function () {
|
||||
|
||||
|
||||
|
||||
// 旧版场景获客对外接口(计划级 apiKey,保持兼容)
|
||||
Route::group('v1/api/scenarios', function () {
|
||||
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 () {
|
||||
|
||||
@@ -21,8 +21,8 @@ class PostCreateAddFriendPlanV1Controller extends BaseController
|
||||
*/
|
||||
public function generateApiKey()
|
||||
{
|
||||
// 生成5组随机字符串,每组5个字符
|
||||
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
// 生成5组随机字符串,每组5个字符(包含大小写字母和数字)
|
||||
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
$apiKey = '';
|
||||
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
|
||||
@@ -201,7 +201,7 @@ class GetWechatMomentsV1Controller extends BaseController
|
||||
// 格式化数据
|
||||
$rows = [];
|
||||
foreach ($moments as $moment) {
|
||||
$resUrls = $this->decodeJson($moment['resUrls'] ?? null);
|
||||
$resUrls = $this->preferredMediaUrls($moment);
|
||||
$imageUrls = is_array($resUrls) ? $resUrls : [];
|
||||
|
||||
// 格式化日期和时间
|
||||
@@ -300,7 +300,8 @@ class GetWechatMomentsV1Controller extends BaseController
|
||||
'content' => $row['content'] ?? '',
|
||||
'commentList' => $this->decodeJson($row['commentList'] ?? 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),
|
||||
'momentEntity' => [
|
||||
'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字段解析
|
||||
*
|
||||
|
||||
57
application/job/MediaArchiveJob.php
Normal file
57
application/job/MediaArchiveJob.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,75 @@ use app\chukebao\model\Reply;
|
||||
use app\chukebao\model\ReplyGroup;
|
||||
use think\Db;
|
||||
use think\queue\Job;
|
||||
use app\api\controller\WebSocketController;
|
||||
use think\facade\Env;
|
||||
|
||||
class SyncContentJob
|
||||
{
|
||||
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();
|
||||
foreach ($ddd as $v) {
|
||||
$d = Db::table('ck_task_customer')->where('task_id','167')->where('phone',$v['wechatId'])->find();
|
||||
|
||||
@@ -333,8 +333,9 @@ class WorkbenchGroupPushJob
|
||||
{
|
||||
$sendData = [];
|
||||
|
||||
// 内容处理
|
||||
if (!empty($content['content'])) {
|
||||
// 内容处理(小程序素材 content 为 JSON,不能按文本推送)
|
||||
$contentTypeNum = (int)($content['contentType'] ?? 0);
|
||||
if (!empty($content['content']) && $contentTypeNum !== 5) {
|
||||
// 京东转链
|
||||
if (!empty($config['promotionSiteId'])) {
|
||||
$WorkbenchController = new WorkbenchController();
|
||||
@@ -430,6 +431,49 @@ class WorkbenchGroupPushJob
|
||||
];
|
||||
}
|
||||
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;
|
||||
|
||||
@@ -81,6 +81,17 @@ return [
|
||||
// 中频任务(每 2-5 分钟)
|
||||
// ===========================
|
||||
|
||||
// 内容库同步
|
||||
'content_collect' => [
|
||||
'name' => '同步内容库',
|
||||
'command' => 'content:collect',
|
||||
'schedule' => '*/5 * * * *', // 每5分钟
|
||||
'options' => [],
|
||||
'enabled' => true,
|
||||
'max_concurrent' => 1,
|
||||
'log_file' => 'content_collect.log',
|
||||
],
|
||||
|
||||
// 同步微信设备列表(未删除设备),用于设备管理与监控
|
||||
'device_active' => [
|
||||
'name' => '同步微信设备列表(未删除)',
|
||||
@@ -354,16 +365,6 @@ return [
|
||||
'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' => [
|
||||
|
||||
116
crontab_tasks.md
116
crontab_tasks.md
@@ -4,85 +4,85 @@
|
||||
|
||||
```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`)
|
||||
- 日志文件位于 `/www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/` 目录下
|
||||
- 日志文件位于 `/www/wwwroot/ckbapi.quwanzhi.com/runtime/log/` 目录下
|
||||
- 大部分任务每5分钟执行一次(`*/5 * * * *` 表示每小时的第0,5,10,15...55分钟执行)
|
||||
- 设备列表的未删除设备任务每天凌晨1点执行一次(`0 1 * * *`)
|
||||
- 自动创建分配规则每小时整点执行一次(`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点执行)
|
||||
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执行)
|
||||
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 * * * * 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分执行)
|
||||
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 * * * * 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分执行)
|
||||
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 * * * * 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 * * * * 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 * * * * 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 * * * * 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分钟执行)
|
||||
*/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分钟执行)
|
||||
*/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 * * * * 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分钟执行)
|
||||
*/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点同步所有好友
|
||||
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
|
||||
|
||||
@@ -592,6 +592,14 @@ class Adapter implements WeChatServiceInterface
|
||||
// 建立WebSocket
|
||||
$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;
|
||||
foreach ($msgConf as $messages) {
|
||||
@@ -626,7 +634,73 @@ class Adapter implements WeChatServiceInterface
|
||||
|
||||
case 'miniprogram':
|
||||
$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;
|
||||
|
||||
case 'link':
|
||||
|
||||
Reference in New Issue
Block a user