diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..39ab957
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,18 @@
+.idea
+.env
+.vscode
+.DS_Store
+runtime
+public/upload
+vendor
+/public/.user.ini
+/404.html
+# SpecStory explanation file
+.specstory/.what-is-this.md
+/
+.cursorindexingignore
+.user.ini
+nginx.htaccess
+.cursor/
+thinkphp/
+public/static/
diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..e69de29
diff --git a/README.en.md b/README.en.md
deleted file mode 100644
index 54703ad..0000000
--- a/README.en.md
+++ /dev/null
@@ -1,36 +0,0 @@
-# 应用总接口
-
-#### Description
-{**When you're done, you can delete the content in this README and update the file with details for others getting started with your repository**}
-
-#### Software Architecture
-Software architecture description
-
-#### Installation
-
-1. xxxx
-2. xxxx
-3. xxxx
-
-#### Instructions
-
-1. xxxx
-2. xxxx
-3. xxxx
-
-#### Contribution
-
-1. Fork the repository
-2. Create Feat_xxx branch
-3. Commit your code
-4. Create Pull Request
-
-
-#### Gitee Feature
-
-1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
-2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
-3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
-4. The most valuable open source project [GVP](https://gitee.com/gvp)
-5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
-6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
diff --git a/README.md b/README.md
deleted file mode 100644
index fc9a5a3..0000000
--- a/README.md
+++ /dev/null
@@ -1,39 +0,0 @@
-# 应用总接口
-
-#### 介绍
-{**以下是 Gitee 平台说明,您可以替换此简介**
-Gitee 是 OSCHINA 推出的基于 Git 的代码托管平台(同时支持 SVN)。专为开发者提供稳定、高效、安全的云端软件开发协作平台
-无论是个人、团队、或是企业,都能够用 Gitee 实现代码托管、项目管理、协作开发。企业项目请看 [https://gitee.com/enterprises](https://gitee.com/enterprises)}
-
-#### 软件架构
-软件架构说明
-
-
-#### 安装教程
-
-1. xxxx
-2. xxxx
-3. xxxx
-
-#### 使用说明
-
-1. xxxx
-2. xxxx
-3. xxxx
-
-#### 参与贡献
-
-1. Fork 本仓库
-2. 新建 Feat_xxx 分支
-3. 提交代码
-4. 新建 Pull Request
-
-
-#### 特技
-
-1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
-2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
-3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
-4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
-5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
-6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
diff --git a/README_moments.md b/README_moments.md
new file mode 100644
index 0000000..9e31075
--- /dev/null
+++ b/README_moments.md
@@ -0,0 +1,147 @@
+# 微信朋友圈数据处理功能
+
+本模块提供了微信朋友圈数据的获取、存储和查询功能,支持保留驼峰命名结构的原始数据。
+
+## 数据库表结构
+
+项目包含一个数据表:
+
+**wechat_moments** - 存储朋友圈基本信息
+- `id`: 自增主键
+- `wechatAccountId`: 微信账号ID
+- `wechatFriendId`: 微信好友ID
+- `snsId`: 朋友圈消息ID
+- `commentList`: 评论列表JSON
+- `createTime`: 创建时间戳
+- `likeList`: 点赞列表JSON
+- `content`: 朋友圈内容
+- `lat`: 纬度
+- `lng`: 经度
+- `location`: 位置信息
+- `picSize`: 图片大小
+- `resUrls`: 资源URL列表
+- `userName`: 用户名
+- `type`: 朋友圈类型
+- `create_time`: 数据创建时间
+- `update_time`: 数据更新时间
+
+## API接口
+
+### 1. 获取朋友圈信息
+
+```
+GET/POST /api/websocket/getMoments
+```
+
+**参数:**
+- `wechatAccountId`: 微信账号ID
+- `wechatFriendId`: 微信好友ID
+- `count`: 获取条数,默认5条
+
+获取指定账号和好友的朋友圈信息,并自动保存到数据库。
+
+### 2. 保存单条朋友圈数据
+
+```
+POST /api/websocket/saveSingleMoment
+```
+
+**参数:**
+- `commentList`: 评论列表
+- `createTime`: 创建时间戳
+- `likeList`: 点赞列表
+- `momentEntity`: 朋友圈实体,包含以下字段:
+ - `content`: 朋友圈内容
+ - `lat`: 纬度
+ - `lng`: 经度
+ - `location`: 位置信息
+ - `picSize`: 图片大小
+ - `resUrls`: 资源URL列表
+ - `urls`: 媒体URL列表
+ - `userName`: 用户名
+- `snsId`: 朋友圈ID
+- `type`: 朋友圈类型
+- `wechatAccountId`: 微信账号ID
+- `wechatFriendId`: 微信好友ID
+
+保存单条朋友圈数据到数据库,保持原有的驼峰数据结构。系统会将`momentEntity`中的字段提取并单独存储,不包括`objectType`和`createTime`字段。
+
+### 3. 获取朋友圈数据列表
+
+```
+GET/POST /api/websocket/getMomentsList
+```
+
+**参数:**
+- `wechatAccountId`: 微信账号ID (可选)
+- `wechatFriendId`: 微信好友ID (可选)
+- `page`: 页码,默认1
+- `pageSize`: 每页条数,默认10
+- `startTime`: 开始时间戳 (可选)
+- `endTime`: 结束时间戳 (可选)
+
+获取已保存的朋友圈数据列表,支持分页和条件筛选。返回的数据会自动构建`momentEntity`字段以保持API兼容性。
+
+### 4. 获取朋友圈详情
+
+```
+GET/POST /api/websocket/getMomentDetail
+```
+
+**参数:**
+- `snsId`: 朋友圈ID
+- `wechatAccountId`: 微信账号ID
+
+获取单条朋友圈的详细信息,包括评论、点赞和资源URL等。返回的数据会自动构建`momentEntity`字段以保持API兼容性。
+
+## 使用示例
+
+### 保存单条朋友圈数据
+
+```php
+$data = [
+ 'commentList' => [],
+ 'createTime' => 1742777232,
+ 'likeList' => [],
+ 'momentEntity' => [
+ 'content' => "第一位个人与Stussy联名的中国名人,不是陈冠希,不是葛民辉,而是周杰伦!",
+ 'lat' => 0,
+ 'lng' => 0,
+ 'location' => "",
+ 'picSize' => 0,
+ 'resUrls' => [],
+ 'snsId' => "-3827269039168736643",
+ 'urls' => ["http://wxapp.tc.qq.com/251/20304/stodownload?encfilekey=..."],
+ 'userName' => "wxid_afixeeh53lt012"
+ ],
+ 'snsId' => "-3827269039168736643",
+ 'type' => 28,
+ 'wechatAccountId' => 123456, // 替换为实际的微信账号ID
+ 'wechatFriendId' => "wxid_example" // 替换为实际的微信好友ID
+];
+
+// 发送请求
+$result = curl_post('/api/websocket/saveSingleMoment', $data);
+```
+
+### 查询朋友圈列表
+
+```php
+// 获取特定账号的朋友圈
+$params = [
+ 'wechatAccountId' => 123456,
+ 'page' => 1,
+ 'pageSize' => 20
+];
+
+// 发送请求
+$result = curl_get('/api/websocket/getMomentsList', $params);
+```
+
+## 注意事项
+
+1. 所有JSON格式的数据在保存时都会进行编码,查询时会自动解码并还原为原始数据结构。
+2. 数据库中的字段名保持驼峰命名格式,与微信API返回的数据结构保持一致。
+3. 尽管数据库中将`momentEntity`的字段拆分为独立字段存储,但API接口返回时会重新构建`momentEntity`结构,以保持与原始API的兼容性。
+4. `objectType`和`createTime`字段已从`momentEntity`中移除,不再单独存储。
+5. 图片或视频资源URLs直接存储在朋友圈主表中,不再单独存储到资源表。
\ No newline at end of file
diff --git a/README_scheduler.md b/README_scheduler.md
new file mode 100644
index 0000000..4c48568
--- /dev/null
+++ b/README_scheduler.md
@@ -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 表达式调度
+- 支持任务锁机制
+
diff --git a/README_wechat_chatroom_sync.md b/README_wechat_chatroom_sync.md
new file mode 100644
index 0000000..fb2e186
--- /dev/null
+++ b/README_wechat_chatroom_sync.md
@@ -0,0 +1,105 @@
+# 微信群聊同步功能
+
+本功能用于自动同步微信群聊数据,支持分页获取群聊列表以及群成员信息,并将数据保存到数据库中。
+
+## 功能特点
+
+1. 支持分页获取微信群聊列表
+2. 自动获取每个群聊的成员信息
+3. 支持通过关键词筛选群聊
+4. 支持按微信账号筛选群聊
+5. 可选择是否包含已删除的群聊
+6. 使用队列处理,支持大量数据的同步
+7. 支持失败重试机制
+8. 提供命令行和HTTP接口两种触发方式
+
+## 数据表结构
+
+本功能使用以下数据表:
+
+1. **wechat_chatroom** - 存储微信群聊信息
+2. **wechat_chatroom_member** - 存储微信群聊成员信息
+
+## 使用方法
+
+### 1. HTTP接口触发
+
+```
+GET/POST /api/wechat_chatroom/syncChatrooms
+```
+
+**参数:**
+- `pageIndex`: 起始页码,默认0
+- `pageSize`: 每页大小,默认100
+- `keyword`: 群名关键词,可选
+- `wechatAccountKeyword`: 微信账号关键词,可选
+- `isDeleted`: 是否包含已删除群聊,可选
+
+**示例:**
+```
+/api/wechat_chatroom/syncChatrooms?pageSize=50
+```
+
+### 2. 命令行触发
+
+```bash
+php think sync:wechat:chatrooms [选项]
+```
+
+**选项:**
+- `-p, --pageIndex`: 起始页码,默认0
+- `-s, --pageSize`: 每页大小,默认100
+- `-k, --keyword`: 群名关键词,可选
+- `-a, --account`: 微信账号关键词,可选
+- `-d, --deleted`: 是否包含已删除群聊,可选
+
+**示例:**
+```bash
+# 基本用法
+php think sync:wechat:chatrooms
+
+# 指定页大小和关键词
+php think sync:wechat:chatrooms -s 50 -k "测试群"
+
+# 指定账号关键词
+php think sync:wechat:chatrooms --account "张三"
+```
+
+### 3. 定时任务配置
+
+可以将命令添加到系统的定时任务(crontab)中,实现定期自动同步:
+
+```
+# 每天凌晨3点执行微信群聊同步
+0 3 * * * cd /path/to/your/project && php think sync:wechat:chatrooms
+```
+
+## 队列消费者配置
+
+为了处理同步任务,需要启动队列消费者:
+
+```bash
+# 启动微信群聊队列消费者
+php think queue:work --queue wechat_chatrooms
+```
+
+建议在生产环境中使用supervisor等工具来管理队列消费者进程。
+
+## 同步过程
+
+1. 触发同步任务,将初始页任务加入队列
+2. 队列消费者处理任务,获取当前页的群聊列表
+3. 如果当前页有数据且数量等于页大小,则将下一页任务加入队列
+4. 对每个获取到的群聊,添加获取群成员的任务
+5. 所有数据会自动保存到数据库中
+
+## 调试与日志
+
+同步过程的日志会记录在应用的日志目录中,可以通过查看日志了解同步状态和错误信息。
+
+## 注意事项
+
+1. 页大小建议设置为合理值(50-100),过大会导致请求超时
+2. 当数据量较大时,建议增加队列消费者的数量
+3. 确保系统授权信息正确,否则无法获取数据
+4. 数据同步是增量的,会自动更新已存在的记录
\ No newline at end of file
diff --git a/RFM客户价值评分体系技术实施文档.md b/RFM客户价值评分体系技术实施文档.md
new file mode 100644
index 0000000..4b5fb84
--- /dev/null
+++ b/RFM客户价值评分体系技术实施文档.md
@@ -0,0 +1,249 @@
+# RFM 客户价值评分体系技术实施文档
+
+## 1. 文档目的
+
+本文档旨在明确 RFM(Recency-Frequency-Monetary)客户价值评分体系的技术实现标准,包括维度定义、评分规则、数据处理流程、参数配置及异常处理方案,为系统开发、数据分析及业务应用提供统一依据。
+
+## 2. 核心术语定义
+
+
+
+| 术语 | 英文缩写 | 定义 | 数据来源 | 统计周期说明 |
+| ------ | ------------ | ------------------------------- | --------- | ---------------------------------- |
+| 最近消费时间 | Recency(R) | 客户最后一次有效消费行为距统计截止日的时间间隔(单位:天) | 订单系统、交易日志 | 支持自定义配置(默认 3-12 个月,按业务场景调整) |
+| 消费频率 | Frequency(F) | 统计周期内客户发生有效消费行为的总次数 | 订单系统、交易日志 | 与 R 维度统计周期一致,剔除重复下单、取消订单等无效记录 |
+| 消费金额 | Monetary(M) | 统计周期内客户有效消费行为的总金额(单位:元,支持多币种换算) | 订单系统、支付日志 | 仅统计已支付完成的订单金额,剔除退款、优惠抵扣部分 |
+| RFM 总分 | RFM Score | 基于 R、F、M 三个维度的分项得分,按预设权重计算的综合得分 | 系统计算生成 | 得分范围 1-15 分(5 分制单项)或 1-100 分(标准化后) |
+
+## 3. 评分规则技术规范
+
+### 3.1 分项评分规则(默认 5 分制)
+
+#### 3.1.1 Recency(R)评分规则
+
+
+
+* 核心逻辑:时间间隔越短,得分越高(反向映射)
+
+* 分段标准:采用**五分位法**(按数据分布自动划分区间,避免均分失真)
+
+
+
+| 得分 | 时间间隔区间(天) | 划分逻辑 |
+| --- | ---------- | ----------------- |
+| 5 分 | \[0, T1] | 统计周期内最近消费的 20% 客户 |
+| 4 分 | (T1, T2] | 统计周期内次近消费的 20% 客户 |
+| 3 分 | (T2, T3] | 统计周期内中间消费的 20% 客户 |
+| 2 分 | (T3, T4] | 统计周期内较久消费的 20% 客户 |
+| 1 分 | (T4, Tmax] | 统计周期内最久消费的 20% 客户 |
+
+
+
+* 区间计算方式:T1=PERCENTILE\_CONT (0.2)、T2=PERCENTILE\_CONT (0.4)、T3=PERCENTILE\_CONT (0.6)、T4=PERCENTILE\_CONT (0.8),其中 Tmax 为统计周期总天数
+
+#### 3.1.2 Frequency(F)评分规则
+
+
+
+* 核心逻辑:消费次数越多,得分越高(正向映射)
+
+* 分段标准:采用**五分位法**(支持最小消费次数阈值配置)
+
+
+
+| 得分 | 消费次数区间 | 划分逻辑 |
+| --- | ----------- | ------------------- |
+| 5 分 | \[F4, +∞) | 统计周期内消费次数最多的 20% 客户 |
+| 4 分 | \[F3, F4) | 统计周期内消费次数次多的 20% 客户 |
+| 3 分 | \[F2, F3) | 统计周期内消费次数中间的 20% 客户 |
+| 2 分 | \[F1, F2) | 统计周期内消费次数较少的 20% 客户 |
+| 1 分 | \[Fmin, F1) | 统计周期内消费次数最少的 20% 客户 |
+
+
+
+* 区间计算方式:F1=PERCENTILE\_CONT (0.2)、F2=PERCENTILE\_CONT (0.4)、F3=PERCENTILE\_CONT (0.6)、F4=PERCENTILE\_CONT (0.8),其中 Fmin 为 1(仅统计有效消费次数≥1 的客户)
+
+#### 3.1.3 Monetary(M)评分规则
+
+
+
+* 核心逻辑:消费金额越高,得分越高(正向映射)
+
+* 分段标准:采用**五分位法**(支持剔除大额异常值后划分)
+
+
+
+| 得分 | 消费金额区间(元) | 划分逻辑 |
+| --- | ----------- | ------------------- |
+| 5 分 | \[M4, +∞) | 统计周期内消费金额最高的 20% 客户 |
+| 4 分 | \[M3, M4) | 统计周期内消费金额次高的 20% 客户 |
+| 3 分 | \[M2, M3) | 统计周期内消费金额中间的 20% 客户 |
+| 2 分 | \[M1, M2) | 统计周期内消费金额较低的 20% 客户 |
+| 1 分 | \[Mmin, M1) | 统计周期内消费金额最低的 20% 客户 |
+
+
+
+* 区间计算方式:M1=PERCENTILE\_CONT (0.2)、M2=PERCENTILE\_CONT (0.4)、M3=PERCENTILE\_CONT (0.6)、M4=PERCENTILE\_CONT (0.8),其中 Mmin 为统计周期内最小有效订单金额
+
+### 3.2 总分计算规则
+
+#### 3.2.1 加权求和公式
+
+$RFM_{Score} = R_{Score} \times W_R + F_{Score} \times W_F + M_{Score} \times W_M$
+
+
+
+* 权重配置:支持自定义(默认配置:$W_R=0.4$,$W_F=0.3$,$W_M=0.3$)
+
+* 权重约束:$W_R + W_F + W_M = 1.0$,且单个权重取值范围为 \[0.1, 0.8]
+
+#### 3.2.2 得分标准化(可选)
+
+
+
+* 若需将总分映射为 1-100 分,采用线性标准化公式:
+
+ $RFM_{StandardScore} = \frac{RFM_{Score} - RFM_{Min}}{RFM_{Max} - RFM_{Min}} \times 99 + 1$
+
+* 其中:$RFM_{Min}=W_R \times 1 + W_F \times 1 + W_M \times 1$,$RFM_{Max}=W_R \times 5 + W_F \times 5 + W_M \times 5$
+
+## 4. 数据处理流程
+
+### 4.1 数据输入要求
+
+
+
+| 数据项 | 数据类型 | 格式要求 | 校验规则 |
+| ------ | ------------- | ------------------- | ------------ |
+| 客户唯一标识 | String/Int | 全局唯一(如用户 ID、会员 ID) | 非空、去重 |
+| 订单唯一标识 | String/Int | 全局唯一(如订单号) | 非空、去重 |
+| 消费时间 | DateTime | yyyy-MM-dd HH:mm:ss | 需在统计周期内 |
+| 消费金额 | Decimal(18,2) | 大于 0 | 剔除负数、0 值 |
+| 订单状态 | String | 枚举值(已支付、已取消、已退款等) | 仅保留 “已支付” 状态 |
+
+### 4.2 数据预处理步骤
+
+
+
+1. **数据过滤**:
+
+* 剔除统计周期外的订单数据
+
+* 剔除订单状态为 “已取消”“已退款”“无效” 的记录
+
+* 剔除员工内部订单、测试订单(按订单标签或用户标签过滤)
+
+* 剔除单笔金额超过$M_{99分位值} \times 3$的异常大额订单(可配置开关)
+
+1. **数据聚合**:
+
+* 按客户唯一标识分组,计算 R、F、M 原始指标:
+
+
+ * R:MAX (消费时间) 到统计截止日的时间间隔(天)
+
+ * F:COUNT (DISTINCT 订单唯一标识)
+
+ * M:SUM (消费金额)
+
+1. **缺失值处理**:
+
+* 统计周期内无消费记录的客户:R = 统计周期总天数,F=0,M=0,分项得分均为 1 分
+
+* 单个指标缺失(如仅缺失 M):按 1 分计分项得分
+
+### 4.3 评分计算流程
+
+
+
+```mermaid
+graph TD
+ A[数据输入] --> B[数据过滤]
+ B --> C[数据聚合计算R/F/M原始值]
+ C --> D[缺失值处理]
+ D --> E[按五分位法划分各维度区间]
+ E --> F[计算R/F/M分项得分]
+ F --> G[按权重计算RFM总分]
+ G --> H[可选:标准化为1-100分]
+ H --> I[输出客户RFM评分结果]
+```
+
+## 5. 参数配置说明
+
+
+
+| 参数名称 | 配置项 | 取值范围 | 默认值 | 配置方式 |
+| ------- | ---------------------- | ------------- | ------ | --------------- |
+| 统计周期 | cycle\_days | 30-365 | 180 | 系统配置页手动输入 |
+| R 维度权重 | weight\_R | 0.1-0.8 | 0.4 | 系统配置页滑动条调整 |
+| F 维度权重 | weight\_F | 0.1-0.8 | 0.3 | 系统配置页滑动条调整 |
+| M 维度权重 | weight\_M | 0.1-0.8 | 0.3 | 系统配置页滑动条调整 |
+| 异常金额阈值 | abnormal\_money\_ratio | 1.5-5.0 | 3.0 | 系统配置页手动输入(倍数关系) |
+| 评分分制 | score\_scale | 5 分制 / 100 分制 | 5 分制 | 系统配置页单选 |
+| 缺失值处理策略 | missing\_strategy | 按 1 分计 / 剔除客户 | 按 1 分计 | 系统配置页单选 |
+
+## 6. 异常处理方案
+
+### 6.1 数据异常
+
+
+
+| 异常类型 | 表现形式 | 处理逻辑 | 影响范围 |
+| ------ | ------------------------------ | ---------------------- | --------------- |
+| 重复订单 | 同一客户同一时间相同订单号 | 去重保留 1 条有效记录 | 不影响 F、M 计算 |
+| 大额异常订单 | 单笔金额 > $M_{99分位值} \times 异常阈值$ | 自动标记,可选择剔除或保留 | 仅影响 M 维度区间划分 |
+| 消费时间异常 | 消费时间晚于统计截止日 | 视为无效数据,剔除 | 不影响最终结果 |
+| 客户标识重复 | 同一客户多个唯一标识 | 按客户合并规则(如手机号、身份证号关联)聚合 | 需提前完成客户统一 ID 映射 |
+
+### 6.2 计算异常
+
+
+
+| 异常类型 | 触发条件 | 处理逻辑 | 输出结果 |
+| -------- | --------------------- | --------------------------------------- | --------------- |
+| 维度区间为空 | 某维度所有客户数据相同(如 F 均为 1) | 强制均分 5 个区间 | 分项得分按 1-5 分依次分配 |
+| 权重总和不为 1 | 配置权重时计算错误 | 系统自动归一化处理($W'_X = W_X / (W_R+W_F+W_M)$) | 不影响总分有效性 |
+| 统计周期过短 | 小于 30 天导致数据量不足 | 系统给出警告,允许强制执行 | 区间划分可能失真,建议延长周期 |
+
+## 7. 输出结果格式
+
+### 7.1 单客户评分结果
+
+
+
+| 字段名 | 数据类型 | 示例 |
+| --------- | ------------- | ----------------------- |
+| 客户 ID | String | CUST2023001 |
+| R 原始值(天) | Int | 15 |
+| R 得分 | Int | 5 |
+| F 原始值(次) | Int | 8 |
+| F 得分 | Int | 4 |
+| M 原始值(元) | Decimal(18,2) | 2560.00 |
+| M 得分 | Int | 5 |
+| RFM 总分 | Decimal(5,2) | 4.70 |
+| 标准化得分(可选) | Int | 94 |
+| 统计周期 | String | 2023-01-01 至 2023-06-30 |
+| 计算时间 | DateTime | 2023-07-01 00:30:25 |
+
+### 7.2 批量输出文件格式
+
+
+
+* 支持 CSV、Parquet、JSON 格式导出
+
+* 编码格式:UTF-8
+
+* 压缩方式:默认 GZIP(可配置关闭)
+
+## 8. 业务适配建议
+
+
+
+| 业务场景 | 统计周期建议 | 权重调整建议 | 特殊配置 |
+| --------------- | ------- | --------------------------- | ----------------- |
+| 快消零售 | 3-6 个月 | $W_R=0.5, W_F=0.3, W_M=0.2$ | 提高 R 维度权重,关注复购及时性 |
+| 高客单价行业(如奢侈品、家居) | 12 个月 | $W_R=0.3, W_F=0.2, W_M=0.5$ | 提高 M 维度权重,关注消费能力 |
+| 新品推广期 | 1-3 个月 | $W_R=0.6, W_F=0.2, W_M=0.2$ | 重点关注近期新客户 |
+| 会员体系运营 | 6-12 个月 | $W_R=0.4, W_F=0.4, W_M=0.2$ | 提高 F 维度权重,鼓励高频消费 |
+
+> (注:文档部分内容可能由 AI 生成)
\ No newline at end of file
diff --git a/application/.htaccess b/application/.htaccess
new file mode 100644
index 0000000..3418e55
--- /dev/null
+++ b/application/.htaccess
@@ -0,0 +1 @@
+deny from all
\ No newline at end of file
diff --git a/application/ai/config/route.php b/application/ai/config/route.php
new file mode 100644
index 0000000..704b7d0
--- /dev/null
+++ b/application/ai/config/route.php
@@ -0,0 +1,18 @@
+middleware(['jwt']);
\ No newline at end of file
diff --git a/application/ai/controller/CozeAI.php b/application/ai/controller/CozeAI.php
new file mode 100644
index 0000000..5208326
--- /dev/null
+++ b/application/ai/controller/CozeAI.php
@@ -0,0 +1,406 @@
+apiUrl = Env::get('cozeAi.api_url');
+ $this->accessToken = Env::get('cozeAi.token');
+
+ if (empty($this->accessToken) || empty($this->apiUrl)) {
+ return json_encode(['code' => 500, 'msg' => '参数缺失']);
+ }
+
+ // 设置请求头
+ $this->headers = [
+ 'Authorization: Bearer ' . $this->accessToken,
+ 'Content-Type: application/json'
+ ];
+ }
+
+
+ /**
+ * 创建智能体
+ * @param $data
+ * @return false|string|\think\response\Json
+ */
+ public function createBot($data = [])
+ {
+ $space_id = Env::get('cozeAi.space_id');
+ $name = !empty($data['name']) ? $data['name'] : '';
+ $model_id = !empty($data['model_id']) ? $data['model_id'] : '';
+ $prompt_info = !empty($data['prompt_info']) ? $data['prompt_info'] : '';
+ $plugin_id_list = [
+ 'id_list' => [
+ ['api_id' => '7362852017859035163', 'plugin_id' => '7362852017859018779'],
+ ['api_id' => '7472045461050851367', 'plugin_id' => '7472045461050834983'],
+ ]
+ ];
+ if (empty($name)) {
+ return json_encode(['code' => 500, 'msg' => '参数缺失']);
+ }
+
+ $model_info_config = [
+ 'model_id' => (string)$model_id,
+ ];
+
+ $params = [
+ 'space_id' => $space_id,
+ 'name' => $name,
+ 'model_info_config' => (object)$model_info_config,
+ 'plugin_id_list' => (object)$plugin_id_list
+ ];
+
+ if (!empty($prompt_info)){
+ $new_prompt_info = [
+ 'prompt' => $prompt_info
+ ];
+ $params['prompt_info'] = (object) $new_prompt_info;
+ }
+
+ $result = requestCurl($this->apiUrl . '/v1/bot/create', $params, 'POST', $this->headers, 'json');
+ $result = json_decode($result, true);
+ if ($result['code'] != 0) {
+ return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]);
+ }
+
+ return json_encode(['code' => 200, 'msg' => '创建成功', 'data' => $result['data']]);
+ }
+
+
+ /**
+ * 创建智能体
+ * @param $data
+ * @return false|string|\think\response\Json
+ */
+ public function updateBot($data = [])
+ {
+ $space_id = Env::get('cozeAi.space_id');
+ $bot_id = !empty($data['bot_id']) ? $data['bot_id'] : '';
+ $name = !empty($data['name']) ? $data['name'] : '';
+ $model_id = !empty($data['model_id']) ? $data['model_id'] : '';
+ $prompt_info = !empty($data['prompt_info']) ? $data['prompt_info'] : '';
+ $dataset_ids = !empty($data['dataset_ids']) ? $data['dataset_ids'] : '';
+ $plugin_id_list = [
+ 'id_list' => [
+ ['api_id' => '7362852017859035163', 'plugin_id' => '7362852017859018779'],
+ ['api_id' => '7472045461050851367', 'plugin_id' => '7472045461050834983'],
+ ]
+ ];
+ if (empty($name) || empty($bot_id)) {
+ return json_encode(['code' => 500, 'msg' => '参数缺失']);
+ }
+
+ $model_info_config = [
+ 'model_id' => (string)$model_id,
+ ];
+
+ $params = [
+ 'bot_id' => $bot_id,
+ 'space_id' => $space_id,
+ 'name' => $name,
+ 'model_info_config' => (object)$model_info_config,
+ 'plugin_id_list' => (object)$plugin_id_list
+ ];
+
+
+ if (!empty($prompt_info)){
+ $new_prompt_info = [
+ 'prompt' => $prompt_info
+ ];
+ $params['prompt_info'] = (object) $new_prompt_info;
+ }
+
+ if (!empty($dataset_ids)){
+ $knowledge = [
+ 'dataset_ids' => $dataset_ids
+ ];
+ $params['knowledge'] = (object) $knowledge;
+ }
+
+ $result = requestCurl($this->apiUrl . '/v1/bot/update', $params, 'POST', $this->headers, 'json');
+ $result = json_decode($result, true);
+ if ($result['code'] != 0) {
+ return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]);
+ }
+ return json_encode(['code' => 200, 'msg' => '更新成功', 'data' => []]);
+ }
+
+
+ /**
+ * 发布智能体
+ * @param $data
+ * @return false|string|\think\response\Json
+ */
+ public function botPublish($data = [])
+ {
+ $bot_id = !empty($data['bot_id']) ? $data['bot_id'] : '';
+ $connector_ids = ['1024', '999'];
+ if (empty($bot_id) || empty($connector_ids)) {
+ return json_encode(['code' => 500, 'msg' => '参数缺失']);
+ }
+
+ $params = [
+ 'bot_id' => $bot_id,
+ 'connector_ids' => $connector_ids,
+ ];
+ $result = requestCurl($this->apiUrl . '/v1/bot/publish', $params, 'POST', $this->headers, 'json');
+ $result = json_decode($result, true);
+ if ($result['code'] != 0) {
+ return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]);
+ }
+ return json_encode(['code' => 200, 'msg' => '发布成功', 'data' => []]);
+ }
+
+
+ /**
+ * 创建知识库
+ * @param $data
+ * @return false|string|\think\response\Json
+ */
+ public function createKnowledge($data = [])
+ {
+
+ $space_id = Env::get('cozeAi.space_id');
+ $name = !empty($data['name']) ? $data['name'] : '';
+ if (empty($space_id) || empty($name)) {
+ return json_encode(['code' => 500, 'msg' => '参数缺失']);
+ }
+
+ $params = [
+ 'space_id' => $space_id,
+ 'format_type' => 0,
+ 'name' => $name,
+ ];
+ $result = requestCurl($this->apiUrl . '/v1/datasets', $params, 'POST', $this->headers, 'json');
+ $result = json_decode($result, true);
+
+ if ($result['code'] != 0) {
+ return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]);
+ }
+ return json_encode(['code' => 200, 'msg' => '创建成功','data' => $result['data']]);
+ }
+
+
+ public function createDocument($data = [])
+ {
+ // 文件路径
+ $filePath = !empty($data['filePath']) ? $data['filePath'] : '';
+ $fileName = !empty($data['fileName']) ? $data['fileName'] : '';
+ if (empty($filePath)) {
+ return json_encode(['code' => 500, 'msg' => '参数缺失']);
+ }
+ // 读取文件内容
+ $fileContent = file_get_contents($filePath);
+ // 将文件内容编码为Base64
+ $base64EncodedContent = base64_encode($fileContent);
+
+
+ $dataset_id = !empty($data['dataset_id']) ? $data['dataset_id'] : '';
+
+ $document_bases = [
+ ['name' => $fileName,'source_info' => ['file_base64' => $base64EncodedContent]]
+ ];
+
+ $chunk_strategy = [
+ 'chunk_type' => 0,
+ 'remove_extra_spaces' => true
+ ];
+ $params = [
+ 'dataset_id' => (string) $dataset_id,
+ 'document_bases' => $document_bases,
+ 'chunk_strategy' => (object) $chunk_strategy,
+ 'format_type' => 0
+ ];
+ $headers = array_merge($this->headers, ['Agw-Js-Conv: str']);
+ $result = requestCurl($this->apiUrl . '/open_api/knowledge/document/create', $params, 'POST', $headers, 'json');
+ $result = json_decode($result, true);
+ if ($result['code'] != 0) {
+ return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]);
+ }
+ return json_encode(['code' => 200, 'msg' => '创建成功','data' => $result['document_infos']]);
+ }
+
+
+ /**
+ * 删除知识库文件
+ * @param $data
+ * @return false|string|\think\response\Json
+ */
+ public function deleteDocument($data = [])
+ {
+ if (empty($data)) {
+ return json_encode(['code' => 500, 'msg' => '参数缺失']);
+ }
+ $params = [
+ 'document_ids' => $data,
+ ];
+ $headers = array_merge($this->headers, ['Agw-Js-Conv: str']);
+ $result = requestCurl($this->apiUrl . '/open_api/knowledge/document/delete', $params, 'POST', $headers, 'json');
+ $result = json_decode($result, true);
+ if ($result['code'] != 0) {
+ return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]);
+ }
+ return json_encode(['code' => 200, 'msg' => '删除成功', 'data' => []]);
+
+ }
+
+
+ /**
+ * 创建会话
+ * @param $data
+ * @return false|string|\think\response\Json
+ */
+ public function createConversation($data = [])
+ {
+ $bot_id = !empty($data['bot_id']) ? $data['bot_id'] : '';
+ $name = !empty($data['name']) ? $data['name'] : '';
+ $meta_data = !empty($data['meta_data']) ? $data['meta_data'] : [];
+
+ if (empty($bot_id) || empty($name)) {
+ return json_encode(['code' => 500, 'msg' => '参数缺失']);
+ }
+ $params = [
+ 'bot_id' => $bot_id,
+ 'name' => $name,
+ ];
+ if (!empty($meta_data)){
+ $params['meta_data'] = $meta_data;
+ }
+ $result = requestCurl($this->apiUrl . '/v1/conversation/create', $params, 'POST', $this->headers, 'json');
+ $result = json_decode($result, true);
+ if ($result['code'] != 0) {
+ return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]);
+ }
+ return json_encode(['code' => 200, 'msg' => '创建成功','data' => $result['data']]);
+ }
+
+
+ /**
+ * 开始对话
+ * @param $data
+ * @return false|string|\think\response\Json
+ */
+ public function createChat($data = [])
+ {
+ try {
+ $bot_id = !empty($data['bot_id']) ? $data['bot_id'] : '';
+ $uid = !empty($data['uid']) ? $data['uid'] : '';
+ $conversation_id = !empty($data['conversation_id']) ? $data['conversation_id'] : '';
+ $question = !empty($data['question']) ? $data['question'] : [];
+
+
+ if(empty($bot_id)){
+ return json_encode(['code' => 500, 'msg' => '智能体ID不能为空', 'data' => []]);
+ }
+
+ if(empty($conversation_id)){
+ return json_encode(['code' => 500, 'msg' => '会话ID不能为空', 'data' => []]);
+ }
+
+ if(empty($question)){
+ return json_encode(['code' => 500, 'msg' => '问题不能为空', 'data' => []]);
+ }
+
+ // 构建请求数据
+ $params = [
+ 'bot_id' => strval($bot_id),
+ 'user_id' => strval($uid),
+ 'additional_messages' => $question,
+ 'stream' => false,
+ 'auto_save_history' => true
+ ];
+
+ $url = $this->apiUrl . '/v3/chat?conversation_id='.$conversation_id;
+ $result = requestCurl($url, $params, 'POST', $this->headers, 'json');
+ $result = json_decode($result, true);
+ if ($result['code'] != 0) {
+ return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]);
+ }
+ return json_encode(['code' => 200, 'msg' => '发送成功','data' => $result['data']]);
+
+ } catch (\Exception $e) {
+ return json_encode(['code' => 500, 'msg' => '创建对话失败:' . $e->getMessage(), 'data' => []]);
+ }
+ }
+
+
+ /**
+ * 查看对话详情
+ * @param $data
+ * @return false|string|\think\response\Json
+ */
+ public function getConversationChat($data = [])
+ {
+ $conversation_id = !empty($data['conversation_id']) ? $data['conversation_id'] : '';
+ $chat_id = !empty($data['chat_id']) ? $data['chat_id'] : '';
+ $url = $this->apiUrl . '/v3/chat/retrieve?conversation_id='.$conversation_id.'&chat_id='.$chat_id;
+ $result = requestCurl($url, [], 'GET', $this->headers, 'json');
+ $result = json_decode($result, true);
+ if ($result['code'] != 0) {
+ return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]);
+ }
+ return json_encode(['code' => 200, 'msg' => '获取成功','data' => $result['data']]);
+ }
+
+
+ /**
+ * 查看对话消息详情
+ * @param $data
+ * @return false|string|\think\response\Json
+ */
+ public function listConversationMessage($data = [])
+ {
+ $conversation_id = !empty($data['conversation_id']) ? $data['conversation_id'] : '';
+ $chat_id = !empty($data['chat_id']) ? $data['chat_id'] : '';
+ $url = $this->apiUrl . '/v3/chat/message/list?conversation_id='.$conversation_id.'&chat_id='.$chat_id;
+ $result = requestCurl($url, [], 'GET', $this->headers, 'json');
+ $result = json_decode($result, true);
+ if ($result['code'] != 0) {
+ return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]);
+ }
+ return json_encode(['code' => 200, 'msg' => '获取成功','data' => $result['data']]);
+ }
+
+
+ /**
+ * 取消进行中的对话
+ * @param $data
+ * @return false|string|\think\response\Json
+ */
+ public function cancelConversationChat($data = [])
+ {
+ $conversation_id = !empty($data['conversation_id']) ? $data['conversation_id'] : '';
+ $chat_id = !empty($data['chat_id']) ? $data['chat_id'] : '';
+
+ // 构建请求数据
+ $params = [
+ 'conversation_id' => (string) $conversation_id,
+ 'chat_id' => (string) $chat_id
+ ];
+
+ $url = $this->apiUrl . '/v3/chat/cancel';
+ $result = requestCurl($url, $params, 'POST', $this->headers, 'json');
+ $result = json_decode($result, true);
+ if ($result['code'] != 0) {
+ return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]);
+ }
+ return json_encode(['code' => 200, 'msg' => '取消成功', 'data' => []]);
+ }
+
+}
\ No newline at end of file
diff --git a/application/ai/controller/DouBaoAI.php b/application/ai/controller/DouBaoAI.php
new file mode 100644
index 0000000..a3c942c
--- /dev/null
+++ b/application/ai/controller/DouBaoAI.php
@@ -0,0 +1,68 @@
+apiUrl = Env::get('doubaoAi.api_url');
+ $this->apiKey = Env::get('doubaoAi.api_key');
+
+ // 设置请求头
+ $this->headers = [
+ 'Content-Type: application/json',
+ 'Authorization: Bearer ' . $this->apiKey
+ ];
+
+ if (empty($this->apiKey) || empty($this->apiUrl)) {
+ return json_encode(['code' => 500, 'msg' => '参数缺失']);
+ }
+ }
+
+
+
+ public function text($params = [])
+ {
+
+ if (empty($params)){
+ $content = $this->request->param('content', '');
+ $model = $this->request->param('model', 'doubao-seed-1-8-251215');
+ if(empty($content)){
+ return json_encode(['code' => 500, 'msg' => '提示词缺失']);
+ }
+ $params = [
+ 'model' => $model,
+ 'messages' => [
+ ['role' => 'system', 'content' => '你现在是存客宝的AI助理,你精通中国大陆的法律'],
+ ['role' => 'user', 'content' => $content],
+ ],
+ ];
+ }
+ $result = requestCurl($this->apiUrl, $params, 'POST', $this->headers, 'json');
+ $result = json_decode($result, true);
+ if(isset($result['error'])){
+ $error = $result['error'];
+ return json_encode(['code' => 500, 'msg' => $error['message']]);
+ }else{
+ $content = $result['choices'][0]['message']['content'];
+ $token = intval($result['usage']['total_tokens']) * 20;
+
+ exit_data($content);
+ return json_encode(['code' => 200, 'msg' => '成功','data' => ['token' => $token,'content' => $content]]);
+ }
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/application/ai/controller/OpenAi.php b/application/ai/controller/OpenAi.php
new file mode 100644
index 0000000..ac0ff24
--- /dev/null
+++ b/application/ai/controller/OpenAi.php
@@ -0,0 +1,145 @@
+apiUrl = Env::get('openAi.apiUrl');
+ $this->apiKey = Env::get('openAi.apiKey');
+
+ // 设置请求头
+ $this->headers = [
+ 'Content-Type: application/json',
+ 'Authorization: Bearer '.$this->apiKey
+ ];
+ }
+
+
+ public function text()
+ {
+
+ $params = [
+ 'model' => 'gpt-3.5-turbo-0125',
+ 'input' => 'DHA 从孕期到出生到老年都需要,助力大脑发育🧠/减缓脑压力有助记忆/给大脑动力#贝蒂喜藻油DHA 双标认证每粒 150毫克,高含量、高性价比从小吃到老,长期吃更健康 重写这条朋友圈 要求: 1、原本的字数和意思不要修改超过10% 2、出现品牌名或个人名字就去除'
+ ];
+ $result = $this->httpRequest( $this->apiUrl, 'POST', $params,$this->headers);
+ exit_data($result);
+ }
+
+ /**
+ * 示例:调用OpenAI API生成睡前故事
+ * 对应curl命令:
+ * curl "https://api.ai.com/v1/responses" \
+ * -H "Content-Type: application/json" \
+ * -H "Authorization: Bearer $OPENAI_API_KEY" \
+ * -d '{
+ * "model": "gpt-5",
+ * "input": "Write a one-sentence bedtime story about a unicorn."
+ * }'
+ */
+ public function bedtimeStory()
+ {
+
+ // API请求参数
+ $params = [
+ 'model' => 'gpt-5',
+ 'input' => 'Write a one-sentence bedtime story about a unicorn.'
+ ];
+
+ // 发送请求到OpenAI API
+ $url = 'https://api.openai.com/v1/responses';
+ $result = $this->httpRequest($url, 'POST', $params, $this->headers);
+
+ // 返回结果
+ exit_data($result);
+ }
+
+
+
+
+ /**
+ * CURL请求 - 专门用于JSON API请求
+ *
+ * @param $url 请求url地址
+ * @param $method 请求方法 get post
+ * @param null $postfields post数据数组
+ * @param array $headers 请求header信息
+ * @param int $timeout 超时时间
+ * @param bool|false $debug 调试开启 默认false
+ * @return mixed
+ */
+ protected function httpRequest($url, $method = "GET", $postfields = null, $headers = array(), $timeout = 30, $debug = false)
+ {
+ $method = strtoupper($method);
+ $ci = curl_init();
+
+ /* Curl settings */
+ curl_setopt($ci, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0");
+ curl_setopt($ci, CURLOPT_CONNECTTIMEOUT, 60); /* 在发起连接前等待的时间,如果设置为0,则无限等待 */
+ curl_setopt($ci, CURLOPT_TIMEOUT, $timeout); /* 设置cURL允许执行的最长秒数 */
+ curl_setopt($ci, CURLOPT_RETURNTRANSFER, true);
+
+ switch ($method) {
+ case "POST":
+ curl_setopt($ci, CURLOPT_POST, true);
+ if (!empty($postfields)) {
+ // 对于JSON API,直接将数组转换为JSON字符串
+ if (is_array($postfields)) {
+ $tmpdatastr = json_encode($postfields);
+ } else {
+ $tmpdatastr = $postfields;
+ }
+ curl_setopt($ci, CURLOPT_POSTFIELDS, $tmpdatastr);
+ }
+ break;
+ default:
+ curl_setopt($ci, CURLOPT_CUSTOMREQUEST, $method); /* //设置请求方式 */
+ break;
+ }
+
+ $ssl = preg_match('/^https:\/\//i', $url) ? TRUE : FALSE;
+ curl_setopt($ci, CURLOPT_URL, $url);
+ if ($ssl) {
+ curl_setopt($ci, CURLOPT_SSL_VERIFYPEER, FALSE); // https请求 不验证证书和hosts
+ curl_setopt($ci, CURLOPT_SSL_VERIFYHOST, FALSE); // 不从证书中检查SSL加密算法是否存在
+ }
+
+ if (ini_get('open_basedir') == '' && ini_get('safe_mode' == 'Off')) {
+ curl_setopt($ci, CURLOPT_FOLLOWLOCATION, 1);
+ }
+ curl_setopt($ci, CURLOPT_MAXREDIRS, 2);/*指定最多的HTTP重定向的数量,这个选项是和CURLOPT_FOLLOWLOCATION一起使用的*/
+ curl_setopt($ci, CURLOPT_HTTPHEADER, $headers);
+ curl_setopt($ci, CURLINFO_HEADER_OUT, true);
+
+ $response = curl_exec($ci);
+ $requestinfo = curl_getinfo($ci);
+ $http_code = curl_getinfo($ci, CURLINFO_HTTP_CODE);
+
+ if ($debug) {
+ echo "=====post data======\r\n";
+ var_dump($postfields);
+ echo "=====info===== \r\n";
+ print_r($requestinfo);
+ echo "=====response=====\r\n";
+ print_r($response);
+ }
+
+ curl_close($ci);
+ return $response;
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/application/api/config/route.php b/application/api/config/route.php
new file mode 100644
index 0000000..8bee16d
--- /dev/null
+++ b/application/api/config/route.php
@@ -0,0 +1,109 @@
+request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ if ($isInner) {
+ return json_encode(['code' => 500, 'msg' => '缺少授权信息']);
+ } else {
+ return errorJson('缺少授权信息');
+ }
+ }
+
+ try {
+ // 构建请求参数
+ $params = [
+ 'showNormalAccount' => $showNormalAccount,
+ 'keyword' => $keyword,
+ 'departmentId' => $departmentId,
+ 'pageIndex' => $pageIndex,
+ 'pageSize' => $pageSize
+ ];
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'plain');
+
+ // 发送请求获取公司账号列表
+ $result = requestCurl($this->baseUrl . 'api/Account/myTenantPageAccounts', $params, 'GET', $header);
+ $response = handleApiResponse($result);
+
+ // 保存数据到数据库
+ if (!empty($response['results'])) {
+ foreach ($response['results'] as $item) {
+ $this->saveAccount($item);
+ }
+ }
+
+ if ($isInner) {
+ return json_encode(['code' => 200, 'msg' => '获取公司账号列表成功', 'data' => $response]);
+ } else {
+
+ return successJson($response);
+ }
+ } catch (\Exception $e) {
+ if ($isInner) {
+ return json_encode(['code' => 500, 'msg' => '获取公司账号列表失败:' . $e->getMessage()]);
+ } else {
+ return errorJson('获取公司账号列表失败:' . $e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * 创建新账号
+ * @return \think\response\Json
+ */
+ public function createAccount()
+ {
+ // 获取授权token
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ return errorJson('缺少授权信息');
+ }
+
+ try {
+ // 获取并验证请求参数
+ $userName = $this->request->param('userName', '');
+ $password = $this->request->param('password', '');
+ $realName = $this->request->param('realName', '');
+ $nickname = $this->request->param('nickname', '');
+ $memo = $this->request->param('memo', '');
+ $departmentId = $this->request->param('departmentId', 0);
+
+
+ // 参数验证
+ if (empty($userName)) {
+ return errorJson('用户名不能为空');
+ }
+ // if (!preg_match('/^[a-zA-Z][a-zA-Z0-9]{5,9}$/', $userName)) {
+ // return errorJson('用户名必须以字母开头,只能包含字母和数字,长度6-10位');
+ // }
+ if (empty($password)) {
+ return errorJson('密码不能为空');
+ }
+
+ if (empty($realName)) {
+ return errorJson('真实姓名不能为空');
+ }
+ if (empty($departmentId)) {
+ return errorJson('公司ID不能为空');
+ }
+
+ // 检查账号是否已存在
+ $existingAccount = CompanyAccountModel::where('userName', $userName)->find();
+ if (!empty($existingAccount)) {
+ return errorJson('账号已存在');
+ }
+
+
+ // 构建请求参数
+ $params = [
+ 'userName' => $userName,
+ 'password' => $password,
+ 'realName' => $realName,
+ 'nickname' => $nickname,
+ 'memo' => $memo,
+ 'departmentId' => $departmentId,
+ 'departmentIdArr' => empty($departmentId) ? [914] : [914, $departmentId]
+ ];
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求创建账号
+ $result = requestCurl($this->baseUrl . 'api/account/newAccount', $params, 'POST', $header, 'json');
+
+ if (is_numeric($result)) {
+ $res = CompanyAccountModel::create([
+ 'id' => $result,
+ 'tenantId' => 242,
+ 'userName' => $userName,
+ 'realName' => $realName,
+ 'nickname' => $nickname,
+ 'passwordMd5' => md5($password),
+ 'passwordLocal' => localEncrypt($password),
+ 'memo' => $memo,
+ 'accountType' => 11,
+ 'departmentId' => $departmentId,
+ 'createTime' => time(),
+ 'privilegeIds' => json_encode([])
+ ]);
+ $this->setPrivileges(['id' => $result]);
+ return successJson($res);
+ } else {
+ return errorJson($result);
+ }
+ } catch (\Exception $e) {
+ return errorJson('创建账号失败:' . $e->getMessage());
+ }
+ }
+
+
+ /**
+ * 创建新账号(包含创建部门)
+ * @return \think\response\Json
+ */
+ public function createNewAccount()
+ {
+ // 获取授权token
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ return errorJson('缺少授权信息');
+ }
+
+ DB::startTrans();
+ try {
+ // 获取参数
+ $departmentName = $this->request->param('departmentName', '');
+ $departmentMemo = $this->request->param('departmentMemo', '');
+ $accountName = $this->request->param('accountName', '');
+ $accountPassword = $this->request->param('accountPassword', '');
+ $accountRealName = $this->request->param('accountRealName', '');
+ $accountNickname = $this->request->param('accountNickname', '');
+ $accountMemo = $this->request->param('accountMemo', '');
+
+ // 验证参数
+ if (empty($departmentName)) {
+ return errorJson('部门名称不能为空');
+ }
+ if (empty($accountName)) {
+ return errorJson('账号名称不能为空');
+ }
+ if (empty($accountPassword)) {
+ return errorJson('账号密码不能为空');
+ }
+
+ // 检查部门是否已存在
+ $existingDepartment = CompanyModel::where('name', $departmentName)->find();
+ if (!empty($existingDepartment)) {
+ return errorJson('部门以存在');
+ }
+
+ // 检查账号是否已存在
+ $existingAccount = CompanyAccountModel::where('userName', $accountName)->find();
+ if (!empty($existingAccount)) {
+ return errorJson('账号已存在');
+ }
+
+
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 1. 创建部门
+ $departmentParams = [
+ 'name' => $departmentName,
+ 'memo' => $departmentMemo,
+ 'departmentIdArr' => [914],
+ 'parentId' => 914
+ ];
+
+ $departmentResult = requestCurl($this->baseUrl . 'api/Department/createDepartment', $departmentParams, 'POST', $header, 'json');
+ if (is_numeric($departmentResult)) {
+ // 保存部门到数据库
+ CompanyModel::create([
+ 'id' => $departmentResult,
+ 'name' => $departmentName,
+ 'memo' => $departmentMemo,
+ 'tenantId' => 242,
+ 'isTop' => 0,
+ 'level' => 1,
+ 'parentId' => 914,
+ 'privileges' => '',
+ 'createTime' => time(),
+ 'lastUpdateTime' => 0
+ ]);
+
+ $this->setPrivileges(['id' => $departmentResult]);
+
+ } else {
+ DB::rollback();
+ return errorJson('创建部门失败:' . $departmentResult);
+ }
+
+ // 2. 创建账号
+ $accountParams = [
+ 'userName' => $accountName,
+ 'password' => $accountPassword,
+ 'realName' => $accountRealName,
+ 'nickname' => $accountNickname,
+ 'memo' => $accountMemo,
+ 'departmentId' => $departmentResult,
+ 'departmentIdArr' => [914, $departmentResult]
+ ];
+
+ $accountResult = requestCurl($this->baseUrl . 'api/Account/newAccount', $accountParams, 'POST', $header, 'json');
+
+ if (is_numeric($accountResult)) {
+ $res = CompanyAccountModel::create([
+ 'id' => $accountResult,
+ 'tenantId' => 242,
+ 'userName' => $accountName,
+ 'realName' => $accountRealName,
+ 'nickname' => $accountNickname,
+ 'passwordMd5' => md5($accountPassword),
+ 'passwordLocal' => localEncrypt($accountPassword),
+ 'memo' => $accountMemo,
+ 'accountType' => 11,
+ 'departmentId' => $departmentResult,
+ 'createTime' => time(),
+ 'privilegeIds' => json_encode([])
+ ]);
+ DB::commit();
+ return successJson($res, '账号创建成功');
+ } else {
+ // 如果创建账号失败,删除已创建的部门
+ $this->deleteDepartment($accountResult);
+ DB::rollback();
+ return errorJson('创建账号失败:' . $accountResult);
+ }
+
+ } catch (\Exception $e) {
+ DB::rollback();
+ return errorJson('创建账号失败:' . $e->getMessage());
+ }
+ }
+
+
+
+ /************************ 部门管理相关接口 ************************/
+
+ /**
+ * 获取部门列表
+ * @return \think\response\Json
+ */
+ public function getDepartmentList($isInner = false)
+ {
+ // 获取授权token
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ if ($isInner) {
+ return json_encode(['code' => 500, 'msg' => '缺少授权信息']);
+ } else {
+ return errorJson('缺少授权信息');
+ }
+ }
+
+ try {
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求获取部门列表
+ $url = $this->baseUrl . 'api/Department/fetchMyAndSubordinateDepartment';
+ $result = requestCurl($url, [], 'GET', $header, 'json');
+
+ // 处理返回结果
+ $response = handleApiResponse($result);
+
+ // 保存数据到数据库
+ if (!empty($response)) {
+ CompanyModel::where('1=1')->delete();
+ $this->processDepartments($response);
+ }
+
+ if ($isInner) {
+ return json_encode(['code' => 200, 'msg' => '获取部门列表成功', 'data' => $response]);
+ } else {
+ return successJson($response, '获取部门列表成功');
+ }
+ } catch (\Exception $e) {
+ if ($isInner) {
+ return json_encode(['code' => 500, 'msg' => '获取部门列表失败:' . $e->getMessage()]);
+ } else {
+ return errorJson('获取部门列表失败:' . $e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * 创建部门
+ * @return \think\response\Json
+ */
+ public function createDepartment()
+ {
+ // 获取授权token
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ return errorJson('缺少授权信息');
+ }
+
+ try {
+ // 获取并验证请求参数
+ $name = $this->request->param('name', '');
+ $memo = $this->request->param('memo', '');
+ if (empty($name)) {
+ return errorJson('请输入公司名称');
+ }
+
+ // 检查部门名称是否已存在
+ $departmentId = CompanyModel::where('name', $name)->find();
+ if (!empty($departmentId)) {
+ return errorJson('部门已存在');
+ }
+
+ // 构建请求参数
+ $params = [
+ 'name' => $name,
+ 'memo' => $memo,
+ 'departmentIdArr' => [914],
+ 'parentId' => 914
+ ];
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求创建部门
+ $result = requestCurl($this->baseUrl . 'api/Department/createDepartment', $params, 'POST', $header, 'json');
+
+ // 处理返回结果
+ if (is_numeric($result)) {
+ $res = CompanyModel::create([
+ 'id' => $result,
+ 'name' => $name,
+ 'memo' => $memo,
+ 'tenantId' => 242,
+ 'isTop' => 0,
+ 'level' => 1,
+ 'parentId' => 914,
+ 'privileges' => '',
+ 'createTime' => time(),
+ 'lastUpdateTime' => 0
+ ]);
+ return successJson($res);
+ } else {
+ return errorJson($result);
+ }
+ } catch (\Exception $e) {
+ return errorJson('创建部门失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 修改部门信息
+ * @return \think\response\Json
+ */
+ public function updateDepartment()
+ {
+ // 获取授权token
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ return errorJson('缺少授权信息');
+ }
+
+ try {
+ // 获取并验证请求参数
+ $id = $this->request->param('id', 0);
+ $name = $this->request->param('name', '');
+ $memo = $this->request->param('memo', '');
+
+ if (empty($id)) {
+ return errorJson('部门ID不能为空');
+ }
+ if (empty($name)) {
+ return errorJson('部门名称不能为空');
+ }
+
+ // 验证部门是否存在
+ $department = CompanyModel::where('id', $id)->find();
+ if (empty($department)) {
+ return errorJson('部门不存在');
+ }
+
+ // 构建请求参数
+ $departmentIdArr = $department->parentId == 914 ? [914] : [914, $department->parentId];
+ $params = [
+ 'id' => $id,
+ 'name' => $name,
+ 'memo' => $memo,
+ 'departmentIdArr' => $departmentIdArr,
+ 'tenantId' => 242,
+ 'createTime' => $department->createTime,
+ 'isTop' => $department->isTop,
+ 'level' => $department->level,
+ 'parentId' => $department->parentId,
+ 'lastUpdateTime' => $department->lastUpdateTime,
+ 'privileges' => $department->privileges
+ ];
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求修改部门
+ $result = requestCurl($this->baseUrl . 'api/Department/department', $params, 'PUT', $header, 'json');
+ $response = handleApiResponse($result);
+
+ // 更新本地数据库
+ $department->name = $name;
+ $department->memo = $memo;
+ $department->save();
+
+ return successJson([], '部门修改成功');
+ } catch (\Exception $e) {
+ return errorJson('修改部门失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 删除部门
+ * @return \think\response\Json
+ */
+ public function deleteDepartment($id = '')
+ {
+ // 获取授权token
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ return errorJson('缺少授权信息');
+ }
+
+ try {
+ // 获取并验证部门ID
+ $id = !empty($id) ? $id : $this->request->param('id', '');
+ if (empty($id)) {
+ return errorJson('部门ID不能为空');
+ }
+
+ // 验证部门是否存在
+ $department = CompanyModel::where('id', $id)->find();
+ if (empty($department)) {
+ return errorJson('部门不存在');
+ }
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送删除请求
+ $result = requestCurl($this->baseUrl . 'api/Department/del/' . $id, [], 'DELETE', $header);
+
+ if ($result) {
+ return errorJson($result);
+ } else {
+ // 删除本地数据库记录
+ $department->delete();
+ return successJson([], '部门删除成功');
+ }
+
+
+ } catch (\Exception $e) {
+ return errorJson('删除部门失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 修改部门权限
+ * @return \think\response\Json
+ */
+ public function setPrivileges($data = [])
+ {
+ // 获取授权token
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ return errorJson('缺少授权信息');
+ }
+
+ try {
+ // 获取并验证请求参数
+ $id = !empty($data['id']) ? $data['id'] : $this->request->param('id', 0);
+ if (empty($id)) {
+ return errorJson('部门ID不能为空');
+ }
+
+ $privilegeIds = !empty($data['privilegeIds']) ? $data['privilegeIds'] : '1001,1002,1004,1023,1406,20003,20021,20022,20023,20032,20041,20049,20054,20055,20060,20100,20102,20107';
+ $privilegeIds = explode(',',$privilegeIds);
+
+ // 验证部门是否存在
+ $department = CompanyModel::where('id', $id)->find();
+ if (empty($department)) {
+ return errorJson('部门不存在');
+ }
+
+
+
+ // 构建请求参数
+ $params = [
+ 'departmentId' => $id,
+ 'privilegeIds' => $privilegeIds,
+ 'syncPrivilege' => true
+ ];
+
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求修改部门
+ $result = requestCurl($this->baseUrl . 'api/Department/privileges', $params, 'PUT', $header, 'json');
+ $response = handleApiResponse($result);
+
+
+ return successJson([], '部门权限修改成功');
+ } catch (\Exception $e) {
+ return errorJson('修改部门权限失败:' . $e->getMessage());
+ }
+ }
+
+
+
+ public function accountModify($data = [])
+ {
+ // 获取授权token
+ $authorization = $this->authorization;
+ if (empty($authorization)) {
+ return errorJson('缺少授权信息');
+ }
+
+
+ $id = !empty($data['id']) ? $data['id'] : '';
+ if (empty($id)) {
+ return errorJson('账号ID不能为空');
+ }
+
+ $account = CompanyAccountModel::where('id', $id)->find();
+
+
+
+ if (empty($account)) {
+ return errorJson('账号不存在');
+ }
+ $privilegeIds = json_decode($account->privilegeIds,true);
+ $privilegeIds = !empty($privilegeIds) ? $privilegeIds : [1001,1002,1004,1023,1406,20003,20021,20022,20023,20032,20041,20049,20054,20055,20060,20100,20102,20107,20055];
+
+ // 构建请求参数
+ $params = [
+ 'accountType' => !empty($data['accountType']) ? $data['accountType'] : $account->accountType,
+ 'alive' => !empty($data['alive']) ? $data['alive'] : $account->alive,
+ 'avatar' => !empty($data['avatar']) ? $data['avatar'] : $account->avatar,
+ 'createTime' => !empty($data['createTime']) ? $data['createTime'] : $account->createTime,
+ 'creator' => !empty($data['creator']) ? $data['creator'] : $account->creator,
+ 'creatorRealName' => !empty($data['creatorRealName']) ? $data['creatorRealName'] : $account->creatorRealName,
+ 'creatorUserName' => !empty($data['creatorUserName']) ? $data['creatorUserName'] : $account->creatorUserName,
+ 'departmentId' => !empty($data['departmentId']) ? $data['departmentId'] : $account->departmentId,
+ 'departmentIdArr' => !empty($data['departmentIdArr']) ? $data['departmentIdArr'] : [914,$account->departmentId],
+ 'departmentName' => !empty($data['departmentName']) ? $data['departmentName'] : $account->departmentName,
+ 'hasXiakeAccount' => !empty($data['hasXiakeAccount']) ? $data['hasXiakeAccount'] : false,
+ 'id' => !empty($data['id']) ? $data['id'] : $account->id,
+ 'memo' => !empty($data['memo']) ? $data['memo'] : $account->memo,
+ 'nickname' => !empty($data['nickname']) ? $data['nickname'] : $account->nickname,
+ 'privilegeIds' => !empty($data['privilegeIds']) ? $data['privilegeIds'] : $privilegeIds,
+ 'realName' => !empty($data['realName']) ? $data['realName'] : $account->realName,
+ 'status' => !empty($data['status']) ? $data['status'] : $account->status,
+ 'tenantId' => !empty($data['tenantId']) ? $data['tenantId'] : $account->tenantId,
+ 'userName' => !empty($data['userName']) ? $data['userName'] : $account->userName,
+ ];
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求修改部门
+ $result = requestCurl($this->baseUrl . 'api/account/modify', $params, 'PUT', $header, 'json');
+ $response = handleApiResponse($result);
+
+
+ if(empty($response)){
+ $newData = [
+ 'nickname' => $params['nickname'],
+ 'avatar' => $params['avatar'],
+ ];
+ CompanyAccountModel::where('id', $id)->update($newData);
+ return json_encode(['code' => 200, 'msg' => '账号修改成功']);
+ }else{
+ return json_encode(['code' => 500, 'msg' => $response]);
+ }
+ }
+
+
+
+
+
+
+ /************************ 私有辅助方法 ************************/
+
+ /**
+ * 递归处理部门列表
+ * @param array $departments 部门数据
+ */
+ private function processDepartments($departments)
+ {
+ if (empty($departments) || !is_array($departments)) {
+ return;
+ }
+
+ foreach ($departments as $item) {
+ // 保存当前部门
+ $this->saveDepartment($item);
+
+ // 递归处理子部门
+ if (!empty($item['children']) && is_array($item['children'])) {
+ $this->processDepartments($item['children']);
+ }
+ }
+ }
+
+ /**
+ * 保存部门数据到数据库
+ * @param array $item 部门数据
+ */
+ private function saveDepartment($item)
+ {
+ $data = [
+ 'id' => isset($item['id']) ? $item['id'] : 0,
+ 'name' => isset($item['name']) ? $item['name'] : '',
+ 'memo' => isset($item['memo']) ? $item['memo'] : '',
+ 'level' => isset($item['level']) ? $item['level'] : 0,
+ 'isTop' => isset($item['isTop']) ? $item['isTop'] : false,
+ 'parentId' => isset($item['parentId']) ? $item['parentId'] : 0,
+ 'tenantId' => isset($item['tenantId']) ? $item['tenantId'] : 0,
+ 'privileges' => isset($item['privileges']) ? (is_array($item['privileges']) ? json_encode($item['privileges']) : $item['privileges']) : '',
+ 'createTime' => isset($item['createTime']) ? strtotime($item['createTime']) : 0,
+ 'lastUpdateTime' => isset($item['lastUpdateTime']) ? ($item['lastUpdateTime'] == '0001-01-01T00:00:00' ? 0 : strtotime($item['lastUpdateTime'])) : 0
+ ];
+
+ // 使用id作为唯一性判断
+ $department = CompanyModel::where('id', $item['id'])->find();
+ if ($department) {
+ $department->save($data);
+ } else {
+ CompanyModel::create($data);
+ }
+ }
+
+ /**
+ * 保存账号数据到数据库
+ * @param array $item 账号数据
+ */
+ private function saveAccount($item)
+ {
+ // 将日期时间字符串转换为时间戳
+ $createTime = isset($item['createTime']) ? strtotime($item['createTime']) : null;
+ $deleteTime = isset($item['deleteTime']) ? strtotime($item['deleteTime']) : null;
+
+ $data = [
+ 'id' => $item['id'],
+ 'accountType' => isset($item['accountType']) ? $item['accountType'] : 0,
+ 'status' => isset($item['status']) ? $item['status'] : 0,
+ 'tenantId' => isset($item['tenantId']) ? $item['tenantId'] : 0,
+ 'userName' => isset($item['userName']) ? $item['userName'] : '',
+ 'realName' => isset($item['realName']) ? $item['realName'] : '',
+ 'nickname' => isset($item['nickname']) ? $item['nickname'] : '',
+ 'avatar' => isset($item['avatar']) ? $item['avatar'] : '',
+ 'phone' => isset($item['phone']) ? $item['phone'] : '',
+ 'memo' => isset($item['memo']) ? $item['memo'] : '',
+ 'createTime' => $createTime,
+ 'creator' => isset($item['creator']) ? $item['creator'] : 0,
+ 'creatorUserName' => isset($item['creatorUserName']) ? $item['creatorUserName'] : '',
+ 'creatorRealName' => isset($item['creatorRealName']) ? $item['creatorRealName'] : '',
+ 'departmentId' => isset($item['departmentId']) ? $item['departmentId'] : 0,
+ 'departmentName' => isset($item['departmentName']) ? $item['departmentName'] : '',
+ 'privilegeIds' => isset($item['privilegeIds']) ? json_encode($item['privilegeIds']) : json_encode([]),
+ 'alive' => isset($item['alive']) ? $item['alive'] : false,
+ 'hasXiakeAccount' => isset($item['hasXiakeAccount']) ? $item['hasXiakeAccount'] : false,
+ 'isDeleted' => isset($item['isDeleted']) ? $item['isDeleted'] : false,
+ 'deleteTime' => $deleteTime
+ ];
+
+ // 使用tenantId作为唯一性判断
+ $account = CompanyAccountModel::where('id', $item['id'])->find();
+ if ($account) {
+ $account->save($data);
+ } else {
+ CompanyAccountModel::create($data);
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/api/controller/AllotRuleController.php b/application/api/controller/AllotRuleController.php
new file mode 100644
index 0000000..db590d6
--- /dev/null
+++ b/application/api/controller/AllotRuleController.php
@@ -0,0 +1,362 @@
+request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ if ($isInner) {
+ return json_encode(['code' => 500, 'msg' => '缺少授权信息']);
+ } else {
+ return errorJson('缺少授权信息');
+ }
+ }
+
+ try {
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'plain');
+
+ // 发送请求获取所有分配规则
+ $result = requestCurl($this->baseUrl . 'api/AllotRule/all', [], 'GET', $header);
+ $response = handleApiResponse($result);
+
+ // 保存数据到数据库
+ if (!empty($response)) {
+ AllotRuleModel::where('1=1')->update(['isDel' => 1]);
+ foreach ($response as $item) {
+ $this->saveAllotRule($item);
+ }
+ }
+
+ if ($isInner) {
+ return json_encode(['code' => 200, 'msg' => 'success', 'data' => $response]);
+ } else {
+ return successJson($response);
+ }
+ } catch (\Exception $e) {
+ if ($isInner) {
+ return json_encode(['code' => 500, 'msg' => '获取分配规则失败:' . $e->getMessage()]);
+ } else {
+ return errorJson('获取分配规则失败:' . $e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * 手动触发分配规则同步任务
+ * @return \think\response\Json
+ */
+ public function startJob()
+ {
+ try {
+ $data = [
+ 'time' => time()
+ ];
+
+ // 添加到队列,设置任务名为 allotrule_list
+ $isSuccess = Queue::push(AllotRuleListJob::class, $data, 'allotrule_list');
+
+ if ($isSuccess !== false) {
+ return successJson([], '分配规则同步任务已添加到队列');
+ } else {
+ return errorJson('添加分配规则同步任务到队列失败');
+ }
+ } catch (\Exception $e) {
+ return errorJson('触发分配规则同步任务失败:' . $e->getMessage());
+ }
+ }
+
+ /************************************
+ * 分配规则CRUD操作方法
+ ************************************/
+
+ /**
+ * 创建分配规则
+ * @param array $data 请求数据
+ * @param bool $isInner 是否为内部调用
+ * @return \think\response\Json
+ */
+ public function createRule($data = [], $isInner = false)
+ {
+ // 获取授权token
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ if ($isInner) {
+ return json_encode(['code' => 500, 'msg' => '缺少授权信息']);
+ } else {
+ return errorJson('缺少授权信息');
+ }
+ }
+
+ try {
+ $wechatData = input('wechatData', '[]') ?: [];
+ $priorityStrategy = input('priorityStrategy', '[]') ?: [];
+
+ // 构建请求数据
+ $params = [
+ 'allotType' => input('allotType', 1),
+ 'kefuRange' => input('kefuRange', 5),
+ 'wechatRange' => input('wechatRange',3),
+ 'kefuData' => input('kefuData', '[]') ?: [],
+ 'wechatData' => $wechatData,
+ 'labels' => input('labels', '[]') ?: [],
+ 'priorityStrategy' => json_encode($priorityStrategy,256)
+ ];
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求到微信接口
+ $result = requestCurl($this->baseUrl . 'api/AllotRule/new', $params, 'POST', $header, 'json');
+ if (empty($result)) {
+ // 异步更新所有规则列表
+ AllotRuleModel::where('1=1')->update(['isDel' => 1]);
+ $this->getAllRules();
+ $res = AllotRuleModel::where('isDel',0)->order('id','DESC')->find();
+ $res->departmentId = !empty($data['departmentId']) ? $data['departmentId'] : 0;
+ $res->save();
+ return successJson($res, '创建分配规则成功');
+ } else {
+ return errorJson($result);
+ }
+ } catch (\Exception $e) {
+ return errorJson('创建分配规则失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 更新分配规则
+ * @param array $data 请求数据
+ * @param bool $isInner 是否为内部调用
+ * @return \think\response\Json
+ */
+ public function updateRule($data = [], $isInner = false)
+ {
+ // 获取授权token
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ return errorJson('缺少授权信息');
+ }
+
+ try {
+ // 获取请求参数
+ $id = !empty($data['id']) ? $data['id'] : input('id',0);
+ if (empty($id)) {
+ return errorJson('规则ID不能为空');
+ }
+
+ $rule = AllotRuleModel::where('id', $id)->find();
+ if (empty($rule)) {
+ return errorJson('规则不存在');
+ }
+
+ // 构建请求数据
+ $params = [
+ 'id' => $id,
+ 'tenantId' => $rule['tenantId'],
+ 'allotType' => !empty($data['allotType']) ? $data['allotType'] : input('allotType',1),
+ 'allotOnline' => !empty($data['allotOnline']) ? $data['allotOnline'] : input('allotOnline',false),
+ 'kefuRange' => !empty($data['kefuRange']) ? $data['kefuRange'] : input('kefuRange',5),
+ 'wechatRange' => !empty($data['wechatRange']) ? $data['wechatRange'] : input('wechatRange',3),
+ 'kefuData' => !empty($data['kefuData']) ? $data['kefuData'] : input('kefuData',[]),
+ 'wechatData' => !empty($data['wechatData']) ? $data['wechatData'] : input('wechatData',[]),
+ 'labels' => !empty($data['labels']) ? $data['labels'] : input('labels',[]),
+ 'priorityStrategy' => json_encode(!empty($data['priorityStrategy']) ? $data['priorityStrategy'] : input('priorityStrategy',[]),256),
+ ];
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求到微信接口
+ $result = requestCurl($this->baseUrl . 'api/AllotRule/update', $params, 'PUT', $header, 'json');
+
+ if (empty($result)) {
+ $this->getAllRules();
+ return successJson([], '更新分配规则成功');
+ } else {
+ return errorJson($result);
+ }
+ } catch (\Exception $e) {
+ return errorJson('更新分配规则失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 删除分配规则
+ * @param array $data 请求数据
+ * @param bool $isInner 是否为内部调用
+ * @return \think\response\Json
+ */
+ public function deleteRule($data = [], $isInner = false)
+ {
+ // 获取授权token
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ if ($isInner) {
+ return json_encode(['code' => 500, 'msg' => '缺少授权信息']);
+ } else {
+ return errorJson('缺少授权信息');
+ }
+ }
+
+ try {
+ // 获取请求参数
+ $id = !empty($data['id']) ? $data['id'] : input('id', 0);
+ if (empty($id)) {
+ return errorJson('规则ID不能为空');
+ }
+
+ // 检查规则是否存在
+ $rule = AllotRuleModel::where('id', $id)->find();
+ if (empty($rule)) {
+ return errorJson('规则不存在');
+ }
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'plain');
+
+ // 发送请求到微信接口
+ $result = requestCurl($this->baseUrl . 'api/AllotRule/delete?id=' . $id, [], 'DELETE', $header);
+ $response = handleApiResponse($result);
+
+ if (empty($response)) {
+ // 删除成功,同步本地数据库
+ AllotRuleModel::where('id', $id)->update(['isDel' => 1]);
+ return successJson([], '删除分配规则成功');
+ } else {
+ return errorJson($response);
+ }
+ } catch (\Exception $e) {
+ return errorJson('删除分配规则失败:' . $e->getMessage());
+ }
+ }
+
+ /************************************
+ * 数据查询相关方法
+ ************************************/
+
+ /**
+ * 自动创建分配规则
+ * 根据今日新增微信账号自动创建或更新分配规则
+ * @param array $data 请求数据
+ * @param bool $isInner 是否为内部调用
+ * @return \think\response\Json|string
+ */
+ public function autoCreateAllotRules($data = [], $isInner = false)
+ {
+ try {
+ // 获取今天的开始时间和结束时间
+ $todayStart = strtotime(date('Y-m-d 00:00:00'));
+ $todayEnd = strtotime(date('Y-m-d 23:59:59'));
+
+ // 查询今天新增的微信账号
+ $newAccounts = Db::table('s2_wechat_account')
+ ->alias('wa')
+ ->join(['s2_company_account' => 'ca'], 'wa.deviceAccountId = ca.id', 'LEFT')
+ ->field([
+ 'wa.id',
+ 'wa.wechatId',
+ 'wa.nickname',
+ 'wa.deviceAccountId',
+ 'wa.alias',
+ 'wa.createTime',
+ 'ca.departmentId',
+ ])
+ ->where('wa.createTime', 'BETWEEN', [$todayStart, $todayEnd])
+ ->where('wa.isDeleted', 0)
+ ->order('wa.createTime', 'DESC')
+ ->select();
+
+ // 没有今日新增微信账号,直接返回
+ if (empty($newAccounts)) {
+ $result = ['code' => 200, 'msg' => '没有今日新增微信账号,无需创建分配规则', 'data' => []];
+ return $isInner ? json_encode($result) : successJson([], '没有今日新增微信账号,无需创建分配规则');
+ }
+
+ // 获取所有分配规则
+ foreach ($newAccounts as $key => $value) {
+ $rules = AllotRuleModel::where(['departmentId' => $value['departmentId'],'isDel' => 0])->order('id','DESC')->find();
+ if (!empty($rules)) {
+ $wechatData = json_decode($rules['wechatData'], true);
+ if (!in_array($value['id'], $wechatData)) {
+ $wechatData[] = $value['id'];
+ $kefuData = [$value['deviceAccountId']];
+ $this->updateRule(['id' => $rules['id'],'wechatData' => $wechatData,'kefuData' => $kefuData],true);
+ }
+ }else{
+ $wechatData =[$value['id']];
+ $kefuData = [$value['deviceAccountId']];
+ $this->createRule(['wechatData' => $wechatData,'kefuData' => $kefuData,'departmentId' => $value['departmentId']],true);
+ }
+ }
+ $result = ['code' => 200, 'msg' => '自动分配规则成功', 'data' => []];
+ return $isInner ? json_encode($result) : successJson([], '自动分配规则成功');
+ } catch (\Exception $e) {
+ $error = '自动分配规则失败: ' . $e->getMessage();
+ $result = ['code' => 500, 'msg' => $error];
+ return $isInner ? json_encode($result) : errorJson($error);
+ }
+ }
+
+ /************************************
+ * 辅助方法
+ ************************************/
+
+ /**
+ * 保存分配规则数据到数据库
+ * @param array $item 分配规则数据
+ */
+ private function saveAllotRule($item)
+ {
+ $data = [
+ 'id' => isset($item['id']) ? $item['id'] : '',
+ 'tenantId' => isset($item['tenantId']) ? $item['tenantId'] : 0,
+ 'allotType' => isset($item['allotType']) ? $item['allotType'] : 0,
+ 'allotOnline' => isset($item['allotOnline']) ? $item['allotOnline'] : false,
+ 'kefuRange' => isset($item['kefuRange']) ? $item['kefuRange'] : 0,
+ 'wechatRange' => isset($item['wechatRange']) ? $item['wechatRange'] : 0,
+ 'kefuData' => isset($item['kefuData']) ? json_encode($item['kefuData']) : json_encode([]),
+ 'wechatData' => isset($item['wechatData']) ? json_encode($item['wechatData']) : json_encode([]),
+ 'labels' => isset($item['labels']) ? json_encode($item['labels']) : json_encode([]),
+ 'priorityStrategy' => isset($item['priorityStrategy']) ? json_encode($item['priorityStrategy']) : json_encode([]),
+ 'sortIndex' => isset($item['sortIndex']) ? $item['sortIndex'] : 0,
+ 'creatorAccountId' => isset($item['creatorAccountId']) ? $item['creatorAccountId'] : 0,
+ 'createTime' => isset($item['createTime']) ? (strtotime($item['createTime']) ?: 0) : 0,
+ 'ruleName' => isset($item['ruleName']) ? $item['ruleName'] : '',
+ 'isDel' => 0,
+ ];
+
+ // 使用ID作为唯一性判断
+ $rule = AllotRuleModel::where('id', $item['id'])->find();
+
+ if ($rule) {
+ $rule->save($data);
+ } else {
+ AllotRuleModel::create($data);
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/api/controller/AutomaticAssign.php b/application/api/controller/AutomaticAssign.php
new file mode 100644
index 0000000..6a0953c
--- /dev/null
+++ b/application/api/controller/AutomaticAssign.php
@@ -0,0 +1,346 @@
+request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'缺少授权信息']);
+ }else{
+ return errorJson('缺少授权信息');
+ }
+ }
+
+ try {
+ // 获取请求参数
+ $toAccountId = !empty($data['toAccountId']) ? $data['toAccountId'] : input('toAccountId', '');
+ $wechatAccountKeyword = !empty($data['wechatAccountKeyword']) ? $data['wechatAccountKeyword'] : input('wechatAccountKeyword', '');
+ $isDeleted = !empty($data['isDeleted']) ? $data['isDeleted'] : input('isDeleted', false);
+
+ if (empty($toAccountId)) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'目标账号ID不能为空']);
+ }else{
+ return errorJson('目标账号ID不能为空');
+ }
+ }
+
+ $params = [
+ 'accountKeyword' => !empty($data['accountKeyword']) ? $data['accountKeyword'] : '',
+ 'addFrom' => !empty($data['addFrom']) ? $data['addFrom'] : [],
+ 'allotAccountId' => !empty($data['allotAccountId']) ? $data['allotAccountId'] : '',
+ 'containAllLabel' => !empty($data['containAllLabel']) ? $data['containAllLabel'] : false,
+ 'containSubDepartment' => !empty($data['containSubDepartment']) ? $data['containSubDepartment'] : false,
+ 'departmentId' => !empty($data['departmentId']) ? $data['departmentId'] : '',
+ 'extendFields' => !empty($data['extendFields']) ? $data['extendFields'] : [],
+ 'friendKeyword' => !empty($data['friendKeyword']) ? $data['friendKeyword'] : '',
+ 'friendPhoneKeyword' => !empty($data['friendPhoneKeyword']) ? $data['friendPhoneKeyword'] : '',
+ 'friendPinYinKeyword' => !empty($data['friendPinYinKeyword']) ? $data['friendPinYinKeyword'] : '',
+ 'friendRegionKeyword' => !empty($data['friendRegionKeyword']) ? $data['friendRegionKeyword'] : '',
+ 'friendRemarkKeyword' => !empty($data['friendRemarkKeyword']) ? $data['friendRemarkKeyword'] : '',
+ 'gender' => !empty($data['gender']) ? $data['gender'] : '',
+ 'groupId' => !empty($data['groupId']) ? $data['groupId'] : null,
+ 'isByRule' => !empty($data['isByRule']) ? $data['isByRule'] : false,
+ 'isDeleted' => $isDeleted,
+ 'isPass' => !empty($data['isPass']) ? $data['isPass'] : true,
+ 'keyword' => !empty($data['keyword']) ? $data['keyword'] : '',
+ 'labels' => !empty($data['labels']) ? $data['labels'] : [],
+ 'pageIndex' => !empty($data['pageIndex']) ? $data['pageIndex'] : 0,
+ 'pageSize' => !empty($data['pageSize']) ? $data['pageSize'] : 100,
+ 'preFriendId' => !empty($data['preFriendId']) ? $data['preFriendId'] : '',
+ 'toAccountId' => $toAccountId,
+ 'wechatAccountKeyword' => $wechatAccountKeyword
+ ];
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求
+ $result = requestCurl($this->baseUrl . 'api/WechatFriend/allotSearchResult', $params, 'PUT', $header,'json');
+ $response = handleApiResponse($result);
+
+ if($response){
+ if($isInner){
+ return json_encode(['code'=>200,'msg'=>'微信好友自动分配成功']);
+ }else{
+ return successJson([],'微信好友自动分配成功');
+ }
+ }else{
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>$response]);
+ }else{
+ return errorJson($response);
+ }
+ }
+
+ } catch (\Exception $e) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'微信好友自动分配失败:' . $e->getMessage()]);
+ }else{
+ return errorJson('微信好友自动分配失败:' . $e->getMessage());
+ }
+ }
+ }
+
+
+ /**
+ * 自动分配微信群聊
+ * @param string $toAccountId 目标账号ID
+ * @param string $wechatAccountKeyword 微信账号关键字
+ * @param bool $isDeleted 是否已删除
+ * @return \think\response\Json
+ */
+ public function autoAllotWechatChatroom($data = [])
+ {
+ // 获取授权token
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ return errorJson('缺少授权信息');
+ }
+
+ try {
+ // 获取请求参数
+ $toAccountId = !empty($data['toAccountId']) ? $data['toAccountId'] : input('toAccountId', '');
+ $wechatAccountKeyword = !empty($data['wechatAccountKeyword']) ? $data['wechatAccountKeyword'] : input('wechatAccountKeyword', '');
+ $isDeleted = !empty($data['isDeleted']) ? $data['isDeleted'] : input('isDeleted', false);
+
+ if (empty($toAccountId)) {
+ return errorJson('目标账号ID不能为空');
+ }
+
+ $params = [
+ 'AllotBySearch' => true,
+ 'byRule' => false,
+ 'comment' => '',
+ 'groupId' => null,
+ 'isDeleted' => $isDeleted,
+ 'keyword' => '',
+ 'memberKeyword' => '',
+ 'notifyReceiver' => false,
+ 'toAccountId' => $toAccountId,
+ 'wechatAccountKeyword' => $wechatAccountKeyword,
+ 'wechatChatroomId' => 0,
+ 'wechatChatroomIds' => []
+ ];
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求
+ $result = requestCurl($this->baseUrl . 'api/WechatChatroom/allotChatroom', $params, 'PUT', $header, 'json');
+ $response = handleApiResponse($result);
+
+ if($response){
+ return successJson([], '微信群聊自动分配成功');
+ }else{
+ return errorJson($response);
+ }
+ } catch (\Exception $e) {
+ return errorJson('微信群聊自动分配失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 指定微信好友分配到指定账号
+ * @param int $wechatFriendId 微信好友ID
+ * @param int $toAccountId 目标账号ID
+ * @param string $comment 评论/备注
+ * @param bool $notifyReceiver 是否通知接收者
+ * @param int $optFrom 操作来源
+ * @return \think\response\Json
+ */
+ public function allotWechatFriend($data = [],$isInner = false,$errorNum = 0)
+ {
+ // 获取授权token
+ $authorization = $this->authorization;
+
+ if (empty($authorization)) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'缺少授权信息']);
+ }else{
+ return errorJson('缺少授权信息');
+ }
+ }
+
+ try {
+ // 获取请求参数
+ $wechatFriendId = !empty($data['wechatFriendId']) ? $data['wechatFriendId'] : input('wechatFriendId', 0);
+ $toAccountId = !empty($data['toAccountId']) ? $data['toAccountId'] : input('toAccountId', 0);
+ $comment = !empty($data['comment']) ? $data['comment'] : input('comment', '');
+ $notifyReceiver = !empty($data['notifyReceiver']) ? $data['notifyReceiver'] : input('notifyReceiver', 'false');
+ $optFrom = !empty($data['optFrom']) ? $data['optFrom'] : input('optFrom', 4); // 默认操作来源为4
+
+ // 参数验证
+ if (empty($wechatFriendId)) {
+ return json_encode(['code'=>500,'msg'=>'微信好友ID不能为空']);
+
+ }
+
+ if (empty($toAccountId)) {
+ return json_encode(['code'=>500,'msg'=>'目标账号ID不能为空']);
+ }
+
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求
+ $url = $this->baseUrl . 'api/WechatFriend/allot?wechatFriendId='.$wechatFriendId.'¬ifyReceiver='.$notifyReceiver.'&comment='.$comment.'&toAccountId='.$toAccountId.'&optFrom='.$optFrom;
+ $result = requestCurl($url, [], 'PUT', $header, 'json');
+ $response = handleApiResponse($result);
+ if (empty($response)) {
+ if($isInner){
+ return json_encode(['code'=>200,'msg'=>'微信好友分配成功']);
+ }else{
+ return successJson([], '微信好友分配成功');
+ }
+ } else {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>$result]);
+ }else{
+ return errorJson($result);
+ }
+ }
+ } catch (\Exception $e) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'微信好友分配失败:' . $e->getMessage()]);
+ }else{
+ Cache::rm('system_authorization_token');
+ Cache::rm('system_refresh_token');
+ $errorNum ++;
+ if ($errorNum <= 3) {
+ $this->allotWechatFriend($data,$isInner,$errorNum);
+ }
+ return json_encode(['code'=>500,'msg'=> $result]);
+ return errorJson('微信好友分配失败:' . $e->getMessage());
+ }
+ }
+ }
+
+
+ public function multiAllotFriendToAccount($data = [],$errorNum = 0){
+ // 获取授权token
+ $authorization = $this->authorization;
+ if (empty($authorization)) {
+ return json_encode(['code'=>500,'msg'=>'缺少授权信息']);
+ }
+
+ $wechatFriendIds = !empty($data['wechatFriendIds']) ? $data['wechatFriendIds'] : input('wechatFriendIds', []);
+ $toAccountId = !empty($data['toAccountId']) ? $data['toAccountId'] : input('toAccountId', 0);
+ $notifyReceiver = !empty($data['notifyReceiver']) ? $data['notifyReceiver'] : input('notifyReceiver', 'false');
+ // 参数验证
+ if (empty($wechatFriendIds)) {
+ return json_encode(['code'=>500,'msg'=>'微信好友ID不能为空']);
+ }
+
+ if (empty($toAccountId)) {
+ return json_encode(['code'=>500,'msg'=>'目标账号ID不能为空']);
+ }
+
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求
+ $url = $this->baseUrl . 'api/WechatFriend/multiAllotFriendToAccount?wechatFriendIds='.$wechatFriendIds.'&toAccountId='.$toAccountId.'¬ifyReceiver='.$notifyReceiver;
+ $result = requestCurl($url, [], 'PUT', $header, 'json');
+ if (empty($result)) {
+ return json_encode(['code'=>200,'msg'=>'微信好友分配成功']);
+ } else {
+ Cache::rm('system_authorization_token');
+ Cache::rm('system_refresh_token');
+ $errorNum ++;
+ if ($errorNum <= 3) {
+ $this->multiAllotFriendToAccount($data,$errorNum);
+ }
+ return json_encode(['code'=>500,'msg'=> $result]);
+ }
+
+ }
+
+
+ /**
+ * 分配搜索结果
+ * @param array $data 请求参数
+ * @return \think\response\Json
+ */
+ public function allotSearchResult($data = [])
+ {
+ // 获取授权token
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ return json_encode(['code'=>500,'msg'=>'缺少授权信息']);
+ }
+
+ try {
+
+ $params = [
+ 'accountKeyword' => !empty($data['accountKeyword']) ? $data['accountKeyword'] : '',
+ 'addFrom' => !empty($data['addFrom']) ? $data['addFrom'] : [],
+ 'allotAccountId' => !empty($data['allotAccountId']) ? $data['allotAccountId'] : '',
+ 'containAllLabel' => !empty($data['containAllLabel']) ? $data['containAllLabel'] : false,
+ 'containSubDepartment' => !empty($data['containSubDepartment']) ? $data['containSubDepartment'] : false,
+ 'departmentId' => !empty($data['departmentId']) ? $data['departmentId'] : '',
+ 'extendFields' => !empty($data['extendFields']) ? json_encode($data['extendFields']) : json_encode([]),
+ 'friendKeyword' => !empty($data['friendKeyword']) ? $data['friendKeyword'] : '',
+ 'friendPhoneKeyword' => !empty($data['friendPhoneKeyword']) ? $data['friendPhoneKeyword'] : '',
+ 'friendPinYinKeyword' => !empty($data['friendPinYinKeyword']) ? $data['friendPinYinKeyword'] : '',
+ 'friendRegionKeyword' => !empty($data['friendRegionKeyword']) ? $data['friendRegionKeyword'] : '',
+ 'friendRemarkKeyword' => !empty($data['friendRemarkKeyword']) ? $data['friendRemarkKeyword'] : '',
+ 'gender' => !empty($data['gender']) ? $data['gender'] : '',
+ 'groupId' => !empty($data['groupId']) ? $data['groupId'] : null,
+ 'isByRule' => !empty($data['isByRule']) ? $data['isByRule'] : false,
+ 'isDeleted' => !empty($data['isDeleted']) ? $data['isDeleted'] : false,
+ 'isPass' => !empty($data['isPass']) ? $data['isPass'] : true,
+ 'keyword' => !empty($data['keyword']) ? $data['keyword'] : '',
+ 'labels' => !empty($data['labels']) ? $data['labels'] : [],
+ 'pageIndex' => !empty($data['pageIndex']) ? $data['pageIndex'] : 0,
+ 'pageSize' => !empty($data['pageSize']) ? $data['pageSize'] : 20,
+ 'preFriendId' => !empty($data['preFriendId']) ? $data['preFriendId'] : '',
+ 'toAccountId' => !empty($data['toAccountId']) ? $data['toAccountId'] : '',
+ 'wechatAccountKeyword' => !empty($data['wechatAccountKeyword']) ? $data['wechatAccountKeyword'] : ''
+ ];
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求
+ $result = requestCurl($this->baseUrl . 'api/WechatFriend/allotSearchResult', $params, 'POST', $header, 'json');
+ $response = handleApiResponse($result);
+ if($response){
+ return json_encode(['code'=>200,'msg'=>'分配成功']);
+ }else{
+ return json_encode(['code'=>500,'msg'=>$response]);
+ }
+ } catch (\Exception $e) {
+ return json_encode(['code'=>500,'msg'=>'微信好友分配失败:' . $e->getMessage()]);
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/application/api/controller/BaseController.php b/application/api/controller/BaseController.php
new file mode 100644
index 0000000..84f6993
--- /dev/null
+++ b/application/api/controller/BaseController.php
@@ -0,0 +1,26 @@
+baseUrl = Env::get('api.wechat_url');
+ $this->authorization = AuthService::getSystemAuthorization();
+ }
+}
diff --git a/application/api/controller/CallRecordingController.php b/application/api/controller/CallRecordingController.php
new file mode 100644
index 0000000..a811261
--- /dev/null
+++ b/application/api/controller/CallRecordingController.php
@@ -0,0 +1,142 @@
+request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ if ($isInner) {
+ return json_encode(['code' => 500, 'msg' => '缺少授权信息']);
+ } else {
+ return errorJson('缺少授权信息');
+ }
+ }
+
+ try {
+ // 构建请求参数
+ $params = [
+ 'keyword' => $keyword,
+ 'isCallOut' => $isCallOut,
+ 'secondMin' => $secondMin,
+ 'secondMax' => $secondMax,
+ 'departmentIds' => $departmentIds,
+ 'pageIndex' => $pageIndex,
+ 'pageSize' => $pageSize,
+ 'from' => $from,
+ 'to' => $to,
+ 'departmentId' => $departmentId
+ ];
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'plain');
+
+ // 发送请求获取通话记录列表
+ $result = requestCurl($this->baseUrl . 'api/CallRecording/list', $params, 'GET', $header);
+ $response = handleApiResponse($result);
+
+ // 保存数据到数据库
+ if (!empty($response['results'])) {
+ foreach ($response['results'] as $item) {
+ $this->saveCallRecording($item);
+ }
+ }
+
+ if ($isInner) {
+ return json_encode(['code' => 200, 'msg' => '获取通话记录列表成功', 'data' => $response]);
+ } else {
+ return successJson($response, '获取通话记录列表成功');
+ }
+ } catch (\Exception $e) {
+ if ($isInner) {
+ return json_encode(['code' => 500, 'msg' => '获取通话记录列表失败:' . $e->getMessage()]);
+ } else {
+ return errorJson('获取通话记录列表失败:' . $e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * 保存通话记录数据到数据库
+ * @param array $item 通话记录数据
+ */
+ private function saveCallRecording($item)
+ {
+ // 将时间戳转换为秒级时间戳(API返回的是毫秒级)
+ $beginTime = isset($item['beginTime']) ? intval($item['beginTime'] / 1000) : 0;
+ $endTime = isset($item['endTime']) ? intval($item['endTime'] / 1000) : 0;
+ $callBeginTime = isset($item['callBeginTime']) ? intval($item['callBeginTime'] / 1000) : 0;
+
+ // 将日期时间字符串转换为时间戳
+ $createTime = isset($item['createTime']) ? strtotime($item['createTime']) : 0;
+ $lastUpdateTime = isset($item['lastUpdateTime']) ? strtotime($item['lastUpdateTime']) : 0;
+
+ $data = [
+ 'id' => isset($item['id']) ? $item['id'] : 0,
+ 'tenantId' => isset($item['tenantId']) ? $item['tenantId'] : 0,
+ 'deviceOwnerId' => isset($item['deviceOwnerId']) ? $item['deviceOwnerId'] : 0,
+ 'userName' => isset($item['userName']) ? $item['userName'] : '',
+ 'nickname' => isset($item['nickname']) ? $item['nickname'] : '',
+ 'realName' => isset($item['realName']) ? $item['realName'] : '',
+ 'deviceMemo' => isset($item['deviceMemo']) ? $item['deviceMemo'] : '',
+ 'fileName' => isset($item['fileName']) ? $item['fileName'] : '',
+ 'imei' => isset($item['imei']) ? $item['imei'] : '',
+ 'phone' => isset($item['phone']) ? $item['phone'] : '',
+ 'isCallOut' => isset($item['isCallOut']) ? $item['isCallOut'] : false,
+ 'beginTime' => $beginTime,
+ 'endTime' => $endTime,
+ 'audioUrl' => isset($item['audioUrl']) ? $item['audioUrl'] : '',
+ 'mp3AudioUrl' => isset($item['mp3AudioUrl']) ? $item['mp3AudioUrl'] : '',
+ 'callBeginTime' => $callBeginTime,
+ 'callLogId' => isset($item['callLogId']) ? $item['callLogId'] : 0,
+ 'callType' => isset($item['callType']) ? $item['callType'] : 0,
+ 'duration' => isset($item['duration']) ? $item['duration'] : 0,
+ 'skipReason' => isset($item['skipReason']) ? $item['skipReason'] : '',
+ 'skipUpload' => isset($item['skipUpload']) ? $item['skipUpload'] : false,
+ 'isDeleted' => isset($item['isDeleted']) ? $item['isDeleted'] : false,
+ 'createTime' => $createTime,
+ 'lastUpdateTime' => $lastUpdateTime
+ ];
+
+ // 使用id作为唯一性判断
+ $callRecording = CallRecordingModel::where('id', $item['id'])->find();
+ if ($callRecording) {
+ $callRecording->save($data);
+ } else {
+ CallRecordingModel::create($data);
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/api/controller/DeviceController.php b/application/api/controller/DeviceController.php
new file mode 100644
index 0000000..33dd45d
--- /dev/null
+++ b/application/api/controller/DeviceController.php
@@ -0,0 +1,769 @@
+authorization;
+ if (empty($authorization)) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'缺少授权信息']);
+ }else{
+ return errorJson('缺少授权信息');
+ }
+ }
+
+ try {
+ // 根据isDel设置对应的deleteType值
+ $deleteType = 'unDeleted'; // 默认值
+ if ($isDel == 1) {
+ $deleteType = 'deleted';
+ } elseif ($isDel == 2) {
+ $deleteType = 'deletedAndStop';
+ }
+
+ // 构建请求参数
+ $params = [
+ 'accountId' => !empty($data['accountId']) ? $data['accountId'] : $this->request->param('accountId', ''),
+ 'keyword' => $this->request->param('keyword', ''),
+ 'imei' => $this->request->param('imei', ''),
+ 'groupId' => $this->request->param('groupId', ''),
+ 'brand' => $this->request->param('brand', ''),
+ 'model' => $this->request->param('model', ''),
+ 'deleteType' => $this->request->param('deleteType', $deleteType),
+ 'operatingSystem' => $this->request->param('operatingSystem', ''),
+ 'softwareVersion' => $this->request->param('softwareVersion', ''),
+ 'phoneAppVersion' => $this->request->param('phoneAppVersion', ''),
+ 'recorderVersion' => $this->request->param('recorderVersion', ''),
+ 'contactsVersion' => $this->request->param('contactsVersion', ''),
+ 'rooted' => $this->request->param('rooted', ''),
+ 'xPosed' => $this->request->param('xPosed', ''),
+ 'alive' => $this->request->param('alive', ''),
+ 'hasWechat' => $this->request->param('hasWechat', ''),
+ 'departmentId' => $this->request->param('departmentId', ''),
+ 'pageIndex' => !empty($data['pageIndex']) ? $data['pageIndex'] : $this->request->param('pageIndex', 0),
+ 'pageSize' => !empty($data['pageSize']) ? $data['pageSize'] : $this->request->param('pageSize', 20)
+ ];
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'plain');
+
+ // 发送请求获取设备列表
+ $result = requestCurl($this->baseUrl . 'api/device/pageResult', $params, 'GET', $header);
+ $response = handleApiResponse($result);
+
+ // 保存数据到数据库
+ if (!empty($response['results'])) {
+ foreach ($response['results'] as $item) {
+ $this->saveDevice($item);
+ }
+ }
+
+ if($isInner){
+ return json_encode(['code'=>200,'msg'=>'success','data'=>$response]);
+ }else{
+ return successJson($response);
+ }
+ } catch (\Exception $e) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'获取设备列表失败:' . $e->getMessage()]);
+ }else{
+ return errorJson('获取设备列表失败:' . $e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * 生成设备二维码
+ * @param int $accountId 账号ID
+ * @return \think\response\Json
+ */
+ public function addDevice($accountId = 0,$isInner = false)
+ {
+ if (empty($accountId)) {
+ $accountId = $this->request->param('accountId', '');
+ }
+
+ if (empty($accountId)) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'账号ID不能为空']);
+ }else{
+ return errorJson('账号ID不能为空');
+ }
+ }
+
+ try {
+ // 获取环境配置
+ $tenantGuid = Env::get('api.guid', '');
+ $deviceSocketHost = Env::get('api.deviceSocketHost', '');
+
+ if (empty($tenantGuid) || empty($deviceSocketHost)) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'环境配置不完整,请检查api.guid和api.deviceSocketHost配置']);
+ }else{
+ return errorJson('环境配置不完整,请检查api.guid和api.deviceSocketHost配置');
+ }
+ }
+
+ // 构建设备配置数据
+ $data = [
+ 'tenantGuid' => $tenantGuid,
+ 'deviceSocketHost' => $deviceSocketHost,
+ 'checkVersionUrl' => '',
+ 'accountId' => intval($accountId)
+ ];
+
+ // 将数据转换为JSON
+ $jsonData = json_encode($data);
+
+ // 生成二维码图片
+ $qrCode = $this->generateQrCodeImage($jsonData);
+
+ return successJson([
+ 'qrCode' => $qrCode,
+ 'config' => $data
+ ]);
+ } catch (\Exception $e) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'生成设备二维码失败:' . $e->getMessage()]);
+ }else{
+ return errorJson('生成设备二维码失败:' . $e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * 更新设备账号
+ * @return \think\response\Json
+ */
+ public function updateaccount($data = [],$isInner = false)
+ {
+ // 获取授权token
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'缺少授权信息']);
+ }else{
+ return errorJson('缺少授权信息');
+ }
+ }
+
+ try {
+ // 获取参数
+ $id = !empty($data['id']) ? $data['id'] : $this->request->param('id', '');
+ $accountId = !empty($data['accountId']) ? $data['accountId'] : $this->request->param('accountId', '');
+
+ if (empty($id)) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'设备ID不能为空']);
+ }else{
+ return errorJson('设备ID不能为空');
+ }
+ }
+
+ if (empty($accountId)) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'账号id不能为空']);
+ }else{
+ return errorJson('账号id不能为空');
+ }
+ }
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'plain');
+
+ // 发送请求
+ $result = requestCurl($this->baseUrl . 'api/device/updateaccount?accountId=' . $accountId . '&deviceId=' . $id, [], 'PUT', $header);
+ $response = handleApiResponse($result);
+
+ if(empty($response)){
+ return successJson([],'操作成功');
+ }else{
+ return errorJson([],$response);
+ }
+ } catch (\Exception $e) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'更新设备账号失败:' . $e->getMessage()]);
+ }else{
+ return errorJson('更新设备账号失败:' . $e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * 更新设备所属分组
+ * @param int $id 设备ID
+ * @param int $groupId 分组ID
+ * @return \think\response\Json
+ */
+ public function updateDeviceToGroup($data = [])
+ {
+ // 获取授权token
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ return errorJson('缺少授权信息');
+ }
+
+ try {
+ // 获取参数
+ $id = !empty($data['id']) ? $data['id'] : $this->request->param('id', '');
+ $groupId = !empty($data['groupId']) ? $data['groupId'] : $this->request->param('groupId', '');
+
+ if (empty($id)) {
+ return errorJson('设备ID不能为空');
+ }
+
+ if (empty($groupId)) {
+ return errorJson('分组ID不能为空');
+ }
+
+ // 验证设备是否存在
+ $device = DeviceModel::where('id', $id)->find();
+ if (empty($device)) {
+ return errorJson('设备不存在');
+ }
+
+ // 验证分组是否存在
+ $group = DeviceGroupModel::where('id', $groupId)->find();
+ if (empty($group)) {
+ return errorJson('分组不存在');
+ }
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'plain');
+
+ // 发送请求到微信接口
+ $result = requestCurl($this->baseUrl . 'api/device/updateDeviceGroup?id=' . $id . '&groupId=' . $groupId, [], 'PUT', $header);
+ $response = handleApiResponse($result);
+
+ if (empty($response)) {
+ // 更新成功,更新本地数据库
+ $device->groupId = $groupId;
+ $device->groupName = $group->groupName;
+ $device->save();
+
+ return successJson([], '设备分组更新成功');
+ } else {
+ return errorJson([], $response);
+ }
+ } catch (\Exception $e) {
+ return errorJson('更新设备分组失败:' . $e->getMessage());
+ }
+ }
+
+
+ /**
+ * 删除设备
+ *
+ * @param $deviceId
+ * @return false|string
+ * @throws \think\db\exception\DataNotFoundException
+ * @throws \think\db\exception\ModelNotFoundException
+ * @throws \think\exception\DbException
+ */
+ public function delDevice($deviceId = '')
+ {
+ $authorization = $this->authorization;
+ if (empty($authorization)) {
+ return json_encode(['code'=>500,'msg'=>'缺少授权信息']);
+ }
+
+ if (empty($deviceId)) {
+ return json_encode(['code'=>500,'msg'=>'删除的设备不能为空']);
+ }
+
+ $device = Db::table('s2_device')->where('id', $deviceId)->find();
+ if (empty($device)) {
+ return json_encode(['code'=>500,'msg'=>'设备不存在']);
+ }
+
+ try {
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+ // 发送请求
+ $result = requestCurl($this->baseUrl . 'api/device/del/'.$deviceId, [], 'DELETE', $header,'json');
+ if (empty($result)) {
+ Db::table('s2_device')->where('id', $deviceId)->update([
+ 'isDeleted' => 1,
+ 'deleteTime' => time()
+ ]);
+ return json_encode(['code'=>200,'msg'=>'删除成功']);
+ }else{
+ return json_encode(['code'=>200,'msg'=>'删除失败']);
+ }
+ } catch (\Exception $e) {
+ return json_encode(['code'=>500,'msg'=>'获取设备分组列表失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 更新设备联系人
+ * @param int $id 设备ID
+ * @param int $groupId 分组ID
+ * @return \think\response\Json
+ */
+ public function importContact($data = [],$isInner = false)
+ {
+ // 获取授权token
+ $authorization = $this->authorization;
+ if (empty($authorization)) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'缺少授权信息']);
+ }else{
+ return errorJson('缺少授权信息');
+ }
+ }
+
+ try {
+ // 获取参数
+ $deviceId = !empty($data['deviceId']) ? $data['deviceId'] : $this->request->param('deviceId', '');
+ $rawContactJson = !empty($data['contactJson']) ? $data['contactJson'] : $this->request->param('contactJson', '');
+ $clearContact = !empty($data['clearContact']) ? $data['clearContact'] : $this->request->param('clearContact', false);
+
+
+ if (empty($deviceId)) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'设备ID不能为空']);
+ }else{
+ return errorJson('设备ID不能为空');
+ }
+ }
+
+ $contacts = [];
+ if (!empty($rawContactJson)) {
+ if (is_string($rawContactJson)) {
+ $decodedContacts = json_decode($rawContactJson, true);
+ if (json_last_error() === JSON_ERROR_NONE) {
+ // It's a valid JSON string
+ $contacts = $decodedContacts;
+ } else {
+ // It's not a JSON string, treat as multi-line text
+ $lines = explode("\n", str_replace("\r\n", "\n", $rawContactJson));
+ foreach ($lines as $line) {
+ $line = trim($line);
+ if (empty($line)) continue;
+ $parts = explode(',', $line);
+ if (count($parts) == 2) {
+ $contacts[] = ['name' => trim($parts[0]), 'phone' => trim($parts[1])];
+ }
+ }
+ }
+ } elseif (is_array($rawContactJson)) {
+ // It's already an array
+ $contacts = $rawContactJson;
+ }
+ }
+
+
+ if (empty($contacts)){
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'更新设备联系人失败:通讯录不能为空' ]);
+ }else{
+ return errorJson('更新设备联系人失败:通讯录不能为空' );
+ }
+ }
+
+
+ // Trim whitespace from name and phone in all cases
+ if (!empty($contacts)) {
+ foreach ($contacts as &$contact) {
+ if (isset($contact['name'])) {
+ $contact['name'] = trim($contact['name']);
+ }
+ if (isset($contact['phone'])) {
+ $contact['phone'] = trim($contact['phone']);
+ }
+ }
+ unset($contact); // Unset reference to the last element
+ }
+
+ $contactJsonForApi = json_encode($contacts);
+
+ // 构建请求参数
+ $params = [
+ 'deviceId' => $deviceId,
+ 'contactJson' => $contactJsonForApi,
+ 'clearContact' => $clearContact
+ ];
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求
+ $result = requestCurl($this->baseUrl . 'api/device/importContact', $params, 'POST', $header,'json');
+ $response = handleApiResponse($result);
+
+ if(empty($response)){
+ if($isInner){
+ return json_encode(['code'=>200,'msg'=>'更新设备联系人成功' ]);
+ }else{
+ return successJson([],'更新设备联系人失败:通讯录不能为空' );
+ }
+ }else{
+ if($isInner){
+ return json_encode(['code'=>200,'msg'=> $response ]);
+ }else{
+ return successJson([],$response );
+ }
+ }
+ } catch (\Exception $e) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'更新设备联系人失败:' . $e->getMessage()]);
+ }else{
+ return errorJson('更新设备联系人失败:' . $e->getMessage());
+ }
+ }
+ }
+
+
+ /************************ 设备分组相关接口 ************************/
+
+ /**
+ * 获取设备分组列表
+ * @return \think\response\Json
+ */
+ public function getGroupList($data = [],$isInner = false)
+ {
+ // 获取授权token
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'缺少授权信息']);
+ }else{
+ return errorJson('缺少授权信息');
+ }
+ }
+
+ try {
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求
+ $result = requestCurl($this->baseUrl . 'api/DeviceGroup/list', [], 'GET', $header,'json');
+ $response = handleApiResponse($result);
+ // 保存数据到数据库
+ if (!empty($response)) {
+ foreach ($response as $item) {
+ $this->saveDeviceGroup($item);
+ }
+ }
+ if($isInner){
+ return json_encode(['code'=>200,'msg'=>'success','data'=>$response]);
+ }else{
+ return successJson($response);
+ }
+ } catch (\Exception $e) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'获取设备分组列表失败:' . $e->getMessage()]);
+ }else{
+ return errorJson('获取设备分组列表失败:' . $e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * 创建设备分组
+ * @return \think\response\Json
+ */
+ public function createGroup($data = [],$isInner = false)
+ {
+ // 获取授权token
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'缺少授权信息']);
+ }else{
+ return errorJson('缺少授权信息');
+ }
+ }
+
+ try {
+ // 获取参数
+ $groupName = !empty($data['groupName']) ? $data['groupName'] : $this->request->param('groupName', '');
+ $groupMemo = !empty($data['groupMemo']) ? $data['groupMemo'] : $this->request->param('groupMemo', '');
+
+ if (empty($groupName)) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'分组名称不能为空']);
+ }else{
+ return errorJson('分组名称不能为空');
+ }
+ }
+
+ // 构建请求参数
+ $params = [
+ 'groupName' => $groupName,
+ 'groupMemo' => $groupMemo
+ ];
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求
+ $result = requestCurl($this->baseUrl . 'api/DeviceGroup/new', $params, 'POST', $header,'json');
+ if(empty($result)){
+ // $res = $this->getGroupList([],true);
+ // $res = json_decode($res,true);
+ // if(!empty($res['data'])){
+ // $data = $res['data'][0];
+ // }
+
+ $data = [];
+
+ if($isInner){
+ return json_encode(['code'=>200,'msg'=>'success','data'=>$data]);
+ }else{
+ return successJson($data,'操作成功');
+ }
+ }else{
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=> $result]);
+ }else{
+ return errorJson($result);
+ }
+ }
+ } catch (\Exception $e) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'创建设备分组失败:' . $e->getMessage()]);
+ }else{
+ return errorJson('创建设备分组失败:' . $e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * 更新设备分组
+ * @return \think\response\Json
+ */
+ public function updateDeviceGroup($data = [],$isInner = false)
+ {
+ // 获取授权token
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'缺少授权信息']);
+ }else{
+ return errorJson('缺少授权信息');
+ }
+ }
+
+ try {
+ // 获取参数
+ $id = !empty($data['id']) ? $data['id'] : $this->request->param('id', '');
+ $groupName = !empty($data['groupName']) ? $data['groupName'] : $this->request->param('groupName', '');
+ $groupMemo = !empty($data['groupMemo']) ? $data['groupMemo'] : $this->request->param('groupMemo', '');
+
+ if (empty($id)) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'分组ID不能为空']);
+ }else{
+ return errorJson('分组ID不能为空');
+ }
+ }
+
+ if (empty($groupName)) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'分组名称不能为空']);
+ }else{
+ return errorJson('分组名称不能为空');
+ }
+ }
+
+ $group = DeviceGroupModel::where('id', $id)->find();
+ if(empty($group)){
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'分组不存在']);
+ }else{
+ return errorJson('分组不存在');
+ }
+ }
+
+ $isGroupName = DeviceGroupModel::where('groupName', $groupName)->find();
+ if(!empty($isGroupName)){
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'分组名称已存在']);
+ }else{
+ return errorJson('分组名称已存在');
+ }
+ }
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 从数据库获取对象后,创建一个正确格式的数组用于API请求
+ $requestData = [
+ 'id' => $group->id,
+ 'tenantId' => $group->tenantId,
+ 'groupName' => $groupName,
+ 'groupMemo' => $groupMemo
+ ];
+
+ // 发送请求
+ $result = requestCurl($this->baseUrl . 'api/DeviceGroup/update', $requestData, 'PUT', $header, 'json');
+ if(empty($result)){
+ $group->groupName = $groupName;
+ $group->groupMemo = $groupMemo;
+ $group->save();
+ if($isInner){
+ return json_encode(['code'=>200,'msg'=>'success','data'=>$group]);
+ }else{
+ return successJson($group,'操作成功');
+ }
+ }else{
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=> $result]);
+ }else{
+ return errorJson($result);
+ }
+ }
+ } catch (\Exception $e) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'更新设备分组失败:' . $e->getMessage()]);
+ }else{
+ return errorJson('更新设备分组失败:' . $e->getMessage());
+ }
+ }
+ }
+
+ /************************ 私有辅助方法 ************************/
+
+ /**
+ * 保存设备数据到数据库
+ * @param array $item 设备数据
+ */
+ private function saveDevice($item)
+ {
+ $data = [
+ 'id' => isset($item['id']) ? $item['id'] : '',
+ 'userName' => isset($item['userName']) ? $item['userName'] : '',
+ 'nickname' => isset($item['nickname']) ? $item['nickname'] : '',
+ 'realName' => isset($item['realName']) ? $item['realName'] : '',
+ 'groupName' => isset($item['groupName']) ? $item['groupName'] : '',
+ 'wechatAccounts' => isset($item['wechatAccounts']) ? json_encode($item['wechatAccounts']) : json_encode([]),
+ 'alive' => isset($item['alive']) ? $item['alive'] : false,
+ 'lastAliveTime' => isset($item['lastAliveTime']) ? $item['lastAliveTime'] : null,
+ 'tenantId' => isset($item['tenantId']) ? $item['tenantId'] : 0,
+ 'groupId' => isset($item['groupId']) ? $item['groupId'] : 0,
+ 'currentAccountId' => isset($item['currentAccountId']) ? $item['currentAccountId'] : 0,
+ 'imei' => $item['imei'],
+ 'memo' => isset($item['memo']) ? $item['memo'] : '',
+ 'createTime' => isset($item['createTime']) ? strtotime($item['createTime']) : 0,
+ 'isDeleted' => isset($item['isDeleted']) ? $item['isDeleted'] : false,
+ 'deletedAndStop' => isset($item['deletedAndStop']) ? $item['deletedAndStop'] : false,
+ 'deleteTime' => empty($item['isDeleted']) ? 0 : strtotime($item['deleteTime']),
+ 'rooted' => isset($item['rooted']) ? $item['rooted'] : false,
+ 'xPosed' => isset($item['xPosed']) ? $item['xPosed'] : false,
+ 'brand' => isset($item['brand']) ? $item['brand'] : '',
+ 'model' => isset($item['model']) ? $item['model'] : '',
+ 'operatingSystem' => isset($item['operatingSystem']) ? $item['operatingSystem'] : '',
+ 'softwareVersion' => isset($item['softwareVersion']) ? $item['softwareVersion'] : '',
+ 'extra' => isset($item['extra']) ? json_encode($item['extra']) : json_encode([]),
+ 'phone' => isset($item['phone']) ? $item['phone'] : '',
+ 'lastUpdateTime' => isset($item['lastUpdateTime']) ? ($item['lastUpdateTime'] == '0001-01-01T00:00:00' ? 0 : strtotime($item['lastUpdateTime'])) : 0
+ ];
+
+ if (!empty($data['alive'])){
+ $data['aliveTime'] = time();
+ }
+
+
+ // 使用imei作为唯一性判断
+ $device = DeviceModel::where('id', $item['id'])->find();
+
+ if ($device) {
+ $device->save($data);
+ } else {
+
+ // autoLike:自动点赞
+ // momentsSync:朋友圈同步
+ // autoCustomerDev:自动开发客户
+ // groupMessageDeliver:群消息推送
+ // autoGroup:自动建群
+
+ $data['taskConfig'] = json_encode([
+ 'autoLike' => true,
+ 'momentsSync' => true,
+ 'autoCustomerDev' => true,
+ 'groupMessageDeliver' => true,
+ 'autoGroup' => true,
+ ]);
+ DeviceModel::create($data);
+ }
+ }
+
+ /**
+ * 保存设备分组数据到数据库
+ * @param array $item 设备分组数据
+ */
+ private function saveDeviceGroup($item)
+ {
+ $data = [
+ 'id' => $item['id'],
+ 'tenantId' => $item['tenantId'],
+ 'groupName' => $item['groupName'],
+ 'groupMemo' => $item['groupMemo'],
+ 'count' => isset($item['count']) ? $item['count'] : 0,
+ 'createTime' => $item['createTime'] == '0001-01-01T00:00:00' ? 0 : strtotime($item['createTime'])
+ ];
+
+ // 使用ID作为唯一性判断
+ $group = DeviceGroupModel::where('id', $item['id'])->find();
+
+ if ($group) {
+ $group->save($data);
+ } else {
+ DeviceGroupModel::create($data);
+ }
+ }
+
+ /**
+ * 生成二维码图片(base64格式)
+ * @param string $data 二维码数据
+ * @return string base64编码的图片
+ */
+ private function generateQrCodeImage($data)
+ {
+ // 使用endroid/qr-code 2.5版本生成二维码
+ $qrCode = new QrCode($data);
+ $qrCode->setSize(300);
+ $qrCode->setMargin(10);
+ $qrCode->setWriterByName('png');
+ $qrCode->setEncoding('UTF-8');
+
+ // 使用枚举常量而不是字符串
+ $qrCode->setErrorCorrectionLevel(ErrorCorrectionLevel::HIGH);
+
+ // 直接获取base64内容
+ $base64 = 'data:image/png;base64,' . base64_encode($qrCode->writeString());
+
+ return $base64;
+ }
+}
\ No newline at end of file
diff --git a/application/api/controller/FriendTaskController.php b/application/api/controller/FriendTaskController.php
new file mode 100644
index 0000000..893a810
--- /dev/null
+++ b/application/api/controller/FriendTaskController.php
@@ -0,0 +1,192 @@
+authorization;
+ if (empty($authorization)) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'缺少授权信息']);
+ }else{
+ return errorJson('缺少授权信息');
+ }
+ }
+
+ try {
+ // 构建请求参数
+ $params = [
+ 'keyword' => $this->request->param('keyword', ''),
+ 'status' => $this->request->param('status', ''),
+ 'pageIndex' => !empty($pageIndex) ? $pageIndex : $this->request->param('pageIndex', 0),
+ 'pageSize' => !empty($pageSize) ? $pageSize : $this->request->param('pageSize', 20),
+ ];
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求获取添加好友记录列表
+ $result = requestCurl($this->baseUrl . 'api/AddFriendByPhoneTask/list', $params, 'GET', $header,'json');
+ $response = handleApiResponse($result);
+
+
+ // 保存数据到数据库
+ if (!empty($response['results'])) {
+ foreach ($response['results'] as $item) {
+ $this->saveFriendTask($item);
+ }
+ }
+ if($isInner){
+ return json_encode(['code'=>200,'msg'=>'获取添加好友记录列表成功','data'=>$response]);
+ }else{
+ return successJson($response);
+ }
+ } catch (\Exception $e) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'获取添加好友记录列表失败:' . $e->getMessage()]);
+ }else{
+ return errorJson('获取添加好友记录列表失败:' . $e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * 添加好友任务
+ * @return \think\response\Json
+ */
+ public function addFriendTask($data = [])
+ {
+ // 获取授权token
+ $authorization =$this->authorization;
+ if (empty($authorization)) {
+ return json_encode(['code'=>500,'msg'=>'缺少授权信息']);
+ }
+
+ try {
+ // 获取请求参数
+ $phone = !empty($data['phone']) ? $data['phone'] : '';
+ $message = !empty($data['message']) ? $data['message'] : '';
+ $remark = !empty($data['remark']) ? $data['remark'] : '';
+ $labels = !empty($data['labels']) ? $data['labels'] : '';
+ $wechatAccountId = !empty($data['wechatAccountId']) ? $data['wechatAccountId'] : '';
+
+ // 参数验证
+ if (empty($phone)) {
+ return json_encode(['code'=>500,'msg'=>'手机号不能为空']);
+ }
+
+ if (empty($wechatAccountId)) {
+ return json_encode(['code'=>500,'msg'=>'微信号不能为空']);
+ }
+
+ // 构建请求参数
+ $params = [
+ 'phone' => $phone,
+ 'message' => $message,
+ 'remark' => $remark,
+ 'labels' => is_array($labels) ? $labels : [$labels],
+ 'wechatAccountId' => (int)$wechatAccountId
+ ];
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求添加好友任务
+ $result = requestCurl($this->baseUrl . 'api/AddFriendByPhoneTask/add', $params, 'POST', $header, 'json');
+
+ // 处理响应
+ return json_encode(['code'=>200,'msg'=>'添加好友任务创建成功']);
+ } catch (\Exception $e) {
+ return json_encode(['code'=>500,'msg'=> '添加好友任务失败:' . $e->getMessage()]);
+ }
+ }
+
+ /************************ 私有辅助方法 ************************/
+
+ /**
+ * 保存添加好友记录到数据库
+ * @param array $item 添加好友记录数据
+ */
+ private function saveFriendTask($item)
+ {
+ // 将日期时间字符串转换为时间戳
+ $createTime = isset($item['createTime']) ? strtotime($item['createTime']) : null;
+
+ $data = [
+ 'id' => $item['id'],
+ 'tenantId' => $item['tenantId'],
+ 'operatorAccountId' => $item['operatorAccountId'],
+ 'status' => $item['status'],
+ 'phone' => $item['phone'],
+ 'msgContent' => $item['msgContent'],
+ 'wechatAccountId' => $item['wechatAccountId'],
+ 'createTime' => $createTime,
+ 'remark' => $item['remark'],
+ 'extra' => $item['extra'],
+ 'labels' => $item['labels'],
+ 'from' => $item['from'],
+ 'alias' => $item['alias'],
+ 'wechatId' => $item['wechatId'],
+ 'wechatAvatar' => $item['wechatAvatar'],
+ 'wechatNickname' => $item['wechatNickname'],
+ 'accountNickname' => $item['accountNickname'],
+ 'accountRealName' => $item['accountRealName'],
+ 'accountUsername' => $item['accountUsername']
+ ];
+
+ // 使用taskId作为唯一性判断
+ $task = FriendTaskModel::where('id', $item['id'])->find();
+ if ($task) {
+ $task->save($data);
+ } else {
+ FriendTaskModel::create($data);
+ }
+
+ //创建非法记录
+ if ($item['status'] == 2){
+ $data = [
+ 'level' => 2,
+ 'taskId' => $item['id'],
+ 'reason' => '',
+ 'memo' => '',
+ 'wechatId' => $item['wechatId'],
+ 'companyId' => '',
+ 'restrictTime' => time(),
+ 'recoveryTime' => time() + 3600 * 72,
+ ];
+ if (strpos('操作过于频繁', $item['extra']) !== false){
+ $data['reason'] = '频繁添加好友';
+ $data['memo'] = '操作过于频繁,请稍后再试';
+ }
+
+ if (strpos('当前账号存在安全风险', $item['extra']) !== false){
+ $data['reason'] = '账号风险';
+ $data['memo'] = '当前账号存在安全风险,需先到「微信团队」进行安全验证后才能继续使用当前功能';
+ }
+ $res = WechatRestricts::where('taskId', $item['id'])->find();
+ if (empty($res)) {
+ WechatRestricts::create($data);
+ }
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/application/api/controller/MessageController.php b/application/api/controller/MessageController.php
new file mode 100644
index 0000000..daedae1
--- /dev/null
+++ b/application/api/controller/MessageController.php
@@ -0,0 +1,584 @@
+request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'缺少授权信息']);
+ }else{
+ return errorJson('缺少授权信息');
+ }
+ }
+
+ $fromTime = $this->request->param('fromTime', date('Y-m-d 00:00:00', strtotime('-1 days')));
+ $toTime = $this->request->param('toTime', date('Y-m-d 23:59:59'));
+
+
+ try {
+ // 构建请求参数
+ $params = [
+ 'chatroomKeyword' => $this->request->param('chatroomKeyword', ''),
+ 'friendKeyword' => $this->request->param('friendKeyword', ''),
+ 'friendPhoneKeyword' => $this->request->param('friendPhoneKeyword', ''),
+ 'friendPinYinKeyword' => $this->request->param('friendPinYinKeyword', ''),
+ 'friendRegionKeyword' => $this->request->param('friendRegionKeyword', ''),
+ 'friendRemarkKeyword' => $this->request->param('friendRemarkKeyword', ''),
+ 'groupId' => $this->request->param('groupId', null),
+ 'kefuId' => $this->request->param('kefuId', null),
+ 'labels' => $this->request->param('labels', []),
+ 'msgFrom' => $fromTime,
+ 'msgKeyword' => $this->request->param('msgKeyword', ''),
+ 'msgTo' => $toTime,
+ 'msgType' => $this->request->param('msgType', ''),
+ 'pageIndex' => !empty($pageIndex) ? $pageIndex : input('pageIndex', 0),
+ 'pageSize' => !empty($pageSize) ? $pageSize : input('pageSize', 20),
+ 'reverse' => $this->request->param('reverse', false),
+ 'type' => $this->request->param('type', 'friend'),
+ 'wechatAccountIds' => $this->request->param('wechatAccountIds', [])
+ ];
+
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求获取好友列表
+ $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;
+ $to = strtotime($toTime) * 1000;
+
+
+ foreach ($response['results'] as &$friend) {
+ // 构建获取消息的参数
+ $messageParams = [
+ 'keyword' => '',
+ 'msgType' => '',
+ 'accountId' => '',
+ 'count' => 20,
+ 'messageId' => '',
+ 'olderData' => true,
+ 'wechatAccountId' => $friend['wechatAccountId'],
+ 'wechatFriendId' => $friend['wechatFriendId'],
+ 'from' => $from,
+ 'to' => $to,
+ 'searchFrom' => 'admin'
+ ];
+
+ // 调用获取消息的接口
+ $messageResult = requestCurl($this->baseUrl . 'api/FriendMessage/searchMessage', $messageParams, 'GET', $header, 'json');
+ $messageResponse = handleApiResponse($messageResult);
+ // 保存消息到数据库
+ if (!empty($messageResponse)) {
+ foreach ($messageResponse as $item) {
+ $this->saveMessage($item);
+ }
+ }
+
+ // 将消息列表添加到好友数据中
+ $friend['messages'] = $messageResponse ?? [];
+ }
+ unset($friend);
+ }
+ if($isInner){
+ return json_encode(['code'=>200,'msg'=>'获取好友列表成功','data'=>$response]);
+ }else{
+ return successJson($response);
+ }
+ } catch (\Exception $e) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'获取好友列表失败:' . $e->getMessage()]);
+ }else{
+ return errorJson('获取好友列表失败:' . $e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * 用户聊天记录
+ * @return \think\response\Json
+ */
+ public function getMessageList()
+ {
+ // 获取授权token
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ return errorJson('缺少授权信息');
+ }
+
+ try {
+ // 构建请求参数
+ $params = [
+ 'keyword' => $this->request->param('keyword', ''),
+ 'msgType' => $this->request->param('msgType', ''),
+ 'accountId' => $this->request->param('accountId', ''),
+ 'count' => $this->request->param('count', 100),
+ 'messageId' => $this->request->param('messageId', ''),
+ 'olderData' => $this->request->param('olderData', true),
+ 'wechatAccountId' => $this->request->param('wechatAccountId', ''),
+ 'wechatFriendId' => $this->request->param('wechatFriendId', ''),
+ 'from' => $this->request->param('from', ''),
+ 'to' => $this->request->param('to', ''),
+ 'searchFrom' => $this->request->param('searchFrom', 'admin')
+ ];
+
+ // 参数验证
+ if (empty($params['wechatAccountId'])) {
+ return errorJson('微信账号ID不能为空');
+ }
+ if (empty($params['wechatFriendId'])) {
+ return errorJson('好友ID不能为空');
+ }
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求获取聊天记录
+ $result = requestCurl($this->baseUrl . 'api/FriendMessage/searchMessage', $params, 'GET', $header, 'json');
+ $response = handleApiResponse($result);
+
+ // 保存数据到数据库
+ if (!empty($response)) {
+ foreach ($response as $item) {
+ $this->saveMessage($item);
+ }
+ }
+
+ return successJson($response);
+ } catch (\Exception $e) {
+ return errorJson('获取聊天记录失败:' . $e->getMessage());
+ }
+ }
+
+ /************************ 群聊消息相关接口 ************************/
+
+ /**
+ * 获取微信群聊列表
+ * @return \think\response\Json
+ */
+ public function getChatroomList($pageIndex = '',$pageSize = '',$isInner = false)
+ {
+ // 获取授权token
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'缺少授权信息']);
+ }else{
+ return errorJson('缺少授权信息');
+ }
+ }
+
+ $fromTime = $this->request->param('fromTime', date('Y-m-d 00:00:00', strtotime('-1 days')));
+ $toTime = $this->request->param('toTime', date('Y-m-d 23:59:59'));
+
+
+ try {
+ // 构建请求参数
+ $params = [
+ 'chatroomKeyword' => $this->request->param('chatroomKeyword', ''),
+ 'friendKeyword' => $this->request->param('friendKeyword', ''),
+ 'friendInKeyword' => $this->request->param('friendInKeyword', ''),
+ 'friendInTimeKeyword' => $this->request->param('friendInTimeKeyword', ''),
+ 'friendOutKeyword' => $this->request->param('friendOutKeyword', ''),
+ 'friendRemarkKeyword' => $this->request->param('friendRemarkKeyword', ''),
+ 'groupId' => $this->request->param('groupId', null),
+ 'kefuId' => $this->request->param('kefuId', null),
+ 'labels' => $this->request->param('labels', []),
+ 'msgFrom' => $fromTime,
+ 'msgKeyword' => $this->request->param('msgKeyword', ''),
+ 'msgTo' => $toTime,
+ 'msgType' => $this->request->param('msgType', ''),
+ 'pageIndex' => $this->request->param('pageIndex', 0),
+ 'pageSize' => $this->request->param('pageSize', 100),
+ 'reverse' => $this->request->param('reverse', false),
+ 'type' => $this->request->param('type', 'chatroom'),
+ 'wechatAccountIds' => $this->request->param('wechatAccountIds', [])
+ ];
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求获取群聊列表
+ $result = requestCurl($this->baseUrl . 'api/WechatChatroom/listWechatChatroomForMsgPagination', $params, 'POST', $header, 'json');
+ $response = handleApiResponse($result);
+
+ // 获取同步消息标志
+ $syncMessages = $this->request->param('syncMessages', true);
+
+ // 如果需要同步消息,则获取每个群的消息
+ if ($syncMessages && !empty($response)) {
+ $from = strtotime($fromTime) * 1000;
+ $to = strtotime($toTime) * 1000;
+ foreach ($response['results'] as &$chatroom) {
+
+ // 构建获取消息的参数
+ $messageParams = [
+ 'keyword' => '',
+ 'msgType' =>'',
+ 'accountId' => '',
+ 'count' => 20,
+ 'messageId' => '',
+ 'olderData' => true,
+ 'wechatId' => '',
+ 'wechatAccountId' => $chatroom['wechatAccountId'],
+ 'wechatChatroomId' => $chatroom['wechatChatroomId'],
+ 'from' => $from,
+ 'to' => $to,
+ 'searchFrom' => 'admin'
+ ];
+
+ // 调用获取消息的接口
+ $messageResult = requestCurl($this->baseUrl . 'api/ChatroomMessage/searchMessage', $messageParams, 'GET', $header, 'json');
+ $messageResponse = handleApiResponse($messageResult);
+
+ // 保存消息到数据库
+ if (!empty($messageResponse)) {
+ foreach ($messageResponse as $item) {
+ $this->saveChatroomMessage($item);
+ }
+ }
+
+ // 将消息列表添加到群聊数据中
+ $chatroom['messages'] = $messageResponse ?? [];
+ }
+ unset($chatroom);
+ }
+ if($isInner){
+ return json_encode(['code'=>200,'msg'=>'获取群聊列表成功','data'=>$response]);
+ }else{
+ return successJson($response);
+ }
+ } catch (\Exception $e) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'获取群聊列表失败:' . $e->getMessage()]);
+ }else{
+ return errorJson('获取群聊列表失败:' . $e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * 获取群聊消息列表
+ * @return \think\response\Json
+ */
+ public function getChatroomMessages()
+ {
+ // 获取授权token
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ return errorJson('缺少授权信息');
+ }
+
+ try {
+ // 构建请求参数
+ $params = [
+ 'keyword' => $this->request->param('keyword', ''),
+ 'msgType' => $this->request->param('msgType', ''),
+ 'accountId' => $this->request->param('accountId', ''),
+ 'count' => $this->request->param('count', 100),
+ 'messageId' => $this->request->param('messageId', ''),
+ 'olderData' => $this->request->param('olderData', true),
+ 'wechatId' => $this->request->param('wechatId', ''),
+ 'wechatAccountId' => $this->request->param('wechatAccountId', ''),
+ 'wechatChatroomId' => $this->request->param('wechatChatroomId', ''),
+ 'from' => $this->request->param('from', strtotime(date('Y-m-d 00:00:00', strtotime('-1 days')))),
+ 'to' => $this->request->param('to', strtotime(date('Y-m-d 00:00:00'))),
+ 'searchFrom' => $this->request->param('searchFrom', 'admin')
+ ];
+
+ // 参数验证
+ if (empty($params['wechatAccountId'])) {
+ return errorJson('微信账号ID不能为空');
+ }
+ if (empty($params['wechatChatroomId'])) {
+ return errorJson('群聊ID不能为空');
+ }
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求获取群聊消息
+ $result = requestCurl($this->baseUrl . 'api/ChatroomMessage/searchMessage', $params, 'GET', $header, 'json');
+ $response = handleApiResponse($result);
+
+ // 保存数据到数据库
+ if (!empty($response)) {
+ foreach ($response as $item) {
+ $res = $this->saveChatroomMessage($item);
+ if(!$res){
+ return errorJson('保存群聊消息失败');
+ }
+ }
+ }
+
+ return successJson($response);
+ } catch (\Exception $e) {
+ return errorJson('获取群聊消息失败:' . $e->getMessage());
+ }
+ }
+
+ /************************ 私有辅助方法 ************************/
+
+ /**
+ * 保存消息记录到数据库
+ * @param array $item 消息记录数据
+ */
+ public function saveMessage($item)
+ {
+ // 检查消息是否已存在
+ $exists = WechatMessageModel::where('id', $item['id']) ->find();
+
+ if (!empty($exists) && $exists['sendStatus'] == 0){
+ return true;
+ }
+
+
+ // 将毫秒时间戳转换为秒级时间戳
+ $createTime = isset($item['createTime']) ? strtotime($item['createTime']) : null;
+ $deleteTime = !empty($item['isDeleted']) ? strtotime($item['deleteTime']) : null;
+ $wechatTime = isset($item['wechatTime']) ? floor($item['wechatTime'] / 1000) : null;
+
+ $data = [
+ 'id' => $item['id'],
+ 'type' => 1,
+ 'accountId' => $item['accountId'],
+ 'content' => $item['content'],
+ 'createTime' => $createTime,
+ 'deleteTime' => $deleteTime,
+ 'isDeleted' => $item['isDeleted'] ?? false,
+ 'isSend' => $item['isSend'] ?? true,
+ 'msgId' => $item['msgId'],
+ 'msgSubType' => $item['msgSubType'] ?? 0,
+ 'msgSvrId' => $item['msgSvrId'] ?? '',
+ 'msgType' => $item['msgType'],
+ 'origin' => $item['origin'] ?? 0,
+ 'recallId' => $item['recallId'] ?? false,
+ 'sendStatus' => $item['sendStatus'] ?? 0,
+ 'synergyAccountId' => $item['synergyAccountId'] ?? 0,
+ 'tenantId' => $item['tenantId'],
+ 'wechatAccountId' => $item['wechatAccountId'],
+ 'wechatFriendId' => $item['wechatFriendId'],
+ '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{
+ //优先分配在线客服
+ $friend = Db::table('s2_wechat_friend')->where('id',$item['wechatFriendId'])->find();
+ if (!empty($friend)){
+ $accountId = $item['accountId'];
+ $accountData = Db::table('s2_company_account')->where('id',$accountId)->find();
+ if (!empty($accountData)){
+ $account = new AccountController();
+ $account->getlist(['pageIndex' => 0,'pageSize' => 100,'departmentId' => $accountData['departmentId']]);
+ $accountIds = Db::table('s2_company_account')->where(['departmentId' => $accountData['departmentId'],'alive' => 1])->column('id');
+ if (!empty($accountIds)){
+ if (!in_array($friend['accountId'],$accountIds)){
+ // 执行切换好友命令
+ $randomKey = array_rand($accountIds, 1);
+ $toAccountId = $accountIds[$randomKey];
+ $toAccountData = Db::table('s2_company_account')->where('id',$toAccountId)->find();
+ $automaticAssign = new AutomaticAssign();
+ $automaticAssign->allotWechatFriend([
+ 'wechatFriendId' => $friend['id'],
+ 'toAccountId' => $toAccountId
+ ], true);
+ Db::table('s2_wechat_friend')
+ ->where('id',$friend['id'])
+ ->update([
+ 'accountId' => $toAccountId,
+ 'accountUserName' => $toAccountData['userName'],
+ 'accountRealName' => $toAccountData['realName'],
+ 'accountNickname' => $toAccountData['nickname'],
+ ]);
+ }
+ }
+
+ }
+ }
+ }
+
+ $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])){
+ $friend = Db::name('wechat_friendship')->where('id',$item['wechatFriendId'])->find();
+ if (!empty($friend)){
+ $trafficPoolId = Db::name('traffic_pool')->where('identifier',$friend['wechatId'])->value('id');
+ if (!empty($trafficPoolId)){
+ $data = [
+ 'type' => 4,
+ 'companyId' => $friend['companyId'],
+ 'trafficPoolId' => $trafficPoolId,
+ 'source' => 0,
+ 'uniqueId' => $id,
+ 'sourceData' => json_encode([]),
+ 'remark' => '用户发送了消息',
+ 'createTime' => time(),
+ 'updateTime' => time()
+ ];
+ Db::name('user_portrait')->insert($data);
+
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 保存群聊消息记录到数据库
+ * @param array $item 消息记录数据
+ * @return bool 是否保存成功
+ */
+ public function saveChatroomMessage($item)
+ {
+ // 检查消息是否已存在
+ $exists = WechatMessageModel::where('id', $item['id'])->find();
+
+ if (!empty($exists) && $exists['sendStatus'] == 0){
+ return true;
+ }
+
+ // 处理发送者信息
+ $sender = $item['sender'] ?? [];
+
+ // 处理消息内容,提取发送者ID和消息内容
+ $originalContent = $item['content'] ?? '';
+ $processedResult = $this->processMessageContent($originalContent);
+ $senderId = $processedResult['senderId'];
+ $processedContent = $processedResult['content'];
+
+ // 将毫秒时间戳转换为秒级时间戳
+ $createTime = isset($item['createTime']) ? strtotime($item['createTime']) : null;
+ $deleteTime = !empty($item['isDeleted']) ? strtotime($item['deleteTime']) : null;
+ $wechatTime = isset($item['wechatTime']) ? floor($item['wechatTime'] / 1000) : null;
+
+ $data = [
+ 'id' => $item['id'],
+ 'type' => 2,
+ 'wechatChatroomId' => $item['wechatChatroomId'],
+ // sender信息,添加sender前缀
+ 'senderNickname' => $sender['nickname'] ?? '',
+ 'senderWechatId' => $sender['wechatId'] ?? $senderId, // 使用提取的发送者ID作为备选
+ 'senderIsAdmin' => $sender['isAdmin'] ?? false,
+ 'senderIsDeleted' => $sender['isDeleted'] ?? false,
+ 'senderChatroomNickname' => $sender['chatroomNickname'] ?? '',
+ 'senderWechatAccountId' => $sender['wechatAccountId'] ?? '',
+ // 其他字段
+ 'wechatAccountId' => $item['wechatAccountId'],
+ 'tenantId' => $item['tenantId'],
+ 'accountId' => $item['accountId'],
+ 'synergyAccountId' => $item['synergyAccountId'] ?? 0,
+ 'content' => $processedContent, // 使用处理后的内容
+ 'originalContent' => $originalContent, // 保存原始内容
+ 'msgType' => $item['msgType'],
+ 'msgSubType' => $item['msgSubType'] ?? 0,
+ 'msgSvrId' => $item['msgSvrId'] ?? '',
+ 'isSend' => $item['isSend'] ?? true,
+ 'createTime' => $createTime,
+ 'isDeleted' => $item['isDeleted'] ?? false,
+ 'deleteTime' => $deleteTime,
+ 'sendStatus' => $item['sendStatus'] ?? 0,
+ 'wechatTime' => $wechatTime,
+ 'origin' => $item['origin'] ?? 0,
+ 'msgId' => $item['msgId'],
+ 'recallId' => $item['recallId'] ?? false
+ ];
+
+ // 创建新记录
+ try {
+ if(empty($exists)){
+ WechatMessageModel::create($data);
+ }else{
+ unset($data['id']);
+ $exists->save($data);
+ }
+ return true;
+ } catch (\Exception $e) {
+ return false;
+ }
+ }
+
+ /**
+ * 处理消息内容,提取发送者ID和消息内容
+ * @param string $content 原始消息内容
+ * @return array 包含senderId和content的数组
+ */
+ private function processMessageContent($content)
+ {
+ if (empty($content)) {
+ return [
+ 'senderId' => '',
+ 'content' => ''
+ ];
+ }
+
+ // 处理消息格式:wxid_vr2qafb1vg0d22:\n安德玛儿童
+ if (preg_match('/^([^:]+):\n(.+)$/s', $content, $matches)) {
+ $senderId = trim($matches[1]);
+ $messageContent = trim($matches[2]);
+
+ // 检查消息内容是否为JSON格式
+ if (substr($messageContent, 0, 1) === '{' && substr($messageContent, -1) === '}') {
+ try {
+ // 尝试解析JSON
+ $jsonData = json_decode($messageContent, true);
+ if (json_last_error() == JSON_ERROR_NONE && isset($jsonData['text'])) {
+ // 如果是合法的JSON且包含text字段,则提取text字段作为内容
+ $messageContent = $jsonData['text'];
+ }
+ } catch (\Exception $e) {
+ // JSON解析出错,保持原内容不变
+ }
+ }
+
+ return [
+ 'senderId' => $senderId,
+ 'content' => $messageContent
+ ];
+ }
+
+ // 如果没有匹配到格式,则返回原始内容
+ return [
+ 'senderId' => '',
+ 'content' => $content
+ ];
+ }
+}
\ No newline at end of file
diff --git a/application/api/controller/MomentsController.php b/application/api/controller/MomentsController.php
new file mode 100644
index 0000000..812ec4b
--- /dev/null
+++ b/application/api/controller/MomentsController.php
@@ -0,0 +1,153 @@
+authorization;
+ if (empty($authorization)) {
+ return json_encode(['msg' => '缺少授权信息','code' => 400]);
+ }
+
+ try {
+ // 获取请求参数
+ $text = $data['text'] ?? ''; // 朋友圈文本内容
+ $picUrlList = $data['picUrlList'] ?? []; // 图片URL列表
+ $videoUrl = $data['videoUrl'] ?? ''; // 视频URL
+ $immediately = $data['immediately'] ?? true; // 是否立即发布
+ $timingTime = $data['timingTime'] ?? ''; // 定时发布时间
+ $beginTime = $data['beginTime'] ?? ''; // 开始时间
+ $endTime = $data['endTime'] ?? ''; // 结束时间
+ $isUseLocation = $data['isUseLocation'] ?? false; // 是否使用位置信息
+ $poiName = $data['poiName'] ?? ''; // 位置名称
+ $poiAddress = $data['poiAddress'] ?? ''; // 位置地址
+ $lat = $data['lat'] ?? 0; // 纬度
+ $lng = $data['lng'] ?? 0; // 经度
+ $momentContentType = $data['momentContentType'] ?? 1; // 朋友圈内容类型
+ $publicMode = $data['publicMode'] ?? 0; // 发布模式
+ $altList = $data['altList'] ?? ''; // 替代列表
+ $link = $data['link'] ?? []; // 链接信息
+ $jobPublishWechatMomentsItems = $data['jobPublishWechatMomentsItems'] ?? []; // 发布账号和评论信息
+
+ // 必填参数验证
+ if (empty($jobPublishWechatMomentsItems) || !is_array($jobPublishWechatMomentsItems)) {
+ return json_encode(['msg' => '至少需要选择一个发布账号','code' => 400]);
+ }
+
+ // 根据朋友圈类型验证必填字段
+ if ($momentContentType == 1 && empty($text)) { // 纯文本
+ return json_encode(['msg' => '朋友圈内容不能为空','code' => 400]);
+ } else if ($momentContentType == 2 && (empty($picUrlList) || empty($text))) { // 图片+文字
+ return json_encode(['msg' => '朋友圈内容和图片不能为空','code' => 400]);
+ } else if ($momentContentType == 3 && (empty($videoUrl) || empty($text))) { // 视频+文字
+ return json_encode(['msg' => '朋友圈内容和视频不能为空','code' => 400]);
+ } else if ($momentContentType == 4 && (empty($link) || empty($text))) { // 链接+文字
+ return json_encode(['msg' => '朋友圈内容和链接不能为空','code' => 400]);
+ }
+
+ // 构建请求参数
+ $params = [
+ 'text' => $text,
+ 'picUrlList' => $picUrlList,
+ 'videoUrl' => $videoUrl,
+ 'immediately' => $immediately,
+ 'timingTime' => $timingTime,
+ 'beginTime' => $beginTime,
+ 'endTime' => $endTime,
+ 'isUseLocation' => $isUseLocation,
+ 'poiName' => $poiName,
+ 'poiAddress' => $poiAddress,
+ 'lat' => $lat,
+ 'lng' => $lng,
+ 'momentContentType' => (int)$momentContentType,
+ 'publicMode' => (int)$publicMode,
+ 'altList' => $altList,
+ 'link' => $link,
+ 'jobPublishWechatMomentsItems' => $jobPublishWechatMomentsItems
+ ];
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求发布朋友圈
+ $result = requestCurl($this->baseUrl . 'api/JobPublishWechatMoments/addJob', $params, 'POST', $header, 'json');
+ // 处理响应
+ if (empty($result)) {
+ return json_encode(['msg' => '朋友圈任务创建成功','code' => 200]);
+ } else {
+ // 如果返回的是错误信息
+ return json_encode(['msg' => $result,'code' => 400]);
+ }
+ } catch (\Exception $e) {
+ return json_encode(['msg' => '发布朋友圈失败','code' => 400]);
+ }
+ }
+
+ /************************ 朋友圈任务管理相关接口 ************************/
+
+ /**
+ * 获取朋友圈任务列表
+ * @return \think\response\Json
+ */
+ public function getList()
+ {
+ // 获取授权token
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ return errorJson('缺少授权信息');
+ }
+
+ try {
+ // 获取请求参数
+ $keyword = $this->request->param('keyword', ''); // 关键词搜索
+ $jobStatus = $this->request->param('jobStatus', ''); // 任务状态筛选
+ $contentType = $this->request->param('contentType', ''); // 内容类型筛选
+ $only = $this->request->param('only', 'false'); // 是否只查看自己的
+ $pageIndex = $this->request->param('pageIndex', 0); // 当前页码
+ $pageSize = $this->request->param('pageSize', 10); // 每页数量
+ $from = $this->request->param('from', ''); // 开始日期
+ $to = $this->request->param('to', ''); // 结束日期
+
+ // 构建请求参数
+ $params = [
+ 'keyword' => $keyword,
+ 'jobStatus' => $jobStatus,
+ 'contentType' => $contentType,
+ 'only' => $only,
+ 'pageIndex' => (int)$pageIndex,
+ 'pageSize' => (int)$pageSize
+ ];
+
+ // 添加日期筛选条件(如果有)
+ if (!empty($from)) {
+ $params['from'] = $from;
+ }
+ if (!empty($to)) {
+ $params['to'] = $to;
+ }
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求获取朋友圈任务列表
+ $result = requestCurl($this->baseUrl . 'api/JobPublishWechatMoments/listPagination', $params, 'GET', $header, 'json');
+ $response = handleApiResponse($result);
+
+ return successJson($response);
+ } catch (\Exception $e) {
+ return errorJson('获取朋友圈任务列表失败:' . $e->getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/api/controller/StatsController.php b/application/api/controller/StatsController.php
new file mode 100644
index 0000000..8ae0bb1
--- /dev/null
+++ b/application/api/controller/StatsController.php
@@ -0,0 +1,75 @@
+request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ return errorJson('缺少授权信息');
+ }
+
+ $headerData = ['client:' . self::CLIENT_TYPE];
+ $header = setHeader($headerData, $authorization, 'plain');
+
+ try {
+ $result = requestCurl($this->baseUrl . '/api/DashBoard/ListHomePageStatistics', ['refresh' => 10000], 'GET', $header);
+ return successJson($result);
+ } catch (\Exception $e) {
+ return errorJson('获取基础数据失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 好友统计
+ * @return \think\response\Json
+ */
+ public function FansStatistics(){
+ /* 参数说明
+ lidu 数据搜索类型 0 小时 1 天 2月
+ from to 时间 当lidu为 0时(2025-03-12 09:54:42) 当lidu为 1时(2025-03-12) 当lidu为 2时(2025-03)
+ */
+
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ $lidu = trim($this->request->param('lidu', ''));
+ $from = trim($this->request->param('from', ''));
+ $to = trim($this->request->param('to', ''));
+
+ if (empty($authorization)) {
+ return errorJson('缺少授权信息');
+ }
+
+ $params = [
+ 'lidu' => $lidu,
+ 'from' => $from,
+ 'to' => $to,
+ ];
+
+ $headerData = ['client:' . self::CLIENT_TYPE];
+ $header = setHeader($headerData, $authorization, 'plain');
+
+ try {
+ $result = requestCurl($this->baseUrl . 'api/DashBoard/listStatisticsCountDTOByCreateTimeAsync', $params, 'GET', $header);
+ return successJson($result);
+ } catch (\Exception $e) {
+ return errorJson('获取粉丝统计数据失败:' . $e->getMessage());
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/application/api/controller/UserController.php b/application/api/controller/UserController.php
new file mode 100644
index 0000000..ab4ce13
--- /dev/null
+++ b/application/api/controller/UserController.php
@@ -0,0 +1,365 @@
+validateLoginParams();
+ if (!is_array($params)) {
+ return $params;
+ }
+
+ // 验证账号是否存在
+ $existingAccount = CompanyAccountModel::where('userName', $params['username'])->find();
+ if (empty($existingAccount)) {
+ // 记录登录失败日志
+ recordUserLog(0, $params['username'], 'LOGIN', '账号不存在', $params, 500, '账号不存在');
+ return errorJson('账号不存在');
+ }
+
+ // 获取验证码会话ID和用户输入的验证码
+ $verifySessionId = $this->request->param('verifySessionId', '');
+ $verifyCode = $this->request->param('verifyCode', '');
+
+ // 设置请求头
+ $headerData = ['client:' . self::CLIENT_TYPE];
+
+ // 如果存在验证码信息,添加到请求头
+ if (!empty($verifySessionId) && !empty($verifyCode)) {
+ $headerData[] = 'verifysessionid:' . $verifySessionId;
+ $headerData[] = 'verifycode:' . $verifyCode;
+ }
+
+ $header = setHeader($headerData, '', 'plain');
+
+ try {
+ // 请求登录接口
+ $result = requestCurl($this->baseUrl . 'token', $params, 'POST', $header);
+ $result_array = handleApiResponse($result);
+
+ if (is_array($result_array) && isset($result_array['error'])) {
+ // 记录登录失败日志
+ recordUserLog(0, $params['username'], 'LOGIN', '登录失败', $params, 500, $result_array['error_description']);
+ return errorJson($result_array['error_description']);
+ }
+
+ // 获取客户端IP地址
+ $ip = $this->request->ip();
+
+ // 登录成功,更新密码信息和登录信息
+ $updateData = [
+ 'passwordMd5' => md5($params['password']),
+ 'passwordLocal' => localEncrypt($params['password']),
+ 'lastLoginIp' => $ip,
+ 'lastLoginTime' => time()
+ ];
+
+ // 更新密码信息
+ CompanyAccountModel::where('userName', $params['username'])->update($updateData);
+
+ // 记录登录成功日志
+ recordUserLog($existingAccount['id'], $params['username'], 'LOGIN', '登录成功', [], 200, '登录成功');
+
+ return successJson($result_array);
+ } catch (\Exception $e) {
+ // 记录登录异常日志
+ recordUserLog(0, $params['username'], 'LOGIN', '登录请求失败', $params, 500, $e->getMessage());
+ return errorJson('登录请求失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 获取新的token
+ * @return \think\response\Json
+ */
+ public function getNewToken()
+ {
+ $grant_type = $this->request->param('grant_type', 'refresh_token');
+ $refresh_token = $this->request->param('refresh_token', '');
+ $authorization = $this->request->header('authorization', $this->authorization);
+
+ if (empty($grant_type) || empty($authorization)) {
+ return errorJson('参数错误');
+ }
+
+ $params = [
+ 'grant_type' => $grant_type,
+ 'refresh_token' => $refresh_token,
+ ];
+
+
+
+ $headerData = ['client:' . self::CLIENT_TYPE];
+ $header = setHeader($headerData, $authorization, 'system');
+
+ try {
+ $result = requestCurl($this->baseUrl . 'token', $params, 'POST', $header);
+ $result_array = handleApiResponse($result);
+
+ if (is_array($result_array) && isset($result_array['error'])) {
+ recordUserLog(0, '', 'REFRESH_TOKEN', '刷新token失败', $params, 500, $result_array['error_description']);
+ return errorJson($result_array['error_description']);
+ }
+
+ recordUserLog(0, '', 'REFRESH_TOKEN', '刷新token成功', $params, 200, '刷新成功');
+ return successJson($result_array);
+ } catch (\Exception $e) {
+ recordUserLog(0, '', 'REFRESH_TOKEN', '刷新token异常', $params, 500, $e->getMessage());
+ return errorJson('获取新token失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 获取商户基本信息
+ * @return \think\response\Json
+ */
+ public function getAccountInfo()
+ {
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ return errorJson('缺少授权信息');
+ }
+
+ $headerData = ['client:' . self::CLIENT_TYPE];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ try {
+ $result = requestCurl($this->baseUrl . 'api/Account/self', [], 'GET', $header,'json');
+ $response = handleApiResponse($result);
+ if (!empty($response['account'])) {
+ $accountData = $response['account'];
+
+ // 准备数据库字段映射,保持驼峰命名
+ $dbData = [
+ 'tenantId' => $accountData['id'],
+ 'realName' => $accountData['realName'],
+ 'nickname' => $accountData['nickname'],
+ 'memo' => $accountData['memo'],
+ 'avatar' => $accountData['avatar'],
+ 'userName' => $accountData['userName'],
+ 'secret' => $accountData['secret'],
+ 'accountType' => $accountData['accountType'],
+ 'companyId' => $accountData['departmentId'],
+ 'useGoogleSecretKey' => $accountData['useGoogleSecretKey'],
+ 'hasVerifyGoogleSecret' => $accountData['hasVerifyGoogleSecret'],
+ 'updateTime' => time()
+ ];
+
+
+ // 查找是否存在该账户
+ $existingAccount = CompanyAccountModel::where('userName', $accountData['userName'])->find();
+ if ($existingAccount) {
+ // 更新现有记录
+ CompanyAccountModel::where('userName', $accountData['userName'])->update($dbData);
+ } else {
+ // 创建新记录
+ $dbData['createTime'] = time();
+ CompanyAccountModel::create($dbData);
+ }
+ return successJson($response['account']);
+ }else{
+ return successJson($response);
+ }
+
+
+ } catch (\Exception $e) {
+ recordUserLog(0, '', 'GET_ACCOUNT_INFO', '获取账户信息异常', [], 500, $e->getMessage());
+ return errorJson('获取账户信息失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 修改密码
+ * @return \think\response\Json
+ */
+ public function modifyPwd($data = [])
+ {
+
+ if (empty($data)) {
+ return json_encode(['code' => 400,'msg' => '参数缺失']);
+ }
+
+ if (!isset($data['id']) || !isset($data['pwd'])) {
+ return json_encode(['code' => 401,'msg' => '参数缺失']);
+ }
+ $authorization = $this->authorization;
+
+ if (empty($authorization)) {
+ return json_encode(['code' => 400,'msg' => '缺少授权信息']);
+ }
+
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+ $params = [
+ 'id' => $data['id'],
+ 'newPw' => $data['pwd'],
+ ];
+
+ try {
+ $result = requestCurl($this->baseUrl . 'api/Account/modifypw', $params, 'PUT', $header,'json');
+ $response = handleApiResponse($result);
+ if (empty($response)) {
+ return json_encode(['code' => 200,'msg' => '修改成功']);
+ }
+ return json_encode(['code' => 400,'msg' => $response]);
+ } catch (\Exception $e) {
+ return json_encode(['code' => 400,'msg' => '修改密码失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 登出
+ * @return \think\response\Json
+ */
+ public function logout()
+ {
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ return errorJson('缺少授权信息');
+ }
+
+ $headerData = ['client:' . self::CLIENT_TYPE];
+ $header = setHeader($headerData, $authorization, 'system');
+
+ try {
+ // 调用外部退出登录接口
+ $result = requestCurl($this->baseUrl . 'api/Account/SignOut', [], 'GET', $header);
+ return successJson([] , '退出成功');
+ } catch (\Exception $e) {
+ recordUserLog(0, '', 'LOGOUT', '退出登录异常', [], 500, $e->getMessage());
+ return errorJson('退出登录失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 获取验证码
+ * @return \think\response\Json
+ */
+ public function getVerifyCode($isJson = false)
+ {
+ $headerData = ['client:' . self::CLIENT_TYPE];
+ $header = setHeader($headerData, '', 'plain');
+
+ try {
+ $result = requestCurl($this->baseUrl . 'api/Account/getVerifyCode', [], 'GET', $header);
+ $response = handleApiResponse($result);
+
+ // 检查返回的数据格式
+ if (is_array($response)) {
+ // 如果verifyCodeImage和verifySessionId都不为null,返回它们
+ if (!empty($response['verifyCodeImage']) && !empty($response['verifySessionId'])) {
+ $returnData = [
+ 'verifyCodeImage' => $response['verifyCodeImage'],
+ 'verifySessionId' => $response['verifySessionId']
+ ];
+ return !empty($isJson) ? json_encode(['code' => 200,'data' => $returnData]) : successJson($returnData);
+ }
+ }
+
+ // 如果不是预期的格式,返回原始数据
+ return !empty($isJson) ? json_encode(['code' => 200,'msg' => '无需验证码','data' => ['verifyCodeImage' => '', 'verifySessionId' => '']]) : successJson(['verifyCodeImage' => '', 'verifySessionId' => ''],'无需验证码');
+ } catch (\Exception $e) {
+ $msg = '获取验证码失败'. $e->getMessage();
+ return !empty($isJson) ? json_encode(['code' => 400,'msg' => $msg]) : errorJson($msg);
+ }
+ }
+
+ /**
+ * 验证登录参数
+ * @return array|\think\response\Json
+ */
+ private function validateLoginParams()
+ {
+ $username = trim($this->request->param('username', ''));
+ $password = trim($this->request->param('password', ''));
+ $verifyCode = trim($this->request->param('verifyCode', ''));
+ $verifySessionId = trim($this->request->param('verifySessionId', ''));
+
+ if (empty($username) || empty($password)) {
+ return errorJson('用户名和密码不能为空');
+ }
+
+ // 验证密码格式
+ $passwordValidation = validateString($password, 'password',['max_length' => 20]);
+ if (!$passwordValidation['status']) {
+ return errorJson($passwordValidation['message']);
+ }
+
+ // 如果提供了验证码,验证格式
+ if (!empty($verifyCode)) {
+ if (empty($verifySessionId)) {
+ return errorJson('验证码会话ID不能为空');
+ }
+ // 验证码格式验证(假设是4位数字)
+ if (!preg_match('/^\d{4}$/', $verifyCode)) {
+ return errorJson('验证码格式不正确');
+ }
+ }
+
+ return [
+ 'grant_type' => 'password',
+ 'username' => $username,
+ 'password' => $password,
+ ];
+ }
+
+ /**
+ * 验证修改密码参数
+ * @return array|\think\response\Json
+ */
+ private function validateModifyPwdParams()
+ {
+ $cPw = trim($this->request->param('cPw', ''));
+ $newPw = trim($this->request->param('newPw', ''));
+ $oldPw = trim($this->request->param('oldPw', ''));
+
+ if (empty($cPw) || empty($newPw) || empty($oldPw)) {
+ return errorJson('密码参数不完整');
+ }
+
+ if ($newPw !== $cPw) {
+ return errorJson('两次输入的新密码不一致');
+ }
+
+ // 验证新密码格式
+ $passwordValidation = validateString($newPw, 'password');
+ if (!$passwordValidation['status']) {
+ return errorJson($passwordValidation['message']);
+ }
+
+ return [
+ 'cPw' => $cPw,
+ 'newPw' => $newPw,
+ 'oldPw' => $oldPw,
+ ];
+ }
+}
diff --git a/application/api/controller/WebSocketController.php b/application/api/controller/WebSocketController.php
new file mode 100644
index 0000000..69afb2e
--- /dev/null
+++ b/application/api/controller/WebSocketController.php
@@ -0,0 +1,1157 @@
+initConnection($userData);
+ }
+
+ /**
+ * 初始化WebSocket连接
+ * @param array $userData 用户数据
+ */
+ protected function initConnection($userData = [])
+ {
+ if (!empty($userData) && count($userData)) {
+ if (empty($userData['userName']) || empty($userData['password'])) {
+ return json_encode(['code' => 400, 'msg' => '参数缺失']);
+ }
+
+ // 检查缓存中是否存在有效的token
+ $cacheKey = 'websocket_token_' . $userData['userName'];
+ $cachedToken = Cache::get($cacheKey);
+ if ($cachedToken) {
+ $this->authorized = $cachedToken;
+ $this->accountId = $userData['accountId'];
+ } else {
+ $params = [
+ 'grant_type' => 'password',
+ 'username' => $userData['userName'],
+ 'password' => $userData['password']
+ ];
+ // 调用登录接口获取token
+ $headerData = ['client:kefu-client'];
+
+ $headerData[] = 'verifysessionid:2fbc51c9-db70-4e84-9568-21ef3667e1be';
+ $headerData[] = 'verifycode:5bcd';
+ $header = setHeader($headerData, '', 'plain');
+ $result = requestCurl('https://s2.siyuguanli.com:9991/token', $params, 'POST', $header);
+ $result_array = handleApiResponse($result);
+ if (isset($result_array['access_token']) && !empty($result_array['access_token'])) {
+ $this->authorized = $result_array['access_token'];
+ $this->accountId = $userData['accountId'];
+
+ // 将token存入缓存,有效期5分钟
+ Cache::set($cacheKey, $this->authorized, 300);
+ } else {
+ return json_encode(['code' => 400, 'msg' => '获取系统授权信息失败']);
+ }
+ }
+ } else {
+ $this->authorized = $this->request->header('authorization', '');
+ $this->accountId = $this->request->param('accountId', '');
+ }
+
+ if (empty($this->authorized) || empty($this->accountId)) {
+ return json_encode(['code' => 400, 'msg' => '缺失关键参数']);
+ }
+
+ $this->connect();
+ }
+
+ /**
+ * 建立WebSocket连接
+ */
+ protected function connect()
+ {
+ try {
+ //证书
+ $context = stream_context_create();
+ stream_context_set_option($context, 'ssl', 'verify_peer', false);
+ stream_context_set_option($context, 'ssl', 'verify_peer_name', false);
+
+ //开启WS链接
+ $result = [
+ "accessToken" => $this->authorized,
+ "accountId" => $this->accountId,
+ "client" => "kefu-client",
+ "cmdType" => "CmdSignIn",
+ "seq" => 1,
+ ];
+
+ $content = json_encode($result);
+ $this->client = new Client("wss://s2.siyuguanli.com:9993",
+ [
+ 'filter' => ['text', 'binary', 'ping', 'pong', 'close', 'receive', 'send'],
+ 'context' => $context,
+ 'headers' => [
+ 'Sec-WebSocket-Protocol' => 'soap',
+ 'origin' => 'localhost',
+ ],
+ 'timeout' => 86400,
+ ]
+ );
+
+ $this->client->send($content);
+ $this->isConnected = true;
+ $this->lastHeartbeatTime = time();
+
+ // 启动心跳检测
+ //$this->startHeartbeat();
+
+ } catch (\Exception $e) {
+ Log::error("WebSocket连接失败:" . $e->getMessage());
+ $this->isConnected = false;
+ }
+ }
+
+ /**
+ * 启动心跳检测
+ */
+ protected function startHeartbeat()
+ {
+ // 使用定时器发送心跳
+ \Swoole\Timer::tick($this->heartbeatInterval * 1000, function () {
+ if ($this->isConnected) {
+ $this->sendHeartbeat();
+ }
+ });
+ }
+
+ /**
+ * 发送心跳包
+ */
+ protected function sendHeartbeat()
+ {
+ try {
+ $heartbeat = [
+ "cmdType" => "CmdHeartbeat",
+ "seq" => time()
+ ];
+
+ $this->client->send(json_encode($heartbeat));
+ $this->lastHeartbeatTime = time();
+
+ } catch (\Exception $e) {
+ Log::error("发送心跳包失败:" . $e->getMessage());
+ $this->reconnect();
+ }
+ }
+
+ /**
+ * 重连机制
+ */
+ protected function reconnect()
+ {
+ try {
+ if ($this->client) {
+ $this->client->close();
+ }
+ $this->isConnected = false;
+ $this->connect();
+ } catch (\Exception $e) {
+ Log::error("WebSocket重连失败:" . $e->getMessage());
+ }
+ }
+
+ /**
+ * 检查连接状态
+ */
+ protected function checkConnection()
+ {
+ if (!$this->isConnected || (time() - $this->lastHeartbeatTime) > $this->heartbeatInterval * 2) {
+ $this->reconnect();
+ }
+ }
+
+ /**
+ * 发送消息
+ * @param array $data 消息数据
+ * @return array
+ */
+ protected function sendMessage($data,$receive = true)
+ {
+ $this->checkConnection();
+
+ try {
+ $this->client->send(json_encode($data));
+ if ($receive){
+ $response = $this->client->receive();
+ return json_decode($response, true);
+ }else{
+ return ['code' => 200, 'msg' => '成功'];
+ }
+ } catch (\Exception $e) {
+ Log::error("发送消息失败:" . $e->getMessage());
+ $this->reconnect();
+ return ['code' => 500, 'msg' => '发送消息失败'];
+ }
+ }
+
+ /************************************
+ * 朋友圈相关功能
+ ************************************/
+
+ /**
+ * 获取指定账号朋友圈信息
+ * @param array $data 请求参数
+ * @return \think\response\Json
+ */
+ public function getMoments($data = [])
+ {
+ $count = !empty($data['count']) ? $data['count'] : 10;
+ $wechatAccountId = !empty($data['wechatAccountId']) ? $data['wechatAccountId'] : '';
+ $wechatFriendId = !empty($data['wechatFriendId']) ? $data['wechatFriendId'] : 0;
+ $prevSnsId = !empty($data['prevSnsId']) ? $data['prevSnsId'] : 0;
+ $maxPages = 1; // 最大页数限制为20
+ $currentPage = 1; // 当前页码
+ $allMoments = []; // 存储所有朋友圈数据
+
+ //过滤消息
+ if (empty($wechatAccountId)) {
+ return json_encode(['code' => 400, 'msg' => '指定账号不能为空']);
+ }
+
+ try {
+ do {
+ $params = [
+ "cmdType" => "CmdFetchMoment",
+ "count" => $count,
+ "createTimeSec" => time(),
+ "isTimeline" => false,
+ "prevSnsId" => $prevSnsId,
+ "wechatAccountId" => $wechatAccountId,
+ "wechatFriendId" => $wechatFriendId,
+ "seq" => time(),
+ ];
+ $message = $this->sendMessage($params);
+
+
+ // 检查是否遇到频率限制
+ if (isset($message['extra']) && strpos($message['extra'], '朋友圈太频繁了') !== false) {
+ sleep(10);
+ continue;
+ }
+
+
+ // 检查返回结果
+ if (!isset($message['result']) || empty($message['result']) || !is_array($message['result'])) {
+ break;
+ }
+
+
+ // 检查是否遇到旧数据
+ $hasOldData = false;
+ foreach ($message['result'] as $moment) {
+ $momentId = WechatMoments::where('snsId', $moment['snsId'])
+ ->where('wechatAccountId', $wechatAccountId)
+ ->value('id');
+
+ if (!empty($momentId)) {
+ $hasOldData = true;
+ break;
+ }
+ }
+
+ // 如果遇到旧数据,结束本次任务
+ if ($hasOldData) {
+ // Log::info('遇到旧数据,结束本次任务');
+ // break;
+ }
+
+ // 合并朋友圈数据
+ $allMoments = array_merge($allMoments, $message['result']);
+ // 存储当前页的朋友圈数据到数据库
+ $this->saveMomentsToDatabase($message['result'], $wechatAccountId, $wechatFriendId);
+
+ // 获取最后一条数据的snsId,用于下次查询
+ $lastMoment = end($message['result']);
+ if (!$lastMoment || !isset($lastMoment['snsId'])) {
+ break;
+ }
+
+ $prevSnsId = $lastMoment['snsId'];
+ $currentPage++;
+
+ // 如果已经达到最大页数,退出循环
+ if ($currentPage > $maxPages) {
+ break;
+ }
+
+ // 如果返回的数据少于请求的数量,说明没有更多数据了
+ if (count($message['result']) < $count) {
+ break;
+ }
+
+ } while (true);
+
+ // 构建返回数据
+ $result = [
+ 'code' => 200,
+ 'msg' => '获取朋友圈信息成功',
+ 'data' => [
+ 'list' => $allMoments,
+ 'total' => count($allMoments),
+ 'nextPrevSnsId' => $prevSnsId
+ ]
+ ];
+
+ return json_encode($result);
+ } catch (\Exception $e) {
+ return json_encode(['code' => 500, 'msg' => $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 朋友圈点赞
+ * @return \think\response\Json
+ */
+ public function momentInteract($data = [])
+ {
+
+ $snsId = !empty($data['snsId']) ? $data['snsId'] : '';
+ $wechatAccountId = !empty($data['wechatAccountId']) ? $data['wechatAccountId'] : '';
+ $wechatFriendId = !empty($data['wechatFriendId']) ? $data['wechatFriendId'] : 0;
+
+
+ //过滤消息
+ if (empty($snsId)) {
+ return json_encode(['code' => 400, 'msg' => 'snsId不能为空']);
+ }
+ if (empty($wechatAccountId)) {
+ return json_encode(['code' => 400, 'msg' => '微信id不能为空']);
+ }
+
+ try {
+ $result = [
+ "cmdType" => "CmdMomentInteract",
+ "momentInteractType" => 1,
+ "seq" => time(),
+ "snsId" => $snsId,
+ "wechatAccountId" => $wechatAccountId,
+ "wechatFriendId" => $wechatFriendId,
+ ];
+
+ $message = $this->sendMessage($result);
+ return json_encode(['code' => 200, 'msg' => '点赞成功', 'data' => $message]);
+ } catch (\Exception $e) {
+ return json_encode(['code' => 500, 'msg' => $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 朋友圈取消点赞
+ * @return \think\response\Json
+ */
+ public function momentCancelInteract()
+ {
+ if ($this->request->isPost()) {
+ $data = $this->request->param();
+
+ if (empty($data)) {
+ return json_encode(['code' => 400, 'msg' => '参数缺失']);
+ }
+
+ //过滤消息
+ if (empty($data['snsId'])) {
+ return json_encode(['code' => 400, 'msg' => 'snsId不能为空']);
+ }
+ if (empty($data['wechatAccountId'])) {
+ return json_encode(['code' => 400, 'msg' => '微信id不能为空']);
+ }
+
+ try {
+ $result = [
+ "CommentId2" => '',
+ "CommentTime" => 0,
+ "cmdType" => "CmdMomentCancelInteract",
+ "optType" => 1,
+ "seq" => time(),
+ "snsId" => $data['snsId'],
+ "wechatAccountId" => $data['wechatAccountId'],
+ "wechatFriendId" => 0,
+ ];
+
+ $message = $this->sendMessage($result);
+ return json_encode(['code' => 200, 'msg' => '取消点赞成功', 'data' => $message]);
+ } catch (\Exception $e) {
+ return json_encode(['code' => 500, 'msg' => $e->getMessage()]);
+ }
+ } else {
+ return json_encode(['code' => 400, 'msg' => '非法请求']);
+ }
+ }
+
+ /**
+ * 获取指定账号朋友圈图片地址
+ * @param array $data 请求参数
+ * @return string JSON响应
+ */
+ public function getMomentSourceRealUrl($data = [])
+ {
+ try {
+ // 参数验证
+ if (empty($data)) {
+ return json_encode(['code' => 400, 'msg' => '参数缺失']);
+ }
+
+ // 验证必要参数
+ $requiredParams = ['snsId', 'snsUrls', 'wechatAccountId'];
+ foreach ($requiredParams as $param) {
+ if (empty($data[$param])) {
+ return json_encode(['code' => 400, 'msg' => "参数 {$param} 不能为空"]);
+ }
+ }
+
+ // 验证snsUrls是否为数组
+ if (!is_array($data['snsUrls'])) {
+ return json_encode(['code' => 400, 'msg' => '资源信息格式错误,应为数组']);
+ }
+
+ // 判断snsUrls是否已经是远程图片(http/https)
+ $allRemoteUrls = true;
+ foreach ($data['snsUrls'] as $url) {
+ if (empty($url)) {
+ continue;
+ }
+ // 检查URL是否以http://或https://开头
+ if (!preg_match('/^https?:\/\//i', $url)) {
+ $allRemoteUrls = false;
+ break;
+ }
+ }
+
+ // 如果全部都是远程URL,直接使用,不需要通过WebSocket下载
+ if ($allRemoteUrls && !empty($data['snsUrls'])) {
+ $urls = json_encode($data['snsUrls'], 256);
+
+ // 上传图片到OSS
+ $ossUrls = $this->uploadMomentImagesToOss($data['snsUrls'], $data['snsId']);
+
+ // 更新数据库:保存原始URL和OSS URL,并标记已上传
+ $updateData = [
+ 'resUrls' => $urls,
+ 'isOssUploaded' => 1, // 标识已上传到OSS
+ 'update_time' => time()
+ ];
+
+ // 如果有OSS URL,保存到ossUrls字段
+ if (!empty($ossUrls)) {
+ $updateData['ossUrls'] = json_encode($ossUrls, 256);
+ }
+ Db::table('s2_wechat_moments')->where('snsId', $data['snsId'])->update($updateData);
+
+ return json_encode(['code' => 200, 'msg' => '获取朋友圈资源链接成功(已为远程URL)', 'data' => ['urls' => $data['snsUrls']]]);
+ }
+
+ // 如果不是远程URL,需要通过WebSocket下载
+ // 检查连接状态
+ if (!$this->isConnected) {
+ $this->connect();
+ if (!$this->isConnected) {
+ return json_encode(['code' => 500, 'msg' => 'WebSocket连接失败']);
+ }
+ }
+
+ // 构建请求参数
+ $params = [
+ "cmdType" => 'CmdDownloadMomentImages',
+ "snsId" => $data['snsId'],
+ "urls" => $data['snsUrls'],
+ "wechatAccountId" => $data['wechatAccountId'],
+ "seq" => time(),
+ ];
+
+ $message = $this->sendMessage($params);
+
+ if (empty($message)) {
+ return json_encode(['code' => 500, 'msg' => '获取朋友圈资源链接失败']);
+ }
+ if ($message['cmdType'] == 'CmdDownloadMomentImagesResult' && is_array($message['urls']) && count($message['urls']) > 0) {
+ $urls = json_encode($message['urls'], 256);
+
+ // 上传图片到OSS
+ $ossUrls = $this->uploadMomentImagesToOss($message['urls'], $data['snsId']);
+
+ // 更新数据库:保存原始URL和OSS URL,并标记已上传
+ $updateData = [
+ 'resUrls' => $urls,
+ 'isOssUploaded' => 1, // 标识已上传到OSS
+ 'update_time' => time()
+ ];
+
+ // 如果有OSS URL,保存到ossUrls字段
+ if (!empty($ossUrls)) {
+ $updateData['ossUrls'] = json_encode($ossUrls, 256);
+ }
+ Db::table('s2_wechat_moments')->where('snsId', $data['snsId'])->update($updateData);
+
+ }
+ return json_encode(['code' => 200, 'msg' => '获取朋友圈资源链接成功', 'data' => $message]);
+ } catch (\Exception $e) {
+ // 记录错误日志
+ Log::error('获取朋友圈资源链接异常:' . $e->getMessage());
+ Log::error('异常堆栈:' . $e->getTraceAsString());
+
+ // 尝试重连
+ try {
+ $this->reconnect();
+ } catch (\Exception $reconnectError) {
+ Log::error('WebSocket重连失败:' . $reconnectError->getMessage());
+ }
+
+ return json_encode([
+ 'code' => 500,
+ 'msg' => '获取朋友圈资源链接失败:' . $e->getMessage()
+ ]);
+ }
+ }
+
+ /**
+ * 上传朋友圈图片到OSS
+ * @param array $urls 图片URL数组
+ * @param string $snsId 朋友圈ID
+ * @return array OSS URL数组
+ */
+ protected function uploadMomentImagesToOss($urls, $snsId)
+ {
+ $ossUrls = [];
+
+ if (empty($urls) || !is_array($urls)) {
+ return $ossUrls;
+ }
+
+ try {
+ // 创建临时目录(兼容无 runtime_path() 辅助函数的环境)
+ if (function_exists('runtime_path')) {
+ $baseRuntimePath = rtrim(runtime_path(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
+ } elseif (defined('RUNTIME_PATH')) {
+ $baseRuntimePath = rtrim(RUNTIME_PATH, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
+ } else {
+ // 兜底:使用项目根目录下的 runtime 目录
+ $baseRuntimePath = rtrim(ROOT_PATH, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'runtime' . DIRECTORY_SEPARATOR;
+ }
+
+ $tempDir = $baseRuntimePath . 'temp' . DIRECTORY_SEPARATOR . 'moments' . DIRECTORY_SEPARATOR . date('Y' . DIRECTORY_SEPARATOR . 'm' . DIRECTORY_SEPARATOR . 'd') . DIRECTORY_SEPARATOR;
+
+ if (!is_dir($tempDir)) {
+ mkdir($tempDir, 0755, true);
+ }
+
+ foreach ($urls as $index => $url) {
+ if (empty($url)) {
+ continue;
+ }
+
+ try {
+ // 下载图片到临时文件
+ $tempFile = $tempDir . md5($url . $snsId . $index) . '.jpg';
+
+ // 使用curl下载图片
+ $ch = curl_init($url);
+ $fp = fopen($tempFile, 'wb');
+ curl_setopt($ch, CURLOPT_FILE, $fp);
+ curl_setopt($ch, CURLOPT_HEADER, 0);
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+ curl_setopt($ch, CURLOPT_TIMEOUT, 30);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+ curl_exec($ch);
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+ fclose($fp);
+
+ if ($httpCode != 200 || !file_exists($tempFile) || filesize($tempFile) == 0) {
+ Log::warning('下载朋友圈图片失败:' . $url . ', HTTP Code: ' . $httpCode);
+ @unlink($tempFile);
+ continue;
+ }
+
+ // 生成OSS对象名称
+ $objectName = 'moments/' . date('Y/m/d/') . md5($snsId . $index . time()) . '.jpg';
+
+ // 上传到OSS
+ $result = AliyunOSS::uploadFile($tempFile, $objectName);
+ if ($result['success']) {
+ $ossUrls[] = $result['url'];
+ } else {
+ Log::error('朋友圈图片上传OSS失败:' . $url . ', 错误:' . ($result['error'] ?? '未知错误'));
+ }
+
+ // 删除临时文件
+ @unlink($tempFile);
+
+ } catch (\Exception $e) {
+ Log::error('上传朋友圈图片到OSS异常:' . $e->getMessage() . ', URL: ' . $url);
+ if (isset($tempFile) && file_exists($tempFile)) {
+ @unlink($tempFile);
+ }
+ }
+ }
+
+ } catch (\Exception $e) {
+ Log::error('上传朋友圈图片到OSS异常:' . $e->getMessage());
+ }
+
+ return $ossUrls;
+ }
+
+ /**
+ * 保存朋友圈数据到数据库
+ * @param array $momentList 朋友圈数据列表
+ * @param int $wechatAccountId 微信账号ID
+ * @param string $wechatFriendId 微信好友ID
+ * @return bool
+ */
+ protected function saveMomentsToDatabase($momentList, $wechatAccountId, $wechatFriendId)
+ {
+ if (empty($momentList) || !is_array($momentList)) {
+ return false;
+ }
+
+
+ try {
+ foreach ($momentList as $moment) {
+ // 提取momentEntity中的数据
+ $momentEntity = $moment['momentEntity'] ?? [];
+
+ // 检查朋友圈数据是否已存在,并获取isOssUploaded状态
+ $existingMoment = Db::table('s2_wechat_moments')
+ ->where('snsId', $moment['snsId'])
+ ->where('wechatAccountId', $wechatAccountId)
+ ->find();
+
+ $momentId = $existingMoment['id'] ?? null;
+ $isOssUploaded = isset($existingMoment['isOssUploaded']) ? (int)$existingMoment['isOssUploaded'] : 0;
+
+ $dataToSave = [
+ 'commentList' => json_encode($moment['commentList'] ?? [], 256),
+ 'createTime' => $moment['createTime'] ?? 0,
+ 'likeList' => json_encode($moment['likeList'] ?? [], 256),
+ 'content' => $momentEntity['content'] ?? '',
+ 'lat' => $momentEntity['lat'] ?? 0,
+ 'lng' => $momentEntity['lng'] ?? 0,
+ 'location' => $momentEntity['location'] ?? '',
+ 'picSize' => $momentEntity['picSize'] ?? 0,
+ 'resUrls' => json_encode($momentEntity['resUrls'] ?? [], 256),
+ 'urls' => json_encode($momentEntity['urls'] ?? [], 256),
+ 'userName' => $momentEntity['userName'] ?? '',
+ 'snsId' => $moment['snsId'] ?? '',
+ 'type' => $moment['type'] ?? 0,
+ 'title' => $momentEntity['title'] ?? '',
+ 'coverImage' => $momentEntity['coverImage'] ?? '',
+ 'update_time' => time()
+ ];
+
+ if (!empty($momentId)) {
+ // 如果已存在,则更新数据(保留isOssUploaded和ossUrls字段,不覆盖)
+ Db::table('s2_wechat_moments')->where('id', $momentId)->update($dataToSave);
+ } else {
+ if (empty($wechatFriendId)) {
+ $wechatFriendId = WechatFriend::where('wechatAccountId', $wechatAccountId)->where('wechatId', $momentEntity['userName'])->value('id');
+ }
+ // 如果不存在,则插入新数据
+ $dataToSave['wechatAccountId'] = $wechatAccountId;
+ $dataToSave['wechatFriendId'] = $wechatFriendId ?? 0;
+ $dataToSave['create_time'] = time();
+ $dataToSave['ossUrls'] = json_encode([],256);
+ $dataToSave['isOssUploaded'] = 0; // 新记录默认为未上传
+ $res = WechatMoments::create($dataToSave);
+ }
+
+ // 获取资源链接(检查是否已上传到OSS,如果已上传则跳过)
+ if(empty($momentEntity['urls']) || $moment['type'] != 1) {
+ // 如果没有urls或类型不是1,跳过
+ } elseif ($isOssUploaded == 1) {
+ // 如果已上传到OSS,跳过采集
+ } else {
+ // 未上传到OSS,执行采集
+ $snsData = [
+ 'snsId' => $moment['snsId'],
+ 'snsUrls' => $momentEntity['urls'],
+ 'wechatAccountId' => $wechatAccountId,
+ ];
+ $this->getMomentSourceRealUrl($snsData);
+ }
+
+ }
+ //Log::write('朋友圈数据已存入数据库,共' . count($momentList) . '条');
+ return true;
+ } catch (\Exception $e) {
+ //Log::write('保存朋友圈数据失败:' . $e->getMessage(), 'error');
+ return false;
+ }
+ }
+
+
+ /**
+ * 修改好友标签
+ * @param array $data 请求参数
+ * @return string JSON响应
+ */
+ public function modifyFriendLabel($data = [])
+ {
+ // 获取请求参数
+ $wechatFriendId = !empty($data['wechatFriendId']) ? $data['wechatFriendId'] : 0;
+ $wechatAccountId = !empty($data['wechatAccountId']) ? $data['wechatAccountId'] : '';
+ $labels = !empty($data['labels']) ? $data['labels'] : [];
+
+ // 验证必要参数
+ if (empty($wechatFriendId)) {
+ return json_encode(['code' => 400, 'msg' => '好友ID不能为空']);
+ }
+
+ if (empty($wechatAccountId)) {
+ return json_encode(['code' => 400, 'msg' => '微信账号ID不能为空']);
+ }
+
+ if (empty($labels)) {
+ return json_encode(['code' => 400, 'msg' => '标签不能为空']);
+ }
+
+ try {
+ // 构建请求参数
+ $params = [
+ "cmdType" => "CmdModifyFriendLabel",
+ "labels" => $labels,
+ "seq" => time(),
+ "wechatAccountId" => $wechatAccountId,
+ "wechatFriendId" => $wechatFriendId,
+ ];
+
+ // 发送请求并获取响应
+ $message = $this->sendMessage($params);
+
+ // 返回成功响应
+ return json_encode(['code' => 200, 'msg' => '修改标签成功', 'data' => $message]);
+ } catch (\Exception $e) {
+ // 记录错误日志
+ Log::error('修改好友标签失败:' . $e->getMessage());
+
+ // 返回错误响应
+ return json_encode(['code' => 500, 'msg' => '修改标签失败:' . $e->getMessage()]);
+ }
+ }
+
+ /************************************
+ * 消息发送相关功能
+ ************************************/
+
+ /**
+ * 个人消息发送
+ * @return \think\response\Json
+ */
+ public function sendPersonal(array $dataArray)
+ {
+ //过滤消息
+ if (empty($dataArray['content'])) {
+ return json_encode(['code' => 400, 'msg' => '内容缺失']);
+ }
+ if (empty($dataArray['wechatAccountId'])) {
+ return json_encode(['code' => 400, 'msg' => '微信id不能为空']);
+ }
+ if (empty($dataArray['wechatFriendId'])) {
+ return json_encode(['code' => 400, 'msg' => '接收人不能为空']);
+ }
+
+ if (empty($dataArray['msgType'])) {
+ return json_encode(['code' => 400, 'msg' => '类型缺失']);
+ }
+
+ // 消息拼接 msgType(1:文本 3:图片 43:视频 47:动图表情包(gif、其他表情包) 49:小程序/其他:图文、文件)
+ // 当前,type 为文本、图片、动图表情包的时候,content为string, 其他情况为对象 {type: 'file/link/...', url: '', title: '', thunmbPath: '', desc: ''}
+ $params = [
+ "cmdType" => "CmdSendMessage",
+ "content" => $dataArray['content'],
+ "msgSubType" => 0,
+ "msgType" => $dataArray['msgType'],
+ "seq" => time(),
+ "wechatAccountId" => $dataArray['wechatAccountId'],
+ "wechatChatroomId" => 0,
+ "wechatFriendId" => $dataArray['wechatFriendId'],
+ ];
+ // 发送请求
+ $this->client->send(json_encode($params));
+ // 接收响应
+ $response = $this->client->receive();
+ $message = json_decode($response, true);
+ if (!empty($message)) {
+ return json_encode(['code' => 200, 'msg' => '信息发送成功', 'data' => $message]);
+ }
+ }
+
+ /**
+ * 发送群消息
+ * @return \think\response\Json
+ */
+ public function sendCommunity($dataArray = [])
+ {
+ if (!is_array($dataArray)) {
+ return json_encode(['code' => 400, 'msg' => '数据格式错误']);
+ }
+
+ //过滤消息
+ if (empty($dataArray['content'])) {
+ return json_encode(['code' => 400, 'msg' => '内容缺失']);
+ }
+ if (empty($dataArray['wechatAccountId'])) {
+ return json_encode(['code' => 400, 'msg' => '微信id不能为空']);
+ }
+
+ if (empty($dataArray['msgType'])) {
+ return json_encode(['code' => 400, 'msg' => '类型缺失']);
+ }
+ if (empty($dataArray['wechatChatroomId'])) {
+ return json_encode(['code' => 400, 'msg' => '群id不能为空']);
+ }
+
+ $message = [];
+ try {
+ //消息拼接 msgType(1:文本 3:图片 43:视频 47:动图表情包 49:小程序)
+ $params = [
+ "cmdType" => "CmdSendMessage",
+ "content" => htmlspecialchars_decode($dataArray['content']),
+ "msgSubType" => 0,
+ "msgType" => $dataArray['msgType'],
+ "seq" => time(),
+ "wechatAccountId" => $dataArray['wechatAccountId'],
+ "wechatChatroomId" => $dataArray['wechatChatroomId'],
+ "wechatFriendId" => 0,
+ ];
+
+ // 发送请求
+ $this->client->send(json_encode($params));
+ // 接收响应
+ $response = $this->client->receive();
+ $message = json_decode($response, true);
+ if (!empty($message)) {
+ return json_encode(['code' => 200, 'msg' => '信息发送成功', 'data' => $message]);
+ }
+ } catch (\Exception $e) {
+ $msg = $e->getMessage();
+ return json_encode(['code' => 400, 'msg' => $msg, 'data' => $message]);
+ }
+
+
+ }
+
+ /**
+ * 发送群消息(内部调用版)
+ * @param array $data 消息数据
+ * @return \think\response\Json
+ */
+ public function sendCommunitys($data = [])
+ {
+ if (empty($data)) {
+ return json_encode(['code' => 400, 'msg' => '参数缺失']);
+ }
+ $dataArray = $data;
+ if (!is_array($dataArray)) {
+ return json_encode(['code' => 400, 'msg' => '数据格式错误']);
+ }
+
+ //过滤消息
+ if (empty($dataArray['content'])) {
+ return json_encode(['code' => 400, 'msg' => '内容缺失']);
+ }
+ if (empty($dataArray['wechatAccountId'])) {
+ return json_encode(['code' => 400, 'msg' => '微信id不能为空']);
+ }
+
+ if (empty($dataArray['msgType'])) {
+ return json_encode(['code' => 400, 'msg' => '类型缺失']);
+ }
+ if (empty($dataArray['wechatChatroomId'])) {
+ return json_encode(['code' => 400, 'msg' => '群id不能为空']);
+ }
+
+ $msg = '消息成功发送';
+ $message = [];
+ try {
+ //消息拼接 msgType(1:文本 3:图片 43:视频 47:动图表情包 49:小程序)
+ $result = [
+ "cmdType" => "CmdSendMessage",
+ "content" => $dataArray['content'],
+ "msgSubType" => 0,
+ "msgType" => $dataArray['msgType'],
+ "seq" => time(),
+ "wechatAccountId" => $dataArray['wechatAccountId'],
+ "wechatChatroomId" => $dataArray['wechatChatroomId'],
+ "wechatFriendId" => 0,
+ ];
+
+ $result = json_encode($result);
+ $this->client->send($result);
+ $message = $this->client->receive();
+ //关闭WS链接
+ $this->client->close();
+ //Log::write('WS群消息发送');
+ //Log::write($message);
+ $message = json_decode($message, 1);
+ } catch (\Exception $e) {
+ $msg = $e->getMessage();
+ }
+
+ return json_encode(['code' => 200, 'msg' => $msg, 'data' => $message]);
+ }
+
+
+ /**
+ * 添加群好友
+ * @param $data
+ * @return false|string
+ */
+ public function CmdChatroomOperate($data = [])
+ {
+ try {
+ // 参数验证
+ if (empty($data)) {
+ return json_encode(['code' => 400, 'msg' => '参数缺失']);
+ }
+
+ // 验证必要参数
+ if (empty($data['wechatId'])) {
+ return json_encode(['code' => 400, 'msg' => 'wechatId不能为空']);
+ }
+
+ if (empty($data['sendWord'])) {
+ return json_encode(['code' => 400, 'msg' => '添加的招呼语不能为空']);
+ }
+
+ if (empty($data['wechatAccountId'])) {
+ return json_encode(['code' => 400, 'msg' => '微信账号ID不能为空']);
+ }
+ if (empty($data['wechatChatroomId'])) {
+ return json_encode(['code' => 400, 'msg' => '群ID不能为空']);
+ }
+
+
+ // 构建请求参数
+ $params = [
+ "chatroomOperateType" => 1,
+ "cmdType" => "CmdChatroomOperate",
+ "extra" => [
+ 'wechatId' => $data['wechatId'],
+ 'sendWord' => $data['sendWord']
+ ],
+ "seq" => time(),
+ "wechatAccountId" => $data['wechatAccountId'],
+ "wechatChatroomId" => $data['wechatChatroomId'],
+ ];
+ $message = $this->sendMessage($params);
+ return json_encode(['code' => 200, 'msg' => '添加好友请求发送成功', 'data' => $message]);
+ } catch (\Exception $e) {
+ // 返回错误响应
+ return json_encode(['code' => 500, 'msg' => '添加群好友异常:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 创建群聊
+ * @param array $data 请求参数
+ * @return string JSON响应
+ */
+ public function CmdChatroomCreate($data = [])
+ {
+ try {
+ // 参数验证
+ if (empty($data)) {
+ return json_encode(['code' => 400, 'msg' => '参数缺失']);
+ }
+
+ // 验证必要参数
+ if (empty($data['chatroomName'])) {
+ return json_encode(['code' => 400, 'msg' => '群名称不能为空']);
+ }
+
+ if (empty($data['wechatFriendIds']) || !is_array($data['wechatFriendIds'])) {
+ return json_encode(['code' => 400, 'msg' => '好友ID列表不能为空且必须为数组']);
+ }
+
+ if (count($data['wechatFriendIds']) < 2) {
+ return json_encode(['code' => 400, 'msg' => '创建群聊至少需要2个好友']);
+ }
+
+ if (empty($data['wechatAccountId'])) {
+ return json_encode(['code' => 400, 'msg' => '微信账号ID不能为空']);
+ }
+
+ // 构建请求参数
+ $params = [
+ "cmdType" => "CmdChatroomCreate",
+ "seq" => time(),
+ "wechatAccountId" => $data['wechatAccountId'],
+ "chatroomName" => $data['chatroomName'],
+// "wechatFriendIds" => $data['wechatFriendIds']
+ "wechatFriendIds" => [17453051,17453058]
+ ];
+ $message = $this->sendMessage($params,false);
+ return json_encode(['code' => 200, 'msg' => '群聊创建成功', 'data' => $message]);
+ } catch (\Exception $e) {
+ // 记录错误日志
+ Log::error('创建群聊异常:' . $e->getMessage());
+ // 返回错误响应
+ return json_encode(['code' => 500, 'msg' => '创建群聊异常:' . $e->getMessage()]);
+ }
+ }
+
+
+ /**
+ * 邀请好友入群
+ * @param array $data 请求参数
+ * @return string JSON响应
+ */
+ public function CmdChatroomInvite($data = [])
+ {
+ try {
+ // 参数验证
+ if (empty($data)) {
+ return json_encode(['code' => 400, 'msg' => '参数缺失']);
+ }
+
+ // 验证必要参数
+ if (empty($data['wechatChatroomId'])) {
+ return json_encode(['code' => 400, 'msg' => '群ID不能为空']);
+ }
+ if (empty($data['wechatFriendIds'])) {
+ return json_encode(['code' => 400, 'msg' => '好友ID不能为空']);
+ }
+
+ if (!is_array($data['wechatFriendIds'])) {
+ return json_encode(['code' => 400, 'msg' => '好友数据格式必须为数组']);
+ }
+
+
+ // 构建请求参数
+ $params = [
+ "cmdType" => "CmdChatroomInvite",
+ "seq" => time(),
+ "wechatChatroomId" => $data['wechatChatroomId'],
+ "wechatFriendIds" => $data['wechatFriendIds']
+ ];
+
+ $message = $this->sendMessage($params,false);
+ return json_encode(['code' => 200, 'msg' => '邀请成功', 'data' => $message]);
+ } catch (\Exception $e) {
+ // 记录错误日志
+ Log::error('邀请好友入群异常:' . $e->getMessage());
+ // 返回错误响应
+ return json_encode(['code' => 500, 'msg' => '邀请好友入群异常:' . $e->getMessage()]);
+ }
+ }
+
+
+
+ /**
+ * 修改群信息(群昵称和群公告)
+ * @param array $data 请求参数
+ * @return string JSON响应
+ */
+ public function CmdChatroomModifyInfo($data = [])
+ {
+ try {
+ // 参数验证
+ if (empty($data)) {
+ return json_encode(['code' => 400, 'msg' => '参数缺失']);
+ }
+
+ // 验证必要参数
+ if (empty($data['wechatChatroomId'])) {
+ return json_encode(['code' => 400, 'msg' => '群ID不能为空']);
+ }
+
+ if (empty($data['wechatAccountId'])) {
+ return json_encode(['code' => 400, 'msg' => '微信账号ID不能为空']);
+ }
+
+ // 检查是否至少提供了一个修改项
+ if (empty($data['chatroomName']) && !isset($data['announce'])) {
+ return json_encode(['code' => 400, 'msg' => '请至少提供群昵称或群公告中的一个参数']);
+ }
+
+
+ if (!empty($data['chatroomName'])) {
+ $extra = [
+ "chatroomName" => $data['chatroomName']
+ ];
+ } else {
+ $extra = [
+ "announce" => $data['announce']
+ ];
+ }
+
+ //chatroomOperateType 4退群 6群公告 5群名称
+ $params = [
+ "chatroomOperateType" => !empty($data['chatroomName']) ? 6 : 5,
+ "cmdType" => "CmdChatroomOperate",
+ "extra" => json_encode($extra,256),
+ "seq" => time(),
+ "wechatAccountId" => $data['wechatAccountId'],
+ "wechatChatroomId" => $data['wechatChatroomId']
+ ];
+ $message = $this->sendMessage($params,false);
+ return json_encode(['code' => 200, 'msg' => '修改群信息成功', 'data' => $message]);
+ } catch (\Exception $e) {
+ Log::error('修改群信息异常: ' . $e->getMessage());
+ return json_encode(['code' => 500, 'msg' => '修改群信息失败: ' . $e->getMessage()]);
+ }
+ }
+
+
+
+
+ public function ttttt($data = [])
+ {
+ try {
+ $params = [
+ "chatroomOperateType" => $data['chatroomOperateType'],
+ "cmdType" => "CmdChatroomOperate",
+ "seq" => time(),
+ "wechatAccountId" => $data['wechatAccountId'],
+ "wechatChatroomId" => $data['wechatChatroomId']
+ ];
+ $message = $this->sendMessage($params,false);
+ return json_encode(['code' => 200, 'msg' => '234', 'data' => $message]);
+ } catch (\Exception $e) {
+ Log::error('修改群信息异常: ' . $e->getMessage());
+ return json_encode(['code' => 500, 'msg' => '567567: ' . $e->getMessage()]);
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/application/api/controller/WebSocketControllerCopy.php b/application/api/controller/WebSocketControllerCopy.php
new file mode 100644
index 0000000..12c516b
--- /dev/null
+++ b/application/api/controller/WebSocketControllerCopy.php
@@ -0,0 +1,559 @@
+400,'msg'=>'参数缺失']);
+ }
+ $params = [
+ 'grant_type' => 'password',
+ 'username' => $userData['userName'],
+ 'password' => $userData['password']
+ ];
+
+ // 调用登录接口获取token
+ // 设置请求头
+ $headerData = ['client:kefu-client'];
+ $header = setHeader($headerData, '', 'plain');
+ $result = requestCurl('https://s2.siyuguanli.com:9991/token', $params, 'POST',$header);
+ $result_array = handleApiResponse($result);
+
+ if (isset($result_array['access_token']) && !empty($result_array['access_token'])) {
+ $authorization = $result_array['access_token'];
+ $this->authorized = $authorization;
+ $this->accountId = $userData['accountId'];
+
+ } else {
+ return json_encode(['code'=>400,'msg'=>'获取系统授权信息失败']);
+ }
+ }else{
+ $this->authorized = $this->request->header('authorization', '');
+ $this->accountId = $this->request->param('accountId', '');
+ }
+
+
+ if (empty($this->authorized) || empty($this->accountId)) {
+ $data['authorized'] = $this->authorized;
+ $data['accountId'] = $this->accountId;
+ return json_encode(['code'=>400,'msg'=>'缺失关键参数']);
+ }
+
+ //证书
+ $context = stream_context_create();
+ stream_context_set_option($context, 'ssl', 'verify_peer', false);
+ stream_context_set_option($context, 'ssl', 'verify_peer_name', false);
+ //开启WS链接
+ $result = [
+ "accessToken" => $this->authorized,
+ "accountId" => $this->accountId,
+ "client" => "kefu-client",
+ "cmdType" => "CmdSignIn",
+ "seq" => 1,
+ ];
+
+
+ $content = json_encode($result);
+ $this->client = new Client("wss://s2.siyuguanli.com:9993",
+ [
+ 'filter' => ['text', 'binary', 'ping', 'pong', 'close','receive', 'send'],
+ 'context' => $context,
+ 'headers' => [
+ 'Sec-WebSocket-Protocol' => 'soap',
+ 'origin' => 'localhost',
+ ],
+ 'timeout' => 86400,
+ ]
+ );
+ $this->client->send($content);
+ }
+
+ /************************************
+ * 朋友圈相关功能
+ ************************************/
+
+ /**
+ * 获取指定账号朋友圈信息
+ * @param array $data 请求参数
+ * @return \think\response\Json
+ */
+ public function getMoments($data = [])
+ {
+
+ $count = !empty($data['count']) ? $data['count'] : 10;
+ $wechatAccountId = !empty($data['wechatAccountId']) ? $data['wechatAccountId'] : '';
+ $wechatFriendId = !empty($data['id']) ? $data['id'] : '';
+
+ //过滤消息
+ if (empty($wechatAccountId)) {
+ return json_encode(['code'=>400,'msg'=>'指定账号不能为空']);
+ }
+ if (empty($wechatFriendId)) {
+ return json_encode(['code'=>400,'msg'=>'指定好友不能为空']);
+ }
+ $msg = '获取朋友圈信息成功';
+ $message = [];
+ try {
+ $params = [
+ "cmdType" => "CmdFetchMoment",
+ "count" => $count,
+ "createTimeSec" => time(),
+ "isTimeline" => false,
+ "prevSnsId" => 0,
+ "wechatAccountId" => $wechatAccountId,
+ "wechatFriendId" => $wechatFriendId,
+ "seq" => time(),
+ ];
+ $params = json_encode($params);
+ //Log::write('WS获取朋友圈信息参数:' . json_encode($params, 256));
+ $this->client->send($params);
+ $message = $this->client->receive();
+ //Log::write('WS获取朋友圈信息成功,结果:' . $message);
+ $message = json_decode($message, 1);
+
+ // 存储朋友圈数据到数据库
+ if (isset($message['result']) && !empty($message['result'])) {
+ $this->saveMomentsToDatabase($message['result'], $wechatAccountId, $wechatFriendId);
+ }
+
+ //关闭WS链接
+ $this->client->close();
+ } catch (\Exception $e) {
+ $msg = $e->getMessage();
+ }
+
+ return json_encode(['code'=>200,'msg'=>$msg,'data'=>$message]);
+ }
+
+ /**
+ * 朋友圈点赞
+ * @return \think\response\Json
+ */
+ public function momentInteract()
+ {
+ if ($this->request->isPost()) {
+ $data = $this->request->param();
+
+ if (empty($data)) {
+ return json_encode(['code'=>400,'msg'=>'参数缺失']);
+ }
+ $dataArray = $data;
+ if (!is_array($dataArray)) {
+ return json_encode(['code'=>400,'msg'=>'数据格式错误']);
+ }
+
+ //过滤消息
+ if (empty($dataArray['snsId'])) {
+ return json_encode(['code'=>400,'msg'=>'snsId不能为空']);
+ }
+ if (empty($dataArray['wechatAccountId'])) {
+ return json_encode(['code'=>400,'msg'=>'微信id不能为空']);
+ }
+
+
+ $result = [
+ "cmdType" => "CmdMomentInteract",
+ "momentInteractType" => 1,
+ "seq" => time(),
+ "snsId" => $dataArray['snsId'],
+ "wechatAccountId" => $dataArray['wechatAccountId'],
+ "wechatFriendId" => 0,
+ ];
+
+ $result = json_encode($result);
+ $this->client->send($result);
+ $message = $this->client->receive();
+ $message = json_decode($message, 1);
+ //关闭WS链接
+ $this->client->close();
+ //Log::write('WS个人消息发送');
+ return json_encode(['code'=>200,'msg'=>'点赞成功','data'=>$message]);
+ } else {
+ return json_encode(['code'=>400,'msg'=>'非法请求']);
+ }
+ }
+
+ /**
+ * 朋友圈取消点赞
+ * @return \think\response\Json
+ */
+ public function momentCancelInteract()
+ {
+ if ($this->request->isPost()) {
+ $data = $this->request->param();
+
+ if (empty($data)) {
+ return json_encode(['code'=>400,'msg'=>'参数缺失']);
+ }
+ $dataArray = $data;
+ if (!is_array($dataArray)) {
+ return json_encode(['code'=>400,'msg'=>'数据格式错误']);
+ }
+
+ //过滤消息
+ if (empty($dataArray['snsId'])) {
+ return json_encode(['code'=>400,'msg'=>'snsId不能为空']);
+ }
+ if (empty($dataArray['wechatAccountId'])) {
+ return json_encode(['code'=>400,'msg'=>'微信id不能为空']);
+ }
+
+
+ $result = [
+ "CommentId2" => '',
+ "CommentTime" => 0,
+ "cmdType" => "CmdMomentCancelInteract",
+ "optType" => 1,
+ "seq" => time(),
+ "snsId" => $dataArray['snsId'],
+ "wechatAccountId" => $dataArray['wechatAccountId'],
+ "wechatFriendId" => 0,
+ ];
+
+ $result = json_encode($result);
+ $this->client->send($result);
+ $message = $this->client->receive();
+ $message = json_decode($message, 1);
+ //关闭WS链接
+ $this->client->close();
+ //Log::write('WS个人消息发送');
+ return json_encode(['code'=>200,'msg'=>'取消点赞成功','data'=>$message]);
+ } else {
+ return json_encode(['code'=>400,'msg'=>'非法请求']);
+ }
+ }
+
+ /**
+ * 获取指定账号朋友圈图片地址
+ * @return \think\response\Json
+ */
+ public function getMomentSourceRealUrl()
+ {
+ if ($this->request->isPost()) {
+ $data = $this->request->param();
+
+ if (empty($data)) {
+ return json_encode(['code'=>400,'msg'=>'参数缺失']);
+ }
+ $dataArray = $data;
+ if (!is_array($dataArray)) {
+ return json_encode(['code'=>400,'msg'=>'数据格式错误']);
+ }
+ //获取数据条数
+// $count = isset($dataArray['count']) ? $dataArray['count'] : 10;
+ //过滤消息
+ if (empty($dataArray['wechatAccountId'])) {
+ return json_encode(['code'=>400,'msg'=>'指定账号不能为空']);
+ }
+ if (empty($dataArray['snsId'])) {
+ return json_encode(['code'=>400,'msg'=>'指定消息ID不能为空']);
+ }
+ if (empty($dataArray['snsUrls'])) {
+ return json_encode(['code'=>400,'msg'=>'资源信息不能为空']);
+ }
+ $msg = '获取朋友圈资源链接成功';
+ $message = [];
+ try {
+ $params = [
+ "cmdType" => $dataArray['type'],
+ "snsId" => $dataArray['snsId'],
+ "urls" => $dataArray['snsUrls'],
+ "wechatAccountId" => $dataArray['wechatAccountId'],
+ "seq" => time(),
+ ];
+ $params = json_encode($params);
+ $this->client->send($params);
+ $message = $this->client->receive();
+ //Log::write('WS获取朋友圈图片/视频链接成功,结果:' . json_encode($message, 256));
+ //关闭WS链接
+ $this->client->close();
+ } catch (\Exception $e) {
+ $msg = $e->getMessage();
+ }
+
+ return json_encode(['code'=>200,'msg'=>$msg,'data'=>$message]);
+ } else {
+ return json_encode(['code'=>400,'msg'=>'非法请求']);
+ }
+ }
+
+ /**
+ * 保存朋友圈数据到数据库
+ * @param array $momentList 朋友圈数据列表
+ * @param int $wechatAccountId 微信账号ID
+ * @param string $wechatFriendId 微信好友ID
+ * @return bool
+ */
+ protected function saveMomentsToDatabase($momentList, $wechatAccountId, $wechatFriendId)
+ {
+ if (empty($momentList) || !is_array($momentList)) {
+ return false;
+ }
+
+ try {
+ foreach ($momentList as $moment) {
+ // 提取momentEntity中的数据
+ $momentEntity = $moment['momentEntity'] ?? [];
+
+ // 检查朋友圈数据是否已存在
+ $momentId = Db::table('s2_wechat_moments')
+ ->where('snsId', $moment['snsId'])
+ ->where('wechatAccountId', $wechatAccountId)
+ ->value('id');
+
+ $dataToSave = [
+ 'commentList' => json_encode($moment['commentList'] ?? [], 256),
+ 'createTime' => $moment['createTime'] ?? 0,
+ 'likeList' => json_encode($moment['likeList'] ?? [], 256),
+ 'content' => $momentEntity['content'] ?? '',
+ 'lat' => $momentEntity['lat'] ?? 0,
+ 'lng' => $momentEntity['lng'] ?? 0,
+ 'location' => $momentEntity['location'] ?? '',
+ 'picSize' => $momentEntity['picSize'] ?? 0,
+ 'resUrls' => json_encode($momentEntity['resUrls'] ?? [], 256),
+ 'userName' => $momentEntity['userName'] ?? '',
+ 'snsId' => $moment['snsId'] ?? '',
+ 'type' => $moment['type'] ?? 0,
+ 'title' => $moment['title'] ?? '',
+ 'coverImage' => $moment['coverImage'] ?? '',
+ 'update_time' => time()
+ ];
+
+ if ($momentId) {
+ // 如果已存在,则更新数据
+ Db::table('s2_wechat_moments')->where('id', $momentId)->update($dataToSave);
+ } else {
+ if(empty($wechatFriendId)){
+ $wechatFriendId = WechatFriend::where('wechatAccountId', $wechatAccountId)->where('wechatId', $momentEntity['userName'])->value('id');
+ }
+ // 如果不存在,则插入新数据
+ $dataToSave['wechatAccountId'] = $wechatAccountId;
+ $dataToSave['wechatFriendId'] = $wechatFriendId;
+ $dataToSave['create_time'] = time();
+ Db::table('s2_wechat_moments')->insert($dataToSave);
+ }
+ }
+
+ //Log::write('朋友圈数据已存入数据库,共' . count($momentList) . '条');
+ return true;
+ } catch (\Exception $e) {
+ //Log::write('保存朋友圈数据失败:' . $e->getMessage(), 'error');
+ return false;
+ }
+ }
+
+ /************************************
+ * 消息发送相关功能
+ ************************************/
+
+ /**
+ * 个人消息发送
+ * @return \think\response\Json
+ */
+ public function sendPersonal()
+ {
+ if ($this->request->isPost()) {
+ $data = $this->request->param();
+
+ if (empty($data)) {
+ return json_encode(['code'=>400,'msg'=>'参数缺失']);
+ }
+ $dataArray = $data;
+ if (!is_array($dataArray)) {
+ return json_encode(['code'=>400,'msg'=>'数据格式错误']);
+ }
+
+ //过滤消息
+ if (empty($dataArray['content'])) {
+ return json_encode(['code'=>400,'msg'=>'内容缺失']);
+ }
+ if (empty($dataArray['wechatAccountId'])) {
+ return json_encode(['code'=>400,'msg'=>'微信id不能为空']);
+ }
+ if (empty($dataArray['wechatFriendId'])) {
+ return json_encode(['code'=>400,'msg'=>'接收人不能为空']);
+ }
+
+ if (empty($dataArray['msgType'])) {
+ return json_encode(['code'=>400,'msg'=>'类型缺失']);
+ }
+
+ //消息拼接 msgType(1:文本 3:图片 43:视频 47:动图表情包 49:小程序)
+ $result = [
+ "cmdType" => "CmdSendMessage",
+ "content" => $dataArray['content'],
+ "msgSubType" => 0,
+ "msgType" => $dataArray['msgType'],
+ "seq" => time(),
+ "wechatAccountId" => $dataArray['wechatAccountId'],
+ "wechatChatroomId" => 0,
+ "wechatFriendId" => $dataArray['wechatFriendId'],
+ ];
+
+ $result = json_encode($result);
+ $this->client->send($result);
+ $message = $this->client->receive();
+ $message = json_decode($message, 1);
+ //关闭WS链接
+ $this->client->close();
+ //Log::write('WS个人消息发送');
+ return json_encode(['code'=>200,'msg'=>'消息成功发送','data'=>$message]);
+ //return successJson($message, '消息成功发送');
+ } else {
+ return json_encode(['code'=>400,'msg'=>'非法请求']);
+ //return errorJson('非法请求');
+ }
+ }
+
+ /**
+ * 发送群消息
+ * @return \think\response\Json
+ */
+ public function sendCommunity()
+ {
+ if ($this->request->isPost()) {
+ $data = $this->request->post();
+ if (empty($data)) {
+ return json_encode(['code'=>400,'msg'=>'参数缺失']);
+ }
+ $dataArray = $data;
+ if (!is_array($dataArray)) {
+ return json_encode(['code'=>400,'msg'=>'数据格式错误']);
+ }
+
+ //过滤消息
+ if (empty($dataArray['content'])) {
+ return json_encode(['code'=>400,'msg'=>'内容缺失']);
+ }
+ if (empty($dataArray['wechatAccountId'])) {
+ return json_encode(['code'=>400,'msg'=>'微信id不能为空']);
+ }
+
+ if (empty($dataArray['msgType'])) {
+ return json_encode(['code'=>400,'msg'=>'类型缺失']);
+ }
+ if (empty($dataArray['wechatChatroomId'])) {
+ return json_encode(['code'=>400,'msg'=>'群id不能为空']);
+ }
+
+ $msg = '消息成功发送';
+ $message = [];
+ try {
+ //消息拼接 msgType(1:文本 3:图片 43:视频 47:动图表情包 49:小程序)
+ $result = [
+ "cmdType" => "CmdSendMessage",
+ "content" => htmlspecialchars_decode($dataArray['content']),
+ "msgSubType" => 0,
+ "msgType" => $dataArray['msgType'],
+ "seq" => time(),
+ "wechatAccountId" => $dataArray['wechatAccountId'],
+ "wechatChatroomId" => $dataArray['wechatChatroomId'],
+ "wechatFriendId" => 0,
+ ];
+
+ $result = json_encode($result);
+ $this->client->send($result);
+ $message = $this->client->receive();
+ //关闭WS链接
+ $this->client->close();
+ //Log::write('WS群消息发送');
+ //Log::write($message);
+ $message = json_decode($message, 1);
+ } catch (\Exception $e) {
+ $msg = $e->getMessage();
+ }
+ return json_encode(['code'=>200,'msg'=>$msg,'data'=>$message]);
+
+ } else {
+ return json_encode(['code'=>400,'msg'=>'非法请求']);
+ //return errorJson('非法请求');
+ }
+ }
+
+ /**
+ * 发送群消息(内部调用版)
+ * @param array $data 消息数据
+ * @return \think\response\Json
+ */
+ public function sendCommunitys($data = [])
+ {
+ if (empty($data)) {
+ return json_encode(['code'=>400,'msg'=>'参数缺失']);
+ }
+ $dataArray = $data;
+ if (!is_array($dataArray)) {
+ return json_encode(['code'=>400,'msg'=>'数据格式错误']);
+ }
+
+ //过滤消息
+ if (empty($dataArray['content'])) {
+ return json_encode(['code'=>400,'msg'=>'内容缺失']);
+ }
+ if (empty($dataArray['wechatAccountId'])) {
+ return json_encode(['code'=>400,'msg'=>'微信id不能为空']);
+ }
+
+ if (empty($dataArray['msgType'])) {
+ return json_encode(['code'=>400,'msg'=>'类型缺失']);
+ }
+ if (empty($dataArray['wechatChatroomId'])) {
+ return json_encode(['code'=>400,'msg'=>'群id不能为空']);
+ }
+
+ $msg = '消息成功发送';
+ $message = [];
+ try {
+ //消息拼接 msgType(1:文本 3:图片 43:视频 47:动图表情包 49:小程序)
+ $result = [
+ "cmdType" => "CmdSendMessage",
+ "content" => $dataArray['content'],
+ "msgSubType" => 0,
+ "msgType" => $dataArray['msgType'],
+ "seq" => time(),
+ "wechatAccountId" => $dataArray['wechatAccountId'],
+ "wechatChatroomId" => $dataArray['wechatChatroomId'],
+ "wechatFriendId" => 0,
+ ];
+
+ $result = json_encode($result);
+ $this->client->send($result);
+ $message = $this->client->receive();
+ //关闭WS链接
+ $this->client->close();
+ //Log::write('WS群消息发送');
+ //Log::write($message);
+ $message = json_decode($message, 1);
+ } catch (\Exception $e) {
+ $msg = $e->getMessage();
+ }
+
+ return json_encode(['code'=>200,'msg'=>$msg,'data'=>$message]);
+ }
+}
\ No newline at end of file
diff --git a/application/api/controller/WechatChatroomController.php b/application/api/controller/WechatChatroomController.php
new file mode 100644
index 0000000..a1a0f9d
--- /dev/null
+++ b/application/api/controller/WechatChatroomController.php
@@ -0,0 +1,255 @@
+authorization;
+ if (empty($authorization)) {
+ return json_encode(['code'=>500,'msg'=>'缺少授权信息']);
+ }
+
+ try {
+ // 根据isDel设置对应的isDeleted值
+ $isDeleted = '';
+ if ($isDel === '0' || $isDel === 0) {
+ $isDeleted = false;
+ } elseif ($isDel === '1' || $isDel === 1) {
+ $isDeleted = true;
+ }
+
+ // 构建请求参数
+ $params = [
+ 'keyword' => $data['keyword'] ?? '',
+ 'wechatAccountKeyword' => $data['wechatAccountKeyword'] ?? '',
+ 'isDeleted' => $data['isDeleted'] ?? $isDeleted ,
+ 'allotAccountId' => $data['allotAccountId'] ?? '',
+ 'groupId' => $data['groupId'] ?? '',
+ 'wechatChatroomId' => $data['wechatChatroomId'] ?? '',
+ 'memberKeyword' => $data['memberKeyword'] ?? '',
+ 'pageIndex' => $data['pageIndex'] ?? 0,
+ 'pageSize' => $data['pageSize'] ?? 20
+ ];
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ // 发送请求获取群聊列表
+ $result = requestCurl($this->baseUrl . 'api/WechatChatroom/pagelist', $params, 'GET', $header,'json');
+ $response = handleApiResponse($result);
+
+ // 保存数据到数据库
+ if (!empty($response['results'])) {
+ $isUpdate = false;
+ foreach ($response['results'] as $item) {
+ $updated = $this->saveChatroom($item);
+ if($updated && $isDel == 0){
+ $isUpdate = true;
+ }
+ }
+ }
+
+ if($isInner){
+ return json_encode(['code'=>200,'msg'=>'success','data'=>$response,'isUpdate'=>$isUpdate]);
+ }else{
+ return successJson($response);
+ }
+ } catch (\Exception $e) {
+ if($isInner){
+ return json_encode(['code'=>200,'msg'=>'获取微信群聊列表失败' . $e->getMessage()]);
+ }else{
+ return errorJson('获取微信群聊列表失败:' . $e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * 保存群聊数据到数据库
+ * @param array $item 群聊数据
+ */
+ private function saveChatroom($item)
+ {
+ $data = [
+ 'id' => $item['id'],
+ 'wechatAccountId' => $item['wechatAccountId'],
+ 'wechatAccountAlias' => $item['wechatAccountAlias'],
+ 'wechatAccountWechatId' => $item['wechatAccountWechatId'],
+ 'wechatAccountAvatar' => $item['wechatAccountAvatar'],
+ 'wechatAccountNickname' => $item['wechatAccountNickname'],
+ 'chatroomId' => $item['chatroomId'],
+ 'hasMe' => $item['hasMe'],
+ 'chatroomOwnerNickname' => isset($item['chatroomOwnerNickname']) ? $item['chatroomOwnerNickname'] : '',
+ 'chatroomOwnerAvatar' => isset($item['chatroomOwnerAvatar']) ? $item['chatroomOwnerAvatar'] : '',
+ 'conRemark' => isset($item['conRemark']) ? $item['conRemark'] : '',
+ 'nickname' => isset($item['nickname']) ? $item['nickname'] : '',
+ 'pyInitial' => isset($item['pyInitial']) ? $item['pyInitial'] : '',
+ 'quanPin' => isset($item['quanPin']) ? $item['quanPin'] : '',
+ 'chatroomAvatar' => isset($item['chatroomAvatar']) ? $item['chatroomAvatar'] : '',
+ 'members' => is_array($item['members']) ? json_encode($item['members']) : json_encode([]),
+ 'isDeleted' => isset($item['isDeleted']) ? $item['isDeleted'] : 0,
+ 'deleteTime' => !empty($item['isDeleted']) ? strtotime($item['deleteTime']) : 0,
+ 'createTime' => isset($item['createTime']) ? strtotime($item['createTime']) : 0,
+ 'accountId' => isset($item['accountId']) ? $item['accountId'] : 0,
+ 'accountUserName' => isset($item['accountUserName']) ? $item['accountUserName'] : '',
+ 'accountRealName' => isset($item['accountRealName']) ? $item['accountRealName'] : '',
+ 'accountNickname' => isset($item['accountNickname']) ? $item['accountNickname'] : '',
+ 'groupId' => isset($item['groupId']) ? $item['groupId'] : 0,
+ 'updateTime' => time()
+ ];
+
+ // 使用chatroomId和wechatAccountId的组合作为唯一性判断
+ $chatroom = WechatChatroomModel::where('id',$item['id'])->find();
+
+ if ($chatroom) {
+ $chatroom->save($data);
+ return true;
+ } else {
+ WechatChatroomModel::create($data);
+ return false;
+ }
+
+ // // 同时保存群成员数据
+ // if (!empty($item['members'])) {
+ // foreach ($item['members'] as $member) {
+ // $this->saveChatroomMember($member, $item['chatroomId']);
+ // }
+ // }
+ }
+
+ /**
+ * 获取群成员列表
+ * @param string $wechatChatroomId 微信群ID
+ * @return \think\response\Json
+ */
+ public function listChatroomMember($wechatChatroomId = '',$chatroomId = '',$isInner = false)
+ {
+ // 获取授权token
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ $wechatChatroomId = !empty($wechatChatroomId) ? $wechatChatroomId : $this->request->param('id', '');
+ $chatroomId = !empty($chatroomId) ? $chatroomId : $this->request->param('chatroomId', '');
+
+
+ if (empty($authorization)) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'缺少授权信息']);
+ }else{
+ return errorJson('缺少授权信息');
+ }
+ }
+
+ if (empty($wechatChatroomId)) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'群ID不能为空']);
+ }else{
+ return errorJson('群ID不能为空');
+ }
+ }
+
+ try {
+ // 构建请求参数
+ $params = [
+ 'wechatChatroomId' => $wechatChatroomId
+ ];
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'plain');
+
+ // 发送请求获取群成员列表
+ $result = requestCurl($this->baseUrl . 'api/WechatChatroom/listChatroomMember', $params, 'GET', $header);
+ $response = handleApiResponse($result);
+
+ // 保存数据到数据库
+ if (!empty($response)) {
+ foreach ($response as $item) {
+ $this->saveChatroomMember($item, $chatroomId);
+ }
+ }
+
+ if($isInner){
+ return json_encode(['code'=>200,'msg'=>'success','data'=>$response]);
+ }else{
+ return successJson($response);
+ }
+ } catch (\Exception $e) {
+ if($isInner){
+ return json_encode(['code'=>500,'msg'=>'获取群成员列表失败:' . $e->getMessage()]);
+ }else{
+ return errorJson('获取群成员列表失败:' . $e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * 保存群成员数据到数据库
+ * @param array $item 群成员数据
+ * @param string $wechatChatroomId 微信群ID
+ */
+ private function saveChatroomMember($item, $wechatChatroomId)
+ {
+ $data = [
+ 'chatroomId' => $wechatChatroomId,
+ 'wechatId' => isset($item['wechatId']) ? $item['wechatId'] : '',
+ 'nickname' => isset($item['nickname']) ? $item['nickname'] : '',
+ 'avatar' => isset($item['avatar']) ? $item['avatar'] : '',
+ 'conRemark' => isset($item['conRemark']) ? $item['conRemark'] : '',
+ 'alias' => isset($item['alias']) ? $item['alias'] : '',
+ 'friendType' => isset($item['friendType']) ? $item['friendType'] : false,
+ 'updateTime' => time()
+ ];
+
+ // 使用chatroomId和wechatId的组合作为唯一性判断
+ $member = WechatChatroomMemberModel::where([
+ ['chatroomId', '=', $wechatChatroomId],
+ ['wechatId', '=', $item['wechatId']]
+ ])->find();
+
+ if ($member) {
+ $member->savea($data);
+ } else {
+ $data['createTime'] = time();
+ WechatChatroomMemberModel::create($data);
+ }
+ }
+
+ /**
+ * 同步微信群聊数据
+ * 此方法用于手动触发微信群聊数据同步任务
+ * @return \think\response\Json
+ */
+ public function syncChatrooms()
+ {
+ try {
+ // 获取请求参数
+ $pageIndex = $this->request->param('pageIndex', 0);
+ $pageSize = $this->request->param('pageSize', 100);
+ $keyword = $this->request->param('keyword', '');
+ $wechatAccountKeyword = $this->request->param('wechatAccountKeyword', '');
+ $isDeleted = $this->request->param('isDeleted', '');
+
+ // 添加同步任务到队列
+ $result = WechatChatroomJob::addSyncTask($pageIndex, $pageSize, $keyword, $wechatAccountKeyword, $isDeleted);
+
+ if ($result) {
+ return successJson([], '微信群聊同步任务已添加到队列');
+ } else {
+ return errorJson('添加同步任务失败');
+ }
+ } catch (\Exception $e) {
+ return errorJson('添加同步任务异常:' . $e->getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/api/controller/WechatController.php b/application/api/controller/WechatController.php
new file mode 100644
index 0000000..b3903bc
--- /dev/null
+++ b/application/api/controller/WechatController.php
@@ -0,0 +1,295 @@
+request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ if ($isInner) {
+ return json_encode(['code' => 500, 'msg' => '缺少授权信息']);
+ } else {
+ return errorJson('缺少授权信息');
+ }
+ }
+
+ try {
+ // 构建请求参数
+ $params = [
+ 'wechatAlive' => $this->request->param('wechatAlive', ''),
+ 'keyword' => $this->request->param('keyword', ''),
+ 'groupId' => $this->request->param('groupId', ''),
+ 'departmentId' => $this->request->param('departmentId', ''),
+ 'hasDevice' => $this->request->param('hasDevice', ''),
+ 'deviceGroupId' => $this->request->param('deviceGroupId', ''),
+ 'containSubDepartment' => $this->request->param('containSubDepartment', 'false'),
+ 'pageIndex' => !empty($pageIndex) ? $pageIndex : $this->request->param('pageIndex', 0),
+ 'pageSize' => !empty($pageSize) ? $pageSize : $this->request->param('pageSize', 10)
+ ];
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'plain');
+
+ // 发送请求获取基本信息
+ $result = requestCurl($this->baseUrl . 'api/WechatAccount/list', $params, 'GET', $header);
+ $response = handleApiResponse($result);
+ // 保存基本数据到数据库
+ if (!empty($response['results'])) {
+ foreach ($response['results'] as $item) {
+ $this->saveWechatAccount($item);
+ }
+
+ // 获取并更新微信账号状态信息
+ $this->getListTenantWechatPartial($authorization);
+ }
+
+
+
+ if ($isInner) {
+ return json_encode(['code' => 200, 'msg' => '获取微信账号列表成功', 'data' => $response]);
+ } else {
+ return successJson($response);
+ }
+ } catch (\Exception $e) {
+ if ($isInner) {
+ return json_encode(['code' => 500, 'msg' => '获取微信账号列表失败:' . $e->getMessage()]);
+ } else {
+ return errorJson('获取微信账号列表失败:' . $e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * 获取微信账号状态信息
+ *
+ * @param string $authorization 授权token
+ * @param int $pageIndex 页码,默认为1
+ * @param int $pageSize 每页数量,默认为40
+ * @return \think\response\Json|void
+ */
+ public function getListTenantWechatPartial($authorization = '', $pageIndex = 1, $pageSize = 40)
+ {
+ // 获取授权token(如果未传入)
+ if (empty($authorization)) {
+ $authorization = trim($this->request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ return errorJson('缺少授权信息');
+ }
+ }
+
+ try {
+ // 从数据库获取微信账号和设备信息
+ $wechatList = Db::table('s2_wechat_account')
+ ->where('imei', 'not null')
+ ->page($pageIndex, $pageSize)
+ ->select();
+ if (empty($wechatList)) {
+ return;
+ }
+
+ // 构造请求参数
+ $wechatAccountIds = [];
+ $deviceIds = [];
+ $accountIds = [];
+
+ foreach ($wechatList as $item) {
+ $wechatAccountIds[] = $item['id'];
+ $deviceIds[] = $item['currentDeviceId'] ?: 0;
+ $accountIds[] = $item['deviceAccountId'] ?: 0;
+ }
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+ $params = [
+ 'wechatAccountIdsStr' => json_encode($wechatAccountIds),
+ 'deviceIdsStr' => json_encode($deviceIds),
+ 'accountIdsStr' => json_encode($accountIds),
+ 'groupId' => ''
+ ];
+ // 发送请求获取状态信息
+ $result = requestCurl($this->baseUrl . 'api/WechatAccount/listTenantWechatPartial', $params, 'GET', $header,'json');
+ $response = handleApiResponse($result);
+ // 如果请求成功并返回数据,则更新数据库
+ if (!empty($response)) {
+ $this->batchUpdateWechatAccounts($response);
+ }
+
+ // 递归调用获取下一页数据
+ $this->getListTenantWechatPartial($authorization, $pageIndex + 1, $pageSize);
+
+ } catch (\Exception $e) {
+ if (empty($authorization)) { // 只有作为独立API调用时才返回
+ return json(['code' => 500, 'msg' => '获取失败:' . $e->getMessage()]);
+ }
+ }
+ }
+
+ /**
+ * 批量更新微信账号数据
+ *
+ * @param array $data 接口返回的数据
+ */
+ private function batchUpdateWechatAccounts($data)
+ {
+ // 更新微信账号信息
+ if (!empty($data['totalFriend'])) {
+ // 遍历所有微信账号ID
+ $wechatIds = array_keys($data['totalFriend']);
+ foreach ($wechatIds as $wechatId) {
+ // 构建更新数据
+ $updateData = [
+ 'maleFriend' => $data['maleFriend'][$wechatId] ?? 0,
+ 'femaleFriend' => $data['femaleFriend'][$wechatId] ?? 0,
+ 'unknowFriend' => $data['unknowFriend'][$wechatId] ?? 0,
+ 'totalFriend' => $data['totalFriend'][$wechatId] ?? 0,
+ 'yesterdayMsgCount' => $data['yesterdayMsgCount'][$wechatId] ?? 0,
+ 'sevenDayMsgCount' => $data['sevenDayMsgCount'][$wechatId] ?? 0,
+ 'thirtyDayMsgCount' => $data['thirtyDayMsgCount'][$wechatId] ?? 0,
+ 'wechatAlive' => isset($data['wechatAlive'][$wechatId]) ? (int)$data['wechatAlive'][$wechatId] : 0,
+ 'updateTime' => time()
+ ];
+
+ if (!empty($updateData['wechatAlive'])) {
+ $updateData['wechatAliveTime'] = time();
+ }
+
+
+ // 更新数据库
+ Db::table('s2_wechat_account')
+ ->where('id', $wechatId)
+ ->update($updateData);
+ }
+ }
+
+ // 更新设备状态
+ if (!empty($data['deviceAlive'])) {
+ foreach ($data['deviceAlive'] as $deviceId => $isAlive) {
+ // 更新微信账号的设备状态
+ Db::table('s2_wechat_account')
+ ->where('currentDeviceId', $deviceId)
+ ->update([
+ 'deviceAlive' => (int)$isAlive,
+ 'updateTime' => time()
+ ]);
+
+ // 更新设备表的状态
+ Db::table('s2_device')
+ ->where('id', $deviceId)
+ ->update([
+ 'alive' => (int)$isAlive,
+ 'updateTime' => time()
+ ]);
+ }
+ }
+ }
+
+ /**
+ * 保存微信账号基本数据到数据库
+ *
+ * @param array $item 微信账号数据
+ */
+ private function saveWechatAccount($item)
+ {
+ // 处理时间字段
+ $createTime = isset($item['createTime']) ? strtotime($item['createTime']) : 0;
+ $deleteTime = !empty($item['isDeleted']) ? strtotime($item['deleteTime']) : 0;
+
+ // 构建数据
+ $data = [
+ 'id' => $item['id'],
+ 'wechatId' => $item['wechatId'] ?? '',
+ 'deviceAccountId' => $item['deviceAccountId'] ?? 0,
+ 'imei' => $item['imei'] ?? '',
+ 'deviceMemo' => $item['deviceMemo'] ?? '',
+ 'accountUserName' => $item['accountUserName'] ?? '',
+ 'accountRealName' => $item['accountRealName'] ?? '',
+ 'accountNickname' => $item['accountNickname'] ?? '',
+ 'wechatGroupName' => $item['wechatGroupName'] ?? '',
+ 'alias' => $item['alias'] ?? '',
+ 'tenantId' => $item['tenantId'] ?? 0,
+ 'nickname' => $item['nickname'] ?? '',
+ 'avatar' => $item['avatar'] ?? '',
+ 'gender' => $item['gender'] ?? 0,
+ 'region' => $item['region'] ?? '',
+ 'signature' => $item['signature'] ?? '',
+ 'bindQQ' => $item['bindQQ'] ?? '',
+ 'bindEmail' => $item['bindEmail'] ?? '',
+ 'bindMobile' => $item['bindMobile'] ?? '',
+ 'currentDeviceId' => $item['currentDeviceId'] ?? 0,
+ 'isDeleted' => $item['isDeleted'] ?? 0,
+ 'groupId' => $item['groupId'] ?? 0,
+ 'memo' => $item['memo'] ?? '',
+ 'wechatVersion' => $item['wechatVersion'] ?? '',
+ 'labels' => !empty($item['labels']) ? json_encode($item['labels']) : json_encode([]),
+ 'createTime' => $createTime,
+ 'deleteTime' => $deleteTime,
+ 'updateTime' => time()
+ ];
+
+ // 保存或更新数据
+ $account = WechatAccountModel::where('id', $item['id'])->find();
+ if ($account) {
+ $account->save($data);
+ } else {
+ WechatAccountModel::create($data);
+ }
+ }
+
+
+ public function chatroomCreate($data = [])
+ {
+
+ $authorization = $this->authorization;
+
+ if (empty($authorization)) {
+ return json_encode(['code' => 500, 'msg' => '缺少授权信息']);
+ }
+
+ try {
+ // 设置请求头
+ $headerData = ['Client:system'];
+ $header = setHeader($headerData, $authorization,'json');
+ $params = [
+ "chatroomOperateType" => 7,
+ "extra" => "{chatroomName:{$data['chatroomName']}}",
+ "wechatAccountId" => $data['wechatAccountId'],
+ "wechatChatroomId" => 0,
+ "wechatFriendIds" => $data['wechatFriendIds']
+ ];
+
+ // 发送请求获取状态信息
+ $result = requestCurl($this->baseUrl . 'api/WechatChatroom/chatroomOperate', $params, 'POST', $header,'json');
+ $response = handleApiResponse($result);
+ if (!empty($response)) {
+ return json_encode(['code' => 500, 'msg' =>$response]);
+ }else{
+ return json_encode(['code' => 200, 'msg' =>'成功']);
+ }
+ } catch (\Exception $e) {
+ if (empty($authorization)) { // 只有作为独立API调用时才返回
+ return json_encode(['code' => 500, 'msg' => '获取失败:' . $e->getMessage()]);
+ }
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/application/api/controller/WechatFriendController.php b/application/api/controller/WechatFriendController.php
new file mode 100644
index 0000000..18fbe43
--- /dev/null
+++ b/application/api/controller/WechatFriendController.php
@@ -0,0 +1,165 @@
+request->header('authorization', $this->authorization));
+ if (empty($authorization)) {
+ if ($isInner) {
+ return json_encode(['code' => 500, 'msg' => '缺少授权信息']);
+ } else {
+ return errorJson('缺少授权信息');
+ }
+ }
+
+ $pageIndex = !empty($data['pageIndex']) ? $data['pageIndex'] : '';
+ $pageSize = !empty($data['pageSize']) ? $data['pageSize'] : '';
+ $preFriendId = !empty($data['preFriendId']) ? $data['preFriendId'] : '';
+ $friendKeyword = !empty($data['friendKeyword']) ? $data['friendKeyword'] : '';
+ $wechatAccountKeyword = !empty($data['wechatAccountKeyword']) ? $data['wechatAccountKeyword'] : '';
+
+
+ try {
+ // 初始化isUpdate标志为false
+ $isUpdate = false;
+
+ // 根据isDel设置对应的isDeleted值
+ $isDeleted = null; // 默认值
+ if ($isDel == '0' || $isDel == 0) {
+ $isDeleted = false;
+ } elseif ($isDel == '1' || $isDel == 1) {
+ $isDeleted = true;
+ }
+
+ // 构建请求参数
+ $params = [
+ 'accountKeyword' => '',
+ 'addFrom' => '[]',
+ 'allotAccountId' => input('allotAccountId', ''),
+ 'containSubDepartment' => false,
+ 'departmentId' => '',
+ 'extendFields' => '{}',
+ 'gender' => '',
+ 'groupId' => null,
+ 'isDeleted' => $isDeleted,
+ 'isPass' => null,
+ 'keyword' => input('keyword', ''),
+ 'labels' => '[]',
+ 'pageIndex' => !empty($pageIndex) ? $pageIndex : input('pageIndex', 0),
+ 'pageSize' => !empty($pageSize) ? $pageSize : input('pageSize', 20),
+ 'preFriendId' => !empty($preFriendId) ? $preFriendId : input('preFriendId', ''),
+ 'friendKeyword' => !empty($friendKeyword) ? $friendKeyword : input('friendKeyword', ''),
+ 'wechatAccountKeyword' => !empty($wechatAccountKeyword) ? $wechatAccountKeyword : input('wechatAccountKeyword', '')
+ ];
+
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization);
+
+ // 发送请求获取好友列表
+ $result = requestCurl($this->baseUrl . 'api/WechatFriend/friendlistData', $params, 'POST', $header, 'json');
+ $response = handleApiResponse($result);
+
+ // 保存数据到数据库
+ if (is_array($response)) {
+ $isUpdate = false;
+ foreach ($response as $item) {
+ $updated = $this->saveFriend($item);
+ if($updated && $isDel == 0){
+ $isUpdate = true;
+ }
+ }
+ }
+
+ if ($isInner) {
+ return json_encode(['code' => 200, 'msg' => 'success', 'data' => $response, 'isUpdate' => $isUpdate]);
+ } else {
+ return successJson($response);
+ }
+
+ } catch (\Exception $e) {
+ if ($isInner) {
+ return json_encode(['code' => 500, 'msg' => '获取微信好友列表失败:' . $e->getMessage()]);
+ } else {
+ return errorJson('获取微信好友列表失败:' . $e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * 保存微信好友数据到数据库
+ * @param array $item 微信好友数据
+ * @return bool 是否创建或更新了记录
+ */
+ private function saveFriend($item)
+ {
+ $data = [
+ 'id' => $item['id'],
+ 'wechatAccountId' => $item['wechatAccountId'],
+ 'alias' => $item['alias'],
+ 'wechatId' => $item['wechatId'],
+ 'conRemark' => $item['conRemark'],
+ 'nickname' => $item['nickname'],
+ 'pyInitial' => $item['pyInitial'],
+ 'quanPin' => $item['quanPin'],
+ 'avatar' => $item['avatar'],
+ 'gender' => $item['gender'],
+ 'region' => $item['region'],
+ 'addFrom' => $item['addFrom'],
+ 'labels' => is_array($item['labels']) ? json_encode($item['labels']) : json_encode([]),
+ 'siteLabels' => json_encode([]),
+ 'signature' => $item['signature'],
+ 'isDeleted' => $item['isDeleted'],
+ 'isPassed' => $item['isPassed'],
+ 'deleteTime' => !empty($item['isDeleted']) ? strtotime($item['deleteTime']) : 0,
+ 'accountId' => $item['accountId'],
+ 'extendFields' => is_array($item['extendFields']) ? json_encode($item['extendFields']) : json_encode([]),
+ 'accountUserName' => $item['accountUserName'],
+ 'accountRealName' => $item['accountRealName'],
+ 'accountNickname' => $item['accountNickname'],
+ 'ownerAlias' => $item['ownerAlias'],
+ 'ownerWechatId' => $item['ownerWechatId'],
+ 'ownerNickname' => $item['ownerNickname'],
+ 'ownerAvatar' => $item['ownerAvatar'],
+ 'phone' => $item['phone'],
+ 'thirdParty' => is_array($item['thirdParty']) ? json_encode($item['thirdParty']) : json_encode([]),
+ 'groupId' => $item['groupId'],
+ 'passTime' => !empty($item['isPassed']) && $item['passTime'] != '0001-01-01T00:00:00' ? strtotime($item['passTime']) : 0,
+ 'additionalPicture' => $item['additionalPicture'],
+ 'desc' => $item['desc'],
+ 'country' => $item['country'],
+ 'privince' => isset($item['privince']) ? $item['privince'] : '',
+ 'city' => isset($item['city']) ? $item['city'] : '',
+ 'createTime' => isset($item['createTime']) ? strtotime($item['createTime']) : 0,
+ 'updateTime' => time()
+ ];
+
+ // 使用ID作为唯一性判断
+ $friend = WechatFriendModel::where('id', $item['id'])->find();
+
+ if ($friend) {
+ unset($data['siteLabels']);
+ $friend->save($data);
+ return true;
+ } else {
+ WechatFriendModel::create($data);
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/api/model/AllotRuleModel.php b/application/api/model/AllotRuleModel.php
new file mode 100644
index 0000000..5fa4b6d
--- /dev/null
+++ b/application/api/model/AllotRuleModel.php
@@ -0,0 +1,11 @@
+ 'integer',
+ 'tenantId' => 'integer',
+ 'count' => 'integer',
+ 'createTime' => 'integer',
+ 'updateTime' => 'integer'
+ ];
+}
\ No newline at end of file
diff --git a/application/api/model/DeviceModel.php b/application/api/model/DeviceModel.php
new file mode 100644
index 0000000..168b129
--- /dev/null
+++ b/application/api/model/DeviceModel.php
@@ -0,0 +1,10 @@
+ ['ownerWechatId', 'wechatId','wechatAccountId'] // uk_owner_wechat_account 是数据库中组合唯一键的名称
+ ];*/
+}
\ No newline at end of file
diff --git a/application/api/model/WechatMessageModel.php b/application/api/model/WechatMessageModel.php
new file mode 100644
index 0000000..59c937e
--- /dev/null
+++ b/application/api/model/WechatMessageModel.php
@@ -0,0 +1,11 @@
+middleware(['jwt']);
+
+
+// 客服登录
+Route::group('v1/kefu', function () {
+ Route::post('login', 'app\chukebao\controller\LoginController@index'); // 登录
+});
+
+
+
+
+
+return [];
\ No newline at end of file
diff --git a/application/chukebao/controller/AccountsController.php b/application/chukebao/controller/AccountsController.php
new file mode 100644
index 0000000..d65e998
--- /dev/null
+++ b/application/chukebao/controller/AccountsController.php
@@ -0,0 +1,71 @@
+getUserInfo('companyId');
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 401);
+ }
+
+ if (empty($companyId)) {
+ return ResponseHelper::error('请先登录', 401);
+ }
+
+ $page = max(1, intval($this->request->param('page', 1)));
+ $limit = max(1, intval($this->request->param('limit', 10)));
+ $keyword = trim((string)$this->request->param('keyword', ''));
+
+ $query = Db::table('s2_company_account')
+ ->alias('a')
+ ->join('users u', 'a.id = u.s2_accountId')
+ ->where([
+ ['a.departmentId', '=', $companyId],
+ ['a.status', '=', 0],
+ ])
+ ->whereNotLike('a.userName', '%_offline')
+ ->whereNotLike('a.userName', '%_delete');
+
+ if ($keyword !== '') {
+ $query->where(function ($subQuery) use ($keyword) {
+ $likeKeyword = '%' . $keyword . '%';
+ $subQuery->whereLike('a.userName', $likeKeyword)
+ ->whereOrLike('a.realName', $likeKeyword)
+ ->whereOrLike('a.nickname', $likeKeyword);
+ });
+ }
+
+ $total = (clone $query)->count();
+ $list = $query->field([
+ 'a.id',
+ 'u.id as uid',
+ 'a.userName',
+ 'a.realName',
+ 'a.nickname',
+ 'a.departmentId',
+ 'a.departmentName',
+ 'a.avatar'
+ ])
+ ->order('a.id', 'desc')
+ ->page($page, $limit)
+ ->select();
+
+
+
+ return ResponseHelper::success([
+ 'total' => $total,
+ 'list' => $list,
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/application/chukebao/controller/AiChatController.php b/application/chukebao/controller/AiChatController.php
new file mode 100644
index 0000000..7b4dd78
--- /dev/null
+++ b/application/chukebao/controller/AiChatController.php
@@ -0,0 +1,832 @@
+validateAndInitParams();
+
+ if ($params === false) {
+ return ResponseHelper::error('参数验证失败');
+ }
+
+ // 并发控制:检查并处理同一用户的重复请求
+ $this->requestKey = "aichat_{$params['friendId']}_{$params['wechatAccountId']}";
+ $this->requestId = uniqid('req_', true);
+
+ $concurrentCheck = $this->handleConcurrentRequest($params);
+ if ($concurrentCheck !== true) {
+ return $concurrentCheck; // 返回错误响应
+ }
+
+ $this->currentStep = 1;
+
+ // 2. 验证Tokens余额
+ $this->updateRequestStep(2);
+ if ($this->isRequestCanceled()) {
+ return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消');
+ }
+ $hasBalance = $this->checkTokensBalance($params['companyId']);
+
+ if (!$hasBalance) {
+ $this->clearRequestCache();
+ return ResponseHelper::error('Tokens余额不足,请充值后再试');
+ }
+
+ // 3. 获取AI配置
+ $this->updateRequestStep(3);
+ if ($this->isRequestCanceled()) {
+ return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消');
+ }
+ $setting = $this->getAiSettings($params['companyId']);
+
+ if (!$setting) {
+ $this->clearRequestCache();
+ return ResponseHelper::error('未找到AI配置信息,请先配置AI策略');
+ }
+
+ // 4. 获取好友AI设置
+ $this->updateRequestStep(4);
+ if ($this->isRequestCanceled()) {
+ return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消');
+ }
+ $friendSettings = $this->getFriendSettings($params['companyId'], $params['friendId']);
+
+ if (!$friendSettings) {
+ $this->clearRequestCache();
+ return ResponseHelper::error('该好友未配置或未开启AI功能');
+ }
+
+ // 5. 确保会话存在
+ $this->updateRequestStep(5);
+ if ($this->isRequestCanceled()) {
+ return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消');
+ }
+ $conversationId = $this->ensureConversation($friendSettings, $setting, $params);
+
+ if (empty($conversationId)) {
+ $this->clearRequestCache();
+ return ResponseHelper::error('创建会话失败');
+ }
+
+ // 6. 获取历史消息
+ $this->updateRequestStep(6);
+ if ($this->isRequestCanceled()) {
+ return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消');
+ }
+ $msgData = $this->getHistoryMessages($params['friendId'], $friendSettings);
+
+ // 7. 创建AI对话(从这步开始需要保存对话ID以便取消)
+ $this->updateRequestStep(7);
+ if ($this->isRequestCanceled($conversationId, null)) {
+ return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消');
+ }
+ $chatId = $this->createAiChat($setting, $friendSettings, $msgData);
+
+ if (empty($chatId)) {
+ $this->clearRequestCache();
+ return ResponseHelper::error('创建对话失败');
+ }
+
+ // 保存对话ID到缓存,以便新请求可以取消
+ $this->updateRequestStep(7, $conversationId, $chatId);
+
+ // 8. 等待AI处理完成(轮询)
+ $this->updateRequestStep(8, $conversationId, $chatId);
+ if ($this->isRequestCanceled($conversationId, $chatId)) {
+ return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消');
+ }
+ $chatResult = $this->waitForChatCompletion($conversationId, $chatId);
+
+ if (!$chatResult['success']) {
+ $this->clearRequestCache();
+ return ResponseHelper::error($chatResult['error']);
+ }
+
+ $chatResult = $chatResult['data'];
+
+ // 9. 扣除Tokens
+ $this->updateRequestStep(9, $conversationId, $chatId);
+ if ($this->isRequestCanceled($conversationId, $chatId)) {
+ return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消');
+ }
+ $this->consumeTokens($chatResult, $params, $friendSettings);
+
+ // 10. 获取对话消息
+ $this->updateRequestStep(10, $conversationId, $chatId);
+ if ($this->isRequestCanceled($conversationId, $chatId)) {
+ return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消');
+ }
+ $messages = $this->getChatMessages($conversationId, $chatId);
+
+ if (!$messages) {
+ return ResponseHelper::error('获取对话消息失败');
+ }
+
+ // 筛选type为answer的消息(AI回复的内容)
+ $answerContent = '';
+ foreach ($messages as $msg) {
+ if (isset($msg['type']) && $msg['type'] === 'answer') {
+ $answerContent = $msg['content'] ?? '';
+ break;
+ }
+ }
+
+ if (empty($answerContent)) {
+ Log::warning('未找到AI回复内容,messages: ' . json_encode($messages));
+ return ResponseHelper::error('未获取到AI回复内容');
+ }
+
+ // 清理请求缓存
+ $this->clearRequestCache();
+
+ // 返回结果
+ return ResponseHelper::success(['content' => $answerContent], '对话成功');
+
+ } catch (\Exception $e) {
+ Log::error('AI聊天异常:' . $e->getMessage());
+
+ // 清理请求缓存
+ $this->clearRequestCache();
+
+ return ResponseHelper::error('系统异常:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 取消AI对话
+ * 取消当前正在进行的AI对话请求
+ *
+ * @return \think\response\Json
+ */
+ public function cancel()
+ {
+ try {
+ // 获取参数
+ $friendId = $this->request->param('friendId', '');
+ $wechatAccountId = $this->request->param('wechatAccountId', '');
+
+ if (empty($wechatAccountId) || empty($friendId)) {
+ return ResponseHelper::error('参数缺失');
+ }
+
+ // 生成缓存键
+ $requestKey = "aichat_{$friendId}_{$wechatAccountId}";
+
+ // 获取缓存数据
+ $cacheData = Cache::get($requestKey);
+
+ if (!$cacheData) {
+ return ResponseHelper::error('当前没有正在进行的AI对话');
+ }
+
+ $requestId = $cacheData['request_id'] ?? '';
+ $step = $cacheData['step'] ?? 0;
+ $conversationId = $cacheData['conversation_id'] ?? '';
+ $chatId = $cacheData['chat_id'] ?? '';
+
+ Log::info("手动取消AI对话 - 请求ID: {$requestId}, 步骤: {$step}");
+
+ // 如果已经到达步骤7或之后,需要调用取消API
+ if ($step >= 7 && !empty($conversationId) && !empty($chatId)) {
+ try {
+ $cozeAI = new CozeAI();
+ $cancelResult = $cozeAI->cancelConversationChat([
+ 'conversation_id' => $conversationId,
+ 'chat_id' => $chatId,
+ ]);
+
+ $result = json_decode($cancelResult, true);
+ if ($result['code'] != 200) {
+ Log::error("调用取消API失败 - conversation_id: {$conversationId}, chat_id: {$chatId}, 错误: " . ($result['msg'] ?? '未知错误'));
+ } else {
+ Log::info("成功调用取消API - conversation_id: {$conversationId}, chat_id: {$chatId}");
+ }
+ } catch (\Exception $e) {
+ Log::error("调用取消API异常:" . $e->getMessage());
+ }
+ }
+
+ // 清理缓存
+ Cache::rm($requestKey);
+ Log::info("已清理AI对话缓存 - 请求ID: {$requestId}");
+
+ return ResponseHelper::success([
+ 'canceled_request_id' => $requestId,
+ 'step' => $step
+ ], 'AI对话已取消');
+
+ } catch (\Exception $e) {
+ Log::error('取消AI对话异常:' . $e->getMessage());
+ return ResponseHelper::error('系统异常:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 验证和初始化参数
+ *
+ * @return array|false
+ */
+ private function validateAndInitParams()
+ {
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+ $friendId = $this->request->param('friendId', '');
+ $wechatAccountId = $this->request->param('wechatAccountId', '');
+
+ if (empty($wechatAccountId) || empty($friendId)) {
+ return false;
+ }
+
+ return [
+ 'userId' => $userId,
+ 'companyId' => $companyId,
+ 'friendId' => $friendId,
+ 'wechatAccountId' => $wechatAccountId
+ ];
+ }
+
+ /**
+ * 检查Tokens余额
+ *
+ * @param int $companyId 公司ID
+ * @return bool
+ */
+ private function checkTokensBalance($companyId)
+ {
+ $tokens = TokensCompany::where(['companyId' => $companyId])->value('tokens');
+ return !empty($tokens) && $tokens > 1000;
+ }
+
+ /**
+ * 获取AI配置
+ *
+ * @param int $companyId 公司ID
+ * @return AiSettings|null
+ */
+ private function getAiSettings($companyId)
+ {
+ return AiSettings::where(['companyId' => $companyId])->find();
+ }
+
+ /**
+ * 获取好友AI设置
+ *
+ * @param int $companyId 公司ID
+ * @param string $friendId 好友ID
+ * @return FriendSettings|null
+ */
+ private function getFriendSettings($companyId, $friendId)
+ {
+ $friendSettings = FriendSettings::where([
+ 'companyId' => $companyId,
+ 'friendId' => $friendId
+ ])->find();
+
+ if (empty($friendSettings) || $friendSettings->type == 0) {
+ return null;
+ }
+
+ return $friendSettings;
+ }
+
+ /**
+ * 确保会话存在
+ *
+ * @param FriendSettings $friendSettings 好友设置
+ * @param AiSettings $setting AI设置
+ * @param array $params 参数
+ * @return string|null 会话ID
+ */
+ private function ensureConversation($friendSettings, $setting, $params)
+ {
+ if (!empty($friendSettings->conversationId)) {
+ return $friendSettings->conversationId;
+ }
+
+ // 创建新会话
+ $cozeAI = new CozeAI();
+ $data = [
+ 'bot_id' => $setting->botId,
+ 'name' => '与好友' . $params['friendId'] . '的对话',
+ 'meta_data' => [
+ 'friendId' => (string)$friendSettings->friendId,
+ 'wechatAccountId' => (string)$params['wechatAccountId'],
+ ],
+ ];
+
+ $res = $cozeAI->createConversation($data);
+ $res = json_decode($res, true);
+
+ if ($res['code'] != 200) {
+ Log::error('创建会话失败:' . ($res['msg'] ?? '未知错误'));
+ return null;
+ }
+
+ // 保存会话ID
+ $conversationId = $res['data']['id'];
+ $friendSettings->conversationId = $conversationId;
+ $friendSettings->conversationTime = time();
+ $friendSettings->save();
+ return $conversationId;
+ }
+
+ /**
+ * 获取历史消息
+ *
+ * @param string $friendId 好友ID
+ * @param FriendSettings $friendSettings 好友设置
+ * @return array
+ */
+ private function getHistoryMessages($friendId, $friendSettings)
+ {
+ $msgData = [];
+
+ // 会话创建时间小于1分钟,加载最近10条消息
+ if ($friendSettings->conversationTime >= time() - 60) {
+ $messages = Db::table('s2_wechat_message')
+ ->where('wechatFriendId', $friendId)
+ ->where('msgType', '<', 50)
+ ->order('wechatTime desc')
+ ->field('id,content,msgType,isSend,wechatTime')
+ ->limit(10)
+ ->select();
+
+ // 按时间正序排列
+ usort($messages, function ($a, $b) {
+ return $a['wechatTime'] <=> $b['wechatTime'];
+ });
+
+ // 处理聊天数据
+ foreach ($messages as $val) {
+ if (empty($val['content'])) {
+ continue;
+ }
+
+ $msg = [
+ 'role' => empty($val['isSend']) ? 'user' : 'assistant',
+ 'content' => $val['content'],
+ 'type' => empty($val['isSend']) ? 'question' : 'answer',
+ 'content_type' => 'text'
+ ];
+ $msgData[] = $msg;
+ }
+ } else {
+ // 只加载最新一条用户消息
+ $message = Db::table('s2_wechat_message')
+ ->where('wechatFriendId', $friendId)
+ ->where('msgType', '<', 50)
+ ->where('isSend', 0)
+ ->order('wechatTime desc')
+ ->field('id,content,msgType,isSend,wechatTime')
+ ->find();
+
+ if (!empty($message) && !empty($message['content'])) {
+ $msgData[] = [
+ 'role' => 'user',
+ 'content' => $message['content'],
+ 'type' => 'question',
+ 'content_type' => 'text'
+ ];
+ }
+ }
+
+ return $msgData;
+ }
+
+ /**
+ * 创建AI对话
+ *
+ * @param AiSettings $setting AI设置
+ * @param FriendSettings $friendSettings 好友设置
+ * @param array $msgData 消息数据
+ * @return string|null 对话ID
+ */
+ private function createAiChat($setting, $friendSettings, $msgData)
+ {
+ $cozeAI = new CozeAI();
+ $data = [
+ 'bot_id' => $setting->botId,
+ 'uid' => $friendSettings->friendId,
+ 'conversation_id' => $friendSettings->conversationId,
+ 'question' => $msgData,
+ ];
+
+ $res = $cozeAI->createChat($data);
+ $res = json_decode($res, true);
+
+ if ($res['code'] != 200) {
+ Log::error('创建对话失败:' . ($res['msg'] ?? '未知错误'));
+ return null;
+ }
+
+ return $res['data']['id'];
+ }
+
+ /**
+ * 等待AI处理完成(轮询机制)
+ *
+ * @param string $conversationId 会话ID
+ * @param string $chatId 对话ID
+ * @return array ['success' => bool, 'data' => array|null, 'error' => string]
+ */
+ private function waitForChatCompletion($conversationId, $chatId)
+ {
+ $cozeAI = new CozeAI();
+ $retryCount = 0;
+
+ while ($retryCount < self::MAX_RETRY_TIMES) {
+ // 获取对话状态
+ $res = $cozeAI->getConversationChat([
+ 'conversation_id' => $conversationId,
+ 'chat_id' => $chatId,
+ ]);
+ $res = json_decode($res, true);
+
+ if ($res['code'] != 200) {
+ $errorMsg = 'AI接口调用失败:' . ($res['msg'] ?? '未知错误');
+ Log::error($errorMsg);
+ return ['success' => false, 'data' => null, 'error' => $errorMsg];
+ }
+
+ $status = $res['data']['status'] ?? '';
+
+ // 处理不同的状态
+ switch ($status) {
+ case self::STATUS_COMPLETED:
+ // 对话完成,返回结果
+ return ['success' => true, 'data' => $res['data'], 'error' => ''];
+
+ case self::STATUS_IN_PROGRESS:
+ case self::STATUS_CREATED:
+ // 继续等待
+ $retryCount++;
+ usleep(self::RETRY_INTERVAL);
+ break;
+
+ case self::STATUS_FAILED:
+ $errorMsg = 'AI对话处理失败';
+ Log::error($errorMsg . ',chat_id: ' . $chatId);
+ return ['success' => false, 'data' => null, 'error' => $errorMsg];
+
+ case self::STATUS_CANCELED:
+ $errorMsg = 'AI对话已被取消';
+ Log::error($errorMsg . ',chat_id: ' . $chatId);
+ return ['success' => false, 'data' => null, 'error' => $errorMsg];
+
+ case self::STATUS_REQUIRES_ACTION:
+ $errorMsg = 'AI对话需要进一步处理';
+ Log::warning($errorMsg . ',chat_id: ' . $chatId);
+ return ['success' => false, 'data' => null, 'error' => $errorMsg];
+
+ default:
+ $errorMsg = 'AI返回未知状态:' . $status;
+ Log::error($errorMsg);
+ return ['success' => false, 'data' => null, 'error' => $errorMsg];
+ }
+ }
+
+ // 超时
+ $errorMsg = 'AI对话处理超时,已等待' . (self::MAX_RETRY_TIMES * self::RETRY_INTERVAL / 1000000) . '秒';
+ Log::error($errorMsg . ',chat_id: ' . $chatId);
+ return ['success' => false, 'data' => null, 'error' => $errorMsg];
+ }
+
+ /**
+ * 扣除Tokens
+ *
+ * @param array $chatResult 对话结果
+ * @param array $params 参数
+ * @param FriendSettings $friendSettings 好友设置
+ */
+ private function consumeTokens($chatResult, $params, $friendSettings)
+ {
+ $tokenCount = $chatResult['usage']['token_count'] ?? 0;
+
+ if (empty($tokenCount)) {
+ return;
+ }
+
+ // 获取好友昵称
+ $nickname = WechatFriendModel::where('id', $friendSettings->friendId)->value('nickname');
+ $remarks = !empty($nickname) ? '与好友【' . $nickname . '】聊天' : '与好友聊天';
+
+ // 扣除Tokens
+ $tokensRecord = new tokensRecord();
+ $data = [
+ 'tokens' => $tokenCount * 20,
+ 'type' => 0,
+ 'form' => 13,
+ 'wechatAccountId' => $params['wechatAccountId'],
+ 'friendIdOrGroupId' => $params['friendId'],
+ 'remarks' => $remarks,
+ ];
+
+ $tokensRecord->consumeTokens($data);
+ }
+
+ /**
+ * 获取对话消息
+ *
+ * @param string $conversationId 会话ID
+ * @param string $chatId 对话ID
+ * @return array|null
+ */
+ private function getChatMessages($conversationId, $chatId)
+ {
+ $cozeAI = new CozeAI();
+ $res = $cozeAI->listConversationMessage([
+ 'conversation_id' => $conversationId,
+ 'chat_id' => $chatId,
+ ]);
+ $res = json_decode($res, true);
+
+ if ($res['code'] != 200) {
+ Log::error('获取对话消息失败:' . ($res['msg'] ?? '未知错误'));
+ return null;
+ }
+
+ return $res['data'] ?? [];
+ }
+
+ /**
+ * 处理并发请求
+ * 检查是否有同一用户的旧请求正在处理,如果有则取消旧请求
+ *
+ * @param array $params 请求参数
+ * @return true|\think\response\Json true表示可以继续,否则返回错误响应
+ */
+ private function handleConcurrentRequest($params)
+ {
+ $cacheData = Cache::get($this->requestKey);
+
+ if ($cacheData) {
+ // 有旧请求正在处理
+ $oldRequestId = $cacheData['request_id'] ?? '';
+ $oldStep = $cacheData['step'] ?? 0;
+ $oldConversationId = $cacheData['conversation_id'] ?? '';
+ $oldChatId = $cacheData['chat_id'] ?? '';
+
+ Log::info("检测到并发请求 - 旧请求: {$oldRequestId} (步骤{$oldStep}), 新请求: {$this->requestId}");
+
+ // 如果旧请求已经到达步骤7或之后,需要调用取消API
+ if ($oldStep >= 7 && !empty($oldConversationId) && !empty($oldChatId)) {
+ try {
+ $cozeAI = new CozeAI();
+ $cancelResult = $cozeAI->cancelConversationChat([
+ 'conversation_id' => $oldConversationId,
+ 'chat_id' => $oldChatId,
+ ]);
+ Log::info("已调用取消API取消旧请求的对话 - conversation_id: {$oldConversationId}, chat_id: {$oldChatId}");
+ } catch (\Exception $e) {
+ Log::error("取消旧请求对话失败:" . $e->getMessage());
+ }
+ }
+
+ // 标记旧请求为已取消(通过更新缓存的 canceled 标志)
+ $cacheData['canceled'] = true;
+ $cacheData['canceled_by'] = $this->requestId;
+ Cache::set($this->requestKey, $cacheData, self::CACHE_EXPIRE);
+ }
+
+ // 设置当前请求为活动请求
+ $newCacheData = [
+ 'request_id' => $this->requestId,
+ 'step' => 1,
+ 'start_time' => time(),
+ 'canceled' => false,
+ 'conversation_id' => '',
+ 'chat_id' => '',
+ ];
+ Cache::set($this->requestKey, $newCacheData, self::CACHE_EXPIRE);
+
+ return true;
+ }
+
+ /**
+ * 检查当前请求是否被新请求取消
+ *
+ * @param string $conversationId 会话ID(可选,用于取消对话)
+ * @param string $chatId 对话ID(可选,用于取消对话)
+ * @return bool
+ */
+ private function isRequestCanceled($conversationId = '', $chatId = '')
+ {
+ $cacheData = Cache::get($this->requestKey);
+
+ if (!$cacheData) {
+ // 缓存不存在,说明被清理或过期,视为被取消
+ return true;
+ }
+
+ $currentRequestId = $cacheData['request_id'] ?? '';
+ $isCanceled = $cacheData['canceled'] ?? false;
+
+ // 如果缓存中的请求ID与当前请求ID不一致,或者被标记为取消
+ if ($currentRequestId !== $this->requestId || $isCanceled) {
+ Log::info("当前请求已被取消 - 请求ID: {$this->requestId}, 缓存请求ID: {$currentRequestId}, 取消标志: " . ($isCanceled ? 'true' : 'false'));
+
+ // 如果提供了对话ID,尝试取消对话
+ if (!empty($conversationId) && !empty($chatId) && $this->currentStep >= 7) {
+ try {
+ $cozeAI = new CozeAI();
+ $cancelResult = $cozeAI->cancelConversationChat([
+ 'conversation_id' => $conversationId,
+ 'chat_id' => $chatId,
+ ]);
+ Log::info("已取消当前请求的对话 - conversation_id: {$conversationId}, chat_id: {$chatId}");
+ } catch (\Exception $e) {
+ Log::error("取消当前请求对话失败:" . $e->getMessage());
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 更新请求步骤
+ *
+ * @param int $step 当前步骤
+ * @param string $conversationId 会话ID(可选)
+ * @param string $chatId 对话ID(可选)
+ */
+ private function updateRequestStep($step, $conversationId = '', $chatId = '')
+ {
+ $this->currentStep = $step;
+
+ $cacheData = Cache::get($this->requestKey);
+
+ if ($cacheData && $cacheData['request_id'] === $this->requestId) {
+ $cacheData['step'] = $step;
+ $cacheData['update_time'] = time();
+
+ if (!empty($conversationId)) {
+ $cacheData['conversation_id'] = $conversationId;
+ }
+ if (!empty($chatId)) {
+ $cacheData['chat_id'] = $chatId;
+ }
+
+ Cache::set($this->requestKey, $cacheData, self::CACHE_EXPIRE);
+ }
+ }
+
+ /**
+ * 清理请求缓存
+ */
+ private function clearRequestCache()
+ {
+ if (!empty($this->requestKey)) {
+ $cacheData = Cache::get($this->requestKey);
+
+ // 只有当前请求才能清理自己的缓存
+ if ($cacheData && isset($cacheData['request_id']) && $cacheData['request_id'] === $this->requestId) {
+ Cache::rm($this->requestKey);
+ Log::info("已清理请求缓存 - 请求ID: {$this->requestId}");
+ }
+ }
+ }
+
+
+ public function index2222()
+ {
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+ $friendId = $this->request->param('friendId', '');
+ $wechatAccountId = $this->request->param('wechatAccountId', '');
+ $content = $this->request->param('content', '');
+
+ if (empty($wechatAccountId) || empty($friendId)) {
+ return ResponseHelper::error('参数缺失');
+ }
+
+ $tokens = TokensCompany::where(['companyId' => $companyId])->value('tokens');
+ if (empty($tokens) || $tokens <= 0) {
+ return ResponseHelper::error('用户Tokens余额不足');
+ }
+
+
+ //读取AI配置
+ $setting = Db::name('ai_settings')->where(['companyId' => $companyId, 'userId' => $userId])->find();
+ if (empty($setting)) {
+ return ResponseHelper::error('未找到配置信息,请先配置AI策略');
+ }
+ $config = json_decode($setting['config'], true);
+ $modelSetting = $config['modelSetting'];
+ $round = isset($config['round']) ? $config['round'] : 10;
+
+
+ // 导出聊天
+ $messages = Db::table('s2_wechat_message')
+ ->where('wechatFriendId', $friendId)
+ ->order('wechatTime desc')
+ ->field('id,content,msgType,isSend,wechatTime')
+ ->limit($round)
+ ->select();
+
+ usort($messages, function ($a, $b) {
+ return $a['wechatTime'] <=> $b['wechatTime'];
+ });
+
+ //处理聊天数据
+ $msg = [];
+ foreach ($messages as $val) {
+ if (empty($val['content'])) {
+ continue;
+ }
+ if (!empty($val['isSend'])) {
+ $msg[] = '客服:' . $val['content'];
+ } else {
+ $msg[] = '用户:' . $val['content'];
+ }
+ }
+ $content = implode("\n", $msg);
+
+
+ $params = [
+ 'model' => 'doubao-1-5-pro-32k-250115',
+ 'messages' => [
+ // ['role' => 'system', 'content' => '请完成跟客户的对话'],
+ ['role' => 'system', 'content' => '角色设定:' . $modelSetting['role']],
+ ['role' => 'system', 'content' => '公司背景:' . $modelSetting['businessBackground']],
+ ['role' => 'system', 'content' => '对话风格:' . $modelSetting['dialogueStyle']],
+ ['role' => 'user', 'content' => $content],
+ ],
+ ];
+
+ //AI处理
+ $ai = new DouBaoAI();
+ $res = $ai->text($params);
+ $res = json_decode($res, true);
+
+ if ($res['code'] == 200) {
+ //扣除Tokens
+ $tokensRecord = new tokensRecord();
+ $nickname = Db::table('s2_wechat_friend')->where(['id' => $friendId])->value('nickname');
+ $remarks = !empty($nickname) ? '与好友【' . $nickname . '】聊天' : '与好友聊天';
+ $data = [
+ 'tokens' => $res['data']['token'],
+ 'type' => 0,
+ 'form' => 13,
+ 'wechatAccountId' => $wechatAccountId,
+ 'friendIdOrGroupId' => $friendId,
+ 'remarks' => $remarks,
+ ];
+ $tokensRecord->consumeTokens($data);
+ return ResponseHelper::success($res['data']['content']);
+ } else {
+ return ResponseHelper::error($res['msg']);
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/application/chukebao/controller/AiPushController.php b/application/chukebao/controller/AiPushController.php
new file mode 100644
index 0000000..b1cecbd
--- /dev/null
+++ b/application/chukebao/controller/AiPushController.php
@@ -0,0 +1,505 @@
+request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ $where = [
+ ['companyId', '=', $companyId],
+ ['userId', '=', $userId],
+ ['isDel', '=', 0],
+ ];
+
+ if (!empty($keyword)) {
+ $where[] = ['name', 'like', '%' . $keyword . '%'];
+ }
+
+ $query = AiPush::where($where);
+ $total = $query->count();
+ $list = $query->where($where)->page($page, $limit)->order('id desc')->select();
+
+ // 处理数据
+ $list = is_array($list) ? $list : $list->toArray();
+ foreach ($list as &$item) {
+ // 解析标签数组
+ $item['tags'] = json_decode($item['tags'], true);
+ if (!is_array($item['tags'])) {
+ $item['tags'] = [];
+ }
+ // 格式化推送时机显示文本
+ $timingTypes = [
+ 1 => '立即推送',
+ 2 => 'AI最佳时机',
+ 3 => '定时推送'
+ ];
+ $item['timingText'] = $timingTypes[$item['pushTiming']] ?? '未知';
+ // 处理定时推送时间
+ if ($item['pushTiming'] == 3 && !empty($item['scheduledTime'])) {
+ $item['scheduledTime'] = date('Y-m-d H:i:s', $item['scheduledTime']);
+ } else {
+ $item['scheduledTime'] = '';
+ }
+ // 从记录表计算实际成功率
+ $pushId = $item['id'];
+ $totalCount = Db::name('kf_ai_push_record')
+ ->where('pushId', $pushId)
+ ->count();
+ $sendCount = Db::name('kf_ai_push_record')
+ ->where('pushId', $pushId)
+ ->where('isSend', 1)
+ ->count();
+ $item['successRate'] = $totalCount > 0 ? round(($sendCount * 100) / $totalCount, 1) : 0;
+ $item['totalPushCount'] = $totalCount; // 推送总数
+ $item['sendCount'] = $sendCount; // 成功发送数
+ }
+ unset($item);
+
+ return ResponseHelper::success(['list' => $list, 'total' => $total]);
+ }
+
+ /**
+ * 添加
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function add()
+ {
+ $name = $this->request->param('name', '');
+ $tags = $this->request->param('tags', ''); // 标签,支持逗号分隔的字符串或数组
+ $content = $this->request->param('content', '');
+ $pushTiming = $this->request->param('pushTiming', 1); // 1=立即推送,2=最佳时机(AI决定),3=定时推送
+ $scheduledTime = $this->request->param('scheduledTime', ''); // 定时推送的时间
+ $status = $this->request->param('status', 1);
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($name) || empty($content)) {
+ return ResponseHelper::error('推送名称和推送内容不能为空');
+ }
+
+ // 验证推送时机
+ if (!in_array($pushTiming, [1, 2, 3])) {
+ return ResponseHelper::error('无效的推送时机类型');
+ }
+
+ // 如果是定时推送,需要验证时间
+ if ($pushTiming == 3) {
+ if (empty($scheduledTime)) {
+ return ResponseHelper::error('定时推送需要设置推送时间');
+ }
+ // 验证时间格式
+ $timestamp = strtotime($scheduledTime);
+ if ($timestamp === false || $timestamp <= time()) {
+ return ResponseHelper::error('定时推送时间格式不正确或必须大于当前时间');
+ }
+ } else {
+ $scheduledTime = '';
+ }
+
+ // 处理标签
+ $tagsArray = [];
+ if (!empty($tags)) {
+ if (is_string($tags)) {
+ // 如果是字符串,按逗号分割
+ $tagsArray = array_filter(array_map('trim', explode(',', $tags)));
+ } elseif (is_array($tags)) {
+ $tagsArray = array_filter(array_map('trim', $tags));
+ }
+ }
+
+ if (empty($tagsArray)) {
+ return ResponseHelper::error('目标用户标签不能为空');
+ }
+
+ Db::startTrans();
+ try {
+ $aiPush = new AiPush();
+ $aiPush->name = $name;
+ $aiPush->tags = json_encode($tagsArray, JSON_UNESCAPED_UNICODE);
+ $aiPush->content = $content;
+ $aiPush->pushTiming = $pushTiming;
+ $aiPush->scheduledTime = $pushTiming == 3 && !empty($scheduledTime) ? strtotime($scheduledTime) : 0;
+ $aiPush->status = $status;
+ $aiPush->successRate = 0; // 初始成功率为0
+ $aiPush->userId = $userId;
+ $aiPush->companyId = $companyId;
+ $aiPush->createTime = time();
+ $aiPush->updateTime = time();
+ $aiPush->save();
+ Db::commit();
+ return ResponseHelper::success(['id' => $aiPush->id], '创建成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('创建失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 详情
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function details()
+ {
+ $id = $this->request->param('id', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($id)) {
+ return ResponseHelper::error('参数缺失');
+ }
+
+ $data = AiPush::where(['id' => $id, 'isDel' => 0, 'userId' => $userId, 'companyId' => $companyId])->find();
+ if (empty($data)) {
+ return ResponseHelper::error('该推送已被删除或者不存在');
+ }
+
+ $data = $data->toArray();
+ // 解析标签数组
+ $data['tags'] = json_decode($data['tags'], true);
+ if (!is_array($data['tags'])) {
+ $data['tags'] = [];
+ }
+ // 标签转为逗号分隔的字符串(用于编辑时回显)
+ $data['tagsString'] = implode(',', $data['tags']);
+
+ // 处理定时推送时间
+ if ($data['pushTiming'] == 3 && !empty($data['scheduledTime'])) {
+ $data['scheduledTime'] = date('Y-m-d H:i:s', $data['scheduledTime']);
+ } else {
+ $data['scheduledTime'] = '';
+ }
+
+ // 成功率保留一位小数
+ $data['successRate'] = isset($data['successRate']) ? round($data['successRate'], 1) : 0;
+
+ return ResponseHelper::success($data, '获取成功');
+ }
+
+ /**
+ * 删除
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function del()
+ {
+ $id = $this->request->param('id', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($id)) {
+ return ResponseHelper::error('参数缺失');
+ }
+
+ $data = AiPush::where(['id' => $id, 'isDel' => 0, 'userId' => $userId, 'companyId' => $companyId])->find();
+ if (empty($data)) {
+ return ResponseHelper::error('该推送已被删除或者不存在');
+ }
+
+ Db::startTrans();
+ try {
+ $data->isDel = 1;
+ $data->delTime = time();
+ $data->save();
+ Db::commit();
+ return ResponseHelper::success('', '删除成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('删除失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 更新
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function update()
+ {
+ $id = $this->request->param('id', '');
+ $name = $this->request->param('name', '');
+ $tags = $this->request->param('tags', '');
+ $content = $this->request->param('content', '');
+ $pushTiming = $this->request->param('pushTiming', 1);
+ $scheduledTime = $this->request->param('scheduledTime', '');
+ $status = $this->request->param('status', 1);
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($id) || empty($name) || empty($content)) {
+ return ResponseHelper::error('参数缺失');
+ }
+
+ // 验证推送时机
+ if (!in_array($pushTiming, [1, 2, 3])) {
+ return ResponseHelper::error('无效的推送时机类型');
+ }
+
+ // 如果是定时推送,需要验证时间
+ if ($pushTiming == 3) {
+ if (empty($scheduledTime)) {
+ return ResponseHelper::error('定时推送需要设置推送时间');
+ }
+ // 验证时间格式
+ $timestamp = strtotime($scheduledTime);
+ if ($timestamp === false || $timestamp <= time()) {
+ return ResponseHelper::error('定时推送时间格式不正确或必须大于当前时间');
+ }
+ } else {
+ $scheduledTime = '';
+ }
+
+ // 处理标签
+ $tagsArray = [];
+ if (!empty($tags)) {
+ if (is_string($tags)) {
+ $tagsArray = array_filter(array_map('trim', explode(',', $tags)));
+ } elseif (is_array($tags)) {
+ $tagsArray = array_filter(array_map('trim', $tags));
+ }
+ }
+
+ if (empty($tagsArray)) {
+ return ResponseHelper::error('目标用户标签不能为空');
+ }
+
+ $query = AiPush::where(['id' => $id, 'isDel' => 0, 'userId' => $userId, 'companyId' => $companyId])->find();
+ if (empty($query)) {
+ return ResponseHelper::error('该推送已被删除或者不存在');
+ }
+
+ Db::startTrans();
+ try {
+ $query->name = $name;
+ $query->tags = json_encode($tagsArray, JSON_UNESCAPED_UNICODE);
+ $query->content = $content;
+ $query->pushTiming = $pushTiming;
+ $query->scheduledTime = $pushTiming == 3 && !empty($scheduledTime) ? strtotime($scheduledTime) : 0;
+ $query->status = $status;
+ $query->updateTime = time();
+ $query->save();
+ Db::commit();
+ return ResponseHelper::success('', '修改成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('修改失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 修改状态
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function setStatus()
+ {
+ $id = $this->request->param('id', '');
+ $status = $this->request->param('status', 1);
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($id)) {
+ return ResponseHelper::error('参数缺失');
+ }
+
+ if (!in_array($status, [0, 1])) {
+ return ResponseHelper::error('状态值无效');
+ }
+
+ $data = AiPush::where(['id' => $id, 'isDel' => 0, 'userId' => $userId, 'companyId' => $companyId])->find();
+ if (empty($data)) {
+ return ResponseHelper::error('该推送已被删除或者不存在');
+ }
+
+ Db::startTrans();
+ try {
+ $data->status = $status;
+ $data->updateTime = time();
+ $data->save();
+ Db::commit();
+ return ResponseHelper::success('', $status == 1 ? '启用成功' : '禁用成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('操作失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 统计概览(整合自动问候和AI推送)
+ * - 活跃规则(自动问候规则,近30天)
+ * - 总触发次数(自动问候记录总数)
+ * - AI推送成功率(AI推送的成功率)
+ * - AI智能推送(AI推送规则,近30天活跃)
+ * - 规则效果排行(自动问候规则,按使用次数排序)
+ * @return \think\response\Json
+ */
+ public function stats()
+ {
+ $companyId = $this->getUserInfo('companyId');
+ $userId = $this->getUserInfo('id');
+
+ $start30d = time() - 30 * 24 * 3600;
+
+ try {
+ // 公司维度(用于除排行外的统计)
+ $companyWhere = [
+ ['companyId', '=', $companyId],
+ ];
+ // 排行维度(限定个人)
+ $rankingWhere = [
+ ['companyId', '=', $companyId],
+ ['userId', '=', $userId],
+ ];
+
+ // ========== 自动问候统计 ==========
+
+ // 1) 活跃规则(自动问候规则,近30天有记录的)
+ $activeRules = Db::name('kf_auto_greetings_record')
+ ->where($companyWhere)
+ ->where('createTime', '>=', $start30d)
+ ->distinct(true)
+ ->count('autoId');
+
+ // 2) 总触发次数(自动问候记录总数)
+ $totalTriggers = Db::name('kf_auto_greetings_record')
+ ->where($companyWhere)
+ ->count();
+
+ // ========== AI推送统计 ==========
+
+ // 3) AI推送成功率
+ $totalPushes = Db::name('kf_ai_push_record')
+ ->where($companyWhere)
+ ->count();
+ $sendCount = Db::name('kf_ai_push_record')
+ ->where($companyWhere)
+ ->where('isSend', '=', 1)
+ ->count();
+ // 成功率:百分比,保留整数(75%)
+ $aiPushSuccessRate = $totalPushes > 0 ? round(($sendCount * 100) / $totalPushes, 0) : 0;
+
+ // 4) AI智能推送(AI推送规则,近30天活跃的)
+ $aiPushCount = Db::name('kf_ai_push_record')
+ ->where($companyWhere)
+ ->where('createTime', '>=', $start30d)
+ ->distinct(true)
+ ->count('pushId');
+
+ // ========== 规则效果排行(自动问候规则,按使用次数排序)==========
+ $ruleRanking = Db::name('kf_auto_greetings_record')
+ ->where($rankingWhere)
+ ->field([
+ 'autoId AS id',
+ 'COUNT(*) AS usageCount'
+ ])
+ ->group('autoId')
+ ->order('usageCount DESC')
+ ->limit(20)
+ ->select();
+
+ // 附加规则名称和触发类型
+ $autoIds = array_values(array_unique(array_column($ruleRanking, 'id')));
+ $autoIdToRule = [];
+ if (!empty($autoIds)) {
+ $rules = AutoGreetings::where([['id', 'in', $autoIds]])
+ ->field('id,name,trigger')
+ ->select();
+ foreach ($rules as $rule) {
+ $triggerTypes = [
+ 1 => '新好友',
+ 2 => '首次发消息',
+ 3 => '时间触发',
+ 4 => '关键词',
+ 5 => '生日触发',
+ 6 => '自定义'
+ ];
+ $autoIdToRule[$rule['id']] = [
+ 'name' => $rule['name'],
+ 'trigger' => $rule['trigger'],
+ 'triggerText' => $triggerTypes[$rule['trigger']] ?? '未知',
+ ];
+ }
+ }
+
+ foreach ($ruleRanking as &$row) {
+ $row['usageCount'] = (int)($row['usageCount'] ?? 0);
+ $row['name'] = $autoIdToRule[$row['id']]['name'] ?? '';
+ $row['trigger'] = $autoIdToRule[$row['id']]['trigger'] ?? null;
+ $row['triggerText'] = $autoIdToRule[$row['id']]['triggerText'] ?? '';
+ // 格式化使用次数显示
+ $row['usageCountText'] = $row['usageCount'] . ' 次';
+ }
+ unset($row);
+
+ // 更新主表中的成功率字段(异步或定期更新)
+ $this->updatePushSuccessRate($companyId);
+
+ return ResponseHelper::success([
+ 'activeRules' => (int)$activeRules,
+ 'totalTriggers' => (int)$totalTriggers,
+ 'aiPushSuccessRate' => (int)$aiPushSuccessRate,
+ 'aiPushCount' => (int)$aiPushCount,
+ 'ruleRanking' => $ruleRanking,
+ ], '统计成功');
+ } catch (\Exception $e) {
+ return ResponseHelper::error('统计失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 更新推送表的成功率字段
+ * @param int $companyId
+ * @return void
+ */
+ private function updatePushSuccessRate($companyId)
+ {
+ try {
+ // 获取所有启用的推送
+ $pushes = AiPush::where([
+ ['companyId', '=', $companyId],
+ ['isDel', '=', 0]
+ ])->field('id')->select();
+
+ foreach ($pushes as $push) {
+ $pushId = $push['id'];
+ $totalCount = Db::name('kf_ai_push_record')
+ ->where('pushId', $pushId)
+ ->count();
+ $sendCount = Db::name('kf_ai_push_record')
+ ->where('pushId', $pushId)
+ ->where('isSend', 1)
+ ->count();
+
+ $successRate = $totalCount > 0 ? round(($sendCount * 100) / $totalCount, 2) : 0.00;
+
+ AiPush::where('id', $pushId)->update([
+ 'successRate' => $successRate,
+ 'updateTime' => time()
+ ]);
+ }
+ } catch (\Exception $e) {
+ // 静默失败,不影响主流程
+ }
+ }
+}
+
diff --git a/application/chukebao/controller/AiSettingsController.php b/application/chukebao/controller/AiSettingsController.php
new file mode 100644
index 0000000..bef43ac
--- /dev/null
+++ b/application/chukebao/controller/AiSettingsController.php
@@ -0,0 +1,266 @@
+ false,
+ 'round' => 10,
+ 'aiStopSetting' => [
+ 'status' => true,
+ 'key' => ['好', '不错', '好的', '下次', '可以']
+ ],
+ 'fileSetting' => [
+ 'type' => 1,
+ 'content' => ''
+ ],
+ 'modelSetting' => [
+ 'model' => 'GPT-4',
+ 'role' => '你是一名销售的AI助理,同时也是一个工智能技术专家,你的名字叫小灵,你是单身女性,出生于2003年10月10日,喜欢听音乐和看电影有着丰富的人生阅历,前成熟大方,分享用幽默风趣的语言和客户交流,顾客问起你的感情,回复内容中不要使用号,特别注意不要跟客户问题,不要更多选择发送的信息。',
+ 'businessBackground' => '灵销智能公司开发了多款AI营销智能技术产品,以提升销售GPT AI大模型为核心,接入打造的销售/营销/客服等AI智能应用,为企业AI办公,AI助理,AI销售,AI营销,AI直播等大AI应用产品。',
+ 'dialogueStyle' => '客户:你们的AI解决方案具体是怎么收费的?销售:嗯,朋友,我们的AI解决方案是根据项目需求来定的,这样吧,你能跟我说说你们的具体情况吗,不过这样一分钱,您看怎么样?我们可以给您做个详细的方案对比。',
+ ]
+ ];
+
+ const TYPE_DATA = ['audioSetting', 'round', 'aiStopSetting', 'fileSetting', 'modelSetting'];
+
+ /**
+ * 获取配置信息
+ * @return \think\response\Json
+ * @throws \think\db\exception\DataNotFoundException
+ * @throws \think\db\exception\ModelNotFoundException
+ * @throws \think\exception\DbException
+ */
+ public function getSetting()
+ {
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ $data = Db::name('ai_settings')->where(['userId' => $userId, 'companyId' => $companyId])->find();
+ if (empty($data)) {
+ $setting = self::SETTING_DEFAULT;
+ $data = [
+ 'companyId' => $companyId,
+ 'userId' => $userId,
+ 'config' => json_encode($setting, 256),
+ 'createTime' => time(),
+ 'updateTime' => time()
+ ];
+ Db::name('ai_settings')->insert($data);
+
+ } else {
+ $setting = json_decode($data['config'], true);
+ }
+
+ return ResponseHelper::success($setting, '获取成功');
+ }
+
+
+ /**
+ * 配置
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function setSetting()
+ {
+ $key = $this->request->param('key', '');
+ $value = $this->request->param('value', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($key) || empty($value)) {
+ return ResponseHelper::error('参数缺失');
+ }
+
+
+ if (!in_array($key, self::TYPE_DATA)) {
+ return ResponseHelper::error('该类型不在配置项');
+ }
+
+ Db::startTrans();
+ try {
+ $data = Db::name('ai_settings')->where(['userId' => $userId, 'companyId' => $companyId])->find();
+ if (empty($data)) {
+ $setting = self::SETTING_DEFAULT;
+ } else {
+ $setting = json_decode($data['config'], true);
+ }
+ $setting[$key] = $value;
+ $setting = json_encode($setting, 256);
+ Db::name('ai_settings')->where(['id' => $data['id']])->update(['config' => $setting, 'updateTime' => time()]);
+ Db::commit();
+ return ResponseHelper::success(' ', '配置成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('配置失败:' . $e->getMessage());
+ }
+ }
+
+
+ public function getUserTokens()
+ {
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+ $tokens = Db::name('users')
+ ->where('id', $userId)
+ ->where('companyId', $companyId)
+ ->value('tokens');
+
+ return ResponseHelper::success($tokens, '获取成功');
+ }
+
+
+
+ public function getFriend()
+ {
+ $friendId = $this->request->param('friendId', '');
+ $wechatAccountId = $this->request->param('wechatAccountId', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+ $aiType = FriendSettings::where(['userId' => $userId, 'companyId' => $companyId,'friendId' => $friendId,'wechatAccountId' => $wechatAccountId])->value('type');
+ if (empty($aiType)) {
+ $aiType = 0;
+ }
+ return ResponseHelper::success($aiType, '获取成功');
+ }
+
+
+
+
+
+ public function setFriend()
+ {
+ $friendId = $this->request->param('friendId', '');
+ $wechatAccountId = $this->request->param('wechatAccountId', '');
+ $type = $this->request->param('type', 0);
+
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($friendId) || empty($wechatAccountId)) {
+ return ResponseHelper::error('参数缺失');
+ }
+ $friend = Db::table('s2_wechat_friend')->where(['id' => $friendId,'wechatAccountId' => $wechatAccountId])->find();
+
+ if (empty($friend)) {
+ return ResponseHelper::error('该好友不存在');
+ }
+
+ $friendSettings = FriendSettings::where(['userId' => $userId, 'companyId' => $companyId,'friendId' => $friendId,'wechatAccountId' => $wechatAccountId])->find();
+ Db::startTrans();
+ try {
+ if (empty($friendSettings)) {
+ $friendSettings = new FriendSettings();
+ $friendSettings->companyId = $companyId;
+ $friendSettings->userId = $userId;
+ $friendSettings->type = $type;
+ $friendSettings->wechatAccountId = $wechatAccountId;
+ $friendSettings->friendId = $friendId;
+ $friendSettings->createTime = time();
+ $friendSettings->updateTime = time();
+ }else{
+ $friendSettings->type = $type;
+ $friendSettings->updateTime = time();
+ }
+ $friendSettings->save();
+ Db::commit();
+ return ResponseHelper::success(' ', '配置成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('配置失败:' . $e->getMessage());
+ }
+
+ }
+
+
+
+
+ public function setAllFriend()
+ {
+ $packageId = $this->request->param('packageId', []);
+ $type = $this->request->param('type', 0);
+ $isUpdata = $this->request->param('isUpdata', 0);
+
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($packageId)) {
+ return ResponseHelper::error('参数缺失');
+ }
+ //列出所有好友
+ $row = Db::name('traffic_source_package_item')->alias('a')
+ ->join('wechat_friendship f','a.identifier = f.wechatId and f.companyId = '.$companyId)
+ ->join(['s2_wechat_account' => 'wa'],'f.ownerWechatId = wa.wechatId')
+ ->whereIn('a.packageId' , $packageId)
+ ->field('f.id as friendId,wa.id as wechatAccountId')
+ ->group('f.id')
+ ->select();
+
+ if (empty($row)) {
+ return ResponseHelper::error('`好友不存在');
+ }
+
+
+
+
+
+
+ // 1000条为一组进行批量处理
+ $batchSize = 1000;
+ $totalRows = count($row);
+
+ for ($i = 0; $i < $totalRows; $i += $batchSize) {
+ $batchRows = array_slice($row, $i, $batchSize);
+ if (!empty($batchRows)) {
+ // 1. 提取当前批次的phone
+ $friendIds = array_column($batchRows, 'friendId');
+ // 2. 批量查询已存在的phone
+ $existingPhones = [];
+ if (!empty($friendIds)) {
+ //强制更新
+ if(!empty($isUpdata)){
+ FriendSettings::whereIn('friendId',$friendIds)->update(['type' => $type,'updateTime' => time()]);
+ }
+
+ $existing = FriendSettings::where('companyId', $companyId)->where('friendId', 'in', $friendIds)->field('friendId')->select()->toArray();
+ $existingPhones = array_column($existing, 'friendId');
+ }
+
+ // 3. 过滤出新数据,批量插入
+ $newData = [];
+ foreach ($batchRows as $row) {
+ if (!empty($friendIds) && !in_array($row['friendId'], $existingPhones)) {
+ $newData[] = [
+ 'companyId' => $companyId,
+ 'userId' => $userId,
+ 'type' => $type,
+ 'wechatAccountId' => $row['wechatAccountId'],
+ 'friendId' => $row['friendId'],
+ 'createTime' => time(),
+ 'updateTime' => time(),
+ ];
+ }
+ }
+ // 4. 批量插入新数据
+ if (!empty($newData)) {
+ FriendSettings::insertAll($newData);
+ }
+ }
+ }
+ try {
+ return ResponseHelper::success(' ', '配置成功');
+ } catch (\Exception $e) {
+ return ResponseHelper::error('配置失败:' . $e->getMessage());
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/application/chukebao/controller/AutoGreetingsController.php b/application/chukebao/controller/AutoGreetingsController.php
new file mode 100644
index 0000000..ef186b3
--- /dev/null
+++ b/application/chukebao/controller/AutoGreetingsController.php
@@ -0,0 +1,754 @@
+request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+ $is_template = $this->request->param('is_template', 0);
+ $triggerType = $this->request->param('triggerType', ''); // 触发类型筛选
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if($is_template == 1){
+ $where = [
+ ['is_template','=',1],
+ ['isDel' ,'=', 0],
+ ];
+ }else{
+ $where = [
+ ['companyId','=',$companyId],
+ ['userId' ,'=', $userId],
+ ['isDel' ,'=', 0],
+ ];
+ }
+
+ if(!empty($keyword)){
+ $where[] = ['name','like','%'.$keyword.'%'];
+ }
+
+ if(!empty($triggerType)){
+ $where[] = ['trigger','=',$triggerType];
+ }
+
+ $query = AutoGreetings::where($where);
+ $total = $query->count();
+ $list = $query->where($where)->page($page,$limit)->order('level asc,id desc')->select();
+
+ // 获取使用次数
+ $list = is_array($list) ? $list : $list->toArray();
+ $ids = array_column($list, 'id');
+ $usageCounts = [];
+ if (!empty($ids)) {
+ $counts = Db::name('kf_auto_greetings_record')
+ ->where('autoId', 'in', $ids)
+ ->field('autoId, COUNT(*) as count')
+ ->group('autoId')
+ ->select();
+ foreach ($counts as $count) {
+ $usageCounts[$count['autoId']] = (int)$count['count'];
+ }
+ }
+
+ foreach ($list as &$item) {
+ $item['condition'] = json_decode($item['condition'], true);
+ $item['usageCount'] = $usageCounts[$item['id']] ?? 0;
+ // 格式化触发类型显示文本
+ $triggerTypes = [
+ 1 => '新好友',
+ 2 => '首次发消息',
+ 3 => '时间触发',
+ 4 => '关键词触发',
+ 5 => '生日触发',
+ 6 => '自定义'
+ ];
+ $item['triggerText'] = $triggerTypes[$item['trigger']] ?? '未知';
+ }
+ unset($item);
+
+ return ResponseHelper::success(['list'=>$list,'total'=>$total]);
+ }
+
+
+ /**
+ * 校验trigger类型对应的condition
+ * @param int $trigger 触发类型
+ * @param mixed $condition 条件参数
+ * @return array|string 返回处理后的condition数组,或错误信息字符串
+ */
+ private function validateTriggerCondition($trigger, $condition)
+ {
+ // trigger类型:1=新好友,2=首次发消息,3=时间触发,4=关键词触发,5=生日触发,6=自定义
+ switch ($trigger) {
+ case 1: // 新好友
+ // 不需要condition
+ return [];
+
+ case 2: // 首次发消息
+ // 不需要condition
+ return [];
+
+ case 3: // 时间触发
+ // 需要condition,格式为:{"type": "daily_time|yearly_datetime|fixed_range|workday", "value": "..."}
+ if (empty($condition)) {
+ return '时间触发类型需要配置具体的触发条件';
+ }
+ $condition = is_array($condition) ? $condition : json_decode($condition, true);
+ if (empty($condition) || !is_array($condition)) {
+ return '时间触发类型的条件格式不正确,应为数组格式';
+ }
+
+ // 验证必须包含type字段
+ if (!isset($condition['type']) || empty($condition['type'])) {
+ return '时间触发类型必须指定触发方式:daily_time(每天固定时间)、yearly_datetime(每年固定日期时间)、fixed_range(固定时间段)、workday(工作日)';
+ }
+
+ $timeType = $condition['type'];
+ $allowedTypes = ['daily_time', 'yearly_datetime', 'fixed_range', 'workday'];
+ // 兼容旧版本的 fixed_time,自动转换为 daily_time
+ if ($timeType === 'fixed_time') {
+ $timeType = 'daily_time';
+ }
+ if (!in_array($timeType, $allowedTypes)) {
+ return '时间触发类型无效,必须为:daily_time(每天固定时间)、yearly_datetime(每年固定日期时间)、fixed_range(固定时间段)、workday(工作日)';
+ }
+
+ // 根据不同的type验证value
+ switch ($timeType) {
+ case 'daily_time': // 每天固定时间(每天的几点几分)
+ // value应该是时间字符串,格式:HH:mm,如 "14:30"
+ if (!isset($condition['value']) || empty($condition['value'])) {
+ return '每天固定时间类型需要配置具体时间,格式:HH:mm(如 14:30)';
+ }
+ $timeValue = $condition['value'];
+ if (!preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $timeValue)) {
+ return '每天固定时间格式不正确,应为 HH:mm 格式(如 14:30)';
+ }
+ return [
+ 'type' => 'daily_time',
+ 'value' => $timeValue
+ ];
+
+ case 'yearly_datetime': // 每年固定日期时间(每年的几月几号几点几分)
+ // value应该是日期时间字符串,格式:MM-dd HH:mm,如 "12-25 14:30"
+ if (!isset($condition['value']) || empty($condition['value'])) {
+ return '每年固定日期时间类型需要配置具体日期和时间,格式:MM-dd HH:mm(如 12-25 14:30)';
+ }
+ $datetimeValue = $condition['value'];
+ // 验证格式:MM-dd HH:mm
+ if (!preg_match('/^(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) ([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $datetimeValue)) {
+ return '每年固定日期时间格式不正确,应为 MM-dd HH:mm 格式(如 12-25 14:30)';
+ }
+ // 进一步验证日期是否有效(例如2月30日不存在)
+ list($datePart, $timePart) = explode(' ', $datetimeValue);
+ list($month, $day) = explode('-', $datePart);
+ if (!checkdate((int)$month, (int)$day, 2000)) { // 使用2000年作为参考年份验证日期有效性
+ return '日期无效,请检查月份和日期是否正确(如2月不能有30日)';
+ }
+ return [
+ 'type' => 'yearly_datetime',
+ 'value' => $datetimeValue
+ ];
+
+ case 'fixed_range': // 固定时间段
+ // value应该是时间段数组,格式:["09:00", "18:00"]
+ if (!isset($condition['value']) || !is_array($condition['value'])) {
+ return '固定时间段类型需要配置时间段,格式:["开始时间", "结束时间"](如 ["09:00", "18:00"])';
+ }
+ $rangeValue = $condition['value'];
+ if (count($rangeValue) !== 2) {
+ return '固定时间段应为包含两个时间点的数组,格式:["09:00", "18:00"]';
+ }
+ // 验证时间格式
+ foreach ($rangeValue as $time) {
+ if (!preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $time)) {
+ return '时间段格式不正确,应为 HH:mm 格式(如 09:00)';
+ }
+ }
+ // 验证开始时间小于结束时间
+ $startTime = strtotime('2000-01-01 ' . $rangeValue[0]);
+ $endTime = strtotime('2000-01-01 ' . $rangeValue[1]);
+ if ($startTime >= $endTime) {
+ return '开始时间必须小于结束时间';
+ }
+ return [
+ 'type' => 'fixed_range',
+ 'value' => $rangeValue
+ ];
+
+ case 'workday': // 工作日
+ // 工作日需要配置时间,格式:HH:mm(如 09:00)
+ if (!isset($condition['value']) || empty($condition['value'])) {
+ return '工作日触发类型需要配置时间,格式:HH:mm(如 09:00)';
+ }
+ $timeValue = trim($condition['value']);
+ // 验证格式:HH:mm
+ if (!preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $timeValue)) {
+ return '工作日时间格式不正确,应为 HH:mm 格式(如 09:00)';
+ }
+ return [
+ 'type' => 'workday',
+ 'value' => $timeValue
+ ];
+
+ default:
+ return '时间触发类型无效';
+ }
+
+ case 4: // 关键词触发
+ // 需要condition,格式:{"keywords": ["关键词1", "关键词2"], "match_type": "exact|fuzzy"}
+ if (empty($condition)) {
+ return '关键词触发类型需要配置至少一个关键词';
+ }
+
+ // 如果是字符串,尝试解析JSON
+ if (is_string($condition)) {
+ $decoded = json_decode($condition, true);
+ if (json_last_error() === JSON_ERROR_NONE) {
+ $condition = $decoded;
+ } else {
+ return '关键词触发类型格式错误,应为对象格式:{"keywords": ["关键词1", "关键词2"], "match_type": "exact|fuzzy"}';
+ }
+ }
+
+ // 必须是对象格式
+ if (!is_array($condition) || !isset($condition['keywords'])) {
+ return '关键词触发类型格式错误,应为对象格式:{"keywords": ["关键词1", "关键词2"], "match_type": "exact|fuzzy"}';
+ }
+
+ $keywords = $condition['keywords'];
+ $matchType = isset($condition['match_type']) ? $condition['match_type'] : 'fuzzy';
+
+ // 验证match_type
+ if (!in_array($matchType, ['exact', 'fuzzy'])) {
+ return '匹配类型无效,必须为:exact(精准匹配)或 fuzzy(模糊匹配)';
+ }
+
+ // 处理keywords
+ if (is_string($keywords)) {
+ $keywords = explode(',', $keywords);
+ }
+ if (!is_array($keywords)) {
+ return '关键词格式不正确,应为数组格式';
+ }
+
+ // 过滤空值并去重
+ $keywords = array_filter(array_map('trim', $keywords));
+ if (empty($keywords)) {
+ return '关键词触发类型需要配置至少一个关键词';
+ }
+
+ // 验证每个关键词不为空
+ foreach ($keywords as $keyword) {
+ if (empty($keyword)) {
+ return '关键词不能为空';
+ }
+ }
+
+ return [
+ 'keywords' => array_values($keywords),
+ 'match_type' => $matchType
+ ];
+
+ case 5: // 生日触发
+ // 需要condition,格式支持:
+ // 1. 月日字符串:'10-10' 或 '10-10 09:00'(MM-DD格式,不包含年份)
+ // 2. 对象格式:{'month': 10, 'day': 10, 'time': '09:00'} 或 {'month': '10', 'day': '10', 'time_range': ['09:00', '10:00']}
+ if (empty($condition)) {
+ return '生日触发类型需要配置日期条件';
+ }
+
+ // 如果是字符串,只接受 MM-DD 格式(不包含年份)
+ if (is_string($condition)) {
+ // 检查是否包含时间部分
+ if (preg_match('/^(\d{1,2})-(\d{1,2})\s+(\d{2}:\d{2})$/', $condition, $matches)) {
+ // 格式:'10-10 09:00'
+ $month = (int)$matches[1];
+ $day = (int)$matches[2];
+ if ($month < 1 || $month > 12 || $day < 1 || $day > 31) {
+ return '生日日期格式不正确,月份应为1-12,日期应为1-31';
+ }
+ return [
+ 'month' => $month,
+ 'day' => $day,
+ 'time' => $matches[3]
+ ];
+ } elseif (preg_match('/^(\d{1,2})-(\d{1,2})$/', $condition, $matches)) {
+ // 格式:'10-10'(不指定时间,当天任何时间都可以触发)
+ $month = (int)$matches[1];
+ $day = (int)$matches[2];
+ if ($month < 1 || $month > 12 || $day < 1 || $day > 31) {
+ return '生日日期格式不正确,月份应为1-12,日期应为1-31';
+ }
+ return [
+ 'month' => $month,
+ 'day' => $day
+ ];
+ } else {
+ return '生日日期格式不正确,应为 MM-DD 或 MM-DD HH:mm 格式(如 10-10 或 10-10 09:00),不包含年份';
+ }
+ }
+
+ // 如果是数组,可能是对象格式或旧格式
+ if (is_array($condition)) {
+ // 检查是否是旧格式(仅兼容 MM-DD 格式的数组)
+ if (isset($condition[0]) && is_string($condition[0])) {
+ $dateStr = $condition[0];
+ // 只接受 MM-DD 格式:'10-10' 或 '10-10 09:00'
+ if (preg_match('/^(\d{1,2})-(\d{1,2})(?:\s+(\d{2}:\d{2}))?$/', $dateStr, $matches)) {
+ $month = (int)$matches[1];
+ $day = (int)$matches[2];
+ if ($month < 1 || $month > 12 || $day < 1 || $day > 31) {
+ return '生日日期格式不正确,月份应为1-12,日期应为1-31';
+ }
+ if (isset($matches[3])) {
+ return [
+ 'month' => $month,
+ 'day' => $day,
+ 'time' => $matches[3]
+ ];
+ } else {
+ return [
+ 'month' => $month,
+ 'day' => $day
+ ];
+ }
+ } else {
+ return '生日日期格式不正确,应为 MM-DD 格式(如 10-10),不包含年份';
+ }
+ }
+
+ // 新格式:{'month': 10, 'day': 10, 'time': '09:00'}
+ if (isset($condition['month']) && isset($condition['day'])) {
+ $month = (int)$condition['month'];
+ $day = (int)$condition['day'];
+
+ if ($month < 1 || $month > 12) {
+ return '生日月份格式不正确,应为1-12';
+ }
+ if ($day < 1 || $day > 31) {
+ return '生日日期格式不正确,应为1-31';
+ }
+
+ $result = [
+ 'month' => $month,
+ 'day' => $day
+ ];
+
+ // 检查是否配置了时间
+ if (isset($condition['time']) && !empty($condition['time'])) {
+ $time = trim($condition['time']);
+ if (!preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $time)) {
+ return '生日时间格式不正确,应为 HH:mm 格式(如 09:00)';
+ }
+ $result['time'] = $time;
+ }
+
+ // 检查是否配置了时间范围
+ if (isset($condition['time_range']) && is_array($condition['time_range']) && count($condition['time_range']) === 2) {
+ $startTime = trim($condition['time_range'][0]);
+ $endTime = trim($condition['time_range'][1]);
+ if (!preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $startTime) ||
+ !preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $endTime)) {
+ return '生日时间范围格式不正确,应为 ["HH:mm", "HH:mm"] 格式';
+ }
+ $result['time_range'] = [$startTime, $endTime];
+ }
+
+ return $result;
+ }
+
+ return '生日触发条件格式不正确,需要提供month和day字段';
+ }
+
+ return '生日触发条件格式不正确';
+
+ case 6: // 自定义
+ // 自定义类型,condition可选,如果有则必须是数组格式
+ if (!empty($condition)) {
+ $condition = is_array($condition) ? $condition : json_decode($condition, true);
+ if (!is_array($condition)) {
+ return '自定义类型的条件格式不正确,应为数组格式';
+ }
+ return $condition;
+ }
+ return [];
+
+ default:
+ return '无效的触发类型';
+ }
+ }
+
+ /**
+ * 添加
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function create(){
+ $name = $this->request->param('name', '');
+ $trigger = $this->request->param('trigger', 0);
+ $condition = $this->request->param('condition', '');
+ $content = $this->request->param('content', '');
+ $level = $this->request->param('level', 0);
+ $status = $this->request->param('status', 1);
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($name) || empty($trigger) || empty($content)){
+ return ResponseHelper::error('参数缺失');
+ }
+
+ // 校验trigger类型
+ if (!in_array($trigger, [1, 2, 3, 4, 5, 6])) {
+ return ResponseHelper::error('无效的触发类型');
+ }
+
+ // 校验并处理condition
+ $conditionResult = $this->validateTriggerCondition($trigger, $condition);
+ if (is_string($conditionResult)) {
+ // 返回的是错误信息
+ return ResponseHelper::error($conditionResult);
+ }
+ $condition = $conditionResult;
+
+
+ Db::startTrans();
+ try {
+ $AutoGreetings = new AutoGreetings();
+ $AutoGreetings->name = $name;
+ $AutoGreetings->trigger = $trigger;
+ $AutoGreetings->condition = json_encode($condition,256);
+ $AutoGreetings->content = $content;
+ $AutoGreetings->level = $level;
+ $AutoGreetings->status = $status;
+ $AutoGreetings->userId = $userId;
+ $AutoGreetings->companyId = $companyId;
+ $AutoGreetings->updateTime = time();
+ $AutoGreetings->createTime = time();
+ $AutoGreetings->usageCount = 0; // 初始化使用次数为0
+ $AutoGreetings->save();
+ Db::commit();
+ return ResponseHelper::success(['id' => $AutoGreetings->id],'创建成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('创建失败:'.$e->getMessage());
+ }
+ }
+
+
+
+
+ /**
+ * 详情
+ * @return \think\response\Json
+ */
+ public function details()
+ {
+ $id = $this->request->param('id', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+ if (empty($id)){
+ return ResponseHelper::error('参数缺失');
+ }
+ $data = AutoGreetings::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find();
+ if (empty($data)){
+ return ResponseHelper::error('该内容已被删除或者不存在');
+ }
+
+
+ $data['condition'] = json_decode($data['condition'],true);
+
+ // 获取使用次数
+ $usageCount = Db::name('kf_auto_greetings_record')
+ ->where('autoId', $id)
+ ->count();
+ $data['usageCount'] = (int)$usageCount;
+
+ return ResponseHelper::success($data,'获取成功');
+ }
+
+ /**
+ * 删除
+ * @return \think\response\Json
+ */
+ public function del()
+ {
+ $id = $this->request->param('id', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+ if (empty($id)){
+ return ResponseHelper::error('参数缺失');
+ }
+ $data = AutoGreetings::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find();
+ if (empty($data)){
+ return ResponseHelper::error('该已被删除或者不存在');
+ }
+ Db::startTrans();
+ try {
+ $data->isDel = 1;
+ $data->delTime = time();
+ $data->save();
+ Db::commit();
+ return ResponseHelper::success('','删除成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('删除失败:'.$e->getMessage());
+ }
+ }
+
+
+ /**
+ * 更新
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function update(){
+ $id = $this->request->param('id', '');
+ $name = $this->request->param('name', '');
+ $trigger = $this->request->param('trigger', 0);
+ $condition = $this->request->param('condition', '');
+ $content = $this->request->param('content', '');
+ $level = $this->request->param('level', 0);
+ $status = $this->request->param('status', 1);
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($id) || empty($name) || empty($trigger) || empty($content)){
+ return ResponseHelper::error('参数缺失');
+ }
+
+ // 校验trigger类型
+ if (!in_array($trigger, [1, 2, 3, 4, 5, 6])) {
+ return ResponseHelper::error('无效的触发类型');
+ }
+
+ // 校验并处理condition
+ $conditionResult = $this->validateTriggerCondition($trigger, $condition);
+ if (is_string($conditionResult)) {
+ // 返回的是错误信息
+ return ResponseHelper::error($conditionResult);
+ }
+ $condition = $conditionResult;
+
+
+ $query = AutoGreetings::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find();
+ if (empty($query)){
+ return ResponseHelper::error('该内容已被删除或者不存在');
+ }
+ Db::startTrans();
+ try {
+ $query->name = $name;
+ $query->trigger = $trigger;
+ $query->condition = !empty($condition) ? json_encode($condition,256) : json_encode([]);
+ $query->content = $content;
+ $query->level = $level;
+ $query->status = $status;
+ $query->userId = $userId;
+ $query->companyId = $companyId;
+ $query->updateTime = time();
+ $query->createTime = time();
+ $query->save();
+ Db::commit();
+ return ResponseHelper::success(' ','修改成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('修改失败:'.$e->getMessage());
+ }
+ }
+
+ /**
+ * 修改状态
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function setStatus(){
+ $id = $this->request->param('id', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($id)){
+ return ResponseHelper::error('参数缺失');
+ }
+
+ $query = AutoGreetings::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find();
+ if (empty($query)){
+ return ResponseHelper::error('该内容已被删除或者不存在');
+ }
+ Db::startTrans();
+ try {
+ $status = $this->request->param('status', '');
+ if ($status !== '') {
+ $query->status = (int)$status;
+ } else {
+ $query->status = $query->status == 1 ? 0 : 1;
+ }
+ $query->updateTime = time();
+ $query->save();
+ Db::commit();
+ return ResponseHelper::success(['status' => $query->status],'修改成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('修改失败:'.$e->getMessage());
+ }
+ }
+
+
+
+
+ /**
+ * 拷贝
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function copy(){
+ $id = $this->request->param('id', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($id) ){
+ return ResponseHelper::error('参数缺失');
+ }
+
+ $data = AutoGreetings::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find();
+ if (empty($data)){
+ return ResponseHelper::error('该内容已被删除或者不存在');
+ }
+ Db::startTrans();
+ try {
+ $query = new AutoGreetings();
+ $query->name = $data['name'] . '_copy';
+ $query->trigger = $data['trigger'];
+ $query->condition = $data['condition'];
+ $query->content = $data['content'];
+ $query->level = $data['level'];
+ $query->status = $data['status'];
+ $query->userId = $userId;
+ $query->companyId = $companyId;
+ $query->updateTime = time();
+ $query->createTime = time();
+ $query->save();
+ Db::commit();
+ return ResponseHelper::success(' ','拷贝成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('拷贝失败:'.$e->getMessage());
+ }
+ }
+
+
+ /**
+ * 统计概览
+ * - 总触发次数
+ * - 活跃规则(近一个月)
+ * - 发送成功率
+ * - 平均响应时间(秒)
+ * - 规则效果排行(按发送次数降序、平均响应时间升序)
+ * @return \think\response\Json
+ */
+ public function stats()
+ {
+ $companyId = $this->getUserInfo('companyId');
+ $userId = $this->getUserInfo('id');
+
+ $start30d = time() - 30 * 24 * 3600;
+
+ try {
+ // 公司维度(用于除排行外的统计)
+ $companyWhere = [
+ ['companyId', '=', $companyId],
+ ];
+ // 排行维度(限定个人)
+ $rankingWhere = [
+ ['companyId', '=', $companyId],
+ ['userId', '=', $userId],
+ ];
+
+ // 1) 总触发次数
+ $totalTriggers = Db::name('kf_auto_greetings_record')
+ ->where($companyWhere)
+ ->count();
+
+ // 2) 近30天活跃规则(仅返回数量,按公司维度,distinct autoId)
+ $activeRulesCount = Db::name('kf_auto_greetings_record')
+ ->where($companyWhere)
+ ->where('createTime', '>=', $start30d)
+ ->distinct(true)
+ ->count('autoId');
+
+ // 3) 发送成功率
+ $sendCount = Db::name('kf_auto_greetings_record')
+ ->where($companyWhere)
+ ->where('isSend', '=', 1)
+ ->count();
+ // 成功率:百分比,保留两位小数
+ $sendRate = $totalTriggers > 0 ? round(($sendCount * 100) / $totalTriggers, 2) : 0.00;
+
+ // 4) 平均响应时间(receiveTime - sendTime,单位秒)
+ $avgResponse = Db::name('kf_auto_greetings_record')
+ ->where($companyWhere)
+ ->whereRaw('sendTime IS NOT NULL AND receiveTime IS NOT NULL AND receiveTime >= sendTime')
+ ->avg(Db::raw('(receiveTime - sendTime)'));
+ $avgResponse = $avgResponse ? (int)round($avgResponse) : 0;
+
+ // 5) 规则效果排行(按发送次数降序、平均响应时间升序)
+ $ranking = Db::name('kf_auto_greetings_record')
+ ->where($rankingWhere)
+ ->field([
+ 'autoId AS id',
+ 'COUNT(*) AS totalCount',
+ 'SUM(CASE WHEN isSend = 1 THEN 1 ELSE 0 END) AS sendCount',
+ 'AVG(CASE WHEN sendTime IS NOT NULL AND receiveTime IS NOT NULL AND receiveTime >= sendTime THEN (receiveTime - sendTime) END) AS avgResp'
+ ])
+ ->group('autoId')
+ ->orderRaw('sendCount DESC, avgResp ASC')
+ ->limit(20)
+ ->select();
+
+ // 附加规则名称(如存在)
+ $autoIds = array_values(array_unique(array_column($ranking, 'id')));
+ $autoIdToRule = [];
+ if (!empty($autoIds)) {
+ $rules = AutoGreetings::where([['id', 'in', $autoIds]])
+ ->field('id,name,trigger')
+ ->select();
+ foreach ($rules as $rule) {
+ $autoIdToRule[$rule['id']] = [
+ 'name' => $rule['name'],
+ 'trigger' => $rule['trigger'],
+ ];
+ }
+ }
+
+ foreach ($ranking as &$row) {
+ $row['avgResp'] = isset($row['avgResp']) && $row['avgResp'] !== null ? (int)round($row['avgResp']) : 0;
+ // 百分比,两位小数
+ $row['sendRate'] = ($row['totalCount'] ?? 0) > 0 ? round((($row['sendCount'] ?? 0) * 100) / $row['totalCount'], 2) : 0.00;
+ $row['name'] = $autoIdToRule[$row['id']]['name'] ?? '';
+ $row['trigger'] = $autoIdToRule[$row['id']]['trigger'] ?? null;
+ }
+ unset($row);
+
+ return ResponseHelper::success([
+ 'totalTriggers' => (int)$totalTriggers,
+ 'activeRules' => (int)$activeRulesCount,
+ 'sendSuccessRate' => $sendRate,
+ 'avgResponseSeconds' => $avgResponse,
+ 'ruleRanking' => $ranking,
+ ], '统计成功');
+ } catch (\Exception $e) {
+ return ResponseHelper::error('统计失败:' . $e->getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/chukebao/controller/BaseController.php b/application/chukebao/controller/BaseController.php
new file mode 100644
index 0000000..2c87750
--- /dev/null
+++ b/application/chukebao/controller/BaseController.php
@@ -0,0 +1,29 @@
+request->userInfo;
+
+ if (!$user) {
+ throw new \Exception('未授权访问,缺少有效的身份凭证', 401);
+ }
+
+ return $column ? $user[$column] : $user;
+ }
+}
\ No newline at end of file
diff --git a/application/chukebao/controller/ContentController.php b/application/chukebao/controller/ContentController.php
new file mode 100644
index 0000000..92e49b2
--- /dev/null
+++ b/application/chukebao/controller/ContentController.php
@@ -0,0 +1,665 @@
+request->param('keyword', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ $query = Material::where(['userId' => $userId,'companyId' => $companyId,'isDel' => 0,'status' => 1])
+ ->field('id,title,cover')
+ ->order('id desc');
+
+ $list = $query->select()->toArray();
+
+ return ResponseHelper::success($list);
+ }
+
+
+ /**
+ * 素材列表
+ * @return \think\response\Json
+ * @throws \think\db\exception\DataNotFoundException
+ * @throws \think\db\exception\ModelNotFoundException
+ * @throws \think\exception\DbException
+ */
+ public function getMaterial(){
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ $query = Material::where(['userId' => $userId,'companyId' => $companyId,'isDel' => 0])
+ ->order('id desc');
+ if (!empty($keyword)){
+ $query->where('title', 'like', '%'.$keyword.'%');
+ }
+ $list = $query->page($page, $limit)->select()->toArray();
+ $total = $query->count();
+
+ foreach ($list as $k => &$v){
+ $user = Db::name('users')->where(['id' => $v['userId']])->field('username,account')->find();
+ if (!empty($user)){
+ $v['userName'] = !empty($user['username']) ? $user['username'] : $user['account'];
+ }else{
+ $v['userName'] = '';
+ }
+ }
+ unset($v);
+ return ResponseHelper::success(['list'=>$list,'total'=>$total]);
+ }
+
+
+ /**
+ * 素材添加
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function createMaterial(){
+ $title = $this->request->param('title', '');
+ $content = $this->request->param('content', []);
+ $cover = $this->request->param('cover', '');
+ $status = $this->request->param('status', 0);
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($title) || empty($content) || empty($cover)){
+ return ResponseHelper::error('参数缺失');
+ }
+ $newContent = [];
+ foreach ($content as $k => $v){
+ if (in_array($v['type'],['text','image','video','audio','file','link'])){
+ $newContent[] = $v;
+ }
+ }
+
+ Db::startTrans();
+ try {
+ $query = new Material();
+ $query->title = $title;
+ $query->content = !empty($newContent) ? json_encode($newContent,256) : json_encode([],256);
+ $query->cover = $cover;
+ $query->status = $status;
+ $query->userId = $userId;
+ $query->companyId = $companyId;
+ $query->createTime = time();
+ $query->updateTime = time();
+ $query->save();
+ Db::commit();
+ return ResponseHelper::success(' ','创建成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('创建失败:'.$e->getMessage());
+ }
+ }
+
+ /**
+ * 素材详情
+ * @return \think\response\Json
+ */
+ public function detailsMaterial()
+ {
+ $id = $this->request->param('id', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+ if (empty($id)){
+ return ResponseHelper::error('参数缺失');
+ }
+ $data = Material::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find();
+ if (empty($data)){
+ return ResponseHelper::error('该素材已被删除或者不存在');
+ }
+ $data['content'] = json_decode($data['content'],true);
+ unset($data['createTime'],$data['updateTime'],$data['isDel'],$data['delTime']);
+ return ResponseHelper::success($data,'获取成功');
+ }
+
+ /**
+ * 删除素材
+ * @return \think\response\Json
+ */
+ public function delMaterial()
+ {
+ $id = $this->request->param('id', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+ if (empty($id)){
+ return ResponseHelper::error('参数缺失');
+ }
+ $data = Material::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find();
+ if (empty($data)){
+ return ResponseHelper::error('该素材已被删除或者不存在');
+ }
+ Db::startTrans();
+ try {
+ $data->isDel = 1;
+ $data->delTime = time();
+ $data->save();
+ Db::commit();
+ return ResponseHelper::success('','删除成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('删除失败:'.$e->getMessage());
+ }
+ }
+
+
+ /**
+ * 修改素材
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function updateMaterial(){
+ $id = $this->request->param('id', '');
+ $title = $this->request->param('title', '');
+ $content = $this->request->param('content', []);
+ $cover = $this->request->param('cover', '');
+ $status = $this->request->param('status', 0);
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($id) || empty($title) || empty($content) || empty($cover)){
+ return ResponseHelper::error('参数缺失');
+ }
+ $newContent = [];
+ foreach ($content as $k => $v){
+ if (in_array($v['type'],['text','image','video','audio','file','link'])){
+ $newContent[] = $v;
+ }
+ }
+ $query = Material::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find();
+ if (empty($query)){
+ return ResponseHelper::error('该素材已被删除或者不存在');
+ }
+ Db::startTrans();
+ try {
+ $query->title = $title;
+ $query->content = !empty($newContent) ? json_encode($newContent,256) : json_encode([],256);
+ $query->cover = $cover;
+ $query->status = $status;
+ $query->updateTime = time();
+ $query->save();
+ Db::commit();
+ return ResponseHelper::success(' ','修改成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('修改失败:'.$e->getMessage());
+ }
+ }
+ //===================================================== 素材管理 =====================================================
+
+
+
+
+
+ //==================================================== 违禁词管理 ====================================================
+
+ /**
+ * 违禁词列表
+ * @return \think\response\Json
+ * @throws \think\db\exception\DataNotFoundException
+ * @throws \think\db\exception\ModelNotFoundException
+ * @throws \think\exception\DbException
+ */
+ public function getSensitiveWord(){
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ $query = SensitiveWord::where(['userId' => $userId,'companyId' => $companyId,'isDel' => 0])
+ ->order('id desc');
+ if (!empty($keyword)){
+ $query->where('title', 'like', '%'.$keyword.'%');
+ }
+ $total = $query->count();
+ $list = $query->page($page, $limit)->select()->toArray();
+
+
+ foreach ($list as $k => &$v){
+ $user = Db::name('users')->where(['id' => $v['userId']])->field('username,account')->find();
+ if (!empty($user)){
+ $v['userName'] = !empty($user['username']) ? $user['username'] : $user['account'];
+ }else{
+ $v['userName'] = '';
+ }
+ }
+ unset($v);
+ return ResponseHelper::success(['list'=>$list,'total'=>$total]);
+ }
+
+
+ /**
+ * 违禁词添加
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function createSensitiveWord(){
+ $title = $this->request->param('title', '');
+ $keywords = $this->request->param('keywords', '');
+ $content = $this->request->param('content', '');
+ $status = $this->request->param('status', 0);
+ $operation = $this->request->param('operation', 0);
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($title) || empty($keywords)){
+ return ResponseHelper::error('参数缺失');
+ }
+
+ $keywords = explode(',',$keywords);
+
+ Db::startTrans();
+ try {
+ $query = new SensitiveWord();
+ $query->title = $title;
+ $query->keywords = $keywords;
+ $query->content = $content;
+ $query->status = $status;
+ $query->operation = $operation;
+ $query->userId = $userId;
+ $query->companyId = $companyId;
+ $query->createTime = time();
+ $query->updateTime = time();
+ $query->save();
+ Db::commit();
+ return ResponseHelper::success(' ','创建成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('创建失败:'.$e->getMessage());
+ }
+ }
+
+
+
+ /**
+ * 违禁词详情
+ * @return \think\response\Json
+ */
+ public function detailsSensitiveWord()
+ {
+ $id = $this->request->param('id', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+ if (empty($id)){
+ return ResponseHelper::error('参数缺失');
+ }
+ $data = SensitiveWord::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find();
+ if (empty($data)){
+ return ResponseHelper::error('该素材已被删除或者不存在');
+ }
+ $data['keywords'] = json_decode($data['keywords'],true);
+ $data['keywords'] = implode(',',$data['keywords']);
+ unset($data['createTime'],$data['updateTime'],$data['isDel'],$data['delTime']);
+ return ResponseHelper::success($data,'获取成功');
+ }
+
+ /**
+ * 违禁词删除
+ * @return \think\response\Json
+ */
+ public function delSensitiveWord()
+ {
+ $id = $this->request->param('id', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+ if (empty($id)){
+ return ResponseHelper::error('参数缺失');
+ }
+ $data = SensitiveWord::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find();
+ if (empty($data)){
+ return ResponseHelper::error('该素材已被删除或者不存在');
+ }
+ Db::startTrans();
+ try {
+ $data->isDel = 1;
+ $data->delTime = time();
+ $data->save();
+ Db::commit();
+ return ResponseHelper::success('','删除成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('删除失败:'.$e->getMessage());
+ }
+ }
+
+
+ /**
+ * 更新违禁词
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function updateSensitiveWord(){
+ $id = $this->request->param('id', '');
+ $title = $this->request->param('title', '');
+ $keywords = $this->request->param('keywords', '');
+ $content = $this->request->param('content', '');
+ $status = $this->request->param('status', 0);
+ $operation = $this->request->param('operation', 0);
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($id) || empty($title) || empty($keywords)){
+ return ResponseHelper::error('参数缺失');
+ }
+
+ $keywords = explode(',',$keywords);
+
+ $query = SensitiveWord::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find();
+ if (empty($query)){
+ return ResponseHelper::error('该素材已被删除或者不存在');
+ }
+ Db::startTrans();
+ try {
+ $query->title = $title;
+ $query->keywords = $keywords;
+ $query->content = $content;
+ $query->status = $status;
+ $query->operation = $operation;
+ $query->updateTime = time();
+ $query->save();
+ Db::commit();
+ return ResponseHelper::success(' ','修改成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('修改失败:'.$e->getMessage());
+ }
+ }
+
+ /**
+ * 修改违禁词状态
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function setSensitiveWordStatus(){
+ $id = $this->request->param('id', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($id)){
+ return ResponseHelper::error('参数缺失');
+ }
+
+ $query = SensitiveWord::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find();
+ if (empty($query)){
+ return ResponseHelper::error('该素材已被删除或者不存在');
+ }
+ Db::startTrans();
+ try {
+ $query->status = !empty($query['status']) ? 0 : 1;;
+ $query->updateTime = time();
+ $query->save();
+ Db::commit();
+ return ResponseHelper::success(' ','修改成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('修改失败:'.$e->getMessage());
+ }
+ }
+
+
+ //==================================================== 违禁词管理 ====================================================
+
+
+
+
+
+ //=================================================== 关键词词管理 ====================================================
+
+ /**
+ * 关键词列表
+ * @return \think\response\Json
+ * @throws \think\db\exception\DataNotFoundException
+ * @throws \think\db\exception\ModelNotFoundException
+ * @throws \think\exception\DbException
+ */
+ public function getKeywords(){
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ $query = Keywords::where(['userId' => $userId,'companyId' => $companyId,'isDel' => 0])
+ ->order('id desc');
+ if (!empty($keyword)){
+ $query->where('title', 'like', '%'.$keyword.'%');
+ }
+ $total = $query->count();
+ $list = $query->page($page, $limit)->select()->toArray();
+
+
+ foreach ($list as $k => &$v){
+ $v['metailGroups'] = json_decode($v['metailGroups'],true);
+ $v['content'] = json_decode($v['content'],true);
+ $v['keywords'] = json_decode($v['keywords'],true);
+
+ $metailData = Material::where(['isDel' => 0,'userId' => $userId,'companyId' => $companyId])
+ ->whereIn('id',$v['metailGroups'])
+ ->select()->toArray();
+ $v['metailGroupsOptions'] = $metailData;
+
+
+ $user = Db::name('users')->where(['id' => $v['userId']])->field('username,account')->find();
+ if (!empty($user)){
+ $v['userName'] = !empty($user['username']) ? $user['username'] : $user['account'];
+ }else{
+ $v['userName'] = '';
+ }
+ }
+ unset($v);
+ return ResponseHelper::success(['list'=>$list,'total'=>$total]);
+ }
+
+
+ /**
+ * 关键词添加
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function createKeywords(){
+ $title = $this->request->param('title', '');
+ $type = $this->request->param('type', 0);
+ $keywords = $this->request->param('keywords', '');
+ $replyType = $this->request->param('replyType', 0);
+ $content = $this->request->param('content','');
+ $metailGroups = $this->request->param('metailGroups',[]);
+ $status = $this->request->param('status', 0);
+ $level = $this->request->param('level', 50);
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($title) || empty($keywords) || (empty(metailGroups) && empty($content))){
+ return ResponseHelper::error('参数缺失');
+ }
+
+ $keywords = explode(',',$keywords);
+
+ Db::startTrans();
+ try {
+ $query = new Keywords();
+ $query->title = $title;
+ $query->type = $type;
+ $query->keywords = !empty($keywords) ? json_encode($keywords,256) : json_encode([]);
+ $query->replyType = $replyType;
+ $query->content = !empty($content) ? json_encode($content,256) : json_encode([]);;
+ $query->metailGroups = !empty($metailGroups) ? json_encode($metailGroups,256) : json_encode([]);;
+ $query->status = $status;
+ $query->level = $level;
+ $query->userId = $userId;
+ $query->companyId = $companyId;
+ $query->createTime = time();
+ $query->updateTime = time();
+ $query->save();
+ Db::commit();
+ return ResponseHelper::success(' ','创建成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('创建失败:'.$e->getMessage());
+ }
+ }
+
+
+
+ /**
+ * 关键词详情
+ * @return \think\response\Json
+ */
+ public function detailsKeywords()
+ {
+ $id = $this->request->param('id', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+ if (empty($id)){
+ return ResponseHelper::error('参数缺失');
+ }
+ $data = Keywords::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find();
+ if (empty($data)){
+ return ResponseHelper::error('该素材已被删除或者不存在');
+ }
+
+ $data['metailGroups'] = json_decode($data['metailGroups'],true);
+ $metailData = Material::where(['isDel' => 0,'userId' => $userId,'companyId' => $companyId])
+ ->whereIn('id',$data['metailGroups'])
+ ->select()->toArray();
+ $data['metailGroupsOptions'] = $metailData;
+
+ $data['content'] = json_decode($data['content'],true);
+ $data['keywords'] = json_decode($data['keywords'],true);
+ $data['keywords'] = implode(',',$data['keywords']);
+ unset($data['createTime'],$data['updateTime'],$data['isDel'],$data['delTime']);
+ return ResponseHelper::success($data,'获取成功');
+ }
+
+ /**
+ * 关键词删除
+ * @return \think\response\Json
+ */
+ public function delKeywords()
+ {
+ $id = $this->request->param('id', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+ if (empty($id)){
+ return ResponseHelper::error('参数缺失');
+ }
+ $data = Keywords::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find();
+ if (empty($data)){
+ return ResponseHelper::error('该关键词已被删除或者不存在');
+ }
+ Db::startTrans();
+ try {
+ $data->isDel = 1;
+ $data->delTime = time();
+ $data->save();
+ Db::commit();
+ return ResponseHelper::success('','删除成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('删除失败:'.$e->getMessage());
+ }
+ }
+
+
+ /**
+ * 更新关键词
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function updateKeywords(){
+ $id = $this->request->param('id', '');
+ $title = $this->request->param('title', '');
+ $type = $this->request->param('type', 0);
+ $keywords = $this->request->param('keywords', '');
+ $replyType = $this->request->param('replyType', 0);
+ $content = $this->request->param('content','');
+ $metailGroups = $this->request->param('metailGroups','');
+ $status = $this->request->param('status', 0);
+ $level = $this->request->param('level', 50);
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($title) || empty($keywords) || (empty($metailGroups) && empty($content))){
+ return ResponseHelper::error('参数缺失');
+ }
+
+ $keywords = explode(',',$keywords);
+
+ $query = Keywords::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find();
+ if (empty($query)){
+ return ResponseHelper::error('该素材已被删除或者不存在');
+ }
+ Db::startTrans();
+ try {
+ $query->title = $title;
+ $query->type = $type;
+ $query->keywords = !empty($keywords) ? json_encode($keywords,256) : json_encode([]);
+ $query->replyType = $replyType;
+ $query->content = !empty($content) ? json_encode($content,256) : json_encode([]);;
+ $query->metailGroups = !empty($metailGroups) ? json_encode($metailGroups,256) : json_encode([]);;;
+ $query->status = $status;
+ $query->level = $level;
+ $query->save();
+ Db::commit();
+ return ResponseHelper::success(' ','修改成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('修改失败:'.$e->getMessage());
+ }
+ }
+
+ /**
+ * 修改关键词状态
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function setKeywordStatus(){
+ $id = $this->request->param('id', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($id)){
+ return ResponseHelper::error('参数缺失');
+ }
+
+ $query = Keywords::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find();
+ if (empty($query)){
+ return ResponseHelper::error('该素材已被删除或者不存在');
+ }
+ Db::startTrans();
+ try {
+ $query->status = !empty($query['status']) ? 0 : 1;
+ $query->updateTime = time();
+ $query->save();
+ Db::commit();
+ return ResponseHelper::success(' ','修改成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('修改失败:'.$e->getMessage());
+ }
+ }
+
+
+
+ //=================================================== 关键词词管理 ====================================================
+
+
+
+
+
+
+}
\ No newline at end of file
diff --git a/application/chukebao/controller/CustomerServiceController.php b/application/chukebao/controller/CustomerServiceController.php
new file mode 100644
index 0000000..d0eefb6
--- /dev/null
+++ b/application/chukebao/controller/CustomerServiceController.php
@@ -0,0 +1,51 @@
+getUserInfo('s2_accountId');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+ if (empty($accountId)){
+ return ResponseHelper::error('请先登录');
+ }
+
+ $accountIds1= Db::table('s2_wechat_friend')->where(['accountId' => $accountId,'isDeleted' => 0])->group('wechatAccountId')->column('wechatAccountId');
+ $accountIds2 = Db::table('s2_wechat_chatroom')->where(['accountId' => $accountId,'isDeleted' => 0])->group('wechatAccountId')->column('wechatAccountId');
+ // 确保即使有空数组也不会报错,并且去除重复值
+ $accountIds = array_unique(array_merge($accountIds1 ?: [], $accountIds2 ?: []));
+
+
+
+ $wechatAliveTime = time() - 86400 * 30;
+ $list = Db::table('s2_wechat_account')
+ ->whereIn('id',$accountIds)
+ ->where('wechatAliveTime','>',$wechatAliveTime)
+ ->order('id desc')
+ ->group('id')
+ ->select();
+ foreach ($list as $k=>&$v){
+ $v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s',$v['createTime']) : '';
+ $v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s',$v['updateTime']) : '';
+ $v['labels'] = json_decode($v['labels'],true);
+ $momentsSetting = Db::name('kf_moments_settings')->where(['userId' => $userId,'companyId' => $companyId,'wechatId' =>$v['id']])->find();
+ $v['momentsMax'] = !empty($momentsSetting['max']) ? $momentsSetting['max'] : 5;
+ $v['momentsNum'] = !empty($momentsSetting['sendNum']) ? $momentsSetting['sendNum'] : 0;
+
+ unset(
+ $v['accountUserName'],
+ $v['accountRealName'],
+ $v['accountNickname'],
+ );
+ }
+ unset($v);
+
+ return ResponseHelper::success($list);
+ }
+}
\ No newline at end of file
diff --git a/application/chukebao/controller/DataProcessing.php b/application/chukebao/controller/DataProcessing.php
new file mode 100644
index 0000000..af8070c
--- /dev/null
+++ b/application/chukebao/controller/DataProcessing.php
@@ -0,0 +1,206 @@
+getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+ $type = $this->request->param('type', '');
+ $wechatAccountId = $this->request->param('wechatAccountId', '');
+ //微信好友
+ $toAccountId = $this->request->param('toAccountId', '');
+ $wechatFriendId = $this->request->param('wechatFriendId', '');
+ $newRemark = $this->request->param('newRemark', '');
+ $labels = $this->request->param('labels', []);
+ //微信群
+ $wechatChatroomId = $this->request->param('wechatChatroomId', '');
+
+ //新消息
+ $friendMessage = $this->request->param('friendMessage', '');
+ $chatroomMessage = $this->request->param('chatroomMessage', '');
+
+ $typeData = [
+ 'CmdModifyFriendRemark', //好友修改备注 {newRemark、wechatAccountId、wechatFriendId}
+ 'CmdModifyFriendLabel', //好友修改标签 {labels、wechatAccountId、wechatFriendId}
+ 'CmdAllotFriend', //转让好友 {labels、wechatAccountId、wechatFriendId}
+ 'CmdChatroomOperate', //修改群信息 {chatroomName(群名)、announce(公告)、extra(公告)、wechatAccountId、wechatChatroomId}
+ 'CmdNewMessage', //接收消息
+ 'CmdSendMessageResult', //更新消息状态
+ 'CmdPinToTop', //置顶
+ ];
+
+ if (empty($type) || empty($wechatAccountId)) {
+ return ResponseHelper::error('参数缺失');
+ }
+
+ if (!in_array($type, $typeData)) {
+ return ResponseHelper::error('类型错误');
+ }
+ $msg = '';
+ $codee = 200;
+ switch ($type) {
+ case 'CmdModifyFriendRemark': //修改好友备注
+ if(empty($wechatFriendId) || empty($newRemark)){
+ return ResponseHelper::error('参数缺失');
+ }
+ $friend = WechatFriendModel::where(['id' => $wechatFriendId,'wechatAccountId' => $wechatAccountId])->find();
+ if(empty($friend)){
+ return ResponseHelper::error('好友不存在');
+ }
+ $friend->conRemark = $newRemark;
+ $friend->updateTime = time();
+ $friend->save();
+ $msg = '修改备成功';
+ break;
+ case 'CmdModifyFriendLabel': //修改好友标签
+ if(empty($wechatFriendId)){
+ return ResponseHelper::error('参数缺失');
+ }
+ $friend = WechatFriendModel::where(['id' => $wechatFriendId,'wechatAccountId' => $wechatAccountId])->find();
+ if(empty($friend)){
+ return ResponseHelper::error('好友不存在');
+ }
+ $friend->labels = json_encode($labels,256);
+ $friend->updateTime = time();
+ $friend->save();
+ $msg = '修标签成功';
+ break;
+ case 'CmdAllotFriend': //迁移好友
+ if(empty($toAccountId)){
+ return ResponseHelper::error('参数缺失');
+ }
+ if(empty($wechatFriendId) && empty($wechatChatroomId)){
+ return ResponseHelper::error('参数缺失');
+ }
+
+
+ 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)){
+ return ResponseHelper::error('参数缺失');
+ }
+
+ if(is_array($friendMessage) && is_array($chatroomMessage)){
+ return ResponseHelper::error('数据类型错误');
+ }
+
+
+ $messageController = new MessageController();
+ if (!empty($friendMessage)){
+ $res = $messageController->saveMessage($friendMessage[0]);
+ }else{
+ $res = $messageController->saveChatroomMessage($chatroomMessage[0]);
+ }
+ if (!empty($res)){
+ $msg = '消息记录成功';
+ }else{
+ $msg = '消息记录失败';
+ $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);
+ }
+}
\ No newline at end of file
diff --git a/application/chukebao/controller/FollowUpController.php b/application/chukebao/controller/FollowUpController.php
new file mode 100644
index 0000000..35d7d56
--- /dev/null
+++ b/application/chukebao/controller/FollowUpController.php
@@ -0,0 +1,144 @@
+request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+ $isRemind = $this->request->param('isRemind', '');
+ $isProcess = $this->request->param('isProcess', '');
+ $type = $this->request->param('type', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+
+ $where = [
+ ['companyId','=',$companyId],
+ ['userId' ,'=', $userId]
+ ];
+
+ if ($isRemind != '') {
+ $where[] = ['isRemind','=',$isRemind];
+ }
+ if ($type != '') {
+ $where[] = ['type','=',$type];
+ }
+ if ($isProcess != '') {
+ $where[] = ['isProcess','=',$isProcess];
+ }
+
+ if(!empty($keyword)){
+ $where[] = ['title|description','like','%'.$keyword.'%'];
+ }
+
+ $query = FollowUp::where($where);
+
+ $total = $query->count();
+ $list = $query->where($where)->page($page,$limit)->order('id desc')->select();
+
+
+ foreach ($list as &$item) {
+ $nickname = Db::table('s2_wechat_friend')->where(['id' => $item['friendId']])->value('nickname');
+ $item['nickname'] = !empty($nickname) ? $nickname : '-';
+ $item['reminderTime'] = date('Y-m-d H:i:s',$item['reminderTime']);
+ }
+ unset($item);
+
+ return ResponseHelper::success(['list'=>$list,'total'=>$total]);
+ }
+
+
+ /**
+ * 添加
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function create(){
+ $type = $this->request->param('type', 0);
+ $title = $this->request->param('title', '');
+ $reminderTime = $this->request->param('reminderTime', '');
+ $description = $this->request->param('description', '');
+ $friendId = $this->request->param('friendId', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($title) || empty($reminderTime) || empty($description) || empty($friendId)){
+ return ResponseHelper::error('参数缺失');
+ }
+ $friend = Db::table('s2_wechat_friend')->where(['id' => $friendId])->find();
+ if (empty($friend)) {
+ return ResponseHelper::error('好友不存在');
+ }
+
+
+ Db::startTrans();
+ try {
+ $FollowUp = new FollowUp();
+ $FollowUp->type = $type;
+ $FollowUp->title = $title;
+ $FollowUp->friendId = $friendId;
+ $FollowUp->reminderTime = !empty($reminderTime) ? strtotime($reminderTime) : time();
+ $FollowUp->description = $description;
+ $FollowUp->userId = $userId;
+ $FollowUp->companyId = $companyId;
+ $FollowUp->updateTime = time();
+ $FollowUp->createTime = time();
+ $FollowUp->save();
+ Db::commit();
+ return ResponseHelper::success(' ','创建成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('创建失败:'.$e->getMessage());
+ }
+ }
+
+
+ /**
+ * 处理代办事项
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function process(){
+ $ids = $this->request->param('ids','');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($ids)){
+ return ResponseHelper::error('参数缺失');
+ }
+ $ids = explode(',',$ids);
+
+ if (!is_array($ids)){
+ return ResponseHelper::error('格式错误');
+ }
+
+ $FollowUpIds = FollowUp::where(['userId' => $userId,'companyId' => $companyId,'isProcess' => 0])->whereIn('id',$ids)->column('id');
+ if (empty($FollowUpIds)){
+ return ResponseHelper::error('代办事项不存在');
+ }
+
+ Db::startTrans();
+ try {
+ FollowUp::whereIn('id',$FollowUpIds)->update(['isProcess' => 1,'isRemind' => 1,'updateTime' => time()]);
+ Db::commit();
+ return ResponseHelper::success(' ','已处理');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('处理失败:'.$e->getMessage());
+ }
+
+
+
+
+
+ }
+
+}
\ No newline at end of file
diff --git a/application/chukebao/controller/LoginController.php b/application/chukebao/controller/LoginController.php
new file mode 100644
index 0000000..832d055
--- /dev/null
+++ b/application/chukebao/controller/LoginController.php
@@ -0,0 +1,141 @@
+request->param('account', '');
+ $password = !empty($password) ? $password : $this->request->param('password', '');
+ $verifySessionId =!empty($verifySessionId) ? $verifySessionId : $this->request->param('verifySessionId', '');
+ $verifyCode = !empty($verifyCode) ? $verifyCode : $this->request->param('verifyCode', '');
+ $token = JwtUtil::getRequestToken();
+ $payload = '';
+ if (!empty($token)){
+ $payload = JwtUtil::verifyToken($token);
+ }
+
+ if ((empty($username) || empty($password)) && empty($payload)){
+ return ResponseHelper::error('请输入账号密码');
+ }
+
+ // 验证账号是否存在(支持账号或手机号登录)
+ if (empty($payload11)){
+ $user = Db::name('users')
+ ->where(function ($query) use ($username) {
+ $query->where('account', $username)->whereOr('phone', $username);
+ })
+ ->where(function ($query2) use ($password) {
+ $query2->where('passwordMd5', md5($password))->whereOr('passwordLocal', localEncrypt($password));
+ })
+ ->find();
+ }else{
+ $user = $payload;
+ }
+
+ if (empty($user)) {
+ return ResponseHelper::error('账号不存在或密码错误');
+ }
+
+ if($user['status'] != 1){
+ return ResponseHelper::error('账号已禁用');
+ }
+
+ //登录参数
+ $params = [
+ 'grant_type' => 'password',
+ 'username' => $user['account'],
+ 'password' => !empty($user['passwordLocal']) ? localDecrypt($user['passwordLocal']) : $password
+ ];
+ try {
+ // 调用登录接口获取token
+ $headerData = ['client:kefu-client'];
+ if (!empty($verifySessionId) && !empty($verifyCode)){
+ $headerData[] = 'verifysessionid:'.$verifySessionId;
+ $headerData[] = 'verifycode:'.$verifyCode;
+ }
+ $header = setHeader($headerData, '', 'plain');
+ $result = requestCurl('https://s2.siyuguanli.com:9991/token', $params, 'POST', $header);
+ $result = handleApiResponse($result);
+ if (isset($result['access_token']) && !empty($result['access_token'])) {
+ $kefuData['token'] = $result;
+ $headerData = ['client:kefu-client'];
+ $header = setHeader($headerData, $result['access_token']);
+ $result2 = requestCurl('https://s2.siyuguanli.com:9991/api/account/self', [], 'GET', $header, 'json');
+ $self = handleApiResponse($result2);
+ $kefuData['self'] = $self;
+ Db::name('users')->where('id', $user['id'])->update(['passwordLocal' => localEncrypt($params['password']),'updateTime' => time()]);
+ }else{
+ $kefuData = [
+ 'token' => [
+ "access_token"=> "27gINKZqGux6V4j9QLawOcTKlWXg-j4zxQjKvScvDTq-YlLcwIrDP2AFaNZKnOo9zLzepOBC8qrdXh4z9GxxkwE9TKGRQI1FjITRlMZzrim13IbSEbJUoywGs_BhDmIZnnPhfjqxDB1vjZgVtT2Kp4bxbUCV3i2uO_FTv_DT2G7NUFFLjq8oIuUrd_c1YXeYkH8m8Fw1AM4yPZJZyfdaHSSMOpJ2Bk2LAghnB6OaZCYWNFQcwWARsmh1BSAANUOAoadjkztZC7Fme-GGOm2sLo0WL6Mf26NfeLmnkluewTiPMyacD7RYclAR2LZ_8Mhwr3pwRg",
+ "token_type"=> "bearer",
+ "expires_in"=> 195519999,
+ "refresh_token"=> "a9545daa-d1c4-4c87-8c4c-b713631d4f0d"
+ ],
+ 'self' => [
+ 'account' => [
+ "id"=> 5538,
+ "realName"=> "测试",
+ "nickname"=> "",
+ "memo"=> "",
+ "avatar"=> "",
+ "userName"=> "wz_02",
+ "secret"=> "8f6f743395ad4198b6a4c0e6ca0e452f",
+ "accountType"=> 10,
+ "departmentId"=> 2130,
+ "useGoogleSecretKey"=> false,
+ "hasVerifyGoogleSecret"=> true
+ ],
+ 'tenant' => [
+ "id" => 242,
+ "name"=> "泉州市卡若网络技术有限公司",
+ "guid"=> "5E2C38F5A275450D935F3ECEC076124E",
+ "thirdParty"=> null,
+ "tenantType"=> 0,
+ "deployName"=> "deploy-s2"
+ ]
+ ]
+ ];
+ //return ResponseHelper::error($result['error_description']);
+ }
+
+
+ unset($user['passwordMd5'],$user['deleteTime']);
+ $userData['member'] = $user;
+
+ // 生成JWT令牌
+ $expired = 86400 * 30;
+ $token = JwtUtil::createToken($user, $expired);
+ $token_expired = time() + $expired;
+
+ $userData['token'] = $token;
+ $userData['token_expired'] = $token_expired;
+ $userData['kefuData'] = $kefuData;
+
+ return ResponseHelper::success($userData, '登录成功');
+ } catch (Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/chukebao/controller/MessageController.php b/application/chukebao/controller/MessageController.php
new file mode 100644
index 0000000..3ffeb9f
--- /dev/null
+++ b/application/chukebao/controller/MessageController.php
@@ -0,0 +1,476 @@
+baseUrl = Env::get('api.wechat_url');
+ $this->authorization = AuthService::getSystemAuthorization();
+ }
+
+ public function getList()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $accountId = $this->getUserInfo('s2_accountId');
+ if (empty($accountId)) {
+ return ResponseHelper::error('请先登录');
+ }
+
+ $friends = Db::table('s2_wechat_friend')
+ ->where(['accountId' => $accountId, 'isDeleted' => 0])
+ ->column('id,nickname,avatar,conRemark,labels,groupId,wechatAccountId,wechatId,extendFields,phone,region,isTop');
+
+
+ // 构建好友子查询
+ $friendSubQuery = Db::table('s2_wechat_friend')
+ ->where(['accountId' => $accountId, 'isDeleted' => 0])
+ ->field('id')
+ ->buildSql();
+
+ // 优化后的查询:使用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, wc.isTop
+ FROM s2_wechat_chatroom wc
+ INNER JOIN s2_wechat_message m ON wc.id = m.wechatChatroomId AND m.type = 2
+ INNER JOIN (
+ SELECT wechatChatroomId, MAX(wechatTime) as maxTime, MAX(id) as maxId
+ FROM s2_wechat_message
+ WHERE type = 2
+ GROUP BY wechatChatroomId
+ ) latest ON m.wechatChatroomId = latest.wechatChatroomId AND m.wechatTime = latest.maxTime AND m.id = latest.maxId
+ 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, 0 as isTop
+ FROM s2_wechat_message m
+ INNER JOIN (
+ SELECT wechatFriendId, MAX(wechatTime) as maxTime, MAX(id) as maxId
+ FROM s2_wechat_message
+ WHERE type = 1 AND wechatFriendId IN {$friendSubQuery}
+ GROUP BY wechatFriendId
+ ) latest ON m.wechatFriendId = latest.wechatFriendId AND m.wechatTime = latest.maxTime AND m.id = latest.maxId
+ WHERE m.type = 1 AND m.wechatFriendId IN {$friendSubQuery}
+ )
+ ORDER BY wechatTime DESC
+ LIMIT " . (($page - 1) * $limit) . ", {$limit}
+ ";
+
+ $list = Db::query($unionQuery);
+
+ // 对分页后的结果进行排序(按wechatTime降序)
+ usort($list, function ($a, $b) {
+ return $b['wechatTime'] <=> $a['wechatTime'];
+ });
+
+ // 批量统计未读数量(isRead=0),按好友/群聊分别聚合
+ $friendIds = [];
+ $chatroomIds = [];
+ foreach ($list as $row) {
+ if (!empty($row['wechatFriendId'])) {
+ $friendIds[] = $row['wechatFriendId'];
+ }
+ if (!empty($row['wechatChatroomId'])) {
+ $chatroomIds[] = $row['wechatChatroomId'];
+ }
+ }
+ $friendIds = array_values(array_unique(array_filter($friendIds)));
+ $chatroomIds = array_values(array_unique(array_filter($chatroomIds)));
+
+ $friendUnreadMap = [];
+ if (!empty($friendIds)) {
+ // 获取未读消息数量
+ $friendUnreadMap = Db::table('s2_wechat_message')
+ ->where(['isRead' => 0])
+ ->whereIn('wechatFriendId', $friendIds)
+ ->group('wechatFriendId')
+ ->column('COUNT(*) AS cnt', 'wechatFriendId');
+ }
+
+ $chatroomUnreadMap = [];
+ if (!empty($chatroomIds)) {
+ // 获取未读消息数量
+ $chatroomUnreadMap = Db::table('s2_wechat_message')
+ ->where(['isRead' => 0])
+ ->whereIn('wechatChatroomId', $chatroomIds)
+ ->group('wechatChatroomId')
+ ->column('COUNT(*) AS cnt', 'wechatChatroomId');
+ }
+
+ $aiTypeData = [];
+ if (!empty($friendIds)) {
+ $aiTypeData = FriendSettings::where('friendId', 'in', $friendIds)->column('friendId,type');
+ }
+
+ foreach ($list as $k => &$v) {
+
+ $createTime = !empty($v['createTime']) ? date('Y-m-d H:i:s', $v['createTime']) : '';
+ $wechatTime = !empty($v['wechatTime']) ? date('Y-m-d H:i:s', $v['wechatTime']) : '';
+
+
+ $unreadCount = 0;
+ $v['aiType'] = 0;
+ if (!empty($v['wechatFriendId'])) {
+ $v['nickname'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['nickname'] : '';
+ $v['avatar'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['avatar'] : '';
+ $v['conRemark'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['conRemark'] : '';
+ $v['groupId'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['groupId'] : '';
+ $v['wechatAccountId'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['wechatAccountId'] : '';
+ $v['wechatId'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['wechatId'] : '';
+ $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;
+ $v['aiType'] = isset($aiTypeData[$v['wechatFriendId']]) ? $aiTypeData[$v['wechatFriendId']] : 0;
+ unset($v['chatroomId']);
+ }
+
+ if (!empty($v['wechatChatroomId'])) {
+ $v['conRemark'] = '';
+ $unreadCount = isset($chatroomUnreadMap[$v['wechatChatroomId']]) ? (int)$chatroomUnreadMap[$v['wechatChatroomId']] : 0;
+ }
+
+ $v['id'] = !empty($v['wechatFriendId']) ? $v['wechatFriendId'] : $v['wechatChatroomId'];
+ $v['config'] = [
+ 'top' => !empty($v['isTop']) ? true : false,
+ 'unreadCount' => $unreadCount,
+ 'chat' => true,
+ 'msgTime' => $v['wechatTime'],
+ ];
+ $v['createTime'] = $createTime;
+ $v['lastUpdateTime'] = $wechatTime;
+
+ // 最新消息内容已经在UNION查询中获取,直接使用
+ $v['latestMessage'] = [
+ 'content' => $v['content'],
+ 'wechatTime' => $wechatTime
+ ];
+
+ unset($v['wechatFriendId'], $v['wechatChatroomId'],$v['isTop']);
+
+ }
+ unset($v);
+ return ResponseHelper::success($list);
+ }
+
+
+ public function readMessage()
+ {
+ $wechatFriendId = $this->request->param('wechatFriendId', '');
+ $wechatChatroomId = $this->request->param('wechatChatroomId', '');
+ $accountId = $this->getUserInfo('s2_accountId');
+ if (empty($accountId)) {
+ return ResponseHelper::error('请先登录');
+ }
+ if (empty($wechatChatroomId) && empty($wechatFriendId)) {
+ return ResponseHelper::error('参数缺失');
+ }
+
+ $where = [];
+ if (!empty($wechatChatroomId)) {
+ $where[] = ['wechatChatroomId', '=', $wechatChatroomId];
+ }
+
+ if (!empty($wechatFriendId)) {
+ $where[] = ['wechatFriendId', '=', $wechatFriendId];
+ }
+
+ Db::table('s2_wechat_message')->where($where)->update(['isRead' => 1]);
+ return ResponseHelper::success([]);
+ }
+
+
+ /**
+ * 获取单条消息发送状态(带轮询功能)
+ * @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', '');
+ $wechatChatroomId = $this->request->param('wechatChatroomId', '');
+ $wechatAccountId = $this->request->param('wechatAccountId', '');
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $from = $this->request->param('From', '');
+ $to = $this->request->param('To', '');
+ $olderData = $this->request->param('olderData', false);
+ $accountId = $this->getUserInfo('s2_accountId');
+ if (empty($accountId)) {
+ return ResponseHelper::error('请先登录');
+ }
+ if (empty($wechatChatroomId) && empty($wechatFriendId)) {
+ return ResponseHelper::error('参数缺失');
+ }
+
+ $where = [];
+ if (!empty($wechatChatroomId)) {
+ $where[] = ['wechatChatroomId', '=', $wechatChatroomId];
+ }
+
+ if (!empty($wechatFriendId)) {
+ $where[] = ['wechatFriendId', '=', $wechatFriendId];
+ }
+
+ if (!empty($From) && !empty($To)) {
+ $where[] = ['wechatTime', 'between', [$from, $to]];
+ }
+
+ $total = Db::table('s2_wechat_message')->where($where)->count();
+ $list = Db::table('s2_wechat_message')->where($where)->page($page, $limit)->order('id DESC')->select();
+
+ // 检查消息是否有sendStatus字段,如果有且不为0,则请求线上最新接口
+ foreach ($list as $k => &$item) {
+ // 检查是否存在sendStatus字段且不为0(0表示已发送成功)
+ 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());
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/application/chukebao/controller/MomentsController.php b/application/chukebao/controller/MomentsController.php
new file mode 100644
index 0000000..f0d6a4d
--- /dev/null
+++ b/application/chukebao/controller/MomentsController.php
@@ -0,0 +1,504 @@
+getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ // 获取请求参数
+ $text = $this->request->param('content', ''); // 朋友圈内容
+ $picUrlList = $this->request->param('picUrlList', []); // 图片列表
+ $videoUrl = $this->request->param('videoUrl', ''); // 视频链接
+ $link = $this->request->param('link', []); // 链接信息
+ $momentContentType = (int)$this->request->param('type', 1); // 内容类型 1文本 2图文 3视频 4链接
+ $publicMode = (int)$this->request->param('publicMode', 0); // 公开模式
+ $wechatIds = $this->request->param('wechatIds', []); // 微信账号ID列表
+ $labels = $this->request->param('labels', []); // 标签列表
+ $timingTime = $this->request->param('timingTime', date('Y-m-d H:i:s')); // 定时发布时间
+ $immediately = $this->request->param('immediately', false); // 是否立即发布
+
+ // 格式化时间字符串为统一格式
+ $timingTime = $this->normalizeTimingTime($timingTime);
+ if ($timingTime === false) {
+ return ResponseHelper::error('定时发布时间格式不正确');
+ }
+
+ // 参数验证
+ if (empty($text) && empty($picUrlList) && empty($videoUrl)) {
+ return ResponseHelper::error('朋友圈内容不能为空');
+ }
+
+ if (empty($wechatIds)) {
+ return ResponseHelper::error('请选择发布账号');
+ }
+
+ // 校验内容类型
+ if (!in_array($momentContentType, [1, 2, 3, 4])) {
+ return ResponseHelper::error('内容类型不合法,支持:1文本 2图文 3视频 4链接');
+ }
+
+ if(!empty($labels)){
+ $publicMode = 2;
+ }
+
+ // 根据内容类型校验必要参数
+ switch ($momentContentType) {
+ case 1: // 文本
+ if (empty($text)) {
+ return ResponseHelper::error('文本类型必须填写内容');
+ }
+ break;
+ case 2: // 图文
+ if (empty($text) || empty($picUrlList)) {
+ return ResponseHelper::error('图文类型必须填写内容和上传图片');
+ }
+ break;
+ case 3: // 视频
+ if (empty($videoUrl)) {
+ return ResponseHelper::error('视频类型必须上传视频');
+ }
+ break;
+ case 4: // 链接
+ if (empty($link)) {
+ return ResponseHelper::error('链接类型必须填写链接信息');
+ }
+ if (empty($link['url'])) {
+ return ResponseHelper::error('链接类型必须填写链接地址');
+ }
+ if (empty($link['desc'])) {
+ return ResponseHelper::error('链接类型必须填写链接描述');
+ }
+ if (empty($link['image'])) {
+ return ResponseHelper::error('链接类型必须填写链接图片');
+ }
+ break;
+ }
+
+ // 处理链接信息 - 所有链接都必须验证
+ if (!empty($link)) {
+ $link = [
+ 'desc' => $link['desc'] ?? '',
+ 'image' => $link['image'] ?? '',
+ 'url' => $link['url'] ?? ''
+ ];
+
+ // 验证链接URL格式
+ if (!empty($link['url']) && !filter_var($link['url'], FILTER_VALIDATE_URL)) {
+ return ResponseHelper::error('链接地址格式不正确');
+ }
+ } else {
+ $link = ['desc' => '', 'image' => '', 'url' => ''];
+ }
+
+ // 构建发布账号列表
+ $jobPublishWechatMomentsItems = $this->buildJobPublishWechatMomentsItems($wechatIds, $labels);
+ if (empty($jobPublishWechatMomentsItems)) {
+ return ResponseHelper::error('无法获取有效的发布账号信息');
+ }
+
+ try {
+ // 构建发送数据
+ $sendData = [
+ 'altList' => '',
+ 'beginTime' => $timingTime,
+ 'endTime' => date('Y-m-d H:i:s', strtotime($timingTime) + 3600),
+ 'immediately' => $immediately,
+ 'isUseLocation' => false,
+ 'jobPublishWechatMomentsItems' => $jobPublishWechatMomentsItems,
+ 'lat' => 0,
+ 'lng' => 0,
+ 'link' => $link,
+ 'momentContentType' => $momentContentType,
+ 'picUrlList' => $picUrlList,
+ 'poiAddress' => '',
+ 'poiName' => '',
+ 'publicMode' => $publicMode,
+ 'text' => $text,
+ 'timingTime' => $timingTime ?: date('Y-m-d H:i:s'),
+ 'videoUrl' => $videoUrl
+ ];
+
+ // 保存到数据库
+ $moments = new KfMoments();
+ $moments->companyId = $companyId;
+ $moments->userId = $userId;
+ $moments->sendData = json_encode($sendData, 256);
+ $nowTs = time();
+ $moments->createTime = $nowTs;
+ $moments->updateTime = $nowTs;
+ $moments->isDel = 0;
+ $moments->delTime = null;
+ $moments->isSend = $immediately ? 1 : 0;
+ $moments->sendTime = $immediately ? $nowTs : strtotime($timingTime);
+ $moments->save();
+
+ // 如果立即发布,调用发布接口
+ if ($immediately) {
+ $this->publishMoments($sendData);
+ }
+ return ResponseHelper::success('', '朋友圈创建成功');
+ } catch (\Exception $e) {
+ return ResponseHelper::error('创建失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 编辑朋友圈
+ * @return \think\response\Json
+ */
+ public function update()
+ {
+ $id = (int)$this->request->param('id', 0);
+ if ($id <= 0) {
+ return ResponseHelper::error('ID不合法');
+ }
+
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ // 获取请求参数(与创建一致的字段名)
+ $text = $this->request->param('content', '');
+ $picUrlList = $this->request->param('picUrlList', []);
+ $videoUrl = $this->request->param('videoUrl', '');
+ $link = $this->request->param('link', []);
+ $momentContentType = (int)$this->request->param('type', 1);
+ $publicMode = (int)$this->request->param('publicMode', 0);
+ $wechatIds = $this->request->param('wechatIds', []);
+ $labels = $this->request->param('labels', []);
+ $timingTime = $this->request->param('timingTime', date('Y-m-d H:i:s'));
+ $immediately = $this->request->param('immediately', false);
+
+ // 格式化时间字符串为统一格式
+ $timingTime = $this->normalizeTimingTime($timingTime);
+ if ($timingTime === false) {
+ return ResponseHelper::error('定时发布时间格式不正确');
+ }
+
+ // 读取待编辑记录
+ /** @var KfMoments|null $moments */
+ $moments = KfMoments::where(['id' => $id, 'companyId' => $companyId, 'userId' => $userId, 'isDel' => 0])->find();
+ if (empty($moments)) {
+ return ResponseHelper::error('朋友圈不存在');
+ }
+
+ // 参数校验
+ if (empty($text) && empty($picUrlList) && empty($videoUrl)) {
+ return ResponseHelper::error('朋友圈内容不能为空');
+ }
+ if (empty($wechatIds)) {
+ return ResponseHelper::error('请选择发布账号');
+ }
+ if (!in_array($momentContentType, [1, 2, 3, 4])) {
+ return ResponseHelper::error('内容类型不合法,支持:1文本 2图文 3视频 4链接');
+ }
+ if (!empty($labels)) {
+ $publicMode = 2;
+ }
+ switch ($momentContentType) {
+ case 1:
+ if (empty($text)) {
+ return ResponseHelper::error('文本类型必须填写内容');
+ }
+ break;
+ case 2:
+ if (empty($text) || empty($picUrlList)) {
+ return ResponseHelper::error('图文类型必须填写内容和上传图片');
+ }
+ break;
+ case 3:
+ if (empty($videoUrl)) {
+ return ResponseHelper::error('视频类型必须上传视频');
+ }
+ break;
+ case 4:
+ if (empty($link)) {
+ return ResponseHelper::error('链接类型必须填写链接信息');
+ }
+ if (empty($link['url'])) {
+ return ResponseHelper::error('链接类型必须填写链接地址');
+ }
+ if (empty($link['desc'])) {
+ return ResponseHelper::error('链接类型必须填写链接描述');
+ }
+ if (empty($link['image'])) {
+ return ResponseHelper::error('链接类型必须填写链接图片');
+ }
+ break;
+ }
+ if (!empty($link)) {
+ $link = [
+ 'desc' => $link['desc'] ?? '',
+ 'image' => $link['image'] ?? '',
+ 'url' => $link['url'] ?? ''
+ ];
+ if (!empty($link['url']) && !filter_var($link['url'], FILTER_VALIDATE_URL)) {
+ return ResponseHelper::error('链接地址格式不正确');
+ }
+ } else {
+ $link = ['desc' => '', 'image' => '', 'url' => ''];
+ }
+
+ // 构建账号列表
+ $jobPublishWechatMomentsItems = $this->buildJobPublishWechatMomentsItems($wechatIds, $labels);
+ if (empty($jobPublishWechatMomentsItems)) {
+ return ResponseHelper::error('无法获取有效的发布账号信息');
+ }
+
+ try {
+ $sendData = [
+ 'altList' => '',
+ 'beginTime' => $timingTime,
+ 'endTime' => date('Y-m-d H:i:s', strtotime($timingTime) + 1800),
+ 'immediately' => $immediately,
+ 'isUseLocation' => false,
+ 'jobPublishWechatMomentsItems' => $jobPublishWechatMomentsItems,
+ 'lat' => 0,
+ 'lng' => 0,
+ 'link' => $link,
+ 'momentContentType' => $momentContentType,
+ 'picUrlList' => $picUrlList,
+ 'poiAddress' => '',
+ 'poiName' => '',
+ 'publicMode' => $publicMode,
+ 'text' => $text,
+ 'timingTime' => $timingTime ?: date('Y-m-d H:i:s'),
+ 'videoUrl' => $videoUrl
+ ];
+
+ $moments->sendData = json_encode($sendData, 256);
+ $moments->isSend = $immediately ? 1 : 0;
+ $moments->sendTime = $immediately ? time() : strtotime($timingTime);
+ $moments->updateTime = time();
+ $moments->save();
+
+ if ($immediately) {
+ $this->publishMoments($sendData);
+ }
+
+ return ResponseHelper::success('', '朋友圈更新成功');
+ } catch (\Exception $e) {
+ return ResponseHelper::error('更新失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 构建发布账号列表
+ * @param array $wechatIds 微信账号ID列表
+ * @param array $labels 标签列表
+ * @return array
+ */
+ private function buildJobPublishWechatMomentsItems($wechatIds, $labels)
+ {
+ try {
+ // 查询微信账号信息
+ $wechatAccounts = Db::table('s2_wechat_account')
+ ->whereIn('id', $wechatIds)
+ ->field('id,labels')
+ ->select();
+ if (empty($wechatAccounts)) {
+ return [];
+ }
+
+ $result = [];
+ foreach ($wechatAccounts as $account) {
+ $accountLabels = [];
+
+ // 如果账号有标签,解析标签
+ if (!empty($account['labels'])) {
+ $accountLabels = is_string($account['labels'])
+ ? json_decode($account['labels'], true)
+ : $account['labels'];
+ }
+
+ // 取传入标签与账号标签的交集
+ $finalLabels = array_intersect($labels, $accountLabels);
+
+ $result[] = [
+ 'wechatAccountId' => $account['id'],
+ 'labels' => array_values($finalLabels), // 重新索引数组
+ 'comments' => []
+ ];
+ }
+
+ return $result;
+
+ } catch (\Exception $e) {
+ \think\facade\Log::error('构建发布账号列表失败:' . $e->getMessage());
+ return [];
+ }
+ }
+
+ /**
+ * 发布朋友圈到微信
+ * @param array $sendData
+ * @return bool
+ */
+ private function publishMoments($sendData)
+ {
+ try {
+ // 这里调用实际的朋友圈发布接口
+ // 根据您的系统架构,可能需要调用 WebSocket 或其他服务
+ // 示例:调用 MomentsController 的 addJob 方法
+ $moments = new \app\api\controller\MomentsController();
+ return $moments->addJob($sendData);
+ } catch (\Exception $e) {
+ // 记录错误日志
+ \think\facade\Log::error('朋友圈发布失败:' . $e->getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * 获取朋友圈列表
+ * @return \think\response\Json
+ */
+ public function getList()
+ {
+ $page = (int)$this->request->param('page', 1);
+ $limit = (int)$this->request->param('limit', 10);
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ try {
+ $list = KfMoments::where(['companyId' => $companyId, 'userId' => $userId, 'isDel' => 0])
+ ->order('createTime desc')
+ ->page($page, $limit)
+ ->select();
+
+ $total = KfMoments::where(['companyId' => $companyId, 'userId' => $userId, 'isDel' => 0])->count();
+
+ // 处理数据
+ $data = [];
+ foreach ($list as $item) {
+ $sendData = json_decode($item->sendData,true);
+ $data[] = [
+ 'id' => $item->id,
+ 'content' => $sendData['text'] ?? '',
+ 'momentContentType' => $sendData['momentContentType'] ?? 1,
+ 'picUrlList' => $sendData['picUrlList'] ?? [],
+ 'videoUrl' => $sendData['videoUrl'] ?? '',
+ 'link' => $sendData['link'] ?? [],
+ 'publicMode' => $sendData['publicMode'] ?? 2,
+ 'isSend' => $item->isSend,
+ 'sendTime' => date('Y-m-d H:i:s',$item->sendTime),
+ 'accountCount' => count($sendData['jobPublishWechatMomentsItems'] ?? [])
+ ];
+ }
+
+ return ResponseHelper::success([
+ 'list' => $data,
+ 'total' => $total,
+ 'page' => $page,
+ 'limit' => $limit
+ ], '获取成功');
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('获取失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 删除朋友圈
+ * @return \think\response\Json
+ */
+ public function delete()
+ {
+ $id = (int)$this->request->param('id', 0);
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if ($id <= 0) {
+ return ResponseHelper::error('ID不合法');
+ }
+
+ try {
+ $moments = KfMoments::where(['id' => $id, 'companyId' => $companyId, 'userId' => $userId, 'isDel' => 0])->find();
+ if (empty($moments)) {
+ return ResponseHelper::error('朋友圈不存在');
+ }
+
+ $moments->isDel = 1;
+ $moments->delTime = time();
+ $moments->updateTime = time();
+ $moments->save();
+ return ResponseHelper::success([], '删除成功');
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('删除失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 规范化时间字符串为 Y-m-d H:i:s 格式
+ * 支持多种时间格式:
+ * - "2026年1月5日15:43:00"
+ * - "2026-01-05 15:43:00"
+ * - "2026/01/05 15:43:00"
+ * - 时间戳
+ * @param string|int $timingTime 时间字符串或时间戳
+ * @return string|false 格式化后的时间字符串,失败返回false
+ */
+ private function normalizeTimingTime($timingTime)
+ {
+ if (empty($timingTime)) {
+ return date('Y-m-d H:i:s');
+ }
+
+ // 如果是时间戳
+ if (is_numeric($timingTime) && strlen($timingTime) == 10) {
+ return date('Y-m-d H:i:s', $timingTime);
+ }
+
+ // 如果是毫秒时间戳
+ if (is_numeric($timingTime) && strlen($timingTime) == 13) {
+ return date('Y-m-d H:i:s', intval($timingTime / 1000));
+ }
+
+ // 如果已经是标准格式,直接返回
+ if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $timingTime)) {
+ return $timingTime;
+ }
+
+ // 处理中文日期格式:2026年1月5日15:43:00 或 2026年01月05日15:43:00
+ if (preg_match('/^(\d{4})年(\d{1,2})月(\d{1,2})日(\d{1,2}):(\d{1,2}):(\d{1,2})$/', $timingTime, $matches)) {
+ $year = $matches[1];
+ $month = str_pad($matches[2], 2, '0', STR_PAD_LEFT);
+ $day = str_pad($matches[3], 2, '0', STR_PAD_LEFT);
+ $hour = str_pad($matches[4], 2, '0', STR_PAD_LEFT);
+ $minute = str_pad($matches[5], 2, '0', STR_PAD_LEFT);
+ $second = str_pad($matches[6], 2, '0', STR_PAD_LEFT);
+ return "{$year}-{$month}-{$day} {$hour}:{$minute}:{$second}";
+ }
+
+ // 处理中文日期格式(无秒):2026年1月5日15:43
+ if (preg_match('/^(\d{4})年(\d{1,2})月(\d{1,2})日(\d{1,2}):(\d{1,2})$/', $timingTime, $matches)) {
+ $year = $matches[1];
+ $month = str_pad($matches[2], 2, '0', STR_PAD_LEFT);
+ $day = str_pad($matches[3], 2, '0', STR_PAD_LEFT);
+ $hour = str_pad($matches[4], 2, '0', STR_PAD_LEFT);
+ $minute = str_pad($matches[5], 2, '0', STR_PAD_LEFT);
+ return "{$year}-{$month}-{$day} {$hour}:{$minute}:00";
+ }
+
+ // 尝试使用 strtotime 解析其他格式
+ $timestamp = strtotime($timingTime);
+ if ($timestamp !== false) {
+ return date('Y-m-d H:i:s', $timestamp);
+ }
+
+ // 如果所有方法都失败,返回 false
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/application/chukebao/controller/NoticeController.php b/application/chukebao/controller/NoticeController.php
new file mode 100644
index 0000000..d755eb9
--- /dev/null
+++ b/application/chukebao/controller/NoticeController.php
@@ -0,0 +1,116 @@
+request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+ $accountId = $this->getUserInfo('s2_accountId');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($accountId)) {
+ return ResponseHelper::error('请先登录');
+ }
+
+ $noRead = NoticeModel::where(['userId' => $userId, 'companyId' => $companyId,'isRead' => 0])->count();
+
+ $query = NoticeModel::where(['userId' => $userId, 'companyId' => $companyId])
+ ->order('id desc');
+ if (!empty($keyword)) {
+ $query->where('title|message', 'like', '%' . $keyword . '%');
+ }
+ $total = $query->count();
+ $list = $query->page($page, $limit)->order('isRead ASC,id DESC')->select()->toArray();
+
+
+ foreach ($list as $k => &$v) {
+ if ($v['type'] == 1) {
+ $friendId = ToDo::where(['id' => $v['bindId']])->value('friendId');
+ } elseif ($v['type'] == 2) {
+ $friendId = FollowUp::where(['id' => $v['bindId']])->value('friendId');
+ }
+ if (!empty($friendId)) {
+ $friend = Db::table('s2_wechat_friend')->where(['id' => $friendId])->field('nickname,avatar')->find();
+ } else {
+ $friend = ['nickname' => '', 'avatar' => ''];
+ }
+ $v['friendData'] = $friend;
+
+ $v['readTime'] = !empty($v['readTime']) ? date('Y-m-d H:i:s', $v['readTime']) : '';
+ }
+ unset($v);
+ return ResponseHelper::success(['list' => $list, 'total' => $total,'noRead' => $noRead]);
+ }
+
+
+ public function readMessage()
+ {
+ $id = $this->request->param('id', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($id)) {
+ return ResponseHelper::error('参数缺失');
+ }
+ Db::startTrans();
+ try {
+ $notice = NoticeModel::where(['userId' => $userId, 'companyId' => $companyId, 'id' => $id, 'isRead' => 0])->find();
+ if (empty($notice)) {
+ return ResponseHelper::error('该消息不存在或标记已读');
+ }
+ $notice->isRead = 1;
+ $notice->readTime = time();
+ $notice->save();
+ if ($notice->type == 1) {
+ ToDo::where(['userId' => $userId, 'companyId' => $companyId, 'id' => $notice->bindId])->update(['isProcess' => 1, 'updateTime' => time()]);
+ } elseif ($notice->type == 2) {
+ FollowUp::where(['userId' => $userId, 'companyId' => $companyId, 'id' => $notice->bindId])->update(['isProcess' => 1, 'updateTime' => time()]);
+ }
+ Db::commit();
+ return ResponseHelper::success(' ', '处理成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('处理失败:' . $e->getMessage());
+ }
+ }
+
+
+ public function readAll()
+ {
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ Db::startTrans();
+ try {
+ $noticeData = NoticeModel::where(['userId' => $userId, 'companyId' => $companyId, 'isRead' => 0])->select()->toArray();
+ if (empty($noticeData)) {
+ return ResponseHelper::error('暂未有新消息');
+ }
+ NoticeModel::where(['userId' => $userId, 'companyId' => $companyId, 'isRead' => 0])->update(['isRead' => 1, 'readTime' => time()]);
+ FollowUp::where(['userId' => $userId, 'companyId' => $companyId, 'isProcess' => 0])->update(['isProcess' => 1, 'updateTime' => time()]);
+ ToDo::where(['userId' => $userId, 'companyId' => $companyId, 'isProcess' => 0])->update(['isProcess' => 1, 'updateTime' => time()]);
+ Db::commit();
+ return ResponseHelper::success(' ', '全部已读');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('处理失败:' . $e->getMessage());
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/application/chukebao/controller/QuestionsController.php b/application/chukebao/controller/QuestionsController.php
new file mode 100644
index 0000000..21096bb
--- /dev/null
+++ b/application/chukebao/controller/QuestionsController.php
@@ -0,0 +1,233 @@
+request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+ $accountId = $this->getUserInfo('s2_accountId');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($accountId)){
+ return ResponseHelper::error('请先登录');
+ }
+ $query = Questions::where(['userId' => $userId,'companyId' => $companyId,'isDel' => 0])
+ ->order('id desc');
+ if (!empty($keyword)){
+ $query->where('questions|answers', 'like', '%'.$keyword.'%');
+ }
+ $total = $query->count();
+ $list = $query->page($page, $limit)->select()->toArray();
+
+
+ foreach ($list as $k => &$v){
+ $user = Db::name('users')->where(['id' => $v['userId']])->field('username,account')->find();
+ if (!empty($user)){
+ $v['userName'] = !empty($user['username']) ? $user['username'] : $user['account'];
+ }else{
+ $v['userName'] = '';
+ }
+ $v['answers'] = json_decode($v['answers'],true);
+ }
+ unset($v);
+ return ResponseHelper::success(['list'=>$list,'total'=>$total]);
+ }
+
+
+ /**
+ * 新增
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function create(){
+
+ $type = $this->request->param('type', 0);
+ $questions = $this->request->param('questions', '');
+ $answers = $this->request->param('answers', []);
+ $status = $this->request->param('status', 0);
+ $accountId = $this->getUserInfo('s2_accountId');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($accountId)){
+ return ResponseHelper::error('请先登录');
+ }
+
+ if (empty($questions) || empty($answers)){
+ return ResponseHelper::error('问题和答案不能为空');
+ }
+
+ Db::startTrans();
+ try {
+ $questionsModel = new Questions();
+ $questionsModel->type = $type;
+ $questionsModel->questions = $questions;
+ $questionsModel->answers = !empty($answers) ? json_encode($answers,256) : json_encode([],256);
+ $questionsModel->status = $status;
+ $questionsModel->accountId = $accountId;
+ $questionsModel->userId = $userId;
+ $questionsModel->companyId = $companyId;
+ $questionsModel->createTime = time();
+ $questionsModel->updateTime = time();
+ $questionsModel->save();
+ Db::commit();
+ return ResponseHelper::success(' ','创建成功');
+ } 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);
+ $accountId = $this->getUserInfo('s2_accountId');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+ $type = $this->request->param('type', 0);
+ $questions = $this->request->param('questions', '');
+ $answers = $this->request->param('answers', []);
+ $status = $this->request->param('status', 0);
+ $accountId = $this->getUserInfo('s2_accountId');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($accountId)){
+ return ResponseHelper::error('请先登录');
+ }
+
+ if (empty($id)){
+ return ResponseHelper::error('参数缺失');
+ }
+
+ if (empty($questions) || empty($answers)){
+ return ResponseHelper::error('问题和答案不能为空');
+ }
+ Db::startTrans();
+ try {
+ $questionsData = Questions::where(['id' => $id,'userId' => $userId,'companyId' => $companyId,'isDel' => 0])->find();
+ $questionsData->type = $type;
+ $questionsData->questions = $questions;
+ $questionsData->answers = !empty($answers) ? json_encode($answers,256) : json_encode([],256);
+ $questionsData->status = $status;
+ $questionsData->accountId = $accountId;
+ $questionsData->userId = $userId;
+ $questionsData->companyId = $companyId;
+ $questionsData->updateTime = time();
+ $questionsData->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);
+ $accountId = $this->getUserInfo('s2_accountId');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($accountId)){
+ return ResponseHelper::error('请先登录');
+ }
+
+ if (empty($id)){
+ return ResponseHelper::error('参数缺失');
+ }
+ $questions = Questions::where(['id' => $id,'userId' => $userId,'companyId' => $companyId,'isDel' => 0])->find();
+
+ if (empty($questions)){
+ return ResponseHelper::error('该问题不存在或者已删除');
+ }
+ $res = Questions::where(['id' => $id])->update(['isDel' => 1,'deleteTime' => time()]);
+
+ if (!empty($res)){
+ return ResponseHelper::success('','已删除');
+ }else{
+ return ResponseHelper::error('删除失败');
+ }
+
+ }
+
+
+ /**
+ * 详情
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function detail(){
+
+ $id = $this->request->param('id', 0);
+ $accountId = $this->getUserInfo('s2_accountId');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($accountId)){
+ return ResponseHelper::error('请先登录');
+ }
+
+ if (empty($id)){
+ return ResponseHelper::error('参数缺失');
+ }
+ $questions = Questions::where(['id' => $id,'userId' => $userId,'companyId' => $companyId,'isDel' => 0])->find();
+
+ if (empty($questions)){
+ return ResponseHelper::error('该问题不存在或者已删除');
+ }
+
+ $questions['answers'] = json_decode($questions['answers'],true);
+ $user = Db::name('users')->where(['id' => $questions['userId']])->field('username,account')->find();
+ if (!empty($user)){
+ $questions['userName'] = !empty($user['username']) ? $user['username'] : $user['account'];
+ }else{
+ $questions['userName'] = '';
+ }
+
+ unset(
+ $questions['isDel'],
+ $questions['deleteTime'],
+ $questions['createTime'],
+ $questions['updateTime']
+ );
+
+ return ResponseHelper::success($questions,'获取成功');
+
+
+ }
+
+
+
+
+}
\ No newline at end of file
diff --git a/application/chukebao/controller/ReplyController.php b/application/chukebao/controller/ReplyController.php
new file mode 100644
index 0000000..fb1dea7
--- /dev/null
+++ b/application/chukebao/controller/ReplyController.php
@@ -0,0 +1,371 @@
+request->param('replyType', 0);
+ $keyword = $this->request->param('keyword', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ try {
+ // 构建分组查询条件
+ $groupWhere = [
+ ['isDel','=',0]
+ ];
+ switch ($replyType) {
+ case 0:
+ //公共快捷语
+ $groupWhere[] = ['replyType', '=', 0];
+ break;
+ case 1:
+ //私有快捷语
+ $groupWhere[] = ['companyId', '=', $companyId];
+ $groupWhere[] = ['userId', '=', $userId];
+ $groupWhere[] = ['replyType', '=', 1];
+ break;
+ case 2:
+ //公司快捷语
+ $groupWhere[] = ['companyId', '=', $companyId];
+ $groupWhere[] = ['replyType', '=', 2];
+ break;
+ default:
+ $groupWhere[] = ['replyType', '=', 0];
+ break;
+ }
+
+
+ if (!empty($keyword)) {
+ $groupWhere[] = ['groupName','like', '%' . $keyword . '%'];
+ }
+
+ // 获取所有分组
+ $allGroups = ReplyGroup::where($groupWhere)
+ ->order('sortIndex asc,id DESC')
+ ->select();
+ // 构建树形结构
+ $result = $this->buildGroupTree($allGroups, $keyword);
+
+ return ResponseHelper::success($result, '获取成功');
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('获取失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 新增快捷语分组
+ * @return \think\response\Json
+ */
+ public function addGroup()
+ {
+ $groupName = $this->request->param('groupName', '');
+ $parentId = (int)$this->request->param('parentId', 0);
+ $replyType = (int)$this->request->param('replyType', 0); // 0公共 1私有 2公司
+ $sortIndex = (string)$this->request->param('sortIndex', 50);
+
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+ $accountId = $this->getUserInfo('s2_accountId');
+
+ if ($groupName === '') {
+ return ResponseHelper::error('分组名称不能为空');
+ }
+
+ try {
+ $data = [
+ 'groupName' => $groupName,
+ 'parentId' => $parentId,
+ 'replyType' => $replyType,
+ 'sortIndex' => $sortIndex,
+ // 兼容现有程序中使用到的字段
+ 'companyId' => $companyId,
+ 'userId' => $userId,
+ ];
+
+ /** @var ReplyGroup $group */
+ $group = new ReplyGroup();
+ $group->save($data);
+
+ return ResponseHelper::success($group->toArray(), '创建成功');
+ } catch (\Exception $e) {
+ return ResponseHelper::error('创建失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 新增快捷语
+ * @return \think\response\Json
+ */
+ public function addReply()
+ {
+ $groupId = (int)$this->request->param('groupId', 0);
+ $title = $this->request->param('title', '');
+ $msgType = (int)$this->request->param('msgType', 1); // 1文本 3图片 43视频 49链接 等
+ $content = $this->request->param('content', '');
+ $sortIndex = (string)$this->request->param('sortIndex', 50);
+
+ $accountId = $this->getUserInfo('s2_accountId');
+ $companyId = $this->getUserInfo('companyId');
+ $userId = $this->getUserInfo('id');
+
+ if ($groupId <= 0) {
+ return ResponseHelper::error('分组ID不合法');
+ }
+ if ($title === '') {
+ return ResponseHelper::error('标题不能为空');
+ }
+
+ try {
+ $now = time();
+ $data = [
+ 'tenantId' => $companyId,
+ 'groupId' => $groupId,
+ 'accountId' => $accountId,
+ 'title' => $title,
+ 'msgType' => $msgType,
+ 'content' => $content,
+ 'sortIndex' => $sortIndex,
+ 'createTime' => $now,
+ 'lastUpdateTime' => $now,
+ 'userId' => $userId,
+ ];
+
+ /** @var Reply $reply */
+ $reply = new Reply();
+ $reply->save($data);
+
+ return ResponseHelper::success($reply->toArray(), '创建成功');
+ } catch (\Exception $e) {
+ return ResponseHelper::error('创建失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 编辑快捷语分组
+ * @return \think\response\Json
+ */
+ public function updateGroup()
+ {
+ $id = (int)$this->request->param('id', 0);
+ if ($id <= 0) {
+ return ResponseHelper::error('分组ID不合法');
+ }
+
+ $data = [];
+ $groupName = $this->request->param('groupName', null);
+ $parentId = $this->request->param('parentId', null);
+ $replyType = $this->request->param('replyType', null);
+ $sortIndex = $this->request->param('sortIndex', null);
+
+ if ($groupName !== null) $data['groupName'] = $groupName;
+ if ($parentId !== null) $data['parentId'] = (int)$parentId;
+ if ($replyType !== null) $data['replyType'] = (int)$replyType;
+ if ($sortIndex !== null) $data['sortIndex'] = (string)$sortIndex;
+
+ if (empty($data)) {
+ return ResponseHelper::error('无可更新字段');
+ }
+
+ try {
+ $group = ReplyGroup::where(['id' => $id,'isDel' => 0])->find();
+ if (empty($group)) {
+ return ResponseHelper::error('分组不存在');
+ }
+ $group->save($data);
+ return ResponseHelper::success($group->toArray(), '更新成功');
+ } catch (\Exception $e) {
+ return ResponseHelper::error('更新失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 假删除快捷语分组
+ * @return \think\response\Json
+ */
+ public function deleteGroup()
+ {
+ $id = (int)$this->request->param('id', 0);
+ if ($id <= 0) {
+ return ResponseHelper::error('分组ID不合法');
+ }
+ try {
+ $group = ReplyGroup::where(['id' => $id,'isDel' => 0])->find();
+ if (empty($group)) {
+ return ResponseHelper::error('分组不存在');
+ }
+ $group->save(['isDel' => 1,'delTime' => time()]);
+ return ResponseHelper::success([], '删除成功');
+ } catch (\Exception $e) {
+ return ResponseHelper::error('删除失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 编辑快捷语
+ * @return \think\response\Json
+ */
+ public function updateReply()
+ {
+ $id = (int)$this->request->param('id', 0);
+ if ($id <= 0) {
+ return ResponseHelper::error('快捷语ID不合法');
+ }
+
+ $data = [];
+ $groupId = $this->request->param('groupId', null);
+ $title = $this->request->param('title', null);
+ $msgType = $this->request->param('msgType', null);
+ $content = $this->request->param('content', null);
+ $sortIndex = $this->request->param('sortIndex', null);
+
+ if ($groupId !== null) $data['groupId'] = (int)$groupId;
+ if ($title !== null) $data['title'] = $title;
+ if ($msgType !== null) $data['msgType'] = (int)$msgType;
+ if ($content !== null) $data['content'] = $content;
+ if ($sortIndex !== null) $data['sortIndex'] = (string)$sortIndex;
+ if (!empty($data)) {
+ $data['lastUpdateTime'] = time();
+ }
+
+ if (empty($data)) {
+ return ResponseHelper::error('无可更新字段');
+ }
+
+ try {
+ $reply = Reply::where(['id' => $id,'isDel' => 0])->find();
+ if (empty($reply)) {
+ return ResponseHelper::error('快捷语不存在');
+ }
+ $reply->save($data);
+ return ResponseHelper::success($reply->toArray(), '更新成功');
+ } catch (\Exception $e) {
+ return ResponseHelper::error('更新失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 假删除快捷语
+ * @return \think\response\Json
+ */
+ public function deleteReply()
+ {
+ $id = (int)$this->request->param('id', 0);
+ if ($id <= 0) {
+ return ResponseHelper::error('快捷语ID不合法');
+ }
+ try {
+ $reply = Reply::where(['id' => $id,'isDel' => 0])->find();
+ if (empty($reply)) {
+ return ResponseHelper::error('快捷语不存在');
+ }
+ $reply->save(['isDel' => 1, 'delTime' => time()]);
+ return ResponseHelper::success([], '删除成功');
+ } catch (\Exception $e) {
+ return ResponseHelper::error('删除失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 构建分组树形结构
+ * @param array $groups 所有分组数据
+ * @param string $keyword 搜索关键词
+ * @return array
+ */
+ private function buildGroupTree($groups, $keyword = '')
+ {
+ $tree = [];
+ $groupMap = [];
+
+ // 先构建分组映射
+ foreach ($groups as $group) {
+ $groupMap[$group->id] = $group->toArray();
+ }
+
+ // 构建树形结构
+ foreach ($groups as $group) {
+ $groupData = $this->buildGroupData($group, $keyword);
+
+ if ($group->parentId == null || $group->parentId == 0) {
+ // 顶级分组
+ $tree[] = $groupData;
+ } else {
+ // 子分组,需要找到父分组并添加到其children中
+ $this->addToParentGroup($tree, $group->parentId, $groupData);
+ }
+ }
+
+ return $tree;
+ }
+
+ /**
+ * 构建单个分组数据
+ * @param object $group 分组对象
+ * @param string $keyword 搜索关键词
+ * @return array
+ */
+ private function buildGroupData($group, $keyword = '')
+ {
+ // 构建快捷回复查询条件
+ $replyWhere[] =[
+ ['groupId' ,'=', $group->id],
+ ['isDel','=',0]
+ ];
+ if (!empty($keyword)) {
+ $replyWhere[] = ['title','like', '%' . $keyword . '%'];
+ }
+
+ // 获取该分组下的快捷回复
+ $replies = Reply::where($replyWhere)
+ ->order('sortIndex asc, id desc
+ ')
+ ->select();
+
+ return [
+ 'id' => $group->id,
+ 'groupName' => $group->groupName,
+ 'sortIndex' => $group->sortIndex,
+ 'parentId' => $group->parentId,
+ 'replyType' => $group->replyType,
+ 'replys' => $group->replys,
+ 'companyId' => $group->companyId,
+ 'userId' => $group->userId,
+ 'replies' => $replies->toArray(),
+ 'children' => [] // 子分组
+ ];
+ }
+
+ /**
+ * 将子分组添加到父分组中
+ * @param array $tree 树形结构
+ * @param int $parentId 父分组ID
+ * @param array $childGroup 子分组数据
+ */
+ private function addToParentGroup(&$tree, $parentId, $childGroup)
+ {
+ foreach ($tree as &$group) {
+ if ($group['id'] == $parentId) {
+ $group['children'][] = $childGroup;
+ return;
+ }
+
+ // 递归查找子分组
+ if (!empty($group['children'])) {
+ $this->addToParentGroup($group['children'], $parentId, $childGroup);
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/application/chukebao/controller/ToDoController.php b/application/chukebao/controller/ToDoController.php
new file mode 100644
index 0000000..6914141
--- /dev/null
+++ b/application/chukebao/controller/ToDoController.php
@@ -0,0 +1,143 @@
+request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+ $isRemind = $this->request->param('isRemind', '');
+ $isProcess = $this->request->param('isProcess', '');
+ $level = $this->request->param('level', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+
+ $where = [
+ ['companyId','=',$companyId],
+ ['userId' ,'=', $userId]
+ ];
+
+ if ($isRemind != '') {
+ $where[] = ['isRemind','=',$isRemind];
+ }
+ if ($level != '') {
+ $where[] = ['level','=',$level];
+ }
+ if ($isProcess != '') {
+ $where[] = ['isProcess','=',$isProcess];
+ }
+
+ if(!empty($keyword)){
+ $where[] = ['title|description','like','%'.$keyword.'%'];
+ }
+
+ $query = ToDo::where($where);
+ $total = $query->count();
+ $list = $query->where($where)->page($page,$limit)->order('id desc')->select();
+
+
+ foreach ($list as &$item) {
+ $nickname = Db::table('s2_wechat_friend')->where(['id' => $item['friendId']])->value('nickname');
+ $item['nickname'] = !empty($nickname) ? $nickname : '-';
+ $item['reminderTime'] = date('Y-m-d H:i:s',$item['reminderTime']);
+ }
+ unset($item);
+
+ return ResponseHelper::success(['list'=>$list,'total'=>$total]);
+ }
+
+
+ /**
+ * 添加
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function create(){
+ $level = $this->request->param('level', 0);
+ $title = $this->request->param('title', '');
+ $reminderTime = $this->request->param('reminderTime', '');
+ $description = $this->request->param('description', '');
+ $friendId = $this->request->param('friendId', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($title) || empty($reminderTime) || empty($description) || empty($friendId)){
+ return ResponseHelper::error('参数缺失');
+ }
+ $friend = Db::table('s2_wechat_friend')->where(['id' => $friendId])->find();
+ if (empty($friend)) {
+ return ResponseHelper::error('好友不存在');
+ }
+
+
+ Db::startTrans();
+ try {
+ $todo = new ToDo();
+ $todo->level = $level;
+ $todo->title = $title;
+ $todo->friendId = $friendId;
+ $todo->reminderTime = !empty($reminderTime) ? strtotime($reminderTime) : time();
+ $todo->description = $description;
+ $todo->userId = $userId;
+ $todo->companyId = $companyId;
+ $todo->updateTime = time();
+ $todo->createTime = time();
+ $todo->save();
+ Db::commit();
+ return ResponseHelper::success(' ','创建成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('创建失败:'.$e->getMessage());
+ }
+ }
+
+
+ /**
+ * 处理代办事项
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function process(){
+ $ids = $this->request->param('ids','');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($ids)){
+ return ResponseHelper::error('参数缺失');
+ }
+ $ids = explode(',',$ids);
+
+ if (!is_array($ids)){
+ return ResponseHelper::error('格式错误');
+ }
+
+ $todoIds = ToDo::where(['userId' => $userId,'companyId' => $companyId,'isProcess' => 0])->whereIn('id',$ids)->column('id');
+ if (empty($todoIds)){
+ return ResponseHelper::error('代办事项不存在');
+ }
+
+ Db::startTrans();
+ try {
+ ToDo::whereIn('id',$todoIds)->update(['isProcess' => 1,'isRemind' => 1,'updateTime' => time()]);
+ Db::commit();
+ return ResponseHelper::success(' ','已处理');
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('处理失败:'.$e->getMessage());
+ }
+
+
+
+
+
+ }
+
+}
\ No newline at end of file
diff --git a/application/chukebao/controller/TokensRecordController.php b/application/chukebao/controller/TokensRecordController.php
new file mode 100644
index 0000000..03b7a2e
--- /dev/null
+++ b/application/chukebao/controller/TokensRecordController.php
@@ -0,0 +1,192 @@
+request->param('page', 1);
+ $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');
+
+
+ $where = [
+ ['companyId','=',$companyId],
+ ['userId' ,'=', $userId]
+ ];
+
+ if ($type != '') {
+ $where[] = ['type','=',$type];
+ }
+ if ($form != '') {
+ $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();
+ $list = $query->where($where)->page($page,$limit)->order('id desc')->select();
+
+
+ foreach ($list as &$item) {
+ if (in_array($item['type'],[1])){
+ $nickname = Db::table('s2_wechat_friend')->where(['id' => $item['friendIdOrGroupId']])->value('nickname');
+ $item['nickname'] = !empty($nickname) ? $nickname : '-';
+ }
+ if (in_array($item['type'],[2,3])){
+ $nickname = Db::table('s2_wechat_chatroom')->where(['id' => $item['friendIdOrGroupId']])->value('nickname');
+ $item['nickname'] = !empty($nickname) ? $nickname : '-';
+ }
+ }
+ unset($item);
+
+ return ResponseHelper::success(['list'=>$list,'total'=>$total]);
+ }
+
+
+ public function consumeTokens($data = [])
+ {
+ if (empty($data)){
+ return ResponseHelper::error('数据缺失');
+ }
+
+ $tokens = isset($data['tokens']) ? intval($data['tokens']) : 0;
+ $type = isset($data['type']) ? intval($data['type']) : 0;
+ $form = isset($data['form']) ? intval($data['form']) : 0;
+ $wechatAccountId = isset($data['wechatAccountId']) ? intval($data['wechatAccountId']) : 0;
+ $friendIdOrGroupId = isset($data['friendIdOrGroupId']) ? intval($data['friendIdOrGroupId']) : 0;
+ $remarks = isset($data['remarks']) ? $data['remarks'] : '';
+ $companyId = isset($data['companyId']) ? intval($data['companyId']) : $this->getUserInfo('companyId');
+ $userId = isset($data['userId']) ? intval($data['userId']) : $this->getUserInfo('id');
+
+ // 验证必要参数
+ if ($tokens <= 0) {
+ return ResponseHelper::error('tokens数量必须大于0');
+ }
+
+ if (!in_array($type, [0, 1])) {
+ return ResponseHelper::error('类型参数错误,0为减少,1为增加');
+ }
+
+
+ // 重试机制,最多重试3次
+ $maxRetries = 3;
+ $retryCount = 0;
+ while ($retryCount < $maxRetries) {
+ try {
+ return $this->doConsumeTokens($userId, $companyId, $tokens, $type, $form, $wechatAccountId, $friendIdOrGroupId, $remarks);
+ } catch (\Exception $e) {
+ $retryCount++;
+ if ($retryCount >= $maxRetries) {
+ return ResponseHelper::error('操作失败,请稍后重试:' . $e->getMessage());
+ }
+ // 短暂延迟后重试
+ usleep(100000); // 100ms
+ }
+ }
+ }
+
+ /**
+ * 执行tokens消费的核心方法
+ */
+ private function doConsumeTokens($userId, $companyId, $tokens, $type, $form, $wechatAccountId, $friendIdOrGroupId, $remarks)
+ {
+ // 开启数据库事务
+ Db::startTrans();
+ try {
+ // 使用悲观锁获取用户当前tokens余额,确保并发安全
+ $userInfo = TokensCompany::where(['companyId'=> $companyId,'userId' => $userId])->lock(true)->find();
+ if (!$userInfo) {
+ throw new \Exception('用户不存在');
+ }
+
+ $currentTokens = intval($userInfo['tokens']);
+
+ // 计算新的余额
+ $newBalance = $type == 1 ? ($currentTokens + $tokens) : ($currentTokens - $tokens);
+
+ // 使用原子更新操作,基于当前值进行更新,防止并发覆盖
+ $updateResult = TokensCompany::where('companyId', $companyId)
+ ->where('companyId', $companyId)
+ ->update([
+ 'tokens' => $newBalance,
+ 'updateTime' => time()
+ ]);
+
+ if (!$updateResult) {
+ // 如果更新失败,说明tokens值已被其他事务修改,需要重新获取
+ throw new \Exception('tokens余额已被其他操作修改,请重试');
+ }
+
+ // 记录tokens变动
+ $recordData = [
+ 'companyId' => $companyId,
+ 'userId' => $userId,
+ 'wechatAccountId' => $wechatAccountId,
+ 'friendIdOrGroupId' => $friendIdOrGroupId,
+ 'form' => $form,
+ 'type' => $type,
+ 'tokens' => $tokens,
+ 'balanceTokens' => $newBalance,
+ 'remarks' => $remarks,
+ 'createTime' => time()
+ ];
+
+ $recordId = Db::name('tokens_record')->insertGetId($recordData);
+
+ if (!$recordId) {
+ throw new \Exception('记录tokens变动失败');
+ }
+
+ // 提交事务
+ Db::commit();
+
+ return ResponseHelper::success([
+ 'recordId' => $recordId,
+ 'oldBalance' => $currentTokens,
+ 'newBalance' => $newBalance,
+ 'changeAmount' => $type == 1 ? $tokens : -$tokens
+ ], 'tokens变动记录成功');
+
+ } catch (\Exception $e) {
+ // 回滚事务
+ Db::rollback();
+ throw $e; // 重新抛出异常,让重试机制处理
+ }
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/application/chukebao/controller/WechatChatroomController.php b/application/chukebao/controller/WechatChatroomController.php
new file mode 100644
index 0000000..936c837
--- /dev/null
+++ b/application/chukebao/controller/WechatChatroomController.php
@@ -0,0 +1,269 @@
+request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+ $groupIds = $this->request->param('groupId', '');
+ $ownerWechatId = $this->request->param('ownerWechatId', '');
+ $accountId = $this->getUserInfo('s2_accountId');
+ if (empty($accountId)){
+ return ResponseHelper::error('请先登录');
+ }
+ $query = Db::table('s2_wechat_chatroom')
+ ->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);
+ }
+
+ if (!empty($ownerWechatId)) {
+ $query->where('ownerWechatId', $ownerWechatId);
+ }
+
+ $query->order('id desc');
+ $total = $query->count();
+ $list = $query->page($page, $limit)->select();
+
+
+
+ // 提取所有聊天室ID,用于批量查询
+ $chatroomIds = array_column($list, 'id');
+
+
+ // 一次性查询所有聊天室的未读消息数量
+ $unreadCounts = [];
+ if (!empty($chatroomIds)) {
+ $unreadResults = Db::table('s2_wechat_message')
+ ->field('wechatChatroomId, COUNT(*) as count')
+ ->where('wechatChatroomId', 'in', $chatroomIds)
+ ->where('isRead', 0)
+ ->group('wechatChatroomId')
+ ->select();
+
+ foreach ($unreadResults as $result) {
+ $unreadCounts[$result['wechatChatroomId']] = $result['count'];
+ }
+ }
+ // 一次性查询所有聊天室的最新消息
+ $latestMessages = [];
+ if (!empty($chatroomIds)) {
+ // 使用子查询获取每个聊天室的最新消息ID
+ $subQuery = Db::table('s2_wechat_message')
+ ->field('MAX(id) as max_id, wechatChatroomId')
+ ->where('wechatChatroomId', 'in', $chatroomIds)
+ ->group('wechatChatroomId')
+ ->buildSql();
+
+ // 查询最新消息的详细信息
+ $messageResults = Db::table('s2_wechat_message')
+ ->alias('m')
+ ->join([$subQuery => 'sub'], 'm.id = sub.max_id')
+ ->field('m.*, sub.wechatChatroomId')
+ ->select();
+
+ foreach ($messageResults as $message) {
+ $latestMessages[$message['wechatChatroomId']] = $message;
+ }
+ }
+
+ // 处理每个聊天室的数据
+ foreach ($list as $k => &$v) {
+ $v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s', $v['createTime']) : '';
+ $v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s', $v['updateTime']) : '';
+
+ $config = [
+ 'unreadCount' => isset($unreadCounts[$v['id']]) ? $unreadCounts[$v['id']] : 0,
+ 'chat' => isset($latestMessages[$v['id']]),
+ 'msgTime' => isset($latestMessages[$v['id']]) ? $latestMessages[$v['id']]['wechatTime'] : 0
+ ];
+ $v['config'] = $config;
+ }
+ unset($v);
+
+ return ResponseHelper::success(['list'=>$list,'total'=>$total]);
+ }
+
+ public function getDetail(){
+ $id = input('id', 0);
+
+ if (!$id) {
+ return ResponseHelper::error('聊天室ID不能为空');
+ }
+
+ $accountId = $this->getUserInfo('s2_accountId');
+ if (empty($accountId)){
+ return ResponseHelper::error('请先登录');
+ }
+
+ $detail = Db::table('s2_wechat_chatroom')
+ ->where(['accountId' => $accountId, 'id' => $id, 'isDeleted' => 0])
+ ->find();
+
+ if (!$detail) {
+ return ResponseHelper::error('聊天室不存在或无权限访问');
+ }
+
+ // 处理时间格式
+ $detail['createTime'] = !empty($detail['createTime']) ? date('Y-m-d H:i:s', $detail['createTime']) : '';
+ $detail['updateTime'] = !empty($detail['updateTime']) ? date('Y-m-d H:i:s', $detail['updateTime']) : '';
+
+ // 查询未读消息数量
+ $unreadCount = Db::table('s2_wechat_message')
+ ->where('wechatChatroomId', $id)
+ ->where('isRead', 0)
+ ->count();
+
+ // 查询最新消息
+ $latestMessage = Db::table('s2_wechat_message')
+ ->where('wechatChatroomId', $id)
+ ->order('id desc')
+ ->find();
+
+ $config = [
+ 'unreadCount' => $unreadCount,
+ 'chat' => !empty($latestMessage),
+ 'msgTime' => isset($latestMessage['wechatTime']) ? $latestMessage['wechatTime'] : 0
+ ];
+ $detail['config'] = $config;
+
+ return ResponseHelper::success($detail);
+ }
+
+ public function getMembers()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $groupId = $this->request->param('groupId', '');
+ $keyword = $this->request->param('keyword', '');
+
+ $accountId = $this->getUserInfo('s2_accountId');
+ if (empty($accountId)) {
+ return ResponseHelper::error('请先登录');
+ }
+
+ // 验证群组ID必填
+ if (empty($groupId)) {
+ return ResponseHelper::error('群组ID不能为空');
+ }
+
+ // 验证群组是否属于当前账号
+ $chatroom = Db::table('s2_wechat_chatroom')
+ ->where(['id' => $groupId, 'isDeleted' => 0])
+ ->find();
+
+ if (!$chatroom) {
+ return ResponseHelper::error('群组不存在或无权限访问');
+ }
+
+ // 获取群组的chatroomId(微信群聊ID)
+ $chatroomId = $chatroom['chatroomId'] ?? $chatroom['id'];
+
+ // 如果chatroomId为空,使用id作为chatroomId
+ if (empty($chatroomId)) {
+ $chatroomId = $chatroom['id'];
+ }
+
+ // 构建查询
+ $query = Db::table('s2_wechat_chatroom_member')
+ ->where('chatroomId', $chatroomId);
+
+ // 关键字搜索:昵称、备注、别名
+ if ($keyword !== '' && $keyword !== null) {
+ $query->where(function ($q) use ($keyword) {
+ $like = '%' . $keyword . '%';
+ $q->whereLike('nickname', $like)
+ ->whereOr('conRemark', 'like', $like)
+ ->whereOr('alias', 'like', $like);
+ });
+ }
+
+ $query->order('id desc');
+ $total = $query->count();
+ $list = $query->page($page, $limit)->select();
+
+ // 处理时间格式
+ foreach ($list as $k => &$v) {
+ $v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s', $v['createTime']) : '';
+ $v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s', $v['updateTime']) : '';
+ }
+ unset($v);
+
+ return ResponseHelper::success(['list' => $list, 'total' => $total]);
+ }
+
+ public function aiAnnouncement()
+ {
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+ $wechatAccountId = $this->request->param('wechatAccountId', '');
+ $groupId = $this->request->param('groupId', '');
+ $content = $this->request->param('content', '');
+
+ if (empty($groupId) || empty($content)|| empty($wechatAccountId)){
+ return ResponseHelper::error('参数缺失');
+ }
+
+ $tokens = TokensCompany::where(['companyId' => $companyId])->value('tokens');
+ if (empty($tokens) || $tokens <= 0){
+ return ResponseHelper::error('用户Tokens余额不足');
+ }
+
+ $params = [
+ 'model' => 'doubao-1-5-pro-32k-250115',
+ 'messages' => [
+ ['role' => 'system', 'content' => '你现在是存客宝的AI助理,你精通中国大陆的法律'],
+ ['role' => 'user', 'content' => $content],
+ ],
+ ];
+
+ //AI处理
+ $ai = new DouBaoAI();
+ $res = $ai->text($params);
+ $res = json_decode($res,true);
+
+
+ if ($res['code'] == 200) {
+ //扣除Tokens
+ $tokensRecord = new tokensRecord();
+ $nickname = Db::table('s2_wechat_chatroom')->where(['id' => $groupId])->value('nickname');
+ $remarks = !empty($nickname) ? '生成【'.$nickname.'】群公告' : '生成群公告';
+ $data = [
+ 'tokens' => $res['data']['token'],
+ 'type' => 0,
+ 'form' => 14,
+ 'wechatAccountId' => $wechatAccountId,
+ 'friendIdOrGroupId' => $groupId,
+ 'remarks' => $remarks,
+ ];
+ $tokensRecord->consumeTokens($data);
+ return ResponseHelper::success($res['data']['content']);
+ }else{
+ return ResponseHelper::error($res['msg']);
+ }
+
+
+ }
+
+}
\ No newline at end of file
diff --git a/application/chukebao/controller/WechatFriendController.php b/application/chukebao/controller/WechatFriendController.php
new file mode 100644
index 0000000..aaf541f
--- /dev/null
+++ b/application/chukebao/controller/WechatFriendController.php
@@ -0,0 +1,302 @@
+request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+ $groupIds = $this->request->param('groupId', '');
+ $ownerWechatId = $this->request->param('ownerWechatId', '');
+ $accountId = $this->getUserInfo('s2_accountId');
+ if (empty($accountId)) {
+ return ResponseHelper::error('请先登录');
+ }
+ $query = Db::table('s2_wechat_friend')
+ ->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);
+ }
+
+ if (!empty($ownerWechatId)) {
+ $query->where('ownerWechatId', $ownerWechatId);
+ }
+
+ $query->order('id desc');
+ $total = $query->count();
+ $list = $query->page($page, $limit)->select();
+
+ // 提取所有好友ID
+ $friendIds = array_column($list, 'id');
+
+ $aiTypeData = [];
+ if (!empty($friendIds)) {
+ $aiTypeData = FriendSettings::where('friendId', 'in', $friendIds)->column('friendId,type');
+ }
+
+
+ // 处理每个好友的数据
+ foreach ($list as $k => &$v) {
+ $v['labels'] = json_decode($v['labels'], true);
+ $v['siteLabels'] = json_decode($v['siteLabels'], true);
+ $v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s', $v['createTime']) : '';
+ $v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s', $v['updateTime']) : '';
+ $v['passTime'] = !empty($v['passTime']) ? date('Y-m-d H:i:s', $v['passTime']) : '';
+ $v['aiType'] = isset($aiTypeData[$v['id']]) ? $aiTypeData[$v['id']] : 0;
+ }
+ unset($v);
+
+ return ResponseHelper::success(['list' => $list, 'total' => $total]);
+ }
+
+ /**
+ * 获取单个好友详情
+ * @return \think\response\Json
+ */
+ public function getDetail()
+ {
+ $friendId = $this->request->param('id');
+ $accountId = $this->getUserInfo('s2_accountId');
+
+ if (empty($accountId)) {
+ return ResponseHelper::error('请先登录');
+ }
+
+ if (empty($friendId)) {
+ return ResponseHelper::error('好友ID不能为空');
+ }
+
+ // 查询好友详情
+ $friend = Db::table('s2_wechat_friend')
+ ->where(['id' => $friendId, 'isDeleted' => 0])
+ ->find();
+
+ if (empty($friend)) {
+ return ResponseHelper::error('好友不存在');
+ }
+
+ // 处理好友数据
+ $friend['labels'] = json_decode($friend['labels'], true);
+ $friend['siteLabels'] = json_decode($friend['siteLabels'], true);
+ $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]);
+ }
+
+ /**
+ * 更新好友资料(公司、姓名、手机号等字段可单独更新)
+ * @return \think\response\Json
+ */
+ public function updateFriendInfo()
+ {
+ $friendId = $this->request->param('id');
+ $accountId = $this->getUserInfo('s2_accountId');
+
+ if (empty($accountId)) {
+ return ResponseHelper::error('请先登录');
+ }
+
+ if (empty($friendId)) {
+ return ResponseHelper::error('好友ID不能为空');
+ }
+
+ $friend = Db::table('s2_wechat_friend')
+ ->where(['id' => $friendId, 'accountId' => $accountId, 'isDeleted' => 0])
+ ->find();
+
+ if (empty($friend)) {
+ return ResponseHelper::error('好友不存在或无权限操作');
+ }
+
+ $requestData = $this->request->param();
+ $updatableColumns = [
+ 'phone',
+ 'conRemark',
+ ];
+ $columnUpdates = [];
+
+ foreach ($updatableColumns as $field) {
+ if (array_key_exists($field, $requestData)) {
+ $columnUpdates[$field] = $requestData[$field];
+ }
+ }
+
+ $extendFieldsData = [];
+ if (!empty($friend['extendFields'])) {
+ $decodedExtend = json_decode($friend['extendFields'], true);
+ $extendFieldsData = is_array($decodedExtend) ? $decodedExtend : [];
+ }
+
+ $extendFieldKeys = [
+ 'company',
+ 'name',
+ 'position',
+ 'email',
+ 'address',
+ 'wechat',
+ 'qq',
+ 'remark'
+ ];
+ $extendFieldsUpdated = false;
+
+ foreach ($extendFieldKeys as $key) {
+ if (array_key_exists($key, $requestData)) {
+ $extendFieldsData[$key] = $requestData[$key];
+ $extendFieldsUpdated = true;
+ }
+ }
+
+ if ($extendFieldsUpdated) {
+ $columnUpdates['extendFields'] = json_encode($extendFieldsData, JSON_UNESCAPED_UNICODE);
+ }
+
+ if (empty($columnUpdates)) {
+ return ResponseHelper::error('没有可更新的字段');
+ }
+
+ $columnUpdates['updateTime'] = time();
+
+ try {
+ Db::table('s2_wechat_friend')->where('id', $friendId)->update($columnUpdates);
+ } catch (\Exception $e) {
+ return ResponseHelper::error('更新失败:' . $e->getMessage());
+ }
+
+ 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] : '未知状态';
+ }
+}
\ No newline at end of file
diff --git a/application/chukebao/controller/WechatGroupController.php b/application/chukebao/controller/WechatGroupController.php
new file mode 100644
index 0000000..8ff0b75
--- /dev/null
+++ b/application/chukebao/controller/WechatGroupController.php
@@ -0,0 +1,273 @@
+getUserInfo('companyId');
+
+ $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']) : '';
+ }
+ unset($v);
+
+ 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. 重置该分组下所有好友的分组ID(s2_wechat_friend.groupIds -> 0)
+ Db::table('s2_wechat_friend')
+ ->where('groupIds', $id)
+ ->update(['groupIds' => 0]);
+
+ // 3. 重置该分组下所有微信群的分组ID(s2_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());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/chukebao/model/AiKnowledgeBase.php b/application/chukebao/model/AiKnowledgeBase.php
new file mode 100644
index 0000000..2e08c8f
--- /dev/null
+++ b/application/chukebao/model/AiKnowledgeBase.php
@@ -0,0 +1,52 @@
+belongsTo(AiKnowledgeBaseType::class, 'typeId', 'id');
+ }
+
+ /**
+ * 获取有效的知识库列表(未删除)
+ */
+ public static function getValidList($companyId, $typeId = null)
+ {
+ $where = [
+ ['isDel', '=', 0],
+ ['companyId', '=', $companyId]
+ ];
+
+ if ($typeId !== null) {
+ $where[] = ['typeId', '=', $typeId];
+ }
+
+ return self::where($where)
+ ->with(['type'])
+ ->order('createTime', 'desc')
+ ->select();
+ }
+}
+
diff --git a/application/chukebao/model/AiKnowledgeBaseType.php b/application/chukebao/model/AiKnowledgeBaseType.php
new file mode 100644
index 0000000..80fa9f2
--- /dev/null
+++ b/application/chukebao/model/AiKnowledgeBaseType.php
@@ -0,0 +1,57 @@
+order('createTime', 'desc')
+ ->select();
+ }
+
+ /**
+ * 检查是否为系统类型
+ */
+ public function isSystemType()
+ {
+ return $this->type == self::TYPE_SYSTEM;
+ }
+}
+
diff --git a/application/chukebao/model/AiPush.php b/application/chukebao/model/AiPush.php
new file mode 100644
index 0000000..567c132
--- /dev/null
+++ b/application/chukebao/model/AiPush.php
@@ -0,0 +1,17 @@
+
+// +----------------------------------------------------------------------
+
+return [
+ 'device:list' => 'app\command\DeviceListCommand', // 设备列表 √
+ 'wechatFriends:list' => 'app\command\WechatFriendCommand', // 微信好友列表 √
+ 'wechatChatroom:list' => 'app\command\WechatChatroomCommand', // 微信群列表 √
+ 'friendTask:list' => 'app\command\FriendTaskCommand', // 添加好友任务列表 √
+ 'wechatList:list' => 'app\command\WechatListCommand', // 微信客服列表 √
+ 'account:list' => 'app\command\AccountListCommand', // 公司账号列表 √
+ 'message:friendsList' => 'app\command\MessageFriendsListCommand', // 微信好友消息列表 √
+ 'message:chatroomList' => 'app\command\MessageChatroomListCommand', // 微信群聊消息列表 √
+ 'department:list' => 'app\command\DepartmentListCommand', // 部门列表 √
+ 'content:sync' => 'app\command\SyncContentCommand', // 同步内容库 XXXXXXXX
+ 'groupFriends:list' => 'app\command\GroupFriendsCommand', // 微信群好友列表
+ // 'allotFriends:run' => 'app\command\AllotFriendCommand', // 自动分配微信好友
+ // 'allotChatroom:run' => 'app\command\AllotChatroomCommand', // 自动分配微信群聊
+ 'allotrule:list' => 'app\command\AllotRuleListCommand', // 分配规则列表 √
+ 'allotrule:autocreate' => 'app\command\AutoCreateAllotRulesCommand', // 自动创建分配规则 √
+ 'content:collect' => 'app\command\ContentCollectCommand', // 内容采集任务 √
+ 'moments:collect' => 'app\command\WechatMomentsCommand', // 朋友圈采集任务
+ 'own:moments:collect' => 'app\command\OwnMomentsCollectCommand', // 采集在线微信账号自己的朋友圈
+ 'switch:friends' => 'app\command\SwitchFriendsCommand',
+ 'call-recording:list' => 'app\command\CallRecordingListCommand', // 通话记录列表 √
+ 'sync:wechatData' => 'app\command\SyncWechatDataToCkbTask', // 同步微信数据到存客宝
+ 'sync:allFriends' => 'app\command\SyncAllFriendsCommand', // 同步所有在线好友
+
+ 'workbench:autoLike' => 'app\command\WorkbenchAutoLikeCommand', // 工作台自动点赞任务
+ 'workbench:moments' => 'app\command\WorkbenchMomentsCommand', // 工作台朋友圈同步任务
+ 'workbench:trafficDistribute' => 'app\command\WorkbenchTrafficDistributeCommand', // 工作台流量分发任务
+ 'workbench:groupPush' => 'app\command\WorkbenchGroupPushCommand', // 工作台群推送任务
+ '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', // 统一任务调度器,支持多进程并发执行
+];
diff --git a/application/command/AccountListCommand.php b/application/command/AccountListCommand.php
new file mode 100644
index 0000000..7a7fabd
--- /dev/null
+++ b/application/command/AccountListCommand.php
@@ -0,0 +1,57 @@
+setName('account:list')
+ ->setDescription('获取公司账号列表,并根据分页自动处理下一页');
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ $output->writeln('开始处理公司账号列表任务...');
+
+ try {
+ // 初始页码
+ $pageIndex = 0;
+ $pageSize = 100; // 每页获取100条记录
+
+ // 将第一页任务添加到队列
+ $this->addToQueue($pageIndex, $pageSize);
+
+ $output->writeln('公司账号列表任务已添加到队列');
+ } catch (\Exception $e) {
+ Log::error('公司账号列表任务添加失败:' . $e->getMessage());
+ $output->writeln('公司账号列表任务添加失败:' . $e->getMessage());
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * 添加任务到队列
+ * @param int $pageIndex 页码
+ * @param int $pageSize 每页大小
+ */
+ protected function addToQueue($pageIndex, $pageSize)
+ {
+ $data = [
+ 'pageIndex' => $pageIndex,
+ 'pageSize' => $pageSize
+ ];
+
+ // 添加到队列,设置任务名为 account_list
+ Queue::push(AccountListJob::class, $data, 'account_list');
+ }
+}
\ No newline at end of file
diff --git a/application/command/AllotChatroomCommand.php b/application/command/AllotChatroomCommand.php
new file mode 100644
index 0000000..e0a7684
--- /dev/null
+++ b/application/command/AllotChatroomCommand.php
@@ -0,0 +1,102 @@
+setName('allotChatroom:run')
+ ->setDescription('自动分配微信群聊')
+ ->addOption('toAccountId', null, Option::VALUE_REQUIRED, '目标账号ID')
+ ->addOption('wechatAccountKeyword', null, Option::VALUE_REQUIRED, '微信账号关键字')
+ ->addOption('isDeleted', null, Option::VALUE_OPTIONAL, '是否已删除状态: 0=未删除(false), 1=已删除(true)', 0)
+ ->addOption('jobId', null, Option::VALUE_OPTIONAL, '任务ID,用于区分不同实例', date('YmdHis') . rand(1000, 9999));
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ $output->writeln('开始处理微信群聊自动分配任务...');
+
+ try {
+ // 获取命令参数
+ $toAccountId = $input->getOption('toAccountId');
+ $wechatAccountKeyword = $input->getOption('wechatAccountKeyword');
+ $isDeleted = $input->getOption('isDeleted');
+ $jobId = $input->getOption('jobId');
+
+ // 验证必填参数
+ if (empty($toAccountId)) {
+ $output->writeln('错误: 目标账号ID不能为空');
+ return false;
+ }
+
+ if (empty($wechatAccountKeyword)) {
+ $output->writeln('错误: 微信账号关键字不能为空');
+ return false;
+ }
+
+ $output->writeln('目标账号ID: ' . $toAccountId);
+ $output->writeln('微信账号关键字: ' . $wechatAccountKeyword);
+ $output->writeln('删除状态: ' . ($isDeleted ? '已删除' : '未删除'));
+ $output->writeln('任务ID: ' . $jobId);
+
+ // 检查队列是否已经在运行
+ $queueLockKey = "queue_lock:{$this->queueName}:{$wechatAccountKeyword}";
+ if (Cache::get($queueLockKey)) {
+ $output->writeln("队列 {$this->queueName} 已经在运行中,wechatAccountKeyword:{$wechatAccountKeyword},跳过执行");
+ Log::warning("队列 {$this->queueName} 已经在运行中,wechatAccountKeyword:{$wechatAccountKeyword},跳过执行");
+ return false;
+ }
+
+ // 设置队列运行锁,有效期1小时
+ Cache::set($queueLockKey, $jobId, 3600);
+ $output->writeln("已设置队列运行锁,键名:{$queueLockKey},值:{$jobId},有效期:1小时");
+
+ // 将任务添加到队列
+ $this->addToQueue($toAccountId, $wechatAccountKeyword, $isDeleted, $jobId, $queueLockKey);
+
+ $output->writeln('微信群聊自动分配任务已添加到队列');
+ } catch (\Exception $e) {
+ Log::error('微信群聊自动分配任务添加失败:' . $e->getMessage());
+ $output->writeln('微信群聊自动分配任务添加失败:' . $e->getMessage());
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * 添加任务到队列
+ * @param string $toAccountId 目标账号ID
+ * @param string $wechatAccountKeyword 微信账号关键字
+ * @param bool $isDeleted 是否已删除状态
+ * @param string $jobId 任务ID
+ * @param string $queueLockKey 队列锁键名
+ */
+ public function addToQueue($toAccountId, $wechatAccountKeyword, $isDeleted = false, $jobId = '', $queueLockKey = '')
+ {
+ $data = [
+ 'toAccountId' => $toAccountId,
+ 'wechatAccountKeyword' => $wechatAccountKeyword,
+ 'isDeleted' => $isDeleted,
+ 'jobId' => $jobId,
+ 'queueLockKey' => $queueLockKey
+ ];
+
+ // 添加到队列
+ Queue::push(AllotChatroomJob::class, $data, $this->queueName);
+ }
+}
\ No newline at end of file
diff --git a/application/command/AllotFriendCommand.php b/application/command/AllotFriendCommand.php
new file mode 100644
index 0000000..d8d34b8
--- /dev/null
+++ b/application/command/AllotFriendCommand.php
@@ -0,0 +1,102 @@
+setName('allotFriends:run')
+ ->setDescription('自动分配微信好友')
+ ->addOption('toAccountId', null, Option::VALUE_REQUIRED, '目标账号ID')
+ ->addOption('wechatAccountKeyword', null, Option::VALUE_REQUIRED, '微信账号关键字')
+ ->addOption('isDeleted', null, Option::VALUE_OPTIONAL, '是否已删除状态: 0=未删除(false), 1=已删除(true)', 0)
+ ->addOption('jobId', null, Option::VALUE_OPTIONAL, '任务ID,用于区分不同实例', date('YmdHis') . rand(1000, 9999));
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ $output->writeln('开始处理微信好友自动分配任务...');
+
+ try {
+ // 获取命令参数
+ $toAccountId = $input->getOption('toAccountId');
+ $wechatAccountKeyword = $input->getOption('wechatAccountKeyword');
+ $isDeleted = $input->getOption('isDeleted');
+ $jobId = $input->getOption('jobId');
+
+ // 验证必填参数
+ if (empty($toAccountId)) {
+ $output->writeln('错误: 目标账号ID不能为空');
+ return false;
+ }
+
+ if (empty($wechatAccountKeyword)) {
+ $output->writeln('错误: 微信账号关键字不能为空');
+ return false;
+ }
+
+ $output->writeln('目标账号ID: ' . $toAccountId);
+ $output->writeln('微信账号关键字: ' . $wechatAccountKeyword);
+ $output->writeln('删除状态: ' . ($isDeleted ? '已删除' : '未删除'));
+ $output->writeln('任务ID: ' . $jobId);
+
+ // 检查队列是否已经在运行
+ $queueLockKey = "queue_lock:{$this->queueName}:{$wechatAccountKeyword}";
+ if (Cache::get($queueLockKey)) {
+ $output->writeln("队列 {$this->queueName} 已经在运行中,wechatAccountKeyword:{$wechatAccountKeyword},跳过执行");
+ Log::warning("队列 {$this->queueName} 已经在运行中,wechatAccountKeyword:{$wechatAccountKeyword},跳过执行");
+ return false;
+ }
+
+ // 设置队列运行锁,有效期1小时
+ Cache::set($queueLockKey, $jobId, 3600);
+ $output->writeln("已设置队列运行锁,键名:{$queueLockKey},值:{$jobId},有效期:1小时");
+
+ // 将任务添加到队列
+ $this->addToQueue($toAccountId, $wechatAccountKeyword, $isDeleted, $jobId, $queueLockKey);
+
+ $output->writeln('微信好友自动分配任务已添加到队列');
+ } catch (\Exception $e) {
+ Log::error('微信好友自动分配任务添加失败:' . $e->getMessage());
+ $output->writeln('微信好友自动分配任务添加失败:' . $e->getMessage());
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * 添加任务到队列
+ * @param string $toAccountId 目标账号ID
+ * @param string $wechatAccountKeyword 微信账号关键字
+ * @param bool $isDeleted 是否已删除状态
+ * @param string $jobId 任务ID
+ * @param string $queueLockKey 队列锁键名
+ */
+ public function addToQueue($toAccountId, $wechatAccountKeyword, $isDeleted = false, $jobId = '', $queueLockKey = '')
+ {
+ $data = [
+ 'toAccountId' => $toAccountId,
+ 'wechatAccountKeyword' => $wechatAccountKeyword,
+ 'isDeleted' => $isDeleted,
+ 'jobId' => $jobId,
+ 'queueLockKey' => $queueLockKey
+ ];
+
+ // 添加到队列
+ Queue::push(AllotFriendJob::class, $data, $this->queueName);
+ }
+}
\ No newline at end of file
diff --git a/application/command/AllotRuleListCommand.php b/application/command/AllotRuleListCommand.php
new file mode 100644
index 0000000..4ef93a5
--- /dev/null
+++ b/application/command/AllotRuleListCommand.php
@@ -0,0 +1,50 @@
+setName('allotrule:list')
+ ->setDescription('获取分配规则列表,自动同步到数据库');
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ $output->writeln('开始处理分配规则列表任务...');
+
+ try {
+ // 将任务添加到队列
+ $this->addToQueue();
+
+ $output->writeln('分配规则列表任务已添加到队列');
+ } catch (\Exception $e) {
+ Log::error('分配规则列表任务添加失败:' . $e->getMessage());
+ $output->writeln('分配规则列表任务添加失败:' . $e->getMessage());
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * 添加任务到队列
+ */
+ protected function addToQueue()
+ {
+ $data = [
+ 'time' => time()
+ ];
+
+ // 添加到队列,设置任务名为 allotrule_list
+ Queue::push(AllotRuleListJob::class, $data, 'allotrule_list');
+ }
+}
\ No newline at end of file
diff --git a/application/command/AutoCreateAllotRulesCommand.php b/application/command/AutoCreateAllotRulesCommand.php
new file mode 100644
index 0000000..3f54726
--- /dev/null
+++ b/application/command/AutoCreateAllotRulesCommand.php
@@ -0,0 +1,50 @@
+setName('allotrule:autocreate')
+ ->setDescription('自动创建微信分配规则');
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ $output->writeln('开始处理自动创建分配规则任务...');
+
+ try {
+ // 将任务添加到队列
+ $this->addToQueue();
+
+ $output->writeln('自动创建分配规则任务已添加到队列');
+ } catch (\Exception $e) {
+ Log::error('自动创建分配规则任务添加失败:' . $e->getMessage());
+ $output->writeln('自动创建分配规则任务添加失败:' . $e->getMessage());
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * 添加任务到队列
+ */
+ protected function addToQueue()
+ {
+ $data = [
+ 'time' => time()
+ ];
+
+ // 添加到队列,设置任务名为 autocreate_allotrule
+ Queue::push(AutoCreateAllotRulesJob::class, $data, 'autocreate_allotrule');
+ }
+}
\ No newline at end of file
diff --git a/application/command/CalculateWechatAccountScoreCommand.php b/application/command/CalculateWechatAccountScoreCommand.php
new file mode 100644
index 0000000..2b1c996
--- /dev/null
+++ b/application/command/CalculateWechatAccountScoreCommand.php
@@ -0,0 +1,554 @@
+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}");
+
+ // 记录命令开始执行的日志(仅在非交互模式下记录)
+ if (!$output->isVerbose()) {
+ 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("
';
+ }
+ call_user_func($func, $data);
+ if ($exit)
+ exit();
+ }
+}
+if (!function_exists('dump')) {
+ /**
+ * 调试打印变量但不终止程序
+ * @return void
+ */
+ function dump()
+ {
+ call_user_func_array(['app\\common\\helper\\Debug', 'dump'], func_get_args());
+ }
+}
+
+if (!function_exists('artificialAllotWechatFriend')) {
+ function artificialAllotWechatFriend($friend = [])
+ {
+ if (empty($friend)) {
+ return false;
+ }
+
+ //记录切换好友
+ $cacheKey = 'allotWechatFriend';
+ $cacheFriend = $friend;
+ $cacheFriend['time'] = time() + 120;
+ $maxRetry = 5;
+ $retry = 0;
+ do {
+ $cacheFriendData = Cache::get($cacheKey, []);
+ // 去重:移除同friendId的旧数据
+ $cacheFriendData = array_filter($cacheFriendData, function($item) use ($cacheFriend) {
+ return $item['friendId'] !== $cacheFriend['friendId'];
+ });
+ $cacheFriendData[] = $cacheFriend;
+ $success = Cache::set($cacheKey, $cacheFriendData);
+ $retry++;
+ } while (!$success && $retry < $maxRetry);
+ }
+}
\ No newline at end of file
diff --git a/application/common/Fork.php b/application/common/Fork.php
new file mode 100644
index 0000000..8154ad8
--- /dev/null
+++ b/application/common/Fork.php
@@ -0,0 +1,122 @@
+ $taskFunc) {
+ $pid = pcntl_fork();
+ if ($pid == -1) {
+ exit('Could not fork.');
+ } elseif ($pid == 0) {
+ call_user_func_array($taskFunc, [$i]);
+ exit(0);
+ }
+ }
+ foreach ($taskFuncs as $taskFunc) {
+ pcntl_wait($status);
+ }
+ }
+
+ /**
+ * 并发执行多条任务
+ *
+ * @param array $taskFuncs
+ */
+ static public function invoke(array $taskFuncs) {
+ $taskRuns = [];
+ $signal = FALSE;
+ if (function_exists('pcntl_async_signals')) {
+ pcntl_async_signals(TRUE);
+ }
+ while (TRUE) {
+ foreach ($taskFuncs as $i => $taskFunc) {
+ if (!in_array($i, $taskRuns)) {
+ $pid = pcntl_fork();
+ if ($pid == -1) {
+ exit('Could not fork.');
+ } elseif ($pid > 0) {
+ $taskRuns[$pid] = $i;
+ if (!$signal) {
+ $signal = TRUE;
+ static::signalHandler();
+ }
+ } else {
+ call_user_func_array($taskFunc, [$i]);
+ exit(0);
+ }
+ }
+ }
+ if ($pid = pcntl_wait($status, WNOHANG)) {
+ unset($taskRuns[$pid]);
+ } else {
+ sleep(1);
+ }
+ }
+ }
+
+ /**
+ * 按指定数量并发执行相同任务
+ *
+ * @param $count
+ * @param $taskFunc
+ * @param int $destroy
+ */
+ static public function execute($count, $taskFunc, $destroy = 100) {
+ $runSum = 0;
+ $runNum = 0;
+ $signal = FALSE;
+ if (function_exists('pcntl_async_signals')) {
+ pcntl_async_signals(TRUE);
+ }
+ while (TRUE) {
+ if ($runNum >= $count) {
+ if (pcntl_wait($status, WNOHANG) > 0) {
+ $runNum --;
+ } else {
+ sleep(1);
+ continue;
+ }
+ }
+
+ $pid = pcntl_fork();
+ if ($pid == -1) {
+ exit('Could not fork.');
+ } elseif ($pid > 0) {
+ $runSum ++;
+ $runNum ++;
+ if (!$signal) {
+ $signal = TRUE;
+ static::signalHandler();
+ }
+ } else {
+ for ($i = 0; $i < $destroy; $i ++) {
+ call_user_func_array($taskFunc, [$runSum % $count]);
+ }
+
+ exit;
+ }
+ }
+ }
+
+ /**
+ * 设置信息处理器
+ *
+ */
+ static public function signalHandler() {
+ $handler = function () {
+ posix_kill(0, SIGKILL);
+ exit(0);
+ };
+ pcntl_signal(SIGINT, $handler);
+ pcntl_signal(SIGTERM, $handler);
+ pcntl_signal(SIGUSR1, $handler);
+ pcntl_signal(SIGUSR2, $handler);
+ }
+}
\ No newline at end of file
diff --git a/application/common/Logger.php b/application/common/Logger.php
new file mode 100644
index 0000000..57b9877
--- /dev/null
+++ b/application/common/Logger.php
@@ -0,0 +1,90 @@
+name = $name;
+ }
+
+ /**
+ * 写入信息
+ *
+ * @param $message
+ * @param array $params
+ */
+ public function info($message, array $params = []) {
+ $this->write(static::INFO, $message, $params);
+ }
+
+ /**
+ * 写入警告
+ *
+ * @param $message
+ * @param array $params
+ */
+ public function warn($message, array $params = []) {
+ $this->write(static::WARN, $message, $params);
+ }
+
+ /**
+ * 写入错误
+ *
+ * @param $message
+ * @param array $params
+ */
+ public function error($message, array $params = []) {
+ $this->write(static::ERROR, $message, $params);
+ }
+
+ /**
+ * 写入日志
+ *
+ * @param $type
+ * @param $message
+ * @param array $params
+ */
+ public function write($type, $message, array $params = []) {
+ foreach ($params as $key => $value) {
+ $message = str_replace('{' . $key . '}', $value, $message);
+ }
+
+ $data = '[' . date('Y-m-d H:i:s') . '][' . $type . '] ' . $message . PHP_EOL;
+ $file = ROOT_PATH . DS . 'runtime' . DS . $this->name . '_' . date('Ymd') . '.txt';
+
+ file_put_contents($file, $data, FILE_APPEND);
+
+ if ($this->print) {
+ echo '[' . date('Y-m-d H:i:s') . '][' . $this->name . '][' . $type . '] ' . $message . PHP_EOL;;
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/common/Server.rar b/application/common/Server.rar
new file mode 100644
index 0000000..d21f54d
Binary files /dev/null and b/application/common/Server.rar differ
diff --git a/application/common/TaskServer.php b/application/common/TaskServer.php
new file mode 100644
index 0000000..50d2d63
--- /dev/null
+++ b/application/common/TaskServer.php
@@ -0,0 +1,93 @@
+ self::PROCESS_COUNT,
+ 'name' => 'ckb_task_server'
+ ];
+
+ /**
+ * 当客户端的连接上发生错误时触发
+ * @param $connection
+ * @param $code
+ * @param $msg
+ */
+ public function onError($connection, $code, $msg)
+ {
+ Log::record("error $code $msg");
+ }
+
+ public function onMessage($connection, $data)
+ {
+ }
+
+ public function onClose($connection)
+ {
+ }
+
+ public function onConnect($connection)
+ {
+ }
+
+ public function onWorkerStart($worker)
+ {
+
+ $current_worker_id = $worker->id;
+
+ $process_count_for_status_0 = self::PROCESS_COUNT - 1;
+
+ $adapter = new ChuKeBaoAdapter();
+
+
+ Log::info('Workerman进程:' . $current_worker_id);
+
+
+ // 在一个进程里处理获客任务新是数据
+ if ($current_worker_id == 4) {
+ Timer::add(60, function () use ($adapter) {
+ $adapter->handleCustomerTaskNewUser();
+ });
+ }
+
+
+ // 在一个进程里处理获客任务添加后的相关逻辑
+ if ($current_worker_id == 3) {
+ Timer::add(60, function () use ($adapter) {
+ $adapter->handleCustomerTaskWithStatusIsCreated();
+ });
+ }
+
+ // 进程处理获客新任务
+ if ($current_worker_id == 2) {
+ Timer::add(1, function () use ($current_worker_id, $process_count_for_status_0, $adapter) {
+ $adapter->handleCustomerTaskWithStatusIsNew($current_worker_id, $process_count_for_status_0);
+ });
+ }
+
+ // 在一个进程里处理自动问候任务
+ if ($current_worker_id == 1) {
+ // 每60秒检查一次自动问候规则
+ Timer::add(60, function () use ($adapter) {
+ //$adapter->handleAutoGreetings();
+ });
+ }
+
+ // 更多其他后台任务
+ // ......
+
+ }
+}
diff --git a/application/common/Utils.php b/application/common/Utils.php
new file mode 100644
index 0000000..c222f05
--- /dev/null
+++ b/application/common/Utils.php
@@ -0,0 +1,36 @@
+ 0 OR $number2 > 0) {
+ $number1 = $total * ($number1 / ($number1 + $number2));
+ $number2 = $total - $number1;
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/common/Video.php b/application/common/Video.php
new file mode 100644
index 0000000..561bad6
--- /dev/null
+++ b/application/common/Video.php
@@ -0,0 +1,101 @@
+ $image) {
+ if (!file_exists($image)) {
+ throw new \Exception('图片不存在:' . $image);
+ }
+ $tempImage = $tempImagesDir . DS . sprintf('%04d.jpg', $index);
+ copy($image, $tempImage);
+ $tempImages[] = $tempImage;
+ }
+
+ // 构建 FFmpeg 命令
+ $filterComplex = [];
+
+ // 处理所有输入流的格式和缩放
+ foreach ($tempImages as $i => $image) {
+ $filterComplex[] = sprintf('[%d:v]format=yuv420p,scale=%d:%d:force_original_aspect_ratio=decrease,pad=%d:%d:(ow-iw)/2:(oh-ih)/2[v%d]',
+ $i, $width, $height, $width, $height, $i);
+ }
+
+ // 构建转场效果链
+ $lastOutput = 'v0';
+ for ($i = 1; $i < count($tempImages); $i++) {
+ $filterComplex[] = sprintf('[%s][v%d]xfade=transition=fade:duration=1:offset=%d[fade%d]',
+ $lastOutput, $i, ($i * 3) - 1, $i);
+ $lastOutput = sprintf('fade%d', $i);
+ }
+
+ $inputFiles = '';
+ foreach ($tempImages as $image) {
+ $inputFiles .= sprintf('-loop 1 -t %d -i "%s" ', count($tempImages) * 3, $image);
+ }
+
+ $duration = count($tempImages) * 3 - (count($tempImages) - 1); // 总时长计算
+
+ $cmd = sprintf(
+ 'ffmpeg5 %s -filter_complex "%s" -map "[%s]" -t %d -c:v libx264 -pix_fmt yuv420p -r %d "%s" 2>&1',
+ $inputFiles,
+ implode(';', $filterComplex),
+ $lastOutput,
+ $duration,
+ $frameRate,
+ $outputFile
+ );
+
+ // 执行命令
+ exec($cmd, $output, $returnCode);
+
+ // 清理临时文件
+ array_map('unlink', glob($tempImagesDir . DS . '*'));
+ rmdir($tempImagesDir);
+
+ if ($returnCode !== 0) {
+ throw new \Exception('视频生成失败:' . implode("\n", $output));
+ }
+
+ return $outputFile;
+ }
+}
\ No newline at end of file
diff --git a/application/common/config/route.php b/application/common/config/route.php
new file mode 100644
index 0000000..54001bd
--- /dev/null
+++ b/application/common/config/route.php
@@ -0,0 +1,33 @@
+middleware(['jwt']); // 获取用户信息
+ Route::post('refresh', 'app\common\controller\Auth@refresh')->middleware(['jwt']); // 刷新令牌
+});
+
+// 附件上传相关路由
+Route::group('v1/', function () {
+ Route::post('attachment/upload', 'app\common\controller\Attachment@upload'); // 上传附件
+ Route::get('attachment/:id', 'app\common\controller\Attachment@info'); // 获取附件信息
+})->middleware(['jwt']);
+
+
+
+Route::group('v1/pay', function () {
+ Route::post('', 'app\cunkebao\controller\Pay@createOrder')->middleware(['jwt']);
+ Route::any('notify', 'app\common\controller\PaymentService@notify');
+});
+
+
+
+
+Route::get('v1/app/update', 'app\common\controller\Api@uploadApp'); //检测app是否需要更新
\ No newline at end of file
diff --git a/application/common/controller/Api.php b/application/common/controller/Api.php
new file mode 100644
index 0000000..459e8ee
--- /dev/null
+++ b/application/common/controller/Api.php
@@ -0,0 +1,151 @@
+requestType = Request::method();
+
+ // 控制器初始化
+ $this->_initialize();
+
+ // 跨域请求检测
+ $this->_checkCors();
+ }
+
+ /**
+ * 初始化操作
+ */
+ protected function _initialize()
+ {
+ // 初始化操作
+ }
+
+ /**
+ * 跨域检测
+ * @deprecated 已由全局中间件 AllowCrossDomain 处理,此方法保留用于兼容
+ */
+ protected function _checkCors()
+ {
+ // 由全局中间件处理跨域,此处不再处理
+ if ($this->requestType === 'OPTIONS') {
+ Response::create()->send();
+ exit;
+ }
+ }
+
+ /**
+ * 操作成功返回的数据
+ * @param string $msg 提示信息
+ * @param mixed $data 返回的数据
+ * @param int $code 错误码,默认为1
+ * @param string $type 输出类型
+ * @param array $header 发送的header信息
+ */
+ protected function success($msg = '', $data = null, $code = 1, $type = null, array $header = [])
+ {
+ $this->result($msg, $data, $code, $type, $header);
+ }
+
+ /**
+ * 操作失败返回的数据
+ * @param string $msg 提示信息
+ * @param mixed $data 返回的数据
+ * @param int $code 错误码,默认为0
+ * @param string $type 输出类型
+ * @param array $header 发送的header信息
+ */
+ protected function error($msg = '', $data = null, $code = 0, $type = null, array $header = [])
+ {
+ $this->result($msg, $data, $code, $type, $header);
+ }
+
+ /**
+ * 返回封装后的API数据
+ * @param string $msg 提示信息
+ * @param mixed $data 要返回的数据
+ * @param int $code 错误码,默认为0
+ * @param string $type 输出类型,支持json/xml/jsonp
+ * @param array $header 发送的header信息
+ */
+ protected function result($msg, $data = null, $code = 0, $type = null, array $header = [])
+ {
+ $result = [
+ 'code' => $code,
+ 'msg' => $msg,
+ 'time' => time(),
+ 'data' => $data,
+ ];
+
+ // 返回数据格式
+ $type = $type ?: $this->responseType;
+
+ // 发送响应
+ $response = Response::create($result, $type)->header($header);
+ $response->send();
+ exit;
+ }
+
+
+ public function uploadApp()
+ {
+ $type = $this->request->param('type', '');
+ if (empty($type)){
+ return ResponseHelper::error('参数缺失');
+ }
+
+ if (!in_array($type,['ckb','aiStore'])){
+ return ResponseHelper::error('参数错误');
+ }
+
+ $data = Db::name('app_version')
+ ->field('version,downloadUrl,updateContent,forceUpdate')
+ ->where(['type'=>$type])
+ ->order('id DESC')
+ ->find();
+ return ResponseHelper::success($data, '获取成功');
+
+ }
+}
\ No newline at end of file
diff --git a/application/common/controller/Attachment.php b/application/common/controller/Attachment.php
new file mode 100644
index 0000000..b3ffabb
--- /dev/null
+++ b/application/common/controller/Attachment.php
@@ -0,0 +1,144 @@
+ 400,
+ 'msg' => '请选择要上传的文件'
+ ]);
+ }
+
+ // 验证文件
+ $validate = \think\facade\Validate::rule([
+ 'file' => [
+ 'fileSize' => 50485760, // 50MB
+ 'fileExt' => 'jpg,jpeg,png,gif,doc,docx,pdf,zip,rar,mp4,mp3,csv,xlsx,xls,ppt,pptx,txt',
+ ]
+ ]);
+
+ if (!$validate->check(['file' => $file])) {
+ return json([
+ 'code' => 400,
+ 'msg' => $validate->getError()
+ ]);
+ }
+
+ // 生成文件hash
+ $hashKey = md5_file($file->getRealPath());
+
+ // 检查文件是否已存在
+ $existFile = AttachmentModel::getByHashKey($hashKey);
+ if ($existFile) {
+ return json([
+ 'code' => 200,
+ 'msg' => '文件已存在',
+ 'data' => [
+ 'id' => $existFile['id'],
+ 'name' => $existFile['name'],
+ 'url' => $existFile['source'],
+ 'size' => isset($existFile['size']) ? $existFile['size'] : 0
+ ]
+ ]);
+ }
+
+
+
+ // 生成OSS对象名称
+ $objectName = AliyunOSS::generateObjectName($file->getInfo('name'));
+
+ // 上传到OSS
+ $result = AliyunOSS::uploadFile($file->getRealPath(), $objectName);
+
+
+ if (!$result['success']) {
+ return json([
+ 'code' => 500,
+ 'msg' => '文件上传失败:' . $result['error']
+ ]);
+ }
+
+ // 保存到数据库
+ $attachmentData = [
+ 'name' => Request::param('name') ?: $file->getInfo('name'),
+ 'hash_key' => $hashKey,
+ 'server' => 'aliyun_oss',
+ 'source' => $result['url'],
+ 'size' => $result['size'],
+ 'suffix' => pathinfo($file->getInfo('name'), PATHINFO_EXTENSION)
+ ];
+
+ $attachment = AttachmentModel::addAttachment($attachmentData);
+
+ if (!$attachment) {
+ return json([
+ 'code' => 500,
+ 'msg' => '保存附件信息失败'
+ ]);
+ }
+
+ return json([
+ 'code' => 200,
+ 'msg' => '上传成功',
+ 'data' => [
+ 'id' => $attachment->id,
+ 'name' => $attachmentData['name'],
+ 'url' => $attachmentData['source'],
+ 'size' => $attachmentData['size']
+ ]
+ ]);
+
+ } catch (\Exception $e) {
+ return json([
+ 'code' => 500,
+ 'msg' => '上传失败:' . $e->getMessage()
+ ]);
+ }
+ }
+
+ /**
+ * 获取附件信息
+ * @param int $id 附件ID
+ * @return \think\response\Json
+ */
+ public function info($id)
+ {
+ try {
+ $attachment = AttachmentModel::find($id);
+
+ if (!$attachment) {
+ return json([
+ 'code' => 404,
+ 'msg' => '附件不存在'
+ ]);
+ }
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => $attachment
+ ]);
+
+ } catch (\Exception $e) {
+ return json([
+ 'code' => 500,
+ 'msg' => '获取失败:' . $e->getMessage()
+ ]);
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/common/controller/Auth.php b/application/common/controller/Auth.php
new file mode 100644
index 0000000..73f7d84
--- /dev/null
+++ b/application/common/controller/Auth.php
@@ -0,0 +1,157 @@
+authService = new AuthService();
+ }
+
+ /**
+ * 用户登录
+ * @return \think\response\Json
+ */
+ public function login()
+ {
+ // 获取登录参数
+ $params = Request::only(['account', 'password', 'typeId']);
+
+ // 参数验证
+ $validate = validate('common/Auth');
+ if (!$validate->scene('login')->check($params)) {
+ return ResponseHelper::error($validate->getError());
+ }
+
+ try {
+ // 调用登录服务
+ $result = $this->authService->login(
+ $params['account'],
+ $params['password'],
+ $params['typeId'],
+ Request::ip()
+ );
+
+ return ResponseHelper::success($result, '登录成功');
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage());
+ }
+ }
+
+ /**
+ * 手机号验证码登录
+ * @return \think\response\Json
+ */
+ public function mobileLogin()
+ {
+ // 获取登录参数
+ $params = Request::only(['account', 'code', 'typeId']);
+
+ // 参数验证
+ $validate = validate('common/Auth');
+ if (!$validate->scene('mobile_login')->check($params)) {
+ return ResponseHelper::error($validate->getError());
+ }
+
+ try {
+ // 判断验证码是否已加密
+ $isEncrypted = isset($params['is_encrypted']) && $params['is_encrypted'] === true;
+
+ // 调用手机号登录服务
+ $result = $this->authService->mobileLogin(
+ $params['account'],
+ $params['code'],
+ Request::ip(),
+ $isEncrypted
+ );
+
+ return ResponseHelper::success($result, '登录成功');
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage());
+ }
+ }
+
+ /**
+ * 发送验证码
+ * @return \think\response\Json
+ */
+ public function sendCode()
+ {
+ // 获取参数
+ $params = Request::only(['account', 'type']);
+
+ // 参数验证
+ $validate = validate('common/Auth');
+ if (!$validate->scene('send_code')->check($params)) {
+ return ResponseHelper::error($validate->getError());
+ }
+
+ try {
+ // 调用发送验证码服务
+ $result = $this->authService->sendLoginCode(
+ $params['account'],
+ $params['type']
+ );
+ return ResponseHelper::success($result, '验证码发送成功');
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage());
+ }
+ }
+
+ /**
+ * 获取用户信息
+ * @return \think\response\Json
+ */
+ public function info()
+ {
+ try {
+ $result = $this->authService->getUserInfo(request()->userInfo);
+ return ResponseHelper::success($result);
+ } catch (\Exception $e) {
+ return ResponseHelper::unauthorized($e->getMessage());
+ }
+ }
+
+ /**
+ * 刷新令牌
+ * @return \think\response\Json
+ */
+ public function refresh()
+ {
+ try {
+ $result = $this->authService->refreshToken(request()->userInfo);
+ return ResponseHelper::success($result, '刷新成功');
+ } catch (\Exception $e) {
+ return ResponseHelper::unauthorized($e->getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/common/controller/BaseController.php b/application/common/controller/BaseController.php
new file mode 100644
index 0000000..81dcf83
--- /dev/null
+++ b/application/common/controller/BaseController.php
@@ -0,0 +1,12 @@
+ 需要在请求结束时清理的临时文件
+ */
+ protected static $tempFiles = [];
+
+ /**
+ * 导出 Excel(支持指定列插入图片)
+ *
+ * @param string $fileName 输出文件名(可不带扩展名)
+ * @param array $headers 列定义,例如 ['name' => '姓名', 'phone' => '电话']
+ * @param array $rows 数据行,需与 $headers 的 key 对应
+ * @param array $imageColumns 需要渲染为图片的列 key 列表
+ * @param string $sheetName 工作表名称
+ * @param array $options 额外选项:
+ * - imageWidth(图片宽度,默认100)
+ * - imageHeight(图片高度,默认100)
+ * - imageColumnWidth(图片列宽,默认15)
+ * - titleRow(标题行内容,支持多行文本数组)
+ *
+ * @throws Exception
+ */
+ public static function exportExcelWithImages(
+ $fileName,
+ array $headers,
+ array $rows,
+ array $imageColumns = [],
+ $sheetName = 'Sheet1',
+ array $options = []
+ ) {
+ if (empty($headers)) {
+ throw new Exception('导出列定义不能为空');
+ }
+ if (empty($rows)) {
+ throw new Exception('导出数据不能为空');
+ }
+
+ // 抑制 PHPExcel 库中已废弃的大括号语法警告(PHP 7.4+)
+ $oldErrorReporting = error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);
+
+ // 默认选项
+ $imageWidth = isset($options['imageWidth']) ? (int)$options['imageWidth'] : 100;
+ $imageHeight = isset($options['imageHeight']) ? (int)$options['imageHeight'] : 100;
+ $imageColumnWidth = isset($options['imageColumnWidth']) ? (float)$options['imageColumnWidth'] : 15;
+ $rowHeight = isset($options['rowHeight']) ? (int)$options['rowHeight'] : ($imageHeight + 10);
+
+ $excel = new PHPExcel();
+ $sheet = $excel->getActiveSheet();
+ $sheet->setTitle($sheetName);
+
+ $columnKeys = array_keys($headers);
+ $totalColumns = count($columnKeys);
+ $lastColumnLetter = self::columnLetter($totalColumns - 1);
+
+ // 定义特定列的固定宽度(如果未指定则使用默认值)
+ $columnWidths = isset($options['columnWidths']) ? $options['columnWidths'] : [];
+
+ // 检查是否有标题行
+ $titleRow = isset($options['titleRow']) ? $options['titleRow'] : null;
+ $dataStartRow = 1; // 数据开始行(表头行)
+
+ // 如果有标题行,先写入标题行(支持数组或字符串)
+ if (!empty($titleRow)) {
+ $dataStartRow = 2; // 数据从第2行开始(第1行是标题,第2行是表头)
+
+ // 合并标题行单元格(从第一列到最后一列)
+ $titleRange = 'A1:' . $lastColumnLetter . '1';
+ $sheet->mergeCells($titleRange);
+
+ // 构建标题内容(支持多行数组或字符串)
+ $titleContent = '';
+ if (is_array($titleRow)) {
+ $titleContent = implode("\n", $titleRow);
+ } else {
+ $titleContent = (string)$titleRow;
+ }
+
+ // 写入标题
+ $sheet->setCellValue('A1', $titleContent);
+
+ // 设置标题行样式
+ $sheet->getStyle('A1')->applyFromArray([
+ 'font' => ['bold' => true, 'size' => 16],
+ 'alignment' => [
+ 'horizontal' => \PHPExcel_Style_Alignment::HORIZONTAL_CENTER,
+ 'vertical' => \PHPExcel_Style_Alignment::VERTICAL_CENTER,
+ 'wrap' => true
+ ],
+ 'fill' => [
+ 'type' => \PHPExcel_Style_Fill::FILL_SOLID,
+ 'color' => ['rgb' => 'FFF8DC'] // 浅黄色背景
+ ],
+ 'borders' => [
+ 'allborders' => [
+ 'style' => \PHPExcel_Style_Border::BORDER_THIN,
+ 'color' => ['rgb' => '000000']
+ ]
+ ]
+ ]);
+ $sheet->getRowDimension(1)->setRowHeight(80); // 标题行高度
+ }
+
+ // 写入表头并设置列宽
+ $headerRow = $dataStartRow;
+ foreach ($columnKeys as $index => $key) {
+ $columnLetter = self::columnLetter($index);
+ $sheet->setCellValue($columnLetter . $headerRow, $headers[$key]);
+
+ // 如果是图片列,设置固定列宽
+ if (in_array($key, $imageColumns, true)) {
+ $sheet->getColumnDimension($columnLetter)->setWidth($imageColumnWidth);
+ } elseif (isset($columnWidths[$key])) {
+ // 如果指定了该列的宽度,使用指定宽度
+ $sheet->getColumnDimension($columnLetter)->setWidth($columnWidths[$key]);
+ } else {
+ // 否则自动调整
+ $sheet->getColumnDimension($columnLetter)->setAutoSize(true);
+ }
+ }
+
+ // 设置表头样式
+ $headerRange = 'A' . $headerRow . ':' . $lastColumnLetter . $headerRow;
+ $sheet->getStyle($headerRange)->applyFromArray([
+ 'font' => ['bold' => true, 'size' => 11],
+ 'alignment' => [
+ 'horizontal' => \PHPExcel_Style_Alignment::HORIZONTAL_CENTER,
+ 'vertical' => \PHPExcel_Style_Alignment::VERTICAL_CENTER,
+ 'wrap' => true
+ ],
+ 'fill' => [
+ 'type' => \PHPExcel_Style_Fill::FILL_SOLID,
+ 'color' => ['rgb' => 'FFF8DC']
+ ],
+ 'borders' => [
+ 'allborders' => [
+ 'style' => \PHPExcel_Style_Border::BORDER_THIN,
+ 'color' => ['rgb' => '000000']
+ ]
+ ]
+ ]);
+ $sheet->getRowDimension($headerRow)->setRowHeight(30); // 增加表头行高以确保文本完整显示
+
+ // 写入数据与图片
+ $dataRowStart = $dataStartRow + 1; // 数据从表头行下一行开始
+ foreach ($rows as $rowIndex => $rowData) {
+ $excelRow = $dataRowStart + $rowIndex; // 数据行
+ $maxRowHeight = $rowHeight; // 记录当前行的最大高度
+
+ foreach ($columnKeys as $colIndex => $key) {
+ $columnLetter = self::columnLetter($colIndex);
+ $cell = $columnLetter . $excelRow;
+ $value = isset($rowData[$key]) ? $rowData[$key] : '';
+
+ if (in_array($key, $imageColumns, true) && !empty($value)) {
+ $imagePath = self::resolveImagePath($value);
+ if ($imagePath) {
+ // 获取图片实际尺寸并等比例缩放
+ $imageSize = @getimagesize($imagePath);
+ if ($imageSize) {
+ $originalWidth = $imageSize[0];
+ $originalHeight = $imageSize[1];
+
+ // 计算等比例缩放后的尺寸
+ $ratio = min($imageWidth / $originalWidth, $imageHeight / $originalHeight);
+ $scaledWidth = $originalWidth * $ratio;
+ $scaledHeight = $originalHeight * $ratio;
+
+ // 确保不超过最大尺寸
+ if ($scaledWidth > $imageWidth) {
+ $scaledWidth = $imageWidth;
+ $scaledHeight = $originalHeight * ($imageWidth / $originalWidth);
+ }
+ if ($scaledHeight > $imageHeight) {
+ $scaledHeight = $imageHeight;
+ $scaledWidth = $originalWidth * ($imageHeight / $originalHeight);
+ }
+
+ $drawing = new PHPExcel_Worksheet_Drawing();
+ $drawing->setPath($imagePath);
+ $drawing->setCoordinates($cell);
+
+ // 居中显示图片(Excel列宽1单位≈7像素,行高1单位≈0.75像素)
+ $cellWidthPx = $imageColumnWidth * 7;
+ $cellHeightPx = $maxRowHeight * 0.75;
+ $offsetX = max(2, ($cellWidthPx - $scaledWidth) / 2);
+ $offsetY = max(2, ($cellHeightPx - $scaledHeight) / 2);
+
+ $drawing->setOffsetX((int)$offsetX);
+ $drawing->setOffsetY((int)$offsetY);
+ $drawing->setWidth((int)$scaledWidth);
+ $drawing->setHeight((int)$scaledHeight);
+ $drawing->setWorksheet($sheet);
+
+ // 更新行高以适应图片(留出一些边距)
+ $neededHeight = (int)($scaledHeight / 0.75) + 10;
+ if ($neededHeight > $maxRowHeight) {
+ $maxRowHeight = $neededHeight;
+ }
+ } else {
+ // 如果无法获取图片尺寸,使用默认尺寸
+ $drawing = new PHPExcel_Worksheet_Drawing();
+ $drawing->setPath($imagePath);
+ $drawing->setCoordinates($cell);
+ $drawing->setOffsetX(5);
+ $drawing->setOffsetY(5);
+ $drawing->setWidth($imageWidth);
+ $drawing->setHeight($imageHeight);
+ $drawing->setWorksheet($sheet);
+ }
+ } else {
+ $sheet->setCellValue($cell, '');
+ }
+ } else {
+ $sheet->setCellValue($cell, $value);
+ // 设置文本对齐和换行
+ $style = $sheet->getStyle($cell);
+ $style->getAlignment()->setVertical(\PHPExcel_Style_Alignment::VERTICAL_CENTER);
+ $style->getAlignment()->setWrapText(true);
+ // 根据列类型设置水平对齐
+ if (in_array($key, ['date', 'postTime'])) {
+ $style->getAlignment()->setHorizontal(\PHPExcel_Style_Alignment::HORIZONTAL_CENTER);
+ } else {
+ $style->getAlignment()->setHorizontal(\PHPExcel_Style_Alignment::HORIZONTAL_LEFT);
+ }
+ }
+ }
+
+ // 设置行高
+ $sheet->getRowDimension($excelRow)->setRowHeight($maxRowHeight);
+ }
+
+ $safeName = preg_replace('/[^\w\-]/', '_', $fileName ?: 'export_' . date('Ymd_His'));
+ if (stripos($safeName, '.xlsx') === false) {
+ $safeName .= '.xlsx';
+ }
+
+ if (ob_get_length()) {
+ ob_end_clean();
+ }
+
+ header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
+ header('Cache-Control: max-age=0');
+ header('Content-Disposition: attachment;filename="' . $safeName . '"');
+
+ try {
+ $writer = PHPExcel_IOFactory::createWriter($excel, 'Excel2007');
+ $writer->save('php://output');
+ } catch (\Exception $e) {
+ // 恢复错误报告级别
+ error_reporting($oldErrorReporting);
+ throw $e;
+ }
+
+ // 恢复错误报告级别
+ error_reporting($oldErrorReporting);
+ self::cleanupTempFiles();
+ exit;
+ }
+
+ /**
+ * 根据列序号生成 Excel 列字母
+ *
+ * @param int $index
+ * @return string
+ */
+ protected static function columnLetter($index)
+ {
+ $letters = '';
+ do {
+ $letters = chr($index % 26 + 65) . $letters;
+ $index = intval($index / 26) - 1;
+ } while ($index >= 0);
+
+ return $letters;
+ }
+
+ /**
+ * 将远程或本地图片路径转换为可用的本地文件路径
+ *
+ * @param string $path
+ * @return string|null
+ */
+ protected static function resolveImagePath($path)
+ {
+ if (empty($path)) {
+ return null;
+ }
+
+ if (preg_match('/^https?:\/\//i', $path)) {
+ $tempFile = tempnam(sys_get_temp_dir(), 'export_img_');
+ $stream = @file_get_contents($path);
+ if ($stream === false) {
+ return null;
+ }
+ file_put_contents($tempFile, $stream);
+ self::$tempFiles[] = $tempFile;
+ return $tempFile;
+ }
+
+ if (file_exists($path)) {
+ return $path;
+ }
+
+ return null;
+ }
+
+ /**
+ * 清理所有临时文件
+ */
+ protected static function cleanupTempFiles()
+ {
+ foreach (self::$tempFiles as $file) {
+ if (file_exists($file)) {
+ @unlink($file);
+ }
+ }
+ self::$tempFiles = [];
+ }
+}
\ No newline at end of file
diff --git a/application/common/controller/GetOpenid.php b/application/common/controller/GetOpenid.php
new file mode 100644
index 0000000..b33c81d
--- /dev/null
+++ b/application/common/controller/GetOpenid.php
@@ -0,0 +1,52 @@
+ Env::get('weChat.appid'),
+ 'secret' => Env::get('weChat.secret'),
+ 'response_type' => 'array'
+ ];
+ $this->app = Factory::officialAccount($config);
+ }
+
+
+
+ public function index()
+ {
+ $app = $this->app;
+ $oauth = $app->oauth;
+
+ // 未登录
+ if (empty($_SESSION['wechat_user'])) {
+
+ $_SESSION['target_url'] = 'user/profile';
+
+ $redirectUrl = $oauth->redirect();
+
+ exit_data($redirectUrl);
+ header("Location: {$redirectUrl}");
+ exit;
+ }
+
+ // 已经登录过
+ $user = $_SESSION['wechat_user'];
+
+ exit_data($user);
+
+ return 'Hello, World!';
+ }
+
+}
\ No newline at end of file
diff --git a/application/common/controller/PasswordLoginController.php b/application/common/controller/PasswordLoginController.php
new file mode 100644
index 0000000..232e4e6
--- /dev/null
+++ b/application/common/controller/PasswordLoginController.php
@@ -0,0 +1,157 @@
+where('phone', $account)->whereOr('account', $account);
+ }
+ )
+ ->where(
+ function ($query) use ($typeId) {
+ $query->where('status', 1)->where('typeId', $typeId);
+ }
+ )->find();
+
+ if(!empty($user)){
+ return $user;
+ }else{
+ return '';
+ }
+ }
+
+ /**
+ * 获取用户信息
+ *
+ * @param string $account 账号(手机号)
+ * @param string $password 密码(可能是加密后的)
+ * @param int $typeId 身份信息
+ * @return array|null
+ */
+ protected function getUser(string $account, string $password, int $typeId): array
+ {
+ $user = $this->getUserProfileWithAccountAndType($account, $typeId);
+ if (!$user) {
+ throw new \Exception('用户不存在或已禁用', 403);
+ }
+
+ $password = md5($password);
+ if ($user->passwordMd5 !== $password) {
+ throw new \Exception('账号或密码错误', 403);
+ }
+
+ return array_merge($user->toArray(), [
+ 'lastLoginIp' => $this->request->ip(),
+ 'lastLoginTime' => time()
+ ]);
+ }
+
+ /**
+ * 数据验证
+ *
+ * @param array $params
+ * @return $this
+ * @throws \Exception
+ */
+ protected function dataValidate(array $params): self
+ {
+ $validate = Validate::make([
+ 'account' => 'require',
+ 'password' => 'require|length:6,64',
+ 'typeId' => 'require|in:1,2',
+ ], [
+ 'account.require' => '账号不能为空',
+ 'password.require' => '密码不能为空',
+ 'password.length' => '密码长度必须在6-64个字符之间',
+ 'typeId.require' => '用户类型不能为空',
+ 'typeId.in' => '用户类型错误',
+ ]);
+
+ if (!$validate->check($params)) {
+ throw new \Exception($validate->getError(), 400);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 用户登录
+ *
+ * @param string $account 账号(手机号)
+ * @param string $password 密码(可能是加密后的)
+ * @param string $typeId 登录IP
+ * @param string $deviceId 本地设备imei
+ * @return array
+ * @throws \Exception
+ */
+ protected function doLogin(string $account, string $password, int $typeId, string $deviceId): array
+ {
+ // 获取用户信息
+ $member = $this->getUser($account, $password, $typeId);
+ $deviceTotal = Db::name('device')->where(['companyId' => $member['companyId'],'deleteTime' => 0])->count();
+
+ //更新设备imei
+ if ($typeId == 2 && !empty($deviceId)){
+ $deviceUser = Db::name('device_user')->where(['companyId' => $member['companyId'],'userId' => $member['id'],'deleteTime' => 0])->find();
+ if (!empty($deviceUser)){
+ $device = Db::name('device')->where(['companyId' => $member['companyId'],'deleteTime' => 0,'id' => $deviceUser['deviceId']])->find();
+ if (!empty($device) && empty($device['deviceImei'])){
+ Db::table('s2_device')->where(['id' => $device['id']])->update(['deviceImei' => $deviceId,'updateTime' => time()]);
+ Db::name('device')->where(['id' => $device['id']])->update(['deviceImei' => $deviceId,'updateTime' => time()]);
+ }
+ }
+ }
+
+
+ // 生成JWT令牌
+ $token = JwtUtil::createToken($member, 86400 * 30);
+ $token_expired = time() + 86400 * 30;
+ return compact('member', 'token', 'token_expired','deviceTotal');
+ }
+
+ /**
+ * 用户登录
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ $params = $this->request->only(['account', 'password', 'typeId','deviceId']);
+ try {
+ $deviceId = isset($params['deviceId']) ? $params['deviceId'] : '';
+ $userData = $this->dataValidate($params)->doLogin(
+ $params['account'],
+ $params['password'],
+ $params['typeId'],
+ $deviceId
+ );
+
+
+ return ResponseHelper::success($userData, '登录成功');
+ } catch (Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/common/controller/PaymentService.php b/application/common/controller/PaymentService.php
new file mode 100644
index 0000000..c8a030e
--- /dev/null
+++ b/application/common/controller/PaymentService.php
@@ -0,0 +1,543 @@
+ $service,
+ 'sign_type' => PaymentUtil::SIGN_TYPE_MD5,
+ 'mch_id' => Env::get('payment.mchId'),
+ 'out_trade_no' => $order['orderNo'],
+ 'body' => $order['goodsName'] ?? '',
+ 'total_fee' => $order['money'] ?? 0,
+ 'mch_create_ip' => Request::ip(),
+ 'notify_url' => $order['notify_url'] ?? Env::get('payment.notify_url', '127.0.0.1'),
+ 'nonce_str' => PaymentUtil::generateNonceStr(),
+ ];
+
+ // 微信JSAPI支付需要openid
+ if ($service == 'pay.weixin.jspay') {
+ // $params['sub_openid'] = 'oB44Yw1T6bfVAZwjj729P-6CUSPE';
+ $params['is_raw'] = 0;
+ $params['mch_app_name'] = '存客宝';
+ $params['mch_app_id'] = 'https://kr-op.quwanzhi.com';
+ }
+
+ // 支付宝JSAPI支付需要buyer_id(可选)
+ if ($service == 'pay.alipay.jspay') {
+ $params['is_raw'] = 0;
+ $params['quit_url'] = $params['notify_url'];
+ $params['buyer_id'] = '';
+ }
+
+ Db::startTrans();
+ try {
+ // 签名
+ $secret = Env::get('payment.key');
+ $params['sign_type'] = 'MD5';
+ $params['sign'] = PaymentUtil::generateSign($params, $secret, 'MD5');
+
+ $url = Env::get('payment.url');
+ if (empty($url)) {
+ throw new \Exception('支付网关地址未配置');
+ }
+
+ // 创建订单
+ Order::create([
+ 'mchId' => $params['mch_id'],
+ 'companyId' => isset($order['companyId']) ? $order['companyId'] : 0,
+ 'userId' => isset($order['userId']) ? $order['userId'] : 0,
+ 'orderType' => isset($order['orderType']) ? $order['orderType'] : 1,
+ 'status' => 0,
+ 'goodsId' => isset($order['goodsId']) ? $order['goodsId'] : 0,
+ 'goodsName' => isset($order['goodsName']) ? $order['goodsName'] : '',
+ 'money' => isset($order['money']) ? $order['money'] : 0,
+ 'goodsSpecs' => isset($order['goodsSpecs']) ? json_encode($order['goodsSpecs'], 256) : json_encode([]),
+ 'orderNo' => isset($order['orderNo']) ? $order['orderNo'] : '',
+ 'ip' => Request::ip(),
+ 'nonceStr' => isset($order['nonceStr']) ? $order['nonceStr'] : '',
+ 'createTime' => time(),
+ ]);
+
+ // XML POST 请求
+ $xmlBody = $this->arrayToXml($params);
+ $response = $this->postXml($url, $xmlBody);
+ $parsed = $this->parseXmlOrRaw($response);
+
+
+ if ($parsed['status'] == 0 && $parsed['result_code'] == 0) {
+ Db::commit();
+
+ // 根据service类型返回不同的数据格式(仅返回接口文档中的字段)
+ $responseData = null;
+ if ($service == 'unified.trade.native') {
+ // 扫码支付返回二维码URL
+ $responseData = $parsed['code_img_url'] ?? '';
+ } elseif ($service == 'pay.weixin.jspay') {
+ // 微信JSAPI支付返回支付参数(仅返回接口文档中存在的字段)
+ $responseData = [];
+ if (isset($parsed['appid'])) $responseData['appid'] = $parsed['appid'];
+ if (isset($parsed['time_stamp'])) $responseData['time_stamp'] = $parsed['time_stamp'];
+ if (isset($parsed['nonce_str'])) $responseData['nonce_str'] = $parsed['nonce_str'];
+ if (isset($parsed['package'])) $responseData['package'] = $parsed['package'];
+ if (isset($parsed['sign_type'])) $responseData['sign_type'] = $parsed['sign_type'];
+ if (isset($parsed['pay_sign'])) $responseData['pay_sign'] = $parsed['pay_sign'];
+ } elseif ($service == 'pay.alipay.jspay') {
+ // 支付宝JSAPI支付返回订单信息(仅返回接口文档中存在的字段)
+ $responseData = [];
+ if (isset($parsed['order_info'])) $responseData['order_info'] = $parsed['order_info'];
+ if (isset($parsed['order_string'])) $responseData['order_string'] = $parsed['order_string'];
+ }
+
+ return json_encode(['code' => 200, 'msg' => '订单创建成功', 'data' => $responseData]);
+ } else {
+ Db::rollback();
+ return json_encode(['code' => 500, 'msg' => '订单创建失败:' . ($parsed['err_msg'] ?? '未知错误')]);
+ }
+
+ } catch (\Exception $e) {
+ Db::rollback();
+ return json_encode(['code' => 500, 'msg' => '订单创建失败:' . $e->getMessage()]);
+ }
+ }
+
+
+ /**
+ * POST 请求(x-www-form-urlencoded)
+ */
+ protected function httpPost(string $url, array $params, array $headers = [])
+ {
+ if (!function_exists('requestCurl')) {
+ throw new \RuntimeException('requestCurl 未定义');
+ }
+ return requestCurl($url, $params, 'POST', $headers, 'dataBuild');
+ }
+
+ /**
+ * 解析响应
+ */
+ protected function parseResponse($response)
+ {
+ if ($response === '' || $response === null) {
+ return '';
+ }
+ $decoded = json_decode($response, true);
+ if (json_last_error() === JSON_ERROR_NONE) {
+ return $decoded;
+ }
+ if (strpos($response, '=') !== false && strpos($response, '&') !== false) {
+ $arr = [];
+ foreach (explode('&', $response) as $pair) {
+ if ($pair === '') continue;
+ $kv = explode('=', $pair, 2);
+ $arr[$kv[0]] = $kv[1] ?? '';
+ }
+ return $arr;
+ }
+ return $response;
+ }
+
+ /**
+ * 以 XML 方式 POST(text/xml)
+ */
+ protected function postXml(string $url, string $xml)
+ {
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_POST, 1);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($ch, CURLOPT_TIMEOUT, 30);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
+ 'Content-Type: text/xml; charset=UTF-8'
+ ]);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
+ $res = curl_exec($ch);
+ curl_close($ch);
+ return $res;
+ }
+
+ /**
+ * 数组转 XML(按 ASCII 升序,字符串走 CDATA)
+ */
+ protected function arrayToXml(array $data): string
+ {
+ // 过滤空值
+ $filtered = [];
+ foreach ($data as $k => $v) {
+ if ($v === '' || $v === null) continue;
+ $filtered[$k] = $v;
+ }
+ ksort($filtered, SORT_STRING);
+
+ $xml = '';
+ foreach ($filtered as $key => $value) {
+ if (is_numeric($value)) {
+ $xml .= "<{$key}>{$value}{$key}>";
+ } else {
+ $xml .= "<{$key}>{$key}>";
+ }
+ }
+ $xml .= ' ';
+ return $xml;
+ }
+
+ /**
+ * 解析 XML 响应
+ */
+ protected function parseXmlOrRaw($response)
+ {
+ if (!is_string($response) || $response === '') {
+ return $response;
+ }
+ libxml_use_internal_errors(true);
+ $xml = simplexml_load_string($response, 'SimpleXMLElement', LIBXML_NOCDATA);
+ if ($xml !== false) {
+ $json = json_encode($xml, JSON_UNESCAPED_UNICODE);
+ return json_decode($json, true);
+ }
+ return $response;
+ }
+
+ /**
+ * 支付结果异步通知
+ * - 威富通回调为 XML;需校验签名与业务字段并更新订单
+ * - 支持扫码付款、微信支付、支付宝支付的通知
+ * - 回应:成功返回XML格式SUCCESS,失败返回XML格式FAIL
+ * @return string XML响应
+ */
+ public function notify()
+ {
+ $rawBody = file_get_contents('php://input');
+ $payload = $this->parseXmlOrRaw($rawBody);
+ if (!is_array($payload) || empty($payload)) {
+ \think\facade\Log::error('支付通知:XML解析错误', ['rawBody' => $rawBody]);
+ return ' ';
+ }
+
+ // 验证签名
+ $secret = Env::get('payment.key');
+ if (!empty($secret) && isset($payload['sign'])) {
+ $signType = $payload['sign_type'] ?? 'MD5';
+ if (!PaymentUtil::verifySign($payload, $secret, $signType)) {
+ \think\facade\Log::error('支付通知:签名验证失败', ['payload' => $payload]);
+ return ' ';
+ }
+ }
+
+ // 检查通信状态
+ if (isset($payload['status']) && $payload['status'] != 0) {
+ $errMsg = $payload['err_msg'] ?? '通信失败';
+ \think\facade\Log::error('支付通知:通信失败', ['payload' => $payload]);
+ return ' ';
+ }
+
+ // 检查业务结果
+ if (isset($payload['result_code']) && $payload['result_code'] != 0) {
+ $errMsg = $payload['err_msg'] ?? '业务处理失败';
+ \think\facade\Log::error('支付通知:业务处理失败', ['payload' => $payload]);
+ return ' ';
+ }
+
+
+ // 业务处理:更新订单
+ Db::startTrans();
+ try {
+ $outTradeNo = $payload['out_trade_no'] ?? '';
+ $pay_result = $payload['pay_result'] ?? 0;
+ $time_end = $payload['time_end'] ?? '';
+
+ if (empty($outTradeNo)) {
+ Db::rollback();
+ \think\facade\Log::error('支付通知:订单号为空', ['payload' => $payload]);
+ return ' ';
+ }
+
+ $order = Order::where('orderNo', $outTradeNo)->find();
+ if (!$order) {
+ Db::rollback();
+ \think\facade\Log::error('支付通知:订单不存在', ['out_trade_no' => $outTradeNo]);
+ return ' ';
+ }
+
+ // 如果订单已支付,直接返回成功(防止重复处理)
+ if ($order->status == 1) {
+ Db::rollback();
+ return ' ';
+ }
+
+ if ($pay_result != 0) {
+ $order->payInfo = $payload['pay_info'] ?? '支付失败';
+ $order->status = 3;
+ $order->save();
+ Db::commit();
+ \think\facade\Log::error('支付通知:支付失败', ['orderNo' => $outTradeNo, 'pay_info' => $payload['pay_info'] ?? '']);
+ return ' ';
+ }
+
+ // 根据trade_type判断支付方式
+ $tradeType = $payload['trade_type'] ?? '';
+ if (strpos($tradeType, 'wechat') !== false || strpos($tradeType, 'weixin') !== false) {
+ $order->payType = 1; // 微信支付
+ } elseif (strpos($tradeType, 'alipay') !== false) {
+ $order->payType = 2; // 支付宝支付
+ } else {
+ // 默认根据原有逻辑判断
+ $order->payType = $tradeType == 'pay.wechat.jspay' ? 1 : 2;
+ }
+
+ $order->status = 1;
+ $order->payTime = $this->parsePayTime($time_end);
+ $order->transactionId = $payload['transaction_id'] ?? '';
+ $order->save();
+ //订单处理
+ $this->processOrder($order);
+ Db::commit();
+
+ // 返回成功响应(XML格式)
+ return ' ';
+ } catch (\Exception $e) {
+ Db::rollback();
+ \think\facade\Log::error('支付通知:处理异常', ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
+ return ' ';
+ }
+ }
+
+ /**
+ * 解析威富通时间(yyyyMMddHHmmss)为时间戳
+ */
+ protected function parsePayTime(string $timeEnd)
+ {
+ if ($timeEnd === '') {
+ return 0;
+ }
+ // 期望格式:20250102153045
+ if (preg_match('/^\\d{14}$/', $timeEnd) !== 1) {
+ return 0;
+ }
+ $dt = \DateTime::createFromFormat('YmdHis', $timeEnd, new \DateTimeZone('Asia/Shanghai'));
+ return $dt ? $dt->getTimestamp() : 0;
+ }
+
+ /**
+ * 查询订单(威富通 unified.trade.query)
+ * - 入参:商户订单号或平台交易号
+ * - 出参:统一 JSON 格式,包含交易状态与关键信息
+ * @param array $query
+ * - out_trade_no: string 商户订单号(与 transaction_id 二选一)
+ * - transaction_id: string 平台交易号(与 out_trade_no 二选一)
+ * @return \think\response\Json
+ */
+ public function queryOrder($orderNo = '')
+ {
+ if (empty($orderNo)) {
+ return json(['code' => 422, 'msg' => '订单号缺失']);
+ }
+
+ $params = [
+ 'service' => 'unified.trade.query',
+ 'mch_id' => Env::get('payment.mchId'),
+ 'out_trade_no' => $orderNo ?: null,
+ 'nonce_str' => PaymentUtil::generateNonceStr(),
+ 'sign_type' => 'MD5',
+ ];
+
+ // 过滤空值后签名
+ $secret = Env::get('payment.key');
+ if (empty($secret)) {
+ return json_encode(['code' => 500, 'msg' => '支付密钥未配置']);
+ }
+
+ $filtered = [];
+ foreach ($params as $k => $v) {
+ if ($v === '' || $v === null) continue;
+ $filtered[$k] = $v;
+ }
+ $filtered['sign'] = PaymentUtil::generateSign($filtered, $secret, $filtered['sign_type']);
+
+ $url = Env::get('payment.url');
+ if (empty($url)) {
+ return json_encode(['code' => 500, 'msg' => '支付网关地址未配置']);
+ }
+
+ // 请求网关
+ $xmlBody = $this->arrayToXml($filtered);
+ $response = $this->postXml($url, $xmlBody);
+ $parsed = $this->parseXmlOrRaw($response);
+
+ if (!is_array($parsed)) {
+ return json_encode(['code' => 500, 'msg' => '响应解析失败', 'data' => $response]);
+ }
+
+ if ($parsed['status'] != 0) {
+ return json_encode(['code' => 500, 'msg' => '通信失败']);
+ }
+
+
+ if ($parsed['result_code'] == 0) {
+ $order = Order::where('orderNo', $orderNo)->lock(true)->find();
+ if (empty($order)) {
+ return json_encode(['code' => 500, 'msg' => '订单不存在']);
+ }
+
+ if ($order['status'] == 1) {
+ return json_encode(['code' => 200, 'msg' => '支付成功']);
+ }
+
+
+ $tradeState = $parsed['trade_state'] ?? '';
+ $resp = [
+ 'trade_state' => $tradeState,
+ 'trade_state_desc' => $parsed['trade_state_desc'] ?? '',
+ 'transaction_id' => $parsed['transaction_id'] ?? '',
+ 'out_trade_no' => $parsed['out_trade_no'] ?? $orderNo,
+ 'total_fee' => isset($parsed['total_fee']) ? (int)$parsed['total_fee'] : null,
+ 'time_end' => $parsed['time_end'] ?? '',
+ 'buyer_logon_id' => $parsed['buyer_logon_id'] ?? '',
+ 'bank_type' => $parsed['bank_type'] ?? '',
+ ];
+
+ // 若已支付,同步本地订单
+ if ($tradeState == 'SUCCESS') {
+ Db::startTrans();
+ try {
+ /** @var Order|null $order */
+ $order = Order::where('orderNo', $resp['out_trade_no'])->lock(true)->find();
+ if ($order) {
+ $paidAt = $this->parsePayTime($resp['time_end'] ?? '') ?: time();
+ if ($order['status'] != 1) {
+ $order->save([
+ 'status' => 1,
+ 'transactionId' => $resp['transaction_id'] ?? '',
+ 'payTime' => $paidAt,
+ 'updateTime' => time(),
+ ]);
+ }
+ }
+ //订单处理
+ $this->processOrder($order);
+ Db::commit();
+ return json_encode(['code' => 200, 'msg' => '支付成功']);
+ } catch (\Exception $e) {
+ Db::rollback();
+ return json_encode(['code' => 500, 'msg' => '付款失败' . $e->getMessage()]);
+ }
+ } else {
+ $order = Order::where('orderNo', $resp['out_trade_no'])->lock(true)->find();
+ if ($order) {
+ $order->status = 3;
+ $order->payInfo = $resp['trade_state_desc'] ?? '';
+ $order->save();
+ }
+ return json_encode(['code' => 500, 'msg' => '支付失败', 'data' => $resp]);
+ }
+
+ } else {
+ return json_encode(['code' => 500, 'msg' => '通信失败']);
+ }
+ }
+
+
+ public function processOrder($order = [])
+ {
+ if (empty($order)) {
+ return false;
+ }
+
+ switch ($order['orderType']) {
+ case 1:
+ // 处理购买算力
+ // 查询用户信息,判断是否为管理员(需要同时匹配userId和companyId)
+ $user = User::where([
+ 'id' => $order->userId,
+ 'companyId' => $order->companyId
+ ])->find();
+ $isAdmin = (!empty($user) && isset($user->isAdmin) && $user->isAdmin == 1) ? 1 : 0;
+
+ $token = TokensCompany::where(['companyId' => $order->companyId,'userId' => $order->userId])->find();
+ $goodsSpecs = json_decode($order->goodsSpecs, true);
+ if (!empty($token)) {
+ $token->tokens = $token->tokens + $goodsSpecs['tokens'];
+ $token->updateTime = time();
+ $token->save();
+ $newTokens = $token->tokens;
+ } else {
+ $tokensCompany = new TokensCompany();
+ $tokensCompany->userId = $order->userId;
+ $tokensCompany->companyId = $order->companyId;
+ $tokensCompany->tokens = $goodsSpecs['tokens'];
+ $tokensCompany->isAdmin = $isAdmin;
+ $tokensCompany->createTime = time();
+ $tokensCompany->updateTime = time();
+ $tokensCompany->save();
+ $newTokens = $tokensCompany->tokens;
+ }
+ //添加记录
+ $record = new TokensRecord();
+ $record->companyId = $order->companyId;
+ $record->userId = $order->userId;
+ $record->type = 1;
+ $record->form = 5;
+ $record->wechatAccountId = 0;
+ $record->friendIdOrGroupId = 0;
+ $record->remarks = '购买算力【' . $goodsSpecs['name'] . '】';
+ $record->tokens = $goodsSpecs['tokens'];
+ $record->balanceTokens = $newTokens;
+ $record->createTime = time();
+ $record->save();
+ break;
+ }
+
+ return true;
+ }
+
+}
\ No newline at end of file
diff --git a/application/common/controller/SendCodeController.php b/application/common/controller/SendCodeController.php
new file mode 100644
index 0000000..7dc58d2
--- /dev/null
+++ b/application/common/controller/SendCodeController.php
@@ -0,0 +1,39 @@
+request->only(['account', 'type']);
+
+ // 参数验证
+ $validate = validate('common/Auth');
+ if (!$validate->scene('send_code')->check($params)) {
+ return ResponseHelper::error($validate->getError());
+ }
+
+ try {
+ // 调用发送验证码服务
+ $result = $this->authService->sendLoginCode(
+ $params['account'],
+ $params['type']
+ );
+ return ResponseHelper::success($result, '验证码发送成功');
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/common/middleware/AllowCrossDomain.php b/application/common/middleware/AllowCrossDomain.php
new file mode 100644
index 0000000..096ce7c
--- /dev/null
+++ b/application/common/middleware/AllowCrossDomain.php
@@ -0,0 +1,59 @@
+header('origin');
+//
+// // 当请求使用 credentials 模式时,不能使用通配符
+// // 必须指定具体的域名或提取请求中的 Origin
+// $allowOrigin = '*';
+// if ($origin) {
+// // 如果需要限制特定域名,可以在这里判断
+// // 以下是允许的域名列表,如果请求来自这些域名之一,则允许跨域
+// $allowDomains = [ /* */ ];
+//
+// // 如果请求来源在允许列表中,直接使用该源
+// if (in_array($origin, $allowDomains)) {
+// $allowOrigin = $origin;
+// }
+// }
+//
+// // 设置允许的请求头信息
+// $allowHeaders = [
+// 'Authorization', 'Content-Type', 'If-Match', 'If-Modified-Since',
+// 'If-None-Match', 'If-Unmodified-Since', 'X-Requested-With',
+// 'X-Token', 'X-Api-Token', 'Accept', 'Origin'
+// ];
+//
+// $response = $next($request);
+//
+// // 添加跨域响应头
+// $response->header([
+// 'Access-Control-Allow-Origin' => $allowOrigin,
+// 'Access-Control-Allow-Headers' => implode(', ', $allowHeaders),
+// 'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS',
+// 'Access-Control-Allow-Credentials' => 'true',
+// 'Access-Control-Max-Age' => '86400',
+// ]);
+//
+// // 对于预检请求,直接返回成功响应
+// if ($request->method(true) == 'OPTIONS') {
+// return response()->code(200);
+// }
+//
+// return $response;
+ }
+}
\ No newline at end of file
diff --git a/application/common/middleware/jwt.php b/application/common/middleware/jwt.php
new file mode 100644
index 0000000..09331b1
--- /dev/null
+++ b/application/common/middleware/jwt.php
@@ -0,0 +1,49 @@
+ 401,
+ 'msg' => '未授权访问,缺少有效的身份凭证',
+ 'data' => null
+ ])->header(['Content-Type' => 'application/json; charset=utf-8']);
+ }
+
+ $payload = JwtUtil::verifyToken($token);
+ if (!$payload) {
+ return json([
+ 'code' => 401,
+ 'msg' => '授权已过期或无效',
+ 'data' => null
+ ])->header(['Content-Type' => 'application/json; charset=utf-8']);
+ }
+
+ // 将用户信息附加到请求中
+ $request->userInfo = $payload;
+
+ // 写入日志
+ Log::info('JWT认证通过', ['user_id' => $payload['id'] ?? 0, 'username' => $payload['username'] ?? '']);
+
+ return $next($request);
+ }
+}
\ No newline at end of file
diff --git a/application/common/model/Administrator.php b/application/common/model/Administrator.php
new file mode 100644
index 0000000..9a2ef17
--- /dev/null
+++ b/application/common/model/Administrator.php
@@ -0,0 +1,31 @@
+allowField(true)->save($data);
+
+ return $log->id;
+ }
+}
\ No newline at end of file
diff --git a/application/common/model/DeviceTaskconf.php b/application/common/model/DeviceTaskconf.php
new file mode 100644
index 0000000..a4f2c68
--- /dev/null
+++ b/application/common/model/DeviceTaskconf.php
@@ -0,0 +1,24 @@
+whereIn('deviceId', $deviceIds)
+ ->group('deviceId')
+ ->select()
+ ->toArray();
+ }
+}
\ No newline at end of file
diff --git a/application/common/model/Menu.php b/application/common/model/Menu.php
new file mode 100644
index 0000000..acd26dc
--- /dev/null
+++ b/application/common/model/Menu.php
@@ -0,0 +1,17 @@
+ 'integer',
+ 'baseScore' => 'integer',
+ 'baseScoreCalculated' => 'integer',
+ 'baseInfoScore' => 'integer',
+ 'friendCountScore' => 'integer',
+ 'friendCount' => 'integer',
+ 'dynamicScore' => 'integer',
+ 'frequentCount' => 'integer',
+ 'frequentPenalty' => 'integer',
+ 'consecutiveNoFrequentDays' => 'integer',
+ 'noFrequentBonus' => 'integer',
+ 'banPenalty' => 'integer',
+ 'healthScore' => 'integer',
+ 'maxAddFriendPerDay' => 'integer',
+ 'isModifiedAlias' => 'integer',
+ 'isBanned' => 'integer',
+ 'lastFrequentTime' => 'integer',
+ 'lastNoFrequentTime' => 'integer',
+ 'baseScoreCalcTime' => 'integer',
+ 'createTime' => 'integer',
+ 'updateTime' => 'integer',
+ ];
+}
+
diff --git a/application/common/model/WechatCustomer.php b/application/common/model/WechatCustomer.php
new file mode 100644
index 0000000..c467338
--- /dev/null
+++ b/application/common/model/WechatCustomer.php
@@ -0,0 +1,21 @@
+where('phone', $account)->whereOr('account', $account);
+ })
+ ->where(function ($query) use ($typeId) {
+ $query->where('status', 1)->where('typeId', $typeId);
+ })->find();
+
+ return $user;
+ }
+
+ /**
+ * 获取用户信息
+ *
+ * @param string $account 账号(手机号)
+ * @param string $password 密码(可能是加密后的)
+ * @param int $typeId 身份信息
+ * @return array|null
+ */
+ protected function getUser(string $account, string $password, int $typeId): array
+ {
+ $user = $this->getUserProfileWithAccountAndType($account, $typeId);
+
+ if (!$user) {
+ throw new \Exception('用户不存在或已禁用', 403);
+ }
+
+ if ($user->passwordMd5 !== md5($password)) {
+ throw new \Exception('账号或密码错误', 403);
+ }
+
+ return $user->toArray();
+ }
+
+ /**
+ * 构造函数
+ */
+ public function __construct()
+ {
+ $this->smsService = new SmsService();
+ }
+
+ /**
+ * 用户登录
+ *
+ * @param string $account 账号(手机号)
+ * @param string $password 密码(可能是加密后的)
+ * @param string $ip 登录IP
+ * @return array
+ * @throws \Exception
+ */
+ public function login(string $account, string $password, int $typeId, string $ip)
+ {
+ // 获取用户信息
+ $member = $this->getUser($account, $password, $typeId);
+
+ // 生成JWT令牌
+ $token = JwtUtil::createToken($user, self::TOKEN_EXPIRE);
+ $token_expired = time() + self::TOKEN_EXPIRE;
+
+ return compact('member', 'token', 'token_expired');
+ }
+
+ /**
+ * 手机号验证码登录
+ *
+ * @param string $account 手机号
+ * @param string $code 验证码(可能是加密后的)
+ * @param string $ip 登录IP
+ * @param bool $isEncrypted 验证码是否已加密
+ * @return array
+ * @throws \Exception
+ */
+ public function mobileLogin($account, $code, $ip, $isEncrypted = false)
+ {
+ // 验证验证码
+ if (!$this->smsService->verifyCode($account, $code, 'login', $isEncrypted)) {
+ Log::info('验证码验证失败', ['account' => $account, 'ip' => $ip, 'is_encrypted' => $isEncrypted]);
+ throw new \Exception('验证码错误或已过期', 404);
+ }
+
+ // 获取用户信息
+ $user = User::getUserByMobile($account);
+ if (empty($user)) {
+ Log::info('用户不存在', ['account' => $account, 'ip' => $ip]);
+ throw new \Exception('用户不存在', 404);
+ }
+
+ // 生成JWT令牌
+ $token = JwtUtil::createToken($user, self::TOKEN_EXPIRE);
+ $expireTime = time() + self::TOKEN_EXPIRE;
+
+ // 记录登录成功
+ Log::info('手机号登录成功', ['account' => $account, 'ip' => $ip]);
+
+ return [
+ 'token' => $token,
+ 'token_expired' => $expireTime,
+ 'member' => $user
+ ];
+ }
+
+ /**
+ * 发送登录验证码
+ *
+ * @param string $account 手机号
+ * @param string $type 验证码类型
+ * @return array
+ * @throws \Exception
+ */
+ public function sendLoginCode($account, $type)
+ {
+ return $this->smsService->sendCode($account, $type);
+ }
+
+ /**
+ * 获取用户信息
+ *
+ * @param array $userInfo JWT中的用户信息
+ * @return array
+ * @throws \Exception
+ */
+ public function getUserInfo($userInfo)
+ {
+ if (empty($userInfo)) {
+ throw new \Exception('获取用户信息失败');
+ }
+
+ // 移除不需要返回的字段
+ unset($userInfo['exp']);
+ unset($userInfo['iat']);
+
+ return $userInfo;
+ }
+
+ /**
+ * 刷新令牌
+ *
+ * @param array $userInfo JWT中的用户信息
+ * @return array
+ * @throws \Exception
+ */
+ public function refreshToken($userInfo)
+ {
+ if (empty($userInfo)) {
+ throw new \Exception('刷新令牌失败');
+ }
+
+ // 移除过期时间信息
+ unset($userInfo['exp']);
+ unset($userInfo['iat']);
+
+ // 生成新令牌
+ $token = JwtUtil::createToken($userInfo, self::TOKEN_EXPIRE);
+ $expireTime = time() + self::TOKEN_EXPIRE;
+
+ return [
+ 'token' => $token,
+ 'token_expired' => $expireTime
+ ];
+ }
+
+ /**
+ * 获取系统授权信息,使用缓存存储10分钟
+ *
+ * @param bool $useCache 是否使用缓存
+ * @return string
+ */
+ public static function getSystemAuthorization($useCache = true)
+ {
+ // 定义缓存键名
+ $cacheKey = 'system_authorization_token';
+
+ // 尝试从缓存获取授权信息
+ $authorization = Cache::get($cacheKey);
+ //$authorization = '';
+ // 如果缓存中没有或已过期,则重新获取
+ if (empty($authorization) || !$useCache) {
+ try {
+ // 从环境变量中获取API用户名和密码
+ $username = Env::get('api.username', '');
+ $password = Env::get('api.password', '');
+
+ if (empty($username) || empty($password)) {
+ Log::error('缺少API用户名或密码配置');
+ return '';
+ }
+
+ // 构建登录参数
+ $params = [
+ 'grant_type' => 'password',
+ 'username' => $username,
+ 'password' => $password
+ ];
+
+ // 获取API基础URL
+ $baseUrl = Env::get('api.wechat_url', '');
+ if (empty($baseUrl)) {
+ Log::error('缺少API基础URL配置');
+ return '';
+ }
+
+ // 调用登录接口获取token
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, '', 'plain');
+ $result = requestCurl($baseUrl . 'token', $params, 'POST', $header);
+ $result_array = handleApiResponse($result);
+
+ if (isset($result_array['access_token']) && !empty($result_array['access_token'])) {
+ $authorization = $result_array['access_token'];
+
+ // 存入缓存,有效期10分钟(600秒)
+ Cache::set($cacheKey, $authorization, 600);
+ Cache::set('system_refresh_token', $result_array['refresh_token'], 600);
+
+ Log::info('已重新获取系统授权信息并缓存');
+ return $authorization;
+ } else {
+ Log::error('获取系统授权信息失败:' . ($response['message'] ?? '未知错误'));
+ return '';
+ }
+ } catch (\Exception $e) {
+ Log::error('获取系统授权信息异常:' . $e->getMessage());
+ return '';
+ }
+ }
+
+ return $authorization;
+ }
+}
\ No newline at end of file
diff --git a/application/common/service/ClassTableService.php b/application/common/service/ClassTableService.php
new file mode 100644
index 0000000..365e882
--- /dev/null
+++ b/application/common/service/ClassTableService.php
@@ -0,0 +1,82 @@
+app = $app;
+ $this->classTable = ClassTable::getSelfInstance();
+ }
+
+ /**
+ * 绑定实例或类到容器
+ * @param string|array $alias
+ * @param mixed $instance
+ * @param string|null $tag
+ */
+ public function bind($alias, $instance = null, string $tag = null)
+ {
+ $this->classTable->bind($alias, $instance, $tag);
+ return $this;
+ }
+
+ /**
+ * 获取实例
+ * @param string|object $class
+ * @param array $parameters
+ * @return object
+ */
+ public function getInstance($class, ...$parameters)
+ {
+ return $this->classTable->getInstance($class, ...$parameters);
+ }
+
+ /**
+ * 获取共享实例
+ * @param string $alias
+ * @param array $parameters
+ * @return object|null
+ */
+ public function getShared($alias, array $parameters = [])
+ {
+ return $this->classTable->getShared($alias, $parameters);
+ }
+
+ /**
+ * 根据标签获取类
+ * @param string $tag
+ * @return array|null
+ */
+ public function getClassByTag(string $tag)
+ {
+ return $this->classTable->getClassByTag($tag);
+ }
+
+ /**
+ * 检查别名是否存在
+ * @param string $alias
+ * @return bool
+ */
+ public function has(string $alias)
+ {
+ return $this->classTable->has($alias);
+ }
+
+ /**
+ * 复制实例
+ * @param mixed $class
+ * @param string|null $name
+ * @return object
+ */
+ public function copy($class, string $name = null)
+ {
+ return $this->classTable->copy($class, $name);
+ }
+}
\ No newline at end of file
diff --git a/application/common/service/SmsService.php b/application/common/service/SmsService.php
new file mode 100644
index 0000000..e0552f1
--- /dev/null
+++ b/application/common/service/SmsService.php
@@ -0,0 +1,203 @@
+checkSendLimit($mobile, $type);
+
+ // 生成验证码
+ $code = $this->generateCode();
+
+ // 缓存验证码
+ $this->saveCode($mobile, $code, $type);
+
+ // 发送验证码(实际项目中对接短信平台)
+ $this->doSend($mobile, $code, $type);
+
+ // 记录日志
+ Log::info('发送验证码', [
+ 'mobile' => $mobile,
+ 'type' => $type,
+ 'code' => $code
+ ]);
+
+ return [
+ 'mobile' => $mobile,
+ 'expire' => self::CODE_EXPIRE,
+ // 测试环境返回验证码,生产环境不应返回
+ 'code' => $code
+ ];
+ }
+
+ /**
+ * 验证验证码
+ * @param string $mobile 手机号
+ * @param string $code 验证码(可能是加密后的)
+ * @param string $type 验证码类型
+ * @param bool $isEncrypted 验证码是否已加密
+ * @return bool
+ */
+ public function verifyCode($mobile, $code, $type, $isEncrypted = false)
+ {
+ $cacheKey = $this->getCodeCacheKey($mobile, $type);
+ $cacheCode = Cache::get($cacheKey);
+
+ if (!$cacheCode) {
+ Log::info('验证码不存在或已过期', [
+ 'mobile' => $mobile,
+ 'type' => $type
+ ]);
+ return false;
+ }
+
+ // 验证码是否匹配
+ $isValid = false;
+
+ if ($isEncrypted) {
+ // 前端已加密,需要对缓存中的验证码进行相同的加密处理
+ $encryptedCacheCode = $this->encryptCode($cacheCode);
+ $isValid = hash_equals($encryptedCacheCode, $code);
+
+ // 记录日志
+ Log::info('加密验证码验证', [
+ 'mobile' => $mobile,
+ 'cache_code' => $cacheCode,
+ 'encrypted_cache_code' => $encryptedCacheCode,
+ 'input_code' => $code,
+ 'is_valid' => $isValid
+ ]);
+ } else {
+ // 未加密,直接比较
+ $isValid = ($cacheCode === $code);
+
+ // 记录日志
+ Log::info('明文验证码验证', [
+ 'mobile' => $mobile,
+ 'cache_code' => $cacheCode,
+ 'input_code' => $code,
+ 'is_valid' => $isValid
+ ]);
+ }
+
+ // 验证成功后删除缓存
+ if ($isValid) {
+ Cache::rm($cacheKey);
+ }
+
+ return $isValid;
+ }
+
+ /**
+ * 检查发送频率限制
+ * @param string $mobile 手机号
+ * @param string $type 验证码类型
+ * @throws \Exception
+ */
+ protected function checkSendLimit($mobile, $type)
+ {
+ $cacheKey = $this->getCodeCacheKey($mobile, $type);
+
+ // 检查是否存在未过期的验证码
+ if (Cache::has($cacheKey)) {
+ throw new \Exception('验证码已发送,请稍后再试');
+ }
+
+ // 检查当日发送次数限制
+ $limitKey = "sms_limit:{$mobile}:" . date('Ymd');
+ $sendCount = Cache::get($limitKey, 0);
+
+ if ($sendCount >= 10) {
+ throw new \Exception('今日发送次数已达上限');
+ }
+
+ // 更新发送次数
+ Cache::set($limitKey, $sendCount + 1, 86400);
+ }
+
+ /**
+ * 生成随机验证码
+ * @return string
+ */
+ protected function generateCode()
+ {
+ // 生成4位数字验证码
+ return sprintf("%0" . self::CODE_LENGTH . "d", mt_rand(0, pow(10, self::CODE_LENGTH) - 1));
+ }
+
+ /**
+ * 保存验证码到缓存
+ * @param string $mobile 手机号
+ * @param string $code 验证码
+ * @param string $type 验证码类型
+ */
+ protected function saveCode($mobile, $code, $type)
+ {
+ $cacheKey = $this->getCodeCacheKey($mobile, $type);
+ Cache::set($cacheKey, $code, self::CODE_EXPIRE);
+ }
+
+ /**
+ * 执行发送验证码
+ * @param string $mobile 手机号
+ * @param string $code 验证码
+ * @param string $type 验证码类型
+ * @return bool
+ */
+ protected function doSend($mobile, $code, $type)
+ {
+ // 实际项目中对接短信平台API
+ // 这里仅做模拟,返回成功
+ return true;
+ }
+
+ /**
+ * 获取验证码缓存键名
+ * @param string $mobile 手机号
+ * @param string $type 验证码类型
+ * @return string
+ */
+ protected function getCodeCacheKey($mobile, $type)
+ {
+ return "sms_code:{$mobile}:{$type}";
+ }
+
+ /**
+ * 加密验证码
+ * 使用与前端相同的加密算法
+ * @param string $code 原始验证码
+ * @return string 加密后的验证码
+ */
+ protected function encryptCode($code)
+ {
+ // 使用与前端相同的加密算法
+ $salt = 'yishi_salt_2024'; // 与前端相同的盐值
+ return hash('sha256', $code . $salt);
+ }
+}
\ No newline at end of file
diff --git a/application/common/service/WechatAccountHealthScoreService.php b/application/common/service/WechatAccountHealthScoreService.php
new file mode 100644
index 0000000..22f82e4
--- /dev/null
+++ b/application/common/service/WechatAccountHealthScoreService.php
@@ -0,0 +1,1506 @@
+where('id', $accountId)
+ ->find();
+
+ // 减少不必要的日志记录
+ }
+
+ if (empty($accountData)) {
+ $errorMsg = "账号不存在:{$accountId}";
+ Log::error($errorMsg);
+ throw new Exception($errorMsg);
+ }
+
+ $wechatId = $accountData['wechatId'] ?? '';
+ if (empty($wechatId)) {
+ $errorMsg = "账号wechatId为空:{$accountId}";
+ Log::error($errorMsg);
+ throw new Exception($errorMsg);
+ }
+
+ // 减少不必要的日志记录
+
+ // 获取或创建评分记录
+ $scoreRecord = $this->getOrCreateScoreRecord($accountId, $wechatId);
+ $scoreSnapshotBefore = $this->buildScoreSnapshotForLogging($scoreRecord);
+ // 减少不必要的日志记录
+
+ // 计算基础分(只计算一次,除非强制重新计算)
+ if (!$scoreRecord['baseScoreCalculated'] || $forceRecalculateBase) {
+
+ $baseScoreData = $this->calculateBaseScore($accountData, $scoreRecord);
+ $this->updateBaseScore($accountId, $baseScoreData);
+
+ // 减少不必要的日志记录
+
+ // 重新获取记录以获取最新数据
+ $scoreRecord = $this->getScoreRecord($accountId);
+ }
+
+ // 计算动态分(每次都要重新计算)
+ $dynamicScoreData = $this->calculateDynamicScore($accountData, $scoreRecord);
+
+ // 计算总分
+ $baseScore = $scoreRecord['baseScore'];
+ $dynamicScore = $dynamicScoreData['total'];
+ $healthScore = $baseScore + $dynamicScore;
+
+ // 确保健康分在合理范围内(0-100)
+ $healthScore = max(0, min(100, $healthScore));
+
+ // 计算每日最大加人次数
+ $maxAddFriendPerDay = $this->getMaxAddFriendPerDay($healthScore);
+
+ // 更新评分记录
+ $updateData = [
+ 'dynamicScore' => $dynamicScore,
+ 'frequentPenalty' => $dynamicScoreData['frequentPenalty'],
+ 'noFrequentBonus' => $dynamicScoreData['noFrequentBonus'],
+ 'banPenalty' => $dynamicScoreData['banPenalty'],
+ 'lastFrequentTime' => $dynamicScoreData['lastFrequentTime'],
+ 'frequentCount' => $dynamicScoreData['frequentCount'],
+ 'lastNoFrequentTime' => $dynamicScoreData['lastNoFrequentTime'],
+ 'consecutiveNoFrequentDays' => $dynamicScoreData['consecutiveNoFrequentDays'],
+ 'isBanned' => $dynamicScoreData['isBanned'],
+ 'lastBanTime' => $dynamicScoreData['lastBanTime'],
+ 'healthScore' => $healthScore,
+ 'maxAddFriendPerDay' => $maxAddFriendPerDay,
+ 'updateTime' => time()
+ ];
+
+ $updateResult = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)
+ ->where('accountId', $accountId)
+ ->update($updateData);
+
+ // 更新成功后,清除缓存
+ if ($updateResult !== false) {
+ $this->logScoreChangesIfNeeded(
+ $accountId,
+ $wechatId,
+ $scoreSnapshotBefore,
+ [
+ 'frequentPenalty' => $dynamicScoreData['frequentPenalty'],
+ 'banPenalty' => $dynamicScoreData['banPenalty'],
+ 'noFrequentBonus' => $dynamicScoreData['noFrequentBonus'],
+ 'dynamicScore' => $dynamicScore,
+ 'healthScore' => $healthScore
+ ],
+ $dynamicScoreData
+ );
+ $this->clearScoreCache($accountId);
+ }
+
+ $result = [
+ 'accountId' => $accountId,
+ 'wechatId' => $wechatId,
+ 'healthScore' => $healthScore,
+ 'baseScore' => $baseScore,
+ 'baseInfoScore' => $scoreRecord['baseInfoScore'],
+ 'friendCountScore' => $scoreRecord['friendCountScore'],
+ 'dynamicScore' => $dynamicScore,
+ 'frequentPenalty' => $dynamicScoreData['frequentPenalty'],
+ 'noFrequentBonus' => $dynamicScoreData['noFrequentBonus'],
+ 'banPenalty' => $dynamicScoreData['banPenalty'],
+ 'maxAddFriendPerDay' => $maxAddFriendPerDay
+ ];
+
+ // 减少不必要的日志记录
+ return $result;
+
+ } catch (\PDOException $e) {
+ // 数据库异常
+ $errorMsg = "数据库操作失败,accountId: {$accountId}, 错误: " . $e->getMessage();
+ Log::error($errorMsg);
+ throw new Exception($errorMsg, $e->getCode(), $e);
+ } catch (\Throwable $e) {
+ // 其他所有异常
+ $errorMsg = "计算健康分失败,accountId: {$accountId}, 错误: " . $e->getMessage();
+ Log::error($errorMsg);
+ throw new Exception($errorMsg, $e->getCode(), $e);
+ }
+ }
+
+ /**
+ * 获取或创建评分记录
+ * 优化:使用事务和锁避免并发问题,减少重复查询
+ *
+ * @param int $accountId 账号ID
+ * @param string $wechatId 微信ID
+ * @return array 评分记录
+ */
+ private function getOrCreateScoreRecord($accountId, $wechatId)
+ {
+ // 尝试获取现有记录
+ $record = $this->getScoreRecord($accountId);
+
+ // 如果记录不存在,创建新记录
+ if (empty($record)) {
+ // 使用事务避免并发问题
+ Db::startTrans();
+ try {
+ // 再次检查记录是否存在(避免并发问题)
+ $record = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)
+ ->where('accountId', $accountId)
+ ->lock(true) // 加锁防止并发插入
+ ->find();
+
+ if (empty($record)) {
+ Log::info("为账号 {$accountId} 创建新的评分记录");
+
+ // 检查表中是否存在lastBanTime字段
+ $this->ensureScoreTableFields();
+
+ // 创建新记录
+ $data = [
+ 'accountId' => $accountId,
+ 'wechatId' => $wechatId,
+ 'baseScore' => 0,
+ 'baseScoreCalculated' => 0,
+ 'baseInfoScore' => 0,
+ 'friendCountScore' => 0,
+ 'dynamicScore' => 0,
+ 'frequentCount' => 0,
+ 'consecutiveNoFrequentDays' => 0,
+ 'healthScore' => 0,
+ 'maxAddFriendPerDay' => 0,
+ 'lastBanTime' => null,
+ 'createTime' => time(),
+ 'updateTime' => time()
+ ];
+
+ Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)->insert($data);
+ $record = $data;
+ }
+
+ Db::commit();
+ } catch (\Exception $e) {
+ Db::rollback();
+ Log::error("创建评分记录失败: " . $e->getMessage());
+ throw $e;
+ }
+ }
+
+ return $record;
+ }
+
+ /**
+ * 确保评分表有所需字段
+ * 优化:使用静态变量缓存结果,避免重复检查
+ *
+ * @return void
+ */
+ private function ensureScoreTableFields()
+ {
+ // 使用静态变量缓存检查结果,避免重复检查
+ static $fieldsChecked = false;
+
+ if ($fieldsChecked) {
+ return;
+ }
+
+ try {
+ // 检查表中是否存在lastBanTime字段
+ $hasLastBanTimeField = false;
+ $tableFields = Db::query("SHOW COLUMNS FROM " . self::TABLE_WECHAT_ACCOUNT_SCORE);
+ foreach ($tableFields as $field) {
+ if ($field['Field'] == 'lastBanTime') {
+ $hasLastBanTimeField = true;
+ break;
+ }
+ }
+
+ // 如果字段不存在,添加字段
+ if (!$hasLastBanTimeField) {
+ Log::info("添加lastBanTime字段到" . self::TABLE_WECHAT_ACCOUNT_SCORE . "表");
+ Db::execute("ALTER TABLE " . self::TABLE_WECHAT_ACCOUNT_SCORE . " ADD COLUMN lastBanTime INT(11) DEFAULT NULL COMMENT '最后一次封号时间'");
+ }
+
+ $fieldsChecked = true;
+ } catch (\Exception $e) {
+ Log::error("检查或添加字段失败: " . $e->getMessage());
+ // 出错时不影响后续逻辑,继续执行
+ }
+ }
+
+ /**
+ * 获取评分记录
+ * 优化:使用多级缓存策略,提高缓存命中率
+ *
+ * @param int $accountId 账号ID
+ * @param bool $useCache 是否使用缓存(默认true)
+ * @return array 评分记录,如果不存在则返回空数组
+ */
+ private function getScoreRecord($accountId, $useCache = true)
+ {
+ // 生成缓存键
+ $cacheKey = self::CACHE_PREFIX . 'score:' . $accountId;
+
+ // 如果使用缓存且缓存存在,则直接返回缓存数据
+ if ($useCache && Cache::has($cacheKey)) {
+ $cachedData = Cache::get($cacheKey);
+ // 减少日志记录,提高性能
+ return $cachedData ?: [];
+ }
+
+ // 从数据库获取记录
+ $record = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)
+ ->where('accountId', $accountId)
+ ->find();
+
+ // 如果记录存在且使用缓存,则缓存记录
+ if ($record && $useCache) {
+ // 根据数据更新频率设置不同的缓存时间
+ // 如果记录最近更新过(1小时内),使用短期缓存
+ $updateTime = $record['updateTime'] ?? 0;
+ $cacheTime = (time() - $updateTime < 3600) ? self::CACHE_TTL_SHORT : self::CACHE_TTL;
+
+ Cache::set($cacheKey, $record, $cacheTime);
+ Log::debug("缓存评分记录,accountId: {$accountId}, 缓存时间: {$cacheTime}秒");
+ }
+
+ return $record ?: [];
+ }
+
+ /**
+ * 计算基础分(只计算一次)
+ * 基础分 = 默认60分 + 基础信息分(10分) + 好友数量分(3-12分)
+ *
+ * @param array $accountData 账号数据
+ * @param array $scoreRecord 现有评分记录
+ * @return array 基础分数据
+ */
+ private function calculateBaseScore($accountData, $scoreRecord = [])
+ {
+ $baseScore = self::DEFAULT_BASE_SCORE;
+
+ // 基础信息分(已修改微信号得10分)
+ $baseInfoScore = $this->getBaseInfoScore($accountData);
+ $baseScore += $baseInfoScore;
+
+ // 好友数量分(特殊处理:使用快照值,避免同步问题)
+ $friendCountScore = 0;
+ $friendCount = 0;
+ $friendCountSource = 'manual';
+
+ // 如果已有评分记录且好友数量分已计算,使用历史值
+ if (!empty($scoreRecord['friendCountScore']) && $scoreRecord['friendCountScore'] > 0) {
+ $friendCountScore = $scoreRecord['friendCountScore'];
+ $friendCount = $scoreRecord['friendCount'] ?? 0;
+ $friendCountSource = $scoreRecord['friendCountSource'] ?? 'manual';
+ } else {
+ // 首次计算:使用当前好友数量,但标记为手动计算
+ $totalFriend = $accountData['totalFriend'] ?? 0;
+ $friendCountScore = $this->getFriendCountScore($totalFriend);
+ $friendCount = $totalFriend;
+ $friendCountSource = 'manual';
+ }
+
+ $baseScore += $friendCountScore;
+
+ // 检查是否已修改微信号
+ $isModifiedAlias = $this->checkIsModifiedAlias($accountData);
+
+ return [
+ 'baseScore' => $baseScore,
+ 'baseInfoScore' => $baseInfoScore,
+ 'friendCountScore' => $friendCountScore,
+ 'friendCount' => $friendCount,
+ 'friendCountSource' => $friendCountSource,
+ 'isModifiedAlias' => $isModifiedAlias ? 1 : 0,
+ 'baseScoreCalculated' => 1,
+ 'baseScoreCalcTime' => time()
+ ];
+ }
+
+ /**
+ * 更新基础分
+ *
+ * @param int $accountId 账号ID
+ * @param array $baseScoreData 基础分数据
+ * @return bool 更新是否成功
+ */
+ private function updateBaseScore($accountId, $baseScoreData)
+ {
+ try {
+ $result = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)
+ ->where('accountId', $accountId)
+ ->update($baseScoreData);
+
+ // 减少不必要的日志记录
+
+ // 更新成功后,清除缓存
+ if ($result !== false) {
+ $this->clearScoreCache($accountId);
+ }
+
+ return $result !== false;
+ } catch (Exception $e) {
+ Log::error("更新基础分失败,accountId: {$accountId}, 错误: " . $e->getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * 清除评分记录缓存
+ *
+ * @param int $accountId 账号ID
+ * @return bool 是否成功清除缓存
+ */
+ private function clearScoreCache($accountId)
+ {
+ $cacheKey = self::CACHE_PREFIX . 'score:' . $accountId;
+ $result = Cache::rm($cacheKey);
+ // 减少不必要的日志记录
+ return $result;
+ }
+
+ /**
+ * 获取基础信息分
+ * 已修改微信号:10分
+ *
+ * @param array $accountData 账号数据
+ * @return int 基础信息分
+ */
+ private function getBaseInfoScore($accountData)
+ {
+ if ($this->checkIsModifiedAlias($accountData)) {
+ return self::BASE_INFO_SCORE;
+ }
+ return 0;
+ }
+
+ /**
+ * 检查是否已修改微信号
+ * 判断标准:wechatId和alias不一致且都不为空,则认为已修改微信号
+ * 注意:这里只用于评分,不修复数据
+ *
+ * @param array $accountData 账号数据
+ * @return bool
+ */
+ private function checkIsModifiedAlias($accountData)
+ {
+ $wechatId = trim($accountData['wechatId'] ?? '');
+ $alias = trim($accountData['alias'] ?? '');
+
+ // 如果wechatId和alias不一致且都不为空,则认为已修改微信号(用于评分)
+ if (!empty($wechatId) && !empty($alias) && $wechatId !== $alias) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 获取好友数量分
+ * 根据好友数量区间得分(最高12分)
+ *
+ * @param int $totalFriend 总好友数
+ * @return int 好友数量分
+ */
+ private function getFriendCountScore($totalFriend)
+ {
+ if ($totalFriend <= 50) {
+ return self::FRIEND_COUNT_SCORE_0_50;
+ } elseif ($totalFriend <= 500) {
+ return self::FRIEND_COUNT_SCORE_51_500;
+ } elseif ($totalFriend <= 3000) {
+ return self::FRIEND_COUNT_SCORE_501_3000;
+ } else {
+ return self::FRIEND_COUNT_SCORE_3001_PLUS;
+ }
+ }
+
+ /**
+ * 手动更新好友数量分(用于处理同步问题)
+ *
+ * @param int $accountId 账号ID
+ * @param int $friendCount 好友数量
+ * @param string $source 来源(manual=手动,sync=同步)
+ * @return bool 更新是否成功
+ * @throws Exception 如果参数无效或更新过程中出现错误
+ */
+ public function updateFriendCountScore($accountId, $friendCount, $source = 'manual')
+ {
+ // 参数验证
+ if (empty($accountId) || !is_numeric($accountId)) {
+ $errorMsg = "无效的账号ID: " . (is_scalar($accountId) ? $accountId : gettype($accountId));
+ Log::error($errorMsg);
+ throw new Exception($errorMsg);
+ }
+
+ if (!is_numeric($friendCount) || $friendCount < 0) {
+ $errorMsg = "无效的好友数量: {$friendCount}";
+ Log::error($errorMsg);
+ throw new Exception($errorMsg);
+ }
+
+ if (!in_array($source, ['manual', 'sync'])) {
+ $errorMsg = "无效的来源: {$source},必须是 'manual' 或 'sync'";
+ Log::error($errorMsg);
+ throw new Exception($errorMsg);
+ }
+
+ try {
+ $scoreRecord = $this->getScoreRecord($accountId);
+
+ // 如果基础分已计算,不允许修改好友数量分(除非是手动更新)
+ if (!empty($scoreRecord['baseScoreCalculated']) && $source === 'sync') {
+ // 同步数据不允许修改已计算的基础分
+ Log::warning("同步数据不允许修改已计算的基础分,accountId: {$accountId}");
+ return false;
+ }
+ }
+ catch (\Exception $e) {
+ $errorMsg = "获取评分记录失败,accountId: {$accountId}, 错误: " . $e->getMessage();
+ Log::error($errorMsg);
+ throw new Exception($errorMsg, $e->getCode(), $e);
+ }
+
+ $friendCountScore = $this->getFriendCountScore($friendCount);
+
+ // 重新计算基础分
+ $oldBaseScore = $scoreRecord['baseScore'] ?? self::DEFAULT_BASE_SCORE;
+ $oldFriendCountScore = $scoreRecord['friendCountScore'] ?? 0;
+ $baseInfoScore = $scoreRecord['baseInfoScore'] ?? 0;
+
+ $newBaseScore = self::DEFAULT_BASE_SCORE + $baseInfoScore + $friendCountScore;
+
+ $updateData = [
+ 'friendCountScore' => $friendCountScore,
+ 'friendCount' => $friendCount,
+ 'friendCountSource' => $source,
+ 'baseScore' => $newBaseScore,
+ 'updateTime' => time()
+ ];
+
+ // 如果基础分已计算,需要更新总分
+ if (!empty($scoreRecord['baseScoreCalculated'])) {
+ $dynamicScore = $scoreRecord['dynamicScore'] ?? 0;
+ $healthScore = $newBaseScore + $dynamicScore;
+ $healthScore = max(0, min(100, $healthScore));
+ $updateData['healthScore'] = $healthScore;
+ $updateData['maxAddFriendPerDay'] = $this->getMaxAddFriendPerDay($healthScore);
+ }
+
+ try {
+ $result = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)
+ ->where('accountId', $accountId)
+ ->update($updateData);
+
+ // 更新成功后,清除缓存
+ if ($result !== false) {
+ $this->clearScoreCache($accountId);
+ $this->clearHealthScoreCache($accountId);
+ Log::info("更新好友数量分成功,accountId: {$accountId}, friendCount: {$friendCount}, source: {$source}");
+ } else {
+ Log::warning("更新好友数量分失败,accountId: {$accountId}, friendCount: {$friendCount}, source: {$source}");
+ }
+
+ return $result !== false;
+ } catch (\PDOException $e) {
+ $errorMsg = "数据库操作失败,accountId: {$accountId}, 错误: " . $e->getMessage();
+ Log::error($errorMsg);
+ throw new Exception($errorMsg, $e->getCode(), $e);
+ } catch (\Throwable $e) {
+ $errorMsg = "更新好友数量分失败,accountId: {$accountId}, 错误: " . $e->getMessage();
+ Log::error($errorMsg);
+ throw new Exception($errorMsg, $e->getCode(), $e);
+ }
+ }
+
+ /**
+ * 计算动态分
+ * 动态分 = 扣分 + 加分
+ * 如果添加好友记录表没有记录,则动态分为0
+ *
+ * @param array $accountData 账号数据
+ * @param array $scoreRecord 现有评分记录
+ * @return array 动态分数据
+ */
+ private function calculateDynamicScore($accountData, $scoreRecord)
+ {
+ $accountId = $accountData['id'] ?? 0;
+ $wechatId = $accountData['wechatId'] ?? '';
+
+ Log::debug("开始计算动态分,accountId: {$accountId}, wechatId: {$wechatId}");
+
+ $result = [
+ 'total' => 0,
+ 'frequentPenalty' => 0,
+ 'noFrequentBonus' => 0,
+ 'banPenalty' => 0,
+ 'lastFrequentTime' => null,
+ 'frequentCount' => 0,
+ 'lastNoFrequentTime' => null,
+ 'consecutiveNoFrequentDays' => 0,
+ 'isBanned' => 0,
+ 'lastBanTime' => null,
+ 'frequentTaskIds' => [],
+ 'banMessageId' => null
+ ];
+
+ if (empty($accountId) || empty($wechatId)) {
+ Log::warning("计算动态分失败: accountId或wechatId为空");
+ return $result;
+ }
+
+ // 不再使用30天限制
+
+ // 检查添加好友记录表是否有记录,如果没有记录则动态分为0
+ // 使用EXISTS子查询优化性能,只检查是否存在记录,不需要计数
+ $hasFriendTask = Db::table(self::TABLE_FRIEND_TASK)
+ ->where('wechatAccountId', $accountId)
+ ->where(function($query) use ($wechatId) {
+ if (!empty($wechatId)) {
+ $query->where('wechatId', $wechatId);
+ }
+ })
+ ->value('id'); // 只获取ID,比count()更高效
+
+ // 如果添加好友记录表没有记录,则动态分为0
+ if (empty($hasFriendTask)) {
+ Log::info("账号没有添加好友记录,动态分为0,accountId: {$accountId}");
+ return $result;
+ }
+
+ Log::debug("账号有添加好友记录,继续计算动态分,accountId: {$accountId}");
+
+ // 继承现有数据
+ if (!empty($scoreRecord)) {
+ $result['lastFrequentTime'] = $scoreRecord['lastFrequentTime'] ?? null;
+ $result['frequentCount'] = $scoreRecord['frequentCount'] ?? 0;
+ $result['lastNoFrequentTime'] = $scoreRecord['lastNoFrequentTime'] ?? null;
+ $result['consecutiveNoFrequentDays'] = $scoreRecord['consecutiveNoFrequentDays'] ?? 0;
+ $result['frequentPenalty'] = $scoreRecord['frequentPenalty'] ?? 0;
+ $result['noFrequentBonus'] = $scoreRecord['noFrequentBonus'] ?? 0;
+ $result['banPenalty'] = $scoreRecord['banPenalty'] ?? 0;
+ $result['lastBanTime'] = $scoreRecord['lastBanTime'] ?? null;
+ }
+
+ // 1. 检查频繁记录(从s2_friend_task表查询,不限制时间)
+ $frequentData = $this->checkFrequentFromFriendTask($accountId, $wechatId, $scoreRecord);
+ $result['lastFrequentTime'] = $frequentData['lastFrequentTime'] ?? null;
+ $result['frequentCount'] = $frequentData['frequentCount'] ?? 0;
+ $result['frequentPenalty'] = $frequentData['frequentPenalty'] ?? 0;
+ $result['frequentTaskIds'] = $frequentData['taskIds'] ?? [];
+
+ // 2. 检查封号记录(从s2_wechat_message表查询,不限制时间)
+ $banData = $this->checkBannedFromMessage($accountId, $wechatId);
+ if (!empty($banData)) {
+ $result['isBanned'] = $banData['isBanned'];
+ $result['banPenalty'] = $banData['banPenalty'];
+ $result['lastBanTime'] = $banData['lastBanTime'];
+ $result['banMessageId'] = $banData['messageId'] ?? null;
+ }
+
+ // 3. 计算不频繁加分(基于频繁记录,反向参考频繁规则)
+ $noFrequentData = $this->calculateNoFrequentBonus($accountId, $wechatId, $frequentData);
+ $result['noFrequentBonus'] = $noFrequentData['bonus'] ?? 0;
+ $result['consecutiveNoFrequentDays'] = $noFrequentData['consecutiveDays'] ?? 0;
+ $result['lastNoFrequentTime'] = $noFrequentData['lastNoFrequentTime'] ?? null;
+
+ // 计算总分
+ $result['total'] = $result['frequentPenalty'] + $result['noFrequentBonus'] + $result['banPenalty'];
+
+ Log::debug("动态分计算结果,accountId: {$accountId}, frequentPenalty: {$result['frequentPenalty']}, " .
+ "noFrequentBonus: {$result['noFrequentBonus']}, banPenalty: {$result['banPenalty']}, " .
+ "total: {$result['total']}");
+
+ return $result;
+ }
+
+ /**
+ * 从s2_friend_task表检查频繁记录
+ * extra字段包含"操作过于频繁"即需要扣分
+ * 统计所有时间的数据(不限制30天)
+ * 每条记录只统计一次,使用is_counted字段标记
+ *
+ * @param int $accountId 账号ID
+ * @param string $wechatId 微信ID
+ * @param array $scoreRecord 现有评分记录
+ * @param int $thirtyDaysAgo 已废弃参数,保留是为了兼容性
+ * @return array|null
+ */
+ private function checkFrequentFromFriendTask($accountId, $wechatId, $scoreRecord, $thirtyDaysAgo = null)
+ {
+ // 不再使用30天限制
+
+ // 减少不必要的日志记录
+
+ // 查询包含"操作过于频繁"的记录(统计所有时间且未被统计过的记录)
+ // extra字段可能是文本或JSON格式,使用LIKE查询
+ // 优化查询:只查询必要的字段,减少数据传输量
+ // 添加is_counted条件,只查询未被统计过的记录
+ $frequentTasks = Db::table(self::TABLE_FRIEND_TASK)
+ ->where('wechatAccountId', $accountId)
+ ->where(function($query) use ($wechatId) {
+ if (!empty($wechatId)) {
+ $query->where('wechatId', $wechatId);
+ }
+ })
+ ->where(function($query) {
+ // 检查extra字段是否包含"操作过于频繁"(可能是文本或JSON)
+ $query->where('extra', 'like', '%操作过于频繁%')
+ ->whereOr('extra', 'like', '%"当前账号存在安全风险"%');
+ })
+ ->where(function($query) {
+ // 只查询未被统计过的记录
+ // 注意:需要兼容is_counted字段不存在的情况
+ $query->where('is_counted', 0)
+ ->whereOr('is_counted', null);
+ })
+ ->order('createTime', 'desc')
+ ->field('id, createTime, extra')
+ ->select();
+
+ // 获取最新的频繁时间
+ $latestFrequentTime = !empty($frequentTasks) ? $frequentTasks[0]['createTime'] : null;
+
+ // 计算频繁次数(统计近30天内包含"操作过于频繁"的记录)
+ $frequentCount = count($frequentTasks);
+
+ Log::info("找到 {$frequentCount} 条未统计的频繁记录,accountId: {$accountId}, wechatId: {$wechatId}");
+
+ // 标记这些记录为已统计
+ if (!empty($frequentTasks)) {
+ $taskIds = array_column($frequentTasks, 'id');
+ try {
+ // 检查表中是否存在is_counted字段
+ $hasIsCountedField = false;
+ $tableFields = Db::query("SHOW COLUMNS FROM " . self::TABLE_FRIEND_TASK);
+ foreach ($tableFields as $field) {
+ if ($field['Field'] == 'is_counted') {
+ $hasIsCountedField = true;
+ break;
+ }
+ }
+
+ // 如果字段不存在,添加字段
+ if (!$hasIsCountedField) {
+ Log::info("添加is_counted字段到" . self::TABLE_FRIEND_TASK . "表");
+ Db::execute("ALTER TABLE " . self::TABLE_FRIEND_TASK . " ADD COLUMN is_counted TINYINT(1) DEFAULT 0 COMMENT '是否已统计(0=未统计,1=已统计)'");
+ }
+
+ // 更新记录为已统计
+ Db::table(self::TABLE_FRIEND_TASK)
+ ->where('id', 'in', $taskIds)
+ ->update(['is_counted' => 1]);
+
+ // 减少不必要的日志记录
+ } catch (\Exception $e) {
+ Log::error("标记频繁记录失败: " . $e->getMessage());
+ // 出错时不影响后续逻辑,继续执行
+ }
+ }
+
+ // 如果30天内没有频繁记录,清除扣分
+ if (empty($frequentTasks)) {
+ return [
+ 'lastFrequentTime' => null,
+ 'frequentCount' => 0,
+ 'frequentPenalty' => 0,
+ 'taskIds' => []
+ ];
+ }
+
+ // 根据30天内的频繁次数计算扣分
+ $penalty = 0;
+ if ($frequentCount == 1) {
+ $penalty = self::PENALTY_FIRST_FREQUENT; // 首次频繁-15分
+ Log::info("首次频繁,扣除 " . abs(self::PENALTY_FIRST_FREQUENT) . " 分,accountId: {$accountId}");
+ } elseif ($frequentCount >= 2) {
+ $penalty = self::PENALTY_SECOND_FREQUENT; // 再次频繁-25分
+ Log::info("再次频繁,扣除 " . abs(self::PENALTY_SECOND_FREQUENT) . " 分,accountId: {$accountId}");
+ }
+
+ return [
+ 'lastFrequentTime' => $latestFrequentTime,
+ 'frequentCount' => $frequentCount,
+ 'frequentPenalty' => $penalty,
+ 'taskIds' => $taskIds
+ ];
+ }
+
+ /**
+ * 从s2_wechat_message表检查封号记录
+ * content包含"你的账号被限制"且msgType为10000
+ * 统计所有时间的数据(不限制30天)
+ * 每条记录只统计一次,使用is_counted字段标记
+ *
+ * @param int $accountId 账号ID
+ * @param string $wechatId 微信ID
+ * @param int $thirtyDaysAgo 已废弃参数,保留是为了兼容性
+ * @return array|null
+ */
+ private function checkBannedFromMessage($accountId, $wechatId, $thirtyDaysAgo = null)
+ {
+ // 不再使用30天限制
+
+ // 减少不必要的日志记录
+
+ // 查询封号消息(统计所有时间且未被统计过的记录)
+ // 优化查询:只查询必要的字段,减少数据传输量
+ $banMessage = Db::table(self::TABLE_WECHAT_MESSAGE)
+ ->where('wechatAccountId', $accountId)
+ ->where('msgType', 10000)
+ ->where('content', 'like', '%你的账号被限制%')
+ ->where('isDeleted', 0)
+ ->where(function($query) {
+ // 只查询未被统计过的记录
+ // 注意:需要兼容is_counted字段不存在的情况
+ $query->where('is_counted', 0)
+ ->whereOr('is_counted', null);
+ })
+ ->field('id, createTime') // 只查询必要的字段
+ ->order('createTime', 'desc')
+ ->find();
+
+ if (!empty($banMessage)) {
+ try {
+ // 检查表中是否存在is_counted字段
+ $hasIsCountedField = false;
+ $tableFields = Db::query("SHOW COLUMNS FROM " . self::TABLE_WECHAT_MESSAGE);
+ foreach ($tableFields as $field) {
+ if ($field['Field'] == 'is_counted') {
+ $hasIsCountedField = true;
+ break;
+ }
+ }
+
+ // 如果字段不存在,添加字段
+ if (!$hasIsCountedField) {
+ Log::info("添加is_counted字段到" . self::TABLE_WECHAT_MESSAGE . "表");
+ Db::execute("ALTER TABLE " . self::TABLE_WECHAT_MESSAGE . " ADD COLUMN is_counted TINYINT(1) DEFAULT 0 COMMENT '是否已统计(0=未统计,1=已统计)'");
+ }
+
+ // 更新记录为已统计
+ Db::table(self::TABLE_WECHAT_MESSAGE)
+ ->where('id', $banMessage['id'])
+ ->update(['is_counted' => 1]);
+
+ // 减少不必要的日志记录
+ Log::info("发现封号记录,扣除 " . abs(self::PENALTY_BANNED) . " 分,accountId: {$accountId}");
+ } catch (\Exception $e) {
+ Log::error("标记封号记录失败: " . $e->getMessage());
+ // 出错时不影响后续逻辑,继续执行
+ }
+
+ return [
+ 'isBanned' => 1,
+ 'banPenalty' => self::PENALTY_BANNED, // 封号-60分
+ 'lastBanTime' => $banMessage['createTime'],
+ 'messageId' => $banMessage['id']
+ ];
+ }
+
+ return [
+ 'isBanned' => 0,
+ 'banPenalty' => 0,
+ 'lastBanTime' => null,
+ 'messageId' => null
+ ];
+ }
+
+ /**
+ * 计算不频繁加分
+ * 反向参考频繁规则:计算连续不频繁天数
+ * 规则:连续不频繁的,只要有一次频繁就得重新计算(重置连续不频繁天数)
+ * 如果连续3天没有频繁,则每天+5分
+ *
+ * @param int $accountId 账号ID
+ * @param string $wechatId 微信ID
+ * @param array $frequentData 频繁数据(包含lastFrequentTime和frequentCount)
+ * @param int $thirtyDaysAgo 已废弃参数,保留是为了兼容性
+ * @return array 包含bonus、consecutiveDays、lastNoFrequentTime
+ */
+ private function calculateNoFrequentBonus($accountId, $wechatId, $frequentData, $thirtyDaysAgo = null)
+ {
+ $result = [
+ 'bonus' => 0,
+ 'consecutiveDays' => 0,
+ 'lastNoFrequentTime' => null
+ ];
+
+ if (empty($accountId) || empty($wechatId)) {
+ return $result;
+ }
+
+ $currentTime = time();
+
+ // 获取最后一次频繁时间
+ $lastFrequentTime = $frequentData['lastFrequentTime'] ?? null;
+
+ // 规则:连续不频繁的,只要有一次频繁就得重新计算(重置连续不频繁天数)
+ if (empty($lastFrequentTime)) {
+ // 情况1:没有频繁记录,说明一直连续不频繁
+ // 默认给30天的连续不频繁天数(可以根据需要调整)
+ $consecutiveDays = 30;
+ } else {
+ // 情况2:有频繁记录,从最后一次频繁时间开始重新计算连续不频繁天数
+ // 只要有一次频繁,连续不频繁天数就从最后一次频繁时间开始重新计算
+ // 计算从最后一次频繁时间到现在,连续多少天没有频繁
+ $timeDiff = $currentTime - $lastFrequentTime;
+ $consecutiveDays = floor($timeDiff / 86400); // 向下取整,得到完整的天数
+ }
+
+ // 如果连续3天或以上没有频繁,则每天+5分
+ if ($consecutiveDays >= 3) {
+ $bonus = $consecutiveDays * self::BONUS_NO_FREQUENT_PER_DAY;
+ $result['bonus'] = $bonus;
+ $result['consecutiveDays'] = $consecutiveDays;
+ $result['lastNoFrequentTime'] = $currentTime;
+ } else {
+ $result['consecutiveDays'] = $consecutiveDays;
+ }
+
+ return $result;
+ }
+
+ /**
+ * 构建日志快照(用于对比前后分值)
+ *
+ * @param array $scoreRecord
+ * @return array
+ */
+ private function buildScoreSnapshotForLogging($scoreRecord)
+ {
+ $baseScore = $scoreRecord['baseScore'] ?? self::DEFAULT_BASE_SCORE;
+ $dynamicScore = $scoreRecord['dynamicScore'] ?? 0;
+ $healthScore = $scoreRecord['healthScore'] ?? ($baseScore + $dynamicScore);
+
+ return [
+ 'frequentPenalty' => $scoreRecord['frequentPenalty'] ?? 0,
+ 'banPenalty' => $scoreRecord['banPenalty'] ?? 0,
+ 'noFrequentBonus' => $scoreRecord['noFrequentBonus'] ?? 0,
+ 'dynamicScore' => $dynamicScore,
+ 'healthScore' => $healthScore
+ ];
+ }
+
+ /**
+ * 根据前后快照写加减分日志
+ *
+ * @param int $accountId
+ * @param string $wechatId
+ * @param array $before
+ * @param array $after
+ * @param array $context
+ * @return void
+ */
+ private function logScoreChangesIfNeeded($accountId, $wechatId, array $before, array $after, array $context = [])
+ {
+ $healthBefore = $before['healthScore'] ?? 0;
+ $healthAfter = $after['healthScore'] ?? 0;
+
+ $this->recordScoreLog($accountId, $wechatId, 'frequentPenalty', $before['frequentPenalty'] ?? 0, $after['frequentPenalty'] ?? 0, [
+ 'category' => 'penalty',
+ 'source' => 'friend_task',
+ 'sourceId' => !empty($context['frequentTaskIds']) ? $context['frequentTaskIds'][0] : null,
+ 'extra' => [
+ 'taskIds' => $context['frequentTaskIds'] ?? [],
+ 'frequentCount' => $context['frequentCount'] ?? 0,
+ 'lastFrequentTime' => $context['lastFrequentTime'] ?? null
+ ],
+ 'totalScoreBefore' => $healthBefore,
+ 'totalScoreAfter' => $healthAfter
+ ]);
+
+ $this->recordScoreLog($accountId, $wechatId, 'banPenalty', $before['banPenalty'] ?? 0, $after['banPenalty'] ?? 0, [
+ 'category' => 'penalty',
+ 'source' => 'wechat_message',
+ 'sourceId' => $context['banMessageId'] ?? null,
+ 'extra' => [
+ 'lastBanTime' => $context['lastBanTime'] ?? null
+ ],
+ 'totalScoreBefore' => $healthBefore,
+ 'totalScoreAfter' => $healthAfter
+ ]);
+
+ $this->recordScoreLog($accountId, $wechatId, 'noFrequentBonus', $before['noFrequentBonus'] ?? 0, $after['noFrequentBonus'] ?? 0, [
+ 'category' => 'bonus',
+ 'source' => 'system',
+ 'extra' => [
+ 'consecutiveDays' => $context['consecutiveNoFrequentDays'] ?? 0,
+ 'lastNoFrequentTime' => $context['lastNoFrequentTime'] ?? null
+ ],
+ 'totalScoreBefore' => $healthBefore,
+ 'totalScoreAfter' => $healthAfter
+ ]);
+
+ $this->recordScoreLog($accountId, $wechatId, 'dynamicScore', $before['dynamicScore'] ?? 0, $after['dynamicScore'] ?? 0, [
+ 'category' => 'dynamic_total',
+ 'source' => 'system',
+ 'totalScoreBefore' => $healthBefore,
+ 'totalScoreAfter' => $healthAfter
+ ]);
+
+ $this->recordScoreLog($accountId, $wechatId, 'healthScore', $before['healthScore'] ?? 0, $after['healthScore'] ?? 0, [
+ 'category' => 'health_total',
+ 'source' => 'system',
+ 'totalScoreBefore' => $healthBefore,
+ 'totalScoreAfter' => $healthAfter
+ ]);
+ }
+
+ /**
+ * 插入健康分加减分日志
+ *
+ * @param int $accountId
+ * @param string $wechatId
+ * @param string $field
+ * @param int|null $beforeValue
+ * @param int|null $afterValue
+ * @param array $context
+ * @return void
+ */
+ private function recordScoreLog($accountId, $wechatId, $field, $beforeValue, $afterValue, array $context = [])
+ {
+ $beforeValue = (int)($beforeValue ?? 0);
+ $afterValue = (int)($afterValue ?? 0);
+
+ if ($beforeValue === $afterValue) {
+ return;
+ }
+
+ $extraPayload = $context['extra'] ?? null;
+ if (is_array($extraPayload)) {
+ $extraPayload = json_encode($extraPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+ } elseif (!is_string($extraPayload)) {
+ $extraPayload = null;
+ }
+
+ $sourceId = null;
+ if (array_key_exists('sourceId', $context)) {
+ $sourceId = $context['sourceId'];
+ }
+
+ $totalScoreBefore = null;
+ if (array_key_exists('totalScoreBefore', $context)) {
+ $totalScoreBefore = $context['totalScoreBefore'];
+ }
+
+ $totalScoreAfter = null;
+ if (array_key_exists('totalScoreAfter', $context)) {
+ $totalScoreAfter = $context['totalScoreAfter'];
+ }
+
+ $data = [
+ 'accountId' => $accountId,
+ 'wechatId' => $wechatId,
+ 'field' => $field,
+ 'changeValue' => $afterValue - $beforeValue,
+ 'valueBefore' => $beforeValue,
+ 'valueAfter' => $afterValue,
+ 'category' => $context['category'] ?? null,
+ 'source' => $context['source'] ?? null,
+ 'sourceId' => $sourceId,
+ 'extra' => $extraPayload,
+ 'totalScoreBefore' => $totalScoreBefore,
+ 'totalScoreAfter' => $totalScoreAfter,
+ 'createTime' => time()
+ ];
+
+ try {
+ Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE_LOG)->insert($data);
+ } catch (\Exception $e) {
+ Log::error("记录健康分加减分日志失败,accountId: {$accountId}, field: {$field}, 错误: " . $e->getMessage());
+ }
+ }
+
+ /**
+ * 根据健康分计算每日最大加人次数
+ * 公式:每日最大加人次数 = 健康分 * 0.2
+ *
+ * @param int $healthScore 健康分
+ * @return int 每日最大加人次数
+ */
+ public function getMaxAddFriendPerDay($healthScore)
+ {
+ return (int)floor($healthScore * 0.2);
+ }
+
+ /**
+ * 批量计算并更新多个账号的健康分
+ * 优化:使用多线程处理、优化批处理逻辑、减少日志记录
+ *
+ * @param array $accountIds 账号ID数组(为空则处理所有账号)
+ * @param int $batchSize 每批处理数量
+ * @param bool $forceRecalculateBase 是否强制重新计算基础分
+ * @param bool $useMultiThread 是否使用多线程处理(需要pcntl扩展支持)
+ * @return array 处理结果统计
+ * @throws Exception 如果参数无效或批量处理过程中出现严重错误
+ */
+ public function batchCalculateAndUpdate($accountIds = [], $batchSize = 50, $forceRecalculateBase = false, $useMultiThread = false)
+ {
+ // 参数验证
+ if (!is_array($accountIds)) {
+ $errorMsg = "无效的账号ID数组: " . gettype($accountIds);
+ Log::error($errorMsg);
+ throw new Exception($errorMsg);
+ }
+
+ if (!is_numeric($batchSize) || $batchSize <= 0) {
+ $errorMsg = "无效的批处理大小: {$batchSize}";
+ Log::error($errorMsg);
+ throw new Exception($errorMsg);
+ }
+
+ // 检查是否支持多线程
+ if ($useMultiThread && !function_exists('pcntl_fork')) {
+ $useMultiThread = false;
+ Log::warning("系统不支持pcntl扩展,无法使用多线程处理,将使用单线程模式");
+ }
+
+ try {
+ $startTime = microtime(true);
+ // 去除开始日志,减少日志空间消耗
+
+
+ $stats = [
+ 'total' => 0,
+ 'success' => 0,
+ 'failed' => 0,
+ 'errors' => []
+ ];
+
+ // 如果没有指定账号ID,则处理所有账号
+ if (empty($accountIds)) {
+ $accountIds = Db::table(self::TABLE_WECHAT_ACCOUNT)
+ ->where('isDeleted', 0)
+ ->column('id');
+ }
+
+ $stats['total'] = count($accountIds);
+
+ // 优化:减小批次大小,提高并行处理效率
+ $batchSize = min($batchSize, 50);
+
+ // 分批处理
+ $batches = array_chunk($accountIds, $batchSize);
+ $batchCount = count($batches);
+ Log::info("分批处理,共 {$batchCount} 批");
+
+ // 多线程处理
+ if ($useMultiThread && $batchCount > 1) {
+ $childPids = [];
+ $maxProcesses = 4; // 最大并行进程数
+ $runningProcesses = 0;
+
+ for ($i = 0; $i < $batchCount; $i++) {
+ // 如果达到最大进程数,等待某个子进程结束
+ if ($runningProcesses >= $maxProcesses) {
+ $pid = pcntl_wait($status);
+ $runningProcesses--;
+ }
+
+ // 创建子进程
+ $pid = pcntl_fork();
+
+ if ($pid == -1) {
+ // 创建进程失败
+ Log::error("创建子进程失败");
+ continue;
+ } elseif ($pid == 0) {
+ // 子进程
+ $this->processBatch($batches[$i], $i, $batchCount, $forceRecalculateBase);
+ exit(0);
+ } else {
+ // 父进程
+ $childPids[] = $pid;
+ $runningProcesses++;
+ }
+ }
+
+ // 等待所有子进程结束
+ foreach ($childPids as $pid) {
+ pcntl_waitpid($pid, $status);
+ }
+
+ Log::info("所有批次处理完成");
+ } else {
+ // 单线程处理
+ foreach ($batches as $batchIndex => $batch) {
+ $batchStats = $this->processBatch($batch, $batchIndex, $batchCount, $forceRecalculateBase);
+ $stats['success'] += $batchStats['success'];
+ $stats['failed'] += $batchStats['failed'];
+ $stats['errors'] = array_merge($stats['errors'], $batchStats['errors']);
+ }
+ }
+
+ $endTime = microtime(true);
+ $totalDuration = round($endTime - $startTime, 2);
+ // 只在有失败时记录日志
+ if ($stats['failed'] > 0) {
+ Log::warning("批量计算健康分完成,总耗时: {$totalDuration}秒,成功: {$stats['success']},失败: {$stats['failed']}");
+ }
+
+ return $stats;
+ } catch (\PDOException $e) {
+ $errorMsg = "批量计算健康分过程中数据库操作失败: " . $e->getMessage();
+ Log::error($errorMsg);
+ throw new Exception($errorMsg, $e->getCode(), $e);
+ } catch (\Throwable $e) {
+ $errorMsg = "批量计算健康分过程中发生严重错误: " . $e->getMessage();
+ Log::error($errorMsg);
+ throw new Exception($errorMsg, $e->getCode(), $e);
+ }
+ }
+
+ /**
+ * 处理单个批次的账号
+ *
+ * @param array $batch 批次账号ID数组
+ * @param int $batchIndex 批次索引
+ * @param int $batchCount 总批次数
+ * @param bool $forceRecalculateBase 是否强制重新计算基础分
+ * @return array 处理结果统计
+ */
+ private function processBatch($batch, $batchIndex, $batchCount, $forceRecalculateBase)
+ {
+ $batchStartTime = microtime(true);
+ // 去除批次开始日志,减少日志空间消耗
+
+ $stats = [
+ 'success' => 0,
+ 'failed' => 0,
+ 'errors' => []
+ ];
+
+ // 优化:预先获取账号数据,减少重复查询
+ $accountIds = implode(',', $batch);
+ $accountDataMap = [];
+ if (!empty($batch)) {
+ $accountDataList = Db::table(self::TABLE_WECHAT_ACCOUNT)
+ ->where('id', 'in', $batch)
+ ->select();
+
+ foreach ($accountDataList as $accountData) {
+ $accountDataMap[$accountData['id']] = $accountData;
+ }
+ }
+
+ // 批量处理账号
+ foreach ($batch as $accountId) {
+ try {
+ $accountData = $accountDataMap[$accountId] ?? null;
+ $this->calculateAndUpdate($accountId, $accountData, $forceRecalculateBase);
+ $stats['success']++;
+
+ // 减少日志记录,每10个账号记录一次进度
+ if ($stats['success'] % 10 == 0) {
+ Log::debug("批次 " . ($batchIndex + 1) . " 已处理 {$stats['success']} 个账号");
+ }
+ } catch (Exception $e) {
+ $stats['failed']++;
+ $stats['errors'][] = [
+ 'accountId' => $accountId,
+ 'error' => $e->getMessage()
+ ];
+ Log::error("账号 {$accountId} 计算失败: " . $e->getMessage());
+ }
+ }
+
+ $batchEndTime = microtime(true);
+ $batchDuration = round($batchEndTime - $batchStartTime, 2);
+ Log::info("第 " . ($batchIndex + 1) . "/" . $batchCount . " 批处理完成,耗时: {$batchDuration}秒," .
+ "成功: {$stats['success']},失败: {$stats['failed']}");
+
+ return $stats;
+ }
+
+ /**
+ * 记录频繁事件(已废弃,改为从s2_friend_task表自动检测)
+ * 保留此方法以兼容旧代码,实际频繁检测在calculateDynamicScore中完成
+ *
+ * @param int $accountId 账号ID
+ * @return bool
+ */
+ public function recordFrequent($accountId)
+ {
+ // 频繁检测已改为从s2_friend_task表自动检测
+ // 直接重新计算健康分即可
+ try {
+ $this->calculateAndUpdate($accountId);
+ return true;
+ } catch (\Exception $e) {
+ return false;
+ }
+ }
+
+ /**
+ * 记录不频繁事件(用于加分)
+ *
+ * @param int $accountId 账号ID
+ * @return bool
+ */
+ public function recordNoFrequent($accountId)
+ {
+ $scoreRecord = $this->getScoreRecord($accountId);
+
+ if (empty($scoreRecord)) {
+ // 如果记录不存在,先创建
+ $accountData = Db::table(self::TABLE_WECHAT_ACCOUNT)
+ ->where('id', $accountId)
+ ->find();
+
+ if (empty($accountData)) {
+ return false;
+ }
+
+ $this->getOrCreateScoreRecord($accountId, $accountData['wechatId']);
+ $scoreRecord = $this->getScoreRecord($accountId);
+ }
+
+ $lastNoFrequentTime = $scoreRecord['lastNoFrequentTime'] ?? null;
+ $consecutiveNoFrequentDays = $scoreRecord['consecutiveNoFrequentDays'] ?? 0;
+ $currentTime = time();
+
+ // 如果上次不频繁时间是昨天或更早,则增加连续天数
+ if (empty($lastNoFrequentTime) || ($currentTime - $lastNoFrequentTime) >= 86400) {
+ // 如果间隔超过2天,重置为1天
+ if (!empty($lastNoFrequentTime) && ($currentTime - $lastNoFrequentTime) > 86400 * 2) {
+ $consecutiveNoFrequentDays = 1;
+ } else {
+ $consecutiveNoFrequentDays++;
+ }
+ }
+
+ // 计算加分(连续3天及以上才加分)
+ $bonus = 0;
+ if ($consecutiveNoFrequentDays >= 3) {
+ $bonus = $consecutiveNoFrequentDays * self::BONUS_NO_FREQUENT_PER_DAY;
+ }
+
+ $updateData = [
+ 'lastNoFrequentTime' => $currentTime,
+ 'consecutiveNoFrequentDays' => $consecutiveNoFrequentDays,
+ 'noFrequentBonus' => $bonus,
+ 'updateTime' => $currentTime
+ ];
+
+ Db::table('s2_wechat_account_score')
+ ->where('accountId', $accountId)
+ ->update($updateData);
+
+ // 重新计算健康分
+ $this->calculateAndUpdate($accountId);
+
+ return true;
+ }
+
+ /**
+ * 获取账号健康分信息
+ * 优化:使用多级缓存策略,提高缓存命中率
+ *
+ * @param int $accountId 账号ID
+ * @param bool $useCache 是否使用缓存(默认true)
+ * @param bool $forceRecalculate 是否强制重新计算(默认false)
+ * @return array|null
+ */
+ public function getHealthScore($accountId, $useCache = true, $forceRecalculate = false)
+ {
+ // 如果强制重新计算,则不使用缓存
+ if ($forceRecalculate) {
+ Log::info("强制重新计算健康分,accountId: {$accountId}");
+ return $this->calculateAndUpdate($accountId, null, false);
+ }
+
+ // 生成缓存键
+ $cacheKey = self::CACHE_PREFIX . 'health:' . $accountId;
+
+ // 如果使用缓存且缓存存在,则直接返回缓存数据
+ if ($useCache && !$forceRecalculate && Cache::has($cacheKey)) {
+ $cachedData = Cache::get($cacheKey);
+ // 减少日志记录,提高性能
+ return $cachedData;
+ }
+
+ // 从数据库获取记录
+ $scoreRecord = $this->getScoreRecord($accountId, $useCache);
+
+ if (empty($scoreRecord)) {
+ return null;
+ }
+
+ $healthScoreInfo = [
+ 'accountId' => $scoreRecord['accountId'],
+ 'wechatId' => $scoreRecord['wechatId'],
+ 'healthScore' => $scoreRecord['healthScore'] ?? 0,
+ 'baseScore' => $scoreRecord['baseScore'] ?? 0,
+ 'baseInfoScore' => $scoreRecord['baseInfoScore'] ?? 0,
+ 'friendCountScore' => $scoreRecord['friendCountScore'] ?? 0,
+ 'friendCount' => $scoreRecord['friendCount'] ?? 0,
+ 'dynamicScore' => $scoreRecord['dynamicScore'] ?? 0,
+ 'frequentPenalty' => $scoreRecord['frequentPenalty'] ?? 0,
+ 'noFrequentBonus' => $scoreRecord['noFrequentBonus'] ?? 0,
+ 'banPenalty' => $scoreRecord['banPenalty'] ?? 0,
+ 'maxAddFriendPerDay' => $scoreRecord['maxAddFriendPerDay'] ?? 0,
+ 'baseScoreCalculated' => $scoreRecord['baseScoreCalculated'] ?? 0,
+ 'lastFrequentTime' => $scoreRecord['lastFrequentTime'] ?? null,
+ 'frequentCount' => $scoreRecord['frequentCount'] ?? 0,
+ 'isBanned' => $scoreRecord['isBanned'] ?? 0,
+ 'lastBanTime' => $scoreRecord['lastBanTime'] ?? null
+ ];
+
+ // 如果使用缓存,则缓存健康分信息
+ if ($useCache) {
+ // 根据数据更新频率设置不同的缓存时间
+ // 如果有频繁记录或封号记录,使用短期缓存
+ $cacheTime = (!empty($scoreRecord['lastFrequentTime']) || !empty($scoreRecord['isBanned']))
+ ? self::CACHE_TTL_SHORT
+ : self::CACHE_TTL;
+
+ Cache::set($cacheKey, $healthScoreInfo, $cacheTime);
+ Log::debug("缓存健康分信息,accountId: {$accountId}, 缓存时间: {$cacheTime}秒");
+ }
+
+ return $healthScoreInfo;
+ }
+
+ /**
+ * 清除健康分信息缓存
+ *
+ * @param int $accountId 账号ID
+ * @return bool 是否成功清除缓存
+ */
+ public function clearHealthScoreCache($accountId)
+ {
+ $cacheKey = self::CACHE_PREFIX . 'health:' . $accountId;
+ $result = Cache::rm($cacheKey);
+
+ // 同时清除评分记录缓存
+ $this->clearScoreCache($accountId);
+
+ // 减少不必要的日志记录
+ return $result;
+ }
+}
diff --git a/application/common/socket/Events.php b/application/common/socket/Events.php
new file mode 100644
index 0000000..0418e46
--- /dev/null
+++ b/application/common/socket/Events.php
@@ -0,0 +1,126 @@
+info('连接成功: {id}', [
+ 'id' => $clientId,
+ ]);
+ } catch (\Exception $ex) {
+ Gateway::closeClient($clientId);
+
+ Logger::_(static::LOGGER)->info('连接错误: {id}|{error}', [
+ 'id' => $clientId,
+ 'error' => $ex->getMessage() . '<' . $ex->getCode() . '>',
+ ]);
+ }
+ }
+
+ /**
+ * onMessage 事件回调
+ * 当客户端发来数据(Gateway进程收到数据)后触发
+ *
+ * @access public
+ * @param int $client_id
+ * @param mixed $data
+ * @return void
+ */
+ public static function onMessage($clientId, $data) {
+ $json = @json_decode($data, TRUE);
+ if (!empty($json)
+ AND !empty($json['type'])
+ AND is_array($json['data'])) {
+ $session = Gateway::getSession($clientId);
+ if (empty($session)) {
+ throw new \Exception('获取SESSION失败: ' . $clientId);
+ }
+
+ } else {
+ Gateway::closeClient($clientId);
+
+ Logger::_(static::LOGGER)->error('消息错误: {id}|{data}', [
+ 'id' => $clientId,
+ 'data' => $data,
+ ]);
+ }
+ }
+
+ /**
+ * onClose 事件回调 当用户断开连接时触发的方法
+ *
+ * @param integer $clientId 断开连接的客户端client_id
+ * @return void
+ */
+ public static function onClose($clientId) {
+ Gateway::setSession($clientId, []);
+ Logger::_(static::LOGGER)->info('连接关闭: {id}', [
+ 'id' => $clientId,
+ ]);
+ }
+
+ /**
+ * onWorkerStart 事件回调
+ * 当businessWorker进程启动时触发。每个进程生命周期内都只会触发一次
+ *
+ * @access public
+ * @param \Workerman\Worker $businessWorker
+ * @return void
+ */
+ public static function onWorkerStart(Worker $businessWorker) {
+ Logger::_(static::LOGGER)->info('进程启动: {id}', [
+ 'id' => $businessWorker->id,
+ ]);
+ }
+
+ /**
+ * onWorkerStop 事件回调
+ * 当businessWorker进程退出时触发。每个进程生命周期内都只会触发一次。
+ *
+ * @param \Workerman\Worker $businessWorker
+ * @return void
+ */
+ public static function onWorkerStop(Worker $businessWorker) {
+ Logger::_(static::LOGGER)->info('进程关闭: {id}', [
+ 'id' => $businessWorker->id,
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/application/common/util/AliyunOSS.php b/application/common/util/AliyunOSS.php
new file mode 100644
index 0000000..d25cf7a
--- /dev/null
+++ b/application/common/util/AliyunOSS.php
@@ -0,0 +1,79 @@
+getMessage());
+ }
+ }
+
+ /**
+ * 上传文件到OSS
+ * @param string $filePath 本地文件路径
+ * @param string $objectName OSS对象名称
+ * @return array
+ * @throws OssException
+ */
+ public static function uploadFile($filePath, $objectName)
+ {
+ try {
+ $client = self::getClient();
+
+ // 上传文件
+ $result = $client->uploadFile(self::BUCKET, $objectName, $filePath);
+
+ // 获取文件访问URL
+ $url = !empty($result['oss-request-url']) ? $result['oss-request-url'] : $client->signUrl(self::BUCKET, $objectName, 3600);
+
+ return [
+ 'success' => true,
+ 'url' => $url,
+ 'object_name' => $objectName,
+ 'size' => filesize($filePath),
+ 'mime_type' => mime_content_type($filePath)
+ ];
+ } catch (OssException $e) {
+ return [
+ 'success' => false,
+ 'error' => $e->getMessage()
+ ];
+ }
+ }
+
+ /**
+ * 生成OSS对象名称
+ * @param string $originalName 原始文件名
+ * @return string
+ */
+ public static function generateObjectName($originalName)
+ {
+ $ext = pathinfo($originalName, PATHINFO_EXTENSION);
+ $name = md5(uniqid(mt_rand(), true));
+ return date('Y/m/d/') . $name . '.' . $ext;
+ }
+}
\ No newline at end of file
diff --git a/application/common/util/AliyunSMS.php b/application/common/util/AliyunSMS.php
new file mode 100644
index 0000000..e742591
--- /dev/null
+++ b/application/common/util/AliyunSMS.php
@@ -0,0 +1,65 @@
+ static::ACCESS_KEY_ID,
+ // AccessKey Secret
+ 'accessKeySecret' => static::ACCESS_KEY_SECRET
+ ]);
+
+ // 访问的域名
+ $config->endpoint = 'dysmsapi.aliyuncs.com';
+
+ return new Dysmsapi($config);
+ }
+
+ /**
+ * 发送验证码
+ *
+ * @param $phoneNumbers
+ * @param $templateCode
+ * @param array $templateParam
+ * @return bool
+ */
+ static public function send($phoneNumbers, $templateCode, array $templateParam = array()) {
+ $client = static::createClient();
+ $sendSmsRequest = new SendSmsRequest([
+ 'phoneNumbers' => $phoneNumbers,
+ 'signName' => static::SIGN_NAME,
+ 'templateCode' => $templateCode,
+ 'templateParam' => json_encode($templateParam)
+ ]);
+ $runtime = new RuntimeOptions([]);
+ $logFile = ROOT_PATH . DS . 'aliyun-sms.txt';
+ try {
+ $data = $client->sendSmsWithOptions($sendSmsRequest, $runtime);
+ if ($data->body->code === 'OK') {
+ return TRUE;
+ }
+
+ $logData = print_r($data, TRUE);
+ } catch (\Exception $ex) {
+ $logData = print_r($ex, TRUE);
+ }
+
+ file_put_contents($logFile, '[' . date('Y-m-d H:i:s') . ']' . PHP_EOL . $logData . PHP_EOL . PHP_EOL, FILE_APPEND);
+
+ return FALSE;
+ }
+}
\ No newline at end of file
diff --git a/application/common/util/JwtUtil.php b/application/common/util/JwtUtil.php
new file mode 100644
index 0000000..8285fd5
--- /dev/null
+++ b/application/common/util/JwtUtil.php
@@ -0,0 +1,137 @@
+ 'HS256', // 加密算法
+ 'typ' => 'JWT' // 类型
+ ];
+
+ /**
+ * 创建JWT令牌
+ * @param array $payload 载荷信息
+ * @param int $expire 过期时间(秒),默认2小时
+ * @return string
+ */
+ public static function createToken($payload, $expire = 7200)
+ {
+ $header = self::base64UrlEncode(json_encode(self::$header, JSON_UNESCAPED_UNICODE));
+
+ // 附加过期时间
+ $payload['exp'] = time() + $expire;
+ $payload['iat'] = time(); // 签发时间
+
+ unset($payload['passwordMd5']);
+
+ $payload = self::base64UrlEncode(json_encode($payload, JSON_UNESCAPED_UNICODE));
+ $signature = self::signature($header . '.' . $payload, self::$secret);
+
+ return $header . '.' . $payload . '.' . $signature;
+ }
+
+ /**
+ * 验证令牌
+ * @param string $token 令牌
+ * @return array|bool 验证通过返回载荷信息,失败返回false
+ */
+ public static function verifyToken($token)
+ {
+ if (empty($token)) {
+ return false;
+ }
+
+ $tokenArray = explode('.', $token);
+ if (count($tokenArray) != 3) {
+ return false;
+ }
+
+ list($header, $payload, $signature) = $tokenArray;
+
+ // 验证签名
+ if (self::signature($header . '.' . $payload, self::$secret) !== $signature) {
+ return false;
+ }
+
+ // 解码载荷
+ $payload = json_decode(self::base64UrlDecode($payload), true);
+
+ // 验证是否过期
+ if (isset($payload['exp']) && $payload['exp'] < time()) {
+ return false;
+ }
+
+ return $payload;
+ }
+
+ /**
+ * 生成签名
+ * @param string $input 输入
+ * @param string $key 密钥
+ * @return string
+ */
+ private static function signature($input, $key)
+ {
+ return self::base64UrlEncode(hash_hmac('sha256', $input, $key, true));
+ }
+
+ /**
+ * URL安全的Base64编码
+ * @param string $input
+ * @return string
+ */
+ private static function base64UrlEncode($input)
+ {
+ return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($input));
+ }
+
+ /**
+ * URL安全的Base64解码
+ * @param string $input
+ * @return string
+ */
+ private static function base64UrlDecode($input)
+ {
+ $remainder = strlen($input) % 4;
+ if ($remainder) {
+ $input .= str_repeat('=', 4 - $remainder);
+ }
+ return base64_decode(str_replace(['-', '_'], ['+', '/'], $input));
+ }
+
+ /**
+ * 从请求头中获取Token
+ * @return string|null
+ */
+ public static function getRequestToken()
+ {
+ $authorization = Request::header('Authorization');
+ if (!$authorization) {
+ return null;
+ }
+
+ // 检查Bearer前缀
+ if (strpos($authorization, 'Bearer ') !== 0) {
+ return null;
+ }
+
+ return substr($authorization, 7);
+ }
+}
\ No newline at end of file
diff --git a/application/common/util/PaymentUtil.php b/application/common/util/PaymentUtil.php
new file mode 100644
index 0000000..9c1dd9d
--- /dev/null
+++ b/application/common/util/PaymentUtil.php
@@ -0,0 +1,255 @@
+ $value) {
+ $pairs[] = $key . '=' . $value;
+ }
+ return implode('&', $pairs);
+ }
+
+ /**
+ * 生成MD5签名
+ *
+ * @param string $queryString 待签名字符串
+ * @param string $secretKey 密钥
+ * @return string MD5签名
+ */
+ private static function generateMd5Sign(string $queryString, string $secretKey): string
+ {
+ $signString = $queryString . '&key=' . $secretKey;
+ return strtoupper(md5($signString));
+ }
+
+ /**
+ * 生成RSA256签名
+ *
+ * @param string $queryString 待签名字符串
+ * @param string $privateKey 私钥
+ * @return string RSA256签名
+ */
+ private static function generateRsa256Sign(string $queryString, string $privateKey): string
+ {
+ $privateKey = self::formatPrivateKey($privateKey);
+ $key = openssl_pkey_get_private($privateKey);
+ if (!$key) {
+ throw new \Exception('RSA私钥格式错误');
+ }
+
+ $signature = '';
+ $result = openssl_sign($queryString, $signature, $key, OPENSSL_ALGO_SHA256);
+ openssl_pkey_free($key);
+
+ if (!$result) {
+ throw new \Exception('RSA256签名失败');
+ }
+
+ return base64_encode($signature);
+ }
+
+ /**
+ * 生成RSA1签名
+ *
+ * @param string $queryString 待签名字符串
+ * @param string $privateKey 私钥
+ * @return string RSA1签名
+ */
+ private static function generateRsa1Sign(string $queryString, string $privateKey): string
+ {
+ $privateKey = self::formatPrivateKey($privateKey);
+ $key = openssl_pkey_get_private($privateKey);
+ if (!$key) {
+ throw new \Exception('RSA私钥格式错误');
+ }
+
+ $signature = '';
+ $result = openssl_sign($queryString, $signature, $key, OPENSSL_ALGO_SHA1);
+ openssl_pkey_free($key);
+
+ if (!$result) {
+ throw new \Exception('RSA1签名失败');
+ }
+
+ return base64_encode($signature);
+ }
+
+ /**
+ * 格式化私钥
+ *
+ * @param string $privateKey 原始私钥
+ * @return string 格式化后的私钥
+ */
+ private static function formatPrivateKey(string $privateKey): string
+ {
+ $privateKey = str_replace(['-----BEGIN PRIVATE KEY-----', '-----END PRIVATE KEY-----', "\n", "\r"], '', $privateKey);
+ $privateKey = chunk_split($privateKey, 64, "\n");
+ return "-----BEGIN PRIVATE KEY-----\n" . $privateKey . "-----END PRIVATE KEY-----";
+ }
+
+ /**
+ * 格式化公钥
+ *
+ * @param string $publicKey 原始公钥
+ * @return string 格式化后的公钥
+ */
+ private static function formatPublicKey(string $publicKey): string
+ {
+ $publicKey = str_replace(['-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----', "\n", "\r"], '', $publicKey);
+ $publicKey = chunk_split($publicKey, 64, "\n");
+ return "-----BEGIN PUBLIC KEY-----\n" . $publicKey . "-----END PUBLIC KEY-----";
+ }
+
+ /**
+ * 验证RSA签名
+ *
+ * @param string $queryString 原始字符串
+ * @param string $signature 签名
+ * @param string $publicKey 公钥
+ * @param string $signType 签名类型
+ * @return bool 验证结果
+ */
+ public static function verifyRsaSign(string $queryString, string $signature, string $publicKey, string $signType = self::SIGN_TYPE_RSA_1_256): bool
+ {
+ $publicKey = self::formatPublicKey($publicKey);
+ $key = openssl_pkey_get_public($publicKey);
+ if (!$key) {
+ return false;
+ }
+
+ $algorithm = $signType === self::SIGN_TYPE_RSA_1_1 ? OPENSSL_ALGO_SHA1 : OPENSSL_ALGO_SHA256;
+ $result = openssl_verify($queryString, base64_decode($signature), $key, $algorithm);
+ openssl_pkey_free($key);
+
+ return $result === 1;
+ }
+
+ /**
+ * 生成随机字符串
+ *
+ * @param int $length 长度
+ * @return string 随机字符串
+ */
+ public static function generateNonceStr(int $length = 32): string
+ {
+ $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
+ $str = '';
+ for ($i = 0; $i < $length; $i++) {
+ $str .= $chars[mt_rand(0, strlen($chars) - 1)];
+ }
+ return $str;
+ }
+
+ /**
+ * 生成时间戳
+ *
+ * @return int 时间戳
+ */
+ public static function generateTimestamp(): int
+ {
+ return time();
+ }
+
+ /**
+ * 格式化金额(分转元)
+ *
+ * @param int $amount 金额(分)
+ * @return string 格式化后的金额(元)
+ */
+ public static function formatAmount(int $amount): string
+ {
+ return number_format($amount / 100, 2, '.', '');
+ }
+
+ /**
+ * 解析金额(元转分)
+ *
+ * @param string $amount 金额(元)
+ * @return int 金额(分)
+ */
+ public static function parseAmount(string $amount): int
+ {
+ return (int) round(floatval($amount) * 100);
+ }
+}
diff --git a/application/common/util/Signer.php b/application/common/util/Signer.php
new file mode 100644
index 0000000..9b68840
--- /dev/null
+++ b/application/common/util/Signer.php
@@ -0,0 +1,135 @@
+ $value) {
+ if ($key === 'sign') {
+ continue;
+ }
+ if ($value === '' || $value === null) {
+ continue;
+ }
+ $filtered[$key] = $value;
+ }
+
+ ksort($filtered, SORT_STRING);
+
+ $pairs = [];
+ foreach ($filtered as $key => $value) {
+ // 原始值拼接,不做 urlencode
+ $pairs[] = $key . '=' . (is_bool($value) ? ($value ? '1' : '0') : (string)$value);
+ }
+
+ return implode('&', $pairs);
+ }
+
+ /**
+ * MD5 签名
+ * - 若提供 secret,则原串末尾追加 &key=SECRET
+ * - 返回 32 位小写
+ *
+ * @param string $signString
+ * @param string|null $secret
+ * @return string
+ */
+ protected static function signMd5($signString, $secret = null)
+ {
+ if ($secret !== null && $secret !== '') {
+ $signString .= '&key=' . $secret;
+ }
+ return strtolower(md5($signString));
+ }
+
+ /**
+ * RSA 签名
+ *
+ * @param string $signString
+ * @param array $options 必填:private_key,可选:passphrase
+ * @param string $hashAlgo sha256|sha1
+ * @return string base64 签名
+ * @throws \InvalidArgumentException
+ */
+ protected static function signRsa($signString, array $options, $hashAlgo = 'sha256')
+ {
+ if (empty($options['private_key'])) {
+ throw new \InvalidArgumentException('RSA signing requires private_key.');
+ }
+
+ $privateKey = $options['private_key'];
+ $passphrase = isset($options['passphrase']) ? (string)$options['passphrase'] : '';
+
+ // 兼容无头尾私钥,自动包裹为 PEM
+ if (strpos($privateKey, 'BEGIN') === false) {
+ $privateKey = "-----BEGIN PRIVATE KEY-----\n" . trim(chunk_split(str_replace(["\r", "\n"], '', $privateKey), 64, "\n")) . "\n-----END PRIVATE KEY-----";
+ }
+
+ $pkeyId = openssl_pkey_get_private($privateKey, $passphrase);
+ if ($pkeyId === false) {
+ throw new \InvalidArgumentException('Invalid RSA private key or passphrase.');
+ }
+
+ $signature = '';
+ $algoConst = $hashAlgo === 'sha1' ? OPENSSL_ALGO_SHA1 : OPENSSL_ALGO_SHA256;
+ $ok = openssl_sign($signString, $signature, $pkeyId, $algoConst);
+ openssl_free_key($pkeyId);
+
+ if (!$ok) {
+ throw new \InvalidArgumentException('OpenSSL sign failed.');
+ }
+
+ return base64_encode($signature);
+ }
+}
+
+
diff --git a/application/common/validate/Auth.php b/application/common/validate/Auth.php
new file mode 100644
index 0000000..3fc8364
--- /dev/null
+++ b/application/common/validate/Auth.php
@@ -0,0 +1,46 @@
+ 'require',
+ 'password' => 'require|length:6,64',
+ 'code' => 'require|length:4,6',
+ 'typeId' => 'require|in:1,2',
+ ];
+
+ /**
+ * 错误信息
+ * @var array
+ */
+ protected $message = [
+ 'account.require' => '账号不能为空',
+ 'password.require' => '密码不能为空',
+ 'password.length' => '密码长度必须在6-64个字符之间',
+ 'code.require' => '验证码不能为空',
+ 'code.length' => '验证码长度必须在4-6个字符之间',
+ 'typeId.require' => '用户类型不能为空',
+ 'typeId.in' => '用户类型错误',
+ ];
+
+ /**
+ * 验证场景
+ * @var array
+ */
+ protected $scene = [
+ 'login' => ['account', 'password', 'typeId'],
+ 'mobile_login' => ['account', 'code', 'typeId'],
+ 'refresh' => [],
+ 'send_code' => ['account', 'type'],
+ ];
+}
\ No newline at end of file
diff --git a/application/common/view/tpl/dispatch_jump.tpl b/application/common/view/tpl/dispatch_jump.tpl
new file mode 100644
index 0000000..50d63d7
--- /dev/null
+++ b/application/common/view/tpl/dispatch_jump.tpl
@@ -0,0 +1,93 @@
+{__NOLAYOUT__}
+
+
+
+
+ 跳转提示
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/cozeai/config/route.php b/application/cozeai/config/route.php
new file mode 100644
index 0000000..ca6c702
--- /dev/null
+++ b/application/cozeai/config/route.php
@@ -0,0 +1,24 @@
+middleware(['jwt']);
\ No newline at end of file
diff --git a/application/cozeai/controller/BaseController.php b/application/cozeai/controller/BaseController.php
new file mode 100644
index 0000000..20d1dc7
--- /dev/null
+++ b/application/cozeai/controller/BaseController.php
@@ -0,0 +1,110 @@
+apiUrl = Env::get('cozeAi.api_url');
+ $this->accessToken = Env::get('cozeAi.token');
+
+ // 设置请求头
+ $this->headers = [
+ 'Authorization: Bearer ' . $this->accessToken,
+ 'Content-Type: application/json'
+ ];
+ }
+
+
+
+/**
+ * CURL请求
+ *
+ * @param $url 请求url地址
+ * @param $method 请求方法 get post
+ * @param null $postfields post数据数组
+ * @param array $headers 请求header信息
+ * @param bool|false $debug 调试开启 默认false
+ * @return mixed
+ */
+ protected function httpRequest($url, $method = "GET", $postfields = null, $headers = array(), $timeout = 30, $debug = false)
+ {
+ $method = strtoupper($method);
+ $ci = curl_init();
+ /* Curl settings */
+ // curl_setopt($ci, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); // 使用哪个版本
+ curl_setopt($ci, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0");
+ curl_setopt($ci, CURLOPT_CONNECTTIMEOUT, 60); /* 在发起连接前等待的时间,如果设置为0,则无限等待 */
+ // curl_setopt($ci, CURLOPT_TIMEOUT, 7); /* 设置cURL允许执行的最长秒数 */
+ curl_setopt($ci, CURLOPT_TIMEOUT, $timeout); /* 设置cURL允许执行的最长秒数 */
+ curl_setopt($ci, CURLOPT_RETURNTRANSFER, true);
+ switch ($method) {
+ case "POST":
+ curl_setopt($ci, CURLOPT_POST, true);
+ if (!empty($postfields)) {
+ if (is_string($postfields) && preg_match('/^([\w\-]+=[\w\-]+(&[\w\-]+=[\w\-]+)*)$/', $postfields)) {
+ parse_str($postfields, $output);
+ $postfields = $output;
+ }
+ if (is_array($postfields)) {
+ $tmpdatastr = http_build_query($postfields);
+ } else {
+ $tmpdatastr = $postfields;
+ }
+ curl_setopt($ci, CURLOPT_POSTFIELDS, $tmpdatastr);
+ }
+ break;
+ default:
+ curl_setopt($ci, CURLOPT_CUSTOMREQUEST, $method); /* //设置请求方式 */
+ break;
+ }
+
+ $ssl = preg_match('/^https:\/\//i', $url) ? TRUE : FALSE;
+ curl_setopt($ci, CURLOPT_URL, $url);
+ if ($ssl) {
+ curl_setopt($ci, CURLOPT_SSL_VERIFYPEER, FALSE); // https请求 不验证证书和hosts
+ curl_setopt($ci, CURLOPT_SSL_VERIFYHOST, FALSE); // 不从证书中检查SSL加密算法是否存在
+ // curl_setopt($ci, CURLOPT_SSLVERSION, 4); //因为之前的POODLE 病毒爆发,许多网站禁用了sslv3(nginx默认是禁用的,ssl_protocols 默认值为TLSv1 TLSv1.1 TLSv1.2;),最新使用sslv4
+ }
+ //curl_setopt($ci, CURLOPT_HEADER, true); /*启用时会将头文件的信息作为数据流输出*/
+ if (ini_get('open_basedir') == '' && ini_get('safe_mode' == 'Off')) {
+ curl_setopt($ci, CURLOPT_FOLLOWLOCATION, 1);
+ }
+ curl_setopt($ci, CURLOPT_MAXREDIRS, 2);/*指定最多的HTTP重定向的数量,这个选项是和CURLOPT_FOLLOWLOCATION一起使用的*/
+ curl_setopt($ci, CURLOPT_HTTPHEADER, $headers);
+ curl_setopt($ci, CURLINFO_HEADER_OUT, true);
+ /*curl_setopt($ci, CURLOPT_COOKIE, $Cookiestr); * *COOKIE带过去** */
+ $response = curl_exec($ci);
+ $requestinfo = curl_getinfo($ci);
+ $http_code = curl_getinfo($ci, CURLINFO_HTTP_CODE);
+ if ($debug) {
+ echo "=====post data======\r\n";
+ var_dump($postfields);
+ echo "=====info===== \r\n";
+ print_r($requestinfo);
+ echo "=====response=====\r\n";
+ print_r($response);
+ }
+ curl_close($ci);
+ return $response;
+ //return array($http_code, $response,$requestinfo);
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/application/cozeai/controller/ConversationController.php b/application/cozeai/controller/ConversationController.php
new file mode 100644
index 0000000..8882113
--- /dev/null
+++ b/application/cozeai/controller/ConversationController.php
@@ -0,0 +1,413 @@
+find();
+ $meta_data = $conversation['meta_data'] ?? [];
+ if (!$exists) {
+ // 不存在则插入
+ $data = [
+ 'conversation_id' => $conversation['id'],
+ 'bot_id' => $bot_id,
+ 'created_at' => $conversation['created_at'],
+ 'meta_data' => $meta_data,
+ 'create_time' => time(),
+ 'update_time' => time()
+ ];
+
+ if(isset($meta_data['uid']) && !empty($meta_data['uid'])){
+ $data['userId'] = $meta_data['uid'];
+ }
+
+ if(isset($meta_data['companyId']) && !empty($meta_data['companyId'])){
+ $data['companyId'] = $meta_data['companyId'];
+ }
+ return ConversationModel::create($data);
+ } else {
+ // 存在则更新
+ return $exists->save([
+ 'meta_data' => json_encode($meta_data),
+ 'update_time' => time()
+ ]);
+ }
+ }
+
+ /**
+ * 获取会话列表
+ */
+ public function list()
+ {
+ try {
+ $bot_id = input('bot_id','');
+ if(empty($bot_id)){
+ if($is_internal){
+ return json_encode([
+ 'code' => 400,
+ 'msg' => '智能体ID不能为空',
+ 'data' => []
+ ]);
+ }else{
+ return errorJson('智能体ID不能为空');
+ }
+ }
+ $page = input('page',1);
+ $limit = input('limit',20);
+
+ $params = [
+ 'bot_id' => $bot_id,
+ 'page_num' => $page,
+ 'page_size' => $limit,
+ 'sort_order' => 'desc'
+ ];
+
+ $result = requestCurl($this->apiUrl . "/v1/conversations", $params, 'GET', $this->headers);
+ $result = json_decode($result, true);
+
+ if ($result['code'] != 0) {
+ if($is_internal){
+ return json_encode([
+ 'code' => $result['code'],
+ 'msg' => $result['msg'],
+ 'data' => []
+ ]);
+ }else{
+ return errorJson($result['msg'], $result['code']);
+ }
+ }
+
+ // 处理返回的数据并存入数据库
+ if (!empty($result['data']['conversations'])) {
+ foreach ($result['data']['conversations'] as $item) {
+ $this->saveConversation($item, $bot_id);
+ }
+ }
+
+ return successJson($result['data'], '获取成功');
+
+ } catch (\Exception $e) {
+ return errorJson('获取对话列表失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 创建会话
+ */
+ public function create($is_internal = false)
+ {
+ try {
+ $bot_id = Env::get('cozeAi.bot_id');
+ $userInfo = request()->userInfo;
+ $uid = $userInfo['id'];
+ $companyId = $userInfo['companyId'];
+
+ if(empty($bot_id)){
+ if($is_internal){
+ return json_encode([
+ 'code' => 400,
+ 'msg' => '智能体ID不能为空',
+ 'data' => []
+ ]);
+ }else{
+ return errorJson('智能体ID不能为空');
+ }
+ }
+
+ // 构建元数据和消息
+ $meta_data = [
+ 'uid' => strval($uid),
+ 'companyId' => strval($companyId),
+ ];
+ $messages[] = [
+ 'role' => 'assistant',
+ 'content' => Env::get('cozeAi.content'),
+ 'type' => 'answer',
+ 'content_type' => 'text',
+ ];
+
+ $params = [
+ 'bot_id' => strval($bot_id),
+ 'meta_data' => $meta_data,
+ 'messages' => $messages,
+ ];
+ $url = $this->apiUrl . '/v1/conversation/create';
+ $result = $this->httpRequest($url, 'POST', json_encode($params,256), $this->headers);
+ $result = json_decode($result, true);
+ if ($result['code'] != 0) {
+ if($is_internal){
+ return json_encode([
+ 'code' => $result['code'],
+ 'msg' => $result['msg'],
+ 'data' => []
+ ]);
+ }else{
+ return errorJson($result['msg'], $result['code']);
+ }
+ }
+
+ // 获取返回的对话数据并保存
+ $conversation = $result['data'] ?? [];
+ if (!empty($conversation)) {
+ $this->saveConversation($conversation, $bot_id);
+
+
+ // 保存用户发送的消息
+ $userMessageData = [
+ 'chat_id' => $conversation['id'],
+ 'conversation_id' => $conversation['id'],
+ 'bot_id' => $bot_id,
+ 'content' => Env::get('cozeAi.content'),
+ 'content_type' => 'text',
+ 'role' => 'assistant',
+ 'type' => 'answer',
+ 'created_at' => time(),
+ 'updated_at' => time()
+ ];
+ MessageModel::create($userMessageData);
+
+ }
+
+ if($is_internal){
+ return json_encode([
+ 'code' => 200,
+ 'data' => $conversation,
+ 'msg' => '创建成功'
+ ]);
+ }else{
+ return successJson($conversation, '创建成功');
+ }
+
+ } catch (\Exception $e) {
+ if($is_internal){
+ return json_encode([
+ 'code' => 500,
+ 'msg' => '创建对话失败:' . $e->getMessage(),
+ 'data' => []
+ ]);
+ }else{
+ return errorJson('创建对话失败:' . $e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * 创建对话
+ */
+ public function createChat()
+ {
+ try {
+ $bot_id = Env::get('cozeAi.bot_id');
+ $conversation_id = input('conversation_id','');
+ $question = input('question','');
+
+ if(empty($bot_id)){
+ return errorJson('智能体ID不能为空');
+ }
+
+ if(empty($conversation_id)){
+ return errorJson('会话ID不能为空');
+ }
+
+ if(empty($question)){
+ return errorJson('问题不能为空');
+ }
+
+ $userInfo = request()->userInfo;
+ $uid = $userInfo['id'];
+ $companyId = $userInfo['companyId'];
+
+ // 构建请求数据
+ $params = [
+ 'bot_id' => strval($bot_id),
+ 'user_id' => strval($uid),
+ 'additional_messages' => [
+ [
+ 'role' => 'user',
+ 'content' => $question,
+ 'type' => 'question',
+ 'content_type' => 'text'
+ ]
+ ],
+ 'stream' => false,
+ 'auto_save_history' => true
+ ];
+
+ $url = $this->apiUrl . '/v3/chat?conversation_id='.$conversation_id;
+ $result = $this->httpRequest($url, 'POST', json_encode($params,256), $this->headers);
+ $result = json_decode($result, true);
+ if ($result['code'] != 0) {
+ return errorJson($result['msg'], $result['code']);
+ }
+
+ // 保存用户发送的消息
+ $userMessageData = [
+ 'chat_id' => $result['data']['id'],
+ 'conversation_id' => $conversation_id,
+ 'bot_id' => $bot_id,
+ 'content' => $question,
+ 'content_type' => 'text',
+ 'role' => 'user',
+ 'type' => 'question',
+ 'created_at' => $result['data']['created_at'],
+ 'updated_at' => $result['data']['created_at']
+ ];
+ MessageModel::create($userMessageData);
+
+
+ return successJson($result['data'], '发送成功');
+
+ } catch (\Exception $e) {
+ return errorJson('创建对话失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 查看对话详情
+ */
+ public function chatRetrieve()
+ {
+ $conversation_id = input('conversation_id','');
+ $chat_id = input('chat_id','');
+ if(empty($conversation_id) && empty($chat_id)){
+ return errorJson('参数缺失');
+ }
+ $conversation = ConversationModel::where('conversation_id', $conversation_id)->find();
+ if(empty($conversation)){
+ return errorJson('会话不存在');
+ }
+
+ $params = [
+ 'conversation_id' => $conversation_id,
+ 'chat_id' => $chat_id
+ ];
+
+ $url = $this->apiUrl . '/v3/chat/retrieve?' . dataBuild($params);
+
+ $result = $this->httpRequest($url, 'GET', [], $this->headers);
+ $result = json_decode($result, true);
+
+
+ if ($result['code'] != 0) {
+ return errorJson($result['msg'], $result['code']);
+ }
+ $status = [
+ 'created' => '对话已创建',
+ 'in_progress' => '智能体正在处理中',
+ 'completed' => '智能体已完成处理,本次对话结束',
+ 'failed' => '对话失败',
+ 'requires_action' => '对话中断,需要进一步处理',
+ 'canceled' => '对话已取消',
+ ];
+
+ $status_msg = $status[$result['data']['status']] ?? '未知状态';
+ if($result['data']['status'] == 'failed'){
+ $last_error = $result['data']['last_error'];
+ $error_msg = $status_msg . ',错误信息:' . $last_error['msg'];
+ return errorJson($error_msg);
+ }else{
+ return successJson($result['data'],$status_msg);
+ }
+ }
+
+
+
+ /**
+ * 获取对话消息详情
+ */
+ public function chatMessage(){
+ $conversation_id = input('conversation_id','');
+ $chat_id = input('chat_id','');
+ if(empty($conversation_id) && empty($chat_id)){
+ return errorJson('参数缺失');
+ }
+ $conversation = ConversationModel::where('conversation_id', $conversation_id)->find();
+ if(empty($conversation)){
+ return errorJson('会话不存在');
+ }
+
+ $params = [
+ 'conversation_id' => $conversation_id,
+ 'chat_id' => $chat_id
+ ];
+ $url = $this->apiUrl . '/v3/chat/message/list?' . dataBuild($params);
+ $result = $this->httpRequest($url, 'GET', [], $this->headers);
+ $result = json_decode($result, true);
+
+ if ($result['code'] != 0) {
+ return errorJson($result['msg'], $result['code']);
+ }
+
+ $data = $result['data'];
+ $list = [];
+ foreach($data as $item){
+ if($item['type'] == 'answer'){
+ $timestamp = $item['updated_at'];
+ $now = time();
+ $today = strtotime(date('Y-m-d'));
+ $yesterday = strtotime('-1 day', $today);
+ $thisYear = strtotime(date('Y-01-01'));
+
+ // 格式化时间
+ if($timestamp >= $today) {
+ $time = date('H:i', $timestamp);
+ } elseif($timestamp >= $yesterday) {
+ $time = '昨天 ' . date('H:i', $timestamp);
+ } elseif($timestamp >= $thisYear) {
+ $time = date('m-d H:i', $timestamp);
+ } else {
+ $time = date('Y-m-d H:i', $timestamp);
+ }
+
+ // 保存消息记录
+ $messageData = [
+ 'chat_id' => $item['id'],
+ 'conversation_id' => $conversation_id,
+ 'bot_id' => $item['bot_id'],
+ 'content' => $item['content'],
+ 'content_type' => $item['content_type'],
+ 'role' => $item['role'],
+ 'type' => $item['type'],
+ 'created_at' => $item['created_at'],
+ 'updated_at' => $item['updated_at']
+ ];
+
+ // 检查消息是否已存在
+ $exists = MessageModel::where('chat_id', $item['id'])->find();
+ if (!$exists) {
+ MessageModel::create($messageData);
+ }
+
+ $list = [
+ 'id' => $item['id'],
+ 'type' => 'assistant',
+ 'content' => $item['content'],
+ 'time' => $time
+ ];
+ break;
+ }
+ }
+
+ return successJson($list, '获取成功');
+ }
+}
\ No newline at end of file
diff --git a/application/cozeai/controller/MessageController.php b/application/cozeai/controller/MessageController.php
new file mode 100644
index 0000000..8c3fbd1
--- /dev/null
+++ b/application/cozeai/controller/MessageController.php
@@ -0,0 +1,119 @@
+userInfo;
+ $uid = $userInfo['id'];
+ $companyId = $userInfo['companyId'];
+
+ // 获取会话ID
+ $conversation_id = input('conversation_id', '');
+
+ // 如果没有传入会话ID,则查询用户最新的会话
+ if (empty($conversation_id)) {
+ // 查询用户是否有会话记录
+ $conversation = ConversationModel::where([
+ ['userId', '=', $uid],
+ ['companyId', '=', $companyId]
+ ])->order('create_time', 'desc')->find();
+
+ // 如果没有会话记录,创建新会话
+ if (empty($conversation)) {
+ $conversationController = new ConversationController();
+ $result = $conversationController->create(true);
+ $resultData = json_decode($result, true);
+ if ($resultData['code'] != 200) {
+ return errorJson('创建会话失败:' . $resultData['msg']);
+ }
+ $conversation_id = $resultData['data']['id'];
+ } else {
+ $conversation_id = $conversation['conversation_id'];
+ }
+ } else {
+ // 验证会话是否属于当前用户
+ $conversation = ConversationModel::where([
+ ['conversation_id', '=', $conversation_id],
+ ['userId', '=', $uid],
+ ['companyId', '=', $companyId]
+ ])->find();
+
+ if (empty($conversation)) {
+ return errorJson('会话不存在或无权访问');
+ }
+ }
+
+
+
+ // 分页参数
+ $page = input('page', 1);
+ $limit = input('limit', 20);
+
+ // 查询消息记录
+ $messages = MessageModel::where('conversation_id', $conversation_id)
+ ->order('id', 'DESC')
+ ->page($page, $limit)
+ ->select()
+ ->each(function($item) {
+ // 格式化时间显示
+ $timestamp = $item['created_at'];
+ $today = strtotime(date('Y-m-d'));
+ $yesterday = strtotime('-1 day', $today);
+ $thisYear = strtotime(date('Y-01-01'));
+
+ if($timestamp >= $today) {
+ $item['show_time'] = date('H:i', $timestamp);
+ } elseif($timestamp >= $yesterday) {
+ $item['show_time'] = '昨天 ' . date('H:i', $timestamp);
+ } elseif($timestamp >= $thisYear) {
+ $item['show_time'] = date('m-d H:i', $timestamp);
+ } else {
+ $item['show_time'] = date('Y-m-d H:i', $timestamp);
+ }
+
+ // 根据role设置type
+ if ($item['role'] == 'assistant') {
+ $item['type'] = 'assistant';
+ } else {
+ $item['type'] = 'user';
+ }
+ unset($item['role']);
+
+ return $item;
+ });
+
+ // 对消息进行倒序处理
+ $messages = array_reverse($messages->toArray());
+
+ // 获取总记录数
+ $total = MessageModel::where('conversation_id', $conversation_id)->count();
+
+ $data = [
+ 'list' => $messages,
+ 'total' => $total,
+ 'conversation_id' => $conversation_id
+ ];
+
+ return successJson($data, '获取成功');
+
+ } catch (\Exception $e) {
+ return errorJson('获取对话记录失败:' . $e->getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cozeai/controller/WorkspaceController.php b/application/cozeai/controller/WorkspaceController.php
new file mode 100644
index 0000000..133563a
--- /dev/null
+++ b/application/cozeai/controller/WorkspaceController.php
@@ -0,0 +1,67 @@
+ $page,
+ 'page_size' => $limit
+ ];
+
+ $result =requestCurl($this->apiUrl . '/v1/workspaces', $params, 'GET', $this->headers);
+ $result = json_decode($result, true);
+ if ($result['code'] != 0) {
+ return errorJson($result['msg'],$result['code']);
+ }
+
+ return successJson($result['data'], '获取成功');
+ } catch (\Exception $e) {
+ return errorJson('获取工作区列表失败:' . $e->getMessage());
+ }
+ }
+
+
+
+ /**
+ * 获取智能体列表
+ */
+ public function getBotsList()
+ {
+ try {
+ $space_id = input('space_id','');
+ if(empty($space_id)){
+ return errorJson('Space ID不能为空');
+ }
+ $page = input('page',1);
+ $limit = input('limit',20);
+
+ $params = [
+ 'space_id' => $space_id,
+ 'page_index' => $page,
+ 'page_size' => $limit
+ ];
+
+ $result = requestCurl($this->apiUrl . '/v1/space/published_bots_list', $params, 'GET', $this->headers);
+ $result = json_decode($result, true);
+ if ($result['code'] != 0) {
+ return errorJson($result['msg'],$result['code']);
+ }
+ return successJson($result['data'], '获取成功');
+ } catch (\Exception $e) {
+ return errorJson('获取智能体列表失败:'.$e->getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cozeai/model/Conversation.php b/application/cozeai/model/Conversation.php
new file mode 100644
index 0000000..fd563f8
--- /dev/null
+++ b/application/cozeai/model/Conversation.php
@@ -0,0 +1,60 @@
+ 'int',
+ 'conversation_id' => 'string',
+ 'workspace_id' => 'string',
+ 'bot_id' => 'string',
+ 'title' => 'string',
+ 'create_time' => 'datetime',
+ 'update_time' => 'datetime'
+ ];
+
+ // 自动写入时间戳
+ protected $autoWriteTimestamp = true;
+
+ /**
+ * 根据对话ID获取对话信息
+ */
+ public function getByConversationId($conversationId)
+ {
+ return $this->where('conversation_id', $conversationId)->find();
+ }
+
+ /**
+ * 保存对话信息
+ */
+ public function saveConversation($data)
+ {
+ $conversation = $this->getByConversationId($data['conversation_id']);
+ if ($conversation) {
+ return $this->where('conversation_id', $data['conversation_id'])->update($data);
+ } else {
+ return $this->save($data);
+ }
+ }
+
+ /**
+ * 删除对话
+ */
+ public function deleteConversation($conversationId)
+ {
+ return $this->where('conversation_id', $conversationId)->delete();
+ }
+}
\ No newline at end of file
diff --git a/application/cozeai/model/Message.php b/application/cozeai/model/Message.php
new file mode 100644
index 0000000..4befe5a
--- /dev/null
+++ b/application/cozeai/model/Message.php
@@ -0,0 +1,24 @@
+ 'integer',
+ 'updated_at' => 'integer',
+ 'create_time' => 'integer',
+ 'update_time' => 'integer'
+ ];
+}
\ No newline at end of file
diff --git a/application/cozeai/model/Workspace.php b/application/cozeai/model/Workspace.php
new file mode 100644
index 0000000..fbf838a
--- /dev/null
+++ b/application/cozeai/model/Workspace.php
@@ -0,0 +1,64 @@
+ 'int',
+ 'workspace_id' => 'string',
+ 'name' => 'string',
+ 'description' => 'string',
+ 'create_time' => 'datetime',
+ 'update_time' => 'datetime'
+ ];
+
+ // 自动写入时间戳
+ protected $autoWriteTimestamp = true;
+ protected $createTime = 'create_time';
+ protected $updateTime = 'update_time';
+
+ /**
+ * 根据工作区ID获取工作区信息
+ */
+ public function getByWorkspaceId($workspaceId)
+ {
+ return $this->where('workspace_id', $workspaceId)->find();
+ }
+
+ /**
+ * 保存工作区信息
+ */
+ public function saveWorkspace($data)
+ {
+ $workspace = $this->getByWorkspaceId($data['workspace_id']);
+
+ if ($workspace) {
+ // 更新
+ return $this->where('workspace_id', $data['workspace_id'])->update($data);
+ } else {
+ // 新增
+ return $this->save($data);
+ }
+ }
+
+ /**
+ * 删除工作区
+ */
+ public function deleteWorkspace($workspaceId)
+ {
+ return $this->where('workspace_id', $workspaceId)->delete();
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/config/route.php b/application/cunkebao/config/route.php
new file mode 100644
index 0000000..07a9e8f
--- /dev/null
+++ b/application/cunkebao/config/route.php
@@ -0,0 +1,275 @@
+middleware(['jwt']);
+
+
+
+
+Route::group('v1/api/scenarios', function () {
+ Route::any('', 'app\cunkebao\controller\plan\PostExternalApiV1Controller@index');
+});
+
+
+//小程序
+Route::group('v1/frontend', function () {
+ Route::group('business/poster', function () {
+ Route::post('getone', 'app\cunkebao\controller\plan\PosterWeChatMiniProgram@getPosterTaskData');
+ Route::post('decryptphone', 'app\cunkebao\controller\plan\PosterWeChatMiniProgram@getPhoneNumber');
+ //Route::post('decryptphones', 'app\cunkebao\controller\plan\PosterWeChatMiniProgram@decryptphones');
+ });
+ Route::post('business/form/importsave', 'app\cunkebao\controller\plan\PosterWeChatMiniProgram@decryptphones');
+
+ // 分销渠道注册(H5扫码)
+ Route::group('distribution/channel', function () {
+ Route::get('register', 'app\cunkebao\controller\distribution\ChannelController@registerByQrCode'); // H5页面(GET显示表单)
+ Route::post('register', 'app\cunkebao\controller\distribution\ChannelController@registerByQrCode'); // 提交渠道信息(POST)
+ });
+
+ // 分销渠道用户端(无需JWT认证,通过渠道编码访问)
+ Route::group('distribution/user', function () {
+ Route::post('login', 'app\cunkebao\controller\distribution\ChannelUserController@login'); // 渠道登录
+ Route::get('home', 'app\cunkebao\controller\distribution\ChannelUserController@index'); // 获取渠道首页数据
+ Route::get('revenue-records', 'app\cunkebao\controller\distribution\ChannelUserController@revenueRecords'); // 获取收益明细列表
+ Route::get('withdrawal-records', 'app\cunkebao\controller\distribution\ChannelUserController@withdrawalRecords'); // 获取提现明细列表
+ Route::post('change-password', 'app\cunkebao\controller\distribution\ChannelUserController@changePassword'); // 修改密码
+ });
+});
+
+
+
+
+return [];
\ No newline at end of file
diff --git a/application/cunkebao/controller/AiKnowledgeBaseController.php b/application/cunkebao/controller/AiKnowledgeBaseController.php
new file mode 100644
index 0000000..06a95c8
--- /dev/null
+++ b/application/cunkebao/controller/AiKnowledgeBaseController.php
@@ -0,0 +1,707 @@
+getUserInfo('companyId');
+ if (empty($companyId)) {
+ return ResponseHelper::error('公司信息获取失败');
+ }
+
+ // 获取分页参数
+ $page = $this->request->param('page', 1);
+ $pageSize = $this->request->param('pageSize', 20);
+ $includeSystem = $this->request->param('includeSystem', 1); // 是否包含系统类型
+
+ // 构建查询条件
+ $where = [['isDel', '=', 0]];
+
+ if ($includeSystem == 1) {
+ // 包含系统类型和本公司创建的类型
+ $where[] = ['companyId', 'in', [$companyId, 0]];
+ } else {
+ // 只显示本公司创建的类型
+ $where[] = ['companyId', '=', $companyId];
+ $where[] = ['type', '=', AiKnowledgeBaseType::TYPE_USER];
+ }
+
+ // 统计开启的类型总数
+ $enabledCountWhere = $where;
+ $enabledCountWhere[] = ['status', '=', 1];
+ $enabledCount = AiKnowledgeBaseType::where($enabledCountWhere)->count();
+
+ // 查询数据
+ $list = AiKnowledgeBaseType::where($where)
+ ->order('type', 'asc') // 系统类型排在前面
+ ->order('createTime', 'desc')
+ ->paginate($pageSize, false, ['page' => $page]);
+
+ // 为每个类型添加素材数量统计
+ $listData = $list->toArray();
+ foreach ($listData['data'] as &$item) {
+ // 统计该类型下的知识库数量(素材数量)
+ $item['materialCount'] = AiKnowledgeBase::where([
+ ['typeId', '=', $item['id']],
+ ['isDel', '=', 0]
+ ])->count();
+ }
+
+ // 重新构造返回数据
+ $result = [
+ 'total' => $listData['total'],
+ 'data' => $listData['data'],
+ 'enabledCount' => $enabledCount, // 开启的类型总数
+ ];
+
+ return ResponseHelper::success($result, '获取成功');
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统异常:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 添加知识库类型
+ *
+ * @return \think\response\Json
+ */
+ public function addType()
+ {
+ try {
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($companyId)) {
+ return ResponseHelper::error('公司信息获取失败');
+ }
+
+ // 获取参数
+ $name = $this->request->param('name', '');
+ $description = $this->request->param('description', '');
+ $label = $this->request->param('label', []);
+ $prompt = $this->request->param('prompt', '');
+ $status = $this->request->param('status', 1); // 默认启用
+
+ // 参数验证
+ if (empty($name)) {
+ return ResponseHelper::error('类型名称不能为空');
+ }
+
+ // 检查名称是否重复
+ $exists = AiKnowledgeBaseType::where([
+ ['companyId', '=', $companyId],
+ ['name', '=', $name],
+ ['isDel', '=', 0]
+ ])->find();
+
+ if ($exists) {
+ return ResponseHelper::error('该类型名称已存在');
+ }
+
+ // 创建类型
+ $typeModel = new AiKnowledgeBaseType();
+ $data = [
+ 'type' => AiKnowledgeBaseType::TYPE_USER,
+ 'name' => $name,
+ 'description' => $description,
+ 'label' => json_encode($label,256),
+ 'prompt' => $prompt,
+ 'status' => $status,
+ 'companyId' => $companyId,
+ 'userId' => $userId,
+ 'createTime' => time(),
+ 'updateTime' => time(),
+ 'isDel' => 0
+ ];
+
+ if ($typeModel->save($data)) {
+ return ResponseHelper::success(['id' => $typeModel->id], '添加成功');
+ } else {
+ return ResponseHelper::error('添加失败');
+ }
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统异常:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 编辑知识库类型
+ *
+ * @return \think\response\Json
+ */
+ public function editType()
+ {
+ try {
+ $companyId = $this->getUserInfo('companyId');
+ if (empty($companyId)) {
+ return ResponseHelper::error('公司信息获取失败');
+ }
+
+ // 获取参数
+ $id = $this->request->param('id', 0);
+ $name = $this->request->param('name', '');
+ $description = $this->request->param('description', '');
+ $label = $this->request->param('label', []);
+ $prompt = $this->request->param('prompt', '');
+ $status = $this->request->param('status', '');
+
+ // 参数验证
+ if (empty($id)) {
+ return ResponseHelper::error('类型ID不能为空');
+ }
+
+ if (empty($name)) {
+ return ResponseHelper::error('类型名称不能为空');
+ }
+
+ // 查找类型
+ $typeModel = AiKnowledgeBaseType::where([
+ ['id', '=', $id],
+ ['isDel', '=', 0]
+ ])->find();
+
+ if (!$typeModel) {
+ return ResponseHelper::error('类型不存在');
+ }
+
+ // 检查是否为系统类型
+ if ($typeModel->isSystemType()) {
+ return ResponseHelper::error('系统类型不允许编辑');
+ }
+
+ // 检查权限(只能编辑本公司的类型)
+ if ($typeModel->companyId != $companyId) {
+ return ResponseHelper::error('无权限编辑该类型');
+ }
+
+ // 检查名称是否重复(排除自己)
+ $exists = AiKnowledgeBaseType::where([
+ ['companyId', '=', $companyId],
+ ['name', '=', $name],
+ ['id', '<>', $id],
+ ['isDel', '=', 0]
+ ])->find();
+
+ if ($exists) {
+ return ResponseHelper::error('该类型名称已存在');
+ }
+
+ // 更新数据
+ $typeModel->name = $name;
+ $typeModel->description = $description;
+ $typeModel->label = json_encode($label,256);
+ $typeModel->prompt = $prompt;
+ if ($status !== '') {
+ $typeModel->status = $status;
+ }
+ $typeModel->updateTime = time();
+
+ if ($typeModel->save()) {
+ return ResponseHelper::success([], '更新成功');
+ } else {
+ return ResponseHelper::error('更新失败');
+ }
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统异常:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 修改知识库类型状态
+ *
+ * @return \think\response\Json
+ */
+ public function updateTypeStatus()
+ {
+ try {
+ $companyId = $this->getUserInfo('companyId');
+ if (empty($companyId)) {
+ return ResponseHelper::error('公司信息获取失败');
+ }
+
+ // 获取参数
+ $id = $this->request->param('id', 0);
+ $status = $this->request->param('status', -1);
+
+ // 参数验证
+ if (empty($id)) {
+ return ResponseHelper::error('类型ID不能为空');
+ }
+
+ if ($status != 0 && $status != 1) {
+ return ResponseHelper::error('状态参数错误');
+ }
+
+ // 查找类型
+ $typeModel = AiKnowledgeBaseType::where([
+ ['id', '=', $id],
+ ['isDel', '=', 0]
+ ])->find();
+
+ if (!$typeModel) {
+ return ResponseHelper::error('类型不存在');
+ }
+
+ // 检查是否为系统类型
+ if ($typeModel->isSystemType()) {
+ return ResponseHelper::error('系统类型不允许修改状态');
+ }
+
+ // 检查权限(只能修改本公司的类型)
+ if ($typeModel->companyId != $companyId) {
+ return ResponseHelper::error('无权限修改该类型');
+ }
+
+ // 更新状态
+ $typeModel->status = $status;
+ $typeModel->updateTime = time();
+
+ if ($typeModel->save()) {
+ $message = $status == 0 ? '禁用成功' : '启用成功';
+ return ResponseHelper::success([], $message);
+ } else {
+ return ResponseHelper::error('操作失败');
+ }
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统异常:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 获取知识库类型详情
+ *
+ * @return \think\response\Json
+ */
+ public function detailType()
+ {
+ try {
+ $companyId = $this->getUserInfo('companyId');
+ if (empty($companyId)) {
+ return ResponseHelper::error('公司信息获取失败');
+ }
+
+ // 获取参数
+ $id = $this->request->param('id', 0);
+
+ // 参数验证
+ if (empty($id)) {
+ return ResponseHelper::error('类型ID不能为空');
+ }
+
+ // 查找类型
+ $typeModel = AiKnowledgeBaseType::where([
+ ['id', '=', $id],
+ ['isDel', '=', 0]
+ ])->find();
+
+ if (!$typeModel) {
+ return ResponseHelper::error('类型不存在');
+ }
+
+ // 检查权限(系统类型或本公司的类型都可以查看)
+ if ($typeModel->companyId != 0 && $typeModel->companyId != $companyId) {
+ return ResponseHelper::error('无权限查看该类型');
+ }
+
+ return ResponseHelper::success($typeModel, '获取成功');
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统异常:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 删除知识库类型
+ *
+ * @return \think\response\Json
+ */
+ public function deleteType()
+ {
+ try {
+ $companyId = $this->getUserInfo('companyId');
+ if (empty($companyId)) {
+ return ResponseHelper::error('公司信息获取失败');
+ }
+
+ // 获取参数
+ $id = $this->request->param('id', 0);
+
+ // 参数验证
+ if (empty($id)) {
+ return ResponseHelper::error('类型ID不能为空');
+ }
+
+ // 查找类型
+ $typeModel = AiKnowledgeBaseType::where([
+ ['id', '=', $id],
+ ['isDel', '=', 0]
+ ])->find();
+
+ if (!$typeModel) {
+ return ResponseHelper::error('类型不存在');
+ }
+
+ // 检查是否为系统类型
+ if ($typeModel->isSystemType()) {
+ return ResponseHelper::error('系统类型不允许删除');
+ }
+
+ // 检查权限(只能删除本公司的类型)
+ if ($typeModel->companyId != $companyId) {
+ return ResponseHelper::error('无权限删除该类型');
+ }
+
+ // 检查是否有关联的知识库
+ $hasKnowledge = AiKnowledgeBase::where([
+ ['typeId', '=', $id],
+ ['isDel', '=', 0]
+ ])->count();
+
+ if ($hasKnowledge > 0) {
+ return ResponseHelper::error('该类型下存在知识库,无法删除');
+ }
+
+ // 软删除
+ $typeModel->isDel = 1;
+ $typeModel->delTime = time();
+
+ if ($typeModel->save()) {
+ return ResponseHelper::success([], '删除成功');
+ } else {
+ return ResponseHelper::error('删除失败');
+ }
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统异常:' . $e->getMessage());
+ }
+ }
+
+ // ==================== 知识库管理 ====================
+
+ /**
+ * 获取知识库列表
+ *
+ * @return \think\response\Json
+ */
+ public function getList()
+ {
+ try {
+ $companyId = $this->getUserInfo('companyId');
+ if (empty($companyId)) {
+ return ResponseHelper::error('公司信息获取失败');
+ }
+
+ // 获取分页参数
+ $page = $this->request->param('page', 1);
+ $pageSize = $this->request->param('pageSize', 20);
+ $typeId = $this->request->param('typeId', 0); // 类型筛选
+ $keyword = $this->request->param('keyword', ''); // 关键词搜索
+
+ // 构建查询条件
+ $where = [
+ ['isDel', '=', 0],
+ ['companyId', '=', $companyId]
+ ];
+
+ if ($typeId > 0) {
+ $where[] = ['typeId', '=', $typeId];
+ }
+
+ if (!empty($keyword)) {
+ $where[] = ['name', 'like', '%' . $keyword . '%'];
+ }
+
+ // 查询数据
+ $list = AiKnowledgeBase::where($where)
+ ->with(['type'])
+ ->order('createTime', 'desc')
+ ->paginate($pageSize, false, ['page' => $page]);
+
+ foreach ($list as &$v){
+ $v['size'] = 0;
+ }
+ unset($v);
+
+ return ResponseHelper::success($list, '获取成功');
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统异常:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 添加知识库
+ *
+ * @return \think\response\Json
+ */
+ public function add()
+ {
+ try {
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($companyId)) {
+ return ResponseHelper::error('公司信息获取失败');
+ }
+
+ $datasetId = AiSettingsModel::where(['companyId' => $companyId])->value('datasetId');
+
+
+ // 获取参数
+ $typeId = $this->request->param('typeId', 0);
+ $name = $this->request->param('name', '');
+ $label = $this->request->param('label', []);
+ $fileUrl = $this->request->param('fileUrl', '');
+
+ // 参数验证
+ if (empty($typeId)) {
+ return ResponseHelper::error('请选择知识库类型');
+ }
+
+ if (empty($name)) {
+ return ResponseHelper::error('知识库名称不能为空');
+ }
+
+ if (empty($fileUrl)) {
+ return ResponseHelper::error('文件地址不能为空');
+ }
+
+ // 检查类型是否存在
+ $typeExists = AiKnowledgeBaseType::where([
+ ['id', '=', $typeId],
+ ['isDel', '=', 0]
+ ])->find();
+
+ if (!$typeExists) {
+ return ResponseHelper::error('知识库类型不存在');
+ }
+
+ // 创建知识库
+ $knowledgeModel = new AiKnowledgeBase();
+ $data = [
+ 'typeId' => $typeId,
+ 'name' => $name,
+ 'label' => json_encode($label, 256),
+ 'fileUrl' => $fileUrl,
+ 'companyId' => $companyId,
+ 'userId' => $userId,
+ 'createTime' => time(),
+ 'updateTime' => time(),
+ 'isDel' => 0
+ ];
+
+ if ($knowledgeModel->save($data)) {
+ if (!empty($datasetId)) {
+ $createDocumentData = [
+ 'filePath' => $fileUrl,
+ 'fileName' => $name,
+ 'dataset_id' => $datasetId
+ ];
+ $cozeAI = new CozeAI();
+ $result = $cozeAI->createDocument($createDocumentData);
+ $result = json_decode($result, true);
+ if ($result['code'] == 200) {
+ $documentId = $result['data'][0]['document_id'];
+ AiKnowledgeBase::where('id', $knowledgeModel->id)->update(['documentId' => $documentId, 'updateTime' => time()]);
+ AiSettingsModel::where(['companyId' => $companyId])->update(['isRelease' => 0,'updateTime' => time()]);
+ }
+ }
+ return ResponseHelper::success(['id' => $knowledgeModel->id], '添加成功');
+ } else {
+ return ResponseHelper::error('添加失败');
+ }
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统异常:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 编辑知识库
+ *
+ * @return \think\response\Json
+ */
+ public function edit()
+ {
+ try {
+ $companyId = $this->getUserInfo('companyId');
+ if (empty($companyId)) {
+ return ResponseHelper::error('公司信息获取失败');
+ }
+
+ // 获取参数
+ $id = $this->request->param('id', 0);
+ $typeId = $this->request->param('typeId', 0);
+ $name = $this->request->param('name', '');
+ $label = $this->request->param('label', []);
+ $fileUrl = $this->request->param('fileUrl', '');
+
+ // 参数验证
+ if (empty($id)) {
+ return ResponseHelper::error('知识库ID不能为空');
+ }
+
+ if (empty($typeId)) {
+ return ResponseHelper::error('请选择知识库类型');
+ }
+
+ if (empty($name)) {
+ return ResponseHelper::error('知识库名称不能为空');
+ }
+
+ // 查找知识库
+ $knowledgeModel = AiKnowledgeBase::where([
+ ['id', '=', $id],
+ ['companyId', '=', $companyId],
+ ['isDel', '=', 0]
+ ])->find();
+
+ if (!$knowledgeModel) {
+ return ResponseHelper::error('知识库不存在或无权限编辑');
+ }
+
+ // 检查类型是否存在
+ $typeExists = AiKnowledgeBaseType::where([
+ ['id', '=', $typeId],
+ ['isDel', '=', 0]
+ ])->find();
+
+ if (!$typeExists) {
+ return ResponseHelper::error('知识库类型不存在');
+ }
+
+ // 更新数据
+ $knowledgeModel->typeId = $typeId;
+ $knowledgeModel->name = $name;
+ $knowledgeModel->label = json_encode($label, 256);
+ if (!empty($fileUrl)) {
+ $knowledgeModel->fileUrl = $fileUrl;
+ }
+ $knowledgeModel->updateTime = time();
+
+ if ($knowledgeModel->save()) {
+ return ResponseHelper::success([], '更新成功');
+ } else {
+ return ResponseHelper::error('更新失败');
+ }
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统异常:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 删除知识库
+ *
+ * @return \think\response\Json
+ */
+ public function delete()
+ {
+ try {
+ $companyId = $this->getUserInfo('companyId');
+ if (empty($companyId)) {
+ return ResponseHelper::error('公司信息获取失败');
+ }
+
+ // 获取参数
+ $id = $this->request->param('id', 0);
+
+ // 参数验证
+ if (empty($id)) {
+ return ResponseHelper::error('知识库ID不能为空');
+ }
+
+ // 查找知识库
+ $knowledgeModel = AiKnowledgeBase::where([
+ ['id', '=', $id],
+ ['companyId', '=', $companyId],
+ ['isDel', '=', 0]
+ ])->find();
+
+ if (!$knowledgeModel) {
+ return ResponseHelper::error('知识库不存在或无权限删除');
+ }
+
+ // 软删除
+ $knowledgeModel->isDel = 1;
+ $knowledgeModel->delTime = time();
+
+ if ($knowledgeModel->save()) {
+ if (!empty($knowledgeModel->documentId)){
+ $cozeAI = new CozeAI();
+ $cozeAI->deleteDocument([$knowledgeModel->documentId]);
+ AiSettingsModel::where(['companyId' => $companyId])->update(['isRelease' => 0,'updateTime' => time()]);
+ }
+ return ResponseHelper::success([], '删除成功');
+ } else {
+ return ResponseHelper::error('删除失败');
+ }
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统异常:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 获取知识库详情
+ *
+ * @return \think\response\Json
+ */
+ public function detail()
+ {
+ try {
+ $companyId = $this->getUserInfo('companyId');
+ if (empty($companyId)) {
+ return ResponseHelper::error('公司信息获取失败');
+ }
+
+ // 获取参数
+ $id = $this->request->param('id', 0);
+
+ // 参数验证
+ if (empty($id)) {
+ return ResponseHelper::error('知识库ID不能为空');
+ }
+
+ // 查找知识库
+ $knowledge = AiKnowledgeBase::where([
+ ['id', '=', $id],
+ ['companyId', '=', $companyId],
+ ['isDel', '=', 0]
+ ])->with(['type'])->find();
+
+ if (!$knowledge) {
+ return ResponseHelper::error('知识库不存在或无权限查看');
+ }
+
+ return ResponseHelper::success($knowledge, '获取成功');
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统异常:' . $e->getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/AiSettingsController.php b/application/cunkebao/controller/AiSettingsController.php
new file mode 100644
index 0000000..429410b
--- /dev/null
+++ b/application/cunkebao/controller/AiSettingsController.php
@@ -0,0 +1,433 @@
+getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($companyId)) {
+ return ResponseHelper::error('公司信息获取失败');
+ }
+
+ // 查找公司AI设置
+ $settings = $this->getOrCreateAiSettings($companyId, $userId);
+
+ if (!$settings) {
+ return ResponseHelper::error('AI设置初始化失败');
+ }
+
+ // 确保智能体已创建
+ if (empty($settings->botId)) {
+ $settings->releaseTime = 0;
+ $botCreated = $this->createBot($settings);
+ if (!$botCreated) {
+ return ResponseHelper::error('智能体创建失败');
+ }
+ }
+
+ // 确保知识库已创建
+ if (empty($settings->datasetId)) {
+ $settings->releaseTime = 0;
+ $knowledgeCreated = $this->createKnowledge($settings);
+ if (!$knowledgeCreated) {
+ return ResponseHelper::error('知识库创建失败');
+ }
+ }
+ if (!empty($settings->botId) && !empty($settings->datasetId) && $settings->releaseTime <= 0) {
+ $cozeAI = new CozeAI();
+ $config = json_decode($settings->config,true);
+ $config['bot_id'] = $settings->botId;
+ $config['dataset_ids'] = [$settings->datasetId];
+ $cozeAI->updateBot($config);
+ }
+
+ // 解析配置信息
+ $settings->config = json_decode($settings->config, true);
+
+ return ResponseHelper::success($settings, 'AI设置初始化成功');
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统异常:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 获取或创建AI设置
+ *
+ * @param int $companyId 公司ID
+ * @param int $userId 用户ID
+ * @return AiSettingsModel|false
+ */
+ private function getOrCreateAiSettings($companyId, $userId)
+ {
+ // 查找现有设置
+ $settings = AiSettingsModel::where(['companyId' => $companyId])->find();
+
+ if (empty($settings)) {
+ // 获取公司信息
+ $company = CompanyModel::where('id', $companyId)->find();
+ if (empty($company)) {
+ return false;
+ }
+
+ // 创建默认配置
+ $config = $this->getDefaultConfig($company['name']);
+
+ // 保存AI设置
+ $settings = $this->saveAiSettings($companyId, $userId, $config);
+ }
+
+ return $settings;
+ }
+
+ /**
+ * 获取默认AI配置
+ *
+ * @param string $companyName 公司名称
+ * @return array
+ */
+ private function getDefaultConfig($companyName)
+ {
+ return [
+ 'name' => $companyName,
+ 'model_id' => '1737521813', // 默认模型ID
+ 'prompt_info' => $this->getDefaultPrompt()
+ ];
+ }
+
+ /**
+ * 获取默认提示词
+ *
+ * @return string
+ */
+ private function getDefaultPrompt()
+ {
+ return '# 角色
+你是一位全能知识客服,作为专业的客服智能体,具备全面的知识储备,能够回答用户提出的各类问题。在回答问题前,会仔细查阅知识库内容,并且始终严格遵守中国法律法规。
+
+## 技能
+### 技能 1: 回答用户问题
+1. 当用户提出问题时,首先在知识库中进行搜索查找相关信息。
+2. 依据知识库中的内容,为用户提供准确、清晰、完整的回答。
+
+## 限制
+- 仅依据知识库内容回答问题,对于知识库中没有的信息,如实告知用户无法回答。
+- 回答必须严格遵循中国法律法规,不得出现任何违法违规内容。
+- 回答需简洁明了,避免冗长复杂的表述(尽量在100字内)。
+- 适当加些表情点缀。';
+ }
+
+ /**
+ * 保存AI设置到数据库
+ *
+ * @param int $companyId 公司ID
+ * @param int $userId 用户ID
+ * @param array $config 配置信息
+ * @return AiSettingsModel|false
+ */
+ private function saveAiSettings($companyId, $userId, $config)
+ {
+ $data = [
+ 'companyId' => $companyId,
+ 'userId' => $userId,
+ 'config' => json_encode($config, JSON_UNESCAPED_UNICODE),
+ 'createTime' => time(),
+ 'updateTime' => time(),
+ 'botId' => 0,
+ 'datasetId' => 0,
+ ];
+
+ $aiSettingsModel = new AiSettingsModel();
+ $result = $aiSettingsModel->save($data);
+
+ if ($result) {
+ return AiSettingsModel::where(['companyId' => $companyId])->find();
+ }
+
+ return false;
+ }
+
+ /**
+ * 创建AI智能体
+ *
+ * @param AiSettingsModel $settings AI设置对象
+ * @return bool
+ */
+ private function createBot($settings)
+ {
+ if (empty($settings)) {
+ return false;
+ }
+
+ try {
+ $config = json_decode($settings->config, true);
+ if (empty($config)) {
+ return false;
+ }
+
+ // 调用CozeAI创建智能体
+ $cozeAI = new CozeAI();
+ $result = $cozeAI->createBot($config);
+ $result = json_decode($result, true);
+
+ if ($result['code'] != 200) {
+ \think\facade\Log::error('智能体创建失败:' . ($result['msg'] ?? '未知错误'));
+ return false;
+ }
+
+ // 更新智能体ID
+ $settings->botId = $result['data']['bot_id'];
+ $settings->updateTime = time();
+
+ return $settings->save();
+
+ } catch (\Exception $e) {
+ \think\facade\Log::error('创建智能体异常:' . $e->getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * 创建知识库
+ *
+ * @param AiSettingsModel $settings AI设置对象
+ * @return bool
+ */
+ private function createKnowledge($settings)
+ {
+ if (empty($settings)) {
+ return false;
+ }
+
+ try {
+ $config = json_decode($settings->config, true);
+ if (empty($config)) {
+ return false;
+ }
+
+ // 调用CozeAI创建知识库
+ $cozeAI = new CozeAI();
+ $result = $cozeAI->createKnowledge(['name' => $config['name']]);
+ $result = json_decode($result, true);
+
+ if ($result['code'] != 200) {
+ \think\facade\Log::error('知识库创建失败:' . ($result['msg'] ?? '未知错误'));
+ return false;
+ }
+
+ // 更新知识库ID
+ $settings->datasetId = $result['data']['dataset_id'];
+ $settings->updateTime = time();
+
+ return $settings->save();
+
+ } catch (\Exception $e) {
+ \think\facade\Log::error('创建知识库异常:' . $e->getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * 更新AI配置
+ *
+ * @return \think\response\Json
+ */
+ public function updateConfig()
+ {
+ try {
+ $companyId = $this->getUserInfo('companyId');
+ if (empty($companyId)) {
+ return ResponseHelper::error('公司信息获取失败');
+ }
+
+ // 获取请求参数
+ $config = $this->request->param('config', []);
+ if (empty($config)) {
+ return ResponseHelper::error('配置参数不能为空');
+ }
+
+ // 查找现有设置
+ $settings = AiSettingsModel::where(['companyId' => $companyId])->find();
+ if (empty($settings)) {
+ return ResponseHelper::error('AI设置不存在,请先初始化');
+ }
+
+ // 更新配置
+ $settings->config = json_encode($config, JSON_UNESCAPED_UNICODE);
+ $settings->updateTime = time();
+
+ if ($settings->save()) {
+ return ResponseHelper::success([], '配置更新成功');
+ } else {
+ return ResponseHelper::error('配置更新失败');
+ }
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统异常:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 获取AI设置详情
+ *
+ * @return \think\response\Json
+ */
+ public function getSettings()
+ {
+ try {
+ $companyId = $this->getUserInfo('companyId');
+ if (empty($companyId)) {
+ return ResponseHelper::error('公司信息获取失败');
+ }
+
+ $settings = AiSettingsModel::where(['companyId' => $companyId])->find();
+ if (empty($settings)) {
+ return ResponseHelper::error('AI设置不存在');
+ }
+
+ // 解析配置信息
+ $settings->config = json_decode($settings->config, true);
+
+ return ResponseHelper::success($settings, '获取成功');
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统异常:' . $e->getMessage());
+ }
+ }
+
+
+ /**
+ * 发布智能体
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function release()
+ {
+ $companyId = $this->getUserInfo('companyId');
+ $settings = AiSettingsModel::where(['companyId' => $companyId])->find();
+ if (!empty($settings->isRelease)) {
+ return ResponseHelper::success('', '已发布,无需重复发布');
+ }
+
+ $cozeAI = new CozeAI();
+ $res = $cozeAI->botPublish(['bot_id' => $settings->botId]);
+ $res = json_decode($res, true);
+
+ if ($res['code'] != 200) {
+ $msg = '发布失败失败:' . ($res['msg'] ?? '未知错误');
+ return ResponseHelper::error($msg);
+ }
+ $settings->isRelease = 1;
+ $settings->releaseTime = time();
+ $settings->save();
+ return ResponseHelper::success('', '发布成功');
+ }
+
+ /**
+ * 保存统一提示词
+ * 先更新数据库,再调用CozeAI接口更新智能体
+ *
+ * @return \think\response\Json
+ */
+ public function savePrompt()
+ {
+ try {
+ $companyId = $this->getUserInfo('companyId');
+ if (empty($companyId)) {
+ return ResponseHelper::error('公司信息获取失败');
+ }
+
+ // 获取提示词参数
+ $promptInfo = $this->request->param('promptInfo', '');
+ if (empty($promptInfo)) {
+ return ResponseHelper::error('提示词内容不能为空');
+ }
+
+ // 查找AI设置
+ $settings = AiSettingsModel::where(['companyId' => $companyId])->find();
+ if (empty($settings)) {
+ return ResponseHelper::error('AI设置不存在,请先初始化');
+ }
+
+ // 检查智能体是否已创建
+ if (empty($settings->botId)) {
+ return ResponseHelper::error('智能体未创建,请先初始化AI设置');
+ }
+
+ // 解析现有配置
+ $config = json_decode($settings->config, true);
+ if (!is_array($config)) {
+ $config = [];
+ }
+
+ // 更新提示词
+ $config['prompt_info'] = $promptInfo;
+
+ // 第一步:更新数据库
+ $settings->config = json_encode($config, JSON_UNESCAPED_UNICODE);
+ $settings->isRelease = 0; // 标记为未发布状态
+ $settings->updateTime = time();
+
+ if (!$settings->save()) {
+ return ResponseHelper::error('数据库更新失败');
+ }
+
+ // 第二步:调用CozeAI接口更新智能体
+ try {
+ $cozeAI = new CozeAI();
+
+ // 参考 init 方法的参数格式,传递完整的 config
+ $updateData = $config;
+ $updateData['bot_id'] = $settings->botId;
+
+ // 如果有知识库,也一并传入
+ if (!empty($settings->datasetId)) {
+ $updateData['dataset_ids'] = [$settings->datasetId];
+ }
+
+ $result = $cozeAI->updateBot($updateData);
+ $result = json_decode($result, true);
+
+ if ($result['code'] != 200) {
+ \think\facade\Log::error('更新智能体提示词失败:' . json_encode($result));
+ return ResponseHelper::error('更新智能体失败:' . ($result['msg'] ?? '未知错误'));
+ }
+
+ return ResponseHelper::success([
+ 'prompt_info' => $promptInfo,
+ 'isRelease' => 0
+ ], '提示词保存成功,请重新发布智能体');
+
+ } catch (\Exception $e) {
+ \think\facade\Log::error('调用CozeAI更新接口异常:' . $e->getMessage());
+ return ResponseHelper::error('更新智能体接口调用失败:' . $e->getMessage());
+ }
+
+ } catch (\Exception $e) {
+ \think\facade\Log::error('保存提示词异常:' . $e->getMessage());
+ return ResponseHelper::error('系统异常:' . $e->getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/BaseController.php b/application/cunkebao/controller/BaseController.php
new file mode 100644
index 0000000..f3875d5
--- /dev/null
+++ b/application/cunkebao/controller/BaseController.php
@@ -0,0 +1,163 @@
+classTable = $classTable;
+
+ parent::__construct();
+ }
+
+ /**
+ * 初始化
+ */
+ protected function initialize()
+ {
+ parent::initialize();
+
+ date_default_timezone_set('Asia/Shanghai');
+ }
+
+ /**
+ * 获取用户信息
+ *
+ * @param string $column
+ * @return mixed
+ * @throws \Exception
+ */
+ protected function getUserInfo(?string $column = null)
+ {
+ $user = $this->request->userInfo;
+
+ if (!$user) {
+ throw new \Exception('未授权访问,缺少有效的身份凭证', 401);
+ }
+
+ return $column ? $user[$column] : $user;
+ }
+
+
+ public function editUserInfo()
+ {
+ $userId = $this->request->param('userId', '');
+ $nickname = $this->request->param('nickname', '');
+ $avatar = $this->request->param('avatar', '');
+ $phone = $this->request->param('phone', '');
+ $companyId = $this->getUserInfo('companyId');
+ if (empty($userId)) {
+ return ResponseHelper::error('用户id不能为空');
+ }
+
+ if (empty($nickname) && empty($avatar) && empty($phone)) {
+ return ResponseHelper::error('修改的用户信息不能为空');
+ }
+
+ $user = Db::name('users')->where(['id' => $userId, 'companyId' => $companyId])->find();
+ if (empty($user)) {
+ return ResponseHelper::error('用户不存在');
+ }
+
+ $user2 = Db::name('users')->where(['phone' => $phone])->find();
+ if (!empty($user2) && $user2['id'] != $userId) {
+ return ResponseHelper::error('修改的手机号已存在');
+ }
+
+ $data = [
+ 'id' => $user['s2_accountId'],
+ ];
+
+ if (!empty($nickname)) {
+ $data['nickname'] = $nickname;
+ }
+ if (!empty($avatar)) {
+ $data['avatar'] = $avatar;
+ }
+ if (!empty($phone)) {
+ $data['phone'] = $phone;
+ }
+
+ $AccountControllel = new AccountController();
+ $res = $AccountControllel->accountModify($data);
+ $res = json_decode($res, true);
+ if ($res['code'] == 200) {
+ unset($data['id']);
+ if (!empty($nickname)) {
+ $data['username'] = $nickname;
+ unset($data['nickname']);
+ }
+ Db::name('users')->where(['id' => $userId, 'companyId' => $companyId])->update($data);
+ return ResponseHelper::success('更新成功');
+ } else {
+ return ResponseHelper::error($res['msg']);
+ }
+ }
+
+
+ public function editPassWord()
+ {
+ $userId = $this->request->param('userId', '');
+ $passWord = $this->request->param('passWord', '');
+ $companyId = $this->getUserInfo('companyId');
+ if (empty($userId)) {
+ return ResponseHelper::error('用户id不能为空');
+ }
+
+ if (empty($passWord)) {
+ return ResponseHelper::error('密码不能为空');
+ }
+
+ $user = Db::name('users')->where(['id' => $userId, 'companyId' => $companyId])->find();
+ if (empty($user)) {
+ return ResponseHelper::error('用户不存在');
+ }
+ if ($user['passwordMd5'] == md5($passWord)) {
+ return ResponseHelper::error('新密码与旧密码一致');
+ }
+
+ $data = [
+ 'passwordMd5' => md5($passWord),
+ 'passwordLocal' => localEncrypt($passWord),
+ 'updateTime' => time()
+ ];
+
+ $res = Db::name('users')->where(['id' => $userId, 'companyId' => $companyId])->update($data);
+ if (!empty($res)) {
+ if ($user['typeId'] == 1 && !empty($user['s2_accountId'])) {
+ $UserController = new UserController();
+ $UserController->modifyPwd(['id' => $user['s2_accountId'],'pwd' => $passWord]);
+ }
+
+
+ return ResponseHelper::success('密码修改成功');
+ } else {
+ return ResponseHelper::error('密码修改失败');
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/ContentLibraryController.php b/application/cunkebao/controller/ContentLibraryController.php
new file mode 100644
index 0000000..8cdb213
--- /dev/null
+++ b/application/cunkebao/controller/ContentLibraryController.php
@@ -0,0 +1,2913 @@
+request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ // 获取请求参数
+ $param = $this->request->post();
+
+ // 验证参数
+ if (empty($param['name'])) {
+ return json(['code' => 400, 'msg' => '内容库名称不能为空']);
+ }
+
+
+ // 检查内容库名称是否已存在
+ $where = [
+ ['name', '=', $param['name']],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+ // 查询内容库是否存在
+ $exists = ContentLibrary::where($where)->find();
+
+
+ if ($exists) {
+ return json(['code' => 400, 'msg' => '内容库名称已存在']);
+ }
+
+ Db::startTrans();
+ try {
+
+ $keywordInclude = isset($param['keywordInclude']) ? json_encode($param['keywordInclude'], 256) : json_encode([]);
+ $keywordExclude = isset($param['keywordExclude']) ? json_encode($param['keywordExclude'], 256) : json_encode([]);
+ $devices = isset($param['devices']) ? json_encode($param['devices'], 256) : json_encode([]);
+ $sourceType = isset($param['sourceType']) ? $param['sourceType'] : 1;
+
+
+ // 构建数据
+ $data = [
+ 'name' => $param['name'],
+ // 数据来源配置
+ 'sourceFriends' => $sourceType == 1 && isset($param['friendsGroups']) ? json_encode($param['friendsGroups']) : json_encode([]), // 选择的微信好友
+ 'sourceGroups' => $sourceType == 2 && isset($param['wechatGroups']) ? json_encode($param['wechatGroups']) : json_encode([]), // 选择的微信群
+ 'groupMembers' => $sourceType == 2 && isset($param['groupMembers']) ? json_encode($param['groupMembers']) : json_encode([]), // 群组成员
+ 'catchType' => isset($param['catchType']) ? json_encode($param['catchType']) : json_encode([]), // 采集类型
+ 'devices' => $devices,
+ // 关键词配置
+ 'keywordInclude' => $keywordInclude, // 包含的关键词
+ 'keywordExclude' => $keywordExclude, // 排除的关键词
+ // AI配置
+ 'aiEnabled' => isset($param['aiEnabled']) ? $param['aiEnabled'] : 0, // 是否启用AI
+ 'aiPrompt' => isset($param['aiPrompt']) ? $param['aiPrompt'] : '', // AI提示词
+ // 时间配置
+ 'timeEnabled' => isset($param['timeEnabled']) ? $param['timeEnabled'] : 0, // 是否启用时间限制
+ 'timeStart' => isset($param['startTime']) ? strtotime($param['startTime']) : 0, // 开始时间(转换为时间戳)
+ 'timeEnd' => isset($param['endTime']) ? strtotime($param['endTime']) : 0, // 结束时间(转换为时间戳)
+ // 来源类型
+ 'sourceType' => $sourceType, // 1=好友,2=群,3=好友和群
+ // 表单类型
+ 'formType' => isset($param['formType']) ? intval($param['formType']) : 1, // 表单类型,默认为0
+ // 基础信息
+ 'status' => isset($param['status']) ? $param['status'] : 0, // 状态:0=禁用,1=启用
+ 'userId' => $this->request->userInfo['id'],
+ 'companyId' => $this->request->userInfo['companyId'],
+ 'createTime' => time(),
+ 'updateTime' => time()
+ ];
+
+ // 创建内容库
+ $library = new ContentLibrary;
+ $result = $library->save($data);
+
+ if (!$result) {
+ Db::rollback();
+ return json(['code' => 500, 'msg' => '创建内容库失败']);
+ }
+
+ Db::commit();
+ return json(['code' => 200, 'msg' => '创建成功', 'data' => ['id' => $library->id]]);
+ } catch (\Exception $e) {
+ Db::rollback();
+ return json(['code' => 500, 'msg' => '创建失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 获取内容库列表
+ * @return \think\response\Json
+ */
+ public function getList()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+ $sourceType = $this->request->param('sourceType', ''); // 来源类型,1=好友,2=群
+ $formType = $this->request->param('formType', 0); // 表单类型筛选
+ $companyId = $this->request->userInfo['companyId'];
+ $userId = $this->request->userInfo['id'];
+ $isAdmin = !empty($this->request->userInfo['isAdmin']);
+
+ // 构建基础查询条件
+ $where = [
+ ['companyId', '=', $companyId],
+ ['isDel', '=', 0] // 只查询未删除的记录
+ ];
+
+ // 非管理员只能查看自己的内容库
+ if (!$isAdmin) {
+ $where[] = ['userId', '=', $userId];
+ }
+
+ // 添加名称模糊搜索
+ if ($keyword !== '') {
+ $where[] = ['name', 'like', '%' . $keyword . '%'];
+ }
+
+ // 添加来源类型筛选
+ if (!empty($sourceType)) {
+ $where[] = ['sourceType', '=', $sourceType];
+ }
+
+ // 添加表单类型筛选
+ if ($formType !== '') {
+ $where[] = ['formType', '=', $formType];
+ }
+
+ // 获取总记录数
+ $total = ContentLibrary::where($where)->count();
+
+ // 获取分页数据
+ $list = ContentLibrary::where($where)
+ ->field('id,name,sourceFriends,sourceGroups,keywordInclude,keywordExclude,aiEnabled,aiPrompt,timeEnabled,timeStart,timeEnd,status,sourceType,formType,userId,createTime,updateTime')
+ ->with(['user' => function ($query) {
+ $query->field('id,username');
+ }])
+ ->order('id', 'desc')
+ ->page($page, $limit)
+ ->select();
+
+ // 收集所有需要查询的ID
+ $libraryIds = [];
+ $friendIdsByLibrary = [];
+ $groupIdsByLibrary = [];
+
+ foreach ($list as $item) {
+ $libraryIds[] = $item['id'];
+
+ // 解析JSON字段
+ $item['sourceFriends'] = json_decode($item['sourceFriends'] ?: '[]', true);
+ $item['sourceGroups'] = json_decode($item['sourceGroups'] ?: '[]', true);
+ $item['keywordInclude'] = json_decode($item['keywordInclude'] ?: '[]', true);
+ $item['keywordExclude'] = json_decode($item['keywordExclude'] ?: '[]', true);
+
+ // 收集好友和群组ID
+ if (!empty($item['sourceFriends']) && $item['sourceType'] == 1) {
+ $friendIdsByLibrary[$item['id']] = $item['sourceFriends'];
+ }
+
+ if (!empty($item['sourceGroups']) && $item['sourceType'] == 2) {
+ $groupIdsByLibrary[$item['id']] = $item['sourceGroups'];
+ }
+ }
+
+ // 批量查询内容项数量
+ $itemCounts = [];
+ if (!empty($libraryIds)) {
+ $counts = Db::name('content_item')
+ ->field('libraryId, COUNT(*) as count')
+ ->whereIn('libraryId', $libraryIds)
+ ->where('isDel', 0)
+ ->group('libraryId')
+ ->select();
+
+ foreach ($counts as $count) {
+ $itemCounts[$count['libraryId']] = $count['count'];
+ }
+ }
+
+ // 批量查询好友信息
+ $friendsInfoMap = [];
+ $allFriendIds = [];
+ foreach ($friendIdsByLibrary as $libraryId => $friendIds) {
+ if (!empty($friendIds)) {
+ $allFriendIds = array_merge($allFriendIds, $friendIds);
+ }
+ }
+
+ if (!empty($allFriendIds)) {
+ $allFriendIds = array_unique($allFriendIds);
+ $friendsInfo = Db::name('wechat_friendship')->alias('wf')
+ ->field('wf.id,wf.wechatId, wa.nickname, wa.avatar')
+ ->join('wechat_account wa', 'wf.wechatId = wa.wechatId')
+ ->whereIn('wf.id', $allFriendIds)
+ ->select();
+
+ foreach ($friendsInfo as $friend) {
+ $friendsInfoMap[$friend['id']] = $friend;
+ }
+ }
+
+ // 批量查询群组信息
+ $groupsInfoMap = [];
+ $allGroupIds = [];
+ foreach ($groupIdsByLibrary as $libraryId => $groupIds) {
+ if (!empty($groupIds)) {
+ $allGroupIds = array_merge($allGroupIds, $groupIds);
+ }
+ }
+
+ if (!empty($allGroupIds)) {
+ $allGroupIds = array_unique($allGroupIds);
+ $groupsInfo = Db::name('wechat_group')->alias('g')
+ ->field('g.id, g.chatroomId, g.name, g.avatar, g.ownerWechatId')
+ ->whereIn('g.id', $allGroupIds)
+ ->select();
+
+ foreach ($groupsInfo as $group) {
+ $groupsInfoMap[$group['id']] = $group;
+ }
+ }
+
+ // 处理每个内容库的数据
+ foreach ($list as &$item) {
+ // 添加创建人名称
+ $item['creatorName'] = $item['user']['username'] ?? '';
+
+ // 添加内容项数量
+ $item['itemCount'] = $itemCounts[$item['id']] ?? 0;
+
+ // 处理好友信息
+ if (!empty($friendIdsByLibrary[$item['id']])) {
+ $selectedFriends = [];
+ foreach ($friendIdsByLibrary[$item['id']] as $friendId) {
+ if (isset($friendsInfoMap[$friendId])) {
+ $selectedFriends[] = $friendsInfoMap[$friendId];
+ }
+ }
+ $item['selectedFriends'] = $selectedFriends;
+ }
+
+ // 处理群组信息
+ if (!empty($groupIdsByLibrary[$item['id']])) {
+ $selectedGroups = [];
+ foreach ($groupIdsByLibrary[$item['id']] as $groupId) {
+ if (isset($groupsInfoMap[$groupId])) {
+ $selectedGroups[] = $groupsInfoMap[$groupId];
+ }
+ }
+ $item['selectedGroups'] = $selectedGroups;
+ }
+
+ unset($item['user']); // 移除关联数据
+ }
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $page,
+ ]
+ ]);
+ }
+
+ /**
+ * 获取内容库详情
+ * @return \think\response\Json
+ */
+ public function detail()
+ {
+ $id = $this->request->param('id', 0);
+ $companyId = $this->request->userInfo['companyId'];
+ $userId = $this->request->userInfo['id'];
+ $isAdmin = !empty($this->request->userInfo['isAdmin']);
+
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ // 构建查询条件
+ $where = [
+ ['id', '=', $id],
+ ['companyId', '=', $companyId],
+ ['isDel', '=', 0] // 只查询未删除的记录
+ ];
+
+ if (!$isAdmin) {
+ $where[] = ['userId', '=', $userId];
+ }
+
+ // 查询内容库信息
+ $library = ContentLibrary::where($where)
+ ->field('id,name,sourceType,formType,devices ,sourceFriends,sourceGroups,keywordInclude,keywordExclude,aiEnabled,aiPrompt,timeEnabled,timeStart,timeEnd,status,userId,companyId,createTime,updateTime,groupMembers,catchType')
+ ->find();
+
+ if (empty($library)) {
+ return json(['code' => 500, 'msg' => '内容库不存在']);
+ }
+
+ // 处理JSON字段转数组
+ $library['friendsGroups'] = json_decode($library['sourceFriends'] ?: [], true);
+ $library['wechatGroups'] = json_decode($library['sourceGroups'] ?: [], true);
+ $library['keywordInclude'] = json_decode($library['keywordInclude'] ?: [], true);
+ $library['keywordExclude'] = json_decode($library['keywordExclude'] ?: [], true);
+ $library['groupMembers'] = json_decode($library['groupMembers'] ?: [], true);
+ $library['catchType'] = json_decode($library['catchType'] ?: [], true);
+ $library['deviceGroups'] = json_decode($library['devices'] ?: [], true);
+ unset($library['sourceFriends'], $library['sourceGroups'], $library['devices']);
+
+ // 将时间戳转换为日期格式(精确到日)
+ if (!empty($library['timeStart'])) {
+ $library['timeStart'] = date('Y-m-d', $library['timeStart']);
+ }
+ if (!empty($library['timeEnd'])) {
+ $library['timeEnd'] = date('Y-m-d', $library['timeEnd']);
+ }
+
+ // 初始化选项数组
+ $library['friendsGroupsOptions'] = [];
+ $library['wechatGroupsOptions'] = [];
+ $library['groupMembersOptions'] = [];
+
+ // 批量查询好友信息
+ if (!empty($library['friendsGroups'])) {
+ $friendIds = $library['friendsGroups'];
+ if (!empty($friendIds)) {
+ // 查询好友信息,使用wechat_friendship表
+ $library['friendsGroupsOptions'] = Db::name('wechat_friendship')->alias('wf')
+ ->field('wf.id,wf.wechatId, wa.nickname, wa.avatar')
+ ->join('wechat_account wa', 'wf.wechatId = wa.wechatId')
+ ->order('wa.id DESC')
+ ->whereIn('wf.id', $friendIds)
+ ->select();
+ }
+ }
+
+ // 批量查询群组信息
+ if (!empty($library['wechatGroups'])) {
+ $groupIds = $library['wechatGroups'];
+ if (!empty($groupIds)) {
+ // 查询群组信息
+ $library['wechatGroupsOptions'] = Db::name('wechat_group')->alias('g')
+ ->field('g.id, g.chatroomId, g.name, g.avatar, g.ownerWechatId, wa.nickname as ownerNickname, wa.avatar as ownerAvatar, wa.alias as ownerAlias')
+ ->join('wechat_account wa', 'g.ownerWechatId = wa.wechatId', 'LEFT') // 使用LEFT JOIN避免因群主不存在导致查询失败
+ ->whereIn('g.id', $groupIds)
+ ->select();
+ }
+ }
+
+ // 批量查询群成员信息
+ if (!empty($library['groupMembers'])) {
+ // groupMembers格式: {"826825": ["413771", "413769"], "840818": ["496300", "496302"]}
+ // 键是群组ID,值是成员ID数组
+ $allMemberIds = [];
+ $groupMembersMap = [];
+
+ if (is_array($library['groupMembers'])) {
+ foreach ($library['groupMembers'] as $groupId => $memberIds) {
+ if (is_array($memberIds) && !empty($memberIds)) {
+ $allMemberIds = array_merge($allMemberIds, $memberIds);
+ // 保存群组ID和成员ID的映射关系
+ $groupMembersMap[$groupId] = $memberIds;
+ }
+ }
+ }
+
+ if (!empty($allMemberIds)) {
+ // 去重
+ $allMemberIds = array_unique($allMemberIds);
+
+ // 查询群成员信息
+ $members = Db::table('s2_wechat_chatroom_member')
+ ->field('id, chatroomId, wechatId, nickname, avatar, conRemark, alias, friendType, createTime, updateTime')
+ ->whereIn('id', $allMemberIds)
+ ->select();
+
+ // 将成员数据按ID建立索引
+ $membersById = [];
+ foreach ($members as $member) {
+ // 格式化时间字段
+ $member['createTime'] = !empty($member['createTime']) ? date('Y-m-d H:i:s', $member['createTime']) : '';
+ $member['updateTime'] = !empty($member['updateTime']) ? date('Y-m-d H:i:s', $member['updateTime']) : '';
+ $membersById[$member['id']] = $member;
+ }
+
+ // 按照群组ID分组返回
+ $groupMembersOptions = [];
+ foreach ($groupMembersMap as $groupId => $memberIds) {
+ $groupMembersOptions[$groupId] = [];
+ foreach ($memberIds as $memberId) {
+ if (isset($membersById[$memberId])) {
+ $groupMembersOptions[$groupId][] = $membersById[$memberId];
+ }
+ }
+ }
+
+ $library['groupMembersOptions'] = $groupMembersOptions;
+ } else {
+ $library['groupMembersOptions'] = [];
+ }
+ } else {
+ $library['groupMembersOptions'] = [];
+ }
+
+ //获取设备信息
+ if (!empty($library['deviceGroups'])) {
+ $deviceList = DeviceModel::alias('d')
+ ->field([
+ 'd.id', 'd.imei', 'd.memo', 'd.alive',
+ 'l.wechatId',
+ 'a.nickname', 'a.alias', 'a.avatar', 'a.alias', '0 totalFriend'
+ ])
+ ->leftJoin('device_wechat_login l', 'd.id = l.deviceId and l.alive =' . DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE . ' and l.companyId = d.companyId')
+ ->leftJoin('wechat_account a', 'l.wechatId = a.wechatId')
+ ->whereIn('d.id', $library['deviceGroups'])
+ ->order('d.id desc')
+ ->select();
+
+ foreach ($deviceList as &$device) {
+ $curstomer = WechatCustomerModel::field('friendShip')->where(['wechatId' => $device['wechatId']])->find();
+ $device['totalFriend'] = $curstomer->friendShip->totalFriend ?? 0;
+ }
+ unset($device);
+ $library['deviceGroupsOptions'] = $deviceList;
+ } else {
+ $library['deviceGroupsOptions'] = [];
+ }
+
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => $library
+ ]);
+ }
+
+ /**
+ * 更新内容库
+ * @return \think\response\Json
+ */
+ public function update()
+ {
+ if (!$this->request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ // 获取请求参数
+ $param = $this->request->post();
+
+ // 简单验证
+ if (empty($param['id'])) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ if (empty($param['name'])) {
+ return json(['code' => 400, 'msg' => '内容库名称不能为空']);
+ }
+
+
+ $where = [
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['isDel', '=', 0], // 只查询未删除的记录
+ ['id', '=', $param['id']]
+ ];
+
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+ // 查询内容库是否存在
+ $library = ContentLibrary::where($where)->find();
+
+ if (!$library) {
+ return json(['code' => 500, 'msg' => '内容库不存在']);
+ }
+
+ Db::startTrans();
+ try {
+
+ $keywordInclude = isset($param['keywordInclude']) ? json_encode($param['keywordInclude'], 256) : json_encode([]);
+ $keywordExclude = isset($param['keywordExclude']) ? json_encode($param['keywordExclude'], 256) : json_encode([]);
+ $devices = isset($param['devices']) ? json_encode($param['devices'], 256) : json_encode([]);
+
+ // 更新内容库基本信息
+ $library->name = $param['name'];
+ $library->sourceType = isset($param['sourceType']) ? $param['sourceType'] : 1;
+ $library->sourceFriends = $param['sourceType'] == 1 && isset($param['friendsGroups']) ? json_encode($param['friendsGroups']) : json_encode([]);
+ $library->sourceGroups = $param['sourceType'] == 2 && isset($param['wechatGroups']) ? json_encode($param['wechatGroups']) : json_encode([]);
+ $library->groupMembers = $param['sourceType'] == 2 && isset($param['groupMembers']) ? json_encode($param['groupMembers']) : json_encode([]);
+ $library->catchType = isset($param['catchType']) ? json_encode($param['catchType']) : json_encode([]);// 采集类型
+ $library->devices = $devices;
+ $library->keywordInclude = $keywordInclude;
+ $library->keywordExclude = $keywordExclude;
+ $library->aiEnabled = isset($param['aiEnabled']) ? $param['aiEnabled'] : 0;
+ $library->aiPrompt = isset($param['aiPrompt']) ? $param['aiPrompt'] : '';
+ $library->timeEnabled = isset($param['timeEnabled']) ? $param['timeEnabled'] : 0;
+ $library->timeStart = isset($param['startTime']) ? strtotime($param['startTime']) : 0;
+ $library->timeEnd = isset($param['endTime']) ? strtotime($param['endTime']) : 0;
+ $library->formType = isset($param['formType']) ? intval($param['formType']) : $library->formType; // 表单类型,如果未传则保持原值
+ $library->status = isset($param['status']) ? $param['status'] : 0;
+ $library->updateTime = time();
+ $library->save();
+
+ Db::commit();
+ return json(['code' => 200, 'msg' => '更新成功']);
+ } catch (\Exception $e) {
+ Db::rollback();
+ return json(['code' => 500, 'msg' => '更新失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 删除内容库
+ * @return \think\response\Json
+ */
+ public function delete()
+ {
+ $id = $this->request->param('id', 0);
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+
+ $where = [
+ ['id', '=', $id],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+ $library = ContentLibrary::where($where)->find();
+
+ if (empty($library)) {
+ return json(['code' => 500, 'msg' => '内容库不存在']);
+ }
+
+ try {
+ // 软删除
+ $library->isDel = 1;
+ $library->deleteTime = time();
+ $library->save();
+
+ return json(['code' => 200, 'msg' => '删除成功']);
+ } catch (\Exception $e) {
+ return json(['code' => 500, 'msg' => '删除失败:' . $e->getMessage()]);
+ }
+ }
+
+ /************************************
+ * 内容项目管理功能
+ ************************************/
+
+ /**
+ * 获取内容库素材列表
+ * @return \think\response\Json
+ */
+ public function getItemList()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $libraryId = $this->request->param('libraryId', 0);
+ $keyword = $this->request->param('keyword', ''); // 搜索关键词
+ $companyId = $this->request->userInfo['companyId'];
+ $userId = $this->request->userInfo['id'];
+ $isAdmin = !empty($this->request->userInfo['isAdmin']);
+
+ if (empty($libraryId)) {
+ return json(['code' => 400, 'msg' => '内容库ID不能为空']);
+ }
+
+ // 验证内容库权限
+ $libraryWhere = [
+ ['id', '=', $libraryId],
+ ['companyId', '=', $companyId],
+ ['isDel', '=', 0]
+ ];
+
+
+ if (!$isAdmin) {
+ $libraryWhere[] = ['userId', '=', $userId];
+ }
+
+ $library = ContentLibrary::where($libraryWhere)->find();
+
+ if (empty($library)) {
+ return json(['code' => 500, 'msg' => '内容库不存在或无权限访问']);
+ }
+
+ // 构建查询条件
+ $where = [
+ ['libraryId', '=', $libraryId],
+ ['isDel', '=', 0]
+ ];
+
+ // 关键词搜索
+ if (!empty($keyword)) {
+ $where[] = ['content|title', 'like', '%' . $keyword . '%'];
+ }
+
+ // 获取总数
+ $total = ContentItem::where($where)->count();
+
+ // 查询数据
+ $list = ContentItem::where($where)
+ ->field('id,libraryId,type,title,content,contentAi,contentType,resUrls,urls,friendId,wechatId,wechatChatroomId,createTime,createMomentTime,createMessageTime,coverImage,ossUrls')
+ ->order('createMomentTime DESC,createMessageTime DESC,createTime DESC')
+ ->page($page, $limit)
+ ->select();
+
+ // 收集需要查询的ID
+ $friendIds = [];
+ $chatroomIds = [];
+ $wechatIds = [];
+
+ foreach ($list as $item) {
+ if ($item['type'] == 'moment' && !empty($item['friendId'])) {
+ $friendIds[] = $item['friendId'];
+ } else if ($item['type'] == 'group_message' && !empty($item['wechatChatroomId'])) {
+ $chatroomIds[] = $item['wechatChatroomId'];
+ if (!empty($item['wechatId'])) {
+ $wechatIds[] = $item['wechatId'];
+ }
+ }
+ }
+
+ // 批量查询好友信息
+ $friendInfoMap = [];
+ if (!empty($friendIds)) {
+ $friendIds = array_unique($friendIds);
+ $friendInfos = Db::table('s2_wechat_friend')
+ ->whereIn('id', $friendIds)
+ ->field('id, nickname, avatar')
+ ->select();
+
+ foreach ($friendInfos as $info) {
+ $friendInfoMap[$info['id']] = $info;
+ }
+ }
+
+ // 批量查询群成员信息
+ $memberInfoMap = [];
+ if (!empty($wechatIds)) {
+ $wechatIds = array_unique($wechatIds);
+ $memberInfos = Db::table('s2_wechat_chatroom_member')
+ ->whereIn('wechatId', $wechatIds)
+ ->field('wechatId, nickname, avatar')
+ ->select();
+
+ foreach ($memberInfos as $info) {
+ $memberInfoMap[$info['wechatId']] = $info;
+ }
+ }
+
+
+ // 处理数据
+ foreach ($list as &$item) {
+ // 使用AI内容(如果有)
+ $item['content'] = !empty($item['contentAi']) ? $item['contentAi'] : $item['content'];
+
+ // 处理JSON字段
+ $item['resUrls'] = json_decode($item['resUrls'] ?: [], true);
+ $item['urls'] = json_decode($item['urls'] ?: [], true);
+ $item['ossUrls'] = json_decode($item['ossUrls'] ?: [], true);
+
+ if (!empty($item['ossUrls']) && count($item['ossUrls']) > 0) {
+ $item['resUrls'] = $item['ossUrls'];
+ }
+
+
+ // 格式化时间
+ if (!empty($item['createMomentTime']) && is_numeric($item['createMomentTime'])) {
+ $item['time'] = date('Y-m-d H:i:s', (int)$item['createMomentTime']);
+ } elseif (!empty($item['createMessageTime']) && is_numeric($item['createMessageTime'])) {
+ $item['time'] = date('Y-m-d H:i:s', (int)$item['createMessageTime']);
+ } elseif (!empty($item['createTime']) && is_numeric($item['createTime'])) {
+ $item['time'] = date('Y-m-d H:i:s', (int)$item['createTime']);
+ } else {
+ $item['time'] = date('Y-m-d H:i:s'); // 如果没有有效的时间戳,使用当前时间
+ }
+
+ // 设置发送者信息
+ $item['senderNickname'] = '';
+ $item['senderAvatar'] = '';
+
+ // 从映射表获取发送者信息
+ if ($item['type'] == 'moment' && !empty($item['friendId'])) {
+ if (isset($friendInfoMap[$item['friendId']])) {
+ $friendInfo = $friendInfoMap[$item['friendId']];
+ $item['senderNickname'] = $friendInfo['nickname'] ?? '';
+ $item['senderAvatar'] = $friendInfo['avatar'] ?? '';
+ }
+ } else if ($item['type'] == 'group_message' && !empty($item['wechatId'])) {
+ if (isset($memberInfoMap[$item['wechatId']])) {
+ $memberInfo = $memberInfoMap[$item['wechatId']];
+ $item['senderNickname'] = $memberInfo['nickname'] ?? '';
+ $item['senderAvatar'] = $memberInfo['avatar'] ?? '';
+ }
+ }
+
+ unset($item['contentAi'], $item['ossUrls']);
+ }
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]);
+ }
+
+ /**
+ * 添加内容项目
+ * @return \think\response\Json
+ */
+ public function addItem()
+ {
+ if (!$this->request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ // 获取请求参数
+ $param = $this->request->post();
+
+ // A简单验证
+ if (empty($param['libraryId'])) {
+ return json(['code' => 400, 'msg' => '内容库ID不能为空']);
+ }
+
+ if (empty($param['type'])) {
+ return json(['code' => 400, 'msg' => '内容类型不能为空']);
+ }
+
+ if (empty($param['content'])) {
+ return json(['code' => 400, 'msg' => '内容数据不能为空']);
+ }
+
+ // 当类型为群消息时,限制图片只能上传一张
+ if ($param['type'] == 'group_message') {
+ $images = isset($param['images']) ? $param['images'] : [];
+ if (is_string($images)) {
+ $images = json_decode($images, true);
+ }
+
+ if (count($images) > 1) {
+ return json(['code' => 400, 'msg' => '群消息类型只能上传一张图片']);
+ }
+ }
+
+ $where = [
+ ['id', '=', $param['libraryId']],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+ // 查询内容库是否存在
+ $library = ContentLibrary::where($where)->find();
+
+ if (!$library) {
+ return json(['code' => 500, 'msg' => '内容库不存在']);
+ }
+
+ try {
+ // 创建内容项目
+ $item = new ContentItem;
+ $item->libraryId = $param['libraryId'];
+ $item->contentType = $param['type'];
+ $item->type = 'diy';
+ $item->title = $param['title'] ?? '自定义内容';
+ $item->content = $param['content'];
+ $item->comment = $param['comment'] ?? '';
+ $item->sendTime = strtotime($param['sendTime']);
+ $item->resUrls = json_encode($param['resUrls'] ?? [], 256);
+ $item->urls = json_encode($param['urls'] ?? [], 256);
+ $item->ossUrls = json_encode($param['ossUrls'] ?? [], 256);
+ $item->senderNickname = '系统创建';
+ $item->coverImage = $param['coverImage'] ?? '';
+ $item->save();
+
+ return json(['code' => 200, 'msg' => '添加成功', 'data' => ['id' => $item->id]]);
+ } catch (\Exception $e) {
+ return json(['code' => 500, 'msg' => '添加失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 删除内容项目
+ * @param int $id 内容项目ID
+ * @return \think\response\Json
+ */
+ public function deleteItem()
+ {
+
+ $id = $this->request->param('id', 0);
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+
+ $where = [
+ ['i.id', '=', $id],
+ ['l.companyId', '=', $this->request->userInfo['companyId']]
+ ];
+
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['l.userId', '=', $this->request->userInfo['id']];
+ }
+
+ // 查询内容项目是否存在并检查权限
+ $item = ContentItem::alias('i')
+ ->join('content_library l', 'i.libraryId = l.id')
+ ->where($where)
+ ->find();
+
+
+ if (empty($item)) {
+ return json(['code' => 500, 'msg' => '内容项目不存在或无权限操作']);
+ }
+
+ try {
+ // 删除内容项目
+ $service = new \app\cunkebao\service\ContentItemService();
+ $result = $service->deleteItem($id);
+ if ($result['code'] != 200) {
+ return json($result);
+ }
+
+ return json(['code' => 200, 'msg' => '删除成功']);
+ } catch (\Exception $e) {
+ return json(['code' => 500, 'msg' => '删除失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 获取内容项目详情
+ * @return \think\response\Json
+ */
+ public function getItemDetail()
+ {
+ $id = $this->request->param('id', 0);
+ $userId = $this->request->userInfo['id'];
+ $isAdmin = !empty($this->request->userInfo['isAdmin']);
+
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ // 构建查询条件
+ $where = [
+ ['i.id', '=', $id],
+ ['i.isDel', '=', 0]
+ ];
+
+ // 非管理员只能查看自己的内容项
+ if (!$isAdmin) {
+ $where[] = ['l.userId', '=', $userId];
+ }
+
+ // 查询内容项目是否存在并检查权限
+ $item = ContentItem::alias('i')
+ ->join('content_library l', 'i.libraryId = l.id')
+ ->where($where)
+ ->field('i.*')
+ ->find();
+
+ if (empty($item)) {
+ return json(['code' => 500, 'msg' => '内容项目不存在或无权限访问']);
+ }
+
+ // 处理JSON字段
+ $item['resUrls'] = json_decode($item['resUrls'] ?: [], true);
+ $item['urls'] = json_decode($item['urls'] ?: [], true);
+
+ // 添加内容类型的文字描述
+ $contentTypeMap = [
+ 0 => '未知',
+ 1 => '图片',
+ 2 => '链接',
+ 3 => '视频',
+ 4 => '文本',
+ 5 => '小程序',
+ 6 => '图文'
+ ];
+ $item['contentTypeName'] = $contentTypeMap[$item['contentType'] ?? 0] ?? '未知';
+
+ // 格式化时间
+ if (!empty($item['createMomentTime']) && is_numeric($item['createMomentTime'])) {
+ $item['createMomentTimeFormatted'] = date('Y-m-d H:i:s', (int)$item['createMomentTime']);
+ }
+ if (!empty($item['createMessageTime']) && is_numeric($item['createMessageTime'])) {
+ $item['createMessageTimeFormatted'] = date('Y-m-d H:i:s', (int)$item['createMessageTime']);
+ }
+ if (!empty($item['createTime']) && is_numeric($item['createTime'])) {
+ $item['createTimeFormatted'] = date('Y-m-d H:i:s', (int)$item['createTime']);
+ }
+
+ // 格式化发送时间
+ if (!empty($item['sendTime']) && is_numeric($item['sendTime'])) {
+ $item['sendTimeFormatted'] = date('Y-m-d H:i:s', (int)$item['sendTime']);
+ // 保持原字段兼容
+ $item['sendTime'] = $item['sendTimeFormatted'];
+ }
+
+ // 初始化发送者和群组信息
+ $item['senderInfo'] = [];
+ $item['groupInfo'] = [];
+
+ // 批量获取关联信息
+ if ($item['type'] == 'moment' && !empty($item['friendId'])) {
+ // 获取朋友圈发送者信息
+ $friendInfo = Db::name('wechat_friendship')
+ ->alias('wf')
+ ->join('wechat_account wa', 'wf.wechatId = wa.wechatId', 'LEFT')
+ ->where('wf.id', $item['friendId'])
+ ->field('wf.id, wf.wechatId, wa.nickname, wa.avatar')
+ ->find();
+
+ if ($friendInfo) {
+ $item['senderInfo'] = $friendInfo;
+ }
+ } elseif ($item['type'] == 'group_message' && !empty($item['wechatChatroomId'])) {
+ // 获取群组信息
+ $groupInfo = Db::name('wechat_group')
+ ->where('id', $item['wechatChatroomId'])
+ ->field('id, chatroomId, name, avatar, ownerWechatId')
+ ->find();
+
+ if ($groupInfo) {
+ $item['groupInfo'] = $groupInfo;
+
+ // 如果有发送者信息,也获取发送者详情
+ if (!empty($item['wechatId'])) {
+ $senderInfo = Db::table('s2_wechat_chatroom_member')
+ ->where([
+ 'chatroomId' => $groupInfo['chatroomId'],
+ 'wechatId' => $item['wechatId']
+ ])
+ ->field('wechatId, nickname, avatar')
+ ->find();
+
+ if ($senderInfo) {
+ $item['senderInfo'] = $senderInfo;
+ }
+ }
+ }
+ }
+
+ // 如果有AI内容,添加到返回数据中
+ if (!empty($item['contentAi'])) {
+ $item['contentOriginal'] = $item['content'];
+ $item['content'] = $item['contentAi'];
+ }
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => $item
+ ]);
+ }
+
+ /**
+ * 更新内容项目
+ * @return \think\response\Json
+ */
+ public function updateItem()
+ {
+ if (!$this->request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ // 获取请求参数
+ $param = $this->request->post();
+
+ // 简单验证
+ if (empty($param['id'])) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ // 查询内容项目是否存在并检查权限
+ $item = ContentItem::where([
+ ['id', '=', $param['id']],
+ ['isDel', '=', 0]
+ ])->find();
+
+ if (!$item) {
+ return json(['code' => 500, 'msg' => '内容项目不存在或无权限操作']);
+ }
+
+ try {
+ // 更新内容项目
+ $item->title = $param['title'] ?? $item->title;
+ $item->content = $param['content'] ?? $item->content;
+ $item->comment = $param['comment'] ?? $item->comment;
+
+ // 处理发送时间
+ if (!empty($param['sendTime'])) {
+ $item->sendTime = strtotime($param['sendTime']);
+ }
+
+ // 处理内容类型
+ if (isset($param['contentType'])) {
+ $item->contentType = $param['contentType'];
+ }
+
+ // 处理资源URL
+ if (isset($param['resUrls'])) {
+ $resUrls = is_string($param['resUrls']) ? json_decode($param['resUrls'], true) : $param['resUrls'];
+ $item->resUrls = json_encode($resUrls, JSON_UNESCAPED_UNICODE);
+
+ // 设置封面图片
+ if (!empty($resUrls[0])) {
+ $item->coverImage = $resUrls[0];
+ }
+ }
+
+ // 处理链接URL
+ if (isset($param['urls'])) {
+ $urls = is_string($param['urls']) ? json_decode($param['urls'], true) : $param['urls'];
+ $item->urls = json_encode($urls, JSON_UNESCAPED_UNICODE);
+ }
+
+ // 处理地理位置信息
+ if (isset($param['location'])) {
+ $item->location = $param['location'];
+ }
+ if (isset($param['lat'])) {
+ $item->lat = $param['lat'];
+ }
+ if (isset($param['lng'])) {
+ $item->lng = $param['lng'];
+ }
+
+ // 更新修改时间
+ $item->updateTime = time();
+ // 保存更新
+ $item->save();
+
+ return json(['code' => 200, 'msg' => '更新成功']);
+ } catch (\Exception $e) {
+ return json(['code' => 500, 'msg' => '更新失败:' . $e->getMessage()]);
+ }
+ }
+
+
+ public function aiEditContent()
+ {
+
+ $id = Request::param('id', '');
+ $aiPrompt = Request::param('aiPrompt', '');
+ $content = Request::param('content', '');
+ $companyId = $this->request->userInfo['companyId'];
+ // 简单验证
+ if (empty($id) && empty($content)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ if (!empty($id)) {
+ // 查询内容项目是否存在并检查权限
+ $item = ContentItem::alias('ci')
+ ->join('content_library cl', 'ci.libraryId = cl.id')
+ ->where(['ci.id' => $id, 'ci.isDel' => 0, 'cl.isDel' => 0, 'cl.companyId' => $companyId])
+ ->field('ci.*')
+ ->find();
+ } else {
+ $item['content'] = $content;
+ }
+
+ if (empty($item)) {
+ return json(['code' => 500, 'msg' => '内容项目不存在或无权限操作']);
+ }
+
+ if (empty($item['content'])) {
+ return json(['code' => 500, 'msg' => '内容不能为空']);
+ }
+ $contentFront = !empty($item['contentAi']) ? $item['contentAi'] : $item['content'];
+ if (!$this->request->isPost()) {
+ try {
+ $contentAi = $this->aiRewrite(['aiEnabled' => true, 'aiPrompt' => $aiPrompt], $contentFront);
+ if (!empty($contentAi)) {
+ return json(['code' => 200, 'msg' => 'ai编写成功', 'data' => ['contentAfter' => $contentAi, 'contentFront' => $contentFront]]);
+ } else {
+ return json(['code' => 500, 'msg' => 'ai编写失败']);
+ }
+ } catch (\Exception $e) {
+ return json(['code' => 500, 'msg' => 'ai编写失败:' . $e->getMessage()]);
+ }
+ } else {
+ if (empty($content)) {
+ return json(['code' => 500, 'msg' => '新内容不能为空']);
+ }
+ $res = ContentItem::where(['id' => $item['id']])->update(['contentAi' => $content, 'updateTime' => time()]);
+ if (!empty($res)) {
+ return json(['code' => 200, 'msg' => '更新成功']);
+ } else {
+ return json(['code' => 500, 'msg' => '更新失败']);
+ }
+ }
+ }
+
+
+ /************************************
+ * 数据采集相关功能
+ ************************************/
+
+ function getExternalPageDetails($url)
+ {
+ $html = file_get_contents($url);
+ $dom = new \DOMDocument();
+ @$dom->loadHTML($html);
+ $xpath = new \DOMXPath($dom);
+
+ // 获取标题
+ $titleNode = $xpath->query('//title');
+ $title = $titleNode->length > 0 ? $titleNode->item(0)->nodeValue : '';
+
+ // 获取图标链接
+ $iconNode = $xpath->query('//link[@rel="shortcut icon"]/@href');
+ $icon = $iconNode->length > 0 ? $iconNode->item(0)->nodeValue : '';
+
+ return ['title' => $title, 'icon' => $icon];
+ }
+
+
+ /**
+ * 执行朋友圈采集任务
+ * @return \think\response\Json
+ */
+ public function collectMoments()
+ {
+ // 查询条件:未删除且已开启的内容库
+ $where = [
+ ['isDel', '=', 0], // 未删除
+ ['status', '=', 1], // 已开启
+ ];
+
+ // 查询符合条件的内容库
+ $libraries = ContentLibrary::where($where)
+ ->field('id,name,sourceType,sourceFriends,sourceGroups,keywordInclude,keywordExclude,aiEnabled,aiPrompt,timeEnabled,timeStart,timeEnd,status,userId,companyId,createTime,updateTime,groupMembers,catchType')
+ ->order('id', 'desc')
+ ->select()->toArray();
+
+ if (empty($libraries)) {
+ return json_encode(['code' => 200, 'msg' => '没有可用的内容库配置'], 256);
+ }
+
+ $successCount = 0;
+ $failCount = 0;
+ $results = [];
+ $processedLibraries = 0;
+ $totalLibraries = count($libraries);
+
+ // 预处理内容库数据
+ foreach ($libraries as &$library) {
+ // 解析JSON字段
+ $library['sourceFriends'] = json_decode($library['sourceFriends'] ?: [], true);
+ $library['sourceGroups'] = json_decode($library['sourceGroups'] ?: [], true);
+ $library['keywordInclude'] = json_decode($library['keywordInclude'] ?: [], true);
+ $library['keywordExclude'] = json_decode($library['keywordExclude'] ?: [], true);
+ $library['groupMembers'] = json_decode($library['groupMembers'] ?: [], true);
+ $library['catchType'] = json_decode($library['catchType'] ?: [], true);
+ }
+ unset($library); // 解除引用
+
+ // 处理每个内容库的采集任务
+ foreach ($libraries as $library) {
+ try {
+ $processedLibraries++;
+
+ // 根据数据来源类型执行不同的采集逻辑
+ $collectResult = [
+ 'status' => 'skipped',
+ 'message' => '没有配置数据来源'
+ ];
+
+ switch ($library['sourceType']) {
+ case 1: // 好友类型
+ if (!empty($library['sourceFriends'])) {
+ $collectResult = $this->collectFromFriends($library);
+ }
+ break;
+
+ case 2: // 群类型
+ if (!empty($library['sourceGroups'])) {
+ $collectResult = $this->collectFromGroups($library);
+ }
+ break;
+
+ default:
+ $collectResult = [
+ 'status' => 'failed',
+ 'message' => '不支持的数据来源类型'
+ ];
+ }
+
+ // 统计成功和失败数量
+ if ($collectResult['status'] == 'success') {
+ $successCount++;
+ } elseif ($collectResult['status'] == 'failed' || $collectResult['status'] == 'error') {
+ $failCount++;
+ }
+
+ // 记录结果
+ $results[] = [
+ 'library_id' => $library['id'],
+ 'library_name' => $library['name'],
+ 'source_type' => $library['sourceType'] == 1 ? '好友' : ($library['sourceType'] == 2 ? '群组' : '未知'),
+ 'status' => $collectResult['status'],
+ 'message' => $collectResult['message'] ?? '',
+ 'data' => $collectResult['data'] ?? []
+ ];
+
+ // 每处理5个内容库,释放一次内存
+ if ($processedLibraries % 5 == 0 && $processedLibraries < $totalLibraries) {
+ gc_collect_cycles();
+ }
+ } catch (\Exception $e) {
+ $failCount++;
+ $results[] = [
+ 'library_id' => $library['id'],
+ 'library_name' => $library['name'],
+ 'source_type' => $library['sourceType'] == 1 ? '好友' : ($library['sourceType'] == 2 ? '群组' : '未知'),
+ 'status' => 'error',
+ 'message' => $e->getMessage()
+ ];
+
+ // 记录错误日志
+ \think\facade\Log::error('内容库采集错误: ' . $e->getMessage() . ' [库ID: ' . $library['id'] . ']');
+ }
+ }
+
+ // 返回采集结果
+ return json_encode([
+ 'code' => 200,
+ 'msg' => '采集任务执行完成',
+ 'data' => [
+ 'total' => $totalLibraries,
+ 'success' => $successCount,
+ 'fail' => $failCount,
+ 'skipped' => $totalLibraries - $successCount - $failCount,
+ 'results' => $results
+ ]
+ ], 256);
+ }
+
+ /**
+ * 从好友采集朋友圈内容
+ * @param array $library 内容库配置
+ * @return array 采集结果
+ */
+ private function collectFromFriends($library)
+ {
+ $friendIds = $library['sourceFriends'];
+ if (empty($friendIds)) {
+ return [
+ 'status' => 'failed',
+ 'message' => '没有指定要采集的好友'
+ ];
+ }
+
+ try {
+ // 获取API配置
+ $toAccountId = '';
+ $username = Env::get('api.username2', '');
+ $password = Env::get('api.password2', '');
+ $needFetch = false;
+
+ // 检查是否需要主动获取朋友圈
+ if (!empty($username) && !empty($password)) {
+ $toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId');
+ $needFetch = !empty($toAccountId);
+ }
+
+ // 批量查询好友信息
+ $friends = Db::table('s2_wechat_friend')
+ ->field('id, wechatAccountId, wechatId, accountId, nickname, avatar')
+ ->whereIn('id', $friendIds)
+ ->where('isDeleted', 0)
+ ->select();
+
+ if (empty($friends)) {
+ return [
+ 'status' => 'failed',
+ 'message' => '未找到有效的好友信息'
+ ];
+ }
+
+ // 从朋友圈采集内容
+ $collectedData = [];
+ $totalMomentsCount = 0;
+ $processedFriends = 0;
+ $totalFriends = count($friends);
+
+ // 获取采集类型限制
+ $catchTypes = $library['catchType'] ?? [];
+
+ foreach ($friends as $friend) {
+ $processedFriends++;
+
+ // 如果配置了API并且需要主动获取朋友圈
+ if ($needFetch) {
+ try {
+ // 执行切换好友命令
+ $automaticAssign = new AutomaticAssign();
+ $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['id'], 'toAccountId' => $toAccountId], true);
+
+ // 存入缓存
+ $friendData = $friend;
+ $friendData['friendId'] = $friend['id'];
+ artificialAllotWechatFriend($friendData);
+
+ // 执行采集朋友圈命令
+ $webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]);
+ $webSocket->getMoments(['wechatFriendId' => $friend['id'], 'wechatAccountId' => $friend['wechatAccountId']]);
+
+ // 采集完毕切换回原账号
+ $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['id'], 'toAccountId' => $friend['accountId']], true);
+ } catch (\Exception $e) {
+ \think\facade\Log::error('采集朋友圈失败: ' . $e->getMessage() . ' [好友ID: ' . $friend['id'] . ']');
+ // 继续处理下一个好友,不中断整个流程
+ }
+ }
+
+ // 从s2_wechat_moments表获取朋友圈数据
+ $query = Db::table('s2_wechat_moments')
+ ->where([
+ //'wechatAccountId' => $friend['wechatAccountId'],
+ 'userName' => $friend['wechatId'],
+ ])
+ ->order('createTime', 'desc')
+ ->group('snsId');
+
+ // 如果启用了时间限制
+ if ($library['timeEnabled'] && $library['timeStart'] > 0 && $library['timeEnd'] > 0) {
+ $query->whereBetween('createTime', [$library['timeStart'], $library['timeEnd']]);
+ }
+
+ // 如果指定了采集类型,进行过滤
+ /*if (!empty($catchTypes)) {
+ $query->whereIn('type', $catchTypes);
+ }*/
+
+ // 获取最近20条朋友圈
+ $moments = $query->page(1, 100)->select();
+ if (empty($moments)) {
+ continue;
+ }
+
+ $nickname = $friend['nickname'] ?? '未知好友';
+ $friendMomentsCount = 0;
+ $filteredMoments = [];
+
+ // 处理每条朋友圈数据
+ foreach ($moments as $moment) {
+ // 处理关键词过滤
+ $content = $moment['content'] ?? '';
+
+ // 应用关键词过滤
+ if (!$this->passKeywordFilter($content, $library['keywordInclude'], $library['keywordExclude'])) {
+ continue;
+ }
+
+ /* // 如果启用了AI处理
+ if (!empty($library['aiEnabled']) && !empty($content)) {
+ try {
+ $contentAi = $this->aiRewrite($library, $content);
+ if (!empty($contentAi)) {
+ $moment['contentAi'] = $contentAi;
+ }
+ } catch (\Exception $e) {
+ \think\facade\Log::error('AI处理失败: ' . $e->getMessage() . ' [朋友圈ID: ' . ($moment['id'] ?? 'unknown') . ']');
+ $moment['contentAi'] = '';
+ }
+ }*/
+
+ // 保存到内容库的content_item表
+ if ($this->saveMomentToContentItem($moment, $library['id'], $friend, $nickname)) {
+ $friendMomentsCount++;
+ $filteredMoments[] = [
+ 'id' => $moment['id'] ?? '',
+ 'content' => mb_substr($content, 0, 50) . (mb_strlen($content) > 50 ? '...' : ''),
+ 'time' => date('Y-m-d H:i:s', $moment['createTime'] ?? time())
+ ];
+ }
+ }
+
+ if ($friendMomentsCount > 0) {
+ // 记录采集结果
+ $collectedData[$friend['wechatId']] = [
+ 'friendId' => $friend['id'],
+ 'nickname' => $nickname,
+ 'count' => $friendMomentsCount,
+ 'samples' => array_slice($filteredMoments, 0, 3) // 只保留前3条示例
+ ];
+
+ $totalMomentsCount += $friendMomentsCount;
+ }
+
+ // 每处理5个好友,释放一次内存
+ if ($processedFriends % 5 == 0 && $processedFriends < $totalFriends) {
+ gc_collect_cycles();
+ }
+ }
+
+ if (empty($collectedData)) {
+ return [
+ 'status' => 'warning',
+ 'message' => '未采集到任何朋友圈内容'
+ ];
+ }
+
+ return [
+ 'status' => 'success',
+ 'message' => '成功采集到' . count($collectedData) . '位好友的' . $totalMomentsCount . '条朋友圈内容',
+ 'data' => [
+ 'friend_count' => count($collectedData),
+ 'collected_count' => $totalMomentsCount,
+ 'details' => $collectedData
+ ]
+ ];
+
+ } catch (\Exception $e) {
+ \think\facade\Log::error('从好友采集朋友圈失败: ' . $e->getMessage() . ' [内容库ID: ' . ($library['id'] ?? 'unknown') . ']');
+ return [
+ 'status' => 'error',
+ 'message' => '采集过程发生错误: ' . $e->getMessage()
+ ];
+ }
+ }
+
+ /**
+ * 应用关键词过滤规则
+ * @param string $content 内容文本
+ * @param array $includeKeywords 包含关键词
+ * @param array $excludeKeywords 排除关键词
+ * @return bool 是否通过过滤
+ */
+ private function passKeywordFilter($content, $includeKeywords, $excludeKeywords)
+ {
+ // 如果内容为空,跳过
+ if (empty($content)) {
+ return false;
+ }
+
+ // 检查是否包含必须关键词
+ $includeMatch = empty($includeKeywords);
+ if (!empty($includeKeywords)) {
+ foreach ($includeKeywords as $keyword) {
+ if (strpos($content, $keyword) !== false) {
+ $includeMatch = true;
+ break;
+ }
+ }
+ }
+
+ // 如果不满足包含条件,跳过
+ if (!$includeMatch) {
+ return false;
+ }
+
+ // 检查是否包含排除关键词
+ if (!empty($excludeKeywords)) {
+ foreach ($excludeKeywords as $keyword) {
+ if (strpos($content, $keyword) !== false) {
+ return false; // 包含排除关键词,不通过过滤
+ }
+ }
+ }
+
+ return true; // 通过所有过滤条件
+ }
+
+ /**
+ * 从群组采集消息内容
+ * @param array $library 内容库配置
+ * @return array 采集结果
+ */
+ private function collectFromGroups($library)
+ {
+ $groupIds = $library['sourceGroups'];
+ if (empty($groupIds)) {
+ return [
+ 'status' => 'failed',
+ 'message' => '没有指定要采集的群组'
+ ];
+ }
+
+ try {
+ // 查询群组信息
+ $groups = Db::table('s2_wechat_chatroom')->alias('g')
+ ->field('g.id, g.chatroomId, g.nickname as name, g.wechatAccountWechatId as ownerWechatId')
+ ->whereIn('g.id', $groupIds)
+ ->where('g.deleteTime', 0)
+ ->select();
+
+ if (empty($groups)) {
+ return [
+ 'status' => 'failed',
+ 'message' => '未找到有效的群组信息'
+ ];
+ }
+
+ // 获取群成员信息
+ $groupMembers = $library['groupMembers'];
+ if (empty($groupMembers)) {
+ // 如果没有指定群成员,则尝试获取所有群成员
+ return [
+ 'status' => 'failed',
+ 'message' => '未找到有效的群成员信息'
+ ];
+ }
+
+ // groupMembers格式: {"826825": ["413771", "413769"], "840818": ["496300", "496302"]}
+ // 键是群组ID,值是该群组的成员ID数组
+ // 需要按群组分组处理,确保每个群组只采集该群组配置的成员
+
+ // 建立群组ID到成员ID数组的映射
+ $groupIdToMemberIds = [];
+ if (is_array($groupMembers)) {
+ foreach ($groupMembers as $groupId => $memberIds) {
+ if (is_array($memberIds) && !empty($memberIds)) {
+ $groupIdToMemberIds[$groupId] = $memberIds;
+ }
+ }
+ }
+ if (empty($groupIdToMemberIds)) {
+ return [
+ 'status' => 'failed',
+ 'message' => '未找到有效的群成员ID'
+ ];
+ }
+
+ // 为每个群组查询成员信息,建立群组ID到成员wechatId数组的映射
+ $groupIdToMemberWechatIds = [];
+ foreach ($groupIdToMemberIds as $groupId => $memberIds) {
+ // 查询该群组的成员信息,获取wechatId
+ $members = Db::table('s2_wechat_chatroom_member')
+ ->field('id, wechatId')
+ ->whereIn('id', $memberIds)
+ ->select();
+
+ $wechatIds = [];
+ foreach ($members as $member) {
+ if (!empty($member['wechatId'])) {
+ $wechatIds[] = $member['wechatId'];
+ }
+ }
+
+ if (!empty($wechatIds)) {
+ $groupIdToMemberWechatIds[$groupId] = array_unique($wechatIds);
+ }
+ }
+ if (empty($groupIdToMemberWechatIds)) {
+ return [
+ 'status' => 'failed',
+ 'message' => '未找到有效的群成员微信ID'
+ ];
+ }
+
+ // 从群组采集内容
+ $collectedData = [];
+ $totalMessagesCount = 0;
+ $chatroomIds = array_column($groups, 'id');
+
+ // 获取群消息 - 支持时间范围过滤(先不添加群成员过滤,后面按群组分别过滤)
+ $messageWhere = [
+ ['wechatChatroomId', 'in', $chatroomIds],
+ ['type', '=', 2]
+ ];
+
+ // 如果启用时间限制
+ if ($library['timeEnabled'] && $library['timeStart'] > 0 && $library['timeEnd'] > 0) {
+ $messageWhere[] = ['createTime', 'between', [$library['timeStart'], $library['timeEnd']]];
+ }
+
+ // 查询群消息(先查询所有消息,后面按群组和成员过滤)
+ $groupMessages = Db::table('s2_wechat_message')
+ ->where($messageWhere)
+ ->order('createTime', 'desc')
+ ->limit(500) // 限制最大消息数量
+ ->select();
+ if (empty($groupMessages)) {
+ return [
+ 'status' => 'warning',
+ 'message' => '未找到符合条件的群消息'
+ ];
+ }
+ // 按群组分组处理消息
+ $groupedMessages = [];
+ foreach ($groupMessages as $message) {
+ $chatroomId = $message['wechatChatroomId'];
+ $senderWechatId = $message['senderWechatId'] ?? '';
+
+ // 找到对应的群组信息
+ $groupInfo = null;
+ foreach ($groups as $group) {
+ if ($group['id'] == $chatroomId) {
+ $groupInfo = $group;
+ break;
+ }
+ }
+
+ if (!$groupInfo) {
+ continue;
+ }
+
+ // 检查该消息的发送者是否在该群组的配置成员列表中
+ $groupId = $groupInfo['id'];
+ if (!isset($groupIdToMemberWechatIds[$groupId])) {
+ // 该群组没有配置成员,跳过
+ continue;
+ }
+
+ // 检查发送者是否在配置的成员列表中
+ if (!in_array($senderWechatId, $groupIdToMemberWechatIds[$groupId])) {
+ // 发送者不在该群组的配置成员列表中,跳过
+ continue;
+ }
+
+ if (!isset($groupedMessages[$chatroomId])) {
+ $groupedMessages[$chatroomId] = [
+ 'count' => 0,
+ 'messages' => []
+ ];
+ }
+
+ // 处理消息内容
+ $content = $message['content'] ?? '';
+
+ // 如果启用了关键词过滤
+ $includeKeywords = $library['keywordInclude'];
+ $excludeKeywords = $library['keywordExclude'];
+
+
+ // 检查是否包含必须关键词
+ $includeMatch = empty($includeKeywords);
+ if (!empty($includeKeywords)) {
+ foreach ($includeKeywords as $keyword) {
+ if (strpos($content, $keyword) !== false) {
+ $includeMatch = true;
+ break;
+ }
+ }
+ }
+
+ // 如果不满足包含条件,跳过
+ if (!$includeMatch) {
+ continue;
+ }
+
+ // 检查是否包含排除关键词
+ $excludeMatch = false;
+ if (!empty($excludeKeywords)) {
+ foreach ($excludeKeywords as $keyword) {
+ if (strpos($content, $keyword) !== false) {
+ $excludeMatch = true;
+ break;
+ }
+ }
+ }
+
+ // 如果满足排除条件,跳过
+ if ($excludeMatch) {
+ continue;
+ }
+
+
+ // 如果启用了AI处理
+ if (!empty($library['aiEnabled']) && !empty($content)) {
+ $contentAi = $this->aiRewrite($library, $content);
+ if (!empty($contentAi)) {
+ $message['contentAi'] = $contentAi;
+ } else {
+ $message['contentAi'] = '';
+ }
+ }
+
+
+ // 保存消息到内容库
+ $this->saveMessageToContentItem($message, $library['id'], $groupInfo);
+
+ // 累计计数
+ $groupedMessages[$chatroomId]['count']++;
+ $groupedMessages[$chatroomId]['messages'][] = [
+ 'id' => $message['id'],
+ 'content' => mb_substr($content, 0, 50) . (mb_strlen($content) > 50 ? '...' : ''),
+ 'sender' => $message['senderNickname'],
+ 'time' => date('Y-m-d H:i:s', $message['createTime'])
+ ];
+
+ $totalMessagesCount++;
+ }
+
+ // 构建结果数据
+ foreach ($groups as $group) {
+ $chatroomId = $group['chatroomId'];
+ if (isset($groupedMessages[$chatroomId]) && $groupedMessages[$chatroomId]['count'] > 0) {
+ $collectedData[$chatroomId] = [
+ 'groupId' => $group['id'],
+ 'groupName' => $group['name'],
+ 'count' => $groupedMessages[$chatroomId]['count'],
+ 'messages' => $groupedMessages[$chatroomId]['messages']
+ ];
+ }
+ }
+
+ if (empty($collectedData)) {
+ return [
+ 'status' => 'warning',
+ 'message' => '未采集到符合条件的群消息内容'
+ ];
+ }
+
+ return [
+ 'status' => 'success',
+ 'message' => '成功采集到' . count($collectedData) . '个群的' . $totalMessagesCount . '条消息',
+ 'data' => [
+ 'group_count' => count($collectedData),
+ 'collected_count' => $totalMessagesCount,
+ 'details' => $collectedData
+ ]
+ ];
+
+ } catch (\Exception $e) {
+ return [
+ 'status' => 'error',
+ 'message' => '采集过程发生错误: ' . $e->getMessage()
+ ];
+ }
+ }
+
+ /**
+ * 判断内容类型
+ * @param string $content 内容文本
+ * @param array $resUrls 资源URL数组
+ * @param array $urls URL数组
+ * @return int 内容类型: 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序
+ */
+ private function determineContentType($content, $resUrls = [], $urls = [])
+ {
+ // 判断是否为空
+ if (empty($content) && empty($resUrls) && empty($urls)) {
+ return 0; // 未知类型
+ }
+
+ // 分析内容中可能包含的链接或图片地址
+ if (!empty($content)) {
+ // 检查内容中是否有链接
+ $urlPattern = '/https?:\/\/[-A-Za-z0-9+&@#\/%?=~_|!:,.;]+[-A-Za-z0-9+&@#\/%=~_|]/';
+ preg_match_all($urlPattern, $content, $contentUrlMatches);
+
+ if (!empty($contentUrlMatches[0])) {
+ // 将内容中的链接添加到urls数组中(去重)
+ foreach ($contentUrlMatches[0] as $url) {
+ if (!in_array($url, $urls)) {
+ $urls[] = $url;
+ }
+ }
+ }
+
+ // 检查内容中是否包含图片或视频链接
+ foreach ($contentUrlMatches[0] ?? [] as $url) {
+ // 检查是否为图片文件
+ if (stripos($url, '.jpg') !== false ||
+ stripos($url, '.jpeg') !== false ||
+ stripos($url, '.png') !== false ||
+ stripos($url, '.gif') !== false ||
+ stripos($url, '.webp') !== false ||
+ stripos($url, '.bmp') !== false ||
+ stripos($url, 'image') !== false) {
+ if (!in_array($url, $resUrls)) {
+ $resUrls[] = $url;
+ }
+ }
+
+ // 检查是否为视频文件
+ if (stripos($url, '.mp4') !== false ||
+ stripos($url, '.mov') !== false ||
+ stripos($url, '.avi') !== false ||
+ stripos($url, '.wmv') !== false ||
+ stripos($url, '.flv') !== false ||
+ stripos($url, 'video') !== false) {
+ if (!in_array($url, $resUrls)) {
+ $resUrls[] = $url;
+ }
+ }
+ }
+ }
+
+ // 判断是否有小程序信息
+ if (strpos($content, '小程序') !== false || strpos($content, 'appid') !== false) {
+ return 5; // 小程序
+ }
+
+ // 检查资源URL中是否有视频或图片
+ $hasVideo = false;
+ $hasImage = false;
+
+ if (!empty($resUrls)) {
+ foreach ($resUrls as $url) {
+ // 检查是否为视频文件
+ if (stripos($url, '.mp4') !== false ||
+ stripos($url, '.mov') !== false ||
+ stripos($url, '.avi') !== false ||
+ stripos($url, '.wmv') !== false ||
+ stripos($url, '.flv') !== false ||
+ stripos($url, 'video') !== false) {
+ $hasVideo = true;
+ break; // 一旦发现视频文件,立即退出循环
+ }
+
+ // 检查是否为图片文件
+ if (stripos($url, '.jpg') !== false ||
+ stripos($url, '.jpeg') !== false ||
+ stripos($url, '.png') !== false ||
+ stripos($url, '.gif') !== false ||
+ stripos($url, '.webp') !== false ||
+ stripos($url, '.bmp') !== false ||
+ stripos($url, 'image') !== false) {
+ $hasImage = true;
+ // 不退出循环,继续检查是否有视频(视频优先级更高)
+ }
+ }
+ }
+
+ // 如果发现视频文件,判定为视频类型
+ if ($hasVideo) {
+ return 3; // 视频
+ }
+
+ // 判断内容是否纯链接
+ $isPureLink = false;
+ if (!empty($content) && !empty($urls)) {
+ $contentWithoutUrls = $content;
+ foreach ($urls as $url) {
+ $contentWithoutUrls = str_replace($url, '', $contentWithoutUrls);
+ }
+ // 如果去除链接后内容为空,则认为是纯链接
+ if (empty(trim($contentWithoutUrls))) {
+ $isPureLink = true;
+ }
+ }
+
+ // 如果内容是纯链接,判定为链接类型
+ if ($isPureLink) {
+ return 2; // 链接
+ }
+
+ // 优先判断内容文本
+ // 如果有文本内容(不仅仅是链接)
+ if (!empty($content) && !$isPureLink) {
+ // 如果有图片,则为图文类型
+ if ($hasImage) {
+ return 1; // 图文
+ } else {
+ return 4; // 纯文本
+ }
+ }
+
+ // 判断是否为图片类型
+ if ($hasImage) {
+ return 1; // 图片
+ }
+
+ // 判断是否为链接类型
+ if (!empty($urls)) {
+ return 2; // 链接
+ }
+
+ // 默认为文本类型
+ return 4; // 文本
+ }
+
+ /**
+ * 保存朋友圈数据到内容项目表
+ * @param array $moment 朋友圈数据
+ * @param int $libraryId 内容库ID
+ * @param array $friend 好友信息
+ * @param string $nickname 好友昵称
+ * @return bool 是否保存成功
+ */
+ private function saveMomentToContentItem($moment, $libraryId, $friend, $nickname)
+ {
+ if (empty($moment) || empty($libraryId)) {
+ return false;
+ }
+
+
+ try {
+
+ // 检查朋友圈数据是否已存在于内容项目中
+ $exists = ContentItem::where('libraryId', $libraryId)
+ ->where('snsId', $moment['snsId'] ?? '')
+ ->find();
+
+
+ // 解析资源URL (可能是JSON字符串)
+ $resUrls = $moment['resUrls'];
+ if (is_string($resUrls)) {
+ $resUrls = json_decode($resUrls, true);
+ }
+
+ // 处理urls字段
+ $urls = $moment['urls'] ?? [];
+ if (is_string($urls)) {
+ $urls = json_decode($urls, true);
+ }
+
+ // 构建封面图片
+ $coverImage = '';
+ if (!empty($resUrls) && is_array($resUrls) && count($resUrls) > 0) {
+ $coverImage = $resUrls[0];
+ }
+
+ // 判断内容类型 (0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序)
+ if ($moment['type'] == 1) {
+ //图文
+ $contentType = 1;
+ } elseif ($moment['type'] == 3) {
+ //链接
+ $contentType = 2;
+ $urls = [];
+ $url = is_string($moment['urls']) ? json_decode($moment['urls'], true) : $moment['urls'] ?? [];
+ $url = $url[0];
+
+ //兼容链接采集不到标题及图标
+ if (empty($moment['title'])) {
+ // 检查是否是飞书链接
+ if (strpos($url, 'feishu.cn') !== false) {
+ // 飞书文档需要登录,无法直接获取内容,返回默认信息
+ $urls[] = [
+ 'url' => $url,
+ 'image' => 'http://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/2025/07/09/3db2a5d7fe49011ab68175a42a5094ce.jpeg',
+ 'desc' => '飞书文档'
+ ];
+ } else {
+ $getUrlDetails = $this->getExternalPageDetails($url);
+ $icon = 'http://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/2025/07/09/ec039d96fad6eab1d960f207d3d9ca9f.jpeg';
+ if (!empty($getUrlDetails['title'])) {
+ $urls[] = [
+ 'url' => $url,
+ 'image' => $icon,
+ 'desc' => '点击查看详情'
+ ];
+ } else {
+ $urls[] = [
+ 'url' => $url,
+ 'image' => !empty($getUrlDetails['icon']) ? $getUrlDetails['icon'] : $icon,
+ 'desc' => $getUrlDetails['title']
+ ];
+ }
+ }
+ } else {
+ if (strpos($url, 'feishu.cn') !== false) {
+ $coverImage = 'http://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/2025/07/09/3db2a5d7fe49011ab68175a42a5094ce.jpeg';
+ } else {
+ $coverImage = 'http://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/2025/07/09/ec039d96fad6eab1d960f207d3d9ca9f.jpeg';
+ }
+
+ $urls[] = [
+ 'url' => $url,
+ 'image' => !empty($moment['coverImage']) ? $moment['coverImage'] : $coverImage,
+ 'desc' => $moment['title']
+ ];
+ }
+ $moment['urls'] = $urls;
+ } elseif ($moment['type'] == 15) {
+ //视频
+ $contentType = 3;
+ } elseif ($moment['type'] == 2) {
+ //纯文本
+ $contentType = 4;
+ } elseif ($moment['type'] == 30) {
+ //小程序
+ $contentType = 5;
+ } else {
+ $contentType = 1;
+ }
+
+ // 如果不存在,则创建新的内容项目
+
+ if (empty($exists)) {
+ $exists = new ContentItem();
+ }
+
+ $exists->libraryId = $libraryId;
+ $exists->type = 'moment'; // 朋友圈类型
+ $exists->title = '来自 ' . $nickname . ' 的朋友圈';
+ $exists->contentData = json_encode($moment, JSON_UNESCAPED_UNICODE);
+ $exists->snsId = $moment['snsId'] ?? ''; // 存储snsId便于后续查询
+ $exists->createTime = time();
+ $exists->wechatId = $friend['wechatId'];
+ $exists->friendId = $friend['id'];
+ $exists->createMomentTime = $moment['createTime'] ?? 0;
+ $exists->content = $moment['content'] ?? '';
+ $exists->contentAi = $moment['contentAi'] ?? '';
+ $exists->coverImage = $coverImage;
+ $exists->contentType = $contentType; // 设置内容类型
+ $exists->ossUrls = $moment['ossUrls'] ?? json_decode([]);
+
+ // 独立存储resUrls和urls字段
+ $exists->resUrls = is_string($moment['resUrls']) ? $moment['resUrls'] : json_encode($resUrls, JSON_UNESCAPED_UNICODE);
+ $exists->urls = is_string($moment['urls']) ? $moment['urls'] : json_encode($urls, JSON_UNESCAPED_UNICODE);
+
+ // 保存地理位置信息
+ $exists->location = $moment['location'] ?? '';
+ $exists->lat = $moment['lat'] ?? 0;
+ $exists->lng = $moment['lng'] ?? 0;
+ $exists->save();
+
+
+ return true;
+ } catch (\Exception $e) {
+ // 记录错误日志
+ \think\facade\Log::error('保存朋友圈数据失败: ' . $e->getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * 保存群聊消息到内容项目表
+ * @param array $message 消息数据
+ * @param int $libraryId 内容库ID
+ * @param array $group 群组信息
+ * @return bool 是否保存成功
+ */
+ private function saveMessageToContentItem($message, $libraryId, $group)
+ {
+ if (empty($message) || empty($libraryId)) {
+ return false;
+ }
+
+ try {
+ // 检查消息是否已存在于内容项目中
+ $exists = ContentItem::where('libraryId', $libraryId)
+ ->where('msgId', $message['msgSvrId'] ?? '')
+ ->find();
+
+ if ($exists) {
+ return true;
+ }
+
+ $resUrls = [];
+ $links = [];
+ $contentType = 4;
+ $content = '';
+ switch ($message['msgType']) {
+ case 1: // 文字
+ $content = $message['content'];
+ $contentType = 4;
+ break;
+ case 3: //图片
+ $resUrls[] = $message['content'];
+ $contentType = 1;
+ break;
+ case 47: //动态图片
+ $resUrls[] = $message['content'];
+ $contentType = 1;
+ break;
+ case 34: //语言
+ return false;
+ case 43: //视频
+ $resUrls[] = $message['content'];
+ $contentType = 3;
+ break;
+ case 42: //名片
+ return false;
+ case 49: //文件 链接
+ $link = json_decode($message['content'], true);
+ switch ($link['type']) {
+ case 'link':
+ $links[] = [
+ 'desc' => $link['desc'],
+ 'image' => $link['thumbPath'],
+ 'url' => $link['url'],
+ ];
+ $contentType = 2;
+ break;
+ default:
+ return false;
+ }
+ break;
+ default:
+ return false;
+ }
+
+ // 创建新的内容项目
+ $item = new ContentItem();
+ $item->libraryId = $libraryId;
+ $item->type = 'group_message'; // 群消息类型
+ $item->title = '来自 ' . ($group['name'] ?? '未知群组') . ' 的消息';
+ $item->contentData = json_encode($message, JSON_UNESCAPED_UNICODE);
+ $item->msgId = $message['msgSvrId'] ?? ''; // 存储msgSvrId便于后续查询
+ $item->createTime = time();
+ $item->content = $content;
+ $item->contentType = $contentType; // 设置内容类型
+
+ // 设置发送者信息
+ $item->wechatId = $message['senderWechatId'] ?? '';
+ $item->wechatChatroomId = $message['wechatChatroomId'] ?? '';
+ $item->senderNickname = $message['senderNickname'] ?? '';
+ $item->createMessageTime = $message['createTime'] ?? 0;
+
+ // 处理资源URL
+ if (!empty($resUrls)) {
+ $item->resUrls = json_encode($resUrls, JSON_UNESCAPED_UNICODE);
+ // 设置封面图片
+ if (!empty($resUrls[0])) {
+ $item->coverImage = $resUrls[0];
+ }
+ } else {
+ $item->resUrls = json_encode([], JSON_UNESCAPED_UNICODE);
+ }
+
+ // 处理链接
+ if (!empty($links)) {
+ $item->urls = json_encode($links, JSON_UNESCAPED_UNICODE);
+ } else {
+ $item->urls = json_encode([], JSON_UNESCAPED_UNICODE);
+ }
+ $item->ossUrls = json_encode([], JSON_UNESCAPED_UNICODE);
+
+ // 设置商品信息(需根据消息内容解析)
+ $this->extractProductInfo($item, $content);
+
+ $item->save();
+ return true;
+ } catch (\Exception $e) {
+ // 记录错误日志
+ \think\facade\Log::error('保存群消息数据失败: ' . $e->getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * 从消息内容中提取商品信息
+ * @param ContentItem $item 内容项目对象
+ * @param string $content 消息内容
+ * @return void
+ */
+ private function extractProductInfo($item, $content)
+ {
+ // 尝试提取商品名称
+ $titlePatterns = [
+ '/【(.+?)】/', // 匹配【】中的内容
+ '/《(.+?)》/', // 匹配《》中的内容
+ '/商品名称[::](.+?)[\r\n]/' // 匹配"商品名称:"后的内容
+ ];
+
+ foreach ($titlePatterns as $pattern) {
+ preg_match($pattern, $content, $matches);
+ if (!empty($matches[1])) {
+ $item->productTitle = trim($matches[1]);
+ break;
+ }
+ }
+
+ // 如果没有找到商品名称,尝试使用内容的前部分作为标题
+ if (empty($item->productTitle)) {
+ // 获取第一行非空内容作为标题
+ $lines = explode("\n", $content);
+ foreach ($lines as $line) {
+ $line = trim($line);
+ if (!empty($line) && mb_strlen($line) > 2) {
+ $item->productTitle = mb_substr($line, 0, 30);
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * 获取朋友圈数据
+ * @param string $wechatId 微信ID
+ * @return array 朋友圈数据
+ */
+ private function getMomentsData($wechatId)
+ {
+ // 这里应该是实际从API或数据库获取朋友圈数据的逻辑
+ // 这里仅作示例返回
+ return [
+ // 示例数据
+ ['id' => 1, 'content' => '今天天气真好!', 'createTime' => time() - 3600],
+ ['id' => 2, 'content' => '分享一个有趣的项目', 'createTime' => time() - 7200],
+ ];
+ }
+
+ /**
+ * 根据关键词过滤朋友圈内容
+ * @param array $moments 朋友圈内容
+ * @param array $includeKeywords 包含关键词
+ * @param array $excludeKeywords 排除关键词
+ * @return array 过滤后的内容
+ */
+ private function filterMomentsByKeywords($moments, $includeKeywords, $excludeKeywords)
+ {
+ if (empty($moments)) {
+ return [];
+ }
+
+ $filtered = [];
+ foreach ($moments as $moment) {
+ $content = $moment['content'] ?? '';
+
+ // 如果内容为空,跳过
+ if (empty($content)) {
+ continue;
+ }
+
+ // 检查是否包含必须关键词
+ $includeMatch = empty($includeKeywords);
+ if (!empty($includeKeywords)) {
+ foreach ($includeKeywords as $keyword) {
+ if (strpos($content, $keyword) !== false) {
+ $includeMatch = true;
+ break;
+ }
+ }
+ }
+
+ // 如果不满足包含条件,跳过
+ if (!$includeMatch) {
+ continue;
+ }
+
+ // 检查是否包含排除关键词
+ $excludeMatch = false;
+ if (!empty($excludeKeywords)) {
+ foreach ($excludeKeywords as $keyword) {
+ if (strpos($content, $keyword) !== false) {
+ $excludeMatch = true;
+ break;
+ }
+ }
+ }
+
+ // 如果满足排除条件,跳过
+ if ($excludeMatch) {
+ continue;
+ }
+
+ // 通过所有过滤,添加到结果中
+ $filtered[] = $moment;
+ }
+
+ return $filtered;
+ }
+
+ /**
+ * 使用AI处理采集的数据
+ * @param array $data 采集的数据
+ * @param string $prompt AI提示词
+ * @return array 处理后的数据
+ */
+ private function processWithAI($data, $prompt)
+ {
+ // 这里应该是调用AI处理数据的逻辑
+ // 实际实现需要根据具体的AI API
+ return $data;
+ }
+
+ /**
+ * 保存采集的数据到内容项目
+ * @param array $data 采集的数据
+ * @param int $libraryId 内容库ID
+ * @return bool 是否保存成功
+ */
+ private function saveCollectedData($data, $libraryId)
+ {
+ if (empty($data) || empty($libraryId)) {
+ return false;
+ }
+
+ try {
+ foreach ($data as $wechatId => $userData) {
+ foreach ($userData['moments'] as $moment) {
+ // 创建内容项目
+ $item = new ContentItem;
+ $item->libraryId = $libraryId;
+ $item->type = 'moment'; // 朋友圈类型
+ $item->title = '来自 ' . $userData['nickname'] . ' 的朋友圈';
+ $item->contentData = json_encode($moment);
+ $item->createTime = time();
+ $item->save();
+ }
+ }
+ return true;
+ } catch (\Exception $e) {
+ // 记录错误日志
+ \think\facade\Log::error('保存采集数据失败: ' . $e->getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * 获取所有群成员
+ * @param array $groupIds 群组ID列表
+ * @return array 群成员列表
+ */
+ private function getAllGroupMembers($groupIds)
+ {
+ if (empty($groupIds)) {
+ return [];
+ }
+
+ try {
+ // 查询群成员信息
+ $members = Db::name('wechat_group_member')->alias('gm')
+ ->field('gm.id, gm.memberId, gm.groupId, wa.nickname')
+ ->join('wechat_account wa', 'gm.memberId = wa.wechatId')
+ ->whereIn('gm.groupId', $groupIds)
+ ->where('gm.isDel', 0)
+ ->select();
+
+ return $members;
+ } catch (\Exception $e) {
+ \think\facade\Log::error('获取群成员失败: ' . $e->getMessage());
+ return [];
+ }
+ }
+
+
+ /**
+ * 解析URL获取网页信息(内部调用)
+ * @param string $url 要解析的URL
+ * @return array 包含title、icon的数组,失败返回空数组
+ */
+ public function parseUrl($url)
+ {
+ if (empty($url) || !filter_var($url, FILTER_VALIDATE_URL)) {
+ return [];
+ }
+
+ try {
+ // 设置请求头,模拟浏览器访问
+ $context = stream_context_create([
+ 'http' => [
+ 'method' => 'GET',
+ 'header' => [
+ 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
+ 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
+ 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8',
+ 'Accept-Encoding: gzip, deflate',
+ 'Connection: keep-alive',
+ 'Upgrade-Insecure-Requests: 1'
+ ],
+ 'timeout' => 10,
+ 'follow_location' => true,
+ 'max_redirects' => 3
+ ]
+ ]);
+
+ // 获取网页内容
+ $html = @file_get_contents($url, false, $context);
+
+ if ($html === false) {
+ return [];
+ }
+
+ // 检测编码并转换为UTF-8
+ $encoding = mb_detect_encoding($html, ['UTF-8', 'GBK', 'GB2312', 'BIG5', 'ASCII']);
+ if ($encoding && $encoding !== 'UTF-8') {
+ $html = mb_convert_encoding($html, 'UTF-8', $encoding);
+ }
+
+ // 解析HTML
+ $dom = new \DOMDocument();
+ @$dom->loadHTML($html, LIBXML_NOERROR | LIBXML_NOWARNING);
+ $xpath = new \DOMXPath($dom);
+
+ $result = [
+ 'title' => '',
+ 'icon' => '',
+ 'url' => $url
+ ];
+
+ // 提取标题
+ $titleNodes = $xpath->query('//title');
+ if ($titleNodes->length > 0) {
+ $result['title'] = trim($titleNodes->item(0)->textContent);
+ }
+
+ // 提取图标 - 优先获取favicon
+ $iconNodes = $xpath->query('//link[@rel="icon"]/@href | //link[@rel="shortcut icon"]/@href | //link[@rel="apple-touch-icon"]/@href');
+ if ($iconNodes->length > 0) {
+ $iconUrl = trim($iconNodes->item(0)->value);
+ $result['icon'] = $this->makeAbsoluteUrl($iconUrl, $url);
+ } else {
+ // 尝试获取Open Graph图片
+ $ogImageNodes = $xpath->query('//meta[@property="og:image"]/@content');
+ if ($ogImageNodes->length > 0) {
+ $result['icon'] = trim($ogImageNodes->item(0)->value);
+ } else {
+ // 默认favicon路径
+ $result['icon'] = $this->makeAbsoluteUrl('/favicon.ico', $url);
+ }
+ }
+
+ // 清理和验证数据
+ $result['title'] = $this->cleanText($result['title']);
+
+ return $result;
+
+ } catch (\Exception $e) {
+ // 记录错误日志但不抛出异常
+ \think\facade\Log::error('URL解析失败: ' . $e->getMessage() . ' URL: ' . $url);
+ return [];
+ }
+ }
+
+
+ /**
+ * 将相对URL转换为绝对URL
+ * @param string $relativeUrl 相对URL
+ * @param string $baseUrl 基础URL
+ * @return string 绝对URL
+ */
+ private function makeAbsoluteUrl($relativeUrl, $baseUrl)
+ {
+ if (empty($relativeUrl)) {
+ return '';
+ }
+
+ // 如果已经是绝对URL,直接返回
+ if (filter_var($relativeUrl, FILTER_VALIDATE_URL)) {
+ return $relativeUrl;
+ }
+
+ // 解析基础URL
+ $baseParts = parse_url($baseUrl);
+ if (!$baseParts) {
+ return $relativeUrl;
+ }
+
+ // 处理以/开头的绝对路径
+ if (strpos($relativeUrl, '/') === 0) {
+ return $baseParts['scheme'] . '://' . $baseParts['host'] .
+ (isset($baseParts['port']) ? ':' . $baseParts['port'] : '') .
+ $relativeUrl;
+ }
+
+ // 处理相对路径
+ $basePath = isset($baseParts['path']) ? dirname($baseParts['path']) : '/';
+ if ($basePath === '.') {
+ $basePath = '/';
+ }
+
+ return $baseParts['scheme'] . '://' . $baseParts['host'] .
+ (isset($baseParts['port']) ? ':' . $baseParts['port'] : '') .
+ $basePath . '/' . $relativeUrl;
+ }
+
+ /**
+ * 清理文本内容
+ * @param string $text 要清理的文本
+ * @return string 清理后的文本
+ */
+ private function cleanText($text)
+ {
+ if (empty($text)) {
+ return '';
+ }
+
+ // 移除HTML实体
+ $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
+
+ // 移除多余的空白字符
+ $text = preg_replace('/\s+/', ' ', $text);
+
+ // 移除控制字符
+ $text = preg_replace('/[\x00-\x1F\x7F]/', '', $text);
+
+ return trim($text);
+ }
+
+
+ public function aiRewrite($library = [], $content = '')
+ {
+ if (empty($library['aiEnabled']) && empty($content)) {
+ return false;
+ }
+
+ // 此处实现AI处理逻辑,暂未实现
+ $utl = Env::get('doubaoAi.api_url', '');
+ $apiKey = Env::get('doubaoAi.api_key', '');
+ $model = Env::get('doubaoAi.model', 'doubao-1-5-pro-32k-250115');
+ if (empty($apiKey)) {
+ return false;
+ }
+
+ if (!empty($library['aiPrompt'])) {
+ $aiPrompt = $library['aiPrompt'];
+ } else {
+ $aiPrompt = '重写这条朋友圈 要求:
+1、原本的字数和意思不要修改超过10%
+2、出现品牌名或个人名字就去除';
+ }
+
+ $content = $aiPrompt . ' ' . $content;
+ $headerData = ['Authorization:Bearer ' . $apiKey];
+ $header = setHeader($headerData);
+
+ // 发送请求
+ $params = [
+ 'model' => $model,
+ 'messages' => [
+ ['role' => 'system', 'content' => '你是人工智能助手.'],
+ ['role' => 'user', 'content' => $content],
+ ]
+ ];
+ $result = requestCurl($utl, $params, 'POST', $header, 'json');
+ $result = json_decode($result, true);
+ if (!empty($result['choices'])) {
+ $contentAI = $result['choices'][0]['message']['content'];
+ return $contentAI;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * 导入Excel表格(支持图片导入)
+ * @return \think\response\Json
+ */
+ public function importExcel()
+ {
+ try {
+ $libraryId = $this->request->param('id', 0);
+ $companyId = $this->request->userInfo['companyId'];
+ $userId = $this->request->userInfo['id'];
+ $isAdmin = !empty($this->request->userInfo['isAdmin']);
+
+ if (empty($libraryId)) {
+ return json(['code' => 400, 'msg' => '内容库ID不能为空']);
+ }
+
+ // 验证内容库权限
+ $libraryWhere = [
+ ['id', '=', $libraryId],
+ ['companyId', '=', $companyId],
+ ['isDel', '=', 0]
+ ];
+
+ if (!$isAdmin) {
+ $libraryWhere[] = ['userId', '=', $userId];
+ }
+
+ $library = ContentLibrary::where($libraryWhere)->find();
+ if (empty($library)) {
+ return json(['code' => 500, 'msg' => '内容库不存在或无权限访问']);
+ }
+
+ // 获取文件(可能是上传的文件或远程URL)
+ $fileUrl = $this->request->param('fileUrl', '');
+ $file = Request::file('file');
+ $tmpFile = '';
+
+ if (!empty($fileUrl)) {
+ // 处理远程URL
+ if (!preg_match('/^https?:\/\//i', $fileUrl)) {
+ return json(['code' => 400, 'msg' => '无效的文件URL']);
+ }
+
+ // 验证文件扩展名
+ $urlExt = strtolower(pathinfo(parse_url($fileUrl, PHP_URL_PATH), PATHINFO_EXTENSION));
+ if (!in_array($urlExt, ['xls', 'xlsx'])) {
+ return json(['code' => 400, 'msg' => '只支持Excel文件(.xls, .xlsx)']);
+ }
+
+ // 下载远程文件到临时目录
+ $tmpFile = tempnam(sys_get_temp_dir(), 'excel_import_') . '.' . $urlExt;
+ $fileContent = @file_get_contents($fileUrl);
+
+ if ($fileContent === false) {
+ return json(['code' => 400, 'msg' => '下载远程文件失败,请检查URL是否可访问']);
+ }
+
+ file_put_contents($tmpFile, $fileContent);
+
+ } elseif ($file) {
+ // 处理上传的文件
+ $ext = strtolower($file->getExtension());
+ if (!in_array($ext, ['xls', 'xlsx'])) {
+ return json(['code' => 400, 'msg' => '只支持Excel文件(.xls, .xlsx)']);
+ }
+
+ // 保存临时文件
+ $tmpFile = $file->getRealPath();
+ if (empty($tmpFile)) {
+ $savePath = $file->move(sys_get_temp_dir());
+ $tmpFile = $savePath->getRealPath();
+ }
+ } else {
+ return json(['code' => 400, 'msg' => '请上传Excel文件或提供文件URL']);
+ }
+
+ if (empty($tmpFile) || !file_exists($tmpFile)) {
+ return json(['code' => 400, 'msg' => '文件不存在或无法访问']);
+ }
+
+ // 加载Excel文件
+ $excel = PHPExcel_IOFactory::load($tmpFile);
+ $sheet = $excel->getActiveSheet();
+
+ // 获取所有图片
+ $images = [];
+ try {
+ $drawings = $sheet->getDrawingCollection();
+ foreach ($drawings as $drawing) {
+ if ($drawing instanceof \PHPExcel_Worksheet_Drawing) {
+ $coordinates = $drawing->getCoordinates();
+ $imagePath = $drawing->getPath();
+
+ // 如果是嵌入的图片(zip://格式),提取到临时文件
+ if (strpos($imagePath, 'zip://') === 0) {
+ $zipEntry = str_replace('zip://', '', $imagePath);
+ $zipEntry = explode('#', $zipEntry);
+ $zipFile = $zipEntry[0];
+ $imageEntry = isset($zipEntry[1]) ? $zipEntry[1] : '';
+
+ if (!empty($imageEntry)) {
+ $zip = new \ZipArchive();
+ if ($zip->open($zipFile) === true) {
+ $imageContent = $zip->getFromName($imageEntry);
+ if ($imageContent !== false) {
+ $tempImageFile = tempnam(sys_get_temp_dir(), 'excel_img_');
+ file_put_contents($tempImageFile, $imageContent);
+ $images[$coordinates] = $tempImageFile;
+ }
+ $zip->close();
+ }
+ }
+ } elseif (file_exists($imagePath)) {
+ // 如果是外部文件路径
+ $images[$coordinates] = $imagePath;
+ }
+ } elseif ($drawing instanceof \PHPExcel_Worksheet_MemoryDrawing) {
+ // 处理内存中的图片
+ $coordinates = $drawing->getCoordinates();
+ $imageResource = $drawing->getImageResource();
+
+ if ($imageResource) {
+ $tempImageFile = tempnam(sys_get_temp_dir(), 'excel_img_') . '.png';
+ $imageType = $drawing->getMimeType();
+
+ switch ($imageType) {
+ case 'image/png':
+ imagepng($imageResource, $tempImageFile);
+ break;
+ case 'image/jpeg':
+ case 'image/jpg':
+ imagejpeg($imageResource, $tempImageFile);
+ break;
+ case 'image/gif':
+ imagegif($imageResource, $tempImageFile);
+ break;
+ default:
+ imagepng($imageResource, $tempImageFile);
+ }
+
+ $images[$coordinates] = $tempImageFile;
+ }
+ }
+ }
+ } catch (\Exception $e) {
+ \think\facade\Log::error('提取Excel图片失败:' . $e->getMessage());
+ }
+
+ // 读取数据(实际内容从第三行开始,前两行是标题和说明)
+ $data = $sheet->toArray();
+ if (count($data) < 3) {
+ return json(['code' => 400, 'msg' => 'Excel文件数据为空']);
+ }
+
+ // 移除前两行(标题行和说明行)
+ array_shift($data); // 移除第1行
+ array_shift($data); // 移除第2行
+
+ $successCount = 0;
+ $failCount = 0;
+ $errors = [];
+
+ Db::startTrans();
+ try {
+ foreach ($data as $rowIndex => $row) {
+ $rowNum = $rowIndex + 3; // Excel行号(从3开始,因为前两行是标题和说明)
+
+ // 跳过空行
+ if (empty(array_filter($row))) {
+ continue;
+ }
+
+ try {
+ // 解析数据(根据图片中的表格结构)
+ // A:日期, B:投放时间, C:作用分类, D:朋友圈文案, E:自回评内容, F:朋友圈展示形式, G-O:配图1-9
+ $date = isset($row[0]) ? trim($row[0]) : '';
+ $placementTime = isset($row[1]) ? trim($row[1]) : '';
+ $functionCategory = isset($row[2]) ? trim($row[2]) : '';
+ $content = isset($row[3]) ? trim($row[3]) : '';
+ $selfReply = isset($row[4]) ? trim($row[4]) : '';
+ $displayForm = isset($row[5]) ? trim($row[5]) : '';
+
+ // 如果没有朋友圈文案,跳过
+ if (empty($content)) {
+ continue;
+ }
+
+ // 提取配图(G-O列,索引6-14)
+ $imageUrls = [];
+ for ($colIndex = 6; $colIndex <= 14; $colIndex++) {
+ $columnLetter = $this->columnLetter($colIndex);
+ $cellCoordinate = $columnLetter . $rowNum;
+
+ // 检查是否有图片
+ if (isset($images[$cellCoordinate])) {
+ $imagePath = $images[$cellCoordinate];
+
+ // 上传图片到OSS
+ $imageExt = 'jpg';
+ if (file_exists($imagePath)) {
+ $imageInfo = @getimagesize($imagePath);
+ if ($imageInfo) {
+ $imageExt = image_type_to_extension($imageInfo[2], false);
+ if ($imageExt === 'jpeg') {
+ $imageExt = 'jpg';
+ }
+ }
+ }
+
+ $objectName = AliyunOSS::generateObjectName('excel_img_' . $rowNum . '_' . ($colIndex - 5) . '.' . $imageExt);
+ $uploadResult = AliyunOSS::uploadFile($imagePath, $objectName);
+
+ if ($uploadResult['success']) {
+ $imageUrls[] = $uploadResult['url'];
+ }
+ }
+ }
+
+ // 解析日期和时间
+ $createMomentTime = 0;
+ if (!empty($date)) {
+ // 尝试解析日期格式:2025年11月25日 或 2025-11-25
+ $dateStr = $date;
+ if (preg_match('/(\d{4})[年\-](\d{1,2})[月\-](\d{1,2})/', $dateStr, $matches)) {
+ $year = $matches[1];
+ $month = str_pad($matches[2], 2, '0', STR_PAD_LEFT);
+ $day = str_pad($matches[3], 2, '0', STR_PAD_LEFT);
+
+ // 解析时间
+ $hour = 0;
+ $minute = 0;
+ if (!empty($placementTime) && preg_match('/(\d{1,2}):(\d{2})/', $placementTime, $timeMatches)) {
+ $hour = intval($timeMatches[1]);
+ $minute = intval($timeMatches[2]);
+ }
+
+ $createMomentTime = strtotime("{$year}-{$month}-{$day} {$hour}:{$minute}:00");
+ }
+ }
+
+ if ($createMomentTime == 0) {
+ $createMomentTime = time();
+ }
+
+ // 判断内容类型
+ $contentType = 4; // 默认文本
+ if (!empty($imageUrls)) {
+ $contentType = 1; // 图文
+ }
+
+ // 创建内容项
+ $item = new ContentItem();
+ $item->libraryId = $libraryId;
+ $item->type = 'diy'; // 自定义类型
+ $item->title = !empty($date) ? $date . ' ' . $placementTime : '导入的内容';
+ $item->content = $content;
+ $item->comment = $selfReply; // 自回评内容
+ $item->contentType = $contentType;
+ $item->resUrls = json_encode($imageUrls, JSON_UNESCAPED_UNICODE);
+ $item->urls = json_encode([], JSON_UNESCAPED_UNICODE);
+ $item->createMomentTime = $createMomentTime;
+ $item->createTime = time();
+
+ // 设置封面图片
+ if (!empty($imageUrls[0])) {
+ $item->coverImage = $imageUrls[0];
+ }
+
+ // 保存其他信息到contentData
+ $contentData = [
+ 'date' => $date,
+ 'placementTime' => $placementTime,
+ 'functionCategory' => $functionCategory,
+ 'displayForm' => $displayForm,
+ 'selfReply' => $selfReply
+ ];
+ $item->contentData = json_encode($contentData, JSON_UNESCAPED_UNICODE);
+
+ $item->save();
+ $successCount++;
+
+ } catch (\Exception $e) {
+ $failCount++;
+ $errors[] = "第{$rowNum}行处理失败:" . $e->getMessage();
+ \think\facade\Log::error('导入Excel第' . $rowNum . '行失败:' . $e->getMessage());
+ }
+ }
+
+ Db::commit();
+
+ // 清理临时图片文件
+ foreach ($images as $imagePath) {
+ if (file_exists($imagePath) && strpos($imagePath, sys_get_temp_dir()) === 0) {
+ @unlink($imagePath);
+ }
+ }
+
+ // 清理临时Excel文件
+ if (file_exists($tmpFile) && strpos($tmpFile, sys_get_temp_dir()) === 0) {
+ @unlink($tmpFile);
+ }
+
+ return json([
+ 'code' => 200,
+ 'msg' => '导入完成',
+ 'data' => [
+ 'success' => $successCount,
+ 'fail' => $failCount,
+ 'errors' => $errors
+ ]
+ ]);
+
+ } catch (\Exception $e) {
+ Db::rollback();
+
+ // 清理临时文件
+ foreach ($images as $imagePath) {
+ if (file_exists($imagePath) && strpos($imagePath, sys_get_temp_dir()) === 0) {
+ @unlink($imagePath);
+ }
+ }
+ if (file_exists($tmpFile) && strpos($tmpFile, sys_get_temp_dir()) === 0) {
+ @unlink($tmpFile);
+ }
+
+ return json(['code' => 500, 'msg' => '导入失败:' . $e->getMessage()]);
+ }
+
+ } catch (\Exception $e) {
+ return json(['code' => 500, 'msg' => '导入失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 根据列序号生成Excel列字母
+ * @param int $index 列索引(从0开始)
+ * @return string 列字母(如A, B, C, ..., Z, AA, AB等)
+ */
+ private function columnLetter($index)
+ {
+ $letters = '';
+ do {
+ $letters = chr($index % 26 + 65) . $letters;
+ $index = intval($index / 26) - 1;
+ } while ($index >= 0);
+ return $letters;
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/Pay.php b/application/cunkebao/controller/Pay.php
new file mode 100644
index 0000000..99553f0
--- /dev/null
+++ b/application/cunkebao/controller/Pay.php
@@ -0,0 +1,27 @@
+ 111,
+ 'userId' => 111,
+ 'orderNo' => date('YmdHis') . rand(100000, 999999),
+ 'goodsId' => 34,
+ 'goodsName' => '测试测试',
+ 'orderType' => 1,
+ 'money' => 1
+ ];
+
+ $paymentService = new PaymentService();
+ $res = $paymentService->createOrder($order);
+ return $res;
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/Plan.php b/application/cunkebao/controller/Plan.php
new file mode 100644
index 0000000..2c5f147
--- /dev/null
+++ b/application/cunkebao/controller/Plan.php
@@ -0,0 +1,254 @@
+ Request::post('name', ''),
+ 'sceneId' => Request::post('sceneId', 0),
+ 'status' => Request::post('status', 0),
+ 'reqConf' => Request::post('reqConf', ''),
+ 'msgConf' => Request::post('msgConf', ''),
+ 'tagConf' => Request::post('tagConf', ''),
+ 'createTime' => time(),
+ 'updateTime' => time()
+ ];
+
+ // 验证必填字段
+ if (empty($data['name'])) {
+ return ResponseHelper::error('计划名称不能为空', 400);
+ }
+
+ if (empty($data['sceneId'])) {
+ return ResponseHelper::error('场景ID不能为空', 400);
+ }
+
+ // 验证数据格式
+ if (!$this->validateJson($data['reqConf'])) {
+ return ResponseHelper::error('好友申请设置格式不正确', 400);
+ }
+
+ if (!$this->validateJson($data['msgConf'])) {
+ return ResponseHelper::error('消息设置格式不正确', 400);
+ }
+
+ if (!$this->validateJson($data['tagConf'])) {
+ return ResponseHelper::error('标签设置格式不正确', 400);
+ }
+
+ // 插入数据库
+ $result = Db::name('friend_plan')->insert($data);
+
+ if ($result) {
+ return ResponseHelper::success([], '添加计划任务成功');
+ } else {
+ return ResponseHelper::error('添加计划任务失败', 500);
+ }
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500);
+ }
+ }
+
+ /**
+ * 获取计划任务列表
+ *
+ * @return \think\response\Json
+ */
+ public function getList()
+ {
+ try {
+ // 获取分页参数
+ $id = Request::param('id', 1);
+ $page = Request::param('page', 1);
+ $pageSize = Request::param('pageSize', 10);
+
+ // 构建查询条件
+ $where = [];
+
+ // 过滤已删除的记录
+ $where[] = ['deleteTime', 'null'];
+
+ // 查询总数
+ $total = Db::name('friend_plan')->where('sceneId', $id)->count();
+
+ // 查询列表数据
+ $list = Db::name('friend_plan')
+ ->where('sceneId', $id)
+ ->field('id, name, status, createTime, updateTime, sceneId')
+ ->order('createTime desc')
+ ->page($page, $pageSize)
+ ->select();
+ // 遍历列表,获取每个计划的统计信息
+ foreach ($list as &$item) {
+ // 获取计划的统计信息
+ $stats = $this->getPlanStats($item['id']);
+
+ // 合并统计信息到结果中
+ $item = array_merge($item, $stats);
+
+ // 格式化状态为文字描述
+ $item['statusText'] = $item['status'] == 1 ? '进行中' : '已暂停';
+
+ // 格式化时间
+ $item['createTimeFormat'] = date('Y-m-d H:i', $item['createTime']);
+
+ // 获取最近一次执行时间
+ $lastExecution = $this->getLastExecution($item['id']);
+ $item['lastExecutionTime'] = $lastExecution['lastTime'] ?? '';
+ $item['nextExecutionTime'] = $lastExecution['nextTime'] ?? '';
+ }
+
+ // 返回结果
+ $result = [
+ 'total' => $total,
+ 'list' => $list,
+ 'page' => $page,
+ 'pageSize' => $pageSize
+ ];
+
+ return ResponseHelper::success($result);
+ } catch (\Exception $e) {
+ return ResponseHelper::error('获取数据失败: ' . $e->getMessage(), 500);
+ }
+ }
+
+ /**
+ * 获取计划的统计信息
+ *
+ * @param int $planId 计划ID
+ * @return array
+ */
+ private function getPlanStats($planId)
+ {
+ try {
+ // 获取设备数
+ $deviceCount = $this->getDeviceCount($planId);
+
+ // 获取已获客数
+ $customerCount = $this->getCustomerCount($planId);
+
+ // 获取已添加数
+ $addedCount = 1; //$this->getAddedCount($planId);
+
+ // 计算通过率
+ $passRate = $customerCount > 0 ? round(($addedCount / $customerCount) * 100) : 0;
+
+ return [
+ 'deviceCount' => $deviceCount,
+ 'customerCount' => $customerCount,
+ 'addedCount' => $addedCount,
+ 'passRate' => $passRate
+ ];
+ } catch (\Exception $e) {
+ return [
+ 'deviceCount' => 0,
+ 'customerCount' => 0,
+ 'addedCount' => 0,
+ 'passRate' => 0
+ ];
+ }
+ }
+
+ /**
+ * 获取计划使用的设备数
+ *
+ * @param int $planId 计划ID
+ * @return int
+ */
+ private function getDeviceCount($planId)
+ {
+ try {
+ // 获取计划
+ $plan = Db::name('friend_plan')->where('id', $planId)->find();
+ if (!$plan) {
+ return 0;
+ }
+
+ // 解析reqConf
+ $reqConf = json_decode($plan['reqConf'], true);
+
+ // 返回设备数量
+ return isset($reqConf['selectedDevices']) ? count($reqConf['selectedDevices']) : 0;
+ } catch (\Exception $e) {
+ return 0;
+ }
+ }
+
+ /**
+ * 获取计划的已获客数
+ *
+ * @param int $planId 计划ID
+ * @return int
+ */
+ private function getCustomerCount($planId)
+ {
+ // 模拟数据,实际应从相关表获取
+ return rand(10, 50);
+ }
+
+ /**
+ * 获取计划的已添加数
+ *
+ * @param int $planId 计划ID
+ * @return int
+ */
+ private function getAddedCount($planId)
+ {
+ // 模拟数据,实际应从相关表获取
+ $customerCount = $this->getCustomerCount($planId);
+ return rand(5, $customerCount);
+ }
+
+ /**
+ * 获取计划的最近一次执行时间
+ *
+ * @param int $planId 计划ID
+ * @return array
+ */
+ private function getLastExecution($planId)
+ {
+ // 模拟数据,实际应从执行记录表获取
+ $now = time();
+ $lastTime = $now - rand(3600, 86400);
+ $nextTime = $now + rand(3600, 86400);
+
+ return [
+ 'lastTime' => date('Y-m-d H:i', $lastTime),
+ 'nextTime' => date('Y-m-d H:i:s', $nextTime)
+ ];
+ }
+
+ /**
+ * 验证JSON格式是否正确
+ *
+ * @param string $string
+ * @return bool
+ */
+ private function validateJson($string)
+ {
+ if (empty($string)) {
+ return true;
+ }
+
+ json_decode($string);
+ return (json_last_error() == JSON_ERROR_NONE);
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/RFMController.php b/application/cunkebao/controller/RFMController.php
new file mode 100644
index 0000000..f9163db
--- /dev/null
+++ b/application/cunkebao/controller/RFMController.php
@@ -0,0 +1,401 @@
+=', $startTime],
+ ['createTime', '<', $endTime],
+ ];
+
+ // identifier 条件
+ if (!empty($identifier)) {
+ $where[] = ['identifier', '=', $identifier];
+ }
+
+ // ownerWechatId 条件
+ if (!empty($ownerWechatId)) {
+ $where[] = ['ownerWechatId', '=', $ownerWechatId];
+ }
+
+ // 1. 数据过滤和聚合 - 获取每个客户的R、F、M原始值
+ $orderModel = new TrafficOrderModel();
+ $customers = $orderModel
+ ->where($where)
+ ->where(function ($query) {
+ // 只统计有效订单(actualPay大于0)
+ $query->where('actualPay', '>', 0);
+ })
+ ->field('identifier, MAX(createTime) as lastOrderTime, COUNT(DISTINCT id) as orderCount, SUM(CAST(actualPay AS DECIMAL(18,2))) as totalAmount')
+ ->group('identifier')
+ ->select();
+
+ if (empty($customers)) {
+ return [
+ 'code' => 200,
+ 'msg' => '暂无数据',
+ 'data' => []
+ ];
+ }
+
+ // 2. 计算每个客户的R值(最近消费天数)
+ $customerData = [];
+ foreach ($customers as $customer) {
+ $recencyDays = floor(($endTime - $customer['lastOrderTime']) / (24 * 3600));
+ $customerData[] = [
+ 'identifier' => $customer['identifier'],
+ 'R' => $recencyDays,
+ 'F' => (int)$customer['orderCount'],
+ 'M' => (float)$customer['totalAmount'],
+ ];
+ }
+
+ // 3. 异常值处理 - 剔除大额异常订单
+ $mValues = array_column($customerData, 'M');
+ if (!empty($mValues)) {
+ sort($mValues);
+ $m99Percentile = $this->percentile($mValues, 0.99);
+ $abnormalThreshold = $m99Percentile * $abnormalMoneyRatio;
+
+ // 标记异常客户(但不删除,仅在计算M维度区间时考虑)
+ foreach ($customerData as &$customer) {
+ $customer['isAbnormal'] = $customer['M'] > $abnormalThreshold;
+ }
+ }
+
+ // 4. 使用五分位法计算各维度的区间阈值
+ $rThresholds = $this->calculatePercentiles(array_column($customerData, 'R'), true); // R是反向的
+ $fThresholds = $this->calculatePercentiles(array_column($customerData, 'F'), false);
+ // M维度排除异常值计算区间
+ $mValuesForPercentile = array_filter(array_column($customerData, 'M'), function($m) use ($abnormalThreshold) {
+ return isset($abnormalThreshold) ? $m <= $abnormalThreshold : true;
+ });
+ $mThresholds = $this->calculatePercentiles(array_values($mValuesForPercentile), false);
+
+ // 5. 计算每个客户的RFM分项得分
+ $results = [];
+ foreach ($customerData as $customer) {
+ $rScore = $this->scoreByPercentile($customer['R'], $rThresholds, true); // R是反向的
+ $fScore = $this->scoreByPercentile($customer['F'], $fThresholds, false);
+ $mScore = $customer['isAbnormal'] ? 5 : $this->scoreByPercentile($customer['M'], $mThresholds, false); // 异常值给最高分
+
+ // 计算RFM总分(加权求和)
+ $rfmScore = $rScore * $weightR + $fScore * $weightF + $mScore * $weightM;
+
+ // 可选:标准化为1-100分
+ $standardScore = null;
+ if ($scoreScale == 100) {
+ $rfmMin = $weightR * 1 + $weightF * 1 + $weightM * 1;
+ $rfmMax = $weightR * 5 + $weightF * 5 + $weightM * 5;
+ $standardScore = (int)round(($rfmScore - $rfmMin) / ($rfmMax - $rfmMin) * 99 + 1);
+ }
+
+ $results[] = [
+ 'identifier' => $customer['identifier'],
+ 'R_raw' => $customer['R'],
+ 'R_score' => $rScore,
+ 'F_raw' => $customer['F'],
+ 'F_score' => $fScore,
+ 'M_raw' => round($customer['M'], 2),
+ 'M_score' => $mScore,
+ 'RFM_score' => round($rfmScore, 2),
+ 'RFM_standard_score' => $standardScore,
+ 'cycle_start' => date('Y-m-d H:i:s', $startTime),
+ 'cycle_end' => date('Y-m-d H:i:s', $endTime),
+ 'calculate_time' => date('Y-m-d H:i:s'),
+ ];
+ }
+
+ // 按RFM总分降序排序
+ usort($results, function($a, $b) {
+ return $b['RFM_score'] <=> $a['RFM_score'];
+ });
+
+ // 6. 更新 ck_traffic_source 和 s2_wechat_friend 表的RFM值
+ $this->updateRfmToTables($results, $ownerWechatId);
+
+ return [
+ 'code' => 200,
+ 'msg' => '计算成功',
+ 'data' => [
+ 'results' => $results,
+ 'config' => [
+ 'cycle_days' => $cycleDays,
+ 'weight_R' => $weightR,
+ 'weight_F' => $weightF,
+ 'weight_M' => $weightM,
+ 'score_scale' => $scoreScale,
+ ],
+ 'statistics' => [
+ 'total_customers' => count($results),
+ 'avg_rfm_score' => round(array_sum(array_column($results, 'RFM_score')) / count($results), 2),
+ ]
+ ]
+ ];
+
+ } catch (\Exception $e) {
+ return [
+ 'code' => 500,
+ 'msg' => '计算失败:' . $e->getMessage(),
+ 'data' => []
+ ];
+ }
+ }
+
+ /**
+ * 计算 RFM 评分(兼容旧方法,使用固定阈值)
+ * @param int|null $recencyDays 最近购买天数
+ * @param int $frequency 购买次数
+ * @param float $monetary 购买金额
+ * @return array{R:int,F:int,M:int}
+ */
+ public static function calcRfmScores($recencyDays = 30, $frequency, $monetary)
+ {
+ $recencyDays = is_numeric($recencyDays) ? (int)$recencyDays : 9999;
+ $frequency = max(0, (int)$frequency);
+ $monetary = max(0, (float)$monetary);
+ return [
+ 'R' => self::scoreR_Default($recencyDays),
+ 'F' => self::scoreF_Default($frequency),
+ 'M' => self::scoreM_Default($monetary),
+ ];
+ }
+
+ /**
+ * 使用固定阈值计算R得分(保留兼容性)
+ */
+ protected static function scoreR_Default(int $days): int
+ {
+ if ($days <= 30) return 5;
+ if ($days <= 60) return 4;
+ if ($days <= 90) return 3;
+ if ($days <= 120) return 2;
+ return 1;
+ }
+
+ /**
+ * 使用固定阈值计算F得分(保留兼容性)
+ */
+ protected static function scoreF_Default(int $times): int
+ {
+ if ($times >= 10) return 5;
+ if ($times >= 6) return 4;
+ if ($times >= 3) return 3;
+ if ($times >= 2) return 2;
+ if ($times >= 1) return 1;
+ return 0;
+ }
+
+ /**
+ * 使用固定阈值计算M得分(保留兼容性)
+ */
+ protected static function scoreM_Default(float $amount): int
+ {
+ if ($amount >= 2000) return 5;
+ if ($amount >= 1000) return 4;
+ if ($amount >= 500) return 3;
+ if ($amount >= 200) return 2;
+ if ($amount > 0) return 1;
+ return 0;
+ }
+
+ /**
+ * 计算百分位数(五分位法)
+ * @param array $values 数值数组
+ * @param bool $reverse 是否反向(R维度需要反向,值越小得分越高)
+ * @return array 返回[0.2, 0.4, 0.6, 0.8]分位数的阈值数组
+ */
+ private function calculatePercentiles($values, $reverse = false)
+ {
+ if (empty($values)) {
+ return [0, 0, 0, 0];
+ }
+
+ // 去重并排序
+ $uniqueValues = array_unique($values);
+ sort($uniqueValues);
+
+ // 如果所有值相同,强制均分5个区间
+ if (count($uniqueValues) == 1) {
+ $singleValue = $uniqueValues[0];
+ if ($reverse) {
+ return [$singleValue, $singleValue, $singleValue, $singleValue];
+ } else {
+ return [$singleValue, $singleValue, $singleValue, $singleValue];
+ }
+ }
+
+ $percentiles = [0.2, 0.4, 0.6, 0.8];
+ $thresholds = [];
+
+ foreach ($percentiles as $p) {
+ $thresholds[] = $this->percentile($uniqueValues, $p);
+ }
+
+ return $thresholds;
+ }
+
+ /**
+ * 计算百分位数
+ * @param array $sortedArray 已排序的数组
+ * @param float $percentile 百分位数(0-1之间)
+ * @return float
+ */
+ private function percentile($sortedArray, $percentile)
+ {
+ if (empty($sortedArray)) {
+ return 0;
+ }
+
+ $count = count($sortedArray);
+ $index = ($count - 1) * $percentile;
+ $floor = floor($index);
+ $ceil = ceil($index);
+
+ if ($floor == $ceil) {
+ return $sortedArray[(int)$index];
+ }
+
+ $weight = $index - $floor;
+ return $sortedArray[(int)$floor] * (1 - $weight) + $sortedArray[(int)$ceil] * $weight;
+ }
+
+ /**
+ * 根据五分位法阈值计算得分
+ * @param float $value 当前值
+ * @param array $thresholds 阈值数组[T1, T2, T3, T4]
+ * @param bool $reverse 是否反向(R维度反向:值越小得分越高)
+ * @return int 得分1-5
+ */
+ private function scoreByPercentile($value, $thresholds, $reverse = false)
+ {
+ if (empty($thresholds) || count($thresholds) < 4) {
+ return 1;
+ }
+
+ list($t1, $t2, $t3, $t4) = $thresholds;
+
+ if ($reverse) {
+ // R维度:值越小得分越高
+ if ($value <= $t1) return 5;
+ if ($value <= $t2) return 4;
+ if ($value <= $t3) return 3;
+ if ($value <= $t4) return 2;
+ return 1;
+ } else {
+ // F和M维度:值越大得分越高
+ if ($value >= $t4) return 5;
+ if ($value >= $t3) return 4;
+ if ($value >= $t2) return 3;
+ if ($value >= $t1) return 2;
+ return 1;
+ }
+ }
+
+ /**
+ * 更新RFM值到 ck_traffic_source 和 s2_wechat_friend 表
+ *
+ * @param array $results RFM计算结果数组
+ * @param string|null $ownerWechatId 微信ID,用于过滤更新范围
+ */
+ private function updateRfmToTables($results, $ownerWechatId = null)
+ {
+ try {
+ foreach ($results as $result) {
+ $identifier = $result['identifier'];
+ $rScore = (string)$result['R_score'];
+ $fScore = (string)$result['F_score'];
+ $mScore = (string)$result['M_score'];
+
+ // 更新 ck_traffic_source 表
+ // 根据 identifier 更新所有匹配的记录
+ $trafficSourceUpdate = [
+ 'R' => $rScore,
+ 'F' => $fScore,
+ 'M' => $mScore,
+ 'updateTime' => time()
+ ];
+ TrafficSource::where('identifier', $identifier)->update($trafficSourceUpdate);
+
+ // 更新 s2_wechat_friend 表
+ // wechatId 对应 identifier
+ $wechatFriendUpdate = [
+ 'R' => $rScore,
+ 'F' => $fScore,
+ 'M' => $mScore,
+ 'updateTime' => time()
+ ];
+ $wechatFriendWhere = ['wechatId' => $identifier];
+ if (!empty($ownerWechatId)) {
+ $wechatFriendWhere['ownerWechatId'] = $ownerWechatId;
+ }
+ WechatFriendModel::where($wechatFriendWhere)->update($wechatFriendUpdate);
+ }
+
+ } catch (\Exception $e) {
+ // 记录错误但不影响主流程
+ \think\Log::error('更新RFM值失败:' . $e->getMessage());
+ }
+ }
+}
+
+
diff --git a/application/cunkebao/controller/StatsController.php b/application/cunkebao/controller/StatsController.php
new file mode 100644
index 0000000..45ec6ff
--- /dev/null
+++ b/application/cunkebao/controller/StatsController.php
@@ -0,0 +1,438 @@
+ '周日',
+ 1 => '周一',
+ 2 => '周二',
+ 3 => '周三',
+ 4 => '周四',
+ 5 => '周五',
+ 6 => '周六',
+ ];
+
+ /**
+ * 基础信息
+ * @return \think\response\Json
+ */
+ public function baseInfoStats()
+ {
+
+ $where = [
+ ['departmentId','=',$this->request->userInfo['companyId']]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])){
+ $where[] = ['id','=',$this->request->userInfo['s2_accountId']];
+ }
+ $accounts = Db::table('s2_company_account')->where($where)->column('id');
+
+ $deviceNum = Db::table('s2_device')->whereIn('currentAccountId',$accounts)->where(['isDeleted' => 0])->count();
+ $wechatNum = Db::table('s2_wechat_account')->whereIn('deviceAccountId',$accounts)->count();
+ $aliveWechatNum = Db::table('s2_wechat_account')->whereIn('deviceAccountId',$accounts)->where(['wechatAlive' => 1])->count();
+ $data = [
+ 'deviceNum' => $deviceNum,
+ 'wechatNum' => $wechatNum,
+ 'aliveWechatNum' => $aliveWechatNum,
+ ];
+ return successJson($data, '获取成功');
+ }
+
+ /**
+ * 场景获客统计
+ * @return \think\response\Json
+ * @throws \think\db\exception\DataNotFoundException
+ * @throws \think\db\exception\ModelNotFoundException
+ * @throws \think\exception\DbException
+ */
+ public function planStats()
+ {
+
+ $num = $this->request->param('num', 4);
+ $planScene = Db::name('plan_scene')
+ ->field('id,name,image')
+ ->where(['status' => 1])
+ ->order('sort DESC')
+ ->page(1, $num)
+ ->select();
+
+ if (empty($planScene)) {
+ return successJson([], '获取成功');
+ }
+
+ $sceneIds = array_column($planScene, 'id');
+ $companyId = $this->request->userInfo['companyId'];
+
+ $stats = Db::name('customer_acquisition_task')->alias('ac')
+ ->join('task_customer tc', 'tc.task_id = ac.id')
+ ->where([
+ ['ac.companyId', '=', $companyId],
+ ['ac.deleteTime', '=', 0],
+ ['ac.sceneId', 'in', $sceneIds],
+ ])
+ ->field([
+ 'ac.sceneId',
+ Db::raw('COUNT(1) as allNum'),
+ Db::raw("SUM(CASE WHEN tc.status IN (1,2,3,4) THEN 1 ELSE 0 END) as addNum"),
+ Db::raw("SUM(CASE WHEN tc.status = 4 THEN 1 ELSE 0 END) as passNum"),
+ ])
+ ->group('ac.sceneId')
+ ->select();
+
+ $statsMap = [];
+ foreach ($stats as $row) {
+ $sceneId = is_array($row) ? ($row['sceneId'] ?? 0) : ($row->sceneId ?? 0);
+ if (!$sceneId) {
+ continue;
+ }
+ $statsMap[$sceneId] = [
+ 'allNum' => (int)(is_array($row) ? ($row['allNum'] ?? 0) : ($row->allNum ?? 0)),
+ 'addNum' => (int)(is_array($row) ? ($row['addNum'] ?? 0) : ($row->addNum ?? 0)),
+ 'passNum' => (int)(is_array($row) ? ($row['passNum'] ?? 0) : ($row->passNum ?? 0)),
+ ];
+ }
+
+ foreach ($planScene as &$item) {
+ $sceneStats = $statsMap[$item['id']] ?? ['allNum' => 0, 'addNum' => 0, 'passNum' => 0];
+ $item['allNum'] = $sceneStats['allNum'];
+ $item['addNum'] = $sceneStats['addNum'];
+ $item['passNum'] = $sceneStats['passNum'];
+ }
+ unset($item);
+
+ return successJson($planScene, '获取成功');
+ }
+
+
+ public function todayStats()
+ {
+ $date = date('Y-m-d',time());
+ $start = strtotime($date . ' 00:00:00');
+ $end = strtotime($date . ' 23:59:59');
+ $companyId = $this->request->userInfo['companyId'];
+
+
+ $momentsNum = Db::name('workbench')->alias('w')
+ ->join('workbench_moments_sync_item wi', 'w.id = wi.workbenchId')
+ ->where(['w.companyId' => $companyId])
+ ->where('wi.createTime', 'between', [$start, $end])
+ ->count();
+
+ $groupPushNum = Db::name('workbench')->alias('w')
+ ->join('workbench_group_push_item wi', 'w.id = wi.workbenchId')
+ ->where(['w.companyId' => $companyId])
+ ->where('wi.createTime', 'between', [$start, $end])
+ ->count();
+
+
+ $addNum = Db::name('customer_acquisition_task')->alias('ac')
+ ->join('task_customer tc', 'tc.task_id = ac.id')
+ ->where(['ac.companyId' => $companyId, 'ac.deleteTime' => 0])
+ ->where('tc.updateTime', 'between', [$start, $end])
+ ->whereIn('tc.status', [1, 2, 3, 4])
+ ->count();
+
+ // 通过量
+ $passNum = Db::name('customer_acquisition_task')->alias('ac')
+ ->join('task_customer tc', 'tc.task_id = ac.id')
+ ->where(['ac.companyId' => $companyId, 'ac.deleteTime' => 0])
+ ->where('tc.updateTime', 'between', [$start, $end])
+ ->whereIn('tc.status', [4])
+ ->count();
+
+ if (!empty($passNum)){
+ $passRate = number_format(($addNum / $passNum) * 100,2) ;
+ }else{
+ $passRate = '0%';
+ }
+
+ $sysActive = '90%';
+ $data = [
+ 'momentsNum' => $momentsNum,
+ 'groupPushNum' => $groupPushNum,
+ 'addNum' => $addNum,
+ 'passNum' => $passNum,
+ 'passRate' => $passRate,
+ 'sysActive' => $sysActive,
+ ];
+ return successJson($data, '获取成功');
+ }
+
+
+
+ /**
+ * 近7天获客统计
+ * @return \think\response\Json
+ */
+ public function customerAcquisitionStats7Days()
+ {
+ $companyId = $this->request->userInfo['companyId'];
+ $days = 7;
+
+ $endTime = strtotime(date('Y-m-d 23:59:59'));
+ $startTime = strtotime(date('Y-m-d 00:00:00', strtotime('-' . ($days - 1) . ' day')));
+
+ $dateMap = [];
+ $dateLabels = [];
+ for ($i = 0; $i < $days; $i++) {
+ $currentDate = date('Y-m-d', strtotime("-" . ($days - 1 - $i) . " day"));
+ $weekIndex = date("w", strtotime($currentDate));
+ $dateMap[$currentDate] = self::WEEK[$weekIndex];
+ $dateLabels[] = self::WEEK[$weekIndex];
+ }
+
+ $baseWhere = [
+ ['ac.companyId', '=', $companyId],
+ ['ac.deleteTime', '=', 0],
+ ];
+
+ $fetchCounts = function (string $timeField, array $status = []) use ($baseWhere, $startTime, $endTime) {
+ $query = Db::name('customer_acquisition_task')->alias('ac')
+ ->join('task_customer tc', 'tc.task_id = ac.id')
+ ->where($baseWhere)
+ ->whereBetween('tc.' . $timeField, [$startTime, $endTime]);
+ if (!empty($status)) {
+ $query->whereIn('tc.status', $status);
+ }
+ $rows = $query->field([
+ "FROM_UNIXTIME(tc.{$timeField}, '%Y-%m-%d')" => 'day',
+ 'COUNT(1)' => 'total'
+ ])->group('day')->select();
+
+ $result = [];
+ foreach ($rows as $row) {
+ $day = is_array($row) ? ($row['day'] ?? '') : ($row->day ?? '');
+ $total = (int)(is_array($row) ? ($row['total'] ?? 0) : ($row->total ?? 0));
+ if ($day) {
+ $result[$day] = $total;
+ }
+ }
+ return $result;
+ };
+
+ $allNumDict = $fetchCounts('createTime');
+ $addNumDict = $fetchCounts('updateTime', [1, 2, 3, 4]);
+ $passNumDict = $fetchCounts('updateTime', [4]);
+
+ $allNum = [];
+ $addNum = [];
+ $passNum = [];
+ foreach (array_keys($dateMap) as $dateKey) {
+ $allNum[] = $allNumDict[$dateKey] ?? 0;
+ $addNum[] = $addNumDict[$dateKey] ?? 0;
+ $passNum[] = $passNumDict[$dateKey] ?? 0;
+ }
+
+ $data = [
+ 'date' => $dateLabels,
+ 'allNum' => $allNum,
+ 'addNum' => $addNum,
+ 'passNum' => $passNum,
+ ];
+
+ return successJson($data, '获取成功');
+ }
+
+
+ /**
+ * 场景获客数据统计
+ * @return \think\response\Json
+ * @throws \think\db\exception\DataNotFoundException
+ * @throws \think\db\exception\ModelNotFoundException
+ * @throws \think\exception\DbException
+ */
+ public function getFriendRequestTaskStats()
+ {
+ $companyId = $this->request->userInfo['companyId'];
+ $taskId = $this->request->param('taskId', '');
+ if(empty($taskId)){
+ return errorJson('任务id不能为空');
+ }
+
+ $task = Db::name('customer_acquisition_task')->where(['id' => $taskId, 'companyId' => $companyId,'deleteTime' => 0])->find();
+ if(empty($task)){
+ return errorJson('任务不存在或已删除');
+ }
+
+
+ // 1. 获取startTime和endTime,格式是日期
+ $startTime = $this->request->param('startTime', '');
+ $endTime = $this->request->param('endTime', '');
+
+ // 如果获取不到则默认为7天的跨度
+ if (empty($startTime)) {
+ $startTime = date('Y-m-d', time() - 86400 * 6);
+ }
+ if (empty($endTime)) {
+ $endTime = date('Y-m-d', time());
+ }
+
+ // 转换成时间戳格式
+ $startTimestamp = strtotime($startTime . ' 00:00:00');
+ $endTimestamp = strtotime($endTime . ' 23:59:59');
+
+ // 同时生成日期数组和时间戳二维数组
+ $dateArray = [];
+ $timestampArray = [];
+ $currentTimestamp = $startTimestamp;
+
+ while ($currentTimestamp <= $endTimestamp) {
+ // 生成日期格式数组
+ $dateArray[] = date('m-d', $currentTimestamp);
+
+ // 生成时间戳二维数组
+ $dayStart = $currentTimestamp;
+ $dayEnd = strtotime('+1 day', $currentTimestamp) - 1; // 23:59:59
+ $timestampArray[] = [$dayStart, $dayEnd];
+
+ $currentTimestamp = strtotime('+1 day', $currentTimestamp);
+ }
+
+
+ // 使用分组聚合统计,减少 SQL 次数
+ $allRows = Db::name('task_customer')
+ ->field("FROM_UNIXTIME(createTime, '%m-%d') AS d, COUNT(*) AS c")
+ ->where(['task_id' => $taskId])
+ ->where('createTime', 'between', [$startTimestamp, $endTimestamp])
+ ->group('d')
+ ->select();
+
+ $successRows = Db::name('task_customer')
+ ->field("FROM_UNIXTIME(addTime, '%m-%d') AS d, COUNT(*) AS c")
+ ->where(['task_id' => $taskId])
+ ->where('addTime', 'between', [$startTimestamp, $endTimestamp])
+ ->whereIn('status', [1, 2, 4, 5])
+ ->group('d')
+ ->select();
+
+ $passRows = Db::name('task_customer')
+ ->field("FROM_UNIXTIME(passTime, '%m-%d') AS d, COUNT(*) AS c")
+ ->where(['task_id' => $taskId])
+ ->where('passTime', 'between', [$startTimestamp, $endTimestamp])
+ ->group('d')
+ ->select();
+
+ $errorRows = Db::name('task_customer')
+ ->field("FROM_UNIXTIME(updateTime, '%m-%d') AS d, COUNT(*) AS c")
+ ->where(['task_id' => $taskId, 'status' => 3])
+ ->where('updateTime', 'between', [$startTimestamp, $endTimestamp])
+ ->group('d')
+ ->select();
+
+ // 将分组结果映射到连续日期数组
+ $mapToSeries = function(array $rows) use ($dateArray) {
+ $dict = [];
+ foreach ($rows as $row) {
+ // 兼容对象/数组两种返回
+ $d = is_array($row) ? ($row['d'] ?? '') : ($row->d ?? '');
+ $c = (int)(is_array($row) ? ($row['c'] ?? 0) : ($row->c ?? 0));
+ if ($d !== '') {
+ $dict[$d] = $c;
+ }
+ }
+ $series = [];
+ foreach ($dateArray as $d) {
+ $series[] = $dict[$d] ?? 0;
+ }
+ return $series;
+ };
+
+ $allNumArray = $mapToSeries($allRows);
+ $successNumArray = $mapToSeries($successRows);
+ $passNumArray = $mapToSeries($passRows);
+ $errorNumArray = $mapToSeries($errorRows);
+
+ // 计算通过率和成功率
+ $passRateArray = [];
+ $successRateArray = [];
+
+ for ($i = 0; $i < count($dateArray); $i++) {
+ // 通过率 = 通过数 / 总数
+ $passRate = ($allNumArray[$i] > 0) ? round(($passNumArray[$i] / $allNumArray[$i]) * 100, 2) : 0;
+ $passRateArray[] = $passRate;
+
+ // 成功率 = 成功数 / 总数
+ $successRate = ($allNumArray[$i] > 0) ? round(($successNumArray[$i] / $allNumArray[$i]) * 100, 2) : 0;
+ $successRateArray[] = $successRate;
+ }
+
+ // 计算总体统计
+ $totalAll = array_sum($allNumArray);
+ $totalSuccess = array_sum($successNumArray);
+ $totalPass = array_sum($passNumArray);
+ $totalError = array_sum($errorNumArray);
+
+ $totalPassRate = ($totalAll > 0) ? round(($totalPass / $totalAll) * 100, 2) : 0;
+ $totalSuccessRate = ($totalAll > 0) ? round(($totalSuccess / $totalAll) * 100, 2) : 0;
+
+ // 返回结果
+ $result = [
+ 'startTime' => $startTime,
+ 'endTime' => $endTime,
+ 'dateArray' => $dateArray,
+ 'allNumArray' => $allNumArray,
+ 'successNumArray' => $successNumArray,
+ 'passNumArray' => $passNumArray,
+ 'errorNumArray' => $errorNumArray,
+ 'passRateArray' => $passRateArray,
+ 'successRateArray' => $successRateArray,
+ 'totalStats' => [
+ 'totalAll' => $totalAll,
+ 'totalSuccess' => $totalSuccess,
+ 'totalPass' => $totalPass,
+ 'totalError' => $totalError,
+ 'totalPassRate' => $totalPassRate,
+ 'totalSuccessRate' => $totalSuccessRate
+ ]
+ ];
+
+ return successJson($result, '获取成功');
+ }
+
+
+ public function userInfoStats()
+ {
+ $companyId = $this->request->userInfo['companyId'];
+ $userId = $this->request->userInfo['id'];
+ $isAdmin = $this->request->userInfo['isAdmin'];
+
+
+ $where = [
+ ['departmentId','=',$companyId]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])){
+ $where[] = ['id','=',$this->request->userInfo['s2_accountId']];
+ }
+ $accounts = Db::table('s2_company_account')->where($where)->column('id');
+
+
+ $userNum = Db::table('s2_wechat_friend')->whereIn('accountId',$accounts)->where(['isDeleted' => 0])->count();
+ $deviceNum = Db::table('s2_device')->whereIn('currentAccountId',$accounts)->where(['isDeleted' => 0])->count();
+ $wechatNum = Db::table('s2_wechat_account')->whereIn('deviceAccountId',$accounts)->count();
+
+
+ $contentLibrary = Db::name('content_library')->where(['companyId' => $companyId,'isDel' => 0]);
+ if(empty($isAdmin)){
+ $contentLibrary = $contentLibrary->where(['userId' => $userId]);
+ }
+ $contentLibraryNum = $contentLibrary->count();
+
+
+ $data = [
+ 'deviceNum' => $deviceNum,
+ 'wechatNum' => $wechatNum,
+ 'contentLibraryNum' => $contentLibraryNum,
+ 'userNum' => $userNum,
+ ];
+ return successJson($data, '获取成功');
+ }
+
+
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/StoreAccountController.php b/application/cunkebao/controller/StoreAccountController.php
new file mode 100644
index 0000000..f0b4464
--- /dev/null
+++ b/application/cunkebao/controller/StoreAccountController.php
@@ -0,0 +1,404 @@
+request->param('account', '');
+ $username = $this->request->param('username', '');
+ $phone = $this->request->param('phone', '');
+ $password = $this->request->param('password', '');
+ $deviceId = $this->request->param('deviceId', 0);
+
+ $companyId = $this->getUserInfo('companyId');
+
+ // 参数验证
+ if (empty($account)) {
+ return ResponseHelper::error('账号不能为空');
+ }
+ if (empty($username)) {
+ return ResponseHelper::error('昵称不能为空');
+ }
+ if (empty($phone)) {
+ return ResponseHelper::error('手机号不能为空');
+ }
+ if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
+ return ResponseHelper::error('手机号格式不正确');
+ }
+ if (empty($password)) {
+ return ResponseHelper::error('密码不能为空');
+ }
+ if (strlen($password) < 6 || strlen($password) > 20) {
+ return ResponseHelper::error('密码长度必须在6-20个字符之间');
+ }
+ if (empty($deviceId)) {
+ return ResponseHelper::error('请选择设备');
+ }
+
+ // 检查账号是否已存在(同一 typeId 和 companyId 下不能重复)
+ $existUser = Db::name('users')->where(['account' => $account, 'companyId' => $companyId, 'typeId' => 2, 'deleteTime' => 0])
+ ->find();
+ if ($existUser) {
+ return ResponseHelper::error('账号已存在');
+ }
+
+ // 检查手机号是否已存在(同一 typeId 和 companyId 下不能重复)
+ $existPhone = Db::name('users')->where(['phone' => $phone, 'companyId' => $companyId, 'typeId' => 2, 'deleteTime' => 0])
+ ->find();
+ if ($existPhone) {
+ return ResponseHelper::error('手机号已被使用');
+ }
+
+ // 检查设备是否存在且属于当前公司
+ $device = Device::where('id', $deviceId)
+ ->where('companyId', $companyId)
+ ->find();
+ if (!$device) {
+ return ResponseHelper::error('设备不存在或没有权限');
+ }
+
+ // 开始事务
+ Db::startTrans();
+ try {
+ // 创建用户
+ $userData = [
+ 'account' => $account,
+ 'username' => $username,
+ 'phone' => $phone,
+ 'passwordMd5' => md5($password),
+ 'passwordLocal' => localEncrypt($password),
+ 'avatar' => '',
+ 'isAdmin' => 0,
+ 'companyId' => $companyId,
+ 'typeId' => 2, // 门店端固定为2
+ 'status' => 1, // 默认可用
+ 'balance' => 0,
+ 'tokens' => 0,
+ 'createTime' => time(),
+ ];
+
+ $userId = Db::name('users')->insertGetId($userData);
+
+ // 绑定设备
+ Db::name('device_user')->insert([
+ 'companyId' => $companyId,
+ 'userId' => $userId,
+ 'deviceId' => $deviceId,
+ 'deleteTime' => 0,
+ ]);
+
+ // 提交事务
+ Db::commit();
+
+ return ResponseHelper::success('创建账号成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ throw $e;
+ }
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500);
+ }
+ }
+
+ /**
+ * 编辑账号
+ * @return \think\response\Json
+ */
+ public function update()
+ {
+ try {
+ $userId = $this->request->param('userId', 0);
+ $account = $this->request->param('account', '');
+ $username = $this->request->param('username', '');
+ $phone = $this->request->param('phone', '');
+ $password = $this->request->param('password', '');
+ $deviceId = $this->request->param('deviceId', 0);
+
+ $companyId = $this->getUserInfo('companyId');
+
+ // 参数验证
+ if (empty($userId)) {
+ return ResponseHelper::error('用户ID不能为空');
+ }
+
+ // 检查用户是否存在且属于当前公司
+ $user = Db::name('users')->where(['id' => $userId, 'companyId' => $companyId, 'typeId' => 2])->find();
+ if (!$user) {
+ return ResponseHelper::error('用户不存在或没有权限');
+ }
+
+ $updateData = [];
+
+ // 更新账号
+ if (!empty($account)) {
+ // 检查账号是否已被其他用户使用(同一 typeId 下)
+ $existUser = Db::name('users')->where(['account' => $account, 'companyId' => $companyId, 'typeId' => 2, 'deleteTime' => 0])
+ ->where('id', '<>', $userId)
+ ->find();
+ if ($existUser) {
+ return ResponseHelper::error('账号已被使用');
+ }
+ $updateData['account'] = $account;
+ }
+
+ // 更新昵称
+ if (!empty($username)) {
+ $updateData['username'] = $username;
+ }
+
+ // 更新手机号
+ if (!empty($phone)) {
+ if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
+ return ResponseHelper::error('手机号格式不正确');
+ }
+ // 检查手机号是否已被其他用户使用(同一 typeId 下)
+ $existPhone = Db::name('users')->where(['phone' => $phone, 'companyId' => $companyId, 'typeId' => 2, 'deleteTime' => 0])
+ ->where('id', '<>', $userId)
+ ->find();
+ if ($existPhone) {
+ return ResponseHelper::error('手机号已被使用');
+ }
+ $updateData['phone'] = $phone;
+ }
+
+ // 更新密码
+ if (!empty($password)) {
+ if (strlen($password) < 6 || strlen($password) > 20) {
+ return ResponseHelper::error('密码长度必须在6-20个字符之间');
+ }
+ $updateData['passwordMd5'] = md5($password);
+ $updateData['passwordLocal'] = localEncrypt($password);
+ }
+
+ // 更新设备绑定
+ if (!empty($deviceId)) {
+ // 检查设备是否存在且属于当前公司
+ $device = Device::where('id', $deviceId)
+ ->where('companyId', $companyId)
+ ->find();
+ if (!$device) {
+ return ResponseHelper::error('设备不存在或没有权限');
+ }
+ }
+
+ // 开始事务
+ Db::startTrans();
+ try {
+ // 更新用户信息
+ if (!empty($updateData)) {
+ $updateData['updateTime'] = time();
+ Db::name('users')->where(['id' => $userId])->update($updateData);
+ }
+
+ // 更新设备绑定
+ if (!empty($deviceId)) {
+ // 删除旧的设备绑定
+ Db::name('device_user')->where(['userId' => $userId, 'companyId' => $companyId])->delete();
+
+ // 添加新的设备绑定
+ Db::name('device_user')->insert([
+ 'companyId' => $companyId,
+ 'userId' => $userId,
+ 'deviceId' => $deviceId,
+ 'deleteTime' => 0,
+ ]);
+ }
+
+ // 提交事务
+ Db::commit();
+
+ return ResponseHelper::success('更新账号成功');
+ } catch (\Exception $e) {
+ Db::rollback();
+ throw $e;
+ }
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500);
+ }
+ }
+
+ /**
+ * 删除账号
+ * @return \think\response\Json
+ */
+ public function delete()
+ {
+ try {
+ $userId = $this->request->param('userId', 0);
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($userId)) {
+ return ResponseHelper::error('用户ID不能为空');
+ }
+
+ // 检查用户是否存在且属于当前公司
+ $user = Db::name('users')->where(['id' => $userId, 'companyId' => $companyId, 'typeId' => 2])->find();
+ if (!$user) {
+ return ResponseHelper::error('用户不存在或没有权限');
+ }
+
+ // 检查是否是管理账号
+ if ($user['isAdmin'] == 1) {
+ return ResponseHelper::error('管理账号无法删除');
+ }
+
+ // 软删除用户
+ Db::name('users')->where(['id' => $userId])->update([
+ 'deleteTime' => time(),
+ 'updateTime' => time()
+ ]);
+
+ // 软删除设备绑定关系
+ Db::name('device_user')->where(['userId' => $userId, 'companyId' => $companyId])->update([
+ 'deleteTime' => time()
+ ]);
+
+ return ResponseHelper::success('删除账号成功');
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500);
+ }
+ }
+
+ /**
+ * 禁用/启用账号
+ * @return \think\response\Json
+ */
+ public function disable()
+ {
+ try {
+ $userId = $this->request->param('userId', 0);
+ $status = $this->request->param('status', -1); // 0-禁用 1-启用
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($userId)) {
+ return ResponseHelper::error('用户ID不能为空');
+ }
+
+ if ($status != 0 && $status != 1) {
+ return ResponseHelper::error('状态参数错误');
+ }
+
+ // 检查用户是否存在且属于当前公司
+ $user = Db::name('users')->where(['id' => $userId, 'companyId' => $companyId, 'typeId' => 2])->find();
+ if (!$user) {
+ return ResponseHelper::error('用户不存在或没有权限');
+ }
+
+ // 检查是否是管理账号
+ if ($user['isAdmin'] == 1 && $status == 0) {
+ return ResponseHelper::error('管理账号无法禁用');
+ }
+
+ // 更新状态
+ Db::name('users')->where(['id' => $userId])->update([
+ 'status' => $status,
+ 'updateTime' => time()
+ ]);
+
+ $message = $status == 0 ? '禁用账号成功' : '启用账号成功';
+ return ResponseHelper::success($message);
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500);
+ }
+ }
+
+ /**
+ * 获取账号列表
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ $keyword = $this->request->param('keyword', '');
+ $status = $this->request->param('status', '');
+ $page = $this->request->param('page/d', 1);
+ $limit = $this->request->param('limit/d', 10);
+
+ $companyId = $this->getUserInfo('companyId');
+
+ // 构建查询条件
+ $where = [
+ ['companyId', '=', $companyId],
+ ['typeId', '=', 2], // 只查询门店端账号
+ ['deleteTime', '=', 0]
+ ];
+
+ // 关键词搜索(账号、昵称、手机号)
+ if (!empty($keyword)) {
+ $where[] = ['account|username|phone', "LIKE", '%'.$keyword.'%'];
+ }
+
+ // 状态筛选
+ if ($status !== '') {
+ $where[] = ['status', '=', $status];
+ }
+
+ // 分页查询
+ $query = Db::name('users')->where($where);
+ $total = $query->count();
+
+ $list = $query->field('id,account,username,phone,avatar,isAdmin,status,balance,tokens,createTime')
+ ->order('id desc')
+ ->page($page, $limit)
+ ->select();
+
+
+ // 获取每个账号绑定的设备(单个设备)
+ if (!empty($list)) {
+ $userIds = array_column($list, 'id');
+ $deviceBindings = Db::name('device_user')
+ ->alias('du')
+ ->join('device d', 'd.id = du.deviceId', 'left')
+ ->where([
+ ['du.userId', 'in', $userIds],
+ ['du.companyId', '=', $companyId],
+ ['du.deleteTime', '=', 0]
+ ])
+ ->field('du.userId,du.deviceId,d.imei,d.memo')
+ ->order('du.id desc')
+ ->select();
+
+ // 组织设备数据(单个设备对象)
+ $deviceMap = [];
+ foreach ($deviceBindings as $binding) {
+ $deviceMap[$binding['userId']] = [
+ 'deviceId' => $binding['deviceId'],
+ 'imei' => $binding['imei'],
+ 'memo' => $binding['memo']
+ ];
+ }
+
+ // 将设备信息添加到用户数据中
+ foreach ($list as &$item) {
+ $item['device'] = $deviceMap[$item['id']] ?? null;
+ }
+ }
+
+ return ResponseHelper::success([
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $page,
+ 'limit' => $limit
+ ]);
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500);
+ }
+ }
+}
diff --git a/application/cunkebao/controller/Task.php b/application/cunkebao/controller/Task.php
new file mode 100644
index 0000000..6ffcffb
--- /dev/null
+++ b/application/cunkebao/controller/Task.php
@@ -0,0 +1,369 @@
+scene ? $task->scene->toArray() : null;
+ $task['device'] = $task->device ? $task->device->toArray() : null;
+ }
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => $result
+ ]);
+ }
+
+ /**
+ * 获取任务详情
+ *
+ * @param int $id
+ * @return \think\response\Json
+ */
+ public function read($id)
+ {
+ $task = PlanTask::get($id, ['scene', 'device']);
+ if (!$task) {
+ return json([
+ 'code' => 404,
+ 'msg' => '任务不存在'
+ ]);
+ }
+
+ // 获取执行记录
+ $executions = PlanExecution::where('plan_id', $id)
+ ->order('createTime DESC')
+ ->select();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'task' => $task,
+ 'executions' => $executions
+ ]
+ ]);
+ }
+
+ /**
+ * 创建任务
+ *
+ * @return \think\response\Json
+ */
+ public function save()
+ {
+ $data = Request::post();
+
+ // 数据验证
+ $validate = validate('app\cunkebao\validate\Task');
+ if (!$validate->check($data)) {
+ return json([
+ 'code' => 400,
+ 'msg' => $validate->getError()
+ ]);
+ }
+
+ // 添加任务
+ $task = new PlanTask;
+ $task->save([
+ 'name' => $data['name'],
+ 'device_id' => $data['device_id'] ?? null,
+ 'scene_id' => $data['scene_id'] ?? null,
+ 'scene_config' => $data['scene_config'] ?? [],
+ 'status' => $data['status'] ?? 0,
+ 'current_step' => 0,
+ 'priority' => $data['priority'] ?? 5,
+ 'created_by' => $data['created_by'] ?? 0
+ ]);
+
+ return json([
+ 'code' => 200,
+ 'msg' => '创建成功',
+ 'data' => $task->id
+ ]);
+ }
+
+ /**
+ * 更新任务
+ *
+ * @param int $id
+ * @return \think\response\Json
+ */
+ public function update($id)
+ {
+ $data = Request::put();
+
+ // 检查任务是否存在
+ $task = PlanTask::get($id);
+ if (!$task) {
+ return json([
+ 'code' => 404,
+ 'msg' => '任务不存在'
+ ]);
+ }
+
+ // 准备更新数据
+ $updateData = [];
+
+ // 只允许更新特定字段
+ $allowedFields = ['name', 'device_id', 'scene_id', 'scene_config', 'status', 'priority'];
+ foreach ($allowedFields as $field) {
+ if (isset($data[$field])) {
+ $updateData[$field] = $data[$field];
+ }
+ }
+
+ // 更新任务
+ $task->save($updateData);
+
+ return json([
+ 'code' => 200,
+ 'msg' => '更新成功'
+ ]);
+ }
+
+ /**
+ * 删除任务
+ *
+ * @param int $id
+ * @return \think\response\Json
+ */
+ public function delete($id)
+ {
+ // 检查任务是否存在
+ $task = PlanTask::get($id);
+ if (!$task) {
+ return json([
+ 'code' => 404,
+ 'msg' => '任务不存在'
+ ]);
+ }
+
+ // 软删除任务
+ $task->delete();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '删除成功'
+ ]);
+ }
+
+ /**
+ * 启动任务
+ *
+ * @param int $id
+ * @return \think\response\Json
+ */
+ public function start($id)
+ {
+ // 检查任务是否存在
+ $task = PlanTask::get($id);
+ if (!$task) {
+ return json([
+ 'code' => 404,
+ 'msg' => '任务不存在'
+ ]);
+ }
+
+ // 更新状态为启用
+ $task->save([
+ 'status' => 1,
+ 'current_step' => 0
+ ]);
+
+ return json([
+ 'code' => 200,
+ 'msg' => '任务已启动'
+ ]);
+ }
+
+ /**
+ * 停止任务
+ *
+ * @param int $id
+ * @return \think\response\Json
+ */
+ public function stop($id)
+ {
+ // 检查任务是否存在
+ $task = PlanTask::get($id);
+ if (!$task) {
+ return json([
+ 'code' => 404,
+ 'msg' => '任务不存在'
+ ]);
+ }
+
+ // 更新状态为停用
+ $task->save([
+ 'status' => 0
+ ]);
+
+ return json([
+ 'code' => 200,
+ 'msg' => '任务已停止'
+ ]);
+ }
+
+ /**
+ * 执行定时任务(供外部调用)
+ *
+ * @return \think\response\Json
+ */
+ public function cron()
+ {
+ // 获取密钥
+ $key = Request::param('key', '');
+
+ // 验证密钥(实际生产环境应当使用更安全的验证方式)
+ if ($key !== config('task.cron_key')) {
+ return json([
+ 'code' => 403,
+ 'msg' => '访问密钥无效'
+ ]);
+ }
+
+ try {
+ // 获取待执行的任务
+ $tasks = PlanTask::getPendingTasks(5);
+ if ($tasks->isEmpty()) {
+ return json([
+ 'code' => 200,
+ 'msg' => '没有需要执行的任务',
+ 'data' => []
+ ]);
+ }
+
+ $results = [];
+
+ // 逐一执行任务
+ foreach ($tasks as $task) {
+ $runner = new TaskRunner($task);
+ $result = $runner->run();
+
+ $results[] = [
+ 'task_id' => $task->id,
+ 'name' => $task->name,
+ 'result' => $result
+ ];
+
+ // 记录执行信息
+ Log::info('任务执行', [
+ 'task_id' => $task->id,
+ 'name' => $task->name,
+ 'result' => $result
+ ]);
+ }
+
+ return json([
+ 'code' => 200,
+ 'msg' => '任务执行完成',
+ 'data' => $results
+ ]);
+
+ } catch (\Exception $e) {
+ Log::error('任务执行异常', [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString()
+ ]);
+
+ return json([
+ 'code' => 500,
+ 'msg' => '任务执行异常:' . $e->getMessage()
+ ]);
+ }
+ }
+
+ /**
+ * 手动执行任务
+ *
+ * @param int $id
+ * @return \think\response\Json
+ */
+ public function execute($id)
+ {
+ // 检查任务是否存在
+ $task = PlanTask::get($id);
+ if (!$task) {
+ return json([
+ 'code' => 404,
+ 'msg' => '任务不存在'
+ ]);
+ }
+
+ try {
+ // 执行任务
+ $runner = new TaskRunner($task);
+ $result = $runner->run();
+
+ // 记录执行信息
+ Log::info('手动执行任务', [
+ 'task_id' => $task->id,
+ 'name' => $task->name,
+ 'result' => $result
+ ]);
+
+ return json([
+ 'code' => 200,
+ 'msg' => '任务执行完成',
+ 'data' => $result
+ ]);
+
+ } catch (\Exception $e) {
+ Log::error('手动执行任务异常', [
+ 'task_id' => $task->id,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString()
+ ]);
+
+ return json([
+ 'code' => 500,
+ 'msg' => '任务执行异常:' . $e->getMessage()
+ ]);
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/TokensController.php b/application/cunkebao/controller/TokensController.php
new file mode 100644
index 0000000..8934c3c
--- /dev/null
+++ b/application/cunkebao/controller/TokensController.php
@@ -0,0 +1,535 @@
+request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $where = [
+ ['isDel', '=', 0],
+ ['status', '=', 1],
+ ];
+ $query = TokensPackage::where($where);
+ $total = $query->count();
+ $list = $query->where($where)->page($page, $limit)->order('sort ASC,id desc')->select();
+ foreach ($list as &$item) {
+ $item['description'] = json_decode($item['description'], true);
+ $item['discount'] = round(((($item['originalPrice'] - $item['price']) / $item['originalPrice']) * 100), 2);
+ $item['price'] = round($item['price'], 2);
+ $item['unitPrice'] = round($item['price'] / $item['tokens'], 6);
+ $item['originalPrice'] = round($item['originalPrice'] / 100, 2);
+ $item['tokens'] = number_format($item['tokens']);
+ }
+ unset($item);
+ return ResponseHelper::success(['list' => $list, 'total' => $total]);
+ }
+
+
+ public function pay()
+ {
+ $id = $this->request->param('id', '');
+ $price = $this->request->param('price', '');
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+ $payType = $this->request->param('payType', 'qrCode');
+
+ if (!in_array($payType, ['wechat', 'alipay', 'qrCode'])) {
+ return ResponseHelper::error('付款类型不正确');
+ }
+
+
+ if (empty($id) && empty($price)) {
+ return ResponseHelper::error('套餐和自定义购买金额必须选一个');
+ }
+
+ if (!empty($id)) {
+ $package = TokensPackage::where(['id' => $id, 'status' => 1, 'isDel' => 0])->find();
+ if (empty($package)) {
+ return ResponseHelper::error('套餐不存在或者已禁用');
+ }
+
+ if ($package['price'] <= 0) {
+ return ResponseHelper::error('套餐金额异常');
+ }
+
+ $specs = [
+ 'id' => $package['id'],
+ 'name' => $package['name'],
+ 'price' => $package['price'],
+ 'tokens' => $package['tokens'],
+ ];
+
+ } else {
+ //获取配置的tokens比例
+ $tokens_multiple = Env::get('payment.tokens_multiple', 20);
+ $specs = [
+ 'id' => 0,
+ 'name' => '自定义购买算力',
+ 'price' => intval($price * 100),
+ 'tokens' => intval($price * $tokens_multiple),
+ ];
+ }
+
+
+ $orderNo = date('YmdHis') . rand(100000, 999999);
+ $order = [
+ 'companyId' => $companyId,
+ 'userId' => $userId,
+ 'orderNo' => $orderNo,
+ 'goodsId' => $specs['id'],
+ 'goodsName' => $specs['name'],
+ 'goodsSpecs' => $specs,
+ 'orderType' => 1,
+ 'money' => $specs['price'],
+ 'service' => $payType
+ ];
+ $paymentService = new PaymentService();
+ $res = $paymentService->createOrder($order);
+ $res = json_decode($res, true);
+ if ($res['code'] == 200) {
+ return ResponseHelper::success(['orderNo' => $orderNo, 'code_url' => $res['data']], '订单创建成功');
+ } else {
+ return ResponseHelper::error($res['msg']);
+ }
+ }
+
+ public function queryOrder()
+ {
+ $orderNo = $this->request->param('orderNo', '');
+ $order = Order::where('orderNo', $orderNo)->find();
+ if (!$order) {
+ return ResponseHelper::error('该订单不存在');
+ }
+ if ($order->status != 1) {
+ $paymentService = new PaymentService();
+ $res = $paymentService->queryOrder($orderNo);
+ $res = json_decode($res, true);
+ if ($res['code'] == 200) {
+ return ResponseHelper::success($order, '订单已支付');
+ } else {
+ $errorMsg = !empty($order['payInfo']) ? $order['payInfo'] : '订单未支付';
+ return ResponseHelper::success($order,$errorMsg);
+ }
+ } else {
+ return ResponseHelper::success($order, '订单已支付');
+ }
+ }
+
+
+
+ /**
+ * 获取订单列表
+ * @return \think\response\Json
+ */
+ public function getOrderList()
+ {
+ try {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $status = $this->request->param('status', ''); // 订单状态筛选
+ $keyword = $this->request->param('keyword', ''); // 关键词搜索(订单号)
+ $orderType = $this->request->param('orderType', ''); // 订单类型筛选
+ $payType = $this->request->param('payType', ''); // 支付类型筛选
+ $startTime = $this->request->param('startTime', ''); // 开始时间
+ $endTime = $this->request->param('endTime', ''); // 结束时间
+
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+
+ // 构建查询条件
+ $where = [
+ ['userId', '=', $userId],
+ ['companyId', '=', $companyId]
+ ];
+
+ // 关键词搜索(订单号、商品名称)
+ if (!empty($keyword)) {
+ $where[] = ['orderNo|goodsName', 'like', '%' . $keyword . '%'];
+ }
+
+ // 状态筛选 (0-待支付 1-已付款 2-已退款 3-付款失败)
+ if ($status !== '') {
+ $where[] = ['status', '=', $status];
+ }
+
+ // 订单类型筛选
+ if ($orderType !== '') {
+ $where[] = ['orderType', '=', $orderType];
+ }
+
+ // 支付类型筛选
+ if($payType !== '') {
+ $where[] = ['payType', '=', $payType];
+ }
+
+ // 时间范围筛选
+ if (!empty($startTime)) {
+ $where[] = ['createTime', '>=', strtotime($startTime)];
+ }
+ if (!empty($endTime)) {
+ $where[] = ['createTime', '<=', strtotime($endTime . ' 23:59:59')];
+ }
+
+ // 分页查询
+ $query = Order::where($where)
+ ->where(function ($query) {
+ $query->whereNull('deleteTime')->whereOr('deleteTime', 0);
+ });
+ $total = $query->count();
+
+ $list = $query->field('id,orderNo,goodsId,goodsName,goodsSpecs,orderType,money,status,payType,payTime,createTime')
+ ->order('id desc')
+ ->page($page, $limit)
+ ->select();
+
+ // 格式化数据
+ foreach ($list as &$item) {
+ // 金额转换(分转元)
+ $item['money'] = round($item['money'] / 100, 2);
+
+ // 解析商品规格
+ if (!empty($item['goodsSpecs'])) {
+ $specs = is_string($item['goodsSpecs']) ? json_decode($item['goodsSpecs'], true) : $item['goodsSpecs'];
+ $item['goodsSpecs'] = $specs;
+
+ // 添加算力数量
+ if (isset($specs['tokens'])) {
+ $item['tokens'] = number_format($specs['tokens']);
+ }
+ }
+
+ // 状态文本
+ $statusText = [
+ 0 => '待支付',
+ 1 => '已付款',
+ 2 => '已退款',
+ 3 => '付款失败'
+ ];
+ $item['statusText'] = $statusText[$item['status']] ?? '未知';
+
+ // 订单类型文本
+ $orderTypeText = [
+ 1 => '购买算力'
+ ];
+ $item['orderTypeText'] = $orderTypeText[$item['orderType']] ?? '其他';
+
+ // 支付类型文本
+ $payTypeText = [
+ 1 => '微信支付',
+ 2 => '支付宝'
+ ];
+ $item['payTypeText'] = !empty($item['payType']) ? ($payTypeText[$item['payType']] ?? '未知') : '';
+
+ // 格式化时间
+ $item['createTime'] = $item['createTime'] ? date('Y-m-d H:i:s', $item['createTime']) : '';
+ $item['payTime'] = $item['payTime'] ? date('Y-m-d H:i:s', $item['payTime']) : '';
+ }
+ unset($item);
+
+ return ResponseHelper::success([
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $page,
+ 'limit' => $limit
+ ]);
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('获取订单列表失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 获取公司算力统计信息
+ * 包括:总算力、今日使用、本月使用、剩余算力
+ *
+ * @return \think\response\Json
+ */
+ public function getTokensStatistics()
+ {
+ try {
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+ if (empty($companyId)) {
+ return ResponseHelper::error('公司信息获取失败');
+ }
+
+ // 获取公司算力余额
+ $tokensCompany = TokensCompany::where(['companyId' => $companyId,'userId' => $userId])->find();
+ $remainingTokens = $tokensCompany ? intval($tokensCompany->tokens) : 0;
+
+ // 获取今日开始和结束时间戳
+ $todayStart = strtotime(date('Y-m-d 00:00:00'));
+ $todayEnd = strtotime(date('Y-m-d 23:59:59'));
+
+ // 获取本月开始和结束时间戳
+ $monthStart = strtotime(date('Y-m-01 00:00:00'));
+ $monthEnd = strtotime(date('Y-m-t 23:59:59'));
+
+ // 统计今日消费(type=0表示消费)
+ $todayUsed = TokensRecord::where([
+ ['userId', '=', $userId],
+ ['companyId', '=', $companyId],
+ ['type', '=', 0], // 0为减少(消费)
+ ['createTime', '>=', $todayStart],
+ ['createTime', '<=', $todayEnd]
+ ])->sum('tokens');
+ $todayUsed = intval($todayUsed);
+
+ // 统计本月消费
+ $monthUsed = TokensRecord::where([
+ ['userId', '=', $userId],
+ ['companyId', '=', $companyId],
+ ['type', '=', 0], // 0为减少(消费)
+ ['createTime', '>=', $monthStart],
+ ['createTime', '<=', $monthEnd]
+ ])->sum('tokens');
+ $monthUsed = intval($monthUsed);
+
+ // 计算总算力(当前剩余 + 历史总消费)
+ $totalConsumed = TokensRecord::where([
+ ['userId', '=', $userId],
+ ['companyId', '=', $companyId],
+ ['type', '=', 0]
+ ])->sum('tokens');
+ $totalConsumed = intval($totalConsumed);
+
+ // 总充值算力
+ $totalRecharged = TokensRecord::where([
+ ['userId', '=', $userId],
+ ['companyId', '=', $companyId],
+ ['type', '=', 1] // 1为增加(充值)
+ ])->sum('tokens');
+ $totalRecharged = intval($totalRecharged);
+
+ // 计算预计可用天数(基于过去一个月的平均消耗)
+ $estimatedDays = $this->calculateEstimatedDays($userId,$companyId, $remainingTokens);
+
+ return ResponseHelper::success([
+ 'totalTokens' => $totalRecharged, // 总算力(累计充值)
+ 'todayUsed' => $todayUsed, // 今日使用
+ 'monthUsed' => $monthUsed, // 本月使用
+ 'remainingTokens' => $remainingTokens, // 剩余算力
+ 'totalConsumed' => $totalConsumed, // 累计消费
+ 'estimatedDays' => $estimatedDays, // 预计可用天数
+ ], '获取成功');
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('获取算力统计失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 计算预计可用天数(基于过去一个月的平均消耗)
+ * @param int $userId 用户ID
+ * @param int $companyId 公司ID
+ * @param int $remainingTokens 当前剩余算力
+ * @return int 预计可用天数,-1表示无法计算(无消耗记录或余额为0)
+ */
+ private function calculateEstimatedDays($userId,$companyId, $remainingTokens)
+ {
+ // 如果余额为0或负数,无法计算
+ if ($remainingTokens <= 0) {
+ return -1;
+ }
+
+ // 计算过去30天的消耗总量(只统计减少的记录,type=0)
+ $oneMonthAgo = time() - (30 * 24 * 60 * 60); // 30天前的时间戳
+
+ $totalConsumed = TokensRecord::where([
+ ['userId', '=', $userId],
+ ['companyId', '=', $companyId],
+ ['type', '=', 0], // 只统计减少的记录
+ ['createTime', '>=', $oneMonthAgo]
+ ])->sum('tokens');
+
+ $totalConsumed = intval($totalConsumed);
+
+ // 如果过去30天没有消耗记录,无法计算
+ if ($totalConsumed <= 0) {
+ return -1;
+ }
+
+ // 计算平均每天消耗量
+ $avgDailyConsumption = $totalConsumed / 30;
+
+ // 如果平均每天消耗为0,无法计算
+ if ($avgDailyConsumption <= 0) {
+ return -1;
+ }
+
+ // 计算预计可用天数 = 当前余额 / 平均每天消耗量
+ $estimatedDays = floor($remainingTokens / $avgDailyConsumption);
+
+ return $estimatedDays;
+ }
+
+ /**
+ * 分配token(仅管理员可用)
+ * @return \think\response\Json
+ */
+ public function allocateTokens()
+ {
+ try {
+ $userId = $this->getUserInfo('id');
+ $companyId = $this->getUserInfo('companyId');
+ $targetUserId = (int)$this->request->param('targetUserId', 0);
+ $tokens = (int)$this->request->param('tokens', 0);
+ $remarks = $this->request->param('remarks', '');
+
+ // 验证参数
+ if (empty($targetUserId)) {
+ return ResponseHelper::error('目标用户ID不能为空');
+ }
+
+ if ($tokens <= 0) {
+ return ResponseHelper::error('分配的token数量必须大于0');
+ }
+
+ if (empty($companyId)) {
+ return ResponseHelper::error('公司信息获取失败');
+ }
+
+ // 验证当前用户是否为管理员
+ $currentUser = User::where([
+ 'id' => $userId,
+ 'companyId' => $companyId
+ ])->find();
+
+ if (empty($currentUser)) {
+ return ResponseHelper::error('用户信息不存在');
+ }
+
+ if (empty($currentUser->isAdmin) || $currentUser->isAdmin != 1) {
+ return ResponseHelper::error('只有管理员才能分配token');
+ }
+
+ // 验证目标用户是否存在且属于同一公司
+ $targetUser = User::where([
+ 'id' => $targetUserId,
+ 'companyId' => $companyId
+ ])->find();
+
+ if (empty($targetUser)) {
+ return ResponseHelper::error('目标用户不存在或不属于同一公司');
+ }
+
+ // 检查分配者的token余额
+ $allocatorTokens = TokensCompany::where([
+ 'companyId' => $companyId,
+ 'userId' => $userId
+ ])->find();
+
+ $allocatorBalance = $allocatorTokens ? intval($allocatorTokens->tokens) : 0;
+
+ if ($allocatorBalance < $tokens) {
+ return ResponseHelper::error('token余额不足,当前余额:' . $allocatorBalance);
+ }
+
+ // 开始事务
+ Db::startTrans();
+
+ try {
+ // 1. 减少分配者的token
+ if (!empty($allocatorTokens)) {
+ $allocatorTokens->tokens = $allocatorBalance - $tokens;
+ $allocatorTokens->updateTime = time();
+ $allocatorTokens->save();
+ $allocatorNewBalance = $allocatorTokens->tokens;
+ } else {
+ // 如果分配者没有记录,创建一条(余额为0)
+ $allocatorTokens = new TokensCompany();
+ $allocatorTokens->userId = $userId;
+ $allocatorTokens->companyId = $companyId;
+ $allocatorTokens->tokens = 0;
+ $allocatorTokens->isAdmin = 1;
+ $allocatorTokens->createTime = time();
+ $allocatorTokens->updateTime = time();
+ $allocatorTokens->save();
+ $allocatorNewBalance = 0;
+ }
+
+ // 2. 记录分配者的减少记录
+ $targetUserAccount = $targetUser->account ?? $targetUser->phone ?? '用户ID[' . $targetUserId . ']';
+ $allocatorRecord = new TokensRecord();
+ $allocatorRecord->companyId = $companyId;
+ $allocatorRecord->userId = $userId;
+ $allocatorRecord->type = 0; // 0为减少
+ $allocatorRecord->form = 1001; // 1001表示分配
+ $allocatorRecord->wechatAccountId = 0;
+ $allocatorRecord->friendIdOrGroupId = $targetUserId;
+ $allocatorRecord->remarks = !empty($remarks) ? $remarks : '分配给' . $targetUserAccount;
+ $allocatorRecord->tokens = $tokens;
+ $allocatorRecord->balanceTokens = $allocatorNewBalance;
+ $allocatorRecord->createTime = time();
+ $allocatorRecord->save();
+
+ // 3. 增加接收者的token
+ $receiverTokens = TokensCompany::where([
+ 'companyId' => $companyId,
+ 'userId' => $targetUserId
+ ])->find();
+
+ if (!empty($receiverTokens)) {
+ $receiverTokens->tokens = intval($receiverTokens->tokens) + $tokens;
+ $receiverTokens->updateTime = time();
+ $receiverTokens->save();
+ $receiverNewBalance = $receiverTokens->tokens;
+ } else {
+ // 如果接收者没有记录,创建一条
+ $receiverTokens = new TokensCompany();
+ $receiverTokens->userId = $targetUserId;
+ $receiverTokens->companyId = $companyId;
+ $receiverTokens->tokens = $tokens;
+ $receiverTokens->isAdmin = (!empty($targetUser->isAdmin) && $targetUser->isAdmin == 1) ? 1 : 0;
+ $receiverTokens->createTime = time();
+ $receiverTokens->updateTime = time();
+ $receiverTokens->save();
+ $receiverNewBalance = $tokens;
+ }
+
+ // 4. 记录接收者的增加记录
+ $adminAccount = $currentUser->account ?? $currentUser->phone ?? '管理员';
+ $receiverRecord = new TokensRecord();
+ $receiverRecord->companyId = $companyId;
+ $receiverRecord->userId = $targetUserId;
+ $receiverRecord->type = 1; // 1为增加
+ $receiverRecord->form = 1001; // 1001表示分配
+ $receiverRecord->wechatAccountId = 0;
+ $receiverRecord->friendIdOrGroupId = $userId;
+ $receiverRecord->remarks = !empty($remarks) ? '管理员分配:' . $remarks : '管理员分配';
+ $receiverRecord->tokens = $tokens;
+ $receiverRecord->balanceTokens = $receiverNewBalance;
+ $receiverRecord->createTime = time();
+ $receiverRecord->save();
+
+ Db::commit();
+
+ return ResponseHelper::success([
+ 'allocatorBalance' => $allocatorNewBalance,
+ 'receiverBalance' => $receiverNewBalance,
+ 'allocatedTokens' => $tokens
+ ], '分配成功');
+
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error('分配失败:' . $e->getMessage());
+ }
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('分配失败:' . $e->getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/TrafficController.php b/application/cunkebao/controller/TrafficController.php
new file mode 100644
index 0000000..4ceefc9
--- /dev/null
+++ b/application/cunkebao/controller/TrafficController.php
@@ -0,0 +1,321 @@
+request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+
+ $companyId = $this->getUserInfo('companyId');
+ $package = Db::name('traffic_source_package')->alias('tsp')
+ ->join('traffic_source_package_item tspi', 'tspi.packageId=tsp.id', 'left')
+ ->whereIn('tsp.companyId', [$companyId, 0])
+ ->field('tsp.id,tsp.name,tsp.description,tsp.pic,tsp.isSys as type,tsp.createTime,count(tspi.id) as num')
+ ->group('tsp.id');
+
+ if (!empty($keyword)) {
+ $package->where('tsp.name|tsp.description', 'like', '%' . $keyword . '%');
+ }
+
+ $list = $package->page($page, $limit)->order('isSys ASC,id DESC')->select();
+ $total = $package->count();
+
+ $rfmRule = 'default';
+ foreach ($list as $k => &$v) {
+ if ($v['type'] != 1) {
+ $v['createTime'] = !empty($v['createTime']) ? formatRelativeTime($v['createTime']) : '';
+ } else {
+ $v['createTime'] = '';
+ }
+
+ // RFM 评分(示例:以创建时间近似最近活跃,num 近似频次;金额若无则为 0)
+ $recencyDays = isset($v['createTime']) && is_numeric($v['createTime']) ? floor((time() - (int)$v['createTime']) / 86400) : null;
+ // 如果上方被格式化为文本,则尝试从原始结果集取原值
+ if (!is_numeric($recencyDays) || $recencyDays === null) {
+ $rawCreate = isset($list[$k]['createTime']) ? $list[$k]['createTime'] : null;
+ $recencyDays = is_numeric($rawCreate) ? floor((time() - (int)$rawCreate) / 86400) : 9999;
+ }
+ $frequency = (int)($v['num'] ?? 0);
+ $monetary = (float)($v['monetary'] ?? 0);
+
+ $scores = RFMController::calcRfmScores($recencyDays, $frequency, $monetary);
+ $v['R'] = $scores['R'];
+ $v['F'] = $scores['F'];
+ $v['M'] = $scores['M'];
+ $v['RFM'] = $scores['R'] + $scores['F'] + $scores['M'];
+ }
+ unset($v);
+
+ $data = [
+ 'total' => $total,
+ 'list' => $list,
+ ];
+
+ return ResponseHelper::success($data);
+ }
+
+ /**
+ * 添加流量池
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function addPackage()
+ {
+ $packageName = $this->request->param('packageName', '');
+ $description = $this->request->param('description', '');
+ $pic = $this->request->param('pic', '');
+ $companyId = $this->getUserInfo('companyId');
+ $userId = $this->getUserInfo('id');
+
+ if (empty($packageName)) {
+ return ResponseHelper::error('流量池名称不能为空');
+ }
+
+ $package = TrafficSourcePackage::where(['isDel' => 0, 'name' => $packageName])
+ ->whereIn('companyId', [$companyId, 0])
+ ->field('id,name')
+ ->find();
+ if (!empty($package)) {
+ return ResponseHelper::error('该流量池名称已存在');
+ }
+ $packageId = TrafficSourcePackage::insertGetId([
+ 'userId' => $userId,
+ 'companyId' => $companyId,
+ 'name' => $packageName,
+ 'description' => $description,
+ 'pic' => $pic,
+ 'matchingRules' => json_encode([]),
+ 'createTime' => time(),
+ 'isDel' => 0,
+ ]);
+
+ if (!empty($packageId)) {
+ return ResponseHelper::success($packageId, '该流量添加成功');
+ } else {
+ return ResponseHelper::error('该流量添加失败');
+ }
+ }
+
+ /**
+ * 编辑流量池
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function editPackage()
+ {
+ $packageId = $this->request->param('packageId', '');
+ $packageName = $this->request->param('packageName', '');
+ $description = $this->request->param('description', '');
+ $pic = $this->request->param('pic', '');
+ $companyId = $this->getUserInfo('companyId');
+ $userId = $this->getUserInfo('id');
+
+ if (empty($packageId)) {
+ return ResponseHelper::error('流量池ID不能为空');
+ }
+
+ if (empty($packageName)) {
+ return ResponseHelper::error('流量池名称不能为空');
+ }
+
+ // 检查流量池是否存在且属于当前公司
+ $package = TrafficSourcePackage::where(['id' => $packageId, 'isDel' => 0])
+ ->whereIn('companyId', [$companyId, 0])
+ ->find();
+ if (empty($package)) {
+ return ResponseHelper::error('流量池不存在或已删除');
+ }
+
+ // 检查系统流量池是否可编辑
+ if ($package['isSys'] == 1) {
+ return ResponseHelper::error('系统流量池不允许编辑');
+ }
+
+ // 检查名称是否重复(排除当前记录)
+ $existPackage = TrafficSourcePackage::where(['isDel' => 0, 'name' => $packageName])
+ ->whereIn('companyId', [$companyId, 0])
+ ->where('id', '<>', $packageId)
+ ->field('id,name')
+ ->find();
+ if (!empty($existPackage)) {
+ return ResponseHelper::error('该流量池名称已存在');
+ }
+
+ // 更新流量池信息
+ $updateData = [
+ 'name' => $packageName,
+ 'updateTime' => time(),
+ ];
+
+ // 更新描述字段(允许为空)
+ $updateData['description'] = $description;
+
+ // 更新图片字段(允许为空)
+ $updateData['pic'] = $pic;
+
+ $result = TrafficSourcePackage::where('id', $packageId)->update($updateData);
+
+ if ($result !== false) {
+ return ResponseHelper::success($packageId, '流量池编辑成功');
+ } else {
+ return ResponseHelper::error('流量池编辑失败');
+ }
+ }
+
+ /**
+ * 删除流量池(假删除)
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function deletePackage()
+ {
+ $packageId = $this->request->param('packageId', '');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($packageId)) {
+ return ResponseHelper::error('流量池ID不能为空');
+ }
+
+ // 检查流量池是否存在且属于当前公司
+ $package = TrafficSourcePackage::where(['id' => $packageId, 'isDel' => 0])
+ ->whereIn('companyId', [$companyId, 0])
+ ->find();
+ if (empty($package)) {
+ return ResponseHelper::error('流量池不存在或已删除');
+ }
+
+ // 检查系统流量池是否可删除
+ if ($package['isSys'] == 1) {
+ return ResponseHelper::error('系统流量池不允许删除');
+ }
+
+ // 开启事务
+ Db::startTrans();
+ try {
+ // 执行流量池假删除
+ $result = TrafficSourcePackage::where('id', $packageId)->update([
+ 'isDel' => 1,
+ 'deleteTime' => time()
+ ]);
+
+ if ($result === false) {
+ throw new \Exception('流量池删除失败');
+ }
+
+ // 删除流量池内容(TrafficSourcePackageItem)假删除
+ $itemResult = TrafficSourcePackageItem::where([
+ 'packageId' => $packageId,
+ 'companyId' => $companyId,
+ 'isDel' => 0
+ ])->update([
+ 'isDel' => 1,
+ 'deleteTime' => time()
+ ]);
+
+ // 提交事务
+ Db::commit();
+
+ return ResponseHelper::success($packageId, '流量池及内容删除成功');
+
+ } catch (\Exception $e) {
+ // 回滚事务
+ Db::rollback();
+ return ResponseHelper::error('删除失败:' . $e->getMessage());
+ }
+ }
+
+
+ /**
+ * 流量池列表
+ * @return \think\response\Json
+ * @throws \Exception
+ */
+ public function getTrafficPoolList()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+ $packageId = $this->request->param('packageId', '');
+ $companyId = $this->getUserInfo('companyId');
+ $userId = $this->getUserInfo('id');
+
+ if (empty($packageId)) {
+ return ResponseHelper::error('流量包id不能为空');
+ }
+
+ $trafficSourcePackage = TrafficSourcePackage::where(['id' => $packageId, 'isDel' => 0])->whereIn('companyId', [$companyId, 0])->find();
+ if (empty($trafficSourcePackage)) {
+ return ResponseHelper::error('流量包不存在或已删除');
+ }
+ $where = [
+ ['tspi.companyId', '=', $companyId],
+ ['tspi.packageId', '=', $packageId],
+ ];
+
+ if (empty($keyword)) {
+ $where[] = ['wa.nickname|wa.phone|wa.alias|wa.wechatId|p.mobile|p.identifier', 'like', '%' . $keyword . '%'];
+ }
+
+ $query = TrafficSourcePackageItem::alias('tspi')
+ ->field(
+ [
+ 'p.id', 'p.identifier', 'p.mobile', 'p.wechatId', 'tspi.companyId',
+ 'wa.nickname', 'wa.avatar', 'wa.gender', 'wa.phone', 'wa.alias'
+ ]
+ )
+ ->join('traffic_pool p', 'p.identifier=tspi.identifier', 'left')
+ ->join('wechat_account wa', 'tspi.identifier=wa.wechatId', 'left')
+ ->where($where);
+
+ $query->order('tspi.id DESC,p.id DESC')->group('p.identifier');
+
+ $list = $query->page($page, $limit)->select()->toArray();
+ $total = $query->count();
+
+ foreach ($list as $k => &$v) {
+ //流量池筛选
+ $package = TrafficSourcePackageItem::alias('tspi')
+ ->join('traffic_source_package p', 'tspi.packageId=p.id AND tspi.companyId=p.companyId')
+ ->where(['tspi.identifier' => $v['identifier']])
+ ->whereIn('tspi.companyId', [0, $v['companyId']])
+ ->column('p.name');
+ $v['packages'] = $package;
+ $v['phone'] = !empty($v['phone']) ? $v['phone'] : $v['mobile'];
+ unset($v['mobile']);
+
+
+ $scores = RFMController::calcRfmScores(30, 30, 30);
+ $v['R'] = $scores['R'];
+ $v['F'] = $scores['F'];
+ $v['M'] = $scores['M'];
+ $v['RFM'] = $scores['R'] + $scores['F'] + $scores['M'];
+ $v['money'] = 2222;
+ $v['msgCount'] = 2222;
+ $v['tag'] = ['test', 'test2'];
+ }
+ unset($v);
+
+
+ $data = ['list' => $list, 'total' => $total];
+ return ResponseHelper::success($data);
+ }
+
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/chatroom/GetChatroomListV1Controller.php b/application/cunkebao/controller/chatroom/GetChatroomListV1Controller.php
new file mode 100644
index 0000000..d9b11a0
--- /dev/null
+++ b/application/cunkebao/controller/chatroom/GetChatroomListV1Controller.php
@@ -0,0 +1,152 @@
+request->param('page', 1);
+ $limit = $this->request->param('limit', 20);
+ $keyword = $this->request->param('keyword', '');
+ try {
+
+ $companyId = (int)$this->getUserInfo('companyId');
+ $wechatIds = Db::name('device')->alias('d')
+ // 仅关联每个设备在 device_wechat_login 中的最新一条记录
+ ->join('(SELECT MAX(id) AS id, deviceId FROM ck_device_wechat_login WHERE companyId='.$companyId.' GROUP BY deviceId) dwl_max','dwl_max.deviceId = d.id')
+ ->join('device_wechat_login dwl','dwl.id = dwl_max.id')
+ ->where(['d.companyId' => $companyId,'d.deleteTime' => 0])
+ ->column('dwl.wechatId');
+
+
+ /* $wechatIds = Db::name('device')->alias('d')
+ ->join('device_wechat_login dwl','dwl.deviceId=d.id AND dwl.companyId='.$this->getUserInfo('companyId'))
+ ->where(['d.companyId' => $this->getUserInfo('companyId'),'d.deleteTime' => 0])
+ ->column('dwl.wechatId');*/
+
+
+ $where = [];
+ if ($this->getUserInfo('isAdmin') == 1) {
+ $where[] = ['gg.isDeleted', '=', 0];
+ $where[] = ['g.ownerWechatId', 'in', $wechatIds];
+ } else {
+ $where[] = ['gg.isDeleted', '=', 0];
+ $where[] = ['g.ownerWechatId', 'in', $wechatIds];
+ //$where[] = ['g.userId', '=', $this->getUserInfo('id')];
+ }
+
+ if(!empty($keyword)){
+ $where[] = ['g.name', 'like', '%'.$keyword.'%'];
+ }
+
+ $data = WechatChatroom::alias('g')
+ ->field(['g.id', 'g.chatroomId', 'g.name', 'g.avatar','g.ownerWechatId', 'g.identifier', 'g.createTime',
+ 'wa.nickname as ownerNickname','wa.avatar as ownerAvatar','wa.alias as ownerAlias'])
+ ->join('wechat_account wa', 'g.ownerWechatId = wa.wechatId', 'LEFT')
+ ->join(['s2_wechat_chatroom' => 'gg'], 'g.id = gg.id', 'LEFT')
+ ->where($where);
+
+ $total = $data->count();
+ $list = $data->page($page, $limit)->order('g.id DESC')->select();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ ]
+ ]);
+ } catch (\Exception $e) {
+ return json([
+ 'code' => $e->getCode(),
+ 'msg' => $e->getMessage()
+ ]);
+ }
+ }
+
+ /**
+ * 获取群成员列表
+ * @return \think\response\Json
+ */
+ public function getMemberList()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 20);
+ $keyword = $this->request->param('keyword', '');
+ $groupId = $this->request->param('groupId', 0);
+
+ if (empty($groupId)) {
+ return json([
+ 'code' => 400,
+ 'msg' => '群ID不能为空'
+ ]);
+ }
+
+ try {
+ $where = [];
+ $where[] = ['m.groupId', '=', $groupId];
+ $where[] = ['m.deleteTime', '<=', 0];
+
+ // 如果有搜索关键词
+ if (!empty($keyword)) {
+ $where[] = ['wa.nickname|m.identifier', 'like', '%'.$keyword.'%'];
+ }
+
+ $data = Db::name('wechat_group_member')
+ ->alias('m')
+ ->field([
+ 'm.id',
+ 'm.identifier',
+ 'm.customerIs',
+ 'wa.nickname',
+ 'wa.avatar',
+ 'm.groupId',
+ 'm.createTime',
+ 'g.name as groupName',
+ 'g.chatroomId'
+ ])
+ ->join('wechat_group g', 'm.groupId = g.id', 'LEFT')
+ ->join('wechat_account wa', 'wa.wechatId = m.identifier', 'LEFT')
+ ->where($where);
+
+ $total = $data->count();
+ $list = $data->page($page, $limit)
+ ->order('m.id DESC')
+ ->select();
+
+ // 格式化时间
+ foreach ($list as &$item) {
+ if (!empty($item['createTime'])) {
+ $item['createTime'] = date('Y-m-d H:i:s', $item['createTime']);
+ }
+ }
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ ]
+ ]);
+ } catch (\Exception $e) {
+ return json([
+ 'code' => $e->getCode() ?: 500,
+ 'msg' => $e->getMessage()
+ ]);
+ }
+ }
+}
diff --git a/application/cunkebao/controller/device/DeleteDeviceV1Controller.php b/application/cunkebao/controller/device/DeleteDeviceV1Controller.php
new file mode 100644
index 0000000..cb1b4a2
--- /dev/null
+++ b/application/cunkebao/controller/device/DeleteDeviceV1Controller.php
@@ -0,0 +1,145 @@
+getUserInfo('companyId');
+ $deviceUser = DeviceUserModel::where(compact('companyId', 'deviceId'))->find();
+
+ // 有关联数据则删除
+ if ($deviceUser) {
+ if (!$deviceUser->delete()) {
+ throw new \Exception('设备用户关联数据删除失败', 402);
+ }
+ }
+ }
+
+ /**
+ * 删除设备任务配置记录
+ *
+ * @param int $deviceId
+ * @return void
+ * @throws \Exception
+ */
+ protected function deleteDeviceConf(int $deviceId): void
+ {
+ $companyId = $this->getUserInfo('companyId');
+ $deviceConf = DeviceTaskconfModel::where(compact('companyId', 'deviceId'))->find();
+
+ // 有配置信息则删除
+ if ($deviceConf) {
+ if (!$deviceConf->delete()) {
+ throw new \Exception('设备设置信息删除失败', 402);
+ }
+ }
+ }
+
+ /**
+ * 删除主设备信息
+ *
+ * @param int $id
+ * @return DeviceModel
+ * @throws \Exception
+ */
+ protected function deleteDevice(int $id): void
+ {
+ $device = DeviceModel::where('companyId', $this->getUserInfo('companyId'))->find($id);
+
+ if (!$device) {
+ throw new \Exception('设备不存在或无权限操作', 404);
+ }
+
+ if (!$device->delete()) {
+ throw new \Exception('设备删除失败', 402);
+ }
+ }
+
+ /**
+ * 删除存客宝设备数据
+ *
+ * @param int $id
+ * @return $this
+ * @throws \Exception
+ */
+ protected function deleteCkbAbout(int $id): self
+ {
+ $apiDevice = new ApiDevice();
+ $res = $apiDevice->delDevice($id);
+ $res = json_decode($res, true);
+ if ($res['code'] == 200){
+ $this->deleteDevice($id);
+ $this->deleteDeviceConf($id);
+ $this->deleteDeviceUser($id);
+ return $this;
+ }else{
+ return false;
+ }
+ }
+
+ /**
+ * TODO 删除存客宝设备数据
+ *
+ * @return self
+ */
+ protected function deleteS2About(): self
+ {
+ return $this;
+ }
+
+ /**
+ * 检查用户权限,只有操盘手可以删除设备
+ *
+ * @return $this
+ */
+ protected function checkPermission(): self
+ {
+ if ($this->getUserInfo('typeId') != UserModel::MASTER_USER) {
+ throw new \Exception('您没有权限删除设备', 403);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 删除设备
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ $id = $this->request->param('id/d');
+
+ Db::startTrans();
+ $this->checkPermission();
+ $this->deleteCkbAbout($id)->deleteS2About($id);
+ Db::commit();
+
+ return ResponseHelper::success();
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/device/GetAddResultedV1Controller.php b/application/cunkebao/controller/device/GetAddResultedV1Controller.php
new file mode 100644
index 0000000..a2c78b0
--- /dev/null
+++ b/application/cunkebao/controller/device/GetAddResultedV1Controller.php
@@ -0,0 +1,165 @@
+value('companyId');
+ }
+
+ /**
+ * 获取项目下的所有设备。
+ *
+ * @param int $companyId
+ * @return array
+ */
+ protected function getAllDevicesIdWithInCompany(int $companyId): array
+ {
+ return DeviceModel::where('companyId', $companyId)->column('id') ?: [0];
+ }
+
+ /**
+ * 执行数据迁移。
+ *
+ * @param int $accountId
+ * @return void
+ */
+ protected function migrateData(int $accountId): void
+ {
+ $companyId = $this->getCompanyIdByAccountId($accountId);
+ $deviceIds = $this->getAllDevicesIdWithInCompany($companyId) ?: [0];
+
+ // 从 s2_device 导入数据。
+ $this->getNewDeviceFromS2_device($deviceIds, $companyId);
+ }
+
+ /**
+ * 从 s2_device 导入数据。
+ *
+ * @param array $ids
+ * @param int $companyId
+ * @return void
+ */
+ protected function getNewDeviceFromS2_device(array $ids, int $companyId): void
+ {
+ $ids = implode(',', $ids);
+
+ $sql = "INSERT INTO ck_device(`id`, `imei`, `model`, phone, operatingSystem, memo, alive, brand, rooted, xPosed, softwareVersion, extra, createTime, updateTime, deleteTime, companyId)
+ SELECT
+ d.id, d.imei, d.model, d.phone, d.operatingSystem, d.memo, d.alive, d.brand, d.rooted, d.xPosed, d.softwareVersion, d.extra, d.createTime, d.lastUpdateTime, d.deleteTime, a.departmentId AS companyId
+ FROM s2_device d
+ JOIN s2_company_account a ON d.currentAccountId = a.id
+ WHERE isDeleted = 0 AND deletedAndStop = 0 AND d.id NOT IN ({$ids}) AND a.departmentId = {$companyId}
+ ON DUPLICATE KEY UPDATE
+ imei = VALUES(imei),
+ model = VALUES(model),
+ phone = VALUES(phone),
+ operatingSystem = VALUES(operatingSystem),
+ memo = VALUES(memo),
+ alive = VALUES(alive),
+ brand = VALUES(brand),
+ rooted = VALUES(rooted),
+ xPosed = VALUES(xPosed),
+ softwareVersion = VALUES(softwareVersion),
+ extra = VALUES(extra),
+ updateTime = VALUES(updateTime),
+ deleteTime = VALUES(deleteTime),
+ companyId = VALUES(companyId)";
+
+ Db::query($sql);
+ }
+
+ /**
+ * 获取当前设备数量
+ *
+ * @return int
+ */
+ protected function getCkbDeviceCount(): int
+ {
+ $companyId = $this->getUserInfo('companyId');
+ $cacheKey = 'deviceNum_'.$companyId;
+ $deviceNum = Cache::get($cacheKey);
+ if (empty($deviceNum)) {
+ $deviceNum = DeviceModel::where(['companyId' => $companyId])->count('*');
+ Cache::set($cacheKey,$deviceNum,120);
+ }
+ return $deviceNum;
+ }
+
+ /**
+ * 获取添加的关联设备结果。
+ *
+ * @param int $accountId
+ * @return bool
+ */
+ protected function getAddResulted(int $accountId): bool
+ {
+ $deviceNum = $this->getCkbDeviceCount();
+ $result = (new ApiDeviceController())->getlist(
+ [
+ 'accountId' => $accountId,
+ 'pageIndex' => 0,
+ 'pageSize' => 100
+ ],
+ true
+ );
+ $result = json_decode($result, true);
+ $result = $result['data']['results'] ?? false;
+
+ if (empty($result)){
+ return false;
+ }else{
+ if (count($result) > $deviceNum){
+ $companyId = $this->getUserInfo('companyId');
+ $cacheKey = 'deviceNum_'.$companyId;
+ Cache::rm($cacheKey);
+ return true;
+ }else{
+ return false;
+ }
+ }
+
+ }
+
+ /**
+ * 获取基础统计信息
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ $accountId = $this->request->param('accountId/d');
+
+ if (empty($accountId)){
+ return ResponseHelper::error('参数缺失');
+ }
+
+ $isAdded = $this->getAddResulted($accountId);
+ $isAdded && $this->migrateData($accountId);
+
+ return ResponseHelper::success(
+ [
+ 'added' => $isAdded
+ ]
+ );
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/device/GetDeviceDetailV1Controller.php b/application/cunkebao/controller/device/GetDeviceDetailV1Controller.php
new file mode 100644
index 0000000..d61bad5
--- /dev/null
+++ b/application/cunkebao/controller/device/GetDeviceDetailV1Controller.php
@@ -0,0 +1,188 @@
+ $deviceId,
+ 'userId' => $this->getUserInfo('id'),
+ 'companyId' => $this->getUserInfo('companyId')
+ ]
+ )
+ ->count() > 0;
+
+ if (!$hasPermission) {
+ throw new \Exception('您没有权限查看该设备', 403);
+ }
+ }
+
+ /**
+ * 解析设备额外信息
+ *
+ * @param string $extra
+ * @return int
+ */
+ protected function parseExtraForBattery(string $extra): int
+ {
+ if (!empty($extra)) {
+ $extra = json_decode($extra);
+
+ if ($extra && isset($extra->battery)) {
+ return intval($extra->battery);
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * 获取设备最新登录微信的 wechatId
+ *
+ * @param int $deviceId
+ * @return string|null
+ * @throws \Exception
+ */
+ protected function getDeviceLatestWechatLogin(int $deviceId): ?string
+ {
+ return DeviceWechatLoginModel::where(
+ [
+ 'companyId' => $this->getUserInfo('companyId'),
+ 'deviceId' => $deviceId,
+ 'alive' => DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE
+ ]
+ )
+ ->value('wechatId');
+ }
+
+ /**
+ * 获取设备绑定的客服信息
+ *
+ * @param int $deviceId
+ * @return array
+ * @throws \Exception
+ */
+ protected function getWechatCustomerInfo(int $deviceId): array
+ {
+ $curstomer = WechatCustomerModel::field('activity,friendShip')
+ ->where(
+ [
+ 'companyId' => $this->getUserInfo('companyId'),
+ 'wechatId' => $this->getDeviceLatestWechatLogin($deviceId)
+ ]
+ )
+ ->find();
+
+ return $curstomer ? [
+ 'lastUpdateTime' => $curstomer->activity->lastActivityTime ?? '',
+ 'thirtyDayMsgCount' => $curstomer->activity->totalMsgCount ?? 0,
+ 'totalFriend' => $curstomer->friendShip->totalFriend ?? 0,
+ ] : [
+ 'lastUpdateTime' => '',
+ 'thirtyDayMsgCount' => 0,
+ 'totalFriend' => 0,
+ ];
+ }
+
+ /**
+ * 获取设备详情
+ *
+ * @param int $id
+ * @return array
+ */
+ protected function getDeviceInfo(int $id): array
+ {
+ // 查询设备基础信息与关联的微信账号信息
+ $device = DeviceModel::alias('d')
+ ->field([
+ 'd.id', 'd.imei', 'd.memo', 'd.alive', 'd.extra'
+ ])
+ ->find($id);
+
+ if (empty($device)) {
+ throw new \Exception('设备不存在', 404);
+ }
+
+ $device->battery = $this->parseExtraForBattery($device->extra);
+
+ // 删除冗余字段
+ unset($device->extra);
+
+ return $device->toArray();
+ }
+
+ /**
+ * 获取设备详情
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ $id = $this->request->param('id/d');
+
+ if ($this->getUserInfo('isAdmin') != UserModel::ADMIN_STP) {
+ $this->checkUserDevicePermission($id);
+ }
+
+ return ResponseHelper::success(
+ $this->getDeviceInfo($id) + $this->getWechatCustomerInfo($id)
+ );
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+
+
+ public function isUpdataWechat()
+ {
+ $id = $this->request->param('id/d');
+ $companyId = $this->getUserInfo('companyId');
+ $newWechat = DeviceWechatLoginModel::alias('a')
+ ->field('b.*')
+ ->join('wechat_account b', 'a.wechatId = b.wechatId')
+ ->where(['a.deviceId' => $id,'a.isTips' => 0,'a.companyId' => $companyId])
+ ->order('a.id', 'desc')
+ ->find();
+ if (empty($newWechat)){
+ return ResponseHelper::success('','该设备绑定的微信无需迁移',201);
+ }
+
+ $oldWechat = DeviceWechatLoginModel::alias('a')
+ ->field('b.*')
+ ->join('wechat_account b', 'a.wechatId = b.wechatId')
+ ->where(['a.companyId' => $companyId])
+ ->where('a.deviceId' ,'<>', $id)
+ ->order('a.id', 'desc')
+ ->find();
+ if (empty($oldWechat)){
+ return ResponseHelper::success('','该设备绑定的微信无需迁移',201);
+ }else{
+ DeviceWechatLoginModel::where(['deviceId' => $id,'isTips' => 0,'companyId' => $companyId])->update(['isTips' => 1]);;
+ return ResponseHelper::success(['newWechat' => $newWechat,'oldWechat' => $oldWechat]);
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/device/GetDeviceHandleLogsV1Controller.php b/application/cunkebao/controller/device/GetDeviceHandleLogsV1Controller.php
new file mode 100644
index 0000000..ccd031c
--- /dev/null
+++ b/application/cunkebao/controller/device/GetDeviceHandleLogsV1Controller.php
@@ -0,0 +1,82 @@
+ $deviceId,
+ 'userId' => $this->getUserInfo('id'),
+ 'companyId' => $this->getUserInfo('companyId')
+ ];
+
+ $hasPermission = DeviceUserModel::where($where)->count() > 0;
+
+ if (!$hasPermission) {
+ throw new \Exception('您没有权限查看该设备', 403);
+ }
+ }
+
+ /**
+ * 查询设备操作记录,并关联用户表获取操作人信息
+ *
+ * @param int $deviceId
+ * @return \think\Paginator
+ */
+ protected function getHandleLogs(int $deviceId): \think\Paginator
+ {
+ return DeviceHandleLog::alias('l')
+ ->field([
+ 'l.id', 'l.content', 'l.createTime',
+ 'u.username'
+ ])
+ ->leftJoin('users u', 'l.userId = u.id')
+ ->where('l.deviceId', $deviceId)
+ ->order('l.createTime desc')
+ ->paginate($this->request->param('limit/d', 10), false, ['page' => $this->request->param('page/d', 1)]);
+ }
+
+ /**
+ * 获取设备操作记录
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ $deviceId = $this->request->param('id/d');
+
+ if ($this->getUserInfo('isAdmin') != UserModel::ADMIN_STP) {
+ $this->checkUserDevicePermission($deviceId);
+ }
+
+ $logs = $this->getHandleLogs($deviceId);
+
+ return ResponseHelper::success(
+ [
+ 'total' => $logs->total(),
+ 'list' => $logs->items()
+ ]
+ );
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/device/GetDeviceListV1Controller.php b/application/cunkebao/controller/device/GetDeviceListV1Controller.php
new file mode 100644
index 0000000..f55310f
--- /dev/null
+++ b/application/cunkebao/controller/device/GetDeviceListV1Controller.php
@@ -0,0 +1,186 @@
+request->param('keyword'))) {
+ $where[] = ['exp', "d.imei LIKE '%{$keyword}%' OR d.memo LIKE '%{$keyword}%'"];
+ }
+
+ // 设备在线状态
+ if (is_numeric($alive = $this->request->param('alive'))) {
+ $where['d.alive'] = $alive;
+ }
+
+ $where['d.companyId'] = $this->getUserInfo('companyId');
+
+ return array_merge($params, $where);
+ }
+
+ /**
+ * 获取指定用户的所有设备ID
+ *
+ * @return array
+ */
+ protected function makeDeviceIdsWhere(): array
+ {
+ $deviceIds = DeviceUserModel::where(
+ [
+ 'userId' => $this->getUserInfo('id'),
+ 'companyId' => $this->getUserInfo('companyId')
+ ]
+ )
+ ->column('deviceId');
+
+ if (empty($deviceIds)) {
+ throw new \Exception('请联系管理员绑定设备', 403);
+ }
+
+ $where['d.id'] = array('in', $deviceIds);
+
+ return $where;
+ }
+
+ /**
+ * 获取设备列表
+ *
+ * @param array $where 查询条件
+ * @return \think\Paginator 分页对象
+ */
+ protected function getDeviceList(array $where): \think\Paginator
+ {
+
+ $companyId = $this->getUserInfo('companyId');
+ $query = DeviceModel::alias('d')
+ ->field([
+ 'd.id', 'd.imei', 'd.memo', 'd.alive',
+ 'l.wechatId',
+ 'a.nickname', 'a.alias', 'a.avatar', '0 totalFriend'
+ ])
+ ->join('(SELECT MAX(id) AS id, deviceId FROM ck_device_wechat_login WHERE companyId='.$companyId.' GROUP BY deviceId) dwl_max','dwl_max.deviceId = d.id')
+ ->join('device_wechat_login l','l.id = dwl_max.id')
+ ->join('wechat_account a', 'l.wechatId = a.wechatId')
+ ->order('d.id desc');
+
+ foreach ($where as $key => $value) {
+ if (is_numeric($key) && is_array($value) && isset($value[0]) && $value[0] === 'exp') {
+ $query->whereExp('', $value[1]);
+ continue;
+ }
+
+ $query->where($key, $value);
+ }
+
+ return $query->paginate($this->request->param('limit/d', 10), false, ['page' => $this->request->param('page/d', 1)]);
+ }
+
+ /**
+ * 获取设备最新登录微信的 wechatId
+ *
+ * @param int $deviceId
+ * @return string|null
+ * @throws \Exception
+ */
+ protected function getDeviceLatestWechatLogin(int $deviceId): ?string
+ {
+ return DeviceWechatLoginModel::where(
+ [
+ 'companyId' => $this->getUserInfo('companyId'),
+ 'deviceId' => $deviceId,
+ 'alive' => DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE
+ ]
+ )
+ ->value('wechatId');
+ }
+
+ /**
+ * 获取设备绑定的客服信息
+ *
+ * @param string $wechatId
+ * @return int
+ * @throws \Exception
+ */
+ protected function getWechatCustomerInfo(string $wechatId): int
+ {
+ $curstomer = WechatCustomerModel::field('friendShip')
+ ->where(
+ [
+ //'companyId' => $this->getUserInfo('companyId'),
+ 'wechatId' => $wechatId
+ ]
+ )
+ ->find();
+
+ return $curstomer->friendShip->totalFriend ?? 0;
+ }
+
+ /**
+ * 统计微信好友
+ *
+ * @param \think\Paginator $list
+ * @return array
+ */
+ protected function countFriend(\think\Paginator $list): array
+ {
+ $resultSets = [];
+
+ foreach ($list->items() as $item) {
+ $wechatId = $this->getDeviceLatestWechatLogin($item->id);
+
+ $item->totalFriend = $wechatId ? $this->getWechatCustomerInfo($wechatId) : 0;
+
+ array_push($resultSets, $item->toArray());
+ }
+
+ return $resultSets;
+ }
+
+ /**
+ * 获取设备列表
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ if ($this->getUserInfo('isAdmin') == UserModel::ADMIN_STP) {
+ $where = $this->makeWhere();
+ $result = $this->getDeviceList($where);
+ }else {
+ //$where = $this->makeWhere( $this->makeDeviceIdsWhere() );
+ $where = $this->makeWhere();
+ $result = $this->getDeviceList($where);
+ }
+
+ return ResponseHelper::success(
+ [
+ 'list' => $this->countFriend($result),
+ 'total' => $result->total(),
+ ]
+ );
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/device/GetDeviceTaskConfigV1Controller.php b/application/cunkebao/controller/device/GetDeviceTaskConfigV1Controller.php
new file mode 100644
index 0000000..5cacb6a
--- /dev/null
+++ b/application/cunkebao/controller/device/GetDeviceTaskConfigV1Controller.php
@@ -0,0 +1,85 @@
+ $deviceId,
+ 'userId' => $this->getUserInfo('id'),
+ 'companyId' => $this->getUserInfo('companyId')
+ ]
+ )
+ ->count() > 0;
+
+ if (!$hasPermission) {
+ throw new \Exception('您没有权限查看该设备', 403);
+ }
+ }
+
+ /**
+ * 解析taskConfig字段获取功能开关
+ *
+ * @param int $deviceId
+ * @return int[]
+ * @throws \Exception
+ */
+ protected function getTaskConfig(int $deviceId): array
+ {
+ $conf = DeviceTaskconfModel::alias('c')
+ ->field([
+ 'c.autoAddFriend', 'c.autoReply', 'c.momentsSync', 'c.aiChat'
+ ])
+ ->where(
+ [
+ 'companyId' => $this->getUserInfo('companyId'),
+ 'deviceId' => $deviceId
+ ]
+ )
+ ->find();
+
+ // 未配置时赋予默认关闭的状态
+ return !is_null($conf) ? $conf->toArray() : ArrHelper::getValue('autoAddFriend,autoReply,momentsSync,aiChat', [], 0);
+ }
+
+ /**
+ * 获取设备详情
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ $id = $this->request->param('id/d');
+
+ if ($this->getUserInfo('isAdmin') != UserModel::ADMIN_STP) {
+ $this->checkUserDevicePermission($id);
+ }
+
+ return ResponseHelper::success(
+ $this->getTaskConfig($id)
+ );
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/device/PostAddDeviceV1Controller.php b/application/cunkebao/controller/device/PostAddDeviceV1Controller.php
new file mode 100644
index 0000000..5a8477c
--- /dev/null
+++ b/application/cunkebao/controller/device/PostAddDeviceV1Controller.php
@@ -0,0 +1,20 @@
+getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/device/UpdateDeviceTaskConfigV1Controller.php b/application/cunkebao/controller/device/UpdateDeviceTaskConfigV1Controller.php
new file mode 100644
index 0000000..97c326a
--- /dev/null
+++ b/application/cunkebao/controller/device/UpdateDeviceTaskConfigV1Controller.php
@@ -0,0 +1,140 @@
+ $deviceId,
+ 'companyId' => $this->getUserInfo('companyId'),
+ ];
+
+ $device = DeviceModel::find($where);
+
+ if (!$device) {
+ throw new \Exception('设备不存在或已删除', 404);
+ }
+ }
+
+ /**
+ * 检查用户是否有权限操作指定设备
+ *
+ * @param int $deviceId
+ * @return void
+ */
+ protected function checkUserDevicePermission(int $deviceId): void
+ {
+ $where = [
+ 'deviceId' => $deviceId,
+ 'userId' => $this->getUserInfo('id'),
+ 'companyId' => $this->getUserInfo('companyId')
+ ];
+
+ $hasPermission = DeviceUserModel::where($where)->count() > 0;
+
+ if (!$hasPermission) {
+ throw new \Exception('您没有权限操作该设备', 403);
+ }
+ }
+
+ /**
+ * 添加设备操作日志
+ *
+ * @param int $deviceId
+ * @return void
+ * @throws \Exception
+ */
+ protected function addHandleLog(int $deviceId): void
+ {
+ $data = $this->request->post();
+ $content = null;
+
+ if (isset($data['autoAddFriend']))/**/ $content = $data['autoAddFriend'] ? '开启自动添加好友' : '关闭自动添加好友';
+ if (isset($data['autoReply']))/* */ $content = $data['autoReply'] ? '开启自动回复' : '关闭自动回复';
+ if (isset($data['momentsSync']))/* */ $content = $data['momentsSync'] ? '开启朋友圈同步' : '关闭朋友圈同步';
+ if (isset($data['aiChat']))/* */ $content = $data['aiChat'] ? '开启AI会话' : '关闭AI会话';
+
+ if (empty($content)) {
+ throw new \Exception('参数错误', 400);
+ }
+
+ DeviceHandleLogModel::addLog(
+ [
+ 'deviceId' => $deviceId,
+ 'content' => $content,
+ 'userId' => $this->getUserInfo('id'),
+ 'companyId' => $this->getUserInfo('companyId'),
+ ]
+ );
+ }
+
+ /**
+ * 更新设备taskConfig字段
+ *
+ * @param int $deviceId
+ * @return void
+ */
+ protected function setTaskconf(int $deviceId): void
+ {
+ $data = $this->request->post();
+ $conf = DeviceTaskconf::where('deviceId', $deviceId)->find();
+
+ if ($conf) {
+ DeviceTaskconf::where('deviceId', $deviceId)->update($data);
+ } else {
+ DeviceTaskconf::create(array_merge($data, [
+ 'companyId' => $this->getUserInfo('companyId'),
+ ]));
+ }
+ }
+
+ /**
+ * 更新设备任务配置
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ $id = $this->request->param('deviceId/d');
+
+ $this->checkDeviceExists($id);
+
+ if ($this->getUserInfo('isAdmin') != UserModel::ADMIN_STP) {
+ $this->checkUserDevicePermission($id);
+ }
+
+ try {
+ Db::startTrans();
+
+ $this->addHandleLog($id);
+ $this->setTaskconf($id);
+
+ Db::commit();
+
+ return ResponseHelper::success();
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/distribution/ChannelController.php b/application/cunkebao/controller/distribution/ChannelController.php
new file mode 100644
index 0000000..6df58d1
--- /dev/null
+++ b/application/cunkebao/controller/distribution/ChannelController.php
@@ -0,0 +1,1454 @@
+request->param('name', '');
+ $phone = $this->request->param('phone', '');
+ $wechatId = $this->request->param('wechatId', '');
+ $remarks = $this->request->param('remarks', '');
+ $createType = $this->request->param('createType', DistributionChannel::CREATE_TYPE_MANUAL); // 默认为手动创建
+
+ $companyId = $this->getUserInfo('companyId');
+ $userId = $this->getUserInfo('id');
+
+ // 参数验证
+ if (empty($name)) {
+ return ResponseHelper::error('渠道名称不能为空', 400);
+ }
+
+ // 验证渠道名称长度
+ if (mb_strlen($name) > 50) {
+ return ResponseHelper::error('渠道名称长度不能超过50个字符', 400);
+ }
+
+ // 验证手机号格式(如果提供)
+ if (!empty($phone)) {
+ if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
+ return ResponseHelper::error('手机号格式不正确,请输入11位数字且以1开头', 400);
+ }
+
+ // 检查手机号是否已存在(排除已删除的渠道)
+ $existChannel = Db::name('distribution_channel')
+ ->where([
+ ['companyId', '=', $companyId],
+ ['phone', '=', $phone],
+ ['deleteTime', '=', 0]
+ ])
+ ->find();
+
+ if ($existChannel) {
+ return ResponseHelper::error('手机号已被使用', 400);
+ }
+ }
+
+ // 验证微信号长度(如果提供)
+ if (!empty($wechatId) && mb_strlen($wechatId) > 50) {
+ return ResponseHelper::error('微信号长度不能超过50个字符', 400);
+ }
+
+ // 验证备注长度(如果提供)
+ if (!empty($remarks) && mb_strlen($remarks) > 200) {
+ return ResponseHelper::error('备注信息长度不能超过200个字符', 400);
+ }
+
+ // 验证创建类型
+ if (!in_array($createType, [DistributionChannel::CREATE_TYPE_MANUAL, DistributionChannel::CREATE_TYPE_AUTO])) {
+ $createType = DistributionChannel::CREATE_TYPE_MANUAL;
+ }
+
+ // 生成渠道编码
+ $code = DistributionChannel::generateChannelCode();
+
+ // 准备插入数据
+ $data = [
+ 'companyId' => $companyId,
+ 'userId' => $userId,
+ 'name' => $name,
+ 'code' => $code,
+ 'phone' => $phone ?: '',
+ 'password' => md5('123456'), // 默认密码123456,MD5加密
+ 'wechatId' => $wechatId ?: '',
+ 'remarks' => $remarks ?: '',
+ 'createType' => $createType,
+ 'status' => DistributionChannel::STATUS_ENABLED,
+ 'totalCustomers' => 0,
+ 'todayCustomers' => 0,
+ 'totalFriends' => 0,
+ 'todayFriends' => 0,
+ 'withdrawableAmount' => 0, // 以分为单位存储
+ 'createTime' => time(),
+ 'updateTime' => time(),
+ ];
+
+ // 插入数据库
+ $channelId = Db::name('distribution_channel')->insertGetId($data);
+
+ if (!$channelId) {
+ return ResponseHelper::error('创建渠道失败', 500);
+ }
+
+ // 获取创建的数据
+ $channel = Db::name('distribution_channel')->where('id', $channelId)->find();
+
+ // 格式化返回数据
+ $result = [
+ 'id' => $channel['id'],
+ 'name' => $channel['name'],
+ 'code' => $channel['code'],
+ 'phone' => $channel['phone'] ?: '',
+ 'wechatId' => $channel['wechatId'] ?: '',
+ 'companyId' => (int)$companyId, // 返回companyId,方便小程序自动跳转
+ 'userId' => (int)($channel['userId'] ?? 0),
+ 'createType' => $channel['createType'],
+ 'status' => $channel['status'],
+ 'totalCustomers' => (int)$channel['totalCustomers'],
+ 'todayCustomers' => (int)$channel['todayCustomers'],
+ 'totalFriends' => (int)$channel['totalFriends'],
+ 'todayFriends' => (int)$channel['todayFriends'],
+ 'withdrawableAmount' => round(($channel['withdrawableAmount'] ?? 0) / 100, 2), // 分转元,保留2位小数
+ 'createTime' => !empty($channel['createTime']) ? date('Y-m-d H:i:s', $channel['createTime']) : date('Y-m-d H:i:s'),
+ ];
+
+ // 返回符合需求的格式(包含success字段)
+ return json([
+ 'code' => 200,
+ 'success' => true,
+ 'msg' => '创建成功',
+ 'data' => $result
+ ]);
+
+ } catch (Exception $e) {
+ return ResponseHelper::error('创建渠道失败:' . $e->getMessage(), $e->getCode() ?: 500);
+ }
+ }
+
+ /**
+ * 获取渠道列表
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ // 获取参数
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 20);
+ $keyword = $this->request->param('keyword', '');
+ $status = $this->request->param('status', 'all'); // all、enabled、disabled
+
+ $companyId = $this->getUserInfo('companyId');
+
+ // 参数验证
+ $page = max(1, intval($page));
+ $limit = max(1, min(100, intval($limit))); // 限制最大100
+
+ // 验证状态参数
+ $validStatuses = ['all', DistributionChannel::STATUS_ENABLED, DistributionChannel::STATUS_DISABLED];
+ if (!in_array($status, $validStatuses)) {
+ $status = 'all';
+ }
+
+ // 构建查询条件
+ $where = [];
+ $where[] = ['companyId', '=', $companyId];
+ $where[] = ['deleteTime', '=', 0];
+
+ // 如果不是管理员,只能查看自己创建的数据
+ if (!$this->getUserInfo('isAdmin')) {
+ $where[] = ['userId', '=', $this->getUserInfo('id')];
+ }
+
+ // 状态筛选
+ if ($status !== 'all') {
+ $where[] = ['status', '=', $status];
+ }
+
+ // 关键词搜索(模糊匹配 name、code、phone、wechatId)
+ if (!empty($keyword)) {
+ $keyword = trim($keyword);
+ // 使用 | 分隔字段表示OR关系(ThinkPHP语法)
+ $where[] = ['name|code|phone|wechatId', 'like', '%' . $keyword . '%'];
+ }
+
+ // 查询总数
+ $total = Db::name('distribution_channel')
+ ->where($where)
+ ->count();
+
+ // 查询列表(按创建时间倒序)
+ $list = Db::name('distribution_channel')
+ ->where($where)
+ ->order('createTime DESC')
+ ->page($page, $limit)
+ ->select();
+
+ // 格式化数据
+ $formattedList = [];
+ foreach ($list as $item) {
+ $formattedItem = [
+ 'id' => (int)$item['id'],
+ 'name' => $item['name'] ?? '',
+ 'code' => $item['code'] ?? '',
+ 'phone' => !empty($item['phone']) ? $item['phone'] : null,
+ 'wechatId' => !empty($item['wechatId']) ? $item['wechatId'] : null,
+ 'companyId' => (int)($item['companyId'] ?? 0),
+ 'userId' => (int)($item['userId'] ?? 0),
+ 'createType' => $item['createType'] ?? 'manual',
+ 'status' => $item['status'] ?? 'enabled',
+ 'totalCustomers' => (int)($item['totalCustomers'] ?? 0),
+ 'todayCustomers' => (int)($item['todayCustomers'] ?? 0),
+ 'totalFriends' => (int)($item['totalFriends'] ?? 0),
+ 'todayFriends' => (int)($item['todayFriends'] ?? 0),
+ 'withdrawableAmount' => round(($item['withdrawableAmount'] ?? 0) / 100, 2), // 分转元,保留2位小数
+ 'createTime' => !empty($item['createTime']) ? date('Y-m-d H:i:s', $item['createTime']) : date('Y-m-d H:i:s'),
+ ];
+ $formattedList[] = $formattedItem;
+ }
+
+ // 返回结果
+ return json([
+ 'code' => 200,
+ 'success' => true,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $formattedList,
+ 'total' => (int)$total
+ ]
+ ]);
+
+ } catch (Exception $e) {
+ return json([
+ 'code' => $e->getCode() ?: 500,
+ 'success' => false,
+ 'msg' => '获取渠道列表失败:' . $e->getMessage(),
+ 'data' => null
+ ]);
+ }
+ }
+
+ /**
+ * 编辑渠道
+ * @return \think\response\Json
+ */
+ public function update()
+ {
+ try {
+ // 获取参数
+ $id = $this->request->param('id', 0);
+ $name = $this->request->param('name', '');
+ $phone = $this->request->param('phone', '');
+ $wechatId = $this->request->param('wechatId', '');
+ $remarks = $this->request->param('remarks', '');
+
+ $companyId = $this->getUserInfo('companyId');
+
+ // 参数验证
+ if (empty($id)) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '渠道ID不能为空',
+ 'data' => null
+ ]);
+ }
+
+ // 检查渠道是否存在且属于当前公司
+ $channel = Db::name('distribution_channel')
+ ->where(['id' => $id, 'companyId' => $companyId, 'deleteTime' => 0])
+ ->find();
+
+ if (!$channel) {
+ return json([
+ 'code' => 404,
+ 'success' => false,
+ 'msg' => '渠道不存在或没有权限',
+ 'data' => null
+ ]);
+ }
+
+ // 准备更新数据
+ $updateData = [];
+ $updateData['updateTime'] = time();
+
+ // 更新渠道名称
+ if (!empty($name)) {
+ if (mb_strlen($name) > 50) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '渠道名称长度不能超过50个字符',
+ 'data' => null
+ ]);
+ }
+ $updateData['name'] = $name;
+ }
+
+ // 更新手机号
+ if (isset($phone)) { // 允许设置为空
+ if (!empty($phone)) {
+ if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '手机号格式不正确,请输入11位数字且以1开头',
+ 'data' => null
+ ]);
+ }
+
+ // 检查手机号是否已被其他渠道使用(排除当前渠道和已删除的渠道)
+ $existChannel = Db::name('distribution_channel')
+ ->where([
+ ['companyId', '=', $companyId],
+ ['phone', '=', $phone],
+ ['id', '<>', $id],
+ ['deleteTime', '=', 0]
+ ])
+ ->find();
+
+ if ($existChannel) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '手机号已被其他渠道使用',
+ 'data' => null
+ ]);
+ }
+ }
+ $updateData['phone'] = $phone ?: '';
+ }
+
+ // 更新微信号
+ if (isset($wechatId)) { // 允许设置为空
+ if (!empty($wechatId) && mb_strlen($wechatId) > 50) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '微信号长度不能超过50个字符',
+ 'data' => null
+ ]);
+ }
+ $updateData['wechatId'] = $wechatId ?: '';
+ }
+
+ // 更新备注
+ if (isset($remarks)) { // 允许设置为空
+ if (!empty($remarks) && mb_strlen($remarks) > 200) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '备注信息长度不能超过200个字符',
+ 'data' => null
+ ]);
+ }
+ $updateData['remarks'] = $remarks ?: '';
+ }
+
+ // 如果没有要更新的数据
+ if (count($updateData) <= 1) { // 只有updateTime
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '没有要更新的数据',
+ 'data' => null
+ ]);
+ }
+
+ // 更新数据库
+ $result = Db::name('distribution_channel')
+ ->where(['id' => $id, 'companyId' => $companyId])
+ ->update($updateData);
+
+ if ($result === false) {
+ return json([
+ 'code' => 500,
+ 'success' => false,
+ 'msg' => '更新渠道失败',
+ 'data' => null
+ ]);
+ }
+
+ // 获取更新后的数据
+ $updatedChannel = Db::name('distribution_channel')
+ ->where('id', $id)
+ ->find();
+
+ // 格式化返回数据
+ $resultData = [
+ 'id' => (int)$updatedChannel['id'],
+ 'name' => $updatedChannel['name'],
+ 'code' => $updatedChannel['code'],
+ 'phone' => !empty($updatedChannel['phone']) ? $updatedChannel['phone'] : null,
+ 'wechatId' => !empty($updatedChannel['wechatId']) ? $updatedChannel['wechatId'] : null,
+ 'companyId' => (int)($updatedChannel['companyId'] ?? 0),
+ 'userId' => (int)($updatedChannel['userId'] ?? 0),
+ 'createType' => $updatedChannel['createType'],
+ 'status' => $updatedChannel['status'],
+ 'totalCustomers' => (int)$updatedChannel['totalCustomers'],
+ 'todayCustomers' => (int)$updatedChannel['todayCustomers'],
+ 'totalFriends' => (int)$updatedChannel['totalFriends'],
+ 'todayFriends' => (int)$updatedChannel['todayFriends'],
+ 'withdrawableAmount' => round(($updatedChannel['withdrawableAmount'] ?? 0) / 100, 2), // 分转元,保留2位小数
+ 'createTime' => !empty($updatedChannel['createTime']) ? date('Y-m-d H:i:s', $updatedChannel['createTime']) : date('Y-m-d H:i:s'),
+ ];
+
+ return json([
+ 'code' => 200,
+ 'success' => true,
+ 'msg' => '更新成功',
+ 'data' => $resultData
+ ]);
+
+ } catch (Exception $e) {
+ return json([
+ 'code' => $e->getCode() ?: 500,
+ 'success' => false,
+ 'msg' => '更新渠道失败:' . $e->getMessage(),
+ 'data' => null
+ ]);
+ }
+ }
+
+ /**
+ * 删除渠道(软删除)
+ * @return \think\response\Json
+ */
+ public function delete()
+ {
+ try {
+ // 获取参数
+ $id = $this->request->param('id', 0);
+
+ $companyId = $this->getUserInfo('companyId');
+
+ // 参数验证
+ if (empty($id)) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '渠道ID不能为空',
+ 'data' => null
+ ]);
+ }
+
+ // 检查渠道是否存在且属于当前公司
+ $channel = Db::name('distribution_channel')
+ ->where(['id' => $id, 'companyId' => $companyId, 'deleteTime' => 0])
+ ->find();
+
+ if (!$channel) {
+ return json([
+ 'code' => 404,
+ 'success' => false,
+ 'msg' => '渠道不存在或没有权限',
+ 'data' => null
+ ]);
+ }
+
+ // 软删除
+ $result = Db::name('distribution_channel')
+ ->where(['id' => $id, 'companyId' => $companyId])
+ ->update([
+ 'deleteTime' => time(),
+ 'updateTime' => time()
+ ]);
+
+ if ($result === false) {
+ return json([
+ 'code' => 500,
+ 'success' => false,
+ 'msg' => '删除渠道失败',
+ 'data' => null
+ ]);
+ }
+
+ return json([
+ 'code' => 200,
+ 'success' => true,
+ 'msg' => '删除成功',
+ 'data' => null
+ ]);
+
+ } catch (Exception $e) {
+ return json([
+ 'code' => $e->getCode() ?: 500,
+ 'success' => false,
+ 'msg' => '删除渠道失败:' . $e->getMessage(),
+ 'data' => null
+ ]);
+ }
+ }
+
+ /**
+ * 禁用/启用渠道
+ * @return \think\response\Json
+ */
+ public function toggleStatus()
+ {
+ try {
+ // 获取参数
+ $id = $this->request->param('id', 0);
+ $status = $this->request->param('status', ''); // enabled 或 disabled
+
+ $companyId = $this->getUserInfo('companyId');
+
+ // 参数验证
+ if (empty($id)) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '渠道ID不能为空',
+ 'data' => null
+ ]);
+ }
+
+ if (!in_array($status, [DistributionChannel::STATUS_ENABLED, DistributionChannel::STATUS_DISABLED])) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '状态参数错误,必须为 enabled 或 disabled',
+ 'data' => null
+ ]);
+ }
+
+ // 检查渠道是否存在且属于当前公司
+ $channel = Db::name('distribution_channel')
+ ->where(['id' => $id, 'companyId' => $companyId, 'deleteTime' => 0])
+ ->find();
+
+ if (!$channel) {
+ return json([
+ 'code' => 404,
+ 'success' => false,
+ 'msg' => '渠道不存在或没有权限',
+ 'data' => null
+ ]);
+ }
+
+ // 如果状态相同,直接返回成功
+ if ($channel['status'] === $status) {
+ $msg = $status === DistributionChannel::STATUS_ENABLED ? '渠道已启用' : '渠道已禁用';
+ return json([
+ 'code' => 200,
+ 'success' => true,
+ 'msg' => $msg,
+ 'data' => null
+ ]);
+ }
+
+ // 更新状态
+ $result = Db::name('distribution_channel')
+ ->where(['id' => $id, 'companyId' => $companyId])
+ ->update([
+ 'status' => $status,
+ 'updateTime' => time()
+ ]);
+
+ if ($result === false) {
+ return json([
+ 'code' => 500,
+ 'success' => false,
+ 'msg' => '更新状态失败',
+ 'data' => null
+ ]);
+ }
+
+ $msg = $status === DistributionChannel::STATUS_ENABLED ? '启用成功' : '禁用成功';
+
+ return json([
+ 'code' => 200,
+ 'success' => true,
+ 'msg' => $msg,
+ 'data' => [
+ 'id' => (int)$id,
+ 'status' => $status
+ ]
+ ]);
+
+ } catch (Exception $e) {
+ return json([
+ 'code' => $e->getCode() ?: 500,
+ 'success' => false,
+ 'msg' => '更新状态失败:' . $e->getMessage(),
+ 'data' => null
+ ]);
+ }
+ }
+
+ /**
+ * 获取渠道统计数据
+ * @return \think\response\Json
+ */
+ public function statistics()
+ {
+ try {
+ $companyId = $this->getUserInfo('companyId');
+
+ // 获取今日开始和结束时间戳
+ $todayStart = strtotime(date('Y-m-d 00:00:00'));
+ $todayEnd = strtotime(date('Y-m-d 23:59:59'));
+
+ // 构建基础查询条件
+ $baseWhere = [
+ ['companyId', '=', $companyId],
+ ['deleteTime', '=', 0]
+ ];
+
+ // 如果不是管理员,只能查看自己创建的数据
+ if (!$this->getUserInfo('isAdmin')) {
+ $baseWhere[] = ['userId', '=', $this->getUserInfo('id')];
+ }
+
+ // 1. 总渠道数
+ $totalChannels = Db::name('distribution_channel')
+ ->where($baseWhere)
+ ->count();
+
+ // 2. 今日新增渠道数
+ $todayChannels = Db::name('distribution_channel')
+ ->where($baseWhere)
+ ->where('createTime', 'between', [$todayStart, $todayEnd])
+ ->count();
+
+ // 3. 统计所有渠道的获客数和加好友数(使用聚合函数)
+ $statistics = Db::name('distribution_channel')
+ ->where($baseWhere)
+ ->field([
+ 'SUM(totalCustomers) as totalCustomers',
+ 'SUM(todayCustomers) as todayCustomers',
+ 'SUM(totalFriends) as totalFriends',
+ 'SUM(todayFriends) as todayFriends'
+ ])
+ ->find();
+
+ // 格式化统计数据
+ $data = [
+ 'totalChannels' => (int)$totalChannels,
+ 'todayChannels' => (int)$todayChannels,
+ 'totalCustomers' => (int)($statistics['totalCustomers'] ?? 0),
+ 'todayCustomers' => (int)($statistics['todayCustomers'] ?? 0),
+ 'totalFriends' => (int)($statistics['totalFriends'] ?? 0),
+ 'todayFriends' => (int)($statistics['todayFriends'] ?? 0),
+ ];
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => $data
+ ]);
+
+ } catch (Exception $e) {
+ return json([
+ 'code' => $e->getCode() ?: 500,
+ 'msg' => '获取统计数据失败:' . $e->getMessage(),
+ 'data' => null
+ ]);
+ }
+ }
+
+ /**
+ * 获取渠道收益统计(全局统计)
+ * @return \think\response\Json
+ */
+ public function revenueStatistics()
+ {
+ try {
+ $companyId = $this->getUserInfo('companyId');
+
+ // 构建基础查询条件
+ $baseWhere = [
+ ['companyId', '=', $companyId]
+ ];
+
+ // 如果不是管理员,只能查看自己创建的提现申请
+ if (!$this->getUserInfo('isAdmin')) {
+ $baseWhere[] = ['userId', '=', $this->getUserInfo('id')];
+ }
+
+ // 1. 总支出:所有已打款的提现申请金额总和(状态为paid)
+ $totalExpenditure = Db::name('distribution_withdrawal')
+ ->where($baseWhere)
+ ->where('status', DistributionWithdrawal::STATUS_PAID)
+ ->sum('amount');
+ $totalExpenditure = intval($totalExpenditure ?? 0);
+
+ // 2. 已提现:所有已打款的提现申请金额总和(状态为paid)
+ $withdrawn = $totalExpenditure; // 已提现 = 总支出
+
+ // 3. 待审核:所有待审核的提现申请金额总和(状态为pending)
+ $pendingReview = Db::name('distribution_withdrawal')
+ ->where($baseWhere)
+ ->where('status', DistributionWithdrawal::STATUS_PENDING)
+ ->sum('amount');
+ $pendingReview = intval($pendingReview ?? 0);
+
+ // 格式化返回数据(分转元)
+ $data = [
+ 'totalExpenditure' => round($totalExpenditure / 100, 2), // 总支出(元)
+ 'withdrawn' => round($withdrawn / 100, 2), // 已提现(元)
+ 'pendingReview' => round($pendingReview / 100, 2), // 待审核(元)
+ ];
+
+ return json([
+ 'code' => 200,
+ 'success' => true,
+ 'msg' => '获取成功',
+ 'data' => $data
+ ]);
+
+ } catch (Exception $e) {
+ return json([
+ 'code' => $e->getCode() ?: 500,
+ 'success' => false,
+ 'msg' => '获取收益统计失败:' . $e->getMessage(),
+ 'data' => null
+ ]);
+ }
+ }
+
+ /**
+ * 获取渠道收益明细列表(多个渠道的统计)
+ * @return \think\response\Json
+ */
+ public function revenueDetail()
+ {
+ try {
+ // 获取参数
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 20);
+ $keyword = $this->request->param('keyword', '');
+
+ $companyId = $this->getUserInfo('companyId');
+
+ // 参数验证
+ $page = max(1, intval($page));
+ $limit = max(1, min(100, intval($limit))); // 限制最大100
+
+ // 构建查询条件
+ $where = [];
+ $where[] = ['companyId', '=', $companyId];
+ $where[] = ['deleteTime', '=', 0];
+
+ // 如果不是管理员,只能查看自己创建的数据
+ if (!$this->getUserInfo('isAdmin')) {
+ $where[] = ['userId', '=', $this->getUserInfo('id')];
+ }
+
+ // 关键词搜索(模糊匹配 name、code)
+ if (!empty($keyword)) {
+ $keyword = trim($keyword);
+ $where[] = ['name|code', 'like', '%' . $keyword . '%'];
+ }
+
+ // 查询总数
+ $total = Db::name('distribution_channel')
+ ->where($where)
+ ->count();
+
+ // 查询渠道列表(按创建时间倒序)
+ $channels = Db::name('distribution_channel')
+ ->where($where)
+ ->order('createTime DESC')
+ ->page($page, $limit)
+ ->select();
+
+ // 批量获取所有渠道的提现统计数据(提高性能)
+ $channelIds = array_column($channels, 'id');
+ $withdrawalStats = [];
+ if (!empty($channelIds)) {
+ // 构建提现查询条件
+ $withdrawalWhere = [
+ ['companyId', '=', $companyId],
+ ['channelId', 'in', $channelIds]
+ ];
+
+ // 如果不是管理员,只能查看自己创建的提现申请
+ if (!$this->getUserInfo('isAdmin')) {
+ $withdrawalWhere[] = ['userId', '=', $this->getUserInfo('id')];
+ }
+
+ // 按渠道ID和状态分组统计提现金额
+ $stats = Db::name('distribution_withdrawal')
+ ->where($withdrawalWhere)
+ ->field([
+ 'channelId',
+ 'status',
+ 'SUM(amount) as totalAmount'
+ ])
+ ->group('channelId, status')
+ ->select();
+
+ // 组织统计数据
+ foreach ($stats as $stat) {
+ $cid = $stat['channelId'];
+ if (!isset($withdrawalStats[$cid])) {
+ $withdrawalStats[$cid] = [
+ 'totalRevenue' => 0, // 总收益(不包括驳回的)
+ 'withdrawn' => 0, // 已打款(paid)
+ 'pendingReview' => 0 // 待审核(pending)
+ ];
+ }
+ $amount = intval($stat['totalAmount'] ?? 0);
+ $status = $stat['status'];
+
+ // totalRevenue 不包括驳回(rejected)状态的金额
+ if ($status !== DistributionWithdrawal::STATUS_REJECTED) {
+ $withdrawalStats[$cid]['totalRevenue'] += $amount;
+ }
+
+ if ($status === DistributionWithdrawal::STATUS_PAID) {
+ $withdrawalStats[$cid]['withdrawn'] += $amount;
+ } elseif ($status === DistributionWithdrawal::STATUS_PENDING) {
+ $withdrawalStats[$cid]['pendingReview'] += $amount;
+ }
+ }
+ }
+
+ // 格式化数据
+ $formattedList = [];
+ foreach ($channels as $channel) {
+ $channelId = $channel['id'];
+ $stats = $withdrawalStats[$channelId] ?? [
+ 'totalRevenue' => 0,
+ 'withdrawn' => 0,
+ 'pendingReview' => 0
+ ];
+
+ // 可提现金额:渠道的withdrawableAmount
+ $withdrawableAmount = intval($channel['withdrawableAmount'] ?? 0);
+
+ $formattedItem = [
+ 'channelId' => (string)$channelId,
+ 'channelName' => $channel['name'] ?? '',
+ 'channelCode' => $channel['code'] ?? '',
+ 'totalRevenue' => round($stats['totalRevenue'] / 100, 2), // 总收益(元)
+ 'withdrawable' => round($withdrawableAmount / 100, 2), // 可提现(元)
+ 'withdrawn' => round($stats['withdrawn'] / 100, 2), // 已提现(元)
+ 'pendingReview' => round($stats['pendingReview'] / 100, 2), // 待审核(元)
+ ];
+ $formattedList[] = $formattedItem;
+ }
+
+ // 返回结果
+ return json([
+ 'code' => 200,
+ 'success' => true,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $formattedList,
+ 'total' => (int)$total
+ ]
+ ]);
+
+ } catch (Exception $e) {
+ return json([
+ 'code' => $e->getCode() ?: 500,
+ 'success' => false,
+ 'msg' => '获取收益明细失败:' . $e->getMessage(),
+ 'data' => null
+ ]);
+ }
+ }
+
+ /**
+ * 生成渠道二维码(H5或小程序码)
+ * @return \think\response\Json
+ */
+ public function generateQrCode()
+ {
+ try {
+ // 获取参数
+ $type = $this->request->param('type', 'h5'); // h5 或 miniprogram
+
+ $companyId = $this->getUserInfo('companyId');
+ $userId = $this->getUserInfo('id');
+
+ // 参数验证
+ if (!in_array($type, ['h5', 'miniprogram'])) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '类型参数错误,必须为 h5 或 miniprogram',
+ 'data' => null
+ ]);
+ }
+
+ // 生成临时token(包含公司ID和用户ID,有效期24小时)
+ // 用户扫码后需要自己填写所有信息
+ $tokenData = [
+ 'companyId' => $companyId,
+ 'userId' => $userId,
+ 'expireTime' => time() + 86400 // 24小时后过期
+ ];
+ $token = base64_encode(json_encode($tokenData));
+
+ // 如果是小程序码,提前计算scene并存储映射关系到数据库
+ if ($type === 'miniprogram') {
+ $scene = substr(md5($token), 0, 32);
+
+ // 使用数据库存储映射关系(更可靠)
+ try {
+ Db::name('distribution_channel_scene_token')->insert([
+ 'scene' => $scene,
+ 'token' => $token,
+ 'companyId' => $companyId,
+ 'expireTime' => time() + 86400,
+ 'createTime' => time()
+ ]);
+ } catch (\Exception $e) {
+ // 如果表不存在,尝试创建表
+ $this->createSceneTokenTable();
+ // 重试一次
+ try {
+ Db::name('distribution_channel_scene_token')->insert([
+ 'scene' => $scene,
+ 'token' => $token,
+ 'companyId' => $companyId,
+ 'expireTime' => time() + 86400,
+ 'createTime' => time()
+ ]);
+ } catch (\Exception $e2) {
+ // 静默失败,不影响主流程
+ }
+ }
+
+ // 同时存储到缓存(双重保险)
+ $sceneCacheKey = 'channel_register_scene_' . $scene;
+ Cache::set($sceneCacheKey, $token, 86400);
+ }
+
+ if ($type === 'h5') {
+ // 生成H5二维码
+ // 获取H5页面URL(需要根据实际项目配置)
+ $h5BaseUrl = Env::get('rpc.H5_FORM_URL', 'https://h5.ckb.quwanzhi.com/#');
+ // 确保URL格式正确(去除末尾斜杠)
+ $h5BaseUrl = rtrim($h5BaseUrl, '/');
+ $h5Url = $h5BaseUrl . '/pages/channel/add?token=' . urlencode($token);
+
+ // 生成二维码
+ $qrCode = new QrCode($h5Url);
+ $qrCode->setSize(300);
+ $qrCode->setMargin(10);
+ $qrCode->setWriterByName('png');
+ $qrCode->setEncoding('UTF-8');
+ $qrCode->setErrorCorrectionLevel(ErrorCorrectionLevel::HIGH);
+
+ // 转换为base64
+ $qrCodeBase64 = 'data:image/png;base64,' . base64_encode($qrCode->writeString());
+
+ return json([
+ 'code' => 200,
+ 'success' => true,
+ 'msg' => '生成H5二维码成功',
+ 'data' => [
+ 'type' => 'h5',
+ 'qrCode' => $qrCodeBase64,
+ 'url' => $h5Url
+ ]
+ ]);
+
+ } else {
+ // 生成小程序码
+ try {
+ // 从环境变量获取小程序配置
+ $miniProgramConfig = [
+ 'app_id' => Env::get('weChat.appidMiniApp', 'wx789850448e26c91d'),
+ 'secret' => Env::get('weChat.secretMiniApp', 'd18f75b3a3623cb40da05648b08365a1'),
+ 'response_type' => 'array'
+ ];
+
+ $app = Factory::miniProgram($miniProgramConfig);
+
+ // scene参数长度限制为32位,使用token的MD5值
+ $scene = substr(md5($token), 0, 32);
+
+ // 再次确保映射关系已存储到数据库和缓存(双重保险)
+ $sceneCacheKey = 'channel_register_scene_' . $scene;
+ Cache::set($sceneCacheKey, $token, 86400);
+
+
+ // 调用接口生成小程序码
+ // 注意:page 必须是小程序里已经存在且发布过的页面路径
+ // 根据你的前端约定,改为和 H5 一致的添加渠道页面
+ $response = $app->app_code->getUnlimit($scene, [
+ 'page' => 'pages/channel/add', // 请确保小程序里存在该页面
+ 'width' => 430, // 二维码的宽度
+ ]);
+
+ // 成功时返回的是 StreamResponse,失败时通常返回数组(包含 errcode/errmsg)
+ if ($response instanceof \EasyWeChat\Kernel\Http\StreamResponse) {
+ $img = $response->getBody()->getContents();
+ $imgBase64 = 'data:image/png;base64,' . base64_encode($img);
+
+ return json([
+ 'code' => 200,
+ 'success' => true,
+ 'msg' => '生成小程序码成功',
+ 'data' => [
+ 'type' => 'miniprogram',
+ 'qrCode' => $imgBase64,
+ 'scene' => $scene, // 返回scene供小程序端使用
+ 'token' => $token // 返回token,小程序端可以通过scene查询
+ ]
+ ]);
+ }
+
+ // 如果不是流响应,而是数组(错误信息),则解析错误返回
+ if (is_array($response) && isset($response['errcode']) && $response['errcode'] != 0) {
+ $errMsg = isset($response['errmsg']) ? $response['errmsg'] : '微信接口返回错误';
+ return json([
+ 'code' => 500,
+ 'success' => false,
+ 'msg' => '生成小程序码失败:' . $errMsg,
+ 'data' => $response
+ ]);
+ }
+
+ // 其他未知格式
+ return json([
+ 'code' => 500,
+ 'success' => false,
+ 'msg' => '生成小程序码失败:响应格式错误',
+ 'data' => $response
+ ]);
+ } catch (\Exception $e) {
+ return json([
+ 'code' => 500,
+ 'success' => false,
+ 'msg' => '生成小程序码失败:' . $e->getMessage(),
+ 'data' => null
+ ]);
+ }
+ }
+
+ } catch (Exception $e) {
+ return json([
+ 'code' => $e->getCode() ?: 500,
+ 'success' => false,
+ 'msg' => '生成二维码失败:' . $e->getMessage(),
+ 'data' => null
+ ]);
+ }
+ }
+
+ /**
+ * 生成渠道登录二维码(H5或小程序码)
+ * 通用登录二维码,不绑定特定渠道,用户扫码后输入手机号和密码登录
+ * @return \think\response\Json
+ */
+ public function generateLoginQrCode()
+ {
+ try {
+ // 获取参数
+ $type = $this->request->param('type', 'h5'); // h5 或 miniprogram
+
+ $companyId = $this->getUserInfo('companyId');
+
+ // 参数验证
+ if (!in_array($type, ['h5', 'miniprogram'])) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '类型参数错误,必须为 h5 或 miniprogram',
+ 'data' => null
+ ]);
+ }
+
+ if ($type === 'h5') {
+ // 生成H5登录二维码
+ $h5BaseUrl = Env::get('rpc.H5_FORM_URL', 'https://h5.ckb.quwanzhi.com/#');
+ $h5BaseUrl = rtrim($h5BaseUrl, '/');
+ // H5登录页面路径,需要根据实际项目调整
+ // 登录入口只需要携带 companyId 参数
+ $h5Url = $h5BaseUrl . '/pages/channel/login?companyId=' . urlencode($companyId);
+
+ // 生成二维码
+ $qrCode = new QrCode($h5Url);
+ $qrCode->setSize(300);
+ $qrCode->setMargin(10);
+ $qrCode->setWriterByName('png');
+ $qrCode->setEncoding('UTF-8');
+ $qrCode->setErrorCorrectionLevel(ErrorCorrectionLevel::HIGH);
+
+ // 转换为base64
+ $qrCodeBase64 = 'data:image/png;base64,' . base64_encode($qrCode->writeString());
+
+ return json([
+ 'code' => 200,
+ 'success' => true,
+ 'msg' => '生成H5登录二维码成功',
+ 'data' => [
+ 'type' => 'h5',
+ 'qrCode' => $qrCodeBase64,
+ 'url' => $h5Url
+ ]
+ ]);
+
+ } else {
+ // 生成小程序登录码
+ try {
+ // 从环境变量获取小程序配置
+ $miniProgramConfig = [
+ 'app_id' => Env::get('weChat.appidMiniApp', 'wx789850448e26c91d'),
+ 'secret' => Env::get('weChat.secretMiniApp', 'd18f75b3a3623cb40da05648b08365a1'),
+ 'response_type' => 'array'
+ ];
+
+ $app = Factory::miniProgram($miniProgramConfig);
+
+ // scene 参数直接使用 companyId(字符串),并确保长度不超过32
+ $scene = (string)$companyId;
+ if (strlen($scene) > 32) {
+ $scene = substr($scene, 0, 32);
+ }
+
+ // 调用接口生成小程序码
+ // 小程序登录页面路径,需要根据实际项目调整
+ $response = $app->app_code->getUnlimit($scene, [
+ 'page' => 'pages/channel/login', // 请确保小程序里存在该页面
+ 'width' => 430,
+ ]);
+
+ // 成功时返回的是 StreamResponse
+ if ($response instanceof \EasyWeChat\Kernel\Http\StreamResponse) {
+ $img = $response->getBody()->getContents();
+ $imgBase64 = 'data:image/png;base64,' . base64_encode($img);
+
+ return json([
+ 'code' => 200,
+ 'success' => true,
+ 'msg' => '生成小程序登录码成功',
+ 'data' => [
+ 'type' => 'miniprogram',
+ 'qrCode' => $imgBase64,
+ 'scene' => $scene
+ ]
+ ]);
+ }
+
+ // 如果不是流响应,而是数组(错误信息),则解析错误返回
+ if (is_array($response) && isset($response['errcode']) && $response['errcode'] != 0) {
+ $errMsg = isset($response['errmsg']) ? $response['errmsg'] : '微信接口返回错误';
+ return json([
+ 'code' => 500,
+ 'success' => false,
+ 'msg' => '生成小程序登录码失败:' . $errMsg,
+ 'data' => $response
+ ]);
+ }
+
+ // 其他未知格式
+ return json([
+ 'code' => 500,
+ 'success' => false,
+ 'msg' => '生成小程序登录码失败:响应格式错误',
+ 'data' => $response
+ ]);
+ } catch (\Exception $e) {
+ return json([
+ 'code' => 500,
+ 'success' => false,
+ 'msg' => '生成小程序登录码失败:' . $e->getMessage(),
+ 'data' => null
+ ]);
+ }
+ }
+
+ } catch (Exception $e) {
+ return json([
+ 'code' => $e->getCode() ?: 500,
+ 'success' => false,
+ 'msg' => '生成登录二维码失败:' . $e->getMessage(),
+ 'data' => null
+ ]);
+ }
+ }
+
+ /**
+ * 扫码提交渠道信息(H5和小程序共用)
+ * GET请求:返回预填信息
+ * POST请求:提交渠道信息
+ * @return \think\response\Json
+ */
+ public function registerByQrCode()
+ {
+ try {
+ // 获取参数
+ $token = $this->request->param('token', '');
+
+ // 参数验证
+ if (empty($token)) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => 'token不能为空',
+ 'data' => null
+ ]);
+ }
+
+ // 判断传入的是scene(32位MD5)还是token(base64编码)
+ // 如果是32位MD5字符串,则从数据库或缓存中查找对应的token
+ if (strlen($token) == 32 && preg_match('/^[a-f0-9]{32}$/i', $token)) {
+ // 这是scene,先从数据库查找对应的token
+ $sceneData = Db::name('distribution_channel_scene_token')
+ ->where('scene', $token)
+ ->where('expireTime', '>', time())
+ ->find();
+
+ if ($sceneData && !empty($sceneData['token'])) {
+ $realToken = $sceneData['token'];
+ } else {
+ // 如果数据库中没有,尝试从缓存获取
+ $sceneCacheKey = 'channel_register_scene_' . $token;
+ $realToken = Cache::get($sceneCacheKey);
+
+ if (empty($realToken)) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '二维码已过期,请重新生成',
+ 'data' => null
+ ]);
+ }
+ }
+
+ $token = $realToken;
+ }
+
+ // 解析token
+ $tokenData = json_decode(base64_decode($token), true);
+ if (!$tokenData || !isset($tokenData['companyId'])) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => 'token无效',
+ 'data' => null
+ ]);
+ }
+
+ // 检查token是否过期
+ if (isset($tokenData['expireTime']) && $tokenData['expireTime'] < time()) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '二维码已过期,请重新生成',
+ 'data' => null
+ ]);
+ }
+
+ $companyId = $tokenData['companyId'];
+ $userId = isset($tokenData['userId']) ? $tokenData['userId'] : 0; // 兼容旧token,如果没有userId则默认为0
+
+ // GET请求:返回token验证成功信息(前端可以显示表单)
+ if ($this->request->isGet()) {
+ return json([
+ 'code' => 200,
+ 'success' => true,
+ 'msg' => 'token验证成功',
+ 'data' => [
+ 'valid' => true
+ ]
+ ]);
+ }
+
+ // POST请求:提交渠道信息(所有信息都需要用户填写)
+ $name = $this->request->param('name', '');
+ $phone = $this->request->param('phone', '');
+ $wechatId = $this->request->param('wechatId', '');
+ $remarks = $this->request->param('remarks', '');
+
+ // 参数验证
+ if (empty($name)) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '渠道名称不能为空',
+ 'data' => null
+ ]);
+ }
+
+ // 验证渠道名称长度
+ if (mb_strlen($name) > 50) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '渠道名称长度不能超过50个字符',
+ 'data' => null
+ ]);
+ }
+
+ // 验证手机号格式(如果提供)
+ if (!empty($phone)) {
+ if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '手机号格式不正确,请输入11位数字且以1开头',
+ 'data' => null
+ ]);
+ }
+
+ // 检查手机号是否已存在(排除已删除的渠道)
+ $existChannel = Db::name('distribution_channel')
+ ->where([
+ ['companyId', '=', $companyId],
+ ['phone', '=', $phone],
+ ['deleteTime', '=', 0]
+ ])
+ ->find();
+
+ if ($existChannel) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '手机号已被使用',
+ 'data' => null
+ ]);
+ }
+ }
+
+ // 验证微信号长度(如果提供)
+ if (!empty($wechatId) && mb_strlen($wechatId) > 50) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '微信号长度不能超过50个字符',
+ 'data' => null
+ ]);
+ }
+
+ // 验证备注长度(如果提供)
+ if (!empty($remarks) && mb_strlen($remarks) > 200) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '备注信息长度不能超过200个字符',
+ 'data' => null
+ ]);
+ }
+
+ // 生成渠道编码
+ $code = DistributionChannel::generateChannelCode();
+
+ // 准备插入数据(从token中获取userId,记录是哪个用户生成的二维码)
+ $data = [
+ 'companyId' => $companyId,
+ 'userId' => $userId, // 从token中获取userId,记录生成二维码的用户
+ 'name' => $name,
+ 'code' => $code,
+ 'phone' => $phone ?: '',
+ 'password' => md5('123456'), // 默认密码123456,MD5加密
+ 'wechatId' => $wechatId ?: '',
+ 'remarks' => $remarks ?: '',
+ 'createType' => DistributionChannel::CREATE_TYPE_AUTO, // 扫码创建
+ 'status' => DistributionChannel::STATUS_ENABLED,
+ 'totalCustomers' => 0,
+ 'todayCustomers' => 0,
+ 'totalFriends' => 0,
+ 'todayFriends' => 0,
+ 'withdrawableAmount' => 0, // 以分为单位存储
+ 'createTime' => time(),
+ 'updateTime' => time(),
+ ];
+
+ // 插入数据库
+ $channelId = Db::name('distribution_channel')->insertGetId($data);
+
+ if (!$channelId) {
+ return json([
+ 'code' => 500,
+ 'success' => false,
+ 'msg' => '创建渠道失败',
+ 'data' => null
+ ]);
+ }
+
+ // 获取创建的数据
+ $channel = Db::name('distribution_channel')->where('id', $channelId)->find();
+
+ // 格式化返回数据
+ $result = [
+ 'id' => (string)$channel['id'],
+ 'name' => $channel['name'],
+ 'code' => $channel['code'],
+ 'phone' => $channel['phone'] ?: '',
+ 'wechatId' => $channel['wechatId'] ?: '',
+ 'companyId' => (int)$companyId, // 返回companyId,方便小程序自动跳转
+ 'userId' => (int)($channel['userId'] ?? 0),
+ 'createType' => $channel['createType'],
+ 'status' => $channel['status'],
+ 'totalCustomers' => (int)$channel['totalCustomers'],
+ 'todayCustomers' => (int)$channel['todayCustomers'],
+ 'totalFriends' => (int)$channel['totalFriends'],
+ 'todayFriends' => (int)$channel['todayFriends'],
+ 'withdrawableAmount' => round(($channel['withdrawableAmount'] ?? 0) / 100, 2), // 分转元,保留2位小数
+ 'createTime' => !empty($channel['createTime']) ? date('Y-m-d H:i:s', $channel['createTime']) : date('Y-m-d H:i:s'),
+ ];
+
+ return json([
+ 'code' => 200,
+ 'success' => true,
+ 'msg' => '渠道注册成功',
+ 'data' => $result
+ ]);
+
+ } catch (Exception $e) {
+ return json([
+ 'code' => $e->getCode() ?: 500,
+ 'success' => false,
+ 'msg' => '渠道注册失败:' . $e->getMessage(),
+ 'data' => null
+ ]);
+ }
+ }
+
+ /**
+ * 创建scene和token映射表(如果不存在)
+ */
+ protected function createSceneTokenTable()
+ {
+ try {
+ $sql = "CREATE TABLE IF NOT EXISTS `ck_distribution_channel_scene_token` (
+ `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `scene` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '小程序scene参数(MD5值)',
+ `token` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '原始token(base64编码)',
+ `companyId` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '公司ID',
+ `expireTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '过期时间戳',
+ `createTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间',
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE KEY `uk_scene` (`scene`) USING BTREE,
+ INDEX `idx_companyId` (`companyId`) USING BTREE,
+ INDEX `idx_expireTime` (`expireTime`) USING BTREE
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分销渠道小程序码scene和token映射表';";
+
+ Db::execute($sql);
+ } catch (\Exception $e) {
+ // 静默失败
+ }
+ }
+}
+
diff --git a/application/cunkebao/controller/distribution/ChannelUserController.php b/application/cunkebao/controller/distribution/ChannelUserController.php
new file mode 100644
index 0000000..4c27dc0
--- /dev/null
+++ b/application/cunkebao/controller/distribution/ChannelUserController.php
@@ -0,0 +1,722 @@
+request->method(true) == 'OPTIONS') {
+ $origin = $this->request->header('origin', '*');
+ header("Access-Control-Allow-Origin: " . $origin);
+ header("Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization, Cookie");
+ header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, PATCH');
+ header("Access-Control-Allow-Credentials: true");
+ header("Access-Control-Max-Age: 86400");
+ exit;
+ }
+ }
+
+ /**
+ * 设置跨域响应头
+ * @param \think\response\Json $response
+ * @return \think\response\Json
+ */
+ protected function setCorsHeaders($response)
+ {
+ $origin = $this->request->header('origin', '*');
+ $response->header([
+ 'Access-Control-Allow-Origin' => $origin,
+ 'Access-Control-Allow-Headers' => 'Origin, X-Requested-With, Content-Type, Accept, Authorization, Cookie',
+ 'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS, PATCH',
+ 'Access-Control-Allow-Credentials' => 'true',
+ 'Access-Control-Max-Age' => '86400',
+ ]);
+ return $response;
+ }
+
+ /**
+ * 渠道登录
+ * @return \think\response\Json
+ */
+ public function login()
+ {
+ try {
+ // 获取参数
+ $phone = $this->request->param('phone', '');
+ $password = $this->request->param('password', '');
+ $companyId = $this->request->param('companyId', 0);
+ // 参数验证
+ if (empty($phone)) {
+ return $this->setCorsHeaders(json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '手机号不能为空',
+ 'data' => null
+ ]));
+ }
+
+ if (empty($password)) {
+ return $this->setCorsHeaders(json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '密码不能为空',
+ 'data' => null
+ ]));
+ }
+
+ // 查询渠道信息(通过手机号)
+ $channel = Db::name('distribution_channel')
+ ->where([
+ ['phone', '=', $phone],
+ ['companyId', '=', $companyId],
+ ['deleteTime', '=', 0]
+ ])
+ ->find();
+
+
+ if (!$channel) {
+ return $this->setCorsHeaders(json([
+ 'code' => 404,
+ 'success' => false,
+ 'msg' => '渠道不存在',
+ 'data' => null
+ ]));
+ }
+
+ // 检查渠道状态
+ if ($channel['status'] !== DistributionChannel::STATUS_ENABLED) {
+ return $this->setCorsHeaders(json([
+ 'code' => 403,
+ 'success' => false,
+ 'msg' => '渠道已被禁用',
+ 'data' => null
+ ]));
+ }
+
+ // 验证密码(MD5加密)
+ $passwordMd5 = md5($password);
+ if ($channel['password'] !== $passwordMd5) {
+ return $this->setCorsHeaders(json([
+ 'code' => 401,
+ 'success' => false,
+ 'msg' => '密码错误',
+ 'data' => null
+ ]));
+ }
+
+ // 准备token载荷(不包含密码)
+ $payload = [
+ 'id' => $channel['id'],
+ 'channelId' => $channel['id'],
+ 'channelCode' => $channel['code'],
+ 'channelName' => $channel['name'],
+ 'companyId' => $channel['companyId'],
+ 'type' => 'channel', // 标识这是渠道登录
+ ];
+
+ // 生成JWT令牌(30天有效期)
+ $expire = 86400 * 30;
+ $token = JwtUtil::createToken($payload, $expire);
+ $tokenExpired = time() + $expire;
+
+ // 更新最后登录时间(可选)
+ Db::name('distribution_channel')
+ ->where('id', $channel['id'])
+ ->update([
+ 'updateTime' => time()
+ ]);
+
+ // 返回数据(不包含密码)
+ $data = [
+ 'token' => $token,
+ 'tokenExpired' => $tokenExpired,
+ 'channelInfo' => [
+ 'id' => (string)$channel['id'],
+ 'channelCode' => $channel['code'],
+ 'channelName' => $channel['name'],
+ 'phone' => $channel['phone'] ?: '',
+ 'wechatId' => $channel['wechatId'] ?: '',
+ 'companyId' => (int)$channel['companyId'], // 返回companyId,方便小程序自动跳转
+ 'status' => $channel['status'],
+ 'totalCustomers' => (int)$channel['totalCustomers'],
+ 'todayCustomers' => (int)$channel['todayCustomers'],
+ 'totalFriends' => (int)$channel['totalFriends'],
+ 'todayFriends' => (int)$channel['todayFriends'],
+ 'withdrawableAmount' => round(($channel['withdrawableAmount'] ?? 0) / 100, 2), // 分转元
+ ]
+ ];
+
+ return $this->setCorsHeaders(json([
+ 'code' => 200,
+ 'success' => true,
+ 'msg' => '登录成功',
+ 'data' => $data
+ ]));
+
+ } catch (Exception $e) {
+ return $this->setCorsHeaders(json([
+ 'code' => $e->getCode() ?: 500,
+ 'success' => false,
+ 'msg' => '登录失败:' . $e->getMessage(),
+ 'data' => null
+ ]));
+ }
+ }
+
+ /**
+ * 获取渠道首页数据
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ // 获取参数
+ $channelCode = $this->request->param('channelCode', '');
+
+ // 参数验证
+ if (empty($channelCode)) {
+ return $this->setCorsHeaders(json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '渠道编码不能为空',
+ 'data' => null
+ ]));
+ }
+
+ // 查询渠道信息
+ $channel = Db::name('distribution_channel')
+ ->where([
+ ['code', '=', $channelCode],
+ ['status', '=', DistributionChannel::STATUS_ENABLED],
+ ['deleteTime', '=', 0]
+ ])
+ ->find();
+
+ if (!$channel) {
+ return $this->setCorsHeaders(json([
+ 'code' => 404,
+ 'success' => false,
+ 'msg' => '渠道不存在或已被禁用',
+ 'data' => null
+ ]));
+ }
+
+ $channelId = $channel['id'];
+ $companyId = $channel['companyId'];
+
+ // 1. 渠道基本信息
+ $channelInfo = [
+ 'channelName' => $channel['name'] ?? '',
+ 'channelCode' => $channel['code'] ?? '',
+ 'phone' => $channel['phone'] ?? '',
+ 'wechatId' => $channel['wechatId'] ?? '',
+ 'remark' => $channel['remark'] ?? '',
+ 'createTime' => !empty($channel['createTime']) ? date('Y-m-d H:i:s', $channel['createTime']) : '',
+ 'createType' => $channel['createType'] ?? '',
+ ];
+
+ // 2. 财务统计
+ // 当前可提现金额
+ $withdrawableAmount = round(($channel['withdrawableAmount'] ?? 0) / 100, 2); // 分转元
+
+ // 已提现金额(已打款的提现申请)
+ $withdrawnAmount = Db::name('distribution_withdrawal')
+ ->where([
+ ['companyId', '=', $companyId],
+ ['channelId', '=', $channelId],
+ ['status', '=', DistributionWithdrawal::STATUS_PAID]
+ ])
+ ->sum('amount');
+ $withdrawnAmount = round(($withdrawnAmount ?? 0) / 100, 2); // 分转元
+
+ // 待审核金额(待审核的提现申请)
+ $pendingReviewAmount = Db::name('distribution_withdrawal')
+ ->where([
+ ['companyId', '=', $companyId],
+ ['channelId', '=', $channelId],
+ ['status', '=', DistributionWithdrawal::STATUS_PENDING]
+ ])
+ ->sum('amount');
+ $pendingReviewAmount = round(($pendingReviewAmount ?? 0) / 100, 2); // 分转元
+
+ // 总收益(所有收益记录的总和)
+ $totalRevenue = Db::name('distribution_revenue_record')
+ ->where([
+ ['companyId', '=', $companyId],
+ ['channelId', '=', $channelId]
+ ])
+ ->sum('amount');
+ $totalRevenue = round(($totalRevenue ?? 0) / 100, 2); // 分转元
+
+ $financialStats = [
+ 'withdrawableAmount' => $withdrawableAmount, // 当前可提现金额
+ 'totalRevenue' => $totalRevenue, // 总收益
+ 'pendingReview' => $pendingReviewAmount, // 待审核
+ 'withdrawn' => $withdrawnAmount, // 已提现
+ ];
+
+ // 3. 客户和好友统计
+ $customerStats = [
+ 'totalFriends' => (int)($channel['totalFriends'] ?? 0), // 总加好友数
+ 'todayFriends' => (int)($channel['todayFriends'] ?? 0), // 今日加好友数
+ 'totalCustomers' => (int)($channel['totalCustomers'] ?? 0), // 总获客数
+ 'todayCustomers' => (int)($channel['todayCustomers'] ?? 0), // 今日获客数
+ ];
+
+ // 返回数据
+ $data = [
+ 'channelInfo' => $channelInfo,
+ 'financialStats' => $financialStats,
+ 'customerStats' => $customerStats,
+ ];
+
+ return $this->setCorsHeaders(json([
+ 'code' => 200,
+ 'success' => true,
+ 'msg' => '获取成功',
+ 'data' => $data
+ ]));
+
+ } catch (Exception $e) {
+ return $this->setCorsHeaders(json([
+ 'code' => $e->getCode() ?: 500,
+ 'success' => false,
+ 'msg' => '获取数据失败:' . $e->getMessage(),
+ 'data' => null
+ ]));
+ }
+ }
+
+ /**
+ * 获取收益明细列表
+ * @return \think\response\Json
+ */
+ public function revenueRecords()
+ {
+ try {
+ // 获取参数
+ $channelCode = $this->request->param('channelCode', '');
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $type = $this->request->param('type', 'all'); // all, customer_acquisition, add_friend, order, poster, phone, other
+ $date = $this->request->param('date', ''); // 日期筛选,格式:Y-m-d
+
+ // 参数验证
+ if (empty($channelCode)) {
+ return $this->setCorsHeaders(json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '渠道编码不能为空',
+ 'data' => null
+ ]));
+ }
+
+ $page = max(1, intval($page));
+ $limit = max(1, min(100, intval($limit)));
+
+ // 查询渠道信息
+ $channel = Db::name('distribution_channel')
+ ->where([
+ ['code', '=', $channelCode],
+ ['status', '=', DistributionChannel::STATUS_ENABLED],
+ ['deleteTime', '=', 0]
+ ])
+ ->find();
+
+ if (!$channel) {
+ return $this->setCorsHeaders(json([
+ 'code' => 404,
+ 'success' => false,
+ 'msg' => '渠道不存在或已被禁用',
+ 'data' => null
+ ]));
+ }
+
+ $channelId = $channel['id'];
+ $companyId = $channel['companyId'];
+
+ // 构建查询条件
+ $where = [
+ ['companyId', '=', $companyId],
+ ['channelId', '=', $channelId]
+ ];
+
+ // 类型筛选
+ if ($type !== 'all') {
+ $where[] = ['type', '=', $type];
+ }
+
+ // 日期筛选
+ if (!empty($date)) {
+ $dateStart = strtotime($date . ' 00:00:00');
+ $dateEnd = strtotime($date . ' 23:59:59');
+ if ($dateStart && $dateEnd) {
+ $where[] = ['createTime', 'between', [$dateStart, $dateEnd]];
+ }
+ }
+
+ // 查询总数
+ $total = Db::name('distribution_revenue_record')
+ ->where($where)
+ ->count();
+
+ // 查询列表(按创建时间倒序)
+ $list = Db::name('distribution_revenue_record')
+ ->where($where)
+ ->order('createTime DESC')
+ ->page($page, $limit)
+ ->select();
+
+ // 从活动表(customer_acquisition_task)获取类型标签映射(使用 sourceId 关联活动ID)
+ $formattedList = [];
+ if (!empty($list)) {
+ // 收集本页涉及到的活动ID
+ $taskIds = [];
+ foreach ($list as $row) {
+ if (!empty($row['sourceId'])) {
+ $taskIds[] = (int)$row['sourceId'];
+ }
+ }
+ $taskIds = array_values(array_unique($taskIds));
+
+ // 获取活动名称映射:taskId => name
+ $taskNameMap = [];
+ if (!empty($taskIds)) {
+ $taskNameMap = Db::name('customer_acquisition_task')
+ ->whereIn('id', $taskIds)
+ ->column('name', 'id');
+ }
+
+ // 格式化数据
+ foreach ($list as $item) {
+ $taskId = !empty($item['sourceId']) ? (int)$item['sourceId'] : 0;
+ $taskName = $taskId && isset($taskNameMap[$taskId]) ? $taskNameMap[$taskId] : null;
+
+ $formattedItem = [
+ 'id' => (string)$item['id'],
+ 'sourceType' => $item['sourceType'] ?? '其他',
+ 'type' => $item['type'] ?? 'other',
+ // 类型标签优先取活动名称,没有则回退为 sourceType 或 “其他”
+ 'typeLabel' => $taskName ?: (!empty($item['sourceType']) ? $item['sourceType'] : '其他'),
+ 'amount' => round($item['amount'] / 100, 2), // 分转元
+ 'remark' => isset($item['remark']) && $item['remark'] !== '' ? $item['remark'] : null,
+ 'createTime' => !empty($item['createTime']) ? date('Y-m-d H:i', $item['createTime']) : '',
+ ];
+ $formattedList[] = $formattedItem;
+ }
+ }
+
+ return $this->setCorsHeaders(json([
+ 'code' => 200,
+ 'success' => true,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $formattedList,
+ 'total' => (int)$total,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]));
+
+ } catch (Exception $e) {
+ return $this->setCorsHeaders(json([
+ 'code' => $e->getCode() ?: 500,
+ 'success' => false,
+ 'msg' => '获取收益明细失败:' . $e->getMessage(),
+ 'data' => null
+ ]));
+ }
+ }
+
+ /**
+ * 获取提现明细列表
+ * @return \think\response\Json
+ */
+ public function withdrawalRecords()
+ {
+ try {
+ // 获取参数
+ $channelCode = $this->request->param('channelCode', '');
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $status = $this->request->param('status', 'all'); // all, pending, approved, rejected, paid
+ $payType = $this->request->param('payType', 'all'); // all, wechat, alipay, bankcard
+ $date = $this->request->param('date', ''); // 日期筛选,格式:Y-m-d
+
+ // 参数验证
+ if (empty($channelCode)) {
+ return $this->setCorsHeaders(json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '渠道编码不能为空',
+ 'data' => null
+ ]));
+ }
+
+ $page = max(1, intval($page));
+ $limit = max(1, min(100, intval($limit)));
+
+ // 校验到账方式参数
+ $validPayTypes = ['all', 'wechat', 'alipay', 'bankcard'];
+ if (!in_array($payType, $validPayTypes)) {
+ $payType = 'all';
+ }
+
+ // 查询渠道信息
+ $channel = Db::name('distribution_channel')
+ ->where([
+ ['code', '=', $channelCode],
+ ['status', '=', DistributionChannel::STATUS_ENABLED],
+ ['deleteTime', '=', 0]
+ ])
+ ->find();
+
+ if (!$channel) {
+ return $this->setCorsHeaders(json([
+ 'code' => 404,
+ 'success' => false,
+ 'msg' => '渠道不存在或已被禁用',
+ 'data' => null
+ ]));
+ }
+
+ $channelId = $channel['id'];
+ $companyId = $channel['companyId'];
+
+ // 构建查询条件
+ $where = [
+ ['companyId', '=', $companyId],
+ ['channelId', '=', $channelId]
+ ];
+
+ // 状态筛选
+ if ($status !== 'all') {
+ $where[] = ['status', '=', $status];
+ }
+
+ // 到账方式筛选
+ if ($payType !== 'all') {
+ $where[] = ['payType', '=', $payType];
+ }
+
+ // 日期筛选
+ if (!empty($date)) {
+ $dateStart = strtotime($date . ' 00:00:00');
+ $dateEnd = strtotime($date . ' 23:59:59');
+ if ($dateStart && $dateEnd) {
+ $where[] = ['applyTime', 'between', [$dateStart, $dateEnd]];
+ }
+ }
+
+ // 查询总数
+ $total = Db::name('distribution_withdrawal')
+ ->where($where)
+ ->count();
+
+ // 查询列表(按申请时间倒序)
+ $list = Db::name('distribution_withdrawal')
+ ->where($where)
+ ->order('applyTime DESC')
+ ->page($page, $limit)
+ ->select();
+
+ // 格式化数据
+ $formattedList = [];
+ foreach ($list as $item) {
+ // 状态标签映射
+ $statusLabels = [
+ 'pending' => '待审核',
+ 'approved' => '已通过',
+ 'rejected' => '已拒绝',
+ 'paid' => '已打款'
+ ];
+
+ // 支付类型标签映射
+ $payTypeLabels = [
+ 'wechat' => '微信',
+ 'alipay' => '支付宝',
+ 'bankcard' => '银行卡'
+ ];
+
+ $payType = !empty($item['payType']) ? $item['payType'] : null;
+
+ $formattedItem = [
+ 'id' => (string)$item['id'],
+ 'amount' => round($item['amount'] / 100, 2), // 分转元
+ 'status' => $item['status'] ?? 'pending',
+ 'statusLabel' => $statusLabels[$item['status'] ?? 'pending'] ?? '待审核',
+ 'payType' => $payType,
+ 'payTypeLabel' => $payType && isset($payTypeLabels[$payType]) ? $payTypeLabels[$payType] : null,
+ 'applyTime' => !empty($item['applyTime']) ? date('Y-m-d H:i', $item['applyTime']) : '',
+ 'reviewTime' => !empty($item['reviewTime']) ? date('Y-m-d H:i', $item['reviewTime']) : null,
+ 'reviewer' => !empty($item['reviewer']) ? $item['reviewer'] : null,
+ 'remark' => !empty($item['remark']) ? $item['remark'] : null,
+ ];
+ $formattedList[] = $formattedItem;
+ }
+
+ return $this->setCorsHeaders(json([
+ 'code' => 200,
+ 'success' => true,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $formattedList,
+ 'total' => (int)$total,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]));
+
+ } catch (Exception $e) {
+ return $this->setCorsHeaders(json([
+ 'code' => $e->getCode() ?: 500,
+ 'success' => false,
+ 'msg' => '获取提现明细失败:' . $e->getMessage(),
+ 'data' => null
+ ]));
+ }
+ }
+
+ /**
+ * 修改渠道分销员密码
+ * @return \think\response\Json
+ */
+ public function changePassword()
+ {
+ try {
+ // 获取参数并去除首尾空格
+ $channelCode = trim($this->request->param('channelCode', ''));
+ $oldPassword = trim($this->request->param('oldPassword', ''));
+ $newPassword = trim($this->request->param('newPassword', ''));
+
+ // 参数验证
+ if (empty($channelCode)) {
+ return $this->setCorsHeaders(json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '渠道编码不能为空',
+ 'data' => null
+ ]));
+ }
+
+ if (empty($oldPassword)) {
+ return $this->setCorsHeaders(json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '原密码不能为空',
+ 'data' => null
+ ]));
+ }
+
+ if (empty($newPassword)) {
+ return $this->setCorsHeaders(json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '新密码不能为空',
+ 'data' => null
+ ]));
+ }
+
+ // 验证新密码长度(至少6位)
+ if (mb_strlen($newPassword) < 6) {
+ return $this->setCorsHeaders(json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '新密码长度至少为6位',
+ 'data' => null
+ ]));
+ }
+
+ // 查询渠道信息
+ $channel = Db::name('distribution_channel')
+ ->where([
+ ['code', '=', $channelCode],
+ ['status', '=', DistributionChannel::STATUS_ENABLED],
+ ['deleteTime', '=', 0]
+ ])
+ ->find();
+
+ if (!$channel) {
+ return $this->setCorsHeaders(json([
+ 'code' => 404,
+ 'success' => false,
+ 'msg' => '渠道不存在或已被禁用',
+ 'data' => null
+ ]));
+ }
+
+ // 验证原密码(MD5加密)
+ $oldPasswordMd5 = md5($oldPassword);
+ if ($channel['password'] !== $oldPasswordMd5) {
+ return $this->setCorsHeaders(json([
+ 'code' => 401,
+ 'success' => false,
+ 'msg' => '原密码错误',
+ 'data' => null
+ ]));
+ }
+
+ // 检查新密码是否与原密码相同
+ $newPasswordMd5 = md5($newPassword);
+ if ($channel['password'] === $newPasswordMd5) {
+ return $this->setCorsHeaders(json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '新密码不能与原密码相同',
+ 'data' => null
+ ]));
+ }
+
+ // 更新密码
+ $updateResult = Db::name('distribution_channel')
+ ->where('id', $channel['id'])
+ ->update([
+ 'password' => $newPasswordMd5,
+ 'updateTime' => time()
+ ]);
+
+ if ($updateResult === false) {
+ return $this->setCorsHeaders(json([
+ 'code' => 500,
+ 'success' => false,
+ 'msg' => '密码修改失败,请稍后重试',
+ 'data' => null
+ ]));
+ }
+
+ return $this->setCorsHeaders(json([
+ 'code' => 200,
+ 'success' => true,
+ 'msg' => '密码修改成功',
+ 'data' => null
+ ]));
+
+ } catch (Exception $e) {
+ return $this->setCorsHeaders(json([
+ 'code' => $e->getCode() ?: 500,
+ 'success' => false,
+ 'msg' => '密码修改失败:' . $e->getMessage(),
+ 'data' => null
+ ]));
+ }
+ }
+}
+
diff --git a/application/cunkebao/controller/distribution/WithdrawalController.php b/application/cunkebao/controller/distribution/WithdrawalController.php
new file mode 100644
index 0000000..4cdf7fb
--- /dev/null
+++ b/application/cunkebao/controller/distribution/WithdrawalController.php
@@ -0,0 +1,692 @@
+request->param('page', 1);
+ $limit = $this->request->param('limit', 20);
+ $status = $this->request->param('status', 'all');
+ $date = $this->request->param('date', '');
+ $keyword = $this->request->param('keyword', '');
+
+ $companyId = $this->getUserInfo('companyId');
+
+ // 参数验证
+ $page = max(1, intval($page));
+ $limit = max(1, min(100, intval($limit))); // 限制最大100
+
+ // 验证状态参数
+ $validStatuses = ['all', DistributionWithdrawal::STATUS_PENDING, DistributionWithdrawal::STATUS_APPROVED, DistributionWithdrawal::STATUS_REJECTED, DistributionWithdrawal::STATUS_PAID];
+ if (!in_array($status, $validStatuses)) {
+ $status = 'all';
+ }
+
+ // 构建查询条件
+ $where = [];
+ $where[] = ['w.companyId', '=', $companyId];
+
+ // 如果不是管理员,只能查看自己创建的提现申请
+ if (!$this->getUserInfo('isAdmin')) {
+ $where[] = ['w.userId', '=', $this->getUserInfo('id')];
+ }
+
+ // 状态筛选
+ if ($status !== 'all') {
+ $where[] = ['w.status', '=', $status];
+ }
+
+ // 日期筛选(格式:YYYY/MM/DD)
+ if (!empty($date)) {
+ // 转换日期格式 YYYY/MM/DD 为时间戳范围
+ $dateParts = explode('/', $date);
+ if (count($dateParts) === 3) {
+ $dateStr = $dateParts[0] . '-' . $dateParts[1] . '-' . $dateParts[2];
+ $dateStart = strtotime($dateStr . ' 00:00:00');
+ $dateEnd = strtotime($dateStr . ' 23:59:59');
+ if ($dateStart && $dateEnd) {
+ $where[] = ['w.applyTime', 'between', [$dateStart, $dateEnd]];
+ }
+ }
+ }
+
+ // 关键词搜索(模糊匹配渠道名称、渠道编码)
+ if (!empty($keyword)) {
+ $keyword = trim($keyword);
+ // 需要关联渠道表进行搜索
+ }
+
+ // 构建查询(关联渠道表获取渠道名称和编码,只关联未删除的渠道)
+ $query = Db::name('distribution_withdrawal')
+ ->alias('w')
+ ->join('distribution_channel c', 'w.channelId = c.id AND c.deleteTime = 0', 'left')
+ ->where($where);
+
+ // 关键词搜索(如果有关键词,添加渠道表关联条件)
+ if (!empty($keyword)) {
+ $query->where(function ($query) use ($keyword) {
+ $query->where('c.name', 'like', '%' . $keyword . '%')
+ ->whereOr('c.code', 'like', '%' . $keyword . '%');
+ });
+ }
+
+ // 查询总数
+ $total = $query->count();
+
+ // 查询列表(按申请时间倒序)
+ $list = $query->field([
+ 'w.id',
+ 'w.channelId',
+ 'w.userId',
+ 'w.amount',
+ 'w.status',
+ 'w.payType',
+ 'w.applyTime',
+ 'w.reviewTime',
+ 'w.reviewer',
+ 'w.remark',
+ 'c.name as channelName',
+ 'c.code as channelCode'
+ ])
+ ->order('w.applyTime DESC')
+ ->page($page, $limit)
+ ->select();
+
+ // 格式化数据
+ $formattedList = [];
+ foreach ($list as $item) {
+ // 格式化申请日期为 YYYY/MM/DD
+ $applyDate = '';
+ if (!empty($item['applyTime'])) {
+ $applyDate = date('Y/m/d', $item['applyTime']);
+ }
+
+ // 格式化审核日期
+ $reviewDate = null;
+ if (!empty($item['reviewTime'])) {
+ $reviewDate = date('Y-m-d H:i:s', $item['reviewTime']);
+ }
+
+ $formattedItem = [
+ 'id' => (string)$item['id'],
+ 'channelId' => (string)$item['channelId'],
+ 'channelName' => $item['channelName'] ?? '',
+ 'channelCode' => $item['channelCode'] ?? '',
+ 'userId' => (int)($item['userId'] ?? 0),
+ 'amount' => round($item['amount'] / 100, 2), // 分转元,保留2位小数
+ 'status' => $item['status'] ?? DistributionWithdrawal::STATUS_PENDING,
+ 'payType' => !empty($item['payType']) ? $item['payType'] : null, // 支付类型
+ 'applyDate' => $applyDate,
+ 'reviewDate' => $reviewDate,
+ 'reviewer' => !empty($item['reviewer']) ? $item['reviewer'] : null,
+ 'remark' => !empty($item['remark']) ? $item['remark'] : null,
+ ];
+ $formattedList[] = $formattedItem;
+ }
+
+ // 返回结果
+ return json([
+ 'code' => 200,
+ 'success' => true,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $formattedList,
+ 'total' => (int)$total
+ ]
+ ]);
+
+ } catch (Exception $e) {
+ return json([
+ 'code' => $e->getCode() ?: 500,
+ 'success' => false,
+ 'msg' => '获取提现申请列表失败:' . $e->getMessage(),
+ 'data' => null
+ ]);
+ }
+ }
+
+ /**
+ * 创建提现申请
+ * @return \think\response\Json
+ */
+ public function create()
+ {
+ try {
+ // 获取参数(接口接收的金额单位为元)
+ // 原先使用 channelId,现在改为使用渠道编码 channelCode
+ $channelCode = $this->request->param('channelCode', '');
+ $amount = $this->request->param('amount', 0); // 金额单位:元
+
+ $companyId = $this->getUserInfo('companyId');
+
+ // 参数验证
+ if (empty($channelCode)) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '渠道编码不能为空',
+ 'data' => null
+ ]);
+ }
+
+ // 验证金额(转换为浮点数进行验证)
+ $amount = floatval($amount);
+ if (empty($amount) || $amount <= 0) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '提现金额必须大于0',
+ 'data' => null
+ ]);
+ }
+
+ // 验证金额格式(最多2位小数)
+ if (!preg_match('/^\d+(\.\d{1,2})?$/', (string)$amount)) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '提现金额格式不正确,最多保留2位小数',
+ 'data' => null
+ ]);
+ }
+
+ // 检查渠道是否存在且属于当前公司(通过渠道编码查询)
+ $channel = Db::name('distribution_channel')
+ ->where([
+ ['code', '=', $channelCode],
+ ['companyId', '=', $companyId],
+ ['deleteTime', '=', 0]
+ ])
+ ->find();
+
+ if (!$channel) {
+ return json([
+ 'code' => 404,
+ 'success' => false,
+ 'msg' => '渠道不存在或没有权限',
+ 'data' => null
+ ]);
+ }
+
+ // 统一使用渠道ID变量,后续逻辑仍然基于 channelId
+ $channelId = $channel['id'];
+ // 从渠道获取创建者的userId,而不是当前登录用户的userId
+ $userId = intval($channel['userId'] ?? 0);
+
+ // 检查渠道状态
+ if ($channel['status'] !== 'enabled') {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '渠道已禁用,无法申请提现',
+ 'data' => null
+ ]);
+ }
+
+ // 检查可提现金额
+ // 数据库存储的是分,接口接收的是元,需要统一单位进行比较
+ $withdrawableAmountInFen = intval($channel['withdrawableAmount'] ?? 0); // 数据库中的分
+ $withdrawableAmountInYuan = round($withdrawableAmountInFen / 100, 2); // 转换为元用于提示
+ $amountInFen = intval(round($amount * 100)); // 将接口接收的元转换为分
+
+ if ($amountInFen > $withdrawableAmountInFen) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '提现金额不能超过可提现金额(' . number_format($withdrawableAmountInYuan, 2) . '元)',
+ 'data' => null
+ ]);
+ }
+
+ // 检查是否有待审核的申请
+ $pendingWithdrawal = Db::name('distribution_withdrawal')
+ ->where([
+ ['channelId', '=', $channelId],
+ ['companyId', '=', $companyId],
+ ['status', '=', DistributionWithdrawal::STATUS_PENDING]
+ ])
+ ->find();
+
+ if ($pendingWithdrawal) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '该渠道已有待审核的提现申请,请等待审核完成后再申请',
+ 'data' => null
+ ]);
+ }
+
+ // 开始事务
+ Db::startTrans();
+ try {
+ // 创建提现申请(金额以分存储)
+ $withdrawalData = [
+ 'companyId' => $companyId,
+ 'channelId' => $channelId,
+ 'userId' => $userId,
+ 'amount' => $amountInFen, // 存储为分
+ 'status' => DistributionWithdrawal::STATUS_PENDING,
+ 'applyTime' => time(),
+ 'createTime' => time(),
+ 'updateTime' => time(),
+ ];
+
+ $withdrawalId = Db::name('distribution_withdrawal')->insertGetId($withdrawalData);
+
+ if (!$withdrawalId) {
+ throw new Exception('创建提现申请失败');
+ }
+
+ // 扣除渠道可提现金额(以分为单位)
+ Db::name('distribution_channel')
+ ->where('id', $channelId)
+ ->setDec('withdrawableAmount', $amountInFen);
+
+ // 提交事务
+ Db::commit();
+
+ // 获取创建的申请数据
+ $withdrawal = Db::name('distribution_withdrawal')
+ ->alias('w')
+ ->join('distribution_channel c', 'w.channelId = c.id', 'left')
+ ->where('w.id', $withdrawalId)
+ ->field([
+ 'w.id',
+ 'w.channelId',
+ 'w.userId',
+ 'w.amount',
+ 'w.status',
+ 'w.payType',
+ 'w.applyTime',
+ 'c.name as channelName',
+ 'c.code as channelCode'
+ ])
+ ->find();
+
+ // 格式化返回数据(分转元)
+ $result = [
+ 'id' => (string)$withdrawal['id'],
+ 'channelId' => (string)$withdrawal['channelId'],
+ 'channelName' => $withdrawal['channelName'] ?? '',
+ 'channelCode' => $withdrawal['channelCode'] ?? '',
+ 'userId' => (int)($withdrawal['userId'] ?? 0),
+ 'amount' => round($withdrawal['amount'] / 100, 2), // 分转元,保留2位小数
+ 'status' => $withdrawal['status'],
+ 'payType' => !empty($withdrawal['payType']) ? $withdrawal['payType'] : null, // 支付类型:wechat、alipay、bankcard(创建时为null)
+ 'applyDate' => !empty($withdrawal['applyTime']) ? date('Y/m/d', $withdrawal['applyTime']) : '',
+ 'reviewDate' => null,
+ 'reviewer' => null,
+ 'remark' => null,
+ ];
+
+ return json([
+ 'code' => 200,
+ 'success' => true,
+ 'msg' => '提现申请提交成功',
+ 'data' => $result
+ ]);
+
+ } catch (Exception $e) {
+ Db::rollback();
+ throw $e;
+ }
+
+ } catch (Exception $e) {
+ return json([
+ 'code' => $e->getCode() ?: 500,
+ 'success' => false,
+ 'msg' => '提交提现申请失败:' . $e->getMessage(),
+ 'data' => null
+ ]);
+ }
+ }
+
+ /**
+ * 审核提现申请(通过/拒绝)
+ * @return \think\response\Json
+ */
+ public function review()
+ {
+ try {
+ // 获取参数
+ $id = $this->request->param('id', 0);
+ $action = $this->request->param('action', ''); // approve 或 reject
+ $remark = $this->request->param('remark', '');
+
+ $companyId = $this->getUserInfo('companyId');
+ $reviewer = $this->getUserInfo('username') ?: $this->getUserInfo('account') ?: '系统管理员';
+
+ // 参数验证
+ if (empty($id)) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '申请ID不能为空',
+ 'data' => null
+ ]);
+ }
+
+ if (!in_array($action, ['approve', 'reject'])) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '审核操作参数错误,必须为 approve 或 reject',
+ 'data' => null
+ ]);
+ }
+
+ // 如果是拒绝,备注必填
+ if ($action === 'reject' && empty($remark)) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '拒绝申请时,拒绝理由不能为空',
+ 'data' => null
+ ]);
+ }
+
+ // 检查申请是否存在且属于当前公司
+ $withdrawal = Db::name('distribution_withdrawal')
+ ->where([
+ ['id', '=', $id],
+ ['companyId', '=', $companyId]
+ ])
+ ->find();
+
+ if (!$withdrawal) {
+ return json([
+ 'code' => 404,
+ 'success' => false,
+ 'msg' => '提现申请不存在或没有权限',
+ 'data' => null
+ ]);
+ }
+
+ // 检查申请状态
+ if ($withdrawal['status'] !== DistributionWithdrawal::STATUS_PENDING) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '该申请已审核,无法重复审核',
+ 'data' => null
+ ]);
+ }
+
+ // 开始事务
+ Db::startTrans();
+ try {
+ $updateData = [
+ 'reviewTime' => time(),
+ 'reviewer' => $reviewer,
+ 'remark' => $remark ?: '',
+ 'updateTime' => time(),
+ ];
+
+ if ($action === 'approve') {
+ // 审核通过
+ $updateData['status'] = DistributionWithdrawal::STATUS_APPROVED;
+ } else {
+ // 审核拒绝,退回可提现金额(金额以分存储)
+ $updateData['status'] = DistributionWithdrawal::STATUS_REJECTED;
+
+ // 退回渠道可提现金额(以分为单位)
+ Db::name('distribution_channel')
+ ->where('id', $withdrawal['channelId'])
+ ->setInc('withdrawableAmount', intval($withdrawal['amount']));
+ }
+
+ // 更新申请状态
+ Db::name('distribution_withdrawal')
+ ->where('id', $id)
+ ->update($updateData);
+
+ // 提交事务
+ Db::commit();
+
+ $msg = $action === 'approve' ? '审核通过成功' : '审核拒绝成功';
+
+ return json([
+ 'code' => 200,
+ 'success' => true,
+ 'msg' => $msg,
+ 'data' => [
+ 'id' => (string)$id,
+ 'status' => $updateData['status']
+ ]
+ ]);
+
+ } catch (Exception $e) {
+ Db::rollback();
+ throw $e;
+ }
+
+ } catch (Exception $e) {
+ return json([
+ 'code' => $e->getCode() ?: 500,
+ 'success' => false,
+ 'msg' => '审核失败:' . $e->getMessage(),
+ 'data' => null
+ ]);
+ }
+ }
+
+ /**
+ * 打款(标记为已打款)
+ * @return \think\response\Json
+ */
+ public function markPaid()
+ {
+ try {
+ // 获取参数
+ $id = $this->request->param('id', 0);
+ $payType = $this->request->param('payType', ''); // 支付类型:wechat、alipay、bankcard
+ $remark = $this->request->param('remark', '');
+
+ $companyId = $this->getUserInfo('companyId');
+
+ // 参数验证
+ if (empty($id)) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '申请ID不能为空',
+ 'data' => null
+ ]);
+ }
+
+ // 验证支付类型
+ $validPayTypes = [
+ DistributionWithdrawal::PAY_TYPE_WECHAT,
+ DistributionWithdrawal::PAY_TYPE_ALIPAY,
+ DistributionWithdrawal::PAY_TYPE_BANKCARD
+ ];
+ if (empty($payType) || !in_array($payType, $validPayTypes)) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '支付类型不能为空,必须为:wechat(微信)、alipay(支付宝)、bankcard(银行卡)',
+ 'data' => null
+ ]);
+ }
+
+ // 检查申请是否存在且属于当前公司
+ $withdrawal = Db::name('distribution_withdrawal')
+ ->where([
+ ['id', '=', $id],
+ ['companyId', '=', $companyId]
+ ])
+ ->find();
+
+ if (!$withdrawal) {
+ return json([
+ 'code' => 404,
+ 'success' => false,
+ 'msg' => '提现申请不存在或没有权限',
+ 'data' => null
+ ]);
+ }
+
+ // 检查申请状态(只有已通过的申请才能打款)
+ if ($withdrawal['status'] !== DistributionWithdrawal::STATUS_APPROVED) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '只有已通过的申请才能标记为已打款',
+ 'data' => null
+ ]);
+ }
+
+ // 更新状态为已打款
+ $result = Db::name('distribution_withdrawal')
+ ->where('id', $id)
+ ->update([
+ 'status' => DistributionWithdrawal::STATUS_PAID,
+ 'payType' => $payType,
+ 'remark' => !empty($remark) ? $remark : $withdrawal['remark'],
+ 'updateTime' => time()
+ ]);
+
+ if ($result === false) {
+ return json([
+ 'code' => 500,
+ 'success' => false,
+ 'msg' => '标记打款失败',
+ 'data' => null
+ ]);
+ }
+
+ return json([
+ 'code' => 200,
+ 'success' => true,
+ 'msg' => '标记打款成功',
+ 'data' => [
+ 'id' => (string)$id,
+ 'status' => DistributionWithdrawal::STATUS_PAID,
+ 'payType' => $payType
+ ]
+ ]);
+
+ } catch (Exception $e) {
+ return json([
+ 'code' => $e->getCode() ?: 500,
+ 'success' => false,
+ 'msg' => '标记打款失败:' . $e->getMessage(),
+ 'data' => null
+ ]);
+ }
+ }
+
+ /**
+ * 获取提现申请详情
+ * @return \think\response\Json
+ */
+ public function detail()
+ {
+ try {
+ // 获取参数
+ $id = $this->request->param('id', 0);
+
+ $companyId = $this->getUserInfo('companyId');
+
+ // 参数验证
+ if (empty($id)) {
+ return json([
+ 'code' => 400,
+ 'success' => false,
+ 'msg' => '申请ID不能为空',
+ 'data' => null
+ ]);
+ }
+
+ // 构建查询条件
+ $where = [
+ ['w.id', '=', $id],
+ ['w.companyId', '=', $companyId]
+ ];
+
+ // 如果不是管理员,只能查看自己创建的提现申请
+ if (!$this->getUserInfo('isAdmin')) {
+ $where[] = ['w.userId', '=', $this->getUserInfo('id')];
+ }
+
+ // 查询申请详情(关联渠道表)
+ $withdrawal = Db::name('distribution_withdrawal')
+ ->alias('w')
+ ->join('distribution_channel c', 'w.channelId = c.id AND c.deleteTime = 0', 'left')
+ ->where($where)
+ ->field([
+ 'w.id',
+ 'w.channelId',
+ 'w.userId',
+ 'w.amount',
+ 'w.status',
+ 'w.payType',
+ 'w.applyTime',
+ 'w.reviewTime',
+ 'w.reviewer',
+ 'w.remark',
+ 'c.name as channelName',
+ 'c.code as channelCode'
+ ])
+ ->find();
+
+ if (!$withdrawal) {
+ return json([
+ 'code' => 404,
+ 'success' => false,
+ 'msg' => '提现申请不存在或没有权限',
+ 'data' => null
+ ]);
+ }
+
+ // 格式化返回数据(分转元)
+ $result = [
+ 'id' => (string)$withdrawal['id'],
+ 'channelId' => (string)$withdrawal['channelId'],
+ 'channelName' => $withdrawal['channelName'] ?? '',
+ 'channelCode' => $withdrawal['channelCode'] ?? '',
+ 'userId' => (int)($withdrawal['userId'] ?? 0),
+ 'amount' => round($withdrawal['amount'] / 100, 2), // 分转元,保留2位小数
+ 'status' => $withdrawal['status'],
+ 'payType' => !empty($withdrawal['payType']) ? $withdrawal['payType'] : null, // 支付类型:wechat、alipay、bankcard
+ 'applyDate' => !empty($withdrawal['applyTime']) ? date('Y/m/d', $withdrawal['applyTime']) : '',
+ 'reviewDate' => !empty($withdrawal['reviewTime']) ? date('Y-m-d H:i:s', $withdrawal['reviewTime']) : null,
+ 'reviewer' => !empty($withdrawal['reviewer']) ? $withdrawal['reviewer'] : null,
+ 'remark' => !empty($withdrawal['remark']) ? $withdrawal['remark'] : null,
+ ];
+
+ return json([
+ 'code' => 200,
+ 'success' => true,
+ 'msg' => '获取成功',
+ 'data' => $result
+ ]);
+
+ } catch (Exception $e) {
+ return json([
+ 'code' => $e->getCode() ?: 500,
+ 'success' => false,
+ 'msg' => '获取详情失败:' . $e->getMessage(),
+ 'data' => null
+ ]);
+ }
+ }
+}
+
diff --git a/application/cunkebao/controller/friend/GetFriendListV1Controller.php b/application/cunkebao/controller/friend/GetFriendListV1Controller.php
new file mode 100644
index 0000000..4ba300f
--- /dev/null
+++ b/application/cunkebao/controller/friend/GetFriendListV1Controller.php
@@ -0,0 +1,211 @@
+request->param('page',1);
+ $limit = $this->request->param('limit',20);
+ $keyword = $this->request->param('keyword','');
+ $deviceIds = $this->request->param('deviceIds','');
+
+ if(!empty($deviceIds)){
+ $deviceIds = explode(',',$deviceIds);
+ }
+
+ try {
+
+ $where = [];
+ if ($this->getUserInfo('isAdmin') == 1) {
+ $where[] = ['isDeleted','=',0];
+ } else {
+ $where[] = ['isDeleted','=',0];
+ }
+
+ if(!empty($keyword)){
+ $where[] = ['nickname|alias|wechatId','like','%'.$keyword.'%'];
+ }
+
+ /* $wechatIds = Db::name('device')->alias('d')
+ ->join('device_wechat_login dwl','dwl.deviceId=d.id AND dwl.companyId='.$this->getUserInfo('companyId'))
+ ->where(['d.companyId' => $this->getUserInfo('companyId'),'d.deleteTime' => 0])
+ ->group('dwl.deviceId')
+ ->order('dwl.id desc');*/
+
+
+ $companyId = $this->getUserInfo('companyId');
+
+ $wechatIds = Db::name('device')->alias('d')
+ // 仅关联每个设备在 device_wechat_login 中的最新一条记录
+ ->join('(SELECT MAX(id) AS id, deviceId FROM ck_device_wechat_login WHERE companyId='.$companyId.' GROUP BY deviceId) dwl_max','dwl_max.deviceId = d.id')
+ ->join('device_wechat_login dwl','dwl.id = dwl_max.id')
+ ->where(['d.companyId' => $companyId,'d.deleteTime' => 0]);
+
+
+ if (!empty($deviceIds)){
+ $wechatIds = $wechatIds->where('d.id','in',$deviceIds);
+ }
+ $wechatIds = $wechatIds->column('dwl.wechatId');
+
+ $where[] = ['ownerWechatId','in',$wechatIds];
+
+ $data = Db::table('s2_wechat_friend')
+ ->field([
+ 'id', 'nickname', 'avatar', 'alias', 'wechatId',
+ 'gender', 'phone', 'createTime', 'updateTime', 'deleteTime',
+ 'ownerNickname', 'ownerAlias', 'ownerWechatId',
+ 'accountUserName', 'accountNickname', 'accountRealName'
+ ])
+ ->where($where);
+ $total = $data->count();
+ $list = $data->page($page, $limit)->order('id DESC')->select();
+
+ // 格式化时间字段和处理数据
+ $formattedList = [];
+ foreach ($list as $item) {
+ $formattedItem = [
+ 'id' => $item['id'],
+ 'nickname' => $item['nickname'] ?? '',
+ 'avatar' => $item['avatar'] ?? '',
+ 'alias' => $item['alias'] ?? '',
+ 'wechatId' => $item['wechatId'] ?? '',
+ 'gender' => $item['gender'] ?? 0,
+ 'phone' => $item['phone'] ?? '',
+ 'account' => $item['accountUserName'] ?? '',
+ 'username' => $item['accountRealName'] ?? '',
+ 'createTime' => !empty($item['createTime']) ? date('Y-m-d H:i:s', $item['createTime']) : '1970-01-01 08:00:00',
+ 'updateTime' => !empty($item['updateTime']) ? date('Y-m-d H:i:s', $item['updateTime']) : '1970-01-01 08:00:00',
+ 'deleteTime' => !empty($item['deleteTime']) ? date('Y-m-d H:i:s', $item['deleteTime']) : '1970-01-01 08:00:00',
+ 'ownerNickname' => $item['ownerNickname'] ?? '',
+ 'ownerAlias' => $item['ownerAlias'] ?? '',
+ 'ownerWechatId' => $item['ownerWechatId'] ?? '',
+ 'accountNickname' => $item['accountNickname'] ?? ''
+ ];
+ $formattedList[] = $formattedItem;
+ }
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $formattedList,
+ 'total' => $total,
+ 'companyId' => $this->getUserInfo('companyId')
+ ]
+ ]);
+ } catch (\Exception $e) {
+ return json([
+ 'code' => $e->getCode(),
+ 'msg' => $e->getMessage()
+ ]);
+ }
+ }
+
+ /**
+ * 好友转移
+ * @return \think\response\Json
+ */
+ public function transfer()
+ {
+ $friendId = $this->request->param('friendId', 0);
+ $toAccountId = $this->request->param('toAccountId', '');
+ $comment = $this->request->param('comment', '');
+ $companyId = $this->getUserInfo('companyId');
+
+ // 参数验证
+ if (empty($friendId)) {
+ return json([
+ 'code' => 400,
+ 'msg' => '好友ID不能为空'
+ ]);
+ }
+
+ if (empty($toAccountId)) {
+ return json([
+ 'code' => 400,
+ 'msg' => '目标账号ID不能为空'
+ ]);
+ }
+
+ try {
+ // 验证目标账号是否存在且属于当前公司
+ $accountInfo = Db::table('s2_company_account')
+ ->where('id', $toAccountId)
+ ->where('departmentId', $companyId)
+ ->field('id as accountId, userName as accountUserName, realName as accountRealName, nickname as accountNickname, tenantId')
+ ->find();
+
+ if (empty($accountInfo)) {
+ return json([
+ 'code' => 404,
+ 'msg' => '目标账号不存在'
+ ]);
+ }
+
+
+ // 调用 AutomaticAssign 进行好友转移
+ $automaticAssign = new AutomaticAssign();
+ $result = $automaticAssign->allotWechatFriend([
+ 'wechatFriendId' => $friendId,
+ 'toAccountId' => $toAccountId,
+ 'comment' => $comment,
+ 'notifyReceiver' => false,
+ 'optFrom' => 4
+ ], true);
+
+ $resultData = json_decode($result, true);
+
+ if (!empty($resultData) && $resultData['code'] == 200) {
+ // 转移成功后更新数据库
+ $updateData = [
+ 'accountId' => $accountInfo['accountId'],
+ 'accountUserName' => $accountInfo['accountUserName'],
+ 'accountRealName' => $accountInfo['accountRealName'],
+ 'accountNickname' => $accountInfo['accountNickname'],
+ 'updateTime' => time()
+ ];
+
+ Db::table('s2_wechat_friend')
+ ->where('id', $friendId)
+ ->update($updateData);
+
+ return json([
+ 'code' => 200,
+ 'msg' => '好友转移成功',
+ 'data' => [
+ 'friendId' => $friendId,
+ 'toAccountId' => $toAccountId
+ ]
+ ]);
+ } else {
+ return json([
+ 'code' => 500,
+ 'msg' => '好友转移失败:' . ($resultData['msg'] ?? '未知错误')
+ ]);
+ }
+
+ } catch (\Exception $e) {
+ return json([
+ 'code' => 500,
+ 'msg' => '好友转移失败:' . $e->getMessage()
+ ]);
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/plan/GetAddFriendPlanDetailV1Controller.php b/application/cunkebao/controller/plan/GetAddFriendPlanDetailV1Controller.php
new file mode 100644
index 0000000..3d262ab
--- /dev/null
+++ b/application/cunkebao/controller/plan/GetAddFriendPlanDetailV1Controller.php
@@ -0,0 +1,231 @@
+ '测试客户',
+ 'phone' => '18888888888',
+ 'apiKey' => $apiKey,
+ 'timestamp' => time()
+ ];
+
+ // 生成签名
+ $sign = $this->generateSignature($testParams, $apiKey);
+ $testParams['sign'] = $sign;
+
+ // 构建签名过程说明
+ $signParams = $testParams;
+ unset($signParams['sign'], $signParams['apiKey']);
+ ksort($signParams);
+ $signStr = implode('', array_values($signParams));
+
+ // 构建完整URL参数,不对中文进行编码
+ $urlParams = [];
+ foreach ($testParams as $key => $value) {
+ $urlParams[] = $key . '=' . $value;
+ }
+ $fullUrl = implode('&', $urlParams);
+
+ return [
+ 'apiKey' => $apiKey,
+ 'originalString' => $signStr,
+ 'sign' => $sign,
+ 'fullUrl' => $fullUrl
+ ];
+
+ } catch (\Exception $e) {
+ return [];
+ }
+ }
+
+ /**
+ * 获取计划详情
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ $planId = $this->request->param('planId');
+
+ if (empty($planId)) {
+ return ResponseHelper::error('计划ID不能为空', 400);
+ }
+
+ // 查询计划详情
+ $plan = Db::name('customer_acquisition_task')
+ ->where('id', $planId)
+ ->find();
+
+ if (!$plan) {
+ return ResponseHelper::error('计划不存在', 404);
+ }
+
+ // 解析JSON字段
+ $sceneConf = json_decode($plan['sceneConf'], true) ?: [];
+ $reqConf = json_decode($plan['reqConf'], true) ?: [];
+ $reqConf['deviceGroups'] = $reqConf['device'];
+ $msgConf = json_decode($plan['msgConf'], true) ?: [];
+ $tagConf = json_decode($plan['tagConf'], true) ?: [];
+
+ // 处理分销配置
+ $distributionConfig = $sceneConf['distribution'] ?? [
+ 'enabled' => false,
+ 'channels' => [],
+ 'customerRewardAmount' => 0,
+ 'addFriendRewardAmount' => 0,
+ ];
+
+ // 格式化分销配置(分转元,并获取渠道详情)
+ $distributionEnabled = !empty($distributionConfig['enabled']);
+ $distributionChannels = [];
+ if ($distributionEnabled && !empty($distributionConfig['channels'])) {
+ $channels = Db::name('distribution_channel')
+ ->where([
+ ['id', 'in', $distributionConfig['channels']],
+ ['deleteTime', '=', 0]
+ ])
+ ->field('id,code,name')
+ ->select();
+ $distributionChannels = array_map(function($channel) {
+ return [
+ 'id' => (int)$channel['id'],
+ 'code' => $channel['code'],
+ 'name' => $channel['name']
+ ];
+ }, $channels);
+ }
+
+ // 将分销配置添加到返回数据中
+ $sceneConf['distributionEnabled'] = $distributionEnabled;
+ $sceneConf['distributionChannels'] = $distributionChannels;
+ $sceneConf['customerRewardAmount'] = round(($distributionConfig['customerRewardAmount'] ?? 0) / 100, 2); // 分转元
+ $sceneConf['addFriendRewardAmount'] = round(($distributionConfig['addFriendRewardAmount'] ?? 0) / 100, 2); // 分转元
+
+
+
+ if(!empty($sceneConf['wechatGroups'])){
+ $groupList = Db::name('wechat_group')->alias('wg')
+ ->join('wechat_account wa', 'wa.wechatId = wg.ownerWechatId')
+ ->where('wg.id', 'in', $sceneConf['wechatGroups'])
+ ->order('wg.id', 'desc')
+ ->field('wg.id,wg.name,wg.chatroomId,wg.ownerWechatId,wa.nickName as ownerNickName,wa.avatar as ownerAvatar,wa.alias as ownerAlias,wg.avatar')
+ ->select();
+ $sceneConf['wechatGroupsOptions'] = $groupList;
+ }else{
+ $sceneConf['wechatGroupsOptions'] = [];
+ }
+
+
+ if (!empty($reqConf['deviceGroups'])){
+ $deviceGroupsOptions = DeviceModel::alias('d')
+ ->field([
+ 'd.id', 'd.imei', 'd.memo', 'd.alive',
+ 'l.wechatId',
+ 'a.nickname', 'a.alias', '0 totalFriend', '0 totalFriend'
+ ])
+ ->leftJoin('device_wechat_login l', 'd.id = l.deviceId and l.alive =' . DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE . ' and l.companyId = d.companyId')
+ ->leftJoin('wechat_account a', 'l.wechatId = a.wechatId')
+ ->order('d.id desc')
+ ->whereIn('d.id',$reqConf['deviceGroups'])
+ ->select();
+ foreach ($deviceGroupsOptions as &$device) {
+ $curstomer = WechatCustomerModel::field('friendShip')->where(['wechatId' => $device['wechatId']])->find();
+ $device['totalFriend'] = $curstomer->friendShip->totalFriend ?? 0;
+ }
+ unset($device);
+ $reqConf['deviceGroupsOptions'] = $deviceGroupsOptions;
+ }else{
+ $reqConf['deviceGroupsOptions'] = [];
+ }
+
+
+
+
+ unset(
+ $reqConf['device'],
+ $sceneConf['groupSelected'],
+ );
+
+ // 合并数据
+ $newData['messagePlans'] = $msgConf;
+ $newData = array_merge($newData, $sceneConf, $reqConf, $tagConf, $plan);
+
+ // 移除不需要的字段
+ unset(
+ $newData['sceneConf'],
+ $newData['reqConf'],
+ $newData['msgConf'],
+ $newData['tagConf'],
+ $newData['userInfo'],
+ $newData['createTime'],
+ $newData['updateTime'],
+ $newData['deleteTime']
+ );
+
+ // 生成测试URL
+ $newData['textUrl'] = $this->testUrl($newData['apiKey']);
+
+ return ResponseHelper::success($newData, '获取计划详情成功');
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500);
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/plan/GetCreateAddFriendPlanV1Controller.php b/application/cunkebao/controller/plan/GetCreateAddFriendPlanV1Controller.php
new file mode 100644
index 0000000..6941a8d
--- /dev/null
+++ b/application/cunkebao/controller/plan/GetCreateAddFriendPlanV1Controller.php
@@ -0,0 +1,135 @@
+ 0 ? '-' : '') . $segment;
+ }
+
+ // 检查是否已存在
+ $exists = Db::name('customer_acquisition_task')
+ ->where('apiKey', $apiKey)
+ ->find();
+
+ if ($exists) {
+ // 如果已存在,递归重新生成
+ return $this->generateApiKey();
+ }
+
+ return $apiKey;
+ }
+
+ /**
+ * 拷贝计划任务
+ *
+ * @return \think\response\Json
+ */
+ public function copy()
+ {
+ try {
+ $params = $this->request->param();
+ $planId = isset($params['planId']) ? intval($params['planId']) : 0;
+
+ if ($planId <= 0) {
+ return ResponseHelper::error('计划ID不能为空', 400);
+ }
+
+ $plan = Db::name('customer_acquisition_task')->where('id', $planId)->find();
+ if (!$plan) {
+ return ResponseHelper::error('计划不存在', 404);
+ }
+
+ unset($plan['id']);
+ $plan['name'] = $plan['name'] . ' (拷贝)';
+ $plan['createTime'] = time();
+ $plan['updateTime'] = time();
+ $plan['apiKey'] = $this->generateApiKey(); // 生成新的API密钥
+
+ $newPlanId = Db::name('customer_acquisition_task')->insertGetId($plan);
+ if (!$newPlanId) {
+ return ResponseHelper::error('拷贝计划失败', 500);
+ }
+
+ return ResponseHelper::success(['planId' => $newPlanId], '拷贝计划任务成功');
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500);
+ }
+ }
+
+ /**
+ * 删除计划任务
+ *
+ * @return \think\response\Json
+ */
+ public function delete()
+ {
+ try {
+ $params = $this->request->param();
+ $planId = isset($params['planId']) ? intval($params['planId']) : 0;
+
+ if ($planId <= 0) {
+ return ResponseHelper::error('计划ID不能为空', 400);
+ }
+
+ $result = Db::name('customer_acquisition_task')->where('id', $planId)->update(['deleteTime' => time()]);
+ if (!$result) {
+ return ResponseHelper::error('删除计划失败', 500);
+ }
+
+ return ResponseHelper::success([], '删除计划任务成功');
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500);
+ }
+ }
+
+ /**
+ * 修改计划任务状态
+ *
+ * @return \think\response\Json
+ */
+ public function updateStatus()
+ {
+ try {
+ $params = $this->request->param();
+ $planId = isset($params['planId']) ? intval($params['planId']) : 0;
+ $status = isset($params['status']) ? intval($params['status']) : 0;
+
+ if ($planId <= 0) {
+ return ResponseHelper::error('计划ID不能为空', 400);
+ }
+
+ $result = Db::name('customer_acquisition_task')->where('id', $planId)->update(['status' => $status, 'updateTime' => time()]);
+ if (!$result) {
+ return ResponseHelper::error('修改计划状态失败', 500);
+ }
+
+ return ResponseHelper::success([], '修改计划任务状态成功');
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500);
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/plan/GetPlanSceneListV1Controller.php b/application/cunkebao/controller/plan/GetPlanSceneListV1Controller.php
new file mode 100644
index 0000000..887bfee
--- /dev/null
+++ b/application/cunkebao/controller/plan/GetPlanSceneListV1Controller.php
@@ -0,0 +1,294 @@
+ PlansSceneModel::STATUS_ACTIVE];
+
+ // 搜索条件
+ if (!empty($params['keyword'])) {
+ $where[] = ['name', 'like', '%' . $params['keyword'] . '%'];
+ }
+
+ // 标签筛选
+ if (!empty($params['tag'])) {
+ $where[] = ['scenarioTags', 'like', '%' . $params['tag'] . '%'];
+ }
+
+ // 查询数据
+ $query = PlansSceneModel::where($where);
+
+ // 获取分页数据
+ $list = $query->order('sort DESC')->select()->toArray();
+
+ if (empty($list)) {
+ return [];
+ }
+
+ $sceneIds = array_column($list, 'id');
+ $companyId = $this->getUserInfo('companyId');
+ $statsMap = $this->buildSceneStats($sceneIds, (int)$companyId);
+
+ // 处理数据
+ foreach($list as &$val) {
+ $val['scenarioTags'] = json_decode($val['scenarioTags'], true) ?: [];
+ $sceneStats = $statsMap[$val['id']] ?? ['count' => 0, 'growth' => '0%'];
+ $val['count'] = $sceneStats['count'];
+ $val['growth'] = $sceneStats['growth'];
+ }
+ unset($val);
+
+ return $list;
+
+ } catch (\Exception $e) {
+ throw new \Exception('获取场景列表失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 获取场景列表
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ $params = $this->request->param();
+ $result = $this->getSceneList($params);
+ return ResponseHelper::success($result);
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), 500);
+ }
+ }
+
+ /**
+ * 获取场景详情
+ *
+ * @return \think\response\Json
+ */
+ public function detail()
+ {
+ try {
+ $id = $this->request->param('id', '');
+ if(empty($id)) {
+ return ResponseHelper::error('参数缺失');
+ }
+
+ $data = PlansSceneModel::where([
+ 'status' => PlansSceneModel::STATUS_ACTIVE,
+ 'id' => $id
+ ])->find();
+
+ if(empty($data)) {
+ return ResponseHelper::error('场景不存在');
+ }
+
+ $data['scenarioTags'] = json_decode($data['scenarioTags'], true) ?: [];
+ $data['count'] = $this->getPlanCount($id);
+ $data['growth'] = $this->calculateGrowth($id);
+
+ return ResponseHelper::success($data);
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), 500);
+ }
+ }
+
+ /**
+ * 获取计划数量
+ *
+ * @param int $sceneId 场景ID
+ * @return int
+ */
+ private function getPlanCount(int $sceneId): int
+ {
+ return Db::name('customer_acquisition_task')
+ ->where('sceneId', $sceneId)
+ ->where('companyId',$this->getUserInfo('companyId'))
+ ->where('deleteTime', 0)
+ ->count();
+ }
+
+ /**
+ * 计算增长率
+ *
+ * @param int $sceneId 场景ID
+ * @return string
+ */
+ private function calculateGrowth(int $sceneId): string
+ {
+ $companyId = $this->getUserInfo('companyId');
+ $currentStart = strtotime(date('Y-m-01 00:00:00'));
+ $nextMonthStart = strtotime(date('Y-m-01 00:00:00', strtotime('+1 month')));
+ $lastMonthStart = strtotime(date('Y-m-01 00:00:00', strtotime('-1 month')));
+
+ $currentMonth = $this->getSceneMonthlyCount($sceneId, $companyId, $currentStart, $nextMonthStart - 1);
+ $lastMonth = $this->getSceneMonthlyCount($sceneId, $companyId, $lastMonthStart, $currentStart - 1);
+
+ return $this->formatGrowthPercentage($currentMonth, $lastMonth);
+ }
+
+ /**
+ * 批量构建场景统计数据
+ * @param array $sceneIds
+ * @param int $companyId
+ * @return array
+ */
+ private function buildSceneStats(array $sceneIds, int $companyId): array
+ {
+ if (empty($sceneIds)) {
+ return [];
+ }
+
+ $totalCounts = $this->getSceneTaskCounts($sceneIds, $companyId);
+
+ $currentStart = strtotime(date('Y-m-01 00:00:00'));
+ $nextMonthStart = strtotime(date('Y-m-01 00:00:00', strtotime('+1 month')));
+ $lastMonthStart = strtotime(date('Y-m-01 00:00:00', strtotime('-1 month')));
+
+ $currentMonthCounts = $this->getSceneMonthlyCounts($sceneIds, $companyId, $currentStart, $nextMonthStart - 1);
+ $lastMonthCounts = $this->getSceneMonthlyCounts($sceneIds, $companyId, $lastMonthStart, $currentStart - 1);
+
+ $stats = [];
+ foreach ($sceneIds as $sceneId) {
+ $current = $currentMonthCounts[$sceneId] ?? 0;
+ $last = $lastMonthCounts[$sceneId] ?? 0;
+ $stats[$sceneId] = [
+ 'count' => $totalCounts[$sceneId] ?? 0,
+ 'growth' => $this->formatGrowthPercentage($current, $last),
+ ];
+ }
+
+ return $stats;
+ }
+
+ /**
+ * 获取场景计划总数
+ * @param array $sceneIds
+ * @param int $companyId
+ * @return array
+ */
+ private function getSceneTaskCounts(array $sceneIds, int $companyId): array
+ {
+ if (empty($sceneIds)) {
+ return [];
+ }
+
+
+ $where = [
+ ['companyId', '=', $companyId],
+ ['deleteTime', '=', 0],
+ ['sceneId', 'in', $sceneIds],
+ ];
+ if(!$this->getUserInfo('isAdmin')){
+ $where[] = ['userId', '=', $this->getUserInfo('id')];
+ }
+
+
+ $rows = Db::name('customer_acquisition_task')
+ ->where($where)
+ ->field('sceneId, COUNT(*) as total')
+ ->group('sceneId')
+ ->select();
+
+ $result = [];
+ foreach ($rows as $row) {
+ $sceneId = is_array($row) ? ($row['sceneId'] ?? 0) : ($row->sceneId ?? 0);
+ if (!$sceneId) {
+ continue;
+ }
+ $result[$sceneId] = (int)(is_array($row) ? ($row['total'] ?? 0) : ($row->total ?? 0));
+ }
+
+ return $result;
+ }
+
+ /**
+ * 获取场景月度数据
+ * @param array $sceneIds
+ * @param int $companyId
+ * @param int $startTime
+ * @param int $endTime
+ * @return array
+ */
+ private function getSceneMonthlyCounts(array $sceneIds, int $companyId, int $startTime, int $endTime): array
+ {
+ if (empty($sceneIds)) {
+ return [];
+ }
+
+ $rows = Db::name('customer_acquisition_task')
+ ->whereIn('sceneId', $sceneIds)
+ ->where('companyId', $companyId)
+ ->where('status', 1)
+ ->where('deleteTime', 0)
+ ->whereBetween('createTime', [$startTime, $endTime])
+ ->field('sceneId, COUNT(*) as total')
+ ->group('sceneId')
+ ->select();
+
+ $result = [];
+ foreach ($rows as $row) {
+ $sceneId = is_array($row) ? ($row['sceneId'] ?? 0) : ($row->sceneId ?? 0);
+ if (!$sceneId) {
+ continue;
+ }
+ $result[$sceneId] = (int)(is_array($row) ? ($row['total'] ?? 0) : ($row->total ?? 0));
+ }
+
+ return $result;
+ }
+
+ /**
+ * 获取单个场景的月度数据
+ * @param int $sceneId
+ * @param int $companyId
+ * @param int $startTime
+ * @param int $endTime
+ * @return int
+ */
+ private function getSceneMonthlyCount(int $sceneId, int $companyId, int $startTime, int $endTime): int
+ {
+ return Db::name('customer_acquisition_task')
+ ->where('sceneId', $sceneId)
+ ->where('companyId', $companyId)
+ ->where('status', 1)
+ ->where('deleteTime', 0)
+ ->whereBetween('createTime', [$startTime, $endTime])
+ ->count();
+ }
+
+ /**
+ * 计算增长百分比
+ * @param int $current
+ * @param int $last
+ * @return string
+ */
+ private function formatGrowthPercentage(int $current, int $last): string
+ {
+ if ($last == 0) {
+ return $current > 0 ? '100%' : '0%';
+ }
+
+ $growth = round(($current - $last) / $last * 100, 2);
+ return $growth . '%';
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/plan/PlanSceneV1Controller.php b/application/cunkebao/controller/plan/PlanSceneV1Controller.php
new file mode 100644
index 0000000..3812e56
--- /dev/null
+++ b/application/cunkebao/controller/plan/PlanSceneV1Controller.php
@@ -0,0 +1,554 @@
+request->param();
+ $page = isset($params['page']) ? intval($params['page']) : 1;
+ $limit = isset($params['limit']) ? intval($params['limit']) : 10;
+ $keyword = isset($params['keyword']) ? trim($params['keyword']) : '';
+ $sceneId = $this->request->param('sceneId','');
+ $where = [
+ 'deleteTime' => 0,
+ 'companyId' => $this->getUserInfo('companyId'),
+ ];
+
+ if(!$this->getUserInfo('isAdmin')){
+ $where['userId'] = $this->getUserInfo('id');
+ }
+
+ if(!empty($sceneId)){
+ $where['sceneId'] = $sceneId;
+ }
+
+ if(!empty($keyword)){
+ $where[] = ['name', 'like', '%' . $keyword . '%'];
+ }
+
+
+ $total = Db::name('customer_acquisition_task')->where($where)->count();
+ $list = Db::name('customer_acquisition_task')
+ ->where($where)
+ ->order('createTime', 'desc')
+ ->page($page, $limit)
+ ->select();
+
+ if (!empty($list)) {
+ $taskIds = array_column($list, 'id');
+ $statsMap = $this->buildTaskStats($taskIds);
+
+ foreach($list as &$val){
+ $val['createTime'] = !empty($val['createTime']) ? date('Y-m-d H:i:s', $val['createTime']) : '';
+ $val['updateTime'] = !empty($val['updateTime']) ? date('Y-m-d H:i:s', $val['updateTime']) : '';
+ $val['sceneConf'] = json_decode($val['sceneConf'],true) ?: [];
+ $val['reqConf'] = json_decode($val['reqConf'],true) ?: [];
+ $val['msgConf'] = json_decode($val['msgConf'],true) ?: [];
+ $val['tagConf'] = json_decode($val['tagConf'],true) ?: [];
+
+ $stats = $statsMap[$val['id']] ?? [
+ 'acquiredCount' => 0,
+ 'addedCount' => 0,
+ 'passCount' => 0,
+ 'lastUpdated' => 0
+ ];
+
+ $val['acquiredCount'] = $stats['acquiredCount'];
+ $val['addedCount'] = $stats['addedCount'];
+ $val['passCount'] = $stats['passCount'];
+ $val['passRate'] = ($stats['addedCount'] > 0 && $stats['passCount'] > 0)
+ ? number_format(($stats['passCount'] / $stats['addedCount']) * 100, 2)
+ : 0;
+ $val['lastUpdated'] = !empty($stats['lastUpdated']) ? date('Y-m-d H:i', $stats['lastUpdated']) : '--';
+ }
+ unset($val);
+ }
+ return ResponseHelper::success([
+ 'total' => $total,
+ 'list' => $list
+ ], '获取计划任务列表成功');
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500);
+ }
+ }
+
+
+ /**
+ * 删除计划任务
+ *
+ * @return \think\response\Json
+ */
+ public function delete()
+ {
+ try {
+ $params = $this->request->param();
+ $planId = isset($params['planId']) ? intval($params['planId']) : 0;
+
+ if ($planId <= 0) {
+ return ResponseHelper::error('计划ID不能为空', 400);
+ }
+
+ $result = Db::name('customer_acquisition_task')->where('id', $planId)->update(['deleteTime' => time()]);
+ if (!$result) {
+ return ResponseHelper::error('删除计划失败', 500);
+ }
+
+ return ResponseHelper::success([], '删除计划任务成功');
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500);
+ }
+ }
+
+ /**
+ * 修改计划任务状态
+ *
+ * @return \think\response\Json
+ */
+ public function updateStatus()
+ {
+ try {
+ $params = $this->request->param();
+ $planId = isset($params['planId']) ? intval($params['planId']) : 0;
+ $status = isset($params['status']) ? intval($params['status']) : 0;
+
+ if ($planId <= 0) {
+ return ResponseHelper::error('计划ID不能为空', 400);
+ }
+
+ $result = Db::name('customer_acquisition_task')->where('id', $planId)->update(['status' => $status, 'updateTime' => time()]);
+ if (!$result) {
+ return ResponseHelper::error('修改计划状态失败', 500);
+ }
+
+ return ResponseHelper::success([], '修改计划任务状态成功');
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500);
+ }
+ }
+
+ /**
+ * 获取获客计划设备列表
+ *
+ * @return \think\response\Json
+ */
+ public function getPlanDevices()
+ {
+ try {
+ $params = $this->request->param();
+ $planId = isset($params['planId']) ? intval($params['planId']) : 0;
+ $page = isset($params['page']) ? intval($params['page']) : 1;
+ $limit = isset($params['limit']) ? intval($params['limit']) : 10;
+ $deviceStatus = isset($params['deviceStatus']) ? $params['deviceStatus'] : '';
+ $searchKeyword = isset($params['searchKeyword']) ? trim($params['searchKeyword']) : '';
+
+ // 验证计划ID
+ if ($planId <= 0) {
+ return ResponseHelper::error('计划ID不能为空', 400);
+ }
+
+ // 验证计划是否存在且用户有权限
+ $plan = Db::name('customer_acquisition_task')
+ ->where([
+ 'id' => $planId,
+ 'deleteTime' => 0,
+ 'companyId' => $this->getUserInfo('companyId')
+ ])
+ ->find();
+
+ if (!$plan) {
+ return ResponseHelper::error('计划不存在或无权限访问', 404);
+ }
+
+ // 如果是管理员,需要验证用户权限
+ if (!$this->getUserInfo('isAdmin')) {
+ $userPlan = Db::name('customer_acquisition_task')
+ ->where([
+ 'id' => $planId,
+ 'userId' => $this->getUserInfo('id')
+ ])
+ ->find();
+
+ if (!$userPlan) {
+ return ResponseHelper::error('您没有权限访问该计划', 403);
+ }
+ }
+
+ // 构建查询条件
+ $where = [
+ 'pt.plan_id' => $planId,
+ 'd.deleteTime' => 0,
+ 'd.companyId' => $this->getUserInfo('companyId')
+ ];
+
+ // 设备状态筛选
+ if (!empty($deviceStatus)) {
+ $where['d.alive'] = $deviceStatus;
+ }
+
+ // 搜索关键词
+ $searchWhere = [];
+ if (!empty($searchKeyword)) {
+ $searchWhere[] = ['d.imei', 'like', "%{$searchKeyword}%"];
+ $searchWhere[] = ['d.memo', 'like', "%{$searchKeyword}%"];
+ }
+
+ // 查询设备总数
+ $totalQuery = Db::name('plan_task_device')->alias('pt')
+ ->join('device d', 'pt.device_id = d.id')
+ ->where($where);
+
+ if (!empty($searchWhere)) {
+ $totalQuery->where(function ($query) use ($searchWhere) {
+ foreach ($searchWhere as $condition) {
+ $query->whereOr($condition[0], $condition[1], $condition[2]);
+ }
+ });
+ }
+
+ $total = $totalQuery->count();
+
+ // 查询设备列表
+ $listQuery = Db::name('plan_task_device')->alias('pt')
+ ->join('device d', 'pt.device_id = d.id')
+ ->field([
+ 'd.id',
+ 'd.imei',
+ 'd.memo',
+ 'd.alive',
+ 'd.extra',
+ 'd.createTime',
+ 'd.updateTime',
+ 'pt.status as plan_device_status',
+ 'pt.createTime as assign_time'
+ ])
+ ->where($where)
+ ->order('pt.createTime', 'desc');
+
+ if (!empty($searchWhere)) {
+ $listQuery->where(function ($query) use ($searchWhere) {
+ foreach ($searchWhere as $condition) {
+ $query->whereOr($condition[0], $condition[1], $condition[2]);
+ }
+ });
+ }
+
+ $list = $listQuery->page($page, $limit)->select();
+
+ // 处理设备数据
+ foreach ($list as &$device) {
+ // 格式化时间
+ $device['createTime'] = date('Y-m-d H:i:s', $device['createTime']);
+ $device['updateTime'] = date('Y-m-d H:i:s', $device['updateTime']);
+ $device['assign_time'] = date('Y-m-d H:i:s', $device['assign_time']);
+
+ // 解析设备额外信息
+ if (!empty($device['extra'])) {
+ $extra = json_decode($device['extra'], true);
+ $device['battery'] = isset($extra['battery']) ? intval($extra['battery']) : 0;
+ $device['device_info'] = $extra;
+ } else {
+ $device['battery'] = 0;
+ $device['device_info'] = [];
+ }
+
+ // 设备状态文本
+ $device['alive_text'] = $this->getDeviceStatusText($device['alive']);
+ $device['plan_device_status_text'] = $this->getPlanDeviceStatusText($device['plan_device_status']);
+
+ // 获取设备当前微信登录信息
+ $wechatLogin = Db::name('device_wechat_login')
+ ->where([
+ 'deviceId' => $device['id'],
+ 'companyId' => $this->getUserInfo('companyId'),
+ 'alive' => 1
+ ])
+ ->order('createTime', 'desc')
+ ->find();
+
+ $device['current_wechat'] = $wechatLogin ? [
+ 'wechatId' => $wechatLogin['wechatId'],
+ 'nickname' => $wechatLogin['nickname'] ?? '',
+ 'loginTime' => date('Y-m-d H:i:s', $wechatLogin['createTime'])
+ ] : null;
+
+ // 获取设备在该计划中的任务统计
+ $device['task_stats'] = $this->getDeviceTaskStats($device['id'], $planId);
+
+ // 移除原始extra字段
+ unset($device['extra']);
+ }
+ unset($device);
+
+ return ResponseHelper::success([
+ 'total' => $total,
+ 'list' => $list,
+ 'plan_info' => [
+ 'id' => $plan['id'],
+ 'name' => $plan['name'],
+ 'status' => $plan['status']
+ ]
+ ], '获取计划设备列表成功');
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500);
+ }
+ }
+
+ /**
+ * 获取设备状态文本
+ *
+ * @param int $status
+ * @return string
+ */
+ private function getDeviceStatusText($status)
+ {
+ $statusMap = [
+ 0 => '离线',
+ 1 => '在线',
+ 2 => '忙碌',
+ 3 => '故障'
+ ];
+ return isset($statusMap[$status]) ? $statusMap[$status] : '未知';
+ }
+
+ /**
+ * 获取计划设备状态文本
+ *
+ * @param int $status
+ * @return string
+ */
+ private function getPlanDeviceStatusText($status)
+ {
+ $statusMap = [
+ 0 => '待分配',
+ 1 => '已分配',
+ 2 => '执行中',
+ 3 => '已完成',
+ 4 => '已暂停',
+ 5 => '已取消'
+ ];
+ return isset($statusMap[$status]) ? $statusMap[$status] : '未知';
+ }
+
+ /**
+ * 获取设备在指定计划中的任务统计
+ *
+ * @param int $deviceId
+ * @param int $planId
+ * @return array
+ */
+ private function getDeviceTaskStats($deviceId, $planId)
+ {
+ // 获取该设备在计划中的任务总数
+ $totalTasks = Db::name('task_customer')
+ ->where([
+ 'task_id' => $planId,
+ 'device_id' => $deviceId
+ ])
+ ->count();
+
+ // 获取已完成的任务数
+ $completedTasks = Db::name('task_customer')
+ ->where([
+ 'task_id' => $planId,
+ 'device_id' => $deviceId,
+ 'status' => 4
+ ])
+ ->count();
+
+ // 获取进行中的任务数
+ $processingTasks = Db::name('task_customer')
+ ->where([
+ 'task_id' => $planId,
+ 'device_id' => $deviceId,
+ 'status' => ['in', [1, 2, 3]]
+ ])
+ ->count();
+
+ return [
+ 'total_tasks' => $totalTasks,
+ 'completed_tasks' => $completedTasks,
+ 'processing_tasks' => $processingTasks,
+ 'completion_rate' => $totalTasks > 0 ? round(($completedTasks / $totalTasks) * 100, 2) : 0
+ ];
+ }
+
+ /**
+ * 构建任务统计
+ * @param array $taskIds
+ * @return array
+ */
+ private function buildTaskStats(array $taskIds): array
+ {
+ if (empty($taskIds)) {
+ return [];
+ }
+
+ $rows = Db::name('task_customer')
+ ->whereIn('task_id', $taskIds)
+ ->field([
+ 'task_id as taskId',
+ 'COUNT(1) as acquiredCount',
+ "SUM(CASE WHEN status IN (1,2,3,4,5) THEN 1 ELSE 0 END) as addedCount",
+ "SUM(CASE WHEN status IN (4,5) THEN 1 ELSE 0 END) as passCount",
+ 'MAX(updateTime) as lastUpdated'
+ ])
+ ->group('task_id')
+ ->select();
+
+ $stats = [];
+ foreach ($rows as $row) {
+ $taskId = is_array($row) ? ($row['taskId'] ?? 0) : ($row->taskId ?? 0);
+ if (!$taskId) {
+ continue;
+ }
+ $stats[$taskId] = [
+ 'acquiredCount' => (int)(is_array($row) ? ($row['acquiredCount'] ?? 0) : ($row->acquiredCount ?? 0)),
+ 'addedCount' => (int)(is_array($row) ? ($row['addedCount'] ?? 0) : ($row->addedCount ?? 0)),
+ 'passCount' => (int)(is_array($row) ? ($row['passCount'] ?? 0) : ($row->passCount ?? 0)),
+ 'lastUpdated' => (int)(is_array($row) ? ($row['lastUpdated'] ?? 0) : ($row->lastUpdated ?? 0)),
+ ];
+ }
+
+ return $stats;
+ }
+
+
+ /**
+ * 获取微信小程序码
+ * @return \think\response\Json
+ * @throws \think\db\exception\DataNotFoundException
+ * @throws \think\db\exception\ModelNotFoundException
+ * @throws \think\exception\DbException
+ */
+ public function getWxMinAppCode()
+ {
+ $params = $this->request->param();
+ $taskId = isset($params['taskId']) ? intval($params['taskId']) : 0;
+ $channelId = isset($params['channelId']) ? intval($params['channelId']) : 0;
+
+ if($taskId <= 0) {
+ return ResponseHelper::error('任务ID或场景ID不能为空', 400);
+ }
+
+ $task = Db::name('customer_acquisition_task')->where(['id' => $taskId, 'deleteTime' => 0])->find();
+ if(!$task) {
+ return ResponseHelper::error('任务不存在', 400);
+ }
+
+ // 如果提供了channelId,验证渠道是否存在且有效
+ if ($channelId > 0) {
+ $channel = Db::name('distribution_channel')
+ ->where([
+ ['id', '=', $channelId],
+ ['companyId', '=', $task['companyId']],
+ ['status', '=', 'enabled'],
+ ['deleteTime', '=', 0]
+ ])
+ ->find();
+
+ if (!$channel) {
+ return ResponseHelper::error('分销渠道不存在或已被禁用', 400);
+ }
+ }
+
+ $posterWeChatMiniProgram = new PosterWeChatMiniProgram();
+ $result = $posterWeChatMiniProgram->generateMiniProgramCodeWithScene($taskId, $channelId);
+ $result = json_decode($result, true);
+ if ($result['code'] == 200){
+ return ResponseHelper::success($result['data'], '获取小程序码成功');
+ }else{
+ return ResponseHelper::error('获取小程序失败:' . $result['msg']);
+ }
+
+ }
+
+
+ /**
+ * 获取已获客/已添加用户
+ * @return \think\response\Json
+ * @throws \think\db\exception\DataNotFoundException
+ * @throws \think\db\exception\ModelNotFoundException
+ * @throws \think\exception\DbException
+ */
+ public function getUserList(){
+ $type = $this->request->param('type',1);
+ $planId = $this->request->param('planId','');
+ $page = $this->request->param('page',1);
+ $pageSize = $this->request->param('pageSize',10);
+ $keyword = $this->request->param('keyword','');
+
+ if (!in_array($type, [1, 2])) {
+ return ResponseHelper::error('类型错误');
+ }
+
+ if (empty($planId)){
+ return ResponseHelper::error('获客场景id不能为空');
+ }
+
+ $task = Db::name('customer_acquisition_task')
+ ->where(['id' => $planId, 'deleteTime' => 0,'companyId' => $this->getUserInfo('companyId')])
+ ->find();
+ if(empty($task)) {
+ return ResponseHelper::error('活动不存在');
+ }
+ $query = Db::name('task_customer')->where(['task_id' => $task['id']]);
+
+ if ($type == 2){
+ $query = $query->whereIn('status',[4,5]);
+ }
+
+ if (!empty($keyword)) {
+ $query = $query->where('name|phone|tags|siteTags', 'like', '%' . $keyword . '%');
+ }
+
+ $total = $query->count();
+ $list = $query->page($page, $pageSize)->order('id', 'desc')->select();
+ foreach ($list as &$item) {
+ unset($item['processed_wechat_ids'],$item['task_id']);
+ $userinfo = Db::table('s2_wechat_friend')
+ ->field('alias,wechatId,nickname,avatar')
+ ->where('alias|wechatId|phone|conRemark','like','%'.$item['phone'].'%')
+ ->order('id DESC')
+ ->find();
+
+ if (!empty($userinfo)) {
+ $item['userinfo'] = $userinfo;
+ }else{
+ $item['userinfo'] = [];
+ }
+
+ $item['tags'] = json_decode($item['tags'], true);
+ $item['siteTags'] = json_decode($item['siteTags'], true);
+ $item['createTime'] = !empty($item['createTime']) ? date('Y-m-d H:i:s', $item['createTime']) : '';
+ $item['updateTime'] = !empty($item['updateTime']) ? date('Y-m-d H:i:s', $item['updateTime']) : '';
+ }
+
+
+
+ $data = [
+ 'total' => $total,
+ 'list' => $list,
+ ];
+ return ResponseHelper::success($data,'获取成功');
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php b/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php
new file mode 100644
index 0000000..e7bdb52
--- /dev/null
+++ b/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php
@@ -0,0 +1,412 @@
+ 0 ? '-' : '') . $segment;
+ }
+
+ // 检查是否已存在
+ $exists = Db::name('customer_acquisition_task')
+ ->where('apiKey', $apiKey)
+ ->find();
+
+ if ($exists) {
+ // 如果已存在,递归重新生成
+ return $this->generateApiKey();
+ }
+
+ return $apiKey;
+ }
+
+ /**
+ * 添加计划任务
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ $params = $this->request->param();
+
+ // 验证必填字段
+ if (empty($params['name'])) {
+ return ResponseHelper::error('计划名称不能为空', 400);
+ }
+
+ if (empty($params['sceneId'])) {
+ return ResponseHelper::error('场景ID不能为空', 400);
+ }
+
+ if (empty($params['deviceGroups'])) {
+ return ResponseHelper::error('请选择设备', 400);
+ }
+
+ $companyId = $this->getUserInfo('companyId');
+
+ // 处理分销配置
+ $distributionConfig = $this->processDistributionConfig($params, $companyId);
+
+ // 归类参数
+ $msgConf = isset($params['messagePlans']) ? $params['messagePlans'] : [];
+ $tagConf = [
+ 'scenarioTags' => $params['scenarioTags'] ?? [],
+ 'customTags' => $params['customTags'] ?? [],
+ ];
+ $reqConf = [
+ 'device' => $params['deviceGroups'] ?? [],
+ 'remarkType' => $params['remarkType'] ?? '',
+ 'greeting' => $params['greeting'] ?? '',
+ 'addFriendInterval' => $params['addFriendInterval'] ?? '',
+ 'startTime' => $params['startTime'] ?? '',
+ 'endTime' => $params['endTime'] ?? '',
+ ];
+ // 其余参数归为sceneConf
+ $sceneConf = $params;
+ unset(
+ $sceneConf['id'],
+ $sceneConf['apiKey'],
+ $sceneConf['userId'],
+ $sceneConf['status'],
+ $sceneConf['planId'],
+ $sceneConf['name'],
+ $sceneConf['sceneId'],
+ $sceneConf['messagePlans'],
+ $sceneConf['scenarioTags'],
+ $sceneConf['customTags'],
+ $sceneConf['device'],
+ $sceneConf['orderTableFileName'],
+ $sceneConf['userInfo'],
+ $sceneConf['textUrl'],
+ $sceneConf['remarkType'],
+ $sceneConf['greeting'],
+ $sceneConf['addFriendInterval'],
+ $sceneConf['startTime'],
+ $sceneConf['orderTableFile'],
+ $sceneConf['endTime'],
+ $sceneConf['distributionEnabled'],
+ $sceneConf['distributionChannels'],
+ $sceneConf['customerRewardAmount'],
+ $sceneConf['addFriendRewardAmount']
+ );
+
+ // 将分销配置添加到sceneConf中
+ $sceneConf['distribution'] = $distributionConfig;
+
+ // 构建数据
+ $data = [
+ 'name' => $params['name'],
+ 'sceneId' => $params['sceneId'],
+ 'sceneConf' => json_encode($sceneConf, JSON_UNESCAPED_UNICODE),
+ 'reqConf' => json_encode($reqConf, JSON_UNESCAPED_UNICODE),
+ 'msgConf' => json_encode($msgConf, JSON_UNESCAPED_UNICODE),
+ 'tagConf' => json_encode($tagConf, JSON_UNESCAPED_UNICODE),
+ 'userId' => $this->getUserInfo('id'),
+ 'companyId' => $this->getUserInfo('companyId'),
+ 'status' => !empty($params['status']) ? 1 : 0,
+ 'apiKey' => $this->generateApiKey(), // 生成API密钥
+ 'createTime' => time(),
+ 'updateTime' => time(),
+ ];
+
+
+ try {
+ Db::startTrans();
+ // 插入数据
+ $planId = Db::name('customer_acquisition_task')->insertGetId($data);
+
+ if (!$planId) {
+ throw new \Exception('添加计划失败');
+ }
+
+
+ //订单
+ if ($params['sceneId'] == 2) {
+ if (!empty($params['orderFileUrl'])) {
+ // 先下载到本地临时文件,再分析,最后删除
+ $originPath = $params['orderFileUrl'];
+ $tmpFile = tempnam(sys_get_temp_dir(), 'order_');
+ // 判断是否为远程文件
+ if (preg_match('/^https?:\/\//i', $originPath)) {
+ // 远程URL,下载到本地
+ $fileContent = file_get_contents($originPath);
+ if ($fileContent === false) {
+ exit('远程文件下载失败: ' . $originPath);
+ }
+ file_put_contents($tmpFile, $fileContent);
+ } else {
+ // 本地文件,直接copy
+ if (!file_exists($originPath)) {
+ exit('文件不存在: ' . $originPath);
+ }
+ copy($originPath, $tmpFile);
+ }
+ // 解析临时文件
+ $ext = strtolower(pathinfo($originPath, PATHINFO_EXTENSION));
+ $rows = [];
+ if (in_array($ext, ['xls', 'xlsx'])) {
+ // 直接用composer自动加载的PHPExcel
+ $excel = \PHPExcel_IOFactory::load($tmpFile);
+ $sheet = $excel->getActiveSheet();
+ $data = $sheet->toArray();
+ if (count($data) > 1) {
+ array_shift($data); // 去掉表头
+ }
+
+ foreach ($data as $cols) {
+ $rows[] = [
+ 'name' => isset($cols[0]) ? trim($cols[0]) : '',
+ 'phone' => isset($cols[1]) ? trim($cols[1]) : '',
+ 'wechatId' => isset($cols[2]) ? trim($cols[2]) : '',
+ 'source' => isset($cols[3]) ? trim($cols[3]) : '',
+ 'orderAmount' => isset($cols[4]) ? trim($cols[4]) : '',
+ 'orderDate' => isset($cols[5]) ? trim($cols[5]) : '',
+ ];
+ }
+ } elseif ($ext === 'csv') {
+ $content = file_get_contents($tmpFile);
+ $lines = preg_split('/\r\n|\r|\n/', $content);
+ if (count($lines) > 1) {
+ array_shift($lines); // 去掉表头
+ foreach ($lines as $line) {
+ if (trim($line) === '') continue;
+ $cols = str_getcsv($line);
+ if (count($cols) >= 6) {
+ $rows[] = [
+ 'name' => isset($cols[0]) ? trim($cols[0]) : '',
+ 'phone' => isset($cols[1]) ? trim($cols[1]) : '',
+ 'wechatId' => isset($cols[2]) ? trim($cols[2]) : '',
+ 'source' => isset($cols[3]) ? trim($cols[3]) : '',
+ 'orderAmount' => isset($cols[4]) ? trim($cols[4]) : '',
+ 'orderDate' => isset($cols[5]) ? trim($cols[5]) : '',
+ ];
+ }
+ }
+ }
+ } else {
+ unlink($tmpFile);
+ exit('暂不支持的文件类型: ' . $ext);
+ }
+ // 删除临时文件
+ unlink($tmpFile);
+ }
+ }
+
+ //电话获客
+ if ($params['sceneId'] == 5) {
+ $rows = Db::name('call_recording')
+ ->where('companyId', $this->getUserInfo('companyId'))
+ ->group('phone')
+ ->field('id,phone')
+ ->select();
+ }
+
+
+ //群获客
+ if ($params['sceneId'] == 7) {
+ if (!empty($params['wechatGroups']) && is_array($params['wechatGroups'])) {
+ $rows = Db::name('wechat_group_member')->alias('gm')
+ ->join('wechat_account wa', 'gm.identifier = wa.wechatId')
+ ->whereIn('gm.groupId', $params['wechatGroups'])
+ ->group('gm.identifier')
+ ->column('wa.id,wa.wechatId,wa.alias,wa.phone');
+ }
+ }
+
+
+ if (in_array($params['sceneId'], [2, 5, 7]) && !empty($rows) && is_array($rows)) {
+ // 1000条为一组进行批量处理
+ $batchSize = 1000;
+ $totalRows = count($rows);
+
+ for ($i = 0; $i < $totalRows; $i += $batchSize) {
+ $batchRows = array_slice($rows, $i, $batchSize);
+
+ if (!empty($batchRows)) {
+ // 1. 提取当前批次的phone
+ $phones = [];
+ foreach ($batchRows as $row) {
+ if (!empty($row['phone'])) {
+ $phone = $row['phone'];
+ } elseif (!empty($row['alias'])) {
+ $phone = $row['alias'];
+ } else {
+ $phone = $row['wechatId'];
+ }
+ if (!empty($phone)) {
+ $phones[] = $phone;
+ }
+ }
+
+ // 2. 批量查询已存在的phone
+ $existingPhones = [];
+ if (!empty($phones)) {
+ $existing = Db::name('task_customer')
+ ->where('task_id', $planId)
+ ->where('phone', 'in', $phones)
+ ->field('phone')
+ ->select();
+ $existingPhones = array_column($existing, 'phone');
+ }
+
+ // 3. 过滤出新数据,批量插入
+ $newData = [];
+ foreach ($batchRows as $row) {
+ if (!empty($row['phone'])) {
+ $phone = $row['phone'];
+ } elseif (!empty($row['alias'])) {
+ $phone = $row['alias'];
+ } else {
+ $phone = $row['wechatId'];
+ }
+ if (!empty($phone) && !in_array($phone, $existingPhones)) {
+ $newData[] = [
+ 'task_id' => $planId,
+ 'name' => '',
+ 'source' => '场景获客_' . $params['name'] ?? '',
+ 'phone' => $phone,
+ 'tags' => json_encode([], JSON_UNESCAPED_UNICODE),
+ 'siteTags' => json_encode([], JSON_UNESCAPED_UNICODE),
+ 'createTime' => time(),
+ ];
+ }
+ }
+
+ // 4. 批量插入新数据
+ if (!empty($newData)) {
+ Db::name('task_customer')->insertAll($newData);
+ }
+ }
+ }
+ }
+
+
+ Db::commit();
+
+ return ResponseHelper::success(['planId' => $planId], '添加计划任务成功');
+
+ } catch (\Exception $e) {
+ // 回滚事务
+ Db::rollback();
+ throw $e;
+ }
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500);
+ }
+ }
+
+ /**
+ * 验证JSON格式是否正确
+ *
+ * @param string $string
+ * @return bool
+ */
+ private function validateJson($string)
+ {
+ if (empty($string)) {
+ return true;
+ }
+
+ json_decode($string);
+ return (json_last_error() == JSON_ERROR_NONE);
+ }
+
+ /**
+ * 处理分销配置
+ *
+ * @param array $params 请求参数
+ * @param int $companyId 公司ID
+ * @return array 分销配置
+ */
+ private function processDistributionConfig($params, $companyId)
+ {
+ $distributionEnabled = !empty($params['distributionEnabled']) ? true : false;
+
+ $config = [
+ 'enabled' => $distributionEnabled,
+ 'channels' => [],
+ 'customerRewardAmount' => 0, // 获客奖励金额(分)
+ 'addFriendRewardAmount' => 0, // 添加奖励金额(分)
+ ];
+
+ // 如果未开启分销,直接返回默认配置
+ if (!$distributionEnabled) {
+ return $config;
+ }
+
+ // 验证渠道ID
+ $channelIds = $params['distributionChannels'] ?? [];
+ if (empty($channelIds) || !is_array($channelIds)) {
+ throw new \Exception('请选择至少一个分销渠道');
+ }
+
+ // 查询有效的渠道(只保留存在且已启用的渠道)
+ $channels = Db::name('distribution_channel')
+ ->where([
+ ['id', 'in', $channelIds],
+ ['companyId', '=', $companyId],
+ ['status', '=', 'enabled'],
+ ['deleteTime', '=', 0]
+ ])
+ ->field('id,code,name')
+ ->select();
+
+ // 如果没有有效渠道,才报错
+ if (empty($channels)) {
+ throw new \Exception('所选的分销渠道均不存在或已被禁用,请重新选择');
+ }
+
+ // 只保留有效的渠道ID
+ $config['channels'] = array_column($channels, 'id');
+
+ // 验证获客奖励金额(元转分)
+ $customerRewardAmount = isset($params['customerRewardAmount']) ? floatval($params['customerRewardAmount']) : 0;
+ if ($customerRewardAmount < 0) {
+ throw new \Exception('获客奖励金额不能为负数');
+ }
+ if ($customerRewardAmount > 0 && !preg_match('/^\d+(\.\d{1,2})?$/', (string)$customerRewardAmount)) {
+ throw new \Exception('获客奖励金额格式不正确,最多保留2位小数');
+ }
+ $config['customerRewardAmount'] = intval(round($customerRewardAmount * 100)); // 元转分
+
+ // 验证添加奖励金额(元转分)
+ $addFriendRewardAmount = isset($params['addFriendRewardAmount']) ? floatval($params['addFriendRewardAmount']) : 0;
+ if ($addFriendRewardAmount < 0) {
+ throw new \Exception('添加奖励金额不能为负数');
+ }
+ if ($addFriendRewardAmount > 0 && !preg_match('/^\d+(\.\d{1,2})?$/', (string)$addFriendRewardAmount)) {
+ throw new \Exception('添加奖励金额格式不正确,最多保留2位小数');
+ }
+ $config['addFriendRewardAmount'] = intval(round($addFriendRewardAmount * 100)); // 元转分
+
+ return $config;
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/plan/PostExternalApiV1Controller.php b/application/cunkebao/controller/plan/PostExternalApiV1Controller.php
new file mode 100644
index 0000000..4edd546
--- /dev/null
+++ b/application/cunkebao/controller/plan/PostExternalApiV1Controller.php
@@ -0,0 +1,288 @@
+request->param();
+
+ // 验证必填参数
+ if (empty($params['apiKey'])) {
+ return ResponseHelper::error('apiKey不能为空', 400);
+ }
+
+ if (empty($params['sign'])) {
+ return ResponseHelper::error('sign不能为空', 400);
+ }
+
+ if (empty($params['timestamp'])) {
+ return ResponseHelper::error('timestamp不能为空', 400);
+ }
+
+ // 验证时间戳(允许5分钟误差)
+ if (abs(time() - intval($params['timestamp'])) > 300) {
+ return ResponseHelper::error('请求已过期', 400);
+ }
+
+ // 查询API密钥是否存在
+ $plan = Db::name('customer_acquisition_task')
+ ->where('apiKey', $params['apiKey'])
+ ->where('status', 1)
+ ->find();
+
+ if (!$plan) {
+ return ResponseHelper::error('无效的apiKey', 401);
+ }
+
+ // 验证签名
+ if (!$this->validateSign($params,$params['apiKey'], $params['sign'])) {
+ return ResponseHelper::error('签名验证失败', 401);
+ }
+
+ $identifier = !empty($params['wechatId']) ? $params['wechatId'] : $params['phone'];
+
+ // 渠道ID(cid),对应 distribution_channel.id
+ $channelId = !empty($params['cid']) ? intval($params['cid']) : 0;
+
+
+ $trafficPool = Db::name('traffic_pool')->where('identifier', $identifier)->find();
+ if (!$trafficPool) {
+ $trafficPoolId =Db::name('traffic_pool')->insertGetId([
+ 'identifier' => $identifier,
+ 'mobile' => !empty($params['phone']) ? $params['phone'] : '',
+ 'createTime' => time()
+ ]);
+ }else{
+ $trafficPoolId = $trafficPool['id'];
+ }
+
+ $taskCustomer = Db::name('task_customer')
+ ->where('task_id', $plan['id'])
+ ->where('phone', $identifier)
+ ->find();
+
+ // 处理用户画像
+ if(!empty($params['portrait']) && is_array($params['portrait'])){
+ $this->updatePortrait($params['portrait'],$trafficPoolId,$plan['companyId']);
+ }
+ if (!$taskCustomer) {
+ $tags = !empty($params['tags']) ? explode(',', $params['tags']) : [];
+ $siteTags = !empty($params['siteTags']) ? explode(',', $params['siteTags']) : [];
+
+ // 处理渠道ID:只有在分销配置中允许、且渠道本身正常时,才记录到task_customer
+ $finalChannelId = 0;
+ if ($channelId > 0) {
+ $sceneConf = json_decode($plan['sceneConf'], true) ?: [];
+ $distributionConfig = $sceneConf['distribution'] ?? null;
+ $allowedChannelIds = $distributionConfig['channels'] ?? [];
+ if (!empty($distributionConfig) && !empty($distributionConfig['enabled']) && in_array($channelId, $allowedChannelIds)) {
+ // 验证渠道是否存在且正常
+ $channel = Db::name('distribution_channel')
+ ->where([
+ ['id', '=', $channelId],
+ ['companyId', '=', $plan['companyId']],
+ ['status', '=', 'enabled'],
+ ['deleteTime', '=', 0]
+ ])
+ ->find();
+ if ($channel) {
+ $finalChannelId = intval($channelId);
+ }
+ }
+ }
+
+ $customerId = Db::name('task_customer')->insertGetId([
+ 'task_id' => $plan['id'],
+ 'channelId' => $finalChannelId,
+ 'phone' => $identifier,
+ 'name' => !empty($params['name']) ? $params['name'] : '',
+ 'source' => !empty($params['source']) ? $params['source'] : '',
+ 'remark' => !empty($params['remark']) ? $params['remark'] : '',
+ 'tags' => json_encode($tags,256),
+ 'siteTags' => json_encode($siteTags,256),
+ 'createTime' => time(),
+ ]);
+
+ // 记录获客奖励(异步处理,不影响主流程)
+ if ($customerId) {
+ try {
+ // 只有在存在有效渠道ID时才触发分佣
+ if ($finalChannelId > 0) {
+ DistributionRewardService::recordCustomerReward($plan['id'], $customerId, $identifier, $finalChannelId);
+ }
+ } catch (\Exception $e) {
+ // 记录错误但不影响主流程
+ \think\facade\Log::error('记录获客奖励失败:' . $e->getMessage());
+ }
+ }
+
+ return json([
+ 'code' => 200,
+ 'message' => '新增成功',
+ 'data' => $identifier
+ ]);
+ }else{
+ $siteTags = !empty($params['siteTags']) ? explode(',',$params['siteTags']) : [];
+
+ // 更新新老标签数据,实现去重
+ $this->updateSiteTags($taskCustomer['id'], $siteTags);
+
+ return json([
+ 'code' => 200,
+ 'message' => '已存在',
+ 'data' => $identifier
+ ]);
+ }
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500);
+ }
+ }
+
+ /**
+ * 用户画像
+ * @param array $data 用户画像数据
+ * @param int $trafficPoolId 流量池id
+ */
+ public function updatePortrait($data,$trafficPoolId,$companyId)
+ {
+ if(empty($data) || empty($trafficPoolId) || !is_array($data)){
+ return;
+ }
+
+ $type = !empty($data['type']) ? $data['type'] : 0;
+ $source = !empty($data['source']) ? $data['source'] : 0;
+ $sourceData = !empty($data['sourceData']) ? $data['sourceData'] : [];
+ $remark = !empty($data['remark']) ? $data['remark'] : '';
+ $uniqueId = !empty($data['uniqueId']) ? $data['uniqueId'] : 0;
+ ksort($sourceData);
+ $sourceData = json_encode($sourceData,256);
+
+
+ $data = [
+ 'companyId' => $companyId,
+ 'trafficPoolId' => $trafficPoolId,
+ 'type' => $type,
+ 'source' => $source,
+ 'sourceData' => $sourceData,
+ 'remark' => $remark,
+ 'uniqueId' => $uniqueId,
+ 'count' => 1,
+ 'createTime' => time(),
+ 'updateTime' => time(),
+ ];
+
+ $res= Db::name('user_portrait')
+ ->where(['trafficPoolId'=>$trafficPoolId,'type'=>$type,'source'=>$source,'uniqueId'=>$uniqueId])
+ ->where('createTime','>',time()-1800)
+ ->find();
+ if($res){
+ $count = $res['count'] + 1;
+ Db::name('user_portrait')->where(['id'=>$res['id']])->update(['count'=>$count,'updateTime'=>time()]);
+ }else{
+ Db::name('user_portrait')->insert($data);
+ }
+
+ }
+
+ /**
+ * 更新站点标签数据,实现去重
+ * @param int $taskCustomerId 任务客户ID
+ * @param array $newSiteTags 新的站点标签数组
+ */
+ private function updateSiteTags($taskCustomerId, $newSiteTags)
+ {
+
+ if (empty($taskCustomerId) || empty($newSiteTags) || !is_array($newSiteTags)) {
+ return;
+ }
+
+ try {
+ // 获取当前任务客户的站点标签
+ $taskCustomer = Db::name('task_customer')->where('id', $taskCustomerId)->find();
+
+ if (!$taskCustomer) {
+ return;
+ }
+
+ // 解析现有的站点标签
+ $existingSiteTags = [];
+ if (!empty($taskCustomer['siteTags'])) {
+ $existingSiteTags = json_decode($taskCustomer['siteTags'], true);
+ if (!is_array($existingSiteTags)) {
+ $existingSiteTags = [];
+ }
+ }
+
+ // 合并新老标签并去重
+ $mergedSiteTags = array_merge($existingSiteTags, $newSiteTags);
+ $uniqueSiteTags = array_unique($mergedSiteTags);
+
+ // 过滤空值并重新索引数组
+ $uniqueSiteTags = array_values(array_filter($uniqueSiteTags, function($tag) {
+ return !empty(trim($tag));
+ }));
+
+
+ // 更新数据库中的站点标签
+ Db::name('task_customer')->where('id', $taskCustomerId)->update([
+ 'siteTags' => json_encode($uniqueSiteTags, JSON_UNESCAPED_UNICODE),
+ 'updateTime' => time()
+ ]);
+
+ } catch (\Exception $e) {
+ // 记录错误日志,但不影响主流程
+ \think\facade\Log::error('更新站点标签失败: ' . $e->getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/plan/PostUpdateAddFriendPlanV1Controller.php b/application/cunkebao/controller/plan/PostUpdateAddFriendPlanV1Controller.php
new file mode 100644
index 0000000..95a4468
--- /dev/null
+++ b/application/cunkebao/controller/plan/PostUpdateAddFriendPlanV1Controller.php
@@ -0,0 +1,369 @@
+request->param();
+
+ // 验证必填字段
+ if (empty($params['planId'])) {
+ return ResponseHelper::error('计划ID不能为空', 400);
+ }
+
+ if (empty($params['name'])) {
+ return ResponseHelper::error('计划名称不能为空', 400);
+ }
+
+ if (empty($params['sceneId'])) {
+ return ResponseHelper::error('场景ID不能为空', 400);
+ }
+
+ if (empty($params['deviceGroups'])) {
+ return ResponseHelper::error('请选择设备', 400);
+ }
+
+ // 检查计划是否存在
+ $plan = Db::name('customer_acquisition_task')
+ ->where('id', $params['planId'])
+ ->find();
+
+ if (!$plan) {
+ return ResponseHelper::error('计划不存在', 404);
+ }
+
+ $companyId = $this->getUserInfo('companyId');
+
+ // 处理分销配置
+ $distributionConfig = $this->processDistributionConfig($params, $companyId);
+
+ // 归类参数
+ $msgConf = isset($params['messagePlans']) ? $params['messagePlans'] : [];
+ $tagConf = [
+ 'scenarioTags' => $params['scenarioTags'] ?? [],
+ 'customTags' => $params['customTags'] ?? [],
+ ];
+ $reqConf = [
+ 'device' => $params['deviceGroups'] ?? [],
+ 'remarkType' => $params['remarkType'] ?? '',
+ 'greeting' => $params['greeting'] ?? '',
+ 'addFriendInterval' => $params['addFriendInterval'] ?? '',
+ 'startTime' => $params['startTime'] ?? '',
+ 'endTime' => $params['endTime'] ?? '',
+ ];
+
+ // 其余参数归为sceneConf
+ $sceneConf = $params;
+ unset(
+ $sceneConf['id'],
+ $sceneConf['apiKey'],
+ $sceneConf['userId'],
+ $sceneConf['status'],
+ $sceneConf['planId'],
+ $sceneConf['name'],
+ $sceneConf['sceneId'],
+ $sceneConf['messagePlans'],
+ $sceneConf['scenarioTags'],
+ $sceneConf['customTags'],
+ $sceneConf['deviceGroups'],
+ $sceneConf['orderTableFileName'],
+ $sceneConf['userInfo'],
+ $sceneConf['textUrl'],
+ $sceneConf['remarkType'],
+ $sceneConf['greeting'],
+ $sceneConf['addFriendInterval'],
+ $sceneConf['startTime'],
+ $sceneConf['orderTableFile'],
+ $sceneConf['endTime'],
+ $sceneConf['distributionEnabled'],
+ $sceneConf['distributionChannels'],
+ $sceneConf['customerRewardAmount'],
+ $sceneConf['addFriendRewardAmount']
+ );
+
+ // 将分销配置添加到sceneConf中
+ $sceneConf['distribution'] = $distributionConfig;
+
+ // 构建更新数据
+ $data = [
+ 'name' => $params['name'],
+ 'sceneId' => $params['sceneId'],
+ 'sceneConf' => json_encode($sceneConf, JSON_UNESCAPED_UNICODE),
+ 'reqConf' => json_encode($reqConf, JSON_UNESCAPED_UNICODE),
+ 'msgConf' => json_encode($msgConf, JSON_UNESCAPED_UNICODE),
+ 'tagConf' => json_encode($tagConf, JSON_UNESCAPED_UNICODE),
+ 'status' => !empty($params['status']) ? 1 : 0,
+ 'updateTime' => time(),
+ ];
+
+
+ try {
+ // 更新数据
+ $result = Db::name('customer_acquisition_task')
+ ->where('id', $params['planId'])
+ ->update($data);
+
+ if ($result === false) {
+ throw new \Exception('更新计划失败');
+ }
+
+ //订单
+ if ($params['sceneId'] == 2) {
+ if (!empty($params['orderFileUrl'])) {
+ // 先下载到本地临时文件,再分析,最后删除
+ $originPath = $params['orderFileUrl'];
+ $tmpFile = tempnam(sys_get_temp_dir(), 'order_');
+ // 判断是否为远程文件
+ if (preg_match('/^https?:\/\//i', $originPath)) {
+ // 远程URL,下载到本地
+ $fileContent = file_get_contents($originPath);
+ if ($fileContent === false) {
+ exit('远程文件下载失败: ' . $originPath);
+ }
+ file_put_contents($tmpFile, $fileContent);
+ } else {
+ // 本地文件,直接copy
+ if (!file_exists($originPath)) {
+ exit('文件不存在: ' . $originPath);
+ }
+ copy($originPath, $tmpFile);
+ }
+ // 解析临时文件
+ $ext = strtolower(pathinfo($originPath, PATHINFO_EXTENSION));
+ $rows = [];
+ if (in_array($ext, ['xls', 'xlsx'])) {
+ // 直接用composer自动加载的PHPExcel
+ $excel = \PHPExcel_IOFactory::load($tmpFile);
+ $sheet = $excel->getActiveSheet();
+ $data = $sheet->toArray();
+ if (count($data) > 1) {
+ array_shift($data); // 去掉表头
+ }
+
+ foreach ($data as $cols) {
+ $rows[] = [
+ 'name' => isset($cols[0]) ? trim($cols[0]) : '',
+ 'phone' => isset($cols[1]) ? trim($cols[1]) : '',
+ 'wechatId' => isset($cols[2]) ? trim($cols[2]) : '',
+ 'source' => isset($cols[3]) ? trim($cols[3]) : '',
+ 'orderAmount' => isset($cols[4]) ? trim($cols[4]) : '',
+ 'orderDate' => isset($cols[5]) ? trim($cols[5]) : '',
+ ];
+ }
+ } elseif ($ext === 'csv') {
+ $content = file_get_contents($tmpFile);
+ $lines = preg_split('/\r\n|\r|\n/', $content);
+ if (count($lines) > 1) {
+ array_shift($lines); // 去掉表头
+ foreach ($lines as $line) {
+ if (trim($line) === '') continue;
+ $cols = str_getcsv($line);
+ if (count($cols) >= 6) {
+ $rows[] = [
+ 'name' => isset($cols[0]) ? trim($cols[0]) : '',
+ 'phone' => isset($cols[1]) ? trim($cols[1]) : '',
+ 'wechatId' => isset($cols[2]) ? trim($cols[2]) : '',
+ 'source' => isset($cols[3]) ? trim($cols[3]) : '',
+ 'orderAmount' => isset($cols[4]) ? trim($cols[4]) : '',
+ 'orderDate' => isset($cols[5]) ? trim($cols[5]) : '',
+ ];
+ }
+ }
+ }
+ } else {
+ unlink($tmpFile);
+ exit('暂不支持的文件类型: ' . $ext);
+ }
+ // 删除临时文件
+ unlink($tmpFile);
+ }
+ }
+
+
+ //电话获客
+ if ($params['sceneId'] == 5) {
+ $rows = Db::name('call_recording')
+ ->where('companyId', $this->getUserInfo('companyId'))
+ ->group('phone')
+ ->field('id,phone')
+ ->select();
+ }
+
+ //群获客
+ if ($params['sceneId'] == 7) {
+ if (!empty($params['wechatGroups']) && is_array($params['wechatGroups'])) {
+ $rows = Db::name('wechat_group_member')->alias('gm')
+ ->join('wechat_account wa', 'gm.identifier = wa.wechatId')
+ ->whereIn('gm.groupId', $params['wechatGroups'])
+ ->group('gm.identifier')
+ ->column('wa.id,wa.wechatId,wa.alias,wa.phone');
+ }
+ }
+
+
+ if (in_array($params['sceneId'], [2, 5, 7]) && !empty($rows) && is_array($rows)) {
+ // 1000条为一组进行批量处理
+ $batchSize = 1000;
+ $totalRows = count($rows);
+
+ for ($i = 0; $i < $totalRows; $i += $batchSize) {
+ $batchRows = array_slice($rows, $i, $batchSize);
+ if (!empty($batchRows)) {
+ // 1. 提取当前批次的phone
+ // 1. 提取当前批次的phone
+ $phones = [];
+ foreach ($batchRows as $row) {
+ if (!empty($row['phone'])) {
+ $phone = $row['phone'];
+ } elseif (!empty($row['alias'])) {
+ $phone = $row['alias'];
+ } else {
+ $phone = $row['wechatId'];
+ }
+ if (!empty($phone)) {
+ $phones[] = $phone;
+ }
+ }
+ // 2. 批量查询已存在的phone
+ $existingPhones = [];
+ if (!empty($phones)) {
+ $existing = Db::name('task_customer')
+ ->where('task_id', $params['planId'])
+ ->where('phone', 'in', $phones)
+ ->field('phone')
+ ->select();
+ $existingPhones = array_column($existing, 'phone');
+ }
+
+ // 3. 过滤出新数据,批量插入
+ $newData = [];
+ foreach ($batchRows as $row) {
+ if (!empty($row['phone'])) {
+ $phone = $row['phone'];
+ } elseif (!empty($row['alias'])) {
+ $phone = $row['alias'];
+ } else {
+ $phone = $row['wechatId'];
+ }
+ if (!empty($phone) && !in_array($phone, $existingPhones)) {
+ $newData[] = [
+ 'task_id' => $params['planId'],
+ 'name' => !empty($row['name']) ? $row['name'] : '',
+ 'source' => '场景获客_' . $params['name'] ?? '',
+ 'phone' => $phone,
+ 'tags' => json_encode([], JSON_UNESCAPED_UNICODE),
+ 'siteTags' => json_encode([], JSON_UNESCAPED_UNICODE),
+ 'createTime' => time(),
+ ];
+ }
+ }
+
+ // 4. 批量插入新数据
+ if (!empty($newData)) {
+ Db::name('task_customer')->insertAll($newData);
+ }
+ }
+ }
+ }
+
+
+ return ResponseHelper::success(['planId' => $params['planId']], '更新计划任务成功');
+
+ } catch (\Exception $e) {
+ // 回滚事务
+ Db::rollback();
+ throw $e;
+ }
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500);
+ }
+ }
+
+ /**
+ * 处理分销配置
+ *
+ * @param array $params 请求参数
+ * @param int $companyId 公司ID
+ * @return array 分销配置
+ */
+ private function processDistributionConfig($params, $companyId)
+ {
+ $distributionEnabled = !empty($params['distributionEnabled']) ? true : false;
+
+ $config = [
+ 'enabled' => $distributionEnabled,
+ 'channels' => [],
+ 'customerRewardAmount' => 0, // 获客奖励金额(分)
+ 'addFriendRewardAmount' => 0, // 添加奖励金额(分)
+ ];
+
+ // 如果未开启分销,直接返回默认配置
+ if (!$distributionEnabled) {
+ return $config;
+ }
+
+ // 验证渠道ID
+ $channelIds = $params['distributionChannels'] ?? [];
+ if (empty($channelIds) || !is_array($channelIds)) {
+ throw new \Exception('请选择至少一个分销渠道');
+ }
+
+ // 查询有效的渠道(只保留存在且已启用的渠道)
+ $channels = Db::name('distribution_channel')
+ ->where([
+ ['id', 'in', $channelIds],
+ ['companyId', '=', $companyId],
+ ['status', '=', 'enabled'],
+ ['deleteTime', '=', 0]
+ ])
+ ->field('id,code,name')
+ ->select();
+
+ // 如果没有有效渠道,才报错
+ if (empty($channels)) {
+ throw new \Exception('所选的分销渠道均不存在或已被禁用,请重新选择');
+ }
+
+ // 只保留有效的渠道ID
+ $config['channels'] = array_column($channels, 'id');
+
+ // 验证获客奖励金额(元转分)
+ $customerRewardAmount = isset($params['customerRewardAmount']) ? floatval($params['customerRewardAmount']) : 0;
+ if ($customerRewardAmount < 0) {
+ throw new \Exception('获客奖励金额不能为负数');
+ }
+ if ($customerRewardAmount > 0 && !preg_match('/^\d+(\.\d{1,2})?$/', (string)$customerRewardAmount)) {
+ throw new \Exception('获客奖励金额格式不正确,最多保留2位小数');
+ }
+ $config['customerRewardAmount'] = intval(round($customerRewardAmount * 100)); // 元转分
+
+ // 验证添加奖励金额(元转分)
+ $addFriendRewardAmount = isset($params['addFriendRewardAmount']) ? floatval($params['addFriendRewardAmount']) : 0;
+ if ($addFriendRewardAmount < 0) {
+ throw new \Exception('添加奖励金额不能为负数');
+ }
+ if ($addFriendRewardAmount > 0 && !preg_match('/^\d+(\.\d{1,2})?$/', (string)$addFriendRewardAmount)) {
+ throw new \Exception('添加奖励金额格式不正确,最多保留2位小数');
+ }
+ $config['addFriendRewardAmount'] = intval(round($addFriendRewardAmount * 100)); // 元转分
+
+ return $config;
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php b/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php
new file mode 100644
index 0000000..a9b5d4f
--- /dev/null
+++ b/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php
@@ -0,0 +1,403 @@
+config = [
+ 'app_id' => Env::get('weChat.appidMiniApp','wx789850448e26c91d'),
+ 'secret' => Env::get('weChat.secretMiniApp','d18f75b3a3623cb40da05648b08365a1'),
+ 'response_type' => 'array'
+ ];
+ }
+
+
+ public function index()
+ {
+ return 'Hello, World!';
+ }
+
+
+ // 生成小程序码,存客宝-操盘手调用
+ public function generateMiniProgramCodeWithScene($taskId = '', $channelId = 0)
+ {
+
+ if (empty($taskId)) {
+ return json_encode(['code' => 500, 'data' => '', 'msg' => '任务id不能为空']);
+ }
+
+
+ try {
+ $app = Factory::miniProgram($this->config);
+ // scene参数长度限制为32位
+ // 如果提供了channelId,格式为:taskId,channelId
+ // 如果没有channelId,格式为:taskId
+ if (!empty($channelId) && $channelId > 0) {
+ $scene = sprintf("%s,%s", $taskId, $channelId);
+ } else {
+ $scene = sprintf("%s", $taskId);
+ }
+
+ // 确保scene长度不超过32位
+ if (strlen($scene) > 32) {
+ $scene = substr($scene, 0, 32);
+ }
+
+ // 调用接口生成小程序码
+ $response = $app->app_code->getUnlimit($scene, [
+ 'page' => 'pages/poster/index2', // 必须是已经发布的小程序页面
+ 'width' => 430, // 二维码的宽度,默认430
+ // 'auto_color' => false, // 自动配置线条颜色
+ // 'line_color' => ['r' => 0, 'g' => 0, 'b' => 0], // 颜色设置
+ // 'is_hyaline' => false, // 是否需要透明底色
+ ]);
+ // 保存小程序码到文件
+ if ($response instanceof StreamResponse) {
+ // $filename = 'minicode_' . $taskId . '.png';
+ // $response->saveAs('path/to/codes', $filename);
+ // return 'path/to/codes/' . $filename;
+
+ $img = $response->getBody()->getContents();//获取图片二进制流
+ $img_base64 = 'data:image/png;base64,' . base64_encode($img);//转化base64
+ return json_encode(['code' => 200, 'data' => $img_base64]);
+ }
+ } catch (\Exception $e) {
+ return json_encode(['code' => 500, 'data' => '', 'msg' => $e->getMessage()]);
+ }
+ }
+
+ // getPhoneNumber
+ public function getPhoneNumber()
+ {
+
+ $taskId = request()->param('id');
+ $code = request()->param('code');
+ // code 不能为空
+ if (!$code) {
+ return json([
+ 'code' => 400,
+ 'message' => 'code不能为空'
+ ]);
+ }
+
+ $task = Db::name('customer_acquisition_task')->where('id', $taskId)->find();
+ if (!$task) {
+ return json([
+ 'code' => 400,
+ 'message' => '任务不存在'
+ ]);
+ }
+
+ $app = Factory::miniProgram($this->config);
+
+ $result = $app->phone_number->getUserPhoneNumber($code);
+
+ if ($result['errcode'] == 0 && isset($result['phone_info']['phoneNumber'])) {
+
+ // TODO 拿到手机号之后的后续操作:
+ // 1. 先写入 ck_traffic_pool 表 identifier mobile 都是 用 phone字段的值
+ $trafficPool = Db::name('traffic_pool')->where('identifier', $result['phone_info']['phoneNumber'])->find();
+ if (!$trafficPool) {
+ Db::name('traffic_pool')->insert([
+ 'identifier' => $result['phone_info']['phoneNumber'],
+ 'mobile' => $result['phone_info']['phoneNumber'],
+ 'createTime' => time()
+ ]);
+ }
+ // 2. 写入 ck_task_customer: 以 task_id ~~identifier~~ phone 为条件,如果存在则忽略,使用类似laravel的firstOrcreate(但我不知道thinkphp5.1里的写法)
+ // $taskCustomer = Db::name('task_customer')->where('task_id', $taskId)->where('identifier', $result['phone_info']['phoneNumber'])->find();
+ $taskCustomer = Db::name('task_customer')
+ ->where('task_id', $taskId)
+ ->where('phone', $result['phone_info']['phoneNumber'])
+ ->find();
+ if (!$taskCustomer) {
+ // 渠道ID(cid),对应 distribution_channel.id
+ $channelId = intval($this->request->param('cid', 0));
+
+ $finalChannelId = 0;
+ if ($channelId > 0) {
+ // 获取任务信息,解析分销配置
+ $sceneConf = json_decode($task['sceneConf'] ?? '[]', true) ?: [];
+ $distributionConfig = $sceneConf['distribution'] ?? null;
+ $allowedChannelIds = $distributionConfig['channels'] ?? [];
+ if (!empty($distributionConfig) && !empty($distributionConfig['enabled']) && in_array($channelId, $allowedChannelIds)) {
+ // 验证渠道是否存在且正常
+ $channel = Db::name('distribution_channel')
+ ->where([
+ ['id', '=', $channelId],
+ ['companyId', '=', $task['companyId']],
+ ['status', '=', 'enabled'],
+ ['deleteTime', '=', 0]
+ ])
+ ->find();
+ if ($channel) {
+ $finalChannelId = $channelId;
+ }
+ }
+ }
+
+ $customerId = Db::name('task_customer')->insertGetId([
+ 'task_id' => $taskId,
+ 'channelId' => $finalChannelId,
+ // 'identifier' => $result['phone_info']['phoneNumber'],
+ 'phone' => $result['phone_info']['phoneNumber'],
+ 'source' => $task['name'],
+ 'createTime' => time(),
+ 'tags' => json_encode([]),
+ 'siteTags' => json_encode([]),
+ ]);
+
+ // 记录获客奖励(异步处理,不影响主流程)
+ if ($customerId) {
+ try {
+ if ($finalChannelId > 0) {
+ \app\cunkebao\service\DistributionRewardService::recordCustomerReward(
+ $taskId,
+ $customerId,
+ $result['phone_info']['phoneNumber'],
+ $finalChannelId
+ );
+ }
+ } catch (\Exception $e) {
+ // 记录错误但不影响主流程
+ \think\facade\Log::error('记录获客奖励失败:' . $e->getMessage());
+ }
+ }
+ }
+ // return $result['phone_info']['phoneNumber'];
+ return json([
+ 'code' => 200,
+ 'message' => '获取手机号成功',
+ 'data' => $result['phone_info']['phoneNumber']
+ ]);
+ } else {
+ // return null;
+ return json([
+ 'code' => 400,
+ 'message' => '获取手机号失败: ' . $result['errmsg'] ?? '未知错误'
+ ]);
+ }
+
+ // return $result;
+
+ }
+
+
+ public function decryptphones()
+ {
+
+ $taskId = request()->param('id');
+ $rawInput = trim((string)request()->param('phone', ''));
+ // 渠道ID(cid),对应 distribution_channel.id
+ $channelId = intval(request()->param('cid', 0));
+ if ($rawInput === '') {
+ return json([
+ 'code' => 400,
+ 'message' => '手机号或微信号不能为空'
+ ]);
+ }
+ $task = Db::name('customer_acquisition_task')->where('id', $taskId)->find();
+
+ if (!$task) {
+ return json([
+ 'code' => 400,
+ 'message' => '任务不存在'
+ ]);
+ }
+
+ // 预先根据任务的分销配置校验渠道是否有效(仅当传入了cid时)
+ $finalChannelId = 0;
+ if ($channelId > 0) {
+ $sceneConf = json_decode($task['sceneConf'] ?? '[]', true) ?: [];
+ $distributionConfig = $sceneConf['distribution'] ?? null;
+ $allowedChannelIds = $distributionConfig['channels'] ?? [];
+ if (!empty($distributionConfig) && !empty($distributionConfig['enabled']) && in_array($channelId, $allowedChannelIds)) {
+ // 验证渠道是否存在且正常
+ $channel = Db::name('distribution_channel')
+ ->where([
+ ['id', '=', $channelId],
+ ['companyId', '=', $task['companyId']],
+ ['status', '=', 'enabled'],
+ ['deleteTime', '=', 0]
+ ])
+ ->find();
+ if ($channel) {
+ $finalChannelId = $channelId;
+ }
+ }
+ }
+
+
+ $lines = preg_split('/\r\n|\r|\n/', $rawInput);
+ foreach ($lines as $line) {
+ $line = trim($line);
+ if ($line === '') {
+ continue;
+ }
+ $parts = array_map('trim', explode(',', $line, 2));
+ $identifier = $parts[0] ?? '';
+ $remark = $parts[1] ?? '';
+ if ($identifier === '') {
+ continue;
+ }
+ $isPhone = preg_match('/^\+?\d{6,}$/', $identifier);
+ $trafficPool = Db::name('traffic_pool')->where('identifier', $identifier)->find();
+ if (!$trafficPool) {
+ $insertData = [
+ 'identifier' => $identifier,
+ 'createTime' => time()
+ ];
+ if ($isPhone) {
+ $insertData['mobile'] = $identifier;
+ } else {
+ $insertData['wechatId'] = $identifier;
+ }
+ Db::name('traffic_pool')->insert($insertData);
+ } else {
+ $updates = [];
+ if ($isPhone && empty($trafficPool['mobile'])) {
+ $updates['mobile'] = $identifier;
+ }
+ if (!$isPhone && empty($trafficPool['wechatId'])) {
+ $updates['wechatId'] = $identifier;
+ }
+ if (!empty($updates)) {
+ $updates['updateTime'] = time();
+ Db::name('traffic_pool')->where('id', $trafficPool['id'])->update($updates);
+ }
+ }
+
+ $taskCustomer = Db::name('task_customer')
+ ->where('task_id', $taskId)
+ ->where('phone', $identifier)
+ ->find();
+ if (empty($taskCustomer)) {
+ $insertCustomer = [
+ 'task_id' => $taskId,
+ 'channelId' => $finalChannelId, // 记录本次导入归属的分销渠道(如有)
+ 'phone' => $identifier,
+ 'source' => $task['name'],
+ 'createTime'=> time(),
+ 'tags' => json_encode([]),
+ 'siteTags' => json_encode([]),
+ ];
+ if ($remark !== '') {
+ $insertCustomer['remark'] = $remark;
+ }
+ // 使用 insertGetId 以便在需要时记录获客奖励
+ $customerId = Db::name('task_customer')->insertGetId($insertCustomer);
+
+ // 表单录入成功即视为一次获客:
+ // 仅在存在有效渠道ID时,记录获客奖励(谁的cid谁获客)
+ if (!empty($customerId) && $finalChannelId > 0) {
+ try {
+ \app\cunkebao\service\DistributionRewardService::recordCustomerReward(
+ $taskId,
+ $customerId,
+ $identifier,
+ $finalChannelId
+ );
+ } catch (\Exception $e) {
+ // 记录错误但不影响主流程
+ \think\facade\Log::error('记录获客奖励失败:' . $e->getMessage());
+ }
+ }
+ } elseif ($remark !== '' && $taskCustomer['remark'] !== $remark) {
+ Db::name('task_customer')
+ ->where('id', $taskCustomer['id'])
+ ->update([
+ 'remark' => $remark,
+ 'updateTime' => time()
+ ]);
+ }
+
+ }
+
+ // return $phone;
+ return json([
+ 'code' => 200,
+ 'message' => '操作成功',
+ ]);
+
+ }
+
+ // return $result;
+
+
+// todo 获取海报获客任务的任务/海报数据 -- 表还没设计好,不急 ck_customer_acquisition_task
+ public
+ function getPosterTaskData()
+ {
+ $id = request()->param('id');
+ $task = Db::name('customer_acquisition_task')
+ ->where(['id' => $id, 'deleteTime' => 0])
+ ->field('id,name,sceneConf,status')
+ ->find();
+ if (!$task) {
+ return json([
+ 'code' => 400,
+ 'message' => '任务不存在'
+ ]);
+ }
+
+ if ($task['status'] == 0) {
+ return json([
+ 'code' => 400,
+ 'message' => '任务已结束'
+ ]);
+ }
+
+ $sceneConf = json_decode($task['sceneConf'], true);
+
+ if (isset($sceneConf['posters']['url'])) {
+ $posterUrl = !empty($sceneConf['posters']['url']);
+ } else {
+ $posterUrl = 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%92%A8%E8%AF%A2-FTiyAMAPop2g9LvjLOLDz0VwPg3KVu.gif';
+ }
+
+
+ if (isset($sceneConf['tips'])) {
+ $sTip = $sceneConf['tips'];
+ } else {
+ $sTip = '';
+ }
+
+ unset($task['sceneConf']);
+ $task['sTip'] = $sTip;
+
+ $data = [
+ 'id' => $task['id'],
+ 'name' => $task['name'],
+ 'poster' => ['sUrl' => $posterUrl],
+ 'task' => $task,
+ ];
+
+
+ // todo 只需 返回 poster_url success_tip
+ return json([
+ 'code' => 200,
+ 'message' => '获取海报获客任务数据成功',
+ 'data' => $data
+ ]);
+ }
+
+
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/traffic/GetConvertedListWithInCompanyV1Controller.php b/application/cunkebao/controller/traffic/GetConvertedListWithInCompanyV1Controller.php
new file mode 100644
index 0000000..b152c8d
--- /dev/null
+++ b/application/cunkebao/controller/traffic/GetConvertedListWithInCompanyV1Controller.php
@@ -0,0 +1,112 @@
+items() as $item) {
+ $item->tags = json_decode($item->tags);
+
+ array_push($resultSets, $item->toArray());
+ }
+
+ return $resultSets;
+ }
+
+ /**
+ * 构建查询条件
+ *
+ * @param array $params
+ * @return array
+ */
+ protected function makeWhere(array $params = []): array
+ {
+ if (!empty($keyword = $this->request->param('keyword'))) {
+ $where[] = ['exp', "w.alias LIKE '%{$keyword}%' OR w.nickname LIKE '%{$keyword}%'"];
+ }
+
+ // 来源的筛选
+ if ($fromd = $this->request->param('fromd')) {
+ $where['s.fromd'] = $fromd;
+ }
+
+ $where['s.companyId'] = $this->getUserInfo('companyId');
+ $where['s.status'] = TrafficSourceModel::STATUS_PASSED;
+
+ return array_merge($where, $params);
+ }
+
+ /**
+ * 获取流量池列表
+ *
+ * @param array $where
+ * @return \think\Paginator
+ */
+ protected function getPoolListByCompanyId(array $where): \think\Paginator
+ {
+ $query = TrafficSourceModel::alias('s')
+ ->field(
+ [
+ 'w.id', 'w.nickname', 'w.avatar',
+ 'CASE WHEN w.alias IS NULL OR w.alias = "" THEN w.wechatId ELSE w.alias END AS wechatId',
+ 's.fromd',
+ 'f.tags', 'f.createTime', TrafficSourceModel::STATUS_PASSED . ' status'
+ ]
+ )
+ ->join('traffic_pool p', 'p.identifier=s.identifier')
+ ->join('wechat_account w', 'p.wechatId=w.wechatId')
+ ->join('wechat_friendship f', 'w.wechatId=f.wechatId and f.deleteTime=0')
+ ->order('s.id desc');
+
+ foreach ($where as $key => $value) {
+ if (is_numeric($key) && is_array($value) && isset($value[0]) && $value[0] === 'exp') {
+ $query->whereExp('', $value[1]);
+ continue;
+ }
+
+ $query->where($key, $value);
+ }
+
+ return $query->paginate($this->request->param('limit/d', 10), false, ['page' => $this->request->param('page/d', 1)]);
+ }
+
+ /**
+ * 获取流量池列表
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ $result = $this->getPoolListByCompanyId( $this->makeWhere() );
+
+ return ResponseHelper::success(
+ [
+ 'list' => $this->makeResultedSet($result),
+ 'total' => $result->total(),
+ ]
+ );
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/traffic/GetPoolStatisticsV1Controller.php b/application/cunkebao/controller/traffic/GetPoolStatisticsV1Controller.php
new file mode 100644
index 0000000..eed3f48
--- /dev/null
+++ b/application/cunkebao/controller/traffic/GetPoolStatisticsV1Controller.php
@@ -0,0 +1,70 @@
+ $this->getUserInfo('companyId'),
+ 'status' => TrafficSourceModel::STATUS_PASSED,
+ ]
+ )
+ ->whereBetween('updateTime',
+ [
+ strtotime(date('Y-m-d 00:00:00')),
+ strtotime(date('Y-m-d 23:59:59'))
+ ]
+ )
+ ->count('*');
+ }
+
+ /**
+ * 获取流量池总数
+ *
+ * @return int
+ * @throws \Exception
+ */
+ protected function getTotalCount(): int
+ {
+ return TrafficSourceModel::where(
+ [
+ 'companyId' => $this->getUserInfo('companyId')
+ ]
+ )
+ ->count('*');
+ }
+
+ /**
+ * 获取流量池数据统计
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ return ResponseHelper::success(
+ [
+ 'totalCount' => $this->getTotalCount(),
+ 'todayAddCount' => $this->getTodayAddedCount(),
+ ]
+ );
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/traffic/GetPotentialListWithInCompanyV1Controller.php b/application/cunkebao/controller/traffic/GetPotentialListWithInCompanyV1Controller.php
new file mode 100644
index 0000000..434a128
--- /dev/null
+++ b/application/cunkebao/controller/traffic/GetPotentialListWithInCompanyV1Controller.php
@@ -0,0 +1,655 @@
+request->param('keyword', '');
+ $device = $this->request->param('deviceId');
+ $status = $this->request->param('addStatus', '');
+ $taskId = $this->request->param('taskId', '');
+ $packageId = $this->request->param('packageId', '');
+ $where = [];
+ if (!empty($keyword)) {
+ $where[] = ['p.identifier|wa.nickname|wa.phone|wa.wechatId|wa.alias', 'like', '%' . $keyword . '%'];
+ }
+
+ // 状态筛选
+ if (!empty($status)) {
+ if ($status == 1) {
+ $where[] = ['s.status', '=', 4];
+ } elseif ($status == 2) {
+ $where[] = ['s.status', '=', 0];
+ } elseif ($status == -1) {
+ $where[] = ['s.status', '=', 2];
+ } elseif ($status == 3) {
+ $where[] = ['s.status', '=', 2];
+ }
+ }
+
+ // 来源的筛选
+ if ($packageId) {
+ if ($packageId != -1) {
+ $where[] = ['tsp.id', '=', $packageId];
+ } else {
+ $where[] = ['tsp.id', '=', null];
+ }
+
+ }
+
+ if (!empty($device)) {
+// $where[] = ['d.deviceId', '=', $device];
+ }
+
+ if (!empty($taskId)) {
+ //$where[] = ['t.sceneId', '=', $taskId];
+ }
+ $where[] = ['s.companyId', '=', $this->getUserInfo('companyId')];
+
+ return $where;
+ }
+
+ /**
+ * 获取流量池列表
+ *
+ * @param array $where
+ * @return \think\Paginator
+ */
+ protected function getPoolListByCompanyId(array $where, $isPage = true)
+ {
+ $query = TrafficPoolModel::alias('p')
+ ->field(
+ [
+ 'p.id', 'p.identifier', 'p.mobile', 'p.wechatId', 'p.identifier',
+ 's.fromd', 's.status', 's.createTime', 's.companyId', 's.sourceId', 's.type',
+ 'wa.nickname', 'wa.avatar', 'wa.gender', 'wa.phone', 'wa.alias'
+ ]
+ )
+ ->join('traffic_source s', 'p.identifier=s.identifier')
+ ->join('wechat_account wa', 'p.identifier=wa.wechatId', 'left')
+ ->join('traffic_source_package_item tspi', 'p.identifier = tspi.identifier AND s.companyId = tspi.companyId', 'left')
+ ->join('traffic_source_package tsp', 'tspi.packageId=tsp.id', 'left')
+ ->join('device_wechat_login d', 's.sourceId=d.wechatId', 'left')
+ ->where($where);
+
+
+ $result = $query->order('p.id DESC,s.id DESC')->group('p.identifier');
+
+ if ($isPage) {
+ $result = $query->paginate($this->request->param('limit/d', 10), false, ['page' => $this->request->param('page/d', 1)]);
+ $list = $result->items();
+ $total = $result->total();
+ } else {
+ $list = $result->select();
+ $total = '';
+ }
+
+
+ if ($isPage) {
+ foreach ($list as &$item) {
+ //流量池筛选
+ $package = Db::name('traffic_source_package_item')->alias('tspi')
+ ->join('traffic_source_package p', 'tspi.packageId=p.id AND tspi.companyId=p.companyId')
+ ->where(['tspi.identifier' => $item->identifier])
+ ->whereIn('tspi.companyId', [0, $item->companyId])
+ ->column('p.name');
+ $item['packages'] = $package;
+ if ($item->type == 1) {
+ $tag = Db::name('wechat_friendship')->where(['wechatId' => $item->wechatId])->column('tags');
+ $tags = [];
+ foreach ($tag as $k => $v) {
+ $v = json_decode($v, true);
+ if (!empty($v)) {
+ $tags = array_merge($tags, $v);
+ }
+ }
+ $item['tags'] = $tags;
+ }
+
+ }
+ }
+ unset($item);
+ $data = ['list' => $list, 'total' => $total];
+ return json_encode($data, JSON_UNESCAPED_UNICODE);
+ }
+
+ /**
+ * 获取流量池列表
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ $result = $this->getPoolListByCompanyId($this->makeWhere());
+ $result = json_decode($result, true);
+ return ResponseHelper::success(
+ [
+ 'list' => $result['list'],
+ 'total' => $result['total'],
+ ]
+ );
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+
+ public function getUser()
+ {
+
+ $wechatId = $this->request->param('wechatId', '');
+ $companyId = $this->getUserInfo('companyId');
+
+ if (empty($wechatId)) {
+ return json_encode(['code' => 500, 'msg' => '微信id不能为空']);
+ }
+
+ $total = [
+ 'msg' => 0,
+ 'money' => 0,
+ 'isFriend' => false,
+ 'percentage' => '0.00%',
+ ];
+
+
+ $data = TrafficPoolModel::alias('p')
+ ->field(['p.id', 'p.identifier', 'p.wechatId',
+ 'wa.nickname', 'wa.avatar', 'wa.gender', 'wa.phone', 'wa.alias'])
+ ->join('wechat_account wa', 'p.identifier=wa.wechatId', 'left')
+ ->order('p.id DESC')
+ ->where(['p.identifier' => $wechatId])
+ ->group('p.identifier')
+ ->find();
+ $data['lastMsgTime'] = '';
+
+ //来源
+ $source = Db::name('traffic_source')->alias('ts')
+ ->field(['wa.nickname', 'wa.avatar', 'wa.gender', 'wa.phone', 'wa.wechatId', 'wa.alias',
+ 'ts.createTime',
+ 'wf.id as friendId', 'wf.wechatAccountId'])
+ ->join('wechat_account wa', 'ts.sourceId=wa.wechatId', 'left')
+ ->join(['s2_wechat_friend' => 'wf'], 'wa.wechatId=wf.ownerWechatId', 'left')
+ ->where(['ts.companyId' => $companyId, 'ts.identifier' => $data['identifier'], 'wf.wechatId' => $data['wechatId']])
+ ->order('ts.createTime DESC')
+ ->select();
+
+ $wechatFriendId = [];
+ if (!empty($source)) {
+ $total['isFriend'] = true;
+ foreach ($source as &$v) {
+ $wechatFriendId[] = $v['friendId'];
+ //最后消息
+ $v['createTime'] = date('Y-m-d H:i:s', $v['createTime']);
+ $lastMsgTime = Db::table('s2_wechat_message')
+ ->where(['wechatFriendId' => $v['friendId'], 'wechatAccountId' => $v['wechatAccountId']])
+ ->value('wechatTime');
+ $v['lastMsgTime'] = !empty($lastMsgTime) ? date('Y-m-d H:i:s', $lastMsgTime) : '';
+
+ //设备信息
+ $device = Db::name('device_wechat_login')->alias('dwl')
+ ->join('device d', 'd.id=dwl.deviceId')
+ ->where(['dwl.wechatId' => $v['wechatId']])
+ ->field('d.id,d.memo,d.imei,d.brand,d.extra,d.alive')
+ ->order('dwl.id DESC')
+ ->find();
+ $extra = json_decode($device['extra'], true);
+ unset($device['extra']);
+ $device['address'] = !empty($extra['address']) ? $extra['address'] : '';
+ $v['device'] = $device;
+ }
+ unset($v);
+ }
+ $data['source'] = $source;
+
+
+ //流量池
+ $package = Db::name('traffic_source_package_item')->alias('tspi')
+ ->join('traffic_source_package p', 'tspi.packageId=p.id AND tspi.companyId=p.companyId')
+ ->where(['tspi.companyId' => $companyId, 'tspi.identifier' => $data['identifier']])
+ ->column('p.name');
+ $package2 = Db::name('traffic_source_package_item')->alias('tspi')
+ ->join('traffic_source_package p', 'tspi.packageId=p.id')
+ ->where(['tspi.companyId' => $companyId, 'tspi.identifier' => $data['identifier']])
+ ->column('p.name');
+ $packages = array_merge($package, $package2);
+ $data['packages'] = $packages;
+
+
+ if (!empty($wechatFriendId)) {
+ //消息统计
+ $msgTotal = Db::table('s2_wechat_message')
+ ->whereIn('wechatFriendId', $wechatFriendId)
+ ->count();
+ $total['msg'] = $msgTotal;
+
+ //金额计算
+ $money = Db::table('s2_wechat_message')
+ ->whereIn('wechatFriendId', $wechatFriendId)
+ ->where(['isSend' => 1, 'msgType' => 419430449])
+ ->select();
+ if (!empty($money)) {
+ foreach ($money as $v) {
+ $content = json_decode($v['content'], true);
+ if ($content['paysubtype'] == 1) {
+ $number = number_format(str_replace("¥", "", $content['feedesc']), 2);
+ $floatValue = floatval($number);
+ $total['money'] += $floatValue;
+ }
+ }
+ }
+ }
+
+ $taskNum = Db::name('task_customer')->alias('tc')
+ ->join('customer_acquisition_task t', 'tc.task_id=t.id')
+ ->where(['t.companyId' => $companyId, 't.deleteTime' => 0])
+ ->whereIn('tc.phone', [$data['phone'], $data['wechatId'], $data['alias']])
+ ->count();
+
+ $passNum = Db::name('task_customer')->alias('tc')
+ ->join('customer_acquisition_task t', 'tc.task_id=t.id')
+ ->where(['t.companyId' => $companyId, 't.deleteTime' => 0, 'tc.status' => 4])
+ ->whereIn('tc.phone', [$data['phone'], $data['wechatId'], $data['alias']])
+ ->count();
+
+ if (!empty($taskNum) && !empty($passNum)) {
+ $percentage = number_format(($taskNum / $passNum) * 100, 2);
+ $total['percentage'] = $percentage;
+ }
+
+
+ $data['total'] = $total;
+ $data['rmm'] = [
+ 'r' => 0,
+ 'f' => 0,
+ 'm' => 0,
+ ];
+ return ResponseHelper::success($data);
+ }
+
+
+ /**
+ * 用户旅程
+ * @return false|string
+ * @throws \think\db\exception\DataNotFoundException
+ * @throws \think\db\exception\ModelNotFoundException
+ * @throws \think\exception\DbException
+ */
+ public function getUserJourney()
+ {
+ $page = $this->request->param('page', 1);
+ $pageSize = $this->request->param('pageSize', 10);
+ $userId = $this->request->param('userId', '');
+ if (empty($userId)) {
+ return json_encode(['code' => 500, 'msg' => '用户id不能为空']);
+ }
+
+ $query = Db::name('user_portrait')
+ ->field('id,type,trafficPoolId,remark,count,createTime,updateTime')
+ ->where(['trafficPoolId' => $userId]);
+
+ $total = $query->count();
+
+ $list = $query->order('createTime desc')
+ ->page($page, $pageSize)
+ ->select();
+
+
+ foreach ($list as $k => $v) {
+ $list[$k]['createTime'] = date('Y-m-d H:i:s', $v['createTime']);
+ $list[$k]['updateTime'] = date('Y-m-d H:i:s', $v['updateTime']);
+ }
+ return ResponseHelper::success(['list' => $list, 'total' => $total]);
+
+ }
+
+
+ public function getUserTags()
+ {
+ $userId = $this->request->param('userId', '');
+ $companyId = $this->getUserInfo('companyId');
+ if (empty($userId)) {
+ return json_encode(['code' => 500, 'msg' => '用户id不能为空']);
+ }
+ $data = Db::name('traffic_pool')->alias('tp')
+ ->join('wechat_friendship f', 'tp.wechatId=f.wechatId AND f.companyId=' . $companyId, 'left')
+ ->join(['s2_wechat_friend' => 'wf'], 'f.wechatId=wf.wechatId', 'left')
+ ->where(['tp.id' => $userId])
+ ->order('tp.createTime desc')
+ ->column('wf.id,wf.labels,wf.siteLabels');
+ if (empty($data)) {
+ return ResponseHelper::success(['wechat' => [], 'siteLabels' => []]);
+ }
+
+
+ $tags = [];
+ $siteLabels = [];
+ foreach ($data as $k => $v) {
+ $tag = json_decode($v['labels'], true);
+ $tag2 = json_decode($v['siteLabels'], true);
+ if (!empty($tag)) {
+ $tags = array_merge($tags, $tag);
+ }
+ if (!empty($tag2)) {
+ $siteLabels = array_merge($siteLabels, $tag2);
+ }
+ }
+ $tags = array_unique($tags);
+ $tags = array_values($tags);
+ $siteLabels = array_unique($siteLabels);
+ $siteLabels = array_values($siteLabels);
+ return ResponseHelper::success(['wechat' => $tags, 'siteLabels' => $siteLabels]);
+ }
+
+
+
+
+
+ public function addPackage()
+ {
+ try {
+ $type = $this->request->param('type', '');
+ $addPackageId = $this->request->param('addPackageId', '');
+ $packageName = $this->request->param('packageName', '');
+ $userIds = $this->request->param('userIds', []);
+ $tableFile = $this->request->param('tableFile', '');
+
+
+ $companyId = $this->getUserInfo('companyId');
+ $userId = $this->getUserInfo('id');
+ if (empty($addPackageId) && empty($packageName)) {
+ return ResponseHelper::error('存储的流量池不能为空');
+ }
+
+ if (empty($type)) {
+ return ResponseHelper::error('请选择类型');
+ }
+
+ if (!empty($addPackageId)) {
+ $package = Db::name('traffic_source_package')
+ ->where(['id' => $addPackageId, 'isDel' => 0])
+ ->whereIn('companyId', [$companyId, 0])
+ ->field('id,name')
+ ->find();
+ if (empty($package)) {
+ return ResponseHelper::error('该流量池不存在');
+ }
+ $packageId = $package['id'];
+ } else {
+ $package = Db::name('traffic_source_package')
+ ->where(['isDel' => 0, 'name' => $packageName])
+ ->whereIn('companyId', [$companyId, 0])
+ ->field('id,name')
+ ->find();
+ if (!empty($package)) {
+ return ResponseHelper::error('该流量池名称已存在');
+ }
+ $packageId = Db::name('traffic_source_package')->insertGetId([
+ 'userId' => $userId,
+ 'companyId' => $companyId,
+ 'name' => $packageName,
+ 'matchingRules' => json_encode($this->makeWhere()),
+ 'createTime' => time(),
+ 'isDel' => 0,
+ ]);
+ }
+
+
+ if ($type == 1) {
+ $result = $this->getPoolListByCompanyId($this->makeWhere(), false);
+ $result = json_decode($result, true);
+ $result = array_column($result['list'], 'identifier');
+ } elseif ($type == 2) {
+ if (empty($packageId)) {
+ return ResponseHelper::error('选择的用户');
+ }
+ //================== 表格数据处理 ==================
+ if (!is_array($userIds)) {
+ return ResponseHelper::error('选择的用户类型错误');
+ }
+ $result = Db::name('traffic_pool')->alias('tp')
+ ->join('traffic_source tc', 'tp.identifier=tc.identifier')
+ ->whereIn('tp.id', $userIds)
+ ->where(['companyId' => $companyId])
+ ->group('tp.identifier')
+ ->column('tc.identifier');
+ } else {
+ /*if (empty($tableFile)){
+ return ResponseHelper::error('请上传用户文件');
+ }
+
+ // 先下载到本地临时文件,再分析,最后删除
+ $originPath = $tableFile;
+ $tmpFile = tempnam(sys_get_temp_dir(), 'user_');
+ // 判断是否为远程文件
+ if (preg_match('/^https?:\/\//i', $originPath)) {
+ // 远程URL,下载到本地
+ $fileContent = file_get_contents($originPath);
+ if ($fileContent === false) {
+ exit('远程文件下载失败: ' . $originPath);
+ }
+ file_put_contents($tmpFile, $fileContent);
+ } else {
+ // 本地文件,直接copy
+ if (!file_exists($originPath)) {
+ exit('文件不存在: ' . $originPath);
+ }
+ copy($originPath, $tmpFile);
+ }
+ // 解析临时文件
+ $ext = strtolower(pathinfo($originPath, PATHINFO_EXTENSION));
+ $rows = [];
+ if (in_array($ext, ['xls', 'xlsx'])) {
+ // 直接用composer自动加载的PHPExcel
+ $excel = \PHPExcel_IOFactory::load($tmpFile);
+ $sheet = $excel->getActiveSheet();
+ $data = $sheet->toArray();
+ if (count($data) > 1) {
+ array_shift($data); // 去掉表头
+ }
+
+ foreach ($data as $cols) {
+ $rows[] = [
+ 'name' => isset($cols[0]) ? trim($cols[0]) : '',
+ 'phone' => isset($cols[1]) ? trim($cols[1]) : '',
+ 'source' => isset($cols[2]) ? trim($cols[2]) : '',
+ ];
+ }
+ } elseif ($ext === 'csv') {
+ $content = file_get_contents($tmpFile);
+ $lines = preg_split('/\r\n|\r|\n/', $content);
+ if (count($lines) > 1) {
+ array_shift($lines); // 去掉表头
+ foreach ($lines as $line) {
+ if (trim($line) === '') continue;
+ $cols = str_getcsv($line);
+ if (count($cols) >= 6) {
+ $rows[] = [
+ 'name' => isset($cols[0]) ? trim($cols[0]) : '',
+ 'phone' => isset($cols[1]) ? trim($cols[1]) : '',
+ 'source' => isset($cols[2]) ? trim($cols[2]) : '',
+ ];
+ }
+ }
+ }
+ } else {
+ unlink($tmpFile);
+ exit('暂不支持的文件类型: ' . $ext);
+ }
+ // 删除临时文件
+ unlink($tmpFile);*/
+ //================== 表格数据处理 ==================
+ }
+
+ $rows = [
+ ['name' => '张三', 'phone' => '18883458888', 'source' => '234'],
+ ['name' => '李四', 'phone' => '18878988889', 'source' => '456'],
+ ];
+
+
+ if (in_array($type, [1, 2])) {
+ // 1000条为一组进行批量处理
+ $batchSize = 1000;
+ $totalRows = count($result);
+
+ for ($i = 0; $i < $totalRows; $i += $batchSize) {
+ $batchRows = array_slice($result, $i, $batchSize);
+ if (!empty($batchRows)) {
+ // 2. 批量查询已存在的手机
+ $existing = Db::name('traffic_source_package_item')
+ ->where(['companyId' => $companyId, 'packageId' => $packageId])
+ ->whereIn('identifier', $batchRows)
+ ->field('identifier')
+ ->select();
+ $existingPhones = array_column($existing, 'identifier');
+ // 3. 过滤出新数据,批量插入
+ $newData = [];
+ foreach ($batchRows as $row) {
+ if (!in_array($row, $existingPhones)) {
+ $newData[] = [
+ 'packageId' => $packageId,
+ 'companyId' => $companyId,
+ 'identifier' => $row,
+ 'createTime' => time(),
+ ];
+ }
+ }
+ // 4. 批量插入新数据
+ if (!empty($newData)) {
+ Db::name('traffic_source_package_item')->insertAll($newData);
+ }
+ }
+ }
+ } else {
+ // 1000条为一组进行批量处理
+ $batchSize = 1000;
+ $totalRows = count($rows);
+
+ try {
+ for ($i = 0; $i < $totalRows; $i += $batchSize) {
+ Db::startTrans();
+ $batchRows = array_slice($rows, $i, $batchSize);
+ if (!empty($batchRows)) {
+ $identifiers = array_column($batchRows, 'phone');
+ //流量池处理
+ $existing = Db::name('traffic_pool')
+ ->whereIn('identifier', $identifiers)
+ ->column('identifier');
+
+ $newData = [];
+ foreach ($batchRows as $row) {
+ if (!in_array($row['phone'], $existing)) {
+ $newData[] = [
+ 'identifier' => $row['phone'],
+ 'mobile' => $row['phone'],
+ 'createTime' => time(),
+ ];
+ }
+ }
+ if (!empty($newData)) {
+ Db::name('traffic_pool')->insertAll($newData);
+ }
+
+ //流量池来源处理
+ $newData2 = [];
+ $existing2 = Db::name('traffic_source')
+ ->where(['companyId' => $companyId])
+ ->whereIn('identifier', $identifiers)
+ ->column('identifier');
+ foreach ($batchRows as $row) {
+ if (!in_array($row['phone'], $existing2)) {
+ $newData2[] = [
+ 'type' => 0,
+ 'name' => $row['name'],
+ 'identifier' => $row['phone'],
+ 'fromd' => $row['source'],
+ 'companyId' => $companyId,
+ 'createTime' => time(),
+ 'updateTime' => time(),
+ ];
+ }
+ }
+ if (!empty($newData2)) {
+ Db::name('traffic_source')->insertAll($newData2);
+ }
+
+ //流量池包数据处理
+ $newData3 = [];
+ $existing3 = Db::name('traffic_source_package_item')
+ ->where(['companyId' => $companyId, 'packageId' => $packageId])
+ ->whereIn('identifier', $identifiers)
+ ->field('identifier')
+ ->select();
+ foreach ($batchRows as $row) {
+ if (!in_array($row['phone'], $existing3)) {
+ $newData3[] = [
+ 'packageId' => $packageId,
+ 'companyId' => $companyId,
+ 'identifier' => $row['phone'],
+ 'createTime' => time(),
+ ];
+ }
+ }
+ if (!empty($newData3)) {
+ Db::name('traffic_source_package_item')->insertAll($newData3);
+ }
+
+ Db::commit();
+ }
+ }
+ } catch (\Exception $e) {
+ DB::rollback();
+ }
+ }
+
+
+ return ResponseHelper::success('添加成功');
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+
+
+ /* public function editUserTags()
+ {
+ $userId = $this->request->param('userId', '');
+ if (empty($userId)) {
+ return json_encode(['code' => 500, 'msg' => '用户id不能为空']);
+ }
+ $tags = $this->request->param('tags', []);
+ $tags = $this->request->param('tags', []);
+ $isWechat = $this->request->param('isWechat', false);
+ $companyId = $this->getUserInfo('companyId');
+
+ $friend = Db::name('traffic_pool')->alias('tp')
+ ->join('wechat_friendship f', 'tp.wechatId=f.wechatId AND f.companyId='.$companyId, 'left')
+ ->join(['s2_wechat_friend' => 'wf'], 'f.wechatId=wf.wechatId', 'left')
+ ->where(['tp.id' => $userId])
+ ->order('tp.createTime desc')
+ ->column('wf.id,wf.accountId,wf.labels,wf.siteLabels');
+ if (empty($data)) {
+ return ResponseHelper::error('该用户不存在');
+ }
+
+
+ }*/
+
+
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/traffic/GetPotentialTypeSectionV1Controller.php b/application/cunkebao/controller/traffic/GetPotentialTypeSectionV1Controller.php
new file mode 100644
index 0000000..c1ccfb4
--- /dev/null
+++ b/application/cunkebao/controller/traffic/GetPotentialTypeSectionV1Controller.php
@@ -0,0 +1,62 @@
+ TrafficSourceModel::STATUS_PENDING,
+ 'name' => '待处理'
+ ],
+ [
+ 'id' => TrafficSourceModel::STATUS_WORKING,
+ 'name' => '处理中'
+ ],
+ [
+ 'id' => TrafficSourceModel::STATUS_REFUSED,
+ 'name' => '已拒绝'
+ ],
+ [
+ 'id' => TrafficSourceModel::STATUS_EXPIRED,
+ 'name' => '已过期'
+ ],
+ [
+ 'id' => TrafficSourceModel::STATUS_CANCELED,
+ 'name' => '已取消'
+ ]
+ ];
+ }
+
+ /**
+ * 获取流量池状态筛选列表
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ return ResponseHelper::success(
+ $this->getTypeSectionCols()
+ );
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/traffic/GetTrafficSourceSectionV1Controller.php b/application/cunkebao/controller/traffic/GetTrafficSourceSectionV1Controller.php
new file mode 100644
index 0000000..273e395
--- /dev/null
+++ b/application/cunkebao/controller/traffic/GetTrafficSourceSectionV1Controller.php
@@ -0,0 +1,45 @@
+ $this->getUserInfo('companyId')
+ ]
+ )
+ ->field('fromd name,id')->group('fromd')->select()->toArray();
+ }
+
+ /**
+ * 获取流量来源筛选列表
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ return ResponseHelper::success(
+ $this->getSourceSectionCols()
+ );
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/wechat/GetWechatController.php b/application/cunkebao/controller/wechat/GetWechatController.php
new file mode 100644
index 0000000..23a4b60
--- /dev/null
+++ b/application/cunkebao/controller/wechat/GetWechatController.php
@@ -0,0 +1,189 @@
+WechatCustomerModel)) {
+ $this->WechatCustomerModel = WechatCustomerModel::where(
+ [
+ 'wechatId' => $wechatId,
+ 'companyId' => $this->getUserInfo('companyId')
+ ]
+ )
+ ->find();
+ }
+
+ return $this->WechatCustomerModel;
+ }
+
+
+ /**
+ * 获取昨日聊天次数
+ *
+ * @param WechatCustomerModel $customer
+ * @return int
+ */
+ protected function getChatTimesPerDay(?WechatCustomerModel $customer): int
+ {
+ return $customer->activity->yesterdayMsgCount ?? 0;
+ }
+
+ /**
+ * 总聊天数量
+ *
+ * @param WechatCustomerModel $customer
+ * @return int
+ */
+ protected function getChatTimesTotal(?WechatCustomerModel $customer): int
+ {
+ return $customer->activity->totalMsgCount ?? 0;
+ }
+
+ /**
+ * 计算活跃程度(根据消息数)
+ *
+ * @param string $wechatId
+ * @return string
+ */
+ protected function getActivityLevel(string $wechatId): array
+ {
+ $customer = $this->getWechatCustomerModel($wechatId);
+
+ return [
+ 'allTimes' => $this->getChatTimesTotal($customer),
+ 'dayTimes' => $this->getChatTimesPerDay($customer),
+ ];
+ }
+
+ /**
+ * 获取限制记录
+ *
+ * @param string $wechatId
+ * @return array
+ */
+ protected function getRestrict(string $wechatId): array
+ {
+ return WechatRestrictsModel::alias('r')
+ ->field(
+ [
+ 'r.id', 'r.restrictTime date', 'r.level', 'r.reason'
+ ]
+ )
+ ->where('r.wechatId', $wechatId)->select()
+ ->toArray();
+ }
+
+ /**
+ * 获取账号权重
+ *
+ * @param string $wechatId
+ * @return array
+ */
+ protected function getAccountWeight(string $wechatId): array
+ {
+ $customer = $this->getWechatCustomerModel($wechatId);
+ $seeders = $customer ? (array)$customer->weight : array();
+
+ // 严谨返回
+ return ArrHelper::getValue('ageWeight,activityWeigth,restrictWeight,realNameWeight,scope', $seeders, 0);
+ }
+
+ /**
+ * 获取当日最高添加好友记录
+ *
+ * @param string $wechatId
+ * @return int
+ * @throws \Exception
+ */
+ protected function getAccountWeightAddLimit(string $wechatId): int
+ {
+ return $this->getWechatCustomerModel($wechatId)->weight->addLimit ?? 20;
+ }
+
+ /**
+ * 计算今日新增好友数量
+ *
+ * @param string $ownerWechatId
+ * @return int
+ */
+ protected function getTodayNewFriendCount(string $ownerWechatId): int
+ {
+
+ return Db::table('s2_friend_task')
+ ->where('wechatId',$ownerWechatId)
+ ->whereBetween('createTime', [strtotime(date('Y-m-d 00:00:00')), strtotime(date('Y-m-d 23:59:59'))])
+ ->count();
+ }
+
+ /**
+ * 获取账号加友统计数据.
+ *
+ * @param string $wechatId
+ * @return array
+ */
+ protected function getStatistics(string $wechatId): array
+ {
+ return [
+ 'todayAdded' => $this->getTodayNewFriendCount($wechatId),
+ 'addLimit' => $this->getAccountWeightAddLimit($wechatId)
+ ];
+ }
+
+
+
+
+ public function getWechatInfo()
+ {
+ $wechatId = $this->request->param('wechatId','');
+ if (empty($wechatId)) {
+ return ResponseHelper::error('微信id不能为空');
+ }
+ $userInfo = Db::name('wechat_customer')->alias('wc')
+ ->join('wechat_account wa','wc.wechatId = wa.wechatId')
+ ->where(['wc.wechatId' => $wechatId])
+ ->field('wc.*,wa.nickname,wa.alias,wa.avatar,wa.gender')
+ ->find();
+
+ if (empty($userInfo)){
+ return ResponseHelper::error('该微信不存在');
+ }
+ $accountAge= !empty($userInfo['createTime']) ? date('Y-m-d H:i:s',$userInfo['createTime']) : date('Y-m-d H:i:s');
+ unset($userInfo['basic'],$userInfo['companyId'],$userInfo['createTime'],$userInfo['updateTime'],$userInfo['id']);
+
+ $userInfo['weight'] = json_decode($userInfo['weight'],true);
+ $userInfo['activity'] = json_decode($userInfo['activity'],true);
+ $userInfo['friendShip'] = json_decode($userInfo['friendShip'],true);
+
+
+ $newData = [
+ 'userInfo' => $userInfo,
+ 'accountAge' => $accountAge,
+ 'activityLevel' => $this->getActivityLevel($wechatId),
+ 'accountWeight' => $this->getAccountWeight($wechatId),
+ 'statistics' => $this->getStatistics($wechatId),
+// 'restrictions' => $this->getRestrict($wechatId),
+ ];
+
+
+
+ return ResponseHelper::success($newData);
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/wechat/GetWechatMomentsV1Controller.php b/application/cunkebao/controller/wechat/GetWechatMomentsV1Controller.php
new file mode 100644
index 0000000..32dae25
--- /dev/null
+++ b/application/cunkebao/controller/wechat/GetWechatMomentsV1Controller.php
@@ -0,0 +1,364 @@
+getUserInfo('companyId'))
+ ->column('id');
+ }
+
+ /**
+ * 非主操盘手仅可查看分配到的设备
+ *
+ * @return array
+ */
+ protected function getUserDevicesId(): array
+ {
+ return DeviceUserModel::where([
+ 'userId' => $this->getUserInfo('id'),
+ 'companyId' => $this->getUserInfo('companyId'),
+ ])->column('deviceId');
+ }
+
+ /**
+ * 获取当前用户可访问的设备ID
+ *
+ * @return array
+ */
+ protected function getDevicesId(): array
+ {
+ return ($this->getUserInfo('isAdmin') == UserModel::ADMIN_STP)
+ ? $this->getCompanyDevicesId()
+ : $this->getUserDevicesId();
+ }
+
+ /**
+ * 获取用户可访问的微信ID集合
+ *
+ * @return array
+ * @throws \Exception
+ */
+ protected function getAccessibleWechatIds(): array
+ {
+ $deviceIds = $this->getDevicesId();
+ if (empty($deviceIds)) {
+ throw new \Exception('暂无可用设备', 200);
+ }
+
+ return DeviceWechatLoginModel::distinct(true)
+ ->where('companyId', $this->getUserInfo('companyId'))
+ ->whereIn('deviceId', $deviceIds)
+ ->column('wechatId');
+ }
+
+ /**
+ * 查看朋友圈列表
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ $wechatId = $this->request->param('wechatId/s', '');
+ if (empty($wechatId)) {
+ return ResponseHelper::error('wechatId不能为空');
+ }
+
+ // 权限校验:只能查看当前账号可访问的微信
+ $accessibleWechatIds = $this->getAccessibleWechatIds();
+ if (!in_array($wechatId, $accessibleWechatIds, true)) {
+ return ResponseHelper::error('无权查看该微信的朋友圈', 403);
+ }
+
+ // 获取对应的微信账号ID
+ $accountId = Db::table('s2_wechat_account')
+ ->where('wechatId', $wechatId)
+ ->value('id');
+
+ if (empty($accountId)) {
+ return ResponseHelper::error('微信账号不存在或尚未同步', 404);
+ }
+
+ $query = Db::table('s2_wechat_moments')
+ ->where('wechatAccountId', $accountId)
+ ->where('userName', $wechatId);
+
+ // 关键词搜索
+ if ($keyword = trim((string)$this->request->param('keyword', ''))) {
+ $query->whereLike('content', '%' . $keyword . '%');
+ }
+
+ // 类型筛选
+ $type = $this->request->param('type', '');
+ if ($type !== '' && $type !== null) {
+ $query->where('type', (int)$type);
+ }
+
+ // 时间筛选
+ $startTime = $this->request->param('startTime', '');
+ $endTime = $this->request->param('endTime', '');
+ if ($startTime || $endTime) {
+ $start = $startTime ? strtotime($startTime) : 0;
+ $end = $endTime ? strtotime($endTime) : time();
+ if ($start && $end && $end < $start) {
+ return ResponseHelper::error('结束时间不能早于开始时间');
+ }
+ $query->whereBetween('createTime', [$start ?: 0, $end ?: time()]);
+ }
+
+ $page = (int)$this->request->param('page', 1);
+ $limit = (int)$this->request->param('limit', 10);
+
+ $paginator = $query->order('createTime', 'desc')
+ ->paginate($limit, false, ['page' => $page]);
+
+ $list = array_map(function ($item) {
+ return $this->formatMomentRow($item);
+ }, $paginator->items());
+
+ return ResponseHelper::success([
+ 'list' => $list,
+ 'total' => $paginator->total(),
+ 'page' => $page,
+ 'limit' => $limit,
+ ]);
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500);
+ }
+ }
+
+ /**
+ * 导出朋友圈数据到Excel
+ *
+ * @return void
+ */
+ public function export()
+ {
+ try {
+ $wechatId = $this->request->param('wechatId/s', '');
+ if (empty($wechatId)) {
+ return ResponseHelper::error('wechatId不能为空');
+ }
+
+ // 权限校验:只能查看当前账号可访问的微信
+ $accessibleWechatIds = $this->getAccessibleWechatIds();
+ if (!in_array($wechatId, $accessibleWechatIds, true)) {
+ return ResponseHelper::error('无权查看该微信的朋友圈', 403);
+ }
+
+ // 获取对应的微信账号ID
+ $accountId = Db::table('s2_wechat_account')
+ ->where('wechatId', $wechatId)
+ ->value('id');
+
+ if (empty($accountId)) {
+ return ResponseHelper::error('微信账号不存在或尚未同步', 404);
+ }
+
+ $query = Db::table('s2_wechat_moments')
+ ->where('wechatAccountId', $accountId);
+
+ // 关键词搜索
+ if ($keyword = trim((string)$this->request->param('keyword', ''))) {
+ $query->whereLike('content', '%' . $keyword . '%');
+ }
+
+ // 类型筛选
+ $type = $this->request->param('type', '');
+ if ($type !== '' && $type !== null) {
+ $query->where('type', (int)$type);
+ }
+
+ // 时间筛选
+ $startTime = $this->request->param('startTime', '');
+ $endTime = $this->request->param('endTime', '');
+ if ($startTime || $endTime) {
+ $start = $startTime ? strtotime($startTime) : 0;
+ $end = $endTime ? strtotime($endTime) : time();
+ if ($start && $end && $end < $start) {
+ return ResponseHelper::error('结束时间不能早于开始时间');
+ }
+ $query->whereBetween('createTime', [$start ?: 0, $end ?: time()]);
+ }
+
+ // 获取所有数据(不分页)
+ $moments = $query->order('createTime', 'desc')->select();
+
+ if (empty($moments)) {
+ return ResponseHelper::error('暂无数据可导出');
+ }
+
+ // 定义表头
+ $headers = [
+ 'date' => '日期',
+ 'postTime' => '投放时间',
+ 'functionCategory' => '作用分类',
+ 'content' => '朋友圈文案',
+ 'selfReply' => '自回评内容',
+ 'displayForm' => '朋友圈展示形式',
+ 'image1' => '配图1',
+ 'image2' => '配图2',
+ 'image3' => '配图3',
+ 'image4' => '配图4',
+ 'image5' => '配图5',
+ 'image6' => '配图6',
+ 'image7' => '配图7',
+ 'image8' => '配图8',
+ 'image9' => '配图9',
+ ];
+
+ // 格式化数据
+ $rows = [];
+ foreach ($moments as $moment) {
+ $resUrls = $this->decodeJson($moment['resUrls'] ?? null);
+ $imageUrls = is_array($resUrls) ? $resUrls : [];
+
+ // 格式化日期和时间
+ $createTime = !empty($moment['createTime'])
+ ? (is_numeric($moment['createTime']) ? $moment['createTime'] : strtotime($moment['createTime']))
+ : 0;
+ $date = $createTime ? date('Y年m月d日', $createTime) : '';
+ $postTime = $createTime ? date('H:i', $createTime) : '';
+
+ // 判断展示形式
+ $displayForm = '';
+ if (!empty($moment['content']) && !empty($imageUrls)) {
+ $displayForm = '文字+图片';
+ } elseif (!empty($moment['content'])) {
+ $displayForm = '文字';
+ } elseif (!empty($imageUrls)) {
+ $displayForm = '图片';
+ }
+
+ $row = [
+ 'date' => $date,
+ 'postTime' => $postTime,
+ 'functionCategory' => '', // 暂时放空
+ 'content' => $moment['content'] ?? '',
+ 'selfReply' => '', // 暂时放空
+ 'displayForm' => $displayForm,
+ ];
+
+ // 分配图片到配图1-9列
+ for ($i = 1; $i <= 9; $i++) {
+ $imageKey = 'image' . $i;
+ $row[$imageKey] = isset($imageUrls[$i - 1]) ? $imageUrls[$i - 1] : '';
+ }
+
+ $rows[] = $row;
+ }
+
+ // 定义图片列(配图1-9)
+ $imageColumns = ['image1', 'image2', 'image3', 'image4', 'image5', 'image6', 'image7', 'image8', 'image9'];
+
+ // 生成文件名
+ $fileName = '朋友圈投放_' . date('Ymd_His');
+
+ // 调用导出方法,优化图片显示效果
+ ExportController::exportExcelWithImages(
+ $fileName,
+ $headers,
+ $rows,
+ $imageColumns,
+ '朋友圈投放',
+ [
+ 'imageWidth' => 120, // 图片宽度(像素)
+ 'imageHeight' => 120, // 图片高度(像素)
+ 'imageColumnWidth' => 18, // 图片列宽(Excel单位)
+ 'rowHeight' => 130, // 行高(像素)
+ 'columnWidths' => [ // 特定列的固定宽度
+ 'date' => 15, // 日期列宽
+ 'postTime' => 12, // 投放时间列宽
+ 'functionCategory' => 15, // 作用分类列宽
+ 'content' => 40, // 朋友圈文案列宽(自动调整可能不够)
+ 'selfReply' => 30, // 自回评内容列宽
+ 'displayForm' => 18, // 朋友圈展示形式列宽
+ ],
+ 'titleRow' => [ // 标题行内容(第一行)
+ '朋友圈投放',
+ '我能提供什么价值? (40%) 有谁正在和我合作 (20%) 如何和我合作? (20%) 你找我合作需要付多少钱? (20%)'
+ ]
+ ]
+ );
+ } catch (\Exception $e) {
+ return ResponseHelper::error('导出失败:' . $e->getMessage(), 500);
+ }
+ }
+
+ /**
+ * 格式化朋友圈数据
+ *
+ * @param array $row
+ * @return array
+ */
+ protected function formatMomentRow(array $row): array
+ {
+ $formatTime = function ($timestamp) {
+ if (empty($timestamp)) {
+ return '';
+ }
+ return is_numeric($timestamp)
+ ? date('Y-m-d H:i:s', $timestamp)
+ : date('Y-m-d H:i:s', strtotime($timestamp));
+ };
+
+ return [
+ 'id' => (int)$row['id'],
+ 'snsId' => $row['snsId'] ?? '',
+ 'type' => (int)($row['type'] ?? 0),
+ 'content' => $row['content'] ?? '',
+ 'commentList' => $this->decodeJson($row['commentList'] ?? null),
+ 'likeList' => $this->decodeJson($row['likeList'] ?? null),
+ 'resUrls' => $this->decodeJson($row['resUrls'] ?? null),
+ 'createTime' => $formatTime($row['createTime'] ?? null),
+ 'momentEntity' => [
+ 'lat' => $row['lat'] ?? 0,
+ 'lng' => $row['lng'] ?? 0,
+ 'location' => $row['location'] ?? '',
+ 'picSize' => $row['picSize'] ?? 0,
+ 'userName' => $row['userName'] ?? '',
+ ],
+ ];
+ }
+
+ /**
+ * JSON字段解析
+ *
+ * @param mixed $value
+ * @return array
+ */
+ protected function decodeJson($value): array
+ {
+ if (empty($value)) {
+ return [];
+ }
+
+ if (is_array($value)) {
+ return $value;
+ }
+
+ $decoded = json_decode($value, true);
+ return $decoded ?: [];
+ }
+}
+
diff --git a/application/cunkebao/controller/wechat/GetWechatOnDeviceFriendsV1Controller.php b/application/cunkebao/controller/wechat/GetWechatOnDeviceFriendsV1Controller.php
new file mode 100644
index 0000000..cd544c4
--- /dev/null
+++ b/application/cunkebao/controller/wechat/GetWechatOnDeviceFriendsV1Controller.php
@@ -0,0 +1,105 @@
+items() as $item) {
+ $item->tags = json_decode($item->tags);
+ array_push($resultSets, $item->toArray());
+ }
+
+ return $resultSets;
+ }
+
+ /**
+ * 根据微信账号ID获取好友列表
+ *
+ * @param array $where
+ * @return \think\Paginator 分页对象
+ */
+ protected function getFriendsByWechatIdAndQueryParams(array $where): \think\Paginator
+ {
+ $query = WechatFriendShipModel::alias('f')
+ ->field(
+ [
+ 'w.id', 'w.nickname', 'w.avatar', 'w.wechatId',
+ 'CASE WHEN w.alias IS NULL OR w.alias = "" THEN w.wechatId ELSE w.alias END AS wechatAccount',
+ 'f.memo', 'f.tags',
+ 'ff.accountUserName', 'ff.accountRealName','ff.id AS friendId'
+ ]
+ )
+ ->join('wechat_account w', 'w.wechatId = f.wechatId')
+ ->join(['s2_wechat_friend' => 'ff'], 'ff.id = f.id');
+
+ foreach ($where as $key => $value) {
+ if (is_numeric($key) && is_array($value) && isset($value[0]) && $value[0] === 'exp') {
+ $query->whereExp('', $value[1]);
+ continue;
+ }
+
+ $query->where($key, $value);
+ }
+
+ return $query->paginate($this->request->param('limit/d', 10), false, ['page' => $this->request->param('page/d', 1)]);
+ }
+
+ /**
+ * 构建查询条件
+ *
+ * @param array $params
+ * @return array
+ */
+ protected function makeWhere(array $params = []): array
+ {
+ // 关键词搜索(同时搜索好友备注和标签)
+ if (!empty($keyword = $this->request->param('keyword'))) {
+ $where[] = ['exp', "f.memo LIKE '%{$keyword}%' OR f.tags LIKE '%{$keyword}%'"];
+ }
+
+ $where['f.ownerWechatId'] = $this->request->param('id/s') ?: 'x_x';
+
+ return array_merge($where, $params);
+ }
+
+ /**
+ * 获取微信好友列表
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ $result = $this->getFriendsByWechatIdAndQueryParams(
+ $this->makeWhere()
+ );
+
+ return ResponseHelper::success(
+ [
+ 'list' => $this->makeResultedSet($result),
+ 'total' => $result->total(),
+ ]
+ );
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/wechat/GetWechatOnDeviceSummarizeV1Controller.php b/application/cunkebao/controller/wechat/GetWechatOnDeviceSummarizeV1Controller.php
new file mode 100644
index 0000000..c3eb6c6
--- /dev/null
+++ b/application/cunkebao/controller/wechat/GetWechatOnDeviceSummarizeV1Controller.php
@@ -0,0 +1,190 @@
+WechatCustomerModel)) {
+ $this->WechatCustomerModel = WechatCustomerModel::where(
+ [
+ 'wechatId' => $wechatId,
+ 'companyId' => $this->getUserInfo('companyId')
+ ]
+ )
+ ->find();
+ }
+
+ return $this->WechatCustomerModel;
+ }
+
+ /**
+ * 计算账号年龄(从创建时间到现在)
+ *
+ * @param string $wechatId
+ * @return string
+ */
+ protected function getRegisterDate(string $wechatId): string
+ {
+ return $this->getWechatCustomerModel($wechatId)->basic->registerDate ?? date('Y-m-d', time());
+ }
+
+ /**
+ * 获取昨日聊天次数
+ *
+ * @param WechatCustomerModel $customer
+ * @return int
+ */
+ protected function getChatTimesPerDay(?WechatCustomerModel $customer): int
+ {
+ return $customer->activity->yesterdayMsgCount ?? 0;
+ }
+
+ /**
+ * 总聊天数量
+ *
+ * @param WechatCustomerModel $customer
+ * @return int
+ */
+ protected function getChatTimesTotal(?WechatCustomerModel $customer): int
+ {
+ return $customer->activity->totalMsgCount ?? 0;
+ }
+
+ /**
+ * 计算活跃程度(根据消息数)
+ *
+ * @param string $wechatId
+ * @return string
+ */
+ protected function getActivityLevel(string $wechatId): array
+ {
+ $customer = $this->getWechatCustomerModel($wechatId);
+
+ return [
+ 'allTimes' => $this->getChatTimesTotal($customer),
+ 'dayTimes' => $this->getChatTimesPerDay($customer),
+ ];
+ }
+
+ /**
+ * 获取限制记录
+ *
+ * @param string $wechatId
+ * @return array
+ */
+ protected function getRestrict(string $wechatId): array
+ {
+ return WechatRestrictsModel::alias('r')
+ ->field(
+ [
+ 'r.id', 'r.restrictTime date', 'r.level', 'r.reason'
+ ]
+ )
+ ->where('r.wechatId', $wechatId)->select()
+ ->toArray();
+ }
+
+ /**
+ * 获取账号权重
+ *
+ * @param string $wechatId
+ * @return array
+ */
+ protected function getAccountWeight(string $wechatId): array
+ {
+ $customer = $this->getWechatCustomerModel($wechatId);
+ $seeders = $customer ? (array)$customer->weight : array();
+
+ // 严谨返回
+ return ArrHelper::getValue('ageWeight,activityWeigth,restrictWeight,realNameWeight,scope', $seeders, 0);
+ }
+
+ /**
+ * 获取当日最高添加好友记录
+ *
+ * @param string $wechatId
+ * @return int
+ * @throws \Exception
+ */
+ protected function getAccountWeightAddLimit(string $wechatId): int
+ {
+ return $this->getWechatCustomerModel($wechatId)->weight->addLimit ?? 0;
+ }
+
+ /**
+ * 计算今日新增好友数量
+ *
+ * @param string $ownerWechatId
+ * @return int
+ */
+ protected function getTodayNewFriendCount(string $ownerWechatId): int
+ {
+ return WechatFriendShipModel::where(compact('ownerWechatId'))
+ ->whereBetween('createTime',
+ [
+ strtotime(date('Y-m-d 00:00:00')),
+ strtotime(date('Y-m-d 23:59:59'))
+ ]
+ )
+ ->count('*');
+ }
+
+ /**
+ * 获取账号加友统计数据.
+ *
+ * @param string $wechatId
+ * @return array
+ */
+ protected function getStatistics(string $wechatId): array
+ {
+ return [
+ 'todayAdded' => $this->getTodayNewFriendCount($wechatId),
+ 'addLimit' => $this->getAccountWeightAddLimit($wechatId)
+ ];
+ }
+
+ /**
+ * 获取微信号详情
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ $wechatId = $this->request->param('id/s');
+
+ return ResponseHelper::success(
+ [
+ 'accountAge' => $this->getRegisterDate($wechatId),
+ 'activityLevel' => $this->getActivityLevel($wechatId),
+ 'accountWeight' => $this->getAccountWeight($wechatId),
+ 'statistics' => $this->getStatistics($wechatId),
+ 'restrictions' => $this->getRestrict($wechatId),
+ ]
+ );
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/wechat/GetWechatOverviewV1Controller.php b/application/cunkebao/controller/wechat/GetWechatOverviewV1Controller.php
new file mode 100644
index 0000000..a48156d
--- /dev/null
+++ b/application/cunkebao/controller/wechat/GetWechatOverviewV1Controller.php
@@ -0,0 +1,411 @@
+request->param('wechatId', '');
+
+ if (empty($wechatId)) {
+ return ResponseHelper::error('微信ID不能为空');
+ }
+
+ $companyId = $this->getUserInfo('companyId');
+
+ // 获取微信账号ID(accountId)
+ $account = Db::table('s2_wechat_account')
+ ->where('wechatId', $wechatId)
+ ->find();
+
+ if (empty($account)) {
+ return ResponseHelper::error('微信账号不存在');
+ }
+
+ $accountId = $account['id'];
+
+ // 1. 健康分评估
+ $healthScoreData = $this->getHealthScoreAssessment($accountId, $wechatId);
+
+ // 2. 账号价值(模拟数据)
+ $accountValue = $this->getAccountValue($accountId);
+
+ // 3. 今日价值变化(模拟数据)
+ $todayValueChange = $this->getTodayValueChange($accountId);
+
+ // 4. 好友总数
+ $totalFriends = $this->getTotalFriends($wechatId, $companyId);
+
+ // 5. 今日新增好友
+ $todayNewFriends = $this->getTodayNewFriends($wechatId);
+
+ // 6. 高价群聊
+ $highValueChatrooms = $this->getHighValueChatrooms($wechatId, $companyId);
+
+ // 7. 今日新增群聊
+ $todayNewChatrooms = $this->getTodayNewChatrooms($wechatId, $companyId);
+
+ $result = [
+ 'healthScoreAssessment' => $healthScoreData,
+ 'accountValue' => $accountValue,
+ 'todayValueChange' => $todayValueChange,
+ 'totalFriends' => $totalFriends,
+ 'todayNewFriends' => $todayNewFriends,
+ 'highValueChatrooms' => $highValueChatrooms,
+ 'todayNewChatrooms' => $todayNewChatrooms,
+ ];
+
+ return ResponseHelper::success($result);
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500);
+ }
+ }
+
+ /**
+ * 获取健康分评估数据
+ *
+ * @param int $accountId 账号ID
+ * @param string $wechatId 微信ID
+ * @return array
+ */
+ protected function getHealthScoreAssessment($accountId, $wechatId)
+ {
+ // 获取健康分信息
+ $healthScoreService = new WechatAccountHealthScoreService();
+ $healthScoreInfo = $healthScoreService->getHealthScore($accountId);
+
+ $healthScore = $healthScoreInfo['healthScore'] ?? 0;
+ $maxAddFriendPerDay = $healthScoreInfo['maxAddFriendPerDay'] ?? 0;
+
+ // 获取今日已加好友数
+ $todayAdded = $this->getTodayAddedCount($wechatId);
+
+ // 获取最后添加时间
+ $lastAddTime = $this->getLastAddTime($wechatId);
+
+ // 判断状态标签
+ $statusTag = $todayAdded > 0 ? '已添加加人' : '';
+
+ // 获取基础构成
+ $baseComposition = $this->getBaseComposition($healthScoreInfo);
+
+ // 获取动态记录
+ $dynamicRecords = $this->getDynamicRecords($healthScoreInfo);
+
+ return [
+ 'score' => $healthScore,
+ 'dailyLimit' => $maxAddFriendPerDay,
+ 'todayAdded' => $todayAdded,
+ 'lastAddTime' => $lastAddTime,
+ 'statusTag' => $statusTag,
+ 'baseComposition' => $baseComposition,
+ 'dynamicRecords' => $dynamicRecords,
+ ];
+ }
+
+ /**
+ * 获取基础构成数据
+ *
+ * @param array $healthScoreInfo 健康分信息
+ * @return array
+ */
+ protected function getBaseComposition($healthScoreInfo)
+ {
+ $baseScore = $healthScoreInfo['baseScore'] ?? 0;
+ $baseInfoScore = $healthScoreInfo['baseInfoScore'] ?? 0;
+ $friendCountScore = $healthScoreInfo['friendCountScore'] ?? 0;
+ $friendCount = $healthScoreInfo['friendCount'] ?? 0;
+
+ // 账号基础分(默认60分)
+ $accountBaseScore = 60;
+
+ // 已修改微信号(如果baseInfoScore > 0,说明已修改)
+ $isModifiedAlias = $baseInfoScore > 0;
+
+ $composition = [
+ [
+ 'name' => '账号基础分',
+ 'score' => $accountBaseScore,
+ 'formatted' => '+' . $accountBaseScore,
+ ]
+ ];
+
+ // 如果已修改微信号,添加基础信息分
+ if ($isModifiedAlias) {
+ $composition[] = [
+ 'name' => '已修改微信号',
+ 'score' => $baseInfoScore,
+ 'formatted' => '+' . $baseInfoScore,
+ ];
+ }
+
+ // 好友数量加成
+ if ($friendCountScore > 0) {
+ $composition[] = [
+ 'name' => '好友数量加成',
+ 'score' => $friendCountScore,
+ 'formatted' => '+' . $friendCountScore,
+ 'friendCount' => $friendCount, // 显示好友总数
+ ];
+ }
+
+ return $composition;
+ }
+
+ /**
+ * 获取动态记录数据
+ *
+ * @param array $healthScoreInfo 健康分信息
+ * @return array
+ */
+ protected function getDynamicRecords($healthScoreInfo)
+ {
+ $records = [];
+
+ $frequentPenalty = $healthScoreInfo['frequentPenalty'] ?? 0;
+ $frequentCount = $healthScoreInfo['frequentCount'] ?? 0;
+ $banPenalty = $healthScoreInfo['banPenalty'] ?? 0;
+ $isBanned = $healthScoreInfo['isBanned'] ?? 0;
+ $noFrequentBonus = $healthScoreInfo['noFrequentBonus'] ?? 0;
+ $consecutiveNoFrequentDays = $healthScoreInfo['consecutiveNoFrequentDays'] ?? 0;
+ $lastFrequentTime = $healthScoreInfo['lastFrequentTime'] ?? null;
+
+ // 频繁扣分记录
+ // 根据frequentCount判断是首次还是再次
+ // frequentPenalty存储的是当前状态的扣分(-15或-25),不是累计值
+ if ($frequentCount > 0 && $frequentPenalty < 0) {
+ if ($frequentCount == 1) {
+ // 首次频繁:-15分
+ $records[] = [
+ 'name' => '首次触发限额',
+ 'score' => $frequentPenalty,
+ 'formatted' => (string)$frequentPenalty,
+ 'type' => 'penalty',
+ 'time' => $lastFrequentTime ? date('Y-m-d H:i:s', $lastFrequentTime) : null,
+ ];
+ } else {
+ // 再次频繁:-25分
+ $records[] = [
+ 'name' => '再次触发限额',
+ 'score' => $frequentPenalty,
+ 'formatted' => (string)$frequentPenalty,
+ 'type' => 'penalty',
+ 'time' => $lastFrequentTime ? date('Y-m-d H:i:s', $lastFrequentTime) : null,
+ ];
+ }
+ }
+
+ // 封号扣分记录
+ if ($isBanned && $banPenalty < 0) {
+ $lastBanTime = $healthScoreInfo['lastBanTime'] ?? null;
+ $records[] = [
+ 'name' => '封号',
+ 'score' => $banPenalty,
+ 'formatted' => (string)$banPenalty,
+ 'type' => 'penalty',
+ 'time' => $lastBanTime ? date('Y-m-d H:i:s', $lastBanTime) : null,
+ ];
+ }
+
+ // 不频繁加分记录
+ if ($noFrequentBonus > 0 && $consecutiveNoFrequentDays >= 3) {
+ $lastNoFrequentTime = $healthScoreInfo['lastNoFrequentTime'] ?? null;
+ $records[] = [
+ 'name' => '连续' . $consecutiveNoFrequentDays . '天不触发频繁',
+ 'score' => $noFrequentBonus,
+ 'formatted' => '+' . $noFrequentBonus,
+ 'type' => 'bonus',
+ 'time' => $lastNoFrequentTime ? date('Y-m-d H:i:s', $lastNoFrequentTime) : null,
+ ];
+ }
+
+ return $records;
+ }
+
+ /**
+ * 获取今日已加好友数
+ *
+ * @param string $wechatId 微信ID
+ * @return int
+ */
+ protected function getTodayAddedCount($wechatId)
+ {
+ $start = strtotime(date('Y-m-d 00:00:00'));
+ $end = strtotime(date('Y-m-d 23:59:59'));
+
+ return Db::table('s2_friend_task')
+ ->where('wechatId', $wechatId)
+ ->whereBetween('createTime', [$start, $end])
+ ->count();
+ }
+
+ /**
+ * 获取最后添加时间
+ *
+ * @param string $wechatId 微信ID
+ * @return string
+ */
+ protected function getLastAddTime($wechatId)
+ {
+ $lastTask = Db::table('s2_friend_task')
+ ->where('wechatId', $wechatId)
+ ->order('createTime', 'desc')
+ ->find();
+
+ if (empty($lastTask) || empty($lastTask['createTime'])) {
+ return '';
+ }
+
+ return date('H:i:s', $lastTask['createTime']);
+ }
+
+ /**
+ * 获取账号价值(模拟数据)
+ *
+ * @param int $accountId 账号ID
+ * @return array
+ */
+ protected function getAccountValue($accountId)
+ {
+ // TODO: 后续替换为真实计算逻辑
+ // 模拟数据:¥29,800
+ $value = 29800;
+
+ return [
+ 'value' => $value,
+ 'formatted' => '¥' . number_format($value, 0, '.', ','),
+ ];
+ }
+
+ /**
+ * 获取今日价值变化(模拟数据)
+ *
+ * @param int $accountId 账号ID
+ * @return array
+ */
+ protected function getTodayValueChange($accountId)
+ {
+ // TODO: 后续替换为真实计算逻辑
+ // 模拟数据:+500
+ $change = 500;
+
+ return [
+ 'change' => $change,
+ 'formatted' => $change > 0 ? '+' . $change : (string)$change,
+ 'isPositive' => $change > 0,
+ ];
+ }
+
+ /**
+ * 获取好友总数
+ *
+ * @param string $wechatId 微信ID
+ * @param int $companyId 公司ID
+ * @return int
+ */
+ protected function getTotalFriends($wechatId, $companyId)
+ {
+ // 优先从 s2_wechat_account 表获取
+ $account = Db::table('s2_wechat_account')
+ ->where('wechatId', $wechatId)
+ ->field('totalFriend')
+ ->find();
+
+ if (!empty($account) && isset($account['totalFriend'])) {
+ return (int)$account['totalFriend'];
+ }
+
+ // 如果 totalFriend 为空,则从 s2_wechat_friend 表统计
+ return Db::table('s2_wechat_friend')
+ ->where('ownerWechatId', $wechatId)
+ ->where('isDeleted', 0)
+ ->count();
+ }
+
+ /**
+ * 获取今日新增好友数
+ *
+ * @param string $wechatId 微信ID
+ * @return int
+ */
+ protected function getTodayNewFriends($wechatId)
+ {
+ $start = strtotime(date('Y-m-d 00:00:00'));
+ $end = strtotime(date('Y-m-d 23:59:59'));
+
+ // 从 s2_wechat_friend 表统计今日新增
+ return Db::table('s2_wechat_friend')
+ ->where('ownerWechatId', $wechatId)
+ ->whereBetween('createTime', [$start, $end])
+ ->where('isDeleted', 0)
+ ->count();
+ }
+
+ /**
+ * 获取高价群聊数量
+ * 高价群聊定义:群成员数 >= 50 的群聊
+ *
+ * @param string $wechatId 微信ID
+ * @param int $companyId 公司ID
+ * @return int
+ */
+ protected function getHighValueChatrooms($wechatId, $companyId)
+ {
+ // 高价群聊定义:群成员数 >= 50
+ $minMemberCount = 50;
+
+ // 查询该微信账号下的高价群聊
+ // 使用子查询统计每个群的成员数
+ $result = Db::query("
+ SELECT COUNT(DISTINCT c.chatroomId) as count
+ FROM s2_wechat_chatroom c
+ INNER JOIN (
+ SELECT chatroomId, COUNT(*) as memberCount
+ FROM s2_wechat_chatroom_member
+ GROUP BY chatroomId
+ HAVING memberCount >= ?
+ ) m ON c.chatroomId = m.chatroomId
+ WHERE c.wechatAccountWechatId = ?
+ AND c.isDeleted = 0
+ ", [$minMemberCount, $wechatId]);
+
+ return !empty($result) ? (int)$result[0]['count'] : 0;
+ }
+
+ /**
+ * 获取今日新增群聊数
+ *
+ * @param string $wechatId 微信ID
+ * @param int $companyId 公司ID
+ * @return int
+ */
+ protected function getTodayNewChatrooms($wechatId, $companyId)
+ {
+ $start = strtotime(date('Y-m-d 00:00:00'));
+ $end = strtotime(date('Y-m-d 23:59:59'));
+
+ return Db::table('s2_wechat_chatroom')
+ ->where('wechatAccountWechatId', $wechatId)
+ ->whereBetween('createTime', [$start, $end])
+ ->where('isDeleted', 0)
+ ->count();
+ }
+}
+
diff --git a/application/cunkebao/controller/wechat/GetWechatProfileV1Controller.php b/application/cunkebao/controller/wechat/GetWechatProfileV1Controller.php
new file mode 100644
index 0000000..ad7a7f5
--- /dev/null
+++ b/application/cunkebao/controller/wechat/GetWechatProfileV1Controller.php
@@ -0,0 +1,115 @@
+field('t.id')
+ ->join('traffic_source s', 's.identifier = p.identifier')
+ ->where('p.wechatId', $wechatId)
+ ->value('fromd');
+ }
+
+ /**
+ * 获取微信账号
+ *
+ * @param string $wechatId
+ * @return array
+ * @throws \Exception
+ */
+ protected function getWechatAccountProfileByWechatId(string $wechatId): array
+ {
+ $account = WechatAccountModel::alias('w')
+ ->field(
+ [
+ 'w.id', 'w.avatar', 'w.nickname', 'w.region', 'w.wechatId',
+ 'CASE WHEN w.alias IS NULL OR w.alias = "" THEN w.wechatId ELSE w.alias END AS wechatId',
+ 'f.createTime', 'f.tags', 'f.memo'
+ ]
+ )
+ ->join('wechat_friendship f', 'w.wechatId=f.wechatId')
+ ->where('w.wechatId', $wechatId)
+ ->find();
+
+ if (is_null($account)) {
+ throw new \Exception('未获取到微信账号数据', 404);
+ }
+
+ return $account->toArray();
+ }
+
+ /**
+ * 获取微信好友详情
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ $results = $this->getWechatAccountProfileByWechatId(
+ $this->request->param('wechatId/s')
+ );
+
+ return ResponseHelper::success(
+ array_merge($results, [
+ 'playDate' => $this->getLastPlayTime($results['wechatId']),
+ 'source' => $this->getTrafficSource($results['wechatId']),
+ 'tags' => $this->getWechatTags($results['tags']),
+ 'addDate' => $this->getAddShipDate($results['createTime']),
+ ])
+ );
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/wechat/GetWechatsOnDevicesV1Controller.php b/application/cunkebao/controller/wechat/GetWechatsOnDevicesV1Controller.php
new file mode 100644
index 0000000..0b50aa9
--- /dev/null
+++ b/application/cunkebao/controller/wechat/GetWechatsOnDevicesV1Controller.php
@@ -0,0 +1,422 @@
+ $this->getUserInfo('companyId')
+ ]
+ )
+ ->column('id');
+ }
+
+ /**
+ * 非主操盘手获取分配的设备
+ *
+ * @return array
+ */
+ protected function getUserDevicesId(): array
+ {
+ return DeviceUserModel::where(
+ [
+ 'userId' => $this->getUserInfo('id'),
+ 'companyId' => $this->getUserInfo('companyId')
+ ]
+ )
+ ->column('deviceId');
+ }
+
+ /**
+ * 根据不同角色,显示的设备数量不同
+ *
+ * @return array
+ * @throws \Exception
+ */
+ protected function getDevicesId(): array
+ {
+ return ($this->getUserInfo('isAdmin') == UserModel::ADMIN_STP)
+ ? $this->getCompanyDevicesId() // 主操盘手获取所有的设备
+ : $this->getUserDevicesId(); // 非主操盘手获取分配的设备
+ }
+
+ /**
+ * 获取有登录设备的微信id
+ * 优化:使用索引字段,减少数据查询量
+ *
+ * @return array
+ */
+ protected function getWechatIdsOnDevices(): array
+ {
+ // 关联设备id查询,过滤掉已删除的设备
+ if (empty($deviceIds = $this->getDevicesId())) {
+ throw new \Exception('暂无设备数据', 200);
+ }
+
+ // 优化:直接使用DISTINCT减少数据传输量
+ return DeviceWechatLoginModel::distinct(true)
+ ->where([
+ 'companyId' => $this->getUserInfo('companyId'),
+// 'alive' => DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE,
+ ])
+ ->where('deviceId', 'in', $deviceIds)
+ ->column('wechatId');
+ }
+
+ /**
+ * 构建查询条件
+ *
+ * @param array $params
+ * @return array
+ */
+ protected function makeWhere(array $params = []): array
+ {
+ if (empty($wechatIds = $this->getWechatIdsOnDevices())) {
+ throw new \Exception('设备尚未有登录微信', 200);
+ }
+
+ // 关键词搜索(同时搜索微信号和昵称)
+ if (!empty($keyword = $this->request->param('keyword'))) {
+ $where[] = ["w.wechatId|w.alias|w.nickname", 'LIKE', '%' . $keyword . '%'];
+ }
+
+ $where['w.wechatId'] = array('in', implode(',', $wechatIds));
+
+ return array_merge($where, $params);
+ }
+
+ /**
+ * 获取在线微信账号列表
+ * 优化:减少查询字段,使用索引,优化JOIN条件
+ *
+ * @param array $where
+ * @return \think\Paginator 分页对象
+ */
+ protected function getOnlineWechatList(array $where): \think\Paginator
+ {
+ // 获取微信在线状态筛选参数(1=在线,0=离线,不传=全部)
+ $wechatStatus = $this->request->param('wechatStatus');
+
+ // 优化:只查询必要字段,使用FORCE INDEX提示数据库使用索引
+ $query = WechatAccountModel::alias('w')
+ ->field(
+ [
+ 'w.id', 'w.nickname', 'w.avatar', 'w.wechatId',
+ 'CASE WHEN w.alias IS NULL OR w.alias = "" THEN w.wechatId ELSE w.alias END AS wechatAccount',
+ 'MAX(l.deviceId) as deviceId', 'MAX(l.alive) as alive' // 使用MAX确保GROUP BY时获取正确的在线状态
+ ]
+ )
+ // 优化:使用INNER JOIN代替LEFT JOIN,并添加索引提示
+ ->join('device_wechat_login l', 'w.wechatId = l.wechatId AND l.companyId = '. $this->getUserInfo('companyId'), 'INNER')
+ // 添加s2_wechat_account表的LEFT JOIN,用于筛选微信在线状态
+ ->join(['s2_wechat_account' => 'sa'], 'w.wechatId = sa.wechatId', 'LEFT')
+ ->group('w.wechatId')
+ // 优化:在线状态优先排序(alive=1的排在前面),然后按wechatId排序
+ // 注意:ORDER BY使用SELECT中定义的别名alive,而不是聚合函数
+ ->order('alive desc, w.wechatId desc');
+
+ // 根据wechatStatus参数筛选(1=在线,0=离线,不传=全部)
+ if ($wechatStatus !== null && $wechatStatus !== '') {
+ $wechatStatus = (int)$wechatStatus;
+ if ($wechatStatus === 1) {
+ // 筛选在线:wechatAlive = 1
+ $query->where('sa.wechatAlive', 1);
+ } elseif ($wechatStatus === 0) {
+ // 筛选离线:wechatAlive = 0 或 NULL
+ $query->where(function($query) {
+ $query->where('sa.wechatAlive', 0)
+ ->whereOr('sa.wechatAlive', 'exp', 'IS NULL');
+ });
+ }
+ }
+
+ // 应用查询条件
+ foreach ($where as $key => $value) {
+ if (is_numeric($key) && is_array($value) && isset($value[0]) && $value[0] === 'exp') {
+ $query->whereExp('', $value[1]);
+ continue;
+ }
+
+ if (is_array($value)) {
+ $query->where($key, ...$value);
+ continue;
+ }
+
+ $query->where($key, $value);
+ }
+
+ // 优化:使用简单计数查询
+ return $query->paginate(
+ $this->request->param('limit/d', 10),
+ false,
+ ['page' => $this->request->param('page/d', 1)]
+ );
+ }
+
+ /**
+ * 构建返回数据
+ *
+ * @param \think\Paginator $result
+ * @return array
+ */
+ protected function makeResultedSet(\think\Paginator $result): array
+ {
+ $resultSets = [];
+ $items = $result->items();
+
+ if (empty($items)) {
+ return $resultSets;
+ }
+
+ $wechatIds = array_values(array_unique(array_map(function ($item) {
+ return $item->wechatId ?? ($item['wechatId'] ?? '');
+ }, $items)));
+
+ $metrics = $this->collectWechatMetrics($wechatIds);
+
+ foreach ($items as $item) {
+ $addLimit = $metrics['addLimit'][$item->wechatId] ?? 0;
+ $todayAdded = $metrics['todayAdded'][$item->wechatId] ?? 0;
+ // 计算今日可添加数量 = 可添加额度 - 今日已添加
+ $todayCanAdd = max(0, $addLimit - $todayAdded);
+
+ $sections = $item->toArray() + [
+ 'times' => $addLimit,
+ 'addedCount' => $todayAdded,
+ 'todayCanAdd' => $todayCanAdd, // 今日可添加数量
+ 'wechatStatus' => $metrics['wechatStatus'][$item->wechatId] ?? 0,
+ 'totalFriend' => $metrics['totalFriend'][$item->wechatId] ?? 0,
+ 'deviceMemo' => $metrics['deviceMemo'][$item->wechatId] ?? '',
+ 'activeTime' => $metrics['activeTime'][$item->wechatId] ?? '-',
+ ];
+
+ array_push($resultSets, $sections);
+ }
+
+ return $resultSets;
+ }
+
+ /**
+ * 批量收集微信账号的统计信息
+ * 优化:合并查询,减少数据库访问次数,使用缓存
+ *
+ * @param array $wechatIds
+ * @return array
+ */
+ protected function collectWechatMetrics(array $wechatIds): array
+ {
+ $metrics = [
+ 'addLimit' => [],
+ 'todayAdded' => [],
+ 'totalFriend' => [],
+ 'wechatStatus' => [],
+ 'deviceMemo' => [],
+ 'activeTime' => [],
+ ];
+
+ if (empty($wechatIds)) {
+ return $metrics;
+ }
+
+ $companyId = $this->getUserInfo('companyId');
+
+ // 使用缓存键,避免短时间内重复查询
+ $cacheKey = 'wechat_metrics_' . md5(implode(',', $wechatIds) . '_' . $companyId);
+
+ // 尝试从缓存获取数据(缓存5分钟)
+ $cachedMetrics = cache($cacheKey);
+ if ($cachedMetrics) {
+ return $cachedMetrics;
+ }
+
+ // 优化1:可添加好友额度 - 从s2_wechat_account_score表获取maxAddFriendPerDay
+ $scoreRows = Db::table('s2_wechat_account_score')
+ ->whereIn('wechatId', $wechatIds)
+ ->column('maxAddFriendPerDay', 'wechatId');
+ foreach ($scoreRows as $wechatId => $maxAddFriendPerDay) {
+ $metrics['addLimit'][$wechatId] = (int)($maxAddFriendPerDay ?? 0);
+ }
+
+ // 优化2:今日新增好友 - 使用索引字段和预计算
+ $start = strtotime(date('Y-m-d 00:00:00'));
+ $end = strtotime(date('Y-m-d 23:59:59'));
+
+ // 使用单次查询获取所有wechatIds的今日新增和总好友数
+ // 根据数据库结构使用s2_wechat_friend表而不是wechat_friend_ship
+ $friendshipStats = Db::query("
+ SELECT
+ ownerWechatId,
+ SUM(IF(createTime BETWEEN {$start} AND {$end}, 1, 0)) as today_added,
+ COUNT(*) as total_friend
+ FROM
+ s2_wechat_friend
+ WHERE
+ ownerWechatId IN ('" . implode("','", $wechatIds) . "')
+ AND isDeleted = 0
+ GROUP BY
+ ownerWechatId
+ ");
+
+ // 处理结果
+ foreach ($friendshipStats as $row) {
+ $wechatId = $row['ownerWechatId'] ?? '';
+ if ($wechatId) {
+ $metrics['todayAdded'][$wechatId] = (int)($row['today_added'] ?? 0);
+ $metrics['totalFriend'][$wechatId] = (int)($row['total_friend'] ?? 0);
+ }
+ }
+
+ // 优化3:微信在线状态 - 从s2_wechat_account表获取wechatAlive
+ $wechatAccountRows = Db::table('s2_wechat_account')
+ ->whereIn('wechatId', $wechatIds)
+ ->field('wechatId, wechatAlive')
+ ->select();
+
+ foreach ($wechatAccountRows as $row) {
+ $wechatId = $row['wechatId'] ?? '';
+ if (!empty($wechatId)) {
+ $metrics['wechatStatus'][$wechatId] = (int)($row['wechatAlive'] ?? 0);
+ }
+ }
+
+ // 优化4:设备状态与备注 - 使用INNER JOIN和索引
+ $loginRows = Db::name('device_wechat_login')
+ ->alias('l')
+ ->join('device d', 'd.id = l.deviceId', 'LEFT')
+ ->field('l.wechatId, l.alive, d.memo')
+ ->where('l.companyId', $companyId)
+ ->whereIn('l.wechatId', $wechatIds)
+ ->order('l.id', 'desc')
+ ->select();
+
+ // 使用临时数组避免重复处理
+ $processedWechatIds = [];
+ foreach ($loginRows as $row) {
+ $wechatId = $row['wechatId'] ?? '';
+ // 只处理每个wechatId的第一条记录(最新的)
+ if (!empty($wechatId) && !in_array($wechatId, $processedWechatIds)) {
+ // 如果s2_wechat_account表中没有wechatAlive,则使用device_wechat_login的alive作为备用
+ if (!isset($metrics['wechatStatus'][$wechatId])) {
+ $metrics['wechatStatus'][$wechatId] = (int)($row['alive'] ?? 0);
+ }
+ $metrics['deviceMemo'][$wechatId] = $row['memo'] ?? '';
+ $processedWechatIds[] = $wechatId;
+ }
+ }
+
+ // 优化5:活跃时间 - 使用JOIN减少查询次数
+ $activeTimeResults = Db::query("
+ SELECT
+ a.wechatId,
+ MAX(m.wechatTime) as lastTime
+ FROM
+ s2_wechat_account a
+ LEFT JOIN
+ s2_wechat_message m ON a.id = m.wechatAccountId
+ WHERE
+ a.wechatId IN ('" . implode("','", $wechatIds) . "')
+ GROUP BY
+ a.wechatId
+ ");
+
+ foreach ($activeTimeResults as $row) {
+ $wechatId = $row['wechatId'] ?? '';
+ $lastTime = (int)($row['lastTime'] ?? 0);
+ if (!empty($wechatId) && $lastTime > 0) {
+ $metrics['activeTime'][$wechatId] = date('Y-m-d H:i:s', $lastTime);
+ } else {
+ $metrics['activeTime'][$wechatId] = '-';
+ }
+ }
+
+ // 确保所有wechatId都有wechatStatus值(默认0)
+ foreach ($wechatIds as $wechatId) {
+ if (!isset($metrics['wechatStatus'][$wechatId])) {
+ $metrics['wechatStatus'][$wechatId] = 0;
+ }
+ }
+
+ // 存入缓存,有效期5分钟
+ cache($cacheKey, $metrics, 300);
+
+ return $metrics;
+ }
+
+ /**
+ * 获取在线微信账号列表
+ * 优化:添加缓存,优化分页逻辑
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ // 获取分页参数
+ $page = $this->request->param('page/d', 1);
+ $limit = $this->request->param('limit/d', 10);
+ $keyword = $this->request->param('keyword');
+ $wechatStatus = $this->request->param('wechatStatus');
+
+ // 创建缓存键(基于用户、分页、搜索条件和在线状态筛选)
+ $cacheKey = 'wechat_list_' . $this->getUserInfo('id') . '_' . $page . '_' . $limit . '_' . md5($keyword ?? '') . '_' . ($wechatStatus ?? 'all');
+
+ // 尝试从缓存获取数据(缓存2分钟)
+ $cachedData = cache($cacheKey);
+ if ($cachedData) {
+ return ResponseHelper::success($cachedData);
+ }
+
+ // 如果没有缓存,执行查询
+ $result = $this->getOnlineWechatList(
+ $this->makeWhere()
+ );
+
+ $responseData = [
+ 'list' => $this->makeResultedSet($result),
+ 'total' => $result->total(),
+ ];
+
+ // 存入缓存,有效期2分钟
+ cache($cacheKey, $responseData, 120);
+
+ return ResponseHelper::success($responseData);
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/wechat/GetWechatsRelatedDeviceV1Controller.php b/application/cunkebao/controller/wechat/GetWechatsRelatedDeviceV1Controller.php
new file mode 100644
index 0000000..92d20ce
--- /dev/null
+++ b/application/cunkebao/controller/wechat/GetWechatsRelatedDeviceV1Controller.php
@@ -0,0 +1,180 @@
+ $deviceId,
+ 'userId' => $this->getUserInfo('id'),
+ 'companyId' => $this->getUserInfo('companyId')
+ ]
+ )
+ ->count() > 0;
+
+ if (!$hasPermission) {
+ throw new \Exception('您没有权限查看该设备', 403);
+ }
+ }
+
+ /**
+ * 查询设备关联的微信ID列表
+ *
+ * @param int $deviceId
+ * @return array
+ */
+ protected function getDeviceWechatIds(int $deviceId): array
+ {
+ return DeviceWechatLoginModel::where(
+ [
+ 'deviceId' => $deviceId,
+ 'companyId' => $this->getUserInfo('companyId')
+ ]
+ )
+ ->group('wechatId')->column('wechatId');
+ }
+
+ /**
+ * 通过设备关联的微信号列表获取微信账号
+ *
+ * @param array $wechatIds
+ * @return ResultCollection
+ */
+ protected function getWechatAccountsByIds(array $wechatIds): ResultCollection
+ {
+ return WechatAccountModel::alias('w')
+ ->field([
+ 'w.wechatId', 'w.nickname', 'w.avatar', 'w.gender', 'w.createTime',
+ 'CASE WHEN w.alias IS NULL OR w.alias = "" THEN w.wechatId ELSE w.alias END AS wechatAccount',
+ ])
+ ->whereIn('w.wechatId', $wechatIds)
+ ->select();
+ }
+
+ /**
+ * TODO 通过微信id获取微信最后活跃时间
+ *
+ * @param int $time
+ * @return string
+ */
+ protected function getWechatLastActiveTime(string $wechatId): string
+ {
+ return date('Y-m-d H:i:s', time());
+ }
+
+ /**
+ * TODO 加友状态
+ *
+ * @param string $wechatId
+ * @return string
+ */
+ protected function getWechatStatusText(string $wechatId): string
+ {
+ return 1 ? '可加友' : '已停用';
+ }
+
+ /**
+ * TODO 账号状态
+ *
+ * @param string $wechatId
+ * @return string
+ */
+ protected function getWechatAliveText(string $wechatId): string
+ {
+ return 1 ? '正常' : '异常';
+ }
+
+ /**
+ * 统计微信好友
+ *
+ * @param string $ownerWechatId
+ * @return int
+ */
+ protected function getCountFriend(string $ownerWechatId): int
+ {
+ return WechatFriendShipModel::where(
+ [
+ 'ownerWechatId' => $ownerWechatId,
+ 'companyId' => $this->getUserInfo('companyId')
+ ]
+ )
+ ->count();
+ }
+
+ /**
+ * 获取设备关联的微信账号信息
+ *
+ * @param int $deviceId
+ * @return array
+ */
+ protected function getDeviceRelatedAccounts(int $deviceId): array
+ {
+ // 获取设备关联的微信ID列表
+ $wechatIds = $this->getDeviceWechatIds($deviceId);
+
+ if (!empty($wechatIds)) {
+ $collection = $this->getWechatAccountsByIds($wechatIds);
+
+ foreach ($collection as $account) {
+ $account->lastActive = $this->getWechatLastActiveTime($account->wechatId);
+ $account->statusText = $this->getWechatStatusText($account->wechatId);
+ $account->totalFriend = $this->getCountFriend($account->wechatId);
+ $account->wechatAliveText = $this->getWechatAliveText($account->wechatId);
+ }
+
+ return $collection->toArray();
+ }
+
+ return [];
+ }
+
+ /**
+ * 获取设备关联的微信账号
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ $deviceId = $this->request->param('id/d');
+
+ if ($this->getUserInfo('isAdmin') != UserModel::ADMIN_STP) {
+ $this->checkUserDevicePermission($deviceId);
+ }
+
+ // 获取设备关联的微信账号
+ $wechatAccounts = $this->getDeviceRelatedAccounts($deviceId);
+
+ return ResponseHelper::success(
+ [
+ 'deviceId' => $deviceId,
+ 'accounts' => $wechatAccounts,
+ 'total' => count($wechatAccounts)
+ ]
+ );
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/wechat/PostTransferFriends.php b/application/cunkebao/controller/wechat/PostTransferFriends.php
new file mode 100644
index 0000000..f408ae4
--- /dev/null
+++ b/application/cunkebao/controller/wechat/PostTransferFriends.php
@@ -0,0 +1,157 @@
+request->param('wechatId', '');
+ $inherit = $this->request->param('inherit', '');
+ $greeting = $this->request->param('greeting', '');
+ $firstMessage = $this->request->param('firstMessage', '');
+ $devices = $this->request->param('devices', []);
+ $companyId = $this->getUserInfo('companyId');
+
+
+ if (empty($wechatId)){
+ return ResponseHelper::error('迁移的微信不能为空');
+ }
+
+ if (empty($devices)){
+ return ResponseHelper::error('迁移的设备不能为空');
+ }
+ if (empty($greeting)){
+ return ResponseHelper::error('打招呼不能为空');
+ }
+ if (!is_array($devices)){
+ return ResponseHelper::error('迁移的设备必须为数组');
+ }
+
+ $wechat = Db::name('wechat_customer')->alias('wc')
+ ->join('wechat_account wa', 'wc.wechatId = wa.wechatId')
+ ->where(['wc.wechatId' => $wechatId])
+ ->field('wa.*')
+ ->find();
+
+ if (empty($wechat)) {
+ return ResponseHelper::error('该微信不存在');
+ }
+
+ $devices = Db::name('device')
+ ->where(['companyId' => $companyId,'deleteTime' => 0])
+ ->whereIn('id', $devices)
+ ->column('id');
+
+
+
+
+
+ try {
+ $sceneConf = [
+ 'enabled' => true,
+ 'posters' => [
+ 'id' => 'poster-3',
+ 'name' => '点击咨询',
+ 'src' => 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%92%A8%E8%AF%A2-FTiyAMAPop2g9LvjLOLDz0VwPg3KVu.gif'
+ ]
+ ];
+ $reqConf = [
+ 'device' => $devices,
+ 'startTime' => '09:00',
+ 'endTime' => '18:00',
+ 'remarkType' => 'phone',
+ 'addFriendInterval' => 60,
+ 'greeting' => !empty($greeting) ? $greeting :'我是'. $wechat['nickname'] .'的新号,请通过'
+ ];
+
+ if (!empty($firstMessage)){
+ $msgConf = [
+ [
+ 'day' => 0,
+ 'messages' => [
+ [
+ 'id' => 1,
+ 'type' => 'text',
+ 'content' => $firstMessage,
+ 'intervalUnit' => 'seconds',
+ 'sendInterval' => 5,
+ ]
+ ]
+ ]
+ ];
+ }else{
+ $msgConf = [];
+ }
+
+ // 使用容器获取控制器实例,而不是直接实例化
+ $createAddFriendPlan = app('app\cunkebao\controller\plan\PostCreateAddFriendPlanV1Controller');
+
+ $taskId = Db::name('customer_acquisition_task')->insertGetId([
+ 'name' => '迁移好友('. $wechat['nickname'] .')',
+ 'sceneId' => 10,
+ 'sceneConf' => json_encode($sceneConf,256),
+ 'reqConf' => json_encode($reqConf,256),
+ 'tagConf' => json_encode([]),
+ 'msgConf' => json_encode($msgConf,256),
+ 'userId' => $this->getUserInfo('id'),
+ 'companyId' => $companyId,
+ 'status' => 0,
+ 'createTime' => time(),
+ 'apiKey' => $createAddFriendPlan->generateApiKey(),
+ ]);
+
+ $friends = Db::table('s2_wechat_friend')
+ ->where(['ownerWechatId' => $wechatId])
+ ->group('wechatId')
+ ->order('id DESC')
+ ->column('id', 'wechatId,alias,phone,labels,conRemark');
+
+ // 1000条为一组进行批量处理
+ $batchSize = 1000;
+ $totalRows = count($friends);
+
+ for ($i = 0; $i < $totalRows; $i += $batchSize) {
+ $batchRows = array_slice($friends, $i, $batchSize);
+ if (!empty($batchRows)) {
+ $newData = [];
+ foreach ($batchRows as $row) {
+ if (!empty($row['phone'])) {
+ $phone = $row['phone'];
+ } elseif (!empty($row['alias'])) {
+ $phone = $row['alias'];
+ } else {
+ $phone = $row['wechatId'];
+ }
+
+ $tags = !empty($row['labels']) ? json_decode($row['labels'], true) : [];
+ $newData[] = [
+ 'task_id' => $taskId,
+ 'name' => '',
+ 'source' => '迁移好友('. $wechat['nickname'] .')',
+ 'phone' => $phone,
+ 'remark' => !empty($inherit) ? $row['conRemark'] : '',
+ 'tags' => !empty($inherit) ? json_encode($tags, JSON_UNESCAPED_UNICODE) : json_encode([]),
+ 'siteTags' => json_encode([]),
+ 'status' => 0,
+ 'createTime' => time(),
+ ];
+ }
+ Db::name('task_customer')->insertAll($newData);
+ }
+ }
+ return ResponseHelper::success('好友迁移创建成功' );
+ } catch (\Exception $e) {
+ // 回滚事务
+ Db::rollback();
+ return ResponseHelper::error('好友迁移创建失败:' . $e->getMessage());
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/workbench/CommonFunctionsController.php b/application/cunkebao/controller/workbench/CommonFunctionsController.php
new file mode 100644
index 0000000..d13d1dc
--- /dev/null
+++ b/application/cunkebao/controller/workbench/CommonFunctionsController.php
@@ -0,0 +1,50 @@
+getUserInfo('companyId');
+
+ // 从数据库查询常用功能列表
+ $functions = Db::name('workbench_function')
+ ->where('status', 1)
+ ->order('sort ASC, id ASC')
+ ->select();
+
+
+ // 处理数据,判断是否显示New标签(创建时间近1个月)
+ $oneMonthAgo = time() - 30 * 24 * 60 * 60; // 30天前的时间戳
+ foreach ($functions as &$function) {
+ // 判断是否显示New标签:创建时间在近1个月内
+ $function['isNew'] = ($function['createTime'] >= $oneMonthAgo) ? true : false;
+ $function['labels'] = json_decode($function['labels'],true);
+ }
+ unset($function);
+
+ return ResponseHelper::success([
+ 'list' => $functions
+ ]);
+
+ } catch (\Exception $e) {
+ return ResponseHelper::error('获取常用功能列表失败:' . $e->getMessage());
+ }
+ }
+
+
+}
+
diff --git a/application/cunkebao/controller/workbench/WorkbenchAutoLikeController.php b/application/cunkebao/controller/workbench/WorkbenchAutoLikeController.php
new file mode 100644
index 0000000..de112ba
--- /dev/null
+++ b/application/cunkebao/controller/workbench/WorkbenchAutoLikeController.php
@@ -0,0 +1,108 @@
+request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $workbenchId = $this->request->param('workbenchId', 0);
+
+ $where = [
+ ['wali.workbenchId', '=', $workbenchId]
+ ];
+
+ // 查询点赞记录
+ $list = Db::name('workbench_auto_like_item')->alias('wali')
+ ->join(['s2_wechat_moments' => 'wm'], 'wali.snsId = wm.snsId')
+ ->field([
+ 'wali.id',
+ 'wali.workbenchId',
+ 'wali.momentsId',
+ 'wali.snsId',
+ 'wali.wechatAccountId',
+ 'wali.wechatFriendId',
+ 'wali.createTime as likeTime',
+ 'wm.content',
+ 'wm.resUrls',
+ 'wm.createTime as momentTime',
+ 'wm.userName',
+ ])
+ ->where($where)
+ ->order('wali.createTime', 'desc')
+ ->group('wali.id')
+ ->page($page, $limit)
+ ->select();
+
+
+ // 处理数据
+ foreach ($list as &$item) {
+ //处理用户信息
+ $friend = Db::table('s2_wechat_friend')
+ ->where(['id' => $item['wechatFriendId']])
+ ->field('nickName,avatar')
+ ->find();
+ if (!empty($friend)) {
+ $item['friendName'] = $friend['nickName'];
+ $item['friendAvatar'] = $friend['avatar'];
+ } else {
+ $item['friendName'] = '';
+ $item['friendAvatar'] = '';
+ }
+
+
+ //处理客服
+ $friend = Db::table('s2_wechat_account')
+ ->where(['id' => $item['wechatAccountId']])
+ ->field('nickName,avatar')
+ ->find();
+ if (!empty($friend)) {
+ $item['operatorName'] = $friend['nickName'];
+ $item['operatorAvatar'] = $friend['avatar'];
+ } else {
+ $item['operatorName'] = '';
+ $item['operatorAvatar'] = '';
+ }
+
+ // 处理时间格式
+ $item['likeTime'] = date('Y-m-d H:i:s', $item['likeTime']);
+ $item['momentTime'] = !empty($item['momentTime']) ? date('Y-m-d H:i:s', $item['momentTime']) : '';
+
+ // 处理资源链接
+ if (!empty($item['resUrls'])) {
+ $item['resUrls'] = json_decode($item['resUrls'], true);
+ } else {
+ $item['resUrls'] = [];
+ }
+ }
+
+ // 获取总记录数
+ $total = Db::name('workbench_auto_like_item')->alias('wali')
+ ->where($where)
+ ->count();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]);
+ }
+}
+
diff --git a/application/cunkebao/controller/workbench/WorkbenchController.php b/application/cunkebao/controller/workbench/WorkbenchController.php
new file mode 100644
index 0000000..7f75653
--- /dev/null
+++ b/application/cunkebao/controller/workbench/WorkbenchController.php
@@ -0,0 +1,3353 @@
+request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ // 获取登录用户信息
+ $userInfo = request()->userInfo;
+
+ // 获取请求参数
+ $param = $this->request->post();
+
+
+ // 根据业务默认值补全参数
+ if (
+ isset($param['type']) &&
+ intval($param['type']) === self::TYPE_GROUP_PUSH
+ ) {
+ if (empty($param['startTime'])) {
+ $param['startTime'] = '09:00';
+ }
+ if (empty($param['endTime'])) {
+ $param['endTime'] = '21:00';
+ }
+ }
+
+ // 验证数据
+ $validate = new WorkbenchValidate;
+ if (!$validate->scene('create')->check($param)) {
+ return json(['code' => 400, 'msg' => $validate->getError()]);
+ }
+
+ Db::startTrans();
+ try {
+ // 创建工作台基本信息
+ $workbench = new Workbench;
+ $workbench->name = $param['name'];
+ $workbench->type = $param['type'];
+ $workbench->status = !empty($param['status']) ? 1 : 0;
+ $workbench->autoStart = !empty($param['autoStart']) ? 1 : 0;
+ $workbench->userId = $userInfo['id'];
+ $workbench->companyId = $userInfo['companyId'];
+ $workbench->createTime = time();
+ $workbench->updateTime = time();
+ $workbench->save();
+
+ // 根据类型创建对应的配置
+ switch ($param['type']) {
+ case self::TYPE_AUTO_LIKE: // 自动点赞
+ $config = new WorkbenchAutoLike;
+ $config->workbenchId = $workbench->id;
+ $config->interval = $param['interval'];
+ $config->maxLikes = $param['maxLikes'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->contentTypes = json_encode($param['contentTypes']);
+ $config->devices = json_encode($param['deviceGroups']);
+ $config->friends = json_encode($param['wechatFriends']);
+ // $config->targetGroups = json_encode($param['targetGroups']);
+ // $config->tagOperator = $param['tagOperator'];
+ $config->friendMaxLikes = $param['friendMaxLikes'];
+ $config->friendTags = $param['friendTags'];
+ $config->enableFriendTags = $param['enableFriendTags'];
+ $config->createTime = time();
+ $config->updateTime = time();
+ $config->save();
+ break;
+ case self::TYPE_MOMENTS_SYNC: // 朋友圈同步
+ $config = new WorkbenchMomentsSync;
+ $config->workbenchId = $workbench->id;
+ $config->syncInterval = $param['syncInterval'];
+ $config->syncCount = $param['syncCount'];
+ $config->syncType = $param['syncType'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->accountType = $param['accountType'];
+ $config->devices = json_encode($param['deviceGroups']);
+ $config->contentLibraries = json_encode($param['contentGroups'] ?? []);
+ $config->createTime = time();
+ $config->updateTime = time();
+ $config->save();
+ break;
+ case self::TYPE_GROUP_PUSH: // 群消息推送
+ $ownerWechatIds = $this->normalizeOwnerWechatIds($param['ownerWechatIds'] ?? []);
+ $groupPushData = $this->prepareGroupPushData($param, $ownerWechatIds);
+ $groupPushData['workbenchId'] = $workbench->id;
+ $groupPushData['createTime'] = time();
+ $groupPushData['updateTime'] = time();
+ $config = new WorkbenchGroupPush;
+ $config->save($groupPushData);
+ break;
+ case self::TYPE_GROUP_CREATE: // 自动建群
+ $config = new WorkbenchGroupCreate;
+ $config->workbenchId = $workbench->id;
+ $config->planType = !empty($param['planType']) ? $param['planType'] : 0;
+ $config->executorId = !empty($param['executorId']) ? $param['executorId'] : 0;
+
+ $config->devices = json_encode($param['deviceGroups'] ?? [], JSON_UNESCAPED_UNICODE);
+ $config->startTime = $param['startTime'] ?? '';
+ $config->endTime = $param['endTime'] ?? '';
+ $config->groupSizeMin = intval($param['groupSizeMin'] ?? 3);
+ $config->groupSizeMax = intval($param['groupSizeMax'] ?? 38);
+ $config->maxGroupsPerDay = intval($param['maxGroupsPerDay'] ?? 20);
+ $config->groupNameTemplate = $param['groupNameTemplate'] ?? '';
+ $config->groupDescription = $param['groupDescription'] ?? '';
+ $config->poolGroups = json_encode($param['poolGroups'] ?? [], JSON_UNESCAPED_UNICODE);
+ $config->wechatGroups = json_encode($param['wechatGroups'] ?? [], JSON_UNESCAPED_UNICODE);
+
+ // 处理群管理员:如果启用了群管理员且有指定管理员,则保存到admins字段
+ $admins = [];
+ if (!empty($param['groupAdminEnabled']) && !empty($param['groupAdminWechatId'])) {
+ // 如果groupAdminWechatId是数组,取第一个;如果是单个值,直接使用
+ $adminWechatId = is_array($param['groupAdminWechatId']) ? $param['groupAdminWechatId'][0] : $param['groupAdminWechatId'];
+ // 如果是好友ID,直接添加到admins;如果是wechatId,需要转换为好友ID
+ if (is_numeric($adminWechatId)) {
+ $admins[] = intval($adminWechatId);
+ } else {
+ // 如果是wechatId字符串,需要查询对应的好友ID
+ $friend = Db::table('s2_wechat_friend')->where('wechatId', $adminWechatId)->find();
+ if ($friend) {
+ $admins[] = intval($friend['id']);
+ }
+ }
+ }
+ // 如果传入了admins参数,优先使用(兼容旧逻辑)
+ if (!empty($param['admins']) && is_array($param['admins'])) {
+ $admins = array_merge($admins, $param['admins']);
+ }
+ $config->admins = json_encode(array_unique($admins), JSON_UNESCAPED_UNICODE);
+
+ $config->fixedWechatIds = json_encode($param['fixedWechatIds'] ?? [], JSON_UNESCAPED_UNICODE);
+ $config->createTime = time();
+ $config->updateTime = time();
+ $config->save();
+ break;
+ case self::TYPE_TRAFFIC_DISTRIBUTION: // 流量分发
+ $config = new WorkbenchTrafficConfig;
+ $config->workbenchId = $workbench->id;
+ $config->distributeType = $param['distributeType'];
+ $config->maxPerDay = $param['maxPerDay'];
+ $config->timeType = $param['timeType'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->devices = json_encode($param['deviceGroups'], JSON_UNESCAPED_UNICODE);
+ $config->pools = json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE);
+ $config->account = json_encode($param['accountGroups'], JSON_UNESCAPED_UNICODE);
+ $config->createTime = time();
+ $config->updateTime = time();
+ $config->save();
+ break;
+ case self::TYPE_IMPORT_CONTACT: //联系人导入
+ $config = new WorkbenchImportContact;
+ $config->workbenchId = $workbench->id;
+ $config->devices = json_encode($param['deviceGroups'], JSON_UNESCAPED_UNICODE);
+ $config->pools = json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE);
+ $config->num = $param['num'];
+ $config->clearContact = $param['clearContact'];
+ $config->remark = $param['remark'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->createTime = time();
+ $config->save();
+ break;
+ }
+
+ Db::commit();
+ return json(['code' => 200, 'msg' => '创建成功', 'data' => ['id' => $workbench->id]]);
+ } catch (\Exception $e) {
+ Db::rollback();
+ return json(['code' => 500, 'msg' => '创建失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 获取工作台列表
+ * @return \think\response\Json
+ */
+ public function getList()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $type = $this->request->param('type', '');
+ $keyword = $this->request->param('keyword', '');
+
+ $where = [
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['isDel', '=', 0]
+ ];
+
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+
+
+ // 添加类型筛选
+ if ($type !== '') {
+ $where[] = ['type', '=', $type];
+ }
+
+ // 添加名称模糊搜索
+ if ($keyword !== '') {
+ $where[] = ['name', 'like', '%' . $keyword . '%'];
+ }
+
+ // 定义关联关系
+ $with = [
+ 'autoLike' => function ($query) {
+ $query->field('workbenchId,interval,maxLikes,startTime,endTime,contentTypes,devices,friends');
+ },
+ 'momentsSync' => function ($query) {
+ $query->field('workbenchId,syncInterval,syncCount,syncType,startTime,endTime,accountType,devices,contentLibraries');
+ },
+ 'trafficConfig' => function ($query) {
+ $query->field('workbenchId,distributeType,maxPerDay,timeType,startTime,endTime,devices,pools,account');
+ },
+ 'groupPush' => function ($query) {
+ $query->field('workbenchId,pushType,targetType,groupPushSubType,startTime,endTime,maxPerDay,pushOrder,isLoop,status,groups,friends,ownerWechatIds,trafficPools,contentLibraries,friendIntervalMin,friendIntervalMax,messageIntervalMin,messageIntervalMax,isRandomTemplate,postPushTags,announcementContent,enableAiRewrite,aiRewritePrompt');
+ },
+ 'groupCreate' => function ($query) {
+ $query->field('workbenchId,devices,startTime,endTime,groupSizeMin,groupSizeMax,maxGroupsPerDay,groupNameTemplate,groupDescription,poolGroups,wechatGroups,admins');
+ },
+ 'importContact' => function ($query) {
+ $query->field('workbenchId,devices,pools,num,remarkType,remark,clearContact,startTime,endTime');
+ },
+ 'user' => function ($query) {
+ $query->field('id,username');
+ }
+ ];
+
+ $list = Workbench::where($where)
+ ->with($with)
+ ->field('id,companyId,name,type,status,autoStart,userId,createTime,updateTime')
+ ->order('id', 'desc')
+ ->page($page, $limit)
+ ->select()
+ ->each(function ($item) {
+ // 处理配置信息
+ switch ($item->type) {
+ case self::TYPE_AUTO_LIKE:
+ if (!empty($item->autoLike)) {
+ $item->config = $item->autoLike;
+ $item->config->devices = json_decode($item->config->devices, true);
+ $item->config->contentTypes = json_decode($item->config->contentTypes, true);
+ $item->config->friends = json_decode($item->config->friends, true);
+
+ // 添加今日点赞数
+ $startTime = strtotime(date('Y-m-d') . ' 00:00:00');
+ $endTime = strtotime(date('Y-m-d') . ' 23:59:59');
+ $todayLikeCount = Db::name('workbench_auto_like_item')
+ ->where('workbenchId', $item->id)
+ ->whereTime('createTime', 'between', [$startTime, $endTime])
+ ->count();
+
+ // 添加总点赞数
+ $totalLikeCount = Db::name('workbench_auto_like_item')
+ ->where('workbenchId', $item->id)
+ ->count();
+
+ $item->config->todayLikeCount = $todayLikeCount;
+ $item->config->totalLikeCount = $totalLikeCount;
+ }
+ unset($item->autoLike, $item->auto_like);
+ break;
+ case self::TYPE_MOMENTS_SYNC:
+ if (!empty($item->momentsSync)) {
+ $item->config = $item->momentsSync;
+ $item->config->devices = json_decode($item->config->devices, true);
+ $item->config->contentGroups = json_decode($item->config->contentLibraries, true);
+ //同步记录
+ $sendNum = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $item->id])->count();
+ $item->syncCount = $sendNum;
+ $lastTime = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $item->id])->order('id DESC')->value('createTime');
+ $item->lastSyncTime = !empty($lastTime) ? date('Y-m-d H:i', $lastTime) : '--';
+
+
+ // 获取内容库名称
+ if (!empty($item->config->contentGroups)) {
+ $libraryNames = ContentLibrary::where('id', 'in', $item->config->contentGroups)->select();
+ $item->config->contentGroupsOptions = $libraryNames;
+ } else {
+ $item->config->contentGroupsOptions = [];
+ }
+ }
+ unset($item->momentsSync, $item->moments_sync, $item->config->contentLibraries);
+ break;
+ case self::TYPE_GROUP_PUSH:
+ if (!empty($item->groupPush)) {
+ $item->config = $item->groupPush;
+ $item->config->pushType = $item->config->pushType;
+ $item->config->targetType = isset($item->config->targetType) ? intval($item->config->targetType) : 1; // 默认1=群推送
+ $item->config->groupPushSubType = isset($item->config->groupPushSubType) ? intval($item->config->groupPushSubType) : 1; // 默认1=群群发
+ $item->config->startTime = $item->config->startTime;
+ $item->config->endTime = $item->config->endTime;
+ $item->config->maxPerDay = $item->config->maxPerDay;
+ $item->config->pushOrder = $item->config->pushOrder;
+ $item->config->isLoop = $item->config->isLoop;
+ $item->config->status = $item->config->status;
+ $item->config->ownerWechatIds = json_decode($item->config->ownerWechatIds ?? '[]', true) ?: [];
+ // 根据targetType解析不同的数据
+ if ($item->config->targetType == 1) {
+ // 群推送
+ $item->config->wechatGroups = json_decode($item->config->groups, true) ?: [];
+ $item->config->wechatFriends = [];
+ // 群推送不需要devices字段
+ // 群公告相关字段
+ if ($item->config->groupPushSubType == 2) {
+ $item->config->announcementContent = isset($item->config->announcementContent) ? $item->config->announcementContent : '';
+ $item->config->enableAiRewrite = isset($item->config->enableAiRewrite) ? intval($item->config->enableAiRewrite) : 0;
+ $item->config->aiRewritePrompt = isset($item->config->aiRewritePrompt) ? $item->config->aiRewritePrompt : '';
+ }
+ $item->config->trafficPools = [];
+ } else {
+ // 好友推送
+ $item->config->wechatFriends = json_decode($item->config->friends, true) ?: [];
+ $item->config->wechatGroups = [];
+ $item->config->trafficPools = json_decode($item->config->trafficPools ?? '[]', true) ?: [];
+ }
+ $item->config->contentLibraries = json_decode($item->config->contentLibraries, true);
+ $item->config->postPushTags = json_decode($item->config->postPushTags ?? '[]', true) ?: [];
+ $item->config->lastPushTime = '';
+ if (!empty($item->config->ownerWechatIds)) {
+ $ownerWechatOptions = Db::name('wechat_account')
+ ->whereIn('id', $item->config->ownerWechatIds)
+ ->field('id,wechatId,nickName,avatar,alias')
+ ->select();
+ $item->config->ownerWechatOptions = $ownerWechatOptions;
+ } else {
+ $item->config->ownerWechatOptions = [];
+ }
+ }
+ unset($item->groupPush, $item->group_push);
+ break;
+ case self::TYPE_GROUP_CREATE:
+ if (!empty($item->groupCreate)) {
+ $item->config = $item->groupCreate;
+ $item->config->devices = json_decode($item->config->devices, true);
+ $item->config->poolGroups = json_decode($item->config->poolGroups, true);
+ $item->config->wechatGroups = json_decode($item->config->wechatGroups, true);
+ $item->config->admins = json_decode($item->config->admins ?? '[]', true) ?: [];
+
+ // 处理群管理员相关字段
+ $item->config->groupAdminEnabled = !empty($item->config->admins) ? 1 : 0;
+
+ if (!empty($item->config->admins)) {
+ $adminOptions = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->where('wf.id', 'in', $item->config->admins)
+ ->order('wf.id', 'desc')
+ ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar')
+ ->select();
+ $item->config->adminsOptions = $adminOptions;
+ // 如果有管理员,设置groupAdminWechatId为第一个管理员的ID(用于前端回显)
+ $item->config->groupAdminWechatId = !empty($item->config->admins) ? $item->config->admins[0] : null;
+ } else {
+ $item->config->adminsOptions = [];
+ $item->config->groupAdminWechatId = null;
+ }
+ }
+ unset($item->groupCreate, $item->group_create);
+ break;
+ case self::TYPE_TRAFFIC_DISTRIBUTION:
+ if (!empty($item->trafficConfig)) {
+ $item->config = $item->trafficConfig;
+ $item->config->devices = json_decode($item->config->devices, true);
+ $item->config->poolGroups = json_decode($item->config->pools, true);
+ $item->config->account = json_decode($item->config->account, true);
+ $config_item = Db::name('workbench_traffic_config_item')->where(['workbenchId' => $item->id])->order('id DESC')->find();
+ $item->config->lastUpdated = !empty($config_item) ? date('Y-m-d H:i', $config_item['createTime']) : '--';
+
+ //统计
+ $labels = $item->config->poolGroups;
+ $totalUsers = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->where([
+ ['wf.isDeleted', '=', 0],
+ ['sa.departmentId', '=', $item->companyId]
+ ])
+ ->whereIn('wa.currentDeviceId', $item->config->devices);
+
+ if (!empty($labels) && count($labels) > 0) {
+ $totalUsers = $totalUsers->where(function ($q) use ($labels) {
+ foreach ($labels as $label) {
+ $q->whereOrRaw("JSON_CONTAINS(wf.labels, '\"{$label}\"')");
+ }
+ });
+ }
+
+ $totalUsers = $totalUsers->count();
+ $totalAccounts = count($item->config->account);
+ $dailyAverage = Db::name('workbench_traffic_config_item')
+ ->where('workbenchId', $item->id)
+ ->count();
+ $day = (time() - strtotime($item->createTime)) / 86400;
+ $day = intval($day);
+ if ($dailyAverage > 0 && $totalAccounts > 0 && $day > 0) {
+ $dailyAverage = $dailyAverage / $totalAccounts / $day;
+ }
+ $item->config->total = [
+ 'dailyAverage' => intval($dailyAverage),
+ 'totalAccounts' => $totalAccounts,
+ 'deviceCount' => count($item->config->devices),
+ 'poolCount' => !empty($item->config->poolGroups) ? count($item->config->poolGroups) : 'ALL',
+ 'totalUsers' => $totalUsers >> 0
+ ];
+ }
+ unset($item->trafficConfig, $item->traffic_config);
+ break;
+
+ case self::TYPE_IMPORT_CONTACT:
+ if (!empty($item->importContact)) {
+ $item->config = $item->importContact;
+ $item->config->devices = json_decode($item->config->devices, true);
+ $item->config->poolGroups = json_decode($item->config->pools, true);
+ }
+ unset($item->importContact, $item->import_contact);
+ break;
+ }
+ // 添加创建人名称
+ $item['creatorName'] = $item->user ? $item->user->username : '';
+ unset($item['user']); // 移除关联数据
+ return $item;
+ });
+
+ $total = Workbench::where($where)->count();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]);
+ }
+
+ /**
+ * 获取工作台详情
+ * @param int $id 工作台ID
+ * @return \think\response\Json
+ */
+ public function detail()
+ {
+ $id = $this->request->param('id', '');
+
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ // 定义关联关系
+ $with = [
+ 'autoLike' => function ($query) {
+ $query->field('workbenchId,interval,maxLikes,startTime,endTime,contentTypes,devices,friends,friendMaxLikes,friendTags,enableFriendTags');
+ },
+ 'momentsSync' => function ($query) {
+ $query->field('workbenchId,syncInterval,syncCount,syncType,startTime,endTime,accountType,devices,contentLibraries');
+ },
+ 'trafficConfig' => function ($query) {
+ $query->field('workbenchId,distributeType,maxPerDay,timeType,startTime,endTime,devices,pools,account');
+ },
+ 'groupPush' => function ($query) {
+ $query->field('workbenchId,pushType,targetType,groupPushSubType,startTime,endTime,maxPerDay,pushOrder,isLoop,status,groups,friends,ownerWechatIds,trafficPools,contentLibraries,friendIntervalMin,friendIntervalMax,messageIntervalMin,messageIntervalMax,isRandomTemplate,postPushTags,announcementContent,enableAiRewrite,aiRewritePrompt');
+ },
+ 'groupCreate' => function ($query) {
+ $query->field('workbenchId,devices,startTime,endTime,groupSizeMin,groupSizeMax,maxGroupsPerDay,groupNameTemplate,groupDescription,poolGroups,wechatGroups,admins');
+ },
+ 'importContact' => function ($query) {
+ $query->field('workbenchId,devices,pools,num,remarkType,remark,clearContact,startTime,endTime');
+ },
+ ];
+
+ $where = [
+ ['id', '=', $id],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+
+ $workbench = Workbench::where($where)
+ ->field('id,name,type,status,autoStart,createTime,updateTime,companyId')
+ ->with($with)
+ ->find();
+
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在']);
+ }
+
+ // 处理配置信息
+ switch ($workbench->type) {
+ //自动点赞
+ case self::TYPE_AUTO_LIKE:
+ if (!empty($workbench->autoLike)) {
+ $workbench->config = $workbench->autoLike;
+ $workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
+ $workbench->config->wechatFriends = json_decode($workbench->config->friends, true);
+ $workbench->config->targetType = 2;
+ //$workbench->config->targetGroups = json_decode($workbench->config->targetGroups, true);
+ $workbench->config->contentTypes = json_decode($workbench->config->contentTypes, true);
+
+ // 添加今日点赞数
+ $startTime = strtotime(date('Y-m-d') . ' 00:00:00');
+ $endTime = strtotime(date('Y-m-d') . ' 23:59:59');
+ $todayLikeCount = Db::name('workbench_auto_like_item')
+ ->where('workbenchId', $workbench->id)
+ ->whereTime('createTime', 'between', [$startTime, $endTime])
+ ->count();
+
+ // 添加总点赞数
+ $totalLikeCount = Db::name('workbench_auto_like_item')
+ ->where('workbenchId', $workbench->id)
+ ->count();
+
+ $workbench->config->todayLikeCount = $todayLikeCount;
+ $workbench->config->totalLikeCount = $totalLikeCount;
+
+ unset($workbench->autoLike, $workbench->auto_like);
+ }
+ break;
+ //自动同步朋友圈
+ case self::TYPE_MOMENTS_SYNC:
+ if (!empty($workbench->momentsSync)) {
+ $workbench->config = $workbench->momentsSync;
+ $workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
+ $workbench->config->contentGroups = json_decode($workbench->config->contentLibraries, true);
+
+ //同步记录
+ $sendNum = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $workbench->id])->count();
+ $workbench->syncCount = $sendNum;
+ $lastTime = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $workbench->id])->order('id DESC')->value('createTime');
+ $workbench->lastSyncTime = !empty($lastTime) ? date('Y-m-d H:i', $lastTime) : '--';
+ unset($workbench->momentsSync, $workbench->moments_sync);
+ }
+ break;
+ //群推送
+ case self::TYPE_GROUP_PUSH:
+ if (!empty($workbench->groupPush)) {
+ $workbench->config = $workbench->groupPush;
+ $workbench->config->targetType = isset($workbench->config->targetType) ? intval($workbench->config->targetType) : 1; // 默认1=群推送
+ $workbench->config->groupPushSubType = isset($workbench->config->groupPushSubType) ? intval($workbench->config->groupPushSubType) : 1; // 默认1=群群发
+ $workbench->config->ownerWechatIds = json_decode($workbench->config->ownerWechatIds ?? '[]', true) ?: [];
+ // 根据targetType解析不同的数据
+ if ($workbench->config->targetType == 1) {
+ // 群推送
+ $workbench->config->wechatGroups = json_decode($workbench->config->groups, true) ?: [];
+ $workbench->config->wechatFriends = [];
+ $workbench->config->trafficPools = [];
+ // 群推送不需要devices字段
+ // 群公告相关字段
+ if ($workbench->config->groupPushSubType == 2) {
+ $workbench->config->announcementContent = isset($workbench->config->announcementContent) ? $workbench->config->announcementContent : '';
+ $workbench->config->enableAiRewrite = isset($workbench->config->enableAiRewrite) ? intval($workbench->config->enableAiRewrite) : 0;
+ $workbench->config->aiRewritePrompt = isset($workbench->config->aiRewritePrompt) ? $workbench->config->aiRewritePrompt : '';
+ }
+ } else {
+ // 好友推送
+ $workbench->config->wechatFriends = json_decode($workbench->config->friends, true) ?: [];
+ $workbench->config->wechatGroups = [];
+ $workbench->config->trafficPools = json_decode($workbench->config->trafficPools ?? '[]', true) ?: [];
+ }
+ $workbench->config->contentLibraries = json_decode($workbench->config->contentLibraries, true);
+ $workbench->config->postPushTags = json_decode($workbench->config->postPushTags ?? '[]', true) ?: [];
+ unset($workbench->groupPush, $workbench->group_push);
+ }
+ break;
+ //建群助手
+ case self::TYPE_GROUP_CREATE:
+ if (!empty($workbench->groupCreate)) {
+ $workbench->config = $workbench->groupCreate;
+ $workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
+ $workbench->config->poolGroups = json_decode($workbench->config->poolGroups, true);
+ $workbench->config->wechatGroups = json_decode($workbench->config->wechatGroups, true);
+ $workbench->config->admins = json_decode($workbench->config->admins ?? '[]', true) ?: [];
+
+ // 处理群管理员相关字段
+ $workbench->config->groupAdminEnabled = !empty($workbench->config->admins) ? 1 : 0;
+
+ // 如果有管理员,设置groupAdminWechatId为第一个管理员的ID(用于前端回显)
+ $workbench->config->groupAdminWechatId = !empty($workbench->config->admins) ? $workbench->config->admins[0] : null;
+
+ // 统计已建群数(状态为成功且groupId不为空的记录,按groupId分组去重)
+ $createdGroupsCount = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbench->id)
+ ->where('status', 2) // STATUS_SUCCESS = 2
+ ->where('groupId', '<>', null)
+ ->group('groupId')
+ ->count();
+
+ // 统计总人数(该工作台的所有记录数)
+ $totalMembersCount = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbench->id)
+ ->count();
+
+ // 添加统计信息
+ $workbench->config->stats = [
+ 'createdGroupsCount' => $createdGroupsCount,
+ 'totalMembersCount' => $totalMembersCount
+ ];
+
+ unset($workbench->groupCreate, $workbench->group_create);
+ }
+ break;
+ //流量分发
+ case self::TYPE_TRAFFIC_DISTRIBUTION:
+ if (!empty($workbench->trafficConfig)) {
+ $workbench->config = $workbench->trafficConfig;
+ $workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
+ $workbench->config->accountGroups = json_decode($workbench->config->account, true);
+ $workbench->config->poolGroups = json_decode($workbench->config->pools, true);
+ $config_item = Db::name('workbench_traffic_config_item')->where(['workbenchId' => $workbench->id])->order('id DESC')->find();
+ $workbench->config->lastUpdated = !empty($config_item) ? date('Y-m-d H:i', $config_item['createTime']) : '--';
+
+ //统计
+ $labels = $workbench->config->poolGroups;
+ $totalUsers = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->where([
+ ['wf.isDeleted', '=', 0],
+ ['sa.departmentId', '=', $workbench->companyId]
+ ])
+ ->whereIn('wa.currentDeviceId', $workbench->config->deviceGroups)
+ ->field('wf.id,wf.wechatAccountId,wf.wechatId,wf.labels,sa.userName,wa.currentDeviceId as deviceId')
+ ->where(function ($q) use ($labels) {
+ foreach ($labels as $label) {
+ $q->whereOrRaw("JSON_CONTAINS(wf.labels, '\"{$label}\"')");
+ }
+ })->count();
+
+ $totalAccounts = Db::table('s2_company_account')
+ ->alias('a')
+ ->where(['a.departmentId' => $workbench->companyId, 'a.status' => 0])
+ ->whereNotLike('a.userName', '%_offline%')
+ ->whereNotLike('a.userName', '%_delete%')
+ ->group('a.id')
+ ->count();
+
+ $dailyAverage = Db::name('workbench_traffic_config_item')
+ ->where('workbenchId', $workbench->id)
+ ->count();
+ $day = (time() - strtotime($workbench->createTime)) / 86400;
+ $day = intval($day);
+
+
+ if ($dailyAverage > 0) {
+ $dailyAverage = $dailyAverage / $totalAccounts / $day;
+ }
+
+ $workbench->config->total = [
+ 'dailyAverage' => intval($dailyAverage),
+ 'totalAccounts' => $totalAccounts,
+ 'deviceCount' => count($workbench->config->deviceGroups),
+ 'poolCount' => count($workbench->config->poolGroups),
+ 'totalUsers' => $totalUsers >> 0
+ ];
+ unset($workbench->trafficConfig, $workbench->traffic_config);
+ }
+ break;
+ case self::TYPE_IMPORT_CONTACT:
+ if (!empty($workbench->importContact)) {
+ $workbench->config = $workbench->importContact;
+ $workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
+ $workbench->config->poolGroups = json_decode($workbench->config->pools, true);
+ }
+ unset($workbench->importContact, $workbench->import_contact);
+ break;
+ }
+ unset(
+ $workbench->autoLike,
+ $workbench->momentsSync,
+ $workbench->groupPush,
+ $workbench->groupCreate,
+ $workbench->config->devices,
+ $workbench->config->friends,
+ $workbench->config->groups,
+ $workbench->config->contentLibraries,
+ $workbench->config->account,
+ );
+
+
+ //获取设备信息
+ if (!empty($workbench->config->deviceGroups)) {
+ $deviceList = DeviceModel::alias('d')
+ ->field([
+ 'd.id', 'd.imei', 'd.memo', 'd.alive',
+ 'l.wechatId',
+ 'a.nickname', 'a.alias', 'a.avatar', 'a.alias', '0 totalFriend'
+ ])
+ ->leftJoin('device_wechat_login l', 'd.id = l.deviceId and l.alive =' . DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE . ' and l.companyId = d.companyId')
+ ->leftJoin('wechat_account a', 'l.wechatId = a.wechatId')
+ ->whereIn('d.id', $workbench->config->deviceGroups)
+ ->order('d.id desc')
+ ->select();
+
+ foreach ($deviceList as &$device) {
+ $curstomer = WechatCustomerModel::field('friendShip')->where(['wechatId' => $device['wechatId']])->find();
+ $device['totalFriend'] = $curstomer->friendShip->totalFriend ?? 0;
+ }
+ unset($device);
+
+ $workbench->config->deviceGroupsOptions = $deviceList;
+ } else {
+ $workbench->config->deviceGroupsOptions = [];
+ }
+
+
+ // 获取群(当targetType=1时)
+ if (!empty($workbench->config->wechatGroups) && isset($workbench->config->targetType) && $workbench->config->targetType == 1) {
+ $groupList = Db::name('wechat_group')->alias('wg')
+ ->join('wechat_account wa', 'wa.wechatId = wg.ownerWechatId')
+ ->where('wg.id', 'in', $workbench->config->wechatGroups)
+ ->order('wg.id', 'desc')
+ ->field('wg.id,wg.name as groupName,wg.ownerWechatId,wa.nickName,wa.avatar,wa.alias,wg.avatar as groupAvatar')
+ ->select();
+ $workbench->config->wechatGroupsOptions = $groupList;
+ } else {
+ $workbench->config->wechatGroupsOptions = [];
+ }
+
+ // 获取好友(当targetType=2时)
+ if (!empty($workbench->config->wechatFriends) && isset($workbench->config->targetType) && $workbench->config->targetType == 2) {
+ $friendList = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->where('wf.id', 'in', $workbench->config->wechatFriends)
+ ->order('wf.id', 'desc')
+ ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar')
+ ->select();
+ $workbench->config->wechatFriendsOptions = $friendList;
+ } else {
+ $workbench->config->wechatFriendsOptions = [];
+ }
+
+ // 获取流量池(当targetType=2时)
+ if (!empty($workbench->config->trafficPools) && isset($workbench->config->targetType) && $workbench->config->targetType == 2) {
+ $poolList = Db::name('traffic_source_package')->alias('tsp')
+ ->leftJoin('traffic_source_package_item tspi', 'tspi.packageId = tsp.id and tspi.isDel = 0')
+ ->whereIn('tsp.id', $workbench->config->trafficPools)
+ ->where('tsp.isDel', 0)
+ ->whereIn('tsp.companyId', [$this->request->userInfo['companyId'], 0])
+ ->field('tsp.id,tsp.name,tsp.description,tsp.pic,COUNT(tspi.id) as itemCount')
+ ->group('tsp.id')
+ ->order('tsp.id', 'desc')
+ ->select();
+ $workbench->config->trafficPoolsOptions = $poolList;
+ } else {
+ $workbench->config->trafficPoolsOptions = [];
+ }
+
+ // 获取内容库名称
+ if (!empty($workbench->config->contentGroups)) {
+ $libraryNames = ContentLibrary::where('id', 'in', $workbench->config->contentGroups)->select();
+ $workbench->config->contentGroupsOptions = $libraryNames;
+ } else {
+ $workbench->config->contentGroupsOptions = [];
+ }
+
+ //账号
+ if (!empty($workbench->config->accountGroups)) {
+ $account = Db::table('s2_company_account')->alias('a')
+ ->where(['a.departmentId' => $this->request->userInfo['companyId'], 'a.status' => 0])
+ ->whereIn('a.id', $workbench->config->accountGroups)
+ ->whereNotLike('a.userName', '%_offline%')
+ ->whereNotLike('a.userName', '%_delete%')
+ ->field('a.id,a.userName,a.realName,a.nickname,a.memo')
+ ->select();
+ $workbench->config->accountGroupsOptions = $account;
+ } else {
+ $workbench->config->accountGroupsOptions = [];
+ }
+
+ if (!empty($workbench->config->poolGroups)) {
+ $poolGroupsOptions = Db::name('traffic_source_package')->alias('tsp')
+ ->join('traffic_source_package_item tspi', 'tspi.packageId=tsp.id', 'left')
+ ->whereIn('tsp.companyId', [$this->request->userInfo['companyId'], 0])
+ ->whereIn('tsp.id', $workbench->config->poolGroups)
+ ->field('tsp.id,tsp.name,tsp.description,tsp.createTime,count(tspi.id) as num')
+ ->group('tsp.id')
+ ->select();
+ $workbench->config->poolGroupsOptions = $poolGroupsOptions;
+ } else {
+ $workbench->config->poolGroupsOptions = [];
+ }
+
+ if (!empty($workbench->config->ownerWechatIds)) {
+ $ownerWechatOptions = Db::name('wechat_account')
+ ->whereIn('id', $workbench->config->ownerWechatIds)
+ ->field('id,wechatId,nickName,avatar,alias')
+ ->select();
+ $workbench->config->ownerWechatOptions = $ownerWechatOptions;
+ } else {
+ $workbench->config->ownerWechatOptions = [];
+ }
+
+ // 获取群组选项(自动建群)
+ if ($workbench->type == self::TYPE_GROUP_CREATE && !empty($workbench->config->wechatGroups)) {
+ // 分离数字ID(好友ID)和字符串ID(手动创建的群组)
+ $friendIds = [];
+ $manualGroupIds = [];
+
+ foreach ($workbench->config->wechatGroups as $groupId) {
+ if (is_numeric($groupId)) {
+ $friendIds[] = intval($groupId);
+ } else {
+ $manualGroupIds[] = $groupId;
+ }
+ }
+
+ $wechatGroupsOptions = [];
+
+ // 查询好友信息(数字ID)
+ if (!empty($friendIds)) {
+ $friendList = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->join(['s2_company_account' => 'ca'], 'ca.id = wf.accountId', 'left')
+ ->where('wf.id', 'in', $friendIds)
+ ->order('wf.id', 'desc')
+ ->field('wf.id,wf.wechatId,wf.nickname,wf.avatar,wf.alias,wf.gender,wf.phone,wa.nickName as accountNickname,ca.userName as account,ca.realName as username,wf.createTime,wf.updateTime,wf.deleteTime,wf.ownerWechatId')
+ ->select();
+
+ // 获取群主信息
+ foreach ($friendList as &$friend) {
+ if (!empty($friend['ownerWechatId'])) {
+ $owner = Db::name('wechat_account')
+ ->where('wechatId', $friend['ownerWechatId'])
+ ->field('nickName,alias')
+ ->find();
+ $friend['ownerNickname'] = $owner['nickName'] ?? '';
+ $friend['ownerAlias'] = $owner['alias'] ?? '';
+ } else {
+ $friend['ownerNickname'] = '';
+ $friend['ownerAlias'] = '';
+ }
+ $friend['isManual'] = '';
+ // 格式化时间
+ $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['deleteTime'] = !empty($friend['deleteTime']) ? date('Y-m-d H:i:s', $friend['deleteTime']) : '';
+ }
+ unset($friend);
+
+ $wechatGroupsOptions = array_merge($wechatGroupsOptions, $friendList);
+ }
+
+ // 处理手动创建的群组(字符串ID)
+ if (!empty($manualGroupIds)) {
+ foreach ($manualGroupIds as $groupId) {
+ // 手动创建的群组,只返回基本信息
+ $wechatGroupsOptions[] = [
+ 'id' => $groupId,
+ 'wechatId' => $groupId,
+ 'nickname' => $groupId,
+ 'avatar' => '',
+ 'isManual' => 1
+ ];
+ }
+ }
+
+ $workbench->config->wechatGroupsOptions = $wechatGroupsOptions;
+ } else {
+ $workbench->config->wechatGroupsOptions = [];
+ }
+
+ // 获取管理员选项(自动建群)
+ if ($workbench->type == self::TYPE_GROUP_CREATE && !empty($workbench->config->admins)) {
+ $adminOptions = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->where('wf.id', 'in', $workbench->config->admins)
+ ->order('wf.id', 'desc')
+ ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar')
+ ->select();
+ $workbench->config->adminsOptions = $adminOptions;
+ } else {
+ $workbench->config->adminsOptions = [];
+ }
+
+ return json(['code' => 200, 'msg' => '获取成功', 'data' => $workbench]);
+ }
+
+ /**
+ * 更新工作台
+ * @return \think\response\Json
+ */
+ public function update()
+ {
+ if (!$this->request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ // 获取请求参数
+ $param = $this->request->post();
+
+ // 验证数据
+ $validate = new WorkbenchValidate;
+ if (!$validate->scene('update')->check($param)) {
+ return json(['code' => 400, 'msg' => $validate->getError()]);
+ }
+
+
+ $where = [
+ ['id', '=', $param['id']],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+ // 查询工作台是否存在
+ $workbench = Workbench::where($where)->find();
+ if (!$workbench) {
+ return json(['code' => 404, 'msg' => '工作台不存在']);
+ }
+
+ Db::startTrans();
+ try {
+ // 更新工作台基本信息
+ $workbench->name = $param['name'];
+ $workbench->status = !empty($param['status']) ? 1 : 0;
+ $workbench->autoStart = !empty($param['autoStart']) ? 1 : 0;
+ $workbench->updateTime = time();
+ $workbench->save();
+
+ // 根据类型更新对应的配置
+ switch ($workbench->type) {
+ case self::TYPE_AUTO_LIKE:
+ $config = WorkbenchAutoLike::where('workbenchId', $param['id'])->find();
+ if ($config) {
+ $config->interval = $param['interval'];
+ $config->maxLikes = $param['maxLikes'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->contentTypes = json_encode($param['contentTypes']);
+ $config->devices = json_encode($param['deviceGroups']);
+ $config->friends = json_encode($param['wechatFriends']);
+ // $config->targetGroups = json_encode($param['targetGroups']);
+ // $config->tagOperator = $param['tagOperator'];
+ $config->friendMaxLikes = $param['friendMaxLikes'];
+ $config->friendTags = $param['friendTags'];
+ $config->enableFriendTags = $param['enableFriendTags'];
+ $config->updateTime = time();
+ $config->save();
+ }
+ break;
+
+ case self::TYPE_MOMENTS_SYNC:
+ $config = WorkbenchMomentsSync::where('workbenchId', $param['id'])->find();
+ if ($config) {
+ if (!empty($param['contentGroups'])) {
+ foreach ($param['contentGroups'] as $library) {
+ if (isset($library['id']) && !empty($library['id'])) {
+ $contentLibraries[] = $library['id'];
+ } else {
+ $contentLibraries[] = $library;
+ }
+ }
+ } else {
+ $contentLibraries = [];
+ }
+
+ $config->syncInterval = $param['syncInterval'];
+ $config->syncCount = $param['syncCount'];
+ $config->syncType = $param['syncType'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->accountType = $param['accountType'];
+ $config->devices = json_encode($param['deviceGroups']);
+ $config->contentLibraries = json_encode($contentLibraries);
+ $config->updateTime = time();
+ $config->save();
+ }
+ break;
+
+ case self::TYPE_GROUP_PUSH:
+ $config = WorkbenchGroupPush::where('workbenchId', $param['id'])->find();
+ if ($config) {
+ $ownerWechatIds = $this->normalizeOwnerWechatIds($param['ownerWechatIds'] ?? null, $config);
+ $groupPushData = $this->prepareGroupPushData($param, $ownerWechatIds, $config);
+ $groupPushData['updateTime'] = time();
+ $config->save($groupPushData);
+ }
+ break;
+
+ case self::TYPE_GROUP_CREATE:
+ $config = WorkbenchGroupCreate::where('workbenchId', $param['id'])->find();
+ if ($config) {
+ $config->devices = json_encode($param['deviceGroups'] ?? [], JSON_UNESCAPED_UNICODE);
+ $config->startTime = $param['startTime'] ?? '';
+ $config->endTime = $param['endTime'] ?? '';
+ $config->groupSizeMin = intval($param['groupSizeMin'] ?? 3);
+ $config->groupSizeMax = intval($param['groupSizeMax'] ?? 38);
+ $config->maxGroupsPerDay = intval($param['maxGroupsPerDay'] ?? 20);
+ $config->groupNameTemplate = $param['groupNameTemplate'] ?? '';
+ $config->groupDescription = $param['groupDescription'] ?? '';
+ $config->poolGroups = json_encode($param['poolGroups'] ?? [], JSON_UNESCAPED_UNICODE);
+ $config->wechatGroups = json_encode($param['wechatGroups'] ?? [], JSON_UNESCAPED_UNICODE);
+
+ // 处理群管理员:如果启用了群管理员且有指定管理员,则保存到admins字段
+ $admins = [];
+ if (!empty($param['groupAdminEnabled']) && !empty($param['groupAdminWechatId'])) {
+ // 如果groupAdminWechatId是数组,取第一个;如果是单个值,直接使用
+ $adminWechatId = is_array($param['groupAdminWechatId']) ? $param['groupAdminWechatId'][0] : $param['groupAdminWechatId'];
+ // 如果是好友ID,直接添加到admins;如果是wechatId,需要转换为好友ID
+ if (is_numeric($adminWechatId)) {
+ $admins[] = intval($adminWechatId);
+ } else {
+ // 如果是wechatId字符串,需要查询对应的好友ID
+ $friend = Db::table('s2_wechat_friend')->where('wechatId', $adminWechatId)->find();
+ if ($friend) {
+ $admins[] = intval($friend['id']);
+ }
+ }
+ }
+ // 如果传入了admins参数,优先使用(兼容旧逻辑)
+ if (!empty($param['admins']) && is_array($param['admins'])) {
+ $admins = array_merge($admins, $param['admins']);
+ }
+ $config->admins = json_encode(array_unique($admins), JSON_UNESCAPED_UNICODE);
+
+ $config->updateTime = time();
+ $config->save();
+ }
+ break;
+ case self::TYPE_TRAFFIC_DISTRIBUTION:
+ $config = WorkbenchTrafficConfig::where('workbenchId', $param['id'])->find();
+ if ($config) {
+ $config->distributeType = $param['distributeType'];
+ $config->maxPerDay = $param['maxPerDay'];
+ $config->timeType = $param['timeType'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->devices = json_encode($param['deviceGroups']);
+ $config->pools = json_encode($param['poolGroups']);
+ $config->account = json_encode($param['accountGroups']);
+ $config->updateTime = time();
+ $config->save();
+ }
+ break;
+ case self::TYPE_IMPORT_CONTACT: //联系人导入
+ $config = WorkbenchImportContact::where('workbenchId', $param['id'])->find();;
+ if ($config) {
+ $config->devices = json_encode($param['deviceGroups']);
+ $config->pools = json_encode($param['poolGroups']);
+ $config->num = $param['num'];
+ $config->clearContact = $param['clearContact'];
+ $config->remark = $param['remark'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->save();
+ }
+ break;
+ }
+
+ Db::commit();
+ return json(['code' => 200, 'msg' => '更新成功']);
+ } catch (\Exception $e) {
+ Db::rollback();
+ return json(['code' => 500, 'msg' => '更新失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 更新工作台状态
+ * @return \think\response\Json
+ */
+ public function updateStatus()
+ {
+ if (!$this->request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ $id = $this->request->param('id', '');
+
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+
+
+ $where = [
+ ['id', '=', $id],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+ $workbench = Workbench::where($where)->find();
+
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在']);
+ }
+
+ $workbench->status = !$workbench['status'];
+ $workbench->save();
+
+ return json(['code' => 200, 'msg' => '更新成功']);
+ }
+
+ /**
+ * 删除工作台(软删除)
+ */
+ public function delete()
+ {
+ $id = $this->request->param('id');
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ $where = [
+ ['id', '=', $id],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+ $workbench = Workbench::where($where)->find();
+
+ if (!$workbench) {
+ return json(['code' => 404, 'msg' => '工作台不存在']);
+ }
+
+ // 软删除
+ $workbench->isDel = 1;
+ $workbench->deleteTime = time();
+ $workbench->save();
+
+ return json(['code' => 200, 'msg' => '删除成功']);
+ }
+
+ /**
+ * 拷贝工作台
+ * @return \think\response\Json
+ */
+ public function copy()
+ {
+ if (!$this->request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ $id = $this->request->post('id');
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ // 验证权限并获取原数据
+ $workbench = Workbench::where([
+ ['id', '=', $id],
+ ['userId', '=', $this->request->userInfo['id']]
+ ])->find();
+
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在']);
+ }
+
+ Db::startTrans();
+ try {
+ // 创建新的工作台基本信息
+ $newWorkbench = new Workbench;
+ $newWorkbench->name = $workbench->name . ' copy';
+ $newWorkbench->type = $workbench->type;
+ $newWorkbench->status = 1; // 新拷贝的默认启用
+ $newWorkbench->autoStart = $workbench->autoStart;
+ $newWorkbench->userId = $this->request->userInfo['id'];
+ $newWorkbench->companyId = $this->request->userInfo['companyId'];
+ $newWorkbench->save();
+
+ // 根据类型拷贝对应的配置
+ switch ($workbench->type) {
+ case self::TYPE_AUTO_LIKE:
+ $config = WorkbenchAutoLike::where('workbenchId', $id)->find();
+ if ($config) {
+ $newConfig = new WorkbenchAutoLike;
+ $newConfig->workbenchId = $newWorkbench->id;
+ $newConfig->interval = $config->interval;
+ $newConfig->maxLikes = $config->maxLikes;
+ $newConfig->startTime = $config->startTime;
+ $newConfig->endTime = $config->endTime;
+ $newConfig->contentTypes = $config->contentTypes;
+ $newConfig->devices = $config->devices;
+ $newConfig->friends = $config->friends;
+ $newConfig->createTime = time();
+ $newConfig->updateTime = time();
+ $newConfig->save();
+ }
+ break;
+ case self::TYPE_MOMENTS_SYNC:
+ $config = WorkbenchMomentsSync::where('workbenchId', $id)->find();
+ if ($config) {
+ $newConfig = new WorkbenchMomentsSync;
+ $newConfig->workbenchId = $newWorkbench->id;
+ $newConfig->syncInterval = $config->syncInterval;
+ $newConfig->syncCount = $config->syncCount;
+ $newConfig->syncType = $config->syncType;
+ $newConfig->startTime = $config->startTime;
+ $newConfig->endTime = $config->endTime;
+ $newConfig->accountType = $config->accountType;
+ $newConfig->devices = $config->devices;
+ $newConfig->contentLibraries = $config->contentLibraries;
+ $newConfig->createTime = time();
+ $newConfig->updateTime = time();
+ $newConfig->save();
+ }
+ break;
+ case self::TYPE_GROUP_PUSH:
+ $config = WorkbenchGroupPush::where('workbenchId', $id)->find();
+ if ($config) {
+ $newConfig = new WorkbenchGroupPush;
+ $newConfig->workbenchId = $newWorkbench->id;
+ $newConfig->pushType = $config->pushType;
+ $newConfig->targetType = isset($config->targetType) ? $config->targetType : 1; // 默认1=群推送
+ $newConfig->startTime = $config->startTime;
+ $newConfig->endTime = $config->endTime;
+ $newConfig->maxPerDay = $config->maxPerDay;
+ $newConfig->pushOrder = $config->pushOrder;
+ $newConfig->isLoop = $config->isLoop;
+ $newConfig->status = $config->status;
+ $newConfig->groups = $config->groups;
+ $newConfig->friends = $config->friends;
+ $newConfig->contentLibraries = $config->contentLibraries;
+ $newConfig->trafficPools = property_exists($config, 'trafficPools') ? $config->trafficPools : json_encode([], JSON_UNESCAPED_UNICODE);
+ $newConfig->socialMediaId = $config->socialMediaId;
+ $newConfig->promotionSiteId = $config->promotionSiteId;
+ $newConfig->ownerWechatIds = $config->ownerWechatIds;
+ $newConfig->createTime = time();
+ $newConfig->updateTime = time();
+ $newConfig->save();
+ }
+ break;
+ case self::TYPE_GROUP_CREATE:
+ $config = WorkbenchGroupCreate::where('workbenchId', $id)->find();
+ if ($config) {
+ $newConfig = new WorkbenchGroupCreate;
+ $newConfig->workbenchId = $newWorkbench->id;
+ $newConfig->devices = $config->devices;
+ $newConfig->startTime = $config->startTime;
+ $newConfig->endTime = $config->endTime;
+ $newConfig->groupSizeMin = $config->groupSizeMin;
+ $newConfig->groupSizeMax = $config->groupSizeMax;
+ $newConfig->maxGroupsPerDay = $config->maxGroupsPerDay;
+ $newConfig->groupNameTemplate = $config->groupNameTemplate;
+ $newConfig->groupDescription = $config->groupDescription;
+ $newConfig->poolGroups = $config->poolGroups;
+ $newConfig->wechatGroups = $config->wechatGroups;
+ $newConfig->admins = $config->admins ?? json_encode([], JSON_UNESCAPED_UNICODE);
+ $newConfig->createTime = time();
+ $newConfig->updateTime = time();
+ $newConfig->save();
+ }
+ break;
+ case self::TYPE_IMPORT_CONTACT: //联系人导入
+ $config = WorkbenchImportContact::where('workbenchId', $id)->find();
+ if ($config) {
+ $newConfig = new WorkbenchImportContact;
+ $newConfig->workbenchId = $newWorkbench->id;
+ $newConfig->devices = $config->devices;
+ $newConfig->pools = $config->pools;
+ $newConfig->num = $config->num;
+ $newConfig->clearContact = $config->clearContact;
+ $newConfig->remark = $config->remark;
+ $newConfig->startTime = $config->startTime;
+ $newConfig->endTime = $config->endTime;
+ $newConfig->createTime = time();
+ $newConfig->save();
+ }
+ break;
+ }
+
+ Db::commit();
+ return json(['code' => 200, 'msg' => '拷贝成功', 'data' => ['id' => $newWorkbench->id]]);
+ } catch (\Exception $e) {
+ Db::rollback();
+ return json(['code' => 500, 'msg' => '拷贝失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 获取点赞记录列表
+ * @return \think\response\Json
+ */
+ public function getLikeRecords()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $workbenchId = $this->request->param('workbenchId', 0);
+
+ $where = [
+ ['wali.workbenchId', '=', $workbenchId]
+ ];
+
+ // 查询点赞记录
+ $list = Db::name('workbench_auto_like_item')->alias('wali')
+ ->join(['s2_wechat_moments' => 'wm'], 'wali.snsId = wm.snsId')
+ ->field([
+ 'wali.id',
+ 'wali.workbenchId',
+ 'wali.momentsId',
+ 'wali.snsId',
+ 'wali.wechatAccountId',
+ 'wali.wechatFriendId',
+ 'wali.createTime as likeTime',
+ 'wm.content',
+ 'wm.resUrls',
+ 'wm.createTime as momentTime',
+ 'wm.userName',
+ ])
+ ->where($where)
+ ->order('wali.createTime', 'desc')
+ ->group('wali.id')
+ ->page($page, $limit)
+ ->select();
+
+ // 处理数据
+ foreach ($list as &$item) {
+ //处理用户信息
+ $friend = Db::table('s2_wechat_friend')
+ ->where(['id' => $item['wechatFriendId']])
+ ->field('nickName,avatar')
+ ->find();
+ if (!empty($friend)) {
+ $item['friendName'] = $friend['nickName'];
+ $item['friendAvatar'] = $friend['avatar'];
+ } else {
+ $item['friendName'] = '';
+ $item['friendAvatar'] = '';
+ }
+
+ //处理客服
+ $friend = Db::table('s2_wechat_account')
+ ->where(['id' => $item['wechatAccountId']])
+ ->field('nickName,avatar')
+ ->find();
+ if (!empty($friend)) {
+ $item['operatorName'] = $friend['nickName'];
+ $item['operatorAvatar'] = $friend['avatar'];
+ } else {
+ $item['operatorName'] = '';
+ $item['operatorAvatar'] = '';
+ }
+
+ // 处理时间格式
+ $item['likeTime'] = date('Y-m-d H:i:s', $item['likeTime']);
+ $item['momentTime'] = !empty($item['momentTime']) ? date('Y-m-d H:i:s', $item['momentTime']) : '';
+
+ // 处理资源链接
+ if (!empty($item['resUrls'])) {
+ $item['resUrls'] = json_decode($item['resUrls'], true);
+ } else {
+ $item['resUrls'] = [];
+ }
+ }
+
+ // 获取总记录数
+ $total = Db::name('workbench_auto_like_item')->alias('wali')
+ ->where($where)
+ ->count();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]);
+ }
+
+ /**
+ * 获取朋友圈发布记录列表
+ * @return \think\response\Json
+ */
+ public function getMomentsRecords()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $workbenchId = $this->request->param('workbenchId', 0);
+
+ $where = [
+ ['wmsi.workbenchId', '=', $workbenchId]
+ ];
+
+ // 查询发布记录
+ $list = Db::name('workbench_moments_sync_item')->alias('wmsi')
+ ->join('content_item ci', 'ci.id = wmsi.contentId', 'left')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wmsi.wechatAccountId', 'left')
+ ->field([
+ 'wmsi.id',
+ 'wmsi.workbenchId',
+ 'wmsi.createTime as publishTime',
+ 'ci.contentType',
+ 'ci.content',
+ 'ci.resUrls',
+ 'ci.urls',
+ 'wa.nickName as operatorName',
+ 'wa.avatar as operatorAvatar'
+ ])
+ ->where($where)
+ ->order('wmsi.createTime', 'desc')
+ ->page($page, $limit)
+ ->select();
+
+ foreach ($list as &$item) {
+ $item['resUrls'] = json_decode($item['resUrls'], true);
+ $item['urls'] = json_decode($item['urls'], true);
+ }
+
+ // 获取总记录数
+ $total = Db::name('workbench_moments_sync_item')->alias('wmsi')
+ ->where($where)
+ ->count();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]);
+ }
+
+ /**
+ * 获取朋友圈发布统计
+ * @return \think\response\Json
+ */
+ public function getMomentsStats()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ // 获取今日数据
+ $todayStart = strtotime(date('Y-m-d') . ' 00:00:00');
+ $todayEnd = strtotime(date('Y-m-d') . ' 23:59:59');
+
+ $todayStats = Db::name('workbench_moments_sync_item')
+ ->where([
+ ['workbenchId', '=', $workbenchId],
+ ['createTime', 'between', [$todayStart, $todayEnd]]
+ ])
+ ->field([
+ 'COUNT(*) as total',
+ 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success',
+ 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed'
+ ])
+ ->find();
+
+ // 获取总数据
+ $totalStats = Db::name('workbench_moments_sync_item')
+ ->where('workbenchId', $workbenchId)
+ ->field([
+ 'COUNT(*) as total',
+ 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success',
+ 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed'
+ ])
+ ->find();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'today' => [
+ 'total' => intval($todayStats['total']),
+ 'success' => intval($todayStats['success']),
+ 'failed' => intval($todayStats['failed'])
+ ],
+ 'total' => [
+ 'total' => intval($totalStats['total']),
+ 'success' => intval($totalStats['success']),
+ 'failed' => intval($totalStats['failed'])
+ ]
+ ]
+ ]);
+ }
+
+ /**
+ * 获取流量分发记录列表
+ * @return \think\response\Json
+ */
+ public function getTrafficDistributionRecords()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $workbenchId = $this->request->param('workbenchId', 0);
+
+ $where = [
+ ['wtdi.workbenchId', '=', $workbenchId]
+ ];
+
+ // 查询分发记录
+ $list = Db::name('workbench_traffic_distribution_item')->alias('wtdi')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wtdi.wechatAccountId', 'left')
+ ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wtdi.wechatFriendId', 'left')
+ ->field([
+ 'wtdi.id',
+ 'wtdi.workbenchId',
+ 'wtdi.wechatAccountId',
+ 'wtdi.wechatFriendId',
+ 'wtdi.createTime as distributeTime',
+ 'wtdi.status',
+ 'wtdi.errorMsg',
+ 'wa.nickName as operatorName',
+ 'wa.avatar as operatorAvatar',
+ 'wf.nickName as friendName',
+ 'wf.avatar as friendAvatar',
+ 'wf.gender',
+ 'wf.province',
+ 'wf.city'
+ ])
+ ->where($where)
+ ->order('wtdi.createTime', 'desc')
+ ->page($page, $limit)
+ ->select();
+
+ // 处理数据
+ foreach ($list as &$item) {
+ // 处理时间格式
+ $item['distributeTime'] = date('Y-m-d H:i:s', $item['distributeTime']);
+
+ // 处理性别
+ $genderMap = [
+ 0 => '未知',
+ 1 => '男',
+ 2 => '女'
+ ];
+ $item['genderText'] = $genderMap[$item['gender']] ?? '未知';
+
+ // 处理状态文字
+ $statusMap = [
+ 0 => '待分发',
+ 1 => '分发成功',
+ 2 => '分发失败'
+ ];
+ $item['statusText'] = $statusMap[$item['status']] ?? '未知状态';
+ }
+
+ // 获取总记录数
+ $total = Db::name('workbench_traffic_distribution_item')->alias('wtdi')
+ ->where($where)
+ ->count();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]);
+ }
+
+ /**
+ * 获取流量分发统计
+ * @return \think\response\Json
+ */
+ public function getTrafficDistributionStats()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ // 获取今日数据
+ $todayStart = strtotime(date('Y-m-d') . ' 00:00:00');
+ $todayEnd = strtotime(date('Y-m-d') . ' 23:59:59');
+
+ $todayStats = Db::name('workbench_traffic_distribution_item')
+ ->where([
+ ['workbenchId', '=', $workbenchId],
+ ['createTime', 'between', [$todayStart, $todayEnd]]
+ ])
+ ->field([
+ 'COUNT(*) as total',
+ 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success',
+ 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed'
+ ])
+ ->find();
+
+ // 获取总数据
+ $totalStats = Db::name('workbench_traffic_distribution_item')
+ ->where('workbenchId', $workbenchId)
+ ->field([
+ 'COUNT(*) as total',
+ 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success',
+ 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed'
+ ])
+ ->find();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'today' => [
+ 'total' => intval($todayStats['total']),
+ 'success' => intval($todayStats['success']),
+ 'failed' => intval($todayStats['failed'])
+ ],
+ 'total' => [
+ 'total' => intval($totalStats['total']),
+ 'success' => intval($totalStats['success']),
+ 'failed' => intval($totalStats['failed'])
+ ]
+ ]
+ ]);
+ }
+
+ /**
+ * 获取流量分发详情
+ * @return \think\response\Json
+ */
+ public function getTrafficDistributionDetail()
+ {
+ $id = $this->request->param('id', 0);
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ $detail = Db::name('workbench_traffic_distribution_item')->alias('wtdi')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wtdi.wechatAccountId', 'left')
+ ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wtdi.wechatFriendId', 'left')
+ ->field([
+ 'wtdi.id',
+ 'wtdi.workbenchId',
+ 'wtdi.wechatAccountId',
+ 'wtdi.wechatFriendId',
+ 'wtdi.createTime as distributeTime',
+ 'wtdi.status',
+ 'wtdi.errorMsg',
+ 'wa.nickName as operatorName',
+ 'wa.avatar as operatorAvatar',
+ 'wf.nickName as friendName',
+ 'wf.avatar as friendAvatar',
+ 'wf.gender',
+ 'wf.province',
+ 'wf.city',
+ 'wf.signature',
+ 'wf.remark'
+ ])
+ ->where('wtdi.id', $id)
+ ->find();
+
+ if (empty($detail)) {
+ return json(['code' => 404, 'msg' => '记录不存在']);
+ }
+
+ // 处理数据
+ $detail['distributeTime'] = date('Y-m-d H:i:s', $detail['distributeTime']);
+
+ // 处理性别
+ $genderMap = [
+ 0 => '未知',
+ 1 => '男',
+ 2 => '女'
+ ];
+ $detail['genderText'] = $genderMap[$detail['gender']] ?? '未知';
+
+ // 处理状态文字
+ $statusMap = [
+ 0 => '待分发',
+ 1 => '分发成功',
+ 2 => '分发失败'
+ ];
+ $detail['statusText'] = $statusMap[$detail['status']] ?? '未知状态';
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => $detail
+ ]);
+ }
+
+ /**
+ * 创建流量分发计划
+ * @return \think\response\Json
+ */
+ public function createTrafficPlan()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchTrafficController();
+ $controller->request = $this->request;
+ return $controller->createTrafficPlan();
+ }
+
+ /**
+ * 获取流量列表
+ * @return \think\response\Json
+ */
+ public function getTrafficList()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchTrafficController();
+ $controller->request = $this->request;
+ return $controller->getTrafficList();
+ }
+
+ /**
+ * 获取所有微信好友标签及数量统计
+ * @return \think\response\Json
+ */
+ public function getDeviceLabels()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController();
+ $controller->request = $this->request;
+ return $controller->getDeviceLabels();
+ }
+
+ /**
+ * 获取群列表
+ * @return \think\response\Json
+ */
+ public function getGroupList()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController();
+ $controller->request = $this->request;
+ return $controller->getGroupList();
+ }
+
+ /**
+ * 获取流量池列表
+ * @return \think\response\Json
+ */
+ public function getTrafficPoolList()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController();
+ $controller->request = $this->request;
+ return $controller->getTrafficPoolList();
+ }
+
+ /**
+ * 获取账号列表
+ * @return \think\response\Json
+ */
+ public function getAccountList()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController();
+ $controller->request = $this->request;
+ return $controller->getAccountList();
+ }
+
+ /**
+ * 获取京东联盟导购媒体
+ * @return \think\response\Json
+ */
+ public function getJdSocialMedia()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController();
+ $controller->request = $this->request;
+ return $controller->getJdSocialMedia();
+ }
+
+ /**
+ * 获取京东联盟广告位
+ * @return \think\response\Json
+ */
+ public function getJdPromotionSite()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController();
+ $controller->request = $this->request;
+ return $controller->getJdPromotionSite();
+ }
+
+ /**
+ * 京东转链-京推推
+ * @param string $content
+ * @param string $positionid
+ * @return string
+ */
+ public function changeLink($content = '', $positionid = '')
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController();
+ $controller->request = $this->request;
+ return $controller->changeLink($content, $positionid);
+ }
+
+ /**
+ * 规范化客服微信ID列表
+ * @param mixed $ownerWechatIds
+ * @param WorkbenchGroupPush|null $originalConfig
+ * @return array
+ * @throws \Exception
+ */
+ private function normalizeOwnerWechatIds($ownerWechatIds, WorkbenchGroupPush $originalConfig = null): array
+ {
+ if ($ownerWechatIds === null) {
+ $existing = $originalConfig ? $this->decodeJsonArray($originalConfig->ownerWechatIds ?? []) : [];
+ if (empty($existing)) {
+ throw new \Exception('请至少选择一个客服微信');
+ }
+ return $existing;
+ }
+
+ if (!is_array($ownerWechatIds)) {
+ throw new \Exception('客服参数格式错误');
+ }
+
+ $normalized = $this->extractIdList($ownerWechatIds, '客服参数格式错误');
+ if (empty($normalized)) {
+ throw new \Exception('请至少选择一个客服微信');
+ }
+ return $normalized;
+ }
+
+ /**
+ * 构建群推送配置数据
+ * @param array $param
+ * @param array $ownerWechatIds
+ * @param WorkbenchGroupPush|null $originalConfig
+ * @return array
+ * @throws \Exception
+ */
+ private function prepareGroupPushData(array $param, array $ownerWechatIds, WorkbenchGroupPush $originalConfig = null): array
+ {
+ $targetTypeDefault = $originalConfig ? intval($originalConfig->targetType) : 1;
+ $targetType = intval($this->getParamValue($param, 'targetType', $targetTypeDefault)) ?: 1;
+
+ $groupPushSubTypeDefault = $originalConfig ? intval($originalConfig->groupPushSubType) : 1;
+ $groupPushSubType = intval($this->getParamValue($param, 'groupPushSubType', $groupPushSubTypeDefault)) ?: 1;
+ if (!in_array($groupPushSubType, [1, 2], true)) {
+ $groupPushSubType = 1;
+ }
+
+ $data = [
+ 'pushType' => $this->toBoolInt($this->getParamValue($param, 'pushType', $originalConfig->pushType ?? 0)),
+ 'targetType' => $targetType,
+ 'startTime' => $this->getParamValue($param, 'startTime', $originalConfig->startTime ?? ''),
+ 'endTime' => $this->getParamValue($param, 'endTime', $originalConfig->endTime ?? ''),
+ 'maxPerDay' => intval($this->getParamValue($param, 'maxPerDay', $originalConfig->maxPerDay ?? 0)),
+ 'pushOrder' => $this->getParamValue($param, 'pushOrder', $originalConfig->pushOrder ?? 1),
+ 'groupPushSubType' => $groupPushSubType,
+ 'status' => $this->toBoolInt($this->getParamValue($param, 'status', $originalConfig->status ?? 0)),
+ 'socialMediaId' => $this->getParamValue($param, 'socialMediaId', $originalConfig->socialMediaId ?? ''),
+ 'promotionSiteId' => $this->getParamValue($param, 'promotionSiteId', $originalConfig->promotionSiteId ?? ''),
+ 'friendIntervalMin' => intval($this->getParamValue($param, 'friendIntervalMin', $originalConfig->friendIntervalMin ?? 10)),
+ 'friendIntervalMax' => intval($this->getParamValue($param, 'friendIntervalMax', $originalConfig->friendIntervalMax ?? 20)),
+ 'messageIntervalMin' => intval($this->getParamValue($param, 'messageIntervalMin', $originalConfig->messageIntervalMin ?? 1)),
+ 'messageIntervalMax' => intval($this->getParamValue($param, 'messageIntervalMax', $originalConfig->messageIntervalMax ?? 12)),
+ 'isRandomTemplate' => $this->toBoolInt($this->getParamValue($param, 'isRandomTemplate', $originalConfig->isRandomTemplate ?? 0)),
+ 'ownerWechatIds' => json_encode($ownerWechatIds, JSON_UNESCAPED_UNICODE),
+ ];
+
+ if ($data['friendIntervalMin'] > $data['friendIntervalMax']) {
+ throw new \Exception('目标间最小间隔不能大于最大间隔');
+ }
+ if ($data['messageIntervalMin'] > $data['messageIntervalMax']) {
+ throw new \Exception('消息间最小间隔不能大于最大间隔');
+ }
+
+ $contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : [];
+ $contentGroupsParam = $this->getParamValue($param, 'contentGroups', null);
+ $contentGroups = $contentGroupsParam !== null
+ ? $this->extractIdList($contentGroupsParam, '内容库参数格式错误')
+ : $contentGroupsExisting;
+ $data['contentLibraries'] = json_encode($contentGroups, JSON_UNESCAPED_UNICODE);
+
+ $postPushTagsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->postPushTags ?? []) : [];
+ $postPushTagsParam = $this->getParamValue($param, 'postPushTags', null);
+ $postPushTags = $postPushTagsParam !== null
+ ? $this->extractIdList($postPushTagsParam, '推送标签参数格式错误')
+ : $postPushTagsExisting;
+ $data['postPushTags'] = json_encode($postPushTags, JSON_UNESCAPED_UNICODE);
+
+ if ($targetType === 1) {
+ $data['isLoop'] = $this->toBoolInt($this->getParamValue($param, 'isLoop', $originalConfig->isLoop ?? 0));
+
+ $groupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->groups ?? []) : [];
+ $wechatGroups = array_key_exists('wechatGroups', $param)
+ ? $this->extractIdList($param['wechatGroups'], '群参数格式错误')
+ : $groupsExisting;
+ if (empty($wechatGroups)) {
+ throw new \Exception('群推送必须选择微信群');
+ }
+ $data['groups'] = json_encode($wechatGroups, JSON_UNESCAPED_UNICODE);
+ $data['friends'] = json_encode([], JSON_UNESCAPED_UNICODE);
+ $data['trafficPools'] = json_encode([], JSON_UNESCAPED_UNICODE);
+
+ if ($groupPushSubType === 2) {
+ $announcementContent = $this->getParamValue($param, 'announcementContent', $originalConfig->announcementContent ?? '');
+ if (empty($announcementContent)) {
+ throw new \Exception('群公告必须输入公告内容');
+ }
+ $enableAiRewrite = $this->toBoolInt($this->getParamValue($param, 'enableAiRewrite', $originalConfig->enableAiRewrite ?? 0));
+ $aiRewritePrompt = trim((string)$this->getParamValue($param, 'aiRewritePrompt', $originalConfig->aiRewritePrompt ?? ''));
+ if ($enableAiRewrite === 1 && $aiRewritePrompt === '') {
+ throw new \Exception('启用AI智能话术改写时,必须输入改写提示词');
+ }
+ $data['announcementContent'] = $announcementContent;
+ $data['enableAiRewrite'] = $enableAiRewrite;
+ $data['aiRewritePrompt'] = $aiRewritePrompt;
+ } else {
+ $data['groupPushSubType'] = 1;
+ $data['announcementContent'] = '';
+ $data['enableAiRewrite'] = 0;
+ $data['aiRewritePrompt'] = '';
+ }
+ } else {
+ $data['isLoop'] = 0;
+ $friendsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->friends ?? []) : [];
+ $trafficPoolsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->trafficPools ?? []) : [];
+
+ $friendTargets = array_key_exists('wechatFriends', $param)
+ ? $this->extractIdList($param['wechatFriends'], '好友参数格式错误')
+ : $friendsExisting;
+ $trafficPools = array_key_exists('trafficPools', $param)
+ ? $this->extractIdList($param['trafficPools'], '流量池参数格式错误')
+ : $trafficPoolsExisting;
+
+ if (empty($friendTargets) && empty($trafficPools)) {
+ throw new \Exception('好友推送需至少选择好友或流量池');
+ }
+
+ $data['friends'] = json_encode($friendTargets, JSON_UNESCAPED_UNICODE);
+ $data['trafficPools'] = json_encode($trafficPools, JSON_UNESCAPED_UNICODE);
+ $data['groups'] = json_encode([], JSON_UNESCAPED_UNICODE);
+ $data['groupPushSubType'] = 1;
+ $data['announcementContent'] = '';
+ $data['enableAiRewrite'] = 0;
+ $data['aiRewritePrompt'] = '';
+ }
+
+ return $data;
+ }
+
+ /**
+ * 获取参数值,若不存在则返回默认值
+ * @param array $param
+ * @param string $key
+ * @param mixed $default
+ * @return mixed
+ */
+ private function getParamValue(array $param, string $key, $default)
+ {
+ return array_key_exists($key, $param) ? $param[$key] : $default;
+ }
+
+ /**
+ * 将值转换为整型布尔
+ * @param mixed $value
+ * @return int
+ */
+ private function toBoolInt($value): int
+ {
+ return empty($value) ? 0 : 1;
+ }
+
+ /**
+ * 从参数中提取ID列表
+ * @param mixed $items
+ * @param string $errorMessage
+ * @return array
+ * @throws \Exception
+ */
+ private function extractIdList($items, string $errorMessage = '参数格式错误'): array
+ {
+ if (!is_array($items)) {
+ throw new \Exception($errorMessage);
+ }
+
+ $ids = [];
+ foreach ($items as $item) {
+ if (is_array($item) && isset($item['id'])) {
+ $item = $item['id'];
+ }
+ if ($item === '' || $item === null) {
+ continue;
+ }
+ $ids[] = $item;
+ }
+
+ return array_values(array_unique($ids));
+ }
+
+ /**
+ * 解码JSON数组
+ * @param mixed $value
+ * @return array
+ */
+ private function decodeJsonArray($value): array
+ {
+ if (empty($value)) {
+ return [];
+ }
+ if (is_array($value)) {
+ return $value;
+ }
+ $decoded = json_decode($value, true);
+ return is_array($decoded) ? $decoded : [];
+ }
+
+ /**
+ * 验证内容是否包含链接
+ * @param string $content 要检测的内容
+ * @return bool
+ */
+ private function containsLink($content)
+ {
+ // 定义各种链接的正则表达式模式
+ $patterns = [
+ // HTTP/HTTPS链接
+ '/https?:\/\/[^\s]+/i',
+ // 京东商品链接
+ '/item\.jd\.com\/\d+/i',
+ // 京东短链接
+ '/u\.jd\.com\/[a-zA-Z0-9]+/i',
+ // 淘宝商品链接
+ '/item\.taobao\.com\/item\.htm\?id=\d+/i',
+ // 天猫商品链接
+ '/detail\.tmall\.com\/item\.htm\?id=\d+/i',
+ // 淘宝短链接
+ '/m\.tb\.cn\/[a-zA-Z0-9]+/i',
+ // 拼多多链接
+ '/mobile\.yangkeduo\.com\/goods\.html\?goods_id=\d+/i',
+ // 苏宁易购链接
+ '/product\.suning\.com\/\d+\/\d+\.html/i',
+ // 通用域名模式(包含常见电商域名)
+ '/(?:jd|taobao|tmall|yangkeduo|suning|amazon|dangdang)\.com[^\s]*/i',
+ // 通用短链接模式
+ '/[a-zA-Z0-9-]+\.[a-zA-Z]{2,}\/[a-zA-Z0-9\-._~:\/?#\[\]@!$&\'()*+,;=]+/i'
+ ];
+
+ // 遍历所有模式进行匹配
+ foreach ($patterns as $pattern) {
+ if (preg_match($pattern, $content)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+
+ /**
+ * 获取通讯录导入记录列表
+ * @return \think\response\Json
+ */
+ public function getImportContact()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $workbenchId = $this->request->param('workbenchId', 0);
+
+ $where = [
+ ['wici.workbenchId', '=', $workbenchId]
+ ];
+
+ // 查询发布记录
+ $list = Db::name('workbench_import_contact_item')->alias('wici')
+ ->join('traffic_pool tp', 'tp.id = wici.poolId', 'left')
+ ->join('traffic_source tc', 'tc.identifier = tp.identifier', 'left')
+ ->join('wechat_account wa', 'wa.wechatId = tp.wechatId', 'left')
+ ->field([
+ 'wici.id',
+ 'wici.workbenchId',
+ 'wici.createTime',
+ 'tp.identifier',
+ 'tp.mobile',
+ 'tp.wechatId',
+ 'tc.name',
+ 'wa.nickName',
+ 'wa.avatar',
+ 'wa.alias',
+ ])
+ ->where($where)
+ ->order('tc.name DESC,wici.createTime DESC')
+ ->group('tp.identifier')
+ ->page($page, $limit)
+ ->select();
+
+ foreach ($list as &$item) {
+ $item['createTime'] = date('Y-m-d H:i:s', $item['createTime']);
+ }
+
+
+ // 获取总记录数
+ $total = Db::name('workbench_import_contact_item')->alias('wici')
+ ->where($where)
+ ->count();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ ]
+ ]);
+ }
+
+ /**
+ * 获取群发统计数据
+ * @return \think\response\Json
+ */
+ public function getGroupPushStats()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchGroupPushController();
+ $controller->request = $this->request;
+ return $controller->getGroupPushStats();
+ }
+
+ /**
+ * 计算基础统计数据
+ */
+ private function calculateBasicStats($workbenchId, $where, $startTime, $endTime, $contentIds = null)
+ {
+ // 获取工作台配置,计算计划发送数
+ // 如果 workbenchId 为空,则查询所有工作台的配置
+ $configQuery = WorkbenchGroupPush::alias('wgp')
+ ->join('workbench w', 'w.id = wgp.workbenchId', 'left')
+ ->where('w.type', self::TYPE_GROUP_PUSH)
+ ->where('w.isDel', 0);
+
+ if (!empty($workbenchId)) {
+ $configQuery->where('wgp.workbenchId', $workbenchId);
+ } else {
+ // 如果没有指定工作台ID,需要从 where 条件中获取 workbenchId 列表
+ $workbenchIdCondition = null;
+ foreach ($where as $condition) {
+ if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') {
+ if ($condition[1] === 'in' && is_array($condition[2])) {
+ $workbenchIdCondition = $condition[2];
+ break;
+ } elseif ($condition[1] === '=') {
+ $workbenchIdCondition = [$condition[2]];
+ break;
+ }
+ }
+ }
+ if ($workbenchIdCondition) {
+ $configQuery->whereIn('wgp.workbenchId', $workbenchIdCondition);
+ }
+ }
+
+ $configs = $configQuery->select();
+ $targetType = 1; // 默认值
+ if (!empty($configs)) {
+ // 如果只有一个配置,使用它的 targetType;如果有多个,默认使用1
+ $targetType = intval($configs[0]->targetType ?? 1);
+ }
+
+ // 计划发送数(根据配置计算)
+ $plannedSend = 0;
+ if (!empty($configs)) {
+ $days = ceil(($endTime - $startTime) / 86400);
+ foreach ($configs as $config) {
+ $maxPerDay = intval($config->maxPerDay ?? 0);
+ $configTargetType = intval($config->targetType ?? 1);
+ if ($configTargetType == 1) {
+ // 群推送:计划发送数 = 每日推送次数 * 天数 * 群数量
+ $groups = $this->decodeJsonArray($config->groups ?? []);
+ $plannedSend += $maxPerDay * $days * count($groups);
+ } else {
+ // 好友推送:计划发送数 = 每日推送人数 * 天数
+ $plannedSend += $maxPerDay * $days;
+ }
+ }
+ }
+
+ // 构建查询条件
+ $queryWhere = $where;
+ if ($contentIds !== null) {
+ $queryWhere[] = ['wgpi.contentId', 'in', $contentIds];
+ }
+
+ // 实际成功发送数(从推送记录表统计)
+ $successSend = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($queryWhere)
+ ->count();
+
+ // 触达率 = 成功发送数 / 计划发送数
+ $reachRate = $plannedSend > 0 ? round(($successSend / $plannedSend) * 100, 1) : 0;
+
+ // 获取发送记录列表,用于查询回复
+ $sentItemIds = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($queryWhere)
+ ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId')
+ ->select();
+
+ // 回复统计(通过消息表查询)
+ $replyStats = $this->calculateReplyStats($sentItemIds, $targetType, $startTime, $endTime);
+
+ // 链接点击统计
+ $clickStats = $this->calculateClickStats($sentItemIds, $targetType, $startTime, $endTime);
+
+ // 计算本月对比数据(简化处理,实际应该查询上个月同期数据)
+ $currentMonthStart = strtotime(date('Y-m-01 00:00:00'));
+ $lastMonthStart = strtotime(date('Y-m-01 00:00:00', strtotime('-1 month')));
+ $lastMonthEnd = $currentMonthStart - 1;
+
+ // 获取本月统计数据(避免递归调用)
+ $currentMonthWhere = [
+ ['wgpi.createTime', '>=', $currentMonthStart]
+ ];
+ // 复制 workbenchId 条件
+ foreach ($where as $condition) {
+ if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') {
+ $currentMonthWhere[] = $condition;
+ break;
+ }
+ }
+ if ($contentIds !== null) {
+ $currentMonthWhere[] = ['wgpi.contentId', 'in', $contentIds];
+ }
+ $currentMonthSend = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($currentMonthWhere)
+ ->count();
+
+ // 获取本月配置
+ $currentMonthConfigQuery = WorkbenchGroupPush::alias('wgp')
+ ->join('workbench w', 'w.id = wgp.workbenchId', 'left')
+ ->where('w.type', self::TYPE_GROUP_PUSH)
+ ->where('w.isDel', 0);
+ if (!empty($workbenchId)) {
+ $currentMonthConfigQuery->where('wgp.workbenchId', $workbenchId);
+ } else {
+ foreach ($where as $condition) {
+ if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId' && $condition[1] === 'in') {
+ $currentMonthConfigQuery->whereIn('wgp.workbenchId', $condition[2]);
+ break;
+ }
+ }
+ }
+ $currentMonthConfigs = $currentMonthConfigQuery->select();
+ $currentMonthPlanned = 0;
+ if (!empty($currentMonthConfigs)) {
+ $currentMonthDays = ceil(($endTime - $currentMonthStart) / 86400);
+ foreach ($currentMonthConfigs as $currentMonthConfig) {
+ $currentMonthMaxPerDay = intval($currentMonthConfig->maxPerDay ?? 0);
+ $currentMonthTargetType = intval($currentMonthConfig->targetType ?? 1);
+ if ($currentMonthTargetType == 1) {
+ $currentMonthGroups = $this->decodeJsonArray($currentMonthConfig->groups ?? []);
+ $currentMonthPlanned += $currentMonthMaxPerDay * $currentMonthDays * count($currentMonthGroups);
+ } else {
+ $currentMonthPlanned += $currentMonthMaxPerDay * $currentMonthDays;
+ }
+ }
+ }
+ $currentMonthReachRate = $currentMonthPlanned > 0 ? round(($currentMonthSend / $currentMonthPlanned) * 100, 1) : 0;
+
+ // 获取上个月统计数据
+ $lastMonthWhere = [
+ ['wgpi.createTime', '>=', $lastMonthStart],
+ ['wgpi.createTime', '<=', $lastMonthEnd]
+ ];
+ // 复制 workbenchId 条件
+ foreach ($where as $condition) {
+ if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') {
+ $lastMonthWhere[] = $condition;
+ break;
+ }
+ }
+ if ($contentIds !== null) {
+ $lastMonthWhere[] = ['wgpi.contentId', 'in', $contentIds];
+ }
+ $lastMonthSend = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($lastMonthWhere)
+ ->count();
+
+ // 获取上个月配置
+ $lastMonthConfigQuery = WorkbenchGroupPush::alias('wgp')
+ ->join('workbench w', 'w.id = wgp.workbenchId', 'left')
+ ->where('w.type', self::TYPE_GROUP_PUSH)
+ ->where('w.isDel', 0);
+ if (!empty($workbenchId)) {
+ $lastMonthConfigQuery->where('wgp.workbenchId', $workbenchId);
+ } else {
+ foreach ($where as $condition) {
+ if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId' && $condition[1] === 'in') {
+ $lastMonthConfigQuery->whereIn('wgp.workbenchId', $condition[2]);
+ break;
+ }
+ }
+ }
+ $lastMonthConfigs = $lastMonthConfigQuery->select();
+
+ $lastMonthPlanned = 0;
+ if (!empty($lastMonthConfigs)) {
+ $lastMonthDays = ceil(($lastMonthEnd - $lastMonthStart) / 86400);
+ foreach ($lastMonthConfigs as $lastMonthConfig) {
+ $lastMonthMaxPerDay = intval($lastMonthConfig->maxPerDay ?? 0);
+ $lastMonthTargetType = intval($lastMonthConfig->targetType ?? 1);
+ if ($lastMonthTargetType == 1) {
+ $lastMonthGroups = $this->decodeJsonArray($lastMonthConfig->groups ?? []);
+ $lastMonthPlanned += $lastMonthMaxPerDay * $lastMonthDays * count($lastMonthGroups);
+ } else {
+ $lastMonthPlanned += $lastMonthMaxPerDay * $lastMonthDays;
+ }
+ }
+ }
+ $lastMonthReachRate = $lastMonthPlanned > 0 ? round(($lastMonthSend / $lastMonthPlanned) * 100, 1) : 0;
+
+ // 获取上个月的回复和点击统计(简化处理)
+ $lastMonthSentItems = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($lastMonthWhere)
+ ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId')
+ ->select();
+ $lastMonthReplyStats = $this->calculateReplyStats($lastMonthSentItems, $targetType, $lastMonthStart, $lastMonthEnd);
+ $lastMonthClickStats = $this->calculateClickStats($lastMonthSentItems, $targetType, $lastMonthStart, $lastMonthEnd);
+
+ return [
+ 'reachRate' => [
+ 'value' => $reachRate,
+ 'trend' => round($reachRate - $lastMonthReachRate, 1),
+ 'unit' => '%',
+ 'description' => '成功发送/计划发送'
+ ],
+ 'replyRate' => [
+ 'value' => $replyStats['replyRate'],
+ 'trend' => round($replyStats['replyRate'] - $lastMonthReplyStats['replyRate'], 1),
+ 'unit' => '%',
+ 'description' => '收到回复/成功发送'
+ ],
+ 'avgReplyTime' => [
+ 'value' => $replyStats['avgReplyTime'],
+ 'trend' => round($lastMonthReplyStats['avgReplyTime'] - $replyStats['avgReplyTime'], 0),
+ 'unit' => '分钟',
+ 'description' => '从发送到回复的平均时长'
+ ],
+ 'clickRate' => [
+ 'value' => $clickStats['clickRate'],
+ 'trend' => round($clickStats['clickRate'] - $lastMonthClickStats['clickRate'], 1),
+ 'unit' => '%',
+ 'description' => '点击链接/成功发送'
+ ],
+ 'plannedSend' => $plannedSend,
+ 'successSend' => $successSend,
+ 'replyCount' => $replyStats['replyCount'],
+ 'clickCount' => $clickStats['clickCount']
+ ];
+ }
+
+ /**
+ * 计算回复统计
+ */
+ private function calculateReplyStats($sentItems, $targetType, $startTime, $endTime)
+ {
+ if (empty($sentItems)) {
+ return ['replyRate' => 0, 'avgReplyTime' => 0, 'replyCount' => 0];
+ }
+
+ $replyCount = 0;
+ $totalReplyTime = 0;
+ $replyTimes = [];
+
+ foreach ($sentItems as $item) {
+ $itemArray = is_array($item) ? $item : (array)$item;
+ $sendTime = $itemArray['createTime'] ?? 0;
+ $accountId = $itemArray['wechatAccountId'] ?? 0;
+
+ if ($targetType == 1) {
+ // 群推送:查找群内回复消息
+ $groupId = $itemArray['groupId'] ?? 0;
+ $group = Db::name('wechat_group')->where('id', $groupId)->find();
+ if ($group) {
+ $replyMsg = Db::table('s2_wechat_message')
+ ->where('wechatChatroomId', $group['chatroomId'])
+ ->where('wechatAccountId', $accountId)
+ ->where('isSend', 0) // 接收的消息
+ ->where('wechatTime', '>', $sendTime)
+ ->where('wechatTime', '<=', $sendTime + 86400) // 24小时内回复
+ ->order('wechatTime', 'asc')
+ ->find();
+
+ if ($replyMsg) {
+ $replyCount++;
+ $replyTime = $replyMsg['wechatTime'] - $sendTime;
+ $replyTimes[] = $replyTime;
+ $totalReplyTime += $replyTime;
+ }
+ }
+ } else {
+ // 好友推送:查找好友回复消息
+ $friendId = $itemArray['friendId'] ?? 0;
+ $friend = Db::table('s2_wechat_friend')->where('id', $friendId)->find();
+ if ($friend) {
+ $replyMsg = Db::table('s2_wechat_message')
+ ->where('wechatFriendId', $friendId)
+ ->where('wechatAccountId', $accountId)
+ ->where('isSend', 0) // 接收的消息
+ ->where('wechatTime', '>', $sendTime)
+ ->where('wechatTime', '<=', $sendTime + 86400) // 24小时内回复
+ ->order('wechatTime', 'asc')
+ ->find();
+
+ if ($replyMsg) {
+ $replyCount++;
+ $replyTime = $replyMsg['wechatTime'] - $sendTime;
+ $replyTimes[] = $replyTime;
+ $totalReplyTime += $replyTime;
+ }
+ }
+ }
+ }
+
+ $successSend = count($sentItems);
+ $replyRate = $successSend > 0 ? round(($replyCount / $successSend) * 100, 1) : 0;
+ $avgReplyTime = $replyCount > 0 ? round(($totalReplyTime / $replyCount) / 60, 0) : 0; // 转换为分钟
+
+ return [
+ 'replyRate' => $replyRate,
+ 'avgReplyTime' => $avgReplyTime,
+ 'replyCount' => $replyCount
+ ];
+ }
+
+ /**
+ * 计算链接点击统计
+ */
+ private function calculateClickStats($sentItems, $targetType, $startTime, $endTime)
+ {
+ if (empty($sentItems)) {
+ return ['clickRate' => 0, 'clickCount' => 0];
+ }
+
+ $clickCount = 0;
+ $linkContentIds = [];
+
+ // 获取所有发送的内容ID
+ foreach ($sentItems as $item) {
+ $itemArray = is_array($item) ? $item : (array)$item;
+ $contentId = $itemArray['contentId'] ?? 0;
+ if ($contentId > 0) {
+ $linkContentIds[] = $contentId;
+ }
+ }
+
+ if (empty($linkContentIds)) {
+ return ['clickRate' => 0, 'clickCount' => 0];
+ }
+
+ // 查询包含链接的内容
+ $linkContents = Db::name('content_item')
+ ->whereIn('id', array_unique($linkContentIds))
+ ->where('contentType', 2) // 链接类型
+ ->column('id');
+
+ // 统计发送了链接内容的记录数
+ $linkSendCount = 0;
+ foreach ($sentItems as $item) {
+ $itemArray = is_array($item) ? $item : (array)$item;
+ $contentId = $itemArray['contentId'] ?? 0;
+ if (in_array($contentId, $linkContents)) {
+ $linkSendCount++;
+ }
+ }
+
+ // 简化处理:假设点击率基于链接消息的发送(实际应该从点击追踪系统获取)
+ // 这里可以根据业务需求调整,比如通过消息中的链接点击事件统计
+ $clickCount = $linkSendCount; // 简化处理,实际需要真实的点击数据
+
+ $successSend = count($sentItems);
+ $clickRate = $successSend > 0 ? round(($clickCount / $successSend) * 100, 1) : 0;
+
+ return [
+ 'clickRate' => $clickRate,
+ 'clickCount' => $clickCount
+ ];
+ }
+
+ /**
+ * 获取话术组对比数据
+ */
+ private function getContentLibraryComparison($workbenchId, $where, $startTime, $endTime, $contentIds = null)
+ {
+ $queryWhere = $where;
+ if ($contentIds !== null) {
+ $queryWhere[] = ['wgpi.contentId', 'in', $contentIds];
+ }
+
+ $comparison = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->join('content_item ci', 'ci.id = wgpi.contentId', 'left')
+ ->join('content_library cl', 'cl.id = ci.libraryId', 'left')
+ ->where($queryWhere)
+ ->where('cl.id', '<>', null)
+ ->field([
+ 'cl.id as libraryId',
+ 'cl.name as libraryName',
+ 'COUNT(DISTINCT wgpi.id) as pushCount'
+ ])
+ ->group('cl.id, cl.name')
+ ->select();
+
+ $result = [];
+ foreach ($comparison as $item) {
+ $libraryId = $item['libraryId'];
+ $pushCount = intval($item['pushCount']);
+
+ // 获取该内容库的详细统计
+ $libraryContentIds = Db::name('content_item')
+ ->where('libraryId', $libraryId)
+ ->column('id');
+ if (empty($libraryContentIds)) {
+ $libraryContentIds = [-1];
+ }
+
+ $libraryWhere = array_merge($where, [['wgpi.contentId', 'in', $libraryContentIds]]);
+ $librarySentItems = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($libraryWhere)
+ ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId')
+ ->select();
+
+ $config = WorkbenchGroupPush::where('workbenchId', $workbenchId)->find();
+ $targetType = $config ? intval($config->targetType) : 1;
+
+ $replyStats = $this->calculateReplyStats($librarySentItems, $targetType, $startTime, $endTime);
+ $clickStats = $this->calculateClickStats($librarySentItems, $targetType, $startTime, $endTime);
+
+ // 计算转化率(简化处理,实际需要根据业务定义)
+ $conversionRate = $pushCount > 0 ? round(($replyStats['replyCount'] / $pushCount) * 100, 1) : 0;
+
+ $result[] = [
+ 'libraryId' => $libraryId,
+ 'libraryName' => $item['libraryName'],
+ 'pushCount' => $pushCount,
+ 'reachRate' => 100, // 简化处理,实际应该计算
+ 'replyRate' => $replyStats['replyRate'],
+ 'clickRate' => $clickStats['clickRate'],
+ 'conversionRate' => $conversionRate,
+ 'avgReplyTime' => $replyStats['avgReplyTime'],
+ 'level' => $this->getPerformanceLevel($replyStats['replyRate'], $clickStats['clickRate'], $conversionRate)
+ ];
+ }
+
+ // 按回复率排序
+ usort($result, function($a, $b) {
+ return $b['replyRate'] <=> $a['replyRate'];
+ });
+
+ return $result;
+ }
+
+ /**
+ * 获取性能等级
+ */
+ private function getPerformanceLevel($replyRate, $clickRate, $conversionRate)
+ {
+ $score = ($replyRate * 0.4) + ($clickRate * 0.3) + ($conversionRate * 0.3);
+
+ if ($score >= 40) {
+ return '优秀';
+ } elseif ($score >= 25) {
+ return '良好';
+ } elseif ($score >= 15) {
+ return '一般';
+ } else {
+ return '待提升';
+ }
+ }
+
+ /**
+ * 获取时段分析数据
+ */
+ private function getTimePeriodAnalysis($workbenchId, $where, $startTime, $endTime, $contentIds = null)
+ {
+ $queryWhere = $where;
+ if ($contentIds !== null) {
+ $queryWhere[] = ['wgpi.contentId', 'in', $contentIds];
+ }
+
+ $analysis = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($queryWhere)
+ ->field([
+ 'FROM_UNIXTIME(wgpi.createTime, "%H") as hour',
+ 'COUNT(*) as count'
+ ])
+ ->group('hour')
+ ->order('hour', 'asc')
+ ->select();
+
+ $result = [];
+ foreach ($analysis as $item) {
+ $result[] = [
+ 'hour' => intval($item['hour']),
+ 'count' => intval($item['count'])
+ ];
+ }
+
+ return $result;
+ }
+
+ /**
+ * 获取互动深度数据
+ */
+ private function getInteractionDepth($workbenchId, $where, $startTime, $endTime, $contentIds = null)
+ {
+ // 简化处理,实际需要更复杂的统计逻辑
+ return [
+ 'singleReply' => 0, // 单次回复
+ 'multipleReply' => 0, // 多次回复
+ 'deepInteraction' => 0 // 深度互动
+ ];
+ }
+
+ /**
+ * 获取推送历史记录列表
+ * @return \think\response\Json
+ */
+ public function getGroupPushHistory()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchGroupPushController();
+ $controller->request = $this->request;
+ return $controller->getGroupPushHistory();
+ }
+
+ /**
+ * 获取状态文本
+ * @param string $status 状态码
+ * @return string 状态文本
+ */
+ private function getStatusText($status)
+ {
+ $statusMap = [
+ 'success' => '已完成',
+ 'partial' => '进行中',
+ 'pending' => '进行中',
+ 'failed' => '失败'
+ ];
+ return $statusMap[$status] ?? '未知';
+ }
+
+ /**
+ * 获取已创建的群列表(自动建群)
+ * @return \think\response\Json
+ */
+ public function getCreatedGroupsList()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 100);
+ $keyword = $this->request->param('keyword', '');
+
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '工作台ID不能为空']);
+ }
+
+ // 验证工作台权限
+ $where = [
+ ['id', '=', $workbenchId],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['type', '=', self::TYPE_GROUP_CREATE],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+ $workbench = Workbench::where($where)->find();
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在或无权限']);
+ }
+
+ // 获取已创建的群ID列表(状态为成功且groupId不为空)
+ $groupIds = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('status', 2) // STATUS_SUCCESS = 2
+ ->where('groupId', '<>', null)
+ ->group('groupId')
+ ->column('groupId');
+
+ if (empty($groupIds)) {
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => [],
+ 'total' => 0,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]);
+ }
+
+ // 查询群组详细信息(从s2_wechat_chatroom表查询)
+ $query = Db::table('s2_wechat_chatroom')->alias('wc')
+ ->join('wechat_account wa', 'wa.wechatId = wc.wechatAccountWechatId', 'left')
+ ->where('wc.id', 'in', $groupIds)
+ ->where('wc.isDeleted', 0);
+
+ // 关键字搜索
+ if (!empty($keyword)) {
+ $query->where(function ($q) use ($keyword) {
+ $q->where('wc.nickname', 'like', '%' . $keyword . '%')
+ ->whereOr('wc.chatroomId', 'like', '%' . $keyword . '%')
+ ->whereOr('wa.nickName', 'like', '%' . $keyword . '%');
+ });
+ }
+
+ $total = $query->count();
+ $list = $query->field('wc.id,wc.nickname as groupName,wc.chatroomId,wc.chatroomAvatar as groupAvatar,wc.wechatAccountWechatId as ownerWechatId,wc.createTime,wc.chatroomOwnerNickname as ownerNickname,wc.chatroomOwnerAvatar as ownerAvatar,wa.alias as ownerAlias')
+ ->order('wc.createTime', 'desc')
+ ->page($page, $limit)
+ ->select();
+
+ // 统计每个群的成员数量和成员信息
+ foreach ($list as &$item) {
+ // 统计该群的成员数量(从workbench_group_create_item表统计)
+ $memberCount = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('groupId', $item['id'])
+ ->where('status', 'in', [2, 4]) // 创建成功和管理员好友已拉入
+ ->count();
+
+ // 获取成员列表(用于显示成员头像)
+ $memberList = Db::name('workbench_group_create_item')->alias('wgci')
+ ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wgci.friendId', 'left')
+ ->where('wgci.workbenchId', $workbenchId)
+ ->where('wgci.groupId', $item['id'])
+ ->where('wgci.status', 'in', [2, 4])
+ ->field('wf.avatar, wf.wechatId, wf.nickname')
+ ->order('wgci.createTime', 'asc')
+ ->limit(10) // 最多显示10个成员头像
+ ->select();
+
+ // 格式化成员头像列表
+ $memberAvatars = [];
+ foreach ($memberList as $member) {
+ if (!empty($member['avatar'])) {
+ $memberAvatars[] = [
+ 'avatar' => $member['avatar'],
+ 'wechatId' => $member['wechatId'] ?? '',
+ 'nickname' => $member['nickname'] ?? ''
+ ];
+ }
+ }
+
+ // 计算剩余成员数(用于显示"+XX")
+ $remainingCount = $memberCount > count($memberAvatars) ? $memberCount - count($memberAvatars) : 0;
+
+ // 格式化返回数据
+ $item['memberCount'] = $memberCount;
+ $item['memberCountText'] = $memberCount . '人'; // 格式化为"XX人"
+ $item['createTime'] = !empty($item['createTime']) ? date('Y-m-d', $item['createTime']) : ''; // 格式化为"YYYY-MM-DD"
+ $item['memberAvatars'] = $memberAvatars; // 成员头像列表(最多10个)
+ $item['remainingCount'] = $remainingCount; // 剩余成员数(用于显示"+XX")
+
+ // 保留原有字段,但调整格式
+ $item['groupName'] = $item['groupName'] ?? '';
+ $item['groupAvatar'] = $item['groupAvatar'] ?? '';
+ }
+ unset($item);
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]);
+ }
+
+ /**
+ * 获取已创建群的详情(自动建群)
+ * @return \think\response\Json
+ */
+ public function getCreatedGroupDetail()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ $groupId = $this->request->param('groupId', 0);
+
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '工作台ID不能为空']);
+ }
+
+ if (empty($groupId)) {
+ return json(['code' => 400, 'msg' => '群ID不能为空']);
+ }
+
+ // 验证工作台权限
+ $where = [
+ ['id', '=', $workbenchId],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['type', '=', self::TYPE_GROUP_CREATE],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+ $workbench = Workbench::where($where)->find();
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在或无权限']);
+ }
+
+ // 验证该群是否属于该工作台
+ $groupItem = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('groupId', $groupId)
+ ->where('status', 2) // STATUS_SUCCESS = 2
+ ->find();
+
+ if (empty($groupItem)) {
+ return json(['code' => 404, 'msg' => '群不存在或不属于该工作台']);
+ }
+
+ // 查询群基本信息(从s2_wechat_chatroom表查询)
+ $group = Db::table('s2_wechat_chatroom')->alias('wc')
+ ->join('wechat_account wa', 'wa.wechatId = wc.wechatAccountWechatId', 'left')
+ ->where('wc.id', $groupId)
+ ->where('wc.isDeleted', 0)
+ ->field('wc.id,wc.nickname as groupName,wc.chatroomId,wc.chatroomAvatar as groupAvatar,wc.wechatAccountWechatId as ownerWechatId,wc.createTime,wc.chatroomOwnerNickname as ownerNickname,wc.chatroomOwnerAvatar as ownerAvatar,wa.alias as ownerAlias,wc.announce')
+ ->find();
+
+ if (empty($group)) {
+ return json(['code' => 404, 'msg' => '群不存在']);
+ }
+
+ // 获取chatroomId
+ $chatroomId = $group['chatroomId'] ?? '';
+ if (empty($chatroomId)) {
+ return json(['code' => 400, 'msg' => '群聊ID不存在']);
+ }
+
+ // 从s2_wechat_chatroom_member表查询所有成员列表(不限制数量)
+ $memberList = Db::table('s2_wechat_chatroom_member')->alias('wcm')
+ ->join(['s2_wechat_friend' => 'wf'], 'wf.wechatId = wcm.wechatId', 'left')
+ ->where('wcm.chatroomId', $chatroomId)
+ ->field('wcm.wechatId,wcm.nickname as memberNickname,wcm.avatar as memberAvatar,wcm.conRemark as memberRemark,wcm.alias as memberAlias,wcm.createTime as joinTime,wcm.updateTime,wf.id as friendId,wf.nickname as friendNickname,wf.avatar as friendAvatar')
+ ->order('wcm.createTime', 'asc')
+ ->select();
+
+ // 去重:按wechatId去重,保留第一条记录
+ $memberMap = [];
+ foreach ($memberList as $member) {
+ $wechatId = $member['wechatId'] ?? '';
+ if (!empty($wechatId) && !isset($memberMap[$wechatId])) {
+ $memberMap[$wechatId] = $member;
+ }
+ }
+ $memberList = array_values($memberMap); // 重新索引数组
+
+ // 获取在群中的成员wechatId列表(用于判断是否已退群)
+ $inGroupWechatIds = array_column($memberList, 'wechatId');
+ $inGroupWechatIds = array_filter($inGroupWechatIds); // 过滤空值
+
+ // 获取通过自动建群加入的成员信息(用于判断入群状态和已退群成员)
+ $autoJoinMemberList = Db::name('workbench_group_create_item')
+ ->alias('wgci')
+ ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wgci.friendId', 'left')
+ ->where('wgci.workbenchId', $workbenchId)
+ ->where('wgci.groupId', $groupId)
+ ->where('wgci.status', 'in', [2, 4]) // 创建成功和管理员好友已拉入
+ ->field('wf.wechatId,wf.id as friendId,wf.nickname as friendNickname,wf.avatar as friendAvatar,wgci.createTime as autoJoinTime')
+ ->select();
+
+ // 去重:按wechatId去重,保留第一条记录
+ $autoJoinMemberMap = [];
+ foreach ($autoJoinMemberList as $autoMember) {
+ $wechatId = $autoMember['wechatId'] ?? '';
+ if (!empty($wechatId) && !isset($autoJoinMemberMap[$wechatId])) {
+ $autoJoinMemberMap[$wechatId] = $autoMember;
+ }
+ }
+ $autoJoinMemberList = array_values($autoJoinMemberMap); // 重新索引数组
+
+ // 统计成员总数(包括在群中的成员和已退群的成员)
+ // 先统计在群中的成员数
+ $inGroupCount = count($memberList);
+
+ // 统计已退群的成员数(在自动建群记录中但不在群中的成员)
+ $quitCount = 0;
+ if (!empty($autoJoinMemberList)) {
+ foreach ($autoJoinMemberList as $autoMember) {
+ $wechatId = $autoMember['wechatId'] ?? '';
+ if (!empty($wechatId) && !in_array($wechatId, $inGroupWechatIds)) {
+ $quitCount++;
+ }
+ }
+ }
+
+ // 总成员数 = 在群中的成员数 + 已退群的成员数
+ $memberCount = $inGroupCount + $quitCount;
+
+ // 获取自动建群加入的成员wechatId列表(用于判断入群状态)
+ $autoJoinWechatIds = array_column($autoJoinMemberList, 'wechatId');
+ $autoJoinWechatIds = array_filter($autoJoinWechatIds); // 过滤空值
+
+ // 格式化在群中的成员列表
+ $members = [];
+ $addedWechatIds = []; // 用于记录已添加的成员wechatId,避免重复
+ $ownerWechatId = $group['ownerWechatId'] ?? '';
+ foreach ($memberList as $member) {
+ $wechatId = $member['wechatId'] ?? '';
+
+ // 跳过空wechatId和已添加的成员
+ if (empty($wechatId) || in_array($wechatId, $addedWechatIds)) {
+ continue;
+ }
+
+ // 标记为已添加
+ $addedWechatIds[] = $wechatId;
+
+ // 根据wechatId判断是否为群主
+ $isOwner = (!empty($ownerWechatId) && $wechatId == $ownerWechatId) ? 1 : 0;
+
+ // 判断入群状态:如果在自动建群记录中,说明是通过自动建群加入的;否则是其他方式加入的
+ $joinStatus = in_array($wechatId, $autoJoinWechatIds) ? 'auto' : 'manual';
+
+ // 判断是否已退群:如果成员在s2_wechat_chatroom_member表中存在,说明在群中;否则已退群
+ // 由于我们已经从s2_wechat_chatroom_member表查询,所以这里都是"在群中"状态
+ $isQuit = 0; // 0=在群中,1=已退群
+
+ // 优先使用friend表的昵称和头像,如果没有则使用member表的
+ $nickname = !empty($member['friendNickname']) ? $member['friendNickname'] : ($member['memberNickname'] ?? '');
+ $avatar = !empty($member['friendAvatar']) ? $member['friendAvatar'] : ($member['memberAvatar'] ?? '');
+
+ $members[] = [
+ 'friendId' => $member['friendId'] ?? 0,
+ 'wechatId' => $wechatId,
+ 'nickname' => $nickname,
+ 'avatar' => $avatar,
+ 'alias' => $member['memberAlias'] ?? '',
+ 'remark' => $member['memberRemark'] ?? '',
+ 'isOwner' => $isOwner, // 标记群主
+ 'joinStatus' => $joinStatus, // 入群状态:auto=自动建群加入,manual=其他方式加入
+ 'isQuit' => $isQuit, // 是否已退群:0=在群中,1=已退群
+ 'joinTime' => !empty($member['joinTime']) ? date('Y-m-d H:i:s', $member['joinTime']) : '', // 入群时间
+ ];
+ }
+
+ // 添加已退群的成员(在自动建群记录中但不在群中的成员)
+ foreach ($autoJoinMemberList as $autoMember) {
+ $wechatId = $autoMember['wechatId'] ?? '';
+
+ // 跳过空wechatId、已在群中的成员和已添加的成员
+ if (empty($wechatId) || in_array($wechatId, $inGroupWechatIds) || in_array($wechatId, $addedWechatIds)) {
+ continue;
+ }
+
+ // 标记为已添加
+ $addedWechatIds[] = $wechatId;
+
+ // 根据wechatId判断是否为群主
+ $isOwner = (!empty($ownerWechatId) && $wechatId == $ownerWechatId) ? 1 : 0;
+
+ $members[] = [
+ 'friendId' => $autoMember['friendId'] ?? 0,
+ 'wechatId' => $wechatId,
+ 'nickname' => $autoMember['friendNickname'] ?? '',
+ 'avatar' => $autoMember['friendAvatar'] ?? '',
+ 'alias' => '',
+ 'remark' => '',
+ 'isOwner' => $isOwner, // 标记群主
+ 'joinStatus' => 'auto', // 入群状态:auto=自动建群加入
+ 'isQuit' => 1, // 是否已退群:1=已退群
+ 'joinTime' => !empty($autoMember['autoJoinTime']) ? date('Y-m-d H:i:s', $autoMember['autoJoinTime']) : '', // 入群时间
+ ];
+ }
+
+ // 将群主排在第一位
+ usort($members, function($a, $b) {
+ if ($a['isOwner'] == $b['isOwner']) {
+ return 0;
+ }
+ return $a['isOwner'] > $b['isOwner'] ? -1 : 1;
+ });
+
+ // 格式化返回数据
+ $result = [
+ 'id' => $group['id'],
+ 'groupName' => $group['groupName'] ?? '',
+ 'chatroomId' => $group['chatroomId'] ?? '',
+ 'groupAvatar' => $group['groupAvatar'] ?? '',
+ 'ownerWechatId' => $group['ownerWechatId'] ?? '',
+ 'ownerNickname' => $group['ownerNickname'] ?? '',
+ 'ownerAvatar' => $group['ownerAvatar'] ?? '',
+ 'ownerAlias' => $group['ownerAlias'] ?? '',
+ 'announce' => $group['announce'] ?? '',
+ 'createTime' => !empty($group['createTime']) ? date('Y-m-d H:i', $group['createTime']) : '', // 格式化为"YYYY-MM-DD HH:MM"
+ 'memberCount' => $memberCount,
+ 'memberCountText' => $memberCount . '人', // 格式化为"XX人"
+ 'workbenchName' => $workbench->name ?? '', // 任务名称(工作台名称)
+ 'members' => $members // 所有成员列表
+ ];
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => $result
+ ]);
+ }
+
+ /**
+ * 同步群最新信息(包括群成员)
+ * @return \think\response\Json
+ */
+ public function syncGroupInfo()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ $groupId = $this->request->param('groupId', 0);
+
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '工作台ID不能为空']);
+ }
+
+ if (empty($groupId)) {
+ return json(['code' => 400, 'msg' => '群ID不能为空']);
+ }
+
+ // 验证工作台权限
+ $where = [
+ ['id', '=', $workbenchId],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['type', '=', self::TYPE_GROUP_CREATE],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+ $workbench = Workbench::where($where)->find();
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在或无权限']);
+ }
+
+ // 验证该群是否属于该工作台
+ $groupItem = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('groupId', $groupId)
+ ->where('status', 2) // STATUS_SUCCESS = 2
+ ->find();
+
+ if (empty($groupItem)) {
+ return json(['code' => 404, 'msg' => '群不存在或不属于该工作台']);
+ }
+
+ // 查询群基本信息,获取chatroomId和wechatAccountWechatId
+ $group = Db::table('s2_wechat_chatroom')
+ ->where('id', $groupId)
+ ->where('isDeleted', 0)
+ ->field('id,chatroomId,wechatAccountWechatId')
+ ->find();
+
+ if (empty($group)) {
+ return json(['code' => 404, 'msg' => '群不存在']);
+ }
+
+ $chatroomId = $group['chatroomId'] ?? '';
+ $wechatAccountWechatId = $group['wechatAccountWechatId'] ?? '';
+
+ if (empty($chatroomId)) {
+ return json(['code' => 400, 'msg' => '群聊ID不存在']);
+ }
+
+ try {
+ // 实例化WechatChatroomController
+ $chatroomController = new \app\api\controller\WechatChatroomController();
+
+ // 1. 同步群信息(调用getlist方法)
+ $syncData = [
+ 'wechatChatroomId' => $chatroomId, // 使用chatroomId作为wechatChatroomId来指定要同步的群
+ 'wechatAccountKeyword' => $wechatAccountWechatId, // 通过群主微信ID筛选
+ 'isDeleted' => false,
+ 'pageIndex' => 1,
+ 'pageSize' => 100 // 获取足够多的数据
+ ];
+ $syncResult = $chatroomController->getlist($syncData, true, 0); // isInner = true
+ $syncResponse = json_decode($syncResult, true);
+
+ if (empty($syncResponse['code']) || $syncResponse['code'] != 200) {
+ return json(['code' => 500, 'msg' => '同步群信息失败:' . ($syncResponse['msg'] ?? '未知错误')]);
+ }
+
+ // 2. 同步群成员信息(调用listChatroomMember方法)
+ // wechatChatroomId 使用 s2_wechat_chatroom 表的 id(即groupId)
+ // chatroomId 使用群聊ID(chatroomId)
+ $wechatChatroomId = $groupId; // s2_wechat_chatroom表的id
+ $memberSyncResult = $chatroomController->listChatroomMember($wechatChatroomId, $chatroomId, true); // isInner = true
+ $memberSyncResponse = json_decode($memberSyncResult, true);
+
+ if (empty($memberSyncResponse['code']) || $memberSyncResponse['code'] != 200) {
+ // 成员同步失败不影响整体结果,记录警告即可
+ \think\facade\Log::warning("同步群成员失败。群ID: {$groupId}, 群聊ID: {$chatroomId}, 错误: " . ($memberSyncResponse['msg'] ?? '未知错误'));
+ }
+
+ return json([
+ 'code' => 200,
+ 'msg' => '同步成功',
+ 'data' => [
+ 'groupId' => $groupId,
+ 'chatroomId' => $chatroomId,
+ 'groupInfoSynced' => true,
+ 'memberInfoSynced' => !empty($memberSyncResponse['code']) && $memberSyncResponse['code'] == 200
+ ]
+ ]);
+ } catch (\Exception $e) {
+ \think\facade\Log::error("同步群信息异常。群ID: {$groupId}, 错误: " . $e->getMessage());
+ return json(['code' => 500, 'msg' => '同步失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 修改群名称、群公告
+ * @return \think\response\Json
+ */
+ public function modifyGroupInfo()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ $groupId = $this->request->param('groupId', 0);
+ $chatroomName = $this->request->param('chatroomName', '');
+ $announce = $this->request->param('announce', '');
+
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '工作台ID不能为空']);
+ }
+
+ if (empty($groupId)) {
+ return json(['code' => 400, 'msg' => '群ID不能为空']);
+ }
+
+ // 至少需要提供一个修改项
+ if (empty($chatroomName) && empty($announce)) {
+ return json(['code' => 400, 'msg' => '请至少提供群名称或群公告中的一个参数']);
+ }
+
+ // 查询群基本信息
+ $group = Db::table('s2_wechat_chatroom')
+ ->where('id', $groupId)
+ ->where('isDeleted', 0)
+ ->field('id,chatroomId,wechatAccountWechatId,accountId,wechatAccountId')
+ ->find();
+
+ if (empty($group)) {
+ return json(['code' => 404, 'msg' => '群不存在']);
+ }
+
+ $chatroomId = $group['id'] ?? '';
+
+ if (empty($chatroomId)) {
+ return json(['code' => 400, 'msg' => '群聊ID不存在']);
+ }
+
+ try {
+ // 直接使用群表中的账号信息
+ $executeAccountId = $group['accountId'] ?? 0;
+ $executeWechatAccountId = $group['wechatAccountId'] ?? 0;
+ $executeWechatId = $group['wechatAccountWechatId'] ?? '';
+
+ // 确保 wechatId 不为空
+ if (empty($executeWechatId)) {
+ return json(['code' => 400, 'msg' => '无法获取微信账号ID']);
+ }
+
+ // 调用 WebSocketController 修改群信息
+ // 获取系统API账号信息(用于WebSocket连接)
+ $username = Env::get('api.username2', '');
+ $password = Env::get('api.password2', '');
+
+ if (empty($username) || empty($password)) {
+ return json(['code' => 500, 'msg' => '系统API账号配置缺失']);
+ }
+
+ // 获取系统账号ID
+ $systemAccountId = Db::name('users')->where('account', $username)->value('s2_accountId');
+ if (empty($systemAccountId)) {
+ return json(['code' => 500, 'msg' => '未找到系统账号ID']);
+ }
+
+ $webSocketController = new WebSocketController([
+ 'userName' => $username,
+ 'password' => $password,
+ 'accountId' => $systemAccountId
+ ]);
+ // 构建修改参数
+ $modifyData = [
+ 'wechatChatroomId' => $chatroomId,
+ 'wechatAccountId' => $executeWechatAccountId,
+ ];
+ if (!empty($chatroomName)) {
+ $modifyData['chatroomName'] = $chatroomName;
+ }
+ if (!empty($announce)) {
+ $modifyData['announce'] = $announce;
+ }
+
+
+ $modifyResult = $webSocketController->CmdChatroomModifyInfo($modifyData);
+ $modifyResponse = json_decode($modifyResult, true);
+ if (empty($modifyResponse['code']) || $modifyResponse['code'] != 200) {
+ return json(['code' => 500, 'msg' => '修改群信息失败:' . ($modifyResponse['msg'] ?? '未知错误')]);
+ }
+
+ // 修改成功后更新数据库
+ $updateData = [
+ 'updateTime' => time()
+ ];
+
+ // 如果修改了群名称,更新数据库
+ if (!empty($chatroomName)) {
+ $updateData['nickname'] = $chatroomName;
+ }
+
+ // 如果修改了群公告,更新数据库
+ if (!empty($announce)) {
+ $updateData['announce'] = $announce;
+ }
+
+ // 更新数据库
+ Db::table('s2_wechat_chatroom')
+ ->where('id', $groupId)
+ ->update($updateData);
+
+ return json([
+ 'code' => 200,
+ 'msg' => '修改成功',
+ 'data' => [
+ 'groupId' => $groupId,
+ 'chatroomId' => $chatroomId,
+ 'chatroomName' => $chatroomName,
+ 'announce' => $announce
+ ]
+ ]);
+ } catch (\Exception $e) {
+ \think\facade\Log::error("修改群信息异常。群ID: {$groupId}, 错误: " . $e->getMessage());
+ return json(['code' => 500, 'msg' => '修改失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 转移群聊到指定账号
+ * @param int $groupId 群ID(s2_wechat_chatroom表的id)
+ * @param string $chatroomId 群聊ID
+ * @param int $toAccountId 目标账号ID(s2_company_account表的id)
+ * @param int $toWechatAccountId 目标微信账号ID(s2_wechat_account表的id)
+ * @return array ['success' => bool, 'msg' => string]
+ */
+ protected function transferChatroomToAccount($groupId, $chatroomId, $toAccountId, $toWechatAccountId)
+ {
+ try {
+ // 查询目标账号信息
+ $targetAccount = Db::table('s2_company_account')
+ ->where('id', $toAccountId)
+ ->field('id,userName,realName,nickname')
+ ->find();
+
+ if (empty($targetAccount)) {
+ return ['success' => false, 'msg' => '目标账号不存在'];
+ }
+
+ // 查询目标微信账号信息
+ $targetWechatAccount = Db::table('s2_wechat_account')
+ ->where('id', $toWechatAccountId)
+ ->field('id,wechatId,deviceAccountId')
+ ->find();
+
+ if (empty($targetWechatAccount)) {
+ return ['success' => false, 'msg' => '目标微信账号不存在'];
+ }
+
+ // 调用 AutomaticAssign 进行群聊转移
+ $automaticAssign = new \app\api\controller\AutomaticAssign();
+
+ // 构建转移参数(通过 API 调用)
+ $transferData = [
+ 'wechatChatroomId' => $chatroomId, // 使用群聊ID
+ 'toAccountId' => $toAccountId,
+ 'wechatAccountKeyword' => $targetWechatAccount['wechatId'] ?? '',
+ 'isDeleted' => false
+ ];
+
+ // 直接更新数据库(因为 API 可能不支持指定单个群聊ID转移)
+ // 更新 s2_wechat_chatroom 表的 accountId 和 wechatAccountId
+ Db::table('s2_wechat_chatroom')
+ ->where('id', $groupId)
+ ->update([
+ 'accountId' => $toAccountId,
+ 'accountUserName' => $targetAccount['userName'] ?? '',
+ 'accountRealName' => $targetAccount['realName'] ?? '',
+ 'accountNickname' => $targetAccount['nickname'] ?? '',
+ 'wechatAccountId' => $toWechatAccountId,
+ 'wechatAccountWechatId' => $targetWechatAccount['wechatId'] ?? '',
+ 'updateTime' => time()
+ ]);
+
+ return ['success' => true, 'msg' => '转移成功'];
+ } catch (\Exception $e) {
+ \think\facade\Log::error("转移群聊异常。群ID: {$groupId}, 目标账号ID: {$toAccountId}, 错误: " . $e->getMessage());
+ return ['success' => false, 'msg' => $e->getMessage()];
+ }
+ }
+
+ /**
+ * 退群功能(自动建群)
+ * @return \think\response\Json
+ */
+ public function quitGroup()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchGroupCreateController();
+ $controller->request = $this->request;
+ return $controller->quitGroup();
+ }
+
+
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/workbench/WorkbenchController_new.php b/application/cunkebao/controller/workbench/WorkbenchController_new.php
new file mode 100644
index 0000000..8363ea6
--- /dev/null
+++ b/application/cunkebao/controller/workbench/WorkbenchController_new.php
@@ -0,0 +1,3006 @@
+request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ // 获取登录用户信息
+ $userInfo = request()->userInfo;
+
+ // 获取请求参数
+ $param = $this->request->post();
+
+
+ // 根据业务默认值补全参数
+ if (
+ isset($param['type']) &&
+ intval($param['type']) === self::TYPE_GROUP_PUSH
+ ) {
+ if (empty($param['startTime'])) {
+ $param['startTime'] = '09:00';
+ }
+ if (empty($param['endTime'])) {
+ $param['endTime'] = '21:00';
+ }
+ }
+
+ // 验证数据
+ $validate = new WorkbenchValidate;
+ if (!$validate->scene('create')->check($param)) {
+ return json(['code' => 400, 'msg' => $validate->getError()]);
+ }
+
+ Db::startTrans();
+ try {
+ // 创建工作台基本信息
+ $workbench = new Workbench;
+ $workbench->name = $param['name'];
+ $workbench->type = $param['type'];
+ $workbench->status = !empty($param['status']) ? 1 : 0;
+ $workbench->autoStart = !empty($param['autoStart']) ? 1 : 0;
+ $workbench->userId = $userInfo['id'];
+ $workbench->companyId = $userInfo['companyId'];
+ $workbench->createTime = time();
+ $workbench->updateTime = time();
+ $workbench->save();
+
+ // 根据类型创建对应的配置
+ switch ($param['type']) {
+ case self::TYPE_AUTO_LIKE: // 自动点赞
+ $config = new WorkbenchAutoLike;
+ $config->workbenchId = $workbench->id;
+ $config->interval = $param['interval'];
+ $config->maxLikes = $param['maxLikes'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->contentTypes = json_encode($param['contentTypes']);
+ $config->devices = json_encode($param['deviceGroups']);
+ $config->friends = json_encode($param['wechatFriends']);
+ // $config->targetGroups = json_encode($param['targetGroups']);
+ // $config->tagOperator = $param['tagOperator'];
+ $config->friendMaxLikes = $param['friendMaxLikes'];
+ $config->friendTags = $param['friendTags'];
+ $config->enableFriendTags = $param['enableFriendTags'];
+ $config->createTime = time();
+ $config->updateTime = time();
+ $config->save();
+ break;
+ case self::TYPE_MOMENTS_SYNC: // 朋友圈同步
+ $config = new WorkbenchMomentsSync;
+ $config->workbenchId = $workbench->id;
+ $config->syncInterval = $param['syncInterval'];
+ $config->syncCount = $param['syncCount'];
+ $config->syncType = $param['syncType'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->accountType = $param['accountType'];
+ $config->devices = json_encode($param['deviceGroups']);
+ $config->contentLibraries = json_encode($param['contentGroups'] ?? []);
+ $config->createTime = time();
+ $config->updateTime = time();
+ $config->save();
+ break;
+ case self::TYPE_GROUP_PUSH: // 群消息推送
+ $ownerWechatIds = $this->normalizeOwnerWechatIds($param['ownerWechatIds'] ?? []);
+ $groupPushData = $this->prepareGroupPushData($param, $ownerWechatIds);
+ $groupPushData['workbenchId'] = $workbench->id;
+ $groupPushData['createTime'] = time();
+ $groupPushData['updateTime'] = time();
+ $config = new WorkbenchGroupPush;
+ $config->save($groupPushData);
+ break;
+ case self::TYPE_GROUP_CREATE: // 自动建群
+ $config = new WorkbenchGroupCreate;
+ $config->workbenchId = $workbench->id;
+ $config->planType = !empty($param['planType']) ? $param['planType'] : 0;
+ $config->executorId = !empty($param['executorId']) ? $param['executorId'] : 0;
+
+ $config->devices = json_encode($param['deviceGroups'] ?? [], JSON_UNESCAPED_UNICODE);
+ $config->startTime = $param['startTime'] ?? '';
+ $config->endTime = $param['endTime'] ?? '';
+ $config->groupSizeMin = intval($param['groupSizeMin'] ?? 3);
+ $config->groupSizeMax = intval($param['groupSizeMax'] ?? 38);
+ $config->maxGroupsPerDay = intval($param['maxGroupsPerDay'] ?? 20);
+ $config->groupNameTemplate = $param['groupNameTemplate'] ?? '';
+ $config->groupDescription = $param['groupDescription'] ?? '';
+ $config->poolGroups = json_encode($param['poolGroups'] ?? [], JSON_UNESCAPED_UNICODE);
+ $config->wechatGroups = json_encode($param['wechatGroups'] ?? [], JSON_UNESCAPED_UNICODE);
+
+ // 处理群管理员:如果启用了群管理员且有指定管理员,则保存到admins字段
+ $admins = [];
+ if (!empty($param['groupAdminEnabled']) && !empty($param['groupAdminWechatId'])) {
+ // 如果groupAdminWechatId是数组,取第一个;如果是单个值,直接使用
+ $adminWechatId = is_array($param['groupAdminWechatId']) ? $param['groupAdminWechatId'][0] : $param['groupAdminWechatId'];
+ // 如果是好友ID,直接添加到admins;如果是wechatId,需要转换为好友ID
+ if (is_numeric($adminWechatId)) {
+ $admins[] = intval($adminWechatId);
+ } else {
+ // 如果是wechatId字符串,需要查询对应的好友ID
+ $friend = Db::table('s2_wechat_friend')->where('wechatId', $adminWechatId)->find();
+ if ($friend) {
+ $admins[] = intval($friend['id']);
+ }
+ }
+ }
+ // 如果传入了admins参数,优先使用(兼容旧逻辑)
+ if (!empty($param['admins']) && is_array($param['admins'])) {
+ $admins = array_merge($admins, $param['admins']);
+ }
+ $config->admins = json_encode(array_unique($admins), JSON_UNESCAPED_UNICODE);
+
+ $config->fixedWechatIds = json_encode($param['fixedWechatIds'] ?? [], JSON_UNESCAPED_UNICODE);
+ $config->createTime = time();
+ $config->updateTime = time();
+ $config->save();
+ break;
+ case self::TYPE_TRAFFIC_DISTRIBUTION: // 流量分发
+ $config = new WorkbenchTrafficConfig;
+ $config->workbenchId = $workbench->id;
+ $config->distributeType = $param['distributeType'];
+ $config->maxPerDay = $param['maxPerDay'];
+ $config->timeType = $param['timeType'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->devices = json_encode($param['deviceGroups'], JSON_UNESCAPED_UNICODE);
+ $config->pools = json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE);
+ $config->account = json_encode($param['accountGroups'], JSON_UNESCAPED_UNICODE);
+ $config->createTime = time();
+ $config->updateTime = time();
+ $config->save();
+ break;
+ case self::TYPE_IMPORT_CONTACT: //联系人导入
+ $config = new WorkbenchImportContact;
+ $config->workbenchId = $workbench->id;
+ $config->devices = json_encode($param['deviceGroups'], JSON_UNESCAPED_UNICODE);
+ $config->pools = json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE);
+ $config->num = $param['num'];
+ $config->clearContact = $param['clearContact'];
+ $config->remark = $param['remark'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->createTime = time();
+ $config->save();
+ break;
+ }
+
+ Db::commit();
+ return json(['code' => 200, 'msg' => '创建成功', 'data' => ['id' => $workbench->id]]);
+ } catch (\Exception $e) {
+ Db::rollback();
+ return json(['code' => 500, 'msg' => '创建失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 获取工作台列表
+ * @return \think\response\Json
+ */
+ public function getList()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $type = $this->request->param('type', '');
+ $keyword = $this->request->param('keyword', '');
+
+ $where = [
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['isDel', '=', 0]
+ ];
+
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+
+
+ // 添加类型筛选
+ if ($type !== '') {
+ $where[] = ['type', '=', $type];
+ }
+
+ // 添加名称模糊搜索
+ if ($keyword !== '') {
+ $where[] = ['name', 'like', '%' . $keyword . '%'];
+ }
+
+ // 定义关联关系
+ $with = [
+ 'autoLike' => function ($query) {
+ $query->field('workbenchId,interval,maxLikes,startTime,endTime,contentTypes,devices,friends');
+ },
+ 'momentsSync' => function ($query) {
+ $query->field('workbenchId,syncInterval,syncCount,syncType,startTime,endTime,accountType,devices,contentLibraries');
+ },
+ 'trafficConfig' => function ($query) {
+ $query->field('workbenchId,distributeType,maxPerDay,timeType,startTime,endTime,devices,pools,account');
+ },
+ 'groupPush' => function ($query) {
+ $query->field('workbenchId,pushType,targetType,groupPushSubType,startTime,endTime,maxPerDay,pushOrder,isLoop,status,groups,friends,ownerWechatIds,trafficPools,contentLibraries,friendIntervalMin,friendIntervalMax,messageIntervalMin,messageIntervalMax,isRandomTemplate,postPushTags,announcementContent,enableAiRewrite,aiRewritePrompt');
+ },
+ 'groupCreate' => function ($query) {
+ $query->field('workbenchId,devices,startTime,endTime,groupSizeMin,groupSizeMax,maxGroupsPerDay,groupNameTemplate,groupDescription,poolGroups,wechatGroups,admins');
+ },
+ 'importContact' => function ($query) {
+ $query->field('workbenchId,devices,pools,num,remarkType,remark,clearContact,startTime,endTime');
+ },
+ 'user' => function ($query) {
+ $query->field('id,username');
+ }
+ ];
+
+ $list = Workbench::where($where)
+ ->with($with)
+ ->field('id,companyId,name,type,status,autoStart,userId,createTime,updateTime')
+ ->order('id', 'desc')
+ ->page($page, $limit)
+ ->select()
+ ->each(function ($item) {
+ // 处理配置信息
+ switch ($item->type) {
+ case self::TYPE_AUTO_LIKE:
+ if (!empty($item->autoLike)) {
+ $item->config = $item->autoLike;
+ $item->config->devices = json_decode($item->config->devices, true);
+ $item->config->contentTypes = json_decode($item->config->contentTypes, true);
+ $item->config->friends = json_decode($item->config->friends, true);
+
+ // 添加今日点赞数
+ $startTime = strtotime(date('Y-m-d') . ' 00:00:00');
+ $endTime = strtotime(date('Y-m-d') . ' 23:59:59');
+ $todayLikeCount = Db::name('workbench_auto_like_item')
+ ->where('workbenchId', $item->id)
+ ->whereTime('createTime', 'between', [$startTime, $endTime])
+ ->count();
+
+ // 添加总点赞数
+ $totalLikeCount = Db::name('workbench_auto_like_item')
+ ->where('workbenchId', $item->id)
+ ->count();
+
+ $item->config->todayLikeCount = $todayLikeCount;
+ $item->config->totalLikeCount = $totalLikeCount;
+ }
+ unset($item->autoLike, $item->auto_like);
+ break;
+ case self::TYPE_MOMENTS_SYNC:
+ if (!empty($item->momentsSync)) {
+ $item->config = $item->momentsSync;
+ $item->config->devices = json_decode($item->config->devices, true);
+ $item->config->contentGroups = json_decode($item->config->contentLibraries, true);
+ //同步记录
+ $sendNum = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $item->id])->count();
+ $item->syncCount = $sendNum;
+ $lastTime = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $item->id])->order('id DESC')->value('createTime');
+ $item->lastSyncTime = !empty($lastTime) ? date('Y-m-d H:i', $lastTime) : '--';
+
+
+ // 获取内容库名称
+ if (!empty($item->config->contentGroups)) {
+ $libraryNames = ContentLibrary::where('id', 'in', $item->config->contentGroups)->select();
+ $item->config->contentGroupsOptions = $libraryNames;
+ } else {
+ $item->config->contentGroupsOptions = [];
+ }
+ }
+ unset($item->momentsSync, $item->moments_sync, $item->config->contentLibraries);
+ break;
+ case self::TYPE_GROUP_PUSH:
+ if (!empty($item->groupPush)) {
+ $item->config = $item->groupPush;
+ $item->config->pushType = $item->config->pushType;
+ $item->config->targetType = isset($item->config->targetType) ? intval($item->config->targetType) : 1; // 默认1=群推送
+ $item->config->groupPushSubType = isset($item->config->groupPushSubType) ? intval($item->config->groupPushSubType) : 1; // 默认1=群群发
+ $item->config->startTime = $item->config->startTime;
+ $item->config->endTime = $item->config->endTime;
+ $item->config->maxPerDay = $item->config->maxPerDay;
+ $item->config->pushOrder = $item->config->pushOrder;
+ $item->config->isLoop = $item->config->isLoop;
+ $item->config->status = $item->config->status;
+ $item->config->ownerWechatIds = json_decode($item->config->ownerWechatIds ?? '[]', true) ?: [];
+ // 根据targetType解析不同的数据
+ if ($item->config->targetType == 1) {
+ // 群推送
+ $item->config->wechatGroups = json_decode($item->config->groups, true) ?: [];
+ $item->config->wechatFriends = [];
+ // 群推送不需要devices字段
+ // 群公告相关字段
+ if ($item->config->groupPushSubType == 2) {
+ $item->config->announcementContent = isset($item->config->announcementContent) ? $item->config->announcementContent : '';
+ $item->config->enableAiRewrite = isset($item->config->enableAiRewrite) ? intval($item->config->enableAiRewrite) : 0;
+ $item->config->aiRewritePrompt = isset($item->config->aiRewritePrompt) ? $item->config->aiRewritePrompt : '';
+ }
+ $item->config->trafficPools = [];
+ } else {
+ // 好友推送
+ $item->config->wechatFriends = json_decode($item->config->friends, true) ?: [];
+ $item->config->wechatGroups = [];
+ $item->config->trafficPools = json_decode($item->config->trafficPools ?? '[]', true) ?: [];
+ }
+ $item->config->contentLibraries = json_decode($item->config->contentLibraries, true);
+ $item->config->postPushTags = json_decode($item->config->postPushTags ?? '[]', true) ?: [];
+ $item->config->lastPushTime = '';
+ if (!empty($item->config->ownerWechatIds)) {
+ $ownerWechatOptions = Db::name('wechat_account')
+ ->whereIn('id', $item->config->ownerWechatIds)
+ ->field('id,wechatId,nickName,avatar,alias')
+ ->select();
+ $item->config->ownerWechatOptions = $ownerWechatOptions;
+ } else {
+ $item->config->ownerWechatOptions = [];
+ }
+ }
+ unset($item->groupPush, $item->group_push);
+ break;
+ case self::TYPE_GROUP_CREATE:
+ if (!empty($item->groupCreate)) {
+ $item->config = $item->groupCreate;
+ $item->config->devices = json_decode($item->config->devices, true);
+ $item->config->poolGroups = json_decode($item->config->poolGroups, true);
+ $item->config->wechatGroups = json_decode($item->config->wechatGroups, true);
+ $item->config->admins = json_decode($item->config->admins ?? '[]', true) ?: [];
+
+ // 处理群管理员相关字段
+ $item->config->groupAdminEnabled = !empty($item->config->admins) ? 1 : 0;
+
+ if (!empty($item->config->admins)) {
+ $adminOptions = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->where('wf.id', 'in', $item->config->admins)
+ ->order('wf.id', 'desc')
+ ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar')
+ ->select();
+ $item->config->adminsOptions = $adminOptions;
+ // 如果有管理员,设置groupAdminWechatId为第一个管理员的ID(用于前端回显)
+ $item->config->groupAdminWechatId = !empty($item->config->admins) ? $item->config->admins[0] : null;
+ } else {
+ $item->config->adminsOptions = [];
+ $item->config->groupAdminWechatId = null;
+ }
+ }
+ unset($item->groupCreate, $item->group_create);
+ break;
+ case self::TYPE_TRAFFIC_DISTRIBUTION:
+ if (!empty($item->trafficConfig)) {
+ $item->config = $item->trafficConfig;
+ $item->config->devices = json_decode($item->config->devices, true);
+ $item->config->poolGroups = json_decode($item->config->pools, true);
+ $item->config->account = json_decode($item->config->account, true);
+ $config_item = Db::name('workbench_traffic_config_item')->where(['workbenchId' => $item->id])->order('id DESC')->find();
+ $item->config->lastUpdated = !empty($config_item) ? date('Y-m-d H:i', $config_item['createTime']) : '--';
+
+ //统计
+ $labels = $item->config->poolGroups;
+ $totalUsers = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->where([
+ ['wf.isDeleted', '=', 0],
+ ['sa.departmentId', '=', $item->companyId]
+ ])
+ ->whereIn('wa.currentDeviceId', $item->config->devices);
+
+ if (!empty($labels) && count($labels) > 0) {
+ $totalUsers = $totalUsers->where(function ($q) use ($labels) {
+ foreach ($labels as $label) {
+ $q->whereOrRaw("JSON_CONTAINS(wf.labels, '\"{$label}\"')");
+ }
+ });
+ }
+
+ $totalUsers = $totalUsers->count();
+ $totalAccounts = count($item->config->account);
+ $dailyAverage = Db::name('workbench_traffic_config_item')
+ ->where('workbenchId', $item->id)
+ ->count();
+ $day = (time() - strtotime($item->createTime)) / 86400;
+ $day = intval($day);
+ if ($dailyAverage > 0 && $totalAccounts > 0 && $day > 0) {
+ $dailyAverage = $dailyAverage / $totalAccounts / $day;
+ }
+ $item->config->total = [
+ 'dailyAverage' => intval($dailyAverage),
+ 'totalAccounts' => $totalAccounts,
+ 'deviceCount' => count($item->config->devices),
+ 'poolCount' => !empty($item->config->poolGroups) ? count($item->config->poolGroups) : 'ALL',
+ 'totalUsers' => $totalUsers >> 0
+ ];
+ }
+ unset($item->trafficConfig, $item->traffic_config);
+ break;
+
+ case self::TYPE_IMPORT_CONTACT:
+ if (!empty($item->importContact)) {
+ $item->config = $item->importContact;
+ $item->config->devices = json_decode($item->config->devices, true);
+ $item->config->poolGroups = json_decode($item->config->pools, true);
+ }
+ unset($item->importContact, $item->import_contact);
+ break;
+ }
+ // 添加创建人名称
+ $item['creatorName'] = $item->user ? $item->user->username : '';
+ unset($item['user']); // 移除关联数据
+ return $item;
+ });
+
+ $total = Workbench::where($where)->count();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]);
+ }
+
+ /**
+ * 获取工作台详情
+ * @param int $id 工作台ID
+ * @return \think\response\Json
+ */
+ public function detail()
+ {
+ $id = $this->request->param('id', '');
+
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ // 定义关联关系
+ $with = [
+ 'autoLike' => function ($query) {
+ $query->field('workbenchId,interval,maxLikes,startTime,endTime,contentTypes,devices,friends,friendMaxLikes,friendTags,enableFriendTags');
+ },
+ 'momentsSync' => function ($query) {
+ $query->field('workbenchId,syncInterval,syncCount,syncType,startTime,endTime,accountType,devices,contentLibraries');
+ },
+ 'trafficConfig' => function ($query) {
+ $query->field('workbenchId,distributeType,maxPerDay,timeType,startTime,endTime,devices,pools,account');
+ },
+ 'groupPush' => function ($query) {
+ $query->field('workbenchId,pushType,targetType,groupPushSubType,startTime,endTime,maxPerDay,pushOrder,isLoop,status,groups,friends,ownerWechatIds,trafficPools,contentLibraries,friendIntervalMin,friendIntervalMax,messageIntervalMin,messageIntervalMax,isRandomTemplate,postPushTags,announcementContent,enableAiRewrite,aiRewritePrompt');
+ },
+ 'groupCreate' => function ($query) {
+ $query->field('workbenchId,devices,startTime,endTime,groupSizeMin,groupSizeMax,maxGroupsPerDay,groupNameTemplate,groupDescription,poolGroups,wechatGroups,admins');
+ },
+ 'importContact' => function ($query) {
+ $query->field('workbenchId,devices,pools,num,remarkType,remark,clearContact,startTime,endTime');
+ },
+ ];
+
+ $where = [
+ ['id', '=', $id],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+
+ $workbench = Workbench::where($where)
+ ->field('id,name,type,status,autoStart,createTime,updateTime,companyId')
+ ->with($with)
+ ->find();
+
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在']);
+ }
+
+ // 处理配置信息
+ switch ($workbench->type) {
+ //自动点赞
+ case self::TYPE_AUTO_LIKE:
+ if (!empty($workbench->autoLike)) {
+ $workbench->config = $workbench->autoLike;
+ $workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
+ $workbench->config->wechatFriends = json_decode($workbench->config->friends, true);
+ $workbench->config->targetType = 2;
+ //$workbench->config->targetGroups = json_decode($workbench->config->targetGroups, true);
+ $workbench->config->contentTypes = json_decode($workbench->config->contentTypes, true);
+
+ // 添加今日点赞数
+ $startTime = strtotime(date('Y-m-d') . ' 00:00:00');
+ $endTime = strtotime(date('Y-m-d') . ' 23:59:59');
+ $todayLikeCount = Db::name('workbench_auto_like_item')
+ ->where('workbenchId', $workbench->id)
+ ->whereTime('createTime', 'between', [$startTime, $endTime])
+ ->count();
+
+ // 添加总点赞数
+ $totalLikeCount = Db::name('workbench_auto_like_item')
+ ->where('workbenchId', $workbench->id)
+ ->count();
+
+ $workbench->config->todayLikeCount = $todayLikeCount;
+ $workbench->config->totalLikeCount = $totalLikeCount;
+
+ unset($workbench->autoLike, $workbench->auto_like);
+ }
+ break;
+ //自动同步朋友圈
+ case self::TYPE_MOMENTS_SYNC:
+ if (!empty($workbench->momentsSync)) {
+ $workbench->config = $workbench->momentsSync;
+ $workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
+ $workbench->config->contentGroups = json_decode($workbench->config->contentLibraries, true);
+
+ //同步记录
+ $sendNum = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $workbench->id])->count();
+ $workbench->syncCount = $sendNum;
+ $lastTime = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $workbench->id])->order('id DESC')->value('createTime');
+ $workbench->lastSyncTime = !empty($lastTime) ? date('Y-m-d H:i', $lastTime) : '--';
+ unset($workbench->momentsSync, $workbench->moments_sync);
+ }
+ break;
+ //群推送
+ case self::TYPE_GROUP_PUSH:
+ if (!empty($workbench->groupPush)) {
+ $workbench->config = $workbench->groupPush;
+ $workbench->config->targetType = isset($workbench->config->targetType) ? intval($workbench->config->targetType) : 1; // 默认1=群推送
+ $workbench->config->groupPushSubType = isset($workbench->config->groupPushSubType) ? intval($workbench->config->groupPushSubType) : 1; // 默认1=群群发
+ $workbench->config->ownerWechatIds = json_decode($workbench->config->ownerWechatIds ?? '[]', true) ?: [];
+ // 根据targetType解析不同的数据
+ if ($workbench->config->targetType == 1) {
+ // 群推送
+ $workbench->config->wechatGroups = json_decode($workbench->config->groups, true) ?: [];
+ $workbench->config->wechatFriends = [];
+ $workbench->config->trafficPools = [];
+ // 群推送不需要devices字段
+ // 群公告相关字段
+ if ($workbench->config->groupPushSubType == 2) {
+ $workbench->config->announcementContent = isset($workbench->config->announcementContent) ? $workbench->config->announcementContent : '';
+ $workbench->config->enableAiRewrite = isset($workbench->config->enableAiRewrite) ? intval($workbench->config->enableAiRewrite) : 0;
+ $workbench->config->aiRewritePrompt = isset($workbench->config->aiRewritePrompt) ? $workbench->config->aiRewritePrompt : '';
+ }
+ } else {
+ // 好友推送
+ $workbench->config->wechatFriends = json_decode($workbench->config->friends, true) ?: [];
+ $workbench->config->wechatGroups = [];
+ $workbench->config->trafficPools = json_decode($workbench->config->trafficPools ?? '[]', true) ?: [];
+ }
+ $workbench->config->contentLibraries = json_decode($workbench->config->contentLibraries, true);
+ $workbench->config->postPushTags = json_decode($workbench->config->postPushTags ?? '[]', true) ?: [];
+ unset($workbench->groupPush, $workbench->group_push);
+ }
+ break;
+ //建群助手
+ case self::TYPE_GROUP_CREATE:
+ if (!empty($workbench->groupCreate)) {
+ $workbench->config = $workbench->groupCreate;
+ $workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
+ $workbench->config->poolGroups = json_decode($workbench->config->poolGroups, true);
+ $workbench->config->wechatGroups = json_decode($workbench->config->wechatGroups, true);
+ $workbench->config->admins = json_decode($workbench->config->admins ?? '[]', true) ?: [];
+
+ // 处理群管理员相关字段
+ $workbench->config->groupAdminEnabled = !empty($workbench->config->admins) ? 1 : 0;
+
+ // 如果有管理员,设置groupAdminWechatId为第一个管理员的ID(用于前端回显)
+ $workbench->config->groupAdminWechatId = !empty($workbench->config->admins) ? $workbench->config->admins[0] : null;
+
+ // 统计已建群数(状态为成功且groupId不为空的记录,按groupId分组去重)
+ $createdGroupsCount = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbench->id)
+ ->where('status', 2) // STATUS_SUCCESS = 2
+ ->where('groupId', '<>', null)
+ ->group('groupId')
+ ->count();
+
+ // 统计总人数(该工作台的所有记录数)
+ $totalMembersCount = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbench->id)
+ ->count();
+
+ // 添加统计信息
+ $workbench->config->stats = [
+ 'createdGroupsCount' => $createdGroupsCount,
+ 'totalMembersCount' => $totalMembersCount
+ ];
+
+ unset($workbench->groupCreate, $workbench->group_create);
+ }
+ break;
+ //流量分发
+ case self::TYPE_TRAFFIC_DISTRIBUTION:
+ if (!empty($workbench->trafficConfig)) {
+ $workbench->config = $workbench->trafficConfig;
+ $workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
+ $workbench->config->accountGroups = json_decode($workbench->config->account, true);
+ $workbench->config->poolGroups = json_decode($workbench->config->pools, true);
+ $config_item = Db::name('workbench_traffic_config_item')->where(['workbenchId' => $workbench->id])->order('id DESC')->find();
+ $workbench->config->lastUpdated = !empty($config_item) ? date('Y-m-d H:i', $config_item['createTime']) : '--';
+
+ //统计
+ $labels = $workbench->config->poolGroups;
+ $totalUsers = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->where([
+ ['wf.isDeleted', '=', 0],
+ ['sa.departmentId', '=', $workbench->companyId]
+ ])
+ ->whereIn('wa.currentDeviceId', $workbench->config->deviceGroups)
+ ->field('wf.id,wf.wechatAccountId,wf.wechatId,wf.labels,sa.userName,wa.currentDeviceId as deviceId')
+ ->where(function ($q) use ($labels) {
+ foreach ($labels as $label) {
+ $q->whereOrRaw("JSON_CONTAINS(wf.labels, '\"{$label}\"')");
+ }
+ })->count();
+
+ $totalAccounts = Db::table('s2_company_account')
+ ->alias('a')
+ ->where(['a.departmentId' => $workbench->companyId, 'a.status' => 0])
+ ->whereNotLike('a.userName', '%_offline%')
+ ->whereNotLike('a.userName', '%_delete%')
+ ->group('a.id')
+ ->count();
+
+ $dailyAverage = Db::name('workbench_traffic_config_item')
+ ->where('workbenchId', $workbench->id)
+ ->count();
+ $day = (time() - strtotime($workbench->createTime)) / 86400;
+ $day = intval($day);
+
+
+ if ($dailyAverage > 0) {
+ $dailyAverage = $dailyAverage / $totalAccounts / $day;
+ }
+
+ $workbench->config->total = [
+ 'dailyAverage' => intval($dailyAverage),
+ 'totalAccounts' => $totalAccounts,
+ 'deviceCount' => count($workbench->config->deviceGroups),
+ 'poolCount' => count($workbench->config->poolGroups),
+ 'totalUsers' => $totalUsers >> 0
+ ];
+ unset($workbench->trafficConfig, $workbench->traffic_config);
+ }
+ break;
+ case self::TYPE_IMPORT_CONTACT:
+ if (!empty($workbench->importContact)) {
+ $workbench->config = $workbench->importContact;
+ $workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
+ $workbench->config->poolGroups = json_decode($workbench->config->pools, true);
+ }
+ unset($workbench->importContact, $workbench->import_contact);
+ break;
+ }
+ unset(
+ $workbench->autoLike,
+ $workbench->momentsSync,
+ $workbench->groupPush,
+ $workbench->groupCreate,
+ $workbench->config->devices,
+ $workbench->config->friends,
+ $workbench->config->groups,
+ $workbench->config->contentLibraries,
+ $workbench->config->account,
+ );
+
+
+ //获取设备信息
+ if (!empty($workbench->config->deviceGroups)) {
+ $deviceList = DeviceModel::alias('d')
+ ->field([
+ 'd.id', 'd.imei', 'd.memo', 'd.alive',
+ 'l.wechatId',
+ 'a.nickname', 'a.alias', 'a.avatar', 'a.alias', '0 totalFriend'
+ ])
+ ->leftJoin('device_wechat_login l', 'd.id = l.deviceId and l.alive =' . DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE . ' and l.companyId = d.companyId')
+ ->leftJoin('wechat_account a', 'l.wechatId = a.wechatId')
+ ->whereIn('d.id', $workbench->config->deviceGroups)
+ ->order('d.id desc')
+ ->select();
+
+ foreach ($deviceList as &$device) {
+ $curstomer = WechatCustomerModel::field('friendShip')->where(['wechatId' => $device['wechatId']])->find();
+ $device['totalFriend'] = $curstomer->friendShip->totalFriend ?? 0;
+ }
+ unset($device);
+
+ $workbench->config->deviceGroupsOptions = $deviceList;
+ } else {
+ $workbench->config->deviceGroupsOptions = [];
+ }
+
+
+ // 获取群(当targetType=1时)
+ if (!empty($workbench->config->wechatGroups) && isset($workbench->config->targetType) && $workbench->config->targetType == 1) {
+ $groupList = Db::name('wechat_group')->alias('wg')
+ ->join('wechat_account wa', 'wa.wechatId = wg.ownerWechatId')
+ ->where('wg.id', 'in', $workbench->config->wechatGroups)
+ ->order('wg.id', 'desc')
+ ->field('wg.id,wg.name as groupName,wg.ownerWechatId,wa.nickName,wa.avatar,wa.alias,wg.avatar as groupAvatar')
+ ->select();
+ $workbench->config->wechatGroupsOptions = $groupList;
+ } else {
+ $workbench->config->wechatGroupsOptions = [];
+ }
+
+ // 获取好友(当targetType=2时)
+ if (!empty($workbench->config->wechatFriends) && isset($workbench->config->targetType) && $workbench->config->targetType == 2) {
+ $friendList = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->where('wf.id', 'in', $workbench->config->wechatFriends)
+ ->order('wf.id', 'desc')
+ ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar')
+ ->select();
+ $workbench->config->wechatFriendsOptions = $friendList;
+ } else {
+ $workbench->config->wechatFriendsOptions = [];
+ }
+
+ // 获取流量池(当targetType=2时)
+ if (!empty($workbench->config->trafficPools) && isset($workbench->config->targetType) && $workbench->config->targetType == 2) {
+ $poolList = Db::name('traffic_source_package')->alias('tsp')
+ ->leftJoin('traffic_source_package_item tspi', 'tspi.packageId = tsp.id and tspi.isDel = 0')
+ ->whereIn('tsp.id', $workbench->config->trafficPools)
+ ->where('tsp.isDel', 0)
+ ->whereIn('tsp.companyId', [$this->request->userInfo['companyId'], 0])
+ ->field('tsp.id,tsp.name,tsp.description,tsp.pic,COUNT(tspi.id) as itemCount')
+ ->group('tsp.id')
+ ->order('tsp.id', 'desc')
+ ->select();
+ $workbench->config->trafficPoolsOptions = $poolList;
+ } else {
+ $workbench->config->trafficPoolsOptions = [];
+ }
+
+ // 获取内容库名称
+ if (!empty($workbench->config->contentGroups)) {
+ $libraryNames = ContentLibrary::where('id', 'in', $workbench->config->contentGroups)->select();
+ $workbench->config->contentGroupsOptions = $libraryNames;
+ } else {
+ $workbench->config->contentGroupsOptions = [];
+ }
+
+ //账号
+ if (!empty($workbench->config->accountGroups)) {
+ $account = Db::table('s2_company_account')->alias('a')
+ ->where(['a.departmentId' => $this->request->userInfo['companyId'], 'a.status' => 0])
+ ->whereIn('a.id', $workbench->config->accountGroups)
+ ->whereNotLike('a.userName', '%_offline%')
+ ->whereNotLike('a.userName', '%_delete%')
+ ->field('a.id,a.userName,a.realName,a.nickname,a.memo')
+ ->select();
+ $workbench->config->accountGroupsOptions = $account;
+ } else {
+ $workbench->config->accountGroupsOptions = [];
+ }
+
+ if (!empty($workbench->config->poolGroups)) {
+ $poolGroupsOptions = Db::name('traffic_source_package')->alias('tsp')
+ ->join('traffic_source_package_item tspi', 'tspi.packageId=tsp.id', 'left')
+ ->whereIn('tsp.companyId', [$this->request->userInfo['companyId'], 0])
+ ->whereIn('tsp.id', $workbench->config->poolGroups)
+ ->field('tsp.id,tsp.name,tsp.description,tsp.createTime,count(tspi.id) as num')
+ ->group('tsp.id')
+ ->select();
+ $workbench->config->poolGroupsOptions = $poolGroupsOptions;
+ } else {
+ $workbench->config->poolGroupsOptions = [];
+ }
+
+ if (!empty($workbench->config->ownerWechatIds)) {
+ $ownerWechatOptions = Db::name('wechat_account')
+ ->whereIn('id', $workbench->config->ownerWechatIds)
+ ->field('id,wechatId,nickName,avatar,alias')
+ ->select();
+ $workbench->config->ownerWechatOptions = $ownerWechatOptions;
+ } else {
+ $workbench->config->ownerWechatOptions = [];
+ }
+
+ // 获取群组选项(自动建群)
+ if ($workbench->type == self::TYPE_GROUP_CREATE && !empty($workbench->config->wechatGroups)) {
+ // 分离数字ID(好友ID)和字符串ID(手动创建的群组)
+ $friendIds = [];
+ $manualGroupIds = [];
+
+ foreach ($workbench->config->wechatGroups as $groupId) {
+ if (is_numeric($groupId)) {
+ $friendIds[] = intval($groupId);
+ } else {
+ $manualGroupIds[] = $groupId;
+ }
+ }
+
+ $wechatGroupsOptions = [];
+
+ // 查询好友信息(数字ID)
+ if (!empty($friendIds)) {
+ $friendList = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->join(['s2_company_account' => 'ca'], 'ca.id = wf.accountId', 'left')
+ ->where('wf.id', 'in', $friendIds)
+ ->order('wf.id', 'desc')
+ ->field('wf.id,wf.wechatId,wf.nickname,wf.avatar,wf.alias,wf.gender,wf.phone,wa.nickName as accountNickname,ca.userName as account,ca.realName as username,wf.createTime,wf.updateTime,wf.deleteTime,wf.ownerWechatId')
+ ->select();
+
+ // 获取群主信息
+ foreach ($friendList as &$friend) {
+ if (!empty($friend['ownerWechatId'])) {
+ $owner = Db::name('wechat_account')
+ ->where('wechatId', $friend['ownerWechatId'])
+ ->field('nickName,alias')
+ ->find();
+ $friend['ownerNickname'] = $owner['nickName'] ?? '';
+ $friend['ownerAlias'] = $owner['alias'] ?? '';
+ } else {
+ $friend['ownerNickname'] = '';
+ $friend['ownerAlias'] = '';
+ }
+ $friend['isManual'] = '';
+ // 格式化时间
+ $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['deleteTime'] = !empty($friend['deleteTime']) ? date('Y-m-d H:i:s', $friend['deleteTime']) : '';
+ }
+ unset($friend);
+
+ $wechatGroupsOptions = array_merge($wechatGroupsOptions, $friendList);
+ }
+
+ // 处理手动创建的群组(字符串ID)
+ if (!empty($manualGroupIds)) {
+ foreach ($manualGroupIds as $groupId) {
+ // 手动创建的群组,只返回基本信息
+ $wechatGroupsOptions[] = [
+ 'id' => $groupId,
+ 'wechatId' => $groupId,
+ 'nickname' => $groupId,
+ 'avatar' => '',
+ 'isManual' => 1
+ ];
+ }
+ }
+
+ $workbench->config->wechatGroupsOptions = $wechatGroupsOptions;
+ } else {
+ $workbench->config->wechatGroupsOptions = [];
+ }
+
+ // 获取管理员选项(自动建群)
+ if ($workbench->type == self::TYPE_GROUP_CREATE && !empty($workbench->config->admins)) {
+ $adminOptions = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->where('wf.id', 'in', $workbench->config->admins)
+ ->order('wf.id', 'desc')
+ ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar')
+ ->select();
+ $workbench->config->adminsOptions = $adminOptions;
+ } else {
+ $workbench->config->adminsOptions = [];
+ }
+
+ return json(['code' => 200, 'msg' => '获取成功', 'data' => $workbench]);
+ }
+
+ /**
+ * 更新工作台
+ * @return \think\response\Json
+ */
+ public function update()
+ {
+ if (!$this->request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ // 获取请求参数
+ $param = $this->request->post();
+
+ // 验证数据
+ $validate = new WorkbenchValidate;
+ if (!$validate->scene('update')->check($param)) {
+ return json(['code' => 400, 'msg' => $validate->getError()]);
+ }
+
+
+ $where = [
+ ['id', '=', $param['id']],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+ // 查询工作台是否存在
+ $workbench = Workbench::where($where)->find();
+ if (!$workbench) {
+ return json(['code' => 404, 'msg' => '工作台不存在']);
+ }
+
+ Db::startTrans();
+ try {
+ // 更新工作台基本信息
+ $workbench->name = $param['name'];
+ $workbench->status = !empty($param['status']) ? 1 : 0;
+ $workbench->autoStart = !empty($param['autoStart']) ? 1 : 0;
+ $workbench->updateTime = time();
+ $workbench->save();
+
+ // 根据类型更新对应的配置
+ switch ($workbench->type) {
+ case self::TYPE_AUTO_LIKE:
+ $config = WorkbenchAutoLike::where('workbenchId', $param['id'])->find();
+ if ($config) {
+ $config->interval = $param['interval'];
+ $config->maxLikes = $param['maxLikes'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->contentTypes = json_encode($param['contentTypes']);
+ $config->devices = json_encode($param['deviceGroups']);
+ $config->friends = json_encode($param['wechatFriends']);
+ // $config->targetGroups = json_encode($param['targetGroups']);
+ // $config->tagOperator = $param['tagOperator'];
+ $config->friendMaxLikes = $param['friendMaxLikes'];
+ $config->friendTags = $param['friendTags'];
+ $config->enableFriendTags = $param['enableFriendTags'];
+ $config->updateTime = time();
+ $config->save();
+ }
+ break;
+
+ case self::TYPE_MOMENTS_SYNC:
+ $config = WorkbenchMomentsSync::where('workbenchId', $param['id'])->find();
+ if ($config) {
+ if (!empty($param['contentGroups'])) {
+ foreach ($param['contentGroups'] as $library) {
+ if (isset($library['id']) && !empty($library['id'])) {
+ $contentLibraries[] = $library['id'];
+ } else {
+ $contentLibraries[] = $library;
+ }
+ }
+ } else {
+ $contentLibraries = [];
+ }
+
+ $config->syncInterval = $param['syncInterval'];
+ $config->syncCount = $param['syncCount'];
+ $config->syncType = $param['syncType'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->accountType = $param['accountType'];
+ $config->devices = json_encode($param['deviceGroups']);
+ $config->contentLibraries = json_encode($contentLibraries);
+ $config->updateTime = time();
+ $config->save();
+ }
+ break;
+
+ case self::TYPE_GROUP_PUSH:
+ $config = WorkbenchGroupPush::where('workbenchId', $param['id'])->find();
+ if ($config) {
+ $ownerWechatIds = $this->normalizeOwnerWechatIds($param['ownerWechatIds'] ?? null, $config);
+ $groupPushData = $this->prepareGroupPushData($param, $ownerWechatIds, $config);
+ $groupPushData['updateTime'] = time();
+ $config->save($groupPushData);
+ }
+ break;
+
+ case self::TYPE_GROUP_CREATE:
+ $config = WorkbenchGroupCreate::where('workbenchId', $param['id'])->find();
+ if ($config) {
+ $config->devices = json_encode($param['deviceGroups'] ?? [], JSON_UNESCAPED_UNICODE);
+ $config->startTime = $param['startTime'] ?? '';
+ $config->endTime = $param['endTime'] ?? '';
+ $config->groupSizeMin = intval($param['groupSizeMin'] ?? 3);
+ $config->groupSizeMax = intval($param['groupSizeMax'] ?? 38);
+ $config->maxGroupsPerDay = intval($param['maxGroupsPerDay'] ?? 20);
+ $config->groupNameTemplate = $param['groupNameTemplate'] ?? '';
+ $config->groupDescription = $param['groupDescription'] ?? '';
+ $config->poolGroups = json_encode($param['poolGroups'] ?? [], JSON_UNESCAPED_UNICODE);
+ $config->wechatGroups = json_encode($param['wechatGroups'] ?? [], JSON_UNESCAPED_UNICODE);
+
+ // 处理群管理员:如果启用了群管理员且有指定管理员,则保存到admins字段
+ $admins = [];
+ if (!empty($param['groupAdminEnabled']) && !empty($param['groupAdminWechatId'])) {
+ // 如果groupAdminWechatId是数组,取第一个;如果是单个值,直接使用
+ $adminWechatId = is_array($param['groupAdminWechatId']) ? $param['groupAdminWechatId'][0] : $param['groupAdminWechatId'];
+ // 如果是好友ID,直接添加到admins;如果是wechatId,需要转换为好友ID
+ if (is_numeric($adminWechatId)) {
+ $admins[] = intval($adminWechatId);
+ } else {
+ // 如果是wechatId字符串,需要查询对应的好友ID
+ $friend = Db::table('s2_wechat_friend')->where('wechatId', $adminWechatId)->find();
+ if ($friend) {
+ $admins[] = intval($friend['id']);
+ }
+ }
+ }
+ // 如果传入了admins参数,优先使用(兼容旧逻辑)
+ if (!empty($param['admins']) && is_array($param['admins'])) {
+ $admins = array_merge($admins, $param['admins']);
+ }
+ $config->admins = json_encode(array_unique($admins), JSON_UNESCAPED_UNICODE);
+
+ $config->updateTime = time();
+ $config->save();
+ }
+ break;
+ case self::TYPE_TRAFFIC_DISTRIBUTION:
+ $config = WorkbenchTrafficConfig::where('workbenchId', $param['id'])->find();
+ if ($config) {
+ $config->distributeType = $param['distributeType'];
+ $config->maxPerDay = $param['maxPerDay'];
+ $config->timeType = $param['timeType'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->devices = json_encode($param['deviceGroups']);
+ $config->pools = json_encode($param['poolGroups']);
+ $config->account = json_encode($param['accountGroups']);
+ $config->updateTime = time();
+ $config->save();
+ }
+ break;
+ case self::TYPE_IMPORT_CONTACT: //联系人导入
+ $config = WorkbenchImportContact::where('workbenchId', $param['id'])->find();;
+ if ($config) {
+ $config->devices = json_encode($param['deviceGroups']);
+ $config->pools = json_encode($param['poolGroups']);
+ $config->num = $param['num'];
+ $config->clearContact = $param['clearContact'];
+ $config->remark = $param['remark'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->save();
+ }
+ break;
+ }
+
+ Db::commit();
+ return json(['code' => 200, 'msg' => '更新成功']);
+ } catch (\Exception $e) {
+ Db::rollback();
+ return json(['code' => 500, 'msg' => '更新失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 更新工作台状态
+ * @return \think\response\Json
+ */
+ public function updateStatus()
+ {
+ if (!$this->request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ $id = $this->request->param('id', '');
+
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+
+
+ $where = [
+ ['id', '=', $id],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+ $workbench = Workbench::where($where)->find();
+
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在']);
+ }
+
+ $workbench->status = !$workbench['status'];
+ $workbench->save();
+
+ return json(['code' => 200, 'msg' => '更新成功']);
+ }
+
+ /**
+ * 删除工作台(软删除)
+ */
+ public function delete()
+ {
+ $id = $this->request->param('id');
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ $where = [
+ ['id', '=', $id],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+ $workbench = Workbench::where($where)->find();
+
+ if (!$workbench) {
+ return json(['code' => 404, 'msg' => '工作台不存在']);
+ }
+
+ // 软删除
+ $workbench->isDel = 1;
+ $workbench->deleteTime = time();
+ $workbench->save();
+
+ return json(['code' => 200, 'msg' => '删除成功']);
+ }
+
+ /**
+ * 拷贝工作台
+ * @return \think\response\Json
+ */
+ public function copy()
+ {
+ if (!$this->request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ $id = $this->request->post('id');
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ // 验证权限并获取原数据
+ $workbench = Workbench::where([
+ ['id', '=', $id],
+ ['userId', '=', $this->request->userInfo['id']]
+ ])->find();
+
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在']);
+ }
+
+ Db::startTrans();
+ try {
+ // 创建新的工作台基本信息
+ $newWorkbench = new Workbench;
+ $newWorkbench->name = $workbench->name . ' copy';
+ $newWorkbench->type = $workbench->type;
+ $newWorkbench->status = 1; // 新拷贝的默认启用
+ $newWorkbench->autoStart = $workbench->autoStart;
+ $newWorkbench->userId = $this->request->userInfo['id'];
+ $newWorkbench->companyId = $this->request->userInfo['companyId'];
+ $newWorkbench->save();
+
+ // 根据类型拷贝对应的配置
+ switch ($workbench->type) {
+ case self::TYPE_AUTO_LIKE:
+ $config = WorkbenchAutoLike::where('workbenchId', $id)->find();
+ if ($config) {
+ $newConfig = new WorkbenchAutoLike;
+ $newConfig->workbenchId = $newWorkbench->id;
+ $newConfig->interval = $config->interval;
+ $newConfig->maxLikes = $config->maxLikes;
+ $newConfig->startTime = $config->startTime;
+ $newConfig->endTime = $config->endTime;
+ $newConfig->contentTypes = $config->contentTypes;
+ $newConfig->devices = $config->devices;
+ $newConfig->friends = $config->friends;
+ $newConfig->createTime = time();
+ $newConfig->updateTime = time();
+ $newConfig->save();
+ }
+ break;
+ case self::TYPE_MOMENTS_SYNC:
+ $config = WorkbenchMomentsSync::where('workbenchId', $id)->find();
+ if ($config) {
+ $newConfig = new WorkbenchMomentsSync;
+ $newConfig->workbenchId = $newWorkbench->id;
+ $newConfig->syncInterval = $config->syncInterval;
+ $newConfig->syncCount = $config->syncCount;
+ $newConfig->syncType = $config->syncType;
+ $newConfig->startTime = $config->startTime;
+ $newConfig->endTime = $config->endTime;
+ $newConfig->accountType = $config->accountType;
+ $newConfig->devices = $config->devices;
+ $newConfig->contentLibraries = $config->contentLibraries;
+ $newConfig->createTime = time();
+ $newConfig->updateTime = time();
+ $newConfig->save();
+ }
+ break;
+ case self::TYPE_GROUP_PUSH:
+ $config = WorkbenchGroupPush::where('workbenchId', $id)->find();
+ if ($config) {
+ $newConfig = new WorkbenchGroupPush;
+ $newConfig->workbenchId = $newWorkbench->id;
+ $newConfig->pushType = $config->pushType;
+ $newConfig->targetType = isset($config->targetType) ? $config->targetType : 1; // 默认1=群推送
+ $newConfig->startTime = $config->startTime;
+ $newConfig->endTime = $config->endTime;
+ $newConfig->maxPerDay = $config->maxPerDay;
+ $newConfig->pushOrder = $config->pushOrder;
+ $newConfig->isLoop = $config->isLoop;
+ $newConfig->status = $config->status;
+ $newConfig->groups = $config->groups;
+ $newConfig->friends = $config->friends;
+ $newConfig->contentLibraries = $config->contentLibraries;
+ $newConfig->trafficPools = property_exists($config, 'trafficPools') ? $config->trafficPools : json_encode([], JSON_UNESCAPED_UNICODE);
+ $newConfig->socialMediaId = $config->socialMediaId;
+ $newConfig->promotionSiteId = $config->promotionSiteId;
+ $newConfig->ownerWechatIds = $config->ownerWechatIds;
+ $newConfig->createTime = time();
+ $newConfig->updateTime = time();
+ $newConfig->save();
+ }
+ break;
+ case self::TYPE_GROUP_CREATE:
+ $config = WorkbenchGroupCreate::where('workbenchId', $id)->find();
+ if ($config) {
+ $newConfig = new WorkbenchGroupCreate;
+ $newConfig->workbenchId = $newWorkbench->id;
+ $newConfig->devices = $config->devices;
+ $newConfig->startTime = $config->startTime;
+ $newConfig->endTime = $config->endTime;
+ $newConfig->groupSizeMin = $config->groupSizeMin;
+ $newConfig->groupSizeMax = $config->groupSizeMax;
+ $newConfig->maxGroupsPerDay = $config->maxGroupsPerDay;
+ $newConfig->groupNameTemplate = $config->groupNameTemplate;
+ $newConfig->groupDescription = $config->groupDescription;
+ $newConfig->poolGroups = $config->poolGroups;
+ $newConfig->wechatGroups = $config->wechatGroups;
+ $newConfig->admins = $config->admins ?? json_encode([], JSON_UNESCAPED_UNICODE);
+ $newConfig->createTime = time();
+ $newConfig->updateTime = time();
+ $newConfig->save();
+ }
+ break;
+ case self::TYPE_IMPORT_CONTACT: //联系人导入
+ $config = WorkbenchImportContact::where('workbenchId', $id)->find();
+ if ($config) {
+ $newConfig = new WorkbenchImportContact;
+ $newConfig->workbenchId = $newWorkbench->id;
+ $newConfig->devices = $config->devices;
+ $newConfig->pools = $config->pools;
+ $newConfig->num = $config->num;
+ $newConfig->clearContact = $config->clearContact;
+ $newConfig->remark = $config->remark;
+ $newConfig->startTime = $config->startTime;
+ $newConfig->endTime = $config->endTime;
+ $newConfig->createTime = time();
+ $newConfig->save();
+ }
+ break;
+ }
+
+ Db::commit();
+ return json(['code' => 200, 'msg' => '拷贝成功', 'data' => ['id' => $newWorkbench->id]]);
+ } catch (\Exception $e) {
+ Db::rollback();
+ return json(['code' => 500, 'msg' => '拷贝失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 获取点赞记录列表
+ * @return \think\response\Json
+ */
+ public function getLikeRecords()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchAutoLikeController();
+ $controller->request = $this->request;
+ return $controller->getLikeRecords();
+ }
+
+ /**
+ * 获取朋友圈发布记录列表
+ * @return \think\response\Json
+ */
+ public function getMomentsRecords()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchMomentsController();
+ $controller->request = $this->request;
+ return $controller->getMomentsRecords();
+ }
+
+ /**
+ * 获取朋友圈发布统计
+ * @return \think\response\Json
+ */
+ public function getMomentsStats()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchMomentsController();
+ $controller->request = $this->request;
+ return $controller->getMomentsStats();
+ }
+
+ /**
+ * 获取流量分发记录列表
+ * @return \think\response\Json
+ */
+ public function getTrafficDistributionRecords()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchTrafficController();
+ $controller->request = $this->request;
+ return $controller->getTrafficDistributionRecords();
+ }
+
+ /**
+ * 获取流量分发统计
+ * @return \think\response\Json
+ */
+ public function getTrafficDistributionStats()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchTrafficController();
+ $controller->request = $this->request;
+ return $controller->getTrafficDistributionStats();
+ }
+
+ /**
+ * 获取流量分发详情
+ * @return \think\response\Json
+ */
+ public function getTrafficDistributionDetail()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchTrafficController();
+ $controller->request = $this->request;
+ return $controller->getTrafficDistributionDetail();
+ }
+
+ /**
+ * 创建流量分发计划
+ * @return \think\response\Json
+ */
+ public function createTrafficPlan()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchTrafficController();
+ $controller->request = $this->request;
+ return $controller->createTrafficPlan();
+ }
+
+ /**
+ * 获取流量列表
+ * @return \think\response\Json
+ */
+ public function getTrafficList()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchTrafficController();
+ $controller->request = $this->request;
+ return $controller->getTrafficList();
+ }
+
+ /**
+ * 获取所有微信好友标签及数量统计
+ * @return \think\response\Json
+ */
+ public function getDeviceLabels()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController();
+ $controller->request = $this->request;
+ return $controller->getDeviceLabels();
+ }
+
+ /**
+ * 获取群列表
+ * @return \think\response\Json
+ */
+ public function getGroupList()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController();
+ $controller->request = $this->request;
+ return $controller->getGroupList();
+ }
+
+ /**
+ * 获取流量池列表
+ * @return \think\response\Json
+ */
+ public function getTrafficPoolList()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController();
+ $controller->request = $this->request;
+ return $controller->getTrafficPoolList();
+ }
+
+ /**
+ * 获取账号列表
+ * @return \think\response\Json
+ */
+ public function getAccountList()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController();
+ $controller->request = $this->request;
+ return $controller->getAccountList();
+ }
+
+ /**
+ * 获取京东联盟导购媒体
+ * @return \think\response\Json
+ */
+ public function getJdSocialMedia()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController();
+ $controller->request = $this->request;
+ return $controller->getJdSocialMedia();
+ }
+
+ /**
+ * 获取京东联盟广告位
+ * @return \think\response\Json
+ */
+ public function getJdPromotionSite()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController();
+ $controller->request = $this->request;
+ return $controller->getJdPromotionSite();
+ }
+
+ /**
+ * 京东转链-京推推
+ * @param string $content
+ * @param string $positionid
+ * @return string
+ */
+ public function changeLink($content = '', $positionid = '')
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController();
+ $controller->request = $this->request;
+ return $controller->changeLink($content, $positionid);
+ }
+
+ /**
+ * 规范化客服微信ID列表
+ * @param mixed $ownerWechatIds
+ * @param WorkbenchGroupPush|null $originalConfig
+ * @return array
+ * @throws \Exception
+ */
+ private function normalizeOwnerWechatIds($ownerWechatIds, WorkbenchGroupPush $originalConfig = null): array
+ {
+ if ($ownerWechatIds === null) {
+ $existing = $originalConfig ? $this->decodeJsonArray($originalConfig->ownerWechatIds ?? []) : [];
+ if (empty($existing)) {
+ throw new \Exception('请至少选择一个客服微信');
+ }
+ return $existing;
+ }
+
+ if (!is_array($ownerWechatIds)) {
+ throw new \Exception('客服参数格式错误');
+ }
+
+ $normalized = $this->extractIdList($ownerWechatIds, '客服参数格式错误');
+ if (empty($normalized)) {
+ throw new \Exception('请至少选择一个客服微信');
+ }
+ return $normalized;
+ }
+
+ /**
+ * 构建群推送配置数据
+ * @param array $param
+ * @param array $ownerWechatIds
+ * @param WorkbenchGroupPush|null $originalConfig
+ * @return array
+ * @throws \Exception
+ */
+ private function prepareGroupPushData(array $param, array $ownerWechatIds, WorkbenchGroupPush $originalConfig = null): array
+ {
+ $targetTypeDefault = $originalConfig ? intval($originalConfig->targetType) : 1;
+ $targetType = intval($this->getParamValue($param, 'targetType', $targetTypeDefault)) ?: 1;
+
+ $groupPushSubTypeDefault = $originalConfig ? intval($originalConfig->groupPushSubType) : 1;
+ $groupPushSubType = intval($this->getParamValue($param, 'groupPushSubType', $groupPushSubTypeDefault)) ?: 1;
+ if (!in_array($groupPushSubType, [1, 2], true)) {
+ $groupPushSubType = 1;
+ }
+
+ $data = [
+ 'pushType' => $this->toBoolInt($this->getParamValue($param, 'pushType', $originalConfig->pushType ?? 0)),
+ 'targetType' => $targetType,
+ 'startTime' => $this->getParamValue($param, 'startTime', $originalConfig->startTime ?? ''),
+ 'endTime' => $this->getParamValue($param, 'endTime', $originalConfig->endTime ?? ''),
+ 'maxPerDay' => intval($this->getParamValue($param, 'maxPerDay', $originalConfig->maxPerDay ?? 0)),
+ 'pushOrder' => $this->getParamValue($param, 'pushOrder', $originalConfig->pushOrder ?? 1),
+ 'groupPushSubType' => $groupPushSubType,
+ 'status' => $this->toBoolInt($this->getParamValue($param, 'status', $originalConfig->status ?? 0)),
+ 'socialMediaId' => $this->getParamValue($param, 'socialMediaId', $originalConfig->socialMediaId ?? ''),
+ 'promotionSiteId' => $this->getParamValue($param, 'promotionSiteId', $originalConfig->promotionSiteId ?? ''),
+ 'friendIntervalMin' => intval($this->getParamValue($param, 'friendIntervalMin', $originalConfig->friendIntervalMin ?? 10)),
+ 'friendIntervalMax' => intval($this->getParamValue($param, 'friendIntervalMax', $originalConfig->friendIntervalMax ?? 20)),
+ 'messageIntervalMin' => intval($this->getParamValue($param, 'messageIntervalMin', $originalConfig->messageIntervalMin ?? 1)),
+ 'messageIntervalMax' => intval($this->getParamValue($param, 'messageIntervalMax', $originalConfig->messageIntervalMax ?? 12)),
+ 'isRandomTemplate' => $this->toBoolInt($this->getParamValue($param, 'isRandomTemplate', $originalConfig->isRandomTemplate ?? 0)),
+ 'ownerWechatIds' => json_encode($ownerWechatIds, JSON_UNESCAPED_UNICODE),
+ ];
+
+ if ($data['friendIntervalMin'] > $data['friendIntervalMax']) {
+ throw new \Exception('目标间最小间隔不能大于最大间隔');
+ }
+ if ($data['messageIntervalMin'] > $data['messageIntervalMax']) {
+ throw new \Exception('消息间最小间隔不能大于最大间隔');
+ }
+
+ $contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : [];
+ $contentGroupsParam = $this->getParamValue($param, 'contentGroups', null);
+ $contentGroups = $contentGroupsParam !== null
+ ? $this->extractIdList($contentGroupsParam, '内容库参数格式错误')
+ : $contentGroupsExisting;
+ $data['contentLibraries'] = json_encode($contentGroups, JSON_UNESCAPED_UNICODE);
+
+ $postPushTagsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->postPushTags ?? []) : [];
+ $postPushTagsParam = $this->getParamValue($param, 'postPushTags', null);
+ $postPushTags = $postPushTagsParam !== null
+ ? $this->extractIdList($postPushTagsParam, '推送标签参数格式错误')
+ : $postPushTagsExisting;
+ $data['postPushTags'] = json_encode($postPushTags, JSON_UNESCAPED_UNICODE);
+
+ if ($targetType === 1) {
+ $data['isLoop'] = $this->toBoolInt($this->getParamValue($param, 'isLoop', $originalConfig->isLoop ?? 0));
+
+ $groupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->groups ?? []) : [];
+ $wechatGroups = array_key_exists('wechatGroups', $param)
+ ? $this->extractIdList($param['wechatGroups'], '群参数格式错误')
+ : $groupsExisting;
+ if (empty($wechatGroups)) {
+ throw new \Exception('群推送必须选择微信群');
+ }
+ $data['groups'] = json_encode($wechatGroups, JSON_UNESCAPED_UNICODE);
+ $data['friends'] = json_encode([], JSON_UNESCAPED_UNICODE);
+ $data['trafficPools'] = json_encode([], JSON_UNESCAPED_UNICODE);
+
+ if ($groupPushSubType === 2) {
+ $announcementContent = $this->getParamValue($param, 'announcementContent', $originalConfig->announcementContent ?? '');
+ if (empty($announcementContent)) {
+ throw new \Exception('群公告必须输入公告内容');
+ }
+ $enableAiRewrite = $this->toBoolInt($this->getParamValue($param, 'enableAiRewrite', $originalConfig->enableAiRewrite ?? 0));
+ $aiRewritePrompt = trim((string)$this->getParamValue($param, 'aiRewritePrompt', $originalConfig->aiRewritePrompt ?? ''));
+ if ($enableAiRewrite === 1 && $aiRewritePrompt === '') {
+ throw new \Exception('启用AI智能话术改写时,必须输入改写提示词');
+ }
+ $data['announcementContent'] = $announcementContent;
+ $data['enableAiRewrite'] = $enableAiRewrite;
+ $data['aiRewritePrompt'] = $aiRewritePrompt;
+ } else {
+ $data['groupPushSubType'] = 1;
+ $data['announcementContent'] = '';
+ $data['enableAiRewrite'] = 0;
+ $data['aiRewritePrompt'] = '';
+ }
+ } else {
+ $data['isLoop'] = 0;
+ $friendsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->friends ?? []) : [];
+ $trafficPoolsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->trafficPools ?? []) : [];
+
+ $friendTargets = array_key_exists('wechatFriends', $param)
+ ? $this->extractIdList($param['wechatFriends'], '好友参数格式错误')
+ : $friendsExisting;
+ $trafficPools = array_key_exists('trafficPools', $param)
+ ? $this->extractIdList($param['trafficPools'], '流量池参数格式错误')
+ : $trafficPoolsExisting;
+
+ if (empty($friendTargets) && empty($trafficPools)) {
+ throw new \Exception('好友推送需至少选择好友或流量池');
+ }
+
+ $data['friends'] = json_encode($friendTargets, JSON_UNESCAPED_UNICODE);
+ $data['trafficPools'] = json_encode($trafficPools, JSON_UNESCAPED_UNICODE);
+ $data['groups'] = json_encode([], JSON_UNESCAPED_UNICODE);
+ $data['groupPushSubType'] = 1;
+ $data['announcementContent'] = '';
+ $data['enableAiRewrite'] = 0;
+ $data['aiRewritePrompt'] = '';
+ }
+
+ return $data;
+ }
+
+ /**
+ * 获取参数值,若不存在则返回默认值
+ * @param array $param
+ * @param string $key
+ * @param mixed $default
+ * @return mixed
+ */
+ private function getParamValue(array $param, string $key, $default)
+ {
+ return array_key_exists($key, $param) ? $param[$key] : $default;
+ }
+
+ /**
+ * 将值转换为整型布尔
+ * @param mixed $value
+ * @return int
+ */
+ private function toBoolInt($value): int
+ {
+ return empty($value) ? 0 : 1;
+ }
+
+ /**
+ * 从参数中提取ID列表
+ * @param mixed $items
+ * @param string $errorMessage
+ * @return array
+ * @throws \Exception
+ */
+ private function extractIdList($items, string $errorMessage = '参数格式错误'): array
+ {
+ if (!is_array($items)) {
+ throw new \Exception($errorMessage);
+ }
+
+ $ids = [];
+ foreach ($items as $item) {
+ if (is_array($item) && isset($item['id'])) {
+ $item = $item['id'];
+ }
+ if ($item === '' || $item === null) {
+ continue;
+ }
+ $ids[] = $item;
+ }
+
+ return array_values(array_unique($ids));
+ }
+
+ /**
+ * 解码JSON数组
+ * @param mixed $value
+ * @return array
+ */
+ private function decodeJsonArray($value): array
+ {
+ if (empty($value)) {
+ return [];
+ }
+ if (is_array($value)) {
+ return $value;
+ }
+ $decoded = json_decode($value, true);
+ return is_array($decoded) ? $decoded : [];
+ }
+
+ /**
+ * 验证内容是否包含链接
+ * @param string $content 要检测的内容
+ * @return bool
+ */
+ private function containsLink($content)
+ {
+ // 定义各种链接的正则表达式模式
+ $patterns = [
+ // HTTP/HTTPS链接
+ '/https?:\/\/[^\s]+/i',
+ // 京东商品链接
+ '/item\.jd\.com\/\d+/i',
+ // 京东短链接
+ '/u\.jd\.com\/[a-zA-Z0-9]+/i',
+ // 淘宝商品链接
+ '/item\.taobao\.com\/item\.htm\?id=\d+/i',
+ // 天猫商品链接
+ '/detail\.tmall\.com\/item\.htm\?id=\d+/i',
+ // 淘宝短链接
+ '/m\.tb\.cn\/[a-zA-Z0-9]+/i',
+ // 拼多多链接
+ '/mobile\.yangkeduo\.com\/goods\.html\?goods_id=\d+/i',
+ // 苏宁易购链接
+ '/product\.suning\.com\/\d+\/\d+\.html/i',
+ // 通用域名模式(包含常见电商域名)
+ '/(?:jd|taobao|tmall|yangkeduo|suning|amazon|dangdang)\.com[^\s]*/i',
+ // 通用短链接模式
+ '/[a-zA-Z0-9-]+\.[a-zA-Z]{2,}\/[a-zA-Z0-9\-._~:\/?#\[\]@!$&\'()*+,;=]+/i'
+ ];
+
+ // 遍历所有模式进行匹配
+ foreach ($patterns as $pattern) {
+ if (preg_match($pattern, $content)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+
+ /**
+ * 获取通讯录导入记录列表
+ * @return \think\response\Json
+ */
+ public function getImportContact()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $workbenchId = $this->request->param('workbenchId', 0);
+
+ $where = [
+ ['wici.workbenchId', '=', $workbenchId]
+ ];
+
+ // 查询发布记录
+ $list = Db::name('workbench_import_contact_item')->alias('wici')
+ ->join('traffic_pool tp', 'tp.id = wici.poolId', 'left')
+ ->join('traffic_source tc', 'tc.identifier = tp.identifier', 'left')
+ ->join('wechat_account wa', 'wa.wechatId = tp.wechatId', 'left')
+ ->field([
+ 'wici.id',
+ 'wici.workbenchId',
+ 'wici.createTime',
+ 'tp.identifier',
+ 'tp.mobile',
+ 'tp.wechatId',
+ 'tc.name',
+ 'wa.nickName',
+ 'wa.avatar',
+ 'wa.alias',
+ ])
+ ->where($where)
+ ->order('tc.name DESC,wici.createTime DESC')
+ ->group('tp.identifier')
+ ->page($page, $limit)
+ ->select();
+
+ foreach ($list as &$item) {
+ $item['createTime'] = date('Y-m-d H:i:s', $item['createTime']);
+ }
+
+
+ // 获取总记录数
+ $total = Db::name('workbench_import_contact_item')->alias('wici')
+ ->where($where)
+ ->count();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ ]
+ ]);
+ }
+
+ /**
+ * 获取群发统计数据
+ * @return \think\response\Json
+ */
+ public function getGroupPushStats()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchGroupPushController();
+ $controller->request = $this->request;
+ return $controller->getGroupPushStats();
+ }
+
+ /**
+ * 计算基础统计数据
+ */
+ private function calculateBasicStats($workbenchId, $where, $startTime, $endTime, $contentIds = null)
+ {
+ // 获取工作台配置,计算计划发送数
+ // 如果 workbenchId 为空,则查询所有工作台的配置
+ $configQuery = WorkbenchGroupPush::alias('wgp')
+ ->join('workbench w', 'w.id = wgp.workbenchId', 'left')
+ ->where('w.type', self::TYPE_GROUP_PUSH)
+ ->where('w.isDel', 0);
+
+ if (!empty($workbenchId)) {
+ $configQuery->where('wgp.workbenchId', $workbenchId);
+ } else {
+ // 如果没有指定工作台ID,需要从 where 条件中获取 workbenchId 列表
+ $workbenchIdCondition = null;
+ foreach ($where as $condition) {
+ if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') {
+ if ($condition[1] === 'in' && is_array($condition[2])) {
+ $workbenchIdCondition = $condition[2];
+ break;
+ } elseif ($condition[1] === '=') {
+ $workbenchIdCondition = [$condition[2]];
+ break;
+ }
+ }
+ }
+ if ($workbenchIdCondition) {
+ $configQuery->whereIn('wgp.workbenchId', $workbenchIdCondition);
+ }
+ }
+
+ $configs = $configQuery->select();
+ $targetType = 1; // 默认值
+ if (!empty($configs)) {
+ // 如果只有一个配置,使用它的 targetType;如果有多个,默认使用1
+ $targetType = intval($configs[0]->targetType ?? 1);
+ }
+
+ // 计划发送数(根据配置计算)
+ $plannedSend = 0;
+ if (!empty($configs)) {
+ $days = ceil(($endTime - $startTime) / 86400);
+ foreach ($configs as $config) {
+ $maxPerDay = intval($config->maxPerDay ?? 0);
+ $configTargetType = intval($config->targetType ?? 1);
+ if ($configTargetType == 1) {
+ // 群推送:计划发送数 = 每日推送次数 * 天数 * 群数量
+ $groups = $this->decodeJsonArray($config->groups ?? []);
+ $plannedSend += $maxPerDay * $days * count($groups);
+ } else {
+ // 好友推送:计划发送数 = 每日推送人数 * 天数
+ $plannedSend += $maxPerDay * $days;
+ }
+ }
+ }
+
+ // 构建查询条件
+ $queryWhere = $where;
+ if ($contentIds !== null) {
+ $queryWhere[] = ['wgpi.contentId', 'in', $contentIds];
+ }
+
+ // 实际成功发送数(从推送记录表统计)
+ $successSend = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($queryWhere)
+ ->count();
+
+ // 触达率 = 成功发送数 / 计划发送数
+ $reachRate = $plannedSend > 0 ? round(($successSend / $plannedSend) * 100, 1) : 0;
+
+ // 获取发送记录列表,用于查询回复
+ $sentItemIds = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($queryWhere)
+ ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId')
+ ->select();
+
+ // 回复统计(通过消息表查询)
+ $replyStats = $this->calculateReplyStats($sentItemIds, $targetType, $startTime, $endTime);
+
+ // 链接点击统计
+ $clickStats = $this->calculateClickStats($sentItemIds, $targetType, $startTime, $endTime);
+
+ // 计算本月对比数据(简化处理,实际应该查询上个月同期数据)
+ $currentMonthStart = strtotime(date('Y-m-01 00:00:00'));
+ $lastMonthStart = strtotime(date('Y-m-01 00:00:00', strtotime('-1 month')));
+ $lastMonthEnd = $currentMonthStart - 1;
+
+ // 获取本月统计数据(避免递归调用)
+ $currentMonthWhere = [
+ ['wgpi.createTime', '>=', $currentMonthStart]
+ ];
+ // 复制 workbenchId 条件
+ foreach ($where as $condition) {
+ if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') {
+ $currentMonthWhere[] = $condition;
+ break;
+ }
+ }
+ if ($contentIds !== null) {
+ $currentMonthWhere[] = ['wgpi.contentId', 'in', $contentIds];
+ }
+ $currentMonthSend = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($currentMonthWhere)
+ ->count();
+
+ // 获取本月配置
+ $currentMonthConfigQuery = WorkbenchGroupPush::alias('wgp')
+ ->join('workbench w', 'w.id = wgp.workbenchId', 'left')
+ ->where('w.type', self::TYPE_GROUP_PUSH)
+ ->where('w.isDel', 0);
+ if (!empty($workbenchId)) {
+ $currentMonthConfigQuery->where('wgp.workbenchId', $workbenchId);
+ } else {
+ foreach ($where as $condition) {
+ if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId' && $condition[1] === 'in') {
+ $currentMonthConfigQuery->whereIn('wgp.workbenchId', $condition[2]);
+ break;
+ }
+ }
+ }
+ $currentMonthConfigs = $currentMonthConfigQuery->select();
+ $currentMonthPlanned = 0;
+ if (!empty($currentMonthConfigs)) {
+ $currentMonthDays = ceil(($endTime - $currentMonthStart) / 86400);
+ foreach ($currentMonthConfigs as $currentMonthConfig) {
+ $currentMonthMaxPerDay = intval($currentMonthConfig->maxPerDay ?? 0);
+ $currentMonthTargetType = intval($currentMonthConfig->targetType ?? 1);
+ if ($currentMonthTargetType == 1) {
+ $currentMonthGroups = $this->decodeJsonArray($currentMonthConfig->groups ?? []);
+ $currentMonthPlanned += $currentMonthMaxPerDay * $currentMonthDays * count($currentMonthGroups);
+ } else {
+ $currentMonthPlanned += $currentMonthMaxPerDay * $currentMonthDays;
+ }
+ }
+ }
+ $currentMonthReachRate = $currentMonthPlanned > 0 ? round(($currentMonthSend / $currentMonthPlanned) * 100, 1) : 0;
+
+ // 获取上个月统计数据
+ $lastMonthWhere = [
+ ['wgpi.createTime', '>=', $lastMonthStart],
+ ['wgpi.createTime', '<=', $lastMonthEnd]
+ ];
+ // 复制 workbenchId 条件
+ foreach ($where as $condition) {
+ if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') {
+ $lastMonthWhere[] = $condition;
+ break;
+ }
+ }
+ if ($contentIds !== null) {
+ $lastMonthWhere[] = ['wgpi.contentId', 'in', $contentIds];
+ }
+ $lastMonthSend = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($lastMonthWhere)
+ ->count();
+
+ // 获取上个月配置
+ $lastMonthConfigQuery = WorkbenchGroupPush::alias('wgp')
+ ->join('workbench w', 'w.id = wgp.workbenchId', 'left')
+ ->where('w.type', self::TYPE_GROUP_PUSH)
+ ->where('w.isDel', 0);
+ if (!empty($workbenchId)) {
+ $lastMonthConfigQuery->where('wgp.workbenchId', $workbenchId);
+ } else {
+ foreach ($where as $condition) {
+ if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId' && $condition[1] === 'in') {
+ $lastMonthConfigQuery->whereIn('wgp.workbenchId', $condition[2]);
+ break;
+ }
+ }
+ }
+ $lastMonthConfigs = $lastMonthConfigQuery->select();
+
+ $lastMonthPlanned = 0;
+ if (!empty($lastMonthConfigs)) {
+ $lastMonthDays = ceil(($lastMonthEnd - $lastMonthStart) / 86400);
+ foreach ($lastMonthConfigs as $lastMonthConfig) {
+ $lastMonthMaxPerDay = intval($lastMonthConfig->maxPerDay ?? 0);
+ $lastMonthTargetType = intval($lastMonthConfig->targetType ?? 1);
+ if ($lastMonthTargetType == 1) {
+ $lastMonthGroups = $this->decodeJsonArray($lastMonthConfig->groups ?? []);
+ $lastMonthPlanned += $lastMonthMaxPerDay * $lastMonthDays * count($lastMonthGroups);
+ } else {
+ $lastMonthPlanned += $lastMonthMaxPerDay * $lastMonthDays;
+ }
+ }
+ }
+ $lastMonthReachRate = $lastMonthPlanned > 0 ? round(($lastMonthSend / $lastMonthPlanned) * 100, 1) : 0;
+
+ // 获取上个月的回复和点击统计(简化处理)
+ $lastMonthSentItems = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($lastMonthWhere)
+ ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId')
+ ->select();
+ $lastMonthReplyStats = $this->calculateReplyStats($lastMonthSentItems, $targetType, $lastMonthStart, $lastMonthEnd);
+ $lastMonthClickStats = $this->calculateClickStats($lastMonthSentItems, $targetType, $lastMonthStart, $lastMonthEnd);
+
+ return [
+ 'reachRate' => [
+ 'value' => $reachRate,
+ 'trend' => round($reachRate - $lastMonthReachRate, 1),
+ 'unit' => '%',
+ 'description' => '成功发送/计划发送'
+ ],
+ 'replyRate' => [
+ 'value' => $replyStats['replyRate'],
+ 'trend' => round($replyStats['replyRate'] - $lastMonthReplyStats['replyRate'], 1),
+ 'unit' => '%',
+ 'description' => '收到回复/成功发送'
+ ],
+ 'avgReplyTime' => [
+ 'value' => $replyStats['avgReplyTime'],
+ 'trend' => round($lastMonthReplyStats['avgReplyTime'] - $replyStats['avgReplyTime'], 0),
+ 'unit' => '分钟',
+ 'description' => '从发送到回复的平均时长'
+ ],
+ 'clickRate' => [
+ 'value' => $clickStats['clickRate'],
+ 'trend' => round($clickStats['clickRate'] - $lastMonthClickStats['clickRate'], 1),
+ 'unit' => '%',
+ 'description' => '点击链接/成功发送'
+ ],
+ 'plannedSend' => $plannedSend,
+ 'successSend' => $successSend,
+ 'replyCount' => $replyStats['replyCount'],
+ 'clickCount' => $clickStats['clickCount']
+ ];
+ }
+
+ /**
+ * 计算回复统计
+ */
+ private function calculateReplyStats($sentItems, $targetType, $startTime, $endTime)
+ {
+ if (empty($sentItems)) {
+ return ['replyRate' => 0, 'avgReplyTime' => 0, 'replyCount' => 0];
+ }
+
+ $replyCount = 0;
+ $totalReplyTime = 0;
+ $replyTimes = [];
+
+ foreach ($sentItems as $item) {
+ $itemArray = is_array($item) ? $item : (array)$item;
+ $sendTime = $itemArray['createTime'] ?? 0;
+ $accountId = $itemArray['wechatAccountId'] ?? 0;
+
+ if ($targetType == 1) {
+ // 群推送:查找群内回复消息
+ $groupId = $itemArray['groupId'] ?? 0;
+ $group = Db::name('wechat_group')->where('id', $groupId)->find();
+ if ($group) {
+ $replyMsg = Db::table('s2_wechat_message')
+ ->where('wechatChatroomId', $group['chatroomId'])
+ ->where('wechatAccountId', $accountId)
+ ->where('isSend', 0) // 接收的消息
+ ->where('wechatTime', '>', $sendTime)
+ ->where('wechatTime', '<=', $sendTime + 86400) // 24小时内回复
+ ->order('wechatTime', 'asc')
+ ->find();
+
+ if ($replyMsg) {
+ $replyCount++;
+ $replyTime = $replyMsg['wechatTime'] - $sendTime;
+ $replyTimes[] = $replyTime;
+ $totalReplyTime += $replyTime;
+ }
+ }
+ } else {
+ // 好友推送:查找好友回复消息
+ $friendId = $itemArray['friendId'] ?? 0;
+ $friend = Db::table('s2_wechat_friend')->where('id', $friendId)->find();
+ if ($friend) {
+ $replyMsg = Db::table('s2_wechat_message')
+ ->where('wechatFriendId', $friendId)
+ ->where('wechatAccountId', $accountId)
+ ->where('isSend', 0) // 接收的消息
+ ->where('wechatTime', '>', $sendTime)
+ ->where('wechatTime', '<=', $sendTime + 86400) // 24小时内回复
+ ->order('wechatTime', 'asc')
+ ->find();
+
+ if ($replyMsg) {
+ $replyCount++;
+ $replyTime = $replyMsg['wechatTime'] - $sendTime;
+ $replyTimes[] = $replyTime;
+ $totalReplyTime += $replyTime;
+ }
+ }
+ }
+ }
+
+ $successSend = count($sentItems);
+ $replyRate = $successSend > 0 ? round(($replyCount / $successSend) * 100, 1) : 0;
+ $avgReplyTime = $replyCount > 0 ? round(($totalReplyTime / $replyCount) / 60, 0) : 0; // 转换为分钟
+
+ return [
+ 'replyRate' => $replyRate,
+ 'avgReplyTime' => $avgReplyTime,
+ 'replyCount' => $replyCount
+ ];
+ }
+
+ /**
+ * 计算链接点击统计
+ */
+ private function calculateClickStats($sentItems, $targetType, $startTime, $endTime)
+ {
+ if (empty($sentItems)) {
+ return ['clickRate' => 0, 'clickCount' => 0];
+ }
+
+ $clickCount = 0;
+ $linkContentIds = [];
+
+ // 获取所有发送的内容ID
+ foreach ($sentItems as $item) {
+ $itemArray = is_array($item) ? $item : (array)$item;
+ $contentId = $itemArray['contentId'] ?? 0;
+ if ($contentId > 0) {
+ $linkContentIds[] = $contentId;
+ }
+ }
+
+ if (empty($linkContentIds)) {
+ return ['clickRate' => 0, 'clickCount' => 0];
+ }
+
+ // 查询包含链接的内容
+ $linkContents = Db::name('content_item')
+ ->whereIn('id', array_unique($linkContentIds))
+ ->where('contentType', 2) // 链接类型
+ ->column('id');
+
+ // 统计发送了链接内容的记录数
+ $linkSendCount = 0;
+ foreach ($sentItems as $item) {
+ $itemArray = is_array($item) ? $item : (array)$item;
+ $contentId = $itemArray['contentId'] ?? 0;
+ if (in_array($contentId, $linkContents)) {
+ $linkSendCount++;
+ }
+ }
+
+ // 简化处理:假设点击率基于链接消息的发送(实际应该从点击追踪系统获取)
+ // 这里可以根据业务需求调整,比如通过消息中的链接点击事件统计
+ $clickCount = $linkSendCount; // 简化处理,实际需要真实的点击数据
+
+ $successSend = count($sentItems);
+ $clickRate = $successSend > 0 ? round(($clickCount / $successSend) * 100, 1) : 0;
+
+ return [
+ 'clickRate' => $clickRate,
+ 'clickCount' => $clickCount
+ ];
+ }
+
+ /**
+ * 获取话术组对比数据
+ */
+ private function getContentLibraryComparison($workbenchId, $where, $startTime, $endTime, $contentIds = null)
+ {
+ $queryWhere = $where;
+ if ($contentIds !== null) {
+ $queryWhere[] = ['wgpi.contentId', 'in', $contentIds];
+ }
+
+ $comparison = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->join('content_item ci', 'ci.id = wgpi.contentId', 'left')
+ ->join('content_library cl', 'cl.id = ci.libraryId', 'left')
+ ->where($queryWhere)
+ ->where('cl.id', '<>', null)
+ ->field([
+ 'cl.id as libraryId',
+ 'cl.name as libraryName',
+ 'COUNT(DISTINCT wgpi.id) as pushCount'
+ ])
+ ->group('cl.id, cl.name')
+ ->select();
+
+ $result = [];
+ foreach ($comparison as $item) {
+ $libraryId = $item['libraryId'];
+ $pushCount = intval($item['pushCount']);
+
+ // 获取该内容库的详细统计
+ $libraryContentIds = Db::name('content_item')
+ ->where('libraryId', $libraryId)
+ ->column('id');
+ if (empty($libraryContentIds)) {
+ $libraryContentIds = [-1];
+ }
+
+ $libraryWhere = array_merge($where, [['wgpi.contentId', 'in', $libraryContentIds]]);
+ $librarySentItems = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($libraryWhere)
+ ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId')
+ ->select();
+
+ $config = WorkbenchGroupPush::where('workbenchId', $workbenchId)->find();
+ $targetType = $config ? intval($config->targetType) : 1;
+
+ $replyStats = $this->calculateReplyStats($librarySentItems, $targetType, $startTime, $endTime);
+ $clickStats = $this->calculateClickStats($librarySentItems, $targetType, $startTime, $endTime);
+
+ // 计算转化率(简化处理,实际需要根据业务定义)
+ $conversionRate = $pushCount > 0 ? round(($replyStats['replyCount'] / $pushCount) * 100, 1) : 0;
+
+ $result[] = [
+ 'libraryId' => $libraryId,
+ 'libraryName' => $item['libraryName'],
+ 'pushCount' => $pushCount,
+ 'reachRate' => 100, // 简化处理,实际应该计算
+ 'replyRate' => $replyStats['replyRate'],
+ 'clickRate' => $clickStats['clickRate'],
+ 'conversionRate' => $conversionRate,
+ 'avgReplyTime' => $replyStats['avgReplyTime'],
+ 'level' => $this->getPerformanceLevel($replyStats['replyRate'], $clickStats['clickRate'], $conversionRate)
+ ];
+ }
+
+ // 按回复率排序
+ usort($result, function($a, $b) {
+ return $b['replyRate'] <=> $a['replyRate'];
+ });
+
+ return $result;
+ }
+
+ /**
+ * 获取性能等级
+ */
+ private function getPerformanceLevel($replyRate, $clickRate, $conversionRate)
+ {
+ $score = ($replyRate * 0.4) + ($clickRate * 0.3) + ($conversionRate * 0.3);
+
+ if ($score >= 40) {
+ return '优秀';
+ } elseif ($score >= 25) {
+ return '良好';
+ } elseif ($score >= 15) {
+ return '一般';
+ } else {
+ return '待提升';
+ }
+ }
+
+ /**
+ * 获取时段分析数据
+ */
+ private function getTimePeriodAnalysis($workbenchId, $where, $startTime, $endTime, $contentIds = null)
+ {
+ $queryWhere = $where;
+ if ($contentIds !== null) {
+ $queryWhere[] = ['wgpi.contentId', 'in', $contentIds];
+ }
+
+ $analysis = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($queryWhere)
+ ->field([
+ 'FROM_UNIXTIME(wgpi.createTime, "%H") as hour',
+ 'COUNT(*) as count'
+ ])
+ ->group('hour')
+ ->order('hour', 'asc')
+ ->select();
+
+ $result = [];
+ foreach ($analysis as $item) {
+ $result[] = [
+ 'hour' => intval($item['hour']),
+ 'count' => intval($item['count'])
+ ];
+ }
+
+ return $result;
+ }
+
+ /**
+ * 获取互动深度数据
+ */
+ private function getInteractionDepth($workbenchId, $where, $startTime, $endTime, $contentIds = null)
+ {
+ // 简化处理,实际需要更复杂的统计逻辑
+ return [
+ 'singleReply' => 0, // 单次回复
+ 'multipleReply' => 0, // 多次回复
+ 'deepInteraction' => 0 // 深度互动
+ ];
+ }
+
+ /**
+ * 获取推送历史记录列表
+ * @return \think\response\Json
+ */
+ public function getGroupPushHistory()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchGroupPushController();
+ $controller->request = $this->request;
+ return $controller->getGroupPushHistory();
+ }
+
+ /**
+ * 获取状态文本
+ * @param string $status 状态码
+ * @return string 状态文本
+ */
+ private function getStatusText($status)
+ {
+ $statusMap = [
+ 'success' => '已完成',
+ 'partial' => '进行中',
+ 'pending' => '进行中',
+ 'failed' => '失败'
+ ];
+ return $statusMap[$status] ?? '未知';
+ }
+
+ /**
+ * 获取已创建的群列表(自动建群)
+ * @return \think\response\Json
+ */
+ public function getCreatedGroupsList()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 100);
+ $keyword = $this->request->param('keyword', '');
+
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '工作台ID不能为空']);
+ }
+
+ // 验证工作台权限
+ $where = [
+ ['id', '=', $workbenchId],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['type', '=', self::TYPE_GROUP_CREATE],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+ $workbench = Workbench::where($where)->find();
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在或无权限']);
+ }
+
+ // 获取已创建的群ID列表(状态为成功且groupId不为空)
+ $groupIds = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('status', 2) // STATUS_SUCCESS = 2
+ ->where('groupId', '<>', null)
+ ->group('groupId')
+ ->column('groupId');
+
+ if (empty($groupIds)) {
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => [],
+ 'total' => 0,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]);
+ }
+
+ // 查询群组详细信息(从s2_wechat_chatroom表查询)
+ $query = Db::table('s2_wechat_chatroom')->alias('wc')
+ ->join('wechat_account wa', 'wa.wechatId = wc.wechatAccountWechatId', 'left')
+ ->where('wc.id', 'in', $groupIds)
+ ->where('wc.isDeleted', 0);
+
+ // 关键字搜索
+ if (!empty($keyword)) {
+ $query->where(function ($q) use ($keyword) {
+ $q->where('wc.nickname', 'like', '%' . $keyword . '%')
+ ->whereOr('wc.chatroomId', 'like', '%' . $keyword . '%')
+ ->whereOr('wa.nickName', 'like', '%' . $keyword . '%');
+ });
+ }
+
+ $total = $query->count();
+ $list = $query->field('wc.id,wc.nickname as groupName,wc.chatroomId,wc.chatroomAvatar as groupAvatar,wc.wechatAccountWechatId as ownerWechatId,wc.createTime,wc.chatroomOwnerNickname as ownerNickname,wc.chatroomOwnerAvatar as ownerAvatar,wa.alias as ownerAlias')
+ ->order('wc.createTime', 'desc')
+ ->page($page, $limit)
+ ->select();
+
+ // 统计每个群的成员数量和成员信息
+ foreach ($list as &$item) {
+ // 统计该群的成员数量(从workbench_group_create_item表统计)
+ $memberCount = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('groupId', $item['id'])
+ ->where('status', 'in', [2, 4]) // 创建成功和管理员好友已拉入
+ ->count();
+
+ // 获取成员列表(用于显示成员头像)
+ $memberList = Db::name('workbench_group_create_item')->alias('wgci')
+ ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wgci.friendId', 'left')
+ ->where('wgci.workbenchId', $workbenchId)
+ ->where('wgci.groupId', $item['id'])
+ ->where('wgci.status', 'in', [2, 4])
+ ->field('wf.avatar, wf.wechatId, wf.nickname')
+ ->order('wgci.createTime', 'asc')
+ ->limit(10) // 最多显示10个成员头像
+ ->select();
+
+ // 格式化成员头像列表
+ $memberAvatars = [];
+ foreach ($memberList as $member) {
+ if (!empty($member['avatar'])) {
+ $memberAvatars[] = [
+ 'avatar' => $member['avatar'],
+ 'wechatId' => $member['wechatId'] ?? '',
+ 'nickname' => $member['nickname'] ?? ''
+ ];
+ }
+ }
+
+ // 计算剩余成员数(用于显示"+XX")
+ $remainingCount = $memberCount > count($memberAvatars) ? $memberCount - count($memberAvatars) : 0;
+
+ // 格式化返回数据
+ $item['memberCount'] = $memberCount;
+ $item['memberCountText'] = $memberCount . '人'; // 格式化为"XX人"
+ $item['createTime'] = !empty($item['createTime']) ? date('Y-m-d', $item['createTime']) : ''; // 格式化为"YYYY-MM-DD"
+ $item['memberAvatars'] = $memberAvatars; // 成员头像列表(最多10个)
+ $item['remainingCount'] = $remainingCount; // 剩余成员数(用于显示"+XX")
+
+ // 保留原有字段,但调整格式
+ $item['groupName'] = $item['groupName'] ?? '';
+ $item['groupAvatar'] = $item['groupAvatar'] ?? '';
+ }
+ unset($item);
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]);
+ }
+
+ /**
+ * 获取已创建群的详情(自动建群)
+ * @return \think\response\Json
+ */
+ public function getCreatedGroupDetail()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ $groupId = $this->request->param('groupId', 0);
+
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '工作台ID不能为空']);
+ }
+
+ if (empty($groupId)) {
+ return json(['code' => 400, 'msg' => '群ID不能为空']);
+ }
+
+ // 验证工作台权限
+ $where = [
+ ['id', '=', $workbenchId],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['type', '=', self::TYPE_GROUP_CREATE],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+ $workbench = Workbench::where($where)->find();
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在或无权限']);
+ }
+
+ // 验证该群是否属于该工作台
+ $groupItem = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('groupId', $groupId)
+ ->where('status', 2) // STATUS_SUCCESS = 2
+ ->find();
+
+ if (empty($groupItem)) {
+ return json(['code' => 404, 'msg' => '群不存在或不属于该工作台']);
+ }
+
+ // 查询群基本信息(从s2_wechat_chatroom表查询)
+ $group = Db::table('s2_wechat_chatroom')->alias('wc')
+ ->join('wechat_account wa', 'wa.wechatId = wc.wechatAccountWechatId', 'left')
+ ->where('wc.id', $groupId)
+ ->where('wc.isDeleted', 0)
+ ->field('wc.id,wc.nickname as groupName,wc.chatroomId,wc.chatroomAvatar as groupAvatar,wc.wechatAccountWechatId as ownerWechatId,wc.createTime,wc.chatroomOwnerNickname as ownerNickname,wc.chatroomOwnerAvatar as ownerAvatar,wa.alias as ownerAlias,wc.announce')
+ ->find();
+
+ if (empty($group)) {
+ return json(['code' => 404, 'msg' => '群不存在']);
+ }
+
+ // 获取chatroomId
+ $chatroomId = $group['chatroomId'] ?? '';
+ if (empty($chatroomId)) {
+ return json(['code' => 400, 'msg' => '群聊ID不存在']);
+ }
+
+ // 从s2_wechat_chatroom_member表查询所有成员列表(不限制数量)
+ $memberList = Db::table('s2_wechat_chatroom_member')->alias('wcm')
+ ->join(['s2_wechat_friend' => 'wf'], 'wf.wechatId = wcm.wechatId', 'left')
+ ->where('wcm.chatroomId', $chatroomId)
+ ->field('wcm.wechatId,wcm.nickname as memberNickname,wcm.avatar as memberAvatar,wcm.conRemark as memberRemark,wcm.alias as memberAlias,wcm.createTime as joinTime,wcm.updateTime,wf.id as friendId,wf.nickname as friendNickname,wf.avatar as friendAvatar')
+ ->order('wcm.createTime', 'asc')
+ ->select();
+
+ // 去重:按wechatId去重,保留第一条记录
+ $memberMap = [];
+ foreach ($memberList as $member) {
+ $wechatId = $member['wechatId'] ?? '';
+ if (!empty($wechatId) && !isset($memberMap[$wechatId])) {
+ $memberMap[$wechatId] = $member;
+ }
+ }
+ $memberList = array_values($memberMap); // 重新索引数组
+
+ // 获取在群中的成员wechatId列表(用于判断是否已退群)
+ $inGroupWechatIds = array_column($memberList, 'wechatId');
+ $inGroupWechatIds = array_filter($inGroupWechatIds); // 过滤空值
+
+ // 获取通过自动建群加入的成员信息(用于判断入群状态和已退群成员)
+ $autoJoinMemberList = Db::name('workbench_group_create_item')
+ ->alias('wgci')
+ ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wgci.friendId', 'left')
+ ->where('wgci.workbenchId', $workbenchId)
+ ->where('wgci.groupId', $groupId)
+ ->where('wgci.status', 'in', [2, 4]) // 创建成功和管理员好友已拉入
+ ->field('wf.wechatId,wf.id as friendId,wf.nickname as friendNickname,wf.avatar as friendAvatar,wgci.createTime as autoJoinTime')
+ ->select();
+
+ // 去重:按wechatId去重,保留第一条记录
+ $autoJoinMemberMap = [];
+ foreach ($autoJoinMemberList as $autoMember) {
+ $wechatId = $autoMember['wechatId'] ?? '';
+ if (!empty($wechatId) && !isset($autoJoinMemberMap[$wechatId])) {
+ $autoJoinMemberMap[$wechatId] = $autoMember;
+ }
+ }
+ $autoJoinMemberList = array_values($autoJoinMemberMap); // 重新索引数组
+
+ // 统计成员总数(包括在群中的成员和已退群的成员)
+ // 先统计在群中的成员数
+ $inGroupCount = count($memberList);
+
+ // 统计已退群的成员数(在自动建群记录中但不在群中的成员)
+ $quitCount = 0;
+ if (!empty($autoJoinMemberList)) {
+ foreach ($autoJoinMemberList as $autoMember) {
+ $wechatId = $autoMember['wechatId'] ?? '';
+ if (!empty($wechatId) && !in_array($wechatId, $inGroupWechatIds)) {
+ $quitCount++;
+ }
+ }
+ }
+
+ // 总成员数 = 在群中的成员数 + 已退群的成员数
+ $memberCount = $inGroupCount + $quitCount;
+
+ // 获取自动建群加入的成员wechatId列表(用于判断入群状态)
+ $autoJoinWechatIds = array_column($autoJoinMemberList, 'wechatId');
+ $autoJoinWechatIds = array_filter($autoJoinWechatIds); // 过滤空值
+
+ // 格式化在群中的成员列表
+ $members = [];
+ $addedWechatIds = []; // 用于记录已添加的成员wechatId,避免重复
+ $ownerWechatId = $group['ownerWechatId'] ?? '';
+ foreach ($memberList as $member) {
+ $wechatId = $member['wechatId'] ?? '';
+
+ // 跳过空wechatId和已添加的成员
+ if (empty($wechatId) || in_array($wechatId, $addedWechatIds)) {
+ continue;
+ }
+
+ // 标记为已添加
+ $addedWechatIds[] = $wechatId;
+
+ // 根据wechatId判断是否为群主
+ $isOwner = (!empty($ownerWechatId) && $wechatId == $ownerWechatId) ? 1 : 0;
+
+ // 判断入群状态:如果在自动建群记录中,说明是通过自动建群加入的;否则是其他方式加入的
+ $joinStatus = in_array($wechatId, $autoJoinWechatIds) ? 'auto' : 'manual';
+
+ // 判断是否已退群:如果成员在s2_wechat_chatroom_member表中存在,说明在群中;否则已退群
+ // 由于我们已经从s2_wechat_chatroom_member表查询,所以这里都是"在群中"状态
+ $isQuit = 0; // 0=在群中,1=已退群
+
+ // 优先使用friend表的昵称和头像,如果没有则使用member表的
+ $nickname = !empty($member['friendNickname']) ? $member['friendNickname'] : ($member['memberNickname'] ?? '');
+ $avatar = !empty($member['friendAvatar']) ? $member['friendAvatar'] : ($member['memberAvatar'] ?? '');
+
+ $members[] = [
+ 'friendId' => $member['friendId'] ?? 0,
+ 'wechatId' => $wechatId,
+ 'nickname' => $nickname,
+ 'avatar' => $avatar,
+ 'alias' => $member['memberAlias'] ?? '',
+ 'remark' => $member['memberRemark'] ?? '',
+ 'isOwner' => $isOwner, // 标记群主
+ 'joinStatus' => $joinStatus, // 入群状态:auto=自动建群加入,manual=其他方式加入
+ 'isQuit' => $isQuit, // 是否已退群:0=在群中,1=已退群
+ 'joinTime' => !empty($member['joinTime']) ? date('Y-m-d H:i:s', $member['joinTime']) : '', // 入群时间
+ ];
+ }
+
+ // 添加已退群的成员(在自动建群记录中但不在群中的成员)
+ foreach ($autoJoinMemberList as $autoMember) {
+ $wechatId = $autoMember['wechatId'] ?? '';
+
+ // 跳过空wechatId、已在群中的成员和已添加的成员
+ if (empty($wechatId) || in_array($wechatId, $inGroupWechatIds) || in_array($wechatId, $addedWechatIds)) {
+ continue;
+ }
+
+ // 标记为已添加
+ $addedWechatIds[] = $wechatId;
+
+ // 根据wechatId判断是否为群主
+ $isOwner = (!empty($ownerWechatId) && $wechatId == $ownerWechatId) ? 1 : 0;
+
+ $members[] = [
+ 'friendId' => $autoMember['friendId'] ?? 0,
+ 'wechatId' => $wechatId,
+ 'nickname' => $autoMember['friendNickname'] ?? '',
+ 'avatar' => $autoMember['friendAvatar'] ?? '',
+ 'alias' => '',
+ 'remark' => '',
+ 'isOwner' => $isOwner, // 标记群主
+ 'joinStatus' => 'auto', // 入群状态:auto=自动建群加入
+ 'isQuit' => 1, // 是否已退群:1=已退群
+ 'joinTime' => !empty($autoMember['autoJoinTime']) ? date('Y-m-d H:i:s', $autoMember['autoJoinTime']) : '', // 入群时间
+ ];
+ }
+
+ // 将群主排在第一位
+ usort($members, function($a, $b) {
+ if ($a['isOwner'] == $b['isOwner']) {
+ return 0;
+ }
+ return $a['isOwner'] > $b['isOwner'] ? -1 : 1;
+ });
+
+ // 格式化返回数据
+ $result = [
+ 'id' => $group['id'],
+ 'groupName' => $group['groupName'] ?? '',
+ 'chatroomId' => $group['chatroomId'] ?? '',
+ 'groupAvatar' => $group['groupAvatar'] ?? '',
+ 'ownerWechatId' => $group['ownerWechatId'] ?? '',
+ 'ownerNickname' => $group['ownerNickname'] ?? '',
+ 'ownerAvatar' => $group['ownerAvatar'] ?? '',
+ 'ownerAlias' => $group['ownerAlias'] ?? '',
+ 'announce' => $group['announce'] ?? '',
+ 'createTime' => !empty($group['createTime']) ? date('Y-m-d H:i', $group['createTime']) : '', // 格式化为"YYYY-MM-DD HH:MM"
+ 'memberCount' => $memberCount,
+ 'memberCountText' => $memberCount . '人', // 格式化为"XX人"
+ 'workbenchName' => $workbench->name ?? '', // 任务名称(工作台名称)
+ 'members' => $members // 所有成员列表
+ ];
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => $result
+ ]);
+ }
+
+ /**
+ * 同步群最新信息(包括群成员)
+ * @return \think\response\Json
+ */
+ public function syncGroupInfo()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ $groupId = $this->request->param('groupId', 0);
+
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '工作台ID不能为空']);
+ }
+
+ if (empty($groupId)) {
+ return json(['code' => 400, 'msg' => '群ID不能为空']);
+ }
+
+ // 验证工作台权限
+ $where = [
+ ['id', '=', $workbenchId],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['type', '=', self::TYPE_GROUP_CREATE],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+ $workbench = Workbench::where($where)->find();
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在或无权限']);
+ }
+
+ // 验证该群是否属于该工作台
+ $groupItem = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('groupId', $groupId)
+ ->where('status', 2) // STATUS_SUCCESS = 2
+ ->find();
+
+ if (empty($groupItem)) {
+ return json(['code' => 404, 'msg' => '群不存在或不属于该工作台']);
+ }
+
+ // 查询群基本信息,获取chatroomId和wechatAccountWechatId
+ $group = Db::table('s2_wechat_chatroom')
+ ->where('id', $groupId)
+ ->where('isDeleted', 0)
+ ->field('id,chatroomId,wechatAccountWechatId')
+ ->find();
+
+ if (empty($group)) {
+ return json(['code' => 404, 'msg' => '群不存在']);
+ }
+
+ $chatroomId = $group['chatroomId'] ?? '';
+ $wechatAccountWechatId = $group['wechatAccountWechatId'] ?? '';
+
+ if (empty($chatroomId)) {
+ return json(['code' => 400, 'msg' => '群聊ID不存在']);
+ }
+
+ try {
+ // 实例化WechatChatroomController
+ $chatroomController = new \app\api\controller\WechatChatroomController();
+
+ // 1. 同步群信息(调用getlist方法)
+ $syncData = [
+ 'wechatChatroomId' => $chatroomId, // 使用chatroomId作为wechatChatroomId来指定要同步的群
+ 'wechatAccountKeyword' => $wechatAccountWechatId, // 通过群主微信ID筛选
+ 'isDeleted' => false,
+ 'pageIndex' => 1,
+ 'pageSize' => 100 // 获取足够多的数据
+ ];
+ $syncResult = $chatroomController->getlist($syncData, true, 0); // isInner = true
+ $syncResponse = json_decode($syncResult, true);
+
+ if (empty($syncResponse['code']) || $syncResponse['code'] != 200) {
+ return json(['code' => 500, 'msg' => '同步群信息失败:' . ($syncResponse['msg'] ?? '未知错误')]);
+ }
+
+ // 2. 同步群成员信息(调用listChatroomMember方法)
+ // wechatChatroomId 使用 s2_wechat_chatroom 表的 id(即groupId)
+ // chatroomId 使用群聊ID(chatroomId)
+ $wechatChatroomId = $groupId; // s2_wechat_chatroom表的id
+ $memberSyncResult = $chatroomController->listChatroomMember($wechatChatroomId, $chatroomId, true); // isInner = true
+ $memberSyncResponse = json_decode($memberSyncResult, true);
+
+ if (empty($memberSyncResponse['code']) || $memberSyncResponse['code'] != 200) {
+ // 成员同步失败不影响整体结果,记录警告即可
+ \think\facade\Log::warning("同步群成员失败。群ID: {$groupId}, 群聊ID: {$chatroomId}, 错误: " . ($memberSyncResponse['msg'] ?? '未知错误'));
+ }
+
+ return json([
+ 'code' => 200,
+ 'msg' => '同步成功',
+ 'data' => [
+ 'groupId' => $groupId,
+ 'chatroomId' => $chatroomId,
+ 'groupInfoSynced' => true,
+ 'memberInfoSynced' => !empty($memberSyncResponse['code']) && $memberSyncResponse['code'] == 200
+ ]
+ ]);
+ } catch (\Exception $e) {
+ \think\facade\Log::error("同步群信息异常。群ID: {$groupId}, 错误: " . $e->getMessage());
+ return json(['code' => 500, 'msg' => '同步失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 修改群名称、群公告
+ * @return \think\response\Json
+ */
+ public function modifyGroupInfo()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ $groupId = $this->request->param('groupId', 0);
+ $chatroomName = $this->request->param('chatroomName', '');
+ $announce = $this->request->param('announce', '');
+
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '工作台ID不能为空']);
+ }
+
+ if (empty($groupId)) {
+ return json(['code' => 400, 'msg' => '群ID不能为空']);
+ }
+
+ // 至少需要提供一个修改项
+ if (empty($chatroomName) && empty($announce)) {
+ return json(['code' => 400, 'msg' => '请至少提供群名称或群公告中的一个参数']);
+ }
+
+ // 查询群基本信息
+ $group = Db::table('s2_wechat_chatroom')
+ ->where('id', $groupId)
+ ->where('isDeleted', 0)
+ ->field('id,chatroomId,wechatAccountWechatId,accountId,wechatAccountId')
+ ->find();
+
+ if (empty($group)) {
+ return json(['code' => 404, 'msg' => '群不存在']);
+ }
+
+ $chatroomId = $group['id'] ?? '';
+
+ if (empty($chatroomId)) {
+ return json(['code' => 400, 'msg' => '群聊ID不存在']);
+ }
+
+ try {
+ // 直接使用群表中的账号信息
+ $executeAccountId = $group['accountId'] ?? 0;
+ $executeWechatAccountId = $group['wechatAccountId'] ?? 0;
+ $executeWechatId = $group['wechatAccountWechatId'] ?? '';
+
+ // 确保 wechatId 不为空
+ if (empty($executeWechatId)) {
+ return json(['code' => 400, 'msg' => '无法获取微信账号ID']);
+ }
+
+ // 调用 WebSocketController 修改群信息
+ // 获取系统API账号信息(用于WebSocket连接)
+ $username = Env::get('api.username2', '');
+ $password = Env::get('api.password2', '');
+
+ if (empty($username) || empty($password)) {
+ return json(['code' => 500, 'msg' => '系统API账号配置缺失']);
+ }
+
+ // 获取系统账号ID
+ $systemAccountId = Db::name('users')->where('account', $username)->value('s2_accountId');
+ if (empty($systemAccountId)) {
+ return json(['code' => 500, 'msg' => '未找到系统账号ID']);
+ }
+
+ $webSocketController = new WebSocketController([
+ 'userName' => $username,
+ 'password' => $password,
+ 'accountId' => $systemAccountId
+ ]);
+ // 构建修改参数
+ $modifyData = [
+ 'wechatChatroomId' => $chatroomId,
+ 'wechatAccountId' => $executeWechatAccountId,
+ ];
+ if (!empty($chatroomName)) {
+ $modifyData['chatroomName'] = $chatroomName;
+ }
+ if (!empty($announce)) {
+ $modifyData['announce'] = $announce;
+ }
+
+
+ $modifyResult = $webSocketController->CmdChatroomModifyInfo($modifyData);
+ $modifyResponse = json_decode($modifyResult, true);
+ if (empty($modifyResponse['code']) || $modifyResponse['code'] != 200) {
+ return json(['code' => 500, 'msg' => '修改群信息失败:' . ($modifyResponse['msg'] ?? '未知错误')]);
+ }
+
+ // 修改成功后更新数据库
+ $updateData = [
+ 'updateTime' => time()
+ ];
+
+ // 如果修改了群名称,更新数据库
+ if (!empty($chatroomName)) {
+ $updateData['nickname'] = $chatroomName;
+ }
+
+ // 如果修改了群公告,更新数据库
+ if (!empty($announce)) {
+ $updateData['announce'] = $announce;
+ }
+
+ // 更新数据库
+ Db::table('s2_wechat_chatroom')
+ ->where('id', $groupId)
+ ->update($updateData);
+
+ return json([
+ 'code' => 200,
+ 'msg' => '修改成功',
+ 'data' => [
+ 'groupId' => $groupId,
+ 'chatroomId' => $chatroomId,
+ 'chatroomName' => $chatroomName,
+ 'announce' => $announce
+ ]
+ ]);
+ } catch (\Exception $e) {
+ \think\facade\Log::error("修改群信息异常。群ID: {$groupId}, 错误: " . $e->getMessage());
+ return json(['code' => 500, 'msg' => '修改失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 转移群聊到指定账号
+ * @param int $groupId 群ID(s2_wechat_chatroom表的id)
+ * @param string $chatroomId 群聊ID
+ * @param int $toAccountId 目标账号ID(s2_company_account表的id)
+ * @param int $toWechatAccountId 目标微信账号ID(s2_wechat_account表的id)
+ * @return array ['success' => bool, 'msg' => string]
+ */
+ protected function transferChatroomToAccount($groupId, $chatroomId, $toAccountId, $toWechatAccountId)
+ {
+ try {
+ // 查询目标账号信息
+ $targetAccount = Db::table('s2_company_account')
+ ->where('id', $toAccountId)
+ ->field('id,userName,realName,nickname')
+ ->find();
+
+ if (empty($targetAccount)) {
+ return ['success' => false, 'msg' => '目标账号不存在'];
+ }
+
+ // 查询目标微信账号信息
+ $targetWechatAccount = Db::table('s2_wechat_account')
+ ->where('id', $toWechatAccountId)
+ ->field('id,wechatId,deviceAccountId')
+ ->find();
+
+ if (empty($targetWechatAccount)) {
+ return ['success' => false, 'msg' => '目标微信账号不存在'];
+ }
+
+ // 调用 AutomaticAssign 进行群聊转移
+ $automaticAssign = new \app\api\controller\AutomaticAssign();
+
+ // 构建转移参数(通过 API 调用)
+ $transferData = [
+ 'wechatChatroomId' => $chatroomId, // 使用群聊ID
+ 'toAccountId' => $toAccountId,
+ 'wechatAccountKeyword' => $targetWechatAccount['wechatId'] ?? '',
+ 'isDeleted' => false
+ ];
+
+ // 直接更新数据库(因为 API 可能不支持指定单个群聊ID转移)
+ // 更新 s2_wechat_chatroom 表的 accountId 和 wechatAccountId
+ Db::table('s2_wechat_chatroom')
+ ->where('id', $groupId)
+ ->update([
+ 'accountId' => $toAccountId,
+ 'accountUserName' => $targetAccount['userName'] ?? '',
+ 'accountRealName' => $targetAccount['realName'] ?? '',
+ 'accountNickname' => $targetAccount['nickname'] ?? '',
+ 'wechatAccountId' => $toWechatAccountId,
+ 'wechatAccountWechatId' => $targetWechatAccount['wechatId'] ?? '',
+ 'updateTime' => time()
+ ]);
+
+ return ['success' => true, 'msg' => '转移成功'];
+ } catch (\Exception $e) {
+ \think\facade\Log::error("转移群聊异常。群ID: {$groupId}, 目标账号ID: {$toAccountId}, 错误: " . $e->getMessage());
+ return ['success' => false, 'msg' => $e->getMessage()];
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/workbench/WorkbenchGroupCreateController.php b/application/cunkebao/controller/workbench/WorkbenchGroupCreateController.php
new file mode 100644
index 0000000..8057731
--- /dev/null
+++ b/application/cunkebao/controller/workbench/WorkbenchGroupCreateController.php
@@ -0,0 +1,3895 @@
+request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ // 获取登录用户信息
+ $userInfo = request()->userInfo;
+
+ // 获取请求参数
+ $param = $this->request->post();
+
+
+ // 根据业务默认值补全参数
+ if (
+ isset($param['type']) &&
+ intval($param['type']) === self::TYPE_GROUP_PUSH
+ ) {
+ if (empty($param['startTime'])) {
+ $param['startTime'] = '09:00';
+ }
+ if (empty($param['endTime'])) {
+ $param['endTime'] = '21:00';
+ }
+ }
+
+ // 验证数据
+ $validate = new WorkbenchValidate;
+ if (!$validate->scene('create')->check($param)) {
+ return json(['code' => 400, 'msg' => $validate->getError()]);
+ }
+
+ Db::startTrans();
+ try {
+ // 创建工作台基本信息
+ $workbench = new Workbench;
+ $workbench->name = $param['name'];
+ $workbench->type = $param['type'];
+ $workbench->status = !empty($param['status']) ? 1 : 0;
+ $workbench->autoStart = !empty($param['autoStart']) ? 1 : 0;
+ $workbench->userId = $userInfo['id'];
+ $workbench->companyId = $userInfo['companyId'];
+ $workbench->createTime = time();
+ $workbench->updateTime = time();
+ $workbench->save();
+
+ // 根据类型创建对应的配置
+ switch ($param['type']) {
+ case self::TYPE_AUTO_LIKE: // 自动点赞
+ $config = new WorkbenchAutoLike;
+ $config->workbenchId = $workbench->id;
+ $config->interval = $param['interval'];
+ $config->maxLikes = $param['maxLikes'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->contentTypes = json_encode($param['contentTypes']);
+ $config->devices = json_encode($param['deviceGroups']);
+ $config->friends = json_encode($param['wechatFriends']);
+ // $config->targetGroups = json_encode($param['targetGroups']);
+ // $config->tagOperator = $param['tagOperator'];
+ $config->friendMaxLikes = $param['friendMaxLikes'];
+ $config->friendTags = $param['friendTags'];
+ $config->enableFriendTags = $param['enableFriendTags'];
+ $config->createTime = time();
+ $config->updateTime = time();
+ $config->save();
+ break;
+ case self::TYPE_MOMENTS_SYNC: // 朋友圈同步
+ $config = new WorkbenchMomentsSync;
+ $config->workbenchId = $workbench->id;
+ $config->syncInterval = $param['syncInterval'];
+ $config->syncCount = $param['syncCount'];
+ $config->syncType = $param['syncType'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->accountType = $param['accountType'];
+ $config->devices = json_encode($param['deviceGroups']);
+ $config->contentLibraries = json_encode($param['contentGroups'] ?? []);
+ $config->createTime = time();
+ $config->updateTime = time();
+ $config->save();
+ break;
+ case self::TYPE_GROUP_PUSH: // 群消息推送
+ $ownerWechatIds = $this->normalizeOwnerWechatIds($param['ownerWechatIds'] ?? []);
+ $groupPushData = $this->prepareGroupPushData($param, $ownerWechatIds);
+ $groupPushData['workbenchId'] = $workbench->id;
+ $groupPushData['createTime'] = time();
+ $groupPushData['updateTime'] = time();
+ $config = new WorkbenchGroupPush;
+ $config->save($groupPushData);
+ break;
+ case self::TYPE_GROUP_CREATE: // 自动建群
+ $config = new WorkbenchGroupCreate;
+ $config->workbenchId = $workbench->id;
+ $config->planType = !empty($param['planType']) ? $param['planType'] : 0;
+ $config->executorId = !empty($param['executorId']) ? $param['executorId'] : 0;
+
+ $config->devices = json_encode($param['deviceGroups'] ?? [], JSON_UNESCAPED_UNICODE);
+ $config->startTime = $param['startTime'] ?? '';
+ $config->endTime = $param['endTime'] ?? '';
+ $config->groupSizeMin = intval($param['groupSizeMin'] ?? 3);
+ $config->groupSizeMax = intval($param['groupSizeMax'] ?? 38);
+ $config->maxGroupsPerDay = intval($param['maxGroupsPerDay'] ?? 20);
+ $config->groupNameTemplate = $param['groupNameTemplate'] ?? '';
+ $config->groupDescription = $param['groupDescription'] ?? '';
+ $config->poolGroups = json_encode($param['poolGroups'] ?? [], JSON_UNESCAPED_UNICODE);
+ $config->wechatGroups = json_encode($param['wechatGroups'] ?? [], JSON_UNESCAPED_UNICODE);
+
+ // 处理群管理员:如果启用了群管理员且有指定管理员,则保存到admins字段
+ $admins = [];
+ if (!empty($param['groupAdminEnabled']) && !empty($param['groupAdminWechatId'])) {
+ // 如果groupAdminWechatId是数组,取第一个;如果是单个值,直接使用
+ $adminWechatId = is_array($param['groupAdminWechatId']) ? $param['groupAdminWechatId'][0] : $param['groupAdminWechatId'];
+ // 如果是好友ID,直接添加到admins;如果是wechatId,需要转换为好友ID
+ if (is_numeric($adminWechatId)) {
+ $admins[] = intval($adminWechatId);
+ } else {
+ // 如果是wechatId字符串,需要查询对应的好友ID
+ $friend = Db::table('s2_wechat_friend')->where('wechatId', $adminWechatId)->find();
+ if ($friend) {
+ $admins[] = intval($friend['id']);
+ }
+ }
+ }
+ // 如果传入了admins参数,优先使用(兼容旧逻辑)
+ if (!empty($param['admins']) && is_array($param['admins'])) {
+ $admins = array_merge($admins, $param['admins']);
+ }
+ $config->admins = json_encode(array_unique($admins), JSON_UNESCAPED_UNICODE);
+
+ $config->fixedWechatIds = json_encode($param['fixedWechatIds'] ?? [], JSON_UNESCAPED_UNICODE);
+ $config->createTime = time();
+ $config->updateTime = time();
+ $config->save();
+ break;
+ case self::TYPE_TRAFFIC_DISTRIBUTION: // 流量分发
+ $config = new WorkbenchTrafficConfig;
+ $config->workbenchId = $workbench->id;
+ $config->distributeType = $param['distributeType'];
+ $config->maxPerDay = $param['maxPerDay'];
+ $config->timeType = $param['timeType'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->devices = json_encode($param['deviceGroups'], JSON_UNESCAPED_UNICODE);
+ $config->pools = json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE);
+ $config->account = json_encode($param['accountGroups'], JSON_UNESCAPED_UNICODE);
+ $config->createTime = time();
+ $config->updateTime = time();
+ $config->save();
+ break;
+ case self::TYPE_IMPORT_CONTACT: //联系人导入
+ $config = new WorkbenchImportContact;
+ $config->workbenchId = $workbench->id;
+ $config->devices = json_encode($param['deviceGroups'], JSON_UNESCAPED_UNICODE);
+ $config->pools = json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE);
+ $config->num = $param['num'];
+ $config->clearContact = $param['clearContact'];
+ $config->remark = $param['remark'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->createTime = time();
+ $config->save();
+ break;
+ }
+
+ Db::commit();
+ return json(['code' => 200, 'msg' => '创建成功', 'data' => ['id' => $workbench->id]]);
+ } catch (\Exception $e) {
+ Db::rollback();
+ return json(['code' => 500, 'msg' => '创建失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 获取工作台列表
+ * @return \think\response\Json
+ */
+ public function getList()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $type = $this->request->param('type', '');
+ $keyword = $this->request->param('keyword', '');
+
+ $where = [
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['isDel', '=', 0]
+ ];
+
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+
+
+ // 添加类型筛选
+ if ($type !== '') {
+ $where[] = ['type', '=', $type];
+ }
+
+ // 添加名称模糊搜索
+ if ($keyword !== '') {
+ $where[] = ['name', 'like', '%' . $keyword . '%'];
+ }
+
+ // 定义关联关系
+ $with = [
+ 'autoLike' => function ($query) {
+ $query->field('workbenchId,interval,maxLikes,startTime,endTime,contentTypes,devices,friends');
+ },
+ 'momentsSync' => function ($query) {
+ $query->field('workbenchId,syncInterval,syncCount,syncType,startTime,endTime,accountType,devices,contentLibraries');
+ },
+ 'trafficConfig' => function ($query) {
+ $query->field('workbenchId,distributeType,maxPerDay,timeType,startTime,endTime,devices,pools,account');
+ },
+ 'groupPush' => function ($query) {
+ $query->field('workbenchId,pushType,targetType,groupPushSubType,startTime,endTime,maxPerDay,pushOrder,isLoop,status,groups,friends,ownerWechatIds,trafficPools,contentLibraries,friendIntervalMin,friendIntervalMax,messageIntervalMin,messageIntervalMax,isRandomTemplate,postPushTags,announcementContent,enableAiRewrite,aiRewritePrompt');
+ },
+ 'groupCreate' => function ($query) {
+ $query->field('workbenchId,devices,startTime,endTime,groupSizeMin,groupSizeMax,maxGroupsPerDay,groupNameTemplate,groupDescription,poolGroups,wechatGroups,admins');
+ },
+ 'importContact' => function ($query) {
+ $query->field('workbenchId,devices,pools,num,remarkType,remark,clearContact,startTime,endTime');
+ },
+ 'user' => function ($query) {
+ $query->field('id,username');
+ }
+ ];
+
+ $list = Workbench::where($where)
+ ->with($with)
+ ->field('id,companyId,name,type,status,autoStart,userId,createTime,updateTime')
+ ->order('id', 'desc')
+ ->page($page, $limit)
+ ->select()
+ ->each(function ($item) {
+ // 处理配置信息
+ switch ($item->type) {
+ case self::TYPE_AUTO_LIKE:
+ if (!empty($item->autoLike)) {
+ $item->config = $item->autoLike;
+ $item->config->devices = json_decode($item->config->devices, true);
+ $item->config->contentTypes = json_decode($item->config->contentTypes, true);
+ $item->config->friends = json_decode($item->config->friends, true);
+
+ // 添加今日点赞数
+ $startTime = strtotime(date('Y-m-d') . ' 00:00:00');
+ $endTime = strtotime(date('Y-m-d') . ' 23:59:59');
+ $todayLikeCount = Db::name('workbench_auto_like_item')
+ ->where('workbenchId', $item->id)
+ ->whereTime('createTime', 'between', [$startTime, $endTime])
+ ->count();
+
+ // 添加总点赞数
+ $totalLikeCount = Db::name('workbench_auto_like_item')
+ ->where('workbenchId', $item->id)
+ ->count();
+
+ $item->config->todayLikeCount = $todayLikeCount;
+ $item->config->totalLikeCount = $totalLikeCount;
+ }
+ unset($item->autoLike, $item->auto_like);
+ break;
+ case self::TYPE_MOMENTS_SYNC:
+ if (!empty($item->momentsSync)) {
+ $item->config = $item->momentsSync;
+ $item->config->devices = json_decode($item->config->devices, true);
+ $item->config->contentGroups = json_decode($item->config->contentLibraries, true);
+ //同步记录
+ $sendNum = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $item->id])->count();
+ $item->syncCount = $sendNum;
+ $lastTime = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $item->id])->order('id DESC')->value('createTime');
+ $item->lastSyncTime = !empty($lastTime) ? date('Y-m-d H:i', $lastTime) : '--';
+
+
+ // 获取内容库名称
+ if (!empty($item->config->contentGroups)) {
+ $libraryNames = ContentLibrary::where('id', 'in', $item->config->contentGroups)->select();
+ $item->config->contentGroupsOptions = $libraryNames;
+ } else {
+ $item->config->contentGroupsOptions = [];
+ }
+ }
+ unset($item->momentsSync, $item->moments_sync, $item->config->contentLibraries);
+ break;
+ case self::TYPE_GROUP_PUSH:
+ if (!empty($item->groupPush)) {
+ $item->config = $item->groupPush;
+ $item->config->pushType = $item->config->pushType;
+ $item->config->targetType = isset($item->config->targetType) ? intval($item->config->targetType) : 1; // 默认1=群推送
+ $item->config->groupPushSubType = isset($item->config->groupPushSubType) ? intval($item->config->groupPushSubType) : 1; // 默认1=群群发
+ $item->config->startTime = $item->config->startTime;
+ $item->config->endTime = $item->config->endTime;
+ $item->config->maxPerDay = $item->config->maxPerDay;
+ $item->config->pushOrder = $item->config->pushOrder;
+ $item->config->isLoop = $item->config->isLoop;
+ $item->config->status = $item->config->status;
+ $item->config->ownerWechatIds = json_decode($item->config->ownerWechatIds ?? '[]', true) ?: [];
+ // 根据targetType解析不同的数据
+ if ($item->config->targetType == 1) {
+ // 群推送
+ $item->config->wechatGroups = json_decode($item->config->groups, true) ?: [];
+ $item->config->wechatFriends = [];
+ // 群推送不需要devices字段
+ // 群公告相关字段
+ if ($item->config->groupPushSubType == 2) {
+ $item->config->announcementContent = isset($item->config->announcementContent) ? $item->config->announcementContent : '';
+ $item->config->enableAiRewrite = isset($item->config->enableAiRewrite) ? intval($item->config->enableAiRewrite) : 0;
+ $item->config->aiRewritePrompt = isset($item->config->aiRewritePrompt) ? $item->config->aiRewritePrompt : '';
+ }
+ $item->config->trafficPools = [];
+ } else {
+ // 好友推送
+ $item->config->wechatFriends = json_decode($item->config->friends, true) ?: [];
+ $item->config->wechatGroups = [];
+ $item->config->trafficPools = json_decode($item->config->trafficPools ?? '[]', true) ?: [];
+ }
+ $item->config->contentLibraries = json_decode($item->config->contentLibraries, true);
+ $item->config->postPushTags = json_decode($item->config->postPushTags ?? '[]', true) ?: [];
+ $item->config->lastPushTime = '';
+ if (!empty($item->config->ownerWechatIds)) {
+ $ownerWechatOptions = Db::name('wechat_account')
+ ->whereIn('id', $item->config->ownerWechatIds)
+ ->field('id,wechatId,nickName,avatar,alias')
+ ->select();
+ $item->config->ownerWechatOptions = $ownerWechatOptions;
+ } else {
+ $item->config->ownerWechatOptions = [];
+ }
+ }
+ unset($item->groupPush, $item->group_push);
+ break;
+ case self::TYPE_GROUP_CREATE:
+ if (!empty($item->groupCreate)) {
+ $item->config = $item->groupCreate;
+ $item->config->devices = json_decode($item->config->devices, true);
+ $item->config->poolGroups = json_decode($item->config->poolGroups, true);
+ $item->config->wechatGroups = json_decode($item->config->wechatGroups, true);
+ $item->config->admins = json_decode($item->config->admins ?? '[]', true) ?: [];
+
+ // 处理群管理员相关字段
+ $item->config->groupAdminEnabled = !empty($item->config->admins) ? 1 : 0;
+
+ if (!empty($item->config->admins)) {
+ $adminOptions = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->where('wf.id', 'in', $item->config->admins)
+ ->order('wf.id', 'desc')
+ ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar')
+ ->select();
+ $item->config->adminsOptions = $adminOptions;
+ // 如果有管理员,设置groupAdminWechatId为第一个管理员的ID(用于前端回显)
+ $item->config->groupAdminWechatId = !empty($item->config->admins) ? $item->config->admins[0] : null;
+ } else {
+ $item->config->adminsOptions = [];
+ $item->config->groupAdminWechatId = null;
+ }
+ }
+ unset($item->groupCreate, $item->group_create);
+ break;
+ case self::TYPE_TRAFFIC_DISTRIBUTION:
+ if (!empty($item->trafficConfig)) {
+ $item->config = $item->trafficConfig;
+ $item->config->devices = json_decode($item->config->devices, true);
+ $item->config->poolGroups = json_decode($item->config->pools, true);
+ $item->config->account = json_decode($item->config->account, true);
+ $config_item = Db::name('workbench_traffic_config_item')->where(['workbenchId' => $item->id])->order('id DESC')->find();
+ $item->config->lastUpdated = !empty($config_item) ? date('Y-m-d H:i', $config_item['createTime']) : '--';
+
+ //统计
+ $labels = $item->config->poolGroups;
+ $totalUsers = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->where([
+ ['wf.isDeleted', '=', 0],
+ ['sa.departmentId', '=', $item->companyId]
+ ])
+ ->whereIn('wa.currentDeviceId', $item->config->devices);
+
+ if (!empty($labels) && count($labels) > 0) {
+ $totalUsers = $totalUsers->where(function ($q) use ($labels) {
+ foreach ($labels as $label) {
+ $q->whereOrRaw("JSON_CONTAINS(wf.labels, '\"{$label}\"')");
+ }
+ });
+ }
+
+ $totalUsers = $totalUsers->count();
+ $totalAccounts = count($item->config->account);
+ $dailyAverage = Db::name('workbench_traffic_config_item')
+ ->where('workbenchId', $item->id)
+ ->count();
+ $day = (time() - strtotime($item->createTime)) / 86400;
+ $day = intval($day);
+ if ($dailyAverage > 0 && $totalAccounts > 0 && $day > 0) {
+ $dailyAverage = $dailyAverage / $totalAccounts / $day;
+ }
+ $item->config->total = [
+ 'dailyAverage' => intval($dailyAverage),
+ 'totalAccounts' => $totalAccounts,
+ 'deviceCount' => count($item->config->devices),
+ 'poolCount' => !empty($item->config->poolGroups) ? count($item->config->poolGroups) : 'ALL',
+ 'totalUsers' => $totalUsers >> 0
+ ];
+ }
+ unset($item->trafficConfig, $item->traffic_config);
+ break;
+
+ case self::TYPE_IMPORT_CONTACT:
+ if (!empty($item->importContact)) {
+ $item->config = $item->importContact;
+ $item->config->devices = json_decode($item->config->devices, true);
+ $item->config->poolGroups = json_decode($item->config->pools, true);
+ }
+ unset($item->importContact, $item->import_contact);
+ break;
+ }
+ // 添加创建人名称
+ $item['creatorName'] = $item->user ? $item->user->username : '';
+ unset($item['user']); // 移除关联数据
+ return $item;
+ });
+
+ $total = Workbench::where($where)->count();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]);
+ }
+
+ /**
+ * 获取工作台详情
+ * @param int $id 工作台ID
+ * @return \think\response\Json
+ */
+ public function detail()
+ {
+ $id = $this->request->param('id', '');
+
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ // 定义关联关系
+ $with = [
+ 'autoLike' => function ($query) {
+ $query->field('workbenchId,interval,maxLikes,startTime,endTime,contentTypes,devices,friends,friendMaxLikes,friendTags,enableFriendTags');
+ },
+ 'momentsSync' => function ($query) {
+ $query->field('workbenchId,syncInterval,syncCount,syncType,startTime,endTime,accountType,devices,contentLibraries');
+ },
+ 'trafficConfig' => function ($query) {
+ $query->field('workbenchId,distributeType,maxPerDay,timeType,startTime,endTime,devices,pools,account');
+ },
+ 'groupPush' => function ($query) {
+ $query->field('workbenchId,pushType,targetType,groupPushSubType,startTime,endTime,maxPerDay,pushOrder,isLoop,status,groups,friends,ownerWechatIds,trafficPools,contentLibraries,friendIntervalMin,friendIntervalMax,messageIntervalMin,messageIntervalMax,isRandomTemplate,postPushTags,announcementContent,enableAiRewrite,aiRewritePrompt');
+ },
+ 'groupCreate' => function ($query) {
+ $query->field('workbenchId,devices,startTime,endTime,groupSizeMin,groupSizeMax,maxGroupsPerDay,groupNameTemplate,groupDescription,poolGroups,wechatGroups,admins');
+ },
+ 'importContact' => function ($query) {
+ $query->field('workbenchId,devices,pools,num,remarkType,remark,clearContact,startTime,endTime');
+ },
+ ];
+
+ $where = [
+ ['id', '=', $id],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+
+ $workbench = Workbench::where($where)
+ ->field('id,name,type,status,autoStart,createTime,updateTime,companyId')
+ ->with($with)
+ ->find();
+
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在']);
+ }
+
+ // 处理配置信息
+ switch ($workbench->type) {
+ //自动点赞
+ case self::TYPE_AUTO_LIKE:
+ if (!empty($workbench->autoLike)) {
+ $workbench->config = $workbench->autoLike;
+ $workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
+ $workbench->config->wechatFriends = json_decode($workbench->config->friends, true);
+ $workbench->config->targetType = 2;
+ //$workbench->config->targetGroups = json_decode($workbench->config->targetGroups, true);
+ $workbench->config->contentTypes = json_decode($workbench->config->contentTypes, true);
+
+ // 添加今日点赞数
+ $startTime = strtotime(date('Y-m-d') . ' 00:00:00');
+ $endTime = strtotime(date('Y-m-d') . ' 23:59:59');
+ $todayLikeCount = Db::name('workbench_auto_like_item')
+ ->where('workbenchId', $workbench->id)
+ ->whereTime('createTime', 'between', [$startTime, $endTime])
+ ->count();
+
+ // 添加总点赞数
+ $totalLikeCount = Db::name('workbench_auto_like_item')
+ ->where('workbenchId', $workbench->id)
+ ->count();
+
+ $workbench->config->todayLikeCount = $todayLikeCount;
+ $workbench->config->totalLikeCount = $totalLikeCount;
+
+ unset($workbench->autoLike, $workbench->auto_like);
+ }
+ break;
+ //自动同步朋友圈
+ case self::TYPE_MOMENTS_SYNC:
+ if (!empty($workbench->momentsSync)) {
+ $workbench->config = $workbench->momentsSync;
+ $workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
+ $workbench->config->contentGroups = json_decode($workbench->config->contentLibraries, true);
+
+ //同步记录
+ $sendNum = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $workbench->id])->count();
+ $workbench->syncCount = $sendNum;
+ $lastTime = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $workbench->id])->order('id DESC')->value('createTime');
+ $workbench->lastSyncTime = !empty($lastTime) ? date('Y-m-d H:i', $lastTime) : '--';
+ unset($workbench->momentsSync, $workbench->moments_sync);
+ }
+ break;
+ //群推送
+ case self::TYPE_GROUP_PUSH:
+ if (!empty($workbench->groupPush)) {
+ $workbench->config = $workbench->groupPush;
+ $workbench->config->targetType = isset($workbench->config->targetType) ? intval($workbench->config->targetType) : 1; // 默认1=群推送
+ $workbench->config->groupPushSubType = isset($workbench->config->groupPushSubType) ? intval($workbench->config->groupPushSubType) : 1; // 默认1=群群发
+ $workbench->config->ownerWechatIds = json_decode($workbench->config->ownerWechatIds ?? '[]', true) ?: [];
+ // 根据targetType解析不同的数据
+ if ($workbench->config->targetType == 1) {
+ // 群推送
+ $workbench->config->wechatGroups = json_decode($workbench->config->groups, true) ?: [];
+ $workbench->config->wechatFriends = [];
+ $workbench->config->trafficPools = [];
+ // 群推送不需要devices字段
+ // 群公告相关字段
+ if ($workbench->config->groupPushSubType == 2) {
+ $workbench->config->announcementContent = isset($workbench->config->announcementContent) ? $workbench->config->announcementContent : '';
+ $workbench->config->enableAiRewrite = isset($workbench->config->enableAiRewrite) ? intval($workbench->config->enableAiRewrite) : 0;
+ $workbench->config->aiRewritePrompt = isset($workbench->config->aiRewritePrompt) ? $workbench->config->aiRewritePrompt : '';
+ }
+ } else {
+ // 好友推送
+ $workbench->config->wechatFriends = json_decode($workbench->config->friends, true) ?: [];
+ $workbench->config->wechatGroups = [];
+ $workbench->config->trafficPools = json_decode($workbench->config->trafficPools ?? '[]', true) ?: [];
+ }
+ $workbench->config->contentLibraries = json_decode($workbench->config->contentLibraries, true);
+ $workbench->config->postPushTags = json_decode($workbench->config->postPushTags ?? '[]', true) ?: [];
+ unset($workbench->groupPush, $workbench->group_push);
+ }
+ break;
+ //建群助手
+ case self::TYPE_GROUP_CREATE:
+ if (!empty($workbench->groupCreate)) {
+ $workbench->config = $workbench->groupCreate;
+ $workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
+ $workbench->config->poolGroups = json_decode($workbench->config->poolGroups, true);
+ $workbench->config->wechatGroups = json_decode($workbench->config->wechatGroups, true);
+ $workbench->config->admins = json_decode($workbench->config->admins ?? '[]', true) ?: [];
+
+ // 处理群管理员相关字段
+ $workbench->config->groupAdminEnabled = !empty($workbench->config->admins) ? 1 : 0;
+
+ // 如果有管理员,设置groupAdminWechatId为第一个管理员的ID(用于前端回显)
+ $workbench->config->groupAdminWechatId = !empty($workbench->config->admins) ? $workbench->config->admins[0] : null;
+
+ // 统计已建群数(状态为成功且groupId不为空的记录,按groupId分组去重)
+ $createdGroupsCount = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbench->id)
+ ->where('status', 2) // STATUS_SUCCESS = 2
+ ->where('groupId', '<>', null)
+ ->group('groupId')
+ ->count();
+
+ // 统计总人数(该工作台的所有记录数)
+ $totalMembersCount = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbench->id)
+ ->count();
+
+ // 添加统计信息
+ $workbench->config->stats = [
+ 'createdGroupsCount' => $createdGroupsCount,
+ 'totalMembersCount' => $totalMembersCount
+ ];
+
+ unset($workbench->groupCreate, $workbench->group_create);
+ }
+ break;
+ //流量分发
+ case self::TYPE_TRAFFIC_DISTRIBUTION:
+ if (!empty($workbench->trafficConfig)) {
+ $workbench->config = $workbench->trafficConfig;
+ $workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
+ $workbench->config->accountGroups = json_decode($workbench->config->account, true);
+ $workbench->config->poolGroups = json_decode($workbench->config->pools, true);
+ $config_item = Db::name('workbench_traffic_config_item')->where(['workbenchId' => $workbench->id])->order('id DESC')->find();
+ $workbench->config->lastUpdated = !empty($config_item) ? date('Y-m-d H:i', $config_item['createTime']) : '--';
+
+ //统计
+ $labels = $workbench->config->poolGroups;
+ $totalUsers = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->where([
+ ['wf.isDeleted', '=', 0],
+ ['sa.departmentId', '=', $workbench->companyId]
+ ])
+ ->whereIn('wa.currentDeviceId', $workbench->config->deviceGroups)
+ ->field('wf.id,wf.wechatAccountId,wf.wechatId,wf.labels,sa.userName,wa.currentDeviceId as deviceId')
+ ->where(function ($q) use ($labels) {
+ foreach ($labels as $label) {
+ $q->whereOrRaw("JSON_CONTAINS(wf.labels, '\"{$label}\"')");
+ }
+ })->count();
+
+ $totalAccounts = Db::table('s2_company_account')
+ ->alias('a')
+ ->where(['a.departmentId' => $workbench->companyId, 'a.status' => 0])
+ ->whereNotLike('a.userName', '%_offline%')
+ ->whereNotLike('a.userName', '%_delete%')
+ ->group('a.id')
+ ->count();
+
+ $dailyAverage = Db::name('workbench_traffic_config_item')
+ ->where('workbenchId', $workbench->id)
+ ->count();
+ $day = (time() - strtotime($workbench->createTime)) / 86400;
+ $day = intval($day);
+
+
+ if ($dailyAverage > 0) {
+ $dailyAverage = $dailyAverage / $totalAccounts / $day;
+ }
+
+ $workbench->config->total = [
+ 'dailyAverage' => intval($dailyAverage),
+ 'totalAccounts' => $totalAccounts,
+ 'deviceCount' => count($workbench->config->deviceGroups),
+ 'poolCount' => count($workbench->config->poolGroups),
+ 'totalUsers' => $totalUsers >> 0
+ ];
+ unset($workbench->trafficConfig, $workbench->traffic_config);
+ }
+ break;
+ case self::TYPE_IMPORT_CONTACT:
+ if (!empty($workbench->importContact)) {
+ $workbench->config = $workbench->importContact;
+ $workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
+ $workbench->config->poolGroups = json_decode($workbench->config->pools, true);
+ }
+ unset($workbench->importContact, $workbench->import_contact);
+ break;
+ }
+ unset(
+ $workbench->autoLike,
+ $workbench->momentsSync,
+ $workbench->groupPush,
+ $workbench->groupCreate,
+ $workbench->config->devices,
+ $workbench->config->friends,
+ $workbench->config->groups,
+ $workbench->config->contentLibraries,
+ $workbench->config->account,
+ );
+
+
+ //获取设备信息
+ if (!empty($workbench->config->deviceGroups)) {
+ $deviceList = DeviceModel::alias('d')
+ ->field([
+ 'd.id', 'd.imei', 'd.memo', 'd.alive',
+ 'l.wechatId',
+ 'a.nickname', 'a.alias', 'a.avatar', 'a.alias', '0 totalFriend'
+ ])
+ ->leftJoin('device_wechat_login l', 'd.id = l.deviceId and l.alive =' . DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE . ' and l.companyId = d.companyId')
+ ->leftJoin('wechat_account a', 'l.wechatId = a.wechatId')
+ ->whereIn('d.id', $workbench->config->deviceGroups)
+ ->order('d.id desc')
+ ->select();
+
+ foreach ($deviceList as &$device) {
+ $curstomer = WechatCustomerModel::field('friendShip')->where(['wechatId' => $device['wechatId']])->find();
+ $device['totalFriend'] = $curstomer->friendShip->totalFriend ?? 0;
+ }
+ unset($device);
+
+ $workbench->config->deviceGroupsOptions = $deviceList;
+ } else {
+ $workbench->config->deviceGroupsOptions = [];
+ }
+
+
+ // 获取群(当targetType=1时)
+ if (!empty($workbench->config->wechatGroups) && isset($workbench->config->targetType) && $workbench->config->targetType == 1) {
+ $groupList = Db::name('wechat_group')->alias('wg')
+ ->join('wechat_account wa', 'wa.wechatId = wg.ownerWechatId')
+ ->where('wg.id', 'in', $workbench->config->wechatGroups)
+ ->order('wg.id', 'desc')
+ ->field('wg.id,wg.name as groupName,wg.ownerWechatId,wa.nickName,wa.avatar,wa.alias,wg.avatar as groupAvatar')
+ ->select();
+ $workbench->config->wechatGroupsOptions = $groupList;
+ } else {
+ $workbench->config->wechatGroupsOptions = [];
+ }
+
+ // 获取好友(当targetType=2时)
+ if (!empty($workbench->config->wechatFriends) && isset($workbench->config->targetType) && $workbench->config->targetType == 2) {
+ $friendList = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->where('wf.id', 'in', $workbench->config->wechatFriends)
+ ->order('wf.id', 'desc')
+ ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar')
+ ->select();
+ $workbench->config->wechatFriendsOptions = $friendList;
+ } else {
+ $workbench->config->wechatFriendsOptions = [];
+ }
+
+ // 获取流量池(当targetType=2时)
+ if (!empty($workbench->config->trafficPools) && isset($workbench->config->targetType) && $workbench->config->targetType == 2) {
+ $poolList = Db::name('traffic_source_package')->alias('tsp')
+ ->leftJoin('traffic_source_package_item tspi', 'tspi.packageId = tsp.id and tspi.isDel = 0')
+ ->whereIn('tsp.id', $workbench->config->trafficPools)
+ ->where('tsp.isDel', 0)
+ ->whereIn('tsp.companyId', [$this->request->userInfo['companyId'], 0])
+ ->field('tsp.id,tsp.name,tsp.description,tsp.pic,COUNT(tspi.id) as itemCount')
+ ->group('tsp.id')
+ ->order('tsp.id', 'desc')
+ ->select();
+ $workbench->config->trafficPoolsOptions = $poolList;
+ } else {
+ $workbench->config->trafficPoolsOptions = [];
+ }
+
+ // 获取内容库名称
+ if (!empty($workbench->config->contentGroups)) {
+ $libraryNames = ContentLibrary::where('id', 'in', $workbench->config->contentGroups)->select();
+ $workbench->config->contentGroupsOptions = $libraryNames;
+ } else {
+ $workbench->config->contentGroupsOptions = [];
+ }
+
+ //账号
+ if (!empty($workbench->config->accountGroups)) {
+ $account = Db::table('s2_company_account')->alias('a')
+ ->where(['a.departmentId' => $this->request->userInfo['companyId'], 'a.status' => 0])
+ ->whereIn('a.id', $workbench->config->accountGroups)
+ ->whereNotLike('a.userName', '%_offline%')
+ ->whereNotLike('a.userName', '%_delete%')
+ ->field('a.id,a.userName,a.realName,a.nickname,a.memo')
+ ->select();
+ $workbench->config->accountGroupsOptions = $account;
+ } else {
+ $workbench->config->accountGroupsOptions = [];
+ }
+
+ if (!empty($workbench->config->poolGroups)) {
+ $poolGroupsOptions = Db::name('traffic_source_package')->alias('tsp')
+ ->join('traffic_source_package_item tspi', 'tspi.packageId=tsp.id', 'left')
+ ->whereIn('tsp.companyId', [$this->request->userInfo['companyId'], 0])
+ ->whereIn('tsp.id', $workbench->config->poolGroups)
+ ->field('tsp.id,tsp.name,tsp.description,tsp.createTime,count(tspi.id) as num')
+ ->group('tsp.id')
+ ->select();
+ $workbench->config->poolGroupsOptions = $poolGroupsOptions;
+ } else {
+ $workbench->config->poolGroupsOptions = [];
+ }
+
+ if (!empty($workbench->config->ownerWechatIds)) {
+ $ownerWechatOptions = Db::name('wechat_account')
+ ->whereIn('id', $workbench->config->ownerWechatIds)
+ ->field('id,wechatId,nickName,avatar,alias')
+ ->select();
+ $workbench->config->ownerWechatOptions = $ownerWechatOptions;
+ } else {
+ $workbench->config->ownerWechatOptions = [];
+ }
+
+ // 获取群组选项(自动建群)
+ if ($workbench->type == self::TYPE_GROUP_CREATE && !empty($workbench->config->wechatGroups)) {
+ // 分离数字ID(好友ID)和字符串ID(手动创建的群组)
+ $friendIds = [];
+ $manualGroupIds = [];
+
+ foreach ($workbench->config->wechatGroups as $groupId) {
+ if (is_numeric($groupId)) {
+ $friendIds[] = intval($groupId);
+ } else {
+ $manualGroupIds[] = $groupId;
+ }
+ }
+
+ $wechatGroupsOptions = [];
+
+ // 查询好友信息(数字ID)
+ if (!empty($friendIds)) {
+ $friendList = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->join(['s2_company_account' => 'ca'], 'ca.id = wf.accountId', 'left')
+ ->where('wf.id', 'in', $friendIds)
+ ->order('wf.id', 'desc')
+ ->field('wf.id,wf.wechatId,wf.nickname,wf.avatar,wf.alias,wf.gender,wf.phone,wa.nickName as accountNickname,ca.userName as account,ca.realName as username,wf.createTime,wf.updateTime,wf.deleteTime,wf.ownerWechatId')
+ ->select();
+
+ // 获取群主信息
+ foreach ($friendList as &$friend) {
+ if (!empty($friend['ownerWechatId'])) {
+ $owner = Db::name('wechat_account')
+ ->where('wechatId', $friend['ownerWechatId'])
+ ->field('nickName,alias')
+ ->find();
+ $friend['ownerNickname'] = $owner['nickName'] ?? '';
+ $friend['ownerAlias'] = $owner['alias'] ?? '';
+ } else {
+ $friend['ownerNickname'] = '';
+ $friend['ownerAlias'] = '';
+ }
+ $friend['isManual'] = '';
+ // 格式化时间
+ $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['deleteTime'] = !empty($friend['deleteTime']) ? date('Y-m-d H:i:s', $friend['deleteTime']) : '';
+ }
+ unset($friend);
+
+ $wechatGroupsOptions = array_merge($wechatGroupsOptions, $friendList);
+ }
+
+ // 处理手动创建的群组(字符串ID)
+ if (!empty($manualGroupIds)) {
+ foreach ($manualGroupIds as $groupId) {
+ // 手动创建的群组,只返回基本信息
+ $wechatGroupsOptions[] = [
+ 'id' => $groupId,
+ 'wechatId' => $groupId,
+ 'nickname' => $groupId,
+ 'avatar' => '',
+ 'isManual' => 1
+ ];
+ }
+ }
+
+ $workbench->config->wechatGroupsOptions = $wechatGroupsOptions;
+ } else {
+ $workbench->config->wechatGroupsOptions = [];
+ }
+
+ // 获取管理员选项(自动建群)
+ if ($workbench->type == self::TYPE_GROUP_CREATE && !empty($workbench->config->admins)) {
+ $adminOptions = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->where('wf.id', 'in', $workbench->config->admins)
+ ->order('wf.id', 'desc')
+ ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar')
+ ->select();
+ $workbench->config->adminsOptions = $adminOptions;
+ } else {
+ $workbench->config->adminsOptions = [];
+ }
+
+ return json(['code' => 200, 'msg' => '获取成功', 'data' => $workbench]);
+ }
+
+ /**
+ * 更新工作台
+ * @return \think\response\Json
+ */
+ public function update()
+ {
+ if (!$this->request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ // 获取请求参数
+ $param = $this->request->post();
+
+ // 验证数据
+ $validate = new WorkbenchValidate;
+ if (!$validate->scene('update')->check($param)) {
+ return json(['code' => 400, 'msg' => $validate->getError()]);
+ }
+
+
+ $where = [
+ ['id', '=', $param['id']],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+ // 查询工作台是否存在
+ $workbench = Workbench::where($where)->find();
+ if (!$workbench) {
+ return json(['code' => 404, 'msg' => '工作台不存在']);
+ }
+
+ Db::startTrans();
+ try {
+ // 更新工作台基本信息
+ $workbench->name = $param['name'];
+ $workbench->status = !empty($param['status']) ? 1 : 0;
+ $workbench->autoStart = !empty($param['autoStart']) ? 1 : 0;
+ $workbench->updateTime = time();
+ $workbench->save();
+
+ // 根据类型更新对应的配置
+ switch ($workbench->type) {
+ case self::TYPE_AUTO_LIKE:
+ $config = WorkbenchAutoLike::where('workbenchId', $param['id'])->find();
+ if ($config) {
+ $config->interval = $param['interval'];
+ $config->maxLikes = $param['maxLikes'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->contentTypes = json_encode($param['contentTypes']);
+ $config->devices = json_encode($param['deviceGroups']);
+ $config->friends = json_encode($param['wechatFriends']);
+ // $config->targetGroups = json_encode($param['targetGroups']);
+ // $config->tagOperator = $param['tagOperator'];
+ $config->friendMaxLikes = $param['friendMaxLikes'];
+ $config->friendTags = $param['friendTags'];
+ $config->enableFriendTags = $param['enableFriendTags'];
+ $config->updateTime = time();
+ $config->save();
+ }
+ break;
+
+ case self::TYPE_MOMENTS_SYNC:
+ $config = WorkbenchMomentsSync::where('workbenchId', $param['id'])->find();
+ if ($config) {
+ if (!empty($param['contentGroups'])) {
+ foreach ($param['contentGroups'] as $library) {
+ if (isset($library['id']) && !empty($library['id'])) {
+ $contentLibraries[] = $library['id'];
+ } else {
+ $contentLibraries[] = $library;
+ }
+ }
+ } else {
+ $contentLibraries = [];
+ }
+
+ $config->syncInterval = $param['syncInterval'];
+ $config->syncCount = $param['syncCount'];
+ $config->syncType = $param['syncType'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->accountType = $param['accountType'];
+ $config->devices = json_encode($param['deviceGroups']);
+ $config->contentLibraries = json_encode($contentLibraries);
+ $config->updateTime = time();
+ $config->save();
+ }
+ break;
+
+ case self::TYPE_GROUP_PUSH:
+ $config = WorkbenchGroupPush::where('workbenchId', $param['id'])->find();
+ if ($config) {
+ $ownerWechatIds = $this->normalizeOwnerWechatIds($param['ownerWechatIds'] ?? null, $config);
+ $groupPushData = $this->prepareGroupPushData($param, $ownerWechatIds, $config);
+ $groupPushData['updateTime'] = time();
+ $config->save($groupPushData);
+ }
+ break;
+
+ case self::TYPE_GROUP_CREATE:
+ $config = WorkbenchGroupCreate::where('workbenchId', $param['id'])->find();
+ if ($config) {
+ $config->devices = json_encode($param['deviceGroups'] ?? [], JSON_UNESCAPED_UNICODE);
+ $config->startTime = $param['startTime'] ?? '';
+ $config->endTime = $param['endTime'] ?? '';
+ $config->groupSizeMin = intval($param['groupSizeMin'] ?? 3);
+ $config->groupSizeMax = intval($param['groupSizeMax'] ?? 38);
+ $config->maxGroupsPerDay = intval($param['maxGroupsPerDay'] ?? 20);
+ $config->groupNameTemplate = $param['groupNameTemplate'] ?? '';
+ $config->groupDescription = $param['groupDescription'] ?? '';
+ $config->poolGroups = json_encode($param['poolGroups'] ?? [], JSON_UNESCAPED_UNICODE);
+ $config->wechatGroups = json_encode($param['wechatGroups'] ?? [], JSON_UNESCAPED_UNICODE);
+
+ // 处理群管理员:如果启用了群管理员且有指定管理员,则保存到admins字段
+ $admins = [];
+ if (!empty($param['groupAdminEnabled']) && !empty($param['groupAdminWechatId'])) {
+ // 如果groupAdminWechatId是数组,取第一个;如果是单个值,直接使用
+ $adminWechatId = is_array($param['groupAdminWechatId']) ? $param['groupAdminWechatId'][0] : $param['groupAdminWechatId'];
+ // 如果是好友ID,直接添加到admins;如果是wechatId,需要转换为好友ID
+ if (is_numeric($adminWechatId)) {
+ $admins[] = intval($adminWechatId);
+ } else {
+ // 如果是wechatId字符串,需要查询对应的好友ID
+ $friend = Db::table('s2_wechat_friend')->where('wechatId', $adminWechatId)->find();
+ if ($friend) {
+ $admins[] = intval($friend['id']);
+ }
+ }
+ }
+ // 如果传入了admins参数,优先使用(兼容旧逻辑)
+ if (!empty($param['admins']) && is_array($param['admins'])) {
+ $admins = array_merge($admins, $param['admins']);
+ }
+ $config->admins = json_encode(array_unique($admins), JSON_UNESCAPED_UNICODE);
+
+ $config->updateTime = time();
+ $config->save();
+ }
+ break;
+ case self::TYPE_TRAFFIC_DISTRIBUTION:
+ $config = WorkbenchTrafficConfig::where('workbenchId', $param['id'])->find();
+ if ($config) {
+ $config->distributeType = $param['distributeType'];
+ $config->maxPerDay = $param['maxPerDay'];
+ $config->timeType = $param['timeType'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->devices = json_encode($param['deviceGroups']);
+ $config->pools = json_encode($param['poolGroups']);
+ $config->account = json_encode($param['accountGroups']);
+ $config->updateTime = time();
+ $config->save();
+ }
+ break;
+ case self::TYPE_IMPORT_CONTACT: //联系人导入
+ $config = WorkbenchImportContact::where('workbenchId', $param['id'])->find();;
+ if ($config) {
+ $config->devices = json_encode($param['deviceGroups']);
+ $config->pools = json_encode($param['poolGroups']);
+ $config->num = $param['num'];
+ $config->clearContact = $param['clearContact'];
+ $config->remark = $param['remark'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->save();
+ }
+ break;
+ }
+
+ Db::commit();
+ return json(['code' => 200, 'msg' => '更新成功']);
+ } catch (\Exception $e) {
+ Db::rollback();
+ return json(['code' => 500, 'msg' => '更新失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 更新工作台状态
+ * @return \think\response\Json
+ */
+ public function updateStatus()
+ {
+ if (!$this->request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ $id = $this->request->param('id', '');
+
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+
+
+ $where = [
+ ['id', '=', $id],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+ $workbench = Workbench::where($where)->find();
+
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在']);
+ }
+
+ $workbench->status = !$workbench['status'];
+ $workbench->save();
+
+ return json(['code' => 200, 'msg' => '更新成功']);
+ }
+
+ /**
+ * 删除工作台(软删除)
+ */
+ public function delete()
+ {
+ $id = $this->request->param('id');
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ $where = [
+ ['id', '=', $id],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+ $workbench = Workbench::where($where)->find();
+
+ if (!$workbench) {
+ return json(['code' => 404, 'msg' => '工作台不存在']);
+ }
+
+ // 软删除
+ $workbench->isDel = 1;
+ $workbench->deleteTime = time();
+ $workbench->save();
+
+ return json(['code' => 200, 'msg' => '删除成功']);
+ }
+
+ /**
+ * 拷贝工作台
+ * @return \think\response\Json
+ */
+ public function copy()
+ {
+ if (!$this->request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ $id = $this->request->post('id');
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ // 验证权限并获取原数据
+ $workbench = Workbench::where([
+ ['id', '=', $id],
+ ['userId', '=', $this->request->userInfo['id']]
+ ])->find();
+
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在']);
+ }
+
+ Db::startTrans();
+ try {
+ // 创建新的工作台基本信息
+ $newWorkbench = new Workbench;
+ $newWorkbench->name = $workbench->name . ' copy';
+ $newWorkbench->type = $workbench->type;
+ $newWorkbench->status = 1; // 新拷贝的默认启用
+ $newWorkbench->autoStart = $workbench->autoStart;
+ $newWorkbench->userId = $this->request->userInfo['id'];
+ $newWorkbench->companyId = $this->request->userInfo['companyId'];
+ $newWorkbench->save();
+
+ // 根据类型拷贝对应的配置
+ switch ($workbench->type) {
+ case self::TYPE_AUTO_LIKE:
+ $config = WorkbenchAutoLike::where('workbenchId', $id)->find();
+ if ($config) {
+ $newConfig = new WorkbenchAutoLike;
+ $newConfig->workbenchId = $newWorkbench->id;
+ $newConfig->interval = $config->interval;
+ $newConfig->maxLikes = $config->maxLikes;
+ $newConfig->startTime = $config->startTime;
+ $newConfig->endTime = $config->endTime;
+ $newConfig->contentTypes = $config->contentTypes;
+ $newConfig->devices = $config->devices;
+ $newConfig->friends = $config->friends;
+ $newConfig->createTime = time();
+ $newConfig->updateTime = time();
+ $newConfig->save();
+ }
+ break;
+ case self::TYPE_MOMENTS_SYNC:
+ $config = WorkbenchMomentsSync::where('workbenchId', $id)->find();
+ if ($config) {
+ $newConfig = new WorkbenchMomentsSync;
+ $newConfig->workbenchId = $newWorkbench->id;
+ $newConfig->syncInterval = $config->syncInterval;
+ $newConfig->syncCount = $config->syncCount;
+ $newConfig->syncType = $config->syncType;
+ $newConfig->startTime = $config->startTime;
+ $newConfig->endTime = $config->endTime;
+ $newConfig->accountType = $config->accountType;
+ $newConfig->devices = $config->devices;
+ $newConfig->contentLibraries = $config->contentLibraries;
+ $newConfig->createTime = time();
+ $newConfig->updateTime = time();
+ $newConfig->save();
+ }
+ break;
+ case self::TYPE_GROUP_PUSH:
+ $config = WorkbenchGroupPush::where('workbenchId', $id)->find();
+ if ($config) {
+ $newConfig = new WorkbenchGroupPush;
+ $newConfig->workbenchId = $newWorkbench->id;
+ $newConfig->pushType = $config->pushType;
+ $newConfig->targetType = isset($config->targetType) ? $config->targetType : 1; // 默认1=群推送
+ $newConfig->startTime = $config->startTime;
+ $newConfig->endTime = $config->endTime;
+ $newConfig->maxPerDay = $config->maxPerDay;
+ $newConfig->pushOrder = $config->pushOrder;
+ $newConfig->isLoop = $config->isLoop;
+ $newConfig->status = $config->status;
+ $newConfig->groups = $config->groups;
+ $newConfig->friends = $config->friends;
+ $newConfig->contentLibraries = $config->contentLibraries;
+ $newConfig->trafficPools = property_exists($config, 'trafficPools') ? $config->trafficPools : json_encode([], JSON_UNESCAPED_UNICODE);
+ $newConfig->socialMediaId = $config->socialMediaId;
+ $newConfig->promotionSiteId = $config->promotionSiteId;
+ $newConfig->ownerWechatIds = $config->ownerWechatIds;
+ $newConfig->createTime = time();
+ $newConfig->updateTime = time();
+ $newConfig->save();
+ }
+ break;
+ case self::TYPE_GROUP_CREATE:
+ $config = WorkbenchGroupCreate::where('workbenchId', $id)->find();
+ if ($config) {
+ $newConfig = new WorkbenchGroupCreate;
+ $newConfig->workbenchId = $newWorkbench->id;
+ $newConfig->devices = $config->devices;
+ $newConfig->startTime = $config->startTime;
+ $newConfig->endTime = $config->endTime;
+ $newConfig->groupSizeMin = $config->groupSizeMin;
+ $newConfig->groupSizeMax = $config->groupSizeMax;
+ $newConfig->maxGroupsPerDay = $config->maxGroupsPerDay;
+ $newConfig->groupNameTemplate = $config->groupNameTemplate;
+ $newConfig->groupDescription = $config->groupDescription;
+ $newConfig->poolGroups = $config->poolGroups;
+ $newConfig->wechatGroups = $config->wechatGroups;
+ $newConfig->admins = $config->admins ?? json_encode([], JSON_UNESCAPED_UNICODE);
+ $newConfig->createTime = time();
+ $newConfig->updateTime = time();
+ $newConfig->save();
+ }
+ break;
+ case self::TYPE_IMPORT_CONTACT: //联系人导入
+ $config = WorkbenchImportContact::where('workbenchId', $id)->find();
+ if ($config) {
+ $newConfig = new WorkbenchImportContact;
+ $newConfig->workbenchId = $newWorkbench->id;
+ $newConfig->devices = $config->devices;
+ $newConfig->pools = $config->pools;
+ $newConfig->num = $config->num;
+ $newConfig->clearContact = $config->clearContact;
+ $newConfig->remark = $config->remark;
+ $newConfig->startTime = $config->startTime;
+ $newConfig->endTime = $config->endTime;
+ $newConfig->createTime = time();
+ $newConfig->save();
+ }
+ break;
+ }
+
+ Db::commit();
+ return json(['code' => 200, 'msg' => '拷贝成功', 'data' => ['id' => $newWorkbench->id]]);
+ } catch (\Exception $e) {
+ Db::rollback();
+ return json(['code' => 500, 'msg' => '拷贝失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 获取点赞记录列表
+ * @return \think\response\Json
+ */
+ public function getLikeRecords()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchAutoLikeController();
+ $controller->request = $this->request;
+ return $controller->getLikeRecords();
+ }
+
+ /**
+ * 获取朋友圈发布记录列表
+ * @return \think\response\Json
+ */
+ public function getMomentsRecords()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchMomentsController();
+ $controller->request = $this->request;
+ return $controller->getMomentsRecords();
+ }
+
+ /**
+ * 获取朋友圈发布统计
+ * @return \think\response\Json
+ */
+ public function getMomentsStats()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchMomentsController();
+ $controller->request = $this->request;
+ return $controller->getMomentsStats();
+ }
+
+ /**
+ * 获取流量分发记录列表
+ * @return \think\response\Json
+ */
+ public function getTrafficDistributionRecords()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $workbenchId = $this->request->param('workbenchId', 0);
+
+ $where = [
+ ['wtdi.workbenchId', '=', $workbenchId]
+ ];
+
+ // 查询分发记录
+ $list = Db::name('workbench_traffic_distribution_item')->alias('wtdi')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wtdi.wechatAccountId', 'left')
+ ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wtdi.wechatFriendId', 'left')
+ ->field([
+ 'wtdi.id',
+ 'wtdi.workbenchId',
+ 'wtdi.wechatAccountId',
+ 'wtdi.wechatFriendId',
+ 'wtdi.createTime as distributeTime',
+ 'wtdi.status',
+ 'wtdi.errorMsg',
+ 'wa.nickName as operatorName',
+ 'wa.avatar as operatorAvatar',
+ 'wf.nickName as friendName',
+ 'wf.avatar as friendAvatar',
+ 'wf.gender',
+ 'wf.province',
+ 'wf.city'
+ ])
+ ->where($where)
+ ->order('wtdi.createTime', 'desc')
+ ->page($page, $limit)
+ ->select();
+
+ // 处理数据
+ foreach ($list as &$item) {
+ // 处理时间格式
+ $item['distributeTime'] = date('Y-m-d H:i:s', $item['distributeTime']);
+
+ // 处理性别
+ $genderMap = [
+ 0 => '未知',
+ 1 => '男',
+ 2 => '女'
+ ];
+ $item['genderText'] = $genderMap[$item['gender']] ?? '未知';
+
+ // 处理状态文字
+ $statusMap = [
+ 0 => '待分发',
+ 1 => '分发成功',
+ 2 => '分发失败'
+ ];
+ $item['statusText'] = $statusMap[$item['status']] ?? '未知状态';
+ }
+
+ // 获取总记录数
+ $total = Db::name('workbench_traffic_distribution_item')->alias('wtdi')
+ ->where($where)
+ ->count();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]);
+ }
+
+ /**
+ * 获取流量分发统计
+ * @return \think\response\Json
+ */
+ public function getTrafficDistributionStats()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ // 获取今日数据
+ $todayStart = strtotime(date('Y-m-d') . ' 00:00:00');
+ $todayEnd = strtotime(date('Y-m-d') . ' 23:59:59');
+
+ $todayStats = Db::name('workbench_traffic_distribution_item')
+ ->where([
+ ['workbenchId', '=', $workbenchId],
+ ['createTime', 'between', [$todayStart, $todayEnd]]
+ ])
+ ->field([
+ 'COUNT(*) as total',
+ 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success',
+ 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed'
+ ])
+ ->find();
+
+ // 获取总数据
+ $totalStats = Db::name('workbench_traffic_distribution_item')
+ ->where('workbenchId', $workbenchId)
+ ->field([
+ 'COUNT(*) as total',
+ 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success',
+ 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed'
+ ])
+ ->find();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'today' => [
+ 'total' => intval($todayStats['total']),
+ 'success' => intval($todayStats['success']),
+ 'failed' => intval($todayStats['failed'])
+ ],
+ 'total' => [
+ 'total' => intval($totalStats['total']),
+ 'success' => intval($totalStats['success']),
+ 'failed' => intval($totalStats['failed'])
+ ]
+ ]
+ ]);
+ }
+
+ /**
+ * 获取流量分发详情
+ * @return \think\response\Json
+ */
+ public function getTrafficDistributionDetail()
+ {
+ $id = $this->request->param('id', 0);
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ $detail = Db::name('workbench_traffic_distribution_item')->alias('wtdi')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wtdi.wechatAccountId', 'left')
+ ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wtdi.wechatFriendId', 'left')
+ ->field([
+ 'wtdi.id',
+ 'wtdi.workbenchId',
+ 'wtdi.wechatAccountId',
+ 'wtdi.wechatFriendId',
+ 'wtdi.createTime as distributeTime',
+ 'wtdi.status',
+ 'wtdi.errorMsg',
+ 'wa.nickName as operatorName',
+ 'wa.avatar as operatorAvatar',
+ 'wf.nickName as friendName',
+ 'wf.avatar as friendAvatar',
+ 'wf.gender',
+ 'wf.province',
+ 'wf.city',
+ 'wf.signature',
+ 'wf.remark'
+ ])
+ ->where('wtdi.id', $id)
+ ->find();
+
+ if (empty($detail)) {
+ return json(['code' => 404, 'msg' => '记录不存在']);
+ }
+
+ // 处理数据
+ $detail['distributeTime'] = date('Y-m-d H:i:s', $detail['distributeTime']);
+
+ // 处理性别
+ $genderMap = [
+ 0 => '未知',
+ 1 => '男',
+ 2 => '女'
+ ];
+ $detail['genderText'] = $genderMap[$detail['gender']] ?? '未知';
+
+ // 处理状态文字
+ $statusMap = [
+ 0 => '待分发',
+ 1 => '分发成功',
+ 2 => '分发失败'
+ ];
+ $detail['statusText'] = $statusMap[$detail['status']] ?? '未知状态';
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => $detail
+ ]);
+ }
+
+ /**
+ * 创建流量分发计划
+ * @return \think\response\Json
+ */
+ public function createTrafficPlan()
+ {
+ $param = $this->request->post();
+ Db::startTrans();
+ try {
+ // 1. 创建主表
+ $planId = Db::name('ck_workbench')->insertGetId([
+ 'name' => $param['name'],
+ 'type' => self::TYPE_TRAFFIC_DISTRIBUTION,
+ 'status' => 1,
+ 'autoStart' => $param['autoStart'] ?? 0,
+ 'userId' => $this->request->userInfo['id'],
+ 'companyId' => $this->request->userInfo['companyId'],
+ 'createTime' => time(),
+ 'updateTime' => time()
+ ]);
+ // 2. 创建扩展表
+ Db::name('ck_workbench_traffic_config')->insert([
+ 'workbenchId' => $planId,
+ 'distributeType' => $param['distributeType'],
+ 'maxPerDay' => $param['maxPerDay'],
+ 'timeType' => $param['timeType'],
+ 'startTime' => $param['startTime'],
+ 'endTime' => $param['endTime'],
+ 'targets' => json_encode($param['targets'], JSON_UNESCAPED_UNICODE),
+ 'pools' => json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE),
+ 'createTime' => time(),
+ 'updateTime' => time()
+ ]);
+ Db::commit();
+ return json(['code' => 200, 'msg' => '创建成功']);
+ } catch (\Exception $e) {
+ Db::rollback();
+ return json(['code' => 500, 'msg' => '创建失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 获取所有微信好友标签及数量统计
+ * @return \think\response\Json
+ */
+ public function getDeviceLabels()
+ {
+ $deviceIds = $this->request->param('deviceIds', '');
+ $companyId = $this->request->userInfo['companyId'];
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+
+ $where = [
+ ['wc.companyId', '=', $companyId],
+ ];
+
+ if (!empty($deviceIds)) {
+ $deviceIds = explode(',', $deviceIds);
+ $where[] = ['dwl.deviceId', 'in', $deviceIds];
+ }
+
+ $wechatAccounts = Db::name('wechat_customer')->alias('wc')
+ ->join('device_wechat_login dwl', 'dwl.wechatId = wc.wechatId AND dwl.companyId = wc.companyId AND dwl.alive = 1')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.wechatId = wc.wechatId')
+ ->where($where)
+ ->field('wa.id,wa.wechatId,wa.nickName,wa.labels')
+ ->select();
+ $labels = [];
+ $wechatIds = [];
+ foreach ($wechatAccounts as $account) {
+ $labelArr = json_decode($account['labels'], true);
+ if (is_array($labelArr)) {
+ foreach ($labelArr as $label) {
+ if ($label !== '' && $label !== null) {
+ $labels[] = $label;
+ }
+ }
+ }
+ $wechatIds[] = $account['wechatId'];
+ }
+ // 去重(只保留一个)
+ $labels = array_values(array_unique($labels));
+ $wechatIds = array_unique($wechatIds);
+
+ // 搜索过滤
+ if (!empty($keyword)) {
+ $labels = array_filter($labels, function ($label) use ($keyword) {
+ return mb_stripos($label, $keyword) !== false;
+ });
+ $labels = array_values($labels); // 重新索引数组
+ }
+
+
+ // 分页处理
+ $labels2 = array_slice($labels, ($page - 1) * $limit, $limit);
+
+ // 统计数量
+ $newLabel = [];
+ foreach ($labels2 as $label) {
+ $friendCount = Db::table('s2_wechat_friend')
+ ->whereIn('ownerWechatId', $wechatIds)
+ ->where('labels', 'like', '%"' . $label . '"%')
+ ->count();
+ $newLabel[] = [
+ 'label' => $label,
+ 'count' => $friendCount
+ ];
+ }
+
+ // 返回结果
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $newLabel,
+ 'total' => count($labels),
+ ]
+ ]);
+ }
+
+
+ /**
+ * 获取群列表
+ * @return \think\response\Json
+ */
+ public function getGroupList()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+
+ $where = [
+ ['wg.deleteTime', '=', 0],
+ ['wg.companyId', '=', $this->request->userInfo['companyId']],
+ ];
+
+ if (!empty($keyword)) {
+ $where[] = ['wg.name', 'like', '%' . $keyword . '%'];
+ }
+
+ $query = Db::name('wechat_group')->alias('wg')
+ ->join('wechat_account wa', 'wa.wechatId = wg.ownerWechatId')
+ ->where($where);
+
+ $total = $query->count();
+ $list = $query->order('wg.id', 'desc')
+ ->field('wg.id,wg.name as groupName,wg.ownerWechatId,wa.nickName,wg.createTime,wa.avatar,wa.alias,wg.avatar as groupAvatar')
+ ->page($page, $limit)
+ ->select();
+
+ // 优化:格式化时间,头像兜底
+ $defaultGroupAvatar = '';
+ $defaultAvatar = '';
+ foreach ($list as &$item) {
+ $item['createTime'] = $item['createTime'] ? date('Y-m-d H:i:s', $item['createTime']) : '';
+ $item['groupAvatar'] = $item['groupAvatar'] ?: $defaultGroupAvatar;
+ $item['avatar'] = $item['avatar'] ?: $defaultAvatar;
+ }
+
+ return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]);
+ }
+
+ /**
+ * 获取流量池列表
+ * @return \think\response\Json
+ */
+ public function getTrafficPoolList()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+ $companyId = $this->request->userInfo['companyId'];
+
+ $baseQuery = Db::name('traffic_source_package')->alias('tsp')
+ ->where('tsp.isDel', 0)
+ ->whereIn('tsp.companyId', [$companyId, 0]);
+
+ if (!empty($keyword)) {
+ $baseQuery->whereLike('tsp.name', '%' . $keyword . '%');
+ }
+
+ $total = (clone $baseQuery)->count();
+
+ $list = $baseQuery
+ ->leftJoin('traffic_source_package_item tspi', 'tspi.packageId = tsp.id and tspi.isDel = 0')
+ ->field('tsp.id,tsp.name,tsp.description,tsp.pic,tsp.companyId,COUNT(tspi.id) as itemCount,max(tspi.createTime) as latestImportTime')
+ ->group('tsp.id')
+ ->order('tsp.id', 'desc')
+ ->page($page, $limit)
+ ->select();
+
+ foreach ($list as &$item) {
+ $item['latestImportTime'] = !empty($item['latestImportTime']) ? date('Y-m-d H:i:s', $item['latestImportTime']) : '';
+ }
+ unset($item);
+
+ return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]);
+ }
+
+
+ public function getAccountList()
+ {
+ $companyId = $this->request->userInfo['companyId'];
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $query = Db::table('s2_company_account')
+ ->alias('a')
+ ->where(['a.departmentId' => $companyId, 'a.status' => 0])
+ ->whereNotLike('a.userName', '%_offline%')
+ ->whereNotLike('a.userName', '%_delete%');
+
+ $total = $query->count();
+ $list = $query->field('a.id,a.userName,a.realName,a.nickname,a.memo')
+ ->page($page, $limit)
+ ->select();
+
+
+ return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]);
+ }
+
+
+ /**
+ * 获取京东联盟导购媒体
+ * @return \think\response\Json
+ * @throws \think\db\exception\DataNotFoundException
+ * @throws \think\db\exception\ModelNotFoundException
+ * @throws \think\exception\DbException
+ */
+ public function getJdSocialMedia()
+ {
+ $data = Db::name('jd_social_media')->order('id DESC')->select();
+ return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]);
+ }
+
+ /**
+ * 获取京东联盟广告位
+ * @return \think\response\Json
+ * @throws \think\db\exception\DataNotFoundException
+ * @throws \think\db\exception\ModelNotFoundException
+ * @throws \think\exception\DbException
+ */
+ public function getJdPromotionSite()
+ {
+ $id = $this->request->param('id', '');
+ if (empty($id)) {
+ return json(['code' => 500, 'msg' => '参数缺失']);
+ }
+
+ $data = Db::name('jd_promotion_site')->where('jdSocialMediaId', $id)->order('id DESC')->select();
+ return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]);
+ }
+
+
+ //京东转链-京推推
+ public function changeLink($content = '', $positionid = '')
+ {
+ $unionId = Env::get('jd.unionId', '');
+ $jttAppId = Env::get('jd.jttAppId', '');
+ $appKey = Env::get('jd.appKey', '');
+ $apiUrl = Env::get('jd.apiUrl', '');
+
+ $content = !empty($content) ? $content : $this->request->param('content', '');
+ $positionid = !empty($positionid) ? $positionid : $this->request->param('positionid', '');
+
+
+ if (empty($content)) {
+ return json_encode(['code' => 500, 'msg' => '转链的内容为空']);
+ }
+
+ // 验证是否包含链接
+ if (!$this->containsLink($content)) {
+ return json_encode(['code' => 500, 'msg' => '内容中未检测到有效链接']);
+ }
+
+ if (empty($unionId) || empty($jttAppId) || empty($appKey) || empty($apiUrl)) {
+ return json_encode(['code' => 500, 'msg' => '参数缺失']);
+ }
+ $params = [
+ 'unionid' => $unionId,
+ 'content' => $content,
+ 'appid' => $jttAppId,
+ 'appkey' => $appKey,
+ 'v' => 'v2'
+ ];
+
+ if (!empty($positionid)) {
+ $params['positionid'] = $positionid;
+ }
+
+ $res = requestCurl($apiUrl, $params, 'GET', [], 'json');
+ $res = json_decode($res, true);
+ if (empty($res)) {
+ return json_encode(['code' => 500, 'msg' => '未知错误']);
+ }
+ $result = $res['result'];
+ if ($res['return'] == 0) {
+ return json_encode(['code' => 200, 'data' => $result['chain_content'], 'msg' => $result['msg']]);
+ } else {
+ return json_encode(['code' => 500, 'msg' => $result['msg']]);
+ }
+ }
+
+
+ public function getTrafficList()
+ {
+ $companyId = $this->request->userInfo['companyId'];
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+ $workbenchId = $this->request->param('workbenchId', '');
+ $isRecycle = $this->request->param('isRecycle', '');
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ $workbench = Db::name('workbench')->where(['id' => $workbenchId, 'isDel' => 0, 'companyId' => $companyId, 'type' => 5])->find();
+
+ if (empty($workbench)) {
+ return json(['code' => 400, 'msg' => '该任务不存在或已删除']);
+ }
+ $query = Db::name('workbench_traffic_config_item')->alias('wtc')
+ ->join(['s2_wechat_friend' => 'wf'], 'wtc.wechatFriendId = wf.id')
+ ->join('users u', 'wtc.wechatAccountId = u.s2_accountId', 'left')
+ ->field([
+ 'wtc.id', 'wtc.isRecycle', 'wtc.isRecycle', 'wtc.createTime','wtc.recycleTime',
+ 'wf.wechatId', 'wf.alias', 'wf.nickname', 'wf.avatar', 'wf.gender', 'wf.phone',
+ 'u.account', 'u.username'
+ ])
+ ->where(['wtc.workbenchId' => $workbenchId])
+ ->order('wtc.id DESC');
+
+ if (!empty($keyword)) {
+ $query->where('wf.wechatId|wf.alias|wf.nickname|wf.phone|u.account|u.username', 'like', '%' . $keyword . '%');
+ }
+
+ if ($isRecycle != '' || $isRecycle != null) {
+ $query->where('isRecycle',$isRecycle);
+ }
+
+
+
+ $total = $query->count();
+ $list = $query->page($page, $limit)->select();
+
+ foreach ($list as &$item) {
+ $item['createTime'] = date('Y-m-d H:i:s', $item['createTime']);
+ $item['recycleTime'] = date('Y-m-d H:i:s', $item['recycleTime']);
+ }
+ unset($item);
+
+
+ $data = [
+ 'total' => $total,
+ 'list' => $list,
+ ];
+
+ return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]);
+
+ }
+
+ /**
+ * 规范化客服微信ID列表
+ * @param mixed $ownerWechatIds
+ * @param WorkbenchGroupPush|null $originalConfig
+ * @return array
+ * @throws \Exception
+ */
+ private function normalizeOwnerWechatIds($ownerWechatIds, WorkbenchGroupPush $originalConfig = null): array
+ {
+ if ($ownerWechatIds === null) {
+ $existing = $originalConfig ? $this->decodeJsonArray($originalConfig->ownerWechatIds ?? []) : [];
+ if (empty($existing)) {
+ throw new \Exception('请至少选择一个客服微信');
+ }
+ return $existing;
+ }
+
+ if (!is_array($ownerWechatIds)) {
+ throw new \Exception('客服参数格式错误');
+ }
+
+ $normalized = $this->extractIdList($ownerWechatIds, '客服参数格式错误');
+ if (empty($normalized)) {
+ throw new \Exception('请至少选择一个客服微信');
+ }
+ return $normalized;
+ }
+
+ /**
+ * 构建群推送配置数据
+ * @param array $param
+ * @param array $ownerWechatIds
+ * @param WorkbenchGroupPush|null $originalConfig
+ * @return array
+ * @throws \Exception
+ */
+ private function prepareGroupPushData(array $param, array $ownerWechatIds, WorkbenchGroupPush $originalConfig = null): array
+ {
+ $targetTypeDefault = $originalConfig ? intval($originalConfig->targetType) : 1;
+ $targetType = intval($this->getParamValue($param, 'targetType', $targetTypeDefault)) ?: 1;
+
+ $groupPushSubTypeDefault = $originalConfig ? intval($originalConfig->groupPushSubType) : 1;
+ $groupPushSubType = intval($this->getParamValue($param, 'groupPushSubType', $groupPushSubTypeDefault)) ?: 1;
+ if (!in_array($groupPushSubType, [1, 2], true)) {
+ $groupPushSubType = 1;
+ }
+
+ $data = [
+ 'pushType' => $this->toBoolInt($this->getParamValue($param, 'pushType', $originalConfig->pushType ?? 0)),
+ 'targetType' => $targetType,
+ 'startTime' => $this->getParamValue($param, 'startTime', $originalConfig->startTime ?? ''),
+ 'endTime' => $this->getParamValue($param, 'endTime', $originalConfig->endTime ?? ''),
+ 'maxPerDay' => intval($this->getParamValue($param, 'maxPerDay', $originalConfig->maxPerDay ?? 0)),
+ 'pushOrder' => $this->getParamValue($param, 'pushOrder', $originalConfig->pushOrder ?? 1),
+ 'groupPushSubType' => $groupPushSubType,
+ 'status' => $this->toBoolInt($this->getParamValue($param, 'status', $originalConfig->status ?? 0)),
+ 'socialMediaId' => $this->getParamValue($param, 'socialMediaId', $originalConfig->socialMediaId ?? ''),
+ 'promotionSiteId' => $this->getParamValue($param, 'promotionSiteId', $originalConfig->promotionSiteId ?? ''),
+ 'friendIntervalMin' => intval($this->getParamValue($param, 'friendIntervalMin', $originalConfig->friendIntervalMin ?? 10)),
+ 'friendIntervalMax' => intval($this->getParamValue($param, 'friendIntervalMax', $originalConfig->friendIntervalMax ?? 20)),
+ 'messageIntervalMin' => intval($this->getParamValue($param, 'messageIntervalMin', $originalConfig->messageIntervalMin ?? 1)),
+ 'messageIntervalMax' => intval($this->getParamValue($param, 'messageIntervalMax', $originalConfig->messageIntervalMax ?? 12)),
+ 'isRandomTemplate' => $this->toBoolInt($this->getParamValue($param, 'isRandomTemplate', $originalConfig->isRandomTemplate ?? 0)),
+ 'ownerWechatIds' => json_encode($ownerWechatIds, JSON_UNESCAPED_UNICODE),
+ ];
+
+ if ($data['friendIntervalMin'] > $data['friendIntervalMax']) {
+ throw new \Exception('目标间最小间隔不能大于最大间隔');
+ }
+ if ($data['messageIntervalMin'] > $data['messageIntervalMax']) {
+ throw new \Exception('消息间最小间隔不能大于最大间隔');
+ }
+
+ $contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : [];
+ $contentGroupsParam = $this->getParamValue($param, 'contentGroups', null);
+ $contentGroups = $contentGroupsParam !== null
+ ? $this->extractIdList($contentGroupsParam, '内容库参数格式错误')
+ : $contentGroupsExisting;
+ $data['contentLibraries'] = json_encode($contentGroups, JSON_UNESCAPED_UNICODE);
+
+ $postPushTagsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->postPushTags ?? []) : [];
+ $postPushTagsParam = $this->getParamValue($param, 'postPushTags', null);
+ $postPushTags = $postPushTagsParam !== null
+ ? $this->extractIdList($postPushTagsParam, '推送标签参数格式错误')
+ : $postPushTagsExisting;
+ $data['postPushTags'] = json_encode($postPushTags, JSON_UNESCAPED_UNICODE);
+
+ if ($targetType === 1) {
+ $data['isLoop'] = $this->toBoolInt($this->getParamValue($param, 'isLoop', $originalConfig->isLoop ?? 0));
+
+ $groupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->groups ?? []) : [];
+ $wechatGroups = array_key_exists('wechatGroups', $param)
+ ? $this->extractIdList($param['wechatGroups'], '群参数格式错误')
+ : $groupsExisting;
+ if (empty($wechatGroups)) {
+ throw new \Exception('群推送必须选择微信群');
+ }
+ $data['groups'] = json_encode($wechatGroups, JSON_UNESCAPED_UNICODE);
+ $data['friends'] = json_encode([], JSON_UNESCAPED_UNICODE);
+ $data['trafficPools'] = json_encode([], JSON_UNESCAPED_UNICODE);
+
+ if ($groupPushSubType === 2) {
+ $announcementContent = $this->getParamValue($param, 'announcementContent', $originalConfig->announcementContent ?? '');
+ if (empty($announcementContent)) {
+ throw new \Exception('群公告必须输入公告内容');
+ }
+ $enableAiRewrite = $this->toBoolInt($this->getParamValue($param, 'enableAiRewrite', $originalConfig->enableAiRewrite ?? 0));
+ $aiRewritePrompt = trim((string)$this->getParamValue($param, 'aiRewritePrompt', $originalConfig->aiRewritePrompt ?? ''));
+ if ($enableAiRewrite === 1 && $aiRewritePrompt === '') {
+ throw new \Exception('启用AI智能话术改写时,必须输入改写提示词');
+ }
+ $data['announcementContent'] = $announcementContent;
+ $data['enableAiRewrite'] = $enableAiRewrite;
+ $data['aiRewritePrompt'] = $aiRewritePrompt;
+ } else {
+ $data['groupPushSubType'] = 1;
+ $data['announcementContent'] = '';
+ $data['enableAiRewrite'] = 0;
+ $data['aiRewritePrompt'] = '';
+ }
+ } else {
+ $data['isLoop'] = 0;
+ $friendsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->friends ?? []) : [];
+ $trafficPoolsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->trafficPools ?? []) : [];
+
+ $friendTargets = array_key_exists('wechatFriends', $param)
+ ? $this->extractIdList($param['wechatFriends'], '好友参数格式错误')
+ : $friendsExisting;
+ $trafficPools = array_key_exists('trafficPools', $param)
+ ? $this->extractIdList($param['trafficPools'], '流量池参数格式错误')
+ : $trafficPoolsExisting;
+
+ if (empty($friendTargets) && empty($trafficPools)) {
+ throw new \Exception('好友推送需至少选择好友或流量池');
+ }
+
+ $data['friends'] = json_encode($friendTargets, JSON_UNESCAPED_UNICODE);
+ $data['trafficPools'] = json_encode($trafficPools, JSON_UNESCAPED_UNICODE);
+ $data['groups'] = json_encode([], JSON_UNESCAPED_UNICODE);
+ $data['groupPushSubType'] = 1;
+ $data['announcementContent'] = '';
+ $data['enableAiRewrite'] = 0;
+ $data['aiRewritePrompt'] = '';
+ }
+
+ return $data;
+ }
+
+ /**
+ * 获取参数值,若不存在则返回默认值
+ * @param array $param
+ * @param string $key
+ * @param mixed $default
+ * @return mixed
+ */
+ private function getParamValue(array $param, string $key, $default)
+ {
+ return array_key_exists($key, $param) ? $param[$key] : $default;
+ }
+
+ /**
+ * 将值转换为整型布尔
+ * @param mixed $value
+ * @return int
+ */
+ private function toBoolInt($value): int
+ {
+ return empty($value) ? 0 : 1;
+ }
+
+ /**
+ * 从参数中提取ID列表
+ * @param mixed $items
+ * @param string $errorMessage
+ * @return array
+ * @throws \Exception
+ */
+ private function extractIdList($items, string $errorMessage = '参数格式错误'): array
+ {
+ if (!is_array($items)) {
+ throw new \Exception($errorMessage);
+ }
+
+ $ids = [];
+ foreach ($items as $item) {
+ if (is_array($item) && isset($item['id'])) {
+ $item = $item['id'];
+ }
+ if ($item === '' || $item === null) {
+ continue;
+ }
+ $ids[] = $item;
+ }
+
+ return array_values(array_unique($ids));
+ }
+
+ /**
+ * 解码JSON数组
+ * @param mixed $value
+ * @return array
+ */
+ private function decodeJsonArray($value): array
+ {
+ if (empty($value)) {
+ return [];
+ }
+ if (is_array($value)) {
+ return $value;
+ }
+ $decoded = json_decode($value, true);
+ return is_array($decoded) ? $decoded : [];
+ }
+
+ /**
+ * 验证内容是否包含链接
+ * @param string $content 要检测的内容
+ * @return bool
+ */
+ private function containsLink($content)
+ {
+ // 定义各种链接的正则表达式模式
+ $patterns = [
+ // HTTP/HTTPS链接
+ '/https?:\/\/[^\s]+/i',
+ // 京东商品链接
+ '/item\.jd\.com\/\d+/i',
+ // 京东短链接
+ '/u\.jd\.com\/[a-zA-Z0-9]+/i',
+ // 淘宝商品链接
+ '/item\.taobao\.com\/item\.htm\?id=\d+/i',
+ // 天猫商品链接
+ '/detail\.tmall\.com\/item\.htm\?id=\d+/i',
+ // 淘宝短链接
+ '/m\.tb\.cn\/[a-zA-Z0-9]+/i',
+ // 拼多多链接
+ '/mobile\.yangkeduo\.com\/goods\.html\?goods_id=\d+/i',
+ // 苏宁易购链接
+ '/product\.suning\.com\/\d+\/\d+\.html/i',
+ // 通用域名模式(包含常见电商域名)
+ '/(?:jd|taobao|tmall|yangkeduo|suning|amazon|dangdang)\.com[^\s]*/i',
+ // 通用短链接模式
+ '/[a-zA-Z0-9-]+\.[a-zA-Z]{2,}\/[a-zA-Z0-9\-._~:\/?#\[\]@!$&\'()*+,;=]+/i'
+ ];
+
+ // 遍历所有模式进行匹配
+ foreach ($patterns as $pattern) {
+ if (preg_match($pattern, $content)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+
+ /**
+ * 获取通讯录导入记录列表
+ * @return \think\response\Json
+ */
+ public function getImportContact()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $workbenchId = $this->request->param('workbenchId', 0);
+
+ $where = [
+ ['wici.workbenchId', '=', $workbenchId]
+ ];
+
+ // 查询发布记录
+ $list = Db::name('workbench_import_contact_item')->alias('wici')
+ ->join('traffic_pool tp', 'tp.id = wici.poolId', 'left')
+ ->join('traffic_source tc', 'tc.identifier = tp.identifier', 'left')
+ ->join('wechat_account wa', 'wa.wechatId = tp.wechatId', 'left')
+ ->field([
+ 'wici.id',
+ 'wici.workbenchId',
+ 'wici.createTime',
+ 'tp.identifier',
+ 'tp.mobile',
+ 'tp.wechatId',
+ 'tc.name',
+ 'wa.nickName',
+ 'wa.avatar',
+ 'wa.alias',
+ ])
+ ->where($where)
+ ->order('tc.name DESC,wici.createTime DESC')
+ ->group('tp.identifier')
+ ->page($page, $limit)
+ ->select();
+
+ foreach ($list as &$item) {
+ $item['createTime'] = date('Y-m-d H:i:s', $item['createTime']);
+ }
+
+
+ // 获取总记录数
+ $total = Db::name('workbench_import_contact_item')->alias('wici')
+ ->where($where)
+ ->count();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ ]
+ ]);
+ }
+
+ /**
+ * 获取群发统计数据
+ * @return \think\response\Json
+ */
+ public function getGroupPushStats()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ $timeRange = $this->request->param('timeRange', '7'); // 默认最近7天
+ $contentLibraryIds = $this->request->param('contentLibraryIds', ''); // 话术组筛选
+ $userId = $this->request->userInfo['id'];
+
+ // 如果指定了工作台ID,则验证权限
+ if (!empty($workbenchId)) {
+ $workbench = Workbench::where([
+ ['id', '=', $workbenchId],
+ ['userId', '=', $userId],
+ ['type', '=', self::TYPE_GROUP_PUSH],
+ ['isDel', '=', 0]
+ ])->find();
+
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在']);
+ }
+ }
+
+ // 计算时间范围
+ $days = intval($timeRange);
+ $startTime = strtotime(date('Y-m-d 00:00:00', strtotime("-{$days} days")));
+ $endTime = time();
+
+ // 构建查询条件
+ $where = [
+ ['wgpi.createTime', '>=', $startTime],
+ ['wgpi.createTime', '<=', $endTime]
+ ];
+
+ // 如果指定了工作台ID,则限制查询范围
+ if (!empty($workbenchId)) {
+ $where[] = ['wgpi.workbenchId', '=', $workbenchId];
+ } else {
+ // 如果没有指定工作台ID,则查询当前用户的所有群推送工作台
+ $workbenchIds = Workbench::where([
+ ['userId', '=', $userId],
+ ['type', '=', self::TYPE_GROUP_PUSH],
+ ['isDel', '=', 0]
+ ])->column('id');
+
+ if (empty($workbenchIds)) {
+ // 如果没有工作台,返回空结果
+ $workbenchIds = [-1];
+ }
+ $where[] = ['wgpi.workbenchId', 'in', $workbenchIds];
+ }
+
+ // 话术组筛选 - 先获取符合条件的内容ID列表
+ $contentIds = null;
+ if (!empty($contentLibraryIds)) {
+ $libraryIds = is_array($contentLibraryIds) ? $contentLibraryIds : explode(',', $contentLibraryIds);
+ $libraryIds = array_filter(array_map('intval', $libraryIds));
+ if (!empty($libraryIds)) {
+ // 查询符合条件的内容ID
+ $contentIds = Db::name('content_item')
+ ->whereIn('libraryId', $libraryIds)
+ ->column('id');
+ if (empty($contentIds)) {
+ // 如果没有符合条件的内容,返回空结果
+ $contentIds = [-1]; // 使用不存在的ID,确保查询结果为空
+ }
+ }
+ }
+
+ // 1. 基础统计:触达率、回复率、平均回复时间、链接点击率
+ $stats = $this->calculateBasicStats($workbenchId, $where, $startTime, $endTime, $contentIds);
+
+ // 2. 话术组对比
+ $contentLibraryComparison = $this->getContentLibraryComparison($workbenchId, $where, $startTime, $endTime, $contentIds);
+
+ // 3. 时段分析
+ $timePeriodAnalysis = $this->getTimePeriodAnalysis($workbenchId, $where, $startTime, $endTime, $contentIds);
+
+ // 4. 互动深度(可选,需要更多数据)
+ $interactionDepth = $this->getInteractionDepth($workbenchId, $where, $startTime, $endTime, $contentIds);
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'basicStats' => $stats,
+ 'contentLibraryComparison' => $contentLibraryComparison,
+ 'timePeriodAnalysis' => $timePeriodAnalysis,
+ 'interactionDepth' => $interactionDepth
+ ]
+ ]);
+ }
+
+ /**
+ * 计算基础统计数据
+ */
+ private function calculateBasicStats($workbenchId, $where, $startTime, $endTime, $contentIds = null)
+ {
+ // 获取工作台配置,计算计划发送数
+ // 如果 workbenchId 为空,则查询所有工作台的配置
+ $configQuery = WorkbenchGroupPush::alias('wgp')
+ ->join('workbench w', 'w.id = wgp.workbenchId', 'left')
+ ->where('w.type', self::TYPE_GROUP_PUSH)
+ ->where('w.isDel', 0);
+
+ if (!empty($workbenchId)) {
+ $configQuery->where('wgp.workbenchId', $workbenchId);
+ } else {
+ // 如果没有指定工作台ID,需要从 where 条件中获取 workbenchId 列表
+ $workbenchIdCondition = null;
+ foreach ($where as $condition) {
+ if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') {
+ if ($condition[1] === 'in' && is_array($condition[2])) {
+ $workbenchIdCondition = $condition[2];
+ break;
+ } elseif ($condition[1] === '=') {
+ $workbenchIdCondition = [$condition[2]];
+ break;
+ }
+ }
+ }
+ if ($workbenchIdCondition) {
+ $configQuery->whereIn('wgp.workbenchId', $workbenchIdCondition);
+ }
+ }
+
+ $configs = $configQuery->select();
+ $targetType = 1; // 默认值
+ if (!empty($configs)) {
+ // 如果只有一个配置,使用它的 targetType;如果有多个,默认使用1
+ $targetType = intval($configs[0]->targetType ?? 1);
+ }
+
+ // 计划发送数(根据配置计算)
+ $plannedSend = 0;
+ if (!empty($configs)) {
+ $days = ceil(($endTime - $startTime) / 86400);
+ foreach ($configs as $config) {
+ $maxPerDay = intval($config->maxPerDay ?? 0);
+ $configTargetType = intval($config->targetType ?? 1);
+ if ($configTargetType == 1) {
+ // 群推送:计划发送数 = 每日推送次数 * 天数 * 群数量
+ $groups = $this->decodeJsonArray($config->groups ?? []);
+ $plannedSend += $maxPerDay * $days * count($groups);
+ } else {
+ // 好友推送:计划发送数 = 每日推送人数 * 天数
+ $plannedSend += $maxPerDay * $days;
+ }
+ }
+ }
+
+ // 构建查询条件
+ $queryWhere = $where;
+ if ($contentIds !== null) {
+ $queryWhere[] = ['wgpi.contentId', 'in', $contentIds];
+ }
+
+ // 实际成功发送数(从推送记录表统计)
+ $successSend = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($queryWhere)
+ ->count();
+
+ // 触达率 = 成功发送数 / 计划发送数
+ $reachRate = $plannedSend > 0 ? round(($successSend / $plannedSend) * 100, 1) : 0;
+
+ // 获取发送记录列表,用于查询回复
+ $sentItemIds = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($queryWhere)
+ ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId')
+ ->select();
+
+ // 回复统计(通过消息表查询)
+ $replyStats = $this->calculateReplyStats($sentItemIds, $targetType, $startTime, $endTime);
+
+ // 链接点击统计
+ $clickStats = $this->calculateClickStats($sentItemIds, $targetType, $startTime, $endTime);
+
+ // 计算本月对比数据(简化处理,实际应该查询上个月同期数据)
+ $currentMonthStart = strtotime(date('Y-m-01 00:00:00'));
+ $lastMonthStart = strtotime(date('Y-m-01 00:00:00', strtotime('-1 month')));
+ $lastMonthEnd = $currentMonthStart - 1;
+
+ // 获取本月统计数据(避免递归调用)
+ $currentMonthWhere = [
+ ['wgpi.createTime', '>=', $currentMonthStart]
+ ];
+ // 复制 workbenchId 条件
+ foreach ($where as $condition) {
+ if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') {
+ $currentMonthWhere[] = $condition;
+ break;
+ }
+ }
+ if ($contentIds !== null) {
+ $currentMonthWhere[] = ['wgpi.contentId', 'in', $contentIds];
+ }
+ $currentMonthSend = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($currentMonthWhere)
+ ->count();
+
+ // 获取本月配置
+ $currentMonthConfigQuery = WorkbenchGroupPush::alias('wgp')
+ ->join('workbench w', 'w.id = wgp.workbenchId', 'left')
+ ->where('w.type', self::TYPE_GROUP_PUSH)
+ ->where('w.isDel', 0);
+ if (!empty($workbenchId)) {
+ $currentMonthConfigQuery->where('wgp.workbenchId', $workbenchId);
+ } else {
+ foreach ($where as $condition) {
+ if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId' && $condition[1] === 'in') {
+ $currentMonthConfigQuery->whereIn('wgp.workbenchId', $condition[2]);
+ break;
+ }
+ }
+ }
+ $currentMonthConfigs = $currentMonthConfigQuery->select();
+ $currentMonthPlanned = 0;
+ if (!empty($currentMonthConfigs)) {
+ $currentMonthDays = ceil(($endTime - $currentMonthStart) / 86400);
+ foreach ($currentMonthConfigs as $currentMonthConfig) {
+ $currentMonthMaxPerDay = intval($currentMonthConfig->maxPerDay ?? 0);
+ $currentMonthTargetType = intval($currentMonthConfig->targetType ?? 1);
+ if ($currentMonthTargetType == 1) {
+ $currentMonthGroups = $this->decodeJsonArray($currentMonthConfig->groups ?? []);
+ $currentMonthPlanned += $currentMonthMaxPerDay * $currentMonthDays * count($currentMonthGroups);
+ } else {
+ $currentMonthPlanned += $currentMonthMaxPerDay * $currentMonthDays;
+ }
+ }
+ }
+ $currentMonthReachRate = $currentMonthPlanned > 0 ? round(($currentMonthSend / $currentMonthPlanned) * 100, 1) : 0;
+
+ // 获取上个月统计数据
+ $lastMonthWhere = [
+ ['wgpi.createTime', '>=', $lastMonthStart],
+ ['wgpi.createTime', '<=', $lastMonthEnd]
+ ];
+ // 复制 workbenchId 条件
+ foreach ($where as $condition) {
+ if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') {
+ $lastMonthWhere[] = $condition;
+ break;
+ }
+ }
+ if ($contentIds !== null) {
+ $lastMonthWhere[] = ['wgpi.contentId', 'in', $contentIds];
+ }
+ $lastMonthSend = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($lastMonthWhere)
+ ->count();
+
+ // 获取上个月配置
+ $lastMonthConfigQuery = WorkbenchGroupPush::alias('wgp')
+ ->join('workbench w', 'w.id = wgp.workbenchId', 'left')
+ ->where('w.type', self::TYPE_GROUP_PUSH)
+ ->where('w.isDel', 0);
+ if (!empty($workbenchId)) {
+ $lastMonthConfigQuery->where('wgp.workbenchId', $workbenchId);
+ } else {
+ foreach ($where as $condition) {
+ if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId' && $condition[1] === 'in') {
+ $lastMonthConfigQuery->whereIn('wgp.workbenchId', $condition[2]);
+ break;
+ }
+ }
+ }
+ $lastMonthConfigs = $lastMonthConfigQuery->select();
+
+ $lastMonthPlanned = 0;
+ if (!empty($lastMonthConfigs)) {
+ $lastMonthDays = ceil(($lastMonthEnd - $lastMonthStart) / 86400);
+ foreach ($lastMonthConfigs as $lastMonthConfig) {
+ $lastMonthMaxPerDay = intval($lastMonthConfig->maxPerDay ?? 0);
+ $lastMonthTargetType = intval($lastMonthConfig->targetType ?? 1);
+ if ($lastMonthTargetType == 1) {
+ $lastMonthGroups = $this->decodeJsonArray($lastMonthConfig->groups ?? []);
+ $lastMonthPlanned += $lastMonthMaxPerDay * $lastMonthDays * count($lastMonthGroups);
+ } else {
+ $lastMonthPlanned += $lastMonthMaxPerDay * $lastMonthDays;
+ }
+ }
+ }
+ $lastMonthReachRate = $lastMonthPlanned > 0 ? round(($lastMonthSend / $lastMonthPlanned) * 100, 1) : 0;
+
+ // 获取上个月的回复和点击统计(简化处理)
+ $lastMonthSentItems = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($lastMonthWhere)
+ ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId')
+ ->select();
+ $lastMonthReplyStats = $this->calculateReplyStats($lastMonthSentItems, $targetType, $lastMonthStart, $lastMonthEnd);
+ $lastMonthClickStats = $this->calculateClickStats($lastMonthSentItems, $targetType, $lastMonthStart, $lastMonthEnd);
+
+ return [
+ 'reachRate' => [
+ 'value' => $reachRate,
+ 'trend' => round($reachRate - $lastMonthReachRate, 1),
+ 'unit' => '%',
+ 'description' => '成功发送/计划发送'
+ ],
+ 'replyRate' => [
+ 'value' => $replyStats['replyRate'],
+ 'trend' => round($replyStats['replyRate'] - $lastMonthReplyStats['replyRate'], 1),
+ 'unit' => '%',
+ 'description' => '收到回复/成功发送'
+ ],
+ 'avgReplyTime' => [
+ 'value' => $replyStats['avgReplyTime'],
+ 'trend' => round($lastMonthReplyStats['avgReplyTime'] - $replyStats['avgReplyTime'], 0),
+ 'unit' => '分钟',
+ 'description' => '从发送到回复的平均时长'
+ ],
+ 'clickRate' => [
+ 'value' => $clickStats['clickRate'],
+ 'trend' => round($clickStats['clickRate'] - $lastMonthClickStats['clickRate'], 1),
+ 'unit' => '%',
+ 'description' => '点击链接/成功发送'
+ ],
+ 'plannedSend' => $plannedSend,
+ 'successSend' => $successSend,
+ 'replyCount' => $replyStats['replyCount'],
+ 'clickCount' => $clickStats['clickCount']
+ ];
+ }
+
+ /**
+ * 计算回复统计
+ */
+ private function calculateReplyStats($sentItems, $targetType, $startTime, $endTime)
+ {
+ if (empty($sentItems)) {
+ return ['replyRate' => 0, 'avgReplyTime' => 0, 'replyCount' => 0];
+ }
+
+ $replyCount = 0;
+ $totalReplyTime = 0;
+ $replyTimes = [];
+
+ foreach ($sentItems as $item) {
+ $itemArray = is_array($item) ? $item : (array)$item;
+ $sendTime = $itemArray['createTime'] ?? 0;
+ $accountId = $itemArray['wechatAccountId'] ?? 0;
+
+ if ($targetType == 1) {
+ // 群推送:查找群内回复消息
+ $groupId = $itemArray['groupId'] ?? 0;
+ $group = Db::name('wechat_group')->where('id', $groupId)->find();
+ if ($group) {
+ $replyMsg = Db::table('s2_wechat_message')
+ ->where('wechatChatroomId', $group['chatroomId'])
+ ->where('wechatAccountId', $accountId)
+ ->where('isSend', 0) // 接收的消息
+ ->where('wechatTime', '>', $sendTime)
+ ->where('wechatTime', '<=', $sendTime + 86400) // 24小时内回复
+ ->order('wechatTime', 'asc')
+ ->find();
+
+ if ($replyMsg) {
+ $replyCount++;
+ $replyTime = $replyMsg['wechatTime'] - $sendTime;
+ $replyTimes[] = $replyTime;
+ $totalReplyTime += $replyTime;
+ }
+ }
+ } else {
+ // 好友推送:查找好友回复消息
+ $friendId = $itemArray['friendId'] ?? 0;
+ $friend = Db::table('s2_wechat_friend')->where('id', $friendId)->find();
+ if ($friend) {
+ $replyMsg = Db::table('s2_wechat_message')
+ ->where('wechatFriendId', $friendId)
+ ->where('wechatAccountId', $accountId)
+ ->where('isSend', 0) // 接收的消息
+ ->where('wechatTime', '>', $sendTime)
+ ->where('wechatTime', '<=', $sendTime + 86400) // 24小时内回复
+ ->order('wechatTime', 'asc')
+ ->find();
+
+ if ($replyMsg) {
+ $replyCount++;
+ $replyTime = $replyMsg['wechatTime'] - $sendTime;
+ $replyTimes[] = $replyTime;
+ $totalReplyTime += $replyTime;
+ }
+ }
+ }
+ }
+
+ $successSend = count($sentItems);
+ $replyRate = $successSend > 0 ? round(($replyCount / $successSend) * 100, 1) : 0;
+ $avgReplyTime = $replyCount > 0 ? round(($totalReplyTime / $replyCount) / 60, 0) : 0; // 转换为分钟
+
+ return [
+ 'replyRate' => $replyRate,
+ 'avgReplyTime' => $avgReplyTime,
+ 'replyCount' => $replyCount
+ ];
+ }
+
+ /**
+ * 计算链接点击统计
+ */
+ private function calculateClickStats($sentItems, $targetType, $startTime, $endTime)
+ {
+ if (empty($sentItems)) {
+ return ['clickRate' => 0, 'clickCount' => 0];
+ }
+
+ $clickCount = 0;
+ $linkContentIds = [];
+
+ // 获取所有发送的内容ID
+ foreach ($sentItems as $item) {
+ $itemArray = is_array($item) ? $item : (array)$item;
+ $contentId = $itemArray['contentId'] ?? 0;
+ if ($contentId > 0) {
+ $linkContentIds[] = $contentId;
+ }
+ }
+
+ if (empty($linkContentIds)) {
+ return ['clickRate' => 0, 'clickCount' => 0];
+ }
+
+ // 查询包含链接的内容
+ $linkContents = Db::name('content_item')
+ ->whereIn('id', array_unique($linkContentIds))
+ ->where('contentType', 2) // 链接类型
+ ->column('id');
+
+ // 统计发送了链接内容的记录数
+ $linkSendCount = 0;
+ foreach ($sentItems as $item) {
+ $itemArray = is_array($item) ? $item : (array)$item;
+ $contentId = $itemArray['contentId'] ?? 0;
+ if (in_array($contentId, $linkContents)) {
+ $linkSendCount++;
+ }
+ }
+
+ // 简化处理:假设点击率基于链接消息的发送(实际应该从点击追踪系统获取)
+ // 这里可以根据业务需求调整,比如通过消息中的链接点击事件统计
+ $clickCount = $linkSendCount; // 简化处理,实际需要真实的点击数据
+
+ $successSend = count($sentItems);
+ $clickRate = $successSend > 0 ? round(($clickCount / $successSend) * 100, 1) : 0;
+
+ return [
+ 'clickRate' => $clickRate,
+ 'clickCount' => $clickCount
+ ];
+ }
+
+ /**
+ * 获取话术组对比数据
+ */
+ private function getContentLibraryComparison($workbenchId, $where, $startTime, $endTime, $contentIds = null)
+ {
+ $queryWhere = $where;
+ if ($contentIds !== null) {
+ $queryWhere[] = ['wgpi.contentId', 'in', $contentIds];
+ }
+
+ $comparison = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->join('content_item ci', 'ci.id = wgpi.contentId', 'left')
+ ->join('content_library cl', 'cl.id = ci.libraryId', 'left')
+ ->where($queryWhere)
+ ->where('cl.id', '<>', null)
+ ->field([
+ 'cl.id as libraryId',
+ 'cl.name as libraryName',
+ 'COUNT(DISTINCT wgpi.id) as pushCount'
+ ])
+ ->group('cl.id, cl.name')
+ ->select();
+
+ $result = [];
+ foreach ($comparison as $item) {
+ $libraryId = $item['libraryId'];
+ $pushCount = intval($item['pushCount']);
+
+ // 获取该内容库的详细统计
+ $libraryContentIds = Db::name('content_item')
+ ->where('libraryId', $libraryId)
+ ->column('id');
+ if (empty($libraryContentIds)) {
+ $libraryContentIds = [-1];
+ }
+
+ $libraryWhere = array_merge($where, [['wgpi.contentId', 'in', $libraryContentIds]]);
+ $librarySentItems = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($libraryWhere)
+ ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId')
+ ->select();
+
+ $config = WorkbenchGroupPush::where('workbenchId', $workbenchId)->find();
+ $targetType = $config ? intval($config->targetType) : 1;
+
+ $replyStats = $this->calculateReplyStats($librarySentItems, $targetType, $startTime, $endTime);
+ $clickStats = $this->calculateClickStats($librarySentItems, $targetType, $startTime, $endTime);
+
+ // 计算转化率(简化处理,实际需要根据业务定义)
+ $conversionRate = $pushCount > 0 ? round(($replyStats['replyCount'] / $pushCount) * 100, 1) : 0;
+
+ $result[] = [
+ 'libraryId' => $libraryId,
+ 'libraryName' => $item['libraryName'],
+ 'pushCount' => $pushCount,
+ 'reachRate' => 100, // 简化处理,实际应该计算
+ 'replyRate' => $replyStats['replyRate'],
+ 'clickRate' => $clickStats['clickRate'],
+ 'conversionRate' => $conversionRate,
+ 'avgReplyTime' => $replyStats['avgReplyTime'],
+ 'level' => $this->getPerformanceLevel($replyStats['replyRate'], $clickStats['clickRate'], $conversionRate)
+ ];
+ }
+
+ // 按回复率排序
+ usort($result, function($a, $b) {
+ return $b['replyRate'] <=> $a['replyRate'];
+ });
+
+ return $result;
+ }
+
+ /**
+ * 获取性能等级
+ */
+ private function getPerformanceLevel($replyRate, $clickRate, $conversionRate)
+ {
+ $score = ($replyRate * 0.4) + ($clickRate * 0.3) + ($conversionRate * 0.3);
+
+ if ($score >= 40) {
+ return '优秀';
+ } elseif ($score >= 25) {
+ return '良好';
+ } elseif ($score >= 15) {
+ return '一般';
+ } else {
+ return '待提升';
+ }
+ }
+
+ /**
+ * 获取时段分析数据
+ */
+ private function getTimePeriodAnalysis($workbenchId, $where, $startTime, $endTime, $contentIds = null)
+ {
+ $queryWhere = $where;
+ if ($contentIds !== null) {
+ $queryWhere[] = ['wgpi.contentId', 'in', $contentIds];
+ }
+
+ $analysis = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($queryWhere)
+ ->field([
+ 'FROM_UNIXTIME(wgpi.createTime, "%H") as hour',
+ 'COUNT(*) as count'
+ ])
+ ->group('hour')
+ ->order('hour', 'asc')
+ ->select();
+
+ $result = [];
+ foreach ($analysis as $item) {
+ $result[] = [
+ 'hour' => intval($item['hour']),
+ 'count' => intval($item['count'])
+ ];
+ }
+
+ return $result;
+ }
+
+ /**
+ * 获取互动深度数据
+ */
+ private function getInteractionDepth($workbenchId, $where, $startTime, $endTime, $contentIds = null)
+ {
+ // 简化处理,实际需要更复杂的统计逻辑
+ return [
+ 'singleReply' => 0, // 单次回复
+ 'multipleReply' => 0, // 多次回复
+ 'deepInteraction' => 0 // 深度互动
+ ];
+ }
+
+ /**
+ * 获取推送历史记录列表
+ * @return \think\response\Json
+ */
+ public function getGroupPushHistory()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $workbenchId = $this->request->param('workbenchId', 0);
+ $keyword = $this->request->param('keyword', '');
+ $pushType = $this->request->param('pushType', ''); // 推送类型筛选:''=全部, 'friend'=好友消息, 'group'=群消息, 'announcement'=群公告
+ $status = $this->request->param('status', ''); // 状态筛选:''=全部, 'success'=已完成, 'progress'=进行中, 'failed'=失败
+ $userId = $this->request->userInfo['id'];
+
+ // 构建工作台查询条件
+ $workbenchWhere = [
+ ['w.userId', '=', $userId],
+ ['w.type', '=', self::TYPE_GROUP_PUSH],
+ ['w.isDel', '=', 0]
+ ];
+
+ // 如果指定了工作台ID,则验证权限并限制查询范围
+ if (!empty($workbenchId)) {
+ $workbench = Workbench::where([
+ ['id', '=', $workbenchId],
+ ['userId', '=', $userId],
+ ['type', '=', self::TYPE_GROUP_PUSH],
+ ['isDel', '=', 0]
+ ])->find();
+
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在']);
+ }
+ $workbenchWhere[] = ['w.id', '=', $workbenchId];
+ }
+
+ // 1. 先查询所有已执行的推送记录(按推送时间分组)
+ $pushHistoryQuery = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->join('workbench w', 'w.id = wgpi.workbenchId', 'left')
+ ->join('workbench_group_push wgp', 'wgp.workbenchId = wgpi.workbenchId', 'left')
+ ->join('content_item ci', 'ci.id = wgpi.contentId', 'left')
+ ->join('content_library cl', 'cl.id = ci.libraryId', 'left')
+ ->where($workbenchWhere)
+ ->field([
+ 'wgpi.workbenchId',
+ 'w.name as workbenchName',
+ 'wgpi.contentId',
+ 'FROM_UNIXTIME(wgpi.createTime, "%Y-%m-%d %H:00:00") as pushTime',
+ 'wgpi.targetType',
+ 'wgp.groupPushSubType',
+ 'MIN(wgpi.createTime) as createTime',
+ 'COUNT(DISTINCT wgpi.id) as totalCount',
+ 'cl.name as contentLibraryName'
+ ])
+ ->group('wgpi.workbenchId, wgpi.contentId, pushTime, wgpi.targetType, wgp.groupPushSubType');
+
+ if (!empty($keyword)) {
+ $pushHistoryQuery->where('w.name|cl.name|ci.content', 'like', '%' . $keyword . '%');
+ }
+
+ $pushHistoryList = $pushHistoryQuery->order('createTime', 'desc')->select();
+
+ // 2. 查询所有任务(包括未执行的)
+ $allTasksQuery = Db::name('workbench')
+ ->alias('w')
+ ->join('workbench_group_push wgp', 'wgp.workbenchId = w.id', 'left')
+ ->where($workbenchWhere)
+ ->field([
+ 'w.id as workbenchId',
+ 'w.name as workbenchName',
+ 'w.createTime',
+ 'wgp.targetType',
+ 'wgp.groupPushSubType',
+ 'wgp.groups',
+ 'wgp.friends',
+ 'wgp.trafficPools'
+ ]);
+
+ if (!empty($keyword)) {
+ $allTasksQuery->where('w.name', 'like', '%' . $keyword . '%');
+ }
+
+ $allTasks = $allTasksQuery->select();
+
+ // 3. 合并数据:已执行的推送记录 + 未执行的任务
+ $resultList = [];
+ $executedWorkbenchIds = [];
+
+ // 处理已执行的推送记录
+ foreach ($pushHistoryList as $item) {
+ $itemWorkbenchId = $item['workbenchId'];
+ $contentId = $item['contentId'];
+ $pushTime = $item['pushTime'];
+ $targetType = intval($item['targetType']);
+ $groupPushSubType = isset($item['groupPushSubType']) ? intval($item['groupPushSubType']) : 1;
+
+ // 标记该工作台已有执行记录
+ if (!in_array($itemWorkbenchId, $executedWorkbenchIds)) {
+ $executedWorkbenchIds[] = $itemWorkbenchId;
+ }
+
+ // 将时间字符串转换为时间戳范围(小时级别)
+ $pushTimeStart = strtotime($pushTime);
+ $pushTimeEnd = $pushTimeStart + 3600; // 一小时后
+
+ // 获取该次推送的详细统计
+ $pushWhere = [
+ ['wgpi.workbenchId', '=', $itemWorkbenchId],
+ ['wgpi.contentId', '=', $contentId],
+ ['wgpi.createTime', '>=', $pushTimeStart],
+ ['wgpi.createTime', '<', $pushTimeEnd],
+ ['wgpi.targetType', '=', $targetType]
+ ];
+
+ // 目标数量
+ if ($targetType == 1) {
+ // 群推送:统计群数量
+ $targetCount = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($pushWhere)
+ ->where('wgpi.groupId', '<>', null)
+ ->distinct(true)
+ ->count('wgpi.groupId');
+ } else {
+ // 好友推送:统计好友数量
+ $targetCount = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($pushWhere)
+ ->where('wgpi.friendId', '<>', null)
+ ->distinct(true)
+ ->count('wgpi.friendId');
+ }
+
+ // 成功数和失败数(简化处理,实际需要根据发送状态判断)
+ $successCount = intval($item['totalCount']); // 简化处理
+ $failCount = 0; // 简化处理,实际需要从发送状态获取
+
+ // 状态判断
+ $itemStatus = $successCount > 0 ? 'success' : 'failed';
+ if ($failCount > 0 && $successCount > 0) {
+ $itemStatus = 'partial';
+ }
+
+ // 推送类型判断
+ $pushTypeText = '';
+ $pushTypeCode = '';
+ if ($targetType == 1) {
+ // 群推送
+ if ($groupPushSubType == 2) {
+ $pushTypeText = '群公告';
+ $pushTypeCode = 'announcement';
+ } else {
+ $pushTypeText = '群消息';
+ $pushTypeCode = 'group';
+ }
+ } else {
+ // 好友推送
+ $pushTypeText = '好友消息';
+ $pushTypeCode = 'friend';
+ }
+
+ $resultList[] = [
+ 'workbenchId' => $itemWorkbenchId,
+ 'taskName' => $item['workbenchName'] ?? '',
+ 'pushType' => $pushTypeText,
+ 'pushTypeCode' => $pushTypeCode,
+ 'targetCount' => $targetCount,
+ 'successCount' => $successCount,
+ 'failCount' => $failCount,
+ 'status' => $itemStatus,
+ 'statusText' => $this->getStatusText($itemStatus),
+ 'createTime' => date('Y-m-d H:i:s', $item['createTime']),
+ 'contentLibraryName' => $item['contentLibraryName'] ?? ''
+ ];
+ }
+
+ // 处理未执行的任务
+ foreach ($allTasks as $task) {
+ $taskWorkbenchId = $task['workbenchId'];
+
+ // 如果该任务已有执行记录,跳过(避免重复)
+ if (in_array($taskWorkbenchId, $executedWorkbenchIds)) {
+ continue;
+ }
+
+ $targetType = isset($task['targetType']) ? intval($task['targetType']) : 1;
+ $groupPushSubType = isset($task['groupPushSubType']) ? intval($task['groupPushSubType']) : 1;
+
+ // 计算目标数量(从配置中获取)
+ $targetCount = 0;
+ if ($targetType == 1) {
+ // 群推送:统计配置的群数量
+ $groups = json_decode($task['groups'] ?? '[]', true);
+ $targetCount = is_array($groups) ? count($groups) : 0;
+ } else {
+ // 好友推送:统计配置的好友数量或流量池数量
+ $friends = json_decode($task['friends'] ?? '[]', true);
+ $trafficPools = json_decode($task['trafficPools'] ?? '[]', true);
+ $friendCount = is_array($friends) ? count($friends) : 0;
+ $poolCount = is_array($trafficPools) ? count($trafficPools) : 0;
+ // 如果配置了流量池,目标数量暂时显示为流量池数量(实际数量需要从流量池中统计)
+ $targetCount = $friendCount > 0 ? $friendCount : $poolCount;
+ }
+
+ // 推送类型判断
+ $pushTypeText = '';
+ $pushTypeCode = '';
+ if ($targetType == 1) {
+ // 群推送
+ if ($groupPushSubType == 2) {
+ $pushTypeText = '群公告';
+ $pushTypeCode = 'announcement';
+ } else {
+ $pushTypeText = '群消息';
+ $pushTypeCode = 'group';
+ }
+ } else {
+ // 好友推送
+ $pushTypeText = '好友消息';
+ $pushTypeCode = 'friend';
+ }
+
+ $resultList[] = [
+ 'workbenchId' => $taskWorkbenchId,
+ 'taskName' => $task['workbenchName'] ?? '',
+ 'pushType' => $pushTypeText,
+ 'pushTypeCode' => $pushTypeCode,
+ 'targetCount' => $targetCount,
+ 'successCount' => 0,
+ 'failCount' => 0,
+ 'status' => 'pending',
+ 'statusText' => '进行中',
+ 'createTime' => date('Y-m-d H:i:s', $task['createTime']),
+ 'contentLibraryName' => ''
+ ];
+ }
+
+ // 应用筛选条件
+ $filteredList = [];
+ foreach ($resultList as $item) {
+ // 推送类型筛选
+ if (!empty($pushType)) {
+ if ($pushType === 'friend' && $item['pushTypeCode'] !== 'friend') {
+ continue;
+ }
+ if ($pushType === 'group' && $item['pushTypeCode'] !== 'group') {
+ continue;
+ }
+ if ($pushType === 'announcement' && $item['pushTypeCode'] !== 'announcement') {
+ continue;
+ }
+ }
+
+ // 状态筛选
+ if (!empty($status)) {
+ if ($status === 'success' && $item['status'] !== 'success') {
+ continue;
+ }
+ if ($status === 'progress') {
+ // 进行中:包括 partial 和 pending
+ if ($item['status'] !== 'partial' && $item['status'] !== 'pending') {
+ continue;
+ }
+ }
+ if ($status === 'failed' && $item['status'] !== 'failed') {
+ continue;
+ }
+ }
+
+ $filteredList[] = $item;
+ }
+
+ // 按创建时间倒序排序
+ usort($filteredList, function($a, $b) {
+ return strtotime($b['createTime']) - strtotime($a['createTime']);
+ });
+
+ // 分页处理
+ $total = count($filteredList);
+ $offset = ($page - 1) * $limit;
+ $list = array_slice($filteredList, $offset, $limit);
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]);
+ }
+
+ /**
+ * 获取状态文本
+ * @param string $status 状态码
+ * @return string 状态文本
+ */
+ private function getStatusText($status)
+ {
+ $statusMap = [
+ 'success' => '已完成',
+ 'partial' => '进行中',
+ 'pending' => '进行中',
+ 'failed' => '失败'
+ ];
+ return $statusMap[$status] ?? '未知';
+ }
+
+ /**
+ * 获取已创建的群列表(自动建群)
+ * @return \think\response\Json
+ */
+ public function getCreatedGroupsList()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 100);
+ $keyword = $this->request->param('keyword', '');
+
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '工作台ID不能为空']);
+ }
+
+ // 验证工作台权限
+ $where = [
+ ['id', '=', $workbenchId],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['type', '=', self::TYPE_GROUP_CREATE],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+ $workbench = Workbench::where($where)->find();
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在或无权限']);
+ }
+
+ // 获取已创建的群ID列表(状态为成功且groupId不为空)
+ $groupIds = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('status', 2) // STATUS_SUCCESS = 2
+ ->where('groupId', '<>', null)
+ ->group('groupId')
+ ->column('groupId');
+
+ if (empty($groupIds)) {
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => [],
+ 'total' => 0,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]);
+ }
+
+ // 查询群组详细信息(从s2_wechat_chatroom表查询)
+ $query = Db::table('s2_wechat_chatroom')->alias('wc')
+ ->join('wechat_account wa', 'wa.wechatId = wc.wechatAccountWechatId', 'left')
+ ->where('wc.id', 'in', $groupIds)
+ ->where('wc.isDeleted', 0);
+
+ // 关键字搜索
+ if (!empty($keyword)) {
+ $query->where(function ($q) use ($keyword) {
+ $q->where('wc.nickname', 'like', '%' . $keyword . '%')
+ ->whereOr('wc.chatroomId', 'like', '%' . $keyword . '%')
+ ->whereOr('wa.nickName', 'like', '%' . $keyword . '%');
+ });
+ }
+
+ $total = $query->count();
+ $list = $query->field('wc.id,wc.nickname as groupName,wc.chatroomId,wc.chatroomAvatar as groupAvatar,wc.wechatAccountWechatId as ownerWechatId,wc.createTime,wc.chatroomOwnerNickname as ownerNickname,wc.chatroomOwnerAvatar as ownerAvatar,wa.alias as ownerAlias')
+ ->order('wc.createTime', 'desc')
+ ->page($page, $limit)
+ ->select();
+
+ // 统计每个群的成员数量和成员信息
+ foreach ($list as &$item) {
+ // 统计该群的成员数量(从workbench_group_create_item表统计)
+ $memberCount = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('groupId', $item['id'])
+ ->where('status', 'in', [2, 4]) // 创建成功和管理员好友已拉入
+ ->count();
+
+ // 获取成员列表(用于显示成员头像)
+ $memberList = Db::name('workbench_group_create_item')->alias('wgci')
+ ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wgci.friendId', 'left')
+ ->where('wgci.workbenchId', $workbenchId)
+ ->where('wgci.groupId', $item['id'])
+ ->where('wgci.status', 'in', [2, 4])
+ ->field('wf.avatar, wf.wechatId, wf.nickname')
+ ->order('wgci.createTime', 'asc')
+ ->limit(10) // 最多显示10个成员头像
+ ->select();
+
+ // 格式化成员头像列表
+ $memberAvatars = [];
+ foreach ($memberList as $member) {
+ if (!empty($member['avatar'])) {
+ $memberAvatars[] = [
+ 'avatar' => $member['avatar'],
+ 'wechatId' => $member['wechatId'] ?? '',
+ 'nickname' => $member['nickname'] ?? ''
+ ];
+ }
+ }
+
+ // 计算剩余成员数(用于显示"+XX")
+ $remainingCount = $memberCount > count($memberAvatars) ? $memberCount - count($memberAvatars) : 0;
+
+ // 格式化返回数据
+ $item['memberCount'] = $memberCount;
+ $item['memberCountText'] = $memberCount . '人'; // 格式化为"XX人"
+ $item['createTime'] = !empty($item['createTime']) ? date('Y-m-d', $item['createTime']) : ''; // 格式化为"YYYY-MM-DD"
+ $item['memberAvatars'] = $memberAvatars; // 成员头像列表(最多10个)
+ $item['remainingCount'] = $remainingCount; // 剩余成员数(用于显示"+XX")
+
+ // 保留原有字段,但调整格式
+ $item['groupName'] = $item['groupName'] ?? '';
+ $item['groupAvatar'] = $item['groupAvatar'] ?? '';
+ }
+ unset($item);
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]);
+ }
+
+ /**
+ * 获取已创建群的详情(自动建群)
+ * @return \think\response\Json
+ */
+ public function getCreatedGroupDetail()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ $groupId = $this->request->param('groupId', 0);
+
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '工作台ID不能为空']);
+ }
+
+ if (empty($groupId)) {
+ return json(['code' => 400, 'msg' => '群ID不能为空']);
+ }
+
+ // 验证工作台权限
+ $where = [
+ ['id', '=', $workbenchId],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['type', '=', self::TYPE_GROUP_CREATE],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+ $workbench = Workbench::where($where)->find();
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在或无权限']);
+ }
+
+ // 验证该群是否属于该工作台
+ $groupItem = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('groupId', $groupId)
+ ->where('status', 2) // STATUS_SUCCESS = 2
+ ->find();
+
+ if (empty($groupItem)) {
+ return json(['code' => 404, 'msg' => '群不存在或不属于该工作台']);
+ }
+
+ // 查询群基本信息(从s2_wechat_chatroom表查询)
+ $group = Db::table('s2_wechat_chatroom')->alias('wc')
+ ->join('wechat_account wa', 'wa.wechatId = wc.wechatAccountWechatId', 'left')
+ ->where('wc.id', $groupId)
+ ->where('wc.isDeleted', 0)
+ ->field('wc.id,wc.nickname as groupName,wc.chatroomId,wc.chatroomAvatar as groupAvatar,wc.wechatAccountWechatId as ownerWechatId,wc.createTime,wc.chatroomOwnerNickname as ownerNickname,wc.chatroomOwnerAvatar as ownerAvatar,wa.alias as ownerAlias,wc.announce')
+ ->find();
+
+ if (empty($group)) {
+ return json(['code' => 404, 'msg' => '群不存在']);
+ }
+
+ // 获取chatroomId
+ $chatroomId = $group['chatroomId'] ?? '';
+ if (empty($chatroomId)) {
+ return json(['code' => 400, 'msg' => '群聊ID不存在']);
+ }
+
+ // 从s2_wechat_chatroom_member表查询所有成员列表(不限制数量)
+ $memberList = Db::table('s2_wechat_chatroom_member')->alias('wcm')
+ ->join(['s2_wechat_friend' => 'wf'], 'wf.wechatId = wcm.wechatId', 'left')
+ ->where('wcm.chatroomId', $chatroomId)
+ ->field('wcm.wechatId,wcm.nickname as memberNickname,wcm.avatar as memberAvatar,wcm.conRemark as memberRemark,wcm.alias as memberAlias,wcm.createTime as joinTime,wcm.updateTime,wf.id as friendId,wf.nickname as friendNickname,wf.avatar as friendAvatar')
+ ->order('wcm.createTime', 'asc')
+ ->select();
+
+ // 去重:按wechatId去重,保留第一条记录
+ $memberMap = [];
+ foreach ($memberList as $member) {
+ $wechatId = $member['wechatId'] ?? '';
+ if (!empty($wechatId) && !isset($memberMap[$wechatId])) {
+ $memberMap[$wechatId] = $member;
+ }
+ }
+ $memberList = array_values($memberMap); // 重新索引数组
+
+ // 获取在群中的成员wechatId列表(用于判断是否已退群)
+ $inGroupWechatIds = array_column($memberList, 'wechatId');
+ $inGroupWechatIds = array_filter($inGroupWechatIds); // 过滤空值
+
+ // 获取通过自动建群加入的成员信息(用于判断入群状态和已退群成员)
+ $autoJoinMemberList = Db::name('workbench_group_create_item')
+ ->alias('wgci')
+ ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wgci.friendId', 'left')
+ ->where('wgci.workbenchId', $workbenchId)
+ ->where('wgci.groupId', $groupId)
+ ->where('wgci.status', 'in', [2, 4]) // 创建成功和管理员好友已拉入
+ ->field('wf.wechatId,wf.id as friendId,wf.nickname as friendNickname,wf.avatar as friendAvatar,wgci.createTime as autoJoinTime')
+ ->select();
+
+ // 去重:按wechatId去重,保留第一条记录
+ $autoJoinMemberMap = [];
+ foreach ($autoJoinMemberList as $autoMember) {
+ $wechatId = $autoMember['wechatId'] ?? '';
+ if (!empty($wechatId) && !isset($autoJoinMemberMap[$wechatId])) {
+ $autoJoinMemberMap[$wechatId] = $autoMember;
+ }
+ }
+ $autoJoinMemberList = array_values($autoJoinMemberMap); // 重新索引数组
+
+ // 统计成员总数(包括在群中的成员和已退群的成员)
+ // 先统计在群中的成员数
+ $inGroupCount = count($memberList);
+
+ // 统计已退群的成员数(在自动建群记录中但不在群中的成员)
+ $quitCount = 0;
+ if (!empty($autoJoinMemberList)) {
+ foreach ($autoJoinMemberList as $autoMember) {
+ $wechatId = $autoMember['wechatId'] ?? '';
+ if (!empty($wechatId) && !in_array($wechatId, $inGroupWechatIds)) {
+ $quitCount++;
+ }
+ }
+ }
+
+ // 总成员数 = 在群中的成员数 + 已退群的成员数
+ $memberCount = $inGroupCount + $quitCount;
+
+ // 获取自动建群加入的成员wechatId列表(用于判断入群状态)
+ $autoJoinWechatIds = array_column($autoJoinMemberList, 'wechatId');
+ $autoJoinWechatIds = array_filter($autoJoinWechatIds); // 过滤空值
+
+ // 格式化在群中的成员列表
+ $members = [];
+ $addedWechatIds = []; // 用于记录已添加的成员wechatId,避免重复
+ $ownerWechatId = $group['ownerWechatId'] ?? '';
+ foreach ($memberList as $member) {
+ $wechatId = $member['wechatId'] ?? '';
+
+ // 跳过空wechatId和已添加的成员
+ if (empty($wechatId) || in_array($wechatId, $addedWechatIds)) {
+ continue;
+ }
+
+ // 标记为已添加
+ $addedWechatIds[] = $wechatId;
+
+ // 根据wechatId判断是否为群主
+ $isOwner = (!empty($ownerWechatId) && $wechatId == $ownerWechatId) ? 1 : 0;
+
+ // 判断入群状态:如果在自动建群记录中,说明是通过自动建群加入的;否则是其他方式加入的
+ $joinStatus = in_array($wechatId, $autoJoinWechatIds) ? 'auto' : 'manual';
+
+ // 判断是否已退群:如果成员在s2_wechat_chatroom_member表中存在,说明在群中;否则已退群
+ // 由于我们已经从s2_wechat_chatroom_member表查询,所以这里都是"在群中"状态
+ $isQuit = 0; // 0=在群中,1=已退群
+
+ // 优先使用friend表的昵称和头像,如果没有则使用member表的
+ $nickname = !empty($member['friendNickname']) ? $member['friendNickname'] : ($member['memberNickname'] ?? '');
+ $avatar = !empty($member['friendAvatar']) ? $member['friendAvatar'] : ($member['memberAvatar'] ?? '');
+
+ $members[] = [
+ 'friendId' => $member['friendId'] ?? 0,
+ 'wechatId' => $wechatId,
+ 'nickname' => $nickname,
+ 'avatar' => $avatar,
+ 'alias' => $member['memberAlias'] ?? '',
+ 'remark' => $member['memberRemark'] ?? '',
+ 'isOwner' => $isOwner, // 标记群主
+ 'joinStatus' => $joinStatus, // 入群状态:auto=自动建群加入,manual=其他方式加入
+ 'isQuit' => $isQuit, // 是否已退群:0=在群中,1=已退群
+ 'joinTime' => !empty($member['joinTime']) ? date('Y-m-d H:i:s', $member['joinTime']) : '', // 入群时间
+ ];
+ }
+
+ // 添加已退群的成员(在自动建群记录中但不在群中的成员)
+ foreach ($autoJoinMemberList as $autoMember) {
+ $wechatId = $autoMember['wechatId'] ?? '';
+
+ // 跳过空wechatId、已在群中的成员和已添加的成员
+ if (empty($wechatId) || in_array($wechatId, $inGroupWechatIds) || in_array($wechatId, $addedWechatIds)) {
+ continue;
+ }
+
+ // 标记为已添加
+ $addedWechatIds[] = $wechatId;
+
+ // 根据wechatId判断是否为群主
+ $isOwner = (!empty($ownerWechatId) && $wechatId == $ownerWechatId) ? 1 : 0;
+
+ $members[] = [
+ 'friendId' => $autoMember['friendId'] ?? 0,
+ 'wechatId' => $wechatId,
+ 'nickname' => $autoMember['friendNickname'] ?? '',
+ 'avatar' => $autoMember['friendAvatar'] ?? '',
+ 'alias' => '',
+ 'remark' => '',
+ 'isOwner' => $isOwner, // 标记群主
+ 'joinStatus' => 'auto', // 入群状态:auto=自动建群加入
+ 'isQuit' => 1, // 是否已退群:1=已退群
+ 'joinTime' => !empty($autoMember['autoJoinTime']) ? date('Y-m-d H:i:s', $autoMember['autoJoinTime']) : '', // 入群时间
+ ];
+ }
+
+ // 将群主排在第一位
+ usort($members, function($a, $b) {
+ if ($a['isOwner'] == $b['isOwner']) {
+ return 0;
+ }
+ return $a['isOwner'] > $b['isOwner'] ? -1 : 1;
+ });
+
+ // 格式化返回数据
+ $result = [
+ 'id' => $group['id'],
+ 'groupName' => $group['groupName'] ?? '',
+ 'chatroomId' => $group['chatroomId'] ?? '',
+ 'groupAvatar' => $group['groupAvatar'] ?? '',
+ 'ownerWechatId' => $group['ownerWechatId'] ?? '',
+ 'ownerNickname' => $group['ownerNickname'] ?? '',
+ 'ownerAvatar' => $group['ownerAvatar'] ?? '',
+ 'ownerAlias' => $group['ownerAlias'] ?? '',
+ 'announce' => $group['announce'] ?? '',
+ 'createTime' => !empty($group['createTime']) ? date('Y-m-d H:i', $group['createTime']) : '', // 格式化为"YYYY-MM-DD HH:MM"
+ 'memberCount' => $memberCount,
+ 'memberCountText' => $memberCount . '人', // 格式化为"XX人"
+ 'workbenchName' => $workbench->name ?? '', // 任务名称(工作台名称)
+ 'members' => $members // 所有成员列表
+ ];
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => $result
+ ]);
+ }
+
+ /**
+ * 同步群最新信息(包括群成员)
+ * @return \think\response\Json
+ */
+ public function syncGroupInfo()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ $groupId = $this->request->param('groupId', 0);
+
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '工作台ID不能为空']);
+ }
+
+ if (empty($groupId)) {
+ return json(['code' => 400, 'msg' => '群ID不能为空']);
+ }
+
+ // 验证工作台权限
+ $where = [
+ ['id', '=', $workbenchId],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['type', '=', self::TYPE_GROUP_CREATE],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+ $workbench = Workbench::where($where)->find();
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在或无权限']);
+ }
+
+ // 验证该群是否属于该工作台
+ $groupItem = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('groupId', $groupId)
+ ->where('status', 2) // STATUS_SUCCESS = 2
+ ->find();
+
+ if (empty($groupItem)) {
+ return json(['code' => 404, 'msg' => '群不存在或不属于该工作台']);
+ }
+
+ // 查询群基本信息,获取chatroomId和wechatAccountWechatId
+ $group = Db::table('s2_wechat_chatroom')
+ ->where('id', $groupId)
+ ->where('isDeleted', 0)
+ ->field('id,chatroomId,wechatAccountWechatId')
+ ->find();
+
+ if (empty($group)) {
+ return json(['code' => 404, 'msg' => '群不存在']);
+ }
+
+ $chatroomId = $group['chatroomId'] ?? '';
+ $wechatAccountWechatId = $group['wechatAccountWechatId'] ?? '';
+
+ if (empty($chatroomId)) {
+ return json(['code' => 400, 'msg' => '群聊ID不存在']);
+ }
+
+ try {
+ // 实例化WechatChatroomController
+ $chatroomController = new \app\api\controller\WechatChatroomController();
+
+ // 1. 同步群信息(调用getlist方法)
+ $syncData = [
+ 'wechatChatroomId' => $chatroomId, // 使用chatroomId作为wechatChatroomId来指定要同步的群
+ 'wechatAccountKeyword' => $wechatAccountWechatId, // 通过群主微信ID筛选
+ 'isDeleted' => false,
+ 'pageIndex' => 1,
+ 'pageSize' => 100 // 获取足够多的数据
+ ];
+ $syncResult = $chatroomController->getlist($syncData, true, 0); // isInner = true
+ $syncResponse = json_decode($syncResult, true);
+
+ if (empty($syncResponse['code']) || $syncResponse['code'] != 200) {
+ return json(['code' => 500, 'msg' => '同步群信息失败:' . ($syncResponse['msg'] ?? '未知错误')]);
+ }
+
+ // 2. 同步群成员信息(调用listChatroomMember方法)
+ // wechatChatroomId 使用 s2_wechat_chatroom 表的 id(即groupId)
+ // chatroomId 使用群聊ID(chatroomId)
+ $wechatChatroomId = $groupId; // s2_wechat_chatroom表的id
+ $memberSyncResult = $chatroomController->listChatroomMember($wechatChatroomId, $chatroomId, true); // isInner = true
+ $memberSyncResponse = json_decode($memberSyncResult, true);
+
+ if (empty($memberSyncResponse['code']) || $memberSyncResponse['code'] != 200) {
+ // 成员同步失败不影响整体结果,记录警告即可
+ \think\facade\Log::warning("同步群成员失败。群ID: {$groupId}, 群聊ID: {$chatroomId}, 错误: " . ($memberSyncResponse['msg'] ?? '未知错误'));
+ }
+
+ return json([
+ 'code' => 200,
+ 'msg' => '同步成功',
+ 'data' => [
+ 'groupId' => $groupId,
+ 'chatroomId' => $chatroomId,
+ 'groupInfoSynced' => true,
+ 'memberInfoSynced' => !empty($memberSyncResponse['code']) && $memberSyncResponse['code'] == 200
+ ]
+ ]);
+ } catch (\Exception $e) {
+ \think\facade\Log::error("同步群信息异常。群ID: {$groupId}, 错误: " . $e->getMessage());
+ return json(['code' => 500, 'msg' => '同步失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 修改群名称、群公告
+ * @return \think\response\Json
+ */
+ public function modifyGroupInfo()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ $groupId = $this->request->param('groupId', 0);
+ $chatroomName = $this->request->param('chatroomName', '');
+ $announce = $this->request->param('announce', '');
+
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '工作台ID不能为空']);
+ }
+
+ if (empty($groupId)) {
+ return json(['code' => 400, 'msg' => '群ID不能为空']);
+ }
+
+ // 至少需要提供一个修改项
+ if (empty($chatroomName) && empty($announce)) {
+ return json(['code' => 400, 'msg' => '请至少提供群名称或群公告中的一个参数']);
+ }
+
+ // 查询群基本信息
+ $group = Db::table('s2_wechat_chatroom')
+ ->where('id', $groupId)
+ ->where('isDeleted', 0)
+ ->field('id,chatroomId,wechatAccountWechatId,accountId,wechatAccountId')
+ ->find();
+
+ if (empty($group)) {
+ return json(['code' => 404, 'msg' => '群不存在']);
+ }
+
+ $chatroomId = $group['id'] ?? '';
+
+ if (empty($chatroomId)) {
+ return json(['code' => 400, 'msg' => '群聊ID不存在']);
+ }
+
+ try {
+ // 直接使用群表中的账号信息
+ $executeAccountId = $group['accountId'] ?? 0;
+ $executeWechatAccountId = $group['wechatAccountId'] ?? 0;
+ $executeWechatId = $group['wechatAccountWechatId'] ?? '';
+
+ // 确保 wechatId 不为空
+ if (empty($executeWechatId)) {
+ return json(['code' => 400, 'msg' => '无法获取微信账号ID']);
+ }
+
+ // 调用 WebSocketController 修改群信息
+ // 获取系统API账号信息(用于WebSocket连接)
+ $username = Env::get('api.username2', '');
+ $password = Env::get('api.password2', '');
+
+ if (empty($username) || empty($password)) {
+ return json(['code' => 500, 'msg' => '系统API账号配置缺失']);
+ }
+
+ // 获取系统账号ID
+ $systemAccountId = Db::name('users')->where('account', $username)->value('s2_accountId');
+ if (empty($systemAccountId)) {
+ return json(['code' => 500, 'msg' => '未找到系统账号ID']);
+ }
+
+ $webSocketController = new WebSocketController([
+ 'userName' => $username,
+ 'password' => $password,
+ 'accountId' => $systemAccountId
+ ]);
+ // 构建修改参数
+ $modifyData = [
+ 'wechatChatroomId' => $chatroomId,
+ 'wechatAccountId' => $executeWechatAccountId,
+ ];
+ if (!empty($chatroomName)) {
+ $modifyData['chatroomName'] = $chatroomName;
+ }
+ if (!empty($announce)) {
+ $modifyData['announce'] = $announce;
+ }
+
+
+ $modifyResult = $webSocketController->CmdChatroomModifyInfo($modifyData);
+ $modifyResponse = json_decode($modifyResult, true);
+ if (empty($modifyResponse['code']) || $modifyResponse['code'] != 200) {
+ return json(['code' => 500, 'msg' => '修改群信息失败:' . ($modifyResponse['msg'] ?? '未知错误')]);
+ }
+
+ // 修改成功后更新数据库
+ $updateData = [
+ 'updateTime' => time()
+ ];
+
+ // 如果修改了群名称,更新数据库
+ if (!empty($chatroomName)) {
+ $updateData['nickname'] = $chatroomName;
+ }
+
+ // 如果修改了群公告,更新数据库
+ if (!empty($announce)) {
+ $updateData['announce'] = $announce;
+ }
+
+ // 更新数据库
+ Db::table('s2_wechat_chatroom')
+ ->where('id', $groupId)
+ ->update($updateData);
+
+ return json([
+ 'code' => 200,
+ 'msg' => '修改成功',
+ 'data' => [
+ 'groupId' => $groupId,
+ 'chatroomId' => $chatroomId,
+ 'chatroomName' => $chatroomName,
+ 'announce' => $announce
+ ]
+ ]);
+ } catch (\Exception $e) {
+ \think\facade\Log::error("修改群信息异常。群ID: {$groupId}, 错误: " . $e->getMessage());
+ return json(['code' => 500, 'msg' => '修改失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 退群功能
+ * @return \think\response\Json
+ */
+ public function quitGroup()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ $groupId = $this->request->param('groupId', 0);
+
+ if (empty($groupId)) {
+ return json(['code' => 400, 'msg' => '群ID不能为空']);
+ }
+
+ // 查询群基本信息
+ $group = Db::table('s2_wechat_chatroom')
+ ->where('id', $groupId)
+ ->where('isDeleted', 0)
+ ->field('id,chatroomId,wechatAccountWechatId,accountId,wechatAccountId')
+ ->find();
+
+ if (empty($group)) {
+ return json(['code' => 404, 'msg' => '群不存在']);
+ }
+
+ $chatroomId = $group['chatroomId'] ?? '';
+ if (empty($chatroomId)) {
+ return json(['code' => 400, 'msg' => '群聊ID不存在']);
+ }
+
+ try {
+ // 直接使用群表中的账号信息
+ $executeWechatId = $group['wechatAccountWechatId'] ?? '';
+
+ // 确保 wechatId 不为空
+ if (empty($executeWechatId)) {
+ return json(['code' => 400, 'msg' => '无法获取微信账号ID']);
+ }
+
+ // 调用 WebSocketController 退群
+ // 获取系统API账号信息(用于WebSocket连接)
+ $username = Env::get('api.username2', '');
+ $password = Env::get('api.password2', '');
+
+ if (empty($username) || empty($password)) {
+ return json(['code' => 500, 'msg' => '系统API账号配置缺失']);
+ }
+
+ // 获取系统账号ID
+ $systemAccountId = Db::name('users')->where('account', $username)->value('s2_accountId');
+ if (empty($systemAccountId)) {
+ return json(['code' => 500, 'msg' => '未找到系统账号ID']);
+ }
+
+ $webSocketController = new WebSocketController([
+ 'userName' => $username,
+ 'password' => $password,
+ 'accountId' => $systemAccountId
+ ]);
+
+ // 使用和 modifyGroupInfo 相同的方式,但操作类型改为 4(退群)
+ $params = [
+ "chatroomOperateType" => 4, // 4 表示退群
+ "cmdType" => "CmdChatroomOperate",
+ "seq" => time(),
+ "wechatAccountId" => $executeWechatId,
+ "wechatChatroomId" => $chatroomId
+ ];
+
+ // 使用反射调用 protected 的 sendMessage 方法
+ $reflection = new \ReflectionClass($webSocketController);
+ $method = $reflection->getMethod('sendMessage');
+ $method->setAccessible(true);
+ $quitResult = $method->invoke($webSocketController, $params, false);
+
+ // sendMessage 返回的是数组
+ if (empty($quitResult) || (isset($quitResult['code']) && $quitResult['code'] != 200)) {
+ return json(['code' => 500, 'msg' => '退群失败:' . ($quitResult['msg'] ?? '未知错误')]);
+ }
+
+ // 退群成功后更新数据库
+ // 将群标记为已删除
+ Db::table('s2_wechat_chatroom')
+ ->where('id', $groupId)
+ ->update([
+ 'isDeleted' => 1,
+ 'deleteTime' => time(),
+ 'updateTime' => time()
+ ]);
+
+ // 如果提供了 workbenchId,更新工作台建群记录的状态(标记为已退群)
+ if (!empty($workbenchId)) {
+ Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('groupId', $groupId)
+ ->update([
+ 'status' => 5, // 可以定义一个新状态:5 = 已退群
+ 'updateTime' => time()
+ ]);
+ }
+
+ return json([
+ 'code' => 200,
+ 'msg' => '退群成功',
+ 'data' => [
+ 'groupId' => $groupId,
+ 'chatroomId' => $chatroomId
+ ]
+ ]);
+ } catch (\Exception $e) {
+ \think\facade\Log::error("退群异常。群ID: {$groupId}, 错误: " . $e->getMessage());
+ return json(['code' => 500, 'msg' => '退群失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 转移群聊到指定账号
+ * @param int $groupId 群ID(s2_wechat_chatroom表的id)
+ * @param string $chatroomId 群聊ID
+ * @param int $toAccountId 目标账号ID(s2_company_account表的id)
+ * @param int $toWechatAccountId 目标微信账号ID(s2_wechat_account表的id)
+ * @return array ['success' => bool, 'msg' => string]
+ */
+ protected function transferChatroomToAccount($groupId, $chatroomId, $toAccountId, $toWechatAccountId)
+ {
+ try {
+ // 查询目标账号信息
+ $targetAccount = Db::table('s2_company_account')
+ ->where('id', $toAccountId)
+ ->field('id,userName,realName,nickname')
+ ->find();
+
+ if (empty($targetAccount)) {
+ return ['success' => false, 'msg' => '目标账号不存在'];
+ }
+
+ // 查询目标微信账号信息
+ $targetWechatAccount = Db::table('s2_wechat_account')
+ ->where('id', $toWechatAccountId)
+ ->field('id,wechatId,deviceAccountId')
+ ->find();
+
+ if (empty($targetWechatAccount)) {
+ return ['success' => false, 'msg' => '目标微信账号不存在'];
+ }
+
+ // 调用 AutomaticAssign 进行群聊转移
+ $automaticAssign = new \app\api\controller\AutomaticAssign();
+
+ // 构建转移参数(通过 API 调用)
+ $transferData = [
+ 'wechatChatroomId' => $chatroomId, // 使用群聊ID
+ 'toAccountId' => $toAccountId,
+ 'wechatAccountKeyword' => $targetWechatAccount['wechatId'] ?? '',
+ 'isDeleted' => false
+ ];
+
+ // 直接更新数据库(因为 API 可能不支持指定单个群聊ID转移)
+ // 更新 s2_wechat_chatroom 表的 accountId 和 wechatAccountId
+ Db::table('s2_wechat_chatroom')
+ ->where('id', $groupId)
+ ->update([
+ 'accountId' => $toAccountId,
+ 'accountUserName' => $targetAccount['userName'] ?? '',
+ 'accountRealName' => $targetAccount['realName'] ?? '',
+ 'accountNickname' => $targetAccount['nickname'] ?? '',
+ 'wechatAccountId' => $toWechatAccountId,
+ 'wechatAccountWechatId' => $targetWechatAccount['wechatId'] ?? '',
+ 'updateTime' => time()
+ ]);
+
+ return ['success' => true, 'msg' => '转移成功'];
+ } catch (\Exception $e) {
+ \think\facade\Log::error("转移群聊异常。群ID: {$groupId}, 目标账号ID: {$toAccountId}, 错误: " . $e->getMessage());
+ return ['success' => false, 'msg' => $e->getMessage()];
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/workbench/WorkbenchGroupPushController.php b/application/cunkebao/controller/workbench/WorkbenchGroupPushController.php
new file mode 100644
index 0000000..52e3005
--- /dev/null
+++ b/application/cunkebao/controller/workbench/WorkbenchGroupPushController.php
@@ -0,0 +1,3782 @@
+request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ // 获取登录用户信息
+ $userInfo = request()->userInfo;
+
+ // 获取请求参数
+ $param = $this->request->post();
+
+
+ // 根据业务默认值补全参数
+ if (
+ isset($param['type']) &&
+ intval($param['type']) === self::TYPE_GROUP_PUSH
+ ) {
+ if (empty($param['startTime'])) {
+ $param['startTime'] = '09:00';
+ }
+ if (empty($param['endTime'])) {
+ $param['endTime'] = '21:00';
+ }
+ }
+
+ // 验证数据
+ $validate = new WorkbenchValidate;
+ if (!$validate->scene('create')->check($param)) {
+ return json(['code' => 400, 'msg' => $validate->getError()]);
+ }
+
+ Db::startTrans();
+ try {
+ // 创建工作台基本信息
+ $workbench = new Workbench;
+ $workbench->name = $param['name'];
+ $workbench->type = $param['type'];
+ $workbench->status = !empty($param['status']) ? 1 : 0;
+ $workbench->autoStart = !empty($param['autoStart']) ? 1 : 0;
+ $workbench->userId = $userInfo['id'];
+ $workbench->companyId = $userInfo['companyId'];
+ $workbench->createTime = time();
+ $workbench->updateTime = time();
+ $workbench->save();
+
+ // 根据类型创建对应的配置
+ switch ($param['type']) {
+ case self::TYPE_AUTO_LIKE: // 自动点赞
+ $config = new WorkbenchAutoLike;
+ $config->workbenchId = $workbench->id;
+ $config->interval = $param['interval'];
+ $config->maxLikes = $param['maxLikes'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->contentTypes = json_encode($param['contentTypes']);
+ $config->devices = json_encode($param['deviceGroups']);
+ $config->friends = json_encode($param['wechatFriends']);
+ // $config->targetGroups = json_encode($param['targetGroups']);
+ // $config->tagOperator = $param['tagOperator'];
+ $config->friendMaxLikes = $param['friendMaxLikes'];
+ $config->friendTags = $param['friendTags'];
+ $config->enableFriendTags = $param['enableFriendTags'];
+ $config->createTime = time();
+ $config->updateTime = time();
+ $config->save();
+ break;
+ case self::TYPE_MOMENTS_SYNC: // 朋友圈同步
+ $config = new WorkbenchMomentsSync;
+ $config->workbenchId = $workbench->id;
+ $config->syncInterval = $param['syncInterval'];
+ $config->syncCount = $param['syncCount'];
+ $config->syncType = $param['syncType'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->accountType = $param['accountType'];
+ $config->devices = json_encode($param['deviceGroups']);
+ $config->contentLibraries = json_encode($param['contentGroups'] ?? []);
+ $config->createTime = time();
+ $config->updateTime = time();
+ $config->save();
+ break;
+ case self::TYPE_GROUP_PUSH: // 群消息推送
+ $ownerWechatIds = $this->normalizeOwnerWechatIds($param['ownerWechatIds'] ?? []);
+ $groupPushData = $this->prepareGroupPushData($param, $ownerWechatIds);
+ $groupPushData['workbenchId'] = $workbench->id;
+ $groupPushData['createTime'] = time();
+ $groupPushData['updateTime'] = time();
+ $config = new WorkbenchGroupPush;
+ $config->save($groupPushData);
+ break;
+ case self::TYPE_GROUP_CREATE: // 自动建群
+ $config = new WorkbenchGroupCreate;
+ $config->workbenchId = $workbench->id;
+ $config->planType = !empty($param['planType']) ? $param['planType'] : 0;
+ $config->executorId = !empty($param['executorId']) ? $param['executorId'] : 0;
+
+ $config->devices = json_encode($param['deviceGroups'] ?? [], JSON_UNESCAPED_UNICODE);
+ $config->startTime = $param['startTime'] ?? '';
+ $config->endTime = $param['endTime'] ?? '';
+ $config->groupSizeMin = intval($param['groupSizeMin'] ?? 3);
+ $config->groupSizeMax = intval($param['groupSizeMax'] ?? 38);
+ $config->maxGroupsPerDay = intval($param['maxGroupsPerDay'] ?? 20);
+ $config->groupNameTemplate = $param['groupNameTemplate'] ?? '';
+ $config->groupDescription = $param['groupDescription'] ?? '';
+ $config->poolGroups = json_encode($param['poolGroups'] ?? [], JSON_UNESCAPED_UNICODE);
+ $config->wechatGroups = json_encode($param['wechatGroups'] ?? [], JSON_UNESCAPED_UNICODE);
+
+ // 处理群管理员:如果启用了群管理员且有指定管理员,则保存到admins字段
+ $admins = [];
+ if (!empty($param['groupAdminEnabled']) && !empty($param['groupAdminWechatId'])) {
+ // 如果groupAdminWechatId是数组,取第一个;如果是单个值,直接使用
+ $adminWechatId = is_array($param['groupAdminWechatId']) ? $param['groupAdminWechatId'][0] : $param['groupAdminWechatId'];
+ // 如果是好友ID,直接添加到admins;如果是wechatId,需要转换为好友ID
+ if (is_numeric($adminWechatId)) {
+ $admins[] = intval($adminWechatId);
+ } else {
+ // 如果是wechatId字符串,需要查询对应的好友ID
+ $friend = Db::table('s2_wechat_friend')->where('wechatId', $adminWechatId)->find();
+ if ($friend) {
+ $admins[] = intval($friend['id']);
+ }
+ }
+ }
+ // 如果传入了admins参数,优先使用(兼容旧逻辑)
+ if (!empty($param['admins']) && is_array($param['admins'])) {
+ $admins = array_merge($admins, $param['admins']);
+ }
+ $config->admins = json_encode(array_unique($admins), JSON_UNESCAPED_UNICODE);
+
+ $config->fixedWechatIds = json_encode($param['fixedWechatIds'] ?? [], JSON_UNESCAPED_UNICODE);
+ $config->createTime = time();
+ $config->updateTime = time();
+ $config->save();
+ break;
+ case self::TYPE_TRAFFIC_DISTRIBUTION: // 流量分发
+ $config = new WorkbenchTrafficConfig;
+ $config->workbenchId = $workbench->id;
+ $config->distributeType = $param['distributeType'];
+ $config->maxPerDay = $param['maxPerDay'];
+ $config->timeType = $param['timeType'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->devices = json_encode($param['deviceGroups'], JSON_UNESCAPED_UNICODE);
+ $config->pools = json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE);
+ $config->account = json_encode($param['accountGroups'], JSON_UNESCAPED_UNICODE);
+ $config->createTime = time();
+ $config->updateTime = time();
+ $config->save();
+ break;
+ case self::TYPE_IMPORT_CONTACT: //联系人导入
+ $config = new WorkbenchImportContact;
+ $config->workbenchId = $workbench->id;
+ $config->devices = json_encode($param['deviceGroups'], JSON_UNESCAPED_UNICODE);
+ $config->pools = json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE);
+ $config->num = $param['num'];
+ $config->clearContact = $param['clearContact'];
+ $config->remark = $param['remark'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->createTime = time();
+ $config->save();
+ break;
+ }
+
+ Db::commit();
+ return json(['code' => 200, 'msg' => '创建成功', 'data' => ['id' => $workbench->id]]);
+ } catch (\Exception $e) {
+ Db::rollback();
+ return json(['code' => 500, 'msg' => '创建失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 获取工作台列表
+ * @return \think\response\Json
+ */
+ public function getList()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $type = $this->request->param('type', '');
+ $keyword = $this->request->param('keyword', '');
+
+ $where = [
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['isDel', '=', 0]
+ ];
+
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+
+
+ // 添加类型筛选
+ if ($type !== '') {
+ $where[] = ['type', '=', $type];
+ }
+
+ // 添加名称模糊搜索
+ if ($keyword !== '') {
+ $where[] = ['name', 'like', '%' . $keyword . '%'];
+ }
+
+ // 定义关联关系
+ $with = [
+ 'autoLike' => function ($query) {
+ $query->field('workbenchId,interval,maxLikes,startTime,endTime,contentTypes,devices,friends');
+ },
+ 'momentsSync' => function ($query) {
+ $query->field('workbenchId,syncInterval,syncCount,syncType,startTime,endTime,accountType,devices,contentLibraries');
+ },
+ 'trafficConfig' => function ($query) {
+ $query->field('workbenchId,distributeType,maxPerDay,timeType,startTime,endTime,devices,pools,account');
+ },
+ 'groupPush' => function ($query) {
+ $query->field('workbenchId,pushType,targetType,groupPushSubType,startTime,endTime,maxPerDay,pushOrder,isLoop,status,groups,friends,ownerWechatIds,trafficPools,contentLibraries,friendIntervalMin,friendIntervalMax,messageIntervalMin,messageIntervalMax,isRandomTemplate,postPushTags,announcementContent,enableAiRewrite,aiRewritePrompt');
+ },
+ 'groupCreate' => function ($query) {
+ $query->field('workbenchId,devices,startTime,endTime,groupSizeMin,groupSizeMax,maxGroupsPerDay,groupNameTemplate,groupDescription,poolGroups,wechatGroups,admins');
+ },
+ 'importContact' => function ($query) {
+ $query->field('workbenchId,devices,pools,num,remarkType,remark,clearContact,startTime,endTime');
+ },
+ 'user' => function ($query) {
+ $query->field('id,username');
+ }
+ ];
+
+ $list = Workbench::where($where)
+ ->with($with)
+ ->field('id,companyId,name,type,status,autoStart,userId,createTime,updateTime')
+ ->order('id', 'desc')
+ ->page($page, $limit)
+ ->select()
+ ->each(function ($item) {
+ // 处理配置信息
+ switch ($item->type) {
+ case self::TYPE_AUTO_LIKE:
+ if (!empty($item->autoLike)) {
+ $item->config = $item->autoLike;
+ $item->config->devices = json_decode($item->config->devices, true);
+ $item->config->contentTypes = json_decode($item->config->contentTypes, true);
+ $item->config->friends = json_decode($item->config->friends, true);
+
+ // 添加今日点赞数
+ $startTime = strtotime(date('Y-m-d') . ' 00:00:00');
+ $endTime = strtotime(date('Y-m-d') . ' 23:59:59');
+ $todayLikeCount = Db::name('workbench_auto_like_item')
+ ->where('workbenchId', $item->id)
+ ->whereTime('createTime', 'between', [$startTime, $endTime])
+ ->count();
+
+ // 添加总点赞数
+ $totalLikeCount = Db::name('workbench_auto_like_item')
+ ->where('workbenchId', $item->id)
+ ->count();
+
+ $item->config->todayLikeCount = $todayLikeCount;
+ $item->config->totalLikeCount = $totalLikeCount;
+ }
+ unset($item->autoLike, $item->auto_like);
+ break;
+ case self::TYPE_MOMENTS_SYNC:
+ if (!empty($item->momentsSync)) {
+ $item->config = $item->momentsSync;
+ $item->config->devices = json_decode($item->config->devices, true);
+ $item->config->contentGroups = json_decode($item->config->contentLibraries, true);
+ //同步记录
+ $sendNum = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $item->id])->count();
+ $item->syncCount = $sendNum;
+ $lastTime = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $item->id])->order('id DESC')->value('createTime');
+ $item->lastSyncTime = !empty($lastTime) ? date('Y-m-d H:i', $lastTime) : '--';
+
+
+ // 获取内容库名称
+ if (!empty($item->config->contentGroups)) {
+ $libraryNames = ContentLibrary::where('id', 'in', $item->config->contentGroups)->select();
+ $item->config->contentGroupsOptions = $libraryNames;
+ } else {
+ $item->config->contentGroupsOptions = [];
+ }
+ }
+ unset($item->momentsSync, $item->moments_sync, $item->config->contentLibraries);
+ break;
+ case self::TYPE_GROUP_PUSH:
+ if (!empty($item->groupPush)) {
+ $item->config = $item->groupPush;
+ $item->config->pushType = $item->config->pushType;
+ $item->config->targetType = isset($item->config->targetType) ? intval($item->config->targetType) : 1; // 默认1=群推送
+ $item->config->groupPushSubType = isset($item->config->groupPushSubType) ? intval($item->config->groupPushSubType) : 1; // 默认1=群群发
+ $item->config->startTime = $item->config->startTime;
+ $item->config->endTime = $item->config->endTime;
+ $item->config->maxPerDay = $item->config->maxPerDay;
+ $item->config->pushOrder = $item->config->pushOrder;
+ $item->config->isLoop = $item->config->isLoop;
+ $item->config->status = $item->config->status;
+ $item->config->ownerWechatIds = json_decode($item->config->ownerWechatIds ?? '[]', true) ?: [];
+ // 根据targetType解析不同的数据
+ if ($item->config->targetType == 1) {
+ // 群推送
+ $item->config->wechatGroups = json_decode($item->config->groups, true) ?: [];
+ $item->config->wechatFriends = [];
+ // 群推送不需要devices字段
+ // 群公告相关字段
+ if ($item->config->groupPushSubType == 2) {
+ $item->config->announcementContent = isset($item->config->announcementContent) ? $item->config->announcementContent : '';
+ $item->config->enableAiRewrite = isset($item->config->enableAiRewrite) ? intval($item->config->enableAiRewrite) : 0;
+ $item->config->aiRewritePrompt = isset($item->config->aiRewritePrompt) ? $item->config->aiRewritePrompt : '';
+ }
+ $item->config->trafficPools = [];
+ } else {
+ // 好友推送
+ $item->config->wechatFriends = json_decode($item->config->friends, true) ?: [];
+ $item->config->wechatGroups = [];
+ $item->config->trafficPools = json_decode($item->config->trafficPools ?? '[]', true) ?: [];
+ }
+ $item->config->contentLibraries = json_decode($item->config->contentLibraries, true);
+ $item->config->postPushTags = json_decode($item->config->postPushTags ?? '[]', true) ?: [];
+ $item->config->lastPushTime = '';
+ if (!empty($item->config->ownerWechatIds)) {
+ $ownerWechatOptions = Db::name('wechat_account')
+ ->whereIn('id', $item->config->ownerWechatIds)
+ ->field('id,wechatId,nickName,avatar,alias')
+ ->select();
+ $item->config->ownerWechatOptions = $ownerWechatOptions;
+ } else {
+ $item->config->ownerWechatOptions = [];
+ }
+ }
+ unset($item->groupPush, $item->group_push);
+ break;
+ case self::TYPE_GROUP_CREATE:
+ if (!empty($item->groupCreate)) {
+ $item->config = $item->groupCreate;
+ $item->config->devices = json_decode($item->config->devices, true);
+ $item->config->poolGroups = json_decode($item->config->poolGroups, true);
+ $item->config->wechatGroups = json_decode($item->config->wechatGroups, true);
+ $item->config->admins = json_decode($item->config->admins ?? '[]', true) ?: [];
+
+ // 处理群管理员相关字段
+ $item->config->groupAdminEnabled = !empty($item->config->admins) ? 1 : 0;
+
+ if (!empty($item->config->admins)) {
+ $adminOptions = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->where('wf.id', 'in', $item->config->admins)
+ ->order('wf.id', 'desc')
+ ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar')
+ ->select();
+ $item->config->adminsOptions = $adminOptions;
+ // 如果有管理员,设置groupAdminWechatId为第一个管理员的ID(用于前端回显)
+ $item->config->groupAdminWechatId = !empty($item->config->admins) ? $item->config->admins[0] : null;
+ } else {
+ $item->config->adminsOptions = [];
+ $item->config->groupAdminWechatId = null;
+ }
+ }
+ unset($item->groupCreate, $item->group_create);
+ break;
+ case self::TYPE_TRAFFIC_DISTRIBUTION:
+ if (!empty($item->trafficConfig)) {
+ $item->config = $item->trafficConfig;
+ $item->config->devices = json_decode($item->config->devices, true);
+ $item->config->poolGroups = json_decode($item->config->pools, true);
+ $item->config->account = json_decode($item->config->account, true);
+ $config_item = Db::name('workbench_traffic_config_item')->where(['workbenchId' => $item->id])->order('id DESC')->find();
+ $item->config->lastUpdated = !empty($config_item) ? date('Y-m-d H:i', $config_item['createTime']) : '--';
+
+ //统计
+ $labels = $item->config->poolGroups;
+ $totalUsers = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->where([
+ ['wf.isDeleted', '=', 0],
+ ['sa.departmentId', '=', $item->companyId]
+ ])
+ ->whereIn('wa.currentDeviceId', $item->config->devices);
+
+ if (!empty($labels) && count($labels) > 0) {
+ $totalUsers = $totalUsers->where(function ($q) use ($labels) {
+ foreach ($labels as $label) {
+ $q->whereOrRaw("JSON_CONTAINS(wf.labels, '\"{$label}\"')");
+ }
+ });
+ }
+
+ $totalUsers = $totalUsers->count();
+ $totalAccounts = count($item->config->account);
+ $dailyAverage = Db::name('workbench_traffic_config_item')
+ ->where('workbenchId', $item->id)
+ ->count();
+ $day = (time() - strtotime($item->createTime)) / 86400;
+ $day = intval($day);
+ if ($dailyAverage > 0 && $totalAccounts > 0 && $day > 0) {
+ $dailyAverage = $dailyAverage / $totalAccounts / $day;
+ }
+ $item->config->total = [
+ 'dailyAverage' => intval($dailyAverage),
+ 'totalAccounts' => $totalAccounts,
+ 'deviceCount' => count($item->config->devices),
+ 'poolCount' => !empty($item->config->poolGroups) ? count($item->config->poolGroups) : 'ALL',
+ 'totalUsers' => $totalUsers >> 0
+ ];
+ }
+ unset($item->trafficConfig, $item->traffic_config);
+ break;
+
+ case self::TYPE_IMPORT_CONTACT:
+ if (!empty($item->importContact)) {
+ $item->config = $item->importContact;
+ $item->config->devices = json_decode($item->config->devices, true);
+ $item->config->poolGroups = json_decode($item->config->pools, true);
+ }
+ unset($item->importContact, $item->import_contact);
+ break;
+ }
+ // 添加创建人名称
+ $item['creatorName'] = $item->user ? $item->user->username : '';
+ unset($item['user']); // 移除关联数据
+ return $item;
+ });
+
+ $total = Workbench::where($where)->count();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]);
+ }
+
+ /**
+ * 获取工作台详情
+ * @param int $id 工作台ID
+ * @return \think\response\Json
+ */
+ public function detail()
+ {
+ $id = $this->request->param('id', '');
+
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ // 定义关联关系
+ $with = [
+ 'autoLike' => function ($query) {
+ $query->field('workbenchId,interval,maxLikes,startTime,endTime,contentTypes,devices,friends,friendMaxLikes,friendTags,enableFriendTags');
+ },
+ 'momentsSync' => function ($query) {
+ $query->field('workbenchId,syncInterval,syncCount,syncType,startTime,endTime,accountType,devices,contentLibraries');
+ },
+ 'trafficConfig' => function ($query) {
+ $query->field('workbenchId,distributeType,maxPerDay,timeType,startTime,endTime,devices,pools,account');
+ },
+ 'groupPush' => function ($query) {
+ $query->field('workbenchId,pushType,targetType,groupPushSubType,startTime,endTime,maxPerDay,pushOrder,isLoop,status,groups,friends,ownerWechatIds,trafficPools,contentLibraries,friendIntervalMin,friendIntervalMax,messageIntervalMin,messageIntervalMax,isRandomTemplate,postPushTags,announcementContent,enableAiRewrite,aiRewritePrompt');
+ },
+ 'groupCreate' => function ($query) {
+ $query->field('workbenchId,devices,startTime,endTime,groupSizeMin,groupSizeMax,maxGroupsPerDay,groupNameTemplate,groupDescription,poolGroups,wechatGroups,admins');
+ },
+ 'importContact' => function ($query) {
+ $query->field('workbenchId,devices,pools,num,remarkType,remark,clearContact,startTime,endTime');
+ },
+ ];
+
+ $where = [
+ ['id', '=', $id],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+
+ $workbench = Workbench::where($where)
+ ->field('id,name,type,status,autoStart,createTime,updateTime,companyId')
+ ->with($with)
+ ->find();
+
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在']);
+ }
+
+ // 处理配置信息
+ switch ($workbench->type) {
+ //自动点赞
+ case self::TYPE_AUTO_LIKE:
+ if (!empty($workbench->autoLike)) {
+ $workbench->config = $workbench->autoLike;
+ $workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
+ $workbench->config->wechatFriends = json_decode($workbench->config->friends, true);
+ $workbench->config->targetType = 2;
+ //$workbench->config->targetGroups = json_decode($workbench->config->targetGroups, true);
+ $workbench->config->contentTypes = json_decode($workbench->config->contentTypes, true);
+
+ // 添加今日点赞数
+ $startTime = strtotime(date('Y-m-d') . ' 00:00:00');
+ $endTime = strtotime(date('Y-m-d') . ' 23:59:59');
+ $todayLikeCount = Db::name('workbench_auto_like_item')
+ ->where('workbenchId', $workbench->id)
+ ->whereTime('createTime', 'between', [$startTime, $endTime])
+ ->count();
+
+ // 添加总点赞数
+ $totalLikeCount = Db::name('workbench_auto_like_item')
+ ->where('workbenchId', $workbench->id)
+ ->count();
+
+ $workbench->config->todayLikeCount = $todayLikeCount;
+ $workbench->config->totalLikeCount = $totalLikeCount;
+
+ unset($workbench->autoLike, $workbench->auto_like);
+ }
+ break;
+ //自动同步朋友圈
+ case self::TYPE_MOMENTS_SYNC:
+ if (!empty($workbench->momentsSync)) {
+ $workbench->config = $workbench->momentsSync;
+ $workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
+ $workbench->config->contentGroups = json_decode($workbench->config->contentLibraries, true);
+
+ //同步记录
+ $sendNum = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $workbench->id])->count();
+ $workbench->syncCount = $sendNum;
+ $lastTime = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $workbench->id])->order('id DESC')->value('createTime');
+ $workbench->lastSyncTime = !empty($lastTime) ? date('Y-m-d H:i', $lastTime) : '--';
+ unset($workbench->momentsSync, $workbench->moments_sync);
+ }
+ break;
+ //群推送
+ case self::TYPE_GROUP_PUSH:
+ if (!empty($workbench->groupPush)) {
+ $workbench->config = $workbench->groupPush;
+ $workbench->config->targetType = isset($workbench->config->targetType) ? intval($workbench->config->targetType) : 1; // 默认1=群推送
+ $workbench->config->groupPushSubType = isset($workbench->config->groupPushSubType) ? intval($workbench->config->groupPushSubType) : 1; // 默认1=群群发
+ $workbench->config->ownerWechatIds = json_decode($workbench->config->ownerWechatIds ?? '[]', true) ?: [];
+ // 根据targetType解析不同的数据
+ if ($workbench->config->targetType == 1) {
+ // 群推送
+ $workbench->config->wechatGroups = json_decode($workbench->config->groups, true) ?: [];
+ $workbench->config->wechatFriends = [];
+ $workbench->config->trafficPools = [];
+ // 群推送不需要devices字段
+ // 群公告相关字段
+ if ($workbench->config->groupPushSubType == 2) {
+ $workbench->config->announcementContent = isset($workbench->config->announcementContent) ? $workbench->config->announcementContent : '';
+ $workbench->config->enableAiRewrite = isset($workbench->config->enableAiRewrite) ? intval($workbench->config->enableAiRewrite) : 0;
+ $workbench->config->aiRewritePrompt = isset($workbench->config->aiRewritePrompt) ? $workbench->config->aiRewritePrompt : '';
+ }
+ } else {
+ // 好友推送
+ $workbench->config->wechatFriends = json_decode($workbench->config->friends, true) ?: [];
+ $workbench->config->wechatGroups = [];
+ $workbench->config->trafficPools = json_decode($workbench->config->trafficPools ?? '[]', true) ?: [];
+ }
+ $workbench->config->contentLibraries = json_decode($workbench->config->contentLibraries, true);
+ $workbench->config->postPushTags = json_decode($workbench->config->postPushTags ?? '[]', true) ?: [];
+ unset($workbench->groupPush, $workbench->group_push);
+ }
+ break;
+ //建群助手
+ case self::TYPE_GROUP_CREATE:
+ if (!empty($workbench->groupCreate)) {
+ $workbench->config = $workbench->groupCreate;
+ $workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
+ $workbench->config->poolGroups = json_decode($workbench->config->poolGroups, true);
+ $workbench->config->wechatGroups = json_decode($workbench->config->wechatGroups, true);
+ $workbench->config->admins = json_decode($workbench->config->admins ?? '[]', true) ?: [];
+
+ // 处理群管理员相关字段
+ $workbench->config->groupAdminEnabled = !empty($workbench->config->admins) ? 1 : 0;
+
+ // 如果有管理员,设置groupAdminWechatId为第一个管理员的ID(用于前端回显)
+ $workbench->config->groupAdminWechatId = !empty($workbench->config->admins) ? $workbench->config->admins[0] : null;
+
+ // 统计已建群数(状态为成功且groupId不为空的记录,按groupId分组去重)
+ $createdGroupsCount = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbench->id)
+ ->where('status', 2) // STATUS_SUCCESS = 2
+ ->where('groupId', '<>', null)
+ ->group('groupId')
+ ->count();
+
+ // 统计总人数(该工作台的所有记录数)
+ $totalMembersCount = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbench->id)
+ ->count();
+
+ // 添加统计信息
+ $workbench->config->stats = [
+ 'createdGroupsCount' => $createdGroupsCount,
+ 'totalMembersCount' => $totalMembersCount
+ ];
+
+ unset($workbench->groupCreate, $workbench->group_create);
+ }
+ break;
+ //流量分发
+ case self::TYPE_TRAFFIC_DISTRIBUTION:
+ if (!empty($workbench->trafficConfig)) {
+ $workbench->config = $workbench->trafficConfig;
+ $workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
+ $workbench->config->accountGroups = json_decode($workbench->config->account, true);
+ $workbench->config->poolGroups = json_decode($workbench->config->pools, true);
+ $config_item = Db::name('workbench_traffic_config_item')->where(['workbenchId' => $workbench->id])->order('id DESC')->find();
+ $workbench->config->lastUpdated = !empty($config_item) ? date('Y-m-d H:i', $config_item['createTime']) : '--';
+
+ //统计
+ $labels = $workbench->config->poolGroups;
+ $totalUsers = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->where([
+ ['wf.isDeleted', '=', 0],
+ ['sa.departmentId', '=', $workbench->companyId]
+ ])
+ ->whereIn('wa.currentDeviceId', $workbench->config->deviceGroups)
+ ->field('wf.id,wf.wechatAccountId,wf.wechatId,wf.labels,sa.userName,wa.currentDeviceId as deviceId')
+ ->where(function ($q) use ($labels) {
+ foreach ($labels as $label) {
+ $q->whereOrRaw("JSON_CONTAINS(wf.labels, '\"{$label}\"')");
+ }
+ })->count();
+
+ $totalAccounts = Db::table('s2_company_account')
+ ->alias('a')
+ ->where(['a.departmentId' => $workbench->companyId, 'a.status' => 0])
+ ->whereNotLike('a.userName', '%_offline%')
+ ->whereNotLike('a.userName', '%_delete%')
+ ->group('a.id')
+ ->count();
+
+ $dailyAverage = Db::name('workbench_traffic_config_item')
+ ->where('workbenchId', $workbench->id)
+ ->count();
+ $day = (time() - strtotime($workbench->createTime)) / 86400;
+ $day = intval($day);
+
+
+ if ($dailyAverage > 0) {
+ $dailyAverage = $dailyAverage / $totalAccounts / $day;
+ }
+
+ $workbench->config->total = [
+ 'dailyAverage' => intval($dailyAverage),
+ 'totalAccounts' => $totalAccounts,
+ 'deviceCount' => count($workbench->config->deviceGroups),
+ 'poolCount' => count($workbench->config->poolGroups),
+ 'totalUsers' => $totalUsers >> 0
+ ];
+ unset($workbench->trafficConfig, $workbench->traffic_config);
+ }
+ break;
+ case self::TYPE_IMPORT_CONTACT:
+ if (!empty($workbench->importContact)) {
+ $workbench->config = $workbench->importContact;
+ $workbench->config->deviceGroups = json_decode($workbench->config->devices, true);
+ $workbench->config->poolGroups = json_decode($workbench->config->pools, true);
+ }
+ unset($workbench->importContact, $workbench->import_contact);
+ break;
+ }
+ unset(
+ $workbench->autoLike,
+ $workbench->momentsSync,
+ $workbench->groupPush,
+ $workbench->groupCreate,
+ $workbench->config->devices,
+ $workbench->config->friends,
+ $workbench->config->groups,
+ $workbench->config->contentLibraries,
+ $workbench->config->account,
+ );
+
+
+ //获取设备信息
+ if (!empty($workbench->config->deviceGroups)) {
+ $deviceList = DeviceModel::alias('d')
+ ->field([
+ 'd.id', 'd.imei', 'd.memo', 'd.alive',
+ 'l.wechatId',
+ 'a.nickname', 'a.alias', 'a.avatar', 'a.alias', '0 totalFriend'
+ ])
+ ->leftJoin('device_wechat_login l', 'd.id = l.deviceId and l.alive =' . DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE . ' and l.companyId = d.companyId')
+ ->leftJoin('wechat_account a', 'l.wechatId = a.wechatId')
+ ->whereIn('d.id', $workbench->config->deviceGroups)
+ ->order('d.id desc')
+ ->select();
+
+ foreach ($deviceList as &$device) {
+ $curstomer = WechatCustomerModel::field('friendShip')->where(['wechatId' => $device['wechatId']])->find();
+ $device['totalFriend'] = $curstomer->friendShip->totalFriend ?? 0;
+ }
+ unset($device);
+
+ $workbench->config->deviceGroupsOptions = $deviceList;
+ } else {
+ $workbench->config->deviceGroupsOptions = [];
+ }
+
+
+ // 获取群(当targetType=1时)
+ if (!empty($workbench->config->wechatGroups) && isset($workbench->config->targetType) && $workbench->config->targetType == 1) {
+ $groupList = Db::name('wechat_group')->alias('wg')
+ ->join('wechat_account wa', 'wa.wechatId = wg.ownerWechatId')
+ ->where('wg.id', 'in', $workbench->config->wechatGroups)
+ ->order('wg.id', 'desc')
+ ->field('wg.id,wg.name as groupName,wg.ownerWechatId,wa.nickName,wa.avatar,wa.alias,wg.avatar as groupAvatar')
+ ->select();
+ $workbench->config->wechatGroupsOptions = $groupList;
+ } else {
+ $workbench->config->wechatGroupsOptions = [];
+ }
+
+ // 获取好友(当targetType=2时)
+ if (!empty($workbench->config->wechatFriends) && isset($workbench->config->targetType) && $workbench->config->targetType == 2) {
+ $friendList = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->where('wf.id', 'in', $workbench->config->wechatFriends)
+ ->order('wf.id', 'desc')
+ ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar')
+ ->select();
+ $workbench->config->wechatFriendsOptions = $friendList;
+ } else {
+ $workbench->config->wechatFriendsOptions = [];
+ }
+
+ // 获取流量池(当targetType=2时)
+ if (!empty($workbench->config->trafficPools) && isset($workbench->config->targetType) && $workbench->config->targetType == 2) {
+ $poolList = Db::name('traffic_source_package')->alias('tsp')
+ ->leftJoin('traffic_source_package_item tspi', 'tspi.packageId = tsp.id and tspi.isDel = 0')
+ ->whereIn('tsp.id', $workbench->config->trafficPools)
+ ->where('tsp.isDel', 0)
+ ->whereIn('tsp.companyId', [$this->request->userInfo['companyId'], 0])
+ ->field('tsp.id,tsp.name,tsp.description,tsp.pic,COUNT(tspi.id) as itemCount')
+ ->group('tsp.id')
+ ->order('tsp.id', 'desc')
+ ->select();
+ $workbench->config->trafficPoolsOptions = $poolList;
+ } else {
+ $workbench->config->trafficPoolsOptions = [];
+ }
+
+ // 获取内容库名称
+ if (!empty($workbench->config->contentGroups)) {
+ $libraryNames = ContentLibrary::where('id', 'in', $workbench->config->contentGroups)->select();
+ $workbench->config->contentGroupsOptions = $libraryNames;
+ } else {
+ $workbench->config->contentGroupsOptions = [];
+ }
+
+ //账号
+ if (!empty($workbench->config->accountGroups)) {
+ $account = Db::table('s2_company_account')->alias('a')
+ ->where(['a.departmentId' => $this->request->userInfo['companyId'], 'a.status' => 0])
+ ->whereIn('a.id', $workbench->config->accountGroups)
+ ->whereNotLike('a.userName', '%_offline%')
+ ->whereNotLike('a.userName', '%_delete%')
+ ->field('a.id,a.userName,a.realName,a.nickname,a.memo')
+ ->select();
+ $workbench->config->accountGroupsOptions = $account;
+ } else {
+ $workbench->config->accountGroupsOptions = [];
+ }
+
+ if (!empty($workbench->config->poolGroups)) {
+ $poolGroupsOptions = Db::name('traffic_source_package')->alias('tsp')
+ ->join('traffic_source_package_item tspi', 'tspi.packageId=tsp.id', 'left')
+ ->whereIn('tsp.companyId', [$this->request->userInfo['companyId'], 0])
+ ->whereIn('tsp.id', $workbench->config->poolGroups)
+ ->field('tsp.id,tsp.name,tsp.description,tsp.createTime,count(tspi.id) as num')
+ ->group('tsp.id')
+ ->select();
+ $workbench->config->poolGroupsOptions = $poolGroupsOptions;
+ } else {
+ $workbench->config->poolGroupsOptions = [];
+ }
+
+ if (!empty($workbench->config->ownerWechatIds)) {
+ $ownerWechatOptions = Db::name('wechat_account')
+ ->whereIn('id', $workbench->config->ownerWechatIds)
+ ->field('id,wechatId,nickName,avatar,alias')
+ ->select();
+ $workbench->config->ownerWechatOptions = $ownerWechatOptions;
+ } else {
+ $workbench->config->ownerWechatOptions = [];
+ }
+
+ // 获取群组选项(自动建群)
+ if ($workbench->type == self::TYPE_GROUP_CREATE && !empty($workbench->config->wechatGroups)) {
+ // 分离数字ID(好友ID)和字符串ID(手动创建的群组)
+ $friendIds = [];
+ $manualGroupIds = [];
+
+ foreach ($workbench->config->wechatGroups as $groupId) {
+ if (is_numeric($groupId)) {
+ $friendIds[] = intval($groupId);
+ } else {
+ $manualGroupIds[] = $groupId;
+ }
+ }
+
+ $wechatGroupsOptions = [];
+
+ // 查询好友信息(数字ID)
+ if (!empty($friendIds)) {
+ $friendList = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->join(['s2_company_account' => 'ca'], 'ca.id = wf.accountId', 'left')
+ ->where('wf.id', 'in', $friendIds)
+ ->order('wf.id', 'desc')
+ ->field('wf.id,wf.wechatId,wf.nickname,wf.avatar,wf.alias,wf.gender,wf.phone,wa.nickName as accountNickname,ca.userName as account,ca.realName as username,wf.createTime,wf.updateTime,wf.deleteTime,wf.ownerWechatId')
+ ->select();
+
+ // 获取群主信息
+ foreach ($friendList as &$friend) {
+ if (!empty($friend['ownerWechatId'])) {
+ $owner = Db::name('wechat_account')
+ ->where('wechatId', $friend['ownerWechatId'])
+ ->field('nickName,alias')
+ ->find();
+ $friend['ownerNickname'] = $owner['nickName'] ?? '';
+ $friend['ownerAlias'] = $owner['alias'] ?? '';
+ } else {
+ $friend['ownerNickname'] = '';
+ $friend['ownerAlias'] = '';
+ }
+ $friend['isManual'] = '';
+ // 格式化时间
+ $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['deleteTime'] = !empty($friend['deleteTime']) ? date('Y-m-d H:i:s', $friend['deleteTime']) : '';
+ }
+ unset($friend);
+
+ $wechatGroupsOptions = array_merge($wechatGroupsOptions, $friendList);
+ }
+
+ // 处理手动创建的群组(字符串ID)
+ if (!empty($manualGroupIds)) {
+ foreach ($manualGroupIds as $groupId) {
+ // 手动创建的群组,只返回基本信息
+ $wechatGroupsOptions[] = [
+ 'id' => $groupId,
+ 'wechatId' => $groupId,
+ 'nickname' => $groupId,
+ 'avatar' => '',
+ 'isManual' => 1
+ ];
+ }
+ }
+
+ $workbench->config->wechatGroupsOptions = $wechatGroupsOptions;
+ } else {
+ $workbench->config->wechatGroupsOptions = [];
+ }
+
+ // 获取管理员选项(自动建群)
+ if ($workbench->type == self::TYPE_GROUP_CREATE && !empty($workbench->config->admins)) {
+ $adminOptions = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->where('wf.id', 'in', $workbench->config->admins)
+ ->order('wf.id', 'desc')
+ ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar')
+ ->select();
+ $workbench->config->adminsOptions = $adminOptions;
+ } else {
+ $workbench->config->adminsOptions = [];
+ }
+
+ return json(['code' => 200, 'msg' => '获取成功', 'data' => $workbench]);
+ }
+
+ /**
+ * 更新工作台
+ * @return \think\response\Json
+ */
+ public function update()
+ {
+ if (!$this->request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ // 获取请求参数
+ $param = $this->request->post();
+
+ // 验证数据
+ $validate = new WorkbenchValidate;
+ if (!$validate->scene('update')->check($param)) {
+ return json(['code' => 400, 'msg' => $validate->getError()]);
+ }
+
+
+ $where = [
+ ['id', '=', $param['id']],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+ // 查询工作台是否存在
+ $workbench = Workbench::where($where)->find();
+ if (!$workbench) {
+ return json(['code' => 404, 'msg' => '工作台不存在']);
+ }
+
+ Db::startTrans();
+ try {
+ // 更新工作台基本信息
+ $workbench->name = $param['name'];
+ $workbench->status = !empty($param['status']) ? 1 : 0;
+ $workbench->autoStart = !empty($param['autoStart']) ? 1 : 0;
+ $workbench->updateTime = time();
+ $workbench->save();
+
+ // 根据类型更新对应的配置
+ switch ($workbench->type) {
+ case self::TYPE_AUTO_LIKE:
+ $config = WorkbenchAutoLike::where('workbenchId', $param['id'])->find();
+ if ($config) {
+ $config->interval = $param['interval'];
+ $config->maxLikes = $param['maxLikes'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->contentTypes = json_encode($param['contentTypes']);
+ $config->devices = json_encode($param['deviceGroups']);
+ $config->friends = json_encode($param['wechatFriends']);
+ // $config->targetGroups = json_encode($param['targetGroups']);
+ // $config->tagOperator = $param['tagOperator'];
+ $config->friendMaxLikes = $param['friendMaxLikes'];
+ $config->friendTags = $param['friendTags'];
+ $config->enableFriendTags = $param['enableFriendTags'];
+ $config->updateTime = time();
+ $config->save();
+ }
+ break;
+
+ case self::TYPE_MOMENTS_SYNC:
+ $config = WorkbenchMomentsSync::where('workbenchId', $param['id'])->find();
+ if ($config) {
+ if (!empty($param['contentGroups'])) {
+ foreach ($param['contentGroups'] as $library) {
+ if (isset($library['id']) && !empty($library['id'])) {
+ $contentLibraries[] = $library['id'];
+ } else {
+ $contentLibraries[] = $library;
+ }
+ }
+ } else {
+ $contentLibraries = [];
+ }
+
+ $config->syncInterval = $param['syncInterval'];
+ $config->syncCount = $param['syncCount'];
+ $config->syncType = $param['syncType'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->accountType = $param['accountType'];
+ $config->devices = json_encode($param['deviceGroups']);
+ $config->contentLibraries = json_encode($contentLibraries);
+ $config->updateTime = time();
+ $config->save();
+ }
+ break;
+
+ case self::TYPE_GROUP_PUSH:
+ $config = WorkbenchGroupPush::where('workbenchId', $param['id'])->find();
+ if ($config) {
+ $ownerWechatIds = $this->normalizeOwnerWechatIds($param['ownerWechatIds'] ?? null, $config);
+ $groupPushData = $this->prepareGroupPushData($param, $ownerWechatIds, $config);
+ $groupPushData['updateTime'] = time();
+ $config->save($groupPushData);
+ }
+ break;
+
+ case self::TYPE_GROUP_CREATE:
+ $config = WorkbenchGroupCreate::where('workbenchId', $param['id'])->find();
+ if ($config) {
+ $config->devices = json_encode($param['deviceGroups'] ?? [], JSON_UNESCAPED_UNICODE);
+ $config->startTime = $param['startTime'] ?? '';
+ $config->endTime = $param['endTime'] ?? '';
+ $config->groupSizeMin = intval($param['groupSizeMin'] ?? 3);
+ $config->groupSizeMax = intval($param['groupSizeMax'] ?? 38);
+ $config->maxGroupsPerDay = intval($param['maxGroupsPerDay'] ?? 20);
+ $config->groupNameTemplate = $param['groupNameTemplate'] ?? '';
+ $config->groupDescription = $param['groupDescription'] ?? '';
+ $config->poolGroups = json_encode($param['poolGroups'] ?? [], JSON_UNESCAPED_UNICODE);
+ $config->wechatGroups = json_encode($param['wechatGroups'] ?? [], JSON_UNESCAPED_UNICODE);
+
+ // 处理群管理员:如果启用了群管理员且有指定管理员,则保存到admins字段
+ $admins = [];
+ if (!empty($param['groupAdminEnabled']) && !empty($param['groupAdminWechatId'])) {
+ // 如果groupAdminWechatId是数组,取第一个;如果是单个值,直接使用
+ $adminWechatId = is_array($param['groupAdminWechatId']) ? $param['groupAdminWechatId'][0] : $param['groupAdminWechatId'];
+ // 如果是好友ID,直接添加到admins;如果是wechatId,需要转换为好友ID
+ if (is_numeric($adminWechatId)) {
+ $admins[] = intval($adminWechatId);
+ } else {
+ // 如果是wechatId字符串,需要查询对应的好友ID
+ $friend = Db::table('s2_wechat_friend')->where('wechatId', $adminWechatId)->find();
+ if ($friend) {
+ $admins[] = intval($friend['id']);
+ }
+ }
+ }
+ // 如果传入了admins参数,优先使用(兼容旧逻辑)
+ if (!empty($param['admins']) && is_array($param['admins'])) {
+ $admins = array_merge($admins, $param['admins']);
+ }
+ $config->admins = json_encode(array_unique($admins), JSON_UNESCAPED_UNICODE);
+
+ $config->updateTime = time();
+ $config->save();
+ }
+ break;
+ case self::TYPE_TRAFFIC_DISTRIBUTION:
+ $config = WorkbenchTrafficConfig::where('workbenchId', $param['id'])->find();
+ if ($config) {
+ $config->distributeType = $param['distributeType'];
+ $config->maxPerDay = $param['maxPerDay'];
+ $config->timeType = $param['timeType'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->devices = json_encode($param['deviceGroups']);
+ $config->pools = json_encode($param['poolGroups']);
+ $config->account = json_encode($param['accountGroups']);
+ $config->updateTime = time();
+ $config->save();
+ }
+ break;
+ case self::TYPE_IMPORT_CONTACT: //联系人导入
+ $config = WorkbenchImportContact::where('workbenchId', $param['id'])->find();;
+ if ($config) {
+ $config->devices = json_encode($param['deviceGroups']);
+ $config->pools = json_encode($param['poolGroups']);
+ $config->num = $param['num'];
+ $config->clearContact = $param['clearContact'];
+ $config->remark = $param['remark'];
+ $config->startTime = $param['startTime'];
+ $config->endTime = $param['endTime'];
+ $config->save();
+ }
+ break;
+ }
+
+ Db::commit();
+ return json(['code' => 200, 'msg' => '更新成功']);
+ } catch (\Exception $e) {
+ Db::rollback();
+ return json(['code' => 500, 'msg' => '更新失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 更新工作台状态
+ * @return \think\response\Json
+ */
+ public function updateStatus()
+ {
+ if (!$this->request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ $id = $this->request->param('id', '');
+
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+
+
+ $where = [
+ ['id', '=', $id],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+ $workbench = Workbench::where($where)->find();
+
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在']);
+ }
+
+ $workbench->status = !$workbench['status'];
+ $workbench->save();
+
+ return json(['code' => 200, 'msg' => '更新成功']);
+ }
+
+ /**
+ * 删除工作台(软删除)
+ */
+ public function delete()
+ {
+ $id = $this->request->param('id');
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ $where = [
+ ['id', '=', $id],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+ $workbench = Workbench::where($where)->find();
+
+ if (!$workbench) {
+ return json(['code' => 404, 'msg' => '工作台不存在']);
+ }
+
+ // 软删除
+ $workbench->isDel = 1;
+ $workbench->deleteTime = time();
+ $workbench->save();
+
+ return json(['code' => 200, 'msg' => '删除成功']);
+ }
+
+ /**
+ * 拷贝工作台
+ * @return \think\response\Json
+ */
+ public function copy()
+ {
+ if (!$this->request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ $id = $this->request->post('id');
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ // 验证权限并获取原数据
+ $workbench = Workbench::where([
+ ['id', '=', $id],
+ ['userId', '=', $this->request->userInfo['id']]
+ ])->find();
+
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在']);
+ }
+
+ Db::startTrans();
+ try {
+ // 创建新的工作台基本信息
+ $newWorkbench = new Workbench;
+ $newWorkbench->name = $workbench->name . ' copy';
+ $newWorkbench->type = $workbench->type;
+ $newWorkbench->status = 1; // 新拷贝的默认启用
+ $newWorkbench->autoStart = $workbench->autoStart;
+ $newWorkbench->userId = $this->request->userInfo['id'];
+ $newWorkbench->companyId = $this->request->userInfo['companyId'];
+ $newWorkbench->save();
+
+ // 根据类型拷贝对应的配置
+ switch ($workbench->type) {
+ case self::TYPE_AUTO_LIKE:
+ $config = WorkbenchAutoLike::where('workbenchId', $id)->find();
+ if ($config) {
+ $newConfig = new WorkbenchAutoLike;
+ $newConfig->workbenchId = $newWorkbench->id;
+ $newConfig->interval = $config->interval;
+ $newConfig->maxLikes = $config->maxLikes;
+ $newConfig->startTime = $config->startTime;
+ $newConfig->endTime = $config->endTime;
+ $newConfig->contentTypes = $config->contentTypes;
+ $newConfig->devices = $config->devices;
+ $newConfig->friends = $config->friends;
+ $newConfig->createTime = time();
+ $newConfig->updateTime = time();
+ $newConfig->save();
+ }
+ break;
+ case self::TYPE_MOMENTS_SYNC:
+ $config = WorkbenchMomentsSync::where('workbenchId', $id)->find();
+ if ($config) {
+ $newConfig = new WorkbenchMomentsSync;
+ $newConfig->workbenchId = $newWorkbench->id;
+ $newConfig->syncInterval = $config->syncInterval;
+ $newConfig->syncCount = $config->syncCount;
+ $newConfig->syncType = $config->syncType;
+ $newConfig->startTime = $config->startTime;
+ $newConfig->endTime = $config->endTime;
+ $newConfig->accountType = $config->accountType;
+ $newConfig->devices = $config->devices;
+ $newConfig->contentLibraries = $config->contentLibraries;
+ $newConfig->createTime = time();
+ $newConfig->updateTime = time();
+ $newConfig->save();
+ }
+ break;
+ case self::TYPE_GROUP_PUSH:
+ $config = WorkbenchGroupPush::where('workbenchId', $id)->find();
+ if ($config) {
+ $newConfig = new WorkbenchGroupPush;
+ $newConfig->workbenchId = $newWorkbench->id;
+ $newConfig->pushType = $config->pushType;
+ $newConfig->targetType = isset($config->targetType) ? $config->targetType : 1; // 默认1=群推送
+ $newConfig->startTime = $config->startTime;
+ $newConfig->endTime = $config->endTime;
+ $newConfig->maxPerDay = $config->maxPerDay;
+ $newConfig->pushOrder = $config->pushOrder;
+ $newConfig->isLoop = $config->isLoop;
+ $newConfig->status = $config->status;
+ $newConfig->groups = $config->groups;
+ $newConfig->friends = $config->friends;
+ $newConfig->contentLibraries = $config->contentLibraries;
+ $newConfig->trafficPools = property_exists($config, 'trafficPools') ? $config->trafficPools : json_encode([], JSON_UNESCAPED_UNICODE);
+ $newConfig->socialMediaId = $config->socialMediaId;
+ $newConfig->promotionSiteId = $config->promotionSiteId;
+ $newConfig->ownerWechatIds = $config->ownerWechatIds;
+ $newConfig->createTime = time();
+ $newConfig->updateTime = time();
+ $newConfig->save();
+ }
+ break;
+ case self::TYPE_GROUP_CREATE:
+ $config = WorkbenchGroupCreate::where('workbenchId', $id)->find();
+ if ($config) {
+ $newConfig = new WorkbenchGroupCreate;
+ $newConfig->workbenchId = $newWorkbench->id;
+ $newConfig->devices = $config->devices;
+ $newConfig->startTime = $config->startTime;
+ $newConfig->endTime = $config->endTime;
+ $newConfig->groupSizeMin = $config->groupSizeMin;
+ $newConfig->groupSizeMax = $config->groupSizeMax;
+ $newConfig->maxGroupsPerDay = $config->maxGroupsPerDay;
+ $newConfig->groupNameTemplate = $config->groupNameTemplate;
+ $newConfig->groupDescription = $config->groupDescription;
+ $newConfig->poolGroups = $config->poolGroups;
+ $newConfig->wechatGroups = $config->wechatGroups;
+ $newConfig->admins = $config->admins ?? json_encode([], JSON_UNESCAPED_UNICODE);
+ $newConfig->createTime = time();
+ $newConfig->updateTime = time();
+ $newConfig->save();
+ }
+ break;
+ case self::TYPE_IMPORT_CONTACT: //联系人导入
+ $config = WorkbenchImportContact::where('workbenchId', $id)->find();
+ if ($config) {
+ $newConfig = new WorkbenchImportContact;
+ $newConfig->workbenchId = $newWorkbench->id;
+ $newConfig->devices = $config->devices;
+ $newConfig->pools = $config->pools;
+ $newConfig->num = $config->num;
+ $newConfig->clearContact = $config->clearContact;
+ $newConfig->remark = $config->remark;
+ $newConfig->startTime = $config->startTime;
+ $newConfig->endTime = $config->endTime;
+ $newConfig->createTime = time();
+ $newConfig->save();
+ }
+ break;
+ }
+
+ Db::commit();
+ return json(['code' => 200, 'msg' => '拷贝成功', 'data' => ['id' => $newWorkbench->id]]);
+ } catch (\Exception $e) {
+ Db::rollback();
+ return json(['code' => 500, 'msg' => '拷贝失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 获取点赞记录列表
+ * @return \think\response\Json
+ */
+ public function getLikeRecords()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchAutoLikeController();
+ $controller->request = $this->request;
+ return $controller->getLikeRecords();
+ }
+
+ /**
+ * 获取朋友圈发布记录列表
+ * @return \think\response\Json
+ */
+ public function getMomentsRecords()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchMomentsController();
+ $controller->request = $this->request;
+ return $controller->getMomentsRecords();
+ }
+
+ /**
+ * 获取朋友圈发布统计
+ * @return \think\response\Json
+ */
+ public function getMomentsStats()
+ {
+ $controller = new \app\cunkebao\controller\workbench\WorkbenchMomentsController();
+ $controller->request = $this->request;
+ return $controller->getMomentsStats();
+ }
+
+ /**
+ * 获取流量分发记录列表
+ * @return \think\response\Json
+ */
+ public function getTrafficDistributionRecords()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $workbenchId = $this->request->param('workbenchId', 0);
+
+ $where = [
+ ['wtdi.workbenchId', '=', $workbenchId]
+ ];
+
+ // 查询分发记录
+ $list = Db::name('workbench_traffic_distribution_item')->alias('wtdi')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wtdi.wechatAccountId', 'left')
+ ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wtdi.wechatFriendId', 'left')
+ ->field([
+ 'wtdi.id',
+ 'wtdi.workbenchId',
+ 'wtdi.wechatAccountId',
+ 'wtdi.wechatFriendId',
+ 'wtdi.createTime as distributeTime',
+ 'wtdi.status',
+ 'wtdi.errorMsg',
+ 'wa.nickName as operatorName',
+ 'wa.avatar as operatorAvatar',
+ 'wf.nickName as friendName',
+ 'wf.avatar as friendAvatar',
+ 'wf.gender',
+ 'wf.province',
+ 'wf.city'
+ ])
+ ->where($where)
+ ->order('wtdi.createTime', 'desc')
+ ->page($page, $limit)
+ ->select();
+
+ // 处理数据
+ foreach ($list as &$item) {
+ // 处理时间格式
+ $item['distributeTime'] = date('Y-m-d H:i:s', $item['distributeTime']);
+
+ // 处理性别
+ $genderMap = [
+ 0 => '未知',
+ 1 => '男',
+ 2 => '女'
+ ];
+ $item['genderText'] = $genderMap[$item['gender']] ?? '未知';
+
+ // 处理状态文字
+ $statusMap = [
+ 0 => '待分发',
+ 1 => '分发成功',
+ 2 => '分发失败'
+ ];
+ $item['statusText'] = $statusMap[$item['status']] ?? '未知状态';
+ }
+
+ // 获取总记录数
+ $total = Db::name('workbench_traffic_distribution_item')->alias('wtdi')
+ ->where($where)
+ ->count();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]);
+ }
+
+ /**
+ * 获取流量分发统计
+ * @return \think\response\Json
+ */
+ public function getTrafficDistributionStats()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ // 获取今日数据
+ $todayStart = strtotime(date('Y-m-d') . ' 00:00:00');
+ $todayEnd = strtotime(date('Y-m-d') . ' 23:59:59');
+
+ $todayStats = Db::name('workbench_traffic_distribution_item')
+ ->where([
+ ['workbenchId', '=', $workbenchId],
+ ['createTime', 'between', [$todayStart, $todayEnd]]
+ ])
+ ->field([
+ 'COUNT(*) as total',
+ 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success',
+ 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed'
+ ])
+ ->find();
+
+ // 获取总数据
+ $totalStats = Db::name('workbench_traffic_distribution_item')
+ ->where('workbenchId', $workbenchId)
+ ->field([
+ 'COUNT(*) as total',
+ 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success',
+ 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed'
+ ])
+ ->find();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'today' => [
+ 'total' => intval($todayStats['total']),
+ 'success' => intval($todayStats['success']),
+ 'failed' => intval($todayStats['failed'])
+ ],
+ 'total' => [
+ 'total' => intval($totalStats['total']),
+ 'success' => intval($totalStats['success']),
+ 'failed' => intval($totalStats['failed'])
+ ]
+ ]
+ ]);
+ }
+
+ /**
+ * 获取流量分发详情
+ * @return \think\response\Json
+ */
+ public function getTrafficDistributionDetail()
+ {
+ $id = $this->request->param('id', 0);
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ $detail = Db::name('workbench_traffic_distribution_item')->alias('wtdi')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wtdi.wechatAccountId', 'left')
+ ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wtdi.wechatFriendId', 'left')
+ ->field([
+ 'wtdi.id',
+ 'wtdi.workbenchId',
+ 'wtdi.wechatAccountId',
+ 'wtdi.wechatFriendId',
+ 'wtdi.createTime as distributeTime',
+ 'wtdi.status',
+ 'wtdi.errorMsg',
+ 'wa.nickName as operatorName',
+ 'wa.avatar as operatorAvatar',
+ 'wf.nickName as friendName',
+ 'wf.avatar as friendAvatar',
+ 'wf.gender',
+ 'wf.province',
+ 'wf.city',
+ 'wf.signature',
+ 'wf.remark'
+ ])
+ ->where('wtdi.id', $id)
+ ->find();
+
+ if (empty($detail)) {
+ return json(['code' => 404, 'msg' => '记录不存在']);
+ }
+
+ // 处理数据
+ $detail['distributeTime'] = date('Y-m-d H:i:s', $detail['distributeTime']);
+
+ // 处理性别
+ $genderMap = [
+ 0 => '未知',
+ 1 => '男',
+ 2 => '女'
+ ];
+ $detail['genderText'] = $genderMap[$detail['gender']] ?? '未知';
+
+ // 处理状态文字
+ $statusMap = [
+ 0 => '待分发',
+ 1 => '分发成功',
+ 2 => '分发失败'
+ ];
+ $detail['statusText'] = $statusMap[$detail['status']] ?? '未知状态';
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => $detail
+ ]);
+ }
+
+ /**
+ * 创建流量分发计划
+ * @return \think\response\Json
+ */
+ public function createTrafficPlan()
+ {
+ $param = $this->request->post();
+ Db::startTrans();
+ try {
+ // 1. 创建主表
+ $planId = Db::name('ck_workbench')->insertGetId([
+ 'name' => $param['name'],
+ 'type' => self::TYPE_TRAFFIC_DISTRIBUTION,
+ 'status' => 1,
+ 'autoStart' => $param['autoStart'] ?? 0,
+ 'userId' => $this->request->userInfo['id'],
+ 'companyId' => $this->request->userInfo['companyId'],
+ 'createTime' => time(),
+ 'updateTime' => time()
+ ]);
+ // 2. 创建扩展表
+ Db::name('ck_workbench_traffic_config')->insert([
+ 'workbenchId' => $planId,
+ 'distributeType' => $param['distributeType'],
+ 'maxPerDay' => $param['maxPerDay'],
+ 'timeType' => $param['timeType'],
+ 'startTime' => $param['startTime'],
+ 'endTime' => $param['endTime'],
+ 'targets' => json_encode($param['targets'], JSON_UNESCAPED_UNICODE),
+ 'pools' => json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE),
+ 'createTime' => time(),
+ 'updateTime' => time()
+ ]);
+ Db::commit();
+ return json(['code' => 200, 'msg' => '创建成功']);
+ } catch (\Exception $e) {
+ Db::rollback();
+ return json(['code' => 500, 'msg' => '创建失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 获取所有微信好友标签及数量统计
+ * @return \think\response\Json
+ */
+ public function getDeviceLabels()
+ {
+ $deviceIds = $this->request->param('deviceIds', '');
+ $companyId = $this->request->userInfo['companyId'];
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+
+ $where = [
+ ['wc.companyId', '=', $companyId],
+ ];
+
+ if (!empty($deviceIds)) {
+ $deviceIds = explode(',', $deviceIds);
+ $where[] = ['dwl.deviceId', 'in', $deviceIds];
+ }
+
+ $wechatAccounts = Db::name('wechat_customer')->alias('wc')
+ ->join('device_wechat_login dwl', 'dwl.wechatId = wc.wechatId AND dwl.companyId = wc.companyId AND dwl.alive = 1')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.wechatId = wc.wechatId')
+ ->where($where)
+ ->field('wa.id,wa.wechatId,wa.nickName,wa.labels')
+ ->select();
+ $labels = [];
+ $wechatIds = [];
+ foreach ($wechatAccounts as $account) {
+ $labelArr = json_decode($account['labels'], true);
+ if (is_array($labelArr)) {
+ foreach ($labelArr as $label) {
+ if ($label !== '' && $label !== null) {
+ $labels[] = $label;
+ }
+ }
+ }
+ $wechatIds[] = $account['wechatId'];
+ }
+ // 去重(只保留一个)
+ $labels = array_values(array_unique($labels));
+ $wechatIds = array_unique($wechatIds);
+
+ // 搜索过滤
+ if (!empty($keyword)) {
+ $labels = array_filter($labels, function ($label) use ($keyword) {
+ return mb_stripos($label, $keyword) !== false;
+ });
+ $labels = array_values($labels); // 重新索引数组
+ }
+
+
+ // 分页处理
+ $labels2 = array_slice($labels, ($page - 1) * $limit, $limit);
+
+ // 统计数量
+ $newLabel = [];
+ foreach ($labels2 as $label) {
+ $friendCount = Db::table('s2_wechat_friend')
+ ->whereIn('ownerWechatId', $wechatIds)
+ ->where('labels', 'like', '%"' . $label . '"%')
+ ->count();
+ $newLabel[] = [
+ 'label' => $label,
+ 'count' => $friendCount
+ ];
+ }
+
+ // 返回结果
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $newLabel,
+ 'total' => count($labels),
+ ]
+ ]);
+ }
+
+
+ /**
+ * 获取群列表
+ * @return \think\response\Json
+ */
+ public function getGroupList()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+
+ $where = [
+ ['wg.deleteTime', '=', 0],
+ ['wg.companyId', '=', $this->request->userInfo['companyId']],
+ ];
+
+ if (!empty($keyword)) {
+ $where[] = ['wg.name', 'like', '%' . $keyword . '%'];
+ }
+
+ $query = Db::name('wechat_group')->alias('wg')
+ ->join('wechat_account wa', 'wa.wechatId = wg.ownerWechatId')
+ ->where($where);
+
+ $total = $query->count();
+ $list = $query->order('wg.id', 'desc')
+ ->field('wg.id,wg.name as groupName,wg.ownerWechatId,wa.nickName,wg.createTime,wa.avatar,wa.alias,wg.avatar as groupAvatar')
+ ->page($page, $limit)
+ ->select();
+
+ // 优化:格式化时间,头像兜底
+ $defaultGroupAvatar = '';
+ $defaultAvatar = '';
+ foreach ($list as &$item) {
+ $item['createTime'] = $item['createTime'] ? date('Y-m-d H:i:s', $item['createTime']) : '';
+ $item['groupAvatar'] = $item['groupAvatar'] ?: $defaultGroupAvatar;
+ $item['avatar'] = $item['avatar'] ?: $defaultAvatar;
+ }
+
+ return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]);
+ }
+
+ /**
+ * 获取流量池列表
+ * @return \think\response\Json
+ */
+ public function getTrafficPoolList()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+ $companyId = $this->request->userInfo['companyId'];
+
+ $baseQuery = Db::name('traffic_source_package')->alias('tsp')
+ ->where('tsp.isDel', 0)
+ ->whereIn('tsp.companyId', [$companyId, 0]);
+
+ if (!empty($keyword)) {
+ $baseQuery->whereLike('tsp.name', '%' . $keyword . '%');
+ }
+
+ $total = (clone $baseQuery)->count();
+
+ $list = $baseQuery
+ ->leftJoin('traffic_source_package_item tspi', 'tspi.packageId = tsp.id and tspi.isDel = 0')
+ ->field('tsp.id,tsp.name,tsp.description,tsp.pic,tsp.companyId,COUNT(tspi.id) as itemCount,max(tspi.createTime) as latestImportTime')
+ ->group('tsp.id')
+ ->order('tsp.id', 'desc')
+ ->page($page, $limit)
+ ->select();
+
+ foreach ($list as &$item) {
+ $item['latestImportTime'] = !empty($item['latestImportTime']) ? date('Y-m-d H:i:s', $item['latestImportTime']) : '';
+ }
+ unset($item);
+
+ return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]);
+ }
+
+
+ public function getAccountList()
+ {
+ $companyId = $this->request->userInfo['companyId'];
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $query = Db::table('s2_company_account')
+ ->alias('a')
+ ->where(['a.departmentId' => $companyId, 'a.status' => 0])
+ ->whereNotLike('a.userName', '%_offline%')
+ ->whereNotLike('a.userName', '%_delete%');
+
+ $total = $query->count();
+ $list = $query->field('a.id,a.userName,a.realName,a.nickname,a.memo')
+ ->page($page, $limit)
+ ->select();
+
+
+ return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]);
+ }
+
+
+ /**
+ * 获取京东联盟导购媒体
+ * @return \think\response\Json
+ * @throws \think\db\exception\DataNotFoundException
+ * @throws \think\db\exception\ModelNotFoundException
+ * @throws \think\exception\DbException
+ */
+ public function getJdSocialMedia()
+ {
+ $data = Db::name('jd_social_media')->order('id DESC')->select();
+ return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]);
+ }
+
+ /**
+ * 获取京东联盟广告位
+ * @return \think\response\Json
+ * @throws \think\db\exception\DataNotFoundException
+ * @throws \think\db\exception\ModelNotFoundException
+ * @throws \think\exception\DbException
+ */
+ public function getJdPromotionSite()
+ {
+ $id = $this->request->param('id', '');
+ if (empty($id)) {
+ return json(['code' => 500, 'msg' => '参数缺失']);
+ }
+
+ $data = Db::name('jd_promotion_site')->where('jdSocialMediaId', $id)->order('id DESC')->select();
+ return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]);
+ }
+
+
+ //京东转链-京推推
+ public function changeLink($content = '', $positionid = '')
+ {
+ $unionId = Env::get('jd.unionId', '');
+ $jttAppId = Env::get('jd.jttAppId', '');
+ $appKey = Env::get('jd.appKey', '');
+ $apiUrl = Env::get('jd.apiUrl', '');
+
+ $content = !empty($content) ? $content : $this->request->param('content', '');
+ $positionid = !empty($positionid) ? $positionid : $this->request->param('positionid', '');
+
+
+ if (empty($content)) {
+ return json_encode(['code' => 500, 'msg' => '转链的内容为空']);
+ }
+
+ // 验证是否包含链接
+ if (!$this->containsLink($content)) {
+ return json_encode(['code' => 500, 'msg' => '内容中未检测到有效链接']);
+ }
+
+ if (empty($unionId) || empty($jttAppId) || empty($appKey) || empty($apiUrl)) {
+ return json_encode(['code' => 500, 'msg' => '参数缺失']);
+ }
+ $params = [
+ 'unionid' => $unionId,
+ 'content' => $content,
+ 'appid' => $jttAppId,
+ 'appkey' => $appKey,
+ 'v' => 'v2'
+ ];
+
+ if (!empty($positionid)) {
+ $params['positionid'] = $positionid;
+ }
+
+ $res = requestCurl($apiUrl, $params, 'GET', [], 'json');
+ $res = json_decode($res, true);
+ if (empty($res)) {
+ return json_encode(['code' => 500, 'msg' => '未知错误']);
+ }
+ $result = $res['result'];
+ if ($res['return'] == 0) {
+ return json_encode(['code' => 200, 'data' => $result['chain_content'], 'msg' => $result['msg']]);
+ } else {
+ return json_encode(['code' => 500, 'msg' => $result['msg']]);
+ }
+ }
+
+
+ public function getTrafficList()
+ {
+ $companyId = $this->request->userInfo['companyId'];
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+ $workbenchId = $this->request->param('workbenchId', '');
+ $isRecycle = $this->request->param('isRecycle', '');
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ $workbench = Db::name('workbench')->where(['id' => $workbenchId, 'isDel' => 0, 'companyId' => $companyId, 'type' => 5])->find();
+
+ if (empty($workbench)) {
+ return json(['code' => 400, 'msg' => '该任务不存在或已删除']);
+ }
+ $query = Db::name('workbench_traffic_config_item')->alias('wtc')
+ ->join(['s2_wechat_friend' => 'wf'], 'wtc.wechatFriendId = wf.id')
+ ->join('users u', 'wtc.wechatAccountId = u.s2_accountId', 'left')
+ ->field([
+ 'wtc.id', 'wtc.isRecycle', 'wtc.isRecycle', 'wtc.createTime','wtc.recycleTime',
+ 'wf.wechatId', 'wf.alias', 'wf.nickname', 'wf.avatar', 'wf.gender', 'wf.phone',
+ 'u.account', 'u.username'
+ ])
+ ->where(['wtc.workbenchId' => $workbenchId])
+ ->order('wtc.id DESC');
+
+ if (!empty($keyword)) {
+ $query->where('wf.wechatId|wf.alias|wf.nickname|wf.phone|u.account|u.username', 'like', '%' . $keyword . '%');
+ }
+
+ if ($isRecycle != '' || $isRecycle != null) {
+ $query->where('isRecycle',$isRecycle);
+ }
+
+
+
+ $total = $query->count();
+ $list = $query->page($page, $limit)->select();
+
+ foreach ($list as &$item) {
+ $item['createTime'] = date('Y-m-d H:i:s', $item['createTime']);
+ $item['recycleTime'] = date('Y-m-d H:i:s', $item['recycleTime']);
+ }
+ unset($item);
+
+
+ $data = [
+ 'total' => $total,
+ 'list' => $list,
+ ];
+
+ return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]);
+
+ }
+
+ /**
+ * 规范化客服微信ID列表
+ * @param mixed $ownerWechatIds
+ * @param WorkbenchGroupPush|null $originalConfig
+ * @return array
+ * @throws \Exception
+ */
+ private function normalizeOwnerWechatIds($ownerWechatIds, WorkbenchGroupPush $originalConfig = null): array
+ {
+ if ($ownerWechatIds === null) {
+ $existing = $originalConfig ? $this->decodeJsonArray($originalConfig->ownerWechatIds ?? []) : [];
+ if (empty($existing)) {
+ throw new \Exception('请至少选择一个客服微信');
+ }
+ return $existing;
+ }
+
+ if (!is_array($ownerWechatIds)) {
+ throw new \Exception('客服参数格式错误');
+ }
+
+ $normalized = $this->extractIdList($ownerWechatIds, '客服参数格式错误');
+ if (empty($normalized)) {
+ throw new \Exception('请至少选择一个客服微信');
+ }
+ return $normalized;
+ }
+
+ /**
+ * 构建群推送配置数据
+ * @param array $param
+ * @param array $ownerWechatIds
+ * @param WorkbenchGroupPush|null $originalConfig
+ * @return array
+ * @throws \Exception
+ */
+ private function prepareGroupPushData(array $param, array $ownerWechatIds, WorkbenchGroupPush $originalConfig = null): array
+ {
+ $targetTypeDefault = $originalConfig ? intval($originalConfig->targetType) : 1;
+ $targetType = intval($this->getParamValue($param, 'targetType', $targetTypeDefault)) ?: 1;
+
+ $groupPushSubTypeDefault = $originalConfig ? intval($originalConfig->groupPushSubType) : 1;
+ $groupPushSubType = intval($this->getParamValue($param, 'groupPushSubType', $groupPushSubTypeDefault)) ?: 1;
+ if (!in_array($groupPushSubType, [1, 2], true)) {
+ $groupPushSubType = 1;
+ }
+
+ $data = [
+ 'pushType' => $this->toBoolInt($this->getParamValue($param, 'pushType', $originalConfig->pushType ?? 0)),
+ 'targetType' => $targetType,
+ 'startTime' => $this->getParamValue($param, 'startTime', $originalConfig->startTime ?? ''),
+ 'endTime' => $this->getParamValue($param, 'endTime', $originalConfig->endTime ?? ''),
+ 'maxPerDay' => intval($this->getParamValue($param, 'maxPerDay', $originalConfig->maxPerDay ?? 0)),
+ 'pushOrder' => $this->getParamValue($param, 'pushOrder', $originalConfig->pushOrder ?? 1),
+ 'groupPushSubType' => $groupPushSubType,
+ 'status' => $this->toBoolInt($this->getParamValue($param, 'status', $originalConfig->status ?? 0)),
+ 'socialMediaId' => $this->getParamValue($param, 'socialMediaId', $originalConfig->socialMediaId ?? ''),
+ 'promotionSiteId' => $this->getParamValue($param, 'promotionSiteId', $originalConfig->promotionSiteId ?? ''),
+ 'friendIntervalMin' => intval($this->getParamValue($param, 'friendIntervalMin', $originalConfig->friendIntervalMin ?? 10)),
+ 'friendIntervalMax' => intval($this->getParamValue($param, 'friendIntervalMax', $originalConfig->friendIntervalMax ?? 20)),
+ 'messageIntervalMin' => intval($this->getParamValue($param, 'messageIntervalMin', $originalConfig->messageIntervalMin ?? 1)),
+ 'messageIntervalMax' => intval($this->getParamValue($param, 'messageIntervalMax', $originalConfig->messageIntervalMax ?? 12)),
+ 'isRandomTemplate' => $this->toBoolInt($this->getParamValue($param, 'isRandomTemplate', $originalConfig->isRandomTemplate ?? 0)),
+ 'ownerWechatIds' => json_encode($ownerWechatIds, JSON_UNESCAPED_UNICODE),
+ ];
+
+ if ($data['friendIntervalMin'] > $data['friendIntervalMax']) {
+ throw new \Exception('目标间最小间隔不能大于最大间隔');
+ }
+ if ($data['messageIntervalMin'] > $data['messageIntervalMax']) {
+ throw new \Exception('消息间最小间隔不能大于最大间隔');
+ }
+
+ $contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : [];
+ $contentGroupsParam = $this->getParamValue($param, 'contentGroups', null);
+ $contentGroups = $contentGroupsParam !== null
+ ? $this->extractIdList($contentGroupsParam, '内容库参数格式错误')
+ : $contentGroupsExisting;
+ $data['contentLibraries'] = json_encode($contentGroups, JSON_UNESCAPED_UNICODE);
+
+ $postPushTagsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->postPushTags ?? []) : [];
+ $postPushTagsParam = $this->getParamValue($param, 'postPushTags', null);
+ $postPushTags = $postPushTagsParam !== null
+ ? $this->extractIdList($postPushTagsParam, '推送标签参数格式错误')
+ : $postPushTagsExisting;
+ $data['postPushTags'] = json_encode($postPushTags, JSON_UNESCAPED_UNICODE);
+
+ if ($targetType === 1) {
+ $data['isLoop'] = $this->toBoolInt($this->getParamValue($param, 'isLoop', $originalConfig->isLoop ?? 0));
+
+ $groupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->groups ?? []) : [];
+ $wechatGroups = array_key_exists('wechatGroups', $param)
+ ? $this->extractIdList($param['wechatGroups'], '群参数格式错误')
+ : $groupsExisting;
+ if (empty($wechatGroups)) {
+ throw new \Exception('群推送必须选择微信群');
+ }
+ $data['groups'] = json_encode($wechatGroups, JSON_UNESCAPED_UNICODE);
+ $data['friends'] = json_encode([], JSON_UNESCAPED_UNICODE);
+ $data['trafficPools'] = json_encode([], JSON_UNESCAPED_UNICODE);
+
+ if ($groupPushSubType === 2) {
+ $announcementContent = $this->getParamValue($param, 'announcementContent', $originalConfig->announcementContent ?? '');
+ if (empty($announcementContent)) {
+ throw new \Exception('群公告必须输入公告内容');
+ }
+ $enableAiRewrite = $this->toBoolInt($this->getParamValue($param, 'enableAiRewrite', $originalConfig->enableAiRewrite ?? 0));
+ $aiRewritePrompt = trim((string)$this->getParamValue($param, 'aiRewritePrompt', $originalConfig->aiRewritePrompt ?? ''));
+ if ($enableAiRewrite === 1 && $aiRewritePrompt === '') {
+ throw new \Exception('启用AI智能话术改写时,必须输入改写提示词');
+ }
+ $data['announcementContent'] = $announcementContent;
+ $data['enableAiRewrite'] = $enableAiRewrite;
+ $data['aiRewritePrompt'] = $aiRewritePrompt;
+ } else {
+ $data['groupPushSubType'] = 1;
+ $data['announcementContent'] = '';
+ $data['enableAiRewrite'] = 0;
+ $data['aiRewritePrompt'] = '';
+ }
+ } else {
+ $data['isLoop'] = 0;
+ $friendsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->friends ?? []) : [];
+ $trafficPoolsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->trafficPools ?? []) : [];
+
+ $friendTargets = array_key_exists('wechatFriends', $param)
+ ? $this->extractIdList($param['wechatFriends'], '好友参数格式错误')
+ : $friendsExisting;
+ $trafficPools = array_key_exists('trafficPools', $param)
+ ? $this->extractIdList($param['trafficPools'], '流量池参数格式错误')
+ : $trafficPoolsExisting;
+
+ if (empty($friendTargets) && empty($trafficPools)) {
+ throw new \Exception('好友推送需至少选择好友或流量池');
+ }
+
+ $data['friends'] = json_encode($friendTargets, JSON_UNESCAPED_UNICODE);
+ $data['trafficPools'] = json_encode($trafficPools, JSON_UNESCAPED_UNICODE);
+ $data['groups'] = json_encode([], JSON_UNESCAPED_UNICODE);
+ $data['groupPushSubType'] = 1;
+ $data['announcementContent'] = '';
+ $data['enableAiRewrite'] = 0;
+ $data['aiRewritePrompt'] = '';
+ }
+
+ return $data;
+ }
+
+ /**
+ * 获取参数值,若不存在则返回默认值
+ * @param array $param
+ * @param string $key
+ * @param mixed $default
+ * @return mixed
+ */
+ private function getParamValue(array $param, string $key, $default)
+ {
+ return array_key_exists($key, $param) ? $param[$key] : $default;
+ }
+
+ /**
+ * 将值转换为整型布尔
+ * @param mixed $value
+ * @return int
+ */
+ private function toBoolInt($value): int
+ {
+ return empty($value) ? 0 : 1;
+ }
+
+ /**
+ * 从参数中提取ID列表
+ * @param mixed $items
+ * @param string $errorMessage
+ * @return array
+ * @throws \Exception
+ */
+ private function extractIdList($items, string $errorMessage = '参数格式错误'): array
+ {
+ if (!is_array($items)) {
+ throw new \Exception($errorMessage);
+ }
+
+ $ids = [];
+ foreach ($items as $item) {
+ if (is_array($item) && isset($item['id'])) {
+ $item = $item['id'];
+ }
+ if ($item === '' || $item === null) {
+ continue;
+ }
+ $ids[] = $item;
+ }
+
+ return array_values(array_unique($ids));
+ }
+
+ /**
+ * 解码JSON数组
+ * @param mixed $value
+ * @return array
+ */
+ private function decodeJsonArray($value): array
+ {
+ if (empty($value)) {
+ return [];
+ }
+ if (is_array($value)) {
+ return $value;
+ }
+ $decoded = json_decode($value, true);
+ return is_array($decoded) ? $decoded : [];
+ }
+
+ /**
+ * 验证内容是否包含链接
+ * @param string $content 要检测的内容
+ * @return bool
+ */
+ private function containsLink($content)
+ {
+ // 定义各种链接的正则表达式模式
+ $patterns = [
+ // HTTP/HTTPS链接
+ '/https?:\/\/[^\s]+/i',
+ // 京东商品链接
+ '/item\.jd\.com\/\d+/i',
+ // 京东短链接
+ '/u\.jd\.com\/[a-zA-Z0-9]+/i',
+ // 淘宝商品链接
+ '/item\.taobao\.com\/item\.htm\?id=\d+/i',
+ // 天猫商品链接
+ '/detail\.tmall\.com\/item\.htm\?id=\d+/i',
+ // 淘宝短链接
+ '/m\.tb\.cn\/[a-zA-Z0-9]+/i',
+ // 拼多多链接
+ '/mobile\.yangkeduo\.com\/goods\.html\?goods_id=\d+/i',
+ // 苏宁易购链接
+ '/product\.suning\.com\/\d+\/\d+\.html/i',
+ // 通用域名模式(包含常见电商域名)
+ '/(?:jd|taobao|tmall|yangkeduo|suning|amazon|dangdang)\.com[^\s]*/i',
+ // 通用短链接模式
+ '/[a-zA-Z0-9-]+\.[a-zA-Z]{2,}\/[a-zA-Z0-9\-._~:\/?#\[\]@!$&\'()*+,;=]+/i'
+ ];
+
+ // 遍历所有模式进行匹配
+ foreach ($patterns as $pattern) {
+ if (preg_match($pattern, $content)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+
+ /**
+ * 获取通讯录导入记录列表
+ * @return \think\response\Json
+ */
+ public function getImportContact()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $workbenchId = $this->request->param('workbenchId', 0);
+
+ $where = [
+ ['wici.workbenchId', '=', $workbenchId]
+ ];
+
+ // 查询发布记录
+ $list = Db::name('workbench_import_contact_item')->alias('wici')
+ ->join('traffic_pool tp', 'tp.id = wici.poolId', 'left')
+ ->join('traffic_source tc', 'tc.identifier = tp.identifier', 'left')
+ ->join('wechat_account wa', 'wa.wechatId = tp.wechatId', 'left')
+ ->field([
+ 'wici.id',
+ 'wici.workbenchId',
+ 'wici.createTime',
+ 'tp.identifier',
+ 'tp.mobile',
+ 'tp.wechatId',
+ 'tc.name',
+ 'wa.nickName',
+ 'wa.avatar',
+ 'wa.alias',
+ ])
+ ->where($where)
+ ->order('tc.name DESC,wici.createTime DESC')
+ ->group('tp.identifier')
+ ->page($page, $limit)
+ ->select();
+
+ foreach ($list as &$item) {
+ $item['createTime'] = date('Y-m-d H:i:s', $item['createTime']);
+ }
+
+
+ // 获取总记录数
+ $total = Db::name('workbench_import_contact_item')->alias('wici')
+ ->where($where)
+ ->count();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ ]
+ ]);
+ }
+
+ /**
+ * 获取群发统计数据
+ * @return \think\response\Json
+ */
+ public function getGroupPushStats()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ $timeRange = $this->request->param('timeRange', '7'); // 默认最近7天
+ $contentLibraryIds = $this->request->param('contentLibraryIds', ''); // 话术组筛选
+ $userId = $this->request->userInfo['id'];
+
+ // 如果指定了工作台ID,则验证权限
+ if (!empty($workbenchId)) {
+ $workbench = Workbench::where([
+ ['id', '=', $workbenchId],
+ ['userId', '=', $userId],
+ ['type', '=', self::TYPE_GROUP_PUSH],
+ ['isDel', '=', 0]
+ ])->find();
+
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在']);
+ }
+ }
+
+ // 计算时间范围
+ $days = intval($timeRange);
+ $startTime = strtotime(date('Y-m-d 00:00:00', strtotime("-{$days} days")));
+ $endTime = time();
+
+ // 构建查询条件
+ $where = [
+ ['wgpi.createTime', '>=', $startTime],
+ ['wgpi.createTime', '<=', $endTime]
+ ];
+
+ // 如果指定了工作台ID,则限制查询范围
+ if (!empty($workbenchId)) {
+ $where[] = ['wgpi.workbenchId', '=', $workbenchId];
+ } else {
+ // 如果没有指定工作台ID,则查询当前用户的所有群推送工作台
+ $workbenchIds = Workbench::where([
+ ['userId', '=', $userId],
+ ['type', '=', self::TYPE_GROUP_PUSH],
+ ['isDel', '=', 0]
+ ])->column('id');
+
+ if (empty($workbenchIds)) {
+ // 如果没有工作台,返回空结果
+ $workbenchIds = [-1];
+ }
+ $where[] = ['wgpi.workbenchId', 'in', $workbenchIds];
+ }
+
+ // 话术组筛选 - 先获取符合条件的内容ID列表
+ $contentIds = null;
+ if (!empty($contentLibraryIds)) {
+ $libraryIds = is_array($contentLibraryIds) ? $contentLibraryIds : explode(',', $contentLibraryIds);
+ $libraryIds = array_filter(array_map('intval', $libraryIds));
+ if (!empty($libraryIds)) {
+ // 查询符合条件的内容ID
+ $contentIds = Db::name('content_item')
+ ->whereIn('libraryId', $libraryIds)
+ ->column('id');
+ if (empty($contentIds)) {
+ // 如果没有符合条件的内容,返回空结果
+ $contentIds = [-1]; // 使用不存在的ID,确保查询结果为空
+ }
+ }
+ }
+
+ // 1. 基础统计:触达率、回复率、平均回复时间、链接点击率
+ $stats = $this->calculateBasicStats($workbenchId, $where, $startTime, $endTime, $contentIds);
+
+ // 2. 话术组对比
+ $contentLibraryComparison = $this->getContentLibraryComparison($workbenchId, $where, $startTime, $endTime, $contentIds);
+
+ // 3. 时段分析
+ $timePeriodAnalysis = $this->getTimePeriodAnalysis($workbenchId, $where, $startTime, $endTime, $contentIds);
+
+ // 4. 互动深度(可选,需要更多数据)
+ $interactionDepth = $this->getInteractionDepth($workbenchId, $where, $startTime, $endTime, $contentIds);
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'basicStats' => $stats,
+ 'contentLibraryComparison' => $contentLibraryComparison,
+ 'timePeriodAnalysis' => $timePeriodAnalysis,
+ 'interactionDepth' => $interactionDepth
+ ]
+ ]);
+ }
+
+ /**
+ * 计算基础统计数据
+ */
+ private function calculateBasicStats($workbenchId, $where, $startTime, $endTime, $contentIds = null)
+ {
+ // 获取工作台配置,计算计划发送数
+ // 如果 workbenchId 为空,则查询所有工作台的配置
+ $configQuery = WorkbenchGroupPush::alias('wgp')
+ ->join('workbench w', 'w.id = wgp.workbenchId', 'left')
+ ->where('w.type', self::TYPE_GROUP_PUSH)
+ ->where('w.isDel', 0);
+
+ if (!empty($workbenchId)) {
+ $configQuery->where('wgp.workbenchId', $workbenchId);
+ } else {
+ // 如果没有指定工作台ID,需要从 where 条件中获取 workbenchId 列表
+ $workbenchIdCondition = null;
+ foreach ($where as $condition) {
+ if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') {
+ if ($condition[1] === 'in' && is_array($condition[2])) {
+ $workbenchIdCondition = $condition[2];
+ break;
+ } elseif ($condition[1] === '=') {
+ $workbenchIdCondition = [$condition[2]];
+ break;
+ }
+ }
+ }
+ if ($workbenchIdCondition) {
+ $configQuery->whereIn('wgp.workbenchId', $workbenchIdCondition);
+ }
+ }
+
+ $configs = $configQuery->select();
+ $targetType = 1; // 默认值
+ if (!empty($configs)) {
+ // 如果只有一个配置,使用它的 targetType;如果有多个,默认使用1
+ $targetType = intval($configs[0]->targetType ?? 1);
+ }
+
+ // 计划发送数(根据配置计算)
+ $plannedSend = 0;
+ if (!empty($configs)) {
+ $days = ceil(($endTime - $startTime) / 86400);
+ foreach ($configs as $config) {
+ $maxPerDay = intval($config->maxPerDay ?? 0);
+ $configTargetType = intval($config->targetType ?? 1);
+ if ($configTargetType == 1) {
+ // 群推送:计划发送数 = 每日推送次数 * 天数 * 群数量
+ $groups = $this->decodeJsonArray($config->groups ?? []);
+ $plannedSend += $maxPerDay * $days * count($groups);
+ } else {
+ // 好友推送:计划发送数 = 每日推送人数 * 天数
+ $plannedSend += $maxPerDay * $days;
+ }
+ }
+ }
+
+ // 构建查询条件
+ $queryWhere = $where;
+ if ($contentIds !== null) {
+ $queryWhere[] = ['wgpi.contentId', 'in', $contentIds];
+ }
+
+ // 实际成功发送数(从推送记录表统计)
+ $successSend = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($queryWhere)
+ ->count();
+
+ // 触达率 = 成功发送数 / 计划发送数
+ $reachRate = $plannedSend > 0 ? round(($successSend / $plannedSend) * 100, 1) : 0;
+
+ // 获取发送记录列表,用于查询回复
+ $sentItemIds = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($queryWhere)
+ ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId')
+ ->select();
+
+ // 回复统计(通过消息表查询)
+ $replyStats = $this->calculateReplyStats($sentItemIds, $targetType, $startTime, $endTime);
+
+ // 链接点击统计
+ $clickStats = $this->calculateClickStats($sentItemIds, $targetType, $startTime, $endTime);
+
+ // 计算本月对比数据(简化处理,实际应该查询上个月同期数据)
+ $currentMonthStart = strtotime(date('Y-m-01 00:00:00'));
+ $lastMonthStart = strtotime(date('Y-m-01 00:00:00', strtotime('-1 month')));
+ $lastMonthEnd = $currentMonthStart - 1;
+
+ // 获取本月统计数据(避免递归调用)
+ $currentMonthWhere = [
+ ['wgpi.createTime', '>=', $currentMonthStart]
+ ];
+ // 复制 workbenchId 条件
+ foreach ($where as $condition) {
+ if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') {
+ $currentMonthWhere[] = $condition;
+ break;
+ }
+ }
+ if ($contentIds !== null) {
+ $currentMonthWhere[] = ['wgpi.contentId', 'in', $contentIds];
+ }
+ $currentMonthSend = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($currentMonthWhere)
+ ->count();
+
+ // 获取本月配置
+ $currentMonthConfigQuery = WorkbenchGroupPush::alias('wgp')
+ ->join('workbench w', 'w.id = wgp.workbenchId', 'left')
+ ->where('w.type', self::TYPE_GROUP_PUSH)
+ ->where('w.isDel', 0);
+ if (!empty($workbenchId)) {
+ $currentMonthConfigQuery->where('wgp.workbenchId', $workbenchId);
+ } else {
+ foreach ($where as $condition) {
+ if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId' && $condition[1] === 'in') {
+ $currentMonthConfigQuery->whereIn('wgp.workbenchId', $condition[2]);
+ break;
+ }
+ }
+ }
+ $currentMonthConfigs = $currentMonthConfigQuery->select();
+ $currentMonthPlanned = 0;
+ if (!empty($currentMonthConfigs)) {
+ $currentMonthDays = ceil(($endTime - $currentMonthStart) / 86400);
+ foreach ($currentMonthConfigs as $currentMonthConfig) {
+ $currentMonthMaxPerDay = intval($currentMonthConfig->maxPerDay ?? 0);
+ $currentMonthTargetType = intval($currentMonthConfig->targetType ?? 1);
+ if ($currentMonthTargetType == 1) {
+ $currentMonthGroups = $this->decodeJsonArray($currentMonthConfig->groups ?? []);
+ $currentMonthPlanned += $currentMonthMaxPerDay * $currentMonthDays * count($currentMonthGroups);
+ } else {
+ $currentMonthPlanned += $currentMonthMaxPerDay * $currentMonthDays;
+ }
+ }
+ }
+ $currentMonthReachRate = $currentMonthPlanned > 0 ? round(($currentMonthSend / $currentMonthPlanned) * 100, 1) : 0;
+
+ // 获取上个月统计数据
+ $lastMonthWhere = [
+ ['wgpi.createTime', '>=', $lastMonthStart],
+ ['wgpi.createTime', '<=', $lastMonthEnd]
+ ];
+ // 复制 workbenchId 条件
+ foreach ($where as $condition) {
+ if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') {
+ $lastMonthWhere[] = $condition;
+ break;
+ }
+ }
+ if ($contentIds !== null) {
+ $lastMonthWhere[] = ['wgpi.contentId', 'in', $contentIds];
+ }
+ $lastMonthSend = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($lastMonthWhere)
+ ->count();
+
+ // 获取上个月配置
+ $lastMonthConfigQuery = WorkbenchGroupPush::alias('wgp')
+ ->join('workbench w', 'w.id = wgp.workbenchId', 'left')
+ ->where('w.type', self::TYPE_GROUP_PUSH)
+ ->where('w.isDel', 0);
+ if (!empty($workbenchId)) {
+ $lastMonthConfigQuery->where('wgp.workbenchId', $workbenchId);
+ } else {
+ foreach ($where as $condition) {
+ if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId' && $condition[1] === 'in') {
+ $lastMonthConfigQuery->whereIn('wgp.workbenchId', $condition[2]);
+ break;
+ }
+ }
+ }
+ $lastMonthConfigs = $lastMonthConfigQuery->select();
+
+ $lastMonthPlanned = 0;
+ if (!empty($lastMonthConfigs)) {
+ $lastMonthDays = ceil(($lastMonthEnd - $lastMonthStart) / 86400);
+ foreach ($lastMonthConfigs as $lastMonthConfig) {
+ $lastMonthMaxPerDay = intval($lastMonthConfig->maxPerDay ?? 0);
+ $lastMonthTargetType = intval($lastMonthConfig->targetType ?? 1);
+ if ($lastMonthTargetType == 1) {
+ $lastMonthGroups = $this->decodeJsonArray($lastMonthConfig->groups ?? []);
+ $lastMonthPlanned += $lastMonthMaxPerDay * $lastMonthDays * count($lastMonthGroups);
+ } else {
+ $lastMonthPlanned += $lastMonthMaxPerDay * $lastMonthDays;
+ }
+ }
+ }
+ $lastMonthReachRate = $lastMonthPlanned > 0 ? round(($lastMonthSend / $lastMonthPlanned) * 100, 1) : 0;
+
+ // 获取上个月的回复和点击统计(简化处理)
+ $lastMonthSentItems = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($lastMonthWhere)
+ ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId')
+ ->select();
+ $lastMonthReplyStats = $this->calculateReplyStats($lastMonthSentItems, $targetType, $lastMonthStart, $lastMonthEnd);
+ $lastMonthClickStats = $this->calculateClickStats($lastMonthSentItems, $targetType, $lastMonthStart, $lastMonthEnd);
+
+ return [
+ 'reachRate' => [
+ 'value' => $reachRate,
+ 'trend' => round($reachRate - $lastMonthReachRate, 1),
+ 'unit' => '%',
+ 'description' => '成功发送/计划发送'
+ ],
+ 'replyRate' => [
+ 'value' => $replyStats['replyRate'],
+ 'trend' => round($replyStats['replyRate'] - $lastMonthReplyStats['replyRate'], 1),
+ 'unit' => '%',
+ 'description' => '收到回复/成功发送'
+ ],
+ 'avgReplyTime' => [
+ 'value' => $replyStats['avgReplyTime'],
+ 'trend' => round($lastMonthReplyStats['avgReplyTime'] - $replyStats['avgReplyTime'], 0),
+ 'unit' => '分钟',
+ 'description' => '从发送到回复的平均时长'
+ ],
+ 'clickRate' => [
+ 'value' => $clickStats['clickRate'],
+ 'trend' => round($clickStats['clickRate'] - $lastMonthClickStats['clickRate'], 1),
+ 'unit' => '%',
+ 'description' => '点击链接/成功发送'
+ ],
+ 'plannedSend' => $plannedSend,
+ 'successSend' => $successSend,
+ 'replyCount' => $replyStats['replyCount'],
+ 'clickCount' => $clickStats['clickCount']
+ ];
+ }
+
+ /**
+ * 计算回复统计
+ */
+ private function calculateReplyStats($sentItems, $targetType, $startTime, $endTime)
+ {
+ if (empty($sentItems)) {
+ return ['replyRate' => 0, 'avgReplyTime' => 0, 'replyCount' => 0];
+ }
+
+ $replyCount = 0;
+ $totalReplyTime = 0;
+ $replyTimes = [];
+
+ foreach ($sentItems as $item) {
+ $itemArray = is_array($item) ? $item : (array)$item;
+ $sendTime = $itemArray['createTime'] ?? 0;
+ $accountId = $itemArray['wechatAccountId'] ?? 0;
+
+ if ($targetType == 1) {
+ // 群推送:查找群内回复消息
+ $groupId = $itemArray['groupId'] ?? 0;
+ $group = Db::name('wechat_group')->where('id', $groupId)->find();
+ if ($group) {
+ $replyMsg = Db::table('s2_wechat_message')
+ ->where('wechatChatroomId', $group['chatroomId'])
+ ->where('wechatAccountId', $accountId)
+ ->where('isSend', 0) // 接收的消息
+ ->where('wechatTime', '>', $sendTime)
+ ->where('wechatTime', '<=', $sendTime + 86400) // 24小时内回复
+ ->order('wechatTime', 'asc')
+ ->find();
+
+ if ($replyMsg) {
+ $replyCount++;
+ $replyTime = $replyMsg['wechatTime'] - $sendTime;
+ $replyTimes[] = $replyTime;
+ $totalReplyTime += $replyTime;
+ }
+ }
+ } else {
+ // 好友推送:查找好友回复消息
+ $friendId = $itemArray['friendId'] ?? 0;
+ $friend = Db::table('s2_wechat_friend')->where('id', $friendId)->find();
+ if ($friend) {
+ $replyMsg = Db::table('s2_wechat_message')
+ ->where('wechatFriendId', $friendId)
+ ->where('wechatAccountId', $accountId)
+ ->where('isSend', 0) // 接收的消息
+ ->where('wechatTime', '>', $sendTime)
+ ->where('wechatTime', '<=', $sendTime + 86400) // 24小时内回复
+ ->order('wechatTime', 'asc')
+ ->find();
+
+ if ($replyMsg) {
+ $replyCount++;
+ $replyTime = $replyMsg['wechatTime'] - $sendTime;
+ $replyTimes[] = $replyTime;
+ $totalReplyTime += $replyTime;
+ }
+ }
+ }
+ }
+
+ $successSend = count($sentItems);
+ $replyRate = $successSend > 0 ? round(($replyCount / $successSend) * 100, 1) : 0;
+ $avgReplyTime = $replyCount > 0 ? round(($totalReplyTime / $replyCount) / 60, 0) : 0; // 转换为分钟
+
+ return [
+ 'replyRate' => $replyRate,
+ 'avgReplyTime' => $avgReplyTime,
+ 'replyCount' => $replyCount
+ ];
+ }
+
+ /**
+ * 计算链接点击统计
+ */
+ private function calculateClickStats($sentItems, $targetType, $startTime, $endTime)
+ {
+ if (empty($sentItems)) {
+ return ['clickRate' => 0, 'clickCount' => 0];
+ }
+
+ $clickCount = 0;
+ $linkContentIds = [];
+
+ // 获取所有发送的内容ID
+ foreach ($sentItems as $item) {
+ $itemArray = is_array($item) ? $item : (array)$item;
+ $contentId = $itemArray['contentId'] ?? 0;
+ if ($contentId > 0) {
+ $linkContentIds[] = $contentId;
+ }
+ }
+
+ if (empty($linkContentIds)) {
+ return ['clickRate' => 0, 'clickCount' => 0];
+ }
+
+ // 查询包含链接的内容
+ $linkContents = Db::name('content_item')
+ ->whereIn('id', array_unique($linkContentIds))
+ ->where('contentType', 2) // 链接类型
+ ->column('id');
+
+ // 统计发送了链接内容的记录数
+ $linkSendCount = 0;
+ foreach ($sentItems as $item) {
+ $itemArray = is_array($item) ? $item : (array)$item;
+ $contentId = $itemArray['contentId'] ?? 0;
+ if (in_array($contentId, $linkContents)) {
+ $linkSendCount++;
+ }
+ }
+
+ // 简化处理:假设点击率基于链接消息的发送(实际应该从点击追踪系统获取)
+ // 这里可以根据业务需求调整,比如通过消息中的链接点击事件统计
+ $clickCount = $linkSendCount; // 简化处理,实际需要真实的点击数据
+
+ $successSend = count($sentItems);
+ $clickRate = $successSend > 0 ? round(($clickCount / $successSend) * 100, 1) : 0;
+
+ return [
+ 'clickRate' => $clickRate,
+ 'clickCount' => $clickCount
+ ];
+ }
+
+ /**
+ * 获取话术组对比数据
+ */
+ private function getContentLibraryComparison($workbenchId, $where, $startTime, $endTime, $contentIds = null)
+ {
+ $queryWhere = $where;
+ if ($contentIds !== null) {
+ $queryWhere[] = ['wgpi.contentId', 'in', $contentIds];
+ }
+
+ $comparison = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->join('content_item ci', 'ci.id = wgpi.contentId', 'left')
+ ->join('content_library cl', 'cl.id = ci.libraryId', 'left')
+ ->where($queryWhere)
+ ->where('cl.id', '<>', null)
+ ->field([
+ 'cl.id as libraryId',
+ 'cl.name as libraryName',
+ 'COUNT(DISTINCT wgpi.id) as pushCount'
+ ])
+ ->group('cl.id, cl.name')
+ ->select();
+
+ $result = [];
+ foreach ($comparison as $item) {
+ $libraryId = $item['libraryId'];
+ $pushCount = intval($item['pushCount']);
+
+ // 获取该内容库的详细统计
+ $libraryContentIds = Db::name('content_item')
+ ->where('libraryId', $libraryId)
+ ->column('id');
+ if (empty($libraryContentIds)) {
+ $libraryContentIds = [-1];
+ }
+
+ $libraryWhere = array_merge($where, [['wgpi.contentId', 'in', $libraryContentIds]]);
+ $librarySentItems = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($libraryWhere)
+ ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId')
+ ->select();
+
+ $config = WorkbenchGroupPush::where('workbenchId', $workbenchId)->find();
+ $targetType = $config ? intval($config->targetType) : 1;
+
+ $replyStats = $this->calculateReplyStats($librarySentItems, $targetType, $startTime, $endTime);
+ $clickStats = $this->calculateClickStats($librarySentItems, $targetType, $startTime, $endTime);
+
+ // 计算转化率(简化处理,实际需要根据业务定义)
+ $conversionRate = $pushCount > 0 ? round(($replyStats['replyCount'] / $pushCount) * 100, 1) : 0;
+
+ $result[] = [
+ 'libraryId' => $libraryId,
+ 'libraryName' => $item['libraryName'],
+ 'pushCount' => $pushCount,
+ 'reachRate' => 100, // 简化处理,实际应该计算
+ 'replyRate' => $replyStats['replyRate'],
+ 'clickRate' => $clickStats['clickRate'],
+ 'conversionRate' => $conversionRate,
+ 'avgReplyTime' => $replyStats['avgReplyTime'],
+ 'level' => $this->getPerformanceLevel($replyStats['replyRate'], $clickStats['clickRate'], $conversionRate)
+ ];
+ }
+
+ // 按回复率排序
+ usort($result, function($a, $b) {
+ return $b['replyRate'] <=> $a['replyRate'];
+ });
+
+ return $result;
+ }
+
+ /**
+ * 获取性能等级
+ */
+ private function getPerformanceLevel($replyRate, $clickRate, $conversionRate)
+ {
+ $score = ($replyRate * 0.4) + ($clickRate * 0.3) + ($conversionRate * 0.3);
+
+ if ($score >= 40) {
+ return '优秀';
+ } elseif ($score >= 25) {
+ return '良好';
+ } elseif ($score >= 15) {
+ return '一般';
+ } else {
+ return '待提升';
+ }
+ }
+
+ /**
+ * 获取时段分析数据
+ */
+ private function getTimePeriodAnalysis($workbenchId, $where, $startTime, $endTime, $contentIds = null)
+ {
+ $queryWhere = $where;
+ if ($contentIds !== null) {
+ $queryWhere[] = ['wgpi.contentId', 'in', $contentIds];
+ }
+
+ $analysis = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($queryWhere)
+ ->field([
+ 'FROM_UNIXTIME(wgpi.createTime, "%H") as hour',
+ 'COUNT(*) as count'
+ ])
+ ->group('hour')
+ ->order('hour', 'asc')
+ ->select();
+
+ $result = [];
+ foreach ($analysis as $item) {
+ $result[] = [
+ 'hour' => intval($item['hour']),
+ 'count' => intval($item['count'])
+ ];
+ }
+
+ return $result;
+ }
+
+ /**
+ * 获取互动深度数据
+ */
+ private function getInteractionDepth($workbenchId, $where, $startTime, $endTime, $contentIds = null)
+ {
+ // 简化处理,实际需要更复杂的统计逻辑
+ return [
+ 'singleReply' => 0, // 单次回复
+ 'multipleReply' => 0, // 多次回复
+ 'deepInteraction' => 0 // 深度互动
+ ];
+ }
+
+ /**
+ * 获取推送历史记录列表
+ * @return \think\response\Json
+ */
+ public function getGroupPushHistory()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $workbenchId = $this->request->param('workbenchId', 0);
+ $keyword = $this->request->param('keyword', '');
+ $pushType = $this->request->param('pushType', ''); // 推送类型筛选:''=全部, 'friend'=好友消息, 'group'=群消息, 'announcement'=群公告
+ $status = $this->request->param('status', ''); // 状态筛选:''=全部, 'success'=已完成, 'progress'=进行中, 'failed'=失败
+ $userId = $this->request->userInfo['id'];
+
+ // 构建工作台查询条件
+ $workbenchWhere = [
+ ['w.userId', '=', $userId],
+ ['w.type', '=', self::TYPE_GROUP_PUSH],
+ ['w.isDel', '=', 0]
+ ];
+
+ // 如果指定了工作台ID,则验证权限并限制查询范围
+ if (!empty($workbenchId)) {
+ $workbench = Workbench::where([
+ ['id', '=', $workbenchId],
+ ['userId', '=', $userId],
+ ['type', '=', self::TYPE_GROUP_PUSH],
+ ['isDel', '=', 0]
+ ])->find();
+
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在']);
+ }
+ $workbenchWhere[] = ['w.id', '=', $workbenchId];
+ }
+
+ // 1. 先查询所有已执行的推送记录(按推送时间分组)
+ $pushHistoryQuery = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->join('workbench w', 'w.id = wgpi.workbenchId', 'left')
+ ->join('workbench_group_push wgp', 'wgp.workbenchId = wgpi.workbenchId', 'left')
+ ->join('content_item ci', 'ci.id = wgpi.contentId', 'left')
+ ->join('content_library cl', 'cl.id = ci.libraryId', 'left')
+ ->where($workbenchWhere)
+ ->field([
+ 'wgpi.workbenchId',
+ 'w.name as workbenchName',
+ 'wgpi.contentId',
+ 'FROM_UNIXTIME(wgpi.createTime, "%Y-%m-%d %H:00:00") as pushTime',
+ 'wgpi.targetType',
+ 'wgp.groupPushSubType',
+ 'MIN(wgpi.createTime) as createTime',
+ 'COUNT(DISTINCT wgpi.id) as totalCount',
+ 'cl.name as contentLibraryName'
+ ])
+ ->group('wgpi.workbenchId, wgpi.contentId, pushTime, wgpi.targetType, wgp.groupPushSubType');
+
+ if (!empty($keyword)) {
+ $pushHistoryQuery->where('w.name|cl.name|ci.content', 'like', '%' . $keyword . '%');
+ }
+
+ $pushHistoryList = $pushHistoryQuery->order('createTime', 'desc')->select();
+
+ // 2. 查询所有任务(包括未执行的)
+ $allTasksQuery = Db::name('workbench')
+ ->alias('w')
+ ->join('workbench_group_push wgp', 'wgp.workbenchId = w.id', 'left')
+ ->where($workbenchWhere)
+ ->field([
+ 'w.id as workbenchId',
+ 'w.name as workbenchName',
+ 'w.createTime',
+ 'wgp.targetType',
+ 'wgp.groupPushSubType',
+ 'wgp.groups',
+ 'wgp.friends',
+ 'wgp.trafficPools'
+ ]);
+
+ if (!empty($keyword)) {
+ $allTasksQuery->where('w.name', 'like', '%' . $keyword . '%');
+ }
+
+ $allTasks = $allTasksQuery->select();
+
+ // 3. 合并数据:已执行的推送记录 + 未执行的任务
+ $resultList = [];
+ $executedWorkbenchIds = [];
+
+ // 处理已执行的推送记录
+ foreach ($pushHistoryList as $item) {
+ $itemWorkbenchId = $item['workbenchId'];
+ $contentId = $item['contentId'];
+ $pushTime = $item['pushTime'];
+ $targetType = intval($item['targetType']);
+ $groupPushSubType = isset($item['groupPushSubType']) ? intval($item['groupPushSubType']) : 1;
+
+ // 标记该工作台已有执行记录
+ if (!in_array($itemWorkbenchId, $executedWorkbenchIds)) {
+ $executedWorkbenchIds[] = $itemWorkbenchId;
+ }
+
+ // 将时间字符串转换为时间戳范围(小时级别)
+ $pushTimeStart = strtotime($pushTime);
+ $pushTimeEnd = $pushTimeStart + 3600; // 一小时后
+
+ // 获取该次推送的详细统计
+ $pushWhere = [
+ ['wgpi.workbenchId', '=', $itemWorkbenchId],
+ ['wgpi.contentId', '=', $contentId],
+ ['wgpi.createTime', '>=', $pushTimeStart],
+ ['wgpi.createTime', '<', $pushTimeEnd],
+ ['wgpi.targetType', '=', $targetType]
+ ];
+
+ // 目标数量
+ if ($targetType == 1) {
+ // 群推送:统计群数量
+ $targetCount = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($pushWhere)
+ ->where('wgpi.groupId', '<>', null)
+ ->distinct(true)
+ ->count('wgpi.groupId');
+ } else {
+ // 好友推送:统计好友数量
+ $targetCount = Db::name('workbench_group_push_item')
+ ->alias('wgpi')
+ ->where($pushWhere)
+ ->where('wgpi.friendId', '<>', null)
+ ->distinct(true)
+ ->count('wgpi.friendId');
+ }
+
+ // 成功数和失败数(简化处理,实际需要根据发送状态判断)
+ $successCount = intval($item['totalCount']); // 简化处理
+ $failCount = 0; // 简化处理,实际需要从发送状态获取
+
+ // 状态判断
+ $itemStatus = $successCount > 0 ? 'success' : 'failed';
+ if ($failCount > 0 && $successCount > 0) {
+ $itemStatus = 'partial';
+ }
+
+ // 推送类型判断
+ $pushTypeText = '';
+ $pushTypeCode = '';
+ if ($targetType == 1) {
+ // 群推送
+ if ($groupPushSubType == 2) {
+ $pushTypeText = '群公告';
+ $pushTypeCode = 'announcement';
+ } else {
+ $pushTypeText = '群消息';
+ $pushTypeCode = 'group';
+ }
+ } else {
+ // 好友推送
+ $pushTypeText = '好友消息';
+ $pushTypeCode = 'friend';
+ }
+
+ $resultList[] = [
+ 'workbenchId' => $itemWorkbenchId,
+ 'taskName' => $item['workbenchName'] ?? '',
+ 'pushType' => $pushTypeText,
+ 'pushTypeCode' => $pushTypeCode,
+ 'targetCount' => $targetCount,
+ 'successCount' => $successCount,
+ 'failCount' => $failCount,
+ 'status' => $itemStatus,
+ 'statusText' => $this->getStatusText($itemStatus),
+ 'createTime' => date('Y-m-d H:i:s', $item['createTime']),
+ 'contentLibraryName' => $item['contentLibraryName'] ?? ''
+ ];
+ }
+
+ // 处理未执行的任务
+ foreach ($allTasks as $task) {
+ $taskWorkbenchId = $task['workbenchId'];
+
+ // 如果该任务已有执行记录,跳过(避免重复)
+ if (in_array($taskWorkbenchId, $executedWorkbenchIds)) {
+ continue;
+ }
+
+ $targetType = isset($task['targetType']) ? intval($task['targetType']) : 1;
+ $groupPushSubType = isset($task['groupPushSubType']) ? intval($task['groupPushSubType']) : 1;
+
+ // 计算目标数量(从配置中获取)
+ $targetCount = 0;
+ if ($targetType == 1) {
+ // 群推送:统计配置的群数量
+ $groups = json_decode($task['groups'] ?? '[]', true);
+ $targetCount = is_array($groups) ? count($groups) : 0;
+ } else {
+ // 好友推送:统计配置的好友数量或流量池数量
+ $friends = json_decode($task['friends'] ?? '[]', true);
+ $trafficPools = json_decode($task['trafficPools'] ?? '[]', true);
+ $friendCount = is_array($friends) ? count($friends) : 0;
+ $poolCount = is_array($trafficPools) ? count($trafficPools) : 0;
+ // 如果配置了流量池,目标数量暂时显示为流量池数量(实际数量需要从流量池中统计)
+ $targetCount = $friendCount > 0 ? $friendCount : $poolCount;
+ }
+
+ // 推送类型判断
+ $pushTypeText = '';
+ $pushTypeCode = '';
+ if ($targetType == 1) {
+ // 群推送
+ if ($groupPushSubType == 2) {
+ $pushTypeText = '群公告';
+ $pushTypeCode = 'announcement';
+ } else {
+ $pushTypeText = '群消息';
+ $pushTypeCode = 'group';
+ }
+ } else {
+ // 好友推送
+ $pushTypeText = '好友消息';
+ $pushTypeCode = 'friend';
+ }
+
+ $resultList[] = [
+ 'workbenchId' => $taskWorkbenchId,
+ 'taskName' => $task['workbenchName'] ?? '',
+ 'pushType' => $pushTypeText,
+ 'pushTypeCode' => $pushTypeCode,
+ 'targetCount' => $targetCount,
+ 'successCount' => 0,
+ 'failCount' => 0,
+ 'status' => 'pending',
+ 'statusText' => '进行中',
+ 'createTime' => date('Y-m-d H:i:s', $task['createTime']),
+ 'contentLibraryName' => ''
+ ];
+ }
+
+ // 应用筛选条件
+ $filteredList = [];
+ foreach ($resultList as $item) {
+ // 推送类型筛选
+ if (!empty($pushType)) {
+ if ($pushType === 'friend' && $item['pushTypeCode'] !== 'friend') {
+ continue;
+ }
+ if ($pushType === 'group' && $item['pushTypeCode'] !== 'group') {
+ continue;
+ }
+ if ($pushType === 'announcement' && $item['pushTypeCode'] !== 'announcement') {
+ continue;
+ }
+ }
+
+ // 状态筛选
+ if (!empty($status)) {
+ if ($status === 'success' && $item['status'] !== 'success') {
+ continue;
+ }
+ if ($status === 'progress') {
+ // 进行中:包括 partial 和 pending
+ if ($item['status'] !== 'partial' && $item['status'] !== 'pending') {
+ continue;
+ }
+ }
+ if ($status === 'failed' && $item['status'] !== 'failed') {
+ continue;
+ }
+ }
+
+ $filteredList[] = $item;
+ }
+
+ // 按创建时间倒序排序
+ usort($filteredList, function($a, $b) {
+ return strtotime($b['createTime']) - strtotime($a['createTime']);
+ });
+
+ // 分页处理
+ $total = count($filteredList);
+ $offset = ($page - 1) * $limit;
+ $list = array_slice($filteredList, $offset, $limit);
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]);
+ }
+
+ /**
+ * 获取状态文本
+ * @param string $status 状态码
+ * @return string 状态文本
+ */
+ private function getStatusText($status)
+ {
+ $statusMap = [
+ 'success' => '已完成',
+ 'partial' => '进行中',
+ 'pending' => '进行中',
+ 'failed' => '失败'
+ ];
+ return $statusMap[$status] ?? '未知';
+ }
+
+ /**
+ * 获取已创建的群列表(自动建群)
+ * @return \think\response\Json
+ */
+ public function getCreatedGroupsList()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 100);
+ $keyword = $this->request->param('keyword', '');
+
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '工作台ID不能为空']);
+ }
+
+ // 验证工作台权限
+ $where = [
+ ['id', '=', $workbenchId],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['type', '=', self::TYPE_GROUP_CREATE],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+ $workbench = Workbench::where($where)->find();
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在或无权限']);
+ }
+
+ // 获取已创建的群ID列表(状态为成功且groupId不为空)
+ $groupIds = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('status', 2) // STATUS_SUCCESS = 2
+ ->where('groupId', '<>', null)
+ ->group('groupId')
+ ->column('groupId');
+
+ if (empty($groupIds)) {
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => [],
+ 'total' => 0,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]);
+ }
+
+ // 查询群组详细信息(从s2_wechat_chatroom表查询)
+ $query = Db::table('s2_wechat_chatroom')->alias('wc')
+ ->join('wechat_account wa', 'wa.wechatId = wc.wechatAccountWechatId', 'left')
+ ->where('wc.id', 'in', $groupIds)
+ ->where('wc.isDeleted', 0);
+
+ // 关键字搜索
+ if (!empty($keyword)) {
+ $query->where(function ($q) use ($keyword) {
+ $q->where('wc.nickname', 'like', '%' . $keyword . '%')
+ ->whereOr('wc.chatroomId', 'like', '%' . $keyword . '%')
+ ->whereOr('wa.nickName', 'like', '%' . $keyword . '%');
+ });
+ }
+
+ $total = $query->count();
+ $list = $query->field('wc.id,wc.nickname as groupName,wc.chatroomId,wc.chatroomAvatar as groupAvatar,wc.wechatAccountWechatId as ownerWechatId,wc.createTime,wc.chatroomOwnerNickname as ownerNickname,wc.chatroomOwnerAvatar as ownerAvatar,wa.alias as ownerAlias')
+ ->order('wc.createTime', 'desc')
+ ->page($page, $limit)
+ ->select();
+
+ // 统计每个群的成员数量和成员信息
+ foreach ($list as &$item) {
+ // 统计该群的成员数量(从workbench_group_create_item表统计)
+ $memberCount = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('groupId', $item['id'])
+ ->where('status', 'in', [2, 4]) // 创建成功和管理员好友已拉入
+ ->count();
+
+ // 获取成员列表(用于显示成员头像)
+ $memberList = Db::name('workbench_group_create_item')->alias('wgci')
+ ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wgci.friendId', 'left')
+ ->where('wgci.workbenchId', $workbenchId)
+ ->where('wgci.groupId', $item['id'])
+ ->where('wgci.status', 'in', [2, 4])
+ ->field('wf.avatar, wf.wechatId, wf.nickname')
+ ->order('wgci.createTime', 'asc')
+ ->limit(10) // 最多显示10个成员头像
+ ->select();
+
+ // 格式化成员头像列表
+ $memberAvatars = [];
+ foreach ($memberList as $member) {
+ if (!empty($member['avatar'])) {
+ $memberAvatars[] = [
+ 'avatar' => $member['avatar'],
+ 'wechatId' => $member['wechatId'] ?? '',
+ 'nickname' => $member['nickname'] ?? ''
+ ];
+ }
+ }
+
+ // 计算剩余成员数(用于显示"+XX")
+ $remainingCount = $memberCount > count($memberAvatars) ? $memberCount - count($memberAvatars) : 0;
+
+ // 格式化返回数据
+ $item['memberCount'] = $memberCount;
+ $item['memberCountText'] = $memberCount . '人'; // 格式化为"XX人"
+ $item['createTime'] = !empty($item['createTime']) ? date('Y-m-d', $item['createTime']) : ''; // 格式化为"YYYY-MM-DD"
+ $item['memberAvatars'] = $memberAvatars; // 成员头像列表(最多10个)
+ $item['remainingCount'] = $remainingCount; // 剩余成员数(用于显示"+XX")
+
+ // 保留原有字段,但调整格式
+ $item['groupName'] = $item['groupName'] ?? '';
+ $item['groupAvatar'] = $item['groupAvatar'] ?? '';
+ }
+ unset($item);
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]);
+ }
+
+ /**
+ * 获取已创建群的详情(自动建群)
+ * @return \think\response\Json
+ */
+ public function getCreatedGroupDetail()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ $groupId = $this->request->param('groupId', 0);
+
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '工作台ID不能为空']);
+ }
+
+ if (empty($groupId)) {
+ return json(['code' => 400, 'msg' => '群ID不能为空']);
+ }
+
+ // 验证工作台权限
+ $where = [
+ ['id', '=', $workbenchId],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['type', '=', self::TYPE_GROUP_CREATE],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+ $workbench = Workbench::where($where)->find();
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在或无权限']);
+ }
+
+ // 验证该群是否属于该工作台
+ $groupItem = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('groupId', $groupId)
+ ->where('status', 2) // STATUS_SUCCESS = 2
+ ->find();
+
+ if (empty($groupItem)) {
+ return json(['code' => 404, 'msg' => '群不存在或不属于该工作台']);
+ }
+
+ // 查询群基本信息(从s2_wechat_chatroom表查询)
+ $group = Db::table('s2_wechat_chatroom')->alias('wc')
+ ->join('wechat_account wa', 'wa.wechatId = wc.wechatAccountWechatId', 'left')
+ ->where('wc.id', $groupId)
+ ->where('wc.isDeleted', 0)
+ ->field('wc.id,wc.nickname as groupName,wc.chatroomId,wc.chatroomAvatar as groupAvatar,wc.wechatAccountWechatId as ownerWechatId,wc.createTime,wc.chatroomOwnerNickname as ownerNickname,wc.chatroomOwnerAvatar as ownerAvatar,wa.alias as ownerAlias,wc.announce')
+ ->find();
+
+ if (empty($group)) {
+ return json(['code' => 404, 'msg' => '群不存在']);
+ }
+
+ // 获取chatroomId
+ $chatroomId = $group['chatroomId'] ?? '';
+ if (empty($chatroomId)) {
+ return json(['code' => 400, 'msg' => '群聊ID不存在']);
+ }
+
+ // 从s2_wechat_chatroom_member表查询所有成员列表(不限制数量)
+ $memberList = Db::table('s2_wechat_chatroom_member')->alias('wcm')
+ ->join(['s2_wechat_friend' => 'wf'], 'wf.wechatId = wcm.wechatId', 'left')
+ ->where('wcm.chatroomId', $chatroomId)
+ ->field('wcm.wechatId,wcm.nickname as memberNickname,wcm.avatar as memberAvatar,wcm.conRemark as memberRemark,wcm.alias as memberAlias,wcm.createTime as joinTime,wcm.updateTime,wf.id as friendId,wf.nickname as friendNickname,wf.avatar as friendAvatar')
+ ->order('wcm.createTime', 'asc')
+ ->select();
+
+ // 去重:按wechatId去重,保留第一条记录
+ $memberMap = [];
+ foreach ($memberList as $member) {
+ $wechatId = $member['wechatId'] ?? '';
+ if (!empty($wechatId) && !isset($memberMap[$wechatId])) {
+ $memberMap[$wechatId] = $member;
+ }
+ }
+ $memberList = array_values($memberMap); // 重新索引数组
+
+ // 获取在群中的成员wechatId列表(用于判断是否已退群)
+ $inGroupWechatIds = array_column($memberList, 'wechatId');
+ $inGroupWechatIds = array_filter($inGroupWechatIds); // 过滤空值
+
+ // 获取通过自动建群加入的成员信息(用于判断入群状态和已退群成员)
+ $autoJoinMemberList = Db::name('workbench_group_create_item')
+ ->alias('wgci')
+ ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wgci.friendId', 'left')
+ ->where('wgci.workbenchId', $workbenchId)
+ ->where('wgci.groupId', $groupId)
+ ->where('wgci.status', 'in', [2, 4]) // 创建成功和管理员好友已拉入
+ ->field('wf.wechatId,wf.id as friendId,wf.nickname as friendNickname,wf.avatar as friendAvatar,wgci.createTime as autoJoinTime')
+ ->select();
+
+ // 去重:按wechatId去重,保留第一条记录
+ $autoJoinMemberMap = [];
+ foreach ($autoJoinMemberList as $autoMember) {
+ $wechatId = $autoMember['wechatId'] ?? '';
+ if (!empty($wechatId) && !isset($autoJoinMemberMap[$wechatId])) {
+ $autoJoinMemberMap[$wechatId] = $autoMember;
+ }
+ }
+ $autoJoinMemberList = array_values($autoJoinMemberMap); // 重新索引数组
+
+ // 统计成员总数(包括在群中的成员和已退群的成员)
+ // 先统计在群中的成员数
+ $inGroupCount = count($memberList);
+
+ // 统计已退群的成员数(在自动建群记录中但不在群中的成员)
+ $quitCount = 0;
+ if (!empty($autoJoinMemberList)) {
+ foreach ($autoJoinMemberList as $autoMember) {
+ $wechatId = $autoMember['wechatId'] ?? '';
+ if (!empty($wechatId) && !in_array($wechatId, $inGroupWechatIds)) {
+ $quitCount++;
+ }
+ }
+ }
+
+ // 总成员数 = 在群中的成员数 + 已退群的成员数
+ $memberCount = $inGroupCount + $quitCount;
+
+ // 获取自动建群加入的成员wechatId列表(用于判断入群状态)
+ $autoJoinWechatIds = array_column($autoJoinMemberList, 'wechatId');
+ $autoJoinWechatIds = array_filter($autoJoinWechatIds); // 过滤空值
+
+ // 格式化在群中的成员列表
+ $members = [];
+ $addedWechatIds = []; // 用于记录已添加的成员wechatId,避免重复
+ $ownerWechatId = $group['ownerWechatId'] ?? '';
+ foreach ($memberList as $member) {
+ $wechatId = $member['wechatId'] ?? '';
+
+ // 跳过空wechatId和已添加的成员
+ if (empty($wechatId) || in_array($wechatId, $addedWechatIds)) {
+ continue;
+ }
+
+ // 标记为已添加
+ $addedWechatIds[] = $wechatId;
+
+ // 根据wechatId判断是否为群主
+ $isOwner = (!empty($ownerWechatId) && $wechatId == $ownerWechatId) ? 1 : 0;
+
+ // 判断入群状态:如果在自动建群记录中,说明是通过自动建群加入的;否则是其他方式加入的
+ $joinStatus = in_array($wechatId, $autoJoinWechatIds) ? 'auto' : 'manual';
+
+ // 判断是否已退群:如果成员在s2_wechat_chatroom_member表中存在,说明在群中;否则已退群
+ // 由于我们已经从s2_wechat_chatroom_member表查询,所以这里都是"在群中"状态
+ $isQuit = 0; // 0=在群中,1=已退群
+
+ // 优先使用friend表的昵称和头像,如果没有则使用member表的
+ $nickname = !empty($member['friendNickname']) ? $member['friendNickname'] : ($member['memberNickname'] ?? '');
+ $avatar = !empty($member['friendAvatar']) ? $member['friendAvatar'] : ($member['memberAvatar'] ?? '');
+
+ $members[] = [
+ 'friendId' => $member['friendId'] ?? 0,
+ 'wechatId' => $wechatId,
+ 'nickname' => $nickname,
+ 'avatar' => $avatar,
+ 'alias' => $member['memberAlias'] ?? '',
+ 'remark' => $member['memberRemark'] ?? '',
+ 'isOwner' => $isOwner, // 标记群主
+ 'joinStatus' => $joinStatus, // 入群状态:auto=自动建群加入,manual=其他方式加入
+ 'isQuit' => $isQuit, // 是否已退群:0=在群中,1=已退群
+ 'joinTime' => !empty($member['joinTime']) ? date('Y-m-d H:i:s', $member['joinTime']) : '', // 入群时间
+ ];
+ }
+
+ // 添加已退群的成员(在自动建群记录中但不在群中的成员)
+ foreach ($autoJoinMemberList as $autoMember) {
+ $wechatId = $autoMember['wechatId'] ?? '';
+
+ // 跳过空wechatId、已在群中的成员和已添加的成员
+ if (empty($wechatId) || in_array($wechatId, $inGroupWechatIds) || in_array($wechatId, $addedWechatIds)) {
+ continue;
+ }
+
+ // 标记为已添加
+ $addedWechatIds[] = $wechatId;
+
+ // 根据wechatId判断是否为群主
+ $isOwner = (!empty($ownerWechatId) && $wechatId == $ownerWechatId) ? 1 : 0;
+
+ $members[] = [
+ 'friendId' => $autoMember['friendId'] ?? 0,
+ 'wechatId' => $wechatId,
+ 'nickname' => $autoMember['friendNickname'] ?? '',
+ 'avatar' => $autoMember['friendAvatar'] ?? '',
+ 'alias' => '',
+ 'remark' => '',
+ 'isOwner' => $isOwner, // 标记群主
+ 'joinStatus' => 'auto', // 入群状态:auto=自动建群加入
+ 'isQuit' => 1, // 是否已退群:1=已退群
+ 'joinTime' => !empty($autoMember['autoJoinTime']) ? date('Y-m-d H:i:s', $autoMember['autoJoinTime']) : '', // 入群时间
+ ];
+ }
+
+ // 将群主排在第一位
+ usort($members, function($a, $b) {
+ if ($a['isOwner'] == $b['isOwner']) {
+ return 0;
+ }
+ return $a['isOwner'] > $b['isOwner'] ? -1 : 1;
+ });
+
+ // 格式化返回数据
+ $result = [
+ 'id' => $group['id'],
+ 'groupName' => $group['groupName'] ?? '',
+ 'chatroomId' => $group['chatroomId'] ?? '',
+ 'groupAvatar' => $group['groupAvatar'] ?? '',
+ 'ownerWechatId' => $group['ownerWechatId'] ?? '',
+ 'ownerNickname' => $group['ownerNickname'] ?? '',
+ 'ownerAvatar' => $group['ownerAvatar'] ?? '',
+ 'ownerAlias' => $group['ownerAlias'] ?? '',
+ 'announce' => $group['announce'] ?? '',
+ 'createTime' => !empty($group['createTime']) ? date('Y-m-d H:i', $group['createTime']) : '', // 格式化为"YYYY-MM-DD HH:MM"
+ 'memberCount' => $memberCount,
+ 'memberCountText' => $memberCount . '人', // 格式化为"XX人"
+ 'workbenchName' => $workbench->name ?? '', // 任务名称(工作台名称)
+ 'members' => $members // 所有成员列表
+ ];
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => $result
+ ]);
+ }
+
+ /**
+ * 同步群最新信息(包括群成员)
+ * @return \think\response\Json
+ */
+ public function syncGroupInfo()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ $groupId = $this->request->param('groupId', 0);
+
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '工作台ID不能为空']);
+ }
+
+ if (empty($groupId)) {
+ return json(['code' => 400, 'msg' => '群ID不能为空']);
+ }
+
+ // 验证工作台权限
+ $where = [
+ ['id', '=', $workbenchId],
+ ['companyId', '=', $this->request->userInfo['companyId']],
+ ['type', '=', self::TYPE_GROUP_CREATE],
+ ['isDel', '=', 0]
+ ];
+ if (empty($this->request->userInfo['isAdmin'])) {
+ $where[] = ['userId', '=', $this->request->userInfo['id']];
+ }
+
+ $workbench = Workbench::where($where)->find();
+ if (empty($workbench)) {
+ return json(['code' => 404, 'msg' => '工作台不存在或无权限']);
+ }
+
+ // 验证该群是否属于该工作台
+ $groupItem = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('groupId', $groupId)
+ ->where('status', 2) // STATUS_SUCCESS = 2
+ ->find();
+
+ if (empty($groupItem)) {
+ return json(['code' => 404, 'msg' => '群不存在或不属于该工作台']);
+ }
+
+ // 查询群基本信息,获取chatroomId和wechatAccountWechatId
+ $group = Db::table('s2_wechat_chatroom')
+ ->where('id', $groupId)
+ ->where('isDeleted', 0)
+ ->field('id,chatroomId,wechatAccountWechatId')
+ ->find();
+
+ if (empty($group)) {
+ return json(['code' => 404, 'msg' => '群不存在']);
+ }
+
+ $chatroomId = $group['chatroomId'] ?? '';
+ $wechatAccountWechatId = $group['wechatAccountWechatId'] ?? '';
+
+ if (empty($chatroomId)) {
+ return json(['code' => 400, 'msg' => '群聊ID不存在']);
+ }
+
+ try {
+ // 实例化WechatChatroomController
+ $chatroomController = new \app\api\controller\WechatChatroomController();
+
+ // 1. 同步群信息(调用getlist方法)
+ $syncData = [
+ 'wechatChatroomId' => $chatroomId, // 使用chatroomId作为wechatChatroomId来指定要同步的群
+ 'wechatAccountKeyword' => $wechatAccountWechatId, // 通过群主微信ID筛选
+ 'isDeleted' => false,
+ 'pageIndex' => 1,
+ 'pageSize' => 100 // 获取足够多的数据
+ ];
+ $syncResult = $chatroomController->getlist($syncData, true, 0); // isInner = true
+ $syncResponse = json_decode($syncResult, true);
+
+ if (empty($syncResponse['code']) || $syncResponse['code'] != 200) {
+ return json(['code' => 500, 'msg' => '同步群信息失败:' . ($syncResponse['msg'] ?? '未知错误')]);
+ }
+
+ // 2. 同步群成员信息(调用listChatroomMember方法)
+ // wechatChatroomId 使用 s2_wechat_chatroom 表的 id(即groupId)
+ // chatroomId 使用群聊ID(chatroomId)
+ $wechatChatroomId = $groupId; // s2_wechat_chatroom表的id
+ $memberSyncResult = $chatroomController->listChatroomMember($wechatChatroomId, $chatroomId, true); // isInner = true
+ $memberSyncResponse = json_decode($memberSyncResult, true);
+
+ if (empty($memberSyncResponse['code']) || $memberSyncResponse['code'] != 200) {
+ // 成员同步失败不影响整体结果,记录警告即可
+ \think\facade\Log::warning("同步群成员失败。群ID: {$groupId}, 群聊ID: {$chatroomId}, 错误: " . ($memberSyncResponse['msg'] ?? '未知错误'));
+ }
+
+ return json([
+ 'code' => 200,
+ 'msg' => '同步成功',
+ 'data' => [
+ 'groupId' => $groupId,
+ 'chatroomId' => $chatroomId,
+ 'groupInfoSynced' => true,
+ 'memberInfoSynced' => !empty($memberSyncResponse['code']) && $memberSyncResponse['code'] == 200
+ ]
+ ]);
+ } catch (\Exception $e) {
+ \think\facade\Log::error("同步群信息异常。群ID: {$groupId}, 错误: " . $e->getMessage());
+ return json(['code' => 500, 'msg' => '同步失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 修改群名称、群公告
+ * @return \think\response\Json
+ */
+ public function modifyGroupInfo()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ $groupId = $this->request->param('groupId', 0);
+ $chatroomName = $this->request->param('chatroomName', '');
+ $announce = $this->request->param('announce', '');
+
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '工作台ID不能为空']);
+ }
+
+ if (empty($groupId)) {
+ return json(['code' => 400, 'msg' => '群ID不能为空']);
+ }
+
+ // 至少需要提供一个修改项
+ if (empty($chatroomName) && empty($announce)) {
+ return json(['code' => 400, 'msg' => '请至少提供群名称或群公告中的一个参数']);
+ }
+
+ // 查询群基本信息
+ $group = Db::table('s2_wechat_chatroom')
+ ->where('id', $groupId)
+ ->where('isDeleted', 0)
+ ->field('id,chatroomId,wechatAccountWechatId,accountId,wechatAccountId')
+ ->find();
+
+ if (empty($group)) {
+ return json(['code' => 404, 'msg' => '群不存在']);
+ }
+
+ $chatroomId = $group['id'] ?? '';
+
+ if (empty($chatroomId)) {
+ return json(['code' => 400, 'msg' => '群聊ID不存在']);
+ }
+
+ try {
+ // 直接使用群表中的账号信息
+ $executeAccountId = $group['accountId'] ?? 0;
+ $executeWechatAccountId = $group['wechatAccountId'] ?? 0;
+ $executeWechatId = $group['wechatAccountWechatId'] ?? '';
+
+ // 确保 wechatId 不为空
+ if (empty($executeWechatId)) {
+ return json(['code' => 400, 'msg' => '无法获取微信账号ID']);
+ }
+
+ // 调用 WebSocketController 修改群信息
+ // 获取系统API账号信息(用于WebSocket连接)
+ $username = Env::get('api.username2', '');
+ $password = Env::get('api.password2', '');
+
+ if (empty($username) || empty($password)) {
+ return json(['code' => 500, 'msg' => '系统API账号配置缺失']);
+ }
+
+ // 获取系统账号ID
+ $systemAccountId = Db::name('users')->where('account', $username)->value('s2_accountId');
+ if (empty($systemAccountId)) {
+ return json(['code' => 500, 'msg' => '未找到系统账号ID']);
+ }
+
+ $webSocketController = new WebSocketController([
+ 'userName' => $username,
+ 'password' => $password,
+ 'accountId' => $systemAccountId
+ ]);
+ // 构建修改参数
+ $modifyData = [
+ 'wechatChatroomId' => $chatroomId,
+ 'wechatAccountId' => $executeWechatAccountId,
+ ];
+ if (!empty($chatroomName)) {
+ $modifyData['chatroomName'] = $chatroomName;
+ }
+ if (!empty($announce)) {
+ $modifyData['announce'] = $announce;
+ }
+
+
+ $modifyResult = $webSocketController->CmdChatroomModifyInfo($modifyData);
+ $modifyResponse = json_decode($modifyResult, true);
+ if (empty($modifyResponse['code']) || $modifyResponse['code'] != 200) {
+ return json(['code' => 500, 'msg' => '修改群信息失败:' . ($modifyResponse['msg'] ?? '未知错误')]);
+ }
+
+ // 修改成功后更新数据库
+ $updateData = [
+ 'updateTime' => time()
+ ];
+
+ // 如果修改了群名称,更新数据库
+ if (!empty($chatroomName)) {
+ $updateData['nickname'] = $chatroomName;
+ }
+
+ // 如果修改了群公告,更新数据库
+ if (!empty($announce)) {
+ $updateData['announce'] = $announce;
+ }
+
+ // 更新数据库
+ Db::table('s2_wechat_chatroom')
+ ->where('id', $groupId)
+ ->update($updateData);
+
+ return json([
+ 'code' => 200,
+ 'msg' => '修改成功',
+ 'data' => [
+ 'groupId' => $groupId,
+ 'chatroomId' => $chatroomId,
+ 'chatroomName' => $chatroomName,
+ 'announce' => $announce
+ ]
+ ]);
+ } catch (\Exception $e) {
+ \think\facade\Log::error("修改群信息异常。群ID: {$groupId}, 错误: " . $e->getMessage());
+ return json(['code' => 500, 'msg' => '修改失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 转移群聊到指定账号
+ * @param int $groupId 群ID(s2_wechat_chatroom表的id)
+ * @param string $chatroomId 群聊ID
+ * @param int $toAccountId 目标账号ID(s2_company_account表的id)
+ * @param int $toWechatAccountId 目标微信账号ID(s2_wechat_account表的id)
+ * @return array ['success' => bool, 'msg' => string]
+ */
+ protected function transferChatroomToAccount($groupId, $chatroomId, $toAccountId, $toWechatAccountId)
+ {
+ try {
+ // 查询目标账号信息
+ $targetAccount = Db::table('s2_company_account')
+ ->where('id', $toAccountId)
+ ->field('id,userName,realName,nickname')
+ ->find();
+
+ if (empty($targetAccount)) {
+ return ['success' => false, 'msg' => '目标账号不存在'];
+ }
+
+ // 查询目标微信账号信息
+ $targetWechatAccount = Db::table('s2_wechat_account')
+ ->where('id', $toWechatAccountId)
+ ->field('id,wechatId,deviceAccountId')
+ ->find();
+
+ if (empty($targetWechatAccount)) {
+ return ['success' => false, 'msg' => '目标微信账号不存在'];
+ }
+
+ // 调用 AutomaticAssign 进行群聊转移
+ $automaticAssign = new \app\api\controller\AutomaticAssign();
+
+ // 构建转移参数(通过 API 调用)
+ $transferData = [
+ 'wechatChatroomId' => $chatroomId, // 使用群聊ID
+ 'toAccountId' => $toAccountId,
+ 'wechatAccountKeyword' => $targetWechatAccount['wechatId'] ?? '',
+ 'isDeleted' => false
+ ];
+
+ // 直接更新数据库(因为 API 可能不支持指定单个群聊ID转移)
+ // 更新 s2_wechat_chatroom 表的 accountId 和 wechatAccountId
+ Db::table('s2_wechat_chatroom')
+ ->where('id', $groupId)
+ ->update([
+ 'accountId' => $toAccountId,
+ 'accountUserName' => $targetAccount['userName'] ?? '',
+ 'accountRealName' => $targetAccount['realName'] ?? '',
+ 'accountNickname' => $targetAccount['nickname'] ?? '',
+ 'wechatAccountId' => $toWechatAccountId,
+ 'wechatAccountWechatId' => $targetWechatAccount['wechatId'] ?? '',
+ 'updateTime' => time()
+ ]);
+
+ return ['success' => true, 'msg' => '转移成功'];
+ } catch (\Exception $e) {
+ \think\facade\Log::error("转移群聊异常。群ID: {$groupId}, 目标账号ID: {$toAccountId}, 错误: " . $e->getMessage());
+ return ['success' => false, 'msg' => $e->getMessage()];
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/application/cunkebao/controller/workbench/WorkbenchHelperController.php b/application/cunkebao/controller/workbench/WorkbenchHelperController.php
new file mode 100644
index 0000000..285fcab
--- /dev/null
+++ b/application/cunkebao/controller/workbench/WorkbenchHelperController.php
@@ -0,0 +1,313 @@
+request->param('deviceIds', '');
+ $companyId = $this->request->userInfo['companyId'];
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+
+ $where = [
+ ['wc.companyId', '=', $companyId],
+ ];
+
+ if (!empty($deviceIds)) {
+ $deviceIds = explode(',', $deviceIds);
+ $where[] = ['dwl.deviceId', 'in', $deviceIds];
+ }
+
+ $wechatAccounts = Db::name('wechat_customer')->alias('wc')
+ ->join('device_wechat_login dwl', 'dwl.wechatId = wc.wechatId AND dwl.companyId = wc.companyId AND dwl.alive = 1')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.wechatId = wc.wechatId')
+ ->where($where)
+ ->field('wa.id,wa.wechatId,wa.nickName,wa.labels')
+ ->select();
+ $labels = [];
+ $wechatIds = [];
+ foreach ($wechatAccounts as $account) {
+ $labelArr = json_decode($account['labels'], true);
+ if (is_array($labelArr)) {
+ foreach ($labelArr as $label) {
+ if ($label !== '' && $label !== null) {
+ $labels[] = $label;
+ }
+ }
+ }
+ $wechatIds[] = $account['wechatId'];
+ }
+ // 去重(只保留一个)
+ $labels = array_values(array_unique($labels));
+ $wechatIds = array_unique($wechatIds);
+
+ // 搜索过滤
+ if (!empty($keyword)) {
+ $labels = array_filter($labels, function ($label) use ($keyword) {
+ return mb_stripos($label, $keyword) !== false;
+ });
+ $labels = array_values($labels); // 重新索引数组
+ }
+
+ // 分页处理
+ $labels2 = array_slice($labels, ($page - 1) * $limit, $limit);
+
+ // 统计数量
+ $newLabel = [];
+ foreach ($labels2 as $label) {
+ $friendCount = Db::table('s2_wechat_friend')
+ ->whereIn('ownerWechatId', $wechatIds)
+ ->where('labels', 'like', '%"' . $label . '"%')
+ ->count();
+ $newLabel[] = [
+ 'label' => $label,
+ 'count' => $friendCount
+ ];
+ }
+
+ // 返回结果
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $newLabel,
+ 'total' => count($labels),
+ ]
+ ]);
+ }
+
+ /**
+ * 获取群列表
+ * @return \think\response\Json
+ */
+ public function getGroupList()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+
+ $where = [
+ ['wg.deleteTime', '=', 0],
+ ['wg.companyId', '=', $this->request->userInfo['companyId']],
+ ];
+
+ if (!empty($keyword)) {
+ $where[] = ['wg.name', 'like', '%' . $keyword . '%'];
+ }
+
+ $query = Db::name('wechat_group')->alias('wg')
+ ->join('wechat_account wa', 'wa.wechatId = wg.ownerWechatId')
+ ->where($where);
+
+ $total = $query->count();
+ $list = $query->order('wg.id', 'desc')
+ ->field('wg.id,wg.name as groupName,wg.ownerWechatId,wa.nickName,wg.createTime,wa.avatar,wa.alias,wg.avatar as groupAvatar')
+ ->page($page, $limit)
+ ->select();
+
+ // 优化:格式化时间,头像兜底
+ $defaultGroupAvatar = '';
+ $defaultAvatar = '';
+ foreach ($list as &$item) {
+ $item['createTime'] = $item['createTime'] ? date('Y-m-d H:i:s', $item['createTime']) : '';
+ $item['groupAvatar'] = $item['groupAvatar'] ?: $defaultGroupAvatar;
+ $item['avatar'] = $item['avatar'] ?: $defaultAvatar;
+ }
+
+ return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]);
+ }
+
+ /**
+ * 获取流量池列表
+ * @return \think\response\Json
+ */
+ public function getTrafficPoolList()
+ {
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+ $companyId = $this->request->userInfo['companyId'];
+
+ $baseQuery = Db::name('traffic_source_package')->alias('tsp')
+ ->where('tsp.isDel', 0)
+ ->whereIn('tsp.companyId', [$companyId, 0]);
+
+ if (!empty($keyword)) {
+ $baseQuery->whereLike('tsp.name', '%' . $keyword . '%');
+ }
+
+ $total = (clone $baseQuery)->count();
+
+ $list = $baseQuery
+ ->leftJoin('traffic_source_package_item tspi', 'tspi.packageId = tsp.id and tspi.isDel = 0')
+ ->field('tsp.id,tsp.name,tsp.description,tsp.pic,tsp.companyId,COUNT(tspi.id) as itemCount,max(tspi.createTime) as latestImportTime')
+ ->group('tsp.id')
+ ->order('tsp.id', 'desc')
+ ->page($page, $limit)
+ ->select();
+
+ foreach ($list as &$item) {
+ $item['latestImportTime'] = !empty($item['latestImportTime']) ? date('Y-m-d H:i:s', $item['latestImportTime']) : '';
+ }
+ unset($item);
+
+ return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]);
+ }
+
+ /**
+ * 获取账号列表
+ * @return \think\response\Json
+ */
+ public function getAccountList()
+ {
+ $companyId = $this->request->userInfo['companyId'];
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $query = Db::table('s2_company_account')
+ ->alias('a')
+ ->where(['a.departmentId' => $companyId, 'a.status' => 0])
+ ->whereNotLike('a.userName', '%_offline%')
+ ->whereNotLike('a.userName', '%_delete%');
+
+ $total = $query->count();
+ $list = $query->field('a.id,a.userName,a.realName,a.nickname,a.memo')
+ ->page($page, $limit)
+ ->select();
+
+ return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]);
+ }
+
+ /**
+ * 获取京东联盟导购媒体
+ * @return \think\response\Json
+ */
+ public function getJdSocialMedia()
+ {
+ $data = Db::name('jd_social_media')->order('id DESC')->select();
+ return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]);
+ }
+
+ /**
+ * 获取京东联盟广告位
+ * @return \think\response\Json
+ */
+ public function getJdPromotionSite()
+ {
+ $id = $this->request->param('id', '');
+ if (empty($id)) {
+ return json(['code' => 500, 'msg' => '参数缺失']);
+ }
+
+ $data = Db::name('jd_promotion_site')->where('jdSocialMediaId', $id)->order('id DESC')->select();
+ return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]);
+ }
+
+ /**
+ * 京东转链-京推推
+ * @param string $content
+ * @param string $positionid
+ * @return string
+ */
+ public function changeLink($content = '', $positionid = '')
+ {
+ $unionId = Env::get('jd.unionId', '');
+ $jttAppId = Env::get('jd.jttAppId', '');
+ $appKey = Env::get('jd.appKey', '');
+ $apiUrl = Env::get('jd.apiUrl', '');
+
+ $content = !empty($content) ? $content : $this->request->param('content', '');
+ $positionid = !empty($positionid) ? $positionid : $this->request->param('positionid', '');
+
+ if (empty($content)) {
+ return json_encode(['code' => 500, 'msg' => '转链的内容为空']);
+ }
+
+ // 验证是否包含链接
+ if (!$this->containsLink($content)) {
+ return json_encode(['code' => 500, 'msg' => '内容中未检测到有效链接']);
+ }
+
+ if (empty($unionId) || empty($jttAppId) || empty($appKey) || empty($apiUrl)) {
+ return json_encode(['code' => 500, 'msg' => '参数缺失']);
+ }
+ $params = [
+ 'unionid' => $unionId,
+ 'content' => $content,
+ 'appid' => $jttAppId,
+ 'appkey' => $appKey,
+ 'v' => 'v2'
+ ];
+
+ if (!empty($positionid)) {
+ $params['positionid'] = $positionid;
+ }
+
+ $res = requestCurl($apiUrl, $params, 'GET', [], 'json');
+ $res = json_decode($res, true);
+ if (empty($res)) {
+ return json_encode(['code' => 500, 'msg' => '未知错误']);
+ }
+ $result = $res['result'];
+ if ($res['return'] == 0) {
+ return json_encode(['code' => 200, 'data' => $result['chain_content'], 'msg' => $result['msg']]);
+ } else {
+ return json_encode(['code' => 500, 'msg' => $result['msg']]);
+ }
+ }
+
+ /**
+ * 验证内容是否包含链接
+ * @param string $content 要检测的内容
+ * @return bool
+ */
+ private function containsLink($content)
+ {
+ // 定义各种链接的正则表达式模式
+ $patterns = [
+ // HTTP/HTTPS链接
+ '/https?:\/\/[^\s]+/i',
+ // 京东商品链接
+ '/item\.jd\.com\/\d+/i',
+ // 京东短链接
+ '/u\.jd\.com\/[a-zA-Z0-9]+/i',
+ // 淘宝商品链接
+ '/item\.taobao\.com\/item\.htm\?id=\d+/i',
+ // 天猫商品链接
+ '/detail\.tmall\.com\/item\.htm\?id=\d+/i',
+ // 淘宝短链接
+ '/m\.tb\.cn\/[a-zA-Z0-9]+/i',
+ // 拼多多链接
+ '/mobile\.yangkeduo\.com\/goods\.html\?goods_id=\d+/i',
+ // 苏宁易购链接
+ '/product\.suning\.com\/\d+\/\d+\.html/i',
+ // 通用域名模式(包含常见电商域名)
+ '/(?:jd|taobao|tmall|yangkeduo|suning|amazon|dangdang)\.com[^\s]*/i',
+ // 通用短链接模式
+ '/[a-zA-Z0-9-]+\.[a-zA-Z]{2,}\/[a-zA-Z0-9\-._~:\/?#\[\]@!$&\'()*+,;=]+/i'
+ ];
+
+ // 遍历所有模式进行匹配
+ foreach ($patterns as $pattern) {
+ if (preg_match($pattern, $content)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
+
diff --git a/application/cunkebao/controller/workbench/WorkbenchImportContactController.php b/application/cunkebao/controller/workbench/WorkbenchImportContactController.php
new file mode 100644
index 0000000..5c1d5ea
--- /dev/null
+++ b/application/cunkebao/controller/workbench/WorkbenchImportContactController.php
@@ -0,0 +1,69 @@
+request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $workbenchId = $this->request->param('workbenchId', 0);
+
+ $where = [
+ ['wici.workbenchId', '=', $workbenchId]
+ ];
+
+ // 查询发布记录
+ $list = Db::name('workbench_import_contact_item')->alias('wici')
+ ->join('traffic_pool tp', 'tp.id = wici.poolId', 'left')
+ ->join('traffic_source tc', 'tc.identifier = tp.identifier', 'left')
+ ->join('wechat_account wa', 'wa.wechatId = tp.wechatId', 'left')
+ ->field([
+ 'wici.id',
+ 'wici.workbenchId',
+ 'wici.createTime',
+ 'tp.identifier',
+ 'tp.mobile',
+ 'tp.wechatId',
+ 'tc.name',
+ 'wa.nickName',
+ 'wa.avatar',
+ 'wa.alias',
+ ])
+ ->where($where)
+ ->order('tc.name DESC,wici.createTime DESC')
+ ->group('tp.identifier')
+ ->page($page, $limit)
+ ->select();
+
+ foreach ($list as &$item) {
+ $item['createTime'] = date('Y-m-d H:i:s', $item['createTime']);
+ }
+
+ // 获取总记录数
+ $total = Db::name('workbench_import_contact_item')->alias('wici')
+ ->where($where)
+ ->count();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ ]
+ ]);
+ }
+}
+
diff --git a/application/cunkebao/controller/workbench/WorkbenchMomentsController.php b/application/cunkebao/controller/workbench/WorkbenchMomentsController.php
new file mode 100644
index 0000000..92d001a
--- /dev/null
+++ b/application/cunkebao/controller/workbench/WorkbenchMomentsController.php
@@ -0,0 +1,125 @@
+request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $workbenchId = $this->request->param('workbenchId', 0);
+
+ $where = [
+ ['wmsi.workbenchId', '=', $workbenchId]
+ ];
+
+ // 查询发布记录
+ $list = Db::name('workbench_moments_sync_item')->alias('wmsi')
+ ->join('content_item ci', 'ci.id = wmsi.contentId', 'left')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wmsi.wechatAccountId', 'left')
+ ->field([
+ 'wmsi.id',
+ 'wmsi.workbenchId',
+ 'wmsi.createTime as publishTime',
+ 'ci.contentType',
+ 'ci.content',
+ 'ci.resUrls',
+ 'ci.urls',
+ 'wa.nickName as operatorName',
+ 'wa.avatar as operatorAvatar'
+ ])
+ ->where($where)
+ ->order('wmsi.createTime', 'desc')
+ ->page($page, $limit)
+ ->select();
+
+ foreach ($list as &$item) {
+ $item['resUrls'] = json_decode($item['resUrls'], true);
+ $item['urls'] = json_decode($item['urls'], true);
+ }
+
+
+ // 获取总记录数
+ $total = Db::name('workbench_moments_sync_item')->alias('wmsi')
+ ->where($where)
+ ->count();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]);
+ }
+
+ /**
+ * 获取朋友圈发布统计
+ * @return \think\response\Json
+ */
+ public function getMomentsStats()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ // 获取今日数据
+ $todayStart = strtotime(date('Y-m-d') . ' 00:00:00');
+ $todayEnd = strtotime(date('Y-m-d') . ' 23:59:59');
+
+ $todayStats = Db::name('workbench_moments_sync_item')
+ ->where([
+ ['workbenchId', '=', $workbenchId],
+ ['createTime', 'between', [$todayStart, $todayEnd]]
+ ])
+ ->field([
+ 'COUNT(*) as total',
+ 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success',
+ 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed'
+ ])
+ ->find();
+
+ // 获取总数据
+ $totalStats = Db::name('workbench_moments_sync_item')
+ ->where('workbenchId', $workbenchId)
+ ->field([
+ 'COUNT(*) as total',
+ 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success',
+ 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed'
+ ])
+ ->find();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'today' => [
+ 'total' => intval($todayStats['total']),
+ 'success' => intval($todayStats['success']),
+ 'failed' => intval($todayStats['failed'])
+ ],
+ 'total' => [
+ 'total' => intval($totalStats['total']),
+ 'success' => intval($totalStats['success']),
+ 'failed' => intval($totalStats['failed'])
+ ]
+ ]
+ ]);
+ }
+}
+
diff --git a/application/cunkebao/controller/workbench/WorkbenchTrafficController.php b/application/cunkebao/controller/workbench/WorkbenchTrafficController.php
new file mode 100644
index 0000000..284f01c
--- /dev/null
+++ b/application/cunkebao/controller/workbench/WorkbenchTrafficController.php
@@ -0,0 +1,309 @@
+request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $workbenchId = $this->request->param('workbenchId', 0);
+
+ $where = [
+ ['wtdi.workbenchId', '=', $workbenchId]
+ ];
+
+ // 查询分发记录
+ $list = Db::name('workbench_traffic_distribution_item')->alias('wtdi')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wtdi.wechatAccountId', 'left')
+ ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wtdi.wechatFriendId', 'left')
+ ->field([
+ 'wtdi.id',
+ 'wtdi.workbenchId',
+ 'wtdi.wechatAccountId',
+ 'wtdi.wechatFriendId',
+ 'wtdi.createTime as distributeTime',
+ 'wtdi.status',
+ 'wtdi.errorMsg',
+ 'wa.nickName as operatorName',
+ 'wa.avatar as operatorAvatar',
+ 'wf.nickName as friendName',
+ 'wf.avatar as friendAvatar',
+ 'wf.gender',
+ 'wf.province',
+ 'wf.city'
+ ])
+ ->where($where)
+ ->order('wtdi.createTime', 'desc')
+ ->page($page, $limit)
+ ->select();
+
+ // 处理数据
+ foreach ($list as &$item) {
+ // 处理时间格式
+ $item['distributeTime'] = date('Y-m-d H:i:s', $item['distributeTime']);
+
+ // 处理性别
+ $genderMap = [
+ 0 => '未知',
+ 1 => '男',
+ 2 => '女'
+ ];
+ $item['genderText'] = $genderMap[$item['gender']] ?? '未知';
+
+ // 处理状态文字
+ $statusMap = [
+ 0 => '待分发',
+ 1 => '分发成功',
+ 2 => '分发失败'
+ ];
+ $item['statusText'] = $statusMap[$item['status']] ?? '未知状态';
+ }
+
+ // 获取总记录数
+ $total = Db::name('workbench_traffic_distribution_item')->alias('wtdi')
+ ->where($where)
+ ->count();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]);
+ }
+
+ /**
+ * 获取流量分发统计
+ * @return \think\response\Json
+ */
+ public function getTrafficDistributionStats()
+ {
+ $workbenchId = $this->request->param('workbenchId', 0);
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ // 获取今日数据
+ $todayStart = strtotime(date('Y-m-d') . ' 00:00:00');
+ $todayEnd = strtotime(date('Y-m-d') . ' 23:59:59');
+
+ $todayStats = Db::name('workbench_traffic_distribution_item')
+ ->where([
+ ['workbenchId', '=', $workbenchId],
+ ['createTime', 'between', [$todayStart, $todayEnd]]
+ ])
+ ->field([
+ 'COUNT(*) as total',
+ 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success',
+ 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed'
+ ])
+ ->find();
+
+ // 获取总数据
+ $totalStats = Db::name('workbench_traffic_distribution_item')
+ ->where('workbenchId', $workbenchId)
+ ->field([
+ 'COUNT(*) as total',
+ 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success',
+ 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed'
+ ])
+ ->find();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'today' => [
+ 'total' => intval($todayStats['total']),
+ 'success' => intval($todayStats['success']),
+ 'failed' => intval($todayStats['failed'])
+ ],
+ 'total' => [
+ 'total' => intval($totalStats['total']),
+ 'success' => intval($totalStats['success']),
+ 'failed' => intval($totalStats['failed'])
+ ]
+ ]
+ ]);
+ }
+
+ /**
+ * 获取流量分发详情
+ * @return \think\response\Json
+ */
+ public function getTrafficDistributionDetail()
+ {
+ $id = $this->request->param('id', 0);
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ $detail = Db::name('workbench_traffic_distribution_item')->alias('wtdi')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wtdi.wechatAccountId', 'left')
+ ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wtdi.wechatFriendId', 'left')
+ ->field([
+ 'wtdi.id',
+ 'wtdi.workbenchId',
+ 'wtdi.wechatAccountId',
+ 'wtdi.wechatFriendId',
+ 'wtdi.createTime as distributeTime',
+ 'wtdi.status',
+ 'wtdi.errorMsg',
+ 'wa.nickName as operatorName',
+ 'wa.avatar as operatorAvatar',
+ 'wf.nickName as friendName',
+ 'wf.avatar as friendAvatar',
+ 'wf.gender',
+ 'wf.province',
+ 'wf.city',
+ 'wf.signature',
+ 'wf.remark'
+ ])
+ ->where('wtdi.id', $id)
+ ->find();
+
+ if (empty($detail)) {
+ return json(['code' => 404, 'msg' => '记录不存在']);
+ }
+
+ // 处理数据
+ $detail['distributeTime'] = date('Y-m-d H:i:s', $detail['distributeTime']);
+
+ // 处理性别
+ $genderMap = [
+ 0 => '未知',
+ 1 => '男',
+ 2 => '女'
+ ];
+ $detail['genderText'] = $genderMap[$detail['gender']] ?? '未知';
+
+ // 处理状态文字
+ $statusMap = [
+ 0 => '待分发',
+ 1 => '分发成功',
+ 2 => '分发失败'
+ ];
+ $detail['statusText'] = $statusMap[$detail['status']] ?? '未知状态';
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => $detail
+ ]);
+ }
+
+ /**
+ * 创建流量分发计划
+ * @return \think\response\Json
+ */
+ public function createTrafficPlan()
+ {
+ $param = $this->request->post();
+ Db::startTrans();
+ try {
+ // 1. 创建主表
+ $planId = Db::name('ck_workbench')->insertGetId([
+ 'name' => $param['name'],
+ 'type' => 5, // TYPE_TRAFFIC_DISTRIBUTION
+ 'status' => 1,
+ 'autoStart' => $param['autoStart'] ?? 0,
+ 'userId' => $this->request->userInfo['id'],
+ 'companyId' => $this->request->userInfo['companyId'],
+ 'createTime' => time(),
+ 'updateTime' => time()
+ ]);
+ // 2. 创建扩展表
+ Db::name('ck_workbench_traffic_config')->insert([
+ 'workbenchId' => $planId,
+ 'distributeType' => $param['distributeType'],
+ 'maxPerDay' => $param['maxPerDay'],
+ 'timeType' => $param['timeType'],
+ 'startTime' => $param['startTime'],
+ 'endTime' => $param['endTime'],
+ 'targets' => json_encode($param['targets'], JSON_UNESCAPED_UNICODE),
+ 'pools' => json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE),
+ 'createTime' => time(),
+ 'updateTime' => time()
+ ]);
+ Db::commit();
+ return json(['code' => 200, 'msg' => '创建成功']);
+ } catch (\Exception $e) {
+ Db::rollback();
+ return json(['code' => 500, 'msg' => '创建失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 获取流量列表
+ * @return \think\response\Json
+ */
+ public function getTrafficList()
+ {
+ $companyId = $this->request->userInfo['companyId'];
+ $page = $this->request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+ $workbenchId = $this->request->param('workbenchId', '');
+ $isRecycle = $this->request->param('isRecycle', '');
+ if (empty($workbenchId)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ $workbench = Db::name('workbench')->where(['id' => $workbenchId, 'isDel' => 0, 'companyId' => $companyId, 'type' => 5])->find();
+
+ if (empty($workbench)) {
+ return json(['code' => 400, 'msg' => '该任务不存在或已删除']);
+ }
+ $query = Db::name('workbench_traffic_config_item')->alias('wtc')
+ ->join(['s2_wechat_friend' => 'wf'], 'wtc.wechatFriendId = wf.id')
+ ->join('users u', 'wtc.wechatAccountId = u.s2_accountId', 'left')
+ ->field([
+ 'wtc.id', 'wtc.isRecycle', 'wtc.isRecycle', 'wtc.createTime','wtc.recycleTime',
+ 'wf.wechatId', 'wf.alias', 'wf.nickname', 'wf.avatar', 'wf.gender', 'wf.phone',
+ 'u.account', 'u.username'
+ ])
+ ->where(['wtc.workbenchId' => $workbenchId])
+ ->order('wtc.id DESC');
+
+ if (!empty($keyword)) {
+ $query->where('wf.wechatId|wf.alias|wf.nickname|wf.phone|u.account|u.username', 'like', '%' . $keyword . '%');
+ }
+
+ if ($isRecycle != '' || $isRecycle != null) {
+ $query->where('isRecycle',$isRecycle);
+ }
+
+ $total = $query->count();
+ $list = $query->page($page, $limit)->select();
+
+ foreach ($list as &$item) {
+ $item['createTime'] = date('Y-m-d H:i:s', $item['createTime']);
+ $item['recycleTime'] = date('Y-m-d H:i:s', $item['recycleTime']);
+ }
+ unset($item);
+
+ $data = [
+ 'total' => $total,
+ 'list' => $list,
+ ];
+
+ return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]);
+ }
+}
+
diff --git a/application/cunkebao/model/AiKnowledgeBase.php b/application/cunkebao/model/AiKnowledgeBase.php
new file mode 100644
index 0000000..fbf0664
--- /dev/null
+++ b/application/cunkebao/model/AiKnowledgeBase.php
@@ -0,0 +1,11 @@
+ $item) {
+ if (is_integer($key)) {
+ $temp[] = "{$item} = VALUES({$item})";
+ } else {
+ $temp[] = "{$key} = {$item}";
+ }
+ }
+ $duplicate = implode(',', $temp);
+ }
+ }
+ if (!empty($limit)) {
+ // 分批写入 自动启动事务支持
+ $this->startTrans();
+
+ try {
+ // array_chunk 函数把数组分割为新的数组块。
+ //其中每个数组的单元数目由 size 参数决定。最后一个数组的单元数目可能会少几个。
+ //可选参数 preserve_key 是一个布尔值,它指定新数组的元素是否有和原数组相同的键(用于关联数组),还是从 0 开始的新数字键(用于索引数组)。默认是分配新的键。
+ $array = array_chunk($data, $limit, true);
+ $count = 0;
+
+ foreach ($array as $item) {
+ $sql = $this->fetchSql(true)->insertAll($item);
+ $sql = preg_replace("/INSERT\s*INTO\s*/", "INSERT IGNORE INTO ", $sql);
+ if (is_string($duplicate)) {
+ $sql = $sql . ' ON DUPLICATE KEY UPDATE ' . $duplicate;
+ }
+ $count += $this->execute($sql); // 获取影响函数
+ }
+ // 提交事务
+ $this->commit();
+ } catch (\Exception $e) {
+ $this->rollback();
+ throw $e;
+ }
+ return $count;
+ } else {
+ $sql = $this->fetchSql(true)->insertAll($data);
+ $sql = preg_replace("/INSERT\s*INTO\s*/", "INSERT IGNORE INTO ", $sql);
+ if (is_string($duplicate)) {
+ $sql = $sql . ' ON DUPLICATE KEY UPDATE ' . $duplicate;
+ }
+ return $this->execute($sql);
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/model/ContentItem.php b/application/cunkebao/model/ContentItem.php
new file mode 100644
index 0000000..efc9202
--- /dev/null
+++ b/application/cunkebao/model/ContentItem.php
@@ -0,0 +1,20 @@
+belongsTo('User', 'userId', 'id');
+ }
+
+ // 定义关联的内容项目
+ public function items()
+ {
+ return $this->hasMany('ContentItem', 'libraryId', 'id');
+ }
+
+ // 根据ID数组获取内容库列表
+ public static function getByIds($ids)
+ {
+ if (empty($ids)) {
+ return [];
+ }
+
+ return self::where('id', 'in', $ids)->select();
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/model/DistributionChannel.php b/application/cunkebao/model/DistributionChannel.php
new file mode 100644
index 0000000..500ada9
--- /dev/null
+++ b/application/cunkebao/model/DistributionChannel.php
@@ -0,0 +1,87 @@
+ 'integer',
+ 'companyId' => 'integer',
+ 'totalCustomers' => 'integer',
+ 'todayCustomers' => 'integer',
+ 'totalFriends' => 'integer',
+ 'todayFriends' => 'integer',
+ 'withdrawableAmount' => 'integer',
+ 'createTime' => 'timestamp',
+ 'updateTime' => 'timestamp',
+ 'deleteTime' => 'timestamp',
+ ];
+
+ /**
+ * 生成渠道编码
+ * 格式:QD + 时间戳 + 9位随机字符串
+ *
+ * @return string
+ */
+ public static function generateChannelCode()
+ {
+ $prefix = 'QD';
+ $timestamp = time();
+ $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
+ $randomStr = '';
+
+ // 生成9位随机字符串
+ for ($i = 0; $i < 9; $i++) {
+ $randomStr .= $chars[mt_rand(0, strlen($chars) - 1)];
+ }
+
+ $code = $prefix . $timestamp . $randomStr;
+
+ // 检查是否已存在
+ $exists = self::where('code', $code)->find();
+ if ($exists) {
+ // 如果已存在,递归重新生成
+ return self::generateChannelCode();
+ }
+
+ return $code;
+ }
+
+ /**
+ * 创建类型:manual(手动创建)
+ */
+ const CREATE_TYPE_MANUAL = 'manual';
+
+ /**
+ * 创建类型:auto(扫码创建)
+ */
+ const CREATE_TYPE_AUTO = 'auto';
+
+ /**
+ * 状态:enabled(启用)
+ */
+ const STATUS_ENABLED = 'enabled';
+
+ /**
+ * 状态:disabled(禁用)
+ */
+ const STATUS_DISABLED = 'disabled';
+}
+
diff --git a/application/cunkebao/model/DistributionWithdrawal.php b/application/cunkebao/model/DistributionWithdrawal.php
new file mode 100644
index 0000000..64e0861
--- /dev/null
+++ b/application/cunkebao/model/DistributionWithdrawal.php
@@ -0,0 +1,71 @@
+ 'integer',
+ 'companyId' => 'integer',
+ 'channelId' => 'integer',
+ 'amount' => 'integer',
+ 'reviewTime' => 'timestamp',
+ 'applyTime' => 'timestamp',
+ 'createTime' => 'timestamp',
+ 'updateTime' => 'timestamp',
+ ];
+
+ /**
+ * 状态:pending(待审核)
+ */
+ const STATUS_PENDING = 'pending';
+
+ /**
+ * 状态:approved(已通过)
+ */
+ const STATUS_APPROVED = 'approved';
+
+ /**
+ * 状态:rejected(已拒绝)
+ */
+ const STATUS_REJECTED = 'rejected';
+
+ /**
+ * 状态:paid(已打款)
+ */
+ const STATUS_PAID = 'paid';
+
+ /**
+ * 支付类型:wechat(微信)
+ */
+ const PAY_TYPE_WECHAT = 'wechat';
+
+ /**
+ * 支付类型:alipay(支付宝)
+ */
+ const PAY_TYPE_ALIPAY = 'alipay';
+
+ /**
+ * 支付类型:bankcard(银行卡)
+ */
+ const PAY_TYPE_BANKCARD = 'bankcard';
+}
+
+
+
+
diff --git a/application/cunkebao/model/FriendTask.php b/application/cunkebao/model/FriendTask.php
new file mode 100644
index 0000000..0be5936
--- /dev/null
+++ b/application/cunkebao/model/FriendTask.php
@@ -0,0 +1,286 @@
+ '待处理',
+ self::STATUS_PROCESSING => '处理中',
+ self::STATUS_APPROVED => '已通过',
+ self::STATUS_REJECTED => '已拒绝',
+ self::STATUS_EXPIRED => '已过期',
+ self::STATUS_CANCELLED => '已取消'
+ ];
+
+ return isset($statusMap[$status]) ? $statusMap[$status] : '未知状态';
+ }
+
+ /**
+ * 获取好友任务列表
+ * @param array $where 查询条件
+ * @param string $order 排序条件
+ * @param int $page 页码
+ * @param int $limit 每页数量
+ * @return \think\Paginator
+ */
+ public static function getTaskList($where = [], $order = 'createTime desc', $page = 1, $limit = 10)
+ {
+ return self::where($where)
+ ->order($order)
+ ->paginate($limit, false, ['page' => $page]);
+ }
+
+ /**
+ * 获取任务详情
+ * @param int $id 任务ID
+ * @return array|null
+ */
+ public static function getTaskDetail($id)
+ {
+ return self::where('id', $id)->find();
+ }
+
+ /**
+ * 创建好友任务
+ * @param array $data 任务数据
+ * @return int|bool 任务ID或false
+ */
+ public static function createTask($data)
+ {
+ // 确保必填字段存在
+ if (!isset($data['id'])) {
+ return false;
+ }
+
+ // 设置默认值
+ if (!isset($data['status'])) {
+ $data['status'] = self::STATUS_PENDING;
+ }
+
+ // 设置创建时间
+ $data['createTime'] = time();
+ $data['updateTime'] = time();
+
+ // 创建任务
+ $task = new self;
+ $task->allowField(true)->save($data);
+
+ return $task->id;
+ }
+
+ /**
+ * 更新任务信息
+ * @param int $id 任务ID
+ * @param array $data 更新数据
+ * @return bool
+ */
+ public static function updateTask($id, $data)
+ {
+ // 更新时间
+ $data['updateTime'] = time();
+
+ return self::where('id', $id)->update($data);
+ }
+
+ /**
+ * 更新任务状态
+ * @param int $id 任务ID
+ * @param int $status 新状态
+ * @param string $remark 备注
+ * @return bool
+ */
+ public static function updateTaskStatus($id, $status, $remark = '')
+ {
+ $data = [
+ 'status' => $status,
+ 'updateTime' => time()
+ ];
+
+ if (!empty($remark)) {
+ $data['remark'] = $remark;
+ }
+
+ return self::where('id', $id)->update($data);
+ }
+
+ /**
+ * 取消任务
+ * @param int $id 任务ID
+ * @param string $remark 取消原因
+ * @return bool
+ */
+ public static function cancelTask($id, $remark = '')
+ {
+ return self::updateTaskStatus($id, self::STATUS_CANCELLED, $remark);
+ }
+
+ /**
+ * 任务审批通过
+ * @param int $id 任务ID
+ * @param string $remark 备注信息
+ * @return bool
+ */
+ public static function approveTask($id, $remark = '')
+ {
+ return self::updateTaskStatus($id, self::STATUS_APPROVED, $remark);
+ }
+
+ /**
+ * 任务拒绝
+ * @param int $id 任务ID
+ * @param string $remark 拒绝原因
+ * @return bool
+ */
+ public static function rejectTask($id, $remark = '')
+ {
+ return self::updateTaskStatus($id, self::STATUS_REJECTED, $remark);
+ }
+
+ /**
+ * 根据微信账号ID获取任务列表
+ * @param int $wechatAccountId 微信账号ID
+ * @param array $status 状态数组,默认查询所有状态
+ * @param int $page 页码
+ * @param int $limit 每页数量
+ * @return \think\Paginator
+ */
+ public static function getTasksByWechatAccount($wechatAccountId, $status = [], $page = 1, $limit = 10)
+ {
+ $where = ['wechatAccountId' => $wechatAccountId];
+
+ if (!empty($status)) {
+ $where['status'] = ['in', $status];
+ }
+
+ return self::getTaskList($where, 'createTime desc', $page, $limit);
+ }
+
+ /**
+ * 根据操作账号ID获取任务列表
+ * @param int $operatorAccountId 操作账号ID
+ * @param array $status 状态数组,默认查询所有状态
+ * @param int $page 页码
+ * @param int $limit 每页数量
+ * @return \think\Paginator
+ */
+ public static function getTasksByOperator($operatorAccountId, $status = [], $page = 1, $limit = 10)
+ {
+ $where = ['operatorAccountId' => $operatorAccountId];
+
+ if (!empty($status)) {
+ $where['status'] = ['in', $status];
+ }
+
+ return self::getTaskList($where, 'createTime desc', $page, $limit);
+ }
+
+ /**
+ * 根据手机号/微信号查询任务
+ * @param string $phone 手机号/微信号
+ * @param int $tenantId 租户ID
+ * @return array
+ */
+ public static function getTasksByPhone($phone, $tenantId = null)
+ {
+ $where = ['phone' => $phone];
+
+ if ($tenantId !== null) {
+ $where['tenantId'] = $tenantId;
+ }
+
+ return self::where($where)->select();
+ }
+
+ /**
+ * 获取统计数据
+ * @param int $tenantId 租户ID
+ * @param int $timeRange 时间范围(秒)
+ * @return array
+ */
+ public static function getTaskStats($tenantId = null, $timeRange = 86400)
+ {
+ $where = [];
+
+ if ($tenantId !== null) {
+ $where['tenantId'] = $tenantId;
+ }
+
+ // 时间范围
+ $startTime = time() - $timeRange;
+ $where['createTime'] = ['>=', $startTime];
+
+ // 获取各状态的任务数量
+ $stats = [
+ 'total' => self::where($where)->count(),
+ 'pending' => self::where(array_merge($where, ['status' => self::STATUS_PENDING]))->count(),
+ 'processing' => self::where(array_merge($where, ['status' => self::STATUS_PROCESSING]))->count(),
+ 'approved' => self::where(array_merge($where, ['status' => self::STATUS_APPROVED]))->count(),
+ 'rejected' => self::where(array_merge($where, ['status' => self::STATUS_REJECTED]))->count(),
+ 'expired' => self::where(array_merge($where, ['status' => self::STATUS_EXPIRED]))->count(),
+ 'cancelled' => self::where(array_merge($where, ['status' => self::STATUS_CANCELLED]))->count()
+ ];
+
+ return $stats;
+ }
+
+ /**
+ * 任务处理结果统计
+ * @param int $tenantId 租户ID
+ * @param int $timeRange 时间范围(秒)
+ * @return array
+ */
+ public static function getTaskResultStats($tenantId = null, $timeRange = 86400 * 30)
+ {
+ $where = [];
+
+ if ($tenantId !== null) {
+ $where['tenantId'] = $tenantId;
+ }
+
+ // 时间范围
+ $startTime = time() - $timeRange;
+ $where['createTime'] = ['>=', $startTime];
+
+ // 获取处理结果数据
+ $stats = [
+ 'total' => self::where($where)->count(),
+ 'approved' => self::where(array_merge($where, ['status' => self::STATUS_APPROVED]))->count(),
+ 'rejected' => self::where(array_merge($where, ['status' => self::STATUS_REJECTED]))->count(),
+ 'pending' => self::where(array_merge($where, ['status' => ['in', [self::STATUS_PENDING, self::STATUS_PROCESSING]]]))->count(),
+ 'other' => self::where(array_merge($where, ['status' => ['in', [self::STATUS_EXPIRED, self::STATUS_CANCELLED]]]))->count()
+ ];
+
+ // 计算成功率
+ $stats['approvalRate'] = $stats['total'] > 0 ? round($stats['approved'] / $stats['total'] * 100, 2) : 0;
+
+ return $stats;
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/model/PlanTask.php b/application/cunkebao/model/PlanTask.php
new file mode 100644
index 0000000..d4ebc9f
--- /dev/null
+++ b/application/cunkebao/model/PlanTask.php
@@ -0,0 +1,13 @@
+belongsTo('Workbench', 'id', 'userId');
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/model/WechatChatroom.php b/application/cunkebao/model/WechatChatroom.php
new file mode 100644
index 0000000..4c07e96
--- /dev/null
+++ b/application/cunkebao/model/WechatChatroom.php
@@ -0,0 +1,15 @@
+hasOne('WorkbenchAutoLike', 'workbenchId', 'id');
+ }
+
+ // 朋友圈同步配置关联
+ public function momentsSync()
+ {
+ return $this->hasOne('WorkbenchMomentsSync', 'workbenchId', 'id');
+ }
+
+ // 群消息推送配置关联
+ public function groupPush()
+ {
+ return $this->hasOne('WorkbenchGroupPush', 'workbenchId', 'id');
+ }
+
+ // 自动建群配置关联
+ public function groupCreate()
+ {
+ return $this->hasOne('WorkbenchGroupCreate', 'workbenchId', 'id');
+ }
+
+ // 流量分发配置关联
+ public function trafficConfig()
+ {
+ return $this->hasOne('WorkbenchTrafficConfig', 'workbenchId', 'id');
+ }
+
+ public function importContact()
+ {
+ return $this->hasOne('WorkbenchImportContact', 'workbenchId', 'id');
+ }
+
+
+ /**
+ * 用户关联
+ */
+ public function user()
+ {
+ return $this->belongsTo('User', 'userId', 'id');
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/model/WorkbenchAutoLike.php b/application/cunkebao/model/WorkbenchAutoLike.php
new file mode 100644
index 0000000..44fd263
--- /dev/null
+++ b/application/cunkebao/model/WorkbenchAutoLike.php
@@ -0,0 +1,25 @@
+belongsTo('Workbench', 'workbenchId', 'id');
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/model/WorkbenchGroupCreate.php b/application/cunkebao/model/WorkbenchGroupCreate.php
new file mode 100644
index 0000000..8721db9
--- /dev/null
+++ b/application/cunkebao/model/WorkbenchGroupCreate.php
@@ -0,0 +1,23 @@
+belongsTo('Workbench', 'workbenchId', 'id');
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/model/WorkbenchGroupPush.php b/application/cunkebao/model/WorkbenchGroupPush.php
new file mode 100644
index 0000000..b5e4447
--- /dev/null
+++ b/application/cunkebao/model/WorkbenchGroupPush.php
@@ -0,0 +1,23 @@
+belongsTo('Workbench', 'workbenchId', 'id');
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/model/WorkbenchImportContact.php b/application/cunkebao/model/WorkbenchImportContact.php
new file mode 100644
index 0000000..58b73b6
--- /dev/null
+++ b/application/cunkebao/model/WorkbenchImportContact.php
@@ -0,0 +1,25 @@
+belongsTo('Workbench', 'workbenchId', 'id');
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/model/WorkbenchMomentsSync.php b/application/cunkebao/model/WorkbenchMomentsSync.php
new file mode 100644
index 0000000..625f372
--- /dev/null
+++ b/application/cunkebao/model/WorkbenchMomentsSync.php
@@ -0,0 +1,59 @@
+belongsTo('Workbench', 'workbenchId', 'id');
+ }
+
+ // 定义关联的内容库
+ public function contentLibraries()
+ {
+ return $this->belongsToMany('ContentLibrary', 'workbench_content_relation', 'contentLibraryId', 'workbenchId');
+ }
+
+ // 开始时间获取器
+ public function getStartTimeAttr($value)
+ {
+ return $value ? date('H:i', strtotime($value)) : '';
+ }
+
+ // 结束时间获取器
+ public function getEndTimeAttr($value)
+ {
+ return $value ? date('H:i', strtotime($value)) : '';
+ }
+
+ // 同步类型获取器
+ public function getSyncTypeTextAttr($value, $data)
+ {
+ $types = [
+ self::SYNC_TYPE_TEXT => '文本',
+ self::SYNC_TYPE_IMAGE => '图片',
+ self::SYNC_TYPE_VIDEO => '视频',
+ self::SYNC_TYPE_LINK => '链接'
+ ];
+ return isset($types[$data['syncType']]) ? $types[$data['syncType']] : '未知';
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/model/WorkbenchTrafficConfig.php b/application/cunkebao/model/WorkbenchTrafficConfig.php
new file mode 100644
index 0000000..abf54be
--- /dev/null
+++ b/application/cunkebao/model/WorkbenchTrafficConfig.php
@@ -0,0 +1,25 @@
+belongsTo('Workbench', 'workbenchId', 'id');
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/service/ContentItemService.php b/application/cunkebao/service/ContentItemService.php
new file mode 100644
index 0000000..b0a1df3
--- /dev/null
+++ b/application/cunkebao/service/ContentItemService.php
@@ -0,0 +1,158 @@
+save($this->prepareItemData($data, $libraryId));
+
+ if (!$result) {
+ Db::rollback();
+ return ['code' => 500, 'msg' => '创建内容项目失败'];
+ }
+
+ Db::commit();
+ return ['code' => 200, 'msg' => '创建成功', 'data' => ['id' => $item->id]];
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ['code' => 500, 'msg' => '创建失败:' . $e->getMessage()];
+ }
+ }
+
+ /**
+ * 准备内容项目数据
+ * @param array $data 原始数据
+ * @param int $libraryId 内容库ID
+ * @return array
+ */
+ private function prepareItemData($data, $libraryId)
+ {
+ return [
+ 'libraryId' => $libraryId,
+ 'title' => $data['title'] ?? '',
+ 'content' => $data['content'] ?? '',
+ 'images' => isset($data['images']) ? json_encode($data['images']) : json_encode([]),
+ 'videos' => isset($data['videos']) ? json_encode($data['videos']) : json_encode([]),
+ 'status' => $data['status'] ?? 0,
+ 'createTime' => time(),
+ 'updateTime' => time()
+ ];
+ }
+
+ /**
+ * 删除内容项目
+ * @param int $itemId 内容项目ID
+ * @return array
+ */
+ public function deleteItem($itemId)
+ {
+ try {
+ $result = ContentItem::where('id', $itemId)
+ ->update(['isDel' => 1, 'delTime' => time()]);
+
+ if ($result === false) {
+ return ['code' => 500, 'msg' => '删除失败'];
+ }
+
+ return ['code' => 200, 'msg' => '删除成功'];
+ } catch (\Exception $e) {
+ return ['code' => 500, 'msg' => '删除失败:' . $e->getMessage()];
+ }
+ }
+
+ // ==================== 查询相关 ====================
+
+ /**
+ * 获取内容项目列表
+ * @param array $params 查询参数
+ * @param int $libraryId 内容库ID
+ * @return array
+ */
+ public function getItemList($params, $libraryId)
+ {
+ $where = [
+ ['libraryId', '=', $libraryId],
+ ['isDel', '=', 0]
+ ];
+
+ if (!empty($params['keyword'])) {
+ $where[] = ['title', 'like', '%' . $params['keyword'] . '%'];
+ }
+
+ if (isset($params['status'])) {
+ $where[] = ['status', '=', $params['status']];
+ }
+
+ $list = ContentItem::where($where)
+ ->field('id,title,content,images,videos,status,createTime,updateTime')
+ ->order('id', 'desc')
+ ->page($params['page'], $params['limit'])
+ ->select();
+
+ $this->processItemList($list);
+
+ $total = ContentItem::where($where)->count();
+
+ return [
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $params['page']
+ ]
+ ];
+ }
+
+ /**
+ * 处理内容项目列表数据
+ * @param array $list 内容项目列表
+ */
+ private function processItemList(&$list)
+ {
+ foreach ($list as &$item) {
+ $item['images'] = json_decode($item['images'] ?: '[]', true);
+ $item['videos'] = json_decode($item['videos'] ?: '[]', true);
+ }
+ }
+
+ // ==================== 状态管理 ====================
+
+ /**
+ * 更新内容项目状态
+ * @param int $itemId 内容项目ID
+ * @param int $status 状态
+ * @return array
+ */
+ public function updateItemStatus($itemId, $status)
+ {
+ try {
+ $result = ContentItem::where('id', $itemId)
+ ->update(['status' => $status, 'updateTime' => time()]);
+
+ if ($result === false) {
+ return ['code' => 500, 'msg' => '更新状态失败'];
+ }
+
+ return ['code' => 200, 'msg' => '更新成功'];
+ } catch (\Exception $e) {
+ return ['code' => 500, 'msg' => '更新失败:' . $e->getMessage()];
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/service/ContentLibraryService.php b/application/cunkebao/service/ContentLibraryService.php
new file mode 100644
index 0000000..23f0d9c
--- /dev/null
+++ b/application/cunkebao/service/ContentLibraryService.php
@@ -0,0 +1,185 @@
+ $data['name'],
+ 'userId' => $userId,
+ 'isDel' => 0
+ ])->find();
+
+ if ($exists) {
+ return ['code' => 400, 'msg' => '内容库名称已存在'];
+ }
+
+ Db::startTrans();
+ try {
+ $library = new ContentLibrary;
+ $result = $library->save($this->prepareLibraryData($data, $userId));
+
+ if (!$result) {
+ Db::rollback();
+ return ['code' => 500, 'msg' => '创建内容库失败'];
+ }
+
+ Db::commit();
+ return ['code' => 200, 'msg' => '创建成功', 'data' => ['id' => $library->id]];
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ['code' => 500, 'msg' => '创建失败:' . $e->getMessage()];
+ }
+ }
+
+ /**
+ * 准备内容库数据
+ * @param array $data 原始数据
+ * @param int $userId 用户ID
+ * @return array
+ */
+ private function prepareLibraryData($data, $userId)
+ {
+ return [
+ 'name' => $data['name'],
+ 'sourceFriends' => $data['sourceType'] == 1 ? json_encode($data['friends']) : json_encode([]),
+ 'sourceGroups' => $data['sourceType'] == 2 ? json_encode($data['groups']) : json_encode([]),
+ 'groupMembers' => $data['sourceType'] == 2 ? json_encode($data['groupMembers']) : json_encode([]),
+ 'keywordInclude' => isset($data['keywordInclude']) ? json_encode($data['keywordInclude'], 256) : json_encode([]),
+ 'keywordExclude' => isset($data['keywordExclude']) ? json_encode($data['keywordExclude'], 256) : json_encode([]),
+ 'aiEnabled' => $data['aiEnabled'] ?? 0,
+ 'aiPrompt' => $data['aiPrompt'] ?? '',
+ 'timeEnabled' => $data['timeEnabled'] ?? 0,
+ 'timeStart' => isset($data['startTime']) ? strtotime($data['startTime']) : 0,
+ 'timeEnd' => isset($data['endTime']) ? strtotime($data['endTime']) : 0,
+ 'sourceType' => $data['sourceType'] ?? 1,
+ 'status' => $data['status'] ?? 0,
+ 'userId' => $userId,
+ 'createTime' => time(),
+ 'updateTime' => time()
+ ];
+ }
+
+ // ==================== 查询相关 ====================
+
+ /**
+ * 获取内容库列表
+ * @param array $params 查询参数
+ * @param int $userId 用户ID
+ * @return array
+ */
+ public function getLibraryList($params, $userId)
+ {
+ $where = [
+ ['userId', '=', $userId],
+ ['isDel', '=', 0]
+ ];
+
+ if (!empty($params['keyword'])) {
+ $where[] = ['name', 'like', '%' . $params['keyword'] . '%'];
+ }
+
+ if (!empty($params['sourceType'])) {
+ $where[] = ['sourceType', '=', $params['sourceType']];
+ }
+
+ $list = ContentLibrary::where($where)
+ ->field('id,name,sourceFriends,sourceGroups,keywordInclude,keywordExclude,aiEnabled,aiPrompt,timeEnabled,timeStart,timeEnd,status,sourceType,userId,createTime,updateTime')
+ ->with(['user' => function($query) {
+ $query->field('id,username');
+ }])
+ ->order('id', 'desc')
+ ->page($params['page'], $params['limit'])
+ ->select();
+
+ $this->processLibraryList($list);
+
+ $total = ContentLibrary::where($where)->count();
+
+ return [
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $params['page']
+ ]
+ ];
+ }
+
+ /**
+ * 处理内容库列表数据
+ * @param array $list 内容库列表
+ */
+ private function processLibraryList(&$list)
+ {
+ foreach ($list as &$item) {
+ $item['sourceFriends'] = json_decode($item['sourceFriends'] ?: '[]', true);
+ $item['sourceGroups'] = json_decode($item['sourceGroups'] ?: '[]', true);
+ $item['keywordInclude'] = json_decode($item['keywordInclude'] ?: '[]', true);
+ $item['keywordExclude'] = json_decode($item['keywordExclude'] ?: '[]', true);
+ $item['creatorName'] = $item['user']['username'] ?? '';
+
+ if (!empty($item['sourceFriends']) && $item['sourceType'] == 1) {
+ $item['selectedFriends'] = $this->getFriendsInfo($item['sourceFriends']);
+ }
+
+ if (!empty($item['sourceGroups']) && $item['sourceType'] == 2) {
+ $item['selectedGroups'] = $this->getGroupsInfo($item['sourceGroups']);
+ }
+
+ unset($item['user']);
+ }
+ }
+
+ // ==================== 数据关联查询 ====================
+
+ /**
+ * 获取好友信息
+ * @param array $friendIds 好友ID列表
+ * @return array
+ */
+ private function getFriendsInfo($friendIds)
+ {
+ if (empty($friendIds)) {
+ return [];
+ }
+
+ return Db::name('wechat_friend')->alias('wf')
+ ->field('wf.id,wf.wechatId, wa.nickname, wa.avatar')
+ ->join('wechat_account wa', 'wf.wechatId = wa.wechatId')
+ ->whereIn('wf.id', $friendIds)
+ ->select();
+ }
+
+ /**
+ * 获取群组信息
+ * @param array $groupIds 群组ID列表
+ * @return array
+ */
+ private function getGroupsInfo($groupIds)
+ {
+ if (empty($groupIds)) {
+ return [];
+ }
+
+ return Db::name('wechat_group')->alias('g')
+ ->field('g.id, g.chatroomId, g.name, g.avatar, g.ownerWechatId')
+ ->whereIn('g.id', $groupIds)
+ ->select();
+ }
+}
\ No newline at end of file
diff --git a/application/cunkebao/service/DistributionRewardService.php b/application/cunkebao/service/DistributionRewardService.php
new file mode 100644
index 0000000..668f3f6
--- /dev/null
+++ b/application/cunkebao/service/DistributionRewardService.php
@@ -0,0 +1,276 @@
+where('id', $taskId)
+ ->find();
+
+ if (!$task) {
+ return false;
+ }
+
+ // 解析分销配置
+ $sceneConf = json_decode($task['sceneConf'], true) ?: [];
+ $distributionConfig = $sceneConf['distribution'] ?? null;
+
+ // 检查是否开启分销
+ if (empty($distributionConfig) || empty($distributionConfig['enabled'])) {
+ return false;
+ }
+
+ // 检查是否有获客奖励
+ $rewardAmount = intval($distributionConfig['customerRewardAmount'] ?? 0);
+ if ($rewardAmount <= 0) {
+ return false;
+ }
+
+ // 获取渠道列表(从分销配置中获取允许分佣的渠道)
+ $channelIds = $distributionConfig['channels'] ?? [];
+ if (empty($channelIds) || !is_array($channelIds)) {
+ return false;
+ }
+
+ $companyId = $task['companyId'];
+ $sceneId = $task['sceneId'];
+
+ // 获取场景名称(用于展示来源类型)
+ $scene = Db::name('plan_scene')
+ ->where('id', $sceneId)
+ ->field('name')
+ ->find();
+ $sceneName = $scene['name'] ?? '未知场景';
+
+ // 如果指定了 channelId(cid),仅允许该渠道获得分佣
+ if (!empty($channelId)) {
+ // 必须在配置的渠道列表中,且是有效ID
+ if (!in_array($channelId, $channelIds)) {
+ // 该渠道不在本计划允许分佣的渠道列表中,直接返回
+ return false;
+ }
+ $channelIds = [$channelId];
+ }
+
+ // 开始事务
+ Db::startTrans();
+ try {
+ // 为每个渠道记录收益并更新可提现金额
+ foreach ($channelIds as $channelId) {
+ // 验证渠道是否存在
+ $channel = Db::name('distribution_channel')
+ ->where([
+ ['id', '=', $channelId],
+ ['companyId', '=', $companyId],
+ ['status', '=', 'enabled'],
+ ['deleteTime', '=', 0]
+ ])
+ ->find();
+
+ if (!$channel) {
+ continue; // 跳过不存在的渠道
+ }
+
+ // 记录收益明细
+ Db::name('distribution_revenue_record')->insert([
+ 'companyId' => $companyId,
+ 'channelId' => $channelId,
+ 'channelCode' => $channel['code'],
+ 'type' => 'customer_acquisition', // 获客类型
+ 'sourceType' => $sceneName,
+ 'sourceId' => $taskId, // 活动ID(获客任务ID)
+ 'amount' => $rewardAmount, // 金额(分)
+ 'remark' => '获客奖励:' . $phone,
+ 'createTime' => time(),
+ 'updateTime' => time(),
+ ]);
+
+ // 更新渠道可提现金额
+ Db::name('distribution_channel')
+ ->where('id', $channelId)
+ ->setInc('withdrawableAmount', $rewardAmount);
+
+ // 更新渠道获客统计
+ Db::name('distribution_channel')
+ ->where('id', $channelId)
+ ->setInc('totalCustomers', 1);
+
+ // 更新今日获客统计(如果是今天)
+ $todayStart = strtotime(date('Y-m-d 00:00:00'));
+ $todayEnd = strtotime(date('Y-m-d 23:59:59'));
+ $createTime = time();
+ if ($createTime >= $todayStart && $createTime <= $todayEnd) {
+ Db::name('distribution_channel')
+ ->where('id', $channelId)
+ ->setInc('todayCustomers', 1);
+ }
+ }
+
+ Db::commit();
+ return true;
+
+ } catch (Exception $e) {
+ Db::rollback();
+ throw $e;
+ }
+
+ } catch (Exception $e) {
+ // 记录错误日志,但不影响主流程
+ \think\Log::error('记录获客奖励失败:' . $e->getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * 记录添加好友奖励
+ *
+ * @param int $taskId 获客计划ID
+ * @param int $customerId 客户ID(task_customer表的id)
+ * @param string $phone 客户手机号
+ * @param int|null $channelId 渠道ID(分销渠道ID,对应distribution_channel.id)。为空时按配置的所有渠道分配
+ * @return bool
+ */
+ public static function recordAddFriendReward($taskId, $customerId, $phone, $channelId = null)
+ {
+ try {
+ // 获取获客计划信息
+ $task = Db::name('customer_acquisition_task')
+ ->where('id', $taskId)
+ ->find();
+
+ if (!$task) {
+ return false;
+ }
+
+ // 解析分销配置
+ $sceneConf = json_decode($task['sceneConf'], true) ?: [];
+ $distributionConfig = $sceneConf['distribution'] ?? null;
+
+ // 检查是否开启分销
+ if (empty($distributionConfig) || empty($distributionConfig['enabled'])) {
+ return false;
+ }
+
+ // 检查是否有添加奖励
+ $rewardAmount = intval($distributionConfig['addFriendRewardAmount'] ?? 0);
+ if ($rewardAmount <= 0) {
+ return false;
+ }
+
+ // 获取渠道列表(从分销配置中获取允许分佣的渠道)
+ $channelIds = $distributionConfig['channels'] ?? [];
+ if (empty($channelIds) || !is_array($channelIds)) {
+ return false;
+ }
+
+ $companyId = $task['companyId'];
+ $sceneId = $task['sceneId'];
+
+ // 获取场景名称(用于展示来源类型)
+ $scene = Db::name('plan_scene')
+ ->where('id', $sceneId)
+ ->field('name')
+ ->find();
+ $sceneName = $scene['name'] ?? '未知场景';
+
+ // 如果指定了 channelId(cid),仅允许该渠道获得分佣
+ if (!empty($channelId)) {
+ // 必须在配置的渠道列表中,且是有效ID
+ if (!in_array($channelId, $channelIds)) {
+ // 该渠道不在本计划允许分佣的渠道列表中,直接返回
+ return false;
+ }
+ $channelIds = [$channelId];
+ }
+
+ // 开始事务
+ Db::startTrans();
+ try {
+ // 为每个渠道记录收益并更新可提现金额
+ foreach ($channelIds as $channelId) {
+ // 验证渠道是否存在
+ $channel = Db::name('distribution_channel')
+ ->where([
+ ['id', '=', $channelId],
+ ['companyId', '=', $companyId],
+ ['status', '=', 'enabled'],
+ ['deleteTime', '=', 0]
+ ])
+ ->find();
+
+ if (!$channel) {
+ continue; // 跳过不存在的渠道
+ }
+
+ // 记录收益明细
+ Db::name('distribution_revenue_record')->insert([
+ 'companyId' => $companyId,
+ 'channelId' => $channelId,
+ 'channelCode' => $channel['code'],
+ 'type' => 'add_friend', // 添加好友类型
+ 'sourceType' => $sceneName,
+ 'sourceId' => $taskId, // 活动ID(获客任务ID)
+ 'amount' => $rewardAmount, // 金额(分)
+ 'remark' => '添加好友奖励:' . $phone,
+ 'createTime' => time(),
+ 'updateTime' => time(),
+ ]);
+
+ // 更新渠道可提现金额
+ Db::name('distribution_channel')
+ ->where('id', $channelId)
+ ->setInc('withdrawableAmount', $rewardAmount);
+
+ // 更新渠道好友统计
+ Db::name('distribution_channel')
+ ->where('id', $channelId)
+ ->setInc('totalFriends', 1);
+
+ // 更新今日好友统计(如果是今天)
+ $todayStart = strtotime(date('Y-m-d 00:00:00'));
+ $todayEnd = strtotime(date('Y-m-d 23:59:59'));
+ $createTime = time();
+ if ($createTime >= $todayStart && $createTime <= $todayEnd) {
+ Db::name('distribution_channel')
+ ->where('id', $channelId)
+ ->setInc('todayFriends', 1);
+ }
+ }
+
+ Db::commit();
+ return true;
+
+ } catch (Exception $e) {
+ Db::rollback();
+ throw $e;
+ }
+
+ } catch (Exception $e) {
+ // 记录错误日志,但不影响主流程
+ \think\Log::error('记录添加好友奖励失败:' . $e->getMessage());
+ return false;
+ }
+ }
+}
+
diff --git a/application/cunkebao/validate/DistributionChannel.php b/application/cunkebao/validate/DistributionChannel.php
new file mode 100644
index 0000000..11016e0
--- /dev/null
+++ b/application/cunkebao/validate/DistributionChannel.php
@@ -0,0 +1,31 @@
+ 'require|length:1,50',
+ 'phone' => 'regex:^1[3-9]\d{9}$',
+ 'wechatId' => 'max:50',
+ 'remarks' => 'max:200',
+ ];
+
+ protected $message = [
+ 'name.require' => '渠道名称不能为空',
+ 'name.length' => '渠道名称长度必须在1-50个字符之间',
+ 'phone.regex' => '手机号格式不正确,请输入11位数字且以1开头',
+ 'wechatId.max' => '微信号长度不能超过50个字符',
+ 'remarks.max' => '备注信息长度不能超过200个字符',
+ ];
+
+ protected $scene = [
+ 'create' => ['name', 'phone', 'wechatId', 'remarks'],
+ ];
+}
+
diff --git a/application/cunkebao/validate/Task.php b/application/cunkebao/validate/Task.php
new file mode 100644
index 0000000..b64f847
--- /dev/null
+++ b/application/cunkebao/validate/Task.php
@@ -0,0 +1,48 @@
+ 'require|max:100',
+ 'device_id' => 'number',
+ 'scene_id' => 'number',
+ 'scene_config' => 'array',
+ 'status' => 'in:0,1,2,3',
+ 'priority' => 'between:1,10',
+ 'created_by' => 'number'
+ ];
+
+ /**
+ * 错误信息
+ * @var array
+ */
+ protected $message = [
+ 'name.require' => '任务名称不能为空',
+ 'name.max' => '任务名称不能超过100个字符',
+ 'device_id.number' => '设备ID必须是数字',
+ 'scene_id.number' => '场景ID必须是数字',
+ 'scene_config.array'=> '场景配置必须是数组',
+ 'status.in' => '状态值无效',
+ 'priority.between' => '优先级必须在1到10之间',
+ 'created_by.number' => '创建者ID必须是数字'
+ ];
+
+ /**
+ * 验证场景
+ * @var array
+ */
+ protected $scene = [
+ 'create' => ['name', 'device_id', 'scene_id', 'scene_config', 'status', 'priority', 'created_by'],
+ 'update' => ['name', 'device_id', 'scene_id', 'scene_config', 'status', 'priority']
+ ];
+}
\ No newline at end of file
diff --git a/application/cunkebao/validate/Traffic.php b/application/cunkebao/validate/Traffic.php
new file mode 100644
index 0000000..2c51c62
--- /dev/null
+++ b/application/cunkebao/validate/Traffic.php
@@ -0,0 +1,51 @@
+ 'require|mobile',
+ 'gender' => 'in:0,1,2',
+ 'age' => 'number|between:0,120',
+ 'tags' => 'max:255',
+ 'province' => 'max:50',
+ 'city' => 'max:50',
+ 'source_channel' => 'max:50',
+ 'source_detail' => 'array'
+ ];
+
+ /**
+ * 错误信息
+ * @var array
+ */
+ protected $message = [
+ 'mobile.require' => '手机号不能为空',
+ 'mobile.mobile' => '手机号格式不正确',
+ 'gender.in' => '性别值无效',
+ 'age.number' => '年龄必须是数字',
+ 'age.between' => '年龄必须在0到120之间',
+ 'tags.max' => '标签不能超过255个字符',
+ 'province.max' => '省份不能超过50个字符',
+ 'city.max' => '城市不能超过50个字符',
+ 'source_channel.max' => '来源渠道不能超过50个字符',
+ 'source_detail.array'=> '来源详情必须是数组'
+ ];
+
+ /**
+ * 验证场景
+ * @var array
+ */
+ protected $scene = [
+ 'create' => ['mobile', 'gender', 'age', 'tags', 'province', 'city', 'source_channel', 'source_detail'],
+ 'update' => ['gender', 'age', 'tags', 'province', 'city']
+ ];
+}
\ No newline at end of file
diff --git a/application/cunkebao/validate/Workbench.php b/application/cunkebao/validate/Workbench.php
new file mode 100644
index 0000000..7225c5a
--- /dev/null
+++ b/application/cunkebao/validate/Workbench.php
@@ -0,0 +1,386 @@
+ 'require|max:100',
+ 'type' => 'require|in:1,2,3,4,5,6',
+ //'autoStart' => 'require|boolean',
+ // 自动点赞特有参数
+ 'interval' => 'requireIf:type,1|number|min:1',
+ 'maxLikes' => 'requireIf:type,1|number|min:1',
+ 'startTime' => 'requireIf:type,1|dateFormat:H:i',
+ 'endTime' => 'requireIf:type,1|dateFormat:H:i',
+ 'contentTypes' => 'requireIf:type,1|array|contentTypeEnum:text,image,video',
+ //'targetGroups' => 'requireIf:type,1|array',
+ // 朋友圈同步特有参数
+ //'syncInterval' => 'requireIf:type,2|number|min:1',
+ 'syncCount' => 'requireIf:type,2|number|min:1',
+ 'syncType' => 'requireIf:type,2|in:1,2,3,4',
+ 'startTime' => 'requireIf:type,2|dateFormat:H:i',
+ 'endTime' => 'requireIf:type,2|dateFormat:H:i',
+ 'accountGroups' => 'requireIf:type,2|in:1,2',
+ 'contentGroups' => 'requireIf:type,2|array',
+ // 群消息推送特有参数
+ 'pushType' => 'requireIf:type,3|in:0,1', // 推送方式 0定时 1立即
+ 'targetType' => 'requireIf:type,3|in:1,2', // 推送目标类型:1=群推送,2=好友推送
+ 'groupPushSubType' => 'checkGroupPushSubType|in:1,2', // 群推送子类型:1=群群发,2=群公告(仅当targetType=1时有效)
+ 'maxPerDay' => 'requireIf:type,3|number|min:1',
+ 'pushOrder' => 'requireIf:type,3|in:1,2', // 1最早 2最新
+ 'isLoop' => 'requireIf:type,3|in:0,1',
+ 'status' => 'requireIf:type,3|in:0,1',
+ 'wechatGroups' => 'checkGroupPushTarget|array|min:1', // 当targetType=1时必填
+ 'wechatFriends' => 'checkFriendPushTarget|array', // 当targetType=2时可选(可以为空)
+ 'ownerWechatId' => 'checkFriendPushService', // 当targetType=2且未选择好友/流量池时必填
+ 'contentGroups' => 'requireIf:type,3|array|min:1',
+ // 群公告特有参数
+ 'announcementContent' => 'checkAnnouncementContent|max:5000', // 群公告内容(当groupPushSubType=2时必填)
+ 'enableAiRewrite' => 'checkEnableAiRewrite|in:0,1', // 是否启用AI智能话术改写
+ 'aiRewritePrompt' => 'checkAiRewritePrompt|max:500', // AI改写提示词(当enableAiRewrite=1时必填)
+ // 自动建群特有参数
+ 'groupNameTemplate' => 'requireIf:type,4|max:50',
+ 'maxGroupsPerDay' => 'requireIf:type,4|number|min:1',
+ 'groupSizeMin' => 'requireIf:type,4|number|min:1|max:50',
+ 'groupSizeMax' => 'requireIf:type,4|number|min:1|max:50',
+ // 流量分发特有参数
+ 'distributeType' => 'requireIf:type,5|in:1,2',
+ 'maxPerDay' => 'requireIf:type,5|number|min:1',
+ 'timeType' => 'requireIf:type,5|in:1,2',
+ 'accountGroups' => 'requireIf:type,5|array|min:1',
+ // 通用参数
+ 'deviceGroups' => 'requireIf:type,1,2,5|array',
+ 'trafficPools' => 'checkFriendPushPools',
+ ];
+
+ /**
+ * 错误信息
+ */
+ protected $message = [
+ 'name.require' => '请输入任务名称',
+ 'name.max' => '任务名称最多100个字符',
+ 'type.require' => '请选择工作台类型',
+ 'type.in' => '工作台类型错误',
+ 'autoStart.require' => '请选择是否自动启动',
+ 'autoStart.boolean' => '自动启动参数必须为布尔值',
+ // 自动点赞相关提示
+ 'interval.requireIf' => '请设置点赞间隔',
+ 'interval.number' => '点赞间隔必须为数字',
+ 'interval.min' => '点赞间隔必须大于0',
+ 'maxLikes.requireIf' => '请设置每日最大点赞数',
+ 'maxLikes.number' => '每日最大点赞数必须为数字',
+ 'maxLikes.min' => '每日最大点赞数必须大于0',
+ 'startTime.requireIf' => '请设置开始时间',
+ 'startTime.dateFormat' => '开始时间格式错误',
+ 'endTime.requireIf' => '请设置结束时间',
+ 'endTime.dateFormat' => '结束时间格式错误',
+ 'contentTypes.requireIf' => '请选择点赞内容类型',
+ 'contentTypes.array' => '点赞内容类型必须是数组',
+ 'contentTypes.contentTypeEnum' => '点赞内容类型只能是text、image、video',
+ // 朋友圈同步相关提示
+ /* 'syncInterval.requireIf' => '请设置同步间隔',
+ 'syncInterval.number' => '同步间隔必须为数字',
+ 'syncInterval.min' => '同步间隔必须大于0',*/
+ 'syncCount.requireIf' => '请设置同步数量',
+ 'syncCount.number' => '同步数量必须为数字',
+ 'syncCount.min' => '同步数量必须大于0',
+ 'syncType.requireIf' => '请选择同步类型',
+ 'syncType.in' => '同步类型错误',
+ 'startTime.requireIf' => '请设置发布开始时间',
+ 'startTime.dateFormat' => '发布开始时间格式错误',
+ 'endTime.requireIf' => '请设置发布结束时间',
+ 'endTime.dateFormat' => '发布结束时间格式错误',
+ 'accountGroups.requireIf' => '请选择账号类型',
+ 'accountGroups.in' => '账号类型错误',
+ 'contentGroups.requireIf' => '请选择内容库',
+ 'contentGroups.array' => '内容库格式错误',
+ // 群消息推送相关提示
+ 'pushType.requireIf' => '请选择推送方式',
+ 'startTime.requireIf' => '请设置推送开始时间',
+ 'startTime.dateFormat' => '推送开始时间格式错误',
+ 'endTime.requireIf' => '请设置推送结束时间',
+ 'endTime.dateFormat' => '推送结束时间格式错误',
+ 'maxPerDay.requireIf' => '请设置每日最大推送数',
+ 'maxPerDay.number' => '每日最大推送数必须为数字',
+ 'maxPerDay.min' => '每日最大推送数必须大于0',
+ 'pushOrder.requireIf' => '请选择推送顺序',
+ 'pushOrder.in' => '推送顺序错误',
+ 'isLoop.requireIf' => '请选择是否循环推送',
+ 'isLoop.in' => '循环推送参数错误',
+ 'targetType.requireIf' => '请选择推送目标类型',
+ 'targetType.in' => '推送目标类型错误,只能选择群推送或好友推送',
+ 'wechatGroups.requireIf' => '请选择推送群组',
+ 'wechatGroups.checkGroupPushTarget' => '群推送时必须选择推送群组',
+ 'wechatGroups.array' => '推送群组格式错误',
+ 'wechatGroups.min' => '至少选择一个推送群组',
+ 'groupPushSubType.checkGroupPushSubType' => '群推送子类型错误',
+ 'groupPushSubType.in' => '群推送子类型只能是群群发或群公告',
+ 'announcementContent.checkAnnouncementContent' => '群公告必须输入公告内容',
+ 'announcementContent.max' => '公告内容最多5000个字符',
+ 'enableAiRewrite.checkEnableAiRewrite' => 'AI智能话术改写参数错误',
+ 'enableAiRewrite.in' => 'AI智能话术改写参数只能是0或1',
+ 'aiRewritePrompt.checkAiRewritePrompt' => '启用AI智能话术改写时,必须输入改写提示词',
+ 'aiRewritePrompt.max' => '改写提示词最多500个字符',
+ 'wechatFriends.requireIf' => '请选择推送好友',
+ 'wechatFriends.checkFriendPushTarget' => '好友推送时必须选择推送好友',
+ 'wechatFriends.array' => '推送好友格式错误',
+ 'deviceGroups.requireIf' => '请选择设备',
+ 'deviceGroups.array' => '设备格式错误',
+ 'ownerWechatId.checkFriendPushService' => '好友推送需选择客服或提供好友/流量池',
+ // 自动建群相关提示
+ 'groupNameTemplate.requireIf' => '请设置群名称前缀',
+ 'groupNameTemplate.max' => '群名称前缀最多50个字符',
+ 'maxGroupsPerDay.requireIf' => '请设置最大建群数量',
+ 'maxGroupsPerDay.number' => '最大建群数量必须为数字',
+ 'maxGroupsPerDay.min' => '最大建群数量必须大于0',
+ 'groupSizeMin.requireIf' => '请设置每个群的人数',
+ 'groupSizeMin.number' => '每个群的人数必须为数字',
+ 'groupSizeMin.min' => '每个群的人数必须大于0',
+ 'groupSizeMin.max' => '每个群的人数最大50人',
+ 'groupSizeMax.requireIf' => '请设置每个群的人数',
+ 'groupSizeMax.number' => '每个群的人数必须为数字',
+ 'groupSizeMax.min' => '每个群的人数必须大于0',
+ 'groupSizeMax.max' => '每个群的人数最大50人',
+ // 流量分发相关提示
+ 'distributeType.requireIf' => '请选择流量分发类型',
+ 'distributeType.in' => '流量分发类型错误',
+ 'maxPerDay.requireIf' => '请设置每日最大流量',
+ 'maxPerDay.number' => '每日最大流量必须为数字',
+ 'maxPerDay.min' => '每日最大流量必须大于0',
+ 'timeType.requireIf' => '请选择时间类型',
+
+ // 通用提示
+ 'deviceGroups.require' => '请选择设备',
+ 'deviceGroups.array' => '设备格式错误',
+ 'targetGroups.require' => '请选择目标用户组',
+ 'targetGroups.array' => '目标用户组格式错误',
+ 'accountGroups.requireIf' => '流量分发时必须选择分发账号',
+ 'accountGroups.array' => '分发账号格式错误',
+ 'accountGroups.min' => '至少选择一个分发账号',
+ 'trafficPools.checkFriendPushPools' => '好友推送时请选择好友或流量池',
+ ];
+
+ /**
+ * 验证场景
+ */
+ protected $scene = [
+ 'create' => ['name', 'type', 'autoStart', 'deviceGroups', 'targetGroups',
+ 'interval', 'maxLikes', 'startTime', 'endTime', 'contentTypes',
+ 'syncCount', 'syncType', 'accountGroups',
+ 'pushType', 'targetType', 'groupPushSubType', 'startTime', 'endTime', 'maxPerDay', 'pushOrder', 'isLoop', 'status', 'wechatGroups', 'wechatFriends', 'trafficPools', 'ownerWechatId', 'contentGroups',
+ 'announcementContent', 'enableAiRewrite', 'aiRewritePrompt',
+ 'groupNameTemplate', 'maxGroupsPerDay', 'groupSizeMin', 'groupSizeMax',
+ 'distributeType', 'timeType', 'accountGroups',
+ ],
+ 'update_status' => ['id', 'status'],
+ 'update' => ['name', 'type', 'autoStart', 'deviceGroups', 'targetGroups',
+ 'interval', 'maxLikes', 'startTime', 'endTime', 'contentTypes',
+ 'syncCount', 'syncType', 'accountGroups',
+ 'pushType', 'targetType', 'groupPushSubType', 'startTime', 'endTime', 'maxPerDay', 'pushOrder', 'isLoop', 'status', 'wechatGroups', 'wechatFriends', 'trafficPools', 'ownerWechatId', 'contentGroups',
+ 'announcementContent', 'enableAiRewrite', 'aiRewritePrompt',
+ 'groupNameTemplate', 'maxGroupsPerDay', 'groupSizeMin', 'groupSizeMax',
+ 'distributeType', 'timeType', 'accountGroups',
+ ]
+ ];
+
+ /**
+ * 自定义验证规则
+ */
+ protected function contentTypeEnum($value, $rule, $data)
+ {
+ $allowTypes = explode(',', $rule);
+ foreach ($value as $type) {
+ if (!in_array($type, $allowTypes)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 验证群推送目标(当targetType=1时,wechatGroups必填)
+ */
+ protected function checkGroupPushTarget($value, $rule, $data)
+ {
+ // 如果是群消息推送类型
+ if (isset($data['type']) && $data['type'] == self::TYPE_GROUP_PUSH) {
+ // 如果targetType=1(群推送),则wechatGroups必填
+ $targetType = isset($data['targetType']) ? intval($data['targetType']) : 1; // 默认1
+ if ($targetType == 1) {
+ // 检查值是否存在且有效
+ if (!isset($value) || $value === null || $value === '') {
+ return false;
+ }
+ if (!is_array($value) || count($value) < 1) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 验证好友推送目标(当targetType=2时,wechatFriends可选,可以为空)
+ */
+ protected function checkFriendPushTarget($value, $rule, $data)
+ {
+ // 如果是群消息推送类型
+ if (isset($data['type']) && $data['type'] == self::TYPE_GROUP_PUSH) {
+ // 如果targetType=2(好友推送),wechatFriends可以为空数组
+ $targetType = isset($data['targetType']) ? intval($data['targetType']) : 1; // 默认1
+ if ($targetType == 2) {
+ // 如果提供了值,则必须是数组
+ if (isset($value) && $value !== null && $value !== '') {
+ if (!is_array($value)) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 验证好友推送时设备必填(当targetType=2时,deviceGroups必填)
+ */
+ protected function checkFriendPushService($value, $rule, $data)
+ {
+ if (isset($data['type']) && $data['type'] == self::TYPE_GROUP_PUSH) {
+ $targetType = isset($data['targetType']) ? intval($data['targetType']) : 1; // 默认1
+ if ($targetType == 2) {
+ if ($value !== null && $value !== '' && !is_array($value)) {
+ return false;
+ }
+
+ $hasFriends = isset($data['wechatFriends']) && is_array($data['wechatFriends']) && count($data['wechatFriends']) > 0;
+ $hasPools = isset($data['trafficPools']) && is_array($data['trafficPools']) && count($data['trafficPools']) > 0;
+ $hasServices = is_array($value) && count(array_filter($value, function ($item) {
+ if (is_array($item)) {
+ return !empty($item['ownerWechatId'] ?? $item['wechatId'] ?? $item['id']);
+ }
+ return $item !== null && $item !== '';
+ })) > 0;
+
+ if (!$hasFriends && !$hasPools && !$hasServices) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 验证好友推送时是否选择好友或流量池(至少其一)
+ */
+ protected function checkFriendPushPools($value, $rule, $data)
+ {
+ if (isset($data['type']) && $data['type'] == self::TYPE_GROUP_PUSH) {
+ $targetType = isset($data['targetType']) ? intval($data['targetType']) : 1; // 默认1
+ if ($targetType == 2) {
+ $hasFriends = isset($data['wechatFriends']) && !empty($data['wechatFriends']);
+ $hasPools = isset($value) && $value !== null && $value !== '' && is_array($value) && count($value) > 0;
+ if (!$hasFriends && !$hasPools) {
+ return false;
+ }
+ if (isset($value) && $value !== null && $value !== '') {
+ if (!is_array($value)) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 验证群推送子类型(当targetType=1时,groupPushSubType必填且只能是1或2)
+ */
+ protected function checkGroupPushSubType($value, $rule, $data)
+ {
+ // 如果是群消息推送类型
+ if (isset($data['type']) && $data['type'] == self::TYPE_GROUP_PUSH) {
+ // 如果targetType=1(群推送),则groupPushSubType必填
+ $targetType = isset($data['targetType']) ? intval($data['targetType']) : 1; // 默认1
+ if ($targetType == 1) {
+ // 检查值是否存在且有效
+ if (!isset($value) || !in_array(intval($value), [1, 2])) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 验证群公告内容(当groupPushSubType=2时,announcementContent必填)
+ */
+ protected function checkAnnouncementContent($value, $rule, $data)
+ {
+ // 如果是群消息推送类型
+ if (isset($data['type']) && $data['type'] == self::TYPE_GROUP_PUSH) {
+ // 如果targetType=1且groupPushSubType=2(群公告),则announcementContent必填
+ $targetType = isset($data['targetType']) ? intval($data['targetType']) : 1; // 默认1
+ $groupPushSubType = isset($data['groupPushSubType']) ? intval($data['groupPushSubType']) : 1; // 默认1
+ if ($targetType == 1 && $groupPushSubType == 2) {
+ // 检查值是否存在且有效
+ if (!isset($value) || $value === null || trim($value) === '') {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 验证AI智能话术改写(当enableAiRewrite=1时,aiRewritePrompt必填)
+ */
+ protected function checkEnableAiRewrite($value, $rule, $data)
+ {
+ // 如果是群消息推送类型且是群公告
+ if (isset($data['type']) && $data['type'] == self::TYPE_GROUP_PUSH) {
+ $targetType = isset($data['targetType']) ? intval($data['targetType']) : 1; // 默认1
+ $groupPushSubType = isset($data['groupPushSubType']) ? intval($data['groupPushSubType']) : 1; // 默认1
+ if ($targetType == 1 && $groupPushSubType == 2) {
+ // 检查值是否存在且有效
+ if (!isset($value) || !in_array(intval($value), [0, 1])) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 验证AI改写提示词(当enableAiRewrite=1时,aiRewritePrompt必填)
+ */
+ protected function checkAiRewritePrompt($value, $rule, $data)
+ {
+ // 如果是群消息推送类型且是群公告
+ if (isset($data['type']) && $data['type'] == self::TYPE_GROUP_PUSH) {
+ $targetType = isset($data['targetType']) ? intval($data['targetType']) : 1; // 默认1
+ $groupPushSubType = isset($data['groupPushSubType']) ? intval($data['groupPushSubType']) : 1; // 默认1
+ $enableAiRewrite = isset($data['enableAiRewrite']) ? intval($data['enableAiRewrite']) : 0; // 默认0
+ if ($targetType == 1 && $groupPushSubType == 2 && $enableAiRewrite == 1) {
+ // 如果启用AI改写,提示词必填
+ if (!isset($value) || $value === null || trim($value) === '') {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/application/http/middleware/Jwt.php b/application/http/middleware/Jwt.php
new file mode 100644
index 0000000..4f2fb7d
--- /dev/null
+++ b/application/http/middleware/Jwt.php
@@ -0,0 +1,49 @@
+ 401,
+ 'msg' => '未授权访问,缺少有效的身份凭证',
+ 'data' => null
+ ])->header(['Content-Type' => 'application/json; charset=utf-8']);
+ }
+
+ $payload = JwtUtil::verifyToken($token);
+ if (!$payload) {
+ return json([
+ 'code' => 401,
+ 'msg' => '授权已过期或无效',
+ 'data' => null
+ ])->header(['Content-Type' => 'application/json; charset=utf-8']);
+ }
+
+ // 将用户信息附加到请求中
+ $request->userInfo = $payload;
+
+ // 写入日志
+ Log::info('JWT认证通过', ['user_id' => $payload['id'] ?? 0, 'username' => $payload['username'] ?? '']);
+
+ return $next($request);
+ }
+}
diff --git a/application/job/AccountListJob.php b/application/job/AccountListJob.php
new file mode 100644
index 0000000..6d76ab3
--- /dev/null
+++ b/application/job/AccountListJob.php
@@ -0,0 +1,115 @@
+processAccountList($data, $job->attempts())) {
+ $job->delete();
+ Log::info('公司账号列表任务执行成功,页码:' . $data['pageIndex']);
+ } else {
+ if ($job->attempts() > 3) {
+ // 超过重试次数,删除任务
+ Log::error('公司账号列表任务执行失败,已超过重试次数,页码:' . $data['pageIndex']);
+ $job->delete();
+ } else {
+ // 任务失败,重新放回队列
+ Log::warning('公司账号列表任务执行失败,重试次数:' . $job->attempts() . ',页码:' . $data['pageIndex']);
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ }
+ } catch (\Exception $e) {
+ // 出现异常,记录日志
+ Log::error('公司账号列表任务异常:' . $e->getMessage());
+ if ($job->attempts() > 3) {
+ $job->delete();
+ } else {
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ }
+ }
+
+ /**
+ * 处理公司账号列表获取
+ * @param array $data 任务数据
+ * @param int $attempts 重试次数
+ * @return bool
+ */
+ protected function processAccountList($data, $attempts)
+ {
+ // 获取参数
+ $pageIndex = isset($data['pageIndex']) ? $data['pageIndex'] : 0;
+ $pageSize = isset($data['pageSize']) ? $data['pageSize'] : 100;
+
+ Log::info('开始获取公司账号列表,页码:' . $pageIndex . ',页大小:' . $pageSize);
+
+ // 实例化控制器
+ $accountController = new AccountController();
+
+ // 构建请求参数
+ $params = [
+ 'pageIndex' => $pageIndex,
+ 'pageSize' => $pageSize
+ ];
+
+ // 设置请求信息
+ $request = request();
+ $request->withGet($params);
+
+ // 调用公司账号列表获取方法
+ $result = $accountController->getlist(['pageIndex' => $pageIndex,'pageSize' => $pageSize],true);
+ $response = json_decode($result,true);
+
+
+ // 判断是否成功
+ if ($response['code'] == 200) {
+ $data = $response['data'];
+
+ // 判断是否有下一页
+ if (!empty($data) && count($data['results']) > 0) {
+ // 有下一页,将下一页任务添加到队列
+ $nextPageIndex = $pageIndex + 1;
+ $this->addNextPageToQueue($nextPageIndex, $pageSize);
+ Log::info('添加下一页任务到队列,页码:' . $nextPageIndex);
+ }
+
+ return true;
+ } else {
+ $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误';
+ Log::error('获取公司账号列表失败:' . $errorMsg);
+ return false;
+ }
+ }
+
+ /**
+ * 添加下一页任务到队列
+ * @param int $pageIndex 页码
+ * @param int $pageSize 每页大小
+ */
+ protected function addNextPageToQueue($pageIndex, $pageSize)
+ {
+ $data = [
+ 'pageIndex' => $pageIndex,
+ 'pageSize' => $pageSize
+ ];
+
+ // 添加到队列,设置任务名为 account_list
+ Queue::push(self::class, $data, 'account_list');
+ }
+}
\ No newline at end of file
diff --git a/application/job/AllotChatroomJob.php b/application/job/AllotChatroomJob.php
new file mode 100644
index 0000000..96c01b1
--- /dev/null
+++ b/application/job/AllotChatroomJob.php
@@ -0,0 +1,96 @@
+ $toAccountId,
+ 'wechatAccountKeyword' => $wechatAccountKeyword,
+ 'isDeleted' => $isDeleted,
+ 'jobId' => $jobId,
+ 'queueLockKey' => $queueLockKey
+ ]));
+
+ // 如果没有提供队列锁键,生成一个
+ if (empty($queueLockKey)) {
+ $queueLockKey = "queue_lock:allot_chatroom:{$wechatAccountKeyword}";
+ }
+
+ // 实例化控制器
+ $automaticAssignController = new AutomaticAssign();
+
+ // 调用微信群聊自动分配方法
+ $result = $automaticAssignController->autoAllotWechatChatroom($toAccountId, $wechatAccountKeyword, $isDeleted);
+ $response = json_decode($result, true);
+
+ // 判断是否成功
+ if ($response['code'] == 1) {
+ Log::info("微信群聊自动分配成功: toAccountId={$toAccountId}, wechatAccountKeyword={$wechatAccountKeyword}");
+
+ // 释放队列锁
+ Cache::rm($queueLockKey);
+ Log::info("任务完成,释放队列锁: {$queueLockKey}");
+
+ $job->delete();
+ return true;
+ } else {
+ // API调用出错,记录错误
+ $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误';
+ Log::error("微信群聊自动分配失败: {$errorMsg}");
+
+ if ($job->attempts() > 3) {
+ // 超过重试次数,删除任务并释放队列锁
+ Cache::rm($queueLockKey);
+ Log::info("由于错误多次尝试失败,释放队列锁: {$queueLockKey}");
+ $job->delete();
+ } else {
+ // 任务失败,重新放回队列
+ Log::warning("微信群聊自动分配任务执行失败,重试次数: " . $job->attempts());
+ $job->release(10); // 延迟10秒后重试
+ }
+
+ return false;
+ }
+ } catch (\Exception $e) {
+ // 出现异常,记录错误并释放队列锁
+ Log::error('微信群聊自动分配任务异常: ' . $e->getMessage());
+
+ if (!empty($queueLockKey)) {
+ Cache::rm($queueLockKey);
+ Log::info("由于异常释放队列锁: {$queueLockKey}");
+ }
+
+ if ($job->attempts() > 3) {
+ $job->delete();
+ } else {
+ $job->release(10);
+ }
+
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/job/AllotFriendJob.php b/application/job/AllotFriendJob.php
new file mode 100644
index 0000000..baeeb6e
--- /dev/null
+++ b/application/job/AllotFriendJob.php
@@ -0,0 +1,96 @@
+ $toAccountId,
+ 'wechatAccountKeyword' => $wechatAccountKeyword,
+ 'isDeleted' => $isDeleted,
+ 'jobId' => $jobId,
+ 'queueLockKey' => $queueLockKey
+ ]));
+
+ // 如果没有提供队列锁键,生成一个
+ if (empty($queueLockKey)) {
+ $queueLockKey = "queue_lock:allot_friends:{$wechatAccountKeyword}";
+ }
+
+ // 实例化控制器
+ $automaticAssignController = new AutomaticAssign();
+
+ // 调用微信好友自动分配方法
+ $result = $automaticAssignController->autoAllotWechatFriend($toAccountId, $wechatAccountKeyword, $isDeleted);
+ $response = json_decode($result, true);
+
+ // 判断是否成功
+ if ($response['code'] == 1) {
+ Log::info("微信好友自动分配成功: toAccountId={$toAccountId}, wechatAccountKeyword={$wechatAccountKeyword}");
+
+ // 释放队列锁
+ Cache::rm($queueLockKey);
+ Log::info("任务完成,释放队列锁: {$queueLockKey}");
+
+ $job->delete();
+ return true;
+ } else {
+ // API调用出错,记录错误
+ $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误';
+ Log::error("微信好友自动分配失败: {$errorMsg}");
+
+ if ($job->attempts() > 3) {
+ // 超过重试次数,删除任务并释放队列锁
+ Cache::rm($queueLockKey);
+ Log::info("由于错误多次尝试失败,释放队列锁: {$queueLockKey}");
+ $job->delete();
+ } else {
+ // 任务失败,重新放回队列
+ Log::warning("微信好友自动分配任务执行失败,重试次数: " . $job->attempts());
+ $job->release(10); // 延迟10秒后重试
+ }
+
+ return false;
+ }
+ } catch (\Exception $e) {
+ // 出现异常,记录错误并释放队列锁
+ Log::error('微信好友自动分配任务异常: ' . $e->getMessage());
+
+ if (!empty($queueLockKey)) {
+ Cache::rm($queueLockKey);
+ Log::info("由于异常释放队列锁: {$queueLockKey}");
+ }
+
+ if ($job->attempts() > 3) {
+ $job->delete();
+ } else {
+ $job->release(10);
+ }
+
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/job/AllotRuleListJob.php b/application/job/AllotRuleListJob.php
new file mode 100644
index 0000000..52a6437
--- /dev/null
+++ b/application/job/AllotRuleListJob.php
@@ -0,0 +1,75 @@
+processAllotRuleList($data, $job->attempts())) {
+ $job->delete();
+ Log::info('分配规则列表任务执行成功');
+ } else {
+ if ($job->attempts() > 3) {
+ // 超过重试次数,删除任务
+ Log::error('分配规则列表任务执行失败,已超过重试次数');
+ $job->delete();
+ } else {
+ // 任务失败,重新放回队列
+ Log::warning('分配规则列表任务执行失败,重试次数:' . $job->attempts());
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ }
+ } catch (\Exception $e) {
+ // 出现异常,记录日志
+ Log::error('分配规则列表任务异常:' . $e->getMessage());
+ if ($job->attempts() > 3) {
+ $job->delete();
+ } else {
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ }
+ }
+
+ /**
+ * 处理分配规则列表获取
+ * @param array $data 任务数据
+ * @param int $attempts 重试次数
+ * @return bool
+ */
+ protected function processAllotRuleList($data, $attempts)
+ {
+ Log::info('开始获取分配规则列表');
+
+ // 实例化控制器
+ $allotRuleController = new AllotRuleController();
+
+ // 调用分配规则列表获取方法
+ $result = $allotRuleController->getAllRules([], true);
+ $response = json_decode($result, true);
+
+ // 判断是否成功
+ if ($response['code'] == 200) {
+ Log::info('获取分配规则列表成功,共获取 ' . (isset($response['data']) ? count($response['data']) : 0) . ' 条记录');
+ return true;
+ } else {
+ $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误';
+ Log::error('获取分配规则列表失败:' . $errorMsg);
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/job/AutoCreateAllotRulesJob.php b/application/job/AutoCreateAllotRulesJob.php
new file mode 100644
index 0000000..5129eba
--- /dev/null
+++ b/application/job/AutoCreateAllotRulesJob.php
@@ -0,0 +1,75 @@
+processAutoCreateAllotRules($data, $job->attempts())) {
+ $job->delete();
+ Log::info('自动创建分配规则任务执行成功,时间:' . date('Y-m-d H:i:s'));
+ } else {
+ if ($job->attempts() > 3) {
+ // 超过重试次数,删除任务
+ Log::error('自动创建分配规则任务执行失败,已超过重试次数');
+ $job->delete();
+ } else {
+ // 任务失败,重新放回队列
+ Log::warning('自动创建分配规则任务执行失败,重试次数:' . $job->attempts());
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ }
+ } catch (\Exception $e) {
+ // 出现异常,记录日志
+ Log::error('自动创建分配规则任务异常:' . $e->getMessage());
+ if ($job->attempts() > 3) {
+ $job->delete();
+ } else {
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ }
+ }
+
+ /**
+ * 处理自动创建分配规则
+ * @param array $data 任务数据
+ * @param int $attempts 重试次数
+ * @return bool
+ */
+ protected function processAutoCreateAllotRules($data, $attempts)
+ {
+ Log::info('开始执行自动创建分配规则任务,重试次数:' . $attempts);
+
+ // 实例化控制器
+ $allotRuleController = new AllotRuleController();
+
+ // 调用自动创建分配规则方法
+ $result = $allotRuleController->autoCreateAllotRules([], true);
+ $response = json_decode($result, true);
+
+ // 判断是否成功
+ if (isset($response['code']) && $response['code'] == 200) {
+ Log::info('自动创建分配规则成功:' . json_encode($response['data']));
+ return true;
+ } else {
+ $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误';
+ Log::error('自动创建分配规则失败:' . $errorMsg);
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/job/CallRecordingListJob.php b/application/job/CallRecordingListJob.php
new file mode 100644
index 0000000..3ec8d6c
--- /dev/null
+++ b/application/job/CallRecordingListJob.php
@@ -0,0 +1,118 @@
+processCallRecordingList($data, $job->attempts())) {
+ $job->delete();
+ Log::info('通话记录列表任务执行成功,页码:' . $data['pageIndex']);
+ } else {
+ if ($job->attempts() > 3) {
+ // 超过重试次数,删除任务
+ Log::error('通话记录列表任务执行失败,已超过重试次数,页码:' . $data['pageIndex']);
+ $job->delete();
+ } else {
+ // 任务失败,重新放回队列
+ Log::warning('通话记录列表任务执行失败,重试次数:' . $job->attempts() . ',页码:' . $data['pageIndex']);
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ }
+ } catch (\Exception $e) {
+ // 出现异常,记录日志
+ Log::error('通话记录列表任务异常:' . $e->getMessage());
+ if ($job->attempts() > 3) {
+ $job->delete();
+ } else {
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ }
+ }
+
+ /**
+ * 处理通话记录列表获取
+ * @param array $data 任务数据
+ * @param int $attempts 重试次数
+ * @return bool
+ */
+ protected function processCallRecordingList($data, $attempts)
+ {
+ // 获取参数
+ $pageIndex = isset($data['pageIndex']) ? $data['pageIndex'] : 0;
+ $pageSize = isset($data['pageSize']) ? $data['pageSize'] : 100;
+
+ Log::info('开始获取通话记录列表,页码:' . $pageIndex . ',页大小:' . $pageSize);
+
+ // 实例化控制器
+ $callRecordingController = new CallRecordingController();
+
+ // 构建请求参数
+ $params = [
+ 'pageIndex' => $pageIndex,
+ 'pageSize' => $pageSize,
+ 'keyword' => '',
+ 'isCallOut' => '',
+ 'secondMin' => 0,
+ 'secondMax' => 99999,
+ 'departmentIds' => '',
+ 'from' => date('Y-m-d 00:00:00', strtotime('-100 days')),
+ 'to' => date('Y-m-d 23:59:59'),
+ 'departmentId' => ''
+ ];
+
+ // 调用通话记录列表获取方法
+ $result = $callRecordingController->getlist($params, true);
+ $response = json_decode($result, true);
+
+ // 判断是否成功
+ if ($response['code'] == 200) {
+ $data = $response['data'];
+
+ // 判断是否有下一页
+ if (!empty($data) && isset($data['results']) && count($data['results']) > 0) {
+ // 有下一页,将下一页任务添加到队列
+ $nextPageIndex = $pageIndex + 1;
+ $this->addNextPageToQueue($nextPageIndex, $pageSize);
+ Log::info('添加下一页任务到队列,页码:' . $nextPageIndex);
+ }
+
+ return true;
+ } else {
+ $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误';
+ Log::error('获取通话记录列表失败:' . $errorMsg);
+ return false;
+ }
+ }
+
+ /**
+ * 添加下一页任务到队列
+ * @param int $pageIndex 页码
+ * @param int $pageSize 每页大小
+ */
+ protected function addNextPageToQueue($pageIndex, $pageSize)
+ {
+ $data = [
+ 'pageIndex' => $pageIndex,
+ 'pageSize' => $pageSize
+ ];
+
+ // 添加到队列,设置任务名为 call_recording_list
+ Queue::push(self::class, $data, 'call_recording_list');
+ }
+}
diff --git a/application/job/ContentCollectJob.php b/application/job/ContentCollectJob.php
new file mode 100644
index 0000000..fcdde95
--- /dev/null
+++ b/application/job/ContentCollectJob.php
@@ -0,0 +1,90 @@
+processContentCollect($data, $job->attempts())) {
+ $job->delete();
+ // 去除成功日志,减少日志空间消耗
+ } else {
+ if ($job->attempts() > 3) {
+ // 超过重试次数,删除任务
+ Log::error('内容采集任务执行失败,已超过重试次数,内容库ID:' . $data['libraryId']);
+ $job->delete();
+ } else {
+ // 任务失败,重新放回队列
+ Log::warning('内容采集任务执行失败,重试次数:' . $job->attempts() . ',内容库ID:' . $data['libraryId']);
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ }
+ } catch (\Exception $e) {
+ // 出现异常,记录日志
+ Log::error('内容采集任务异常:' . $e->getMessage());
+ if ($job->attempts() > 3) {
+ $job->delete();
+ } else {
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ }
+ }
+
+ /**
+ * 处理内容采集
+ * @param array $data 任务数据
+ * @param int $attempts 重试次数
+ * @return bool
+ */
+ protected function processContentCollect($data, $attempts)
+ {
+ try {
+ $controller = new ContentLibraryController();
+ $result = $controller->collectMoments();
+ $response = json_decode($result, true);
+
+ if ($response['code'] == 200) {
+ // 记录详细的采集结果
+ if (!empty($response['data'])) {
+ foreach ($response['data'] as $result) {
+ if ($result['status'] == 'success') {
+ Log::info(sprintf(
+ '内容库[%s]采集成功: %s',
+ $result['library_name'],
+ $result['message']
+ ));
+ } else {
+ Log::warning(sprintf(
+ '内容库[%s]采集失败: %s',
+ $result['library_name'],
+ $result['message']
+ ));
+ }
+ }
+ }
+ return true;
+ } else {
+ Log::error('内容采集失败:' . ($response['msg'] ?? '未知错误'));
+ return false;
+ }
+ } catch (\Exception $e) {
+ Log::error('内容采集处理异常:' . $e->getMessage());
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/job/DepartmentListJob.php b/application/job/DepartmentListJob.php
new file mode 100644
index 0000000..852c121
--- /dev/null
+++ b/application/job/DepartmentListJob.php
@@ -0,0 +1,115 @@
+processDepartmentList($data, $job->attempts())) {
+ $job->delete();
+ Log::info('部门列表任务执行成功,页码:' . $data['pageIndex']);
+ } else {
+ if ($job->attempts() > 3) {
+ // 超过重试次数,删除任务
+ Log::error('部门列表任务执行失败,已超过重试次数,页码:' . $data['pageIndex']);
+ $job->delete();
+ } else {
+ // 任务失败,重新放回队列
+ Log::warning('部门列表任务执行失败,重试次数:' . $job->attempts() . ',页码:' . $data['pageIndex']);
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ }
+ } catch (\Exception $e) {
+ // 出现异常,记录日志
+ Log::error('部门列表任务异常:' . $e->getMessage());
+ if ($job->attempts() > 3) {
+ $job->delete();
+ } else {
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ }
+ }
+
+ /**
+ * 处理部门列表获取
+ * @param array $data 任务数据
+ * @param int $attempts 重试次数
+ * @return bool
+ */
+ protected function processDepartmentList($data, $attempts)
+ {
+ // 获取参数
+ $pageIndex = isset($data['pageIndex']) ? $data['pageIndex'] : 0;
+ $pageSize = isset($data['pageSize']) ? $data['pageSize'] : 100;
+
+ Log::info('开始获取部门列表,页码:' . $pageIndex . ',页大小:' . $pageSize);
+
+ // 实例化控制器
+ $accountController = new AccountController();
+
+ // 构建请求参数
+ $params = [
+ 'pageIndex' => $pageIndex,
+ 'pageSize' => $pageSize
+ ];
+
+ // 设置请求信息
+ $request = request();
+ $request->withGet($params);
+
+ // 调用公司账号列表获取方法
+ $result = $accountController->getDepartmentList(true);
+ $response = json_decode($result,true);
+
+
+ // 判断是否成功
+ if ($response['code'] == 200) {
+ $data = $response['data'];
+
+ // 判断是否有下一页
+ if (!empty($data) && count($data['results']) > 0) {
+ // 有下一页,将下一页任务添加到队列
+ $nextPageIndex = $pageIndex + 1;
+ $this->addNextPageToQueue($nextPageIndex, $pageSize);
+ Log::info('添加下一页任务到队列,页码:' . $nextPageIndex);
+ }
+
+ return true;
+ } else {
+ $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误';
+ Log::error('获取部门列表失败:' . $errorMsg);
+ return false;
+ }
+ }
+
+ /**
+ * 添加下一页任务到队列
+ * @param int $pageIndex 页码
+ * @param int $pageSize 每页大小
+ */
+ protected function addNextPageToQueue($pageIndex, $pageSize)
+ {
+ $data = [
+ 'pageIndex' => $pageIndex,
+ 'pageSize' => $pageSize
+ ];
+
+ // 添加到队列,设置任务名为 account_list
+ Queue::push(self::class, $data, 'department_list');
+ }
+}
\ No newline at end of file
diff --git a/application/job/DeviceListJob.php b/application/job/DeviceListJob.php
new file mode 100644
index 0000000..1dd337b
--- /dev/null
+++ b/application/job/DeviceListJob.php
@@ -0,0 +1,155 @@
+ $pageIndex,
+ 'pageSize' => $pageSize,
+ 'isDel' => $isDel,
+ 'jobId' => $jobId,
+ 'cacheKey' => $cacheKey,
+ 'queueLockKey' => $queueLockKey
+ ]));
+
+ // 如果没有提供缓存键,根据删除状态和任务ID生成一个
+ if (empty($cacheKey)) {
+ $cacheKeyPrefix = "devicePage:" . ($jobId ?: date('YmdHis') . rand(1000, 9999));
+ $cacheKeySuffix = $isDel === '' ? '' : ":{$isDel}";
+ $cacheKey = $cacheKeyPrefix . $cacheKeySuffix;
+ }
+
+ // 如果没有提供队列锁键,生成一个
+ if (empty($queueLockKey)) {
+ $queueLockKey = "queue_lock:device_list:{$isDel}";
+ }
+
+ // 实例化控制器
+ $deviceController = new DeviceController();
+
+ // 设置请求信息
+ $request = request();
+ $request->withGet([
+ 'pageIndex' => $pageIndex,
+ 'pageSize' => $pageSize
+ ]);
+
+ // 调用设备列表获取方法,传入isDel参数
+ $result = $deviceController->getlist(['pageIndex' => $pageIndex, 'pageSize' => $pageSize], true, $isDel);
+ $response = json_decode($result, true);
+
+ if ($response['code'] == 200) {
+ $data = $response['data'];
+ $dataCount = count($data['results']);
+ $totalCount = $data['total'];
+
+ Log::info("设备列表获取成功,当前页:{$pageIndex},获取数量:{$dataCount},总数量:{$totalCount}");
+
+ // 计算是否还有下一页
+ $hasNextPage = ($pageIndex + 1) * $pageSize < $totalCount;
+
+ if ($hasNextPage) {
+ // 缓存页码信息,设置有效期1天
+ $nextPageIndex = $pageIndex + 1;
+ Cache::set($cacheKey, $nextPageIndex, 600);
+ Log::info("更新缓存页码: {$nextPageIndex}, 缓存键: {$cacheKey}");
+
+ // 添加下一页任务到队列
+ $command = new DeviceListCommand();
+ $command->addToQueue($nextPageIndex, $pageSize, $isDel, $jobId, $cacheKey, $queueLockKey);
+ Log::info("已添加下一页任务到队列: 页码 {$nextPageIndex}");
+ } else {
+ // 处理完所有页面,重置页码并释放队列锁
+ Cache::set($cacheKey, 0, 600);
+ Cache::rm($queueLockKey);
+ Log::info("所有设备列表页面处理完毕,重置页码为0,释放队列锁: {$queueLockKey}");
+ }
+
+ $job->delete();
+ return true;
+ } else {
+ // API调用出错,记录错误并释放队列锁
+ $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误';
+ Log::error("设备列表获取失败: " . $errorMsg);
+
+ if ($job->attempts() > 3) {
+ // 超过重试次数,删除任务并释放队列锁
+ Cache::rm($queueLockKey);
+ Log::info("由于错误释放队列锁: {$queueLockKey}");
+ $job->delete();
+ } else {
+ // 任务失败,重新放回队列
+ Log::warning('设备列表任务执行失败,重试次数:' . $job->attempts() . ',页码:' . $pageIndex);
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+
+ return false;
+ }
+ } catch (\Exception $e) {
+ // 出现异常,记录错误并释放队列锁
+ Log::error('设备列表任务处理异常: ' . $e->getMessage());
+
+ if (!empty($queueLockKey)) {
+ Cache::rm($queueLockKey);
+ Log::info("由于异常释放队列锁: {$queueLockKey}");
+ }
+
+ if ($job->attempts() > 3) {
+ $job->delete();
+ } else {
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+
+ return false;
+ }
+ }
+
+ /**
+ * 获取删除状态的文本描述
+ * @param string $isDel 删除状态
+ * @return string 删除状态文本
+ */
+ protected function getDeleteTypeText($isDel)
+ {
+ switch ($isDel) {
+ case '0':
+ case 0:
+ return '未删除(unDeleted)';
+ case '1':
+ case 1:
+ return '已删除(deleted)';
+ case '2':
+ case 2:
+ return '已停用(deletedAndStop)';
+ default:
+ return '全部';
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/job/FriendTaskJob.php b/application/job/FriendTaskJob.php
new file mode 100644
index 0000000..03ec65b
--- /dev/null
+++ b/application/job/FriendTaskJob.php
@@ -0,0 +1,123 @@
+processFriendTask($data, $job->attempts())) {
+ $job->delete();
+ Log::info('添加好友任务执行成功,页码:' . $data['pageIndex']);
+ } else {
+ if ($job->attempts() > 3) {
+ // 超过重试次数,删除任务
+ Log::error('添加好友任务执行失败,已超过重试次数,页码:' . $data['pageIndex']);
+ $job->delete();
+ } else {
+ // 任务失败,重新放回队列
+ Log::warning('添加好友任务执行失败,重试次数:' . $job->attempts() . ',页码:' . $data['pageIndex']);
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ }
+ } catch (\Exception $e) {
+ // 出现异常,记录日志
+ Log::error('添加好友任务异常:' . $e->getMessage());
+ if ($job->attempts() > 3) {
+ $job->delete();
+ } else {
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ }
+ }
+
+ /**
+ * 处理添加好友任务获取
+ * @param array $data 任务数据
+ * @param int $attempts 重试次数
+ * @return bool
+ */
+ protected function processFriendTask($data, $attempts)
+ {
+ // 获取参数
+ $pageIndex = isset($data['pageIndex']) ? $data['pageIndex'] : 0;
+ $pageSize = isset($data['pageSize']) ? $data['pageSize'] : 100;
+
+ Log::info('开始获取添加好友任务,页码:' . $pageIndex . ',页大小:' . $pageSize);
+
+ // 实例化控制器
+ $friendTaskController = new FriendTaskController();
+
+ // 构建请求参数
+ $params = [
+ 'pageIndex' => $pageIndex,
+ 'pageSize' => $pageSize
+ ];
+
+ // 设置请求信息
+ $request = request();
+ $request->withGet($params);
+
+ // 调用添加好友任务获取方法
+ $result = $friendTaskController->getlist($pageIndex, $pageSize, true);
+ $response = json_decode($result, true);
+
+ // 判断是否成功
+ if ($response['code'] == 200) {
+ $data = $response['data'];
+
+ // 判断是否有下一页
+ if (!empty($data) && count($data['results']) > 0 && $pageIndex < 2) {
+ // 更新缓存中的页码,设置10分钟过期
+ Cache::set('friendTaskPage', $pageIndex + 1, 600);
+ Log::info('更新缓存,下一页页码:' . ($pageIndex + 1) . ',缓存时间:10分钟');
+
+ // 有下一页,将下一页任务添加到队列
+ $nextPageIndex = $pageIndex + 1;
+ $this->addNextPageToQueue($nextPageIndex, $pageSize);
+ Log::info('添加下一页任务到队列,页码:' . $nextPageIndex);
+ } else {
+ // 没有下一页,重置缓存,设置10分钟过期
+ Cache::set('friendTaskPage', 0, 600);
+ Log::info('获取完成,重置缓存,缓存时间:10分钟');
+ }
+
+ return true;
+ } else {
+ $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误';
+ Log::error('获取添加好友任务失败:' . $errorMsg);
+ return false;
+ }
+ }
+
+ /**
+ * 添加下一页任务到队列
+ * @param int $pageIndex 页码
+ * @param int $pageSize 每页大小
+ */
+ protected function addNextPageToQueue($pageIndex, $pageSize)
+ {
+ $data = [
+ 'pageIndex' => $pageIndex,
+ 'pageSize' => $pageSize
+ ];
+
+ // 添加到队列,设置任务名为 friend_task
+ Queue::push(self::class, $data, 'friend_task');
+ }
+}
\ No newline at end of file
diff --git a/application/job/GroupFriendsJob.php b/application/job/GroupFriendsJob.php
new file mode 100644
index 0000000..54cf56e
--- /dev/null
+++ b/application/job/GroupFriendsJob.php
@@ -0,0 +1,145 @@
+processGroupFriendsList($data, $job->attempts())) {
+ $job->delete();
+ Log::info('微信群好友列表任务执行成功,页码:' . $data['pageIndex']);
+ } else {
+ if ($job->attempts() > 3) {
+ // 超过重试次数,删除任务
+ Log::error('微信群好友列表任务执行失败,已超过重试次数,页码:' . $data['pageIndex']);
+ $job->delete();
+ } else {
+ // 任务失败,重新放回队列
+ Log::warning('微信群好友列表任务执行失败,重试次数:' . $job->attempts() . ',页码:' . $data['pageIndex']);
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ }
+ } catch (\Exception $e) {
+ // 出现异常,记录日志
+ Log::error('微信群好友列表任务异常:' . $e->getMessage());
+ if ($job->attempts() > 3) {
+ $job->delete();
+ } else {
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ }
+ }
+
+ /**
+ * 处理微信群好友列表获取
+ * @param array $data 任务数据
+ * @param int $attempts 重试次数
+ * @return bool
+ */
+ protected function processGroupFriendsList($data, $attempts)
+ {
+ // 获取参数
+ $pageIndex = isset($data['pageIndex']) ? $data['pageIndex'] : 0;
+ $pageSize = isset($data['pageSize']) ? $data['pageSize'] : 100;
+
+ Log::info('开始获取微信群好友列表,页码:' . $pageIndex . ',页大小:' . $pageSize);
+
+ try {
+ // 从数据库获取未删除的群聊列表
+ $chatrooms = WechatChatroomModel::where('isDeleted', 0)
+ ->page($pageIndex + 1, $pageSize)
+ ->order('id', 'desc')
+ ->select();
+
+ if (empty($chatrooms)) {
+ Log::info('未找到需要处理的群聊数据,页码:' . $pageIndex);
+ return true;
+ }
+
+
+ // 实例化控制器
+ $wechatChatroomController = new WechatChatroomController();
+
+ // 遍历每个群聊,获取其成员
+ foreach ($chatrooms as $chatroom) {
+ try {
+ // 调用获取群成员列表方法
+ $result = $wechatChatroomController->listChatroomMember($chatroom['id'],$chatroom['chatroomId'],true);
+ $response = is_string($result) ? json_decode($result, true) : $result;
+
+ // 判断是否成功
+ if (is_array($response) && isset($response['code']) && $response['code'] == 200) {
+ //Log::info('成功获取群 ' . $chatroom['chatroomId'] . ' 的成员列表');
+ } else {
+ $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误';
+ Log::error('获取群 ' . $chatroom['chatroomId'] . ' 的成员列表失败:' . $errorMsg);
+ }
+ } catch (\Exception $e) {
+ Log::error('获取群 ' . $chatroom['chatroomId'] . ' 的成员列表异常:' . $e->getMessage());
+ }
+
+ }
+
+ //Log::info('群成员获取完成,成功:' . $successCount . ',失败:' . $failCount);
+
+ // 计算总数量
+ $totalCount = WechatChatroomModel::where('isDeleted', 0)->count();
+ $processedCount = ($pageIndex + 1) * $pageSize;
+
+ // 判断是否有下一页
+ if ($processedCount < $totalCount) {
+ // 更新缓存中的页码,设置一天过期
+ Cache::set('groupFriendsPage', $pageIndex + 1, 600);
+ //Log::info('更新缓存,下一页页码:' . ($pageIndex + 1) . ',缓存时间:1天');
+
+ // 有下一页,将下一页任务添加到队列
+ $nextPageIndex = $pageIndex + 1;
+ $this->addNextPageToQueue($nextPageIndex, $pageSize);
+ Log::info('添加下一页任务到队列,页码:' . $nextPageIndex);
+ } else {
+ // 没有下一页,重置缓存,设置一天过期
+ Cache::set('groupFriendsPage', 0, 600);
+ Log::info('获取完成,重置缓存,缓存时间:1天');
+ }
+
+ return true;
+ } catch (\Exception $e) {
+ Log::error('获取微信群好友列表处理失败:' . $e->getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * 添加下一页任务到队列
+ * @param int $pageIndex 页码
+ * @param int $pageSize 每页大小
+ */
+ protected function addNextPageToQueue($pageIndex, $pageSize)
+ {
+ $data = [
+ 'pageIndex' => $pageIndex,
+ 'pageSize' => $pageSize
+ ];
+
+ // 添加到队列,设置任务名为 group_friends
+ Queue::push(self::class, $data, 'group_friends');
+ }
+}
\ No newline at end of file
diff --git a/application/job/MessageChatroomListJob.php b/application/job/MessageChatroomListJob.php
new file mode 100644
index 0000000..e3db929
--- /dev/null
+++ b/application/job/MessageChatroomListJob.php
@@ -0,0 +1,115 @@
+processMessageChatroomList($data, $job->attempts())) {
+ $job->delete();
+ Log::info('微信群聊消息列表任务执行成功,页码:' . $data['pageIndex']);
+ } else {
+ if ($job->attempts() > 3) {
+ // 超过重试次数,删除任务
+ Log::error('微信群聊消息列表任务执行失败,已超过重试次数,页码:' . $data['pageIndex']);
+ $job->delete();
+ } else {
+ // 任务失败,重新放回队列
+ Log::warning('微信群聊消息列表任务执行失败,重试次数:' . $job->attempts() . ',页码:' . $data['pageIndex']);
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ }
+ } catch (\Exception $e) {
+ // 出现异常,记录日志
+ Log::error('微信群聊消息列表任务异常:' . $e->getMessage());
+ if ($job->attempts() > 3) {
+ $job->delete();
+ } else {
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ }
+ }
+
+ /**
+ * 处理微信群聊消息列表获取
+ * @param array $data 任务数据
+ * @param int $attempts 重试次数
+ * @return bool
+ */
+ protected function processMessageChatroomList($data, $attempts)
+ {
+ // 获取参数
+ $pageIndex = isset($data['pageIndex']) ? $data['pageIndex'] : 0;
+ $pageSize = isset($data['pageSize']) ? $data['pageSize'] : 100;
+
+ Log::info('开始获取微信群聊消息列表,页码:' . $pageIndex . ',页大小:' . $pageSize);
+
+ // 实例化控制器
+ $messageController = new MessageController();
+
+ // 构建请求参数
+ $params = [
+ 'pageIndex' => $pageIndex,
+ 'pageSize' => $pageSize
+ ];
+
+ // 设置请求信息
+ $request = request();
+ $request->withGet($params);
+
+ // 调用添加好友任务获取方法
+ $result = $messageController->getChatroomList($pageIndex,$pageSize,true);
+ $response = json_decode($result,true);
+
+
+ // 判断是否成功
+ if ($response['code'] == 200) {
+ $data = $response['data'];
+
+ // 判断是否有下一页
+ if (!empty($data) && count($data['results']) > 0) {
+ // 有下一页,将下一页任务添加到队列
+ $nextPageIndex = $pageIndex + 1;
+ $this->addNextPageToQueue($nextPageIndex, $pageSize);
+ Log::info('添加下一页任务到队列,页码:' . $nextPageIndex);
+ }
+
+ return true;
+ } else {
+ $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误';
+ Log::error('获取微信群聊消息列表失败:' . $errorMsg);
+ return false;
+ }
+ }
+
+ /**
+ * 添加下一页任务到队列
+ * @param int $pageIndex 页码
+ * @param int $pageSize 每页大小
+ */
+ protected function addNextPageToQueue($pageIndex, $pageSize)
+ {
+ $data = [
+ 'pageIndex' => $pageIndex,
+ 'pageSize' => $pageSize
+ ];
+
+ // 添加到队列,设置任务名为 message_chatroom_list
+ Queue::push(self::class, $data, 'message_chatroom_list');
+ }
+}
\ No newline at end of file
diff --git a/application/job/MessageFriendsListJob.php b/application/job/MessageFriendsListJob.php
new file mode 100644
index 0000000..ee7c309
--- /dev/null
+++ b/application/job/MessageFriendsListJob.php
@@ -0,0 +1,117 @@
+processMessageFriendsList($data, $job->attempts())) {
+ $job->delete();
+ // 去除成功日志,减少日志空间消耗
+ } else {
+ if ($job->attempts() > 3) {
+ // 超过重试次数,删除任务
+ Log::error('好友消息列表任务执行失败,已超过重试次数,页码:' . $data['pageIndex']);
+ $job->delete();
+ } else {
+ // 任务失败,重新放回队列
+ Log::warning('好友消息列表任务执行失败,重试次数:' . $job->attempts() . ',页码:' . $data['pageIndex']);
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ }
+ } catch (\Exception $e) {
+ // 出现异常,记录日志
+ Log::error('好友消息列表任务异常:' . $e->getMessage());
+ if ($job->attempts() > 3) {
+ $job->delete();
+ } else {
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ }
+ }
+
+ /**
+ * 处理好友消息列表获取
+ * @param array $data 任务数据
+ * @param int $attempts 重试次数
+ * @return bool
+ */
+ protected function processMessageFriendsList($data, $attempts)
+ {
+ // 获取参数
+ $pageIndex = isset($data['pageIndex']) ? $data['pageIndex'] : 0;
+ $pageSize = isset($data['pageSize']) ? $data['pageSize'] : 100;
+
+ // 去除开始日志,减少日志空间消耗
+
+ // 实例化控制器
+ $messageController = new MessageController();
+
+ // 构建请求参数
+ $params = [
+ 'pageIndex' => $pageIndex,
+ 'pageSize' => $pageSize
+ ];
+
+ // 设置请求信息
+ $request = request();
+ $request->withGet($params);
+
+
+
+ // 调用添加好友任务获取方法
+ $result = $messageController->getFriendsList($pageIndex,$pageSize,true);
+ $response = json_decode($result,true);
+
+
+ // 判断是否成功
+ if ($response['code'] == 200) {
+ $data = $response['data'];
+
+ // 判断是否有下一页
+ if (!empty($data) && count($data) > 0) {
+ // 有下一页,将下一页任务添加到队列
+ $nextPageIndex = $pageIndex + 1;
+ $this->addNextPageToQueue($nextPageIndex, $pageSize);
+ Log::info('添加下一页任务到队列,页码:' . $nextPageIndex);
+ }
+
+ return true;
+ } else {
+ $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误';
+ Log::error('获取好友消息列表失败:' . $errorMsg);
+ return false;
+ }
+ }
+
+ /**
+ * 添加下一页任务到队列
+ * @param int $pageIndex 页码
+ * @param int $pageSize 每页大小
+ */
+ protected function addNextPageToQueue($pageIndex, $pageSize)
+ {
+ $data = [
+ 'pageIndex' => $pageIndex,
+ 'pageSize' => $pageSize
+ ];
+
+ // 添加到队列,设置任务名为 message_friends_list
+ Queue::push(self::class, $data, 'message_friends_list');
+ }
+}
\ No newline at end of file
diff --git a/application/job/OwnMomentsCollectJob.php b/application/job/OwnMomentsCollectJob.php
new file mode 100644
index 0000000..9b8987c
--- /dev/null
+++ b/application/job/OwnMomentsCollectJob.php
@@ -0,0 +1,152 @@
+processOwnMomentsCollect($data, $job->attempts())) {
+ $job->delete();
+ } else {
+ if ($job->attempts() > 3) {
+ // 超过重试次数,删除任务
+ Log::error('自己朋友圈采集任务执行失败,已超过重试次数');
+ $job->delete();
+ } else {
+ // 任务失败,重新放回队列
+ Log::warning('自己朋友圈采集任务执行失败,重试次数:' . $job->attempts());
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ }
+ } catch (\Exception $e) {
+ // 出现异常,记录日志
+ Log::error('自己朋友圈采集任务异常:' . $e->getMessage());
+ if ($job->attempts() > 3) {
+ $job->delete();
+ } else {
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ }
+ }
+
+ /**
+ * 处理自己朋友圈采集
+ * @param array $data 任务数据
+ * @param int $attempts 重试次数
+ * @return bool
+ */
+ protected function processOwnMomentsCollect($data, $attempts)
+ {
+ try {
+ // 获取在线微信账号列表(只获取在线微信)
+ $onlineWechatAccounts = $this->getOnlineWechatAccounts();
+
+ if (empty($onlineWechatAccounts)) {
+ return true;
+ }
+
+ // 获取API账号配置
+ $username = Env::get('api.username2', '');
+ $password = Env::get('api.password2', '');
+
+ if (empty($username) || empty($password)) {
+ Log::error('API账号配置缺失,无法执行朋友圈采集');
+ return false;
+ }
+
+ // 获取账号ID
+ $toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId');
+ if (empty($toAccountId)) {
+ Log::error('未找到API账号对应的账号ID');
+ return false;
+ }
+
+ $successCount = 0;
+ $failCount = 0;
+
+ // 遍历每个在线微信账号,采集自己的朋友圈
+ foreach ($onlineWechatAccounts as $account) {
+ try {
+ $wechatAccountId = $account['id'];
+ $wechatId = $account['wechatId'];
+
+ // 创建WebSocket控制器实例
+ $webSocket = new WebSocketController([
+ 'userName' => $username,
+ 'password' => $password,
+ 'accountId' => $toAccountId
+ ]);
+
+ // 采集自己的朋友圈(wechatFriendId传0或空,表示采集自己的朋友圈)
+ $result = $webSocket->getMoments([
+ 'wechatAccountId' => $wechatAccountId,
+ 'wechatFriendId' => 0, // 0表示采集自己的朋友圈
+ 'count' => 10 // 每次采集10条
+ ]);
+
+ $resultData = json_decode($result, true);
+ if (!empty($resultData) && $resultData['code'] == 200) {
+ $successCount++;
+ } else {
+ $failCount++;
+ Log::warning("微信账号 {$wechatId} 朋友圈采集失败:" . ($resultData['msg'] ?? '未知错误'));
+ }
+
+ // 避免请求过于频繁,每个账号之间稍作延迟
+ usleep(500000); // 延迟0.5秒
+
+ } catch (\Exception $e) {
+ $failCount++;
+ Log::error("采集微信账号 {$account['wechatId']} 朋友圈异常:" . $e->getMessage());
+ continue;
+ }
+ }
+
+ Log::info("自己朋友圈采集任务完成,成功:{$successCount},失败:{$failCount}");
+ return true;
+
+ } catch (\Exception $e) {
+ Log::error('自己朋友圈采集处理异常:' . $e->getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * 获取在线微信账号列表
+ * @return array
+ */
+ protected function getOnlineWechatAccounts()
+ {
+ try {
+ // 查询在线微信账号(deviceAlive=1 且 wechatAlive=1)
+ $accounts = Db::table('s2_wechat_account')
+ ->where('deviceAlive', 1)
+ ->where('wechatAlive', 1)
+ ->field('id, wechatId, nickname, alias')
+ ->select();
+
+ return $accounts ?: [];
+ } catch (\Exception $e) {
+ Log::error('获取在线微信账号列表失败:' . $e->getMessage());
+ return [];
+ }
+ }
+}
+
diff --git a/application/job/SyncAllFriendsJob.php b/application/job/SyncAllFriendsJob.php
new file mode 100644
index 0000000..fc82bd5
--- /dev/null
+++ b/application/job/SyncAllFriendsJob.php
@@ -0,0 +1,70 @@
+getlist([
+ 'wechatAccountKeyword' => $wechatId,
+ 'pageIndex' => $pageIndex,
+ 'pageSize' => $pageSize,
+ 'preFriendId' => $preFriendId
+ ], true);
+
+ if (is_string($result)) {
+ $result = json_decode($result, true);
+ }
+
+ if ($result['code'] == 200) {
+ $friends = $result['data']['list'] ?? $result['data'];
+ if (is_array($friends) && count($friends) == $pageSize) {
+ $lastFriendId = $friends[count($friends) - 1]['id'];
+ // 还有下一页,重新入队
+ $nextPageIndex = $pageIndex + 1;
+ \think\Queue::push(self::class, [
+ 'wechatId' => $wechatId,
+ 'pageIndex' => $nextPageIndex,
+ 'pageSize' => $pageSize,
+ 'preFriendId' => $lastFriendId,
+ 'jobId' => $jobId,
+ 'queueLockKey' => $queueLockKey
+ ], $job->getQueue());
+ Log::info("微信id: {$wechatId} 下一页任务已入队,pageIndex: {$nextPageIndex},preFriendId: {$lastFriendId}");
+ }
+ }
+
+ $job->delete();
+ Log::info('同步微信id: ' . $wechatId . ' 第' . $pageIndex . '页任务执行成功');
+ // 释放锁逻辑可在所有账号所有分页都完成后处理
+ return true;
+ } catch (\Exception $e) {
+ Log::error('同步所有好友任务异常:' . $e->getMessage());
+ if (!empty($data['queueLockKey'])) {
+ \think\facade\Cache::rm($data['queueLockKey']);
+ }
+ if ($job->attempts() > 3) {
+ $job->delete();
+ } else {
+ $job->release(\think\facade\Config::get('queue.failed_delay', 10));
+ }
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/job/SyncContentJob.php b/application/job/SyncContentJob.php
new file mode 100644
index 0000000..522368e
--- /dev/null
+++ b/application/job/SyncContentJob.php
@@ -0,0 +1,66 @@
+where('ownerWechatId','wxid_h7nsh7vxseyn29')->select();
+ foreach ($ddd as $v) {
+ $d = Db::table('ck_task_customer')->where('task_id','167')->where('phone',$v['wechatId'])->find();
+ if (!empty($d) && !in_array($d['status'],[4,5])) {
+ Db::table('ck_task_customer')->where('id',$d['id'])->update(['status'=>5]);
+ }
+ }
+
+ exit_data(111);
+
+ return true;
+ /*$ddd= '[{"id":21909,"groupName":"私域运营招聘","sortIndex":240426546,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":8074,"groupName":"存客宝新客户介绍","sortIndex":230,"parentId":0,"replyType":1,"children":[{"id":8081,"groupName":"客户系统上线准备","sortIndex":1,"parentId":8074,"replyType":1,"children":[],"replys":null}],"replys":null},{"id":10441,"groupName":"BOSS直聘","sortIndex":229,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":15111,"groupName":"点了码新客户了解","sortIndex":228,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":12732,"groupName":"测试","sortIndex":219,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":8814,"groupName":"封单话术","sortIndex":176,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":8216,"groupName":"私域合伙人","sortIndex":172,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":6476,"groupName":"新客户了解","sortIndex":119,"parentId":0,"replyType":1,"children":[],"replys":null}]';
+ $ddd=json_decode($ddd,1);*/
+
+ $ddd = ReplyGroup::where('companyId',2778)->where('companyId','<>',21898)->where('isDel',0)->select()->toArray();
+
+
+ $authorization = 'OE7kh6Dsw_0SqqH1FTAPCB2ewCQDhx7VvPw6PrsE_p9tcRKbtlFsZau8kjk2NQ829Yah90KhTh0C_35ek569uRQgM_gC0NtKzfRPDDoqMIUE5mI6AO_hm0dm-xDJqhAFYkXHCdXnJYzQZxWS5dleJCIwtQxgRuIzIbr-_G_5C-7DeLEOSt2vi1oGPleLt00QGQ1WYVYqoHYrbPGMghMQpWIbgk5qNcUCeANlLJ_s7QFC3QzArU95_YiK0HlhU81hZqr8kI_5lmdrRBoR-yNIlyhySLRCmEZYGzOxCiUHL3uFHYZA1VnLBAVbryNj5DElZjMgwA';
+ // 设置请求头
+ $headerData = ['client:system'];
+ $header = setHeader($headerData, $authorization, 'json');
+
+
+
+ foreach ($ddd as $key => $value) {
+ $data = [];
+ // 发送请求获取公司账号列表
+ $result = requestCurl('https://s2.siyuguanli.com:9991/api/Reply/listReply?groupId='.$value['id'], '', 'GET', $header,'json');
+ $response = handleApiResponse($result);
+ foreach ($response as $k => $v) {
+ $data[] = [
+ 'groupId' => $v['groupId'],
+ 'userId' => $value['userId'],
+ 'title' => $v['title'],
+ 'msgType' => $v['msgType'],
+ 'content' => $v['content'],
+ 'createTime' => strtotime($v['createTime']),
+ 'lastUpdateTime' => strtotime($v['lastUpdateTime']),
+ 'sortIndex' => 50
+ ];
+ }
+ $Reply = new Reply();
+ $Reply->insertAll($data);
+ }
+
+
+ exit_data(11111);
+
+
+
+
+ }
+}
\ No newline at end of file
diff --git a/application/job/WechatChatroomJob.php b/application/job/WechatChatroomJob.php
new file mode 100644
index 0000000..d5b25b0
--- /dev/null
+++ b/application/job/WechatChatroomJob.php
@@ -0,0 +1,101 @@
+ $pageIndex,
+ 'pageSize' => $pageSize,
+ 'isDel' => $isDel,
+ 'jobId' => $jobId,
+ 'cacheKey' => $cacheKey,
+ 'queueLockKey' => $queueLockKey
+ ]));
+
+ // 如果没有提供缓存键,根据删除状态和任务ID生成一个
+ if (empty($cacheKey)) {
+ $cacheKeyPrefix = "chatroomPage:" . ($jobId ?: date('YmdHis') . rand(1000, 9999));
+ $cacheKeySuffix = $isDel === '' ? '' : ":{$isDel}";
+ $cacheKey = $cacheKeyPrefix . $cacheKeySuffix;
+ }
+
+ // 如果没有提供队列锁键,生成一个
+ if (empty($queueLockKey)) {
+ $queueLockKey = "queue_lock:wechat_chatroom:{$isDel}";
+ }
+
+ // 调用业务逻辑获取微信聊天室列表
+ $logic = new WechatChatroomController();
+ $result = $logic->getlist(['pageIndex' => $pageIndex, 'pageSize' => $pageSize],true, $isDel);
+ $response = json_decode($result, true);
+ $data = $response['data'];
+ // 判断是否有下一页
+ if (!empty($data) && count($data['results']) > 0 && empty($response['isUpdate'])) {
+ $dataCount = count($data['results']);
+ $totalCount = $data['total'];
+
+ // 计算是否还有下一页
+ $hasNextPage = ($pageIndex + 1) * $pageSize < $totalCount;
+
+ if ($hasNextPage) {
+ // 缓存页码信息,设置有效期1天
+ $nextPageIndex = $pageIndex + 1;
+ Cache::set($cacheKey, $nextPageIndex, 600);
+ Log::info("更新缓存页码: {$nextPageIndex}, 缓存键: {$cacheKey}");
+
+ // 添加下一页任务到队列
+ $command = new WechatChatroomCommand();
+ $command->addToQueue($nextPageIndex, $pageSize, $isDel, $jobId, $cacheKey, $queueLockKey);
+ Log::info("已添加下一页任务到队列: 页码 {$nextPageIndex}");
+ } else {
+ // 处理完所有页面,重置页码并释放队列锁
+ Cache::set($cacheKey, 0, 600);
+ Cache::rm($queueLockKey);
+ Log::info("所有微信聊天室列表页面处理完毕,重置页码为0,释放队列锁: {$queueLockKey}");
+ }
+ } else {
+ // API调用出错,记录错误并释放队列锁
+ Log::error("微信聊天室列表获取失败: " . $response['msg']);
+ Cache::rm($queueLockKey);
+ Log::info("由于错误释放队列锁: {$queueLockKey}");
+ }
+
+ $job->delete();
+ return true;
+ } catch (\Exception $e) {
+ // 出现异常,记录错误并释放队列锁
+ Log::error('微信聊天室列表任务处理异常: ' . $e->getMessage());
+ if (!empty($queueLockKey)) {
+ Cache::rm($queueLockKey);
+ Log::info("由于异常释放队列锁: {$queueLockKey}");
+ }
+
+ $job->delete();
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/job/WechatFriendJob.php b/application/job/WechatFriendJob.php
new file mode 100644
index 0000000..e6af5fd
--- /dev/null
+++ b/application/job/WechatFriendJob.php
@@ -0,0 +1,169 @@
+ $pageIndex,
+ 'pageSize' => $pageSize,
+ 'preFriendId' => $preFriendId,
+ 'isDel' => $isDel,
+ 'jobId' => $jobId,
+ 'pageIndexCacheKey' => $pageIndexCacheKey,
+ 'preFriendIdCacheKey' => $preFriendIdCacheKey,
+ 'queueLockKey' => $queueLockKey
+ ]));
+
+ // 如果没有提供缓存键,根据删除状态和任务ID生成
+ if (empty($pageIndexCacheKey)) {
+ $cacheKeyPrefix = "friendsPage:" . ($jobId ?: date('YmdHis') . rand(1000, 9999));
+ $cacheKeySuffix = $isDel === '' ? '' : ":{$isDel}";
+ $pageIndexCacheKey = $cacheKeyPrefix . $cacheKeySuffix;
+ }
+
+ if (empty($preFriendIdCacheKey)) {
+ $cacheKeyPrefix = "preFriendId:" . ($jobId ?: date('YmdHis') . rand(1000, 9999));
+ $cacheKeySuffix = $isDel === '' ? '' : ":{$isDel}";
+ $preFriendIdCacheKey = $cacheKeyPrefix . $cacheKeySuffix;
+ }
+
+ // 如果没有提供队列锁键,生成一个
+ if (empty($queueLockKey)) {
+ $queueLockKey = "queue_lock:wechat_friends:{$isDel}";
+ }
+
+ // 实例化控制器
+ $wechatFriendController = new WechatFriendController();
+
+ // 设置请求信息
+ $request = request();
+ $request->withGet([
+ 'pageIndex' => $pageIndex,
+ 'pageSize' => $pageSize,
+ 'preFriendId' => $preFriendId
+ ]);
+
+ // 调用微信好友列表获取方法,传入isDel参数
+ $result = $wechatFriendController->getlist([
+ 'pageIndex' => $pageIndex,
+ 'pageSize' => $pageSize,
+ 'preFriendId' => $preFriendId,
+ ], true, $isDel);
+ $response = json_decode($result, true);
+
+ // 判断是否成功
+ if ($response['code'] == 200) {
+ $data = $response['data'];
+
+ // 判断是否有下一页
+ if (!empty($data) && count($data) > 0 && empty($response['isUpdate'])) {
+ // 获取最后一条记录的ID
+ $lastFriendId = $data[count($data)-1]['id'];
+
+ // 更新缓存中的页码和最后一个好友ID,设置1天过期
+ $nextPageIndex = $pageIndex + 1;
+ Cache::set($pageIndexCacheKey, $nextPageIndex, 600);
+ Cache::set($preFriendIdCacheKey, $lastFriendId, 600);
+
+ Log::info("更新缓存,下一页页码:{$nextPageIndex},最后好友ID:{$lastFriendId},缓存键: {$pageIndexCacheKey}, {$preFriendIdCacheKey}");
+
+ // 有下一页,将下一页任务添加到队列
+ $command = new WechatFriendCommand();
+ $command->addToQueue($nextPageIndex, $pageSize, $lastFriendId, $isDel, $jobId, $pageIndexCacheKey, $preFriendIdCacheKey, $queueLockKey);
+ Log::info("已添加下一页任务到队列: 页码 {$nextPageIndex}");
+ } else {
+ // 没有下一页,重置缓存并释放队列锁
+ Cache::set($pageIndexCacheKey, 0, 600);
+ Cache::set($preFriendIdCacheKey, '', 600);
+ Cache::rm($queueLockKey);
+ Log::info("所有微信好友列表页面处理完毕,重置页码为0,释放队列锁: {$queueLockKey}");
+ }
+
+ $job->delete();
+ Log::info('微信好友列表任务执行成功,页码:' . $pageIndex . ',删除状态:' . $this->getDeleteStatusText($isDel));
+ return true;
+ } else {
+ // API调用出错,记录错误
+ $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误';
+ Log::error('获取微信好友列表失败:' . $errorMsg);
+
+ if ($job->attempts() > 3) {
+ // 超过重试次数,删除任务并释放队列锁
+ Cache::rm($queueLockKey);
+ Log::info("由于错误释放队列锁: {$queueLockKey}");
+ $job->delete();
+ } else {
+ // 任务失败,重新放回队列
+ Log::warning('微信好友列表任务执行失败,重试次数:' . $job->attempts() . ',页码:' . $pageIndex);
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+
+ return false;
+ }
+ } catch (\Exception $e) {
+ // 出现异常,记录错误并释放队列锁
+ Log::error('微信好友列表任务异常:' . $e->getMessage());
+
+ if (!empty($queueLockKey)) {
+ Cache::rm($queueLockKey);
+ Log::info("由于异常释放队列锁: {$queueLockKey}");
+ }
+
+ if ($job->attempts() > 3) {
+ $job->delete();
+ } else {
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+
+ return false;
+ }
+ }
+
+ /**
+ * 获取删除状态的文本描述
+ * @param string $isDel 删除状态
+ * @return string 状态文本描述
+ */
+ protected function getDeleteStatusText($isDel)
+ {
+ switch ($isDel) {
+ case '0':
+ case 0:
+ return '未删除(false)';
+ case '1':
+ case 1:
+ return '已删除(true)';
+ default:
+ return '全部';
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/job/WechatListJob.php b/application/job/WechatListJob.php
new file mode 100644
index 0000000..01641fb
--- /dev/null
+++ b/application/job/WechatListJob.php
@@ -0,0 +1,117 @@
+processWechatList($data, $job->attempts())) {
+ $job->delete();
+ Log::info('微信客服列表任务执行成功,页码:' . $data['pageIndex']);
+ } else {
+ if ($job->attempts() > 3) {
+ // 超过重试次数,删除任务
+ Log::error('微信客服列表任务执行失败,已超过重试次数,页码:' . $data['pageIndex']);
+ $job->delete();
+ } else {
+ // 任务失败,重新放回队列
+ Log::warning('微信客服列表任务执行失败,重试次数:' . $job->attempts() . ',页码:' . $data['pageIndex']);
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ }
+ } catch (\Exception $e) {
+ // 出现异常,记录日志
+ Log::error('微信客服列表任务异常:' . $e->getMessage());
+ if ($job->attempts() > 3) {
+ $job->delete();
+ } else {
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ }
+ }
+
+ /**
+ * 处理微信客服列表获取
+ * @param array $data 任务数据
+ * @param int $attempts 重试次数
+ * @return bool
+ */
+ protected function processWechatList($data, $attempts)
+ {
+ // 获取参数
+ $pageIndex = isset($data['pageIndex']) ? $data['pageIndex'] : 0;
+ $pageSize = isset($data['pageSize']) ? $data['pageSize'] : 1000;
+
+ Log::info('开始获取微信客服列表,页码:' . $pageIndex . ',页大小:' . $pageSize);
+
+ // 实例化控制器
+ $wechatController = new WechatController();
+
+ // 构建请求参数
+ $params = [
+ 'pageIndex' => $pageIndex,
+ 'pageSize' => $pageSize
+ ];
+
+ // 设置请求信息
+ $request = request();
+ $request->withGet($params);
+
+
+ // 调用设备列表获取方法
+ $result = $wechatController->getlist($pageIndex,$pageSize,true);
+ $response = json_decode($result,true);
+
+
+
+ // 判断是否成功
+ if ($response['code'] == 200) {
+ $data = $response['data'];
+
+ // 判断是否有下一页
+ if (!empty($data) && count($data['results']) > 0) {
+ // 有下一页,将下一页任务添加到队列
+ $nextPageIndex = $pageIndex + 1;
+ $this->addNextPageToQueue($nextPageIndex, $pageSize);
+ Log::info('添加下一页任务到队列,页码:' . $nextPageIndex);
+ }
+
+ return true;
+ } else {
+ $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误';
+ Log::error('获取微信客服列表失败:' . $errorMsg);
+ return false;
+ }
+ }
+
+ /**
+ * 添加下一页任务到队列
+ * @param int $pageIndex 页码
+ * @param int $pageSize 每页大小
+ */
+ protected function addNextPageToQueue($pageIndex, $pageSize)
+ {
+ $data = [
+ 'pageIndex' => $pageIndex,
+ 'pageSize' => $pageSize
+ ];
+
+ // 添加到队列,设置任务名为 wechat_list
+ Queue::push(self::class, $data, 'wechat_list');
+ }
+}
\ No newline at end of file
diff --git a/application/job/WechatMomentsJob.php b/application/job/WechatMomentsJob.php
new file mode 100644
index 0000000..2aabff4
--- /dev/null
+++ b/application/job/WechatMomentsJob.php
@@ -0,0 +1,131 @@
+where('account',$username)->value('s2_accountId');
+ }else{
+ Log::error("没有账号配置");
+ return;
+ }
+
+ try {
+ $jobId = $data['jobId'] ?? '';
+ $queueLockKey = $data['queueLockKey'] ?? '';
+ Log::info("开始处理朋友圈采集任务,任务ID:{$jobId}");
+
+ // 获取好友列表
+ $friends = $this->getFriends($data['pageIndex'], $data['pageSize']);
+ if (empty($friends)) {
+ Log::info("没有更多好友数据,任务完成");
+ Cache::rm($queueLockKey);
+ $job->delete();
+ return;
+ }
+
+ foreach ($friends as $friend) {
+ try {
+ // 执行切换好友命令
+ $automaticAssign = new AutomaticAssign();
+ $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['friendId'], 'toAccountId' => $toAccountId], true);
+ //存入缓存
+ artificialAllotWechatFriend($friend);
+
+ // 执行采集朋友圈命令
+ $webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]);
+ $webSocket->getMoments(['wechatFriendId' => $friend['friendId'], 'wechatAccountId' => $friend['wechatAccountId']]);
+
+ // 处理完毕切换回原账号
+ $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['friendId'], 'toAccountId' => $friend['accountId']], true);
+
+
+ } catch (\Exception $e) {
+ // 发生异常时也要切换回原账号
+ $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['friendId'], 'toAccountId' => $friend['accountId']], true);
+ Log::error("采集好友 {$friend['id']} 的朋友圈失败:" . $e->getMessage());
+ continue;
+ }
+ }
+
+ // 判断是否需要继续翻页
+ if (count($friends) < $data['pageSize']) {
+ // 如果返回的数据少于页面大小,说明已经没有更多数据了
+ Log::info("朋友圈采集任务完成,没有更多数据");
+ Cache::rm($queueLockKey);
+ $job->delete();
+ } else {
+ // 还有更多数据,继续处理下一页
+ $data['pageIndex']++;
+ if ($data['pageIndex'] > $this->maxPages) {
+ Log::info("已达到最大页数限制 {$this->maxPages},任务完成");
+ Cache::rm($data['pageIndexCacheKey']);
+ Cache::rm($queueLockKey);
+ $job->delete();
+ } else {
+ // 处理下一页
+ Cache::set($data['pageIndexCacheKey'], $data['pageIndex']);
+
+ // 有下一页,将下一页任务添加到队列
+ $command = new WechatMomentsCommand();
+ $command->addToQueue($data['pageIndex'], $data['pageSize'], $jobId, $queueLockKey);
+ }
+ }
+ } catch (\Exception $e) {
+ $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['friendId'], 'toAccountId' => $friend['accountId']], true);
+ Log::error("朋友圈采集任务异常:" . $e->getMessage());
+ Cache::rm($queueLockKey);
+ $job->delete();
+ }
+ }
+
+
+ /**
+ * 获取账号的好友列表
+ * @param int $accountId 账号ID
+ * @return array
+ */
+ private function getFriends($page = 1 ,$pageSize = 100)
+ {
+ $list = Db::table('s2_company_account')
+ ->alias('ca')
+ ->join(['s2_wechat_account' => 'wa'], 'ca.id = wa.deviceAccountId')
+ ->join(['s2_wechat_friend' => 'wf'], 'ca.id = wf.accountId AND wf.wechatAccountId = wa.id')
+ ->where([
+ 'ca.status' => 0,
+ 'wf.isDeleted' => 0,
+ 'wa.deviceAlive' => 1,
+ 'wa.wechatAlive' => 1
+ ])
+ ->field([
+ 'ca.id as accountId',
+ 'ca.userName',
+ 'wf.id as friendId',
+ 'wf.wechatId',
+ 'wf.wechatAccountId',
+ 'wa.wechatId as wechatAccountWechatId',
+ 'wa.currentDeviceId as deviceId'
+ ])->group('wf.wechatId')
+ ->order('wf.id DESC')
+ ->page($page, $pageSize)
+ ->select();
+ return $list;
+ }
+}
\ No newline at end of file
diff --git a/application/job/WorkbenchAutoLikeJob.php b/application/job/WorkbenchAutoLikeJob.php
new file mode 100644
index 0000000..11c8d84
--- /dev/null
+++ b/application/job/WorkbenchAutoLikeJob.php
@@ -0,0 +1,483 @@
+logJobStart($jobId, $queueLockKey);
+ $workbenches = $this->getActiveWorkbenches();
+ if (empty($workbenches)) {
+ $this->handleEmptyWorkbenches($job, $queueLockKey);
+ return true;
+ }
+ $this->processWorkbenches($workbenches);
+ $this->handleJobSuccess($job, $queueLockKey);
+ return true;
+
+ } catch (\Exception $e) {
+ return $this->handleJobError($e, $job, $queueLockKey);
+ }
+ }
+
+ /**
+ * 获取活跃的工作台
+ * @return \think\Collection
+ */
+ protected function getActiveWorkbenches()
+ {
+ return Workbench::where([
+ ['status', '=', 1],
+ ['isDel', '=', 0],
+ ['type', '=', 1] // 只获取自动点赞类型的工作台
+ ])->order('id DESC')->select();
+ }
+
+ /**
+ * 处理工作台列表
+ * @param \think\Collection $workbenches
+ */
+ protected function processWorkbenches($workbenches)
+ {
+ foreach ($workbenches as $workbench) {
+ try {
+ $this->processSingleWorkbench($workbench);
+ } catch (\Exception $e) {
+ Log::error("处理工作台 {$workbench->id} 失败: " . $e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * 处理单个工作台
+ * @param Workbench $workbench
+ */
+ protected function processSingleWorkbench($workbench)
+ {
+ $config = WorkbenchAutoLike::where('workbenchId', $workbench->id)->find();
+ if (!$config) {
+ Log::error("工作台 {$workbench->id} 配置获取失败");
+ return;
+ }
+
+ $this->handleAutoLike($workbench, $config);
+ }
+
+ /**
+ * 处理自动点赞任务
+ * @param Workbench $workbench
+ * @param WorkbenchAutoLike $config
+ */
+ protected function handleAutoLike($workbench, $config)
+ {
+ if (!$this->validateAutoLikeConfig($workbench, $config)) {
+ return;
+ }
+
+ // 验证是否在点赞时间范围内
+ if (!$this->isWithinLikeTimeRange($config)) {
+ return;
+ }
+
+ // 处理分页获取好友列表
+ $this->processAllFriends($workbench, $config);
+ }
+
+ /**
+ * 处理所有好友分页
+ * @param Workbench $workbench
+ * @param WorkbenchAutoLike $config
+ * @param int $page 当前页码
+ * @param int $pageSize 每页大小
+ */
+ protected function processAllFriends($workbench, $config, $page = 1, $pageSize = 100)
+ {
+ $friendList = $this->getFriendList($config, $page, $pageSize);
+
+ if (empty($friendList)) {
+ return;
+ }
+
+ // 直接顺序处理所有好友
+ foreach ($friendList as $friend) {
+ // 验证是否达到点赞次数上限
+ $likeCount = $this->getTodayLikeCount($workbench, $config, $friend['deviceId']);
+ if ($likeCount >= $config['maxLikes']) {
+ Log::info("工作台 {$workbench->id} 点赞次数已达上限");
+ continue;
+ }
+
+ // 验证是否达到好友点赞次数上限
+ $friendMaxLikes = Db::name('workbench_auto_like_item')
+ ->where('workbenchId', $workbench->id)
+ ->where('wechatFriendId', $friend['friendId'])
+ ->count();
+
+ if ($friendMaxLikes < $config['friendMaxLikes']) {
+ $this->processFriendMoments($workbench, $config, $friend);
+ }
+ }
+
+ // 如果当前页数据量等于页大小,说明可能还有更多数据,继续处理下一页
+ if (count($friendList) == $pageSize) {
+ $this->processAllFriends($workbench, $config, $page + 1, $pageSize);
+ }
+ }
+
+ /**
+ * 获取好友列表
+ * @param WorkbenchAutoLike $config 配置
+ * @param int $page 页码
+ * @param int $pageSize 每页大小
+ * @return array
+ */
+ protected function getFriendList($config, $page = 1, $pageSize = 100)
+ {
+ $friends = json_decode($config['friends'], true);
+ $devices = json_decode($config['devices'], true);
+
+ $list = Db::table('s2_company_account')
+ ->alias('ca')
+ ->join(['s2_wechat_account' => 'wa'], 'ca.id = wa.deviceAccountId')
+ ->join(['s2_wechat_friend' => 'wf'], 'wf.wechatAccountId = wa.id')
+ ->join('workbench_auto_like_item wali', 'wali.wechatFriendId = wf.id AND wali.workbenchId = ' . $config['workbenchId'], 'left')
+ ->where([
+ 'ca.status' => 0,
+ 'wf.isDeleted' => 0,
+ 'wa.deviceAlive' => 1,
+ 'wa.wechatAlive' => 1
+ ])
+ ->whereIn('wa.currentDeviceId', $devices)
+ ->field([
+ 'ca.id as accountId',
+ 'ca.userName',
+ 'wf.id as friendId',
+ 'wf.wechatId',
+ 'wf.wechatAccountId',
+ 'wa.wechatId as wechatAccountWechatId',
+ 'wa.currentDeviceId as deviceId',
+ 'COUNT(wali.id) as like_count'
+ ]);
+
+ if (!empty($friends) && is_array($friends) && count($friends) > 0) {
+ $list = $list->whereIn('wf.id', $friends);
+ }
+
+ $list = $list->group('wf.wechatId')
+ ->having('like_count < ' . $config['friendMaxLikes'])
+ ->order('wf.id DESC')
+ ->page($page, $pageSize)
+ ->select();
+
+ return $list;
+ }
+
+ /**
+ * 处理好友朋友圈
+ * @param Workbench $workbench
+ * @param WorkbenchAutoLike $config
+ * @param array $friend
+ */
+ protected function processFriendMoments($workbench, $config, $friend)
+ {
+ $toAccountId = '';
+ $username = Env::get('api.username2', '');
+ $password = Env::get('api.password2', '');
+ if (!empty($username) || !empty($password)) {
+ $toAccountId = Db::name('users')->where('account',$username)->value('s2_accountId');
+ }
+
+ try {
+ // 执行切换好友命令
+ $automaticAssign = new AutomaticAssign();
+ $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['friendId'], 'toAccountId' => $toAccountId], true);
+ //存入缓存
+ artificialAllotWechatFriend($friend);
+ // 创建WebSocket链接
+ $webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]);
+
+ // 查询未点赞的朋友圈
+ $moments = $this->getUnlikedMoments($friend['wechatId']);
+ if (empty($moments) || count($moments) == 0) {
+ //采集最新朋友圈
+ $webSocket->getMoments(['wechatFriendId' => $friend['friendId'], 'wechatAccountId' => $friend['wechatAccountId']]);
+ $moments = $this->getUnlikedMoments($friend['wechatId']);
+ }
+
+
+ if (empty($moments) || count($moments) == 0) {
+ // 处理完毕切换回原账号
+ $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['friendId'], 'toAccountId' => $friend['accountId']], true);
+ Log::info("好友 {$friend['friendId']} 没有需要点赞的朋友圈");
+ return;
+ }
+
+
+ foreach ($moments as $moment) {
+ // 点赞朋友圈
+ $this->likeMoment($workbench, $config, $friend, $moment, $webSocket);
+
+ if(!empty($config['enableFriendTags']) && !empty($config['friendTags'])){
+ // 修改好友标签
+ $labels = $this->getFriendLabels($friend);
+ $labels[] = $config['friendTags'];
+ $webSocket->modifyFriendLabel(['wechatFriendId' => $friend['friendId'], 'wechatAccountId' => $friend['wechatAccountId'], 'labels' => $labels]);
+
+ //更新用户标签
+ $friendData = Db::table('s2_wechat_friend')->where('id', $friend['friendId'])->find();
+ Db::table('s2_wechat_friend')->where('id', $friend['friendId'])->update(['labels' => json_encode($labels,256)]);
+ }
+ break;
+ }
+
+ // 处理完毕切换回原账号
+ $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['friendId'], 'toAccountId' => $friend['accountId']], true);
+ } catch (\Exception $e) {
+ // 异常情况下也要确保切换回原账号
+ $automaticAssign = new AutomaticAssign();
+ $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['friendId'], 'toAccountId' => $friend['accountId']], true);
+ Log::error("处理好友 {$friend['friendId']} 朋友圈失败异常: " . $e->getMessage());
+ }
+ }
+
+ /**
+ * 获取未点赞的朋友圈
+ * @param int $wechatId
+ * @return \think\Collection
+ */
+ protected function getUnlikedMoments($wechatId)
+ {
+ return Db::table('s2_wechat_moments')
+ ->alias('wm')
+ ->join('workbench_auto_like_item wali', 'wali.momentsId = wm.id', 'left')
+ ->where([
+ ['wm.userName', '=', $wechatId],
+ ['wali.id', 'null', null]
+ ])
+ ->where('wm.update_time', '>=', time() - 86400)
+ ->field('wm.id, wm.snsId')
+ ->group('wali.wechatFriendId')
+ ->order('wm.createTime DESC')
+ ->page(1,1)
+ ->select();
+ }
+
+ /**
+ * 点赞朋友圈
+ * @param Workbench $workbench
+ * @param WorkbenchAutoLike $config
+ * @param array $friend
+ * @param array $moment
+ * @param WebSocketController $webSocket
+ */
+ protected function likeMoment($workbench, $config, $friend, $moment, $webSocket)
+ {
+ try {
+ $result = $webSocket->momentInteract([
+ 'snsId' => $moment['snsId'],
+ 'wechatAccountId' => $friend['wechatAccountId'],
+ ]);
+
+ $result = json_decode($result, true);
+
+ if ($result['code'] == 200) {
+ $this->recordLike($workbench, $moment, $friend);
+
+ // 添加间隔时间
+ if (!empty($config['interval'])) {
+ sleep($config['interval']);
+ }
+ } else {
+ Log::error("工作台 {$workbench->id} 点赞失败: " . ($result['msg'] ?? '未知错误'));
+ }
+ } catch (\Exception $e) {
+ Log::error("工作台 {$workbench->id} 点赞异常: " . $e->getMessage());
+ }
+ }
+
+ /**
+ * 记录点赞
+ * @param Workbench $workbench
+ * @param array $moment
+ * @param array $friend
+ */
+ protected function recordLike($workbench, $moment, $friend)
+ {
+ Db::name('workbench_auto_like_item')->insert([
+ 'workbenchId' => $workbench->id,
+ 'deviceId' => $friend['deviceId'],
+ 'momentsId' => $moment['id'],
+ 'snsId' => $moment['snsId'],
+ 'wechatAccountId' => $friend['wechatAccountId'],
+ 'wechatFriendId' => $friend['friendId'],
+ 'createTime' => time()
+ ]);
+ Log::info("工作台 {$workbench->id} 点赞成功: {$moment['snsId']}");
+ }
+
+ /**
+ * 获取好友标签
+ * @param array $friend
+ * @return array
+ */
+ protected function getFriendLabels($friend)
+ {
+ $wechatFriendController = new WechatFriendController();
+ $result = $wechatFriendController->getlist([
+ 'friendKeyword' => $friend['wechatId'],
+ 'wechatAccountKeyword' => $friend['wechatAccountWechatId']
+ ], true);
+
+ $result = json_decode($result, true);
+ $labels = [];
+
+ if(!empty($result['data'])){
+ foreach($result['data'] as $item){
+ $labels = array_merge($labels, $item['labels']);
+ }
+ }
+
+ return $labels;
+ }
+
+ /**
+ * 验证自动点赞配置
+ * @param Workbench $workbench
+ * @param WorkbenchAutoLike $config
+ * @return bool
+ */
+ protected function validateAutoLikeConfig($workbench, $config)
+ {
+ $requiredFields = ['contentTypes', 'interval', 'maxLikes', 'startTime', 'endTime'];
+ foreach ($requiredFields as $field) {
+ if (empty($config[$field])) {
+ Log::error("工作台 {$workbench->id} 配置字段 {$field} 为空");
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 获取今日点赞次数
+ * @param Workbench $workbench
+ * @param WorkbenchAutoLike $config
+ * @return int
+ */
+ protected function getTodayLikeCount($workbench, $config, $deviceId)
+ {
+ return Db::name('workbench_auto_like_item')
+ ->where('workbenchId', $workbench->id)
+ ->where('deviceId', $deviceId)
+ ->whereTime('createTime', 'between', [
+ strtotime(date('Y-m-d') . ' ' . $config['startTime'] . ':00'),
+ strtotime(date('Y-m-d') . ' ' . $config['endTime'] . ':00')
+ ])
+ ->count();
+ }
+
+ /**
+ * 检查是否在点赞时间范围内
+ * @param WorkbenchAutoLike $config
+ * @return bool
+ */
+ protected function isWithinLikeTimeRange($config)
+ {
+ $currentTime = date('H:i');
+ if ($currentTime < $config['startTime'] || $currentTime > $config['endTime']) {
+ Log::info("当前时间 {$currentTime} 不在点赞时间范围内 ({$config['startTime']} - {$config['endTime']})");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * 记录任务开始
+ * @param string $jobId
+ * @param string $queueLockKey
+ */
+ protected function logJobStart($jobId, $queueLockKey)
+ {
+ Log::info('开始处理工作台自动点赞任务: ' . json_encode([
+ 'jobId' => $jobId,
+ 'queueLockKey' => $queueLockKey
+ ]));
+ }
+
+ /**
+ * 处理任务成功
+ * @param Job $job
+ * @param string $queueLockKey
+ */
+ protected function handleJobSuccess($job, $queueLockKey)
+ {
+ $job->delete();
+ Cache::rm($queueLockKey);
+ Log::info('工作台自动点赞任务执行成功');
+ }
+
+ /**
+ * 处理任务错误
+ * @param \Exception $e
+ * @param Job $job
+ * @param string $queueLockKey
+ * @return bool
+ */
+ protected function handleJobError(\Exception $e, $job, $queueLockKey)
+ {
+ Log::error('工作台自动点赞任务异常:' . $e->getMessage());
+
+ if (!empty($queueLockKey)) {
+ Cache::rm($queueLockKey);
+ Log::info("由于异常释放队列锁: {$queueLockKey}");
+ }
+
+ if ($job->attempts() > self::MAX_RETRY_ATTEMPTS) {
+ $job->delete();
+ } else {
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+
+ return false;
+ }
+
+ /**
+ * 处理空工作台情况
+ * @param Job $job
+ * @param string $queueLockKey
+ */
+ protected function handleEmptyWorkbenches(Job $job, $queueLockKey)
+ {
+ Log::info('没有需要处理的工作台自动点赞任务');
+ $job->delete();
+ Cache::rm($queueLockKey);
+ }
+}
\ No newline at end of file
diff --git a/application/job/WorkbenchGroupCreateAdminFriendJob.php b/application/job/WorkbenchGroupCreateAdminFriendJob.php
new file mode 100644
index 0000000..459db92
--- /dev/null
+++ b/application/job/WorkbenchGroupCreateAdminFriendJob.php
@@ -0,0 +1,155 @@
+delete();
+ return true;
+ }
+
+ // 获取管理员信息
+ $adminFriends = Db::table('s2_wechat_friend')
+ ->where('id', 'in', $adminFriendIds)
+ ->column('id,wechatId,ownerWechatId');
+
+ if (empty($adminFriends)) {
+ Log::warning("未找到管理员好友信息。工作台ID: {$workbenchId}");
+ $job->delete();
+ return true;
+ }
+
+ // 获取微信账号信息
+ $wechatAccount = Db::table('s2_wechat_account')->where('id', $wechatAccountId)->find();
+ if (empty($wechatAccount)) {
+ Log::error("未找到微信账号。微信账号ID: {$wechatAccountId}");
+ $job->delete();
+ return false;
+ }
+
+ // 从流量池用户中查找每个管理员的好友
+ // 管理员的好友:从s2_wechat_friend表中查找,ownerWechatId=管理员的wechatId,且wechatId在流量池用户中
+ $allAdminFriendIds = [];
+ foreach ($adminFriends as $adminFriend) {
+ $adminWechatId = $adminFriend['wechatId'];
+
+ // 从好友表中查找该管理员的好友(在流量池用户中)
+ $adminFriendsList = Db::table('s2_wechat_friend')
+ ->where('ownerWechatId', $adminWechatId)
+ ->whereIn('wechatId', $poolUsers)
+ ->column('id,wechatId');
+
+ if (!empty($adminFriendsList)) {
+ $allAdminFriendIds = array_merge($allAdminFriendIds, array_keys($adminFriendsList));
+ }
+ }
+
+ $allAdminFriendIds = array_unique($allAdminFriendIds);
+
+ if (empty($allAdminFriendIds)) {
+ Log::info("未找到管理员的好友,跳过拉人。工作台ID: {$workbenchId}");
+ $job->delete();
+ return true;
+ }
+
+ // 初始化WebSocket
+ $toAccountId = '';
+ $username = Env::get('api.username2', '');
+ $password = Env::get('api.password2', '');
+ if (!empty($username) || !empty($password)) {
+ $toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId');
+ }
+ $webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]);
+
+ // 拉管理员好友进群
+ $inviteResult = $webSocket->CmdChatroomInvite([
+ 'wechatChatroomId' => $groupId,
+ 'wechatFriendIds' => $allAdminFriendIds
+ ]);
+
+ // 记录管理员好友进群
+ $installData = [];
+ foreach ($allAdminFriendIds as $friendId) {
+ $friendInfo = Db::table('s2_wechat_friend')->where('id', $friendId)->find();
+ $installData[] = [
+ 'workbenchId' => $workbenchId,
+ 'friendId' => $friendId,
+ 'wechatId' => $friendInfo['wechatId'] ?? '',
+ 'groupId' => $groupId,
+ 'wechatAccountId' => $wechatAccountId,
+ 'status' => self::STATUS_ADMIN_FRIEND_ADDED,
+ 'memberType' => self::MEMBER_TYPE_ADMIN_FRIEND,
+ 'retryCount' => 0,
+ 'chatroomId' => $chatroomId,
+ 'createTime' => time(),
+ ];
+ }
+
+ if (!empty($installData)) {
+ Db::name('workbench_group_create_item')->insertAll($installData);
+ Log::info("管理员好友已拉入群。工作台ID: {$workbenchId}, 群ID: {$groupId}, 好友数: " . count($installData));
+ }
+
+ $job->delete();
+ return true;
+ } catch (\Exception $e) {
+ Log::error("拉管理员好友任务异常:{$e->getMessage()}");
+
+ if ($job->attempts() > self::MAX_RETRY_ATTEMPTS) {
+ $job->delete();
+ } else {
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+
+ return false;
+ }
+ }
+}
+
diff --git a/application/job/WorkbenchGroupCreateJob.php b/application/job/WorkbenchGroupCreateJob.php
new file mode 100644
index 0000000..5359ab8
--- /dev/null
+++ b/application/job/WorkbenchGroupCreateJob.php
@@ -0,0 +1,852 @@
+logJobStart($jobId, $queueLockKey);
+ $this->execute();
+ $this->handleJobSuccess($job, $queueLockKey);
+ return true;
+ } catch (\Exception $e) {
+ return $this->handleJobError($e, $job, $queueLockKey);
+ }
+ }
+
+ /**
+ * 成员类型常量
+ */
+ const MEMBER_TYPE_OWNER = 1; // 群主成员
+ const MEMBER_TYPE_ADMIN = 2; // 管理员
+ const MEMBER_TYPE_OWNER_FRIEND = 3; // 群主好友
+ const MEMBER_TYPE_ADMIN_FRIEND = 4; // 管理员好友
+
+ /**
+ * 状态常量
+ */
+ const STATUS_PENDING = 0; // 待创建
+ const STATUS_CREATING = 1; // 创建中
+ const STATUS_SUCCESS = 2; // 创建成功
+ const STATUS_FAILED = 3; // 创建失败
+ const STATUS_ADMIN_FRIEND_ADDED = 4; // 管理员好友已拉入
+
+ /**
+ * 执行任务
+ * @throws \Exception
+ */
+ public function execute()
+ {
+ try {
+ // 1. 查询启用了建群功能的数据
+ $workbenches = Workbench::where(['status' => 0, 'type' => 4, 'isDel' => 0,'id' => 354])->order('id desc')->select();
+
+ foreach ($workbenches as $workbench) {
+ // 获取工作台配置
+ $config = WorkbenchGroupCreate::where('workbenchId', $workbench->id)->find();
+ if (!$config) {
+ continue;
+ }
+
+ // 解析配置
+ $config['poolGroups'] = json_decode($config['poolGroups'] ?? '[]', true) ?: [];
+ $config['devices'] = json_decode($config['devices'] ?? '[]', true) ?: [];
+ $config['wechatGroups'] = json_decode($config['wechatGroups'] ?? '[]', true) ?: [];
+ $config['admins'] = json_decode($config['admins'] ?? '[]', true) ?: [];
+
+ // 检查时间限制
+ if (!$this->isWithinTimeRange($config)) {
+ continue;
+ }
+
+ // 检查每日建群数量限制
+ if (!$this->checkDailyLimit($workbench->id, $config)) {
+ continue;
+ }
+
+ // 检查是否有正在创建中的群,如果有则跳过(避免重复创建)
+ $creatingCount = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbench->id)
+ ->where('status', self::STATUS_CREATING)
+ ->where('groupId', '<>', null) // 有groupId的记录
+ ->group('groupId')
+ ->count();
+ if ($creatingCount > 0) {
+ Log::info("工作台ID: {$workbench->id} 有正在创建中的群({$creatingCount}个),跳过本次执行");
+ continue;
+ }
+
+ if (empty($config['devices'])) {
+ continue;
+ }
+ // 获取群主成员(从设备中获取)
+ $groupMember = [];
+ $wechatIds = Db::name('device_wechat_login')
+ ->whereIn('deviceId', $config['devices'])
+ ->where('alive', DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE)
+ ->order('id desc')
+ ->column('wechatId');
+
+ if (empty($wechatIds)) {
+ continue;
+ }
+ $groupMember = array_unique($wechatIds);
+
+ // 获取群主好友ID映射(所有群主的好友)
+ $groupMemberWechatId = [];
+ $groupMemberId = [];
+
+ foreach ($groupMember as $ownerWechatId) {
+ $friends = Db::table('s2_wechat_friend')
+ ->where('ownerWechatId', $ownerWechatId)
+ ->whereIn('wechatId', $groupMember)
+ ->where('isDeleted', 0)
+ ->field('id,wechatId')
+ ->select();
+
+ foreach ($friends as $friend) {
+ if (!isset($groupMemberWechatId[$friend['id']])) {
+ $groupMemberWechatId[$friend['id']] = $friend['wechatId'];
+ $groupMemberId[] = $friend['id'];
+ }
+ }
+ }
+
+ // 如果配置了wechatGroups,从指定的群组中获取成员
+ if (!empty($config['wechatGroups'])) {
+ $this->addGroupMembersFromWechatGroups($config['wechatGroups'], $groupMember, $groupMemberId, $groupMemberWechatId);
+ }
+
+ if (empty($groupMemberId)) {
+ continue;
+ }
+
+ // 获取流量池用户(如果配置了流量池)
+ $poolItem = [];
+ if (!empty($config['poolGroups'])) {
+ $poolItem = Db::name('traffic_source_package_item')
+ ->whereIn('packageId', $config['poolGroups'])
+ ->where('isDel', 0)
+ ->group('identifier')
+ ->column('identifier');
+ }
+
+ // 如果既没有流量池也没有指定群组,跳过
+ if (empty($poolItem) && empty($config['wechatGroups'])) {
+ continue;
+ }
+
+ // 获取已入群的用户(排除已成功入群的)
+ $groupUser = [];
+ if (!empty($poolItem)) {
+ $groupUser = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbench->id)
+ ->where('status', 'in', [self::STATUS_SUCCESS, self::STATUS_ADMIN_FRIEND_ADDED, self::STATUS_CREATING])
+ ->whereIn('wechatId', $poolItem)
+ ->group('wechatId')
+ ->column('wechatId');
+ }
+
+ // 待入群的用户(从流量池中筛选)
+ $joinUser = !empty($poolItem) ? array_diff($poolItem, $groupUser) : [];
+
+ // 如果流量池用户已用完或没有配置流量池,但配置了wechatGroups,至少创建一次(使用群主成员)
+ if (empty($joinUser) && !empty($config['wechatGroups'])) {
+ // 如果没有流量池用户,创建一个空批次,让processBatchUsers处理只有群主成员的情况
+ $joinUser = []; // 空数组,但会继续执行
+ }
+
+ // 如果既没有流量池用户也没有配置wechatGroups,跳过
+ if (empty($joinUser) && empty($config['wechatGroups'])) {
+ continue;
+ }
+
+ // 计算随机群人数(不包含管理员,只减去群主成员数)
+ // 群主成员数 = 群主好友ID数量
+ $minGroupSize = max(2, $config['groupSizeMin']); // 至少2人才能建群
+ $maxGroupSize = max($minGroupSize, $config['groupSizeMax']);
+ $groupRandNum = mt_rand($minGroupSize, $maxGroupSize) - count($groupMemberId);
+ if ($groupRandNum <= 0) {
+ $groupRandNum = 1; // 至少需要1个成员
+ }
+
+ // 分批处理待入群用户
+ $addGroupUser = [];
+ if (!empty($joinUser)) {
+ $totalRows = count($joinUser);
+ for ($i = 0; $i < $totalRows; $i += $groupRandNum) {
+ $batchRows = array_slice($joinUser, $i, $groupRandNum);
+ if (!empty($batchRows)) {
+ $addGroupUser[] = $batchRows;
+ }
+ }
+ } else {
+ // 如果没有流量池用户但配置了wechatGroups,创建一个空批次
+ $addGroupUser[] = [];
+ }
+ // 初始化WebSocket
+ $toAccountId = '';
+ $username = Env::get('api.username2', '');
+ $password = Env::get('api.password2', '');
+ if (!empty($username) || !empty($password)) {
+ $toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId');
+ }
+ $webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]);
+
+ // 遍历每批用户
+ foreach ($addGroupUser as $batchUsers) {
+ $this->processBatchUsers($workbench, $config, $batchUsers, $groupMemberId, $groupMemberWechatId, $groupRandNum, $webSocket);
+ }
+ }
+ } catch (\Exception $e) {
+ Log::error("工作台建群任务异常: " . $e->getMessage());
+ throw $e;
+ }
+ }
+
+ /**
+ * 处理一批用户
+ * @param Workbench $workbench 工作台
+ * @param array $config 配置
+ * @param array $batchUsers 批次用户(微信ID数组,来自流量池)
+ * @param array $groupMemberId 群主成员ID数组
+ * @param array $groupMemberWechatId 群主成员微信ID映射
+ * @param int $groupRandNum 随机群人数(不包含管理员)
+ * @param WebSocketController $webSocket WebSocket实例
+ */
+ protected function processBatchUsers($workbench, $config, $batchUsers, $groupMemberId, $groupMemberWechatId, $groupRandNum, $webSocket)
+ {
+ // 1. 获取群主微信ID列表(用于验证管理员)
+ // 从群主成员的好友记录中提取所有群主的微信ID(ownerWechatId)
+ $groupOwnerWechatIds = [];
+ foreach ($groupMemberId as $memberId) {
+ $member = Db::table('s2_wechat_friend')->where('id', $memberId)->find();
+ if ($member && !in_array($member['ownerWechatId'], $groupOwnerWechatIds)) {
+ $groupOwnerWechatIds[] = $member['ownerWechatId'];
+ }
+ }
+
+
+ // 如果从好友表获取不到,使用群主成员微信ID列表(作为备用)
+ if (empty($groupOwnerWechatIds)) {
+ $groupOwnerWechatIds = array_values(array_unique($groupMemberWechatId));
+ }
+ // 2. 验证并获取管理员好友ID(管理员必须是群主的好友)
+ $adminFriendIds = [];
+ $adminWechatIds = [];
+ if (!empty($config['admins'])) {
+ $adminFriends = Db::table('s2_wechat_friend')
+ ->where('id', 'in', $config['admins'])
+ ->field('id,wechatId,ownerWechatId')
+ ->select();
+
+ foreach ($adminFriends as $adminFriend) {
+ // 验证:管理员必须是群主的好友
+ if (in_array($adminFriend['ownerWechatId'], $groupOwnerWechatIds)) {
+ $adminFriendIds[] = $adminFriend['id'];
+ $adminWechatIds[$adminFriend['id']] = $adminFriend['wechatId'];
+ }
+ }
+ }
+
+
+ // 3. 从流量池用户中筛选出是群主好友的用户(按微信账号分组)
+ $ownerFriendIdsByAccount = [];
+ $wechatIds = [];
+
+ // 如果batchUsers为空,说明没有流量池用户,但可能配置了wechatGroups
+ // 这种情况下,使用群主成员作为基础,按账号分组
+ if (empty($batchUsers)) {
+ // 按账号分组群主成员
+ foreach ($groupMemberId as $memberId) {
+ $member = Db::table('s2_wechat_friend')->where('id', $memberId)->find();
+ if ($member) {
+ $accountWechatId = $member['ownerWechatId'];
+ $account = Db::table('s2_wechat_account')
+ ->where('wechatId', $accountWechatId)
+ ->field('id')
+ ->find();
+
+ if ($account) {
+ $wechatAccountId = $account['id'];
+ if (!isset($ownerFriendIdsByAccount[$wechatAccountId])) {
+ $ownerFriendIdsByAccount[$wechatAccountId] = [];
+ }
+ $ownerFriendIdsByAccount[$wechatAccountId][] = $memberId;
+ $wechatIds[$memberId] = $groupMemberWechatId[$memberId] ?? '';
+ }
+ }
+ }
+ } else {
+ // 获取群主的好友关系(从流量池中筛选)
+ $ownerFriends = Db::table('s2_wechat_friend')->alias('f')
+ ->join(['s2_wechat_account' => 'a'], 'f.wechatAccountId=a.id')
+ ->whereIn('f.wechatId', $batchUsers)
+ ->whereIn('a.wechatId', $groupOwnerWechatIds)
+ ->where('f.isDeleted', 0)
+ ->field('f.id,f.wechatId,a.id as wechatAccountId')
+ ->select();
+
+ if (empty($ownerFriends)) {
+ Log::warning("未找到群主的好友,跳过。工作台ID: {$workbench->id}");
+ return;
+ }
+
+ // 按微信账号分组群主好友
+ foreach ($ownerFriends as $friend) {
+ $wechatAccountId = $friend['wechatAccountId'];
+ if (!isset($ownerFriendIdsByAccount[$wechatAccountId])) {
+ $ownerFriendIdsByAccount[$wechatAccountId] = [];
+ }
+ $ownerFriendIdsByAccount[$wechatAccountId][] = $friend['id'];
+ $wechatIds[$friend['id']] = $friend['wechatId'];
+ }
+ }
+
+ // 如果没有找到任何好友,跳过
+ if (empty($ownerFriendIdsByAccount)) {
+ Log::warning("未找到任何群主好友或成员,跳过。工作台ID: {$workbench->id}");
+ return;
+ }
+
+ // 4. 遍历每个微信账号,创建群
+ foreach ($ownerFriendIdsByAccount as $wechatAccountId => $ownerFriendIds) {
+ // 4.1 获取当前账号的管理员好友ID
+ $currentAdminFriendIds = [];
+ $accountWechatId = Db::table('s2_wechat_account')->where('id', $wechatAccountId)->value('wechatId');
+ foreach ($adminFriendIds as $adminFriendId) {
+ $adminFriend = Db::table('s2_wechat_friend')->where('id', $adminFriendId)->find();
+ if ($adminFriend && $adminFriend['ownerWechatId'] == $accountWechatId) {
+ $currentAdminFriendIds[] = $adminFriendId;
+ $wechatIds[$adminFriendId] = $adminWechatIds[$adminFriendId];
+ }
+ }
+
+ // 4.2 获取当前账号的群主成员ID
+ $currentGroupMemberIds = [];
+ foreach ($groupMemberId as $memberId) {
+ $member = Db::table('s2_wechat_friend')->where('id', $memberId)->find();
+ if ($member && $member['ownerWechatId'] == $accountWechatId) {
+ $currentGroupMemberIds[] = $memberId;
+ if (!isset($wechatIds[$memberId])) {
+ $wechatIds[$memberId] = $groupMemberWechatId[$memberId] ?? '';
+ }
+ }
+ }
+
+ // 4.3 限制群主好友数量(按随机群人数)
+ // 如果ownerFriendIds只包含群主成员(没有流量池用户),则不需要限制
+ $limitedOwnerFriendIds = $ownerFriendIds;
+ if (count($ownerFriendIds) > $groupRandNum) {
+ $limitedOwnerFriendIds = array_slice($ownerFriendIds, 0, $groupRandNum);
+ }
+
+ // 4.4 创建群:管理员 + 群主成员 + 群主好友(从流量池筛选)
+ // 合并时去重,避免重复添加群主成员
+ $createFriendIds = array_merge($currentAdminFriendIds, $currentGroupMemberIds);
+ foreach ($limitedOwnerFriendIds as $friendId) {
+ if (!in_array($friendId, $createFriendIds)) {
+ $createFriendIds[] = $friendId;
+ }
+ }
+
+ // 微信建群至少需要2个人
+ if (count($createFriendIds) < 2) {
+ Log::warning("建群好友数量不足(至少需要2人),跳过。工作台ID: {$workbench->id}, 微信账号ID: {$wechatAccountId}, 当前人数: " . count($createFriendIds));
+ continue;
+ }
+
+ // 4.5 检查当前账号是否有正在创建中的群,如果有则跳过
+ $creatingGroupCount = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbench->id)
+ ->where('wechatAccountId', $wechatAccountId)
+ ->where('status', self::STATUS_CREATING)
+ ->where('groupId', '<>', null)
+ ->group('groupId')
+ ->count();
+
+ if ($creatingGroupCount > 0) {
+ Log::info("工作台ID: {$workbench->id}, 微信账号ID: {$wechatAccountId} 有正在创建中的群({$creatingGroupCount}个),跳过本次创建");
+ continue;
+ }
+
+ // 4.6 生成群名称
+ $existingGroupCount = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbench->id)
+ ->where('wechatAccountId', $wechatAccountId)
+ ->where('status', self::STATUS_SUCCESS)
+ ->where('groupId', '<>', null) // 排除groupId为NULL的记录
+ ->group('groupId')
+ ->count();
+
+ $chatroomName = $existingGroupCount > 0
+ ? $config['groupNameTemplate'] . ($existingGroupCount + 1) . '群'
+ : $config['groupNameTemplate'];
+
+ // 4.7 调用建群接口
+ $createTime = time();
+ $createResult = $webSocket->CmdChatroomCreate([
+ 'chatroomName' => $chatroomName,
+ 'wechatFriendIds' => $createFriendIds,
+ 'wechatAccountId' => $wechatAccountId
+ ]);
+
+ $createResultData = json_decode($createResult, true);
+
+ // 4.8 解析建群结果,获取群ID
+ // chatroomId: varchar(64) - 微信的群聊ID(字符串)
+ // groupId: int(10) - 数据库中的群组ID(整数)
+ $chatroomId = null; // 微信群聊ID(字符串)
+ $groupId = 0; // 数据库群组ID(整数)
+ $tempGroupId = null; // 临时群标识,用于轮询查询
+
+ if (!empty($createResultData) && isset($createResultData['code']) && $createResultData['code'] == 200) {
+ // 尝试从返回数据中获取群ID(根据实际API返回格式调整)
+ if (isset($createResultData['data']['chatroomId'])) {
+ // API返回的是chatroomId(字符串)
+ $chatroomId = (string)$createResultData['data']['chatroomId'];
+ // 通过chatroomId查询数据库获取groupId
+ $group = Db::name('wechat_group')
+ ->where('chatroomId', $chatroomId)
+ ->where('deleteTime', 0)
+ ->find();
+ if ($group) {
+ $groupId = intval($group['id']);
+ }
+ } elseif (isset($createResultData['data']['id'])) {
+ // API返回的是数据库ID(整数)
+ $groupId = intval($createResultData['data']['id']);
+ // 通过groupId查询chatroomId
+ $group = Db::name('wechat_group')
+ ->where('id', $groupId)
+ ->where('deleteTime', 0)
+ ->find();
+ if ($group && !empty($group['chatroomId'])) {
+ $chatroomId = (string)$group['chatroomId'];
+ }
+ }
+ // 如果有临时标识,保存用于轮询
+ if (isset($createResultData['data']['tempId'])) {
+ $tempGroupId = $createResultData['data']['tempId'];
+ }
+ }
+
+ // 4.9 如果建群接口没有立即返回群ID,进行同步轮询检查
+ if ($groupId == 0) {
+ // 获取账号的微信ID(群主微信ID)
+ $accountWechatId = Db::table('s2_wechat_account')
+ ->where('id', $wechatAccountId)
+ ->value('wechatId');
+
+ if (!empty($accountWechatId)) {
+ $pollResult = $this->pollGroupCreation($chatroomName, $accountWechatId, $wechatAccountId, $tempGroupId);
+ if ($pollResult && is_array($pollResult)) {
+ $groupId = intval($pollResult['groupId'] ?? 0);
+ $chatroomId = !empty($pollResult['chatroomId']) ? (string)$pollResult['chatroomId'] : null;
+ } elseif ($pollResult > 0) {
+ // 兼容旧返回值(只返回groupId)
+ $groupId =0;
+ $chatroomId = null;
+ }
+ }
+ }
+
+ // 4.10 记录创建请求
+ $installData = [];
+ foreach ($createFriendIds as $friendId) {
+ $memberType = in_array($friendId, $currentAdminFriendIds)
+ ? self::MEMBER_TYPE_ADMIN
+ : (in_array($friendId, $currentGroupMemberIds) ? self::MEMBER_TYPE_OWNER : self::MEMBER_TYPE_OWNER_FRIEND);
+
+ $installData[] = [
+ 'workbenchId' => $workbench->id,
+ 'friendId' => $friendId,
+ 'wechatId' => $wechatIds[$friendId] ?? ($groupMemberWechatId[$friendId] ?? ''),
+ 'groupId' => $groupId > 0 ? $groupId : null, // int类型
+ 'wechatAccountId' => $wechatAccountId,
+ 'status' => $groupId > 0 ? self::STATUS_SUCCESS : self::STATUS_FAILED,
+ 'memberType' => $memberType,
+ 'retryCount' => 0,
+ 'chatroomId' => $chatroomId, // varchar类型
+ 'createTime' => $createTime,
+ ];
+ }
+ Db::name('workbench_group_create_item')->insertAll($installData);
+
+ // 5. 如果群创建成功,拉管理员的好友进群
+ // 注意:拉人接口需要chatroomId(字符串),而不是groupId(整数)
+ if (!empty($chatroomId) && !empty($currentAdminFriendIds)) {
+ $this->inviteAdminFriends($workbench, $config, $batchUsers, $currentAdminFriendIds, $chatroomId, $groupId, $wechatAccountId, $wechatIds, $createTime, $webSocket);
+ }
+ }
+ }
+
+ /**
+ * 拉管理员的好友进群
+ * @param Workbench $workbench 工作台
+ * @param array $config 配置
+ * @param array $batchUsers 批次用户(流量池微信ID数组)
+ * @param array $adminFriendIds 管理员好友ID数组
+ * @param string $chatroomId 群聊ID(字符串,用于API调用)
+ * @param int $groupId 数据库群组ID(整数)
+ * @param int $wechatAccountId 微信账号ID
+ * @param array $wechatIds 好友ID到微信ID的映射
+ * @param int $createTime 创建时间
+ * @param WebSocketController $webSocket WebSocket实例
+ */
+ protected function inviteAdminFriends($workbench, $config, $batchUsers, $adminFriendIds, $chatroomId, $groupId, $wechatAccountId, $wechatIds, $createTime, $webSocket)
+ {
+ // 获取管理员的微信ID列表
+ $adminWechatIds = [];
+ foreach ($adminFriendIds as $adminFriendId) {
+ if (isset($wechatIds[$adminFriendId])) {
+ $adminWechatIds[] = $wechatIds[$adminFriendId];
+ }
+ }
+
+ if (empty($adminWechatIds)) {
+ return;
+ }
+
+ // 从流量池用户中筛选出是管理员好友的用户
+ $adminFriendsFromPool = Db::table('s2_wechat_friend')->alias('f')
+ ->join(['s2_wechat_account' => 'a'], 'f.wechatAccountId=a.id')
+ ->whereIn('f.wechatId', $batchUsers)
+ ->whereIn('a.wechatId', $adminWechatIds)
+ ->where('a.id', $wechatAccountId)
+ ->where('f.isDeleted', 0)
+ ->field('f.id,f.wechatId')
+ ->select();
+
+ if (empty($adminFriendsFromPool)) {
+ Log::info("未找到管理员的好友,跳过拉人。工作台ID: {$workbench->id}, 群ID: {$chatroomId}");
+ return;
+ }
+
+ // 提取好友ID列表
+ $adminFriendIdsToInvite = [];
+ foreach ($adminFriendsFromPool as $friend) {
+ $adminFriendIdsToInvite[] = $friend['id'];
+ $wechatIds[$friend['id']] = $friend['wechatId'];
+ }
+
+ // 调用拉人接口(使用chatroomId字符串)
+ $inviteResult = $webSocket->CmdChatroomInvite([
+ 'wechatChatroomId' => $chatroomId,
+ 'wechatFriendIds' => $adminFriendIdsToInvite
+ ]);
+
+ $inviteResultData = json_decode($inviteResult, true);
+ $inviteSuccess = !empty($inviteResultData) && isset($inviteResultData['code']) && $inviteResultData['code'] == 200;
+
+ // 记录管理员好友拉入状态
+ $adminFriendData = [];
+ foreach ($adminFriendIdsToInvite as $friendId) {
+ $adminFriendData[] = [
+ 'workbenchId' => $workbench->id,
+ 'friendId' => $friendId,
+ 'wechatId' => $wechatIds[$friendId] ?? '',
+ 'groupId' => $groupId > 0 ? $groupId : null, // int类型
+ 'wechatAccountId' => $wechatAccountId,
+ 'status' => $inviteSuccess ? self::STATUS_ADMIN_FRIEND_ADDED : self::STATUS_FAILED,
+ 'memberType' => self::MEMBER_TYPE_ADMIN_FRIEND,
+ 'retryCount' => 0,
+ 'chatroomId' => $chatroomId, // varchar类型
+ 'createTime' => $createTime,
+ ];
+ }
+ Db::name('workbench_group_create_item')->insertAll($adminFriendData);
+
+ if ($inviteSuccess) {
+ // 去除成功日志,减少日志空间消耗
+ } else {
+ Log::warning("管理员好友拉入失败。工作台ID: {$workbench->id}, 群组ID: {$groupId}, 群聊ID: {$chatroomId}");
+ }
+ }
+
+
+ /**
+ * 轮询检查群是否创建成功
+ * @param string $chatroomName 群名称
+ * @param string $ownerWechatId 群主微信ID
+ * @param int $wechatAccountId 微信账号ID
+ * @param string|null $tempGroupId 临时群标识(如果有)
+ * @return array|int 返回数组包含groupId和chatroomId,或只返回groupId(兼容旧代码),如果未找到返回0
+ */
+ protected function pollGroupCreation($chatroomName, $ownerWechatId, $wechatAccountId, $tempGroupId = null)
+ {
+ $maxAttempts = 10; // 最多查询10次
+ $interval = 5; // 每次间隔5秒
+
+ // 获取账号ID(accountId)和微信账号的微信ID(wechatAccountWechatId),用于查询s2_wechat_chatroom表
+ $accountInfo = Db::table('s2_wechat_account')
+ ->where('id', $wechatAccountId)
+ ->field('id,wechatId')
+ ->find();
+
+ $accountId = $accountInfo['id'] ?? null;
+ $wechatAccountWechatId = $accountInfo['wechatId'] ?? null;
+
+ if (empty($accountId) && empty($wechatAccountWechatId)) {
+ Log::warning("无法获取账号ID和微信账号ID,跳过轮询。微信账号ID: {$wechatAccountId}");
+ return 0;
+ }
+
+ // 获取授权信息(用于调用同步接口)
+ $username = Env::get('api.username2', '');
+ $password = Env::get('api.password2', '');
+
+ for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
+ // 等待5秒(第一次立即查询,后续等待)
+ if ($attempt > 1) {
+ sleep($interval);
+ }
+
+ // 1. 先调用接口同步最新的群组信息
+ try {
+ $chatroomController = new \app\api\controller\WechatChatroomController();
+ // 构建同步参数
+ $syncData = [
+ 'wechatAccountKeyword' => $ownerWechatId, // 通过群主微信ID筛选
+ 'isDeleted' => false,
+ 'pageIndex' => 0,
+ 'pageSize' => 5 // 获取足够多的数据
+ ];
+ // 调用getlist方法同步数据(内部调用,isInner=true)
+ $chatroomController->getlist($syncData, true, 0);
+ } catch (\Exception $e) {
+ Log::warning("同步群组信息失败: " . $e->getMessage());
+ // 即使同步失败,也继续查询本地数据
+ }
+
+ // 2. 查询本地表 s2_wechat_chatroom
+ // 计算5分钟前的时间戳
+ $fiveMinutesAgo = time() - 300; // 5分钟 = 300秒
+ $now = time();
+
+ // 查询群聊:通过群名称、账号ID或微信账号ID和创建时间查询
+ // 如果accountId不为空,优先使用accountId查询;如果accountId为空,则使用wechatAccountWechatId查询
+ $chatroom = Db::table('s2_wechat_chatroom')
+ ->where('nickname', $chatroomName)
+ ->where('isDeleted', 0)
+ ->where('createTime', '>=', $fiveMinutesAgo) // 创建时间在5分钟内
+ ->where('createTime', '<=', $now)
+ ->where('wechatAccountWechatId', $wechatAccountWechatId)
+ ->order('createTime', 'desc')
+ ->find();
+
+
+ // 如果找到了群聊,返回群ID和chatroomId
+ if ($chatroom && !empty($chatroom['id'])) {
+ $chatroomId = !empty($chatroom['chatroomId']) ? (string)$chatroom['chatroomId'] : null;
+ // 如果有chatroomId,尝试查询wechat_group表获取groupId
+ $groupId = $chatroom['id'];
+ Log::info("轮询检查群创建成功。群名称: {$chatroomName}, 群聊ID: {$chatroom['id']}, chatroomId: {$chatroomId}, 群组ID: {$groupId}, 尝试次数: {$attempt}");
+ return [
+ 'groupId' => $groupId > 0 ? $groupId : intval($chatroom['id']), // 如果没有groupId,使用chatroom的id
+ 'chatroomId' => $chatroomId ?: (string)$chatroom['id']
+ ];
+ }
+
+ Log::debug("轮询检查群创建中。群名称: {$chatroomName}, 尝试次数: {$attempt}/{$maxAttempts}");
+ }
+
+ // 10次查询后仍未找到,返回0表示失败
+ Log::warning("轮询检查群创建失败,已查询{$maxAttempts}次仍未找到群组。群名称: {$chatroomName}, 群主微信ID: {$ownerWechatId}, 账号ID: {$accountId}");
+ return 0;
+ }
+
+ /**
+ * 检查是否在时间范围内
+ * @param array $config 配置
+ * @return bool
+ */
+ protected function isWithinTimeRange($config)
+ {
+ if (empty($config['startTime']) || empty($config['endTime'])) {
+ return true; // 如果没有配置时间,则允许执行
+ }
+
+ $today = date('Y-m-d');
+ $startTimestamp = strtotime($today . ' ' . $config['startTime'] . ':00');
+ $endTimestamp = strtotime($today . ' ' . $config['endTime'] . ':00');
+
+ $currentTime = time();
+
+ // 如果开始时间大于当前时间,还未到执行时间
+ if ($startTimestamp > $currentTime) {
+ return false;
+ }
+
+ // 如果结束时间小于当前时间,已过执行时间
+ if ($endTimestamp < $currentTime) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * 检查每日建群数量限制
+ * @param int $workbenchId 工作台ID
+ * @param array $config 配置
+ * @return bool
+ */
+ protected function checkDailyLimit($workbenchId, $config)
+ {
+ if (empty($config['maxGroupsPerDay']) || $config['maxGroupsPerDay'] <= 0) {
+ return true; // 如果没有配置限制,则允许执行
+ }
+
+ $today = date('Y-m-d');
+ $startTimestamp = strtotime($today . ' 00:00:00');
+ $endTimestamp = strtotime($today . ' 23:59:59');
+
+ // 查询今日已创建的群数量(状态为成功)
+ $todayCount = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('status', self::STATUS_SUCCESS)
+ ->where('groupId', '<>', null) // 排除groupId为NULL的记录
+ ->where('createTime', 'between', [$startTimestamp, $endTimestamp])
+ ->group('groupId')
+ ->count();
+
+ return $todayCount < $config['maxGroupsPerDay'];
+ }
+
+ /**
+ * 从指定的微信群组中获取成员
+ * @param array $wechatGroups 群组ID数组(可能是好友ID或群组ID)
+ * @param array $groupMember 群主成员微信ID数组(引用传递)
+ * @param array $groupMemberId 群主成员好友ID数组(引用传递)
+ * @param array $groupMemberWechatId 群主成员微信ID映射(引用传递)
+ */
+ protected function addGroupMembersFromWechatGroups($wechatGroups, &$groupMember, &$groupMemberId, &$groupMemberWechatId)
+ {
+ foreach ($wechatGroups as $groupId) {
+ if (is_numeric($groupId)) {
+ // 数字ID:可能是好友ID,查询好友信息
+ $friend = Db::table('s2_wechat_friend')
+ ->where('id', $groupId)
+ ->where('isDeleted', 0)
+ ->field('id,wechatId,ownerWechatId')
+ ->find();
+
+ if ($friend) {
+ // 添加到群主成员
+ if (!in_array($friend['ownerWechatId'], $groupMember)) {
+ $groupMember[] = $friend['ownerWechatId'];
+ }
+
+ if (!isset($groupMemberWechatId[$friend['id']])) {
+ $groupMemberWechatId[$friend['id']] = $friend['wechatId'];
+ $groupMemberId[] = $friend['id'];
+ }
+ } else {
+ // 如果不是好友ID,可能是群组ID,查询群组信息
+ $group = Db::name('wechat_group')
+ ->where('id', $groupId)
+ ->where('deleteTime', 0)
+ ->field('ownerWechatId')
+ ->find();
+
+ if ($group && !in_array($group['ownerWechatId'], $groupMember)) {
+ $groupMember[] = $group['ownerWechatId'];
+ }
+ }
+ } else {
+ // 字符串ID:手动创建的群组,可能是wechatId
+ if (!in_array($groupId, $groupMember)) {
+ $groupMember[] = $groupId;
+ }
+ }
+ }
+ }
+
+
+ /**
+ * 记录任务开始
+ * @param string $jobId
+ * @param string $queueLockKey
+ */
+ protected function logJobStart($jobId, $queueLockKey)
+ {
+ // 去除开始日志,减少日志空间消耗
+ }
+
+ /**
+ * 处理任务成功
+ * @param Job $job
+ * @param string $queueLockKey
+ */
+ protected function handleJobSuccess($job, $queueLockKey)
+ {
+ $job->delete();
+ Cache::rm($queueLockKey);
+ // 去除成功日志,减少日志空间消耗
+ }
+
+ /**
+ * 处理任务错误
+ * @param \Exception $e
+ * @param Job $job
+ * @param string $queueLockKey
+ * @return bool
+ */
+ protected function handleJobError(\Exception $e, $job, $queueLockKey)
+ {
+ Log::error('工作台消息群发任务异常:' . $e->getMessage());
+
+ if (!empty($queueLockKey)) {
+ Cache::rm($queueLockKey);
+ Log::info("由于异常释放队列锁: {$queueLockKey}");
+ }
+
+ if ($job->attempts() > self::MAX_RETRY_ATTEMPTS) {
+ $job->delete();
+ } else {
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+
+ return false;
+ }
+
+}
\ No newline at end of file
diff --git a/application/job/WorkbenchGroupCreateOwnerFriendJob.php b/application/job/WorkbenchGroupCreateOwnerFriendJob.php
new file mode 100644
index 0000000..6409720
--- /dev/null
+++ b/application/job/WorkbenchGroupCreateOwnerFriendJob.php
@@ -0,0 +1,109 @@
+delete();
+ return true;
+ }
+
+ // 初始化WebSocket
+ $toAccountId = '';
+ $username = Env::get('api.username2', '');
+ $password = Env::get('api.password2', '');
+ if (!empty($username) || !empty($password)) {
+ $toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId');
+ }
+ $webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]);
+
+ // 拉群主好友进群
+ $inviteResult = $webSocket->CmdChatroomInvite([
+ 'wechatChatroomId' => $groupId,
+ 'wechatFriendIds' => $ownerFriendIds
+ ]);
+
+ // 获取好友微信ID映射
+ $friendWechatIds = Db::table('s2_wechat_friend')
+ ->where('id', 'in', $ownerFriendIds)
+ ->column('id,wechatId');
+
+ // 更新群主好友记录状态
+ Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('wechatAccountId', $wechatAccountId)
+ ->where('status', 1) // 创建中
+ ->where('memberType', self::MEMBER_TYPE_OWNER_FRIEND)
+ ->where('createTime', '>=', $createTime - 10)
+ ->where('createTime', '<=', $createTime + 10)
+ ->update([
+ 'status' => self::STATUS_SUCCESS,
+ 'groupId' => $groupId,
+ 'chatroomId' => $chatroomId,
+ 'verifyTime' => time()
+ ]);
+
+ Log::info("群主好友已拉入群。工作台ID: {$workbenchId}, 群ID: {$groupId}, 好友数: " . count($ownerFriendIds));
+
+ $job->delete();
+ return true;
+ } catch (\Exception $e) {
+ Log::error("拉群主好友任务异常:{$e->getMessage()}");
+
+ if ($job->attempts() > self::MAX_RETRY_ATTEMPTS) {
+ $job->delete();
+ } else {
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+
+ return false;
+ }
+ }
+}
+
diff --git a/application/job/WorkbenchGroupCreateRetryJob.php b/application/job/WorkbenchGroupCreateRetryJob.php
new file mode 100644
index 0000000..8f4ab9c
--- /dev/null
+++ b/application/job/WorkbenchGroupCreateRetryJob.php
@@ -0,0 +1,179 @@
+find();
+ if (!$workbench) {
+ Log::error("未找到工作台。工作台ID: {$workbenchId}");
+ $job->delete();
+ return false;
+ }
+
+ $config = WorkbenchGroupCreate::where('workbenchId', $workbench->id)->find();
+ if (!$config) {
+ Log::error("未找到工作台配置。工作台ID: {$workbenchId}");
+ $job->delete();
+ return false;
+ }
+
+ // 获取失败记录
+ $failedItems = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('wechatAccountId', $wechatAccountId)
+ ->where('createTime', '>=', $createTime - 10)
+ ->where('createTime', '<=', $createTime + 10)
+ ->where('status', 'in', [1, 3]) // 创建中或失败
+ ->select();
+
+ if (empty($failedItems)) {
+ Log::info("未找到需要重试的记录。工作台ID: {$workbenchId}");
+ $job->delete();
+ return true;
+ }
+
+ // 解析配置
+ $config['poolGroups'] = json_decode($config['poolGroups'], true);
+ $config['devices'] = json_decode($config['devices'], true);
+ $config['admins'] = json_decode($config['admins'] ?? '[]', true) ?: [];
+
+ // 获取群主成员
+ $groupMember = Db::name('device_wechat_login')->alias('dwl')
+ ->join(['s2_wechat_account' => 'a'], 'dwl.wechatId = a.wechatId')
+ ->whereIn('dwl.deviceId', $config['devices'])
+ ->group('a.id')
+ ->column('a.wechatId');
+
+ $groupMemberWechatId = Db::table('s2_wechat_friend')
+ ->where('ownerWechatId', $groupMember[0])
+ ->whereIn('wechatId', $groupMember)
+ ->column('id,wechatId');
+
+ $groupMemberId = array_keys($groupMemberWechatId);
+
+ // 获取管理员好友ID
+ $adminFriendIds = [];
+ if (!empty($config['admins'])) {
+ $adminFriends = Db::table('s2_wechat_friend')
+ ->where('id', 'in', $config['admins'])
+ ->column('id,wechatId,ownerWechatId');
+
+ $accountWechatId = Db::table('s2_wechat_account')->where('id', $wechatAccountId)->value('wechatId');
+ foreach ($adminFriends as $adminFriend) {
+ if ($adminFriend['ownerWechatId'] == $accountWechatId) {
+ $adminFriendIds[] = $adminFriend['id'];
+ }
+ }
+ }
+
+ // 初始化WebSocket
+ $toAccountId = '';
+ $username = Env::get('api.username2', '');
+ $password = Env::get('api.password2', '');
+ if (!empty($username) || !empty($password)) {
+ $toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId');
+ }
+ $webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]);
+
+ // 重新创建群
+ $createFriendIds = array_merge($adminFriendIds, $groupMemberId);
+
+ if (count($createFriendIds) < 2) {
+ Log::error("重试建群好友数量不足。工作台ID: {$workbenchId}");
+ $job->delete();
+ return false;
+ }
+
+ // 生成群名称
+ $existingGroupCount = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('wechatAccountId', $wechatAccountId)
+ ->where('status', 2) // 成功
+ ->group('groupId')
+ ->count();
+
+ $chatroomName = $existingGroupCount > 0
+ ? $config['groupNameTemplate'] . ($existingGroupCount + 1) . '群'
+ : $config['groupNameTemplate'];
+
+ // 调用建群接口
+ $createResult = $webSocket->CmdChatroomCreate([
+ 'chatroomName' => $chatroomName,
+ 'wechatFriendIds' => $createFriendIds,
+ 'wechatAccountId' => $wechatAccountId
+ ]);
+
+ // 更新记录状态为创建中
+ Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('wechatAccountId', $wechatAccountId)
+ ->where('createTime', '>=', $createTime - 10)
+ ->where('createTime', '<=', $createTime + 10)
+ ->update([
+ 'status' => 1, // 创建中
+ 'createTime' => time() // 更新创建时间
+ ]);
+
+ // 创建新的轮询验证任务
+ Queue::later(5, 'app\job\WorkbenchGroupCreateVerifyJob', [
+ 'workbenchId' => $workbenchId,
+ 'wechatAccountId' => $wechatAccountId,
+ 'createTime' => time(),
+ 'adminFriendIds' => $adminFriendIds,
+ 'poolUsers' => [], // 重试时暂时不传poolUsers,后续可以优化
+ ], 'default');
+
+ Log::info("重试建群任务已创建。工作台ID: {$workbenchId}, 微信账号ID: {$wechatAccountId}");
+
+ $job->delete();
+ return true;
+ } catch (\Exception $e) {
+ Log::error("重试建群任务异常:{$e->getMessage()}");
+
+ if ($job->attempts() > self::MAX_RETRY_ATTEMPTS) {
+ $job->delete();
+ } else {
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+
+ return false;
+ }
+ }
+}
+
diff --git a/application/job/WorkbenchGroupCreateVerifyJob.php b/application/job/WorkbenchGroupCreateVerifyJob.php
new file mode 100644
index 0000000..9c8b091
--- /dev/null
+++ b/application/job/WorkbenchGroupCreateVerifyJob.php
@@ -0,0 +1,248 @@
+attempts();
+
+ // 查询待验证的群记录
+ $groupItems = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('wechatAccountId', $wechatAccountId)
+ ->where('status', self::STATUS_CREATING)
+ ->where('createTime', '>=', $createTime - 10) // 允许10秒误差
+ ->where('createTime', '<=', $createTime + 10)
+ ->group('wechatAccountId')
+ ->select();
+
+ if (empty($groupItems)) {
+ // 去除信息日志,减少日志空间消耗
+ $job->delete();
+ return true;
+ }
+
+ // 获取微信账号信息
+ $wechatAccount = Db::table('s2_wechat_account')->where('id', $wechatAccountId)->find();
+ if (empty($wechatAccount)) {
+ Log::error("未找到微信账号,任务失败。微信账号ID: {$wechatAccountId}");
+ $job->delete();
+ return false;
+ }
+
+ // 调用接口查询群聊列表
+ $chatroomController = new WechatChatroomController();
+ $chatroomList = $chatroomController->getlist([
+ 'wechatAccountKeyword' => $wechatAccount['wechatId'],
+ 'pageIndex' => 0,
+ 'pageSize' => 100
+ ], true);
+
+ $chatroomListData = json_decode($chatroomList, true);
+
+ if (empty($chatroomListData['data']['results'])) {
+ // 如果超过最大重试次数,标记为失败并重试创建
+ if ($attempts >= self::MAX_RETRY_ATTEMPTS) {
+ $this->handleCreateFailed($workbenchId, $wechatAccountId, $createTime, $job);
+ return false;
+ }
+
+ // 继续轮询
+ $job->release(self::POLL_INTERVAL);
+ return false;
+ }
+
+ // 查找符合条件的群(chatroomOwnerAvatar和chatroomOwnerNickname不为空)
+ $successGroup = null;
+ foreach ($chatroomListData['data']['results'] as $chatroom) {
+ if (!empty($chatroom['chatroomOwnerAvatar']) && !empty($chatroom['chatroomOwnerNickname'])) {
+ // 检查创建时间是否匹配(允许30秒误差)
+ $chatroomCreateTime = isset($chatroom['createTime']) ? strtotime($chatroom['createTime']) : 0;
+ if (abs($chatroomCreateTime - $createTime) <= 30) {
+ $successGroup = $chatroom;
+ break;
+ }
+ }
+ }
+
+ if ($successGroup) {
+ // 群创建成功,更新记录状态
+ $groupId = $successGroup['id'] ?? 0;
+ $chatroomId = $successGroup['chatroomId'] ?? '';
+
+ // 更新管理员和群主成员的记录状态
+ Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('wechatAccountId', $wechatAccountId)
+ ->where('status', self::STATUS_CREATING)
+ ->where('memberType', 'in', [1, 2]) // 群主成员和管理员
+ ->where('createTime', '>=', $createTime - 10)
+ ->where('createTime', '<=', $createTime + 10)
+ ->update([
+ 'status' => self::STATUS_SUCCESS,
+ 'groupId' => $groupId,
+ 'chatroomId' => $chatroomId,
+ 'verifyTime' => time()
+ ]);
+
+ // 去除成功日志,减少日志空间消耗
+
+ // 3. 拉群主好友进群(在验证成功后执行)
+ $ownerFriendIds = $data['ownerFriendIds'] ?? [];
+ if (!empty($ownerFriendIds)) {
+ Queue::push('app\job\WorkbenchGroupCreateOwnerFriendJob', [
+ 'workbenchId' => $workbenchId,
+ 'wechatAccountId' => $wechatAccountId,
+ 'groupId' => $groupId,
+ 'chatroomId' => $chatroomId,
+ 'ownerFriendIds' => $ownerFriendIds,
+ 'createTime' => $createTime
+ ], 'default');
+ }
+
+ // 5. 创建拉管理员好友的任务(在群主好友拉入后执行)
+ if (!empty($adminFriendIds) && !empty($poolUsers)) {
+ Queue::push('app\job\WorkbenchGroupCreateAdminFriendJob', [
+ 'workbenchId' => $workbenchId,
+ 'wechatAccountId' => $wechatAccountId,
+ 'groupId' => $groupId,
+ 'chatroomId' => $chatroomId,
+ 'adminFriendIds' => $adminFriendIds,
+ 'poolUsers' => $poolUsers
+ ], 'default');
+ }
+
+ $job->delete();
+ return true;
+ } else {
+ // 如果超过最大重试次数,标记为失败并重试创建
+ if ($attempts >= self::MAX_RETRY_ATTEMPTS) {
+ $this->handleCreateFailed($workbenchId, $wechatAccountId, $createTime, $job);
+ return false;
+ }
+
+ // 继续轮询
+ $job->release(self::POLL_INTERVAL);
+ return false;
+ }
+ } catch (\Exception $e) {
+ Log::error("群创建验证任务异常:{$e->getMessage()}");
+
+ if ($job->attempts() >= self::MAX_RETRY_ATTEMPTS) {
+ $job->delete();
+ } else {
+ $job->release(self::POLL_INTERVAL);
+ }
+
+ return false;
+ }
+ }
+
+ /**
+ * 处理创建失败的情况(重试创建)
+ * @param int $workbenchId 工作台ID
+ * @param int $wechatAccountId 微信账号ID
+ * @param int $createTime 创建时间
+ * @param Job $job 队列任务
+ */
+ protected function handleCreateFailed($workbenchId, $wechatAccountId, $createTime, $job)
+ {
+ // 更新状态为失败
+ Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('wechatAccountId', $wechatAccountId)
+ ->where('status', self::STATUS_CREATING)
+ ->where('createTime', '>=', $createTime - 10)
+ ->where('createTime', '<=', $createTime + 10)
+ ->update([
+ 'status' => self::STATUS_FAILED,
+ 'verifyTime' => time()
+ ]);
+
+ Log::warning("群创建失败,准备重试。工作台ID: {$workbenchId}, 微信账号ID: {$wechatAccountId}");
+
+ // 检查重试次数
+ $failedItems = Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('wechatAccountId', $wechatAccountId)
+ ->where('createTime', '>=', $createTime - 10)
+ ->where('createTime', '<=', $createTime + 10)
+ ->select();
+
+ $maxRetryCount = 0;
+ foreach ($failedItems as $item) {
+ if ($item['retryCount'] >= 3) {
+ Log::error("群创建重试次数已达上限,放弃重试。工作台ID: {$workbenchId}, 微信账号ID: {$wechatAccountId}");
+ $job->delete();
+ return;
+ }
+ $maxRetryCount = max($maxRetryCount, $item['retryCount']);
+ }
+
+ // 增加重试次数并重置状态
+ Db::name('workbench_group_create_item')
+ ->where('workbenchId', $workbenchId)
+ ->where('wechatAccountId', $wechatAccountId)
+ ->where('createTime', '>=', $createTime - 10)
+ ->where('createTime', '<=', $createTime + 10)
+ ->update([
+ 'status' => self::STATUS_CREATING,
+ 'retryCount' => Db::raw('retryCount + 1')
+ ]);
+
+ // 重新创建建群任务(延迟10秒)
+ Queue::later(10, 'app\job\WorkbenchGroupCreateRetryJob', [
+ 'workbenchId' => $workbenchId,
+ 'wechatAccountId' => $wechatAccountId,
+ 'createTime' => $createTime
+ ], 'default');
+
+ $job->delete();
+ }
+}
+
diff --git a/application/job/WorkbenchGroupPushJob.php b/application/job/WorkbenchGroupPushJob.php
new file mode 100644
index 0000000..b14c266
--- /dev/null
+++ b/application/job/WorkbenchGroupPushJob.php
@@ -0,0 +1,1045 @@
+logJobStart($jobId, $queueLockKey);
+ $this->execute();
+ $this->handleJobSuccess($job, $queueLockKey);
+ return true;
+ } catch (\Exception $e) {
+ return $this->handleJobError($e, $job, $queueLockKey);
+ }
+ }
+
+ /**
+ * 执行任务
+ * @throws \Exception
+ */
+ public function execute()
+ {
+ try {
+ // 获取所有工作台
+ $workbenches = Workbench::where(['status' => 1, 'type' => 3, 'isDel' => 0,'id' => 264])->order('id desc')->select();
+ foreach ($workbenches as $workbench) {
+ // 获取工作台配置
+ $configModel = WorkbenchGroupPush::where('workbenchId', $workbench->id)->find();
+ if (!$configModel) {
+ continue;
+ }
+
+ // 标准化配置
+ $config = $this->normalizeConfig($configModel->toArray());
+ if ($config === false) {
+ Log::warning("消息群发:配置无效,工作台ID: {$workbench->id}");
+ continue;
+ }
+
+ //判断是否推送
+ $isPush = $this->isPush($workbench, $config);
+ if (empty($isPush)) {
+ continue;
+ }
+
+ $targetType = intval($config['targetType']);
+ $groupPushSubType = intval($config['groupPushSubType']);
+
+ // 如果是群推送且是群公告,暂时跳过(晚点处理)
+ if ($targetType == 1 && $groupPushSubType == 2) {
+ Log::info("群公告功能暂未实现,工作台ID: {$workbench->id}");
+ continue;
+ }
+
+ // 获取内容库(群群发需要内容库,好友推送也需要内容库)
+ $contentLibrary = $this->getContentLibrary($workbench, $config);
+ if (empty($contentLibrary)) {
+ continue;
+ }
+ // 处理内容发送
+ $this->sendMsgToGroup($workbench, $config, $contentLibrary);
+ }
+ } catch (\Exception $e) {
+ Log::error("消息群发任务异常: " . $e->getMessage());
+ throw $e;
+ }
+ }
+
+
+ // 发送消息(支持群推送和好友推送)
+ public function sendMsgToGroup($workbench, $config, $msgConf)
+ {
+ // 消息拼接 msgType(1:文本 3:图片 43:视频 47:动图表情包(gif、其他表情包) 49:小程序/其他:图文、文件)
+ // 当前,type 为文本、图片、动图表情包的时候,content为string, 其他情况为对象 {type: 'file/link/...', url: '', title: '', thunmbPath: '', desc: ''}
+
+ $targetType = intval($config['targetType']); // 默认1=群推送
+
+ $toAccountId = '';
+ $username = Env::get('api.username', '');
+ $password = Env::get('api.password', '');
+ if (!empty($username) || !empty($password)) {
+ $toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId');
+ }
+ // 建立WebSocket
+ $wsController = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]);
+ $ownerWechatIds = $config['ownerWechatIds'] ?? $this->getOwnerWechatIds($config);
+ if ($targetType == 1) {
+ // 群推送
+ $this->sendToGroups($workbench, $config, $msgConf, $wsController, $ownerWechatIds);
+ } else {
+ // 好友推送
+ $this->sendToFriends($workbench, $config, $msgConf, $wsController, $ownerWechatIds);
+ }
+ }
+
+ /**
+ * 发送群消息
+ */
+ protected function sendToGroups($workbench, $config, $msgConf, $wsController, array $ownerWechatIds = [])
+ {
+ // 获取群推送子类型:1=群群发,2=群公告
+ $groupPushSubType = intval($config['groupPushSubType'] ?? 1); // 默认1=群群发
+
+ // 如果是群公告,暂时跳过(晚点处理)
+ if ($groupPushSubType == 2) {
+ Log::info("群公告功能暂未实现,工作台ID: {$workbench['id']}");
+ return false;
+ }
+
+ // 群群发:从groups字段获取群ID列表
+ $groups = $config['groups'] ?? [];
+ if (empty($groups)) {
+ Log::warning("群群发:未选择微信群,工作台ID: {$workbench['id']}");
+ return false;
+ }
+
+ $query = Db::name('wechat_group')
+ ->whereIn('id', $groups);
+
+ if (!empty($ownerWechatIds)) {
+ $query->whereIn('wechatAccountId', $ownerWechatIds);
+ }
+
+ $groupsData = $query
+ ->field('id,wechatAccountId,chatroomId,companyId,ownerWechatId')
+ ->select();
+ if (empty($groupsData)) {
+ Log::warning("群群发:未找到微信群数据,工作台ID: {$workbench['id']}");
+ return false;
+ }
+
+ foreach ($msgConf as $content) {
+ $sqlData = [];
+
+ foreach ($groupsData as $group) {
+ // 构建发送数据
+ $sendData = $this->buildSendData($content, $config, $group['wechatAccountId'], $group['id'], 'group');
+ if (empty($sendData)) {
+ continue;
+ }
+
+ //发送消息
+ foreach ($sendData as $send) {
+ $wsController->sendCommunity($send);
+ }
+
+ // 准备插入发送记录
+ $sqlData[] = [
+ 'workbenchId' => $workbench['id'],
+ 'contentId' => $content['id'],
+ 'groupId' => $group['id'],
+ 'friendId' => null,
+ 'targetType' => 1,
+ 'wechatAccountId' => $group['wechatAccountId'],
+ 'createTime' => time()
+ ];
+ }
+
+ // 批量插入发送记录
+ if (!empty($sqlData)) {
+ Db::name('workbench_group_push_item')->insertAll($sqlData);
+ Log::info("群群发:推送了" . count($sqlData) . "个群,工作台ID: {$workbench['id']}");
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * 发送好友消息
+ */
+ protected function sendToFriends($workbench, $config, $msgConf, $wsController, array $ownerWechatIds = [])
+ {
+ $friends = $config['friends'] ?? [];
+ $trafficPools = $config['trafficPools'] ?? [];
+ $devices = $config['devices'] ?? [];
+
+ $friendsData = [];
+
+ // 指定好友
+ if (!empty($friends)) {
+ $friendsData = array_merge($friendsData, $this->getFriendsByIds($friends, $ownerWechatIds));
+ }
+
+ // 流量池好友
+ if (!empty($trafficPools)) {
+ $friendsData = array_merge($friendsData, $this->getFriendsByTrafficPools($trafficPools, $workbench, $ownerWechatIds));
+ }
+
+ // 如果未选择好友或流量池,则根据设备查询所有好友
+ if (empty($friendsData)) {
+ if (empty($devices)) {
+ Log::warning('好友推送:未选择好友或流量池,且未选择设备,无法推送');
+ return false;
+ }
+ $friendsData = $this->getFriendsByDevices($devices, $ownerWechatIds);
+ }
+ $friendsData = $this->deduplicateFriends($friendsData);
+ if (empty($friendsData)) {
+ return false;
+ }
+
+ // 获取已推送的好友ID列表(不限制时间范围,避免重复推送)
+ $sentFriendIds = Db::name('workbench_group_push_item')
+ ->where('workbenchId', $workbench->id)
+ ->where('targetType', 2)
+ ->column('friendId');
+ $sentFriendIds = array_unique(array_filter($sentFriendIds));
+
+ // 过滤掉所有已推送的好友
+ $friendsData = array_filter($friendsData, function($friend) use ($sentFriendIds) {
+ return !in_array($friend['id'], $sentFriendIds);
+ });
+
+ if (empty($friendsData)) {
+ Log::info('好友推送:所有好友都已推送过');
+ return false;
+ }
+
+ // 重新索引数组
+ $friendsData = array_values($friendsData);
+
+ // 计算剩余可推送人数(基于累计推送人数)
+ $sentFriendCount = count($sentFriendIds);
+ $maxPerDay = intval($config['maxPerDay']);
+ $remainingCount = $maxPerDay - $sentFriendCount;
+
+ if ($remainingCount <= 0) {
+ Log::info('好友推送:累计推送人数已达上限');
+ return false;
+ }
+
+ // 限制本次推送人数(不超过剩余可推送人数)
+ $friendsData = array_slice($friendsData, 0, $remainingCount);
+
+ // 批量处理:每批最多500人
+ $batchSize = 500;
+ $batches = array_chunk($friendsData, $batchSize);
+
+ foreach ($msgConf as $content) {
+ foreach ($batches as $batchIndex => $batch) {
+ $sqlData = [];
+
+ foreach ($batch as $friend) {
+ // 构建发送数据
+ $sendData = $this->buildSendData($content, $config, $friend['wechatAccountId'], $friend['id'], 'friend');
+
+ if (empty($sendData)) {
+ continue;
+ }
+
+ // 发送个人消息
+ foreach ($sendData as $send) {
+ if ($send['msgType'] == 49){
+ $sendContent = json_encode($send['content'], 256);
+ } else {
+ $sendContent = $send['content'];
+ }
+ $wsController->sendPersonal([
+ 'wechatFriendId' => $friend['id'],
+ 'wechatAccountId' => $friend['wechatAccountId'],
+ 'msgType' => $send['msgType'],
+ 'content' => $sendContent,
+ ]);
+ }
+
+ // 准备插入发送记录
+ $sqlData[] = [
+ 'workbenchId' => $workbench['id'],
+ 'contentId' => $content['id'],
+ 'groupId' => null,
+ 'friendId' => $friend['id'],
+ 'targetType' => 2,
+ 'wechatAccountId' => $friend['wechatAccountId'],
+ 'createTime' => time()
+ ];
+ }
+
+ // 批量插入发送记录
+ if (!empty($sqlData)) {
+ Db::name('workbench_group_push_item')->insertAll($sqlData);
+ Log::info("好友推送:第" . ($batchIndex + 1) . "批,推送了" . count($sqlData) . "个好友");
+ }
+
+ // 如果不是最后一批,等待一下再处理下一批(避免一次性推送太多)
+ if ($batchIndex < count($batches) - 1) {
+ sleep(1); // 等待1秒
+ }
+ }
+ }
+ }
+
+ /**
+ * 构建发送数据
+ */
+ protected function buildSendData($content, $config, $wechatAccountId, $targetId, $type = 'group')
+ {
+ $sendData = [];
+
+ // 内容处理
+ if (!empty($content['content'])) {
+ // 京东转链
+ if (!empty($config['promotionSiteId'])) {
+ $WorkbenchController = new WorkbenchController();
+ $jdLink = $WorkbenchController->changeLink($content['content'], $config['promotionSiteId']);
+ $jdLink = json_decode($jdLink, true);
+ if ($jdLink['code'] == 200) {
+ $content['content'] = $jdLink['data'];
+ }
+ }
+
+ if ($type == 'group') {
+ $sendData[] = [
+ 'content' => $content['content'],
+ 'msgType' => 1,
+ 'wechatAccountId' => $wechatAccountId,
+ 'wechatChatroomId' => $targetId,
+ ];
+ } else {
+ $sendData[] = [
+ 'content' => $content['content'],
+ 'msgType' => 1,
+ ];
+ }
+ }
+
+ // 根据内容类型处理
+ switch ($content['contentType']) {
+ case 1:
+ // 图片解析
+ $imgs = json_decode($content['resUrls'], true);
+ if (!empty($imgs)) {
+ foreach ($imgs as $img) {
+ if ($type == 'group') {
+ $sendData[] = [
+ 'content' => $img,
+ 'msgType' => 3,
+ 'wechatAccountId' => $wechatAccountId,
+ 'wechatChatroomId' => $targetId,
+ ];
+ } else {
+ $sendData[] = [
+ 'content' => $img,
+ 'msgType' => 3,
+ ];
+ }
+ }
+ }
+ break;
+ case 2:
+ // 链接解析
+ $url = json_decode($content['urls'], true);
+ if (!empty($url[0])) {
+ $url = $url[0];
+ $linkContent = [
+ 'desc' => $url['desc'],
+ 'thumbPath' => $url['image'],
+ 'title' => $url['desc'],
+ 'type' => 'link',
+ 'url' => $url['url'],
+ ];
+ if ($type == 'group') {
+ $sendData[] = [
+ 'content' => $linkContent,
+ 'msgType' => 49,
+ 'wechatAccountId' => $wechatAccountId,
+ 'wechatChatroomId' => $targetId,
+ ];
+ } else {
+ $sendData[] = [
+ 'content' => $linkContent,
+ 'msgType' => 49,
+ ];
+ }
+ }
+ break;
+ case 3:
+ // 视频解析
+ $video = json_decode($content['resUrls'], true);
+ if (!empty($video)) {
+ $video = $video[0];
+ }
+ if ($type == 'group') {
+ $sendData[] = [
+ 'content' => $video,
+ 'msgType' => 43,
+ 'wechatAccountId' => $wechatAccountId,
+ 'wechatChatroomId' => $targetId,
+ ];
+ } else {
+ $sendData[] = [
+ 'content' => $video,
+ 'msgType' => 43,
+ ];
+ }
+ break;
+ }
+
+ return $sendData;
+ }
+
+ /**
+ * 根据好友ID获取好友信息
+ * @param array $friendIds
+ * @return array
+ */
+ protected function getFriendsByIds(array $friendIds, array $ownerWechatIds = [])
+ {
+ if (empty($friendIds)) {
+ return [];
+ }
+ $query = Db::table('s2_wechat_friend')
+ ->whereIn('id', $friendIds)
+ ->where('isDeleted', 0);
+
+ if (!empty($ownerWechatIds)) {
+ $query->whereIn('wechatAccountId', $ownerWechatIds);
+ }
+
+ $friends = $query
+ ->field('id,wechatAccountId,wechatId,ownerWechatId')
+ ->select();
+ if ($friends === false) {
+ return [];
+ }
+
+ return $friends;
+ }
+
+ /**
+ * 根据设备获取好友信息
+ * @param array $deviceIds
+ * @return array
+ */
+ protected function getFriendsByDevices(array $deviceIds, array $ownerWechatIds = [])
+ {
+ if (empty($deviceIds)) {
+ return [];
+ }
+
+ $query = Db::table('s2_company_account')
+ ->alias('ca')
+ ->join(['s2_wechat_account' => 'wa'], 'ca.id = wa.deviceAccountId')
+ ->join(['s2_wechat_friend' => 'wf'], 'wf.wechatAccountId = wa.id')
+ ->where([
+ 'ca.status' => 0,
+ 'wf.isDeleted' => 0,
+ 'wa.deviceAlive' => 1,
+ 'wa.wechatAlive' => 1
+ ])
+ ->whereIn('wa.currentDeviceId', $deviceIds);
+
+ if (!empty($ownerWechatIds)) {
+ $query->whereIn('wf.wechatAccountId', $ownerWechatIds);
+ }
+
+ $friends = $query
+ ->field('wf.id,wf.wechatAccountId,wf.wechatId,wf.ownerWechatId')
+ ->group('wf.id')
+ ->select();
+
+ if ($friends === false) {
+ return [];
+ }
+
+ return $friends->toArray();
+ }
+
+ /**
+ * 根据流量池获取好友信息
+ * @param array $trafficPools
+ * @param Workbench $workbench
+ * @return array
+ */
+ protected function getFriendsByTrafficPools(array $trafficPools, $workbench, array $ownerWechatIds = [])
+ {
+ if (empty($trafficPools)) {
+ return [];
+ }
+
+ $companyId = $workbench->companyId ?? 0;
+
+ $query = Db::name('traffic_source_package_item')
+ ->alias('tspi')
+ ->leftJoin('traffic_source_package tsp', 'tsp.id = tspi.packageId')
+ ->leftJoin('traffic_pool tp', 'tp.identifier = tspi.identifier')
+ ->leftJoin(['s2_wechat_friend' => 'wf'], 'wf.wechatId = tp.wechatId')
+ ->leftJoin(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId')
+ ->whereIn('tspi.packageId', $trafficPools)
+ ->where('tsp.isDel', 0)
+ ->where('wf.isDeleted', 0)
+ ->whereNotNull('wf.id')
+ ->whereNotNull('wf.wechatAccountId')
+ ->where(function ($query) use ($companyId) {
+ $query->whereIn('tsp.companyId', [$companyId, 0]);
+ })
+ ->where(function ($query) use ($companyId) {
+ $query->whereIn('tspi.companyId', [$companyId, 0]);
+ });
+
+ if (!empty($ownerWechatIds)) {
+ $query->whereIn('wf.wechatAccountId', $ownerWechatIds);
+ }
+
+ $friends = $query
+ ->field('wf.id,wf.wechatAccountId,wf.wechatId,wf.ownerWechatId')
+ ->group('wf.id')
+ ->select();
+
+ if (empty($friends)) {
+ Log::info('好友推送:流量池未匹配到好友');
+ return [];
+ }
+
+ if ($friends === false) {
+ return [];
+ }
+
+ return $friends;
+ }
+
+ /**
+ * 标准化群推送配置
+ * @param array $config
+ * @return array|false
+ */
+ protected function normalizeConfig(array $config)
+ {
+ $config['targetType'] = intval($config['targetType'] ?? 1);
+ $config['groupPushSubType'] = intval($config['groupPushSubType'] ?? 1);
+ if (!in_array($config['groupPushSubType'], [1, 2], true)) {
+ $config['groupPushSubType'] = 1;
+ }
+
+ $config['pushType'] = !empty($config['pushType']) ? 1 : 0;
+ $config['status'] = !empty($config['status']) ? 1 : 0;
+ $config['isLoop'] = !empty($config['isLoop']) ? 1 : 0;
+
+ $config['startTime'] = $this->normalizeTimeString($config['startTime'] ?? '00:00');
+ $config['endTime'] = $this->normalizeTimeString($config['endTime'] ?? '23:59');
+ $config['maxPerDay'] = max(0, intval($config['maxPerDay'] ?? 0));
+
+ $config['friendIntervalMin'] = max(0, intval($config['friendIntervalMin'] ?? 0));
+ $config['friendIntervalMax'] = max(0, intval($config['friendIntervalMax'] ?? $config['friendIntervalMin']));
+ if ($config['friendIntervalMin'] > $config['friendIntervalMax']) {
+ $config['friendIntervalMax'] = $config['friendIntervalMin'];
+ }
+
+ $config['messageIntervalMin'] = max(0, intval($config['messageIntervalMin'] ?? 0));
+ $config['messageIntervalMax'] = max(0, intval($config['messageIntervalMax'] ?? $config['messageIntervalMin']));
+ if ($config['messageIntervalMin'] > $config['messageIntervalMax']) {
+ $config['messageIntervalMax'] = $config['messageIntervalMin'];
+ }
+
+ $config['ownerWechatIds'] = $this->deduplicateIds($this->jsonToArray($config['ownerWechatIds'] ?? []));
+ $config['groups'] = $this->deduplicateIds($this->jsonToArray($config['groups'] ?? []));
+ $config['friends'] = $this->deduplicateIds($this->jsonToArray($config['friends'] ?? []));
+ $config['trafficPools'] = $this->deduplicateIds($this->jsonToArray($config['trafficPools'] ?? []));
+ $config['devices'] = $this->deduplicateIds($this->jsonToArray($config['devices'] ?? []));
+ $config['contentLibraries'] = $this->deduplicateIds($this->jsonToArray($config['contentLibraries'] ?? []));
+ $config['postPushTags'] = $this->deduplicateIds($this->jsonToArray($config['postPushTags'] ?? []));
+
+ return $config;
+ }
+
+ /**
+ * 将混合类型转换为数组
+ * @param mixed $value
+ * @return array
+ */
+ protected function jsonToArray($value): array
+ {
+ if (empty($value)) {
+ return [];
+ }
+
+ if (is_array($value)) {
+ return $value;
+ }
+
+ if (is_string($value)) {
+ $decoded = json_decode($value, true);
+ if (json_last_error() === JSON_ERROR_NONE) {
+ return is_array($decoded) ? $decoded : [];
+ }
+ }
+
+ return [];
+ }
+
+ /**
+ * 归一化时间字符串,保留到分钟
+ * @param string $time
+ * @return string
+ */
+ protected function normalizeTimeString(string $time): string
+ {
+ if (empty($time)) {
+ return '00:00';
+ }
+ $parts = explode(':', $time);
+ $hour = str_pad(intval($parts[0] ?? 0), 2, '0', STR_PAD_LEFT);
+ $minute = str_pad(intval($parts[1] ?? 0), 2, '0', STR_PAD_LEFT);
+ return "{$hour}:{$minute}";
+ }
+
+ /**
+ * 对ID数组进行去重并清理无效值
+ * @param array $ids
+ * @return array
+ */
+ protected function deduplicateIds(array $ids)
+ {
+ if (empty($ids)) {
+ return [];
+ }
+
+ $normalized = array_map(function ($value) {
+ if (is_array($value) && isset($value['id'])) {
+ return $value['id'];
+ }
+ if (is_object($value) && isset($value->id)) {
+ return $value->id;
+ }
+ return $value;
+ }, $ids);
+
+ $filtered = array_filter($normalized, function ($value) {
+ return $value !== null && $value !== '';
+ });
+
+ if (empty($filtered)) {
+ return [];
+ }
+
+ return array_values(array_unique($filtered));
+ }
+
+ /**
+ * 对内容列表根据内容ID去重
+ * @param mixed $contents
+ * @return array
+ */
+ protected function deduplicateContentList($contents)
+ {
+ if (empty($contents)) {
+ return [];
+ }
+
+ if ($contents instanceof \think\Collection || $contents instanceof \think\model\Collection) {
+ $contents = $contents->toArray();
+ } elseif ($contents instanceof \think\Model) {
+ $contents = [$contents->toArray()];
+ }
+
+ if (!is_array($contents)) {
+ return [];
+ }
+
+ $result = [];
+ $unique = [];
+
+ foreach ($contents as $content) {
+ if ($content instanceof \think\Model) {
+ $content = $content->toArray();
+ } elseif (is_object($content)) {
+ $content = (array)$content;
+ }
+
+ if (!is_array($content)) {
+ continue;
+ }
+
+ $contentId = $content['id'] ?? null;
+ if (empty($contentId) || isset($unique[$contentId])) {
+ continue;
+ }
+
+ $unique[$contentId] = true;
+ $result[] = $content;
+ }
+
+ return $result;
+ }
+
+ /**
+ * 对好友数据进行去重
+ * @param array $friends
+ * @return array
+ */
+ protected function deduplicateFriends(array $friends)
+ {
+ if (empty($friends)) {
+ return [];
+ }
+
+ $unique = [];
+ $result = [];
+
+ foreach ($friends as $friend) {
+ if (empty($friend['id'])) {
+ continue;
+ }
+ if (isset($unique[$friend['id']])) {
+ continue;
+ }
+ $unique[$friend['id']] = true;
+ $result[] = $friend;
+ }
+
+ return $result;
+ }
+
+ /**
+ * 获取配置中的客服微信ID列表
+ * @param array $config
+ * @return array
+ */
+ protected function getOwnerWechatIds($config)
+ {
+ if (empty($config['ownerWechatIds'])) {
+ return [];
+ }
+
+ $ownerWechatIds = $config['ownerWechatIds'];
+
+ if (is_string($ownerWechatIds)) {
+ $decoded = json_decode($ownerWechatIds, true);
+ if (json_last_error() === JSON_ERROR_NONE) {
+ $ownerWechatIds = $decoded;
+ }
+ }
+
+ if (!is_array($ownerWechatIds)) {
+ return [];
+ }
+
+ $ownerWechatIds = array_map(function ($id) {
+ return is_numeric($id) ? intval($id) : $id;
+ }, $ownerWechatIds);
+
+ return $this->deduplicateIds($ownerWechatIds);
+ }
+
+
+ /**
+ * 记录发送历史
+ * @param Workbench $workbench
+ * @param array $devices
+ * @param array $contentLibrary
+ */
+ protected function recordSendHistory($workbench, $devices, $contentLibrary)
+ {
+ $now = time();
+ $data = [];
+ foreach ($devices as $device) {
+ $data = [
+ 'workbenchId' => $workbench->id,
+ 'deviceId' => $device['deviceId'],
+ 'contentId' => $contentLibrary['id'],
+ 'wechatAccountId' => $device['wechatAccountId'],
+ 'createTime' => $now,
+ ];
+ Db::name('workbench_group_push_item')->insert($data);
+ }
+
+ }
+
+ /**
+ * 判断是否推送
+ * @param Workbench $workbench 工作台
+ * @param array $config 配置
+ * @return bool
+ */
+ protected function isPush($workbench, $config)
+ {
+ // 检查发送间隔(新逻辑:根据startTime、endTime、maxPerDay动态计算)
+ $today = date('Y-m-d');
+ $startTimestamp = strtotime($today . ' ' . $config['startTime'] . ':00');
+ $endTimestamp = strtotime($today . ' ' . $config['endTime'] . ':00');
+
+ // 如果时间不符,则跳过
+ if (($startTimestamp > time() || $endTimestamp < time()) && empty($config['pushType'])) {
+ return false;
+ }
+
+ $totalSeconds = $endTimestamp - $startTimestamp;
+ if ($totalSeconds <= 0 || empty($config['maxPerDay'])) {
+ return false;
+ }
+
+ $targetType = intval($config['targetType']); // 默认1=群推送
+
+ if ($targetType == 2) {
+ // 好友推送:maxPerDay表示每日推送人数
+ // 查询已推送的好友ID列表(去重)
+ $sentFriendIds = Db::name('workbench_group_push_item')
+ ->where('workbenchId', $workbench->id)
+ ->where('targetType', 2)
+ ->column('friendId');
+ $sentFriendIds = array_filter($sentFriendIds); // 过滤null值
+ $count = count(array_unique($sentFriendIds)); // 去重后统计累计推送人数
+
+ if ($count >= $config['maxPerDay']) {
+ return false;
+ }
+
+ // 计算本次同步的最早允许时间(基于好友/消息间隔配置)
+ $friendIntervalMin = max(0, intval($config['friendIntervalMin'] ?? 0));
+ $messageIntervalMin = max(0, intval($config['messageIntervalMin'] ?? 0));
+ $minInterval = max(1, $friendIntervalMin + $messageIntervalMin);
+
+ $lastSendTime = Db::name('workbench_group_push_item')
+ ->where('workbenchId', $workbench->id)
+ ->where('targetType', 2)
+ ->order('id', 'desc')
+ ->value('createTime');
+
+ if (!empty($lastSendTime) && (time() - $lastSendTime) < $minInterval) {
+ return false;
+ }
+ } else {
+ // 群推送:maxPerDay表示每日推送次数
+ $interval = floor($totalSeconds / $config['maxPerDay']);
+
+ // 查询今日已同步次数
+ $count = Db::name('workbench_group_push_item')
+ ->where('workbenchId', $workbench->id)
+ ->where('targetType', 1)
+ ->whereTime('createTime', 'between', [$startTimestamp, $endTimestamp])
+ ->count();
+ if ($count >= $config['maxPerDay']) {
+ return false;
+ }
+
+ // 计算本次同步的最早允许时间
+ $nextSyncTime = $startTimestamp + $count * $interval;
+ if (time() < $nextSyncTime) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * 获取内容库
+ * @param Workbench $workbench 工作台
+ * @param array $config 配置
+ * @return array|bool
+ */
+ protected function getContentLibrary($workbench, $config)
+ {
+ $targetType = intval($config['targetType']); // 默认1=群推送
+ $groupPushSubType = intval($config['groupPushSubType']); // 默认1=群群发
+
+ // 如果是群公告,不需要内容库(晚点处理)
+ if ($targetType == 1 && $groupPushSubType == 2) {
+ return false;
+ }
+
+ $contentids = $config['contentLibraries'] ?? [];
+ if (empty($contentids)) {
+ Log::warning("未选择内容库,工作台ID: {$workbench->id}");
+ return false;
+ }
+
+ if ($config['pushType'] == 1) {
+ $limit = 10;
+ } else {
+ $limit = 1;
+ }
+
+ //推送顺序
+ if ($config['pushOrder'] == 1) {
+ $order = 'ci.sendTime desc, ci.id asc';
+ } else {
+ $order = 'ci.sendTime desc, ci.id desc';
+ }
+
+ // 基础查询,根据targetType过滤记录
+ $query = Db::name('content_library')->alias('cl')
+ ->join('content_item ci', 'ci.libraryId = cl.id')
+ ->join('workbench_group_push_item wgpi', 'wgpi.contentId = ci.id and wgpi.workbenchId = ' . $workbench->id . ' and wgpi.targetType = ' . $targetType, 'left')
+ ->where(['cl.isDel' => 0, 'ci.isDel' => 0])
+ ->where('ci.sendTime <= ' . (time() + 60))
+ ->whereIn('cl.id', $contentids)
+ ->field([
+ 'ci.id',
+ 'ci.libraryId',
+ 'ci.contentType',
+ 'ci.title',
+ 'ci.content',
+ 'ci.resUrls',
+ 'ci.urls',
+ 'ci.comment',
+ 'ci.sendTime'
+ ]);
+ // 复制 query
+ $query2 = clone $query;
+ $query3 = clone $query;
+ // 根据isLoop处理不同的发送逻辑
+ if ($config['isLoop'] == 1) {
+ // 可以循环发送(只有群推送时才能为1)
+ // 1. 优先获取未发送的内容
+ $unsentContent = $this->deduplicateContentList(
+ $query->where('wgpi.id', 'null')
+ ->order($order)
+ ->limit(0, $limit)
+ ->select()
+ );
+ if (!empty($unsentContent)) {
+ return $unsentContent;
+ }
+ $lastSendData = Db::name('workbench_group_push_item')
+ ->where('workbenchId', $workbench->id)
+ ->where('targetType', $targetType)
+ ->order('id desc')
+ ->find();
+ $fastSendData = Db::name('workbench_group_push_item')
+ ->where('workbenchId', $workbench->id)
+ ->where('targetType', $targetType)
+ ->order('id asc')
+ ->find();
+
+ if (empty($lastSendData) || empty($fastSendData)) {
+ return [];
+ }
+
+ $sentContent = $this->deduplicateContentList(
+ $query2->where('wgpi.contentId', '<', $lastSendData['contentId'])
+ ->order('wgpi.id ASC')
+ ->group('wgpi.contentId')
+ ->limit(0, $limit)
+ ->select()
+ );
+
+ if (empty($sentContent)) {
+ $sentContent = $this->deduplicateContentList(
+ $query3->where('wgpi.contentId', '=', $fastSendData['contentId'])
+ ->order('wgpi.id ASC')
+ ->group('wgpi.contentId')
+ ->limit(0, $limit)
+ ->select()
+ );
+ }
+ return $sentContent;
+ } else {
+ // 不能循环发送,只获取未发送的内容(好友推送时isLoop=0)
+ $list = $this->deduplicateContentList(
+ $query->where('wgpi.id', 'null')
+ ->order($order)
+ ->limit(0, $limit)
+ ->select()
+ );
+ return $list;
+ }
+ }
+
+ /**
+ * 记录任务开始
+ * @param string $jobId
+ * @param string $queueLockKey
+ */
+ protected function logJobStart($jobId, $queueLockKey)
+ {
+ Log::info('开始处理工作台消息群发任务: ' . json_encode([
+ 'jobId' => $jobId,
+ 'queueLockKey' => $queueLockKey
+ ]));
+ }
+
+ /**
+ * 处理任务成功
+ * @param Job $job
+ * @param string $queueLockKey
+ */
+ protected function handleJobSuccess($job, $queueLockKey)
+ {
+ $job->delete();
+ Cache::rm($queueLockKey);
+ Log::info('工作台消息群发任务执行成功');
+ }
+
+ /**
+ * 处理任务错误
+ * @param \Exception $e
+ * @param Job $job
+ * @param string $queueLockKey
+ * @return bool
+ */
+ protected function handleJobError(\Exception $e, $job, $queueLockKey)
+ {
+ Log::error('工作台消息群发任务异常:' . $e->getMessage());
+
+ if (!empty($queueLockKey)) {
+ Cache::rm($queueLockKey);
+ Log::info("由于异常释放队列锁: {$queueLockKey}");
+ }
+
+ if ($job->attempts() > self::MAX_RETRY_ATTEMPTS) {
+ $job->delete();
+ } else {
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/application/job/WorkbenchImportContactJob.php b/application/job/WorkbenchImportContactJob.php
new file mode 100644
index 0000000..a8edfba
--- /dev/null
+++ b/application/job/WorkbenchImportContactJob.php
@@ -0,0 +1,399 @@
+logJobStart($jobId, $queueLockKey);
+ $this->execute();
+ $this->handleJobSuccess($job, $queueLockKey);
+ return true;
+ } catch (\Exception $e) {
+ return $this->handleJobError($e, $job, $queueLockKey);
+ }
+ }
+
+ /**
+ * 执行任务
+ * @throws \Exception
+ */
+ public function execute()
+ {
+ try {
+ // 获取所有启用的通讯录导入工作台
+ $workbenches = Workbench::where(['status' => 1, 'type' => 6, 'isDel' => 0])->order('id desc')->select();
+ foreach ($workbenches as $workbench) {
+ // 获取工作台配置
+ $config = WorkbenchImportContact::where('workbenchId', $workbench->id)->find();
+ if (!$config) {
+ continue;
+ }
+
+ // 判断是否需要导入
+ $shouldImport = $this->shouldImport($workbench, $config);
+ if (!$shouldImport) {
+ continue;
+ }
+
+ // 获取需要导入的设备列表
+ $devices = $this->getDeviceList($workbench, $config);
+ if (empty($devices)) {
+ continue;
+ }
+
+ // 获取通讯录数据
+ $contactData = $this->getContactFromDatabase($workbench, $config);
+ if (empty($contactData)) {
+ continue;
+ }
+
+ // 执行通讯录导入
+ $this->importContactToDevices($workbench, $config, $devices, $contactData);
+ }
+ } catch (\Exception $e) {
+ Log::error("通讯录导入任务异常: " . $e->getMessage());
+ throw $e;
+ }
+ }
+
+ /**
+ * 导入通讯录到设备
+ * @param Workbench $workbench
+ * @param WorkbenchImportContact $config
+ * @param array $devices
+ * @param array $contactData
+ */
+ public function importContactToDevices($workbench, $config, $devices, $contactData)
+ {
+ $deviceController = new DeviceController();
+
+ // 根据设备数量平分通讯录数据
+ $deviceCount = count($devices);
+ if ($deviceCount == 0) {
+ Log::warning("没有可用设备进行通讯录导入");
+ return;
+ }
+
+ $contactCount = count($contactData);
+ if ($contactCount == 0) {
+ Log::warning("没有通讯录数据需要导入");
+ return;
+ }
+
+ // 计算每个设备分配的联系人数量
+ $contactsPerDevice = ceil($contactCount / $deviceCount);
+ foreach ($devices as $index => $device) {
+ try {
+ // 计算当前设备的联系人数据范围
+ $startIndex = $index * $contactsPerDevice;
+ $endIndex = min($startIndex + $contactsPerDevice, $contactCount);
+
+ // 如果起始索引超出范围,跳过
+ if ($startIndex >= $contactCount) {
+ continue;
+ }
+
+ // 获取当前设备的联系人数据片段
+ $deviceContactData = array_slice($contactData, $startIndex, $endIndex - $startIndex);
+
+ if (empty($deviceContactData)) {
+ continue;
+ }
+ // 准备联系人数据
+ $contactJson = $this->formatContactData($deviceContactData, $config);
+
+ // 调用设备控制器的导入联系人方法
+ $result = $deviceController->importContact([
+ 'deviceId' => $device['deviceId'],
+ 'contactJson' => $contactJson,
+ 'clearContact' => $config['clearContact'] ?? false
+ ], true);
+
+ $resultData = json_decode($result, true);
+
+ // 记录导入历史
+ $this->recordImportHistory($workbench, $device, $deviceContactData);
+
+ if ($resultData['code'] == 200) {
+ Log::info("设备 {$device['deviceId']} 通讯录导入成功,导入联系人数量: " . count($deviceContactData));
+ } else {
+ Log::error("设备 {$device['deviceId']} 通讯录导入失败: " . ($resultData['msg'] ?? '未知错误'));
+ }
+
+ // 添加延迟,避免频繁请求
+ if ($config['importInterval'] ?? 0 > 0) {
+ sleep($config['importInterval']);
+ }
+
+ } catch (\Exception $e) {
+ Log::error("设备 {$device['deviceId']} 通讯录导入异常: " . $e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * 格式化联系人数据
+ * @param array $contactData
+ * @param WorkbenchImportContact $config
+ * @return array|string
+ */
+ protected function formatContactData($contactData, $config)
+ {
+ $remarkType = $config['remarkType'] ?? 0;
+ $remark = $config['remark'] ?? '';
+
+ // 根据remarkType添加备注
+ $suffix = '';
+ switch ($remarkType) {
+ case 0:
+ // 不添加备注
+ $suffix = '';
+ break;
+ case 1:
+ // 添加年月日
+ $suffix = date('Ymd') . '_';
+ break;
+ case 2:
+ // 添加月日
+ $suffix = date('md') . '_';
+ break;
+ case 3:
+ // 自定义备注
+ $suffix = $remark . '_';
+ break;
+ default:
+ $suffix = '';
+ break;
+ }
+ // 返回数组格式
+ $contacts = [];
+ foreach ($contactData as $contact) {
+ $name = !empty($contact['name']) ? trim($contact['name']) : trim($contact['phone']);
+ if (!empty($suffix)) {
+ $name = $suffix . $name;
+ }
+ $contacts[] = [
+ 'name' => $name,
+ 'phone' => trim($contact['phone'])
+ ];
+ }
+ return $contacts;
+
+ }
+
+ /**
+ * 记录导入历史
+ * @param Workbench $workbench
+ * @param array $device
+ * @param array $contactData
+ * @param array $result
+ */
+ protected function recordImportHistory($workbench, $device, $contactData)
+ {
+ $data = [];
+ foreach ($contactData as $v){
+ $data[] = [
+ 'workbenchId' => $workbench->id,
+ 'deviceId' => $device['deviceId'],
+ 'packageId' => !empty($v['packageId']) ? $v['packageId'] : 0,
+ 'poolId' => !empty($v['id']) ? $v['id'] : 0,
+ 'createTime' => time(),
+ ];
+ }
+ Db::name('workbench_import_contact_item')->insertAll($data);
+ }
+
+ /**
+ * 获取设备列表
+ * @param Workbench $workbench 工作台
+ * @param WorkbenchImportContact $config 配置
+ * @return array
+ */
+ protected function getDeviceList($workbench, $config)
+ {
+ $deviceIds = json_decode($config['devices'], true);
+ if (empty($deviceIds)) {
+ return [];
+ }
+
+ // 从数据库获取设备信息
+ $devices = Db::table('s2_device')
+ ->whereIn('id', $deviceIds)
+ ->where('isDeleted', 0)
+ ->where('alive', 1) // 只选择在线设备
+ ->field('id as deviceId, imei, nickname')
+ ->select();
+
+ return $devices;
+ }
+
+ /**
+ * 判断是否需要导入
+ * @param Workbench $workbench 工作台
+ * @param WorkbenchImportContact $config 配置
+ * @return bool
+ */
+ protected function shouldImport($workbench, $config)
+ {
+ // 检查导入间隔
+ $today = date('Y-m-d');
+ $startTimestamp = strtotime($today . ' ' . $config['startTime'] . ':00');
+ $endTimestamp = strtotime($today . ' ' . $config['endTime'] . ':00');
+ // 如果不在指定时间范围内,则跳过
+ if ($startTimestamp > time() || $endTimestamp < time()) {
+ return false;
+ }
+
+ $maxPerDay = $config['num'];
+ if ($maxPerDay <= 0) {
+ return false;
+ }
+
+ // 查询今日已导入次数
+ $count = Db::name('workbench_import_contact_item')
+ ->where('workbenchId', $workbench->id)
+ ->whereTime('createTime', 'between', [$startTimestamp, $endTimestamp])
+ ->count();
+
+ if ($count >= $maxPerDay) {
+ return false;
+ }
+
+ // 计算导入间隔
+ $totalSeconds = $endTimestamp - $startTimestamp;
+ $interval = floor($totalSeconds / $maxPerDay);
+ $nextImportTime = $startTimestamp + $count * $interval;
+
+ if (time() < $nextImportTime) {
+ return false;
+ }
+
+ return true;
+ }
+
+
+
+ /**
+ * 从数据库读取通讯录
+ * @param WorkbenchImportContact $config
+ * @return array
+ */
+ protected function getContactFromDatabase($workbench,$config)
+ {
+ $pools = json_decode($config['pools'], true);
+ $deviceIds = json_decode($config['devices'], true);
+ if (empty($pools) || empty($deviceIds)) {
+ return false;
+ }
+ $deviceNum = count($deviceIds);
+ $contactNum = $deviceNum * $config['num'];
+ if (empty($contactNum)) {
+ return false;
+ }
+ //过滤已删除的数据
+ $packageIds = Db::name('traffic_source_package')
+ ->where(['isDel' => 0])
+ ->whereIn('id', $pools)
+ ->column('id');
+
+ if (empty($packageIds)) {
+ return false;
+ }
+
+ $data = Db::name('traffic_source_package_item')->alias('tpi')
+ ->join('traffic_pool tp', 'tp.identifier = tpi.identifier')
+ ->join('traffic_source ts', 'ts.identifier = tpi.identifier','left')
+ ->join('workbench_import_contact_item wici', 'wici.poolId = tp.id AND wici.workbenchId = '.$workbench->id,'left')
+ ->where('tp.mobile', '>',0)
+ ->where('wici.id','null')
+ ->whereIn('tpi.packageId',$packageIds)
+ ->field('tp.id,tpi.packageId,tp.mobile as phone,ts.name')
+ ->order('tp.id DESC')
+ ->group('tpi.identifier')
+ ->limit($contactNum)
+ ->select();
+ return $data;
+ }
+
+ /**
+ * 记录任务开始
+ * @param string $jobId
+ * @param string $queueLockKey
+ */
+ protected function logJobStart($jobId, $queueLockKey)
+ {
+ Log::info('开始处理工作台通讯录导入任务: ' . json_encode([
+ 'jobId' => $jobId,
+ 'queueLockKey' => $queueLockKey
+ ]));
+ }
+
+ /**
+ * 处理任务成功
+ * @param Job $job
+ * @param string $queueLockKey
+ */
+ protected function handleJobSuccess($job, $queueLockKey)
+ {
+ $job->delete();
+ Cache::rm($queueLockKey);
+ Log::info('工作台通讯录导入任务执行成功');
+ }
+
+ /**
+ * 处理任务错误
+ * @param \Exception $e
+ * @param Job $job
+ * @param string $queueLockKey
+ * @return bool
+ */
+ protected function handleJobError(\Exception $e, $job, $queueLockKey)
+ {
+ Log::error('工作台通讯录导入任务异常:' . $e->getMessage());
+
+ if (!empty($queueLockKey)) {
+ Cache::rm($queueLockKey);
+ Log::info("由于异常释放队列锁: {$queueLockKey}");
+ }
+
+ if ($job->attempts() > self::MAX_RETRY_ATTEMPTS) {
+ $job->delete();
+ } else {
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/application/job/WorkbenchMomentsJob.php b/application/job/WorkbenchMomentsJob.php
new file mode 100644
index 0000000..62db79d
--- /dev/null
+++ b/application/job/WorkbenchMomentsJob.php
@@ -0,0 +1,562 @@
+ 1, // 未知 -> 文本
+ 1 => 2, // 图片 -> 图文
+ 2 => 4, // 链接 -> 链接
+ 3 => 3, // 视频 -> 视频
+ 4 => 1, // 文本 -> 文本
+ 5 => 1, // 小程序 -> 文本
+ 6 => 2, // 图文 -> 图文
+ ];
+
+ /**
+ * 最大重试次数
+ */
+ const MAX_RETRY_ATTEMPTS = 3;
+
+ /**
+ * 队列任务处理
+ * @param Job $job 队列任务
+ * @param array $data 任务数据
+ * @return bool
+ */
+ public function fire(Job $job, $data)
+ {
+ $jobId = $data['jobId'] ?? '';
+ $queueLockKey = $data['queueLockKey'] ?? '';
+ try {
+ $this->logJobStart($jobId, $queueLockKey);
+ $this->execute2();
+ $this->execute();
+ $this->handleJobSuccess($job, $queueLockKey);
+ return true;
+ } catch (\Exception $e) {
+ return $this->handleJobError($e, $job, $queueLockKey);
+ }
+ }
+
+ /**
+ * 执行任务
+ * @throws \Exception
+ */
+ public function execute()
+ {
+ try {
+ // 获取所有工作台
+ $workbenches = Workbench::where(['status' => 1, 'type' => 2, 'isDel' => 0])->order('id desc')->select();
+ foreach ($workbenches as $workbench) {
+ // 获取工作台配置
+ $config = WorkbenchMoments::where('workbenchId', $workbench->id)->find();
+ if (!$config) {
+ continue;
+ }
+ $startTime = strtotime(date('Y-m-d ' . $config['startTime']));
+ $endTime = strtotime(date('Y-m-d ' . $config['endTime']));
+ // 如果时间不符,则跳过
+ if ($startTime > time() || $endTime < time()) {
+ continue;
+ }
+
+ // 获取设备
+ $devices = $this->getDevice($workbench, $config);
+ if (empty($devices)) {
+ continue;
+ }
+
+ // 获取内容库
+ $contentLibrary = $this->getContentLibrary($workbench, $config);
+ if (empty($contentLibrary)) {
+ continue;
+ }
+ // 处理内容发送
+ $this->handleContentSend($workbench, $config, $devices, $contentLibrary);
+ }
+ } catch (\Exception $e) {
+ Log::error("朋友圈同步任务异常: " . $e->getMessage());
+ throw $e;
+ }
+ }
+
+ public function execute2()
+ {
+ try {
+ // 1) 每日重置
+ $this->resetDailyCountersIfNeeded();
+
+ // 2) 获取发送窗口内的任务
+ [$nowTs, $kfMoments] = $this->getWindowTasks();
+ foreach ($kfMoments as $val) {
+ $companyId = (int)($val['companyId'] ?? 0);
+ $userId = (int)($val['userId'] ?? 0);
+
+ // 2.1) 数据规范化
+ $sendData = json_decode($val->sendData, true);
+ $sendData = $this->normalizeSendData($sendData);
+
+ // 2.2) 账号额度过滤
+ $items = $sendData['jobPublishWechatMomentsItems'] ?? [];
+ if (empty($items)) { continue; }
+ $allowed = $this->filterAccountsByQuota($companyId, $userId, $items);
+ if (empty($allowed)) { continue; }
+ $sendData['jobPublishWechatMomentsItems'] = $allowed;
+
+ // 3) 下发
+ $moments = new Moments();
+ $res = $moments->addJob($sendData);
+ $res = json_decode($res, true);
+ if ($res['code'] == 200){
+ KfMoments::where(['id' => $val['id']])->update(['isSend' => 1]);
+
+ // 4) 统计
+ $this->incrementSendStats($companyId, $userId, $allowed);
+ }
+ }
+ } catch (\Exception $e) {
+ Log::error("朋友圈同步任务异常: " . $e->getMessage());
+ throw $e;
+ }
+ }
+
+ protected function resetDailyCountersIfNeeded()
+ {
+ $now = time();
+ $todayStart = strtotime(date('Y-m-d 00:00:00'));
+ if ($now - $todayStart >= 0 && $now - $todayStart <= 600) {
+ $cacheKey = 'moments_settings_reset_' . date('Ymd');
+ if (!Cache::has($cacheKey)) {
+ Db::table('ck_kf_moments_settings')->where('sendNum', '<>', 0)
+ ->update(['sendNum' => 0, 'updateTime' => $now]);
+ Cache::set($cacheKey, 1, 7200);
+ }
+ }
+ }
+
+ protected function getWindowTasks()
+ {
+ $nowTs = time();
+ $windowStart = $nowTs - 300;
+ $windowEnd = $nowTs + 300;
+ $kfMoments = KfMoments::where(['isSend' => 0, 'isDel' => 0])
+ ->whereBetween('sendTime', [$windowStart, $windowEnd])
+ ->order('id desc')->select();
+ return [$nowTs, $kfMoments];
+ }
+
+ protected function normalizeSendData(array $sendData)
+ {
+ $endTime = strtotime($sendData['endTime'] ?? '');
+ if ($endTime <= time() + 1800) {
+ $endTime = time() + 3600;
+ $sendData['endTime'] = date('Y-m-d H:i:s', $endTime);
+ }
+ switch ($sendData['momentContentType'] ?? 1) {
+ case 1:
+ $sendData['link'] = ['image' => ''];
+ $sendData['picUrlList'] = [];
+ $sendData['videoUrl'] = '';
+ break;
+ case 2:
+ $sendData['link'] = ['image' => ''];
+ $sendData['videoUrl'] = '';
+ break;
+ case 3:
+ $sendData['link'] = ['image' => ''];
+ $sendData['picUrlList'] = [];
+ break;
+ case 4:
+ $sendData['picUrlList'] = [];
+ $sendData['videoUrl'] = '';
+ break;
+ default:
+ $sendData['link'] = ['image' => ''];
+ $sendData['picUrlList'] = [];
+ $sendData['videoUrl'] = '';
+ break;
+ }
+ return $sendData;
+ }
+
+ protected function filterAccountsByQuota(int $companyId, int $userId, array $items)
+ {
+ $wechatIds = array_values(array_filter(array_map(function($it){ return (int)($it['wechatAccountId'] ?? 0); }, $items)));
+ if (empty($wechatIds)) { return []; }
+ $settings = Db::table('ck_kf_moments_settings')
+ ->where('companyId', $companyId)
+ ->where('userId', $userId)
+ ->whereIn('wechatId', $wechatIds)
+ ->column('id,max,sendNum', 'wechatId');
+ $allowed = [];
+ foreach ($items as $it) {
+ $wid = (int)($it['wechatAccountId'] ?? 0);
+ if ($wid <= 0) { continue; }
+ if (isset($settings[$wid])) {
+ $max = (int)$settings[$wid]['max'];
+ $sent = (int)$settings[$wid]['sendNum'];
+ if ($sent < ($max > 0 ? $max : 5)) { $allowed[] = $it; }
+ } else {
+ $allowed[] = $it;
+ }
+ }
+ return $allowed;
+ }
+
+ protected function incrementSendStats(int $companyId, int $userId, array $items)
+ {
+ try {
+ $nowTs = time();
+ foreach ($items as $it) {
+ $wechatId = (int)($it['wechatAccountId'] ?? 0);
+ if ($wechatId <= 0) { continue; }
+ $cond = ['companyId' => $companyId, 'userId' => $userId, 'wechatId' => $wechatId];
+ $setting = Db::table('ck_kf_moments_settings')->where($cond)->find();
+ if ($setting) {
+ Db::table('ck_kf_moments_settings')->where('id', $setting['id'])
+ ->update(['sendNum' => Db::raw('sendNum + 1'), 'updateTime' => $nowTs]);
+ } else {
+ Db::table('ck_kf_moments_settings')->insert([
+ 'companyId' => $companyId,
+ 'userId' => $userId,
+ 'wechatId' => $wechatId,
+ 'max' => 5,
+ 'sendNum' => 1,
+ 'createTime' => $nowTs,
+ 'updateTime' => $nowTs,
+ ]);
+ }
+ }
+ } catch (\Throwable $e) {
+ Log::error('朋友圈发送统计失败: ' . $e->getMessage());
+ }
+ }
+
+
+ /**
+ * 处理内容发送
+ * @param Workbench $workbench
+ * @param WorkbenchMoments $config
+ * @param array $devices
+ * @param array $contentLibrary
+ */
+ protected function handleContentSend($workbench, $config, $devices, $contentLibrary)
+ {
+ // 准备评论数据
+ $comment = [];
+ if (!empty($contentLibrary['comment'])) {
+ $comment[] = $contentLibrary['comment'];
+ }
+
+ // 准备发送数据
+ $jobPublishWechatMomentsItems = [];
+ foreach ($devices as $device) {
+ $jobPublishWechatMomentsItems[] = [
+ 'comments' => $comment,
+ 'labels' => [],
+ 'wechatAccountId' => $device['wechatAccountId']
+ ];
+ }
+
+ // 转换内容类型
+ $momentContentType = self::CONTENT_TYPE_MAP[$contentLibrary['contentType']] ?? 1;
+ $sendTime = !empty($contentLibrary['sendTime']) ? $contentLibrary['sendTime'] : time();
+
+ // 图片url
+ if ($momentContentType == 2) {
+ $picUrlList = json_decode($contentLibrary['resUrls'], true);
+ } else {
+ $picUrlList = [];
+ }
+
+ // 视频url
+ if ($momentContentType == 3) {
+ $videoUrl = json_decode($contentLibrary['urls'], true);
+ $videoUrl = $videoUrl[0] ?? '';
+ } else {
+ $videoUrl = '';
+ }
+
+ // 链接url
+ if ($momentContentType == 4) {
+ $urls = json_decode($contentLibrary['urls'], true);
+ $url = $urls[0] ?? [];
+ $link = [
+ 'desc' => $url['desc'] ?? '',
+ 'image' => $url['image'] ?? '',
+ 'url' => $url['url'] ?? ''
+ ];
+ } else {
+ $link = ['image' => ''];
+ }
+
+ // 准备发送参数
+ $data = [
+ 'altList' => '',
+ 'immediately' => false,
+ 'isUseLocation' => false,
+ 'jobPublishWechatMomentsItems' => $jobPublishWechatMomentsItems,
+ 'lat' => 0,
+ 'lng' => 0,
+ 'link' => $link,
+ 'momentContentType' => $momentContentType,
+ 'picUrlList' => $picUrlList,
+ 'poiAddress' => '',
+ 'poiName' => '',
+ 'publicMode' => '',
+ 'text' => !empty($contentLibrary['contentAi']) ? $contentLibrary['contentAi'] : $contentLibrary['content'],
+ 'timingTime' => date('Y-m-d H:i:s', $sendTime),
+ 'beginTime' => date('Y-m-d H:i:s', $sendTime),
+ 'endTime' => date('Y-m-d H:i:s', $sendTime + 3600),
+ 'videoUrl' => $videoUrl,
+ ];
+ // 发送朋友圈
+ $moments = new Moments();
+ $res = $moments->addJob($data);
+ $res = json_decode($res,true);
+ if ($res['code'] == 200){
+ // 记录发送记录
+ $this->recordSendHistory($workbench, $devices, $contentLibrary);
+ }
+ }
+
+
+ /**
+ * 记录发送历史
+ * @param Workbench $workbench
+ * @param array $devices
+ * @param array $contentLibrary
+ */
+ protected function recordSendHistory($workbench, $devices, $contentLibrary)
+ {
+ $now = time();
+ $data = [];
+ foreach ($devices as $device) {
+ $data = [
+ 'workbenchId' => $workbench->id,
+ 'deviceId' => $device['deviceId'],
+ 'contentId' => $contentLibrary['id'],
+ 'wechatAccountId' => $device['wechatAccountId'],
+ 'isLoop' => 0, // 初始状态为未完成循环
+ 'createTime' => $now,
+ ];
+ Db::name('workbench_moments_sync_item')->insert($data);
+ }
+
+ }
+
+ /**
+ * 获取设备列表
+ * @param Workbench $workbench 工作台
+ * @param WorkbenchMoments $config 配置
+ * @return array|bool
+ */
+ protected function getDevice($workbench, $config)
+ {
+ $devices = json_decode($config['devices'], true);
+ if (empty($devices)) {
+ return false;
+ }
+
+ $list = Db::name('device')->alias('d')
+ ->join('device_wechat_login dw', 'dw.alive = 1 and dw.deviceId = d.id and dw.companyId = d.companyId')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.wechatId = dw.wechatId')
+ ->where(['d.companyId' => $workbench->companyId, 'd.alive' => 1])
+ ->whereIn('d.id', $devices)
+ ->field('d.id as deviceId, d.memo as deviceName, d.companyId, dw.wechatId, wa.id as wechatAccountId')
+ ->select();
+
+ $newList = [];
+ foreach ($list as $val) {
+ // 检查发送间隔(新逻辑:根据startTime、endTime、syncCount动态计算)
+ $today = date('Y-m-d');
+ $startTimestamp = strtotime($today . ' ' . $config['startTime'] . ':00');
+ $endTimestamp = strtotime($today . ' ' . $config['endTime'] . ':00');
+ $totalSeconds = $endTimestamp - $startTimestamp;
+ if ($totalSeconds <= 0 || empty($config['syncCount'])) {
+ continue;
+ }
+ $interval = floor($totalSeconds / $config['syncCount']);
+
+ // 查询今日已同步次数
+ $count = Db::name('workbench_moments_sync_item')
+ ->where('workbenchId', $workbench->id)
+ ->where('deviceId', $val['deviceId'])
+ ->whereTime('createTime', 'between', [$startTimestamp, $endTimestamp])
+ ->count();
+
+ if ($count >= $config['syncCount']) {
+ continue;
+ }
+
+ // 计算本次同步的最早允许时间
+ $nextSyncTime = $startTimestamp + $count * $interval;
+ if (time() < $nextSyncTime) {
+ continue;
+ }
+
+ $newList[] = $val;
+ }
+
+ return $newList;
+ }
+
+ /**
+ * 获取内容库
+ * @param Workbench $workbench 工作台
+ * @param WorkbenchMoments $config 配置
+ * @return array|bool
+ */
+ protected function getContentLibrary($workbench, $config)
+ {
+ $contentids = json_decode($config['contentLibraries'], true);
+ // 清洗 contentids:去除 null/空字符串,并去重,保持原顺序
+ if (is_array($contentids)) {
+ $contentids = array_values(array_unique(array_filter($contentids, function ($v) {
+ return $v !== null && $v !== '';
+ })));
+ } else {
+ $contentids = [];
+ }
+ if (empty($contentids)) {
+ return false;
+ }
+ // 基础查询
+ $query = Db::name('content_library')->alias('cl')
+ ->join('content_item ci', 'ci.libraryId = cl.id')
+ ->where(['cl.isDel' => 0, 'ci.isDel' => 0])
+ ->whereIn('cl.id', $contentids)
+ ->field([
+ 'ci.id',
+ 'ci.libraryId',
+ 'ci.contentType',
+ 'ci.title',
+ 'ci.content',
+ 'ci.resUrls',
+ 'ci.urls',
+ 'ci.comment',
+ 'ci.sendTime'
+ ]);
+ // 复制 query
+ $query2 = clone $query;
+ $query3 = clone $query;
+ // 根据accountType处理不同的发送逻辑
+ if ($config['accountType'] == 1) {
+ // 可以循环发送
+ // 1. 优先获取未发送的内容
+ $unsentContent = $query2->join('workbench_moments_sync_item wmsi', 'wmsi.contentId = ci.id and wmsi.workbenchId = ' . $workbench->id, 'left')
+ ->where('wmsi.id', 'null')
+ ->where('ci.sendTime <= ' . (time() + 60))
+ ->order('ci.sendTime desc, ci.id desc')
+ ->find();
+
+ if (!empty($unsentContent)) {
+ return $unsentContent;
+ }
+
+ // 获取下一个要发送的内容(从内容库中查询,排除isLoop为0的数据)
+ $isPushIds = Db::name('workbench_moments_sync_item')
+ ->where(['workbenchId' => $workbench->id, 'isLoop' => 0])
+ ->column('contentId');
+
+ if (empty($isPushIds)) {
+ $isPushIds = [0];
+ }
+ $sentContent = $query3
+ ->whereNotIn('ci.id', $isPushIds)
+ ->group('ci.id')
+ ->order('ci.id asc')
+ ->find();
+ // 4. 如果仍然没有内容,说明内容库为空,将所有记录的isLoop标记为1
+ if (empty($sentContent)) {
+ // 将所有该工作台的记录标记为循环完成
+ Db::name('workbench_moments_sync_item')
+ ->where('workbenchId', $workbench->id)
+ ->where('isLoop', 0)
+ ->update(['isLoop' => 1]);
+ return false;
+ }
+
+ return $sentContent;
+ } else {
+ // 不能循环发送,只获取未发送的内容
+ $list = $query2->join('workbench_moments_sync_item wmsi', 'wmsi.contentId = ci.id and wmsi.workbenchId = ' . $workbench->id, 'left')
+ ->where('wmsi.id', 'null')
+ ->order('ci.sendTime desc, ci.id desc')
+ ->find();
+ return $list;
+ }
+ }
+
+ /**
+ * 记录任务开始
+ * @param string $jobId
+ * @param string $queueLockKey
+ */
+ protected function logJobStart($jobId, $queueLockKey)
+ {
+ // 去除开始日志,减少日志空间消耗
+ }
+
+ /**
+ * 处理任务成功
+ * @param Job $job
+ * @param string $queueLockKey
+ */
+ protected function handleJobSuccess($job, $queueLockKey)
+ {
+ $job->delete();
+ Cache::rm($queueLockKey);
+ // 去除成功日志,减少日志空间消耗
+ }
+
+ /**
+ * 处理任务错误
+ * @param \Exception $e
+ * @param Job $job
+ * @param string $queueLockKey
+ * @return bool
+ */
+ protected function handleJobError(\Exception $e, $job, $queueLockKey)
+ {
+ Log::error('工作台朋友圈同步任务异常:' . $e->getMessage());
+
+ if (!empty($queueLockKey)) {
+ Cache::rm($queueLockKey);
+ Log::info("由于异常释放队列锁: {$queueLockKey}");
+ }
+
+ if ($job->attempts() > self::MAX_RETRY_ATTEMPTS) {
+ $job->delete();
+ } else {
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/application/job/WorkbenchTrafficDistributeJob.php b/application/job/WorkbenchTrafficDistributeJob.php
new file mode 100644
index 0000000..fb2ddaa
--- /dev/null
+++ b/application/job/WorkbenchTrafficDistributeJob.php
@@ -0,0 +1,267 @@
+logJobStart($jobId, $queueLockKey);
+ $workbenches = $this->getActiveWorkbenches();
+ if (empty($workbenches)) {
+ $this->handleEmptyWorkbenches($job, $queueLockKey);
+ return true;
+ }
+ $this->processWorkbenches($workbenches);
+ $this->handleJobSuccess($job, $queueLockKey);
+ return true;
+ } catch (\Exception $e) {
+ return $this->handleJobError($e, $job, $queueLockKey);
+ }
+ }
+
+ protected function getActiveWorkbenches()
+ {
+ return Workbench::where([
+ ['status', '=', 1],
+ ['isDel', '=', 0],
+ ['type', '=', 5]
+ ])->order('id DESC')->select();
+ }
+
+ protected function processWorkbenches($workbenches)
+ {
+ foreach ($workbenches as $workbench) {
+ try {
+ $this->processSingleWorkbench($workbench);
+ } catch (\Exception $e) {
+ Log::error("处理流量分发工作台 {$workbench->id} 失败: " . $e->getMessage());
+ }
+ }
+ }
+
+ protected function processSingleWorkbench($workbench)
+ {
+ $page = 1;
+ $pageSize = 20;
+
+ $config = WorkbenchTrafficConfig::where('workbenchId', $workbench->id)->find();
+ if (!$config) {
+ Log::error("流量分发工作台 {$workbench->id} 配置获取失败");
+ return;
+ }
+
+ // 验证是否在流量分发时间范围内
+ if (!$this->isTimeRange($config) && $config['timeType'] == 2) {
+ return;
+ }
+ // 获取当天未超额的可用账号
+ if(empty($config['account'])){
+ Log::error("流量分发工作台 {$workbench->id} 未配置分发的客服");
+ return;
+ }
+ $accountIds = json_decode($config['account'],true);
+ $todayStart = strtotime(date('Y-m-d 00:00:00'));
+ $todayEnd = strtotime(date('Y-m-d 23:59:59'));
+ $accounts = Db::table('s2_company_account')
+ ->alias('a')
+ ->where(['a.departmentId' => $workbench->companyId, 'a.status' => 0])
+ ->whereIn('a.id',$accountIds)
+ ->whereNotLike('a.userName', '%_offline%')
+ ->whereNotLike('a.userName', '%_delete%')
+ ->leftJoin('workbench_traffic_config_item wti', "wti.wechatAccountId = a.id AND wti.workbenchId = {$workbench->id} AND wti.createTime BETWEEN {$todayStart} AND {$todayEnd}")
+ ->field('a.id,a.userName,a.realName,a.nickname,COUNT(wti.id) as todayCount')
+ ->group('a.id')
+ ->having('todayCount <= ' . $config['maxPerDay'])
+ ->select();
+ $accountNum = count($accounts);
+ if ($accountNum < 1) {
+ Log::info("流量分发工作台 {$workbench->id} 可分配账号少于1个");
+ return;
+ }
+ $automaticAssign = new AutomaticAssign();
+ do {
+ $friends = $this->getFriendsByLabels($workbench, $config, $page, $pageSize);
+
+ if (empty($friends) || count($friends) == 0) {
+ Log::info("流量分发工作台 {$workbench->id} 没有可分配的好友");
+ break;
+ }
+ $i = 0;
+ $accountNum = count($accounts);
+ foreach ($friends as $friend) {
+ if ($accountNum == 0) {
+ Log::info("流量分发工作台 {$workbench->id} 所有账号今日分配已满");
+ break 2;
+ }
+ if ($i >= $accountNum) {
+ $i = 0;
+ }
+ $account = $accounts[$i];
+
+ // 如果该账号今天分配的记录数加上本次分配的记录数超过最大限制
+ if (($account['todayCount'] + $pageSize) >= $config['maxPerDay']) {
+ // 查询该客服账号当天分配记录数
+ $todayCount = Db::name('workbench_traffic_config_item')
+ ->where('workbenchId', $workbench->id)
+ ->where('wechatAccountId', $account['id'])
+ ->whereBetween('createTime', [$todayStart, $todayEnd])
+ ->count();
+ if ($todayCount >= $config['maxPerDay']) {
+ unset($accounts[$i]);
+ $accounts = array_values($accounts);
+ $accountNum = count($accounts);
+ $i++;
+ continue;
+ }
+ }
+
+ // 执行切换好友命令
+ $res = $automaticAssign->allotWechatFriend([
+ 'wechatFriendId' => $friend['id'],
+ 'toAccountId' => $account['id']
+ ], true);
+
+ $res = json_decode($res,true);
+ if ($res['code'] == 200){
+ Db::table('s2_wechat_friend')
+ ->where('id',$friend['id'])
+ ->update([
+ 'accountId' => $account['id'],
+ 'accountUserName' => $account['userName'],
+ 'accountRealName' => $account['realName'],
+ 'accountNickname' => $account['nickname'],
+ ]);
+ // 写入分配记录表
+ Db::name('workbench_traffic_config_item')->insert([
+ 'workbenchId' => $workbench->id,
+ 'deviceId' => $friend['deviceId'],
+ 'wechatFriendId' => $friend['id'],
+ 'wechatAccountId' => $account['id'],
+ 'createTime' => time(),
+ 'exp' => $config['exp'],
+ 'expTime' => time() + 86400 * $config['exp'],
+ ]);
+- // 去除成功日志,减少日志空间消耗
+ }
+ $i++;
+ }
+ break;
+ $page++;
+ } while (true);
+ // 去除完成日志,减少日志空间消耗
+ }
+
+
+ /**
+ * 检查是否在流量分发时间范围内
+ * @param WorkbenchAutoLike $config
+ * @return bool
+ */
+ protected function isTimeRange($config)
+ {
+ $currentTime = date('H:i');
+ if ($currentTime < $config['startTime'] || $currentTime > $config['endTime']) {
+ Log::info("当前时间 {$currentTime} 不在流量分发时间范围内 ({$config['startTime']} - {$config['endTime']})");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * 一次性查出所有包含指定标签数组的好友(支持分页)
+ * @param object $workbench 工作台对象
+ * @param object $config 配置对象
+ * @param int $page 页码
+ * @param int $pageSize 每页数量
+ * @return array
+ */
+ protected function getFriendsByLabels($workbench, $config, $page = 1, $pageSize = 20)
+ {
+ $labels = [];
+ if (!empty($config['pools'])) {
+ $labels = is_array($config['pools']) ? $config['pools'] : json_decode($config['pools'], true);
+ }
+
+ $devices = [];
+ if (!empty($config['devices'])) {
+ $devices = is_array($config['devices']) ? $config['devices'] : json_decode($config['devices'], true);
+ }
+ if (empty($devices)) {
+ return [];
+ }
+ $query = Db::table('s2_wechat_friend')->alias('wf')
+ ->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left')
+ ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left')
+ ->join('workbench_traffic_config_item wtci', 'wtci.isRecycle = 0 and wtci.wechatFriendId = wf.id AND wtci.workbenchId = ' . $config['workbenchId'], 'left')
+ ->where([
+ ['wf.isDeleted', '=', 0],
+ ['wf.isPassed', '=', 1],
+ //['sa.departmentId', '=', $workbench->companyId],
+ ['wtci.id', 'null', null]
+ ])
+ ->whereIn('wa.currentDeviceId', $devices)
+ ->field('wf.id,wf.wechatAccountId,wf.wechatId,wf.labels,sa.userName,wa.currentDeviceId as deviceId');
+
+
+ if(!empty($labels)){
+ $query->where(function ($q) use ($labels) {
+ foreach ($labels as $label) {
+ $q->whereOrRaw("JSON_CONTAINS(wf.labels, '\"{$label}\"')");
+ }
+ });
+ }
+ $list = $query->page($page, $pageSize)->order('wf.id DESC')->select();
+
+ return $list;
+ }
+
+ protected function logJobStart($jobId, $queueLockKey)
+ {
+ // 去除开始日志,减少日志空间消耗
+ }
+
+ protected function handleJobSuccess($job, $queueLockKey)
+ {
+ $job->delete();
+ Cache::rm($queueLockKey);
+ // 去除成功日志,减少日志空间消耗
+ }
+
+ protected function handleJobError(\Exception $e, $job, $queueLockKey)
+ {
+ Log::error('流量分发任务异常:' . $e->getMessage());
+ if (!empty($queueLockKey)) {
+ Cache::rm($queueLockKey);
+ Log::info("由于异常释放队列锁: {$queueLockKey}");
+ }
+ if ($job->attempts() > self::MAX_RETRY_ATTEMPTS) {
+ $job->delete();
+ } else {
+ $job->release(Config::get('queue.failed_delay', 10));
+ }
+ return false;
+ }
+
+ protected function handleEmptyWorkbenches(Job $job, $queueLockKey)
+ {
+ Log::info('没有需要处理的流量分发任务');
+ $job->delete();
+ Cache::rm($queueLockKey);
+ }
+}
\ No newline at end of file
diff --git a/application/provider.php b/application/provider.php
new file mode 100644
index 0000000..d453b84
--- /dev/null
+++ b/application/provider.php
@@ -0,0 +1,16 @@
+
+// +----------------------------------------------------------------------
+
+// 应用容器绑定定义
+return [
+ // 类的映射表
+ 'ClassTable' => app\common\service\ClassTableService::class,
+];
diff --git a/application/store/config/route.php b/application/store/config/route.php
new file mode 100644
index 0000000..ae09c5d
--- /dev/null
+++ b/application/store/config/route.php
@@ -0,0 +1,49 @@
+middleware(['jwt']);
+
+Route::get('v1/store/login', 'app\store\controller\LoginController@index');
\ No newline at end of file
diff --git a/application/store/controller/BaseController.php b/application/store/controller/BaseController.php
new file mode 100644
index 0000000..c8cec8b
--- /dev/null
+++ b/application/store/controller/BaseController.php
@@ -0,0 +1,65 @@
+userInfo = request()->userInfo;
+
+ // 生成缓存key
+ $cacheKey = 'device_info_' . $this->userInfo['id'] . '_' . $this->userInfo['companyId'];
+
+ // 尝试从缓存获取设备信息
+ $device = Cache::get($cacheKey);
+ // 如果缓存不存在,则从数据库获取
+ if (!$device) {
+ $device = Db::name('device_user')
+ ->alias('du')
+ ->join('device d', 'd.id = du.deviceId','left')
+ ->join('device_wechat_login dwl', 'dwl.deviceId = du.deviceId','left')
+ ->join('wechat_account wa', 'dwl.wechatId = wa.wechatId','left')
+ ->where([
+ 'du.userId' => $this->userInfo['id'],
+ 'du.companyId' => $this->userInfo['companyId']
+ ])
+ ->field('d.*,wa.wechatId,wa.alias,wa.s2_wechatAccountId as wechatAccountId')
+ ->find();
+ // 将设备信息存入缓存
+ if ($device) {
+ Cache::set($cacheKey, $device, $this->cacheExpire);
+ }
+ }
+ $this->device = $device;
+ }
+
+ /**
+ * 清除设备信息缓存
+ */
+ protected function clearDeviceCache()
+ {
+ $cacheKey = 'device_info_' . $this->userInfo['id'] . '_' . $this->userInfo['companyId'];
+ Cache::rm($cacheKey);
+ }
+}
\ No newline at end of file
diff --git a/application/store/controller/CustomerController.php b/application/store/controller/CustomerController.php
new file mode 100644
index 0000000..e61e62e
--- /dev/null
+++ b/application/store/controller/CustomerController.php
@@ -0,0 +1,93 @@
+request->param();
+
+ // 获取分页参数
+ $page = isset($params['page']) ? intval($params['page']) : 1;
+ $pageSize = isset($params['pageSize']) ? intval($params['pageSize']) : 10;
+ $userInfo = request()->userInfo;
+
+ $where = [];
+ // 必要的查询条件
+ $userId = $userInfo['id'];
+ $companyId = $userInfo['companyId'];
+
+ if (empty($userId) || empty($companyId)) {
+ return errorJson('缺少必要参数');
+ }
+
+ // 构建查询条件
+ $deviceIds = Db::name('device_user')->where(['userId' => $userId, 'companyId' => $companyId])->order('id DESC')->column('deviceId');
+ if (empty($deviceIds)) {
+ return errorJson('设备不存在');
+ }
+ $wechatIds = [];
+ foreach ($deviceIds as $deviceId) {
+ $wechatIds[] = Db::name('device_wechat_login')
+ ->where(['deviceId' => $deviceId])
+ ->order('id DESC')
+ ->value('wechatId');
+ }
+
+
+
+ // 搜索条件
+ if (!empty($params['keyword'])) {
+ $where['alias|nickname|wechatId'] = ['like', '%' . $params['keyword'] . '%'];
+ }
+ // if (!empty($params['email'])) {
+ // $where['wa.bindEmail'] = ['like', '%' . $params['email'] . '%'];
+ // }
+ // if (!empty($params['name'])) {
+ // $where['wa.accountRealName|wa.accountUserName|wa.nickname'] = ['like', '%' . $params['name'] . '%'];
+ // }
+
+ // 构建查询
+ $query = Db::table('s2_wechat_friend')
+ ->where($where)
+ ->whereIn('ownerWechatId',$wechatIds)
+ ->group('wechatId'); // 防止重复数据
+
+ // 克隆查询对象,用于计算总数
+ $countQuery = clone $query;
+ $total = $countQuery->count();
+
+ // 获取分页数据
+ $list = $query->page($page, $pageSize)
+ ->order('id DESC')
+ ->select();
+
+
+ // 格式化数据
+ foreach ($list as &$item) {
+ $item['labels'] = json_decode($item['labels'], true);
+ $item['createTime'] = date('Y-m-d H:i:s', $item['createTime']);
+ }
+ unset($item);
+
+ return successJson([
+ 'list' => $list,
+ 'total' => $total
+ ], '获取成功');
+ }
+}
\ No newline at end of file
diff --git a/application/store/controller/FlowPackageController.php b/application/store/controller/FlowPackageController.php
new file mode 100644
index 0000000..a4f4035
--- /dev/null
+++ b/application/store/controller/FlowPackageController.php
@@ -0,0 +1,295 @@
+request->param();
+
+ // 查询条件
+ $where = [];
+
+ // 只获取未删除的数据
+ $where[] = ['isDel', '=', 0];
+
+ // 套餐模型
+ $model = new FlowPackageModel();
+
+ // 查询数据
+ $list = $model->where($where)
+ ->field('id, name, tag, originalPrice, price, monthlyFlow, duration, privileges')
+ ->order('sort', 'asc')
+ ->select();
+
+ // 格式化返回数据,添加计算字段
+ $result = [];
+ foreach ($list as $item) {
+ $result[] = [
+ 'id' => $item['id'],
+ 'name' => $item['name'],
+ 'tag' => $item['tag'],
+ 'originalPrice' => $item['originalPrice'],
+ 'price' => $item['price'],
+ 'monthlyFlow' => $item['monthlyFlow'],
+ 'duration' => $item['duration'],
+ 'discount' => $item->discount,
+ 'totalFlow' => $item->totalFlow,
+ 'privileges' => $item['privileges'],
+ ];
+ }
+
+ return successJson($result, '获取成功');
+ }
+
+ /**
+ * 获取流量套餐详情
+ *
+ * @param int $id 套餐ID
+ * @return \think\Response
+ */
+ public function detail($id)
+ {
+ if (empty($id)) {
+ return errorJson('参数错误');
+ }
+
+ // 套餐模型
+ $model = new FlowPackageModel();
+
+ // 查询数据
+ $info = $model->where('id', $id)->where('isDel', 0)->find();
+
+ if (empty($info)) {
+ return errorJson('套餐不存在');
+ }
+
+ // 格式化返回数据,添加计算字段
+ $result = [
+ 'id' => $info['id'],
+ 'name' => $info['name'],
+ 'tag' => $info['tag'],
+ 'originalPrice' => $info['originalPrice'],
+ 'price' => $info['price'],
+ 'monthlyFlow' => $info['monthlyFlow'],
+ 'duration' => $info['duration'],
+ 'discount' => $info->discount,
+ 'totalFlow' => $info->totalFlow,
+ 'privileges' => $info['privileges'],
+ ];
+
+ return successJson($result, '获取成功');
+ }
+
+ /**
+ * 展示用户流量套餐使用情况
+ *
+ * @return \think\Response
+ */
+ public function remainingFlow()
+ {
+ $params = $this->request->param();
+
+ $userInfo = request()->userInfo;
+ // 获取用户ID,通常应该从会话或令牌中获取
+ $userId = $userInfo['id'];
+
+ if (empty($userId)) {
+ return errorJson('请先登录');
+ }
+
+ // 获取用户当前有效的流量套餐
+ $userPackage = UserFlowPackageModel::getUserActivePackage($userId);
+
+ if (empty($userPackage)) {
+ return errorJson('您没有有效的流量套餐');
+ }
+
+ // 获取套餐详情
+ $packageId = $userPackage['packageId'];
+ $flowPackage = FlowPackageModel::where('id', $packageId)->where('isDel', 0)->find();
+
+ if (empty($flowPackage)) {
+ return errorJson('套餐信息不存在');
+ }
+
+ // 计算剩余流量
+ $totalFlow = $userPackage['totalFlow'] ?? $flowPackage->totalFlow; // 总流量
+ $usedFlow = $userPackage['usedFlow'] ?? 0; // 已使用流量
+ $remainingFlow = $totalFlow - $usedFlow; // 剩余流量
+ $remainingFlow = $remainingFlow > 0 ? $remainingFlow : 0; // 确保不为负数
+
+ // 计算剩余天数
+ $now = time();
+ $expireTime = $userPackage['expireTime'];
+ $remainingDays = ceil(($expireTime - $now) / 86400); // 向上取整,剩余天数
+ $remainingDays = $remainingDays > 0 ? $remainingDays : 0; // 确保不为负数
+
+ // 剩余百分比
+ $flowPercentage = $totalFlow > 0 ? round(($remainingFlow / $totalFlow) * 100, 1) : 0;
+ $timePercentage = $userPackage['duration'] > 0 ?
+ round(($remainingDays / ($userPackage['duration'] * 30)) * 100, 1) : 0;
+
+ // 返回数据
+ $result = [
+ 'packageName' => $flowPackage['name'], // 套餐名称
+ 'remainingFlow' => $remainingFlow, // 剩余流量(人)
+ 'totalFlow' => $totalFlow, // 总流量(人)
+ 'flowPercentage' => $flowPercentage, // 剩余流量百分比
+ 'remainingDays' => $remainingDays, // 剩余天数
+ 'totalDays' => $userPackage['duration'] * 30, // 总天数(按30天/月计算)
+ 'timePercentage' => $timePercentage, // 剩余时间百分比
+ 'expireTime' => date('Y-m-d', $expireTime), // 到期日期
+ 'startTime' => date('Y-m-d', $userPackage['startTime']), // 开始日期
+ ];
+
+ return successJson($result, '获取成功');
+ }
+
+ /**
+ * 创建流量采购订单
+ *
+ * @return \think\Response
+ */
+ public function createOrder()
+ {
+ $params = $this->request->param();
+
+ $userInfo = request()->userInfo;
+ // 获取用户ID,通常应该从会话或令牌中获取
+ $userId = $userInfo['id'];
+
+ if (empty($userId)) {
+ return errorJson('请先登录');
+ }
+
+ // 获取套餐ID
+ $packageId = isset($params['packageId']) ? intval($params['packageId']) : 0;
+
+ if (empty($packageId)) {
+ return errorJson('请选择套餐');
+ }
+
+ // 查询套餐信息
+ $flowPackage = FlowPackageModel::where('id', $packageId)->where('isDel', 0)->find();
+
+ if (empty($flowPackage)) {
+ return errorJson('套餐不存在');
+ }
+
+ // 获取支付方式(可选)
+ $payType = isset($params['payType']) ? $params['payType'] : 'wechat';
+
+ // 套餐价格和信息
+ $amount = floatval($flowPackage['price']);
+ $packageName = $flowPackage['name'];
+ $duration = intval($flowPackage['duration']);
+ $remark = isset($params['remark']) ? $params['remark'] : '';
+
+ // 处理金额为0的特殊情况
+ if ($amount <= 0) {
+ // 金额为0,无需支付,直接创建订单并设置为已支付
+ $order = FlowPackageOrderModel::createOrder(
+ $userId,
+ $packageId,
+ $packageName,
+ 0,
+ $duration,
+ 'nopay',
+ $remark
+ );
+
+ if (!$order) {
+ return errorJson('订单创建失败');
+ }
+
+ // 创建用户流量套餐记录
+ $this->createUserFlowPackage($userId, $packageId, $order['id']);
+
+ // 返回成功信息
+ return successJson(['orderNo' => $order['orderNo'],'status' => 'success'], '购买成功');
+ } else {
+ // 创建正常需要支付的订单
+ $order = FlowPackageOrderModel::createOrder(
+ $userId,
+ $packageId,
+ $packageName,
+ $amount,
+ $duration,
+ $payType,
+ $remark
+ );
+
+ if (!$order) {
+ return errorJson('订单创建失败');
+ }
+
+ // 返回订单信息,前端需要跳转到支付页面
+ return successJson([
+ 'orderNo' => $order['orderNo'],
+ 'amount' => $amount,
+ 'payType' => $payType,
+ 'status' => 'pending'
+ ], '订单创建成功');
+ }
+ }
+
+ /**
+ * 创建用户流量套餐记录
+ *
+ * @param int $userId 用户ID
+ * @param int $packageId 套餐ID
+ * @param int $orderId 订单ID
+ * @return bool
+ */
+ private function createUserFlowPackage($userId, $packageId, $orderId)
+ {
+ // 获取套餐信息
+ $flowPackage = FlowPackageModel::where('id', $packageId)->where('isDel', 0)->find();
+
+ if (empty($flowPackage)) {
+ return false;
+ }
+
+ // 计算到期时间(当前时间 + 套餐时长(月) * 30天)
+ $now = time();
+ $expireTime = $now + (intval($flowPackage['duration']) * 30 * 86400);
+
+ // 用户流量套餐数据
+ $data = [
+ 'userId' => $userId,
+ 'packageId' => $packageId,
+ 'orderId' => $orderId,
+ 'packageName' => $flowPackage['name'],
+ 'monthlyFlow' => $flowPackage['monthlyFlow'],
+ 'duration' => $flowPackage['duration'],
+ 'totalFlow' => $flowPackage->totalFlow, // 使用计算属性获取总流量
+ 'usedFlow' => 0,
+ 'startTime' => $now,
+ 'expireTime' => $expireTime,
+ 'status' => 1, // 1:有效 0:无效
+ 'isDel' => 0
+ ];
+
+ // 创建用户流量套餐记录
+ return UserFlowPackageModel::create($data) ? true : false;
+ }
+}
diff --git a/application/store/controller/LoginController.php b/application/store/controller/LoginController.php
new file mode 100644
index 0000000..8dc571d
--- /dev/null
+++ b/application/store/controller/LoginController.php
@@ -0,0 +1,43 @@
+request->param('deviceId', '');
+ if (empty($deviceId)) {
+ return errorJson('缺少必要参数');
+ }
+
+ $user = Db::name('users')->alias('u')
+ ->field('u.*')
+ ->join('device_user du', 'u.id = du.userId and u.companyId = du.companyId')
+ ->join('device d', 'du.deviceId = d.id and u.companyId = du.companyId')
+ ->where(['d.deviceImei' => $deviceId, 'u.deleteTime' => 0, 'du.deleteTime' => 0, 'd.deleteTime' => 0])
+ ->find();
+ if (empty($user)) {
+ return errorJson('用户不存在');
+ }
+ $member = array_merge($user, [
+ 'lastLoginIp' => $this->request->ip(),
+ 'lastLoginTime' => time()
+ ]);
+
+ // 生成JWT令牌
+ $token = JwtUtil::createToken($user, 86400 * 30);
+ $token_expired = time() + 86400 * 30;
+
+ $data = [
+ 'member' => $member,
+ 'token' => $token,
+ 'token_expired' => $token_expired
+ ];
+ return successJson($data, '登录成功');
+ }
+}
\ No newline at end of file
diff --git a/application/store/controller/StatisticsController.php b/application/store/controller/StatisticsController.php
new file mode 100644
index 0000000..17c9226
--- /dev/null
+++ b/application/store/controller/StatisticsController.php
@@ -0,0 +1,482 @@
+userInfo['companyId'];
+ $userId = $this->userInfo['id'];
+
+ // 构建查询条件
+ $deviceIds = Db::name('device_user')->where(['userId' => $userId, 'companyId' => $companyId])->order('id DESC')->column('deviceId');
+ if (empty($deviceIds)) {
+ return errorJson('设备不存在');
+ }
+ $ownerWechatIds = [];
+ foreach ($deviceIds as $deviceId) {
+ $ownerWechatIds[] = Db::name('device_wechat_login')
+ ->where(['deviceId' => $deviceId])
+ ->order('id DESC')
+ ->value('wechatId');
+ }
+
+ $wechatAccountIds = Db::table('s2_wechat_account')->whereIn('wechatId', $ownerWechatIds)->column('id');
+
+
+ // 获取时间范围
+ $timeRange = $this->getTimeRange();
+ $startTime = $timeRange['start_time'];
+ $endTime = $timeRange['end_time'];
+ $lastStartTime = $timeRange['last_start_time'];
+ $lastEndTime = $timeRange['last_end_time'];
+
+
+ // 1. 总客户数
+ $totalCustomers = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds)
+ ->where('isDeleted', 0)
+ ->whereTime('createTime', '>=', $startTime)
+ ->whereTime('createTime', '<', $endTime)
+ ->count();
+
+ // 上期总客户数
+ $lastTotalCustomers = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds)
+ ->whereTime('createTime', '>=', $lastStartTime)
+ ->whereTime('createTime', '<', $lastEndTime)
+ ->count();
+
+ // 2. 新增客户数
+ $newCustomers = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds)
+ ->whereTime('createTime', '>=', $startTime)
+ ->whereTime('createTime', '<', $endTime)
+ ->count();
+
+ // 上期新增客户数
+ $lastNewCustomers = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds)
+ ->whereTime('createTime', '>=', $lastStartTime)
+ ->whereTime('createTime', '<', $lastEndTime)
+ ->count();
+
+ //3. 互动次数
+ $interactionCount = WechatMessageModel::whereIn('wechatAccountId', $wechatAccountIds)
+ ->where('createTime', '>=', $startTime)
+ ->where('createTime', '<', $endTime)
+ ->count();
+
+ // 上期互动次数
+ $lastInteractionCount = WechatMessageModel::whereIn('wechatAccountId', $wechatAccountIds)
+ ->where('createTime', '>=', $lastStartTime)
+ ->where('createTime', '<', $lastEndTime)
+ ->count();
+
+ // 4. RFM 平均值计算(不查询上期数据)
+ $rfmStats = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds)
+ ->where('isDeleted', 0)
+ ->field('AVG(`R`) as avgR, AVG(`F`) as avgF, AVG(`M`) as avgM')
+ ->find();
+
+ // 处理查询结果,如果字段为null则默认为0
+ $avgR = isset($rfmStats['avgR']) && $rfmStats['avgR'] !== null ? round((float)$rfmStats['avgR'], 2) : 0;
+ $avgF = isset($rfmStats['avgF']) && $rfmStats['avgF'] !== null ? round((float)$rfmStats['avgF'], 2) : 0;
+ $avgM = isset($rfmStats['avgM']) && $rfmStats['avgM'] !== null ? round((float)$rfmStats['avgM'], 2) : 0;
+
+ // 计算三者的平均值
+ $avgRFM = ($avgR + $avgF + $avgM) / 3;
+ $avgRFM = round($avgRFM, 2);
+
+ // 计算环比增长率
+ $customerGrowth = $this->calculateGrowth($totalCustomers, $lastTotalCustomers);
+ $newCustomerGrowth = $this->calculateGrowth($newCustomers, $lastNewCustomers);
+ $interactionGrowth = $this->calculateGrowth($interactionCount, $lastInteractionCount);
+ $data = [
+ 'total_customers' => [
+ 'value' => $totalCustomers,
+ 'growth' => $customerGrowth
+ ],
+ 'new_customers' => [
+ 'value' => $newCustomers,
+ 'growth' => $newCustomerGrowth
+ ],
+ 'interaction_count' => [
+ 'value' => $interactionCount,
+ 'growth' => $interactionGrowth
+ ],
+ 'conversion_rate' => [
+ 'value' => 10,
+ 'growth' => 15
+ ],
+ 'account_value' => [
+ 'avg_r' => $avgR,
+ 'avg_f' => $avgF,
+ 'avg_m' => $avgM,
+ 'avg_rfm' => $avgRFM
+ ]
+ ];
+
+ return successJson($data);
+ } catch (\Exception $e) {
+ return errorJson('获取数据概览失败:' . $e->getMessage());
+ }
+ }
+
+
+ /**
+ * 获取综合分析数据
+ */
+ public function getComprehensiveAnalysis()
+ {
+ try {
+ $companyId = $this->userInfo['companyId'];
+ $userId = $this->userInfo['id'];
+
+ // 构建查询条件
+ $deviceIds = Db::name('device_user')->where(['userId' => $userId, 'companyId' => $companyId])->order('id DESC')->column('deviceId');
+ if (empty($deviceIds)) {
+ return errorJson('设备不存在');
+ }
+ $ownerWechatIds = [];
+ foreach ($deviceIds as $deviceId) {
+ $ownerWechatIds[] = Db::name('device_wechat_login')
+ ->where(['deviceId' => $deviceId])
+ ->order('id DESC')
+ ->value('wechatId');
+ }
+ $wechatAccountIds = Db::table('s2_wechat_account')->whereIn('wechatId', $ownerWechatIds)->column('id');
+
+ // 获取时间范围
+ $timeRange = $this->getTimeRange();
+ $startTime = $timeRange['start_time'];
+ $endTime = $timeRange['end_time'];
+ $lastStartTime = $timeRange['last_start_time'];
+ $lastEndTime = $timeRange['last_end_time'];
+
+ // ========== 1. 客户平均转化金额 ==========
+ // 获取有订单的客户数(去重)
+ $convertedCustomers = TrafficOrderModel::whereIn('ownerWechatId', $ownerWechatIds)
+ ->where('createTime', '>=', $startTime)
+ ->where('createTime', '<', $endTime)
+ ->group('identifier')
+ ->column('identifier');
+ $convertedCustomerCount = count($convertedCustomers);
+
+ // 总销售额
+ $totalSales = TrafficOrderModel::whereIn('ownerWechatId', $ownerWechatIds)
+ ->where('createTime', '>=', $startTime)
+ ->where('createTime', '<', $endTime)
+ ->sum('actualPay');
+ $totalSales = $totalSales ?: 0;
+
+ // 客户平均转化金额
+ $avgConversionAmount = $convertedCustomerCount > 0 ? round($totalSales / $convertedCustomerCount, 2) : 0;
+
+ // ========== 2. 价值指标 ==========
+ // 销售总额(已计算)
+
+ // 平均订单金额(总订单数)
+ $totalOrderCount = TrafficOrderModel::whereIn('ownerWechatId', $ownerWechatIds)
+ ->where('createTime', '>=', $startTime)
+ ->where('createTime', '<', $endTime)
+ ->count();
+ $avgOrderAmount = $totalOrderCount > 0 ? round($totalSales / $totalOrderCount, 2) : 0;
+
+ // 高价值客户(消费超过平均订单金额的客户)
+ // 先获取每个客户的消费总额
+ $customerTotalSpend = TrafficOrderModel::whereIn('ownerWechatId', $ownerWechatIds)
+ ->where('createTime', '>=', $startTime)
+ ->where('createTime', '<', $endTime)
+ ->field('identifier, SUM(actualPay) as totalSpend')
+ ->group('identifier')
+ ->select();
+
+ $highValueCustomerCount = 0;
+ $avgCustomerSpend = $convertedCustomerCount > 0 ? ($totalSales / $convertedCustomerCount) : 0;
+ foreach ($customerTotalSpend as $customer) {
+ if ($customer['totalSpend'] > $avgCustomerSpend) {
+ $highValueCustomerCount++;
+ }
+ }
+
+ // 高价值客户百分比
+ $totalCustomersForCalc = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds)
+ ->where('isDeleted', 0)
+ ->count();
+ $highValueCustomerPercent = $totalCustomersForCalc > 0 ? round(($highValueCustomerCount / $totalCustomersForCalc) * 100, 1) : 0;
+
+ // ========== 3. 增长趋势 ==========
+ // 上期销售额
+ $lastTotalSales = TrafficOrderModel::whereIn('ownerWechatId', $ownerWechatIds)
+ ->where('createTime', '>=', $lastStartTime)
+ ->where('createTime', '<', $lastEndTime)
+ ->sum('actualPay');
+ $lastTotalSales = $lastTotalSales ?: 0;
+
+ // 周收益增长(金额差值)
+ $weeklyRevenueGrowth = round($totalSales - $lastTotalSales, 2);
+
+ // 新客转化(新客户中有订单的人数)
+ $newCustomers = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds)
+ ->where('createTime', '>=', $startTime)
+ ->where('createTime', '<', $endTime)
+ ->column('wechatId');
+
+ // 获取新客户中有订单的(identifier 对应 wechatId)
+ $newConvertedCustomers = 0;
+ if (!empty($newCustomers)) {
+ $newConvertedCustomers = TrafficOrderModel::whereIn('ownerWechatId', $ownerWechatIds)
+ ->where('createTime', '>=', $startTime)
+ ->where('createTime', '<', $endTime)
+ ->whereIn('identifier', $newCustomers)
+ ->group('identifier')
+ ->count();
+ }
+
+ // 活跃客户增长(有互动的客户)
+ $activeCustomers = WechatMessageModel::whereIn('wechatAccountId', $wechatAccountIds)
+ ->where('createTime', '>=', $startTime)
+ ->where('createTime', '<', $endTime)
+ ->group('wechatFriendId')
+ ->count();
+
+ $lastActiveCustomers = WechatMessageModel::whereIn('wechatAccountId', $wechatAccountIds)
+ ->where('createTime', '>=', $lastStartTime)
+ ->where('createTime', '<', $lastEndTime)
+ ->group('wechatFriendId')
+ ->count();
+
+ // 活跃客户增长(人数差值)
+ $activeCustomerGrowth = $activeCustomers - $lastActiveCustomers;
+
+ // ========== 4. 客户活跃度 ==========
+ // 按天统计每个客户的互动次数,然后分类
+ // 高频互动用户数(平均每天3次以上)
+ $days = max(1, ($endTime - $startTime) / 86400); // 计算天数
+ $highFrequencyThreshold = $days * 3; // 高频阈值
+
+ $highFrequencyUsers = WechatMessageModel::whereIn('wechatAccountId', $wechatAccountIds)
+ ->where('createTime', '>=', $startTime)
+ ->where('createTime', '<', $endTime)
+ ->field('wechatFriendId, COUNT(*) as count')
+ ->group('wechatFriendId')
+ ->having('count > ' . $highFrequencyThreshold)
+ ->count();
+
+ // 中频互动用户数(平均每天1-3次)
+ $midFrequencyThreshold = $days * 1;
+ $midFrequencyUsers = WechatMessageModel::whereIn('wechatAccountId', $wechatAccountIds)
+ ->where('createTime', '>=', $startTime)
+ ->where('createTime', '<', $endTime)
+ ->field('wechatFriendId, COUNT(*) as count')
+ ->group('wechatFriendId')
+ ->having('count >= ' . $midFrequencyThreshold . ' AND count <= ' . $highFrequencyThreshold)
+ ->count();
+
+ // 低频互动用户数(少于平均每天1次)
+ $lowFrequencyUsers = WechatMessageModel::whereIn('wechatAccountId', $wechatAccountIds)
+ ->where('createTime', '>=', $startTime)
+ ->where('createTime', '<', $endTime)
+ ->field('wechatFriendId, COUNT(*) as count')
+ ->group('wechatFriendId')
+ ->having('count < ' . $midFrequencyThreshold)
+ ->count();
+
+ $frequency_analysis = [
+ ['name' => '高频', 'value' => $highFrequencyUsers],
+ ['name' => '中频', 'value' => $midFrequencyUsers],
+ ['name' => '低频', 'value' => $lowFrequencyUsers]
+ ];
+
+ // ========== 5. 转化客户来源 ==========
+ // 只统计有订单的客户来源(identifier 对应 wechatId)
+ $convertedFriendIds = TrafficOrderModel::whereIn('ownerWechatId', $ownerWechatIds)
+ ->where('createTime', '>=', $startTime)
+ ->where('createTime', '<', $endTime)
+ ->group('identifier')
+ ->column('identifier');
+
+ $friendRecommend = 0;
+ $wechatSearch = 0;
+ $wechatGroup = 0;
+
+ if (!empty($convertedFriendIds)) {
+ // 朋友推荐(有订单的)
+ $friendRecommend = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds)
+ ->whereIn('wechatId', $convertedFriendIds)
+ ->whereIn('addFrom', [17, 1000017])
+ ->count();
+
+ // 微信搜索(有订单的)
+ $wechatSearch = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds)
+ ->whereIn('wechatId', $convertedFriendIds)
+ ->whereIn('addFrom', [3, 15, 1000003, 1000015])
+ ->count();
+
+ // 微信群(有订单的)
+ $wechatGroup = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds)
+ ->whereIn('wechatId', $convertedFriendIds)
+ ->whereIn('addFrom', [14, 1000014])
+ ->count();
+ }
+
+ $totalConvertedCustomers = $convertedCustomerCount;
+ $otherSource = max(0, $totalConvertedCustomers - $friendRecommend - $wechatSearch - $wechatGroup);
+
+ // 计算百分比
+ $calculatePercentage = function ($value) use ($totalConvertedCustomers) {
+ if ($totalConvertedCustomers <= 0) return 0;
+ return round(($value / $totalConvertedCustomers) * 100, 2);
+ };
+
+ $sourceDistribution = [
+ [
+ 'name' => '朋友推荐',
+ 'value' => $calculatePercentage($friendRecommend) . '%',
+ 'count' => $friendRecommend
+ ],
+ [
+ 'name' => '微信搜索',
+ 'value' => $calculatePercentage($wechatSearch) . '%',
+ 'count' => $wechatSearch
+ ],
+ [
+ 'name' => '微信群',
+ 'value' => $calculatePercentage($wechatGroup) . '%',
+ 'count' => $wechatGroup
+ ]
+ ];
+
+ // 构建返回数据
+ $data = [
+ 'avg_conversion_amount' => $avgConversionAmount, // 客户平均转化金额
+ 'value_indicators' => [
+ 'total_sales' => round($totalSales, 2), // 销售总额
+ 'avg_order_amount' => $avgOrderAmount, // 平均订单金额
+ 'high_value_customers' => $highValueCustomerPercent . '%' // 高价值客户
+ ],
+ 'growth_trend' => [
+ 'weekly_revenue_growth' => $weeklyRevenueGrowth, // 周收益增长(金额)
+ 'new_customer_conversion' => $newConvertedCustomers, // 新客转化(人数)
+ 'active_customer_growth' => $activeCustomerGrowth // 活跃客户增长(人数差值)
+ ],
+ 'frequency_analysis' => $frequency_analysis, // 客户活跃度
+ 'source_distribution' => $sourceDistribution // 转化客户来源
+ ];
+
+ return successJson($data);
+ } catch (\Exception $e) {
+ return errorJson('获取互动分析数据失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 获取时间范围
+ *
+ * @param bool $toTimestamp 是否将日期转为时间戳,默认为true
+ * @return array 时间范围数组
+ */
+ private function getTimeRange($toTimestamp = true)
+ {
+ // 可选:today, yesterday, this_week, last_week, this_month, this_quarter, this_year
+ $timeType = input('time_type', 'this_week');
+
+ switch ($timeType) {
+ case 'today': // 今日
+ $startTime = date('Y-m-d');
+ $endTime = date('Y-m-d', strtotime('+1 day'));
+ $lastStartTime = date('Y-m-d', strtotime('-1 day')); // 昨日
+ $lastEndTime = $startTime;
+ break;
+
+ case 'yesterday': // 昨日
+ $startTime = date('Y-m-d', strtotime('-1 day'));
+ $endTime = date('Y-m-d');
+ $lastStartTime = date('Y-m-d', strtotime('-2 day')); // 前日
+ $lastEndTime = $startTime;
+ break;
+
+ case 'this_week': // 本周
+ $startTime = date('Y-m-d', strtotime('monday this week'));
+ $endTime = date('Y-m-d', strtotime('monday next week'));
+ $lastStartTime = date('Y-m-d', strtotime('monday last week')); // 上周一
+ $lastEndTime = $startTime;
+ break;
+
+ case 'last_week': // 上周
+ $startTime = date('Y-m-d', strtotime('monday last week'));
+ $endTime = date('Y-m-d', strtotime('monday this week'));
+ $lastStartTime = date('Y-m-d', strtotime('monday last week', strtotime('last week'))); // 上上周一
+ $lastEndTime = $startTime;
+ break;
+
+ case 'this_month': // 本月
+ $startTime = date('Y-m-01');
+ $endTime = date('Y-m-d', strtotime(date('Y-m-01') . ' +1 month'));
+ $lastStartTime = date('Y-m-01', strtotime('-1 month')); // 上月初
+ $lastEndTime = $startTime;
+ break;
+
+ case 'this_quarter': // 本季度
+ $month = date('n');
+ $quarter = ceil($month / 3);
+ $startMonth = ($quarter - 1) * 3 + 1;
+ $startTime = date('Y-') . str_pad($startMonth, 2, '0', STR_PAD_LEFT) . '-01';
+ $endTime = date('Y-m-d', strtotime($startTime . ' +3 month'));
+ // 上季度
+ $lastStartTime = date('Y-m-d', strtotime($startTime . ' -3 month'));
+ $lastEndTime = $startTime;
+ break;
+
+ case 'this_year': // 本年度
+ $startTime = date('Y-01-01');
+ $endTime = (date('Y') + 1) . '-01-01';
+ $lastStartTime = (date('Y') - 1) . '-01-01'; // 去年初
+ $lastEndTime = $startTime;
+ break;
+
+ default:
+ $startTime = date('Y-m-d', strtotime('monday this week'));
+ $endTime = date('Y-m-d', strtotime('monday next week'));
+ $lastStartTime = date('Y-m-d', strtotime('monday last week'));
+ $lastEndTime = $startTime;
+ }
+
+ // 如果需要转换为时间戳
+ if ($toTimestamp) {
+ $startTime = strtotime($startTime);
+ $endTime = strtotime($endTime);
+ $lastStartTime = strtotime($lastStartTime);
+ $lastEndTime = strtotime($lastEndTime);
+ }
+
+ return [
+ 'start_time' => $startTime,
+ 'end_time' => $endTime,
+ 'last_start_time' => $lastStartTime,
+ 'last_end_time' => $lastEndTime
+ ];
+ }
+
+ /**
+ * 计算环比增长率
+ */
+ private function calculateGrowth($current, $last)
+ {
+ if ($last == 0) {
+ return $current > 0 ? 100 : 0;
+ }
+ return round((($current - $last) / $last) * 100, 1);
+ }
+}
\ No newline at end of file
diff --git a/application/store/controller/SystemConfigController.php b/application/store/controller/SystemConfigController.php
new file mode 100644
index 0000000..6eaf8cf
--- /dev/null
+++ b/application/store/controller/SystemConfigController.php
@@ -0,0 +1,151 @@
+device['id'] ?? 0;
+ if (!$deviceId) {
+ return $this->error('设备不存在');
+ }
+
+ // 从新表中获取配置
+ $config = Db::name('device_taskconf')
+ ->where('deviceId', $deviceId)
+ ->field('id,autoLike,autoCustomerDev,groupMessageDeliver,autoGroup,contentSync,aiChat,autoReply,momentsSync')
+ ->find();
+
+ // 如果没有找到配置,创建默认配置
+ if (empty($config)) {
+ $taskConfig = [
+ 'deviceId' => $deviceId,
+ 'autoLike' => 0,
+ 'autoCustomerDev' => 0,
+ 'groupMessageDeliver' => 0,
+ 'autoGroup' => 0,
+ 'contentSync' => 0,
+ 'aiChat' => 0,
+ 'autoReply' => 0,
+ 'momentsSync' => 0,
+ 'companyId' => $this->device['companyId'] ?? 0,
+ 'createTime' => time(),
+ 'updateTime' => time()
+ ];
+
+ // 添加到数据库
+ Db::name('device_taskconf')->insert($taskConfig);
+
+ // 返回默认配置
+ return successJson($taskConfig);
+ }
+
+ // 返回开关状态
+ return successJson($config);
+
+ } catch (\Exception $e) {
+ Log::error('获取开关状态异常:' . $e->getMessage());
+ return $this->error('获取开关状态失败');
+ }
+ }
+
+ /**
+ * 更新系统开关状态
+ *
+ * @return \think\Response
+ */
+ public function updateSwitchStatus()
+ {
+ try {
+ // 获取参数
+ if (empty($this->device)) {
+ return errorJson('设备不存在');
+ }
+
+ $switchName = $this->request->param('switchName');
+ $deviceId = $this->device['id'];
+
+ if (empty($switchName)) {
+ return errorJson('开关名称不能为空');
+ }
+
+ // 验证开关名称是否有效
+ $validSwitches = ['autoLike', 'autoCustomerDev', 'groupMessageDeliver', 'autoGroup', 'contentSync', 'aiChat', 'autoReply', 'momentsSync'];
+ if (!in_array($switchName, $validSwitches)) {
+ return errorJson('无效的开关名称');
+ }
+
+ // 获取当前配置
+ $taskConfig = Db::name('device_taskconf')
+ ->where('deviceId', $deviceId)
+ ->find();
+
+ // 如果没有找到配置,创建默认配置
+ if (empty($taskConfig)) {
+ $taskConfig = [
+ 'deviceId' => $deviceId,
+ 'autoLike' => 0,
+ 'autoCustomerDev' => 0,
+ 'groupMessageDeliver' => 0,
+ 'autoGroup' => 0,
+ 'contentSync' => 0,
+ 'aiChat' => 0,
+ 'autoReply' => 0,
+ 'momentsSync' => 0,
+ 'companyId' => $this->device['companyId'] ?? 0,
+ 'createTime' => time(),
+ 'updateTime' => time()
+ ];
+
+ // 设置要更新的开关
+ $taskConfig[$switchName] = 1;
+
+ // 添加到数据库
+ Db::name('device_taskconf')->insert($taskConfig);
+ } else {
+ // 更新指定开关状态
+ $updateData = [
+ $switchName => !$taskConfig[$switchName],
+ 'updateTime' => time()
+ ];
+
+ // 更新数据库
+ $result = Db::name('device_taskconf')
+ ->where('deviceId', $deviceId)
+ ->update($updateData);
+
+ if ($result === false) {
+ Log::error("更新设备{$switchName}开关状态失败,设备ID:{$deviceId}");
+ return errorJson('更新失败');
+ }
+ }
+
+ // 清除缓存
+ $this->clearDeviceCache();
+
+ return successJson([], '更新成功');
+
+ } catch (\Exception $e) {
+ return errorJson('系统错误'. $e->getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/store/controller/TrafficPackage.php b/application/store/controller/TrafficPackage.php
new file mode 100644
index 0000000..664ce70
--- /dev/null
+++ b/application/store/controller/TrafficPackage.php
@@ -0,0 +1,97 @@
+field([
+ 'id',
+ 'name',
+ 'tags',
+ 'originalPrice',
+ 'price',
+ 'monthlyTraffic',
+ 'duration',
+ 'privileges',
+ 'createTime'
+ ])->select();
+
+ // 处理数据
+ $list = collection($list)->each(function($item) {
+ // 添加计算字段
+ $item['discount'] = $item->discount; // 折扣
+ $item['totalTraffic'] = $item->totalTraffic; // 总流量
+ // 确保特权是数组格式
+ $item['privileges'] = $item->privileges; // 使用模型的获取器处理特权
+ // 格式化时间
+ $item['createTime'] = date('Y-m-d H:i:s', strtotime($item['createTime']));
+ return $item;
+ });
+ return successJson($list,'获取成功');
+ }
+
+
+ /**
+ * 获取当前套餐使用情况
+ * @return \think\response\Json
+ */
+ public function getUsage()
+ {
+ // 获取用户ID,可以从session或token中获取
+ $userId = input('userId', 0, 'intval');
+ if (empty($userId)) {
+ return errorJson('请先登录');
+ }
+
+ // 获取用户当前生效的套餐订单
+ $order = model('TrafficPackageOrder')
+ ->where('userId', $userId)
+ ->where('status', 1) // 1表示生效中
+ ->where('expireTime', '>', time()) // 未过期
+ ->order('expireTime', 'desc') // 取最晚过期的
+ ->find();
+
+ if (empty($order)) {
+ return errorJson('未找到有效的套餐');
+ }
+
+ // 获取套餐详情
+ $package = TrafficPackageModel::get($order['packageId']);
+ if (empty($package)) {
+ return errorJson('套餐信息不存在');
+ }
+
+ // 计算套餐使用情况
+ $totalUsers = $package['monthlyTraffic'] * $package['duration']; // 总人数
+ $usedUsers = model('TrafficUsageLog')
+ ->where('orderId', $order['id'])
+ ->count(); // 已使用人数
+
+ // 计算剩余有效期(天数)
+ $remainDays = ceil(($order['expireTime'] - time()) / (60 * 60 * 24));
+ $remainDays = max(0, $remainDays); // 确保不会出现负数
+
+ $data = [
+ 'packageName' => $package['name'], // 套餐名称
+ 'totalUsers' => $totalUsers, // 总人数
+ 'usedUsers' => $usedUsers, // 已使用人数
+ 'remainUsers' => $totalUsers - $usedUsers, // 剩余可用人数
+ 'remainDays' => $remainDays, // 剩余有效期(天)
+ 'expireTime' => date('Y-m-d', $order['expireTime']), // 过期时间
+ 'usagePercent' => $totalUsers > 0 ? round(($usedUsers / $totalUsers) * 100, 1) : 0, // 使用百分比
+ ];
+
+ return successJson($data, '获取成功');
+ }
+
+}
\ No newline at end of file
diff --git a/application/store/controller/VendorController.php b/application/store/controller/VendorController.php
new file mode 100644
index 0000000..929bade
--- /dev/null
+++ b/application/store/controller/VendorController.php
@@ -0,0 +1,534 @@
+request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $keyword = $this->request->param('keyword', '');
+ $status = $this->request->param('status', '');
+
+ $where = [
+ ['isDel', '=', 0]
+ ];
+
+ // 关键词搜索
+ if (!empty($keyword)) {
+ $where[] = ['name', 'like', "%{$keyword}%"];
+ }
+
+ // 状态筛选
+ if ($status !== '') {
+ $where[] = ['status', '=', $status];
+ }
+
+ $list = VendorPackageModel::where($where)
+ ->order('id', 'desc')
+ ->page($page, $limit)
+ ->select();
+
+ $total = VendorPackageModel::where($where)->count();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ ]
+ ]);
+ } catch (\Exception $e) {
+ Log::error('获取套餐列表失败:' . $e->getMessage());
+ return json(['code' => 500, 'msg' => '获取失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 获取套餐详情
+ *
+ * @return \think\response\Json
+ */
+ public function detail()
+ {
+ try {
+ $id = $this->request->param('id', 0);
+
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ // 查询套餐基本信息
+ $package = VendorPackageModel::where([
+ ['id', '=', $id],
+ ['isDel', '=', 0]
+ ])->find();
+
+ if (empty($package)) {
+ return json(['code' => 404, 'msg' => '套餐不存在']);
+ }
+
+ // 查询项目列表
+ $projects = VendorProjectModel::where([
+ ['packageId', '=', $id],
+ ['isDel', '=', 0]
+ ])->select();
+
+ $package['projects'] = $projects;
+
+ return json(['code' => 200, 'msg' => '获取成功', 'data' => $package]);
+ } catch (\Exception $e) {
+ Log::error('获取套餐详情失败:' . $e->getMessage());
+ return json(['code' => 500, 'msg' => '获取失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 添加套餐
+ *
+ * @return \think\response\Json
+ */
+ public function add()
+ {
+ try {
+ if (!$this->request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ $param = $this->request->post();
+
+ // 参数验证
+ if (empty($param['name'])) {
+ return json(['code' => 400, 'msg' => '套餐名称不能为空']);
+ }
+
+ // 检查名称是否已存在
+ $exists = VendorPackageModel::where([
+ ['name', '=', $param['name']],
+ ['isDel', '=', 0]
+ ])->find();
+
+ if ($exists) {
+ return json(['code' => 400, 'msg' => '该套餐名称已存在']);
+ }
+
+ Db::startTrans();
+ try {
+ // 创建套餐
+ $package = new VendorPackageModel;
+ $package->name = $param['name'];
+ $package->originalPrice = $param['originalPrice'] ?? 0;
+ $package->price = $param['price'] ?? 0;
+ $package->discount = $param['discount'] ?? 0;
+ $package->advancePayment = $param['advancePayment'] ?? 0;
+ $package->tags = $param['tags'] ?? '';
+ $package->description = $param['description'] ?? '';
+ $package->cover = $param['cover'] ?? '';
+ $package->status = $param['status'] ?? 1;
+ $package->createTime = time();
+ $package->updateTime = time();
+ $package->save();
+
+ // 处理项目信息
+ if (!empty($param['projects']) && is_array($param['projects'])) {
+ foreach ($param['projects'] as $projectData) {
+ if (empty($projectData['name'])) {
+ continue;
+ }
+
+ // 创建项目
+ $project = new VendorProjectModel;
+ $project->packageId = $package->id;
+ $project->name = $projectData['name'];
+ $project->originalPrice = $projectData['originalPrice'] ?? 0;
+ $project->price = $projectData['price'] ?? 0;
+ $project->duration = $projectData['duration'] ?? 0;
+ $project->image = $projectData['image'] ?? '';
+ $project->detail = $projectData['detail'] ?? '';
+ $project->createTime = time();
+ $project->updateTime = time();
+ $project->save();
+ }
+ }
+
+ Db::commit();
+ return json(['code' => 200, 'msg' => '添加成功', 'data' => ['id' => $package->id]]);
+ } catch (\Exception $e) {
+ Db::rollback();
+ Log::error('添加套餐失败:' . $e->getMessage());
+ return json(['code' => 500, 'msg' => '添加失败:' . $e->getMessage()]);
+ }
+ } catch (\Exception $e) {
+ Log::error('添加套餐异常:' . $e->getMessage());
+ return json(['code' => 500, 'msg' => '添加异常:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 编辑套餐
+ *
+ * @return \think\response\Json
+ */
+ public function edit()
+ {
+ try {
+ if (!$this->request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ $param = $this->request->post();
+
+ // 参数验证
+ if (empty($param['id'])) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ if (empty($param['name'])) {
+ return json(['code' => 400, 'msg' => '套餐名称不能为空']);
+ }
+
+ // 检查套餐是否存在
+ $package = VendorPackageModel::where([
+ ['id', '=', $param['id']],
+ ['isDel', '=', 0]
+ ])->find();
+
+ if (!$package) {
+ return json(['code' => 404, 'msg' => '套餐不存在']);
+ }
+
+ // 检查名称是否已存在
+ $exists = VendorPackageModel::where([
+ ['name', '=', $param['name']],
+ ['id', '<>', $param['id']],
+ ['isDel', '=', 0]
+ ])->find();
+
+ if ($exists) {
+ return json(['code' => 400, 'msg' => '该套餐名称已存在']);
+ }
+
+ Db::startTrans();
+ try {
+ // 更新套餐
+ $package->name = $param['name'];
+ $package->originalPrice = $param['originalPrice'] ?? $package->originalPrice;
+ $package->price = $param['price'] ?? $package->price;
+ $package->discount = $param['discount'] ?? $package->discount;
+ $package->advancePayment = $param['advancePayment'] ?? $package->advancePayment;
+ $package->tags = $param['tags'] ?? $package->tags;
+ $package->description = $param['description'] ?? $package->description;
+ $package->cover = $param['cover'] ?? $package->cover;
+ $package->status = $param['status'] ?? $package->status;
+ $package->updateTime = time();
+ $package->save();
+
+ Db::commit();
+ return json(['code' => 200, 'msg' => '更新成功']);
+ } catch (\Exception $e) {
+ Db::rollback();
+ Log::error('更新套餐失败:' . $e->getMessage());
+ return json(['code' => 500, 'msg' => '更新失败:' . $e->getMessage()]);
+ }
+ } catch (\Exception $e) {
+ Log::error('编辑套餐异常:' . $e->getMessage());
+ return json(['code' => 500, 'msg' => '编辑异常:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 删除套餐
+ *
+ * @return \think\response\Json
+ */
+ public function delete()
+ {
+ try {
+ $id = $this->request->param('id', 0);
+
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ // 检查套餐是否存在
+ $package = VendorPackageModel::where([
+ ['id', '=', $id],
+ ['isDel', '=', 0]
+ ])->find();
+
+ if (!$package) {
+ return json(['code' => 404, 'msg' => '套餐不存在']);
+ }
+
+ Db::startTrans();
+ try {
+ // 软删除套餐
+ $package->isDel = 1;
+ $package->updateTime = time();
+ $package->save();
+
+ // 软删除关联的项目
+ VendorProjectModel::where('packageId', $id)
+ ->update([
+ 'isDel' => 1,
+ 'updateTime' => time()
+ ]);
+
+ Db::commit();
+ return json(['code' => 200, 'msg' => '删除成功']);
+ } catch (\Exception $e) {
+ Db::rollback();
+ Log::error('删除套餐失败:' . $e->getMessage());
+ return json(['code' => 500, 'msg' => '删除失败:' . $e->getMessage()]);
+ }
+ } catch (\Exception $e) {
+ Log::error('删除套餐异常:' . $e->getMessage());
+ return json(['code' => 500, 'msg' => '删除异常:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 添加项目
+ *
+ * @return \think\response\Json
+ */
+ public function addProject()
+ {
+ try {
+ if (!$this->request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ $param = $this->request->post();
+
+ // 参数验证
+ if (empty($param['packageId'])) {
+ return json(['code' => 400, 'msg' => '套餐ID不能为空']);
+ }
+
+ if (empty($param['name'])) {
+ return json(['code' => 400, 'msg' => '项目名称不能为空']);
+ }
+
+ // 检查套餐是否存在
+ $package = VendorPackageModel::where([
+ ['id', '=', $param['packageId']],
+ ['isDel', '=', 0]
+ ])->find();
+
+ if (!$package) {
+ return json(['code' => 404, 'msg' => '套餐不存在']);
+ }
+
+ try {
+ // 创建项目
+ $project = new VendorProjectModel;
+ $project->packageId = $param['packageId'];
+ $project->name = $param['name'];
+ $project->originalPrice = $param['originalPrice'] ?? 0;
+ $project->price = $param['price'] ?? 0;
+ $project->duration = $param['duration'] ?? 0;
+ $project->image = $param['image'] ?? '';
+ $project->detail = $param['detail'] ?? '';
+ $project->createTime = time();
+ $project->updateTime = time();
+ $project->save();
+
+ return json(['code' => 200, 'msg' => '添加成功', 'data' => ['id' => $project->id]]);
+ } catch (\Exception $e) {
+ Log::error('添加项目失败:' . $e->getMessage());
+ return json(['code' => 500, 'msg' => '添加失败:' . $e->getMessage()]);
+ }
+ } catch (\Exception $e) {
+ Log::error('添加项目异常:' . $e->getMessage());
+ return json(['code' => 500, 'msg' => '添加异常:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 编辑项目
+ *
+ * @return \think\response\Json
+ */
+ public function editProject()
+ {
+ try {
+ if (!$this->request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ $param = $this->request->post();
+
+ // 参数验证
+ if (empty($param['id'])) {
+ return json(['code' => 400, 'msg' => '项目ID不能为空']);
+ }
+
+ if (empty($param['name'])) {
+ return json(['code' => 400, 'msg' => '项目名称不能为空']);
+ }
+
+ // 检查项目是否存在
+ $project = VendorProjectModel::where([
+ ['id', '=', $param['id']],
+ ['isDel', '=', 0]
+ ])->find();
+
+ if (!$project) {
+ return json(['code' => 404, 'msg' => '项目不存在']);
+ }
+
+ try {
+ // 更新项目
+ $project->name = $param['name'];
+ $project->originalPrice = $param['originalPrice'] ?? $project->originalPrice;
+ $project->price = $param['price'] ?? $project->price;
+ $project->duration = $param['duration'] ?? $project->duration;
+ $project->image = $param['image'] ?? $project->image;
+ $project->detail = $param['detail'] ?? $project->detail;
+ $project->updateTime = time();
+ $project->save();
+
+ return json(['code' => 200, 'msg' => '更新成功']);
+ } catch (\Exception $e) {
+ Log::error('更新项目失败:' . $e->getMessage());
+ return json(['code' => 500, 'msg' => '更新失败:' . $e->getMessage()]);
+ }
+ } catch (\Exception $e) {
+ Log::error('编辑项目异常:' . $e->getMessage());
+ return json(['code' => 500, 'msg' => '编辑异常:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 删除项目
+ *
+ * @return \think\response\Json
+ */
+ public function deleteProject()
+ {
+ try {
+ $id = $this->request->param('id', 0);
+
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ // 检查项目是否存在
+ $project = VendorProjectModel::where([
+ ['id', '=', $id],
+ ['isDel', '=', 0]
+ ])->find();
+
+ if (!$project) {
+ return json(['code' => 404, 'msg' => '项目不存在']);
+ }
+
+ try {
+ // 软删除项目
+ $project->isDel = 1;
+ $project->updateTime = time();
+ $project->save();
+
+ return json(['code' => 200, 'msg' => '删除成功']);
+ } catch (\Exception $e) {
+ Log::error('删除项目失败:' . $e->getMessage());
+ return json(['code' => 500, 'msg' => '删除失败:' . $e->getMessage()]);
+ }
+ } catch (\Exception $e) {
+ Log::error('删除项目异常:' . $e->getMessage());
+ return json(['code' => 500, 'msg' => '删除异常:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 创建订单
+ *
+ * @return \think\response\Json
+ */
+ public function createOrder()
+ {
+ try {
+ if (!$this->request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ $param = $this->request->post();
+
+ // 参数验证
+ if (empty($param['packageId'])) {
+ return json(['code' => 400, 'msg' => '套餐ID不能为空']);
+ }
+
+ // 检查套餐是否存在
+ $package = VendorPackageModel::where([
+ ['id', '=', $param['packageId']],
+ ['isDel', '=', 0],
+ ['status', '=', 1]
+ ])->find();
+
+ if (!$package) {
+ return json(['code' => 404, 'msg' => '套餐不存在或已下架']);
+ }
+
+ // 获取当前用户信息
+ $userId = $this->request->userInfo['id'];
+
+ if (empty($userId)) {
+ return json(['code' => 401, 'msg' => '请先登录']);
+ }
+
+ Db::startTrans();
+ try {
+ // 生成订单
+ $order = new VendorOrderModel;
+ $order->orderNo = VendorOrderModel::generateOrderNo();
+ $order->userId = $userId;
+ $order->packageId = $package->id;
+ $order->packageName = $package->name;
+ $order->totalAmount = $package->price;
+ $order->payAmount = $package->price;
+ $order->advancePayment = $package->advancePayment;
+ $order->status = VendorOrderModel::STATUS_UNPAID;
+ $order->remark = $param['remark'] ?? '';
+ $order->createTime = time();
+ $order->updateTime = time();
+ $order->save();
+
+ Db::commit();
+ return json([
+ 'code' => 200,
+ 'msg' => '订单创建成功',
+ 'data' => [
+ 'orderId' => $order->id,
+ 'orderNo' => $order->orderNo
+ ]
+ ]);
+ } catch (\Exception $e) {
+ Db::rollback();
+ Log::error('创建订单失败:' . $e->getMessage());
+ return json(['code' => 500, 'msg' => '创建订单失败:' . $e->getMessage()]);
+ }
+ } catch (\Exception $e) {
+ Log::error('创建订单异常:' . $e->getMessage());
+ return json(['code' => 500, 'msg' => '创建订单异常:' . $e->getMessage()]);
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/store/controller/VendorOrderController.php b/application/store/controller/VendorOrderController.php
new file mode 100644
index 0000000..f63e224
--- /dev/null
+++ b/application/store/controller/VendorOrderController.php
@@ -0,0 +1,229 @@
+request->param('page', 1);
+ $limit = $this->request->param('limit', 10);
+ $status = $this->request->param('status', '');
+ $keyword = $this->request->param('keyword', '');
+
+ // 获取当前用户信息
+ $userId = $this->request->userInfo['id'];
+
+ $where = [
+ ['userId', '=', $userId]
+ ];
+
+ // 关键词搜索
+ if (!empty($keyword)) {
+ $where[] = ['orderNo|packageName', 'like', "%{$keyword}%"];
+ }
+
+ // 状态筛选
+ if ($status !== '') {
+ $where[] = ['status', '=', $status];
+ }
+
+ $list = VendorOrderModel::with(['package'])
+ ->where($where)
+ ->order('id', 'desc')
+ ->page($page, $limit)
+ ->select();
+
+ $total = VendorOrderModel::where($where)->count();
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => [
+ 'list' => $list,
+ 'total' => $total,
+ 'page' => $page,
+ 'limit' => $limit
+ ]
+ ]);
+ } catch (\Exception $e) {
+ Log::error('获取订单列表失败:' . $e->getMessage());
+ return json(['code' => 500, 'msg' => '获取失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 获取订单详情
+ *
+ * @return \think\response\Json
+ */
+ public function detail()
+ {
+ try {
+ $id = $this->request->param('id', 0);
+
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ // 获取当前用户信息
+ $userId = $this->request->userInfo['id'];
+
+ // 查询订单
+ $order = VendorOrderModel::with(['package'])
+ ->where([
+ ['id', '=', $id],
+ ['userId', '=', $userId]
+ ])->find();
+
+ if (empty($order)) {
+ return json(['code' => 404, 'msg' => '订单不存在']);
+ }
+
+ // 查询套餐项目
+ if (!empty($order['package'])) {
+ $projects = VendorProjectModel::where([
+ ['packageId', '=', $order['packageId']],
+ ['isDel', '=', 0]
+ ])->select();
+
+ $order['package']['projects'] = $projects;
+ }
+
+ return json(['code' => 200, 'msg' => '获取成功', 'data' => $order]);
+ } catch (\Exception $e) {
+ Log::error('获取订单详情失败:' . $e->getMessage());
+ return json(['code' => 500, 'msg' => '获取失败:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 更新订单状态
+ *
+ * @return \think\response\Json
+ */
+ public function updateStatus()
+ {
+ try {
+ if (!$this->request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ $param = $this->request->post();
+
+ // 参数验证
+ if (empty($param['id'])) {
+ return json(['code' => 400, 'msg' => '订单ID不能为空']);
+ }
+
+ if (!isset($param['status'])) {
+ return json(['code' => 400, 'msg' => '订单状态不能为空']);
+ }
+
+ // 检查订单是否存在
+ $order = VendorOrderModel::where('id', $param['id'])->find();
+
+ if (!$order) {
+ return json(['code' => 404, 'msg' => '订单不存在']);
+ }
+
+ // 检查状态是否有效
+ $validStatus = [
+ VendorOrderModel::STATUS_UNPAID,
+ VendorOrderModel::STATUS_PAID,
+ VendorOrderModel::STATUS_COMPLETED,
+ VendorOrderModel::STATUS_CANCELED
+ ];
+
+ if (!in_array($param['status'], $validStatus)) {
+ return json(['code' => 400, 'msg' => '无效的订单状态']);
+ }
+
+ // 更新订单状态
+ $updateData = [
+ 'status' => $param['status'],
+ 'updateTime' => time()
+ ];
+
+ // 如果订单状态为已支付,记录支付时间
+ if ($param['status'] == VendorOrderModel::STATUS_PAID) {
+ $updateData['payTime'] = time();
+ }
+
+ try {
+ $order->save($updateData);
+ return json(['code' => 200, 'msg' => '更新成功']);
+ } catch (\Exception $e) {
+ Log::error('更新订单状态失败:' . $e->getMessage());
+ return json(['code' => 500, 'msg' => '更新失败:' . $e->getMessage()]);
+ }
+ } catch (\Exception $e) {
+ Log::error('更新订单状态异常:' . $e->getMessage());
+ return json(['code' => 500, 'msg' => '更新异常:' . $e->getMessage()]);
+ }
+ }
+
+ /**
+ * 取消订单
+ *
+ * @return \think\response\Json
+ */
+ public function cancel()
+ {
+ try {
+ if (!$this->request->isPost()) {
+ return json(['code' => 400, 'msg' => '请求方式错误']);
+ }
+
+ $id = $this->request->param('id', 0);
+
+ if (empty($id)) {
+ return json(['code' => 400, 'msg' => '参数错误']);
+ }
+
+ // 获取当前用户信息
+ $userId = $this->request->userInfo['id'];
+
+ // 检查订单是否存在
+ $order = VendorOrderModel::where([
+ ['id', '=', $id],
+ ['userId', '=', $userId],
+ ['status', '=', VendorOrderModel::STATUS_UNPAID]
+ ])->find();
+
+ if (!$order) {
+ return json(['code' => 404, 'msg' => '订单不存在或状态不允许取消']);
+ }
+
+ try {
+ // 更新订单状态为已取消
+ $order->status = VendorOrderModel::STATUS_CANCELED;
+ $order->updateTime = time();
+ $order->save();
+
+ return json(['code' => 200, 'msg' => '取消成功']);
+ } catch (\Exception $e) {
+ Log::error('取消订单失败:' . $e->getMessage());
+ return json(['code' => 500, 'msg' => '取消失败:' . $e->getMessage()]);
+ }
+ } catch (\Exception $e) {
+ Log::error('取消订单异常:' . $e->getMessage());
+ return json(['code' => 500, 'msg' => '取消异常:' . $e->getMessage()]);
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/store/model/FlowPackageModel.php b/application/store/model/FlowPackageModel.php
new file mode 100644
index 0000000..1977aa8
--- /dev/null
+++ b/application/store/model/FlowPackageModel.php
@@ -0,0 +1,64 @@
+ 'array',
+ ];
+
+ /**
+ * 特权字段获取器 - 将多行文本转换为数组
+ * @param $value
+ * @return array
+ */
+ public function getPrivilegesAttr($value)
+ {
+ if (empty($value)) {
+ return [];
+ }
+
+ // 如果已经是数组则直接返回
+ if (is_array($value)) {
+ return $value;
+ }
+
+ // 按行分割文本
+ return array_filter(explode("\n", $value));
+ }
+
+ /**
+ * 折扣获取器 - 根据原价和售价计算折扣
+ * @param $value
+ * @param $data
+ * @return string
+ */
+ public function getDiscountAttr($value, $data)
+ {
+ if (empty($data['originalPrice']) || $data['originalPrice'] <= 0) {
+ return '原价';
+ }
+
+ $discount = round(($data['price'] / $data['originalPrice']) * 10, 1);
+ return $discount . '折';
+ }
+
+ /**
+ * 总流量获取器 - 计算套餐总流量
+ * @param $value
+ * @param $data
+ * @return int
+ */
+ public function getTotalFlowAttr($value, $data)
+ {
+ return isset($data['monthlyFlow']) && isset($data['duration']) ?
+ intval($data['monthlyFlow']) * intval($data['duration']) : 0;
+ }
+}
\ No newline at end of file
diff --git a/application/store/model/FlowPackageOrderModel.php b/application/store/model/FlowPackageOrderModel.php
new file mode 100644
index 0000000..4bbe378
--- /dev/null
+++ b/application/store/model/FlowPackageOrderModel.php
@@ -0,0 +1,93 @@
+ 'integer',
+ 'userId' => 'integer',
+ 'packageId' => 'integer',
+ 'amount' => 'float',
+ 'duration' => 'integer',
+ 'createTime' => 'timestamp',
+ 'updateTime' => 'timestamp',
+ 'payTime' => 'timestamp',
+ 'status' => 'integer',
+ 'payStatus' => 'integer',
+ 'isDel' => 'integer',
+ ];
+
+ /**
+ * 生成订单号
+ * 规则:LL + 年月日时分秒 + 5位随机数
+ *
+ * @return string
+ */
+ public static function generateOrderNo()
+ {
+ $prefix = 'LL';
+ $date = date('YmdHis');
+ $random = mt_rand(10000, 99999);
+
+ return $prefix . $date . $random;
+ }
+
+ /**
+ * 创建订单
+ *
+ * @param int $userId 用户ID
+ * @param int $packageId 套餐ID
+ * @param string $packageName 套餐名称
+ * @param float $amount 订单金额
+ * @param int $duration 购买时长(月)
+ * @param string $payType 支付类型 (wechat|alipay|nopay)
+ * @param string $remark 备注
+ * @return array|false
+ */
+ public static function createOrder($userId, $packageId, $packageName, $amount, $duration, $payType = 'wechat', $remark = '')
+ {
+ // 生成订单号
+ $orderNo = self::generateOrderNo();
+
+ // 订单数据
+ $data = [
+ 'userId' => $userId,
+ 'packageId' => $packageId,
+ 'packageName' => $packageName,
+ 'orderNo' => $orderNo,
+ 'amount' => $amount,
+ 'duration' => $duration,
+ 'payType' => $payType,
+ 'createTime' => time(),
+ 'status' => 0, // 0:待支付 1:已完成 2:已取消 3:已退款
+ 'payStatus' => $payType == 'nopay' ? 10 : 0, // 0:未支付 1:已支付 10:无需支付
+ 'remark' => $remark,
+ 'isDel' => 0,
+ ];
+
+ // 创建订单
+ $model = new self();
+ $result = $model->save($data);
+
+ if ($result) {
+ return $model->toArray();
+ } else {
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/store/model/TrafficOrderModel.php b/application/store/model/TrafficOrderModel.php
new file mode 100644
index 0000000..4d51d66
--- /dev/null
+++ b/application/store/model/TrafficOrderModel.php
@@ -0,0 +1,11 @@
+ 'timestamp',
+ 'updateTime' => 'timestamp',
+ 'expireTime' => 'timestamp',
+ ];
+}
\ No newline at end of file
diff --git a/application/store/model/TrafficUsageLog.php b/application/store/model/TrafficUsageLog.php
new file mode 100644
index 0000000..8f5b46b
--- /dev/null
+++ b/application/store/model/TrafficUsageLog.php
@@ -0,0 +1,14 @@
+ 'timestamp',
+ 'updateTime' => 'timestamp',
+ ];
+}
\ No newline at end of file
diff --git a/application/store/model/UserFlowPackageModel.php b/application/store/model/UserFlowPackageModel.php
new file mode 100644
index 0000000..5569bf0
--- /dev/null
+++ b/application/store/model/UserFlowPackageModel.php
@@ -0,0 +1,103 @@
+where('status', 1) // 1表示有效
+ ->where('expireTime', '>', time()) // 未过期
+ ->order('expireTime', 'asc') // 按到期时间排序,最先到期的排在前面
+ ->find();
+ }
+
+ /**
+ * 创建用户套餐订阅记录
+ *
+ * @param int $userId 用户ID
+ * @param int $packageId 套餐ID
+ * @param int $duration 套餐时长(月)
+ * @return bool 是否创建成功
+ */
+ public static function createSubscription($userId, $packageId, $duration = 0)
+ {
+ if (empty($userId) || empty($packageId)) {
+ return false;
+ }
+
+ // 获取套餐信息
+ $package = FlowPackageModel::where('id', $packageId)->where('isDel', 0)->find();
+ if (empty($package)) {
+ return false;
+ }
+
+ // 如果未指定时长,则使用套餐默认时长
+ if (empty($duration)) {
+ $duration = $package['duration'];
+ }
+
+ // 计算开始时间和到期时间
+ $now = time();
+ $startTime = $now;
+ $expireTime = strtotime("+{$duration} month", $now);
+
+ // 创建新订阅
+ $data = [
+ 'userId' => $userId,
+ 'packageId' => $packageId,
+ 'duration' => $duration,
+ 'totalFlow' => $package->totalFlow,
+ 'usedFlow' => 0,
+ 'status' => 1, // 1表示有效
+ 'startTime' => $startTime,
+ 'expireTime' => $expireTime,
+ 'createTime' => $now,
+ 'updateTime' => $now
+ ];
+
+ return self::create($data) ? true : false;
+ }
+
+ /**
+ * 更新用户已使用流量
+ *
+ * @param int $id 用户套餐ID
+ * @param int $usedFlow 已使用流量
+ * @return bool 是否更新成功
+ */
+ public static function updateUsedFlow($id, $usedFlow)
+ {
+ if (empty($id)) {
+ return false;
+ }
+
+ $userPackage = self::where('id', $id)->find();
+ if (empty($userPackage)) {
+ return false;
+ }
+
+ // 确保使用量不超过总量
+ $maxFlow = $userPackage['totalFlow'];
+ $usedFlow = $usedFlow > $maxFlow ? $maxFlow : $usedFlow;
+
+ return self::where('id', $id)->update([
+ 'usedFlow' => $usedFlow,
+ 'updateTime' => time()
+ ]) ? true : false;
+ }
+}
\ No newline at end of file
diff --git a/application/store/model/VendorModel.php b/application/store/model/VendorModel.php
new file mode 100644
index 0000000..e28896a
--- /dev/null
+++ b/application/store/model/VendorModel.php
@@ -0,0 +1,34 @@
+hasMany('VendorPackageModel', 'vendorId', 'id')
+ ->where('isDel', 0);
+ }
+}
\ No newline at end of file
diff --git a/application/store/model/VendorOrderModel.php b/application/store/model/VendorOrderModel.php
new file mode 100644
index 0000000..ab1fc89
--- /dev/null
+++ b/application/store/model/VendorOrderModel.php
@@ -0,0 +1,45 @@
+belongsTo('VendorPackageModel', 'packageId', 'id');
+ }
+
+ /**
+ * 生成唯一订单号
+ * @return string
+ */
+ public static function generateOrderNo()
+ {
+ return date('YmdHis') . rand(1000, 9999);
+ }
+}
\ No newline at end of file
diff --git a/application/store/model/VendorPackageModel.php b/application/store/model/VendorPackageModel.php
new file mode 100644
index 0000000..bccb054
--- /dev/null
+++ b/application/store/model/VendorPackageModel.php
@@ -0,0 +1,50 @@
+hasMany('VendorProjectModel', 'packageId', 'id')
+ ->where('isDel', 0);
+ }
+
+ /**
+ * 标签获取器
+ */
+ public function getTagsAttr($value)
+ {
+ return $value ? explode(',', $value) : [];
+ }
+
+ /**
+ * 标签修改器
+ */
+ public function setTagsAttr($value)
+ {
+ return is_array($value) ? implode(',', $value) : $value;
+ }
+}
\ No newline at end of file
diff --git a/application/store/model/VendorProjectModel.php b/application/store/model/VendorProjectModel.php
new file mode 100644
index 0000000..a9add85
--- /dev/null
+++ b/application/store/model/VendorProjectModel.php
@@ -0,0 +1,33 @@
+belongsTo('VendorPackageModel', 'packageId', 'id');
+ }
+}
\ No newline at end of file
diff --git a/application/store/model/WechatFriendModel.php b/application/store/model/WechatFriendModel.php
new file mode 100644
index 0000000..0dcb758
--- /dev/null
+++ b/application/store/model/WechatFriendModel.php
@@ -0,0 +1,12 @@
+
+// +----------------------------------------------------------------------
+
+// +----------------------------------------------------------------------
+// | Cookie设置
+// +----------------------------------------------------------------------
+return [
+ // cookie 名称前缀
+ 'prefix' => '',
+ // cookie 保存时间
+ 'expire' => 0,
+ // cookie 保存路径
+ 'path' => '/',
+ // cookie 有效域名
+ 'domain' => '',
+ // cookie 启用安全传输
+ 'secure' => false,
+ // httponly设置
+ 'httponly' => '',
+ // 是否使用 setcookie
+ 'setcookie' => true,
+ // 跨站需要
+ 'SameSite' => 'None',
+];
diff --git a/application/superadmin/config/route.php b/application/superadmin/config/route.php
new file mode 100644
index 0000000..337cdfc
--- /dev/null
+++ b/application/superadmin/config/route.php
@@ -0,0 +1,52 @@
+middleware(['app\superadmin\middleware\AdminAuth']);
\ No newline at end of file
diff --git a/application/superadmin/controller/BaseController.php b/application/superadmin/controller/BaseController.php
new file mode 100644
index 0000000..bbd609e
--- /dev/null
+++ b/application/superadmin/controller/BaseController.php
@@ -0,0 +1,46 @@
+request->adminInfo;
+
+ if (!$admin) {
+ throw new \Exception('未授权访问,缺少有效的身份凭证', 401);
+ }
+
+ return $column ? $admin[$column] : $admin;
+ }
+}
\ No newline at end of file
diff --git a/application/superadmin/controller/Menu/GetMenuTreeController.php b/application/superadmin/controller/Menu/GetMenuTreeController.php
new file mode 100644
index 0000000..1a62183
--- /dev/null
+++ b/application/superadmin/controller/Menu/GetMenuTreeController.php
@@ -0,0 +1,179 @@
+buildMenuTree($menus, $menu['id']);
+
+ if (!empty($children)) {
+ $menu['children'] = $children;
+ }
+
+ $tree[] = $menu;
+ }
+ }
+
+ return $tree;
+ }
+
+ /**
+ * 获取管理员权限
+ *
+ * @return array
+ */
+ protected function getPermissions(): array
+ {
+ $record = AdministratorPermissionsModel::where('adminId', $this->getAdminInfo('id'))->find();
+
+ if (!$record || empty($record->permissions)) {
+ return [];
+ }
+
+ $permissions = $record->permissions ? json_decode($record->permissions, true) : [];
+
+ if (isset($permissions['ids']) && !empty($permissions['ids'])) {
+ return is_string($permissions['ids']) ? explode(',', $permissions['ids']) : $permissions['ids'];
+ }
+
+ return [];
+ }
+
+ /**
+ * 获取所有菜单,并组织成树状结构
+ *
+ * @return array
+ */
+ protected function getMenuTree(): array
+ {
+ // 获取所有菜单
+ $allMenus = MenuModel::where('status', MenuModel::STATUS_ACTIVE)->order('sort', 'asc')->select()->toArray();
+
+ // 组织成树状结构
+ return $allMenus ? $this->buildMenuTree($allMenus) : [];
+ }
+
+ /**
+ * 获取所有一级菜单(用户拥有权限的)
+ *
+ * @param array $permissionIds
+ * @return array
+ */
+ protected function getTopMenusInPermissionIds(array $permissionIds): array
+ {
+ $where = [
+ 'parentId' => MenuModel::TOP_LEVEL,
+ 'status' => MenuModel::STATUS_ACTIVE,
+ ];
+
+ return MenuModel::where($where)->whereIn('id', $permissionIds)->order('sort', 'asc')->select()->toArray();
+ }
+
+ /**
+ * 获取所有子菜单.
+ *
+ * @param array $topMenuIds
+ * @return array
+ */
+ protected function getAllChildrenInPermissionIds(array $topMenuIds): array
+ {
+ return MenuModel::where('status', MenuModel::STATUS_ACTIVE)->whereIn('parentId', $topMenuIds)->order('sort', 'asc')->select()->toArray();
+ }
+
+ /**
+ * 获取用户菜单
+ *
+ * @param array $permissionIds
+ * @return array
+ */
+ protected function getUserMenus(array $permissionIds): array
+ {
+ $topMenus = $this->getTopMenusInPermissionIds($permissionIds);
+
+ // 菜单ID集合,用于获取子菜单
+ $childMenus = $this->getAllChildrenInPermissionIds(
+ array_column($topMenus, 'id')
+ );
+
+ return $this->_makeMenuTree($topMenus, $childMenus);
+ }
+
+ /**
+ * 构建菜单树.
+ *
+ * @param array $topMenus
+ * @param array $childMenus
+ * @return array
+ */
+ protected function _makeMenuTree(array $topMenus, array $childMenus): array
+ {
+ // 将子菜单按照父ID进行分组
+ $childMenusGroup = [];
+
+ foreach ($childMenus as $menu) {
+ $childMenusGroup[$menu['parentId']][] = $menu;
+ }
+
+ foreach ($topMenus as $topMenu) {
+ if (isset($childMenusGroup[$topMenu['id']])) {
+ $topMenu['children'] = $childMenusGroup[$topMenu['id']];
+ }
+
+ $menuTree[] = $topMenu;
+ }
+
+ return $menuTree ?? [];
+ }
+
+ /**
+ * 根据权限ID获取相应的菜单树
+ *
+ * @param array $permissionIds 权限ID数组
+ * @return array
+ */
+ protected function getMenuTreeByPermissions(array $permissionIds): array
+ {
+ // 如果没有权限,返回空数组
+ return $permissionIds ? $this->getUserMenus($permissionIds) : [];
+ }
+
+ /**
+ * 获取菜单列表(树状结构)
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ if ($this->getAdminInfo('id') == AdministratorModel::MASTER_ID) {
+ $menuTree = $this->getMenuTree();
+ } else {
+ $menuTree = $this->getMenuTreeByPermissions(
+ $this->getPermissions()
+ );
+ }
+
+ return ResponseHelper::success($menuTree);
+ }
+}
\ No newline at end of file
diff --git a/application/superadmin/controller/Menu/GetTopLevelForPermissionController.php b/application/superadmin/controller/Menu/GetTopLevelForPermissionController.php
new file mode 100644
index 0000000..20c0632
--- /dev/null
+++ b/application/superadmin/controller/Menu/GetTopLevelForPermissionController.php
@@ -0,0 +1,40 @@
+ MenuModel::TOP_LEVEL,
+ 'status' => MenuModel::STATUS_NORMAL
+ ];
+
+ return MenuModel::where($where)->field('id, title')->order('sort', 'asc')->select()->toArray();
+ }
+
+ /**
+ * 获取一级菜单(供权限设置使用)
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ $menus = $this->getTopLevelMenus();
+
+ return ResponseHelper::success($menus);
+ }
+}
\ No newline at end of file
diff --git a/application/superadmin/controller/administrator/AddAdministratorController.php b/application/superadmin/controller/administrator/AddAdministratorController.php
new file mode 100644
index 0000000..22a6a75
--- /dev/null
+++ b/application/superadmin/controller/administrator/AddAdministratorController.php
@@ -0,0 +1,150 @@
+count() > 0;
+
+ if ($exists) {
+ throw new \Exception('账号已存在', 400);
+ }
+ }
+
+ /**
+ * 数据验证
+ *
+ * @param array $params
+ * @return $this
+ * @throws \Exception
+ */
+ protected function dataValidate(array $params): self
+ {
+ $validate = Validate::make([
+ 'account' => 'require|regex:^[a-zA-Z0-9]+$|/\S+/',
+ 'username' => 'require|/\S+/',
+ 'password' => 'require|/\S+/',
+ 'permissionIds' => 'require|array',
+ ], [
+ 'account.require' => '账号不能为空',
+ 'account.regex' => '账号只能用数字或者字母或者数字字母组合',
+ 'username.require' => '用户名不能为空',
+ 'password.require' => '密码不能为空',
+ 'permissionIds.require' => '请至少分配一种权限',
+ ]);
+
+ if (!$validate->check($params)) {
+ throw new \Exception($validate->getError(), 400);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 判断是否有权限修改
+ *
+ * @return $this
+ */
+ protected function checkPermission(): self
+ {
+ if ($this->getAdminInfo('id') != AdministratorModel::MASTER_ID) {
+ throw new \Exception('您没有权限添加管理员', 403);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 保存管理员权限
+ *
+ * @param int $adminId 管理员ID
+ * @param array $permissionIds 权限ID数组
+ * @return bool
+ */
+ protected function savePermissions(int $adminId, array $permissionIds)
+ {
+ $record = AdministratorPermissionsModel::where('adminId', $adminId)->find();
+
+ $permissionData = [
+ 'ids' => is_array($permissionIds) ? implode(',', $permissionIds) : $permissionIds
+ ];
+
+ if ($record) {
+ return $record->save([
+ 'permissions' => json_encode($permissionData),
+ ]);
+ } else {
+ return AdministratorPermissionsModel::create([
+ 'adminId' => $adminId,
+ 'permissions' => json_encode($permissionData),
+ ]);
+ }
+ }
+
+ /**
+ * 添加管理员信息
+ *
+ * @param array $params
+ * @return AdministratorModel
+ * @throws \Exception
+ */
+ protected function addAdministrator(array $params): AdministratorModel
+ {
+ $result = AdministratorModel::create(array_merge($params, ['password' => md5($params['password'])]));
+
+ if (!$result) {
+ throw new \Exception('添加管理员失败', 401);
+ }
+
+ return $result;
+ }
+
+ /**
+ * 添加管理员
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ $params = $this->request->only(['account', 'username', 'password', 'permissionIds']);
+
+ $this->dataValidate($params);
+ $this->checkPermission()->chekAdminIsExist($params['account']);
+
+ Db::startTrans();
+ $admin = $this->addAdministrator($params);
+
+ // 保存权限
+ if (!empty($params['permissionIds'])) {
+ $this->savePermissions($admin->id, $params['permissionIds']);
+ }
+
+ Db::commit();
+ return ResponseHelper::success();
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/superadmin/controller/administrator/DeleteAdministratorController.php b/application/superadmin/controller/administrator/DeleteAdministratorController.php
new file mode 100644
index 0000000..0f7dea2
--- /dev/null
+++ b/application/superadmin/controller/administrator/DeleteAdministratorController.php
@@ -0,0 +1,128 @@
+find();
+
+ if (!$admin) {
+ throw new \Exception('管理员不存在', 404);
+ }
+
+ if (!$admin->delete()) {
+ throw new \Exception('管理员删除失败', 400);
+ }
+ }
+
+ /**
+ * 删除管理员权限
+ *
+ * @param int $adminId
+ * @return void
+ * @throws \Exception
+ */
+ protected function deletePermission(int $adminId): void
+ {
+ $permission = AdministratorPermissionsModel::where('adminId', $adminId)->find();
+
+ if (!$permission->delete()) {
+ throw new \Exception('管理员权限移除失败', 400);
+ }
+ }
+
+ /**
+ * 删除账号的限制条件
+ *
+ * @param int $adminId
+ * @return void
+ * @throws \Exception
+ */
+ protected function canNotDeleteSelf(int $adminId)
+ {
+ // 不能删除自己的账号
+ if ($this->getAdminInfo('id') == $adminId) {
+ throw new \Exception('不能删除自己的账号', 403);
+ }
+
+ // 只有超级管理员(ID为1)可以删除管理员
+ if ($this->getAdminInfo('id') != AdministratorModel::MASTER_ID) {
+ throw new \Exception('您没有权限删除管理员', 403);
+ }
+
+ // 不能删除超级管理员账号
+ if ($adminId == AdministratorModel::MASTER_ID) {
+ throw new \Exception('不能删除超级管理员账号', 403);
+ }
+ }
+
+ /**
+ * 数据验证
+ *
+ * @param array $params
+ * @return $this
+ * @throws \Exception
+ */
+ protected function dataValidate(array $params): self
+ {
+ $validate = Validate::make([
+ 'id' => 'require|regex:/^[1-9]\d*$/',
+ ], [
+ 'id.regex' => '非法请求',
+ 'id.require' => '非法请求',
+ ]);
+
+ if (!$validate->check($params)) {
+ throw new \Exception($validate->getError(), 400);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 删除管理员
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ $params = $this->request->only('id');
+ $adminId = $params['id'];
+
+ $this->dataValidate($params)->canNotDeleteSelf($adminId);
+
+ Db::startTrans();
+
+ $this->deleteAdmin($adminId);
+ $this->deletePermission($adminId);
+
+ Db::commit();
+
+ return ResponseHelper::success();
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/superadmin/controller/administrator/GetAdministratorDetailController.php b/application/superadmin/controller/administrator/GetAdministratorDetailController.php
new file mode 100644
index 0000000..25200dd
--- /dev/null
+++ b/application/superadmin/controller/administrator/GetAdministratorDetailController.php
@@ -0,0 +1,108 @@
+field([
+ 'a.id', 'a.account', 'a.username', 'a.status', 'a.authId', 'a.createTime createdAt', 'a.lastLoginTime lastLogin',
+ 'p.permissions'
+ ])
+ ->leftJoin('administrator_permissions p', 'a.id = p.adminId')
+ ->where('a.id', $adminId)
+ ->find();
+
+ if (!$admin) {
+ throw new \Exception('管理员不存在', 404);
+ }
+
+ return $admin;
+ }
+
+ /**
+ * 解析权限数据
+ *
+ * @param string|null $permission
+ * @return array
+ */
+ protected function parsePermissions(?string $permission): array
+ {
+ $permissionIds = [];
+
+ if (!empty($permission)) {
+ $permissions = json_decode($permission, true);
+ $permissions = is_array($permissions) ? $permissions : json_decode($permissions, true);
+
+ if (isset($permissions['ids'])) {
+ $permissionIds = is_string($permissions['ids']) ? explode(',', $permissions['ids']) : $permissions['ids'];
+ $permissionIds = array_map('intval', $permissionIds);
+ }
+ }
+
+ return $permissionIds;
+ }
+
+ /**
+ * 根据权限ID获取角色名称
+ *
+ * @param int $authId
+ * @return string
+ */
+ protected function getRoleName($authId): string
+ {
+ switch ($authId) {
+ case 1:
+ return '超级管理员';
+ case 2:
+ return '项目管理员';
+ case 3:
+ return '客户管理员';
+ default:
+ return '普通管理员';
+ }
+ }
+
+ /**
+ * 获取详细信息
+ *
+ * @param int $id 管理员ID
+ * @return \think\response\Json
+ */
+ public function index($id)
+ {
+ try {
+ $admin = $this->getAdministrator($id);
+ $roleName = $this->getRoleName($admin->authId);
+ $permissionIds = $this->parsePermissions($admin->permissions);
+
+ return ResponseHelper::success(
+ array_merge($admin->toArray(), [
+ 'roleName' => $roleName,
+ 'permissions' => $permissionIds,
+ 'lastLogin' => $admin->lastLogin ? date('Y-m-d H:i', $admin->lastLogin) : '从未登录',
+ 'createdAt' => date('Y-m-d H:i', $admin->createdAt),
+ ])
+ );
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/superadmin/controller/administrator/GetAdministratorListController.php b/application/superadmin/controller/administrator/GetAdministratorListController.php
new file mode 100644
index 0000000..6bf4e7a
--- /dev/null
+++ b/application/superadmin/controller/administrator/GetAdministratorListController.php
@@ -0,0 +1,175 @@
+request->param('keyword/s', ''))) {
+ $where[] = ['account|username', 'like', "%{$keyword}%"];
+ }
+
+ return array_merge($params, $where);
+ }
+
+ /**
+ * 获取管理员列表
+ *
+ * @param array $where 查询条件
+ * @return \think\Paginator 分页对象
+ */
+ protected function getAdministratorList(array $where): \think\Paginator
+ {
+ $query = AdministratorModel::alias('a')
+ ->field([
+ 'a.id', 'a.account', 'a.username', 'a.status', 'a.authId', 'a.createTime createdAt', 'a.lastLoginTime', 'a.lastLoginIp'
+ ]);
+
+ foreach ($where as $key => $value) {
+ if (is_numeric($key) && is_array($value) && isset($value[0]) && $value[0] === 'exp') {
+ $query->whereExp('', $value[1]);
+ continue;
+ }
+
+ $query->where($key, $value);
+ }
+
+ return $query->paginate($this->request->param('limit/d', 10), false, ['page' => $this->request->param('page/d', 1)]);
+ }
+
+ /**
+ * 根据权限ID获取角色名称
+ *
+ * @param int $authId 权限ID
+ * @return string
+ */
+ protected function getRoleName($authId): string
+ {
+ switch ($authId) {
+ case 1:
+ return '超级管理员';
+ case 2:
+ return '项目管理员';
+ case 3:
+ return '客户管理员';
+ default:
+ return '普通管理员';
+ }
+ }
+
+ /**
+ * 获取管理员权限
+ *
+ * @param int $adminId
+ * @return array
+ */
+ protected function _getPermissions(int $adminId): array
+ {
+ $record = AdministratorPermissionsModel::where('adminId', $adminId)->find();
+
+ if (!$record || empty($record->permissions)) {
+ return [];
+ }
+
+ $permissions = $record->permissions ? json_decode($record->permissions, true) : [];
+
+ if (isset($permissions['ids']) && !empty($permissions['ids'])) {
+ return is_string($permissions['ids']) ? explode(',', $permissions['ids']) : $permissions['ids'];
+ }
+
+ return [];
+ }
+
+ /**
+ * 通过菜单的id获取菜单的名字
+ *
+ * @param array $ids
+ * @return array
+ */
+ protected function getMenusNameByIds(array $ids): array
+ {
+ return MenuModel::whereIn('id', $ids)->column('title');
+ }
+
+ /**
+ * 根据权限ID获取权限列表
+ *
+ * @param int $authId 权限ID
+ * @return array
+ */
+ protected function getPermissions(int $authId): array
+ {
+ $ids = $this->_getPermissions($authId);
+
+ if ($ids) {
+ return $this->getMenusNameByIds($ids);
+ }
+
+ return [];
+ }
+
+ /**
+ * 构建返回数据
+ *
+ * @param \think\Paginator $list
+ * @return array
+ */
+ protected function makeReturnedResult(\think\Paginator $list): array
+ {
+ $result = [];
+
+ foreach ($list->items() as $item) {
+ $section = [
+ 'id' => $item->id,
+ 'account' => $item->account,
+ 'username' => $item->username,
+ 'status' => $item->status,
+ 'createdAt' => date('Y-m-d H:i:s', $item->createdAt),
+ 'lastLogin' => !empty($item->lastLoginTime) ? date('Y-m-d H:i:s', $item->lastLoginTime) : '从未登录',
+ 'role' => $this->getRoleName($item->authId),
+ 'permissions' => $this->getPermissions($item->id),
+ ];
+
+ array_push($result, $section);
+ }
+
+ return $result;
+ }
+
+ /**
+ * 获取管理员列表
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ $where = $this->makeWhere();
+ $result = $this->getAdministratorList($where);
+
+ return ResponseHelper::success(
+ [
+ 'list' => $this->makeReturnedResult($result),
+ 'total' => $result->total(),
+ ]
+ );
+ }
+}
\ No newline at end of file
diff --git a/application/superadmin/controller/administrator/UpdateAdministratorController.php b/application/superadmin/controller/administrator/UpdateAdministratorController.php
new file mode 100644
index 0000000..9e886c0
--- /dev/null
+++ b/application/superadmin/controller/administrator/UpdateAdministratorController.php
@@ -0,0 +1,154 @@
+save($params)) {
+ throw new \Exception('记录更新失败', 402);
+ }
+ }
+
+ /**
+ * 数据验证
+ *
+ * @param array $params
+ * @return $this
+ * @throws \Exception
+ */
+ protected function dataValidate(array $params): self
+ {
+ $validate = Validate::make([
+ 'id' => 'require|regex:/^[1-9]\d*$/',
+ 'account' => 'require|regex:^[a-zA-Z0-9]+$|/\S+/',
+ 'username' => 'require|/\S+/',
+ 'password' => '/\S+/',
+ 'permissionIds' => 'array',
+ ], [
+ 'id.require' => '缺少必要参数',
+ 'account.require' => '账号不能为空',
+ 'account.regex' => '账号只能用数字或者字母或者数字字母组合',
+ 'username.require' => '用户名不能为空',
+ 'permissionIds.array' => '请至少分配一种权限',
+ ]);
+
+ if (!$validate->check($params)) {
+ throw new \Exception($validate->getError(), 400);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 判断是否有权限修改
+ *
+ * @param int $adminId
+ * @param array $params
+ * @return $this
+ */
+ protected function checkPermission(int $adminId, array $params): self
+ {
+ $currentAdminId = $this->getAdminInfo('id');
+
+ if ($currentAdminId != AdministratorModel::MASTER_ID && $currentAdminId != $adminId) {
+ throw new \Exception('您没有权限修改其他管理员', 403);
+ }
+
+ if ($params['id'] != AdministratorModel::MASTER_ID && empty($params['permissionIds'])) {
+ throw new \Exception('请至少分配一种权限', 403);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 保存管理员权限
+ *
+ * @param int $adminId
+ * @param array $permissionIds
+ * @return bool
+ */
+ protected function savePermissions(int $adminId, array $permissionIds)
+ {
+ $record = AdministratorPermissionsModel::where('adminId', $adminId)->find();
+
+ $permissionData = [
+ 'ids' => is_array($permissionIds) ? implode(',', $permissionIds) : $permissionIds
+ ];
+
+ if ($record) {
+ return $record->save([
+ 'permissions' => json_encode($permissionData),
+ ]);
+ } else {
+ return AdministratorPermissionsModel::create([
+ 'adminId' => $adminId,
+ 'permissions' => json_encode($permissionData),
+ ]);
+ }
+ }
+
+ /**
+ * 更新管理员信息
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ $params = $this->request->only(['id', 'account', 'username', 'password', 'permissionIds']);
+
+ // 被修改的管理员id
+ $adminId = $params['id'] ?? 0;
+
+ $this->dataValidate($params)->checkPermission($adminId, $params);
+
+ Db::startTrans();
+
+ $this->udpateAdministrator($params);
+
+ // 如果当前是超级管理员(ID为1),并且修改的不是自己,则更新权限
+ if ($this->getAdminInfo('id') == AdministratorModel::MASTER_ID
+ && $this->getAdminInfo('id') != $adminId
+ && !empty($params['permissionIds'])
+ ) {
+ $this->savePermissions($adminId, $params['permissionIds']);
+ }
+
+ Db::commit();
+ return ResponseHelper::success();
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/superadmin/controller/auth/AuthLoginController.php b/application/superadmin/controller/auth/AuthLoginController.php
new file mode 100644
index 0000000..0c52e29
--- /dev/null
+++ b/application/superadmin/controller/auth/AuthLoginController.php
@@ -0,0 +1,166 @@
+id . '|' . $admin->account . 'cunkebao_admin_secret');
+ }
+
+ /**
+ * 数据验证
+ *
+ * @param array $params
+ * @return $this
+ * @throws \Exception
+ */
+ protected function dataValidate(array $params): self
+ {
+ $validate = Validate::make([
+ 'account' => 'require|/\S+/',
+ 'password' => 'require|/\S+/',
+ ]);
+
+ if (!$validate->check($params)) {
+ throw new \Exception($validate->getError(), 400);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 获取管理员信息
+ *
+ * @param array $params
+ * @return object|AdministratorModel
+ * @throws \Exception
+ */
+ protected function getAdministrator(array $params): AdministratorModel
+ {
+ extract($params);
+
+ $admin = AdministratorModel::where(['account' => $account])->find();
+
+ if (!$admin ||
+ $admin->password !== $password ||
+ $admin->deleteTime
+ ) {
+ throw new \Exception('账号不存在或密码错误', 404);
+ }
+
+ if (!$admin->status) {
+ throw new \Exception('账号已禁用', 404);
+ }
+
+ return $admin;
+ }
+
+ /**
+ * 更新登录信息
+ *
+ * @param AdministratorModel $admin
+ * @return $this
+ */
+ protected function saveLoginInfo(AdministratorModel $admin): self
+ {
+ $admin->lastLoginTime = time();
+ $admin->lastLoginIp = $this->request->ip();
+
+ if (!$admin->save()) {
+ throw new \Exception('拒绝登录', 403);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 设置登录Cookie,有效期24小时
+ *
+ * @param AdministratorModel $admin
+ * @return void
+ */
+ protected function setCookie(AdministratorModel $admin): void
+ {
+ // 获取当前环境
+ $env = app()->env->get('APP_ENV', 'production');
+
+ // 获取请求的域名
+ $origin = $this->request->header('origin');
+ $domain = '';
+
+ if ($origin) {
+ // 解析域名
+ $parsedUrl = parse_url($origin);
+ if (isset($parsedUrl['host'])) {
+ // 如果是测试环境,使用完整的域名
+ if ($env === 'testing') {
+ $domain = $parsedUrl['host'];
+ } else {
+ // 生产环境使用顶级域名
+ $parts = explode('.', $parsedUrl['host']);
+ if (count($parts) > 1) {
+ $domain = '.' . $parts[count($parts) - 2] . '.' . $parts[count($parts) - 1];
+ }
+ }
+ }
+ }
+
+ // 设置cookie选项
+ $options = [
+ 'expire' => 86400,
+ 'path' => '/',
+ 'httponly' => true,
+ 'samesite' => 'None', // 允许跨域
+ 'secure' => true // 仅 HTTPS 下有效
+ ];
+
+ // 如果有域名,添加到选项
+ if ($domain) {
+ $options['domain'] = $domain;
+ }
+
+ // 设置cookies
+ Cookie::set('admin_id', $admin->id, $options);
+ Cookie::set('admin_token', $this->createToken($admin), $options);
+ }
+
+ /**
+ * 管理员登录
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ $params = $this->request->only(['account', 'password']);
+
+ $admin = $this->dataValidate($params)->getAdministrator($params);
+ $this->saveLoginInfo($admin)->setCookie($admin);
+
+ return ResponseHelper::success(
+ [
+ 'id' => $admin->id,
+ 'name' => $admin->username,
+ 'account' => $admin->account,
+ 'token' => Cookie::get('admin_token')
+ ]
+ );
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/superadmin/controller/company/CreateCompanyController.php b/application/superadmin/controller/company/CreateCompanyController.php
new file mode 100644
index 0000000..fcbbc3b
--- /dev/null
+++ b/application/superadmin/controller/company/CreateCompanyController.php
@@ -0,0 +1,288 @@
+setBaseUrl(Env::get('rpc.API_BASE_URL'))
+ ->setMethod('post')
+ ->send('/v1/api/account/create', $params);
+
+ $result = json_decode($response, true);
+
+ if ($result['code'] != 200) {
+ throw new Exception($result['msg'], 210 . $result['code']);
+ }
+
+ return $result['data'] ?: null;
+ }
+
+ /**
+ * S2 创建部门并返回id
+ *
+ * @param array $params
+ * @return array
+ */
+ protected function s2CreateDepartmentAndUser(array $params): ?array
+ {
+ $params = ArrHelper::getValue('name=departmentName,memo=departmentMemo,account=accountName,password=accountPassword,username=accountRealName,username=accountNickname,accountMemo', $params);
+
+ // 创建公司部门
+ $response = CurlHandle::getInstant()
+ ->setBaseUrl(Env::get('rpc.API_BASE_URL'))
+ ->setMethod('post')
+ ->send('/v1/api/account/createNewAccount', $params);
+
+ $result = json_decode($response, true);
+
+ if ($result['code'] != 200) {
+ throw new Exception($result['msg'], 210 . $result['code']);
+ }
+
+ return $result['data'] ?: null;
+ }
+
+ /**
+ * 数据验证
+ *
+ * @param array $params
+ * @return $this
+ * @throws Exception
+ */
+ protected function dataValidate(array $params): self
+ {
+ $validate = Validate::make([
+ 'name' => 'require|max:50|/\S+/',
+ 'account' => 'require|regex:^[a-zA-Z0-9]+$|max:20|/\S+/',
+ 'username' => 'require|max:20|/\S+/',
+ 'phone' => 'require|regex:/^1[3-9]\d{9}$/',
+ 'status' => 'require|in:0,1',
+ 'password' => 'require|/\S+/',
+ 'memo' => '/\S+/',
+ ], [
+ 'name.require' => '请输入项目名称',
+ 'account.require' => '请输入账号',
+ 'account.max' => '账号长度受限',
+ 'account.regex' => '账号只能用数字或者字母或者数字字母组合',
+ 'username.require' => '请输入用户昵称',
+ 'phone.require' => '请输入手机号',
+ 'phone.regex' => '手机号格式错误',
+ 'status.require' => '缺少重要参数',
+ 'status.in' => '非法参数',
+ 'password.require' => '请输入密码',
+ ]);
+
+ if (!$validate->check($params)) {
+ throw new Exception($validate->getError(), 400);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 设备创建分组
+ *
+ * @param array $params
+ * @return void
+ * @throws Exception
+ */
+ protected function s2CreateDeviceGroup(array $params): void
+ {
+ $respon = (new DeviceController())->createGroup($params, true);
+ $respon = json_decode($respon, true);
+
+ if ($respon['code'] != 200) {
+ throw new Exception('设备分组添加错误', 210 . $respon['code']);
+ }
+ }
+
+ /**
+ * S2 部分
+ *
+ * @param array $params
+ * @return array
+ * @throws Exception
+ */
+ protected function creatS2About(array $params): array
+ {
+ $department = $this->s2CreateDepartmentAndUser($params);
+
+ if (!$department || !isset($department['id']) || !isset($department['departmentId'])) {
+ throw new Exception('S2返参异常', 210402);
+ }
+
+ // 设备创建分组
+ $this->s2CreateDeviceGroup(['groupName' => $params['name']]);
+
+ return array_merge($params, [
+ 'companyId' => $department['departmentId'],
+ 's2_accountId' => $department['id'],
+ ]);
+ }
+
+ /**
+ * 存客宝创建项目
+ *
+ * @param array $params
+ * @return void
+ * @throws Exception
+ */
+ protected function ckbCreateCompany(array $params): void
+ {
+ $params = ArrHelper::getValue('companyId=id,companyId,name,memo,status', $params);
+ $result = CompanyModel::create($params);
+
+ if (!$result) {
+ throw new Exception('创建公司记录失败', 402);
+ }
+ }
+
+ /**
+ * 创建功能账号,不可登录,也非管理员,用户也不可见.
+ *
+ * @param array $params
+ * @return void
+ * @throws Exception
+ */
+ protected function createFuncUsers(array $params): void
+ {
+ $seedCols = [
+ ['account' => $params['account'] . '_01', 'username' => $params['username'] . '_子账号01', 'status' => UsersModel::ADMIN_STP, 'isAdmin' => UsersModel::ADMIN_OTP, 'typeId' => UsersModel::MASTER_USER],
+ ['account' => $params['account'] . '_02', 'username' => $params['username'] . '_子账号02', 'status' => UsersModel::ADMIN_STP, 'isAdmin' => UsersModel::ADMIN_OTP, 'typeId' => UsersModel::MASTER_USER],
+ ['account' => $params['account'] . '_03', 'username' => $params['username'] . '_子账号03', 'status' => UsersModel::ADMIN_STP, 'isAdmin' => UsersModel::ADMIN_OTP, 'typeId' => UsersModel::MASTER_USER],
+ ['account' => $params['account'] . '_offline', 'username' => $params['username'] . '_处理离线专用', 'status' => UsersModel::STATUS_STOP, 'isAdmin' => UsersModel::ADMIN_OTP, 'typeId' => UsersModel::NOT_USER],
+ ['account' => $params['account'] . '_delete', 'username' => $params['username'] . '_处理删除专用', 'status' => UsersModel::STATUS_STOP, 'isAdmin' => UsersModel::ADMIN_OTP, 'typeId' => UsersModel::NOT_USER],
+ ];
+
+ foreach ($seedCols as $seeds) {
+ $this->s2CreateUser(array_merge($params, ArrHelper::getValue('account,username', $seeds)));
+ $this->ckbCreateUser(array_merge($params, $seeds));
+ }
+ }
+
+ /**
+ * 存客宝创建账号
+ *
+ * @param array $params
+ * @return void
+ * @throws Exception
+ */
+ protected function ckbCreateUser(array $params): void
+ {
+ $params = ArrHelper::getValue('username,account,password,companyId,s2_accountId,status,phone,isAdmin,typeId', $params);
+
+ $params = array_merge($params, [
+ 'passwordLocal' => localEncrypt($params['password']),
+ 'passwordMd5' => md5($params['password']),
+ ]);
+
+ if (!UsersModel::create($params)) {
+ throw new Exception('创建用户记录失败', 402);
+ }
+ }
+
+ /**
+ * @param array $params
+ * @return void
+ * @throws Exception
+ */
+ protected function createCkbAbout(array $params)
+ {
+ // 1. 存客宝创建项目
+ $this->ckbCreateCompany($params);
+
+ // 2. 存客宝创建操盘手总账号
+ $this->ckbCreateUser(array_merge($params, [
+ 'isAdmin' => UsersModel::ADMIN_STP, // 主要账号默认1
+ 'typeId' => UsersModel::MASTER_USER, // 类型:运营后台/操盘手传1、 门店传2
+ ]));
+ }
+
+ /**
+ * 检查项目名称是否已存在
+ *
+ * @param array $where
+ * @return void
+ * @throws Exception
+ */
+ protected function checkCompanyNameOrAccountOrPhoneExists(array $where): void
+ {
+ extract($where);
+
+ // 项目名称尽量不重名
+ $exists = CompanyModel::where(compact('name'))->count() > 0;
+ if ($exists) {
+ throw new Exception('项目名称已存在', 403);
+ }
+
+ // 账号不重名
+ $exists = UsersModel::where(compact('account'))->count() > 0;
+ if ($exists) {
+ throw new Exception('用户账号已存在', 403);
+ }
+
+ // 手机号不重名
+ $exists = UsersModel::where(compact('phone'))->count() > 0;
+ if ($exists) {
+ throw new Exception('手机号已存在', 403);
+ }
+ }
+
+
+
+ /**
+ * 创建新项目
+ *
+ * @return Json
+ */
+ public function index()
+ {
+ try {
+ $params = $this->request->only(['name', 'status', 'username', 'account', 'password', 'phone', 'memo']);
+ $params = $this->dataValidate($params)->creatS2About($params);
+
+ Db::startTrans();
+
+ $this->checkCompanyNameOrAccountOrPhoneExists(ArrHelper::getValue('name,account,phone', $params));
+ $this->createCkbAbout($params);
+
+ // 创建功能账号,不可登录,也非管理员,用户也不可见
+ $this->createFuncUsers($params);
+ Db::commit();
+
+ return ResponseHelper::success();
+ } catch (Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/superadmin/controller/company/DeleteCompanyController.php b/application/superadmin/controller/company/DeleteCompanyController.php
new file mode 100644
index 0000000..5334d4c
--- /dev/null
+++ b/application/superadmin/controller/company/DeleteCompanyController.php
@@ -0,0 +1,126 @@
+ 'require|regex:/^[1-9]\d*$/',
+ ], [
+ 'id.regex' => '非法请求',
+ 'id.require' => '非法请求',
+ ]);
+
+ if (!$validate->check($params)) {
+ throw new \Exception($validate->getError(), 400);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 删除项目
+ *
+ * @param int $id
+ * @throws \Exception
+ */
+ protected function deleteCompany(int $id): void
+ {
+ $company = CompanyModel::where('id', $id)->find();
+
+ if (!$company) {
+ throw new \Exception('项目不存在', 404);
+ }
+
+ if (!$company->delete()) {
+ throw new \Exception('项目删除失败', 400);
+ }
+ }
+
+ /**
+ * 删除用户
+ *
+ * @param int $companId
+ * @throws \Exception
+ */
+ protected function deleteUsers(int $companId): void
+ {
+ $users = UserModel::where('companyId', $companId)->select();
+
+ foreach ($users as $user) {
+ if (!$user->delete()) {
+ throw new \Exception($user->username . ' 用户删除失败', 400);
+ }
+ }
+ }
+
+ /**
+ * 删除存客宝数据
+ *
+ * @param int $companId
+ * @return self
+ * @throws \Exception
+ */
+ protected function delteCkbAbout(int $companId): self
+ {
+ // 1. 删除项目
+ $this->deleteCompany($companId);
+
+ // 2. 删除用户
+ $this->deleteUsers($companId);
+
+ return $this;
+ }
+
+ /**
+ * 删除 s2 数据
+ *
+ * @return void
+ */
+ protected function deleteS2About()
+ {
+
+ }
+
+ /**
+ * 删除项目
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ $params = $this->request->only('id');
+ $companId = $params['id'];
+
+ Db::startTrans();
+
+ $this->dataValidate($params)->delteCkbAbout($companId)->deleteS2About($companId);
+
+ Db::commit();
+ return ResponseHelper::success();
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/superadmin/controller/company/GetCompanyDetailForProfileController.php b/application/superadmin/controller/company/GetCompanyDetailForProfileController.php
new file mode 100644
index 0000000..787abd6
--- /dev/null
+++ b/application/superadmin/controller/company/GetCompanyDetailForProfileController.php
@@ -0,0 +1,114 @@
+column('wechatId');
+
+ return array_unique($wechatIds);
+ }
+
+ /**
+ * 统计微信好友数量
+ *
+ * @param int $companyId
+ * @return int
+ */
+ protected function getFriendCountByCompanyId(int $companyId): int
+ {
+ $wechatIds = $this->getDeveiceWechats($companyId);
+
+ return WechatFriendModel::whereIn('ownerWechatId', $wechatIds)->count();
+ }
+
+ /**
+ * 根据 CompanyId 获取设备数量
+ *
+ * @param int $companyId
+ * @return int
+ */
+ protected function getDeviceCountByCompanyId(int $companyId): int
+ {
+ return DeviceModel::where('companyId', $companyId)->count();
+ }
+
+ /**
+ * 根据 CompanyId 获取子账号数量
+ *
+ * @param int $companyId
+ * @return int
+ */
+ protected function getUsersCountByCompanyId(int $companyId): int
+ {
+ $where = array_merge(compact('companyId'), array('isAdmin' => UserModel::ADMIN_OTP));
+
+ return UserModel::where($where)->count();
+ }
+
+ /**
+ * 获取项目详情
+ *
+ * @param int $id
+ * @return CompanyModel
+ * @throws \Exception
+ */
+ protected function getCompanyDetail(int $id): array
+ {
+ $detail = CompanyModel::alias('c')
+ ->field([
+ 'c.id', 'c.name', 'c.memo', 'c.companyId', 'c.createTime',
+ 'u.account', 'u.phone'
+ ])
+ ->leftJoin('users u', 'c.companyId = u.companyId and u.isAdmin = ' . UserModel::ADMIN_STP)
+ ->find($id);
+
+ if (!$detail) {
+ throw new \Exception('项目不存在', 404);
+ }
+
+ return $detail->toArray();
+ }
+
+ /**
+ * 获取项目详情
+ *
+ * @param int $id
+ * @return \think\response\Json
+ */
+ public function index($id)
+ {
+ try {
+ $data = $this->getCompanyDetail($id);
+
+ $userCount = $this->getUsersCountByCompanyId($id);
+ $deviceCount = $this->getDeviceCountByCompanyId($id);
+ $friendCount = $this->getFriendCountByCompanyId($id);
+
+ return ResponseHelper::success(
+ array_merge($data, compact('deviceCount', 'friendCount', 'userCount'))
+ );
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/superadmin/controller/company/GetCompanyDetailForUpdateController.php b/application/superadmin/controller/company/GetCompanyDetailForUpdateController.php
new file mode 100644
index 0000000..ddf63ae
--- /dev/null
+++ b/application/superadmin/controller/company/GetCompanyDetailForUpdateController.php
@@ -0,0 +1,76 @@
+field([
+ 'd.id', 'd.memo', 'd.model', 'd.brand', 'd.phone', 'd.imei', 'd.createTime', 'd.alive',
+ ])
+ ->where('companyId', $companyId)
+ ->select()
+ ->toArray() ?: [];
+ }
+
+ /**
+ * 获取项目详情
+ *
+ * @param int $id
+ * @return CompanyModel
+ * @throws \Exception
+ */
+ protected function getCompanyDetail(int $id): array
+ {
+ $detail = CompanyModel::alias('c')
+ ->field([
+ 'c.id', 'c.name', 'c.status', 'c.memo', 'c.companyId',
+ 'u.account', 'u.username', 'u.phone', 'u.s2_accountId'
+ ])
+ ->leftJoin('users u', 'c.companyId = u.companyId and u.isAdmin = ' . UserModel::ADMIN_STP)
+ ->find($id);
+
+ if (!$detail) {
+ throw new \Exception('项目不存在', 404);
+ }
+
+ return $detail->toArray();
+ }
+
+ /**
+ * 获取项目详情
+ *
+ * @param int $id
+ * @return \think\response\Json
+ */
+ public function index($id)
+ {
+ try {
+ $data = $this->getCompanyDetail($id);
+ $devices = $this->getDevicesByCompanyId($data['companyId']);
+
+ return ResponseHelper::success(
+ array_merge($data, compact('devices'))
+ );
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/superadmin/controller/company/GetCompanyDevicesForProfileController.php b/application/superadmin/controller/company/GetCompanyDevicesForProfileController.php
new file mode 100644
index 0000000..de71b5f
--- /dev/null
+++ b/application/superadmin/controller/company/GetCompanyDevicesForProfileController.php
@@ -0,0 +1,105 @@
+request->param('companyId/d', 0);
+
+ $devices = DeviceModel::alias('d')
+ ->field([
+ 'd.id', 'd.memo', 'd.imei', 'd.phone', 'd.model', 'd.brand', 'd.alive', 'd.id deviceId'
+ ])
+ ->where(compact('companyId'))
+ ->select()
+ ->toArray();
+
+ if (empty($devices)) {
+ throw new \Exception('暂无设备', 200);
+ }
+
+ return $devices;
+ }
+
+ /**
+ * 查询设备与微信的关联关系.
+ *
+ * @return array
+ */
+ protected function getDeviceWechatRelationsByDeviceIds(array $deviceIds): array
+ {
+ // 获取设备最新登录记录的id
+ $latestLogs = DeviceWechatLoginModel::getDevicesLatestLogin($deviceIds);
+
+ // 获取最新登录记录id
+ $latestIds = array_column($latestLogs, 'lastedId');
+
+ return DeviceWechatLoginModel::alias('d')
+ ->field([
+ 'd.deviceId', 'd.wechatId', 'd.alive wAlive'
+ ])
+ ->whereIn('id', $latestIds)
+ ->select()
+ ->toArray();
+ }
+
+ /**
+ * 获取设备的微信好友统计
+ *
+ * @param array $ownerWechatId
+ * @return void
+ */
+ protected function getWechatFriendsCount(array $deviceIds): array
+ {
+ // 查询设备与微信的关联关系
+ $relations = $this->getDeviceWechatRelationsByDeviceIds($deviceIds);
+
+ // 统计微信好友数量
+ $friendCounts = WechatFriendShipModel::alias('f')
+ ->field([
+ 'f.ownerWechatId wechatId', 'count(*) friendCount'
+ ])
+ ->whereIn('ownerWechatId', array_column($relations, 'wechatId'))
+ ->group('ownerWechatId')
+ ->select()
+ ->toArray();
+
+ return ArrHelper::leftJoin($relations, $friendCounts, 'wechatId');
+ }
+
+ /**
+ * 获取公司关联的设备列表
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ $devices = $this->getDevicesWithCompanyId();
+ $friendCount = $this->getWechatFriendsCount(array_column($devices, 'id'));
+
+ $result = ArrHelper::leftJoin($devices, $friendCount, 'deviceId');
+
+ return ResponseHelper::success($result);
+ } catch (\Exception $e) {
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/superadmin/controller/company/GetCompanyListController.php b/application/superadmin/controller/company/GetCompanyListController.php
new file mode 100644
index 0000000..74c8abc
--- /dev/null
+++ b/application/superadmin/controller/company/GetCompanyListController.php
@@ -0,0 +1,126 @@
+request->param('keyword/s', ''))) {
+ $where[] = ['name', 'like', "%{$keyword}%"];
+ }
+
+ return array_merge($params, $where);
+ }
+
+ /**
+ * 获取设备统计
+ *
+ * @return array
+ */
+ protected function getDevices()
+ {
+ $devices = DeviceModel::field('companyId, count(id) as numCount')->group('companyId')->select();
+ $devices = $devices ? $devices->toArray() : array();
+
+ return ArrHelper::columnTokey('companyId', $devices);
+ }
+
+
+ /**
+ * 获取项目列表
+ *
+ * @param array $where 查询条件
+ * @param int $page 页码
+ * @param int $limit 每页数量
+ * @return \think\Paginator 分页对象
+ */
+ protected function getCompanyList(array $where): \think\Paginator
+ {
+ $query = CompanyModel::alias('c')
+ ->field([
+ 'c.id', 'c.name', 'c.status', 'c.companyId', 'c.memo', 'c.createTime'
+ ]);
+
+ foreach ($where as $key => $value) {
+ if (is_numeric($key) && is_array($value) && isset($value[0]) && $value[0] === 'exp') {
+ $query->whereExp('', $value[1]);
+ continue;
+ }
+
+ $query->where($key, $value);
+ }
+
+ return $query->order('id', 'desc')
+ ->paginate($this->request->param('limit/d', 10), false, ['page' => $this->request->param('page/d', 1)]);
+ }
+
+ /**
+ * 统计项目下的用户数量
+ *
+ * @param int $companyId
+ * @return int
+ */
+ protected function countUserInCompany(int $companyId): int
+ {
+ return UsersModel::where('companyId', $companyId)->count('id');
+ }
+
+ /**
+ * 构建返回数据
+ *
+ * @param \think\Paginator $Companylist
+ * @return array
+ */
+ protected function makeReturnedResult(\think\Paginator $Companylist): array
+ {
+ $result = [];
+ $devices = $this->getDevices();
+
+ foreach ($Companylist->items() as $item) {
+ $item->userCount = $this->countUserInCompany($item->companyId);
+ $item->deviceCount = $devices[$item->companyId]['numCount'] ?? 0;
+
+ array_push($result, $item->toArray());
+ }
+
+ return $result;
+ }
+
+ /**
+ * 获取项目列表
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ $where = $this->makeWhere();
+ $result = $this->getCompanyList($where);
+
+ return ResponseHelper::success(
+ [
+ 'list' => $this->makeReturnedResult($result),
+ 'total' => $result->total(),
+ ]
+ );
+ }
+}
\ No newline at end of file
diff --git a/application/superadmin/controller/company/GetCompanySubusersForProfileController.php b/application/superadmin/controller/company/GetCompanySubusersForProfileController.php
new file mode 100644
index 0000000..d9a02f8
--- /dev/null
+++ b/application/superadmin/controller/company/GetCompanySubusersForProfileController.php
@@ -0,0 +1,51 @@
+ $this->request->param('companyId/d', 0),
+ 'isAdmin' => UserModel::ADMIN_OTP
+ ];
+
+ return UserModel::alias('u')
+ ->field([
+ 'u.id', 'u.account', 'u.phone', 'u.username', 'u.avatar', 'u.status', 'u.createTime', 'u.typeId'
+ ])
+ ->where($where)
+ ->select()
+ ->toArray();
+ }
+
+ /**
+ * 获取公司关联的设备列表
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ $users = $this->getSubusers();
+
+ foreach ($users as &$user) {
+ $user['createTime'] = date('Y-m-d H:i:s', $user['createTime']);
+ }
+
+ return ResponseHelper::success($users);
+ }
+}
\ No newline at end of file
diff --git a/application/superadmin/controller/company/UpdateCompanyController.php b/application/superadmin/controller/company/UpdateCompanyController.php
new file mode 100644
index 0000000..aa6fac0
--- /dev/null
+++ b/application/superadmin/controller/company/UpdateCompanyController.php
@@ -0,0 +1,228 @@
+request->post('id/d', 0)
+ );
+
+ if (!$company) {
+ throw new \Exception('项目不存在', 404);
+ }
+
+ // 外部使用
+ $this->companyId = $company->id;
+
+ return $company;
+ }
+
+ /**
+ * 通过账号获取用户信息
+ *
+ * @return UsersModel
+ * @throws \Exception
+ */
+ protected function getUserDetailByCompanyId(): ?UsersModel
+ {
+ $where = [
+ 'isAdmin' => UsersModel::MASTER_USER, // 必须保证 isAdmin 有且只有一个
+ 'companyId' => $this->companyId,
+ ];
+
+ $user = UsersModel::where($where)->find();
+
+ if (!$user) {
+ throw new \Exception('用户不存在', 404);
+ }
+
+ return $user;
+ }
+
+ /**
+ * 更新项目信息
+ *
+ * @param array $params
+ * @return void
+ * @throws \Exception
+ */
+ protected function updateCompany(array $params): void
+ {
+ $params = ArrHelper::getValue('name,status,memo', $params);
+ $params = ArrHelper::rmValue($params);
+
+ $company = $this->getCompanyDetailById();
+ if (!$company->save($params)) {
+ throw new \Exception('项目更新失败', 403);
+ }
+ }
+
+ /**
+ * 更新账号信息
+ *
+ * @param array $params
+ * @return void
+ */
+ protected function updateUserAccount(array $params): void
+ {
+ $params = ArrHelper::getValue('username,account,password,phone,status', $params);
+ $params = ArrHelper::rmValue($params);
+
+ if (isset($params['password'])) {
+ $params['passwordMd5'] = md5($params['password']);
+ $params['passwordLocal'] = localEncrypt($params['password']);
+ }
+
+ $user = $this->getUserDetailByCompanyId();
+ if (!$user->save($params)) {
+ throw new \Exception('用户账号更新失败', 403);
+ }
+ }
+
+ /**
+ * 更新存客宝端数据
+ *
+ * @param array $params
+ * @return self
+ * @throws \Exception
+ */
+ protected function updateCkbAbout(array $params): self
+ {
+ // 1. 更新项目信息
+ $this->updateCompany($params);
+
+ // 2. 更新账号信息
+ $this->updateUserAccount($params);
+
+ return $this;
+ }
+
+ /**
+ * 更新触客宝端数据
+ *
+ * @param array $params
+ * @return self
+ * @throws \Exception
+ */
+ protected function updateS2About(array $params): self
+ {
+ // 1. 更新项目信息
+ $this->updateCompany($params);
+
+ // 2. 更新账号信息
+ $this->updateUserAccount($params);
+
+ return $this;
+ }
+
+ /**
+ * 检查项目名称是否已存在(排除自身)
+ *
+ * @param array $where
+ * @return void
+ * @throws \Exception
+ */
+ protected function checkCompanyNameOrAccountOrPhoneExists(array $where): void
+ {
+ extract($where);
+
+ // 项目名称尽量不重名
+ $exists = CompanyModel::where(compact('name'))->where('id', '<>', $id)->count() > 0;
+ if ($exists) {
+ throw new \Exception('项目名称已存在', 403);
+ }
+
+ // TODO(数据迁移时,存客宝,主账号先查询出id,通过id查询出S2的最新信息,然后更新。)
+ $exists = UsersModel::where(compact('account'))->where('companyId', '<>', $id)->count() > 0;
+ if ($exists) {
+ throw new \Exception('用户账号已存在', 403);
+ }
+
+ // 手机号不重复
+ $exists = UsersModel::where(compact('phone'))->where('companyId', '<>', $id)->count() > 0;
+ if ($exists) {
+ throw new \Exception('手机号已存在', 403);
+ }
+ }
+
+ /**
+ * 数据验证
+ *
+ * @param array $params
+ * @return $this
+ * @throws \Exception
+ */
+ protected function dataValidate(array $params): self
+ {
+ $validate = Validate::make([
+ 'id' => 'require',
+ 'name' => 'require|max:50|/\S+/',
+ 'username' => 'require|max:20|/\S+/',
+ 'account' => 'require|regex:^[a-zA-Z0-9]+$|max:20|/\S+/',
+ 'phone' => 'require|regex:/^1[3-9]\d{9}$/',
+ 'status' => 'require|in:0,1'
+ ], [
+ 'id.require' => '非法请求',
+ 'name.require' => '请输入项目名称',
+ 'username.require' => '请输入用户昵称',
+ 'account.require' => '请输入账号',
+ 'account.regex' => '账号只能用数字或者字母或者数字字母组合',
+ 'account.max' => '账号长度受限',
+ 'phone.require' => '请输入手机号',
+ 'phone.regex' => '手机号格式错误',
+ 'status.require' => '缺少重要参数',
+ 'status.in' => '非法参数',
+ ]);
+
+ if (!$validate->check($params)) {
+ throw new \Exception($validate->getError(), 400);
+ }
+
+ return $this;
+ }
+
+ /**
+ * 更新项目信息
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ try {
+ $params = $this->request->only(['id', 'name', 'status', 'username', 'account', 'password', 'phone', 'memo']);
+
+ // 数据验证
+ $this->dataValidate($params);
+ $this->checkCompanyNameOrAccountOrPhoneExists(ArrHelper::getValue('id,name,account,phone', $params));
+
+ Db::startTrans();
+ $this->updateCkbAbout($params)->updateS2About($params);
+ Db::commit();
+
+ return ResponseHelper::success();
+ } catch (\Exception $e) {
+ Db::rollback();
+ return ResponseHelper::error($e->getMessage(), $e->getCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/superadmin/controller/dashboard/GetBasestatisticsController.php b/application/superadmin/controller/dashboard/GetBasestatisticsController.php
new file mode 100644
index 0000000..e4dfc37
--- /dev/null
+++ b/application/superadmin/controller/dashboard/GetBasestatisticsController.php
@@ -0,0 +1,61 @@
+ $this->getCompanyCount(),
+ 'adminCount' => $this->getAdminCount(),
+ 'customerCount' => $this->getDeviceCount(),
+ ]
+ );
+ }
+}
\ No newline at end of file
diff --git a/application/superadmin/controller/devices/GetAddResultedDevicesController.php b/application/superadmin/controller/devices/GetAddResultedDevicesController.php
new file mode 100644
index 0000000..b877fe7
--- /dev/null
+++ b/application/superadmin/controller/devices/GetAddResultedDevicesController.php
@@ -0,0 +1,149 @@
+value('companyId');
+ }
+
+ /**
+ * 获取项目下的所有设备。
+ *
+ * @param int $companyId
+ * @return array
+ */
+ protected function getAllDevicesIdWithInCompany(int $companyId): array
+ {
+ return DeviceModel::where('companyId', $companyId)->column('id') ?: [0];
+ }
+
+ /**
+ * 执行数据迁移。
+ *
+ * @param int $accountId
+ * @return void
+ */
+ protected function migrateData(int $accountId): void
+ {
+ $companyId = $this->getCompanyIdByAccountId($accountId);
+ $deviceIds = $this->getAllDevicesIdWithInCompany($companyId) ?: [0];
+
+ // 从 s2_device 导入数据。
+ $this->getNewDeviceFromS2_device($deviceIds, $companyId);
+ }
+
+ /**
+ * 获取当前设备数量
+ *
+ * @param int $accountId
+ * @return int
+ */
+ protected function getCkbDeviceCount(int $accountId): int
+ {
+ return DeviceModel::where(
+ [
+ 'companyId' => $this->getCompanyIdByAccountId($accountId)
+ ]
+ )
+ ->count('*');
+ }
+
+ /**
+ * 从 s2_device 导入数据。
+ *
+ * @param array $ids
+ * @param int $companyId
+ * @return void
+ */
+ protected function getNewDeviceFromS2_device(array $ids, int $companyId): void
+ {
+ $ids = implode(',', $ids);
+
+ $sql = "INSERT INTO ck_device(`id`, `imei`, `model`, phone, operatingSystem, memo, alive, brand, rooted, xPosed, softwareVersion, extra, createTime, updateTime, deleteTime, companyId)
+ SELECT
+ d.id, d.imei, d.model, d.phone, d.operatingSystem, d.memo, d.alive, d.brand, d.rooted, d.xPosed, d.softwareVersion, d.extra, d.createTime, d.lastUpdateTime, d.deleteTime, a.departmentId AS companyId
+ FROM s2_device d
+ JOIN s2_company_account a ON d.currentAccountId = a.id
+ WHERE isDeleted = 0 AND deletedAndStop = 0 AND d.id NOT IN ({$ids}) AND a.departmentId = {$companyId}
+ ON DUPLICATE KEY UPDATE
+ imei = VALUES(imei),
+ model = VALUES(model),
+ phone = VALUES(phone),
+ operatingSystem = VALUES(operatingSystem),
+ memo = VALUES(memo),
+ alive = VALUES(alive),
+ brand = VALUES(brand),
+ rooted = VALUES(rooted),
+ xPosed = VALUES(xPosed),
+ softwareVersion = VALUES(softwareVersion),
+ extra = VALUES(extra),
+ updateTime = VALUES(updateTime),
+ deleteTime = VALUES(deleteTime),
+ companyId = VALUES(companyId)";
+
+ Db::query($sql);
+ }
+
+ /**
+ * 获取添加的关联设备结果。
+ *
+ * @param int $accountId
+ * @return bool
+ */
+ protected function getAddResulted(int $accountId): bool
+ {
+ $result = (new ApiDeviceController())->getlist(
+ [
+ 'accountId' => $accountId,
+ 'pageIndex' => 0,
+ 'pageSize' => 100
+ ],
+ true
+ );
+
+ $result = json_decode($result, true);
+ $result = $result['data']['results'][0] ?? false;
+
+ return $result ? (
+ count($result) > $this->getCkbDeviceCount($accountId)
+ ) : false;
+ }
+
+ /**
+ * 获取基础统计信息
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ $accountId = $this->request->param('accountId/d');
+
+ $isAdded = $this->getAddResulted($accountId);
+ $isAdded && $this->migrateData($accountId);
+
+ return ResponseHelper::success(
+ [
+ 'added' => $isAdded
+ ]
+ );
+ }
+}
\ No newline at end of file
diff --git a/application/superadmin/controller/traffic/GetPoolDetailController.php b/application/superadmin/controller/traffic/GetPoolDetailController.php
new file mode 100644
index 0000000..f968c7d
--- /dev/null
+++ b/application/superadmin/controller/traffic/GetPoolDetailController.php
@@ -0,0 +1,126 @@
+ 400, 'msg' => '参数错误']);
+ }
+
+ try {
+ // 查询流量来源信息
+ $sourceInfo = TrafficSourceModel::alias('ts')
+ ->join('company c', 'ts.companyId = c.companyId', 'LEFT')
+ ->field([
+ 'ts.fromd as source',
+ 'ts.createTime as addTime',
+ 'c.name as projectName',
+ 'ts.identifier'
+ ])
+ ->where('ts.id', $id)
+ ->find();
+
+ if (!$sourceInfo) {
+ return json(['code' => 404, 'msg' => '记录不存在']);
+ }
+
+ // 查询客户池信息
+ $poolInfo = TrafficPoolModel::where('identifier', $sourceInfo['identifier'])
+ ->field('wechatId')
+ ->find();
+
+ $result = [
+ 'source' => $sourceInfo['source'],
+ 'addTime' => $sourceInfo['addTime'] ? date('Y-m-d H:i:s', $sourceInfo['addTime']) : null,
+ 'projectName' => $sourceInfo['projectName']
+ ];
+
+ // 如果存在微信ID,查询微信账号信息
+ if ($poolInfo && $poolInfo['wechatId']) {
+ // 查询微信账号信息
+ $wechatInfo = WechatAccountModel::where('wechatId', $poolInfo['wechatId'])
+ ->field('avatar,nickname,region,gender')
+ ->find();
+
+ if ($wechatInfo) {
+ $result = array_merge($result, [
+ 'avatar' => $wechatInfo['avatar'],
+ 'nickname' => $wechatInfo['nickname'],
+ 'region' => $wechatInfo['region'],
+ 'gender' => $this->formatGender($wechatInfo['gender'])
+ ]);
+
+ // 查询标签信息
+ $tagInfo = WechatTagModel::where('wechatId', $poolInfo['wechatId'])
+ ->field('tags')
+ ->find();
+
+ if ($tagInfo) {
+ $result['tags'] = is_string($tagInfo['tags']) ?
+ json_decode($tagInfo['tags'], true) :
+ $tagInfo['tags'];
+ } else {
+ $result['tags'] = [];
+ }
+ }
+ } else {
+ $result = array_merge($result, [
+ 'avatar' => '',
+ 'nickname' => '未知',
+ 'region' => '未知',
+ 'gender' => $this->formatGender(0),
+ 'tags' => []
+ ]);
+ }
+
+ return json([
+ 'code' => 200,
+ 'msg' => '获取成功',
+ 'data' => $result
+ ]);
+
+ } catch (\Exception $e) {
+ return json([
+ 'code' => 500,
+ 'msg' => '系统错误:' . $e->getMessage()
+ ]);
+ }
+ }
+
+ /**
+ * 格式化性别显示
+ * @param int $gender
+ * @return string
+ */
+ protected function formatGender($gender)
+ {
+ switch ($gender) {
+ case 1:
+ return '男';
+ case 2:
+ return '女';
+ default:
+ return '保密';
+ }
+ }
+}
\ No newline at end of file
diff --git a/application/superadmin/controller/traffic/GetPoolListController.php b/application/superadmin/controller/traffic/GetPoolListController.php
new file mode 100644
index 0000000..d19ce5d
--- /dev/null
+++ b/application/superadmin/controller/traffic/GetPoolListController.php
@@ -0,0 +1,113 @@
+each(function ($item) {
+ $item->gender = $this->formatGender($item->gender);
+ $item->addTime = $this->formatDate($item->addTime);
+ $item->tags = $this->handlTags($item->tags);
+ });
+
+ return $list;
+ }
+
+ /**
+ * 构建查询.
+ *
+ * @return TrafficPoolModel|\think\Paginator
+ */
+ protected function gePoolList(): \think\Paginator
+ {
+ $query = TrafficPoolModel::alias('tp')
+ ->field([
+ 'tp.wechatId',
+ 'ts.id', 'ts.createTime as addTime', 'ts.fromd as source',
+ 'c.name as projectName',
+ 'wa.avatar', 'wa.gender', 'wa.nickname', 'wa.region',
+ 'wt.tags'
+ ])
+ ->join('traffic_source ts', 'tp.identifier = ts.identifier', 'RIGHT')
+ ->join('company c', 'ts.companyId = c.companyId', 'LEFT')
+ ->join('wechat_account wa', 'tp.wechatId = wa.wechatId', 'LEFT')
+ ->join('wechat_tag wt', 'wa.wechatId = wt.wechatId', 'LEFT');
+
+ return $query->paginate($this->request->param('limit/d', 10), false, ['page' => $this->request->param('page/d', 1)]);
+ }
+
+ /**
+ * 获取客户池列表
+ *
+ * @return \think\response\Json
+ */
+ public function index()
+ {
+ $list = $this->gePoolList();
+
+ return ResponseHelper::success(
+ [
+ 'list' => $this->makeReturnedValue($list)->items(),
+ 'total' => $list->total(),
+ 'page' => $list->currentPage(),
+ 'limit' => $list->listRows()
+ ]
+ );
+ }
+}
\ No newline at end of file
diff --git a/application/superadmin/middleware/AdminAuth.php b/application/superadmin/middleware/AdminAuth.php
new file mode 100644
index 0000000..dc61d93
--- /dev/null
+++ b/application/superadmin/middleware/AdminAuth.php
@@ -0,0 +1,81 @@
+method(true) == 'OPTIONS') {
+ return $next($request);
+ }
+
+ // 获取Cookie中的管理员信息
+ $adminId = cookie('admin_id');
+ $adminToken = cookie('admin_token');
+
+ // 如果没有登录信息,返回401未授权
+ if (empty($adminId) || empty($adminToken)) {
+ return json([
+ 'code' => 401,
+ 'msg' => '请先登录',
+ 'data' => null
+ ]);
+ }
+
+ // 获取管理员信息
+ $admin = Administrator::where([
+ ['id', '=', $adminId],
+ ['status', '=', 1]
+ ])->find();
+
+ // 如果管理员不存在,返回401未授权
+ if (!$admin) {
+ return json([
+ 'code' => 401,
+ 'msg' => '管理员账号不存在或已被禁用',
+ 'data' => null
+ ]);
+ }
+
+ // 验证Token是否有效
+ $expectedToken = $this->createToken($admin);
+
+ if ($adminToken !== $expectedToken) {
+ return json([
+ 'code' => 401,
+ 'msg' => '登录已过期,请重新登录',
+ 'data' => null
+ ]);
+ }
+
+ // 将管理员信息绑定到请求对象,方便后续控制器使用
+ $request->adminInfo = $admin;
+
+ // 继续执行后续操作
+ return $next($request);
+ }
+
+ /**
+ * 创建登录令牌
+ *
+ * @param Administrator $admin
+ * @return string
+ */
+ private function createToken($admin)
+ {
+ $data = $admin->id . '|' . $admin->account;
+ return md5($data . 'cunkebao_admin_secret');
+ }
+}
\ No newline at end of file
diff --git a/application/tags.php b/application/tags.php
new file mode 100644
index 0000000..4b18d10
--- /dev/null
+++ b/application/tags.php
@@ -0,0 +1,28 @@
+
+// +----------------------------------------------------------------------
+
+// 应用行为扩展定义文件
+return [
+ // 应用初始化
+ 'app_init' => [],
+ // 应用开始
+ 'app_begin' => [],
+ // 模块初始化
+ 'module_init' => [],
+ // 操作开始执行
+ 'action_begin' => [],
+ // 视图内容过滤
+ 'view_filter' => [],
+ // 日志写入
+ 'log_write' => [],
+ // 应用结束
+ 'app_end' => [],
+];
diff --git a/build.php b/build.php
new file mode 100644
index 0000000..34ba3c8
--- /dev/null
+++ b/build.php
@@ -0,0 +1,26 @@
+
+// +----------------------------------------------------------------------
+
+return [
+ // 生成应用公共文件
+ '__file__' => ['common.php'],
+
+ // 定义demo模块的自动生成 (按照实际定义的文件名生成)
+ 'demo' => [
+ '__file__' => ['common.php'],
+ '__dir__' => ['behavior', 'controller', 'model', 'view'],
+ 'controller' => ['Index', 'Test', 'UserType'],
+ 'model' => ['User', 'UserType'],
+ 'view' => ['index/index'],
+ ],
+
+ // 其他更多的模块定义
+];
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..ce1721e
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,120 @@
+{
+ "name": "topthink/think",
+ "description": "the new thinkphp framework",
+ "type": "project",
+ "keywords": [
+ "framework",
+ "thinkphp",
+ "ORM"
+ ],
+ "homepage": "http://thinkphp.cn/",
+ "license": "Apache-2.0",
+ "authors": [
+ {
+ "name": "liu21st",
+ "email": "liu21st@gmail.com"
+ },
+ {
+ "name": "yunwuxin",
+ "email": "448901948@qq.com"
+ }
+ ],
+ "require": {
+ "php": ">=5.6.0",
+ "topthink/framework": "5.1.41",
+ "topthink/think-installer": "2.*",
+ "topthink/think-captcha": "^2.0",
+ "topthink/think-helper": "^3.0",
+ "topthink/think-image": "^1.0",
+ "topthink/think-queue": "^2.0",
+ "topthink/think-worker": "^2.0",
+ "textalk/websocket": "^1.5",
+ "aliyuncs/oss-sdk-php": "^2.6",
+ "monolog/monolog": "^1.27",
+ "guzzlehttp/guzzle": "^6.5",
+ "overtrue/wechat": "~4.6",
+ "endroid/qr-code": "^3.9",
+ "phpoffice/phpspreadsheet": "^1.29",
+ "workerman/workerman": "^3.5",
+ "workerman/gateway-worker": "^3.0",
+ "hashids/hashids": "^2.0",
+ "khanamiryan/qrcode-detector-decoder": "^1.0",
+ "lizhichao/word": "^2.0",
+ "adbario/php-dot-notation": "^2.2"
+ },
+ "require-dev": {
+ "symfony/var-dumper": "^3.4|^4.4",
+ "topthink/think-migration": "^2.0",
+ "phpunit/phpunit": "^5.0|^6.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "app\\": "application",
+ "Eison\\": "extend/Eison"
+ },
+ "files": [
+ "application/common.php"
+ ],
+ "homepage": "http://thinkphp.cn/",
+ "license": "Apache-2.0",
+ "authors": [
+ {
+ "name": "liu21st",
+ "email": "liu21st@gmail.com"
+ },
+ {
+ "name": "yunwuxin",
+ "email": "448901948@qq.com"
+ }
+ ],
+ "require": {
+ "php": ">=5.6.0",
+ "topthink/framework": "5.1.41",
+ "topthink/think-installer": "~1.0",
+ "topthink/think-captcha": "^2.0",
+ "topthink/think-helper": "^3.0",
+ "topthink/think-image": "^1.0",
+ "topthink/think-queue": "^2.0",
+ "topthink/think-worker": "^2.0",
+ "textalk/websocket": "^1.2",
+ "aliyuncs/oss-sdk-php": "^2.3",
+ "monolog/monolog": "^1.24",
+ "guzzlehttp/guzzle": "^6.3",
+ "overtrue/wechat": "~4.0",
+ "endroid/qr-code": "^3.5",
+ "phpoffice/phpspreadsheet": "^1.8",
+ "workerman/workerman": "^3.5",
+ "workerman/gateway-worker": "^3.0",
+ "hashids/hashids": "^2.0",
+ "khanamiryan/qrcode-detector-decoder": "^1.0",
+ "lizhichao/word": "^2.0",
+ "adbario/php-dot-notation": "^2.2"
+ },
+ "require-dev": {
+ "symfony/var-dumper": "^3.4",
+ "topthink/think-migration": "^2.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "app\\": "application",
+ "Eison\\": "extend/Eison"
+ },
+ "files": [
+ "application/common.php"
+ ],
+ "classmap": []
+ },
+ "extra": {
+ "think-path": "thinkphp"
+ },
+ "config": {
+ "preferred-install": "dist",
+ "allow-plugins": {
+ "topthink/think-installer": true,
+ "easywechat-composer/easywechat-composer": true
+ }
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true
+ }
+}
\ No newline at end of file
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..56ff4a9
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,4367 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "5bca28194f84a7d13965d1851e6ce38a",
+ "packages": [
+ {
+ "name": "adbario/php-dot-notation",
+ "version": "2.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/adbario/php-dot-notation.git",
+ "reference": "081e2cca50c84bfeeea2e3ef9b2c8d206d80ccae"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/adbario/php-dot-notation/zipball/081e2cca50c84bfeeea2e3ef9b2c8d206d80ccae",
+ "reference": "081e2cca50c84bfeeea2e3ef9b2c8d206d80ccae",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "php": "^5.5 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8|^5.7|^6.6|^7.5|^8.5|^9.5",
+ "squizlabs/php_codesniffer": "^3.6"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/helpers.php"
+ ],
+ "psr-4": {
+ "Adbar\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Riku Särkinen",
+ "email": "riku@adbar.io"
+ }
+ ],
+ "description": "PHP dot notation access to arrays",
+ "homepage": "https://github.com/adbario/php-dot-notation",
+ "keywords": [
+ "ArrayAccess",
+ "dotnotation"
+ ],
+ "support": {
+ "issues": "https://github.com/adbario/php-dot-notation/issues",
+ "source": "https://github.com/adbario/php-dot-notation/tree/2.5.0"
+ },
+ "time": "2022-10-14T20:31:46+00:00"
+ },
+ {
+ "name": "aliyuncs/oss-sdk-php",
+ "version": "v2.7.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/aliyun/aliyun-oss-php-sdk.git",
+ "reference": "483dd0b8bff5d47f0e4ffc99f6077a295c5ccbb5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/aliyun/aliyun-oss-php-sdk/zipball/483dd0b8bff5d47f0e4ffc99f6077a295c5ccbb5",
+ "reference": "483dd0b8bff5d47f0e4ffc99f6077a295c5ccbb5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "*",
+ "phpunit/phpunit": "*"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "OSS\\": "src/OSS"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Aliyuncs",
+ "homepage": "http://www.aliyun.com"
+ }
+ ],
+ "description": "Aliyun OSS SDK for PHP",
+ "homepage": "http://www.aliyun.com/product/oss/",
+ "support": {
+ "issues": "https://github.com/aliyun/aliyun-oss-php-sdk/issues",
+ "source": "https://github.com/aliyun/aliyun-oss-php-sdk/tree/v2.7.2"
+ },
+ "time": "2024-10-28T10:41:12+00:00"
+ },
+ {
+ "name": "bacon/bacon-qr-code",
+ "version": "2.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Bacon/BaconQrCode.git",
+ "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22",
+ "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22",
+ "shasum": ""
+ },
+ "require": {
+ "dasprid/enum": "^1.0.3",
+ "ext-iconv": "*",
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "phly/keep-a-changelog": "^2.1",
+ "phpunit/phpunit": "^7 | ^8 | ^9",
+ "spatie/phpunit-snapshot-assertions": "^4.2.9",
+ "squizlabs/php_codesniffer": "^3.4"
+ },
+ "suggest": {
+ "ext-imagick": "to generate QR code images"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "BaconQrCode\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-2-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Ben Scholzen 'DASPRiD'",
+ "email": "mail@dasprids.de",
+ "homepage": "https://dasprids.de/",
+ "role": "Developer"
+ }
+ ],
+ "description": "BaconQrCode is a QR code generator for PHP.",
+ "homepage": "https://github.com/Bacon/BaconQrCode",
+ "support": {
+ "issues": "https://github.com/Bacon/BaconQrCode/issues",
+ "source": "https://github.com/Bacon/BaconQrCode/tree/2.0.8"
+ },
+ "time": "2022-12-07T17:46:57+00:00"
+ },
+ {
+ "name": "composer/pcre",
+ "version": "3.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/pcre.git",
+ "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
+ "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "conflict": {
+ "phpstan/phpstan": "<1.11.10"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.12 || ^2",
+ "phpstan/phpstan-strict-rules": "^1 || ^2",
+ "phpunit/phpunit": "^8 || ^9"
+ },
+ "type": "library",
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "extension.neon"
+ ]
+ },
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Pcre\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "PCRE wrapping library that offers type-safe preg_* replacements.",
+ "keywords": [
+ "PCRE",
+ "preg",
+ "regex",
+ "regular expression"
+ ],
+ "support": {
+ "issues": "https://github.com/composer/pcre/issues",
+ "source": "https://github.com/composer/pcre/tree/3.3.2"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-11-12T16:29:46+00:00"
+ },
+ {
+ "name": "dasprid/enum",
+ "version": "1.0.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/DASPRiD/Enum.git",
+ "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce",
+ "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1 <9.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11",
+ "squizlabs/php_codesniffer": "*"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "DASPRiD\\Enum\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-2-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Ben Scholzen 'DASPRiD'",
+ "email": "mail@dasprids.de",
+ "homepage": "https://dasprids.de/",
+ "role": "Developer"
+ }
+ ],
+ "description": "PHP 7.1 enum implementation",
+ "keywords": [
+ "enum",
+ "map"
+ ],
+ "support": {
+ "issues": "https://github.com/DASPRiD/Enum/issues",
+ "source": "https://github.com/DASPRiD/Enum/tree/1.0.7"
+ },
+ "time": "2025-09-16T12:23:56+00:00"
+ },
+ {
+ "name": "easywechat-composer/easywechat-composer",
+ "version": "1.4.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/mingyoung/easywechat-composer.git",
+ "reference": "3fc6a7ab6d3853c0f4e2922539b56cc37ef361cd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/mingyoung/easywechat-composer/zipball/3fc6a7ab6d3853c0f4e2922539b56cc37ef361cd",
+ "reference": "3fc6a7ab6d3853c0f4e2922539b56cc37ef361cd",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^1.0 || ^2.0",
+ "php": ">=7.0"
+ },
+ "require-dev": {
+ "composer/composer": "^1.0 || ^2.0",
+ "phpunit/phpunit": "^6.5 || ^7.0"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "EasyWeChatComposer\\Plugin"
+ },
+ "autoload": {
+ "psr-4": {
+ "EasyWeChatComposer\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "张铭阳",
+ "email": "mingyoungcheung@gmail.com"
+ }
+ ],
+ "description": "The composer plugin for EasyWeChat",
+ "support": {
+ "issues": "https://github.com/mingyoung/easywechat-composer/issues",
+ "source": "https://github.com/mingyoung/easywechat-composer/tree/1.4.1"
+ },
+ "time": "2021-07-05T04:03:22+00:00"
+ },
+ {
+ "name": "endroid/qr-code",
+ "version": "3.9.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/endroid/qr-code.git",
+ "reference": "94563d7b3105288e6ac53a67ae720e3669fac1f6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/endroid/qr-code/zipball/94563d7b3105288e6ac53a67ae720e3669fac1f6",
+ "reference": "94563d7b3105288e6ac53a67ae720e3669fac1f6",
+ "shasum": ""
+ },
+ "require": {
+ "bacon/bacon-qr-code": "^2.0",
+ "khanamiryan/qrcode-detector-decoder": "^1.0.5",
+ "myclabs/php-enum": "^1.5",
+ "php": "^7.3||^8.0",
+ "symfony/options-resolver": "^3.4||^4.4||^5.0",
+ "symfony/property-access": "^3.4||^4.4||^5.0"
+ },
+ "require-dev": {
+ "endroid/quality": "^1.5.2",
+ "setasign/fpdf": "^1.8"
+ },
+ "suggest": {
+ "ext-gd": "Required for generating PNG images",
+ "roave/security-advisories": "Avoids installation of package versions with vulnerabilities",
+ "setasign/fpdf": "Required to use the FPDF writer.",
+ "symfony/security-checker": "Checks your composer.lock for vulnerabilities"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Endroid\\QrCode\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jeroen van den Enden",
+ "email": "info@endroid.nl"
+ }
+ ],
+ "description": "Endroid QR Code",
+ "homepage": "https://github.com/endroid/qr-code",
+ "keywords": [
+ "bundle",
+ "code",
+ "endroid",
+ "php",
+ "qr",
+ "qrcode"
+ ],
+ "support": {
+ "issues": "https://github.com/endroid/qr-code/issues",
+ "source": "https://github.com/endroid/qr-code/tree/3.9.7"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/endroid",
+ "type": "github"
+ }
+ ],
+ "time": "2021-04-20T19:10:54+00:00"
+ },
+ {
+ "name": "ezyang/htmlpurifier",
+ "version": "v4.19.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ezyang/htmlpurifier.git",
+ "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf",
+ "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf",
+ "shasum": ""
+ },
+ "require": {
+ "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0"
+ },
+ "require-dev": {
+ "cerdic/css-tidy": "^1.7 || ^2.0",
+ "simpletest/simpletest": "dev-master"
+ },
+ "suggest": {
+ "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
+ "ext-bcmath": "Used for unit conversion and imagecrash protection",
+ "ext-iconv": "Converts text to and from non-UTF-8 encodings",
+ "ext-tidy": "Used for pretty-printing HTML"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "library/HTMLPurifier.composer.php"
+ ],
+ "psr-0": {
+ "HTMLPurifier": "library/"
+ },
+ "exclude-from-classmap": [
+ "/library/HTMLPurifier/Language/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-2.1-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Edward Z. Yang",
+ "email": "admin@htmlpurifier.org",
+ "homepage": "http://ezyang.com"
+ }
+ ],
+ "description": "Standards compliant HTML filter written in PHP",
+ "homepage": "http://htmlpurifier.org/",
+ "keywords": [
+ "html"
+ ],
+ "support": {
+ "issues": "https://github.com/ezyang/htmlpurifier/issues",
+ "source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0"
+ },
+ "time": "2025-10-17T16:34:55+00:00"
+ },
+ {
+ "name": "guzzlehttp/guzzle",
+ "version": "6.5.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/guzzle.git",
+ "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/a52f0440530b54fa079ce76e8c5d196a42cad981",
+ "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "guzzlehttp/promises": "^1.0",
+ "guzzlehttp/psr7": "^1.9",
+ "php": ">=5.5",
+ "symfony/polyfill-intl-idn": "^1.17"
+ },
+ "require-dev": {
+ "ext-curl": "*",
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
+ "psr/log": "^1.1"
+ },
+ "suggest": {
+ "psr/log": "Required for using the Log middleware"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "6.5-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "GuzzleHttp\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Jeremy Lindblom",
+ "email": "jeremeamia@gmail.com",
+ "homepage": "https://github.com/jeremeamia"
+ },
+ {
+ "name": "George Mponos",
+ "email": "gmponos@gmail.com",
+ "homepage": "https://github.com/gmponos"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://github.com/sagikazarmark"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "Guzzle is a PHP HTTP client library",
+ "homepage": "http://guzzlephp.org/",
+ "keywords": [
+ "client",
+ "curl",
+ "framework",
+ "http",
+ "http client",
+ "rest",
+ "web service"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/guzzle/issues",
+ "source": "https://github.com/guzzle/guzzle/tree/6.5.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-06-20T22:16:07+00:00"
+ },
+ {
+ "name": "guzzlehttp/promises",
+ "version": "1.5.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/promises.git",
+ "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/67ab6e18aaa14d753cc148911d273f6e6cb6721e",
+ "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.5"
+ },
+ "require-dev": {
+ "symfony/phpunit-bridge": "^4.4 || ^5.1"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "GuzzleHttp\\Promise\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "Guzzle promises library",
+ "keywords": [
+ "promise"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/promises/issues",
+ "source": "https://github.com/guzzle/promises/tree/1.5.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-05-21T12:31:43+00:00"
+ },
+ {
+ "name": "guzzlehttp/psr7",
+ "version": "1.9.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/psr7.git",
+ "reference": "e4490cabc77465aaee90b20cfc9a770f8c04be6b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/e4490cabc77465aaee90b20cfc9a770f8c04be6b",
+ "reference": "e4490cabc77465aaee90b20cfc9a770f8c04be6b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4.0",
+ "psr/http-message": "~1.0",
+ "ralouphie/getallheaders": "^2.0.5 || ^3.0.0"
+ },
+ "provide": {
+ "psr/http-message-implementation": "1.0"
+ },
+ "require-dev": {
+ "ext-zlib": "*",
+ "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10"
+ },
+ "suggest": {
+ "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "GuzzleHttp\\Psr7\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "George Mponos",
+ "email": "gmponos@gmail.com",
+ "homepage": "https://github.com/gmponos"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://github.com/sagikazarmark"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "PSR-7 message implementation that also provides common utility methods",
+ "keywords": [
+ "http",
+ "message",
+ "psr-7",
+ "request",
+ "response",
+ "stream",
+ "uri",
+ "url"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/psr7/issues",
+ "source": "https://github.com/guzzle/psr7/tree/1.9.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-04-17T16:00:37+00:00"
+ },
+ {
+ "name": "hashids/hashids",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/vinkla/hashids.git",
+ "reference": "7a945a5192d4a5c8888364970feece9bc26179df"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/vinkla/hashids/zipball/7a945a5192d4a5c8888364970feece9bc26179df",
+ "reference": "7a945a5192d4a5c8888364970feece9bc26179df",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6.4 || ^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.7 || ^6.3"
+ },
+ "suggest": {
+ "ext-bcmatch": "Required to use BC Math arbitrary precision mathematics (*).",
+ "ext-gmp": "Required to use GNU multiple precision mathematics (*)."
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Hashids\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ivan Akimov",
+ "email": "ivan@barreleye.com",
+ "homepage": "https://twitter.com/IvanAkimov"
+ },
+ {
+ "name": "Vincent Klaiber",
+ "email": "hello@vinkla.com",
+ "homepage": "https://vinkla.com"
+ }
+ ],
+ "description": "Generate short, unique, non-sequential ids (like YouTube and Bitly) from numbers",
+ "homepage": "http://hashids.org/php",
+ "keywords": [
+ "bitly",
+ "decode",
+ "encode",
+ "hash",
+ "hashid",
+ "hashids",
+ "ids",
+ "obfuscate",
+ "youtube"
+ ],
+ "support": {
+ "issues": "https://github.com/vinkla/hashids/issues",
+ "source": "https://github.com/vinkla/hashids/tree/2.0.4"
+ },
+ "time": "2017-10-28T11:24:20+00:00"
+ },
+ {
+ "name": "khanamiryan/qrcode-detector-decoder",
+ "version": "1.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/khanamiryan/php-qrcode-detector-decoder.git",
+ "reference": "45326fb83a2a375065dbb3a134b5b8a5872da569"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/khanamiryan/php-qrcode-detector-decoder/zipball/45326fb83a2a375065dbb3a134b5b8a5872da569",
+ "reference": "45326fb83a2a375065dbb3a134b5b8a5872da569",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.7 | ^7.5 | ^8.0 | ^9.0",
+ "rector/rector": "^0.13.6",
+ "symplify/easy-coding-standard": "^11.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "lib/Common/customFunctions.php"
+ ],
+ "psr-4": {
+ "Zxing\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT",
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "Ashot Khanamiryan",
+ "email": "a.khanamiryan@gmail.com",
+ "homepage": "https://github.com/khanamiryan",
+ "role": "Developer"
+ }
+ ],
+ "description": "QR code decoder / reader",
+ "homepage": "https://github.com/khanamiryan/php-qrcode-detector-decoder/",
+ "keywords": [
+ "barcode",
+ "qr",
+ "zxing"
+ ],
+ "support": {
+ "issues": "https://github.com/khanamiryan/php-qrcode-detector-decoder/issues",
+ "source": "https://github.com/khanamiryan/php-qrcode-detector-decoder/tree/1.0.6"
+ },
+ "time": "2022-06-29T09:25:13+00:00"
+ },
+ {
+ "name": "lizhichao/word",
+ "version": "v2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/lizhichao/VicWord.git",
+ "reference": "f17172d45f505e7140da0bde2103defc13255326"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/lizhichao/VicWord/zipball/f17172d45f505e7140da0bde2103defc13255326",
+ "reference": "f17172d45f505e7140da0bde2103defc13255326",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Lizhichao\\Word\\": "Lib"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "tanszhe",
+ "email": "1018595261@qq.com"
+ }
+ ],
+ "description": "This is a participle library",
+ "support": {
+ "issues": "https://github.com/lizhichao/VicWord/issues",
+ "source": "https://github.com/lizhichao/VicWord/tree/master"
+ },
+ "time": "2020-07-30T07:33:06+00:00"
+ },
+ {
+ "name": "maennchen/zipstream-php",
+ "version": "2.2.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/maennchen/ZipStream-PHP.git",
+ "reference": "30ad6f93cf3efe4192bc7a4c9cad11ff8f4f237f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/30ad6f93cf3efe4192bc7a4c9cad11ff8f4f237f",
+ "reference": "30ad6f93cf3efe4192bc7a4c9cad11ff8f4f237f",
+ "shasum": ""
+ },
+ "require": {
+ "myclabs/php-enum": "^1.5",
+ "php": "^7.4 || ^8.0",
+ "psr/http-message": "^1.0",
+ "symfony/polyfill-mbstring": "^1.0"
+ },
+ "require-dev": {
+ "ext-zip": "*",
+ "friendsofphp/php-cs-fixer": "^3.9",
+ "guzzlehttp/guzzle": "^6.5.3 || ^7.2.0",
+ "mikey179/vfsstream": "^1.6",
+ "php-coveralls/php-coveralls": "^2.4",
+ "phpunit/phpunit": "^8.5.8 || ^9.4.2",
+ "vimeo/psalm": "^4.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "ZipStream\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Paul Duncan",
+ "email": "pabs@pablotron.org"
+ },
+ {
+ "name": "Jonatan Männchen",
+ "email": "jonatan@maennchen.ch"
+ },
+ {
+ "name": "Jesse Donat",
+ "email": "donatj@gmail.com"
+ },
+ {
+ "name": "András Kolesár",
+ "email": "kolesar@kolesar.hu"
+ }
+ ],
+ "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
+ "keywords": [
+ "stream",
+ "zip"
+ ],
+ "support": {
+ "issues": "https://github.com/maennchen/ZipStream-PHP/issues",
+ "source": "https://github.com/maennchen/ZipStream-PHP/tree/2.2.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/maennchen",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/zipstream",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2022-11-25T18:57:19+00:00"
+ },
+ {
+ "name": "markbaker/complex",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/MarkBaker/PHPComplex.git",
+ "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
+ "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "dev-master",
+ "phpcompatibility/php-compatibility": "^9.3",
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
+ "squizlabs/php_codesniffer": "^3.7"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Complex\\": "classes/src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mark Baker",
+ "email": "mark@lange.demon.co.uk"
+ }
+ ],
+ "description": "PHP Class for working with complex numbers",
+ "homepage": "https://github.com/MarkBaker/PHPComplex",
+ "keywords": [
+ "complex",
+ "mathematics"
+ ],
+ "support": {
+ "issues": "https://github.com/MarkBaker/PHPComplex/issues",
+ "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
+ },
+ "time": "2022-12-06T16:21:08+00:00"
+ },
+ {
+ "name": "markbaker/matrix",
+ "version": "3.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/MarkBaker/PHPMatrix.git",
+ "reference": "728434227fe21be27ff6d86621a1b13107a2562c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
+ "reference": "728434227fe21be27ff6d86621a1b13107a2562c",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "dev-master",
+ "phpcompatibility/php-compatibility": "^9.3",
+ "phpdocumentor/phpdocumentor": "2.*",
+ "phploc/phploc": "^4.0",
+ "phpmd/phpmd": "2.*",
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
+ "sebastian/phpcpd": "^4.0",
+ "squizlabs/php_codesniffer": "^3.7"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Matrix\\": "classes/src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mark Baker",
+ "email": "mark@demon-angel.eu"
+ }
+ ],
+ "description": "PHP Class for working with matrices",
+ "homepage": "https://github.com/MarkBaker/PHPMatrix",
+ "keywords": [
+ "mathematics",
+ "matrix",
+ "vector"
+ ],
+ "support": {
+ "issues": "https://github.com/MarkBaker/PHPMatrix/issues",
+ "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
+ },
+ "time": "2022-12-02T22:17:43+00:00"
+ },
+ {
+ "name": "monolog/monolog",
+ "version": "1.27.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Seldaek/monolog.git",
+ "reference": "904713c5929655dc9b97288b69cfeedad610c9a1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Seldaek/monolog/zipball/904713c5929655dc9b97288b69cfeedad610c9a1",
+ "reference": "904713c5929655dc9b97288b69cfeedad610c9a1",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0",
+ "psr/log": "~1.0"
+ },
+ "provide": {
+ "psr/log-implementation": "1.0.0"
+ },
+ "require-dev": {
+ "aws/aws-sdk-php": "^2.4.9 || ^3.0",
+ "doctrine/couchdb": "~1.0@dev",
+ "graylog2/gelf-php": "~1.0",
+ "php-amqplib/php-amqplib": "~2.4",
+ "php-console/php-console": "^3.1.3",
+ "phpstan/phpstan": "^0.12.59",
+ "phpunit/phpunit": "~4.5",
+ "ruflin/elastica": ">=0.90 <3.0",
+ "sentry/sentry": "^0.13",
+ "swiftmailer/swiftmailer": "^5.3|^6.0"
+ },
+ "suggest": {
+ "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
+ "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
+ "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
+ "ext-mongo": "Allow sending log messages to a MongoDB server",
+ "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
+ "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver",
+ "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
+ "php-console/php-console": "Allow sending log messages to Google Chrome",
+ "rollbar/rollbar": "Allow sending log messages to Rollbar",
+ "ruflin/elastica": "Allow sending log messages to an Elastic Search server",
+ "sentry/sentry": "Allow sending log messages to a Sentry server"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Monolog\\": "src/Monolog"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "Sends your logs to files, sockets, inboxes, databases and various web services",
+ "homepage": "http://github.com/Seldaek/monolog",
+ "keywords": [
+ "log",
+ "logging",
+ "psr-3"
+ ],
+ "support": {
+ "issues": "https://github.com/Seldaek/monolog/issues",
+ "source": "https://github.com/Seldaek/monolog/tree/1.27.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Seldaek",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/monolog/monolog",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-06-09T08:53:42+00:00"
+ },
+ {
+ "name": "myclabs/php-enum",
+ "version": "1.8.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/php-enum.git",
+ "reference": "e7be26966b7398204a234f8673fdad5ac6277802"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/php-enum/zipball/e7be26966b7398204a234f8673fdad5ac6277802",
+ "reference": "e7be26966b7398204a234f8673fdad5ac6277802",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "php": "^7.3 || ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5",
+ "squizlabs/php_codesniffer": "1.*",
+ "vimeo/psalm": "^4.6.2 || ^5.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "MyCLabs\\Enum\\": "src/"
+ },
+ "classmap": [
+ "stubs/Stringable.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP Enum contributors",
+ "homepage": "https://github.com/myclabs/php-enum/graphs/contributors"
+ }
+ ],
+ "description": "PHP Enum implementation",
+ "homepage": "https://github.com/myclabs/php-enum",
+ "keywords": [
+ "enum"
+ ],
+ "support": {
+ "issues": "https://github.com/myclabs/php-enum/issues",
+ "source": "https://github.com/myclabs/php-enum/tree/1.8.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/mnapoli",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-01-14T11:49:03+00:00"
+ },
+ {
+ "name": "overtrue/socialite",
+ "version": "2.0.24",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/overtrue/socialite.git",
+ "reference": "ee7e7b000ec7d64f2b8aba1f6a2eec5cdf3f8bec"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/overtrue/socialite/zipball/ee7e7b000ec7d64f2b8aba1f6a2eec5cdf3f8bec",
+ "reference": "ee7e7b000ec7d64f2b8aba1f6a2eec5cdf3f8bec",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "guzzlehttp/guzzle": "^5.0|^6.0|^7.0",
+ "php": ">=5.6",
+ "symfony/http-foundation": "^2.7|^3.0|^4.0|^5.0"
+ },
+ "require-dev": {
+ "mockery/mockery": "~1.2",
+ "phpunit/phpunit": "^6.0|^7.0|^8.0|^9.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Overtrue\\Socialite\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "overtrue",
+ "email": "anzhengchao@gmail.com"
+ }
+ ],
+ "description": "A collection of OAuth 2 packages that extracts from laravel/socialite.",
+ "keywords": [
+ "login",
+ "oauth",
+ "qq",
+ "social",
+ "wechat",
+ "weibo"
+ ],
+ "support": {
+ "issues": "https://github.com/overtrue/socialite/issues",
+ "source": "https://github.com/overtrue/socialite/tree/2.0.24"
+ },
+ "funding": [
+ {
+ "url": "https://www.patreon.com/overtrue",
+ "type": "patreon"
+ }
+ ],
+ "time": "2021-05-13T16:04:48+00:00"
+ },
+ {
+ "name": "overtrue/wechat",
+ "version": "4.9.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/w7corp/easywechat.git",
+ "reference": "92791f5d957269c633b9aa175f842f6006f945b1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/w7corp/easywechat/zipball/92791f5d957269c633b9aa175f842f6006f945b1",
+ "reference": "92791f5d957269c633b9aa175f842f6006f945b1",
+ "shasum": ""
+ },
+ "require": {
+ "easywechat-composer/easywechat-composer": "^1.1",
+ "ext-fileinfo": "*",
+ "ext-openssl": "*",
+ "ext-simplexml": "*",
+ "guzzlehttp/guzzle": "^6.2 || ^7.0",
+ "monolog/monolog": "^1.22 || ^2.0",
+ "overtrue/socialite": "~2.0",
+ "php": ">=7.2",
+ "pimple/pimple": "^3.0",
+ "psr/simple-cache": "^1.0",
+ "symfony/cache": "^3.3 || ^4.3 || ^5.0",
+ "symfony/event-dispatcher": "^4.3 || ^5.0",
+ "symfony/http-foundation": "^2.7 || ^3.0 || ^4.0 || ^5.0",
+ "symfony/psr-http-message-bridge": "^0.3 || ^1.0 || ^2.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^2.15",
+ "mikey179/vfsstream": "^1.6",
+ "mockery/mockery": "^1.2.3",
+ "phpstan/phpstan": "^0.12.0",
+ "phpunit/phpunit": "^7.5"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/Kernel/Support/Helpers.php",
+ "src/Kernel/Helpers.php"
+ ],
+ "psr-4": {
+ "EasyWeChat\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "overtrue",
+ "email": "anzhengchao@gmail.com"
+ }
+ ],
+ "description": "微信SDK",
+ "keywords": [
+ "easywechat",
+ "sdk",
+ "wechat",
+ "weixin",
+ "weixin-sdk"
+ ],
+ "support": {
+ "issues": "https://github.com/w7corp/easywechat/issues",
+ "source": "https://github.com/w7corp/easywechat/tree/4.9.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/overtrue",
+ "type": "github"
+ }
+ ],
+ "abandoned": "w7corp/easywechat",
+ "time": "2023-04-28T03:30:34+00:00"
+ },
+ {
+ "name": "phpoffice/phpspreadsheet",
+ "version": "1.30.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
+ "reference": "fa8257a579ec623473eabfe49731de5967306c4c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fa8257a579ec623473eabfe49731de5967306c4c",
+ "reference": "fa8257a579ec623473eabfe49731de5967306c4c",
+ "shasum": ""
+ },
+ "require": {
+ "composer/pcre": "^1||^2||^3",
+ "ext-ctype": "*",
+ "ext-dom": "*",
+ "ext-fileinfo": "*",
+ "ext-gd": "*",
+ "ext-iconv": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-simplexml": "*",
+ "ext-xml": "*",
+ "ext-xmlreader": "*",
+ "ext-xmlwriter": "*",
+ "ext-zip": "*",
+ "ext-zlib": "*",
+ "ezyang/htmlpurifier": "^4.15",
+ "maennchen/zipstream-php": "^2.1 || ^3.0",
+ "markbaker/complex": "^3.0",
+ "markbaker/matrix": "^3.0",
+ "php": ">=7.4.0 <8.5.0",
+ "psr/http-client": "^1.0",
+ "psr/http-factory": "^1.0",
+ "psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "dev-main",
+ "dompdf/dompdf": "^1.0 || ^2.0 || ^3.0",
+ "friendsofphp/php-cs-fixer": "^3.2",
+ "mitoteam/jpgraph": "^10.3",
+ "mpdf/mpdf": "^8.1.1",
+ "phpcompatibility/php-compatibility": "^9.3",
+ "phpstan/phpstan": "^1.1",
+ "phpstan/phpstan-phpunit": "^1.0",
+ "phpunit/phpunit": "^8.5 || ^9.0",
+ "squizlabs/php_codesniffer": "^3.7",
+ "tecnickcom/tcpdf": "^6.5"
+ },
+ "suggest": {
+ "dompdf/dompdf": "Option for rendering PDF with PDF Writer",
+ "ext-intl": "PHP Internationalization Functions",
+ "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
+ "mpdf/mpdf": "Option for rendering PDF with PDF Writer",
+ "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Maarten Balliauw",
+ "homepage": "https://blog.maartenballiauw.be"
+ },
+ {
+ "name": "Mark Baker",
+ "homepage": "https://markbakeruk.net"
+ },
+ {
+ "name": "Franck Lefevre",
+ "homepage": "https://rootslabs.net"
+ },
+ {
+ "name": "Erik Tilt"
+ },
+ {
+ "name": "Adrien Crivelli"
+ }
+ ],
+ "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
+ "homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
+ "keywords": [
+ "OpenXML",
+ "excel",
+ "gnumeric",
+ "ods",
+ "php",
+ "spreadsheet",
+ "xls",
+ "xlsx"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
+ "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.1"
+ },
+ "time": "2025-10-26T16:01:04+00:00"
+ },
+ {
+ "name": "phrity/net-uri",
+ "version": "1.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sirn-se/phrity-net-uri.git",
+ "reference": "3f458e0c4d1ddc0e218d7a5b9420127c63925f43"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sirn-se/phrity-net-uri/zipball/3f458e0c4d1ddc0e218d7a5b9420127c63925f43",
+ "reference": "3f458e0c4d1ddc0e218d7a5b9420127c63925f43",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 | ^8.0",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.0 | ^2.0"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.0",
+ "phpunit/phpunit": "^9.0 | ^10.0",
+ "squizlabs/php_codesniffer": "^3.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Phrity\\Net\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Sören Jensen",
+ "email": "sirn@sirn.se",
+ "homepage": "https://phrity.sirn.se"
+ }
+ ],
+ "description": "PSR-7 Uri and PSR-17 UriFactory implementation",
+ "homepage": "https://phrity.sirn.se/net-uri",
+ "keywords": [
+ "psr-17",
+ "psr-7",
+ "uri",
+ "uri factory"
+ ],
+ "support": {
+ "issues": "https://github.com/sirn-se/phrity-net-uri/issues",
+ "source": "https://github.com/sirn-se/phrity-net-uri/tree/1.3.0"
+ },
+ "time": "2023-08-21T10:33:06+00:00"
+ },
+ {
+ "name": "phrity/util-errorhandler",
+ "version": "1.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sirn-se/phrity-util-errorhandler.git",
+ "reference": "483228156e06673963902b1cc1e6bd9541ab4d5e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sirn-se/phrity-util-errorhandler/zipball/483228156e06673963902b1cc1e6bd9541ab4d5e",
+ "reference": "483228156e06673963902b1cc1e6bd9541ab4d5e",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 | ^8.0"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.0",
+ "phpunit/phpunit": "^9.0 | ^10.0 | ^11.0",
+ "squizlabs/php_codesniffer": "^3.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Phrity\\Util\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Sören Jensen",
+ "email": "sirn@sirn.se",
+ "homepage": "https://phrity.sirn.se"
+ }
+ ],
+ "description": "Inline error handler; catch and resolve errors for code block.",
+ "homepage": "https://phrity.sirn.se/util-errorhandler",
+ "keywords": [
+ "error",
+ "warning"
+ ],
+ "support": {
+ "issues": "https://github.com/sirn-se/phrity-util-errorhandler/issues",
+ "source": "https://github.com/sirn-se/phrity-util-errorhandler/tree/1.1.1"
+ },
+ "time": "2024-09-12T06:49:16+00:00"
+ },
+ {
+ "name": "pimple/pimple",
+ "version": "v3.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/silexphp/Pimple.git",
+ "reference": "a70f552d338f9266eec6606c1f0b324da5514c96"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/silexphp/Pimple/zipball/a70f552d338f9266eec6606c1f0b324da5514c96",
+ "reference": "a70f552d338f9266eec6606c1f0b324da5514c96",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "psr/container": "^1.1 || ^2.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "Pimple": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ }
+ ],
+ "description": "Pimple, a simple Dependency Injection Container",
+ "homepage": "https://pimple.symfony.com",
+ "keywords": [
+ "container",
+ "dependency injection"
+ ],
+ "support": {
+ "source": "https://github.com/silexphp/Pimple/tree/v3.6.0"
+ },
+ "time": "2025-11-12T12:31:38+00:00"
+ },
+ {
+ "name": "psr/cache",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/cache.git",
+ "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8",
+ "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Cache\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for caching libraries",
+ "keywords": [
+ "cache",
+ "psr",
+ "psr-6"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/cache/tree/master"
+ },
+ "time": "2016-08-06T20:24:11+00:00"
+ },
+ {
+ "name": "psr/container",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/container.git",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Container\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common Container Interface (PHP FIG PSR-11)",
+ "homepage": "https://github.com/php-fig/container",
+ "keywords": [
+ "PSR-11",
+ "container",
+ "container-interface",
+ "container-interop",
+ "psr"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/container/issues",
+ "source": "https://github.com/php-fig/container/tree/2.0.2"
+ },
+ "time": "2021-11-05T16:47:00+00:00"
+ },
+ {
+ "name": "psr/event-dispatcher",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/event-dispatcher.git",
+ "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0",
+ "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\EventDispatcher\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Standard interfaces for event handling.",
+ "keywords": [
+ "events",
+ "psr",
+ "psr-14"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/event-dispatcher/issues",
+ "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0"
+ },
+ "time": "2019-01-08T18:20:26+00:00"
+ },
+ {
+ "name": "psr/http-client",
+ "version": "1.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-client.git",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP clients",
+ "homepage": "https://github.com/php-fig/http-client",
+ "keywords": [
+ "http",
+ "http-client",
+ "psr",
+ "psr-18"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-client"
+ },
+ "time": "2023-09-23T14:17:50+00:00"
+ },
+ {
+ "name": "psr/http-factory",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-factory.git",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "psr",
+ "psr-17",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-factory"
+ },
+ "time": "2024-04-15T12:06:14+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
+ "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/1.1"
+ },
+ "time": "2023-04-04T09:50:52+00:00"
+ },
+ {
+ "name": "psr/log",
+ "version": "1.1.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/log.git",
+ "reference": "d49695b909c3b7628b6289db5479a1c204601f11"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
+ "reference": "d49695b909c3b7628b6289db5479a1c204601f11",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "Psr/Log/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for logging libraries",
+ "homepage": "https://github.com/php-fig/log",
+ "keywords": [
+ "log",
+ "psr",
+ "psr-3"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/log/tree/1.1.4"
+ },
+ "time": "2021-05-03T11:20:27+00:00"
+ },
+ {
+ "name": "psr/simple-cache",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/simple-cache.git",
+ "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
+ "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\SimpleCache\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interfaces for simple caching",
+ "keywords": [
+ "cache",
+ "caching",
+ "psr",
+ "psr-16",
+ "simple-cache"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/simple-cache/tree/master"
+ },
+ "time": "2017-10-23T01:57:42+00:00"
+ },
+ {
+ "name": "ralouphie/getallheaders",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ralouphie/getallheaders.git",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.1",
+ "phpunit/phpunit": "^5 || ^6.5"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/getallheaders.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ralph Khattar",
+ "email": "ralph.khattar@gmail.com"
+ }
+ ],
+ "description": "A polyfill for getallheaders.",
+ "support": {
+ "issues": "https://github.com/ralouphie/getallheaders/issues",
+ "source": "https://github.com/ralouphie/getallheaders/tree/develop"
+ },
+ "time": "2019-03-08T08:55:37+00:00"
+ },
+ {
+ "name": "symfony/cache",
+ "version": "v4.3.11",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/cache.git",
+ "reference": "8794ccf68ac341fc19311919d2287f7557bfccba"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/cache/zipball/8794ccf68ac341fc19311919d2287f7557bfccba",
+ "reference": "8794ccf68ac341fc19311919d2287f7557bfccba",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3",
+ "psr/cache": "~1.0",
+ "psr/log": "~1.0",
+ "symfony/cache-contracts": "^1.1",
+ "symfony/service-contracts": "^1.1",
+ "symfony/var-exporter": "^4.2"
+ },
+ "conflict": {
+ "doctrine/dbal": "<2.5",
+ "symfony/dependency-injection": "<3.4",
+ "symfony/var-dumper": "<3.4"
+ },
+ "provide": {
+ "psr/cache-implementation": "1.0",
+ "psr/simple-cache-implementation": "1.0",
+ "symfony/cache-implementation": "1.0"
+ },
+ "require-dev": {
+ "cache/integration-tests": "dev-master",
+ "doctrine/cache": "~1.6",
+ "doctrine/dbal": "~2.5",
+ "predis/predis": "~1.1",
+ "psr/simple-cache": "^1.0",
+ "symfony/config": "~4.2",
+ "symfony/dependency-injection": "~3.4|~4.1",
+ "symfony/var-dumper": "^4.1.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Cache\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Cache component with PSR-6, PSR-16, and tags",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "caching",
+ "psr6"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/cache/tree/v4.3.11"
+ },
+ "time": "2020-01-27T09:15:09+00:00"
+ },
+ {
+ "name": "symfony/cache-contracts",
+ "version": "v1.10.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/cache-contracts.git",
+ "reference": "a872a66e0bf7bac179c89bc96c7098bef1949f81"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/a872a66e0bf7bac179c89bc96c7098bef1949f81",
+ "reference": "a872a66e0bf7bac179c89bc96c7098bef1949f81",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1.3",
+ "psr/cache": "^1.0|^2.0|^3.0"
+ },
+ "suggest": {
+ "symfony/cache-implementation": ""
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "1.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Cache\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to caching",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/cache-contracts/tree/v1.10.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-01-02T09:41:36+00:00"
+ },
+ {
+ "name": "symfony/deprecation-contracts",
+ "version": "v2.5.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/deprecation-contracts.git",
+ "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918",
+ "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "2.5-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "function.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "A generic function and convention to trigger deprecation notices",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-25T14:11:13+00:00"
+ },
+ {
+ "name": "symfony/event-dispatcher",
+ "version": "v5.4.45",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/event-dispatcher.git",
+ "reference": "72982eb416f61003e9bb6e91f8b3213600dcf9e9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/72982eb416f61003e9bb6e91f8b3213600dcf9e9",
+ "reference": "72982eb416f61003e9bb6e91f8b3213600dcf9e9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.1|^3",
+ "symfony/event-dispatcher-contracts": "^2|^3",
+ "symfony/polyfill-php80": "^1.16"
+ },
+ "conflict": {
+ "symfony/dependency-injection": "<4.4"
+ },
+ "provide": {
+ "psr/event-dispatcher-implementation": "1.0",
+ "symfony/event-dispatcher-implementation": "2.0"
+ },
+ "require-dev": {
+ "psr/log": "^1|^2|^3",
+ "symfony/config": "^4.4|^5.0|^6.0",
+ "symfony/dependency-injection": "^4.4|^5.0|^6.0",
+ "symfony/error-handler": "^4.4|^5.0|^6.0",
+ "symfony/expression-language": "^4.4|^5.0|^6.0",
+ "symfony/http-foundation": "^4.4|^5.0|^6.0",
+ "symfony/service-contracts": "^1.1|^2|^3",
+ "symfony/stopwatch": "^4.4|^5.0|^6.0"
+ },
+ "suggest": {
+ "symfony/dependency-injection": "",
+ "symfony/http-kernel": ""
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\EventDispatcher\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.45"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-25T14:11:13+00:00"
+ },
+ {
+ "name": "symfony/event-dispatcher-contracts",
+ "version": "v2.5.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/event-dispatcher-contracts.git",
+ "reference": "e0fe3d79b516eb75126ac6fa4cbf19b79b08c99f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/e0fe3d79b516eb75126ac6fa4cbf19b79b08c99f",
+ "reference": "e0fe3d79b516eb75126ac6fa4cbf19b79b08c99f",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "psr/event-dispatcher": "^1"
+ },
+ "suggest": {
+ "symfony/event-dispatcher-implementation": ""
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "2.5-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\EventDispatcher\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to dispatching event",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.4"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-25T14:11:13+00:00"
+ },
+ {
+ "name": "symfony/http-foundation",
+ "version": "v5.4.50",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-foundation.git",
+ "reference": "1a0706e8b8041046052ea2695eb8aeee04f97609"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/1a0706e8b8041046052ea2695eb8aeee04f97609",
+ "reference": "1a0706e8b8041046052ea2695eb8aeee04f97609",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.1|^3",
+ "symfony/polyfill-mbstring": "~1.1",
+ "symfony/polyfill-php80": "^1.16"
+ },
+ "require-dev": {
+ "predis/predis": "^1.0|^2.0",
+ "symfony/cache": "^4.4|^5.0|^6.0",
+ "symfony/dependency-injection": "^5.4|^6.0",
+ "symfony/expression-language": "^4.4|^5.0|^6.0",
+ "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4",
+ "symfony/mime": "^4.4|^5.0|^6.0",
+ "symfony/rate-limiter": "^5.2|^6.0"
+ },
+ "suggest": {
+ "symfony/mime": "To use the file extension guesser"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\HttpFoundation\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Defines an object-oriented layer for the HTTP specification",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/http-foundation/tree/v5.4.50"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-11-03T12:58:48+00:00"
+ },
+ {
+ "name": "symfony/options-resolver",
+ "version": "v5.4.45",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/options-resolver.git",
+ "reference": "74e5b6f0db3e8589e6cfd5efb317a1fc2bb52fb6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/options-resolver/zipball/74e5b6f0db3e8589e6cfd5efb317a1fc2bb52fb6",
+ "reference": "74e5b6f0db3e8589e6cfd5efb317a1fc2bb52fb6",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.1|^3",
+ "symfony/polyfill-php73": "~1.0",
+ "symfony/polyfill-php80": "^1.16"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\OptionsResolver\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides an improved replacement for the array_replace PHP function",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "config",
+ "configuration",
+ "options"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/options-resolver/tree/v5.4.45"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-25T14:11:13+00:00"
+ },
+ {
+ "name": "symfony/polyfill-ctype",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
+ "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-ctype": "*"
+ },
+ "suggest": {
+ "ext-ctype": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-09T11:45:10+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-grapheme",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
+ "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70",
+ "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Grapheme\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's grapheme_* functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "grapheme",
+ "intl",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-06-27T09:58:17+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-idn",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-idn.git",
+ "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3",
+ "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2",
+ "symfony/polyfill-intl-normalizer": "^1.10"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Idn\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Laurent Bassin",
+ "email": "laurent@bassin.info"
+ },
+ {
+ "name": "Trevor Rowbotham",
+ "email": "trevor.rowbotham@pm.me"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "idn",
+ "intl",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-10T14:38:51+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-normalizer",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
+ "reference": "3833d7255cc303546435cb650316bff708a1c75c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c",
+ "reference": "3833d7255cc303546435cb650316bff708a1c75c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Normalizer\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's Normalizer class and related functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "intl",
+ "normalizer",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-09T11:45:10+00:00"
+ },
+ {
+ "name": "symfony/polyfill-mbstring",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
+ "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
+ "shasum": ""
+ },
+ "require": {
+ "ext-iconv": "*",
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-mbstring": "*"
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for the Mbstring extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "mbstring",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-12-23T08:48:59+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php73",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php73.git",
+ "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb",
+ "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php73\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php73/tree/v1.33.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-09T11:45:10+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php80",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php80.git",
+ "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
+ "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php80\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ion Bazan",
+ "email": "ion.bazan@gmail.com"
+ },
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-01-02T08:10:11+00:00"
+ },
+ {
+ "name": "symfony/property-access",
+ "version": "v5.4.45",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/property-access.git",
+ "reference": "111e7ed617509f1a9139686055d234aad6e388e0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/property-access/zipball/111e7ed617509f1a9139686055d234aad6e388e0",
+ "reference": "111e7ed617509f1a9139686055d234aad6e388e0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.1|^3",
+ "symfony/polyfill-php80": "^1.16",
+ "symfony/property-info": "^5.2|^6.0"
+ },
+ "require-dev": {
+ "symfony/cache": "^4.4|^5.0|^6.0"
+ },
+ "suggest": {
+ "psr/cache-implementation": "To cache access methods."
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\PropertyAccess\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides functions to read and write from/to an object or array using a simple string notation",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "access",
+ "array",
+ "extraction",
+ "index",
+ "injection",
+ "object",
+ "property",
+ "property-path",
+ "reflection"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/property-access/tree/v5.4.45"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-25T14:11:13+00:00"
+ },
+ {
+ "name": "symfony/property-info",
+ "version": "v5.4.48",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/property-info.git",
+ "reference": "a0396295ad585f95fccd690bc6a281e5bd303902"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/property-info/zipball/a0396295ad585f95fccd690bc6a281e5bd303902",
+ "reference": "a0396295ad585f95fccd690bc6a281e5bd303902",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.1|^3",
+ "symfony/polyfill-php80": "^1.16",
+ "symfony/string": "^5.1|^6.0"
+ },
+ "conflict": {
+ "phpdocumentor/reflection-docblock": "<3.2.2",
+ "phpdocumentor/type-resolver": "<1.4.0",
+ "symfony/dependency-injection": "<4.4"
+ },
+ "require-dev": {
+ "doctrine/annotations": "^1.10.4|^2",
+ "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
+ "phpstan/phpdoc-parser": "^1.0|^2.0",
+ "symfony/cache": "^4.4|^5.0|^6.0",
+ "symfony/dependency-injection": "^4.4|^5.0|^6.0",
+ "symfony/serializer": "^4.4|^5.0|^6.0"
+ },
+ "suggest": {
+ "phpdocumentor/reflection-docblock": "To use the PHPDoc",
+ "psr/cache-implementation": "To cache results",
+ "symfony/doctrine-bridge": "To use Doctrine metadata",
+ "symfony/serializer": "To use Serializer metadata"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\PropertyInfo\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kévin Dunglas",
+ "email": "dunglas@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Extracts information about PHP class' properties using metadata of popular sources",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "doctrine",
+ "phpdoc",
+ "property",
+ "symfony",
+ "type",
+ "validator"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/property-info/tree/v5.4.48"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-11-25T16:14:41+00:00"
+ },
+ {
+ "name": "symfony/psr-http-message-bridge",
+ "version": "v2.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/psr-http-message-bridge.git",
+ "reference": "581ca6067eb62640de5ff08ee1ba6850a0ee472e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/581ca6067eb62640de5ff08ee1ba6850a0ee472e",
+ "reference": "581ca6067eb62640de5ff08ee1ba6850a0ee472e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "psr/http-message": "^1.0 || ^2.0",
+ "symfony/deprecation-contracts": "^2.5 || ^3.0",
+ "symfony/http-foundation": "^5.4 || ^6.0"
+ },
+ "require-dev": {
+ "nyholm/psr7": "^1.1",
+ "psr/log": "^1.1 || ^2 || ^3",
+ "symfony/browser-kit": "^5.4 || ^6.0",
+ "symfony/config": "^5.4 || ^6.0",
+ "symfony/event-dispatcher": "^5.4 || ^6.0",
+ "symfony/framework-bundle": "^5.4 || ^6.0",
+ "symfony/http-kernel": "^5.4 || ^6.0",
+ "symfony/phpunit-bridge": "^6.2"
+ },
+ "suggest": {
+ "nyholm/psr7": "For a super lightweight PSR-7/17 implementation"
+ },
+ "type": "symfony-bridge",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "2.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Bridge\\PsrHttpMessage\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "http://symfony.com/contributors"
+ }
+ ],
+ "description": "PSR HTTP message bridge",
+ "homepage": "http://symfony.com",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr-17",
+ "psr-7"
+ ],
+ "support": {
+ "issues": "https://github.com/symfony/psr-http-message-bridge/issues",
+ "source": "https://github.com/symfony/psr-http-message-bridge/tree/v2.3.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-07-26T11:53:26+00:00"
+ },
+ {
+ "name": "symfony/service-contracts",
+ "version": "v1.1.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/service-contracts.git",
+ "reference": "191afdcb5804db960d26d8566b7e9a2843cab3a0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/191afdcb5804db960d26d8566b7e9a2843cab3a0",
+ "reference": "191afdcb5804db960d26d8566b7e9a2843cab3a0",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3"
+ },
+ "suggest": {
+ "psr/container": "",
+ "symfony/service-implementation": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Service\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to writing services",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/service-contracts/tree/v1.1.2"
+ },
+ "time": "2019-05-28T07:50:59+00:00"
+ },
+ {
+ "name": "symfony/string",
+ "version": "v5.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/string.git",
+ "reference": "136ca7d72f72b599f2631aca474a4f8e26719799"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/string/zipball/136ca7d72f72b599f2631aca474a4f8e26719799",
+ "reference": "136ca7d72f72b599f2631aca474a4f8e26719799",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-intl-grapheme": "~1.0",
+ "symfony/polyfill-intl-normalizer": "~1.0",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/polyfill-php80": "~1.15"
+ },
+ "conflict": {
+ "symfony/translation-contracts": ">=3.0"
+ },
+ "require-dev": {
+ "symfony/error-handler": "^4.4|^5.0|^6.0",
+ "symfony/http-client": "^4.4|^5.0|^6.0",
+ "symfony/translation-contracts": "^1.1|^2",
+ "symfony/var-exporter": "^4.4|^5.0|^6.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/functions.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\String\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "grapheme",
+ "i18n",
+ "string",
+ "unicode",
+ "utf-8",
+ "utf8"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/string/tree/v5.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-11-10T20:33:58+00:00"
+ },
+ {
+ "name": "symfony/var-exporter",
+ "version": "v4.4.43",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/var-exporter.git",
+ "reference": "4a7a3a3d55c471d396e6d28011368b7b83cb518b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/var-exporter/zipball/4a7a3a3d55c471d396e6d28011368b7b83cb518b",
+ "reference": "4a7a3a3d55c471d396e6d28011368b7b83cb518b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1.3",
+ "symfony/polyfill-php80": "^1.16"
+ },
+ "require-dev": {
+ "symfony/var-dumper": "^4.4.9|^5.0.9"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\VarExporter\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Allows exporting any serializable PHP data structure to plain PHP code",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "clone",
+ "construct",
+ "export",
+ "hydrate",
+ "instantiate",
+ "serialize"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/var-exporter/tree/v4.4.43"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-05-27T11:44:32+00:00"
+ },
+ {
+ "name": "textalk/websocket",
+ "version": "1.6.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Textalk/websocket-php.git",
+ "reference": "67de79745b1a357caf812bfc44e0abf481cee012"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Textalk/websocket-php/zipball/67de79745b1a357caf812bfc44e0abf481cee012",
+ "reference": "67de79745b1a357caf812bfc44e0abf481cee012",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 | ^8.0",
+ "phrity/net-uri": "^1.0",
+ "phrity/util-errorhandler": "^1.0",
+ "psr/http-message": "^1.0",
+ "psr/log": "^1.0 | ^2.0 | ^3.0"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.0",
+ "phpunit/phpunit": "^9.0",
+ "squizlabs/php_codesniffer": "^3.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "WebSocket\\": "lib"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "ISC"
+ ],
+ "authors": [
+ {
+ "name": "Fredrik Liljegren"
+ },
+ {
+ "name": "Sören Jensen"
+ }
+ ],
+ "description": "WebSocket client and server",
+ "support": {
+ "issues": "https://github.com/Textalk/websocket-php/issues",
+ "source": "https://github.com/Textalk/websocket-php/tree/1.6.3"
+ },
+ "time": "2022-11-07T18:59:33+00:00"
+ },
+ {
+ "name": "topthink/framework",
+ "version": "v5.1.12",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/top-think/framework.git",
+ "reference": "f879603ee321af8fde56d8855445cf98bc81b042"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/top-think/framework/zipball/f879603ee321af8fde56d8855445cf98bc81b042",
+ "reference": "f879603ee321af8fde56d8855445cf98bc81b042",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6.0",
+ "topthink/think-installer": "~1.0"
+ },
+ "require-dev": {
+ "johnkary/phpunit-speedtrap": "^1.0",
+ "mikey179/vfsstream": "~1.6",
+ "phpdocumentor/reflection-docblock": "^2.0",
+ "phploc/phploc": "2.*",
+ "phpunit/phpunit": "^5.0|^6.0",
+ "sebastian/phpcpd": "2.*",
+ "squizlabs/php_codesniffer": "2.*"
+ },
+ "type": "think-framework",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "liu21st",
+ "email": "liu21st@gmail.com"
+ },
+ {
+ "name": "yunwuxin",
+ "email": "448901948@qq.com"
+ }
+ ],
+ "description": "the new thinkphp framework",
+ "homepage": "http://thinkphp.cn/",
+ "keywords": [
+ "framework",
+ "orm",
+ "thinkphp"
+ ],
+ "support": {
+ "issues": "https://github.com/top-think/framework/issues",
+ "source": "https://github.com/top-think/framework/tree/5.1"
+ },
+ "time": "2018-04-26T03:46:41+00:00"
+ },
+ {
+ "name": "topthink/think-captcha",
+ "version": "v2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/top-think/think-captcha.git",
+ "reference": "54c8a51552f99ff9ea89ea9c272383a8f738ceee"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/top-think/think-captcha/zipball/54c8a51552f99ff9ea89ea9c272383a8f738ceee",
+ "reference": "54c8a51552f99ff9ea89ea9c272383a8f738ceee",
+ "shasum": ""
+ },
+ "require": {
+ "topthink/framework": "5.1.*"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/helper.php"
+ ],
+ "psr-4": {
+ "think\\captcha\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "yunwuxin",
+ "email": "448901948@qq.com"
+ }
+ ],
+ "description": "captcha package for thinkphp5",
+ "support": {
+ "issues": "https://github.com/top-think/think-captcha/issues",
+ "source": "https://github.com/top-think/think-captcha/tree/2.0"
+ },
+ "time": "2017-12-31T16:37:49+00:00"
+ },
+ {
+ "name": "topthink/think-helper",
+ "version": "v3.1.11",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/top-think/think-helper.git",
+ "reference": "1d6ada9b9f3130046bf6922fe1bd159c8d88a33c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/top-think/think-helper/zipball/1d6ada9b9f3130046bf6922fe1bd159c8d88a33c",
+ "reference": "1d6ada9b9f3130046bf6922fe1bd159c8d88a33c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/helper.php"
+ ],
+ "psr-4": {
+ "think\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "yunwuxin",
+ "email": "448901948@qq.com"
+ }
+ ],
+ "description": "The ThinkPHP6 Helper Package",
+ "support": {
+ "issues": "https://github.com/top-think/think-helper/issues",
+ "source": "https://github.com/top-think/think-helper/tree/v3.1.11"
+ },
+ "time": "2025-04-07T06:55:59+00:00"
+ },
+ {
+ "name": "topthink/think-image",
+ "version": "v1.0.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/top-think/think-image.git",
+ "reference": "d1d748cbb2fe2f29fca6138cf96cb8b5113892f1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/top-think/think-image/zipball/d1d748cbb2fe2f29fca6138cf96cb8b5113892f1",
+ "reference": "d1d748cbb2fe2f29fca6138cf96cb8b5113892f1",
+ "shasum": ""
+ },
+ "require": {
+ "ext-gd": "*"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "4.8.*",
+ "topthink/framework": "^5.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "think\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "yunwuxin",
+ "email": "448901948@qq.com"
+ }
+ ],
+ "description": "The ThinkPHP5 Image Package",
+ "support": {
+ "issues": "https://github.com/top-think/think-image/issues",
+ "source": "https://github.com/top-think/think-image/tree/v1.0.8"
+ },
+ "time": "2024-08-07T10:06:35+00:00"
+ },
+ {
+ "name": "topthink/think-installer",
+ "version": "v1.0.14",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/top-think/think-installer.git",
+ "reference": "eae1740ac264a55c06134b6685dfb9f837d004d1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/top-think/think-installer/zipball/eae1740ac264a55c06134b6685dfb9f837d004d1",
+ "reference": "eae1740ac264a55c06134b6685dfb9f837d004d1",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^1.0||^2.0"
+ },
+ "require-dev": {
+ "composer/composer": "^1.0||^2.0"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "think\\composer\\Plugin"
+ },
+ "autoload": {
+ "psr-4": {
+ "think\\composer\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "yunwuxin",
+ "email": "448901948@qq.com"
+ }
+ ],
+ "support": {
+ "issues": "https://github.com/top-think/think-installer/issues",
+ "source": "https://github.com/top-think/think-installer/tree/v1.0.14"
+ },
+ "time": "2021-03-25T08:34:02+00:00"
+ },
+ {
+ "name": "topthink/think-queue",
+ "version": "v2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/top-think/think-queue.git",
+ "reference": "465320c9cb7811df22d4ff8f29f58ead7d104348"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/top-think/think-queue/zipball/465320c9cb7811df22d4ff8f29f58ead7d104348",
+ "reference": "465320c9cb7811df22d4ff8f29f58ead7d104348",
+ "shasum": ""
+ },
+ "require": {
+ "topthink/think-helper": ">=1.0.4",
+ "topthink/think-installer": ">=1.0.10"
+ },
+ "type": "think-extend",
+ "extra": {
+ "think-config": {
+ "queue": "src/config.php"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/common.php"
+ ],
+ "psr-4": {
+ "think\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "yunwuxin",
+ "email": "448901948@qq.com"
+ }
+ ],
+ "description": "The ThinkPHP5 Queue Package",
+ "support": {
+ "issues": "https://github.com/top-think/think-queue/issues",
+ "source": "https://github.com/top-think/think-queue/tree/master"
+ },
+ "time": "2018-05-04T05:29:53+00:00"
+ },
+ {
+ "name": "topthink/think-worker",
+ "version": "v2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/top-think/think-worker.git",
+ "reference": "7b7a64b2911cc11298b11677508f1a1936a2fb70"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/top-think/think-worker/zipball/7b7a64b2911cc11298b11677508f1a1936a2fb70",
+ "reference": "7b7a64b2911cc11298b11677508f1a1936a2fb70",
+ "shasum": ""
+ },
+ "require": {
+ "topthink/framework": "5.1.*",
+ "workerman/workerman": "^3.3.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/config.php"
+ ],
+ "psr-4": {
+ "think\\worker\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "liu21st",
+ "email": "liu21st@gmail.com"
+ }
+ ],
+ "description": "workerman extend for thinkphp5",
+ "support": {
+ "issues": "https://github.com/top-think/think-worker/issues",
+ "source": "https://github.com/top-think/think-worker/tree/2.0"
+ },
+ "time": "2018-06-28T06:04:53+00:00"
+ },
+ {
+ "name": "workerman/gateway-worker",
+ "version": "v3.0.22",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/walkor/GatewayWorker.git",
+ "reference": "a615036c482d11f68b693998575e804752ef9068"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/walkor/GatewayWorker/zipball/a615036c482d11f68b693998575e804752ef9068",
+ "reference": "a615036c482d11f68b693998575e804752ef9068",
+ "shasum": ""
+ },
+ "require": {
+ "workerman/workerman": ">=3.5.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "GatewayWorker\\": "./src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "homepage": "http://www.workerman.net",
+ "keywords": [
+ "communication",
+ "distributed"
+ ],
+ "support": {
+ "issues": "https://github.com/walkor/GatewayWorker/issues",
+ "source": "https://github.com/walkor/GatewayWorker/tree/v3.0.22"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/walkor",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://www.patreon.com/walkor",
+ "type": "patreon"
+ }
+ ],
+ "time": "2021-12-23T13:13:09+00:00"
+ },
+ {
+ "name": "workerman/workerman",
+ "version": "v3.5.35",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/walkor/workerman.git",
+ "reference": "3cc0adae51ba36db38b11e7996c64250d356dbe7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/walkor/workerman/zipball/3cc0adae51ba36db38b11e7996c64250d356dbe7",
+ "reference": "3cc0adae51ba36db38b11e7996c64250d356dbe7",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3||^7.0"
+ },
+ "suggest": {
+ "ext-event": "For better performance. "
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Workerman\\": "./"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "walkor",
+ "email": "walkor@workerman.net",
+ "homepage": "http://www.workerman.net",
+ "role": "Developer"
+ }
+ ],
+ "description": "An asynchronous event driven PHP framework for easily building fast, scalable network applications.",
+ "homepage": "http://www.workerman.net",
+ "keywords": [
+ "asynchronous",
+ "event-loop"
+ ],
+ "support": {
+ "email": "walkor@workerman.net",
+ "forum": "http://wenda.workerman.net/",
+ "issues": "https://github.com/walkor/workerman/issues",
+ "source": "https://github.com/walkor/workerman",
+ "wiki": "http://doc.workerman.net/"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/workerman",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://www.patreon.com/walkor",
+ "type": "patreon"
+ }
+ ],
+ "time": "2023-09-13T14:30:13+00:00"
+ }
+ ],
+ "packages-dev": [
+ {
+ "name": "symfony/var-dumper",
+ "version": "v3.4.47",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/var-dumper.git",
+ "reference": "0719f6cf4633a38b2c1585140998579ce23b4b7d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0719f6cf4633a38b2c1585140998579ce23b4b7d",
+ "reference": "0719f6cf4633a38b2c1585140998579ce23b4b7d",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.5.9|>=7.0.8",
+ "symfony/polyfill-mbstring": "~1.0"
+ },
+ "conflict": {
+ "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0"
+ },
+ "require-dev": {
+ "ext-iconv": "*",
+ "twig/twig": "~1.34|~2.4"
+ },
+ "suggest": {
+ "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).",
+ "ext-intl": "To show region name in time zone dump",
+ "ext-symfony_debug": ""
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/functions/dump.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\VarDumper\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony mechanism for exploring and dumping PHP variables",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "debug",
+ "dump"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/var-dumper/tree/v3.4.47"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-10-24T10:57:07+00:00"
+ },
+ {
+ "name": "topthink/think-migration",
+ "version": "v2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/top-think/think-migration.git",
+ "reference": "70c89850ca29c2eab988c7c3475d1d5331901bb8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/top-think/think-migration/zipball/70c89850ca29c2eab988c7c3475d1d5331901bb8",
+ "reference": "70c89850ca29c2eab988c7c3475d1d5331901bb8",
+ "shasum": ""
+ },
+ "require": {
+ "topthink/framework": "5.1.*"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/config.php"
+ ],
+ "psr-4": {
+ "Phinx\\": "phinx/src/Phinx",
+ "think\\migration\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "yunwuxin",
+ "email": "448901948@qq.com"
+ }
+ ],
+ "support": {
+ "issues": "https://github.com/top-think/think-migration/issues",
+ "source": "https://github.com/top-think/think-migration/tree/2.0"
+ },
+ "time": "2017-12-31T16:32:22+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "dev",
+ "stability-flags": [],
+ "prefer-stable": true,
+ "prefer-lowest": false,
+ "platform": {
+ "php": ">=5.6.0"
+ },
+ "platform-dev": [],
+ "plugin-api-version": "2.3.0"
+}
diff --git a/config/app.php b/config/app.php
new file mode 100644
index 0000000..78410a0
--- /dev/null
+++ b/config/app.php
@@ -0,0 +1,146 @@
+
+// +----------------------------------------------------------------------
+
+// +----------------------------------------------------------------------
+// | 应用设置
+// +----------------------------------------------------------------------
+use think\facade\Env;
+return [
+ // 应用名称
+ 'app_name' => '',
+ // 应用地址
+ 'app_host' => '',
+ // 应用调试模式
+ 'app_debug' => Env::get('app.debug', false),
+ // 应用Trace
+ 'app_trace' => Env::get('app.trace', false),
+ // 是否支持多模块
+ 'app_multi_module' => true,
+ // 入口自动绑定模块
+ 'auto_bind_module' => false,
+ // 注册的根命名空间
+ 'root_namespace' => [],
+ // 默认输出类型
+ 'default_return_type' => 'html',
+ // 默认AJAX 数据返回格式,可选json xml ...
+ 'default_ajax_return' => 'json',
+ // 默认JSONP格式返回的处理方法
+ 'default_jsonp_handler' => 'jsonpReturn',
+ // 默认JSONP处理方法
+ 'var_jsonp_handler' => 'callback',
+ // 默认时区
+ 'default_timezone' => 'Asia/Shanghai',
+ // 是否开启多语言
+ 'lang_switch_on' => false,
+ // 默认全局过滤方法 用逗号分隔多个
+ 'default_filter' => '',
+ // 默认语言
+ 'default_lang' => 'zh-cn',
+ // 应用类库后缀
+ 'class_suffix' => false,
+ // 控制器类后缀
+ 'controller_suffix' => false,
+
+ // +----------------------------------------------------------------------
+ // | 模块设置
+ // +----------------------------------------------------------------------
+
+ // 默认模块名
+ 'default_module' => 'index',
+ // 禁止访问模块
+ 'deny_module_list' => [],
+ // 默认控制器名
+ 'default_controller' => 'Index',
+ // 默认操作名
+ 'default_action' => 'index',
+ // 默认验证器
+ 'default_validate' => '',
+ // 默认的空模块名
+ 'empty_module' => '',
+ // 默认的空控制器名
+ 'empty_controller' => 'Error',
+ // 操作方法前缀
+ 'use_action_prefix' => false,
+ // 操作方法后缀
+ 'action_suffix' => '',
+ // 自动搜索控制器
+ 'controller_auto_search' => false,
+
+ // +----------------------------------------------------------------------
+ // | URL设置
+ // +----------------------------------------------------------------------
+
+ // PATHINFO变量名 用于兼容模式
+ 'var_pathinfo' => 's',
+ // 兼容PATH_INFO获取
+ 'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'],
+ // pathinfo分隔符
+ 'pathinfo_depr' => '/',
+ // HTTPS代理标识
+ 'https_agent_name' => '',
+ // IP代理获取标识
+ 'http_agent_ip' => 'X-REAL-IP',
+ // URL伪静态后缀
+ 'url_html_suffix' => 'html',
+ // URL普通方式参数 用于自动生成
+ 'url_common_param' => false,
+ // URL参数方式 0 按名称成对解析 1 按顺序解析
+ 'url_param_type' => 0,
+ // 是否开启路由延迟解析
+ 'url_lazy_route' => false,
+ // 是否强制使用路由
+ 'url_route_must' => false,
+ // 合并路由规则
+ 'route_rule_merge' => false,
+ // 路由是否完全匹配
+ 'route_complete_match' => true,
+ // 使用注解路由
+ 'route_annotation' => false,
+ // 域名根,如thinkphp.cn
+ 'url_domain_root' => '',
+ // 是否自动转换URL中的控制器和操作名
+ 'url_convert' => true,
+ // 默认的访问控制器层
+ 'url_controller_layer' => 'controller',
+ // 表单请求类型伪装变量
+ 'var_method' => '_method',
+ // 表单ajax伪装变量
+ 'var_ajax' => '_ajax',
+ // 表单pjax伪装变量
+ 'var_pjax' => '_pjax',
+ // 是否开启请求缓存 true自动缓存 支持设置请求缓存规则
+ 'request_cache' => false,
+ // 请求缓存有效期
+ 'request_cache_expire' => null,
+ // 全局请求缓存排除规则
+ 'request_cache_except' => [],
+ // 是否开启路由缓存
+ 'route_check_cache' => false,
+ // 路由缓存的Key自定义设置(闭包),默认为当前URL和请求类型的md5
+ 'route_check_cache_key' => '',
+ // 路由缓存类型及参数
+ 'route_cache_option' => [],
+
+ // 默认跳转页面对应的模板文件
+ 'dispatch_success_tmpl' => Env::get('think_path') . 'tpl/dispatch_jump.tpl',
+ 'dispatch_error_tmpl' => Env::get('think_path') . 'tpl/dispatch_jump.tpl',
+
+ // 异常页面的模板文件
+ 'exception_tmpl' => Env::get('think_path') . 'tpl/think_exception.tpl',
+
+ // 错误显示信息,非调试模式有效
+ 'error_message' => '页面错误!请稍后再试~',
+ // 显示错误信息
+ 'show_error_msg' => false,
+ // 异常处理handle类 留空使用 \think\exception\Handle
+ 'exception_handle' => '',
+
+];
diff --git a/config/cache.php b/config/cache.php
new file mode 100644
index 0000000..985dbb1
--- /dev/null
+++ b/config/cache.php
@@ -0,0 +1,25 @@
+
+// +----------------------------------------------------------------------
+
+// +----------------------------------------------------------------------
+// | 缓存设置
+// +----------------------------------------------------------------------
+
+return [
+ // 驱动方式
+ 'type' => 'File',
+ // 缓存保存目录
+ 'path' => '',
+ // 缓存前缀
+ 'prefix' => '',
+ // 缓存有效期 0表示永久缓存
+ 'expire' => 0,
+];
diff --git a/config/config.php b/config/config.php
new file mode 100644
index 0000000..442e4ae
--- /dev/null
+++ b/config/config.php
@@ -0,0 +1,5 @@
+ 'http://192.168.3.75:8081'
+];
diff --git a/config/console.php b/config/console.php
new file mode 100644
index 0000000..a7fabca
--- /dev/null
+++ b/config/console.php
@@ -0,0 +1,20 @@
+
+// +----------------------------------------------------------------------
+
+// +----------------------------------------------------------------------
+// | 控制台配置
+// +----------------------------------------------------------------------
+return [
+ 'name' => 'Think Console',
+ 'version' => '0.1',
+ 'user' => null,
+ 'auto_path' => env('app_path') . 'command' . DIRECTORY_SEPARATOR,
+];
diff --git a/config/cookie.php b/config/cookie.php
new file mode 100644
index 0000000..ce3a2f5
--- /dev/null
+++ b/config/cookie.php
@@ -0,0 +1,32 @@
+
+// +----------------------------------------------------------------------
+
+// +----------------------------------------------------------------------
+// | Cookie设置
+// +----------------------------------------------------------------------
+return [
+ // cookie 名称前缀
+ 'prefix' => '',
+ // cookie 保存时间
+ 'expire' => 0,
+ // cookie 保存路径
+ 'path' => '/',
+ // cookie 有效域名
+ 'domain' => '',
+ // cookie 启用安全传输
+ 'secure' => false,
+ // httponly设置
+ 'httponly' => '',
+ // 是否使用 setcookie
+ 'setcookie' => true,
+ // 跨站需要
+ 'SameSite' => 'None',
+];
diff --git a/config/cookies.php b/config/cookies.php
new file mode 100644
index 0000000..9705b35
--- /dev/null
+++ b/config/cookies.php
@@ -0,0 +1,13 @@
+ [
+ 'cna=bjINIOOLP2QBASQOA6MRoWqz',
+ 'xlly_s=1',
+ '_samesite_flag_=true',
+ 't=9aa5a7ca9fc23837b5f62366fb0e3c57',
+ '_tb_token_=e834030feb9b3',
+ 'mtop_partitioned_detect=1',
+ 'tfstk=g0cKNh9DFFQLM2KvYXJgruGcYcYMId0e-DufZuqhFcntlqS3FXVn2Tnnz02WYWc-XD0jKJc-z_USP08UqeJi82PzNnxDoI0E8l7Ra4tgNUg_ZzQ98mhK82PP7NjWnAgF2BFTQWNSV5w_zrP5F76BWla4f6a5Ozs6Wlz7Nyw7FR6_orW7PkiS5FUzfuN7d0NswMOzfMZ5wxUi5afPUl15NfULCd0Qfs4K6yeac2GdN_6zJJEjRlK42WH4eDeKsBI4JAgxSynyg6PQeYG31Xt6N5gqIDatVnB8fxhEw-cpDtrITuoTnvKA9mnUP4USvI7YfX3-6JkGos3THfG3TAKAioMZPbes8nB4SYgmtYnHb_ZnHViTnfjGiSMqXbebGgJKijQhNiqYr9T9WTWzdPRiFilYSSQ0zPEDJ3WPURxaWoY9cTWzdPzTmFVNUTyMb'
+ ],
+];
\ No newline at end of file
diff --git a/config/database.php b/config/database.php
new file mode 100644
index 0000000..3666761
--- /dev/null
+++ b/config/database.php
@@ -0,0 +1,64 @@
+
+// +----------------------------------------------------------------------
+
+use think\facade\Env;
+return [
+ // 数据库类型
+ 'type' => Env::get('database.type', 'mysql'),
+ // 服务器地址
+ 'hostname' => Env::get('database.hostname', '127.0.0.1'),
+ // 数据库名
+ 'database' => Env::get('database.database', 'database'),
+ // 用户名
+ 'username' => Env::get('database.username', 'root'),
+ // 密码
+ 'password' => Env::get('database.password', 'root'),
+ // 端口
+ 'hostport' => Env::get('database.hostport', '3306'),
+ // 连接dsn
+ 'dsn' => '',
+ // 数据库连接参数
+ 'params' => [],
+ // 数据库编码默认采用utf8
+ 'charset' => env('database.charset', 'utf8mb4'),
+ // 数据库表前缀
+ 'prefix' => Env::get('database.prefix', ''),
+ // 数据库调试模式
+ 'debug' => env('database.debug', true),
+ // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
+ 'deploy' => 0,
+ // 数据库读写是否分离 主从式有效
+ 'rw_separate' => false,
+ // 读写分离后 主服务器数量
+ 'master_num' => 1,
+ // 指定从服务器序号
+ 'slave_no' => '',
+ // 自动读取主库数据
+ 'read_master' => false,
+ // 是否严格检查字段是否存在
+ 'fields_strict' => true,
+ // 数据集返回类型
+ 'resultset_type' => 'array',
+ // 自动写入时间戳字段
+ 'auto_timestamp' => true,
+ // 时间字段取出后的默认时间格式
+ 'datetime_format' => 'Y-m-d H:i:s',
+ // 是否需要进行SQL性能分析
+ 'sql_explain' => false,
+ // Builder类
+ 'builder' => '',
+ // Query类
+ 'query' => '\\think\\db\\Query',
+ // 是否需要断线重连
+ 'break_reconnect' => true,
+ // 断线标识字符串
+ 'break_match_str' => [],
+];
diff --git a/config/gateway_worker.php b/config/gateway_worker.php
new file mode 100644
index 0000000..e0c6731
--- /dev/null
+++ b/config/gateway_worker.php
@@ -0,0 +1,46 @@
+
+// +----------------------------------------------------------------------
+// +----------------------------------------------------------------------
+// | Workerman设置 仅对 php think worker:gateway 指令有效
+// +----------------------------------------------------------------------
+return [
+ // 扩展自身需要的配置
+ 'protocol' => 'websocket', // 协议 支持 tcp udp unix http websocket text
+ 'host' => '0.0.0.0', // 监听地址
+ 'port' => 2348, // 监听端口
+ 'socket' => '', // 完整监听地址
+ 'context' => [], // socket 上下文选项
+ 'register_deploy' => true, // 是否需要部署register
+ 'businessWorker_deploy' => true, // 是否需要部署businessWorker
+ 'gateway_deploy' => true, // 是否需要部署gateway
+
+ // Register配置
+ 'registerAddress' => '127.0.0.1:1236',
+
+ // Gateway配置
+ 'name' => 'thinkphp',
+ 'count' => 1,
+ 'lanIp' => '127.0.0.1',
+ 'startPort' => 2000,
+ 'daemonize' => false,
+ 'pingInterval' => 30,
+ 'pingNotResponseLimit' => 0,
+ 'pingData' => '{"type":"ping"}',
+
+ // BusinsessWorker配置
+ 'businessWorker' => [
+ 'name' => 'BusinessWorker',
+ 'count' => 1,
+ 'eventHandler' => '\app\common\socket\Events',
+ ],
+
+ 'qdKey' => 'N2WhAkJ1pgDBM$Bq',
+];
diff --git a/config/log.php b/config/log.php
new file mode 100644
index 0000000..b3d87b4
--- /dev/null
+++ b/config/log.php
@@ -0,0 +1,30 @@
+
+// +----------------------------------------------------------------------
+
+// +----------------------------------------------------------------------
+// | 日志设置
+// +----------------------------------------------------------------------
+return [
+ // 日志记录方式,内置 file socket 支持扩展
+ 'type' => 'File',
+ // 日志保存目录
+ 'path' => '',
+ // 日志记录级别
+ 'level' => [],
+ // 单文件日志写入
+ 'single' => false,
+ // 独立日志级别
+ 'apart_level' => [],
+ // 最大日志文件数量
+ 'max_files' => 0,
+ // 是否关闭日志写入
+ 'close' => false,
+];
diff --git a/config/middleware.php b/config/middleware.php
new file mode 100644
index 0000000..b221b8a
--- /dev/null
+++ b/config/middleware.php
@@ -0,0 +1,34 @@
+
+// +----------------------------------------------------------------------
+
+// +----------------------------------------------------------------------
+// | 中间件配置
+// +----------------------------------------------------------------------
+return [
+ // 默认中间件命名空间
+ 'default_namespace' => 'app\\common\\middleware\\',
+
+ // 优先级设置,此数组中的中间件会按照数组中的顺序优先执行
+ 'priority' => [
+ 'app\\common\\middleware\\AllowCrossDomain'
+ ],
+
+ // 全局中间件
+ 'alias' => [
+ 'cors' => 'app\\common\\middleware\\AllowCrossDomain',
+ 'jwt' => 'app\\http\\middleware\\Jwt'
+ ],
+
+ // 应用中间件
+ 'app' => [
+ 'cors'
+ ],
+];
diff --git a/config/queue.php b/config/queue.php
new file mode 100644
index 0000000..777f844
--- /dev/null
+++ b/config/queue.php
@@ -0,0 +1,33 @@
+ 'redis',
+ 'connections' => [
+ 'sync' => [
+ 'type' => 'sync',
+ ],
+ 'database' => [
+ 'type' => 'database',
+ 'queue' => 'default',
+ 'table' => 'jobs',
+ ],
+ 'redis' => [
+ 'type' => 'redis',
+ 'queue' => 'default',
+ 'host' => '127.0.0.1',
+ 'port' => 6379,
+ 'password' => '',
+ 'select' => 0,
+ 'timeout' => 0,
+ 'persistent' => false,
+ ],
+ ],
+ 'failed' => [
+ 'type' => 'redis',
+ 'table' => 'failed_jobs',
+ ],
+ 'failed_delay' => 30, // 失败重试延迟时间(秒)
+];
\ No newline at end of file
diff --git a/config/robot.php b/config/robot.php
new file mode 100644
index 0000000..ffacd6b
--- /dev/null
+++ b/config/robot.php
@@ -0,0 +1,7 @@
+ 'AKLTNzNiMzU2ZWMwMDYwNDNiZWJkMjFjOTFhYzFjY2JlZGU',
+ 'secretKey' => 'TXpjeVlqSTJNRFpoTUdZd05EY3lPRGsxWTJFMk0yRmpaalkyTWpFd05tUQ==',
+ 'model' => 'ep-20240522132057-rp6t4',
+];
\ No newline at end of file
diff --git a/config/session.php b/config/session.php
new file mode 100644
index 0000000..1d7b6c6
--- /dev/null
+++ b/config/session.php
@@ -0,0 +1,26 @@
+
+// +----------------------------------------------------------------------
+
+// +----------------------------------------------------------------------
+// | 会话设置
+// +----------------------------------------------------------------------
+
+return [
+ 'id' => '',
+ // SESSION_ID的提交变量,解决flash上传跨域
+ 'var_session_id' => '',
+ // SESSION 前缀
+ 'prefix' => 'think',
+ // 驱动方式 支持redis memcache memcached
+ 'type' => '',
+ // 是否自动开启 SESSION
+ 'auto_start' => true,
+];
diff --git a/config/task_scheduler.php b/config/task_scheduler.php
new file mode 100644
index 0000000..7fa7ea3
--- /dev/null
+++ b/config/task_scheduler.php
@@ -0,0 +1,301 @@
+ [
+ // 'command' => '命令名称', // 必填:执行的 ThinkPHP 命令(见 application/command.php)
+ // 'schedule' => 'cron表达式', // 必填:cron 表达式,如 '*/5 * * * *' 表示每5分钟
+ // 'options' => ['--option=value'], // 可选:命令参数(原来 crontab 里的 --xxx=yyy)
+ // 'enabled' => true, // 可选:是否启用,默认 true
+ // 'max_concurrent'=> 1, // 可选:单任务最大并发数(目前由调度器统一控制,可预留)
+ // 'timeout' => 3600, // 可选:超时时间(秒),默认 3600
+ // 'log_file' => 'custom.log', // 可选:日志文件名,默认使用任务标识
+ // ]
+
+ // ===========================
+ // 高频任务(每分钟或更频繁)
+ // ===========================
+
+ // 同步微信好友列表(未删除好友),用于保持系统中好友数据实时更新
+ 'wechat_friends_active' => [
+ 'command' => 'wechatFriends:list',
+ 'schedule' => '*/1 * * * *', // 每1分钟
+ 'options' => ['--isDel=0'],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'crontab_wechatFriends_active.log',
+ ],
+
+ // 拉取“添加好友任务”列表,驱动自动加好友的任务队列
+ 'friend_task' => [
+ 'command' => 'friendTask:list',
+ 'schedule' => '*/1 * * * *', // 每1分钟
+ 'options' => [],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'crontab_friendTask.log',
+ ],
+
+ // 同步微信好友私聊消息列表,写入消息表,供客服工作台使用
+ 'message_friends' => [
+ 'command' => 'message:friendsList',
+ 'schedule' => '*/1 * * * *', // 每1分钟
+ 'options' => [],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'crontab_messageFriends.log',
+ ],
+
+ // 同步微信群聊消息列表,写入消息表,供群聊记录与风控分析
+ 'message_chatroom' => [
+ 'command' => 'message:chatroomList',
+ 'schedule' => '*/1 * * * *', // 每1分钟
+ 'options' => [],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'crontab_messageChatroom.log',
+ ],
+
+ // 客服端消息提醒任务,负责给在线客服推送新消息通知
+ 'kf_notice' => [
+ 'command' => 'kf:notice',
+ 'schedule' => '*/1 * * * *', // 每1分钟
+ 'options' => [],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'kf_notice.log',
+ ],
+
+ // ===========================
+ // 中频任务(每 2-5 分钟)
+ // ===========================
+
+ // 同步微信设备列表(未删除设备),用于设备管理与监控
+ 'device_active' => [
+ 'command' => 'device:list',
+ 'schedule' => '*/5 * * * *', // 每5分钟
+ 'options' => ['--isDel=0'],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'crontab_device_active.log',
+ ],
+
+ // 同步微信群聊列表(未删除群),用于群管理与后续任务分配
+ 'wechat_chatroom_active' => [
+ 'command' => 'wechatChatroom:list',
+ 'schedule' => '*/5 * * * *', // 每5分钟
+ 'options' => ['--isDel=0'],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'crontab_wechatChatroom_active.log',
+ ],
+
+ // 同步微信群成员列表(群好友),维持群成员明细数据
+ 'group_friends' => [
+ 'command' => 'groupFriends:list',
+ 'schedule' => '*/5 * * * *', // 每5分钟
+ 'options' => [],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'crontab_groupFriends.log',
+ ],
+
+ // 同步“微信客服列表”,获取绑定到公司的微信号,用于工作台与分配规则
+ 'wechat_list' => [
+ 'command' => 'wechatList:list',
+ 'schedule' => '*/5 * * * *', // 每5分钟
+ 'options' => [],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'crontab_wechatList.log',
+ ],
+
+ // 同步公司账号列表(企业/租户账号),供后台管理与统计
+ 'account_list' => [
+ 'command' => 'account:list',
+ 'schedule' => '*/5 * * * *', // 每5分钟
+ 'options' => [],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'crontab_account.log',
+ ],
+
+ // 内容采集任务,将外部或设备内容同步到系统内容库
+ 'content_collect' => [
+ 'command' => 'content:collect',
+ 'schedule' => '*/5 * * * *', // 每5分钟
+ 'options' => [],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'crontab_contentCollect.log',
+ ],
+
+ // 工作台:自动点赞好友/客户朋友圈,提高账号活跃度
+ 'workbench_auto_like' => [
+ 'command' => 'workbench:autoLike',
+ 'schedule' => '*/6 * * * *', // 每6分钟
+ 'options' => [],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'crontab_workbench_autoLike.log',
+ ],
+
+ // 工作台:自动建群任务,按规则批量创建微信群
+ 'workbench_group_create' => [
+ 'command' => 'workbench:groupCreate',
+ 'schedule' => '*/5 * * * *', // 每5分钟
+ 'options' => [],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'workbench_groupCreate.log',
+ ],
+
+ // 工作台:自动导入通讯录到系统,生成加粉/建群等任务
+ 'workbench_import_contact' => [
+ 'command' => 'workbench:import-contact',
+ 'schedule' => '*/5 * * * *', // 每5分钟
+ 'options' => [],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'import_contact.log',
+ ],
+
+ // ===========================
+ // 低频任务(每 2 分钟)
+ // ===========================
+
+ // 清洗并同步微信原始数据到存客宝业务表(数据治理任务)
+ 'sync_wechat_data' => [
+ 'command' => 'sync:wechatData',
+ 'schedule' => '*/2 * * * *', // 每2分钟
+ 'options' => [],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'sync_wechat_data.log',
+ ],
+
+ // 工作台:流量分发任务,把流量池中的线索按规则分配给微信号或员工
+ 'workbench_traffic_distribute' => [
+ 'command' => 'workbench:trafficDistribute',
+ 'schedule' => '*/2 * * * *', // 每2分钟
+ 'options' => [],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'traffic_distribute.log',
+ ],
+
+ // 工作台:朋友圈同步任务,拉取并落库朋友圈内容
+ 'workbench_moments' => [
+ 'command' => 'workbench:moments',
+ 'schedule' => '*/2 * * * *', // 每2分钟
+ 'options' => [],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'workbench_moments.log',
+ ],
+
+ // 预防性切换好友任务,监控频繁/风控风险,自动切换加人对象,保护微信号
+ 'switch_friends' => [
+ 'command' => 'switch:friends',
+ 'schedule' => '*/2 * * * *', // 每2分钟
+ 'options' => [],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'switch_friends.log',
+ ],
+
+ // ===========================
+ // 低频任务(每 30 分钟)
+ // ===========================
+
+ // 拉取设备通话记录(语音/电话),用于质检、统计或标签打分
+ 'call_recording' => [
+ 'command' => 'call-recording:list',
+ 'schedule' => '*/30 * * * *', // 每30分钟
+ 'options' => [],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'call_recording.log',
+ ],
+
+ // ===========================
+ // 每日 / 每几天任务
+ // ===========================
+
+ // 每日 1:00 同步“已删除设备”列表,补齐历史状态
+ 'device_deleted' => [
+ 'command' => 'device:list',
+ 'schedule' => '0 1 * * *', // 每天1点
+ 'options' => ['--isDel=1'],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'crontab_device_deleted.log',
+ ],
+
+ // 每日 1:10 同步“已停用设备”列表,更新停用状态
+ 'device_stopped' => [
+ 'command' => 'device:list',
+ 'schedule' => '10 1 * * *', // 每天1:10
+ 'options' => ['--isDel=2'],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'crontab_device_stopped.log',
+ ],
+
+ // 每日 1:30 同步“已删除微信好友”,用于历史恢复与报表
+ 'wechat_friends_deleted' => [
+ 'command' => 'wechatFriends:list',
+ 'schedule' => '30 1 * * *', // 每天1:30
+ 'options' => ['--isDel=1'],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'crontab_wechatFriends_deleted.log',
+ ],
+
+ // 每日 1:30 同步“已删除微信群聊”,用于统计与留痕
+ 'wechat_chatroom_deleted' => [
+ 'command' => 'wechatChatroom:list',
+ 'schedule' => '30 1 * * *', // 每天1:30
+ 'options' => ['--isDel=1'],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'crontab_wechatChatroom_deleted.log',
+ ],
+
+ // 每日 2:00 统一计算所有微信账号健康分(基础分 + 动态分)
+ 'wechat_calculate_score' => [
+ 'command' => 'wechat:calculate-score',
+ 'schedule' => '0 2 * * *', // 每天2点
+ 'options' => [],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'calculate_score.log',
+ ],
+
+ // 每 3 天执行的全量任务
+
+ // 每 3 天 3:00 全量同步所有在线好友,做一次大规模校准
+ 'sync_all_friends' => [
+ 'command' => 'sync:allFriends',
+ 'schedule' => '0 3 */3 * *', // 每3天的3点
+ 'options' => [],
+ 'enabled' => true,
+ 'max_concurrent' => 1,
+ 'log_file' => 'all_friends.log',
+ ],
+
+ // 已禁用的任务(注释掉的任务)
+ // 'workbench_group_push' => [
+ // 'command' => 'workbench:groupPush',
+ // 'schedule' => '*/2 * * * *',
+ // 'options' => [],
+ // 'enabled' => false,
+ // 'log_file' => 'workbench_groupPush.log',
+ // ],
+];
+
diff --git a/config/template.php b/config/template.php
new file mode 100644
index 0000000..2111deb
--- /dev/null
+++ b/config/template.php
@@ -0,0 +1,38 @@
+
+// +----------------------------------------------------------------------
+
+// +----------------------------------------------------------------------
+// | 模板设置
+// +----------------------------------------------------------------------
+
+return [
+ // 模板引擎类型 支持 php think 支持扩展
+ 'type' => 'php',
+ // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 3 保持操作方法
+ 'auto_rule' => 1,
+ // 模板路径
+ 'view_path' => '',
+ // 模板后缀
+ 'view_suffix' => 'php',
+ // 模板文件名分隔符
+ 'view_depr' => DIRECTORY_SEPARATOR,
+ // 模板引擎普通标签开始标记
+ 'tpl_begin' => '{{',
+ // 模板引擎普通标签结束标记
+ 'tpl_end' => '}}',
+ // 标签库标签开始标记
+ 'taglib_begin' => '{{',
+ // 标签库标签结束标记
+ 'taglib_end' => '}}',
+
+ 'layout_on' => true,
+ 'layout_name' => 'layout',
+];
diff --git a/config/trace.php b/config/trace.php
new file mode 100644
index 0000000..425d301
--- /dev/null
+++ b/config/trace.php
@@ -0,0 +1,18 @@
+
+// +----------------------------------------------------------------------
+
+// +----------------------------------------------------------------------
+// | Trace设置 开启 app_trace 后 有效
+// +----------------------------------------------------------------------
+return [
+ // 内置Html Console 支持扩展
+ 'type' => 'Html',
+];
diff --git a/config/wechat_device_api.php b/config/wechat_device_api.php
new file mode 100644
index 0000000..a248025
--- /dev/null
+++ b/config/wechat_device_api.php
@@ -0,0 +1,19 @@
+ 'ChuKeBao',
+
+ // 各个供应商适配器的配置
+ 'adapters' => [
+ 'ChuKeBao' => [
+ 'driver' => \WeChatDeviceApi\Adapters\ChuKebao\Adapter::class,
+// 'api_key' => env('ChuKebao_API_KEY', ''),
+// 'api_secret' => env('ChuKebao_API_SECRET', ''),
+ 'base_url' => env('api.wechat_url'),
+ 'username' => env('api.username', ''),
+ 'password' => env('api.password', ''),
+ ],
+ // ... 更多供应商
+ ],
+];
\ No newline at end of file
diff --git a/config/worker.php b/config/worker.php
new file mode 100644
index 0000000..a385c80
--- /dev/null
+++ b/config/worker.php
@@ -0,0 +1,31 @@
+
+// +----------------------------------------------------------------------
+use think\facade\Env;
+
+// +----------------------------------------------------------------------
+// | Workerman设置 仅对 php think worker 指令有效
+// +----------------------------------------------------------------------
+return [
+ // 扩展自身需要的配置
+ 'host' => '0.0.0.0', // 监听地址
+ 'port' => 2346, // 监听端口
+ 'root' => '', // WEB 根目录 默认会定位public目录
+ 'app_path' => '', // 应用目录 守护进程模式必须设置(绝对路径)
+ 'file_monitor' => false, // 是否开启PHP文件更改监控(调试模式下自动开启)
+ 'file_monitor_interval' => 2, // 文件监控检测时间间隔(秒)
+ 'file_monitor_path' => [], // 文件监控目录 默认监控application和config目录
+
+ // 支持workerman的所有配置参数
+ 'name' => 'thinkphp',
+ 'count' => 4,
+ 'daemonize' => false,
+ 'pidFile' => Env::get('runtime_path') . 'worker.pid',
+];
diff --git a/config/worker_server.php b/config/worker_server.php
new file mode 100644
index 0000000..a1ee5fd
--- /dev/null
+++ b/config/worker_server.php
@@ -0,0 +1,59 @@
+
+// +----------------------------------------------------------------------
+use think\facade\Env;
+
+// +----------------------------------------------------------------------
+// | Workerman设置 仅对 php think worker:server 指令有效
+// +----------------------------------------------------------------------
+return [
+ // 扩展自身需要的配置
+ 'protocol' => 'websocket', // 协议 支持 tcp udp unix http websocket text
+ 'host' => '0.0.0.0', // 监听地址
+ 'port' => 2345, // 监听端口
+ 'socket' => '', // 完整监听地址
+ 'context' => [], // socket 上下文选项
+ 'worker_class' => 'app\common\TaskServer', // 自定义Workerman服务类名 支持数组定义多个服务
+
+ // 支持workerman的所有配置参数
+ 'name' => 'thinkphp',
+ 'count' => 4,
+ 'daemonize' => false,
+ 'pidFile' => __DIR__ . '/../runtime/worker.pid',
+ 'logFile' => __DIR__ . '/../runtime/workerman.log',
+ 'stdoutFile' => __DIR__ . '/../runtime/stdout.log',
+ 'daemonize' => true, // 你用 -d 时会自动变 true
+
+ // 支持事件回调
+ // onWorkerStart
+ 'onWorkerStart' => function ($worker) {
+ //\app\common\socket\MessageHandler::onWorkerStart($worker);
+ },
+ // onWorkerReload
+ 'onWorkerReload' => function ($worker) {
+ //\app\common\socket\MessageHandler::onWorkerReload($worker);
+ },
+ // onConnect
+ 'onConnect' => function ($connection) {
+ //\app\common\socket\MessageHandler::onConnect($connection);
+ },
+ // onMessage
+ 'onMessage' => function ($connection, $data) {
+ //\app\common\socket\MessageHandler::onMessage($connection, $data);
+ },
+ // onClose
+ 'onClose' => function ($connection) {
+ //\app\common\socket\MessageHandler::onClose($connection);
+ },
+ // onError
+ 'onError' => function ($connection, $code, $msg) {
+ //\app\common\socket\MessageHandler::onError($connection);
+ },
+];
diff --git a/crontab_tasks.md b/crontab_tasks.md
new file mode 100644
index 0000000..38e61ea
--- /dev/null
+++ b/crontab_tasks.md
@@ -0,0 +1,181 @@
+# 新版微信服务器定时任务配置
+
+以下为当前 command.php 注册的所有计划任务示例,按需调整执行频率和日志路径。
+
+```bash
+# 设备列表
+*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think device:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/device_list.log 2>&1
+
+# 微信好友列表
+*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatFriends:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/wechat_friends_list.log 2>&1
+
+# 微信群列表
+*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatChatroom:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/wechat_chatroom_list.log 2>&1
+
+# 添加好友任务列表
+*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think friendTask:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/friend_task_list.log 2>&1
+
+# 微信客服列表
+*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatList:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/wechat_list.log 2>&1
+
+# 公司账号列表
+*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think account:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/account_list.log 2>&1
+
+# 微信好友消息列表
+*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think message:friendsList >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/message_friends_list.log 2>&1
+
+# 微信群聊消息列表
+*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think message:chatroomList >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/message_chatroom_list.log 2>&1
+
+# 部门列表
+*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think department:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/department_list.log 2>&1
+
+# 同步内容库
+0 2 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think content:sync >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/content_sync.log 2>&1
+
+# 微信群好友列表
+*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think groupFriends:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/group_friends_list.log 2>&1
+
+# 获取通话记录
+*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think call-recording:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/call_recording.log 2>&1
+
+
+# 分配规则列表
+0 3 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think allotrule:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/allot_rule_list.log 2>&1
+
+# 自动创建分配规则
+0 4 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think allotrule:autocreate >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/allot_rule_autocreate.log 2>&1
+
+# 内容采集任务
+0 5 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think content:collect >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/content_collect.log 2>&1
+
+# 朋友圈采集任务
+0 6 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think moments:collect >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/moments_collect.log 2>&1
+
+# 工作台自动点赞任务
+0 7 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:autoLike >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_auto_like.log 2>&1
+
+# 工作台朋友圈同步任务
+0 8 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:moments >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_moments.log 2>&1
+
+# 同步微信数据到存客宝
+0 9 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think sync:wechatData >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/sync_wechat_data.log 2>&1
+
+# 工作台群发消息
+*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:groupPush >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_groupPush.log 2>&1
+
+# 工作台建群
+*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:groupCreate >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_groupCreate.log 2>&1
+
+# 工作台通讯录导入
+*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:import-contact >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/import_contact.log 2>&1
+
+# 工作台流量分发
+0 9 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:trafficDistribute >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/traffic_distribute.log 2>&1
+
+
+
+# 预防性切换好友
+*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think switch:friends >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/switch_friends.log 2>&1
+
+# 消息提醒
+*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think kf:notice >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/kf_notice.log 2>&1
+
+# 客服评分
+0 2 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechat:calculate-score >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/calculate_score.log 2>&1
+
+
+
+
+
+```
+
+## 说明
+
+- 所有命令都在 `/www/wwwroot/mckb_quwanzhi_com/Server` 目录下执行
+- 默认只获取未删除(活跃)的设备、微信好友和群聊
+- 已注释的命令(以#开头)是获取已删除或已停用数据的任务,可根据需要取消注释启用
+- 每个命令的执行结果都会记录到对应的日志文件中
+- 日志文件名格式包含了数据状态(如 `_active`, `_deleted`, `_stopped`)
+- 日志文件位于 `/www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/` 目录下
+- 大部分任务每5分钟执行一次(`*/5 * * * *` 表示每小时的第0,5,10,15...55分钟执行)
+- 设备列表的未删除设备任务每天凌晨1点执行一次(`0 1 * * *`)
+- 自动创建分配规则每小时整点执行一次(`0 * * * *`)
+- 内容采集任务每5分钟执行一次(`*/5 * * * *`)
+
+## 检查定时任务
+
+使用以下命令查看当前配置的 crontab 任务:
+
+```bash
+crontab -l
+```
+
+```bash
+- 本地: php think worker:server
+- 线上: php think worker:server -d (自带守护进程,无需搭配Supervisor 之类的工具)
+- php think worker:server stop php think worker:server status
+```
+
+
+
+# 设备列表 - 未删除设备(每半小时执行)
+*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think device:list --isDel=0 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_device_active.log 2>&1
+# 设备列表 - 已删除设备(每天1点执行)
+0 1 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think device:list --isDel=1 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_device_deleted.log 2>&1
+# 设备列表 - 已停用设备(每天1:10执行)
+10 1 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think device:list --isDel=2 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_device_stopped.log 2>&1
+# 微信好友列表 - 未删除好友(每1分钟执行)
+*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatFriends:list --isDel=0 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_wechatFriends_active.log 2>&1
+# 微信好友列表 - 已删除好友(每天1:30分执行)
+30 1 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatFriends:list --isDel=1 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_wechatFriends_deleted.log 2>&1
+# 微信群列表 - 未删除群(每5分钟执行)
+*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatChatroom:list --isDel=0 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_wechatChatroom_active.log 2>&1
+# 微信群列表 - 已删除群(每天1:30分执行)
+30 1 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatChatroom:list --isDel=1 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_wechatChatroom_deleted.log 2>&1
+# 微信群好友列表(没5分钟执行)
+*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think groupFriends:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_groupFriends.log 2>&1
+# 添加好友任务列表(每1分钟执行)
+*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think friendTask:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_friendTask.log 2>&1
+# 微信客服列表(每5分钟执行)
+*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatList:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_wechatList.log 2>&1
+# 公司账号列表(每5分钟执行)
+*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think account:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_account.log 2>&1
+# 微信好友消息列表(每30分钟执行)
+*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think message:friendsList >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_messageFriends.log 2>&1
+# 微信群聊消息列表(每30分钟执行)
+*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think message:chatroomList >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_messageChatroom.log 2>&1
+# 获取通话记录
+*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think call-recording:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/call_recording.log 2>&1
+# 清洗微信数据
+*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think sync:wechatData >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/sync_wechat_data.log 2>&1
+# 内容采集任务(每5分钟执行)
+*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think content:collect >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_contentCollect.log 2>&1
+# 工作台任务_自动点赞(每10分钟执行)
+*/6 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:autoLike >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_workbench_autoLike.log 2>&1
+# 每3天的3点同步所有好友
+0 3 */3 * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think sync:allFriends >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/all_friends.log 2>&1
+# 工作台流量分发
+*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:trafficDistribute >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/traffic_distribute.log 2>&1
+# 工作台朋友圈同步任务
+*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:moments >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_moments.log 2>&1
+# 工作台群发消息
+#*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:groupPush >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_groupPush.log 2>&1
+# 预防性切换好友
+*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think switch:friends >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/switch_friends.log 2>&1
+# 工作台建群
+*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:groupCreate >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_groupCreate.log 2>&1
+# 工作台通讯录导入
+*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:import-contact >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/import_contact.log 2>&1
+# 消息提醒
+*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think kf:notice >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/kf_notice.log 2>&1
+# 客服评分
+0 2 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechat:calculate-score >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/calculate_score.log 2>&1
+
+# 采集客服自己的朋友圈
+*/30 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think own:moments:collect >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/own_moments_collect.log 2>&1
+
+
+
+# 每分钟执行一次调度器(调度器内部会自动判断哪些任务需要执行)
+* * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think scheduler:run >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/scheduler.log 2>&1
diff --git a/extend/.gitignore b/extend/.gitignore
new file mode 100644
index 0000000..e69de29
diff --git a/extend/AccountWeight/Exceptions/WechatAccountWeightAssessmentException.php b/extend/AccountWeight/Exceptions/WechatAccountWeightAssessmentException.php
new file mode 100644
index 0000000..51cb167
--- /dev/null
+++ b/extend/AccountWeight/Exceptions/WechatAccountWeightAssessmentException.php
@@ -0,0 +1,16 @@
+ $wechatId,
+ ]
+ )
+ ->value('activity');
+
+ return json_decode($activity)->yesterdayMsgCount ?? 0;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function settingFactor($wechatId): WechatAccountWeightResultSetInterface
+ {
+ $times = intval($this->getChatTimesPerDay($wechatId) / 50 * 100);
+
+ // 规定每天发送50条消息起拥有最高权重
+ $this->weight = $times > 100 ? 100 : $times;
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getResult(): int
+ {
+ return $this->weight ?: 0;
+ }
+}
\ No newline at end of file
diff --git a/extend/AccountWeight/UnitWeight/AgeWeight.php b/extend/AccountWeight/UnitWeight/AgeWeight.php
new file mode 100644
index 0000000..eb40c83
--- /dev/null
+++ b/extend/AccountWeight/UnitWeight/AgeWeight.php
@@ -0,0 +1,69 @@
+ $wechatId,
+ ]
+ )
+ ->value('basic');
+
+ $basic = json_decode($basic);
+
+ // 如果没有设置账号注册时间,则默认今天,即账号年龄为0
+ return $basic && isset($basic->registerDate) ? $basic->registerDate : date('Y-m-d', time());
+ }
+
+ /**
+ * 计算两个时间相差几个月
+ *
+ * @param string $wechatId
+ * @return int
+ * @throws \DateMalformedStringException
+ */
+ private function getDateTimeDiff(string $wechatId): int
+ {
+ $currentData = new \DateTime(date('Y-m-d', time()));
+ $registerDate = new \DateTime($this->getRegisterDate($wechatId));
+
+ $interval = date_diff($currentData, $registerDate);
+
+ return $interval->y * 12 + $interval->m;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function settingFactor($wechatId): WechatAccountWeightResultSetInterface
+ {
+ $cha = ceil($this->getDateTimeDiff($wechatId) / 60) * 100;
+
+ // 规定账号年龄五年起拥有最高权重
+ $this->weight = $cha > 100 ? 100 : $cha;
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getResult(): int
+ {
+ return $this->weight ?: 0;
+ }
+}
\ No newline at end of file
diff --git a/extend/AccountWeight/UnitWeight/RealNameWeight.php b/extend/AccountWeight/UnitWeight/RealNameWeight.php
new file mode 100644
index 0000000..e2dec3d
--- /dev/null
+++ b/extend/AccountWeight/UnitWeight/RealNameWeight.php
@@ -0,0 +1,39 @@
+weight = $this->hereWeGo();
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getResult(): int
+ {
+ return $this->weight ?: 0;
+ }
+}
\ No newline at end of file
diff --git a/extend/AccountWeight/UnitWeight/RestrictWeight.php b/extend/AccountWeight/UnitWeight/RestrictWeight.php
new file mode 100644
index 0000000..df2eb7f
--- /dev/null
+++ b/extend/AccountWeight/UnitWeight/RestrictWeight.php
@@ -0,0 +1,50 @@
+field(
+ [
+ 'r.id', 'r.restrictTime date', 'r.level', 'r.reason'
+ ]
+ )
+ ->where('r.wechatId', $wechatId)->select()
+ ->count('*');
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function settingFactor($wechatId): WechatAccountWeightResultSetInterface
+ {
+ $restrict = 10 - $this->getRestrictCount($wechatId);
+
+ // 规定没有限制记录拥有最高权重,10条以上权重为0
+ $this->weight = ($restrict < 0 ? 0 : $restrict) * 10;
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getResult(): int
+ {
+ return $this->weight ?: 0;
+ }
+}
\ No newline at end of file
diff --git a/extend/AccountWeight/WechatAccountWeightAssessment.php b/extend/AccountWeight/WechatAccountWeightAssessment.php
new file mode 100644
index 0000000..0d53b1f
--- /dev/null
+++ b/extend/AccountWeight/WechatAccountWeightAssessment.php
@@ -0,0 +1,140 @@
+classTable = $classTable ?? app('ClassTable');
+ }
+
+ /**
+ * 获取言
+ * @return string
+ * @throws WechatAccountWeightAssessmentException
+ */
+ private function getWechatId(): string
+ {
+ if (empty($this->wechatId)) {
+ throw new WeightAssessmentException('缺少验证参数');
+ }
+
+ return $this->wechatId;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function calculAgeWeight(): WechatAccountWeightResultSetInterface
+ {
+ $AgeWeight = $this->classTable->getInstance(UnitWeight\AgeWeight::class);
+
+ if (!$this->ageWeight) {
+ $this->ageWeight = $AgeWeight->settingFactor(
+ $this->getWechatId()
+ )
+ ->getResult();
+ }
+
+ return $AgeWeight;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function calculActivityWeigth(): WechatAccountWeightResultSetInterface
+ {
+ $ActivityWeigth = $this->classTable->getInstance(UnitWeight\ActivityWeigth::class);
+
+ if (!$this->activityWeigth) {
+ $this->activityWeigth = $ActivityWeigth->settingFactor(
+ $this->getWechatId()
+ )
+ ->getResult();
+ }
+
+ return $ActivityWeigth;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function calculRestrictWeigth(): WechatAccountWeightResultSetInterface
+ {
+ $RestrictWeight = $this->classTable->getInstance(UnitWeight\RestrictWeight::class);
+
+ if (!$this->restrictWeight) {
+ $this->restrictWeight = $RestrictWeight->settingFactor(
+ $this->getWechatId()
+ )
+ ->getResult();
+ }
+
+ return $RestrictWeight;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function calculRealNameWeigth(): WechatAccountWeightResultSetInterface
+ {
+ $AccountWeight = $this->classTable->getInstance(UnitWeight\RealNameWeight::class);
+
+ if (!$this->realNameWeight) {
+ $this->realNameWeight = $AccountWeight->settingFactor(
+ $this->getWechatId()
+ )
+ ->getResult();
+ }
+
+ return $AccountWeight;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getWeightScope(): int
+ {
+ return ceil(
+ (
+ $this->calculAgeWeight()->getResult() +
+ $this->calculActivityWeigth()->getResult() +
+ $this->calculRestrictWeigth()->getResult() +
+ $this->calculRealNameWeigth()->getResult()
+ ) / 4);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function settingFactor($params): WechatAccountWeightAssessmentInterface
+ {
+ if (!is_string($params)) {
+ throw new WeightAssessmentException('参数错误,只能传微信ID');
+ }
+
+ $this->wechatId = $params;
+
+ return $this;
+ }
+}
\ No newline at end of file
diff --git a/extend/AccountWeight/WechatFriendAddLimitAssessment.php b/extend/AccountWeight/WechatFriendAddLimitAssessment.php
new file mode 100644
index 0000000..49bd887
--- /dev/null
+++ b/extend/AccountWeight/WechatFriendAddLimitAssessment.php
@@ -0,0 +1,25 @@
+getWeightScope();
+ $lastDigit = $scope % 10;
+
+ if ($scope < 10) {
+ $adjusted = $lastDigit < 5 ? 5 : 10;
+ }
+
+ // 每5权重=1好友,最多20个
+ return min(20, floor($adjusted / 5));
+ }
+}
\ No newline at end of file
diff --git a/extend/Eison/Utils/Helper/ArrHelper.php b/extend/Eison/Utils/Helper/ArrHelper.php
new file mode 100644
index 0000000..a659c82
--- /dev/null
+++ b/extend/Eison/Utils/Helper/ArrHelper.php
@@ -0,0 +1,125 @@
+config = $config ?: Config::get('wechat_device_api.');
+ $this->config = $config ?: Config::get('wechat_device_api.adapters.ChuKeBao');
+ // $this->config = $config;
+ // $this->apiClient = new ChuKeBaoApiClient($config['api_key'], $config['api_secret'], $config['base_url']);
+ // 校验配置等...
+ if (empty($this->config['base_url']) || empty($this->config['username']) || empty($this->config['password'])) {
+ throw new \InvalidArgumentException("ChuKeBao username and password are required.");
+ }
+ }
+
+ public function addFriend(string $deviceId, string $targetWxId): bool
+ {
+ // 1. 构建请求参数 (ChuKeBao 特定的格式)
+ $params = [
+ 'device_identifier' => $deviceId,
+ 'wechat_user_to_add' => $targetWxId,
+ 'username' => $this->config['username'],
+ 'password' => $this->config['password'],
+ // ... 其他 ChuKeBao 特定参数
+ ];
+
+ // 2. 调用 ChuKeBao 的 API (例如使用 GuzzleHttp 或 cURL)
+ // $response = $this->apiClient->post('/friend/add', $params);
+ // 伪代码:
+ $url = $this->config['base_url'] . '/friend/add';
+ // $httpClient = new \GuzzleHttp\Client();
+ // $response = $httpClient->request('POST', $url, ['form_params' => $params]);
+ // $responseData = json_decode($response->getBody()->getContents(), true);
+
+ // 模拟API调用
+ echo "ChuKeBao: Adding friend {$targetWxId} using device {$deviceId}\n";
+ $responseData = ['code' => 0, 'message' => 'Success']; // 假设的响应
+
+ // 3. 处理响应,转换为标准结果
+ if (!isset($responseData['code'])) {
+ throw new ApiException("ChuKeBao: Invalid API response for addFriend.");
+ }
+
+ if ($responseData['code'] !== 0) {
+ throw new ApiException("ChuKeBao: Failed to add friend - " . ($responseData['message'] ?? 'Unknown error'));
+ }
+
+ return true;
+ }
+
+ public function likeMoment(string $deviceId, string $momentId): bool
+ {
+ echo "ChuKeBao: Liking moment {$momentId} using device {$deviceId}\n";
+ // 实现 VendorA 的点赞逻辑
+ return true;
+ }
+
+ public function getGroupList(string $deviceId): array
+ {
+ echo "ChuKeBao: Getting group list for device {$deviceId}\n";
+ // 实现 VendorA 的获取群列表逻辑,并转换数据格式
+ return [
+ ['id' => 'group1_va', 'name' => 'ChuKeBao Group 1', 'member_count' => 10],
+ ];
+ }
+
+ public function getFriendList(string $deviceId): array
+ {
+ echo "VendorA: Getting friend list for device {$deviceId}\n";
+ return [
+ ['id' => 'friend1_va', 'nickname' => 'ChuKeBao Friend 1', 'remark' => 'VA-F1'],
+ ];
+ }
+
+ public function getDeviceInfo(string $deviceId): array
+ {
+ echo "ChuKeBao: Getting device info for device {$deviceId}\n";
+ return ['id' => $deviceId, 'status' => 'online_va', 'battery' => '80%'];
+ }
+
+ public function bindDeviceToCompany(string $deviceId, string $companyId): bool
+ {
+ echo "ChuKeBao: Binding device {$deviceId} to company {$companyId}\n";
+ return true;
+ }
+
+ /**
+ * 获取群成员列表
+ * @param string $deviceId 设备ID
+ * @param string $chatroomId 群ID
+ * @return array 群成员列表
+ */
+ public function getChatroomMemberList(string $deviceId, string $chatroomId): array
+ {
+ echo "ChuKeBao: Getting chatroom member list for device {$deviceId}, chatroom {$chatroomId}\n";
+ return [
+ ['id' => 'member1_va', 'nickname' => 'VendorA Member 1', 'avatar' => ''],
+ ];
+ }
+
+ /**
+ * 获取指定微信的朋友圈内容/列表
+ * @param string $deviceId 设备ID
+ * @param string $wxId 微信ID
+ * @return array 朋友圈列表
+ */
+ public function getMomentList(string $deviceId, string $wxId): array
+ {
+ echo "VendorA: Getting moment list for device {$deviceId}, wxId {$wxId}\n";
+ return [
+ ['id' => 'moment1_va', 'content' => 'VendorA Moment 1', 'created_at' => time()],
+ ];
+ }
+
+
+ /**
+ * 发送微信朋友圈
+ * @param string $deviceId 设备ID
+ * @param string $wxId 微信ID
+ * @param string $moment 朋友圈内容
+ * @return bool 是否成功
+ */
+ public function sendMoment(string $deviceId, string $wxId, string $moment): bool
+ {
+ echo "VendorA: Sending moment for device {$deviceId}, wxId {$wxId}, content: {$moment}\n";
+ return true;
+ }
+
+ public function handleCustomerTaskWithStatusIsNew(int $current_worker_id, int $process_count_for_status_0)
+ {
+ $task = Db::name('customer_acquisition_task')
+ ->where(['status' => 1, 'deleteTime' => 0])
+ /* ->whereRaw("id % $process_count_for_status_0 = {$current_worker_id}")*/
+ ->order('id desc')
+ ->select();
+
+ if (empty($task)) {
+ return false;
+ }
+
+ $taskData = [];
+ foreach ($task as $item) {
+ $reqConf = json_decode($item['reqConf'], true);
+ $device = $reqConf['device'] ?? [];
+ $deviceCount = count($device);
+ if ($deviceCount <= 0) {
+ continue;
+ }
+ $tasks = Db::name('task_customer')
+ ->where(['status' => 0, 'task_id' => $item['id']])
+ ->order('id DESC')
+ ->limit($deviceCount)
+ ->select();
+ $taskData = array_merge($taskData, $tasks);
+ }
+ if ($taskData) {
+
+ foreach ($taskData as $task) {
+ $task_id = $task['task_id'];
+ $task_info = $this->getCustomerAcquisitionTask($task_id);
+ if (empty($task_info['status']) || empty($task_info['reqConf']) || empty($task_info['reqConf']['device'])) {
+ continue;
+ }
+ //筛选出设备在线微信在线
+ $wechatIdAccountIdMap = $this->getWeChatIdsAccountIdsMapByDeviceIds($task_info['reqConf']['device']);
+ if (empty($wechatIdAccountIdMap)) {
+ continue;
+ }
+
+ $friendAddTaskCreated = false;
+ foreach ($wechatIdAccountIdMap as $accountId => $wechatId) {
+ // 是否已经是好友的判断,如果已经是好友,直接break; 但状态还是维持1,让另外一个进程处理发消息的逻辑
+ $wechatTags = json_decode($task['tags'], true);
+ $isFriend = $this->checkIfIsWeChatFriendByPhone($wechatId, $task['phone'], $task['siteTags']);
+ if (!empty($isFriend)) {
+ $friendAddTaskCreated = true;
+ $task['processed_wechat_ids'] = $task['processed_wechat_ids'] . ',' . $wechatId; // 处理失败任务用,用于过滤已处理的微信号
+ break;
+ }
+
+ // 判断时间间隔\时间段和最后一次的状态
+ $canCreateFriendAddTask = $this->checkIfCanCreateFriendAddTask($wechatId, $task_info['reqConf']);
+ if (empty($canCreateFriendAddTask)) {
+ continue;
+ }
+
+ // 根据健康分判断24h内加的好友数量限制
+ $healthScoreService = new WechatAccountHealthScoreService();
+ $healthScoreInfo = $healthScoreService->getHealthScore($accountId);
+
+ // 如果健康分记录不存在,先计算一次
+ if (empty($healthScoreInfo)) {
+ try {
+ $healthScoreService->calculateAndUpdate($accountId);
+ $healthScoreInfo = $healthScoreService->getHealthScore($accountId);
+ } catch (\Exception $e) {
+ Log::error("计算健康分失败 (accountId: {$accountId}): " . $e->getMessage());
+ // 如果计算失败,使用默认值5作为兜底
+ $maxAddFriendPerDay = 5;
+ }
+ }
+
+ // 获取每日最大加人次数(基于健康分)
+ $maxAddFriendPerDay = $healthScoreInfo['maxAddFriendPerDay'] ?? 5;
+
+ // 如果健康分为0或很低,不允许添加好友
+ if ($maxAddFriendPerDay <= 0) {
+ Log::info("账号健康分过低,不允许添加好友 (accountId: {$accountId}, wechatId: {$wechatId}, healthScore: " . ($healthScoreInfo['healthScore'] ?? 0) . ")");
+ continue;
+ }
+
+ // 检查频繁暂停限制:首次频繁或再次频繁,暂停24小时
+ $lastFrequentTime = $healthScoreInfo['lastFrequentTime'] ?? null;
+ $frequentCount = $healthScoreInfo['frequentCount'] ?? 0;
+ if (!empty($lastFrequentTime) && $frequentCount > 0) {
+ $frequentPauseHours = 24; // 频繁暂停24小时
+ $frequentPauseTime = $lastFrequentTime + ($frequentPauseHours * 3600);
+ $currentTime = time();
+
+ if ($currentTime < $frequentPauseTime) {
+ $remainingHours = ceil(($frequentPauseTime - $currentTime) / 3600);
+ Log::info("账号频繁,暂停添加好友 (accountId: {$accountId}, wechatId: {$wechatId}, frequentCount: {$frequentCount}, 剩余暂停时间: {$remainingHours}小时)");
+ continue;
+ }
+ }
+
+ // 检查封号暂停限制:封号暂停72小时
+ $isBanned = $healthScoreInfo['isBanned'] ?? 0;
+ if ($isBanned == 1) {
+ // 查询封号时间(从s2_wechat_message表查询最近一次封号消息)
+ $banMessage = Db::table('s2_wechat_message')
+ ->where('wechatAccountId', $accountId)
+ ->where('msgType', 10000)
+ ->where('content', 'like', '%你的账号被限制%')
+ ->where('isDeleted', 0)
+ ->order('createTime', 'desc')
+ ->find();
+
+ if (!empty($banMessage)) {
+ $banTime = $banMessage['createTime'] ?? 0;
+ $banPauseHours = 72; // 封号暂停72小时
+ $banPauseTime = $banTime + ($banPauseHours * 3600);
+ $currentTime = time();
+
+ if ($currentTime < $banPauseTime) {
+ $remainingHours = ceil(($banPauseTime - $currentTime) / 3600);
+ Log::info("账号封号,暂停添加好友 (accountId: {$accountId}, wechatId: {$wechatId}, 剩余暂停时间: {$remainingHours}小时)");
+ continue;
+ }
+ }
+ }
+
+ // 判断今天添加的好友数量,使用健康分计算的每日最大加人次数
+ // 优先使用今天添加的好友数量(更符合"每日"限制)
+ $todayAddedFriendsCount = $this->getTodayAddedFriendsCount($wechatId);
+ if ($todayAddedFriendsCount >= $maxAddFriendPerDay) {
+ Log::info("今天添加好友数量已达上限 (accountId: {$accountId}, wechatId: {$wechatId}, count: {$todayAddedFriendsCount}, max: {$maxAddFriendPerDay}, healthScore: " . ($healthScoreInfo['healthScore'] ?? 0) . ")");
+ continue;
+ }
+
+ // 如果今天添加数量未达上限,再检查24小时内的数量(作为额外保护)
+ $last24hAddedFriendsCount = $this->getLast24hAddedFriendsCount($wechatId);
+ // 24小时内的限制可以稍微宽松一些,设置为每日限制的1.2倍(防止跨天累积)
+ $max24hLimit = (int)ceil($maxAddFriendPerDay * 1.2);
+ if ($last24hAddedFriendsCount >= $max24hLimit) {
+ Log::info("24小时内添加好友数量已达上限 (accountId: {$accountId}, wechatId: {$wechatId}, count: {$last24hAddedFriendsCount}, max24h: {$max24hLimit}, maxDaily: {$maxAddFriendPerDay})");
+ continue;
+ }
+
+ // 采取乐观尝试的策略,假设第一个可以添加的人可以添加成功的; 回头再另外一个任务进程去判断
+
+ // 创建好友添加任务, 对接触客宝
+ $tags = array_merge($task_info['tagConf']['customTags'], $task_info['tagConf']['scenarioTags']);
+ if (!empty($wechatTags)) {
+ $tags = array_merge($tags, $wechatTags);
+ }
+ $tags = array_unique($tags);
+ $tags = array_values($tags);
+ $conf = array_merge($task_info['reqConf'], ['task_name' => $task_info['name'], 'tags' => $tags]);
+
+
+ $this->createFriendAddTask($accountId, $task['phone'], $conf, $task['remark']);
+ $friendAddTaskCreated = true;
+ $task['processed_wechat_ids'] = $task['processed_wechat_ids'] . ',' . $wechatId; // 处理失败任务用,用于过滤已处理的微信号
+ break;
+ }
+ if (!empty($friendAddTaskCreated)){
+ Db::name('task_customer')
+ ->where('id', $task['id'])
+ ->update([
+ 'status' => $friendAddTaskCreated ? 1 : 3,
+ 'fail_reason' => '',
+ 'processed_wechat_ids' => $task['processed_wechat_ids'],
+ 'addTime' => time(),
+ 'updateTime' => time()
+ ]);
+ }
+ // ~~不用管,回头再添加再判断即可~~
+ // 失败一定是另一个进程/定时器在检查的
+
+ }
+ }
+ }
+
+ // 处理添加中的获客任务, only run in workerman process!
+ public function handleCustomerTaskWithStatusIsCreated()
+ {
+
+ $tasks = Db::name('task_customer')
+ ->whereIn('status', [1, 2])
+ ->where('updateTime', '>=', (time() - 86400 * 3))
+ ->limit(50)
+ ->order('updateTime DESC')
+ ->select();
+
+ if (empty($tasks)) {
+ return;
+ }
+
+ foreach ($tasks as $task) {
+ $task_id = $task['task_id'];
+ $task_info = $this->getCustomerAcquisitionTask($task_id);
+
+
+ if (empty($task_info['status']) || empty($task_info['reqConf']) || empty($task_info['reqConf']['device'])) {
+ continue;
+ }
+
+ if (empty($task['processed_wechat_ids'])) {
+ continue;
+ }
+
+ $weChatIds = explode(',', $task['processed_wechat_ids']);
+ $passedWeChatId = '';
+ foreach ($weChatIds as $wechatId) {
+ // 先是否是好友,如果不是好友,先查询执行状态,看是否还能以及需要换账号继续添加,还是直接更新状态为3
+ // 如果添加成功,先更新为2,然后去发消息(先判断有无消息设置,发消息的log记录?)
+ if (!empty($wechatId)) {
+ $isFriend = $this->checkIfIsWeChatFriendByPhone($wechatId, $task['phone']);
+ if ($isFriend) {
+ // 更新状态为5(已通过未发消息)
+ Db::name('task_customer')
+ ->where('id', $task['id'])
+ ->update(['status' => 5,'passTime' => time(), 'updateTime' => time()]);
+ $passedWeChatId = $wechatId;
+ break;
+ }
+ }
+ }
+
+
+ if ($passedWeChatId && !empty($task_info['msgConf'])) {
+
+ // 更新状态为4(已通过并已发消息)
+ Db::name('task_customer')
+ ->where('id', $task['id'])
+ ->update(['status' => 4,'passTime' => time(), 'updateTime' => time()]);
+
+ // 记录添加好友奖励(如果之前没有记录过,status从其他状态变为4时)
+ // 注意:如果status已经是2,说明已经记录过奖励,这里不再重复记录
+ if ($task['status'] != 2 && !empty($task['channelId'])) {
+ try {
+ DistributionRewardService::recordAddFriendReward(
+ $task['task_id'],
+ $task['id'],
+ $task['phone'],
+ intval($task['channelId'])
+ );
+ } catch (\Exception $e) {
+ // 记录错误但不影响主流程
+ Log::error('记录添加好友奖励失败:' . $e->getMessage());
+ }
+ }
+
+ $wechatFriendRecord = $this->getWeChatAccoutIdAndFriendIdByWeChatIdAndFriendPhone($passedWeChatId, $task['phone']);
+ $msgConf = is_string($task_info['msgConf']) ? json_decode($task_info['msgConf'], 1) : $task_info['msgConf'];
+ $wechatFriendRecord && $this->sendMsgToFriend($wechatFriendRecord['id'], $wechatFriendRecord['wechatAccountId'], $msgConf);
+
+ } else {
+
+ foreach ($weChatIds as $wechatId) {
+
+ // 查询执行状态
+ $latestFriendTask = $this->getLatestFriendTaskByPhoneAndWeChatId($task['phone'], $wechatId);
+ if (empty($latestFriendTask)) {
+ continue;
+ }
+
+ // 已经执行成功的话,直接break,同时更新对应task_customer的状态为2(添加成功)
+ if (isset($latestFriendTask['status']) && $latestFriendTask['status'] == 1) {
+ // 更新状态
+ Db::name('task_customer')
+ ->where('id', $task['id'])
+ ->update(['status' => 2, 'updateTime' => time()]);
+
+ // 记录添加好友奖励(异步处理,不影响主流程)
+ if (!empty($task['channelId'])) {
+ try {
+ DistributionRewardService::recordAddFriendReward(
+ $task['task_id'],
+ $task['id'],
+ $task['phone'],
+ intval($task['channelId'])
+ );
+ } catch (\Exception $e) {
+ // 记录错误但不影响主流程
+ Log::error('记录添加好友奖励失败:' . $e->getMessage());
+ }
+ }
+
+ break;
+ }
+
+ // todo 判断处理执行失败的情况 status=2,根据 extra 的描述去处理;-- 可以先直接更新为失败,然后 extra =》fail_reason -- 因为有专门的任务会处理失败的
+ if (isset($latestFriendTask['status']) && $latestFriendTask['status'] == 2) {
+ Db::name('task_customer')
+ ->where('id', $task['id'])
+ ->update(['status' => 3, 'fail_reason' => $latestFriendTask['extra'] ?? '未知原因', 'updateTime' => time()]);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+
+ public function handleCustomerTaskNewUser()
+ {
+ $task = Db::name('customer_acquisition_task')
+ ->where(['status' => 1, 'deleteTime' => 0])
+ ->whereIn('sceneId', [5, 7])
+ ->order('id desc')
+ ->select();
+
+ if (empty($task)) {
+ return false;
+ }
+
+ foreach ($task as $item) {
+ $sceneConf = json_decode($item['sceneConf'], true);
+ //电话
+ if ($item['sceneId'] == 5) {
+ $rows = Db::name('call_recording')
+ ->where('companyId', $item['companyId'])
+ ->group('phone')
+ ->field('id,phone')
+ ->order('id asc')
+ ->limit(0, 100)
+ ->select();
+ }
+
+ if ($item['sceneId'] == 7) {
+ if (!empty($sceneConf['groupSelected']) && is_array($sceneConf['groupSelected'])) {
+ $rows = Db::name('wechat_group_member')->alias('gm')
+ ->join('wechat_account wa', 'gm.identifier = wa.wechatId')
+ ->where('gm.companyId', $item['companyId'])
+ ->whereIn('gm.groupId', $sceneConf['groupSelected'])
+ ->group('gm.identifier')
+ ->column('wa.id,wa.wechatId,wa.alias,wa.phone');
+ }
+ }
+
+
+ if (in_array($item['sceneId'], [5, 7]) && !empty($rows) && is_array($rows)) {
+ // 1000条为一组进行批量处理
+ $batchSize = 1000;
+ $totalRows = count($rows);
+
+ for ($i = 0; $i < $totalRows; $i += $batchSize) {
+ $batchRows = array_slice($rows, $i, $batchSize);
+
+ if (!empty($batchRows)) {
+ // 1. 提取当前批次的phone
+ $phones = [];
+ foreach ($batchRows as $row) {
+ if (!empty($row['phone'])) {
+ $phone = !empty($row['phone']);
+ } elseif (!empty($row['alias'])) {
+ $phone = $row['alias'];
+ } else {
+ $phone = $row['wechatId'];
+ }
+ if (!empty($phone)) {
+ $phones[] = $phone;
+ }
+ }
+
+ // 2. 批量查询已存在的phone
+ $existingPhones = [];
+ if (!empty($phones)) {
+ $existing = Db::name('task_customer')
+ ->where('task_id', $item['id'])
+ ->where('phone', 'in', $phones)
+ ->field('phone')
+ ->select();
+ $existingPhones = array_column($existing, 'phone');
+ }
+
+ // 3. 过滤出新数据,批量插入
+ $newData = [];
+ foreach ($batchRows as $row) {
+ if (!empty($row['phone'])) {
+ $phone = !empty($row['phone']);
+ } elseif (!empty($row['alias'])) {
+ $phone = $row['alias'];
+ } else {
+ $phone = $row['wechatId'];
+ }
+ if (!empty($phone) && !in_array($phone, $existingPhones)) {
+ $newData[] = [
+ 'task_id' => $item['id'],
+ 'name' => '',
+ 'source' => '场景获客_' . $item['name'],
+ 'phone' => $phone,
+ 'tags' => json_encode([], JSON_UNESCAPED_UNICODE),
+ 'siteTags' => json_encode([], JSON_UNESCAPED_UNICODE),
+ 'createTime' => time(),
+ ];
+ }
+ }
+
+ // 4. 批量插入新数据
+ if (!empty($newData)) {
+ Db::name('task_customer')->insertAll($newData);
+ }
+ }
+ }
+ }
+
+
+ }
+ }
+
+
+ // 发微信个人消息
+ public function sendMsgToFriend(int $friendId, int $wechatAccountId, array $msgConf)
+ {
+ // 消息拼接 msgType(1:文本 3:图片 43:视频 47:动图表情包(gif、其他表情包) 49:小程序/其他:图文、文件)
+ // 当前,type 为文本、图片、动图表情包的时候,content为string, 其他情况为对象 {type: 'file/link/...', url: '', title: '', thunmbPath: '', desc: ''}
+ // $result = [
+ // "content" => $dataArray['content'],
+ // "msgSubType" => 0,
+ // "msgType" => $dataArray['msgType'],
+ // "seq" => time(),
+ // "wechatAccountId" => $dataArray['wechatAccountId'],
+ // "wechatChatroomId" => 0,
+ // "wechatFriendId" => $dataArray['wechatFriendId'],
+ // ];
+ $toAccountId = '';
+ $username = Env::get('api.username', '');
+ $password = Env::get('api.password', '');
+ if (!empty($username) || !empty($password)) {
+ $toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId');
+ }
+
+ // 建立WebSocket
+ $wsController = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]);
+
+
+ $gap = 0;
+ foreach ($msgConf as $messages) {
+ foreach ($messages['messages'] as $content) {
+
+ $msgType = 0;
+ $detail = '';
+ switch ($content['type']) {
+ case 'text':
+ $msgType = 1;
+ $detail = $content['content'];
+ break;
+ case 'image':
+ $msgType = 3;
+ $detail = $content['content'];
+ break;
+ case 'video':
+ $msgType = 43;
+ $detail = $content['content'];
+ break;
+
+ case 'file':
+ $msgType = 49;
+
+ $detail = [
+ 'type' => 'file',
+ 'title' => $content['content'][0]['name'],
+ 'url' => $content['content'][0]['url'],
+ ];
+ $detail = json_encode($detail);
+ break;
+
+ case 'miniprogram':
+ $msgType = 49;
+ $detail = '';
+ break;
+
+ case 'link':
+ $msgType = 49;
+ $detail = [
+ 'type' => 'link',
+ 'title' => $content['title'],
+ 'url' => $content['linkUrl'],
+ 'thumbPath' => $content['cover'],
+ 'desc' => $content['description'],
+ ];
+ $detail = json_encode($detail);
+ break;
+
+ case 'group':
+ $msgType = 49;
+ $detail = '';
+ break;
+ default :
+ $msgType = 47;
+ $detail = $content['content'];
+ break;
+ }
+
+
+ if (empty($detail)) {
+ continue;
+ }
+
+ if ($gap) {
+ Timer::add($gap, function () use ($wsController, $friendId, $wechatAccountId, $msgType, $content, $detail) {
+ $wsController->sendPersonal([
+ 'wechatFriendId' => $friendId,
+ 'wechatAccountId' => $wechatAccountId,
+ 'msgType' => $msgType,
+ 'content' => $detail,
+ ]);
+ }, [], false);
+ } else {
+ $wsController->sendPersonal([
+ 'wechatFriendId' => $friendId,
+ 'wechatAccountId' => $wechatAccountId,
+ 'msgType' => $msgType,
+ 'content' => $detail,
+ ]);
+ }
+
+ !empty($content['sendInterval']) && $gap += $content['sendInterval'];
+ }
+ }
+
+ }
+
+ // getCustomerAcquisitionTask
+ public function getCustomerAcquisitionTask($id)
+ {
+ // 先读取缓存
+ $task_info = Db::name('customer_acquisition_task')
+ ->where('id', $id)
+ ->find();
+ if ($task_info) {
+ $task_info['sceneConf'] = json_decode($task_info['sceneConf'], true);
+ $task_info['reqConf'] = json_decode($task_info['reqConf'], true);
+ $task_info['msgConf'] = json_decode($task_info['msgConf'], true);
+ $task_info['tagConf'] = json_decode($task_info['tagConf'], true);
+ }
+ return $task_info;
+ }
+
+ // 检查是否是好友关系
+
+ public function checkIfIsWeChatFriendByPhone($wxId = '', $phone = '', $siteTags = '')
+ {
+ if (empty($wxId) || empty($phone)) {
+ return false;
+ }
+
+ try {
+ $friend = Db::table('s2_wechat_friend')
+ ->where('ownerWechatId', $wxId)
+ ->where(['isPassed' => 1, 'isDeleted' => 0])
+ ->where('phone|alias|wechatId', 'like', $phone . '%')
+ ->order('createTime', 'desc')
+ ->find();
+ if (!empty($friend)) {
+ if (!empty($siteTags)) {
+ $siteTags = json_decode($siteTags, true);
+ $siteLabels = json_decode($friend['siteLabels'], true);
+ $tags = array_merge($siteTags, $siteLabels);
+ $tags = array_unique($tags);
+ $tags = array_values($tags);
+ if (empty($tags)) {
+ $tags = [];
+ }
+ $tags = json_encode($tags, 256);
+ Db::table('s2_wechat_friend')->where(['id' => $friend['id']])->update(['siteLabels' => $tags, 'updateTime' => time()]);
+ }
+ return true;
+ } else {
+ return false;
+ }
+ } catch (\Exception $e) {
+ Log::error("Error in checkIfIsWeChatFriendByPhone (wxId: {$wxId}, phone: {$phone}): " . $e->getMessage());
+ return false;
+ }
+ }
+
+ // getWeChatAccoutIdAndFriendIdByWeChatId
+ public function getWeChatAccoutIdAndFriendIdByWeChatIdAndFriendPhone(string $wechatId, string $phone): array
+ {
+ if (empty($wechatId) || empty($phone)) {
+ return [];
+ }
+
+ return Db::table('s2_wechat_friend')
+ ->where('ownerWechatId', $wechatId)
+ ->where('phone|alias|wechatId', 'like', $phone . '%')
+ ->field('id,wechatAccountId,passTime,createTime')
+ ->find();
+ }
+
+ // 判断是否已添加某手机号为好友并返回添加时间
+ public function getWeChatFriendPassTimeByPhone(string $wxId, string $phone): int
+ {
+ if (empty($wxId) || empty($phone)) {
+ return 0;
+ }
+
+ try {
+ $record = Db::table('s2_wechat_friend')
+ ->where('ownerWechatId', $wxId)
+ ->where('phone|alias|wechatId', 'like', $phone . '%')
+ ->field('id,createTime,passTime')
+ ->find();
+
+ return $record['passTime'] ?? $record['createTime'] ?? 0;
+ } catch (\Exception $e) {
+ Log::error("Error in getWeChatFriendPassTimeByPhone (wxId: {$wxId}, phone: {$phone}): " . $e->getMessage());
+ return 0;
+ }
+ }
+
+ /**
+ * 查询某个微信今天添加了多少个好友
+ * @param string $wechatId 微信ID
+ * @return int 好友数量
+ */
+ public function getTodayAddedFriendsCount(string $wechatId): int
+ {
+ if (empty($wechatId)) {
+ return 0;
+ }
+ try {
+ $count = Db::table('s2_friend_task')
+ ->where('wechatId', $wechatId)
+ ->whereRaw("FROM_UNIXTIME(createTime, '%Y-%m-%d') = CURDATE()")
+ ->count();
+ return (int)$count;
+ } catch (\Exception $e) {
+ Log::error("Error in getTodayAddedFriendsCount (wechatId: {$wechatId}): " . $e->getMessage());
+ return 0;
+ }
+ }
+
+ /**
+ * 查询某个微信24小时内添加了多少个好友
+ * @param string $wechatId 微信ID
+ * @return int 好友数量
+ */
+ public function getLast24hAddedFriendsCount(string $wechatId): int
+ {
+ if (empty($wechatId)) {
+ return 0;
+ }
+ try {
+ $twentyFourHoursAgo = time() - (24 * 60 * 60);
+ $count = Db::table('s2_friend_task')
+ ->where('wechatId', $wechatId)
+ ->where('createTime', '>=', $twentyFourHoursAgo)
+ ->count();
+ return (int)$count;
+ } catch (\Exception $e) {
+ Log::error("Error in getLast24hAddedFriendsCount (wechatId: {$wechatId}): " . $e->getMessage());
+ return 0;
+ }
+ }
+
+ /**
+ * 查询某个微信最新的一条添加好友任务记录
+ * @param string $wechatId 微信ID
+ * @return array|null 任务记录或null
+ */
+ public function getLatestFriendTask(string $wechatId): ?array
+ {
+ if (empty($wechatId)) {
+ return null;
+ }
+ try {
+ $task = Db::table('s2_friend_task')
+ ->where('wechatId', $wechatId)
+ ->order('createTime', 'desc')
+ ->find();
+ return $task;
+ } catch (\Exception $e) {
+ Log::error("Error in getLatestFriendTask (wechatId: {$wechatId}): " . $e->getMessage());
+ return null;
+ }
+ }
+
+ // 获取某微信最后一条添加好友任务
+ public function getLatestFriendTaskByPhoneAndWeChatId(string $phone, string $wechatId): array
+ {
+ if (empty($phone) || empty($wechatId)) {
+ return [];
+ }
+
+ $record = Db::table('s2_friend_task')
+ ->where('phone', $phone)
+ ->where('wechatId', $wechatId)
+ ->order('createTime', 'desc')
+ ->find();
+ return $record ?: [];
+ }
+
+ // 获取最新的一条添加好友任务记录的创建时间
+ public function getLastCreateFriendTaskTime(string $wechatId): int
+ {
+ if (empty($wechatId)) {
+ return 0;
+ }
+ $record = Db::table('s2_friend_task')
+ ->where('wechatId', $wechatId)
+ ->order('createTime', 'desc')
+ ->find();
+ return $record['createTime'] ?? 0;
+ }
+
+ // 判断是否能够加好友
+ public function checkIfCanCreateFriendAddTask(string $wechatId, $conf = []): bool
+ {
+ if (empty($wechatId)) {
+ return false;
+ }
+ //强制请求添加好友的列表
+ $friendController = new FriendTaskController();
+ $friendController->getlist(0, 50);
+
+
+ $record = $this->getLatestFriendTask($wechatId);
+ if (empty($record)) {
+ return true;
+ }
+
+ if (!empty($conf['addFriendInterval']) && isset($record['createTime']) && $record['createTime'] > time() - $conf['addFriendInterval'] * 60) {
+ return false;
+ }
+
+ if (!empty($conf['startTime']) && !empty($conf['endTime'])) {
+ $currentTime = date('H:i');
+ $startTime = $conf['startTime'];
+ $endTime = $conf['endTime'];
+
+ if ($currentTime >= $startTime && $currentTime <= $endTime) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ if (isset($record['status'])) {
+
+ if ($record['status'] == 2) {
+
+ // 判断$record['extra'] 是否包含文字: 操作过于频繁;如果包含判断 updateTime 是否已经超过72min,updateTime是10位时间戳;如果包含指定文字且时间未超过72min,return false
+ if (isset($record['extra']) && strpos($record['extra'], '操作过于频繁') !== false) {
+ $updateTime = isset($record['updateTime']) ? (int)$record['updateTime'] : 0;
+ $now = time();
+ $diff = $now - $updateTime;
+
+ if ($diff < 24 * 60 * 60) {
+ return false;
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ // 获取触客宝系统的客服微信账号id,用于后续微信相关操作
+ public function getWeChatAccountIdByWechatId(string $wechatId): string
+ {
+ if (empty($wechatId)) {
+ return '';
+ }
+ $record = Db::table('s2_wechat_account')
+ ->where('wechatId', $wechatId)
+ ->field('id')
+ ->find();
+ return $record['id'] ?? '';
+ }
+
+ // 获取在线的客服微信账号id列表
+ public function getOnlineWeChatAccountIdsByWechatIds(array $wechatIds): array
+ {
+ if (empty($wechatIds)) {
+ return [];
+ }
+ $records = Db::table('s2_wechat_account')
+ ->where('deviceAlive', 1)
+ ->where('wechatAlive', 1)
+ ->where('wechatId', 'in', $wechatIds)
+ ->field('id,wechatId')
+ ->column('id', 'wechatId');
+
+ return $records;
+ }
+
+ public function getWeChatIdsAccountIdsMapByDeviceIds(array $deviceIds): array
+ {
+ if (empty($deviceIds)) {
+ return [];
+ }
+
+ $records = Db::table('s2_wechat_account')
+ ->where('deviceAlive', 1)
+ ->where('wechatAlive', 1)
+ ->where('currentDeviceId', 'in', $deviceIds)
+ ->field('id,wechatId')
+ ->column('id,wechatId');
+ return $records;
+ }
+
+ // 触客宝添加好友API
+ public function addFriendTaskApi(int $wechatAccountId, string $phone, string $message, string $remark, array $labels, $authorization = '')
+ {
+
+ $authorization = $authorization ?: AuthService::getSystemAuthorization();
+
+ if (empty($authorization)) {
+ return [
+ 'status_code' => 0,
+ 'body' => null,
+ 'error' => true,
+ ];
+ }
+
+ $params = [
+ 'phone' => $phone,
+ 'message' => $message,
+ 'remark' => $remark,
+ 'labels' => $labels,
+ 'wechatAccountId' => $wechatAccountId
+ ];
+
+ //准备发起添加请求
+ $friendController = new FriendTaskController();
+ $result = $friendController->addFriendTask($params);
+ $result = json_decode($result, true);
+ if ($result['code'] == 200) {
+ return $result;
+ } else {
+ $authorization = AuthService::getSystemAuthorization(false);
+ return $this->addFriendTaskApi($wechatAccountId, $phone, $message, $remark, $labels, $authorization);
+ }
+
+
+ }
+
+ // 创建添加好友任务/执行添加
+ public function createFriendAddTask(int $wechatAccountId, string $phone, array $conf, $remark = '')
+ {
+ if (empty($wechatAccountId) || empty($phone) || empty($conf)) {
+ return;
+ }
+
+ if (empty($remark)){
+ switch ($conf['remarkType']) {
+ case 'phone':
+ $remark = $phone . '-' . $conf['task_name'];
+ break;
+ case 'nickname':
+ $remark = '';
+ break;
+ case 'source':
+ $remark = $conf['task_name'];
+ break;
+ default:
+ $remark = '';
+ break;
+ }
+ }
+
+ $tags = [];
+ if (!empty($conf['tags'])) {
+ if (is_array($conf['tags'])) {
+ $tags = $conf['tags'];
+ }
+ }
+ $res = $this->addFriendTaskApi($wechatAccountId, $phone, $conf['greeting'] ?? '你好', $remark, $tags);
+ }
+
+ /* TODO: 以上方法待实现,基于/参考 application/api/controller/WebSocketController.php 去实现;以下同步脚本用的方法转移到其他类 */
+
+
+ // NOTE: run in background; 5min 同步一次
+ public function syncFriendship()
+ {
+ $sql = "INSERT INTO ck_wechat_friendship(id,wechatId,tags,memo,ownerWechatId,createTime,updateTime,deleteTime,companyId)
+ SELECT
+ f.id,f.wechatId,f.labels as tags,f.conRemark as memo,f.ownerWechatId,f.createTime,f.updateTime,f.deleteTime,
+ c.departmentId
+ FROM s2_wechat_friend f
+ LEFT JOIN s2_wechat_account a on a.id = f.wechatAccountId
+ LEFT JOIN s2_company_account c on c.id = a.deviceAccountId
+ ORDER BY f.id DESC
+ LIMIT ?, ?
+ ON DUPLICATE KEY UPDATE
+ id=VALUES(id),
+ tags=VALUES(tags),
+ memo=VALUES(memo),
+ updateTime=VALUES(updateTime),
+ deleteTime=VALUES(deleteTime),
+ companyId=VALUES(companyId)";
+
+ $offset = 0;
+ $limit = 2000;
+ $usleepTime = 50000;
+
+ do {
+ $affected = Db::execute($sql, [$offset, $limit]);
+ $offset += $limit;
+ if ($affected > 0) {
+ usleep($usleepTime);
+ }
+ } while ($affected > 0);
+ }
+
+
+ public function syncWechatAccount()
+ {
+ $pk = 'wechatId';
+ $limit = 1000;
+ // $lastId = '';
+ $lastId = null; // Or some other sentinel indicating "first run"
+
+
+ $totalAffected = 0;
+ $iterations = 0;
+ $maxIterations = 10000;
+
+ do {
+ // Fetch a batch of distinct wechatIds
+ // Important: Order by wechatId for consistent pagination
+ $sourceDb = Db::connect()->table('s2_wechat_friend');
+ // if ($lastId !== '') { // For subsequent iterations
+ if (!is_null($lastId)) { // Check if it's not the first iteration
+ $sourceDb->where($pk, '>', $lastId);
+ }
+ $distinctWechatIds = $sourceDb->order($pk, 'ASC')
+ ->distinct(true)
+ ->limit($limit)
+ ->column($pk); // Get an array of wechatIds
+
+ if (empty($distinctWechatIds)) {
+ break; // No more wechatIds to process
+ }
+
+ // Prepare the main IODKU query for this batch of wechatIds
+ $sql = "INSERT INTO ck_wechat_account(wechatId,alias,nickname,pyInitial,quanPin,avatar,gender,region,signature,phone,country,privince,city,createTime,updateTime)
+ SELECT
+ wechatId,alias,nickname,pyInitial,quanPin,avatar,gender,region,signature,phone,country,privince,city,createTime,updateTime
+ FROM
+ s2_wechat_friend
+ WHERE wechatId IN (" . implode(',', array_fill(0, count($distinctWechatIds), '?')) . ")
+ GROUP BY wechatId -- Grouping within the selected wechatIds
+ ON DUPLICATE KEY UPDATE
+ alias=VALUES(alias),
+ nickname=VALUES(nickname),
+ pyInitial=VALUES(pyInitial),
+ quanPin=VALUES(quanPin),
+ avatar=VALUES(avatar),
+ gender=VALUES(gender),
+ region=VALUES(region),
+ signature=VALUES(signature),
+ phone=VALUES(phone),
+ country=VALUES(country),
+ privince=VALUES(privince),
+ city=VALUES(city),
+ updateTime=VALUES(updateTime)";
+
+ // The parameters for the IN clause are the distinctWechatIds themselves
+ $bindings = $distinctWechatIds;
+
+ try {
+ $affected = Db::execute($sql, $bindings);
+ $totalAffected += $affected;
+ // Log::info("syncWechatAccount: Processed batch of " . count($distinctWechatIds) . " distinct wechatIds. Affected rows: " . $affected);
+
+ // Update lastId for the next iteration
+ $lastId = end($distinctWechatIds);
+
+ if ($affected > 0) {
+ usleep(50000);
+ }
+ } catch (\Exception $e) {
+ Log::error("syncWechatAccount batch error: " . $e->getMessage() . " with wechatIds starting around " . $distinctWechatIds[0] . ". SQL: " . $sql . " Bindings: " . json_encode($bindings));
+ // Decide if you want to break or continue with the next batch
+ break; // Example: break on error
+ }
+ $iterations++;
+ } while (count($distinctWechatIds) === $limit && $iterations < $maxIterations); // Continue if we fetched a full batch
+
+ // Log::info("syncWechatAccount finished. Total affected rows: " . $totalAffected);
+ return $totalAffected;
+ }
+
+
+ public function syncWechatDeviceLoginLog()
+ {
+ try {
+ $cursor = Db::table('s2_wechat_account')
+ ->alias('a')
+ ->join(['s2_device' => 'd'], 'd.imei = a.imei')
+ ->join(['s2_company_account' => 'c'], 'c.id = d.currentAccountId')
+ ->field('d.id as deviceId, a.wechatId, a.wechatAlive as alive, c.departmentId as companyId, a.updateTime as updateTime')
+ ->cursor();
+
+ foreach ($cursor as $item) {
+
+ if (empty($item['deviceId']) || empty($item['wechatId']) || empty($item['companyId'])) {
+ continue;
+ }
+
+ $exists = Db::table('ck_device_wechat_login')
+ ->where('deviceId', $item['deviceId'])
+ ->where('wechatId', $item['wechatId'])
+ ->where('companyId', $item['companyId'])
+ ->find();
+
+ if ($exists) {
+ Db::table('ck_device_wechat_login')
+ ->where('deviceId', $item['deviceId'])
+ ->where('wechatId', $item['wechatId'])
+ ->where('companyId', $item['companyId'])
+ ->update(['alive' => $item['alive'], 'updateTime' => $item['updateTime']]);
+ } else {
+ $item['createTime'] = $item['updateTime'];
+ Db::table('ck_device_wechat_login')->insert($item);
+ }
+
+ }
+
+ return true;
+ } catch (\Exception $e) {
+ Log::error("微信好友同步任务异常: " . $e->getMessage() . ", 堆栈: " . $e->getTraceAsString());
+ return false;
+ }
+ }
+
+ /**
+ * 大数据量分批处理版本
+ * 适用于数据源非常大的情况,避免一次性加载全部数据到内存
+ * 独立脚本执行,30min 同步一次 和 流量来源的更新一起
+ *
+ * @param int $batchSize 每批处理的数据量
+ * @return int 影响的行数
+ */
+ public function syncWechatFriendToTrafficPoolBatch($batchSize = 5000)
+ {
+ Db::execute("CREATE TEMPORARY TABLE IF NOT EXISTS temp_wechat_ids (
+ wechatId VARCHAR(64) PRIMARY KEY
+ ) ENGINE=MEMORY");
+
+ Db::execute("TRUNCATE TABLE temp_wechat_ids");
+
+ // 批量插入去重的wechatId
+ Db::execute("INSERT INTO temp_wechat_ids SELECT DISTINCT wechatId FROM s2_wechat_friend");
+
+ $total = Db::table('temp_wechat_ids')->count();
+
+ $batchCount = ceil($total / $batchSize);
+ $affectedRows = 0;
+
+ try {
+ for ($i = 0; $i < $batchCount; $i++) {
+ $offset = $i * $batchSize;
+
+ $sql = "INSERT IGNORE INTO ck_traffic_pool(`identifier`, `wechatId`, `mobile`)
+ SELECT t.wechatId AS identifier, t.wechatId,
+ (SELECT phone FROM s2_wechat_friend
+ WHERE wechatId = t.wechatId LIMIT 1) AS mobile
+ FROM (
+ SELECT wechatId FROM temp_wechat_ids LIMIT {$offset}, {$batchSize}
+ ) AS t";
+
+ $currentAffected = Db::execute($sql);
+ $affectedRows += $currentAffected;
+
+ if ($i % 5 == 0) {
+ gc_collect_cycles();
+ }
+
+ usleep(30000); // 30毫秒
+ }
+ } catch (\Exception $e) {
+ \think\facade\Log::error("Error in traffic pool sync: " . $e->getMessage());
+ throw $e;
+ } finally {
+ Db::execute("DROP TEMPORARY TABLE IF EXISTS temp_wechat_ids");
+ }
+
+ return $affectedRows;
+ }
+
+
+ /**
+ * 同步/更新微信客服信息到ck_wechat_customer表
+ *
+ * @param int $batchSize 每批处理的数据量
+ * @return int 影响的行数
+ */
+ public function syncWechatCustomer($batchSize = 1000)
+ {
+ try {
+ // 1. 获取要处理的wechatId和companyId列表
+ $customerList = Db::table('ck_device_wechat_login')
+ ->field('DISTINCT wechatId, companyId')
+ ->order('id DESC')
+ ->select();
+
+ $totalAffected = 0;
+ $batchCount = ceil(count($customerList) / $batchSize);
+
+ for ($i = 0; $i < $batchCount; $i++) {
+ $batch = array_slice($customerList, $i * $batchSize, $batchSize);
+ $insertData = [];
+
+ foreach ($batch as $customer) {
+ $wechatId = $customer['wechatId'];
+ $companyId = $customer['companyId'];
+
+ if (empty($wechatId)) continue;
+
+ // 2. 获取s2_wechat_account数据
+ $accountInfo = Db::table('s2_wechat_account')
+ ->where('wechatId', $wechatId)
+ ->find();
+
+ // 3. 获取群数量 (不包含 @openim 结尾的identifier)
+ $groupCount = Db::table('ck_wechat_group_member')
+ ->where('identifier', $wechatId)
+ ->where('customerIs', 1)
+ ->where('identifier', 'not like', '%@openim')
+ ->count();
+
+ // 4. 检查记录是否已存在
+ $existingRecord = Db::table('ck_wechat_customer')
+ ->where('wechatId', $wechatId)
+ ->find();
+
+ // 5. 构建basic JSON数据
+ $basic = [];
+ if ($existingRecord && !empty($existingRecord['basic'])) {
+ $basic = json_decode($existingRecord['basic'], true) ?: [];
+ }
+
+ if (empty($basic['registerDate'])) {
+ $basic['registerDate'] = date('Y-m-d H:i:s', strtotime('-' . mt_rand(1, 150) . ' months'));
+ }
+
+ // 6. 构建activity JSON数据
+ $activity = [];
+ if ($existingRecord && !empty($existingRecord['activity'])) {
+ $activity = json_decode($existingRecord['activity'], true) ?: [];
+ }
+
+ if ($accountInfo) {
+ $activity['yesterdayMsgCount'] = $accountInfo['yesterdayMsgCount'] ?? 0;
+ $activity['sevenDayMsgCount'] = $accountInfo['sevenDayMsgCount'] ?? 0;
+ $activity['thirtyDayMsgCount'] = $accountInfo['thirtyDayMsgCount'] ?? 0;
+
+ // 计算totalMsgCount
+ if (empty($activity['totalMsgCount'])) {
+ $activity['totalMsgCount'] = $activity['thirtyDayMsgCount'];
+ } else {
+ $activity['totalMsgCount'] += $activity['yesterdayMsgCount'];
+ }
+ }
+
+ // 7. 构建friendShip JSON数据
+ $friendShip = [];
+ if ($existingRecord && !empty($existingRecord['friendShip'])) {
+ $friendShip = json_decode($existingRecord['friendShip'], true) ?: [];
+ }
+
+ if ($accountInfo) {
+ $friendShip['totalFriend'] = $accountInfo['totalFriend'] ?? 0;
+ $friendShip['maleFriend'] = $accountInfo['maleFriend'] ?? 0;
+ $friendShip['unknowFriend'] = $accountInfo['unknowFriend'] ?? 0;
+ $friendShip['femaleFriend'] = $accountInfo['femaleFriend'] ?? 0;
+ }
+ $friendShip['groupNumber'] = $groupCount;
+
+ // 8. 构建weight JSON数据 (每天只计算一次)
+ // $weight = [];
+ // if ($existingRecord && !empty($existingRecord['weight'])) {
+ // $weight = json_decode($existingRecord['weight'], true) ?: [];
+
+ // // 如果不是今天更新的,重新计算权重
+ // $lastUpdateDate = date('Y-m-d', $existingRecord['updateTime'] ?? 0);
+ // if ($lastUpdateDate !== date('Y-m-d')) {
+ // $weight = $this->calculateCustomerWeight($basic, $activity, $friendShip);
+ // }
+ // } else {
+ // $weight = $this->calculateCustomerWeight($basic, $activity, $friendShip);
+ // }
+
+ // 9. 准备更新或插入的数据
+ $data = [
+ 'wechatId' => $wechatId,
+ 'companyId' => $companyId,
+ 'basic' => json_encode($basic),
+ 'activity' => json_encode($activity),
+ 'friendShip' => json_encode($friendShip),
+ // 'weight' => json_encode($weight),
+ 'createTime' => $accountInfo['createTime'],
+ 'updateTime' => time()
+ ];
+
+ if ($existingRecord) {
+ // 更新记录
+ Db::table('ck_wechat_customer')
+ ->where('wechatId', $wechatId)
+ ->update($data);
+ } else {
+ // 插入记录
+ Db::table('ck_wechat_customer')->insert($data);
+ }
+
+ $totalAffected++;
+ }
+
+ // 释放内存
+ if ($i % 5 == 0) {
+ gc_collect_cycles();
+ }
+
+ usleep(50000); // 50毫秒短暂休息
+ }
+
+ return $totalAffected;
+ } catch (\Exception $e) {
+ Log::error("同步微信客服信息异常: " . $e->getMessage() . ", 堆栈: " . $e->getTraceAsString());
+ throw $e;
+ }
+ }
+
+ /**
+ * 计算客服权重
+ *
+ * @param array $basic 基础信息
+ * @param array $activity 活跃信息
+ * @param array $friendShip 好友关系信息
+ * @return array 权重信息
+ */
+ private function calculateCustomerWeight($basic, $activity, $friendShip)
+ {
+ // 1. 计算账号年龄权重(最大20分)
+ $ageWeight = 0;
+ if (!empty($basic['registerDate'])) {
+ $registerTime = strtotime($basic['registerDate']);
+ $accountAgeMonths = floor((time() - $registerTime) / (30 * 24 * 3600));
+ $ageWeight = min(20, floor($accountAgeMonths / 12) * 4);
+ }
+
+ // 2. 计算活跃度权重(最大30分)
+ $activityWeight = 0;
+ if (!empty($activity)) {
+ // 基于消息数计算活跃度
+ $msgScore = 0;
+ if ($activity['thirtyDayMsgCount'] > 10000) $msgScore = 15;
+ elseif ($activity['thirtyDayMsgCount'] > 5000) $msgScore = 12;
+ elseif ($activity['thirtyDayMsgCount'] > 1000) $msgScore = 8;
+ elseif ($activity['thirtyDayMsgCount'] > 500) $msgScore = 5;
+ elseif ($activity['thirtyDayMsgCount'] > 100) $msgScore = 3;
+
+ // 连续活跃天数加分(这里简化处理,实际可能需要更复杂逻辑)
+ $activeScore = min(15, $activity['yesterdayMsgCount'] > 10 ? 15 : floor($activity['yesterdayMsgCount'] / 2));
+
+ $activityWeight = $msgScore + $activeScore;
+ }
+
+ // 3. 计算限制影响权重(最大15分)
+ $restrictWeight = 15; // 默认满分,无限制
+
+ // 4. 计算实名认证权重(最大10分)
+ $realNameWeight = 0; // 简化处理,默认未实名
+
+ // 5. 计算可加友数量限制(基于好友数量,最大为5000)
+ $addLimit = 0;
+ if (!empty($friendShip['totalFriend'])) {
+ $addLimit = max(0, min(5000 - $friendShip['totalFriend'], 5000));
+ $addLimit = floor($addLimit / 1000); // 每1000个空位1分,最大5分
+ }
+
+ // 6. 计算总分(满分75+5分)
+ $scope = $ageWeight + $activityWeight + $restrictWeight + $realNameWeight;
+
+ return [
+ 'ageWeight' => $ageWeight,
+ 'activityWeight' => $activityWeight, // 注意这里修正了拼写错误
+ 'restrictWeight' => $restrictWeight,
+ 'realNameWeight' => $realNameWeight,
+ 'scope' => $scope,
+ 'addLimit' => $addLimit
+ ];
+ }
+
+ /**
+ * 同步设备信息到ck_device表
+ * 数据量不大,仅同步一次所有设备
+ *
+ * @return int 影响的行数
+ */
+ public function syncDevice()
+ {
+ try {
+ $sql = "INSERT INTO ck_device(`id`, `imei`, `model`, phone, operatingSystem, memo, alive, brand, rooted, xPosed, softwareVersion, extra, createTime, updateTime, deleteTime, companyId)
+ SELECT
+ d.id, d.imei, d.model, d.phone, d.operatingSystem, d.memo, d.alive, d.brand, d.rooted, d.xPosed, d.softwareVersion, d.extra, d.createTime, d.lastUpdateTime, d.deleteTime, a.departmentId companyId
+ FROM s2_device d
+ JOIN s2_company_account a ON d.currentAccountId = a.id
+ ON DUPLICATE KEY UPDATE
+ `model` = VALUES(`model`),
+ `phone` = VALUES(`phone`),
+ `operatingSystem` = VALUES(`operatingSystem`),
+ `memo` = VALUES(`memo`),
+ `alive` = VALUES(`alive`),
+ `brand` = VALUES(`brand`),
+ `rooted` = VALUES(`rooted`),
+ `xPosed` = VALUES(`xPosed`),
+ `softwareVersion` = VALUES(`softwareVersion`),
+ `extra` = VALUES(`extra`),
+ `updateTime` = VALUES(`updateTime`),
+ `deleteTime` = VALUES(`deleteTime`),
+ `companyId` = VALUES(`companyId`)";
+
+ $affected = Db::execute($sql);
+ return $affected;
+ } catch (\Exception $e) {
+ Log::error("同步设备信息异常: " . $e->getMessage() . ", 堆栈: " . $e->getTraceAsString());
+ return false;
+ }
+ }
+
+ public function syncTrafficSourceUser()
+ {
+ $sql = "insert into ck_traffic_source(`identifier`,companyId,`fromd`,`sourceId`,`createTime`,`type`, `status`)
+ SELECT
+ f.wechatId identifier,
+ c.departmentId companyId,
+ f.ownerNickname fromd,
+ f.ownerWechatId sourceId,
+ f.createTime createTime,
+ 1 as type,
+ CASE WHEN f.isDeleted = 1 THEN -3 ELSE 3 END as status
+ FROM
+ s2_wechat_friend f
+ LEFT JOIN s2_wechat_account a ON f.wechatAccountId = a.id
+ LEFT JOIN s2_company_account c on c.id = a.deviceAccountId
+ ORDER BY f.id DESC
+ LIMIT ?, ?
+ ON DUPLICATE KEY UPDATE
+ identifier=VALUES(identifier),
+ companyId=VALUES(companyId),
+ sourceId=VALUES(sourceId)";
+
+
+ $offset = 0;
+ $limit = 2000;
+ $usleepTime = 50000;
+ do {
+ $affected = Db::execute($sql, [$offset, $limit]);
+ $offset += $limit;
+ if ($affected > 0) {
+ usleep($usleepTime);
+ }
+ } while ($affected > 0);
+ }
+
+ public function syncTrafficSourceGroup()
+ {
+ $sql = "insert into ck_traffic_source(`identifier`,companyId,`fromd`,`sourceId`,`createTime`,`type`, `status`)
+ SELECT
+ m.wechatId identifier,
+ c.departmentId companyId,
+ r.nickname fromd,
+ m.chatroomId sourceId,
+ m.createTime createTime,
+ 2 as type,
+ CASE WHEN m.friendType = 1 THEN 3 ELSE 1 END as status
+ FROM
+ s2_wechat_chatroom_member m
+ JOIN s2_wechat_chatroom r ON m.chatroomId = r.chatroomId
+ LEFT JOIN s2_wechat_account a ON a.id = r.wechatAccountId
+ LEFT JOIN s2_company_account c on c.id = a.deviceAccountId
+ GROUP BY m.wechatId
+ ORDER BY m.id DESC
+ LIMIT ?, ?
+ ON DUPLICATE KEY UPDATE
+ identifier=VALUES(identifier),
+ companyId=VALUES(companyId),
+ sourceId=VALUES(sourceId)";
+
+
+ $offset = 0;
+ $limit = 2000;
+ $usleepTime = 50000;
+ do {
+ $affected = Db::execute($sql, [$offset, $limit]);
+ $offset += $limit;
+ if ($affected > 0) {
+ usleep($usleepTime);
+ }
+ } while ($affected > 0);
+ }
+
+ public function syncWechatGroup()
+ {
+ $sql = "insert into ck_wechat_group(`id`,`wechatAccountId`,`chatroomId`,`name`,`avatar`,`companyId`,`ownerWechatId`,`createTime`,`updateTime`,`deleteTime`)
+ SELECT
+ g.id id,
+ g.wechatAccountId wechatAccountId,
+ g.chatroomId chatroomId,
+ g.nickname name,
+ g.chatroomAvatar avatar,
+ c.departmentId companyId,
+ g.wechatAccountWechatId ownerWechatId,
+ g.createTime createTime,
+ g.updateTime updateTime,
+ g.deleteTime deleteTime
+ FROM
+ s2_wechat_chatroom g
+ LEFT JOIN s2_company_account c ON g.accountId = c.id
+ ORDER BY g.id DESC
+ LIMIT ?, ?
+ ON DUPLICATE KEY UPDATE
+ chatroomId=VALUES(chatroomId),
+ companyId=VALUES(companyId),
+ ownerWechatId=VALUES(ownerWechatId)";
+
+
+ $offset = 0;
+ $limit = 2000;
+ $usleepTime = 50000;
+ do {
+ $affected = Db::execute($sql, [$offset, $limit]);
+ $offset += $limit;
+ if ($affected > 0) {
+ usleep($usleepTime);
+ }
+ } while ($affected > 0);
+ }
+
+ public function syncWechatGroupCustomer()
+ {
+ $sql = "insert into ck_wechat_group_member(`identifier`,`chatroomId`,`companyId`,`groupId`,`createTime`)
+ SELECT
+ m.wechatId identifier,
+ g.chatroomId chatroomId,
+ c.departmentId companyId,
+ g.id groupId,
+ m.createTime createTime
+ FROM
+ s2_wechat_chatroom_member m
+ LEFT JOIN s2_wechat_chatroom g ON g.chatroomId = m.chatroomId
+ LEFT JOIN s2_company_account c ON g.accountId = c.id
+ ORDER BY m.id DESC
+ LIMIT ?, ?
+ ON DUPLICATE KEY UPDATE
+ identifier=VALUES(identifier),
+ chatroomId=VALUES(chatroomId),
+ companyId=VALUES(companyId),
+ groupId=VALUES(groupId)";
+
+ $offset = 0;
+ $limit = 2000;
+ $usleepTime = 50000;
+ do {
+ $affected = Db::execute($sql, [$offset, $limit]);
+ $offset += $limit;
+ if ($affected > 0) {
+ usleep($usleepTime);
+ }
+ } while ($affected > 0);
+ }
+
+
+ public function syncCallRecording()
+ {
+ $sql = "insert into ck_call_recording(`id`,`phone`,`isCallOut`,`companyId`,`callType`,`beginTime`,`endTime`,`createTime`)
+ SELECT
+ c.id id,
+ c.phone phone,
+ c.isCallOut isCallOut,
+ a.departmentId companyId,
+ c.callType callType,
+ c.beginTime beginTime,
+ c.endTime endTime,
+ c.callBeginTime createTime
+ FROM
+ s2_call_recording c
+ LEFT JOIN s2_company_account a ON c.deviceOwnerId = a.id
+ ORDER BY c.id DESC
+ LIMIT ?, ?
+ ON DUPLICATE KEY UPDATE
+ id=VALUES(id),
+ phone=VALUES(phone),
+ isCallOut=VALUES(isCallOut),
+ companyId=VALUES(companyId)";
+
+ $offset = 0;
+ $limit = 2000;
+ $usleepTime = 50000;
+ do {
+ $affected = Db::execute($sql, [$offset, $limit]);
+ $offset += $limit;
+ if ($affected > 0) {
+ usleep($usleepTime);
+ }
+ } while ($affected > 0);
+ }
+
+ /**
+ * 处理自动问候功能
+ * 根据不同的触发类型检查并发送问候消息
+ */
+ public function handleAutoGreetings()
+ {
+ try {
+ // 获取所有启用的问候规则
+ $rules = Db::name('kf_auto_greetings')
+ ->where(['status' => 1, 'isDel' => 0])
+ ->order('level asc, id asc')
+ ->select();
+
+ if (empty($rules)) {
+ return;
+ }
+
+ foreach ($rules as $rule) {
+ $trigger = $rule['trigger'];
+ $condition = json_decode($rule['condition'], true);
+
+ switch ($trigger) {
+ case 1: // 新好友
+ $this->handleNewFriendGreeting($rule);
+ break;
+ case 2: // 首次发消息
+ $this->handleFirstMessageGreeting($rule);
+ break;
+ case 3: // 时间触发
+ $this->handleTimeTriggerGreeting($rule, $condition);
+ break;
+ case 4: // 关键词触发
+ $this->handleKeywordTriggerGreeting($rule, $condition);
+ break;
+ case 5: // 生日触发
+ $this->handleBirthdayTriggerGreeting($rule, $condition);
+ break;
+ case 6: // 自定义
+ $this->handleCustomTriggerGreeting($rule, $condition);
+ break;
+ }
+ }
+ } catch (\Exception $e) {
+ Log::error('自动问候处理失败:' . $e->getMessage());
+ }
+ }
+
+ /**
+ * 处理新好友触发
+ */
+ private function handleNewFriendGreeting($rule)
+ {
+ // 获取最近24小时内添加的好友(避免重复处理)
+ $last24h = time() - 24 * 3600;
+
+ // 查询该用户/公司最近24小时内新添加的好友
+ // 通过 s2_wechat_account -> s2_company_account 关联获取 companyId
+ $friends = Db::table('s2_wechat_friend')
+ ->alias('wf')
+ ->join(['s2_wechat_account' => 'wa'], 'wf.wechatAccountId = wa.id')
+ ->join(['s2_company_account' => 'ca'], 'wa.deviceAccountId = ca.id')
+ ->where([
+ ['wf.isPassed', '=', 1],
+ ['wf.isDeleted', '=', 0],
+ ['wf.passTime', '>=', $last24h],
+ ['ca.departmentId', '=', $rule['companyId']],
+ ])
+ ->field('wf.id, wf.wechatAccountId')
+ ->select();
+
+ foreach ($friends as $friend) {
+ // 检查是否已经发送过问候
+ $exists = Db::name('kf_auto_greetings_record')
+ ->where([
+ 'autoId' => $rule['id'],
+ 'friendIdOrGroupId' => $friend['id'],
+ 'wechatAccountId' => $friend['wechatAccountId'],
+ ])
+ ->find();
+
+ if (!$exists) {
+ $this->sendGreetingMessage($rule, $friend['wechatAccountId'], $friend['id'], 0);
+ }
+ }
+ }
+
+ /**
+ * 处理首次发消息触发
+ */
+ private function handleFirstMessageGreeting($rule)
+ {
+ // 获取最近1小时内收到的消息
+ $last1h = time() - 3600;
+
+ // 查询消息表,找出首次发消息的好友
+ // 通过 s2_wechat_account -> s2_company_account 关联获取 companyId
+ $messages = Db::table('s2_wechat_message')
+ ->alias('wm')
+ ->join(['s2_wechat_account' => 'wa'], 'wm.wechatAccountId = wa.id')
+ ->join(['s2_company_account' => 'ca'], 'wa.deviceAccountId = ca.id')
+ ->where([
+ ['wm.isSend', '=', 0], // 接收的消息
+ ['wm.wechatChatroomId', '=', 0], // 个人消息
+ ['wm.createTime', '>=', $last1h],
+ ['ca.departmentId', '=', $rule['companyId']],
+ ])
+ ->group('wm.wechatFriendId, wm.wechatAccountId')
+ ->field('wm.wechatFriendId, wm.wechatAccountId, MIN(wm.createTime) as firstMsgTime')
+ ->select();
+
+ foreach ($messages as $msg) {
+ // 检查该好友是否之前发送过消息
+ $previousMsg = Db::table('s2_wechat_message')
+ ->where([
+ 'wechatFriendId' => $msg['wechatFriendId'],
+ 'wechatAccountId' => $msg['wechatAccountId'],
+ 'isSend' => 0,
+ ])
+ ->where('createTime', '<', $msg['firstMsgTime'])
+ ->find();
+
+ // 如果是首次发消息,且没有发送过问候
+ if (!$previousMsg) {
+ $exists = Db::name('kf_auto_greetings_record')
+ ->where([
+ 'autoId' => $rule['id'],
+ 'friendIdOrGroupId' => $msg['wechatFriendId'],
+ 'wechatAccountId' => $msg['wechatAccountId'],
+ ])
+ ->find();
+
+ if (!$exists) {
+ $this->sendGreetingMessage($rule, $msg['wechatAccountId'], $msg['wechatFriendId'], 0);
+ }
+ }
+ }
+ }
+
+ /**
+ * 处理时间触发
+ */
+ private function handleTimeTriggerGreeting($rule, $condition)
+ {
+ if (empty($condition) || !isset($condition['type'])) {
+ return;
+ }
+
+ $now = time();
+ $currentTime = date('H:i', $now);
+ $currentDate = date('m-d', $now);
+ $currentDateTime = date('m-d H:i', $now);
+ $currentWeekday = date('w', $now); // 0=周日, 1=周一, ..., 6=周六
+
+ $shouldTrigger = false;
+
+ switch ($condition['type']) {
+ case 'daily_time': // 每天固定时间
+ if ($currentTime === $condition['value']) {
+ $shouldTrigger = true;
+ }
+ break;
+
+ case 'yearly_datetime': // 每年固定日期时间
+ if ($currentDateTime === $condition['value']) {
+ $shouldTrigger = true;
+ }
+ break;
+
+ case 'fixed_range': // 固定时间段
+ if (is_array($condition['value']) && count($condition['value']) === 2) {
+ $startTime = strtotime('2000-01-01 ' . $condition['value'][0]);
+ $endTime = strtotime('2000-01-01 ' . $condition['value'][1]);
+ $currentTimeStamp = strtotime('2000-01-01 ' . $currentTime);
+
+ if ($currentTimeStamp >= $startTime && $currentTimeStamp <= $endTime) {
+ $shouldTrigger = true;
+ }
+ }
+ break;
+
+ case 'workday': // 工作日
+ // 周一到周五(1-5)
+ if ($currentWeekday >= 1 && $currentWeekday <= 5 && $currentTime === $condition['value']) {
+ $shouldTrigger = true;
+ }
+ break;
+ }
+
+ if ($shouldTrigger) {
+ // 获取该用户/公司的所有好友
+ // 通过 s2_wechat_account -> s2_company_account 关联获取 companyId
+ $friends = Db::table('s2_wechat_friend')
+ ->alias('wf')
+ ->join(['s2_wechat_account' => 'wa'], 'wf.wechatAccountId = wa.id')
+ ->join(['s2_company_account' => 'ca'], 'wa.deviceAccountId = ca.id')
+ ->where([
+ ['wf.isPassed', '=', 1],
+ ['wf.isDeleted', '=', 0],
+ ['ca.departmentId', '=', $rule['companyId']],
+ ])
+ ->field('wf.id, wf.wechatAccountId')
+ ->select();
+
+ foreach ($friends as $friend) {
+ // 检查今天是否已经发送过
+ $todayStart = strtotime(date('Y-m-d 00:00:00'));
+ $exists = Db::name('kf_auto_greetings_record')
+ ->where([
+ 'autoId' => $rule['id'],
+ 'friendIdOrGroupId' => $friend['id'],
+ 'wechatAccountId' => $friend['wechatAccountId'],
+ ])
+ ->where('createTime', '>=', $todayStart)
+ ->find();
+
+ if (!$exists) {
+ $this->sendGreetingMessage($rule, $friend['wechatAccountId'], $friend['id'], 0);
+ }
+ }
+ }
+ }
+
+ /**
+ * 处理关键词触发
+ */
+ private function handleKeywordTriggerGreeting($rule, $condition)
+ {
+ if (empty($condition) || empty($condition['keywords'])) {
+ return;
+ }
+
+ $keywords = $condition['keywords'];
+ $matchType = $condition['match_type'] ?? 'fuzzy';
+
+ // 获取最近1小时内收到的消息
+ $last1h = time() - 3600;
+
+ // 通过 s2_wechat_account -> s2_company_account 关联获取 companyId
+ $messages = Db::table('s2_wechat_message')
+ ->alias('wm')
+ ->join(['s2_wechat_account' => 'wa'], 'wm.wechatAccountId = wa.id')
+ ->join(['s2_company_account' => 'ca'], 'wa.deviceAccountId = ca.id')
+ ->where([
+ ['wm.isSend', '=', 0], // 接收的消息
+ ['wm.wechatChatroomId', '=', 0], // 个人消息
+ ['wm.msgType', '=', 1], // 文本消息
+ ['wm.createTime', '>=', $last1h],
+ ['ca.departmentId', '=', $rule['companyId']],
+ ])
+ ->field('wm.*')
+ ->select();
+
+ foreach ($messages as $msg) {
+ $content = $msg['content'] ?? '';
+
+ // 检查关键词匹配
+ $matched = false;
+ foreach ($keywords as $keyword) {
+ if ($matchType === 'exact') {
+ // 精准匹配
+ if ($content === $keyword) {
+ $matched = true;
+ break;
+ }
+ } else {
+ // 模糊匹配
+ if (strpos($content, $keyword) !== false) {
+ $matched = true;
+ break;
+ }
+ }
+ }
+
+ if ($matched) {
+ // 检查是否已经发送过问候(同一好友同一规则,1小时内只发送一次)
+ $last1h = time() - 3600;
+ $exists = Db::name('kf_auto_greetings_record')
+ ->where([
+ 'autoId' => $rule['id'],
+ 'friendIdOrGroupId' => $msg['wechatFriendId'],
+ 'wechatAccountId' => $msg['wechatAccountId'],
+ ])
+ ->where('createTime', '>=', $last1h)
+ ->find();
+
+ if (!$exists) {
+ $this->sendGreetingMessage($rule, $msg['wechatAccountId'], $msg['wechatFriendId'], 0);
+ }
+ }
+ }
+ }
+
+ /**
+ * 处理生日触发
+ */
+ private function handleBirthdayTriggerGreeting($rule, $condition)
+ {
+ if (empty($condition)) {
+ return;
+ }
+
+ // 解析condition格式
+ // 支持格式:
+ // 1. {'month': 10, 'day': 10} - 当天任何时间都可以触发
+ // 2. {'month': 10, 'day': 10, 'time': '09:00'} - 当天指定时间触发
+ // 3. {'month': 10, 'day': 10, 'time_range': ['09:00', '10:00']} - 当天时间范围内触发
+ // 兼容旧格式:['10-10'] 或 '10-10'(仅支持 MM-DD 格式,不包含年份)
+
+ $birthdayMonth = null;
+ $birthdayDay = null;
+ $birthdayTime = null;
+ $timeRange = null;
+
+ if (is_array($condition)) {
+ // 新格式:对象格式 {'month': 10, 'day': 10}
+ if (isset($condition['month']) && isset($condition['day'])) {
+ $birthdayMonth = (int)$condition['month'];
+ $birthdayDay = (int)$condition['day'];
+ $birthdayTime = $condition['time'] ?? null;
+ $timeRange = $condition['time_range'] ?? null;
+ }
+ // 兼容旧格式:['10-10'] 或 ['10-10 09:00'](仅支持 MM-DD 格式)
+ elseif (isset($condition[0])) {
+ $dateStr = $condition[0];
+ // 只接受月日格式:'10-10' 或 '10-10 09:00'
+ if (preg_match('/^(\d{1,2})-(\d{1,2})(?:\s+(\d{2}:\d{2}))?$/', $dateStr, $matches)) {
+ $birthdayMonth = (int)$matches[1];
+ $birthdayDay = (int)$matches[2];
+ if (isset($matches[3])) {
+ $birthdayTime = $matches[3];
+ }
+ }
+ }
+ } elseif (is_string($condition)) {
+ // 字符串格式:只接受 '10-10' 或 '10-10 09:00'(MM-DD 格式,不包含年份)
+ if (preg_match('/^(\d{1,2})-(\d{1,2})(?:\s+(\d{2}:\d{2}))?$/', $condition, $matches)) {
+ $birthdayMonth = (int)$matches[1];
+ $birthdayDay = (int)$matches[2];
+ if (isset($matches[3])) {
+ $birthdayTime = $matches[3];
+ }
+ }
+ }
+
+ if ($birthdayMonth === null || $birthdayDay === null || $birthdayMonth < 1 || $birthdayMonth > 12 || $birthdayDay < 1 || $birthdayDay > 31) {
+ return;
+ }
+
+ $todayMonth = (int)date('m');
+ $todayDay = (int)date('d');
+
+ // 检查今天是否是生日(只匹配月日,不匹配年份)
+ if ($todayMonth !== $birthdayMonth || $todayDay !== $birthdayDay) {
+ return;
+ }
+
+ // 如果配置了时间,检查当前时间是否匹配
+ $now = time();
+ $currentTime = date('H:i', $now);
+
+ if ($birthdayTime !== null) {
+ // 指定了具体时间,检查是否在指定时间(允许1分钟误差,避免定时任务执行时间不精确)
+ $birthdayTimestamp = strtotime('2000-01-01 ' . $birthdayTime);
+ $currentTimestamp = strtotime('2000-01-01 ' . $currentTime);
+ $diff = abs($currentTimestamp - $birthdayTimestamp);
+
+ // 如果时间差超过2分钟,不触发(允许1分钟误差)
+ if ($diff > 120) {
+ return;
+ }
+ } elseif ($timeRange !== null && is_array($timeRange) && count($timeRange) === 2) {
+ // 指定了时间范围,检查当前时间是否在范围内
+ $startTime = strtotime('2000-01-01 ' . $timeRange[0]);
+ $endTime = strtotime('2000-01-01 ' . $timeRange[1]);
+ $currentTimestamp = strtotime('2000-01-01 ' . $currentTime);
+
+ if ($currentTimestamp < $startTime || $currentTimestamp > $endTime) {
+ return;
+ }
+ }
+ // 如果没有配置时间或时间范围,则当天任何时间都可以触发
+
+ // 获取该用户/公司的所有好友
+ // 通过 s2_wechat_account -> s2_company_account 关联获取 companyId
+ $friends = Db::table('s2_wechat_friend')
+ ->alias('wf')
+ ->join(['s2_wechat_account' => 'wa'], 'wf.wechatAccountId = wa.id')
+ ->join(['s2_company_account' => 'ca'], 'wa.deviceAccountId = ca.id')
+ ->where([
+ ['wf.isPassed', '=', 1],
+ ['wf.isDeleted', '=', 0],
+ ['ca.departmentId', '=', $rule['companyId']],
+ ])
+ ->field('wf.id, wf.wechatAccountId')
+ ->select();
+
+ foreach ($friends as $friend) {
+ // 检查今天是否已经发送过
+ $todayStart = strtotime(date('Y-m-d 00:00:00'));
+ $exists = Db::name('kf_auto_greetings_record')
+ ->where([
+ 'autoId' => $rule['id'],
+ 'friendIdOrGroupId' => $friend['id'],
+ 'wechatAccountId' => $friend['wechatAccountId'],
+ ])
+ ->where('createTime', '>=', $todayStart)
+ ->find();
+
+ if (!$exists) {
+ $this->sendGreetingMessage($rule, $friend['wechatAccountId'], $friend['id'], 0);
+ }
+ }
+ }
+
+ /**
+ * 处理自定义触发
+ */
+ private function handleCustomTriggerGreeting($rule, $condition)
+ {
+ // 自定义类型需要根据具体业务需求实现
+ // 这里提供一个基础框架,可根据实际需求扩展
+ // 暂时不实现,留待后续扩展
+ }
+
+ /**
+ * 发送问候消息
+ * @param array $rule 问候规则
+ * @param int $wechatAccountId 微信账号ID
+ * @param int $friendId 好友ID
+ * @param int $groupId 群ID(0表示个人消息)
+ */
+ private function sendGreetingMessage($rule, $wechatAccountId, $friendId, $groupId = 0)
+ {
+ try {
+ $content = $rule['content'];
+
+ // 创建记录
+ $recordId = Db::name('kf_auto_greetings_record')->insertGetId([
+ 'autoId' => $rule['id'],
+ 'userId' => $rule['userId'],
+ 'companyId' => $rule['companyId'],
+ 'wechatAccountId' => $wechatAccountId,
+ 'friendIdOrGroupId' => $friendId,
+ 'isSend' => 0,
+ 'sendTime' => 0,
+ 'receiveTime' => 0,
+ 'createTime' => time(),
+ ]);
+
+ // 发送消息(文本消息)
+ $username = Env::get('api.username', '');
+ $password = Env::get('api.password', '');
+ $toAccountId = '';
+ if (!empty($username) || !empty($password)) {
+ $toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId');
+ }
+
+ $wsController = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]);
+
+ $sendTime = time();
+ $result = $wsController->sendPersonal([
+ 'wechatFriendId' => $friendId,
+ 'wechatAccountId' => $wechatAccountId,
+ 'msgType' => 1, // 文本消息
+ 'content' => $content,
+ ]);
+
+ $isSend = 0;
+ $receiveTime = 0;
+
+ // 解析返回结果
+ $resultData = json_decode($result, true);
+ if (!empty($resultData) && $resultData['code'] == 200) {
+ $isSend = 1;
+ $receiveTime = time(); // 简化处理,实际应该从返回结果中获取
+ }
+
+ // 更新记录
+ Db::name('kf_auto_greetings_record')
+ ->where('id', $recordId)
+ ->update([
+ 'isSend' => $isSend,
+ 'sendTime' => $sendTime,
+ 'receiveTime' => $receiveTime,
+ ]);
+
+ // 更新规则使用次数
+ Db::name('kf_auto_greetings')
+ ->where('id', $rule['id'])
+ ->setInc('usageCount');
+
+ } catch (\Exception $e) {
+ Log::error('发送问候消息失败:' . $e->getMessage() . ',规则ID:' . $rule['id']);
+ }
+ }
+
+}
diff --git a/extend/WeChatDeviceApi/Contracts/WeChatServiceInterface.php b/extend/WeChatDeviceApi/Contracts/WeChatServiceInterface.php
new file mode 100644
index 0000000..182a18b
--- /dev/null
+++ b/extend/WeChatDeviceApi/Contracts/WeChatServiceInterface.php
@@ -0,0 +1,67 @@
+config = $config ?: Config::get('wechat_device_api.');
+ if (empty($this->config)) {
+ throw new \InvalidArgumentException("WeChat Device API configuration not found.");
+ }
+ }
+
+ /**
+ * 获取指定的适配器实例
+ *
+ * @param string|null $name 适配器名称 (例如 'vendor_a', 'vendor_b'),null 则使用默认
+ * @return WeChatServiceInterface
+ * @throws \InvalidArgumentException
+ */
+ public function adapter(string $name = null): WeChatServiceInterface
+ {
+ $name = $name ?: $this->getDefaultAdapterName();
+
+ if (!isset($this->config['adapters'][$name])) {
+ throw new \InvalidArgumentException("Adapter [{$name}] configuration not found.");
+ }
+
+ if (!isset($this->adapters[$name])) {
+ $this->adapters[$name] = $this->createAdapter($name);
+ }
+
+ return $this->adapters[$name];
+ }
+
+ /**
+ * 创建适配器实例
+ *
+ * @param string $name
+ * @return WeChatServiceInterface
+ */
+ protected function createAdapter(string $name): WeChatServiceInterface
+ {
+ $adapterConfig = $this->config['adapters'][$name];
+ $driverClass = $adapterConfig['driver'] ?? null;
+
+ if (!$driverClass || !class_exists($driverClass)) {
+ throw new \InvalidArgumentException("Driver class for adapter [{$name}] not found or not specified.");
+ }
+
+ $adapterInstance = new $driverClass($adapterConfig);
+
+ if (!$adapterInstance instanceof WeChatServiceInterface) {
+ throw new \LogicException("Driver class [{$driverClass}] must implement WeChatServiceInterface.");
+ }
+
+ return $adapterInstance;
+ }
+
+ /**
+ * 获取默认适配器名称
+ *
+ * @return string
+ */
+ public function getDefaultAdapterName(): string
+ {
+ return $this->config['default_adapter'] ?? '';
+ }
+
+ /**
+ * 动态调用默认适配器的方法
+ *
+ * @param string $method
+ * @param array $parameters
+ * @return mixed
+ */
+ public function __call(string $method, array $parameters)
+ {
+ return $this->adapter()->{$method}(...$parameters);
+ }
+}
\ No newline at end of file
diff --git a/extend/library/ClassTable.php b/extend/library/ClassTable.php
new file mode 100644
index 0000000..939eeb4
--- /dev/null
+++ b/extend/library/ClassTable.php
@@ -0,0 +1,243 @@
+{$tag} = (array)($this->{$tag} ?? []);
+
+ // 将值追加到数组中。
+ $this->{$tag} = array_merge($this->{$tag}, $collection);
+ }
+
+ /**
+ * 返回标签的元素
+ *
+ * @param string $tag
+ * @return array|null
+ */
+ private function getTagElements(string $tag): ?array
+ {
+ return $this->{'__tag__' . $tag} ?? null;
+ }
+
+ /**
+ * 创建并返回一个对象
+ *
+ * @param object|string $class
+ * @param array $parameters
+ * @return object
+ * @throws \Throwable
+ */
+ private function _getInstance($class, $parameters = []): object
+ {
+ if ($class instanceof \Closure) {
+ return $class();
+ }
+
+ if (is_object($class)) {
+ return $class;
+ }
+
+ if (class_exists(strval($class))) {
+ // 创建该类的新实例
+ return method_exists($class, 'newInstance') ? $class::newInstance(...$parameters) : new $class(...$parameters);
+ }
+
+ throw new \Exception(static::class . ': 调用了未定义的函数', 532341);
+ }
+
+ /**
+ * 单例
+ *
+ * @param $container
+ * @return void
+ */
+ public static function getSelfInstance(): CallMapInterface
+ {
+ if (!self::$selfInstance instanceof self) {
+ self::$selfInstance = new self();
+ }
+
+ return self::$selfInstance;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function bind($alias, $instance = null, string $tag = null): void
+ {
+ if (is_array($alias)) {
+ foreach ($alias as $name => $instance) {
+ $this->hashTable[$name] = $instance;
+ }
+
+ $this->latest = array_keys($alias);
+
+ if ($tag) {
+ $this->tag($tag, $this->latest);
+ }
+ } else {
+ $this->hashTable[$alias] = $instance;
+ $this->latest = [$alias];
+
+ if ($tag) {
+ $this->tag($tag, $this->latest);
+ }
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function dealloc($alias): void
+ {
+ dealloc($this->hashTable, $alias);
+
+ $insects = array_intersect((array)$alias, $this->latest);
+
+ if ($insects) {
+ $this->latest = array_diff($this->latest, $insects);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function revokeShared($alias): void
+ {
+ $instance = $this->alias($alias);
+
+ $this->bind($alias, get_class($instance));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function tag(string $tag, array $lastElements = []): CallMapInterface
+ {
+ $this->_tag('__tag__' . $tag, ($lastElements ?: $this->latest));
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getInstance($class, ...$parameters): object
+ {
+ $name = is_object($class) ? get_class($class) : $class;
+
+ if (!$this->has($name)) {
+ return $this->hashTable[$name] = $this->_getInstance($class, $parameters);
+ }
+
+ return $this->_getInstance($this->hashTable[$name], $parameters);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function copy($class, string $name = null): object
+ {
+ $instance = clone $this->getInstance($class);
+
+ if (!is_null($name)) {
+ $this->bind($name, $instance);
+ }
+
+ return $instance;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getClassByTag(string $tag): ?array
+ {
+ $elements = $this->getTagElements($tag);
+
+ if ($elements) {
+ foreach ($elements as $alias) {
+ if ($this->has($alias)) {
+ $consequences[$alias] = $this->hashTable[$alias];
+ }
+ }
+ }
+
+ return $consequences ?? null;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function has(string $alias): bool
+ {
+ return isset($this->hashTable[$alias]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function detect(string $precept, $class, string $alias = null): ?object
+ {
+ // Scheme 是临时对象,不会被保存
+ if (!$this->has($precept)) {
+ // 如果想共享该对象,可以调用 bind 方法将其注册到哈希表中
+ $instance = $this->_getInstance($class);
+
+ if ($alias) {
+ $this->hashTable[$alias] = $instance;
+ }
+
+ return $instance;
+ }
+
+ return $this->hashTable[$precept];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function alias($alias, array $parameters = []): ?object
+ {
+ return $this->getInstance($alias, ...$parameters);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getShared($alias, array $parameters = []): ?object
+ {
+ $instance = $this->alias($alias, $parameters);
+
+ $this->bind($alias, $instance);
+
+ return $instance;
+ }
+}
diff --git a/extend/library/Interfaces/CallMap.php b/extend/library/Interfaces/CallMap.php
new file mode 100644
index 0000000..f58d5bc
--- /dev/null
+++ b/extend/library/Interfaces/CallMap.php
@@ -0,0 +1,106 @@
+ $code,
+ 'msg' => $msg,
+ 'data' => $data
+ ]);
+ }
+
+ /**
+ * 错误响应
+ *
+ * @param string $msg
+ * @param int $code
+ * @param mixed $data
+ * @return JsonResponse
+ */
+ public static function error(string $msg = 'fail', int $code = 400, $data = []): JsonResponse
+ {
+ return json([
+ 'code' => $code,
+ 'msg' => $msg,
+ 'data' => $data
+ ]);
+ }
+
+ /**
+ * 未授权响应
+ *
+ * @param string $msg 错误消息
+ * @return JsonResponse
+ */
+ public static function unauthorized(string $msg = 'unauthorized access'): JsonResponse
+ {
+ return static::error($msg, 401);
+ }
+
+ /**
+ * 禁止访问响应
+ *
+ * @param string $msg 错误消息
+ * @return JsonResponse
+ */
+ public static function forbidden(string $msg = 'access denied'): JsonResponse
+ {
+ return static::error($msg, 403);
+ }
+}
\ No newline at end of file
diff --git a/extend/library/s2/CurlHandle.php b/extend/library/s2/CurlHandle.php
new file mode 100644
index 0000000..0d10d51
--- /dev/null
+++ b/extend/library/s2/CurlHandle.php
@@ -0,0 +1,156 @@
+baseUrl = Env::get('api.wechat_url');
+ }
+
+ /**
+ * CurlHandle constructor.
+ */
+ public function __construct()
+ {
+ $this->getBaseUrl();
+ }
+
+ /**
+ * 设置头部
+ *
+ * @param array $headerData 头部数组
+ * @param string $authorization
+ * @param string $type 类型 默认json (json,plain)
+ * @return array
+ */
+ public function setHeader($key, $value): CurlHandle
+ {
+ if (is_array($key)) {
+ $this->header = array_merge($this->header, $key);
+ } else {
+ $this->header[$key] = $value;
+ }
+
+ return $this;
+ }
+
+ private function getHearder(): array
+ {
+ $header = [];
+
+ foreach ($this->header as $key => $value) {
+ $header[] = $key . ':' . $value;
+ }
+
+ return $header;
+ }
+
+ public function setMethod(string $method): CurlHandle
+ {
+ $this->method = $method;
+
+ return $this;
+ }
+
+ /**
+ * @param string $baseUrl
+ * @return $this
+ */
+ public function setBaseUrl(string $baseUrl): CurlHandle
+ {
+ $this->baseUrl = $baseUrl;
+
+ return $this;
+ }
+
+ /**
+ * @param string $url 请求的链接
+ * @param array $params 请求附带的参数
+ * @param string $method 请求的方式, 支持GET, POST, PUT, DELETE等
+ * @param array $header 头部
+ * @param string $type 数据类型,支持dataBuild、json等
+ * @return bool|string
+ */
+ public function send($url, $params = [], $type = 'dataBuild')
+ {
+ $str = '';
+ if (!empty($url)) {
+ try {
+ $ch = curl_init();
+ $method = $this->method;
+ $url = $this->baseUrl . $url;
+
+ // 处理GET请求的参数
+ if (strtoupper($method) == 'GET' && !empty($params)) {
+ $url = $url . '?' . dataBuild($params);
+ }
+
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_HEADER, 0);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($ch, CURLOPT_TIMEOUT, 30); //30秒超时
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, $this->getHearder());
+
+ // 处理不同的请求方法
+ if (strtoupper($method) != 'GET') {
+ // 设置请求方法
+ curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($method));
+
+ // 处理参数格式
+ if ($type == 'dataBuild') {
+ $params = dataBuild($params);
+ } elseif ($type == 'json') {
+ $params = json_encode($params);
+ } else {
+ $params = dataBuild($params);
+ }
+
+ // 设置请求体
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
+ }
+
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); //是否验证对等证书,1则验证,0则不验证
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
+ $str = curl_exec($ch);
+ if(curl_errno($ch)) {
+ echo 'Curl error: ' .curl_errno($ch) . ':' . curl_error($ch);
+ }
+ curl_close($ch);
+ } catch (Exception $e) {
+ $str = '';
+ }
+ }
+
+ return $str;
+ }
+
+ /**
+ * 单例
+ *
+ * @return static|null
+ */
+ public static function getInstant()
+ {
+ if (self::$instant instanceof self) {
+ return self::$instant;
+ }
+
+ return new static();
+ }
+}
diff --git a/extend/library/s2/interfaces/AccountInterface.php b/extend/library/s2/interfaces/AccountInterface.php
new file mode 100644
index 0000000..7fe2807
--- /dev/null
+++ b/extend/library/s2/interfaces/AccountInterface.php
@@ -0,0 +1,23 @@
+ $params['accountId'] ?? '',
+ 'keyword' => $params['keyword'] ?? '',
+ 'imei' => $params['imei'] ?? '',
+ 'groupId' => $params['groupId'] ?? '',
+ 'brand' => $params['brand'] ?? '',
+ 'model' => $params['model'] ?? '',
+ 'deleteType' => $params['deleteType'] ?? 'unDeleted',
+ 'operatingSystem' => $params['operatingSystem'] ?? '',
+ 'softwareVersion' => $params['softwareVersion'] ?? '',
+ 'phoneAppVersion' => $params['phoneAppVersion'] ?? '',
+ 'recorderVersion' => $params['recorderVersion'] ?? '',
+ 'contactsVersion' => $params['contactsVersion'] ?? '',
+ 'rooted' => $params['rooted'] ?? '',
+ 'xPosed' => $params['xPosed'] ?? '',
+ 'alive' => $params['alive'] ?? '',
+ 'hasWechat' => $params['hasWechat'] ?? '',
+ 'departmentId' => $params['departmentId'] ?? '',
+ 'pageIndex' => $params['pageIndex'] ?? 0,
+ 'pageSize' => $params['pageSize'] ?? 20
+ ];
+
+ $JWT = AuthService::getSystemAuthorization();
+ $result = CurlHandle::getInstant()
+ ->setHeader('Content-Type', 'text/plain')
+ ->setHeader('authorization', 'bearer ' . $JWT)
+ ->setMethod('get')
+ ->send('api/Account/myTenantPageAccounts', $params);
+
+ $response = handleApiResponse($result);
+ // 保存数据到数据库
+ if (!empty($response['results'])) {
+ foreach ($response['results'] as $item) {
+ $this->saveData($item);
+ }
+ }
+ return json_encode(['code' => 200, 'msg' => '获取公司账号列表成功', 'data' => $response]);
+
+ } catch (\Exception $e) {
+ return json_encode(['code' => 500, 'msg' => '获取公司账号列表失败:' . $e->getMessage()]);
+ }
+ }
+
+
+ private function saveData($item)
+ {
+ $data = [
+ 'id' => isset($item['id']) ? $item['id'] : '',
+ 'userName' => isset($item['userName']) ? $item['userName'] : '',
+ 'nickname' => isset($item['nickname']) ? $item['nickname'] : '',
+ 'realName' => isset($item['realName']) ? $item['realName'] : '',
+ 'groupName' => isset($item['groupName']) ? $item['groupName'] : '',
+ 'wechatAccounts' => isset($item['wechatAccounts']) ? json_encode($item['wechatAccounts']) : json_encode([]),
+ 'alive' => isset($item['alive']) ? $item['alive'] : false,
+ 'lastAliveTime' => isset($item['lastAliveTime']) ? $item['lastAliveTime'] : null,
+ 'tenantId' => isset($item['tenantId']) ? $item['tenantId'] : 0,
+ 'groupId' => isset($item['groupId']) ? $item['groupId'] : 0,
+ 'currentAccountId' => isset($item['currentAccountId']) ? $item['currentAccountId'] : 0,
+ 'imei' => $item['imei'],
+ 'memo' => isset($item['memo']) ? $item['memo'] : '',
+ 'createTime' => isset($item['createTime']) ? strtotime($item['createTime']) : 0,
+ 'isDeleted' => isset($item['isDeleted']) ? $item['isDeleted'] : false,
+ 'deletedAndStop' => isset($item['deletedAndStop']) ? $item['deletedAndStop'] : false,
+ 'deleteTime' => empty($item['isDeleted']) ? 0 : strtotime($item['deleteTime']),
+ 'rooted' => isset($item['rooted']) ? $item['rooted'] : false,
+ 'xPosed' => isset($item['xPosed']) ? $item['xPosed'] : false,
+ 'brand' => isset($item['brand']) ? $item['brand'] : '',
+ 'model' => isset($item['model']) ? $item['model'] : '',
+ 'operatingSystem' => isset($item['operatingSystem']) ? $item['operatingSystem'] : '',
+ 'softwareVersion' => isset($item['softwareVersion']) ? $item['softwareVersion'] : '',
+ 'extra' => isset($item['extra']) ? json_encode($item['extra']) : json_encode([]),
+ 'phone' => isset($item['phone']) ? $item['phone'] : '',
+ 'lastUpdateTime' => isset($item['lastUpdateTime']) ? ($item['lastUpdateTime'] == '0001-01-01T00:00:00' ? 0 : strtotime($item['lastUpdateTime'])) : 0
+ ];
+
+ // 使用imei作为唯一性判断
+ $device = DeviceModel::where('id', $item['id'])->find();
+
+ if ($device) {
+ $device->save($data);
+ } else {
+
+ // autoLike:自动点赞
+ // momentsSync:朋友圈同步
+ // autoCustomerDev:自动开发客户
+ // groupMessageDeliver:群消息推送
+ // autoGroup:自动建群
+
+ $data['taskConfig'] = json_encode([
+ 'autoLike' => true,
+ 'momentsSync' => true,
+ 'autoCustomerDev' => true,
+ 'groupMessageDeliver' => true,
+ 'autoGroup' => true,
+ ]);
+ DeviceModel::create($data);
+ }
+ }
+
+ /**
+ * 保存设备数据
+ * @param array $item 设备数据
+ * @return bool
+ */
+ public function saveDevice($item)
+ {
+ try {
+ $data = [
+ 'id' => isset($item['id']) ? $item['id'] : '',
+ 'userName' => isset($item['userName']) ? $item['userName'] : '',
+ 'nickname' => isset($item['nickname']) ? $item['nickname'] : '',
+ 'realName' => isset($item['realName']) ? $item['realName'] : '',
+ 'groupName' => isset($item['groupName']) ? $item['groupName'] : '',
+ 'wechatAccounts' => isset($item['wechatAccounts']) ? json_encode($item['wechatAccounts']) : json_encode([]),
+ 'alive' => isset($item['alive']) ? $item['alive'] : false,
+ 'lastAliveTime' => isset($item['lastAliveTime']) ? $item['lastAliveTime'] : null,
+ 'tenantId' => isset($item['tenantId']) ? $item['tenantId'] : 0,
+ 'groupId' => isset($item['groupId']) ? $item['groupId'] : 0,
+ 'currentAccountId' => isset($item['currentAccountId']) ? $item['currentAccountId'] : 0,
+ 'imei' => $item['imei'],
+ 'memo' => isset($item['memo']) ? $item['memo'] : '',
+ 'createTime' => isset($item['createTime']) ? strtotime($item['createTime']) : 0,
+ 'isDeleted' => isset($item['isDeleted']) ? $item['isDeleted'] : false,
+ 'deletedAndStop' => isset($item['deletedAndStop']) ? $item['deletedAndStop'] : false,
+ 'deleteTime' => empty($item['isDeleted']) ? 0 : strtotime($item['deleteTime']),
+ 'rooted' => isset($item['rooted']) ? $item['rooted'] : false,
+ 'xPosed' => isset($item['xPosed']) ? $item['xPosed'] : false,
+ 'brand' => isset($item['brand']) ? $item['brand'] : '',
+ 'model' => isset($item['model']) ? $item['model'] : '',
+ 'operatingSystem' => isset($item['operatingSystem']) ? $item['operatingSystem'] : '',
+ 'softwareVersion' => isset($item['softwareVersion']) ? $item['softwareVersion'] : '',
+ 'extra' => isset($item['extra']) ? json_encode($item['extra']) : json_encode([]),
+ 'phone' => isset($item['phone']) ? $item['phone'] : '',
+ 'lastUpdateTime' => isset($item['lastUpdateTime']) ? ($item['lastUpdateTime'] == '0001-01-01T00:00:00' ? 0 : strtotime($item['lastUpdateTime'])) : 0
+ ];
+
+ // 使用ID作为唯一性判断
+ $device = DeviceModel::where('id', $item['id'])->find();
+
+ if ($device) {
+ $device->save($data);
+ } else {
+ $data['taskConfig'] = json_encode([
+ 'autoLike' => true,
+ 'momentsSync' => true,
+ 'autoCustomerDev' => true,
+ 'groupMessageDeliver' => true,
+ 'autoGroup' => true,
+ ]);
+ DeviceModel::create($data);
+ }
+
+ return true;
+ } catch (\Exception $e) {
+ Log::error('保存设备数据失败:' . $e->getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * 保存设备分组数据
+ * @param array $item 设备分组数据
+ * @return bool
+ */
+ public function saveDeviceGroup($item)
+ {
+ try {
+ $data = [
+ 'id' => $item['id'],
+ 'tenantId' => $item['tenantId'],
+ 'groupName' => $item['groupName'],
+ 'groupMemo' => $item['groupMemo'],
+ 'count' => isset($item['count']) ? $item['count'] : 0,
+ 'createTime' => $item['createTime'] == '0001-01-01T00:00:00' ? 0 : strtotime($item['createTime'])
+ ];
+
+ // 使用ID作为唯一性判断
+ $group = DeviceGroupModel::where('id', $item['id'])->find();
+
+ if ($group) {
+ $group->save($data);
+ } else {
+ DeviceGroupModel::create($data);
+ }
+
+ return true;
+ } catch (\Exception $e) {
+ Log::error('保存设备分组数据失败:' . $e->getMessage());
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/fonts.ttf b/fonts.ttf
new file mode 100644
index 0000000..807b78f
Binary files /dev/null and b/fonts.ttf differ
diff --git a/public/.htaccess b/public/.htaccess
new file mode 100644
index 0000000..e69de29
diff --git a/public/admin/.htaccess b/public/admin/.htaccess
new file mode 100644
index 0000000..9c856c3
--- /dev/null
+++ b/public/admin/.htaccess
@@ -0,0 +1,8 @@
+
+ Options +FollowSymlinks -Multiviews
+ RewriteEngine On
+
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteRule ^admin/(.*)$ /admin/index.html/$1 [QSA,PT,L]
+
diff --git a/public/api_test.php b/public/api_test.php
new file mode 100644
index 0000000..9f5ccb7
--- /dev/null
+++ b/public/api_test.php
@@ -0,0 +1,76 @@
+API测试工具';
+echo '';
+
+// 如果有URL参数,发送API请求
+if (!empty($url)) {
+ // 构建完整URL
+ $fullUrl = 'http://' . $_SERVER['HTTP_HOST'] . '/' . $url;
+
+ // 初始化cURL
+ $ch = curl_init($fullUrl);
+
+ // 设置cURL选项
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+
+ // 设置请求方法
+ if ($method == 'POST') {
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($jsonData));
+ }
+
+ // 设置请求头
+ $headers = ['Content-Type: application/json'];
+ if (!empty($token)) {
+ $headers[] = 'Authorization: ' . $token;
+ }
+ curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
+
+ // 执行请求
+ $result = curl_exec($ch);
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+
+ // 检查是否有错误
+ if (curl_errno($ch)) {
+ echo '' . htmlspecialchars(curl_error($ch)) . ''; + } else { + echo '
' . htmlspecialchars($result) . ''; + + // 尝试解析JSON + $jsonResult = json_decode($result, true); + if (json_last_error() === JSON_ERROR_NONE) { + echo '
' . htmlspecialchars(json_encode($jsonResult, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) . ''; + } + } + + // 关闭cURL资源 + curl_close($ch); +} \ No newline at end of file diff --git a/public/doc/api_v1.md b/public/doc/api_v1.md new file mode 100644 index 0000000..95c8643 --- /dev/null +++ b/public/doc/api_v1.md @@ -0,0 +1,413 @@ +# 对外获客线索上报接口文档(V1) + +## 一、接口概述 + +- **接口名称**:对外获客线索上报接口 +- **接口用途**:供第三方系统向【存客宝】上报客户线索(手机号 / 微信号等),用于后续的跟进、标签管理和画像分析。 +- **接口协议**:HTTP +- **请求方式**:`POST` +- **请求地址**: `http://ckbapi.quwanzhi.com/v1/api/scenarios` + +> 具体 URL 以实际环境配置为准。 + +- **数据格式**: + - 推荐:`application/json` + - 兼容:`application/x-www-form-urlencoded` +- **字符编码**:`UTF-8` + +--- + +## 二、鉴权与签名 + +### 2.1 必填鉴权字段 + +| 字段名 | 类型 | 必填 | 说明 | +|-------------|--------|------|---------------------------------------| +| `apiKey` | string | 是 | 分配给第三方的接口密钥(每个任务唯一)| +| `sign` | string | 是 | 签名值 | +| `timestamp` | int | 是 | 秒级时间戳(与服务器时间差不超过 5 分钟) | + +### 2.2 时间戳校验 + +服务器会校验 `timestamp` 是否在当前时间前后 **5 分钟** 内: + +- 通过条件:`|server_time - timestamp| <= 300` +- 超出范围则返回:`请求已过期` + +### 2.3 签名生成规则 + +接口采用自定义签名机制。**签名字段为 `sign`,生成步骤如下:** + +假设本次请求的所有参数为 `params`,其中包括业务参数 + `apiKey` + `timestamp` + `sign` + 可能存在的 `portrait` 对象。 + +#### 第一步:移除特定字段 + +从 `params` 中移除以下字段: + +- `sign` —— 自身不参与签名 +- `apiKey` —— 不参与参数拼接,仅在最后一步参与二次 MD5 +- `portrait` —— 整个画像对象不参与签名(即使内部还有子字段) + +> 说明:`portrait` 通常是一个 JSON 对象,字段较多,为避免签名实现复杂且双方难以对齐,统一不参与签名。 + +#### 第二步:移除空值字段 + +从剩余参数中,移除值为: + +- `null` +- 空字符串 `''` + +的字段,这些字段不参与签名。 + +#### 第三步:按参数名升序排序 + +对剩余参数按**参数名(键名)升序排序**,排序规则为标准的 ASCII 升序: + +```text +例如: name, phone, source, timestamp +``` + +#### 第四步:拼接参数值 + +将排序后的参数 **只取“值”**,按顺序直接拼接为一个字符串,中间不加任何分隔符: + +- 示例: + 排序后参数为: + + ```text + name = 张三 + phone = 13800000000 + source = 微信广告 + timestamp = 1710000000 + ``` + + 则拼接: + + ```text + stringToSign = "张三13800000000微信广告1710000000" + ``` + +#### 第五步:第一次 MD5 + +对上一步拼接得到的字符串做一次 MD5: + +\[ +\text{firstMd5} = \text{MD5}(\text{stringToSign}) +\] + +#### 第六步:拼接 apiKey 再次 MD5 + +将第一步的结果与 `apiKey` 直接拼接,再做一次 MD5,得到最终签名值: + +\[ +\text{sign} = \text{MD5}(\text{firstMd5} + \text{apiKey}) +\] + +#### 第七步:放入请求 + +将第六步得到的 `sign` 填入请求参数中的 `sign` 字段即可。 + +> 建议: +> - 使用小写 MD5 字符串(双方约定统一即可)。 +> - 请确保参与签名的参数与最终请求发送的参数一致(包括是否传空值)。 + +### 2.4 签名示例(PHP 伪代码) + +```php +$params = [ + 'apiKey' => 'YOUR_API_KEY', + 'timestamp' => '1710000000', + 'phone' => '13800000000', + 'name' => '张三', + 'source' => '微信广告', + 'remark' => '通过H5落地页留资', + // 'portrait' => [...], // 如有画像,这里会存在,但不参与签名 + // 'sign' => '待生成', +]; + +// 1. 去掉 sign、apiKey、portrait +unset($params['sign'], $params['apiKey'], $params['portrait']); + +// 2. 去掉空值 +$params = array_filter($params, function($value) { + return !is_null($value) && $value !== ''; +}); + +// 3. 按键名升序排序 +ksort($params); + +// 4. 拼接参数值 +$stringToSign = implode('', array_values($params)); + +// 5. 第一次 MD5 +$firstMd5 = md5($stringToSign); + +// 6. 第二次 MD5(拼接 apiKey) +$apiKey = 'YOUR_API_KEY'; +$sign = md5($firstMd5 . $apiKey); + +// 将 $sign 作为字段发送 +$params['sign'] = $sign; +``` + +--- + +## 三、请求参数说明 + +### 3.1 主标识字段(至少传一个) + +| 字段名 | 类型 | 必填 | 说明 | +|-----------|--------|------|-------------------------------------------| +| `wechatId`| string | 否 | 微信号,存在时优先作为主标识 | +| `phone` | string | 否 | 手机号,当 `wechatId` 为空时用作主标识 | + +### 3.2 基础信息字段 + +| 字段名 | 类型 | 必填 | 说明 | +|------------|--------|------|-------------------------| +| `name` | string | 否 | 客户姓名 | +| `source` | string | 否 | 线索来源描述,如“百度推广”、“抖音直播间” | +| `remark` | string | 否 | 备注信息 | +| `tags` | string | 否 | 逗号分隔的“微信标签”,如:`"高意向,电商,女装"` | +| `siteTags` | string | 否 | 逗号分隔的“站内标签”,用于站内进一步细分 | + + +### 3.3 用户画像字段 `portrait`(可选) + +`portrait` 为一个对象(JSON),用于记录用户的行为画像数据。 + +#### 3.3.1 基本示例 + +```json +"portrait": { + "type": 1, + "source": 1, + "sourceData": { + "age": 28, + "gender": "female", + "city": "上海", + "productId": "P12345", + "pageUrl": "https://example.com/product/123" + }, + "remark": "画像-基础属性", + "uniqueId": "user_13800000000_20250301_001" +} +``` + +#### 3.3.2 字段详细说明 + +| 字段名 | 类型 | 必填 | 说明 | +|-----------------------|--------|------|----------------------------------------| +| `portrait.type` | int | 否 | 画像类型,枚举值:
响应结果将显示在这里
+'; + print_r(json_decode($result, true)); + echo ''; +} + +// 关闭cURL资源 +curl_close($ch); \ No newline at end of file diff --git a/route/route.php b/route/route.php new file mode 100644 index 0000000..9f6b71c --- /dev/null +++ b/route/route.php @@ -0,0 +1,45 @@ + +// +---------------------------------------------------------------------- + +use think\facade\Route; + + // 允许跨域 + header('Access-Control-Allow-Origin: ' . (isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '*')); + header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, PATCH'); + header('Access-Control-Allow-Headers: Cookie, Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With, X-Token, X-Api-Token'); + header('Access-Control-Max-Age: 1728000'); + header('Access-Control-Allow-Credentials: true'); + +// 加载Store模块路由配置 +include __DIR__ . '/../application/api/config/route.php'; + +// 加载Common模块路由配置 +include __DIR__ . '/../application/common/config/route.php'; + +// 加载Cunkebao模块路由配置 +include __DIR__ . '/../application/cunkebao/config/route.php'; + +// 加载Store模块路由配置 +include __DIR__ . '/../application/store/config/route.php'; + +// 加载Superadmin模块路由配置 +include __DIR__ . '/../application/superadmin/config/route.php'; + +// 加载CozeAI模块路由配置 +include __DIR__ . '/../application/cozeai/config/route.php'; + +// 加载AI模块路由配置 +include __DIR__ . '/../application/ai/config/route.php'; + +// 加载存客宝模块路由配置 +include __DIR__ . '/../application/chukebao/config/route.php'; + +return []; diff --git a/sql.sql b/sql.sql new file mode 100644 index 0000000..633756e --- /dev/null +++ b/sql.sql @@ -0,0 +1,2523 @@ +/* + Navicat Premium Data Transfer + + Source Server : kr_存客宝 + Source Server Type : MySQL + Source Server Version : 50736 + Source Host : 56b4c23f6853c.gz.cdb.myqcloud.com:14413 + Source Schema : cunkebao_v3 + + Target Server Type : MySQL + Target Server Version : 50736 + File Encoding : 65001 + + Date: 16/12/2025 16:39:24 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for ck_administrator_permissions +-- ---------------------------- +DROP TABLE IF EXISTS `ck_administrator_permissions`; +CREATE TABLE `ck_administrator_permissions` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自动ID', + `adminId` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '超管用户ID', + `permissions` json NULL COMMENT '权限对象', + `createTime` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '更新时间', + `deleteTime` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '超级管理员权限配置表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_administrators +-- ---------------------------- +DROP TABLE IF EXISTS `ck_administrators`; +CREATE TABLE `ck_administrators` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `username` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '管理员名字', + `account` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '登录账号', + `password` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '登录密码', + `status` tinyint(3) UNSIGNED NULL DEFAULT 1 COMMENT '1->可用,0->禁用', + `lastLoginTime` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '最近登录时间', + `lastLoginIp` char(18) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '最近登录ip', + `authId` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '权限id', + `createTime` int(10) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(10) NULL DEFAULT NULL COMMENT '更新时间', + `deleteTime` int(11) NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '超级管理员表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_ai_knowledge_base +-- ---------------------------- +DROP TABLE IF EXISTS `ck_ai_knowledge_base`; +CREATE TABLE `ck_ai_knowledge_base` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `typeId` int(11) NULL DEFAULT 1 COMMENT '类型id', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称', + `label` json NULL COMMENT '标签', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + `isDel` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(11) NOT NULL DEFAULT 0 COMMENT '删除时间', + `documentId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '知识库文件id', + `fileUrl` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件地址', + `size` int(10) NULL DEFAULT NULL COMMENT '文件大小', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ai知识库' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_ai_knowledge_base_type +-- ---------------------------- +DROP TABLE IF EXISTS `ck_ai_knowledge_base_type`; +CREATE TABLE `ck_ai_knowledge_base_type` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `type` tinyint(2) NULL DEFAULT 1 COMMENT '类型 0系统 1用户创建', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述', + `label` json NULL COMMENT '标签', + `prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '提示词', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + `isDel` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(11) NOT NULL DEFAULT 0 COMMENT '删除时间', + `status` tinyint(2) NULL DEFAULT 1 COMMENT '状态 1启用 0禁用', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ai知识库类型' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_ai_settings +-- ---------------------------- +DROP TABLE IF EXISTS `ck_ai_settings`; +CREATE TABLE `ck_ai_settings` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `config` json NULL COMMENT '配置信息', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + `isRelease` tinyint(2) NULL DEFAULT 0 COMMENT '是否发布 0未发布 1已发布', + `releaseTime` int(11) NULL DEFAULT NULL COMMENT '发布时间', + `botId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '智能体id', + `datasetId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '知识库id', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'AI配置' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_app_version +-- ---------------------------- +DROP TABLE IF EXISTS `ck_app_version`; +CREATE TABLE `ck_app_version` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `forceUpdate` tinyint(2) NULL DEFAULT 0 COMMENT '是否强制更新', + `version` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `downloadUrl` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `updateContent` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `createTime` int(11) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_attachments +-- ---------------------------- +DROP TABLE IF EXISTS `ck_attachments`; +CREATE TABLE `ck_attachments` ( + `id` int(10) NOT NULL AUTO_INCREMENT COMMENT '自增长ID', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '资源名', + `hash_key` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '资源hash校验值', + `server` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '存储服务商', + `source` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '资源地址', + `dl_count` int(10) NULL DEFAULT 0 COMMENT '下载次数', + `size` int(10) NULL DEFAULT 0 COMMENT '资源大小', + `suffix` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '资源类型', + `scene` tinyint(3) NOT NULL COMMENT '引用场景,获客海报1', + `create_at` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', + `update_at` timestamp(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_at` timestamp(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_hash_key`(`hash_key`) USING BTREE, + INDEX `idx_server`(`server`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 580 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '附件表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_call_recording +-- ---------------------------- +DROP TABLE IF EXISTS `ck_call_recording`; +CREATE TABLE `ck_call_recording` ( + `id` int(11) NOT NULL, + `phone` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号', + `isCallOut` tinyint(2) NULL DEFAULT NULL COMMENT '是否外呼', + `companyId` int(11) NULL DEFAULT NULL, + `callType` tinyint(2) NULL DEFAULT NULL, + `beginTime` int(11) NULL DEFAULT NULL, + `endTime` int(11) NULL DEFAULT NULL, + `createTime` int(11) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_id_phone_isCallOut_companyId`(`id`, `phone`, `isCallOut`, `companyId`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_chat_groups +-- ---------------------------- +DROP TABLE IF EXISTS `ck_chat_groups`; +CREATE TABLE `ck_chat_groups` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `groupName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称', + `groupMemo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述', + `groupType` tinyint(2) NULL DEFAULT NULL COMMENT '类型 1好友 2群', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司ID', + `sort` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '排序', + `createTime` int(11) NULL DEFAULT NULL, + `isDel` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '聊天分组' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_company +-- ---------------------------- +DROP TABLE IF EXISTS `ck_company`; +CREATE TABLE `ck_company` ( + `id` int(11) UNSIGNED NOT NULL COMMENT '项目真实ID,非自增', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '项目名称', + `status` tinyint(1) UNSIGNED NULL DEFAULT 1 COMMENT '状态', + `tenantId` int(11) UNSIGNED NULL DEFAULT 242 COMMENT '触客宝租户ID', + `companyId` int(11) UNSIGNED NOT NULL COMMENT '触客宝部门ID', + `memo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `createTime` int(11) NULL DEFAULT NULL, + `updateTime` int(11) NULL DEFAULT NULL, + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '部门表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_content_item +-- ---------------------------- +DROP TABLE IF EXISTS `ck_content_item`; +CREATE TABLE `ck_content_item` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `libraryId` int(11) NOT NULL COMMENT '所属内容库ID', + `type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'moment' COMMENT '内容类型(moment:朋友圈)', + `contentType` tinyint(1) NULL DEFAULT 0 COMMENT '0:未知 1:图片 2:链接 3:视频 4:文本 5:小程序 ', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '内容标题', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '文本内容', + `contentAi` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '文本内容_Ai版', + `contentData` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '完整内容数据(JSON格式)', + `snsId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '朋友圈唯一标识', + `msgId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群消息唯一标识', + `wechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信ID', + `friendId` int(11) NULL DEFAULT NULL COMMENT '微信好友ID', + `createMomentTime` bigint(20) NULL DEFAULT 0 COMMENT '朋友圈创建时间', + `createTime` int(11) NULL DEFAULT NULL COMMENT '记录创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '记录更新时间', + `coverImage` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '封面图片URL', + `resUrls` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '资源URL列表(JSON格式)', + `urls` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '相对路径URL列表(JSON格式)', + `location` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '地理位置名称', + `lat` decimal(10, 6) NULL DEFAULT 0.000000 COMMENT '纬度', + `lng` decimal(10, 6) NULL DEFAULT 0.000000 COMMENT '经度', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态(0:禁用,1:启用)', + `isDel` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除(0:否,1:是)', + `delTime` int(11) NULL DEFAULT 0 COMMENT '删除时间', + `wechatChatroomId` int(11) NULL DEFAULT NULL, + `senderNickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `createMessageTime` int(11) NULL DEFAULT NULL, + `comment` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '评论', + `sendTime` int(11) NULL DEFAULT 0 COMMENT '预计发布时间', + `sendTimes` int(11) NULL DEFAULT 0 COMMENT '实际发布时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_library`(`libraryId`) USING BTREE, + INDEX `idx_snsid`(`snsId`) USING BTREE, + INDEX `idx_wechatid`(`wechatId`) USING BTREE, + INDEX `idx_friendid`(`friendId`) USING BTREE, + INDEX `idx_create_time`(`createTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 6090 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '内容项目表-存储朋友圈采集数据' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_content_library +-- ---------------------------- +DROP TABLE IF EXISTS `ck_content_library`; +CREATE TABLE `ck_content_library` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `formType` tinyint(2) NULL DEFAULT 0 COMMENT '0存客宝 1触客宝', + `sourceType` tinyint(2) NOT NULL DEFAULT 1 COMMENT '类型 1好友 2群 3自定义', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '内容库名称', + `devices` json NULL COMMENT '设备列表', + `catchType` json NULL COMMENT '采集类型', + `sourceFriends` json NULL COMMENT '选择的微信好友', + `sourceGroups` json NULL COMMENT '选择的微信群', + `groupMembers` json NULL COMMENT '选择的微信群的群成员', + `keywordInclude` json NULL COMMENT '包含的关键词', + `keywordExclude` json NULL COMMENT '排除的关键词', + `aiEnabled` tinyint(1) NULL DEFAULT 0 COMMENT '是否启用AI:0=禁用,1=启用', + `aiPrompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT 'AI提示词', + `timeEnabled` tinyint(1) NULL DEFAULT 0 COMMENT '是否启用时间限制:0=禁用,1=启用', + `timeStart` int(11) NULL DEFAULT NULL COMMENT '开始时间', + `timeEnd` int(11) NULL DEFAULT NULL COMMENT '结束时间', + `status` tinyint(1) NULL DEFAULT 0 COMMENT '状态:0=禁用,1=启用', + `userId` int(11) NOT NULL COMMENT '用户ID', + `companyId` int(11) NOT NULL COMMENT '公司ID', + `createTime` int(11) NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT 0 COMMENT '更新时间', + `isDel` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 135 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '内容库表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_coze_conversation +-- ---------------------------- +DROP TABLE IF EXISTS `ck_coze_conversation`; +CREATE TABLE `ck_coze_conversation` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户id', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `conversation_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '对话ID', + `bot_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '机器人ID', + `created_at` int(11) NOT NULL DEFAULT 0 COMMENT '会话创建时间戳', + `meta_data` json NULL COMMENT '元数据', + `create_time` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间戳', + `update_time` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间戳', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `idx_conversation_id`(`conversation_id`) USING BTREE, + INDEX `idx_bot_id`(`bot_id`) USING BTREE, + INDEX `idx_create_time`(`create_time`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 56 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'Coze AI 会话表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_coze_message +-- ---------------------------- +DROP TABLE IF EXISTS `ck_coze_message`; +CREATE TABLE `ck_coze_message` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `chat_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '消息ID', + `conversation_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '会话ID', + `bot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '机器人ID', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '消息内容', + `content_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'text' COMMENT '内容类型', + `role` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色', + `type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '消息类型', + `created_at` int(11) NOT NULL COMMENT '消息创建时间', + `updated_at` int(11) NOT NULL COMMENT '消息更新时间', + `create_time` int(11) NOT NULL COMMENT '记录创建时间', + `update_time` int(11) NOT NULL COMMENT '记录更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_chat_id`(`chat_id`) USING BTREE, + INDEX `idx_conversation_id`(`conversation_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 184 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '消息记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_coze_workspace +-- ---------------------------- +DROP TABLE IF EXISTS `ck_coze_workspace`; +CREATE TABLE `ck_coze_workspace` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `workspace_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '工作区ID', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '工作区名称', + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '工作区描述', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `workspace_id`(`workspace_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'Coze空间表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_customer_acquisition_task +-- ---------------------------- +DROP TABLE IF EXISTS `ck_customer_acquisition_task`; +CREATE TABLE `ck_customer_acquisition_task` ( + `id` int(10) NOT NULL AUTO_INCREMENT COMMENT '自增长ID', + `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '计划名称', + `sceneId` int(11) NULL DEFAULT 1 COMMENT '场景ID', + `sceneConf` json NULL COMMENT '场景具体配置信息', + `reqConf` json NULL COMMENT '好友申请设置', + `msgConf` json NULL COMMENT '消息设置', + `tagConf` json NULL COMMENT '标签设置', + `userId` int(11) NULL DEFAULT 0 COMMENT '创建者', + `companyId` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '公司ID', + `status` tinyint(3) NOT NULL DEFAULT 0 COMMENT '状态 0禁用 1启用', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '修改时间', + `deleteTime` int(11) NULL DEFAULT 0 COMMENT '删除时间', + `apiKey` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 178 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '获客计划表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_device +-- ---------------------------- +DROP TABLE IF EXISTS `ck_device`; +CREATE TABLE `ck_device` ( + `id` int(11) UNSIGNED NOT NULL COMMENT '设备真实ID,非自增', + `memo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '设备名称', + `imei` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '设备IMEI', + `deviceImei` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '设备本地IMEI', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号', + `operatingSystem` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作系统版本', + `model` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '型号', + `brand` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '品牌', + `rooted` tinyint(1) NULL DEFAULT 0 COMMENT '是否root', + `xPosed` tinyint(1) NULL DEFAULT 0 COMMENT '是否安装xposed', + `softwareVersion` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '软件版本', + `extra` json NULL COMMENT '额外信息JSON', + `alive` tinyint(1) NULL DEFAULT 0 COMMENT '是否在线', + `companyId` int(11) NOT NULL COMMENT '公司ID', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_id_imei`(`imei`, `id`) USING BTREE, + INDEX `idx_group`(`companyId`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_device_handle_log +-- ---------------------------- +DROP TABLE IF EXISTS `ck_device_handle_log`; +CREATE TABLE `ck_device_handle_log` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `content` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作说明', + `deviceId` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '设备id', + `userId` int(11) NULL DEFAULT NULL COMMENT '用户id', + `companyId` int(11) NULL DEFAULT NULL COMMENT '租户id', + `createTime` int(11) NULL DEFAULT NULL COMMENT '操作时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 351 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_device_taskconf +-- ---------------------------- +DROP TABLE IF EXISTS `ck_device_taskconf`; +CREATE TABLE `ck_device_taskconf` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `deviceId` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '设备ID', + `autoLike` tinyint(3) NULL DEFAULT 0 COMMENT '自动点赞', + `momentsSync` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT '朋友圈同步', + `autoCustomerDev` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT '自动开发客户', + `groupMessageDeliver` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT '群消息推送', + `autoGroup` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT '自动建群', + `autoAddFriend` tinyint(3) NULL DEFAULT 0 COMMENT '自动加好友', + `contentSync` tinyint(255) UNSIGNED NULL DEFAULT 0 COMMENT '朋友圈同步', + `aiChat` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT 'AI 会话', + `autoReply` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT '自动回复', + `companyId` int(10) NULL DEFAULT NULL COMMENT '公司ID', + `createTime` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '更新时间', + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 31 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备任务配置表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_device_user +-- ---------------------------- +DROP TABLE IF EXISTS `ck_device_user`; +CREATE TABLE `ck_device_user` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `companyId` int(11) UNSIGNED NOT NULL COMMENT '公司id', + `userId` int(11) UNSIGNED NOT NULL COMMENT '用户id', + `deviceId` int(11) UNSIGNED NOT NULL COMMENT '设备id', + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 24 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备跟操盘手的关联关系' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_device_wechat_login +-- ---------------------------- +DROP TABLE IF EXISTS `ck_device_wechat_login`; +CREATE TABLE `ck_device_wechat_login` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `deviceId` int(11) NULL DEFAULT NULL COMMENT '设备ID', + `wechatId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信ID', + `alive` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT '微信在线否', + `companyId` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '租户ID', + `createTime` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '更新时间', + `isTips` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否提示迁移', + PRIMARY KEY (`id`) USING BTREE, + INDEX `wechatId`(`wechatId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 322 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备登录微信记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_distribution_channel +-- ---------------------------- +DROP TABLE IF EXISTS `ck_distribution_channel`; +CREATE TABLE `ck_distribution_channel` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '渠道ID', + `companyId` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '公司ID', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '渠道名称', + `code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '渠道编码(系统生成)', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '联系电话', + `password` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '密码(MD5加密)', + `wechatId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '微信号', + `remarks` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '备注信息', + `createType` enum('manual','auto') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'manual' COMMENT '创建类型:manual手动创建,auto扫码创建', + `status` enum('enabled','disabled') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'enabled' COMMENT '状态:enabled启用,disabled禁用', + `totalCustomers` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '总获客数', + `todayCustomers` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '今日获客数', + `totalFriends` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '总加好友数', + `todayFriends` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '今日加好友数', + `withdrawableAmount` bigint(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '可提现金额(分)', + `createTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新时间', + `deleteTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '删除时间(软删除)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_companyId`(`companyId`) USING BTREE, + INDEX `idx_code`(`code`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_deleteTime`(`deleteTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '分销渠道表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_distribution_revenue_record +-- ---------------------------- +DROP TABLE IF EXISTS `ck_distribution_revenue_record`; +CREATE TABLE `ck_distribution_revenue_record` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '收益记录ID', + `companyId` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '公司ID', + `channelId` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '渠道ID', + `channelCode` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '渠道编码(冗余字段,方便查询)', + `type` enum('customer_acquisition','add_friend','order','poster','phone','other') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'other' COMMENT '收益类型:customer_acquisition获客,add_friend加好友,order订单,poster海报,phone电话,other其他', + `sourceType` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '来源类型(如:海报获客、加好友任务等)', + `sourceId` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '来源ID(关联任务ID或其他业务ID)', + `amount` bigint(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '收益金额(分,整型,单位分)', + `remark` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '备注信息', + `createTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_companyId`(`companyId`) USING BTREE, + INDEX `idx_channelId`(`channelId`) USING BTREE, + INDEX `idx_channelCode`(`channelCode`) USING BTREE, + INDEX `idx_type`(`type`) USING BTREE, + INDEX `idx_createTime`(`createTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '分销渠道收益明细表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_distribution_withdrawal +-- ---------------------------- +DROP TABLE IF EXISTS `ck_distribution_withdrawal`; +CREATE TABLE `ck_distribution_withdrawal` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '提现申请ID', + `companyId` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '公司ID', + `channelId` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '渠道ID', + `amount` bigint(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '提现金额(分,整型,单位分)', + `payType` enum('wechat','alipay','bankcard') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'wechat' COMMENT '支付类型:wechat微信,alipay支付宝,bankcard银行卡', + `status` enum('pending','approved','rejected','paid') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'pending' COMMENT '状态:pending待审核,approved已通过,rejected已拒绝,paid已打款', + `reviewer` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '审核人', + `reviewTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '审核时间', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '备注/拒绝理由', + `applyTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '申请时间', + `createTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_companyId`(`companyId`) USING BTREE, + INDEX `idx_channelId`(`channelId`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_applyTime`(`applyTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '分销渠道提现申请表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_flow_package +-- ---------------------------- +DROP TABLE IF EXISTS `ck_flow_package`; +CREATE TABLE `ck_flow_package` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '套餐名称', + `tag` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '套餐标签', + `originalPrice` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '原价', + `price` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '售价', + `monthlyFlow` int(11) NOT NULL DEFAULT 0 COMMENT '每月流量(人/月)', + `duration` int(11) NOT NULL DEFAULT 1 COMMENT '套餐时长(月)', + `privileges` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '套餐特权,多行文本存储', + `sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序', + `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态: 0=禁用, 1=启用', + `isDel` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除: 0=否, 1=是', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_name`(`name`) USING BTREE, + INDEX `idx_tag`(`tag`) USING BTREE, + INDEX `idx_is_del`(`isDel`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量套餐表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_flow_package_order +-- ---------------------------- +DROP TABLE IF EXISTS `ck_flow_package_order`; +CREATE TABLE `ck_flow_package_order` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `orderNo` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '订单编号', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `packageId` int(11) NOT NULL DEFAULT 0 COMMENT '套餐ID', + `packageName` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '套餐名称', + `amount` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '订单金额', + `duration` int(11) NOT NULL DEFAULT 1 COMMENT '购买时长(月)', + `payStatus` tinyint(1) NOT NULL DEFAULT 0 COMMENT '支付状态: 0=未支付, 1=已支付,10=无需支付', + `payTime` int(11) NOT NULL DEFAULT 0 COMMENT '支付时间', + `payType` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '支付方式: wechat=微信, alipay=支付宝,nopay=无需支付', + `transactionId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '第三方支付交易号', + `status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '订单状态: 0=待支付, 1=已支付, 2=已取消, 3=已退款', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '备注', + `isDel` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除: 0=否, 1=是', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_order_no`(`orderNo`) USING BTREE, + INDEX `idx_user_id`(`userId`) USING BTREE, + INDEX `idx_package_id`(`packageId`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_pay_status`(`payStatus`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 37 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '套餐订单表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_flow_usage_record +-- ---------------------------- +DROP TABLE IF EXISTS `ck_flow_usage_record`; +CREATE TABLE `ck_flow_usage_record` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `packageId` int(11) NOT NULL DEFAULT 0 COMMENT '套餐ID', + `userPackageId` int(11) NOT NULL DEFAULT 0 COMMENT '用户套餐ID', + `taskId` int(11) NOT NULL DEFAULT 0 COMMENT '关联任务ID', + `phone` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '微信号', + `usageAmount` int(11) NOT NULL DEFAULT 0 COMMENT '使用量(人)', + `usageType` tinyint(1) NOT NULL DEFAULT 1 COMMENT '使用类型: 1=添加好友, 2=群发消息, 3=其他', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '备注', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_user_id`(`userId`) USING BTREE, + INDEX `idx_package_id`(`packageId`) USING BTREE, + INDEX `idx_user_package_id`(`userPackageId`) USING BTREE, + INDEX `idx_task_id`(`taskId`) USING BTREE, + INDEX `idx_phone`(`phone`) USING BTREE, + INDEX `idx_create_time`(`createTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量使用记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_jd_promotion_site +-- ---------------------------- +DROP TABLE IF EXISTS `ck_jd_promotion_site`; +CREATE TABLE `ck_jd_promotion_site` ( + `id` bigint(11) NOT NULL, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `jdSocialMediaId` bigint(11) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_jd_social_media +-- ---------------------------- +DROP TABLE IF EXISTS `ck_jd_social_media`; +CREATE TABLE `ck_jd_social_media` ( + `id` bigint(11) NOT NULL, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `appkey` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `secretkey` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_ai_push +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_ai_push`; +CREATE TABLE `ck_kf_ai_push` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '推送名称', + `tags` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '目标用户标签(JSON数组格式)', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '推送内容(支持变量:{客户名称}{产品功能}{核心价值}等)', + `pushTiming` tinyint(4) NOT NULL DEFAULT 1 COMMENT '推送时机:1=立即推送,2=最佳时机(AI决定),3=定时推送', + `scheduledTime` int(11) NOT NULL DEFAULT 0 COMMENT '定时推送时间(时间戳,仅当pushTiming=3时有效)', + `status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '启用状态:0=禁用,1=启用', + `successRate` decimal(5, 2) NOT NULL DEFAULT 0.00 COMMENT '成功率(百分比,保留两位小数)', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司ID', + `isDel` tinyint(4) NOT NULL DEFAULT 0 COMMENT '删除标记:0=未删除,1=已删除', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间(时间戳)', + `updateTime` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间(时间戳)', + `delTime` int(11) NOT NULL DEFAULT 0 COMMENT '删除时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_company_user`(`companyId`, `userId`) USING BTREE, + INDEX `idx_pushTiming`(`pushTiming`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_isDel`(`isDel`) USING BTREE, + INDEX `idx_scheduledTime`(`scheduledTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'AI智能推送表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_ai_push_record +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_ai_push_record`; +CREATE TABLE `ck_kf_ai_push_record` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `pushId` int(11) NOT NULL DEFAULT 0 COMMENT '推送ID', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司ID', + `wechatAccountId` int(11) NOT NULL DEFAULT 0 COMMENT '微信账号ID', + `friendIdOrGroupId` int(11) NOT NULL DEFAULT 0 COMMENT '好友ID或群ID', + `isSend` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否发送:0=未发送,1=已发送', + `sendTime` int(11) NOT NULL DEFAULT 0 COMMENT '发送时间(时间戳)', + `receiveTime` int(11) NOT NULL DEFAULT 0 COMMENT '接收时间(时间戳)', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_pushId`(`pushId`) USING BTREE, + INDEX `idx_company`(`companyId`) USING BTREE, + INDEX `idx_user`(`userId`) USING BTREE, + INDEX `idx_createTime`(`createTime`) USING BTREE, + INDEX `idx_isSend`(`isSend`) USING BTREE, + INDEX `idx_wechatAccount`(`wechatAccountId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'AI智能推送记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_auto_greetings +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_auto_greetings`; +CREATE TABLE `ck_kf_auto_greetings` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '规则名称', + `trigger` tinyint(4) NOT NULL DEFAULT 0 COMMENT '触发类型:1=好友首次添加,2=首次发消息,3=时间触发,4=关键词触发,5=生日触发,6=自定义', + `condition` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '具体条件(JSON格式)', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '问候内容', + `level` int(11) NOT NULL DEFAULT 0 COMMENT '优先级(数字越小优先级越高)', + `status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '启用状态:0=禁用,1=启用', + `usageCount` int(11) NOT NULL DEFAULT 0 COMMENT '使用次数', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司ID', + `is_template` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否模板:0=否,1=是', + `isDel` tinyint(4) NOT NULL DEFAULT 0 COMMENT '删除标记:0=未删除,1=已删除', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间(时间戳)', + `updateTime` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间(时间戳)', + `delTime` int(11) NOT NULL DEFAULT 0 COMMENT '删除时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_company_user`(`companyId`, `userId`) USING BTREE, + INDEX `idx_trigger`(`trigger`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_isDel`(`isDel`) USING BTREE, + INDEX `idx_level`(`level`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '问候规则表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_auto_greetings_record +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_auto_greetings_record`; +CREATE TABLE `ck_kf_auto_greetings_record` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `autoId` int(11) NOT NULL DEFAULT 0 COMMENT '规则ID', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司ID', + `wechatAccountId` int(11) NOT NULL DEFAULT 0 COMMENT '微信账号ID', + `friendIdOrGroupId` int(11) NOT NULL DEFAULT 0 COMMENT '好友ID或群ID', + `isSend` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否发送:0=未发送,1=已发送', + `sendTime` int(11) NOT NULL DEFAULT 0 COMMENT '发送时间(时间戳)', + `receiveTime` int(11) NOT NULL DEFAULT 0 COMMENT '接收时间(时间戳)', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_autoId`(`autoId`) USING BTREE, + INDEX `idx_company`(`companyId`) USING BTREE, + INDEX `idx_user`(`userId`) USING BTREE, + INDEX `idx_createTime`(`createTime`) USING BTREE, + INDEX `idx_isSend`(`isSend`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '问候规则使用记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_follow_up +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_follow_up`; +CREATE TABLE `ck_kf_follow_up` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标题', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述', + `friendId` int(12) NULL DEFAULT NULL COMMENT '好友id', + `type` tinyint(2) NULL DEFAULT 0 COMMENT '类型 0其他 1电话回访 2发送消息 3安排会议 4发送邮件', + `reminderTime` int(12) NULL DEFAULT NULL COMMENT '提醒时间', + `isRemind` tinyint(2) NULL DEFAULT 0 COMMENT '是否提醒', + `isProcess` tinyint(2) NULL DEFAULT 0 COMMENT '是否处理', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_companyId`(`companyId`) USING BTREE, + INDEX `idx_userId`(`userId`) USING BTREE, + INDEX `idx_level`(`type`) USING BTREE, + INDEX `idx_isRemind`(`isRemind`) USING BTREE, + INDEX `idx_isProcess`(`isProcess`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '跟进提醒' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_friend_settings +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_friend_settings`; +CREATE TABLE `ck_kf_friend_settings` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `type` tinyint(2) NULL DEFAULT 0 COMMENT '匹配类型 0人工接待 1AI辅助 2AI接管', + `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id', + `friendId` int(11) NULL DEFAULT NULL COMMENT '好友id', + `conversationId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '会话id', + `conversationTime` int(11) NULL DEFAULT NULL COMMENT '会话创建时间', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_companyId`(`companyId`) USING BTREE, + INDEX `idx_userId`(`userId`) USING BTREE, + INDEX `idx_wechatAccountId`(`wechatAccountId`) USING BTREE, + INDEX `idx_friendId`(`friendId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 51 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '好友AI配置' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_keywords +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_keywords`; +CREATE TABLE `ck_kf_keywords` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标题', + `keywords` json NULL COMMENT '关键词', + `type` tinyint(2) NULL DEFAULT NULL COMMENT '匹配类型 0模糊 1精确', + `replyType` tinyint(2) NULL DEFAULT NULL COMMENT '回复类型 0素材回复 1自定义', + `content` json NULL COMMENT '自定义内容', + `metailGroups` json NULL COMMENT '素材id', + `status` tinyint(2) NULL DEFAULT NULL COMMENT '状态 0停用 1启用', + `level` tinyint(2) NULL DEFAULT 0 COMMENT '等级 0低优先级 1中优先级 2高优先级', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '关键词管理' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_material +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_material`; +CREATE TABLE `ck_kf_material` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `title` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标题', + `content` json NULL COMMENT '内容', + `cover` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '封面', + `status` tinyint(2) NULL DEFAULT NULL COMMENT '状态 0停用 1启用', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 19 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '素材库管理' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_moments +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_moments`; +CREATE TABLE `ck_kf_moments` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `sendData` json NULL COMMENT '发送的具体信息', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `isSend` tinyint(2) NULL DEFAULT 0 COMMENT '是否发送 0否 1是', + `sendTime` int(11) NULL DEFAULT NULL COMMENT '发送时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '客服端发布朋友圈记录' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_moments_settings +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_moments_settings`; +CREATE TABLE `ck_kf_moments_settings` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `wechatId` int(12) NULL DEFAULT NULL COMMENT '微信客服id', + `max` int(11) NULL DEFAULT 5 COMMENT '每日上限', + `sendNum` int(11) NULL DEFAULT 0 COMMENT '今日发送次数', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '客服朋友圈配置信息' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_notice +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_notice`; +CREATE TABLE `ck_kf_notice` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `type` tinyint(2) NULL DEFAULT NULL COMMENT '通知类型 1代办事项 2跟进提醒 ', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `bindId` int(11) NULL DEFAULT NULL COMMENT '绑定的id', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标题', + `message` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '通知消息', + `isRead` tinyint(2) NULL DEFAULT 0 COMMENT '是否读取', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `readTime` int(12) NULL DEFAULT NULL COMMENT '读取时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 252 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '通知消息' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_questions +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_questions`; +CREATE TABLE `ck_kf_questions` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `type` tinyint(2) NULL DEFAULT 0 COMMENT '匹配类型 0模糊 1精确', + `questions` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '问题', + `answers` json NULL COMMENT '答案', + `status` tinyint(2) NULL DEFAULT 1 COMMENT '状态 0禁用 1启用', + `isDel` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除', + `deleteTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_companyId`(`companyId`) USING BTREE, + INDEX `idx_userId`(`userId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'AI问答' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_reply +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_reply`; +CREATE TABLE `ck_kf_reply` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `groupId` int(11) NULL DEFAULT NULL, + `userId` int(11) NULL DEFAULT NULL, + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `msgType` tinyint(2) NULL DEFAULT NULL, + `content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `createTime` int(11) NULL DEFAULT NULL, + `lastUpdateTime` int(11) NULL DEFAULT NULL, + `sortIndex` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 130753 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '快捷回复' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_reply_group +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_reply_group`; +CREATE TABLE `ck_kf_reply_group` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `userId` int(11) NULL DEFAULT 0, + `companyId` int(11) NULL DEFAULT 0, + `groupName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `sortIndex` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `parentId` int(11) NULL DEFAULT NULL, + `replyType` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `replys` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 21898 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '快捷回复分组' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_sensitive_word +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_sensitive_word`; +CREATE TABLE `ck_kf_sensitive_word` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标题', + `keywords` json NULL COMMENT '关键词', + `content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '替换内容/警告内容', + `operation` tinyint(2) NULL DEFAULT NULL COMMENT '操作 0不操作 1替换 2删除 3警告 4禁止发送', + `status` tinyint(2) NULL DEFAULT NULL COMMENT '状态 0停用 1启用', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '敏感词管理' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_to_do +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_to_do`; +CREATE TABLE `ck_kf_to_do` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标题', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述', + `friendId` int(12) NULL DEFAULT NULL COMMENT '好友id', + `level` tinyint(2) NULL DEFAULT 0 COMMENT '提示等级 0低优先级 1中优先级 2高优先级 3紧急', + `reminderTime` int(12) NULL DEFAULT NULL COMMENT '提醒时间', + `isRemind` tinyint(2) NULL DEFAULT 0 COMMENT '是否提醒', + `isProcess` tinyint(2) NULL DEFAULT 0 COMMENT '是否处理', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_companyId`(`companyId`) USING BTREE, + INDEX `idx_userId`(`userId`) USING BTREE, + INDEX `idx_level`(`level`) USING BTREE, + INDEX `idx_isRemind`(`isRemind`) USING BTREE, + INDEX `idx_isProcess`(`isProcess`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '待办事项' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_menus +-- ---------------------------- +DROP TABLE IF EXISTS `ck_menus`; +CREATE TABLE `ck_menus` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '菜单ID', + `title` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '菜单名称', + `path` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '路由路径', + `icon` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '图标名称', + `parentId` int(11) NOT NULL DEFAULT 0 COMMENT '父菜单ID,0表示顶级菜单', + `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态:1启用,0禁用', + `sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序,数值越小越靠前', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_parent_id`(`parentId`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '系统菜单表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_order +-- ---------------------------- +DROP TABLE IF EXISTS `ck_order`; +CREATE TABLE `ck_order` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `mchId` int(11) NULL DEFAULT NULL COMMENT '门店号', + `companyId` int(11) UNSIGNED NOT NULL, + `userId` int(11) NULL DEFAULT NULL, + `orderType` tinyint(2) NULL DEFAULT NULL COMMENT '订单类型 1购买算力', + `status` tinyint(1) UNSIGNED NULL DEFAULT 0 COMMENT '支付状态 0待支付 1已付款 2已退款 3付款失败', + `goodsId` int(11) NULL DEFAULT 0 COMMENT '商品id', + `goodsName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品名称', + `goodsSpecs` json NULL COMMENT '商品规格', + `money` int(11) NULL DEFAULT 0 COMMENT '金额 单位分', + `orderNo` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单号', + `ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `nonceStr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '随机字符串', + `createTime` int(11) NULL DEFAULT NULL, + `payType` tinyint(2) NULL DEFAULT NULL COMMENT '支付类型 1微信 2支付宝', + `payTime` int(11) NULL DEFAULT NULL, + `payInfo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '错误信息', + `deleteTime` int(11) UNSIGNED NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 106 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_plan_scene +-- ---------------------------- +DROP TABLE IF EXISTS `ck_plan_scene`; +CREATE TABLE `ck_plan_scene` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `name` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '场景名称', + `description` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述', + `image` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '图片icon', + `status` tinyint(3) NULL DEFAULT NULL COMMENT '状态', + `sort` tinyint(3) NULL DEFAULT NULL, + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '修改时间', + `deleteTime` int(11) NULL DEFAULT 0 COMMENT '删除时间', + `scenarioTags` json NULL COMMENT '标签', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '获客场景' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_plan_tags +-- ---------------------------- +DROP TABLE IF EXISTS `ck_plan_tags`; +CREATE TABLE `ck_plan_tags` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `tagName` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签名', + `companyId` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '部门ID', + `createTime` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量标签表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_task_customer +-- ---------------------------- +DROP TABLE IF EXISTS `ck_task_customer`; +CREATE TABLE `ck_task_customer` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `task_id` int(11) NOT NULL, + `channelId` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '渠道ID(分销渠道ID,对应distribution_channel.id)', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户姓名', + `source` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '来源', + `phone` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `remark` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `tags` json NULL, + `siteTags` json NULL COMMENT '站内标签', + `processed_wechat_ids` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '0-未处理,1-已处理/添加中,2-~~添加成功~~ ~~已添加~~添加任务成功 3-添加失败 4-已通过-已发消息', + `fail_reason` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `addTime` int(11) NOT NULL DEFAULT 0 COMMENT '添加时间', + `passTime` int(11) NOT NULL DEFAULT 0 COMMENT '通过时间', + `createTime` int(11) NOT NULL DEFAULT 0, + `updateTime` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE, + INDEX `task_id`(`task_id`) USING BTREE, + INDEX `addTime`(`addTime`) USING BTREE, + INDEX `passTime`(`passTime`) USING BTREE, + INDEX `updateTime`(`updateTime`) USING BTREE, + INDEX `idx_channelId`(`channelId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 28222 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_tokens_company +-- ---------------------------- +DROP TABLE IF EXISTS `ck_tokens_company`; +CREATE TABLE `ck_tokens_company` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `userId` int(10) NULL DEFAULT 0, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `tokens` bigint(100) NULL DEFAULT NULL, + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + `isAdmin` tinyint(2) NULL DEFAULT 0 COMMENT '是否公司主号', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '公司算力账户' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_tokens_form +-- ---------------------------- +DROP TABLE IF EXISTS `ck_tokens_form`; +CREATE TABLE `ck_tokens_form` ( + `id` int(11) UNSIGNED NOT NULL, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称', + `tokens` int(12) NULL DEFAULT 0 COMMENT '消耗token', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `status` tinyint(2) NULL DEFAULT 0 COMMENT '状态', + `createTime` int(11) NULL DEFAULT 0, + `delTime` int(11) NULL DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_tokens_package +-- ---------------------------- +DROP TABLE IF EXISTS `ck_tokens_package`; +CREATE TABLE `ck_tokens_package` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称', + `tokens` int(12) NULL DEFAULT NULL, + `price` int(12) NULL DEFAULT NULL COMMENT '售价 单位分', + `originalPrice` int(12) NULL DEFAULT NULL COMMENT '原价 单位分', + `description` json NULL COMMENT '描述', + `sort` int(12) NULL DEFAULT 50 COMMENT '排序', + `isTrial` tinyint(2) NULL DEFAULT 0 COMMENT '是否试用', + `isRecommend` tinyint(2) NULL DEFAULT 0 COMMENT '是否推荐', + `isHot` tinyint(2) NULL DEFAULT 0 COMMENT '是否热门', + `isVip` tinyint(2) NULL DEFAULT 0 COMMENT '是否VIP', + `status` tinyint(2) NULL DEFAULT 0 COMMENT '状态 0停用 1启用', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'token套餐' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_tokens_record +-- ---------------------------- +DROP TABLE IF EXISTS `ck_tokens_record`; +CREATE TABLE `ck_tokens_record` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id', + `friendIdOrGroupId` int(11) NULL DEFAULT NULL COMMENT '好友id或者群id', + `form` int(11) NULL DEFAULT 0 COMMENT '来源 \r\n0 未知\r\n1 点赞\r\n2 朋友圈同步\r\n3 朋友圈发布\r\n4 群发微信\r\n5 群发群消息\r\n6 群发群公告\r\n7 海报获客\r\n8 订单获客\r\n9 电话获客\r\n10 微信群获客\r\n11 API获客\r\n12 AI改写\r\n13 AI客服\r\n14 生成群公告\r\n\r\n1001 商家 \r\n1002 充值 \r\n1003 系统', + `type` tinyint(2) NULL DEFAULT 0 COMMENT '类型 0减少 1增加', + `tokens` int(11) NULL DEFAULT NULL COMMENT '消耗tokens', + `balanceTokens` int(11) NULL DEFAULT NULL COMMENT '剩余tokens', + `remarks` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 336 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '算力明细记录' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_traffic_order +-- ---------------------------- +DROP TABLE IF EXISTS `ck_traffic_order`; +CREATE TABLE `ck_traffic_order` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(10) UNSIGNED NULL DEFAULT NULL, + `identifier` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '流量池用户', + `createTime` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', + `isDel` tinyint(2) NULL DEFAULT 0, + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', + `orderno` varchar(0) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单编号', + `userId` int(11) NULL DEFAULT NULL, + `storeId` int(11) NULL DEFAULT NULL COMMENT '门店id', + `goddsName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品价格', + `price` int(10) NULL DEFAULT NULL COMMENT '商品价格', + `actualPay` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '实际支付', + `ownerWechatId` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_traffic_pool +-- ---------------------------- +DROP TABLE IF EXISTS `ck_traffic_pool`; +CREATE TABLE `ck_traffic_pool` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `identifier` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '流量标识,可以是手机号、微信号', + `mobile` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号', + `wechatId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信ID', + `createTime` int(10) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(10) NULL DEFAULT NULL COMMENT '修改时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_identifier`(`identifier`) USING BTREE, + INDEX `idx_wechatId`(`wechatId`) USING BTREE, + INDEX `idx_mobile`(`mobile`) USING BTREE, + INDEX `idx_create_time`(`createTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1201225 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量池' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_traffic_profile +-- ---------------------------- +DROP TABLE IF EXISTS `ck_traffic_profile`; +CREATE TABLE `ck_traffic_profile` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `identifier` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '流量标识,可以是手机号、微信号', + `nickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '平台昵称', + `avatar` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '平台头像', + `gender` tinyint(3) NULL DEFAULT 0 COMMENT '平台性别', + `phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '平台手机号', + `platformId` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '平台Id', + `createTime` int(10) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(10) NULL DEFAULT NULL COMMENT '修改时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_identifier`(`identifier`) USING BTREE, + INDEX `idx_mobile`(`phone`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 196606 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量池用户个人信息' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_traffic_source +-- ---------------------------- +DROP TABLE IF EXISTS `ck_traffic_source`; +CREATE TABLE `ck_traffic_source` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `type` tinyint(2) NULL DEFAULT 1 COMMENT '流量来源 0其他 1好友 2群 3场景', + `identifier` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '流量标识', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称', + `status` tinyint(3) NULL DEFAULT 1 COMMENT '1待处理,2处理中,3已通过,4已拒绝,5已过期,6已取消 -3已删除(同步 tk_friend_task 表的 status)', + `sourceId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '来源id(微信id或群id)', + `fromd` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '流量来源(群聊名称)', + `sceneId` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '场景ID', + `companyId` int(11) NULL DEFAULT 0 COMMENT '账号所属项目id', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '修改时间', + `R` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0', + `F` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0', + `M` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_identifier_sourceId_sceneId`(`identifier`, `sourceId`, `sceneId`) USING BTREE, + INDEX `idx_identifier`(`identifier`) USING BTREE, + INDEX `idx_companyId`(`companyId`) USING BTREE, + INDEX `idx_company_status_time`(`companyId`, `status`, `updateTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 586456 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量来源' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_traffic_source_package +-- ---------------------------- +DROP TABLE IF EXISTS `ck_traffic_source_package`; +CREATE TABLE `ck_traffic_source_package` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `userId` int(10) NULL DEFAULT NULL COMMENT '用户id', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述', + `pic` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '图标', + `companyId` int(11) NULL DEFAULT NULL COMMENT '账号所属项目id', + `matchingRules` json NULL COMMENT '匹配规则', + `isSys` tinyint(2) NULL DEFAULT 0 COMMENT '是否系统只有', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `updateTime` int(11) NULL DEFAULT NULL, + `createTime` int(11) NULL DEFAULT 0 COMMENT '创建时间', + `deleteTime` int(11) NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `companyId`(`companyId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量池包' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_traffic_source_package_item +-- ---------------------------- +DROP TABLE IF EXISTS `ck_traffic_source_package_item`; +CREATE TABLE `ck_traffic_source_package_item` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `packageId` int(10) NULL DEFAULT NULL COMMENT '流量包id', + `companyId` int(11) NULL DEFAULT NULL COMMENT '账号所属项目id', + `identifier` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '流量标识,可以是手机号、微信号', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `createTime` int(11) NULL DEFAULT 0 COMMENT '创建时间', + `deleteTime` int(10) NULL DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_packageId_companyId_identifier_isDel`(`packageId`, `companyId`, `identifier`, `isDel`) USING BTREE, + INDEX `packageId`(`packageId`) USING BTREE, + INDEX `companyId`(`companyId`) USING BTREE, + INDEX `identifier`(`identifier`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 34 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_traffic_tag +-- ---------------------------- +DROP TABLE IF EXISTS `ck_traffic_tag`; +CREATE TABLE `ck_traffic_tag` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `tagId` int(11) NULL DEFAULT NULL, + `tagName` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签名', + `tagType` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签值', + `tagValue` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签值', + `companyId` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '部门ID', + `trafficPoolId` int(10) NULL DEFAULT NULL COMMENT '流量池用户id traffic_pool的主键', + `createTime` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', + `isDel` tinyint(2) NULL DEFAULT NULL, + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量标签表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_user_flow_package +-- ---------------------------- +DROP TABLE IF EXISTS `ck_user_flow_package`; +CREATE TABLE `ck_user_flow_package` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `packageId` int(11) NOT NULL DEFAULT 0 COMMENT '套餐ID', + `orderId` int(11) NOT NULL DEFAULT 0 COMMENT '关联订单ID', + `duration` int(11) NOT NULL DEFAULT 1 COMMENT '套餐时长(月)', + `totalFlow` int(11) NOT NULL DEFAULT 0 COMMENT '总流量(人)', + `usedFlow` int(11) NOT NULL DEFAULT 0 COMMENT '已使用流量(人)', + `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态: 0=无效, 1=有效', + `startTime` int(11) NOT NULL DEFAULT 0 COMMENT '开始时间', + `expireTime` int(11) NOT NULL DEFAULT 0 COMMENT '到期时间', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_user_id`(`userId`) USING BTREE, + INDEX `idx_package_id`(`packageId`) USING BTREE, + INDEX `idx_order_id`(`orderId`) USING BTREE, + INDEX `idx_expire_time`(`expireTime`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户流量套餐表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_user_log +-- ---------------------------- +DROP TABLE IF EXISTS `ck_user_log`; +CREATE TABLE `ck_user_log` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `userName` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户名', + `action` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '操作类型', + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '操作描述', + `ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT 'IP地址', + `userAgent` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户设备信息', + `requestMethod` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '请求方法', + `requestUrl` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '请求URL', + `requestData` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '请求数据', + `responseCode` int(11) NULL DEFAULT 0 COMMENT '响应状态码', + `responseMsg` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '响应消息', + `createTime` int(11) NULL DEFAULT 0 COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_user_id`(`userId`) USING BTREE, + INDEX `idx_user_name`(`userName`) USING BTREE, + INDEX `idx_action`(`action`) USING BTREE, + INDEX `idx_create_time`(`createTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 45 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户操作日志表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_user_portrait +-- ---------------------------- +DROP TABLE IF EXISTS `ck_user_portrait`; +CREATE TABLE `ck_user_portrait` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `type` tinyint(2) NULL DEFAULT 0 COMMENT '类型 0浏览 1点击 2下单/购买 3注册 4互动', + `companyId` int(11) NULL DEFAULT 0, + `trafficPoolId` int(10) NULL DEFAULT NULL COMMENT '流量池用户id traffic_pool的主键', + `source` tinyint(2) NULL DEFAULT 0 COMMENT '来源 0本站 1老油条 2老坑爹', + `uniqueId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '来源网站唯一id', + `sourceData` json NULL COMMENT '来源网站数据', + `remark` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `count` int(10) NULL DEFAULT 1 COMMENT '统计次数(半小时内)', + `createTime` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '修改时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 22602 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户画像' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_users +-- ---------------------------- +DROP TABLE IF EXISTS `ck_users`; +CREATE TABLE `ck_users` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID', + `account` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号', + `username` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', + `phone` char(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '登录手机号', + `passwordMd5` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码', + `passwordLocal` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '本地密码', + `avatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'https://img.icons8.com/color/512/circled-user-male-skin-type-7.png' COMMENT '头像', + `isAdmin` tinyint(3) NULL DEFAULT 0 COMMENT '是否管理身份 1->是 0->否', + `companyId` int(10) UNSIGNED NOT NULL COMMENT '账号所属项目id', + `typeId` tinyint(3) NOT NULL DEFAULT -1 COMMENT '类型:运营后台/操盘手 传1 、 门店传2', + `status` tinyint(3) NULL DEFAULT 0 COMMENT '1->可用,0->禁用', + `s2_accountId` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'S2的用户账号id', + `balance` int(11) NULL DEFAULT 0 COMMENT '余额', + `tokens` int(11) NULL DEFAULT 0 COMMENT '算力余额', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '修改时间', + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1666 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_vendor_order +-- ---------------------------- +DROP TABLE IF EXISTS `ck_vendor_order`; +CREATE TABLE `ck_vendor_order` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '订单ID', + `orderNo` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '订单编号', + `userId` int(10) UNSIGNED NOT NULL COMMENT '用户ID', + `packageId` int(10) UNSIGNED NOT NULL COMMENT '套餐ID', + `packageName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '套餐名称', + `totalAmount` decimal(10, 2) NOT NULL COMMENT '订单总额', + `payAmount` decimal(10, 2) NOT NULL COMMENT '支付金额', + `advancePayment` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '预付款', + `status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '状态:0=待支付,1=已支付,2=已完成,3=已取消', + `payTime` int(11) NULL DEFAULT 0 COMMENT '支付时间', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `createTime` int(11) NOT NULL COMMENT '创建时间', + `updateTime` int(11) NOT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `orderNo`(`orderNo`) USING BTREE, + INDEX `userId`(`userId`) USING BTREE, + INDEX `packageId`(`packageId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '供应商订单表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_vendor_package +-- ---------------------------- +DROP TABLE IF EXISTS `ck_vendor_package`; +CREATE TABLE `ck_vendor_package` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '套餐ID', + `userId` int(11) NULL DEFAULT NULL COMMENT '用户id', + `companyId` int(11) NULL DEFAULT NULL COMMENT '公司id', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '套餐名称', + `originalPrice` decimal(10, 2) NOT NULL COMMENT '原价', + `price` decimal(10, 2) NOT NULL COMMENT '售价', + `discount` decimal(4, 2) NULL DEFAULT 0.00 COMMENT '折扣', + `advancePayment` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '预付款', + `tags` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签', + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '套餐描述', + `cover` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '封面图片', + `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态:0=下架,1=上架', + `createTime` int(11) NOT NULL COMMENT '创建时间', + `updateTime` int(11) NOT NULL COMMENT '更新时间', + `isDel` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '供应商套餐表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_vendor_project +-- ---------------------------- +DROP TABLE IF EXISTS `ck_vendor_project`; +CREATE TABLE `ck_vendor_project` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '项目ID', + `packageId` int(10) UNSIGNED NOT NULL COMMENT '套餐ID', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '项目名称', + `originalPrice` decimal(10, 2) NOT NULL COMMENT '原价', + `price` decimal(10, 2) NOT NULL COMMENT '售价', + `duration` int(11) NULL DEFAULT 0 COMMENT '项目时长(分钟)', + `image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '项目图片', + `detail` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '项目详情', + `createTime` int(11) NOT NULL COMMENT '创建时间', + `updateTime` int(11) NOT NULL COMMENT '更新时间', + `isDel` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE, + INDEX `packageId`(`packageId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '供应商套餐项目表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_wechat_account +-- ---------------------------- +DROP TABLE IF EXISTS `ck_wechat_account`; +CREATE TABLE `ck_wechat_account` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `s2_wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '微信账号id', + `wechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信ID', + `alias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信号', + `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', + `pyInitial` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '拼音首字母', + `quanPin` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '全拼', + `avatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像URL', + `gender` tinyint(1) NULL DEFAULT 0 COMMENT '性别 0->保密;1->男;2->女', + `region` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '地区', + `signature` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '个性签名', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '电话', + `country` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '国家', + `privince` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '省份', + `city` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '城市', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_wechatId`(`wechatId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4282931 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_wechat_customer +-- ---------------------------- +DROP TABLE IF EXISTS `ck_wechat_customer`; +CREATE TABLE `ck_wechat_customer` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增id', + `wechatId` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信id', + `basic` json NULL COMMENT '保存基础信息', + `weight` json NULL COMMENT '保存权重信息', + `activity` json NULL COMMENT '保存账号活跃信息', + `friendShip` json NULL COMMENT '保存朋友关系信息', + `companyId` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '公司id', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_wechatId`(`wechatId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 159 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信客服信息' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_wechat_friendship +-- ---------------------------- +DROP TABLE IF EXISTS `ck_wechat_friendship`; +CREATE TABLE `ck_wechat_friendship` ( + `id` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '好友id', + `wechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '微信ID', + `tags` json NULL COMMENT '好友标签', + `memo` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '好友备注', + `ownerWechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '所有者微信ID', + `companyId` int(11) NULL DEFAULT NULL COMMENT '公司ID', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + UNIQUE INDEX `uk_owner_wechat_account`(`ownerWechatId`, `wechatId`) USING BTREE, + INDEX `idx_wechat_id`(`wechatId`) USING BTREE, + INDEX `idx_owner_wechat_id`(`ownerWechatId`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信好友表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_wechat_group +-- ---------------------------- +DROP TABLE IF EXISTS `ck_wechat_group`; +CREATE TABLE `ck_wechat_group` ( + `id` int(11) UNSIGNED NOT NULL COMMENT 'S2微信群id', + `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '微信账号ID', + `chatroomId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信群聊id', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群名称', + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群头像', + `companyId` int(11) NULL DEFAULT NULL COMMENT '项目id', + `ownerWechatId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '所有者微信ID', + `identifier` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群主(流量标识,可以是手机号、微信号)', + `createTime` int(11) UNSIGNED NULL DEFAULT NULL, + `updateTime` int(11) UNSIGNED NULL DEFAULT NULL, + `deleteTime` int(11) UNSIGNED NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_owner_chatroomId`(`chatroomId`, `ownerWechatId`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_wechat_group_member +-- ---------------------------- +DROP TABLE IF EXISTS `ck_wechat_group_member`; +CREATE TABLE `ck_wechat_group_member` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `identifier` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群成员(流量标识,可以是手机号、微信号)', + `chatroomId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群真实id', + `customerIs` tinyint(3) NULL DEFAULT 0 COMMENT '是否客服', + `companyId` int(11) NULL DEFAULT NULL COMMENT '项目id', + `groupId` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '所属群ID', + `createTime` int(11) UNSIGNED NULL DEFAULT 0, + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_identifier_chatroomId_groupId`(`identifier`, `chatroomId`, `groupId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 561848 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群成员' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_wechat_restricts +-- ---------------------------- +DROP TABLE IF EXISTS `ck_wechat_restricts`; +CREATE TABLE `ck_wechat_restricts` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `taskId` int(11) NULL DEFAULT NULL COMMENT '任务id', + `level` tinyint(3) UNSIGNED NULL DEFAULT 1 COMMENT '风险类型 1 普通 2 警告 3 错误', + `reason` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '风险原因', + `memo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '记录更详细的风险信息', + `wechatId` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信id', + `companyId` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '项目id', + `restrictTime` int(11) NULL DEFAULT NULL COMMENT '限制日期', + `recoveryTime` int(11) NULL DEFAULT NULL COMMENT '恢复日期', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1416 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信风险受限记录' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_wechat_tag +-- ---------------------------- +DROP TABLE IF EXISTS `ck_wechat_tag`; +CREATE TABLE `ck_wechat_tag` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `tags` json NULL COMMENT '标签JSON', + `wechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '微信ID', + `companyId` int(11) NULL DEFAULT NULL COMMENT '公司ID', + `createTime` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_wechatId`(`wechatId`) USING BTREE, + INDEX `idx_companyId`(`companyId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 123366 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench`; +CREATE TABLE `ck_workbench` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `userId` int(11) NOT NULL COMMENT '创建用户ID', + `companyId` int(11) NULL DEFAULT 0 COMMENT '公司id', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '工作台名称', + `type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '工作台类型:1=自动点赞,2=朋友圈同步,3=群消息推送,4=自动建群,5=流量分发,6=通讯录导入', + `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态:0=禁用,1=启用', + `autoStart` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否自动启动:0=否,1=是', + `createTime` int(11) NOT NULL COMMENT '创建时间', + `updateTime` int(11) NOT NULL COMMENT '更新时间', + `isDel` tinyint(1) NULL DEFAULT 0, + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_user_id`(`userId`) USING BTREE, + INDEX `idx_type`(`type`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 330 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '工作台主表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_auto_like +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_auto_like`; +CREATE TABLE `ck_workbench_auto_like` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `interval` int(11) NOT NULL DEFAULT 60 COMMENT '点赞间隔(秒)', + `maxLikes` int(11) NOT NULL DEFAULT 100 COMMENT '最大点赞数', + `startTime` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '00:00:00' COMMENT '开始时间', + `endTime` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '23:59:59' COMMENT '结束时间', + `contentTypes` json NULL COMMENT '内容类型', + `devices` json NULL COMMENT '设备列表,JSON格式:[{\"id\":1,\"name\":\"设备1\"},{\"id\":2,\"name\":\"设备2\"}]', + `friends` json NULL COMMENT '用户列表', + `createTime` int(11) NOT NULL COMMENT '创建时间', + `updateTime` int(11) NOT NULL COMMENT '更新时间', + `targetGroups` json NULL COMMENT '目标用户组列表,JSON格式:[{\"id\":1,\"name\":\"用户组1\"},{\"id\":2,\"name\":\"用户组2\"}] 废除', + `tagOperator` tinyint(1) NULL DEFAULT 2 COMMENT '标签匹配规则 1:and 2:or 废除', + `friendMaxLikes` int(10) NULL DEFAULT NULL COMMENT '好友最大点赞数', + `friendTags` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '好友标签', + `enableFriendTags` tinyint(1) NULL DEFAULT 0 COMMENT '启用好友标签', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_workbench_id`(`workbenchId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 54 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '自动点赞配置表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_auto_like_item +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_auto_like_item`; +CREATE TABLE `ck_workbench_auto_like_item` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `deviceId` int(11) NULL DEFAULT 0 COMMENT '设备id', + `snsId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '自动点赞id', + `wechatFriendId` int(11) NULL DEFAULT NULL COMMENT '好友id', + `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id', + `momentsId` int(11) NULL DEFAULT NULL COMMENT '朋友圈id', + `createTime` int(11) NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `workbenchId`(`workbenchId`) USING BTREE, + INDEX `wechatFriendId`(`wechatFriendId`) USING BTREE, + INDEX `wechatAccountId`(`wechatAccountId`) USING BTREE, + INDEX `momentsId`(`momentsId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4653 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '工作台-自动点赞记录' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_group_create +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_group_create`; +CREATE TABLE `ck_workbench_group_create` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `workbenchId` int(11) NOT NULL COMMENT '计划ID', + `devices` json NULL COMMENT '目标设备/客服(JSON数组)', + `admins` json NULL COMMENT '管理员', + `poolGroups` json NULL COMMENT '流量池JSON', + `wechatGroups` json NULL COMMENT '微信客服JSON', + `startTime` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '开始时间', + `endTime` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '结束时间', + `groupSizeMin` int(10) NULL DEFAULT NULL COMMENT '群好友最小人数', + `groupSizeMax` int(10) NULL DEFAULT NULL COMMENT '群好友最大人数', + `maxGroupsPerDay` int(10) NULL DEFAULT NULL COMMENT '每日建群最大数量', + `groupNameTemplate` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群模板信息', + `groupDescription` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群描述', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 27 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_group_create_item +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_group_create_item`; +CREATE TABLE `ck_workbench_group_create_item` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `friendId` int(11) NULL DEFAULT NULL, + `wechatId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '微信id', + `groupId` int(10) NULL DEFAULT NULL COMMENT '群id', + `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id', + `status` tinyint(2) NOT NULL DEFAULT 0 COMMENT '状态:0=待创建,1=创建中,2=创建成功,3=创建失败,4=管理员好友已拉入', + `memberType` tinyint(2) NOT NULL DEFAULT 1 COMMENT '成员类型:1=群主成员,2=管理员,3=群主好友,4=管理员好友', + `retryCount` int(11) NOT NULL DEFAULT 0 COMMENT '重试次数', + `chatroomId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群聊ID(用于查询验证)', + `verifyTime` int(11) NULL DEFAULT NULL COMMENT '验证时间', + `createTime` int(11) NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_status_workbench`(`status`, `workbenchId`) USING BTREE, + INDEX `idx_chatroom_id`(`chatroomId`) USING BTREE, + INDEX `idx_member_type`(`memberType`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 66 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_group_push +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_group_push`; +CREATE TABLE `ck_workbench_group_push` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `pushType` tinyint(1) NOT NULL DEFAULT 1 COMMENT '推送方式 0定时 1立即', + `targetType` tinyint(1) NOT NULL DEFAULT 1 COMMENT '推送目标类型:1=群推送,2=好友推送', + `startTime` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '推送开始时间', + `endTime` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '推送结束时间', + `maxPerDay` int(11) NULL DEFAULT 0 COMMENT '每日推送条数', + `pushOrder` tinyint(1) NULL DEFAULT 1 COMMENT '推送顺序 1最早 2最新', + `isLoop` tinyint(1) NULL DEFAULT 0 COMMENT '是否循环推送 0否 1是', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '是否启用 0否 1是', + `groups` json NULL COMMENT '推送微信群组(JSON)', + `friends` json NULL COMMENT '推送好友列表(JSON)', + `ownerWechatIds` json NULL COMMENT '所属微信id', + `contentLibraries` json NULL COMMENT '内容库(JSON)', + `friendIntervalMin` int(11) NOT NULL DEFAULT 10 COMMENT '好友间最小间隔时间(秒)', + `friendIntervalMax` int(11) NOT NULL DEFAULT 20 COMMENT '好友间最大间隔时间(秒)', + `messageIntervalMin` int(11) NOT NULL DEFAULT 1 COMMENT '消息间最小间隔时间(秒)', + `messageIntervalMax` int(11) NOT NULL DEFAULT 12 COMMENT '消息间最大间隔时间(秒)', + `isRandomTemplate` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否随机选择话术组(0=否,1=是)', + `postPushTags` json NOT NULL COMMENT '推送完成后打标签', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + `socialMediaId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '京东导购媒体', + `promotionSiteId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '京东广告位', + `trafficPools` json NULL COMMENT '流量池', + `devices` json NULL, + `groupPushSubType` tinyint(2) NULL DEFAULT 1 COMMENT '群推送子类型 1=群群发,2=群公告', + `announcementContent` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `enableAiRewrite` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `aiRewritePrompt` tinyint(2) NULL DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_workbench_id`(`workbenchId`) USING BTREE, + INDEX `idx_status_targetType`(`status`, `targetType`, `workbenchId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 34 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '群消息推送扩展表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_group_push_item +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_group_push_item`; +CREATE TABLE `ck_workbench_group_push_item` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `targetType` tinyint(1) NOT NULL DEFAULT 1 COMMENT '推送目标类型:1=群,2=好友', + `contentId` int(11) NULL DEFAULT 0 COMMENT '内容库is', + `groupId` int(10) NULL DEFAULT NULL COMMENT '群id', + `friendId` int(11) NULL DEFAULT NULL COMMENT '好友ID(当targetType=2时使用)', + `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id', + `isLoop` tinyint(2) NULL DEFAULT 0 COMMENT '是否循环完成', + `createTime` int(11) NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_workbench_target_time`(`workbenchId`, `targetType`, `createTime`) USING BTREE, + INDEX `idx_workbench_target_friend`(`workbenchId`, `targetType`, `friendId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 302 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_import_contact +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_import_contact`; +CREATE TABLE `ck_workbench_import_contact` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `devices` json NULL COMMENT '设备id', + `pools` json NULL COMMENT '流量池', + `num` int(11) NULL DEFAULT NULL COMMENT '分配数量', + `clearContact` tinyint(2) NULL DEFAULT 0 COMMENT '是否清除现有联系人', + `remarkType` tinyint(2) NOT NULL DEFAULT 0 COMMENT '备注类型 0不备注 1年月日 2月日 3自定义', + `remark` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `startTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '开始时间', + `endTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '结束时间', + `createTime` int(11) NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_import_contact_item +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_import_contact_item`; +CREATE TABLE `ck_workbench_import_contact_item` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `deviceId` int(11) NULL DEFAULT NULL COMMENT '设备id', + `packageId` int(11) NULL DEFAULT 0 COMMENT '流量包id', + `poolId` int(11) NULL DEFAULT NULL COMMENT '流量id', + `createTime` int(11) NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 140 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_moments_sync +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_moments_sync`; +CREATE TABLE `ck_workbench_moments_sync` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `syncInterval` int(11) NOT NULL DEFAULT 1 COMMENT '同步间隔(小时)', + `syncCount` int(11) NOT NULL DEFAULT 5 COMMENT '每日同步数量', + `syncType` tinyint(1) NOT NULL DEFAULT 1 COMMENT '同步类型:1=文本,2=图片,3=视频,4=链接', + `startTime` time(0) NULL DEFAULT '06:00:00' COMMENT '发布开始时间', + `endTime` time(0) NULL DEFAULT '23:59:00' COMMENT '发布结束时间', + `accountType` tinyint(1) NOT NULL DEFAULT 1 COMMENT '账号类型:1=业务号,2=个人号', + `devices` json NOT NULL COMMENT '设备列表,JSON格式', + `contentLibraries` json NULL COMMENT '内容库ID列表,JSON格式', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_workbench_id`(`workbenchId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 97 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '朋友圈同步配置' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_moments_sync_item +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_moments_sync_item`; +CREATE TABLE `ck_workbench_moments_sync_item` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `deviceId` int(11) NULL DEFAULT 0 COMMENT '设备id', + `contentId` int(10) NULL DEFAULT NULL COMMENT '内容库id', + `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id', + `createTime` int(11) NOT NULL COMMENT '创建时间', + `isLoop` tinyint(2) NULL DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_workbench_time`(`workbenchId`, `createTime`) USING BTREE, + INDEX `idx_workbench_content`(`workbenchId`, `contentId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2308 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '朋友圈同步配置' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_traffic_config +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_traffic_config`; +CREATE TABLE `ck_workbench_traffic_config` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '流量分发计划ID', + `distributeType` tinyint(1) NOT NULL DEFAULT 1 COMMENT '分配方式 1均分 2优先级 3比例', + `maxPerDay` int(11) NOT NULL DEFAULT 0 COMMENT '每日最大分配量', + `timeType` tinyint(1) NOT NULL DEFAULT 1 COMMENT '时间限制 1全天 2自定义', + `startTime` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '开始时间', + `endTime` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '结束时间', + `account` json NULL COMMENT '分发的账号', + `devices` json NULL COMMENT '目标设备/客服(JSON数组)', + `pools` json NULL COMMENT '流量池(JSON数组)', + `exp` int(10) NULL DEFAULT 30 COMMENT '有效期 单位天', + `createTime` int(11) NOT NULL, + `updateTime` int(11) NOT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uniq_workbench`(`workbenchId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 32 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量分发计划扩展表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_traffic_config_item +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_traffic_config_item`; +CREATE TABLE `ck_workbench_traffic_config_item` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL DEFAULT 0 COMMENT '工作台ID', + `deviceId` int(11) NULL DEFAULT 0 COMMENT '设备id', + `wechatFriendId` int(10) NULL DEFAULT NULL COMMENT '好友id', + `wechatAccountId` int(11) NULL DEFAULT 0 COMMENT '客服id', + `expTime` int(11) NULL DEFAULT 0 COMMENT '有效时间', + `exp` int(11) NULL DEFAULT 0 COMMENT '有效时间 天', + `isRecycle` tinyint(2) NULL DEFAULT 0 COMMENT '是否回收', + `recycleTime` int(11) NULL DEFAULT 0 COMMENT '回收时间', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `workbenchId`(`workbenchId`) USING BTREE, + INDEX `deviceId`(`deviceId`) USING BTREE, + INDEX `wechatFriendId`(`wechatFriendId`) USING BTREE, + INDEX `wechatAccountId`(`wechatAccountId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 58212 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量分发计划扩展表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_allot_rule +-- ---------------------------- +DROP TABLE IF EXISTS `s2_allot_rule`; +CREATE TABLE `s2_allot_rule` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '规则ID', + `departmentId` int(11) NULL DEFAULT 0 COMMENT '部门id', + `tenantId` int(11) NOT NULL DEFAULT 0 COMMENT '租户ID', + `allotType` tinyint(4) NOT NULL DEFAULT 0 COMMENT '分配类型', + `allotOnline` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否在线分配', + `kefuRange` tinyint(4) NOT NULL DEFAULT 0 COMMENT '客服范围', + `wechatRange` tinyint(4) NOT NULL DEFAULT 0 COMMENT '微信范围', + `kefuData` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '客服数据JSON', + `wechatData` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '微信ID列表JSON', + `labels` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '标签JSON', + `priorityStrategy` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '优先级策略JSON', + `sortIndex` int(11) NOT NULL DEFAULT 0 COMMENT '排序索引', + `creatorAccountId` int(11) NOT NULL DEFAULT 0 COMMENT '创建者账号ID', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `ruleName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '规则名称', + `isDel` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_tenant`(`tenantId`) USING BTREE, + INDEX `idx_sort`(`sortIndex`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2176 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '分配规则表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_call_recording +-- ---------------------------- +DROP TABLE IF EXISTS `s2_call_recording`; +CREATE TABLE `s2_call_recording` ( + `id` bigint(20) NOT NULL COMMENT '主键ID', + `tenantId` bigint(20) NOT NULL DEFAULT 0 COMMENT '租户ID', + `deviceOwnerId` bigint(20) NOT NULL DEFAULT 0 COMMENT '设备所有者ID', + `userName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户名', + `nickname` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '昵称', + `realName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '真实姓名', + `deviceMemo` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '设备备注', + `fileName` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '文件名', + `imei` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '设备IMEI', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '电话号码', + `isCallOut` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否为呼出电话(0:呼入,1:呼出)', + `beginTime` int(11) NOT NULL DEFAULT 0 COMMENT '通话开始时间戳', + `endTime` int(11) NOT NULL DEFAULT 0 COMMENT '通话结束时间戳', + `audioUrl` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '录音文件URL', + `mp3AudioUrl` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'MP3录音文件URL', + `callBeginTime` int(11) NOT NULL DEFAULT 0 COMMENT '呼叫开始时间戳', + `callLogId` bigint(20) NOT NULL DEFAULT 0 COMMENT '通话日志ID', + `callType` int(11) NOT NULL DEFAULT 0 COMMENT '通话类型', + `duration` int(11) NOT NULL DEFAULT 0 COMMENT '通话时长(秒)', + `skipReason` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '跳过原因', + `skipUpload` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否跳过上传', + `isDeleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已删除', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间戳', + `lastUpdateTime` int(11) NOT NULL DEFAULT 0 COMMENT '最后更新时间戳', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_tenant_id`(`tenantId`) USING BTREE, + INDEX `idx_device_owner_id`(`deviceOwnerId`) USING BTREE, + INDEX `idx_user_name`(`userName`) USING BTREE, + INDEX `idx_phone`(`phone`) USING BTREE, + INDEX `idx_begin_time`(`beginTime`) USING BTREE, + INDEX `idx_end_time`(`endTime`) USING BTREE, + INDEX `idx_call_begin_time`(`callBeginTime`) USING BTREE, + INDEX `idx_imei`(`imei`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '通话记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_company_account +-- ---------------------------- +DROP TABLE IF EXISTS `s2_company_account`; +CREATE TABLE `s2_company_account` ( + `id` int(11) NULL DEFAULT NULL COMMENT 'id', + `tenantId` int(11) NULL DEFAULT NULL, + `userName` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户名', + `realName` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '真实姓名', + `nickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '昵称', + `memo` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '备注', + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '头像', + `secret` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '密钥', + `accountType` int(11) NULL DEFAULT 0 COMMENT '账户类型', + `departmentId` int(11) NULL DEFAULT 0 COMMENT '部门ID', + `departmentName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门名称', + `useGoogleSecretKey` tinyint(1) NULL DEFAULT 0 COMMENT '是否使用谷歌密钥', + `hasVerifyGoogleSecret` tinyint(1) NULL DEFAULT 0 COMMENT '是否验证谷歌密钥', + `passwordMd5` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT 'MD5加密密码', + `passwordLocal` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '本地加密密码', + `lastLoginIp` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '最后登录IP', + `lastLoginTime` int(11) NULL DEFAULT 0 COMMENT '最后登录时间', + `createTime` int(11) NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT 0 COMMENT '更新时间', + `privilegeIds` json NULL COMMENT '权限', + `alive` tinyint(1) NULL DEFAULT NULL, + `creator` int(10) NULL DEFAULT NULL COMMENT '创建者', + `creatorRealName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建者真实姓名', + `creatorUserName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建者用户名', + `status` tinyint(1) NULL DEFAULT 0 COMMENT '状态 0正常 1禁用', + UNIQUE INDEX `idx_username`(`userName`) USING BTREE, + INDEX `idx_create_time`(`createTime`) USING BTREE, + INDEX `idx_update_time`(`updateTime`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '公司账户表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_department +-- ---------------------------- +DROP TABLE IF EXISTS `s2_department`; +CREATE TABLE `s2_department` ( + `id` int(11) NOT NULL, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称', + `memo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `tenantId` int(11) NULL DEFAULT NULL, + `isTop` tinyint(1) NULL DEFAULT 0, + `level` int(10) NULL DEFAULT 0, + `parentId` int(10) NULL DEFAULT 0, + `privileges` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, + `createTime` int(11) NULL DEFAULT NULL, + `lastUpdateTime` int(11) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '部门表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_device +-- ---------------------------- +DROP TABLE IF EXISTS `s2_device`; +CREATE TABLE `s2_device` ( + `id` int(11) NULL DEFAULT NULL COMMENT '设备真实ID', + `userName` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户名', + `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', + `realName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '真实姓名', + `groupName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分组名称', + `wechatAccounts` json NULL COMMENT '微信账号列表JSON', + `alive` tinyint(1) NULL DEFAULT 0 COMMENT '是否在线', + `aliveTime` int(11) NULL DEFAULT 0, + `lastAliveTime` int(11) NULL DEFAULT NULL COMMENT '最后在线时间', + `tenantId` int(11) NULL DEFAULT NULL COMMENT '租户ID', + `groupId` int(11) NULL DEFAULT NULL COMMENT '分组ID', + `currentAccountId` int(11) NULL DEFAULT NULL COMMENT '当前账号ID', + `imei` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '设备IMEI', + `deviceImei` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '设备本地IMEI', + `memo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `isDeleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除', + `deletedAndStop` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除并停止', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + `rooted` tinyint(1) NULL DEFAULT 0 COMMENT '是否root', + `xPosed` tinyint(1) NULL DEFAULT 0 COMMENT '是否安装xposed', + `brand` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '品牌', + `model` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '型号', + `operatingSystem` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作系统版本', + `softwareVersion` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '软件版本', + `extra` json NULL COMMENT '额外信息JSON', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号', + `lastUpdateTime` int(11) NULL DEFAULT NULL COMMENT '最后更新时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + `taskConfig` json NULL COMMENT '自动化任务开关 \r\nautoLike:自动点赞\r\nmomentsSync:朋友圈同步\r\nautoCustomerDev:自动开发客户\r\ngroupMessageDeliver:群消息推送\r\nautoGroup:自动建群', + UNIQUE INDEX `uk_imei`(`imei`) USING BTREE, + INDEX `idx_tenant`(`tenantId`) USING BTREE, + INDEX `idx_group`(`groupId`) USING BTREE, + INDEX `idx_current_account`(`currentAccountId`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_device_group +-- ---------------------------- +DROP TABLE IF EXISTS `s2_device_group`; +CREATE TABLE `s2_device_group` ( + `id` int(11) NOT NULL, + `tenantId` int(11) NOT NULL COMMENT '租户ID', + `groupName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '分组名称', + `groupMemo` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分组备注', + `count` int(11) NULL DEFAULT 0 COMMENT '设备数量', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + INDEX `idx_tenant`(`tenantId`) USING BTREE, + INDEX `idx_group_name`(`groupName`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备分组表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_friend_task +-- ---------------------------- +DROP TABLE IF EXISTS `s2_friend_task`; +CREATE TABLE `s2_friend_task` ( + `id` int(11) NOT NULL COMMENT '任务ID', + `tenantId` int(11) NULL DEFAULT 0 COMMENT '租户ID', + `operatorAccountId` int(11) NULL DEFAULT 0 COMMENT '操作账号ID', + `status` int(11) NULL DEFAULT 1 COMMENT '状态:0执行中,1执行成功,2执行失败', + `phone` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号/微信号', + `msgContent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '验证消息', + `wechatAccountId` int(11) NULL DEFAULT 0 COMMENT '微信账号ID', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间戳', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `extra` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '额外数据JSON', + `labels` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签,逗号分隔', + `from` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '来源', + `alias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信账号别名', + `wechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信ID', + `wechatAvatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信头像', + `wechatNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信昵称', + `accountNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号昵称', + `accountRealName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号真实姓名', + `accountUsername` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号用户名', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间戳', + `is_counted` tinyint(1) NULL DEFAULT 0 COMMENT '是否已统计(0=未统计,1=已统计)', + UNIQUE INDEX `uk_task_id`(`id`) USING BTREE, + INDEX `idx_tenant_id`(`tenantId`) USING BTREE, + INDEX `idx_operator_account_id`(`operatorAccountId`) USING BTREE, + INDEX `idx_wechat_account_id`(`wechatAccountId`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_phone`(`phone`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '添加好友任务记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_moments_item +-- ---------------------------- +DROP TABLE IF EXISTS `s2_moments_item`; +CREATE TABLE `s2_moments_item` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `task_id` int(11) NOT NULL COMMENT '朋友圈任务ID', + `temp_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '临时ID', + `wechat_account_id` int(11) NULL DEFAULT NULL COMMENT '微信账号ID', + `execute_count` int(11) NULL DEFAULT 0 COMMENT '执行次数', + `executed` tinyint(1) NULL DEFAULT 0 COMMENT '是否已执行', + `status` tinyint(1) NULL DEFAULT 0 COMMENT '状态', + `extra` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '额外信息', + `execute_time` int(11) NULL DEFAULT NULL COMMENT '执行时间', + `finished_time` int(11) NULL DEFAULT NULL COMMENT '完成时间', + `labels` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '标签', + `alt_list` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '替代列表', + `comments` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '评论', + `moment_content_type` tinyint(1) NULL DEFAULT 0 COMMENT '朋友圈内容类型', + `text` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '文本内容', + `pic_url_list` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '图片URL列表', + `video_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '视频URL', + `link` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '链接信息', + `is_use_location` tinyint(1) NULL DEFAULT 0 COMMENT '是否使用位置', + `lat` decimal(10, 6) NULL DEFAULT 0.000000 COMMENT '纬度', + `lng` decimal(10, 6) NULL DEFAULT 0.000000 COMMENT '经度', + `poi_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '位置名称', + `poi_address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '位置地址', + `video_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '视频编号', + `created_at` int(11) NULL DEFAULT NULL COMMENT '记录创建时间', + `updated_at` int(11) NULL DEFAULT NULL COMMENT '记录更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `idx_task_temp`(`task_id`, `temp_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 184 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '朋友圈任务项表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_moments_task +-- ---------------------------- +DROP TABLE IF EXISTS `s2_moments_task`; +CREATE TABLE `s2_moments_task` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `task_id` int(11) NOT NULL COMMENT '朋友圈任务ID', + `tenant_id` int(11) NULL DEFAULT NULL COMMENT '租户ID', + `operator_account_id` int(11) NULL DEFAULT NULL COMMENT '操作人账号ID', + `account_username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号用户名', + `account_nickname` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号昵称', + `account_real_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号真实姓名', + `public_mode` tinyint(1) NULL DEFAULT 0 COMMENT '发布模式', + `moment_content_type` tinyint(1) NULL DEFAULT 1 COMMENT '朋友圈内容类型:1纯文本,2图片,3视频,4链接', + `text` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '文本内容', + `pic_url_list` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '图片URL列表', + `video_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '视频URL', + `link` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '链接信息', + `job_status` tinyint(1) NULL DEFAULT 0 COMMENT '任务状态', + `job_origin_status` tinyint(1) NULL DEFAULT 0 COMMENT '任务原始状态', + `job_group` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '任务组', + `job_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '任务名称', + `begin_time` int(11) NULL DEFAULT NULL COMMENT '开始时间', + `end_time` int(11) NULL DEFAULT NULL COMMENT '结束时间', + `timing_time` int(11) NULL DEFAULT NULL COMMENT '定时发布时间', + `create_time` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `immediately` tinyint(1) NULL DEFAULT 1 COMMENT '是否立即发布', + `created_at` int(11) NULL DEFAULT NULL COMMENT '记录创建时间', + `updated_at` int(11) NULL DEFAULT NULL COMMENT '记录更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `idx_task_id`(`task_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 88 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '朋友圈任务表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_reply +-- ---------------------------- +DROP TABLE IF EXISTS `s2_reply`; +CREATE TABLE `s2_reply` ( + `id` int(11) NOT NULL, + `tenantId` int(255) NULL DEFAULT NULL, + `groupId` int(11) NULL DEFAULT NULL, + `accountId` int(11) NULL DEFAULT NULL, + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `msgType` tinyint(2) NULL DEFAULT NULL, + `content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `createTime` int(11) NULL DEFAULT NULL, + `lastUpdateTime` int(11) NULL DEFAULT NULL, + `sortIndex` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '快捷回复' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_reply_group +-- ---------------------------- +DROP TABLE IF EXISTS `s2_reply_group`; +CREATE TABLE `s2_reply_group` ( + `id` int(11) NOT NULL, + `groupName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `sortIndex` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `parentId` int(11) NULL DEFAULT NULL, + `replyType` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `replys` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `departmentId` int(11) NULL DEFAULT 2130, + `accountId` int(11) NULL DEFAULT 5150, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '快捷回复分组' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_account +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_account`; +CREATE TABLE `s2_wechat_account` ( + `id` int(11) NOT NULL COMMENT '微信账号ID', + `wechatId` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '微信ID', + `deviceAccountId` int(11) NULL DEFAULT 0 COMMENT '设备账号ID', + `imei` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'IMEI', + `deviceMemo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '设备备注', + `accountUserName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号用户名', + `accountRealName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号真实姓名', + `accountNickname` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号昵称', + `keFuAlive` tinyint(1) NULL DEFAULT 0 COMMENT '客服是否在线', + `deviceAlive` tinyint(1) NULL DEFAULT 0 COMMENT '设备是否在线', + `wechatAlive` tinyint(1) NULL DEFAULT 0 COMMENT '微信是否在线', + `wechatAliveTime` int(11) NULL DEFAULT 0 COMMENT '在线时间', + `yesterdayMsgCount` int(11) NULL DEFAULT 0 COMMENT '昨日消息数', + `sevenDayMsgCount` int(11) NULL DEFAULT 0 COMMENT '7天消息数', + `thirtyDayMsgCount` int(11) NULL DEFAULT 0 COMMENT '30天消息数', + `totalFriend` int(11) NULL DEFAULT 0 COMMENT '总好友数', + `maleFriend` int(11) NULL DEFAULT 0 COMMENT '男性好友数', + `unknowFriend` int(11) NULL DEFAULT NULL COMMENT '未知好友数', + `femaleFriend` int(11) NULL DEFAULT 0 COMMENT '女性好友数', + `wechatGroupName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信群组名称', + `tenantId` int(11) NULL DEFAULT NULL COMMENT '租户ID', + `nickname` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', + `alias` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '别名', + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像', + `gender` tinyint(1) NULL DEFAULT 0 COMMENT '性别', + `region` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '地区', + `signature` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '签名', + `bindQQ` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '绑定QQ', + `bindEmail` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '绑定邮箱', + `bindMobile` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '绑定手机', + `currentDeviceId` int(11) NULL DEFAULT 0 COMMENT '当前设备ID', + `isDeleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + `groupId` int(11) NULL DEFAULT 0 COMMENT '分组ID', + `memo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `wechatVersion` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信版本', + `labels` json NULL COMMENT '标签', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + `status` tinyint(3) NULL DEFAULT 1 COMMENT '状态值', + `healthScore` int(11) NULL DEFAULT 60 COMMENT '健康分总分(基础分+动态分)', + `baseScore` int(11) NULL DEFAULT 60 COMMENT '基础分(60-100分)', + `dynamicScore` int(11) NULL DEFAULT 0 COMMENT '动态分(扣分和加分)', + `isModifiedAlias` tinyint(1) NULL DEFAULT 0 COMMENT '是否已修改微信号(0=未修改,1=已修改)', + `lastFrequentTime` int(11) NULL DEFAULT NULL COMMENT '最后频繁时间(时间戳)', + `frequentCount` int(11) NULL DEFAULT 0 COMMENT '频繁次数(用于判断首次/再次频繁)', + `lastNoFrequentTime` int(11) NULL DEFAULT NULL COMMENT '最后不频繁时间(时间戳)', + `consecutiveNoFrequentDays` int(11) NULL DEFAULT 0 COMMENT '连续不频繁天数(用于加分)', + `scoreUpdateTime` int(11) NULL DEFAULT NULL COMMENT '评分更新时间', + INDEX `idx_wechat_id`(`wechatId`) USING BTREE, + INDEX `idx_health_score`(`healthScore`) USING BTREE, + INDEX `idx_is_modified_alias`(`isModifiedAlias`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_account_score +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_account_score`; +CREATE TABLE `s2_wechat_account_score` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `accountId` int(11) NOT NULL COMMENT '微信账号ID(s2_wechat_account.id)', + `wechatId` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '微信ID', + `baseScore` int(11) NOT NULL DEFAULT 0 COMMENT '基础分(60-100分)', + `baseScoreCalculated` tinyint(1) NOT NULL DEFAULT 0 COMMENT '基础分是否已计算(0=未计算,1=已计算)', + `baseScoreCalcTime` int(11) NULL DEFAULT NULL COMMENT '基础分计算时间', + `baseInfoScore` int(11) NOT NULL DEFAULT 0 COMMENT '基础信息分(0-10分)', + `isModifiedAlias` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已修改微信号(0=未修改,1=已修改)', + `friendCountScore` int(11) NOT NULL DEFAULT 0 COMMENT '好友数量分(0-30分)', + `friendCount` int(11) NOT NULL DEFAULT 0 COMMENT '好友数量(评分时的快照)', + `friendCountSource` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '好友数量来源(manual=手动,sync=同步)', + `dynamicScore` int(11) NOT NULL DEFAULT 0 COMMENT '动态分(扣分和加分)', + `lastFrequentTime` int(11) NULL DEFAULT NULL COMMENT '最后频繁时间(时间戳)', + `frequentCount` int(11) NOT NULL DEFAULT 0 COMMENT '频繁次数(用于判断首次/再次频繁)', + `frequentPenalty` int(11) NOT NULL DEFAULT 0 COMMENT '频繁扣分(累计)', + `lastNoFrequentTime` int(11) NULL DEFAULT NULL COMMENT '最后不频繁时间(时间戳)', + `consecutiveNoFrequentDays` int(11) NOT NULL DEFAULT 0 COMMENT '连续不频繁天数', + `noFrequentBonus` int(11) NOT NULL DEFAULT 0 COMMENT '不频繁加分(累计)', + `isBanned` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否封号(0=否,1=是)', + `banPenalty` int(11) NOT NULL DEFAULT 0 COMMENT '封号扣分', + `healthScore` int(11) NOT NULL DEFAULT 0 COMMENT '健康分总分(基础分+动态分)', + `maxAddFriendPerDay` int(11) NOT NULL DEFAULT 0 COMMENT '每日最大加人次数', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间', + `lastBanTime` int(11) NULL DEFAULT NULL COMMENT '最后一次封号时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_account_id`(`accountId`) USING BTREE, + INDEX `idx_wechat_id`(`wechatId`) USING BTREE, + INDEX `idx_health_score`(`healthScore`) USING BTREE, + INDEX `idx_base_score_calculated`(`baseScoreCalculated`) USING BTREE, + INDEX `idx_update_time`(`updateTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 368 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号评分记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_account_score_log +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_account_score_log`; +CREATE TABLE `s2_wechat_account_score_log` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `accountId` int(11) NOT NULL COMMENT '微信账号ID', + `wechatId` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '微信ID', + `field` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '变动字段(如frequentPenalty)', + `changeValue` int(11) NOT NULL DEFAULT 0 COMMENT '变动值(正加负减)', + `valueBefore` int(11) NULL DEFAULT NULL COMMENT '变更前的字段值', + `valueAfter` int(11) NULL DEFAULT NULL COMMENT '变更后的字段值', + `category` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分类:penalty/bonus/dynamic_total/health_total等', + `source` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '触发来源 friend_task/wechat_message/system', + `sourceId` bigint(20) NULL DEFAULT NULL COMMENT '关联记录ID(如任务/消息ID)', + `extra` json NULL COMMENT '附加信息(JSON)', + `totalScoreBefore` int(11) NULL DEFAULT NULL COMMENT '变更前健康总分', + `totalScoreAfter` int(11) NULL DEFAULT NULL COMMENT '变更后健康总分', + `createTime` int(11) NOT NULL COMMENT '记录时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_account_field`(`accountId`, `field`) USING BTREE, + INDEX `idx_wechat_id`(`wechatId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 39 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号健康分加减分日志' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_chatroom +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_chatroom`; +CREATE TABLE `s2_wechat_chatroom` ( + `id` int(11) NOT NULL, + `wechatAccountId` int(11) NOT NULL COMMENT '微信账号ID', + `wechatAccountAlias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信账号别名', + `wechatAccountWechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信账号微信ID', + `wechatAccountAvatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信账号头像', + `wechatAccountNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信账号昵称', + `chatroomId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '群聊ID', + `hasMe` tinyint(1) NULL DEFAULT 0 COMMENT '是否包含自己', + `chatroomOwnerNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群主昵称', + `chatroomOwnerAvatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群主头像', + `conRemark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群聊名称', + `pyInitial` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '拼音首字母', + `quanPin` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '全拼', + `chatroomAvatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群头像', + `isDeleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `accountId` int(11) NULL DEFAULT 0 COMMENT '账号ID', + `accountUserName` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号用户名', + `accountRealName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号真实姓名', + `accountNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号昵称', + `groupId` int(11) NULL DEFAULT 0 COMMENT '分组ID', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + `isTop` tinyint(2) NULL DEFAULT 0 COMMENT '是否置顶', + `groupIds` int(11) NULL DEFAULT 0 COMMENT '新分组ID', + UNIQUE INDEX `uk_chatroom_account`(`chatroomId`, `wechatAccountId`) USING BTREE, + INDEX `wechatAccountId`(`wechatAccountId`) USING BTREE, + INDEX `chatroomId`(`chatroomId`) USING BTREE, + INDEX `wechatAccountWechatId`(`wechatAccountWechatId`) USING BTREE, + INDEX `idx_account_deleted`(`accountId`, `isDeleted`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_chatroom_member +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_chatroom_member`; +CREATE TABLE `s2_wechat_chatroom_member` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `chatroomId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '群聊ID', + `wechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '微信ID', + `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', + `avatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像', + `conRemark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `alias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '别名', + `friendType` tinyint(11) NULL DEFAULT 0 COMMENT '好友类型', + `createTime` int(10) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(10) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_chatroom_wechat`(`chatroomId`, `wechatId`) USING BTREE, + INDEX `chatroomId`(`chatroomId`) USING BTREE, + INDEX `wechatId`(`wechatId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 496929 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群成员表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_friend +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_friend`; +CREATE TABLE `s2_wechat_friend` ( + `id` int(11) NULL DEFAULT NULL COMMENT '好友id', + `wechatAccountId` int(11) NOT NULL COMMENT '所有者微信账号ID', + `alias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '好友微信号', + `wechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '好友微信ID', + `conRemark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注名', + `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', + `pyInitial` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '拼音首字母', + `quanPin` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '全拼', + `avatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像URL', + `gender` tinyint(1) NULL DEFAULT 0 COMMENT '性别', + `region` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '地区', + `addFrom` int(11) NULL DEFAULT NULL COMMENT '添加来源', + `labels` json NULL COMMENT '标签JSON', + `siteLabels` json NULL COMMENT '站内标签JSON', + `signature` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '个性签名', + `isDeleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除', + `isPassed` tinyint(1) NULL DEFAULT 1 COMMENT '是否通过', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + `accountId` int(11) NULL DEFAULT 0 COMMENT '账号ID', + `extendFields` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '扩展字段JSON', + `accountUserName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号用户名', + `accountRealName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号真实姓名', + `accountNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号昵称', + `ownerAlias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '所有者别名', + `ownerWechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '所有者微信ID', + `ownerNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '所有者昵称', + `ownerAvatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '所有者头像', + `phone` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '电话', + `thirdParty` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '第三方数据JSON', + `groupId` int(11) NULL DEFAULT 0 COMMENT '分组ID', + `passTime` int(11) NULL DEFAULT NULL COMMENT '通过时间', + `additionalPicture` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '附加图片', + `desc` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '描述', + `country` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '国家', + `privince` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '省份', + `city` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '城市', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + `R` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0', + `F` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0', + `M` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0', + `realName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '姓名', + `company` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '公司', + `position` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '职位', + `isTop` tinyint(2) NULL DEFAULT 0 COMMENT '是否置顶', + `groupIds` int(11) NULL DEFAULT 0 COMMENT '新分组ID', + UNIQUE INDEX `uk_owner_wechat_account`(`ownerWechatId`, `wechatId`, `wechatAccountId`) USING BTREE, + INDEX `idx_wechat_account_id`(`wechatAccountId`) USING BTREE, + INDEX `idx_wechat_id`(`wechatId`) USING BTREE, + INDEX `idx_owner_wechat_id`(`ownerWechatId`) USING BTREE, + INDEX `idx_id`(`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信好友表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_group +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_group`; +CREATE TABLE `s2_wechat_group` ( + `id` int(11) NOT NULL, + `tenantId` int(11) NULL DEFAULT NULL, + `groupName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `groupMemo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `groupType` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `sortIndex` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `groupOwnerType` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `departmentId` int(11) NULL DEFAULT NULL, + `accountId` int(11) NULL DEFAULT NULL, + `createTime` int(11) NULL DEFAULT NULL, + `isDel` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_message +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_message`; +CREATE TABLE `s2_wechat_message` ( + `id` bigint(20) NOT NULL COMMENT '消息ID', + `type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '消息类型 1好友 2群', + `wechatFriendId` bigint(20) NULL DEFAULT NULL COMMENT '微信好友ID', + `wechatChatroomId` bigint(20) NOT NULL COMMENT '微信群聊ID', + `senderNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '发送者昵称', + `senderWechatId` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '发送者微信ID', + `senderIsAdmin` tinyint(1) NULL DEFAULT 0 COMMENT '发送者是否管理员', + `senderIsDeleted` tinyint(1) NULL DEFAULT 0 COMMENT '发送者是否已删除', + `senderChatroomNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '发送者群昵称', + `senderWechatAccountId` bigint(20) NULL DEFAULT NULL COMMENT '发送者微信账号ID', + `wechatAccountId` bigint(20) NULL DEFAULT NULL COMMENT '微信账号ID', + `tenantId` bigint(20) NULL DEFAULT NULL COMMENT '租户ID', + `accountId` bigint(20) NULL DEFAULT NULL COMMENT '账号ID', + `synergyAccountId` bigint(20) NULL DEFAULT 0 COMMENT '协同账号ID', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '消息内容', + `originalContent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '消息内容(原版)', + `msgType` int(11) NULL DEFAULT NULL COMMENT '消息类型 1 文字 3图片 47动态图片 34语言 43视频 42名片 40/20链接 49文件 419430449转账 436207665红包', + `msgSubType` int(11) NULL DEFAULT 0 COMMENT '消息子类型', + `msgSvrId` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '消息服务器ID', + `isSend` tinyint(1) NULL DEFAULT 1 COMMENT '是否发送', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `isDeleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否已删除', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + `sendStatus` int(11) NULL DEFAULT 0 COMMENT '发送状态', + `wechatTime` int(11) NULL DEFAULT NULL COMMENT '微信时间', + `origin` int(11) NULL DEFAULT 0 COMMENT '来源', + `msgId` bigint(20) NULL DEFAULT NULL COMMENT '消息ID', + `recallId` tinyint(1) NULL DEFAULT 0 COMMENT '撤回ID', + `isRead` tinyint(1) NULL DEFAULT 0 COMMENT '是否读取', + `is_counted` tinyint(1) NULL DEFAULT 0 COMMENT '是否已统计(0=未统计,1=已统计)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_wechatChatroomId`(`wechatChatroomId`) USING BTREE, + INDEX `idx_wechatAccountId`(`wechatAccountId`) USING BTREE, + INDEX `idx_msgSvrId`(`msgSvrId`) USING BTREE, + INDEX `idx_type`(`type`) USING BTREE, + INDEX `idx_type_wechatTime`(`type`, `wechatTime`, `id`) USING BTREE, + INDEX `idx_friend_time`(`wechatFriendId`, `wechatTime`, `id`) USING BTREE, + INDEX `idx_chatroom_time`(`wechatChatroomId`, `wechatTime`, `id`) USING BTREE, + INDEX `idx_account_type`(`accountId`, `type`, `wechatTime`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群聊消息记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_moments +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_moments`; +CREATE TABLE `s2_wechat_moments` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `wechatAccountId` int(11) NOT NULL COMMENT '微信账号ID', + `wechatFriendId` int(11) NULL DEFAULT NULL COMMENT '微信好友ID', + `snsId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '朋友圈消息ID', + `commentList` json NULL COMMENT '评论列表JSON', + `createTime` bigint(20) NULL DEFAULT 0 COMMENT '创建时间戳', + `likeList` json NULL COMMENT '点赞列表JSON', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '朋友圈内容', + `lat` decimal(10, 6) NULL DEFAULT 0.000000 COMMENT '纬度', + `lng` decimal(10, 6) NULL DEFAULT 0.000000 COMMENT '经度', + `location` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '位置信息', + `picSize` int(11) NULL DEFAULT 0 COMMENT '图片大小', + `resUrls` json NULL COMMENT '资源URL列表', + `userName` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户名', + `type` int(11) NULL DEFAULT 0 COMMENT '朋友圈类型', + `create_time` int(11) NULL DEFAULT NULL COMMENT '数据创建时间', + `update_time` int(11) NULL DEFAULT NULL COMMENT '数据更新时间', + `coverImage` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `urls` json NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `idx_sns_account`(`snsId`, `wechatAccountId`) USING BTREE, + INDEX `idx_account_friend`(`wechatAccountId`, `wechatFriendId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 40159 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信朋友圈数据表' ROW_FORMAT = Dynamic; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/think b/think new file mode 100644 index 0000000..bd31715 --- /dev/null +++ b/think @@ -0,0 +1,28 @@ +#!/usr/bin/env php + +// +---------------------------------------------------------------------- + +namespace think; + +define('ROOT_PATH', __DIR__); +define('DS', DIRECTORY_SEPARATOR); + +$_SERVER['HTTPS'] = true; +$_SERVER['HTTP_HOST'] = ''; + +// 加载基础文件 +require __DIR__ . '/thinkphp/base.php'; + +// 应用初始化 +Container::get('app')->path(__DIR__ . '/application/')->initialize(); + +// 控制台初始化 +Console::init(); \ No newline at end of file diff --git a/依赖.rar b/依赖.rar new file mode 100644 index 0000000..3b7bd07 Binary files /dev/null and b/依赖.rar differ diff --git a/微信健康分规则v2.md b/微信健康分规则v2.md new file mode 100644 index 0000000..90c63f0 --- /dev/null +++ b/微信健康分规则v2.md @@ -0,0 +1,58 @@ +# 微信健康分规则 v2 + +## 一、定义 + +当客户收到手机设备后,登录了微信号,我们将对其微信号进行健康分的评估。 + +**健康分 = 基础分 + 动态分** + +健康分只与系统中的"每日自动添加好友次数"这个功能相关联。\ +通过健康分体系来定义一个微信号每日**最佳、最稳定的添加次数**。\ +后期还可将健康分作为标签属性,用于快速筛选微信号。 + +**公式:每日最大加人次数 = 健康分 × 0.2** + +## 二、基础分 + +基础分为 **60--100 分**。 + +由 `60 + 40(基础加成分)` 四个维度参数组成,每个参数具有不同权重。 + +### 基础分组成 + + 类型 权重 分数 + ------------ ------ ------ + 基础信息 0.2 10 + 好友数量 0.3 30 + 默认基础分 --- 60 + +### 1. 基础信息(权重 0.2,满分 10) + + 类型 权重 分数 + -------------- ------ ------ + 已修改微信号 1 10 + +### 2. 好友数量(权重 0.3,满分 30) + + 好友数量范围 权重 分数 + -------------- ------ ------ + 0--50 0.1 3 + 51--500 0.2 6 + 501--3000 0.3 8 + 3001 以上 0.4 12 + +## 三、动态分规则 + +### 扣分规则 + + 场景 扣分 处罚 + ---------- ------ -------------- + 首次频繁 15 暂停 24 小时 + 再次频繁 25 暂停 24 小时 + 封号 60 暂停 72 小时 + +### 加分规则 + + 场景 加分 + --------------------- ------ + 连续 3 天不触发频繁 5/日