diff --git a/Server/application/cunkebao/config/route.php b/Server/application/cunkebao/config/route.php index cf64cef7..e6f7ee03 100644 --- a/Server/application/cunkebao/config/route.php +++ b/Server/application/cunkebao/config/route.php @@ -110,6 +110,9 @@ Route::group('v1/', function () { Route::get('getJdSocialMedia', 'app\cunkebao\controller\WorkbenchController@getJdSocialMedia'); // 获取京东联盟导购媒体 Route::get('getJdPromotionSite', 'app\cunkebao\controller\WorkbenchController@getJdPromotionSite'); // 获取京东联盟广告位 Route::get('changeLink', 'app\cunkebao\controller\WorkbenchController@changeLink'); // 获取京东联盟广告位 + + Route::get('group-push-stats', 'app\cunkebao\controller\WorkbenchController@getGroupPushStats'); // 获取群发统计数据 + Route::get('group-push-history', 'app\cunkebao\controller\WorkbenchController@getGroupPushHistory'); // 获取推送历史记录列表 }); // 内容库相关 diff --git a/Server/application/cunkebao/controller/ContentLibraryController.php b/Server/application/cunkebao/controller/ContentLibraryController.php index 0ddac5b7..c527021c 100644 --- a/Server/application/cunkebao/controller/ContentLibraryController.php +++ b/Server/application/cunkebao/controller/ContentLibraryController.php @@ -111,10 +111,16 @@ class ContentLibraryController extends Controller $sourceType = $this->request->param('sourceType', ''); // 新增:来源类型,1=好友,2=群 $where = [ - ['userId', '=', $this->request->userInfo['id']], + ['companyId' , '=', $this->request->userInfo['companyId']], ['isDel', '=', 0] // 只查询未删除的记录 ]; + if(empty($this->request->userInfo['isAdmin'])){ + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + + // 添加名称模糊搜索 if ($keyword !== '') { $where[] = ['name', 'like', '%' . $keyword . '%']; @@ -307,11 +313,18 @@ class ContentLibraryController extends Controller return json(['code' => 400, 'msg' => '内容库名称不能为空']); } + + $where = [ + ['companyId' , '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0] // 只查询未删除的记录 + ]; + + if(empty($this->request->userInfo['isAdmin'])){ + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + // 查询内容库是否存在 - $library = ContentLibrary::where([ - ['id', '=', $param['id']], - ['userId', '=', $this->request->userInfo['id']] - ])->find(); + $library = ContentLibrary::where($where)->find(); if (!$library) { return json(['code' => 500, 'msg' => '内容库不存在']); @@ -766,16 +779,20 @@ class ContentLibraryController extends Controller $content = Request::param('content', ''); $companyId = $this->request->userInfo['companyId']; // 简单验证 - if (empty($id)) { + if (empty($id) && empty($content)) { return json(['code' => 400, 'msg' => '参数错误']); } - // 查询内容项目是否存在并检查权限 - $item = ContentItem::alias('ci') - ->join('content_library cl', 'ci.libraryId = cl.id') - ->where(['ci.id' => $id, 'ci.isDel' => 0, 'cl.isDel' => 0, 'cl.companyId' => $companyId]) - ->field('ci.*') - ->find(); + if(!empty($id)) { + // 查询内容项目是否存在并检查权限 + $item = ContentItem::alias('ci') + ->join('content_library cl', 'ci.libraryId = cl.id') + ->where(['ci.id' => $id, 'ci.isDel' => 0, 'cl.isDel' => 0, 'cl.companyId' => $companyId]) + ->field('ci.*') + ->find(); + }else{ + $item['content'] = $content; + } if (empty($item)) { return json(['code' => 500, 'msg' => '内容项目不存在或无权限操作']); diff --git a/Server/application/cunkebao/controller/WorkbenchController.php b/Server/application/cunkebao/controller/WorkbenchController.php index fde42fa7..66e939c9 100644 --- a/Server/application/cunkebao/controller/WorkbenchController.php +++ b/Server/application/cunkebao/controller/WorkbenchController.php @@ -49,6 +49,19 @@ class WorkbenchController extends Controller // 获取请求参数 $param = $this->request->post(); + // 根据业务默认值补全参数 + if ( + isset($param['type']) && + intval($param['type']) === self::TYPE_GROUP_PUSH + ) { + if (empty($param['startTime'])) { + $param['startTime'] = '09:00'; + } + if (empty($param['endTime'])) { + $param['endTime'] = '21:00'; + } + } + // 验证数据 $validate = new WorkbenchValidate; if (!$validate->scene('create')->check($param)) { @@ -106,35 +119,13 @@ class WorkbenchController extends Controller $config->save(); break; case self::TYPE_GROUP_PUSH: // 群消息推送 + $ownerWechatIds = $this->normalizeOwnerWechatIds($param['ownerWechatIds'] ?? []); + $groupPushData = $this->prepareGroupPushData($param, $ownerWechatIds); + $groupPushData['workbenchId'] = $workbench->id; + $groupPushData['createTime'] = time(); + $groupPushData['updateTime'] = time(); $config = new WorkbenchGroupPush; - $config->workbenchId = $workbench->id; - $config->pushType = !empty($param['pushType']) ? 1 : 0; // 推送方式:定时/立即 - $config->targetType = !empty($param['targetType']) ? intval($param['targetType']) : 1; // 推送目标类型:1=群推送,2=好友推送 - $config->startTime = $param['startTime']; - $config->endTime = $param['endTime']; - $config->maxPerDay = intval($param['maxPerDay']); // 每日推送数 - $config->pushOrder = $param['pushOrder']; // 推送顺序 - // 根据targetType存储不同的数据 - if ($config->targetType == 1) { - // 群推送 - $config->isLoop = !empty($param['isLoop']) ? 1 : 0; // 是否循环 - $config->groups = json_encode($param['wechatGroups'] ?? [], JSON_UNESCAPED_UNICODE); // 群组信息 - $config->friends = json_encode([], JSON_UNESCAPED_UNICODE); // 好友信息为空数组 - $config->devices = json_encode([], JSON_UNESCAPED_UNICODE); // 群推送不需要设备 - } else { - // 好友推送:isLoop必须为0,设备必填 - $config->isLoop = 0; // 好友推送时强制为0 - $config->friends = json_encode($param['wechatFriends'] ?? [], JSON_UNESCAPED_UNICODE); // 好友信息(可以为空数组) - $config->groups = json_encode([], JSON_UNESCAPED_UNICODE); // 群组信息为空数组 - $config->devices = json_encode($param['deviceGroups'] ?? [], JSON_UNESCAPED_UNICODE); // 设备信息(必填) - } - $config->status = !empty($param['status']) ? 1 : 0; // 是否启用 - $config->contentLibraries = json_encode($param['contentGroups'], JSON_UNESCAPED_UNICODE); // 内容库信息 - $config->socialMediaId = !empty($param['socialMediaId']) ? $param['socialMediaId'] : ''; - $config->promotionSiteId = !empty($param['promotionSiteId']) ? $param['promotionSiteId'] : ''; - $config->createTime = time(); - $config->updateTime = time(); - $config->save(); + $config->save($groupPushData); break; case self::TYPE_GROUP_CREATE: // 自动建群 $config = new WorkbenchGroupCreate; @@ -229,12 +220,12 @@ class WorkbenchController extends Controller $query->field('workbenchId,distributeType,maxPerDay,timeType,startTime,endTime,devices,pools,account'); }, 'groupPush' => function ($query) { - $query->field('workbenchId,pushType,targetType,startTime,endTime,maxPerDay,pushOrder,isLoop,status,groups,friends,devices,contentLibraries'); + $query->field('workbenchId,pushType,targetType,groupPushSubType,startTime,endTime,maxPerDay,pushOrder,isLoop,status,groups,friends,ownerWechatIds,trafficPools,contentLibraries,friendIntervalMin,friendIntervalMax,messageIntervalMin,messageIntervalMax,isRandomTemplate,postPushTags,announcementContent,enableAiRewrite,aiRewritePrompt'); }, - 'groupCreate' => function($query) { + 'groupCreate' => function ($query) { $query->field('workbenchId,devices,startTime,endTime,groupSizeMin,groupSizeMax,maxGroupsPerDay,groupNameTemplate,groupDescription,poolGroups,wechatGroups'); }, - 'importContact' => function($query) { + 'importContact' => function ($query) { $query->field('workbenchId,devices,pools,num,remarkType,remark,clearContact,startTime,endTime'); }, 'user' => function ($query) { @@ -296,33 +287,52 @@ class WorkbenchController extends Controller $item->config->contentGroupsOptions = []; } } - unset($item->momentsSync, $item->moments_sync,$item->config->contentLibraries); + unset($item->momentsSync, $item->moments_sync, $item->config->contentLibraries); break; case self::TYPE_GROUP_PUSH: if (!empty($item->groupPush)) { $item->config = $item->groupPush; $item->config->pushType = $item->config->pushType; $item->config->targetType = isset($item->config->targetType) ? intval($item->config->targetType) : 1; // 默认1=群推送 + $item->config->groupPushSubType = isset($item->config->groupPushSubType) ? intval($item->config->groupPushSubType) : 1; // 默认1=群群发 $item->config->startTime = $item->config->startTime; $item->config->endTime = $item->config->endTime; $item->config->maxPerDay = $item->config->maxPerDay; $item->config->pushOrder = $item->config->pushOrder; $item->config->isLoop = $item->config->isLoop; $item->config->status = $item->config->status; + $item->config->ownerWechatIds = json_decode($item->config->ownerWechatIds ?? '[]', true) ?: []; // 根据targetType解析不同的数据 if ($item->config->targetType == 1) { // 群推送 $item->config->wechatGroups = json_decode($item->config->groups, true) ?: []; $item->config->wechatFriends = []; - $item->config->deviceGroups = []; + // 群推送不需要devices字段 + // 群公告相关字段 + if ($item->config->groupPushSubType == 2) { + $item->config->announcementContent = isset($item->config->announcementContent) ? $item->config->announcementContent : ''; + $item->config->enableAiRewrite = isset($item->config->enableAiRewrite) ? intval($item->config->enableAiRewrite) : 0; + $item->config->aiRewritePrompt = isset($item->config->aiRewritePrompt) ? $item->config->aiRewritePrompt : ''; + } + $item->config->trafficPools = []; } else { // 好友推送 $item->config->wechatFriends = json_decode($item->config->friends, true) ?: []; $item->config->wechatGroups = []; - $item->config->deviceGroups = json_decode($item->config->devices ?? '[]', true) ?: []; + $item->config->trafficPools = json_decode($item->config->trafficPools ?? '[]', true) ?: []; } $item->config->contentLibraries = json_decode($item->config->contentLibraries, true); + $item->config->postPushTags = json_decode($item->config->postPushTags ?? '[]', true) ?: []; $item->config->lastPushTime = ''; + if (!empty($item->config->ownerWechatIds)) { + $ownerWechatOptions = Db::name('wechat_account') + ->whereIn('id', $item->config->ownerWechatIds) + ->field('id,wechatId,nickName,avatar,alias') + ->select(); + $item->config->ownerWechatOptions = $ownerWechatOptions; + } else { + $item->config->ownerWechatOptions = []; + } } unset($item->groupPush, $item->group_push); break; @@ -438,12 +448,12 @@ class WorkbenchController extends Controller $query->field('workbenchId,distributeType,maxPerDay,timeType,startTime,endTime,devices,pools,account'); }, 'groupPush' => function ($query) { - $query->field('workbenchId,pushType,targetType,startTime,endTime,maxPerDay,pushOrder,isLoop,status,groups,friends,devices,contentLibraries'); + $query->field('workbenchId,pushType,targetType,groupPushSubType,startTime,endTime,maxPerDay,pushOrder,isLoop,status,groups,friends,ownerWechatIds,trafficPools,contentLibraries,friendIntervalMin,friendIntervalMax,messageIntervalMin,messageIntervalMax,isRandomTemplate,postPushTags,announcementContent,enableAiRewrite,aiRewritePrompt'); }, - 'groupCreate' => function($query) { - $query->field('workbenchId,devices,startTime,endTime,groupSizeMin,groupSizeMax,maxGroupsPerDay,groupNameTemplate,groupDescription,poolGroups,wechatGroups'); - }, - 'importContact' => function($query) { + 'groupCreate' => function ($query) { + $query->field('workbenchId,devices,startTime,endTime,groupSizeMin,groupSizeMax,maxGroupsPerDay,groupNameTemplate,groupDescription,poolGroups,wechatGroups'); + }, + 'importContact' => function ($query) { $query->field('workbenchId,devices,pools,num,remarkType,remark,clearContact,startTime,endTime'); }, ]; @@ -510,19 +520,29 @@ class WorkbenchController extends Controller if (!empty($workbench->groupPush)) { $workbench->config = $workbench->groupPush; $workbench->config->targetType = isset($workbench->config->targetType) ? intval($workbench->config->targetType) : 1; // 默认1=群推送 + $workbench->config->groupPushSubType = isset($workbench->config->groupPushSubType) ? intval($workbench->config->groupPushSubType) : 1; // 默认1=群群发 + $workbench->config->ownerWechatIds = json_decode($workbench->config->ownerWechatIds ?? '[]', true) ?: []; // 根据targetType解析不同的数据 if ($workbench->config->targetType == 1) { // 群推送 $workbench->config->wechatGroups = json_decode($workbench->config->groups, true) ?: []; $workbench->config->wechatFriends = []; - $workbench->config->deviceGroups = []; + $workbench->config->trafficPools = []; + // 群推送不需要devices字段 + // 群公告相关字段 + if ($workbench->config->groupPushSubType == 2) { + $workbench->config->announcementContent = isset($workbench->config->announcementContent) ? $workbench->config->announcementContent : ''; + $workbench->config->enableAiRewrite = isset($workbench->config->enableAiRewrite) ? intval($workbench->config->enableAiRewrite) : 0; + $workbench->config->aiRewritePrompt = isset($workbench->config->aiRewritePrompt) ? $workbench->config->aiRewritePrompt : ''; + } } else { // 好友推送 $workbench->config->wechatFriends = json_decode($workbench->config->friends, true) ?: []; $workbench->config->wechatGroups = []; - $workbench->config->deviceGroups = json_decode($workbench->config->devices ?? '[]', true) ?: []; + $workbench->config->trafficPools = json_decode($workbench->config->trafficPools ?? '[]', true) ?: []; } $workbench->config->contentLibraries = json_decode($workbench->config->contentLibraries, true); + $workbench->config->postPushTags = json_decode($workbench->config->postPushTags ?? '[]', true) ?: []; unset($workbench->groupPush, $workbench->group_push); } break; @@ -629,7 +649,7 @@ class WorkbenchController extends Controller ->select(); foreach ($deviceList as &$device) { - $curstomer = WechatCustomerModel::field('friendShip')->where(['wechatId' => $device['wechatId']])->find(); + $curstomer = WechatCustomerModel::field('friendShip')->where(['wechatId' => $device['wechatId']])->find(); $device['totalFriend'] = $curstomer->friendShip->totalFriend ?? 0; } unset($device); @@ -641,7 +661,7 @@ class WorkbenchController extends Controller // 获取群(当targetType=1时) - if (!empty($workbench->config->wechatGroups) && isset($workbench->config->targetType) && $workbench->config->targetType == 1){ + if (!empty($workbench->config->wechatGroups) && isset($workbench->config->targetType) && $workbench->config->targetType == 1) { $groupList = Db::name('wechat_group')->alias('wg') ->join('wechat_account wa', 'wa.wechatId = wg.ownerWechatId') ->where('wg.id', 'in', $workbench->config->wechatGroups) @@ -649,12 +669,12 @@ class WorkbenchController extends Controller ->field('wg.id,wg.name as groupName,wg.ownerWechatId,wa.nickName,wa.avatar,wa.alias,wg.avatar as groupAvatar') ->select(); $workbench->config->wechatGroupsOptions = $groupList; - }else{ + } else { $workbench->config->wechatGroupsOptions = []; } // 获取好友(当targetType=2时) - if (!empty($workbench->config->wechatFriends) && isset($workbench->config->targetType) && $workbench->config->targetType == 2){ + if (!empty($workbench->config->wechatFriends) && isset($workbench->config->targetType) && $workbench->config->targetType == 2) { $friendList = Db::table('s2_wechat_friend')->alias('wf') ->join('s2_wechat_account wa', 'wa.id = wf.wechatAccountId', 'left') ->where('wf.id', 'in', $workbench->config->wechatFriends) @@ -662,10 +682,26 @@ class WorkbenchController extends Controller ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar') ->select(); $workbench->config->wechatFriendsOptions = $friendList; - }else{ + } else { $workbench->config->wechatFriendsOptions = []; } + // 获取流量池(当targetType=2时) + if (!empty($workbench->config->trafficPools) && isset($workbench->config->targetType) && $workbench->config->targetType == 2) { + $poolList = Db::name('traffic_source_package')->alias('tsp') + ->leftJoin('traffic_source_package_item tspi', 'tspi.packageId = tsp.id and tspi.isDel = 0') + ->whereIn('tsp.id', $workbench->config->trafficPools) + ->where('tsp.isDel', 0) + ->whereIn('tsp.companyId', [$this->request->userInfo['companyId'], 0]) + ->field('tsp.id,tsp.name,tsp.description,tsp.pic,COUNT(tspi.id) as itemCount') + ->group('tsp.id') + ->order('tsp.id', 'desc') + ->select(); + $workbench->config->trafficPoolsOptions = $poolList; + } else { + $workbench->config->trafficPoolsOptions = []; + } + // 获取内容库名称 if (!empty($workbench->config->contentGroups)) { $libraryNames = ContentLibrary::where('id', 'in', $workbench->config->contentGroups)->select(); @@ -675,7 +711,7 @@ class WorkbenchController extends Controller } //账号 - if (!empty($workbench->config->accountGroups)){ + if (!empty($workbench->config->accountGroups)) { $account = Db::table('s2_company_account')->alias('a') ->where(['a.departmentId' => $this->request->userInfo['companyId'], 'a.status' => 0]) ->whereIn('a.id', $workbench->config->accountGroups) @@ -683,24 +719,33 @@ class WorkbenchController extends Controller ->whereNotLike('a.userName', '%_delete%') ->field('a.id,a.userName,a.realName,a.nickname,a.memo') ->select(); - $workbench->config->accountGroupsOptions = $account; - }else{ + $workbench->config->accountGroupsOptions = $account; + } else { $workbench->config->accountGroupsOptions = []; } - if (!empty($workbench->config->poolGroups)){ + if (!empty($workbench->config->poolGroups)) { $poolGroupsOptions = Db::name('traffic_source_package')->alias('tsp') - ->join('traffic_source_package_item tspi','tspi.packageId=tsp.id','left') - ->whereIn('tsp.companyId', [$this->request->userInfo['companyId'],0]) + ->join('traffic_source_package_item tspi', 'tspi.packageId=tsp.id', 'left') + ->whereIn('tsp.companyId', [$this->request->userInfo['companyId'], 0]) ->whereIn('tsp.id', $workbench->config->poolGroups) ->field('tsp.id,tsp.name,tsp.description,tsp.createTime,count(tspi.id) as num') ->group('tsp.id') ->select(); $workbench->config->poolGroupsOptions = $poolGroupsOptions; - }else{ + } else { $workbench->config->poolGroupsOptions = []; } + if (!empty($workbench->config->ownerWechatIds)) { + $ownerWechatOptions = Db::name('wechat_account') + ->whereIn('id', $workbench->config->ownerWechatIds) + ->field('id,wechatId,nickName,avatar,alias') + ->select(); + $workbench->config->ownerWechatOptions = $ownerWechatOptions; + } else { + $workbench->config->ownerWechatOptions = []; + } return json(['code' => 200, 'msg' => '获取成功', 'data' => $workbench]); @@ -797,32 +842,10 @@ class WorkbenchController extends Controller case self::TYPE_GROUP_PUSH: $config = WorkbenchGroupPush::where('workbenchId', $param['id'])->find(); if ($config) { - $config->pushType = !empty($param['pushType']) ? 1 : 0; // 推送方式:定时/立即 - $config->targetType = !empty($param['targetType']) ? intval($param['targetType']) : 1; // 推送目标类型:1=群推送,2=好友推送 - $config->startTime = $param['startTime']; - $config->endTime = $param['endTime']; - $config->maxPerDay = intval($param['maxPerDay']); // 每日推送数 - $config->pushOrder = $param['pushOrder']; // 推送顺序 - // 根据targetType存储不同的数据 - if ($config->targetType == 1) { - // 群推送 - $config->isLoop = !empty($param['isLoop']) ? 1 : 0; // 是否循环 - $config->groups = json_encode($param['wechatGroups'] ?? [], JSON_UNESCAPED_UNICODE); // 群组信息 - $config->friends = json_encode([], JSON_UNESCAPED_UNICODE); // 好友信息为空数组 - $config->devices = json_encode([], JSON_UNESCAPED_UNICODE); // 群推送不需要设备 - } else { - // 好友推送:isLoop必须为0,设备必填 - $config->isLoop = 0; // 好友推送时强制为0 - $config->friends = json_encode($param['wechatFriends'] ?? [], JSON_UNESCAPED_UNICODE); // 好友信息(可以为空数组) - $config->groups = json_encode([], JSON_UNESCAPED_UNICODE); // 群组信息为空数组 - $config->devices = json_encode($param['deviceGroups'] ?? [], JSON_UNESCAPED_UNICODE); // 设备信息(必填) - } - $config->status = !empty($param['status']) ? 1 : 0; // 是否启用 - $config->contentLibraries = json_encode($param['contentGroups'], JSON_UNESCAPED_UNICODE); // 内容库信息 - $config->socialMediaId = !empty($param['socialMediaId']) ? $param['socialMediaId'] : ''; - $config->promotionSiteId = !empty($param['promotionSiteId']) ? $param['promotionSiteId'] : ''; - $config->updateTime = time(); - $config->save(); + $ownerWechatIds = $this->normalizeOwnerWechatIds($param['ownerWechatIds'] ?? null, $config); + $groupPushData = $this->prepareGroupPushData($param, $ownerWechatIds, $config); + $groupPushData['updateTime'] = time(); + $config->save($groupPushData); } break; @@ -859,7 +882,7 @@ class WorkbenchController extends Controller } break; case self::TYPE_IMPORT_CONTACT: //联系人导入 - $config = WorkbenchImportContact::where('workbenchId', $param['id'])->find();; + $config = WorkbenchImportContact::where('workbenchId', $param['id'])->find();; if ($config) { $config->devices = json_encode($param['deviceGroups']); $config->pools = json_encode($param['poolGroups']); @@ -1029,10 +1052,11 @@ class WorkbenchController extends Controller $newConfig->status = $config->status; $newConfig->groups = $config->groups; $newConfig->friends = $config->friends; - $newConfig->devices = $config->devices; $newConfig->contentLibraries = $config->contentLibraries; + $newConfig->trafficPools = property_exists($config, 'trafficPools') ? $config->trafficPools : json_encode([], JSON_UNESCAPED_UNICODE); $newConfig->socialMediaId = $config->socialMediaId; $newConfig->promotionSiteId = $config->promotionSiteId; + $newConfig->ownerWechatIds = $config->ownerWechatIds; $newConfig->createTime = time(); $newConfig->updateTime = time(); $newConfig->save(); @@ -1051,15 +1075,15 @@ class WorkbenchController extends Controller $newConfig->maxGroupsPerDay = $config->maxGroupsPerDay; $newConfig->groupNameTemplate = $config->groupNameTemplate; $newConfig->groupDescription = $config->groupDescription; - $newConfig->poolGroups = $config->poolGroups; - $newConfig->wechatGroups = $config->wechatGroups; + $newConfig->poolGroups = $config->poolGroups; + $newConfig->wechatGroups = $config->wechatGroups; $newConfig->createTime = time(); $newConfig->updateTime = time(); $newConfig->save(); } break; case self::TYPE_IMPORT_CONTACT: //联系人导入 - $config = WorkbenchImportContact::where('workbenchId',$id)->find(); + $config = WorkbenchImportContact::where('workbenchId', $id)->find(); if ($config) { $newConfig = new WorkbenchImportContact; $newConfig->workbenchId = $newWorkbench->id; @@ -1128,10 +1152,10 @@ class WorkbenchController extends Controller ->where(['id' => $item['wechatFriendId']]) ->field('nickName,avatar') ->find(); - if(!empty($friend)){ + if (!empty($friend)) { $item['friendName'] = $friend['nickName']; $item['friendAvatar'] = $friend['avatar']; - }else{ + } else { $item['friendName'] = ''; $item['friendAvatar'] = ''; } @@ -1142,10 +1166,10 @@ class WorkbenchController extends Controller ->where(['id' => $item['wechatAccountId']]) ->field('nickName,avatar') ->find(); - if(!empty($friend)){ + if (!empty($friend)) { $item['operatorName'] = $friend['nickName']; $item['operatorAvatar'] = $friend['avatar']; - }else{ + } else { $item['operatorName'] = ''; $item['operatorAvatar'] = ''; } @@ -1652,6 +1676,43 @@ class WorkbenchController extends Controller return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]); } + /** + * 获取流量池列表 + * @return \think\response\Json + */ + public function getTrafficPoolList() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + $companyId = $this->request->userInfo['companyId']; + + $baseQuery = Db::name('traffic_source_package')->alias('tsp') + ->where('tsp.isDel', 0) + ->whereIn('tsp.companyId', [$companyId, 0]); + + if (!empty($keyword)) { + $baseQuery->whereLike('tsp.name', '%' . $keyword . '%'); + } + + $total = (clone $baseQuery)->count(); + + $list = $baseQuery + ->leftJoin('traffic_source_package_item tspi', 'tspi.packageId = tsp.id and tspi.isDel = 0') + ->field('tsp.id,tsp.name,tsp.description,tsp.pic,tsp.companyId,COUNT(tspi.id) as itemCount,max(tspi.createTime) as latestImportTime') + ->group('tsp.id') + ->order('tsp.id', 'desc') + ->page($page, $limit) + ->select(); + + foreach ($list as &$item) { + $item['latestImportTime'] = !empty($item['latestImportTime']) ? date('Y-m-d H:i:s', $item['latestImportTime']) : ''; + } + unset($item); + + return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]); + } + public function getAccountList() { @@ -1707,7 +1768,7 @@ class WorkbenchController extends Controller //京东转链-京推推 - public function changeLink($content = '',$positionid = '') + public function changeLink($content = '', $positionid = '') { $unionId = Env::get('jd.unionId', ''); $jttAppId = Env::get('jd.jttAppId', ''); @@ -1718,18 +1779,17 @@ class WorkbenchController extends Controller $positionid = !empty($positionid) ? $positionid : $this->request->param('positionid', ''); - - if (empty($content)){ - return json_encode(['code' => 500, 'msg' => '转链的内容为空']) ; + if (empty($content)) { + return json_encode(['code' => 500, 'msg' => '转链的内容为空']); } - + // 验证是否包含链接 if (!$this->containsLink($content)) { - return json_encode(['code' => 500, 'msg' => '内容中未检测到有效链接']) ; + return json_encode(['code' => 500, 'msg' => '内容中未检测到有效链接']); } - if(empty($unionId) || empty($jttAppId) || empty($appKey) || empty($apiUrl)){ - return json_encode(['code' => 500, 'msg' => '参数缺失']) ; + if (empty($unionId) || empty($jttAppId) || empty($appKey) || empty($apiUrl)) { + return json_encode(['code' => 500, 'msg' => '参数缺失']); } $params = [ 'unionid' => $unionId, @@ -1743,21 +1803,20 @@ class WorkbenchController extends Controller $params['positionid'] = $positionid; } - $res = requestCurl($apiUrl,$params,'GET',[],'json'); - $res = json_decode($res,true); - if (empty($res)){ - return json_encode(['code' => 500, 'msg' => '未知错误']) ; + $res = requestCurl($apiUrl, $params, 'GET', [], 'json'); + $res = json_decode($res, true); + if (empty($res)) { + return json_encode(['code' => 500, 'msg' => '未知错误']); } $result = $res['result']; - if ($res['return'] == 0){ - return json_encode(['code' => 200,'data' => $result['chain_content'],'msg' => $result['msg']]) ; - }else{ - return json_encode(['code' => 500, 'msg' => $result['msg']]) ; + if ($res['return'] == 0) { + return json_encode(['code' => 200, 'data' => $result['chain_content'], 'msg' => $result['msg']]); + } else { + return json_encode(['code' => 500, 'msg' => $result['msg']]); } } - public function getTrafficList() { $companyId = $this->request->userInfo['companyId']; @@ -1769,24 +1828,24 @@ class WorkbenchController extends Controller return json(['code' => 400, 'msg' => '参数错误']); } - $workbench = Db::name('workbench')->where(['id' => $workbenchId,'isDel' => 0,'companyId' => $companyId,'type' => 5])->find(); + $workbench = Db::name('workbench')->where(['id' => $workbenchId, 'isDel' => 0, 'companyId' => $companyId, 'type' => 5])->find(); - if (empty($workbench)){ + if (empty($workbench)) { return json(['code' => 400, 'msg' => '该任务不存在或已删除']); } $query = Db::name('workbench_traffic_config_item')->alias('wtc') - ->join(['s2_wechat_friend' => 'wf'],'wtc.wechatFriendId = wf.id') - ->join('users u','wtc.wechatAccountId = u.s2_accountId','left') + ->join(['s2_wechat_friend' => 'wf'], 'wtc.wechatFriendId = wf.id') + ->join('users u', 'wtc.wechatAccountId = u.s2_accountId', 'left') ->field([ - 'wtc.id','wtc.isRecycle','wtc.isRecycle','wtc.createTime', - 'wf.wechatId','wf.alias','wf.nickname','wf.avatar','wf.gender','wf.phone', - 'u.account','u.username' + 'wtc.id', 'wtc.isRecycle', 'wtc.isRecycle', 'wtc.createTime', + 'wf.wechatId', 'wf.alias', 'wf.nickname', 'wf.avatar', 'wf.gender', 'wf.phone', + 'u.account', 'u.username' ]) ->where(['wtc.workbenchId' => $workbenchId]) ->order('wtc.id DESC'); - if (!empty($keyword)){ - $query->where('wf.wechatId|wf.alias|wf.nickname|wf.phone|u.account|u.username','like','%' . $keyword . '%'); + if (!empty($keyword)) { + $query->where('wf.wechatId|wf.alias|wf.nickname|wf.phone|u.account|u.username', 'like', '%' . $keyword . '%'); } $total = $query->count(); @@ -1798,7 +1857,6 @@ class WorkbenchController extends Controller unset($item); - $data = [ 'total' => $total, 'list' => $list, @@ -1808,6 +1866,220 @@ class WorkbenchController extends Controller } + /** + * 规范化客服微信ID列表 + * @param mixed $ownerWechatIds + * @param WorkbenchGroupPush|null $originalConfig + * @return array + * @throws \Exception + */ + private function normalizeOwnerWechatIds($ownerWechatIds, WorkbenchGroupPush $originalConfig = null): array + { + if ($ownerWechatIds === null) { + $existing = $originalConfig ? $this->decodeJsonArray($originalConfig->ownerWechatIds ?? []) : []; + if (empty($existing)) { + throw new \Exception('请至少选择一个客服微信'); + } + return $existing; + } + + if (!is_array($ownerWechatIds)) { + throw new \Exception('客服参数格式错误'); + } + + $normalized = $this->extractIdList($ownerWechatIds, '客服参数格式错误'); + if (empty($normalized)) { + throw new \Exception('请至少选择一个客服微信'); + } + return $normalized; + } + + /** + * 构建群推送配置数据 + * @param array $param + * @param array $ownerWechatIds + * @param WorkbenchGroupPush|null $originalConfig + * @return array + * @throws \Exception + */ + private function prepareGroupPushData(array $param, array $ownerWechatIds, WorkbenchGroupPush $originalConfig = null): array + { + $targetTypeDefault = $originalConfig ? intval($originalConfig->targetType) : 1; + $targetType = intval($this->getParamValue($param, 'targetType', $targetTypeDefault)) ?: 1; + + $groupPushSubTypeDefault = $originalConfig ? intval($originalConfig->groupPushSubType) : 1; + $groupPushSubType = intval($this->getParamValue($param, 'groupPushSubType', $groupPushSubTypeDefault)) ?: 1; + if (!in_array($groupPushSubType, [1, 2], true)) { + $groupPushSubType = 1; + } + + $data = [ + 'pushType' => $this->toBoolInt($this->getParamValue($param, 'pushType', $originalConfig->pushType ?? 0)), + 'targetType' => $targetType, + 'startTime' => $this->getParamValue($param, 'startTime', $originalConfig->startTime ?? ''), + 'endTime' => $this->getParamValue($param, 'endTime', $originalConfig->endTime ?? ''), + 'maxPerDay' => intval($this->getParamValue($param, 'maxPerDay', $originalConfig->maxPerDay ?? 0)), + 'pushOrder' => $this->getParamValue($param, 'pushOrder', $originalConfig->pushOrder ?? 1), + 'groupPushSubType' => $groupPushSubType, + 'status' => $this->toBoolInt($this->getParamValue($param, 'status', $originalConfig->status ?? 0)), + 'socialMediaId' => $this->getParamValue($param, 'socialMediaId', $originalConfig->socialMediaId ?? ''), + 'promotionSiteId' => $this->getParamValue($param, 'promotionSiteId', $originalConfig->promotionSiteId ?? ''), + 'friendIntervalMin' => intval($this->getParamValue($param, 'friendIntervalMin', $originalConfig->friendIntervalMin ?? 10)), + 'friendIntervalMax' => intval($this->getParamValue($param, 'friendIntervalMax', $originalConfig->friendIntervalMax ?? 20)), + 'messageIntervalMin' => intval($this->getParamValue($param, 'messageIntervalMin', $originalConfig->messageIntervalMin ?? 1)), + 'messageIntervalMax' => intval($this->getParamValue($param, 'messageIntervalMax', $originalConfig->messageIntervalMax ?? 12)), + 'isRandomTemplate' => $this->toBoolInt($this->getParamValue($param, 'isRandomTemplate', $originalConfig->isRandomTemplate ?? 0)), + 'ownerWechatIds' => json_encode($ownerWechatIds, JSON_UNESCAPED_UNICODE), + ]; + + if ($data['friendIntervalMin'] > $data['friendIntervalMax']) { + throw new \Exception('目标间最小间隔不能大于最大间隔'); + } + if ($data['messageIntervalMin'] > $data['messageIntervalMax']) { + throw new \Exception('消息间最小间隔不能大于最大间隔'); + } + + $contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : []; + $contentGroupsParam = $this->getParamValue($param, 'contentGroups', null); + $contentGroups = $contentGroupsParam !== null + ? $this->extractIdList($contentGroupsParam, '内容库参数格式错误') + : $contentGroupsExisting; + $data['contentLibraries'] = json_encode($contentGroups, JSON_UNESCAPED_UNICODE); + + $postPushTagsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->postPushTags ?? []) : []; + $postPushTagsParam = $this->getParamValue($param, 'postPushTags', null); + $postPushTags = $postPushTagsParam !== null + ? $this->extractIdList($postPushTagsParam, '推送标签参数格式错误') + : $postPushTagsExisting; + $data['postPushTags'] = json_encode($postPushTags, JSON_UNESCAPED_UNICODE); + + if ($targetType === 1) { + $data['isLoop'] = $this->toBoolInt($this->getParamValue($param, 'isLoop', $originalConfig->isLoop ?? 0)); + + $groupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->groups ?? []) : []; + $wechatGroups = array_key_exists('wechatGroups', $param) + ? $this->extractIdList($param['wechatGroups'], '群参数格式错误') + : $groupsExisting; + if (empty($wechatGroups)) { + throw new \Exception('群推送必须选择微信群'); + } + $data['groups'] = json_encode($wechatGroups, JSON_UNESCAPED_UNICODE); + $data['friends'] = json_encode([], JSON_UNESCAPED_UNICODE); + $data['trafficPools'] = json_encode([], JSON_UNESCAPED_UNICODE); + + if ($groupPushSubType === 2) { + $announcementContent = $this->getParamValue($param, 'announcementContent', $originalConfig->announcementContent ?? ''); + if (empty($announcementContent)) { + throw new \Exception('群公告必须输入公告内容'); + } + $enableAiRewrite = $this->toBoolInt($this->getParamValue($param, 'enableAiRewrite', $originalConfig->enableAiRewrite ?? 0)); + $aiRewritePrompt = trim((string)$this->getParamValue($param, 'aiRewritePrompt', $originalConfig->aiRewritePrompt ?? '')); + if ($enableAiRewrite === 1 && $aiRewritePrompt === '') { + throw new \Exception('启用AI智能话术改写时,必须输入改写提示词'); + } + $data['announcementContent'] = $announcementContent; + $data['enableAiRewrite'] = $enableAiRewrite; + $data['aiRewritePrompt'] = $aiRewritePrompt; + } else { + $data['groupPushSubType'] = 1; + $data['announcementContent'] = ''; + $data['enableAiRewrite'] = 0; + $data['aiRewritePrompt'] = ''; + } + } else { + $data['isLoop'] = 0; + $friendsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->friends ?? []) : []; + $trafficPoolsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->trafficPools ?? []) : []; + + $friendTargets = array_key_exists('wechatFriends', $param) + ? $this->extractIdList($param['wechatFriends'], '好友参数格式错误') + : $friendsExisting; + $trafficPools = array_key_exists('trafficPools', $param) + ? $this->extractIdList($param['trafficPools'], '流量池参数格式错误') + : $trafficPoolsExisting; + + if (empty($friendTargets) && empty($trafficPools)) { + throw new \Exception('好友推送需至少选择好友或流量池'); + } + + $data['friends'] = json_encode($friendTargets, JSON_UNESCAPED_UNICODE); + $data['trafficPools'] = json_encode($trafficPools, JSON_UNESCAPED_UNICODE); + $data['groups'] = json_encode([], JSON_UNESCAPED_UNICODE); + $data['groupPushSubType'] = 1; + $data['announcementContent'] = ''; + $data['enableAiRewrite'] = 0; + $data['aiRewritePrompt'] = ''; + } + + return $data; + } + + /** + * 获取参数值,若不存在则返回默认值 + * @param array $param + * @param string $key + * @param mixed $default + * @return mixed + */ + private function getParamValue(array $param, string $key, $default) + { + return array_key_exists($key, $param) ? $param[$key] : $default; + } + + /** + * 将值转换为整型布尔 + * @param mixed $value + * @return int + */ + private function toBoolInt($value): int + { + return empty($value) ? 0 : 1; + } + + /** + * 从参数中提取ID列表 + * @param mixed $items + * @param string $errorMessage + * @return array + * @throws \Exception + */ + private function extractIdList($items, string $errorMessage = '参数格式错误'): array + { + if (!is_array($items)) { + throw new \Exception($errorMessage); + } + + $ids = []; + foreach ($items as $item) { + if (is_array($item) && isset($item['id'])) { + $item = $item['id']; + } + if ($item === '' || $item === null) { + continue; + } + $ids[] = $item; + } + + return array_values(array_unique($ids)); + } + + /** + * 解码JSON数组 + * @param mixed $value + * @return array + */ + private function decodeJsonArray($value): array + { + if (empty($value)) { + return []; + } + if (is_array($value)) { + return $value; + } + $decoded = json_decode($value, true); + return is_array($decoded) ? $decoded : []; + } + /** * 验证内容是否包含链接 * @param string $content 要检测的内容 @@ -1838,14 +2110,14 @@ class WorkbenchController extends Controller // 通用短链接模式 '/[a-zA-Z0-9-]+\.[a-zA-Z]{2,}\/[a-zA-Z0-9\-._~:\/?#\[\]@!$&\'()*+,;=]+/i' ]; - + // 遍历所有模式进行匹配 foreach ($patterns as $pattern) { if (preg_match($pattern, $content)) { return true; } } - + return false; } @@ -1907,5 +2179,762 @@ class WorkbenchController extends Controller ]); } + /** + * 获取群发统计数据 + * @return \think\response\Json + */ + public function getGroupPushStats() + { + $workbenchId = $this->request->param('workbenchId', 0); + $timeRange = $this->request->param('timeRange', '7'); // 默认最近7天 + $contentLibraryIds = $this->request->param('contentLibraryIds', ''); // 话术组筛选 + $userId = $this->request->userInfo['id']; + + // 如果指定了工作台ID,则验证权限 + if (!empty($workbenchId)) { + $workbench = Workbench::where([ + ['id', '=', $workbenchId], + ['userId', '=', $userId], + ['type', '=', self::TYPE_GROUP_PUSH], + ['isDel', '=', 0] + ])->find(); + + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + } + + // 计算时间范围 + $days = intval($timeRange); + $startTime = strtotime(date('Y-m-d 00:00:00', strtotime("-{$days} days"))); + $endTime = time(); + + // 构建查询条件 + $where = [ + ['wgpi.createTime', '>=', $startTime], + ['wgpi.createTime', '<=', $endTime] + ]; + + // 如果指定了工作台ID,则限制查询范围 + if (!empty($workbenchId)) { + $where[] = ['wgpi.workbenchId', '=', $workbenchId]; + } else { + // 如果没有指定工作台ID,则查询当前用户的所有群推送工作台 + $workbenchIds = Workbench::where([ + ['userId', '=', $userId], + ['type', '=', self::TYPE_GROUP_PUSH], + ['isDel', '=', 0] + ])->column('id'); + + if (empty($workbenchIds)) { + // 如果没有工作台,返回空结果 + $workbenchIds = [-1]; + } + $where[] = ['wgpi.workbenchId', 'in', $workbenchIds]; + } + + // 话术组筛选 - 先获取符合条件的内容ID列表 + $contentIds = null; + if (!empty($contentLibraryIds)) { + $libraryIds = is_array($contentLibraryIds) ? $contentLibraryIds : explode(',', $contentLibraryIds); + $libraryIds = array_filter(array_map('intval', $libraryIds)); + if (!empty($libraryIds)) { + // 查询符合条件的内容ID + $contentIds = Db::name('content_item') + ->whereIn('libraryId', $libraryIds) + ->column('id'); + if (empty($contentIds)) { + // 如果没有符合条件的内容,返回空结果 + $contentIds = [-1]; // 使用不存在的ID,确保查询结果为空 + } + } + } + + // 1. 基础统计:触达率、回复率、平均回复时间、链接点击率 + $stats = $this->calculateBasicStats($workbenchId, $where, $startTime, $endTime, $contentIds); + + // 2. 话术组对比 + $contentLibraryComparison = $this->getContentLibraryComparison($workbenchId, $where, $startTime, $endTime, $contentIds); + + // 3. 时段分析 + $timePeriodAnalysis = $this->getTimePeriodAnalysis($workbenchId, $where, $startTime, $endTime, $contentIds); + + // 4. 互动深度(可选,需要更多数据) + $interactionDepth = $this->getInteractionDepth($workbenchId, $where, $startTime, $endTime, $contentIds); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'basicStats' => $stats, + 'contentLibraryComparison' => $contentLibraryComparison, + 'timePeriodAnalysis' => $timePeriodAnalysis, + 'interactionDepth' => $interactionDepth + ] + ]); + } + + /** + * 计算基础统计数据 + */ + private function calculateBasicStats($workbenchId, $where, $startTime, $endTime, $contentIds = null) + { + // 获取工作台配置,计算计划发送数 + // 如果 workbenchId 为空,则查询所有工作台的配置 + $configQuery = WorkbenchGroupPush::alias('wgp') + ->join('workbench w', 'w.id = wgp.workbenchId', 'left') + ->where('w.type', self::TYPE_GROUP_PUSH) + ->where('w.isDel', 0); + + if (!empty($workbenchId)) { + $configQuery->where('wgp.workbenchId', $workbenchId); + } else { + // 如果没有指定工作台ID,需要从 where 条件中获取 workbenchId 列表 + $workbenchIdCondition = null; + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') { + if ($condition[1] === 'in' && is_array($condition[2])) { + $workbenchIdCondition = $condition[2]; + break; + } elseif ($condition[1] === '=') { + $workbenchIdCondition = [$condition[2]]; + break; + } + } + } + if ($workbenchIdCondition) { + $configQuery->whereIn('wgp.workbenchId', $workbenchIdCondition); + } + } + + $configs = $configQuery->select(); + $targetType = 1; // 默认值 + if (!empty($configs)) { + // 如果只有一个配置,使用它的 targetType;如果有多个,默认使用1 + $targetType = intval($configs[0]->targetType ?? 1); + } + + // 计划发送数(根据配置计算) + $plannedSend = 0; + if (!empty($configs)) { + $days = ceil(($endTime - $startTime) / 86400); + foreach ($configs as $config) { + $maxPerDay = intval($config->maxPerDay ?? 0); + $configTargetType = intval($config->targetType ?? 1); + if ($configTargetType == 1) { + // 群推送:计划发送数 = 每日推送次数 * 天数 * 群数量 + $groups = $this->decodeJsonArray($config->groups ?? []); + $plannedSend += $maxPerDay * $days * count($groups); + } else { + // 好友推送:计划发送数 = 每日推送人数 * 天数 + $plannedSend += $maxPerDay * $days; + } + } + } + + // 构建查询条件 + $queryWhere = $where; + if ($contentIds !== null) { + $queryWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + + // 实际成功发送数(从推送记录表统计) + $successSend = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($queryWhere) + ->count(); + + // 触达率 = 成功发送数 / 计划发送数 + $reachRate = $plannedSend > 0 ? round(($successSend / $plannedSend) * 100, 1) : 0; + + // 获取发送记录列表,用于查询回复 + $sentItemIds = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($queryWhere) + ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId') + ->select(); + + // 回复统计(通过消息表查询) + $replyStats = $this->calculateReplyStats($sentItemIds, $targetType, $startTime, $endTime); + + // 链接点击统计 + $clickStats = $this->calculateClickStats($sentItemIds, $targetType, $startTime, $endTime); + + // 计算本月对比数据(简化处理,实际应该查询上个月同期数据) + $currentMonthStart = strtotime(date('Y-m-01 00:00:00')); + $lastMonthStart = strtotime(date('Y-m-01 00:00:00', strtotime('-1 month'))); + $lastMonthEnd = $currentMonthStart - 1; + + // 获取本月统计数据(避免递归调用) + $currentMonthWhere = [ + ['wgpi.createTime', '>=', $currentMonthStart] + ]; + // 复制 workbenchId 条件 + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') { + $currentMonthWhere[] = $condition; + break; + } + } + if ($contentIds !== null) { + $currentMonthWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + $currentMonthSend = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($currentMonthWhere) + ->count(); + + // 获取本月配置 + $currentMonthConfigQuery = WorkbenchGroupPush::alias('wgp') + ->join('workbench w', 'w.id = wgp.workbenchId', 'left') + ->where('w.type', self::TYPE_GROUP_PUSH) + ->where('w.isDel', 0); + if (!empty($workbenchId)) { + $currentMonthConfigQuery->where('wgp.workbenchId', $workbenchId); + } else { + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId' && $condition[1] === 'in') { + $currentMonthConfigQuery->whereIn('wgp.workbenchId', $condition[2]); + break; + } + } + } + $currentMonthConfigs = $currentMonthConfigQuery->select(); + $currentMonthPlanned = 0; + if (!empty($currentMonthConfigs)) { + $currentMonthDays = ceil(($endTime - $currentMonthStart) / 86400); + foreach ($currentMonthConfigs as $currentMonthConfig) { + $currentMonthMaxPerDay = intval($currentMonthConfig->maxPerDay ?? 0); + $currentMonthTargetType = intval($currentMonthConfig->targetType ?? 1); + if ($currentMonthTargetType == 1) { + $currentMonthGroups = $this->decodeJsonArray($currentMonthConfig->groups ?? []); + $currentMonthPlanned += $currentMonthMaxPerDay * $currentMonthDays * count($currentMonthGroups); + } else { + $currentMonthPlanned += $currentMonthMaxPerDay * $currentMonthDays; + } + } + } + $currentMonthReachRate = $currentMonthPlanned > 0 ? round(($currentMonthSend / $currentMonthPlanned) * 100, 1) : 0; + + // 获取上个月统计数据 + $lastMonthWhere = [ + ['wgpi.createTime', '>=', $lastMonthStart], + ['wgpi.createTime', '<=', $lastMonthEnd] + ]; + // 复制 workbenchId 条件 + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') { + $lastMonthWhere[] = $condition; + break; + } + } + if ($contentIds !== null) { + $lastMonthWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + $lastMonthSend = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($lastMonthWhere) + ->count(); + + // 获取上个月配置 + $lastMonthConfigQuery = WorkbenchGroupPush::alias('wgp') + ->join('workbench w', 'w.id = wgp.workbenchId', 'left') + ->where('w.type', self::TYPE_GROUP_PUSH) + ->where('w.isDel', 0); + if (!empty($workbenchId)) { + $lastMonthConfigQuery->where('wgp.workbenchId', $workbenchId); + } else { + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId' && $condition[1] === 'in') { + $lastMonthConfigQuery->whereIn('wgp.workbenchId', $condition[2]); + break; + } + } + } + $lastMonthConfigs = $lastMonthConfigQuery->select(); + + $lastMonthPlanned = 0; + if (!empty($lastMonthConfigs)) { + $lastMonthDays = ceil(($lastMonthEnd - $lastMonthStart) / 86400); + foreach ($lastMonthConfigs as $lastMonthConfig) { + $lastMonthMaxPerDay = intval($lastMonthConfig->maxPerDay ?? 0); + $lastMonthTargetType = intval($lastMonthConfig->targetType ?? 1); + if ($lastMonthTargetType == 1) { + $lastMonthGroups = $this->decodeJsonArray($lastMonthConfig->groups ?? []); + $lastMonthPlanned += $lastMonthMaxPerDay * $lastMonthDays * count($lastMonthGroups); + } else { + $lastMonthPlanned += $lastMonthMaxPerDay * $lastMonthDays; + } + } + } + $lastMonthReachRate = $lastMonthPlanned > 0 ? round(($lastMonthSend / $lastMonthPlanned) * 100, 1) : 0; + + // 获取上个月的回复和点击统计(简化处理) + $lastMonthSentItems = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($lastMonthWhere) + ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId') + ->select(); + $lastMonthReplyStats = $this->calculateReplyStats($lastMonthSentItems, $targetType, $lastMonthStart, $lastMonthEnd); + $lastMonthClickStats = $this->calculateClickStats($lastMonthSentItems, $targetType, $lastMonthStart, $lastMonthEnd); + + return [ + 'reachRate' => [ + 'value' => $reachRate, + 'trend' => round($reachRate - $lastMonthReachRate, 1), + 'unit' => '%', + 'description' => '成功发送/计划发送' + ], + 'replyRate' => [ + 'value' => $replyStats['replyRate'], + 'trend' => round($replyStats['replyRate'] - $lastMonthReplyStats['replyRate'], 1), + 'unit' => '%', + 'description' => '收到回复/成功发送' + ], + 'avgReplyTime' => [ + 'value' => $replyStats['avgReplyTime'], + 'trend' => round($lastMonthReplyStats['avgReplyTime'] - $replyStats['avgReplyTime'], 0), + 'unit' => '分钟', + 'description' => '从发送到回复的平均时长' + ], + 'clickRate' => [ + 'value' => $clickStats['clickRate'], + 'trend' => round($clickStats['clickRate'] - $lastMonthClickStats['clickRate'], 1), + 'unit' => '%', + 'description' => '点击链接/成功发送' + ], + 'plannedSend' => $plannedSend, + 'successSend' => $successSend, + 'replyCount' => $replyStats['replyCount'], + 'clickCount' => $clickStats['clickCount'] + ]; + } + + /** + * 计算回复统计 + */ + private function calculateReplyStats($sentItems, $targetType, $startTime, $endTime) + { + if (empty($sentItems)) { + return ['replyRate' => 0, 'avgReplyTime' => 0, 'replyCount' => 0]; + } + + $replyCount = 0; + $totalReplyTime = 0; + $replyTimes = []; + + foreach ($sentItems as $item) { + $itemArray = is_array($item) ? $item : (array)$item; + $sendTime = $itemArray['createTime'] ?? 0; + $accountId = $itemArray['wechatAccountId'] ?? 0; + + if ($targetType == 1) { + // 群推送:查找群内回复消息 + $groupId = $itemArray['groupId'] ?? 0; + $group = Db::name('wechat_group')->where('id', $groupId)->find(); + if ($group) { + $replyMsg = Db::table('s2_wechat_message') + ->where('wechatChatroomId', $group['chatroomId']) + ->where('wechatAccountId', $accountId) + ->where('isSend', 0) // 接收的消息 + ->where('wechatTime', '>', $sendTime) + ->where('wechatTime', '<=', $sendTime + 86400) // 24小时内回复 + ->order('wechatTime', 'asc') + ->find(); + + if ($replyMsg) { + $replyCount++; + $replyTime = $replyMsg['wechatTime'] - $sendTime; + $replyTimes[] = $replyTime; + $totalReplyTime += $replyTime; + } + } + } else { + // 好友推送:查找好友回复消息 + $friendId = $itemArray['friendId'] ?? 0; + $friend = Db::table('s2_wechat_friend')->where('id', $friendId)->find(); + if ($friend) { + $replyMsg = Db::table('s2_wechat_message') + ->where('wechatFriendId', $friendId) + ->where('wechatAccountId', $accountId) + ->where('isSend', 0) // 接收的消息 + ->where('wechatTime', '>', $sendTime) + ->where('wechatTime', '<=', $sendTime + 86400) // 24小时内回复 + ->order('wechatTime', 'asc') + ->find(); + + if ($replyMsg) { + $replyCount++; + $replyTime = $replyMsg['wechatTime'] - $sendTime; + $replyTimes[] = $replyTime; + $totalReplyTime += $replyTime; + } + } + } + } + + $successSend = count($sentItems); + $replyRate = $successSend > 0 ? round(($replyCount / $successSend) * 100, 1) : 0; + $avgReplyTime = $replyCount > 0 ? round(($totalReplyTime / $replyCount) / 60, 0) : 0; // 转换为分钟 + + return [ + 'replyRate' => $replyRate, + 'avgReplyTime' => $avgReplyTime, + 'replyCount' => $replyCount + ]; + } + + /** + * 计算链接点击统计 + */ + private function calculateClickStats($sentItems, $targetType, $startTime, $endTime) + { + if (empty($sentItems)) { + return ['clickRate' => 0, 'clickCount' => 0]; + } + + $clickCount = 0; + $linkContentIds = []; + + // 获取所有发送的内容ID + foreach ($sentItems as $item) { + $itemArray = is_array($item) ? $item : (array)$item; + $contentId = $itemArray['contentId'] ?? 0; + if ($contentId > 0) { + $linkContentIds[] = $contentId; + } + } + + if (empty($linkContentIds)) { + return ['clickRate' => 0, 'clickCount' => 0]; + } + + // 查询包含链接的内容 + $linkContents = Db::name('content_item') + ->whereIn('id', array_unique($linkContentIds)) + ->where('contentType', 2) // 链接类型 + ->column('id'); + + // 统计发送了链接内容的记录数 + $linkSendCount = 0; + foreach ($sentItems as $item) { + $itemArray = is_array($item) ? $item : (array)$item; + $contentId = $itemArray['contentId'] ?? 0; + if (in_array($contentId, $linkContents)) { + $linkSendCount++; + } + } + + // 简化处理:假设点击率基于链接消息的发送(实际应该从点击追踪系统获取) + // 这里可以根据业务需求调整,比如通过消息中的链接点击事件统计 + $clickCount = $linkSendCount; // 简化处理,实际需要真实的点击数据 + + $successSend = count($sentItems); + $clickRate = $successSend > 0 ? round(($clickCount / $successSend) * 100, 1) : 0; + + return [ + 'clickRate' => $clickRate, + 'clickCount' => $clickCount + ]; + } + + /** + * 获取话术组对比数据 + */ + private function getContentLibraryComparison($workbenchId, $where, $startTime, $endTime, $contentIds = null) + { + $queryWhere = $where; + if ($contentIds !== null) { + $queryWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + + $comparison = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->join('content_item ci', 'ci.id = wgpi.contentId', 'left') + ->join('content_library cl', 'cl.id = ci.libraryId', 'left') + ->where($queryWhere) + ->where('cl.id', '<>', null) + ->field([ + 'cl.id as libraryId', + 'cl.name as libraryName', + 'COUNT(DISTINCT wgpi.id) as pushCount' + ]) + ->group('cl.id, cl.name') + ->select(); + + $result = []; + foreach ($comparison as $item) { + $libraryId = $item['libraryId']; + $pushCount = intval($item['pushCount']); + + // 获取该内容库的详细统计 + $libraryContentIds = Db::name('content_item') + ->where('libraryId', $libraryId) + ->column('id'); + if (empty($libraryContentIds)) { + $libraryContentIds = [-1]; + } + + $libraryWhere = array_merge($where, [['wgpi.contentId', 'in', $libraryContentIds]]); + $librarySentItems = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($libraryWhere) + ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId') + ->select(); + + $config = WorkbenchGroupPush::where('workbenchId', $workbenchId)->find(); + $targetType = $config ? intval($config->targetType) : 1; + + $replyStats = $this->calculateReplyStats($librarySentItems, $targetType, $startTime, $endTime); + $clickStats = $this->calculateClickStats($librarySentItems, $targetType, $startTime, $endTime); + + // 计算转化率(简化处理,实际需要根据业务定义) + $conversionRate = $pushCount > 0 ? round(($replyStats['replyCount'] / $pushCount) * 100, 1) : 0; + + $result[] = [ + 'libraryId' => $libraryId, + 'libraryName' => $item['libraryName'], + 'pushCount' => $pushCount, + 'reachRate' => 100, // 简化处理,实际应该计算 + 'replyRate' => $replyStats['replyRate'], + 'clickRate' => $clickStats['clickRate'], + 'conversionRate' => $conversionRate, + 'avgReplyTime' => $replyStats['avgReplyTime'], + 'level' => $this->getPerformanceLevel($replyStats['replyRate'], $clickStats['clickRate'], $conversionRate) + ]; + } + + // 按回复率排序 + usort($result, function($a, $b) { + return $b['replyRate'] <=> $a['replyRate']; + }); + + return $result; + } + + /** + * 获取性能等级 + */ + private function getPerformanceLevel($replyRate, $clickRate, $conversionRate) + { + $score = ($replyRate * 0.4) + ($clickRate * 0.3) + ($conversionRate * 0.3); + + if ($score >= 40) { + return '优秀'; + } elseif ($score >= 25) { + return '良好'; + } elseif ($score >= 15) { + return '一般'; + } else { + return '待提升'; + } + } + + /** + * 获取时段分析数据 + */ + private function getTimePeriodAnalysis($workbenchId, $where, $startTime, $endTime, $contentIds = null) + { + $queryWhere = $where; + if ($contentIds !== null) { + $queryWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + + $analysis = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($queryWhere) + ->field([ + 'FROM_UNIXTIME(wgpi.createTime, "%H") as hour', + 'COUNT(*) as count' + ]) + ->group('hour') + ->order('hour', 'asc') + ->select(); + + $result = []; + foreach ($analysis as $item) { + $result[] = [ + 'hour' => intval($item['hour']), + 'count' => intval($item['count']) + ]; + } + + return $result; + } + + /** + * 获取互动深度数据 + */ + private function getInteractionDepth($workbenchId, $where, $startTime, $endTime, $contentIds = null) + { + // 简化处理,实际需要更复杂的统计逻辑 + return [ + 'singleReply' => 0, // 单次回复 + 'multipleReply' => 0, // 多次回复 + 'deepInteraction' => 0 // 深度互动 + ]; + } + + /** + * 获取推送历史记录列表 + * @return \think\response\Json + */ + public function getGroupPushHistory() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $workbenchId = $this->request->param('workbenchId', 0); + $keyword = $this->request->param('keyword', ''); + $userId = $this->request->userInfo['id']; + + // 构建工作台查询条件 + $workbenchWhere = [ + ['w.userId', '=', $userId], + ['w.type', '=', self::TYPE_GROUP_PUSH], + ['w.isDel', '=', 0] + ]; + + // 如果指定了工作台ID,则验证权限并限制查询范围 + if (!empty($workbenchId)) { + $workbench = Workbench::where([ + ['id', '=', $workbenchId], + ['userId', '=', $userId], + ['type', '=', self::TYPE_GROUP_PUSH], + ['isDel', '=', 0] + ])->find(); + + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + $workbenchWhere[] = ['w.id', '=', $workbenchId]; + } + + // 按内容ID、工作台ID和时间分组,统计每次推送 + $query = 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) + ->field([ + 'wgpi.workbenchId', + 'w.name as workbenchName', + 'wgpi.contentId', + 'FROM_UNIXTIME(wgpi.createTime, "%Y-%m-%d %H:00:00") as pushTime', + 'wgpi.targetType', + 'MIN(wgpi.createTime) as createTime', + 'COUNT(DISTINCT wgpi.id) as totalCount', + 'cl.name as contentLibraryName' + ]) + ->group('wgpi.workbenchId, wgpi.contentId, pushTime, wgpi.targetType'); + + if (!empty($keyword)) { + $query->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 + ->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(); + + // 处理每条记录 + foreach ($list as &$item) { + $itemWorkbenchId = $item['workbenchId']; + $contentId = $item['contentId']; + $pushTime = $item['pushTime']; + $targetType = intval($item['targetType']); + + // 将时间字符串转换为时间戳范围(小时级别) + $pushTimeStart = strtotime($pushTime); + $pushTimeEnd = $pushTimeStart + 3600; // 一小时后 + + // 获取该次推送的详细统计 + $pushWhere = [ + ['wgpi.workbenchId', '=', $itemWorkbenchId], + ['wgpi.contentId', '=', $contentId], + ['wgpi.createTime', '>=', $pushTimeStart], + ['wgpi.createTime', '<', $pushTimeEnd], + ['wgpi.targetType', '=', $targetType] + ]; + + // 目标数量 + if ($targetType == 1) { + // 群推送:统计群数量 + $targetCount = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($pushWhere) + ->where('wgpi.groupId', '<>', null) + ->distinct(true) + ->count('wgpi.groupId'); + } else { + // 好友推送:统计好友数量 + $targetCount = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($pushWhere) + ->where('wgpi.friendId', '<>', null) + ->distinct(true) + ->count('wgpi.friendId'); + } + + // 成功数和失败数(简化处理,实际需要根据发送状态判断) + $successCount = intval($item['totalCount']); // 简化处理 + $failCount = 0; // 简化处理,实际需要从发送状态获取 + + // 状态判断 + $status = $successCount > 0 ? 'success' : 'failed'; + if ($failCount > 0 && $successCount > 0) { + $status = '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'] ?? ''; + } + unset($item); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + } \ No newline at end of file diff --git a/Server/application/cunkebao/validate/Workbench.php b/Server/application/cunkebao/validate/Workbench.php index 89b8c3bb..7225c5a5 100644 --- a/Server/application/cunkebao/validate/Workbench.php +++ b/Server/application/cunkebao/validate/Workbench.php @@ -39,16 +39,19 @@ class Workbench extends Validate // 群消息推送特有参数 'pushType' => 'requireIf:type,3|in:0,1', // 推送方式 0定时 1立即 'targetType' => 'requireIf:type,3|in:1,2', // 推送目标类型:1=群推送,2=好友推送 - 'startTime' => 'requireIf:type,3|dateFormat:H:i', - 'endTime' => 'requireIf:type,3|dateFormat:H:i', + 'groupPushSubType' => 'checkGroupPushSubType|in:1,2', // 群推送子类型:1=群群发,2=群公告(仅当targetType=1时有效) 'maxPerDay' => 'requireIf:type,3|number|min:1', 'pushOrder' => 'requireIf:type,3|in:1,2', // 1最早 2最新 'isLoop' => 'requireIf:type,3|in:0,1', 'status' => 'requireIf:type,3|in:0,1', 'wechatGroups' => 'checkGroupPushTarget|array|min:1', // 当targetType=1时必填 'wechatFriends' => 'checkFriendPushTarget|array', // 当targetType=2时可选(可以为空) - 'deviceGroups' => 'checkFriendPushDevice|array|min:1', // 当targetType=2时必填 + 'ownerWechatId' => 'checkFriendPushService', // 当targetType=2且未选择好友/流量池时必填 'contentGroups' => 'requireIf:type,3|array|min:1', + // 群公告特有参数 + 'announcementContent' => 'checkAnnouncementContent|max:5000', // 群公告内容(当groupPushSubType=2时必填) + 'enableAiRewrite' => 'checkEnableAiRewrite|in:0,1', // 是否启用AI智能话术改写 + 'aiRewritePrompt' => 'checkAiRewritePrompt|max:500', // AI改写提示词(当enableAiRewrite=1时必填) // 自动建群特有参数 'groupNameTemplate' => 'requireIf:type,4|max:50', 'maxGroupsPerDay' => 'requireIf:type,4|number|min:1', @@ -61,6 +64,7 @@ class Workbench extends Validate 'accountGroups' => 'requireIf:type,5|array|min:1', // 通用参数 'deviceGroups' => 'requireIf:type,1,2,5|array', + 'trafficPools' => 'checkFriendPushPools', ]; /** @@ -123,13 +127,20 @@ class Workbench extends Validate 'wechatGroups.checkGroupPushTarget' => '群推送时必须选择推送群组', 'wechatGroups.array' => '推送群组格式错误', 'wechatGroups.min' => '至少选择一个推送群组', + 'groupPushSubType.checkGroupPushSubType' => '群推送子类型错误', + 'groupPushSubType.in' => '群推送子类型只能是群群发或群公告', + 'announcementContent.checkAnnouncementContent' => '群公告必须输入公告内容', + 'announcementContent.max' => '公告内容最多5000个字符', + 'enableAiRewrite.checkEnableAiRewrite' => 'AI智能话术改写参数错误', + 'enableAiRewrite.in' => 'AI智能话术改写参数只能是0或1', + 'aiRewritePrompt.checkAiRewritePrompt' => '启用AI智能话术改写时,必须输入改写提示词', + 'aiRewritePrompt.max' => '改写提示词最多500个字符', 'wechatFriends.requireIf' => '请选择推送好友', 'wechatFriends.checkFriendPushTarget' => '好友推送时必须选择推送好友', 'wechatFriends.array' => '推送好友格式错误', 'deviceGroups.requireIf' => '请选择设备', - 'deviceGroups.checkFriendPushDevice' => '好友推送时必须选择设备', 'deviceGroups.array' => '设备格式错误', - 'deviceGroups.min' => '至少选择一个设备', + 'ownerWechatId.checkFriendPushService' => '好友推送需选择客服或提供好友/流量池', // 自动建群相关提示 'groupNameTemplate.requireIf' => '请设置群名称前缀', 'groupNameTemplate.max' => '群名称前缀最多50个字符', @@ -160,6 +171,7 @@ class Workbench extends Validate 'accountGroups.requireIf' => '流量分发时必须选择分发账号', 'accountGroups.array' => '分发账号格式错误', 'accountGroups.min' => '至少选择一个分发账号', + 'trafficPools.checkFriendPushPools' => '好友推送时请选择好友或流量池', ]; /** @@ -169,7 +181,8 @@ class Workbench extends Validate 'create' => ['name', 'type', 'autoStart', 'deviceGroups', 'targetGroups', 'interval', 'maxLikes', 'startTime', 'endTime', 'contentTypes', 'syncCount', 'syncType', 'accountGroups', - 'pushType', 'targetType', 'startTime', 'endTime', 'maxPerDay', 'pushOrder', 'isLoop', 'status', 'wechatGroups', 'wechatFriends', 'contentGroups', + 'pushType', 'targetType', 'groupPushSubType', 'startTime', 'endTime', 'maxPerDay', 'pushOrder', 'isLoop', 'status', 'wechatGroups', 'wechatFriends', 'trafficPools', 'ownerWechatId', 'contentGroups', + 'announcementContent', 'enableAiRewrite', 'aiRewritePrompt', 'groupNameTemplate', 'maxGroupsPerDay', 'groupSizeMin', 'groupSizeMax', 'distributeType', 'timeType', 'accountGroups', ], @@ -177,7 +190,8 @@ class Workbench extends Validate 'update' => ['name', 'type', 'autoStart', 'deviceGroups', 'targetGroups', 'interval', 'maxLikes', 'startTime', 'endTime', 'contentTypes', 'syncCount', 'syncType', 'accountGroups', - 'pushType', 'targetType', 'startTime', 'endTime', 'maxPerDay', 'pushOrder', 'isLoop', 'status', 'wechatGroups', 'wechatFriends', 'deviceGroups', 'contentGroups', + 'pushType', 'targetType', 'groupPushSubType', 'startTime', 'endTime', 'maxPerDay', 'pushOrder', 'isLoop', 'status', 'wechatGroups', 'wechatFriends', 'trafficPools', 'ownerWechatId', 'contentGroups', + 'announcementContent', 'enableAiRewrite', 'aiRewritePrompt', 'groupNameTemplate', 'maxGroupsPerDay', 'groupSizeMin', 'groupSizeMax', 'distributeType', 'timeType', 'accountGroups', ] @@ -243,18 +257,126 @@ class Workbench extends Validate /** * 验证好友推送时设备必填(当targetType=2时,deviceGroups必填) */ - protected function checkFriendPushDevice($value, $rule, $data) + protected function checkFriendPushService($value, $rule, $data) + { + if (isset($data['type']) && $data['type'] == self::TYPE_GROUP_PUSH) { + $targetType = isset($data['targetType']) ? intval($data['targetType']) : 1; // 默认1 + if ($targetType == 2) { + if ($value !== null && $value !== '' && !is_array($value)) { + return false; + } + + $hasFriends = isset($data['wechatFriends']) && is_array($data['wechatFriends']) && count($data['wechatFriends']) > 0; + $hasPools = isset($data['trafficPools']) && is_array($data['trafficPools']) && count($data['trafficPools']) > 0; + $hasServices = is_array($value) && count(array_filter($value, function ($item) { + if (is_array($item)) { + return !empty($item['ownerWechatId'] ?? $item['wechatId'] ?? $item['id']); + } + return $item !== null && $item !== ''; + })) > 0; + + if (!$hasFriends && !$hasPools && !$hasServices) { + return false; + } + } + } + return true; + } + + /** + * 验证好友推送时是否选择好友或流量池(至少其一) + */ + protected function checkFriendPushPools($value, $rule, $data) + { + if (isset($data['type']) && $data['type'] == self::TYPE_GROUP_PUSH) { + $targetType = isset($data['targetType']) ? intval($data['targetType']) : 1; // 默认1 + if ($targetType == 2) { + $hasFriends = isset($data['wechatFriends']) && !empty($data['wechatFriends']); + $hasPools = isset($value) && $value !== null && $value !== '' && is_array($value) && count($value) > 0; + if (!$hasFriends && !$hasPools) { + return false; + } + if (isset($value) && $value !== null && $value !== '') { + if (!is_array($value)) { + return false; + } + } + } + } + return true; + } + + /** + * 验证群推送子类型(当targetType=1时,groupPushSubType必填且只能是1或2) + */ + protected function checkGroupPushSubType($value, $rule, $data) { // 如果是群消息推送类型 if (isset($data['type']) && $data['type'] == self::TYPE_GROUP_PUSH) { - // 如果targetType=2(好友推送),则deviceGroups必填 + // 如果targetType=1(群推送),则groupPushSubType必填 $targetType = isset($data['targetType']) ? intval($data['targetType']) : 1; // 默认1 - if ($targetType == 2) { + if ($targetType == 1) { // 检查值是否存在且有效 - if (!isset($value) || $value === null || $value === '') { + if (!isset($value) || !in_array(intval($value), [1, 2])) { return false; } - if (!is_array($value) || count($value) < 1) { + } + } + return true; + } + + /** + * 验证群公告内容(当groupPushSubType=2时,announcementContent必填) + */ + protected function checkAnnouncementContent($value, $rule, $data) + { + // 如果是群消息推送类型 + if (isset($data['type']) && $data['type'] == self::TYPE_GROUP_PUSH) { + // 如果targetType=1且groupPushSubType=2(群公告),则announcementContent必填 + $targetType = isset($data['targetType']) ? intval($data['targetType']) : 1; // 默认1 + $groupPushSubType = isset($data['groupPushSubType']) ? intval($data['groupPushSubType']) : 1; // 默认1 + if ($targetType == 1 && $groupPushSubType == 2) { + // 检查值是否存在且有效 + if (!isset($value) || $value === null || trim($value) === '') { + return false; + } + } + } + return true; + } + + /** + * 验证AI智能话术改写(当enableAiRewrite=1时,aiRewritePrompt必填) + */ + protected function checkEnableAiRewrite($value, $rule, $data) + { + // 如果是群消息推送类型且是群公告 + if (isset($data['type']) && $data['type'] == self::TYPE_GROUP_PUSH) { + $targetType = isset($data['targetType']) ? intval($data['targetType']) : 1; // 默认1 + $groupPushSubType = isset($data['groupPushSubType']) ? intval($data['groupPushSubType']) : 1; // 默认1 + if ($targetType == 1 && $groupPushSubType == 2) { + // 检查值是否存在且有效 + if (!isset($value) || !in_array(intval($value), [0, 1])) { + return false; + } + } + } + return true; + } + + /** + * 验证AI改写提示词(当enableAiRewrite=1时,aiRewritePrompt必填) + */ + protected function checkAiRewritePrompt($value, $rule, $data) + { + // 如果是群消息推送类型且是群公告 + if (isset($data['type']) && $data['type'] == self::TYPE_GROUP_PUSH) { + $targetType = isset($data['targetType']) ? intval($data['targetType']) : 1; // 默认1 + $groupPushSubType = isset($data['groupPushSubType']) ? intval($data['groupPushSubType']) : 1; // 默认1 + $enableAiRewrite = isset($data['enableAiRewrite']) ? intval($data['enableAiRewrite']) : 0; // 默认0 + if ($targetType == 1 && $groupPushSubType == 2 && $enableAiRewrite == 1) { + // 如果启用AI改写,提示词必填 + if (!isset($value) || $value === null || trim($value) === '') { return false; } } diff --git a/Server/application/job/WorkbenchGroupPushJob.php b/Server/application/job/WorkbenchGroupPushJob.php index e4a1f648..b14c266b 100644 --- a/Server/application/job/WorkbenchGroupPushJob.php +++ b/Server/application/job/WorkbenchGroupPushJob.php @@ -58,11 +58,18 @@ class WorkbenchGroupPushJob { try { // 获取所有工作台 - $workbenches = Workbench::where(['status' => 1, 'type' => 3, 'isDel' => 0,'id' => 256])->order('id desc')->select(); + $workbenches = Workbench::where(['status' => 1, 'type' => 3, 'isDel' => 0,'id' => 264])->order('id desc')->select(); foreach ($workbenches as $workbench) { // 获取工作台配置 - $config = WorkbenchGroupPush::where('workbenchId', $workbench->id)->find(); - if (!$config) { + $configModel = WorkbenchGroupPush::where('workbenchId', $workbench->id)->find(); + if (!$configModel) { + continue; + } + + // 标准化配置 + $config = $this->normalizeConfig($configModel->toArray()); + if ($config === false) { + Log::warning("消息群发:配置无效,工作台ID: {$workbench->id}"); continue; } @@ -72,7 +79,16 @@ class WorkbenchGroupPushJob continue; } - // 获取内容库 + $targetType = intval($config['targetType']); + $groupPushSubType = intval($config['groupPushSubType']); + + // 如果是群推送且是群公告,暂时跳过(晚点处理) + if ($targetType == 1 && $groupPushSubType == 2) { + Log::info("群公告功能暂未实现,工作台ID: {$workbench->id}"); + continue; + } + + // 获取内容库(群群发需要内容库,好友推送也需要内容库) $contentLibrary = $this->getContentLibrary($workbench, $config); if (empty($contentLibrary)) { continue; @@ -93,7 +109,7 @@ class WorkbenchGroupPushJob // 消息拼接 msgType(1:文本 3:图片 43:视频 47:动图表情包(gif、其他表情包) 49:小程序/其他:图文、文件) // 当前,type 为文本、图片、动图表情包的时候,content为string, 其他情况为对象 {type: 'file/link/...', url: '', title: '', thunmbPath: '', desc: ''} - $targetType = isset($config['targetType']) ? intval($config['targetType']) : 1; // 默认1=群推送 + $targetType = intval($config['targetType']); // 默认1=群推送 $toAccountId = ''; $username = Env::get('api.username', ''); @@ -103,47 +119,56 @@ class WorkbenchGroupPushJob } // 建立WebSocket $wsController = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]); - + $ownerWechatIds = $config['ownerWechatIds'] ?? $this->getOwnerWechatIds($config); if ($targetType == 1) { // 群推送 - $this->sendToGroups($workbench, $config, $msgConf, $wsController); + $this->sendToGroups($workbench, $config, $msgConf, $wsController, $ownerWechatIds); } else { // 好友推送 - $this->sendToFriends($workbench, $config, $msgConf, $wsController); + $this->sendToFriends($workbench, $config, $msgConf, $wsController, $ownerWechatIds); } } /** * 发送群消息 */ - protected function sendToGroups($workbench, $config, $msgConf, $wsController) + protected function sendToGroups($workbench, $config, $msgConf, $wsController, array $ownerWechatIds = []) { - $groups = json_decode($config['groups'], true); - if (empty($groups)) { + // 获取群推送子类型:1=群群发,2=群公告 + $groupPushSubType = intval($config['groupPushSubType'] ?? 1); // 默认1=群群发 + + // 如果是群公告,暂时跳过(晚点处理) + if ($groupPushSubType == 2) { + Log::info("群公告功能暂未实现,工作台ID: {$workbench['id']}"); return false; } - $groupsData = Db::name('wechat_group')->whereIn('id', $groups)->field('id,wechatAccountId,chatroomId,companyId,ownerWechatId')->select(); + // 群群发:从groups字段获取群ID列表 + $groups = $config['groups'] ?? []; + if (empty($groups)) { + Log::warning("群群发:未选择微信群,工作台ID: {$workbench['id']}"); + return false; + } + + $query = Db::name('wechat_group') + ->whereIn('id', $groups); + + if (!empty($ownerWechatIds)) { + $query->whereIn('wechatAccountId', $ownerWechatIds); + } + + $groupsData = $query + ->field('id,wechatAccountId,chatroomId,companyId,ownerWechatId') + ->select(); if (empty($groupsData)) { + Log::warning("群群发:未找到微信群数据,工作台ID: {$workbench['id']}"); return false; } foreach ($msgConf as $content) { - $sendData = []; $sqlData = []; foreach ($groupsData as $group) { - // msgType(1:文本 3:图片 43:视频 47:动图表情包(gif、其他表情包) 49:小程序/其他:图文、文件) - $sqlData[] = [ - 'workbenchId' => $workbench['id'], - 'contentId' => $content['id'], - 'groupId' => $group['id'], - 'friendId' => null, - 'targetType' => 1, - 'wechatAccountId' => $group['wechatAccountId'], - 'createTime' => time() - ]; - // 构建发送数据 $sendData = $this->buildSendData($content, $config, $group['wechatAccountId'], $group['id'], 'group'); if (empty($sendData)) { @@ -154,77 +179,71 @@ class WorkbenchGroupPushJob foreach ($sendData as $send) { $wsController->sendCommunity($send); } - //插入发送记录 + + // 准备插入发送记录 + $sqlData[] = [ + 'workbenchId' => $workbench['id'], + 'contentId' => $content['id'], + 'groupId' => $group['id'], + 'friendId' => null, + 'targetType' => 1, + 'wechatAccountId' => $group['wechatAccountId'], + 'createTime' => time() + ]; + } + + // 批量插入发送记录 + if (!empty($sqlData)) { Db::name('workbench_group_push_item')->insertAll($sqlData); + Log::info("群群发:推送了" . count($sqlData) . "个群,工作台ID: {$workbench['id']}"); } } + + return true; } /** * 发送好友消息 */ - protected function sendToFriends($workbench, $config, $msgConf, $wsController) + protected function sendToFriends($workbench, $config, $msgConf, $wsController, array $ownerWechatIds = []) { - $friends = json_decode($config['friends'], true); - $devices = json_decode($config['devices'] ?? '[]', true); + $friends = $config['friends'] ?? []; + $trafficPools = $config['trafficPools'] ?? []; + $devices = $config['devices'] ?? []; - // 如果好友列表为空,则根据设备查询所有好友 - if (empty($friends)) { - if (empty($devices)) { - // 如果没有选择设备,则无法推送 - Log::warning('好友推送:未选择设备,无法推送全部好友'); - return false; - } + $friendsData = []; - // 根据设备查询所有好友 - $friendsData = Db::table('s2_company_account') - ->alias('ca') - ->join(['s2_wechat_account' => 'wa'], 'ca.id = wa.deviceAccountId') - ->join(['s2_wechat_friend' => 'wf'], 'wf.wechatAccountId = wa.id') - ->where([ - 'ca.status' => 0, - 'wf.isDeleted' => 0, - 'wa.deviceAlive' => 1, - 'wa.wechatAlive' => 1 - ]) - ->whereIn('wa.currentDeviceId', $devices) - ->field('wf.id,wf.wechatAccountId,wf.wechatId,wf.ownerWechatId') - ->group('wf.id') - ->select(); - } else { - // 查询指定的好友信息 - $friendsData = Db::table('s2_wechat_friend') - ->whereIn('id', $friends) - ->where('isDeleted', 0) - ->field('id,wechatAccountId,wechatId,ownerWechatId') - ->select(); + // 指定好友 + if (!empty($friends)) { + $friendsData = array_merge($friendsData, $this->getFriendsByIds($friends, $ownerWechatIds)); } + // 流量池好友 + if (!empty($trafficPools)) { + $friendsData = array_merge($friendsData, $this->getFriendsByTrafficPools($trafficPools, $workbench, $ownerWechatIds)); + } + + // 如果未选择好友或流量池,则根据设备查询所有好友 + if (empty($friendsData)) { + if (empty($devices)) { + Log::warning('好友推送:未选择好友或流量池,且未选择设备,无法推送'); + return false; + } + $friendsData = $this->getFriendsByDevices($devices, $ownerWechatIds); + } + $friendsData = $this->deduplicateFriends($friendsData); if (empty($friendsData)) { return false; } - // 获取所有已推送的好友ID列表(去重,不限制时间范围,用于过滤避免重复推送) + // 获取已推送的好友ID列表(不限制时间范围,避免重复推送) $sentFriendIds = Db::name('workbench_group_push_item') ->where('workbenchId', $workbench->id) ->where('targetType', 2) ->column('friendId'); - $sentFriendIds = array_filter($sentFriendIds); // 过滤null值 - $sentFriendIds = array_unique($sentFriendIds); // 去重 + $sentFriendIds = array_unique(array_filter($sentFriendIds)); - // 获取今日已推送的好友ID列表(用于计算今日推送人数) - $today = date('Y-m-d'); - $todayStartTimestamp = strtotime($today . ' 00:00:00'); - $todayEndTimestamp = strtotime($today . ' 23:59:59'); - $todaySentFriendIds = Db::name('workbench_group_push_item') - ->where('workbenchId', $workbench->id) - ->where('targetType', 2) - ->whereTime('createTime', 'between', [$todayStartTimestamp, $todayEndTimestamp]) - ->column('friendId'); - $todaySentFriendIds = array_filter($todaySentFriendIds); // 过滤null值 - $todaySentFriendIds = array_unique($todaySentFriendIds); // 去重 - - // 过滤掉所有已推送的好友(不限制时间范围,避免重复推送) + // 过滤掉所有已推送的好友 $friendsData = array_filter($friendsData, function($friend) use ($sentFriendIds) { return !in_array($friend['id'], $sentFriendIds); }); @@ -237,13 +256,13 @@ class WorkbenchGroupPushJob // 重新索引数组 $friendsData = array_values($friendsData); - // 计算剩余可推送人数(基于今日推送人数) - $todaySentCount = count($todaySentFriendIds); + // 计算剩余可推送人数(基于累计推送人数) + $sentFriendCount = count($sentFriendIds); $maxPerDay = intval($config['maxPerDay']); - $remainingCount = $maxPerDay - $todaySentCount; + $remainingCount = $maxPerDay - $sentFriendCount; if ($remainingCount <= 0) { - Log::info('好友推送:今日推送人数已达上限'); + Log::info('好友推送:累计推送人数已达上限'); return false; } @@ -416,6 +435,349 @@ class WorkbenchGroupPushJob return $sendData; } + /** + * 根据好友ID获取好友信息 + * @param array $friendIds + * @return array + */ + protected function getFriendsByIds(array $friendIds, array $ownerWechatIds = []) + { + if (empty($friendIds)) { + return []; + } + $query = Db::table('s2_wechat_friend') + ->whereIn('id', $friendIds) + ->where('isDeleted', 0); + + if (!empty($ownerWechatIds)) { + $query->whereIn('wechatAccountId', $ownerWechatIds); + } + + $friends = $query + ->field('id,wechatAccountId,wechatId,ownerWechatId') + ->select(); + if ($friends === false) { + return []; + } + + return $friends; + } + + /** + * 根据设备获取好友信息 + * @param array $deviceIds + * @return array + */ + protected function getFriendsByDevices(array $deviceIds, array $ownerWechatIds = []) + { + if (empty($deviceIds)) { + return []; + } + + $query = Db::table('s2_company_account') + ->alias('ca') + ->join(['s2_wechat_account' => 'wa'], 'ca.id = wa.deviceAccountId') + ->join(['s2_wechat_friend' => 'wf'], 'wf.wechatAccountId = wa.id') + ->where([ + 'ca.status' => 0, + 'wf.isDeleted' => 0, + 'wa.deviceAlive' => 1, + 'wa.wechatAlive' => 1 + ]) + ->whereIn('wa.currentDeviceId', $deviceIds); + + if (!empty($ownerWechatIds)) { + $query->whereIn('wf.wechatAccountId', $ownerWechatIds); + } + + $friends = $query + ->field('wf.id,wf.wechatAccountId,wf.wechatId,wf.ownerWechatId') + ->group('wf.id') + ->select(); + + if ($friends === false) { + return []; + } + + return $friends->toArray(); + } + + /** + * 根据流量池获取好友信息 + * @param array $trafficPools + * @param Workbench $workbench + * @return array + */ + protected function getFriendsByTrafficPools(array $trafficPools, $workbench, array $ownerWechatIds = []) + { + if (empty($trafficPools)) { + return []; + } + + $companyId = $workbench->companyId ?? 0; + + $query = Db::name('traffic_source_package_item') + ->alias('tspi') + ->leftJoin('traffic_source_package tsp', 'tsp.id = tspi.packageId') + ->leftJoin('traffic_pool tp', 'tp.identifier = tspi.identifier') + ->leftJoin(['s2_wechat_friend' => 'wf'], 'wf.wechatId = tp.wechatId') + ->leftJoin(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId') + ->whereIn('tspi.packageId', $trafficPools) + ->where('tsp.isDel', 0) + ->where('wf.isDeleted', 0) + ->whereNotNull('wf.id') + ->whereNotNull('wf.wechatAccountId') + ->where(function ($query) use ($companyId) { + $query->whereIn('tsp.companyId', [$companyId, 0]); + }) + ->where(function ($query) use ($companyId) { + $query->whereIn('tspi.companyId', [$companyId, 0]); + }); + + if (!empty($ownerWechatIds)) { + $query->whereIn('wf.wechatAccountId', $ownerWechatIds); + } + + $friends = $query + ->field('wf.id,wf.wechatAccountId,wf.wechatId,wf.ownerWechatId') + ->group('wf.id') + ->select(); + + if (empty($friends)) { + Log::info('好友推送:流量池未匹配到好友'); + return []; + } + + if ($friends === false) { + return []; + } + + return $friends; + } + + /** + * 标准化群推送配置 + * @param array $config + * @return array|false + */ + protected function normalizeConfig(array $config) + { + $config['targetType'] = intval($config['targetType'] ?? 1); + $config['groupPushSubType'] = intval($config['groupPushSubType'] ?? 1); + if (!in_array($config['groupPushSubType'], [1, 2], true)) { + $config['groupPushSubType'] = 1; + } + + $config['pushType'] = !empty($config['pushType']) ? 1 : 0; + $config['status'] = !empty($config['status']) ? 1 : 0; + $config['isLoop'] = !empty($config['isLoop']) ? 1 : 0; + + $config['startTime'] = $this->normalizeTimeString($config['startTime'] ?? '00:00'); + $config['endTime'] = $this->normalizeTimeString($config['endTime'] ?? '23:59'); + $config['maxPerDay'] = max(0, intval($config['maxPerDay'] ?? 0)); + + $config['friendIntervalMin'] = max(0, intval($config['friendIntervalMin'] ?? 0)); + $config['friendIntervalMax'] = max(0, intval($config['friendIntervalMax'] ?? $config['friendIntervalMin'])); + if ($config['friendIntervalMin'] > $config['friendIntervalMax']) { + $config['friendIntervalMax'] = $config['friendIntervalMin']; + } + + $config['messageIntervalMin'] = max(0, intval($config['messageIntervalMin'] ?? 0)); + $config['messageIntervalMax'] = max(0, intval($config['messageIntervalMax'] ?? $config['messageIntervalMin'])); + if ($config['messageIntervalMin'] > $config['messageIntervalMax']) { + $config['messageIntervalMax'] = $config['messageIntervalMin']; + } + + $config['ownerWechatIds'] = $this->deduplicateIds($this->jsonToArray($config['ownerWechatIds'] ?? [])); + $config['groups'] = $this->deduplicateIds($this->jsonToArray($config['groups'] ?? [])); + $config['friends'] = $this->deduplicateIds($this->jsonToArray($config['friends'] ?? [])); + $config['trafficPools'] = $this->deduplicateIds($this->jsonToArray($config['trafficPools'] ?? [])); + $config['devices'] = $this->deduplicateIds($this->jsonToArray($config['devices'] ?? [])); + $config['contentLibraries'] = $this->deduplicateIds($this->jsonToArray($config['contentLibraries'] ?? [])); + $config['postPushTags'] = $this->deduplicateIds($this->jsonToArray($config['postPushTags'] ?? [])); + + return $config; + } + + /** + * 将混合类型转换为数组 + * @param mixed $value + * @return array + */ + protected function jsonToArray($value): array + { + if (empty($value)) { + return []; + } + + if (is_array($value)) { + return $value; + } + + if (is_string($value)) { + $decoded = json_decode($value, true); + if (json_last_error() === JSON_ERROR_NONE) { + return is_array($decoded) ? $decoded : []; + } + } + + return []; + } + + /** + * 归一化时间字符串,保留到分钟 + * @param string $time + * @return string + */ + protected function normalizeTimeString(string $time): string + { + if (empty($time)) { + return '00:00'; + } + $parts = explode(':', $time); + $hour = str_pad(intval($parts[0] ?? 0), 2, '0', STR_PAD_LEFT); + $minute = str_pad(intval($parts[1] ?? 0), 2, '0', STR_PAD_LEFT); + return "{$hour}:{$minute}"; + } + + /** + * 对ID数组进行去重并清理无效值 + * @param array $ids + * @return array + */ + protected function deduplicateIds(array $ids) + { + if (empty($ids)) { + return []; + } + + $normalized = array_map(function ($value) { + if (is_array($value) && isset($value['id'])) { + return $value['id']; + } + if (is_object($value) && isset($value->id)) { + return $value->id; + } + return $value; + }, $ids); + + $filtered = array_filter($normalized, function ($value) { + return $value !== null && $value !== ''; + }); + + if (empty($filtered)) { + return []; + } + + return array_values(array_unique($filtered)); + } + + /** + * 对内容列表根据内容ID去重 + * @param mixed $contents + * @return array + */ + protected function deduplicateContentList($contents) + { + if (empty($contents)) { + return []; + } + + if ($contents instanceof \think\Collection || $contents instanceof \think\model\Collection) { + $contents = $contents->toArray(); + } elseif ($contents instanceof \think\Model) { + $contents = [$contents->toArray()]; + } + + if (!is_array($contents)) { + return []; + } + + $result = []; + $unique = []; + + foreach ($contents as $content) { + if ($content instanceof \think\Model) { + $content = $content->toArray(); + } elseif (is_object($content)) { + $content = (array)$content; + } + + if (!is_array($content)) { + continue; + } + + $contentId = $content['id'] ?? null; + if (empty($contentId) || isset($unique[$contentId])) { + continue; + } + + $unique[$contentId] = true; + $result[] = $content; + } + + return $result; + } + + /** + * 对好友数据进行去重 + * @param array $friends + * @return array + */ + protected function deduplicateFriends(array $friends) + { + if (empty($friends)) { + return []; + } + + $unique = []; + $result = []; + + foreach ($friends as $friend) { + if (empty($friend['id'])) { + continue; + } + if (isset($unique[$friend['id']])) { + continue; + } + $unique[$friend['id']] = true; + $result[] = $friend; + } + + return $result; + } + + /** + * 获取配置中的客服微信ID列表 + * @param array $config + * @return array + */ + protected function getOwnerWechatIds($config) + { + if (empty($config['ownerWechatIds'])) { + return []; + } + + $ownerWechatIds = $config['ownerWechatIds']; + + if (is_string($ownerWechatIds)) { + $decoded = json_decode($ownerWechatIds, true); + if (json_last_error() === JSON_ERROR_NONE) { + $ownerWechatIds = $decoded; + } + } + + if (!is_array($ownerWechatIds)) { + return []; + } + + $ownerWechatIds = array_map(function ($id) { + return is_numeric($id) ? intval($id) : $id; + }, $ownerWechatIds); + + return $this->deduplicateIds($ownerWechatIds); + } + /** * 记录发送历史 @@ -441,10 +803,10 @@ class WorkbenchGroupPushJob } /** - * 获取设备列表 + * 判断是否推送 * @param Workbench $workbench 工作台 - * @param WorkbenchGroupPush $config 配置 - * @return array|bool + * @param array $config 配置 + * @return bool */ protected function isPush($workbench, $config) { @@ -463,27 +825,34 @@ class WorkbenchGroupPushJob return false; } - $targetType = isset($config['targetType']) ? intval($config['targetType']) : 1; // 默认1=群推送 + $targetType = intval($config['targetType']); // 默认1=群推送 if ($targetType == 2) { // 好友推送:maxPerDay表示每日推送人数 - // 查询今日已推送的好友ID列表(去重,仅统计今日) + // 查询已推送的好友ID列表(去重) $sentFriendIds = Db::name('workbench_group_push_item') ->where('workbenchId', $workbench->id) ->where('targetType', 2) - ->whereTime('createTime', 'between', [$startTimestamp, $endTimestamp]) ->column('friendId'); $sentFriendIds = array_filter($sentFriendIds); // 过滤null值 - $count = count(array_unique($sentFriendIds)); // 去重后统计今日推送人数 + $count = count(array_unique($sentFriendIds)); // 去重后统计累计推送人数 if ($count >= $config['maxPerDay']) { return false; } - // 计算本次同步的最早允许时间(按人数计算间隔) - $interval = floor($totalSeconds / $config['maxPerDay']); - $nextSyncTime = $startTimestamp + $count * $interval; - if (time() < $nextSyncTime) { + // 计算本次同步的最早允许时间(基于好友/消息间隔配置) + $friendIntervalMin = max(0, intval($config['friendIntervalMin'] ?? 0)); + $messageIntervalMin = max(0, intval($config['messageIntervalMin'] ?? 0)); + $minInterval = max(1, $friendIntervalMin + $messageIntervalMin); + + $lastSendTime = Db::name('workbench_group_push_item') + ->where('workbenchId', $workbench->id) + ->where('targetType', 2) + ->order('id', 'desc') + ->value('createTime'); + + if (!empty($lastSendTime) && (time() - $lastSendTime) < $minInterval) { return false; } } else { @@ -513,17 +882,24 @@ class WorkbenchGroupPushJob /** * 获取内容库 * @param Workbench $workbench 工作台 - * @param WorkbenchGroupPush $config 配置 + * @param array $config 配置 * @return array|bool */ protected function getContentLibrary($workbench, $config) { - $contentids = json_decode($config['contentLibraries'], true); - if (empty($contentids)) { + $targetType = intval($config['targetType']); // 默认1=群推送 + $groupPushSubType = intval($config['groupPushSubType']); // 默认1=群群发 + + // 如果是群公告,不需要内容库(晚点处理) + if ($targetType == 1 && $groupPushSubType == 2) { return false; } - $targetType = isset($config['targetType']) ? intval($config['targetType']) : 1; // 默认1=群推送 + $contentids = $config['contentLibraries'] ?? []; + if (empty($contentids)) { + Log::warning("未选择内容库,工作台ID: {$workbench->id}"); + return false; + } if ($config['pushType'] == 1) { $limit = 10; @@ -563,10 +939,12 @@ class WorkbenchGroupPushJob if ($config['isLoop'] == 1) { // 可以循环发送(只有群推送时才能为1) // 1. 优先获取未发送的内容 - $unsentContent = $query->where('wgpi.id', 'null') - ->order($order) - ->limit(0, $limit) - ->select(); + $unsentContent = $this->deduplicateContentList( + $query->where('wgpi.id', 'null') + ->order($order) + ->limit(0, $limit) + ->select() + ); if (!empty($unsentContent)) { return $unsentContent; } @@ -585,18 +963,32 @@ class WorkbenchGroupPushJob return []; } - $sentContent = $query2->where('wgpi.contentId', '<', $lastSendData['contentId'])->order('wgpi.id ASC')->group('wgpi.contentId')->limit(0, $limit)->select(); + $sentContent = $this->deduplicateContentList( + $query2->where('wgpi.contentId', '<', $lastSendData['contentId']) + ->order('wgpi.id ASC') + ->group('wgpi.contentId') + ->limit(0, $limit) + ->select() + ); if (empty($sentContent)) { - $sentContent = $query3->where('wgpi.contentId', '=', $fastSendData['contentId'])->order('wgpi.id ASC')->group('wgpi.contentId')->limit(0, $limit)->select(); + $sentContent = $this->deduplicateContentList( + $query3->where('wgpi.contentId', '=', $fastSendData['contentId']) + ->order('wgpi.id ASC') + ->group('wgpi.contentId') + ->limit(0, $limit) + ->select() + ); } return $sentContent; } else { // 不能循环发送,只获取未发送的内容(好友推送时isLoop=0) - $list = $query->where('wgpi.id', 'null') - ->order($order) - ->limit(0, $limit) - ->select(); + $list = $this->deduplicateContentList( + $query->where('wgpi.id', 'null') + ->order($order) + ->limit(0, $limit) + ->select() + ); return $list; } }