自动建群提交 + 触客宝发布朋友圈优化

This commit is contained in:
wong
2026-01-04 17:03:51 +08:00
parent 08d2a811b7
commit b4c813311b
39 changed files with 16264 additions and 504 deletions

View File

@@ -219,6 +219,48 @@
font-size: 16px;
}
.searchBox {
padding: 12px 20px;
border-bottom: 1px solid #f0f0f0;
background: #fff;
display: flex;
align-items: center;
gap: 8px;
}
.searchInputWrapper {
flex: 1;
position: relative;
display: flex;
align-items: center;
}
.searchInput {
flex: 1;
background: #f5f5f5;
border-radius: 8px;
padding: 8px 12px;
font-size: 14px;
}
.clearSearchBtn {
position: absolute;
right: 8px;
width: 20px;
height: 20px;
min-width: 20px;
padding: 0;
color: #999;
display: flex;
align-items: center;
justify-content: center;
}
.searchBtn {
min-width: 60px;
height: 32px;
}
.memberList {
flex: 1;
overflow-y: auto;

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react";
import { SearchOutlined, DeleteOutlined, PlusOutlined } from "@ant-design/icons";
import { SearchOutlined, DeleteOutlined, PlusOutlined, CloseOutlined } from "@ant-design/icons";
import { Button, Input, Popup } from "antd-mobile";
import { Avatar } from "antd-mobile";
import style from "./index.module.scss";
@@ -82,6 +82,9 @@ const GroupSelectionWithMembers: React.FC<GroupSelectionWithMembersProps> = ({
const [allMembers, setAllMembers] = useState<Record<string, GroupMember[]>>({});
const [selectedMembers, setSelectedMembers] = useState<Record<string, GroupMember[]>>({});
const [loadingMembers, setLoadingMembers] = useState(false);
const [memberSearchKeyword, setMemberSearchKeyword] = useState("");
// 存储完整成员列表(用于搜索时切换回完整列表)
const [fullMembersCache, setFullMembersCache] = useState<Record<string, GroupMember[]>>({});
// 处理群选择
const handleGroupSelect = (groups: GroupSelectionItem[]) => {
@@ -110,24 +113,66 @@ const GroupSelectionWithMembers: React.FC<GroupSelectionWithMembersProps> = ({
const handleOpenMemberSelection = async (groupId: string) => {
if (readonly) return;
setMemberSelectionVisible({ visible: true, groupId });
setMemberSearchKeyword(""); // 重置搜索关键词
// 如果还没有加载过该群的成员列表,则加载
if (!allMembers[groupId]) {
// 如果还没有加载过该群的成员列表,则加载所有成员(不使用搜索关键词)
if (!allMembers[groupId] && !fullMembersCache[groupId]) {
setLoadingMembers(true);
try {
const members = await getGroupMembers(groupId);
const members = await getGroupMembers(groupId, 1, 100, "");
setAllMembers(prev => ({ ...prev, [groupId]: members }));
setFullMembersCache(prev => ({ ...prev, [groupId]: members })); // 缓存完整列表
} catch (error) {
console.error("加载群成员失败:", error);
} finally {
setLoadingMembers(false);
}
} else if (fullMembersCache[groupId] && !allMembers[groupId]) {
// 如果有缓存但没有显示列表,恢复完整列表
setAllMembers(prev => ({ ...prev, [groupId]: fullMembersCache[groupId] }));
}
};
// 关闭成员选择弹窗
const handleCloseMemberSelection = () => {
setMemberSelectionVisible({ visible: false, groupId: "" });
setMemberSearchKeyword(""); // 重置搜索关键词
};
// 手动触发搜索
const handleSearchMembers = async () => {
const groupId = memberSelectionVisible.groupId;
if (!groupId) return;
const keyword = memberSearchKeyword.trim();
// 如果搜索关键词为空,使用缓存的完整列表
if (!keyword) {
if (fullMembersCache[groupId] && fullMembersCache[groupId].length > 0) {
setAllMembers(prev => ({ ...prev, [groupId]: fullMembersCache[groupId] }));
}
return;
}
// 有搜索关键词时,调用 API 搜索
setLoadingMembers(true);
try {
const members = await getGroupMembers(groupId, 1, 100, keyword);
setAllMembers(prev => ({ ...prev, [groupId]: members }));
} catch (error) {
console.error("搜索群成员失败:", error);
} finally {
setLoadingMembers(false);
}
};
// 清空搜索
const handleClearSearch = () => {
setMemberSearchKeyword("");
const groupId = memberSelectionVisible.groupId;
if (groupId && fullMembersCache[groupId] && fullMembersCache[groupId].length > 0) {
setAllMembers(prev => ({ ...prev, [groupId]: fullMembersCache[groupId] }));
}
};
// 选择成员
@@ -328,6 +373,36 @@ const GroupSelectionWithMembers: React.FC<GroupSelectionWithMembersProps> = ({
</Button>
</div>
<div className={style.searchBox}>
<div className={style.searchInputWrapper}>
<Input
placeholder="搜索成员昵称或微信号"
value={memberSearchKeyword}
onChange={val => setMemberSearchKeyword(val)}
onEnterPress={handleSearchMembers}
className={style.searchInput}
/>
{memberSearchKeyword && (
<Button
fill="none"
size="small"
className={style.clearSearchBtn}
onClick={handleClearSearch}
>
<CloseOutlined />
</Button>
)}
</div>
<Button
color="primary"
size="small"
onClick={handleSearchMembers}
loading={loadingMembers}
className={style.searchBtn}
>
</Button>
</div>
<div className={style.memberList}>
{loadingMembers ? (
<div className={style.loadingBox}>

View File

@@ -99,13 +99,18 @@ export default function ContentForm() {
: []);
setSelectedDevices(deviceOptions || []);
setSelectedFriends(data.sourceFriends || []);
setSelectedGroups(data.selectedGroups || []);
// 使用 wechatGroupsOptions 作为群列表数据
setSelectedGroupsOptions(data.wechatGroupsOptions || data.selectedGroupsOptions || []);
const groupsOptions = data.wechatGroupsOptions || data.selectedGroupsOptions || [];
setSelectedGroupsOptions(groupsOptions);
// 从 groupsOptions 中提取群 ID 列表,如果没有 selectedGroups 的话
const groupIds = data.selectedGroups && data.selectedGroups.length > 0
? data.selectedGroups
: groupsOptions.map((g: any) => String(g.id));
setSelectedGroups(groupIds);
// 处理带成员的群数据
// groupMembersOptions 是一个对象key是群ID字符串value是成员数组
const groupMembersMap = data.groupMembersOptions || {};
const groupsWithMembers: GroupWithMembers[] = (data.wechatGroupsOptions || data.selectedGroupsOptions || []).map(
const groupsWithMembers: GroupWithMembers[] = groupsOptions.map(
(group: any) => {
const groupIdStr = String(group.id);
const members = groupMembersMap[groupIdStr] || [];

View File

@@ -75,7 +75,7 @@ const PoolListModal: React.FC<PoolListModalProps> = ({
useEffect(() => {
if (!visible || !ruleId) return;
setLoading(true);
setLoading(true);
const params: { startTime?: string; endTime?: string } = {};
if (startTime) {
params.startTime = formatDate(startTime);
@@ -85,26 +85,26 @@ const PoolListModal: React.FC<PoolListModalProps> = ({
}
getFriendRequestTaskStats(ruleId.toString(), params)
.then(res => {
console.log(res);
setXData(res.dateArray);
setYData([
res.allNumArray,
res.errorNumArray,
res.passNumArray,
res.passRateArray,
res.successNumArray,
res.successRateArray,
]);
setStatistics(res.totalStats);
.then(res => {
console.log(res);
setXData(res.dateArray);
setYData([
res.allNumArray,
res.errorNumArray,
res.passNumArray,
res.passRateArray,
res.successNumArray,
res.successRateArray,
]);
setStatistics(res.totalStats);
})
.catch(error => {
console.error("获取统计数据失败:", error);
message.error("获取统计数据失败");
})
.finally(() => {
setLoading(false);
});
})
.finally(() => {
setLoading(false);
});
}, [visible, ruleId, startTime, endTime, formatDate]);
const title = ruleName ? `${ruleName} - 累计统计数据` : "累计统计数据";

View File

@@ -221,7 +221,7 @@ const AddChannelModal: React.FC<AddChannelModalProps> = ({
setQrCodeData(null);
setScanning(false);
const timer = setTimeout(() => {
handleGenerateQRCode();
handleGenerateQRCode();
}, 100);
return () => clearTimeout(timer);
}

View File

@@ -0,0 +1,598 @@
.container {
padding: 16px;
padding-bottom: 100px;
background: #f3f4f6;
min-height: 100vh;
position: relative;
}
.syncOverlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(2px);
}
.syncContent {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
background: #fff;
padding: 32px 40px;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.syncText {
font-size: 14px;
color: #4b5563;
font-weight: 500;
}
.loadingContainer,
.emptyContainer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
}
.emptyText {
font-size: 16px;
color: #64748b;
margin-bottom: 24px;
}
.backButton {
padding: 8px 16px;
background: #3b82f6;
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
}
.moreButton {
padding: 8px;
margin-right: -8px;
background: none;
border: none;
cursor: pointer;
color: #6b7280;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
transition: color 0.2s;
&:hover {
color: #111827;
}
}
// 群组信息卡片
.groupInfoCard {
background: #fff;
border-radius: 16px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
border: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
align-items: center;
}
.groupIconLarge {
width: 80px;
height: 80px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 32px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
margin: 0 auto 16px;
object-fit: cover;
background-size: cover;
background-position: center;
font-weight: 600;
letter-spacing: 1px;
}
.groupTitle {
font-size: 20px;
font-weight: 700;
color: #111827;
text-align: center;
margin: 0 0 4px 0;
}
.createTimeInfo {
display: flex;
align-items: center;
gap: 4px;
font-size: 14px;
color: #6b7280;
margin-bottom: 16px;
}
.createTimeIcon {
font-size: 16px;
}
.actionButtons {
display: flex;
gap: 12px;
width: 100%;
}
.actionButton {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px;
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
border: none;
border-radius: 12px;
font-weight: 500;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
&:active {
background: rgba(59, 130, 246, 0.2);
}
}
.actionButtonIcon {
font-size: 18px;
}
// 基本信息
.basicInfoCard {
background: #fff;
border-radius: 16px;
padding: 20px;
margin-bottom: 24px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
border: 1px solid #e5e7eb;
}
.sectionTitle {
font-size: 16px;
font-weight: 600;
color: #111827;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.sectionTitleDot {
width: 4px;
height: 16px;
background: #3b82f6;
border-radius: 2px;
}
.basicInfoList {
display: flex;
flex-direction: column;
gap: 0;
}
.basicInfoItem {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
}
.basicInfoLabel {
font-size: 14px;
color: #6b7280;
}
.basicInfoValue {
font-size: 14px;
font-weight: 500;
color: #111827;
display: flex;
align-items: center;
gap: 4px;
}
.planIcon {
font-size: 16px;
color: #3b82f6;
margin-right: 4px;
}
.adminAvatar {
width: 20px;
height: 20px;
border-radius: 50%;
background: #fef3c7;
color: #92400e;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 700;
margin-right: 8px;
position: relative;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.adminAvatarText {
position: absolute;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.hidden {
display: none;
}
.basicInfoDivider {
height: 1px;
background: #f3f4f6;
}
.noAnnouncement {
color: #9ca3af;
display: flex;
align-items: center;
}
.chevronIcon {
font-size: 16px;
margin-left: 4px;
}
// 群成员
.membersCard {
background: #fff;
border-radius: 16px;
margin-bottom: 24px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
border: 1px solid #e5e7eb;
overflow: hidden;
}
.membersHeader {
padding: 20px;
border-bottom: 1px solid #f3f4f6;
display: flex;
justify-content: space-between;
align-items: center;
}
.memberCountBadge {
margin-left: 8px;
font-size: 12px;
font-weight: 400;
color: #6b7280;
background: #f3f4f6;
padding: 2px 8px;
border-radius: 9999px;
}
.searchButton {
padding: 6px;
color: #6b7280;
background: none;
border: none;
cursor: pointer;
border-radius: 8px;
transition: background 0.2s;
&:active {
background: #f3f4f6;
}
}
.searchBox {
padding: 12px 20px;
border-bottom: 1px solid #f3f4f6;
}
.searchInput {
width: 100%;
padding: 8px 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
font-size: 14px;
outline: none;
&:focus {
border-color: #3b82f6;
}
}
.membersList {
max-height: 320px;
overflow-y: auto;
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.memberItem {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
border-bottom: 1px solid #f9fafb;
transition: background 0.2s;
&:last-child {
border-bottom: none;
}
&:active {
background: #f9fafb;
}
}
.memberInfo {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.memberInfoText {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.memberNameRow {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.memberAvatarWrapper {
position: relative;
}
.memberAvatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
background: #e5e7eb;
}
.memberAvatarPlaceholder {
width: 40px;
height: 40px;
border-radius: 50%;
background: #e9d5ff;
color: #7c3aed;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 14px;
}
.adminBadge {
position: absolute;
bottom: -2px;
right: -2px;
width: 16px;
height: 16px;
border-radius: 50%;
background: #fbbf24;
border: 2px solid #fff;
display: flex;
align-items: center;
justify-content: center;
}
.adminBadgeIcon {
font-size: 10px;
color: #fff;
}
.memberInfoText {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.memberNameRow {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.memberName {
font-size: 14px;
font-weight: 500;
color: #111827;
}
.memberWechatId {
font-size: 12px;
color: #6b7280;
}
.quitTag {
display: inline-flex;
align-items: center;
padding: 2px 6px;
background: #fee2e2;
color: #dc2626;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.joinStatusTag {
display: inline-flex;
align-items: center;
padding: 2px 6px;
background: #dbeafe;
color: #2563eb;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.memberExtraInfo {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #9ca3af;
flex-wrap: wrap;
}
.divider {
color: #d1d5db;
}
.memberJoinTime {
font-size: 12px;
color: #9ca3af;
}
.memberActions {
display: flex;
align-items: center;
}
.adminTag {
padding: 2px 8px;
border-radius: 4px;
font-size: 10px;
font-weight: 500;
background: #fef3c7;
color: #92400e;
border: 1px solid rgba(146, 64, 14, 0.3);
}
.removeButton {
color: #9ca3af;
background: none;
border: none;
cursor: pointer;
font-size: 20px;
transition: color 0.2s;
&:active {
color: #ef4444;
}
}
.emptyMembers {
padding: 40px 20px;
text-align: center;
}
.emptyMembersText {
font-size: 14px;
color: #9ca3af;
}
// 操作按钮
.actionsSection {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.actionCard {
background: #fff;
border-radius: 12px;
padding: 16px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
border: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
cursor: pointer;
transition: background 0.2s;
&:active {
background: #f9fafb;
}
}
.actionCardIcon {
width: 40px;
height: 40px;
border-radius: 50%;
background: #f3f4f6;
color: #4b5563;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
transition: all 0.2s;
}
.actionCard:hover .actionCardIcon {
background: #dbeafe;
color: #3b82f6;
}
.actionCardText {
font-size: 14px;
font-weight: 500;
color: #111827;
}
.actionCardDanger {
&:active {
background: #fef2f2;
}
}
.actionCardIconDanger {
background: #fee2e2;
color: #ef4444;
}
.actionCardTextDanger {
color: #dc2626;
}

View File

@@ -0,0 +1,675 @@
import React, { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { Toast, SpinLoading, Dialog, Input, TextArea } from "antd-mobile";
import {
EditOutlined,
QrcodeOutlined,
SearchOutlined,
SyncOutlined,
LogoutOutlined,
StarOutlined,
CloseCircleOutlined,
ScheduleOutlined,
FileTextOutlined,
} from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import NavCommon from "@/components/NavCommon";
import { getCreatedGroupDetail, syncGroupInfo, modifyGroupInfo, quitGroup } from "../form/api";
import style from "./group-detail.module.scss";
interface GroupMember {
id?: string;
friendId?: number;
nickname?: string;
wechatId?: string;
avatar?: string;
isOwner?: number; // 1表示是群主0表示不是
isGroupAdmin?: boolean;
joinStatus?: string; // "auto" | "manual" - 入群状态
isQuit?: number; // 0/1 - 是否已退群
joinTime?: string; // 入群时间
alias?: string; // 成员别名
remark?: string; // 成员备注
[key: string]: any;
}
interface GroupDetail {
id: string;
name: string;
createTime?: string;
planName?: string;
groupAdmin?: {
id: string;
nickname?: string;
wechatId?: string;
avatar?: string;
};
announcement?: string;
memberCount?: number;
members?: GroupMember[];
[key: string]: any;
}
const GroupDetailPage: React.FC = () => {
const { id, groupId } = useParams();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [groupDetail, setGroupDetail] = useState<GroupDetail | null>(null);
const [searchKeyword, setSearchKeyword] = useState("");
const [showSearch, setShowSearch] = useState(false);
const [syncing, setSyncing] = useState(false);
const [editNameVisible, setEditNameVisible] = useState(false);
const [editNameValue, setEditNameValue] = useState("");
const [editAnnouncementVisible, setEditAnnouncementVisible] = useState(false);
const [editAnnouncementValue, setEditAnnouncementValue] = useState("");
useEffect(() => {
if (!id || !groupId) return;
fetchGroupDetail();
}, [id, groupId]);
const fetchGroupDetail = async () => {
if (!id || !groupId) return;
setLoading(true);
try {
const res = await getCreatedGroupDetail({
workbenchId: id,
groupId: groupId,
});
const data = res?.data || res || {};
// 处理成员数据确保有id字段
const members = (data.members || []).map((member: any) => ({
...member,
id: String(member.friendId || member.id || member.wechatId || ''),
}));
const detailData: GroupDetail = {
id: String(data.id || groupId),
name: data.groupName || data.name || "未命名群组",
createTime: data.createTime || "",
planName: data.workbenchName || data.planName || "",
groupAdmin: data.ownerWechatId || data.ownerNickname
? {
id: String(data.ownerWechatId || ""),
nickname: data.ownerNickname,
wechatId: data.ownerWechatId,
avatar: data.ownerAvatar,
}
: undefined,
announcement: data.announce || data.announcement, // 接口返回的字段是 announce
memberCount: data.memberCount || members.length || 0,
members: members,
avatar: data.avatar || data.groupAvatar,
groupAvatar: data.groupAvatar,
};
setGroupDetail(detailData);
} catch (e: any) {
Toast.show({ content: e?.message || "获取群组详情失败", position: "top" });
navigate(-1);
} finally {
setLoading(false);
}
};
const handleEditName = () => {
if (!groupDetail) return;
setEditNameValue(groupDetail.name);
setEditNameVisible(true);
};
const handleConfirmEditName = async () => {
if (!id || !groupId || !editNameValue.trim()) {
Toast.show({ content: "群名称不能为空", position: "top" });
return;
}
try {
await modifyGroupInfo({
workbenchId: id,
groupId: groupId,
chatroomName: editNameValue.trim(),
});
Toast.show({ content: "修改成功", position: "top" });
setEditNameVisible(false);
// 刷新群详情
fetchGroupDetail();
} catch (e: any) {
Toast.show({
content: e?.message || "修改群名称失败",
position: "top"
});
}
};
const handleEditAnnouncement = () => {
if (!groupDetail) return;
setEditAnnouncementValue(groupDetail.announcement || "");
setEditAnnouncementVisible(true);
};
const handleConfirmEditAnnouncement = async () => {
if (!id || !groupId) {
return;
}
try {
await modifyGroupInfo({
workbenchId: id,
groupId: groupId,
announce: editAnnouncementValue.trim() || undefined,
});
Toast.show({ content: "修改成功", position: "top" });
setEditAnnouncementVisible(false);
setEditAnnouncementValue("");
// 刷新群详情
fetchGroupDetail();
} catch (e: any) {
Toast.show({
content: e?.message || "修改群公告失败",
position: "top"
});
}
};
const handleShowQRCode = () => {
Toast.show({ content: "群二维码功能待实现", position: "top" });
};
const handleSyncGroup = async () => {
if (!id || !groupId) {
Toast.show({ content: "参数错误", position: "top" });
return;
}
setSyncing(true);
try {
const res = await syncGroupInfo({
workbenchId: id,
groupId: groupId,
});
const data = res?.data || res || {};
const successMessages: string[] = [];
if (data.groupInfoSynced) {
successMessages.push("群信息同步成功");
}
if (data.memberInfoSynced) {
successMessages.push("群成员信息同步成功");
}
if (successMessages.length > 0) {
Toast.show({
content: successMessages.join(""),
position: "top"
});
// 同步成功后刷新群详情
await fetchGroupDetail();
} else {
Toast.show({
content: res?.msg || "同步完成",
position: "top"
});
}
} catch (e: any) {
Toast.show({
content: e?.message || "同步群信息失败",
position: "top"
});
} finally {
setSyncing(false);
}
};
const handleDisbandGroup = () => {
Dialog.confirm({
content: "确定要退出该群组吗?",
confirmText: "确定",
cancelText: "取消",
onConfirm: async () => {
if (!id || !groupId) {
Toast.show({ content: "参数错误", position: "top" });
return;
}
try {
await quitGroup({
workbenchId: id,
groupId: groupId,
});
Toast.show({ content: "退出群组成功", position: "top" });
// 退出成功后返回上一页
navigate(-1);
} catch (e: any) {
Toast.show({
content: e?.message || "退出群组失败",
position: "top"
});
}
},
});
};
const handleRemoveMember = (memberId: string) => {
Dialog.confirm({
content: "确定要移除此成员吗?",
confirmText: "确定",
cancelText: "取消",
onConfirm: () => {
Toast.show({ content: "移除成员功能待实现", position: "top" });
},
});
};
// 过滤成员
const filteredMembers = groupDetail?.members?.filter((member) => {
if (!searchKeyword) return true;
const keyword = searchKeyword.toLowerCase();
return (
member.nickname?.toLowerCase().includes(keyword) ||
member.wechatId?.toLowerCase().includes(keyword)
);
}) || [];
// 群组图标颜色
const iconColors = {
from: "#3b82f6",
to: "#4f46e5",
};
if (loading) {
return (
<Layout
header={<NavCommon title="群详情" backFn={() => navigate(-1)} />}
>
<div className={style.loadingContainer}>
<SpinLoading style={{ "--size": "48px" }} />
</div>
</Layout>
);
}
if (!groupDetail) {
return (
<Layout
header={<NavCommon title="群详情" backFn={() => navigate(-1)} />}
>
<div className={style.emptyContainer}>
<div className={style.emptyText}></div>
<button
className={style.backButton}
onClick={() => navigate(-1)}
>
</button>
</div>
</Layout>
);
}
return (
<Layout
header={
<NavCommon
title="群详情"
backFn={() => navigate(-1)}
/>
}
>
<div className={style.container}>
{/* 同步遮罩层 */}
{syncing && (
<div className={style.syncOverlay}>
<div className={style.syncContent}>
<SpinLoading style={{ "--size": "32px" }} />
<div className={style.syncText}>...</div>
</div>
</div>
)}
{/* 群组信息卡片 */}
<section className={style.groupInfoCard}>
{groupDetail.avatar || groupDetail.groupAvatar ? (
<img
className={style.groupIconLarge}
src={groupDetail.avatar || groupDetail.groupAvatar}
alt={groupDetail.name || ""}
onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = 'none';
const fallback = target.nextElementSibling as HTMLElement;
if (fallback) {
fallback.style.display = 'flex';
}
}}
/>
) : null}
<div
className={style.groupIconLarge}
style={{
display: groupDetail.avatar || groupDetail.groupAvatar ? 'none' : 'flex',
background: `linear-gradient(to bottom right, ${iconColors.from}, ${iconColors.to})`,
}}
>
{(groupDetail.name || '').charAt(0) || '👥'}
</div>
<h2 className={style.groupTitle}>{groupDetail.name}</h2>
<div className={style.createTimeInfo}>
<ScheduleOutlined className={style.createTimeIcon} />
{groupDetail.createTime || "-"}
</div>
<div className={style.actionButtons}>
<button className={style.actionButton} onClick={handleEditName}>
<EditOutlined className={style.actionButtonIcon} />
</button>
{/* 群二维码功能暂时隐藏 */}
{/* <button className={style.actionButton} onClick={handleShowQRCode}>
<QrcodeOutlined className={style.actionButtonIcon} />
群二维码
</button> */}
</div>
</section>
{/* 基本信息 */}
<section className={style.basicInfoCard}>
<h3 className={style.sectionTitle}>
<span className={style.sectionTitleDot}></span>
</h3>
<div className={style.basicInfoList}>
<div className={style.basicInfoItem}>
<span className={style.basicInfoLabel}></span>
<div className={style.basicInfoValue}>
<span className={style.planIcon}>📋</span>
{groupDetail.planName || "-"}
</div>
</div>
<div className={style.basicInfoDivider}></div>
<div className={style.basicInfoItem}>
<span className={style.basicInfoLabel}></span>
<div className={style.basicInfoValue}>
{groupDetail.groupAdmin ? (
<>
<span className={style.adminAvatar}>
{groupDetail.groupAdmin.avatar ? (
<img
src={groupDetail.groupAdmin.avatar}
alt={groupDetail.groupAdmin.nickname || ""}
onError={(e) => {
(e.target as HTMLImageElement).style.display = "none";
(e.target as HTMLImageElement).nextElementSibling?.classList.remove(
style.hidden
);
}}
/>
) : null}
<span className={style.adminAvatarText}>
{groupDetail.groupAdmin.nickname?.charAt(0)?.toUpperCase() || "A"}
</span>
</span>
{groupDetail.groupAdmin.wechatId || "-"}
</>
) : (
"-"
)}
</div>
</div>
<div className={style.basicInfoDivider}></div>
<div className={style.basicInfoItem}>
<span className={style.basicInfoLabel}></span>
<div
className={style.basicInfoValue}
style={{ cursor: "pointer" }}
onClick={handleEditAnnouncement}
>
{groupDetail.announcement ? (
groupDetail.announcement
) : (
<span className={style.noAnnouncement}>
<span className={style.chevronIcon}></span>
</span>
)}
</div>
</div>
</div>
</section>
{/* 群成员 */}
<section className={style.membersCard}>
<div className={style.membersHeader}>
<h3 className={style.sectionTitle}>
<span className={style.sectionTitleDot}></span>
<span className={style.memberCountBadge}>
{groupDetail.memberCount || 0}
</span>
</h3>
<button
className={style.searchButton}
onClick={() => setShowSearch(!showSearch)}
title="搜索成员"
>
<SearchOutlined />
</button>
</div>
{showSearch && (
<div className={style.searchBox}>
<input
className={style.searchInput}
placeholder="搜索成员..."
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
/>
</div>
)}
<div className={style.membersList}>
{filteredMembers.length > 0 ? (
filteredMembers.map((member) => {
const isGroupAdmin = member.isOwner === 1 || member.isGroupAdmin || member.id === groupDetail.groupAdmin?.id;
return (
<div key={member.friendId || member.id || member.wechatId} className={style.memberItem}>
<div className={style.memberInfo}>
<div className={style.memberAvatarWrapper}>
{member.avatar ? (
<img
className={style.memberAvatar}
src={member.avatar}
alt={member.nickname || ""}
onError={(e) => {
(e.target as HTMLImageElement).style.display = "none";
}}
/>
) : (
<div className={style.memberAvatarPlaceholder}>
{member.nickname?.charAt(0)?.toUpperCase() || "?"}
</div>
)}
{isGroupAdmin && (
<div className={style.adminBadge}>
<StarOutlined className={style.adminBadgeIcon} />
</div>
)}
</div>
<div className={style.memberInfoText}>
<div className={style.memberNameRow}>
<span className={style.memberName}>
{member.nickname || "-"}
</span>
{member.isQuit === 1 && (
<span className={style.quitTag}>退</span>
)}
{member.joinStatus && (
<span className={style.joinStatusTag}>
{member.joinStatus === "auto" ? "自动" : "手动"}
</span>
)}
</div>
<div className={style.memberWechatId}>
{member.wechatId || "-"}
</div>
{(member.alias || member.remark) && (
<div className={style.memberExtraInfo}>
{member.alias && <span>{member.alias}</span>}
{member.alias && member.remark && <span className={style.divider}>|</span>}
{member.remark && <span>{member.remark}</span>}
</div>
)}
{member.joinTime && (
<div className={style.memberJoinTime}>
{member.joinTime}
</div>
)}
</div>
</div>
<div className={style.memberActions}>
{isGroupAdmin ? (
<span className={style.adminTag}></span>
) : (
// 移除成员功能暂时隐藏
// <button
// className={style.removeButton}
// onClick={() => handleRemoveMember(String(member.friendId || member.id || member.wechatId || ''))}
// >
// <CloseCircleOutlined />
// </button>
null
)}
</div>
</div>
);
})
) : (
<div className={style.emptyMembers}>
<div className={style.emptyMembersText}>
{searchKeyword ? "未找到匹配的成员" : "暂无成员"}
</div>
</div>
)}
</div>
</section>
{/* 操作按钮 */}
<section className={style.actionsSection}>
<button className={style.actionCard} onClick={handleSyncGroup}>
<div className={style.actionCardIcon}>
<SyncOutlined />
</div>
<span className={style.actionCardText}></span>
</button>
<button
className={`${style.actionCard} ${style.actionCardDanger}`}
onClick={handleDisbandGroup}
>
<div className={`${style.actionCardIcon} ${style.actionCardIconDanger}`}>
<LogoutOutlined />
</div>
<span className={`${style.actionCardText} ${style.actionCardTextDanger}`}>
/退
</span>
</button>
</section>
</div>
{/* 修改群名称弹窗 */}
<Dialog
visible={editNameVisible}
content={
<div style={{ padding: "16px 0" }}>
<div style={{ marginBottom: "8px", fontSize: "14px", color: "#666", fontWeight: 500 }}>
</div>
<Input
placeholder="请输入群名称"
value={editNameValue}
onChange={(val) => setEditNameValue(val)}
style={{
fontSize: "14px",
padding: "10px 12px",
border: "1px solid #e5e5e5",
borderRadius: "6px",
backgroundColor: "#fff",
}}
maxLength={30}
/>
</div>
}
closeOnAction
onClose={() => {
setEditNameVisible(false);
setEditNameValue("");
}}
actions={[
{
key: "cancel",
text: "取消",
onClick: () => {
setEditNameVisible(false);
setEditNameValue("");
},
},
{
key: "confirm",
text: "确定",
primary: true,
onClick: handleConfirmEditName,
},
]}
/>
{/* 修改群公告弹窗 */}
<Dialog
visible={editAnnouncementVisible}
content={
<div style={{ padding: "16px 0" }}>
<div style={{ marginBottom: "8px", fontSize: "14px", color: "#666", fontWeight: 500 }}>
</div>
<TextArea
placeholder="请输入群公告"
value={editAnnouncementValue}
onChange={(val) => setEditAnnouncementValue(val)}
style={{
fontSize: "14px",
padding: "10px 12px",
border: "1px solid #e5e5e5",
borderRadius: "6px",
backgroundColor: "#fff",
minHeight: "120px",
}}
rows={5}
maxLength={500}
showCount
/>
</div>
}
closeOnAction
onClose={() => {
setEditAnnouncementVisible(false);
setEditAnnouncementValue("");
}}
actions={[
{
key: "cancel",
text: "取消",
onClick: () => {
setEditAnnouncementVisible(false);
setEditAnnouncementValue("");
},
},
{
key: "confirm",
text: "确定",
primary: true,
onClick: handleConfirmEditAnnouncement,
},
]}
/>
</Layout>
);
};
export default GroupDetailPage;

View File

@@ -0,0 +1,275 @@
.container {
padding: 16px;
padding-bottom: 100px;
background: #f3f4f6;
min-height: 100vh;
}
.loadingContainer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
}
.moreButton {
padding: 8px;
margin-right: -8px;
background: none;
border: none;
cursor: pointer;
color: #6b7280;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
transition: color 0.2s;
&:hover {
color: #111827;
}
}
// 搜索和筛选区域
.filterSection {
position: sticky;
top: 0;
z-index: 40;
background: #f3f4f6;
padding-top: 8px;
padding-bottom: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.searchBox {
position: relative;
display: flex;
align-items: center;
}
.searchIcon {
position: absolute;
left: 12px;
font-size: 18px;
color: #9ca3af;
pointer-events: none;
transition: color 0.2s;
}
.searchInput {
width: 100%;
padding: 10px 12px 10px 40px;
border: none;
border-radius: 12px;
background: #fff;
color: #111827;
font-size: 14px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
&::placeholder {
color: #9ca3af;
}
&:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5);
}
&:focus + .searchIcon,
&:focus ~ .searchIcon {
color: #3b82f6;
}
}
.filterButtons {
display: flex;
align-items: center;
gap: 8px;
overflow-x: auto;
padding-bottom: 4px;
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.filterButton {
display: flex;
align-items: center;
padding: 6px 12px;
border-radius: 8px;
background: #fff;
border: 1px solid #e5e7eb;
color: #4b5563;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
cursor: pointer;
transition: all 0.2s;
&:active {
background: #f9fafb;
}
}
.filterButtonActive {
background: #3b82f6;
color: #fff;
border-color: #3b82f6;
box-shadow: 0 1px 2px 0 rgba(59, 130, 246, 0.3);
&:active {
background: #2563eb;
}
}
.filterIcon {
font-size: 16px;
margin-left: 4px;
display: flex;
align-items: center;
}
// 群组列表
.groupsList {
display: flex;
flex-direction: column;
gap: 12px;
}
.groupCard {
background: #fff;
border-radius: 12px;
padding: 16px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.08);
border: 1px solid #e5e7eb;
transition: all 0.2s ease;
cursor: pointer;
&:active {
transform: scale(0.98);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
&:hover {
box-shadow: 0 4px 8px -2px rgba(0, 0, 0, 0.12);
border-color: #d1d5db;
}
}
.groupCardContent {
display: flex;
align-items: flex-start;
gap: 12px;
width: 100%;
}
.groupIconWrapper {
position: relative;
width: 48px;
height: 48px;
flex-shrink: 0;
}
.groupIconImg,
.groupIconFallback {
width: 48px;
height: 48px;
border-radius: 10px;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.08);
}
.groupIconImg {
display: block;
object-fit: cover;
width: 100%;
height: 100%;
}
.groupIconFallback {
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 20px;
font-weight: 600;
background-size: cover;
background-position: center;
letter-spacing: 0.5px;
}
.groupInfo {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 6px;
}
.groupName {
font-size: 15px;
font-weight: 600;
color: #111827;
margin: 0;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 8px;
}
.groupMeta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
font-size: 12px;
}
.memberCount {
display: inline-flex;
align-items: center;
gap: 4px;
color: #4b5563;
background: #f3f4f6;
padding: 3px 8px;
border-radius: 6px;
font-weight: 500;
line-height: 1.4;
}
.createTime {
color: #6b7280;
font-weight: 400;
}
.emptyState {
padding: 60px 20px;
text-align: center;
}
.emptyText {
font-size: 14px;
color: #9ca3af;
}
.footer {
margin-top: 32px;
text-align: center;
padding: 20px 0;
}
.footerText {
font-size: 12px;
color: #9ca3af;
margin: 0;
}

View File

@@ -0,0 +1,290 @@
import React, { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { Toast, SpinLoading, InfiniteScroll } from "antd-mobile";
import { SearchOutlined, DownOutlined, UpOutlined } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import NavCommon from "@/components/NavCommon";
import { getCreatedGroupsList } from "../form/api";
import style from "./groups-list.module.scss";
interface Group {
id: number | string;
name?: string;
groupName?: string;
memberCount: number;
memberCountText?: string;
createTime: string;
avatar?: string;
groupAvatar?: string;
memberAvatars?: Array<{
avatar?: string;
wechatId?: string;
nickname?: string;
}>;
ownerNickname?: string;
ownerWechatId?: string;
ownerAvatar?: string;
[key: string]: any;
}
interface GroupListPageProps {
planId?: string;
}
const GroupListPage: React.FC<GroupListPageProps> = ({ planId: propPlanId }) => {
const { id } = useParams();
const navigate = useNavigate();
const planId = propPlanId || id;
const [loading, setLoading] = useState(true);
const [groups, setGroups] = useState<Group[]>([]);
const [searchKeyword, setSearchKeyword] = useState("");
const [sortBy, setSortBy] = useState<"all" | "createTime" | "memberCount">("all");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
const [currentPage, setCurrentPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
useEffect(() => {
if (!planId) return;
fetchGroups(true);
}, [planId]);
useEffect(() => {
if (!planId) return;
const timer = setTimeout(() => {
fetchGroups(true);
}, 500);
return () => clearTimeout(timer);
}, [searchKeyword]);
const fetchGroups = async (reset = false) => {
if (!planId) return;
const page = reset ? 1 : currentPage;
if (reset) {
setLoading(true);
}
try {
const res = await getCreatedGroupsList({
workbenchId: planId,
page,
limit: 20,
keyword: searchKeyword || undefined,
});
// 注意request 拦截器已经提取了 data 字段,所以 res 就是 data 对象
// 接口返回结构:{ list: [...], total: 2, page: "1", limit: "10" }
const groupsData = res?.list || (Array.isArray(res) ? res : []);
if (reset) {
setGroups(groupsData);
setCurrentPage(2); // 重置后下一页是2
} else {
setGroups(prev => [...prev, ...groupsData]);
setCurrentPage(prev => prev + 1);
}
setHasMore(groupsData.length >= 20);
} catch (e: any) {
// request拦截器在code !== 200时会reject并显示Toast
// 如果拦截器没有显示错误(比如网络错误),这里才显示
// 注意拦截器已经在错误时显示了Toast所以这里通常不需要再显示
} finally {
if (reset) {
setLoading(false);
}
}
};
const handleLoadMore = async () => {
if (hasMore && !loading) {
await fetchGroups(false);
}
};
// 排序(搜索和过滤由接口完成)
const sortedGroups = [...groups].sort((a, b) => {
if (sortBy === "createTime") {
const timeA = new Date(a.createTime || 0).getTime();
const timeB = new Date(b.createTime || 0).getTime();
return sortOrder === "asc" ? timeA - timeB : timeB - timeA;
} else if (sortBy === "memberCount") {
return sortOrder === "asc" ? a.memberCount - b.memberCount : b.memberCount - a.memberCount;
}
return 0;
});
const handleSortClick = (type: "createTime" | "memberCount") => {
if (sortBy === type) {
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
} else {
setSortBy(type);
setSortOrder("desc");
}
};
const getSortIcon = (type: "createTime" | "memberCount") => {
if (sortBy !== type) return <span style={{ fontSize: '12px' }}></span>;
return sortOrder === "asc" ? <UpOutlined /> : <DownOutlined />;
};
// 群组图标颜色配置
const iconColors = [
{ from: "#3b82f6", to: "#4f46e5" }, // blue to indigo
{ from: "#6366f1", to: "#8b5cf6" }, // indigo to purple
{ from: "#a855f7", to: "#ec4899" }, // purple to pink
{ from: "#f472b6", to: "#f43f5e" }, // pink to rose
{ from: "#fb923c", to: "#ef4444" }, // orange to red
{ from: "#14b8a6", to: "#10b981" }, // teal to emerald
{ from: "#22d3ee", to: "#3b82f6" }, // cyan to blue
];
const getGroupIcon = (index: number) => {
const colors = iconColors[index % iconColors.length];
return { from: colors.from, to: colors.to };
};
const getGroupIconEmoji = (group: Group, index: number) => {
// 可以根据群组名称或其他属性返回不同的图标
const icons = ["groups", "star", "fiber_new", "local_fire_department", "campaign", "forum", "school"];
return icons[index % icons.length];
};
if (loading) {
return (
<Layout
header={<NavCommon title="所有群组列表" backFn={() => navigate(-1)} />}
>
<div className={style.loadingContainer}>
<SpinLoading style={{ "--size": "48px" }} />
</div>
</Layout>
);
}
return (
<Layout
header={
<NavCommon
title="所有群组列表"
backFn={() => navigate(-1)}
/>
}
>
<div className={style.container}>
{/* 搜索和筛选区域 */}
<div className={style.filterSection}>
<div className={style.searchBox}>
<SearchOutlined className={style.searchIcon} />
<input
className={style.searchInput}
placeholder="搜索群名称、群ID、群主昵称..."
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
/>
</div>
<div className={style.filterButtons}>
<button
className={`${style.filterButton} ${sortBy === "all" ? style.filterButtonActive : ""}`}
onClick={() => {
setSortBy("all");
setSortOrder("desc");
}}
>
</button>
<button
className={`${style.filterButton} ${sortBy === "createTime" ? style.filterButtonActive : ""}`}
onClick={() => handleSortClick("createTime")}
>
<span className={style.filterIcon}>{getSortIcon("createTime")}</span>
</button>
<button
className={`${style.filterButton} ${sortBy === "memberCount" ? style.filterButtonActive : ""}`}
onClick={() => handleSortClick("memberCount")}
>
<span className={style.filterIcon}>{getSortIcon("memberCount")}</span>
</button>
</div>
</div>
{/* 群组列表 */}
<div className={style.groupsList}>
{sortedGroups.length > 0 ? (
sortedGroups.map((group, index) => {
const iconColors = getGroupIcon(index);
return (
<div
key={String(group.id)}
className={style.groupCard}
onClick={() => navigate(`/workspace/group-create/${planId}/groups/${String(group.id)}`)}
>
<div className={style.groupCardContent}>
<div className={style.groupIconWrapper}>
{group.avatar || group.groupAvatar ? (
<img
className={style.groupIconImg}
src={group.avatar || group.groupAvatar}
alt={group.groupName || group.name || ""}
onError={(e) => {
// 加载失败时显示默认图标
const target = e.target as HTMLImageElement;
target.style.display = 'none';
const fallback = target.parentElement?.querySelector(`.${style.groupIconFallback}`) as HTMLElement;
if (fallback) {
fallback.style.display = 'flex';
}
}}
/>
) : null}
<div
className={style.groupIconFallback}
style={{
display: group.avatar || group.groupAvatar ? 'none' : 'flex',
background: `linear-gradient(to bottom right, ${iconColors.from}, ${iconColors.to})`,
}}
>
{(group.groupName || group.name || '').charAt(0) || '👥'}
</div>
</div>
<div className={style.groupInfo}>
<h4 className={style.groupName}>{group.groupName || group.name || `群组 ${index + 1}`}</h4>
<div className={style.groupMeta}>
<span className={style.memberCount}>
👤 {group.memberCountText || `${group.memberCount || 0}`}
</span>
<span className={style.createTime}>{group.createTime || "-"}</span>
</div>
</div>
</div>
</div>
);
})
) : (
<div className={style.emptyState}>
<div className={style.emptyText}></div>
</div>
)}
</div>
{/* 无限滚动加载 */}
{sortedGroups.length > 0 && (
<InfiniteScroll
loadMore={handleLoadMore}
hasMore={hasMore}
threshold={100}
>
{hasMore ? (
<div className={style.footer}>
<SpinLoading style={{ "--size": "24px" }} />
</div>
) : (
<div className={style.footer}>
<p className={style.footerText}> {sortedGroups.length} </p>
</div>
)}
</InfiniteScroll>
)}
</div>
</Layout>
);
};
export default GroupListPage;

View File

@@ -0,0 +1,482 @@
.container {
padding: 16px;
padding-bottom: 100px;
background: #f3f4f6;
min-height: 100vh;
}
.loadingContainer,
.emptyContainer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
}
.emptyText {
font-size: 16px;
color: #64748b;
margin-bottom: 24px;
}
// 基本信息卡片
.infoCard {
background: #fff;
border-radius: 16px;
padding: 20px;
margin-bottom: 24px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
border: 1px solid #e5e7eb;
}
.infoHeader {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.infoTitle {
font-size: 20px;
font-weight: 700;
color: #111827;
margin: 0 0 4px 0;
}
.infoSubtitle {
font-size: 14px;
color: #6b7280;
margin: 0;
}
.statusBadge {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 9999px;
font-size: 12px;
font-weight: 500;
border: 1px solid transparent;
}
.statusRunning {
background: #d1fae5;
color: #065f46;
border-color: #a7f3d0;
}
.statusStopped {
background: #f3f4f6;
color: #6b7280;
border-color: #e5e7eb;
}
.statusDot {
width: 6px;
height: 6px;
background: #10b981;
border-radius: 50%;
margin-right: 6px;
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.infoMeta {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
padding-top: 16px;
border-top: 1px solid #f3f4f6;
}
.metaItem {
display: flex;
flex-direction: column;
gap: 4px;
}
.metaLabel {
font-size: 12px;
color: #9ca3af;
}
.metaValue {
display: flex;
align-items: center;
font-size: 14px;
font-weight: 500;
color: #374151;
}
.metaIcon {
font-size: 16px;
color: #9ca3af;
margin-right: 4px;
}
// 统计信息
.statsSection {
margin-bottom: 24px;
}
.sectionTitle {
font-size: 16px;
font-weight: 600;
color: #111827;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.sectionTitleDot {
width: 4px;
height: 16px;
background: #3b82f6;
border-radius: 2px;
}
.statsGrid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.statCard {
background: #fff;
border-radius: 12px;
padding: 16px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
border: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.statIcon {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
margin-bottom: 8px;
}
.statIconBlue {
background: #dbeafe;
color: #3b82f6;
}
.statIconGreen {
background: #d1fae5;
color: #10b981;
}
.statIconPurple {
background: #f3e8ff;
color: #8b5cf6;
}
.statNumber {
font-size: 24px;
font-weight: 700;
color: #111827;
line-height: 1;
margin-bottom: 4px;
}
.statLabel {
font-size: 12px;
color: #6b7280;
}
// 配置信息
.configCard {
background: #fff;
border-radius: 16px;
padding: 20px;
margin-bottom: 24px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
border: 1px solid #e5e7eb;
}
.configList {
display: flex;
flex-direction: column;
gap: 0;
}
.configItem {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
}
.configLabel {
font-size: 14px;
color: #6b7280;
}
.configValue {
font-size: 14px;
font-weight: 500;
color: #111827;
display: flex;
align-items: center;
}
.deviceIcon {
font-size: 16px;
margin-right: 4px;
}
.configDivider {
height: 1px;
background: #f3f4f6;
}
.wechatTags {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-end;
}
.wechatTag {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 8px;
font-size: 12px;
font-weight: 500;
background: #f3f4f6;
color: #374151;
border: 1px solid #e5e7eb;
}
.wechatTagAdmin {
background: #fef3c7;
color: #92400e;
border-color: #fde68a;
}
.adminBadge {
margin-left: 4px;
padding: 1px 4px;
border-radius: 4px;
font-size: 10px;
border: 1px solid rgba(146, 64, 14, 0.3);
background: rgba(254, 243, 199, 0.5);
}
// 群列表
.groupsSection {
margin-bottom: 24px;
}
.groupsHeader {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.viewAllButton {
font-size: 12px;
font-weight: 500;
color: #3b82f6;
background: none;
border: none;
cursor: pointer;
padding: 0;
}
.groupsList {
display: flex;
flex-direction: column;
gap: 12px;
}
.groupCard {
background: #fff;
border-radius: 12px;
padding: 16px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.08);
border: 1px solid #e5e7eb;
transition: all 0.2s ease;
cursor: pointer;
&:active {
transform: scale(0.98);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
&:hover {
box-shadow: 0 4px 8px -2px rgba(0, 0, 0, 0.12);
border-color: #d1d5db;
}
}
.groupCardHeader {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 12px;
}
.groupCardLeft {
display: flex;
gap: 12px;
align-items: flex-start;
flex: 1;
}
.groupIconWrapper {
position: relative;
width: 48px;
height: 48px;
flex-shrink: 0;
}
.groupIconImg,
.groupIconFallback {
width: 48px;
height: 48px;
border-radius: 10px;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.08);
}
.groupIconImg {
display: block;
object-fit: cover;
width: 100%;
height: 100%;
}
.groupIconFallback {
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 20px;
font-weight: 600;
background-size: cover;
background-position: center;
letter-spacing: 0.5px;
}
.groupName {
font-size: 15px;
font-weight: 600;
color: #111827;
margin: 0 0 6px 0;
line-height: 1.4;
}
.groupMeta {
display: flex;
align-items: center;
gap: 10px;
font-size: 12px;
color: #6b7280;
}
.groupMemberCount {
display: inline-flex;
align-items: center;
gap: 4px;
background: #f3f4f6;
padding: 3px 8px;
border-radius: 6px;
font-weight: 500;
color: #4b5563;
line-height: 1.4;
}
.groupDate {
color: #6b7280;
font-weight: 400;
}
.chevronIcon {
font-size: 24px;
color: #d1d5db;
flex-shrink: 0;
}
.groupMembers {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 12px;
margin-top: 8px;
margin-left: 20px;
border-top: 1px solid #f3f4f6;
}
.memberAvatars {
display: flex;
align-items: center;
margin-left: -10px;
}
.memberAvatar {
width: 24px;
height: 24px;
border-radius: 50%;
border: 2px solid #fff;
margin-left: -10px;
object-fit: cover;
position: relative;
}
.memberMore {
width: 24px;
height: 24px;
border-radius: 50%;
border: 2px solid #fff;
background: #f3f4f6;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 500;
color: #6b7280;
margin-left: -10px;
position: relative;
z-index: 0;
}
.viewDetailText {
font-size: 12px;
color: #9ca3af;
font-weight: 400;
}
.emptyGroups {
padding: 40px 20px;
text-align: center;
}
.emptyGroupsText {
font-size: 14px;
color: #9ca3af;
}

View File

@@ -0,0 +1,401 @@
import React, { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { Button, Toast, SpinLoading } from "antd-mobile";
import { EditOutlined, ScheduleOutlined, HistoryOutlined } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import NavCommon from "@/components/NavCommon";
import { getGroupCreateDetail, getCreatedGroupsList } from "../form/api";
import style from "./index.module.scss";
interface GroupCreateDetail {
id: string;
name: string;
planType: number; // 0-全局计划, 1-独立计划
status: number; // 1-启用, 0-禁用
createTime?: string;
updateTime?: string;
createdGroupsCount?: number; // 已建群数
totalMembersCount?: number; // 总人数
groupSizeMax?: number; // 群组最大人数
config?: {
deviceGroupsOptions?: Array<{
id: number;
nickname?: string;
memo?: string;
wechatId?: string;
}>;
wechatGroupsOptions?: Array<{
id: number;
wechatId?: string;
nickname?: string;
}>;
groupAdminWechatId?: number;
groupNameTemplate?: string;
};
groups?: Array<{
id: string;
name: string;
memberCount: number;
createTime: string;
members?: Array<{
id: string;
avatar?: string;
nickname?: string;
}>;
}>;
[key: string]: any;
}
const GroupCreateDetail: React.FC = () => {
const { id } = useParams();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [detail, setDetail] = useState<GroupCreateDetail | null>(null);
const [groups, setGroups] = useState<Array<{
id: number | string;
name?: string;
groupName?: string;
memberCount: number;
memberCountText?: string;
createTime: string;
avatar?: string;
groupAvatar?: string;
memberAvatars?: Array<{
avatar?: string;
wechatId?: string;
nickname?: string;
}>;
ownerNickname?: string;
ownerWechatId?: string;
ownerAvatar?: string;
}>>([]);
useEffect(() => {
if (!id) return;
fetchDetail();
fetchGroups();
}, [id]);
const fetchGroups = async () => {
if (!id) return;
try {
const res = await getCreatedGroupsList({
workbenchId: id,
page: 1,
limit: 10,
});
const groupsData = res?.list || res?.data?.list || res?.data || [];
setGroups(groupsData);
} catch (e: any) {
// 静默失败,不影响主详情展示
console.error("获取群组列表失败:", e);
}
};
const fetchDetail = async () => {
setLoading(true);
try {
const res = await getGroupCreateDetail(id!);
const config = res.config || {};
const stats = config.stats || {};
const detailData: GroupCreateDetail = {
id: String(res.id),
name: res.name || "",
planType: config.planType ?? res.planType ?? 1,
status: res.status ?? 1,
createTime: res.createTime || "",
updateTime: res.updateTime || res.createTime || "",
createdGroupsCount: stats.createdGroupsCount ?? res.createdGroupsCount ?? 0,
totalMembersCount: stats.totalMembersCount ?? res.totalMembersCount ?? 0,
groupSizeMax: config.groupSizeMax || 38,
config: {
deviceGroupsOptions: config.deviceGroupsOptions || [],
wechatGroupsOptions: config.wechatGroupsOptions || [],
groupAdminWechatId: config.groupAdminWechatId,
groupNameTemplate: config.groupNameTemplate || "",
},
groups: [], // 群列表通过单独的接口获取
};
setDetail(detailData);
} catch (e: any) {
Toast.show({ content: e?.message || "获取详情失败", position: "top" });
} finally {
setLoading(false);
}
};
const handleEdit = () => {
navigate(`/workspace/group-create/${id}/edit`);
};
if (loading) {
return (
<Layout
header={<NavCommon title="计划详情" backFn={() => navigate(-1)} />}
>
<div className={style.loadingContainer}>
<SpinLoading style={{ "--size": "48px" }} />
</div>
</Layout>
);
}
if (!detail) {
return (
<Layout
header={<NavCommon title="计划详情" backFn={() => navigate(-1)} />}
>
<div className={style.emptyContainer}>
<div className={style.emptyText}></div>
<Button color="primary" onClick={() => navigate("/workspace/group-create")}>
</Button>
</div>
</Layout>
);
}
const isRunning = detail.status === 1;
const planTypeText = detail.planType === 0 ? "全局计划" : "独立计划";
const executorDevice = detail.config?.deviceGroupsOptions?.[0];
const executorName = executorDevice?.nickname || executorDevice?.memo || executorDevice?.wechatId || "-";
const fixedWechatIds = detail.config?.wechatGroupsOptions || [];
const groupAdminId = detail.config?.groupAdminWechatId;
return (
<Layout
header={
<NavCommon
title="计划详情"
backFn={() => navigate(-1)}
right={
<Button
size="small"
color="primary"
onClick={handleEdit}
style={{ marginRight: "-8px" }}
>
<EditOutlined />
</Button>
}
/>
}
>
<div className={style.container}>
{/* 基本信息卡片 */}
<section className={style.infoCard}>
<div className={style.infoHeader}>
<div>
<h2 className={style.infoTitle}>{detail.name}</h2>
<p className={style.infoSubtitle}>{planTypeText}</p>
</div>
<span className={`${style.statusBadge} ${isRunning ? style.statusRunning : style.statusStopped}`}>
{isRunning && <span className={style.statusDot}></span>}
{isRunning ? "运行中" : "已停止"}
</span>
</div>
<div className={style.infoMeta}>
<div className={style.metaItem}>
<span className={style.metaLabel}></span>
<div className={style.metaValue}>
<ScheduleOutlined className={style.metaIcon} />
{detail.createTime || "-"}
</div>
</div>
<div className={style.metaItem}>
<span className={style.metaLabel}></span>
<div className={style.metaValue}>
<HistoryOutlined className={style.metaIcon} />
{detail.updateTime || "-"}
</div>
</div>
</div>
</section>
{/* 统计信息 */}
<section className={style.statsSection}>
<h3 className={style.sectionTitle}>
<span className={style.sectionTitleDot}></span>
</h3>
<div className={style.statsGrid}>
<div className={style.statCard}>
<div className={`${style.statIcon} ${style.statIconBlue}`}>
👥
</div>
<span className={style.statNumber}>{detail.createdGroupsCount || 0}</span>
<span className={style.statLabel}></span>
</div>
<div className={style.statCard}>
<div className={`${style.statIcon} ${style.statIconGreen}`}>
👥
</div>
<span className={style.statNumber}>{detail.totalMembersCount || 0}</span>
<span className={style.statLabel}></span>
</div>
<div className={style.statCard}>
<div className={`${style.statIcon} ${style.statIconPurple}`}>
📊
</div>
<span className={style.statNumber}>{detail.groupSizeMax || 38}</span>
<span className={style.statLabel}>/</span>
</div>
</div>
</section>
{/* 配置信息 */}
<section className={style.configCard}>
<h3 className={style.sectionTitle}>
<span className={style.sectionTitleDot}></span>
</h3>
<div className={style.configList}>
<div className={style.configItem}>
<span className={style.configLabel}></span>
<span className={style.configValue}></span>
</div>
<div className={style.configDivider}></div>
<div className={style.configItem}>
<span className={style.configLabel}></span>
<div className={style.configValue}>
<span className={style.deviceIcon}>📱</span>
{executorName}
</div>
</div>
<div className={style.configDivider}></div>
<div className={style.configItem}>
<span className={style.configLabel}></span>
<div className={style.wechatTags}>
{fixedWechatIds.length > 0 ? (
fixedWechatIds.map((wechat: any) => {
const isGroupAdmin = wechat.id === groupAdminId;
const displayText = wechat.nickname && wechat.wechatId
? `${wechat.nickname}${wechat.wechatId}`
: wechat.wechatId || wechat.nickname || "-";
return (
<span
key={wechat.id}
className={`${style.wechatTag} ${isGroupAdmin ? style.wechatTagAdmin : ""}`}
>
{displayText}
{isGroupAdmin && (
<span className={style.adminBadge}></span>
)}
</span>
);
})
) : (
<span className={style.configValue}>-</span>
)}
</div>
</div>
</div>
</section>
{/* 群列表 */}
<section className={style.groupsSection}>
<div className={style.groupsHeader}>
<h3 className={style.sectionTitle}>
<span className={style.sectionTitleDot}></span>
</h3>
<button
className={style.viewAllButton}
onClick={() => navigate(`/workspace/group-create/${id}/groups`)}
>
</button>
</div>
<div className={style.groupsList}>
{groups.length > 0 ? (
groups.map((group, index) => (
<div
key={String(group.id)}
className={style.groupCard}
onClick={() => navigate(`/workspace/group-create/${id}/groups/${String(group.id)}`)}
>
<div className={style.groupCardHeader}>
<div className={style.groupCardLeft}>
<div className={style.groupIconWrapper}>
{group.avatar || group.groupAvatar ? (
<img
className={style.groupIconImg}
src={group.avatar || group.groupAvatar}
alt={group.groupName || group.name || ""}
onError={(e) => {
// 加载失败时显示默认图标
const target = e.target as HTMLImageElement;
target.style.display = 'none';
const fallback = target.parentElement?.querySelector(`.${style.groupIconFallback}`) as HTMLElement;
if (fallback) {
fallback.style.display = 'flex';
}
}}
/>
) : null}
<div
className={style.groupIconFallback}
style={{
display: group.avatar || group.groupAvatar ? 'none' : 'flex',
background: `linear-gradient(to bottom right, ${
['#60a5fa', '#818cf8', '#a78bfa', '#f472b6'][index % 4]
}, ${
['#4f46e5', '#6366f1', '#8b5cf6', '#ec4899'][index % 4]
})`,
}}
>
{(group.groupName || group.name || '').charAt(0) || '👥'}
</div>
</div>
<div>
<h4 className={style.groupName}>{group.groupName || group.name || detail.config?.groupNameTemplate || `群组 ${index + 1}`}</h4>
<div className={style.groupMeta}>
<span className={style.groupMemberCount}>
👤 {group.memberCountText || `${group.memberCount || 0}`}
</span>
<span className={style.groupDate}>{group.createTime || "-"}</span>
</div>
</div>
</div>
<span className={style.chevronIcon}></span>
</div>
{group.memberAvatars && group.memberAvatars.length > 0 && (
<div className={style.groupMembers}>
<div className={style.memberAvatars}>
{group.memberAvatars.slice(0, 6).map((member, memberIndex) => (
<img
key={member.wechatId || memberIndex}
className={style.memberAvatar}
src={member.avatar || "https://via.placeholder.com/24"}
alt={member.nickname || ""}
style={{ zIndex: 6 - memberIndex }}
onError={(e) => {
(e.target as HTMLImageElement).src = "https://via.placeholder.com/24";
}}
/>
))}
{group.memberAvatars.length > 6 && (
<span className={style.memberMore}>
+{group.memberAvatars.length - 6}
</span>
)}
</div>
<span className={style.viewDetailText}></span>
</div>
)}
</div>
))
) : (
<div className={style.emptyGroups}>
<div className={style.emptyGroupsText}></div>
</div>
)}
</div>
</section>
</div>
</Layout>
);
};
export default GroupCreateDetail;

View File

@@ -16,3 +16,59 @@ export const getGroupCreateDetail = (id: string) =>
export const getGroupCreateList = (params: any) =>
request("/v1/workbench/list", params, "GET");
// 删除自动建群任务
export const deleteGroupCreate = (id: string) =>
request("/v1/workbench/delete", { id }, "DELETE");
// 切换任务状态
export const toggleGroupCreateStatus = (data: { id: string | number; status: number }) =>
request("/v1/workbench/update-status", { ...data }, "POST");
// 获取群列表
export const getCreatedGroupsList = (params: {
workbenchId: string;
page?: number;
limit?: number;
keyword?: string;
}) => {
// 如果没有指定 limit默认使用 20
const finalParams = {
...params,
limit: params.limit ?? 20,
};
return request("/v1/workbench/created-groups-list", finalParams, "GET");
};
// 获取群详情
export const getCreatedGroupDetail = (params: {
workbenchId: string;
groupId: string;
}) => {
return request("/v1/workbench/created-group-detail", params, "GET");
};
// 同步群信息
export const syncGroupInfo = (params: {
workbenchId: string;
groupId: string;
}) => {
return request("/v1/workbench/sync-group-info", params, "POST");
};
// 修改群名称/群公告
export const modifyGroupInfo = (params: {
workbenchId: string;
groupId: string;
chatroomName?: string;
announce?: string;
}) => {
return request("/v1/workbench/modify-group-info", params, "POST");
};
// 退出群组
export const quitGroup = (params: {
workbenchId: string;
groupId: string;
}) => {
return request("/v1/workbench/quit-group", params, "POST");
};

View File

@@ -692,3 +692,9 @@
pointer-events: none;
z-index: 1;
}
.statusSwitchContainer {
display: flex;
align-items: center;
justify-content: space-between;
}

View File

@@ -37,12 +37,13 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
onChange({
executor: selectedDevice,
executorId: selectedDevice.id,
groupName: executorName ? `${executorName}的群` : "", // 设置为"XXX的群"格式
groupNameTemplate: executorName ? `${executorName}的群` : "", // 设置为"XXX的群"格式
});
} else {
onChange({
executor: undefined,
executorId: undefined,
groupNameTemplate: "", // 清空群名称
});
}
setExecutorSelectionVisible(false);
@@ -51,7 +52,7 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
// 处理固定微信号选择必须3个
const handleFixedWechatIdsSelect = (friends: FriendSelectionItem[]) => {
// 检查总数是否超过3个包括已添加的手动微信号
const currentManualCount = (formData.fixedWechatIdsOptions || []).filter(f => f.isManual).length;
const currentManualCount = (formData.wechatGroupsOptions || []).filter(f => f.isManual).length;
const newSelectedCount = friends.length;
if (currentManualCount + newSelectedCount > 3) {
Toast.show({ content: "固定微信号最多只能选择3个", position: "top" });
@@ -60,10 +61,10 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
// 标记为选择的(非手动添加),确保所有从选择弹窗来的都标记为非手动
const selectedFriends = friends.map(f => ({ ...f, isManual: false }));
// 合并已添加的手动微信号和新的选择
const manualFriends = (formData.fixedWechatIdsOptions || []).filter(f => f.isManual === true);
const manualFriends = (formData.wechatGroupsOptions || []).filter(f => f.isManual === true);
onChange({
fixedWechatIds: [...manualFriends, ...selectedFriends].map(f => f.id),
fixedWechatIdsOptions: [...manualFriends, ...selectedFriends],
wechatGroups: [...manualFriends, ...selectedFriends].map(f => f.id),
wechatGroupsOptions: [...manualFriends, ...selectedFriends],
});
setFixedWechatIdsSelectionVisible(false);
};
@@ -77,6 +78,27 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
setFixedWechatIdsSelectionVisible(true);
};
// 打开群管理员选择弹窗
const handleOpenGroupAdminSelection = () => {
if (selectedWechatIds.length === 0) {
Toast.show({
content: manualAddedWechatIds.length > 0
? "群管理员只能从已选择的微信号中选择,不能选择手动添加的微信号"
: "请先选择固定微信号",
position: "top"
});
return;
}
// 如果当前选择的群管理员是手动添加的,清空选择
if (formData.groupAdminWechatIdOption && formData.groupAdminWechatIdOption.isManual) {
onChange({
groupAdminWechatIdOption: undefined,
groupAdminWechatId: undefined,
});
}
setGroupAdminSelectionVisible(true);
};
// 处理群管理员选择(单选)
const handleGroupAdminSelect = (friends: FriendSelectionItem[]) => {
if (friends.length > 0) {
@@ -99,12 +121,12 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
Toast.show({ content: "请输入微信号", position: "top" });
return;
}
const existingIds = formData.fixedWechatIdsOptions.map(f => f.wechatId.toLowerCase());
const existingIds = formData.wechatGroupsOptions.map(f => f.wechatId.toLowerCase());
if (existingIds.includes(manualWechatIdInput.trim().toLowerCase())) {
Toast.show({ content: "该微信号已添加", position: "top" });
return;
}
if (formData.fixedWechatIdsOptions.length >= 3) {
if (formData.wechatGroupsOptions.length >= 3) {
Toast.show({ content: "固定微信号最多只能添加3个", position: "top" });
return;
}
@@ -117,18 +139,26 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
isManual: true, // 标记为手动添加
};
onChange({
fixedWechatIds: [...formData.fixedWechatIds, newFriend.id],
fixedWechatIdsOptions: [...formData.fixedWechatIdsOptions, newFriend],
wechatGroups: [...formData.wechatGroups, newFriend.id],
wechatGroupsOptions: [...formData.wechatGroupsOptions, newFriend],
});
setManualWechatIdInput("");
};
// 移除固定微信号
const handleRemoveFixedWechatId = (id: number) => {
onChange({
fixedWechatIds: formData.fixedWechatIds.filter(fid => fid !== id),
fixedWechatIdsOptions: formData.fixedWechatIdsOptions.filter(f => f.id !== id),
});
const removedFriend = formData.wechatGroupsOptions.find(f => f.id === id);
const newOptions = formData.wechatGroupsOptions.filter(f => f.id !== id);
const updateData: Partial<GroupCreateFormData> = {
wechatGroups: formData.wechatGroups.filter(fid => fid !== id),
wechatGroupsOptions: newOptions,
};
// 如果移除的是群管理员,也要清除群管理员设置
if (formData.groupAdminWechatId === id) {
updateData.groupAdminWechatId = undefined;
updateData.groupAdminWechatIdOption = undefined;
}
onChange(updateData);
};
// 暴露方法给父组件
@@ -144,14 +174,14 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
return false;
}
// 固定微信号不是必填的,移除验证
if (!formData.groupName?.trim()) {
Toast.show({ content: "请输入群名称", position: "top" });
if (!formData.groupNameTemplate?.trim()) {
Toast.show({ content: "请输入群名称模板", position: "top" });
return false;
}
// 群名称长度验证2-100个字符
const groupNameLength = formData.groupName.trim().length;
if (groupNameLength < 2 || groupNameLength > 100) {
Toast.show({ content: "群名称长度应在2-100个字符之间", position: "top" });
// 群名称模板长度验证2-100个字符
const groupNameTemplateLength = formData.groupNameTemplate.trim().length;
if (groupNameTemplateLength < 2 || groupNameTemplateLength > 100) {
Toast.show({ content: "群名称模板长度应在2-100个字符之间", position: "top" });
return false;
}
return true;
@@ -163,8 +193,8 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
// 区分已选择的微信号(从下拉选择)和已添加的微信号(手动输入)
// 如果 isManual 未定义,默认为 false即选择的
const selectedWechatIds = (formData.fixedWechatIdsOptions || []).filter(f => !f.isManual);
const manualAddedWechatIds = (formData.fixedWechatIdsOptions || []).filter(f => f.isManual === true);
const selectedWechatIds = (formData.wechatGroupsOptions || []).filter(f => !f.isManual);
const manualAddedWechatIds = (formData.wechatGroupsOptions || []).filter(f => f.isManual === true);
return (
<div className={style.container}>
@@ -355,14 +385,8 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
</div>
{formData.groupAdminEnabled && (
<div
className={`${style.wechatSelectInput} ${(selectedWechatIds.length + manualAddedWechatIds.length) === 0 ? style.disabled : ''}`}
onClick={() => {
if ((selectedWechatIds.length + manualAddedWechatIds.length) === 0) {
Toast.show({ content: "请先选择固定微信号", position: "top" });
return;
}
setGroupAdminSelectionVisible(true);
}}
className={`${style.wechatSelectInput} ${selectedWechatIds.length === 0 ? style.disabled : ''}`}
onClick={handleOpenGroupAdminSelection}
>
<div className={style.selectInputWrapper}>
<span className={style.selectInputPlaceholder}>
@@ -383,8 +407,8 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
<input
type="text"
className={style.input}
value={formData.groupName}
onChange={e => onChange({ groupName: e.target.value })}
value={formData.groupNameTemplate}
onChange={e => onChange({ groupNameTemplate: e.target.value })}
placeholder="请输入群名称"
/>
</div>
@@ -448,8 +472,8 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
<input
type="time"
className={style.timeInput}
value={formData.executeTime || "09:00"}
onChange={e => onChange({ executeTime: e.target.value || "09:00" })}
value={formData.startTime || "09:00"}
onChange={e => onChange({ startTime: e.target.value || "09:00" })}
/>
<ClockCircleOutlined className={style.timeIcon} />
</div>
@@ -458,21 +482,25 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
<input
type="time"
className={style.timeInput}
value={formData.executeEndTime || "18:00"}
onChange={e => onChange({ executeEndTime: e.target.value || "18:00" })}
value={formData.endTime || "21:00"}
onChange={e => onChange({ endTime: e.target.value || "21:00" })}
/>
<ClockCircleOutlined className={style.timeIcon} />
</div>
</div>
</div>
{/* 错误提示 */}
{(selectedWechatIds.length + manualAddedWechatIds.length) !== 3 && (
<div className={style.errorTip}>
<ExclamationCircleOutlined className={style.errorIcon} />
<span className={style.errorText}>3</span>
{/* 是否启用 */}
<div className={style.card}>
<div className={style.statusSwitchContainer}>
<label className={style.label} style={{ marginBottom: 0 }}></label>
<Switch
checked={formData.status === 1}
onChange={(checked) => onChange({ status: checked ? 1 : 0 })}
/>
</div>
)}
</div>
{/* 隐藏的选择组件 */}
<div style={{ display: "none" }}>
@@ -518,12 +546,14 @@ const BasicSettings = forwardRef<BasicSettingsRef, BasicSettingsProps>(
</button>
</div>
<div style={{ maxHeight: "50vh", overflowY: "auto" }}>
{(selectedWechatIds.length + manualAddedWechatIds.length) === 0 ? (
{selectedWechatIds.length === 0 ? (
<div style={{ textAlign: "center", padding: "40px", color: "#94a3b8" }}>
{manualAddedWechatIds.length > 0
? "群管理员只能从已选择的微信号中选择,不能选择手动添加的微信号"
: "暂无固定微信号可选"}
</div>
) : (
[...selectedWechatIds, ...manualAddedWechatIds].map(friend => {
selectedWechatIds.map(friend => {
const isSelected = formData.groupAdminWechatIdOption?.id === friend.id;
return (
<div

View File

@@ -10,25 +10,25 @@ export interface DeviceSelectionStepRef {
}
interface DeviceSelectionStepProps {
selectedDevices?: DeviceSelectionItem[];
selectedDeviceIds?: number[];
deviceGroupsOptions?: DeviceSelectionItem[];
deviceGroups?: number[];
onSelect: (devices: DeviceSelectionItem[]) => void;
}
const DeviceSelectionStep = forwardRef<DeviceSelectionStepRef, DeviceSelectionStepProps>(
({ selectedDevices = [], selectedDeviceIds = [], onSelect }, ref) => {
({ deviceGroupsOptions = [], deviceGroups = [], onSelect }, ref) => {
const [deviceSelectionVisible, setDeviceSelectionVisible] = useState(false);
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
validate: async () => {
if (selectedDeviceIds.length === 0) {
if (deviceGroups.length === 0) {
return false;
}
return true;
},
getValues: () => {
return { selectedDevices, selectedDeviceIds };
return { deviceGroupsOptions, deviceGroups };
},
}));
@@ -48,10 +48,10 @@ const DeviceSelectionStep = forwardRef<DeviceSelectionStepRef, DeviceSelectionSt
className={style.deviceSelector}
onClick={() => setDeviceSelectionVisible(true)}
>
{selectedDevices.length > 0 ? (
{deviceGroupsOptions.length > 0 ? (
<div className={style.selectedDevicesInfo}>
<span className={style.selectedCountText}>
{selectedDevices.length}
{deviceGroupsOptions.length}
</span>
</div>
) : (
@@ -64,9 +64,9 @@ const DeviceSelectionStep = forwardRef<DeviceSelectionStepRef, DeviceSelectionSt
</div>
{/* 已选设备列表 */}
{selectedDevices.length > 0 && (
{deviceGroupsOptions.length > 0 && (
<div className={style.selectedDevicesGrid}>
{selectedDevices.map((device) => (
{deviceGroupsOptions.map((device) => (
<div key={device.id} className={style.selectedDeviceCard}>
<div className={style.deviceCardHeader}>
<div className={`${style.deviceCardIcon} ${device.status === "online" ? style.deviceIconOnline : style.deviceIconOffline}`}>
@@ -88,8 +88,8 @@ const DeviceSelectionStep = forwardRef<DeviceSelectionStepRef, DeviceSelectionSt
className={style.deviceCardDeleteButton}
onClick={(e) => {
e.stopPropagation();
const newSelectedDevices = selectedDevices.filter(d => d.id !== device.id);
onSelect(newSelectedDevices);
const newDeviceGroupsOptions = deviceGroupsOptions.filter(d => d.id !== device.id);
onSelect(newDeviceGroupsOptions);
}}
>
<DeleteOutlined className={style.deviceCardDeleteIcon} />
@@ -102,7 +102,7 @@ const DeviceSelectionStep = forwardRef<DeviceSelectionStepRef, DeviceSelectionSt
{/* 设备选择弹窗 */}
<div style={{ display: "none" }}>
<DeviceSelection
selectedOptions={selectedDevices}
selectedOptions={deviceGroupsOptions}
onSelect={handleDeviceSelect}
placeholder="选择设备"
showInput={false}

View File

@@ -19,24 +19,22 @@ const steps = [
const defaultForm: GroupCreateFormData = {
planType: 1, // 默认独立计划
name: "新建群计划",
name: "",
executorId: undefined,
executor: undefined,
selectedDevices: [],
selectedDeviceIds: [],
fixedWechatIds: [],
fixedWechatIdsOptions: [],
deviceGroupsOptions: [],
deviceGroups: [],
wechatGroups: [],
wechatGroupsOptions: [],
groupAdminEnabled: false,
groupAdminWechatId: undefined,
groupAdminWechatIdOption: undefined,
groupName: "卡若001",
groupMethod: 0, // 默认所有好友自动分组
maxGroupsPerDay: 10,
groupNameTemplate: "",
maxGroupsPerDay: 20,
groupSizeMin: 3,
groupSizeMax: 38,
executeType: 1, // 默认定时执行
executeTime: "09:00", // 默认开始时间
executeEndTime: "18:00", // 默认结束时间
startTime: "09:00", // 默认开始时间
endTime: "21:00", // 默认结束时间
poolGroups: [],
poolGroupsOptions: [],
status: 1, // 默认启用
@@ -61,30 +59,41 @@ const GroupCreateForm: React.FC = () => {
// 获取详情并回填表单
getGroupCreateDetail(id)
.then(res => {
const config = res.config || {};
// 转换 deviceGroups 从字符串数组到数字数组
const deviceGroups = (config.deviceGroups || []).map((id: string | number) => Number(id));
// 转换 poolGroups 保持字符串数组
const poolGroups = (config.poolGroups || []).map((id: string | number) => String(id));
// 转换 wechatGroups 到数字数组
const wechatGroups = (config.wechatGroups || []).map((id: string | number) => Number(id));
// 查找群管理员选项(如果有)
const groupAdminWechatIdOption = config.groupAdminWechatId && config.wechatGroupsOptions
? config.wechatGroupsOptions.find((f: any) => f.id === config.groupAdminWechatId)
: undefined;
const updatedForm: GroupCreateFormData = {
...defaultForm,
id: res.id,
planType: res.planType ?? 1,
id: String(res.id),
planType: config.planType ?? res.planType ?? 1,
name: res.name ?? "",
executorId: res.executorId,
executor: res.executor,
selectedDevices: res.selectedDevices || [],
selectedDeviceIds: res.selectedDeviceIds || [],
fixedWechatIds: res.fixedWechatIds || [],
fixedWechatIdsOptions: res.fixedWechatIdsOptions || [],
groupAdminEnabled: res.groupAdminEnabled ?? false,
groupAdminWechatId: res.groupAdminWechatId,
groupAdminWechatIdOption: res.groupAdminWechatIdOption,
groupName: res.groupName ?? "",
groupMethod: res.groupMethod ?? 0,
maxGroupsPerDay: res.maxGroupsPerDay ?? 10,
groupSizeMin: res.groupSizeMin ?? 3,
groupSizeMax: res.groupSizeMax ?? 38,
executeType: res.executeType ?? 1,
executeTime: res.executeTime ?? "09:00",
executeEndTime: res.executeEndTime ?? "18:00",
poolGroups: res.poolGroups || [],
poolGroupsOptions: res.poolGroupsOptions || [],
executorId: config.executorId,
executor: config.deviceGroupsOptions?.[0], // executor 使用第一个设备(如果需要)
deviceGroupsOptions: config.deviceGroupsOptions || [],
deviceGroups: deviceGroups,
wechatGroups: wechatGroups,
wechatGroupsOptions: config.wechatGroupsOptions || config.wechatFriendsOptions || [],
groupAdminEnabled: config.groupAdminEnabled === 1,
groupAdminWechatId: config.groupAdminWechatId || undefined,
groupAdminWechatIdOption: groupAdminWechatIdOption,
groupNameTemplate: config.groupNameTemplate || "",
maxGroupsPerDay: config.maxGroupsPerDay ?? 20,
groupSizeMin: config.groupSizeMin ?? 3,
groupSizeMax: config.groupSizeMax ?? 38,
startTime: config.startTime || "09:00",
endTime: config.endTime || "21:00",
poolGroups: poolGroups,
poolGroupsOptions: config.poolGroupsOptions || [],
status: res.status ?? 1,
};
setFormData(updatedForm);
@@ -163,9 +172,7 @@ const GroupCreateForm: React.FC = () => {
await createGroupCreate(submitData);
Toast.show({ content: "创建成功" });
}
// 注意:需要导航到列表页面,但目前列表页面还未创建
// navigate("/workspace/group-create");
navigate("/workspace");
navigate("/workspace/group-create");
} catch (e: any) {
Toast.show({ content: e?.message || "提交失败" });
} finally {
@@ -194,13 +201,13 @@ const GroupCreateForm: React.FC = () => {
return (
<DeviceSelectionStep
ref={deviceSelectionStepRef}
selectedDevices={formData.selectedDevices || []}
selectedDeviceIds={formData.selectedDeviceIds || []}
deviceGroupsOptions={formData.deviceGroupsOptions || []}
deviceGroups={formData.deviceGroups || []}
onSelect={(devices) => {
const deviceIds = devices.map(d => d.id);
handleFormDataChange({
selectedDevices: devices,
selectedDeviceIds: deviceIds,
deviceGroupsOptions: devices,
deviceGroups: deviceIds,
// 如果只有一个设备,也设置 executor 和 executorId 用于兼容
executor: devices.length === 1 ? devices[0] : undefined,
executorId: devices.length === 1 ? devices[0].id : undefined,

View File

@@ -9,21 +9,19 @@ export interface GroupCreateFormData {
name: string; // 计划名称
executor?: DeviceSelectionItem; // 执行智能体(执行者)- 单个设备(保留用于兼容)
executorId?: number; // 执行智能体ID设备ID保留用于兼容
selectedDevices?: DeviceSelectionItem[]; // 选中的设备列表
selectedDeviceIds?: number[]; // 选中的设备ID列表
fixedWechatIds: number[]; // 固定微信号ID列表必须3个
fixedWechatIdsOptions: FriendSelectionItem[]; // 固定微信号选项
deviceGroupsOptions?: DeviceSelectionItem[]; // 选中的设备列表
deviceGroups?: number[]; // 选中的设备ID列表
wechatGroups: number[]; // 固定微信号ID列表必须3个
wechatGroupsOptions: FriendSelectionItem[]; // 固定微信号选项
groupAdminEnabled: boolean; // 群管理员开关
groupAdminWechatId?: number; // 群管理员微信号ID
groupAdminWechatIdOption?: FriendSelectionItem; // 群管理员微信号选项
groupName: string; // 群名称
groupMethod: number; // 分组方式0-所有好友自动分组1-指定群数
maxGroupsPerDay: number; // 每日最大建群数当groupMethod为1时使用
groupNameTemplate: string; // 群名称模板
maxGroupsPerDay: number; // 每日最大建群数
groupSizeMin: number; // 群组最小人数
groupSizeMax: number; // 群组最大人数
executeType: number; // 执行类型0-立即执行1-定时执行
executeTime?: string; // 执行开始时间HH:mm默认 09:00
executeEndTime?: string; // 执行结束时间HH:mm默认 18:00
startTime?: string; // 执行开始时间HH:mm默认 09:00
endTime?: string; // 执行结束时间HH:mm默认 21:00
poolGroups?: string[]; // 流量池ID列表
poolGroupsOptions?: PoolSelectionItem[]; // 流量池选项列表
status: number; // 是否启用 (1: 启用, 0: 禁用)
@@ -44,7 +42,7 @@ export const formValidationRules = {
{ min: 2, max: 50, message: "计划名称长度应在2-50个字符之间" },
],
executorId: [{ required: true, message: "请选择执行智能体" }],
fixedWechatIds: [
wechatGroups: [
{ required: true, message: "请选择固定微信号" },
{
validator: (_: any, value: number[]) => {
@@ -55,9 +53,9 @@ export const formValidationRules = {
},
},
],
groupName: [
{ required: true, message: "请输入群名称" },
{ min: 2, max: 100, message: "群名称长度应在2-100个字符之间" },
groupNameTemplate: [
{ required: true, message: "请输入群名称模板" },
{ min: 2, max: 100, message: "群名称模板长度应在2-100个字符之间" },
],
maxGroupsPerDay: [
{ required: true, message: "请输入每日最大建群数" },
@@ -76,28 +74,4 @@ export const formValidationRules = {
{ required: true, message: "请输入群组最大人数" },
{ type: "number", min: 1, max: 500, message: "群组最大人数应在1-500之间" },
],
executeDate: [
{
validator: (_: any, value: string, callback: any) => {
// 如果执行类型是定时执行,则必填
const executeType = callback?.executeType ?? 1;
if (executeType === 1 && !value) {
return Promise.reject(new Error("请选择执行日期"));
}
return Promise.resolve();
},
},
],
executeTime: [
{
validator: (_: any, value: string, callback: any) => {
// 如果执行类型是定时执行,则必填
const executeType = callback?.executeType ?? 1;
if (executeType === 1 && !value) {
return Promise.reject(new Error("请选择执行时间"));
}
return Promise.resolve();
},
},
],
};

View File

@@ -0,0 +1,303 @@
.container {
padding: 16px;
padding-bottom: 100px;
background: #f8fafc;
min-height: 100vh;
}
.infoBox {
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: 12px;
padding: 14px 16px;
margin-bottom: 20px;
display: flex;
align-items: flex-start;
gap: 12px;
}
.infoIcon {
color: #2563eb;
font-size: 20px;
margin-top: 2px;
flex-shrink: 0;
}
.infoText {
font-size: 13px;
color: #1e40af;
line-height: 1.5;
margin: 0;
font-weight: 500;
}
.section {
margin-bottom: 24px;
}
.sectionTitle {
font-size: 15px;
font-weight: 700;
margin-bottom: 12px;
padding-left: 4px;
display: flex;
align-items: center;
gap: 8px;
color: #1e293b;
}
.sectionDot {
width: 4px;
height: 14px;
background: #2563eb;
border-radius: 2px;
box-shadow: 0 0 8px rgba(37, 99, 235, 0.4);
}
.sectionTitleIndependent {
.sectionDot {
background: #fb923c;
box-shadow: 0 0 8px rgba(251, 146, 60, 0.4);
}
}
.sectionDotIndependent {
background: #fb923c;
box-shadow: 0 0 8px rgba(251, 146, 60, 0.4);
}
.planList {
display: flex;
flex-direction: column;
gap: 16px;
}
.planCard {
background: #fff;
border-radius: 16px;
padding: 20px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
border: 1px solid #e2e8f0;
transition: all 0.3s;
position: relative;
overflow: hidden;
&:hover {
box-shadow: 0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04);
}
&::before {
content: "";
position: absolute;
top: 0;
right: 0;
width: 128px;
height: 128px;
background: linear-gradient(to bottom right, rgba(239, 246, 255, 0.5), transparent);
border-radius: 0 0 0 80px;
opacity: 0;
transition: opacity 0.5s;
pointer-events: none;
}
&:hover::before {
opacity: 1;
}
}
.cardHeader {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20px;
position: relative;
z-index: 10;
}
.cardTitleSection {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
max-width: 70%;
}
.cardTitle {
font-size: 16px;
font-weight: 700;
color: #1e293b;
margin: 0;
}
.statusBadge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
border: 1px solid transparent;
}
.statusRunning {
background: #dcfce7;
color: #166534;
border-color: rgba(22, 101, 52, 0.1);
}
.statusStopped {
background: #f1f5f9;
color: #64748b;
border-color: rgba(100, 116, 139, 0.1);
}
.cardActions {
display: flex;
align-items: center;
gap: 8px;
}
.statusSwitch {
--checked-color: #2563eb;
}
.moreButton {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
border: none;
background: transparent;
color: #64748b;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #f8fafc;
}
&:active {
background: #e2e8f0;
}
}
.cardStats {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
margin-bottom: 20px;
padding: 0 4px;
position: relative;
z-index: 10;
}
.statItem {
flex: 1;
}
.statHeader {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
color: #64748b;
}
.statIcon {
font-size: 16px;
}
.statLabel {
font-size: 12px;
font-weight: 500;
margin: 0;
color: #64748b;
}
.statValue {
display: flex;
align-items: baseline;
gap: 8px;
}
.statNumber {
font-size: 24px;
font-weight: 700;
color: #1e293b;
font-family: "Noto Sans SC", sans-serif;
}
.statDivider {
width: 1px;
height: 40px;
background: #e2e8f0;
}
.cardDetails {
border-top: 1px solid #f1f5f9;
padding-top: 12px;
display: flex;
flex-direction: column;
gap: 10px;
position: relative;
z-index: 10;
}
.detailItem {
display: flex;
align-items: center;
font-size: 13px;
color: #64748b;
}
.detailIcon {
font-size: 16px;
margin-right: 8px;
color: #94a3b8;
}
.detailLabel {
flex-shrink: 0;
}
.detailValue {
margin-left: auto;
font-weight: 600;
color: #1e293b;
}
.detailTime {
margin-left: auto;
font-weight: 500;
font-family: monospace;
color: #64748b;
font-size: 11px;
}
.emptyState {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
}
.emptyIcon {
font-size: 48px;
margin-bottom: 16px;
}
.emptyText {
font-size: 16px;
color: #64748b;
margin-bottom: 24px;
}
.emptyButton {
min-width: 140px;
}

View File

@@ -0,0 +1,344 @@
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Button, Toast, Switch, Dialog } from "antd-mobile";
import { Dropdown, Menu } from "antd";
import { MoreOutlined, EditOutlined, DeleteOutlined, PlusOutlined, EyeOutlined } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import NavCommon from "@/components/NavCommon";
import { getGroupCreateList, toggleGroupCreateStatus, deleteGroupCreate } from "../form/api";
import style from "./index.module.scss";
interface GroupCreatePlan {
id: string;
name: string;
planType: number; // 0-全局计划, 1-独立计划
status: number; // 1-启用, 0-禁用
groupNameTemplate?: string;
groupSizeMax?: number;
groupSizeMin?: number;
updateTime?: string;
createTime?: string;
createdGroupsCount?: number; // 已建群数
totalMembersCount?: number; // 总人数
[key: string]: any;
}
const GroupCreateList: React.FC = () => {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [plans, setPlans] = useState<GroupCreatePlan[]>([]);
const [menuLoadingId, setMenuLoadingId] = useState<string | null>(null);
// 获取列表数据
const fetchList = async () => {
setLoading(true);
try {
const res = await getGroupCreateList({ type: 4 });
const list = res?.list || res?.data?.list || res?.data || [];
const normalized: GroupCreatePlan[] = (list as any[]).map((item: any) => {
const stats = item.config?.stats || {};
return {
id: String(item.id),
name: item.name || "",
planType: item.config?.planType ?? item.planType ?? 1,
status: item.status === 1 ? 1 : 0,
groupNameTemplate: item.config?.groupNameTemplate || item.groupNameTemplate || item.groupName || "",
groupSizeMax: item.config?.groupSizeMax || item.groupSizeMax || 38,
groupSizeMin: item.config?.groupSizeMin || item.groupSizeMin || 3,
updateTime: item.updateTime || item.createTime || "",
createTime: item.createTime || "",
createdGroupsCount: stats.createdGroupsCount ?? item.createdGroupsCount ?? 0,
totalMembersCount: stats.totalMembersCount ?? item.totalMembersCount ?? 0,
};
});
setPlans(normalized);
} catch (e: any) {
Toast.show({ content: e?.message || "获取列表失败", position: "top" });
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchList();
}, []);
// 切换状态
const handleStatusToggle = async (plan: GroupCreatePlan) => {
try {
const newStatus = plan.status === 1 ? 0 : 1;
await toggleGroupCreateStatus({
id: plan.id,
status: newStatus,
});
Toast.show({ content: newStatus === 1 ? "已启用" : "已停止", position: "top" });
// 直接更新本地状态
setPlans(prev =>
prev.map(p => (p.id === plan.id ? { ...p, status: newStatus } : p))
);
} catch (e: any) {
Toast.show({ content: e?.message || "操作失败", position: "top" });
}
};
// 删除计划
const handleDelete = async (plan: GroupCreatePlan) => {
const result = await Dialog.confirm({
content: "确定要删除该计划吗?",
confirmText: "删除",
cancelText: "取消",
});
if (!result) return;
setMenuLoadingId(plan.id);
try {
await deleteGroupCreate(plan.id);
Toast.show({ content: "删除成功", position: "top" });
fetchList();
} catch (e: any) {
Toast.show({ content: e?.message || "删除失败", position: "top" });
} finally {
setMenuLoadingId(null);
}
};
// 菜单点击
const handleMenuClick = ({ key }: { key: string }, plan: GroupCreatePlan) => {
if (key === "detail") {
navigate(`/workspace/group-create/${plan.id}`);
} else if (key === "edit") {
navigate(`/workspace/group-create/${plan.id}/edit`);
} else if (key === "delete") {
handleDelete(plan);
}
};
// 创建新计划
const handleCreate = () => {
navigate("/workspace/group-create/new");
};
// 分隔全局计划和独立计划
const globalPlans = plans.filter(p => p.planType === 0);
const independentPlans = plans.filter(p => p.planType === 1);
return (
<Layout
header={
<NavCommon
title="自动建群"
right={
<div style={{ marginRight: "-16px" }}>
<Button
size="small"
color="primary"
onClick={handleCreate}
>
<PlusOutlined />
</Button>
</div>
}
/>
}
loading={loading}
>
<div className={style.container}>
{/* 全局计划提示 */}
{globalPlans.length > 0 && (
<div className={style.infoBox}>
<span className={style.infoIcon}></span>
<p className={style.infoText}>
</p>
</div>
)}
{/* 全局建群计划 */}
{globalPlans.length > 0 && (
<section className={style.section}>
<h2 className={style.sectionTitle}>
<div className={style.sectionDot}></div>
</h2>
{globalPlans.map(plan => (
<PlanCard
key={plan.id}
plan={plan}
onStatusToggle={handleStatusToggle}
onMenuClick={handleMenuClick}
menuLoading={menuLoadingId === plan.id}
/>
))}
</section>
)}
{/* 独立建群计划 */}
{independentPlans.length > 0 && (
<section className={style.section}>
<h2 className={`${style.sectionTitle} ${style.sectionTitleIndependent}`}>
<div className={`${style.sectionDot} ${style.sectionDotIndependent}`}></div>
</h2>
<div className={style.planList}>
{independentPlans.map(plan => (
<PlanCard
key={plan.id}
plan={plan}
onStatusToggle={handleStatusToggle}
onMenuClick={handleMenuClick}
menuLoading={menuLoadingId === plan.id}
/>
))}
</div>
</section>
)}
{/* 空状态 */}
{plans.length === 0 && !loading && (
<div className={style.emptyState}>
<div className={style.emptyIcon}>📋</div>
<div className={style.emptyText}></div>
<Button color="primary" onClick={handleCreate} className={style.emptyButton}>
</Button>
</div>
)}
</div>
</Layout>
);
};
// 计划卡片组件
interface PlanCardProps {
plan: GroupCreatePlan;
onStatusToggle: (plan: GroupCreatePlan) => void;
onMenuClick: (params: { key: string }, plan: GroupCreatePlan) => void;
menuLoading: boolean;
}
const PlanCard: React.FC<PlanCardProps> = ({
plan,
onStatusToggle,
onMenuClick,
menuLoading,
}) => {
const isRunning = plan.status === 1;
return (
<div className={style.planCard}>
{/* 卡片头部 */}
<div className={style.cardHeader}>
<div className={style.cardTitleSection}>
<h3 className={style.cardTitle}>{plan.name}</h3>
<span className={`${style.statusBadge} ${isRunning ? style.statusRunning : style.statusStopped}`}>
{isRunning ? "运行中" : "已停止"}
</span>
</div>
<div className={style.cardActions}>
<Switch
checked={isRunning}
onChange={() => onStatusToggle(plan)}
className={style.statusSwitch}
/>
<Dropdown
menu={{
items: [
{
key: "detail",
icon: <EyeOutlined />,
label: "计划详情",
disabled: menuLoading,
},
{
key: "edit",
icon: <EditOutlined />,
label: "编辑",
disabled: menuLoading,
},
{
key: "delete",
icon: <DeleteOutlined />,
label: "删除",
disabled: menuLoading,
danger: true,
},
],
onClick: (params) => onMenuClick(params, plan),
}}
trigger={["click"]}
placement="bottomRight"
disabled={menuLoading}
>
<button className={style.moreButton}>
<MoreOutlined />
</button>
</Dropdown>
</div>
</div>
{/* 统计数据 */}
<div className={style.cardStats}>
<div className={style.statItem}>
<div className={style.statHeader}>
<span className={style.statIcon}>👥</span>
<p className={style.statLabel}></p>
</div>
<div className={style.statValue}>
<span className={style.statNumber}>{plan.createdGroupsCount || 0}</span>
</div>
</div>
<div className={style.statDivider}></div>
<div className={style.statItem}>
<div className={style.statHeader}>
<span className={style.statIcon}>👥</span>
<p className={style.statLabel}></p>
</div>
<div className={style.statValue}>
<span className={style.statNumber}>{plan.totalMembersCount || 0}</span>
</div>
</div>
</div>
{/* 详细信息 */}
<div className={style.cardDetails}>
<div className={style.detailItem}>
<span className={style.detailIcon}>📝</span>
<span className={style.detailLabel}></span>
<span className={style.detailValue}>{plan.groupNameTemplate || "-"}</span>
</div>
<div className={style.detailItem}>
<span className={style.detailIcon}></span>
<span className={style.detailLabel}></span>
<span className={style.detailValue}>
{plan.groupSizeMax || 38}/
</span>
</div>
<div className={style.detailItem}>
<span className={style.detailIcon}>🕐</span>
<span className={style.detailLabel}></span>
<span className={style.detailTime}>
{plan.updateTime
? new Date(plan.updateTime).toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
}).replace(/\//g, "-")
: plan.createTime
? new Date(plan.createTime).toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
}).replace(/\//g, "-")
: "-"}
</span>
</div>
</div>
</div>
);
};
export default GroupCreateList;

View File

@@ -62,6 +62,17 @@ const Workspace: React.FC = () => {
path: "/workspace/auto-group",
bgColor: "#f6ffed",
},
{
id: "group-create",
name: "自动建群(新版)",
description: "智能拉好友建群",
icon: (
<TeamOutlined className={styles.icon} style={{ color: "#52c41a" }} />
),
path: "/workspace/group-create",
bgColor: "#f6ffed",
isNew: true,
},
{
id: "traffic-distribution",
name: "流量分发",

View File

@@ -52,13 +52,13 @@ const PoolListModal: React.FC<PoolListModalProps> = ({
}
return {
id: pool.id || pool.poolId,
name: pool.name || pool.poolName || `流量池${pool.id}`,
description: pool.description || pool.desc || "",
id: pool.id || pool.poolId,
name: pool.name || pool.poolName || `流量池${pool.id}`,
description: pool.description || pool.desc || "",
userCount: pool.num || pool.userCount || pool.count || 0,
tags: pool.tags || [],
tags: pool.tags || [],
createdAt: createdAt,
deviceIds: pool.deviceIds || [],
deviceIds: pool.deviceIds || [],
};
});

View File

@@ -40,8 +40,12 @@ export const routeGroups = {
"/workspace/auto-like/:id/edit",
"/workspace/auto-group",
"/workspace/auto-group/:id",
"/workspace/group-create",
"/workspace/group-create/new",
"/workspace/group-create/:id",
"/workspace/group-create/:id/groups",
"/workspace/group-create/:id/groups/:groupId",
"/workspace/group-create/:id/edit",
"/workspace/group-push",
"/workspace/group-push/new",
"/workspace/group-push/:id",

View File

@@ -5,7 +5,11 @@ import RecordAutoLike from "@/pages/mobile/workspace/auto-like/record";
import AutoGroupList from "@/pages/mobile/workspace/auto-group/list";
import AutoGroupDetail from "@/pages/mobile/workspace/auto-group/detail";
import AutoGroupForm from "@/pages/mobile/workspace/auto-group/form";
import GroupCreateList from "@/pages/mobile/workspace/group-create/list";
import GroupCreateForm from "@/pages/mobile/workspace/group-create/form";
import GroupCreateDetail from "@/pages/mobile/workspace/group-create/detail";
import GroupListPage from "@/pages/mobile/workspace/group-create/detail/groups-list";
import GroupDetailPage from "@/pages/mobile/workspace/group-create/detail/group-detail";
import GroupPush from "@/pages/mobile/workspace/group-push/list";
import FormGroupPush from "@/pages/mobile/workspace/group-push/form";
import DetailGroupPush from "@/pages/mobile/workspace/group-push/detail";
@@ -76,6 +80,11 @@ const workspaceRoutes = [
auth: true,
},
// 自动建群(新版)
{
path: "/workspace/group-create",
element: <GroupCreateList />,
auth: true,
},
{
path: "/workspace/group-create/new",
element: <GroupCreateForm />,
@@ -83,6 +92,21 @@ const workspaceRoutes = [
},
{
path: "/workspace/group-create/:id",
element: <GroupCreateDetail />,
auth: true,
},
{
path: "/workspace/group-create/:id/groups",
element: <GroupListPage />,
auth: true,
},
{
path: "/workspace/group-create/:id/groups/:groupId",
element: <GroupDetailPage />,
auth: true,
},
{
path: "/workspace/group-create/:id/edit",
element: <GroupCreateForm />,
auth: true,
},

View File

@@ -1020,7 +1020,6 @@ class WebSocketController extends BaseController
// "wechatFriendIds" => $data['wechatFriendIds']
"wechatFriendIds" => [17453051,17453058]
];
$message = $this->sendMessage($params,false);
return json_encode(['code' => 200, 'msg' => '群聊创建成功', 'data' => $message]);
} catch (\Exception $e) {
@@ -1116,6 +1115,7 @@ class WebSocketController extends BaseController
];
}
//chatroomOperateType 4退群 6群公告 5群名称
$params = [
"chatroomOperateType" => !empty($data['chatroomName']) ? 6 : 5,
"cmdType" => "CmdChatroomOperate",
@@ -1125,7 +1125,7 @@ class WebSocketController extends BaseController
"wechatChatroomId" => $data['wechatChatroomId']
];
$message = $this->sendMessage($params,false);
return json_encode(['code' => 200, 'msg' => '群聊创建成功', 'data' => $message]);
return json_encode(['code' => 200, 'msg' => '修改群信息成功', 'data' => $message]);
} catch (\Exception $e) {
Log::error('修改群信息异常: ' . $e->getMessage());
return json_encode(['code' => 500, 'msg' => '修改群信息失败: ' . $e->getMessage()]);

View File

@@ -39,7 +39,7 @@ class WechatChatroomController extends BaseController
'groupId' => $data['groupId'] ?? '',
'wechatChatroomId' => $data['wechatChatroomId'] ?? '',
'memberKeyword' => $data['memberKeyword'] ?? '',
'pageIndex' => $data['pageIndex'] ?? 1,
'pageIndex' => $data['pageIndex'] ?? 0,
'pageSize' => $data['pageSize'] ?? 20
];

View File

@@ -93,27 +93,32 @@ Route::group('v1/', function () {
// 工作台相关
Route::group('workbench', function () {
Route::post('create', 'app\cunkebao\controller\WorkbenchController@create'); // 创建工作台
Route::get('list', 'app\cunkebao\controller\WorkbenchController@getList'); // 获取工作台列表
Route::post('update-status', 'app\cunkebao\controller\WorkbenchController@updateStatus'); // 更新工作台状态
Route::delete('delete', 'app\cunkebao\controller\WorkbenchController@delete'); // 删除工作台
Route::post('copy', 'app\cunkebao\controller\WorkbenchController@copy'); // 拷贝工作台
Route::get('detail', 'app\cunkebao\controller\WorkbenchController@detail'); // 获取工作台详情
Route::post('update', 'app\cunkebao\controller\WorkbenchController@update'); // 更新工作台
Route::get('like-records', 'app\cunkebao\controller\WorkbenchController@getLikeRecords'); // 获取点赞记录列表
Route::get('moments-records', 'app\cunkebao\controller\WorkbenchController@getMomentsRecords'); // 获取朋友圈发布记录列表
Route::get('device-labels', 'app\cunkebao\controller\WorkbenchController@getDeviceLabels'); // 获取设备微信好友标签统计
Route::get('group-list', 'app\cunkebao\controller\WorkbenchController@getGroupList'); // 获取群列表
Route::get('account-list', 'app\cunkebao\controller\WorkbenchController@getAccountList'); // 获取账号列表
Route::get('transfer-friends', 'app\cunkebao\controller\WorkbenchController@getTrafficList'); // 获取账号列表
Route::get('import-contact', 'app\cunkebao\controller\WorkbenchController@getImportContact'); // 获取通讯录导入记录列表
Route::post('create', 'app\cunkebao\controller\workbench\WorkbenchController@create'); // 创建工作台
Route::get('list', 'app\cunkebao\controller\workbench\WorkbenchController@getList'); // 获取工作台列表
Route::post('update-status', 'app\cunkebao\controller\workbench\WorkbenchController@updateStatus'); // 更新工作台状态
Route::delete('delete', 'app\cunkebao\controller\workbench\WorkbenchController@delete'); // 删除工作台
Route::post('copy', 'app\cunkebao\controller\workbench\WorkbenchController@copy'); // 拷贝工作台
Route::get('detail', 'app\cunkebao\controller\workbench\WorkbenchController@detail'); // 获取工作台详情
Route::post('update', 'app\cunkebao\controller\workbench\WorkbenchController@update'); // 更新工作台
Route::get('like-records', 'app\cunkebao\controller\workbench\WorkbenchController@getLikeRecords'); // 获取点赞记录列表
Route::get('moments-records', 'app\cunkebao\controller\workbench\WorkbenchController@getMomentsRecords'); // 获取朋友圈发布记录列表
Route::get('device-labels', 'app\cunkebao\controller\workbench\WorkbenchController@getDeviceLabels'); // 获取设备微信好友标签统计
Route::get('group-list', 'app\cunkebao\controller\workbench\WorkbenchController@getGroupList'); // 获取群列表
Route::get('created-groups-list', 'app\cunkebao\controller\workbench\WorkbenchController@getCreatedGroupsList'); // 获取已创建的群列表(自动建群)
Route::get('created-group-detail', 'app\cunkebao\controller\workbench\WorkbenchController@getCreatedGroupDetail'); // 获取已创建群的详情(自动建群)
Route::post('sync-group-info', 'app\cunkebao\controller\workbench\WorkbenchController@syncGroupInfo'); // 同步群最新信息(包括群成员)
Route::post('modify-group-info', 'app\cunkebao\controller\workbench\WorkbenchController@modifyGroupInfo'); // 修改群名称、群公告
Route::post('quit-group', 'app\cunkebao\controller\workbench\WorkbenchController@quitGroup'); // 退群(自动建群)
Route::get('account-list', 'app\cunkebao\controller\workbench\WorkbenchController@getAccountList'); // 获取账号列表
Route::get('transfer-friends', 'app\cunkebao\controller\workbench\WorkbenchController@getTrafficList'); // 获取账号列表
Route::get('import-contact', 'app\cunkebao\controller\workbench\WorkbenchController@getImportContact'); // 获取通讯录导入记录列表
Route::get('getJdSocialMedia', 'app\cunkebao\controller\WorkbenchController@getJdSocialMedia'); // 获取京东联盟导购媒体
Route::get('getJdPromotionSite', 'app\cunkebao\controller\WorkbenchController@getJdPromotionSite'); // 获取京东联盟广告位
Route::get('changeLink', 'app\cunkebao\controller\WorkbenchController@changeLink'); // 获取京东联盟广告位
Route::get('getJdSocialMedia', 'app\cunkebao\controller\workbench\WorkbenchController@getJdSocialMedia'); // 获取京东联盟导购媒体
Route::get('getJdPromotionSite', 'app\cunkebao\controller\workbench\WorkbenchController@getJdPromotionSite'); // 获取京东联盟广告位
Route::get('changeLink', 'app\cunkebao\controller\workbench\WorkbenchController@changeLink'); // 获取京东联盟广告位
Route::get('group-push-stats', 'app\cunkebao\controller\WorkbenchController@getGroupPushStats'); // 获取群发统计数据
Route::get('group-push-history', 'app\cunkebao\controller\WorkbenchController@getGroupPushHistory'); // 获取推送历史记录列表
Route::get('group-push-stats', 'app\cunkebao\controller\workbench\WorkbenchController@getGroupPushStats'); // 获取群发统计数据
Route::get('group-push-history', 'app\cunkebao\controller\workbench\WorkbenchController@getGroupPushHistory'); // 获取推送历史记录列表
});
// 内容库相关

View File

@@ -643,7 +643,7 @@ class ContentLibraryController extends Controller
// 查询数据
$list = ContentItem::where($where)
->field('id,libraryId,type,title,content,contentAi,contentType,resUrls,urls,friendId,wechatId,wechatChatroomId,createTime,createMomentTime,createMessageTime,coverImage,ossUrls')
->order('createTime DESC,createMomentTime DESC')
->order('createMomentTime DESC,createMessageTime DESC,createTime DESC')
->page($page, $limit)
->select();

View File

@@ -782,8 +782,8 @@ class ChannelController extends BaseController
if (!empty($channelIds)) {
// 构建提现查询条件
$withdrawalWhere = [
['companyId', '=', $companyId],
['channelId', 'in', $channelIds]
['companyId', '=', $companyId],
['channelId', 'in', $channelIds]
];
// 如果不是管理员,只能查看自己创建的提现申请
@@ -817,7 +817,7 @@ class ChannelController extends BaseController
// totalRevenue 不包括驳回rejected状态的金额
if ($status !== DistributionWithdrawal::STATUS_REJECTED) {
$withdrawalStats[$cid]['totalRevenue'] += $amount;
$withdrawalStats[$cid]['totalRevenue'] += $amount;
}
if ($status === DistributionWithdrawal::STATUS_PAID) {

View File

@@ -0,0 +1,108 @@
<?php
namespace app\cunkebao\controller\workbench;
use think\Controller;
use think\Db;
/**
* 工作台 - 自动点赞相关功能
*/
class WorkbenchAutoLikeController extends Controller
{
/**
* 获取点赞记录列表
* @return \think\response\Json
*/
public function getLikeRecords()
{
$page = $this->request->param('page', 1);
$limit = $this->request->param('limit', 10);
$workbenchId = $this->request->param('workbenchId', 0);
$where = [
['wali.workbenchId', '=', $workbenchId]
];
// 查询点赞记录
$list = Db::name('workbench_auto_like_item')->alias('wali')
->join(['s2_wechat_moments' => 'wm'], 'wali.snsId = wm.snsId')
->field([
'wali.id',
'wali.workbenchId',
'wali.momentsId',
'wali.snsId',
'wali.wechatAccountId',
'wali.wechatFriendId',
'wali.createTime as likeTime',
'wm.content',
'wm.resUrls',
'wm.createTime as momentTime',
'wm.userName',
])
->where($where)
->order('wali.createTime', 'desc')
->group('wali.id')
->page($page, $limit)
->select();
// 处理数据
foreach ($list as &$item) {
//处理用户信息
$friend = Db::table('s2_wechat_friend')
->where(['id' => $item['wechatFriendId']])
->field('nickName,avatar')
->find();
if (!empty($friend)) {
$item['friendName'] = $friend['nickName'];
$item['friendAvatar'] = $friend['avatar'];
} else {
$item['friendName'] = '';
$item['friendAvatar'] = '';
}
//处理客服
$friend = Db::table('s2_wechat_account')
->where(['id' => $item['wechatAccountId']])
->field('nickName,avatar')
->find();
if (!empty($friend)) {
$item['operatorName'] = $friend['nickName'];
$item['operatorAvatar'] = $friend['avatar'];
} else {
$item['operatorName'] = '';
$item['operatorAvatar'] = '';
}
// 处理时间格式
$item['likeTime'] = date('Y-m-d H:i:s', $item['likeTime']);
$item['momentTime'] = !empty($item['momentTime']) ? date('Y-m-d H:i:s', $item['momentTime']) : '';
// 处理资源链接
if (!empty($item['resUrls'])) {
$item['resUrls'] = json_decode($item['resUrls'], true);
} else {
$item['resUrls'] = [];
}
}
// 获取总记录数
$total = Db::name('workbench_auto_like_item')->alias('wali')
->where($where)
->count();
return json([
'code' => 200,
'msg' => '获取成功',
'data' => [
'list' => $list,
'total' => $total,
'page' => $page,
'limit' => $limit
]
]);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,313 @@
<?php
namespace app\cunkebao\controller\workbench;
use think\Controller;
use think\Db;
use think\facade\Env;
/**
* 工作台 - 辅助功能
*/
class WorkbenchHelperController extends Controller
{
/**
* 获取所有微信好友标签及数量统计
* @return \think\response\Json
*/
public function getDeviceLabels()
{
$deviceIds = $this->request->param('deviceIds', '');
$companyId = $this->request->userInfo['companyId'];
$page = $this->request->param('page', 1);
$limit = $this->request->param('limit', 10);
$keyword = $this->request->param('keyword', '');
$where = [
['wc.companyId', '=', $companyId],
];
if (!empty($deviceIds)) {
$deviceIds = explode(',', $deviceIds);
$where[] = ['dwl.deviceId', 'in', $deviceIds];
}
$wechatAccounts = Db::name('wechat_customer')->alias('wc')
->join('device_wechat_login dwl', 'dwl.wechatId = wc.wechatId AND dwl.companyId = wc.companyId AND dwl.alive = 1')
->join(['s2_wechat_account' => 'wa'], 'wa.wechatId = wc.wechatId')
->where($where)
->field('wa.id,wa.wechatId,wa.nickName,wa.labels')
->select();
$labels = [];
$wechatIds = [];
foreach ($wechatAccounts as $account) {
$labelArr = json_decode($account['labels'], true);
if (is_array($labelArr)) {
foreach ($labelArr as $label) {
if ($label !== '' && $label !== null) {
$labels[] = $label;
}
}
}
$wechatIds[] = $account['wechatId'];
}
// 去重(只保留一个)
$labels = array_values(array_unique($labels));
$wechatIds = array_unique($wechatIds);
// 搜索过滤
if (!empty($keyword)) {
$labels = array_filter($labels, function ($label) use ($keyword) {
return mb_stripos($label, $keyword) !== false;
});
$labels = array_values($labels); // 重新索引数组
}
// 分页处理
$labels2 = array_slice($labels, ($page - 1) * $limit, $limit);
// 统计数量
$newLabel = [];
foreach ($labels2 as $label) {
$friendCount = Db::table('s2_wechat_friend')
->whereIn('ownerWechatId', $wechatIds)
->where('labels', 'like', '%"' . $label . '"%')
->count();
$newLabel[] = [
'label' => $label,
'count' => $friendCount
];
}
// 返回结果
return json([
'code' => 200,
'msg' => '获取成功',
'data' => [
'list' => $newLabel,
'total' => count($labels),
]
]);
}
/**
* 获取群列表
* @return \think\response\Json
*/
public function getGroupList()
{
$page = $this->request->param('page', 1);
$limit = $this->request->param('limit', 10);
$keyword = $this->request->param('keyword', '');
$where = [
['wg.deleteTime', '=', 0],
['wg.companyId', '=', $this->request->userInfo['companyId']],
];
if (!empty($keyword)) {
$where[] = ['wg.name', 'like', '%' . $keyword . '%'];
}
$query = Db::name('wechat_group')->alias('wg')
->join('wechat_account wa', 'wa.wechatId = wg.ownerWechatId')
->where($where);
$total = $query->count();
$list = $query->order('wg.id', 'desc')
->field('wg.id,wg.name as groupName,wg.ownerWechatId,wa.nickName,wg.createTime,wa.avatar,wa.alias,wg.avatar as groupAvatar')
->page($page, $limit)
->select();
// 优化:格式化时间,头像兜底
$defaultGroupAvatar = '';
$defaultAvatar = '';
foreach ($list as &$item) {
$item['createTime'] = $item['createTime'] ? date('Y-m-d H:i:s', $item['createTime']) : '';
$item['groupAvatar'] = $item['groupAvatar'] ?: $defaultGroupAvatar;
$item['avatar'] = $item['avatar'] ?: $defaultAvatar;
}
return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]);
}
/**
* 获取流量池列表
* @return \think\response\Json
*/
public function getTrafficPoolList()
{
$page = $this->request->param('page', 1);
$limit = $this->request->param('limit', 10);
$keyword = $this->request->param('keyword', '');
$companyId = $this->request->userInfo['companyId'];
$baseQuery = Db::name('traffic_source_package')->alias('tsp')
->where('tsp.isDel', 0)
->whereIn('tsp.companyId', [$companyId, 0]);
if (!empty($keyword)) {
$baseQuery->whereLike('tsp.name', '%' . $keyword . '%');
}
$total = (clone $baseQuery)->count();
$list = $baseQuery
->leftJoin('traffic_source_package_item tspi', 'tspi.packageId = tsp.id and tspi.isDel = 0')
->field('tsp.id,tsp.name,tsp.description,tsp.pic,tsp.companyId,COUNT(tspi.id) as itemCount,max(tspi.createTime) as latestImportTime')
->group('tsp.id')
->order('tsp.id', 'desc')
->page($page, $limit)
->select();
foreach ($list as &$item) {
$item['latestImportTime'] = !empty($item['latestImportTime']) ? date('Y-m-d H:i:s', $item['latestImportTime']) : '';
}
unset($item);
return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]);
}
/**
* 获取账号列表
* @return \think\response\Json
*/
public function getAccountList()
{
$companyId = $this->request->userInfo['companyId'];
$page = $this->request->param('page', 1);
$limit = $this->request->param('limit', 10);
$query = Db::table('s2_company_account')
->alias('a')
->where(['a.departmentId' => $companyId, 'a.status' => 0])
->whereNotLike('a.userName', '%_offline%')
->whereNotLike('a.userName', '%_delete%');
$total = $query->count();
$list = $query->field('a.id,a.userName,a.realName,a.nickname,a.memo')
->page($page, $limit)
->select();
return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]);
}
/**
* 获取京东联盟导购媒体
* @return \think\response\Json
*/
public function getJdSocialMedia()
{
$data = Db::name('jd_social_media')->order('id DESC')->select();
return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]);
}
/**
* 获取京东联盟广告位
* @return \think\response\Json
*/
public function getJdPromotionSite()
{
$id = $this->request->param('id', '');
if (empty($id)) {
return json(['code' => 500, 'msg' => '参数缺失']);
}
$data = Db::name('jd_promotion_site')->where('jdSocialMediaId', $id)->order('id DESC')->select();
return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]);
}
/**
* 京东转链-京推推
* @param string $content
* @param string $positionid
* @return string
*/
public function changeLink($content = '', $positionid = '')
{
$unionId = Env::get('jd.unionId', '');
$jttAppId = Env::get('jd.jttAppId', '');
$appKey = Env::get('jd.appKey', '');
$apiUrl = Env::get('jd.apiUrl', '');
$content = !empty($content) ? $content : $this->request->param('content', '');
$positionid = !empty($positionid) ? $positionid : $this->request->param('positionid', '');
if (empty($content)) {
return json_encode(['code' => 500, 'msg' => '转链的内容为空']);
}
// 验证是否包含链接
if (!$this->containsLink($content)) {
return json_encode(['code' => 500, 'msg' => '内容中未检测到有效链接']);
}
if (empty($unionId) || empty($jttAppId) || empty($appKey) || empty($apiUrl)) {
return json_encode(['code' => 500, 'msg' => '参数缺失']);
}
$params = [
'unionid' => $unionId,
'content' => $content,
'appid' => $jttAppId,
'appkey' => $appKey,
'v' => 'v2'
];
if (!empty($positionid)) {
$params['positionid'] = $positionid;
}
$res = requestCurl($apiUrl, $params, 'GET', [], 'json');
$res = json_decode($res, true);
if (empty($res)) {
return json_encode(['code' => 500, 'msg' => '未知错误']);
}
$result = $res['result'];
if ($res['return'] == 0) {
return json_encode(['code' => 200, 'data' => $result['chain_content'], 'msg' => $result['msg']]);
} else {
return json_encode(['code' => 500, 'msg' => $result['msg']]);
}
}
/**
* 验证内容是否包含链接
* @param string $content 要检测的内容
* @return bool
*/
private function containsLink($content)
{
// 定义各种链接的正则表达式模式
$patterns = [
// HTTP/HTTPS链接
'/https?:\/\/[^\s]+/i',
// 京东商品链接
'/item\.jd\.com\/\d+/i',
// 京东短链接
'/u\.jd\.com\/[a-zA-Z0-9]+/i',
// 淘宝商品链接
'/item\.taobao\.com\/item\.htm\?id=\d+/i',
// 天猫商品链接
'/detail\.tmall\.com\/item\.htm\?id=\d+/i',
// 淘宝短链接
'/m\.tb\.cn\/[a-zA-Z0-9]+/i',
// 拼多多链接
'/mobile\.yangkeduo\.com\/goods\.html\?goods_id=\d+/i',
// 苏宁易购链接
'/product\.suning\.com\/\d+\/\d+\.html/i',
// 通用域名模式(包含常见电商域名)
'/(?:jd|taobao|tmall|yangkeduo|suning|amazon|dangdang)\.com[^\s]*/i',
// 通用短链接模式
'/[a-zA-Z0-9-]+\.[a-zA-Z]{2,}\/[a-zA-Z0-9\-._~:\/?#\[\]@!$&\'()*+,;=]+/i'
];
// 遍历所有模式进行匹配
foreach ($patterns as $pattern) {
if (preg_match($pattern, $content)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace app\cunkebao\controller\workbench;
use think\Controller;
use think\Db;
/**
* 工作台 - 联系人导入相关功能
*/
class WorkbenchImportContactController extends Controller
{
/**
* 获取通讯录导入记录列表
* @return \think\response\Json
*/
public function getImportContact()
{
$page = $this->request->param('page', 1);
$limit = $this->request->param('limit', 10);
$workbenchId = $this->request->param('workbenchId', 0);
$where = [
['wici.workbenchId', '=', $workbenchId]
];
// 查询发布记录
$list = Db::name('workbench_import_contact_item')->alias('wici')
->join('traffic_pool tp', 'tp.id = wici.poolId', 'left')
->join('traffic_source tc', 'tc.identifier = tp.identifier', 'left')
->join('wechat_account wa', 'wa.wechatId = tp.wechatId', 'left')
->field([
'wici.id',
'wici.workbenchId',
'wici.createTime',
'tp.identifier',
'tp.mobile',
'tp.wechatId',
'tc.name',
'wa.nickName',
'wa.avatar',
'wa.alias',
])
->where($where)
->order('tc.name DESC,wici.createTime DESC')
->group('tp.identifier')
->page($page, $limit)
->select();
foreach ($list as &$item) {
$item['createTime'] = date('Y-m-d H:i:s', $item['createTime']);
}
// 获取总记录数
$total = Db::name('workbench_import_contact_item')->alias('wici')
->where($where)
->count();
return json([
'code' => 200,
'msg' => '获取成功',
'data' => [
'list' => $list,
'total' => $total,
]
]);
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace app\cunkebao\controller\workbench;
use think\Controller;
use think\Db;
/**
* 工作台 - 朋友圈同步相关功能
*/
class WorkbenchMomentsController extends Controller
{
/**
* 获取朋友圈发布记录列表
* @return \think\response\Json
*/
public function getMomentsRecords()
{
$page = $this->request->param('page', 1);
$limit = $this->request->param('limit', 10);
$workbenchId = $this->request->param('workbenchId', 0);
$where = [
['wmsi.workbenchId', '=', $workbenchId]
];
// 查询发布记录
$list = Db::name('workbench_moments_sync_item')->alias('wmsi')
->join('content_item ci', 'ci.id = wmsi.contentId', 'left')
->join(['s2_wechat_account' => 'wa'], 'wa.id = wmsi.wechatAccountId', 'left')
->field([
'wmsi.id',
'wmsi.workbenchId',
'wmsi.createTime as publishTime',
'ci.contentType',
'ci.content',
'ci.resUrls',
'ci.urls',
'wa.nickName as operatorName',
'wa.avatar as operatorAvatar'
])
->where($where)
->order('wmsi.createTime', 'desc')
->page($page, $limit)
->select();
foreach ($list as &$item) {
$item['resUrls'] = json_decode($item['resUrls'], true);
$item['urls'] = json_decode($item['urls'], true);
}
// 获取总记录数
$total = Db::name('workbench_moments_sync_item')->alias('wmsi')
->where($where)
->count();
return json([
'code' => 200,
'msg' => '获取成功',
'data' => [
'list' => $list,
'total' => $total,
'page' => $page,
'limit' => $limit
]
]);
}
/**
* 获取朋友圈发布统计
* @return \think\response\Json
*/
public function getMomentsStats()
{
$workbenchId = $this->request->param('workbenchId', 0);
if (empty($workbenchId)) {
return json(['code' => 400, 'msg' => '参数错误']);
}
// 获取今日数据
$todayStart = strtotime(date('Y-m-d') . ' 00:00:00');
$todayEnd = strtotime(date('Y-m-d') . ' 23:59:59');
$todayStats = Db::name('workbench_moments_sync_item')
->where([
['workbenchId', '=', $workbenchId],
['createTime', 'between', [$todayStart, $todayEnd]]
])
->field([
'COUNT(*) as total',
'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success',
'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed'
])
->find();
// 获取总数据
$totalStats = Db::name('workbench_moments_sync_item')
->where('workbenchId', $workbenchId)
->field([
'COUNT(*) as total',
'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success',
'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed'
])
->find();
return json([
'code' => 200,
'msg' => '获取成功',
'data' => [
'today' => [
'total' => intval($todayStats['total']),
'success' => intval($todayStats['success']),
'failed' => intval($todayStats['failed'])
],
'total' => [
'total' => intval($totalStats['total']),
'success' => intval($totalStats['success']),
'failed' => intval($totalStats['failed'])
]
]
]);
}
}

View File

@@ -0,0 +1,309 @@
<?php
namespace app\cunkebao\controller\workbench;
use think\Controller;
use think\Db;
/**
* 工作台 - 流量分发相关功能
*/
class WorkbenchTrafficController extends Controller
{
/**
* 获取流量分发记录列表
* @return \think\response\Json
*/
public function getTrafficDistributionRecords()
{
$page = $this->request->param('page', 1);
$limit = $this->request->param('limit', 10);
$workbenchId = $this->request->param('workbenchId', 0);
$where = [
['wtdi.workbenchId', '=', $workbenchId]
];
// 查询分发记录
$list = Db::name('workbench_traffic_distribution_item')->alias('wtdi')
->join(['s2_wechat_account' => 'wa'], 'wa.id = wtdi.wechatAccountId', 'left')
->join(['s2_wechat_friend' => 'wf'], 'wf.id = wtdi.wechatFriendId', 'left')
->field([
'wtdi.id',
'wtdi.workbenchId',
'wtdi.wechatAccountId',
'wtdi.wechatFriendId',
'wtdi.createTime as distributeTime',
'wtdi.status',
'wtdi.errorMsg',
'wa.nickName as operatorName',
'wa.avatar as operatorAvatar',
'wf.nickName as friendName',
'wf.avatar as friendAvatar',
'wf.gender',
'wf.province',
'wf.city'
])
->where($where)
->order('wtdi.createTime', 'desc')
->page($page, $limit)
->select();
// 处理数据
foreach ($list as &$item) {
// 处理时间格式
$item['distributeTime'] = date('Y-m-d H:i:s', $item['distributeTime']);
// 处理性别
$genderMap = [
0 => '未知',
1 => '男',
2 => '女'
];
$item['genderText'] = $genderMap[$item['gender']] ?? '未知';
// 处理状态文字
$statusMap = [
0 => '待分发',
1 => '分发成功',
2 => '分发失败'
];
$item['statusText'] = $statusMap[$item['status']] ?? '未知状态';
}
// 获取总记录数
$total = Db::name('workbench_traffic_distribution_item')->alias('wtdi')
->where($where)
->count();
return json([
'code' => 200,
'msg' => '获取成功',
'data' => [
'list' => $list,
'total' => $total,
'page' => $page,
'limit' => $limit
]
]);
}
/**
* 获取流量分发统计
* @return \think\response\Json
*/
public function getTrafficDistributionStats()
{
$workbenchId = $this->request->param('workbenchId', 0);
if (empty($workbenchId)) {
return json(['code' => 400, 'msg' => '参数错误']);
}
// 获取今日数据
$todayStart = strtotime(date('Y-m-d') . ' 00:00:00');
$todayEnd = strtotime(date('Y-m-d') . ' 23:59:59');
$todayStats = Db::name('workbench_traffic_distribution_item')
->where([
['workbenchId', '=', $workbenchId],
['createTime', 'between', [$todayStart, $todayEnd]]
])
->field([
'COUNT(*) as total',
'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success',
'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed'
])
->find();
// 获取总数据
$totalStats = Db::name('workbench_traffic_distribution_item')
->where('workbenchId', $workbenchId)
->field([
'COUNT(*) as total',
'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success',
'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed'
])
->find();
return json([
'code' => 200,
'msg' => '获取成功',
'data' => [
'today' => [
'total' => intval($todayStats['total']),
'success' => intval($todayStats['success']),
'failed' => intval($todayStats['failed'])
],
'total' => [
'total' => intval($totalStats['total']),
'success' => intval($totalStats['success']),
'failed' => intval($totalStats['failed'])
]
]
]);
}
/**
* 获取流量分发详情
* @return \think\response\Json
*/
public function getTrafficDistributionDetail()
{
$id = $this->request->param('id', 0);
if (empty($id)) {
return json(['code' => 400, 'msg' => '参数错误']);
}
$detail = Db::name('workbench_traffic_distribution_item')->alias('wtdi')
->join(['s2_wechat_account' => 'wa'], 'wa.id = wtdi.wechatAccountId', 'left')
->join(['s2_wechat_friend' => 'wf'], 'wf.id = wtdi.wechatFriendId', 'left')
->field([
'wtdi.id',
'wtdi.workbenchId',
'wtdi.wechatAccountId',
'wtdi.wechatFriendId',
'wtdi.createTime as distributeTime',
'wtdi.status',
'wtdi.errorMsg',
'wa.nickName as operatorName',
'wa.avatar as operatorAvatar',
'wf.nickName as friendName',
'wf.avatar as friendAvatar',
'wf.gender',
'wf.province',
'wf.city',
'wf.signature',
'wf.remark'
])
->where('wtdi.id', $id)
->find();
if (empty($detail)) {
return json(['code' => 404, 'msg' => '记录不存在']);
}
// 处理数据
$detail['distributeTime'] = date('Y-m-d H:i:s', $detail['distributeTime']);
// 处理性别
$genderMap = [
0 => '未知',
1 => '男',
2 => '女'
];
$detail['genderText'] = $genderMap[$detail['gender']] ?? '未知';
// 处理状态文字
$statusMap = [
0 => '待分发',
1 => '分发成功',
2 => '分发失败'
];
$detail['statusText'] = $statusMap[$detail['status']] ?? '未知状态';
return json([
'code' => 200,
'msg' => '获取成功',
'data' => $detail
]);
}
/**
* 创建流量分发计划
* @return \think\response\Json
*/
public function createTrafficPlan()
{
$param = $this->request->post();
Db::startTrans();
try {
// 1. 创建主表
$planId = Db::name('ck_workbench')->insertGetId([
'name' => $param['name'],
'type' => 5, // TYPE_TRAFFIC_DISTRIBUTION
'status' => 1,
'autoStart' => $param['autoStart'] ?? 0,
'userId' => $this->request->userInfo['id'],
'companyId' => $this->request->userInfo['companyId'],
'createTime' => time(),
'updateTime' => time()
]);
// 2. 创建扩展表
Db::name('ck_workbench_traffic_config')->insert([
'workbenchId' => $planId,
'distributeType' => $param['distributeType'],
'maxPerDay' => $param['maxPerDay'],
'timeType' => $param['timeType'],
'startTime' => $param['startTime'],
'endTime' => $param['endTime'],
'targets' => json_encode($param['targets'], JSON_UNESCAPED_UNICODE),
'pools' => json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE),
'createTime' => time(),
'updateTime' => time()
]);
Db::commit();
return json(['code' => 200, 'msg' => '创建成功']);
} catch (\Exception $e) {
Db::rollback();
return json(['code' => 500, 'msg' => '创建失败:' . $e->getMessage()]);
}
}
/**
* 获取流量列表
* @return \think\response\Json
*/
public function getTrafficList()
{
$companyId = $this->request->userInfo['companyId'];
$page = $this->request->param('page', 1);
$limit = $this->request->param('limit', 10);
$keyword = $this->request->param('keyword', '');
$workbenchId = $this->request->param('workbenchId', '');
$isRecycle = $this->request->param('isRecycle', '');
if (empty($workbenchId)) {
return json(['code' => 400, 'msg' => '参数错误']);
}
$workbench = Db::name('workbench')->where(['id' => $workbenchId, 'isDel' => 0, 'companyId' => $companyId, 'type' => 5])->find();
if (empty($workbench)) {
return json(['code' => 400, 'msg' => '该任务不存在或已删除']);
}
$query = Db::name('workbench_traffic_config_item')->alias('wtc')
->join(['s2_wechat_friend' => 'wf'], 'wtc.wechatFriendId = wf.id')
->join('users u', 'wtc.wechatAccountId = u.s2_accountId', 'left')
->field([
'wtc.id', 'wtc.isRecycle', 'wtc.isRecycle', 'wtc.createTime','wtc.recycleTime',
'wf.wechatId', 'wf.alias', 'wf.nickname', 'wf.avatar', 'wf.gender', 'wf.phone',
'u.account', 'u.username'
])
->where(['wtc.workbenchId' => $workbenchId])
->order('wtc.id DESC');
if (!empty($keyword)) {
$query->where('wf.wechatId|wf.alias|wf.nickname|wf.phone|u.account|u.username', 'like', '%' . $keyword . '%');
}
if ($isRecycle != '' || $isRecycle != null) {
$query->where('isRecycle',$isRecycle);
}
$total = $query->count();
$list = $query->page($page, $limit)->select();
foreach ($list as &$item) {
$item['createTime'] = date('Y-m-d H:i:s', $item['createTime']);
$item['recycleTime'] = date('Y-m-d H:i:s', $item['recycleTime']);
}
unset($item);
$data = [
'total' => $total,
'list' => $list,
];
return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]);
}
}

View File

@@ -3,12 +3,24 @@ namespace app\job;
use app\chukebao\model\Reply;
use app\chukebao\model\ReplyGroup;
use think\Db;
use think\queue\Job;
class SyncContentJob
{
public function fire(Job $job, $data)
{
$ddd= Db::table('s2_wechat_friend')->where('ownerWechatId','wxid_h7nsh7vxseyn29')->select();
foreach ($ddd as $v) {
$d = Db::table('ck_task_customer')->where('task_id','167')->where('phone',$v['wechatId'])->find();
if (!empty($d) && !in_array($d['status'],[4,5])) {
Db::table('ck_task_customer')->where('id',$d['id'])->update(['status'=>5]);
}
}
exit_data(111);
return true;
/*$ddd= '[{"id":21909,"groupName":"私域运营招聘","sortIndex":240426546,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":8074,"groupName":"存客宝新客户介绍","sortIndex":230,"parentId":0,"replyType":1,"children":[{"id":8081,"groupName":"客户系统上线准备","sortIndex":1,"parentId":8074,"replyType":1,"children":[],"replys":null}],"replys":null},{"id":10441,"groupName":"BOSS直聘","sortIndex":229,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":15111,"groupName":"点了码新客户了解","sortIndex":228,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":12732,"groupName":"测试","sortIndex":219,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":8814,"groupName":"封单话术","sortIndex":176,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":8216,"groupName":"私域合伙人","sortIndex":172,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":6476,"groupName":"新客户了解","sortIndex":119,"parentId":0,"replyType":1,"children":[],"replys":null}]';
$ddd=json_decode($ddd,1);*/

View File

@@ -77,7 +77,8 @@ class WorkbenchGroupCreateJob
{
try {
// 1. 查询启用了建群功能的数据
$workbenches = Workbench::where(['status' => 1, 'type' => 4, 'isDel' => 0,'id' => 315])->order('id desc')->select();
$workbenches = Workbench::where(['status' => 0, 'type' => 4, 'isDel' => 0,'id' => 354])->order('id desc')->select();
foreach ($workbenches as $workbench) {
// 获取工作台配置
$config = WorkbenchGroupCreate::where('workbenchId', $workbench->id)->find();
@@ -86,22 +87,49 @@ class WorkbenchGroupCreateJob
}
// 解析配置
$config['poolGroups'] = json_decode($config['poolGroups'], true);
$config['devices'] = json_decode($config['devices'], true);
$config['poolGroups'] = json_decode($config['poolGroups'] ?? '[]', true) ?: [];
$config['devices'] = json_decode($config['devices'] ?? '[]', true) ?: [];
$config['wechatGroups'] = json_decode($config['wechatGroups'] ?? '[]', true) ?: [];
$config['admins'] = json_decode($config['admins'] ?? '[]', true) ?: [];
if (empty($config['poolGroups']) || empty($config['devices'])) {
// 检查时间限制
if (!$this->isWithinTimeRange($config)) {
continue;
}
// 检查每日建群数量限制
if (!$this->checkDailyLimit($workbench->id, $config)) {
continue;
}
// 检查是否有正在创建中的群,如果有则跳过(避免重复创建)
$creatingCount = Db::name('workbench_group_create_item')
->where('workbenchId', $workbench->id)
->where('status', self::STATUS_CREATING)
->where('groupId', '<>', null) // 有groupId的记录
->group('groupId')
->count();
if ($creatingCount > 0) {
Log::info("工作台ID: {$workbench->id} 有正在创建中的群({$creatingCount}个),跳过本次执行");
continue;
}
if (empty($config['devices'])) {
continue;
}
// 获取群主成员(从设备中获取)
$groupMember = [];
$wechatId = Db::name('device_wechat_login')
->whereIn('deviceId',$config['devices'])
$wechatIds = Db::name('device_wechat_login')
->whereIn('deviceId', $config['devices'])
->where('alive', DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE)
->order('id desc')
->value('wechatId');
if (empty($wechatId)) {
->column('wechatId');
if (empty($wechatIds)) {
continue;
}
$groupMember[] = $wechatId;
$groupMember = array_unique($wechatIds);
// 获取群主好友ID映射所有群主的好友
$groupMemberWechatId = [];
$groupMemberId = [];
@@ -110,6 +138,7 @@ class WorkbenchGroupCreateJob
$friends = Db::table('s2_wechat_friend')
->where('ownerWechatId', $ownerWechatId)
->whereIn('wechatId', $groupMember)
->where('isDeleted', 0)
->field('id,wechatId')
->select();
@@ -120,44 +149,78 @@ class WorkbenchGroupCreateJob
}
}
}
if (empty($groupMemberWechatId)) {
// 如果配置了wechatGroups从指定的群组中获取成员
if (!empty($config['wechatGroups'])) {
$this->addGroupMembersFromWechatGroups($config['wechatGroups'], $groupMember, $groupMemberId, $groupMemberWechatId);
}
if (empty($groupMemberId)) {
continue;
}
// 获取流量池用户
$poolItem = Db::name('traffic_source_package_item')
->whereIn('packageId', $config['poolGroups'])
->group('identifier')
->column('identifier');
if (empty($poolItem)) {
// 获取流量池用户(如果配置了流量池)
$poolItem = [];
if (!empty($config['poolGroups'])) {
$poolItem = Db::name('traffic_source_package_item')
->whereIn('packageId', $config['poolGroups'])
->where('isDel', 0)
->group('identifier')
->column('identifier');
}
// 如果既没有流量池也没有指定群组,跳过
if (empty($poolItem) && empty($config['wechatGroups'])) {
continue;
}
// 获取已入群的用户(排除已成功入群的)111
$groupUser = Db::name('workbench_group_create_item')
->where('workbenchId', $workbench->id)
->where('status', 'in', [self::STATUS_SUCCESS, self::STATUS_ADMIN_FRIEND_ADDED, self::STATUS_CREATING])
->whereIn('wechatId', $poolItem)
->group('wechatId')
->column('wechatId');
// 待入群的用户
$joinUser = array_diff($poolItem, $groupUser);
if (empty($joinUser)) {
// 获取已入群的用户(排除已成功入群的)
$groupUser = [];
if (!empty($poolItem)) {
$groupUser = Db::name('workbench_group_create_item')
->where('workbenchId', $workbench->id)
->where('status', 'in', [self::STATUS_SUCCESS, self::STATUS_ADMIN_FRIEND_ADDED, self::STATUS_CREATING])
->whereIn('wechatId', $poolItem)
->group('wechatId')
->column('wechatId');
}
// 待入群的用户(从流量池中筛选)
$joinUser = !empty($poolItem) ? array_diff($poolItem, $groupUser) : [];
// 如果流量池用户已用完或没有配置流量池但配置了wechatGroups至少创建一次使用群主成员
if (empty($joinUser) && !empty($config['wechatGroups'])) {
// 如果没有流量池用户创建一个空批次让processBatchUsers处理只有群主成员的情况
$joinUser = []; // 空数组,但会继续执行
}
// 如果既没有流量池用户也没有配置wechatGroups跳过
if (empty($joinUser) && empty($config['wechatGroups'])) {
continue;
}
// 计算随机群人数(不包含管理员,只减去群主成员数)
$groupRandNum = mt_rand($config['groupSizeMin'], $config['groupSizeMax']) - count($groupMember);
// 群主成员数 = 群主好友ID数量
$minGroupSize = max(2, $config['groupSizeMin']); // 至少2人才能建群
$maxGroupSize = max($minGroupSize, $config['groupSizeMax']);
$groupRandNum = mt_rand($minGroupSize, $maxGroupSize) - count($groupMemberId);
if ($groupRandNum <= 0) {
$groupRandNum = 1; // 至少需要1个成员
}
// 分批处理待入群用户
$addGroupUser = [];
$totalRows = count($joinUser);
for ($i = 0; $i < $totalRows; $i += $groupRandNum) {
$batchRows = array_slice($joinUser, $i, $groupRandNum);
if (!empty($batchRows)) {
$addGroupUser[] = $batchRows;
if (!empty($joinUser)) {
$totalRows = count($joinUser);
for ($i = 0; $i < $totalRows; $i += $groupRandNum) {
$batchRows = array_slice($joinUser, $i, $groupRandNum);
if (!empty($batchRows)) {
$addGroupUser[] = $batchRows;
}
}
} else {
// 如果没有流量池用户但配置了wechatGroups创建一个空批次
$addGroupUser[] = [];
}
// 初始化WebSocket
$toAccountId = '';
@@ -229,28 +292,59 @@ class WorkbenchGroupCreateJob
$ownerFriendIdsByAccount = [];
$wechatIds = [];
// 获取群主的好友关系(从流量池中筛选)
$ownerFriends = Db::table('s2_wechat_friend')->alias('f')
->join(['s2_wechat_account' => 'a'], 'f.wechatAccountId=a.id')
->whereIn('f.wechatId', $batchUsers)
->whereIn('a.wechatId', $groupOwnerWechatIds)
->where('f.isDeleted', 0)
->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] = [];
// 如果batchUsers为空说明没有流量池用户但可能配置了wechatGroups
// 这种情况下,使用群主成员作为基础,按账号分组
if (empty($batchUsers)) {
// 按账号分组群主成员
foreach ($groupMemberId as $memberId) {
$member = Db::table('s2_wechat_friend')->where('id', $memberId)->find();
if ($member) {
$accountWechatId = $member['ownerWechatId'];
$account = Db::table('s2_wechat_account')
->where('wechatId', $accountWechatId)
->field('id')
->find();
if ($account) {
$wechatAccountId = $account['id'];
if (!isset($ownerFriendIdsByAccount[$wechatAccountId])) {
$ownerFriendIdsByAccount[$wechatAccountId] = [];
}
$ownerFriendIdsByAccount[$wechatAccountId][] = $memberId;
$wechatIds[$memberId] = $groupMemberWechatId[$memberId] ?? '';
}
}
}
$ownerFriendIdsByAccount[$wechatAccountId][] = $friend['id'];
$wechatIds[$friend['id']] = $friend['wechatId'];
} else {
// 获取群主的好友关系(从流量池中筛选)
$ownerFriends = Db::table('s2_wechat_friend')->alias('f')
->join(['s2_wechat_account' => 'a'], 'f.wechatAccountId=a.id')
->whereIn('f.wechatId', $batchUsers)
->whereIn('a.wechatId', $groupOwnerWechatIds)
->where('f.isDeleted', 0)
->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'];
}
}
// 如果没有找到任何好友,跳过
if (empty($ownerFriendIdsByAccount)) {
Log::warning("未找到任何群主好友或成员跳过。工作台ID: {$workbench->id}");
return;
}
// 4. 遍历每个微信账号,创建群
@@ -279,21 +373,47 @@ class WorkbenchGroupCreateJob
}
// 4.3 限制群主好友数量(按随机群人数)
$limitedOwnerFriendIds = array_slice($ownerFriendIds, 0, $groupRandNum);
// 如果ownerFriendIds只包含群主成员没有流量池用户则不需要限制
$limitedOwnerFriendIds = $ownerFriendIds;
if (count($ownerFriendIds) > $groupRandNum) {
$limitedOwnerFriendIds = array_slice($ownerFriendIds, 0, $groupRandNum);
}
// 4.4 创建群:管理员 + 群主成员 + 群主好友(从流量池筛选)
$createFriendIds = array_merge($currentAdminFriendIds, $currentGroupMemberIds, $limitedOwnerFriendIds);
// 合并时去重,避免重复添加群主成员
$createFriendIds = array_merge($currentAdminFriendIds, $currentGroupMemberIds);
foreach ($limitedOwnerFriendIds as $friendId) {
if (!in_array($friendId, $createFriendIds)) {
$createFriendIds[] = $friendId;
}
}
// 微信建群至少需要2个人
if (count($createFriendIds) < 2) {
Log::warning("建群好友数量不足跳过。工作台ID: {$workbench->id}, 微信账号ID: {$wechatAccountId}");
Log::warning("建群好友数量不足至少需要2人跳过。工作台ID: {$workbench->id}, 微信账号ID: {$wechatAccountId}, 当前人数: " . count($createFriendIds));
continue;
}
// 4.5 生成群名称
// 4.5 检查当前账号是否有正在创建中的群,如果有则跳过
$creatingGroupCount = Db::name('workbench_group_create_item')
->where('workbenchId', $workbench->id)
->where('wechatAccountId', $wechatAccountId)
->where('status', self::STATUS_CREATING)
->where('groupId', '<>', null)
->group('groupId')
->count();
if ($creatingGroupCount > 0) {
Log::info("工作台ID: {$workbench->id}, 微信账号ID: {$wechatAccountId} 有正在创建中的群({$creatingGroupCount}个),跳过本次创建");
continue;
}
// 4.6 生成群名称
$existingGroupCount = Db::name('workbench_group_create_item')
->where('workbenchId', $workbench->id)
->where('wechatAccountId', $wechatAccountId)
->where('status', self::STATUS_SUCCESS)
->where('groupId', '<>', null) // 排除groupId为NULL的记录
->group('groupId')
->count();
@@ -301,7 +421,7 @@ class WorkbenchGroupCreateJob
? $config['groupNameTemplate'] . ($existingGroupCount + 1) . '群'
: $config['groupNameTemplate'];
// 4.6 调用建群接口
// 4.7 调用建群接口
$createTime = time();
$createResult = $webSocket->CmdChatroomCreate([
'chatroomName' => $chatroomName,
@@ -311,18 +431,65 @@ class WorkbenchGroupCreateJob
$createResultData = json_decode($createResult, true);
// 4.7 解析建群结果获取群ID
$chatroomId = 0;
// 4.8 解析建群结果获取群ID
// chatroomId: varchar(64) - 微信的群聊ID字符串
// groupId: int(10) - 数据库中的群组ID整数
$chatroomId = null; // 微信群聊ID字符串
$groupId = 0; // 数据库群组ID整数
$tempGroupId = null; // 临时群标识,用于轮询查询
if (!empty($createResultData) && isset($createResultData['code']) && $createResultData['code'] == 200) {
// 尝试从返回数据中获取群ID根据实际API返回格式调整
if (isset($createResultData['data']['chatroomId'])) {
$chatroomId = $createResultData['data']['chatroomId'];
// API返回的是chatroomId(字符串)
$chatroomId = (string)$createResultData['data']['chatroomId'];
// 通过chatroomId查询数据库获取groupId
$group = Db::name('wechat_group')
->where('chatroomId', $chatroomId)
->where('deleteTime', 0)
->find();
if ($group) {
$groupId = intval($group['id']);
}
} elseif (isset($createResultData['data']['id'])) {
$chatroomId = $createResultData['data']['id'];
// API返回的是数据库ID整数
$groupId = intval($createResultData['data']['id']);
// 通过groupId查询chatroomId
$group = Db::name('wechat_group')
->where('id', $groupId)
->where('deleteTime', 0)
->find();
if ($group && !empty($group['chatroomId'])) {
$chatroomId = (string)$group['chatroomId'];
}
}
// 如果有临时标识,保存用于轮询
if (isset($createResultData['data']['tempId'])) {
$tempGroupId = $createResultData['data']['tempId'];
}
}
// 4.8 记录创建请求
// 4.9 如果建群接口没有立即返回群ID进行同步轮询检查
if ($groupId == 0) {
// 获取账号的微信ID群主微信ID
$accountWechatId = Db::table('s2_wechat_account')
->where('id', $wechatAccountId)
->value('wechatId');
if (!empty($accountWechatId)) {
$pollResult = $this->pollGroupCreation($chatroomName, $accountWechatId, $wechatAccountId, $tempGroupId);
if ($pollResult && is_array($pollResult)) {
$groupId = intval($pollResult['groupId'] ?? 0);
$chatroomId = !empty($pollResult['chatroomId']) ? (string)$pollResult['chatroomId'] : null;
} elseif ($pollResult > 0) {
// 兼容旧返回值只返回groupId
$groupId =0;
$chatroomId = null;
}
}
}
// 4.10 记录创建请求
$installData = [];
foreach ($createFriendIds as $friendId) {
$memberType = in_array($friendId, $currentAdminFriendIds)
@@ -333,20 +500,21 @@ class WorkbenchGroupCreateJob
'workbenchId' => $workbench->id,
'friendId' => $friendId,
'wechatId' => $wechatIds[$friendId] ?? ($groupMemberWechatId[$friendId] ?? ''),
'groupId' => $chatroomId,
'groupId' => $groupId > 0 ? $groupId : null, // int类型
'wechatAccountId' => $wechatAccountId,
'status' => $chatroomId > 0 ? self::STATUS_SUCCESS : self::STATUS_CREATING,
'status' => $groupId > 0 ? self::STATUS_SUCCESS : self::STATUS_FAILED,
'memberType' => $memberType,
'retryCount' => 0,
'chatroomId' => $chatroomId > 0 ? $chatroomId : null,
'chatroomId' => $chatroomId, // varchar类型
'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);
// 注意拉人接口需要chatroomId字符串而不是groupId整数
if (!empty($chatroomId) && !empty($currentAdminFriendIds)) {
$this->inviteAdminFriends($workbench, $config, $batchUsers, $currentAdminFriendIds, $chatroomId, $groupId, $wechatAccountId, $wechatIds, $createTime, $webSocket);
}
}
}
@@ -357,13 +525,14 @@ class WorkbenchGroupCreateJob
* @param array $config 配置
* @param array $batchUsers 批次用户流量池微信ID数组
* @param array $adminFriendIds 管理员好友ID数组
* @param int $chatroomId 群ID
* @param string $chatroomId 群ID字符串用于API调用
* @param int $groupId 数据库群组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)
protected function inviteAdminFriends($workbench, $config, $batchUsers, $adminFriendIds, $chatroomId, $groupId, $wechatAccountId, $wechatIds, $createTime, $webSocket)
{
// 获取管理员的微信ID列表
$adminWechatIds = [];
@@ -399,7 +568,7 @@ class WorkbenchGroupCreateJob
$wechatIds[$friend['id']] = $friend['wechatId'];
}
// 调用拉人接口
// 调用拉人接口使用chatroomId字符串
$inviteResult = $webSocket->CmdChatroomInvite([
'wechatChatroomId' => $chatroomId,
'wechatFriendIds' => $adminFriendIdsToInvite
@@ -415,12 +584,12 @@ class WorkbenchGroupCreateJob
'workbenchId' => $workbench->id,
'friendId' => $friendId,
'wechatId' => $wechatIds[$friendId] ?? '',
'groupId' => $chatroomId,
'groupId' => $groupId > 0 ? $groupId : null, // int类型
'wechatAccountId' => $wechatAccountId,
'status' => $inviteSuccess ? self::STATUS_ADMIN_FRIEND_ADDED : self::STATUS_FAILED,
'memberType' => self::MEMBER_TYPE_ADMIN_FRIEND,
'retryCount' => 0,
'chatroomId' => $chatroomId,
'chatroomId' => $chatroomId, // varchar类型
'createTime' => $createTime,
];
}
@@ -429,40 +598,209 @@ class WorkbenchGroupCreateJob
if ($inviteSuccess) {
// 去除成功日志,减少日志空间消耗
} else {
Log::warning("管理员好友拉入失败。工作台ID: {$workbench->id}, 群ID: {$chatroomId}");
Log::warning("管理员好友拉入失败。工作台ID: {$workbench->id}, 群组ID: {$groupId}, 群聊ID: {$chatroomId}");
}
}
/**
* 获取设备列表
* @param Workbench $workbench 工作台
* @param WorkbenchGroupPush $config 配置
* @return array|bool
* 轮询检查群是否创建成功
* @param string $chatroomName 群名称
* @param string $ownerWechatId 群主微信ID
* @param int $wechatAccountId 微信账号ID
* @param string|null $tempGroupId 临时群标识(如果有)
* @return array|int 返回数组包含groupId和chatroomId或只返回groupId兼容旧代码如果未找到返回0
*/
protected function isCreate($workbench, $config, $groupIds = [])
protected function pollGroupCreation($chatroomName, $ownerWechatId, $wechatAccountId, $tempGroupId = null)
{
// 检查发送间隔新逻辑根据startTime、endTime、maxPerDay动态计算
$maxAttempts = 10; // 最多查询10次
$interval = 5; // 每次间隔5秒
// 获取账号IDaccountId和微信账号的微信IDwechatAccountWechatId用于查询s2_wechat_chatroom表
$accountInfo = Db::table('s2_wechat_account')
->where('id', $wechatAccountId)
->field('id,wechatId')
->find();
$accountId = $accountInfo['id'] ?? null;
$wechatAccountWechatId = $accountInfo['wechatId'] ?? null;
if (empty($accountId) && empty($wechatAccountWechatId)) {
Log::warning("无法获取账号ID和微信账号ID跳过轮询。微信账号ID: {$wechatAccountId}");
return 0;
}
// 获取授权信息(用于调用同步接口)
$username = Env::get('api.username2', '');
$password = Env::get('api.password2', '');
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
// 等待5秒第一次立即查询后续等待
if ($attempt > 1) {
sleep($interval);
}
// 1. 先调用接口同步最新的群组信息
try {
$chatroomController = new \app\api\controller\WechatChatroomController();
// 构建同步参数
$syncData = [
'wechatAccountKeyword' => $ownerWechatId, // 通过群主微信ID筛选
'isDeleted' => false,
'pageIndex' => 0,
'pageSize' => 5 // 获取足够多的数据
];
// 调用getlist方法同步数据内部调用isInner=true
$chatroomController->getlist($syncData, true, 0);
} catch (\Exception $e) {
Log::warning("同步群组信息失败: " . $e->getMessage());
// 即使同步失败,也继续查询本地数据
}
// 2. 查询本地表 s2_wechat_chatroom
// 计算5分钟前的时间戳
$fiveMinutesAgo = time() - 300; // 5分钟 = 300秒
$now = time();
// 查询群聊通过群名称、账号ID或微信账号ID和创建时间查询
// 如果accountId不为空优先使用accountId查询如果accountId为空则使用wechatAccountWechatId查询
$chatroom = Db::table('s2_wechat_chatroom')
->where('nickname', $chatroomName)
->where('isDeleted', 0)
->where('createTime', '>=', $fiveMinutesAgo) // 创建时间在5分钟内
->where('createTime', '<=', $now)
->where('wechatAccountWechatId', $wechatAccountWechatId)
->order('createTime', 'desc')
->find();
// 如果找到了群聊返回群ID和chatroomId
if ($chatroom && !empty($chatroom['id'])) {
$chatroomId = !empty($chatroom['chatroomId']) ? (string)$chatroom['chatroomId'] : null;
// 如果有chatroomId尝试查询wechat_group表获取groupId
$groupId = $chatroom['id'];
Log::info("轮询检查群创建成功。群名称: {$chatroomName}, 群聊ID: {$chatroom['id']}, chatroomId: {$chatroomId}, 群组ID: {$groupId}, 尝试次数: {$attempt}");
return [
'groupId' => $groupId > 0 ? $groupId : intval($chatroom['id']), // 如果没有groupId使用chatroom的id
'chatroomId' => $chatroomId ?: (string)$chatroom['id']
];
}
Log::debug("轮询检查群创建中。群名称: {$chatroomName}, 尝试次数: {$attempt}/{$maxAttempts}");
}
// 10次查询后仍未找到返回0表示失败
Log::warning("轮询检查群创建失败,已查询{$maxAttempts}次仍未找到群组。群名称: {$chatroomName}, 群主微信ID: {$ownerWechatId}, 账号ID: {$accountId}");
return 0;
}
/**
* 检查是否在时间范围内
* @param array $config 配置
* @return bool
*/
protected function isWithinTimeRange($config)
{
if (empty($config['startTime']) || empty($config['endTime'])) {
return true; // 如果没有配置时间,则允许执行
}
$today = date('Y-m-d');
$startTimestamp = strtotime($today . ' ' . $config['startTime'] . ':00');
$endTimestamp = strtotime($today . ' ' . $config['endTime'] . ':00');
// 如果时间不符,则跳过
if ($startTimestamp > time() || $endTimestamp < time() || empty($groupIds)) {
$currentTime = time();
// 如果开始时间大于当前时间,还未到执行时间
if ($startTimestamp > $currentTime) {
return false;
}
// 查询今日建群数量
$count = Db::name('wechat_group')
->whereIn('id', $groupIds)
->whereTime('createTime', 'between', [$startTimestamp, $endTimestamp])
->count();
if ($count >= $config['maxGroupsPerDay']) {
// 如果结束时间小于当前时间,已过执行时间
if ($endTimestamp < $currentTime) {
return false;
}
return true;
}
/**
* 检查每日建群数量限制
* @param int $workbenchId 工作台ID
* @param array $config 配置
* @return bool
*/
protected function checkDailyLimit($workbenchId, $config)
{
if (empty($config['maxGroupsPerDay']) || $config['maxGroupsPerDay'] <= 0) {
return true; // 如果没有配置限制,则允许执行
}
$today = date('Y-m-d');
$startTimestamp = strtotime($today . ' 00:00:00');
$endTimestamp = strtotime($today . ' 23:59:59');
// 查询今日已创建的群数量(状态为成功)
$todayCount = Db::name('workbench_group_create_item')
->where('workbenchId', $workbenchId)
->where('status', self::STATUS_SUCCESS)
->where('groupId', '<>', null) // 排除groupId为NULL的记录
->where('createTime', 'between', [$startTimestamp, $endTimestamp])
->group('groupId')
->count();
return $todayCount < $config['maxGroupsPerDay'];
}
/**
* 从指定的微信群组中获取成员
* @param array $wechatGroups 群组ID数组可能是好友ID或群组ID
* @param array $groupMember 群主成员微信ID数组引用传递
* @param array $groupMemberId 群主成员好友ID数组引用传递
* @param array $groupMemberWechatId 群主成员微信ID映射引用传递
*/
protected function addGroupMembersFromWechatGroups($wechatGroups, &$groupMember, &$groupMemberId, &$groupMemberWechatId)
{
foreach ($wechatGroups as $groupId) {
if (is_numeric($groupId)) {
// 数字ID可能是好友ID查询好友信息
$friend = Db::table('s2_wechat_friend')
->where('id', $groupId)
->where('isDeleted', 0)
->field('id,wechatId,ownerWechatId')
->find();
if ($friend) {
// 添加到群主成员
if (!in_array($friend['ownerWechatId'], $groupMember)) {
$groupMember[] = $friend['ownerWechatId'];
}
if (!isset($groupMemberWechatId[$friend['id']])) {
$groupMemberWechatId[$friend['id']] = $friend['wechatId'];
$groupMemberId[] = $friend['id'];
}
} else {
// 如果不是好友ID可能是群组ID查询群组信息
$group = Db::name('wechat_group')
->where('id', $groupId)
->where('deleteTime', 0)
->field('ownerWechatId')
->find();
if ($group && !in_array($group['ownerWechatId'], $groupMember)) {
$groupMember[] = $group['ownerWechatId'];
}
}
} else {
// 字符串ID手动创建的群组可能是wechatId
if (!in_array($groupId, $groupMember)) {
$groupMember[] = $groupId;
}
}
}
}
/**
* 记录任务开始
@@ -510,4 +848,5 @@ class WorkbenchGroupCreateJob
return false;
}
}