Merge branch 'develop' of https://gitee.com/cunkebao/cunkebao_v3 into wong-dev
# Conflicts: # Server/composer.json
This commit is contained in:
@@ -201,40 +201,38 @@ class UserController extends BaseController
|
||||
* 修改密码
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function modifyPwd()
|
||||
public function modifyPwd($data = [])
|
||||
{
|
||||
// 获取并验证参数
|
||||
$params = $this->validateModifyPwdParams();
|
||||
if (!is_array($params)) {
|
||||
return $params;
|
||||
|
||||
if (empty($data)) {
|
||||
return json_encode(['code' => 400,'msg' => '参数缺失']);
|
||||
}
|
||||
|
||||
$authorization = trim($this->request->header('authorization', $this->authorization));
|
||||
if (!isset($data['id']) || !isset($data['pwd'])) {
|
||||
return json_encode(['code' => 401,'msg' => '参数缺失']);
|
||||
}
|
||||
$authorization = $this->authorization;
|
||||
|
||||
if (empty($authorization)) {
|
||||
return errorJson('缺少授权信息');
|
||||
return json_encode(['code' => 400,'msg' => '缺少授权信息']);
|
||||
}
|
||||
|
||||
$headerData = ['client:' . self::CLIENT_TYPE];
|
||||
$header = setHeader($headerData, $authorization, 'plain');
|
||||
$headerData = ['client:system'];
|
||||
$header = setHeader($headerData, $authorization, 'json');
|
||||
$params = [
|
||||
'id' => $data['id'],
|
||||
'newPw' => $data['pwd'],
|
||||
];
|
||||
|
||||
try {
|
||||
$result = requestCurl($this->baseUrl . 'api/Account/self', $params, 'PUT', $header);
|
||||
$result = requestCurl($this->baseUrl . 'api/Account/modifypw', $params, 'PUT', $header,'json');
|
||||
$response = handleApiResponse($result);
|
||||
|
||||
if (empty($response)) {
|
||||
// 获取当前用户信息
|
||||
$currentUser = CompanyAccountModel::where('token', $authorization)->find();
|
||||
if ($currentUser) {
|
||||
recordUserLog($currentUser['id'], $currentUser['userName'], 'MODIFY_PASSWORD', '修改密码成功', [], 200, '修改成功');
|
||||
}
|
||||
return successJson(['message' => '修改成功']);
|
||||
return json_encode(['code' => 200,'msg' => '修改成功']);
|
||||
}
|
||||
|
||||
recordUserLog(0, '', 'MODIFY_PASSWORD', '修改密码失败', $params, 500, $response);
|
||||
return errorJson($response);
|
||||
return json_encode(['code' => 400,'msg' => $response]);
|
||||
} catch (\Exception $e) {
|
||||
recordUserLog(0, '', 'MODIFY_PASSWORD', '修改密码异常', $params, 500, $e->getMessage());
|
||||
return errorJson('修改密码失败:' . $e->getMessage());
|
||||
return json_encode(['code' => 400,'msg' => '修改密码失败:' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -457,22 +457,16 @@ class WebSocketController extends BaseController
|
||||
|
||||
// 构建请求参数
|
||||
$params = [
|
||||
"cmdType" => 'CmdDownloadMomentImagesResult',
|
||||
"cmdType" => 'CmdDownloadMomentImages',
|
||||
"snsId" => $data['snsId'],
|
||||
"urls" => $data['snsUrls'],
|
||||
"wechatAccountId" => $data['wechatAccountId'],
|
||||
"seq" => time(),
|
||||
];
|
||||
|
||||
// 记录请求日志
|
||||
Log::info('获取朋友圈资源链接请求:' . json_encode($params, 256));
|
||||
|
||||
// 发送请求
|
||||
$this->client->send(json_encode($params));
|
||||
|
||||
// 接收响应
|
||||
$response = $this->client->receive();
|
||||
$message = json_decode($response, true);
|
||||
$message = $this->sendMessage($params);
|
||||
|
||||
if (empty($message)) {
|
||||
return json_encode(['code' => 500, 'msg' => '获取朋友圈资源链接失败']);
|
||||
@@ -558,15 +552,17 @@ class WebSocketController extends BaseController
|
||||
$dataToSave['create_time'] = time();
|
||||
$res = WechatMoments::create($dataToSave);
|
||||
}
|
||||
// // 获取资源链接
|
||||
// if(empty($momentEntity['resUrls']) && !empty($momentEntity['urls'])){
|
||||
// $snsData = [
|
||||
// 'snsId' => $moment['snsId'],
|
||||
// 'snsUrls' => $momentEntity['urls'],
|
||||
// 'wechatAccountId' => $wechatAccountId,
|
||||
// ];
|
||||
// $this->getMomentSourceRealUrl($snsData);
|
||||
// }
|
||||
|
||||
|
||||
// 获取资源链接
|
||||
if(empty($momentEntity['resUrls']) && !empty($momentEntity['urls']) && $moment['type'] == 1) {
|
||||
$snsData = [
|
||||
'snsId' => $moment['snsId'],
|
||||
'snsUrls' => $momentEntity['urls'],
|
||||
'wechatAccountId' => $wechatAccountId,
|
||||
];
|
||||
$this->getMomentSourceRealUrl($snsData);
|
||||
}
|
||||
|
||||
}
|
||||
//Log::write('朋友圈数据已存入数据库,共' . count($momentList) . '条');
|
||||
|
||||
@@ -7,5 +7,5 @@ use think\Model;
|
||||
class WechatAccountModel extends Model
|
||||
{
|
||||
// 设置表名
|
||||
protected $table = 's2_wechat_account';
|
||||
protected $table = 's2_wechat_account';
|
||||
}
|
||||
@@ -14,6 +14,8 @@ Route::group('v1/', function () {
|
||||
Route::get('list', 'app\chukebao\controller\WechatFriendController@getList'); // 获取好友列表
|
||||
Route::get('detail', 'app\chukebao\controller\WechatFriendController@getDetail'); // 获取好友详情
|
||||
Route::post('updateInfo', 'app\chukebao\controller\WechatFriendController@updateFriendInfo'); // 更新好友资料
|
||||
// 添加好友任务记录相关接口
|
||||
Route::get('addTaskList', 'app\chukebao\controller\WechatFriendController@getAddTaskList'); // 获取添加好友任务记录列表(包含添加者信息、状态、时间等,支持状态筛选,无需传好友ID)
|
||||
});
|
||||
//群相关
|
||||
Route::group('wechatChatroom/', function () {
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
namespace app\chukebao\controller;
|
||||
|
||||
use app\api\model\WechatChatroomModel;
|
||||
use library\ResponseHelper;
|
||||
use app\api\model\WechatFriendModel;
|
||||
use app\api\model\WechatMessageModel;
|
||||
use app\api\controller\MessageController;
|
||||
|
||||
|
||||
@@ -33,6 +35,7 @@ class DataProcessing extends BaseController
|
||||
'CmdAllotFriend', //转让好友 {labels、wechatAccountId、wechatFriendId}
|
||||
'CmdChatroomOperate', //修改群信息 {chatroomName(群名)、announce(公告)、extra(公告)、wechatAccountId、wechatChatroomId}
|
||||
'CmdNewMessage', //接收消息
|
||||
'CmdSendMessageResult', //更新消息状态
|
||||
];
|
||||
|
||||
if (empty($type) || empty($wechatAccountId)) {
|
||||
@@ -75,15 +78,31 @@ class DataProcessing extends BaseController
|
||||
if(empty($toAccountId)){
|
||||
return ResponseHelper::error('参数缺失');
|
||||
}
|
||||
|
||||
$friend = WechatFriendModel::where(['id' => $wechatFriendId,'wechatAccountId' => $wechatAccountId])->find();
|
||||
if(empty($friend)){
|
||||
return ResponseHelper::error('好友不存在');
|
||||
if(empty($wechatFriendId) && empty($wechatChatroomId)){
|
||||
return ResponseHelper::error('参数缺失');
|
||||
}
|
||||
$friend->accountId = $toAccountId;
|
||||
$friend->updateTime = time();
|
||||
$friend->save();
|
||||
$msg = '好友转移成功';
|
||||
|
||||
|
||||
if (!empty($wechatFriendId)){
|
||||
$data = WechatFriendModel::where(['id' => $wechatFriendId,'wechatAccountId' => $wechatAccountId])->find();
|
||||
$msg = '好友转移成功';
|
||||
if(empty($data)){
|
||||
return ResponseHelper::error('好友不存在');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!empty($wechatChatroomId)){
|
||||
$data = WechatChatroomModel::where(['id' => $wechatChatroomId,'wechatAccountId' => $wechatAccountId])->find();
|
||||
$msg = '群聊转移成功';
|
||||
if(empty($data)){
|
||||
return ResponseHelper::error('群聊不存在');
|
||||
}
|
||||
}
|
||||
|
||||
$data->accountId = $toAccountId;
|
||||
$data->updateTime = time();
|
||||
$data->save();
|
||||
break;
|
||||
case 'CmdNewMessage':
|
||||
if(empty($friendMessage) && empty($chatroomMessage)){
|
||||
@@ -105,8 +124,46 @@ class DataProcessing extends BaseController
|
||||
$msg = '消息记录成功';
|
||||
}else{
|
||||
$msg = '消息记录失败';
|
||||
$codee = 400;
|
||||
$codee = 200;
|
||||
}
|
||||
break;
|
||||
case 'CmdSendMessageResult':
|
||||
$friendMessageId = $this->request->param('friendMessageId', 0);
|
||||
$chatroomMessageId = $this->request->param('chatroomMessageId', 0);
|
||||
$sendStatus = $this->request->param('sendStatus', null);
|
||||
$wechatTime = $this->request->param('wechatTime', 0);
|
||||
|
||||
if ($sendStatus === null) {
|
||||
return ResponseHelper::error('sendStatus不能为空');
|
||||
}
|
||||
|
||||
if (empty($friendMessageId) && empty($chatroomMessageId)) {
|
||||
return ResponseHelper::error('friendMessageId或chatroomMessageId至少提供一个');
|
||||
}
|
||||
|
||||
$messageId = $friendMessageId ?: $chatroomMessageId;
|
||||
$update = [
|
||||
'sendStatus' => (int)$sendStatus,
|
||||
];
|
||||
|
||||
if (!empty($wechatTime)) {
|
||||
$update['wechatTime'] = strlen((string)$wechatTime) > 10
|
||||
? intval($wechatTime / 1000)
|
||||
: (int)$wechatTime;
|
||||
}
|
||||
|
||||
$affected = WechatMessageModel::where('id', $messageId)->update($update);
|
||||
|
||||
if ($affected === false) {
|
||||
return ResponseHelper::success('','更新消息状态失败');
|
||||
}
|
||||
|
||||
if ($affected === 0) {
|
||||
return ResponseHelper::success('','消息不存在');
|
||||
}
|
||||
|
||||
$msg = '更新消息状态成功';
|
||||
break;
|
||||
}
|
||||
return ResponseHelper::success('',$msg,$codee);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class WechatFriendController extends BaseController
|
||||
$total = $query->count();
|
||||
$list = $query->page($page, $limit)->select();
|
||||
|
||||
// 提取所有好友ID
|
||||
// 提取所有好友ID
|
||||
$friendIds = array_column($list, 'id');
|
||||
|
||||
$aiTypeData = [];
|
||||
@@ -67,7 +67,7 @@ class WechatFriendController extends BaseController
|
||||
$friend = Db::table('s2_wechat_friend')
|
||||
->where(['id' => $friendId, 'isDeleted' => 0])
|
||||
->find();
|
||||
|
||||
|
||||
if (empty($friend)) {
|
||||
return ResponseHelper::error('好友不存在');
|
||||
}
|
||||
@@ -78,11 +78,11 @@ class WechatFriendController extends BaseController
|
||||
$friend['createTime'] = !empty($friend['createTime']) ? date('Y-m-d H:i:s', $friend['createTime']) : '';
|
||||
$friend['updateTime'] = !empty($friend['updateTime']) ? date('Y-m-d H:i:s', $friend['updateTime']) : '';
|
||||
$friend['passTime'] = !empty($friend['passTime']) ? date('Y-m-d H:i:s', $friend['passTime']) : '';
|
||||
|
||||
|
||||
// 获取AI类型设置
|
||||
$aiTypeSetting = FriendSettings::where('friendId', $friendId)->find();
|
||||
$friend['aiType'] = $aiTypeSetting ? $aiTypeSetting['type'] : 0;
|
||||
|
||||
|
||||
return ResponseHelper::success(['detail' => $friend]);
|
||||
}
|
||||
|
||||
@@ -166,4 +166,112 @@ class WechatFriendController extends BaseController
|
||||
|
||||
return ResponseHelper::success(['id' => $friendId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取添加好友任务记录列表(全新功能)
|
||||
* 返回当前账号的所有添加好友任务记录,无论是否通过都展示
|
||||
* 包含:添加者头像、昵称、微信号、添加状态、添加时间、通过时间等信息
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getAddTaskList()
|
||||
{
|
||||
$page = $this->request->param('page', 1);
|
||||
$limit = $this->request->param('limit', 10);
|
||||
$status = $this->request->param('status', ''); // 可选:筛选状态 0执行中,1执行成功,2执行失败
|
||||
$accountId = $this->getUserInfo('s2_accountId');
|
||||
|
||||
if (empty($accountId)) {
|
||||
return ResponseHelper::error('请先登录');
|
||||
}
|
||||
|
||||
// 直接使用operatorAccountId查询添加好友任务记录
|
||||
$query = Db::table('s2_friend_task')
|
||||
->where('operatorAccountId', $accountId)
|
||||
->order('createTime desc');
|
||||
|
||||
// 如果指定了状态筛选
|
||||
if ($status !== '' && $status !== null) {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
|
||||
$total = $query->count();
|
||||
$tasks = $query->page($page, $limit)->select();
|
||||
|
||||
|
||||
// 处理任务数据
|
||||
$list = [];
|
||||
foreach ($tasks as $task) {
|
||||
// 提取所有任务的phone、wechatId,用于查询好友信息(获取通过时间)
|
||||
$friendInfo = Db::table('s2_wechat_friend')
|
||||
->where(['isDeleted' => 0, 'ownerWechatId' => $task['wechatId']])
|
||||
->where(function ($query) use ($task) {
|
||||
$query->whereLike('phone', '%'.$task['phone'].'%')->whereOr('alias', $task['phone'])->whereOr('wechatId', $task['phone']);
|
||||
})->field('phone,wechatId,alias,passTime,nickname')->find();
|
||||
|
||||
|
||||
|
||||
$item = [
|
||||
'taskId' => $task['id'] ?? 0,
|
||||
'phone' => $task['phone'] ?? '',
|
||||
'wechatId' => $task['wechatId'] ?? '',
|
||||
'alias' => $task['alias'] ?? '',
|
||||
// 添加者信息
|
||||
'adder' => [
|
||||
'avatar' => $task['wechatAvatar'] ?? '', // 添加者头像
|
||||
'nickname' => $task['wechatNickname'] ?? '', // 添加者昵称
|
||||
'username' => $task['accountUsername'] ?? '', // 添加者微信号
|
||||
'accountNickname' => $task['accountNickname'] ?? '', // 账号昵称
|
||||
'accountRealName' => $task['accountRealName'] ?? '', // 账号真实姓名
|
||||
],
|
||||
// 添加状态
|
||||
'status' => [
|
||||
'code' => $task['status'] ?? 0, // 状态码:0执行中,1执行成功,2执行失败
|
||||
'text' => $this->getTaskStatusText($task['status'] ?? 0), // 状态文本
|
||||
'extra' => ''
|
||||
],
|
||||
// 时间信息
|
||||
'time' => [
|
||||
'addTime' => !empty($task['createTime']) ? date('Y-m-d H:i:s', $task['createTime']) : '', // 添加时间
|
||||
'addTimeStamp' => $task['createTime'] ?? 0, // 添加时间戳
|
||||
'updateTime' => !empty($task['updateTime']) ? date('Y-m-d H:i:s', $task['updateTime']) : '', // 更新时间
|
||||
'updateTimeStamp' => $task['updateTime'] ?? 0, // 更新时间戳
|
||||
'passTime' => !empty($friendInfo['passTime']) ? date('Y-m-d H:i:s', $friendInfo['passTime']) : '', // 通过时间
|
||||
'passTimeStamp' => $friendInfo['passTime'] ?? 0, // 通过时间戳
|
||||
],
|
||||
// 好友信息(如果已通过)
|
||||
'friend' => [
|
||||
'nickname' => $friendInfo['nickname'] ?? '', // 好友昵称
|
||||
'isPassed' => !empty($friendInfo['passTime']), // 是否已通过
|
||||
],
|
||||
// 其他信息
|
||||
'other' => [
|
||||
'msgContent' => $task['msgContent'] ?? '', // 验证消息
|
||||
'remark' => $task['remark'] ?? '', // 备注
|
||||
'from' => $task['from'] ?? '', // 来源
|
||||
'labels' => !empty($task['labels']) ? explode(',', $task['labels']) : [], // 标签
|
||||
]
|
||||
];
|
||||
|
||||
$list[] = $item;
|
||||
}
|
||||
|
||||
return ResponseHelper::success(['list' => $list, 'total' => $total]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取任务状态文本
|
||||
* @param int $status 状态码
|
||||
* @return string 状态文本
|
||||
*/
|
||||
private function getTaskStatusText($status)
|
||||
{
|
||||
$statusMap = [
|
||||
0 => '执行中',
|
||||
1 => '执行成功',
|
||||
2 => '执行失败',
|
||||
];
|
||||
|
||||
return isset($statusMap[$status]) ? $statusMap[$status] : '未知状态';
|
||||
}
|
||||
}
|
||||
@@ -39,4 +39,7 @@ return [
|
||||
'workbench:groupCreate' => 'app\command\WorkbenchGroupCreateCommand', // 工作台群创建任务
|
||||
'workbench:import-contact' => 'app\command\WorkbenchImportContactCommand', // 工作台通讯录导入任务
|
||||
'kf:notice' => 'app\command\KfNoticeCommand', // 客服端消息通知
|
||||
|
||||
'wechat:calculate-score' => 'app\command\CalculateWechatAccountScoreCommand', // 统一计算微信账号健康分
|
||||
'wechat:update-score' => 'app\command\UpdateWechatAccountScoreCommand', // 更新微信账号评分记录
|
||||
];
|
||||
|
||||
@@ -0,0 +1,558 @@
|
||||
<?php
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use think\Db;
|
||||
use think\facade\Log;
|
||||
use app\common\service\WechatAccountHealthScoreService;
|
||||
|
||||
/**
|
||||
* 统一计算微信账号健康分命令
|
||||
* 一个命令完成所有评分工作:
|
||||
* 1. 初始化未计算的账号(基础分只计算一次)
|
||||
* 2. 更新评分记录(根据wechatId和alias不一致情况)
|
||||
* 3. 批量更新健康分(只更新动态分)
|
||||
*/
|
||||
class CalculateWechatAccountScoreCommand extends Command
|
||||
{
|
||||
/**
|
||||
* 数据库表名
|
||||
*/
|
||||
const TABLE_WECHAT_ACCOUNT = 's2_wechat_account';
|
||||
const TABLE_WECHAT_ACCOUNT_SCORE = 's2_wechat_account_score';
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('wechat:calculate-score')
|
||||
->setDescription('统一计算微信账号健康分(包含初始化、更新评分记录、批量计算)')
|
||||
->addOption('only-init', null, \think\console\input\Option::VALUE_NONE, '仅执行初始化步骤')
|
||||
->addOption('only-update', null, \think\console\input\Option::VALUE_NONE, '仅执行更新评分记录步骤')
|
||||
->addOption('only-batch', null, \think\console\input\Option::VALUE_NONE, '仅执行批量更新健康分步骤')
|
||||
->addOption('account-id', 'a', \think\console\input\Option::VALUE_OPTIONAL, '指定账号ID,仅处理该账号')
|
||||
->addOption('batch-size', 'b', \think\console\input\Option::VALUE_OPTIONAL, '批处理大小', 50)
|
||||
->addOption('force-recalculate', 'f', \think\console\input\Option::VALUE_NONE, '强制重新计算基础分');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*
|
||||
* @param Input $input 输入对象
|
||||
* @param Output $output 输出对象
|
||||
* @return int 命令执行状态码(0表示成功)
|
||||
*/
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
// 解析命令行参数
|
||||
$onlyInit = $input->getOption('only-init');
|
||||
$onlyUpdate = $input->getOption('only-update');
|
||||
$onlyBatch = $input->getOption('only-batch');
|
||||
$accountId = $input->getOption('account-id');
|
||||
$batchSize = (int)$input->getOption('batch-size');
|
||||
$forceRecalculate = $input->getOption('force-recalculate');
|
||||
|
||||
// 参数验证
|
||||
if ($batchSize <= 0) {
|
||||
$batchSize = 50; // 默认批处理大小
|
||||
}
|
||||
|
||||
// 显示执行参数
|
||||
$output->writeln("==========================================");
|
||||
$output->writeln("开始统一计算微信账号健康分...");
|
||||
$output->writeln("==========================================");
|
||||
|
||||
if ($accountId) {
|
||||
$output->writeln("指定账号ID: {$accountId}");
|
||||
}
|
||||
|
||||
if ($onlyInit) {
|
||||
$output->writeln("仅执行初始化步骤");
|
||||
} elseif ($onlyUpdate) {
|
||||
$output->writeln("仅执行更新评分记录步骤");
|
||||
} elseif ($onlyBatch) {
|
||||
$output->writeln("仅执行批量更新健康分步骤");
|
||||
}
|
||||
|
||||
if ($forceRecalculate) {
|
||||
$output->writeln("强制重新计算基础分");
|
||||
}
|
||||
|
||||
$output->writeln("批处理大小: {$batchSize}");
|
||||
|
||||
// 记录命令开始执行的日志
|
||||
Log::info('开始执行微信账号健康分计算命令', [
|
||||
'accountId' => $accountId,
|
||||
'onlyInit' => $onlyInit ? 'true' : 'false',
|
||||
'onlyUpdate' => $onlyUpdate ? 'true' : 'false',
|
||||
'onlyBatch' => $onlyBatch ? 'true' : 'false',
|
||||
'batchSize' => $batchSize,
|
||||
'forceRecalculate' => $forceRecalculate ? 'true' : 'false'
|
||||
]);
|
||||
|
||||
$startTime = time();
|
||||
|
||||
try {
|
||||
// 实例化服务
|
||||
$service = new WechatAccountHealthScoreService();
|
||||
} catch (\Exception $e) {
|
||||
$errorMsg = "实例化WechatAccountHealthScoreService失败: " . $e->getMessage();
|
||||
$output->writeln("<error>{$errorMsg}</error>");
|
||||
Log::error($errorMsg);
|
||||
return 1; // 返回非零状态码表示失败
|
||||
}
|
||||
|
||||
// 初始化统计数据
|
||||
$initStats = ['success' => 0, 'failed' => 0, 'errors' => []];
|
||||
$updateStats = ['total' => 0];
|
||||
$batchStats = ['success' => 0, 'failed' => 0, 'errors' => []];
|
||||
|
||||
try {
|
||||
// 步骤1: 初始化未计算基础分的账号
|
||||
if (!$onlyUpdate && !$onlyBatch) {
|
||||
$output->writeln("\n[步骤1] 初始化未计算基础分的账号...");
|
||||
Log::info('[步骤1] 开始初始化未计算基础分的账号');
|
||||
$initStats = $this->initUncalculatedAccounts($service, $output, $accountId, $batchSize);
|
||||
$output->writeln("初始化完成:成功 {$initStats['success']} 条,失败 {$initStats['failed']} 条");
|
||||
Log::info("初始化完成:成功 {$initStats['success']} 条,失败 {$initStats['failed']} 条");
|
||||
}
|
||||
|
||||
// 步骤2: 更新评分记录(根据wechatId和alias不一致情况)
|
||||
if (!$onlyInit && !$onlyBatch) {
|
||||
$output->writeln("\n[步骤2] 更新评分记录(根据wechatId和alias不一致情况)...");
|
||||
Log::info('[步骤2] 开始更新评分记录(根据wechatId和alias不一致情况)');
|
||||
$updateStats = $this->updateScoreRecords($service, $output, $accountId, $batchSize);
|
||||
$output->writeln("更新完成:处理了 {$updateStats['total']} 条记录");
|
||||
Log::info("更新评分记录完成:处理了 {$updateStats['total']} 条记录");
|
||||
}
|
||||
|
||||
// 步骤3: 批量更新健康分(只更新动态分,不重新计算基础分)
|
||||
if (!$onlyInit && !$onlyUpdate) {
|
||||
$output->writeln("\n[步骤3] 批量更新健康分(只更新动态分)...");
|
||||
Log::info('[步骤3] 开始批量更新健康分(只更新动态分)');
|
||||
$batchStats = $this->batchUpdateHealthScore($service, $output, $accountId, $batchSize, $forceRecalculate);
|
||||
$output->writeln("批量更新完成:成功 {$batchStats['success']} 条,失败 {$batchStats['failed']} 条");
|
||||
Log::info("批量更新健康分完成:成功 {$batchStats['success']} 条,失败 {$batchStats['failed']} 条");
|
||||
}
|
||||
|
||||
// 统计信息
|
||||
$endTime = time();
|
||||
$duration = $endTime - $startTime;
|
||||
|
||||
$output->writeln("\n==========================================");
|
||||
$output->writeln("任务完成!");
|
||||
$output->writeln("==========================================");
|
||||
$output->writeln("总耗时: {$duration} 秒");
|
||||
$output->writeln("初始化: 成功 {$initStats['success']} 条,失败 {$initStats['failed']} 条");
|
||||
$output->writeln("更新评分记录: {$updateStats['total']} 条");
|
||||
$output->writeln("批量更新: 成功 {$batchStats['success']} 条,失败 {$batchStats['failed']} 条");
|
||||
|
||||
// 记录命令执行完成的日志
|
||||
Log::info("微信账号健康分计算命令执行完成,总耗时: {$duration} 秒," .
|
||||
"初始化: 成功 {$initStats['success']} 条,失败 {$initStats['failed']} 条," .
|
||||
"更新评分记录: {$updateStats['total']} 条," .
|
||||
"批量更新: 成功 {$batchStats['success']} 条,失败 {$batchStats['failed']} 条");
|
||||
|
||||
if (!empty($initStats['errors'])) {
|
||||
$output->writeln("\n初始化错误详情:");
|
||||
Log::warning("初始化阶段出现 " . count($initStats['errors']) . " 个错误");
|
||||
|
||||
foreach (array_slice($initStats['errors'], 0, 10) as $error) {
|
||||
$output->writeln(" 账号ID {$error['accountId']}: {$error['error']}");
|
||||
Log::error("初始化错误 - 账号ID {$error['accountId']}: {$error['error']}");
|
||||
}
|
||||
|
||||
if (count($initStats['errors']) > 10) {
|
||||
$output->writeln(" ... 还有 " . (count($initStats['errors']) - 10) . " 个错误");
|
||||
Log::warning("初始化错误过多,只记录前10个,还有 " . (count($initStats['errors']) - 10) . " 个错误未显示");
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($batchStats['errors'])) {
|
||||
$output->writeln("\n批量更新错误详情:");
|
||||
Log::warning("批量更新阶段出现 " . count($batchStats['errors']) . " 个错误");
|
||||
|
||||
foreach (array_slice($batchStats['errors'], 0, 10) as $error) {
|
||||
$output->writeln(" 账号ID {$error['accountId']}: {$error['error']}");
|
||||
Log::error("批量更新错误 - 账号ID {$error['accountId']}: {$error['error']}");
|
||||
}
|
||||
|
||||
if (count($batchStats['errors']) > 10) {
|
||||
$output->writeln(" ... 还有 " . (count($batchStats['errors']) - 10) . " 个错误");
|
||||
Log::warning("批量更新错误过多,只记录前10个,还有 " . (count($batchStats['errors']) - 10) . " 个错误未显示");
|
||||
}
|
||||
}
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
// 数据库异常
|
||||
$errorMsg = "数据库操作失败: " . $e->getMessage();
|
||||
$output->writeln("\n<error>数据库错误: " . $errorMsg . "</error>");
|
||||
$output->writeln($e->getTraceAsString());
|
||||
|
||||
// 记录数据库错误
|
||||
Log::error("数据库错误: " . $errorMsg);
|
||||
Log::error("错误堆栈: " . $e->getTraceAsString());
|
||||
|
||||
return 2; // 数据库错误状态码
|
||||
} catch (\Exception $e) {
|
||||
// 一般异常
|
||||
$errorMsg = "命令执行失败: " . $e->getMessage();
|
||||
$output->writeln("\n<error>错误: " . $errorMsg . "</error>");
|
||||
$output->writeln($e->getTraceAsString());
|
||||
|
||||
// 记录严重错误
|
||||
Log::error($errorMsg);
|
||||
Log::error("错误堆栈: " . $e->getTraceAsString());
|
||||
|
||||
return 1; // 一般错误状态码
|
||||
} catch (\Throwable $e) {
|
||||
// 其他所有错误
|
||||
$errorMsg = "严重错误: " . $e->getMessage();
|
||||
$output->writeln("\n<error>严重错误: " . $errorMsg . "</error>");
|
||||
$output->writeln($e->getTraceAsString());
|
||||
|
||||
// 记录严重错误
|
||||
Log::critical($errorMsg);
|
||||
Log::critical("错误堆栈: " . $e->getTraceAsString());
|
||||
|
||||
return 3; // 严重错误状态码
|
||||
}
|
||||
|
||||
return 0; // 成功执行
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化未计算基础分的账号
|
||||
*
|
||||
* @param WechatAccountHealthScoreService $service 健康分服务实例
|
||||
* @param Output $output 输出对象
|
||||
* @return array 处理结果统计
|
||||
* @throws \Exception 如果查询或处理过程中出现错误
|
||||
*/
|
||||
private function initUncalculatedAccounts($service, $output, $accountId = null, $batchSize = 50)
|
||||
{
|
||||
$stats = [
|
||||
'total' => 0,
|
||||
'success' => 0,
|
||||
'failed' => 0,
|
||||
'errors' => []
|
||||
];
|
||||
|
||||
try {
|
||||
// 获取所有未计算基础分的账号
|
||||
// 优化查询:使用索引字段,只查询必要的字段
|
||||
$query = Db::table(self::TABLE_WECHAT_ACCOUNT)
|
||||
->alias('a')
|
||||
->leftJoin([self::TABLE_WECHAT_ACCOUNT_SCORE => 's'], 's.accountId = a.id')
|
||||
->where('a.isDeleted', 0)
|
||||
->where(function($query) {
|
||||
$query->whereNull('s.id')
|
||||
->whereOr('s.baseScoreCalculated', 0);
|
||||
});
|
||||
|
||||
// 如果指定了账号ID,则只处理该账号
|
||||
if ($accountId) {
|
||||
$query->where('a.id', $accountId);
|
||||
}
|
||||
|
||||
$accounts = $query->field('a.id, a.wechatId') // 只查询必要的字段
|
||||
->select();
|
||||
} catch (\Exception $e) {
|
||||
Log::error("查询未计算基础分的账号失败: " . $e->getMessage());
|
||||
throw new \Exception("查询未计算基础分的账号失败: " . $e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
$stats['total'] = count($accounts);
|
||||
|
||||
if ($stats['total'] == 0) {
|
||||
$output->writeln("没有需要初始化的账号");
|
||||
Log::info("没有需要初始化的账号");
|
||||
return $stats;
|
||||
}
|
||||
|
||||
$output->writeln("找到 {$stats['total']} 个需要初始化的账号");
|
||||
Log::info("找到 {$stats['total']} 个需要初始化的账号");
|
||||
|
||||
// 优化批处理:使用传入的批处理大小
|
||||
$batches = array_chunk($accounts, $batchSize);
|
||||
$batchCount = count($batches);
|
||||
|
||||
Log::info("将分 {$batchCount} 批处理,每批 {$batchSize} 个账号");
|
||||
|
||||
foreach ($batches as $batchIndex => $batch) {
|
||||
$batchStartTime = microtime(true);
|
||||
$batchSuccessCount = 0;
|
||||
$batchFailedCount = 0;
|
||||
|
||||
foreach ($batch as $account) {
|
||||
try {
|
||||
$service->calculateAndUpdate($account['id']);
|
||||
$stats['success']++;
|
||||
$batchSuccessCount++;
|
||||
|
||||
if ($stats['success'] % 20 == 0) { // 更频繁地显示进度
|
||||
$output->write(".");
|
||||
Log::debug("已成功初始化 {$stats['success']} 个账号");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$stats['failed']++;
|
||||
$batchFailedCount++;
|
||||
$errorMsg = "初始化账号 {$account['id']} 失败: " . $e->getMessage();
|
||||
Log::error($errorMsg);
|
||||
$stats['errors'][] = [
|
||||
'accountId' => $account['id'],
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$batchEndTime = microtime(true);
|
||||
$batchDuration = round($batchEndTime - $batchStartTime, 2);
|
||||
|
||||
// 每批次完成后输出进度信息
|
||||
$output->writeln(" 批次 " . ($batchIndex + 1) . "/{$batchCount} 完成,耗时 {$batchDuration} 秒,成功 {$batchSuccessCount},失败 {$batchFailedCount}");
|
||||
Log::info("初始化批次 " . ($batchIndex + 1) . "/{$batchCount} 完成,耗时 {$batchDuration} 秒,成功 {$batchSuccessCount},失败 {$batchFailedCount}");
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新评分记录(根据wechatId和alias不一致情况)
|
||||
*
|
||||
* @param WechatAccountHealthScoreService $service 健康分服务实例
|
||||
* @param Output $output 输出对象
|
||||
* @return array 处理结果统计
|
||||
* @throws \Exception 如果查询或处理过程中出现错误
|
||||
*/
|
||||
private function updateScoreRecords($service, $output, $accountId = null, $batchSize = 50)
|
||||
{
|
||||
$stats = ['total' => 0];
|
||||
|
||||
try {
|
||||
// 优化查询:合并两次查询为一次,减少数据库访问次数
|
||||
$query = Db::table(self::TABLE_WECHAT_ACCOUNT)
|
||||
->where('isDeleted', 0)
|
||||
->where('wechatId', '<>', '')
|
||||
->where('alias', '<>', '');
|
||||
|
||||
// 如果指定了账号ID,则只处理该账号
|
||||
if ($accountId) {
|
||||
$query->where('id', $accountId);
|
||||
}
|
||||
|
||||
$accounts = $query->field('id, wechatId, alias, IF(wechatId = alias, 0, 1) as isModifiedAlias')
|
||||
->select();
|
||||
|
||||
// 分类处理查询结果
|
||||
$inconsistentAccounts = [];
|
||||
$consistentAccounts = [];
|
||||
|
||||
foreach ($accounts as $account) {
|
||||
if ($account['isModifiedAlias'] == 1) {
|
||||
$inconsistentAccounts[] = $account;
|
||||
} else {
|
||||
$consistentAccounts[] = $account;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error("查询需要更新评分记录的账号失败: " . $e->getMessage());
|
||||
throw new \Exception("查询需要更新评分记录的账号失败: " . $e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
$allAccounts = array_merge($inconsistentAccounts, $consistentAccounts);
|
||||
$stats['total'] = count($allAccounts);
|
||||
|
||||
if ($stats['total'] == 0) {
|
||||
$output->writeln("没有需要更新的账号");
|
||||
Log::info("没有需要更新的评分记录");
|
||||
return $stats;
|
||||
}
|
||||
|
||||
$output->writeln("找到 {$stats['total']} 个需要更新的账号(不一致: " . count($inconsistentAccounts) . ",一致: " . count($consistentAccounts) . ")");
|
||||
Log::info("找到 {$stats['total']} 个需要更新的账号(不一致: " . count($inconsistentAccounts) . ",一致: " . count($consistentAccounts) . ")");
|
||||
|
||||
$updatedCount = 0;
|
||||
|
||||
// 优化批处理:使用传入的批处理大小
|
||||
$batches = array_chunk($allAccounts, $batchSize);
|
||||
$batchCount = count($batches);
|
||||
|
||||
Log::info("将分 {$batchCount} 批更新评分记录,每批 {$batchSize} 个账号");
|
||||
|
||||
foreach ($batches as $batchIndex => $batch) {
|
||||
$batchStartTime = microtime(true);
|
||||
$batchUpdatedCount = 0;
|
||||
|
||||
foreach ($batch as $account) {
|
||||
$isModifiedAlias = isset($account['isModifiedAlias']) ?
|
||||
($account['isModifiedAlias'] == 1) :
|
||||
in_array($account['id'], array_column($inconsistentAccounts, 'id'));
|
||||
|
||||
$this->updateScoreRecord($account['id'], $isModifiedAlias, $service);
|
||||
$updatedCount++;
|
||||
$batchUpdatedCount++;
|
||||
|
||||
if ($batchUpdatedCount % 20 == 0) {
|
||||
$output->write(".");
|
||||
}
|
||||
}
|
||||
|
||||
$batchEndTime = microtime(true);
|
||||
$batchDuration = round($batchEndTime - $batchStartTime, 2);
|
||||
|
||||
// 每批次完成后输出进度信息
|
||||
$output->writeln(" 批次 " . ($batchIndex + 1) . "/{$batchCount} 完成,耗时 {$batchDuration} 秒,更新 {$batchUpdatedCount} 条记录");
|
||||
Log::info("更新评分记录批次 " . ($batchIndex + 1) . "/{$batchCount} 完成,耗时 {$batchDuration} 秒,更新 {$batchUpdatedCount} 条记录");
|
||||
}
|
||||
|
||||
if ($updatedCount > 0 && $updatedCount % 100 == 0) {
|
||||
$output->writeln("");
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新健康分(只更新动态分)
|
||||
*
|
||||
* @param WechatAccountHealthScoreService $service 健康分服务实例
|
||||
* @param Output $output 输出对象
|
||||
* @return array 处理结果统计
|
||||
* @throws \Exception 如果查询或处理过程中出现错误
|
||||
*/
|
||||
private function batchUpdateHealthScore($service, $output, $accountId = null, $batchSize = 50, $forceRecalculate = false)
|
||||
{
|
||||
try {
|
||||
// 获取所有已计算基础分的账号
|
||||
// 优化查询:只查询必要的字段,使用索引字段
|
||||
$query = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)
|
||||
->where('baseScoreCalculated', 1);
|
||||
|
||||
// 如果指定了账号ID,则只处理该账号
|
||||
if ($accountId) {
|
||||
$query->where('accountId', $accountId);
|
||||
}
|
||||
|
||||
$accountIds = $query->column('accountId');
|
||||
} catch (\Exception $e) {
|
||||
Log::error("查询需要批量更新健康分的账号失败: " . $e->getMessage());
|
||||
throw new \Exception("查询需要批量更新健康分的账号失败: " . $e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
$total = count($accountIds);
|
||||
|
||||
if ($total == 0) {
|
||||
$output->writeln("没有需要更新的账号");
|
||||
Log::info("没有需要批量更新健康分的账号");
|
||||
return ['success' => 0, 'failed' => 0, 'errors' => []];
|
||||
}
|
||||
|
||||
$output->writeln("找到 {$total} 个需要更新动态分的账号");
|
||||
Log::info("找到 {$total} 个需要更新动态分的账号");
|
||||
|
||||
// 使用传入的批处理大小和强制重新计算标志
|
||||
Log::info("使用批量大小 {$batchSize} 进行批量更新健康分,强制重新计算基础分: " . ($forceRecalculate ? 'true' : 'false'));
|
||||
$stats = $service->batchCalculateAndUpdate($accountIds, $batchSize, $forceRecalculate);
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新评分记录
|
||||
*
|
||||
* @param int $accountId 账号ID
|
||||
* @param bool $isModifiedAlias 是否已修改微信号
|
||||
* @param WechatAccountHealthScoreService $service 评分服务
|
||||
*/
|
||||
/**
|
||||
* 更新评分记录
|
||||
*
|
||||
* @param int $accountId 账号ID
|
||||
* @param bool $isModifiedAlias 是否已修改微信号
|
||||
* @param WechatAccountHealthScoreService $service 评分服务
|
||||
* @return bool 是否成功更新
|
||||
*/
|
||||
private function updateScoreRecord($accountId, $isModifiedAlias, $service)
|
||||
{
|
||||
Log::debug("开始更新账号 {$accountId} 的评分记录,isModifiedAlias: " . ($isModifiedAlias ? 'true' : 'false'));
|
||||
|
||||
try {
|
||||
// 获取账号数据 - 只查询必要的字段
|
||||
$accountData = Db::table(self::TABLE_WECHAT_ACCOUNT)
|
||||
->where('id', $accountId)
|
||||
->field('id, wechatId, alias') // 只查询必要的字段
|
||||
->find();
|
||||
|
||||
if (empty($accountData)) {
|
||||
Log::warning("账号 {$accountId} 不存在,跳过更新评分记录");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 确保评分记录存在 - 只查询必要的字段
|
||||
$scoreRecord = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)
|
||||
->where('accountId', $accountId)
|
||||
->field('accountId, baseScore, baseScoreCalculated, baseInfoScore, dynamicScore') // 只查询必要的字段
|
||||
->find();
|
||||
|
||||
if (empty($scoreRecord)) {
|
||||
// 如果记录不存在,创建并计算基础分
|
||||
Log::info("账号 {$accountId} 的评分记录不存在,创建并计算基础分");
|
||||
$service->calculateAndUpdate($accountId);
|
||||
$scoreRecord = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)
|
||||
->where('accountId', $accountId)
|
||||
->find();
|
||||
}
|
||||
|
||||
if (empty($scoreRecord)) {
|
||||
Log::warning("账号 {$accountId} 的评分记录创建失败,跳过更新");
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新isModifiedAlias字段
|
||||
$updateData = [
|
||||
'isModifiedAlias' => $isModifiedAlias ? 1 : 0,
|
||||
'updateTime' => time()
|
||||
];
|
||||
|
||||
// 如果基础分已计算,需要更新基础信息分和基础分
|
||||
if ($scoreRecord['baseScoreCalculated']) {
|
||||
$oldBaseInfoScore = $scoreRecord['baseInfoScore'] ?? 0;
|
||||
$newBaseInfoScore = $isModifiedAlias ? 10 : 0; // 已修改微信号得10分
|
||||
|
||||
if ($oldBaseInfoScore != $newBaseInfoScore) {
|
||||
$oldBaseScore = $scoreRecord['baseScore'] ?? 60;
|
||||
$newBaseScore = $oldBaseScore - $oldBaseInfoScore + $newBaseInfoScore;
|
||||
|
||||
$updateData['baseInfoScore'] = $newBaseInfoScore;
|
||||
$updateData['baseScore'] = $newBaseScore;
|
||||
|
||||
// 重新计算健康分
|
||||
$dynamicScore = $scoreRecord['dynamicScore'] ?? 0;
|
||||
$healthScore = $newBaseScore + $dynamicScore;
|
||||
$healthScore = max(0, min(100, $healthScore));
|
||||
$updateData['healthScore'] = $healthScore;
|
||||
$updateData['maxAddFriendPerDay'] = (int)floor($healthScore * 0.2);
|
||||
|
||||
Log::info("账号 {$accountId} 的基础信息分从 {$oldBaseInfoScore} 更新为 {$newBaseInfoScore}," .
|
||||
"基础分从 {$oldBaseScore} 更新为 {$newBaseScore},健康分更新为 {$healthScore}");
|
||||
}
|
||||
} else {
|
||||
// 基础分未计算,只更新标记和基础信息分
|
||||
$updateData['baseInfoScore'] = $isModifiedAlias ? 10 : 0;
|
||||
}
|
||||
|
||||
$result = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)
|
||||
->where('accountId', $accountId)
|
||||
->update($updateData);
|
||||
|
||||
Log::debug("账号 {$accountId} 的评分记录更新" . ($result !== false ? "成功" : "失败"));
|
||||
|
||||
return $result !== false;
|
||||
} catch (\Exception $e) {
|
||||
Log::error("更新账号 {$accountId} 的评分记录失败: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
168
Server/application/command/UpdateWechatAccountScoreCommand.php
Normal file
168
Server/application/command/UpdateWechatAccountScoreCommand.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use think\Db;
|
||||
use app\common\service\WechatAccountHealthScoreService;
|
||||
|
||||
/**
|
||||
* 更新微信账号评分记录
|
||||
* 根据wechatId和alias是否不一致来更新isModifiedAlias字段(仅用于评分,不修复数据)
|
||||
*/
|
||||
class UpdateWechatAccountScoreCommand extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('wechat:update-score')
|
||||
->setDescription('更新微信账号评分记录,根据wechatId和alias不一致情况更新isModifiedAlias字段(仅用于评分)');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$output->writeln("开始更新微信账号评分记录...");
|
||||
|
||||
try {
|
||||
// 1. 查找所有需要更新的账号
|
||||
$output->writeln("步骤1: 查找需要更新的账号...");
|
||||
|
||||
// 查找wechatId和alias不一致的账号
|
||||
$inconsistentAccounts = Db::table('s2_wechat_account')
|
||||
->where('isDeleted', 0)
|
||||
->where('wechatId', '<>', '')
|
||||
->where('alias', '<>', '')
|
||||
->whereRaw('wechatId != alias')
|
||||
->field('id, wechatId, alias')
|
||||
->select();
|
||||
|
||||
// 查找wechatId和alias一致的账号
|
||||
$consistentAccounts = Db::table('s2_wechat_account')
|
||||
->where('isDeleted', 0)
|
||||
->where('wechatId', '<>', '')
|
||||
->where('alias', '<>', '')
|
||||
->whereRaw('wechatId = alias')
|
||||
->field('id, wechatId, alias')
|
||||
->select();
|
||||
|
||||
$output->writeln("发现 " . count($inconsistentAccounts) . " 条不一致记录(已修改微信号)");
|
||||
$output->writeln("发现 " . count($consistentAccounts) . " 条一致记录(未修改微信号)");
|
||||
|
||||
// 2. 更新评分记录表中的isModifiedAlias字段
|
||||
$output->writeln("步骤2: 更新评分记录表...");
|
||||
$updatedCount = 0;
|
||||
$healthScoreService = new WechatAccountHealthScoreService();
|
||||
|
||||
// 更新不一致的记录
|
||||
foreach ($inconsistentAccounts as $account) {
|
||||
$this->updateScoreRecord($account['id'], true, $healthScoreService);
|
||||
$updatedCount++;
|
||||
}
|
||||
|
||||
// 更新一致的记录
|
||||
foreach ($consistentAccounts as $account) {
|
||||
$this->updateScoreRecord($account['id'], false, $healthScoreService);
|
||||
$updatedCount++;
|
||||
}
|
||||
|
||||
$output->writeln("已更新 " . $updatedCount . " 条评分记录");
|
||||
|
||||
// 3. 重新计算健康分(只更新基础信息分,不重新计算基础分)
|
||||
$output->writeln("步骤3: 重新计算健康分...");
|
||||
$allAccountIds = array_merge(
|
||||
array_column($inconsistentAccounts, 'id'),
|
||||
array_column($consistentAccounts, 'id')
|
||||
);
|
||||
|
||||
if (!empty($allAccountIds)) {
|
||||
$stats = $healthScoreService->batchCalculateAndUpdate($allAccountIds, 100, false);
|
||||
$output->writeln("健康分计算完成:成功 " . $stats['success'] . " 条,失败 " . $stats['failed'] . " 条");
|
||||
|
||||
if (!empty($stats['errors'])) {
|
||||
$output->writeln("错误详情:");
|
||||
foreach ($stats['errors'] as $error) {
|
||||
$output->writeln(" 账号ID {$error['accountId']}: {$error['error']}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$output->writeln("任务完成!");
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln("错误: " . $e->getMessage());
|
||||
$output->writeln($e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新评分记录
|
||||
*
|
||||
* @param int $accountId 账号ID
|
||||
* @param bool $isModifiedAlias 是否已修改微信号
|
||||
* @param WechatAccountHealthScoreService $service 评分服务
|
||||
*/
|
||||
private function updateScoreRecord($accountId, $isModifiedAlias, $service)
|
||||
{
|
||||
// 获取或创建评分记录
|
||||
$accountData = Db::table('s2_wechat_account')
|
||||
->where('id', $accountId)
|
||||
->find();
|
||||
|
||||
if (empty($accountData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保评分记录存在
|
||||
$scoreRecord = Db::table('s2_wechat_account_score')
|
||||
->where('accountId', $accountId)
|
||||
->find();
|
||||
|
||||
if (empty($scoreRecord)) {
|
||||
// 如果记录不存在,创建并计算基础分
|
||||
$service->calculateAndUpdate($accountId);
|
||||
$scoreRecord = Db::table('s2_wechat_account_score')
|
||||
->where('accountId', $accountId)
|
||||
->find();
|
||||
}
|
||||
|
||||
if (empty($scoreRecord)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新isModifiedAlias字段
|
||||
$updateData = [
|
||||
'isModifiedAlias' => $isModifiedAlias ? 1 : 0,
|
||||
'updateTime' => time()
|
||||
];
|
||||
|
||||
// 如果基础分已计算,需要更新基础信息分和基础分
|
||||
if ($scoreRecord['baseScoreCalculated']) {
|
||||
$oldBaseInfoScore = $scoreRecord['baseInfoScore'] ?? 0;
|
||||
$newBaseInfoScore = $isModifiedAlias ? 10 : 0; // 已修改微信号得10分
|
||||
|
||||
if ($oldBaseInfoScore != $newBaseInfoScore) {
|
||||
$oldBaseScore = $scoreRecord['baseScore'] ?? 60;
|
||||
$newBaseScore = $oldBaseScore - $oldBaseInfoScore + $newBaseInfoScore;
|
||||
|
||||
$updateData['baseInfoScore'] = $newBaseInfoScore;
|
||||
$updateData['baseScore'] = $newBaseScore;
|
||||
|
||||
// 重新计算健康分
|
||||
$dynamicScore = $scoreRecord['dynamicScore'] ?? 0;
|
||||
$healthScore = $newBaseScore + $dynamicScore;
|
||||
$healthScore = max(0, min(100, $healthScore));
|
||||
$updateData['healthScore'] = $healthScore;
|
||||
$updateData['maxAddFriendPerDay'] = (int)floor($healthScore * 0.2);
|
||||
}
|
||||
} else {
|
||||
// 基础分未计算,只更新标记和基础信息分
|
||||
$updateData['baseInfoScore'] = $isModifiedAlias ? 10 : 0;
|
||||
}
|
||||
|
||||
Db::table('s2_wechat_account_score')
|
||||
->where('accountId', $accountId)
|
||||
->update($updateData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ class TaskServer extends Server
|
||||
if ($current_worker_id == 1) {
|
||||
// 每60秒检查一次自动问候规则
|
||||
Timer::add(60, function () use ($adapter) {
|
||||
$adapter->handleAutoGreetings();
|
||||
//$adapter->handleAutoGreetings();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
333
Server/application/common/controller/ExportController.php
Normal file
333
Server/application/common/controller/ExportController.php
Normal file
@@ -0,0 +1,333 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\controller;
|
||||
|
||||
use PHPExcel;
|
||||
use PHPExcel_IOFactory;
|
||||
use PHPExcel_Worksheet_Drawing;
|
||||
use think\Controller;
|
||||
use think\Exception;
|
||||
|
||||
/**
|
||||
* 通用导出控制器,提供 Excel 导出与图片插入能力
|
||||
*/
|
||||
class ExportController extends Controller
|
||||
{
|
||||
/**
|
||||
* @var array<string> 需要在请求结束时清理的临时文件
|
||||
*/
|
||||
protected static $tempFiles = [];
|
||||
|
||||
/**
|
||||
* 导出 Excel(支持指定列插入图片)
|
||||
*
|
||||
* @param string $fileName 输出文件名(可不带扩展名)
|
||||
* @param array $headers 列定义,例如 ['name' => '姓名', 'phone' => '电话']
|
||||
* @param array $rows 数据行,需与 $headers 的 key 对应
|
||||
* @param array $imageColumns 需要渲染为图片的列 key 列表
|
||||
* @param string $sheetName 工作表名称
|
||||
* @param array $options 额外选项:
|
||||
* - imageWidth(图片宽度,默认100)
|
||||
* - imageHeight(图片高度,默认100)
|
||||
* - imageColumnWidth(图片列宽,默认15)
|
||||
* - titleRow(标题行内容,支持多行文本数组)
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function exportExcelWithImages(
|
||||
$fileName,
|
||||
array $headers,
|
||||
array $rows,
|
||||
array $imageColumns = [],
|
||||
$sheetName = 'Sheet1',
|
||||
array $options = []
|
||||
) {
|
||||
if (empty($headers)) {
|
||||
throw new Exception('导出列定义不能为空');
|
||||
}
|
||||
if (empty($rows)) {
|
||||
throw new Exception('导出数据不能为空');
|
||||
}
|
||||
|
||||
// 抑制 PHPExcel 库中已废弃的大括号语法警告(PHP 7.4+)
|
||||
$oldErrorReporting = error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);
|
||||
|
||||
// 默认选项
|
||||
$imageWidth = isset($options['imageWidth']) ? (int)$options['imageWidth'] : 100;
|
||||
$imageHeight = isset($options['imageHeight']) ? (int)$options['imageHeight'] : 100;
|
||||
$imageColumnWidth = isset($options['imageColumnWidth']) ? (float)$options['imageColumnWidth'] : 15;
|
||||
$rowHeight = isset($options['rowHeight']) ? (int)$options['rowHeight'] : ($imageHeight + 10);
|
||||
|
||||
$excel = new PHPExcel();
|
||||
$sheet = $excel->getActiveSheet();
|
||||
$sheet->setTitle($sheetName);
|
||||
|
||||
$columnKeys = array_keys($headers);
|
||||
$totalColumns = count($columnKeys);
|
||||
$lastColumnLetter = self::columnLetter($totalColumns - 1);
|
||||
|
||||
// 定义特定列的固定宽度(如果未指定则使用默认值)
|
||||
$columnWidths = isset($options['columnWidths']) ? $options['columnWidths'] : [];
|
||||
|
||||
// 检查是否有标题行
|
||||
$titleRow = isset($options['titleRow']) ? $options['titleRow'] : null;
|
||||
$dataStartRow = 1; // 数据开始行(表头行)
|
||||
|
||||
// 如果有标题行,先写入标题行(支持数组或字符串)
|
||||
if (!empty($titleRow)) {
|
||||
$dataStartRow = 2; // 数据从第2行开始(第1行是标题,第2行是表头)
|
||||
|
||||
// 合并标题行单元格(从第一列到最后一列)
|
||||
$titleRange = 'A1:' . $lastColumnLetter . '1';
|
||||
$sheet->mergeCells($titleRange);
|
||||
|
||||
// 构建标题内容(支持多行数组或字符串)
|
||||
$titleContent = '';
|
||||
if (is_array($titleRow)) {
|
||||
$titleContent = implode("\n", $titleRow);
|
||||
} else {
|
||||
$titleContent = (string)$titleRow;
|
||||
}
|
||||
|
||||
// 写入标题
|
||||
$sheet->setCellValue('A1', $titleContent);
|
||||
|
||||
// 设置标题行样式
|
||||
$sheet->getStyle('A1')->applyFromArray([
|
||||
'font' => ['bold' => true, 'size' => 16],
|
||||
'alignment' => [
|
||||
'horizontal' => \PHPExcel_Style_Alignment::HORIZONTAL_CENTER,
|
||||
'vertical' => \PHPExcel_Style_Alignment::VERTICAL_CENTER,
|
||||
'wrap' => true
|
||||
],
|
||||
'fill' => [
|
||||
'type' => \PHPExcel_Style_Fill::FILL_SOLID,
|
||||
'color' => ['rgb' => 'FFF8DC'] // 浅黄色背景
|
||||
],
|
||||
'borders' => [
|
||||
'allborders' => [
|
||||
'style' => \PHPExcel_Style_Border::BORDER_THIN,
|
||||
'color' => ['rgb' => '000000']
|
||||
]
|
||||
]
|
||||
]);
|
||||
$sheet->getRowDimension(1)->setRowHeight(80); // 标题行高度
|
||||
}
|
||||
|
||||
// 写入表头并设置列宽
|
||||
$headerRow = $dataStartRow;
|
||||
foreach ($columnKeys as $index => $key) {
|
||||
$columnLetter = self::columnLetter($index);
|
||||
$sheet->setCellValue($columnLetter . $headerRow, $headers[$key]);
|
||||
|
||||
// 如果是图片列,设置固定列宽
|
||||
if (in_array($key, $imageColumns, true)) {
|
||||
$sheet->getColumnDimension($columnLetter)->setWidth($imageColumnWidth);
|
||||
} elseif (isset($columnWidths[$key])) {
|
||||
// 如果指定了该列的宽度,使用指定宽度
|
||||
$sheet->getColumnDimension($columnLetter)->setWidth($columnWidths[$key]);
|
||||
} else {
|
||||
// 否则自动调整
|
||||
$sheet->getColumnDimension($columnLetter)->setAutoSize(true);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置表头样式
|
||||
$headerRange = 'A' . $headerRow . ':' . $lastColumnLetter . $headerRow;
|
||||
$sheet->getStyle($headerRange)->applyFromArray([
|
||||
'font' => ['bold' => true, 'size' => 11],
|
||||
'alignment' => [
|
||||
'horizontal' => \PHPExcel_Style_Alignment::HORIZONTAL_CENTER,
|
||||
'vertical' => \PHPExcel_Style_Alignment::VERTICAL_CENTER,
|
||||
'wrap' => true
|
||||
],
|
||||
'fill' => [
|
||||
'type' => \PHPExcel_Style_Fill::FILL_SOLID,
|
||||
'color' => ['rgb' => 'FFF8DC']
|
||||
],
|
||||
'borders' => [
|
||||
'allborders' => [
|
||||
'style' => \PHPExcel_Style_Border::BORDER_THIN,
|
||||
'color' => ['rgb' => '000000']
|
||||
]
|
||||
]
|
||||
]);
|
||||
$sheet->getRowDimension($headerRow)->setRowHeight(30); // 增加表头行高以确保文本完整显示
|
||||
|
||||
// 写入数据与图片
|
||||
$dataRowStart = $dataStartRow + 1; // 数据从表头行下一行开始
|
||||
foreach ($rows as $rowIndex => $rowData) {
|
||||
$excelRow = $dataRowStart + $rowIndex; // 数据行
|
||||
$maxRowHeight = $rowHeight; // 记录当前行的最大高度
|
||||
|
||||
foreach ($columnKeys as $colIndex => $key) {
|
||||
$columnLetter = self::columnLetter($colIndex);
|
||||
$cell = $columnLetter . $excelRow;
|
||||
$value = isset($rowData[$key]) ? $rowData[$key] : '';
|
||||
|
||||
if (in_array($key, $imageColumns, true) && !empty($value)) {
|
||||
$imagePath = self::resolveImagePath($value);
|
||||
if ($imagePath) {
|
||||
// 获取图片实际尺寸并等比例缩放
|
||||
$imageSize = @getimagesize($imagePath);
|
||||
if ($imageSize) {
|
||||
$originalWidth = $imageSize[0];
|
||||
$originalHeight = $imageSize[1];
|
||||
|
||||
// 计算等比例缩放后的尺寸
|
||||
$ratio = min($imageWidth / $originalWidth, $imageHeight / $originalHeight);
|
||||
$scaledWidth = $originalWidth * $ratio;
|
||||
$scaledHeight = $originalHeight * $ratio;
|
||||
|
||||
// 确保不超过最大尺寸
|
||||
if ($scaledWidth > $imageWidth) {
|
||||
$scaledWidth = $imageWidth;
|
||||
$scaledHeight = $originalHeight * ($imageWidth / $originalWidth);
|
||||
}
|
||||
if ($scaledHeight > $imageHeight) {
|
||||
$scaledHeight = $imageHeight;
|
||||
$scaledWidth = $originalWidth * ($imageHeight / $originalHeight);
|
||||
}
|
||||
|
||||
$drawing = new PHPExcel_Worksheet_Drawing();
|
||||
$drawing->setPath($imagePath);
|
||||
$drawing->setCoordinates($cell);
|
||||
|
||||
// 居中显示图片(Excel列宽1单位≈7像素,行高1单位≈0.75像素)
|
||||
$cellWidthPx = $imageColumnWidth * 7;
|
||||
$cellHeightPx = $maxRowHeight * 0.75;
|
||||
$offsetX = max(2, ($cellWidthPx - $scaledWidth) / 2);
|
||||
$offsetY = max(2, ($cellHeightPx - $scaledHeight) / 2);
|
||||
|
||||
$drawing->setOffsetX((int)$offsetX);
|
||||
$drawing->setOffsetY((int)$offsetY);
|
||||
$drawing->setWidth((int)$scaledWidth);
|
||||
$drawing->setHeight((int)$scaledHeight);
|
||||
$drawing->setWorksheet($sheet);
|
||||
|
||||
// 更新行高以适应图片(留出一些边距)
|
||||
$neededHeight = (int)($scaledHeight / 0.75) + 10;
|
||||
if ($neededHeight > $maxRowHeight) {
|
||||
$maxRowHeight = $neededHeight;
|
||||
}
|
||||
} else {
|
||||
// 如果无法获取图片尺寸,使用默认尺寸
|
||||
$drawing = new PHPExcel_Worksheet_Drawing();
|
||||
$drawing->setPath($imagePath);
|
||||
$drawing->setCoordinates($cell);
|
||||
$drawing->setOffsetX(5);
|
||||
$drawing->setOffsetY(5);
|
||||
$drawing->setWidth($imageWidth);
|
||||
$drawing->setHeight($imageHeight);
|
||||
$drawing->setWorksheet($sheet);
|
||||
}
|
||||
} else {
|
||||
$sheet->setCellValue($cell, '');
|
||||
}
|
||||
} else {
|
||||
$sheet->setCellValue($cell, $value);
|
||||
// 设置文本对齐和换行
|
||||
$style = $sheet->getStyle($cell);
|
||||
$style->getAlignment()->setVertical(\PHPExcel_Style_Alignment::VERTICAL_CENTER);
|
||||
$style->getAlignment()->setWrapText(true);
|
||||
// 根据列类型设置水平对齐
|
||||
if (in_array($key, ['date', 'postTime'])) {
|
||||
$style->getAlignment()->setHorizontal(\PHPExcel_Style_Alignment::HORIZONTAL_CENTER);
|
||||
} else {
|
||||
$style->getAlignment()->setHorizontal(\PHPExcel_Style_Alignment::HORIZONTAL_LEFT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置行高
|
||||
$sheet->getRowDimension($excelRow)->setRowHeight($maxRowHeight);
|
||||
}
|
||||
|
||||
$safeName = preg_replace('/[^\w\-]/', '_', $fileName ?: 'export_' . date('Ymd_His'));
|
||||
if (stripos($safeName, '.xlsx') === false) {
|
||||
$safeName .= '.xlsx';
|
||||
}
|
||||
|
||||
if (ob_get_length()) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
header('Cache-Control: max-age=0');
|
||||
header('Content-Disposition: attachment;filename="' . $safeName . '"');
|
||||
|
||||
try {
|
||||
$writer = PHPExcel_IOFactory::createWriter($excel, 'Excel2007');
|
||||
$writer->save('php://output');
|
||||
} catch (\Exception $e) {
|
||||
// 恢复错误报告级别
|
||||
error_reporting($oldErrorReporting);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// 恢复错误报告级别
|
||||
error_reporting($oldErrorReporting);
|
||||
self::cleanupTempFiles();
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据列序号生成 Excel 列字母
|
||||
*
|
||||
* @param int $index
|
||||
* @return string
|
||||
*/
|
||||
protected static function columnLetter($index)
|
||||
{
|
||||
$letters = '';
|
||||
do {
|
||||
$letters = chr($index % 26 + 65) . $letters;
|
||||
$index = intval($index / 26) - 1;
|
||||
} while ($index >= 0);
|
||||
|
||||
return $letters;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将远程或本地图片路径转换为可用的本地文件路径
|
||||
*
|
||||
* @param string $path
|
||||
* @return string|null
|
||||
*/
|
||||
protected static function resolveImagePath($path)
|
||||
{
|
||||
if (empty($path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('/^https?:\/\//i', $path)) {
|
||||
$tempFile = tempnam(sys_get_temp_dir(), 'export_img_');
|
||||
$stream = @file_get_contents($path);
|
||||
if ($stream === false) {
|
||||
return null;
|
||||
}
|
||||
file_put_contents($tempFile, $stream);
|
||||
self::$tempFiles[] = $tempFile;
|
||||
return $tempFile;
|
||||
}
|
||||
|
||||
if (file_exists($path)) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有临时文件
|
||||
*/
|
||||
protected static function cleanupTempFiles()
|
||||
{
|
||||
foreach (self::$tempFiles as $file) {
|
||||
if (file_exists($file)) {
|
||||
@unlink($file);
|
||||
}
|
||||
}
|
||||
self::$tempFiles = [];
|
||||
}
|
||||
}
|
||||
44
Server/application/common/model/WechatAccountScore.php
Normal file
44
Server/application/common/model/WechatAccountScore.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 微信账号评分记录模型类
|
||||
*/
|
||||
class WechatAccountScore extends Model
|
||||
{
|
||||
// 设置表名
|
||||
protected $name = 'wechat_account_score';
|
||||
protected $table = 's2_wechat_account_score';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = false;
|
||||
|
||||
// 定义字段类型
|
||||
protected $type = [
|
||||
'accountId' => 'integer',
|
||||
'baseScore' => 'integer',
|
||||
'baseScoreCalculated' => 'integer',
|
||||
'baseInfoScore' => 'integer',
|
||||
'friendCountScore' => 'integer',
|
||||
'friendCount' => 'integer',
|
||||
'dynamicScore' => 'integer',
|
||||
'frequentCount' => 'integer',
|
||||
'frequentPenalty' => 'integer',
|
||||
'consecutiveNoFrequentDays' => 'integer',
|
||||
'noFrequentBonus' => 'integer',
|
||||
'banPenalty' => 'integer',
|
||||
'healthScore' => 'integer',
|
||||
'maxAddFriendPerDay' => 'integer',
|
||||
'isModifiedAlias' => 'integer',
|
||||
'isBanned' => 'integer',
|
||||
'lastFrequentTime' => 'integer',
|
||||
'lastNoFrequentTime' => 'integer',
|
||||
'baseScoreCalcTime' => 'integer',
|
||||
'createTime' => 'integer',
|
||||
'updateTime' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,13 +36,14 @@ Route::group('v1/', function () {
|
||||
Route::get(':id/summary', 'app\cunkebao\controller\wechat\GetWechatOnDeviceSummarizeV1Controller@index');
|
||||
Route::get(':id/friends', 'app\cunkebao\controller\wechat\GetWechatOnDeviceFriendsV1Controller@index');
|
||||
Route::get('getWechatInfo', 'app\cunkebao\controller\wechat\GetWechatController@getWechatInfo');
|
||||
Route::get(':wechatId', 'app\cunkebao\controller\wechat\GetWechatProfileV1Controller@index');
|
||||
Route::post('transfer-friends', 'app\cunkebao\controller\wechat\PostTransferFriends@index'); // 微信好友转移
|
||||
|
||||
Route::get('overview', 'app\cunkebao\controller\wechat\GetWechatOverviewV1Controller@index'); // 获取微信账号概览数据
|
||||
Route::get('moments', 'app\cunkebao\controller\wechat\GetWechatMomentsV1Controller@index'); // 获取微信朋友圈
|
||||
Route::get('moments/export', 'app\cunkebao\controller\wechat\GetWechatMomentsV1Controller@export'); // 导出微信朋友圈
|
||||
Route::get('count', 'app\cunkebao\controller\DeviceWechat@count');
|
||||
Route::get('device-count', 'app\cunkebao\controller\DeviceWechat@deviceCount'); // 获取有登录微信的设备数量
|
||||
Route::put('refresh', 'app\cunkebao\controller\DeviceWechat@refresh'); // 刷新设备微信状态
|
||||
|
||||
Route::post('transfer-friends', 'app\cunkebao\controller\wechat\PostTransferFriends@index'); // 微信好友转移
|
||||
Route::get(':wechatId', 'app\cunkebao\controller\wechat\GetWechatProfileV1Controller@index');
|
||||
});
|
||||
|
||||
// 获客场景相关
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace app\cunkebao\controller;
|
||||
|
||||
use app\api\controller\AccountController;
|
||||
use app\api\controller\UserController;
|
||||
use app\common\service\ClassTableService;
|
||||
use library\ResponseHelper;
|
||||
use think\Controller;
|
||||
@@ -148,6 +149,12 @@ class BaseController extends Controller
|
||||
|
||||
$res = Db::name('users')->where(['id' => $userId, 'companyId' => $companyId])->update($data);
|
||||
if (!empty($res)) {
|
||||
if ($user['typeId'] == 1 && !empty($user['s2_accountId'])) {
|
||||
$UserController = new UserController();
|
||||
$UserController->modifyPwd(['id' => $user['s2_accountId'],'pwd' => $passWord]);
|
||||
}
|
||||
|
||||
|
||||
return ResponseHelper::success('密码修改成功');
|
||||
} else {
|
||||
return ResponseHelper::error('密码修改失败');
|
||||
|
||||
@@ -1061,6 +1061,7 @@ class ContentLibraryController extends Controller
|
||||
$where = [
|
||||
['isDel', '=', 0], // 未删除
|
||||
['status', '=', 1], // 已开启
|
||||
['id', '=', 99], // 已开启
|
||||
];
|
||||
|
||||
// 查询符合条件的内容库
|
||||
@@ -1070,7 +1071,7 @@ class ContentLibraryController extends Controller
|
||||
->select()->toArray();
|
||||
|
||||
if (empty($libraries)) {
|
||||
return json(['code' => 200, 'msg' => '没有可用的内容库配置']);
|
||||
return json_encode(['code' => 200, 'msg' => '没有可用的内容库配置'],256);
|
||||
}
|
||||
|
||||
$successCount = 0;
|
||||
@@ -1159,7 +1160,7 @@ class ContentLibraryController extends Controller
|
||||
}
|
||||
|
||||
// 返回采集结果
|
||||
return json([
|
||||
return json_encode([
|
||||
'code' => 200,
|
||||
'msg' => '采集任务执行完成',
|
||||
'data' => [
|
||||
@@ -1169,7 +1170,7 @@ class ContentLibraryController extends Controller
|
||||
'skipped' => $totalLibraries - $successCount - $failCount,
|
||||
'results' => $results
|
||||
]
|
||||
]);
|
||||
],256);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1206,7 +1207,7 @@ class ContentLibraryController extends Controller
|
||||
->whereIn('id', $friendIds)
|
||||
->where('isDeleted', 0)
|
||||
->select();
|
||||
|
||||
|
||||
if (empty($friends)) {
|
||||
return [
|
||||
'status' => 'failed',
|
||||
@@ -1225,7 +1226,7 @@ class ContentLibraryController extends Controller
|
||||
|
||||
foreach ($friends as $friend) {
|
||||
$processedFriends++;
|
||||
|
||||
|
||||
// 如果配置了API并且需要主动获取朋友圈
|
||||
if ($needFetch) {
|
||||
try {
|
||||
@@ -1264,9 +1265,9 @@ class ContentLibraryController extends Controller
|
||||
}
|
||||
|
||||
// 如果指定了采集类型,进行过滤
|
||||
if (!empty($catchTypes)) {
|
||||
/*if (!empty($catchTypes)) {
|
||||
$query->whereIn('type', $catchTypes);
|
||||
}
|
||||
}*/
|
||||
|
||||
// 获取最近20条朋友圈
|
||||
$moments = $query->page(1, 20)->select();
|
||||
@@ -1289,7 +1290,7 @@ class ContentLibraryController extends Controller
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果启用了AI处理
|
||||
/* // 如果启用了AI处理
|
||||
if (!empty($library['aiEnabled']) && !empty($content)) {
|
||||
try {
|
||||
$contentAi = $this->aiRewrite($library, $content);
|
||||
@@ -1300,7 +1301,7 @@ class ContentLibraryController extends Controller
|
||||
\think\facade\Log::error('AI处理失败: ' . $e->getMessage() . ' [朋友圈ID: ' . ($moment['id'] ?? 'unknown') . ']');
|
||||
$moment['contentAi'] = '';
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
// 保存到内容库的content_item表
|
||||
if ($this->saveMomentToContentItem($moment, $library['id'], $friend, $nickname)) {
|
||||
|
||||
@@ -2816,6 +2816,8 @@ class WorkbenchController extends Controller
|
||||
$limit = $this->request->param('limit', 10);
|
||||
$workbenchId = $this->request->param('workbenchId', 0);
|
||||
$keyword = $this->request->param('keyword', '');
|
||||
$pushType = $this->request->param('pushType', ''); // 推送类型筛选:''=全部, 'friend'=好友消息, 'group'=群消息, 'announcement'=群公告
|
||||
$status = $this->request->param('status', ''); // 状态筛选:''=全部, 'success'=已完成, 'progress'=进行中, 'failed'=失败
|
||||
$userId = $this->request->userInfo['id'];
|
||||
|
||||
// 构建工作台查询条件
|
||||
@@ -2840,10 +2842,11 @@ class WorkbenchController extends Controller
|
||||
$workbenchWhere[] = ['w.id', '=', $workbenchId];
|
||||
}
|
||||
|
||||
// 按内容ID、工作台ID和时间分组,统计每次推送
|
||||
$query = Db::name('workbench_group_push_item')
|
||||
// 1. 先查询所有已执行的推送记录(按推送时间分组)
|
||||
$pushHistoryQuery = Db::name('workbench_group_push_item')
|
||||
->alias('wgpi')
|
||||
->join('workbench w', 'w.id = wgpi.workbenchId', 'left')
|
||||
->join('workbench_group_push wgp', 'wgp.workbenchId = wgpi.workbenchId', 'left')
|
||||
->join('content_item ci', 'ci.id = wgpi.contentId', 'left')
|
||||
->join('content_library cl', 'cl.id = ci.libraryId', 'left')
|
||||
->where($workbenchWhere)
|
||||
@@ -2853,52 +2856,57 @@ class WorkbenchController extends Controller
|
||||
'wgpi.contentId',
|
||||
'FROM_UNIXTIME(wgpi.createTime, "%Y-%m-%d %H:00:00") as pushTime',
|
||||
'wgpi.targetType',
|
||||
'wgp.groupPushSubType',
|
||||
'MIN(wgpi.createTime) as createTime',
|
||||
'COUNT(DISTINCT wgpi.id) as totalCount',
|
||||
'cl.name as contentLibraryName'
|
||||
])
|
||||
->group('wgpi.workbenchId, wgpi.contentId, pushTime, wgpi.targetType');
|
||||
->group('wgpi.workbenchId, wgpi.contentId, pushTime, wgpi.targetType, wgp.groupPushSubType');
|
||||
|
||||
if (!empty($keyword)) {
|
||||
$query->where('w.name|cl.name|ci.content', 'like', '%' . $keyword . '%');
|
||||
$pushHistoryQuery->where('w.name|cl.name|ci.content', 'like', '%' . $keyword . '%');
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
$list = $query->order('createTime', 'desc')
|
||||
->page($page, $limit)
|
||||
->select();
|
||||
|
||||
// 对于有 group by 的查询,统计总数需要重新查询
|
||||
$totalQuery = Db::name('workbench_group_push_item')
|
||||
->alias('wgpi')
|
||||
->join('workbench w', 'w.id = wgpi.workbenchId', 'left')
|
||||
->join('content_item ci', 'ci.id = wgpi.contentId', 'left')
|
||||
->join('content_library cl', 'cl.id = ci.libraryId', 'left')
|
||||
->where($workbenchWhere);
|
||||
|
||||
if (!empty($keyword)) {
|
||||
$totalQuery->where('w.name|cl.name|ci.content', 'like', '%' . $keyword . '%');
|
||||
}
|
||||
|
||||
// 统计分组后的记录数(使用子查询)
|
||||
$subQuery = $totalQuery
|
||||
$pushHistoryList = $pushHistoryQuery->order('createTime', 'desc')->select();
|
||||
|
||||
// 2. 查询所有任务(包括未执行的)
|
||||
$allTasksQuery = Db::name('workbench')
|
||||
->alias('w')
|
||||
->join('workbench_group_push wgp', 'wgp.workbenchId = w.id', 'left')
|
||||
->where($workbenchWhere)
|
||||
->field([
|
||||
'wgpi.workbenchId',
|
||||
'wgpi.contentId',
|
||||
'FROM_UNIXTIME(wgpi.createTime, "%Y-%m-%d %H:00:00") as pushTime',
|
||||
'wgpi.targetType'
|
||||
])
|
||||
->group('wgpi.workbenchId, wgpi.contentId, pushTime, wgpi.targetType')
|
||||
->buildSql();
|
||||
|
||||
$total = Db::table('(' . $subQuery . ') as temp')->count();
|
||||
'w.id as workbenchId',
|
||||
'w.name as workbenchName',
|
||||
'w.createTime',
|
||||
'wgp.targetType',
|
||||
'wgp.groupPushSubType',
|
||||
'wgp.groups',
|
||||
'wgp.friends',
|
||||
'wgp.trafficPools'
|
||||
]);
|
||||
|
||||
// 处理每条记录
|
||||
foreach ($list as &$item) {
|
||||
if (!empty($keyword)) {
|
||||
$allTasksQuery->where('w.name', 'like', '%' . $keyword . '%');
|
||||
}
|
||||
|
||||
$allTasks = $allTasksQuery->select();
|
||||
|
||||
// 3. 合并数据:已执行的推送记录 + 未执行的任务
|
||||
$resultList = [];
|
||||
$executedWorkbenchIds = [];
|
||||
|
||||
// 处理已执行的推送记录
|
||||
foreach ($pushHistoryList as $item) {
|
||||
$itemWorkbenchId = $item['workbenchId'];
|
||||
$contentId = $item['contentId'];
|
||||
$pushTime = $item['pushTime'];
|
||||
$targetType = intval($item['targetType']);
|
||||
$groupPushSubType = isset($item['groupPushSubType']) ? intval($item['groupPushSubType']) : 1;
|
||||
|
||||
// 标记该工作台已有执行记录
|
||||
if (!in_array($itemWorkbenchId, $executedWorkbenchIds)) {
|
||||
$executedWorkbenchIds[] = $itemWorkbenchId;
|
||||
}
|
||||
|
||||
// 将时间字符串转换为时间戳范围(小时级别)
|
||||
$pushTimeStart = strtotime($pushTime);
|
||||
@@ -2937,23 +2945,149 @@ class WorkbenchController extends Controller
|
||||
$failCount = 0; // 简化处理,实际需要从发送状态获取
|
||||
|
||||
// 状态判断
|
||||
$status = $successCount > 0 ? 'success' : 'failed';
|
||||
$itemStatus = $successCount > 0 ? 'success' : 'failed';
|
||||
if ($failCount > 0 && $successCount > 0) {
|
||||
$status = 'partial';
|
||||
$itemStatus = 'partial';
|
||||
}
|
||||
|
||||
$item['pushType'] = $targetType == 1 ? '群推送' : '好友推送';
|
||||
$item['pushTypeCode'] = $targetType;
|
||||
$item['targetCount'] = $targetCount;
|
||||
$item['successCount'] = $successCount;
|
||||
$item['failCount'] = $failCount;
|
||||
$item['status'] = $status;
|
||||
$item['statusText'] = $status == 'success' ? '成功' : ($status == 'partial' ? '部分成功' : '失败');
|
||||
$item['createTime'] = date('Y-m-d H:i:s', $item['createTime']);
|
||||
// 任务名称(工作台名称)
|
||||
$item['taskName'] = $item['workbenchName'] ?? '';
|
||||
// 推送类型判断
|
||||
$pushTypeText = '';
|
||||
$pushTypeCode = '';
|
||||
if ($targetType == 1) {
|
||||
// 群推送
|
||||
if ($groupPushSubType == 2) {
|
||||
$pushTypeText = '群公告';
|
||||
$pushTypeCode = 'announcement';
|
||||
} else {
|
||||
$pushTypeText = '群消息';
|
||||
$pushTypeCode = 'group';
|
||||
}
|
||||
} else {
|
||||
// 好友推送
|
||||
$pushTypeText = '好友消息';
|
||||
$pushTypeCode = 'friend';
|
||||
}
|
||||
|
||||
$resultList[] = [
|
||||
'workbenchId' => $itemWorkbenchId,
|
||||
'taskName' => $item['workbenchName'] ?? '',
|
||||
'pushType' => $pushTypeText,
|
||||
'pushTypeCode' => $pushTypeCode,
|
||||
'targetCount' => $targetCount,
|
||||
'successCount' => $successCount,
|
||||
'failCount' => $failCount,
|
||||
'status' => $itemStatus,
|
||||
'statusText' => $this->getStatusText($itemStatus),
|
||||
'createTime' => date('Y-m-d H:i:s', $item['createTime']),
|
||||
'contentLibraryName' => $item['contentLibraryName'] ?? ''
|
||||
];
|
||||
}
|
||||
unset($item);
|
||||
|
||||
// 处理未执行的任务
|
||||
foreach ($allTasks as $task) {
|
||||
$taskWorkbenchId = $task['workbenchId'];
|
||||
|
||||
// 如果该任务已有执行记录,跳过(避免重复)
|
||||
if (in_array($taskWorkbenchId, $executedWorkbenchIds)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$targetType = isset($task['targetType']) ? intval($task['targetType']) : 1;
|
||||
$groupPushSubType = isset($task['groupPushSubType']) ? intval($task['groupPushSubType']) : 1;
|
||||
|
||||
// 计算目标数量(从配置中获取)
|
||||
$targetCount = 0;
|
||||
if ($targetType == 1) {
|
||||
// 群推送:统计配置的群数量
|
||||
$groups = json_decode($task['groups'] ?? '[]', true);
|
||||
$targetCount = is_array($groups) ? count($groups) : 0;
|
||||
} else {
|
||||
// 好友推送:统计配置的好友数量或流量池数量
|
||||
$friends = json_decode($task['friends'] ?? '[]', true);
|
||||
$trafficPools = json_decode($task['trafficPools'] ?? '[]', true);
|
||||
$friendCount = is_array($friends) ? count($friends) : 0;
|
||||
$poolCount = is_array($trafficPools) ? count($trafficPools) : 0;
|
||||
// 如果配置了流量池,目标数量暂时显示为流量池数量(实际数量需要从流量池中统计)
|
||||
$targetCount = $friendCount > 0 ? $friendCount : $poolCount;
|
||||
}
|
||||
|
||||
// 推送类型判断
|
||||
$pushTypeText = '';
|
||||
$pushTypeCode = '';
|
||||
if ($targetType == 1) {
|
||||
// 群推送
|
||||
if ($groupPushSubType == 2) {
|
||||
$pushTypeText = '群公告';
|
||||
$pushTypeCode = 'announcement';
|
||||
} else {
|
||||
$pushTypeText = '群消息';
|
||||
$pushTypeCode = 'group';
|
||||
}
|
||||
} else {
|
||||
// 好友推送
|
||||
$pushTypeText = '好友消息';
|
||||
$pushTypeCode = 'friend';
|
||||
}
|
||||
|
||||
$resultList[] = [
|
||||
'workbenchId' => $taskWorkbenchId,
|
||||
'taskName' => $task['workbenchName'] ?? '',
|
||||
'pushType' => $pushTypeText,
|
||||
'pushTypeCode' => $pushTypeCode,
|
||||
'targetCount' => $targetCount,
|
||||
'successCount' => 0,
|
||||
'failCount' => 0,
|
||||
'status' => 'pending',
|
||||
'statusText' => '进行中',
|
||||
'createTime' => date('Y-m-d H:i:s', $task['createTime']),
|
||||
'contentLibraryName' => ''
|
||||
];
|
||||
}
|
||||
|
||||
// 应用筛选条件
|
||||
$filteredList = [];
|
||||
foreach ($resultList as $item) {
|
||||
// 推送类型筛选
|
||||
if (!empty($pushType)) {
|
||||
if ($pushType === 'friend' && $item['pushTypeCode'] !== 'friend') {
|
||||
continue;
|
||||
}
|
||||
if ($pushType === 'group' && $item['pushTypeCode'] !== 'group') {
|
||||
continue;
|
||||
}
|
||||
if ($pushType === 'announcement' && $item['pushTypeCode'] !== 'announcement') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (!empty($status)) {
|
||||
if ($status === 'success' && $item['status'] !== 'success') {
|
||||
continue;
|
||||
}
|
||||
if ($status === 'progress') {
|
||||
// 进行中:包括 partial 和 pending
|
||||
if ($item['status'] !== 'partial' && $item['status'] !== 'pending') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ($status === 'failed' && $item['status'] !== 'failed') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$filteredList[] = $item;
|
||||
}
|
||||
|
||||
// 按创建时间倒序排序
|
||||
usort($filteredList, function($a, $b) {
|
||||
return strtotime($b['createTime']) - strtotime($a['createTime']);
|
||||
});
|
||||
|
||||
// 分页处理
|
||||
$total = count($filteredList);
|
||||
$offset = ($page - 1) * $limit;
|
||||
$list = array_slice($filteredList, $offset, $limit);
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
@@ -2967,5 +3101,21 @@ class WorkbenchController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态文本
|
||||
* @param string $status 状态码
|
||||
* @return string 状态文本
|
||||
*/
|
||||
private function getStatusText($status)
|
||||
{
|
||||
$statusMap = [
|
||||
'success' => '已完成',
|
||||
'partial' => '进行中',
|
||||
'pending' => '进行中',
|
||||
'failed' => '失败'
|
||||
];
|
||||
return $statusMap[$status] ?? '未知';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\controller\wechat;
|
||||
|
||||
use app\common\controller\ExportController;
|
||||
use app\common\model\Device as DeviceModel;
|
||||
use app\common\model\DeviceUser as DeviceUserModel;
|
||||
use app\common\model\DeviceWechatLogin as DeviceWechatLoginModel;
|
||||
use app\common\model\User as UserModel;
|
||||
use app\cunkebao\controller\BaseController;
|
||||
use library\ResponseHelper;
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 查看微信朋友圈列表(仅限当前操盘手可访问的微信)
|
||||
*/
|
||||
class GetWechatMomentsV1Controller extends BaseController
|
||||
{
|
||||
/**
|
||||
* 主操盘手获取项目下所有设备ID
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getCompanyDevicesId(): array
|
||||
{
|
||||
return DeviceModel::where('companyId', $this->getUserInfo('companyId'))
|
||||
->column('id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 非主操盘手仅可查看分配到的设备
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getUserDevicesId(): array
|
||||
{
|
||||
return DeviceUserModel::where([
|
||||
'userId' => $this->getUserInfo('id'),
|
||||
'companyId' => $this->getUserInfo('companyId'),
|
||||
])->column('deviceId');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户可访问的设备ID
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getDevicesId(): array
|
||||
{
|
||||
return ($this->getUserInfo('isAdmin') == UserModel::ADMIN_STP)
|
||||
? $this->getCompanyDevicesId()
|
||||
: $this->getUserDevicesId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户可访问的微信ID集合
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function getAccessibleWechatIds(): array
|
||||
{
|
||||
$deviceIds = $this->getDevicesId();
|
||||
if (empty($deviceIds)) {
|
||||
throw new \Exception('暂无可用设备', 200);
|
||||
}
|
||||
|
||||
return DeviceWechatLoginModel::distinct(true)
|
||||
->where('companyId', $this->getUserInfo('companyId'))
|
||||
->whereIn('deviceId', $deviceIds)
|
||||
->column('wechatId');
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看朋友圈列表
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
try {
|
||||
$wechatId = $this->request->param('wechatId/s', '');
|
||||
if (empty($wechatId)) {
|
||||
return ResponseHelper::error('wechatId不能为空');
|
||||
}
|
||||
|
||||
// 权限校验:只能查看当前账号可访问的微信
|
||||
$accessibleWechatIds = $this->getAccessibleWechatIds();
|
||||
if (!in_array($wechatId, $accessibleWechatIds, true)) {
|
||||
return ResponseHelper::error('无权查看该微信的朋友圈', 403);
|
||||
}
|
||||
|
||||
// 获取对应的微信账号ID
|
||||
$accountId = Db::table('s2_wechat_account')
|
||||
->where('wechatId', $wechatId)
|
||||
->value('id');
|
||||
|
||||
if (empty($accountId)) {
|
||||
return ResponseHelper::error('微信账号不存在或尚未同步', 404);
|
||||
}
|
||||
|
||||
$query = Db::table('s2_wechat_moments')
|
||||
->where('wechatAccountId', $accountId);
|
||||
|
||||
// 关键词搜索
|
||||
if ($keyword = trim((string)$this->request->param('keyword', ''))) {
|
||||
$query->whereLike('content', '%' . $keyword . '%');
|
||||
}
|
||||
|
||||
// 类型筛选
|
||||
$type = $this->request->param('type', '');
|
||||
if ($type !== '' && $type !== null) {
|
||||
$query->where('type', (int)$type);
|
||||
}
|
||||
|
||||
// 时间筛选
|
||||
$startTime = $this->request->param('startTime', '');
|
||||
$endTime = $this->request->param('endTime', '');
|
||||
if ($startTime || $endTime) {
|
||||
$start = $startTime ? strtotime($startTime) : 0;
|
||||
$end = $endTime ? strtotime($endTime) : time();
|
||||
if ($start && $end && $end < $start) {
|
||||
return ResponseHelper::error('结束时间不能早于开始时间');
|
||||
}
|
||||
$query->whereBetween('createTime', [$start ?: 0, $end ?: time()]);
|
||||
}
|
||||
|
||||
$page = (int)$this->request->param('page', 1);
|
||||
$limit = (int)$this->request->param('limit', 10);
|
||||
|
||||
$paginator = $query->order('createTime', 'desc')
|
||||
->paginate($limit, false, ['page' => $page]);
|
||||
|
||||
$list = array_map(function ($item) {
|
||||
return $this->formatMomentRow($item);
|
||||
}, $paginator->items());
|
||||
|
||||
return ResponseHelper::success([
|
||||
'list' => $list,
|
||||
'total' => $paginator->total(),
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出朋友圈数据到Excel
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function export()
|
||||
{
|
||||
try {
|
||||
$wechatId = $this->request->param('wechatId/s', '');
|
||||
if (empty($wechatId)) {
|
||||
return ResponseHelper::error('wechatId不能为空');
|
||||
}
|
||||
|
||||
// 权限校验:只能查看当前账号可访问的微信
|
||||
$accessibleWechatIds = $this->getAccessibleWechatIds();
|
||||
if (!in_array($wechatId, $accessibleWechatIds, true)) {
|
||||
return ResponseHelper::error('无权查看该微信的朋友圈', 403);
|
||||
}
|
||||
|
||||
// 获取对应的微信账号ID
|
||||
$accountId = Db::table('s2_wechat_account')
|
||||
->where('wechatId', $wechatId)
|
||||
->value('id');
|
||||
|
||||
if (empty($accountId)) {
|
||||
return ResponseHelper::error('微信账号不存在或尚未同步', 404);
|
||||
}
|
||||
|
||||
$query = Db::table('s2_wechat_moments')
|
||||
->where('wechatAccountId', $accountId);
|
||||
|
||||
// 关键词搜索
|
||||
if ($keyword = trim((string)$this->request->param('keyword', ''))) {
|
||||
$query->whereLike('content', '%' . $keyword . '%');
|
||||
}
|
||||
|
||||
// 类型筛选
|
||||
$type = $this->request->param('type', '');
|
||||
if ($type !== '' && $type !== null) {
|
||||
$query->where('type', (int)$type);
|
||||
}
|
||||
|
||||
// 时间筛选
|
||||
$startTime = $this->request->param('startTime', '');
|
||||
$endTime = $this->request->param('endTime', '');
|
||||
if ($startTime || $endTime) {
|
||||
$start = $startTime ? strtotime($startTime) : 0;
|
||||
$end = $endTime ? strtotime($endTime) : time();
|
||||
if ($start && $end && $end < $start) {
|
||||
return ResponseHelper::error('结束时间不能早于开始时间');
|
||||
}
|
||||
$query->whereBetween('createTime', [$start ?: 0, $end ?: time()]);
|
||||
}
|
||||
|
||||
// 获取所有数据(不分页)
|
||||
$moments = $query->order('createTime', 'desc')->select();
|
||||
|
||||
if (empty($moments)) {
|
||||
return ResponseHelper::error('暂无数据可导出');
|
||||
}
|
||||
|
||||
// 定义表头
|
||||
$headers = [
|
||||
'date' => '日期',
|
||||
'postTime' => '投放时间',
|
||||
'functionCategory' => '作用分类',
|
||||
'content' => '朋友圈文案',
|
||||
'selfReply' => '自回评内容',
|
||||
'displayForm' => '朋友圈展示形式',
|
||||
'image1' => '配图1',
|
||||
'image2' => '配图2',
|
||||
'image3' => '配图3',
|
||||
'image4' => '配图4',
|
||||
'image5' => '配图5',
|
||||
'image6' => '配图6',
|
||||
'image7' => '配图7',
|
||||
'image8' => '配图8',
|
||||
'image9' => '配图9',
|
||||
];
|
||||
|
||||
// 格式化数据
|
||||
$rows = [];
|
||||
foreach ($moments as $moment) {
|
||||
$resUrls = $this->decodeJson($moment['resUrls'] ?? null);
|
||||
$imageUrls = is_array($resUrls) ? $resUrls : [];
|
||||
|
||||
// 格式化日期和时间
|
||||
$createTime = !empty($moment['createTime'])
|
||||
? (is_numeric($moment['createTime']) ? $moment['createTime'] : strtotime($moment['createTime']))
|
||||
: 0;
|
||||
$date = $createTime ? date('Y年m月d日', $createTime) : '';
|
||||
$postTime = $createTime ? date('H:i', $createTime) : '';
|
||||
|
||||
// 判断展示形式
|
||||
$displayForm = '';
|
||||
if (!empty($moment['content']) && !empty($imageUrls)) {
|
||||
$displayForm = '文字+图片';
|
||||
} elseif (!empty($moment['content'])) {
|
||||
$displayForm = '文字';
|
||||
} elseif (!empty($imageUrls)) {
|
||||
$displayForm = '图片';
|
||||
}
|
||||
|
||||
$row = [
|
||||
'date' => $date,
|
||||
'postTime' => $postTime,
|
||||
'functionCategory' => '', // 暂时放空
|
||||
'content' => $moment['content'] ?? '',
|
||||
'selfReply' => '', // 暂时放空
|
||||
'displayForm' => $displayForm,
|
||||
];
|
||||
|
||||
// 分配图片到配图1-9列
|
||||
for ($i = 1; $i <= 9; $i++) {
|
||||
$imageKey = 'image' . $i;
|
||||
$row[$imageKey] = isset($imageUrls[$i - 1]) ? $imageUrls[$i - 1] : '';
|
||||
}
|
||||
|
||||
$rows[] = $row;
|
||||
}
|
||||
|
||||
// 定义图片列(配图1-9)
|
||||
$imageColumns = ['image1', 'image2', 'image3', 'image4', 'image5', 'image6', 'image7', 'image8', 'image9'];
|
||||
|
||||
// 生成文件名
|
||||
$fileName = '朋友圈投放_' . date('Ymd_His');
|
||||
|
||||
// 调用导出方法,优化图片显示效果
|
||||
ExportController::exportExcelWithImages(
|
||||
$fileName,
|
||||
$headers,
|
||||
$rows,
|
||||
$imageColumns,
|
||||
'朋友圈投放',
|
||||
[
|
||||
'imageWidth' => 120, // 图片宽度(像素)
|
||||
'imageHeight' => 120, // 图片高度(像素)
|
||||
'imageColumnWidth' => 18, // 图片列宽(Excel单位)
|
||||
'rowHeight' => 130, // 行高(像素)
|
||||
'columnWidths' => [ // 特定列的固定宽度
|
||||
'date' => 15, // 日期列宽
|
||||
'postTime' => 12, // 投放时间列宽
|
||||
'functionCategory' => 15, // 作用分类列宽
|
||||
'content' => 40, // 朋友圈文案列宽(自动调整可能不够)
|
||||
'selfReply' => 30, // 自回评内容列宽
|
||||
'displayForm' => 18, // 朋友圈展示形式列宽
|
||||
],
|
||||
'titleRow' => [ // 标题行内容(第一行)
|
||||
'朋友圈投放',
|
||||
'我能提供什么价值? (40%) 有谁正在和我合作 (20%) 如何和我合作? (20%) 你找我合作需要付多少钱? (20%)'
|
||||
]
|
||||
]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error('导出失败:' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化朋友圈数据
|
||||
*
|
||||
* @param array $row
|
||||
* @return array
|
||||
*/
|
||||
protected function formatMomentRow(array $row): array
|
||||
{
|
||||
$formatTime = function ($timestamp) {
|
||||
if (empty($timestamp)) {
|
||||
return '';
|
||||
}
|
||||
return is_numeric($timestamp)
|
||||
? date('Y-m-d H:i:s', $timestamp)
|
||||
: date('Y-m-d H:i:s', strtotime($timestamp));
|
||||
};
|
||||
|
||||
return [
|
||||
'id' => (int)$row['id'],
|
||||
'snsId' => $row['snsId'] ?? '',
|
||||
'type' => (int)($row['type'] ?? 0),
|
||||
'content' => $row['content'] ?? '',
|
||||
'commentList' => $this->decodeJson($row['commentList'] ?? null),
|
||||
'likeList' => $this->decodeJson($row['likeList'] ?? null),
|
||||
'resUrls' => $this->decodeJson($row['resUrls'] ?? null),
|
||||
'createTime' => $formatTime($row['createTime'] ?? null),
|
||||
'momentEntity' => [
|
||||
'lat' => $row['lat'] ?? 0,
|
||||
'lng' => $row['lng'] ?? 0,
|
||||
'location' => $row['location'] ?? '',
|
||||
'picSize' => $row['picSize'] ?? 0,
|
||||
'userName' => $row['userName'] ?? '',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON字段解析
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return array
|
||||
*/
|
||||
protected function decodeJson($value): array
|
||||
{
|
||||
if (empty($value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$decoded = json_decode($value, true);
|
||||
return $decoded ?: [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\controller\wechat;
|
||||
|
||||
use app\common\service\WechatAccountHealthScoreService;
|
||||
use app\cunkebao\controller\BaseController;
|
||||
use library\ResponseHelper;
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 微信账号概览控制器
|
||||
* 提供账号概览页面的所有数据接口
|
||||
*/
|
||||
class GetWechatOverviewV1Controller extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取微信账号概览数据
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
try {
|
||||
$wechatId = $this->request->param('wechatId', '');
|
||||
|
||||
if (empty($wechatId)) {
|
||||
return ResponseHelper::error('微信ID不能为空');
|
||||
}
|
||||
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
|
||||
// 获取微信账号ID(accountId)
|
||||
$account = Db::table('s2_wechat_account')
|
||||
->where('wechatId', $wechatId)
|
||||
->find();
|
||||
|
||||
if (empty($account)) {
|
||||
return ResponseHelper::error('微信账号不存在');
|
||||
}
|
||||
|
||||
$accountId = $account['id'];
|
||||
|
||||
// 1. 健康分评估
|
||||
$healthScoreData = $this->getHealthScoreAssessment($accountId, $wechatId);
|
||||
|
||||
// 2. 账号价值(模拟数据)
|
||||
$accountValue = $this->getAccountValue($accountId);
|
||||
|
||||
// 3. 今日价值变化(模拟数据)
|
||||
$todayValueChange = $this->getTodayValueChange($accountId);
|
||||
|
||||
// 4. 好友总数
|
||||
$totalFriends = $this->getTotalFriends($wechatId, $companyId);
|
||||
|
||||
// 5. 今日新增好友
|
||||
$todayNewFriends = $this->getTodayNewFriends($wechatId);
|
||||
|
||||
// 6. 高价群聊
|
||||
$highValueChatrooms = $this->getHighValueChatrooms($wechatId, $companyId);
|
||||
|
||||
// 7. 今日新增群聊
|
||||
$todayNewChatrooms = $this->getTodayNewChatrooms($wechatId, $companyId);
|
||||
|
||||
$result = [
|
||||
'healthScoreAssessment' => $healthScoreData,
|
||||
'accountValue' => $accountValue,
|
||||
'todayValueChange' => $todayValueChange,
|
||||
'totalFriends' => $totalFriends,
|
||||
'todayNewFriends' => $todayNewFriends,
|
||||
'highValueChatrooms' => $highValueChatrooms,
|
||||
'todayNewChatrooms' => $todayNewChatrooms,
|
||||
];
|
||||
|
||||
return ResponseHelper::success($result);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取健康分评估数据
|
||||
*
|
||||
* @param int $accountId 账号ID
|
||||
* @param string $wechatId 微信ID
|
||||
* @return array
|
||||
*/
|
||||
protected function getHealthScoreAssessment($accountId, $wechatId)
|
||||
{
|
||||
// 获取健康分信息
|
||||
$healthScoreService = new WechatAccountHealthScoreService();
|
||||
$healthScoreInfo = $healthScoreService->getHealthScore($accountId);
|
||||
|
||||
$healthScore = $healthScoreInfo['healthScore'] ?? 0;
|
||||
$maxAddFriendPerDay = $healthScoreInfo['maxAddFriendPerDay'] ?? 0;
|
||||
|
||||
// 获取今日已加好友数
|
||||
$todayAdded = $this->getTodayAddedCount($wechatId);
|
||||
|
||||
// 获取最后添加时间
|
||||
$lastAddTime = $this->getLastAddTime($wechatId);
|
||||
|
||||
// 判断状态标签
|
||||
$statusTag = $todayAdded > 0 ? '已添加加人' : '';
|
||||
|
||||
// 获取基础构成
|
||||
$baseComposition = $this->getBaseComposition($healthScoreInfo);
|
||||
|
||||
// 获取动态记录
|
||||
$dynamicRecords = $this->getDynamicRecords($healthScoreInfo);
|
||||
|
||||
return [
|
||||
'score' => $healthScore,
|
||||
'dailyLimit' => $maxAddFriendPerDay,
|
||||
'todayAdded' => $todayAdded,
|
||||
'lastAddTime' => $lastAddTime,
|
||||
'statusTag' => $statusTag,
|
||||
'baseComposition' => $baseComposition,
|
||||
'dynamicRecords' => $dynamicRecords,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取基础构成数据
|
||||
*
|
||||
* @param array $healthScoreInfo 健康分信息
|
||||
* @return array
|
||||
*/
|
||||
protected function getBaseComposition($healthScoreInfo)
|
||||
{
|
||||
$baseScore = $healthScoreInfo['baseScore'] ?? 0;
|
||||
$baseInfoScore = $healthScoreInfo['baseInfoScore'] ?? 0;
|
||||
$friendCountScore = $healthScoreInfo['friendCountScore'] ?? 0;
|
||||
$friendCount = $healthScoreInfo['friendCount'] ?? 0;
|
||||
|
||||
// 账号基础分(默认60分)
|
||||
$accountBaseScore = 60;
|
||||
|
||||
// 已修改微信号(如果baseInfoScore > 0,说明已修改)
|
||||
$isModifiedAlias = $baseInfoScore > 0;
|
||||
|
||||
$composition = [
|
||||
[
|
||||
'name' => '账号基础分',
|
||||
'score' => $accountBaseScore,
|
||||
'formatted' => '+' . $accountBaseScore,
|
||||
]
|
||||
];
|
||||
|
||||
// 如果已修改微信号,添加基础信息分
|
||||
if ($isModifiedAlias) {
|
||||
$composition[] = [
|
||||
'name' => '已修改微信号',
|
||||
'score' => $baseInfoScore,
|
||||
'formatted' => '+' . $baseInfoScore,
|
||||
];
|
||||
}
|
||||
|
||||
// 好友数量加成
|
||||
if ($friendCountScore > 0) {
|
||||
$composition[] = [
|
||||
'name' => '好友数量加成',
|
||||
'score' => $friendCountScore,
|
||||
'formatted' => '+' . $friendCountScore,
|
||||
'friendCount' => $friendCount, // 显示好友总数
|
||||
];
|
||||
}
|
||||
|
||||
return $composition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取动态记录数据
|
||||
*
|
||||
* @param array $healthScoreInfo 健康分信息
|
||||
* @return array
|
||||
*/
|
||||
protected function getDynamicRecords($healthScoreInfo)
|
||||
{
|
||||
$records = [];
|
||||
|
||||
$frequentPenalty = $healthScoreInfo['frequentPenalty'] ?? 0;
|
||||
$frequentCount = $healthScoreInfo['frequentCount'] ?? 0;
|
||||
$banPenalty = $healthScoreInfo['banPenalty'] ?? 0;
|
||||
$isBanned = $healthScoreInfo['isBanned'] ?? 0;
|
||||
$noFrequentBonus = $healthScoreInfo['noFrequentBonus'] ?? 0;
|
||||
$consecutiveNoFrequentDays = $healthScoreInfo['consecutiveNoFrequentDays'] ?? 0;
|
||||
$lastFrequentTime = $healthScoreInfo['lastFrequentTime'] ?? null;
|
||||
|
||||
// 频繁扣分记录
|
||||
// 根据frequentCount判断是首次还是再次
|
||||
// frequentPenalty存储的是当前状态的扣分(-15或-25),不是累计值
|
||||
if ($frequentCount > 0 && $frequentPenalty < 0) {
|
||||
if ($frequentCount == 1) {
|
||||
// 首次频繁:-15分
|
||||
$records[] = [
|
||||
'name' => '首次触发限额',
|
||||
'score' => $frequentPenalty,
|
||||
'formatted' => (string)$frequentPenalty,
|
||||
'type' => 'penalty',
|
||||
'time' => $lastFrequentTime ? date('Y-m-d H:i:s', $lastFrequentTime) : null,
|
||||
];
|
||||
} else {
|
||||
// 再次频繁:-25分
|
||||
$records[] = [
|
||||
'name' => '再次触发限额',
|
||||
'score' => $frequentPenalty,
|
||||
'formatted' => (string)$frequentPenalty,
|
||||
'type' => 'penalty',
|
||||
'time' => $lastFrequentTime ? date('Y-m-d H:i:s', $lastFrequentTime) : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 封号扣分记录
|
||||
if ($isBanned && $banPenalty < 0) {
|
||||
$lastBanTime = $healthScoreInfo['lastBanTime'] ?? null;
|
||||
$records[] = [
|
||||
'name' => '封号',
|
||||
'score' => $banPenalty,
|
||||
'formatted' => (string)$banPenalty,
|
||||
'type' => 'penalty',
|
||||
'time' => $lastBanTime ? date('Y-m-d H:i:s', $lastBanTime) : null,
|
||||
];
|
||||
}
|
||||
|
||||
// 不频繁加分记录
|
||||
if ($noFrequentBonus > 0 && $consecutiveNoFrequentDays >= 3) {
|
||||
$lastNoFrequentTime = $healthScoreInfo['lastNoFrequentTime'] ?? null;
|
||||
$records[] = [
|
||||
'name' => '连续' . $consecutiveNoFrequentDays . '天不触发频繁',
|
||||
'score' => $noFrequentBonus,
|
||||
'formatted' => '+' . $noFrequentBonus,
|
||||
'type' => 'bonus',
|
||||
'time' => $lastNoFrequentTime ? date('Y-m-d H:i:s', $lastNoFrequentTime) : null,
|
||||
];
|
||||
}
|
||||
|
||||
return $records;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取今日已加好友数
|
||||
*
|
||||
* @param string $wechatId 微信ID
|
||||
* @return int
|
||||
*/
|
||||
protected function getTodayAddedCount($wechatId)
|
||||
{
|
||||
$start = strtotime(date('Y-m-d 00:00:00'));
|
||||
$end = strtotime(date('Y-m-d 23:59:59'));
|
||||
|
||||
return Db::table('s2_friend_task')
|
||||
->where('wechatId', $wechatId)
|
||||
->whereBetween('createTime', [$start, $end])
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最后添加时间
|
||||
*
|
||||
* @param string $wechatId 微信ID
|
||||
* @return string
|
||||
*/
|
||||
protected function getLastAddTime($wechatId)
|
||||
{
|
||||
$lastTask = Db::table('s2_friend_task')
|
||||
->where('wechatId', $wechatId)
|
||||
->order('createTime', 'desc')
|
||||
->find();
|
||||
|
||||
if (empty($lastTask) || empty($lastTask['createTime'])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return date('H:i:s', $lastTask['createTime']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账号价值(模拟数据)
|
||||
*
|
||||
* @param int $accountId 账号ID
|
||||
* @return array
|
||||
*/
|
||||
protected function getAccountValue($accountId)
|
||||
{
|
||||
// TODO: 后续替换为真实计算逻辑
|
||||
// 模拟数据:¥29,800
|
||||
$value = 29800;
|
||||
|
||||
return [
|
||||
'value' => $value,
|
||||
'formatted' => '¥' . number_format($value, 0, '.', ','),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取今日价值变化(模拟数据)
|
||||
*
|
||||
* @param int $accountId 账号ID
|
||||
* @return array
|
||||
*/
|
||||
protected function getTodayValueChange($accountId)
|
||||
{
|
||||
// TODO: 后续替换为真实计算逻辑
|
||||
// 模拟数据:+500
|
||||
$change = 500;
|
||||
|
||||
return [
|
||||
'change' => $change,
|
||||
'formatted' => $change > 0 ? '+' . $change : (string)$change,
|
||||
'isPositive' => $change > 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取好友总数
|
||||
*
|
||||
* @param string $wechatId 微信ID
|
||||
* @param int $companyId 公司ID
|
||||
* @return int
|
||||
*/
|
||||
protected function getTotalFriends($wechatId, $companyId)
|
||||
{
|
||||
// 优先从 s2_wechat_account 表获取
|
||||
$account = Db::table('s2_wechat_account')
|
||||
->where('wechatId', $wechatId)
|
||||
->field('totalFriend')
|
||||
->find();
|
||||
|
||||
if (!empty($account) && isset($account['totalFriend'])) {
|
||||
return (int)$account['totalFriend'];
|
||||
}
|
||||
|
||||
// 如果 totalFriend 为空,则从 s2_wechat_friend 表统计
|
||||
return Db::table('s2_wechat_friend')
|
||||
->where('ownerWechatId', $wechatId)
|
||||
->where('isDeleted', 0)
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取今日新增好友数
|
||||
*
|
||||
* @param string $wechatId 微信ID
|
||||
* @return int
|
||||
*/
|
||||
protected function getTodayNewFriends($wechatId)
|
||||
{
|
||||
$start = strtotime(date('Y-m-d 00:00:00'));
|
||||
$end = strtotime(date('Y-m-d 23:59:59'));
|
||||
|
||||
// 从 s2_wechat_friend 表统计今日新增
|
||||
return Db::table('s2_wechat_friend')
|
||||
->where('ownerWechatId', $wechatId)
|
||||
->whereBetween('createTime', [$start, $end])
|
||||
->where('isDeleted', 0)
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取高价群聊数量
|
||||
* 高价群聊定义:群成员数 >= 50 的群聊
|
||||
*
|
||||
* @param string $wechatId 微信ID
|
||||
* @param int $companyId 公司ID
|
||||
* @return int
|
||||
*/
|
||||
protected function getHighValueChatrooms($wechatId, $companyId)
|
||||
{
|
||||
// 高价群聊定义:群成员数 >= 50
|
||||
$minMemberCount = 50;
|
||||
|
||||
// 查询该微信账号下的高价群聊
|
||||
// 使用子查询统计每个群的成员数
|
||||
$result = Db::query("
|
||||
SELECT COUNT(DISTINCT c.chatroomId) as count
|
||||
FROM s2_wechat_chatroom c
|
||||
INNER JOIN (
|
||||
SELECT chatroomId, COUNT(*) as memberCount
|
||||
FROM s2_wechat_chatroom_member
|
||||
GROUP BY chatroomId
|
||||
HAVING memberCount >= ?
|
||||
) m ON c.chatroomId = m.chatroomId
|
||||
WHERE c.wechatAccountWechatId = ?
|
||||
AND c.isDeleted = 0
|
||||
", [$minMemberCount, $wechatId]);
|
||||
|
||||
return !empty($result) ? (int)$result[0]['count'] : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取今日新增群聊数
|
||||
*
|
||||
* @param string $wechatId 微信ID
|
||||
* @param int $companyId 公司ID
|
||||
* @return int
|
||||
*/
|
||||
protected function getTodayNewChatrooms($wechatId, $companyId)
|
||||
{
|
||||
$start = strtotime(date('Y-m-d 00:00:00'));
|
||||
$end = strtotime(date('Y-m-d 23:59:59'));
|
||||
|
||||
return Db::table('s2_wechat_chatroom')
|
||||
->where('wechatAccountWechatId', $wechatId)
|
||||
->whereBetween('createTime', [$start, $end])
|
||||
->where('isDeleted', 0)
|
||||
->count();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,25 @@ use app\common\model\DeviceUser as DeviceUserModel;
|
||||
use app\common\model\DeviceWechatLogin as DeviceWechatLoginModel;
|
||||
use app\common\model\User as UserModel;
|
||||
use app\common\model\WechatAccount as WechatAccountModel;
|
||||
use app\common\model\WechatCustomer as WechatCustomerModel;
|
||||
use app\common\model\WechatFriendShip as WechatFriendShipModel;
|
||||
// 不再使用WechatFriendShipModel和WechatCustomerModel,改为直接查询s2_wechat_friend和s2_wechat_account_score表
|
||||
use app\cunkebao\controller\BaseController;
|
||||
use library\ResponseHelper;
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 微信控制器
|
||||
*
|
||||
* 性能优化建议:
|
||||
* 1. 为以下字段添加索引以提高查询性能:
|
||||
* - device_wechat_login表: (companyId, wechatId), (deviceId)
|
||||
* - wechat_account表: (wechatId)
|
||||
* - wechat_customer表: (companyId, wechatId)
|
||||
* - wechat_friend_ship表: (ownerWechatId), (createTime)
|
||||
* - s2_wechat_message表: (wechatAccountId, wechatTime)
|
||||
*
|
||||
* 2. 考虑创建以下复合索引:
|
||||
* - device_wechat_login表: (companyId, deviceId, wechatId)
|
||||
* - wechat_friend_ship表: (ownerWechatId, createTime)
|
||||
*/
|
||||
class GetWechatsOnDevicesV1Controller extends BaseController
|
||||
{
|
||||
@@ -66,6 +77,7 @@ class GetWechatsOnDevicesV1Controller extends BaseController
|
||||
|
||||
/**
|
||||
* 获取有登录设备的微信id
|
||||
* 优化:使用索引字段,减少数据查询量
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
@@ -76,12 +88,12 @@ class GetWechatsOnDevicesV1Controller extends BaseController
|
||||
throw new \Exception('暂无设备数据', 200);
|
||||
}
|
||||
|
||||
return DeviceWechatLoginModel::where(
|
||||
[
|
||||
// 优化:直接使用DISTINCT减少数据传输量
|
||||
return DeviceWechatLoginModel::distinct(true)
|
||||
->where([
|
||||
'companyId' => $this->getUserInfo('companyId'),
|
||||
// 'alive' => DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE,
|
||||
]
|
||||
)
|
||||
])
|
||||
->where('deviceId', 'in', $deviceIds)
|
||||
->column('wechatId');
|
||||
}
|
||||
@@ -110,24 +122,50 @@ class GetWechatsOnDevicesV1Controller extends BaseController
|
||||
|
||||
/**
|
||||
* 获取在线微信账号列表
|
||||
* 优化:减少查询字段,使用索引,优化JOIN条件
|
||||
*
|
||||
* @param array $where
|
||||
* @return \think\Paginator 分页对象
|
||||
*/
|
||||
protected function getOnlineWechatList(array $where): \think\Paginator
|
||||
{
|
||||
// 获取微信在线状态筛选参数(1=在线,0=离线,不传=全部)
|
||||
$wechatStatus = $this->request->param('wechatStatus');
|
||||
|
||||
// 优化:只查询必要字段,使用FORCE INDEX提示数据库使用索引
|
||||
$query = WechatAccountModel::alias('w')
|
||||
->field(
|
||||
[
|
||||
'w.id', 'w.nickname', 'w.avatar', 'w.wechatId',
|
||||
'CASE WHEN w.alias IS NULL OR w.alias = "" THEN w.wechatId ELSE w.alias END AS wechatAccount',
|
||||
'l.deviceId','l.alive'
|
||||
'MAX(l.deviceId) as deviceId', 'MAX(l.alive) as alive' // 使用MAX确保GROUP BY时获取正确的在线状态
|
||||
]
|
||||
)
|
||||
->join('device_wechat_login l', 'w.wechatId = l.wechatId AND l.companyId = '. $this->getUserInfo('companyId'))
|
||||
->order('w.id desc')
|
||||
->group('w.wechatId');
|
||||
// 优化:使用INNER JOIN代替LEFT JOIN,并添加索引提示
|
||||
->join('device_wechat_login l', 'w.wechatId = l.wechatId AND l.companyId = '. $this->getUserInfo('companyId'), 'INNER')
|
||||
// 添加s2_wechat_account表的LEFT JOIN,用于筛选微信在线状态
|
||||
->join(['s2_wechat_account' => 'sa'], 'w.wechatId = sa.wechatId', 'LEFT')
|
||||
->group('w.wechatId')
|
||||
// 优化:在线状态优先排序(alive=1的排在前面),然后按wechatId排序
|
||||
// 注意:ORDER BY使用SELECT中定义的别名alive,而不是聚合函数
|
||||
->order('alive desc, w.wechatId desc');
|
||||
|
||||
// 根据wechatStatus参数筛选(1=在线,0=离线,不传=全部)
|
||||
if ($wechatStatus !== null && $wechatStatus !== '') {
|
||||
$wechatStatus = (int)$wechatStatus;
|
||||
if ($wechatStatus === 1) {
|
||||
// 筛选在线:wechatAlive = 1
|
||||
$query->where('sa.wechatAlive', 1);
|
||||
} elseif ($wechatStatus === 0) {
|
||||
// 筛选离线:wechatAlive = 0 或 NULL
|
||||
$query->where(function($query) {
|
||||
$query->where('sa.wechatAlive', 0)
|
||||
->whereOr('sa.wechatAlive', 'exp', 'IS NULL');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 应用查询条件
|
||||
foreach ($where as $key => $value) {
|
||||
if (is_numeric($key) && is_array($value) && isset($value[0]) && $value[0] === 'exp') {
|
||||
$query->whereExp('', $value[1]);
|
||||
@@ -142,7 +180,12 @@ class GetWechatsOnDevicesV1Controller extends BaseController
|
||||
$query->where($key, $value);
|
||||
}
|
||||
|
||||
return $query->paginate($this->request->param('limit/d', 10), false, ['page' => $this->request->param('page/d', 1)]);
|
||||
// 优化:使用简单计数查询
|
||||
return $query->paginate(
|
||||
$this->request->param('limit/d', 10),
|
||||
false,
|
||||
['page' => $this->request->param('page/d', 1)]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,9 +210,15 @@ class GetWechatsOnDevicesV1Controller extends BaseController
|
||||
$metrics = $this->collectWechatMetrics($wechatIds);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$addLimit = $metrics['addLimit'][$item->wechatId] ?? 0;
|
||||
$todayAdded = $metrics['todayAdded'][$item->wechatId] ?? 0;
|
||||
// 计算今日可添加数量 = 可添加额度 - 今日已添加
|
||||
$todayCanAdd = max(0, $addLimit - $todayAdded);
|
||||
|
||||
$sections = $item->toArray() + [
|
||||
'times' => $metrics['addLimit'][$item->wechatId] ?? 0,
|
||||
'addedCount' => $metrics['todayAdded'][$item->wechatId] ?? 0,
|
||||
'times' => $addLimit,
|
||||
'addedCount' => $todayAdded,
|
||||
'todayCanAdd' => $todayCanAdd, // 今日可添加数量
|
||||
'wechatStatus' => $metrics['wechatStatus'][$item->wechatId] ?? 0,
|
||||
'totalFriend' => $metrics['totalFriend'][$item->wechatId] ?? 0,
|
||||
'deviceMemo' => $metrics['deviceMemo'][$item->wechatId] ?? '',
|
||||
@@ -184,6 +233,8 @@ class GetWechatsOnDevicesV1Controller extends BaseController
|
||||
|
||||
/**
|
||||
* 批量收集微信账号的统计信息
|
||||
* 优化:合并查询,减少数据库访问次数,使用缓存
|
||||
*
|
||||
* @param array $wechatIds
|
||||
* @return array
|
||||
*/
|
||||
@@ -203,106 +254,167 @@ class GetWechatsOnDevicesV1Controller extends BaseController
|
||||
}
|
||||
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
|
||||
// 可添加好友额度
|
||||
$weightRows = WechatCustomerModel::where('companyId', $companyId)
|
||||
|
||||
// 使用缓存键,避免短时间内重复查询
|
||||
$cacheKey = 'wechat_metrics_' . md5(implode(',', $wechatIds) . '_' . $companyId);
|
||||
|
||||
// 尝试从缓存获取数据(缓存5分钟)
|
||||
$cachedMetrics = cache($cacheKey);
|
||||
if ($cachedMetrics) {
|
||||
return $cachedMetrics;
|
||||
}
|
||||
|
||||
// 优化1:可添加好友额度 - 从s2_wechat_account_score表获取maxAddFriendPerDay
|
||||
$scoreRows = Db::table('s2_wechat_account_score')
|
||||
->whereIn('wechatId', $wechatIds)
|
||||
->column('weight', 'wechatId');
|
||||
foreach ($weightRows as $wechatId => $weight) {
|
||||
$decoded = json_decode($weight, true);
|
||||
$metrics['addLimit'][$wechatId] = $decoded['addLimit'] ?? 0;
|
||||
->column('maxAddFriendPerDay', 'wechatId');
|
||||
foreach ($scoreRows as $wechatId => $maxAddFriendPerDay) {
|
||||
$metrics['addLimit'][$wechatId] = (int)($maxAddFriendPerDay ?? 0);
|
||||
}
|
||||
|
||||
// 今日新增好友
|
||||
// 优化2:今日新增好友 - 使用索引字段和预计算
|
||||
$start = strtotime(date('Y-m-d 00:00:00'));
|
||||
$end = strtotime(date('Y-m-d 23:59:59'));
|
||||
$todayRows = WechatFriendShipModel::whereIn('ownerWechatId', $wechatIds)
|
||||
->whereBetween('createTime', [$start, $end])
|
||||
->field('ownerWechatId, COUNT(*) as total')
|
||||
->group('ownerWechatId')
|
||||
->select();
|
||||
foreach ($todayRows as $row) {
|
||||
$wechatId = is_array($row) ? ($row['ownerWechatId'] ?? '') : ($row->ownerWechatId ?? '');
|
||||
|
||||
// 使用单次查询获取所有wechatIds的今日新增和总好友数
|
||||
// 根据数据库结构使用s2_wechat_friend表而不是wechat_friend_ship
|
||||
$friendshipStats = Db::query("
|
||||
SELECT
|
||||
ownerWechatId,
|
||||
SUM(IF(createTime BETWEEN {$start} AND {$end}, 1, 0)) as today_added,
|
||||
COUNT(*) as total_friend
|
||||
FROM
|
||||
s2_wechat_friend
|
||||
WHERE
|
||||
ownerWechatId IN ('" . implode("','", $wechatIds) . "')
|
||||
AND isDeleted = 0
|
||||
GROUP BY
|
||||
ownerWechatId
|
||||
");
|
||||
|
||||
// 处理结果
|
||||
foreach ($friendshipStats as $row) {
|
||||
$wechatId = $row['ownerWechatId'] ?? '';
|
||||
if ($wechatId) {
|
||||
$metrics['todayAdded'][$wechatId] = (int)(is_array($row) ? ($row['total'] ?? 0) : ($row->total ?? 0));
|
||||
$metrics['todayAdded'][$wechatId] = (int)($row['today_added'] ?? 0);
|
||||
$metrics['totalFriend'][$wechatId] = (int)($row['total_friend'] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 总好友
|
||||
$friendRows = WechatFriendShipModel::whereIn('ownerWechatId', $wechatIds)
|
||||
->field('ownerWechatId, COUNT(*) as total')
|
||||
->group('ownerWechatId')
|
||||
// 优化3:微信在线状态 - 从s2_wechat_account表获取wechatAlive
|
||||
$wechatAccountRows = Db::table('s2_wechat_account')
|
||||
->whereIn('wechatId', $wechatIds)
|
||||
->field('wechatId, wechatAlive')
|
||||
->select();
|
||||
foreach ($friendRows as $row) {
|
||||
$wechatId = is_array($row) ? ($row['ownerWechatId'] ?? '') : ($row->ownerWechatId ?? '');
|
||||
if ($wechatId) {
|
||||
$metrics['totalFriend'][$wechatId] = (int)(is_array($row) ? ($row['total'] ?? 0) : ($row->total ?? 0));
|
||||
|
||||
foreach ($wechatAccountRows as $row) {
|
||||
$wechatId = $row['wechatId'] ?? '';
|
||||
if (!empty($wechatId)) {
|
||||
$metrics['wechatStatus'][$wechatId] = (int)($row['wechatAlive'] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 设备状态与备注
|
||||
// 优化4:设备状态与备注 - 使用INNER JOIN和索引
|
||||
$loginRows = Db::name('device_wechat_login')
|
||||
->alias('l')
|
||||
->leftJoin('device d', 'd.id = l.deviceId')
|
||||
->field('l.wechatId,l.alive,d.memo')
|
||||
->join('device d', 'd.id = l.deviceId', 'LEFT')
|
||||
->field('l.wechatId, l.alive, d.memo')
|
||||
->where('l.companyId', $companyId)
|
||||
->whereIn('l.wechatId', $wechatIds)
|
||||
->order('l.id', 'desc')
|
||||
->select();
|
||||
|
||||
// 使用临时数组避免重复处理
|
||||
$processedWechatIds = [];
|
||||
foreach ($loginRows as $row) {
|
||||
$wechatId = is_array($row) ? ($row['wechatId'] ?? '') : ($row->wechatId ?? '');
|
||||
if (empty($wechatId) || isset($metrics['wechatStatus'][$wechatId])) {
|
||||
continue;
|
||||
$wechatId = $row['wechatId'] ?? '';
|
||||
// 只处理每个wechatId的第一条记录(最新的)
|
||||
if (!empty($wechatId) && !in_array($wechatId, $processedWechatIds)) {
|
||||
// 如果s2_wechat_account表中没有wechatAlive,则使用device_wechat_login的alive作为备用
|
||||
if (!isset($metrics['wechatStatus'][$wechatId])) {
|
||||
$metrics['wechatStatus'][$wechatId] = (int)($row['alive'] ?? 0);
|
||||
}
|
||||
$metrics['deviceMemo'][$wechatId] = $row['memo'] ?? '';
|
||||
$processedWechatIds[] = $wechatId;
|
||||
}
|
||||
$metrics['wechatStatus'][$wechatId] = (int)(is_array($row) ? ($row['alive'] ?? 0) : ($row->alive ?? 0));
|
||||
$metrics['deviceMemo'][$wechatId] = is_array($row) ? ($row['memo'] ?? '') : ($row->memo ?? '');
|
||||
}
|
||||
|
||||
// 活跃时间
|
||||
$accountMap = Db::table('s2_wechat_account')
|
||||
->whereIn('wechatId', $wechatIds)
|
||||
->column('id', 'wechatId');
|
||||
if (!empty($accountMap)) {
|
||||
$accountRows = Db::table('s2_wechat_message')
|
||||
->whereIn('wechatAccountId', array_values($accountMap))
|
||||
->field('wechatAccountId, MAX(wechatTime) as lastTime')
|
||||
->group('wechatAccountId')
|
||||
->select();
|
||||
$accountLastTime = [];
|
||||
foreach ($accountRows as $row) {
|
||||
$accountId = is_array($row) ? ($row['wechatAccountId'] ?? 0) : ($row->wechatAccountId ?? 0);
|
||||
if ($accountId) {
|
||||
$accountLastTime[$accountId] = (int)(is_array($row) ? ($row['lastTime'] ?? 0) : ($row->lastTime ?? 0));
|
||||
}
|
||||
}
|
||||
foreach ($accountMap as $wechatId => $accountId) {
|
||||
if (isset($accountLastTime[$accountId]) && $accountLastTime[$accountId] > 0) {
|
||||
$metrics['activeTime'][$wechatId] = date('Y-m-d H:i:s', $accountLastTime[$accountId]);
|
||||
}
|
||||
// 优化5:活跃时间 - 使用JOIN减少查询次数
|
||||
$activeTimeResults = Db::query("
|
||||
SELECT
|
||||
a.wechatId,
|
||||
MAX(m.wechatTime) as lastTime
|
||||
FROM
|
||||
s2_wechat_account a
|
||||
LEFT JOIN
|
||||
s2_wechat_message m ON a.id = m.wechatAccountId
|
||||
WHERE
|
||||
a.wechatId IN ('" . implode("','", $wechatIds) . "')
|
||||
GROUP BY
|
||||
a.wechatId
|
||||
");
|
||||
|
||||
foreach ($activeTimeResults as $row) {
|
||||
$wechatId = $row['wechatId'] ?? '';
|
||||
$lastTime = (int)($row['lastTime'] ?? 0);
|
||||
if (!empty($wechatId) && $lastTime > 0) {
|
||||
$metrics['activeTime'][$wechatId] = date('Y-m-d H:i:s', $lastTime);
|
||||
} else {
|
||||
$metrics['activeTime'][$wechatId] = '-';
|
||||
}
|
||||
}
|
||||
|
||||
// 确保所有wechatId都有wechatStatus值(默认0)
|
||||
foreach ($wechatIds as $wechatId) {
|
||||
if (!isset($metrics['wechatStatus'][$wechatId])) {
|
||||
$metrics['wechatStatus'][$wechatId] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 存入缓存,有效期5分钟
|
||||
cache($cacheKey, $metrics, 300);
|
||||
|
||||
return $metrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取在线微信账号列表
|
||||
* 优化:添加缓存,优化分页逻辑
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
try {
|
||||
// 获取分页参数
|
||||
$page = $this->request->param('page/d', 1);
|
||||
$limit = $this->request->param('limit/d', 10);
|
||||
$keyword = $this->request->param('keyword');
|
||||
$wechatStatus = $this->request->param('wechatStatus');
|
||||
|
||||
// 创建缓存键(基于用户、分页、搜索条件和在线状态筛选)
|
||||
$cacheKey = 'wechat_list_' . $this->getUserInfo('id') . '_' . $page . '_' . $limit . '_' . md5($keyword ?? '') . '_' . ($wechatStatus ?? 'all');
|
||||
|
||||
// 尝试从缓存获取数据(缓存2分钟)
|
||||
$cachedData = cache($cacheKey);
|
||||
if ($cachedData) {
|
||||
return ResponseHelper::success($cachedData);
|
||||
}
|
||||
|
||||
// 如果没有缓存,执行查询
|
||||
$result = $this->getOnlineWechatList(
|
||||
$this->makeWhere()
|
||||
);
|
||||
|
||||
$responseData = [
|
||||
'list' => $this->makeResultedSet($result),
|
||||
'total' => $result->total(),
|
||||
];
|
||||
|
||||
// 存入缓存,有效期2分钟
|
||||
cache($cacheKey, $responseData, 120);
|
||||
|
||||
return ResponseHelper::success(
|
||||
[
|
||||
'list' => $this->makeResultedSet($result),
|
||||
'total' => $result->total(),
|
||||
]
|
||||
);
|
||||
return ResponseHelper::success($responseData);
|
||||
} catch (\Exception $e) {
|
||||
return ResponseHelper::error($e->getMessage(), $e->getCode());
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ class PostTransferFriends extends BaseController
|
||||
|
||||
$taskId = Db::name('customer_acquisition_task')->insertGetId([
|
||||
'name' => '迁移好友('. $wechat['nickname'] .')',
|
||||
'sceneId' => 1,
|
||||
'sceneId' => 10,
|
||||
'sceneConf' => json_encode($sceneConf),
|
||||
'reqConf' => json_encode($reqConf),
|
||||
'tagConf' => json_encode([]),
|
||||
|
||||
@@ -1,71 +1,120 @@
|
||||
{
|
||||
"name": "topthink/think",
|
||||
"description": "the new thinkphp framework",
|
||||
"type": "project",
|
||||
"keywords": [
|
||||
"framework",
|
||||
"thinkphp",
|
||||
"ORM"
|
||||
],
|
||||
"homepage": "http://thinkphp.cn/",
|
||||
"license": "Apache-2.0",
|
||||
"authors": [
|
||||
{
|
||||
"name": "liu21st",
|
||||
"email": "liu21st@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "yunwuxin",
|
||||
"email": "448901948@qq.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.6.0",
|
||||
"topthink/framework": "5.1.41",
|
||||
"topthink/think-installer": "~1.0",
|
||||
"topthink/think-captcha": "^2.0",
|
||||
"topthink/think-helper": "^3.0",
|
||||
"topthink/think-image": "^1.0",
|
||||
"topthink/think-queue": "^2.0",
|
||||
"topthink/think-worker": "^2.0",
|
||||
"textalk/websocket": "^1.2",
|
||||
"aliyuncs/oss-sdk-php": "^2.3",
|
||||
"monolog/monolog": "^1.24",
|
||||
"guzzlehttp/guzzle": "^6.3",
|
||||
"overtrue/wechat": "~4.0",
|
||||
"endroid/qr-code": "^3.5",
|
||||
"phpoffice/phpspreadsheet": "^1.8",
|
||||
"workerman/workerman": "^3.5",
|
||||
"workerman/gateway-worker": "^3.0",
|
||||
"hashids/hashids": "^2.0",
|
||||
"khanamiryan/qrcode-detector-decoder": "^1.0",
|
||||
"lizhichao/word": "^2.0",
|
||||
"adbario/php-dot-notation": "^2.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/var-dumper": "^3.4",
|
||||
"topthink/think-migration": "^2.0"
|
||||
"name": "topthink/think",
|
||||
"description": "the new thinkphp framework",
|
||||
"type": "project",
|
||||
"keywords": [
|
||||
"framework",
|
||||
"thinkphp",
|
||||
"ORM"
|
||||
],
|
||||
"homepage": "http://thinkphp.cn/",
|
||||
"license": "Apache-2.0",
|
||||
"authors": [
|
||||
{
|
||||
"name": "liu21st",
|
||||
"email": "liu21st@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "yunwuxin",
|
||||
"email": "448901948@qq.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.6.0",
|
||||
"topthink/framework": "5.1.41",
|
||||
"topthink/think-installer": "2.*",
|
||||
"topthink/think-captcha": "^2.0",
|
||||
"topthink/think-helper": "^3.0",
|
||||
"topthink/think-image": "^1.0",
|
||||
"topthink/think-queue": "^2.0",
|
||||
"topthink/think-worker": "^2.0",
|
||||
"textalk/websocket": "^1.5",
|
||||
"aliyuncs/oss-sdk-php": "^2.6",
|
||||
"monolog/monolog": "^1.27",
|
||||
"guzzlehttp/guzzle": "^6.5",
|
||||
"overtrue/wechat": "~4.6",
|
||||
"endroid/qr-code": "^3.9",
|
||||
"phpoffice/phpspreadsheet": "^1.29",
|
||||
"workerman/workerman": "^3.5",
|
||||
"workerman/gateway-worker": "^3.0",
|
||||
"hashids/hashids": "^2.0",
|
||||
"khanamiryan/qrcode-detector-decoder": "^1.0",
|
||||
"lizhichao/word": "^2.0",
|
||||
"adbario/php-dot-notation": "^2.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/var-dumper": "^3.4|^4.4",
|
||||
"topthink/think-migration": "^2.0",
|
||||
"phpunit/phpunit": "^5.0|^6.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"app\\": "application",
|
||||
"Eison\\": "extend/Eison"
|
||||
},
|
||||
"files": [
|
||||
"application/common.php"
|
||||
],
|
||||
"classmap": []
|
||||
},
|
||||
"extra": {
|
||||
"think-path": "thinkphp"
|
||||
},
|
||||
"config": {
|
||||
"preferred-install": "dist",
|
||||
"allow-plugins": {
|
||||
"topthink/think-installer": true,
|
||||
"easywechat-composer/easywechat-composer": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
"psr-4": {
|
||||
"app\\": "application",
|
||||
"Eison\\": "extend/Eison"
|
||||
},
|
||||
"files": [
|
||||
"application/common.php"
|
||||
],
|
||||
"homepage": "http://thinkphp.cn/",
|
||||
"license": "Apache-2.0",
|
||||
"authors": [
|
||||
{
|
||||
"name": "liu21st",
|
||||
"email": "liu21st@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "yunwuxin",
|
||||
"email": "448901948@qq.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.6.0",
|
||||
"topthink/framework": "5.1.41",
|
||||
"topthink/think-installer": "~1.0",
|
||||
"topthink/think-captcha": "^2.0",
|
||||
"topthink/think-helper": "^3.0",
|
||||
"topthink/think-image": "^1.0",
|
||||
"topthink/think-queue": "^2.0",
|
||||
"topthink/think-worker": "^2.0",
|
||||
"textalk/websocket": "^1.2",
|
||||
"aliyuncs/oss-sdk-php": "^2.3",
|
||||
"monolog/monolog": "^1.24",
|
||||
"guzzlehttp/guzzle": "^6.3",
|
||||
"overtrue/wechat": "~4.0",
|
||||
"endroid/qr-code": "^3.5",
|
||||
"phpoffice/phpspreadsheet": "^1.8",
|
||||
"workerman/workerman": "^3.5",
|
||||
"workerman/gateway-worker": "^3.0",
|
||||
"hashids/hashids": "^2.0",
|
||||
"khanamiryan/qrcode-detector-decoder": "^1.0",
|
||||
"lizhichao/word": "^2.0",
|
||||
"adbario/php-dot-notation": "^2.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/var-dumper": "^3.4",
|
||||
"topthink/think-migration": "^2.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"app\\": "application",
|
||||
"Eison\\": "extend/Eison"
|
||||
},
|
||||
"files": [
|
||||
"application/common.php"
|
||||
],
|
||||
"classmap": []
|
||||
},
|
||||
"extra": {
|
||||
"think-path": "thinkphp"
|
||||
},
|
||||
"config": {
|
||||
"preferred-install": "dist",
|
||||
"allow-plugins": {
|
||||
"topthink/think-installer": true,
|
||||
"easywechat-composer/easywechat-composer": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,8 @@
|
||||
# 消息提醒
|
||||
*/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
|
||||
|
||||
# 客服评分
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -107,4 +109,10 @@
|
||||
|
||||
```bash
|
||||
crontab -l
|
||||
```
|
||||
|
||||
```bash
|
||||
- 本地: php think worker:server
|
||||
- 线上: php think worker:server -d (自带守护进程,无需搭配Supervisor 之类的工具)
|
||||
- php think worker:server stop php think worker:server status
|
||||
```
|
||||
@@ -18,6 +18,7 @@ use think\facade\Config;
|
||||
use think\facade\Log;
|
||||
use app\api\controller\FriendTaskController;
|
||||
use app\common\service\AuthService;
|
||||
use app\common\service\WechatAccountHealthScoreService;
|
||||
use app\api\controller\WebSocketController;
|
||||
use Workerman\Lib\Timer;
|
||||
|
||||
@@ -180,13 +181,11 @@ class Adapter implements WeChatServiceInterface
|
||||
->select();
|
||||
$taskData = array_merge($taskData, $tasks);
|
||||
}
|
||||
|
||||
if ($taskData) {
|
||||
|
||||
foreach ($taskData as $task) {
|
||||
$task_id = $task['task_id'];
|
||||
$task_info = $this->getCustomerAcquisitionTask($task_id);
|
||||
|
||||
if (empty($task_info['status']) || empty($task_info['reqConf']) || empty($task_info['reqConf']['device'])) {
|
||||
continue;
|
||||
}
|
||||
@@ -213,9 +212,86 @@ class Adapter implements WeChatServiceInterface
|
||||
continue;
|
||||
}
|
||||
|
||||
// 判断24h内加的好友数量,friend_task 先固定10个人 getLast24hAddedFriendsCount
|
||||
// 根据健康分判断24h内加的好友数量限制
|
||||
$healthScoreService = new WechatAccountHealthScoreService();
|
||||
$healthScoreInfo = $healthScoreService->getHealthScore($accountId);
|
||||
|
||||
// 如果健康分记录不存在,先计算一次
|
||||
if (empty($healthScoreInfo)) {
|
||||
try {
|
||||
$healthScoreService->calculateAndUpdate($accountId);
|
||||
$healthScoreInfo = $healthScoreService->getHealthScore($accountId);
|
||||
} catch (\Exception $e) {
|
||||
Log::error("计算健康分失败 (accountId: {$accountId}): " . $e->getMessage());
|
||||
// 如果计算失败,使用默认值5作为兜底
|
||||
$maxAddFriendPerDay = 5;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取每日最大加人次数(基于健康分)
|
||||
$maxAddFriendPerDay = $healthScoreInfo['maxAddFriendPerDay'] ?? 5;
|
||||
|
||||
// 如果健康分为0或很低,不允许添加好友
|
||||
if ($maxAddFriendPerDay <= 0) {
|
||||
Log::info("账号健康分过低,不允许添加好友 (accountId: {$accountId}, wechatId: {$wechatId}, healthScore: " . ($healthScoreInfo['healthScore'] ?? 0) . ")");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查频繁暂停限制:首次频繁或再次频繁,暂停24小时
|
||||
$lastFrequentTime = $healthScoreInfo['lastFrequentTime'] ?? null;
|
||||
$frequentCount = $healthScoreInfo['frequentCount'] ?? 0;
|
||||
if (!empty($lastFrequentTime) && $frequentCount > 0) {
|
||||
$frequentPauseHours = 24; // 频繁暂停24小时
|
||||
$frequentPauseTime = $lastFrequentTime + ($frequentPauseHours * 3600);
|
||||
$currentTime = time();
|
||||
|
||||
if ($currentTime < $frequentPauseTime) {
|
||||
$remainingHours = ceil(($frequentPauseTime - $currentTime) / 3600);
|
||||
Log::info("账号频繁,暂停添加好友 (accountId: {$accountId}, wechatId: {$wechatId}, frequentCount: {$frequentCount}, 剩余暂停时间: {$remainingHours}小时)");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查封号暂停限制:封号暂停72小时
|
||||
$isBanned = $healthScoreInfo['isBanned'] ?? 0;
|
||||
if ($isBanned == 1) {
|
||||
// 查询封号时间(从s2_wechat_message表查询最近一次封号消息)
|
||||
$banMessage = Db::table('s2_wechat_message')
|
||||
->where('wechatAccountId', $accountId)
|
||||
->where('msgType', 10000)
|
||||
->where('content', 'like', '%你的账号被限制%')
|
||||
->where('isDeleted', 0)
|
||||
->order('createTime', 'desc')
|
||||
->find();
|
||||
|
||||
if (!empty($banMessage)) {
|
||||
$banTime = $banMessage['createTime'] ?? 0;
|
||||
$banPauseHours = 72; // 封号暂停72小时
|
||||
$banPauseTime = $banTime + ($banPauseHours * 3600);
|
||||
$currentTime = time();
|
||||
|
||||
if ($currentTime < $banPauseTime) {
|
||||
$remainingHours = ceil(($banPauseTime - $currentTime) / 3600);
|
||||
Log::info("账号封号,暂停添加好友 (accountId: {$accountId}, wechatId: {$wechatId}, 剩余暂停时间: {$remainingHours}小时)");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 判断今天添加的好友数量,使用健康分计算的每日最大加人次数
|
||||
// 优先使用今天添加的好友数量(更符合"每日"限制)
|
||||
$todayAddedFriendsCount = $this->getTodayAddedFriendsCount($wechatId);
|
||||
if ($todayAddedFriendsCount >= $maxAddFriendPerDay) {
|
||||
Log::info("今天添加好友数量已达上限 (accountId: {$accountId}, wechatId: {$wechatId}, count: {$todayAddedFriendsCount}, max: {$maxAddFriendPerDay}, healthScore: " . ($healthScoreInfo['healthScore'] ?? 0) . ")");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果今天添加数量未达上限,再检查24小时内的数量(作为额外保护)
|
||||
$last24hAddedFriendsCount = $this->getLast24hAddedFriendsCount($wechatId);
|
||||
if ($last24hAddedFriendsCount >= 20) {
|
||||
// 24小时内的限制可以稍微宽松一些,设置为每日限制的1.2倍(防止跨天累积)
|
||||
$max24hLimit = (int)ceil($maxAddFriendPerDay * 1.2);
|
||||
if ($last24hAddedFriendsCount >= $max24hLimit) {
|
||||
Log::info("24小时内添加好友数量已达上限 (accountId: {$accountId}, wechatId: {$wechatId}, count: {$last24hAddedFriendsCount}, max24h: {$max24hLimit}, maxDaily: {$maxAddFriendPerDay})");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -231,7 +307,7 @@ class Adapter implements WeChatServiceInterface
|
||||
$conf = array_merge($task_info['reqConf'], ['task_name' => $task_info['name'], 'tags' => $tags]);
|
||||
|
||||
|
||||
$this->createFriendAddTask($accountId, $task['phone'], $conf);
|
||||
$this->createFriendAddTask($accountId, $task['phone'], $conf, $task['remark']);
|
||||
$friendAddTaskCreated = true;
|
||||
$task['processed_wechat_ids'] = $task['processed_wechat_ids'] . ',' . $wechatId; // 处理失败任务用,用于过滤已处理的微信号
|
||||
break;
|
||||
@@ -828,6 +904,7 @@ class Adapter implements WeChatServiceInterface
|
||||
if (empty($deviceIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$records = Db::table('s2_wechat_account')
|
||||
->where('deviceAlive', 1)
|
||||
->where('wechatAlive', 1)
|
||||
@@ -874,28 +951,29 @@ class Adapter implements WeChatServiceInterface
|
||||
}
|
||||
|
||||
// 创建添加好友任务/执行添加
|
||||
public function createFriendAddTask(int $wechatAccountId, string $phone, array $conf)
|
||||
public function createFriendAddTask(int $wechatAccountId, string $phone, array $conf, $remark = '')
|
||||
{
|
||||
if (empty($wechatAccountId) || empty($phone) || empty($conf)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($conf['remarkType']) {
|
||||
case 'phone':
|
||||
$remark = $phone . '-' . $conf['task_name'];
|
||||
break;
|
||||
case 'nickname':
|
||||
$remark = '';
|
||||
break;
|
||||
case 'source':
|
||||
$remark = $conf['task_name'];
|
||||
break;
|
||||
default:
|
||||
$remark = '';
|
||||
break;
|
||||
if (empty($remark)){
|
||||
switch ($conf['remarkType']) {
|
||||
case 'phone':
|
||||
$remark = $phone . '-' . $conf['task_name'];
|
||||
break;
|
||||
case 'nickname':
|
||||
$remark = '';
|
||||
break;
|
||||
case 'source':
|
||||
$remark = $conf['task_name'];
|
||||
break;
|
||||
default:
|
||||
$remark = '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$tags = [];
|
||||
if (!empty($conf['tags'])) {
|
||||
if (is_array($conf['tags'])) {
|
||||
|
||||
2392
Server/sql.sql
Normal file
2392
Server/sql.sql
Normal file
File diff suppressed because it is too large
Load Diff
58
Server/微信健康分规则v2.md
Normal file
58
Server/微信健康分规则v2.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# 微信健康分规则 v2
|
||||
|
||||
## 一、定义
|
||||
|
||||
当客户收到手机设备后,登录了微信号,我们将对其微信号进行健康分的评估。
|
||||
|
||||
**健康分 = 基础分 + 动态分**
|
||||
|
||||
健康分只与系统中的"每日自动添加好友次数"这个功能相关联。\
|
||||
通过健康分体系来定义一个微信号每日**最佳、最稳定的添加次数**。\
|
||||
后期还可将健康分作为标签属性,用于快速筛选微信号。
|
||||
|
||||
**公式:每日最大加人次数 = 健康分 × 0.2**
|
||||
|
||||
## 二、基础分
|
||||
|
||||
基础分为 **60--100 分**。
|
||||
|
||||
由 `60 + 40(基础加成分)` 四个维度参数组成,每个参数具有不同权重。
|
||||
|
||||
### 基础分组成
|
||||
|
||||
类型 权重 分数
|
||||
------------ ------ ------
|
||||
基础信息 0.2 10
|
||||
好友数量 0.3 30
|
||||
默认基础分 --- 60
|
||||
|
||||
### 1. 基础信息(权重 0.2,满分 10)
|
||||
|
||||
类型 权重 分数
|
||||
-------------- ------ ------
|
||||
已修改微信号 1 10
|
||||
|
||||
### 2. 好友数量(权重 0.3,满分 30)
|
||||
|
||||
好友数量范围 权重 分数
|
||||
-------------- ------ ------
|
||||
0--50 0.1 3
|
||||
51--500 0.2 6
|
||||
501--3000 0.3 8
|
||||
3001 以上 0.4 12
|
||||
|
||||
## 三、动态分规则
|
||||
|
||||
### 扣分规则
|
||||
|
||||
场景 扣分 处罚
|
||||
---------- ------ --------------
|
||||
首次频繁 15 暂停 24 小时
|
||||
再次频繁 25 暂停 24 小时
|
||||
封号 60 暂停 72 小时
|
||||
|
||||
### 加分规则
|
||||
|
||||
场景 加分
|
||||
--------------------- ------
|
||||
连续 3 天不触发频繁 5/日
|
||||
Reference in New Issue
Block a user