140 Commits

Author SHA1 Message Date
wong
c33c62aa6d Merge branch 'release/v1.1.2' 2025-12-11 09:31:19 +08:00
乘风
581e09d45b Merge branch 'develop' into yongpxu-dev 2025-12-10 18:05:11 +08:00
乘风
688a200999 go 2025-12-10 18:03:51 +08:00
wong
cc89df7957 Merge branch 'feature/group' into develop 2025-12-10 17:58:34 +08:00
wong
198e0434b3 群发 2025-12-10 17:58:08 +08:00
wong
55fe2b46df 代码优化 2025-12-10 17:56:55 +08:00
乘风
c7e934f23d 重构MessageRecord组件以支持虚拟化,从而提高大型消息列表的性能。引入VirtualizedMessageList以实现高效渲染,并使用React.memo优化渲染逻辑以防止不必要的重新渲染。 2025-12-10 17:09:08 +08:00
乘风
7752dd136f 重构消息处理逻辑以提高同步和性能,并增强ProfileCard组件以实现快速回复功能。 2025-12-10 16:02:01 +08:00
乘风
af2f32da4a Update package.json to set module type, refactor postcss.config.js to use ES module syntax, and remove vite.config.ts file. 2025-12-10 15:57:02 +08:00
乘风
81859dfacb gogoo 2025-12-10 11:58:47 +08:00
wong
3e145ca123 算力功能改版 2025-12-09 15:01:38 +08:00
wong
c9a5d3091f 内容库优化 + 内容库导入功能 2025-12-09 14:59:24 +08:00
乘风
4374ad5036 1 2025-12-09 11:55:53 +08:00
乘风
1ca2ca7058 更新技术栈文档,新增TanStack Query和Sentry组件,优化项目结构说明,添加关于兼容性和工程化的详细信息。 2025-12-09 11:11:53 +08:00
wong
9fe15b1bec Merge branch 'feature/groupCreate' into develop 2025-12-06 17:13:53 +08:00
wong
a557319b3e 算力功能 2025-12-06 17:13:07 +08:00
wong
f06eb0e9d7 消息同步 2025-12-06 17:12:07 +08:00
乘风
1be091522e 新增asyncMessageStatus接口以获取消息状态,并在消息处理逻辑中集成该接口以优化消息同步。调整ProfileCard组件的状态管理以支持快速回复功能。 2025-12-06 17:10:57 +08:00
wong
63df35b79c 代码优化 2025-12-05 11:36:03 +08:00
wong
2697da15aa 消息聊天自动分配好友 2025-12-05 10:45:21 +08:00
wong
ced0f993de 分组及好友列表优化 2025-12-05 10:44:43 +08:00
wong
55629a8ce4 Merge branch 'feature/groupPush' into develop 2025-12-04 16:05:38 +08:00
wong
923c6a5904 Merge branch 'feature/bug' into feature/groupPush 2025-12-04 16:02:25 +08:00
wong
b63e87d0cf 1 2025-12-04 16:01:51 +08:00
wong
96caa71f03 内容库可以选择设备 2025-12-04 15:56:19 +08:00
wong
b9b1877e04 自动点赞功能优化 2025-12-04 11:10:55 +08:00
wong
81daa8cfdc 修复备注 2025-12-04 09:43:25 +08:00
乘风
903ea005e3 通过向UpdateDataInfoParams添加conRemark并重构相关组件,增强好友信息更新功能。删除未使用的本地数据库更新逻辑并简化DetailValue组件中的数据处理。 2025-12-03 18:44:42 +08:00
乘风
580e9f4f73 Remove package.json file from MCP directory 2025-12-03 16:56:02 +08:00
乘风
51e08d1b2a Merge branch 'develop' into yongpxu-dev2 2025-12-03 16:55:35 +08:00
乘风
3708f5a61e 优化消息同步处理逻辑,提升系统性能和稳定性。 2025-12-03 16:54:21 +08:00
wong
58377b4670 代码提交 2025-12-03 16:29:40 +08:00
wong
5bb5aa3d5a 代码优化 2025-12-03 16:18:10 +08:00
wong
71def21369 消息优化 2025-12-03 14:06:07 +08:00
乘风
1ea565fbdf 先写好接口 2025-12-03 11:03:38 +08:00
乘风
56a9a015f1 重构ChatWindow和MessageEnter组件以提高性能和可维护性。利用useCallback和useMemo进行备忘录化,增强文件类型处理,并简化消息呈现逻辑。在MessageList中引入SessionItem组件以更好地管理会话。 2025-12-03 09:46:19 +08:00
wong
0708b5a134 Merge branch 'develop' of https://gitee.com/cunkebao/cunkebao_v3 into develop 2025-12-02 17:47:32 +08:00
wong
242065d298 微信分组及消息置顶 2025-12-02 17:47:22 +08:00
乘风
b5d6214521 Remove pin/unpin chat session API methods and update message list handling for pinning sessions. 2025-12-02 15:28:57 +08:00
乘风
9c5bccb23c Merge branch 'develop' into yongpxu-dev 2025-12-02 10:54:28 +08:00
wong
5f5b567749 调整消息同步处理 2025-12-02 10:51:27 +08:00
wong
354d6c4f62 代码优化 2025-12-01 18:09:06 +08:00
wong
2c37314c4f 代码优化 2025-12-01 17:41:57 +08:00
乘风
1dbdee155e 添加高频轮询接口白名单,优化请求截流逻辑以支持特定接口的无截流请求。允许通过配置控制截流功能,提升请求处理的灵活性和用户体验。 2025-12-01 16:58:58 +08:00
wong
2939b63998 1 2025-12-01 16:47:57 +08:00
wong
ff7097f2d9 1 2025-12-01 16:46:23 +08:00
wong
33cd4de3c5 Merge branch 'develop' of https://gitee.com/cunkebao/cunkebao_v3 into wong-dev 2025-12-01 16:44:38 +08:00
wong
0bc6c3a22a 代码优化 2025-12-01 16:41:40 +08:00
wong
44edfe7a81 消息同步优化 2025-12-01 16:25:15 +08:00
wong
d3fae3bbd0 队列优化 2025-12-01 15:42:54 +08:00
乘风
11a4ec8c67 在MessageList组件中添加未知联系人补充功能,异步拉取缺失的头像和昵称信息,并更新会话和联系人数据库,以提升用户信息的准确性和完整性。 2025-12-01 15:09:25 +08:00
乘风
75aab08a72 在MessageList组件中实现对丢失头像或昵称的详细获取,并相应更新会话状态和数据库,以确保准确显示用户信息。 2025-12-01 14:43:33 +08:00
乘风
2cc2184002 增强MessageRecord组件以管理消息加载期间的滚动行为。为消息容器和加载状态引入引用,通过在加载更多消息时保留滚动位置来确保平滑的用户体验。 2025-12-01 14:34:19 +08:00
乘风
57c398f537 更新MessageRecord组件中的消息过滤逻辑,以包括其他消息类型(-10001),以改进系统消息的处理并增强用户体验。 2025-12-01 14:27:39 +08:00
乘风
7d9f94984c 在MessageRecord组件中实现parseSystemMessage实用程序,从系统消息中提取纯文本,通过删除HTML标签并为相关内容添加红包图标来增强消息渲染。 2025-12-01 14:23:10 +08:00
乘风
8659049ae9 增强MessageRecord组件以支持RedPacketMessage和TransferMessage的额外道具,从而更好地处理单击事件和套接字请求。更新禁用的转接消息的样式以改善用户体验。 2025-12-01 12:23:19 +08:00
乘风
1ca5b30c15 Refactor TransferMessage component to improve layout and styling. Adjust icon size and add status display for better user experience. 2025-12-01 10:59:50 +08:00
乘风
b9bc0a71a9 在MessageRecord组件中增加微信转账消息支持,更新消息过滤逻辑。调整RedPacketMessage样式以获得更好的布局。 2025-12-01 10:56:05 +08:00
乘风
13ae8bd76f Enhance message filtering to support WeChat red packet messages, allowing for sender identification in chat records. 2025-12-01 10:38:27 +08:00
wong
31fdabaa15 Merge branch 'develop' of https://gitee.com/cunkebao/cunkebao_v3 into wong-dev
# Conflicts:
#	Server/composer.json
2025-12-01 10:27:35 +08:00
wong
43e9930b45 Merge tag 'v1.1.1-a' into develop 2025-12-01 10:17:25 +08:00
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
wong
4baa82449c Merge branch 'develop' of https://gitee.com/cunkebao/cunkebao_v3 into wong-dev
# Conflicts:
#	Server/composer.json
2025-11-20 16:15:30 +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
5a5160b92d 代码优化 2025-11-20 11:48:31 +08:00
wong
3e78122659 composer依赖提交 2025-11-20 11:45:53 +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
218 changed files with 41540 additions and 5455 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

@@ -1,5 +1,5 @@
import request from "@/api/request";
export function getContentLibraryList(params: any) {
return request("/v1/content/library/list", params, "GET");
return request("/v1/content/library/list", { ...params, formType: 0 }, "GET");
}

View File

@@ -26,4 +26,5 @@ export interface DeviceSelectionProps {
showSelectedList?: boolean; // 新增
readonly?: boolean; // 新增
deviceGroups?: any[]; // 传递设备组数据
singleSelect?: boolean; // 新增,是否单选模式
}

View File

@@ -18,6 +18,7 @@ const DeviceSelection: React.FC<DeviceSelectionProps> = ({
showInput = true,
showSelectedList = true,
readonly = false,
singleSelect = false,
}) => {
// 弹窗控制
const [popupVisible, setPopupVisible] = useState(false);
@@ -37,6 +38,9 @@ const DeviceSelection: React.FC<DeviceSelectionProps> = ({
// 获取显示文本
const getDisplayText = () => {
if (selectedOptions.length === 0) return "";
if (singleSelect && selectedOptions.length > 0) {
return selectedOptions[0].memo || selectedOptions[0].wechatId || "已选择设备";
}
return `已选择 ${selectedOptions.length} 个设备`;
};
@@ -179,6 +183,7 @@ const DeviceSelection: React.FC<DeviceSelectionProps> = ({
onClose={() => setRealVisible(false)}
selectedOptions={selectedOptions}
onSelect={onSelect}
singleSelect={singleSelect}
/>
</>
);

View File

@@ -12,6 +12,7 @@ interface SelectionPopupProps {
onClose: () => void;
selectedOptions: DeviceSelectionItem[];
onSelect: (devices: DeviceSelectionItem[]) => void;
singleSelect?: boolean;
}
const PAGE_SIZE = 20;
@@ -21,6 +22,7 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
onClose,
selectedOptions,
onSelect,
singleSelect = false,
}) => {
// 设备数据
const [devices, setDevices] = useState<DeviceSelectionItem[]>([]);
@@ -110,6 +112,15 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
// 处理设备选择
const handleDeviceToggle = (device: DeviceSelectionItem) => {
if (singleSelect) {
// 单选模式:如果已选中,则取消选择;否则替换为当前设备
if (tempSelectedOptions.some(v => v.id === device.id)) {
setTempSelectedOptions([]);
} else {
setTempSelectedOptions([device]);
}
} else {
// 多选模式:原有的逻辑
if (tempSelectedOptions.some(v => v.id === device.id)) {
setTempSelectedOptions(
tempSelectedOptions.filter(v => v.id !== device.id),
@@ -117,6 +128,7 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
} else {
const newSelectedOptions = [...tempSelectedOptions, device];
setTempSelectedOptions(newSelectedOptions);
}
}
};
@@ -179,6 +191,7 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
totalPages={totalPages}
loading={loading}
selectedCount={tempSelectedOptions.length}
singleSelect={singleSelect}
onPageChange={setCurrentPage}
onCancel={onClose}
onConfirm={() => {
@@ -187,7 +200,7 @@ const SelectionPopup: React.FC<SelectionPopupProps> = ({
onClose();
}}
isAllSelected={isCurrentPageAllSelected}
onSelectAll={handleSelectAllCurrentPage}
onSelectAll={singleSelect ? undefined : handleSelectAllCurrentPage}
/>
}
>

View File

@@ -95,9 +95,9 @@ export default function FriendSelection({
{(selectedOptions || []).map(friend => (
<div key={friend.id} className={style.selectedListRow}>
<div className={style.selectedListRowContent}>
<Avatar src={friend.avatar} />
<Avatar src={friend.avatar || friend.friendAvatar} />
<div className={style.selectedListRowContentText}>
<div>{friend.nickname}</div>
<div>{friend.nickname || friend.friendName}</div>
<div>{friend.wechatId}</div>
</div>
{!readonly && (

View File

@@ -14,6 +14,7 @@ interface PopupFooterProps {
// 全选功能相关
isAllSelected?: boolean;
onSelectAll?: (checked: boolean) => void;
singleSelect?: boolean;
}
const PopupFooter: React.FC<PopupFooterProps> = ({
@@ -26,11 +27,13 @@ const PopupFooter: React.FC<PopupFooterProps> = ({
onConfirm,
isAllSelected = false,
onSelectAll,
singleSelect = false,
}) => {
return (
<>
{/* 分页栏 */}
<div className={style.paginationRow}>
{onSelectAll && (
<div className={style.totalCount}>
<Checkbox
checked={isAllSelected}
@@ -40,6 +43,7 @@ const PopupFooter: React.FC<PopupFooterProps> = ({
</Checkbox>
</div>
)}
<div className={style.paginationControls}>
<Button
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
@@ -61,7 +65,13 @@ const PopupFooter: React.FC<PopupFooterProps> = ({
</div>
</div>
<div className={style.popupFooter}>
<div className={style.selectedCount}> {selectedCount} </div>
<div className={style.selectedCount}>
{singleSelect
? selectedCount > 0
? "已选择设备"
: "未选择设备"
: `已选择 ${selectedCount} 条记录`}
</div>
<div className={style.footerBtnGroup}>
<Button color="primary" variant="filled" onClick={onCancel}>

View File

@@ -149,14 +149,14 @@ const Home: React.FC = () => {
<div className={style["stat-card"]} onClick={handleDevicesClick}>
<div className={style["stat-label"]}></div>
<div className={style["stat-value"]}>
<span>{dashboard.deviceNum || 42}</span>
<span>{dashboard.deviceNum || 0}</span>
<MobileOutlined style={{ fontSize: 20, color: "#3b82f6" }} />
</div>
</div>
<div className={style["stat-card"]} onClick={handleWechatClick}>
<div className={style["stat-label"]}></div>
<div className={style["stat-value"]}>
<span>{dashboard.wechatNum || 42}</span>
<span>{dashboard.wechatNum || 0}</span>
<TeamOutlined style={{ fontSize: 20, color: "#3b82f6" }} />
</div>
</div>
@@ -166,7 +166,7 @@ const Home: React.FC = () => {
>
<div className={style["stat-label"]}>线</div>
<div className={style["stat-value"]}>
<span>{dashboard.aliveWechatNum || 35}</span>
<span>{dashboard.aliveWechatNum || 0}</span>
<LineChartOutlined style={{ fontSize: 20, color: "#3b82f6" }} />
</div>
<div className={style["progress-bar"]}>

View File

@@ -14,7 +14,7 @@ export function getContentLibraryDetail(id: string): Promise<any> {
export function createContentLibrary(
params: CreateContentLibraryParams,
): Promise<any> {
return request("/v1/content/library/create", params, "POST");
return request("/v1/content/library/create", { ...params, formType: 0 }, "POST");
}
// 更新内容库

View File

@@ -1,23 +1,37 @@
.form-page {
background: #f7f8fa;
padding: 16px;
padding-bottom: 100px;
}
.form-main {
max-width: 420px;
margin: 0 auto;
padding: 16px 0 0 0;
padding: 0;
}
.form-section {
margin-bottom: 18px;
&:last-child {
margin-bottom: 0;
}
}
.form-card {
border-radius: 16px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06);
padding: 24px 18px 18px 18px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
padding: 16px;
background: #fff;
margin-bottom: 12px;
}
.source-tag {
font-size: 14px;
padding: 4px 16px;
border-radius: 16px;
margin-top: 8px;
display: inline-block;
}
.form-label {
@@ -32,9 +46,23 @@
font-size: 16px;
font-weight: 700;
color: #222;
margin-top: 28px;
margin-top: 20px;
margin-bottom: 12px;
letter-spacing: 0.5px;
padding-left: 8px;
position: relative;
&::before {
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 16px;
background: #1890ff;
border-radius: 2px;
}
}
.section-block {
@@ -47,40 +75,78 @@
.adm-tabs-header {
background: #f7f8fa;
border-radius: 8px;
margin-bottom: 8px;
margin-bottom: 12px;
padding: 4px;
}
.adm-tabs-tab {
font-size: 15px;
font-size: 14px;
font-weight: 500;
padding: 8px 0;
padding: 8px 20px;
border-radius: 6px;
transition: all 0.2s;
&.adm-tabs-tab-active {
background: #1890ff;
color: #fff;
font-weight: 600;
}
}
.adm-tabs-content {
padding: 0;
}
}
.collapse {
margin-top: 12px;
.keyword-collapse {
.adm-collapse-panel-header {
padding: 0;
background: transparent;
border: none;
}
.adm-collapse-panel-content {
padding-bottom: 8px;
background: #f8fafc;
border-radius: 10px;
padding: 18px 14px 10px 14px;
margin-top: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
padding: 16px 0 0 0;
background: transparent;
}
.form-section {
margin-bottom: 22px;
margin-bottom: 18px;
&:last-child {
margin-bottom: 0;
}
}
.form-label {
font-size: 15px;
font-weight: 500;
margin-bottom: 4px;
margin-bottom: 8px;
color: #333;
display: block;
}
.adm-input {
min-height: 42px;
font-size: 15px;
border-radius: 7px;
margin-bottom: 2px;
}
}
.keyword-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0;
}
.keyword-title {
font-size: 16px;
font-weight: 600;
color: #222;
}
.keyword-arrow {
font-size: 12px;
color: #999;
transition: transform 0.3s;
}
.keyword-collapse .adm-collapse-panel-active .keyword-arrow {
transform: rotate(180deg);
}
.ai-row,
@@ -91,9 +157,53 @@
}
.ai-desc {
color: #888;
font-size: 13px;
color: #666;
font-size: 14px;
flex: 1;
line-height: 1.5;
}
.content-type-header {
margin-bottom: 12px;
}
.content-type-title {
font-size: 16px;
font-weight: 600;
color: #222;
}
.content-type-buttons {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.content-type-btn {
padding: 10px 24px;
border: none;
border-radius: 8px;
background: #1890ff;
color: #fff;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
outline: none;
&:not(.active) {
background: #f5f5f5;
color: #666;
}
&:hover {
opacity: 0.9;
}
&.active {
background: #1890ff;
color: #fff;
}
}
.date-row,
@@ -109,6 +219,179 @@
border-radius: 8px;
}
.time-limit-header {
margin-bottom: 12px;
}
.time-limit-title {
font-size: 16px;
font-weight: 600;
color: #222;
}
.date-inputs {
display: flex;
gap: 16px;
align-items: flex-start;
}
.date-item {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.date-label {
font-size: 14px;
color: #666;
white-space: nowrap;
}
.date-input {
width: 100%;
.ant-input {
background: #f5f5f5;
border: 1px solid #e5e6eb;
border-radius: 8px;
padding: 10px 12px;
font-size: 14px;
color: #222;
cursor: pointer;
&:hover {
border-color: #1890ff;
}
&:focus {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
}
}
}
.enable-section {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0;
}
.enable-label {
font-size: 15px;
font-weight: 500;
color: #222;
}
.device-card {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border: 1px solid #e5e6eb;
border-radius: 8px;
background: #fff;
cursor: pointer;
margin-top: 8px;
transition: all 0.2s;
&:hover {
border-color: #1890ff;
}
}
.device-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
flex-shrink: 0;
.avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-text {
font-size: 16px;
color: #fff;
font-weight: 700;
}
}
.device-info {
flex: 1;
min-width: 0;
}
.device-name-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.device-name {
font-size: 15px;
font-weight: 500;
color: #222;
}
.device-tag {
font-size: 12px;
padding: 2px 8px;
background: #f5f5f5;
color: #666;
border-radius: 4px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
&.online {
background: #52c41a;
}
&.offline {
background: #ff4d4f;
}
}
.device-wechat-id {
font-size: 13px;
color: #888;
}
.device-arrow {
color: #999;
font-size: 14px;
}
.device-input-wrapper {
margin-top: 8px;
}
.device-input {
width: 100%;
}
.keyword-select {
width: 100%;
.ant-select-selector {
min-height: 44px;
border-radius: 8px;
}
}
.submit-btn {
margin-top: 32px;
height: 48px !important;

View File

@@ -1,16 +1,19 @@
import React, { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Input as AntdInput, Switch } from "antd";
import { Input as AntdInput, Switch, Tag } from "antd";
import { Button, Collapse, Toast, DatePicker, Tabs } from "antd-mobile";
import { DownOutlined } from "@ant-design/icons";
import NavCommon from "@/components/NavCommon";
import FriendSelection from "@/components/FriendSelection";
import GroupSelection from "@/components/GroupSelection";
import DeviceSelection from "@/components/DeviceSelection";
import Layout from "@/components/Layout/Layout";
import style from "./index.module.scss";
import request from "@/api/request";
import { getContentLibraryDetail, updateContentLibrary } from "./api";
import { GroupSelectionItem } from "@/components/GroupSelection/data";
import { FriendSelectionItem } from "@/components/FriendSelection/data";
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
const { TextArea } = AntdInput;
@@ -29,6 +32,15 @@ export default function ContentForm() {
const isEdit = !!id;
const [sourceType, setSourceType] = useState<"friends" | "groups">("friends");
const [name, setName] = useState("");
const [selectedDevices, setSelectedDevices] = useState<DeviceSelectionItem[]>([]);
const [deviceSelectionVisible, setDeviceSelectionVisible] = useState(false);
// 处理设备选择(单选模式)
const handleDeviceSelect = (devices: DeviceSelectionItem[]) => {
// 单选模式:只保留最后一个选中的设备
setSelectedDevices(devices.length > 0 ? [devices[devices.length - 1]] : []);
setDeviceSelectionVisible(false);
};
const [friendsGroups, setSelectedFriends] = useState<string[]>([]);
const [friendsGroupsOptions, setSelectedFriendsOptions] = useState<
FriendSelectionItem[]
@@ -64,6 +76,23 @@ export default function ContentForm() {
.then(data => {
setName(data.name || "");
setSourceType(data.sourceType === 1 ? "friends" : "groups");
// 详情接口中的采集设备数据可能在 devices / selectedDevices / deviceGroupsOptions 中
const deviceOptions: DeviceSelectionItem[] =
data.devices ||
data.selectedDevices ||
(data.deviceGroupsOptions
? (data.deviceGroupsOptions as any[]).map(item => ({
id: item.id,
memo: item.memo,
imei: item.imei,
wechatId: item.wechatId,
status: item.alive === 1 ? "online" : "offline",
nickname: item.nickname,
avatar: item.avatar,
totalFriend: item.totalFriend,
}))
: []);
setSelectedDevices(deviceOptions || []);
setSelectedFriends(data.sourceFriends || []);
setSelectedGroups(data.selectedGroups || []);
setSelectedGroupsOptions(data.selectedGroupsOptions || []);
@@ -72,7 +101,13 @@ export default function ContentForm() {
setKeywordsExclude((data.keywordExclude || []).join(","));
setCatchType(data.catchType || ["text", "image", "video"]);
setAIPrompt(data.aiPrompt || "");
// aiEnabled 为 AI 提示词开关1 开启 0 关闭
if (typeof data.aiEnabled !== "undefined") {
setUseAI(data.aiEnabled === 1);
} else {
// 兼容旧数据,默认根据是否有 aiPrompt 判断
setUseAI(!!data.aiPrompt);
}
setEnabled(data.status === 1);
// 时间范围
const start = data.timeStart || data.startTime;
@@ -103,6 +138,7 @@ export default function ContentForm() {
const payload = {
name,
sourceType: sourceType === "friends" ? 1 : 2,
devices: selectedDevices.map(d => d.id),
friendsGroups: friendsGroups,
wechatGroups: selectedGroups,
groupMembers: {},
@@ -116,6 +152,7 @@ export default function ContentForm() {
.filter(Boolean),
catchType,
aiPrompt,
aiEnabled: useAI ? 1 : 0,
timeEnabled: dateRange[0] || dateRange[1] ? 1 : 0,
startTime: dateRange[0] ? formatDate(dateRange[0]) : "",
endTime: dateRange[1] ? formatDate(dateRange[1]) : "",
@@ -125,7 +162,7 @@ export default function ContentForm() {
await updateContentLibrary({ id, ...payload });
Toast.show({ content: "保存成功", position: "top" });
} else {
await request("/v1/content/library/create", payload, "POST");
await request("/v1/content/library/create", { ...payload, formType: 0 }, "POST");
Toast.show({ content: "创建成功", position: "top" });
}
navigate("/mine/content");
@@ -178,7 +215,7 @@ export default function ContentForm() {
onSubmit={e => e.preventDefault()}
autoComplete="off"
>
<div className={style["form-section"]}>
<div className={style["form-card"]}>
<label className={style["form-label"]}>
<span style={{ color: "#ff4d4f", marginRight: 4 }}>*</span>
@@ -191,8 +228,100 @@ export default function ContentForm() {
/>
</div>
<div className={style["section-title"]}></div>
<div className={style["form-section"]}>
<div className={style["form-card"]}>
<label className={style["form-label"]}>
<span style={{ color: "#ff4d4f", marginRight: 4 }}>*</span>
</label>
<Tag color="blue" className={style["source-tag"]}>
</Tag>
</div>
<div className={style["form-card"]}>
<label className={style["form-label"]}>
<span style={{ color: "#ff4d4f", marginRight: 4 }}>*</span>
</label>
{selectedDevices.length > 0 ? (
<div
className={style["device-card"]}
onClick={() => setDeviceSelectionVisible(true)}
>
<div className={style["device-avatar"]}>
{selectedDevices[0].avatar ? (
<img
src={selectedDevices[0].avatar}
alt="头像"
className={style["avatar-img"]}
/>
) : (
<span className={style["avatar-text"]}>
{(selectedDevices[0].memo ||
selectedDevices[0].wechatId ||
"设")[0]}
</span>
)}
</div>
<div className={style["device-info"]}>
<div className={style["device-name-row"]}>
<span className={style["device-name"]}>
{selectedDevices[0].nickname || selectedDevices[0].memo}
</span>
{selectedDevices[0].memo &&
selectedDevices[0].nickname &&
selectedDevices[0].memo !== selectedDevices[0].nickname && (
<span className={style["device-tag"]}>
{selectedDevices[0].memo}
</span>
)}
<span
className={`${style["status-dot"]} ${
selectedDevices[0].status === "online"
? style["online"]
: style["offline"]
}`}
/>
</div>
<div className={style["device-wechat-id"]}>
ID: {selectedDevices[0].wechatId}
</div>
</div>
<div className={style["device-arrow"]}>
<DownOutlined />
</div>
</div>
) : (
<div className={style["device-input-wrapper"]}>
<DeviceSelection
selectedOptions={selectedDevices}
onSelect={handleDeviceSelect}
placeholder="选择采集设备"
showInput={true}
showSelectedList={false}
singleSelect={true}
className={style["device-input"]}
/>
</div>
)}
{/* 隐藏的设备选择组件,用于打开弹窗 */}
<div style={{ display: "none" }}>
<DeviceSelection
selectedOptions={selectedDevices}
onSelect={handleDeviceSelect}
placeholder="选择采集设备"
showInput={false}
showSelectedList={false}
singleSelect={true}
mode="dialog"
open={deviceSelectionVisible}
onOpenChange={setDeviceSelectionVisible}
/>
</div>
</div>
<div className={style["section-title"]}></div>
<div className={style["form-card"]}>
<Tabs
activeKey={sourceType}
onChange={key => setSourceType(key as "friends" | "groups")}
@@ -203,6 +332,7 @@ export default function ContentForm() {
selectedOptions={friendsGroupsOptions}
onSelect={handleFriendsChange}
placeholder="选择微信好友"
deviceIds={selectedDevices.map(d => Number(d.id))}
/>
</Tabs.Tab>
<Tabs.Tab title="选择聊天群" key="groups">
@@ -215,10 +345,16 @@ export default function ContentForm() {
</Tabs>
</div>
<Collapse className={style["collapse"]}>
<div className={style["form-card"]}>
<Collapse className={style["keyword-collapse"]}>
<Collapse.Panel
key="keywords"
title={<span className={style["form-label"]}></span>}
title={
<div className={style["keyword-header"]}>
<span className={style["keyword-title"]}></span>
<DownOutlined className={style["keyword-arrow"]} />
</div>
}
>
<div className={style["form-section"]}>
<label className={style["form-label"]}></label>
@@ -242,27 +378,20 @@ export default function ContentForm() {
</div>
</Collapse.Panel>
</Collapse>
</div>
{/* 采集内容类型 */}
<div className={style["section-title"]}></div>
<div className={style["form-section"]}>
<div style={{ display: "flex", flexWrap: "wrap", gap: 12 }}>
<div className={style["form-card"]}>
<div className={style["content-type-header"]}>
<span className={style["content-type-title"]}></span>
</div>
<div className={style["content-type-buttons"]}>
{["text", "image", "video"].map(type => (
<div
<button
key={type}
style={{
display: "flex",
alignItems: "center",
gap: 8,
padding: "8px 12px",
border: "1px solid #d9d9d9",
borderRadius: "6px",
backgroundColor: catchType.includes(type)
? "#1890ff"
: "#fff",
color: catchType.includes(type) ? "#fff" : "#333",
cursor: "pointer",
}}
className={`${style["content-type-btn"]} ${
catchType.includes(type) ? style["active"] : ""
}`}
onClick={() => {
setCatchType(prev =>
prev.includes(type)
@@ -271,26 +400,24 @@ export default function ContentForm() {
);
}}
>
<span>
{type === "text"
? "文本"
: type === "image"
? "图片"
: "视频"}
</span>
</div>
</button>
))}
</div>
</div>
<div className={style["section-title"]}>AI</div>
<div className={style["form-card"]}>
<div
className={style["form-section"]}
style={{ display: "flex", alignItems: "center", gap: 12 }}
>
<Switch checked={useAI} onChange={setUseAI} />
<span className={style["ai-desc"]}>
AI后AI生成
,AI生成
</span>
</div>
{useAI && (
@@ -305,19 +432,24 @@ export default function ContentForm() {
/>
</div>
)}
</div>
<div className={style["section-title"]}></div>
<div
className={style["form-section"]}
style={{ display: "flex", gap: 12 }}
>
<label></label>
<div style={{ flex: 1 }}>
<div className={style["form-card"]}>
<div className={style["time-limit-header"]}>
<span className={style["time-limit-title"]}></span>
</div>
<div className={style["date-inputs"]}>
<div className={style["date-item"]}>
<label className={style["date-label"]}></label>
<AntdInput
readOnly
value={dateRange[0] ? dateRange[0].toLocaleDateString() : ""}
value={
dateRange[0]
? `${dateRange[0].getFullYear()}/${String(dateRange[0].getMonth() + 1).padStart(2, "0")}/${String(dateRange[0].getDate()).padStart(2, "0")}`
: ""
}
placeholder="年/月/日"
className={style["input"]}
className={style["date-input"]}
onClick={() => setShowStartPicker(true)}
/>
<DatePicker
@@ -331,13 +463,17 @@ export default function ContentForm() {
}}
/>
</div>
<label></label>
<div style={{ flex: 1 }}>
<div className={style["date-item"]}>
<label className={style["date-label"]}></label>
<AntdInput
readOnly
value={dateRange[1] ? dateRange[1].toLocaleDateString() : ""}
value={
dateRange[1]
? `${dateRange[1].getFullYear()}/${String(dateRange[1].getMonth() + 1).padStart(2, "0")}/${String(dateRange[1].getDate()).padStart(2, "0")}`
: ""
}
placeholder="年/月/日"
className={style["input"]}
className={style["date-input"]}
onClick={() => setShowEndPicker(true)}
/>
<DatePicker
@@ -350,21 +486,15 @@ export default function ContentForm() {
setShowEndPicker(false);
}}
/>
</div>
</div>
</div>
<div
className={style["section-title"]}
style={{
marginTop: 24,
marginBottom: 8,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<span></span>
<div className={style["form-card"]}>
<div className={style["enable-section"]}>
<span className={style["enable-label"]}></span>
<Switch checked={enabled} onChange={setEnabled} />
</div>
</div>
</form>
</div>

View File

@@ -12,7 +12,7 @@ export function getContentLibraryList(params: {
keyword?: string;
sourceType?: number;
}): Promise<any> {
return request("/v1/content/library/list", params, "GET");
return request("/v1/content/library/list", { ...params, formType: 0 }, "GET");
}
// 获取内容库详情
@@ -24,7 +24,7 @@ export function getContentLibraryDetail(id: string): Promise<any> {
export function createContentLibrary(
params: CreateContentLibraryParams,
): Promise<any> {
return request("/v1/content/library/create", params, "POST");
return request("/v1/content/library/create", { ...params, formType: 0 }, "POST");
}
// 更新内容库

View File

@@ -3,6 +3,8 @@ import {
GetContentItemListParams,
CreateContentItemParams,
UpdateContentItemParams,
AIRewriteParams,
ReplaceContentParams,
} from "./data";
// 获取素材列表
@@ -35,3 +37,21 @@ 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");
}
// 导入Excel素材
export function importMaterialsFromExcel(params: {
id: string;
fileUrl: string;
}) {
return request("/v1/content/library/import-excel", 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,231 @@
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;
}
}
}
}
}
// 导入弹窗样式
.import-popup-content {
padding: 20px;
max-height: 90vh;
overflow-y: auto;
background: #ffffff;
.import-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;
}
}
}
.import-form {
.import-form-item {
margin-bottom: 20px;
.import-form-label {
font-size: 15px;
font-weight: 500;
color: #333;
margin-bottom: 8px;
display: flex;
align-items: center;
&::before {
content: '';
display: inline-block;
width: 3px;
height: 14px;
background: #1677ff;
margin-right: 6px;
border-radius: 2px;
}
}
.import-form-control {
.import-tip {
font-size: 12px;
color: #999;
margin-top: 8px;
line-height: 1.5;
}
}
}
.import-actions {
margin-top: 24px;
button {
height: 44px;
font-size: 16px;
border-radius: 8px;
&:first-child {
box-shadow: 0 2px 6px rgba(22, 119, 255, 0.2);
}
}
}
}
}

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,
@@ -15,10 +15,12 @@ import {
VideoCameraOutlined,
FileTextOutlined,
AppstoreOutlined,
UploadOutlined,
} 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, importMaterialsFromExcel } from "./api";
import FileUpload from "@/components/Upload/FileUpload";
import { ContentItem } from "./data";
import style from "./index.module.scss";
@@ -42,6 +44,19 @@ 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 [showImportPopup, setShowImportPopup] = useState(false);
const [importFileUrl, setImportFileUrl] = useState<string>("");
const [importLoading, setImportLoading] = useState(false);
// 获取素材列表
const fetchMaterials = useCallback(async () => {
if (!id) return;
@@ -104,15 +119,139 @@ 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 = () => {
fetchMaterials();
};
// 处理导入文件上传
const handleImportFileChange = (fileInfo: { fileName: string; fileUrl: string }) => {
setImportFileUrl(fileInfo.fileUrl);
};
// 执行导入
const handleImport = async () => {
if (!id) {
Toast.show({
content: "内容库ID不存在",
position: "top",
});
return;
}
if (!importFileUrl) {
Toast.show({
content: "请先上传Excel文件",
position: "top",
});
return;
}
try {
setImportLoading(true);
await importMaterialsFromExcel({
id: id,
fileUrl: importFileUrl,
});
Toast.show({
content: "导入成功",
position: "top",
});
// 关闭弹窗并重置状态
setShowImportPopup(false);
setImportFileUrl("");
// 刷新素材列表
fetchMaterials();
} catch (error: unknown) {
console.error("导入失败:", error);
Toast.show({
content: error instanceof Error ? error.message : "导入失败,请重试",
position: "top",
});
} finally {
setImportLoading(false);
}
};
// 关闭导入弹窗
const closeImportPopup = () => {
setShowImportPopup(false);
setImportFileUrl("");
};
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
@@ -280,9 +419,18 @@ const MaterialsList: React.FC = () => {
title="素材管理"
backFn={() => navigate("/mine/content")}
right={
<Button type="primary" onClick={handleCreateNew}>
<PlusOutlined />
</Button>
<>
<Button
type="default"
onClick={() => setShowImportPopup(true)}
style={{ marginRight: 8 }}
>
<UploadOutlined />
</Button>
<Button type="primary" onClick={handleCreateNew}>
<PlusOutlined />
</Button>
</>
}
/>
{/* 搜索栏 */}
@@ -348,7 +496,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 +545,7 @@ const MaterialsList: React.FC = () => {
</Button>
<Button
onClick={() => handleView(material.id)}
onClick={() => handleAIRewrite(material)}
className={style["action-btn"]}
>
<BarChartOutlined />
@@ -402,6 +566,165 @@ 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>
{/* 导入弹窗 */}
<Popup
visible={showImportPopup}
onMaskClick={closeImportPopup}
bodyStyle={{
borderRadius: "16px 16px 0 0",
maxHeight: "90vh",
}}
>
<div className={style["import-popup-content"]}>
<div className={style["import-popup-header"]}>
<h3></h3>
<Button
size="small"
onClick={closeImportPopup}
>
</Button>
</div>
<div className={style["import-form"]}>
<div className={style["import-form-item"]}>
<div className={style["import-form-label"]}>Excel文件</div>
<div className={style["import-form-control"]}>
<FileUpload
value={importFileUrl}
onChange={(url) => {
const fileUrl = Array.isArray(url) ? url[0] : url;
setImportFileUrl(fileUrl || "");
}}
acceptTypes={["excel"]}
maxSize={50}
maxCount={1}
showPreview={false}
/>
<div className={style["import-tip"]}>
Excel格式的文件50MB
</div>
</div>
</div>
<div className={style["import-actions"]}>
<Button
block
color="primary"
onClick={handleImport}
loading={importLoading}
disabled={importLoading || !importFileUrl}
>
{importLoading ? "导入中..." : "确认导入"}
</Button>
<Button
block
color="danger"
fill="outline"
onClick={closeImportPopup}
style={{ marginTop: 12 }}
>
</Button>
</div>
</div>
</div>
</Popup>
</Layout>
);
};

View File

@@ -42,3 +42,7 @@ export const fetchDeviceQRCode = (accountId: string) =>
// 通过IMEI添加设备
export const addDeviceByImei = (imei: string, name: string) =>
request("/v1/api/device/add-by-imei", { imei, name }, "POST");
// 获取设备添加结果(用于轮询检查)
export const fetchAddResults = (params: { accountId?: string }) =>
request("/v1/devices/add-results", params, "GET");

View File

@@ -15,6 +15,7 @@ import {
fetchDeviceQRCode,
addDeviceByImei,
deleteDevice,
fetchAddResults,
} from "./api";
import type { Device } from "@/types/device";
import { comfirm } from "@/utils/common";
@@ -44,12 +45,18 @@ const Devices: React.FC = () => {
const [name, setName] = useState("");
const [addLoading, setAddLoading] = useState(false);
// 轮询监听相关
const [isPolling, setIsPolling] = useState(false);
const pollingRef = useRef<NodeJS.Timeout | null>(null);
const loadDevicesRef = useRef<((reset?: boolean) => Promise<void>) | null>(null);
// 删除弹窗
const [delVisible, setDelVisible] = useState(false);
const [delLoading, setDelLoading] = useState(false);
const navigate = useNavigate();
const { user } = useUserStore();
// 加载设备列表
const loadDevices = useCallback(
async (reset = false) => {
@@ -74,6 +81,11 @@ const Devices: React.FC = () => {
[loading, search, page],
);
// 更新 loadDevices 的 ref
useEffect(() => {
loadDevicesRef.current = loadDevices;
}, [loadDevices]);
// 首次加载和搜索
useEffect(() => {
loadDevices(true);
@@ -110,6 +122,56 @@ const Devices: React.FC = () => {
return true;
});
// 开始轮询监听设备状态
const startPolling = useCallback(() => {
if (isPolling) return;
setIsPolling(true);
const pollDeviceStatus = async () => {
try {
const res = await fetchAddResults({ accountId: user?.s2_accountId });
if (res.added) {
Toast.show({ content: "设备添加成功!", position: "top" });
setAddVisible(false);
setIsPolling(false);
if (pollingRef.current) {
clearInterval(pollingRef.current);
pollingRef.current = null;
}
// 刷新设备列表
if (loadDevicesRef.current) {
await loadDevicesRef.current(true);
}
return;
}
} catch (error) {
console.error("轮询检查设备状态失败:", error);
}
};
// 每3秒检查一次设备状态
pollingRef.current = setInterval(pollDeviceStatus, 3000);
}, [isPolling, user?.s2_accountId]);
// 停止轮询
const stopPolling = useCallback(() => {
setIsPolling(false);
if (pollingRef.current) {
clearInterval(pollingRef.current);
pollingRef.current = null;
}
}, []);
// 组件卸载时清理轮询
useEffect(() => {
return () => {
if (pollingRef.current) {
clearInterval(pollingRef.current);
}
};
}, []);
// 获取二维码
const handleGetQr = async () => {
setQrLoading(true);
@@ -119,6 +181,8 @@ const Devices: React.FC = () => {
if (!accountId) throw new Error("未获取到用户信息");
const res = await fetchDeviceQRCode(accountId);
setQrCode(res.qrCode);
// 获取二维码后开始轮询监听
startPolling();
} catch (e: any) {
Toast.show({ content: e.message || "获取二维码失败", position: "top" });
} finally {
@@ -362,7 +426,11 @@ const Devices: React.FC = () => {
{/* 添加设备弹窗 */}
<Popup
visible={addVisible}
onMaskClick={() => setAddVisible(false)}
onMaskClick={() => {
setAddVisible(false);
stopPolling();
setQrCode(null);
}}
bodyStyle={{
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
@@ -403,6 +471,13 @@ const Devices: React.FC = () => {
<div style={{ color: "#888", fontSize: 12, marginTop: 8 }}>
</div>
{isPolling && (
<div
style={{ color: "#1890ff", fontSize: 12, marginTop: 8 }}
>
...
</div>
)}
</div>
)}
</div>

View File

@@ -4,7 +4,7 @@ import request from "@/api/request";
export interface PowerPackage {
id: number;
name: string;
tokens: number; // 算力点数
tokens: number | string; // 算力点数(可能是字符串,如"2,800"
price: number; // 价格(分)
originalPrice: number; // 原价(分)
unitPrice: number; // 单价
@@ -13,7 +13,7 @@ export interface PowerPackage {
isRecommend: number; // 是否推荐
isHot: number; // 是否热门
isVip: number; // 是否VIP
features: string[]; // 功能特性
features?: string[]; // 功能特性(可选)
description: string[]; // 描述关键词
status: number;
createTime: string;

View File

@@ -6,6 +6,9 @@ export interface Statistics {
monthUsed: number; // 本月使用
remainingTokens: number; // 剩余算力
totalConsumed: number; // 总消耗
yesterdayUsed?: number; // 昨日消耗
historyConsumed?: number; // 历史消耗
estimatedDays?: number; // 预计可用天数
}
// 算力统计接口
export function getStatistics(): Promise<Statistics> {
@@ -143,3 +146,56 @@ export function buyPackage(params: { id: number; price: number }) {
export function buyCustomPower(params: { amount: number }) {
return request("/v1/power/buy-custom", params, "POST");
}
// 查询订单状态
export interface QueryOrderResponse {
id: number;
mchId: number;
companyId: number;
userId: number;
orderType: number;
status: number; // 0: 待支付, 1: 已支付
goodsId: number;
goodsName: string;
goodsSpecs: string;
money: number;
orderNo: string;
payType: number | null;
payTime: number | null;
payInfo: any;
createTime: number;
}
export function queryOrder(orderNo: string): Promise<QueryOrderResponse> {
return request("/v1/tokens/queryOrder", { orderNo }, "GET");
}
// 账号信息
export interface Account {
id: number;
uid?: number; // 用户ID用于分配算力
userId?: number; // 用户ID别名
userName: string;
realName: string;
nickname: string;
departmentId: number;
departmentName: string;
avatar: string;
}
// 获取账号列表
export function getAccountList(): Promise<{ list: Account[]; total: number }> {
return request("/v1/kefu/accounts/list", undefined, "GET");
}
// 分配算力接口参数
export interface AllocateTokensParams {
targetUserId: number; // 目标用户ID
tokens: number; // 分配的算力数量
remarks?: string; // 备注
}
// 分配算力
export function allocateTokens(params: AllocateTokensParams): Promise<any> {
return request("/v1/tokens/allocate", params, "POST");
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -36,6 +36,7 @@ export interface TokensUseRecordItem {
export interface TokensUseRecordList {
list: TokensUseRecordItem[];
total?: number;
}
//算力使用明细

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;
@@ -33,6 +49,113 @@ export function transferWechatFriends(params: {
wechatId: string;
devices: number[];
inherit: boolean;
greeting?: string;
firstMessage?: string;
}) {
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,
},
}
);
// 检查响应类型如果是JSON错误响应需要解析错误信息
const contentType = response.headers["content-type"] || "";
if (contentType.includes("application/json")) {
// 如果是JSON响应说明可能是错误信息
const text = await response.data.text();
const errorData = JSON.parse(text);
throw new Error(errorData.message || errorData.msg || "导出失败");
}
// 检查响应状态
if (response.status !== 200) {
throw new Error(`导出失败,状态码: ${response.status}`);
}
// 创建下载链接
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) {
// 如果是我们抛出的错误,直接抛出
if (error.message && error.message !== "导出失败") {
throw error;
}
// 处理axios错误响应
if (error.response) {
// 如果响应是blob类型尝试读取为文本
if (error.response.data instanceof Blob) {
try {
const text = await error.response.data.text();
const errorData = JSON.parse(text);
throw new Error(errorData.message || errorData.msg || "导出失败");
} catch (parseError) {
throw new Error("导出失败,请重试");
}
} else {
throw new Error(
error.response.data?.message ||
error.response.data?.msg ||
error.message ||
"导出失败"
);
}
} else {
throw new Error(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 {

File diff suppressed because it is too large Load Diff

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

View File

@@ -70,24 +70,72 @@ const PlanApi: React.FC<PlanApiProps> = ({
// 处理webhook URL确保包含完整的API地址
const fullWebhookUrl = useMemo(() => {
return buildApiUrl(webhookUrl);
return buildApiUrl('');
}, [webhookUrl]);
// 生成测试URL
// 快速测试使用的 GET 地址(携带示例查询参数,方便在浏览器中直接访问)
const testUrl = useMemo(() => {
if (!fullWebhookUrl) return "";
return `${fullWebhookUrl}?name=测试客户&phone=13800138000&source=API测试`;
}, [fullWebhookUrl]);
return buildApiUrl(webhookUrl);
}, [webhookUrl]);
// 检测是否为移动端
const isMobile = window.innerWidth <= 768;
const handleCopy = (text: string, type: string) => {
navigator.clipboard.writeText(text);
Toast.show({
content: `${type}已复制到剪贴板`,
position: "top",
});
// 先尝试使用 Clipboard API
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard
.writeText(text)
.then(() => {
Toast.show({
content: `${type}已复制到剪贴板`,
position: "top",
});
})
.catch(() => {
// 回退到传统的 textarea 复制方式
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.left = "-9999px";
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand("copy");
Toast.show({
content: `${type}已复制到剪贴板`,
position: "top",
});
} catch {
Toast.show({
content: `${type}复制失败,请手动复制`,
position: "top",
});
}
document.body.removeChild(textarea);
});
} else {
// 不支持 Clipboard API 时直接使用回退方案
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.left = "-9999px";
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand("copy");
Toast.show({
content: `${type}已复制到剪贴板`,
position: "top",
});
} catch {
Toast.show({
content: `${type}复制失败,请手动复制`,
position: "top",
});
}
document.body.removeChild(textarea);
}
};
const handleTestInBrowser = () => {
@@ -96,7 +144,7 @@ const PlanApi: React.FC<PlanApiProps> = ({
const renderConfigTab = () => (
<div className={style["config-content"]}>
{/* API密钥配置 */}
{/* 鉴权参数配置 */}
<div className={style["config-section"]}>
<div className={style["section-header"]}>
<div className={style["section-title"]}>
@@ -122,7 +170,7 @@ const PlanApi: React.FC<PlanApiProps> = ({
</div>
</div>
{/* 接口地址配置 */}
{/* 接口地址与参数说明 */}
<div className={style["config-section"]}>
<div className={style["section-header"]}>
<div className={style["section-title"]}>
@@ -150,27 +198,42 @@ const PlanApi: React.FC<PlanApiProps> = ({
{/* 参数说明 */}
<div className={style["params-grid"]}>
<div className={style["param-section"]}>
<h4></h4>
<h4></h4>
<div className={style["param-list"]}>
<div>
<code>name</code> -
<code>apiKey</code> -
</div>
<div>
<code>phone</code> -
<code>sign</code> -
</div>
<div>
<code>timestamp</code> - 5
</div>
</div>
</div>
<div className={style["param-section"]}>
<h4></h4>
<h4></h4>
<div className={style["param-list"]}>
<div>
<code>source</code> -
<code>wechatId</code> -
</div>
<div>
<code>phone</code> - <code>wechatId</code>
</div>
<div>
<code>name</code> -
</div>
<div>
<code>source</code> - 线广
</div>
<div>
<code>remark</code> -
</div>
<div>
<code>tags</code> -
<code>tags</code> - <code>"高意向,电商,女装"</code>
</div>
<div>
<code>siteTags</code> -
</div>
</div>
</div>
@@ -179,93 +242,131 @@ const PlanApi: React.FC<PlanApiProps> = ({
</div>
);
const renderQuickTestTab = () => (
<div className={style["test-content"]}>
<div className={style["test-section"]}>
<h3>URL</h3>
<div className={style["input-group"]}>
<Input value={testUrl} disabled className={style["test-input"]} />
</div>
<div className={style["test-buttons"]}>
<Button
onClick={() => handleCopy(testUrl, "测试URL")}
className={style["test-btn"]}
>
<CopyOutlined />
URL
</Button>
<Button
type="primary"
onClick={handleTestInBrowser}
className={style["test-btn"]}
>
</Button>
const renderQuickTestTab = () => {
return (
<div className={style["test-content"]}>
<div className={style["test-section"]}>
<h3> URLGET </h3>
<div className={style["input-group"]}>
<Input value={testUrl} disabled className={style["test-input"]} />
</div>
<div className={style["test-buttons"]}>
<Button
onClick={() => handleCopy(testUrl, "测试URL")}
className={style["test-btn"]}
>
<CopyOutlined />
URL
</Button>
<Button
color="primary"
onClick={handleTestInBrowser}
className={style["test-btn"]}
>
</Button>
</div>
</div>
</div>
</div>
);
);
};
const renderDocsTab = () => (
<div className={style["docs-content"]}>
<div className={style["docs-grid"]}>
<Card className={style["doc-card"]}>
<div className={style["doc-icon"]}>
<BookOutlined />
</div>
<h4>API文档</h4>
<p></p>
</Card>
<Card className={style["doc-card"]}>
<div className={style["doc-icon"]}>
<LinkOutlined />
</div>
<h4></h4>
<p></p>
</Card>
const renderDocsTab = () => {
const docUrl = `${import.meta.env.VITE_API_BASE_URL}/doc/api_v1.md`;
return (
<div className={style["docs-content"]}>
<div className={style["docs-grid"]}>
<Card className={style["doc-card"]}>
<div className={style["doc-icon"]}>
<BookOutlined />
</div>
<h4>线V1</h4>
<p></p>
<div className={style["doc-actions"]}>
<Button
size="small"
onClick={() => {
window.open(docUrl, "_blank");
}}
className={style["doc-open-btn"]}
>
</Button>
<Button
size="small"
onClick={() => handleCopy(docUrl, "文档链接")}
className={style["doc-copy-btn"]}
>
<CopyOutlined />
</Button>
</div>
</Card>
</div>
</div>
</div>
);
);
};
const renderCodeTab = () => {
const codeExamples = {
javascript: `fetch('${fullWebhookUrl}', {
javascript: `// 参考 api_v1 文档示例,使用 JSON 方式 POST
fetch('${fullWebhookUrl}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ${apiKey}'
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: '张三',
apiKey: '${apiKey}',
timestamp: 1710000000, // 秒级时间戳
phone: '13800138000',
name: '张三',
source: '官网表单',
remark: '通过H5表单提交',
tags: '高意向,电商',
siteTags: '新客,女装',
// sign 需要根据签名规则生成
sign: '根据签名规则生成的MD5字符串'
})
})`,
python: `import requests
url = '${fullWebhookUrl}'
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ${apiKey}'
'Content-Type': 'application/json'
}
data = {
'name': '张三',
'phone': '13800138000',
'source': '官网表单'
"apiKey": "${apiKey}",
"timestamp": 1710000000,
"phone": "13800138000",
"name": "张三",
"source": "官网表单",
"remark": "通过H5表单提交",
"tags": "高意向,电商",
"siteTags": "新客,女装",
# sign 需要根据签名规则生成
"sign": "根据签名规则生成的MD5字符串"
}
response = requests.post(url, json=data, headers=headers)`,
php: `<?php
$url = '${fullWebhookUrl}';
$data = array(
'name' => '张三',
'apiKey' => '${apiKey}',
'timestamp' => 1710000000,
'phone' => '13800138000',
'source' => '官网表单'
'name' => '张三',
'source' => '官网表单',
'remark' => '通过H5表单提交',
'tags' => '高意向,电商',
'siteTags' => '新客,女装',
// sign 需要根据签名规则生成
'sign' => '根据签名规则生成的MD5字符串'
);
$options = array(
'http' => array(
'header' => "Content-type: application/json\\r\\nAuthorization: Bearer ${apiKey}\\r\\n",
'header' => "Content-type: application/json\\r\\n",
'method' => 'POST',
'content' => json_encode($data)
)
@@ -279,12 +380,11 @@ import java.net.http.HttpResponse;
import java.net.URI;
HttpClient client = HttpClient.newHttpClient();
String json = "{\\"name\\":\\"张三\\",\\"phone\\":\\"13800138000\\",\\"source\\":\\"官网表单\\"}";
String json = "{\\"apiKey\\":\\"${apiKey}\\",\\"timestamp\\":1710000000,\\"phone\\":\\"13800138000\\",\\"name\\":\\"张三\\",\\"source\\":\\"官网表单\\",\\"remark\\":\\"通过H5表单提交\\",\\"tags\\":\\"高意向,电商\\",\\"siteTags\\":\\"新客,女装\\",\\"sign\\":\\"根据签名规则生成的MD5字符串\\"}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("${fullWebhookUrl}"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer ${apiKey}")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
@@ -394,11 +494,7 @@ HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.o
<SafetyOutlined />
HTTPS加密
</div>
<Button
type="primary"
onClick={onClose}
className={style["complete-btn"]}
>
<Button color="primary" onClick={onClose} className={style["complete-btn"]}>
</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

@@ -0,0 +1,235 @@
import React, { useImperativeHandle, forwardRef } from "react";
import { Form, Card, Tabs } from "antd";
import DeviceSelection from "@/components/DeviceSelection";
import FriendSelection from "@/components/FriendSelection";
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
import { FriendSelectionItem } from "@/components/FriendSelection/data";
interface OwnerAdminSelectorProps {
selectedOwners: DeviceSelectionItem[];
selectedAdmins: FriendSelectionItem[];
onNext: (data: {
devices: string[];
devicesOptions: DeviceSelectionItem[];
admins: string[];
adminsOptions: FriendSelectionItem[];
}) => void;
}
export interface OwnerAdminSelectorRef {
validate: () => Promise<boolean>;
getValues: () => any;
}
const OwnerAdminSelector = forwardRef<
OwnerAdminSelectorRef,
OwnerAdminSelectorProps
>(({ selectedOwners, selectedAdmins, onNext }, ref) => {
const [form] = Form.useForm();
const [owners, setOwners] = React.useState<DeviceSelectionItem[]>(
selectedOwners || []
);
const [admins, setAdmins] = React.useState<FriendSelectionItem[]>(
selectedAdmins || []
);
// 当外部传入的 selectedOwners 或 selectedAdmins 变化时,同步内部状态
React.useEffect(() => {
setOwners(selectedOwners || []);
}, [selectedOwners]);
React.useEffect(() => {
setAdmins(selectedAdmins || []);
}, [selectedAdmins]);
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
validate: async () => {
// 验证群主和管理员
if (owners.length === 0) {
form.setFields([
{
name: "devices",
errors: ["请选择一个群主"],
},
]);
return false;
}
if (owners.length > 1) {
form.setFields([
{
name: "devices",
errors: ["群主只能选择一个设备"],
},
]);
return false;
}
if (admins.length === 0) {
form.setFields([
{
name: "admins",
errors: ["请至少选择一个管理员"],
},
]);
return false;
}
// 清除错误
form.setFields([
{
name: "devices",
errors: [],
},
{
name: "admins",
errors: [],
},
]);
return true;
},
getValues: () => {
return {
devices: owners.map(o => o.id.toString()),
admins: admins.map(a => a.id.toString()),
devicesOptions: owners,
adminsOptions: admins,
};
},
}));
// 群主选择(设备选择)
const handleOwnersSelect = (selectedDevices: DeviceSelectionItem[]) => {
const previousOwnerId = owners.length > 0 ? owners[0]?.id : null;
const newOwnerId = selectedDevices.length > 0 ? selectedDevices[0]?.id : null;
// 当群主改变时,清空已选的管理员(因为筛选条件变了)
const shouldClearAdmins = previousOwnerId !== newOwnerId;
setOwners(selectedDevices);
const ownerIds = selectedDevices.map(d => d.id.toString());
form.setFieldValue("devices", ownerIds);
if (shouldClearAdmins) {
setAdmins([]);
form.setFieldValue("admins", []);
}
// 通知父组件数据变化
onNext({
devices: ownerIds,
devicesOptions: selectedDevices,
admins: shouldClearAdmins ? [] : admins.map(a => a.id.toString()),
adminsOptions: shouldClearAdmins ? [] : admins,
});
};
// 管理员选择
const handleAdminsSelect = (selectedFriends: FriendSelectionItem[]) => {
setAdmins(selectedFriends);
const adminIds = selectedFriends.map(f => f.id.toString());
form.setFieldValue("admins", adminIds);
// 通知父组件数据变化
onNext({
devices: owners.map(o => o.id.toString()),
devicesOptions: owners,
admins: adminIds,
adminsOptions: selectedFriends,
});
};
const tabItems = [
{
key: "devices",
label: `群主 (${owners.length})`,
children: (
<div>
<div style={{ marginBottom: 16 }}>
<p style={{ margin: "0 0 8px 0", color: "#666", fontSize: 14 }}>
</p>
</div>
<Form.Item
name="devices"
validateStatus={owners.length === 0 || owners.length > 1 ? "error" : ""}
help={
owners.length === 0
? "请选择一个群主(设备)"
: owners.length > 1
? "群主只能选择一个设备"
: ""
}
>
<DeviceSelection
selectedOptions={owners}
onSelect={handleOwnersSelect}
placeholder="选择群主(设备)"
singleSelect={true}
/>
</Form.Item>
</div>
),
},
{
key: "admins",
label: `管理员 (${admins.length})`,
children: (
<div>
<div style={{ marginBottom: 16 }}>
<p style={{ margin: "0 0 8px 0", color: "#666", fontSize: 14 }}>
{owners.length === 0
? "请先选择群主(设备),然后选择该设备下的好友作为管理员"
: "请选择管理员,管理员将协助管理新建的群聊(仅显示所选设备下的好友)"}
</p>
</div>
<Form.Item
name="admins"
validateStatus={admins.length === 0 ? "error" : ""}
help={
owners.length === 0
? "请先选择群主(设备)"
: admins.length === 0
? "请至少选择一个管理员"
: ""
}
>
<FriendSelection
selectedOptions={admins}
onSelect={handleAdminsSelect}
placeholder={owners.length === 0 ? "请先选择群主" : "选择管理员"}
deviceIds={owners.length > 0 ? owners.map(d => d.id) : []}
enableDeviceFilter={true}
readonly={owners.length === 0}
/>
</Form.Item>
</div>
),
},
];
return (
<Card>
<Form
form={form}
layout="vertical"
initialValues={{
devices: (selectedOwners || []).map(item => item.id.toString()),
admins: (selectedAdmins || []).map(item => item.id.toString()),
}}
>
<div style={{ marginBottom: 20 }}>
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 600 }}>
</h2>
<p style={{ margin: "8px 0 0 0", color: "#666", fontSize: 14 }}>
</p>
</div>
<Tabs items={tabItems} />
</Form>
</Card>
);
});
OwnerAdminSelector.displayName = "OwnerAdminSelector";
export default OwnerAdminSelector;

View File

@@ -7,24 +7,29 @@ import { createAutoGroup, updateAutoGroup, getAutoGroupDetail } from "./api";
import { AutoGroupFormData, StepItem } from "./types";
import StepIndicator from "@/components/StepIndicator";
import BasicSettings, { BasicSettingsRef } from "./components/BasicSettings";
import DeviceSelector, { DeviceSelectorRef } from "./components/DeviceSelector";
import OwnerAdminSelector, {
OwnerAdminSelectorRef,
} from "./components/OwnerAdminSelector";
import PoolSelector, { PoolSelectorRef } from "./components/PoolSelector";
import NavCommon from "@/components/NavCommon/index";
import dayjs from "dayjs";
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
import { FriendSelectionItem } from "@/components/FriendSelection/data";
import { PoolSelectionItem } from "@/components/PoolSelection/data";
const steps: StepItem[] = [
{ id: 1, title: "步骤 1", subtitle: "基础设置" },
{ id: 2, title: "步骤 2", subtitle: "选择设备" },
{ id: 2, title: "步骤 2", subtitle: "选择群主和管理员" },
{ id: 3, title: "步骤 3", subtitle: "选择流量池包" },
];
const defaultForm: AutoGroupFormData = {
name: "",
type: 4,
deviceGroups: [], // 设备组
deviceGroupsOptions: [], // 设备组选项
devices: [], // 群主ID列表
devicesOptions: [], // 群主选项
admins: [], // 管理员ID列表
adminsOptions: [], // 管理员选项
poolGroups: [], // 内容库
poolGroupsOptions: [], // 内容库选项
startTime: dayjs().format("HH:mm"), // 开始时间 (HH:mm)
@@ -45,16 +50,15 @@ const AutoGroupForm: React.FC = () => {
const [loading, setLoading] = useState(false);
const [dataLoaded, setDataLoaded] = useState(!isEdit); // 非编辑模式直接标记为已加载
const [formData, setFormData] = useState<AutoGroupFormData>(defaultForm);
const [deviceGroupsOptions, setDeviceGroupsOptions] = useState<
DeviceSelectionItem[]
>([]);
const [devicesOptions, setDevicesOptions] = useState<DeviceSelectionItem[]>([]);
const [adminsOptions, setAdminsOptions] = useState<FriendSelectionItem[]>([]);
const [poolGroupsOptions, setpoolGroupsOptions] = useState<
PoolSelectionItem[]
>([]);
// 创建子组件的ref
const basicSettingsRef = useRef<BasicSettingsRef>(null);
const deviceSelectorRef = useRef<DeviceSelectorRef>(null);
const ownerAdminSelectorRef = useRef<OwnerAdminSelectorRef>(null);
const poolSelectorRef = useRef<PoolSelectorRef>(null);
useEffect(() => {
@@ -64,8 +68,10 @@ const AutoGroupForm: React.FC = () => {
const updatedForm = {
...defaultForm,
name: res.name,
deviceGroups: res.config.deviceGroups || [],
deviceGroupsOptions: res.config.deviceGroupsOptions || [],
devices: res.config.deviceGroups || res.config.devices || [], // 兼容deviceGroups和devices
devicesOptions: res.config.deviceGroupsOptions || res.config.devicesOptions || [], // 兼容deviceGroupsOptions和devicesOptions
admins: res.config.admins || [],
adminsOptions: res.config.adminsOptions || [],
poolGroups: res.config.poolGroups || [],
poolGroupsOptions: res.config.poolGroupsOptions || [],
startTime: res.config.startTime,
@@ -80,7 +86,8 @@ const AutoGroupForm: React.FC = () => {
id: res.id,
};
setFormData(updatedForm);
setDeviceGroupsOptions(res.config.deviceGroupsOptions || []);
setDevicesOptions(res.config.deviceGroupsOptions || res.config.devicesOptions || []); // 兼容deviceGroupsOptions和devicesOptions
setAdminsOptions(res.config.adminsOptions || []);
setpoolGroupsOptions(res.config.poolGroupsOptions || []);
setDataLoaded(true); // 标记数据已加载
});
@@ -90,16 +97,20 @@ const AutoGroupForm: React.FC = () => {
setFormData(prev => ({ ...prev, ...values }));
};
// 设备组选择
const handleDevicesChange = (data: {
deviceGroups: string[];
deviceGroupsOptions: DeviceSelectionItem[];
// 群主和管理员选择
const handleOwnerAdminChange = (data: {
devices: string[];
devicesOptions: DeviceSelectionItem[];
admins: string[];
adminsOptions: FriendSelectionItem[];
}) => {
setFormData(prev => ({
...prev,
deviceGroups: data.deviceGroups,
devices: data.devices,
admins: data.admins,
}));
setDeviceGroupsOptions(data.deviceGroupsOptions);
setDevicesOptions(data.devicesOptions);
setAdminsOptions(data.adminsOptions);
};
// 流量池包选择
@@ -116,8 +127,16 @@ const AutoGroupForm: React.FC = () => {
Toast.show({ content: "请输入任务名称" });
return;
}
if (formData.deviceGroups.length === 0) {
Toast.show({ content: "请选择至少一个设备组" });
if (formData.devices.length === 0) {
Toast.show({ content: "请选择一个群主" });
return;
}
if (formData.devices.length > 1) {
Toast.show({ content: "群主只能选择一个设备" });
return;
}
if (formData.admins.length === 0) {
Toast.show({ content: "请至少选择一个管理员" });
return;
}
if (formData.poolGroups.length === 0) {
@@ -127,9 +146,13 @@ const AutoGroupForm: React.FC = () => {
setLoading(true);
try {
// 构建提交数据将devices映射为deviceGroups
const { devices, devicesOptions, ...restFormData } = formData;
const submitData = {
...formData,
deviceGroupsOptions: deviceGroupsOptions,
...restFormData,
deviceGroups: devices, // 设备ID数组传输字段名为deviceGroups
deviceGroupsOptions: devicesOptions, // 设备完整信息传输字段名为deviceGroupsOptions
adminsOptions: adminsOptions,
poolGroupsOptions: poolGroupsOptions,
};
@@ -173,8 +196,9 @@ const AutoGroupForm: React.FC = () => {
break;
case 2:
// 调用 DeviceSelector 的表单校验
isValid = (await deviceSelectorRef.current?.validate()) || false;
// 调用 OwnerAdminSelector 的表单校验
isValid =
(await ownerAdminSelectorRef.current?.validate()) || false;
if (isValid) {
setCurrentStep(3);
}
@@ -217,10 +241,11 @@ const AutoGroupForm: React.FC = () => {
);
case 2:
return (
<DeviceSelector
ref={deviceSelectorRef}
selectedDevices={deviceGroupsOptions}
onNext={handleDevicesChange}
<OwnerAdminSelector
ref={ownerAdminSelectorRef}
selectedOwners={devicesOptions}
selectedAdmins={adminsOptions}
onNext={handleOwnerAdminChange}
/>
);
case 3:

View File

@@ -1,13 +1,16 @@
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
import { PoolSelectionItem } from "@/components/PoolSelection/data";
import { DeviceSelectionItem } from "@/components/DeviceSelection/data";
import { FriendSelectionItem } from "@/components/FriendSelection/data";
// 自动建群表单数据类型定义
export interface AutoGroupFormData {
id?: string; // 任务ID
type: number; // 任务类型
name: string; // 任务名称
deviceGroups: string[]; // 设备组
deviceGroupsOptions: DeviceSelectionItem[]; // 设备组选项
devices: string[]; // 群主ID列表设备ID
devicesOptions: DeviceSelectionItem[]; // 群主选项(设备)
admins: string[]; // 管理员ID列表好友ID
adminsOptions: FriendSelectionItem[]; // 管理员选项(好友)
poolGroups: string[]; // 流量池
poolGroupsOptions: PoolSelectionItem[]; // 流量池选项
startTime: string; // 开始时间 (YYYY-MM-DD HH:mm:ss)
@@ -34,9 +37,13 @@ export const formValidationRules = {
{ required: true, message: "请输入任务名称" },
{ min: 2, max: 50, message: "任务名称长度应在2-50个字符之间" },
],
deviceGroups: [
{ required: true, message: "请选择设备组" },
{ type: "array", min: 1, message: "至少选择一个设备" },
devices: [
{ required: true, message: "请选择群主" },
{ type: "array", min: 1, max: 1, message: "群主只能选择一个设备" },
],
admins: [
{ required: true, message: "请选择管理员" },
{ type: "array", min: 1, message: "至少选择一个管理员" },
],
poolGroups: [
{ required: true, message: "请选择内容库" },

View File

@@ -116,20 +116,38 @@ const AutoLike: React.FC = () => {
setLoading(true);
try {
const Res: any = await fetchAutoLikeTasks();
// 直接就是任务数组,无需再解包
const mappedTasks = Res?.list?.map((task: any) => ({
...task,
status: task.status || 2, // 默认为关闭状态
deviceCount: task.deviceCount || 0,
targetGroup: task.targetGroup || "全部好友",
likeInterval: task.likeInterval || 60,
maxLikesPerDay: task.maxLikesPerDay || 100,
lastLikeTime: task.lastLikeTime || "暂无",
createTime: task.createTime || "",
updateTime: task.updateTime || "",
todayLikeCount: task.todayLikeCount || 0,
totalLikeCount: task.totalLikeCount || 0,
}));
// 数据在 data.list 中
const taskList = Res?.data?.list || Res?.list || [];
const mappedTasks = taskList.map((task: any) => {
const config = task.config || {};
const friends = config.friends || [];
const devices = config.devices || [];
// 判断目标人群:如果 friends 为空或未设置,表示选择全部好友
let targetGroup = "全部好友";
if (friends.length > 0) {
targetGroup = `${friends.length} 个好友`;
}
return {
id: task.id?.toString() || "",
name: task.name || "",
status: task.status === 1 ? 1 : 2, // 1: 开启, 2: 关闭
deviceCount: devices.length,
targetGroup: targetGroup,
likeInterval: config.interval || 60,
maxLikesPerDay: config.maxLikes || 100,
lastLikeTime: task.lastLikeTime || "暂无",
createTime: task.createTime || "",
updateTime: task.updateTime || "",
todayLikeCount: config.todayLikeCount || 0,
totalLikeCount: config.totalLikeCount || 0,
// 保留原始数据
config: config,
devices: devices,
friends: friends,
};
});
setTasks(mappedTasks);
} catch (error) {
console.error("获取自动点赞任务失败:", error);
@@ -355,7 +373,7 @@ const AutoLike: React.FC = () => {
/>
<span className={style["stats-label"]}></span>
<span className={style["stats-value"]}>
{task.lastLikeTime}
{task.todayLikeCount || 0}
</span>
</div>
<div className={style["stats-item"]}>

View File

@@ -36,6 +36,7 @@ const NewAutoLike: React.FC = () => {
const [isSubmitting, setIsSubmitting] = useState(false);
const [isLoading, setIsLoading] = useState(isEditMode);
const [autoEnabled, setAutoEnabled] = useState(false);
const [selectAllFriends, setSelectAllFriends] = useState(false);
const [formData, setFormData] = useState<CreateLikeTaskData>({
name: "",
interval: 5,
@@ -45,8 +46,8 @@ const NewAutoLike: React.FC = () => {
contentTypes: ["text", "image", "video"],
deviceGroups: [],
deviceGroupsOptions: [],
friendsGroups: [],
friendsGroupsOptions: [],
wechatFriends: [],
wechatFriendsOptions: [],
targetTags: [],
friendMaxLikes: 10,
enableFriendTags: false,
@@ -74,8 +75,8 @@ const NewAutoLike: React.FC = () => {
contentTypes: config.contentTypes || ["text", "image", "video"],
deviceGroups: config.deviceGroups || [],
deviceGroupsOptions: config.deviceGroupsOptions || [],
friendsGroups: config.friendsgroups || [],
friendsGroupsOptions: config.friendsGroupsOptions || [],
wechatFriends: config.wechatFriends || [],
wechatFriendsOptions: config.wechatFriendsOptions || [],
targetTags: config.targetTags || [],
friendMaxLikes: config.friendMaxLikes || 10,
enableFriendTags: config.enableFriendTags || false,
@@ -85,6 +86,10 @@ const NewAutoLike: React.FC = () => {
(taskDetail as any).status === 1 ||
(taskDetail as any).status === "running",
);
// 如果 wechatFriends 为空或未设置,可能表示选择了全部好友
setSelectAllFriends(
!config.wechatFriends || config.wechatFriends.length === 0
);
}
} catch (error) {
message.error("获取任务详情失败");
@@ -127,11 +132,19 @@ const NewAutoLike: React.FC = () => {
}
setIsSubmitting(true);
try {
// 如果选择了全部好友,提交时传空数组或特殊标识
const submitData = {
...formData,
wechatFriends: selectAllFriends ? [] : formData.wechatFriends,
wechatFriendsOptions: selectAllFriends ? [] : formData.wechatFriendsOptions,
selectAllFriends: selectAllFriends, // 添加标识字段
};
if (isEditMode) {
await updateAutoLikeTask({ ...formData, id });
await updateAutoLikeTask({ ...submitData, id });
message.success("更新成功");
} else {
await createAutoLikeTask(formData);
await createAutoLikeTask(submitData);
message.success("创建成功");
}
navigate("/workspace/auto-like");
@@ -142,6 +155,28 @@ const NewAutoLike: React.FC = () => {
}
};
// 选择全部好友(仅设置标识)
const handleSelectAllFriends = () => {
if (!formData.deviceGroups || formData.deviceGroups.length === 0) {
message.warning("请先选择执行设备");
return;
}
if (selectAllFriends) {
// 取消全选标识
setSelectAllFriends(false);
// 清空已选好友
handleUpdateFormData({
wechatFriends: [],
wechatFriendsOptions: [],
});
} else {
// 设置全选标识
setSelectAllFriends(true);
message.success("已标记为选择全部好友");
}
};
// 步骤器
const renderStepIndicator = () => (
<StepIndicator steps={steps} currentStep={currentStep} />
@@ -364,16 +399,39 @@ const NewAutoLike: React.FC = () => {
const renderFriendSettings = () => (
<div className={style.basicSection}>
<div className={style.formItem}>
<FriendSelection
selectedOptions={formData.friendsGroupsOptions || []}
onSelect={friends =>
handleUpdateFormData({
friendsGroups: friends.map(f => f.id),
friendsGroupsOptions: friends,
})
}
deviceIds={formData.deviceGroups}
/>
<div className={style.friendSelectionHeader}>
<div className={style.formLabel}></div>
<Button
type={selectAllFriends ? "primary" : "default"}
size="small"
onClick={handleSelectAllFriends}
disabled={!formData.deviceGroups || formData.deviceGroups.length === 0}
className={style.selectAllBtn}
>
{selectAllFriends ? "已选择全部" : "选择全部好友"}
</Button>
</div>
{selectAllFriends ? (
<div className={style.selectAllTip}>
<span className={style.selectAllIcon}></span>
</div>
) : (
<FriendSelection
selectedOptions={formData.wechatFriendsOptions || []}
onSelect={friends => {
handleUpdateFormData({
wechatFriends: friends.map(f => f.id),
wechatFriendsOptions: friends,
});
// 如果手动选择了好友,取消全选标识
if (selectAllFriends) {
setSelectAllFriends(false);
}
}}
deviceIds={formData.deviceGroups}
/>
)}
</div>
<Button
onClick={handlePrev}
@@ -390,7 +448,8 @@ const NewAutoLike: React.FC = () => {
size="large"
loading={isSubmitting}
disabled={
!formData.friendsGroups || formData.friendsGroups.length === 0
!selectAllFriends &&
(!formData.wechatFriends || formData.wechatFriends.length === 0)
}
>
{isEditMode ? "更新任务" : "创建任务"}

View File

@@ -126,6 +126,13 @@
gap: 8px;
}
.contentTypeBtn {
flex: 1;
height: 40px;
border-radius: 8px;
font-size: 14px;
}
.contentTypeTag {
padding: 8px 16px;
border-radius: 6px;
@@ -230,3 +237,50 @@
font-size: 15px;
min-width: 120px;
}
.friendSelectionHeader {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.selectAllBtn {
height: 32px;
border-radius: 6px;
font-size: 14px;
padding: 0 16px;
}
.selectAllTip {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: #f0f9ff;
border: 1px solid #91d5ff;
border-radius: 8px;
color: #1890ff;
font-size: 14px;
margin-top: 8px;
}
.selectAllIcon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
background: #1890ff;
color: #fff;
border-radius: 50%;
font-size: 12px;
font-weight: bold;
}
.mainBtn {
height: 44px;
border-radius: 8px;
font-size: 15px;
margin-top: 24px;
}

View File

@@ -63,8 +63,8 @@ const NewMomentsSync: React.FC = () => {
if (res) {
setFormData({
taskName: res.name,
startTime: res.timeRange?.start || "06:00",
endTime: res.timeRange?.end || "23:59",
startTime: res.config?.startTime || "06:00",
endTime: res.config?.endTime || "23:59",
syncCount: res.config?.syncCount || res.syncCount || 5,
syncInterval: res.config?.syncInterval || res.syncInterval || 30,
syncType: res.config?.syncType,

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

@@ -33,14 +33,15 @@ export const getFullApiPath = (): string => {
* - buildApiUrl('https://api.example.com/webhook/123') → 'https://api.example.com/webhook/123'
*/
export const buildApiUrl = (path: string): string => {
if (!path) return "";
const fullApiPath = getFullApiPath();
if (!path) return `${fullApiPath}`;
// 如果已经是完整的URL包含http或https直接返回
if (path.startsWith("http://") || path.startsWith("https://")) {
return path;
}
const fullApiPath = getFullApiPath();
// 如果是相对路径拼接完整API路径
if (path.startsWith("/")) {

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

11
Moncter/MCP/mcp.json Normal file
View File

@@ -0,0 +1,11 @@
{
"mcpServers": {
"MongoDB": {
"command": "npx",
"args": ["-y", "mongodb-mcp-server@latest", "--readOnly"],
"env": {
"MDB_MCP_CONNECTION_STRING": "mongodb://ckb:123456@192.168.1.106:27017/ckb"
}
}
}
}

View File

@@ -0,0 +1,15 @@
# 账户密码
```
HOST 192.168.1.106
PORT 27017
ACCOUNTckb
DBNAMEckb
PASSWROD123456
```
==================
# 环境准备
需要安装一个全局环境否则无法运行
node版本为22.12.0
npm i mongodb-mcp-server -g

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月库");

254
Server/README_scheduler.md Normal file
View File

@@ -0,0 +1,254 @@
# 统一任务调度器使用说明
## 概述
统一任务调度器TaskSchedulerCommand是一个集中管理所有定时任务的调度系统支持
- ✅ 单条 crontab 配置管理所有任务
- ✅ 多进程并发执行任务
- ✅ 自动根据 cron 表达式判断任务执行时间
- ✅ 任务锁机制,防止重复执行
- ✅ 完善的日志记录
## 安装配置
### 1. 配置文件
任务配置位于 `config/task_scheduler.php`,格式如下:
```php
'任务标识' => [
'command' => '命令名称', // 必填:执行的命令
'schedule' => 'cron表达式', // 必填cron表达式
'options' => ['--option=value'], // 可选:命令参数
'enabled' => true, // 可选:是否启用
'max_concurrent' => 1, // 可选:最大并发数
'timeout' => 3600, // 可选:超时时间(秒)
'log_file' => 'custom.log', // 可选:自定义日志文件
]
```
### 2. Cron 表达式格式
标准 cron 格式:`分钟 小时 日 月 星期`
示例:
- `*/1 * * * *` - 每分钟执行
- `*/5 * * * *` - 每5分钟执行
- `*/30 * * * *` - 每30分钟执行
- `0 2 * * *` - 每天凌晨2点执行
- `0 3 */3 * *` - 每3天的3点执行
### 3. Crontab 配置
**只需要在 crontab 中添加一条任务:**
```bash
# 每分钟执行一次调度器(调度器内部会根据 cron 表达式判断哪些任务需要执行)
* * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think scheduler:run >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/scheduler.log 2>&1
```
### 4. 系统要求
- PHP >= 5.6.0
- 推荐启用 `pcntl` 扩展以支持多进程并发(非必需,未启用时使用单进程顺序执行)
检查 pcntl 扩展:
```bash
php -m | grep pcntl
```
## 使用方法
### 手动执行调度器
```bash
# 执行调度器(会自动判断当前时间需要执行的任务)
php think scheduler:run
```
### 查看任务配置
```bash
# 查看所有已注册的命令
php think list
```
### 启用/禁用任务
编辑 `config/task_scheduler.php`,设置 `'enabled' => false` 即可禁用任务。
## 功能特性
### 1. 多进程并发执行
- 默认最大并发数10 个进程
- 自动管理进程池
- 自动清理僵尸进程
### 2. 任务锁机制
- 每个任务在执行时会设置锁5分钟内不重复执行
- 防止任务重复执行
- 锁存储在缓存中,自动过期
### 3. 日志记录
- 调度器日志:`runtime/log/scheduler.log`
- 每个任务的日志:`runtime/log/{log_file}`
- 任务执行开始和结束都有标记
### 4. 超时控制
- 默认超时时间3600 秒1小时
- 可在配置中为每个任务单独设置超时时间
- 超时后自动终止任务
## 配置示例
### 高频任务(每分钟)
```php
'wechat_friends_active' => [
'command' => 'wechatFriends:list',
'schedule' => '*/1 * * * *',
'options' => ['--isDel=0'],
'enabled' => true,
],
```
### 中频任务每5分钟
```php
'device_active' => [
'command' => 'device:list',
'schedule' => '*/5 * * * *',
'options' => ['--isDel=0'],
'enabled' => true,
],
```
### 每日任务
```php
'wechat_calculate_score' => [
'command' => 'wechat:calculate-score',
'schedule' => '0 2 * * *', // 每天凌晨2点
'options' => [],
'enabled' => true,
],
```
### 定期任务每3天
```php
'sync_all_friends' => [
'command' => 'sync:allFriends',
'schedule' => '0 3 */3 * *', // 每3天的3点
'options' => [],
'enabled' => true,
],
```
## 从旧配置迁移
### 旧配置(多条 crontab
```bash
*/5 * * * * cd /path && php think device:list --isDel=0 >> log1.log 2>&1
*/1 * * * * cd /path && php think wechatFriends:list >> log2.log 2>&1
```
### 新配置(单条 crontab + 配置文件)
**Crontab**
```bash
* * * * * cd /path && php think scheduler:run >> scheduler.log 2>&1
```
**config/task_scheduler.php**
```php
'device_active' => [
'command' => 'device:list',
'schedule' => '*/5 * * * *',
'options' => ['--isDel=0'],
'log_file' => 'log1.log',
],
'wechat_friends' => [
'command' => 'wechatFriends:list',
'schedule' => '*/1 * * * *',
'log_file' => 'log2.log',
],
```
## 监控和调试
### 查看调度器日志
```bash
tail -f runtime/log/scheduler.log
```
### 查看任务执行日志
```bash
tail -f runtime/log/crontab_device_active.log
```
### 检查任务是否在执行
```bash
# 查看进程
ps aux | grep "php think"
```
### 手动测试任务
```bash
# 直接执行某个任务
php think device:list --isDel=0
```
## 注意事项
1. **时间同步**:确保服务器时间准确,调度器依赖系统时间判断任务执行时间
2. **资源限制**:根据服务器性能调整 `maxConcurrent` 参数
3. **日志清理**:定期清理日志文件,避免占用过多磁盘空间
4. **任务冲突**:如果任务执行时间较长,建议调整执行频率或增加并发数
5. **缓存依赖**:任务锁使用缓存,确保缓存服务正常运行
## 故障排查
### 任务未执行
1. 检查任务是否启用:`'enabled' => true`
2. 检查 cron 表达式是否正确
3. 检查调度器是否正常运行:查看 `scheduler.log`
4. 检查任务锁任务可能在5分钟内重复执行被跳过
### 任务执行失败
1. 查看任务日志:`runtime/log/{log_file}`
2. 检查命令是否正确:手动执行命令测试
3. 检查权限:确保有执行权限和日志写入权限
### 多进程不工作
1. 检查 pcntl 扩展:`php -m | grep pcntl`
2. 检查系统限制:`ulimit -u` 查看最大进程数
3. 查看调度器日志中的错误信息
## 性能优化建议
1. **合理设置并发数**:根据服务器 CPU 核心数和内存大小调整
2. **错开高频任务**:避免所有任务在同一分钟执行
3. **优化任务执行时间**:减少任务执行时长
4. **使用队列**:对于耗时任务,建议使用队列异步处理
## 更新日志
### v1.0.0 (2024-01-XX)
- 初始版本
- 支持多进程并发执行
- 支持 cron 表达式调度
- 支持任务锁机制

View File

@@ -61,10 +61,8 @@ class MessageController extends BaseController
// 发送请求获取好友列表
$result = requestCurl($this->baseUrl . 'api/WechatFriend/listWechatFriendForMsgPagination', $params, 'POST', $header, 'json');
$response = handleApiResponse($result);
// 获取同步消息标志
$syncMessages = $this->request->param('syncMessages', true);
// 如果需要同步消息,则获取每个好友的消息
if ($syncMessages && !empty($response['results'])) {
$from = strtotime($fromTime) * 1000;
@@ -77,7 +75,7 @@ class MessageController extends BaseController
'keyword' => '',
'msgType' => '',
'accountId' => '',
'count' => 100,
'count' => 20,
'messageId' => '',
'olderData' => true,
'wechatAccountId' => $friend['wechatAccountId'],
@@ -90,7 +88,6 @@ class MessageController extends BaseController
// 调用获取消息的接口
$messageResult = requestCurl($this->baseUrl . 'api/FriendMessage/searchMessage', $messageParams, 'GET', $header, 'json');
$messageResponse = handleApiResponse($messageResult);
// 保存消息到数据库
if (!empty($messageResponse)) {
foreach ($messageResponse as $item) {
@@ -241,7 +238,7 @@ class MessageController extends BaseController
'keyword' => '',
'msgType' =>'',
'accountId' => '',
'count' => 100,
'count' => 20,
'messageId' => '',
'olderData' => true,
'wechatId' => '',
@@ -353,12 +350,12 @@ class MessageController extends BaseController
{
// 检查消息是否已存在
$exists = WechatMessageModel::where('id', $item['id']) ->find();
// 如果消息已存在,直接返回
if ($exists) {
return;
if (!empty($exists) && $exists['sendStatus'] == 0){
return true;
}
// 将毫秒时间戳转换为秒级时间戳
$createTime = isset($item['createTime']) ? strtotime($item['createTime']) : null;
$deleteTime = !empty($item['isDeleted']) ? strtotime($item['deleteTime']) : null;
@@ -387,7 +384,8 @@ class MessageController extends BaseController
'wechatTime' => $wechatTime
];
//已被删除
//已被删除
if ($item['msgType'] == 10000 && strpos($item['content'],'开启了朋友验证') !== false) {
Db::table('s2_wechat_friend')->where('id',$item['wechatFriendId'])->update(['isDeleted'=> 1,'deleteTime' => $wechatTime]);
}else{
@@ -399,9 +397,9 @@ class MessageController extends BaseController
if (!empty($accountData)){
$account = new AccountController();
$account->getlist(['pageIndex' => 0,'pageSize' => 100,'departmentId' => $accountData['departmentId']]);
$accountIds = Db::table('s2_company_account')->where(['id' => $accountId,'alive' => 1])->column('id');
$accountIds = Db::table('s2_company_account')->where(['departmentId' => $accountData['departmentId'],'alive' => 1])->column('id');
if (!empty($accountIds)){
if (!in_array($friend['accountId'],$accountData)){
if (!in_array($friend['accountId'],$accountIds)){
// 执行切换好友命令
$randomKey = array_rand($accountIds, 1);
$toAccountId = $accountIds[$randomKey];
@@ -425,8 +423,19 @@ class MessageController extends BaseController
}
}
}
// 创建新记录
$res = WechatMessageModel::create($data);
$id = '';
if (empty($exists)){
// 创建新记录
$res = WechatMessageModel::create($data);
$id= $res['id'];
}else{
$id = $data['id'];
unset($data['id']);
$res = $exists->save($data);
}
// 1 文字 3图片 47动态图片 34语言 43视频 42名片 40/20链接 49文件
if (!empty($res) && empty($item['isSend']) && in_array($item['msgType'],[1,3,20,34,40,42,43,47,49])){
@@ -439,13 +448,14 @@ class MessageController extends BaseController
'companyId' => $friend['companyId'],
'trafficPoolId' => $trafficPoolId,
'source' => 0,
'uniqueId' => $res['id'],
'uniqueId' => $id,
'sourceData' => json_encode([]),
'remark' => '用户发送了消息',
'createTime' => time(),
'updateTime' => time()
];
Db::name('user_portrait')->insert($data);
Db::name('user_portrait')->insert($data);
}
}
}
@@ -461,9 +471,8 @@ class MessageController extends BaseController
{
// 检查消息是否已存在
$exists = WechatMessageModel::where('id', $item['id'])->find();
// 如果消息已存在,直接返回
if ($exists) {
if (!empty($exists) && $exists['sendStatus'] == 0){
return true;
}
@@ -515,7 +524,12 @@ class MessageController extends BaseController
// 创建新记录
try {
WechatMessageModel::create($data);
if(empty($exists)){
WechatMessageModel::create($data);
}else{
unset($data['id']);
$exists->save($data);
}
return true;
} catch (\Exception $e) {
return false;

View File

@@ -17,7 +17,7 @@ class MomentsController extends BaseController
// 获取授权token
$authorization = $this->authorization;
if (empty($authorization)) {
return errorJson('缺少授权信息');
return json_encode(['msg' => '缺少授权信息','code' => 400]);
}
try {
@@ -42,18 +42,18 @@ class MomentsController extends BaseController
// 必填参数验证
if (empty($jobPublishWechatMomentsItems) || !is_array($jobPublishWechatMomentsItems)) {
return errorJson('至少需要选择一个发布账号');
return json_encode(['msg' => '至少需要选择一个发布账号','code' => 400]);
}
// 根据朋友圈类型验证必填字段
if ($momentContentType == 1 && empty($text)) { // 纯文本
return errorJson('朋友圈内容不能为空');
return json_encode(['msg' => '朋友圈内容不能为空','code' => 400]);
} else if ($momentContentType == 2 && (empty($picUrlList) || empty($text))) { // 图片+文字
return errorJson('朋友圈内容和图片不能为空');
return json_encode(['msg' => '朋友圈内容和图片不能为空','code' => 400]);
} else if ($momentContentType == 3 && (empty($videoUrl) || empty($text))) { // 视频+文字
return errorJson('朋友圈内容和视频不能为空');
return json_encode(['msg' => '朋友圈内容和视频不能为空','code' => 400]);
} else if ($momentContentType == 4 && (empty($link) || empty($text))) { // 链接+文字
return errorJson('朋友圈内容和链接不能为空');
return json_encode(['msg' => '朋友圈内容和链接不能为空','code' => 400]);
}
// 构建请求参数
@@ -84,13 +84,13 @@ class MomentsController extends BaseController
$result = requestCurl($this->baseUrl . 'api/JobPublishWechatMoments/addJob', $params, 'POST', $header, 'json');
// 处理响应
if (empty($result)) {
return successJson([], '朋友圈任务创建成功');
return json_encode(['msg' => '朋友圈任务创建成功','code' => 200]);
} else {
// 如果返回的是错误信息
return errorJson($result);
return json_encode(['msg' => $result,'code' => 400]);
}
} catch (\Exception $e) {
return errorJson('发布朋友圈失败' . $e->getMessage());
return json_encode(['msg' => '发布朋友圈失败','code' => 400]);
}
}

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 () {
@@ -32,15 +34,22 @@ Route::group('v1/', function () {
Route::get('list', 'app\chukebao\controller\AccountsController@getList'); // 获取账号列表
});
//客服相关
//消息相关
Route::group('message/', function () {
Route::get('list', 'app\chukebao\controller\MessageController@getList'); // 获取好友列表
Route::get('readMessage', 'app\chukebao\controller\MessageController@readMessage'); // 读取消息
Route::get('details', 'app\chukebao\controller\MessageController@details'); // 消息详情
Route::get('getMessageStatus', 'app\chukebao\controller\MessageController@getMessageStatus'); // 获取单条消息发送状态
});
//微信分组
Route::get('wechatGroup/list', 'app\chukebao\controller\WechatGroupController@getList'); // 微信分组
Route::group('wechatGroup/', function () {
Route::get('list', 'app\chukebao\controller\WechatGroupController@getList'); // 获取分组列表
Route::post('add', 'app\chukebao\controller\WechatGroupController@create'); // 新增分组
Route::post('update', 'app\chukebao\controller\WechatGroupController@update'); // 更新分组
Route::delete('delete', 'app\chukebao\controller\WechatGroupController@delete'); // 删除分组(假删除)
Route::post('move', 'app\chukebao\controller\WechatGroupController@move'); // 移动分组(好友/群移动到指定分组)
});

View File

@@ -29,6 +29,7 @@ class AccountsController extends BaseController
$query = Db::table('s2_company_account')
->alias('a')
->join('users u', 'a.id = u.s2_accountId')
->where([
['a.departmentId', '=', $companyId],
['a.status', '=', 0],
@@ -48,6 +49,7 @@ class AccountsController extends BaseController
$total = (clone $query)->count();
$list = $query->field([
'a.id',
'u.id as uid',
'a.userName',
'a.realName',
'a.nickname',

View File

@@ -562,7 +562,7 @@ class AiChatController extends BaseController
$data = [
'tokens' => $tokenCount * 20,
'type' => 0,
'form' => 1,
'form' => 13,
'wechatAccountId' => $params['wechatAccountId'],
'friendIdOrGroupId' => $params['friendId'],
'remarks' => $remarks,
@@ -816,7 +816,7 @@ class AiChatController extends BaseController
$data = [
'tokens' => $res['data']['token'],
'type' => 0,
'form' => 1,
'form' => 13,
'wechatAccountId' => $wechatAccountId,
'friendIdOrGroupId' => $friendId,
'remarks' => $remarks,

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,8 @@ class DataProcessing extends BaseController
'CmdAllotFriend', //转让好友 {labels、wechatAccountId、wechatFriendId}
'CmdChatroomOperate', //修改群信息 {chatroomName群名、announce公告、extra公告、wechatAccountId、wechatChatroomId}
'CmdNewMessage', //接收消息
'CmdSendMessageResult', //更新消息状态
'CmdPinToTop', //置顶
];
if (empty($type) || empty($wechatAccountId)) {
@@ -75,15 +79,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 +125,81 @@ 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;
case 'CmdPinToTop': //置顶
$wechatFriendId = $this->request->param('wechatFriendId', 0);
$wechatChatroomId = $this->request->param('wechatChatroomId', 0);
$isTop = $this->request->param('isTop', null);
if ($isTop === null) {
return ResponseHelper::error('isTop不能为空');
}
if (empty($wechatFriendId) && empty($wechatChatroomId)) {
return ResponseHelper::error('wechatFriendId或chatroomId至少提供一个');
}
if (!empty($wechatFriendId)){
$data = WechatFriendModel::where(['id' => $wechatFriendId,'wechatAccountId' => $wechatAccountId])->find();
$msg = $isTop == 1 ? '已置顶' : '取消置顶';
if(empty($data)){
return ResponseHelper::error('好友不存在');
}
}
if (!empty($wechatChatroomId)){
$data = WechatChatroomModel::where(['id' => $wechatChatroomId,'wechatAccountId' => $wechatAccountId])->find();
$msg = $isTop == 1 ? '已置顶' : '取消置顶';
if(empty($data)){
return ResponseHelper::error('群聊不存在');
}
}
$data->updateTime = time();
$data->isTop = $isTop;
$data->save();
break;
}
return ResponseHelper::success('',$msg,$codee);
}

View File

@@ -2,12 +2,24 @@
namespace app\chukebao\controller;
use app\api\model\WechatMessageModel;
use app\chukebao\model\FriendSettings;
use library\ResponseHelper;
use think\Db;
use think\facade\Env;
use app\common\service\AuthService;
class MessageController extends BaseController
{
protected $baseUrl;
protected $authorization;
public function __construct()
{
parent::__construct();
$this->baseUrl = Env::get('api.wechat_url');
$this->authorization = AuthService::getSystemAuthorization();
}
public function getList()
{
@@ -20,7 +32,7 @@ class MessageController extends BaseController
$friends = Db::table('s2_wechat_friend')
->where(['accountId' => $accountId, 'isDeleted' => 0])
->column('id,nickname,avatar,conRemark,labels,groupId,wechatAccountId,wechatId,extendFields,phone,region');
->column('id,nickname,avatar,conRemark,labels,groupId,wechatAccountId,wechatId,extendFields,phone,region,isTop');
// 构建好友子查询
@@ -31,7 +43,7 @@ class MessageController extends BaseController
// 优化后的查询使用MySQL兼容的查询方式
$unionQuery = "
(SELECT m.id, m.content, m.wechatFriendId, m.wechatChatroomId, m.createTime, m.wechatTime,m.wechatAccountId, 2 as msgType, wc.nickname, wc.chatroomAvatar as avatar, wc.chatroomId
(SELECT m.id, m.content, m.wechatFriendId, m.wechatChatroomId, m.createTime, m.wechatTime,m.wechatAccountId, 2 as msgType, wc.nickname, wc.chatroomAvatar as avatar, wc.chatroomId, wc.isTop
FROM s2_wechat_chatroom wc
INNER JOIN s2_wechat_message m ON wc.id = m.wechatChatroomId AND m.type = 2
INNER JOIN (
@@ -43,7 +55,7 @@ class MessageController extends BaseController
WHERE wc.accountId = {$accountId} AND wc.isDeleted = 0
)
UNION ALL
(SELECT m.id, m.content, m.wechatFriendId, m.wechatChatroomId, m.createTime, m.wechatTime, 1 as msgType, 1 as nickname, 1 as avatar, 1 as chatroomId, 1 as wechatAccountId
(SELECT m.id, m.content, m.wechatFriendId, m.wechatChatroomId, m.createTime, m.wechatTime, 1 as msgType, 1 as nickname, 1 as avatar, 1 as chatroomId, 1 as wechatAccountId, 0 as isTop
FROM s2_wechat_message m
INNER JOIN (
SELECT wechatFriendId, MAX(wechatTime) as maxTime, MAX(id) as maxId
@@ -64,7 +76,6 @@ class MessageController extends BaseController
return $b['wechatTime'] <=> $a['wechatTime'];
});
// 批量统计未读数量isRead=0按好友/群聊分别聚合
$friendIds = [];
$chatroomIds = [];
@@ -122,6 +133,7 @@ class MessageController extends BaseController
$v['extendFields'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['extendFields'] : [];
$v['region'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['region'] : '';
$v['phone'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['phone'] : '';
$v['isTop'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['isTop'] : 0;
$v['labels'] = !empty($friends[$v['wechatFriendId']]) ? json_decode($friends[$v['wechatFriendId']]['labels'], true) : [];
$unreadCount = isset($friendUnreadMap[$v['wechatFriendId']]) ? (int)$friendUnreadMap[$v['wechatFriendId']] : 0;
@@ -136,7 +148,7 @@ class MessageController extends BaseController
$v['id'] = !empty($v['wechatFriendId']) ? $v['wechatFriendId'] : $v['wechatChatroomId'];
$v['config'] = [
'top' => false,
'top' => !empty($v['isTop']) ? true : false,
'unreadCount' => $unreadCount,
'chat' => true,
'msgTime' => $v['wechatTime'],
@@ -150,7 +162,7 @@ class MessageController extends BaseController
'wechatTime' => $wechatTime
];
unset($v['wechatFriendId'], $v['wechatChatroomId']);
unset($v['wechatFriendId'], $v['wechatChatroomId'],$v['isTop']);
}
unset($v);
@@ -184,6 +196,110 @@ class MessageController extends BaseController
}
/**
* 获取单条消息发送状态(带轮询功能)
* @return \think\response\Json
*/
public function getMessageStatus()
{
$messageId = $this->request->param('messageId', 0);
$wechatAccountId = $this->request->param('wechatAccountId', '');
$accountId = $this->getUserInfo('s2_accountId');
$wechatFriendId = $this->request->param('wechatFriendId', '');
$wechatChatroomId = $this->request->param('wechatChatroomId', '');
if (empty($accountId)) {
return ResponseHelper::error('请先登录');
}
if (empty($messageId)) {
return ResponseHelper::error('消息ID不能为空');
}
if(empty($wechatFriendId) && empty($wechatChatroomId)) {
return ResponseHelper::error('消息类型不能为空');
}
// 查询单条消息的基本信息(只需要发送状态相关字段)
$message = Db::table('s2_wechat_message')
->where('id', $messageId)
->field('id,wechatAccountId,wechatFriendId,wechatChatroomId,sendStatus')
->find();
if (empty($message)) {
$message = [
'id' => $messageId,
'wechatAccountId' => $wechatAccountId,
'wechatFriendId' => $wechatFriendId,
'wechatChatroomId' => $wechatChatroomId,
'sendStatus' => 0,
];
}
$sendStatus = isset($message['sendStatus']) ? (int)$message['sendStatus'] : 0;
$isUpdated = false;
$pollCount = 0;
$maxPollCount = 10; // 最多轮询10次
// 如果sendStatus不为0开始轮询
if ($sendStatus != 0) {
$messageRequest = [
'id' => $message['id'],
'wechatAccountId' => !empty($wechatAccountId) ? $wechatAccountId : $message['wechatAccountId'],
'wechatFriendId' => !empty($message['wechatFriendId']) ? $message['wechatFriendId'] : '',
'wechatChatroomId' => !empty($message['wechatChatroomId']) ? $message['wechatChatroomId'] : '',
'from' => '',
'to' => '',
];
// 轮询逻辑最多10次
while ($pollCount < $maxPollCount && $sendStatus != 0) {
$pollCount++;
// 请求线上接口获取最新状态
$newData = $this->fetchLatestMessageFromApi($messageRequest);
if (!empty($newData)) {
// 重新查询消息状态(可能已更新)
$updatedMessage = Db::table('s2_wechat_message')
->where('id', $messageId)
->field('sendStatus')
->find();
if (!empty($updatedMessage)) {
$newSendStatus = isset($updatedMessage['sendStatus']) ? (int)$updatedMessage['sendStatus'] : 0;
// 如果状态已更新为0已发送停止轮询
if ($newSendStatus == 0) {
$sendStatus = 0;
$isUpdated = true;
break;
}
// 如果状态仍然是1继续轮询但需要等待一下避免请求过快
if ($newSendStatus != 0 && $pollCount < $maxPollCount) {
// 每次轮询间隔500毫秒0.5秒)
usleep(500000);
}
}
} else {
// 如果请求失败,等待后继续尝试
if ($pollCount < $maxPollCount) {
usleep(500000);
}
}
}
}
// 返回发送状态信息
return ResponseHelper::success([
'messageId' => $messageId,
'sendStatus' => $sendStatus,
'statusText' => $sendStatus == 0 ? '已发送' : '发送中'
]);
}
public function details()
{
$wechatFriendId = $this->request->param('wechatFriendId', '');
@@ -218,14 +334,143 @@ class MessageController extends BaseController
$total = Db::table('s2_wechat_message')->where($where)->count();
$list = Db::table('s2_wechat_message')->where($where)->page($page, $limit)->order('id DESC')->select();
foreach ($list as $k => &$v) {
$v['wechatTime'] = !empty($v['wechatTime']) ? date('Y-m-d H:i:s', $v['wechatTime']) : '';
// 检查消息是否有sendStatus字段如果有且不为0则请求线上最新接口
foreach ($list as $k => &$item) {
// 检查是否存在sendStatus字段且不为00表示已发送成功
if (isset($item['sendStatus']) && $item['sendStatus'] != 0) {
// 需要请求新的数据
$messageRequest = [
'id' => $item['id'],
'wechatAccountId' => $wechatAccountId,
'wechatFriendId' => $wechatFriendId,
'wechatChatroomId' => $wechatChatroomId,
'from' => '',
'to' => '',
];
$newData = $this->fetchLatestMessageFromApi($messageRequest);
if (!empty($newData)){
$item['sendStatus'] = 0;
}
}
// 格式化时间
$item['wechatTime'] = !empty($item['wechatTime']) ? date('Y-m-d H:i:s', $item['wechatTime']) : '';
}
unset($item);
return ResponseHelper::success(['total' => $total, 'list' => $list]);
}
/**
* 从线上接口获取最新消息
* @param array $messageRequest 消息项包含wechatAccountId、wechatFriendId或wechatChatroomId、id等
* @return array|null 最新消息数据失败返回null
*/
private function fetchLatestMessageFromApi($messageRequest)
{
if (empty($this->baseUrl) || empty($this->authorization)) {
return null;
}
try {
// 设置请求头
$headerData = ['client:system'];
$header = setHeader($headerData, $this->authorization, 'json');
// 判断是好友消息还是群聊消息
if (!empty($messageRequest['wechatFriendId'])) {
// 好友消息接口
$params = [
'keyword' => '',
'msgType' => '',
'accountId' => '',
'count' => 20, // 获取多条消息以便找到对应的消息
'messageId' => isset($messageRequest['id']) ? $messageRequest['id'] : '',
'olderData' => true,
'wechatAccountId' => $messageRequest['wechatAccountId'],
'wechatFriendId' => $messageRequest['wechatFriendId'],
'from' => $messageRequest['from'],
'to' => $messageRequest['to'],
'searchFrom' => 'admin'
];
$result = requestCurl($this->baseUrl . 'api/FriendMessage/searchMessage', $params, 'GET', $header, 'json');
$response = handleApiResponse($result);
// 查找对应的消息
if (!empty($response) && is_array($response)) {
$data = $response[0];
if ($data['sendStatus'] == 0){
WechatMessageModel::where(['id' => $data['id']])->update(['sendStatus' => 0]);
return true;
}
}
return false;
} elseif (!empty($messageRequest['wechatChatroomId'])) {
// 群聊消息接口
$params = [
'keyword' => '',
'msgType' => '',
'accountId' => '',
'count' => 20, // 获取多条消息以便找到对应的消息
'messageId' => isset($messageRequest['id']) ? $messageRequest['id'] : '',
'olderData' => true,
'wechatId' => '',
'wechatAccountId' => $messageRequest['wechatAccountId'],
'wechatChatroomId' => $messageRequest['wechatChatroomId'],
'from' => $messageRequest['from'],
'to' => $messageRequest['to'],
'searchFrom' => 'admin'
];
$result = requestCurl($this->baseUrl . 'api/ChatroomMessage/searchMessage', $params, 'GET', $header, 'json');
$response = handleApiResponse($result);
// 查找对应的消息
if (!empty($response) && is_array($response)) {
$data = $response[0];
if ($data['sendStatus'] == 0){
WechatMessageModel::where(['id' => $data['id']])->update(['sendStatus' => 0]);
return true;
}
}
return false;
}
} catch (\Exception $e) {
// 记录错误日志,但不影响主流程
\think\facade\Log::error('获取线上最新消息失败:' . $e->getMessage());
}
return null;
}
/**
* 更新数据库中的消息
* @param array $latestMessage 线上获取的最新消息
* @param array $oldMessage 旧消息数据
*/
private function updateMessageInDatabase($latestMessage, $oldMessage)
{
try {
// 使用API模块的MessageController来保存消息
$apiMessageController = new \app\api\controller\MessageController();
// 判断是好友消息还是群聊消息
if (!empty($oldMessage['wechatFriendId'])) {
// 保存好友消息
$apiMessageController->saveMessage($latestMessage);
} elseif (!empty($oldMessage['wechatChatroomId'])) {
// 保存群聊消息
$apiMessageController->saveChatroomMessage($latestMessage);
}
} catch (\Exception $e) {
// 记录错误日志,但不影响主流程
\think\facade\Log::error('更新数据库消息失败:' . $e->getMessage());
}
}
}

View File

@@ -16,6 +16,8 @@ class TokensRecordController extends BaseController
$limit = $this->request->param('limit', 10);
$type = $this->request->param('type', '');
$form = $this->request->param('form', '');
$startTime = $this->request->param('startTime', '');
$endTime = $this->request->param('endTime', '');
$userId = $this->getUserInfo('id');
$companyId = $this->getUserInfo('companyId');
@@ -32,6 +34,26 @@ class TokensRecordController extends BaseController
$where[] = ['form','=',$form];
}
// 时间筛选
if (!empty($startTime)) {
// 支持时间戳或日期字符串格式
$startTimestamp = is_numeric($startTime) ? intval($startTime) : strtotime($startTime);
if ($startTimestamp !== false) {
$where[] = ['createTime', '>=', $startTimestamp];
}
}
if (!empty($endTime)) {
// 支持时间戳或日期字符串格式
$endTimestamp = is_numeric($endTime) ? intval($endTime) : strtotime($endTime);
if ($endTimestamp !== false) {
// 如果是日期字符串自动设置为当天的23:59:59
if (!is_numeric($endTime)) {
$endTimestamp = strtotime(date('Y-m-d 23:59:59', $endTimestamp));
}
$where[] = ['createTime', '<=', $endTimestamp];
}
}
$query = TokensRecord::where($where);
$total = $query->count();
@@ -78,9 +100,6 @@ class TokensRecordController extends BaseController
return ResponseHelper::error('类型参数错误0为减少1为增加');
}
if (!in_array($form, [0, 1, 2, 3, 4, 5])) {
return ResponseHelper::error('来源参数错误');
}
// 重试机制最多重试3次
$maxRetries = 3;
@@ -108,7 +127,7 @@ class TokensRecordController extends BaseController
Db::startTrans();
try {
// 使用悲观锁获取用户当前tokens余额确保并发安全
$userInfo = TokensCompany::where('companyId', $companyId)->lock(true)->find();
$userInfo = TokensCompany::where(['companyId'=> $companyId,'userId' => $userId])->lock(true)->find();
if (!$userInfo) {
throw new \Exception('用户不存在');
}

View File

@@ -14,13 +14,30 @@ class WechatChatroomController extends BaseController
public function getList(){
$page = $this->request->param('page', 1);
$limit = $this->request->param('limit', 10);
$keyword = $this->request->param('keyword', '');
$groupIds = $this->request->param('groupIds', '');
$accountId = $this->getUserInfo('s2_accountId');
if (empty($accountId)){
return ResponseHelper::error('请先登录');
}
$query = Db::table('s2_wechat_chatroom')
->where(['accountId' => $accountId,'isDeleted' => 0])
->order('id desc');
->where(['accountId' => $accountId,'isDeleted' => 0]);
// 关键字搜索群昵称、微信号这里使用chatroomId作为群标识
if ($keyword !== '' && $keyword !== null) {
$query->where(function ($q) use ($keyword) {
$like = '%' . $keyword . '%';
$q->whereLike('nickname', $like)
->whereOr('conRemark', 'like', $like);
});
}
// 分组筛选groupIds单个分组ID
if ($groupIds !== '' && $groupIds !== null) {
$query->where('groupIds', $groupIds);
}
$query->order('id desc');
$total = $query->count();
$list = $query->page($page, $limit)->select();
@@ -168,7 +185,7 @@ class WechatChatroomController extends BaseController
$data = [
'tokens' => $res['data']['token'],
'type' => 0,
'form' => 3,
'form' => 14,
'wechatAccountId' => $wechatAccountId,
'friendIdOrGroupId' => $groupId,
'remarks' => $remarks,

View File

@@ -13,17 +13,36 @@ class WechatFriendController extends BaseController
{
$page = $this->request->param('page', 1);
$limit = $this->request->param('limit', 10);
$keyword = $this->request->param('keyword', '');
$groupIds = $this->request->param('groupIds', '');
$accountId = $this->getUserInfo('s2_accountId');
if (empty($accountId)) {
return ResponseHelper::error('请先登录');
}
$query = Db::table('s2_wechat_friend')
->where(['accountId' => $accountId, 'isDeleted' => 0])
->order('id desc');
->where(['accountId' => $accountId, 'isDeleted' => 0]);
// 关键字搜索:昵称、备注、微信号
if ($keyword !== '' && $keyword !== null) {
$query->where(function ($q) use ($keyword) {
$like = '%' . $keyword . '%';
$q->whereLike('nickname', $like)
->whereOr('conRemark', 'like', $like)
->whereOr('alias', 'like', $like)
->whereOr('wechatId', 'like', $like);
});
}
// 分组筛选groupIds单个分组ID
if ($groupIds !== '' && $groupIds !== null) {
$query->where('groupIds', $groupIds);
}
$query->order('id desc');
$total = $query->count();
$list = $query->page($page, $limit)->select();
// 提取所有好友ID
// 提取所有好友ID
$friendIds = array_column($list, 'id');
$aiTypeData = [];
@@ -67,7 +86,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 +97,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]);
}
@@ -114,6 +133,7 @@ class WechatFriendController extends BaseController
$requestData = $this->request->param();
$updatableColumns = [
'phone',
'conRemark',
];
$columnUpdates = [];
@@ -166,4 +186,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

@@ -4,27 +4,32 @@ namespace app\chukebao\controller;
use library\ResponseHelper;
use think\Db;
use app\chukebao\model\ChatGroups;
class WechatGroupController extends BaseController
{
public function getList(){
$accountId = $this->getUserInfo('s2_accountId');
$userId = $this->getUserInfo('id');
/**
* 获取分组列表
* @return \think\response\Json
* @throws \Exception
*/
public function getList()
{
// 公司维度分组,不强制校验 userId
$companyId = $this->getUserInfo('companyId');
$query = Db::table('s2_wechat_group')
->where(function ($query) use ($accountId,$companyId) {
$query->where('accountId', $accountId)->whereOr('departmentId', $companyId);
})
->whereIn('groupType',[1,2])
->order('groupType desc,sortIndex desc,id desc');
$list = $query->select();
$query = ChatGroups::where([
'companyId' => $companyId,
'isDel' => 0,
])
->order('groupType desc,sort desc,id desc');
$total = $query->count();
$list = $query->select();
// 处理每个好友的数据
// 处理每个分组的数据
$list = is_array($list) ? $list : $list->toArray();
foreach ($list as $k => &$v) {
$v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s', $v['createTime']) : '';
}
@@ -32,4 +37,237 @@ class WechatGroupController extends BaseController
return ResponseHelper::success(['list'=>$list,'total'=>$total]);
}
/**
* 新增分组
* @return \think\response\Json
* @throws \Exception
*/
public function create()
{
$groupName = $this->request->param('groupName', '');
$groupMemo = $this->request->param('groupMemo', '');
$groupType = $this->request->param('groupType', 1);
$sort = $this->request->param('sort', 0);
$companyId = $this->getUserInfo('companyId');
// 只校验公司维度
if (empty($companyId)) {
return ResponseHelper::error('请先登录');
}
if (empty($groupName)) {
return ResponseHelper::error('分组名称不能为空');
}
// 验证分组类型
if (!in_array($groupType, [1, 2])) {
return ResponseHelper::error('无效的分组类型');
}
Db::startTrans();
try {
$chatGroup = new ChatGroups();
$chatGroup->groupName = $groupName;
$chatGroup->groupMemo = $groupMemo;
$chatGroup->groupType = $groupType;
$chatGroup->sort = $sort;
$chatGroup->userId = $this->getUserInfo('id');
$chatGroup->companyId = $companyId;
$chatGroup->createTime = time();
$chatGroup->isDel = 0;
$chatGroup->save();
Db::commit();
return ResponseHelper::success(['id' => $chatGroup->id], '创建成功');
} catch (\Exception $e) {
Db::rollback();
return ResponseHelper::error('创建失败:' . $e->getMessage());
}
}
/**
* 更新分组
* @return \think\response\Json
* @throws \Exception
*/
public function update()
{
$id = $this->request->param('id', 0);
$groupName = $this->request->param('groupName', '');
$groupMemo = $this->request->param('groupMemo', '');
$groupType = $this->request->param('groupType', 1);
$sort = $this->request->param('sort', 0);
$companyId = $this->getUserInfo('companyId');
if (empty($companyId)) {
return ResponseHelper::error('请先登录');
}
if (empty($id)) {
return ResponseHelper::error('参数缺失');
}
if (empty($groupName)) {
return ResponseHelper::error('分组名称不能为空');
}
// 验证分组类型
if (!in_array($groupType, [1, 2])) {
return ResponseHelper::error('无效的分组类型');
}
// 检查分组是否存在
$chatGroup = ChatGroups::where([
'id' => $id,
'companyId' => $companyId,
'isDel' => 0,
])->find();
if (empty($chatGroup)) {
return ResponseHelper::error('该分组不存在或已删除');
}
Db::startTrans();
try {
$chatGroup->groupName = $groupName;
$chatGroup->groupMemo = $groupMemo;
$chatGroup->groupType = $groupType;
$chatGroup->sort = $sort;
$chatGroup->save();
Db::commit();
return ResponseHelper::success('', '更新成功');
} catch (\Exception $e) {
Db::rollback();
return ResponseHelper::error('更新失败:' . $e->getMessage());
}
}
/**
* 删除分组(假删除)
* @return \think\response\Json
* @throws \Exception
*/
public function delete()
{
$id = $this->request->param('id', 0);
$companyId = $this->getUserInfo('companyId');
if (empty($companyId)) {
return ResponseHelper::error('请先登录');
}
if (empty($id)) {
return ResponseHelper::error('参数缺失');
}
// 检查分组是否存在
$chatGroup = ChatGroups::where([
'id' => $id,
'companyId' => $companyId,
'isDel' => 0,
])->find();
if (empty($chatGroup)) {
return ResponseHelper::error('该分组不存在或已删除');
}
Db::startTrans();
try {
// 1. 假删除当前分组
$chatGroup->isDel = 1;
$chatGroup->deleteTime = time();
$chatGroup->save();
// 2. 重置该分组下所有好友的分组IDs2_wechat_friend.groupIds -> 0
Db::table('s2_wechat_friend')
->where('groupIds', $id)
->update(['groupIds' => 0]);
// 3. 重置该分组下所有微信群的分组IDs2_wechat_chatroom.groupIds -> 0
Db::table('s2_wechat_chatroom')
->where('groupIds', $id)
->update(['groupIds' => 0]);
Db::commit();
return ResponseHelper::success('', '删除成功');
} catch (\Exception $e) {
Db::rollback();
return ResponseHelper::error('删除失败:' . $e->getMessage());
}
}
/**
* 移动分组(将好友或群移动到指定分组)
* @return \think\response\Json
* @throws \Exception
*/
public function move()
{
// type: friend 好友, chatroom 群
$type = $this->request->param('type', 'friend');
$targetId = (int)$this->request->param('groupId', 0);
// 仅支持单个ID移动
$idParam = $this->request->param('id', 0);
$companyId = $this->getUserInfo('companyId');
if (empty($companyId)) {
return ResponseHelper::error('请先登录');
}
if (empty($targetId)) {
return ResponseHelper::error('目标分组ID不能为空');
}
// 仅允许单个 ID禁止批量
$moveId = (int)$idParam;
if (empty($moveId)) {
return ResponseHelper::error('需要移动的ID不能为空');
}
// 校验目标分组是否存在且属于当前公司
$targetGroup = ChatGroups::where([
'id' => $targetId,
'companyId' => $companyId,
'isDel' => 0,
])->find();
if (empty($targetGroup)) {
return ResponseHelper::error('目标分组不存在或已删除');
}
// 校验分组类型与移动对象类型是否匹配
// groupType: 1=好友分组, 2=群分组
if ($type === 'friend' && (int)$targetGroup->groupType !== 1) {
return ResponseHelper::error('目标分组类型错误(需要好友分组)');
}
if ($type === 'chatroom' && (int)$targetGroup->groupType !== 2) {
return ResponseHelper::error('目标分组类型错误(需要群分组)');
}
Db::startTrans();
try {
if ($type === 'friend') {
// 移动单个好友到指定分组:更新 s2_wechat_friend.groupIds
Db::table('s2_wechat_friend')
->where('id', $moveId)
->update(['groupIds' => $targetId]);
} elseif ($type === 'chatroom') {
// 移动单个群到指定分组:更新 s2_wechat_chatroom.groupIds
Db::table('s2_wechat_chatroom')
->where('id', $moveId)
->update(['groupIds' => $targetId]);
} else {
Db::rollback();
return ResponseHelper::error('无效的类型参数');
}
Db::commit();
return ResponseHelper::success('', '移动成功');
} catch (\Exception $e) {
Db::rollback();
return ResponseHelper::error('移动失败:' . $e->getMessage());
}
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace app\chukebao\model;
use think\Model;
class ChatGroups extends Model
{
protected $pk = 'id';
protected $name = 'chat_groups';
// 不开启自动时间戳,手动维护 createTime / deleteTime
protected $autoWriteTimestamp = false;
}

View File

@@ -39,4 +39,10 @@ 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', // 更新微信账号评分记录
// 统一任务调度器
'scheduler:run' => 'app\command\TaskSchedulerCommand', // 统一任务调度器,支持多进程并发执行
];

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,478 @@
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\facade\Config;
use think\facade\Log;
use think\facade\Cache;
/**
* 统一任务调度器
* 支持多进程并发执行任务
*
* 使用方法:
* php think scheduler:run
*
* 在 crontab 中配置:
* * * * * * cd /path/to/project && php think scheduler:run >> /path/to/log/scheduler.log 2>&1
*/
class TaskSchedulerCommand extends Command
{
/**
* 任务配置
*/
protected $tasks = [];
/**
* 最大并发进程数
*/
protected $maxConcurrent = 10;
/**
* 当前运行的进程数
*/
protected $runningProcesses = [];
/**
* 日志目录
*/
protected $logDir = '';
protected function configure()
{
$this->setName('scheduler:run')
->setDescription('统一任务调度器,支持多进程并发执行所有定时任务');
}
protected function execute(Input $input, Output $output)
{
$output->writeln('==========================================');
$output->writeln('任务调度器启动');
$output->writeln('时间: ' . date('Y-m-d H:i:s'));
$output->writeln('==========================================');
// 检查是否支持 pcntl 扩展
if (!function_exists('pcntl_fork')) {
$output->writeln('<error>错误:系统不支持 pcntl 扩展,无法使用多进程功能</error>');
$output->writeln('<info>提示:将使用单进程顺序执行任务</info>');
$this->maxConcurrent = 1;
}
// 加载任务配置(优先使用框架配置,其次直接引入配置文件,避免加载失败)
$this->tasks = Config::get('task_scheduler', []);
// 如果通过 Config 没有读到,再尝试直接 include 配置文件
if (empty($this->tasks)) {
// 以项目根目录为基准查找 config/task_scheduler.php
$configFile = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'task_scheduler.php';
if (is_file($configFile)) {
$config = include $configFile;
if (is_array($config) && !empty($config)) {
$this->tasks = $config;
}
}
}
if (empty($this->tasks)) {
$output->writeln('<error>错误未找到任务配置task_scheduler请检查 config/task_scheduler.php 是否存在且返回数组</error>');
return false;
}
// 设置日志目录ThinkPHP5 中无 runtime_path 辅助函数,直接使用 ROOT_PATH/runtime/log
if (!defined('ROOT_PATH')) {
// CLI 下正常情况下 ROOT_PATH 已在入口脚本 define这里兜底一次
define('ROOT_PATH', dirname(__DIR__, 2));
}
$this->logDir = ROOT_PATH . DIRECTORY_SEPARATOR . 'runtime' . DIRECTORY_SEPARATOR . 'log' . DIRECTORY_SEPARATOR;
if (!is_dir($this->logDir)) {
mkdir($this->logDir, 0755, true);
}
// 获取当前时间
$currentTime = time();
$currentMinute = date('i', $currentTime);
$currentHour = date('H', $currentTime);
$currentDay = date('d', $currentTime);
$currentMonth = date('m', $currentTime);
$currentWeekday = date('w', $currentTime); // 0=Sunday, 6=Saturday
$output->writeln("当前时间: {$currentHour}:{$currentMinute}");
$output->writeln("已加载 " . count($this->tasks) . " 个任务配置");
// 筛选需要执行的任务
$tasksToRun = [];
foreach ($this->tasks as $taskId => $task) {
if (!isset($task['enabled']) || !$task['enabled']) {
continue;
}
if ($this->shouldRun($task['schedule'], $currentMinute, $currentHour, $currentDay, $currentMonth, $currentWeekday)) {
$tasksToRun[$taskId] = $task;
}
}
if (empty($tasksToRun)) {
$output->writeln('<info>当前时间没有需要执行的任务</info>');
return true;
}
$output->writeln("找到 " . count($tasksToRun) . " 个需要执行的任务");
// 执行任务
if ($this->maxConcurrent > 1 && function_exists('pcntl_fork')) {
$this->executeConcurrent($tasksToRun, $output);
} else {
$this->executeSequential($tasksToRun, $output);
}
// 清理僵尸进程
$this->cleanupZombieProcesses();
$output->writeln('==========================================');
$output->writeln('任务调度器执行完成');
$output->writeln('==========================================');
return true;
}
/**
* 判断任务是否应该执行
*
* @param string $schedule cron表达式格式分钟 小时 日 月 星期
* @param int $minute 当前分钟
* @param int $hour 当前小时
* @param int $day 当前日期
* @param int $month 当前月份
* @param int $weekday 当前星期
* @return bool
*/
protected function shouldRun($schedule, $minute, $hour, $day, $month, $weekday)
{
$parts = preg_split('/\s+/', trim($schedule));
if (count($parts) < 5) {
return false;
}
list($scheduleMinute, $scheduleHour, $scheduleDay, $scheduleMonth, $scheduleWeekday) = $parts;
// 解析分钟
if (!$this->matchCronField($scheduleMinute, $minute)) {
return false;
}
// 解析小时
if (!$this->matchCronField($scheduleHour, $hour)) {
return false;
}
// 解析日期
if (!$this->matchCronField($scheduleDay, $day)) {
return false;
}
// 解析月份
if (!$this->matchCronField($scheduleMonth, $month)) {
return false;
}
// 解析星期注意cron中0和7都表示星期日
if ($scheduleWeekday !== '*') {
$scheduleWeekday = str_replace('7', '0', $scheduleWeekday);
if (!$this->matchCronField($scheduleWeekday, $weekday)) {
return false;
}
}
return true;
}
/**
* 匹配cron字段
*
* @param string $field cron字段表达式
* @param int $value 当前值
* @return bool
*/
protected function matchCronField($field, $value)
{
// 通配符
if ($field === '*') {
return true;
}
// 列表(逗号分隔)
if (strpos($field, ',') !== false) {
$values = explode(',', $field);
foreach ($values as $v) {
if ($this->matchCronField(trim($v), $value)) {
return true;
}
}
return false;
}
// 范围(如 1-5
if (strpos($field, '-') !== false) {
list($start, $end) = explode('-', $field);
return $value >= (int)$start && $value <= (int)$end;
}
// 步长(如 */5 或 0-59/5
if (strpos($field, '/') !== false) {
$parts = explode('/', $field);
$base = $parts[0];
$step = (int)$parts[1];
if ($base === '*') {
return $value % $step === 0;
} else {
// 处理范围步长,如 0-59/5
if (strpos($base, '-') !== false) {
list($start, $end) = explode('-', $base);
if ($value >= (int)$start && $value <= (int)$end) {
return ($value - (int)$start) % $step === 0;
}
return false;
} else {
return $value % $step === 0;
}
}
}
// 精确匹配
return (int)$field === $value;
}
/**
* 并发执行任务(多进程)
*
* @param array $tasks 任务列表
* @param Output $output 输出对象
*/
protected function executeConcurrent($tasks, Output $output)
{
$output->writeln('<info>使用多进程并发执行任务(最大并发数:' . $this->maxConcurrent . '</info>');
foreach ($tasks as $taskId => $task) {
// 等待可用进程槽
while (count($this->runningProcesses) >= $this->maxConcurrent) {
$this->waitForProcesses();
usleep(100000); // 等待100ms
}
// 检查任务是否已经在运行(防止重复执行)
$lockKey = "scheduler_task_lock:{$taskId}";
$lockTime = Cache::get($lockKey);
if ($lockTime && (time() - $lockTime) < 300) { // 5分钟内不重复执行
$output->writeln("<comment>任务 {$taskId} 正在运行中,跳过</comment>");
continue;
}
// 创建子进程
$pid = pcntl_fork();
if ($pid == -1) {
// 创建进程失败
$output->writeln("<error>创建子进程失败:{$taskId}</error>");
Log::error("任务调度器:创建子进程失败", ['task' => $taskId]);
continue;
} elseif ($pid == 0) {
// 子进程:执行任务
$this->runTask($taskId, $task);
exit(0);
} else {
// 父进程记录子进程PID
$this->runningProcesses[$pid] = [
'task_id' => $taskId,
'start_time' => time(),
];
$output->writeln("<info>启动任务:{$taskId} (PID: {$pid})</info>");
// 设置任务锁
Cache::set($lockKey, time(), 600); // 10分钟过期
}
}
// 等待所有子进程完成
while (!empty($this->runningProcesses)) {
$this->waitForProcesses();
usleep(500000); // 等待500ms
}
}
/**
* 顺序执行任务(单进程)
*
* @param array $tasks 任务列表
* @param Output $output 输出对象
*/
protected function executeSequential($tasks, Output $output)
{
$output->writeln('<info>使用单进程顺序执行任务</info>');
foreach ($tasks as $taskId => $task) {
$output->writeln("<info>执行任务:{$taskId}</info>");
$this->runTask($taskId, $task);
}
}
/**
* 执行单个任务
*
* @param string $taskId 任务ID
* @param array $task 任务配置
*/
protected function runTask($taskId, $task)
{
$startTime = microtime(true);
$logFile = $this->logDir . ($task['log_file'] ?? "scheduler_{$taskId}.log");
// 确保日志目录存在
$logDir = dirname($logFile);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
// 构建命令
// 使用项目根目录下的 think 脚本(同命令行 php think
if (!defined('ROOT_PATH')) {
define('ROOT_PATH', dirname(__DIR__, 2));
}
$thinkPath = ROOT_PATH . DIRECTORY_SEPARATOR . 'think';
$command = "php {$thinkPath} {$task['command']}";
if (!empty($task['options'])) {
foreach ($task['options'] as $option) {
$command .= ' ' . escapeshellarg($option);
}
}
// 添加日志重定向
$command .= " >> " . escapeshellarg($logFile) . " 2>&1";
// 记录任务开始
$logMessage = "\n" . str_repeat('=', 60) . "\n";
$logMessage .= "任务开始执行: {$taskId}\n";
$logMessage .= "执行时间: " . date('Y-m-d H:i:s') . "\n";
$logMessage .= "命令: {$command}\n";
$logMessage .= str_repeat('=', 60) . "\n";
file_put_contents($logFile, $logMessage, FILE_APPEND);
// 执行命令
$descriptorspec = [
0 => ['file', (PHP_OS_FAMILY === 'Windows' ? 'NUL' : '/dev/null'), 'r'], // stdin
1 => ['file', $logFile, 'a'], // stdout
2 => ['file', $logFile, 'a'], // stderr
];
$process = @proc_open($command, $descriptorspec, $pipes, ROOT_PATH);
if (is_resource($process)) {
// 关闭管道
if (isset($pipes[0])) @fclose($pipes[0]);
if (isset($pipes[1])) @fclose($pipes[1]);
if (isset($pipes[2])) @fclose($pipes[2]);
// 设置超时
$timeout = $task['timeout'] ?? 3600;
$startWaitTime = time();
// 等待进程完成或超时
while (true) {
$status = proc_get_status($process);
if (!$status['running']) {
break;
}
// 检查超时
if ((time() - $startWaitTime) > $timeout) {
if (function_exists('proc_terminate')) {
proc_terminate($process, SIGTERM);
// 等待进程终止
sleep(2);
$status = proc_get_status($process);
if ($status['running']) {
// 强制终止
proc_terminate($process, SIGKILL);
}
}
Log::warning("任务执行超时", [
'task' => $taskId,
'timeout' => $timeout,
]);
break;
}
usleep(500000); // 等待500ms
}
// 关闭进程
proc_close($process);
} else {
// 如果 proc_open 失败,尝试直接执行(后台执行)
if (PHP_OS_FAMILY === 'Windows') {
pclose(popen("start /B " . $command, "r"));
} else {
exec($command . ' > /dev/null 2>&1 &');
}
}
$endTime = microtime(true);
$duration = round($endTime - $startTime, 2);
// 记录任务完成
$logMessage = "\n" . str_repeat('=', 60) . "\n";
$logMessage .= "任务执行完成: {$taskId}\n";
$logMessage .= "完成时间: " . date('Y-m-d H:i:s') . "\n";
$logMessage .= "执行时长: {$duration}\n";
$logMessage .= str_repeat('=', 60) . "\n";
file_put_contents($logFile, $logMessage, FILE_APPEND);
Log::info("任务执行完成", [
'task' => $taskId,
'duration' => $duration,
]);
}
/**
* 等待进程完成
*/
protected function waitForProcesses()
{
foreach ($this->runningProcesses as $pid => $info) {
$status = 0;
$result = pcntl_waitpid($pid, $status, WNOHANG);
if ($result == $pid || $result == -1) {
// 进程已结束
unset($this->runningProcesses[$pid]);
$duration = time() - $info['start_time'];
Log::info("子进程执行完成", [
'pid' => $pid,
'task' => $info['task_id'],
'duration' => $duration,
]);
}
}
}
/**
* 清理僵尸进程
*/
protected function cleanupZombieProcesses()
{
if (!function_exists('pcntl_waitpid')) {
return;
}
$status = 0;
while (($pid = pcntl_waitpid(-1, $status, WNOHANG)) > 0) {
// 清理僵尸进程
}
}
}

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

@@ -34,6 +34,7 @@ class WorkbenchGroupCreateCommand extends Command
// 检查队列是否已经在运行
$queueLockKey = "queue_lock:{$this->queueName}";
Cache::rm($queueLockKey);
if (Cache::get($queueLockKey)) {
$output->writeln("队列 {$this->queueName} 已经在运行中,跳过执行");
Log::warning("队列 {$this->queueName} 已经在运行中,跳过执行");

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