内容库群选择功能 + 场景获客分页修复

This commit is contained in:
wong
2025-12-25 15:39:57 +08:00
parent 302617cd81
commit b8754f6174
11 changed files with 985 additions and 53 deletions

View File

@@ -142,7 +142,11 @@ export default function SelectionPopup({
// 复制一份selectedOptions到临时变量
setTempSelectedOptions([...selectedOptions]);
fetchGroups(1, "");
} else {
// 弹窗关闭时重置状态
setTempSelectedOptions([]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible]);
// 搜索防抖(只在弹窗打开且搜索词变化时执行)

View File

@@ -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;
}

View File

@@ -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<GroupMember[]> => {
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<GroupSelectionWithMembersProps> = ({
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<Record<string, GroupMember[]>>({});
const [selectedMembers, setSelectedMembers] = useState<Record<string, GroupMember[]>>({});
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<string, GroupMember[]> = {};
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 (
<div className={`${style.container} ${className}`}>
{/* 输入框 */}
<div
className={style.inputWrapper}
onClick={() => !readonly && setGroupSelectionVisible(true)}
>
<SearchOutlined className={style.inputIcon} />
<Input
placeholder={placeholder}
value={getDisplayText()}
readOnly
className={style.input}
/>
{!readonly && selectedGroups.length > 0 && (
<Button
fill="none"
size="small"
className={style.clearBtn}
onClick={e => {
e.stopPropagation();
setSelectedMembers({});
onSelect([]);
}}
>
<DeleteOutlined />
</Button>
)}
</div>
{/* 已选群列表 */}
{selectedGroups.length > 0 && (
<div className={style.selectedGroupsList}>
{selectedGroups.map(group => (
<div key={group.id} className={style.groupCard}>
{/* 群信息 */}
<div className={style.groupHeader}>
<div className={style.groupInfo}>
<Avatar src={group.avatar} className={style.groupAvatar} />
<div className={style.groupDetails}>
<div className={style.groupName}>{group.name}</div>
<div className={style.groupId}>ID: {group.chatroomId || group.id}</div>
</div>
</div>
{!readonly && (
<Button
fill="none"
size="small"
className={style.deleteGroupBtn}
onClick={() => handleRemoveGroup(group.id)}
>
<DeleteOutlined />
</Button>
)}
</div>
{/* 成员选择区域 */}
<div className={style.membersSection}>
<div className={style.membersLabel}>
({group.members?.length || 0})
</div>
<div className={style.membersList}>
{group.members?.map(member => (
<div key={member.id} className={style.memberItem}>
<Avatar src={member.avatar} className={style.memberAvatar} />
<div className={style.memberName}>{member.nickname}</div>
{!readonly && (
<Button
fill="none"
size="small"
className={style.removeMemberBtn}
onClick={() => handleRemoveMember(group.id, member.id)}
>
<DeleteOutlined />
</Button>
)}
</div>
))}
{!readonly && (
<div
className={style.addMemberBtn}
onClick={() => handleOpenMemberSelection(group.id)}
>
<PlusOutlined />
<span></span>
</div>
)}
</div>
</div>
</div>
))}
</div>
)}
{/* 群选择弹窗 */}
<GroupSelection
selectedOptions={selectedGroups as GroupSelectionItem[]}
onSelect={handleGroupSelect}
placeholder={placeholder}
visible={groupSelectionVisible}
onVisibleChange={setGroupSelectionVisible}
showInput={false}
showSelectedList={false}
/>
{/* 成员选择弹窗 */}
<Popup
visible={memberSelectionVisible.visible}
onMaskClick={handleCloseMemberSelection}
position="bottom"
bodyStyle={{
height: "70vh",
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
}}
>
<div className={style.memberSelectionPopup}>
<div className={style.popupHeader}>
<div className={style.popupTitle}></div>
<Button
fill="none"
size="small"
onClick={handleCloseMemberSelection}
className={style.closeBtn}
>
</Button>
</div>
<div className={style.memberList}>
{loadingMembers ? (
<div className={style.loadingBox}>
<div className={style.loadingText}>...</div>
</div>
) : currentGroupMembers.length > 0 ? (
currentGroupMembers.map(member => {
const isSelected = currentSelectedMembers.some(m => m.id === member.id);
return (
<div
key={member.id}
className={`${style.memberListItem} ${isSelected ? style.selected : ""}`}
onClick={() => handleSelectMember(memberSelectionVisible.groupId, member)}
>
<Avatar src={member.avatar} className={style.memberListItemAvatar} />
<div className={style.memberListItemName}>{member.nickname}</div>
{isSelected && <div className={style.checkmark}></div>}
</div>
);
})
) : (
<div className={style.emptyBox}>
<div className={style.emptyText}></div>
</div>
)}
</div>
</div>
</Popup>
</div>
);
};
export default GroupSelectionWithMembers;

View File

@@ -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<string, string[]>,
),
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() {
/>
</Tabs.Tab>
<Tabs.Tab title="选择聊天群" key="groups">
<GroupSelection
selectedOptions={selectedGroupsOptions}
onSelect={handleGroupsChange}
<GroupSelectionWithMembers
selectedGroups={selectedGroupsWithMembers}
onSelect={handleGroupsWithMembersChange}
placeholder="选择聊天群"
/>
</Tabs.Tab>

View File

@@ -6,7 +6,7 @@ import { PlanDetail, PlanListResponse, ApiResponse } from "./data";
export function getPlanList(params: {
sceneId: string;
page: number;
pageSize: number;
limit: number;
}): Promise<PlanListResponse> {
return request(`/v1/plan/list`, params, "GET");
}

View File

@@ -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={
<div className="pagination-container">
<Pagination
total={total}
pageSize={pageSize}
current={currentPage}
onChange={handlePageChange}
showSizeChanger={false}
showQuickJumper={false}
/>
</div>
}
>
<div className={style["scenario-list-page"]}>
{/* 计划列表 */}
@@ -530,6 +520,33 @@ const ScenarioList: React.FC = () => {
</div>
</Card>
))}
{/* 上拉加载更多 */}
<InfiniteScroll
loadMore={handleLoadMore}
hasMore={hasMore}
threshold={100}
>
{loadingMore && (
<div style={{ padding: "20px", textAlign: "center" }}>
<SpinLoading color="primary" style={{ fontSize: 16 }} />
<span style={{ marginLeft: 8, color: "#999", fontSize: 12 }}>
...
</span>
</div>
)}
{!hasMore && filteredTasks.length > 0 && (
<div
style={{
padding: "20px",
textAlign: "center",
color: "#999",
fontSize: 12,
}}
>
</div>
)}
</InfiniteScroll>
</>
)}
</div>

View File

@@ -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群公告
});

View File

@@ -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');

View File

@@ -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', // 自动分配微信群聊

View File

@@ -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);

View File

@@ -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();