From b8754f61742f5fc9474cc44c54e43d6dccac68c2 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Thu, 25 Dec 2025 15:39:57 +0800 Subject: [PATCH] =?UTF-8?q?=E5=86=85=E5=AE=B9=E5=BA=93=E7=BE=A4=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E5=8A=9F=E8=83=BD=20+=20=E5=9C=BA=E6=99=AF=E8=8E=B7?= =?UTF-8?q?=E5=AE=A2=E5=88=86=E9=A1=B5=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GroupSelection/selectionPopup.tsx | 4 + .../index.module.scss | 287 ++++++++++++++ .../GroupSelectionWithMembers/index.tsx | 363 ++++++++++++++++++ .../pages/mobile/mine/content/form/index.tsx | 52 ++- .../pages/mobile/scenarios/plan/list/api.ts | 2 +- .../mobile/scenarios/plan/list/index.tsx | 57 ++- Server/application/chukebao/config/route.php | 1 + .../controller/WechatChatroomController.php | 62 +++ Server/application/command.php | 2 +- .../controller/ContentLibraryController.php | 203 ++++++++-- .../chatroom/GetChatroomListV1Controller.php | 5 +- 11 files changed, 985 insertions(+), 53 deletions(-) create mode 100644 Cunkebao/src/components/GroupSelectionWithMembers/index.module.scss create mode 100644 Cunkebao/src/components/GroupSelectionWithMembers/index.tsx diff --git a/Cunkebao/src/components/GroupSelection/selectionPopup.tsx b/Cunkebao/src/components/GroupSelection/selectionPopup.tsx index dd5c9927..8bf39cd6 100644 --- a/Cunkebao/src/components/GroupSelection/selectionPopup.tsx +++ b/Cunkebao/src/components/GroupSelection/selectionPopup.tsx @@ -142,7 +142,11 @@ export default function SelectionPopup({ // 复制一份selectedOptions到临时变量 setTempSelectedOptions([...selectedOptions]); fetchGroups(1, ""); + } else { + // 弹窗关闭时重置状态 + setTempSelectedOptions([]); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [visible]); // 搜索防抖(只在弹窗打开且搜索词变化时执行) diff --git a/Cunkebao/src/components/GroupSelectionWithMembers/index.module.scss b/Cunkebao/src/components/GroupSelectionWithMembers/index.module.scss new file mode 100644 index 00000000..344a93ee --- /dev/null +++ b/Cunkebao/src/components/GroupSelectionWithMembers/index.module.scss @@ -0,0 +1,287 @@ +.container { + width: 100%; +} + +.inputWrapper { + position: relative; + margin-bottom: 12px; + + .inputIcon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: #bdbdbd; + font-size: 20px; + z-index: 1; + pointer-events: none; + } + + .input { + padding-left: 38px !important; + height: 48px; + border-radius: 16px !important; + border: 1px solid #e5e6eb !important; + font-size: 16px; + background: #f8f9fa; + } + + .clearBtn { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + color: #999; + font-size: 16px; + z-index: 1; + } +} + +.selectedGroupsList { + display: flex; + flex-direction: column; + gap: 16px; +} + +.groupCard { + background: #fff; + border-radius: 12px; + padding: 16px; + border: 1px solid #e5e6eb; +} + +.groupHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} + +.groupInfo { + display: flex; + align-items: center; + gap: 12px; + flex: 1; + min-width: 0; +} + +.groupAvatar { + width: 48px; + height: 48px; + border-radius: 8px; + flex-shrink: 0; +} + +.groupDetails { + flex: 1; + min-width: 0; +} + +.groupName { + font-size: 16px; + font-weight: 500; + color: #222; + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.groupId { + font-size: 14px; + color: #888; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.deleteGroupBtn { + color: #ff4d4f; + font-size: 18px; + padding: 4px; + min-width: auto; + height: auto; +} + +.membersSection { + padding-top: 16px; + border-top: 1px solid #f0f0f0; +} + +.membersLabel { + font-size: 14px; + color: #666; + margin-bottom: 12px; + font-weight: 500; +} + +.membersList { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +.memberItem { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + position: relative; + width: 70px; +} + +.memberAvatar { + width: 56px; + height: 56px; + border-radius: 50%; + position: relative; +} + +.removeMemberBtn { + position: absolute; + top: -4px; + right: -4px; + width: 20px; + height: 20px; + min-width: 20px; + padding: 0; + background: #fff; + border: 1px solid #e5e6eb; + border-radius: 50%; + color: #ff4d4f; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; +} + +.memberName { + font-size: 12px; + color: #222; + text-align: center; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.addMemberBtn { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 56px; + height: 56px; + border: 1px dashed #d9d9d9; + border-radius: 50%; + background: #fafafa; + color: #999; + font-size: 20px; + cursor: pointer; + transition: all 0.2s; + gap: 4px; + + span { + font-size: 12px; + } + + &:active { + background: #f0f0f0; + border-color: #1677ff; + color: #1677ff; + } +} + +.memberSelectionPopup { + display: flex; + flex-direction: column; + height: 100%; + background: #fff; +} + +.popupHeader { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + border-bottom: 1px solid #f0f0f0; +} + +.popupTitle { + font-size: 18px; + font-weight: 600; + color: #222; +} + +.closeBtn { + color: #1677ff; + font-size: 16px; +} + +.memberList { + flex: 1; + overflow-y: auto; + padding: 16px 20px; +} + +.memberListItem { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 0; + border-bottom: 1px solid #f5f5f5; + cursor: pointer; + + &:last-child { + border-bottom: none; + } + + &.selected { + .memberListItemName { + color: #1677ff; + } + } +} + +.memberListItemAvatar { + width: 40px; + height: 40px; + border-radius: 50%; +} + +.memberListItemName { + flex: 1; + font-size: 16px; + color: #222; +} + +.checkmark { + color: #1677ff; + font-size: 18px; + font-weight: bold; +} + +.loadingBox { + display: flex; + align-items: center; + justify-content: center; + padding: 40px 20px; +} + +.loadingText { + font-size: 14px; + color: #999; +} + +.emptyBox { + display: flex; + align-items: center; + justify-content: center; + padding: 40px 20px; +} + +.emptyText { + font-size: 14px; + color: #999; +} diff --git a/Cunkebao/src/components/GroupSelectionWithMembers/index.tsx b/Cunkebao/src/components/GroupSelectionWithMembers/index.tsx new file mode 100644 index 00000000..f1f73acd --- /dev/null +++ b/Cunkebao/src/components/GroupSelectionWithMembers/index.tsx @@ -0,0 +1,363 @@ +import React, { useState, useEffect } from "react"; +import { SearchOutlined, DeleteOutlined, PlusOutlined } from "@ant-design/icons"; +import { Button, Input, Popup } from "antd-mobile"; +import { Avatar } from "antd-mobile"; +import style from "./index.module.scss"; +import GroupSelection from "../GroupSelection"; +import { GroupSelectionItem } from "../GroupSelection/data"; +import request from "@/api/request"; + +// 群成员接口 +export interface GroupMember { + id: string; + nickname: string; + wechatId: string; + avatar: string; + gender?: "male" | "female"; + role?: "owner" | "admin" | "member"; +} + +// 带成员的群选项 +export interface GroupWithMembers extends GroupSelectionItem { + members?: GroupMember[]; + groupId?: string; // 用于关联成员和群 +} + +interface GroupSelectionWithMembersProps { + selectedGroups: GroupWithMembers[]; + onSelect: (groups: GroupWithMembers[]) => void; + placeholder?: string; + className?: string; + readonly?: boolean; +} + +// 获取群成员列表 +const getGroupMembers = async ( + groupId: string, + page: number = 1, + limit: number = 100, + keyword: string = "", +): Promise => { + try { + const params: any = { + page, + limit, + groupId, + }; + if (keyword.trim()) { + params.keyword = keyword.trim(); + } + const response = await request("/v1/kefu/wechatChatroom/members", params, "GET"); + // request 拦截器会返回 res.data.data ?? res.data + // 对于 { code: 200, data: { list: [...] } } 的返回,拦截器会返回 { list: [...] } + const memberList = response?.list || response?.data?.list || []; + + // 映射接口返回的数据结构到我们的接口 + return memberList.map((item: any) => ({ + id: String(item.id), + nickname: item.nickname || "", + wechatId: item.wechatId || "", + avatar: item.avatar || "", + gender: undefined, // 接口未返回,暂时设为 undefined + role: undefined, // 接口未返回,暂时设为 undefined + })); + } catch (error) { + console.error("获取群成员失败:", error); + return []; + } +}; + +const GroupSelectionWithMembers: React.FC = ({ + selectedGroups, + onSelect, + placeholder = "选择聊天群", + className = "", + readonly = false, +}) => { + const [groupSelectionVisible, setGroupSelectionVisible] = useState(false); + const [memberSelectionVisible, setMemberSelectionVisible] = useState<{ + visible: boolean; + groupId: string; + }>({ visible: false, groupId: "" }); + const [allMembers, setAllMembers] = useState>({}); + const [selectedMembers, setSelectedMembers] = useState>({}); + const [loadingMembers, setLoadingMembers] = useState(false); + + // 处理群选择 + const handleGroupSelect = (groups: GroupSelectionItem[]) => { + const groupsWithMembers: GroupWithMembers[] = groups.map(group => { + const existing = selectedGroups.find(g => g.id === group.id); + return { + ...group, + members: existing?.members || [], + }; + }); + onSelect(groupsWithMembers); + setGroupSelectionVisible(false); + }; + + // 删除群 + const handleRemoveGroup = (groupId: string) => { + if (readonly) return; + const newGroups = selectedGroups.filter(g => g.id !== groupId); + const newSelectedMembers = { ...selectedMembers }; + delete newSelectedMembers[groupId]; + setSelectedMembers(newSelectedMembers); + onSelect(newGroups); + }; + + // 打开成员选择弹窗 + const handleOpenMemberSelection = async (groupId: string) => { + if (readonly) return; + setMemberSelectionVisible({ visible: true, groupId }); + + // 如果还没有加载过该群的成员列表,则加载 + if (!allMembers[groupId]) { + setLoadingMembers(true); + try { + const members = await getGroupMembers(groupId); + setAllMembers(prev => ({ ...prev, [groupId]: members })); + } catch (error) { + console.error("加载群成员失败:", error); + } finally { + setLoadingMembers(false); + } + } + }; + + // 关闭成员选择弹窗 + const handleCloseMemberSelection = () => { + setMemberSelectionVisible({ visible: false, groupId: "" }); + }; + + // 选择成员 + const handleSelectMember = (groupId: string, member: GroupMember) => { + if (readonly) return; + const currentMembers = selectedMembers[groupId] || []; + const isSelected = currentMembers.some(m => m.id === member.id); + + let newSelectedMembers = { ...selectedMembers }; + if (isSelected) { + newSelectedMembers[groupId] = currentMembers.filter(m => m.id !== member.id); + } else { + newSelectedMembers[groupId] = [...currentMembers, member]; + } + setSelectedMembers(newSelectedMembers); + + // 更新群数据 + const updatedGroups = selectedGroups.map(group => { + if (group.id === groupId) { + return { + ...group, + members: newSelectedMembers[groupId] || [], + }; + } + return group; + }); + onSelect(updatedGroups); + }; + + // 移除成员 + const handleRemoveMember = (groupId: string, memberId: string) => { + if (readonly) return; + const currentMembers = selectedMembers[groupId] || []; + const newMembers = currentMembers.filter(m => m.id !== memberId); + + const newSelectedMembers = { ...selectedMembers }; + newSelectedMembers[groupId] = newMembers; + setSelectedMembers(newSelectedMembers); + + // 更新群数据 + const updatedGroups = selectedGroups.map(group => { + if (group.id === groupId) { + return { + ...group, + members: newMembers, + }; + } + return group; + }); + onSelect(updatedGroups); + }; + + // 同步 selectedGroups 到 selectedMembers + useEffect(() => { + const membersMap: Record = {}; + selectedGroups.forEach(group => { + if (group.members && group.members.length > 0) { + membersMap[group.id] = group.members; + } + }); + setSelectedMembers(membersMap); + }, [selectedGroups.length]); + + // 获取显示文本 + const getDisplayText = () => { + if (selectedGroups.length === 0) return ""; + return `已选择${selectedGroups.length}个群聊`; + }; + + const currentGroupMembers = allMembers[memberSelectionVisible.groupId] || []; + const currentSelectedMembers = selectedMembers[memberSelectionVisible.groupId] || []; + + return ( +
+ {/* 输入框 */} +
!readonly && setGroupSelectionVisible(true)} + > + + + {!readonly && selectedGroups.length > 0 && ( + + )} +
+ + {/* 已选群列表 */} + {selectedGroups.length > 0 && ( +
+ {selectedGroups.map(group => ( +
+ {/* 群信息 */} +
+
+ +
+
{group.name}
+
ID: {group.chatroomId || group.id}
+
+
+ {!readonly && ( + + )} +
+ + {/* 成员选择区域 */} +
+
+ 采集群内指定成员 ({group.members?.length || 0}人) +
+
+ {group.members?.map(member => ( +
+ +
{member.nickname}
+ {!readonly && ( + + )} +
+ ))} + {!readonly && ( +
handleOpenMemberSelection(group.id)} + > + + 添加 +
+ )} +
+
+
+ ))} +
+ )} + + {/* 群选择弹窗 */} + + + {/* 成员选择弹窗 */} + +
+
+
选择成员
+ +
+
+ {loadingMembers ? ( +
+
加载中...
+
+ ) : currentGroupMembers.length > 0 ? ( + currentGroupMembers.map(member => { + const isSelected = currentSelectedMembers.some(m => m.id === member.id); + return ( +
handleSelectMember(memberSelectionVisible.groupId, member)} + > + +
{member.nickname}
+ {isSelected &&
} +
+ ); + }) + ) : ( +
+
暂无成员数据
+
+ )} +
+
+
+
+ ); +}; + +export default GroupSelectionWithMembers; diff --git a/Cunkebao/src/pages/mobile/mine/content/form/index.tsx b/Cunkebao/src/pages/mobile/mine/content/form/index.tsx index f758cee4..b52229bb 100644 --- a/Cunkebao/src/pages/mobile/mine/content/form/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/content/form/index.tsx @@ -6,11 +6,13 @@ import { DownOutlined } from "@ant-design/icons"; import NavCommon from "@/components/NavCommon"; import FriendSelection from "@/components/FriendSelection"; import GroupSelection from "@/components/GroupSelection"; +import GroupSelectionWithMembers from "@/components/GroupSelectionWithMembers"; import DeviceSelection from "@/components/DeviceSelection"; import Layout from "@/components/Layout/Layout"; import style from "./index.module.scss"; import { getContentLibraryDetail, updateContentLibrary, createContentLibrary } from "./api"; import { GroupSelectionItem } from "@/components/GroupSelection/data"; +import { GroupWithMembers } from "@/components/GroupSelectionWithMembers"; import { FriendSelectionItem } from "@/components/FriendSelection/data"; import { DeviceSelectionItem } from "@/components/DeviceSelection/data"; @@ -48,6 +50,9 @@ export default function ContentForm() { const [selectedGroupsOptions, setSelectedGroupsOptions] = useState< GroupSelectionItem[] >([]); + const [selectedGroupsWithMembers, setSelectedGroupsWithMembers] = useState< + GroupWithMembers[] + >([]); const [useAI, setUseAI] = useState(false); const [aiPrompt, setAIPrompt] = useState("重写这条朋友圈 要求: 1、原本的字数和意思不要修改超过10% 2、出现品牌名或个人名字就去除"); const [enabled, setEnabled] = useState(true); @@ -94,7 +99,30 @@ export default function ContentForm() { setSelectedDevices(deviceOptions || []); setSelectedFriends(data.sourceFriends || []); setSelectedGroups(data.selectedGroups || []); - setSelectedGroupsOptions(data.selectedGroupsOptions || []); + // 使用 wechatGroupsOptions 作为群列表数据 + setSelectedGroupsOptions(data.wechatGroupsOptions || data.selectedGroupsOptions || []); + // 处理带成员的群数据 + // groupMembersOptions 是一个对象,key是群ID(字符串),value是成员数组 + const groupMembersMap = data.groupMembersOptions || {}; + const groupsWithMembers: GroupWithMembers[] = (data.wechatGroupsOptions || data.selectedGroupsOptions || []).map( + (group: any) => { + const groupIdStr = String(group.id); + const members = groupMembersMap[groupIdStr] || []; + // 映射成员数据结构 + return { + ...group, + members: members.map((member: any) => ({ + id: String(member.id), + nickname: member.nickname || "", + wechatId: member.wechatId || "", + avatar: member.avatar || "", + gender: undefined, + role: undefined, + })), + }; + }, + ); + setSelectedGroupsWithMembers(groupsWithMembers); setSelectedFriendsOptions(data.friendsGroupsOptions || []); setKeywordsInclude((data.keywordInclude || []).join(",")); setKeywordsExclude((data.keywordExclude || []).join(",")); @@ -140,7 +168,15 @@ export default function ContentForm() { devices: selectedDevices.map(d => d.id), friendsGroups: friendsGroups, wechatGroups: selectedGroups, - groupMembers: {}, + groupMembers: selectedGroupsWithMembers.reduce( + (acc, group) => { + if (group.members && group.members.length > 0) { + acc[group.id] = group.members.map(m => m.id); + } + return acc; + }, + {} as Record, + ), keywordInclude: keywordsInclude .split(/,|,|\n|\s+/) .map(s => s.trim()) @@ -180,6 +216,12 @@ export default function ContentForm() { setSelectedGroupsOptions(groups); }; + const handleGroupsWithMembersChange = (groups: GroupWithMembers[]) => { + setSelectedGroupsWithMembers(groups); + setSelectedGroups(groups.map(g => g.id.toString())); + setSelectedGroupsOptions(groups); + }; + const handleFriendsChange = (friends: FriendSelectionItem[]) => { setSelectedFriends(friends.map(f => f.id.toString())); setSelectedFriendsOptions(friends); @@ -335,9 +377,9 @@ export default function ContentForm() { /> - diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/list/api.ts b/Cunkebao/src/pages/mobile/scenarios/plan/list/api.ts index 3943a92a..b1f5c5f2 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/list/api.ts +++ b/Cunkebao/src/pages/mobile/scenarios/plan/list/api.ts @@ -6,7 +6,7 @@ import { PlanDetail, PlanListResponse, ApiResponse } from "./data"; export function getPlanList(params: { sceneId: string; page: number; - pageSize: number; + limit: number; }): Promise { return request(`/v1/plan/list`, params, "GET"); } diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx index 367187e9..e658e26f 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/list/index.tsx @@ -8,8 +8,9 @@ import { Popup, Card, Tag, + InfiniteScroll, } from "antd-mobile"; -import { Input, Pagination } from "antd"; +import { Input } from "antd"; import { PlusOutlined, CopyOutlined, @@ -80,7 +81,7 @@ const ScenarioList: React.FC = () => { const [hasMore, setHasMore] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [total, setTotal] = useState(0); - const pageSize = 20; + const limit = 20; // 获取计划列表数据 const fetchPlanList = async (page: number, isLoadMore: boolean = false) => { @@ -96,7 +97,7 @@ const ScenarioList: React.FC = () => { const response = await getPlanList({ sceneId: scenarioId, page: page, - pageSize: pageSize, + limit: limit, }); if (response && response.list) { @@ -110,7 +111,7 @@ const ScenarioList: React.FC = () => { // 更新分页信息 setTotal(response.total || 0); - setHasMore(response.list.length === pageSize); + setHasMore(response.list.length === limit); setCurrentPage(page); } } catch (error) { @@ -149,10 +150,11 @@ const ScenarioList: React.FC = () => { fetchScenarioData(); }, [scenarioId]); - // 分页改变处理 - const handlePageChange = async (page: number) => { - setCurrentPage(page); - await fetchPlanList(page, false); + // 加载更多 + const handleLoadMore = async () => { + if (!hasMore || loadingMore || loadingTasks) return; + const nextPage = currentPage + 1; + await fetchPlanList(nextPage, true); }; const handleCopyPlan = async (taskId: string) => { @@ -405,18 +407,6 @@ const ScenarioList: React.FC = () => { } loading={loading} - footer={ -
- -
- } >
{/* 计划列表 */} @@ -530,6 +520,33 @@ const ScenarioList: React.FC = () => {
))} + {/* 上拉加载更多 */} + + {loadingMore && ( +
+ + + 加载中... + +
+ )} + {!hasMore && filteredTasks.length > 0 && ( +
+ 没有更多了 +
+ )} +
)} diff --git a/Server/application/chukebao/config/route.php b/Server/application/chukebao/config/route.php index b22a01b7..7143a686 100644 --- a/Server/application/chukebao/config/route.php +++ b/Server/application/chukebao/config/route.php @@ -21,6 +21,7 @@ Route::group('v1/', function () { Route::group('wechatChatroom/', function () { Route::get('list', 'app\chukebao\controller\WechatChatroomController@getList'); // 获取好友列表 Route::get('detail', 'app\chukebao\controller\WechatChatroomController@getDetail'); // 获取群详情 + Route::get('members', 'app\chukebao\controller\WechatChatroomController@getMembers'); // 获取群成员列表 Route::post('aiAnnouncement', 'app\chukebao\controller\WechatChatroomController@aiAnnouncement'); // AI群公告 }); diff --git a/Server/application/chukebao/controller/WechatChatroomController.php b/Server/application/chukebao/controller/WechatChatroomController.php index 5d521143..936c837d 100644 --- a/Server/application/chukebao/controller/WechatChatroomController.php +++ b/Server/application/chukebao/controller/WechatChatroomController.php @@ -151,6 +151,68 @@ class WechatChatroomController extends BaseController return ResponseHelper::success($detail); } + public function getMembers() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $groupId = $this->request->param('groupId', ''); + $keyword = $this->request->param('keyword', ''); + + $accountId = $this->getUserInfo('s2_accountId'); + if (empty($accountId)) { + return ResponseHelper::error('请先登录'); + } + + // 验证群组ID必填 + if (empty($groupId)) { + return ResponseHelper::error('群组ID不能为空'); + } + + // 验证群组是否属于当前账号 + $chatroom = Db::table('s2_wechat_chatroom') + ->where(['id' => $groupId, 'isDeleted' => 0]) + ->find(); + + if (!$chatroom) { + return ResponseHelper::error('群组不存在或无权限访问'); + } + + // 获取群组的chatroomId(微信群聊ID) + $chatroomId = $chatroom['chatroomId'] ?? $chatroom['id']; + + // 如果chatroomId为空,使用id作为chatroomId + if (empty($chatroomId)) { + $chatroomId = $chatroom['id']; + } + + // 构建查询 + $query = Db::table('s2_wechat_chatroom_member') + ->where('chatroomId', $chatroomId); + + // 关键字搜索:昵称、备注、别名 + if ($keyword !== '' && $keyword !== null) { + $query->where(function ($q) use ($keyword) { + $like = '%' . $keyword . '%'; + $q->whereLike('nickname', $like) + ->whereOr('conRemark', 'like', $like) + ->whereOr('alias', 'like', $like); + }); + } + + $query->order('id desc'); + $total = $query->count(); + $list = $query->page($page, $limit)->select(); + + // 处理时间格式 + foreach ($list as $k => &$v) { + $v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s', $v['createTime']) : ''; + $v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s', $v['updateTime']) : ''; + } + unset($v); + + return ResponseHelper::success(['list' => $list, 'total' => $total]); + } + public function aiAnnouncement() { $userId = $this->getUserInfo('id'); diff --git a/Server/application/command.php b/Server/application/command.php index 061fb69b..0fa99a64 100644 --- a/Server/application/command.php +++ b/Server/application/command.php @@ -19,7 +19,7 @@ return [ 'message:friendsList' => 'app\command\MessageFriendsListCommand', // 微信好友消息列表 √ 'message:chatroomList' => 'app\command\MessageChatroomListCommand', // 微信群聊消息列表 √ 'department:list' => 'app\command\DepartmentListCommand', // 部门列表 √ - 'content:sync' => 'app\command\SyncContentCommand', // 同步内容库 √ + 'content:sync' => 'app\command\SyncContentCommand', // 同步内容库 XXXXXXXX 'groupFriends:list' => 'app\command\GroupFriendsCommand', // 微信群好友列表 // 'allotFriends:run' => 'app\command\AllotFriendCommand', // 自动分配微信好友 // 'allotChatroom:run' => 'app\command\AllotChatroomCommand', // 自动分配微信群聊 diff --git a/Server/application/cunkebao/controller/ContentLibraryController.php b/Server/application/cunkebao/controller/ContentLibraryController.php index 690d8a64..94770ac5 100644 --- a/Server/application/cunkebao/controller/ContentLibraryController.php +++ b/Server/application/cunkebao/controller/ContentLibraryController.php @@ -357,6 +357,7 @@ class ContentLibraryController extends Controller // 初始化选项数组 $library['friendsGroupsOptions'] = []; $library['wechatGroupsOptions'] = []; + $library['groupMembersOptions'] = []; // 批量查询好友信息 if (!empty($library['friendsGroups'])) { @@ -385,6 +386,61 @@ class ContentLibraryController extends Controller } } + // 批量查询群成员信息 + if (!empty($library['groupMembers'])) { + // groupMembers格式: {"826825": ["413771", "413769"], "840818": ["496300", "496302"]} + // 键是群组ID,值是成员ID数组 + $allMemberIds = []; + $groupMembersMap = []; + + if (is_array($library['groupMembers'])) { + foreach ($library['groupMembers'] as $groupId => $memberIds) { + if (is_array($memberIds) && !empty($memberIds)) { + $allMemberIds = array_merge($allMemberIds, $memberIds); + // 保存群组ID和成员ID的映射关系 + $groupMembersMap[$groupId] = $memberIds; + } + } + } + + if (!empty($allMemberIds)) { + // 去重 + $allMemberIds = array_unique($allMemberIds); + + // 查询群成员信息 + $members = Db::table('s2_wechat_chatroom_member') + ->field('id, chatroomId, wechatId, nickname, avatar, conRemark, alias, friendType, createTime, updateTime') + ->whereIn('id', $allMemberIds) + ->select(); + + // 将成员数据按ID建立索引 + $membersById = []; + foreach ($members as $member) { + // 格式化时间字段 + $member['createTime'] = !empty($member['createTime']) ? date('Y-m-d H:i:s', $member['createTime']) : ''; + $member['updateTime'] = !empty($member['updateTime']) ? date('Y-m-d H:i:s', $member['updateTime']) : ''; + $membersById[$member['id']] = $member; + } + + // 按照群组ID分组返回 + $groupMembersOptions = []; + foreach ($groupMembersMap as $groupId => $memberIds) { + $groupMembersOptions[$groupId] = []; + foreach ($memberIds as $memberId) { + if (isset($membersById[$memberId])) { + $groupMembersOptions[$groupId][] = $membersById[$memberId]; + } + } + } + + $library['groupMembersOptions'] = $groupMembersOptions; + } else { + $library['groupMembersOptions'] = []; + } + } else { + $library['groupMembersOptions'] = []; + } + //获取设备信息 if (!empty($library['deviceGroups'])) { $deviceList = DeviceModel::alias('d') @@ -921,7 +977,7 @@ class ContentLibraryController extends Controller // 如果有发送者信息,也获取发送者详情 if (!empty($item['wechatId'])) { - $senderInfo = Db::name('wechat_chatroom_member') + $senderInfo = Db::table('s2_wechat_chatroom_member') ->where([ 'chatroomId' => $groupInfo['chatroomId'], 'wechatId' => $item['wechatId'] @@ -1477,8 +1533,8 @@ class ContentLibraryController extends Controller try { // 查询群组信息 - $groups = Db::name('wechat_group')->alias('g') - ->field('g.id, g.chatroomId, g.name, g.ownerWechatId') + $groups = Db::table('s2_wechat_chatroom')->alias('g') + ->field('g.id, g.chatroomId, g.nickname as name, g.wechatAccountWechatId as ownerWechatId') ->whereIn('g.id', $groupIds) ->where('g.deleteTime', 0) ->select(); @@ -1500,12 +1556,59 @@ class ContentLibraryController extends Controller ]; } + // groupMembers格式: {"826825": ["413771", "413769"], "840818": ["496300", "496302"]} + // 键是群组ID,值是该群组的成员ID数组 + // 需要按群组分组处理,确保每个群组只采集该群组配置的成员 + + // 建立群组ID到成员ID数组的映射 + $groupIdToMemberIds = []; + if (is_array($groupMembers)) { + foreach ($groupMembers as $groupId => $memberIds) { + if (is_array($memberIds) && !empty($memberIds)) { + $groupIdToMemberIds[$groupId] = $memberIds; + } + } + } + if (empty($groupIdToMemberIds)) { + return [ + 'status' => 'failed', + 'message' => '未找到有效的群成员ID' + ]; + } + + // 为每个群组查询成员信息,建立群组ID到成员wechatId数组的映射 + $groupIdToMemberWechatIds = []; + foreach ($groupIdToMemberIds as $groupId => $memberIds) { + // 查询该群组的成员信息,获取wechatId + $members = Db::table('s2_wechat_chatroom_member') + ->field('id, wechatId') + ->whereIn('id', $memberIds) + ->select(); + + $wechatIds = []; + foreach ($members as $member) { + if (!empty($member['wechatId'])) { + $wechatIds[] = $member['wechatId']; + } + } + + if (!empty($wechatIds)) { + $groupIdToMemberWechatIds[$groupId] = array_unique($wechatIds); + } + } + if (empty($groupIdToMemberWechatIds)) { + return [ + 'status' => 'failed', + 'message' => '未找到有效的群成员微信ID' + ]; + } + // 从群组采集内容 $collectedData = []; $totalMessagesCount = 0; $chatroomIds = array_column($groups, 'id'); - // 获取群消息 - 支持时间范围过滤 + // 获取群消息 - 支持时间范围过滤(先不添加群成员过滤,后面按群组分别过滤) $messageWhere = [ ['wechatChatroomId', 'in', $chatroomIds], ['type', '=', 2] @@ -1516,7 +1619,7 @@ class ContentLibraryController extends Controller $messageWhere[] = ['createTime', 'between', [$library['timeStart'], $library['timeEnd']]]; } - // 查询群消息 + // 查询群消息(先查询所有消息,后面按群组和成员过滤) $groupMessages = Db::table('s2_wechat_message') ->where($messageWhere) ->order('createTime', 'desc') @@ -1532,6 +1635,34 @@ class ContentLibraryController extends Controller $groupedMessages = []; foreach ($groupMessages as $message) { $chatroomId = $message['wechatChatroomId']; + $senderWechatId = $message['senderWechatId'] ?? ''; + + // 找到对应的群组信息 + $groupInfo = null; + foreach ($groups as $group) { + if ($group['id'] == $chatroomId) { + $groupInfo = $group; + break; + } + } + + if (!$groupInfo) { + continue; + } + + // 检查该消息的发送者是否在该群组的配置成员列表中 + $groupId = $groupInfo['id']; + if (!isset($groupIdToMemberWechatIds[$groupId])) { + // 该群组没有配置成员,跳过 + continue; + } + + // 检查发送者是否在配置的成员列表中 + if (!in_array($senderWechatId, $groupIdToMemberWechatIds[$groupId])) { + // 发送者不在该群组的配置成员列表中,跳过 + continue; + } + if (!isset($groupedMessages[$chatroomId])) { $groupedMessages[$chatroomId] = [ 'count' => 0, @@ -1579,27 +1710,14 @@ class ContentLibraryController extends Controller continue; } - // 找到对应的群组信息 - $groupInfo = null; - foreach ($groups as $group) { - if ($group['id'] == $chatroomId) { - $groupInfo = $group; - break; - } - } - - if (!$groupInfo) { - continue; - } - // 如果启用了AI处理 if (!empty($library['aiEnabled']) && !empty($content)) { $contentAi = $this->aiRewrite($library, $content); - if (!empty($content)) { - $moment['contentAi'] = $contentAi; + if (!empty($contentAi)) { + $message['contentAi'] = $contentAi; } else { - $moment['contentAi'] = ''; + $message['contentAi'] = ''; } } @@ -1968,7 +2086,38 @@ class ContentLibraryController extends Controller return true; } - // 提取消息内容中的链接 + $resUrls = []; + + $content = ''; + switch ($message['msgType']) { + case 1: // 文字 + $content = $message['content']; + $contentType = 4; + break; + case 3: //图片 + $resUrls[] = $message['content']; + $contentType = 1; + break; + case 47: //动态图片 + $resUrls[] = $message['content']; + $contentType = 1; + break; + case 34: //语言 + return false; + case 43: //视频 + $resUrls[] = $message['content']; + $contentType = 3; + break; + case 42: //名片 + return false; + case 49: //文件 + $links = json_decode($message['content'],true); + return false; + default: + return false; + } + + /*// 提取消息内容中的链接 $content = $message['content'] ?? ''; $links = []; $pattern = '/https?:\/\/[-A-Za-z0-9+&@#\/%?=~_|!:,.;]+[-A-Za-z0-9+&@#\/%=~_|]/'; @@ -1986,6 +2135,8 @@ class ContentLibraryController extends Controller // 判断内容类型 (0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序, 6=图文) $contentType = $this->determineContentType($content, $resUrls, $links); + */ + // 创建新的内容项目 $item = new ContentItem(); @@ -1993,7 +2144,7 @@ class ContentLibraryController extends Controller $item->type = 'group_message'; // 群消息类型 $item->title = '来自 ' . ($group['name'] ?? '未知群组') . ' 的消息'; $item->contentData = json_encode($message, JSON_UNESCAPED_UNICODE); - $item->msgId = $message['msgId'] ?? ''; // 存储msgId便于后续查询 + $item->msgId = $message['msgSvrId'] ?? ''; // 存储msgSvrId便于后续查询 $item->createTime = time(); $item->content = $content; $item->contentType = $contentType; // 设置内容类型 @@ -2011,13 +2162,17 @@ class ContentLibraryController extends Controller if (!empty($resUrls[0])) { $item->coverImage = $resUrls[0]; } + }else{ + $item->resUrls = json_encode([], JSON_UNESCAPED_UNICODE); } // 处理链接 if (!empty($links)) { $item->urls = json_encode($links, JSON_UNESCAPED_UNICODE); + }else{ + $item->urls = json_encode([], JSON_UNESCAPED_UNICODE); } - + $item->ossUrls = json_encode([], JSON_UNESCAPED_UNICODE); // 设置商品信息(需根据消息内容解析) $this->extractProductInfo($item, $content); diff --git a/Server/application/cunkebao/controller/chatroom/GetChatroomListV1Controller.php b/Server/application/cunkebao/controller/chatroom/GetChatroomListV1Controller.php index 8499c6e6..d9b11a07 100644 --- a/Server/application/cunkebao/controller/chatroom/GetChatroomListV1Controller.php +++ b/Server/application/cunkebao/controller/chatroom/GetChatroomListV1Controller.php @@ -39,10 +39,10 @@ class GetChatroomListV1Controller extends BaseController $where = []; if ($this->getUserInfo('isAdmin') == 1) { - $where[] = ['g.deleteTime', '=', 0]; + $where[] = ['gg.isDeleted', '=', 0]; $where[] = ['g.ownerWechatId', 'in', $wechatIds]; } else { - $where[] = ['g.deleteTime', '=', 0]; + $where[] = ['gg.isDeleted', '=', 0]; $where[] = ['g.ownerWechatId', 'in', $wechatIds]; //$where[] = ['g.userId', '=', $this->getUserInfo('id')]; } @@ -55,6 +55,7 @@ class GetChatroomListV1Controller extends BaseController ->field(['g.id', 'g.chatroomId', 'g.name', 'g.avatar','g.ownerWechatId', 'g.identifier', 'g.createTime', 'wa.nickname as ownerNickname','wa.avatar as ownerAvatar','wa.alias as ownerAlias']) ->join('wechat_account wa', 'g.ownerWechatId = wa.wechatId', 'LEFT') + ->join(['s2_wechat_chatroom' => 'gg'], 'g.id = gg.id', 'LEFT') ->where($where); $total = $data->count();