Merge branch 'feature/group' into develop
This commit is contained in:
@@ -94,6 +94,20 @@ export async function exportWechatMoments(params: {
|
||||
}
|
||||
);
|
||||
|
||||
// 检查响应类型,如果是JSON错误响应,需要解析错误信息
|
||||
const contentType = response.headers["content-type"] || "";
|
||||
if (contentType.includes("application/json")) {
|
||||
// 如果是JSON响应,说明可能是错误信息
|
||||
const text = await response.data.text();
|
||||
const errorData = JSON.parse(text);
|
||||
throw new Error(errorData.message || errorData.msg || "导出失败");
|
||||
}
|
||||
|
||||
// 检查响应状态
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`导出失败,状态码: ${response.status}`);
|
||||
}
|
||||
|
||||
// 创建下载链接
|
||||
const blob = new Blob([response.data]);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
@@ -116,6 +130,32 @@ export async function exportWechatMoments(params: {
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response?.data?.message || error.message || "导出失败");
|
||||
// 如果是我们抛出的错误,直接抛出
|
||||
if (error.message && error.message !== "导出失败") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 处理axios错误响应
|
||||
if (error.response) {
|
||||
// 如果响应是blob类型,尝试读取为文本
|
||||
if (error.response.data instanceof Blob) {
|
||||
try {
|
||||
const text = await error.response.data.text();
|
||||
const errorData = JSON.parse(text);
|
||||
throw new Error(errorData.message || errorData.msg || "导出失败");
|
||||
} catch (parseError) {
|
||||
throw new Error("导出失败,请重试");
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
error.response.data?.message ||
|
||||
error.response.data?.msg ||
|
||||
error.message ||
|
||||
"导出失败"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error(error.message || "导出失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,6 +373,12 @@
|
||||
font-weight: 600;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.stat-value-negative {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -868,9 +874,10 @@
|
||||
.type-selector {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
|
||||
.type-option {
|
||||
flex: 1;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
@@ -879,6 +886,7 @@
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background: white;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
border-color: #1677ff;
|
||||
@@ -1331,82 +1339,54 @@
|
||||
}
|
||||
}
|
||||
|
||||
.moments-action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background: white;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.action-button, .action-button-dark {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
background: #1677ff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
color: white;
|
||||
|
||||
&:active {
|
||||
background: #0958d9;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
svg {
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.action-button-dark {
|
||||
background: #1677ff;
|
||||
color: white;
|
||||
|
||||
&:active {
|
||||
background: #0958d9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.moments-content {
|
||||
padding: 16px 0;
|
||||
height: 500px;
|
||||
overflow-y: auto;
|
||||
background: #f5f5f5;
|
||||
|
||||
.moments-action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px 16px;
|
||||
|
||||
.action-button, .action-button-dark {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 70px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
background: #1677ff;
|
||||
|
||||
.action-icon-text, .action-icon-image, .action-icon-video, .action-icon-export {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 2px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 12px;
|
||||
height: 2px;
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
|
||||
.action-icon-image::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 2px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.action-icon-video::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 5px 0 5px 8px;
|
||||
border-color: transparent transparent transparent white;
|
||||
}
|
||||
|
||||
.action-text, .action-text-light {
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.action-button-dark {
|
||||
background: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.moments-list {
|
||||
padding: 0 16px;
|
||||
|
||||
@@ -1459,7 +1439,7 @@
|
||||
|
||||
.image-grid {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
|
||||
// 1张图片:宽度拉伸,高度自适应
|
||||
@@ -1469,56 +1449,57 @@
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 400px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// 2张图片:左右并列
|
||||
// 2张图片:左右并列,1:1比例
|
||||
&.double {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// 3张图片:三张并列
|
||||
// 3张图片:三张并列,1:1比例
|
||||
&.triple {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// 4张图片:2x2网格布局
|
||||
// 4张图片:2x2网格布局,1:1比例
|
||||
&.quad {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// 5张及以上:网格布局(9宫格)
|
||||
// 5张及以上:网格布局(9宫格),1:1比例
|
||||
&.grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.image-more {
|
||||
@@ -1526,11 +1507,11 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 8px;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
height: 100px;
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1547,6 +1528,34 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.moments-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.moments-no-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.friends-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.friends-no-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,13 +12,18 @@ import {
|
||||
Tag,
|
||||
Switch,
|
||||
DatePicker,
|
||||
InfiniteScroll,
|
||||
} from "antd-mobile";
|
||||
import { Input, Pagination } from "antd";
|
||||
import { Input } from "antd";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
UserOutlined,
|
||||
FileTextOutlined,
|
||||
PictureOutlined,
|
||||
VideoCameraOutlined,
|
||||
DownloadOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import style from "./detail.module.scss";
|
||||
@@ -32,6 +37,7 @@ import {
|
||||
} from "./api";
|
||||
import DeviceSelection from "@/components/DeviceSelection";
|
||||
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { WechatAccountSummary, Friend, MomentItem } from "./data";
|
||||
|
||||
@@ -107,6 +113,84 @@ const WechatAccountDetail: React.FC = () => {
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
// 计算账号价值
|
||||
// 规则:
|
||||
// 1. 1个好友3块
|
||||
// 2. 1个群1块
|
||||
// 3. 修改过微信号10块
|
||||
const calculateAccountValue = useCallback(() => {
|
||||
// 获取好友数量(优先使用概览数据,其次使用好友列表总数,最后使用账号信息)
|
||||
const friendsCount = overviewData?.totalFriends || friendsTotal || accountInfo?.friendShip?.totalFriend || 0;
|
||||
|
||||
// 获取群数量(优先使用概览数据,其次使用账号信息)
|
||||
const groupsCount = overviewData?.highValueChatrooms || accountInfo?.friendShip?.groupNumber || 0;
|
||||
|
||||
// 判断是否修改过微信号
|
||||
// 注意:需要根据实际API返回的字段来判断,可能的字段名:
|
||||
// - isWechatIdModified (布尔值)
|
||||
// - wechatIdModified (布尔值)
|
||||
// - hasModifiedWechatId (布尔值)
|
||||
// - wechatIdChangeCount (数字,大于0表示修改过)
|
||||
// 如果API没有返回该字段,需要后端添加或根据其他逻辑判断
|
||||
const isWechatIdModified =
|
||||
accountInfo?.isWechatIdModified ||
|
||||
accountInfo?.wechatIdModified ||
|
||||
accountInfo?.hasModifiedWechatId ||
|
||||
(accountInfo?.wechatIdChangeCount && accountInfo.wechatIdChangeCount > 0) ||
|
||||
false;
|
||||
|
||||
// 计算各部分价值
|
||||
const friendsValue = friendsCount * 3; // 好友数 * 3
|
||||
const groupsValue = groupsCount * 1; // 群数 * 1
|
||||
const wechatIdModifiedValue = isWechatIdModified ? 10 : 0; // 修改过微信号 ? 10 : 0
|
||||
|
||||
// 计算总价值
|
||||
const totalValue = friendsValue + groupsValue + wechatIdModifiedValue;
|
||||
|
||||
return {
|
||||
value: totalValue,
|
||||
formatted: `¥${totalValue.toLocaleString()}`,
|
||||
breakdown: {
|
||||
friends: friendsValue,
|
||||
groups: groupsValue,
|
||||
wechatIdModified: wechatIdModifiedValue,
|
||||
friendsCount,
|
||||
groupsCount,
|
||||
isWechatIdModified,
|
||||
},
|
||||
};
|
||||
}, [overviewData, friendsTotal, accountInfo]);
|
||||
|
||||
// 计算今日价值变化
|
||||
// 规则:
|
||||
// 1. 今日新增好友 * 3块
|
||||
// 2. 今日新增群 * 1块
|
||||
const calculateTodayValueChange = useCallback(() => {
|
||||
// 获取今日新增好友数
|
||||
const todayNewFriends = overviewData?.todayNewFriends || accountSummary?.statistics?.todayAdded || 0;
|
||||
|
||||
// 获取今日新增群数
|
||||
const todayNewChatrooms = overviewData?.todayNewChatrooms || 0;
|
||||
|
||||
// 计算今日价值变化
|
||||
const friendsValueChange = todayNewFriends * 3; // 今日新增好友数 * 3
|
||||
const groupsValueChange = todayNewChatrooms * 1; // 今日新增群数 * 1
|
||||
|
||||
const totalChange = friendsValueChange + groupsValueChange;
|
||||
|
||||
return {
|
||||
change: totalChange,
|
||||
formatted: totalChange >= 0 ? `+${totalChange.toLocaleString()}` : `${totalChange.toLocaleString()}`,
|
||||
isPositive: totalChange >= 0,
|
||||
breakdown: {
|
||||
friends: friendsValueChange,
|
||||
groups: groupsValueChange,
|
||||
todayNewFriends,
|
||||
todayNewChatrooms,
|
||||
},
|
||||
};
|
||||
}, [overviewData, accountSummary]);
|
||||
|
||||
// 获取概览数据
|
||||
const fetchOverviewData = useCallback(async () => {
|
||||
if (!id) return;
|
||||
@@ -122,7 +206,7 @@ const WechatAccountDetail: React.FC = () => {
|
||||
|
||||
// 获取好友列表 - 封装为独立函数
|
||||
const fetchFriendsList = useCallback(
|
||||
async (page: number = 1, keyword: string = "") => {
|
||||
async (page: number = 1, keyword: string = "", append: boolean = false) => {
|
||||
if (!id) return;
|
||||
|
||||
setIsFetchingFriends(true);
|
||||
@@ -132,7 +216,7 @@ const WechatAccountDetail: React.FC = () => {
|
||||
const response = await getWechatFriends({
|
||||
wechatAccount: id,
|
||||
page: page,
|
||||
limit: 5,
|
||||
limit: 20,
|
||||
keyword: keyword,
|
||||
});
|
||||
|
||||
@@ -175,15 +259,17 @@ const WechatAccountDetail: React.FC = () => {
|
||||
};
|
||||
});
|
||||
|
||||
setFriends(newFriends);
|
||||
setFriends(prev => (append ? [...prev, ...newFriends] : newFriends));
|
||||
setFriendsTotal(response.total);
|
||||
setFriendsPage(page);
|
||||
setIsFriendsEmpty(newFriends.length === 0);
|
||||
setIsFriendsEmpty(newFriends.length === 0 && !append);
|
||||
} catch (error) {
|
||||
console.error("获取好友列表失败:", error);
|
||||
setHasFriendLoadError(true);
|
||||
setFriends([]);
|
||||
setIsFriendsEmpty(true);
|
||||
if (!append) {
|
||||
setFriends([]);
|
||||
setIsFriendsEmpty(true);
|
||||
}
|
||||
Toast.show({
|
||||
content: "获取好友列表失败,请检查网络连接",
|
||||
position: "top",
|
||||
@@ -238,22 +324,20 @@ const WechatAccountDetail: React.FC = () => {
|
||||
// 搜索好友
|
||||
const handleSearch = useCallback(() => {
|
||||
setFriendsPage(1);
|
||||
fetchFriendsList(1, searchQuery);
|
||||
fetchFriendsList(1, searchQuery, false);
|
||||
}, [searchQuery, fetchFriendsList]);
|
||||
|
||||
// 刷新好友列表
|
||||
const handleRefreshFriends = useCallback(() => {
|
||||
fetchFriendsList(friendsPage, searchQuery);
|
||||
fetchFriendsList(friendsPage, searchQuery, false);
|
||||
}, [friendsPage, searchQuery, fetchFriendsList]);
|
||||
|
||||
// 分页切换
|
||||
const handlePageChange = useCallback(
|
||||
(page: number) => {
|
||||
setFriendsPage(page);
|
||||
fetchFriendsList(page, searchQuery);
|
||||
},
|
||||
[searchQuery, fetchFriendsList],
|
||||
);
|
||||
// 加载更多好友
|
||||
const handleLoadMoreFriends = async () => {
|
||||
if (isFetchingFriends) return;
|
||||
if (friends.length >= friendsTotal) return;
|
||||
await fetchFriendsList(friendsPage + 1, searchQuery, true);
|
||||
};
|
||||
|
||||
// 初始化数据
|
||||
useEffect(() => {
|
||||
@@ -268,7 +352,7 @@ const WechatAccountDetail: React.FC = () => {
|
||||
if (activeTab === "friends" && id) {
|
||||
setIsFriendsEmpty(false);
|
||||
setHasFriendLoadError(false);
|
||||
fetchFriendsList(1, searchQuery);
|
||||
fetchFriendsList(1, searchQuery, false);
|
||||
}
|
||||
}, [activeTab, id, fetchFriendsList, searchQuery]);
|
||||
|
||||
@@ -298,8 +382,8 @@ const WechatAccountDetail: React.FC = () => {
|
||||
setInheritInfo(true);
|
||||
// 设置默认打招呼内容,使用当前微信账号昵称
|
||||
const nickname = accountInfo?.nickname || "未知";
|
||||
setGreeting(`这个是${nickname}的新号,之前那个号没用了,重新加一下您`);
|
||||
setFirstMessage("");
|
||||
setGreeting(`我是${nickname}的新号,请通过`);
|
||||
setFirstMessage("这个是我的新号,重新加你一下,以后业务就用这个号!");
|
||||
setShowTransferConfirm(true);
|
||||
};
|
||||
|
||||
@@ -385,10 +469,10 @@ const WechatAccountDetail: React.FC = () => {
|
||||
navigate(`/mine/traffic-pool/detail/${friend.wechatId}/${friend.id}`);
|
||||
};
|
||||
|
||||
const handleLoadMoreMoments = () => {
|
||||
const handleLoadMoreMoments = async () => {
|
||||
if (isFetchingMoments) return;
|
||||
if (moments.length >= momentsTotal) return;
|
||||
fetchMomentsList(momentsPage + 1, true);
|
||||
await fetchMomentsList(momentsPage + 1, true);
|
||||
};
|
||||
|
||||
// 处理朋友圈导出
|
||||
@@ -398,6 +482,18 @@ const WechatAccountDetail: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证时间范围不超过1个月
|
||||
if (exportStartTime && exportEndTime) {
|
||||
const maxDate = dayjs(exportStartTime).add(1, "month").toDate();
|
||||
if (exportEndTime > maxDate) {
|
||||
Toast.show({
|
||||
content: "日期范围不能超过1个月",
|
||||
position: "top",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setExportLoading(true);
|
||||
try {
|
||||
// 格式化时间
|
||||
@@ -418,18 +514,27 @@ const WechatAccountDetail: React.FC = () => {
|
||||
});
|
||||
|
||||
Toast.show({ content: "导出成功", position: "top" });
|
||||
setShowExportPopup(false);
|
||||
// 重置筛选条件
|
||||
// 重置筛选条件(先重置,再关闭弹窗)
|
||||
setExportKeyword("");
|
||||
setExportType(undefined);
|
||||
setExportStartTime(null);
|
||||
setExportEndTime(null);
|
||||
setShowStartTimePicker(false);
|
||||
setShowEndTimePicker(false);
|
||||
// 延迟关闭弹窗,确保Toast显示
|
||||
setTimeout(() => {
|
||||
setShowExportPopup(false);
|
||||
}, 500);
|
||||
} catch (error: any) {
|
||||
console.error("导出失败:", error);
|
||||
const errorMessage = error?.message || "导出失败,请重试";
|
||||
Toast.show({
|
||||
content: error.message || "导出失败,请重试",
|
||||
content: errorMessage,
|
||||
position: "top",
|
||||
duration: 2000,
|
||||
});
|
||||
// 确保loading状态被重置
|
||||
setExportLoading(false);
|
||||
} finally {
|
||||
setExportLoading(false);
|
||||
}
|
||||
@@ -548,7 +653,7 @@ const WechatAccountDetail: React.FC = () => {
|
||||
<div className={style["stat-icon-up"]}></div>
|
||||
</div>
|
||||
<div className={style["stat-value"]}>
|
||||
{overviewData?.accountValue?.formatted || `¥${overviewData?.accountValue?.value || "29,800"}`}
|
||||
{calculateAccountValue().formatted}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -558,8 +663,8 @@ const WechatAccountDetail: React.FC = () => {
|
||||
<div className={style["stat-title"]}>今日价值变化</div>
|
||||
<div className={style["stat-icon-plus"]}></div>
|
||||
</div>
|
||||
<div className={style["stat-value-positive"]}>
|
||||
{overviewData?.todayValueChange?.formatted || `+${overviewData?.todayValueChange?.change || "500"}`}
|
||||
<div className={calculateTodayValueChange().isPositive ? style["stat-value-positive"] : style["stat-value-negative"]}>
|
||||
{calculateTodayValueChange().formatted}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -761,7 +866,7 @@ const WechatAccountDetail: React.FC = () => {
|
||||
<div className={style["summary-item"]}>
|
||||
<div className={style["summary-label"]}>好友总估值</div>
|
||||
<div className={style["summary-value-highlight"]}>
|
||||
{overviewData?.accountValue?.formatted || "¥1,500,000"}
|
||||
{calculateAccountValue().formatted}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -780,7 +885,7 @@ const WechatAccountDetail: React.FC = () => {
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() =>
|
||||
fetchFriendsList(friendsPage, searchQuery)
|
||||
fetchFriendsList(friendsPage, searchQuery, false)
|
||||
}
|
||||
>
|
||||
重试
|
||||
@@ -837,47 +942,58 @@ const WechatAccountDetail: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 分页组件 */}
|
||||
{friendsTotal > 20 &&
|
||||
!isFriendsEmpty &&
|
||||
!hasFriendLoadError && (
|
||||
<div className={style["pagination-wrapper"]}>
|
||||
<Pagination
|
||||
total={Math.ceil(friendsTotal / 20)}
|
||||
current={friendsPage}
|
||||
onChange={handlePageChange}
|
||||
/>
|
||||
{/* 无限滚动加载 */}
|
||||
<InfiniteScroll
|
||||
loadMore={handleLoadMoreFriends}
|
||||
hasMore={friends.length < friendsTotal}
|
||||
threshold={100}
|
||||
>
|
||||
{isFetchingFriends && friends.length > 0 && (
|
||||
<div className={style["friends-loading"]}>
|
||||
<SpinLoading color="primary" style={{ fontSize: 16 }} />
|
||||
<span style={{ marginLeft: 8, color: "#999", fontSize: 12 }}>
|
||||
加载中...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{friends.length >= friendsTotal && friends.length > 0 && (
|
||||
<div className={style["friends-no-more"]}>
|
||||
<span style={{ color: "#999", fontSize: 12 }}>没有更多了</span>
|
||||
</div>
|
||||
)}
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
</Tabs.Tab>
|
||||
|
||||
|
||||
<Tabs.Tab title="朋友圈" key="moments">
|
||||
<div className={style["moments-content"]}>
|
||||
{/* 功能按钮栏 */}
|
||||
<div className={style["moments-action-bar"]}>
|
||||
<div className={style["action-button"]}>
|
||||
<span className={style["action-icon-text"]}></span>
|
||||
<span className={style["action-text"]}>文本</span>
|
||||
</div>
|
||||
<div className={style["action-button"]}>
|
||||
<span className={style["action-icon-image"]}></span>
|
||||
<span className={style["action-text"]}>图片</span>
|
||||
</div>
|
||||
<div className={style["action-button"]}>
|
||||
<span className={style["action-icon-video"]}></span>
|
||||
<span className={style["action-text"]}>视频</span>
|
||||
</div>
|
||||
<div
|
||||
className={style["action-button-dark"]}
|
||||
onClick={() => setShowExportPopup(true)}
|
||||
>
|
||||
<span className={style["action-icon-export"]}></span>
|
||||
<span className={style["action-text-light"]}>导出</span>
|
||||
</div>
|
||||
{/* 功能按钮栏 - 移到白色背景上 */}
|
||||
<div className={style["moments-action-bar"]}>
|
||||
<div className={style["action-button"]}>
|
||||
<FileTextOutlined />
|
||||
</div>
|
||||
<div className={style["action-button"]}>
|
||||
<PictureOutlined />
|
||||
</div>
|
||||
<div className={style["action-button"]}>
|
||||
<VideoCameraOutlined />
|
||||
</div>
|
||||
<div
|
||||
className={style["action-button-dark"]}
|
||||
onClick={() => {
|
||||
// 默认设置近7天
|
||||
const today = new Date();
|
||||
const sevenDaysAgo = dayjs(today).subtract(7, "day").toDate();
|
||||
setExportStartTime(sevenDaysAgo);
|
||||
setExportEndTime(today);
|
||||
setShowExportPopup(true);
|
||||
}}
|
||||
>
|
||||
<DownloadOutlined />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={style["moments-content"]}>
|
||||
{/* 朋友圈列表 */}
|
||||
<div className={style["moments-list"]}>
|
||||
{isFetchingMoments && moments.length === 0 ? (
|
||||
@@ -949,18 +1065,25 @@ const WechatAccountDetail: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{moments.length < momentsTotal && (
|
||||
<div className={style["moments-load-more"]}>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleLoadMoreMoments}
|
||||
loading={isFetchingMoments}
|
||||
disabled={isFetchingMoments}
|
||||
>
|
||||
加载更多
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<InfiniteScroll
|
||||
loadMore={handleLoadMoreMoments}
|
||||
hasMore={moments.length < momentsTotal}
|
||||
threshold={100}
|
||||
>
|
||||
{isFetchingMoments && moments.length > 0 && (
|
||||
<div className={style["moments-loading"]}>
|
||||
<SpinLoading color="primary" style={{ fontSize: 16 }} />
|
||||
<span style={{ marginLeft: 8, color: "#999", fontSize: 12 }}>
|
||||
加载中...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{moments.length >= momentsTotal && moments.length > 0 && (
|
||||
<div className={style["moments-no-more"]}>
|
||||
<span style={{ color: "#999", fontSize: 12 }}>没有更多了</span>
|
||||
</div>
|
||||
)}
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
</Tabs.Tab>
|
||||
|
||||
@@ -1124,7 +1247,11 @@ const WechatAccountDetail: React.FC = () => {
|
||||
{/* 朋友圈导出弹窗 */}
|
||||
<Popup
|
||||
visible={showExportPopup}
|
||||
onMaskClick={() => setShowExportPopup(false)}
|
||||
onMaskClick={() => {
|
||||
setShowExportPopup(false);
|
||||
setShowStartTimePicker(false);
|
||||
setShowEndTimePicker(false);
|
||||
}}
|
||||
bodyStyle={{ borderRadius: "16px 16px 0 0" }}
|
||||
>
|
||||
<div className={style["popup-content"]}>
|
||||
@@ -1133,7 +1260,11 @@ const WechatAccountDetail: React.FC = () => {
|
||||
<Button
|
||||
size="small"
|
||||
fill="outline"
|
||||
onClick={() => setShowExportPopup(false)}
|
||||
onClick={() => {
|
||||
setShowExportPopup(false);
|
||||
setShowStartTimePicker(false);
|
||||
setShowEndTimePicker(false);
|
||||
}}
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
@@ -1207,11 +1338,13 @@ const WechatAccountDetail: React.FC = () => {
|
||||
visible={showStartTimePicker}
|
||||
title="开始时间"
|
||||
value={exportStartTime}
|
||||
max={exportEndTime || new Date()}
|
||||
onClose={() => setShowStartTimePicker(false)}
|
||||
onConfirm={val => {
|
||||
setExportStartTime(val);
|
||||
setShowStartTimePicker(false);
|
||||
}}
|
||||
onCancel={() => setShowStartTimePicker(false)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1230,6 +1363,8 @@ const WechatAccountDetail: React.FC = () => {
|
||||
visible={showEndTimePicker}
|
||||
title="结束时间"
|
||||
value={exportEndTime}
|
||||
min={exportStartTime || undefined}
|
||||
max={new Date()}
|
||||
onClose={() => setShowEndTimePicker(false)}
|
||||
onConfirm={val => {
|
||||
setExportEndTime(val);
|
||||
@@ -1259,6 +1394,8 @@ const WechatAccountDetail: React.FC = () => {
|
||||
setExportType(undefined);
|
||||
setExportStartTime(null);
|
||||
setExportEndTime(null);
|
||||
setShowStartTimePicker(false);
|
||||
setShowEndTimePicker(false);
|
||||
}}
|
||||
style={{ marginTop: 12 }}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
import React, { useImperativeHandle, forwardRef } from "react";
|
||||
import { Form, Card, Tabs } from "antd";
|
||||
import DeviceSelection from "@/components/DeviceSelection";
|
||||
import FriendSelection from "@/components/FriendSelection";
|
||||
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
|
||||
import { FriendSelectionItem } from "@/components/FriendSelection/data";
|
||||
|
||||
interface OwnerAdminSelectorProps {
|
||||
selectedOwners: DeviceSelectionItem[];
|
||||
selectedAdmins: FriendSelectionItem[];
|
||||
onNext: (data: {
|
||||
devices: string[];
|
||||
devicesOptions: DeviceSelectionItem[];
|
||||
admins: string[];
|
||||
adminsOptions: FriendSelectionItem[];
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export interface OwnerAdminSelectorRef {
|
||||
validate: () => Promise<boolean>;
|
||||
getValues: () => any;
|
||||
}
|
||||
|
||||
const OwnerAdminSelector = forwardRef<
|
||||
OwnerAdminSelectorRef,
|
||||
OwnerAdminSelectorProps
|
||||
>(({ selectedOwners, selectedAdmins, onNext }, ref) => {
|
||||
const [form] = Form.useForm();
|
||||
const [owners, setOwners] = React.useState<DeviceSelectionItem[]>(
|
||||
selectedOwners || []
|
||||
);
|
||||
const [admins, setAdmins] = React.useState<FriendSelectionItem[]>(
|
||||
selectedAdmins || []
|
||||
);
|
||||
|
||||
// 当外部传入的 selectedOwners 或 selectedAdmins 变化时,同步内部状态
|
||||
React.useEffect(() => {
|
||||
setOwners(selectedOwners || []);
|
||||
}, [selectedOwners]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setAdmins(selectedAdmins || []);
|
||||
}, [selectedAdmins]);
|
||||
|
||||
// 暴露方法给父组件
|
||||
useImperativeHandle(ref, () => ({
|
||||
validate: async () => {
|
||||
// 验证群主和管理员
|
||||
if (owners.length === 0) {
|
||||
form.setFields([
|
||||
{
|
||||
name: "devices",
|
||||
errors: ["请选择一个群主"],
|
||||
},
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
if (owners.length > 1) {
|
||||
form.setFields([
|
||||
{
|
||||
name: "devices",
|
||||
errors: ["群主只能选择一个设备"],
|
||||
},
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
if (admins.length === 0) {
|
||||
form.setFields([
|
||||
{
|
||||
name: "admins",
|
||||
errors: ["请至少选择一个管理员"],
|
||||
},
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
// 清除错误
|
||||
form.setFields([
|
||||
{
|
||||
name: "devices",
|
||||
errors: [],
|
||||
},
|
||||
{
|
||||
name: "admins",
|
||||
errors: [],
|
||||
},
|
||||
]);
|
||||
return true;
|
||||
},
|
||||
getValues: () => {
|
||||
return {
|
||||
devices: owners.map(o => o.id.toString()),
|
||||
admins: admins.map(a => a.id.toString()),
|
||||
devicesOptions: owners,
|
||||
adminsOptions: admins,
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
// 群主选择(设备选择)
|
||||
const handleOwnersSelect = (selectedDevices: DeviceSelectionItem[]) => {
|
||||
const previousOwnerId = owners.length > 0 ? owners[0]?.id : null;
|
||||
const newOwnerId = selectedDevices.length > 0 ? selectedDevices[0]?.id : null;
|
||||
|
||||
// 当群主改变时,清空已选的管理员(因为筛选条件变了)
|
||||
const shouldClearAdmins = previousOwnerId !== newOwnerId;
|
||||
|
||||
setOwners(selectedDevices);
|
||||
const ownerIds = selectedDevices.map(d => d.id.toString());
|
||||
form.setFieldValue("devices", ownerIds);
|
||||
|
||||
if (shouldClearAdmins) {
|
||||
setAdmins([]);
|
||||
form.setFieldValue("admins", []);
|
||||
}
|
||||
|
||||
// 通知父组件数据变化
|
||||
onNext({
|
||||
devices: ownerIds,
|
||||
devicesOptions: selectedDevices,
|
||||
admins: shouldClearAdmins ? [] : admins.map(a => a.id.toString()),
|
||||
adminsOptions: shouldClearAdmins ? [] : admins,
|
||||
});
|
||||
};
|
||||
|
||||
// 管理员选择
|
||||
const handleAdminsSelect = (selectedFriends: FriendSelectionItem[]) => {
|
||||
setAdmins(selectedFriends);
|
||||
const adminIds = selectedFriends.map(f => f.id.toString());
|
||||
form.setFieldValue("admins", adminIds);
|
||||
// 通知父组件数据变化
|
||||
onNext({
|
||||
devices: owners.map(o => o.id.toString()),
|
||||
devicesOptions: owners,
|
||||
admins: adminIds,
|
||||
adminsOptions: selectedFriends,
|
||||
});
|
||||
};
|
||||
|
||||
const tabItems = [
|
||||
{
|
||||
key: "devices",
|
||||
label: `群主 (${owners.length})`,
|
||||
children: (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<p style={{ margin: "0 0 8px 0", color: "#666", fontSize: 14 }}>
|
||||
请选择一个群主(设备),该设备将作为新建群聊的群主
|
||||
</p>
|
||||
</div>
|
||||
<Form.Item
|
||||
name="devices"
|
||||
validateStatus={owners.length === 0 || owners.length > 1 ? "error" : ""}
|
||||
help={
|
||||
owners.length === 0
|
||||
? "请选择一个群主(设备)"
|
||||
: owners.length > 1
|
||||
? "群主只能选择一个设备"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<DeviceSelection
|
||||
selectedOptions={owners}
|
||||
onSelect={handleOwnersSelect}
|
||||
placeholder="选择群主(设备)"
|
||||
singleSelect={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "admins",
|
||||
label: `管理员 (${admins.length})`,
|
||||
children: (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<p style={{ margin: "0 0 8px 0", color: "#666", fontSize: 14 }}>
|
||||
{owners.length === 0
|
||||
? "请先选择群主(设备),然后选择该设备下的好友作为管理员"
|
||||
: "请选择管理员,管理员将协助管理新建的群聊(仅显示所选设备下的好友)"}
|
||||
</p>
|
||||
</div>
|
||||
<Form.Item
|
||||
name="admins"
|
||||
validateStatus={admins.length === 0 ? "error" : ""}
|
||||
help={
|
||||
owners.length === 0
|
||||
? "请先选择群主(设备)"
|
||||
: admins.length === 0
|
||||
? "请至少选择一个管理员"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<FriendSelection
|
||||
selectedOptions={admins}
|
||||
onSelect={handleAdminsSelect}
|
||||
placeholder={owners.length === 0 ? "请先选择群主" : "选择管理员"}
|
||||
deviceIds={owners.length > 0 ? owners.map(d => d.id) : []}
|
||||
enableDeviceFilter={true}
|
||||
readonly={owners.length === 0}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
devices: (selectedOwners || []).map(item => item.id.toString()),
|
||||
admins: (selectedAdmins || []).map(item => item.id.toString()),
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600 }}>
|
||||
选择群主和管理员
|
||||
</h2>
|
||||
<p style={{ margin: "8px 0 0 0", color: "#666", fontSize: 14 }}>
|
||||
请选择一个群主(设备)和管理员(好友),他们将负责管理新建的群聊
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs items={tabItems} />
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
|
||||
OwnerAdminSelector.displayName = "OwnerAdminSelector";
|
||||
|
||||
export default OwnerAdminSelector;
|
||||
@@ -7,24 +7,29 @@ import { createAutoGroup, updateAutoGroup, getAutoGroupDetail } from "./api";
|
||||
import { AutoGroupFormData, StepItem } from "./types";
|
||||
import StepIndicator from "@/components/StepIndicator";
|
||||
import BasicSettings, { BasicSettingsRef } from "./components/BasicSettings";
|
||||
import DeviceSelector, { DeviceSelectorRef } from "./components/DeviceSelector";
|
||||
import OwnerAdminSelector, {
|
||||
OwnerAdminSelectorRef,
|
||||
} from "./components/OwnerAdminSelector";
|
||||
import PoolSelector, { PoolSelectorRef } from "./components/PoolSelector";
|
||||
import NavCommon from "@/components/NavCommon/index";
|
||||
import dayjs from "dayjs";
|
||||
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
|
||||
import { FriendSelectionItem } from "@/components/FriendSelection/data";
|
||||
import { PoolSelectionItem } from "@/components/PoolSelection/data";
|
||||
|
||||
const steps: StepItem[] = [
|
||||
{ id: 1, title: "步骤 1", subtitle: "基础设置" },
|
||||
{ id: 2, title: "步骤 2", subtitle: "选择设备" },
|
||||
{ id: 2, title: "步骤 2", subtitle: "选择群主和管理员" },
|
||||
{ id: 3, title: "步骤 3", subtitle: "选择流量池包" },
|
||||
];
|
||||
|
||||
const defaultForm: AutoGroupFormData = {
|
||||
name: "",
|
||||
type: 4,
|
||||
deviceGroups: [], // 设备组
|
||||
deviceGroupsOptions: [], // 设备组选项
|
||||
devices: [], // 群主ID列表
|
||||
devicesOptions: [], // 群主选项
|
||||
admins: [], // 管理员ID列表
|
||||
adminsOptions: [], // 管理员选项
|
||||
poolGroups: [], // 内容库
|
||||
poolGroupsOptions: [], // 内容库选项
|
||||
startTime: dayjs().format("HH:mm"), // 开始时间 (HH:mm)
|
||||
@@ -45,16 +50,15 @@ const AutoGroupForm: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [dataLoaded, setDataLoaded] = useState(!isEdit); // 非编辑模式直接标记为已加载
|
||||
const [formData, setFormData] = useState<AutoGroupFormData>(defaultForm);
|
||||
const [deviceGroupsOptions, setDeviceGroupsOptions] = useState<
|
||||
DeviceSelectionItem[]
|
||||
>([]);
|
||||
const [devicesOptions, setDevicesOptions] = useState<DeviceSelectionItem[]>([]);
|
||||
const [adminsOptions, setAdminsOptions] = useState<FriendSelectionItem[]>([]);
|
||||
const [poolGroupsOptions, setpoolGroupsOptions] = useState<
|
||||
PoolSelectionItem[]
|
||||
>([]);
|
||||
|
||||
// 创建子组件的ref
|
||||
const basicSettingsRef = useRef<BasicSettingsRef>(null);
|
||||
const deviceSelectorRef = useRef<DeviceSelectorRef>(null);
|
||||
const ownerAdminSelectorRef = useRef<OwnerAdminSelectorRef>(null);
|
||||
const poolSelectorRef = useRef<PoolSelectorRef>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -64,8 +68,10 @@ const AutoGroupForm: React.FC = () => {
|
||||
const updatedForm = {
|
||||
...defaultForm,
|
||||
name: res.name,
|
||||
deviceGroups: res.config.deviceGroups || [],
|
||||
deviceGroupsOptions: res.config.deviceGroupsOptions || [],
|
||||
devices: res.config.deviceGroups || res.config.devices || [], // 兼容deviceGroups和devices
|
||||
devicesOptions: res.config.deviceGroupsOptions || res.config.devicesOptions || [], // 兼容deviceGroupsOptions和devicesOptions
|
||||
admins: res.config.admins || [],
|
||||
adminsOptions: res.config.adminsOptions || [],
|
||||
poolGroups: res.config.poolGroups || [],
|
||||
poolGroupsOptions: res.config.poolGroupsOptions || [],
|
||||
startTime: res.config.startTime,
|
||||
@@ -80,7 +86,8 @@ const AutoGroupForm: React.FC = () => {
|
||||
id: res.id,
|
||||
};
|
||||
setFormData(updatedForm);
|
||||
setDeviceGroupsOptions(res.config.deviceGroupsOptions || []);
|
||||
setDevicesOptions(res.config.deviceGroupsOptions || res.config.devicesOptions || []); // 兼容deviceGroupsOptions和devicesOptions
|
||||
setAdminsOptions(res.config.adminsOptions || []);
|
||||
setpoolGroupsOptions(res.config.poolGroupsOptions || []);
|
||||
setDataLoaded(true); // 标记数据已加载
|
||||
});
|
||||
@@ -90,16 +97,20 @@ const AutoGroupForm: React.FC = () => {
|
||||
setFormData(prev => ({ ...prev, ...values }));
|
||||
};
|
||||
|
||||
// 设备组选择
|
||||
const handleDevicesChange = (data: {
|
||||
deviceGroups: string[];
|
||||
deviceGroupsOptions: DeviceSelectionItem[];
|
||||
// 群主和管理员选择
|
||||
const handleOwnerAdminChange = (data: {
|
||||
devices: string[];
|
||||
devicesOptions: DeviceSelectionItem[];
|
||||
admins: string[];
|
||||
adminsOptions: FriendSelectionItem[];
|
||||
}) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
deviceGroups: data.deviceGroups,
|
||||
devices: data.devices,
|
||||
admins: data.admins,
|
||||
}));
|
||||
setDeviceGroupsOptions(data.deviceGroupsOptions);
|
||||
setDevicesOptions(data.devicesOptions);
|
||||
setAdminsOptions(data.adminsOptions);
|
||||
};
|
||||
|
||||
// 流量池包选择
|
||||
@@ -116,8 +127,16 @@ const AutoGroupForm: React.FC = () => {
|
||||
Toast.show({ content: "请输入任务名称" });
|
||||
return;
|
||||
}
|
||||
if (formData.deviceGroups.length === 0) {
|
||||
Toast.show({ content: "请选择至少一个设备组" });
|
||||
if (formData.devices.length === 0) {
|
||||
Toast.show({ content: "请选择一个群主" });
|
||||
return;
|
||||
}
|
||||
if (formData.devices.length > 1) {
|
||||
Toast.show({ content: "群主只能选择一个设备" });
|
||||
return;
|
||||
}
|
||||
if (formData.admins.length === 0) {
|
||||
Toast.show({ content: "请至少选择一个管理员" });
|
||||
return;
|
||||
}
|
||||
if (formData.poolGroups.length === 0) {
|
||||
@@ -127,9 +146,13 @@ const AutoGroupForm: React.FC = () => {
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// 构建提交数据,将devices映射为deviceGroups
|
||||
const { devices, devicesOptions, ...restFormData } = formData;
|
||||
const submitData = {
|
||||
...formData,
|
||||
deviceGroupsOptions: deviceGroupsOptions,
|
||||
...restFormData,
|
||||
deviceGroups: devices, // 设备ID数组,传输字段名为deviceGroups
|
||||
deviceGroupsOptions: devicesOptions, // 设备完整信息,传输字段名为deviceGroupsOptions
|
||||
adminsOptions: adminsOptions,
|
||||
poolGroupsOptions: poolGroupsOptions,
|
||||
};
|
||||
|
||||
@@ -173,8 +196,9 @@ const AutoGroupForm: React.FC = () => {
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// 调用 DeviceSelector 的表单校验
|
||||
isValid = (await deviceSelectorRef.current?.validate()) || false;
|
||||
// 调用 OwnerAdminSelector 的表单校验
|
||||
isValid =
|
||||
(await ownerAdminSelectorRef.current?.validate()) || false;
|
||||
if (isValid) {
|
||||
setCurrentStep(3);
|
||||
}
|
||||
@@ -217,10 +241,11 @@ const AutoGroupForm: React.FC = () => {
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<DeviceSelector
|
||||
ref={deviceSelectorRef}
|
||||
selectedDevices={deviceGroupsOptions}
|
||||
onNext={handleDevicesChange}
|
||||
<OwnerAdminSelector
|
||||
ref={ownerAdminSelectorRef}
|
||||
selectedOwners={devicesOptions}
|
||||
selectedAdmins={adminsOptions}
|
||||
onNext={handleOwnerAdminChange}
|
||||
/>
|
||||
);
|
||||
case 3:
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
|
||||
import { PoolSelectionItem } from "@/components/PoolSelection/data";
|
||||
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
|
||||
import { FriendSelectionItem } from "@/components/FriendSelection/data";
|
||||
|
||||
// 自动建群表单数据类型定义
|
||||
export interface AutoGroupFormData {
|
||||
id?: string; // 任务ID
|
||||
type: number; // 任务类型
|
||||
name: string; // 任务名称
|
||||
deviceGroups: string[]; // 设备组
|
||||
deviceGroupsOptions: DeviceSelectionItem[]; // 设备组选项
|
||||
devices: string[]; // 群主ID列表(设备ID)
|
||||
devicesOptions: DeviceSelectionItem[]; // 群主选项(设备)
|
||||
admins: string[]; // 管理员ID列表(好友ID)
|
||||
adminsOptions: FriendSelectionItem[]; // 管理员选项(好友)
|
||||
poolGroups: string[]; // 流量池
|
||||
poolGroupsOptions: PoolSelectionItem[]; // 流量池选项
|
||||
startTime: string; // 开始时间 (YYYY-MM-DD HH:mm:ss)
|
||||
@@ -34,9 +37,13 @@ export const formValidationRules = {
|
||||
{ required: true, message: "请输入任务名称" },
|
||||
{ min: 2, max: 50, message: "任务名称长度应在2-50个字符之间" },
|
||||
],
|
||||
deviceGroups: [
|
||||
{ required: true, message: "请选择设备组" },
|
||||
{ type: "array", min: 1, message: "至少选择一个设备组" },
|
||||
devices: [
|
||||
{ required: true, message: "请选择群主" },
|
||||
{ type: "array", min: 1, max: 1, message: "群主只能选择一个设备" },
|
||||
],
|
||||
admins: [
|
||||
{ required: true, message: "请选择管理员" },
|
||||
{ type: "array", min: 1, message: "至少选择一个管理员" },
|
||||
],
|
||||
poolGroups: [
|
||||
{ required: true, message: "请选择内容库" },
|
||||
|
||||
@@ -34,6 +34,7 @@ class WorkbenchGroupCreateCommand extends Command
|
||||
|
||||
// 检查队列是否已经在运行
|
||||
$queueLockKey = "queue_lock:{$this->queueName}";
|
||||
Cache::rm($queueLockKey);
|
||||
if (Cache::get($queueLockKey)) {
|
||||
$output->writeln("队列 {$this->queueName} 已经在运行中,跳过执行");
|
||||
Log::warning("队列 {$this->queueName} 已经在运行中,跳过执行");
|
||||
|
||||
@@ -140,6 +140,7 @@ class WorkbenchController extends Controller
|
||||
$config->groupDescription = $param['groupDescription'];
|
||||
$config->poolGroups = json_encode($param['poolGroups'] ?? []);
|
||||
$config->wechatGroups = json_encode($param['wechatGroups'] ?? []);
|
||||
$config->admins = json_encode($param['admins'] ?? [], JSON_UNESCAPED_UNICODE);
|
||||
$config->createTime = time();
|
||||
$config->updateTime = time();
|
||||
$config->save();
|
||||
@@ -229,7 +230,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');
|
||||
$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');
|
||||
@@ -348,6 +349,18 @@ class WorkbenchController extends Controller
|
||||
$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) ?: [];
|
||||
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;
|
||||
} else {
|
||||
$item->config->adminsOptions = [];
|
||||
}
|
||||
}
|
||||
unset($item->groupCreate, $item->group_create);
|
||||
break;
|
||||
@@ -457,7 +470,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');
|
||||
$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');
|
||||
@@ -567,6 +580,7 @@ class WorkbenchController extends Controller
|
||||
$workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
|
||||
$workbench->config->poolGroups = json_decode($workbench->config->poolGroups, true);
|
||||
$workbench->config->wechatGroups = json_decode($workbench->config->wechatGroups, true);
|
||||
$workbench->config->admins = json_decode($workbench->config->admins ?? '[]', true) ?: [];
|
||||
unset($workbench->groupCreate, $workbench->group_create);
|
||||
}
|
||||
break;
|
||||
@@ -761,6 +775,18 @@ class WorkbenchController extends Controller
|
||||
$workbench->config->ownerWechatOptions = [];
|
||||
}
|
||||
|
||||
// 获取管理员选项(自动建群)
|
||||
if ($workbench->type == self::TYPE_GROUP_CREATE && !empty($workbench->config->admins)) {
|
||||
$adminOptions = Db::table('s2_wechat_friend')->alias('wf')
|
||||
->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
|
||||
->where('wf.id', 'in', $workbench->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();
|
||||
$workbench->config->adminsOptions = $adminOptions;
|
||||
} else {
|
||||
$workbench->config->adminsOptions = [];
|
||||
}
|
||||
|
||||
return json(['code' => 200, 'msg' => '获取成功', 'data' => $workbench]);
|
||||
}
|
||||
@@ -882,6 +908,7 @@ class WorkbenchController extends Controller
|
||||
$config->groupDescription = $param['groupDescription'];
|
||||
$config->poolGroups = json_encode($param['poolGroups'] ?? []);
|
||||
$config->wechatGroups = json_encode($param['wechatGroups'] ?? []);
|
||||
$config->admins = json_encode($param['admins'] ?? [], JSON_UNESCAPED_UNICODE);
|
||||
$config->updateTime = time();
|
||||
$config->save();
|
||||
}
|
||||
@@ -1109,6 +1136,7 @@ class WorkbenchController extends Controller
|
||||
$newConfig->groupDescription = $config->groupDescription;
|
||||
$newConfig->poolGroups = $config->poolGroups;
|
||||
$newConfig->wechatGroups = $config->wechatGroups;
|
||||
$newConfig->admins = $config->admins ?? json_encode([], JSON_UNESCAPED_UNICODE);
|
||||
$newConfig->createTime = time();
|
||||
$newConfig->updateTime = time();
|
||||
$newConfig->save();
|
||||
|
||||
@@ -67,7 +67,7 @@ class PostTransferFriends extends BaseController
|
||||
'endTime' => '18:00',
|
||||
'remarkType' => 'phone',
|
||||
'addFriendInterval' => 60,
|
||||
'greeting' => !empty($greeting) ? $greeting :'这个是'. $wechat['nickname'] .'的新号,之前那个号没用了,重新加一下您'
|
||||
'greeting' => !empty($greeting) ? $greeting :'我是'. $wechat['nickname'] .'的新号,请通过'
|
||||
];
|
||||
|
||||
if (!empty($firstMessage)){
|
||||
|
||||
155
Server/application/job/WorkbenchGroupCreateAdminFriendJob.php
Normal file
155
Server/application/job/WorkbenchGroupCreateAdminFriendJob.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace app\job;
|
||||
|
||||
use app\api\controller\WebSocketController;
|
||||
use think\facade\Log;
|
||||
use think\facade\Env;
|
||||
use think\Db;
|
||||
use think\queue\Job;
|
||||
use think\facade\Cache;
|
||||
use think\facade\Config;
|
||||
|
||||
/**
|
||||
* 工作台群创建-拉管理员好友任务
|
||||
* Class WorkbenchGroupCreateAdminFriendJob
|
||||
* @package app\job
|
||||
*/
|
||||
class WorkbenchGroupCreateAdminFriendJob
|
||||
{
|
||||
/**
|
||||
* 最大重试次数
|
||||
*/
|
||||
const MAX_RETRY_ATTEMPTS = 3;
|
||||
|
||||
/**
|
||||
* 成员类型常量
|
||||
*/
|
||||
const MEMBER_TYPE_ADMIN_FRIEND = 4;
|
||||
|
||||
/**
|
||||
* 状态常量
|
||||
*/
|
||||
const STATUS_SUCCESS = 2;
|
||||
const STATUS_ADMIN_FRIEND_ADDED = 4;
|
||||
|
||||
/**
|
||||
* 队列任务处理
|
||||
* @param Job $job 队列任务
|
||||
* @param array $data 任务数据
|
||||
* @return bool
|
||||
*/
|
||||
public function fire(Job $job, $data)
|
||||
{
|
||||
$workbenchId = $data['workbenchId'] ?? 0;
|
||||
$wechatAccountId = $data['wechatAccountId'] ?? 0;
|
||||
$groupId = $data['groupId'] ?? 0;
|
||||
$chatroomId = $data['chatroomId'] ?? '';
|
||||
$adminFriendIds = $data['adminFriendIds'] ?? [];
|
||||
$poolUsers = $data['poolUsers'] ?? [];
|
||||
|
||||
try {
|
||||
if (empty($adminFriendIds) || empty($poolUsers)) {
|
||||
Log::info("管理员好友或流量池用户为空,跳过。工作台ID: {$workbenchId}");
|
||||
$job->delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取管理员信息
|
||||
$adminFriends = Db::table('s2_wechat_friend')
|
||||
->where('id', 'in', $adminFriendIds)
|
||||
->column('id,wechatId,ownerWechatId');
|
||||
|
||||
if (empty($adminFriends)) {
|
||||
Log::warning("未找到管理员好友信息。工作台ID: {$workbenchId}");
|
||||
$job->delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取微信账号信息
|
||||
$wechatAccount = Db::table('s2_wechat_account')->where('id', $wechatAccountId)->find();
|
||||
if (empty($wechatAccount)) {
|
||||
Log::error("未找到微信账号。微信账号ID: {$wechatAccountId}");
|
||||
$job->delete();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 从流量池用户中查找每个管理员的好友
|
||||
// 管理员的好友:从s2_wechat_friend表中查找,ownerWechatId=管理员的wechatId,且wechatId在流量池用户中
|
||||
$allAdminFriendIds = [];
|
||||
foreach ($adminFriends as $adminFriend) {
|
||||
$adminWechatId = $adminFriend['wechatId'];
|
||||
|
||||
// 从好友表中查找该管理员的好友(在流量池用户中)
|
||||
$adminFriendsList = Db::table('s2_wechat_friend')
|
||||
->where('ownerWechatId', $adminWechatId)
|
||||
->whereIn('wechatId', $poolUsers)
|
||||
->column('id,wechatId');
|
||||
|
||||
if (!empty($adminFriendsList)) {
|
||||
$allAdminFriendIds = array_merge($allAdminFriendIds, array_keys($adminFriendsList));
|
||||
}
|
||||
}
|
||||
|
||||
$allAdminFriendIds = array_unique($allAdminFriendIds);
|
||||
|
||||
if (empty($allAdminFriendIds)) {
|
||||
Log::info("未找到管理员的好友,跳过拉人。工作台ID: {$workbenchId}");
|
||||
$job->delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
// 初始化WebSocket
|
||||
$toAccountId = '';
|
||||
$username = Env::get('api.username2', '');
|
||||
$password = Env::get('api.password2', '');
|
||||
if (!empty($username) || !empty($password)) {
|
||||
$toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId');
|
||||
}
|
||||
$webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]);
|
||||
|
||||
// 拉管理员好友进群
|
||||
$inviteResult = $webSocket->CmdChatroomInvite([
|
||||
'wechatChatroomId' => $groupId,
|
||||
'wechatFriendIds' => $allAdminFriendIds
|
||||
]);
|
||||
|
||||
// 记录管理员好友进群
|
||||
$installData = [];
|
||||
foreach ($allAdminFriendIds as $friendId) {
|
||||
$friendInfo = Db::table('s2_wechat_friend')->where('id', $friendId)->find();
|
||||
$installData[] = [
|
||||
'workbenchId' => $workbenchId,
|
||||
'friendId' => $friendId,
|
||||
'wechatId' => $friendInfo['wechatId'] ?? '',
|
||||
'groupId' => $groupId,
|
||||
'wechatAccountId' => $wechatAccountId,
|
||||
'status' => self::STATUS_ADMIN_FRIEND_ADDED,
|
||||
'memberType' => self::MEMBER_TYPE_ADMIN_FRIEND,
|
||||
'retryCount' => 0,
|
||||
'chatroomId' => $chatroomId,
|
||||
'createTime' => time(),
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($installData)) {
|
||||
Db::name('workbench_group_create_item')->insertAll($installData);
|
||||
Log::info("管理员好友已拉入群。工作台ID: {$workbenchId}, 群ID: {$groupId}, 好友数: " . count($installData));
|
||||
}
|
||||
|
||||
$job->delete();
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
Log::error("拉管理员好友任务异常:{$e->getMessage()}");
|
||||
|
||||
if ($job->attempts() > self::MAX_RETRY_ATTEMPTS) {
|
||||
$job->delete();
|
||||
} else {
|
||||
$job->release(Config::get('queue.failed_delay', 10));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use app\cunkebao\model\Workbench;
|
||||
use app\cunkebao\model\WorkbenchGroupCreate;
|
||||
use app\api\model\WechatFriendModel as WechatFriend;
|
||||
use app\api\model\WechatMomentsModel as WechatMoments;
|
||||
use app\common\model\DeviceWechatLogin as DeviceWechatLoginModel;
|
||||
use think\facade\Log;
|
||||
use think\facade\Env;
|
||||
use think\Db;
|
||||
@@ -16,6 +17,7 @@ use think\facade\Config;
|
||||
use app\api\controller\MomentsController as Moments;
|
||||
use Workerman\Lib\Timer;
|
||||
use app\api\controller\WechatController;
|
||||
use think\Queue;
|
||||
|
||||
/**
|
||||
* 工作台群创建任务
|
||||
@@ -50,6 +52,23 @@ class WorkbenchGroupCreateJob
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 成员类型常量
|
||||
*/
|
||||
const MEMBER_TYPE_OWNER = 1; // 群主成员
|
||||
const MEMBER_TYPE_ADMIN = 2; // 管理员
|
||||
const MEMBER_TYPE_OWNER_FRIEND = 3; // 群主好友
|
||||
const MEMBER_TYPE_ADMIN_FRIEND = 4; // 管理员好友
|
||||
|
||||
/**
|
||||
* 状态常量
|
||||
*/
|
||||
const STATUS_PENDING = 0; // 待创建
|
||||
const STATUS_CREATING = 1; // 创建中
|
||||
const STATUS_SUCCESS = 2; // 创建成功
|
||||
const STATUS_FAILED = 3; // 创建失败
|
||||
const STATUS_ADMIN_FRIEND_ADDED = 4; // 管理员好友已拉入
|
||||
|
||||
/**
|
||||
* 执行任务
|
||||
* @throws \Exception
|
||||
@@ -57,7 +76,7 @@ class WorkbenchGroupCreateJob
|
||||
public function execute()
|
||||
{
|
||||
try {
|
||||
// 获取所有工作台
|
||||
// 1. 查询启用了建群功能的数据
|
||||
$workbenches = Workbench::where(['status' => 1, 'type' => 4, 'isDel' => 0])->order('id desc')->select();
|
||||
foreach ($workbenches as $workbench) {
|
||||
// 获取工作台配置
|
||||
@@ -65,158 +84,356 @@ class WorkbenchGroupCreateJob
|
||||
if (!$config) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 解析配置
|
||||
$config['poolGroups'] = json_decode($config['poolGroups'], true);
|
||||
$config['devices'] = json_decode($config['devices'], true);
|
||||
$config['admins'] = json_decode($config['admins'] ?? '[]', true) ?: [];
|
||||
|
||||
if (empty($config['poolGroups']) || empty($config['devices'])) {
|
||||
continue;
|
||||
}
|
||||
//群主及内部成员
|
||||
$groupMember = Db::name('device_wechat_login')->alias('dwl')
|
||||
->join(['s2_wechat_account' => 'a'], 'dwl.wechatId = a.wechatId')
|
||||
->whereIn('dwl.deviceId', $config['devices'])
|
||||
->group('a.id')
|
||||
->column('a.wechatId');
|
||||
if (empty($groupMember)) {
|
||||
$groupMember = [];
|
||||
$wechatId = Db::name('device_wechat_login')
|
||||
->whereIn('deviceId',$config['devices'])
|
||||
->order('id desc')
|
||||
->value('wechatId');
|
||||
if (empty($wechatId)) {
|
||||
continue;
|
||||
}
|
||||
$groupMemberWechatId = Db::table('s2_wechat_friend')
|
||||
->where('ownerWechatId', $groupMember[0])
|
||||
->whereIn('wechatId', $groupMember)
|
||||
->column('id,wechatId');
|
||||
$groupMember[] = $wechatId;
|
||||
// 获取群主好友ID映射(所有群主的好友)
|
||||
$groupMemberWechatId = [];
|
||||
$groupMemberId = [];
|
||||
|
||||
foreach ($groupMember as $ownerWechatId) {
|
||||
$friends = Db::table('s2_wechat_friend')
|
||||
->where('ownerWechatId', $ownerWechatId)
|
||||
->whereIn('wechatId', $groupMember)
|
||||
->field('id,wechatId')
|
||||
->select();
|
||||
|
||||
foreach ($friends as $friend) {
|
||||
if (!isset($groupMemberWechatId[$friend['id']])) {
|
||||
$groupMemberWechatId[$friend['id']] = $friend['wechatId'];
|
||||
$groupMemberId[] = $friend['id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($groupMemberWechatId)) {
|
||||
continue;
|
||||
}
|
||||
$groupMemberId = array_keys($groupMemberWechatId);
|
||||
|
||||
|
||||
//流量池用户
|
||||
// 获取流量池用户
|
||||
$poolItem = Db::name('traffic_source_package_item')
|
||||
->whereIn('packageId', $config['poolGroups'])
|
||||
->group('identifier')
|
||||
->column('identifier');
|
||||
|
||||
|
||||
if (empty($poolItem)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//群用户
|
||||
// 获取已入群的用户(排除已成功入群的)
|
||||
$groupUser = Db::name('workbench_group_create_item')
|
||||
->where('workbenchId', $workbench->id)
|
||||
->where('status', 'in', [self::STATUS_SUCCESS, self::STATUS_ADMIN_FRIEND_ADDED])
|
||||
->whereIn('wechatId', $poolItem)
|
||||
->group('wechatId')
|
||||
->column('wechatId');
|
||||
|
||||
//待入群的用户
|
||||
// 待入群的用户
|
||||
$joinUser = array_diff($poolItem, $groupUser);
|
||||
if (empty($joinUser)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//随机群人数
|
||||
// 计算随机群人数(不包含管理员,只减去群主成员数)
|
||||
$groupRandNum = mt_rand($config['groupSizeMin'], $config['groupSizeMax']) - count($groupMember);
|
||||
|
||||
//待加入用户
|
||||
// 分批处理待入群用户
|
||||
$addGroupUser = [];
|
||||
$totalRows = count($joinUser);
|
||||
for ($i = 0; $i < $totalRows; $i += $groupRandNum) {
|
||||
$batchRows = array_slice($joinUser, $i, $groupRandNum);
|
||||
if (!empty($batchRows)) {
|
||||
$user = [];
|
||||
foreach ($batchRows as $row) {
|
||||
$user[] = $row;
|
||||
}
|
||||
$addGroupUser[] = $user;
|
||||
$addGroupUser[] = $batchRows;
|
||||
}
|
||||
}
|
||||
foreach ($addGroupUser as $key => $val) {
|
||||
//判断第一组用户是否满足创建群的条件
|
||||
$friendIds = Db::name('wechat_friendship')->alias('f')
|
||||
->join(['s2_wechat_account' => 'a'], 'f.ownerWechatId=a.wechatId')
|
||||
->where('f.companyId', $workbench->companyId)
|
||||
->whereIn('f.wechatId', $val)
|
||||
->group('f.wechatId')
|
||||
->column('f.id,f.wechatId,a.id as wechatAccountId');
|
||||
// 初始化WebSocket
|
||||
$toAccountId = '';
|
||||
$username = Env::get('api.username2', '');
|
||||
$password = Env::get('api.password2', '');
|
||||
if (!empty($username) || !empty($password)) {
|
||||
$toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId');
|
||||
}
|
||||
$webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]);
|
||||
|
||||
// 整理数组:按wechatAccountId分组,值为对应的id数组
|
||||
$groupedFriends = [];
|
||||
$wechatAccountIds = [];
|
||||
$wechatIds = [];
|
||||
foreach ($friendIds as $friend) {
|
||||
$wechatAccountId = $friend['wechatAccountId'];
|
||||
if (!in_array($wechatAccountId, $wechatAccountIds)) {
|
||||
$wechatAccountIds[] = $wechatAccountId;
|
||||
}
|
||||
$friendId = $friend['id'];
|
||||
if (!isset($groupedFriends[$wechatAccountId])) {
|
||||
$groupedFriends[$wechatAccountId] = [];
|
||||
}
|
||||
$groupedFriends[$wechatAccountId][] = $friendId;
|
||||
$wechatIds[$friendId] = $friend['wechatId'];
|
||||
}
|
||||
//==================== 群相关功能开始 ===========================
|
||||
$toAccountId = '';
|
||||
$username = Env::get('api.username2', '');
|
||||
$password = Env::get('api.password2', '');
|
||||
if (!empty($username) || !empty($password)) {
|
||||
$toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId');
|
||||
}
|
||||
$webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]);
|
||||
//$webSocket = new WebSocketController(['userName' => 'wz_03', 'password' => 'key123456', 'accountId' => 5015]);
|
||||
//拉人进群 $webSocket->CmdChatroomInvite(['wechatChatroomId' => 830794, 'wechatFriendIds' => [21168549]]);
|
||||
//修改群名称 $webSocket->CmdChatroomModifyInfo(['wechatChatroomId' => 830794, 'wechatAccountId' => 300745,'chatroomName' => 'test111']);
|
||||
//修改群公告 $webSocket->CmdChatroomModifyInfo(['wechatChatroomId' => 830794, 'wechatAccountId' => 300745,'announce' => 'test111']);
|
||||
//建群 $webSocket->CmdChatroomCreate(['chatroomName' => '聊天测试群', 'wechatFriendIds' => [17453051,17453058],'wechatAccountId' => 300745]);
|
||||
foreach ($groupedFriends as $wechatAccountId => $friendId) {
|
||||
//列出所有群
|
||||
$group = '';
|
||||
$groupMemberNum = 0;
|
||||
$groupIds = Db::name('workbench_group_create_item')->where(['workbenchId' => $workbench->id])->group('groupId')->column('groupId');
|
||||
if (!empty($groupIds)) {
|
||||
//最新创建的群
|
||||
$group = Db::name('wechat_group')->where(['wechatAccountId' => $wechatAccountId])->whereIn('id', $groupIds)->order('createTime DESC')->find();
|
||||
//群用户数量
|
||||
if (!empty($group)) {
|
||||
$groupMemberNum = Db::name('wechat_group_member')->where('groupId', $group['id'])->count();
|
||||
}
|
||||
}
|
||||
|
||||
//拉群或者建群
|
||||
$wechatFriendIds = array_merge($friendId, $groupMemberId);
|
||||
|
||||
if ($groupMemberNum == 0 || (count($wechatFriendIds) + $groupMemberNum) >= $groupRandNum) {
|
||||
if (count($groupIds) > 0) {
|
||||
$chatroomName = $config['groupNameTemplate'] . count($groupIds) + 1 . '群';
|
||||
} else {
|
||||
$chatroomName = $config['groupNameTemplate'];
|
||||
}
|
||||
$webSocket->CmdChatroomCreate(['chatroomName' => $chatroomName, 'wechatFriendIds' => $wechatFriendIds,'wechatAccountId' => $wechatAccountId]);
|
||||
} else {
|
||||
$webSocket->CmdChatroomInvite(['wechatChatroomId' => $group['id'], 'wechatFriendIds' => $wechatFriendIds]);
|
||||
}
|
||||
|
||||
$installData = [];
|
||||
|
||||
//记录进群人员
|
||||
foreach ($wechatFriendIds as $v) {
|
||||
$installData[] = [
|
||||
'workbenchId' => $workbench->id,
|
||||
'friendId' => $v,
|
||||
'wechatId' => !empty($wechatIds[$v]) ? $wechatIds[$v] : $groupMemberWechatId[$v],
|
||||
'groupId' => 0,
|
||||
'wechatAccountId' => $wechatAccountId,
|
||||
'createTime' => time(),
|
||||
];
|
||||
}
|
||||
Db::name('workbench_group_create_item')->insertAll($installData);
|
||||
}
|
||||
// 遍历每批用户
|
||||
foreach ($addGroupUser as $batchUsers) {
|
||||
$this->processBatchUsers($workbench, $config, $batchUsers, $groupMemberId, $groupMemberWechatId, $groupRandNum, $webSocket);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error("消息群发任务异常: " . $e->getMessage());
|
||||
Log::error("工作台建群任务异常: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理一批用户
|
||||
* @param Workbench $workbench 工作台
|
||||
* @param array $config 配置
|
||||
* @param array $batchUsers 批次用户(微信ID数组,来自流量池)
|
||||
* @param array $groupMemberId 群主成员ID数组
|
||||
* @param array $groupMemberWechatId 群主成员微信ID映射
|
||||
* @param int $groupRandNum 随机群人数(不包含管理员)
|
||||
* @param WebSocketController $webSocket WebSocket实例
|
||||
*/
|
||||
protected function processBatchUsers($workbench, $config, $batchUsers, $groupMemberId, $groupMemberWechatId, $groupRandNum, $webSocket)
|
||||
{
|
||||
// 1. 获取群主微信ID列表(用于验证管理员)
|
||||
// 从群主成员的好友记录中提取所有群主的微信ID(ownerWechatId)
|
||||
$groupOwnerWechatIds = [];
|
||||
foreach ($groupMemberId as $memberId) {
|
||||
$member = Db::table('s2_wechat_friend')->where('id', $memberId)->find();
|
||||
if ($member && !in_array($member['ownerWechatId'], $groupOwnerWechatIds)) {
|
||||
$groupOwnerWechatIds[] = $member['ownerWechatId'];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果从好友表获取不到,使用群主成员微信ID列表(作为备用)
|
||||
if (empty($groupOwnerWechatIds)) {
|
||||
$groupOwnerWechatIds = array_values(array_unique($groupMemberWechatId));
|
||||
}
|
||||
// 2. 验证并获取管理员好友ID(管理员必须是群主的好友)
|
||||
$adminFriendIds = [];
|
||||
$adminWechatIds = [];
|
||||
if (!empty($config['admins'])) {
|
||||
$adminFriends = Db::table('s2_wechat_friend')
|
||||
->where('id', 'in', $config['admins'])
|
||||
->field('id,wechatId,ownerWechatId')
|
||||
->select();
|
||||
|
||||
foreach ($adminFriends as $adminFriend) {
|
||||
// 验证:管理员必须是群主的好友
|
||||
if (in_array($adminFriend['ownerWechatId'], $groupOwnerWechatIds)) {
|
||||
$adminFriendIds[] = $adminFriend['id'];
|
||||
$adminWechatIds[$adminFriend['id']] = $adminFriend['wechatId'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exit_data($adminWechatIds);
|
||||
// 3. 从流量池用户中筛选出是群主好友的用户(按微信账号分组)
|
||||
$ownerFriendIdsByAccount = [];
|
||||
$wechatIds = [];
|
||||
|
||||
// 获取群主的好友关系(从流量池中筛选)
|
||||
$ownerFriends = Db::name('wechat_friendship')->alias('f')
|
||||
->join(['s2_wechat_account' => 'a'], 'f.ownerWechatId=a.wechatId')
|
||||
->where('f.companyId', $workbench->companyId)
|
||||
->whereIn('f.wechatId', $batchUsers)
|
||||
->whereIn('f.ownerWechatId', $groupOwnerWechatIds)
|
||||
->field('f.id,f.wechatId,a.id as wechatAccountId')
|
||||
->select();
|
||||
if (empty($ownerFriends)) {
|
||||
Log::warning("未找到群主的好友,跳过。工作台ID: {$workbench->id}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 按微信账号分组群主好友
|
||||
foreach ($ownerFriends as $friend) {
|
||||
$wechatAccountId = $friend['wechatAccountId'];
|
||||
if (!isset($ownerFriendIdsByAccount[$wechatAccountId])) {
|
||||
$ownerFriendIdsByAccount[$wechatAccountId] = [];
|
||||
}
|
||||
$ownerFriendIdsByAccount[$wechatAccountId][] = $friend['id'];
|
||||
$wechatIds[$friend['id']] = $friend['wechatId'];
|
||||
}
|
||||
|
||||
// 4. 遍历每个微信账号,创建群
|
||||
foreach ($ownerFriendIdsByAccount as $wechatAccountId => $ownerFriendIds) {
|
||||
// 4.1 获取当前账号的管理员好友ID
|
||||
$currentAdminFriendIds = [];
|
||||
$accountWechatId = Db::table('s2_wechat_account')->where('id', $wechatAccountId)->value('wechatId');
|
||||
|
||||
foreach ($adminFriendIds as $adminFriendId) {
|
||||
$adminFriend = Db::table('s2_wechat_friend')->where('id', $adminFriendId)->find();
|
||||
if ($adminFriend && $adminFriend['ownerWechatId'] == $accountWechatId) {
|
||||
$currentAdminFriendIds[] = $adminFriendId;
|
||||
$wechatIds[$adminFriendId] = $adminWechatIds[$adminFriendId];
|
||||
}
|
||||
}
|
||||
|
||||
// 4.2 获取当前账号的群主成员ID
|
||||
$currentGroupMemberIds = [];
|
||||
foreach ($groupMemberId as $memberId) {
|
||||
$member = Db::table('s2_wechat_friend')->where('id', $memberId)->find();
|
||||
if ($member && $member['ownerWechatId'] == $accountWechatId) {
|
||||
$currentGroupMemberIds[] = $memberId;
|
||||
if (!isset($wechatIds[$memberId])) {
|
||||
$wechatIds[$memberId] = $groupMemberWechatId[$memberId] ?? '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4.3 限制群主好友数量(按随机群人数)
|
||||
$limitedOwnerFriendIds = array_slice($ownerFriendIds, 0, $groupRandNum);
|
||||
|
||||
// 4.4 创建群:管理员 + 群主成员 + 群主好友(从流量池筛选)
|
||||
$createFriendIds = array_merge($currentAdminFriendIds, $currentGroupMemberIds, $limitedOwnerFriendIds);
|
||||
|
||||
if (count($createFriendIds) < 2) {
|
||||
Log::warning("建群好友数量不足,跳过。工作台ID: {$workbench->id}, 微信账号ID: {$wechatAccountId}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4.5 生成群名称
|
||||
$existingGroupCount = Db::name('workbench_group_create_item')
|
||||
->where('workbenchId', $workbench->id)
|
||||
->where('wechatAccountId', $wechatAccountId)
|
||||
->where('status', self::STATUS_SUCCESS)
|
||||
->group('groupId')
|
||||
->count();
|
||||
|
||||
$chatroomName = $existingGroupCount > 0
|
||||
? $config['groupNameTemplate'] . ($existingGroupCount + 1) . '群'
|
||||
: $config['groupNameTemplate'];
|
||||
|
||||
// 4.6 调用建群接口
|
||||
$createTime = time();
|
||||
$createResult = $webSocket->CmdChatroomCreate([
|
||||
'chatroomName' => $chatroomName,
|
||||
'wechatFriendIds' => $createFriendIds,
|
||||
'wechatAccountId' => $wechatAccountId
|
||||
]);
|
||||
|
||||
$createResultData = json_decode($createResult, true);
|
||||
|
||||
// 4.7 解析建群结果,获取群ID
|
||||
$chatroomId = 0;
|
||||
if (!empty($createResultData) && isset($createResultData['code']) && $createResultData['code'] == 200) {
|
||||
// 尝试从返回数据中获取群ID(根据实际API返回格式调整)
|
||||
if (isset($createResultData['data']['chatroomId'])) {
|
||||
$chatroomId = $createResultData['data']['chatroomId'];
|
||||
} elseif (isset($createResultData['data']['id'])) {
|
||||
$chatroomId = $createResultData['data']['id'];
|
||||
}
|
||||
}
|
||||
|
||||
// 4.8 记录创建请求
|
||||
$installData = [];
|
||||
foreach ($createFriendIds as $friendId) {
|
||||
$memberType = in_array($friendId, $currentAdminFriendIds)
|
||||
? self::MEMBER_TYPE_ADMIN
|
||||
: (in_array($friendId, $currentGroupMemberIds) ? self::MEMBER_TYPE_OWNER : self::MEMBER_TYPE_OWNER_FRIEND);
|
||||
|
||||
$installData[] = [
|
||||
'workbenchId' => $workbench->id,
|
||||
'friendId' => $friendId,
|
||||
'wechatId' => $wechatIds[$friendId] ?? ($groupMemberWechatId[$friendId] ?? ''),
|
||||
'groupId' => $chatroomId,
|
||||
'wechatAccountId' => $wechatAccountId,
|
||||
'status' => $chatroomId > 0 ? self::STATUS_SUCCESS : self::STATUS_CREATING,
|
||||
'memberType' => $memberType,
|
||||
'retryCount' => 0,
|
||||
'chatroomId' => $chatroomId > 0 ? $chatroomId : null,
|
||||
'createTime' => $createTime,
|
||||
];
|
||||
}
|
||||
Db::name('workbench_group_create_item')->insertAll($installData);
|
||||
|
||||
// 5. 如果群创建成功,拉管理员的好友进群
|
||||
if ($chatroomId > 0 && !empty($currentAdminFriendIds)) {
|
||||
$this->inviteAdminFriends($workbench, $config, $batchUsers, $currentAdminFriendIds, $chatroomId, $wechatAccountId, $wechatIds, $createTime, $webSocket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉管理员的好友进群
|
||||
* @param Workbench $workbench 工作台
|
||||
* @param array $config 配置
|
||||
* @param array $batchUsers 批次用户(流量池微信ID数组)
|
||||
* @param array $adminFriendIds 管理员好友ID数组
|
||||
* @param int $chatroomId 群ID
|
||||
* @param int $wechatAccountId 微信账号ID
|
||||
* @param array $wechatIds 好友ID到微信ID的映射
|
||||
* @param int $createTime 创建时间
|
||||
* @param WebSocketController $webSocket WebSocket实例
|
||||
*/
|
||||
protected function inviteAdminFriends($workbench, $config, $batchUsers, $adminFriendIds, $chatroomId, $wechatAccountId, $wechatIds, $createTime, $webSocket)
|
||||
{
|
||||
// 获取管理员的微信ID列表
|
||||
$adminWechatIds = [];
|
||||
foreach ($adminFriendIds as $adminFriendId) {
|
||||
if (isset($wechatIds[$adminFriendId])) {
|
||||
$adminWechatIds[] = $wechatIds[$adminFriendId];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($adminWechatIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 从流量池用户中筛选出是管理员好友的用户
|
||||
$adminFriendsFromPool = Db::name('wechat_friendship')->alias('f')
|
||||
->join(['s2_wechat_account' => 'a'], 'f.ownerWechatId=a.wechatId')
|
||||
->where('f.companyId', $workbench->companyId)
|
||||
->whereIn('f.wechatId', $batchUsers)
|
||||
->whereIn('f.ownerWechatId', $adminWechatIds)
|
||||
->where('a.id', $wechatAccountId)
|
||||
->field('f.id,f.wechatId')
|
||||
->select();
|
||||
|
||||
if (empty($adminFriendsFromPool)) {
|
||||
Log::info("未找到管理员的好友,跳过拉人。工作台ID: {$workbench->id}, 群ID: {$chatroomId}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 提取好友ID列表
|
||||
$adminFriendIdsToInvite = [];
|
||||
foreach ($adminFriendsFromPool as $friend) {
|
||||
$adminFriendIdsToInvite[] = $friend['id'];
|
||||
$wechatIds[$friend['id']] = $friend['wechatId'];
|
||||
}
|
||||
|
||||
// 调用拉人接口
|
||||
$inviteResult = $webSocket->CmdChatroomInvite([
|
||||
'wechatChatroomId' => $chatroomId,
|
||||
'wechatFriendIds' => $adminFriendIdsToInvite
|
||||
]);
|
||||
|
||||
$inviteResultData = json_decode($inviteResult, true);
|
||||
$inviteSuccess = !empty($inviteResultData) && isset($inviteResultData['code']) && $inviteResultData['code'] == 200;
|
||||
|
||||
// 记录管理员好友拉入状态
|
||||
$adminFriendData = [];
|
||||
foreach ($adminFriendIdsToInvite as $friendId) {
|
||||
$adminFriendData[] = [
|
||||
'workbenchId' => $workbench->id,
|
||||
'friendId' => $friendId,
|
||||
'wechatId' => $wechatIds[$friendId] ?? '',
|
||||
'groupId' => $chatroomId,
|
||||
'wechatAccountId' => $wechatAccountId,
|
||||
'status' => $inviteSuccess ? self::STATUS_ADMIN_FRIEND_ADDED : self::STATUS_FAILED,
|
||||
'memberType' => self::MEMBER_TYPE_ADMIN_FRIEND,
|
||||
'retryCount' => 0,
|
||||
'chatroomId' => $chatroomId,
|
||||
'createTime' => $createTime,
|
||||
];
|
||||
}
|
||||
Db::name('workbench_group_create_item')->insertAll($adminFriendData);
|
||||
|
||||
if ($inviteSuccess) {
|
||||
Log::info("管理员好友拉入成功。工作台ID: {$workbench->id}, 群ID: {$chatroomId}, 拉入数量: " . count($adminFriendIdsToInvite));
|
||||
} else {
|
||||
Log::warning("管理员好友拉入失败。工作台ID: {$workbench->id}, 群ID: {$chatroomId}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取设备列表
|
||||
|
||||
109
Server/application/job/WorkbenchGroupCreateOwnerFriendJob.php
Normal file
109
Server/application/job/WorkbenchGroupCreateOwnerFriendJob.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace app\job;
|
||||
|
||||
use app\api\controller\WebSocketController;
|
||||
use think\facade\Log;
|
||||
use think\facade\Env;
|
||||
use think\Db;
|
||||
use think\queue\Job;
|
||||
use think\facade\Cache;
|
||||
use think\facade\Config;
|
||||
|
||||
/**
|
||||
* 工作台群创建-拉群主好友任务
|
||||
* Class WorkbenchGroupCreateOwnerFriendJob
|
||||
* @package app\job
|
||||
*/
|
||||
class WorkbenchGroupCreateOwnerFriendJob
|
||||
{
|
||||
/**
|
||||
* 最大重试次数
|
||||
*/
|
||||
const MAX_RETRY_ATTEMPTS = 3;
|
||||
|
||||
/**
|
||||
* 成员类型常量
|
||||
*/
|
||||
const MEMBER_TYPE_OWNER_FRIEND = 3;
|
||||
|
||||
/**
|
||||
* 状态常量
|
||||
*/
|
||||
const STATUS_SUCCESS = 2;
|
||||
|
||||
/**
|
||||
* 队列任务处理
|
||||
* @param Job $job 队列任务
|
||||
* @param array $data 任务数据
|
||||
* @return bool
|
||||
*/
|
||||
public function fire(Job $job, $data)
|
||||
{
|
||||
$workbenchId = $data['workbenchId'] ?? 0;
|
||||
$wechatAccountId = $data['wechatAccountId'] ?? 0;
|
||||
$groupId = $data['groupId'] ?? 0;
|
||||
$chatroomId = $data['chatroomId'] ?? '';
|
||||
$ownerFriendIds = $data['ownerFriendIds'] ?? [];
|
||||
$createTime = $data['createTime'] ?? 0;
|
||||
|
||||
try {
|
||||
if (empty($ownerFriendIds) || empty($groupId)) {
|
||||
Log::info("群主好友或群ID为空,跳过。工作台ID: {$workbenchId}");
|
||||
$job->delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
// 初始化WebSocket
|
||||
$toAccountId = '';
|
||||
$username = Env::get('api.username2', '');
|
||||
$password = Env::get('api.password2', '');
|
||||
if (!empty($username) || !empty($password)) {
|
||||
$toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId');
|
||||
}
|
||||
$webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]);
|
||||
|
||||
// 拉群主好友进群
|
||||
$inviteResult = $webSocket->CmdChatroomInvite([
|
||||
'wechatChatroomId' => $groupId,
|
||||
'wechatFriendIds' => $ownerFriendIds
|
||||
]);
|
||||
|
||||
// 获取好友微信ID映射
|
||||
$friendWechatIds = Db::table('s2_wechat_friend')
|
||||
->where('id', 'in', $ownerFriendIds)
|
||||
->column('id,wechatId');
|
||||
|
||||
// 更新群主好友记录状态
|
||||
Db::name('workbench_group_create_item')
|
||||
->where('workbenchId', $workbenchId)
|
||||
->where('wechatAccountId', $wechatAccountId)
|
||||
->where('status', 1) // 创建中
|
||||
->where('memberType', self::MEMBER_TYPE_OWNER_FRIEND)
|
||||
->where('createTime', '>=', $createTime - 10)
|
||||
->where('createTime', '<=', $createTime + 10)
|
||||
->update([
|
||||
'status' => self::STATUS_SUCCESS,
|
||||
'groupId' => $groupId,
|
||||
'chatroomId' => $chatroomId,
|
||||
'verifyTime' => time()
|
||||
]);
|
||||
|
||||
Log::info("群主好友已拉入群。工作台ID: {$workbenchId}, 群ID: {$groupId}, 好友数: " . count($ownerFriendIds));
|
||||
|
||||
$job->delete();
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
Log::error("拉群主好友任务异常:{$e->getMessage()}");
|
||||
|
||||
if ($job->attempts() > self::MAX_RETRY_ATTEMPTS) {
|
||||
$job->delete();
|
||||
} else {
|
||||
$job->release(Config::get('queue.failed_delay', 10));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
179
Server/application/job/WorkbenchGroupCreateRetryJob.php
Normal file
179
Server/application/job/WorkbenchGroupCreateRetryJob.php
Normal file
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
namespace app\job;
|
||||
|
||||
use app\api\controller\WebSocketController;
|
||||
use app\cunkebao\model\Workbench;
|
||||
use app\cunkebao\model\WorkbenchGroupCreate;
|
||||
use think\facade\Log;
|
||||
use think\facade\Env;
|
||||
use think\Db;
|
||||
use think\queue\Job;
|
||||
use think\facade\Cache;
|
||||
use think\facade\Config;
|
||||
use think\Queue;
|
||||
|
||||
/**
|
||||
* 工作台群创建重试任务
|
||||
* Class WorkbenchGroupCreateRetryJob
|
||||
* @package app\job
|
||||
*/
|
||||
class WorkbenchGroupCreateRetryJob
|
||||
{
|
||||
/**
|
||||
* 最大重试次数
|
||||
*/
|
||||
const MAX_RETRY_ATTEMPTS = 3;
|
||||
|
||||
/**
|
||||
* 队列任务处理
|
||||
* @param Job $job 队列任务
|
||||
* @param array $data 任务数据
|
||||
* @return bool
|
||||
*/
|
||||
public function fire(Job $job, $data)
|
||||
{
|
||||
$workbenchId = $data['workbenchId'] ?? 0;
|
||||
$wechatAccountId = $data['wechatAccountId'] ?? 0;
|
||||
$createTime = $data['createTime'] ?? 0;
|
||||
|
||||
try {
|
||||
// 获取工作台和配置
|
||||
$workbench = Workbench::where('id', $workbenchId)->find();
|
||||
if (!$workbench) {
|
||||
Log::error("未找到工作台。工作台ID: {$workbenchId}");
|
||||
$job->delete();
|
||||
return false;
|
||||
}
|
||||
|
||||
$config = WorkbenchGroupCreate::where('workbenchId', $workbench->id)->find();
|
||||
if (!$config) {
|
||||
Log::error("未找到工作台配置。工作台ID: {$workbenchId}");
|
||||
$job->delete();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取失败记录
|
||||
$failedItems = Db::name('workbench_group_create_item')
|
||||
->where('workbenchId', $workbenchId)
|
||||
->where('wechatAccountId', $wechatAccountId)
|
||||
->where('createTime', '>=', $createTime - 10)
|
||||
->where('createTime', '<=', $createTime + 10)
|
||||
->where('status', 'in', [1, 3]) // 创建中或失败
|
||||
->select();
|
||||
|
||||
if (empty($failedItems)) {
|
||||
Log::info("未找到需要重试的记录。工作台ID: {$workbenchId}");
|
||||
$job->delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
// 解析配置
|
||||
$config['poolGroups'] = json_decode($config['poolGroups'], true);
|
||||
$config['devices'] = json_decode($config['devices'], true);
|
||||
$config['admins'] = json_decode($config['admins'] ?? '[]', true) ?: [];
|
||||
|
||||
// 获取群主成员
|
||||
$groupMember = Db::name('device_wechat_login')->alias('dwl')
|
||||
->join(['s2_wechat_account' => 'a'], 'dwl.wechatId = a.wechatId')
|
||||
->whereIn('dwl.deviceId', $config['devices'])
|
||||
->group('a.id')
|
||||
->column('a.wechatId');
|
||||
|
||||
$groupMemberWechatId = Db::table('s2_wechat_friend')
|
||||
->where('ownerWechatId', $groupMember[0])
|
||||
->whereIn('wechatId', $groupMember)
|
||||
->column('id,wechatId');
|
||||
|
||||
$groupMemberId = array_keys($groupMemberWechatId);
|
||||
|
||||
// 获取管理员好友ID
|
||||
$adminFriendIds = [];
|
||||
if (!empty($config['admins'])) {
|
||||
$adminFriends = Db::table('s2_wechat_friend')
|
||||
->where('id', 'in', $config['admins'])
|
||||
->column('id,wechatId,ownerWechatId');
|
||||
|
||||
$accountWechatId = Db::table('s2_wechat_account')->where('id', $wechatAccountId)->value('wechatId');
|
||||
foreach ($adminFriends as $adminFriend) {
|
||||
if ($adminFriend['ownerWechatId'] == $accountWechatId) {
|
||||
$adminFriendIds[] = $adminFriend['id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化WebSocket
|
||||
$toAccountId = '';
|
||||
$username = Env::get('api.username2', '');
|
||||
$password = Env::get('api.password2', '');
|
||||
if (!empty($username) || !empty($password)) {
|
||||
$toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId');
|
||||
}
|
||||
$webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]);
|
||||
|
||||
// 重新创建群
|
||||
$createFriendIds = array_merge($adminFriendIds, $groupMemberId);
|
||||
|
||||
if (count($createFriendIds) < 2) {
|
||||
Log::error("重试建群好友数量不足。工作台ID: {$workbenchId}");
|
||||
$job->delete();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 生成群名称
|
||||
$existingGroupCount = Db::name('workbench_group_create_item')
|
||||
->where('workbenchId', $workbenchId)
|
||||
->where('wechatAccountId', $wechatAccountId)
|
||||
->where('status', 2) // 成功
|
||||
->group('groupId')
|
||||
->count();
|
||||
|
||||
$chatroomName = $existingGroupCount > 0
|
||||
? $config['groupNameTemplate'] . ($existingGroupCount + 1) . '群'
|
||||
: $config['groupNameTemplate'];
|
||||
|
||||
// 调用建群接口
|
||||
$createResult = $webSocket->CmdChatroomCreate([
|
||||
'chatroomName' => $chatroomName,
|
||||
'wechatFriendIds' => $createFriendIds,
|
||||
'wechatAccountId' => $wechatAccountId
|
||||
]);
|
||||
|
||||
// 更新记录状态为创建中
|
||||
Db::name('workbench_group_create_item')
|
||||
->where('workbenchId', $workbenchId)
|
||||
->where('wechatAccountId', $wechatAccountId)
|
||||
->where('createTime', '>=', $createTime - 10)
|
||||
->where('createTime', '<=', $createTime + 10)
|
||||
->update([
|
||||
'status' => 1, // 创建中
|
||||
'createTime' => time() // 更新创建时间
|
||||
]);
|
||||
|
||||
// 创建新的轮询验证任务
|
||||
Queue::later(5, 'app\job\WorkbenchGroupCreateVerifyJob', [
|
||||
'workbenchId' => $workbenchId,
|
||||
'wechatAccountId' => $wechatAccountId,
|
||||
'createTime' => time(),
|
||||
'adminFriendIds' => $adminFriendIds,
|
||||
'poolUsers' => [], // 重试时暂时不传poolUsers,后续可以优化
|
||||
], 'default');
|
||||
|
||||
Log::info("重试建群任务已创建。工作台ID: {$workbenchId}, 微信账号ID: {$wechatAccountId}");
|
||||
|
||||
$job->delete();
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
Log::error("重试建群任务异常:{$e->getMessage()}");
|
||||
|
||||
if ($job->attempts() > self::MAX_RETRY_ATTEMPTS) {
|
||||
$job->delete();
|
||||
} else {
|
||||
$job->release(Config::get('queue.failed_delay', 10));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
248
Server/application/job/WorkbenchGroupCreateVerifyJob.php
Normal file
248
Server/application/job/WorkbenchGroupCreateVerifyJob.php
Normal file
@@ -0,0 +1,248 @@
|
||||
<?php
|
||||
|
||||
namespace app\job;
|
||||
|
||||
use app\api\controller\WechatChatroomController;
|
||||
use think\facade\Log;
|
||||
use think\Db;
|
||||
use think\queue\Job;
|
||||
use think\facade\Cache;
|
||||
use think\facade\Config;
|
||||
use think\Queue;
|
||||
|
||||
/**
|
||||
* 工作台群创建验证任务(轮询群创建状态)
|
||||
* Class WorkbenchGroupCreateVerifyJob
|
||||
* @package app\job
|
||||
*/
|
||||
class WorkbenchGroupCreateVerifyJob
|
||||
{
|
||||
/**
|
||||
* 最大重试次数
|
||||
*/
|
||||
const MAX_RETRY_ATTEMPTS = 15; // 最多轮询15次
|
||||
|
||||
/**
|
||||
* 轮询间隔(秒)
|
||||
*/
|
||||
const POLL_INTERVAL = 5;
|
||||
|
||||
/**
|
||||
* 状态常量
|
||||
*/
|
||||
const STATUS_CREATING = 1;
|
||||
const STATUS_SUCCESS = 2;
|
||||
const STATUS_FAILED = 3;
|
||||
|
||||
/**
|
||||
* 队列任务处理
|
||||
* @param Job $job 队列任务
|
||||
* @param array $data 任务数据
|
||||
* @return bool
|
||||
*/
|
||||
public function fire(Job $job, $data)
|
||||
{
|
||||
$workbenchId = $data['workbenchId'] ?? 0;
|
||||
$wechatAccountId = $data['wechatAccountId'] ?? 0;
|
||||
$createTime = $data['createTime'] ?? 0;
|
||||
$adminFriendIds = $data['adminFriendIds'] ?? [];
|
||||
$poolUsers = $data['poolUsers'] ?? [];
|
||||
|
||||
try {
|
||||
$attempts = $job->attempts();
|
||||
|
||||
// 查询待验证的群记录
|
||||
$groupItems = Db::name('workbench_group_create_item')
|
||||
->where('workbenchId', $workbenchId)
|
||||
->where('wechatAccountId', $wechatAccountId)
|
||||
->where('status', self::STATUS_CREATING)
|
||||
->where('createTime', '>=', $createTime - 10) // 允许10秒误差
|
||||
->where('createTime', '<=', $createTime + 10)
|
||||
->group('wechatAccountId')
|
||||
->select();
|
||||
|
||||
if (empty($groupItems)) {
|
||||
Log::info("未找到待验证的群记录,任务完成。工作台ID: {$workbenchId}, 微信账号ID: {$wechatAccountId}");
|
||||
$job->delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取微信账号信息
|
||||
$wechatAccount = Db::table('s2_wechat_account')->where('id', $wechatAccountId)->find();
|
||||
if (empty($wechatAccount)) {
|
||||
Log::error("未找到微信账号,任务失败。微信账号ID: {$wechatAccountId}");
|
||||
$job->delete();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 调用接口查询群聊列表
|
||||
$chatroomController = new WechatChatroomController();
|
||||
$chatroomList = $chatroomController->getlist([
|
||||
'wechatAccountKeyword' => $wechatAccount['wechatId'],
|
||||
'pageIndex' => 0,
|
||||
'pageSize' => 100
|
||||
], true);
|
||||
|
||||
$chatroomListData = json_decode($chatroomList, true);
|
||||
|
||||
if (empty($chatroomListData['data']['results'])) {
|
||||
// 如果超过最大重试次数,标记为失败并重试创建
|
||||
if ($attempts >= self::MAX_RETRY_ATTEMPTS) {
|
||||
$this->handleCreateFailed($workbenchId, $wechatAccountId, $createTime, $job);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 继续轮询
|
||||
$job->release(self::POLL_INTERVAL);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 查找符合条件的群(chatroomOwnerAvatar和chatroomOwnerNickname不为空)
|
||||
$successGroup = null;
|
||||
foreach ($chatroomListData['data']['results'] as $chatroom) {
|
||||
if (!empty($chatroom['chatroomOwnerAvatar']) && !empty($chatroom['chatroomOwnerNickname'])) {
|
||||
// 检查创建时间是否匹配(允许30秒误差)
|
||||
$chatroomCreateTime = isset($chatroom['createTime']) ? strtotime($chatroom['createTime']) : 0;
|
||||
if (abs($chatroomCreateTime - $createTime) <= 30) {
|
||||
$successGroup = $chatroom;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($successGroup) {
|
||||
// 群创建成功,更新记录状态
|
||||
$groupId = $successGroup['id'] ?? 0;
|
||||
$chatroomId = $successGroup['chatroomId'] ?? '';
|
||||
|
||||
// 更新管理员和群主成员的记录状态
|
||||
Db::name('workbench_group_create_item')
|
||||
->where('workbenchId', $workbenchId)
|
||||
->where('wechatAccountId', $wechatAccountId)
|
||||
->where('status', self::STATUS_CREATING)
|
||||
->where('memberType', 'in', [1, 2]) // 群主成员和管理员
|
||||
->where('createTime', '>=', $createTime - 10)
|
||||
->where('createTime', '<=', $createTime + 10)
|
||||
->update([
|
||||
'status' => self::STATUS_SUCCESS,
|
||||
'groupId' => $groupId,
|
||||
'chatroomId' => $chatroomId,
|
||||
'verifyTime' => time()
|
||||
]);
|
||||
|
||||
Log::info("群创建成功!工作台ID: {$workbenchId}, 微信账号ID: {$wechatAccountId}, 群ID: {$groupId}");
|
||||
|
||||
// 3. 拉群主好友进群(在验证成功后执行)
|
||||
$ownerFriendIds = $data['ownerFriendIds'] ?? [];
|
||||
if (!empty($ownerFriendIds)) {
|
||||
Queue::push('app\job\WorkbenchGroupCreateOwnerFriendJob', [
|
||||
'workbenchId' => $workbenchId,
|
||||
'wechatAccountId' => $wechatAccountId,
|
||||
'groupId' => $groupId,
|
||||
'chatroomId' => $chatroomId,
|
||||
'ownerFriendIds' => $ownerFriendIds,
|
||||
'createTime' => $createTime
|
||||
], 'default');
|
||||
}
|
||||
|
||||
// 5. 创建拉管理员好友的任务(在群主好友拉入后执行)
|
||||
if (!empty($adminFriendIds) && !empty($poolUsers)) {
|
||||
Queue::push('app\job\WorkbenchGroupCreateAdminFriendJob', [
|
||||
'workbenchId' => $workbenchId,
|
||||
'wechatAccountId' => $wechatAccountId,
|
||||
'groupId' => $groupId,
|
||||
'chatroomId' => $chatroomId,
|
||||
'adminFriendIds' => $adminFriendIds,
|
||||
'poolUsers' => $poolUsers
|
||||
], 'default');
|
||||
}
|
||||
|
||||
$job->delete();
|
||||
return true;
|
||||
} else {
|
||||
// 如果超过最大重试次数,标记为失败并重试创建
|
||||
if ($attempts >= self::MAX_RETRY_ATTEMPTS) {
|
||||
$this->handleCreateFailed($workbenchId, $wechatAccountId, $createTime, $job);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 继续轮询
|
||||
$job->release(self::POLL_INTERVAL);
|
||||
return false;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error("群创建验证任务异常:{$e->getMessage()}");
|
||||
|
||||
if ($job->attempts() >= self::MAX_RETRY_ATTEMPTS) {
|
||||
$job->delete();
|
||||
} else {
|
||||
$job->release(self::POLL_INTERVAL);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理创建失败的情况(重试创建)
|
||||
* @param int $workbenchId 工作台ID
|
||||
* @param int $wechatAccountId 微信账号ID
|
||||
* @param int $createTime 创建时间
|
||||
* @param Job $job 队列任务
|
||||
*/
|
||||
protected function handleCreateFailed($workbenchId, $wechatAccountId, $createTime, $job)
|
||||
{
|
||||
// 更新状态为失败
|
||||
Db::name('workbench_group_create_item')
|
||||
->where('workbenchId', $workbenchId)
|
||||
->where('wechatAccountId', $wechatAccountId)
|
||||
->where('status', self::STATUS_CREATING)
|
||||
->where('createTime', '>=', $createTime - 10)
|
||||
->where('createTime', '<=', $createTime + 10)
|
||||
->update([
|
||||
'status' => self::STATUS_FAILED,
|
||||
'verifyTime' => time()
|
||||
]);
|
||||
|
||||
Log::warning("群创建失败,准备重试。工作台ID: {$workbenchId}, 微信账号ID: {$wechatAccountId}");
|
||||
|
||||
// 检查重试次数
|
||||
$failedItems = Db::name('workbench_group_create_item')
|
||||
->where('workbenchId', $workbenchId)
|
||||
->where('wechatAccountId', $wechatAccountId)
|
||||
->where('createTime', '>=', $createTime - 10)
|
||||
->where('createTime', '<=', $createTime + 10)
|
||||
->select();
|
||||
|
||||
$maxRetryCount = 0;
|
||||
foreach ($failedItems as $item) {
|
||||
if ($item['retryCount'] >= 3) {
|
||||
Log::error("群创建重试次数已达上限,放弃重试。工作台ID: {$workbenchId}, 微信账号ID: {$wechatAccountId}");
|
||||
$job->delete();
|
||||
return;
|
||||
}
|
||||
$maxRetryCount = max($maxRetryCount, $item['retryCount']);
|
||||
}
|
||||
|
||||
// 增加重试次数并重置状态
|
||||
Db::name('workbench_group_create_item')
|
||||
->where('workbenchId', $workbenchId)
|
||||
->where('wechatAccountId', $wechatAccountId)
|
||||
->where('createTime', '>=', $createTime - 10)
|
||||
->where('createTime', '<=', $createTime + 10)
|
||||
->update([
|
||||
'status' => self::STATUS_CREATING,
|
||||
'retryCount' => Db::raw('retryCount + 1')
|
||||
]);
|
||||
|
||||
// 重新创建建群任务(延迟10秒)
|
||||
Queue::later(10, 'app\job\WorkbenchGroupCreateRetryJob', [
|
||||
'workbenchId' => $workbenchId,
|
||||
'wechatAccountId' => $wechatAccountId,
|
||||
'createTime' => $createTime
|
||||
], 'default');
|
||||
|
||||
$job->delete();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user