diff --git a/application/cunkebao/controller/workbench/WorkbenchAutoLikeController.php b/application/cunkebao/controller/workbench/WorkbenchAutoLikeController.php index de112ba..351d735 100644 --- a/application/cunkebao/controller/workbench/WorkbenchAutoLikeController.php +++ b/application/cunkebao/controller/workbench/WorkbenchAutoLikeController.php @@ -106,3 +106,4 @@ class WorkbenchAutoLikeController extends Controller } } + diff --git a/application/cunkebao/controller/workbench/WorkbenchController.php b/application/cunkebao/controller/workbench/WorkbenchController.php index 7f75653..b88e484 100644 --- a/application/cunkebao/controller/workbench/WorkbenchController.php +++ b/application/cunkebao/controller/workbench/WorkbenchController.php @@ -1896,11 +1896,35 @@ class WorkbenchController extends Controller throw new \Exception('消息间最小间隔不能大于最大间隔'); } - $contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : []; - $contentGroupsParam = $this->getParamValue($param, 'contentGroups', null); - $contentGroups = $contentGroupsParam !== null - ? $this->extractIdList($contentGroupsParam, '内容库参数格式错误') - : $contentGroupsExisting; + // 群公告(groupPushSubType = 2)时,contentGroups 可以为空,不需要验证 + if ($targetType === 1 && $groupPushSubType === 2) { + // 群公告可以不传内容库,允许为空,不进行任何验证 + $contentGroupsParam = $this->getParamValue($param, 'contentGroups', null); + if ($contentGroupsParam !== null) { + // 如果传入了参数,尝试解析,但不验证格式和是否为空 + try { + $contentGroups = $this->extractIdList($contentGroupsParam, '内容库参数格式错误'); + } catch (\Exception $e) { + // 群公告时忽略格式错误,使用空数组 + $contentGroups = []; + } + } else { + // 如果没有传入参数,使用现有配置或空数组 + $contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : []; + $contentGroups = $contentGroupsExisting; + } + } else { + // 其他情况,正常处理并验证 + $contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : []; + $contentGroupsParam = $this->getParamValue($param, 'contentGroups', null); + $contentGroups = $contentGroupsParam !== null + ? $this->extractIdList($contentGroupsParam, '内容库参数格式错误') + : $contentGroupsExisting; + // 其他情况,内容库为必填 + if (empty($contentGroups)) { + throw new \Exception('请至少选择一个内容库'); + } + } $data['contentLibraries'] = json_encode($contentGroups, JSON_UNESCAPED_UNICODE); $postPushTagsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->postPushTags ?? []) : []; diff --git a/application/cunkebao/controller/workbench/WorkbenchGroupPushController.php b/application/cunkebao/controller/workbench/WorkbenchGroupPushController.php index 52e3005..c49356e 100644 --- a/application/cunkebao/controller/workbench/WorkbenchGroupPushController.php +++ b/application/cunkebao/controller/workbench/WorkbenchGroupPushController.php @@ -18,454 +18,40 @@ class WorkbenchGroupPushController extends Controller const TYPE_GROUP_PUSH = 3; // 群消息推送 /** - * 创建工作台 + * 获取群发统计数据 * @return \think\response\Json */ - public function create() + public function getGroupPushStats() { - if (!$this->request->isPost()) { - return json(['code' => 400, 'msg' => '请求方式错误']); - } + $workbenchId = $this->request->param('workbenchId', 0); + $timeRange = $this->request->param('timeRange', '7'); // 默认最近7天 + $contentLibraryIds = $this->request->param('contentLibraryIds', ''); // 话术组筛选 + $userId = $this->request->userInfo['id']; - // 获取登录用户信息 - $userInfo = request()->userInfo; + // 如果指定了工作台ID,则验证权限 + if (!empty($workbenchId)) { + $workbench = Workbench::where([ + ['id', '=', $workbenchId], + ['userId', '=', $userId], + ['type', '=', self::TYPE_GROUP_PUSH], + ['isDel', '=', 0] + ])->find(); - // 获取请求参数 - $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'; + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在']); } } - // 验证数据 - $validate = new WorkbenchValidate; - if (!$validate->scene('create')->check($param)) { - return json(['code' => 400, 'msg' => $validate->getError()]); - } - - Db::startTrans(); - try { - // 创建工作台基本信息 - $workbench = new Workbench; - $workbench->name = $param['name']; - $workbench->type = $param['type']; - $workbench->status = !empty($param['status']) ? 1 : 0; - $workbench->autoStart = !empty($param['autoStart']) ? 1 : 0; - $workbench->userId = $userInfo['id']; - $workbench->companyId = $userInfo['companyId']; - $workbench->createTime = time(); - $workbench->updateTime = time(); - $workbench->save(); - - // 根据类型创建对应的配置 - switch ($param['type']) { - case self::TYPE_AUTO_LIKE: // 自动点赞 - $config = new WorkbenchAutoLike; - $config->workbenchId = $workbench->id; - $config->interval = $param['interval']; - $config->maxLikes = $param['maxLikes']; - $config->startTime = $param['startTime']; - $config->endTime = $param['endTime']; - $config->contentTypes = json_encode($param['contentTypes']); - $config->devices = json_encode($param['deviceGroups']); - $config->friends = json_encode($param['wechatFriends']); - // $config->targetGroups = json_encode($param['targetGroups']); - // $config->tagOperator = $param['tagOperator']; - $config->friendMaxLikes = $param['friendMaxLikes']; - $config->friendTags = $param['friendTags']; - $config->enableFriendTags = $param['enableFriendTags']; - $config->createTime = time(); - $config->updateTime = time(); - $config->save(); - break; - case self::TYPE_MOMENTS_SYNC: // 朋友圈同步 - $config = new WorkbenchMomentsSync; - $config->workbenchId = $workbench->id; - $config->syncInterval = $param['syncInterval']; - $config->syncCount = $param['syncCount']; - $config->syncType = $param['syncType']; - $config->startTime = $param['startTime']; - $config->endTime = $param['endTime']; - $config->accountType = $param['accountType']; - $config->devices = json_encode($param['deviceGroups']); - $config->contentLibraries = json_encode($param['contentGroups'] ?? []); - $config->createTime = time(); - $config->updateTime = time(); - $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->save($groupPushData); - break; - case self::TYPE_GROUP_CREATE: // 自动建群 - $config = new WorkbenchGroupCreate; - $config->workbenchId = $workbench->id; - $config->planType = !empty($param['planType']) ? $param['planType'] : 0; - $config->executorId = !empty($param['executorId']) ? $param['executorId'] : 0; - - $config->devices = json_encode($param['deviceGroups'] ?? [], JSON_UNESCAPED_UNICODE); - $config->startTime = $param['startTime'] ?? ''; - $config->endTime = $param['endTime'] ?? ''; - $config->groupSizeMin = intval($param['groupSizeMin'] ?? 3); - $config->groupSizeMax = intval($param['groupSizeMax'] ?? 38); - $config->maxGroupsPerDay = intval($param['maxGroupsPerDay'] ?? 20); - $config->groupNameTemplate = $param['groupNameTemplate'] ?? ''; - $config->groupDescription = $param['groupDescription'] ?? ''; - $config->poolGroups = json_encode($param['poolGroups'] ?? [], JSON_UNESCAPED_UNICODE); - $config->wechatGroups = json_encode($param['wechatGroups'] ?? [], JSON_UNESCAPED_UNICODE); - - // 处理群管理员:如果启用了群管理员且有指定管理员,则保存到admins字段 - $admins = []; - if (!empty($param['groupAdminEnabled']) && !empty($param['groupAdminWechatId'])) { - // 如果groupAdminWechatId是数组,取第一个;如果是单个值,直接使用 - $adminWechatId = is_array($param['groupAdminWechatId']) ? $param['groupAdminWechatId'][0] : $param['groupAdminWechatId']; - // 如果是好友ID,直接添加到admins;如果是wechatId,需要转换为好友ID - if (is_numeric($adminWechatId)) { - $admins[] = intval($adminWechatId); - } else { - // 如果是wechatId字符串,需要查询对应的好友ID - $friend = Db::table('s2_wechat_friend')->where('wechatId', $adminWechatId)->find(); - if ($friend) { - $admins[] = intval($friend['id']); - } - } - } - // 如果传入了admins参数,优先使用(兼容旧逻辑) - if (!empty($param['admins']) && is_array($param['admins'])) { - $admins = array_merge($admins, $param['admins']); - } - $config->admins = json_encode(array_unique($admins), JSON_UNESCAPED_UNICODE); - - $config->fixedWechatIds = json_encode($param['fixedWechatIds'] ?? [], JSON_UNESCAPED_UNICODE); - $config->createTime = time(); - $config->updateTime = time(); - $config->save(); - break; - case self::TYPE_TRAFFIC_DISTRIBUTION: // 流量分发 - $config = new WorkbenchTrafficConfig; - $config->workbenchId = $workbench->id; - $config->distributeType = $param['distributeType']; - $config->maxPerDay = $param['maxPerDay']; - $config->timeType = $param['timeType']; - $config->startTime = $param['startTime']; - $config->endTime = $param['endTime']; - $config->devices = json_encode($param['deviceGroups'], JSON_UNESCAPED_UNICODE); - $config->pools = json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE); - $config->account = json_encode($param['accountGroups'], JSON_UNESCAPED_UNICODE); - $config->createTime = time(); - $config->updateTime = time(); - $config->save(); - break; - case self::TYPE_IMPORT_CONTACT: //联系人导入 - $config = new WorkbenchImportContact; - $config->workbenchId = $workbench->id; - $config->devices = json_encode($param['deviceGroups'], JSON_UNESCAPED_UNICODE); - $config->pools = json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE); - $config->num = $param['num']; - $config->clearContact = $param['clearContact']; - $config->remark = $param['remark']; - $config->startTime = $param['startTime']; - $config->endTime = $param['endTime']; - $config->createTime = time(); - $config->save(); - break; - } - - Db::commit(); - return json(['code' => 200, 'msg' => '创建成功', 'data' => ['id' => $workbench->id]]); - } catch (\Exception $e) { - Db::rollback(); - return json(['code' => 500, 'msg' => '创建失败:' . $e->getMessage()]); - } - } - - /** - * 获取工作台列表 - * @return \think\response\Json - */ - public function getList() - { - $page = $this->request->param('page', 1); - $limit = $this->request->param('limit', 10); - $type = $this->request->param('type', ''); - $keyword = $this->request->param('keyword', ''); + // 计算时间范围 + $days = intval($timeRange); + $startTime = strtotime(date('Y-m-d 00:00:00', strtotime("-{$days} days"))); + $endTime = time(); + // 构建查询条件 $where = [ - ['companyId', '=', $this->request->userInfo['companyId']], - ['isDel', '=', 0] + ['wgpi.createTime', '>=', $startTime], + ['wgpi.createTime', '<=', $endTime] ]; - - if (empty($this->request->userInfo['isAdmin'])) { - $where[] = ['userId', '=', $this->request->userInfo['id']]; - } - - - - // 添加类型筛选 - if ($type !== '') { - $where[] = ['type', '=', $type]; - } - - // 添加名称模糊搜索 - if ($keyword !== '') { - $where[] = ['name', 'like', '%' . $keyword . '%']; - } - - // 定义关联关系 - $with = [ - 'autoLike' => function ($query) { - $query->field('workbenchId,interval,maxLikes,startTime,endTime,contentTypes,devices,friends'); - }, - 'momentsSync' => function ($query) { - $query->field('workbenchId,syncInterval,syncCount,syncType,startTime,endTime,accountType,devices,contentLibraries'); - }, - 'trafficConfig' => function ($query) { - $query->field('workbenchId,distributeType,maxPerDay,timeType,startTime,endTime,devices,pools,account'); - }, - 'groupPush' => function ($query) { - $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,admins'); - }, - 'importContact' => function ($query) { - $query->field('workbenchId,devices,pools,num,remarkType,remark,clearContact,startTime,endTime'); - }, - 'user' => function ($query) { - $query->field('id,username'); - } - ]; - - $list = Workbench::where($where) - ->with($with) - ->field('id,companyId,name,type,status,autoStart,userId,createTime,updateTime') - ->order('id', 'desc') - ->page($page, $limit) - ->select() - ->each(function ($item) { - // 处理配置信息 - switch ($item->type) { - case self::TYPE_AUTO_LIKE: - if (!empty($item->autoLike)) { - $item->config = $item->autoLike; - $item->config->devices = json_decode($item->config->devices, true); - $item->config->contentTypes = json_decode($item->config->contentTypes, true); - $item->config->friends = json_decode($item->config->friends, true); - - // 添加今日点赞数 - $startTime = strtotime(date('Y-m-d') . ' 00:00:00'); - $endTime = strtotime(date('Y-m-d') . ' 23:59:59'); - $todayLikeCount = Db::name('workbench_auto_like_item') - ->where('workbenchId', $item->id) - ->whereTime('createTime', 'between', [$startTime, $endTime]) - ->count(); - - // 添加总点赞数 - $totalLikeCount = Db::name('workbench_auto_like_item') - ->where('workbenchId', $item->id) - ->count(); - - $item->config->todayLikeCount = $todayLikeCount; - $item->config->totalLikeCount = $totalLikeCount; - } - unset($item->autoLike, $item->auto_like); - break; - case self::TYPE_MOMENTS_SYNC: - if (!empty($item->momentsSync)) { - $item->config = $item->momentsSync; - $item->config->devices = json_decode($item->config->devices, true); - $item->config->contentGroups = json_decode($item->config->contentLibraries, true); - //同步记录 - $sendNum = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $item->id])->count(); - $item->syncCount = $sendNum; - $lastTime = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $item->id])->order('id DESC')->value('createTime'); - $item->lastSyncTime = !empty($lastTime) ? date('Y-m-d H:i', $lastTime) : '--'; - - - // 获取内容库名称 - if (!empty($item->config->contentGroups)) { - $libraryNames = ContentLibrary::where('id', 'in', $item->config->contentGroups)->select(); - $item->config->contentGroupsOptions = $libraryNames; - } else { - $item->config->contentGroupsOptions = []; - } - } - 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 = []; - // 群推送不需要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->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; - case self::TYPE_GROUP_CREATE: - if (!empty($item->groupCreate)) { - $item->config = $item->groupCreate; - $item->config->devices = json_decode($item->config->devices, true); - $item->config->poolGroups = json_decode($item->config->poolGroups, true); - $item->config->wechatGroups = json_decode($item->config->wechatGroups, true); - $item->config->admins = json_decode($item->config->admins ?? '[]', true) ?: []; - - // 处理群管理员相关字段 - $item->config->groupAdminEnabled = !empty($item->config->admins) ? 1 : 0; - - if (!empty($item->config->admins)) { - $adminOptions = Db::table('s2_wechat_friend')->alias('wf') - ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') - ->where('wf.id', 'in', $item->config->admins) - ->order('wf.id', 'desc') - ->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(); - $item->config->adminsOptions = $adminOptions; - // 如果有管理员,设置groupAdminWechatId为第一个管理员的ID(用于前端回显) - $item->config->groupAdminWechatId = !empty($item->config->admins) ? $item->config->admins[0] : null; - } else { - $item->config->adminsOptions = []; - $item->config->groupAdminWechatId = null; - } - } - unset($item->groupCreate, $item->group_create); - break; - case self::TYPE_TRAFFIC_DISTRIBUTION: - if (!empty($item->trafficConfig)) { - $item->config = $item->trafficConfig; - $item->config->devices = json_decode($item->config->devices, true); - $item->config->poolGroups = json_decode($item->config->pools, true); - $item->config->account = json_decode($item->config->account, true); - $config_item = Db::name('workbench_traffic_config_item')->where(['workbenchId' => $item->id])->order('id DESC')->find(); - $item->config->lastUpdated = !empty($config_item) ? date('Y-m-d H:i', $config_item['createTime']) : '--'; - - //统计 - $labels = $item->config->poolGroups; - $totalUsers = Db::table('s2_wechat_friend')->alias('wf') - ->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left') - ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') - ->where([ - ['wf.isDeleted', '=', 0], - ['sa.departmentId', '=', $item->companyId] - ]) - ->whereIn('wa.currentDeviceId', $item->config->devices); - - if (!empty($labels) && count($labels) > 0) { - $totalUsers = $totalUsers->where(function ($q) use ($labels) { - foreach ($labels as $label) { - $q->whereOrRaw("JSON_CONTAINS(wf.labels, '\"{$label}\"')"); - } - }); - } - - $totalUsers = $totalUsers->count(); - $totalAccounts = count($item->config->account); - $dailyAverage = Db::name('workbench_traffic_config_item') - ->where('workbenchId', $item->id) - ->count(); - $day = (time() - strtotime($item->createTime)) / 86400; - $day = intval($day); - if ($dailyAverage > 0 && $totalAccounts > 0 && $day > 0) { - $dailyAverage = $dailyAverage / $totalAccounts / $day; - } - $item->config->total = [ - 'dailyAverage' => intval($dailyAverage), - 'totalAccounts' => $totalAccounts, - 'deviceCount' => count($item->config->devices), - 'poolCount' => !empty($item->config->poolGroups) ? count($item->config->poolGroups) : 'ALL', - 'totalUsers' => $totalUsers >> 0 - ]; - } - unset($item->trafficConfig, $item->traffic_config); - break; - - case self::TYPE_IMPORT_CONTACT: - if (!empty($item->importContact)) { - $item->config = $item->importContact; - $item->config->devices = json_decode($item->config->devices, true); - $item->config->poolGroups = json_decode($item->config->pools, true); - } - unset($item->importContact, $item->import_contact); - break; - } - // 添加创建人名称 - $item['creatorName'] = $item->user ? $item->user->username : ''; - unset($item['user']); // 移除关联数据 - return $item; - }); - - $total = Workbench::where($where)->count(); - - return json([ - 'code' => 200, - 'msg' => '获取成功', - 'data' => [ - 'list' => $list, - 'total' => $total, - 'page' => $page, - 'limit' => $limit - ] - ]); - } - - /** - * 获取工作台详情 - * @param int $id 工作台ID - * @return \think\response\Json - */ - public function detail() { $id = $this->request->param('id', ''); @@ -1001,7 +587,7 @@ class WorkbenchGroupPushController extends Controller case self::TYPE_GROUP_PUSH: $config = WorkbenchGroupPush::where('workbenchId', $param['id'])->find(); if ($config) { - $ownerWechatIds = $this->normalizeOwnerWechatIds($param['ownerWechatIds'] ?? null, $config); + $ownerWechatIds = $this->normalizeOwnerWechatIds($param['ownerWechatIds'] ?? null, $config, $param['deviceGroups'] ?? []); $groupPushData = $this->prepareGroupPushData($param, $ownerWechatIds, $config); $groupPushData['updateTime'] = time(); $config->save($groupPushData); @@ -1899,15 +1485,24 @@ class WorkbenchGroupPushController extends Controller * 规范化客服微信ID列表 * @param mixed $ownerWechatIds * @param WorkbenchGroupPush|null $originalConfig + * @param array $deviceGroups 设备ID数组 * @return array * @throws \Exception */ - private function normalizeOwnerWechatIds($ownerWechatIds, WorkbenchGroupPush $originalConfig = null): array + private function normalizeOwnerWechatIds($ownerWechatIds, WorkbenchGroupPush $originalConfig = null, array $deviceGroups = []): array { + // 处理设备ID数组 + $deviceGroupsList = $this->extractIdList($deviceGroups, '设备参数格式错误'); + if ($ownerWechatIds === null) { $existing = $originalConfig ? $this->decodeJsonArray($originalConfig->ownerWechatIds ?? []) : []; - if (empty($existing)) { - throw new \Exception('请至少选择一个客服微信'); + // 如果原有配置为空,且没有传设备ID,则报错 + if (empty($existing) && empty($deviceGroupsList)) { + throw new \Exception('请至少选择一个客服微信或设备'); + } + // 如果原有配置为空但有设备ID,返回空数组(允许使用设备ID) + if (empty($existing) && !empty($deviceGroupsList)) { + return []; } return $existing; } @@ -1917,8 +1512,9 @@ class WorkbenchGroupPushController extends Controller } $normalized = $this->extractIdList($ownerWechatIds, '客服参数格式错误'); - if (empty($normalized)) { - throw new \Exception('请至少选择一个客服微信'); + // 如果 ownerWechatIds 为空,但传了设备ID,则允许(两个传一个即可) + if (empty($normalized) && empty($deviceGroupsList)) { + throw new \Exception('请至少选择一个客服微信或设备'); } return $normalized; } @@ -1960,6 +1556,14 @@ class WorkbenchGroupPushController extends Controller 'isRandomTemplate' => $this->toBoolInt($this->getParamValue($param, 'isRandomTemplate', $originalConfig->isRandomTemplate ?? 0)), 'ownerWechatIds' => json_encode($ownerWechatIds, JSON_UNESCAPED_UNICODE), ]; + + // 处理设备ID数组(deviceGroups),保存到 devices 字段 + $deviceGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->devices ?? []) : []; + $deviceGroupsParam = $this->getParamValue($param, 'deviceGroups', null); + $deviceGroups = $deviceGroupsParam !== null + ? $this->extractIdList($deviceGroupsParam, '设备参数格式错误') + : $deviceGroupsExisting; + $data['devices'] = json_encode($deviceGroups, JSON_UNESCAPED_UNICODE); if ($data['friendIntervalMin'] > $data['friendIntervalMax']) { throw new \Exception('目标间最小间隔不能大于最大间隔'); @@ -1968,11 +1572,35 @@ class WorkbenchGroupPushController extends Controller throw new \Exception('消息间最小间隔不能大于最大间隔'); } - $contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : []; - $contentGroupsParam = $this->getParamValue($param, 'contentGroups', null); - $contentGroups = $contentGroupsParam !== null - ? $this->extractIdList($contentGroupsParam, '内容库参数格式错误') - : $contentGroupsExisting; + // 群公告(groupPushSubType = 2)时,contentGroups 可以为空,不需要验证 + if ($targetType === 1 && $groupPushSubType === 2) { + // 群公告可以不传内容库,允许为空,不进行任何验证 + $contentGroupsParam = $this->getParamValue($param, 'contentGroups', null); + if ($contentGroupsParam !== null) { + // 如果传入了参数,尝试解析,但不验证格式和是否为空 + try { + $contentGroups = $this->extractIdList($contentGroupsParam, '内容库参数格式错误'); + } catch (\Exception $e) { + // 群公告时忽略格式错误,使用空数组 + $contentGroups = []; + } + } else { + // 如果没有传入参数,使用现有配置或空数组 + $contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : []; + $contentGroups = $contentGroupsExisting; + } + } else { + // 其他情况,正常处理并验证 + $contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : []; + $contentGroupsParam = $this->getParamValue($param, 'contentGroups', null); + $contentGroups = $contentGroupsParam !== null + ? $this->extractIdList($contentGroupsParam, '内容库参数格式错误') + : $contentGroupsExisting; + // 其他情况,内容库为必填 + if (empty($contentGroups)) { + throw new \Exception('请至少选择一个内容库'); + } + } $data['contentLibraries'] = json_encode($contentGroups, JSON_UNESCAPED_UNICODE); $postPushTagsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->postPushTags ?? []) : []; @@ -2150,64 +1778,6 @@ class WorkbenchGroupPushController extends Controller return false; } - - /** - * 获取通讯录导入记录列表 - * @return \think\response\Json - */ - public function getImportContact() - { - $page = $this->request->param('page', 1); - $limit = $this->request->param('limit', 10); - $workbenchId = $this->request->param('workbenchId', 0); - - $where = [ - ['wici.workbenchId', '=', $workbenchId] - ]; - - // 查询发布记录 - $list = Db::name('workbench_import_contact_item')->alias('wici') - ->join('traffic_pool tp', 'tp.id = wici.poolId', 'left') - ->join('traffic_source tc', 'tc.identifier = tp.identifier', 'left') - ->join('wechat_account wa', 'wa.wechatId = tp.wechatId', 'left') - ->field([ - 'wici.id', - 'wici.workbenchId', - 'wici.createTime', - 'tp.identifier', - 'tp.mobile', - 'tp.wechatId', - 'tc.name', - 'wa.nickName', - 'wa.avatar', - 'wa.alias', - ]) - ->where($where) - ->order('tc.name DESC,wici.createTime DESC') - ->group('tp.identifier') - ->page($page, $limit) - ->select(); - - foreach ($list as &$item) { - $item['createTime'] = date('Y-m-d H:i:s', $item['createTime']); - } - - - // 获取总记录数 - $total = Db::name('workbench_import_contact_item')->alias('wici') - ->where($where) - ->count(); - - return json([ - 'code' => 200, - 'msg' => '获取成功', - 'data' => [ - 'list' => $list, - 'total' => $total, - ] - ]); - } - /** * 获取群发统计数据 * @return \think\response\Json @@ -3116,15 +2686,302 @@ class WorkbenchGroupPushController extends Controller } /** - * 获取已创建的群列表(自动建群) - * @return \think\response\Json + * 规范化客服微信ID列表 + * @param mixed $ownerWechatIds + * @param WorkbenchGroupPush|null $originalConfig + * @param array $deviceGroups 设备ID数组 + * @return array + * @throws \Exception */ - public function getCreatedGroupsList() + private function normalizeOwnerWechatIds($ownerWechatIds, WorkbenchGroupPush $originalConfig = null, array $deviceGroups = []): array { - $workbenchId = $this->request->param('workbenchId', 0); - $page = $this->request->param('page', 1); - $limit = $this->request->param('limit', 100); - $keyword = $this->request->param('keyword', ''); + // 处理设备ID数组 + $deviceGroupsList = $this->extractIdList($deviceGroups, '设备参数格式错误'); + + if ($ownerWechatIds === null) { + $existing = $originalConfig ? $this->decodeJsonArray($originalConfig->ownerWechatIds ?? []) : []; + // 如果原有配置为空,且没有传设备ID,则报错 + if (empty($existing) && empty($deviceGroupsList)) { + throw new \Exception('请至少选择一个客服微信或设备'); + } + // 如果原有配置为空但有设备ID,返回空数组(允许使用设备ID) + if (empty($existing) && !empty($deviceGroupsList)) { + return []; + } + return $existing; + } + + if (!is_array($ownerWechatIds)) { + throw new \Exception('客服参数格式错误'); + } + + $normalized = $this->extractIdList($ownerWechatIds, '客服参数格式错误'); + // 如果 ownerWechatIds 为空,但传了设备ID,则允许(两个传一个即可) + if (empty($normalized) && empty($deviceGroupsList)) { + 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), + ]; + + // 处理设备ID数组(deviceGroups),保存到 devices 字段 + $deviceGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->devices ?? []) : []; + $deviceGroupsParam = $this->getParamValue($param, 'deviceGroups', null); + $deviceGroups = $deviceGroupsParam !== null + ? $this->extractIdList($deviceGroupsParam, '设备参数格式错误') + : $deviceGroupsExisting; + $data['devices'] = json_encode($deviceGroups, JSON_UNESCAPED_UNICODE); + + if ($data['friendIntervalMin'] > $data['friendIntervalMax']) { + throw new \Exception('目标间最小间隔不能大于最大间隔'); + } + if ($data['messageIntervalMin'] > $data['messageIntervalMax']) { + throw new \Exception('消息间最小间隔不能大于最大间隔'); + } + + // 群公告(groupPushSubType = 2)时,contentGroups 可以为空,不需要验证 + if ($targetType === 1 && $groupPushSubType === 2) { + // 群公告可以不传内容库,允许为空,不进行任何验证 + $contentGroupsParam = $this->getParamValue($param, 'contentGroups', null); + if ($contentGroupsParam !== null) { + // 如果传入了参数,尝试解析,但不验证格式和是否为空 + try { + $contentGroups = $this->extractIdList($contentGroupsParam, '内容库参数格式错误'); + } catch (\Exception $e) { + // 群公告时忽略格式错误,使用空数组 + $contentGroups = []; + } + } else { + // 如果没有传入参数,使用现有配置或空数组 + $contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : []; + $contentGroups = $contentGroupsExisting; + } + } else { + // 其他情况,正常处理并验证 + $contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : []; + $contentGroupsParam = $this->getParamValue($param, 'contentGroups', null); + $contentGroups = $contentGroupsParam !== null + ? $this->extractIdList($contentGroupsParam, '内容库参数格式错误') + : $contentGroupsExisting; + // 其他情况,内容库为必填 + if (empty($contentGroups)) { + throw new \Exception('请至少选择一个内容库'); + } + } + $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 要检测的内容 + * @return bool + */ + private function containsLink($content) + { + // 定义各种链接的正则表达式模式 + $patterns = [ + // HTTP/HTTPS链接 + '/https?:\/\/[^\s]+/i', + // 京东商品链接 + '/item\.jd\.com\/\d+/i', + // 京东短链接 + '/u\.jd\.com\/[a-zA-Z0-9]+/i', + // 淘宝商品链接 + '/item\.taobao\.com\/item\.htm\?id=\d+/i', + // 天猫商品链接 + '/detail\.tmall\.com\/item\.htm\?id=\d+/i', + // 淘宝短链接 + '/m\.tb\.cn\/[a-zA-Z0-9]+/i', + // 拼多多链接 + '/mobile\.yangkeduo\.com\/goods\.html\?goods_id=\d+/i', + // 苏宁易购链接 + '/product\.suning\.com\/\d+\/\d+\.html/i', + // 通用域名模式(包含常见电商域名) + '/(?:jd|taobao|tmall|yangkeduo|suning|amazon|dangdang)\.com[^\s]*/i', + // 通用短链接模式 + '/[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; + } +} if (empty($workbenchId)) { return json(['code' => 400, 'msg' => '工作台ID不能为空']); @@ -3247,12 +3104,7 @@ class WorkbenchGroupPushController extends Controller ] ]); } - - /** - * 获取已创建群的详情(自动建群) - * @return \think\response\Json - */ - public function getCreatedGroupDetail() +} { $workbenchId = $this->request->param('workbenchId', 0); $groupId = $this->request->param('groupId', 0); diff --git a/application/cunkebao/controller/workbench/WorkbenchHelperController.php b/application/cunkebao/controller/workbench/WorkbenchHelperController.php index 285fcab..11ffdc4 100644 --- a/application/cunkebao/controller/workbench/WorkbenchHelperController.php +++ b/application/cunkebao/controller/workbench/WorkbenchHelperController.php @@ -311,3 +311,4 @@ class WorkbenchHelperController extends Controller } } + diff --git a/application/cunkebao/controller/workbench/WorkbenchImportContactController.php b/application/cunkebao/controller/workbench/WorkbenchImportContactController.php index 5c1d5ea..6dc3b90 100644 --- a/application/cunkebao/controller/workbench/WorkbenchImportContactController.php +++ b/application/cunkebao/controller/workbench/WorkbenchImportContactController.php @@ -67,3 +67,4 @@ class WorkbenchImportContactController extends Controller } } + diff --git a/application/cunkebao/controller/workbench/WorkbenchMomentsController.php b/application/cunkebao/controller/workbench/WorkbenchMomentsController.php index 92d001a..b359d9e 100644 --- a/application/cunkebao/controller/workbench/WorkbenchMomentsController.php +++ b/application/cunkebao/controller/workbench/WorkbenchMomentsController.php @@ -123,3 +123,4 @@ class WorkbenchMomentsController extends Controller } } + diff --git a/application/cunkebao/controller/workbench/WorkbenchTrafficController.php b/application/cunkebao/controller/workbench/WorkbenchTrafficController.php index 284f01c..b6c5c4f 100644 --- a/application/cunkebao/controller/workbench/WorkbenchTrafficController.php +++ b/application/cunkebao/controller/workbench/WorkbenchTrafficController.php @@ -307,3 +307,4 @@ class WorkbenchTrafficController extends Controller } } + diff --git a/application/cunkebao/validate/Workbench.php b/application/cunkebao/validate/Workbench.php index 7225c5a..ffd9fae 100644 --- a/application/cunkebao/validate/Workbench.php +++ b/application/cunkebao/validate/Workbench.php @@ -47,7 +47,7 @@ class Workbench extends Validate 'wechatGroups' => 'checkGroupPushTarget|array|min:1', // 当targetType=1时必填 'wechatFriends' => 'checkFriendPushTarget|array', // 当targetType=2时可选(可以为空) 'ownerWechatId' => 'checkFriendPushService', // 当targetType=2且未选择好友/流量池时必填 - 'contentGroups' => 'requireIf:type,3|array|min:1', + 'contentGroups' => 'checkContentGroups|array', // 群推送时必填,但群公告时可以为空 // 群公告特有参数 'announcementContent' => 'checkAnnouncementContent|max:5000', // 群公告内容(当groupPushSubType=2时必填) 'enableAiRewrite' => 'checkEnableAiRewrite|in:0,1', // 是否启用AI智能话术改写 @@ -106,7 +106,7 @@ class Workbench extends Validate 'endTime.dateFormat' => '发布结束时间格式错误', 'accountGroups.requireIf' => '请选择账号类型', 'accountGroups.in' => '账号类型错误', - 'contentGroups.requireIf' => '请选择内容库', + 'contentGroups.checkContentGroups' => '群群发时必须选择内容库', 'contentGroups.array' => '内容库格式错误', // 群消息推送相关提示 'pushType.requireIf' => '请选择推送方式', @@ -383,4 +383,31 @@ class Workbench extends Validate } return true; } + + /** + * 验证内容库(群推送时必填,但群公告时可以为空) + */ + protected function checkContentGroups($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 + + // 群公告(groupPushSubType=2)时,内容库可以为空,不需要验证 + if ($targetType == 1 && $groupPushSubType == 2) { + // 群公告时允许为空,不进行验证 + return true; + } + + // 其他情况(群群发、好友推送),内容库必填 + if (!isset($value) || $value === null || $value === '') { + return false; + } + if (!is_array($value) || count($value) < 1) { + return false; + } + } + return true; + } } \ No newline at end of file diff --git a/public/doc/api_v1.md b/public/doc/api_v1.md index 95c8643..7a1c263 100644 --- a/public/doc/api_v1.md +++ b/public/doc/api_v1.md @@ -6,7 +6,7 @@ - **接口用途**:供第三方系统向【存客宝】上报客户线索(手机号 / 微信号等),用于后续的跟进、标签管理和画像分析。 - **接口协议**:HTTP - **请求方式**:`POST` -- **请求地址**: `http://ckbapi.quwanzhi.com/v1/api/scenarios` +- **请求地址**: `https://ckbapi.quwanzhi.com/v1/api/scenarios` > 具体 URL 以实际环境配置为准。