diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/api.ts b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/api.ts index 15dc3be5a..4e21c063c 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/api.ts +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/api.ts @@ -5,6 +5,13 @@ import { type TrafficPoolGroup, } from "../api"; +/** + * 是否启用 V2 API(需要先完成数据迁移才能启用) + * 设置为 true 使用新版 V2 接口 + * 设置为 false 使用旧版 V1 接口 + */ +const USE_V2_API = true; + // 兼容旧版 Package 接口 export interface Package { id: number; @@ -26,7 +33,7 @@ export interface PackageList { } /** - * 获取流量池分组列表(V2版本) + * 获取流量池分组列表 * 自动转换为旧版 Package 格式以保持兼容 */ export async function getPackage(params: { @@ -34,6 +41,12 @@ export async function getPackage(params: { pageSize: number; keyword: string; }): Promise { + // 如果未启用 V2 API,直接使用 V1 接口 + if (!USE_V2_API) { + return request("/v1/traffic/pool/getPackage", params, "GET"); + } + + // V2 API 逻辑 try { const groups = await getGroupsV2(); @@ -75,21 +88,27 @@ export async function getPackage(params: { total, }; } catch (error) { - console.error("获取分组列表失败:", error); + console.error("V2 API 调用失败,降级到 V1:", error); // 降级到旧版API return request("/v1/traffic/pool/getPackage", params, "GET"); } } /** - * 删除分组(V2版本) + * 删除分组 */ export async function deletePackage(id: number): Promise<{ success: boolean }> { + // 如果未启用 V2 API,直接使用 V1 接口 + if (!USE_V2_API) { + return request("/v1/traffic/pool/deletePackage", { packageId: id }, "DELETE"); + } + + // V2 API 逻辑 try { await deleteGroupV2(id); return { success: true }; } catch (error) { - console.error("删除分组失败:", error); + console.error("V2 API 调用失败,降级到 V1:", error); // 降级到旧版API return request("/v1/traffic/pool/deletePackage", { packageId: id }, "DELETE"); } diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx index 01acc1307..b7c019b07 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/list/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useRef, useCallback } from "react"; import Layout from "@/components/Layout/Layout"; import { SearchOutlined, @@ -62,38 +62,61 @@ const TrafficPoolList: React.FC = () => { const [page, setPage] = useState(1); const [pageSize] = useState(10); const [total, setTotal] = useState(0); - const [search, setSearch] = useState(""); + const [keyword, setKeyword] = useState(""); + const [searchInput, setSearchInput] = useState(""); - const handleSearch = (value: string) => { - setSearch(value); - setPage(1); + // 防抖定时器 + const debounceTimer = useRef(null); + + // 搜索输入变化时的防抖处理 + const handleSearchChange = (value: string) => { + setSearchInput(value); + + // 清除之前的定时器 + if (debounceTimer.current) { + clearTimeout(debounceTimer.current); + } + + // 设置新的定时器,500ms 后执行搜索 + debounceTimer.current = setTimeout(() => { + setKeyword(value); + setPage(1); + }, 500); }; + // 清理定时器 + useEffect(() => { + return () => { + if (debounceTimer.current) { + clearTimeout(debounceTimer.current); + } + }; + }, []); + const handleRefresh = () => { setPage(1); - // 触发数据重新获取 - const fetchData = async () => { - setLoading(true); - try { - const params = { - page: 1, - pageSize, - keyword: search, - }; - - const res: PackageList = await getPackage(params); - setList(res?.list || []); - setTotal(res?.total || 0); - } catch (error) { - console.error("获取列表失败:", error); - } finally { - setLoading(false); - } - }; - - fetchData(); + fetchData(1); }; + const fetchData = useCallback(async (customPage?: number) => { + setLoading(true); + try { + const params = { + page: customPage || page, + pageSize, + keyword, + }; + + const res: PackageList = await getPackage(params); + setList(res?.list || []); + setTotal(res?.total || 0); + } catch (error) { + console.error("获取列表失败:", error); + } finally { + setLoading(false); + } + }, [page, pageSize, keyword]); + const handleDelete = async (id: number, name: string) => { try { // eslint-disable-next-line no-alert @@ -108,27 +131,8 @@ const TrafficPoolList: React.FC = () => { }; useEffect(() => { - const fetchData = async () => { - setLoading(true); - try { - const params = { - page, - pageSize, - keyword: search, - }; - - const res: PackageList = await getPackage(params); - setList(res?.list || []); - setTotal(res?.total || 0); - } catch (error) { - console.error("获取列表失败:", error); - } finally { - setLoading(false); - } - }; - fetchData(); - }, [page, pageSize, search]); + }, [fetchData]); return ( {
handleSearch(e.target.value)} + value={searchInput} + onChange={e => handleSearchChange(e.target.value)} prefix={} allowClear size="large" diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/api.ts b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/api.ts index 1115caab5..692998663 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/api.ts +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/api.ts @@ -9,6 +9,13 @@ import { type PageResult, } from "../api"; +/** + * 是否启用 V2 API(需要先完成数据迁移才能启用) + * 设置为 true 使用新版 V2 接口 + * 设置为 false 使用旧版 V1 接口 + */ +const USE_V2_API = true; + // 兼容旧版返回格式 export interface PoolListItem { id: number; @@ -33,13 +40,19 @@ export interface PoolListResult { } /** - * 获取流量池列表(V2版本) + * 获取流量池列表 */ export async function fetchTrafficPoolList(params: { page?: number; pageSize?: number; keyword?: string; }): Promise { + // 如果未启用 V2 API,直接使用 V1 接口 + if (!USE_V2_API) { + return request("/v1/traffic/pool", params, "GET"); + } + + // V2 API 逻辑 try { const result = await getPoolList({ page: params.page || 1, @@ -69,7 +82,7 @@ export async function fetchTrafficPoolList(params: { total: result.total, }; } catch (error) { - console.error("获取流量池列表失败:", error); + console.error("V2 API 调用失败,降级到 V1:", error); return request("/v1/traffic/pool", params, "GET"); } } @@ -82,12 +95,18 @@ export async function fetchScenarioOptions() { } /** - * 获取流量池分组选项(V2版本) + * 获取流量池分组选项 */ export async function fetchPackageOptions(): Promise<{ list: Array<{ id: number; name: string; type: number; num: number }>; total: number; }> { + // 如果未启用 V2 API,直接使用 V1 接口 + if (!USE_V2_API) { + return request("/v1/traffic/pool/getPackage", {}, "GET"); + } + + // V2 API 逻辑 try { const groups = await getGroups(); const list = groups.map((g: TrafficPoolGroup) => ({ @@ -98,13 +117,13 @@ export async function fetchPackageOptions(): Promise<{ })); return { list, total: list.length }; } catch (error) { - console.error("获取分组选项失败:", error); + console.error("V2 API 调用失败,降级到 V1:", error); return request("/v1/traffic/pool/getPackage", {}, "GET"); } } /** - * 创建分组或添加成员到分组(V2版本) + * 创建分组或添加成员到分组 */ export async function addPackage(params: { type: string; // 类型 1搜索 2选择用户 3文件上传 @@ -119,6 +138,12 @@ export async function addPackage(params: { userIds?: number[]; // 要添加的用户ID列表 userValue?: number; }) { + // 如果未启用 V2 API,直接使用 V1 接口 + if (!USE_V2_API) { + return request("/v1/traffic/pool/addPackage", params, "POST"); + } + + // V2 API 逻辑 try { // 如果是添加成员到现有分组 if (params.addPackageId && params.userIds && params.userIds.length > 0) { @@ -144,12 +169,12 @@ export async function addPackage(params: { // 降级到旧版API return request("/v1/traffic/pool/addPackage", params, "POST"); } catch (error) { - console.error("操作失败:", error); + console.error("V2 API 调用失败,降级到 V1:", error); return request("/v1/traffic/pool/addPackage", params, "POST"); } } -// 导出V2 API供直接使用 +// 导出V2 API供直接使用(当 USE_V2_API 设置为 true 后可用) export { getGroups, createGroup, diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/index.tsx b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/index.tsx index c5c6306da..9d6066088 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/poolList1/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState, useRef } from "react"; import Layout from "@/components/Layout/Layout"; import { SearchOutlined, @@ -29,7 +29,11 @@ const TrafficPoolList: React.FC = () => { const [page, setPage] = useState(1); const [pageSize] = useState(10); const [total, setTotal] = useState(0); - const [search, setSearch] = useState(""); + const [keyword, setKeyword] = useState(""); + const [searchInput, setSearchInput] = useState(""); + + // 防抖定时器 + const debounceTimer = useRef(null); // 筛选相关 const [showFilter, setShowFilter] = useState(false); @@ -51,6 +55,31 @@ const TrafficPoolList: React.FC = () => { // 数据分析 const [showStats, setShowStats] = useState(false); + // 搜索输入变化时的防抖处理 + const handleSearchChange = (value: string) => { + setSearchInput(value); + + // 清除之前的定时器 + if (debounceTimer.current) { + clearTimeout(debounceTimer.current); + } + + // 设置新的定时器,500ms 后执行搜索 + debounceTimer.current = setTimeout(() => { + setKeyword(value); + setPage(1); + }, 500); + }; + + // 清理定时器 + useEffect(() => { + return () => { + if (debounceTimer.current) { + clearTimeout(debounceTimer.current); + } + }; + }, []); + // 获取列表 const getList = async (customParams?: any) => { setLoading(true); @@ -58,7 +87,7 @@ const TrafficPoolList: React.FC = () => { const params: any = { page, pageSize, - keyword: search, + keyword, packageld: filterParams.packageId, sceneId: filterParams.scenarioId, userValue: filterParams.userValue, @@ -87,6 +116,11 @@ const TrafficPoolList: React.FC = () => { }); }, []); + // 参数变化时重新获取数据 + useEffect(() => { + getList(); + }, [page, pageSize, keyword, filterParams]); + // 全选/反选 const handleSelectAll = (checked: boolean) => { if (checked) { @@ -127,7 +161,7 @@ const TrafficPoolList: React.FC = () => { ...(filterParams.selectedDevices.length > 0 && { deviceId: filterParams.selectedDevices.map(d => d.id).join(","), }), - ...(search && { keyword: search }), + ...(keyword && { keyword }), }; console.log("批量加入请求参数:", params); @@ -155,29 +189,9 @@ const TrafficPoolList: React.FC = () => { } }; - // 搜索防抖处理 - const [searchInput, setSearchInput] = useState(search); - - const debouncedSearch = useCallback(() => { - const timer = setTimeout(() => { - setSearch(searchInput); - // 搜索时重置到第一页并请求列表 - setPage(1); - getList({ keyword: searchInput, page: 1 }); - }, 500); // 500ms 防抖延迟 - - return () => clearTimeout(timer); - }, [searchInput]); - - useEffect(() => { - const cleanup = debouncedSearch(); - return cleanup; - }, [debouncedSearch]); - const handSearch = (value: string) => { - setSearchInput(value); + handleSearchChange(value); setSelectedIds([]); - debouncedSearch(); }; return ( diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/userList/api.ts b/Cunkebao/src/pages/mobile/mine/traffic-pool/userList/api.ts index bc0abba49..d25993a30 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/userList/api.ts +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/userList/api.ts @@ -1,12 +1,12 @@ -import request from "@/api/request"; import { getGroupMembers, getPoolList, type TrafficPoolMember, type PageResult, + SOURCE_TYPE_TEXT, } from "../api"; -// 兼容旧版返回格式 +// 用户列表项(兼容前端页面使用的字段) export interface UserListItem { id: number; identifier: string; @@ -15,15 +15,19 @@ export interface UserListItem { avatar: string | null; gender: number; phone: string | null; + wechatId: string | null; // 微信号(用于显示) + fromd: string; // 来源(中文) + packages: string[]; // 分组名称 + createTime: string; // 创建时间 + // 其他字段 alias: string | null; - packages: string[]; + tags: string[]; R: number; F: number; M: number; RFM: number; money: number; msgCount: number; - tag: string[]; } export interface UserListResult { @@ -47,55 +51,57 @@ export async function fetchTrafficPoolList(params: { const keyword = params.keyword || ""; const groupId = params.groupId || (params.packageId ? parseInt(params.packageId) : 0); - try { - let result: PageResult; + let result: PageResult; - if (groupId && groupId > 0) { - // 按分组查询 - result = await getGroupMembers({ - groupId, - page, - pageSize, - keyword, - }); - } else { - // 全量查询 - result = await getPoolList({ - page, - pageSize, - keyword, - }); - } - - // 转换为旧版格式 - const list: UserListItem[] = result.list.map(item => ({ - id: item.id, - identifier: item.identifier, - companyId: item.companyId, - nickname: item.nickname, - avatar: item.avatar, - gender: item.gender, - phone: item.phone || item.mobile || null, - alias: item.wechatAlias, - packages: [], // 新版不再使用包概念 - R: item.rfmScore?.R || 0, - F: item.rfmScore?.F || 0, - M: item.rfmScore?.M || 0, - RFM: item.rfmScore?.total || 0, - money: item.totalOrderAmount || 0, - msgCount: item.totalMsgCount || 0, - tag: item.tags?.map(t => t.tagName) || [], - })); - - return { - list, - total: result.total, - }; - } catch (error) { - console.error("获取用户列表失败:", error); - // 降级到旧版API - return request("/v1/traffic/pool/user-list", { ...params, packageId: groupId || params.packageId }, "GET"); + if (groupId && groupId > 0) { + // 按分组查询 + result = await getGroupMembers({ + groupId, + page, + pageSize, + keyword, + }); + } else { + // 全量查询 + result = await getPoolList({ + page, + pageSize, + keyword, + }); } + + // 转换为前端页面使用的格式 + const list: UserListItem[] = result.list.map((item: any) => ({ + id: item.id, + identifier: item.identifier, + companyId: item.companyId, + nickname: item.nickname || null, + avatar: item.avatar || null, + gender: item.gender || 0, + phone: item.phone || item.mobile || null, + // 微信号:优先显示 wechatAlias,如果没有则显示 wechatId + wechatId: item.wechatAlias || item.wechatId || null, + // 来源:将数字转换为中文 + fromd: SOURCE_TYPE_TEXT[item.firstSourceType] || "未知来源", + // 分组:暂时为空,后续可扩展 + packages: [], + // 创建时间 + createTime: item.createTime || "", + // 其他字段 + alias: item.wechatAlias || null, + tags: item.tags?.map((t: any) => t.tagName) || [], + R: item.rfmScore?.R || 0, + F: item.rfmScore?.F || 0, + M: item.rfmScore?.M || 0, + RFM: item.rfmScore?.total || 0, + money: item.totalOrderAmount || 0, + msgCount: item.totalMsgCount || 0, + })); + + return { + list, + total: result.total, + }; } /** diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/userList/data.ts b/Cunkebao/src/pages/mobile/mine/traffic-pool/userList/data.ts index 204547faf..934193fed 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/userList/data.ts +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/userList/data.ts @@ -2,20 +2,24 @@ export interface TrafficPoolUser { id: number; identifier: string; - mobile: string; - wechatId: string; - fromd: string; - status: number; - createTime: string; companyId: number; - sourceId: string; - type: number; - nickname: string; - avatar: string; + nickname: string | null; + avatar: string | null; gender: number; - phone: string; - packages: string[]; + phone: string | null; + wechatId: string | null; // 微信号(用于显示) + fromd: string; // 来源(中文) + packages: string[]; // 分组名称 + createTime: string; // 创建时间 + // 其他字段 + alias: string | null; tags: string[]; + R: number; + F: number; + M: number; + RFM: number; + money: number; + msgCount: number; } // 列表响应类型 diff --git a/Cunkebao/src/pages/mobile/mine/traffic-pool/userList/index.tsx b/Cunkebao/src/pages/mobile/mine/traffic-pool/userList/index.tsx index 823ccf655..31a92cad8 100644 --- a/Cunkebao/src/pages/mobile/mine/traffic-pool/userList/index.tsx +++ b/Cunkebao/src/pages/mobile/mine/traffic-pool/userList/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState, useRef } from "react"; import { useParams, useNavigate } from "react-router-dom"; import Layout from "@/components/Layout/Layout"; import { SearchOutlined, ReloadOutlined } from "@ant-design/icons"; @@ -22,61 +22,65 @@ const TrafficPoolUserList: React.FC = () => { const [page, setPage] = useState(1); const [pageSize] = useState(10); const [total, setTotal] = useState(0); - const [search, setSearch] = useState(""); + const [keyword, setKeyword] = useState(""); + const [searchInput, setSearchInput] = useState(""); + + // 防抖定时器 + const debounceTimer = useRef(null); // 获取列表 - const getList = async (customParams?: any) => { + const getList = useCallback(async (customParams?: any) => { setLoading(true); try { const params: any = { page, pageSize, - keyword: search, - packageId: id, // 根据流量包ID筛选用户 - ...customParams, // 允许传入自定义参数覆盖 + keyword, + packageId: id, + ...customParams, }; const res = await fetchTrafficPoolList(params); setList(res.list || []); setTotal(res.total || 0); } catch (error) { - // 忽略请求过于频繁的错误,避免页面崩溃 if (error !== "请求过于频繁,请稍后再试") { console.error("获取列表失败:", error); } } finally { setLoading(false); } - }; + }, [page, pageSize, keyword, id]); - // 搜索防抖处理 - const [searchInput, setSearchInput] = useState(search); - - const debouncedSearch = useCallback(() => { - const timer = setTimeout(() => { - setSearch(searchInput); - // 搜索时重置到第一页并请求列表 - setPage(1); - getList({ keyword: searchInput, page: 1 }); - }, 500); // 500ms 防抖延迟 - - return () => clearTimeout(timer); - }, [searchInput]); - - useEffect(() => { - const cleanup = debouncedSearch(); - return cleanup; - }, [debouncedSearch]); - - const handSearch = (value: string) => { + // 搜索输入变化时的防抖处理 + const handleSearchChange = (value: string) => { setSearchInput(value); - debouncedSearch(); + + // 清除之前的定时器 + if (debounceTimer.current) { + clearTimeout(debounceTimer.current); + } + + // 设置新的定时器,500ms 后执行搜索 + debounceTimer.current = setTimeout(() => { + setKeyword(value); + setPage(1); + }, 500); }; + // 清理定时器 + useEffect(() => { + return () => { + if (debounceTimer.current) { + clearTimeout(debounceTimer.current); + } + }; + }, []); + // 初始加载和参数变化时重新获取数据 useEffect(() => { getList(); - }, [page, pageSize, search, id]); + }, [getList]); return ( { handSearch(e.target.value)} + onChange={e => handleSearchChange(e.target.value)} prefix={} allowClear size="large" diff --git a/Cunkebao/src/router/module/mine.tsx b/Cunkebao/src/router/module/mine.tsx index 58e9ec877..3937e269f 100644 --- a/Cunkebao/src/router/module/mine.tsx +++ b/Cunkebao/src/router/module/mine.tsx @@ -4,6 +4,7 @@ import DeviceDetail from "@/pages/mobile/mine/devices/DeviceDetail"; import TrafficPool from "@/pages/mobile/mine/traffic-pool/list/index"; import TrafficPool2 from "@/pages/mobile/mine/traffic-pool/poolList1/index"; import TrafficPoolUserList from "@/pages/mobile/mine/traffic-pool/userList/index"; +import TrafficPoolDetail from "@/pages/mobile/mine/traffic-pool/detail/index"; import CreateTrafficPackage from "@/pages/mobile/mine/traffic-pool/form/index"; import WechatAccounts from "@/pages/mobile/mine/wechat-accounts/list/index"; import WechatAccountDetail from "@/pages/mobile/mine/wechat-accounts/detail/index"; @@ -55,6 +56,12 @@ const routes = [ element: , auth: true, }, + // 流量池用户详情页 + { + path: "/mine/traffic-pool/detail/:wechatId/:id", + element: , + auth: true, + }, // 微信号管理路由 { diff --git a/Server/application/common/model/TrafficPoolSource.php b/Server/application/common/model/TrafficPoolSource.php index 886704d48..35f7c816a 100644 --- a/Server/application/common/model/TrafficPoolSource.php +++ b/Server/application/common/model/TrafficPoolSource.php @@ -3,6 +3,7 @@ namespace app\common\model; use think\Model; +use think\Db; /** * 流量来源表模型类 @@ -130,6 +131,200 @@ class TrafficPoolSource extends Model ->order('createTime DESC') ->select(); } + + /** + * 获取流量的所有来源(带群归属信息) + * @param int $poolCompanyId + * @return array + */ + public static function getSourcesWithOwners(int $poolCompanyId): array + { + $sources = self::where('poolCompanyId', $poolCompanyId) + ->order('createTime DESC') + ->select() + ->toArray(); + + if (empty($sources)) { + return []; + } + + // 收集所有群ID + $chatroomIds = []; + foreach ($sources as $source) { + if (!empty($source['sourceChatroomId'])) { + $chatroomIds[] = $source['sourceChatroomId']; + } + } + + // 查询群信息和归属客服 + $chatroomOwners = []; + if (!empty($chatroomIds)) { + $chatroomOwners = self::getChatroomOwners($chatroomIds); + } + + // 组装数据(按来源类型和关键标识去重) + $result = []; + $seenChatroomIds = []; // 用于群成员来源去重 + $seenFriendIds = []; // 用于好友添加来源去重 + + foreach ($sources as $source) { + // 群成员来源去重:同一个群只保留一条记录 + if ($source['sourceType'] == self::SOURCE_TYPE_GROUP_MEMBER && !empty($source['sourceChatroomId'])) { + $chatroomId = $source['sourceChatroomId']; + if (isset($seenChatroomIds[$chatroomId])) { + continue; // 跳过重复的群 + } + $seenChatroomIds[$chatroomId] = true; + } + + // 好友添加来源去重:同一个好友(按sourceWechatId或sourceName)只保留一条记录 + if ($source['sourceType'] == self::SOURCE_TYPE_FRIEND_ADD) { + $friendKey = $source['sourceWechatId'] ?: ($source['sourceName'] ?: ''); + if (!empty($friendKey) && isset($seenFriendIds[$friendKey])) { + continue; // 跳过重复的好友来源 + } + if (!empty($friendKey)) { + $seenFriendIds[$friendKey] = true; + } + } + + $sourceData = $source; + + // 格式化时间(兼容时间戳和日期字符串) + if (!empty($source['createTime'])) { + if (is_numeric($source['createTime'])) { + $sourceData['createTimeFormatted'] = date('Y-m-d H:i:s', (int)$source['createTime']); + } else { + $sourceData['createTimeFormatted'] = $source['createTime']; + } + } else { + $sourceData['createTimeFormatted'] = null; + } + + // 添加来源类型名称 + $sourceData['sourceTypeName'] = self::SOURCE_TYPE_NAMES[$source['sourceType']] ?? '未知来源'; + + // 如果是群成员来源,添加群归属信息和群ID展示 + if ($source['sourceType'] == self::SOURCE_TYPE_GROUP_MEMBER && !empty($source['sourceChatroomId'])) { + $chatroomId = $source['sourceChatroomId']; + $sourceData['chatroomOwners'] = $chatroomOwners[$chatroomId] ?? []; + $sourceData['chatroomInfo'] = self::getChatroomInfo($chatroomId); + // 添加群ID用于展示 + $sourceData['displayId'] = $chatroomId; + } elseif ($source['sourceType'] == self::SOURCE_TYPE_FRIEND_ADD) { + // 好友添加来源,添加好友微信ID用于展示 + $sourceData['chatroomOwners'] = []; + $sourceData['chatroomInfo'] = null; + $sourceData['displayId'] = $source['sourceWechatId'] ?: ''; + } else { + $sourceData['chatroomOwners'] = []; + $sourceData['chatroomInfo'] = null; + $sourceData['displayId'] = $source['sourceId'] ?: ''; + } + + $result[] = $sourceData; + } + + return $result; + } + + /** + * 获取群的归属客服信息(支持多个客服) + * @param array $chatroomIds 群聊ID数组 + * @return array [chatroomId => [owner1, owner2, ...]] + */ + protected static function getChatroomOwners(array $chatroomIds): array + { + if (empty($chatroomIds)) { + return []; + } + + // 查询群和归属账号信息 + $chatrooms = Db::table(['s2_wechat_chatroom' => 'wc']) + ->leftJoin(['s2_wechat_account' => 'wa'], 'wa.wechatId = wc.wechatAccountWechatId') + ->whereIn('wc.chatroomId', $chatroomIds) + ->where('wc.isDeleted', 0) + ->field([ + 'wc.chatroomId', + 'wc.nickname as chatroomName', + 'wc.chatroomAvatar', + 'wc.wechatAccountWechatId as ownerWechatId', + 'wc.wechatAccountNickname as ownerNickname', + 'wc.wechatAccountAvatar as ownerAvatar', + 'wc.wechatAccountAlias as ownerAlias', + 'wa.id as accountId', + 'wa.nickName as accountNickname', + ]) + ->select(); + + // 按 chatroomId 分组,一个群可能有多条记录(多个客服管理) + $result = []; + foreach ($chatrooms as $chatroom) { + $chatroomId = $chatroom['chatroomId']; + if (!isset($result[$chatroomId])) { + $result[$chatroomId] = []; + } + + // 避免重复添加相同的客服 + $ownerWechatId = $chatroom['ownerWechatId']; + $exists = false; + foreach ($result[$chatroomId] as $existing) { + if ($existing['ownerWechatId'] === $ownerWechatId) { + $exists = true; + break; + } + } + + if (!$exists && !empty($ownerWechatId)) { + $result[$chatroomId][] = [ + 'ownerWechatId' => $ownerWechatId, + 'ownerNickname' => $chatroom['ownerNickname'] ?: $chatroom['accountNickname'] ?: '', + 'ownerAvatar' => $chatroom['ownerAvatar'] ?: '', + 'ownerAlias' => $chatroom['ownerAlias'] ?: '', + 'accountId' => $chatroom['accountId'], + ]; + } + } + + return $result; + } + + /** + * 获取群信息 + * @param string $chatroomId 群聊ID + * @return array|null + */ + protected static function getChatroomInfo(string $chatroomId): ?array + { + $chatroom = Db::table(['s2_wechat_chatroom' => 'wc']) + ->where('wc.chatroomId', $chatroomId) + ->where('wc.isDeleted', 0) + ->field([ + 'wc.id', + 'wc.chatroomId', + 'wc.nickname as chatroomName', + 'wc.chatroomAvatar', + 'wc.createTime', + ]) + ->find(); + + if (!$chatroom) { + return null; + } + + // 格式化创建时间(兼容时间戳和日期字符串) + if (!empty($chatroom['createTime'])) { + if (is_numeric($chatroom['createTime'])) { + $chatroom['createTimeFormatted'] = date('Y-m-d H:i:s', (int)$chatroom['createTime']); + } else { + $chatroom['createTimeFormatted'] = $chatroom['createTime']; + } + } else { + $chatroom['createTimeFormatted'] = null; + } + + return $chatroom; + } } diff --git a/Server/application/cunkebao/service/TrafficPoolService.php b/Server/application/cunkebao/service/TrafficPoolService.php index a6d52d2b7..270c3c23a 100644 --- a/Server/application/cunkebao/service/TrafficPoolService.php +++ b/Server/application/cunkebao/service/TrafficPoolService.php @@ -287,8 +287,8 @@ class TrafficPoolService // 获取标签 $data['tags'] = TrafficPoolTag::getTagsByPoolCompany($poolCompanyId)->toArray(); - // 获取来源历史 - $data['sources'] = TrafficPoolSource::getSourcesByPoolCompany($poolCompanyId)->toArray(); + // 获取来源历史(带群归属信息) + $data['sources'] = TrafficPoolSource::getSourcesWithOwners($poolCompanyId); // 获取行为轨迹(最近50条) $data['behaviors'] = TrafficPoolBehavior::getUserJourney($poolCompanyId, 50)->toArray();