76 Commits

Author SHA1 Message Date
wong
a77192ebc2 Merge branch 'release/v1.1.1-a' 2025-12-01 10:17:18 +08:00
wong
181a792d0a Merge branch 'develop' of https://gitee.com/cunkebao/cunkebao_v3 into develop
# Conflicts:
#	Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx
2025-12-01 10:15:45 +08:00
wong
43f6a061e1 解决代码冲突问题 2025-12-01 10:05:26 +08:00
超级老白兔
05bba1a461 Merge branch 'develop' of https://gitee.com/cunkebao/cunkebao_v3 into develop 2025-11-28 23:00:44 +08:00
超级老白兔
97fc9959bb Refactor cache clearing functionality in settings page to improve user feedback and error handling. Update WeChat API functions to support debounce options for contact and group list retrieval. Enhance message rendering logic to include red packet messages and improve user identification in chat records. 2025-11-28 23:00:40 +08:00
wong
ad2d1c27ab 优化代码 2025-11-28 17:03:05 +08:00
wong
97144e4ebe Merge tag 'v1.1.1' into develop 2025-11-28 16:55:27 +08:00
wong
0cc30e5cb1 Merge branch 'release/v1.1.1' 2025-11-28 16:55:20 +08:00
超级老白兔
eebb73f49e Merge branch 'develop' into yongpxu-dev
# Conflicts:
#	Cunkebao/src/pages/mobile/mine/wechat-accounts/detail/index.tsx   resolved by yongpxu-dev version
2025-11-28 16:52:00 +08:00
超级老白兔
72626e3367 优化AI触发条件,确保只有对方发送的文字消息才会触发AI处理,提升消息处理逻辑的准确性。 2025-11-28 16:50:30 +08:00
wong
07a44e104d 导出表格优化兼容 2025-11-28 16:48:35 +08:00
wong
e4a8975baa 代码提交 2025-11-28 16:44:08 +08:00
超级老白兔
23de3cac7e 新增同步状态提示栏,优化消息列表组件的同步逻辑,支持逐页同步并即时更新UI,提升用户体验和代码可读性。 2025-11-28 16:32:23 +08:00
wong
0244871fc1 好友/群聊迁移优化 2025-11-28 16:04:29 +08:00
wong
5691d78004 朋友圈表格导出 2025-11-28 16:03:56 +08:00
超级老白兔
9e76d39707 移除ToContract组件中的一键转回按钮,调整代码以改善可读性和维护性。 2025-11-28 15:49:35 +08:00
wong
5087190816 代码提交 2025-11-28 15:46:21 +08:00
超级老白兔
ce47856c81 优化消息列表组件,增加加载状态的视觉反馈,支持根据微信号进行会话筛选,并重构相关逻辑以提升用户体验和代码可读性。 2025-11-28 15:33:32 +08:00
wong
a03c162962 111 2025-11-28 15:32:02 +08:00
wong
e658a32244 1 2025-11-27 18:10:23 +08:00
wong
637dcddee2 聊天消息记录状态 + 表格导出底层 2025-11-27 17:09:53 +08:00
超级老白兔
5c7940538d 更新QuickWords组件的默认选项,将活动标签从公共快捷语调整为个人快捷语,以改善用户体验。 2025-11-27 17:07:48 +08:00
超级老白兔
499b36b03b 新增公共快捷语选项并调整部门快捷语标签,优化QuickWords组件的用户体验。 2025-11-27 17:06:20 +08:00
超级老白兔
9e101e924e 新增最终消息同步处理逻辑,确保在消息发送后正确更新消息状态和相关数据。 2025-11-27 17:00:36 +08:00
超级老白兔
10f876f2f6 新增获取群聊成员列表API,优化消息记录组件以支持群聊成员信息展示,调整状态管理以防止跨联系人数据串联。 2025-11-27 16:38:28 +08:00
超级老白兔
ed4d305165 更新数据处理参数类型,调整消息处理逻辑以支持异步数据库同步,确保消息发送时正确传递群聊和个人信息。 2025-11-27 16:06:40 +08:00
超级老白兔
a5cc4e5e18 优化AI消息处理逻辑,区分AI辅助模式与AI接管模式,确保在接管模式下直接发送消息而不经过MessageEnter组件。同时,调整相关组件的状态管理,提升代码可读性和维护性。 2025-11-27 15:07:34 +08:00
超级老白兔
65da14032b 1、发起群聊全选✔ 2025-11-27 10:45:13 +08:00
wong
7e2dd2914d 健康分功能提交 + 微信客服页面改版 2025-11-26 11:17:23 +08:00
超级老白兔
644dd8912d 重构CustomerList组件样式和结构。更新了用户头像样式,以获得在线/离线状态的更好视觉反馈,添加了头像包装以保持布局一致性,并调整了在线指示器的尺寸和颜色。 2025-11-25 16:05:30 +08:00
超级老白兔
d7e08fe420 Merge branch 'yongpxu-dev' into yongpxu-dev2 2025-11-25 15:38:59 +08:00
超级老白兔
cd27506388 cf 2025-11-25 15:37:51 +08:00
超级老白兔
78de6c92b7 新增好友添加任务列表API,优化NavCommon组件,移除未使用的消息处理逻辑,简化代码结构,提升可读性和维护性。 2025-11-25 15:36:43 +08:00
wong
cd41190663 好友添加记录 2025-11-25 14:52:46 +08:00
wong
bbdfdc136b Merge branch 'develop' of https://gitee.com/cunkebao/cunkebao_v3 into develop 2025-11-24 17:19:06 +08:00
wong
5a52b56d89 代码优化 2025-11-24 17:18:49 +08:00
超级老白兔
d8d1003d7a Merge branch 'develop' of https://gitee.com/cunkebao/cunkebao_v3 into develop 2025-11-24 15:38:20 +08:00
超级老白兔
c801490c2f 删除腾讯地图定位服务修复说明文档,更新CustomerList组件样式以支持离线用户状态,调整MessageList组件的加载状态管理,增强filter工具以支持XML格式位置消息的识别。 2025-11-24 15:37:57 +08:00
wong
30b33ec974 朋友圈采集优化 2025-11-24 11:03:31 +08:00
wong
4140e6d3d7 Merge branch 'feature/rating' into develop 2025-11-24 10:14:06 +08:00
wong
1786dec900 代码优化 2025-11-24 10:13:30 +08:00
超级老白兔
bda57a84e8 优化LocationMessage组件,添加经纬度格式化功能,生成静态地图预览图URL,改进位置信息展示,增强用户体验和视觉效果。 2025-11-22 16:15:33 +08:00
超级老白兔
deac306f69 修正SelectMap组件中的经纬度标记顺序,更新XML生成逻辑以确保特殊字符转义,提升地图位置选择的准确性和稳定性。 2025-11-22 15:57:16 +08:00
超级老白兔
43b60a0049 优化SelectMap组件,增强TMap API加载和初始化逻辑,添加API可用性检查,改进标记样式创建和错误处理,提升用户体验和稳定性。 2025-11-22 14:16:40 +08:00
超级老白兔
ab2ee050de 优化SelectMap组件,更新IP定位服务的实现,增强错误处理逻辑,确保用户位置获取更加准确和稳定,同时调整样式以提升用户体验。 2025-11-22 11:53:54 +08:00
超级老白兔
21de87ca32 更新SelectMap组件以使用新版腾讯地图API,优化地图加载和用户位置获取逻辑,增强错误处理和用户体验。 2025-11-22 11:45:53 +08:00
超级老白兔
782031cbc6 增强SelectMap组件,添加用户当前位置获取功能,优化地址解析逻辑和错误处理,改进加载状态显示,提升用户体验。 2025-11-21 19:02:41 +08:00
超级老白兔
6c9551be05 腾讯地图功能锁定 2025-11-21 18:30:12 +08:00
wong
36eb67a217 客服评分体系优化 2025-11-21 17:22:33 +08:00
超级老白兔
7dcbcfacc1 重置一下 2025-11-21 16:33:02 +08:00
超级老白兔
2c9bda734e 重构SelectMap组件,集成腾讯地图API,替换AMap。更新了标记处理和地址检索逻辑,以利用腾讯的地理编码和位置建议服务。增强的错误处理和加载状态,以改善用户体验。 2025-11-21 15:51:50 +08:00
超级老白兔
d296d019e5 Enhance MessageEnter component by adding a map selection feature. Introduced a new button for map visibility and integrated SelectMap component for improved user interaction. 2025-11-21 14:55:56 +08:00
wong
ccf947c64d Merge branch 'release/rating' into develop 2025-11-21 14:47:42 +08:00
wong
3faee0d8be 客服健康分提交 2025-11-21 14:38:29 +08:00
超级老白兔
fe12f78c1d 优化MessageRecord组件的自动滚动逻辑,确保在新消息到达且没有视频状态变化时才滚动到底部,同时修复了对消息长度的引用以提高性能。 2025-11-21 10:34:55 +08:00
超级老白兔
a2b0041490 更新NavCommon组件中的按钮图标和文本,将内容管理按钮的图标更改为SendOutlined,并将其文本更改为“发朋友圈”。同时,调整powerCenter模块中的数据结构,注释掉内容库配置部分,并优化样式代码。更新ContentManagement组件的标题为“发朋友圈”。 2025-11-20 16:50:20 +08:00
wong
235e3ed994 代码优化 2025-11-20 16:18:16 +08:00
超级老白兔
84ced0c32a 优化ProfileModules组件中的状态管理,使用选择器函数订阅匹配的客服对象,避免不必要的重新渲染。同时在DetailValue组件中引入useRef进行深度比较,确保只有在值真正变化时才更新编辑状态。 2025-11-20 16:13:57 +08:00
wong
beabe1c191 Merge branch 'develop' of https://gitee.com/cunkebao/cunkebao_v3 into release/friend 2025-11-20 16:12:09 +08:00
wong
719ed98996 代码提交 2025-11-20 16:11:27 +08:00
wong
b0a60e428c 账号评分 2025-11-20 16:07:57 +08:00
超级老白兔
16a8656b49 从消息推送助手任务创建中删除弃用的提示示例,更新推送历史API以使用pushTypeCode和状态,并通过改进的类型处理和UI调整来增强PushHistory组件。 2025-11-20 15:52:05 +08:00
超级老白兔
7fdb993f57 Merge branch 'develop' of https://gitee.com/cunkebao/cunkebao_v3 into develop 2025-11-20 14:59:03 +08:00
超级老白兔
0e56f0808c 在NavCommon组件中实现缓存清除功能,包括localStorage、sessionStorage和IndexedDB。在缓存清除期间添加加载消息并相应地更新菜单选项。 2025-11-20 14:57:18 +08:00
超级老白兔
bd45a9252a 存一下 2025-11-20 14:32:24 +08:00
wong
57d85765c3 1 2025-11-20 10:43:29 +08:00
wong
067ae0fd10 1 2025-11-20 10:36:17 +08:00
wong
bf421e1a49 1 2025-11-19 17:58:54 +08:00
wong
339f06e630 1 2025-11-19 17:53:03 +08:00
wong
367a250fba 代码优化 2025-11-19 15:52:47 +08:00
wong
73110acb37 Merge branch 'develop' of https://gitee.com/cunkebao/cunkebao_v3 into wong-dev 2025-11-19 15:32:43 +08:00
wong
0912a0067e 内容库优化 2025-11-19 15:28:24 +08:00
wong
5faf83e437 内容库优化 2025-11-19 15:02:10 +08:00
超级老白兔
3f76f37a4c 从pnpm-lock.yaml中删除dexie依赖项,并更新PushHistory组件中的列标题,以提高清晰度。 2025-11-19 14:39:03 +08:00
wong
f0574ef555 Merge tag 'v1.1.0' into develop 2025-11-19 10:38:29 +08:00
wong
273ea73b1b 代码优化 2025-11-19 10:22:19 +08:00
129 changed files with 19939 additions and 2434 deletions

2
.gitignore vendored
View File

@@ -5,9 +5,11 @@ Store_vue/node_modules/
Cunkebao/.specstory/
*.cursorindexingignore
Server/.specstory/
Server/thinkphp/
Store_vue/.specstory/
Store_vue/unpackage/
Store_vue/.vscode/
SuperAdmin/.specstory/
Cunkebao/dist
Touchkebao/.specstory/
Serverruntime/

View File

@@ -26,9 +26,6 @@ importers:
dayjs:
specifier: ^1.11.13
version: 1.11.13
dexie:
specifier: ^4.2.0
version: 4.2.0
echarts:
specifier: ^5.6.0
version: 5.6.0
@@ -1070,9 +1067,6 @@ packages:
engines: {node: '>=0.10'}
hasBin: true
dexie@4.2.0:
resolution: {integrity: sha512-OSeyyWOUetDy9oFWeddJgi83OnRA3hSFh3RrbltmPgqHszE9f24eUCVLI4mPg0ifsWk0lQTdnS+jyGNrPMvhDA==}
dir-glob@3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'}
@@ -3410,8 +3404,6 @@ snapshots:
detect-libc@1.0.3:
optional: true
dexie@4.2.0: {}
dir-glob@3.0.1:
dependencies:
path-type: 4.0.0

View File

@@ -3,6 +3,8 @@ import {
GetContentItemListParams,
CreateContentItemParams,
UpdateContentItemParams,
AIRewriteParams,
ReplaceContentParams,
} from "./data";
// 获取素材列表
@@ -35,3 +37,13 @@ export function deleteContentItem(id: string) {
export function getContentLibraryDetail(id: string) {
return request("/v1/content/library/detail", { id }, "GET");
}
// AI改写内容
export function aiRewriteContent(params: AIRewriteParams) {
return request("/v1/content/library/aiEditContent", params, "GET");
}
// 替换原内容
export function replaceContent(params: ReplaceContentParams) {
return request("/v1/content/library/aiEditContent", params, "POST");
}

View File

@@ -26,6 +26,7 @@ export interface ContentItem {
delTime: number;
wechatChatroomId?: string | null;
senderNickname: string;
senderAvatar?: string | null;
createMessageTime?: string | null;
comment: string;
sendTime: number;
@@ -104,3 +105,15 @@ export interface UpdateContentItemParams
extends Partial<CreateContentItemParams> {
id: string;
}
// AI改写参数
export interface AIRewriteParams {
id: string;
aiPrompt: string;
}
// 替换内容参数
export interface ReplaceContentParams {
id: string;
content: string;
}

View File

@@ -131,6 +131,25 @@
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
}
.avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-icon-wrapper {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
left: 0;
}
.avatar-icon {
@@ -613,3 +632,147 @@
line-height: 1.6;
}
}
// AI改写弹框样式
.ai-popup-content {
padding: 20px;
max-height: 100vh;
overflow-y: auto;
background: #f9fbfd;
.ai-popup-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid #e8f4ff;
h3 {
font-size: 18px;
font-weight: 600;
color: #1677ff;
margin: 0;
display: flex;
align-items: center;
&::before {
content: '';
display: inline-block;
width: 4px;
height: 18px;
background: #1677ff;
margin-right: 8px;
border-radius: 2px;
}
}
}
.ai-form {
.ai-form-item {
margin-bottom: 20px;
.ai-form-label {
font-size: 15px;
font-weight: 500;
color: #333;
margin-bottom: 6px;
display: flex;
align-items: center;
&::before {
content: '';
display: inline-block;
width: 3px;
height: 14px;
background: #1677ff;
margin-right: 6px;
border-radius: 2px;
}
}
.ai-result-description {
font-size: 12px;
color: #999;
margin-bottom: 10px;
}
}
.ai-submit {
margin: 24px 0;
button {
height: 44px;
font-size: 16px;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(22, 119, 255, 0.2);
}
}
.ai-result-box {
background: #ffffff;
border: 1px solid #e0f0ff;
border-radius: 8px;
padding: 16px;
min-height: 100px;
max-height: 200px;
overflow-y: auto;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
.ai-loading {
display: flex;
justify-content: center;
align-items: center;
height: 120px;
}
.ai-result-content {
font-size: 15px;
line-height: 1.8;
color: #333;
white-space: pre-wrap;
padding: 4px;
}
.ai-result-placeholder {
color: #999;
text-align: center;
padding: 30px 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.placeholder-icon {
font-size: 28px;
margin-bottom: 10px;
}
.placeholder-text {
font-size: 14px;
color: #999;
}
}
}
.ai-replace-action {
margin-top: 20px;
button {
height: 44px;
font-size: 16px;
border-radius: 8px;
background: #1677ff;
border-color: #1677ff;
box-shadow: 0 2px 6px rgba(22, 119, 255, 0.2);
color: #ffffff;
&:hover, &:focus {
background: #4096ff;
border-color: #4096ff;
color: #ffffff;
}
}
}
}
}

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect, useCallback } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Toast, SpinLoading, Dialog, Card } from "antd-mobile";
import { Input, Pagination, Button } from "antd";
import { Toast, SpinLoading, Dialog, Card, Popup, TextArea } from "antd-mobile";
import { Input, Pagination, Button, Spin } from "antd";
import {
PlusOutlined,
SearchOutlined,
@@ -18,7 +18,7 @@ import {
} from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import NavCommon from "@/components/NavCommon";
import { getContentItemList, deleteContentItem } from "./api";
import { getContentItemList, deleteContentItem, aiRewriteContent, replaceContent } from "./api";
import { ContentItem } from "./data";
import style from "./index.module.scss";
@@ -42,6 +42,14 @@ const MaterialsList: React.FC = () => {
const [total, setTotal] = useState(0);
const pageSize = 20;
// AI改写相关状态
const [showAIRewritePopup, setShowAIRewritePopup] = useState(false);
const [currentMaterial, setCurrentMaterial] = useState<ContentItem | null>(null);
const [aiPrompt, setAiPrompt] = useState("");
const [aiResult, setAiResult] = useState("");
const [aiLoading, setAiLoading] = useState(false);
const [replaceLoading, setReplaceLoading] = useState(false);
// 获取素材列表
const fetchMaterials = useCallback(async () => {
if (!id) return;
@@ -104,9 +112,75 @@ const MaterialsList: React.FC = () => {
}
};
const handleView = (materialId: number) => {
// 可以跳转到素材详情页面或显示弹窗
console.log("查看素材:", materialId);
const handleAIRewrite = (material: ContentItem) => {
setCurrentMaterial(material);
setAiPrompt("重写这条朋友圈 要求: 1、原本的字数和意思不要修改超过10% 2、出现品牌名或个人名字就去除 3、适当的换行及加些表情点缀");
setAiResult("");
setShowAIRewritePopup(true);
};
const handleSubmitAIRewrite = async () => {
if (!currentMaterial) return;
try {
setAiLoading(true);
const response = await aiRewriteContent({
id: currentMaterial.id.toString(),
aiPrompt: aiPrompt
});
setAiResult(response.contentAfter || "暂无改写结果");
// 可以在这里显示原内容和改写后内容的对比
console.log("原内容:", response.contentFront);
console.log("改写后内容:", response.contentAfter);
} catch (error) {
console.error("AI改写失败:", error);
Toast.show({
content: "AI改写失败请重试",
position: "top",
});
} finally {
setAiLoading(false);
}
};
const handleReplaceContent = async () => {
if (!currentMaterial || !aiResult) return;
try {
setReplaceLoading(true);
await replaceContent({
id: currentMaterial.id.toString(),
content: aiResult
});
Toast.show({
content: "内容已成功替换",
position: "top",
});
// 刷新素材列表
fetchMaterials();
// 关闭弹窗
closeAIRewritePopup();
} catch (error) {
console.error("替换内容失败:", error);
Toast.show({
content: "替换内容失败,请重试",
position: "top",
});
} finally {
setReplaceLoading(false);
}
};
const closeAIRewritePopup = () => {
setShowAIRewritePopup(false);
setCurrentMaterial(null);
setAiPrompt("");
setAiResult("");
};
const handleRefresh = () => {
@@ -348,7 +422,23 @@ const MaterialsList: React.FC = () => {
<div className={style["card-header"]}>
<div className={style["avatar-section"]}>
<div className={style["avatar"]}>
<UserOutlined className={style["avatar-icon"]} />
{material.senderAvatar ? (
<img
src={material.senderAvatar}
alt="头像"
className={style["avatar-img"]}
onError={(e) => {
e.currentTarget.style.display = 'none';
const nextElement = e.currentTarget.nextSibling as HTMLElement;
if (nextElement) {
nextElement.style.display = 'flex';
}
}}
/>
) : null}
<div className={style["avatar-icon-wrapper"]} style={{display: material.senderAvatar ? 'none' : 'flex'}}>
<UserOutlined className={style["avatar-icon"]}/>
</div>
</div>
<div className={style["header-info"]}>
<span className={style["creator-name"]}>
@@ -381,7 +471,7 @@ const MaterialsList: React.FC = () => {
</Button>
<Button
onClick={() => handleView(material.id)}
onClick={() => handleAIRewrite(material)}
className={style["action-btn"]}
>
<BarChartOutlined />
@@ -402,6 +492,100 @@ const MaterialsList: React.FC = () => {
)}
</div>
</div>
{/* AI改写弹框 */}
<Popup
visible={showAIRewritePopup}
onMaskClick={closeAIRewritePopup}
bodyStyle={{
borderRadius: "16px 16px 0 0",
maxHeight: "90vh",
boxShadow: "0 -4px 12px rgba(0, 0, 0, 0.1)"
}}
>
<div className={style["ai-popup-content"]}>
<div className={style["ai-popup-header"]}>
<h3>AI内容改写</h3>
<Button
size="small"
onClick={closeAIRewritePopup}
>
</Button>
</div>
<div className={style["ai-form"]}>
{/* 提示词输入区 */}
<div className={style["ai-form-item"]}>
<div className={style["ai-form-label"]}></div>
<div className={style["ai-form-control"]}>
<TextArea
placeholder="请输入提示词指导AI如何改写内容"
value={aiPrompt}
onChange={val => setAiPrompt(val)}
rows={4}
showCount
maxLength={500}
style={{
border: "1px solid #d9e8ff",
borderRadius: "8px",
padding: "12px",
fontSize: "14px"
}}
/>
</div>
</div>
<div className={style["ai-submit"]}>
<Button
block
color="primary"
onClick={handleSubmitAIRewrite}
loading={aiLoading}
disabled={aiLoading || !aiPrompt.trim()}
>
{aiLoading ? "生成中..." : "生成内容"}
</Button>
</div>
{/* 改写结果区 */}
<div className={style["ai-form-item"]}>
<div className={style["ai-form-label"]}></div>
<div className={style["ai-result-description"]}>AI生成的内容将显示在下方区域</div>
<div className={style["ai-result-box"]}>
{aiLoading ? (
<div className={style["ai-loading"]}>
<Spin tip="AI正在思考中..." />
</div>
) : aiResult ? (
<div className={style["ai-result-content"]}>
{aiResult}
</div>
) : (
<div className={style["ai-result-placeholder"]}>
<div className={style["placeholder-icon"]}></div>
<div className={style["placeholder-text"]}>"生成内容"AI改写结果</div>
</div>
)}
</div>
{/* 替换按钮 */}
{aiResult && (
<div className={style["ai-replace-action"]}>
<Button
block
color="primary"
onClick={handleReplaceContent}
loading={replaceLoading}
disabled={replaceLoading || !aiResult}
>
{replaceLoading ? "替换中..." : "替换原内容"}
</Button>
</div>
)}
</div>
</div>
</div>
</Popup>
</Layout>
);
};

View File

@@ -8,7 +8,6 @@ import {
LogoutOutlined,
SettingOutlined,
LockOutlined,
ReloadOutlined,
} from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import { useUserStore } from "@/store/module/user";
@@ -16,7 +15,7 @@ import { useSettingsStore } from "@/store/module/settings";
import style from "./index.module.scss";
import NavCommon from "@/components/NavCommon";
import { sendMessageToParent, TYPE_EMUE } from "@/utils/postApp";
import { updateChecker } from "@/utils/updateChecker";
import { clearApplicationCache } from "@/utils/cacheCleaner";
interface SettingItem {
id: string;
@@ -58,13 +57,35 @@ const Setting: React.FC = () => {
const handleClearCache = () => {
Dialog.confirm({
content: "确定要清除缓存吗?这将清除所有本地数据。",
onConfirm: () => {
sendMessageToParent(
{
action: "clearCache",
},
TYPE_EMUE.FUNCTION,
);
onConfirm: async () => {
const handler = Toast.show({
icon: "loading",
content: "正在清理缓存...",
duration: 0,
});
try {
await clearApplicationCache();
sendMessageToParent(
{
action: "clearCache",
},
TYPE_EMUE.FUNCTION,
);
handler.close();
Toast.show({
icon: "success",
content: "缓存清理完成",
position: "top",
});
} catch (error) {
console.error("clear cache failed", error);
handler.close();
Toast.show({
icon: "fail",
content: "缓存清理失败,请稍后再试",
position: "top",
});
}
},
});
};

View File

@@ -1,10 +1,26 @@
import request from "@/api/request";
import axios from "axios";
import { useUserStore } from "@/store/module/user";
// 获取微信号详情
export function getWechatAccountDetail(id: string) {
return request("/v1/wechats/getWechatInfo", { wechatId: id }, "GET");
}
// 获取微信号概览数据
export function getWechatAccountOverview(id: string) {
return request("/v1/wechats/overview", { wechatId: id }, "GET");
}
// 获取微信号朋友圈列表
export function getWechatMoments(params: {
wechatId: string;
page?: number;
limit?: number;
}) {
return request("/v1/wechats/moments", params, "GET");
}
// 获取微信号好友列表
export function getWechatFriends(params: {
wechatAccount: string;
@@ -36,3 +52,68 @@ export function transferWechatFriends(params: {
}) {
return request("/v1/wechats/transfer-friends", params, "POST");
}
// 导出朋友圈接口(直接下载文件)
export async function exportWechatMoments(params: {
wechatId: string;
keyword?: string;
type?: number;
startTime?: string;
endTime?: string;
}): Promise<void> {
const { token } = useUserStore.getState();
const baseURL =
(import.meta as any).env?.VITE_API_BASE_URL || "/api";
// 构建查询参数
const queryParams = new URLSearchParams();
queryParams.append("wechatId", params.wechatId);
if (params.keyword) {
queryParams.append("keyword", params.keyword);
}
if (params.type !== undefined) {
queryParams.append("type", params.type.toString());
}
if (params.startTime) {
queryParams.append("startTime", params.startTime);
}
if (params.endTime) {
queryParams.append("endTime", params.endTime);
}
try {
const response = await axios.get(
`${baseURL}/v1/wechats/moments/export?${queryParams.toString()}`,
{
responseType: "blob",
headers: {
Authorization: token ? `Bearer ${token}` : undefined,
},
}
);
// 创建下载链接
const blob = new Blob([response.data]);
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
// 从响应头获取文件名,如果没有则使用默认文件名
const contentDisposition = response.headers["content-disposition"];
let fileName = "朋友圈导出.xlsx";
if (contentDisposition) {
const fileNameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
if (fileNameMatch && fileNameMatch[1]) {
fileName = decodeURIComponent(fileNameMatch[1].replace(/['"]/g, ""));
}
}
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (error: any) {
throw new Error(error.response?.data?.message || error.message || "导出失败");
}
}

View File

@@ -1,3 +1,41 @@
// 概览数据接口
export interface WechatAccountOverview {
healthScoreAssessment: {
score: number;
dailyLimit: number;
todayAdded: number;
lastAddTime: string;
statusTag: string;
baseComposition?: Array<{
name: string;
score: number;
formatted: string;
friendCount?: number;
}>;
dynamicRecords?: Array<{
title?: string;
description?: string;
time?: string;
score?: number;
formatted?: string;
statusTag?: string;
}>;
};
accountValue: {
value: number;
formatted: string;
};
todayValueChange: {
change: number;
formatted: string;
isPositive: boolean;
};
totalFriends: number;
todayNewFriends: number;
highValueChatrooms: number;
todayNewChatrooms: number;
}
export interface WechatAccountSummary {
accountAge: string;
activityLevel: {
@@ -15,12 +53,51 @@ export interface WechatAccountSummary {
todayAdded: number;
addLimit: number;
};
healthScore?: {
score: number;
lastUpdate?: string;
lastAddTime?: string;
baseScore?: number;
verifiedScore?: number;
friendsScore?: number;
activities?: {
type: string;
time?: string;
score: number;
description?: string;
status?: string;
}[];
};
moments?: {
id: string;
date: string;
month: string;
day: string;
content: string;
images?: string[];
timeAgo?: string;
hasEmoji?: boolean;
}[];
accountValue?: {
value: number;
todayChange?: number;
};
friendsCount?: {
total: number;
todayAdded?: number;
};
groupsCount?: {
total: number;
todayAdded?: number;
};
restrictions: {
id: number;
level: number;
reason: string;
date: string;
}[];
// 新增概览数据
overview?: WechatAccountOverview;
}
export interface Friend {
@@ -39,6 +116,27 @@ export interface Friend {
region: string;
source: string;
notes: string;
value?: number;
valueFormatted?: string;
statusTags?: string[];
}
export interface MomentItem {
id: string;
snsId: string;
type: number;
content: string;
resUrls: string[];
commentList?: any[];
likeList?: any[];
createTime: string;
momentEntity?: {
lat?: string;
lng?: string;
location?: string;
picSize?: number;
userName?: string;
};
}
export interface WechatFriendDetail {

View File

@@ -11,6 +11,7 @@ import {
Avatar,
Tag,
Switch,
DatePicker,
} from "antd-mobile";
import { Input, Pagination } from "antd";
import NavCommon from "@/components/NavCommon";
@@ -21,11 +22,18 @@ import {
} from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import style from "./detail.module.scss";
import { getWechatAccountDetail, getWechatFriends, transferWechatFriends } from "./api";
import {
getWechatAccountDetail,
getWechatFriends,
transferWechatFriends,
getWechatAccountOverview,
getWechatMoments,
exportWechatMoments,
} from "./api";
import DeviceSelection from "@/components/DeviceSelection";
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
import { WechatAccountSummary, Friend } from "./data";
import { WechatAccountSummary, Friend, MomentItem } from "./data";
const WechatAccountDetail: React.FC = () => {
const { id } = useParams<{ id: string }>();
@@ -34,6 +42,7 @@ const WechatAccountDetail: React.FC = () => {
const [accountSummary, setAccountSummary] =
useState<WechatAccountSummary | null>(null);
const [accountInfo, setAccountInfo] = useState<any>(null);
const [overviewData, setOverviewData] = useState<any>(null);
const [showRestrictions, setShowRestrictions] = useState(false);
const [showTransferConfirm, setShowTransferConfirm] = useState(false);
const [selectedDevices, setSelectedDevices] = useState<DeviceSelectionItem[]>([]);
@@ -50,6 +59,22 @@ const WechatAccountDetail: React.FC = () => {
const [isFetchingFriends, setIsFetchingFriends] = useState(false);
const [hasFriendLoadError, setHasFriendLoadError] = useState(false);
const [isFriendsEmpty, setIsFriendsEmpty] = useState(false);
const [moments, setMoments] = useState<MomentItem[]>([]);
const [momentsPage, setMomentsPage] = useState(1);
const [momentsTotal, setMomentsTotal] = useState(0);
const [isFetchingMoments, setIsFetchingMoments] = useState(false);
const [momentsError, setMomentsError] = useState<string | null>(null);
const MOMENTS_LIMIT = 10;
// 导出相关状态
const [showExportPopup, setShowExportPopup] = useState(false);
const [exportKeyword, setExportKeyword] = useState("");
const [exportType, setExportType] = useState<number | undefined>(undefined);
const [exportStartTime, setExportStartTime] = useState<Date | null>(null);
const [exportEndTime, setExportEndTime] = useState<Date | null>(null);
const [showStartTimePicker, setShowStartTimePicker] = useState(false);
const [showEndTimePicker, setShowEndTimePicker] = useState(false);
const [exportLoading, setExportLoading] = useState(false);
// 获取基础信息
const fetchAccountInfo = useCallback(async () => {
@@ -80,6 +105,19 @@ const WechatAccountDetail: React.FC = () => {
}
}, [id]);
// 获取概览数据
const fetchOverviewData = useCallback(async () => {
if (!id) return;
try {
const response = await getWechatAccountOverview(id);
if (response) {
setOverviewData(response);
}
} catch (e) {
console.error("获取概览数据失败:", e);
}
}, [id]);
// 获取好友列表 - 封装为独立函数
const fetchFriendsList = useCallback(
async (page: number = 1, keyword: string = "") => {
@@ -96,26 +134,44 @@ const WechatAccountDetail: React.FC = () => {
keyword: keyword,
});
const newFriends = response.list.map((friend: any) => ({
id: friend.id.toString(),
avatar: friend.avatar || "/placeholder.svg",
nickname: friend.nickname || "未知用户",
wechatId: friend.wechatId || "",
remark: friend.memo || "",
addTime: friend.createTime || new Date().toISOString().split("T")[0],
lastInteraction:
friend.lastInteraction || new Date().toISOString().split("T")[0],
tags: friend.tags
? friend.tags.map((tag: string, index: number) => ({
id: `tag-${index}`,
name: tag,
color: getRandomTagColor(),
}))
: [],
region: friend.region || "未知",
source: friend.source || "未知",
notes: friend.notes || "",
}));
const newFriends = response.list.map((friend: any) => {
const memoTags = Array.isArray(friend.memo)
? friend.memo
: friend.memo
? String(friend.memo)
.split(/[,\s、]+/)
.filter(Boolean)
: [];
const tagList = Array.isArray(friend.tags)
? friend.tags
: friend.tags
? [friend.tags]
: [];
return {
id: friend.id.toString(),
avatar: friend.avatar || "/placeholder.svg",
nickname: friend.nickname || "未知用户",
wechatId: friend.wechatId || "",
remark: friend.notes || "",
addTime:
friend.createTime || new Date().toISOString().split("T")[0],
lastInteraction:
friend.lastInteraction || new Date().toISOString().split("T")[0],
tags: memoTags.map((tag: string, index: number) => ({
id: `tag-${index}`,
name: tag,
color: getRandomTagColor(),
})),
statusTags: tagList,
region: friend.region || "未知",
source: friend.source || "未知",
notes: friend.notes || "",
value: friend.value,
valueFormatted: friend.valueFormatted,
};
});
setFriends(newFriends);
setFriendsTotal(response.total);
@@ -137,6 +193,46 @@ const WechatAccountDetail: React.FC = () => {
[id],
);
const fetchMomentsList = useCallback(
async (page: number = 1, append: boolean = false) => {
if (!id) return;
setIsFetchingMoments(true);
setMomentsError(null);
try {
const response = await getWechatMoments({
wechatId: id,
page,
limit: MOMENTS_LIMIT,
});
const list: MomentItem[] = (response.list || []).map((moment: any) => ({
id: moment.id?.toString() || Math.random().toString(),
snsId: moment.snsId,
type: moment.type,
content: moment.content || "",
resUrls: moment.resUrls || [],
commentList: moment.commentList || [],
likeList: moment.likeList || [],
createTime: moment.createTime || "",
momentEntity: moment.momentEntity || {},
}));
setMoments(prev => (append ? [...prev, ...list] : list));
setMomentsTotal(response.total || list.length);
setMomentsPage(page);
} catch (error) {
console.error("获取朋友圈数据失败:", error);
setMomentsError("获取朋友圈数据失败");
if (!append) {
setMoments([]);
}
} finally {
setIsFetchingMoments(false);
}
},
[id],
);
// 搜索好友
const handleSearch = useCallback(() => {
setFriendsPage(1);
@@ -161,8 +257,9 @@ const WechatAccountDetail: React.FC = () => {
useEffect(() => {
if (id) {
fetchAccountInfo();
fetchOverviewData();
}
}, [id, fetchAccountInfo]);
}, [id, fetchAccountInfo, fetchOverviewData]);
// 监听标签切换 - 只在切换到好友列表时请求一次
useEffect(() => {
@@ -173,6 +270,14 @@ const WechatAccountDetail: React.FC = () => {
}
}, [activeTab, id, fetchFriendsList, searchQuery]);
useEffect(() => {
if (activeTab === "moments" && id) {
if (moments.length === 0) {
fetchMomentsList(1, false);
}
}
}, [activeTab, id, fetchMomentsList, moments.length]);
// 工具函数
const getRandomTagColor = (): string => {
const colors = [
@@ -271,6 +376,85 @@ const WechatAccountDetail: React.FC = () => {
navigate(`/mine/traffic-pool/detail/${friend.wechatId}/${friend.id}`);
};
const handleLoadMoreMoments = () => {
if (isFetchingMoments) return;
if (moments.length >= momentsTotal) return;
fetchMomentsList(momentsPage + 1, true);
};
// 处理朋友圈导出
const handleExportMoments = useCallback(async () => {
if (!id) {
Toast.show({ content: "微信ID不存在", position: "top" });
return;
}
setExportLoading(true);
try {
// 格式化时间
const formatDate = (date: Date | null): string | undefined => {
if (!date) return undefined;
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
};
await exportWechatMoments({
wechatId: id,
keyword: exportKeyword || undefined,
type: exportType,
startTime: formatDate(exportStartTime),
endTime: formatDate(exportEndTime),
});
Toast.show({ content: "导出成功", position: "top" });
setShowExportPopup(false);
// 重置筛选条件
setExportKeyword("");
setExportType(undefined);
setExportStartTime(null);
setExportEndTime(null);
} catch (error: any) {
console.error("导出失败:", error);
Toast.show({
content: error.message || "导出失败,请重试",
position: "top",
});
} finally {
setExportLoading(false);
}
}, [id, exportKeyword, exportType, exportStartTime, exportEndTime]);
const formatMomentDateParts = (dateString: string) => {
const date = new Date(dateString);
if (Number.isNaN(date.getTime())) {
return { day: "--", month: "--" };
}
const day = date.getDate().toString().padStart(2, "0");
const month = `${date.getMonth() + 1}`;
return { day, month };
};
const formatMomentTimeAgo = (dateString: string) => {
const date = new Date(dateString);
if (Number.isNaN(date.getTime())) {
return dateString || "--";
}
const diff = Date.now() - date.getTime();
const minutes = Math.floor(diff / (1000 * 60));
if (minutes < 1) return "刚刚";
if (minutes < 60) return `${minutes}分钟前`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}小时前`;
const days = Math.floor(hours / 24);
if (days < 7) return `${days}天前`;
return date.toLocaleDateString("zh-CN", {
month: "2-digit",
day: "2-digit",
});
};
return (
<Layout header={<NavCommon title="微信号详情" />} loading={loadingInfo}>
<div className={style["wechat-account-detail-page"]}>
@@ -313,73 +497,223 @@ const WechatAccountDetail: React.FC = () => {
onChange={handleTabChange}
className={style["tabs"]}
>
<Tabs.Tab title="账号概览" key="overview">
<Tabs.Tab title="概览" key="overview">
<div className={style["overview-content"]}>
<div className={style["summary-grid"]}>
<div className={style["summary-item"]}>
<div className={style["summary-value"]}>
{accountInfo?.friendShip?.totalFriend ?? "-"}
{/* 健康分评估区域 */}
<div className={style["health-score-section"]}>
<div className={style["health-score-title"]}></div>
<div className={style["health-score-info"]}>
<div className={style["health-score-status"]}>
<span className={style["status-tag"]}>{overviewData?.healthScoreAssessment?.statusTag || "已添加加人"}</span>
<span className={style["status-time"]}>: {overviewData?.healthScoreAssessment?.lastAddTime || "18:44:14"}</span>
</div>
<div className={style["summary-label"]}></div>
</div>
<div className={style["summary-item"]}>
<div className={style["summary-value-green"]}>
+{accountSummary?.statistics.todayAdded ?? "-"}
<div className={style["health-score-display"]}>
<div className={style["score-circle-wrapper"]}>
<div className={style["score-circle"]}>
<div className={style["score-number"]}>
{overviewData?.healthScoreAssessment?.score || 67}
</div>
<div className={style["score-label"]}>SCORE</div>
</div>
</div>
<div className={style["health-score-stats"]}>
<div className={style["stats-row"]}>
<div className={style["stats-label"]}></div>
<div className={style["stats-value"]}>{overviewData?.healthScoreAssessment?.dailyLimit || 0} </div>
</div>
<div className={style["stats-row"]}>
<div className={style["stats-label"]}></div>
<div className={style["stats-value"]}>{overviewData?.healthScoreAssessment?.todayAdded || 0} </div>
</div>
</div>
</div>
<div className={style["summary-label"]}></div>
</div>
</div>
<div className={style["summary-progress-row"]}>
<span>:</span>
<span className={style["summary-progress-text"]}>
{accountSummary?.statistics.todayAdded ?? 0}/
{accountSummary?.statistics.addLimit ?? 0}
</span>
</div>
<div className={style["summary-progress-bar"]}>
<div className={style["progress-bg"]}>
<div
className={style["progress-fill"]}
style={{
width: `${Math.min(((accountSummary?.statistics.todayAdded ?? 0) / (accountSummary?.statistics.addLimit || 1)) * 100, 100)}%`,
}}
/>
</div>
</div>
<div className={style["summary-grid"]}>
<div className={style["summary-item"]}>
<div className={style["summary-value-blue"]}>
{accountInfo?.friendShip?.groupNumber ?? "-"}
{/* 账号价值和好友数量区域 */}
<div className={style["account-stats-grid"]}>
{/* 账号价值 */}
<div className={style["stat-card"]}>
<div className={style["stat-header"]}>
<div className={style["stat-title"]}></div>
<div className={style["stat-icon-up"]}></div>
</div>
<div className={style["summary-label"]}></div>
</div>
<div className={style["summary-item"]}>
<div className={style["summary-value-green"]}>
{accountInfo?.activity?.yesterdayMsgCount ?? "-"}
<div className={style["stat-value"]}>
{overviewData?.accountValue?.formatted || `¥${overviewData?.accountValue?.value || "29,800"}`}
</div>
</div>
{/* 今日价值变化 */}
<div className={style["stat-card"]}>
<div className={style["stat-header"]}>
<div className={style["stat-title"]}></div>
<div className={style["stat-icon-plus"]}></div>
</div>
<div className={style["stat-value-positive"]}>
{overviewData?.todayValueChange?.formatted || `+${overviewData?.todayValueChange?.change || "500"}`}
</div>
<div className={style["summary-label"]}></div>
</div>
</div>
<div className={style["device-card"]}>
<div className={style["device-title"]}></div>
<div className={style["device-row"]}>
<span className={style["device-label"]}>:</span>
<span>{accountInfo?.deviceName ?? "-"}</span>
{/* 好友数量和今日新增好友区域 */}
<div className={style["account-stats-grid"]}>
{/* 好友总数 */}
<div className={style["stat-card"]}>
<div className={style["stat-header"]}>
<div className={style["stat-title"]}></div>
<div className={style["stat-icon-people"]}></div>
</div>
<div className={style["stat-value"]}>
{overviewData?.totalFriends || accountInfo?.friendShip?.totalFriend || "0"}
</div>
</div>
<div className={style["device-row"]}>
<span className={style["device-label"]}>:</span>
<span>{accountInfo?.deviceType ?? "-"}</span>
{/* 今日新增好友 */}
<div className={style["stat-card"]}>
<div className={style["stat-header"]}>
<div className={style["stat-title"]}></div>
<div className={style["stat-icon-plus"]}></div>
</div>
<div className={style["stat-value-positive"]}>
+{overviewData?.todayNewFriends || accountSummary?.statistics.todayAdded || "0"}
</div>
</div>
<div className={style["device-row"]}>
<span className={style["device-label"]}>:</span>
<span>{accountInfo?.deviceVersion ?? "-"}</span>
</div>
{/* 高价群聊区域 */}
<div className={style["account-stats-grid"]}>
{/* 高价群聊 */}
<div className={style["stat-card"]}>
<div className={style["stat-header"]}>
<div className={style["stat-title"]}></div>
<div className={style["stat-icon-chat"]}></div>
</div>
<div className={style["stat-value"]}>
{overviewData?.highValueChatrooms || accountInfo?.friendShip?.groupNumber || "0"}
</div>
</div>
{/* 今日新增群聊 */}
<div className={style["stat-card"]}>
<div className={style["stat-header"]}>
<div className={style["stat-title"]}></div>
<div className={style["stat-icon-plus"]}></div>
</div>
<div className={style["stat-value-positive"]}>
+{overviewData?.todayNewChatrooms || "0"}
</div>
</div>
</div>
</div>
</Tabs.Tab>
<Tabs.Tab title="健康分" key="health">
<div className={style["health-content"]}>
{/* 健康分评估区域 */}
<div className={style["health-score-section"]}>
<div className={style["health-score-title"]}></div>
<div className={style["health-score-info"]}>
<div className={style["health-score-status"]}>
<span className={style["status-tag"]}>{overviewData?.healthScoreAssessment?.statusTag || "已添加加人"}</span>
<span className={style["status-time"]}>: {overviewData?.healthScoreAssessment?.lastAddTime || "18:44:14"}</span>
</div>
<div className={style["health-score-display"]}>
<div className={style["score-circle-wrapper"]}>
<div className={style["score-circle"]}>
<div className={style["score-number"]}>
{overviewData?.healthScoreAssessment?.score || 67}
</div>
<div className={style["score-label"]}>SCORE</div>
</div>
</div>
<div className={style["health-score-stats"]}>
<div className={style["stats-row"]}>
<div className={style["stats-label"]}></div>
<div className={style["stats-value"]}>{overviewData?.healthScoreAssessment?.dailyLimit || 0} </div>
</div>
<div className={style["stats-row"]}>
<div className={style["stats-label"]}></div>
<div className={style["stats-value"]}>{overviewData?.healthScoreAssessment?.todayAdded || 0} </div>
</div>
</div>
</div>
</div>
</div>
{/* 基础构成 */}
<div className={style["health-section"]}>
<div className={style["health-section-title"]}></div>
{(overviewData?.healthScoreAssessment?.baseComposition &&
overviewData.healthScoreAssessment.baseComposition.length > 0
? overviewData.healthScoreAssessment.baseComposition
: [
{ name: "账号基础分", formatted: "+60" },
{ name: "已修改微信号", formatted: "+10" },
{ name: "好友数量加成", formatted: "+12", friendCount: 5595 },
]
).map((item, index) => (
<div className={style["health-item"]} key={`${item.name}-${index}`}>
<div className={style["health-item-label"]}>
{item.name}
{item.friendCount ? ` (${item.friendCount})` : ""}
</div>
<div
className={
(item.score ?? 0) >= 0
? style["health-item-value-positive"]
: style["health-item-value-negative"]
}
>
{item.formatted || `${item.score ?? 0}`}
</div>
</div>
))}
</div>
{/* 动态记录 */}
<div className={style["health-section"]}>
<div className={style["health-section-title"]}></div>
{overviewData?.healthScoreAssessment?.dynamicRecords &&
overviewData.healthScoreAssessment.dynamicRecords.length > 0 ? (
overviewData.healthScoreAssessment.dynamicRecords.map(
(record, index) => (
<div className={style["health-item"]} key={`record-${index}`}>
<div className={style["health-item-label"]}>
<span className={style["health-item-icon-warning"]}></span>
{record.title || record.description || "记录"}
{record.statusTag && (
<span className={style["health-item-tag"]}>
{record.statusTag}
</span>
)}
</div>
<div
className={
(record.score ?? 0) >= 0
? style["health-item-value-positive"]
: style["health-item-value-negative"]
}
>
{record.formatted ||
(record.score && record.score > 0
? `+${record.score}`
: record.score || "-")}
</div>
</div>
),
)
) : (
<div className={style["health-empty"]}></div>
)}
</div>
</div>
</Tabs.Tab>
<Tabs.Tab
title={`好友列表${activeTab === "friends" && friendsTotal > 0 ? ` (${friendsTotal.toLocaleString()})` : ""}`}
title={`好友${activeTab === "friends" && friendsTotal > 0 ? ` (${friendsTotal.toLocaleString()})` : ""}`}
key="friends"
>
<div className={style["friends-content"]}>
@@ -406,6 +740,23 @@ const WechatAccountDetail: React.FC = () => {
</Button>
</div>
{/* 好友概要 */}
<div className={style["friends-summary"]}>
<div className={style["summary-item"]}>
<div className={style["summary-label"]}></div>
<div className={style["summary-value"]}>
{friendsTotal || overviewData?.totalFriends || 0}
</div>
</div>
<div className={style["summary-divider"]} />
<div className={style["summary-item"]}>
<div className={style["summary-label"]}></div>
<div className={style["summary-value-highlight"]}>
{overviewData?.accountValue?.formatted || "¥1,500,000"}
</div>
</div>
</div>
{/* 好友列表 */}
<div className={style["friends-list"]}>
{isFetchingFriends && friends.length === 0 ? (
@@ -431,36 +782,44 @@ const WechatAccountDetail: React.FC = () => {
{friends.map(friend => (
<div
key={friend.id}
className={style["friend-item"]}
className={style["friend-card"]}
onClick={() => handleFriendClick(friend)}
>
<Avatar
src={friend.avatar}
className={style["friend-avatar"]}
/>
<div className={style["friend-info"]}>
<div className={style["friend-header"]}>
<div className={style["friend-avatar"]}>
<Avatar src={friend.avatar} />
</div>
<div className={style["friend-main"]}>
<div className={style["friend-name-row"]}>
<div className={style["friend-name"]}>
{friend.nickname}
{friend.remark && (
<span className={style["friend-remark"]}>
({friend.remark})
</span>
)}
{friend.nickname || "未知好友"}
</div>
</div>
<div className={style["friend-wechat-id"]}>
{friend.wechatId}
<div className={style["friend-id-row"]}>
ID: {friend.wechatId || "-"}
</div>
<div className={style["friend-tags"]}>
{friend.tags?.map((tag, index) => (
<Tag
key={index}
className={style["friend-tag"]}
<div className={style["friend-status-row"]}>
{friend.statusTags?.map((tag, idx) => (
<span
key={idx}
className={style["friend-status-chip"]}
>
{typeof tag === "string" ? tag : tag.name}
</Tag>
{tag}
</span>
))}
{friend.remark && (
<span className={style["friend-status-chip"]}>
{friend.remark}
</span>
)}
</div>
</div>
<div className={style["friend-value"]}>
<div className={style["value-amount"]}>
{friend.valueFormatted
|| (typeof friend.value === "number"
? `¥${friend.value.toLocaleString()}`
: "估值 -")}
</div>
</div>
</div>
@@ -484,45 +843,118 @@ const WechatAccountDetail: React.FC = () => {
</div>
</Tabs.Tab>
<Tabs.Tab title="风险评估" key="risk">
<div className={style["risk-content"]}>
{accountSummary?.restrictions &&
accountSummary.restrictions.length > 0 ? (
<div className={style["restrictions-list"]}>
{accountSummary.restrictions.map(restriction => (
<div
key={restriction.id}
className={style["restriction-item"]}
>
<div className={style["restriction-info"]}>
<div className={style["restriction-reason"]}>
{restriction.reason}
</div>
<div className={style["restriction-date"]}>
{restriction.date
? formatDateTime(restriction.date)
: "暂无时间"}
</div>
</div>
<div className={style["restriction-level"]}>
<span
className={`${style["level-badge"]} ${style[`level-${restriction.level}`]}`}
>
{restriction.level === 1
? "低风险"
: restriction.level === 2
? "中风险"
: "高风险"}
</span>
</div>
</div>
))}
<Tabs.Tab title="朋友圈" key="moments">
<div className={style["moments-content"]}>
{/* 功能按钮栏 */}
<div className={style["moments-action-bar"]}>
<div className={style["action-button"]}>
<span className={style["action-icon-text"]}></span>
<span className={style["action-text"]}></span>
</div>
<div className={style["action-button"]}>
<span className={style["action-icon-image"]}></span>
<span className={style["action-text"]}></span>
</div>
<div className={style["action-button"]}>
<span className={style["action-icon-video"]}></span>
<span className={style["action-text"]}></span>
</div>
<div
className={style["action-button-dark"]}
onClick={() => setShowExportPopup(true)}
>
<span className={style["action-icon-export"]}></span>
<span className={style["action-text-light"]}></span>
</div>
</div>
{/* 朋友圈列表 */}
<div className={style["moments-list"]}>
{isFetchingMoments && moments.length === 0 ? (
<div className={style["loading"]}>
<SpinLoading color="primary" style={{ fontSize: 32 }} />
</div>
) : momentsError ? (
<div className={style["error"]}>{momentsError}</div>
) : moments.length === 0 ? (
<div className={style["empty"]}></div>
) : (
moments.map(moment => {
const { day, month } = formatMomentDateParts(
moment.createTime,
);
const timeAgo = formatMomentTimeAgo(moment.createTime);
const imageCount = moment.resUrls?.length || 0;
// 根据图片数量选择对应的grid类参考素材管理的实现
let gridClass = "";
if (imageCount === 1) gridClass = style["single"];
else if (imageCount === 2) gridClass = style["double"];
else if (imageCount === 3) gridClass = style["triple"];
else if (imageCount === 4) gridClass = style["quad"];
else if (imageCount > 4) gridClass = style["grid"];
return (
<div className={style["moment-item"]} key={moment.id}>
<div className={style["moment-date"]}>
<div className={style["date-day"]}>{day}</div>
<div className={style["date-month"]}>{month}</div>
</div>
<div className={style["moment-content"]}>
{moment.content && (
<div className={style["moment-text"]}>
{moment.content}
</div>
)}
{imageCount > 0 && (
<div className={style["moment-images"]}>
<div
className={`${style["image-grid"]} ${gridClass}`}
>
{moment.resUrls
.slice(0, 9)
.map((url, index) => (
<img
key={`${moment.id}-img-${index}`}
src={url}
alt="朋友圈图片"
/>
))}
{imageCount > 9 && (
<div className={style["image-more"]}>
+{imageCount - 9}
</div>
)}
</div>
</div>
)}
<div className={style["moment-footer"]}>
<span className={style["moment-time"]}>
{timeAgo}
</span>
</div>
</div>
</div>
);
})
)}
</div>
{moments.length < momentsTotal && (
<div className={style["moments-load-more"]}>
<Button
size="small"
onClick={handleLoadMoreMoments}
loading={isFetchingMoments}
disabled={isFetchingMoments}
>
</Button>
</div>
) : (
<div className={style["empty"]}></div>
)}
</div>
</Tabs.Tab>
</Tabs>
</Card>
</div>
@@ -644,6 +1076,153 @@ const WechatAccountDetail: React.FC = () => {
</div>
</Popup>
{/* 朋友圈导出弹窗 */}
<Popup
visible={showExportPopup}
onMaskClick={() => setShowExportPopup(false)}
bodyStyle={{ borderRadius: "16px 16px 0 0" }}
>
<div className={style["popup-content"]}>
<div className={style["popup-header"]}>
<h3></h3>
<Button
size="small"
fill="outline"
onClick={() => setShowExportPopup(false)}
>
</Button>
</div>
<div className={style["export-form"]}>
{/* 关键词搜索 */}
<div className={style["form-item"]}>
<label></label>
<Input
placeholder="请输入关键词"
value={exportKeyword}
onChange={e => setExportKeyword(e.target.value)}
allowClear
/>
</div>
{/* 类型筛选 */}
<div className={style["form-item"]}>
<label></label>
<div className={style["type-selector"]}>
<div
className={`${style["type-option"]} ${
exportType === undefined ? style["active"] : ""
}`}
onClick={() => setExportType(undefined)}
>
</div>
<div
className={`${style["type-option"]} ${
exportType === 4 ? style["active"] : ""
}`}
onClick={() => setExportType(4)}
>
</div>
<div
className={`${style["type-option"]} ${
exportType === 1 ? style["active"] : ""
}`}
onClick={() => setExportType(1)}
>
</div>
<div
className={`${style["type-option"]} ${
exportType === 3 ? style["active"] : ""
}`}
onClick={() => setExportType(3)}
>
</div>
</div>
</div>
{/* 开始时间 */}
<div className={style["form-item"]}>
<label></label>
<Input
readOnly
placeholder="请选择开始时间"
value={
exportStartTime
? exportStartTime.toLocaleDateString("zh-CN")
: ""
}
onClick={() => setShowStartTimePicker(true)}
/>
<DatePicker
visible={showStartTimePicker}
title="开始时间"
value={exportStartTime}
onClose={() => setShowStartTimePicker(false)}
onConfirm={val => {
setExportStartTime(val);
setShowStartTimePicker(false);
}}
/>
</div>
{/* 结束时间 */}
<div className={style["form-item"]}>
<label></label>
<Input
readOnly
placeholder="请选择结束时间"
value={
exportEndTime ? exportEndTime.toLocaleDateString("zh-CN") : ""
}
onClick={() => setShowEndTimePicker(true)}
/>
<DatePicker
visible={showEndTimePicker}
title="结束时间"
value={exportEndTime}
onClose={() => setShowEndTimePicker(false)}
onConfirm={val => {
setExportEndTime(val);
setShowEndTimePicker(false);
}}
/>
</div>
</div>
<div className={style["popup-footer"]}>
<Button
block
color="primary"
onClick={handleExportMoments}
loading={exportLoading}
disabled={exportLoading}
>
{exportLoading ? "导出中..." : "确认导出"}
</Button>
<Button
block
color="danger"
fill="outline"
onClick={() => {
setShowExportPopup(false);
setExportKeyword("");
setExportType(undefined);
setExportStartTime(null);
setExportEndTime(null);
}}
style={{ marginTop: 12 }}
>
</Button>
</div>
</div>
</Popup>
{/* 好友详情弹窗 */}
{/* Removed */}
</Layout>

View File

@@ -2,6 +2,39 @@
padding: 0 12px;
}
.filter-bar {
padding: 12px;
background: #fff;
border-bottom: 1px solid #f0f0f0;
.filter-buttons {
display: flex;
gap: 8px;
.filter-button {
flex: 1;
height: 32px;
border-radius: 6px;
border: 1px solid #d9d9d9;
background: #fff;
color: #666;
font-size: 14px;
transition: all 0.2s;
&:hover {
border-color: #1677ff;
color: #1677ff;
}
&.filter-button-active {
background: #1677ff;
border-color: #1677ff;
color: #fff;
}
}
}
}
.nav-title {
font-size: 18px;
font-weight: 600;

View File

@@ -33,11 +33,12 @@ const WechatAccounts: React.FC = () => {
const [totalAccounts, setTotalAccounts] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [statusFilter, setStatusFilter] = useState<"all" | "online" | "offline">("all");
// 获取路由参数 wechatStatus
const wechatStatus = searchParams.get("wechatStatus");
const fetchAccounts = async (page = 1, keyword = "") => {
const fetchAccounts = async (page = 1, keyword = "", status?: "all" | "online" | "offline") => {
setIsLoading(true);
try {
const params: any = {
@@ -46,8 +47,12 @@ const WechatAccounts: React.FC = () => {
keyword,
};
// 如果有 wechatStatus 参数,添加到请求参数中
if (wechatStatus) {
// 优先使用传入的status参数否则使用路由参数,最后使用状态中的筛选
const filterStatus = status || wechatStatus || statusFilter;
if (filterStatus && filterStatus !== "all") {
params.wechatStatus = filterStatus === "online" ? "1" : "0";
} else if (wechatStatus) {
params.wechatStatus = wechatStatus;
}
@@ -60,7 +65,7 @@ const WechatAccounts: React.FC = () => {
setTotalAccounts(0);
}
} catch (e) {
Toast.show({ content: "获取微信号失败", position: "top" });
setAccounts([]);
setTotalAccounts(0);
} finally {
@@ -69,18 +74,24 @@ const WechatAccounts: React.FC = () => {
};
useEffect(() => {
fetchAccounts(currentPage, searchTerm);
fetchAccounts(currentPage, searchTerm, statusFilter);
// eslint-disable-next-line
}, [currentPage]);
}, [currentPage, statusFilter]);
const handleSearch = () => {
setCurrentPage(1);
fetchAccounts(1, searchTerm);
fetchAccounts(1, searchTerm, statusFilter);
};
const handleStatusFilterChange = (status: "all" | "online" | "offline") => {
setStatusFilter(status);
setCurrentPage(1);
fetchAccounts(1, searchTerm, status);
};
const handleRefresh = async () => {
setIsRefreshing(true);
await fetchAccounts(currentPage, searchTerm);
await fetchAccounts(currentPage, searchTerm, statusFilter);
setIsRefreshing(false);
Toast.show({ content: "刷新成功", position: "top" });
};
@@ -122,6 +133,31 @@ const WechatAccounts: React.FC = () => {
<ReloadOutlined />
</Button>
</div>
<div className={style["filter-bar"]}>
<div className={style["filter-buttons"]}>
<Button
size="small"
className={`${style["filter-button"]} ${statusFilter === "all" ? style["filter-button-active"] : ""}`}
onClick={() => handleStatusFilterChange("all")}
>
</Button>
<Button
size="small"
className={`${style["filter-button"]} ${statusFilter === "online" ? style["filter-button-active"] : ""}`}
onClick={() => handleStatusFilterChange("online")}
>
线
</Button>
<Button
size="small"
className={`${style["filter-button"]} ${statusFilter === "offline" ? style["filter-button-active"] : ""}`}
onClick={() => handleStatusFilterChange("offline")}
>
线
</Button>
</div>
</div>
</>
}
>

View File

@@ -369,13 +369,15 @@ const ScenarioList: React.FC = () => {
backFn={() => navigate("/scenarios")}
title={scenarioName || ""}
right={
<Button
size="small"
color="primary"
onClick={handleCreateNewPlan}
>
<PlusOutlined />
</Button>
scenarioId !== "10" ? (
<Button
size="small"
color="primary"
onClick={handleCreateNewPlan}
>
<PlusOutlined />
</Button>
) : null
}
/>
@@ -424,13 +426,15 @@ const ScenarioList: React.FC = () => {
<div className={style["empty-text"]}>
{searchTerm ? "没有找到匹配的计划" : "暂无计划"}
</div>
<Button
color="primary"
onClick={handleCreateNewPlan}
className={style["create-first-btn"]}
>
<PlusOutlined />
</Button>
{scenarioId !== "10" && (
<Button
color="primary"
onClick={handleCreateNewPlan}
className={style["create-first-btn"]}
>
<PlusOutlined />
</Button>
)}
</div>
) : (
<>

View File

@@ -242,7 +242,9 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
</div>
) : (
<div className={styles["basic-scene-grid"]}>
{sceneList.map(scene => {
{sceneList
.filter(scene => scene.id !== 10)
.map(scene => {
const selected = formData.scenario === scene.id;
return (
<button

View File

@@ -2,13 +2,11 @@
export * from "./module/user";
export * from "./module/app";
export * from "./module/settings";
export * from "./module/websocket/websocket";
// 导入store实例
import { useUserStore } from "./module/user";
import { useAppStore } from "./module/app";
import { useSettingsStore } from "./module/settings";
import { useWebSocketStore } from "./module/websocket/websocket";
// 导出持久化store创建函数
export {
@@ -34,7 +32,6 @@ export interface StoreState {
user: ReturnType<typeof useUserStore.getState>;
app: ReturnType<typeof useAppStore.getState>;
settings: ReturnType<typeof useSettingsStore.getState>;
websocket: ReturnType<typeof useWebSocketStore.getState>;
}
// 便利的store访问函数
@@ -42,14 +39,12 @@ export const getStores = (): StoreState => ({
user: useUserStore.getState(),
app: useAppStore.getState(),
settings: useSettingsStore.getState(),
websocket: useWebSocketStore.getState(),
});
// 获取特定store状态
export const getUserStore = () => useUserStore.getState();
export const getAppStore = () => useAppStore.getState();
export const getSettingsStore = () => useSettingsStore.getState();
export const getWebSocketStore = () => useWebSocketStore.getState();
// 清除所有持久化数据(使用工具函数)
export const clearAllPersistedData = clearAllData;
@@ -61,7 +56,6 @@ export const getPersistKeys = () => Object.values(PERSIST_KEYS);
export const subscribeToUserStore = useUserStore.subscribe;
export const subscribeToAppStore = useAppStore.subscribe;
export const subscribeToSettingsStore = useSettingsStore.subscribe;
export const subscribeToWebSocketStore = useWebSocketStore.subscribe;
// 组合订阅函数
export const subscribeToAllStores = (callback: (state: StoreState) => void) => {
@@ -74,14 +68,10 @@ export const subscribeToAllStores = (callback: (state: StoreState) => void) => {
const unsubscribeSettings = useSettingsStore.subscribe(() => {
callback(getStores());
});
const unsubscribeWebSocket = useWebSocketStore.subscribe(() => {
callback(getStores());
});
return () => {
unsubscribeUser();
unsubscribeApp();
unsubscribeSettings();
unsubscribeWebSocket();
};
};

View File

@@ -0,0 +1,70 @@
// 全局缓存清理工具:浏览器存储 + IndexedDB + Zustand store
import { clearAllPersistedData } from "@/store";
import { useUserStore } from "@/store/module/user";
import { useAppStore } from "@/store/module/app";
import { useSettingsStore } from "@/store/module/settings";
const isBrowser = typeof window !== "undefined";
const safeStorageClear = (storage?: Storage) => {
if (!storage) return;
try {
storage.clear();
} catch (error) {
console.warn("清理存储失败:", error);
}
};
export const clearBrowserStorage = () => {
if (!isBrowser) return;
safeStorageClear(window.localStorage);
safeStorageClear(window.sessionStorage);
try {
clearAllPersistedData();
} catch (error) {
console.warn("清理持久化 store 失败:", error);
}
};
export const clearAllIndexedDB = async (): Promise<void> => {
if (!isBrowser || !window.indexedDB || !indexedDB.databases) return;
const databases = await indexedDB.databases();
const deleteJobs = databases
.map(db => db.name)
.filter((name): name is string => Boolean(name))
.map(
name =>
new Promise<void>((resolve, reject) => {
const request = indexedDB.deleteDatabase(name);
request.onsuccess = () => resolve();
request.onerror = () => reject(new Error(`删除数据库 ${name} 失败`));
request.onblocked = () => {
setTimeout(() => {
const retry = indexedDB.deleteDatabase(name);
retry.onsuccess = () => resolve();
retry.onerror = () =>
reject(new Error(`删除数据库 ${name} 失败`));
}, 100);
};
}),
);
await Promise.allSettled(deleteJobs);
};
export const resetAllStores = () => {
const userStore = useUserStore.getState();
const appStore = useAppStore.getState();
const settingsStore = useSettingsStore.getState();
userStore?.clearUser?.();
appStore?.resetAppState?.();
settingsStore?.resetSettings?.();
};
export const clearApplicationCache = async () => {
clearBrowserStorage();
await clearAllIndexedDB();
resetAllStores();
};

View File

@@ -1,147 +0,0 @@
.chatFooter {
background: #f7f7f7;
border-top: 1px solid #e1e1e1;
padding: 0;
height: auto;
border-radius: 8px;
}
.inputContainer {
padding: 8px 12px;
display: flex;
flex-direction: column;
gap: 6px;
}
.inputToolbar {
display: flex;
align-items: center;
padding: 4px 0;
}
.leftTool {
display: flex;
gap: 4px;
align-items: center;
}
.toolbarButton {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
color: #666;
font-size: 16px;
transition: all 0.15s;
border: none;
background: transparent;
&:hover {
background: #e6e6e6;
color: #333;
}
&:active {
background: #d9d9d9;
}
}
.inputArea {
display: flex;
flex-direction: column;
padding: 4px 0;
}
.inputWrapper {
border: 1px solid #d1d1d1;
border-radius: 4px;
background: #fff;
overflow: hidden;
&:focus-within {
border-color: #07c160;
}
}
.messageInput {
width: 100%;
border: none;
resize: none;
font-size: 13px;
line-height: 1.4;
padding: 8px 10px;
background: transparent;
&:focus {
box-shadow: none;
outline: none;
}
&::placeholder {
color: #b3b3b3;
}
}
.sendButtonArea {
padding: 8px 10px;
display: flex;
justify-content: flex-end;
gap: 8px;
}
.sendButton {
height: 32px;
border-radius: 4px;
font-weight: normal;
min-width: 60px;
font-size: 13px;
background: #07c160;
border-color: #07c160;
&:hover {
background: #06ad56;
border-color: #06ad56;
}
&:active {
background: #059748;
border-color: #059748;
}
&:disabled {
background: #b3b3b3;
border-color: #b3b3b3;
opacity: 1;
}
}
.hintButton {
border: none;
background: transparent;
color: #666;
font-size: 12px;
&:hover {
color: #333;
}
}
.inputHint {
font-size: 11px;
color: #999;
text-align: right;
margin-top: 2px;
}
@media (max-width: 768px) {
.inputToolbar {
flex-wrap: wrap;
gap: 8px;
}
.sendButtonArea {
justify-content: space-between;
}
}

View File

@@ -1,265 +0,0 @@
.stepContent {
.stepHeader {
margin-bottom: 20px;
h3 {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 8px 0;
}
p {
font-size: 14px;
color: #666;
margin: 0;
}
}
}
.step3Content {
display: flex;
gap: 24px;
align-items: flex-start;
.leftColumn {
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
}
.rightColumn {
width: 400px;
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
}
.messagePreview {
border: 2px dashed #52c41a;
border-radius: 8px;
padding: 20px;
background: #f6ffed;
.previewTitle {
font-size: 14px;
color: #52c41a;
font-weight: 500;
margin-bottom: 12px;
}
.messageBubble {
min-height: 60px;
padding: 12px;
background: #fff;
border-radius: 6px;
color: #666;
font-size: 14px;
line-height: 1.6;
.currentEditingLabel {
font-size: 12px;
color: #999;
margin-bottom: 8px;
}
.messageText {
color: #333;
white-space: pre-wrap;
word-break: break-word;
}
}
}
.savedScriptGroups {
.scriptGroupTitle {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 12px;
}
.scriptGroupItem {
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
background: #fff;
.scriptGroupHeader {
display: flex;
justify-content: space-between;
align-items: center;
.scriptGroupLeft {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
:global(.ant-radio) {
margin-right: 4px;
}
.scriptGroupName {
font-size: 14px;
font-weight: 500;
color: #333;
}
.messageCount {
font-size: 12px;
color: #999;
margin-left: 8px;
}
}
.scriptGroupActions {
display: flex;
gap: 4px;
.actionButton {
padding: 4px;
color: #666;
&:hover {
color: #1890ff;
}
}
}
}
.scriptGroupContent {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #f0f0f0;
font-size: 13px;
color: #666;
}
}
}
.messageInputArea {
.messageInput {
margin-bottom: 12px;
}
.attachmentButtons {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.aiRewriteSection {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.messageHint {
font-size: 12px;
color: #999;
}
}
.settingsPanel {
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 20px;
background: #fafafa;
.settingsTitle {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 16px;
}
.settingItem {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
.settingLabel {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 12px;
}
.settingControl {
display: flex;
align-items: center;
gap: 8px;
span {
font-size: 14px;
color: #666;
min-width: 80px;
}
}
}
}
.tagSection {
.settingLabel {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 12px;
}
}
.pushPreview {
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 20px;
background: #f0f7ff;
.previewTitle {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 12px;
}
ul {
list-style: none;
padding: 0;
margin: 0;
li {
font-size: 14px;
color: #666;
line-height: 1.8;
}
}
}
}
@media (max-width: 1200px) {
.step3Content {
.rightColumn {
width: 350px;
}
}
}
@media (max-width: 768px) {
.step3Content {
flex-direction: column;
.leftColumn {
width: 100%;
}
.rightColumn {
width: 100%;
}
}
}

View File

@@ -1,6 +0,0 @@
import ContentSelection from "@/components/ContentSelection";
import { ContentItem } from "@/components/ContentSelection/data";
import InputMessage from "./InputMessage/InputMessage";
import styles from "./index.module.scss";
interface StepSendMessageProps {

View File

@@ -0,0 +1,73 @@
// 缓存清理工具,统一处理浏览器存储与 Zustand store
import { clearAllPersistedData } from "@/store";
import { useUserStore } from "@/store/module/user";
import { useAppStore } from "@/store/module/app";
import { useSettingsStore } from "@/store/module/settings";
const isBrowser = typeof window !== "undefined";
const safeStorageClear = (storage?: Storage) => {
if (!storage) return;
try {
storage.clear();
} catch (error) {
console.warn("清理存储失败:", error);
}
};
export const clearBrowserStorage = () => {
if (!isBrowser) return;
safeStorageClear(window.localStorage);
safeStorageClear(window.sessionStorage);
// 清理自定义持久化数据
try {
clearAllPersistedData();
} catch (error) {
console.warn("清理持久化 store 失败:", error);
}
};
export const clearAllIndexedDB = async (): Promise<void> => {
if (!isBrowser || !window.indexedDB || !indexedDB.databases) return;
const databases = await indexedDB.databases();
const deleteJobs = databases
.map(db => db.name)
.filter((name): name is string => Boolean(name))
.map(
name =>
new Promise<void>((resolve, reject) => {
const request = indexedDB.deleteDatabase(name);
request.onsuccess = () => resolve();
request.onerror = () =>
reject(new Error(`删除数据库 ${name} 失败`));
request.onblocked = () => {
setTimeout(() => {
const retry = indexedDB.deleteDatabase(name);
retry.onsuccess = () => resolve();
retry.onerror = () =>
reject(new Error(`删除数据库 ${name} 失败`));
}, 100);
};
}),
);
await Promise.allSettled(deleteJobs);
};
export const resetAllStores = () => {
const userStore = useUserStore.getState();
const appStore = useAppStore.getState();
const settingsStore = useSettingsStore.getState();
userStore?.clearUser?.();
appStore?.resetAppState?.();
settingsStore?.resetSettings?.();
};
export const clearApplicationCache = async () => {
clearBrowserStorage();
await clearAllIndexedDB();
resetAllStores();
};

3
Moncter/weChat.ts Normal file
View File

@@ -0,0 +1,3 @@

374
Moncter/数据库/KR.js Normal file
View File

@@ -0,0 +1,374 @@
/*
Navicat Premium Data Transfer
Source Server : 标签数据库
Source Server Type : MongoDB
Source Server Version : 60025 (6.0.25)
Source Host : 192.168.2.6:27017
Source Schema : KR
Target Server Type : MongoDB
Target Server Version : 60025 (6.0.25)
File Encoding : 65001
Date: 19/11/2025 15:18:11
*/
// ----------------------------
// Collection structure for system.profile
// ----------------------------
db.getCollection("system.profile").drop();
db.createCollection("system.profile",{
capped: true,
size: 1048576
});
// ----------------------------
// Collection structure for 厦门京东内
// ----------------------------
db.getCollection("厦门京东内").drop();
db.createCollection("厦门京东内");
// ----------------------------
// Collection structure for 厦门用户资产2025年9月
// ----------------------------
db.getCollection("厦门用户资产2025年9月").drop();
db.createCollection("厦门用户资产2025年9月");
// ----------------------------
// Collection structure for 厦门用户资产2025年9月_优化版
// ----------------------------
db.getCollection("厦门用户资产2025年9月_优化版").drop();
db.createCollection("厦门用户资产2025年9月_优化版");
db.getCollection("厦门用户资产2025年9月_优化版").createIndex({
user_level: NumberInt("1")
}, {
name: "user_level_1"
});
db.getCollection("厦门用户资产2025年9月_优化版").createIndex({
total_score: NumberInt("-1")
}, {
name: "total_score_-1"
});
db.getCollection("厦门用户资产2025年9月_优化版").createIndex({
mobile: NumberInt("1")
}, {
name: "mobile_1"
});
db.getCollection("厦门用户资产2025年9月_优化版").createIndex({
city: NumberInt("1")
}, {
name: "city_1"
});
db.getCollection("厦门用户资产2025年9月_优化版").createIndex({
name: NumberInt("1")
}, {
name: "name_1"
});
// ----------------------------
// Collection structure for 游戏_深度用户有过语音聊天、魔兽等
// ----------------------------
db.getCollection("游戏_深度用户有过语音聊天、魔兽等").drop();
db.createCollection("游戏_深度用户有过语音聊天、魔兽等");
db.getCollection("游戏_深度用户有过语音聊天、魔兽等").createIndex({
user_id: NumberInt("1")
}, {
name: "user_id_1"
});
db.getCollection("游戏_深度用户有过语音聊天、魔兽等").createIndex({
user_value: NumberInt("1")
}, {
name: "user_value_1"
});
db.getCollection("游戏_深度用户有过语音聊天、魔兽等").createIndex({
overlap_count: NumberInt("1")
}, {
name: "overlap_count_1"
});
// ----------------------------
// Collection structure for 用户估值
// ----------------------------
db.getCollection("用户估值").drop();
db.createCollection("用户估值");
db.getCollection("用户估值").createIndex({
user_key: NumberInt("1")
}, {
name: "user_key_1",
unique: true
});
db.getCollection("用户估值").createIndex({
phone: NumberInt("1")
}, {
name: "phone_1"
});
db.getCollection("用户估值").createIndex({
phone_masked: NumberInt("1")
}, {
name: "phone_masked_1"
});
db.getCollection("用户估值").createIndex({
email: NumberInt("1")
}, {
name: "email_1"
});
db.getCollection("用户估值").createIndex({
name: NumberInt("1")
}, {
name: "name_1"
});
db.getCollection("用户估值").createIndex({
province: NumberInt("1"),
city: NumberInt("1")
}, {
name: "province_1_city_1"
});
db.getCollection("用户估值").createIndex({
computed_at: NumberInt("-1")
}, {
name: "computed_at_-1"
});
db.getCollection("用户估值").createIndex({
source_channels: NumberInt("1")
}, {
name: "source_channels_1"
});
db.getCollection("用户估值").createIndex({
"data_quality.completeness": NumberInt("-1")
}, {
name: "data_quality.completeness_-1"
});
db.getCollection("用户估值").createIndex({
user_evaluation_score: NumberInt("-1")
}, {
name: "user_evaluation_score_-1"
});
// ----------------------------
// Collection structure for 用户资产整合
// ----------------------------
db.getCollection("用户资产整合").drop();
db.createCollection("用户资产整合");
db.getCollection("用户资产整合").createIndex({
user_key: NumberInt("1")
}, {
name: "idx_user_key"
});
db.getCollection("用户资产整合").createIndex({
phone: NumberInt("1")
}, {
name: "idx_phone"
});
db.getCollection("用户资产整合").createIndex({
consumption_level: NumberInt("1")
}, {
name: "idx_consumption_level"
});
db.getCollection("用户资产整合").createIndex({
consumption_score: NumberInt("-1")
}, {
name: "idx_consumption_score_desc"
});
db.getCollection("用户资产整合").createIndex({
city: NumberInt("1")
}, {
name: "idx_city"
});
db.getCollection("用户资产整合").createIndex({
age_range: NumberInt("1")
}, {
name: "idx_age_range"
});
db.getCollection("用户资产整合").createIndex({
user_evaluation_score: NumberInt("-1")
}, {
name: "idx_user_evaluation_score_desc"
});
db.getCollection("用户资产整合").createIndex({
consumption_level: NumberInt("1"),
consumption_score: NumberInt("-1")
}, {
name: "idx_level_score"
});
// ----------------------------
// Collection structure for 金融客户_厦门_A级用户
// ----------------------------
db.getCollection("金融客户_厦门_A级用户").drop();
db.createCollection("金融客户_厦门_A级用户");
db.getCollection("金融客户_厦门_A级用户").createIndex({
total_score: NumberInt("1")
}, {
name: "total_score_1",
background: true
});
db.getCollection("金融客户_厦门_A级用户").createIndex({
"姓名": NumberInt("1")
}, {
name: "姓名_1",
background: true
});
db.getCollection("金融客户_厦门_A级用户").createIndex({
"手机": NumberInt("1")
}, {
name: "手机_1",
background: true
});
// ----------------------------
// Collection structure for 金融客户_厦门_B级用户
// ----------------------------
db.getCollection("金融客户_厦门_B级用户").drop();
db.createCollection("金融客户_厦门_B级用户");
db.getCollection("金融客户_厦门_B级用户").createIndex({
total_score: NumberInt("1")
}, {
name: "total_score_1",
background: true
});
db.getCollection("金融客户_厦门_B级用户").createIndex({
"姓名": NumberInt("1")
}, {
name: "姓名_1",
background: true
});
db.getCollection("金融客户_厦门_B级用户").createIndex({
"手机": NumberInt("1")
}, {
name: "手机_1",
background: true
});
// ----------------------------
// Collection structure for 金融客户_厦门_C级用户
// ----------------------------
db.getCollection("金融客户_厦门_C级用户").drop();
db.createCollection("金融客户_厦门_C级用户");
db.getCollection("金融客户_厦门_C级用户").createIndex({
total_score: NumberInt("1")
}, {
name: "total_score_1",
background: true
});
db.getCollection("金融客户_厦门_C级用户").createIndex({
"姓名": NumberInt("1")
}, {
name: "姓名_1",
background: true
});
db.getCollection("金融客户_厦门_C级用户").createIndex({
"手机": NumberInt("1")
}, {
name: "手机_1",
background: true
});
// ----------------------------
// Collection structure for 金融客户_厦门_D级用户
// ----------------------------
db.getCollection("金融客户_厦门_D级用户").drop();
db.createCollection("金融客户_厦门_D级用户");
db.getCollection("金融客户_厦门_D级用户").createIndex({
total_score: NumberInt("1")
}, {
name: "total_score_1",
background: true
});
db.getCollection("金融客户_厦门_D级用户").createIndex({
"姓名": NumberInt("1")
}, {
name: "姓名_1",
background: true
});
db.getCollection("金融客户_厦门_D级用户").createIndex({
"手机": NumberInt("1")
}, {
name: "手机_1",
background: true
});
// ----------------------------
// Collection structure for 金融客户_厦门_E级用户
// ----------------------------
db.getCollection("金融客户_厦门_E级用户").drop();
db.createCollection("金融客户_厦门_E级用户");
db.getCollection("金融客户_厦门_E级用户").createIndex({
total_score: NumberInt("1")
}, {
name: "total_score_1",
background: true
});
db.getCollection("金融客户_厦门_E级用户").createIndex({
"姓名": NumberInt("1")
}, {
name: "姓名_1",
background: true
});
db.getCollection("金融客户_厦门_E级用户").createIndex({
"手机": NumberInt("1")
}, {
name: "手机_1",
background: true
});
// ----------------------------
// Collection structure for 金融客户_厦门_S级用户
// ----------------------------
db.getCollection("金融客户_厦门_S级用户").drop();
db.createCollection("金融客户_厦门_S级用户");
db.getCollection("金融客户_厦门_S级用户").createIndex({
total_score: NumberInt("1")
}, {
name: "total_score_1",
background: true
});
db.getCollection("金融客户_厦门_S级用户").createIndex({
"姓名": NumberInt("1")
}, {
name: "姓名_1",
background: true
});
db.getCollection("金融客户_厦门_S级用户").createIndex({
"手机": NumberInt("1")
}, {
name: "手机_1",
background: true
});
// ----------------------------
// Collection structure for 金融组02250905
// ----------------------------
db.getCollection("金融组02250905").drop();
db.createCollection("金融组02250905");
db.getCollection("金融组02250905").createIndex({
"姓名": NumberInt("1")
}, {
name: "姓名_1"
});
db.getCollection("金融组02250905").createIndex({
"手机": NumberInt("1")
}, {
name: "手机_1"
});
db.getCollection("金融组02250905").createIndex({
"身份证": NumberInt("1")
}, {
name: "身份证_1"
});
db.getCollection("金融组02250905").createIndex({
"综合评分": NumberInt("-1")
}, {
name: "综合评分_-1"
});
db.getCollection("金融组02250905").createIndex({
"用户等级": NumberInt("1")
}, {
name: "用户等级_1"
});
db.getCollection("金融组02250905").createIndex({
"城市": NumberInt("1")
}, {
name: "城市_1"
});

509
Moncter/数据库/KR_KR.js Normal file
View File

@@ -0,0 +1,509 @@
/*
Navicat Premium Data Transfer
Source Server : 标签数据库
Source Server Type : MongoDB
Source Server Version : 60025 (6.0.25)
Source Host : 192.168.2.6:27017
Source Schema : KR_KR
Target Server Type : MongoDB
Target Server Version : 60025 (6.0.25)
File Encoding : 65001
Date: 19/11/2025 15:18:19
*/
// ----------------------------
// Collection structure for 34万HR人力资源经理企业1
// ----------------------------
db.getCollection("34万HR人力资源经理企业1").drop();
db.createCollection("34万HR人力资源经理企业1");
db.getCollection("34万HR人力资源经理企业1").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
db.getCollection("34万HR人力资源经理企业1").createIndex({
"电话": NumberInt("1")
}, {
name: "电话_1"
});
// ----------------------------
// Collection structure for 34万HR人力资源经理企业3
// ----------------------------
db.getCollection("34万HR人力资源经理企业3").drop();
db.createCollection("34万HR人力资源经理企业3");
db.getCollection("34万HR人力资源经理企业3").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for 34万HR人力资源经理企业4
// ----------------------------
db.getCollection("34万HR人力资源经理企业4").drop();
db.createCollection("34万HR人力资源经理企业4");
db.getCollection("34万HR人力资源经理企业4").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for 34万HR人力资源经理企业5
// ----------------------------
db.getCollection("34万HR人力资源经理企业5").drop();
db.createCollection("34万HR人力资源经理企业5");
db.getCollection("34万HR人力资源经理企业5").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for 51JOB
// ----------------------------
db.getCollection("51JOB").drop();
db.createCollection("51JOB");
// ----------------------------
// Collection structure for 8万条借款数据
// ----------------------------
db.getCollection("8万条借款数据").drop();
db.createCollection("8万条借款数据");
db.getCollection("8万条借款数据").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for CSDN用户数据
// ----------------------------
db.getCollection("CSDN用户数据").drop();
db.createCollection("CSDN用户数据");
db.getCollection("CSDN用户数据").createIndex({
username: NumberInt("1")
}, {
name: "username_1"
});
db.getCollection("CSDN用户数据").createIndex({
email: NumberInt("1")
}, {
name: "email_1"
});
db.getCollection("CSDN用户数据").createIndex({
domain: NumberInt("1")
}, {
name: "domain_1"
});
db.getCollection("CSDN用户数据").createIndex({
created_at: NumberInt("-1")
}, {
name: "created_at_-1"
});
db.getCollection("CSDN用户数据").createIndex({
data_source: NumberInt("1")
}, {
name: "data_source_1"
});
db.getCollection("CSDN用户数据").createIndex({
username: NumberInt("1"),
email: NumberInt("1")
}, {
name: "username_1_email_1",
unique: true
});
// ----------------------------
// Collection structure for Sheet2
// ----------------------------
db.getCollection("Sheet2").drop();
db.createCollection("Sheet2");
db.getCollection("Sheet2").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for pw_members
// ----------------------------
db.getCollection("pw_members").drop();
db.createCollection("pw_members");
// ----------------------------
// Collection structure for system.profile
// ----------------------------
db.getCollection("system.profile").drop();
db.createCollection("system.profile",{
capped: true,
size: 1048576
});
// ----------------------------
// Collection structure for www.jfclub.com
// ----------------------------
db.getCollection("www.jfclub.com").drop();
db.createCollection("www.jfclub.com");
db.getCollection("www.jfclub.com").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
db.getCollection("www.jfclub.com").createIndex({
u_id: NumberInt("1")
}, {
name: "u_id_1"
});
db.getCollection("www.jfclub.com").createIndex({
email: NumberInt("1")
}, {
name: "email_1"
});
db.getCollection("www.jfclub.com").createIndex({
phone: NumberInt("1")
}, {
name: "phone_1"
});
db.getCollection("www.jfclub.com").createIndex({
name: NumberInt("1")
}, {
name: "name_1"
});
db.getCollection("www.jfclub.com").createIndex({
email: NumberInt("1"),
name: NumberInt("1")
}, {
name: "email_1_name_1"
});
db.getCollection("www.jfclub.com").createIndex({
phone: NumberInt("1"),
name: NumberInt("1")
}, {
name: "phone_1_name_1"
});
// ----------------------------
// Collection structure for 人人网用户数据
// ----------------------------
db.getCollection("人人网用户数据").drop();
db.createCollection("人人网用户数据");
// ----------------------------
// Collection structure for 借贷
// ----------------------------
db.getCollection("借贷").drop();
db.createCollection("借贷");
// ----------------------------
// Collection structure for 全国车主数据集_2020年
// ----------------------------
db.getCollection("全国车主数据集_2020年").drop();
db.createCollection("全国车主数据集_2020年");
// ----------------------------
// Collection structure for 北京本科
// ----------------------------
db.getCollection("北京本科").drop();
db.createCollection("北京本科");
db.getCollection("北京本科").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for 千脑云电脑_qiannao_com
// ----------------------------
db.getCollection("千脑云电脑_qiannao_com").drop();
db.createCollection("千脑云电脑_qiannao_com");
db.getCollection("千脑云电脑_qiannao_com").createIndex({
"用户名": NumberInt("1")
}, {
name: "用户名_1"
});
db.getCollection("千脑云电脑_qiannao_com").createIndex({
"邮箱": NumberInt("1")
}, {
name: "邮箱_1"
});
db.getCollection("千脑云电脑_qiannao_com").createIndex({
"用户ID": NumberInt("1")
}, {
name: "用户ID_1"
});
db.getCollection("千脑云电脑_qiannao_com").createIndex({
"创建时间": NumberInt("1")
}, {
name: "创建时间_1"
});
// ----------------------------
// Collection structure for 天涯论坛tianya.cn
// ----------------------------
db.getCollection("天涯论坛tianya.cn").drop();
db.createCollection("天涯论坛tianya.cn");
// ----------------------------
// Collection structure for 情感
// ----------------------------
db.getCollection("情感").drop();
db.createCollection("情感");
db.getCollection("情感").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
db.getCollection("情感").createIndex({
"手机": NumberInt("1")
}, {
name: "手机_1"
});
// ----------------------------
// Collection structure for 慈溪人才网
// ----------------------------
db.getCollection("慈溪人才网").drop();
db.createCollection("慈溪人才网");
// ----------------------------
// Collection structure for 房产网
// ----------------------------
db.getCollection("房产网").drop();
db.createCollection("房产网");
db.getCollection("房产网").createIndex({
uid: NumberInt("1")
}, {
name: "uid_1",
unique: true
});
db.getCollection("房产网").createIndex({
username: NumberInt("1")
}, {
name: "username_1"
});
db.getCollection("房产网").createIndex({
email: NumberInt("1")
}, {
name: "email_1"
});
db.getCollection("房产网").createIndex({
regdate: NumberInt("1")
}, {
name: "regdate_1"
});
// ----------------------------
// Collection structure for 抖音直播粉
// ----------------------------
db.getCollection("抖音直播粉").drop();
db.createCollection("抖音直播粉");
db.getCollection("抖音直播粉").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
db.getCollection("抖音直播粉").createIndex({
Uid: NumberInt("1")
}, {
name: "Uid_1"
});
db.getCollection("抖音直播粉").createIndex({
sec_uid: NumberInt("1")
}, {
name: "sec_uid_1"
});
// ----------------------------
// Collection structure for 收藏品微信好友
// ----------------------------
db.getCollection("收藏品微信好友").drop();
db.createCollection("收藏品微信好友");
db.getCollection("收藏品微信好友").createIndex({
WechatId: NumberInt("1")
}, {
name: "IX_Kefure_WechatFriend_WechatId"
});
db.getCollection("收藏品微信好友").createIndex({
OwnerWechatId: NumberInt("1")
}, {
name: "IX_Kefure_WechatFriend_OwnerWechatId"
});
db.getCollection("收藏品微信好友").createIndex({
Phone: NumberInt("1")
}, {
name: "Phone_1"
});
// ----------------------------
// Collection structure for 收藏品用户
// ----------------------------
db.getCollection("收藏品用户").drop();
db.createCollection("收藏品用户");
db.getCollection("收藏品用户").createIndex({
Id: NumberInt("1")
}, {
name: "Id_1"
});
db.getCollection("收藏品用户").createIndex({
WechatAccountId: NumberInt("1")
}, {
name: "WechatAccountId_1"
});
db.getCollection("收藏品用户").createIndex({
WechatId: NumberInt("1")
}, {
name: "WechatId_1"
});
db.getCollection("收藏品用户").createIndex({
AccountId: NumberInt("1")
}, {
name: "AccountId_1"
});
db.getCollection("收藏品用户").createIndex({
OwnerWechatId: NumberInt("1")
}, {
name: "OwnerWechatId_1"
});
db.getCollection("收藏品用户").createIndex({
GroupId: NumberInt("1")
}, {
name: "GroupId_1"
});
db.getCollection("收藏品用户").createIndex({
Nickname: NumberInt("1")
}, {
name: "Nickname_1"
});
db.getCollection("收藏品用户").createIndex({
AccountNickname: NumberInt("1")
}, {
name: "AccountNickname_1"
});
db.getCollection("收藏品用户").createIndex({
OwnerNickname: NumberInt("1")
}, {
name: "OwnerNickname_1"
});
// ----------------------------
// Collection structure for 木蚂蚁munayi_com
// ----------------------------
db.getCollection("木蚂蚁munayi_com").drop();
db.createCollection("木蚂蚁munayi_com");
db.getCollection("木蚂蚁munayi_com").createIndex({
username: NumberInt("1")
}, {
name: "username_1",
unique: true,
sparse: true
});
db.getCollection("木蚂蚁munayi_com").createIndex({
email: NumberInt("1")
}, {
name: "email_1",
sparse: true
});
db.getCollection("木蚂蚁munayi_com").createIndex({
uid: NumberInt("1")
}, {
name: "uid_1",
sparse: true
});
db.getCollection("木蚂蚁munayi_com").createIndex({
data_hash: NumberInt("1")
}, {
name: "data_hash_1",
unique: true,
sparse: true
});
// ----------------------------
// Collection structure for 狂人影子库user
// ----------------------------
db.getCollection("狂人影子库user").drop();
db.createCollection("狂人影子库user");
// ----------------------------
// Collection structure for 珍爱网
// ----------------------------
db.getCollection("珍爱网").drop();
db.createCollection("珍爱网");
db.getCollection("珍爱网").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for 用户身份凭证数据集
// ----------------------------
db.getCollection("用户身份凭证数据集").drop();
db.createCollection("用户身份凭证数据集");
// ----------------------------
// Collection structure for 网红库
// ----------------------------
db.getCollection("网红库").drop();
db.createCollection("网红库");
db.getCollection("网红库").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
db.getCollection("网红库").createIndex({
sec_uid: NumberInt("1")
}, {
name: "sec_uid_1"
});
db.getCollection("网红库").createIndex({
idcard: NumberInt("1")
}, {
name: "idcard_1"
});
db.getCollection("网红库").createIndex({
collectId: NumberInt("1")
}, {
name: "collectId_1"
});
db.getCollection("网红库").createIndex({
cateId: NumberInt("1")
}, {
name: "cateId_1"
});
db.getCollection("网红库").createIndex({
authorId: NumberInt("1")
}, {
name: "authorId_1"
});
db.getCollection("网红库").createIndex({
uniqueId: NumberInt("1")
}, {
name: "uniqueId_1"
});
db.getCollection("网红库").createIndex({
phoneNumber: NumberInt("1")
}, {
name: "phoneNumber_1"
});
db.getCollection("网红库").createIndex({
nickname: NumberInt("1")
}, {
name: "nickname_1"
});
db.getCollection("网红库").createIndex({
updatedAt: NumberInt("1")
}, {
name: "updatedAt_1"
});
// ----------------------------
// Collection structure for 雅虎用户
// ----------------------------
db.getCollection("雅虎用户").drop();
db.createCollection("雅虎用户");

View File

@@ -0,0 +1,31 @@
/*
Navicat Premium Data Transfer
Source Server : 标签数据库
Source Server Type : MongoDB
Source Server Version : 60025 (6.0.25)
Source Host : 192.168.2.6:27017
Source Schema : KR_Linkedln
Target Server Type : MongoDB
Target Server Version : 60025 (6.0.25)
File Encoding : 65001
Date: 19/11/2025 15:18:28
*/
// ----------------------------
// Collection structure for linkedin
// ----------------------------
db.getCollection("linkedin").drop();
db.createCollection("linkedin");
// ----------------------------
// Collection structure for system.profile
// ----------------------------
db.getCollection("system.profile").drop();
db.createCollection("system.profile",{
capped: true,
size: 1048576
});

View File

@@ -0,0 +1,31 @@
/*
Navicat Premium Data Transfer
Source Server : 标签数据库
Source Server Type : MongoDB
Source Server Version : 60025 (6.0.25)
Source Host : 192.168.2.6:27017
Source Schema : KR_京东
Target Server Type : MongoDB
Target Server Version : 60025 (6.0.25)
File Encoding : 65001
Date: 19/11/2025 15:19:11
*/
// ----------------------------
// Collection structure for jd_com
// ----------------------------
db.getCollection("jd_com").drop();
db.createCollection("jd_com");
// ----------------------------
// Collection structure for system.profile
// ----------------------------
db.getCollection("system.profile").drop();
db.createCollection("system.profile",{
capped: true,
size: 1048576
});

View File

@@ -0,0 +1,37 @@
/*
Navicat Premium Data Transfer
Source Server : 标签数据库
Source Server Type : MongoDB
Source Server Version : 60025 (6.0.25)
Source Host : 192.168.2.6:27017
Source Schema : KR_人才库
Target Server Type : MongoDB
Target Server Version : 60025 (6.0.25)
File Encoding : 65001
Date: 19/11/2025 15:19:59
*/
// ----------------------------
// Collection structure for 51JOB
// ----------------------------
db.getCollection("51JOB").drop();
db.createCollection("51JOB");
// ----------------------------
// Collection structure for system.profile
// ----------------------------
db.getCollection("system.profile").drop();
db.createCollection("system.profile",{
capped: true,
size: 1048576
});
// ----------------------------
// Collection structure for 慈溪人才网
// ----------------------------
db.getCollection("慈溪人才网").drop();
db.createCollection("慈溪人才网");

View File

@@ -0,0 +1,142 @@
/*
Navicat Premium Data Transfer
Source Server : 标签数据库
Source Server Type : MongoDB
Source Server Version : 60025 (6.0.25)
Source Host : 192.168.2.6:27017
Source Schema : KR_企业
Target Server Type : MongoDB
Target Server Version : 60025 (6.0.25)
File Encoding : 65001
Date: 19/11/2025 15:19:44
*/
// ----------------------------
// Collection structure for system.profile
// ----------------------------
db.getCollection("system.profile").drop();
db.createCollection("system.profile",{
capped: true,
size: 1048576
});
// ----------------------------
// Collection structure for 企业400号段数据
// ----------------------------
db.getCollection("企业400号段数据").drop();
db.createCollection("企业400号段数据");
db.getCollection("企业400号段数据").createIndex({
ID: NumberInt("1")
}, {
name: "ID_1"
});
db.getCollection("企业400号段数据").createIndex({
"企业名称": NumberInt("1")
}, {
name: "企业名称_1"
});
db.getCollection("企业400号段数据").createIndex({
"400号码": NumberInt("1")
}, {
name: "400号码_1"
});
db.getCollection("企业400号段数据").createIndex({
"联系人": NumberInt("1")
}, {
name: "联系人_1"
});
db.getCollection("企业400号段数据").createIndex({
"手机号码": NumberInt("1")
}, {
name: "手机号码_1"
});
db.getCollection("企业400号段数据").createIndex({
"表名": NumberInt("1")
}, {
name: "表名_1"
});
db.getCollection("企业400号段数据").createIndex({
"导入时间": NumberInt("-1")
}, {
name: "导入时间_-1"
});
db.getCollection("企业400号段数据").createIndex({
nid: NumberInt("1")
}, {
name: "nid_1"
});
db.getCollection("企业400号段数据").createIndex({
EnterpriseName: NumberInt("1")
}, {
name: "EnterpriseName_1"
});
db.getCollection("企业400号段数据").createIndex({
Number400: NumberInt("1")
}, {
name: "Number400_1"
});
db.getCollection("企业400号段数据").createIndex({
LinkMan: NumberInt("1")
}, {
name: "LinkMan_1"
});
db.getCollection("企业400号段数据").createIndex({
Mobile: NumberInt("1")
}, {
name: "Mobile_1"
});
// ----------------------------
// Collection structure for 统一用户资产表
// ----------------------------
db.getCollection("统一用户资产表").drop();
db.createCollection("统一用户资产表");
db.getCollection("统一用户资产表").createIndex({
"用户唯一ID": NumberInt("1")
}, {
name: "用户唯一ID_1"
});
db.getCollection("统一用户资产表").createIndex({
"基础信息.手机号": NumberInt("1")
}, {
name: "基础信息.手机号_1"
});
db.getCollection("统一用户资产表").createIndex({
"基础信息.邮箱": NumberInt("1")
}, {
name: "基础信息.邮箱_1"
});
db.getCollection("统一用户资产表").createIndex({
"企业信息.企业ID": NumberInt("1")
}, {
name: "企业信息.企业ID_1"
});
db.getCollection("统一用户资产表").createIndex({
"企业信息.400号码": NumberInt("1")
}, {
name: "企业信息.400号码_1"
});
db.getCollection("统一用户资产表").createIndex({
"资产概览.总资产价值": NumberInt("-1")
}, {
name: "资产概览.总资产价值_-1"
});
db.getCollection("统一用户资产表").createIndex({
"资产概览.资产等级": NumberInt("1")
}, {
name: "资产概览.资产等级_1"
});
db.getCollection("统一用户资产表").createIndex({
"用户类型": NumberInt("1")
}, {
name: "用户类型_1"
});
db.getCollection("统一用户资产表").createIndex({
"创建时间": NumberInt("-1")
}, {
name: "创建时间_-1"
});

View File

@@ -0,0 +1,115 @@
/*
Navicat Premium Data Transfer
Source Server : 标签数据库
Source Server Type : MongoDB
Source Server Version : 60025 (6.0.25)
Source Host : 192.168.2.6:27017
Source Schema : KR_企业名录
Target Server Type : MongoDB
Target Server Version : 60025 (6.0.25)
File Encoding : 65001
Date: 19/11/2025 15:19:51
*/
// ----------------------------
// Collection structure for system.profile
// ----------------------------
db.getCollection("system.profile").drop();
db.createCollection("system.profile",{
capped: true,
size: 1048576
});
// ----------------------------
// Collection structure for 中国医院名录_copy1
// ----------------------------
db.getCollection("中国医院名录_copy1").drop();
db.createCollection("中国医院名录_copy1");
// ----------------------------
// Collection structure for 企业及个体工商目录
// ----------------------------
db.getCollection("企业及个体工商目录").drop();
db.createCollection("企业及个体工商目录");
// ----------------------------
// Collection structure for 企业名录
// ----------------------------
db.getCollection("企业名录").drop();
db.createCollection("企业名录");
// ----------------------------
// Collection structure for 四川企业老板
// ----------------------------
db.getCollection("四川企业老板").drop();
db.createCollection("四川企业老板");
// ----------------------------
// Collection structure for 广东工商企业库1
// ----------------------------
db.getCollection("广东工商企业库1").drop();
db.createCollection("广东工商企业库1");
// ----------------------------
// Collection structure for 广东工商企业库2
// ----------------------------
db.getCollection("广东工商企业库2").drop();
db.createCollection("广东工商企业库2");
// ----------------------------
// Collection structure for 广东工商企业库3
// ----------------------------
db.getCollection("广东工商企业库3").drop();
db.createCollection("广东工商企业库3");
// ----------------------------
// Collection structure for 广东工商企业库4
// ----------------------------
db.getCollection("广东工商企业库4").drop();
db.createCollection("广东工商企业库4");
// ----------------------------
// Collection structure for 广东工商企业库5
// ----------------------------
db.getCollection("广东工商企业库5").drop();
db.createCollection("广东工商企业库5");
// ----------------------------
// Collection structure for 广东工商企业库6
// ----------------------------
db.getCollection("广东工商企业库6").drop();
db.createCollection("广东工商企业库6");
// ----------------------------
// Collection structure for 广东工商企业库7
// ----------------------------
db.getCollection("广东工商企业库7").drop();
db.createCollection("广东工商企业库7");
// ----------------------------
// Collection structure for 行政区划代码
// ----------------------------
db.getCollection("行政区划代码").drop();
db.createCollection("行政区划代码");
// ----------------------------
// Collection structure for 表1
// ----------------------------
db.getCollection("表1").drop();
db.createCollection("表1");
// ----------------------------
// Collection structure for 表2
// ----------------------------
db.getCollection("表2").drop();
db.createCollection("表2");
// ----------------------------
// Collection structure for 销售额3000万元-5000万元企业名录
// ----------------------------
db.getCollection("销售额3000万元-5000万元企业名录").drop();
db.createCollection("销售额3000万元-5000万元企业名录");

View File

@@ -0,0 +1,228 @@
/*
Navicat Premium Data Transfer
Source Server : 标签数据库
Source Server Type : MongoDB
Source Server Version : 60025 (6.0.25)
Source Host : 192.168.2.6:27017
Source Schema : KR_卡若私域
Target Server Type : MongoDB
Target Server Version : 60025 (6.0.25)
File Encoding : 65001
Date: 19/11/2025 15:19:24
*/
// ----------------------------
// Collection structure for system.profile
// ----------------------------
db.getCollection("system.profile").drop();
db.createCollection("system.profile",{
capped: true,
size: 1048576
});
// ----------------------------
// Collection structure for 存客宝流量池分类
// ----------------------------
db.getCollection("存客宝流量池分类").drop();
db.createCollection("存客宝流量池分类");
db.getCollection("存客宝流量池分类").createIndex({
province: NumberInt("1"),
wechat: NumberInt("1"),
phoneNumber: NumberInt("1"),
createdAt: NumberInt("1")
}, {
name: "user_province_wechat_phone_number_created_at"
});
db.getCollection("存客宝流量池分类").createIndex({
province: NumberInt("1"),
wechat: NumberInt("1"),
phoneNumber: NumberInt("1"),
wechatIsOpen: NumberInt("1"),
createdAt: NumberInt("1")
}, {
name: "user_province_wechat_phone_number_wechat_is_open_created_at"
});
// ----------------------------
// Collection structure for 存客宝流量池分类2
// ----------------------------
db.getCollection("存客宝流量池分类2").drop();
db.createCollection("存客宝流量池分类2");
db.getCollection("存客宝流量池分类2").createIndex({
source: NumberInt("1"),
wechat: NumberInt("1")
}, {
name: "user_source_wechat",
unique: true
});
db.getCollection("存客宝流量池分类2").createIndex({
source: NumberInt("1"),
phoneNumber: NumberInt("1")
}, {
name: "user_source_phone_number",
unique: true
});
db.getCollection("存客宝流量池分类2").createIndex({
province: NumberInt("1")
}, {
name: "user_province"
});
db.getCollection("存客宝流量池分类2").createIndex({
wechat: NumberInt("1")
}, {
name: "user_wechat"
});
db.getCollection("存客宝流量池分类2").createIndex({
phoneNumber: NumberInt("1")
}, {
name: "user_phone_number"
});
db.getCollection("存客宝流量池分类2").createIndex({
wechatIsOpen: NumberInt("1")
}, {
name: "user_wechat_is_open"
});
db.getCollection("存客宝流量池分类2").createIndex({
createdAt: NumberInt("1")
}, {
name: "user_created_at"
});
db.getCollection("存客宝流量池分类2").createIndex({
jdIsOpen: NumberInt("1")
}, {
name: "user_jd_is_open"
});
// ----------------------------
// Collection structure for 存客宝采集网红
// ----------------------------
db.getCollection("存客宝采集网红").drop();
db.createCollection("存客宝采集网红");
db.getCollection("存客宝采集网红").createIndex({
OwnerWechatId: NumberInt("1"),
Phone: NumberInt("1"),
Alias: NumberInt("1")
}, {
name: "IX_Kefure_WechatFriend_OwnerWechatId"
});
db.getCollection("存客宝采集网红").createIndex({
WechatId: NumberInt("1"),
IsSend: NumberInt("1"),
IsDeleted: NumberInt("1"),
Gender: NumberInt("1"),
Nickname: NumberInt("1")
}, {
name: "IX_Kefure_WechatFriend_WechatId_IsSend"
});
db.getCollection("存客宝采集网红").createIndex({
WechatAccountId: NumberInt("1")
}, {
name: "WechatAccountId"
});
// ----------------------------
// Collection structure for 存客宝采集网红有微信
// ----------------------------
db.getCollection("存客宝采集网红有微信").drop();
db.createCollection("存客宝采集网红有微信");
// ----------------------------
// Collection structure for 老坑爹商店 shop.lkdie.com
// ----------------------------
db.getCollection("老坑爹商店 shop.lkdie.com").drop();
db.createCollection("老坑爹商店 shop.lkdie.com");
db.getCollection("老坑爹商店 shop.lkdie.com").createIndex({
email: NumberInt("1")
}, {
name: "email",
unique: true
});
db.getCollection("老坑爹商店 shop.lkdie.com").createIndex({
nickname: NumberInt("1")
}, {
name: "nickname",
unique: true
});
db.getCollection("老坑爹商店 shop.lkdie.com").createIndex({
updatedTime: NumberInt("1")
}, {
name: "updatedTime"
});
// ----------------------------
// Collection structure for 老坑爹论坛www.lkdie.com 会员
// ----------------------------
db.getCollection("老坑爹论坛www.lkdie.com 会员").drop();
db.createCollection("老坑爹论坛www.lkdie.com 会员");
db.getCollection("老坑爹论坛www.lkdie.com 会员").createIndex({
username: NumberInt("1")
}, {
name: "username",
unique: true
});
db.getCollection("老坑爹论坛www.lkdie.com 会员").createIndex({
email: NumberInt("1")
}, {
name: "email"
});
// ----------------------------
// Collection structure for 黑科技www.quwanzhi.com 付款邮箱
// ----------------------------
db.getCollection("黑科技www.quwanzhi.com 付款邮箱").drop();
db.createCollection("黑科技www.quwanzhi.com 付款邮箱");
db.getCollection("黑科技www.quwanzhi.com 付款邮箱").createIndex({
trade_no: NumberInt("1")
}, {
name: "tradeNo"
});
db.getCollection("黑科技www.quwanzhi.com 付款邮箱").createIndex({
zid: NumberInt("1")
}, {
name: "zid"
});
db.getCollection("黑科技www.quwanzhi.com 付款邮箱").createIndex({
tid: NumberInt("1")
}, {
name: "tid"
});
// ----------------------------
// Collection structure for 黑科技www.quwanzhi.com 发卡邮箱
// ----------------------------
db.getCollection("黑科技www.quwanzhi.com 发卡邮箱").drop();
db.createCollection("黑科技www.quwanzhi.com 发卡邮箱");
db.getCollection("黑科技www.quwanzhi.com 发卡邮箱").createIndex({
zid: NumberInt("1")
}, {
name: "zid"
});
db.getCollection("黑科技www.quwanzhi.com 发卡邮箱").createIndex({
input: NumberInt("1")
}, {
name: "input"
});
db.getCollection("黑科技www.quwanzhi.com 发卡邮箱").createIndex({
userid: NumberInt("1")
}, {
name: "userid"
});
db.getCollection("黑科技www.quwanzhi.com 发卡邮箱").createIndex({
tradeno: NumberInt("1")
}, {
name: "tradeno"
});
db.getCollection("黑科技www.quwanzhi.com 发卡邮箱").createIndex({
id: NumberInt("1")
}, {
name: "id"
});
db.getCollection("黑科技www.quwanzhi.com 发卡邮箱").createIndex({
tid: NumberInt("1")
}, {
name: "tid"
});

View File

@@ -0,0 +1,122 @@
/*
Navicat Premium Data Transfer
Source Server : 标签数据库
Source Server Type : MongoDB
Source Server Version : 60025 (6.0.25)
Source Host : 192.168.2.6:27017
Source Schema : KR_商城
Target Server Type : MongoDB
Target Server Version : 60025 (6.0.25)
File Encoding : 65001
Date: 19/11/2025 15:20:13
*/
// ----------------------------
// Collection structure for 21年贝蒂喜订单整合
// ----------------------------
db.getCollection("21年贝蒂喜订单整合").drop();
db.createCollection("21年贝蒂喜订单整合");
db.getCollection("21年贝蒂喜订单整合").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
db.getCollection("21年贝蒂喜订单整合").createIndex({
"特权订金订单id": NumberInt("1")
}, {
name: "特权订金订单id_1"
});
db.getCollection("21年贝蒂喜订单整合").createIndex({
"新零售导购门店id": NumberInt("1")
}, {
name: "新零售导购门店id_1"
});
db.getCollection("21年贝蒂喜订单整合").createIndex({
"新零售发货门店id": NumberInt("1")
}, {
name: "新零售发货门店id_1"
});
db.getCollection("21年贝蒂喜订单整合").createIndex({
"新零售成交门店id": NumberInt("1")
}, {
name: "新零售成交门店id_1"
});
db.getCollection("21年贝蒂喜订单整合").createIndex({
"新零售成交经销商id": NumberInt("1")
}, {
name: "新零售成交经销商id_1"
});
db.getCollection("21年贝蒂喜订单整合").createIndex({
"店铺Id": NumberInt("1")
}, {
name: "店铺Id_1"
});
// ----------------------------
// Collection structure for system.profile
// ----------------------------
db.getCollection("system.profile").drop();
db.createCollection("system.profile",{
capped: true,
size: 1048576
});
// ----------------------------
// Collection structure for 凡客诚品_vancl.com
// ----------------------------
db.getCollection("凡客诚品_vancl.com").drop();
db.createCollection("凡客诚品_vancl.com");
db.getCollection("凡客诚品_vancl.com").createIndex({
name: NumberInt("1")
}, {
name: "name_1"
});
db.getCollection("凡客诚品_vancl.com").createIndex({
"数据来源": NumberInt("1"),
"导入时间": NumberInt("-1")
}, {
name: "数据来源_1_导入时间_-1"
});
// ----------------------------
// Collection structure for 嘟嘟牛
// ----------------------------
db.getCollection("嘟嘟牛").drop();
db.createCollection("嘟嘟牛");
// ----------------------------
// Collection structure for 嘟嘟牛 本地同城
// ----------------------------
db.getCollection("嘟嘟牛 本地同城").drop();
db.createCollection("嘟嘟牛 本地同城");
db.getCollection("嘟嘟牛 本地同城").createIndex({
username: NumberInt("1")
}, {
name: "username_1"
});
db.getCollection("嘟嘟牛 本地同城").createIndex({
email: NumberInt("1")
}, {
name: "email_1"
});
// ----------------------------
// Collection structure for 小米 xiaomi_com
// ----------------------------
db.getCollection("小米 xiaomi_com").drop();
db.createCollection("小米 xiaomi_com");
// ----------------------------
// Collection structure for 购物-北京一电购公司2月整理版30万
// ----------------------------
db.getCollection("购物-北京一电购公司2月整理版30万").drop();
db.createCollection("购物-北京一电购公司2月整理版30万");
db.getCollection("购物-北京一电购公司2月整理版30万").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});

View File

@@ -0,0 +1,359 @@
/*
Navicat Premium Data Transfer
Source Server : 标签数据库
Source Server Type : MongoDB
Source Server Version : 60025 (6.0.25)
Source Host : 192.168.2.6:27017
Source Schema : KR_国外
Target Server Type : MongoDB
Target Server Version : 60025 (6.0.25)
File Encoding : 65001
Date: 19/11/2025 15:18:56
*/
// ----------------------------
// Collection structure for Yandex俄罗斯百度用户数据
// ----------------------------
db.getCollection("Yandex俄罗斯百度用户数据").drop();
db.createCollection("Yandex俄罗斯百度用户数据");
db.getCollection("Yandex俄罗斯百度用户数据").createIndex({
email: NumberInt("1")
}, {
name: "email_1"
});
db.getCollection("Yandex俄罗斯百度用户数据").createIndex({
username: NumberInt("1")
}, {
name: "username_1"
});
db.getCollection("Yandex俄罗斯百度用户数据").createIndex({
domain: NumberInt("1")
}, {
name: "domain_1"
});
db.getCollection("Yandex俄罗斯百度用户数据").createIndex({
password_strength: NumberInt("1")
}, {
name: "password_strength_1"
});
db.getCollection("Yandex俄罗斯百度用户数据").createIndex({
country: NumberInt("1")
}, {
name: "country_1"
});
db.getCollection("Yandex俄罗斯百度用户数据").createIndex({
service_provider: NumberInt("1")
}, {
name: "service_provider_1"
});
db.getCollection("Yandex俄罗斯百度用户数据").createIndex({
created_at: NumberInt("-1")
}, {
name: "created_at_-1"
});
db.getCollection("Yandex俄罗斯百度用户数据").createIndex({
data_source: NumberInt("1")
}, {
name: "data_source_1"
});
db.getCollection("Yandex俄罗斯百度用户数据").createIndex({
email: NumberInt("1"),
password: NumberInt("1")
}, {
name: "email_password_unique",
unique: true
});
db.getCollection("Yandex俄罗斯百度用户数据").createIndex({
"$**": "text"
}, {
name: "text_search",
weights: {
email: NumberInt("1"),
username: NumberInt("1")
},
default_language: "english",
language_override: "language",
textIndexVersion: NumberInt("3")
});
// ----------------------------
// Collection structure for Yandex俄罗斯谷歌用户数据
// ----------------------------
db.getCollection("Yandex俄罗斯谷歌用户数据").drop();
db.createCollection("Yandex俄罗斯谷歌用户数据");
db.getCollection("Yandex俄罗斯谷歌用户数据").createIndex({
email: NumberInt("1")
}, {
name: "email_1"
});
db.getCollection("Yandex俄罗斯谷歌用户数据").createIndex({
username: NumberInt("1")
}, {
name: "username_1"
});
db.getCollection("Yandex俄罗斯谷歌用户数据").createIndex({
domain: NumberInt("1")
}, {
name: "domain_1"
});
db.getCollection("Yandex俄罗斯谷歌用户数据").createIndex({
password_strength: NumberInt("1")
}, {
name: "password_strength_1"
});
db.getCollection("Yandex俄罗斯谷歌用户数据").createIndex({
country: NumberInt("1")
}, {
name: "country_1"
});
db.getCollection("Yandex俄罗斯谷歌用户数据").createIndex({
service_provider: NumberInt("1")
}, {
name: "service_provider_1"
});
db.getCollection("Yandex俄罗斯谷歌用户数据").createIndex({
created_at: NumberInt("-1")
}, {
name: "created_at_-1"
});
db.getCollection("Yandex俄罗斯谷歌用户数据").createIndex({
data_source: NumberInt("1")
}, {
name: "data_source_1"
});
db.getCollection("Yandex俄罗斯谷歌用户数据").createIndex({
email: NumberInt("1"),
password: NumberInt("1")
}, {
name: "email_password_unique",
unique: true
});
db.getCollection("Yandex俄罗斯谷歌用户数据").createIndex({
"$**": "text"
}, {
name: "text_search",
weights: {
email: NumberInt("1"),
username: NumberInt("1")
},
default_language: "english",
language_override: "language",
textIndexVersion: NumberInt("3")
});
// ----------------------------
// Collection structure for system.profile
// ----------------------------
db.getCollection("system.profile").drop();
db.createCollection("system.profile",{
capped: true,
size: 1048576
});
// ----------------------------
// Collection structure for www.angeldiet.co.kr 1
// ----------------------------
db.getCollection("www.angeldiet.co.kr 1").drop();
db.createCollection("www.angeldiet.co.kr 1");
db.getCollection("www.angeldiet.co.kr 1").createIndex({
ID: NumberInt("1")
}, {
name: "ID_1"
});
// ----------------------------
// Collection structure for www.angeldiet.co.kr 2
// ----------------------------
db.getCollection("www.angeldiet.co.kr 2").drop();
db.createCollection("www.angeldiet.co.kr 2");
db.getCollection("www.angeldiet.co.kr 2").createIndex({
ID: NumberInt("1")
}, {
name: "ID_1"
});
// ----------------------------
// Collection structure for www.bookoa.com韩国学术出版
// ----------------------------
db.getCollection("www.bookoa.com韩国学术出版").drop();
db.createCollection("www.bookoa.com韩国学术出版");
db.getCollection("www.bookoa.com韩国学术出版").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
db.getCollection("www.bookoa.com韩国学术出版").createIndex({
email: NumberInt("1")
}, {
name: "email_1"
});
db.getCollection("www.bookoa.com韩国学术出版").createIndex({
hphone: NumberInt("1")
}, {
name: "hphone_1"
});
db.getCollection("www.bookoa.com韩国学术出版").createIndex({
name: NumberInt("1")
}, {
name: "name_1"
});
db.getCollection("www.bookoa.com韩国学术出版").createIndex({
email: NumberInt("1"),
name: NumberInt("1")
}, {
name: "email_1_name_1"
});
// ----------------------------
// Collection structure for 卡塔卡银行_APP数据
// ----------------------------
db.getCollection("卡塔卡银行_APP数据").drop();
db.createCollection("卡塔卡银行_APP数据");
// ----------------------------
// Collection structure for 卡塔卡银行_交易明细
// ----------------------------
db.getCollection("卡塔卡银行_交易明细").drop();
db.createCollection("卡塔卡银行_交易明细");
// ----------------------------
// Collection structure for 卡塔卡银行_交易明细临时
// ----------------------------
db.getCollection("卡塔卡银行_交易明细临时").drop();
db.createCollection("卡塔卡银行_交易明细临时");
// ----------------------------
// Collection structure for 卡塔卡银行_卡主表
// ----------------------------
db.getCollection("卡塔卡银行_卡主表").drop();
db.createCollection("卡塔卡银行_卡主表");
// ----------------------------
// Collection structure for 卡塔卡银行_卡列表
// ----------------------------
db.getCollection("卡塔卡银行_卡列表").drop();
db.createCollection("卡塔卡银行_卡列表");
// ----------------------------
// Collection structure for 卡塔卡银行_审计主表
// ----------------------------
db.getCollection("卡塔卡银行_审计主表").drop();
db.createCollection("卡塔卡银行_审计主表");
// ----------------------------
// Collection structure for 卡塔卡银行_客户
// ----------------------------
db.getCollection("卡塔卡银行_客户").drop();
db.createCollection("卡塔卡银行_客户");
// ----------------------------
// Collection structure for 卡塔卡银行_客户主表
// ----------------------------
db.getCollection("卡塔卡银行_客户主表").drop();
db.createCollection("卡塔卡银行_客户主表");
// ----------------------------
// Collection structure for 卡塔卡银行_收款人主表
// ----------------------------
db.getCollection("卡塔卡银行_收款人主表").drop();
db.createCollection("卡塔卡银行_收款人主表");
// ----------------------------
// Collection structure for 卡塔卡银行_用户档案
// ----------------------------
db.getCollection("卡塔卡银行_用户档案").drop();
db.createCollection("卡塔卡银行_用户档案");
// ----------------------------
// Collection structure for 卡塔卡银行_申请记录
// ----------------------------
db.getCollection("卡塔卡银行_申请记录").drop();
db.createCollection("卡塔卡银行_申请记录");
// ----------------------------
// Collection structure for 卡塔卡银行_电子对账单邮箱
// ----------------------------
db.getCollection("卡塔卡银行_电子对账单邮箱").drop();
db.createCollection("卡塔卡银行_电子对账单邮箱");
// ----------------------------
// Collection structure for 卡塔卡银行_订单客户明细
// ----------------------------
db.getCollection("卡塔卡银行_订单客户明细").drop();
db.createCollection("卡塔卡银行_订单客户明细");
// ----------------------------
// Collection structure for 卡塔卡银行_账户主表
// ----------------------------
db.getCollection("卡塔卡银行_账户主表").drop();
db.createCollection("卡塔卡银行_账户主表");
// ----------------------------
// Collection structure for 卡塔卡银行_身份证变更审计
// ----------------------------
db.getCollection("卡塔卡银行_身份证变更审计").drop();
db.createCollection("卡塔卡银行_身份证变更审计");
// ----------------------------
// Collection structure for 卡塔卡银行_黑名单用户
// ----------------------------
db.getCollection("卡塔卡银行_黑名单用户").drop();
db.createCollection("卡塔卡银行_黑名单用户");
// ----------------------------
// Collection structure for 台湾财经电视台
// ----------------------------
db.getCollection("台湾财经电视台").drop();
db.createCollection("台湾财经电视台");
db.getCollection("台湾财经电视台").createIndex({
email: NumberInt("1")
}, {
name: "email_1"
});
db.getCollection("台湾财经电视台").createIndex({
name: NumberInt("1")
}, {
name: "name_1"
});
db.getCollection("台湾财经电视台").createIndex({
phone: NumberInt("1")
}, {
name: "phone_1"
});
db.getCollection("台湾财经电视台").createIndex({
source: NumberInt("1")
}, {
name: "source_1"
});
// ----------------------------
// Collection structure for 土耳其公民
// ----------------------------
db.getCollection("土耳其公民").drop();
db.createCollection("土耳其公民");
// ----------------------------
// Collection structure for 社工库_1.7G_www.comicstorm.co.kr 85w_1
// ----------------------------
db.getCollection("社工库_1.7G_www.comicstorm.co.kr 85w_1").drop();
db.createCollection("社工库_1.7G_www.comicstorm.co.kr 85w_1");
db.getCollection("社工库_1.7G_www.comicstorm.co.kr 85w_1").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for 社工库_1.7G_www.hacker.co.kr 140W_140W
// ----------------------------
db.getCollection("社工库_1.7G_www.hacker.co.kr 140W_140W").drop();
db.createCollection("社工库_1.7G_www.hacker.co.kr 140W_140W");
db.getCollection("社工库_1.7G_www.hacker.co.kr 140W_140W").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});

View File

@@ -0,0 +1,618 @@
/*
Navicat Premium Data Transfer
Source Server : 标签数据库
Source Server Type : MongoDB
Source Server Version : 60025 (6.0.25)
Source Host : 192.168.2.6:27017
Source Schema : KR_存客宝
Target Server Type : MongoDB
Target Server Version : 60025 (6.0.25)
File Encoding : 65001
Date: 19/11/2025 15:18:41
*/
// ----------------------------
// Collection structure for cunkebao_AccountWechat
// ----------------------------
db.getCollection("cunkebao_AccountWechat").drop();
db.createCollection("cunkebao_AccountWechat");
db.getCollection("cunkebao_AccountWechat").createIndex({
lId: NumberInt("1")
}, {
name: "lId_1"
});
// ----------------------------
// Collection structure for cunkebao_DownloadVideoSchedule
// ----------------------------
db.getCollection("cunkebao_DownloadVideoSchedule").drop();
db.createCollection("cunkebao_DownloadVideoSchedule");
db.getCollection("cunkebao_DownloadVideoSchedule").createIndex({
lId: NumberInt("1")
}, {
name: "lId_1"
});
// ----------------------------
// Collection structure for cunkebao_FriendMessageTask
// ----------------------------
db.getCollection("cunkebao_FriendMessageTask").drop();
db.createCollection("cunkebao_FriendMessageTask");
db.getCollection("cunkebao_FriendMessageTask").createIndex({
lId: NumberInt("1")
}, {
name: "lId_1"
});
// ----------------------------
// Collection structure for cunkebao_FriendMessageTpl
// ----------------------------
db.getCollection("cunkebao_FriendMessageTpl").drop();
db.createCollection("cunkebao_FriendMessageTpl");
db.getCollection("cunkebao_FriendMessageTpl").createIndex({
lId: NumberInt("1")
}, {
name: "lId_1"
});
// ----------------------------
// Collection structure for cunkebao_FriendRequestConfig
// ----------------------------
db.getCollection("cunkebao_FriendRequestConfig").drop();
db.createCollection("cunkebao_FriendRequestConfig");
// ----------------------------
// Collection structure for cunkebao_FriendRequestTaskScan
// ----------------------------
db.getCollection("cunkebao_FriendRequestTaskScan").drop();
db.createCollection("cunkebao_FriendRequestTaskScan");
db.getCollection("cunkebao_FriendRequestTaskScan").createIndex({
lId: NumberInt("1")
}, {
name: "lId_1"
});
// ----------------------------
// Collection structure for cunkebao_FrontSession
// ----------------------------
db.getCollection("cunkebao_FrontSession").drop();
db.createCollection("cunkebao_FrontSession");
// ----------------------------
// Collection structure for cunkebao_JdPromotionSite
// ----------------------------
db.getCollection("cunkebao_JdPromotionSite").drop();
db.createCollection("cunkebao_JdPromotionSite");
db.getCollection("cunkebao_JdPromotionSite").createIndex({
lId: NumberInt("1")
}, {
name: "lId_1"
});
// ----------------------------
// Collection structure for cunkebao_JdSocialMedia
// ----------------------------
db.getCollection("cunkebao_JdSocialMedia").drop();
db.createCollection("cunkebao_JdSocialMedia");
db.getCollection("cunkebao_JdSocialMedia").createIndex({
lId: NumberInt("1")
}, {
name: "lId_1"
});
// ----------------------------
// Collection structure for cunkebao_MaterialLib
// ----------------------------
db.getCollection("cunkebao_MaterialLib").drop();
db.createCollection("cunkebao_MaterialLib");
db.getCollection("cunkebao_MaterialLib").createIndex({
lId: NumberInt("1")
}, {
name: "lId_1"
});
// ----------------------------
// Collection structure for cunkebao_MaterialPushLog
// ----------------------------
db.getCollection("cunkebao_MaterialPushLog").drop();
db.createCollection("cunkebao_MaterialPushLog");
db.getCollection("cunkebao_MaterialPushLog").createIndex({
lId: NumberInt("1")
}, {
name: "lId_1"
});
// ----------------------------
// Collection structure for cunkebao_MiniProgrameLib
// ----------------------------
db.getCollection("cunkebao_MiniProgrameLib").drop();
db.createCollection("cunkebao_MiniProgrameLib");
db.getCollection("cunkebao_MiniProgrameLib").createIndex({
lId: NumberInt("1")
}, {
name: "lId_1"
});
// ----------------------------
// Collection structure for cunkebao_PartTimer
// ----------------------------
db.getCollection("cunkebao_PartTimer").drop();
db.createCollection("cunkebao_PartTimer");
db.getCollection("cunkebao_PartTimer").createIndex({
lId: NumberInt("1")
}, {
name: "lId_1"
});
// ----------------------------
// Collection structure for cunkebao_Poster
// ----------------------------
db.getCollection("cunkebao_Poster").drop();
db.createCollection("cunkebao_Poster");
db.getCollection("cunkebao_Poster").createIndex({
lId: NumberInt("1")
}, {
name: "lId_1"
});
// ----------------------------
// Collection structure for cunkebao_SysAttach
// ----------------------------
db.getCollection("cunkebao_SysAttach").drop();
db.createCollection("cunkebao_SysAttach");
// ----------------------------
// Collection structure for cunkebao_SysAttach_20250228
// ----------------------------
db.getCollection("cunkebao_SysAttach_20250228").drop();
db.createCollection("cunkebao_SysAttach_20250228");
// ----------------------------
// Collection structure for cunkebao_SysAttach_copy
// ----------------------------
db.getCollection("cunkebao_SysAttach_copy").drop();
db.createCollection("cunkebao_SysAttach_copy");
// ----------------------------
// Collection structure for cunkebao_SysDataSource
// ----------------------------
db.getCollection("cunkebao_SysDataSource").drop();
db.createCollection("cunkebao_SysDataSource");
// ----------------------------
// Collection structure for cunkebao_SysModule
// ----------------------------
db.getCollection("cunkebao_SysModule").drop();
db.createCollection("cunkebao_SysModule");
// ----------------------------
// Collection structure for cunkebao_SysNavItem
// ----------------------------
db.getCollection("cunkebao_SysNavItem").drop();
db.createCollection("cunkebao_SysNavItem");
// ----------------------------
// Collection structure for cunkebao_SysObject
// ----------------------------
db.getCollection("cunkebao_SysObject").drop();
db.createCollection("cunkebao_SysObject");
// ----------------------------
// Collection structure for cunkebao_SysOperatorPermission
// ----------------------------
db.getCollection("cunkebao_SysOperatorPermission").drop();
db.createCollection("cunkebao_SysOperatorPermission");
// ----------------------------
// Collection structure for cunkebao_SysRelatedList
// ----------------------------
db.getCollection("cunkebao_SysRelatedList").drop();
db.createCollection("cunkebao_SysRelatedList");
// ----------------------------
// Collection structure for cunkebao_SysShare
// ----------------------------
db.getCollection("cunkebao_SysShare").drop();
db.createCollection("cunkebao_SysShare");
// ----------------------------
// Collection structure for cunkebao_SysSolution
// ----------------------------
db.getCollection("cunkebao_SysSolution").drop();
db.createCollection("cunkebao_SysSolution");
// ----------------------------
// Collection structure for cunkebao_SysUI
// ----------------------------
db.getCollection("cunkebao_SysUI").drop();
db.createCollection("cunkebao_SysUI");
// ----------------------------
// Collection structure for cunkebao_UserIncome
// ----------------------------
db.getCollection("cunkebao_UserIncome").drop();
db.createCollection("cunkebao_UserIncome");
db.getCollection("cunkebao_UserIncome").createIndex({
lId: NumberInt("1")
}, {
name: "lId_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_administrator_permissions
// ----------------------------
db.getCollection("cunkebao_v3_ck_administrator_permissions").drop();
db.createCollection("cunkebao_v3_ck_administrator_permissions");
db.getCollection("cunkebao_v3_ck_administrator_permissions").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_administrators
// ----------------------------
db.getCollection("cunkebao_v3_ck_administrators").drop();
db.createCollection("cunkebao_v3_ck_administrators");
db.getCollection("cunkebao_v3_ck_administrators").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_call_recording
// ----------------------------
db.getCollection("cunkebao_v3_ck_call_recording").drop();
db.createCollection("cunkebao_v3_ck_call_recording");
db.getCollection("cunkebao_v3_ck_call_recording").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_company
// ----------------------------
db.getCollection("cunkebao_v3_ck_company").drop();
db.createCollection("cunkebao_v3_ck_company");
db.getCollection("cunkebao_v3_ck_company").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_customer_acquisition_task
// ----------------------------
db.getCollection("cunkebao_v3_ck_customer_acquisition_task").drop();
db.createCollection("cunkebao_v3_ck_customer_acquisition_task");
db.getCollection("cunkebao_v3_ck_customer_acquisition_task").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_flow_package_order
// ----------------------------
db.getCollection("cunkebao_v3_ck_flow_package_order").drop();
db.createCollection("cunkebao_v3_ck_flow_package_order");
db.getCollection("cunkebao_v3_ck_flow_package_order").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_flow_usage_record
// ----------------------------
db.getCollection("cunkebao_v3_ck_flow_usage_record").drop();
db.createCollection("cunkebao_v3_ck_flow_usage_record");
db.getCollection("cunkebao_v3_ck_flow_usage_record").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_jd_promotion_site
// ----------------------------
db.getCollection("cunkebao_v3_ck_jd_promotion_site").drop();
db.createCollection("cunkebao_v3_ck_jd_promotion_site");
db.getCollection("cunkebao_v3_ck_jd_promotion_site").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_jd_social_media
// ----------------------------
db.getCollection("cunkebao_v3_ck_jd_social_media").drop();
db.createCollection("cunkebao_v3_ck_jd_social_media");
db.getCollection("cunkebao_v3_ck_jd_social_media").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_menus
// ----------------------------
db.getCollection("cunkebao_v3_ck_menus").drop();
db.createCollection("cunkebao_v3_ck_menus");
db.getCollection("cunkebao_v3_ck_menus").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_plan_scene
// ----------------------------
db.getCollection("cunkebao_v3_ck_plan_scene").drop();
db.createCollection("cunkebao_v3_ck_plan_scene");
db.getCollection("cunkebao_v3_ck_plan_scene").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_plan_tags
// ----------------------------
db.getCollection("cunkebao_v3_ck_plan_tags").drop();
db.createCollection("cunkebao_v3_ck_plan_tags");
db.getCollection("cunkebao_v3_ck_plan_tags").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_task_customer
// ----------------------------
db.getCollection("cunkebao_v3_ck_task_customer").drop();
db.createCollection("cunkebao_v3_ck_task_customer");
db.getCollection("cunkebao_v3_ck_task_customer").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_traffic_pool
// ----------------------------
db.getCollection("cunkebao_v3_ck_traffic_pool").drop();
db.createCollection("cunkebao_v3_ck_traffic_pool");
db.getCollection("cunkebao_v3_ck_traffic_pool").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_traffic_source
// ----------------------------
db.getCollection("cunkebao_v3_ck_traffic_source").drop();
db.createCollection("cunkebao_v3_ck_traffic_source");
db.getCollection("cunkebao_v3_ck_traffic_source").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_traffic_source_package
// ----------------------------
db.getCollection("cunkebao_v3_ck_traffic_source_package").drop();
db.createCollection("cunkebao_v3_ck_traffic_source_package");
db.getCollection("cunkebao_v3_ck_traffic_source_package").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_traffic_source_package_item
// ----------------------------
db.getCollection("cunkebao_v3_ck_traffic_source_package_item").drop();
db.createCollection("cunkebao_v3_ck_traffic_source_package_item");
db.getCollection("cunkebao_v3_ck_traffic_source_package_item").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_traffic_tag
// ----------------------------
db.getCollection("cunkebao_v3_ck_traffic_tag").drop();
db.createCollection("cunkebao_v3_ck_traffic_tag");
db.getCollection("cunkebao_v3_ck_traffic_tag").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_user_flow_package
// ----------------------------
db.getCollection("cunkebao_v3_ck_user_flow_package").drop();
db.createCollection("cunkebao_v3_ck_user_flow_package");
db.getCollection("cunkebao_v3_ck_user_flow_package").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_user_portrait
// ----------------------------
db.getCollection("cunkebao_v3_ck_user_portrait").drop();
db.createCollection("cunkebao_v3_ck_user_portrait");
db.getCollection("cunkebao_v3_ck_user_portrait").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_vendor_project
// ----------------------------
db.getCollection("cunkebao_v3_ck_vendor_project").drop();
db.createCollection("cunkebao_v3_ck_vendor_project");
db.getCollection("cunkebao_v3_ck_vendor_project").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_wechat_friendship
// ----------------------------
db.getCollection("cunkebao_v3_ck_wechat_friendship").drop();
db.createCollection("cunkebao_v3_ck_wechat_friendship");
db.getCollection("cunkebao_v3_ck_wechat_friendship").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_wechat_group_member
// ----------------------------
db.getCollection("cunkebao_v3_ck_wechat_group_member").drop();
db.createCollection("cunkebao_v3_ck_wechat_group_member");
db.getCollection("cunkebao_v3_ck_wechat_group_member").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_wechat_tag
// ----------------------------
db.getCollection("cunkebao_v3_ck_wechat_tag").drop();
db.createCollection("cunkebao_v3_ck_wechat_tag");
db.getCollection("cunkebao_v3_ck_wechat_tag").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_workbench
// ----------------------------
db.getCollection("cunkebao_v3_ck_workbench").drop();
db.createCollection("cunkebao_v3_ck_workbench");
db.getCollection("cunkebao_v3_ck_workbench").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_workbench_group_create_item
// ----------------------------
db.getCollection("cunkebao_v3_ck_workbench_group_create_item").drop();
db.createCollection("cunkebao_v3_ck_workbench_group_create_item");
db.getCollection("cunkebao_v3_ck_workbench_group_create_item").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_workbench_group_push
// ----------------------------
db.getCollection("cunkebao_v3_ck_workbench_group_push").drop();
db.createCollection("cunkebao_v3_ck_workbench_group_push");
db.getCollection("cunkebao_v3_ck_workbench_group_push").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_ck_workbench_group_push_item
// ----------------------------
db.getCollection("cunkebao_v3_ck_workbench_group_push_item").drop();
db.createCollection("cunkebao_v3_ck_workbench_group_push_item");
db.getCollection("cunkebao_v3_ck_workbench_group_push_item").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_s2_allot_rule
// ----------------------------
db.getCollection("cunkebao_v3_s2_allot_rule").drop();
db.createCollection("cunkebao_v3_s2_allot_rule");
db.getCollection("cunkebao_v3_s2_allot_rule").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for cunkebao_v3_s2_device_group
// ----------------------------
db.getCollection("cunkebao_v3_s2_device_group").drop();
db.createCollection("cunkebao_v3_s2_device_group");
db.getCollection("cunkebao_v3_s2_device_group").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
// ----------------------------
// Collection structure for system.profile
// ----------------------------
db.getCollection("system.profile").drop();
db.createCollection("system.profile",{
capped: true,
size: 1048576
});
// ----------------------------
// Collection structure for 好友关系网络汇总表
// ----------------------------
db.getCollection("好友关系网络汇总表").drop();
db.createCollection("好友关系网络汇总表");
db.getCollection("好友关系网络汇总表").createIndex({
"用户微信ID": NumberInt("1")
}, {
name: "用户微信ID_1"
});
db.getCollection("好友关系网络汇总表").createIndex({
"好友微信ID": NumberInt("1")
}, {
name: "好友微信ID_1"
});
db.getCollection("好友关系网络汇总表").createIndex({
"关系等级": NumberInt("1")
}, {
name: "关系等级_1"
});
db.getCollection("好友关系网络汇总表").createIndex({
"RFM评分.总分": NumberInt("1")
}, {
name: "RFM评分.总分_1"
});
db.getCollection("好友关系网络汇总表").createIndex({
"关系强度评分": NumberInt("1")
}, {
name: "关系强度评分_1"
});
db.getCollection("好友关系网络汇总表").createIndex({
"公司ID": NumberInt("1")
}, {
name: "公司ID_1"
});

View File

@@ -0,0 +1,65 @@
/*
Navicat Premium Data Transfer
Source Server : 标签数据库
Source Server Type : MongoDB
Source Server Version : 60025 (6.0.25)
Source Host : 192.168.2.6:27017
Source Schema : KR_存客宝_四表重构KR_KR版
Target Server Type : MongoDB
Target Server Version : 60025 (6.0.25)
File Encoding : 65001
Date: 19/11/2025 15:18:48
*/
// ----------------------------
// Collection structure for system.profile
// ----------------------------
db.getCollection("system.profile").drop();
db.createCollection("system.profile",{
capped: true,
size: 1048576
});
// ----------------------------
// Collection structure for 估值模式表
// ----------------------------
db.getCollection("估值模式表").drop();
db.createCollection("估值模式表");
// ----------------------------
// Collection structure for 微信号管理表
// ----------------------------
db.getCollection("微信号管理表").drop();
db.createCollection("微信号管理表");
// ----------------------------
// Collection structure for 微信好友表
// ----------------------------
db.getCollection("微信好友表").drop();
db.createCollection("微信好友表");
// ----------------------------
// Collection structure for 设备管理表
// ----------------------------
db.getCollection("设备管理表").drop();
db.createCollection("设备管理表");
db.getCollection("设备管理表").createIndex({
"设备ID": NumberInt("1")
}, {
name: "设备ID_1",
unique: true
});
db.getCollection("设备管理表").createIndex({
"用户ID": NumberInt("1")
}, {
name: "用户ID_1"
});
db.getCollection("设备管理表").createIndex({
"设备类型": NumberInt("1")
}, {
name: "设备类型_1"
});

View File

@@ -0,0 +1,36 @@
/*
Navicat Premium Data Transfer
Source Server : 标签数据库
Source Server Type : MongoDB
Source Server Version : 60025 (6.0.25)
Source Host : 192.168.2.6:27017
Source Schema : KR_微博
Target Server Type : MongoDB
Target Server Version : 60025 (6.0.25)
File Encoding : 65001
Date: 19/11/2025 15:20:57
*/
// ----------------------------
// Collection structure for system.profile
// ----------------------------
db.getCollection("system.profile").drop();
db.createCollection("system.profile",{
capped: true,
size: 1048576
});
// ----------------------------
// Collection structure for 微博uid+手机
// ----------------------------
db.getCollection("微博uid+手机").drop();
db.createCollection("微博uid+手机");
db.getCollection("微博uid+手机").createIndex({
phone: NumberInt("1")
}, {
name: "phone_1"
});

View File

@@ -0,0 +1,90 @@
/*
Navicat Premium Data Transfer
Source Server : 标签数据库
Source Server Type : MongoDB
Source Server Version : 60025 (6.0.25)
Source Host : 192.168.2.6:27017
Source Schema : KR_快递
Target Server Type : MongoDB
Target Server Version : 60025 (6.0.25)
File Encoding : 65001
Date: 19/11/2025 15:19:31
*/
// ----------------------------
// Collection structure for system.profile
// ----------------------------
db.getCollection("system.profile").drop();
db.createCollection("system.profile",{
capped: true,
size: 1048576
});
// ----------------------------
// Collection structure for wlzhwu_jiezhang
// ----------------------------
db.getCollection("wlzhwu_jiezhang").drop();
db.createCollection("wlzhwu_jiezhang");
// ----------------------------
// Collection structure for wlzhwu_wlzhangwu
// ----------------------------
db.getCollection("wlzhwu_wlzhangwu").drop();
db.createCollection("wlzhwu_wlzhangwu");
// ----------------------------
// Collection structure for 可行路由
// ----------------------------
db.getCollection("可行路由").drop();
db.createCollection("可行路由");
db.getCollection("可行路由").createIndex({
_import_time: NumberInt("1")
}, {
name: "_import_time_1"
});
// ----------------------------
// Collection structure for 圆通_普通客户
// ----------------------------
db.getCollection("圆通_普通客户").drop();
db.createCollection("圆通_普通客户");
db.getCollection("圆通_普通客户").createIndex({
username: NumberInt("1")
}, {
name: "username_1"
});
db.getCollection("圆通_普通客户").createIndex({
phone: NumberInt("1")
}, {
name: "phone_1"
});
db.getCollection("圆通_普通客户").createIndex({
user_score: NumberInt("-1")
}, {
name: "user_score_-1"
});
// ----------------------------
// Collection structure for 圆通_潜在客户
// ----------------------------
db.getCollection("圆通_潜在客户").drop();
db.createCollection("圆通_潜在客户");
db.getCollection("圆通_潜在客户").createIndex({
username: NumberInt("1")
}, {
name: "username_1"
});
db.getCollection("圆通_潜在客户").createIndex({
phone: NumberInt("1")
}, {
name: "phone_1"
});
db.getCollection("圆通_潜在客户").createIndex({
user_score: NumberInt("-1")
}, {
name: "user_score_-1"
});

View File

@@ -0,0 +1,46 @@
/*
Navicat Premium Data Transfer
Source Server : 标签数据库
Source Server Type : MongoDB
Source Server Version : 60025 (6.0.25)
Source Host : 192.168.2.6:27017
Source Schema : KR_户口
Target Server Type : MongoDB
Target Server Version : 60025 (6.0.25)
File Encoding : 65001
Date: 19/11/2025 15:19:03
*/
// ----------------------------
// Collection structure for system.profile
// ----------------------------
db.getCollection("system.profile").drop();
db.createCollection("system.profile",{
capped: true,
size: 1048576
});
// ----------------------------
// Collection structure for 户籍数据
// ----------------------------
db.getCollection("户籍数据").drop();
db.createCollection("户籍数据");
db.getCollection("户籍数据").createIndex({
"姓名": NumberInt("1")
}, {
name: "姓名_1"
});
db.getCollection("户籍数据").createIndex({
"身份证号": NumberInt("1")
}, {
name: "身份证号_1"
});
db.getCollection("户籍数据").createIndex({
"户籍地址": NumberInt("1")
}, {
name: "户籍地址_1"
});

View File

@@ -0,0 +1,186 @@
/*
Navicat Premium Data Transfer
Source Server : 标签数据库
Source Server Type : MongoDB
Source Server Version : 60025 (6.0.25)
Source Host : 192.168.2.6:27017
Source Schema : KR_手机
Target Server Type : MongoDB
Target Server Version : 60025 (6.0.25)
File Encoding : 65001
Date: 19/11/2025 15:20:20
*/
// ----------------------------
// Collection structure for system.profile
// ----------------------------
db.getCollection("system.profile").drop();
db.createCollection("system.profile",{
capped: true,
size: 1048576
});
// ----------------------------
// Collection structure for tmpgSyh4.renameCollection
// ----------------------------
db.getCollection("tmpgSyh4.renameCollection").drop();
db.createCollection("tmpgSyh4.renameCollection");
// ----------------------------
// Collection structure for 广东gd5800万神州行动感地带=广州GZBran2_copy2
// ----------------------------
db.getCollection("广东gd5800万神州行动感地带=广州GZBran2_copy2").drop();
db.createCollection("广东gd5800万神州行动感地带=广州GZBran2_copy2");
// ----------------------------
// Collection structure for 广东广州航空数据1
// ----------------------------
db.getCollection("广东广州航空数据1").drop();
db.createCollection("广东广州航空数据1");
// ----------------------------
// Collection structure for 广东广州航空数据2
// ----------------------------
db.getCollection("广东广州航空数据2").drop();
db.createCollection("广东广州航空数据2");
// ----------------------------
// Collection structure for 广东广州航空数据3
// ----------------------------
db.getCollection("广东广州航空数据3").drop();
db.createCollection("广东广州航空数据3");
// ----------------------------
// Collection structure for 广东广州航空数据4
// ----------------------------
db.getCollection("广东广州航空数据4").drop();
db.createCollection("广东广州航空数据4");
db.getCollection("广东广州航空数据4").createIndex({
"电话号码": NumberInt("1")
}, {
name: "电话号码_1"
});
// ----------------------------
// Collection structure for 广东深圳全球通女
// ----------------------------
db.getCollection("广东深圳全球通女").drop();
db.createCollection("广东深圳全球通女");
// ----------------------------
// Collection structure for 江西移动_701鹰潭
// ----------------------------
db.getCollection("江西移动_701鹰潭").drop();
db.createCollection("江西移动_701鹰潭");
// ----------------------------
// Collection structure for 江西移动_792九江
// ----------------------------
db.getCollection("江西移动_792九江").drop();
db.createCollection("江西移动_792九江");
// ----------------------------
// Collection structure for 江西移动_793上饶
// ----------------------------
db.getCollection("江西移动_793上饶").drop();
db.createCollection("江西移动_793上饶");
// ----------------------------
// Collection structure for 江西移动_798景德镇
// ----------------------------
db.getCollection("江西移动_798景德镇").drop();
db.createCollection("江西移动_798景德镇");
// ----------------------------
// Collection structure for 江西移动_799萍乡
// ----------------------------
db.getCollection("江西移动_799萍乡").drop();
db.createCollection("江西移动_799萍乡");
// ----------------------------
// Collection structure for 江西移动_上绕262万
// ----------------------------
db.getCollection("江西移动_上绕262万").drop();
db.createCollection("江西移动_上绕262万");
// ----------------------------
// Collection structure for 江西移动_九江222万
// ----------------------------
db.getCollection("江西移动_九江222万").drop();
db.createCollection("江西移动_九江222万");
// ----------------------------
// Collection structure for 江西移动_南昌368万
// ----------------------------
db.getCollection("江西移动_南昌368万").drop();
db.createCollection("江西移动_南昌368万");
// ----------------------------
// Collection structure for 江西移动_吉安206万
// ----------------------------
db.getCollection("江西移动_吉安206万").drop();
db.createCollection("江西移动_吉安206万");
// ----------------------------
// Collection structure for 江西移动_宜春230万
// ----------------------------
db.getCollection("江西移动_宜春230万").drop();
db.createCollection("江西移动_宜春230万");
// ----------------------------
// Collection structure for 江西移动_抚州143万
// ----------------------------
db.getCollection("江西移动_抚州143万").drop();
db.createCollection("江西移动_抚州143万");
// ----------------------------
// Collection structure for 江西移动_新余67万
// ----------------------------
db.getCollection("江西移动_新余67万").drop();
db.createCollection("江西移动_新余67万");
// ----------------------------
// Collection structure for 江西移动_景德镇76万
// ----------------------------
db.getCollection("江西移动_景德镇76万").drop();
db.createCollection("江西移动_景德镇76万");
// ----------------------------
// Collection structure for 江西移动_江西全球通带身份证
// ----------------------------
db.getCollection("江西移动_江西全球通带身份证").drop();
db.createCollection("江西移动_江西全球通带身份证");
// ----------------------------
// Collection structure for 江西移动_江西全球通身份证
// ----------------------------
db.getCollection("江西移动_江西全球通身份证").drop();
db.createCollection("江西移动_江西全球通身份证");
// ----------------------------
// Collection structure for 江西移动_江西全球通身份证姓名1226056
// ----------------------------
db.getCollection("江西移动_江西全球通身份证姓名1226056").drop();
db.createCollection("江西移动_江西全球通身份证姓名1226056");
// ----------------------------
// Collection structure for 江西移动_萍乡86万
// ----------------------------
db.getCollection("江西移动_萍乡86万").drop();
db.createCollection("江西移动_萍乡86万");
// ----------------------------
// Collection structure for 江西移动_赣州352万
// ----------------------------
db.getCollection("江西移动_赣州352万").drop();
db.createCollection("江西移动_赣州352万");
// ----------------------------
// Collection structure for 江西移动_鹰潭53万
// ----------------------------
db.getCollection("江西移动_鹰潭53万").drop();
db.createCollection("江西移动_鹰潭53万");

View File

@@ -0,0 +1,223 @@
/*
Navicat Premium Data Transfer
Source Server : 标签数据库
Source Server Type : MongoDB
Source Server Version : 60025 (6.0.25)
Source Host : 192.168.2.6:27017
Source Schema : KR_投资
Target Server Type : MongoDB
Target Server Version : 60025 (6.0.25)
File Encoding : 65001
Date: 19/11/2025 15:20:51
*/
// ----------------------------
// Collection structure for system.profile
// ----------------------------
db.getCollection("system.profile").drop();
db.createCollection("system.profile",{
capped: true,
size: 1048576
});
// ----------------------------
// Collection structure for 上海
// ----------------------------
db.getCollection("上海").drop();
db.createCollection("上海");
// ----------------------------
// Collection structure for 云南
// ----------------------------
db.getCollection("云南").drop();
db.createCollection("云南");
// ----------------------------
// Collection structure for 内蒙古
// ----------------------------
db.getCollection("内蒙古").drop();
db.createCollection("内蒙古");
// ----------------------------
// Collection structure for 北京
// ----------------------------
db.getCollection("北京").drop();
db.createCollection("北京");
// ----------------------------
// Collection structure for 吉林
// ----------------------------
db.getCollection("吉林").drop();
db.createCollection("吉林");
// ----------------------------
// Collection structure for 商务部世界买家数据库 此数据由www_10195_com提供
// ----------------------------
db.getCollection("商务部世界买家数据库 此数据由www_10195_com提供").drop();
db.createCollection("商务部世界买家数据库 此数据由www_10195_com提供");
// ----------------------------
// Collection structure for 四川
// ----------------------------
db.getCollection("四川").drop();
db.createCollection("四川");
// ----------------------------
// Collection structure for 天津
// ----------------------------
db.getCollection("天津").drop();
db.createCollection("天津");
// ----------------------------
// Collection structure for 宁夏
// ----------------------------
db.getCollection("宁夏").drop();
db.createCollection("宁夏");
// ----------------------------
// Collection structure for 安徽
// ----------------------------
db.getCollection("安徽").drop();
db.createCollection("安徽");
// ----------------------------
// Collection structure for 山东
// ----------------------------
db.getCollection("山东").drop();
db.createCollection("山东");
// ----------------------------
// Collection structure for 山西
// ----------------------------
db.getCollection("山西").drop();
db.createCollection("山西");
// ----------------------------
// Collection structure for 广东
// ----------------------------
db.getCollection("广东").drop();
db.createCollection("广东");
// ----------------------------
// Collection structure for 广西
// ----------------------------
db.getCollection("广西").drop();
db.createCollection("广西");
// ----------------------------
// Collection structure for 新疆
// ----------------------------
db.getCollection("新疆").drop();
db.createCollection("新疆");
// ----------------------------
// Collection structure for 江苏
// ----------------------------
db.getCollection("江苏").drop();
db.createCollection("江苏");
// ----------------------------
// Collection structure for 江西
// ----------------------------
db.getCollection("江西").drop();
db.createCollection("江西");
// ----------------------------
// Collection structure for 河北
// ----------------------------
db.getCollection("河北").drop();
db.createCollection("河北");
// ----------------------------
// Collection structure for 河南
// ----------------------------
db.getCollection("河南").drop();
db.createCollection("河南");
// ----------------------------
// Collection structure for 浙江
// ----------------------------
db.getCollection("浙江").drop();
db.createCollection("浙江");
// ----------------------------
// Collection structure for 海南
// ----------------------------
db.getCollection("海南").drop();
db.createCollection("海南");
// ----------------------------
// Collection structure for 湖北
// ----------------------------
db.getCollection("湖北").drop();
db.createCollection("湖北");
// ----------------------------
// Collection structure for 湖南
// ----------------------------
db.getCollection("湖南").drop();
db.createCollection("湖南");
// ----------------------------
// Collection structure for 甘肃
// ----------------------------
db.getCollection("甘肃").drop();
db.createCollection("甘肃");
// ----------------------------
// Collection structure for 省份
// ----------------------------
db.getCollection("省份").drop();
db.createCollection("省份");
// ----------------------------
// Collection structure for 福建
// ----------------------------
db.getCollection("福建").drop();
db.createCollection("福建");
// ----------------------------
// Collection structure for 西藏
// ----------------------------
db.getCollection("西藏").drop();
db.createCollection("西藏");
// ----------------------------
// Collection structure for 贵州
// ----------------------------
db.getCollection("贵州").drop();
db.createCollection("贵州");
// ----------------------------
// Collection structure for 辽宁
// ----------------------------
db.getCollection("辽宁").drop();
db.createCollection("辽宁");
// ----------------------------
// Collection structure for 重庆
// ----------------------------
db.getCollection("重庆").drop();
db.createCollection("重庆");
// ----------------------------
// Collection structure for 陕西
// ----------------------------
db.getCollection("陕西").drop();
db.createCollection("陕西");
// ----------------------------
// Collection structure for 青海
// ----------------------------
db.getCollection("青海").drop();
db.createCollection("青海");
// ----------------------------
// Collection structure for 黑龙江
// ----------------------------
db.getCollection("黑龙江").drop();
db.createCollection("黑龙江");

View File

@@ -0,0 +1,223 @@
/*
Navicat Premium Data Transfer
Source Server : 标签数据库
Source Server Type : MongoDB
Source Server Version : 60025 (6.0.25)
Source Host : 192.168.2.6:27017
Source Schema : KR_淘宝
Target Server Type : MongoDB
Target Server Version : 60025 (6.0.25)
File Encoding : 65001
Date: 19/11/2025 15:20:35
*/
// ----------------------------
// Collection structure for system.profile
// ----------------------------
db.getCollection("system.profile").drop();
db.createCollection("system.profile",{
capped: true,
size: 1048576
});
// ----------------------------
// Collection structure for zippo1去重复后33万
// ----------------------------
db.getCollection("zippo1去重复后33万").drop();
db.createCollection("zippo1去重复后33万");
// ----------------------------
// Collection structure for zippo2
// ----------------------------
db.getCollection("zippo2").drop();
db.createCollection("zippo2");
// ----------------------------
// Collection structure for zippo3
// ----------------------------
db.getCollection("zippo3").drop();
db.createCollection("zippo3");
// ----------------------------
// Collection structure for zippo4
// ----------------------------
db.getCollection("zippo4").drop();
db.createCollection("zippo4");
// ----------------------------
// Collection structure for zippo5
// ----------------------------
db.getCollection("zippo5").drop();
db.createCollection("zippo5");
// ----------------------------
// Collection structure for 办公设备文具去重复64.5万)
// ----------------------------
db.getCollection("办公设备文具去重复64.5万)").drop();
db.createCollection("办公设备文具去重复64.5万)");
// ----------------------------
// Collection structure for 包去重复后71万
// ----------------------------
db.getCollection("包去重复后71万").drop();
db.createCollection("包去重复后71万");
// ----------------------------
// Collection structure for 卖家邮箱去重复后300万
// ----------------------------
db.getCollection("卖家邮箱去重复后300万").drop();
db.createCollection("卖家邮箱去重复后300万");
// ----------------------------
// Collection structure for 女士内衣去重复后132.6万)
// ----------------------------
db.getCollection("女士内衣去重复后132.6万)").drop();
db.createCollection("女士内衣去重复后132.6万)");
// ----------------------------
// Collection structure for 女士箱包配件去重复后34.8万)
// ----------------------------
db.getCollection("女士箱包配件去重复后34.8万)").drop();
db.createCollection("女士箱包配件去重复后34.8万)");
// ----------------------------
// Collection structure for 女装1去重复后388.6万)
// ----------------------------
db.getCollection("女装1去重复后388.6万)").drop();
db.createCollection("女装1去重复后388.6万)");
// ----------------------------
// Collection structure for 女装2去重复后83万带旺旺号及支付宝
// ----------------------------
db.getCollection("女装2去重复后83万带旺旺号及支付宝").drop();
db.createCollection("女装2去重复后83万带旺旺号及支付宝");
// ----------------------------
// Collection structure for 女鞋去重复后118.6万)
// ----------------------------
db.getCollection("女鞋去重复后118.6万)").drop();
db.createCollection("女鞋去重复后118.6万)");
// ----------------------------
// Collection structure for 家具定制代购去重复后20万
// ----------------------------
db.getCollection("家具定制代购去重复后20万").drop();
db.createCollection("家具定制代购去重复后20万");
// ----------------------------
// Collection structure for 家用电器去重复后93.5万)
// ----------------------------
db.getCollection("家用电器去重复后93.5万)").drop();
db.createCollection("家用电器去重复后93.5万)");
// ----------------------------
// Collection structure for 床上用品去重复后19万
// ----------------------------
db.getCollection("床上用品去重复后19万").drop();
db.createCollection("床上用品去重复后19万");
// ----------------------------
// Collection structure for 彩妆护肤去重复后138万
// ----------------------------
db.getCollection("彩妆护肤去重复后138万").drop();
db.createCollection("彩妆护肤去重复后138万");
// ----------------------------
// Collection structure for 成人用品避孕用品情趣内衣去重复后8万
// ----------------------------
db.getCollection("成人用品避孕用品情趣内衣去重复后8万").drop();
db.createCollection("成人用品避孕用品情趣内衣去重复后8万");
// ----------------------------
// Collection structure for 户外军品旅游机票去重复后64万
// ----------------------------
db.getCollection("户外军品旅游机票去重复后64万").drop();
db.createCollection("户外军品旅游机票去重复后64万");
// ----------------------------
// Collection structure for 手机去重复后80万
// ----------------------------
db.getCollection("手机去重复后80万").drop();
db.createCollection("手机去重复后80万");
// ----------------------------
// Collection structure for 数码相机图形冲印去重复后23.8万)
// ----------------------------
db.getCollection("数码相机图形冲印去重复后23.8万)").drop();
db.createCollection("数码相机图形冲印去重复后23.8万)");
// ----------------------------
// Collection structure for 数码配件电子元件去重复后51.6万)
// ----------------------------
db.getCollection("数码配件电子元件去重复后51.6万)").drop();
db.createCollection("数码配件电子元件去重复后51.6万)");
// ----------------------------
// Collection structure for 时尚家饰工艺品去重复后41万
// ----------------------------
db.getCollection("时尚家饰工艺品去重复后41万").drop();
db.createCollection("时尚家饰工艺品去重复后41万");
// ----------------------------
// Collection structure for 汽车配件去重复后38.6万)
// ----------------------------
db.getCollection("汽车配件去重复后38.6万)").drop();
db.createCollection("汽车配件去重复后38.6万)");
// ----------------------------
// Collection structure for 珠宝钻石翡翠去重复后44.5万)
// ----------------------------
db.getCollection("珠宝钻石翡翠去重复后44.5万)").drop();
db.createCollection("珠宝钻石翡翠去重复后44.5万)");
// ----------------------------
// Collection structure for 电脑硬件台式机网络设备去重复后20万
// ----------------------------
db.getCollection("电脑硬件台式机网络设备去重复后20万").drop();
db.createCollection("电脑硬件台式机网络设备去重复后20万");
// ----------------------------
// Collection structure for 男装去重复后216万
// ----------------------------
db.getCollection("男装去重复后216万").drop();
db.createCollection("男装去重复后216万");
// ----------------------------
// Collection structure for 童装婴儿服鞋帽去重复后52万
// ----------------------------
db.getCollection("童装婴儿服鞋帽去重复后52万").drop();
db.createCollection("童装婴儿服鞋帽去重复后52万");
// ----------------------------
// Collection structure for 笔记本去重复后32万
// ----------------------------
db.getCollection("笔记本去重复后32万").drop();
db.createCollection("笔记本去重复后32万");
// ----------------------------
// Collection structure for 网络服务电脑软件去重复后17万
// ----------------------------
db.getCollection("网络服务电脑软件去重复后17万").drop();
db.createCollection("网络服务电脑软件去重复后17万");
// ----------------------------
// Collection structure for 装潢灯具五金去重复后107万
// ----------------------------
db.getCollection("装潢灯具五金去重复后107万").drop();
db.createCollection("装潢灯具五金去重复后107万");
// ----------------------------
// Collection structure for 食品保健去重复后84.5万)
// ----------------------------
db.getCollection("食品保健去重复后84.5万)").drop();
db.createCollection("食品保健去重复后84.5万)");
// ----------------------------
// Collection structure for 饰品去重复14.7万)
// ----------------------------
db.getCollection("饰品去重复14.7万)").drop();
db.createCollection("饰品去重复14.7万)");

View File

@@ -0,0 +1,156 @@
/*
Navicat Premium Data Transfer
Source Server : 标签数据库
Source Server Type : MongoDB
Source Server Version : 60025 (6.0.25)
Source Host : 192.168.2.6:27017
Source Schema : KR_游戏
Target Server Type : MongoDB
Target Server Version : 60025 (6.0.25)
File Encoding : 65001
Date: 19/11/2025 15:21:24
*/
// ----------------------------
// Collection structure for 178游戏网
// ----------------------------
db.getCollection("178游戏网").drop();
db.createCollection("178游戏网");
db.getCollection("178游戏网").createIndex({
username: NumberInt("1")
}, {
name: "username_1"
});
// ----------------------------
// Collection structure for 766游戏网
// ----------------------------
db.getCollection("766游戏网").drop();
db.createCollection("766游戏网");
db.getCollection("766游戏网").createIndex({
"data.0": NumberInt("1")
}, {
name: "data.0_1"
});
// ----------------------------
// Collection structure for 7k7k小游戏
// ----------------------------
db.getCollection("7k7k小游戏").drop();
db.createCollection("7k7k小游戏");
db.getCollection("7k7k小游戏").createIndex({
account: NumberInt("1")
}, {
name: "account_1"
});
db.getCollection("7k7k小游戏").createIndex({
domain: NumberInt("1")
}, {
name: "domain_1"
});
db.getCollection("7k7k小游戏").createIndex({
account_type: NumberInt("1")
}, {
name: "account_type_1"
});
db.getCollection("7k7k小游戏").createIndex({
is_email: NumberInt("1")
}, {
name: "is_email_1"
});
// ----------------------------
// Collection structure for DNF
// ----------------------------
db.getCollection("DNF").drop();
db.createCollection("DNF");
db.getCollection("DNF").createIndex({
"data.0": NumberInt("1")
}, {
name: "data.0_1"
});
db.getCollection("DNF").createIndex({
id: NumberInt("1")
}, {
name: "id_1"
});
db.getCollection("DNF").createIndex({
username: NumberInt("1")
}, {
name: "username_1"
});
db.getCollection("DNF").createIndex({
email: NumberInt("1")
}, {
name: "email_1"
});
db.getCollection("DNF").createIndex({
phone: NumberInt("1")
}, {
name: "phone_1"
});
db.getCollection("DNF").createIndex({
character_name: NumberInt("1")
}, {
name: "character_name_1"
});
db.getCollection("DNF").createIndex({
server_name: NumberInt("1")
}, {
name: "server_name_1"
});
db.getCollection("DNF").createIndex({
level: NumberInt("-1")
}, {
name: "level_-1"
});
// ----------------------------
// Collection structure for KR_游戏_重叠用户
// ----------------------------
db.getCollection("KR_游戏_重叠用户").drop();
db.createCollection("KR_游戏_重叠用户");
db.getCollection("KR_游戏_重叠用户").createIndex({
user_id: NumberInt("1")
}, {
name: "user_id_1"
});
db.getCollection("KR_游戏_重叠用户").createIndex({
user_value: NumberInt("1")
}, {
name: "user_value_1"
});
db.getCollection("KR_游戏_重叠用户").createIndex({
overlap_count: NumberInt("1")
}, {
name: "overlap_count_1"
});
// ----------------------------
// Collection structure for system.profile
// ----------------------------
db.getCollection("system.profile").drop();
db.createCollection("system.profile",{
capped: true,
size: 1048576
});
// ----------------------------
// Collection structure for 游戏语音ispeak_cn
// ----------------------------
db.getCollection("游戏语音ispeak_cn").drop();
db.createCollection("游戏语音ispeak_cn");
db.getCollection("游戏语音ispeak_cn").createIndex({
username: NumberInt("1")
}, {
name: "username_1"
});
db.getCollection("游戏语音ispeak_cn").createIndex({
email: NumberInt("1")
}, {
name: "email_1"
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,79 @@
/*
Navicat Premium Data Transfer
Source Server : 标签数据库
Source Server Type : MongoDB
Source Server Version : 60025 (6.0.25)
Source Host : 192.168.2.6:27017
Source Schema : KR_酒店
Target Server Type : MongoDB
Target Server Version : 60025 (6.0.25)
File Encoding : 65001
Date: 19/11/2025 15:19:17
*/
// ----------------------------
// Collection structure for system.profile
// ----------------------------
db.getCollection("system.profile").drop();
db.createCollection("system.profile",{
capped: true,
size: 1048576
});
// ----------------------------
// Collection structure for 如家汉庭
// ----------------------------
db.getCollection("如家汉庭").drop();
db.createCollection("如家汉庭");
db.getCollection("如家汉庭").createIndex({
Mobile: NumberInt("1")
}, {
name: "Mobile_1"
});
// ----------------------------
// Collection structure for 酒店开房记录_2013年8月_2000万
// ----------------------------
db.getCollection("酒店开房记录_2013年8月_2000万").drop();
db.createCollection("酒店开房记录_2013年8月_2000万");
db.getCollection("酒店开房记录_2013年8月_2000万").createIndex({
name: NumberInt("1")
}, {
name: "name_1"
});
db.getCollection("酒店开房记录_2013年8月_2000万").createIndex({
phone: NumberInt("1")
}, {
name: "phone_1"
});
db.getCollection("酒店开房记录_2013年8月_2000万").createIndex({
id_card: NumberInt("1")
}, {
name: "id_card_1"
});
db.getCollection("酒店开房记录_2013年8月_2000万").createIndex({
mobile: NumberInt("1")
}, {
name: "mobile_1"
});
// ----------------------------
// Collection structure for 酒店开房记录_2013年8月_2000万内
// ----------------------------
db.getCollection("酒店开房记录_2013年8月_2000万内").drop();
db.createCollection("酒店开房记录_2013年8月_2000万内");
db.getCollection("酒店开房记录_2013年8月_2000万内").createIndex({
mobile: NumberInt("1")
}, {
name: "mobile_1"
});
// ----------------------------
// Collection structure for 酒店开房记录_2013年8月_2000万内_备份
// ----------------------------
db.getCollection("酒店开房记录_2013年8月_2000万内_备份").drop();
db.createCollection("酒店开房记录_2013年8月_2000万内_备份");

View File

@@ -0,0 +1,85 @@
/*
Navicat Premium Data Transfer
Source Server : 标签数据库
Source Server Type : MongoDB
Source Server Version : 60025 (6.0.25)
Source Host : 192.168.2.6:27017
Source Schema : KR_销售额3000万元-5000万元企业名录
Target Server Type : MongoDB
Target Server Version : 60025 (6.0.25)
File Encoding : 65001
Date: 19/11/2025 15:21:17
*/
// ----------------------------
// Collection structure for system.profile
// ----------------------------
db.getCollection("system.profile").drop();
db.createCollection("system.profile",{
capped: true,
size: 1048576
});
// ----------------------------
// Collection structure for 企业名录 的副本
// ----------------------------
db.getCollection("企业名录 的副本").drop();
db.createCollection("企业名录 的副本");
// ----------------------------
// Collection structure for 企业规模代码
// ----------------------------
db.getCollection("企业规模代码").drop();
db.createCollection("企业规模代码");
// ----------------------------
// Collection structure for 控股情况代码
// ----------------------------
db.getCollection("控股情况代码").drop();
db.createCollection("控股情况代码");
// ----------------------------
// Collection structure for 机构类型代码
// ----------------------------
db.getCollection("机构类型代码").drop();
db.createCollection("机构类型代码");
// ----------------------------
// Collection structure for 经济类型代码
// ----------------------------
db.getCollection("经济类型代码").drop();
db.createCollection("经济类型代码");
// ----------------------------
// Collection structure for 营业收入代码
// ----------------------------
db.getCollection("营业收入代码").drop();
db.createCollection("营业收入代码");
// ----------------------------
// Collection structure for 行业代码
// ----------------------------
db.getCollection("行业代码").drop();
db.createCollection("行业代码");
// ----------------------------
// Collection structure for 行政区划代码
// ----------------------------
db.getCollection("行政区划代码").drop();
db.createCollection("行政区划代码");
// ----------------------------
// Collection structure for 销售额3000万元-5000万元企业名录
// ----------------------------
db.getCollection("销售额3000万元-5000万元企业名录").drop();
db.createCollection("销售额3000万元-5000万元企业名录");
// ----------------------------
// Collection structure for 隶属关系代码
// ----------------------------
db.getCollection("隶属关系代码").drop();
db.createCollection("隶属关系代码");

View File

@@ -0,0 +1,91 @@
/*
Navicat Premium Data Transfer
Source Server : 标签数据库
Source Server Type : MongoDB
Source Server Version : 60025 (6.0.25)
Source Host : 192.168.2.6:27017
Source Schema : KR_顺丰
Target Server Type : MongoDB
Target Server Version : 60025 (6.0.25)
File Encoding : 65001
Date: 19/11/2025 15:20:28
*/
// ----------------------------
// Collection structure for system.profile
// ----------------------------
db.getCollection("system.profile").drop();
db.createCollection("system.profile",{
capped: true,
size: 1048576
});
// ----------------------------
// Collection structure for 顺丰快递数据
// ----------------------------
db.getCollection("顺丰快递数据").drop();
db.createCollection("顺丰快递数据");
db.getCollection("顺丰快递数据").createIndex({
phone: NumberInt("1")
}, {
name: "phone_1"
});
db.getCollection("顺丰快递数据").createIndex({
province: NumberInt("1")
}, {
name: "province_1"
});
db.getCollection("顺丰快递数据").createIndex({
city: NumberInt("1")
}, {
name: "city_1"
});
db.getCollection("顺丰快递数据").createIndex({
province: NumberInt("1"),
city: NumberInt("1")
}, {
name: "province_1_city_1"
});
db.getCollection("顺丰快递数据").createIndex({
phone: NumberInt("1"),
province: NumberInt("1")
}, {
name: "phone_1_province_1"
});
// ----------------------------
// Collection structure for 顺丰快递数据_tmpForCopy1
// ----------------------------
db.getCollection("顺丰快递数据_tmpForCopy1").drop();
db.createCollection("顺丰快递数据_tmpForCopy1");
db.getCollection("顺丰快递数据_tmpForCopy1").createIndex({
phone: NumberInt("1")
}, {
name: "phone_1"
});
db.getCollection("顺丰快递数据_tmpForCopy1").createIndex({
province: NumberInt("1")
}, {
name: "province_1"
});
db.getCollection("顺丰快递数据_tmpForCopy1").createIndex({
city: NumberInt("1")
}, {
name: "city_1"
});
db.getCollection("顺丰快递数据_tmpForCopy1").createIndex({
province: NumberInt("1"),
city: NumberInt("1")
}, {
name: "province_1_city_1"
});
db.getCollection("顺丰快递数据_tmpForCopy1").createIndex({
phone: NumberInt("1"),
province: NumberInt("1")
}, {
name: "phone_1_province_1"
});

View File

@@ -0,0 +1,217 @@
/*
Navicat Premium Data Transfer
Source Server : 标签数据库
Source Server Type : MongoDB
Source Server Version : 60025 (6.0.25)
Source Host : 192.168.2.6:27017
Source Schema : KR_香港在大陆投资企业名录
Target Server Type : MongoDB
Target Server Version : 60025 (6.0.25)
File Encoding : 65001
Date: 19/11/2025 15:21:07
*/
// ----------------------------
// Collection structure for system.profile
// ----------------------------
db.getCollection("system.profile").drop();
db.createCollection("system.profile",{
capped: true,
size: 1048576
});
// ----------------------------
// Collection structure for 上海
// ----------------------------
db.getCollection("上海").drop();
db.createCollection("上海");
// ----------------------------
// Collection structure for 云南
// ----------------------------
db.getCollection("云南").drop();
db.createCollection("云南");
// ----------------------------
// Collection structure for 内蒙
// ----------------------------
db.getCollection("内蒙").drop();
db.createCollection("内蒙");
// ----------------------------
// Collection structure for 北京
// ----------------------------
db.getCollection("北京").drop();
db.createCollection("北京");
// ----------------------------
// Collection structure for 吉林
// ----------------------------
db.getCollection("吉林").drop();
db.createCollection("吉林");
// ----------------------------
// Collection structure for 四川
// ----------------------------
db.getCollection("四川").drop();
db.createCollection("四川");
// ----------------------------
// Collection structure for 天津
// ----------------------------
db.getCollection("天津").drop();
db.createCollection("天津");
// ----------------------------
// Collection structure for 宁夏
// ----------------------------
db.getCollection("宁夏").drop();
db.createCollection("宁夏");
// ----------------------------
// Collection structure for 安徽
// ----------------------------
db.getCollection("安徽").drop();
db.createCollection("安徽");
// ----------------------------
// Collection structure for 山东
// ----------------------------
db.getCollection("山东").drop();
db.createCollection("山东");
// ----------------------------
// Collection structure for 山西
// ----------------------------
db.getCollection("山西").drop();
db.createCollection("山西");
// ----------------------------
// Collection structure for 广东
// ----------------------------
db.getCollection("广东").drop();
db.createCollection("广东");
// ----------------------------
// Collection structure for 广西
// ----------------------------
db.getCollection("广西").drop();
db.createCollection("广西");
// ----------------------------
// Collection structure for 新疆
// ----------------------------
db.getCollection("新疆").drop();
db.createCollection("新疆");
// ----------------------------
// Collection structure for 江苏
// ----------------------------
db.getCollection("江苏").drop();
db.createCollection("江苏");
// ----------------------------
// Collection structure for 江西
// ----------------------------
db.getCollection("江西").drop();
db.createCollection("江西");
// ----------------------------
// Collection structure for 河北
// ----------------------------
db.getCollection("河北").drop();
db.createCollection("河北");
// ----------------------------
// Collection structure for 河南
// ----------------------------
db.getCollection("河南").drop();
db.createCollection("河南");
// ----------------------------
// Collection structure for 浙江
// ----------------------------
db.getCollection("浙江").drop();
db.createCollection("浙江");
// ----------------------------
// Collection structure for 海南
// ----------------------------
db.getCollection("海南").drop();
db.createCollection("海南");
// ----------------------------
// Collection structure for 湖北
// ----------------------------
db.getCollection("湖北").drop();
db.createCollection("湖北");
// ----------------------------
// Collection structure for 湖南
// ----------------------------
db.getCollection("湖南").drop();
db.createCollection("湖南");
// ----------------------------
// Collection structure for 甘肃
// ----------------------------
db.getCollection("甘肃").drop();
db.createCollection("甘肃");
// ----------------------------
// Collection structure for 福建
// ----------------------------
db.getCollection("福建").drop();
db.createCollection("福建");
// ----------------------------
// Collection structure for 西藏
// ----------------------------
db.getCollection("西藏").drop();
db.createCollection("西藏");
// ----------------------------
// Collection structure for 贵州
// ----------------------------
db.getCollection("贵州").drop();
db.createCollection("贵州");
// ----------------------------
// Collection structure for 辽宁
// ----------------------------
db.getCollection("辽宁").drop();
db.createCollection("辽宁");
// ----------------------------
// Collection structure for 重庆
// ----------------------------
db.getCollection("重庆").drop();
db.createCollection("重庆");
// ----------------------------
// Collection structure for 陕西
// ----------------------------
db.getCollection("陕西").drop();
db.createCollection("陕西");
// ----------------------------
// Collection structure for 青海
// ----------------------------
db.getCollection("青海").drop();
db.createCollection("青海");
// ----------------------------
// Collection structure for 香港投资企业名录
// ----------------------------
db.getCollection("香港投资企业名录").drop();
db.createCollection("香港投资企业名录");
// ----------------------------
// Collection structure for 黑龙江
// ----------------------------
db.getCollection("黑龙江").drop();
db.createCollection("黑龙江");

View File

@@ -0,0 +1,103 @@
/*
Navicat Premium Data Transfer
Source Server : 标签数据库
Source Server Type : MongoDB
Source Server Version : 60025 (6.0.25)
Source Host : 192.168.2.6:27017
Source Schema : KR_魔兽世界
Target Server Type : MongoDB
Target Server Version : 60025 (6.0.25)
File Encoding : 65001
Date: 19/11/2025 15:19:37
*/
// ----------------------------
// Collection structure for elysn欧美_魔兽世界主题库
// ----------------------------
db.getCollection("elysn欧美_魔兽世界主题库").drop();
db.createCollection("elysn欧美_魔兽世界主题库");
db.getCollection("elysn欧美_魔兽世界主题库").createIndex({
"账号": NumberInt("1")
}, {
name: "账号_1"
});
db.getCollection("elysn欧美_魔兽世界主题库").createIndex({
"密码": NumberInt("1")
}, {
name: "密码_1"
});
// ----------------------------
// Collection structure for system.profile
// ----------------------------
db.getCollection("system.profile").drop();
db.createCollection("system.profile",{
capped: true,
size: 1048576
});
// ----------------------------
// Collection structure for 网易2500W密正数据
// ----------------------------
db.getCollection("网易2500W密正数据").drop();
db.createCollection("网易2500W密正数据");
db.getCollection("网易2500W密正数据").createIndex({
"账号": NumberInt("1")
}, {
name: "账号_1",
unique: true
});
// ----------------------------
// Collection structure for 网易IS_game.sohu.com邮箱占90%
// ----------------------------
db.getCollection("网易IS_game.sohu.com邮箱占90%").drop();
db.createCollection("网易IS_game.sohu.com邮箱占90%");
// ----------------------------
// Collection structure for 网易正确数据_已确认
// ----------------------------
db.getCollection("网易正确数据_已确认").drop();
db.createCollection("网易正确数据_已确认");
db.getCollection("网易正确数据_已确认").createIndex({
"账号": NumberInt("1")
}, {
name: "账号_1",
background: true,
unique: true
});
// ----------------------------
// Collection structure for 网易正确数据_空白数据
// ----------------------------
db.getCollection("网易正确数据_空白数据").drop();
db.createCollection("网易正确数据_空白数据");
db.getCollection("网易正确数据_空白数据").createIndex({
"账号": NumberInt("1")
}, {
name: "idx_account",
background: true
});
db.getCollection("网易正确数据_空白数据").createIndex({
"密码": NumberInt("1")
}, {
name: "idx_password",
background: true
});
db.getCollection("网易正确数据_空白数据").createIndex({
"账号": NumberInt("1"),
"密码": NumberInt("1")
}, {
name: "idx_account_password",
background: true
});
// ----------------------------
// Collection structure for 魔兽世界_3月库
// ----------------------------
db.getCollection("魔兽世界_3月库").drop();
db.createCollection("魔兽世界_3月库");

View File

@@ -201,40 +201,38 @@ class UserController extends BaseController
* 修改密码
* @return \think\response\Json
*/
public function modifyPwd()
public function modifyPwd($data = [])
{
// 获取并验证参数
$params = $this->validateModifyPwdParams();
if (!is_array($params)) {
return $params;
if (empty($data)) {
return json_encode(['code' => 400,'msg' => '参数缺失']);
}
$authorization = trim($this->request->header('authorization', $this->authorization));
if (!isset($data['id']) || !isset($data['pwd'])) {
return json_encode(['code' => 401,'msg' => '参数缺失']);
}
$authorization = $this->authorization;
if (empty($authorization)) {
return errorJson('缺少授权信息');
return json_encode(['code' => 400,'msg' => '缺少授权信息']);
}
$headerData = ['client:' . self::CLIENT_TYPE];
$header = setHeader($headerData, $authorization, 'plain');
$headerData = ['client:system'];
$header = setHeader($headerData, $authorization, 'json');
$params = [
'id' => $data['id'],
'newPw' => $data['pwd'],
];
try {
$result = requestCurl($this->baseUrl . 'api/Account/self', $params, 'PUT', $header);
$result = requestCurl($this->baseUrl . 'api/Account/modifypw', $params, 'PUT', $header,'json');
$response = handleApiResponse($result);
if (empty($response)) {
// 获取当前用户信息
$currentUser = CompanyAccountModel::where('token', $authorization)->find();
if ($currentUser) {
recordUserLog($currentUser['id'], $currentUser['userName'], 'MODIFY_PASSWORD', '修改密码成功', [], 200, '修改成功');
}
return successJson(['message' => '修改成功']);
return json_encode(['code' => 200,'msg' => '修改成功']);
}
recordUserLog(0, '', 'MODIFY_PASSWORD', '修改密码失败', $params, 500, $response);
return errorJson($response);
return json_encode(['code' => 400,'msg' => $response]);
} catch (\Exception $e) {
recordUserLog(0, '', 'MODIFY_PASSWORD', '修改密码异常', $params, 500, $e->getMessage());
return errorJson('修改密码失败:' . $e->getMessage());
return json_encode(['code' => 400,'msg' => '修改密码失败:' . $e->getMessage()]);
}
}

View File

@@ -457,22 +457,16 @@ class WebSocketController extends BaseController
// 构建请求参数
$params = [
"cmdType" => 'CmdDownloadMomentImagesResult',
"cmdType" => 'CmdDownloadMomentImages',
"snsId" => $data['snsId'],
"urls" => $data['snsUrls'],
"wechatAccountId" => $data['wechatAccountId'],
"seq" => time(),
];
// 记录请求日志
Log::info('获取朋友圈资源链接请求:' . json_encode($params, 256));
// 发送请求
$this->client->send(json_encode($params));
// 接收响应
$response = $this->client->receive();
$message = json_decode($response, true);
$message = $this->sendMessage($params);
if (empty($message)) {
return json_encode(['code' => 500, 'msg' => '获取朋友圈资源链接失败']);
@@ -558,15 +552,17 @@ class WebSocketController extends BaseController
$dataToSave['create_time'] = time();
$res = WechatMoments::create($dataToSave);
}
// // 获取资源链接
// if(empty($momentEntity['resUrls']) && !empty($momentEntity['urls'])){
// $snsData = [
// 'snsId' => $moment['snsId'],
// 'snsUrls' => $momentEntity['urls'],
// 'wechatAccountId' => $wechatAccountId,
// ];
// $this->getMomentSourceRealUrl($snsData);
// }
// 获取资源链接
if(empty($momentEntity['resUrls']) && !empty($momentEntity['urls']) && $moment['type'] == 1) {
$snsData = [
'snsId' => $moment['snsId'],
'snsUrls' => $momentEntity['urls'],
'wechatAccountId' => $wechatAccountId,
];
$this->getMomentSourceRealUrl($snsData);
}
}
//Log::write('朋友圈数据已存入数据库,共' . count($momentList) . '条');

View File

@@ -7,5 +7,5 @@ use think\Model;
class WechatAccountModel extends Model
{
// 设置表名
protected $table = 's2_wechat_account';
protected $table = 's2_wechat_account';
}

View File

@@ -14,6 +14,8 @@ Route::group('v1/', function () {
Route::get('list', 'app\chukebao\controller\WechatFriendController@getList'); // 获取好友列表
Route::get('detail', 'app\chukebao\controller\WechatFriendController@getDetail'); // 获取好友详情
Route::post('updateInfo', 'app\chukebao\controller\WechatFriendController@updateFriendInfo'); // 更新好友资料
// 添加好友任务记录相关接口
Route::get('addTaskList', 'app\chukebao\controller\WechatFriendController@getAddTaskList'); // 获取添加好友任务记录列表包含添加者信息、状态、时间等支持状态筛选无需传好友ID
});
//群相关
Route::group('wechatChatroom/', function () {

View File

@@ -2,8 +2,10 @@
namespace app\chukebao\controller;
use app\api\model\WechatChatroomModel;
use library\ResponseHelper;
use app\api\model\WechatFriendModel;
use app\api\model\WechatMessageModel;
use app\api\controller\MessageController;
@@ -33,6 +35,7 @@ class DataProcessing extends BaseController
'CmdAllotFriend', //转让好友 {labels、wechatAccountId、wechatFriendId}
'CmdChatroomOperate', //修改群信息 {chatroomName群名、announce公告、extra公告、wechatAccountId、wechatChatroomId}
'CmdNewMessage', //接收消息
'CmdSendMessageResult', //更新消息状态
];
if (empty($type) || empty($wechatAccountId)) {
@@ -75,15 +78,31 @@ class DataProcessing extends BaseController
if(empty($toAccountId)){
return ResponseHelper::error('参数缺失');
}
$friend = WechatFriendModel::where(['id' => $wechatFriendId,'wechatAccountId' => $wechatAccountId])->find();
if(empty($friend)){
return ResponseHelper::error('好友不存在');
if(empty($wechatFriendId) && empty($wechatChatroomId)){
return ResponseHelper::error('参数缺失');
}
$friend->accountId = $toAccountId;
$friend->updateTime = time();
$friend->save();
$msg = '好友转移成功';
if (!empty($wechatFriendId)){
$data = WechatFriendModel::where(['id' => $wechatFriendId,'wechatAccountId' => $wechatAccountId])->find();
$msg = '好友转移成功';
if(empty($data)){
return ResponseHelper::error('好友不存在');
}
}
if (!empty($wechatChatroomId)){
$data = WechatChatroomModel::where(['id' => $wechatChatroomId,'wechatAccountId' => $wechatAccountId])->find();
$msg = '群聊转移成功';
if(empty($data)){
return ResponseHelper::error('群聊不存在');
}
}
$data->accountId = $toAccountId;
$data->updateTime = time();
$data->save();
break;
case 'CmdNewMessage':
if(empty($friendMessage) && empty($chatroomMessage)){
@@ -105,8 +124,46 @@ class DataProcessing extends BaseController
$msg = '消息记录成功';
}else{
$msg = '消息记录失败';
$codee = 400;
$codee = 200;
}
break;
case 'CmdSendMessageResult':
$friendMessageId = $this->request->param('friendMessageId', 0);
$chatroomMessageId = $this->request->param('chatroomMessageId', 0);
$sendStatus = $this->request->param('sendStatus', null);
$wechatTime = $this->request->param('wechatTime', 0);
if ($sendStatus === null) {
return ResponseHelper::error('sendStatus不能为空');
}
if (empty($friendMessageId) && empty($chatroomMessageId)) {
return ResponseHelper::error('friendMessageId或chatroomMessageId至少提供一个');
}
$messageId = $friendMessageId ?: $chatroomMessageId;
$update = [
'sendStatus' => (int)$sendStatus,
];
if (!empty($wechatTime)) {
$update['wechatTime'] = strlen((string)$wechatTime) > 10
? intval($wechatTime / 1000)
: (int)$wechatTime;
}
$affected = WechatMessageModel::where('id', $messageId)->update($update);
if ($affected === false) {
return ResponseHelper::success('','更新消息状态失败');
}
if ($affected === 0) {
return ResponseHelper::success('','消息不存在');
}
$msg = '更新消息状态成功';
break;
}
return ResponseHelper::success('',$msg,$codee);
}

View File

@@ -23,7 +23,7 @@ class WechatFriendController extends BaseController
$total = $query->count();
$list = $query->page($page, $limit)->select();
// 提取所有好友ID
// 提取所有好友ID
$friendIds = array_column($list, 'id');
$aiTypeData = [];
@@ -67,7 +67,7 @@ class WechatFriendController extends BaseController
$friend = Db::table('s2_wechat_friend')
->where(['id' => $friendId, 'isDeleted' => 0])
->find();
if (empty($friend)) {
return ResponseHelper::error('好友不存在');
}
@@ -78,11 +78,11 @@ class WechatFriendController extends BaseController
$friend['createTime'] = !empty($friend['createTime']) ? date('Y-m-d H:i:s', $friend['createTime']) : '';
$friend['updateTime'] = !empty($friend['updateTime']) ? date('Y-m-d H:i:s', $friend['updateTime']) : '';
$friend['passTime'] = !empty($friend['passTime']) ? date('Y-m-d H:i:s', $friend['passTime']) : '';
// 获取AI类型设置
$aiTypeSetting = FriendSettings::where('friendId', $friendId)->find();
$friend['aiType'] = $aiTypeSetting ? $aiTypeSetting['type'] : 0;
return ResponseHelper::success(['detail' => $friend]);
}
@@ -166,4 +166,112 @@ class WechatFriendController extends BaseController
return ResponseHelper::success(['id' => $friendId]);
}
/**
* 获取添加好友任务记录列表(全新功能)
* 返回当前账号的所有添加好友任务记录,无论是否通过都展示
* 包含:添加者头像、昵称、微信号、添加状态、添加时间、通过时间等信息
* @return \think\response\Json
*/
public function getAddTaskList()
{
$page = $this->request->param('page', 1);
$limit = $this->request->param('limit', 10);
$status = $this->request->param('status', ''); // 可选:筛选状态 0执行中1执行成功2执行失败
$accountId = $this->getUserInfo('s2_accountId');
if (empty($accountId)) {
return ResponseHelper::error('请先登录');
}
// 直接使用operatorAccountId查询添加好友任务记录
$query = Db::table('s2_friend_task')
->where('operatorAccountId', $accountId)
->order('createTime desc');
// 如果指定了状态筛选
if ($status !== '' && $status !== null) {
$query->where('status', $status);
}
$total = $query->count();
$tasks = $query->page($page, $limit)->select();
// 处理任务数据
$list = [];
foreach ($tasks as $task) {
// 提取所有任务的phone、wechatId用于查询好友信息获取通过时间
$friendInfo = Db::table('s2_wechat_friend')
->where(['isDeleted' => 0, 'ownerWechatId' => $task['wechatId']])
->where(function ($query) use ($task) {
$query->whereLike('phone', '%'.$task['phone'].'%')->whereOr('alias', $task['phone'])->whereOr('wechatId', $task['phone']);
})->field('phone,wechatId,alias,passTime,nickname')->find();
$item = [
'taskId' => $task['id'] ?? 0,
'phone' => $task['phone'] ?? '',
'wechatId' => $task['wechatId'] ?? '',
'alias' => $task['alias'] ?? '',
// 添加者信息
'adder' => [
'avatar' => $task['wechatAvatar'] ?? '', // 添加者头像
'nickname' => $task['wechatNickname'] ?? '', // 添加者昵称
'username' => $task['accountUsername'] ?? '', // 添加者微信号
'accountNickname' => $task['accountNickname'] ?? '', // 账号昵称
'accountRealName' => $task['accountRealName'] ?? '', // 账号真实姓名
],
// 添加状态
'status' => [
'code' => $task['status'] ?? 0, // 状态码0执行中1执行成功2执行失败
'text' => $this->getTaskStatusText($task['status'] ?? 0), // 状态文本
'extra' => ''
],
// 时间信息
'time' => [
'addTime' => !empty($task['createTime']) ? date('Y-m-d H:i:s', $task['createTime']) : '', // 添加时间
'addTimeStamp' => $task['createTime'] ?? 0, // 添加时间戳
'updateTime' => !empty($task['updateTime']) ? date('Y-m-d H:i:s', $task['updateTime']) : '', // 更新时间
'updateTimeStamp' => $task['updateTime'] ?? 0, // 更新时间戳
'passTime' => !empty($friendInfo['passTime']) ? date('Y-m-d H:i:s', $friendInfo['passTime']) : '', // 通过时间
'passTimeStamp' => $friendInfo['passTime'] ?? 0, // 通过时间戳
],
// 好友信息(如果已通过)
'friend' => [
'nickname' => $friendInfo['nickname'] ?? '', // 好友昵称
'isPassed' => !empty($friendInfo['passTime']), // 是否已通过
],
// 其他信息
'other' => [
'msgContent' => $task['msgContent'] ?? '', // 验证消息
'remark' => $task['remark'] ?? '', // 备注
'from' => $task['from'] ?? '', // 来源
'labels' => !empty($task['labels']) ? explode(',', $task['labels']) : [], // 标签
]
];
$list[] = $item;
}
return ResponseHelper::success(['list' => $list, 'total' => $total]);
}
/**
* 获取任务状态文本
* @param int $status 状态码
* @return string 状态文本
*/
private function getTaskStatusText($status)
{
$statusMap = [
0 => '执行中',
1 => '执行成功',
2 => '执行失败',
];
return isset($statusMap[$status]) ? $statusMap[$status] : '未知状态';
}
}

View File

@@ -39,4 +39,7 @@ return [
'workbench:groupCreate' => 'app\command\WorkbenchGroupCreateCommand', // 工作台群创建任务
'workbench:import-contact' => 'app\command\WorkbenchImportContactCommand', // 工作台通讯录导入任务
'kf:notice' => 'app\command\KfNoticeCommand', // 客服端消息通知
'wechat:calculate-score' => 'app\command\CalculateWechatAccountScoreCommand', // 统一计算微信账号健康分
'wechat:update-score' => 'app\command\UpdateWechatAccountScoreCommand', // 更新微信账号评分记录
];

View File

@@ -0,0 +1,558 @@
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\Db;
use think\facade\Log;
use app\common\service\WechatAccountHealthScoreService;
/**
* 统一计算微信账号健康分命令
* 一个命令完成所有评分工作:
* 1. 初始化未计算的账号(基础分只计算一次)
* 2. 更新评分记录根据wechatId和alias不一致情况
* 3. 批量更新健康分(只更新动态分)
*/
class CalculateWechatAccountScoreCommand extends Command
{
/**
* 数据库表名
*/
const TABLE_WECHAT_ACCOUNT = 's2_wechat_account';
const TABLE_WECHAT_ACCOUNT_SCORE = 's2_wechat_account_score';
protected function configure()
{
$this->setName('wechat:calculate-score')
->setDescription('统一计算微信账号健康分(包含初始化、更新评分记录、批量计算)')
->addOption('only-init', null, \think\console\input\Option::VALUE_NONE, '仅执行初始化步骤')
->addOption('only-update', null, \think\console\input\Option::VALUE_NONE, '仅执行更新评分记录步骤')
->addOption('only-batch', null, \think\console\input\Option::VALUE_NONE, '仅执行批量更新健康分步骤')
->addOption('account-id', 'a', \think\console\input\Option::VALUE_OPTIONAL, '指定账号ID仅处理该账号')
->addOption('batch-size', 'b', \think\console\input\Option::VALUE_OPTIONAL, '批处理大小', 50)
->addOption('force-recalculate', 'f', \think\console\input\Option::VALUE_NONE, '强制重新计算基础分');
}
/**
* 执行命令
*
* @param Input $input 输入对象
* @param Output $output 输出对象
* @return int 命令执行状态码0表示成功
*/
protected function execute(Input $input, Output $output)
{
// 解析命令行参数
$onlyInit = $input->getOption('only-init');
$onlyUpdate = $input->getOption('only-update');
$onlyBatch = $input->getOption('only-batch');
$accountId = $input->getOption('account-id');
$batchSize = (int)$input->getOption('batch-size');
$forceRecalculate = $input->getOption('force-recalculate');
// 参数验证
if ($batchSize <= 0) {
$batchSize = 50; // 默认批处理大小
}
// 显示执行参数
$output->writeln("==========================================");
$output->writeln("开始统一计算微信账号健康分...");
$output->writeln("==========================================");
if ($accountId) {
$output->writeln("指定账号ID: {$accountId}");
}
if ($onlyInit) {
$output->writeln("仅执行初始化步骤");
} elseif ($onlyUpdate) {
$output->writeln("仅执行更新评分记录步骤");
} elseif ($onlyBatch) {
$output->writeln("仅执行批量更新健康分步骤");
}
if ($forceRecalculate) {
$output->writeln("强制重新计算基础分");
}
$output->writeln("批处理大小: {$batchSize}");
// 记录命令开始执行的日志
Log::info('开始执行微信账号健康分计算命令', [
'accountId' => $accountId,
'onlyInit' => $onlyInit ? 'true' : 'false',
'onlyUpdate' => $onlyUpdate ? 'true' : 'false',
'onlyBatch' => $onlyBatch ? 'true' : 'false',
'batchSize' => $batchSize,
'forceRecalculate' => $forceRecalculate ? 'true' : 'false'
]);
$startTime = time();
try {
// 实例化服务
$service = new WechatAccountHealthScoreService();
} catch (\Exception $e) {
$errorMsg = "实例化WechatAccountHealthScoreService失败: " . $e->getMessage();
$output->writeln("<error>{$errorMsg}</error>");
Log::error($errorMsg);
return 1; // 返回非零状态码表示失败
}
// 初始化统计数据
$initStats = ['success' => 0, 'failed' => 0, 'errors' => []];
$updateStats = ['total' => 0];
$batchStats = ['success' => 0, 'failed' => 0, 'errors' => []];
try {
// 步骤1: 初始化未计算基础分的账号
if (!$onlyUpdate && !$onlyBatch) {
$output->writeln("\n[步骤1] 初始化未计算基础分的账号...");
Log::info('[步骤1] 开始初始化未计算基础分的账号');
$initStats = $this->initUncalculatedAccounts($service, $output, $accountId, $batchSize);
$output->writeln("初始化完成:成功 {$initStats['success']} 条,失败 {$initStats['failed']}");
Log::info("初始化完成:成功 {$initStats['success']} 条,失败 {$initStats['failed']}");
}
// 步骤2: 更新评分记录根据wechatId和alias不一致情况
if (!$onlyInit && !$onlyBatch) {
$output->writeln("\n[步骤2] 更新评分记录根据wechatId和alias不一致情况...");
Log::info('[步骤2] 开始更新评分记录根据wechatId和alias不一致情况');
$updateStats = $this->updateScoreRecords($service, $output, $accountId, $batchSize);
$output->writeln("更新完成:处理了 {$updateStats['total']} 条记录");
Log::info("更新评分记录完成:处理了 {$updateStats['total']} 条记录");
}
// 步骤3: 批量更新健康分(只更新动态分,不重新计算基础分)
if (!$onlyInit && !$onlyUpdate) {
$output->writeln("\n[步骤3] 批量更新健康分(只更新动态分)...");
Log::info('[步骤3] 开始批量更新健康分(只更新动态分)');
$batchStats = $this->batchUpdateHealthScore($service, $output, $accountId, $batchSize, $forceRecalculate);
$output->writeln("批量更新完成:成功 {$batchStats['success']} 条,失败 {$batchStats['failed']}");
Log::info("批量更新健康分完成:成功 {$batchStats['success']} 条,失败 {$batchStats['failed']}");
}
// 统计信息
$endTime = time();
$duration = $endTime - $startTime;
$output->writeln("\n==========================================");
$output->writeln("任务完成!");
$output->writeln("==========================================");
$output->writeln("总耗时: {$duration}");
$output->writeln("初始化: 成功 {$initStats['success']} 条,失败 {$initStats['failed']}");
$output->writeln("更新评分记录: {$updateStats['total']}");
$output->writeln("批量更新: 成功 {$batchStats['success']} 条,失败 {$batchStats['failed']}");
// 记录命令执行完成的日志
Log::info("微信账号健康分计算命令执行完成,总耗时: {$duration} 秒," .
"初始化: 成功 {$initStats['success']} 条,失败 {$initStats['failed']} 条," .
"更新评分记录: {$updateStats['total']} 条," .
"批量更新: 成功 {$batchStats['success']} 条,失败 {$batchStats['failed']}");
if (!empty($initStats['errors'])) {
$output->writeln("\n初始化错误详情:");
Log::warning("初始化阶段出现 " . count($initStats['errors']) . " 个错误");
foreach (array_slice($initStats['errors'], 0, 10) as $error) {
$output->writeln(" 账号ID {$error['accountId']}: {$error['error']}");
Log::error("初始化错误 - 账号ID {$error['accountId']}: {$error['error']}");
}
if (count($initStats['errors']) > 10) {
$output->writeln(" ... 还有 " . (count($initStats['errors']) - 10) . " 个错误");
Log::warning("初始化错误过多只记录前10个还有 " . (count($initStats['errors']) - 10) . " 个错误未显示");
}
}
if (!empty($batchStats['errors'])) {
$output->writeln("\n批量更新错误详情:");
Log::warning("批量更新阶段出现 " . count($batchStats['errors']) . " 个错误");
foreach (array_slice($batchStats['errors'], 0, 10) as $error) {
$output->writeln(" 账号ID {$error['accountId']}: {$error['error']}");
Log::error("批量更新错误 - 账号ID {$error['accountId']}: {$error['error']}");
}
if (count($batchStats['errors']) > 10) {
$output->writeln(" ... 还有 " . (count($batchStats['errors']) - 10) . " 个错误");
Log::warning("批量更新错误过多只记录前10个还有 " . (count($batchStats['errors']) - 10) . " 个错误未显示");
}
}
} catch (\PDOException $e) {
// 数据库异常
$errorMsg = "数据库操作失败: " . $e->getMessage();
$output->writeln("\n<error>数据库错误: " . $errorMsg . "</error>");
$output->writeln($e->getTraceAsString());
// 记录数据库错误
Log::error("数据库错误: " . $errorMsg);
Log::error("错误堆栈: " . $e->getTraceAsString());
return 2; // 数据库错误状态码
} catch (\Exception $e) {
// 一般异常
$errorMsg = "命令执行失败: " . $e->getMessage();
$output->writeln("\n<error>错误: " . $errorMsg . "</error>");
$output->writeln($e->getTraceAsString());
// 记录严重错误
Log::error($errorMsg);
Log::error("错误堆栈: " . $e->getTraceAsString());
return 1; // 一般错误状态码
} catch (\Throwable $e) {
// 其他所有错误
$errorMsg = "严重错误: " . $e->getMessage();
$output->writeln("\n<error>严重错误: " . $errorMsg . "</error>");
$output->writeln($e->getTraceAsString());
// 记录严重错误
Log::critical($errorMsg);
Log::critical("错误堆栈: " . $e->getTraceAsString());
return 3; // 严重错误状态码
}
return 0; // 成功执行
}
/**
* 初始化未计算基础分的账号
*
* @param WechatAccountHealthScoreService $service 健康分服务实例
* @param Output $output 输出对象
* @return array 处理结果统计
* @throws \Exception 如果查询或处理过程中出现错误
*/
private function initUncalculatedAccounts($service, $output, $accountId = null, $batchSize = 50)
{
$stats = [
'total' => 0,
'success' => 0,
'failed' => 0,
'errors' => []
];
try {
// 获取所有未计算基础分的账号
// 优化查询:使用索引字段,只查询必要的字段
$query = Db::table(self::TABLE_WECHAT_ACCOUNT)
->alias('a')
->leftJoin([self::TABLE_WECHAT_ACCOUNT_SCORE => 's'], 's.accountId = a.id')
->where('a.isDeleted', 0)
->where(function($query) {
$query->whereNull('s.id')
->whereOr('s.baseScoreCalculated', 0);
});
// 如果指定了账号ID则只处理该账号
if ($accountId) {
$query->where('a.id', $accountId);
}
$accounts = $query->field('a.id, a.wechatId') // 只查询必要的字段
->select();
} catch (\Exception $e) {
Log::error("查询未计算基础分的账号失败: " . $e->getMessage());
throw new \Exception("查询未计算基础分的账号失败: " . $e->getMessage(), 0, $e);
}
$stats['total'] = count($accounts);
if ($stats['total'] == 0) {
$output->writeln("没有需要初始化的账号");
Log::info("没有需要初始化的账号");
return $stats;
}
$output->writeln("找到 {$stats['total']} 个需要初始化的账号");
Log::info("找到 {$stats['total']} 个需要初始化的账号");
// 优化批处理:使用传入的批处理大小
$batches = array_chunk($accounts, $batchSize);
$batchCount = count($batches);
Log::info("将分 {$batchCount} 批处理,每批 {$batchSize} 个账号");
foreach ($batches as $batchIndex => $batch) {
$batchStartTime = microtime(true);
$batchSuccessCount = 0;
$batchFailedCount = 0;
foreach ($batch as $account) {
try {
$service->calculateAndUpdate($account['id']);
$stats['success']++;
$batchSuccessCount++;
if ($stats['success'] % 20 == 0) { // 更频繁地显示进度
$output->write(".");
Log::debug("已成功初始化 {$stats['success']} 个账号");
}
} catch (\Exception $e) {
$stats['failed']++;
$batchFailedCount++;
$errorMsg = "初始化账号 {$account['id']} 失败: " . $e->getMessage();
Log::error($errorMsg);
$stats['errors'][] = [
'accountId' => $account['id'],
'error' => $e->getMessage()
];
}
}
$batchEndTime = microtime(true);
$batchDuration = round($batchEndTime - $batchStartTime, 2);
// 每批次完成后输出进度信息
$output->writeln(" 批次 " . ($batchIndex + 1) . "/{$batchCount} 完成,耗时 {$batchDuration} 秒,成功 {$batchSuccessCount},失败 {$batchFailedCount}");
Log::info("初始化批次 " . ($batchIndex + 1) . "/{$batchCount} 完成,耗时 {$batchDuration} 秒,成功 {$batchSuccessCount},失败 {$batchFailedCount}");
}
return $stats;
}
/**
* 更新评分记录根据wechatId和alias不一致情况
*
* @param WechatAccountHealthScoreService $service 健康分服务实例
* @param Output $output 输出对象
* @return array 处理结果统计
* @throws \Exception 如果查询或处理过程中出现错误
*/
private function updateScoreRecords($service, $output, $accountId = null, $batchSize = 50)
{
$stats = ['total' => 0];
try {
// 优化查询:合并两次查询为一次,减少数据库访问次数
$query = Db::table(self::TABLE_WECHAT_ACCOUNT)
->where('isDeleted', 0)
->where('wechatId', '<>', '')
->where('alias', '<>', '');
// 如果指定了账号ID则只处理该账号
if ($accountId) {
$query->where('id', $accountId);
}
$accounts = $query->field('id, wechatId, alias, IF(wechatId = alias, 0, 1) as isModifiedAlias')
->select();
// 分类处理查询结果
$inconsistentAccounts = [];
$consistentAccounts = [];
foreach ($accounts as $account) {
if ($account['isModifiedAlias'] == 1) {
$inconsistentAccounts[] = $account;
} else {
$consistentAccounts[] = $account;
}
}
} catch (\Exception $e) {
Log::error("查询需要更新评分记录的账号失败: " . $e->getMessage());
throw new \Exception("查询需要更新评分记录的账号失败: " . $e->getMessage(), 0, $e);
}
$allAccounts = array_merge($inconsistentAccounts, $consistentAccounts);
$stats['total'] = count($allAccounts);
if ($stats['total'] == 0) {
$output->writeln("没有需要更新的账号");
Log::info("没有需要更新的评分记录");
return $stats;
}
$output->writeln("找到 {$stats['total']} 个需要更新的账号(不一致: " . count($inconsistentAccounts) . ",一致: " . count($consistentAccounts) . "");
Log::info("找到 {$stats['total']} 个需要更新的账号(不一致: " . count($inconsistentAccounts) . ",一致: " . count($consistentAccounts) . "");
$updatedCount = 0;
// 优化批处理:使用传入的批处理大小
$batches = array_chunk($allAccounts, $batchSize);
$batchCount = count($batches);
Log::info("将分 {$batchCount} 批更新评分记录,每批 {$batchSize} 个账号");
foreach ($batches as $batchIndex => $batch) {
$batchStartTime = microtime(true);
$batchUpdatedCount = 0;
foreach ($batch as $account) {
$isModifiedAlias = isset($account['isModifiedAlias']) ?
($account['isModifiedAlias'] == 1) :
in_array($account['id'], array_column($inconsistentAccounts, 'id'));
$this->updateScoreRecord($account['id'], $isModifiedAlias, $service);
$updatedCount++;
$batchUpdatedCount++;
if ($batchUpdatedCount % 20 == 0) {
$output->write(".");
}
}
$batchEndTime = microtime(true);
$batchDuration = round($batchEndTime - $batchStartTime, 2);
// 每批次完成后输出进度信息
$output->writeln(" 批次 " . ($batchIndex + 1) . "/{$batchCount} 完成,耗时 {$batchDuration} 秒,更新 {$batchUpdatedCount} 条记录");
Log::info("更新评分记录批次 " . ($batchIndex + 1) . "/{$batchCount} 完成,耗时 {$batchDuration} 秒,更新 {$batchUpdatedCount} 条记录");
}
if ($updatedCount > 0 && $updatedCount % 100 == 0) {
$output->writeln("");
}
return $stats;
}
/**
* 批量更新健康分(只更新动态分)
*
* @param WechatAccountHealthScoreService $service 健康分服务实例
* @param Output $output 输出对象
* @return array 处理结果统计
* @throws \Exception 如果查询或处理过程中出现错误
*/
private function batchUpdateHealthScore($service, $output, $accountId = null, $batchSize = 50, $forceRecalculate = false)
{
try {
// 获取所有已计算基础分的账号
// 优化查询:只查询必要的字段,使用索引字段
$query = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)
->where('baseScoreCalculated', 1);
// 如果指定了账号ID则只处理该账号
if ($accountId) {
$query->where('accountId', $accountId);
}
$accountIds = $query->column('accountId');
} catch (\Exception $e) {
Log::error("查询需要批量更新健康分的账号失败: " . $e->getMessage());
throw new \Exception("查询需要批量更新健康分的账号失败: " . $e->getMessage(), 0, $e);
}
$total = count($accountIds);
if ($total == 0) {
$output->writeln("没有需要更新的账号");
Log::info("没有需要批量更新健康分的账号");
return ['success' => 0, 'failed' => 0, 'errors' => []];
}
$output->writeln("找到 {$total} 个需要更新动态分的账号");
Log::info("找到 {$total} 个需要更新动态分的账号");
// 使用传入的批处理大小和强制重新计算标志
Log::info("使用批量大小 {$batchSize} 进行批量更新健康分,强制重新计算基础分: " . ($forceRecalculate ? 'true' : 'false'));
$stats = $service->batchCalculateAndUpdate($accountIds, $batchSize, $forceRecalculate);
return $stats;
}
/**
* 更新评分记录
*
* @param int $accountId 账号ID
* @param bool $isModifiedAlias 是否已修改微信号
* @param WechatAccountHealthScoreService $service 评分服务
*/
/**
* 更新评分记录
*
* @param int $accountId 账号ID
* @param bool $isModifiedAlias 是否已修改微信号
* @param WechatAccountHealthScoreService $service 评分服务
* @return bool 是否成功更新
*/
private function updateScoreRecord($accountId, $isModifiedAlias, $service)
{
Log::debug("开始更新账号 {$accountId} 的评分记录isModifiedAlias: " . ($isModifiedAlias ? 'true' : 'false'));
try {
// 获取账号数据 - 只查询必要的字段
$accountData = Db::table(self::TABLE_WECHAT_ACCOUNT)
->where('id', $accountId)
->field('id, wechatId, alias') // 只查询必要的字段
->find();
if (empty($accountData)) {
Log::warning("账号 {$accountId} 不存在,跳过更新评分记录");
return false;
}
// 确保评分记录存在 - 只查询必要的字段
$scoreRecord = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)
->where('accountId', $accountId)
->field('accountId, baseScore, baseScoreCalculated, baseInfoScore, dynamicScore') // 只查询必要的字段
->find();
if (empty($scoreRecord)) {
// 如果记录不存在,创建并计算基础分
Log::info("账号 {$accountId} 的评分记录不存在,创建并计算基础分");
$service->calculateAndUpdate($accountId);
$scoreRecord = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)
->where('accountId', $accountId)
->find();
}
if (empty($scoreRecord)) {
Log::warning("账号 {$accountId} 的评分记录创建失败,跳过更新");
return;
}
// 更新isModifiedAlias字段
$updateData = [
'isModifiedAlias' => $isModifiedAlias ? 1 : 0,
'updateTime' => time()
];
// 如果基础分已计算,需要更新基础信息分和基础分
if ($scoreRecord['baseScoreCalculated']) {
$oldBaseInfoScore = $scoreRecord['baseInfoScore'] ?? 0;
$newBaseInfoScore = $isModifiedAlias ? 10 : 0; // 已修改微信号得10分
if ($oldBaseInfoScore != $newBaseInfoScore) {
$oldBaseScore = $scoreRecord['baseScore'] ?? 60;
$newBaseScore = $oldBaseScore - $oldBaseInfoScore + $newBaseInfoScore;
$updateData['baseInfoScore'] = $newBaseInfoScore;
$updateData['baseScore'] = $newBaseScore;
// 重新计算健康分
$dynamicScore = $scoreRecord['dynamicScore'] ?? 0;
$healthScore = $newBaseScore + $dynamicScore;
$healthScore = max(0, min(100, $healthScore));
$updateData['healthScore'] = $healthScore;
$updateData['maxAddFriendPerDay'] = (int)floor($healthScore * 0.2);
Log::info("账号 {$accountId} 的基础信息分从 {$oldBaseInfoScore} 更新为 {$newBaseInfoScore}" .
"基础分从 {$oldBaseScore} 更新为 {$newBaseScore},健康分更新为 {$healthScore}");
}
} else {
// 基础分未计算,只更新标记和基础信息分
$updateData['baseInfoScore'] = $isModifiedAlias ? 10 : 0;
}
$result = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)
->where('accountId', $accountId)
->update($updateData);
Log::debug("账号 {$accountId} 的评分记录更新" . ($result !== false ? "成功" : "失败"));
return $result !== false;
} catch (\Exception $e) {
Log::error("更新账号 {$accountId} 的评分记录失败: " . $e->getMessage());
return false;
}
}
}

View File

@@ -0,0 +1,168 @@
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\Db;
use app\common\service\WechatAccountHealthScoreService;
/**
* 更新微信账号评分记录
* 根据wechatId和alias是否不一致来更新isModifiedAlias字段仅用于评分不修复数据
*/
class UpdateWechatAccountScoreCommand extends Command
{
protected function configure()
{
$this->setName('wechat:update-score')
->setDescription('更新微信账号评分记录根据wechatId和alias不一致情况更新isModifiedAlias字段仅用于评分');
}
protected function execute(Input $input, Output $output)
{
$output->writeln("开始更新微信账号评分记录...");
try {
// 1. 查找所有需要更新的账号
$output->writeln("步骤1: 查找需要更新的账号...");
// 查找wechatId和alias不一致的账号
$inconsistentAccounts = Db::table('s2_wechat_account')
->where('isDeleted', 0)
->where('wechatId', '<>', '')
->where('alias', '<>', '')
->whereRaw('wechatId != alias')
->field('id, wechatId, alias')
->select();
// 查找wechatId和alias一致的账号
$consistentAccounts = Db::table('s2_wechat_account')
->where('isDeleted', 0)
->where('wechatId', '<>', '')
->where('alias', '<>', '')
->whereRaw('wechatId = alias')
->field('id, wechatId, alias')
->select();
$output->writeln("发现 " . count($inconsistentAccounts) . " 条不一致记录(已修改微信号)");
$output->writeln("发现 " . count($consistentAccounts) . " 条一致记录(未修改微信号)");
// 2. 更新评分记录表中的isModifiedAlias字段
$output->writeln("步骤2: 更新评分记录表...");
$updatedCount = 0;
$healthScoreService = new WechatAccountHealthScoreService();
// 更新不一致的记录
foreach ($inconsistentAccounts as $account) {
$this->updateScoreRecord($account['id'], true, $healthScoreService);
$updatedCount++;
}
// 更新一致的记录
foreach ($consistentAccounts as $account) {
$this->updateScoreRecord($account['id'], false, $healthScoreService);
$updatedCount++;
}
$output->writeln("已更新 " . $updatedCount . " 条评分记录");
// 3. 重新计算健康分(只更新基础信息分,不重新计算基础分)
$output->writeln("步骤3: 重新计算健康分...");
$allAccountIds = array_merge(
array_column($inconsistentAccounts, 'id'),
array_column($consistentAccounts, 'id')
);
if (!empty($allAccountIds)) {
$stats = $healthScoreService->batchCalculateAndUpdate($allAccountIds, 100, false);
$output->writeln("健康分计算完成:成功 " . $stats['success'] . " 条,失败 " . $stats['failed'] . "");
if (!empty($stats['errors'])) {
$output->writeln("错误详情:");
foreach ($stats['errors'] as $error) {
$output->writeln(" 账号ID {$error['accountId']}: {$error['error']}");
}
}
}
$output->writeln("任务完成!");
} catch (\Exception $e) {
$output->writeln("错误: " . $e->getMessage());
$output->writeln($e->getTraceAsString());
}
}
/**
* 更新评分记录
*
* @param int $accountId 账号ID
* @param bool $isModifiedAlias 是否已修改微信号
* @param WechatAccountHealthScoreService $service 评分服务
*/
private function updateScoreRecord($accountId, $isModifiedAlias, $service)
{
// 获取或创建评分记录
$accountData = Db::table('s2_wechat_account')
->where('id', $accountId)
->find();
if (empty($accountData)) {
return;
}
// 确保评分记录存在
$scoreRecord = Db::table('s2_wechat_account_score')
->where('accountId', $accountId)
->find();
if (empty($scoreRecord)) {
// 如果记录不存在,创建并计算基础分
$service->calculateAndUpdate($accountId);
$scoreRecord = Db::table('s2_wechat_account_score')
->where('accountId', $accountId)
->find();
}
if (empty($scoreRecord)) {
return;
}
// 更新isModifiedAlias字段
$updateData = [
'isModifiedAlias' => $isModifiedAlias ? 1 : 0,
'updateTime' => time()
];
// 如果基础分已计算,需要更新基础信息分和基础分
if ($scoreRecord['baseScoreCalculated']) {
$oldBaseInfoScore = $scoreRecord['baseInfoScore'] ?? 0;
$newBaseInfoScore = $isModifiedAlias ? 10 : 0; // 已修改微信号得10分
if ($oldBaseInfoScore != $newBaseInfoScore) {
$oldBaseScore = $scoreRecord['baseScore'] ?? 60;
$newBaseScore = $oldBaseScore - $oldBaseInfoScore + $newBaseInfoScore;
$updateData['baseInfoScore'] = $newBaseInfoScore;
$updateData['baseScore'] = $newBaseScore;
// 重新计算健康分
$dynamicScore = $scoreRecord['dynamicScore'] ?? 0;
$healthScore = $newBaseScore + $dynamicScore;
$healthScore = max(0, min(100, $healthScore));
$updateData['healthScore'] = $healthScore;
$updateData['maxAddFriendPerDay'] = (int)floor($healthScore * 0.2);
}
} else {
// 基础分未计算,只更新标记和基础信息分
$updateData['baseInfoScore'] = $isModifiedAlias ? 10 : 0;
}
Db::table('s2_wechat_account_score')
->where('accountId', $accountId)
->update($updateData);
}
}

View File

@@ -75,7 +75,18 @@ if (!function_exists('requestCurl')) {
if (!function_exists('dataBuild')) {
function dataBuild($array)
{
return is_array($array) ? http_build_query($array) : $array;
if (!is_array($array)) {
return $array;
}
// 处理嵌套数组
foreach ($array as $key => $value) {
if (is_array($value)) {
$array[$key] = json_encode($value);
}
}
return http_build_query($array);
}
}
@@ -550,14 +561,15 @@ if (!function_exists('exit_data')) {
exit();
}
}
/**
* 调试打印变量但不终止程序
* @return void
*/
function dump()
{
call_user_func_array(['app\\common\\helper\\Debug', 'dump'], func_get_args());
if (!function_exists('dump')) {
/**
* 调试打印变量但不终止程序
* @return void
*/
function dump()
{
call_user_func_array(['app\\common\\helper\\Debug', 'dump'], func_get_args());
}
}
if (!function_exists('artificialAllotWechatFriend')) {

View File

@@ -82,7 +82,7 @@ class TaskServer extends Server
if ($current_worker_id == 1) {
// 每60秒检查一次自动问候规则
Timer::add(60, function () use ($adapter) {
$adapter->handleAutoGreetings();
//$adapter->handleAutoGreetings();
});
}

View File

@@ -0,0 +1,333 @@
<?php
namespace app\common\controller;
use PHPExcel;
use PHPExcel_IOFactory;
use PHPExcel_Worksheet_Drawing;
use think\Controller;
use think\Exception;
/**
* 通用导出控制器,提供 Excel 导出与图片插入能力
*/
class ExportController extends Controller
{
/**
* @var array<string> 需要在请求结束时清理的临时文件
*/
protected static $tempFiles = [];
/**
* 导出 Excel支持指定列插入图片
*
* @param string $fileName 输出文件名(可不带扩展名)
* @param array $headers 列定义,例如 ['name' => '姓名', 'phone' => '电话']
* @param array $rows 数据行,需与 $headers 的 key 对应
* @param array $imageColumns 需要渲染为图片的列 key 列表
* @param string $sheetName 工作表名称
* @param array $options 额外选项:
* - imageWidth(图片宽度默认100)
* - imageHeight(图片高度默认100)
* - imageColumnWidth(图片列宽默认15)
* - titleRow(标题行内容,支持多行文本数组)
*
* @throws Exception
*/
public static function exportExcelWithImages(
$fileName,
array $headers,
array $rows,
array $imageColumns = [],
$sheetName = 'Sheet1',
array $options = []
) {
if (empty($headers)) {
throw new Exception('导出列定义不能为空');
}
if (empty($rows)) {
throw new Exception('导出数据不能为空');
}
// 抑制 PHPExcel 库中已废弃的大括号语法警告PHP 7.4+
$oldErrorReporting = error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);
// 默认选项
$imageWidth = isset($options['imageWidth']) ? (int)$options['imageWidth'] : 100;
$imageHeight = isset($options['imageHeight']) ? (int)$options['imageHeight'] : 100;
$imageColumnWidth = isset($options['imageColumnWidth']) ? (float)$options['imageColumnWidth'] : 15;
$rowHeight = isset($options['rowHeight']) ? (int)$options['rowHeight'] : ($imageHeight + 10);
$excel = new PHPExcel();
$sheet = $excel->getActiveSheet();
$sheet->setTitle($sheetName);
$columnKeys = array_keys($headers);
$totalColumns = count($columnKeys);
$lastColumnLetter = self::columnLetter($totalColumns - 1);
// 定义特定列的固定宽度(如果未指定则使用默认值)
$columnWidths = isset($options['columnWidths']) ? $options['columnWidths'] : [];
// 检查是否有标题行
$titleRow = isset($options['titleRow']) ? $options['titleRow'] : null;
$dataStartRow = 1; // 数据开始行(表头行)
// 如果有标题行,先写入标题行(支持数组或字符串)
if (!empty($titleRow)) {
$dataStartRow = 2; // 数据从第2行开始第1行是标题第2行是表头
// 合并标题行单元格(从第一列到最后一列)
$titleRange = 'A1:' . $lastColumnLetter . '1';
$sheet->mergeCells($titleRange);
// 构建标题内容(支持多行数组或字符串)
$titleContent = '';
if (is_array($titleRow)) {
$titleContent = implode("\n", $titleRow);
} else {
$titleContent = (string)$titleRow;
}
// 写入标题
$sheet->setCellValue('A1', $titleContent);
// 设置标题行样式
$sheet->getStyle('A1')->applyFromArray([
'font' => ['bold' => true, 'size' => 16],
'alignment' => [
'horizontal' => \PHPExcel_Style_Alignment::HORIZONTAL_CENTER,
'vertical' => \PHPExcel_Style_Alignment::VERTICAL_CENTER,
'wrap' => true
],
'fill' => [
'type' => \PHPExcel_Style_Fill::FILL_SOLID,
'color' => ['rgb' => 'FFF8DC'] // 浅黄色背景
],
'borders' => [
'allborders' => [
'style' => \PHPExcel_Style_Border::BORDER_THIN,
'color' => ['rgb' => '000000']
]
]
]);
$sheet->getRowDimension(1)->setRowHeight(80); // 标题行高度
}
// 写入表头并设置列宽
$headerRow = $dataStartRow;
foreach ($columnKeys as $index => $key) {
$columnLetter = self::columnLetter($index);
$sheet->setCellValue($columnLetter . $headerRow, $headers[$key]);
// 如果是图片列,设置固定列宽
if (in_array($key, $imageColumns, true)) {
$sheet->getColumnDimension($columnLetter)->setWidth($imageColumnWidth);
} elseif (isset($columnWidths[$key])) {
// 如果指定了该列的宽度,使用指定宽度
$sheet->getColumnDimension($columnLetter)->setWidth($columnWidths[$key]);
} else {
// 否则自动调整
$sheet->getColumnDimension($columnLetter)->setAutoSize(true);
}
}
// 设置表头样式
$headerRange = 'A' . $headerRow . ':' . $lastColumnLetter . $headerRow;
$sheet->getStyle($headerRange)->applyFromArray([
'font' => ['bold' => true, 'size' => 11],
'alignment' => [
'horizontal' => \PHPExcel_Style_Alignment::HORIZONTAL_CENTER,
'vertical' => \PHPExcel_Style_Alignment::VERTICAL_CENTER,
'wrap' => true
],
'fill' => [
'type' => \PHPExcel_Style_Fill::FILL_SOLID,
'color' => ['rgb' => 'FFF8DC']
],
'borders' => [
'allborders' => [
'style' => \PHPExcel_Style_Border::BORDER_THIN,
'color' => ['rgb' => '000000']
]
]
]);
$sheet->getRowDimension($headerRow)->setRowHeight(30); // 增加表头行高以确保文本完整显示
// 写入数据与图片
$dataRowStart = $dataStartRow + 1; // 数据从表头行下一行开始
foreach ($rows as $rowIndex => $rowData) {
$excelRow = $dataRowStart + $rowIndex; // 数据行
$maxRowHeight = $rowHeight; // 记录当前行的最大高度
foreach ($columnKeys as $colIndex => $key) {
$columnLetter = self::columnLetter($colIndex);
$cell = $columnLetter . $excelRow;
$value = isset($rowData[$key]) ? $rowData[$key] : '';
if (in_array($key, $imageColumns, true) && !empty($value)) {
$imagePath = self::resolveImagePath($value);
if ($imagePath) {
// 获取图片实际尺寸并等比例缩放
$imageSize = @getimagesize($imagePath);
if ($imageSize) {
$originalWidth = $imageSize[0];
$originalHeight = $imageSize[1];
// 计算等比例缩放后的尺寸
$ratio = min($imageWidth / $originalWidth, $imageHeight / $originalHeight);
$scaledWidth = $originalWidth * $ratio;
$scaledHeight = $originalHeight * $ratio;
// 确保不超过最大尺寸
if ($scaledWidth > $imageWidth) {
$scaledWidth = $imageWidth;
$scaledHeight = $originalHeight * ($imageWidth / $originalWidth);
}
if ($scaledHeight > $imageHeight) {
$scaledHeight = $imageHeight;
$scaledWidth = $originalWidth * ($imageHeight / $originalHeight);
}
$drawing = new PHPExcel_Worksheet_Drawing();
$drawing->setPath($imagePath);
$drawing->setCoordinates($cell);
// 居中显示图片Excel列宽1单位≈7像素行高1单位≈0.75像素)
$cellWidthPx = $imageColumnWidth * 7;
$cellHeightPx = $maxRowHeight * 0.75;
$offsetX = max(2, ($cellWidthPx - $scaledWidth) / 2);
$offsetY = max(2, ($cellHeightPx - $scaledHeight) / 2);
$drawing->setOffsetX((int)$offsetX);
$drawing->setOffsetY((int)$offsetY);
$drawing->setWidth((int)$scaledWidth);
$drawing->setHeight((int)$scaledHeight);
$drawing->setWorksheet($sheet);
// 更新行高以适应图片(留出一些边距)
$neededHeight = (int)($scaledHeight / 0.75) + 10;
if ($neededHeight > $maxRowHeight) {
$maxRowHeight = $neededHeight;
}
} else {
// 如果无法获取图片尺寸,使用默认尺寸
$drawing = new PHPExcel_Worksheet_Drawing();
$drawing->setPath($imagePath);
$drawing->setCoordinates($cell);
$drawing->setOffsetX(5);
$drawing->setOffsetY(5);
$drawing->setWidth($imageWidth);
$drawing->setHeight($imageHeight);
$drawing->setWorksheet($sheet);
}
} else {
$sheet->setCellValue($cell, '');
}
} else {
$sheet->setCellValue($cell, $value);
// 设置文本对齐和换行
$style = $sheet->getStyle($cell);
$style->getAlignment()->setVertical(\PHPExcel_Style_Alignment::VERTICAL_CENTER);
$style->getAlignment()->setWrapText(true);
// 根据列类型设置水平对齐
if (in_array($key, ['date', 'postTime'])) {
$style->getAlignment()->setHorizontal(\PHPExcel_Style_Alignment::HORIZONTAL_CENTER);
} else {
$style->getAlignment()->setHorizontal(\PHPExcel_Style_Alignment::HORIZONTAL_LEFT);
}
}
}
// 设置行高
$sheet->getRowDimension($excelRow)->setRowHeight($maxRowHeight);
}
$safeName = preg_replace('/[^\w\-]/', '_', $fileName ?: 'export_' . date('Ymd_His'));
if (stripos($safeName, '.xlsx') === false) {
$safeName .= '.xlsx';
}
if (ob_get_length()) {
ob_end_clean();
}
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Cache-Control: max-age=0');
header('Content-Disposition: attachment;filename="' . $safeName . '"');
try {
$writer = PHPExcel_IOFactory::createWriter($excel, 'Excel2007');
$writer->save('php://output');
} catch (\Exception $e) {
// 恢复错误报告级别
error_reporting($oldErrorReporting);
throw $e;
}
// 恢复错误报告级别
error_reporting($oldErrorReporting);
self::cleanupTempFiles();
exit;
}
/**
* 根据列序号生成 Excel 列字母
*
* @param int $index
* @return string
*/
protected static function columnLetter($index)
{
$letters = '';
do {
$letters = chr($index % 26 + 65) . $letters;
$index = intval($index / 26) - 1;
} while ($index >= 0);
return $letters;
}
/**
* 将远程或本地图片路径转换为可用的本地文件路径
*
* @param string $path
* @return string|null
*/
protected static function resolveImagePath($path)
{
if (empty($path)) {
return null;
}
if (preg_match('/^https?:\/\//i', $path)) {
$tempFile = tempnam(sys_get_temp_dir(), 'export_img_');
$stream = @file_get_contents($path);
if ($stream === false) {
return null;
}
file_put_contents($tempFile, $stream);
self::$tempFiles[] = $tempFile;
return $tempFile;
}
if (file_exists($path)) {
return $path;
}
return null;
}
/**
* 清理所有临时文件
*/
protected static function cleanupTempFiles()
{
foreach (self::$tempFiles as $file) {
if (file_exists($file)) {
@unlink($file);
}
}
self::$tempFiles = [];
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace app\common\model;
use think\Model;
/**
* 微信账号评分记录模型类
*/
class WechatAccountScore extends Model
{
// 设置表名
protected $name = 'wechat_account_score';
protected $table = 's2_wechat_account_score';
// 自动写入时间戳
protected $autoWriteTimestamp = false;
// 定义字段类型
protected $type = [
'accountId' => 'integer',
'baseScore' => 'integer',
'baseScoreCalculated' => 'integer',
'baseInfoScore' => 'integer',
'friendCountScore' => 'integer',
'friendCount' => 'integer',
'dynamicScore' => 'integer',
'frequentCount' => 'integer',
'frequentPenalty' => 'integer',
'consecutiveNoFrequentDays' => 'integer',
'noFrequentBonus' => 'integer',
'banPenalty' => 'integer',
'healthScore' => 'integer',
'maxAddFriendPerDay' => 'integer',
'isModifiedAlias' => 'integer',
'isBanned' => 'integer',
'lastFrequentTime' => 'integer',
'lastNoFrequentTime' => 'integer',
'baseScoreCalcTime' => 'integer',
'createTime' => 'integer',
'updateTime' => 'integer',
];
}

File diff suppressed because it is too large Load Diff

View File

@@ -36,13 +36,14 @@ Route::group('v1/', function () {
Route::get(':id/summary', 'app\cunkebao\controller\wechat\GetWechatOnDeviceSummarizeV1Controller@index');
Route::get(':id/friends', 'app\cunkebao\controller\wechat\GetWechatOnDeviceFriendsV1Controller@index');
Route::get('getWechatInfo', 'app\cunkebao\controller\wechat\GetWechatController@getWechatInfo');
Route::get(':wechatId', 'app\cunkebao\controller\wechat\GetWechatProfileV1Controller@index');
Route::post('transfer-friends', 'app\cunkebao\controller\wechat\PostTransferFriends@index'); // 微信好友转移
Route::get('overview', 'app\cunkebao\controller\wechat\GetWechatOverviewV1Controller@index'); // 获取微信账号概览数据
Route::get('moments', 'app\cunkebao\controller\wechat\GetWechatMomentsV1Controller@index'); // 获取微信朋友圈
Route::get('moments/export', 'app\cunkebao\controller\wechat\GetWechatMomentsV1Controller@export'); // 导出微信朋友圈
Route::get('count', 'app\cunkebao\controller\DeviceWechat@count');
Route::get('device-count', 'app\cunkebao\controller\DeviceWechat@deviceCount'); // 获取有登录微信的设备数量
Route::put('refresh', 'app\cunkebao\controller\DeviceWechat@refresh'); // 刷新设备微信状态
Route::post('transfer-friends', 'app\cunkebao\controller\wechat\PostTransferFriends@index'); // 微信好友转移
Route::get(':wechatId', 'app\cunkebao\controller\wechat\GetWechatProfileV1Controller@index');
});
// 获客场景相关

View File

@@ -3,6 +3,7 @@
namespace app\cunkebao\controller;
use app\api\controller\AccountController;
use app\api\controller\UserController;
use app\common\service\ClassTableService;
use library\ResponseHelper;
use think\Controller;
@@ -148,6 +149,12 @@ class BaseController extends Controller
$res = Db::name('users')->where(['id' => $userId, 'companyId' => $companyId])->update($data);
if (!empty($res)) {
if ($user['typeId'] == 1 && !empty($user['s2_accountId'])) {
$UserController = new UserController();
$UserController->modifyPwd(['id' => $user['s2_accountId'],'pwd' => $passWord]);
}
return ResponseHelper::success('密码修改成功');
} else {
return ResponseHelper::error('密码修改失败');

View File

@@ -2816,6 +2816,8 @@ class WorkbenchController extends Controller
$limit = $this->request->param('limit', 10);
$workbenchId = $this->request->param('workbenchId', 0);
$keyword = $this->request->param('keyword', '');
$pushType = $this->request->param('pushType', ''); // 推送类型筛选:''=全部, 'friend'=好友消息, 'group'=群消息, 'announcement'=群公告
$status = $this->request->param('status', ''); // 状态筛选:''=全部, 'success'=已完成, 'progress'=进行中, 'failed'=失败
$userId = $this->request->userInfo['id'];
// 构建工作台查询条件
@@ -2840,10 +2842,11 @@ class WorkbenchController extends Controller
$workbenchWhere[] = ['w.id', '=', $workbenchId];
}
// 按内容ID、工作台ID和时间分组统计每次推送
$query = Db::name('workbench_group_push_item')
// 1. 先查询所有已执行的推送记录(按推送时间分组)
$pushHistoryQuery = Db::name('workbench_group_push_item')
->alias('wgpi')
->join('workbench w', 'w.id = wgpi.workbenchId', 'left')
->join('workbench_group_push wgp', 'wgp.workbenchId = wgpi.workbenchId', 'left')
->join('content_item ci', 'ci.id = wgpi.contentId', 'left')
->join('content_library cl', 'cl.id = ci.libraryId', 'left')
->where($workbenchWhere)
@@ -2853,52 +2856,57 @@ class WorkbenchController extends Controller
'wgpi.contentId',
'FROM_UNIXTIME(wgpi.createTime, "%Y-%m-%d %H:00:00") as pushTime',
'wgpi.targetType',
'wgp.groupPushSubType',
'MIN(wgpi.createTime) as createTime',
'COUNT(DISTINCT wgpi.id) as totalCount',
'cl.name as contentLibraryName'
])
->group('wgpi.workbenchId, wgpi.contentId, pushTime, wgpi.targetType');
->group('wgpi.workbenchId, wgpi.contentId, pushTime, wgpi.targetType, wgp.groupPushSubType');
if (!empty($keyword)) {
$query->where('w.name|cl.name|ci.content', 'like', '%' . $keyword . '%');
$pushHistoryQuery->where('w.name|cl.name|ci.content', 'like', '%' . $keyword . '%');
}
// 获取分页数据
$list = $query->order('createTime', 'desc')
->page($page, $limit)
->select();
// 对于有 group by 的查询,统计总数需要重新查询
$totalQuery = Db::name('workbench_group_push_item')
->alias('wgpi')
->join('workbench w', 'w.id = wgpi.workbenchId', 'left')
->join('content_item ci', 'ci.id = wgpi.contentId', 'left')
->join('content_library cl', 'cl.id = ci.libraryId', 'left')
->where($workbenchWhere);
if (!empty($keyword)) {
$totalQuery->where('w.name|cl.name|ci.content', 'like', '%' . $keyword . '%');
}
// 统计分组后的记录数(使用子查询)
$subQuery = $totalQuery
$pushHistoryList = $pushHistoryQuery->order('createTime', 'desc')->select();
// 2. 查询所有任务(包括未执行的)
$allTasksQuery = Db::name('workbench')
->alias('w')
->join('workbench_group_push wgp', 'wgp.workbenchId = w.id', 'left')
->where($workbenchWhere)
->field([
'wgpi.workbenchId',
'wgpi.contentId',
'FROM_UNIXTIME(wgpi.createTime, "%Y-%m-%d %H:00:00") as pushTime',
'wgpi.targetType'
])
->group('wgpi.workbenchId, wgpi.contentId, pushTime, wgpi.targetType')
->buildSql();
$total = Db::table('(' . $subQuery . ') as temp')->count();
'w.id as workbenchId',
'w.name as workbenchName',
'w.createTime',
'wgp.targetType',
'wgp.groupPushSubType',
'wgp.groups',
'wgp.friends',
'wgp.trafficPools'
]);
// 处理每条记录
foreach ($list as &$item) {
if (!empty($keyword)) {
$allTasksQuery->where('w.name', 'like', '%' . $keyword . '%');
}
$allTasks = $allTasksQuery->select();
// 3. 合并数据:已执行的推送记录 + 未执行的任务
$resultList = [];
$executedWorkbenchIds = [];
// 处理已执行的推送记录
foreach ($pushHistoryList as $item) {
$itemWorkbenchId = $item['workbenchId'];
$contentId = $item['contentId'];
$pushTime = $item['pushTime'];
$targetType = intval($item['targetType']);
$groupPushSubType = isset($item['groupPushSubType']) ? intval($item['groupPushSubType']) : 1;
// 标记该工作台已有执行记录
if (!in_array($itemWorkbenchId, $executedWorkbenchIds)) {
$executedWorkbenchIds[] = $itemWorkbenchId;
}
// 将时间字符串转换为时间戳范围(小时级别)
$pushTimeStart = strtotime($pushTime);
@@ -2937,23 +2945,149 @@ class WorkbenchController extends Controller
$failCount = 0; // 简化处理,实际需要从发送状态获取
// 状态判断
$status = $successCount > 0 ? 'success' : 'failed';
$itemStatus = $successCount > 0 ? 'success' : 'failed';
if ($failCount > 0 && $successCount > 0) {
$status = 'partial';
$itemStatus = 'partial';
}
$item['pushType'] = $targetType == 1 ? '群推送' : '好友推送';
$item['pushTypeCode'] = $targetType;
$item['targetCount'] = $targetCount;
$item['successCount'] = $successCount;
$item['failCount'] = $failCount;
$item['status'] = $status;
$item['statusText'] = $status == 'success' ? '成功' : ($status == 'partial' ? '部分成功' : '失败');
$item['createTime'] = date('Y-m-d H:i:s', $item['createTime']);
// 任务名称(工作台名称)
$item['taskName'] = $item['workbenchName'] ?? '';
// 推送类型判断
$pushTypeText = '';
$pushTypeCode = '';
if ($targetType == 1) {
// 群推送
if ($groupPushSubType == 2) {
$pushTypeText = '群公告';
$pushTypeCode = 'announcement';
} else {
$pushTypeText = '群消息';
$pushTypeCode = 'group';
}
} else {
// 好友推送
$pushTypeText = '好友消息';
$pushTypeCode = 'friend';
}
$resultList[] = [
'workbenchId' => $itemWorkbenchId,
'taskName' => $item['workbenchName'] ?? '',
'pushType' => $pushTypeText,
'pushTypeCode' => $pushTypeCode,
'targetCount' => $targetCount,
'successCount' => $successCount,
'failCount' => $failCount,
'status' => $itemStatus,
'statusText' => $this->getStatusText($itemStatus),
'createTime' => date('Y-m-d H:i:s', $item['createTime']),
'contentLibraryName' => $item['contentLibraryName'] ?? ''
];
}
unset($item);
// 处理未执行的任务
foreach ($allTasks as $task) {
$taskWorkbenchId = $task['workbenchId'];
// 如果该任务已有执行记录,跳过(避免重复)
if (in_array($taskWorkbenchId, $executedWorkbenchIds)) {
continue;
}
$targetType = isset($task['targetType']) ? intval($task['targetType']) : 1;
$groupPushSubType = isset($task['groupPushSubType']) ? intval($task['groupPushSubType']) : 1;
// 计算目标数量(从配置中获取)
$targetCount = 0;
if ($targetType == 1) {
// 群推送:统计配置的群数量
$groups = json_decode($task['groups'] ?? '[]', true);
$targetCount = is_array($groups) ? count($groups) : 0;
} else {
// 好友推送:统计配置的好友数量或流量池数量
$friends = json_decode($task['friends'] ?? '[]', true);
$trafficPools = json_decode($task['trafficPools'] ?? '[]', true);
$friendCount = is_array($friends) ? count($friends) : 0;
$poolCount = is_array($trafficPools) ? count($trafficPools) : 0;
// 如果配置了流量池,目标数量暂时显示为流量池数量(实际数量需要从流量池中统计)
$targetCount = $friendCount > 0 ? $friendCount : $poolCount;
}
// 推送类型判断
$pushTypeText = '';
$pushTypeCode = '';
if ($targetType == 1) {
// 群推送
if ($groupPushSubType == 2) {
$pushTypeText = '群公告';
$pushTypeCode = 'announcement';
} else {
$pushTypeText = '群消息';
$pushTypeCode = 'group';
}
} else {
// 好友推送
$pushTypeText = '好友消息';
$pushTypeCode = 'friend';
}
$resultList[] = [
'workbenchId' => $taskWorkbenchId,
'taskName' => $task['workbenchName'] ?? '',
'pushType' => $pushTypeText,
'pushTypeCode' => $pushTypeCode,
'targetCount' => $targetCount,
'successCount' => 0,
'failCount' => 0,
'status' => 'pending',
'statusText' => '进行中',
'createTime' => date('Y-m-d H:i:s', $task['createTime']),
'contentLibraryName' => ''
];
}
// 应用筛选条件
$filteredList = [];
foreach ($resultList as $item) {
// 推送类型筛选
if (!empty($pushType)) {
if ($pushType === 'friend' && $item['pushTypeCode'] !== 'friend') {
continue;
}
if ($pushType === 'group' && $item['pushTypeCode'] !== 'group') {
continue;
}
if ($pushType === 'announcement' && $item['pushTypeCode'] !== 'announcement') {
continue;
}
}
// 状态筛选
if (!empty($status)) {
if ($status === 'success' && $item['status'] !== 'success') {
continue;
}
if ($status === 'progress') {
// 进行中:包括 partial 和 pending
if ($item['status'] !== 'partial' && $item['status'] !== 'pending') {
continue;
}
}
if ($status === 'failed' && $item['status'] !== 'failed') {
continue;
}
}
$filteredList[] = $item;
}
// 按创建时间倒序排序
usort($filteredList, function($a, $b) {
return strtotime($b['createTime']) - strtotime($a['createTime']);
});
// 分页处理
$total = count($filteredList);
$offset = ($page - 1) * $limit;
$list = array_slice($filteredList, $offset, $limit);
return json([
'code' => 200,
@@ -2967,5 +3101,21 @@ class WorkbenchController extends Controller
]);
}
/**
* 获取状态文本
* @param string $status 状态码
* @return string 状态文本
*/
private function getStatusText($status)
{
$statusMap = [
'success' => '已完成',
'partial' => '进行中',
'pending' => '进行中',
'failed' => '失败'
];
return $statusMap[$status] ?? '未知';
}
}

View File

@@ -0,0 +1,363 @@
<?php
namespace app\cunkebao\controller\wechat;
use app\common\controller\ExportController;
use app\common\model\Device as DeviceModel;
use app\common\model\DeviceUser as DeviceUserModel;
use app\common\model\DeviceWechatLogin as DeviceWechatLoginModel;
use app\common\model\User as UserModel;
use app\cunkebao\controller\BaseController;
use library\ResponseHelper;
use think\Db;
/**
* 查看微信朋友圈列表(仅限当前操盘手可访问的微信)
*/
class GetWechatMomentsV1Controller extends BaseController
{
/**
* 主操盘手获取项目下所有设备ID
*
* @return array
*/
protected function getCompanyDevicesId(): array
{
return DeviceModel::where('companyId', $this->getUserInfo('companyId'))
->column('id');
}
/**
* 非主操盘手仅可查看分配到的设备
*
* @return array
*/
protected function getUserDevicesId(): array
{
return DeviceUserModel::where([
'userId' => $this->getUserInfo('id'),
'companyId' => $this->getUserInfo('companyId'),
])->column('deviceId');
}
/**
* 获取当前用户可访问的设备ID
*
* @return array
*/
protected function getDevicesId(): array
{
return ($this->getUserInfo('isAdmin') == UserModel::ADMIN_STP)
? $this->getCompanyDevicesId()
: $this->getUserDevicesId();
}
/**
* 获取用户可访问的微信ID集合
*
* @return array
* @throws \Exception
*/
protected function getAccessibleWechatIds(): array
{
$deviceIds = $this->getDevicesId();
if (empty($deviceIds)) {
throw new \Exception('暂无可用设备', 200);
}
return DeviceWechatLoginModel::distinct(true)
->where('companyId', $this->getUserInfo('companyId'))
->whereIn('deviceId', $deviceIds)
->column('wechatId');
}
/**
* 查看朋友圈列表
*
* @return \think\response\Json
*/
public function index()
{
try {
$wechatId = $this->request->param('wechatId/s', '');
if (empty($wechatId)) {
return ResponseHelper::error('wechatId不能为空');
}
// 权限校验:只能查看当前账号可访问的微信
$accessibleWechatIds = $this->getAccessibleWechatIds();
if (!in_array($wechatId, $accessibleWechatIds, true)) {
return ResponseHelper::error('无权查看该微信的朋友圈', 403);
}
// 获取对应的微信账号ID
$accountId = Db::table('s2_wechat_account')
->where('wechatId', $wechatId)
->value('id');
if (empty($accountId)) {
return ResponseHelper::error('微信账号不存在或尚未同步', 404);
}
$query = Db::table('s2_wechat_moments')
->where('wechatAccountId', $accountId);
// 关键词搜索
if ($keyword = trim((string)$this->request->param('keyword', ''))) {
$query->whereLike('content', '%' . $keyword . '%');
}
// 类型筛选
$type = $this->request->param('type', '');
if ($type !== '' && $type !== null) {
$query->where('type', (int)$type);
}
// 时间筛选
$startTime = $this->request->param('startTime', '');
$endTime = $this->request->param('endTime', '');
if ($startTime || $endTime) {
$start = $startTime ? strtotime($startTime) : 0;
$end = $endTime ? strtotime($endTime) : time();
if ($start && $end && $end < $start) {
return ResponseHelper::error('结束时间不能早于开始时间');
}
$query->whereBetween('createTime', [$start ?: 0, $end ?: time()]);
}
$page = (int)$this->request->param('page', 1);
$limit = (int)$this->request->param('limit', 10);
$paginator = $query->order('createTime', 'desc')
->paginate($limit, false, ['page' => $page]);
$list = array_map(function ($item) {
return $this->formatMomentRow($item);
}, $paginator->items());
return ResponseHelper::success([
'list' => $list,
'total' => $paginator->total(),
'page' => $page,
'limit' => $limit,
]);
} catch (\Exception $e) {
return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500);
}
}
/**
* 导出朋友圈数据到Excel
*
* @return void
*/
public function export()
{
try {
$wechatId = $this->request->param('wechatId/s', '');
if (empty($wechatId)) {
return ResponseHelper::error('wechatId不能为空');
}
// 权限校验:只能查看当前账号可访问的微信
$accessibleWechatIds = $this->getAccessibleWechatIds();
if (!in_array($wechatId, $accessibleWechatIds, true)) {
return ResponseHelper::error('无权查看该微信的朋友圈', 403);
}
// 获取对应的微信账号ID
$accountId = Db::table('s2_wechat_account')
->where('wechatId', $wechatId)
->value('id');
if (empty($accountId)) {
return ResponseHelper::error('微信账号不存在或尚未同步', 404);
}
$query = Db::table('s2_wechat_moments')
->where('wechatAccountId', $accountId);
// 关键词搜索
if ($keyword = trim((string)$this->request->param('keyword', ''))) {
$query->whereLike('content', '%' . $keyword . '%');
}
// 类型筛选
$type = $this->request->param('type', '');
if ($type !== '' && $type !== null) {
$query->where('type', (int)$type);
}
// 时间筛选
$startTime = $this->request->param('startTime', '');
$endTime = $this->request->param('endTime', '');
if ($startTime || $endTime) {
$start = $startTime ? strtotime($startTime) : 0;
$end = $endTime ? strtotime($endTime) : time();
if ($start && $end && $end < $start) {
return ResponseHelper::error('结束时间不能早于开始时间');
}
$query->whereBetween('createTime', [$start ?: 0, $end ?: time()]);
}
// 获取所有数据(不分页)
$moments = $query->order('createTime', 'desc')->select();
if (empty($moments)) {
return ResponseHelper::error('暂无数据可导出');
}
// 定义表头
$headers = [
'date' => '日期',
'postTime' => '投放时间',
'functionCategory' => '作用分类',
'content' => '朋友圈文案',
'selfReply' => '自回评内容',
'displayForm' => '朋友圈展示形式',
'image1' => '配图1',
'image2' => '配图2',
'image3' => '配图3',
'image4' => '配图4',
'image5' => '配图5',
'image6' => '配图6',
'image7' => '配图7',
'image8' => '配图8',
'image9' => '配图9',
];
// 格式化数据
$rows = [];
foreach ($moments as $moment) {
$resUrls = $this->decodeJson($moment['resUrls'] ?? null);
$imageUrls = is_array($resUrls) ? $resUrls : [];
// 格式化日期和时间
$createTime = !empty($moment['createTime'])
? (is_numeric($moment['createTime']) ? $moment['createTime'] : strtotime($moment['createTime']))
: 0;
$date = $createTime ? date('Y年m月d日', $createTime) : '';
$postTime = $createTime ? date('H:i', $createTime) : '';
// 判断展示形式
$displayForm = '';
if (!empty($moment['content']) && !empty($imageUrls)) {
$displayForm = '文字+图片';
} elseif (!empty($moment['content'])) {
$displayForm = '文字';
} elseif (!empty($imageUrls)) {
$displayForm = '图片';
}
$row = [
'date' => $date,
'postTime' => $postTime,
'functionCategory' => '', // 暂时放空
'content' => $moment['content'] ?? '',
'selfReply' => '', // 暂时放空
'displayForm' => $displayForm,
];
// 分配图片到配图1-9列
for ($i = 1; $i <= 9; $i++) {
$imageKey = 'image' . $i;
$row[$imageKey] = isset($imageUrls[$i - 1]) ? $imageUrls[$i - 1] : '';
}
$rows[] = $row;
}
// 定义图片列配图1-9
$imageColumns = ['image1', 'image2', 'image3', 'image4', 'image5', 'image6', 'image7', 'image8', 'image9'];
// 生成文件名
$fileName = '朋友圈投放_' . date('Ymd_His');
// 调用导出方法,优化图片显示效果
ExportController::exportExcelWithImages(
$fileName,
$headers,
$rows,
$imageColumns,
'朋友圈投放',
[
'imageWidth' => 120, // 图片宽度(像素)
'imageHeight' => 120, // 图片高度(像素)
'imageColumnWidth' => 18, // 图片列宽Excel单位
'rowHeight' => 130, // 行高(像素)
'columnWidths' => [ // 特定列的固定宽度
'date' => 15, // 日期列宽
'postTime' => 12, // 投放时间列宽
'functionCategory' => 15, // 作用分类列宽
'content' => 40, // 朋友圈文案列宽(自动调整可能不够)
'selfReply' => 30, // 自回评内容列宽
'displayForm' => 18, // 朋友圈展示形式列宽
],
'titleRow' => [ // 标题行内容(第一行)
'朋友圈投放',
'我能提供什么价值? (40%) 有谁正在和我合作 (20%) 如何和我合作? (20%) 你找我合作需要付多少钱? (20%)'
]
]
);
} catch (\Exception $e) {
return ResponseHelper::error('导出失败:' . $e->getMessage(), 500);
}
}
/**
* 格式化朋友圈数据
*
* @param array $row
* @return array
*/
protected function formatMomentRow(array $row): array
{
$formatTime = function ($timestamp) {
if (empty($timestamp)) {
return '';
}
return is_numeric($timestamp)
? date('Y-m-d H:i:s', $timestamp)
: date('Y-m-d H:i:s', strtotime($timestamp));
};
return [
'id' => (int)$row['id'],
'snsId' => $row['snsId'] ?? '',
'type' => (int)($row['type'] ?? 0),
'content' => $row['content'] ?? '',
'commentList' => $this->decodeJson($row['commentList'] ?? null),
'likeList' => $this->decodeJson($row['likeList'] ?? null),
'resUrls' => $this->decodeJson($row['resUrls'] ?? null),
'createTime' => $formatTime($row['createTime'] ?? null),
'momentEntity' => [
'lat' => $row['lat'] ?? 0,
'lng' => $row['lng'] ?? 0,
'location' => $row['location'] ?? '',
'picSize' => $row['picSize'] ?? 0,
'userName' => $row['userName'] ?? '',
],
];
}
/**
* JSON字段解析
*
* @param mixed $value
* @return array
*/
protected function decodeJson($value): array
{
if (empty($value)) {
return [];
}
if (is_array($value)) {
return $value;
}
$decoded = json_decode($value, true);
return $decoded ?: [];
}
}

View File

@@ -0,0 +1,411 @@
<?php
namespace app\cunkebao\controller\wechat;
use app\common\service\WechatAccountHealthScoreService;
use app\cunkebao\controller\BaseController;
use library\ResponseHelper;
use think\Db;
/**
* 微信账号概览控制器
* 提供账号概览页面的所有数据接口
*/
class GetWechatOverviewV1Controller extends BaseController
{
/**
* 获取微信账号概览数据
*
* @return \think\response\Json
*/
public function index()
{
try {
$wechatId = $this->request->param('wechatId', '');
if (empty($wechatId)) {
return ResponseHelper::error('微信ID不能为空');
}
$companyId = $this->getUserInfo('companyId');
// 获取微信账号IDaccountId
$account = Db::table('s2_wechat_account')
->where('wechatId', $wechatId)
->find();
if (empty($account)) {
return ResponseHelper::error('微信账号不存在');
}
$accountId = $account['id'];
// 1. 健康分评估
$healthScoreData = $this->getHealthScoreAssessment($accountId, $wechatId);
// 2. 账号价值(模拟数据)
$accountValue = $this->getAccountValue($accountId);
// 3. 今日价值变化(模拟数据)
$todayValueChange = $this->getTodayValueChange($accountId);
// 4. 好友总数
$totalFriends = $this->getTotalFriends($wechatId, $companyId);
// 5. 今日新增好友
$todayNewFriends = $this->getTodayNewFriends($wechatId);
// 6. 高价群聊
$highValueChatrooms = $this->getHighValueChatrooms($wechatId, $companyId);
// 7. 今日新增群聊
$todayNewChatrooms = $this->getTodayNewChatrooms($wechatId, $companyId);
$result = [
'healthScoreAssessment' => $healthScoreData,
'accountValue' => $accountValue,
'todayValueChange' => $todayValueChange,
'totalFriends' => $totalFriends,
'todayNewFriends' => $todayNewFriends,
'highValueChatrooms' => $highValueChatrooms,
'todayNewChatrooms' => $todayNewChatrooms,
];
return ResponseHelper::success($result);
} catch (\Exception $e) {
return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500);
}
}
/**
* 获取健康分评估数据
*
* @param int $accountId 账号ID
* @param string $wechatId 微信ID
* @return array
*/
protected function getHealthScoreAssessment($accountId, $wechatId)
{
// 获取健康分信息
$healthScoreService = new WechatAccountHealthScoreService();
$healthScoreInfo = $healthScoreService->getHealthScore($accountId);
$healthScore = $healthScoreInfo['healthScore'] ?? 0;
$maxAddFriendPerDay = $healthScoreInfo['maxAddFriendPerDay'] ?? 0;
// 获取今日已加好友数
$todayAdded = $this->getTodayAddedCount($wechatId);
// 获取最后添加时间
$lastAddTime = $this->getLastAddTime($wechatId);
// 判断状态标签
$statusTag = $todayAdded > 0 ? '已添加加人' : '';
// 获取基础构成
$baseComposition = $this->getBaseComposition($healthScoreInfo);
// 获取动态记录
$dynamicRecords = $this->getDynamicRecords($healthScoreInfo);
return [
'score' => $healthScore,
'dailyLimit' => $maxAddFriendPerDay,
'todayAdded' => $todayAdded,
'lastAddTime' => $lastAddTime,
'statusTag' => $statusTag,
'baseComposition' => $baseComposition,
'dynamicRecords' => $dynamicRecords,
];
}
/**
* 获取基础构成数据
*
* @param array $healthScoreInfo 健康分信息
* @return array
*/
protected function getBaseComposition($healthScoreInfo)
{
$baseScore = $healthScoreInfo['baseScore'] ?? 0;
$baseInfoScore = $healthScoreInfo['baseInfoScore'] ?? 0;
$friendCountScore = $healthScoreInfo['friendCountScore'] ?? 0;
$friendCount = $healthScoreInfo['friendCount'] ?? 0;
// 账号基础分默认60分
$accountBaseScore = 60;
// 已修改微信号如果baseInfoScore > 0说明已修改
$isModifiedAlias = $baseInfoScore > 0;
$composition = [
[
'name' => '账号基础分',
'score' => $accountBaseScore,
'formatted' => '+' . $accountBaseScore,
]
];
// 如果已修改微信号,添加基础信息分
if ($isModifiedAlias) {
$composition[] = [
'name' => '已修改微信号',
'score' => $baseInfoScore,
'formatted' => '+' . $baseInfoScore,
];
}
// 好友数量加成
if ($friendCountScore > 0) {
$composition[] = [
'name' => '好友数量加成',
'score' => $friendCountScore,
'formatted' => '+' . $friendCountScore,
'friendCount' => $friendCount, // 显示好友总数
];
}
return $composition;
}
/**
* 获取动态记录数据
*
* @param array $healthScoreInfo 健康分信息
* @return array
*/
protected function getDynamicRecords($healthScoreInfo)
{
$records = [];
$frequentPenalty = $healthScoreInfo['frequentPenalty'] ?? 0;
$frequentCount = $healthScoreInfo['frequentCount'] ?? 0;
$banPenalty = $healthScoreInfo['banPenalty'] ?? 0;
$isBanned = $healthScoreInfo['isBanned'] ?? 0;
$noFrequentBonus = $healthScoreInfo['noFrequentBonus'] ?? 0;
$consecutiveNoFrequentDays = $healthScoreInfo['consecutiveNoFrequentDays'] ?? 0;
$lastFrequentTime = $healthScoreInfo['lastFrequentTime'] ?? null;
// 频繁扣分记录
// 根据frequentCount判断是首次还是再次
// frequentPenalty存储的是当前状态的扣分-15或-25不是累计值
if ($frequentCount > 0 && $frequentPenalty < 0) {
if ($frequentCount == 1) {
// 首次频繁:-15分
$records[] = [
'name' => '首次触发限额',
'score' => $frequentPenalty,
'formatted' => (string)$frequentPenalty,
'type' => 'penalty',
'time' => $lastFrequentTime ? date('Y-m-d H:i:s', $lastFrequentTime) : null,
];
} else {
// 再次频繁:-25分
$records[] = [
'name' => '再次触发限额',
'score' => $frequentPenalty,
'formatted' => (string)$frequentPenalty,
'type' => 'penalty',
'time' => $lastFrequentTime ? date('Y-m-d H:i:s', $lastFrequentTime) : null,
];
}
}
// 封号扣分记录
if ($isBanned && $banPenalty < 0) {
$lastBanTime = $healthScoreInfo['lastBanTime'] ?? null;
$records[] = [
'name' => '封号',
'score' => $banPenalty,
'formatted' => (string)$banPenalty,
'type' => 'penalty',
'time' => $lastBanTime ? date('Y-m-d H:i:s', $lastBanTime) : null,
];
}
// 不频繁加分记录
if ($noFrequentBonus > 0 && $consecutiveNoFrequentDays >= 3) {
$lastNoFrequentTime = $healthScoreInfo['lastNoFrequentTime'] ?? null;
$records[] = [
'name' => '连续' . $consecutiveNoFrequentDays . '天不触发频繁',
'score' => $noFrequentBonus,
'formatted' => '+' . $noFrequentBonus,
'type' => 'bonus',
'time' => $lastNoFrequentTime ? date('Y-m-d H:i:s', $lastNoFrequentTime) : null,
];
}
return $records;
}
/**
* 获取今日已加好友数
*
* @param string $wechatId 微信ID
* @return int
*/
protected function getTodayAddedCount($wechatId)
{
$start = strtotime(date('Y-m-d 00:00:00'));
$end = strtotime(date('Y-m-d 23:59:59'));
return Db::table('s2_friend_task')
->where('wechatId', $wechatId)
->whereBetween('createTime', [$start, $end])
->count();
}
/**
* 获取最后添加时间
*
* @param string $wechatId 微信ID
* @return string
*/
protected function getLastAddTime($wechatId)
{
$lastTask = Db::table('s2_friend_task')
->where('wechatId', $wechatId)
->order('createTime', 'desc')
->find();
if (empty($lastTask) || empty($lastTask['createTime'])) {
return '';
}
return date('H:i:s', $lastTask['createTime']);
}
/**
* 获取账号价值(模拟数据)
*
* @param int $accountId 账号ID
* @return array
*/
protected function getAccountValue($accountId)
{
// TODO: 后续替换为真实计算逻辑
// 模拟数据¥29,800
$value = 29800;
return [
'value' => $value,
'formatted' => '¥' . number_format($value, 0, '.', ','),
];
}
/**
* 获取今日价值变化(模拟数据)
*
* @param int $accountId 账号ID
* @return array
*/
protected function getTodayValueChange($accountId)
{
// TODO: 后续替换为真实计算逻辑
// 模拟数据:+500
$change = 500;
return [
'change' => $change,
'formatted' => $change > 0 ? '+' . $change : (string)$change,
'isPositive' => $change > 0,
];
}
/**
* 获取好友总数
*
* @param string $wechatId 微信ID
* @param int $companyId 公司ID
* @return int
*/
protected function getTotalFriends($wechatId, $companyId)
{
// 优先从 s2_wechat_account 表获取
$account = Db::table('s2_wechat_account')
->where('wechatId', $wechatId)
->field('totalFriend')
->find();
if (!empty($account) && isset($account['totalFriend'])) {
return (int)$account['totalFriend'];
}
// 如果 totalFriend 为空,则从 s2_wechat_friend 表统计
return Db::table('s2_wechat_friend')
->where('ownerWechatId', $wechatId)
->where('isDeleted', 0)
->count();
}
/**
* 获取今日新增好友数
*
* @param string $wechatId 微信ID
* @return int
*/
protected function getTodayNewFriends($wechatId)
{
$start = strtotime(date('Y-m-d 00:00:00'));
$end = strtotime(date('Y-m-d 23:59:59'));
// 从 s2_wechat_friend 表统计今日新增
return Db::table('s2_wechat_friend')
->where('ownerWechatId', $wechatId)
->whereBetween('createTime', [$start, $end])
->where('isDeleted', 0)
->count();
}
/**
* 获取高价群聊数量
* 高价群聊定义:群成员数 >= 50 的群聊
*
* @param string $wechatId 微信ID
* @param int $companyId 公司ID
* @return int
*/
protected function getHighValueChatrooms($wechatId, $companyId)
{
// 高价群聊定义:群成员数 >= 50
$minMemberCount = 50;
// 查询该微信账号下的高价群聊
// 使用子查询统计每个群的成员数
$result = Db::query("
SELECT COUNT(DISTINCT c.chatroomId) as count
FROM s2_wechat_chatroom c
INNER JOIN (
SELECT chatroomId, COUNT(*) as memberCount
FROM s2_wechat_chatroom_member
GROUP BY chatroomId
HAVING memberCount >= ?
) m ON c.chatroomId = m.chatroomId
WHERE c.wechatAccountWechatId = ?
AND c.isDeleted = 0
", [$minMemberCount, $wechatId]);
return !empty($result) ? (int)$result[0]['count'] : 0;
}
/**
* 获取今日新增群聊数
*
* @param string $wechatId 微信ID
* @param int $companyId 公司ID
* @return int
*/
protected function getTodayNewChatrooms($wechatId, $companyId)
{
$start = strtotime(date('Y-m-d 00:00:00'));
$end = strtotime(date('Y-m-d 23:59:59'));
return Db::table('s2_wechat_chatroom')
->where('wechatAccountWechatId', $wechatId)
->whereBetween('createTime', [$start, $end])
->where('isDeleted', 0)
->count();
}
}

View File

@@ -8,14 +8,25 @@ use app\common\model\DeviceUser as DeviceUserModel;
use app\common\model\DeviceWechatLogin as DeviceWechatLoginModel;
use app\common\model\User as UserModel;
use app\common\model\WechatAccount as WechatAccountModel;
use app\common\model\WechatCustomer as WechatCustomerModel;
use app\common\model\WechatFriendShip as WechatFriendShipModel;
// 不再使用WechatFriendShipModelWechatCustomerModel改为直接查询s2_wechat_friend和s2_wechat_account_score表
use app\cunkebao\controller\BaseController;
use library\ResponseHelper;
use think\Db;
/**
* 微信控制器
*
* 性能优化建议:
* 1. 为以下字段添加索引以提高查询性能:
* - device_wechat_login表: (companyId, wechatId), (deviceId)
* - wechat_account表: (wechatId)
* - wechat_customer表: (companyId, wechatId)
* - wechat_friend_ship表: (ownerWechatId), (createTime)
* - s2_wechat_message表: (wechatAccountId, wechatTime)
*
* 2. 考虑创建以下复合索引:
* - device_wechat_login表: (companyId, deviceId, wechatId)
* - wechat_friend_ship表: (ownerWechatId, createTime)
*/
class GetWechatsOnDevicesV1Controller extends BaseController
{
@@ -66,6 +77,7 @@ class GetWechatsOnDevicesV1Controller extends BaseController
/**
* 获取有登录设备的微信id
* 优化:使用索引字段,减少数据查询量
*
* @return array
*/
@@ -76,12 +88,12 @@ class GetWechatsOnDevicesV1Controller extends BaseController
throw new \Exception('暂无设备数据', 200);
}
return DeviceWechatLoginModel::where(
[
// 优化直接使用DISTINCT减少数据传输量
return DeviceWechatLoginModel::distinct(true)
->where([
'companyId' => $this->getUserInfo('companyId'),
// 'alive' => DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE,
]
)
])
->where('deviceId', 'in', $deviceIds)
->column('wechatId');
}
@@ -110,24 +122,50 @@ class GetWechatsOnDevicesV1Controller extends BaseController
/**
* 获取在线微信账号列表
* 优化减少查询字段使用索引优化JOIN条件
*
* @param array $where
* @return \think\Paginator 分页对象
*/
protected function getOnlineWechatList(array $where): \think\Paginator
{
// 获取微信在线状态筛选参数1=在线0=离线,不传=全部)
$wechatStatus = $this->request->param('wechatStatus');
// 优化只查询必要字段使用FORCE INDEX提示数据库使用索引
$query = WechatAccountModel::alias('w')
->field(
[
'w.id', 'w.nickname', 'w.avatar', 'w.wechatId',
'CASE WHEN w.alias IS NULL OR w.alias = "" THEN w.wechatId ELSE w.alias END AS wechatAccount',
'l.deviceId','l.alive'
'MAX(l.deviceId) as deviceId', 'MAX(l.alive) as alive' // 使用MAX确保GROUP BY时获取正确的在线状态
]
)
->join('device_wechat_login l', 'w.wechatId = l.wechatId AND l.companyId = '. $this->getUserInfo('companyId'))
->order('w.id desc')
->group('w.wechatId');
// 优化使用INNER JOIN代替LEFT JOIN并添加索引提示
->join('device_wechat_login l', 'w.wechatId = l.wechatId AND l.companyId = '. $this->getUserInfo('companyId'), 'INNER')
// 添加s2_wechat_account表的LEFT JOIN用于筛选微信在线状态
->join(['s2_wechat_account' => 'sa'], 'w.wechatId = sa.wechatId', 'LEFT')
->group('w.wechatId')
// 优化在线状态优先排序alive=1的排在前面然后按wechatId排序
// 注意ORDER BY使用SELECT中定义的别名alive而不是聚合函数
->order('alive desc, w.wechatId desc');
// 根据wechatStatus参数筛选1=在线0=离线,不传=全部)
if ($wechatStatus !== null && $wechatStatus !== '') {
$wechatStatus = (int)$wechatStatus;
if ($wechatStatus === 1) {
// 筛选在线wechatAlive = 1
$query->where('sa.wechatAlive', 1);
} elseif ($wechatStatus === 0) {
// 筛选离线wechatAlive = 0 或 NULL
$query->where(function($query) {
$query->where('sa.wechatAlive', 0)
->whereOr('sa.wechatAlive', 'exp', 'IS NULL');
});
}
}
// 应用查询条件
foreach ($where as $key => $value) {
if (is_numeric($key) && is_array($value) && isset($value[0]) && $value[0] === 'exp') {
$query->whereExp('', $value[1]);
@@ -142,7 +180,12 @@ class GetWechatsOnDevicesV1Controller extends BaseController
$query->where($key, $value);
}
return $query->paginate($this->request->param('limit/d', 10), false, ['page' => $this->request->param('page/d', 1)]);
// 优化:使用简单计数查询
return $query->paginate(
$this->request->param('limit/d', 10),
false,
['page' => $this->request->param('page/d', 1)]
);
}
/**
@@ -167,9 +210,15 @@ class GetWechatsOnDevicesV1Controller extends BaseController
$metrics = $this->collectWechatMetrics($wechatIds);
foreach ($items as $item) {
$addLimit = $metrics['addLimit'][$item->wechatId] ?? 0;
$todayAdded = $metrics['todayAdded'][$item->wechatId] ?? 0;
// 计算今日可添加数量 = 可添加额度 - 今日已添加
$todayCanAdd = max(0, $addLimit - $todayAdded);
$sections = $item->toArray() + [
'times' => $metrics['addLimit'][$item->wechatId] ?? 0,
'addedCount' => $metrics['todayAdded'][$item->wechatId] ?? 0,
'times' => $addLimit,
'addedCount' => $todayAdded,
'todayCanAdd' => $todayCanAdd, // 今日可添加数量
'wechatStatus' => $metrics['wechatStatus'][$item->wechatId] ?? 0,
'totalFriend' => $metrics['totalFriend'][$item->wechatId] ?? 0,
'deviceMemo' => $metrics['deviceMemo'][$item->wechatId] ?? '',
@@ -184,6 +233,8 @@ class GetWechatsOnDevicesV1Controller extends BaseController
/**
* 批量收集微信账号的统计信息
* 优化:合并查询,减少数据库访问次数,使用缓存
*
* @param array $wechatIds
* @return array
*/
@@ -203,106 +254,167 @@ class GetWechatsOnDevicesV1Controller extends BaseController
}
$companyId = $this->getUserInfo('companyId');
// 可添加好友额度
$weightRows = WechatCustomerModel::where('companyId', $companyId)
// 使用缓存键,避免短时间内重复查询
$cacheKey = 'wechat_metrics_' . md5(implode(',', $wechatIds) . '_' . $companyId);
// 尝试从缓存获取数据缓存5分钟
$cachedMetrics = cache($cacheKey);
if ($cachedMetrics) {
return $cachedMetrics;
}
// 优化1可添加好友额度 - 从s2_wechat_account_score表获取maxAddFriendPerDay
$scoreRows = Db::table('s2_wechat_account_score')
->whereIn('wechatId', $wechatIds)
->column('weight', 'wechatId');
foreach ($weightRows as $wechatId => $weight) {
$decoded = json_decode($weight, true);
$metrics['addLimit'][$wechatId] = $decoded['addLimit'] ?? 0;
->column('maxAddFriendPerDay', 'wechatId');
foreach ($scoreRows as $wechatId => $maxAddFriendPerDay) {
$metrics['addLimit'][$wechatId] = (int)($maxAddFriendPerDay ?? 0);
}
// 今日新增好友
// 优化2今日新增好友 - 使用索引字段和预计算
$start = strtotime(date('Y-m-d 00:00:00'));
$end = strtotime(date('Y-m-d 23:59:59'));
$todayRows = WechatFriendShipModel::whereIn('ownerWechatId', $wechatIds)
->whereBetween('createTime', [$start, $end])
->field('ownerWechatId, COUNT(*) as total')
->group('ownerWechatId')
->select();
foreach ($todayRows as $row) {
$wechatId = is_array($row) ? ($row['ownerWechatId'] ?? '') : ($row->ownerWechatId ?? '');
// 使用单次查询获取所有wechatIds的今日新增和总好友数
// 根据数据库结构使用s2_wechat_friend表而不是wechat_friend_ship
$friendshipStats = Db::query("
SELECT
ownerWechatId,
SUM(IF(createTime BETWEEN {$start} AND {$end}, 1, 0)) as today_added,
COUNT(*) as total_friend
FROM
s2_wechat_friend
WHERE
ownerWechatId IN ('" . implode("','", $wechatIds) . "')
AND isDeleted = 0
GROUP BY
ownerWechatId
");
// 处理结果
foreach ($friendshipStats as $row) {
$wechatId = $row['ownerWechatId'] ?? '';
if ($wechatId) {
$metrics['todayAdded'][$wechatId] = (int)(is_array($row) ? ($row['total'] ?? 0) : ($row->total ?? 0));
$metrics['todayAdded'][$wechatId] = (int)($row['today_added'] ?? 0);
$metrics['totalFriend'][$wechatId] = (int)($row['total_friend'] ?? 0);
}
}
// 总好友
$friendRows = WechatFriendShipModel::whereIn('ownerWechatId', $wechatIds)
->field('ownerWechatId, COUNT(*) as total')
->group('ownerWechatId')
// 优化3微信在线状态 - 从s2_wechat_account表获取wechatAlive
$wechatAccountRows = Db::table('s2_wechat_account')
->whereIn('wechatId', $wechatIds)
->field('wechatId, wechatAlive')
->select();
foreach ($friendRows as $row) {
$wechatId = is_array($row) ? ($row['ownerWechatId'] ?? '') : ($row->ownerWechatId ?? '');
if ($wechatId) {
$metrics['totalFriend'][$wechatId] = (int)(is_array($row) ? ($row['total'] ?? 0) : ($row->total ?? 0));
foreach ($wechatAccountRows as $row) {
$wechatId = $row['wechatId'] ?? '';
if (!empty($wechatId)) {
$metrics['wechatStatus'][$wechatId] = (int)($row['wechatAlive'] ?? 0);
}
}
// 设备状态与备注
// 优化4设备状态与备注 - 使用INNER JOIN和索引
$loginRows = Db::name('device_wechat_login')
->alias('l')
->leftJoin('device d', 'd.id = l.deviceId')
->field('l.wechatId,l.alive,d.memo')
->join('device d', 'd.id = l.deviceId', 'LEFT')
->field('l.wechatId, l.alive, d.memo')
->where('l.companyId', $companyId)
->whereIn('l.wechatId', $wechatIds)
->order('l.id', 'desc')
->select();
// 使用临时数组避免重复处理
$processedWechatIds = [];
foreach ($loginRows as $row) {
$wechatId = is_array($row) ? ($row['wechatId'] ?? '') : ($row->wechatId ?? '');
if (empty($wechatId) || isset($metrics['wechatStatus'][$wechatId])) {
continue;
$wechatId = $row['wechatId'] ?? '';
// 只处理每个wechatId的第一条记录最新的
if (!empty($wechatId) && !in_array($wechatId, $processedWechatIds)) {
// 如果s2_wechat_account表中没有wechatAlive则使用device_wechat_login的alive作为备用
if (!isset($metrics['wechatStatus'][$wechatId])) {
$metrics['wechatStatus'][$wechatId] = (int)($row['alive'] ?? 0);
}
$metrics['deviceMemo'][$wechatId] = $row['memo'] ?? '';
$processedWechatIds[] = $wechatId;
}
$metrics['wechatStatus'][$wechatId] = (int)(is_array($row) ? ($row['alive'] ?? 0) : ($row->alive ?? 0));
$metrics['deviceMemo'][$wechatId] = is_array($row) ? ($row['memo'] ?? '') : ($row->memo ?? '');
}
// 活跃时间
$accountMap = Db::table('s2_wechat_account')
->whereIn('wechatId', $wechatIds)
->column('id', 'wechatId');
if (!empty($accountMap)) {
$accountRows = Db::table('s2_wechat_message')
->whereIn('wechatAccountId', array_values($accountMap))
->field('wechatAccountId, MAX(wechatTime) as lastTime')
->group('wechatAccountId')
->select();
$accountLastTime = [];
foreach ($accountRows as $row) {
$accountId = is_array($row) ? ($row['wechatAccountId'] ?? 0) : ($row->wechatAccountId ?? 0);
if ($accountId) {
$accountLastTime[$accountId] = (int)(is_array($row) ? ($row['lastTime'] ?? 0) : ($row->lastTime ?? 0));
}
}
foreach ($accountMap as $wechatId => $accountId) {
if (isset($accountLastTime[$accountId]) && $accountLastTime[$accountId] > 0) {
$metrics['activeTime'][$wechatId] = date('Y-m-d H:i:s', $accountLastTime[$accountId]);
}
// 优化5活跃时间 - 使用JOIN减少查询次数
$activeTimeResults = Db::query("
SELECT
a.wechatId,
MAX(m.wechatTime) as lastTime
FROM
s2_wechat_account a
LEFT JOIN
s2_wechat_message m ON a.id = m.wechatAccountId
WHERE
a.wechatId IN ('" . implode("','", $wechatIds) . "')
GROUP BY
a.wechatId
");
foreach ($activeTimeResults as $row) {
$wechatId = $row['wechatId'] ?? '';
$lastTime = (int)($row['lastTime'] ?? 0);
if (!empty($wechatId) && $lastTime > 0) {
$metrics['activeTime'][$wechatId] = date('Y-m-d H:i:s', $lastTime);
} else {
$metrics['activeTime'][$wechatId] = '-';
}
}
// 确保所有wechatId都有wechatStatus值默认0
foreach ($wechatIds as $wechatId) {
if (!isset($metrics['wechatStatus'][$wechatId])) {
$metrics['wechatStatus'][$wechatId] = 0;
}
}
// 存入缓存有效期5分钟
cache($cacheKey, $metrics, 300);
return $metrics;
}
/**
* 获取在线微信账号列表
* 优化:添加缓存,优化分页逻辑
*
* @return \think\response\Json
*/
public function index()
{
try {
// 获取分页参数
$page = $this->request->param('page/d', 1);
$limit = $this->request->param('limit/d', 10);
$keyword = $this->request->param('keyword');
$wechatStatus = $this->request->param('wechatStatus');
// 创建缓存键(基于用户、分页、搜索条件和在线状态筛选)
$cacheKey = 'wechat_list_' . $this->getUserInfo('id') . '_' . $page . '_' . $limit . '_' . md5($keyword ?? '') . '_' . ($wechatStatus ?? 'all');
// 尝试从缓存获取数据缓存2分钟
$cachedData = cache($cacheKey);
if ($cachedData) {
return ResponseHelper::success($cachedData);
}
// 如果没有缓存,执行查询
$result = $this->getOnlineWechatList(
$this->makeWhere()
);
$responseData = [
'list' => $this->makeResultedSet($result),
'total' => $result->total(),
];
// 存入缓存有效期2分钟
cache($cacheKey, $responseData, 120);
return ResponseHelper::success(
[
'list' => $this->makeResultedSet($result),
'total' => $result->total(),
]
);
return ResponseHelper::success($responseData);
} catch (\Exception $e) {
return ResponseHelper::error($e->getMessage(), $e->getCode());
}

View File

@@ -74,7 +74,7 @@ class PostTransferFriends extends BaseController
$taskId = Db::name('customer_acquisition_task')->insertGetId([
'name' => '迁移好友('. $wechat['nickname'] .'',
'sceneId' => 1,
'sceneId' => 10,
'sceneConf' => json_encode($sceneConf),
'reqConf' => json_encode($reqConf),
'tagConf' => json_encode([]),

View File

@@ -0,0 +1,49 @@
<?php
namespace app\http\middleware;
use app\common\util\JwtUtil;
use think\facade\Log;
/**
* JWT认证中间件
*/
class Jwt
{
/**
* 处理请求
* @param \think\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, \Closure $next)
{
// 获取Token
$token = JwtUtil::getRequestToken();
// 验证Token
if (!$token) {
return json([
'code' => 401,
'msg' => '未授权访问,缺少有效的身份凭证',
'data' => null
])->header(['Content-Type' => 'application/json; charset=utf-8']);
}
$payload = JwtUtil::verifyToken($token);
if (!$payload) {
return json([
'code' => 401,
'msg' => '授权已过期或无效',
'data' => null
])->header(['Content-Type' => 'application/json; charset=utf-8']);
}
// 将用户信息附加到请求中
$request->userInfo = $payload;
// 写入日志
Log::info('JWT认证通过', ['user_id' => $payload['id'] ?? 0, 'username' => $payload['username'] ?? '']);
return $next($request);
}
}

View File

@@ -1,33 +1,72 @@
{
"name": "topthink/think",
"description": "the new thinkphp framework",
"type": "project",
"keywords": [
"framework",
"thinkphp",
"ORM"
],
"homepage": "http://thinkphp.cn/",
"license": "Apache-2.0",
"authors": [
{
"name": "liu21st",
"email": "liu21st@gmail.com"
}
],
"require": {
"php": ">=5.4.0",
"topthink/framework": "5.0.*"
"name": "topthink/think",
"description": "the new thinkphp framework",
"type": "project",
"keywords": [
"framework",
"thinkphp",
"ORM"
],
"homepage": "http://thinkphp.cn/",
"license": "Apache-2.0",
"authors": [
{
"name": "liu21st",
"email": "liu21st@gmail.com"
},
"autoload": {
"psr-4": {
"app\\": "application"
}
},
"extra": {
"think-path": "thinkphp"
},
"config": {
"preferred-install": "dist"
{
"name": "yunwuxin",
"email": "448901948@qq.com"
}
],
"require": {
"php": ">=5.6.0",
"topthink/framework": "5.1.41",
"topthink/think-installer": "2.*",
"topthink/think-captcha": "^2.0",
"topthink/think-helper": "^3.0",
"topthink/think-image": "^1.0",
"topthink/think-queue": "^2.0",
"topthink/think-worker": "^2.0",
"textalk/websocket": "^1.5",
"aliyuncs/oss-sdk-php": "^2.6",
"monolog/monolog": "^1.27",
"guzzlehttp/guzzle": "^6.5",
"overtrue/wechat": "~4.6",
"endroid/qr-code": "^3.9",
"phpoffice/phpspreadsheet": "^1.29",
"workerman/workerman": "^3.5",
"workerman/gateway-worker": "^3.0",
"hashids/hashids": "^2.0",
"khanamiryan/qrcode-detector-decoder": "^1.0",
"lizhichao/word": "^2.0",
"adbario/php-dot-notation": "^2.2"
},
"require-dev": {
"symfony/var-dumper": "^3.4|^4.4",
"topthink/think-migration": "^2.0",
"phpunit/phpunit": "^5.0|^6.0"
},
"autoload": {
"psr-4": {
"app\\": "application",
"Eison\\": "extend/Eison"
},
"files": [
"application/common.php"
],
"classmap": []
},
"extra": {
"think-path": "thinkphp"
},
"config": {
"preferred-install": "dist",
"allow-plugins": {
"topthink/think-installer": true,
"easywechat-composer/easywechat-composer": true
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@@ -23,7 +23,8 @@ return [
// 全局中间件
'alias' => [
'cors' => 'app\\common\\middleware\\AllowCrossDomain'
'cors' => 'app\\common\\middleware\\AllowCrossDomain',
'jwt' => 'app\\http\\middleware\\Jwt'
],
// 应用中间件

View File

@@ -81,6 +81,8 @@
# 消息提醒
*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think kf:notice >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/kf_notice.log 2>&1
# 客服评分
0 2 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechat:calculate-score >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/calculate_score.log 2>&1
@@ -107,4 +109,10 @@
```bash
crontab -l
```
```bash
- 本地: php think worker:server
- 线上: php think worker:server -d (自带守护进程无需搭配Supervisor 之类的工具)
- php think worker:server stop php think worker:server status
```

View File

@@ -0,0 +1,125 @@
<?php
namespace Eison\Utils\Helper;
/**
* 数组辅助类
*/
class ArrHelper
{
/**
* 从数组中提取指定的键值
*
* @param string $keys 键名多个用逗号分隔支持键名映射account=userName
* @param array $array 源数组
* @param mixed $default 默认值,如果键不存在时返回此值
* @return array
*/
public static function getValue(string $keys, array $array, $default = null): array
{
$result = [];
$keyList = explode(',', $keys);
foreach ($keyList as $key) {
$key = trim($key);
// 支持键名映射account=userName
if (strpos($key, '=') !== false) {
list($sourceKey, $targetKey) = explode('=', $key, 2);
$sourceKey = trim($sourceKey);
$targetKey = trim($targetKey);
// 如果源键存在,使用源键的值;否则使用目标键的值;都不存在则使用默认值
if (isset($array[$sourceKey])) {
$result[$targetKey] = $array[$sourceKey];
} elseif (isset($array[$targetKey])) {
$result[$targetKey] = $array[$targetKey];
} else {
// 如果提供了默认值,使用默认值;否则不添加该键
if ($default !== null) {
$result[$targetKey] = $default;
}
}
} else {
// 普通键名
if (isset($array[$key])) {
$result[$key] = $array[$key];
} else {
// 如果提供了默认值,使用默认值;否则不添加该键
if ($default !== null) {
$result[$key] = $default;
}
}
}
}
return $result;
}
/**
* 移除数组中的空值null、空字符串、空数组
*
* @param array $array 源数组
* @return array
*/
public static function rmValue(array $array): array
{
return array_filter($array, function($value) {
if (is_array($value)) {
return !empty($value);
}
return $value !== null && $value !== '';
});
}
/**
* 左连接两个数组
*
* @param array $leftArray 左数组
* @param array $rightArray 右数组
* @param string $key 关联键名
* @return array
*/
public static function leftJoin(array $leftArray, array $rightArray, string $key): array
{
// 将右数组按关联键索引
$rightIndexed = [];
foreach ($rightArray as $item) {
if (isset($item[$key])) {
$rightIndexed[$item[$key]] = $item;
}
}
// 左连接
$result = [];
foreach ($leftArray as $leftItem) {
$leftKeyValue = $leftItem[$key] ?? null;
if ($leftKeyValue !== null && isset($rightIndexed[$leftKeyValue])) {
$result[] = array_merge($leftItem, $rightIndexed[$leftKeyValue]);
} else {
$result[] = $leftItem;
}
}
return $result;
}
/**
* 将数组的某一列作为键,重新组织数组
*
* @param string $key 作为键的列名
* @param array $array 源数组
* @return array
*/
public static function columnTokey(string $key, array $array): array
{
$result = [];
foreach ($array as $item) {
if (isset($item[$key])) {
$result[$item[$key]] = $item;
}
}
return $result;
}
}

View File

@@ -18,6 +18,7 @@ use think\facade\Config;
use think\facade\Log;
use app\api\controller\FriendTaskController;
use app\common\service\AuthService;
use app\common\service\WechatAccountHealthScoreService;
use app\api\controller\WebSocketController;
use Workerman\Lib\Timer;
@@ -180,13 +181,11 @@ class Adapter implements WeChatServiceInterface
->select();
$taskData = array_merge($taskData, $tasks);
}
if ($taskData) {
foreach ($taskData as $task) {
$task_id = $task['task_id'];
$task_info = $this->getCustomerAcquisitionTask($task_id);
if (empty($task_info['status']) || empty($task_info['reqConf']) || empty($task_info['reqConf']['device'])) {
continue;
}
@@ -213,9 +212,86 @@ class Adapter implements WeChatServiceInterface
continue;
}
// 判断24h内加的好友数量friend_task 先固定10个人 getLast24hAddedFriendsCount
// 根据健康分判断24h内加的好友数量限制
$healthScoreService = new WechatAccountHealthScoreService();
$healthScoreInfo = $healthScoreService->getHealthScore($accountId);
// 如果健康分记录不存在,先计算一次
if (empty($healthScoreInfo)) {
try {
$healthScoreService->calculateAndUpdate($accountId);
$healthScoreInfo = $healthScoreService->getHealthScore($accountId);
} catch (\Exception $e) {
Log::error("计算健康分失败 (accountId: {$accountId}): " . $e->getMessage());
// 如果计算失败使用默认值5作为兜底
$maxAddFriendPerDay = 5;
}
}
// 获取每日最大加人次数(基于健康分)
$maxAddFriendPerDay = $healthScoreInfo['maxAddFriendPerDay'] ?? 5;
// 如果健康分为0或很低不允许添加好友
if ($maxAddFriendPerDay <= 0) {
Log::info("账号健康分过低,不允许添加好友 (accountId: {$accountId}, wechatId: {$wechatId}, healthScore: " . ($healthScoreInfo['healthScore'] ?? 0) . ")");
continue;
}
// 检查频繁暂停限制首次频繁或再次频繁暂停24小时
$lastFrequentTime = $healthScoreInfo['lastFrequentTime'] ?? null;
$frequentCount = $healthScoreInfo['frequentCount'] ?? 0;
if (!empty($lastFrequentTime) && $frequentCount > 0) {
$frequentPauseHours = 24; // 频繁暂停24小时
$frequentPauseTime = $lastFrequentTime + ($frequentPauseHours * 3600);
$currentTime = time();
if ($currentTime < $frequentPauseTime) {
$remainingHours = ceil(($frequentPauseTime - $currentTime) / 3600);
Log::info("账号频繁,暂停添加好友 (accountId: {$accountId}, wechatId: {$wechatId}, frequentCount: {$frequentCount}, 剩余暂停时间: {$remainingHours}小时)");
continue;
}
}
// 检查封号暂停限制封号暂停72小时
$isBanned = $healthScoreInfo['isBanned'] ?? 0;
if ($isBanned == 1) {
// 查询封号时间从s2_wechat_message表查询最近一次封号消息
$banMessage = Db::table('s2_wechat_message')
->where('wechatAccountId', $accountId)
->where('msgType', 10000)
->where('content', 'like', '%你的账号被限制%')
->where('isDeleted', 0)
->order('createTime', 'desc')
->find();
if (!empty($banMessage)) {
$banTime = $banMessage['createTime'] ?? 0;
$banPauseHours = 72; // 封号暂停72小时
$banPauseTime = $banTime + ($banPauseHours * 3600);
$currentTime = time();
if ($currentTime < $banPauseTime) {
$remainingHours = ceil(($banPauseTime - $currentTime) / 3600);
Log::info("账号封号,暂停添加好友 (accountId: {$accountId}, wechatId: {$wechatId}, 剩余暂停时间: {$remainingHours}小时)");
continue;
}
}
}
// 判断今天添加的好友数量,使用健康分计算的每日最大加人次数
// 优先使用今天添加的好友数量(更符合"每日"限制)
$todayAddedFriendsCount = $this->getTodayAddedFriendsCount($wechatId);
if ($todayAddedFriendsCount >= $maxAddFriendPerDay) {
Log::info("今天添加好友数量已达上限 (accountId: {$accountId}, wechatId: {$wechatId}, count: {$todayAddedFriendsCount}, max: {$maxAddFriendPerDay}, healthScore: " . ($healthScoreInfo['healthScore'] ?? 0) . ")");
continue;
}
// 如果今天添加数量未达上限再检查24小时内的数量作为额外保护
$last24hAddedFriendsCount = $this->getLast24hAddedFriendsCount($wechatId);
if ($last24hAddedFriendsCount >= 20) {
// 24小时内的限制可以稍微宽松一些设置为每日限制的1.2倍(防止跨天累积)
$max24hLimit = (int)ceil($maxAddFriendPerDay * 1.2);
if ($last24hAddedFriendsCount >= $max24hLimit) {
Log::info("24小时内添加好友数量已达上限 (accountId: {$accountId}, wechatId: {$wechatId}, count: {$last24hAddedFriendsCount}, max24h: {$max24hLimit}, maxDaily: {$maxAddFriendPerDay})");
continue;
}
@@ -231,7 +307,7 @@ class Adapter implements WeChatServiceInterface
$conf = array_merge($task_info['reqConf'], ['task_name' => $task_info['name'], 'tags' => $tags]);
$this->createFriendAddTask($accountId, $task['phone'], $conf);
$this->createFriendAddTask($accountId, $task['phone'], $conf, $task['remark']);
$friendAddTaskCreated = true;
$task['processed_wechat_ids'] = $task['processed_wechat_ids'] . ',' . $wechatId; // 处理失败任务用,用于过滤已处理的微信号
break;
@@ -828,6 +904,7 @@ class Adapter implements WeChatServiceInterface
if (empty($deviceIds)) {
return [];
}
$records = Db::table('s2_wechat_account')
->where('deviceAlive', 1)
->where('wechatAlive', 1)
@@ -874,28 +951,29 @@ class Adapter implements WeChatServiceInterface
}
// 创建添加好友任务/执行添加
public function createFriendAddTask(int $wechatAccountId, string $phone, array $conf)
public function createFriendAddTask(int $wechatAccountId, string $phone, array $conf, $remark = '')
{
if (empty($wechatAccountId) || empty($phone) || empty($conf)) {
return;
}
switch ($conf['remarkType']) {
case 'phone':
$remark = $phone . '-' . $conf['task_name'];
break;
case 'nickname':
$remark = '';
break;
case 'source':
$remark = $conf['task_name'];
break;
default:
$remark = '';
break;
if (empty($remark)){
switch ($conf['remarkType']) {
case 'phone':
$remark = $phone . '-' . $conf['task_name'];
break;
case 'nickname':
$remark = '';
break;
case 'source':
$remark = $conf['task_name'];
break;
default:
$remark = '';
break;
}
}
$tags = [];
if (!empty($conf['tags'])) {
if (is_array($conf['tags'])) {

2392
Server/sql.sql Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,58 @@
# 微信健康分规则 v2
## 一、定义
当客户收到手机设备后,登录了微信号,我们将对其微信号进行健康分的评估。
**健康分 = 基础分 + 动态分**
健康分只与系统中的"每日自动添加好友次数"这个功能相关联。\
通过健康分体系来定义一个微信号每日**最佳、最稳定的添加次数**。\
后期还可将健康分作为标签属性,用于快速筛选微信号。
**公式:每日最大加人次数 = 健康分 × 0.2**
## 二、基础分
基础分为 **60--100 分**
`60 + 40基础加成分` 四个维度参数组成,每个参数具有不同权重。
### 基础分组成
类型 权重 分数
------------ ------ ------
基础信息 0.2 10
好友数量 0.3 30
默认基础分 --- 60
### 1. 基础信息(权重 0.2,满分 10
类型 权重 分数
-------------- ------ ------
已修改微信号 1 10
### 2. 好友数量(权重 0.3,满分 30
好友数量范围 权重 分数
-------------- ------ ------
0--50 0.1 3
51--500 0.2 6
501--3000 0.3 8
3001 以上 0.4 12
## 三、动态分规则
### 扣分规则
场景 扣分 处罚
---------- ------ --------------
首次频繁 15 暂停 24 小时
再次频繁 25 暂停 24 小时
封号 60 暂停 72 小时
### 加分规则
场景 加分
--------------------- ------
连续 3 天不触发频繁 5/日

View File

@@ -11,6 +11,10 @@
</style>
<!-- 引入 uni-app web-view SDK必须 -->
<script type="text/javascript" src="/websdk.js"></script>
<script
charset="utf-8"
src="https://map.qq.com/api/gljs?v=1.exp&libraries=service&key=7DZBZ-ZSRK3-QJN3W-O5VTV-4E2P6-7GFYX"
></script>
</head>
<body>
<div id="root"></div>

View File

@@ -18,11 +18,11 @@ export interface DataProcessingParams {
/**
* 群消息
*/
chatroomMessage?: string[];
chatroomMessage?: any[];
/**
* 个人信息
*/
friendMessage?: string[];
friendMessage?: any[];
/**
* 类型固定值
*/
@@ -30,7 +30,7 @@ export interface DataProcessingParams {
/**
* 公共
*/
wechatAccountId?: string;
wechatAccountId?: number;
[property: string]: any;
}

View File

@@ -28,10 +28,20 @@ instance.interceptors.request.use((config: any) => {
instance.interceptors.response.use(
(res: AxiosResponse) => {
const { code, success, msg } = res.data || {};
if (code === 200 || success) {
return res.data.data ?? res.data;
const payload = res.data || {};
const { code, success, msg } = payload;
const hasBizCode = typeof code === "number";
const hasBizSuccess = typeof success === "boolean";
const bizSuccess = hasBizCode
? code === 200
: hasBizSuccess
? success
: undefined;
if (bizSuccess === true || (!hasBizCode && !hasBizSuccess)) {
return payload.data ?? payload;
}
Toast.show({ content: msg || "接口错误", position: "top" });
if (code === 401) {
localStorage.removeItem("token");

View File

@@ -0,0 +1,431 @@
import React, { useEffect, useState } from "react";
import { Drawer, Avatar, Space, Button, Badge, Empty, Tabs, Tag } from "antd";
import { BellOutlined } from "@ant-design/icons";
import {
noticeList,
readMessage,
readAll,
friendRequestList as fetchFriendRequestListApi,
} from "./api";
import styles from "./index.module.scss";
interface MessageItem {
id: number;
type: number;
companyId: number;
userId: number;
bindId: number;
title: string;
message: string;
isRead: number;
createTime: string;
readTime: string;
friendData: {
nickname: string;
avatar: string;
};
}
interface FriendRequestItem {
taskId: number;
phone: string;
wechatId: string;
adder?: {
avatar?: string;
nickname?: string;
username?: string;
accountNickname?: string;
accountRealName?: string;
};
status?: {
code?: number;
text?: string;
};
time?: {
addTime?: string;
addTimeStamp?: number;
updateTime?: string;
updateTimeStamp?: number;
passTime?: string;
passTimeStamp?: number;
};
friend?: {
nickname?: string;
isPassed?: boolean;
};
other?: {
msgContent?: string;
remark?: string;
from?: string;
labels?: string[];
};
}
const DEFAULT_QUERY = { page: 1, limit: 20 };
const Notice: React.FC = () => {
const [messageDrawerVisible, setMessageDrawerVisible] = useState(false);
const [activeTab, setActiveTab] = useState("messages");
const [messageList, setMessageList] = useState<MessageItem[]>([]);
const [messageCount, setMessageCount] = useState(0);
const [loading, setLoading] = useState(false);
const [friendRequestList, setFriendRequestList] = useState<
FriendRequestItem[]
>([]);
const [friendRequestLoading, setFriendRequestLoading] = useState(false);
const fetchMessageList = async () => {
try {
setLoading(true);
const response = await noticeList(DEFAULT_QUERY);
if (response?.list) {
setMessageList(response.list);
const unreadCount = response.list.filter(
(item: MessageItem) => item.isRead === 0,
).length;
setMessageCount(unreadCount);
}
} catch (error) {
console.error("获取消息列表失败:", error);
} finally {
setLoading(false);
}
};
const refreshUnreadCount = async () => {
try {
const response = await noticeList(DEFAULT_QUERY);
if (response && typeof response.noRead === "number") {
setMessageCount(response.noRead);
}
} catch (error) {
console.error("获取未读消息数失败:", error);
}
};
useEffect(() => {
fetchMessageList();
const timer = window.setInterval(refreshUnreadCount, 30 * 1000);
return () => {
window.clearInterval(timer);
};
}, []);
const handleMessageClick = () => {
setMessageDrawerVisible(true);
fetchMessageList();
fetchFriendRequestList();
};
const handleTabChange = (key: string) => {
setActiveTab(key);
if (key === "friendRequests") {
fetchFriendRequestList();
}
};
const handleMessageDrawerClose = () => {
setMessageDrawerVisible(false);
};
const handleReadMessage = async (messageId: number) => {
try {
await readMessage({ id: messageId });
setMessageList(prev => {
const updated = prev.map(item =>
item.id === messageId ? { ...item, isRead: 1 } : item,
);
const unreadCount = updated.filter(item => item.isRead === 0).length;
setMessageCount(unreadCount);
return updated;
});
} catch (error) {
console.error("标记消息已读失败:", error);
}
};
const handleReadAll = async () => {
try {
await readAll();
setMessageList(prev => prev.map(item => ({ ...item, isRead: 1 })));
setMessageCount(0);
} catch (error) {
console.error("全部已读失败:", error);
}
};
const fetchFriendRequestList = async () => {
try {
setFriendRequestLoading(true);
const response = await fetchFriendRequestListApi(DEFAULT_QUERY);
if (response?.list) {
setFriendRequestList(response.list);
}
} catch (error) {
console.error("获取好友添加记录失败:", error);
} finally {
setFriendRequestLoading(false);
}
};
const formatTime = (timeStr?: string) => {
if (!timeStr) {
return "-";
}
const date = new Date(timeStr);
const now = new Date();
const diff = now.getTime() - date.getTime();
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (days === 0) {
return date.toLocaleTimeString("zh-CN", {
hour: "2-digit",
minute: "2-digit",
});
} else if (days === 1) {
return "昨天";
} else if (days < 7) {
return `${days}天前`;
} else {
return date.toLocaleDateString("zh-CN", {
month: "2-digit",
day: "2-digit",
});
}
};
const getStatusText = (statusCode?: number, statusText?: string) => {
if (statusText) {
return statusText;
}
switch (statusCode) {
case 0:
return "待处理";
case 1:
return "已同意";
case 2:
return "已拒绝";
default:
return "未知";
}
};
const getStatusColor = (statusCode?: number) => {
switch (statusCode) {
case 0:
return "#1890ff";
case 1:
return "#52c41a";
case 2:
return "#ff4d4f";
default:
return "#999";
}
};
const getFriendRequestKey = (item: FriendRequestItem) => {
return (
item.taskId?.toString() ||
item.wechatId ||
item.phone ||
`${item.adder?.username || "unknown"}-${item.time?.addTime || "time"}`
);
};
const getAddedUserName = (item: FriendRequestItem) => {
return (
item.friend?.nickname ||
item.phone ||
item.wechatId ||
item.adder?.nickname ||
"未知好友"
);
};
const getAdderName = (item: FriendRequestItem) => {
return (
item.adder?.nickname ||
item.adder?.username ||
item.adder?.accountNickname ||
item.adder?.accountRealName ||
"未知添加人"
);
};
return (
<>
<div className={styles.messageButton} onClick={handleMessageClick}>
<Badge count={messageCount} size="small">
<BellOutlined style={{ fontSize: 20 }} />
</Badge>
</div>
<Drawer
title="通知中心"
placement="right"
onClose={handleMessageDrawerClose}
open={messageDrawerVisible}
width={400}
className={styles.messageDrawer}
extra={
activeTab === "messages" && (
<Space>
<Button type="text" size="small" onClick={handleReadAll}>
</Button>
</Space>
)
}
>
<div style={{ padding: "0 20px" }}>
<Tabs
activeKey={activeTab}
onChange={handleTabChange}
items={[
{
key: "messages",
label: "消息列表",
children: (
<div className={styles.messageContent}>
{loading ? (
<div style={{ textAlign: "center", padding: "20px" }}>
...
</div>
) : messageList.length === 0 ? (
<Empty description="暂无消息" />
) : (
messageList.map(item => (
<div
key={item.id}
className={`${styles.messageItem} ${
item.isRead === 0 ? styles.unread : ""
}`}
onClick={() => handleReadMessage(item.id)}
>
<div className={styles.messageAvatar}>
<Avatar
size={40}
src={item.friendData?.avatar}
style={{ backgroundColor: "#87d068" }}
>
{item.friendData?.nickname?.charAt(0) || "U"}
</Avatar>
</div>
<div className={styles.messageInfo}>
<div className={styles.messageTitle}>
<span className={styles.messageType}>
{item.title}
</span>
{item.isRead === 0 && (
<div className={styles.messageStatus}></div>
)}
</div>
<div className={styles.messageText}>
{item.message}
</div>
{item.isRead === 0 && (
<div className={styles.messageTime}>
{formatTime(item.createTime)}
<Button
type="link"
size="small"
onClick={event => {
event.stopPropagation();
handleReadMessage(item.id);
}}
>
</Button>
</div>
)}
</div>
</div>
))
)}
</div>
),
},
{
key: "friendRequests",
label: "好友添加记录",
children: (
<div className={styles.messageContent}>
{friendRequestLoading ? (
<div style={{ textAlign: "center", padding: "20px" }}>
...
</div>
) : friendRequestList.length === 0 ? (
<Empty description="暂无好友添加记录" />
) : (
friendRequestList.map(item => (
<div
key={getFriendRequestKey(item)}
className={styles.messageItem}
>
<div className={styles.messageAvatar}>
<Avatar
size={40}
src={item.adder?.avatar}
style={{ backgroundColor: "#87d068" }}
>
{item.adder?.nickname?.charAt(0) || "U"}
</Avatar>
</div>
<div className={styles.messageInfo}>
<div className={styles.messageTitle}>
<span className={styles.messageType}>
<Tag color="blue">{getAddedUserName(item)}</Tag>
</span>
<span
style={{
fontSize: "12px",
color: getStatusColor(item.status?.code),
fontWeight: 500,
}}
>
{getStatusText(
item.status?.code,
item.status?.text,
)}
</span>
</div>
<div
className={styles.messageText}
style={{ color: "#595959" }}
>
{getAdderName(item)}
</div>
<div className={styles.messageText}>
{item.other?.msgContent || "无"}
</div>
<div
className={styles.messageText}
style={{ color: "#999", fontSize: 12 }}
>
{item.other?.remark && (
<Tag color="orange" style={{ marginTop: 4 }}>
{item.other.remark}
</Tag>
)}
</div>
<div className={styles.messageTime}>
{formatTime(item.time?.addTime)}
</div>
</div>
</div>
))
)}
</div>
),
},
]}
/>
</div>
</Drawer>
</>
);
};
export default Notice;

View File

@@ -14,3 +14,8 @@ export const readMessage = (params: { id: number }) => {
export const readAll = () => {
return request(`/v1/kefu/notice/readAll`, undefined, "PUT");
};
// 好友添加任务列表
export const friendRequestList = (params: { page: number; limit: number }) => {
return request(`/v1/kefu/wechatFriend/addTaskList`, params, "GET");
};

View File

@@ -1,28 +1,18 @@
import React, { useState, useEffect } from "react";
import {
Layout,
Drawer,
Avatar,
Space,
Button,
Badge,
Dropdown,
Empty,
} from "antd";
import React, { useState } from "react";
import { Layout, Avatar, Space, Button, Dropdown, message } from "antd";
import {
BarChartOutlined,
UserOutlined,
BellOutlined,
LogoutOutlined,
ThunderboltOutlined,
SettingOutlined,
CalendarOutlined,
RetweetOutlined,
SendOutlined,
ClearOutlined,
} from "@ant-design/icons";
import { noticeList, readMessage, readAll } from "./api";
import { useUserStore } from "@/store/module/user";
import { useNavigate, useLocation } from "react-router-dom";
import styles from "./index.module.scss";
import Notice from "./Notice";
const { Header } = Layout;
@@ -31,39 +21,12 @@ interface NavCommonProps {
onMenuClick?: () => void;
}
// 消息数据类型
interface MessageItem {
id: number;
type: number;
companyId: number;
userId: number;
bindId: number;
title: string;
message: string;
isRead: number;
createTime: string;
readTime: string;
friendData: {
nickname: string;
avatar: string;
};
}
const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
const [messageDrawerVisible, setMessageDrawerVisible] = useState(false);
const [messageList, setMessageList] = useState<MessageItem[]>([]);
const [messageCount, setMessageCount] = useState(0);
const [loading, setLoading] = useState(false);
const [clearingCache, setClearingCache] = useState(false);
const navigate = useNavigate();
const location = useLocation();
const { user, logout } = useUserStore();
// 初始化时获取消息列表
useEffect(() => {
fetchMessageList();
setInterval(IntervalMessageCount, 30 * 1000);
}, []);
// 处理菜单图标点击:在两个路由之间切换
const handleMenuClick = () => {
if (!location.pathname.startsWith("/pc/powerCenter")) {
@@ -72,111 +35,102 @@ const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
navigate("/pc/weChat");
}
};
const isWeChat = () => {
return location.pathname.startsWith("/pc/weChat");
};
// 定时器获取消息条数
const IntervalMessageCount = async () => {
try {
const response = await noticeList({ page: 1, limit: 20 });
if (response && response.noRead) {
setMessageCount(response.noRead);
}
} catch (error) {
console.error("获取消息列表失败:", error);
}
};
// 获取消息列表
const fetchMessageList = async () => {
try {
setLoading(true);
const response = await noticeList({ page: 1, limit: 20 });
if (response && response.list) {
setMessageList(response.list);
// 计算未读消息数量
const unreadCount = response.list.filter(
(item: MessageItem) => item.isRead === 0,
).length;
setMessageCount(unreadCount);
}
} catch (error) {
console.error("获取消息列表失败:", error);
} finally {
setLoading(false);
}
};
// 处理消息中心点击
const handleMessageClick = () => {
setMessageDrawerVisible(true);
fetchMessageList();
};
// 处理消息抽屉关闭
const handleMessageDrawerClose = () => {
setMessageDrawerVisible(false);
};
// 处理退出登录
const handleLogout = () => {
logout(); // 清除localStorage中的token和用户状态
navigate("/login"); // 跳转到登录页面
};
// 处理消息已读
const handleReadMessage = async (messageId: number) => {
try {
await readMessage({ id: messageId }); // 这里需要根据实际API调整参数
// 更新本地状态
setMessageList(prev =>
prev.map(item =>
item.id === messageId ? { ...item, isRead: 1 } : item,
),
);
// 重新计算未读数量
const unreadCount =
messageList.filter(item => item.isRead === 0).length - 1;
setMessageCount(Math.max(0, unreadCount));
} catch (error) {
console.error("标记消息已读失败:", error);
}
// 清除所有 IndexedDB 数据库
const clearAllIndexedDB = async (): Promise<void> => {
return new Promise((resolve, reject) => {
if (!window.indexedDB) {
resolve();
return;
}
// 获取所有数据库名称
const databases: string[] = [];
const request = indexedDB.databases();
request
.then(dbs => {
dbs.forEach(db => {
if (db.name) {
databases.push(db.name);
}
});
// 删除所有数据库
const deletePromises = databases.map(dbName => {
return new Promise<void>((resolveDelete, rejectDelete) => {
const deleteRequest = indexedDB.deleteDatabase(dbName);
deleteRequest.onsuccess = () => {
resolveDelete();
};
deleteRequest.onerror = () => {
rejectDelete(new Error(`删除数据库 ${dbName} 失败`));
};
deleteRequest.onblocked = () => {
// 如果数据库被阻塞,等待一下再重试
setTimeout(() => {
const retryRequest = indexedDB.deleteDatabase(dbName);
retryRequest.onsuccess = () => resolveDelete();
retryRequest.onerror = () =>
rejectDelete(new Error(`删除数据库 ${dbName} 失败`));
}, 100);
};
});
});
Promise.all(deletePromises)
.then(() => resolve())
.catch(reject);
})
.catch(reject);
});
};
// 处理全部已读
const handleReadAll = async () => {
// 处理清除缓存
const handleClearCache = async () => {
try {
await readAll(); // 这里需要根据实际API调整参数
// 更新本地状态
setMessageList(prev => prev.map(item => ({ ...item, isRead: 1 })));
setMessageCount(0);
setClearingCache(true);
const hideLoading = message.loading("正在清除缓存...", 0);
// 1. 清除所有 localStorage
try {
localStorage.clear();
} catch (error) {
console.warn("清除 localStorage 失败:", error);
}
// 2. 清除所有 sessionStorage
try {
sessionStorage.clear();
} catch (error) {
console.warn("清除 sessionStorage 失败:", error);
}
// 3. 清除所有 IndexedDB 数据库
try {
await clearAllIndexedDB();
} catch (error) {
console.warn("清除 IndexedDB 失败:", error);
}
hideLoading();
message.success("缓存清除成功");
// 清除成功后跳转到登录页面
setTimeout(() => {
logout();
navigate("/login");
}, 500);
} catch (error) {
console.error("全部已读失败:", error);
}
};
// 格式化时间
const formatTime = (timeStr: string) => {
const date = new Date(timeStr);
const now = new Date();
const diff = now.getTime() - date.getTime();
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (days === 0) {
return date.toLocaleTimeString("zh-CN", {
hour: "2-digit",
minute: "2-digit",
});
} else if (days === 1) {
return "昨天";
} else if (days < 7) {
return `${days}天前`;
} else {
return date.toLocaleDateString("zh-CN", {
month: "2-digit",
day: "2-digit",
});
console.error("清除缓存失败:", error);
message.error("清除缓存失败,请稍后重试");
} finally {
setClearingCache(false);
}
};
@@ -199,6 +153,13 @@ const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
navigate("/pc/commonConfig");
},
},
{
key: "clearCache",
icon: <ClearOutlined style={{ fontSize: 16 }} />,
label: clearingCache ? "清除缓存中..." : "清除缓存",
onClick: handleClearCache,
disabled: clearingCache,
},
{
key: "logout",
icon: <LogoutOutlined style={{ fontSize: 14 }} />,
@@ -220,10 +181,10 @@ const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
></Button>
<Button
icon={<CalendarOutlined />}
icon={<SendOutlined />}
onClick={handleContentManagementClick}
>
</Button>
<span className={styles.title}>{title}</span>
</div>
@@ -236,11 +197,7 @@ const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
</span>
{user?.tokens}
</span>
<div className={styles.messageButton} onClick={handleMessageClick}>
<Badge count={messageCount} size="small">
<BellOutlined style={{ fontSize: 20 }} />
</Badge>
</div>
<Notice />
<Dropdown
menu={{ items: userMenuItems }}
@@ -264,69 +221,6 @@ const NavCommon: React.FC<NavCommonProps> = ({ title = "触客宝" }) => {
</Space>
</div>
</Header>
<Drawer
title="通知中心"
placement="right"
onClose={handleMessageDrawerClose}
open={messageDrawerVisible}
width={400}
className={styles.messageDrawer}
extra={
<Space>
<Button type="text" size="small" onClick={handleReadAll}>
</Button>
</Space>
}
>
<div className={styles.messageContent}>
{loading ? (
<div style={{ textAlign: "center", padding: "20px" }}>
...
</div>
) : messageList.length === 0 ? (
<Empty description="暂无消息" />
) : (
messageList.map(item => (
<div
key={item.id}
className={`${styles.messageItem} ${
item.isRead === 0 ? styles.unread : ""
}`}
onClick={() => handleReadMessage(item.id)}
>
<div className={styles.messageAvatar}>
<Avatar
size={40}
src={item.friendData?.avatar}
style={{ backgroundColor: "#87d068" }}
>
{item.friendData?.nickname?.charAt(0) || "U"}
</Avatar>
</div>
<div className={styles.messageInfo}>
<div className={styles.messageTitle}>
<span className={styles.messageType}>{item.title}</span>
{item.isRead === 0 && (
<div className={styles.messageStatus}></div>
)}
</div>
<div className={styles.messageText}>{item.message}</div>
{item.isRead === 0 && (
<div className={styles.messageTime}>
{formatTime(item.createTime)}
<Button type="link" size="small">
</Button>
</div>
)}
</div>
</div>
))
)}
</div>
</Drawer>
</>
);
};

View File

@@ -20,7 +20,7 @@ const ContentManagement: React.FC = () => {
return (
<div className={styles.container}>
<PowerNavigation
title="内容管理"
title="发朋友圈"
subtitle="可以讲聊天过程的信息收录到素材库中,也调用。"
showBackButton={true}
backButtonText="返回功能中心"

View File

@@ -1,4 +1,9 @@
import { TeamOutlined, CommentOutlined, BookOutlined, SendOutlined } from "@ant-design/icons";
import {
TeamOutlined,
CommentOutlined,
BookOutlined,
SendOutlined,
} from "@ant-design/icons";
// 数据类型定义
export interface FeatureCard {
@@ -56,21 +61,21 @@ export const featureCategories: FeatureCard[] = [
],
path: "/pc/powerCenter/ai-reception",
},
{
id: "content-library",
title: "AI内容库配置",
description: "管理AI内容库,配置调用权限,优化AI推送效果和内容质量",
icon: <BookOutlined style={{ fontSize: "32px", color: "#52c41a" }} />,
color: "#52c41a",
tag: "内容管理",
features: [
"多库管理与分类",
"AI调用权限配置",
"内容检索规则设置",
"手动内容上传",
],
path: "/pc/powerCenter/content-library",
},
// {
// id: "content-library",
// title: "AI内容库配置",
// description: "管理AI内容库,配置调用权限,优化AI推送效果和内容质量",
// icon: <BookOutlined style={{ fontSize: "32px", color: "#52c41a" }} />,
// color: "#52c41a",
// tag: "内容管理",
// features: [
// "多库管理与分类",
// "AI调用权限配置",
// "内容检索规则设置",
// "手动内容上传",
// ],
// path: "/pc/powerCenter/content-library",
// },
{
id: "message-push-assistant",
title: "消息推送助手",

View File

@@ -39,18 +39,6 @@ const PowerCenter: React.FC = () => {
return (
<div className={styles.powerCenter}>
{/* 页面标题区域 */}
<div className={styles.pageHeader}>
<div className={styles.titleSection}>
<div className={styles.mainTitle}>
<div className={styles.titleIcon}></div>
<h1></h1>
</div>
<p className={styles.subtitle}>
AI智能营销··
</p>
</div>
</div>
{/* KPI统计区域置顶按图展示 */}
<div className={styles.kpiSection}>
<Row gutter={16}>
@@ -157,9 +145,11 @@ const PowerCenter: React.FC = () => {
{card.features.map((feature, index) => (
<li
key={index}
style={{
"--dot-color": card.color,
} as React.CSSProperties}
style={
{
"--dot-color": card.color,
} as React.CSSProperties
}
>
{feature}
</li>
@@ -186,7 +176,7 @@ const PowerCenter: React.FC = () => {
className={styles.cardIcon}
style={{
backgroundColor: getIconBgColor(
featureCategories[3].color
featureCategories[3].color,
),
}}
>
@@ -212,9 +202,11 @@ const PowerCenter: React.FC = () => {
{featureCategories[3].features.map((feature, index) => (
<li
key={index}
style={{
"--dot-color": featureCategories[3].color,
} as React.CSSProperties}
style={
{
"--dot-color": featureCategories[3].color,
} as React.CSSProperties
}
>
{feature}
</li>

View File

@@ -1,6 +1,9 @@
import request from "@/api/request";
import type { CreatePushTaskPayload } from "./types";
// 获取客服列表
export function queryWorkbenchCreate(params) {
// 创建推送任务
export function queryWorkbenchCreate(
params: CreatePushTaskPayload,
): Promise<any> {
return request("/v1/workbench/create", params, "POST");
}

View File

@@ -0,0 +1,92 @@
.stepContent {
.step4Content {
display: flex;
flex-direction: column;
gap: 24px;
max-width: 600px;
}
.settingsPanel {
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 20px;
background: #fafafa;
.settingsTitle {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 16px;
}
.settingItem {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
.settingLabel {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 12px;
}
.settingControl {
display: flex;
align-items: center;
gap: 8px;
span {
font-size: 14px;
color: #666;
min-width: 80px;
}
}
}
}
.tagSection {
.settingLabel {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 12px;
}
}
.pushPreview {
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 20px;
background: #f0f7ff;
.previewTitle {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 12px;
}
ul {
list-style: none;
padding: 0;
margin: 0;
li {
font-size: 14px;
color: #666;
line-height: 1.8;
}
}
}
}
@media (max-width: 768px) {
.stepContent {
.step4Content {
max-width: 100%;
}
}
}

View File

@@ -0,0 +1,108 @@
"use client";
import React from "react";
import { Select, Slider } from "antd";
import styles from "./index.module.scss";
interface StepPushParamsProps {
selectedAccounts: any[];
selectedContacts: any[];
targetLabel: string;
friendInterval: [number, number];
onFriendIntervalChange: (value: [number, number]) => void;
messageInterval: [number, number];
onMessageIntervalChange: (value: [number, number]) => void;
selectedTag: string;
onSelectedTagChange: (value: string) => void;
savedScriptGroups: any[];
}
const StepPushParams: React.FC<StepPushParamsProps> = ({
selectedAccounts,
selectedContacts,
targetLabel,
friendInterval,
onFriendIntervalChange,
messageInterval,
onMessageIntervalChange,
selectedTag,
onSelectedTagChange,
savedScriptGroups,
}) => {
return (
<div className={styles.stepContent}>
<div className={styles.step4Content}>
<div className={styles.settingsPanel}>
<div className={styles.settingsTitle}></div>
<div className={styles.settingItem}>
<div className={styles.settingLabel}></div>
<div className={styles.settingControl}>
<span>()</span>
<Slider
range
min={1}
max={60}
value={friendInterval}
onChange={value =>
onFriendIntervalChange(value as [number, number])
}
style={{ flex: 1, margin: "0 16px" }}
/>
<span>
{friendInterval[0]} - {friendInterval[1]}
</span>
</div>
</div>
<div className={styles.settingItem}>
<div className={styles.settingLabel}></div>
<div className={styles.settingControl}>
<span>()</span>
<Slider
range
min={1}
max={60}
value={messageInterval}
onChange={value =>
onMessageIntervalChange(value as [number, number])
}
style={{ flex: 1, margin: "0 16px" }}
/>
<span>
{messageInterval[0]} - {messageInterval[1]}
</span>
</div>
</div>
</div>
<div className={styles.tagSection}>
<div className={styles.settingLabel}></div>
<Select
value={selectedTag}
onChange={onSelectedTagChange}
placeholder="选择标签"
style={{ width: "100%" }}
>
<Select.Option value="potential"></Select.Option>
<Select.Option value="customer"></Select.Option>
<Select.Option value="partner"></Select.Option>
</Select>
</div>
<div className={styles.pushPreview}>
<div className={styles.previewTitle}></div>
<ul>
<li>: {selectedAccounts.length}</li>
<li>
{targetLabel}: {selectedContacts.length}
</li>
<li>: {savedScriptGroups.length}</li>
<li>随机推送: </li>
<li>: ~1</li>
</ul>
</div>
</div>
</div>
);
};
export default StepPushParams;

View File

@@ -6,6 +6,7 @@ import { EmojiPicker } from "@/components/EmojiSeclection";
import { EmojiInfo } from "@/components/EmojiSeclection/wechatEmoji";
import SimpleFileUpload from "@/components/Upload/SimpleFileUpload";
import AudioRecorder from "@/components/Upload/AudioRecorder";
import type { MessageItem } from "../../../types";
import styles from "./index.module.scss";
@@ -17,6 +18,7 @@ interface InputMessageProps {
defaultValue?: string;
onContentChange?: (value: string) => void;
onSend?: (value: string) => void;
onAddMessage?: (message: MessageItem) => void; // 新增:支持添加非文本消息
clearOnSend?: boolean;
placeholder?: string;
hint?: React.ReactNode;
@@ -68,6 +70,7 @@ const InputMessage: React.FC<InputMessageProps> = ({
defaultValue = "",
onContentChange,
onSend,
onAddMessage,
clearOnSend = false,
placeholder = "输入消息...",
hint,
@@ -169,9 +172,44 @@ const InputMessage: React.FC<InputMessageProps> = ({
msgType,
content,
});
antdMessage.success("附件上传成功,可在推送时使用");
// 如果提供了 onAddMessage 回调,则添加到消息列表
if (onAddMessage) {
let messageItem: MessageItem;
if ([FileType.IMAGE].includes(fileType)) {
messageItem = {
type: "image",
content: filePath.url,
fileName: filePath.name,
};
} else if ([FileType.AUDIO].includes(fileType)) {
messageItem = {
type: "audio",
content: filePath.url,
fileName: filePath.name,
durationMs: filePath.durationMs,
};
} else if ([FileType.FILE].includes(fileType)) {
messageItem = {
type: "file",
content: filePath.url,
fileName: filePath.name,
};
} else {
// 默认作为文本处理
messageItem = {
type: "text",
content: filePath.url,
fileName: filePath.name,
};
}
onAddMessage(messageItem);
antdMessage.success("已添加消息内容");
} else {
antdMessage.success("附件上传成功,可在推送时使用");
}
},
[],
[onAddMessage],
);
const handleAudioUploaded = useCallback(

View File

@@ -19,28 +19,27 @@
.step3Content {
display: flex;
flex-direction: row;
gap: 24px;
align-items: flex-start;
.leftColumn {
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
min-width: 0;
}
.rightColumn {
width: 400px;
flex: 1;
max-width: 500px;
display: flex;
flex-direction: column;
gap: 20px;
flex-shrink: 0;
}
.previewHeader {
display: flex;
justify-content: space-between;
.previewHeaderTitle {
font-size: 16px;
font-weight: 600;
@@ -49,78 +48,178 @@
}
.messagePreview {
border: 2px dashed #52c41a;
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 15px;
padding: 16px;
background: #f5f5f5;
height: 400px;
overflow-y: auto;
.messageBubble {
min-height: 100px;
background: #fff;
border-radius: 6px;
color: #666;
.messagePlaceholder {
color: #999;
font-size: 14px;
line-height: 1.6;
.currentEditingLabel {
font-size: 14px;
color: #52c41a;
font-weight: bold;
margin-bottom: 12px;
}
.messageText {
color: #1a1a1a;
white-space: pre-wrap;
word-break: break-word;
}
.messagePlaceholder {
color: #999;
font-size: 14px;
}
.messageList {
display: flex;
flex-direction: column;
gap: 0;
}
.messageItem {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.messageText {
flex: 1;
}
.messageAction {
color: #ff4d4f;
padding: 0;
}
}
text-align: center;
padding: 40px 20px;
}
.scriptNameInput {
margin-top: 12px;
.messageList {
display: flex;
flex-direction: column;
gap: 12px;
}
.messageBubbleWrapper {
display: flex;
flex-direction: column;
}
.messageBubble {
display: flex;
gap: 10px;
align-items: flex-start;
.messageAvatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 18px;
flex-shrink: 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.messageContent {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
.messageBubbleInner {
background: #fff;
border-radius: 8px;
padding: 10px 14px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
position: relative;
max-width: 100%;
&::before {
content: "";
position: absolute;
left: -6px;
top: 12px;
width: 0;
height: 0;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-right: 6px solid #fff;
}
.messageText {
color: #1a1a1a;
white-space: pre-wrap;
word-break: break-word;
line-height: 1.6;
font-size: 14px;
}
.messageMedia {
display: flex;
flex-direction: column;
gap: 8px;
.messageMediaIcon {
font-size: 24px;
color: #1890ff;
margin-bottom: 4px;
}
.messageImage {
max-width: 100%;
max-height: 200px;
border-radius: 4px;
object-fit: contain;
background: #f5f5f5;
}
.messageFileInfo {
display: flex;
flex-direction: column;
gap: 4px;
}
.messageFileName {
color: #1a1a1a;
font-size: 14px;
font-weight: 500;
word-break: break-all;
}
.messageFileSize {
color: #999;
font-size: 12px;
}
}
}
.messageActions {
display: flex;
gap: 8px;
justify-content: flex-start;
padding-left: 4px;
opacity: 0.7;
transition: opacity 0.2s;
&:hover {
opacity: 1;
}
.aiRewriteButton {
color: #1890ff;
padding: 0;
font-size: 12px;
height: auto;
line-height: 1.5;
&:hover {
color: #40a9ff;
}
}
.messageAction {
color: #ff4d4f;
padding: 0;
font-size: 12px;
height: auto;
line-height: 1.5;
&:hover {
color: #ff7875;
}
}
}
}
}
}
.savedScriptGroups {
.contentLibrarySelector {
margin-bottom: 20px;
padding: 16px;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 8px;
.pushContentHeader {
.pushContentTitle {
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 16px;
}
}
.contentLibrarySelector {
margin-bottom: 20px;
padding: 16px;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 8px;
.contentLibraryHeader {
display: flex;
@@ -139,7 +238,9 @@
font-size: 12px;
color: #999;
}
}
.savedScriptGroups {
.scriptGroupHeaderRow {
display: flex;
align-items: center;
@@ -160,7 +261,7 @@
}
.scriptGroupList {
max-height: 260px;
max-height: calc(100vh - 400px);
overflow-y: auto;
}
@@ -241,132 +342,358 @@
}
}
.messageInputArea {
.messageInput {
margin-bottom: 12px;
}
.scriptNameInput {
margin-top: 0;
}
.attachmentButtons {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.createScriptGroupButton {
margin-top: 0;
}
.aiRewriteSection {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
gap: 12px;
.aiRewriteToggle {
display: flex;
align-items: center;
gap: 8px;
// AI改写弹窗样式
.aiRewriteModalWrap {
:global {
.ant-modal {
border-radius: 12px;
overflow: hidden;
}
.aiRewriteLabel {
font-size: 14px;
color: #1a1a1a;
}
.aiRewriteInput {
max-width: 240px;
}
.aiRewriteActions {
display: flex;
align-items: center;
gap: 8px;
}
.aiRewriteButton {
min-width: 96px;
.ant-modal-content {
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
}
}
}
.settingsPanel {
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 20px;
background: #fafafa;
.settingsTitle {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 16px;
}
.settingItem {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
.aiRewriteModal {
:global {
.ant-modal-header {
border-bottom: 1px solid #f0f0f0;
padding: 20px 24px;
background: linear-gradient(135deg, #fff 0%, #fafafa 100%);
border-radius: 12px 12px 0 0;
}
.settingLabel {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 12px;
.ant-modal-body {
padding: 24px;
background: #ffffff;
// 确保内容区域的样式能够正确应用
// 原文消息内容区域
[class*="aiRewriteModalOriginalText"] {
padding: 20px !important;
background: #f5f5f5 !important;
min-height: 80px !important;
}
// 改写提示词输入框
[class*="aiRewriteModalTextArea"] {
textarea.ant-input {
background: #f5f5f5 !important;
min-height: 80px !important;
}
}
}
.settingControl {
.ant-modal-footer {
border-top: 1px solid #f0f0f0;
padding: 16px 24px;
background: #fafafa;
display: flex;
align-items: center;
gap: 8px;
justify-content: flex-end;
gap: 12px;
border-radius: 0 0 12px 12px;
}
span {
font-size: 14px;
color: #666;
min-width: 80px;
.ant-modal-close {
color: #8c8c8c;
transition: color 0.3s;
top: 20px;
right: 24px;
&:hover {
color: #1a1a1a;
}
}
}
}
.tagSection {
.settingLabel {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 12px;
.aiRewriteModalTitle {
display: flex;
align-items: center;
gap: 8px;
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
.aiRewriteModalTitleIcon {
font-size: 20px;
}
}
.pushPreview {
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 20px;
background: #f0f7ff;
.aiRewriteModalContent {
display: flex;
flex-direction: column;
gap: 24px;
}
.previewTitle {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
margin-bottom: 12px;
.aiRewriteModalCompareSection {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
background: linear-gradient(135deg, #fafafa 0%, #f5f5f5 100%);
border-radius: 12px;
border: 1px solid #e8e8e8;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.aiRewriteModalDivider {
height: 1px;
background: linear-gradient(
90deg,
transparent 0%,
#d9d9d9 20%,
#d9d9d9 80%,
transparent 100%
);
margin: 12px 0;
position: relative;
&::before {
content: "";
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 40px;
height: 1px;
background: #1890ff;
}
ul {
list-style: none;
padding: 0;
margin: 0;
&::after {
content: "";
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: #fafafa;
padding: 0 8px;
color: #1890ff;
font-size: 12px;
}
}
li {
.aiRewriteModalSection {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 0;
}
.aiRewriteModalSectionHeader {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 4px;
.aiRewriteModalSectionIcon {
font-size: 18px;
line-height: 1;
}
.aiRewriteModalLabel {
font-size: 15px;
font-weight: 600;
color: #1a1a1a;
letter-spacing: 0.3px;
}
}
.aiRewriteModalTextArea {
:global {
textarea.ant-input {
border-radius: 8px;
border: 1px solid #d9d9d9;
transition: all 0.3s;
font-size: 14px;
color: #666;
line-height: 1.8;
padding: 12px;
background: #f5f5f5 !important;
min-height: 80px !important;
&:hover {
border-color: #40a9ff;
background: #fafafa !important;
}
&:focus {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
background: #ffffff !important;
}
}
}
}
}
@media (max-width: 1200px) {
.step3Content {
.rightColumn {
width: 350px;
.aiRewriteModalOriginalText {
padding: 20px !important;
background: #f5f5f5 !important;
border: 1px solid #d9d9d9;
border-left: 4px solid #8c8c8c;
border-radius: 8px;
font-size: 14px;
color: #333;
line-height: 1.8;
white-space: pre-wrap;
word-break: break-word;
position: relative;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
margin-top: 4px;
transition: all 0.3s ease;
min-height: 80px !important;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
border-color: #bfbfbf;
background: #fafafa !important;
}
}
.aiRewriteModalLoading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 32px 20px;
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
border: 1px solid #bae6fd;
border-radius: 8px;
gap: 12px;
.aiRewriteModalLoadingIcon {
font-size: 32px;
animation: pulse 1.5s ease-in-out infinite;
}
.aiRewriteModalLoadingText {
color: #1890ff;
font-size: 14px;
font-weight: 500;
}
}
@keyframes pulse {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.05);
}
}
.aiRewriteModalResultText {
padding: 20px;
background: #f0f9ff;
border: 1px solid #91d5ff;
border-left: 4px solid #1890ff;
border-radius: 8px;
font-size: 14px;
color: #1a1a1a;
line-height: 1.8;
white-space: pre-wrap;
word-break: break-word;
max-height: 300px;
overflow-y: auto;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15);
position: relative;
margin-top: 4px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f0f0f0;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: #bfbfbf;
border-radius: 3px;
&:hover {
background: #999;
}
}
}
.aiRewriteExecuteButton {
background-color: #ff6b35 !important;
border-color: #ff6b35 !important;
font-weight: 500;
height: 36px;
padding: 0 20px;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(255, 107, 53, 0.2);
transition: all 0.3s ease;
&:hover:not(:disabled) {
background-color: #ff5722 !important;
border-color: #ff5722 !important;
box-shadow: 0 4px 8px rgba(255, 107, 53, 0.3);
transform: translateY(-1px);
}
&:active:not(:disabled) {
background-color: #e64a19 !important;
border-color: #e64a19 !important;
transform: translateY(0);
box-shadow: 0 2px 4px rgba(255, 107, 53, 0.2);
}
&:disabled {
background-color: #d9d9d9 !important;
border-color: #d9d9d9 !important;
color: #bfbfbf !important;
box-shadow: none;
}
}
.confirmButton {
background-color: #07c160 !important;
border-color: #07c160 !important;
font-weight: 500;
height: 36px;
padding: 0 20px;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(7, 193, 96, 0.2);
transition: all 0.3s ease;
&:hover:not(:disabled) {
background-color: #06ad56 !important;
border-color: #06ad56 !important;
box-shadow: 0 4px 8px rgba(7, 193, 96, 0.3);
transform: translateY(-1px);
}
&:active:not(:disabled) {
background-color: #059c4d !important;
border-color: #059c4d !important;
transform: translateY(0);
box-shadow: 0 2px 4px rgba(7, 193, 96, 0.2);
}
&:disabled {
background-color: #d9d9d9 !important;
border-color: #d9d9d9 !important;
color: #bfbfbf !important;
box-shadow: none;
}
}
}

View File

@@ -5,8 +5,7 @@ import {
Button,
Checkbox,
Input,
Select,
Slider,
Modal,
Switch,
message as antdMessage,
} from "antd";
@@ -15,11 +14,15 @@ import {
DeleteOutlined,
PlusOutlined,
ReloadOutlined,
UserOutlined,
PictureOutlined,
FileOutlined,
SoundOutlined,
} from "@ant-design/icons";
import type { CheckboxChangeEvent } from "antd/es/checkbox";
import styles from "./index.module.scss";
import { ContactItem, ScriptGroup } from "../../types";
import { ContactItem, ScriptGroup, MessageItem } from "../../types";
import InputMessage from "./InputMessage/InputMessage";
import ContentLibrarySelector from "./ContentLibrarySelector";
import type { ContentItem } from "@/components/ContentSelection/data";
@@ -36,12 +39,6 @@ interface StepSendMessageProps {
targetLabel: string;
messageContent: string;
onMessageContentChange: (value: string) => void;
friendInterval: [number, number];
onFriendIntervalChange: (value: [number, number]) => void;
messageInterval: [number, number];
onMessageIntervalChange: (value: [number, number]) => void;
selectedTag: string;
onSelectedTagChange: (value: string) => void;
aiRewriteEnabled: boolean;
onAiRewriteToggle: (value: boolean) => void;
aiPrompt: string;
@@ -64,12 +61,6 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
targetLabel,
messageContent,
onMessageContentChange,
friendInterval,
onFriendIntervalChange,
messageInterval,
onMessageIntervalChange,
selectedTag,
onSelectedTagChange,
aiRewriteEnabled,
onAiRewriteToggle,
aiPrompt,
@@ -88,47 +79,111 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
const [savingScriptGroup, setSavingScriptGroup] = useState(false);
const [aiRewriting, setAiRewriting] = useState(false);
const [deletingGroupIds, setDeletingGroupIds] = useState<string[]>([]);
const [aiRewriteModalVisible, setAiRewriteModalVisible] = useState(false);
const [aiRewriteModalIndex, setAiRewriteModalIndex] = useState<number | null>(
null,
);
const [aiRewriteModalPrompt, setAiRewriteModalPrompt] = useState("");
const [aiRewritingMessage, setAiRewritingMessage] = useState(false);
const [aiRewriteResult, setAiRewriteResult] = useState<string | null>(null);
// 将 string[] 转换为 MessageItem[]
const messagesToItems = useCallback((messages: string[]): MessageItem[] => {
return messages.map(msg => {
try {
// 尝试解析为 JSON新格式
const parsed = JSON.parse(msg);
if (parsed && typeof parsed === "object" && "type" in parsed) {
return parsed as MessageItem;
}
} catch {
// 解析失败,作为文本消息处理
}
// 旧格式:纯文本
return { type: "text", content: msg };
});
}, []);
// 将 MessageItem[] 转换为 string[]
const itemsToMessages = useCallback((items: MessageItem[]): string[] => {
return items.map(item => {
// 如果是纯文本消息,直接返回内容(保持向后兼容)
if (item.type === "text" && !item.fileName) {
return item.content;
}
// 其他类型序列化为 JSON
return JSON.stringify(item);
});
}, []);
// 内部维护的 MessageItem[] 状态
const [messageItems, setMessageItems] = useState<MessageItem[]>(() =>
messagesToItems(currentScriptMessages),
);
// 当 currentScriptMessages 变化时,同步更新 messageItems
React.useEffect(() => {
setMessageItems(messagesToItems(currentScriptMessages));
}, [currentScriptMessages, messagesToItems]);
const handleAddMessage = useCallback(
(content?: string, showSuccess?: boolean) => {
const finalContent = (content ?? messageContent).trim();
if (!finalContent) {
antdMessage.warning("请输入消息内容");
return;
(content?: string | MessageItem, showSuccess?: boolean) => {
let newItem: MessageItem;
if (typeof content === "string") {
const finalContent = (content || messageContent).trim();
if (!finalContent) {
antdMessage.warning("请输入消息内容");
return;
}
newItem = { type: "text", content: finalContent };
} else if (content && typeof content === "object") {
newItem = content;
} else {
const finalContent = messageContent.trim();
if (!finalContent) {
antdMessage.warning("请输入消息内容");
return;
}
newItem = { type: "text", content: finalContent };
}
onCurrentScriptMessagesChange([...currentScriptMessages, finalContent]);
const newItems = [...messageItems, newItem];
setMessageItems(newItems);
onCurrentScriptMessagesChange(itemsToMessages(newItems));
onMessageContentChange("");
if (showSuccess) {
antdMessage.success("已添加消息内容");
}
},
[
currentScriptMessages,
messageContent,
messageItems,
onCurrentScriptMessagesChange,
onMessageContentChange,
itemsToMessages,
],
);
const handleRemoveMessage = useCallback(
(index: number) => {
const next = currentScriptMessages.filter((_, idx) => idx !== index);
onCurrentScriptMessagesChange(next);
const next = messageItems.filter((_, idx) => idx !== index);
setMessageItems(next);
onCurrentScriptMessagesChange(itemsToMessages(next));
},
[currentScriptMessages, onCurrentScriptMessagesChange],
[messageItems, onCurrentScriptMessagesChange, itemsToMessages],
);
const handleSaveScriptGroup = useCallback(async () => {
if (savingScriptGroup) {
return;
}
if (currentScriptMessages.length === 0) {
if (messageItems.length === 0) {
antdMessage.warning("请先添加消息内容");
return;
}
const groupName =
currentScriptName.trim() || `话术组${savedScriptGroups.length + 1}`;
const messages = [...currentScriptMessages];
const messages = itemsToMessages(messageItems);
const params: CreateContentLibraryParams = {
name: groupName,
sourceType: 1,
@@ -155,6 +210,7 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
messages,
};
onSavedScriptGroupsChange([...savedScriptGroups, newGroup]);
setMessageItems([]);
onCurrentScriptMessagesChange([]);
onCurrentScriptNameChange("");
onMessageContentChange("");
@@ -169,7 +225,7 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
}, [
aiPrompt,
aiRewriteEnabled,
currentScriptMessages,
messageItems,
currentScriptName,
onCurrentScriptMessagesChange,
onCurrentScriptNameChange,
@@ -177,6 +233,7 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
onSavedScriptGroupsChange,
savedScriptGroups,
savingScriptGroup,
itemsToMessages,
]);
const handleAiRewrite = useCallback(async () => {
@@ -272,6 +329,138 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
onMessageContentChange,
]);
const handleOpenAiRewriteModal = useCallback((index: number) => {
setAiRewriteModalIndex(index);
setAiRewriteModalPrompt("");
setAiRewriteModalVisible(true);
}, []);
const handleCloseAiRewriteModal = useCallback(() => {
setAiRewriteModalVisible(false);
setAiRewriteModalIndex(null);
setAiRewriteModalPrompt("");
setAiRewriteResult(null);
}, []);
// 执行 AI 改写,获取结果但不立即应用
const handleAiRewriteExecute = useCallback(async () => {
if (aiRewriteModalIndex === null) {
return;
}
const trimmedPrompt = aiRewriteModalPrompt.trim();
if (!trimmedPrompt) {
antdMessage.warning("请输入改写提示词");
return;
}
const messageToRewrite = messageItems[aiRewriteModalIndex];
if (!messageToRewrite) {
antdMessage.error("消息不存在");
return;
}
// AI改写只支持文本消息
if (messageToRewrite.type !== "text") {
antdMessage.warning("AI改写仅支持文本消息");
return;
}
if (aiRewritingMessage) {
return;
}
try {
setAiRewritingMessage(true);
const response = await aiEditContent({
aiPrompt: trimmedPrompt,
content: messageToRewrite.content,
});
const normalizedResponse = response as {
content?: string;
contentAfter?: string;
contentFront?: string;
data?:
| string
| {
content?: string;
contentAfter?: string;
contentFront?: string;
};
result?: string;
};
const dataField = normalizedResponse?.data;
const dataContent =
typeof dataField === "string"
? dataField
: (dataField?.content ?? undefined);
const dataContentAfter =
typeof dataField === "string" ? undefined : dataField?.contentAfter;
const dataContentFront =
typeof dataField === "string" ? undefined : dataField?.contentFront;
const primaryAfter =
normalizedResponse?.contentAfter ?? dataContentAfter ?? undefined;
const primaryFront =
normalizedResponse?.contentFront ?? dataContentFront ?? undefined;
let rewrittenContent = "";
if (typeof response === "string") {
rewrittenContent = response;
} else if (primaryAfter) {
rewrittenContent = primaryFront
? `${primaryFront}\n${primaryAfter}`
: primaryAfter;
} else if (typeof normalizedResponse?.content === "string") {
rewrittenContent = normalizedResponse.content;
} else if (typeof dataContent === "string") {
rewrittenContent = dataContent;
} else if (typeof normalizedResponse?.result === "string") {
rewrittenContent = normalizedResponse.result;
} else if (primaryFront) {
rewrittenContent = primaryFront;
}
if (!rewrittenContent || typeof rewrittenContent !== "string") {
antdMessage.error("AI改写失败请稍后重试");
return;
}
setAiRewriteResult(rewrittenContent.trim());
} catch (error) {
console.error("AI改写失败:", error);
antdMessage.error("AI改写失败请稍后重试");
} finally {
setAiRewritingMessage(false);
}
}, [
aiRewriteModalIndex,
aiRewriteModalPrompt,
messageItems,
aiRewritingMessage,
]);
// 确认并应用 AI 改写结果
const handleConfirmAiRewrite = useCallback(() => {
if (aiRewriteModalIndex === null || !aiRewriteResult) {
return;
}
const messageToRewrite = messageItems[aiRewriteModalIndex];
if (!messageToRewrite) {
antdMessage.error("消息不存在");
return;
}
const newItems = [...messageItems];
newItems[aiRewriteModalIndex] = {
...messageToRewrite,
content: aiRewriteResult,
};
setMessageItems(newItems);
onCurrentScriptMessagesChange(itemsToMessages(newItems));
handleCloseAiRewriteModal();
antdMessage.success("AI改写完成");
}, [
aiRewriteModalIndex,
aiRewriteResult,
messageItems,
onCurrentScriptMessagesChange,
itemsToMessages,
handleCloseAiRewriteModal,
]);
const handleApplyGroup = useCallback(
(group: ScriptGroup) => {
onCurrentScriptMessagesChange(group.messages);
@@ -352,61 +541,167 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
<div className={styles.stepContent}>
<div className={styles.step3Content}>
<div className={styles.leftColumn}>
{/* 1. 模拟推送内容 */}
<div className={styles.previewHeader}>
<div className={styles.previewHeaderTitle}></div>
</div>
{/* 2. 消息列表 */}
<div className={styles.messagePreview}>
{messageItems.length === 0 ? (
<div className={styles.messagePlaceholder}>
...
</div>
) : (
<div className={styles.messageList}>
{messageItems.map((msgItem, index) => (
<div className={styles.messageBubbleWrapper} key={index}>
<div className={styles.messageBubble}>
<div className={styles.messageAvatar}>
<UserOutlined />
</div>
<div className={styles.messageContent}>
<div className={styles.messageBubbleInner}>
{msgItem.type === "text" && (
<div className={styles.messageText}>
{msgItem.content}
</div>
)}
{msgItem.type === "image" && (
<div className={styles.messageMedia}>
<div className={styles.messageMediaIcon}>
<PictureOutlined />
</div>
<img
src={msgItem.content}
alt={msgItem.fileName || "图片"}
className={styles.messageImage}
onError={e => {
const target = e.target as HTMLImageElement;
target.style.display = "none";
}}
/>
{msgItem.fileName && (
<div className={styles.messageFileName}>
{msgItem.fileName}
</div>
)}
</div>
)}
{msgItem.type === "file" && (
<div className={styles.messageMedia}>
<div className={styles.messageMediaIcon}>
<FileOutlined />
</div>
<div className={styles.messageFileInfo}>
<div className={styles.messageFileName}>
{msgItem.fileName || "文件"}
</div>
{msgItem.fileSize && (
<div className={styles.messageFileSize}>
{msgItem.fileSize >= 1024 * 1024
? `${(msgItem.fileSize / 1024 / 1024).toFixed(2)} MB`
: `${(msgItem.fileSize / 1024).toFixed(2)} KB`}
</div>
)}
</div>
</div>
)}
{msgItem.type === "audio" && (
<div className={styles.messageMedia}>
<div className={styles.messageMediaIcon}>
<SoundOutlined />
</div>
<div className={styles.messageFileInfo}>
<div className={styles.messageFileName}>
{msgItem.fileName || "语音消息"}
</div>
{msgItem.durationMs && (
<div className={styles.messageFileSize}>
{Math.floor(msgItem.durationMs / 1000)}
</div>
)}
</div>
</div>
)}
</div>
<div className={styles.messageActions}>
{msgItem.type === "text" && (
<Button
type="text"
size="small"
icon={<ReloadOutlined />}
onClick={() => handleOpenAiRewriteModal(index)}
className={styles.aiRewriteButton}
>
AI改写
</Button>
)}
<Button
type="text"
danger
size="small"
icon={<DeleteOutlined />}
onClick={() => handleRemoveMessage(index)}
className={styles.messageAction}
/>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* 3. 消息输入组件 */}
<div className={styles.messageInputArea}>
<InputMessage
defaultValue={messageContent}
onContentChange={onMessageContentChange}
onSend={value => handleAddMessage(value)}
onAddMessage={message => handleAddMessage(message)}
clearOnSend
placeholder="请输入内容"
hint={`按ENTER发送按住CTRL+ENTER换行已配置${savedScriptGroups.length}个话术组,已选择${selectedScriptGroupIds.length}个进行推送,已选${selectedContentLibraries.length}个内容库`}
/>
</div>
{/* 4. 话术组标题 */}
<div className={styles.scriptNameInput}>
<Input
placeholder="话术组名称(可选)"
value={currentScriptName}
onChange={event => onCurrentScriptNameChange(event.target.value)}
/>
</div>
{/* 5. 创建话术组按钮 */}
<div className={styles.createScriptGroupButton}>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleSaveScriptGroup}
disabled={currentScriptMessages.length === 0 || savingScriptGroup}
loading={savingScriptGroup}
block
>
</Button>
</div>
<div className={styles.messagePreview}>
<div className={styles.messageBubble}>
<div className={styles.currentEditingLabel}></div>
{currentScriptMessages.length === 0 ? (
<div className={styles.messagePlaceholder}>
...
</div>
) : (
<div className={styles.messageList}>
{currentScriptMessages.map((msg, index) => (
<div className={styles.messageItem} key={index}>
<div className={styles.messageText}>{msg}</div>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => handleRemoveMessage(index)}
className={styles.messageAction}
/>
</div>
))}
</div>
)}
</div>
<div className={styles.scriptNameInput}>
<Input
placeholder="话术组名称(可选)"
value={currentScriptName}
onChange={event =>
onCurrentScriptNameChange(event.target.value)
}
/>
</div>
</div>
</div>
<div className={styles.savedScriptGroups}>
{/* 内容库选择组件 */}
<div className={styles.rightColumn}>
<div className={styles.pushContentHeader}>
<div className={styles.pushContentTitle}></div>
<ContentLibrarySelector
selectedContentLibraries={selectedContentLibraries}
onSelectedContentLibrariesChange={
onSelectedContentLibrariesChange
}
/>
</div>
<div className={styles.savedScriptGroups}>
<div className={styles.scriptGroupHeaderRow}>
<div className={styles.scriptGroupTitle}>
({savedScriptGroups.length})
@@ -451,137 +746,119 @@ const StepSendMessage: React.FC<StepSendMessageProps> = ({
/>
</div>
</div>
<div className={styles.scriptGroupContent}>
{group.messages[0]}
{group.messages.length > 1 && " ..."}
</div>
</div>
))
)}
</div>
</div>
<div className={styles.messageInputArea}>
<InputMessage
defaultValue={messageContent}
onContentChange={onMessageContentChange}
onSend={value => handleAddMessage(value)}
clearOnSend
placeholder="请输入内容"
hint={`按住CTRL+ENTER换行已配置${savedScriptGroups.length}个话术组,已选择${selectedScriptGroupIds.length}个进行推送,已选${selectedContentLibraries.length}个内容库`}
/>
<div className={styles.aiRewriteSection}>
<div className={styles.aiRewriteToggle}>
<Switch
checked={aiRewriteEnabled}
onChange={onAiRewriteToggle}
/>
<div className={styles.aiRewriteLabel}>AI智能话术改写</div>
<div>
{aiRewriteEnabled && (
<Input
placeholder="输入改写提示词"
value={aiPrompt}
onChange={event => onAiPromptChange(event.target.value)}
className={styles.aiRewriteInput}
/>
)}
</div>
</div>
<div className={styles.aiRewriteActions}>
<Button
icon={<ReloadOutlined />}
onClick={handleAiRewrite}
disabled={!aiRewriteEnabled}
loading={aiRewriting}
className={styles.aiRewriteButton}
>
AI改写
</Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => handleAddMessage(undefined, true)}
>
</Button>
</div>
</div>
</div>
</div>
<div className={styles.rightColumn}>
<div className={styles.settingsPanel}>
<div className={styles.settingsTitle}></div>
<div className={styles.settingItem}>
<div className={styles.settingLabel}></div>
<div className={styles.settingControl}>
<span>()</span>
<Slider
range
min={1}
max={60}
value={friendInterval}
onChange={value =>
onFriendIntervalChange(value as [number, number])
}
style={{ flex: 1, margin: "0 16px" }}
/>
<span>
{friendInterval[0]} - {friendInterval[1]}
</span>
</div>
</div>
<div className={styles.settingItem}>
<div className={styles.settingLabel}></div>
<div className={styles.settingControl}>
<span>()</span>
<Slider
range
min={1}
max={60}
value={messageInterval}
onChange={value =>
onMessageIntervalChange(value as [number, number])
}
style={{ flex: 1, margin: "0 16px" }}
/>
<span>
{messageInterval[0]} - {messageInterval[1]}
</span>
</div>
</div>
</div>
<div className={styles.tagSection}>
<div className={styles.settingLabel}></div>
<Select
value={selectedTag}
onChange={onSelectedTagChange}
placeholder="选择标签"
style={{ width: "100%" }}
>
<Select.Option value="potential"></Select.Option>
<Select.Option value="customer"></Select.Option>
<Select.Option value="partner"></Select.Option>
</Select>
</div>
<div className={styles.pushPreview}>
<div className={styles.previewTitle}></div>
<ul>
<li>: {selectedAccounts.length}</li>
<li>
{targetLabel}: {selectedContacts.length}
</li>
<li>: {savedScriptGroups.length}</li>
<li>随机推送: </li>
<li>: ~1</li>
</ul>
</div>
</div>
</div>
{/* AI改写弹窗 */}
<Modal
title={
<div className={styles.aiRewriteModalTitle}>
<span className={styles.aiRewriteModalTitleIcon}></span>
<span>AI智能改写</span>
</div>
}
open={aiRewriteModalVisible}
onCancel={handleCloseAiRewriteModal}
width={680}
footer={[
<Button key="cancel" onClick={handleCloseAiRewriteModal}>
</Button>,
<Button
key="execute"
type="primary"
className={styles.aiRewriteExecuteButton}
loading={aiRewritingMessage}
onClick={handleAiRewriteExecute}
disabled={!aiRewriteModalPrompt.trim()}
>
AI改写
</Button>,
<Button
key="confirm"
type="primary"
className={styles.confirmButton}
onClick={handleConfirmAiRewrite}
disabled={!aiRewriteResult || aiRewritingMessage}
>
</Button>,
]}
className={styles.aiRewriteModal}
wrapClassName={styles.aiRewriteModalWrap}
>
<div className={styles.aiRewriteModalContent}>
{/* 原文和结果对比区域 */}
<div className={styles.aiRewriteModalCompareSection}>
{/* 原消息内容区域 */}
{aiRewriteModalIndex !== null && (
<div className={styles.aiRewriteModalSection}>
<div className={styles.aiRewriteModalSectionHeader}>
<span className={styles.aiRewriteModalSectionIcon}>📝</span>
<span className={styles.aiRewriteModalLabel}>
1
</span>
</div>
<div className={styles.aiRewriteModalOriginalText}>
{messageItems[aiRewriteModalIndex]?.type === "text"
? messageItems[aiRewriteModalIndex].content
: "非文本消息不支持AI改写"}
</div>
</div>
)}
{/* Loading 状态 */}
{aiRewritingMessage && (
<div className={styles.aiRewriteModalLoading}>
<div className={styles.aiRewriteModalLoadingIcon}></div>
<div className={styles.aiRewriteModalLoadingText}>
AI正在改写中...
</div>
</div>
)}
{/* 分隔线 */}
{aiRewriteModalIndex !== null && aiRewriteResult && (
<div className={styles.aiRewriteModalDivider} />
)}
{/* 改写结果区域 */}
{aiRewriteResult && (
<div className={styles.aiRewriteModalSection}>
<div className={styles.aiRewriteModalSectionHeader}>
<span className={styles.aiRewriteModalSectionIcon}></span>
<span className={styles.aiRewriteModalLabel}></span>
</div>
<div className={styles.aiRewriteModalResultText}>
{aiRewriteResult}
</div>
</div>
)}
</div>
{/* 提示词输入区域 - 放在最下面 */}
<div className={styles.aiRewriteModalSection}>
<div className={styles.aiRewriteModalSectionHeader}>
<span className={styles.aiRewriteModalSectionIcon}>💡</span>
<span className={styles.aiRewriteModalLabel}></span>
</div>
<Input.TextArea
placeholder="默认提示词为: 1、原本的字数和意思不要修改超过10% 2、出现品牌名或个人名字就去除。"
value={aiRewriteModalPrompt}
onChange={event => setAiRewriteModalPrompt(event.target.value)}
rows={3}
autoFocus
disabled={aiRewritingMessage}
className={styles.aiRewriteModalTextArea}
/>
</div>
</div>
</Modal>
</div>
);
};

View File

@@ -14,7 +14,13 @@ import { getCustomerList } from "@/pages/pc/ckbox/weChat/api";
import StepSelectAccount from "./components/StepSelectAccount";
import StepSelectContacts from "./components/StepSelectContacts";
import StepSendMessage from "./components/StepSendMessage";
import { ContactItem, PushType, ScriptGroup } from "./types";
import StepPushParams from "./components/StepPushParams";
import {
ContactItem,
PushType,
ScriptGroup,
CreatePushTaskPayload,
} from "./types";
import StepIndicator from "@/components/StepIndicator";
import type { ContentItem } from "@/components/ContentSelection/data";
import type { PoolSelectionItem } from "@/components/PoolSelection/data";
@@ -163,6 +169,20 @@ const CreatePushTask: React.FC = () => {
return;
}
setCurrentStep(3);
return;
}
if (currentStep === 3) {
// 验证推送内容
if (
currentScriptMessages.length === 0 &&
selectedScriptGroupIds.length === 0 &&
selectedContentLibraries.length === 0
) {
message.warning("请至少添加一条消息、选择一个话术组或内容库");
return;
}
setCurrentStep(4);
}
};
@@ -184,9 +204,12 @@ const CreatePushTask: React.FC = () => {
if (creatingTask) {
return;
}
// ========== 1. 数据验证和准备 ==========
const selectedGroups = savedScriptGroups.filter(group =>
selectedScriptGroupIds.includes(group.id),
);
if (
currentScriptMessages.length === 0 &&
selectedGroups.length === 0 &&
@@ -195,19 +218,27 @@ const CreatePushTask: React.FC = () => {
message.warning("请添加话术内容、选择话术组或内容库");
return;
}
// 手动消息处理
const manualMessages = currentScriptMessages
.map(item => item.trim())
.filter(Boolean);
if (validPushType === "group-announcement" && manualMessages.length === 0) {
message.warning("请先填写公告内容");
return;
}
const toNumberId = (value: unknown) => {
// ID 转换工具函数
const toNumberId = (value: unknown): number | null => {
if (value === null || value === undefined) return null;
const numeric = Number(value);
return Number.isFinite(numeric) && !Number.isNaN(numeric)
? numeric
: null;
};
// ========== 2. 内容库ID处理 ==========
const contentGroupIds = Array.from(
new Set(
[
@@ -220,6 +251,7 @@ const CreatePushTask: React.FC = () => {
].filter((id): id is number => id !== null),
),
);
if (
manualMessages.length === 0 &&
selectedGroups.length === 0 &&
@@ -228,6 +260,8 @@ const CreatePushTask: React.FC = () => {
message.warning("缺少有效的话术内容,请重新检查");
return;
}
// ========== 3. 账号ID处理 ==========
const ownerWechatIds = Array.from(
new Set(
selectedAccounts
@@ -235,47 +269,25 @@ const CreatePushTask: React.FC = () => {
.filter((id): id is number => id !== null),
),
);
if (ownerWechatIds.length === 0) {
message.error("缺少有效的推送账号信息");
return;
}
// ========== 4. 联系人ID处理 ==========
const selectedContactIds = Array.from(
new Set(
selectedContacts.map(contact => contact?.id).filter(isValidNumber),
),
);
if (selectedContactIds.length === 0) {
message.error("缺少有效的推送对象");
return;
}
const friendIntervalMin = friendInterval[0];
const friendIntervalMax = friendInterval[1];
const messageIntervalMin = messageInterval[0];
const messageIntervalMax = messageInterval[1];
const trafficPoolIds = selectedTrafficPools
.map(pool => pool.id)
.filter(
id => id !== undefined && id !== null && String(id).trim() !== "",
);
const { startTime, endTime } = DEFAULT_TIME_RANGE[validPushType];
const maxPerDay =
selectedContacts.length > 0
? selectedContacts.length
: DEFAULT_MAX_PER_DAY[validPushType];
const pushOrder = DEFAULT_PUSH_ORDER[validPushType];
const normalizedPostPushTags =
selectedTag.trim().length > 0
? [
toNumberId(selectedTag) !== null
? (toNumberId(selectedTag) as number)
: selectedTag,
]
: [];
const taskName =
currentScriptName.trim() ||
selectedGroups[0]?.name ||
(manualMessages[0] ? manualMessages[0].slice(0, 20) : "") ||
`推送任务-${Date.now()}`;
// ========== 5. 设备分组ID处理好友推送必填 ==========
const deviceGroupIds = Array.from(
new Set(
selectedAccounts
@@ -283,84 +295,191 @@ const CreatePushTask: React.FC = () => {
.filter((id): id is number => id !== null),
),
);
if (validPushType === "friend-message" && deviceGroupIds.length === 0) {
message.error("缺少有效的推送设备分组");
return;
}
const basePayload: Record<string, any> = {
name: taskName,
type: 3,
autoStart: DEFAULT_AUTO_START[validPushType],
status: 1,
pushType: DEFAULT_PUSH_TYPE[validPushType],
// ========== 6. 流量池ID处理 ==========
const trafficPoolIds = selectedTrafficPools
.map(pool => {
const id = pool.id;
if (id === undefined || id === null) return null;
const strId = String(id).trim();
return strId !== "" ? strId : null;
})
.filter((id): id is string => id !== null);
// ========== 7. 时间范围 ==========
const { startTime, endTime } = DEFAULT_TIME_RANGE[validPushType];
// ========== 8. 每日最大推送数 ==========
const maxPerDay =
selectedContacts.length > 0
? selectedContacts.length
: DEFAULT_MAX_PER_DAY[validPushType];
// ========== 9. 推送顺序 ==========
const pushOrder = DEFAULT_PUSH_ORDER[validPushType];
// ========== 10. 推送后标签处理 ==========
const postPushTags =
selectedTag.trim().length > 0
? (() => {
const tagId = toNumberId(selectedTag);
return tagId !== null ? [tagId] : [];
})()
: [];
// ========== 11. 任务名称 ==========
const taskName =
currentScriptName.trim() ||
selectedGroups[0]?.name ||
(manualMessages[0] ? manualMessages[0].slice(0, 20) : "") ||
`推送任务-${Date.now()}`;
// ========== 12. 构建基础载荷 ==========
const basePayload: CreatePushTaskPayload = {
name: String(taskName).trim(),
type: 3, // 固定值:工作台类型
autoStart: DEFAULT_AUTO_START[validPushType] ? 1 : 0,
status: 1, // 固定值:启用
pushType: DEFAULT_PUSH_TYPE[validPushType] ? 1 : 0,
targetType: validPushType === "friend-message" ? 2 : 1,
groupPushSubType: validPushType === "group-announcement" ? 2 : 1,
startTime,
endTime,
maxPerDay,
pushOrder,
friendIntervalMin,
friendIntervalMax,
messageIntervalMin,
messageIntervalMax,
startTime: String(startTime),
endTime: String(endTime),
maxPerDay: Number(maxPerDay),
pushOrder: Number(pushOrder),
friendIntervalMin: Number(friendInterval[0]),
friendIntervalMax: Number(friendInterval[1]),
messageIntervalMin: Number(messageInterval[0]),
messageIntervalMax: Number(messageInterval[1]),
isRandomTemplate: selectedScriptGroupIds.length > 1 ? 1 : 0,
contentGroups: contentGroupIds,
postPushTags: normalizedPostPushTags,
ownerWechatIds,
enableAiRewrite: aiRewriteEnabled ? 1 : 0,
contentGroups: contentGroupIds.length > 0 ? contentGroupIds : [],
postPushTags: postPushTags,
ownerWechatIds: ownerWechatIds,
};
if (trafficPoolIds.length > 0) {
basePayload.trafficPools = trafficPoolIds;
}
if (validPushType === "friend-message") {
basePayload.isLoop = 0;
basePayload.deviceGroups = deviceGroupIds;
}
if (manualMessages.length > 0) {
basePayload.manualMessages = manualMessages;
if (currentScriptName.trim()) {
basePayload.manualScriptName = currentScriptName.trim();
}
}
if (selectedScriptGroupIds.length > 0) {
basePayload.selectedScriptGroupIds = selectedScriptGroupIds;
}
if (aiRewriteEnabled && aiPrompt.trim()) {
basePayload.aiRewritePrompt = aiPrompt.trim();
}
if (selectedGroups.length > 0) {
basePayload.scriptGroups = selectedGroups.map(group => ({
id: group.id,
name: group.name,
messages: group.messages,
}));
}
// ========== 13. 根据推送类型添加特定字段 ==========
if (validPushType === "friend-message") {
// 好友推送特有字段
// 注意wechatFriends 必须是字符串数组,不是数字数组
basePayload.wechatFriends = Array.from(
new Set(
selectedContacts
.map(contact => toNumberId(contact?.id))
.filter((id): id is number => id !== null),
.map(contact => {
const id = toNumberId(contact?.id);
return id !== null ? String(id) : null;
})
.filter((id): id is string => id !== null),
),
);
basePayload.targetType = 2;
basePayload.deviceGroups = deviceGroupIds; // 必填,数字数组
basePayload.isLoop = 0; // 固定值
basePayload.targetType = 2; // 确保是好友类型
basePayload.groupPushSubType = 1; // 固定为群群发
} else {
// 群推送特有字段
const groupIds = Array.from(
new Set(
selectedContacts
.map(contact => toNumberId(contact.groupId ?? contact.id))
.map(contact => {
// 优先使用 groupId如果没有则使用 id
const id = contact.groupId ?? contact.id;
return toNumberId(id);
})
.filter((id): id is number => id !== null),
),
);
basePayload.wechatGroups = groupIds;
basePayload.wechatGroups = groupIds; // 数字数组
basePayload.targetType = 1; // 群类型
basePayload.groupPushSubType =
validPushType === "group-announcement" ? 2 : 1;
basePayload.targetType = 1;
// 群公告特有字段
if (validPushType === "group-announcement") {
basePayload.announcementContent = manualMessages.join("\n");
}
}
// ========== 14. 可选字段处理 ==========
// 流量池(如果存在)
if (trafficPoolIds.length > 0) {
basePayload.trafficPools = trafficPoolIds; // 字符串数组
}
// 手动消息(如果存在)
if (manualMessages.length > 0) {
basePayload.manualMessages = manualMessages;
if (currentScriptName.trim()) {
basePayload.manualScriptName = String(currentScriptName.trim());
}
}
// 选中的话术组ID如果存在
if (selectedScriptGroupIds.length > 0) {
basePayload.selectedScriptGroupIds = selectedScriptGroupIds.map(id =>
String(id),
);
}
// AI改写相关如果启用
if (aiRewriteEnabled) {
basePayload.enableAiRewrite = 1;
if (aiPrompt.trim()) {
basePayload.aiRewritePrompt = String(aiPrompt.trim());
}
} else {
basePayload.enableAiRewrite = 0;
}
// 话术组对象(如果存在)
if (selectedGroups.length > 0) {
basePayload.scriptGroups = selectedGroups.map(group => ({
id: String(group.id),
name: String(group.name || ""),
messages: Array.isArray(group.messages)
? group.messages.map(msg => String(msg))
: [],
}));
}
// ========== 15. 数据验证和提交 ==========
// 最终验证:确保必填字段存在
if (validPushType === "friend-message") {
if (
!Array.isArray(basePayload.deviceGroups) ||
basePayload.deviceGroups.length === 0
) {
message.error("好友推送必须选择设备分组");
return;
}
if (
!Array.isArray(basePayload.wechatFriends) ||
basePayload.wechatFriends.length === 0
) {
message.error("好友推送必须选择好友");
return;
}
} else {
if (
!Array.isArray(basePayload.wechatGroups) ||
basePayload.wechatGroups.length === 0
) {
message.error("群推送必须选择群");
return;
}
}
// 提交前打印日志(开发环境)
if (process.env.NODE_ENV === "development") {
console.log("提交数据:", JSON.stringify(basePayload, null, 2));
}
// ========== 16. 提交请求 ==========
let hideLoading: ReturnType<typeof message.loading> | undefined;
try {
setCreatingTask(true);
@@ -386,7 +505,7 @@ const CreatePushTask: React.FC = () => {
<Layout
header={
<>
<div style={{ padding: "20px" }}>
<div style={{ padding: "0 20px" }}>
<PowerNavigation
title={title}
subtitle={subtitle}
@@ -395,26 +514,33 @@ const CreatePushTask: React.FC = () => {
onBackClick={handleClose}
/>
</div>
<StepIndicator
currentStep={currentStep}
steps={[
{
id: 1,
title: "选择微信",
subtitle: "选择微信",
},
{
id: 2,
title: `选择${step2Title}`,
subtitle: `选择${step2Title}`,
},
{
id: 3,
title: "一键群发",
subtitle: "一键群发",
},
]}
/>
<div style={{ margin: "0 20px" }}>
<StepIndicator
currentStep={currentStep}
steps={[
{
id: 1,
title: "选择微信",
subtitle: "选择微信",
},
{
id: 2,
title: `选择${step2Title}`,
subtitle: `选择${step2Title}`,
},
{
id: 3,
title: "推送内容",
subtitle: "推送内容",
},
{
id: 4,
title: "推送参数",
subtitle: "推送参数",
},
]}
/>
</div>
</>
}
footer={
@@ -434,6 +560,12 @@ const CreatePushTask: React.FC = () => {
{selectedContacts.length}
</span>
)}
{currentStep === 4 && (
<span>
: {selectedAccounts.length}, {step2Title}:{" "}
{selectedContacts.length}
</span>
)}
</div>
<div className={styles.footerRight}>
{currentStep === 1 && (
@@ -458,6 +590,14 @@ const CreatePushTask: React.FC = () => {
</>
)}
{currentStep === 3 && (
<>
<Button onClick={handlePrev}></Button>
<Button type="primary" onClick={handleNext}>
&gt;
</Button>
</>
)}
{currentStep === 4 && (
<>
<Button onClick={handlePrev}></Button>
<Button
@@ -511,16 +651,24 @@ const CreatePushTask: React.FC = () => {
onSelectedScriptGroupIdsChange={setSelectedScriptGroupIds}
selectedContentLibraries={selectedContentLibraries}
onSelectedContentLibrariesChange={setSelectedContentLibraries}
aiRewriteEnabled={aiRewriteEnabled}
onAiRewriteToggle={setAiRewriteEnabled}
aiPrompt={aiPrompt}
onAiPromptChange={setAiPrompt}
/>
)}
{currentStep === 4 && (
<StepPushParams
selectedAccounts={selectedAccounts}
selectedContacts={selectedContacts}
targetLabel={step2Title}
friendInterval={friendInterval}
onFriendIntervalChange={setFriendInterval}
messageInterval={messageInterval}
onMessageIntervalChange={setMessageInterval}
selectedTag={selectedTag}
onSelectedTagChange={setSelectedTag}
aiRewriteEnabled={aiRewriteEnabled}
onAiRewriteToggle={setAiRewriteEnabled}
aiPrompt={aiPrompt}
onAiPromptChange={setAiPrompt}
savedScriptGroups={savedScriptGroups}
/>
)}
</div>

View File

@@ -20,8 +20,67 @@ export interface ContactItem {
extendFields?: Record<string, any>;
}
// 消息类型定义
export type MessageType = "text" | "image" | "file" | "audio";
export interface MessageItem {
type: MessageType;
content: string; // 文本内容或文件URL
// 文件相关字段
fileName?: string; // 文件名
fileSize?: number; // 文件大小(字节)
// 语音相关字段
durationMs?: number; // 语音时长(毫秒)
}
export interface ScriptGroup {
id: string;
name: string;
messages: string[];
messages: string[]; // 保持向后兼容,但实际应该使用 MessageItem[]
}
// 接口请求载荷类型定义
export interface CreatePushTaskPayload {
// 基础字段
name: string;
type: 3; // 固定值:工作台类型
autoStart: 0 | 1;
status: 1; // 固定值:启用
pushType: 0 | 1; // 0=定时1=立即
targetType: 1 | 2; // 1=群推送2=好友推送
groupPushSubType: 1 | 2; // 1=群群发2=群公告
startTime: string; // "HH:mm" 格式
endTime: string; // "HH:mm" 格式
maxPerDay: number;
pushOrder: 1 | 2; // 1=最早优先2=最新优先
friendIntervalMin: number;
friendIntervalMax: number;
messageIntervalMin: number;
messageIntervalMax: number;
isRandomTemplate: 0 | 1;
contentGroups: number[]; // 内容库ID数组
postPushTags: number[]; // 推送后标签ID数组
ownerWechatIds: number[]; // 客服ID数组
// 好友推送特有字段
wechatFriends?: string[]; // 好友ID列表字符串数组
deviceGroups?: number[]; // 设备分组ID数组好友推送时必填
isLoop?: 0; // 固定值(好友推送时)
// 群推送特有字段
wechatGroups?: number[]; // 微信群ID数组
announcementContent?: string; // 群公告内容(群公告时必填)
// 可选字段
trafficPools?: string[]; // 流量池ID数组字符串数组
manualMessages?: string[]; // 手动消息数组
manualScriptName?: string; // 手动话术名称
selectedScriptGroupIds?: string[]; // 选中的话术组ID数组
enableAiRewrite?: 0 | 1; // 是否启用AI改写
aiRewritePrompt?: string; // AI改写提示词
scriptGroups?: Array<{
id: string;
name: string;
messages: string[];
}>; // 话术组对象数组
}

View File

@@ -1,79 +0,0 @@
帮我对接数据,以下是传参实例,三种模式都是同一界面的。
群发助手传参实例
{
"name": "群群发-新品宣传", // 任务名称
"type": 3, // 工作台类型3=群消息推送
"autoStart": 1, // 保存后自动启动
"status": 1, // 是否启用
"pushType": 0, // 推送方式0=定时1=立即
"targetType": 1, // 目标类型1=群推送
"groupPushSubType": 1, // 群推送子类型1=群群发2=群公告
"startTime": "09:00", // 推送起始时间
"endTime": "20:00", // 推送结束时间
"maxPerDay": 200, // 每日最大推送群数
"pushOrder": 1, // 推送顺序1=最早优先2=最新优先
"wechatGroups": [102, 205, 318], // 选择的微信群 ID 列表
"contentGroups": [11, 12], // 关联内容库 ID 列表
"friendIntervalMin": 10, // 群间最小间隔(秒)
"friendIntervalMax": 25, // 群间最大间隔(秒)
"messageIntervalMin": 2, // 同一群消息间最小间隔(秒)
"messageIntervalMax": 6, // 同一群消息间最大间隔(秒)
"isRandomTemplate": 1, // 是否随机选择话术模板
"postPushTags": [301, 302], // 推送完成后打的标签
ownerWechatIds[123123,1231231] //客服id
}
//群公告传参实例
{
"name": "群公告-双11活动", // 任务名称
"type": 3, // 群消息推送
"autoStart": 0, // 不自动启动
"status": 1, // 启用
"pushType": 1, // 立即推送
"targetType": 1, // 群推送
"groupPushSubType": 2, // 群公告
"startTime": "08:30", // 开始时间
"endTime": "18:30", // 结束时间
"maxPerDay": 80, // 每日最大公告数
"pushOrder": 2, // 最新优先
"wechatGroups": [5021, 5026], // 公告目标群
"announcementContent": "…", // 公告正文
"enableAiRewrite": 1, // 启用 AI 改写
"aiRewritePrompt": "保持活泼口吻…", // AI 改写提示词
"contentGroups": [21], // 关联内容库
"friendIntervalMin": 15, // 群间最小间隔
"friendIntervalMax": 30, // 群间最大间隔
"messageIntervalMin": 3, // 消息间最小间隔
"messageIntervalMax": 9, // 消息间最大间隔
"isRandomTemplate": 0, // 不随机模板
"postPushTags": [], // 推送后标签
ownerWechatIds[123123,1231231] //客服id
}
//好友传参实例
{
"name": "好友私聊-新客转化", // 任务名称
"type": 3, // 群消息推送
"autoStart": 1, // 自动启动
"status": 1, // 启用
"pushType": 0, // 定时推送
"targetType": 2, // 目标类型2=好友推送
"groupPushSubType": 1, // 固定为群群发(好友推送不支持公告)
"startTime": "10:00", // 开始时间
"endTime": "22:00", // 结束时间
"maxPerDay": 150, // 每日最大推送好友数
"pushOrder": 1, // 最早优先
"wechatFriends": ["12312"], // 指定好友列表(可为空数组)
"deviceGroups": [9001, 9002], // 必选:推送设备分组 ID
"contentGroups": [41, 42], // 话术内容库
"friendIntervalMin": 12, // 好友间最小间隔
"friendIntervalMax": 28, // 好友间最大间隔
"messageIntervalMin": 4, // 消息间最小间隔
"messageIntervalMax": 10, // 消息间最大间隔
"isRandomTemplate": 1, // 随机话术
"postPushTags": [501], // 推送后标签
ownerWechatIds[123123,1231231] //客服id
}
请求接口是 queryWorkbenchCreate

View File

@@ -6,8 +6,10 @@ export interface GetPushHistoryParams {
page?: number;
pageSize?: number;
keyword?: string;
pushType?: string;
status?: string;
pushTypeCode?: string; // 推送类型代码friend, group, announcement
status?: string; // 状态pending, completed, failed
workbenchId?: string;
[property: string]: any;
}
// 获取推送历史接口响应
@@ -27,11 +29,30 @@ export interface GetPushHistoryResponse {
*/
export interface GetGroupPushHistoryParams {
keyword?: string;
limit: string;
page: string;
limit?: string | number;
page?: string | number;
pageSize?: string | number;
pushTypeCode?: string;
status?: string;
workbenchId?: string;
[property: string]: any;
}
export const getPushHistory = async (params: GetGroupPushHistoryParams) => {
return request("/v1/workbench/group-push-history", params, "GET");
export const getPushHistory = async (
params: GetGroupPushHistoryParams,
): Promise<GetPushHistoryResponse> => {
// 转换参数格式,确保 limit 和 page 是字符串
const requestParams: Record<string, any> = {
...params,
};
if (params.page !== undefined) {
requestParams.page = String(params.page);
}
if (params.pageSize !== undefined) {
requestParams.limit = String(params.pageSize);
}
return request("/v1/workbench/group-push-history", requestParams, "GET");
};

View File

@@ -15,30 +15,33 @@ import styles from "./index.module.scss";
const { Option } = Select;
// 推送类型枚举
export enum PushType {
FRIEND_MESSAGE = "friend-message", // 好友消息
GROUP_MESSAGE = "group-message", // 群消息
GROUP_ANNOUNCEMENT = "group-announcement", // 群公告
// 推送类型代码枚举
export enum PushTypeCode {
FRIEND = "friend", // 好友消息
GROUP = "group", // 群消息
ANNOUNCEMENT = "announcement", // 群公告
}
// 推送状态枚举
export enum PushStatus {
PENDING = "pending", // 进行中
COMPLETED = "completed", // 已完成
IN_PROGRESS = "in-progress", // 进行中
FAILED = "failed", // 失败
}
// 推送历史记录接口
export interface PushHistoryRecord {
id: string;
pushType: PushType;
pushContent: string;
workbenchId: number;
taskName: string;
pushType: string; // 推送类型中文名称,如 "好友消息"
pushTypeCode: string; // 推送类型代码,如 "friend"
targetCount: number;
successCount: number;
failureCount: number;
status: PushStatus;
failCount: number;
status: string; // 状态代码,如 "pending"
statusText: string; // 状态中文名称,如 "进行中"
createTime: string;
contentLibraryName: string; // 内容库名称
}
const PushHistory: React.FC = () => {
@@ -59,8 +62,8 @@ const PushHistory: React.FC = () => {
try {
setLoading(true);
const params: any = {
page,
pageSize: pagination.pageSize,
page: String(page),
limit: String(pagination.pageSize),
};
if (searchValue.trim()) {
@@ -68,7 +71,7 @@ const PushHistory: React.FC = () => {
}
if (typeFilter !== "all") {
params.pushType = typeFilter;
params.pushTypeCode = typeFilter;
}
if (statusFilter !== "all") {
@@ -157,13 +160,33 @@ const PushHistory: React.FC = () => {
};
// 获取推送类型标签
const getPushTypeTag = (type: PushType) => {
const typeMap = {
[PushType.FRIEND_MESSAGE]: { text: "好友消息", color: "#666" },
[PushType.GROUP_MESSAGE]: { text: "群消息", color: "#666" },
[PushType.GROUP_ANNOUNCEMENT]: { text: "群公告", color: "#666" },
const getPushTypeTag = (pushType: string, pushTypeCode?: string) => {
// 优先使用中文名称,如果没有则根据代码映射
if (pushType) {
const colorMap: Record<string, string> = {
: "#1890ff",
: "#52c41a",
: "#722ed1",
};
return (
<Tag
color={colorMap[pushType] || "#666"}
style={{ borderRadius: "12px" }}
>
{pushType}
</Tag>
);
}
// 如果没有中文名称,根据代码映射
const codeMap: Record<string, { text: string; color: string }> = {
[PushTypeCode.FRIEND]: { text: "好友消息", color: "#1890ff" },
[PushTypeCode.GROUP]: { text: "群消息", color: "#52c41a" },
[PushTypeCode.ANNOUNCEMENT]: { text: "群公告", color: "#722ed1" },
};
const config = typeMap[type] || { text: "未知", color: "#666" };
const config =
pushTypeCode && codeMap[pushTypeCode]
? codeMap[pushTypeCode]
: { text: pushType || "未知", color: "#666" };
return (
<Tag color={config.color} style={{ borderRadius: "12px" }}>
{config.text}
@@ -172,14 +195,31 @@ const PushHistory: React.FC = () => {
};
// 获取状态标签
const getStatusTag = (status: PushStatus) => {
const statusMap = {
const getStatusTag = (status: string, statusText?: string) => {
// 优先使用中文状态文本
const displayText = statusText || status;
// 根据状态代码或文本匹配
const statusMap: Record<
string,
{ text: string; color: string; icon: React.ReactNode }
> = {
[PushStatus.COMPLETED]: {
text: "已完成",
color: "#52c41a",
icon: <CheckCircleOutlined />,
},
[PushStatus.IN_PROGRESS]: {
completed: {
text: "已完成",
color: "#52c41a",
icon: <CheckCircleOutlined />,
},
[PushStatus.PENDING]: {
text: "进行中",
color: "#1890ff",
icon: <ClockCircleOutlined />,
},
pending: {
text: "进行中",
color: "#1890ff",
icon: <ClockCircleOutlined />,
@@ -189,12 +229,43 @@ const PushHistory: React.FC = () => {
color: "#ff4d4f",
icon: <CloseCircleOutlined />,
},
failed: {
text: "失败",
color: "#ff4d4f",
icon: <CloseCircleOutlined />,
},
};
const config = statusMap[status] || {
text: "未知",
color: "#666",
icon: null,
// 根据状态文本匹配
const textMap: Record<
string,
{ text: string; color: string; icon: React.ReactNode }
> = {
: {
text: "已完成",
color: "#52c41a",
icon: <CheckCircleOutlined />,
},
: {
text: "进行中",
color: "#1890ff",
icon: <ClockCircleOutlined />,
},
: {
text: "失败",
color: "#ff4d4f",
icon: <CloseCircleOutlined />,
},
};
const config = textMap[displayText] ||
statusMap[status] ||
statusMap[status.toLowerCase()] || {
text: displayText,
color: "#666",
icon: null,
};
return (
<Tag
color={config.color}
@@ -217,15 +288,26 @@ const PushHistory: React.FC = () => {
dataIndex: "pushType",
key: "pushType",
width: 120,
render: (type: PushType) => getPushTypeTag(type),
render: (pushType: string, record: PushHistoryRecord) =>
getPushTypeTag(pushType, record.pushTypeCode),
},
{
title: "推送内容",
dataIndex: "pushContent",
key: "pushContent",
title: "任务名称",
dataIndex: "taskName",
key: "taskName",
ellipsis: true,
render: (text: string) => <span style={{ color: "#333" }}>{text}</span>,
},
{
title: "内容库",
dataIndex: "contentLibraryName",
key: "contentLibraryName",
width: 150,
ellipsis: true,
render: (text: string) => (
<span style={{ color: "#666", fontSize: "13px" }}>{text || "-"}</span>
),
},
{
title: "目标数量",
dataIndex: "targetCount",
@@ -246,8 +328,8 @@ const PushHistory: React.FC = () => {
},
{
title: "失败数",
dataIndex: "failureCount",
key: "failureCount",
dataIndex: "failCount",
key: "failCount",
width: 100,
align: "center" as const,
render: (count: number) => (
@@ -260,7 +342,8 @@ const PushHistory: React.FC = () => {
key: "status",
width: 120,
align: "center" as const,
render: (status: PushStatus) => getStatusTag(status),
render: (status: string, record: PushHistoryRecord) =>
getStatusTag(status, record.statusText),
},
{
title: "创建时间",
@@ -329,9 +412,9 @@ const PushHistory: React.FC = () => {
suffixIcon={<span></span>}
>
<Option value="all"></Option>
<Option value={PushType.FRIEND_MESSAGE}></Option>
<Option value={PushType.GROUP_MESSAGE}></Option>
<Option value={PushType.GROUP_ANNOUNCEMENT}></Option>
<Option value={PushTypeCode.FRIEND}></Option>
<Option value={PushTypeCode.GROUP}></Option>
<Option value={PushTypeCode.ANNOUNCEMENT}></Option>
</Select>
<Select
value={statusFilter}
@@ -340,8 +423,8 @@ const PushHistory: React.FC = () => {
suffixIcon={<span></span>}
>
<Option value="all"></Option>
<Option value={PushStatus.PENDING}></Option>
<Option value={PushStatus.COMPLETED}></Option>
<Option value={PushStatus.IN_PROGRESS}></Option>
<Option value={PushStatus.FAILED}></Option>
</Select>
</div>
@@ -353,7 +436,7 @@ const PushHistory: React.FC = () => {
columns={columns}
dataSource={dataSource}
loading={loading}
rowKey="id"
rowKey="workbenchId"
pagination={false}
className={styles.dataTable}
/>

View File

@@ -28,14 +28,30 @@ export function getTrafficPoolList() {
"GET",
);
}
type ListRequestOptions = {
debounceGap?: number;
};
// 好友列表
export function getContactList(params) {
return request("/v1/kefu/wechatFriend/list", params, "GET");
export function getContactList(params, options?: ListRequestOptions) {
return request(
"/v1/kefu/wechatFriend/list",
params,
"GET",
undefined,
options?.debounceGap,
);
}
// 群列表
export function getGroupList(params) {
return request("/v1/kefu/wechatChatroom/list", params, "GET");
export function getGroupList(params, options?: ListRequestOptions) {
return request(
"/v1/kefu/wechatChatroom/list",
params,
"GET",
undefined,
options?.debounceGap,
);
}
// 分组列表
export function getLabelsListByGroup(params) {

Some files were not shown because too many files have changed in this diff Show More