场景获客支持拉群
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
// 步骤定义 - 四个步骤
|
||||
// 步骤定义 - 五个步骤
|
||||
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
|
||||
import { GroupSelectionItem } from "@/components/GroupSelection/data";
|
||||
export const steps = [
|
||||
{ id: 1, title: "步骤一", subtitle: "基础设置" },
|
||||
{ id: 2, title: "步骤二", subtitle: "好友申请设置" },
|
||||
{ id: 3, title: "步骤三", subtitle: "渠道设置" },
|
||||
{ id: 4, title: "步骤四", subtitle: "消息设置" },
|
||||
{ id: 4, title: "步骤四", subtitle: "拉群设置" },
|
||||
{ id: 5, title: "步骤五", subtitle: "消息设置" },
|
||||
];
|
||||
|
||||
// 类型定义
|
||||
@@ -31,6 +32,10 @@ export interface FormData {
|
||||
wechatGroups: string[];
|
||||
wechatGroupsOptions: GroupSelectionItem[];
|
||||
messagePlans: any[];
|
||||
// 拉群设置
|
||||
groupInviteEnabled?: boolean;
|
||||
groupName?: string;
|
||||
fixedGroupMembers?: any[]; // 固定群成员,使用好友选择组件结构
|
||||
// 分销相关
|
||||
distributionEnabled?: boolean;
|
||||
// 选中的分销渠道ID列表(前端使用,提交时转为 distributionChannels)
|
||||
@@ -72,6 +77,9 @@ export const defFormData: FormData = {
|
||||
wechatGroupsOptions: [],
|
||||
contentGroups: [],
|
||||
contentGroupsOptions: [],
|
||||
groupInviteEnabled: false,
|
||||
groupName: "",
|
||||
fixedGroupMembers: [],
|
||||
distributionEnabled: false,
|
||||
distributionChannelIds: [],
|
||||
distributionChannelsOptions: [],
|
||||
|
||||
@@ -5,6 +5,7 @@ import NavCommon from "@/components/NavCommon";
|
||||
import BasicSettings from "./steps/BasicSettings";
|
||||
import FriendRequestSettings from "./steps/FriendRequestSettings";
|
||||
import DistributionSettings from "./steps/DistributionSettings";
|
||||
import GroupSettings from "./steps/GroupSettings";
|
||||
import MessageSettings from "./steps/MessageSettings";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import StepIndicator from "@/components/StepIndicator";
|
||||
@@ -112,6 +113,15 @@ export default function NewPlan() {
|
||||
wechatGroupsOptions: detail.wechatGroupsOptions ?? [],
|
||||
contentGroups: detail.contentGroups ?? [],
|
||||
contentGroupsOptions: detail.contentGroupsOptions ?? [],
|
||||
// 拉群设置
|
||||
groupInviteEnabled: detail.groupInviteEnabled ?? false,
|
||||
groupName: detail.groupName ?? "",
|
||||
// 优先使用后端返回的 options(完整好友信息),否则退回到 ID 数组或旧字段
|
||||
fixedGroupMembers:
|
||||
detail.groupFixedMembersOptions ??
|
||||
detail.fixedGroupMembers ??
|
||||
detail.groupFixedMembers ??
|
||||
[],
|
||||
status: detail.status ?? 0,
|
||||
messagePlans: detail.messagePlans ?? [],
|
||||
// 分销相关数据回填
|
||||
@@ -196,11 +206,26 @@ export default function NewPlan() {
|
||||
submitData.distributionEnabled = false;
|
||||
}
|
||||
|
||||
// 拉群设置字段转换
|
||||
if (formData.groupInviteEnabled) {
|
||||
submitData.groupInviteEnabled = true;
|
||||
submitData.groupName = formData.groupName || "";
|
||||
// 后端期望的字段:groupFixedMembers,使用好友ID数组
|
||||
submitData.groupFixedMembers = (formData.fixedGroupMembers || []).map(
|
||||
(f: any) => f.id,
|
||||
);
|
||||
} else {
|
||||
submitData.groupInviteEnabled = false;
|
||||
submitData.groupName = "";
|
||||
submitData.groupFixedMembers = [];
|
||||
}
|
||||
|
||||
// 移除前端使用的字段,避免提交到后端
|
||||
delete submitData.distributionChannelIds;
|
||||
delete submitData.distributionChannelsOptions;
|
||||
delete submitData.distributionCustomerReward;
|
||||
delete submitData.distributionAddReward;
|
||||
delete submitData.fixedGroupMembers;
|
||||
|
||||
if (isEdit && planId) {
|
||||
// 编辑:拼接后端需要的完整参数
|
||||
@@ -272,6 +297,8 @@ export default function NewPlan() {
|
||||
/>
|
||||
);
|
||||
case 4:
|
||||
return <GroupSettings formData={formData} onChange={onChange} />;
|
||||
case 5:
|
||||
return <MessageSettings formData={formData} onChange={onChange} />;
|
||||
default:
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import React from "react";
|
||||
import { Input, Switch } from "antd";
|
||||
import styles from "./base.module.scss";
|
||||
import FriendSelection from "@/components/FriendSelection";
|
||||
import type { FriendSelectionItem } from "@/components/FriendSelection/data";
|
||||
|
||||
interface GroupSettingsProps {
|
||||
formData: any;
|
||||
onChange: (data: any) => void;
|
||||
}
|
||||
|
||||
const GroupSettings: React.FC<GroupSettingsProps> = ({ formData, onChange }) => {
|
||||
const handleToggleGroupInvite = (value: boolean) => {
|
||||
onChange({
|
||||
...formData,
|
||||
groupInviteEnabled: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleGroupNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange({
|
||||
...formData,
|
||||
groupName: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleFixedMembersSelect = (friends: FriendSelectionItem[]) => {
|
||||
onChange({
|
||||
...formData,
|
||||
fixedGroupMembers: friends,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles["basic-container"]}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
border: "1px solid #f0f0f0",
|
||||
background: "#fff",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontWeight: 500,
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
拉群设置
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "#8c8c8c" }}>
|
||||
开启后,可配置群名称和固定群成员,将用户引导到指定微信群
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={!!formData.groupInviteEnabled}
|
||||
onChange={handleToggleGroupInvite}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{formData.groupInviteEnabled && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
<Input
|
||||
placeholder="请输入群名称"
|
||||
value={formData.groupName || ""}
|
||||
onChange={handleGroupNameChange}
|
||||
/>
|
||||
{/* 固定成员选择,复用好友选择组件 */}
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 13,
|
||||
marginBottom: 4,
|
||||
color: "#595959",
|
||||
}}
|
||||
>
|
||||
固定群成员
|
||||
</div>
|
||||
<FriendSelection
|
||||
selectedOptions={
|
||||
(formData.fixedGroupMembers || []) as FriendSelectionItem[]
|
||||
}
|
||||
onSelect={handleFixedMembersSelect}
|
||||
placeholder="选择固定群成员"
|
||||
showSelectedList={true}
|
||||
// 根据已选择设备过滤好友列表
|
||||
deviceIds={formData.deviceGroups || []}
|
||||
enableDeviceFilter={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupSettings;
|
||||
1
Server/.gitignore
vendored
1
Server/.gitignore
vendored
@@ -16,3 +16,4 @@ nginx.htaccess
|
||||
.cursor/
|
||||
thinkphp/
|
||||
public/static/
|
||||
*.log
|
||||
|
||||
@@ -1017,8 +1017,8 @@ class WebSocketController extends BaseController
|
||||
"seq" => time(),
|
||||
"wechatAccountId" => $data['wechatAccountId'],
|
||||
"chatroomName" => $data['chatroomName'],
|
||||
// "wechatFriendIds" => $data['wechatFriendIds']
|
||||
"wechatFriendIds" => [17453051,17453058]
|
||||
"wechatFriendIds" => $data['wechatFriendIds']
|
||||
//"wechatFriendIds" => [17453051,17453058]
|
||||
];
|
||||
$message = $this->sendMessage($params,false);
|
||||
return json_encode(['code' => 200, 'msg' => '群聊创建成功', 'data' => $message]);
|
||||
|
||||
@@ -124,6 +124,86 @@ class GetAddFriendPlanDetailV1Controller extends Controller
|
||||
$msgConf = json_decode($plan['msgConf'], true) ?: [];
|
||||
$tagConf = json_decode($plan['tagConf'], true) ?: [];
|
||||
|
||||
// 处理拉群固定成员为数组,并构造下拉 options
|
||||
if (!empty($plan['groupFixedMembers'])) {
|
||||
$fixedMembers = json_decode($plan['groupFixedMembers'], true);
|
||||
$plan['groupFixedMembers'] = is_array($fixedMembers) ? $fixedMembers : [];
|
||||
} else {
|
||||
$plan['groupFixedMembers'] = [];
|
||||
}
|
||||
// groupFixedMembersOptions:参考 workbench 中好友 options 的结构,返回完整好友信息
|
||||
$groupFixedMembersOptions = [];
|
||||
if (!empty($plan['groupFixedMembers'])) {
|
||||
$friendIds = [];
|
||||
$manualIds = [];
|
||||
foreach ($plan['groupFixedMembers'] as $member) {
|
||||
if (is_numeric($member)) {
|
||||
$friendIds[] = intval($member);
|
||||
} else {
|
||||
$manualIds[] = $member;
|
||||
}
|
||||
}
|
||||
|
||||
// 数字 ID:从 s2_wechat_friend 中查询好友信息
|
||||
if (!empty($friendIds)) {
|
||||
$friendList = Db::table('s2_wechat_friend')->alias('wf')
|
||||
->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
|
||||
->join(['s2_company_account' => 'ca'], 'ca.id = wf.accountId', 'left')
|
||||
->where('wf.id', 'in', $friendIds)
|
||||
->order('wf.id', 'desc')
|
||||
->field('wf.id,wf.wechatId,wf.nickname,wf.avatar,wf.alias,wf.gender,wf.phone,wa.nickName as accountNickname,ca.userName as account,ca.realName as username,wf.createTime,wf.updateTime,wf.deleteTime,wf.ownerWechatId')
|
||||
->select();
|
||||
|
||||
// 获取群主信息,格式化时间
|
||||
foreach ($friendList as &$friend) {
|
||||
if (!empty($friend['ownerWechatId'])) {
|
||||
$owner = Db::name('wechat_account')
|
||||
->where('wechatId', $friend['ownerWechatId'])
|
||||
->field('nickName,alias')
|
||||
->find();
|
||||
$friend['ownerNickname'] = $owner['nickName'] ?? '';
|
||||
$friend['ownerAlias'] = $owner['alias'] ?? '';
|
||||
} else {
|
||||
$friend['ownerNickname'] = '';
|
||||
$friend['ownerAlias'] = '';
|
||||
}
|
||||
$friend['isManual'] = '';
|
||||
$friend['createTime'] = !empty($friend['createTime']) ? date('Y-m-d H:i:s', $friend['createTime']) : '';
|
||||
$friend['updateTime'] = !empty($friend['updateTime']) ? date('Y-m-d H:i:s', $friend['updateTime']) : '';
|
||||
$friend['deleteTime'] = !empty($friend['deleteTime']) ? date('Y-m-d H:i:s', $friend['deleteTime']) : '';
|
||||
}
|
||||
unset($friend);
|
||||
|
||||
$groupFixedMembersOptions = array_merge($groupFixedMembersOptions, $friendList);
|
||||
}
|
||||
|
||||
// 手动 ID:仅返回基础结构,标记 isManual=1
|
||||
if (!empty($manualIds)) {
|
||||
foreach ($manualIds as $mid) {
|
||||
$groupFixedMembersOptions[] = [
|
||||
'id' => $mid,
|
||||
'wechatId' => $mid,
|
||||
'nickname' => $mid,
|
||||
'avatar' => '',
|
||||
'alias' => '',
|
||||
'gender' => 0,
|
||||
'phone' => '',
|
||||
'accountNickname' => '',
|
||||
'account' => '',
|
||||
'username' => '',
|
||||
'ownerNickname' => '',
|
||||
'ownerAlias' => '',
|
||||
'ownerWechatId' => '',
|
||||
'createTime' => '',
|
||||
'updateTime' => '',
|
||||
'deleteTime' => '',
|
||||
'isManual' => 1,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
$sceneConf['groupFixedMembersOptions'] = $groupFixedMembersOptions;
|
||||
|
||||
// 处理分销配置
|
||||
$distributionConfig = $sceneConf['distribution'] ?? [
|
||||
'enabled' => false,
|
||||
|
||||
@@ -69,6 +69,21 @@ class PostCreateAddFriendPlanV1Controller extends BaseController
|
||||
return ResponseHelper::error('请选择设备', 400);
|
||||
}
|
||||
|
||||
// 拉群配置校验
|
||||
// groupInviteEnabled:拉群开关(0/1)
|
||||
// groupName:群名称
|
||||
// groupFixedMembers:固定成员(数组)
|
||||
$groupInviteEnabled = !empty($params['groupInviteEnabled']) ? 1 : 0;
|
||||
if ($groupInviteEnabled) {
|
||||
if (empty($params['groupName'])) {
|
||||
return ResponseHelper::error('拉群群名不能为空', 400);
|
||||
}
|
||||
|
||||
if (empty($params['groupFixedMembers']) || !is_array($params['groupFixedMembers'])) {
|
||||
return ResponseHelper::error('固定成员不能为空', 400);
|
||||
}
|
||||
}
|
||||
|
||||
$companyId = $this->getUserInfo('companyId');
|
||||
|
||||
// 处理分销配置
|
||||
@@ -114,7 +129,11 @@ class PostCreateAddFriendPlanV1Controller extends BaseController
|
||||
$sceneConf['distributionEnabled'],
|
||||
$sceneConf['distributionChannels'],
|
||||
$sceneConf['customerRewardAmount'],
|
||||
$sceneConf['addFriendRewardAmount']
|
||||
$sceneConf['addFriendRewardAmount'],
|
||||
// 拉群相关字段单独存表,不放到 sceneConf
|
||||
$sceneConf['groupInviteEnabled'],
|
||||
$sceneConf['groupName'],
|
||||
$sceneConf['groupFixedMembers']
|
||||
);
|
||||
|
||||
// 将分销配置添加到sceneConf中
|
||||
@@ -131,6 +150,12 @@ class PostCreateAddFriendPlanV1Controller extends BaseController
|
||||
'userId' => $this->getUserInfo('id'),
|
||||
'companyId' => $this->getUserInfo('companyId'),
|
||||
'status' => !empty($params['status']) ? 1 : 0,
|
||||
// 拉群配置
|
||||
'groupInviteEnabled' => $groupInviteEnabled,
|
||||
'groupName' => $params['groupName'] ?? '',
|
||||
'groupFixedMembers' => !empty($params['groupFixedMembers'])
|
||||
? json_encode($params['groupFixedMembers'], JSON_UNESCAPED_UNICODE)
|
||||
: json_encode([], JSON_UNESCAPED_UNICODE),
|
||||
'apiKey' => $this->generateApiKey(), // 生成API密钥
|
||||
'createTime' => time(),
|
||||
'updateTime' => time(),
|
||||
|
||||
@@ -39,6 +39,18 @@ class PostUpdateAddFriendPlanV1Controller extends BaseController
|
||||
return ResponseHelper::error('请选择设备', 400);
|
||||
}
|
||||
|
||||
// 拉群配置校验
|
||||
$groupInviteEnabled = !empty($params['groupInviteEnabled']) ? 1 : 0;
|
||||
if ($groupInviteEnabled) {
|
||||
if (empty($params['groupName'])) {
|
||||
return ResponseHelper::error('拉群群名不能为空', 400);
|
||||
}
|
||||
|
||||
if (empty($params['groupFixedMembers']) || !is_array($params['groupFixedMembers'])) {
|
||||
return ResponseHelper::error('固定成员不能为空', 400);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查计划是否存在
|
||||
$plan = Db::name('customer_acquisition_task')
|
||||
->where('id', $params['planId'])
|
||||
@@ -94,7 +106,11 @@ class PostUpdateAddFriendPlanV1Controller extends BaseController
|
||||
$sceneConf['distributionEnabled'],
|
||||
$sceneConf['distributionChannels'],
|
||||
$sceneConf['customerRewardAmount'],
|
||||
$sceneConf['addFriendRewardAmount']
|
||||
$sceneConf['addFriendRewardAmount'],
|
||||
// 拉群相关字段单独存表,不放到 sceneConf
|
||||
$sceneConf['groupInviteEnabled'],
|
||||
$sceneConf['groupName'],
|
||||
$sceneConf['groupFixedMembers']
|
||||
);
|
||||
|
||||
// 将分销配置添加到sceneConf中
|
||||
@@ -109,6 +125,12 @@ class PostUpdateAddFriendPlanV1Controller extends BaseController
|
||||
'msgConf' => json_encode($msgConf, JSON_UNESCAPED_UNICODE),
|
||||
'tagConf' => json_encode($tagConf, JSON_UNESCAPED_UNICODE),
|
||||
'status' => !empty($params['status']) ? 1 : 0,
|
||||
// 拉群配置
|
||||
'groupInviteEnabled' => $groupInviteEnabled,
|
||||
'groupName' => $params['groupName'] ?? '',
|
||||
'groupFixedMembers' => !empty($params['groupFixedMembers'])
|
||||
? json_encode($params['groupFixedMembers'], JSON_UNESCAPED_UNICODE)
|
||||
: json_encode([], JSON_UNESCAPED_UNICODE),
|
||||
'updateTime' => time(),
|
||||
];
|
||||
|
||||
|
||||
@@ -505,7 +505,7 @@ class WorkbenchController extends Controller
|
||||
$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');
|
||||
$query->field('workbenchId,devices,startTime,endTime,groupSizeMin,groupSizeMax,maxGroupsPerDay,groupNameTemplate,groupDescription,poolGroups,wechatGroups,admins,executorId');
|
||||
},
|
||||
'importContact' => function ($query) {
|
||||
$query->field('workbenchId,devices,pools,num,remarkType,remark,clearContact,startTime,endTime');
|
||||
@@ -642,6 +642,70 @@ class WorkbenchController extends Controller
|
||||
'totalMembersCount' => $totalMembersCount
|
||||
];
|
||||
|
||||
// 如果 executorId 有值,查询设备详情(格式和 deviceGroupsOptions 一样,但返回一维数组)
|
||||
$executorId = !empty($workbench->config->executorId) ? intval($workbench->config->executorId) : 0;
|
||||
$executor = null;
|
||||
if (!empty($executorId)) {
|
||||
// 查询设备基本信息
|
||||
$device = Db::table('s2_device')
|
||||
->where('id', $executorId)
|
||||
->where('isDeleted', 0)
|
||||
->field('id,imei,memo,alive,wechatAccounts')
|
||||
->find();
|
||||
|
||||
if (!empty($device)) {
|
||||
// 查询关联的微信账号(通过 currentDeviceId)
|
||||
$wechatAccount = Db::table('s2_wechat_account')
|
||||
->where('currentDeviceId', $executorId)
|
||||
->field('wechatId,nickname,alias,avatar,totalFriend')
|
||||
->find();
|
||||
|
||||
// 解析 wechatAccounts JSON 字段
|
||||
$wechatAccountsJson = [];
|
||||
if (!empty($device['wechatAccounts'])) {
|
||||
$wechatAccountsJson = json_decode($device['wechatAccounts'], true);
|
||||
if (!is_array($wechatAccountsJson)) {
|
||||
$wechatAccountsJson = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 优先使用 s2_wechat_account 表的数据,如果没有则使用 wechatAccounts JSON 中的第一个
|
||||
$wechatId = '';
|
||||
$nickname = '';
|
||||
$alias = '';
|
||||
$avatar = '';
|
||||
$totalFriend = 0;
|
||||
|
||||
if (!empty($wechatAccount)) {
|
||||
$wechatId = $wechatAccount['wechatId'] ?? '';
|
||||
$nickname = $wechatAccount['nickname'] ?? '';
|
||||
$alias = $wechatAccount['alias'] ?? '';
|
||||
$avatar = $wechatAccount['avatar'] ?? '';
|
||||
$totalFriend = intval($wechatAccount['totalFriend'] ?? 0);
|
||||
} elseif (!empty($wechatAccountsJson) && is_array($wechatAccountsJson) && count($wechatAccountsJson) > 0) {
|
||||
$firstWechat = $wechatAccountsJson[0];
|
||||
$wechatId = $firstWechat['wechatId'] ?? '';
|
||||
$nickname = $firstWechat['wechatNickname'] ?? '';
|
||||
$alias = $firstWechat['alias'] ?? '';
|
||||
$avatar = $firstWechat['wechatAvatar'] ?? '';
|
||||
$totalFriend = 0; // JSON 中没有 totalFriend 字段
|
||||
}
|
||||
|
||||
$executor = [
|
||||
'id' => $device['id'],
|
||||
'imei' => $device['imei'] ?? '',
|
||||
'memo' => $device['memo'] ?? '',
|
||||
'alive' => $device['alive'] ?? 0,
|
||||
'wechatId' => $wechatId,
|
||||
'nickname' => $nickname,
|
||||
'alias' => $alias,
|
||||
'avatar' => $avatar,
|
||||
'totalFriend' => $totalFriend
|
||||
];
|
||||
}
|
||||
}
|
||||
$workbench->config->executor = $executor;
|
||||
|
||||
unset($workbench->groupCreate, $workbench->group_create);
|
||||
}
|
||||
break;
|
||||
@@ -725,23 +789,83 @@ class WorkbenchController extends Controller
|
||||
|
||||
//获取设备信息
|
||||
if (!empty($workbench->config->deviceGroups)) {
|
||||
$deviceList = DeviceModel::alias('d')
|
||||
->field([
|
||||
'd.id', 'd.imei', 'd.memo', 'd.alive',
|
||||
'l.wechatId',
|
||||
'a.nickname', 'a.alias', 'a.avatar', 'a.alias', '0 totalFriend'
|
||||
])
|
||||
->leftJoin('device_wechat_login l', 'd.id = l.deviceId and l.alive =' . DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE . ' and l.companyId = d.companyId')
|
||||
->leftJoin('wechat_account a', 'l.wechatId = a.wechatId')
|
||||
->whereIn('d.id', $workbench->config->deviceGroups)
|
||||
->order('d.id desc')
|
||||
// 查询设备基本信息(包含 wechatAccounts JSON 字段)
|
||||
$devices = Db::table('s2_device')
|
||||
->whereIn('id', $workbench->config->deviceGroups)
|
||||
->where('isDeleted', 0)
|
||||
->field('id,imei,memo,alive,wechatAccounts')
|
||||
->order('id desc')
|
||||
->select();
|
||||
|
||||
foreach ($deviceList as &$device) {
|
||||
$curstomer = WechatCustomerModel::field('friendShip')->where(['wechatId' => $device['wechatId']])->find();
|
||||
$device['totalFriend'] = $curstomer->friendShip->totalFriend ?? 0;
|
||||
|
||||
$deviceList = [];
|
||||
if (!empty($devices)) {
|
||||
// 批量查询关联的微信账号(通过 currentDeviceId)
|
||||
$deviceIds = array_column($devices, 'id');
|
||||
$wechatAccounts = Db::table('s2_wechat_account')
|
||||
->whereIn('currentDeviceId', $deviceIds)
|
||||
->field('currentDeviceId,wechatId,nickname,alias,avatar,totalFriend')
|
||||
->select();
|
||||
|
||||
// 将微信账号按设备ID分组
|
||||
$wechatAccountsMap = [];
|
||||
foreach ($wechatAccounts as $wa) {
|
||||
$deviceId = $wa['currentDeviceId'];
|
||||
if (!isset($wechatAccountsMap[$deviceId])) {
|
||||
$wechatAccountsMap[$deviceId] = $wa;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理每个设备
|
||||
foreach ($devices as $device) {
|
||||
$deviceId = $device['id'];
|
||||
|
||||
// 查询关联的微信账号(通过 currentDeviceId)
|
||||
$wechatAccount = $wechatAccountsMap[$deviceId] ?? null;
|
||||
|
||||
// 解析 wechatAccounts JSON 字段
|
||||
$wechatAccountsJson = [];
|
||||
if (!empty($device['wechatAccounts'])) {
|
||||
$wechatAccountsJson = json_decode($device['wechatAccounts'], true);
|
||||
if (!is_array($wechatAccountsJson)) {
|
||||
$wechatAccountsJson = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 优先使用 s2_wechat_account 表的数据,如果没有则使用 wechatAccounts JSON 中的第一个
|
||||
$wechatId = '';
|
||||
$nickname = '';
|
||||
$alias = '';
|
||||
$avatar = '';
|
||||
$totalFriend = 0;
|
||||
|
||||
if (!empty($wechatAccount)) {
|
||||
$wechatId = $wechatAccount['wechatId'] ?? '';
|
||||
$nickname = $wechatAccount['nickname'] ?? '';
|
||||
$alias = $wechatAccount['alias'] ?? '';
|
||||
$avatar = $wechatAccount['avatar'] ?? '';
|
||||
$totalFriend = intval($wechatAccount['totalFriend'] ?? 0);
|
||||
} elseif (!empty($wechatAccountsJson) && is_array($wechatAccountsJson) && count($wechatAccountsJson) > 0) {
|
||||
$firstWechat = $wechatAccountsJson[0];
|
||||
$wechatId = $firstWechat['wechatId'] ?? '';
|
||||
$nickname = $firstWechat['wechatNickname'] ?? '';
|
||||
$alias = $firstWechat['alias'] ?? '';
|
||||
$avatar = $firstWechat['wechatAvatar'] ?? '';
|
||||
$totalFriend = 0; // JSON 中没有 totalFriend 字段
|
||||
}
|
||||
|
||||
$deviceList[] = [
|
||||
'id' => $device['id'],
|
||||
'imei' => $device['imei'] ?? '',
|
||||
'memo' => $device['memo'] ?? '',
|
||||
'alive' => $device['alive'] ?? 0,
|
||||
'wechatId' => $wechatId,
|
||||
'nickname' => $nickname,
|
||||
'alias' => $alias,
|
||||
'avatar' => $avatar,
|
||||
'totalFriend' => $totalFriend
|
||||
];
|
||||
}
|
||||
}
|
||||
unset($device);
|
||||
|
||||
$workbench->config->deviceGroupsOptions = $deviceList;
|
||||
} else {
|
||||
@@ -1071,6 +1195,7 @@ class WorkbenchController extends Controller
|
||||
$config->devices = json_encode($param['deviceGroups'] ?? [], JSON_UNESCAPED_UNICODE);
|
||||
$config->startTime = $param['startTime'] ?? '';
|
||||
$config->endTime = $param['endTime'] ?? '';
|
||||
$config->executorId = intval($param['executorId'] ?? 3);
|
||||
$config->groupSizeMin = intval($param['groupSizeMin'] ?? 3);
|
||||
$config->groupSizeMax = intval($param['groupSizeMax'] ?? 38);
|
||||
$config->maxGroupsPerDay = intval($param['maxGroupsPerDay'] ?? 20);
|
||||
@@ -1321,6 +1446,7 @@ class WorkbenchController extends Controller
|
||||
$newConfig->devices = $config->devices;
|
||||
$newConfig->startTime = $config->startTime;
|
||||
$newConfig->endTime = $config->endTime;
|
||||
$newConfig->executorId = $config->executorId;
|
||||
$newConfig->groupSizeMin = $config->groupSizeMin;
|
||||
$newConfig->groupSizeMax = $config->groupSizeMax;
|
||||
$newConfig->maxGroupsPerDay = $config->maxGroupsPerDay;
|
||||
@@ -2206,9 +2332,93 @@ class WorkbenchController extends Controller
|
||||
*/
|
||||
public function getGroupPushStats()
|
||||
{
|
||||
$controller = new \app\cunkebao\controller\workbench\WorkbenchGroupPushController();
|
||||
$controller->request = $this->request;
|
||||
return $controller->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
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2718,9 +2928,291 @@ class WorkbenchController extends Controller
|
||||
*/
|
||||
public function getGroupPushHistory()
|
||||
{
|
||||
$controller = new \app\cunkebao\controller\workbench\WorkbenchGroupPushController();
|
||||
$controller->request = $this->request;
|
||||
return $controller->getGroupPushHistory();
|
||||
$page = $this->request->param('page', 1);
|
||||
$limit = $this->request->param('limit', 10);
|
||||
$workbenchId = $this->request->param('workbenchId', 0);
|
||||
$keyword = $this->request->param('keyword', '');
|
||||
$pushType = $this->request->param('pushType', ''); // 推送类型筛选:''=全部, 'friend'=好友消息, 'group'=群消息, 'announcement'=群公告
|
||||
$status = $this->request->param('status', ''); // 状态筛选:''=全部, 'success'=已完成, 'progress'=进行中, 'failed'=失败
|
||||
$userId = $this->request->userInfo['id'];
|
||||
|
||||
// 构建工作台查询条件
|
||||
$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];
|
||||
}
|
||||
|
||||
// 1. 先查询所有已执行的推送记录(按推送时间分组)
|
||||
$pushHistoryQuery = Db::name('workbench_group_push_item')
|
||||
->alias('wgpi')
|
||||
->join('workbench w', 'w.id = wgpi.workbenchId', 'left')
|
||||
->join('workbench_group_push wgp', 'wgp.workbenchId = wgpi.workbenchId', 'left')
|
||||
->join('content_item ci', 'ci.id = wgpi.contentId', 'left')
|
||||
->join('content_library cl', 'cl.id = ci.libraryId', 'left')
|
||||
->where($workbenchWhere)
|
||||
->field([
|
||||
'wgpi.workbenchId',
|
||||
'w.name as workbenchName',
|
||||
'wgpi.contentId',
|
||||
'FROM_UNIXTIME(wgpi.createTime, "%Y-%m-%d %H:00:00") as pushTime',
|
||||
'wgpi.targetType',
|
||||
'wgp.groupPushSubType',
|
||||
'MIN(wgpi.createTime) as createTime',
|
||||
'COUNT(DISTINCT wgpi.id) as totalCount',
|
||||
'cl.name as contentLibraryName'
|
||||
])
|
||||
->group('wgpi.workbenchId, wgpi.contentId, pushTime, wgpi.targetType, wgp.groupPushSubType');
|
||||
|
||||
if (!empty($keyword)) {
|
||||
$pushHistoryQuery->where('w.name|cl.name|ci.content', 'like', '%' . $keyword . '%');
|
||||
}
|
||||
|
||||
$pushHistoryList = $pushHistoryQuery->order('createTime', 'desc')->select();
|
||||
|
||||
// 2. 查询所有任务(包括未执行的)
|
||||
$allTasksQuery = Db::name('workbench')
|
||||
->alias('w')
|
||||
->join('workbench_group_push wgp', 'wgp.workbenchId = w.id', 'left')
|
||||
->where($workbenchWhere)
|
||||
->field([
|
||||
'w.id as workbenchId',
|
||||
'w.name as workbenchName',
|
||||
'w.createTime',
|
||||
'wgp.targetType',
|
||||
'wgp.groupPushSubType',
|
||||
'wgp.groups',
|
||||
'wgp.friends',
|
||||
'wgp.trafficPools'
|
||||
]);
|
||||
|
||||
if (!empty($keyword)) {
|
||||
$allTasksQuery->where('w.name', 'like', '%' . $keyword . '%');
|
||||
}
|
||||
|
||||
$allTasks = $allTasksQuery->select();
|
||||
|
||||
// 3. 合并数据:已执行的推送记录 + 未执行的任务
|
||||
$resultList = [];
|
||||
$executedWorkbenchIds = [];
|
||||
|
||||
// 处理已执行的推送记录
|
||||
foreach ($pushHistoryList as $item) {
|
||||
$itemWorkbenchId = $item['workbenchId'];
|
||||
$contentId = $item['contentId'];
|
||||
$pushTime = $item['pushTime'];
|
||||
$targetType = intval($item['targetType']);
|
||||
$groupPushSubType = isset($item['groupPushSubType']) ? intval($item['groupPushSubType']) : 1;
|
||||
|
||||
// 标记该工作台已有执行记录
|
||||
if (!in_array($itemWorkbenchId, $executedWorkbenchIds)) {
|
||||
$executedWorkbenchIds[] = $itemWorkbenchId;
|
||||
}
|
||||
|
||||
// 将时间字符串转换为时间戳范围(小时级别)
|
||||
$pushTimeStart = strtotime($pushTime);
|
||||
$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; // 简化处理,实际需要从发送状态获取
|
||||
// 状态判断
|
||||
$itemStatus = $successCount > 0 ? 'success' : 'failed';
|
||||
if ($failCount > 0 && $successCount > 0) {
|
||||
$itemStatus = 'partial';
|
||||
}
|
||||
|
||||
// 推送类型判断
|
||||
$pushTypeText = '';
|
||||
$pushTypeCode = '';
|
||||
if ($targetType == 1) {
|
||||
// 群推送
|
||||
if ($groupPushSubType == 2) {
|
||||
$pushTypeText = '群公告';
|
||||
$pushTypeCode = 'announcement';
|
||||
} else {
|
||||
$pushTypeText = '群消息';
|
||||
$pushTypeCode = 'group';
|
||||
}
|
||||
} else {
|
||||
// 好友推送
|
||||
$pushTypeText = '好友消息';
|
||||
$pushTypeCode = 'friend';
|
||||
}
|
||||
|
||||
$resultList[] = [
|
||||
'workbenchId' => $itemWorkbenchId,
|
||||
'taskName' => $item['workbenchName'] ?? '',
|
||||
'pushType' => $pushTypeText,
|
||||
'pushTypeCode' => $pushTypeCode,
|
||||
'targetCount' => $targetCount,
|
||||
'successCount' => $successCount,
|
||||
'failCount' => $failCount,
|
||||
'status' => $itemStatus,
|
||||
'statusText' => $this->getStatusText($itemStatus),
|
||||
'createTime' => date('Y-m-d H:i:s', $item['createTime']),
|
||||
'contentLibraryName' => $item['contentLibraryName'] ?? ''
|
||||
];
|
||||
}
|
||||
|
||||
// 处理未执行的任务
|
||||
foreach ($allTasks as $task) {
|
||||
$taskWorkbenchId = $task['workbenchId'];
|
||||
|
||||
// 如果该任务已有执行记录,跳过(避免重复)
|
||||
if (in_array($taskWorkbenchId, $executedWorkbenchIds)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$targetType = isset($task['targetType']) ? intval($task['targetType']) : 1;
|
||||
$groupPushSubType = isset($task['groupPushSubType']) ? intval($task['groupPushSubType']) : 1;
|
||||
|
||||
// 计算目标数量(从配置中获取)
|
||||
$targetCount = 0;
|
||||
if ($targetType == 1) {
|
||||
// 群推送:统计配置的群数量
|
||||
$groups = json_decode($task['groups'] ?? '[]', true);
|
||||
$targetCount = is_array($groups) ? count($groups) : 0;
|
||||
} else {
|
||||
// 好友推送:统计配置的好友数量或流量池数量
|
||||
$friends = json_decode($task['friends'] ?? '[]', true);
|
||||
$trafficPools = json_decode($task['trafficPools'] ?? '[]', true);
|
||||
$friendCount = is_array($friends) ? count($friends) : 0;
|
||||
$poolCount = is_array($trafficPools) ? count($trafficPools) : 0;
|
||||
// 如果配置了流量池,目标数量暂时显示为流量池数量(实际数量需要从流量池中统计)
|
||||
$targetCount = $friendCount > 0 ? $friendCount : $poolCount;
|
||||
}
|
||||
|
||||
// 推送类型判断
|
||||
$pushTypeText = '';
|
||||
$pushTypeCode = '';
|
||||
if ($targetType == 1) {
|
||||
// 群推送
|
||||
if ($groupPushSubType == 2) {
|
||||
$pushTypeText = '群公告';
|
||||
$pushTypeCode = 'announcement';
|
||||
} else {
|
||||
$pushTypeText = '群消息';
|
||||
$pushTypeCode = 'group';
|
||||
}
|
||||
} else {
|
||||
// 好友推送
|
||||
$pushTypeText = '好友消息';
|
||||
$pushTypeCode = 'friend';
|
||||
}
|
||||
|
||||
$resultList[] = [
|
||||
'workbenchId' => $taskWorkbenchId,
|
||||
'taskName' => $task['workbenchName'] ?? '',
|
||||
'pushType' => $pushTypeText,
|
||||
'pushTypeCode' => $pushTypeCode,
|
||||
'targetCount' => $targetCount,
|
||||
'successCount' => 0,
|
||||
'failCount' => 0,
|
||||
'status' => 'pending',
|
||||
'statusText' => '进行中',
|
||||
'createTime' => date('Y-m-d H:i:s', $task['createTime']),
|
||||
'contentLibraryName' => ''
|
||||
];
|
||||
}
|
||||
|
||||
// 应用筛选条件
|
||||
$filteredList = [];
|
||||
foreach ($resultList as $item) {
|
||||
// 推送类型筛选
|
||||
if (!empty($pushType)) {
|
||||
if ($pushType === 'friend' && $item['pushTypeCode'] !== 'friend') {
|
||||
continue;
|
||||
}
|
||||
if ($pushType === 'group' && $item['pushTypeCode'] !== 'group') {
|
||||
continue;
|
||||
}
|
||||
if ($pushType === 'announcement' && $item['pushTypeCode'] !== 'announcement') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (!empty($status)) {
|
||||
if ($status === 'success' && $item['status'] !== 'success') {
|
||||
continue;
|
||||
}
|
||||
if ($status === 'progress') {
|
||||
// 进行中:包括 partial 和 pending
|
||||
if ($item['status'] !== 'partial' && $item['status'] !== 'pending') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ($status === 'failed' && $item['status'] !== 'failed') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$filteredList[] = $item;
|
||||
}
|
||||
|
||||
// 按创建时间倒序排序
|
||||
usort($filteredList, function($a, $b) {
|
||||
return strtotime($b['createTime']) - strtotime($a['createTime']);
|
||||
});
|
||||
|
||||
// 分页处理
|
||||
$total = count($filteredList);
|
||||
$offset = ($page - 1) * $limit;
|
||||
$list = array_slice($filteredList, $offset, $limit);
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => [
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'limit' => $limit
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3037,7 +3529,7 @@ class WorkbenchController extends Controller
|
||||
'isOwner' => $isOwner, // 标记群主
|
||||
'joinStatus' => $joinStatus, // 入群状态:auto=自动建群加入,manual=其他方式加入
|
||||
'isQuit' => $isQuit, // 是否已退群:0=在群中,1=已退群
|
||||
'joinTime' => !empty($member['joinTime']) ? date('Y-m-d H:i:s', $member['joinTime']) : '', // 入群时间
|
||||
'joinTime' => (!empty($member['joinTime']) && is_numeric($member['joinTime']) && $member['joinTime'] > 0) ? date('Y-m-d H:i:s', intval($member['joinTime'])) : '', // 入群时间
|
||||
];
|
||||
}
|
||||
|
||||
@@ -3066,7 +3558,7 @@ class WorkbenchController extends Controller
|
||||
'isOwner' => $isOwner, // 标记群主
|
||||
'joinStatus' => 'auto', // 入群状态:auto=自动建群加入
|
||||
'isQuit' => 1, // 是否已退群:1=已退群
|
||||
'joinTime' => !empty($autoMember['autoJoinTime']) ? date('Y-m-d H:i:s', $autoMember['autoJoinTime']) : '', // 入群时间
|
||||
'joinTime' => (!empty($autoMember['autoJoinTime']) && is_numeric($autoMember['autoJoinTime']) && $autoMember['autoJoinTime'] > 0) ? date('Y-m-d H:i:s', intval($autoMember['autoJoinTime'])) : '', // 入群时间
|
||||
];
|
||||
}
|
||||
|
||||
@@ -3078,6 +3570,72 @@ class WorkbenchController extends Controller
|
||||
return $a['isOwner'] > $b['isOwner'] ? -1 : 1;
|
||||
});
|
||||
|
||||
// 获取工作台配置,检查是否有 executorId
|
||||
$groupCreateConfig = WorkbenchGroupCreate::where('workbenchId', $workbenchId)->find();
|
||||
$executorId = !empty($groupCreateConfig) ? intval($groupCreateConfig->executorId ?? 0) : 0;
|
||||
|
||||
// 如果 executorId 有值,查询设备详情(格式和 deviceGroupsOptions 一样,但返回一维数组)
|
||||
$executor = null;
|
||||
if (!empty($executorId)) {
|
||||
// 查询设备基本信息
|
||||
$device = Db::table('s2_device')
|
||||
->where('id', $executorId)
|
||||
->where('isDeleted', 0)
|
||||
->field('id,imei,memo,alive,wechatAccounts')
|
||||
->find();
|
||||
|
||||
if (!empty($device)) {
|
||||
// 查询关联的微信账号(通过 currentDeviceId)
|
||||
$wechatAccount = Db::table('s2_wechat_account')
|
||||
->where('currentDeviceId', $executorId)
|
||||
->field('wechatId,nickname,alias,avatar,totalFriend')
|
||||
->find();
|
||||
|
||||
// 解析 wechatAccounts JSON 字段
|
||||
$wechatAccountsJson = [];
|
||||
if (!empty($device['wechatAccounts'])) {
|
||||
$wechatAccountsJson = json_decode($device['wechatAccounts'], true);
|
||||
if (!is_array($wechatAccountsJson)) {
|
||||
$wechatAccountsJson = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 优先使用 s2_wechat_account 表的数据,如果没有则使用 wechatAccounts JSON 中的第一个
|
||||
$wechatId = '';
|
||||
$nickname = '';
|
||||
$alias = '';
|
||||
$avatar = '';
|
||||
$totalFriend = 0;
|
||||
|
||||
if (!empty($wechatAccount)) {
|
||||
$wechatId = $wechatAccount['wechatId'] ?? '';
|
||||
$nickname = $wechatAccount['nickname'] ?? '';
|
||||
$alias = $wechatAccount['alias'] ?? '';
|
||||
$avatar = $wechatAccount['avatar'] ?? '';
|
||||
$totalFriend = intval($wechatAccount['totalFriend'] ?? 0);
|
||||
} elseif (!empty($wechatAccountsJson) && is_array($wechatAccountsJson) && count($wechatAccountsJson) > 0) {
|
||||
$firstWechat = $wechatAccountsJson[0];
|
||||
$wechatId = $firstWechat['wechatId'] ?? '';
|
||||
$nickname = $firstWechat['wechatNickname'] ?? '';
|
||||
$alias = $firstWechat['alias'] ?? '';
|
||||
$avatar = $firstWechat['wechatAvatar'] ?? '';
|
||||
$totalFriend = 0; // JSON 中没有 totalFriend 字段
|
||||
}
|
||||
|
||||
$executor = [
|
||||
'id' => $device['id'],
|
||||
'imei' => $device['imei'] ?? '',
|
||||
'memo' => $device['memo'] ?? '',
|
||||
'alive' => $device['alive'] ?? 0,
|
||||
'wechatId' => $wechatId,
|
||||
'nickname' => $nickname,
|
||||
'alias' => $alias,
|
||||
'avatar' => $avatar,
|
||||
'totalFriend' => $totalFriend
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化返回数据
|
||||
$result = [
|
||||
'id' => $group['id'],
|
||||
@@ -3089,11 +3647,12 @@ class WorkbenchController extends Controller
|
||||
'ownerAvatar' => $group['ownerAvatar'] ?? '',
|
||||
'ownerAlias' => $group['ownerAlias'] ?? '',
|
||||
'announce' => $group['announce'] ?? '',
|
||||
'createTime' => !empty($group['createTime']) ? date('Y-m-d H:i', $group['createTime']) : '', // 格式化为"YYYY-MM-DD HH:MM"
|
||||
'createTime' => (!empty($group['createTime']) && is_numeric($group['createTime']) && $group['createTime'] > 0) ? date('Y-m-d H:i', intval($group['createTime'])) : '', // 格式化为"YYYY-MM-DD HH:MM"
|
||||
'memberCount' => $memberCount,
|
||||
'memberCountText' => $memberCount . '人', // 格式化为"XX人"
|
||||
'workbenchName' => $workbench->name ?? '', // 任务名称(工作台名称)
|
||||
'members' => $members // 所有成员列表
|
||||
'members' => $members, // 所有成员列表
|
||||
'executor' => $executor // 执行设备详情(当 executorId 有值时返回,格式和 deviceGroupsOptions 一样)
|
||||
];
|
||||
|
||||
return json([
|
||||
@@ -3408,9 +3967,112 @@ class WorkbenchController extends Controller
|
||||
*/
|
||||
public function quitGroup()
|
||||
{
|
||||
$controller = new \app\cunkebao\controller\workbench\WorkbenchGroupCreateController();
|
||||
$controller->request = $this->request;
|
||||
return $controller->quitGroup();
|
||||
$workbenchId = $this->request->param('workbenchId', 0);
|
||||
$groupId = $this->request->param('groupId', 0);
|
||||
|
||||
if (empty($groupId)) {
|
||||
return json(['code' => 400, 'msg' => '群ID不能为空']);
|
||||
}
|
||||
|
||||
// 查询群基本信息
|
||||
$group = Db::table('s2_wechat_chatroom')
|
||||
->where('id', $groupId)
|
||||
->where('isDeleted', 0)
|
||||
->field('id,chatroomId,wechatAccountWechatId,accountId,wechatAccountId')
|
||||
->find();
|
||||
|
||||
if (empty($group)) {
|
||||
return json(['code' => 404, 'msg' => '群不存在']);
|
||||
}
|
||||
|
||||
$chatroomId = $group['chatroomId'] ?? '';
|
||||
if (empty($chatroomId)) {
|
||||
return json(['code' => 400, 'msg' => '群聊ID不存在']);
|
||||
}
|
||||
|
||||
try {
|
||||
// 直接使用群表中的账号信息
|
||||
$executeWechatId = $group['wechatAccountWechatId'] ?? '';
|
||||
|
||||
// 确保 wechatId 不为空
|
||||
if (empty($executeWechatId)) {
|
||||
return json(['code' => 400, 'msg' => '无法获取微信账号ID']);
|
||||
}
|
||||
|
||||
// 调用 WebSocketController 退群
|
||||
// 获取系统API账号信息(用于WebSocket连接)
|
||||
$username = Env::get('api.username2', '');
|
||||
$password = Env::get('api.password2', '');
|
||||
|
||||
if (empty($username) || empty($password)) {
|
||||
return json(['code' => 500, 'msg' => '系统API账号配置缺失']);
|
||||
}
|
||||
|
||||
// 获取系统账号ID
|
||||
$systemAccountId = Db::name('users')->where('account', $username)->value('s2_accountId');
|
||||
if (empty($systemAccountId)) {
|
||||
return json(['code' => 500, 'msg' => '未找到系统账号ID']);
|
||||
}
|
||||
|
||||
$webSocketController = new WebSocketController([
|
||||
'userName' => $username,
|
||||
'password' => $password,
|
||||
'accountId' => $systemAccountId
|
||||
]);
|
||||
|
||||
// 使用与 modifyGroupInfo 相同的方式,但操作类型改为4(退群)
|
||||
$params = [
|
||||
"chatroomOperateType" => 4, // 4 表示退群
|
||||
"cmdType" => "CmdChatroomOperate",
|
||||
"seq" => time(),
|
||||
"wechatAccountId" => $executeWechatId,
|
||||
"wechatChatroomId" => $chatroomId
|
||||
];
|
||||
|
||||
// 使用反射调用 protected 的 sendMessage 方法
|
||||
$reflection = new \ReflectionClass($webSocketController);
|
||||
$method = $reflection->getMethod('sendMessage');
|
||||
$method->setAccessible(true);
|
||||
$quitResult = $method->invoke($webSocketController, $params, false);
|
||||
|
||||
// sendMessage 返回的是数组
|
||||
if (empty($quitResult) || (isset($quitResult['code']) && $quitResult['code'] != 200)) {
|
||||
return json(['code' => 500, 'msg' => '退群失败:' . ($quitResult['msg'] ?? '未知错误')]);
|
||||
}
|
||||
|
||||
// 退群成功后更新数据库
|
||||
// 将群标记为已删除
|
||||
Db::table('s2_wechat_chatroom')
|
||||
->where('id', $groupId)
|
||||
->update([
|
||||
'isDeleted' => 1,
|
||||
'deleteTime' => time(),
|
||||
'updateTime' => time()
|
||||
]);
|
||||
|
||||
// 如果提供了 workbenchId,更新工作台建群记录的状态(标记为已退群)
|
||||
if (!empty($workbenchId)) {
|
||||
Db::name('workbench_group_create_item')
|
||||
->where('workbenchId', $workbenchId)
|
||||
->where('groupId', $groupId)
|
||||
->update([
|
||||
'status' => 5, // 可以定义一个新状态:5 = 已退群
|
||||
'updateTime' => time()
|
||||
]);
|
||||
}
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '退群成功',
|
||||
'data' => [
|
||||
'groupId' => $groupId,
|
||||
'chatroomId' => $chatroomId
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
\think\facade\Log::error("退群异常。群ID: {$groupId}, 错误: " . $e->getMessage());
|
||||
return json(['code' => 500, 'msg' => '退群失败:' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -378,32 +378,39 @@ class Adapter implements WeChatServiceInterface
|
||||
}
|
||||
|
||||
|
||||
if ($passedWeChatId && !empty($task_info['msgConf'])) {
|
||||
|
||||
// 更新状态为4(已通过并已发消息)
|
||||
Db::name('task_customer')
|
||||
->where('id', $task['id'])
|
||||
->update(['status' => 4,'passTime' => time(), 'updateTime' => time()]);
|
||||
|
||||
// 记录添加好友奖励(如果之前没有记录过,status从其他状态变为4时)
|
||||
// 注意:如果status已经是2,说明已经记录过奖励,这里不再重复记录
|
||||
if ($task['status'] != 2 && !empty($task['channelId'])) {
|
||||
try {
|
||||
DistributionRewardService::recordAddFriendReward(
|
||||
$task['task_id'],
|
||||
$task['id'],
|
||||
$task['phone'],
|
||||
intval($task['channelId'])
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
// 记录错误但不影响主流程
|
||||
Log::error('记录添加好友奖励失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if ($passedWeChatId) {
|
||||
// 获取好友记录(用于发消息 & 拉群)
|
||||
$wechatFriendRecord = $this->getWeChatAccoutIdAndFriendIdByWeChatIdAndFriendPhone($passedWeChatId, $task['phone']);
|
||||
$msgConf = is_string($task_info['msgConf']) ? json_decode($task_info['msgConf'], 1) : $task_info['msgConf'];
|
||||
$wechatFriendRecord && $this->sendMsgToFriend($wechatFriendRecord['id'], $wechatFriendRecord['wechatAccountId'], $msgConf);
|
||||
if ($wechatFriendRecord) {
|
||||
// 1. 如配置了消息,则先发送消息,并将状态置为4(已通过并已发消息)
|
||||
if (!empty($task_info['msgConf'])) {
|
||||
Db::name('task_customer')
|
||||
->where('id', $task['id'])
|
||||
->update(['status' => 4,'passTime' => time(), 'updateTime' => time()]);
|
||||
|
||||
// 记录添加好友奖励(如果之前没有记录过,status从其他状态变为4时)
|
||||
if ($task['status'] != 2 && !empty($task['channelId'])) {
|
||||
try {
|
||||
DistributionRewardService::recordAddFriendReward(
|
||||
$task['task_id'],
|
||||
$task['id'],
|
||||
$task['phone'],
|
||||
intval($task['channelId'])
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
// 记录错误但不影响主流程
|
||||
Log::error('记录添加好友奖励失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$msgConf = is_string($task_info['msgConf']) ? json_decode($task_info['msgConf'], 1) : $task_info['msgConf'];
|
||||
$this->sendMsgToFriend($wechatFriendRecord['id'], $wechatFriendRecord['wechatAccountId'], $msgConf);
|
||||
}
|
||||
|
||||
// 2. 好友通过后,如配置了拉群,则建群并拉人:通过的好友 + 固定成员
|
||||
$this->createGroupAfterFriendPass($task_info, $wechatFriendRecord['id'], $wechatFriendRecord['wechatAccountId']);
|
||||
// 如果没有 msgConf,则保持之前更新的状态5(已通过未发消息)不变
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
@@ -685,10 +692,90 @@ class Adapter implements WeChatServiceInterface
|
||||
$task_info['reqConf'] = json_decode($task_info['reqConf'], true);
|
||||
$task_info['msgConf'] = json_decode($task_info['msgConf'], true);
|
||||
$task_info['tagConf'] = json_decode($task_info['tagConf'], true);
|
||||
// 处理拉群固定成员配置(JSON 字段)
|
||||
if (!empty($task_info['groupFixedMembers'])) {
|
||||
$fixedMembers = json_decode($task_info['groupFixedMembers'], true);
|
||||
$task_info['groupFixedMembers'] = is_array($fixedMembers) ? $fixedMembers : [];
|
||||
} else {
|
||||
$task_info['groupFixedMembers'] = [];
|
||||
}
|
||||
}
|
||||
return $task_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 好友通过后,根据任务配置建群并拉人(通过好友 + 固定成员)
|
||||
* @param array $taskInfo customer_acquisition_task 记录(含 groupInviteEnabled/groupName/groupFixedMembers)
|
||||
* @param int $passedFriendId 通过的好友ID(s2_wechat_friend.id)
|
||||
* @param int $wechatAccountId 微信账号ID(建群账号)
|
||||
*/
|
||||
protected function createGroupAfterFriendPass(array $taskInfo, int $passedFriendId, int $wechatAccountId): void
|
||||
{
|
||||
// 1. 校验拉群开关与基础配置
|
||||
if (empty($taskInfo['groupInviteEnabled'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$groupName = $taskInfo['groupName'] ?? '';
|
||||
if ($groupName === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$fixedMembers = $taskInfo['groupFixedMembers'] ?? [];
|
||||
if (!is_array($fixedMembers)) {
|
||||
$fixedMembers = [];
|
||||
}
|
||||
|
||||
// 2. 过滤出有效的固定成员好友ID(数字ID)
|
||||
$fixedFriendIds = [];
|
||||
foreach ($fixedMembers as $member) {
|
||||
if (is_numeric($member)) {
|
||||
$fixedFriendIds[] = intval($member);
|
||||
}
|
||||
}
|
||||
|
||||
// 包含通过的好友
|
||||
$friendIds = array_unique(array_merge([$passedFriendId], $fixedFriendIds));
|
||||
if (empty($friendIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 3. 初始化 WebSocket(参考 sendMsgToFriend / Workbench 群创建逻辑)
|
||||
$toAccountId = '';
|
||||
$username = Env::get('api.username', '');
|
||||
$password = Env::get('api.password', '');
|
||||
if (!empty($username) || !empty($password)) {
|
||||
$toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId');
|
||||
}
|
||||
if (empty($toAccountId)) {
|
||||
Log::warning('createGroupAfterFriendPass: toAccountId 为空,跳过建群');
|
||||
return;
|
||||
}
|
||||
|
||||
$webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]);
|
||||
|
||||
// 4. 调用建群接口:群名 = 配置的 groupName,成员 = 通过好友 + 固定好友
|
||||
$createResult = $webSocket->CmdChatroomCreate([
|
||||
'chatroomName' => $groupName,
|
||||
'wechatFriendIds' => $friendIds,
|
||||
'wechatAccountId' => $wechatAccountId,
|
||||
]);
|
||||
|
||||
$createResultData = json_decode($createResult, true);
|
||||
if (empty($createResultData) || !isset($createResultData['code']) || $createResultData['code'] != 200) {
|
||||
Log::warning('createGroupAfterFriendPass: 建群失败', [
|
||||
'taskId' => $taskInfo['id'] ?? 0,
|
||||
'wechatAccountId' => $wechatAccountId,
|
||||
'friendIds' => $friendIds,
|
||||
'result' => $createResult,
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('createGroupAfterFriendPass 异常: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否是好友关系
|
||||
|
||||
public function checkIfIsWeChatFriendByPhone($wxId = '', $phone = '', $siteTags = '')
|
||||
|
||||
Reference in New Issue
Block a user