存客宝应用接口初始化

This commit is contained in:
Ghost
2026-01-05 10:25:32 +08:00
parent fa37227954
commit 02797ba50a
414 changed files with 85530 additions and 75 deletions

18
.gitignore vendored Normal file
View File

@@ -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/

0
.htaccess Normal file
View File

View File

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

View File

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

147
README_moments.md Normal file
View File

@@ -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直接存储在朋友圈主表中不再单独存储到资源表。

254
README_scheduler.md Normal file
View File

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

View File

@@ -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. 数据同步是增量的,会自动更新已存在的记录

View File

@@ -0,0 +1,249 @@
# RFM 客户价值评分体系技术实施文档
## 1. 文档目的
本文档旨在明确 RFMRecency-Frequency-Monetary客户价值评分体系的技术实现标准包括维度定义、评分规则、数据处理流程、参数配置及异常处理方案为系统开发、数据分析及业务应用提供统一依据。
## 2. 核心术语定义
| 术语 | 英文缩写 | 定义 | 数据来源 | 统计周期说明 |
| ------ | ------------ | ------------------------------- | --------- | ---------------------------------- |
| 最近消费时间 | RecencyR | 客户最后一次有效消费行为距统计截止日的时间间隔(单位:天) | 订单系统、交易日志 | 支持自定义配置(默认 3-12 个月,按业务场景调整) |
| 消费频率 | FrequencyF | 统计周期内客户发生有效消费行为的总次数 | 订单系统、交易日志 | 与 R 维度统计周期一致,剔除重复下单、取消订单等无效记录 |
| 消费金额 | MonetaryM | 统计周期内客户有效消费行为的总金额(单位:元,支持多币种换算) | 订单系统、支付日志 | 仅统计已支付完成的订单金额,剔除退款、优惠抵扣部分 |
| RFM 总分 | RFM Score | 基于 R、F、M 三个维度的分项得分,按预设权重计算的综合得分 | 系统计算生成 | 得分范围 1-15 分5 分制单项)或 1-100 分(标准化后) |
## 3. 评分规则技术规范
### 3.1 分项评分规则(默认 5 分制)
#### 3.1.1 RecencyR评分规则
* 核心逻辑:时间间隔越短,得分越高(反向映射)
* 分段标准:采用**五分位法**(按数据分布自动划分区间,避免均分失真)
| 得分 | 时间间隔区间(天) | 划分逻辑 |
| --- | ---------- | ----------------- |
| 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 FrequencyF评分规则
* 核心逻辑:消费次数越多,得分越高(正向映射)
* 分段标准:采用**五分位法**(支持最小消费次数阈值配置)
| 得分 | 消费次数区间 | 划分逻辑 |
| --- | ----------- | ------------------- |
| 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 MonetaryM评分规则
* 核心逻辑:消费金额越高,得分越高(正向映射)
* 分段标准:采用**五分位法**(支持剔除大额异常值后划分)
| 得分 | 消费金额区间(元) | 划分逻辑 |
| --- | ----------- | ------------------- |
| 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 原始指标:
* RMAX (消费时间) 到统计截止日的时间间隔(天)
* FCOUNT (DISTINCT 订单唯一标识)
* MSUM (消费金额)
1. **缺失值处理**
* 统计周期内无消费记录的客户R = 统计周期总天数F=0M=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 生成)

1
application/.htaccess Normal file
View File

@@ -0,0 +1 @@
deny from all

View File

@@ -0,0 +1,18 @@
<?php
use think\facade\Route;
// 定义RESTful风格的API路由
Route::group('v1/ai', function () {
//openai、chatGPT
Route::group('openai', function () {
Route::post('text', 'app\ai\controller\OpenAI@text');
});
//豆包ai
Route::group('doubao', function () {
Route::post('text', 'app\ai\controller\DouBaoAI@text');
});
})->middleware(['jwt']);

View File

@@ -0,0 +1,406 @@
<?php
namespace app\ai\controller;
use think\facade\Env;
use think\Controller;
class CozeAI extends Controller
{
protected $apiUrl;
protected $accessToken;
protected $headers;
/**
* 初始化
*/
public function __construct()
{
parent::__construct();
// 从环境变量获取配置
$this->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' => []]);
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace app\ai\controller;
use app\common\util\JwtUtil;
use think\facade\Env;
use think\Controller;
class DouBaoAI extends Controller
{
protected $apiUrl;
protected $apiKey;
protected $headers;
public function __construct()
{
parent::__construct();
$this->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]]);
}
}
}

View File

@@ -0,0 +1,145 @@
<?php
namespace app\ai\controller;
use think\facade\Env;
use think\Controller;
class OpenAI extends Controller
{
protected $apiUrl;
protected $apiKey;
protected $headers;
public function __construct()
{
parent::__construct();
$this->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;
}
}

View File

@@ -0,0 +1,109 @@
<?php
// store模块路由配置
use think\facade\Route;
// 定义RESTful风格的API路由
Route::group('v1', function () {
Route::group('api', function () {
// Account控制器路由
Route::group('account', function () {
Route::get('list', 'app\api\controller\AccountController@getList'); // 获取账号列表 √
Route::post('create', 'app\api\controller\AccountController@createAccount'); // 创建账号 √
Route::post('createNewAccount', 'app\api\controller\AccountController@createNewAccount'); // 创建新账号(包含创建部门) √
Route::post('department/create', 'app\api\controller\AccountController@createDepartment'); // 创建部门 √
Route::get('department/list', 'app\api\controller\AccountController@getDepartmentList'); // 获取部门列表 √
Route::post('department/update', 'app\api\controller\AccountController@updateDepartment'); // 更新部门 √
Route::post('department/delete', 'app\api\controller\AccountController@deleteDepartment'); // 删除部门 √
Route::post('department/setPrivileges', 'app\api\controller\AccountController@setPrivileges'); // 设置部门权限 √
});
// Device控制器路由
Route::group('device', function () {
Route::get('list', 'app\api\controller\DeviceController@getList'); // 获取设备列表 √
Route::post('add', 'app\api\controller\DeviceController@addDevice'); // 生成设备二维码POST方式
Route::post('updateDeviceGroup', 'app\api\controller\DeviceController@updateDeviceGroup'); // 更新设备分组 √
Route::post('updateaccount', 'app\api\controller\DeviceController@updateaccount'); // 更新设备账号 √
Route::post('createGroup', 'app\api\controller\DeviceController@createGroup'); // 创建设备分组 √
Route::get('groupList', 'app\api\controller\DeviceController@getGroupList'); // 获取设备分组列表 √
Route::post('updateDeviceToGroup', 'app\api\controller\DeviceController@updateDeviceToGroup'); // 更新设备的分组 √
Route::post('importContact', 'app\api\controller\DeviceController@importContact'); // 更新设备联系人 √
});
// FriendTask控制器路由
Route::group('friend-task', function () {
Route::get('list', 'app\api\controller\FriendTaskController@getList'); // 获取添加好友记录列表 √
Route::post('add', 'app\api\controller\FriendTaskController@addFriendTask'); // 添加好友任务 √
});
// Moments控制器路由
Route::group('moments', function () {
Route::post('add-job', 'app\api\controller\MomentsController@addJob'); // 发布朋友圈
Route::get('list', 'app\api\controller\MomentsController@getList'); // 获取朋友圈任务列表 √
});
// Stats控制器路由
Route::group('stats', function () {
Route::get('basic-data', 'app\api\controller\StatsController@basicData'); // 账号基本信息
Route::get('fans-statistics', 'app\api\controller\StatsController@FansStatistics'); // 好友统计
});
// User控制器路由
Route::group('user', function () {
Route::post('login', 'app\api\controller\UserController@login'); // 登录 √
Route::post('token', 'app\api\controller\UserController@getNewToken'); // 获取新的token √
Route::get('info', 'app\api\controller\UserController@getAccountInfo'); // 获取商户基本信息 √
Route::post('modify-pwd', 'app\api\controller\UserController@modifyPwd'); // 修改密码
Route::get('logout', 'app\api\controller\UserController@logout'); // 登出 √
Route::get('verify-code', 'app\api\controller\UserController@getVerifyCode'); // 获取验证码 √
});
// WebSocket控制器路由
Route::group('websocket', function () {
Route::post('send-personal', 'app\api\controller\WebSocketController@sendPersonal'); // 个人消息发送 √
Route::post('send-community', 'app\api\controller\WebSocketController@sendCommunity'); // 发送群消息 √
Route::get('get-moments', 'app\api\controller\WebSocketController@getMoments'); // 获取指定账号朋友圈信息 √
Route::get('get-moment-source', 'app\api\controller\WebSocketController@getMomentSourceRealUrl'); // 获取指定账号朋友圈图片地址
});
// WechatChatroom控制器路由
Route::group('chatroom', function () {
Route::get('list', 'app\api\controller\WechatChatroomController@getList'); // 获取微信群聊列表 √
Route::get('members', 'app\api\controller\WechatChatroomController@listChatroomMember'); // 获取群成员列表 √
// Route::get('sync', 'app\api\controller\WechatChatroomController@syncChatrooms'); // 同步微信群聊数据 √
});
// Wechat控制器路由
Route::group('wechat', function () {
Route::get('list', 'app\api\controller\WechatController@getList'); // 获取微信账号列表 √
});
// WechatFriend控制器路由
Route::group('friend', function () {
Route::get('list', 'app\api\controller\WechatFriendController@getList'); // 获取微信好友列表数据 √
});
// Message控制器路由
Route::group('message', function () {
Route::get('getFriendsList', 'app\api\controller\MessageController@getFriendsList'); // 获取微信好友列表 √
Route::get('getChatroomList', 'app\api\controller\MessageController@getChatroomList'); // 同步群聊消息 √
});
// AllotRule控制器路由
Route::group('allot-rule', function () {
Route::get('list', 'app\api\controller\AllotRuleController@getAllRules'); // 获取所有分配规则 √
Route::post('create', 'app\api\controller\AllotRuleController@createRule');// 创建分配规则 √
Route::post('edit', 'app\api\controller\AllotRuleController@updateRule');// 编辑分配规则 √
Route::delete('del', 'app\api\controller\AllotRuleController@deleteRule');// 删除分配规则 √
Route::get('autoCreate', 'app\api\controller\AllotRuleController@autoCreateAllotRules');// 自动创建分配规则 √
});
// CallRecording控制器路由
Route::group('call-recording', function () {
Route::get('list', 'app\api\controller\CallRecordingController@getlist'); // 获取通话记录列表 √
});
});
});

View File

@@ -0,0 +1,748 @@
<?php
namespace app\api\controller;
use app\api\model\CompanyAccountModel;
use app\api\model\CompanyModel;
use Library\S2\Logics\AccountLogic;
use think\Db;
use think\facade\Request;
/**
* 账号管理控制器
* 包含账号管理和部门管理的相关功能
*/
class AccountController extends BaseController
{
/************************ 账号管理相关接口 ************************/
/**
* 获取公司账号列表
* @param string $pageIndex 页码
* @param string $pageSize 每页数量
* @param bool $isInner 是否为定时任务调用
* @return \think\response\Json
*/
public function getlist($data = [], $isInner = false)
{
$pageIndex = !empty($data['pageIndex']) ? $data['pageIndex'] : 0;
$pageSize = !empty($data['pageSize']) ? $data['pageSize'] : 20;
$showNormalAccount = !empty($data['showNormalAccount']) ? $data['showNormalAccount'] : '';
$keyword = !empty($data['keyword']) ? $data['keyword'] : '';
$departmentId = !empty($data['departmentId']) ? $data['departmentId'] : '';
// 获取授权token
$authorization = trim($this->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);
}
}
}

View File

@@ -0,0 +1,362 @@
<?php
namespace app\api\controller;
use app\api\model\CompanyAccountModel;
use app\api\model\AllotRuleModel;
use app\api\model\WechatAccountModel;
use think\Queue;
use app\job\AllotRuleListJob;
use think\Db;
class AllotRuleController extends BaseController
{
/************************************
* 分配规则列表和数据同步相关方法
************************************/
/**
* 获取所有分配规则
* @param bool $isInner 是否为内部调用
* @return \think\response\Json
*/
public function getAllRules($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, '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);
}
}
}

View File

@@ -0,0 +1,346 @@
<?php
namespace app\api\controller;
use app\api\model\CompanyAccountModel;
use app\api\model\CompanyModel;
use Library\S2\Logics\AccountLogic;
use think\Db;
use think\facade\Request;
/**
* 账号管理控制器
* 包含账号管理和部门管理的相关功能
*/
class AutomaticAssign extends BaseController
{
/**
* 自动分配微信好友
* @return \think\response\Json
*/
public function autoAllotWechatFriend($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 {
// 获取请求参数
$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.'&notifyReceiver='.$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.'&notifyReceiver='.$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()]);
}
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace app\api\controller;
use think\Controller;
use think\facade\Env;
use app\common\service\AuthService;
class BaseController extends Controller
{
/**
* 令牌
*
* @var string
*/
protected $token = '';
protected $baseUrl;
protected $authorization = '';
public function __construct()
{
parent::__construct();
$this->baseUrl = Env::get('api.wechat_url');
$this->authorization = AuthService::getSystemAuthorization();
}
}

View File

@@ -0,0 +1,142 @@
<?php
namespace app\api\controller;
use app\api\model\CompanyAccountModel;
use app\api\model\CompanyModel;
use app\api\model\CallRecordingModel;
use Library\S2\Logics\AccountLogic;
use think\Db;
use think\facade\Request;
/**
* 通话记录控制器
* 包含通话记录管理的相关功能
*/
class CallRecordingController extends BaseController
{
/**
* 获取通话记录列表
* @param array $data 请求参数
* @param bool $isInner 是否为定时任务调用
* @return \think\response\Json
*/
public function getlist($data = [], $isInner = false)
{
// 获取请求参数
$keyword = !empty($data['keyword']) ? $data['keyword'] : '';
$isCallOut = !empty($data['isCallOut']) ? $data['isCallOut'] : '';
$secondMin = !empty($data['secondMin']) ? $data['secondMin'] : 0;
$secondMax = !empty($data['secondMax']) ? $data['secondMax'] : 99999;
$departmentIds = !empty($data['departmentIds']) ? $data['departmentIds'] : '';
$pageIndex = !empty($data['pageIndex']) ? $data['pageIndex'] : 0;
$pageSize = !empty($data['pageSize']) ? $data['pageSize'] : 100;
$from = !empty($data['from']) ? $data['from'] : '2016-01-01 00:00:00';
$to = !empty($data['to']) ? $data['to'] : '2025-08-31 00:00:00';
$departmentId = !empty($data['departmentId']) ? $data['departmentId'] : '';
// 获取授权token
$authorization = trim($this->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);
}
}
}

View File

@@ -0,0 +1,769 @@
<?php
namespace app\api\controller;
use app\api\model\DeviceModel;
use app\api\model\DeviceGroupModel;
use think\Db;
use think\facade\Request;
use think\facade\Env;
use Endroid\QrCode\QrCode;
use Endroid\QrCode\ErrorCorrectionLevel;
class DeviceController extends BaseController
{
/************************ 设备管理相关接口 ************************/
/**
* 获取设备列表
* @param string $pageIndex 页码
* @param string $pageSize 每页数量
* @param bool $isInner 是否为内部调用
* @return \think\response\Json
*/
public function getlist($data = [],$isInner = false,$isDel = 0)
{
// 获取授权token
$authorization = $this->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;
}
}

View File

@@ -0,0 +1,192 @@
<?php
namespace app\api\controller;
use app\api\model\FriendTaskModel;
use app\common\model\WechatRestricts;
use think\facade\Request;
class FriendTaskController extends BaseController
{
/************************ 好友任务管理相关接口 ************************/
/**
* 获取添加好友记录列表
* @param int $pageIndex 页码
* @param int $pageSize 每页数量
* @param bool $isInner 是否为定时任务调用
* @return \think\response\Json
*/
public function getlist($pageIndex, $pageSize, $isInner = false)
{
// 获取授权token
$authorization = $this->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);
}
}
}
}

View File

@@ -0,0 +1,584 @@
<?php
namespace app\api\controller;
use app\api\model\WechatMessageModel;
use think\Db;
use think\facade\Request;
class MessageController extends BaseController
{
/************************ 好友消息相关接口 ************************/
/**
* 获取微信好友列表
* @return \think\response\Json
*/
public function getFriendsList($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', ''),
'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
];
}
}

View File

@@ -0,0 +1,153 @@
<?php
namespace app\api\controller;
use think\facade\Request;
class MomentsController extends BaseController
{
/************************ 朋友圈发布相关接口 ************************/
/**
* 发布朋友圈
* @return \think\response\Json
*/
public function addJob($data = [])
{
// 获取授权token
$authorization = $this->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());
}
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace app\api\controller;
/**
* 统计控制器
* Class StatsController
* @package app\frontend\controller
*/
class StatsController extends BaseController
{
/**
* API客户端类型
*/
const CLIENT_TYPE = 'system';
/**
* 账号基本信息
* @return \think\response\Json
*/
public function basicData()
{
$authorization = trim($this->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());
}
}
}

View File

@@ -0,0 +1,365 @@
<?php
namespace app\api\controller;
use app\api\model\CompanyAccountModel;
use think\facade\Env;
use think\Response;
/**
* 用户控制器
* Class UserController
* @package app\frontend\controller
*/
class UserController extends BaseController
{
/**
* API客户端类型
*/
const CLIENT_TYPE = 'system';
/**
* 构造函数
*/
public function __construct()
{
parent::__construct();
}
/**
* 登录
* @return \think\response\Json
*/
public function login()
{
// 获取并验证参数
$params = $this->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,
];
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,559 @@
<?php
namespace app\api\controller;
use think\cache\driver\Redis;
use think\Db;
use think\Log;
use WebSocket\Client;
use think\facade\Env;
class WebSocketControllerCopy extends BaseController
{
protected $authorized;
protected $accountId;
protected $client;
/************************************
* 初始化相关功能
************************************/
/**
* 构造函数 - 初始化WebSocket连接
* @param array $userData 用户数据
*/
public function __construct($userData = [])
{
parent::__construct();
if(!empty($userData) && count($userData)){
if (empty($userData['userName']) || empty($userData['password'])) {
return json_encode(['code'=>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]);
}
}

View File

@@ -0,0 +1,255 @@
<?php
namespace app\api\controller;
use app\api\model\WechatChatroomModel;
use app\api\model\WechatChatroomMemberModel;
use app\job\WechatChatroomJob;
use think\facade\Request;
class WechatChatroomController extends BaseController
{
/**
* 获取微信群聊列表
* @return \think\response\Json
*/
public function getlist($data = [],$isInner = false, $isDel = '')
{
// 获取授权token
$authorization = $this->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());
}
}
}

View File

@@ -0,0 +1,295 @@
<?php
namespace app\api\controller;
use app\api\model\WechatAccountModel;
use think\Db;
/**
* 微信账号管理控制器
*/
class WechatController extends BaseController
{
/**
* 获取微信账号列表(主方法)
*
* @param string $pageIndex 页码
* @param string $pageSize 每页大小
* @param bool $isInner 是否为任务调用
* @return \think\response\Json
*/
public function getList($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('缺少授权信息');
}
}
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()]);
}
}
}
}

View File

@@ -0,0 +1,165 @@
<?php
namespace app\api\controller;
use app\api\model\WechatFriendModel;
use think\facade\Request;
use think\facade\Log;
class WechatFriendController extends BaseController
{
/**
* 获取微信好友列表数据
* @param string $pageIndex 页码
* @param string $pageSize 每页大小
* @param string $preFriendId 上一个好友ID
* @param bool $isInner 是否为任务调用
* @return \think\response\Json
*/
public function getlist($data = [], $isInner = false,$isDel = '')
{
// 获取授权token
$authorization = trim($this->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;
}
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace app\api\model;
use think\Model;
class AllotRuleModel extends Model
{
// 设置表名
protected $table = 's2_allot_rule';
}

View File

@@ -0,0 +1,11 @@
<?php
namespace app\api\model;
use think\Model;
class CallRecordingModel extends Model
{
// 设置表名
protected $table = 's2_call_recording';
}

View File

@@ -0,0 +1,11 @@
<?php
namespace app\api\model;
use think\Model;
class CompanyAccountModel extends Model
{
// 设置表名
protected $table = 's2_company_account';
}

View File

@@ -0,0 +1,10 @@
<?php
namespace app\api\model;
use think\Model;
class CompanyModel extends Model {
// 设置表名
protected $table = 's2_department';
}

View File

@@ -0,0 +1,29 @@
<?php
namespace app\api\model;
use think\Model;
class DeviceGroupModel extends Model {
// 设置表名
protected $table = 's2_device_group';
// 设置主键
protected $pk = 'id';
// 自动写入时间戳
protected $autoWriteTimestamp = true;
// 定义时间戳字段名
protected $createTime = 'createTime';
protected $updateTime = 'updateTime';
// 设置字段类型
protected $type = [
'id' => 'integer',
'tenantId' => 'integer',
'count' => 'integer',
'createTime' => 'integer',
'updateTime' => 'integer'
];
}

View File

@@ -0,0 +1,10 @@
<?php
namespace app\api\model;
use think\Model;
class DeviceModel extends Model {
// 设置表名
protected $table = 's2_device';
}

View File

@@ -0,0 +1,11 @@
<?php
namespace app\api\model;
use think\Model;
class FriendTaskModel extends Model
{
// 设置表名
protected $table = 's2_friend_task';
}

View File

@@ -0,0 +1,11 @@
<?php
namespace app\api\model;
use think\Model;
class WechatAccountModel extends Model
{
// 设置表名
protected $table = 's2_wechat_account';
}

View File

@@ -0,0 +1,11 @@
<?php
namespace app\api\model;
use think\Model;
class WechatChatroomMemberModel extends Model
{
// 设置表名
protected $table = 's2_wechat_chatroom_member';
}

View File

@@ -0,0 +1,11 @@
<?php
namespace app\api\model;
use think\Model;
class WechatChatroomModel extends Model
{
// 设置表名
protected $table = 's2_wechat_chatroom';
}

View File

@@ -0,0 +1,16 @@
<?php
namespace app\api\model;
use think\Model;
class WechatFriendModel extends Model
{
// 设置表名
protected $table = 's2_wechat_friend';
protected $pk = 'id';
/*protected $pk = [
'uk_owner_wechat_account' => ['ownerWechatId', 'wechatId','wechatAccountId'] // uk_owner_wechat_account 是数据库中组合唯一键的名称
];*/
}

View File

@@ -0,0 +1,11 @@
<?php
namespace app\api\model;
use think\Model;
class WechatMessageModel extends Model
{
// 设置表名
protected $table = 's2_wechat_message';
}

View File

@@ -0,0 +1,11 @@
<?php
namespace app\api\model;
use think\Model;
class WechatMomentsModel extends Model
{
// 设置表名
protected $table = 's2_wechat_moments';
}

View File

@@ -0,0 +1,214 @@
<?php
// +----------------------------------------------------------------------
// | 设备管理模块路由配置
// +----------------------------------------------------------------------
use think\facade\Route;
// 定义RESTful风格的API路由
Route::group('v1/', function () {
Route::group('kefu/', function () {
//好友相关
Route::group('wechatFriend/', function () {
Route::get('list', 'app\chukebao\controller\WechatFriendController@getList'); // 获取好友列表
Route::get('detail', 'app\chukebao\controller\WechatFriendController@getDetail'); // 获取好友详情
Route::post('updateInfo', 'app\chukebao\controller\WechatFriendController@updateFriendInfo'); // 更新好友资料
// 添加好友任务记录相关接口
Route::get('addTaskList', 'app\chukebao\controller\WechatFriendController@getAddTaskList'); // 获取添加好友任务记录列表包含添加者信息、状态、时间等支持状态筛选无需传好友ID
});
//群相关
Route::group('wechatChatroom/', function () {
Route::get('list', 'app\chukebao\controller\WechatChatroomController@getList'); // 获取好友列表
Route::get('detail', 'app\chukebao\controller\WechatChatroomController@getDetail'); // 获取群详情
Route::get('members', 'app\chukebao\controller\WechatChatroomController@getMembers'); // 获取群成员列表
Route::post('aiAnnouncement', 'app\chukebao\controller\WechatChatroomController@aiAnnouncement'); // AI群公告
});
//客服相关
Route::group('customerService/', function () {
Route::get('list', 'app\chukebao\controller\CustomerServiceController@getList'); // 获取好友列表
});
//账号管理
Route::group('accounts/', function () {
Route::get('list', 'app\chukebao\controller\AccountsController@getList'); // 获取账号列表
});
//消息相关
Route::group('message/', function () {
Route::get('list', 'app\chukebao\controller\MessageController@getList'); // 获取好友列表
Route::get('readMessage', 'app\chukebao\controller\MessageController@readMessage'); // 读取消息
Route::get('details', 'app\chukebao\controller\MessageController@details'); // 消息详情
Route::get('getMessageStatus', 'app\chukebao\controller\MessageController@getMessageStatus'); // 获取单条消息发送状态
});
//微信分组
Route::group('wechatGroup/', function () {
Route::get('list', 'app\chukebao\controller\WechatGroupController@getList'); // 获取分组列表
Route::post('add', 'app\chukebao\controller\WechatGroupController@create'); // 新增分组
Route::post('update', 'app\chukebao\controller\WechatGroupController@update'); // 更新分组
Route::delete('delete', 'app\chukebao\controller\WechatGroupController@delete'); // 删除分组(假删除)
Route::post('move', 'app\chukebao\controller\WechatGroupController@move'); // 移动分组(好友/群移动到指定分组)
});
//AI相关
Route::group('ai/', function () {
//问答
Route::group('questions/', function () {
Route::get('list', 'app\chukebao\controller\QuestionsController@getList'); // 问答列表
Route::post('add', 'app\chukebao\controller\QuestionsController@create'); // 问答添加
Route::post('update', 'app\chukebao\controller\QuestionsController@update'); // 问答更新
Route::delete('delete', 'app\chukebao\controller\QuestionsController@delete'); // 问答删除
Route::get('detail', 'app\chukebao\controller\QuestionsController@detail'); // 问答详情
});
//全局配置
Route::group('settings/', function () {
Route::get('get', 'app\chukebao\controller\AiSettingsController@getSetting');
Route::post('set', 'app\chukebao\controller\AiSettingsController@setSetting');
});
//好友配置
Route::group('friend/', function () {
Route::post('set', 'app\chukebao\controller\AiSettingsController@setFriend');
Route::get('get', 'app\chukebao\controller\AiSettingsController@getFriend');
Route::post('setAll', 'app\chukebao\controller\AiSettingsController@setAllFriend');
});
//ai对话
Route::get('getUserTokens', 'app\chukebao\controller\AiSettingsController@getUserTokens');
Route::post('chat', 'app\chukebao\controller\AiChatController@index');
});
//代办事项
Route::group('todo/', function () {
Route::get('list', 'app\chukebao\controller\ToDoController@getList');
Route::post('add', 'app\chukebao\controller\ToDoController@create');
Route::get('process', 'app\chukebao\controller\ToDoController@process');
});
//跟进提醒
Route::group('followUp/', function () {
Route::get('list', 'app\chukebao\controller\FollowUpController@getList');
Route::post('add', 'app\chukebao\controller\FollowUpController@create');
Route::get('process', 'app\chukebao\controller\FollowUpController@process');
});
//算力相关
Route::group('tokensRecord/', function () {
Route::get('list', 'app\chukebao\controller\TokensRecordController@getList');
});
//内容管理
Route::group('content/', function () {
//素材管理
Route::group('material/', function () {
Route::get('all', 'app\chukebao\controller\ContentController@getAllMaterial');
Route::get('list', 'app\chukebao\controller\ContentController@getMaterial');
Route::post('add', 'app\chukebao\controller\ContentController@createMaterial');
Route::get('details', 'app\chukebao\controller\ContentController@detailsMaterial');
Route::delete('del', 'app\chukebao\controller\ContentController@delMaterial');
Route::post('update', 'app\chukebao\controller\ContentController@updateMaterial');
});
//违禁词管理
Route::group('sensitiveWord/', function () {
Route::get('list', 'app\chukebao\controller\ContentController@getSensitiveWord');
Route::post('add', 'app\chukebao\controller\ContentController@createSensitiveWord');
Route::get('details', 'app\chukebao\controller\ContentController@detailsSensitiveWord');
Route::delete('del', 'app\chukebao\controller\ContentController@delSensitiveWord');
Route::post('update', 'app\chukebao\controller\ContentController@updateSensitiveWord');
Route::get('setStatus', 'app\chukebao\controller\ContentController@setSensitiveWordStatus');
});
//关键词管理
Route::group('keywords/', function () {
Route::get('list', 'app\chukebao\controller\ContentController@getKeywords');
Route::post('add', 'app\chukebao\controller\ContentController@createKeywords');
Route::get('details', 'app\chukebao\controller\ContentController@detailsKeywords');
Route::delete('del', 'app\chukebao\controller\ContentController@delKeywords');
Route::post('update', 'app\chukebao\controller\ContentController@updateKeywords');
Route::get('setStatus', 'app\chukebao\controller\ContentController@setKeywordStatus');
});
});
//自动问候
Route::group('autoGreetings/', function () {
Route::get('list', 'app\chukebao\controller\AutoGreetingsController@getList');
Route::post('add', 'app\chukebao\controller\AutoGreetingsController@create');
Route::get('details', 'app\chukebao\controller\AutoGreetingsController@details');
Route::delete('del', 'app\chukebao\controller\AutoGreetingsController@del');
Route::post('update', 'app\chukebao\controller\AutoGreetingsController@update');
Route::get('setStatus', 'app\chukebao\controller\AutoGreetingsController@setStatus');
Route::get('copy', 'app\chukebao\controller\AutoGreetingsController@copy');
Route::get('stats', 'app\chukebao\controller\AutoGreetingsController@stats');
});
//AI智能推送
Route::group('aiPush/', function () {
Route::get('list', 'app\chukebao\controller\AiPushController@getList'); // 获取推送列表
Route::post('add', 'app\chukebao\controller\AiPushController@add'); // 添加推送
Route::get('details', 'app\chukebao\controller\AiPushController@details'); // 推送详情
Route::delete('del', 'app\chukebao\controller\AiPushController@del'); // 删除推送
Route::post('update', 'app\chukebao\controller\AiPushController@update'); // 更新推送
Route::get('setStatus', 'app\chukebao\controller\AiPushController@setStatus'); // 修改状态
Route::get('stats', 'app\chukebao\controller\AiPushController@stats'); // 统计概览
});
Route::group('notice/', function () {
Route::get('list', 'app\chukebao\controller\NoticeController@getList');
Route::put('readMessage', 'app\chukebao\controller\NoticeController@readMessage');
Route::put('readAll', 'app\chukebao\controller\NoticeController@readAll');
});
Route::group('reply/', function () {
Route::get('list', 'app\chukebao\controller\ReplyController@getList');
Route::post('addGroup', 'app\chukebao\controller\ReplyController@addGroup');
Route::post('addReply', 'app\chukebao\controller\ReplyController@addReply');
Route::post('updateGroup', 'app\chukebao\controller\ReplyController@updateGroup');
Route::post('updateReply', 'app\chukebao\controller\ReplyController@updateReply');
Route::delete('deleteGroup', 'app\chukebao\controller\ReplyController@deleteGroup');
Route::delete('deleteReply', 'app\chukebao\controller\ReplyController@deleteReply');
});
Route::group('moments/', function () {
Route::post('add', 'app\chukebao\controller\MomentsController@create');
Route::post('update', 'app\chukebao\controller\MomentsController@update');
Route::delete('delete', 'app\chukebao\controller\MomentsController@delete');
Route::get('list', 'app\chukebao\controller\MomentsController@getList');
});
Route::post('dataProcessing', 'app\chukebao\controller\DataProcessing@index'); // 修改数据
});
})->middleware(['jwt']);
// 客服登录
Route::group('v1/kefu', function () {
Route::post('login', 'app\chukebao\controller\LoginController@index'); // 登录
});
return [];

View File

@@ -0,0 +1,71 @@
<?php
namespace app\chukebao\controller;
use library\ResponseHelper;
use think\Db;
class AccountsController extends BaseController
{
/**
* 获取账号列表(过滤掉后缀为 _offline 与 _delete 的账号)
* @return \think\response\Json
*/
public function getList()
{
try {
$companyId = $this->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,
]);
}
}

View File

@@ -0,0 +1,832 @@
<?php
namespace app\chukebao\controller;
use app\ai\controller\CozeAI;
use app\ai\controller\DouBaoAI;
use app\api\model\WechatFriendModel;
use app\chukebao\controller\TokensRecordController as tokensRecord;
use app\chukebao\model\AiSettings;
use app\chukebao\model\FriendSettings;
use app\chukebao\model\TokensCompany;
use library\ResponseHelper;
use think\Db;
use think\facade\Cache;
use think\facade\Log;
/**
* AI聊天控制器
* 负责处理与好友的AI对话功能
*/
class AiChatController extends BaseController
{
// 对话状态常量
const STATUS_CREATED = 'created'; // 对话已创建
const STATUS_IN_PROGRESS = 'in_progress'; // 智能体正在处理中
const STATUS_COMPLETED = 'completed'; // 智能体已完成处理
const STATUS_FAILED = 'failed'; // 对话失败
const STATUS_REQUIRES_ACTION = 'requires_action'; // 对话中断,需要进一步处理
const STATUS_CANCELED = 'canceled'; // 对话已取消
// 轮询配置
const MAX_RETRY_TIMES = 1000; // 最大重试次数
const RETRY_INTERVAL = 500000; // 重试间隔微秒即500毫秒
// 并发控制
const CACHE_EXPIRE = 30; // 缓存过期时间(秒)
// 请求唯一标识符
private $requestKey = '';
private $requestId = '';
private $currentStep = 0;
/**
* AI聊天主入口
*
* @return \think\response\Json
*/
public function index()
{
try {
// 1. 参数验证和初始化
$params = $this->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']);
}
}
}

View File

@@ -0,0 +1,505 @@
<?php
namespace app\chukebao\controller;
use app\chukebao\model\AiPush;
use app\chukebao\model\AiPushRecord;
use app\chukebao\model\AutoGreetings;
use library\ResponseHelper;
use think\Db;
class AiPushController extends BaseController
{
/**
* 获取推送列表
* @return \think\response\Json
* @throws \Exception
*/
public function getList()
{
$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');
$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) {
// 静默失败,不影响主流程
}
}
}

View File

@@ -0,0 +1,266 @@
<?php
namespace app\chukebao\controller;
use app\chukebao\model\FriendSettings;
use app\chukebao\model\Questions;
use library\ResponseHelper;
use think\Db;
class AiSettingsController extends BaseController
{
const SETTING_DEFAULT = [
'audioSetting' => 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());
}
}
}

View File

@@ -0,0 +1,754 @@
<?php
namespace app\chukebao\controller;
use app\chukebao\model\AutoGreetings;
use library\ResponseHelper;
use think\Db;
class AutoGreetingsController extends BaseController
{
/**
* 获取问候规则列表
* @return \think\response\Json
*/
public function getList(){
$page = $this->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());
}
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace app\chukebao\controller;
use think\Controller;
/**
* 基础控制器
*/
class BaseController extends Controller
{
/**
* 获取用户信息
*
* @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;
}
}

View File

@@ -0,0 +1,665 @@
<?php
namespace app\chukebao\controller;
use app\chukebao\model\Keywords;
use app\chukebao\model\Material;
use app\chukebao\model\SensitiveWord;
use think\Db;
use library\ResponseHelper;
class ContentController extends BaseController
{
//===================================================== 素材管理 =====================================================
public function getAllMaterial(){
$keyword = $this->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());
}
}
//=================================================== 关键词词管理 ====================================================
}

View File

@@ -0,0 +1,51 @@
<?php
namespace app\chukebao\controller;
use library\ResponseHelper;
use think\Db;
class CustomerServiceController extends BaseController
{
public function getList(){
$accountId = $this->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);
}
}

View File

@@ -0,0 +1,206 @@
<?php
namespace app\chukebao\controller;
use app\api\model\WechatChatroomModel;
use library\ResponseHelper;
use app\api\model\WechatFriendModel;
use app\api\model\WechatMessageModel;
use app\api\controller\MessageController;
class DataProcessing extends BaseController
{
public function index()
{
$userId = $this->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);
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace app\chukebao\controller;
use app\chukebao\model\FollowUp;
use library\ResponseHelper;
use think\Db;
class FollowUpController extends BaseController
{
public function getList(){
$page = $this->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());
}
}
}

View File

@@ -0,0 +1,141 @@
<?php
namespace app\chukebao\controller;
use app\common\util\JwtUtil;
use Exception;
use library\ResponseHelper;
use think\Db;
use think\Controller;
use think\facade\Cache;
/**
* 认证控制器
* 处理用户登录和身份验证
*/
class LoginController extends Controller
{
/**
* 用户登录
*
* @return \think\response\Json
*/
public function index($username = '', $password = '',$verifySessionId = '',$verifyCode = '')
{
$username = !empty($username) ? $username : $this->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());
}
}
}

View File

@@ -0,0 +1,476 @@
<?php
namespace app\chukebao\controller;
use app\api\model\WechatMessageModel;
use app\chukebao\model\FriendSettings;
use library\ResponseHelper;
use think\Db;
use think\facade\Env;
use app\common\service\AuthService;
class MessageController extends BaseController
{
protected $baseUrl;
protected $authorization;
public function __construct()
{
parent::__construct();
$this->baseUrl = Env::get('api.wechat_url');
$this->authorization = AuthService::getSystemAuthorization();
}
public function getList()
{
$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字段且不为00表示已发送成功
if (isset($item['sendStatus']) && $item['sendStatus'] != 0) {
// 需要请求新的数据
$messageRequest = [
'id' => $item['id'],
'wechatAccountId' => $wechatAccountId,
'wechatFriendId' => $wechatFriendId,
'wechatChatroomId' => $wechatChatroomId,
'from' => '',
'to' => '',
];
$newData = $this->fetchLatestMessageFromApi($messageRequest);
if (!empty($newData)){
$item['sendStatus'] = 0;
}
}
// 格式化时间
$item['wechatTime'] = !empty($item['wechatTime']) ? date('Y-m-d H:i:s', $item['wechatTime']) : '';
}
unset($item);
return ResponseHelper::success(['total' => $total, 'list' => $list]);
}
/**
* 从线上接口获取最新消息
* @param array $messageRequest 消息项包含wechatAccountId、wechatFriendId或wechatChatroomId、id等
* @return array|null 最新消息数据失败返回null
*/
private function fetchLatestMessageFromApi($messageRequest)
{
if (empty($this->baseUrl) || empty($this->authorization)) {
return null;
}
try {
// 设置请求头
$headerData = ['client:system'];
$header = setHeader($headerData, $this->authorization, 'json');
// 判断是好友消息还是群聊消息
if (!empty($messageRequest['wechatFriendId'])) {
// 好友消息接口
$params = [
'keyword' => '',
'msgType' => '',
'accountId' => '',
'count' => 20, // 获取多条消息以便找到对应的消息
'messageId' => isset($messageRequest['id']) ? $messageRequest['id'] : '',
'olderData' => true,
'wechatAccountId' => $messageRequest['wechatAccountId'],
'wechatFriendId' => $messageRequest['wechatFriendId'],
'from' => $messageRequest['from'],
'to' => $messageRequest['to'],
'searchFrom' => 'admin'
];
$result = requestCurl($this->baseUrl . 'api/FriendMessage/searchMessage', $params, 'GET', $header, 'json');
$response = handleApiResponse($result);
// 查找对应的消息
if (!empty($response) && is_array($response)) {
$data = $response[0];
if ($data['sendStatus'] == 0){
WechatMessageModel::where(['id' => $data['id']])->update(['sendStatus' => 0]);
return true;
}
}
return false;
} elseif (!empty($messageRequest['wechatChatroomId'])) {
// 群聊消息接口
$params = [
'keyword' => '',
'msgType' => '',
'accountId' => '',
'count' => 20, // 获取多条消息以便找到对应的消息
'messageId' => isset($messageRequest['id']) ? $messageRequest['id'] : '',
'olderData' => true,
'wechatId' => '',
'wechatAccountId' => $messageRequest['wechatAccountId'],
'wechatChatroomId' => $messageRequest['wechatChatroomId'],
'from' => $messageRequest['from'],
'to' => $messageRequest['to'],
'searchFrom' => 'admin'
];
$result = requestCurl($this->baseUrl . 'api/ChatroomMessage/searchMessage', $params, 'GET', $header, 'json');
$response = handleApiResponse($result);
// 查找对应的消息
if (!empty($response) && is_array($response)) {
$data = $response[0];
if ($data['sendStatus'] == 0){
WechatMessageModel::where(['id' => $data['id']])->update(['sendStatus' => 0]);
return true;
}
}
return false;
}
} catch (\Exception $e) {
// 记录错误日志,但不影响主流程
\think\facade\Log::error('获取线上最新消息失败:' . $e->getMessage());
}
return null;
}
/**
* 更新数据库中的消息
* @param array $latestMessage 线上获取的最新消息
* @param array $oldMessage 旧消息数据
*/
private function updateMessageInDatabase($latestMessage, $oldMessage)
{
try {
// 使用API模块的MessageController来保存消息
$apiMessageController = new \app\api\controller\MessageController();
// 判断是好友消息还是群聊消息
if (!empty($oldMessage['wechatFriendId'])) {
// 保存好友消息
$apiMessageController->saveMessage($latestMessage);
} elseif (!empty($oldMessage['wechatChatroomId'])) {
// 保存群聊消息
$apiMessageController->saveChatroomMessage($latestMessage);
}
} catch (\Exception $e) {
// 记录错误日志,但不影响主流程
\think\facade\Log::error('更新数据库消息失败:' . $e->getMessage());
}
}
}

View File

@@ -0,0 +1,504 @@
<?php
namespace app\chukebao\controller;
use app\chukebao\model\KfMoments;
use library\ResponseHelper;
use think\Db;
class MomentsController extends BaseController
{
/**
* 创建朋友圈
* @return \think\response\Json
*/
public function create()
{
$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); // 内容类型 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;
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace app\chukebao\controller;
use app\chukebao\model\FollowUp;
use app\chukebao\model\NoticeModel;
use app\chukebao\model\ToDo;
use library\ResponseHelper;
use think\Db;
class NoticeController extends BaseController
{
/**
* 列表
* @return \think\response\Json
* @throws \Exception
*/
public function getList()
{
$page = $this->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());
}
}
}

View File

@@ -0,0 +1,233 @@
<?php
namespace app\chukebao\controller;
use app\chukebao\model\Questions;
use library\ResponseHelper;
use think\Db;
class QuestionsController extends BaseController
{
/**
* 列表
* @return \think\response\Json
* @throws \Exception
*/
public function getList(){
$page = $this->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,'获取成功');
}
}

View File

@@ -0,0 +1,371 @@
<?php
namespace app\chukebao\controller;
use app\chukebao\model\Reply;
use app\chukebao\model\ReplyGroup;
use library\ResponseHelper;
class ReplyController extends BaseController
{
/**
* 列表
* @return \think\response\Json
* @throws \Exception
*/
public function getList()
{
$replyType = $this->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);
}
}
}
}

View File

@@ -0,0 +1,143 @@
<?php
namespace app\chukebao\controller;
use app\chukebao\model\ToDo;
use library\ResponseHelper;
use think\Db;
class ToDoController extends BaseController
{
public function getList(){
$page = $this->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());
}
}
}

View File

@@ -0,0 +1,192 @@
<?php
namespace app\chukebao\controller;
use app\chukebao\model\TokensCompany;
use app\chukebao\model\TokensRecord;
use library\ResponseHelper;
use think\Db;
class TokensRecordController extends BaseController
{
public function getList(){
$page = $this->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; // 重新抛出异常,让重试机制处理
}
}
}

View File

@@ -0,0 +1,269 @@
<?php
namespace app\chukebao\controller;
use app\ai\controller\DouBaoAI;
use app\chukebao\controller\TokensRecordController as tokensRecord;
use app\chukebao\model\TokensCompany;
use library\ResponseHelper;
use think\Db;
class WechatChatroomController extends BaseController
{
public function getList(){
$page = $this->request->param('page', 1);
$limit = $this->request->param('limit', 10);
$keyword = $this->request->param('keyword', '');
$groupIds = $this->request->param('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']);
}
}
}

View File

@@ -0,0 +1,302 @@
<?php
namespace app\chukebao\controller;
use app\chukebao\model\FriendSettings;
use library\ResponseHelper;
use think\Db;
class WechatFriendController extends BaseController
{
public function getList()
{
$page = $this->request->param('page', 1);
$limit = $this->request->param('limit', 10);
$keyword = $this->request->param('keyword', '');
$groupIds = $this->request->param('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] : '未知状态';
}
}

View File

@@ -0,0 +1,273 @@
<?php
namespace app\chukebao\controller;
use library\ResponseHelper;
use think\Db;
use app\chukebao\model\ChatGroups;
class WechatGroupController extends BaseController
{
/**
* 获取分组列表
* @return \think\response\Json
* @throws \Exception
*/
public function getList()
{
// 公司维度分组,不强制校验 userId
$companyId = $this->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. 重置该分组下所有好友的分组IDs2_wechat_friend.groupIds -> 0
Db::table('s2_wechat_friend')
->where('groupIds', $id)
->update(['groupIds' => 0]);
// 3. 重置该分组下所有微信群的分组IDs2_wechat_chatroom.groupIds -> 0
Db::table('s2_wechat_chatroom')
->where('groupIds', $id)
->update(['groupIds' => 0]);
Db::commit();
return ResponseHelper::success('', '删除成功');
} catch (\Exception $e) {
Db::rollback();
return ResponseHelper::error('删除失败:' . $e->getMessage());
}
}
/**
* 移动分组(将好友或群移动到指定分组)
* @return \think\response\Json
* @throws \Exception
*/
public function move()
{
// type: friend 好友, chatroom 群
$type = $this->request->param('type', 'friend');
$targetId = (int)$this->request->param('groupId', 0);
// 仅支持单个ID移动
$idParam = $this->request->param('id', 0);
$companyId = $this->getUserInfo('companyId');
if (empty($companyId)) {
return ResponseHelper::error('请先登录');
}
if (empty($targetId)) {
return ResponseHelper::error('目标分组ID不能为空');
}
// 仅允许单个 ID禁止批量
$moveId = (int)$idParam;
if (empty($moveId)) {
return ResponseHelper::error('需要移动的ID不能为空');
}
// 校验目标分组是否存在且属于当前公司
$targetGroup = ChatGroups::where([
'id' => $targetId,
'companyId' => $companyId,
'isDel' => 0,
])->find();
if (empty($targetGroup)) {
return ResponseHelper::error('目标分组不存在或已删除');
}
// 校验分组类型与移动对象类型是否匹配
// groupType: 1=好友分组, 2=群分组
if ($type === 'friend' && (int)$targetGroup->groupType !== 1) {
return ResponseHelper::error('目标分组类型错误(需要好友分组)');
}
if ($type === 'chatroom' && (int)$targetGroup->groupType !== 2) {
return ResponseHelper::error('目标分组类型错误(需要群分组)');
}
Db::startTrans();
try {
if ($type === 'friend') {
// 移动单个好友到指定分组:更新 s2_wechat_friend.groupIds
Db::table('s2_wechat_friend')
->where('id', $moveId)
->update(['groupIds' => $targetId]);
} elseif ($type === 'chatroom') {
// 移动单个群到指定分组:更新 s2_wechat_chatroom.groupIds
Db::table('s2_wechat_chatroom')
->where('id', $moveId)
->update(['groupIds' => $targetId]);
} else {
Db::rollback();
return ResponseHelper::error('无效的类型参数');
}
Db::commit();
return ResponseHelper::success('', '移动成功');
} catch (\Exception $e) {
Db::rollback();
return ResponseHelper::error('移动失败:' . $e->getMessage());
}
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace app\chukebao\model;
use think\Model;
/**
* AI知识库模型
*/
class AiKnowledgeBase extends Model
{
// 设置表名
protected $name = 'ai_knowledge_base';
// 设置主键
protected $pk = 'id';
// 设置JSON字段
protected $json = ['label'];
// 设置JSON字段自动转换为数组
protected $jsonAssoc = true;
/**
* 关联知识库类型
*/
public function type()
{
return $this->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();
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace app\chukebao\model;
use think\Model;
/**
* AI知识库类型模型
*/
class AiKnowledgeBaseType extends Model
{
// 设置表名
protected $name = 'ai_knowledge_base_type';
// 设置主键
protected $pk = 'id';
// 设置JSON字段
protected $json = ['label'];
// 设置JSON字段自动转换为数组
protected $jsonAssoc = true;
// 类型常量
const TYPE_SYSTEM = 0; // 系统类型
const TYPE_USER = 1; // 用户创建类型
/**
* 获取有效的类型列表(未删除)
*/
public static function getValidList($companyId, $includeSystem = true)
{
$where = [
['isDel', '=', 0]
];
if ($includeSystem) {
$where[] = ['type|companyId', 'in', [0, $companyId]];
} else {
$where[] = ['companyId', '=', $companyId];
$where[] = ['type', '=', self::TYPE_USER];
}
return self::where($where)
->order('createTime', 'desc')
->select();
}
/**
* 检查是否为系统类型
*/
public function isSystemType()
{
return $this->type == self::TYPE_SYSTEM;
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace app\chukebao\model;
use think\Model;
class AiPush extends Model
{
protected $pk = 'id';
protected $name = 'kf_ai_push';
// 自动写入时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'createTime';
protected $updateTime = 'updateTime';
}

View File

@@ -0,0 +1,16 @@
<?php
namespace app\chukebao\model;
use think\Model;
class AiPushRecord extends Model
{
protected $pk = 'id';
protected $name = 'kf_ai_push_record';
// 自动写入时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'createTime';
}

View File

@@ -0,0 +1,17 @@
<?php
namespace app\chukebao\model;
use think\Model;
class AiSettings extends Model
{
protected $pk = 'id';
protected $name = 'ai_settings';
// 自动写入时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'createTime';
protected $updateTime = 'updateTime';
}

View File

@@ -0,0 +1,17 @@
<?php
namespace app\chukebao\model;
use think\Model;
class AutoGreetings extends Model
{
protected $pk = 'id';
protected $name = 'kf_auto_greetings';
// 自动写入时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'createTime';
protected $updateTime = 'updateTime';
}

View File

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

View File

@@ -0,0 +1,17 @@
<?php
namespace app\chukebao\model;
use think\Model;
class FollowUp extends Model
{
protected $pk = 'id';
protected $name = 'kf_follow_up';
// 自动写入时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'createTime';
protected $updateTime = 'updateTime';
}

View File

@@ -0,0 +1,17 @@
<?php
namespace app\chukebao\model;
use think\Model;
class FriendSettings extends Model
{
protected $pk = 'id';
protected $name = 'kf_friend_settings';
// 自动写入时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'createTime';
protected $updateTime = 'updateTime';
}

View File

@@ -0,0 +1,17 @@
<?php
namespace app\chukebao\model;
use think\Model;
class Keywords extends Model
{
protected $pk = 'id';
protected $name = 'kf_keywords';
// 自动写入时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'createTime';
protected $updateTime = 'updateTime';
}

View File

@@ -0,0 +1,11 @@
<?php
namespace app\chukebao\model;
use think\Model;
class KfMoments extends Model
{
protected $table = 'ck_kf_moments';
}

View File

@@ -0,0 +1,17 @@
<?php
namespace app\chukebao\model;
use think\Model;
class Material extends Model
{
protected $pk = 'id';
protected $name = 'kf_material';
// 自动写入时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'createTime';
protected $updateTime = 'updateTime';
}

View File

@@ -0,0 +1,16 @@
<?php
namespace app\chukebao\model;
use think\Model;
class NoticeModel extends Model
{
protected $pk = 'id';
protected $name = 'kf_notice';
// 自动写入时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'createTime';
protected $updateTime = 'updateTime';
}

View File

@@ -0,0 +1,17 @@
<?php
namespace app\chukebao\model;
use think\Model;
class Questions extends Model
{
protected $pk = 'id';
protected $name = 'kf_questions';
// 自动写入时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'createTime';
protected $updateTime = 'updateTime';
}

View File

@@ -0,0 +1,17 @@
<?php
namespace app\chukebao\model;
use think\Model;
class Reply extends Model
{
protected $pk = 'id';
protected $name = 'kf_reply';
// 自动写入时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'createTime';
protected $updateTime = 'lastUpdateTime';
}

View File

@@ -0,0 +1,10 @@
<?php
namespace app\chukebao\model;
use think\Model;
class ReplyGroup extends Model
{
protected $pk = 'id';
protected $name = 'kf_reply_group';
}

View File

@@ -0,0 +1,17 @@
<?php
namespace app\chukebao\model;
use think\Model;
class SensitiveWord extends Model
{
protected $pk = 'id';
protected $name = 'kf_sensitive_word';
// 自动写入时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'createTime';
protected $updateTime = 'updateTime';
}

View File

@@ -0,0 +1,17 @@
<?php
namespace app\chukebao\model;
use think\Model;
class ToDo extends Model
{
protected $pk = 'id';
protected $name = 'kf_to_do';
// 自动写入时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'createTime';
protected $updateTime = 'updateTime';
}

View File

@@ -0,0 +1,16 @@
<?php
namespace app\chukebao\model;
use think\Model;
class TokensCompany extends Model
{
protected $pk = 'id';
protected $name = 'tokens_company';
// 自动写入时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'createTime';
}

View File

@@ -0,0 +1,16 @@
<?php
namespace app\chukebao\model;
use think\Model;
class TokensRecord extends Model
{
protected $pk = 'id';
protected $name = 'tokens_record';
// 自动写入时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'createTime';
}

49
application/command.php Normal file
View File

@@ -0,0 +1,49 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2016 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: yunwuxin <448901948@qq.com>
// +----------------------------------------------------------------------
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', // 统一任务调度器,支持多进程并发执行
];

View File

@@ -0,0 +1,57 @@
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\facade\Log;
use think\Queue;
use app\job\AccountListJob;
class AccountListCommand extends Command
{
protected function configure()
{
$this->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');
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\console\input\Option;
use think\facade\Log;
use think\Queue;
use app\job\AllotChatroomJob;
use think\facade\Cache;
class AllotChatroomCommand extends Command
{
// 队列名称
protected $queueName = 'allot_chatroom';
protected function configure()
{
$this->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);
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\console\input\Option;
use think\facade\Log;
use think\Queue;
use app\job\AllotFriendJob;
use think\facade\Cache;
class AllotFriendCommand extends Command
{
// 队列名称
protected $queueName = 'allot_friends';
protected function configure()
{
$this->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);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\facade\Log;
use think\Queue;
use app\job\AllotRuleListJob;
class AllotRuleListCommand extends Command
{
protected function configure()
{
$this->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');
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\facade\Log;
use think\Queue;
use app\job\AutoCreateAllotRulesJob;
class AutoCreateAllotRulesCommand extends Command
{
protected function configure()
{
$this->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');
}
}

View File

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

View File

@@ -0,0 +1,57 @@
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\facade\Log;
use think\Queue;
use app\job\CallRecordingListJob;
class CallRecordingListCommand extends Command
{
protected function configure()
{
$this->setName('call-recording: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
];
// 添加到队列,设置任务名为 call_recording_list
Queue::push(CallRecordingListJob::class, $data, 'call_recording_list');
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\console\input\Option;
use think\Db;
class CleanExpiredGroupMessages extends Command
{
protected function configure()
{
$this->setName('clean:expired_group_messages')
->setDescription('Clean expired group messages from the database')
->addOption('days', 'd', Option::VALUE_OPTIONAL, 'Number of days to keep messages (default: 90)', 90)
->addOption('dry-run', null, Option::VALUE_NONE, 'Perform a dry run without deleting any data')
->addOption('batch-size', 'b', Option::VALUE_OPTIONAL, 'Batch size for deletion (default: 1000)', 1000);
}
protected function execute(Input $input, Output $output)
{
$days = (int)$input->getOption('days');
$dryRun = $input->getOption('dry-run');
$batchSize = (int)$input->getOption('batch-size');
if ($dryRun) {
$output->writeln("<info>Running in dry-run mode. No data will be deleted.</info>");
}
$cutoffDate = date('Y-m-d H:i:s', strtotime("-{$days} days"));
$output->writeln("<info>Cleaning group messages older than {$cutoffDate} (keeping last {$days} days)</info>");
// 清理微信群组消息
$this->cleanWechatGroupMessages($cutoffDate, $dryRun, $batchSize, $output);
$output->writeln("<info>Group message cleanup completed successfully.</info>");
}
protected function cleanWechatGroupMessages($cutoffDate, $dryRun, $batchSize, Output $output)
{
$output->writeln("\nCleaning s2_wechat_group_message table...");
// 获取符合条件的消息总数
$totalCount = Db::table('s2_wechat_group_message')
->where('createTime', '<', $cutoffDate)
->count();
if ($totalCount === 0) {
$output->writeln(" <comment>No expired group messages found.</comment>");
return;
}
$output->writeln(" Found {$totalCount} group messages to clean up.");
if ($dryRun) {
$output->writeln(" <comment>Dry run mode: would delete {$totalCount} group messages.</comment>");
return;
}
// 计算需要执行的批次数
$batches = ceil($totalCount / $batchSize);
$deletedCount = 0;
$output->writeln(" Deleting in {$batches} batches of {$batchSize} records...");
// 分批删除数据
for ($i = 0; $i < $batches; $i++) {
// 获取一批要删除的ID
$ids = Db::table('s2_wechat_group_message')
->where('createTime', '<', $cutoffDate)
->limit($batchSize)
->column('id');
if (empty($ids)) {
break;
}
// 删除这批数据
$count = Db::table('s2_wechat_group_message')
->whereIn('id', $ids)
->delete();
$deletedCount += $count;
$progress = round(($deletedCount / $totalCount) * 100, 2);
$output->write(" Progress: {$progress}% ({$deletedCount}/{$totalCount})\r");
// 短暂暂停,减轻数据库负担
usleep(500000); // 暂停0.5秒
}
$output->writeln("");
$output->writeln(" <info>Successfully deleted {$deletedCount} expired group messages.</info>");
// 优化表
$output->writeln(" Optimizing table...");
Db::execute("OPTIMIZE TABLE s2_wechat_group_message");
$output->writeln(" <info>Table optimization completed.</info>");
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\console\input\Option;
use think\Db;
class CleanExpiredMessages extends Command
{
protected function configure()
{
$this->setName('clean:expired_messages')
->setDescription('Clean expired messages from the database')
->addOption('days', 'd', Option::VALUE_OPTIONAL, 'Number of days to keep messages (default: 90)', 90)
->addOption('dry-run', null, Option::VALUE_NONE, 'Perform a dry run without deleting any data')
->addOption('batch-size', 'b', Option::VALUE_OPTIONAL, 'Batch size for deletion (default: 1000)', 1000);
}
protected function execute(Input $input, Output $output)
{
$days = (int)$input->getOption('days');
$dryRun = $input->getOption('dry-run');
$batchSize = (int)$input->getOption('batch-size');
if ($dryRun) {
$output->writeln("<info>Running in dry-run mode. No data will be deleted.</info>");
}
$cutoffDate = date('Y-m-d H:i:s', strtotime("-{$days} days"));
$output->writeln("<info>Cleaning messages older than {$cutoffDate} (keeping last {$days} days)</info>");
// 清理微信消息
$this->cleanWechatMessages($cutoffDate, $dryRun, $batchSize, $output);
$output->writeln("<info>Message cleanup completed successfully.</info>");
}
protected function cleanWechatMessages($cutoffDate, $dryRun, $batchSize, Output $output)
{
$output->writeln("\nCleaning s2_wechat_message table...");
// 获取符合条件的消息总数
$totalCount = Db::table('s2_wechat_message')
->where('createTime', '<', $cutoffDate)
->count();
if ($totalCount === 0) {
$output->writeln(" <comment>No expired messages found.</comment>");
return;
}
$output->writeln(" Found {$totalCount} messages to clean up.");
if ($dryRun) {
$output->writeln(" <comment>Dry run mode: would delete {$totalCount} messages.</comment>");
return;
}
// 计算需要执行的批次数
$batches = ceil($totalCount / $batchSize);
$deletedCount = 0;
$output->writeln(" Deleting in {$batches} batches of {$batchSize} records...");
// 分批删除数据
for ($i = 0; $i < $batches; $i++) {
// 获取一批要删除的ID
$ids = Db::table('s2_wechat_message')
->where('createTime', '<', $cutoffDate)
->limit($batchSize)
->column('id');
if (empty($ids)) {
break;
}
// 删除这批数据
$count = Db::table('s2_wechat_message')
->whereIn('id', $ids)
->delete();
$deletedCount += $count;
$progress = round(($deletedCount / $totalCount) * 100, 2);
$output->write(" Progress: {$progress}% ({$deletedCount}/{$totalCount})\r");
// 短暂暂停,减轻数据库负担
usleep(500000); // 暂停0.5秒
}
$output->writeln("");
$output->writeln(" <info>Successfully deleted {$deletedCount} expired messages.</info>");
// 优化表
$output->writeln(" Optimizing table...");
Db::execute("OPTIMIZE TABLE s2_wechat_message");
$output->writeln(" <info>Table optimization completed.</info>");
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\facade\Log;
use think\Queue;
use app\job\ContentCollectJob;
class ContentCollectCommand extends Command
{
protected function configure()
{
$this->setName('content:collect')
->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 = [
'libraryId' => 0, // 0表示采集所有内容库
'timestamp' => time()
];
// 添加到队列,设置任务名为 content_collect
Queue::push(ContentCollectJob::class, $data, 'content_collect');
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\facade\Log;
use think\Queue;
use app\job\DepartmentListJob;
class DepartmentListCommand extends Command
{
protected function configure()
{
$this->setName('department: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(DepartmentListJob::class, $data, 'department_list');
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\console\input\Option;
use think\facade\Log;
use think\Queue;
use app\job\DeviceListJob;
use think\facade\Cache;
class DeviceListCommand extends Command
{
// 队列名称
protected $queueName = 'device_list';
protected function configure()
{
$this->setName('device:list')
->setDescription('获取设备列表,并根据分页自动处理下一页')
->addOption('isDel', null, Option::VALUE_OPTIONAL, '删除状态: 0=未删除(unDeleted), 1=已删除(deleted), 2=已停用(deletedAndStop)', '')
->addOption('jobId', null, Option::VALUE_OPTIONAL, '任务ID用于区分不同实例', date('YmdHis') . rand(1000, 9999));
}
protected function execute(Input $input, Output $output)
{
$output->writeln('开始处理设备列表任务...');
try {
// 获取是否删除参数和任务ID
$isDel = $input->getOption('isDel');
$jobId = $input->getOption('jobId');
$output->writeln('删除状态参数: ' . ($isDel === '' ? '全部' : ($isDel == 0 ? '未删除' : ($isDel == 1 ? '已删除' : '已停用'))));
$output->writeln('任务ID: ' . $jobId);
// 检查队列是否已经在运行
$queueLockKey = "queue_lock:{$this->queueName}:{$isDel}";
if (Cache::get($queueLockKey)) {
$output->writeln("队列 {$this->queueName} 已经在运行中,删除状态:{$isDel},跳过执行");
Log::warning("队列 {$this->queueName} 已经在运行中,删除状态:{$isDel},跳过执行");
return false;
}
// 设置队列运行锁有效期1小时
Cache::set($queueLockKey, $jobId, 3600);
$output->writeln("已设置队列运行锁,键名:{$queueLockKey},值:{$jobId},有效期:1小时");
// 为不同的删除状态和任务ID使用不同的缓存键名
$cacheKeyPrefix = "devicePage:{$jobId}";
$cacheKeySuffix = $isDel === '' ? '' : ":{$isDel}";
$cacheKey = $cacheKeyPrefix . $cacheKeySuffix;
// 从缓存获取初始页码缓存有效期1天
$pageIndex = Cache::get($cacheKey, 0);
$output->writeln("从缓存获取页码: {$pageIndex}, 缓存键: {$cacheKey}");
$pageSize = 100; // 每页获取100条记录
// 将任务添加到队列
$this->addToQueue($pageIndex, $pageSize, $isDel, $jobId, $cacheKey, $queueLockKey);
$output->writeln('设备列表任务已添加到队列');
} catch (\Exception $e) {
Log::error('设备列表任务添加失败:' . $e->getMessage());
$output->writeln('设备列表任务添加失败:' . $e->getMessage());
return false;
}
return true;
}
/**
* 添加任务到队列
* @param int $pageIndex 页码
* @param int $pageSize 每页大小
* @param string $isDel 删除状态
* @param string $jobId 任务ID
* @param string $cacheKey 缓存键名
* @param string $queueLockKey 队列锁键名
*/
public function addToQueue($pageIndex, $pageSize, $isDel = '', $jobId = '', $cacheKey = '', $queueLockKey = '')
{
$data = [
'pageIndex' => $pageIndex,
'pageSize' => $pageSize,
'isDel' => $isDel,
'jobId' => $jobId,
'cacheKey' => $cacheKey,
'queueLockKey' => $queueLockKey
];
// 添加到队列,设置任务名为 device_list
Queue::push(DeviceListJob::class, $data, $this->queueName);
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\facade\Log;
use think\Queue;
use app\job\FriendTaskJob;
use think\facade\Cache;
class FriendTaskCommand extends Command
{
protected function configure()
{
$this->setName('friend:task')
->setDescription('获取添加好友认为列表,并根据分页自动处理下一页');
}
protected function execute(Input $input, Output $output)
{
$output->writeln('开始处理添加好友任务...');
try {
// 从缓存获取初始页码缓存10分钟有效
$pageIndex = Cache::get('friendTaskPage', 0);
$output->writeln('从缓存获取页码:' . $pageIndex);
$pageSize = 1000; // 每页获取1000条记录
// 将任务添加到队列
$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
];
// 添加到队列,设置任务名为 friend_task
Queue::push(FriendTaskJob::class, $data, 'friend_task');
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\facade\Log;
use think\Queue;
use app\job\GroupFriendsJob;
use think\facade\Cache;
class GroupFriendsCommand extends Command
{
protected function configure()
{
$this->setName('groupFriends:list')
->setDescription('获取微信群好友列表,并根据分页自动处理下一页');
}
protected function execute(Input $input, Output $output)
{
$output->writeln('开始处理微信群好友列表任务...');
try {
// 从缓存获取初始页码,缓存有效期一天
$pageIndex = Cache::get('groupFriendsPage', 0);
$output->writeln('从缓存获取页码:' . $pageIndex);
$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
];
// 添加到队列,设置任务名为 group_friends
Queue::push(GroupFriendsJob::class, $data, 'group_friends');
}
}

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