diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..39ab957 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +.idea +.env +.vscode +.DS_Store +runtime +public/upload +vendor +/public/.user.ini +/404.html +# SpecStory explanation file +.specstory/.what-is-this.md +/ +.cursorindexingignore +.user.ini +nginx.htaccess +.cursor/ +thinkphp/ +public/static/ diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..e69de29 diff --git a/README.en.md b/README.en.md deleted file mode 100644 index 54703ad..0000000 --- a/README.en.md +++ /dev/null @@ -1,36 +0,0 @@ -# 应用总接口 - -#### Description -{**When you're done, you can delete the content in this README and update the file with details for others getting started with your repository**} - -#### Software Architecture -Software architecture description - -#### Installation - -1. xxxx -2. xxxx -3. xxxx - -#### Instructions - -1. xxxx -2. xxxx -3. xxxx - -#### Contribution - -1. Fork the repository -2. Create Feat_xxx branch -3. Commit your code -4. Create Pull Request - - -#### Gitee Feature - -1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md -2. Gitee blog [blog.gitee.com](https://blog.gitee.com) -3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore) -4. The most valuable open source project [GVP](https://gitee.com/gvp) -5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help) -6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) diff --git a/README.md b/README.md deleted file mode 100644 index fc9a5a3..0000000 --- a/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# 应用总接口 - -#### 介绍 -{**以下是 Gitee 平台说明,您可以替换此简介** -Gitee 是 OSCHINA 推出的基于 Git 的代码托管平台(同时支持 SVN)。专为开发者提供稳定、高效、安全的云端软件开发协作平台 -无论是个人、团队、或是企业,都能够用 Gitee 实现代码托管、项目管理、协作开发。企业项目请看 [https://gitee.com/enterprises](https://gitee.com/enterprises)} - -#### 软件架构 -软件架构说明 - - -#### 安装教程 - -1. xxxx -2. xxxx -3. xxxx - -#### 使用说明 - -1. xxxx -2. xxxx -3. xxxx - -#### 参与贡献 - -1. Fork 本仓库 -2. 新建 Feat_xxx 分支 -3. 提交代码 -4. 新建 Pull Request - - -#### 特技 - -1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md -2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com) -3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 -4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 -5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) -6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) diff --git a/README_moments.md b/README_moments.md new file mode 100644 index 0000000..9e31075 --- /dev/null +++ b/README_moments.md @@ -0,0 +1,147 @@ +# 微信朋友圈数据处理功能 + +本模块提供了微信朋友圈数据的获取、存储和查询功能,支持保留驼峰命名结构的原始数据。 + +## 数据库表结构 + +项目包含一个数据表: + +**wechat_moments** - 存储朋友圈基本信息 +- `id`: 自增主键 +- `wechatAccountId`: 微信账号ID +- `wechatFriendId`: 微信好友ID +- `snsId`: 朋友圈消息ID +- `commentList`: 评论列表JSON +- `createTime`: 创建时间戳 +- `likeList`: 点赞列表JSON +- `content`: 朋友圈内容 +- `lat`: 纬度 +- `lng`: 经度 +- `location`: 位置信息 +- `picSize`: 图片大小 +- `resUrls`: 资源URL列表 +- `userName`: 用户名 +- `type`: 朋友圈类型 +- `create_time`: 数据创建时间 +- `update_time`: 数据更新时间 + +## API接口 + +### 1. 获取朋友圈信息 + +``` +GET/POST /api/websocket/getMoments +``` + +**参数:** +- `wechatAccountId`: 微信账号ID +- `wechatFriendId`: 微信好友ID +- `count`: 获取条数,默认5条 + +获取指定账号和好友的朋友圈信息,并自动保存到数据库。 + +### 2. 保存单条朋友圈数据 + +``` +POST /api/websocket/saveSingleMoment +``` + +**参数:** +- `commentList`: 评论列表 +- `createTime`: 创建时间戳 +- `likeList`: 点赞列表 +- `momentEntity`: 朋友圈实体,包含以下字段: + - `content`: 朋友圈内容 + - `lat`: 纬度 + - `lng`: 经度 + - `location`: 位置信息 + - `picSize`: 图片大小 + - `resUrls`: 资源URL列表 + - `urls`: 媒体URL列表 + - `userName`: 用户名 +- `snsId`: 朋友圈ID +- `type`: 朋友圈类型 +- `wechatAccountId`: 微信账号ID +- `wechatFriendId`: 微信好友ID + +保存单条朋友圈数据到数据库,保持原有的驼峰数据结构。系统会将`momentEntity`中的字段提取并单独存储,不包括`objectType`和`createTime`字段。 + +### 3. 获取朋友圈数据列表 + +``` +GET/POST /api/websocket/getMomentsList +``` + +**参数:** +- `wechatAccountId`: 微信账号ID (可选) +- `wechatFriendId`: 微信好友ID (可选) +- `page`: 页码,默认1 +- `pageSize`: 每页条数,默认10 +- `startTime`: 开始时间戳 (可选) +- `endTime`: 结束时间戳 (可选) + +获取已保存的朋友圈数据列表,支持分页和条件筛选。返回的数据会自动构建`momentEntity`字段以保持API兼容性。 + +### 4. 获取朋友圈详情 + +``` +GET/POST /api/websocket/getMomentDetail +``` + +**参数:** +- `snsId`: 朋友圈ID +- `wechatAccountId`: 微信账号ID + +获取单条朋友圈的详细信息,包括评论、点赞和资源URL等。返回的数据会自动构建`momentEntity`字段以保持API兼容性。 + +## 使用示例 + +### 保存单条朋友圈数据 + +```php +$data = [ + 'commentList' => [], + 'createTime' => 1742777232, + 'likeList' => [], + 'momentEntity' => [ + 'content' => "第一位个人与Stussy联名的中国名人,不是陈冠希,不是葛民辉,而是周杰伦!", + 'lat' => 0, + 'lng' => 0, + 'location' => "", + 'picSize' => 0, + 'resUrls' => [], + 'snsId' => "-3827269039168736643", + 'urls' => ["http://wxapp.tc.qq.com/251/20304/stodownload?encfilekey=..."], + 'userName' => "wxid_afixeeh53lt012" + ], + 'snsId' => "-3827269039168736643", + 'type' => 28, + 'wechatAccountId' => 123456, // 替换为实际的微信账号ID + 'wechatFriendId' => "wxid_example" // 替换为实际的微信好友ID +]; + +// 发送请求 +$result = curl_post('/api/websocket/saveSingleMoment', $data); +``` + +### 查询朋友圈列表 + +```php +// 获取特定账号的朋友圈 +$params = [ + 'wechatAccountId' => 123456, + 'page' => 1, + 'pageSize' => 20 +]; + +// 发送请求 +$result = curl_get('/api/websocket/getMomentsList', $params); +``` + +## 注意事项 + +1. 所有JSON格式的数据在保存时都会进行编码,查询时会自动解码并还原为原始数据结构。 +2. 数据库中的字段名保持驼峰命名格式,与微信API返回的数据结构保持一致。 +3. 尽管数据库中将`momentEntity`的字段拆分为独立字段存储,但API接口返回时会重新构建`momentEntity`结构,以保持与原始API的兼容性。 +4. `objectType`和`createTime`字段已从`momentEntity`中移除,不再单独存储。 +5. 图片或视频资源URLs直接存储在朋友圈主表中,不再单独存储到资源表。 \ No newline at end of file diff --git a/README_scheduler.md b/README_scheduler.md new file mode 100644 index 0000000..4c48568 --- /dev/null +++ b/README_scheduler.md @@ -0,0 +1,254 @@ +# 统一任务调度器使用说明 + +## 概述 + +统一任务调度器(TaskSchedulerCommand)是一个集中管理所有定时任务的调度系统,支持: +- ✅ 单条 crontab 配置管理所有任务 +- ✅ 多进程并发执行任务 +- ✅ 自动根据 cron 表达式判断任务执行时间 +- ✅ 任务锁机制,防止重复执行 +- ✅ 完善的日志记录 + +## 安装配置 + +### 1. 配置文件 + +任务配置位于 `config/task_scheduler.php`,格式如下: + +```php +'任务标识' => [ + 'command' => '命令名称', // 必填:执行的命令 + 'schedule' => 'cron表达式', // 必填:cron表达式 + 'options' => ['--option=value'], // 可选:命令参数 + 'enabled' => true, // 可选:是否启用 + 'max_concurrent' => 1, // 可选:最大并发数 + 'timeout' => 3600, // 可选:超时时间(秒) + 'log_file' => 'custom.log', // 可选:自定义日志文件 +] +``` + +### 2. Cron 表达式格式 + +标准 cron 格式:`分钟 小时 日 月 星期` + +示例: +- `*/1 * * * *` - 每分钟执行 +- `*/5 * * * *` - 每5分钟执行 +- `*/30 * * * *` - 每30分钟执行 +- `0 2 * * *` - 每天凌晨2点执行 +- `0 3 */3 * *` - 每3天的3点执行 + +### 3. Crontab 配置 + +**只需要在 crontab 中添加一条任务:** + +```bash +# 每分钟执行一次调度器(调度器内部会根据 cron 表达式判断哪些任务需要执行) +* * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think scheduler:run >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/scheduler.log 2>&1 +``` + +### 4. 系统要求 + +- PHP >= 5.6.0 +- 推荐启用 `pcntl` 扩展以支持多进程并发(非必需,未启用时使用单进程顺序执行) + +检查 pcntl 扩展: +```bash +php -m | grep pcntl +``` + +## 使用方法 + +### 手动执行调度器 + +```bash +# 执行调度器(会自动判断当前时间需要执行的任务) +php think scheduler:run +``` + +### 查看任务配置 + +```bash +# 查看所有已注册的命令 +php think list +``` + +### 启用/禁用任务 + +编辑 `config/task_scheduler.php`,设置 `'enabled' => false` 即可禁用任务。 + +## 功能特性 + +### 1. 多进程并发执行 + +- 默认最大并发数:10 个进程 +- 自动管理进程池 +- 自动清理僵尸进程 + +### 2. 任务锁机制 + +- 每个任务在执行时会设置锁(5分钟内不重复执行) +- 防止任务重复执行 +- 锁存储在缓存中,自动过期 + +### 3. 日志记录 + +- 调度器日志:`runtime/log/scheduler.log` +- 每个任务的日志:`runtime/log/{log_file}` +- 任务执行开始和结束都有标记 + +### 4. 超时控制 + +- 默认超时时间:3600 秒(1小时) +- 可在配置中为每个任务单独设置超时时间 +- 超时后自动终止任务 + +## 配置示例 + +### 高频任务(每分钟) + +```php +'wechat_friends_active' => [ + 'command' => 'wechatFriends:list', + 'schedule' => '*/1 * * * *', + 'options' => ['--isDel=0'], + 'enabled' => true, +], +``` + +### 中频任务(每5分钟) + +```php +'device_active' => [ + 'command' => 'device:list', + 'schedule' => '*/5 * * * *', + 'options' => ['--isDel=0'], + 'enabled' => true, +], +``` + +### 每日任务 + +```php +'wechat_calculate_score' => [ + 'command' => 'wechat:calculate-score', + 'schedule' => '0 2 * * *', // 每天凌晨2点 + 'options' => [], + 'enabled' => true, +], +``` + +### 定期任务(每3天) + +```php +'sync_all_friends' => [ + 'command' => 'sync:allFriends', + 'schedule' => '0 3 */3 * *', // 每3天的3点 + 'options' => [], + 'enabled' => true, +], +``` + +## 从旧配置迁移 + +### 旧配置(多条 crontab) + +```bash +*/5 * * * * cd /path && php think device:list --isDel=0 >> log1.log 2>&1 +*/1 * * * * cd /path && php think wechatFriends:list >> log2.log 2>&1 +``` + +### 新配置(单条 crontab + 配置文件) + +**Crontab:** +```bash +* * * * * cd /path && php think scheduler:run >> scheduler.log 2>&1 +``` + +**config/task_scheduler.php:** +```php +'device_active' => [ + 'command' => 'device:list', + 'schedule' => '*/5 * * * *', + 'options' => ['--isDel=0'], + 'log_file' => 'log1.log', +], +'wechat_friends' => [ + 'command' => 'wechatFriends:list', + 'schedule' => '*/1 * * * *', + 'log_file' => 'log2.log', +], +``` + +## 监控和调试 + +### 查看调度器日志 + +```bash +tail -f runtime/log/scheduler.log +``` + +### 查看任务执行日志 + +```bash +tail -f runtime/log/crontab_device_active.log +``` + +### 检查任务是否在执行 + +```bash +# 查看进程 +ps aux | grep "php think" +``` + +### 手动测试任务 + +```bash +# 直接执行某个任务 +php think device:list --isDel=0 +``` + +## 注意事项 + +1. **时间同步**:确保服务器时间准确,调度器依赖系统时间判断任务执行时间 +2. **资源限制**:根据服务器性能调整 `maxConcurrent` 参数 +3. **日志清理**:定期清理日志文件,避免占用过多磁盘空间 +4. **任务冲突**:如果任务执行时间较长,建议调整执行频率或增加并发数 +5. **缓存依赖**:任务锁使用缓存,确保缓存服务正常运行 + +## 故障排查 + +### 任务未执行 + +1. 检查任务是否启用:`'enabled' => true` +2. 检查 cron 表达式是否正确 +3. 检查调度器是否正常运行:查看 `scheduler.log` +4. 检查任务锁:任务可能在5分钟内重复执行被跳过 + +### 任务执行失败 + +1. 查看任务日志:`runtime/log/{log_file}` +2. 检查命令是否正确:手动执行命令测试 +3. 检查权限:确保有执行权限和日志写入权限 + +### 多进程不工作 + +1. 检查 pcntl 扩展:`php -m | grep pcntl` +2. 检查系统限制:`ulimit -u` 查看最大进程数 +3. 查看调度器日志中的错误信息 + +## 性能优化建议 + +1. **合理设置并发数**:根据服务器 CPU 核心数和内存大小调整 +2. **错开高频任务**:避免所有任务在同一分钟执行 +3. **优化任务执行时间**:减少任务执行时长 +4. **使用队列**:对于耗时任务,建议使用队列异步处理 + +## 更新日志 + +### v1.0.0 (2024-01-XX) +- 初始版本 +- 支持多进程并发执行 +- 支持 cron 表达式调度 +- 支持任务锁机制 + diff --git a/README_wechat_chatroom_sync.md b/README_wechat_chatroom_sync.md new file mode 100644 index 0000000..fb2e186 --- /dev/null +++ b/README_wechat_chatroom_sync.md @@ -0,0 +1,105 @@ +# 微信群聊同步功能 + +本功能用于自动同步微信群聊数据,支持分页获取群聊列表以及群成员信息,并将数据保存到数据库中。 + +## 功能特点 + +1. 支持分页获取微信群聊列表 +2. 自动获取每个群聊的成员信息 +3. 支持通过关键词筛选群聊 +4. 支持按微信账号筛选群聊 +5. 可选择是否包含已删除的群聊 +6. 使用队列处理,支持大量数据的同步 +7. 支持失败重试机制 +8. 提供命令行和HTTP接口两种触发方式 + +## 数据表结构 + +本功能使用以下数据表: + +1. **wechat_chatroom** - 存储微信群聊信息 +2. **wechat_chatroom_member** - 存储微信群聊成员信息 + +## 使用方法 + +### 1. HTTP接口触发 + +``` +GET/POST /api/wechat_chatroom/syncChatrooms +``` + +**参数:** +- `pageIndex`: 起始页码,默认0 +- `pageSize`: 每页大小,默认100 +- `keyword`: 群名关键词,可选 +- `wechatAccountKeyword`: 微信账号关键词,可选 +- `isDeleted`: 是否包含已删除群聊,可选 + +**示例:** +``` +/api/wechat_chatroom/syncChatrooms?pageSize=50 +``` + +### 2. 命令行触发 + +```bash +php think sync:wechat:chatrooms [选项] +``` + +**选项:** +- `-p, --pageIndex`: 起始页码,默认0 +- `-s, --pageSize`: 每页大小,默认100 +- `-k, --keyword`: 群名关键词,可选 +- `-a, --account`: 微信账号关键词,可选 +- `-d, --deleted`: 是否包含已删除群聊,可选 + +**示例:** +```bash +# 基本用法 +php think sync:wechat:chatrooms + +# 指定页大小和关键词 +php think sync:wechat:chatrooms -s 50 -k "测试群" + +# 指定账号关键词 +php think sync:wechat:chatrooms --account "张三" +``` + +### 3. 定时任务配置 + +可以将命令添加到系统的定时任务(crontab)中,实现定期自动同步: + +``` +# 每天凌晨3点执行微信群聊同步 +0 3 * * * cd /path/to/your/project && php think sync:wechat:chatrooms +``` + +## 队列消费者配置 + +为了处理同步任务,需要启动队列消费者: + +```bash +# 启动微信群聊队列消费者 +php think queue:work --queue wechat_chatrooms +``` + +建议在生产环境中使用supervisor等工具来管理队列消费者进程。 + +## 同步过程 + +1. 触发同步任务,将初始页任务加入队列 +2. 队列消费者处理任务,获取当前页的群聊列表 +3. 如果当前页有数据且数量等于页大小,则将下一页任务加入队列 +4. 对每个获取到的群聊,添加获取群成员的任务 +5. 所有数据会自动保存到数据库中 + +## 调试与日志 + +同步过程的日志会记录在应用的日志目录中,可以通过查看日志了解同步状态和错误信息。 + +## 注意事项 + +1. 页大小建议设置为合理值(50-100),过大会导致请求超时 +2. 当数据量较大时,建议增加队列消费者的数量 +3. 确保系统授权信息正确,否则无法获取数据 +4. 数据同步是增量的,会自动更新已存在的记录 \ No newline at end of file diff --git a/RFM客户价值评分体系技术实施文档.md b/RFM客户价值评分体系技术实施文档.md new file mode 100644 index 0000000..4b5fb84 --- /dev/null +++ b/RFM客户价值评分体系技术实施文档.md @@ -0,0 +1,249 @@ +# RFM 客户价值评分体系技术实施文档 + +## 1. 文档目的 + +本文档旨在明确 RFM(Recency-Frequency-Monetary)客户价值评分体系的技术实现标准,包括维度定义、评分规则、数据处理流程、参数配置及异常处理方案,为系统开发、数据分析及业务应用提供统一依据。 + +## 2. 核心术语定义 + + + +| 术语 | 英文缩写 | 定义 | 数据来源 | 统计周期说明 | +| ------ | ------------ | ------------------------------- | --------- | ---------------------------------- | +| 最近消费时间 | Recency(R) | 客户最后一次有效消费行为距统计截止日的时间间隔(单位:天) | 订单系统、交易日志 | 支持自定义配置(默认 3-12 个月,按业务场景调整) | +| 消费频率 | Frequency(F) | 统计周期内客户发生有效消费行为的总次数 | 订单系统、交易日志 | 与 R 维度统计周期一致,剔除重复下单、取消订单等无效记录 | +| 消费金额 | Monetary(M) | 统计周期内客户有效消费行为的总金额(单位:元,支持多币种换算) | 订单系统、支付日志 | 仅统计已支付完成的订单金额,剔除退款、优惠抵扣部分 | +| RFM 总分 | RFM Score | 基于 R、F、M 三个维度的分项得分,按预设权重计算的综合得分 | 系统计算生成 | 得分范围 1-15 分(5 分制单项)或 1-100 分(标准化后) | + +## 3. 评分规则技术规范 + +### 3.1 分项评分规则(默认 5 分制) + +#### 3.1.1 Recency(R)评分规则 + + + +* 核心逻辑:时间间隔越短,得分越高(反向映射) + +* 分段标准:采用**五分位法**(按数据分布自动划分区间,避免均分失真) + + + +| 得分 | 时间间隔区间(天) | 划分逻辑 | +| --- | ---------- | ----------------- | +| 5 分 | \[0, T1] | 统计周期内最近消费的 20% 客户 | +| 4 分 | (T1, T2] | 统计周期内次近消费的 20% 客户 | +| 3 分 | (T2, T3] | 统计周期内中间消费的 20% 客户 | +| 2 分 | (T3, T4] | 统计周期内较久消费的 20% 客户 | +| 1 分 | (T4, Tmax] | 统计周期内最久消费的 20% 客户 | + + + +* 区间计算方式:T1=PERCENTILE\_CONT (0.2)、T2=PERCENTILE\_CONT (0.4)、T3=PERCENTILE\_CONT (0.6)、T4=PERCENTILE\_CONT (0.8),其中 Tmax 为统计周期总天数 + +#### 3.1.2 Frequency(F)评分规则 + + + +* 核心逻辑:消费次数越多,得分越高(正向映射) + +* 分段标准:采用**五分位法**(支持最小消费次数阈值配置) + + + +| 得分 | 消费次数区间 | 划分逻辑 | +| --- | ----------- | ------------------- | +| 5 分 | \[F4, +∞) | 统计周期内消费次数最多的 20% 客户 | +| 4 分 | \[F3, F4) | 统计周期内消费次数次多的 20% 客户 | +| 3 分 | \[F2, F3) | 统计周期内消费次数中间的 20% 客户 | +| 2 分 | \[F1, F2) | 统计周期内消费次数较少的 20% 客户 | +| 1 分 | \[Fmin, F1) | 统计周期内消费次数最少的 20% 客户 | + + + +* 区间计算方式:F1=PERCENTILE\_CONT (0.2)、F2=PERCENTILE\_CONT (0.4)、F3=PERCENTILE\_CONT (0.6)、F4=PERCENTILE\_CONT (0.8),其中 Fmin 为 1(仅统计有效消费次数≥1 的客户) + +#### 3.1.3 Monetary(M)评分规则 + + + +* 核心逻辑:消费金额越高,得分越高(正向映射) + +* 分段标准:采用**五分位法**(支持剔除大额异常值后划分) + + + +| 得分 | 消费金额区间(元) | 划分逻辑 | +| --- | ----------- | ------------------- | +| 5 分 | \[M4, +∞) | 统计周期内消费金额最高的 20% 客户 | +| 4 分 | \[M3, M4) | 统计周期内消费金额次高的 20% 客户 | +| 3 分 | \[M2, M3) | 统计周期内消费金额中间的 20% 客户 | +| 2 分 | \[M1, M2) | 统计周期内消费金额较低的 20% 客户 | +| 1 分 | \[Mmin, M1) | 统计周期内消费金额最低的 20% 客户 | + + + +* 区间计算方式:M1=PERCENTILE\_CONT (0.2)、M2=PERCENTILE\_CONT (0.4)、M3=PERCENTILE\_CONT (0.6)、M4=PERCENTILE\_CONT (0.8),其中 Mmin 为统计周期内最小有效订单金额 + +### 3.2 总分计算规则 + +#### 3.2.1 加权求和公式 + +$RFM_{Score} = R_{Score} \times W_R + F_{Score} \times W_F + M_{Score} \times W_M$ + + + +* 权重配置:支持自定义(默认配置:$W_R=0.4$,$W_F=0.3$,$W_M=0.3$) + +* 权重约束:$W_R + W_F + W_M = 1.0$,且单个权重取值范围为 \[0.1, 0.8] + +#### 3.2.2 得分标准化(可选) + + + +* 若需将总分映射为 1-100 分,采用线性标准化公式: + + $RFM_{StandardScore} = \frac{RFM_{Score} - RFM_{Min}}{RFM_{Max} - RFM_{Min}} \times 99 + 1$ + +* 其中:$RFM_{Min}=W_R \times 1 + W_F \times 1 + W_M \times 1$,$RFM_{Max}=W_R \times 5 + W_F \times 5 + W_M \times 5$ + +## 4. 数据处理流程 + +### 4.1 数据输入要求 + + + +| 数据项 | 数据类型 | 格式要求 | 校验规则 | +| ------ | ------------- | ------------------- | ------------ | +| 客户唯一标识 | String/Int | 全局唯一(如用户 ID、会员 ID) | 非空、去重 | +| 订单唯一标识 | String/Int | 全局唯一(如订单号) | 非空、去重 | +| 消费时间 | DateTime | yyyy-MM-dd HH:mm:ss | 需在统计周期内 | +| 消费金额 | Decimal(18,2) | 大于 0 | 剔除负数、0 值 | +| 订单状态 | String | 枚举值(已支付、已取消、已退款等) | 仅保留 “已支付” 状态 | + +### 4.2 数据预处理步骤 + + + +1. **数据过滤**: + +* 剔除统计周期外的订单数据 + +* 剔除订单状态为 “已取消”“已退款”“无效” 的记录 + +* 剔除员工内部订单、测试订单(按订单标签或用户标签过滤) + +* 剔除单笔金额超过$M_{99分位值} \times 3$的异常大额订单(可配置开关) + +1. **数据聚合**: + +* 按客户唯一标识分组,计算 R、F、M 原始指标: + + + * R:MAX (消费时间) 到统计截止日的时间间隔(天) + + * F:COUNT (DISTINCT 订单唯一标识) + + * M:SUM (消费金额) + +1. **缺失值处理**: + +* 统计周期内无消费记录的客户:R = 统计周期总天数,F=0,M=0,分项得分均为 1 分 + +* 单个指标缺失(如仅缺失 M):按 1 分计分项得分 + +### 4.3 评分计算流程 + + + +```mermaid +graph TD + A[数据输入] --> B[数据过滤] + B --> C[数据聚合计算R/F/M原始值] + C --> D[缺失值处理] + D --> E[按五分位法划分各维度区间] + E --> F[计算R/F/M分项得分] + F --> G[按权重计算RFM总分] + G --> H[可选:标准化为1-100分] + H --> I[输出客户RFM评分结果] +``` + +## 5. 参数配置说明 + + + +| 参数名称 | 配置项 | 取值范围 | 默认值 | 配置方式 | +| ------- | ---------------------- | ------------- | ------ | --------------- | +| 统计周期 | cycle\_days | 30-365 | 180 | 系统配置页手动输入 | +| R 维度权重 | weight\_R | 0.1-0.8 | 0.4 | 系统配置页滑动条调整 | +| F 维度权重 | weight\_F | 0.1-0.8 | 0.3 | 系统配置页滑动条调整 | +| M 维度权重 | weight\_M | 0.1-0.8 | 0.3 | 系统配置页滑动条调整 | +| 异常金额阈值 | abnormal\_money\_ratio | 1.5-5.0 | 3.0 | 系统配置页手动输入(倍数关系) | +| 评分分制 | score\_scale | 5 分制 / 100 分制 | 5 分制 | 系统配置页单选 | +| 缺失值处理策略 | missing\_strategy | 按 1 分计 / 剔除客户 | 按 1 分计 | 系统配置页单选 | + +## 6. 异常处理方案 + +### 6.1 数据异常 + + + +| 异常类型 | 表现形式 | 处理逻辑 | 影响范围 | +| ------ | ------------------------------ | ---------------------- | --------------- | +| 重复订单 | 同一客户同一时间相同订单号 | 去重保留 1 条有效记录 | 不影响 F、M 计算 | +| 大额异常订单 | 单笔金额 > $M_{99分位值} \times 异常阈值$ | 自动标记,可选择剔除或保留 | 仅影响 M 维度区间划分 | +| 消费时间异常 | 消费时间晚于统计截止日 | 视为无效数据,剔除 | 不影响最终结果 | +| 客户标识重复 | 同一客户多个唯一标识 | 按客户合并规则(如手机号、身份证号关联)聚合 | 需提前完成客户统一 ID 映射 | + +### 6.2 计算异常 + + + +| 异常类型 | 触发条件 | 处理逻辑 | 输出结果 | +| -------- | --------------------- | --------------------------------------- | --------------- | +| 维度区间为空 | 某维度所有客户数据相同(如 F 均为 1) | 强制均分 5 个区间 | 分项得分按 1-5 分依次分配 | +| 权重总和不为 1 | 配置权重时计算错误 | 系统自动归一化处理($W'_X = W_X / (W_R+W_F+W_M)$) | 不影响总分有效性 | +| 统计周期过短 | 小于 30 天导致数据量不足 | 系统给出警告,允许强制执行 | 区间划分可能失真,建议延长周期 | + +## 7. 输出结果格式 + +### 7.1 单客户评分结果 + + + +| 字段名 | 数据类型 | 示例 | +| --------- | ------------- | ----------------------- | +| 客户 ID | String | CUST2023001 | +| R 原始值(天) | Int | 15 | +| R 得分 | Int | 5 | +| F 原始值(次) | Int | 8 | +| F 得分 | Int | 4 | +| M 原始值(元) | Decimal(18,2) | 2560.00 | +| M 得分 | Int | 5 | +| RFM 总分 | Decimal(5,2) | 4.70 | +| 标准化得分(可选) | Int | 94 | +| 统计周期 | String | 2023-01-01 至 2023-06-30 | +| 计算时间 | DateTime | 2023-07-01 00:30:25 | + +### 7.2 批量输出文件格式 + + + +* 支持 CSV、Parquet、JSON 格式导出 + +* 编码格式:UTF-8 + +* 压缩方式:默认 GZIP(可配置关闭) + +## 8. 业务适配建议 + + + +| 业务场景 | 统计周期建议 | 权重调整建议 | 特殊配置 | +| --------------- | ------- | --------------------------- | ----------------- | +| 快消零售 | 3-6 个月 | $W_R=0.5, W_F=0.3, W_M=0.2$ | 提高 R 维度权重,关注复购及时性 | +| 高客单价行业(如奢侈品、家居) | 12 个月 | $W_R=0.3, W_F=0.2, W_M=0.5$ | 提高 M 维度权重,关注消费能力 | +| 新品推广期 | 1-3 个月 | $W_R=0.6, W_F=0.2, W_M=0.2$ | 重点关注近期新客户 | +| 会员体系运营 | 6-12 个月 | $W_R=0.4, W_F=0.4, W_M=0.2$ | 提高 F 维度权重,鼓励高频消费 | + +> (注:文档部分内容可能由 AI 生成) \ No newline at end of file diff --git a/application/.htaccess b/application/.htaccess new file mode 100644 index 0000000..3418e55 --- /dev/null +++ b/application/.htaccess @@ -0,0 +1 @@ +deny from all \ No newline at end of file diff --git a/application/ai/config/route.php b/application/ai/config/route.php new file mode 100644 index 0000000..704b7d0 --- /dev/null +++ b/application/ai/config/route.php @@ -0,0 +1,18 @@ +middleware(['jwt']); \ No newline at end of file diff --git a/application/ai/controller/CozeAI.php b/application/ai/controller/CozeAI.php new file mode 100644 index 0000000..5208326 --- /dev/null +++ b/application/ai/controller/CozeAI.php @@ -0,0 +1,406 @@ +apiUrl = Env::get('cozeAi.api_url'); + $this->accessToken = Env::get('cozeAi.token'); + + if (empty($this->accessToken) || empty($this->apiUrl)) { + return json_encode(['code' => 500, 'msg' => '参数缺失']); + } + + // 设置请求头 + $this->headers = [ + 'Authorization: Bearer ' . $this->accessToken, + 'Content-Type: application/json' + ]; + } + + + /** + * 创建智能体 + * @param $data + * @return false|string|\think\response\Json + */ + public function createBot($data = []) + { + $space_id = Env::get('cozeAi.space_id'); + $name = !empty($data['name']) ? $data['name'] : ''; + $model_id = !empty($data['model_id']) ? $data['model_id'] : ''; + $prompt_info = !empty($data['prompt_info']) ? $data['prompt_info'] : ''; + $plugin_id_list = [ + 'id_list' => [ + ['api_id' => '7362852017859035163', 'plugin_id' => '7362852017859018779'], + ['api_id' => '7472045461050851367', 'plugin_id' => '7472045461050834983'], + ] + ]; + if (empty($name)) { + return json_encode(['code' => 500, 'msg' => '参数缺失']); + } + + $model_info_config = [ + 'model_id' => (string)$model_id, + ]; + + $params = [ + 'space_id' => $space_id, + 'name' => $name, + 'model_info_config' => (object)$model_info_config, + 'plugin_id_list' => (object)$plugin_id_list + ]; + + if (!empty($prompt_info)){ + $new_prompt_info = [ + 'prompt' => $prompt_info + ]; + $params['prompt_info'] = (object) $new_prompt_info; + } + + $result = requestCurl($this->apiUrl . '/v1/bot/create', $params, 'POST', $this->headers, 'json'); + $result = json_decode($result, true); + if ($result['code'] != 0) { + return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]); + } + + return json_encode(['code' => 200, 'msg' => '创建成功', 'data' => $result['data']]); + } + + + /** + * 创建智能体 + * @param $data + * @return false|string|\think\response\Json + */ + public function updateBot($data = []) + { + $space_id = Env::get('cozeAi.space_id'); + $bot_id = !empty($data['bot_id']) ? $data['bot_id'] : ''; + $name = !empty($data['name']) ? $data['name'] : ''; + $model_id = !empty($data['model_id']) ? $data['model_id'] : ''; + $prompt_info = !empty($data['prompt_info']) ? $data['prompt_info'] : ''; + $dataset_ids = !empty($data['dataset_ids']) ? $data['dataset_ids'] : ''; + $plugin_id_list = [ + 'id_list' => [ + ['api_id' => '7362852017859035163', 'plugin_id' => '7362852017859018779'], + ['api_id' => '7472045461050851367', 'plugin_id' => '7472045461050834983'], + ] + ]; + if (empty($name) || empty($bot_id)) { + return json_encode(['code' => 500, 'msg' => '参数缺失']); + } + + $model_info_config = [ + 'model_id' => (string)$model_id, + ]; + + $params = [ + 'bot_id' => $bot_id, + 'space_id' => $space_id, + 'name' => $name, + 'model_info_config' => (object)$model_info_config, + 'plugin_id_list' => (object)$plugin_id_list + ]; + + + if (!empty($prompt_info)){ + $new_prompt_info = [ + 'prompt' => $prompt_info + ]; + $params['prompt_info'] = (object) $new_prompt_info; + } + + if (!empty($dataset_ids)){ + $knowledge = [ + 'dataset_ids' => $dataset_ids + ]; + $params['knowledge'] = (object) $knowledge; + } + + $result = requestCurl($this->apiUrl . '/v1/bot/update', $params, 'POST', $this->headers, 'json'); + $result = json_decode($result, true); + if ($result['code'] != 0) { + return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]); + } + return json_encode(['code' => 200, 'msg' => '更新成功', 'data' => []]); + } + + + /** + * 发布智能体 + * @param $data + * @return false|string|\think\response\Json + */ + public function botPublish($data = []) + { + $bot_id = !empty($data['bot_id']) ? $data['bot_id'] : ''; + $connector_ids = ['1024', '999']; + if (empty($bot_id) || empty($connector_ids)) { + return json_encode(['code' => 500, 'msg' => '参数缺失']); + } + + $params = [ + 'bot_id' => $bot_id, + 'connector_ids' => $connector_ids, + ]; + $result = requestCurl($this->apiUrl . '/v1/bot/publish', $params, 'POST', $this->headers, 'json'); + $result = json_decode($result, true); + if ($result['code'] != 0) { + return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]); + } + return json_encode(['code' => 200, 'msg' => '发布成功', 'data' => []]); + } + + + /** + * 创建知识库 + * @param $data + * @return false|string|\think\response\Json + */ + public function createKnowledge($data = []) + { + + $space_id = Env::get('cozeAi.space_id'); + $name = !empty($data['name']) ? $data['name'] : ''; + if (empty($space_id) || empty($name)) { + return json_encode(['code' => 500, 'msg' => '参数缺失']); + } + + $params = [ + 'space_id' => $space_id, + 'format_type' => 0, + 'name' => $name, + ]; + $result = requestCurl($this->apiUrl . '/v1/datasets', $params, 'POST', $this->headers, 'json'); + $result = json_decode($result, true); + + if ($result['code'] != 0) { + return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]); + } + return json_encode(['code' => 200, 'msg' => '创建成功','data' => $result['data']]); + } + + + public function createDocument($data = []) + { + // 文件路径 + $filePath = !empty($data['filePath']) ? $data['filePath'] : ''; + $fileName = !empty($data['fileName']) ? $data['fileName'] : ''; + if (empty($filePath)) { + return json_encode(['code' => 500, 'msg' => '参数缺失']); + } + // 读取文件内容 + $fileContent = file_get_contents($filePath); + // 将文件内容编码为Base64 + $base64EncodedContent = base64_encode($fileContent); + + + $dataset_id = !empty($data['dataset_id']) ? $data['dataset_id'] : ''; + + $document_bases = [ + ['name' => $fileName,'source_info' => ['file_base64' => $base64EncodedContent]] + ]; + + $chunk_strategy = [ + 'chunk_type' => 0, + 'remove_extra_spaces' => true + ]; + $params = [ + 'dataset_id' => (string) $dataset_id, + 'document_bases' => $document_bases, + 'chunk_strategy' => (object) $chunk_strategy, + 'format_type' => 0 + ]; + $headers = array_merge($this->headers, ['Agw-Js-Conv: str']); + $result = requestCurl($this->apiUrl . '/open_api/knowledge/document/create', $params, 'POST', $headers, 'json'); + $result = json_decode($result, true); + if ($result['code'] != 0) { + return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]); + } + return json_encode(['code' => 200, 'msg' => '创建成功','data' => $result['document_infos']]); + } + + + /** + * 删除知识库文件 + * @param $data + * @return false|string|\think\response\Json + */ + public function deleteDocument($data = []) + { + if (empty($data)) { + return json_encode(['code' => 500, 'msg' => '参数缺失']); + } + $params = [ + 'document_ids' => $data, + ]; + $headers = array_merge($this->headers, ['Agw-Js-Conv: str']); + $result = requestCurl($this->apiUrl . '/open_api/knowledge/document/delete', $params, 'POST', $headers, 'json'); + $result = json_decode($result, true); + if ($result['code'] != 0) { + return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]); + } + return json_encode(['code' => 200, 'msg' => '删除成功', 'data' => []]); + + } + + + /** + * 创建会话 + * @param $data + * @return false|string|\think\response\Json + */ + public function createConversation($data = []) + { + $bot_id = !empty($data['bot_id']) ? $data['bot_id'] : ''; + $name = !empty($data['name']) ? $data['name'] : ''; + $meta_data = !empty($data['meta_data']) ? $data['meta_data'] : []; + + if (empty($bot_id) || empty($name)) { + return json_encode(['code' => 500, 'msg' => '参数缺失']); + } + $params = [ + 'bot_id' => $bot_id, + 'name' => $name, + ]; + if (!empty($meta_data)){ + $params['meta_data'] = $meta_data; + } + $result = requestCurl($this->apiUrl . '/v1/conversation/create', $params, 'POST', $this->headers, 'json'); + $result = json_decode($result, true); + if ($result['code'] != 0) { + return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]); + } + return json_encode(['code' => 200, 'msg' => '创建成功','data' => $result['data']]); + } + + + /** + * 开始对话 + * @param $data + * @return false|string|\think\response\Json + */ + public function createChat($data = []) + { + try { + $bot_id = !empty($data['bot_id']) ? $data['bot_id'] : ''; + $uid = !empty($data['uid']) ? $data['uid'] : ''; + $conversation_id = !empty($data['conversation_id']) ? $data['conversation_id'] : ''; + $question = !empty($data['question']) ? $data['question'] : []; + + + if(empty($bot_id)){ + return json_encode(['code' => 500, 'msg' => '智能体ID不能为空', 'data' => []]); + } + + if(empty($conversation_id)){ + return json_encode(['code' => 500, 'msg' => '会话ID不能为空', 'data' => []]); + } + + if(empty($question)){ + return json_encode(['code' => 500, 'msg' => '问题不能为空', 'data' => []]); + } + + // 构建请求数据 + $params = [ + 'bot_id' => strval($bot_id), + 'user_id' => strval($uid), + 'additional_messages' => $question, + 'stream' => false, + 'auto_save_history' => true + ]; + + $url = $this->apiUrl . '/v3/chat?conversation_id='.$conversation_id; + $result = requestCurl($url, $params, 'POST', $this->headers, 'json'); + $result = json_decode($result, true); + if ($result['code'] != 0) { + return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]); + } + return json_encode(['code' => 200, 'msg' => '发送成功','data' => $result['data']]); + + } catch (\Exception $e) { + return json_encode(['code' => 500, 'msg' => '创建对话失败:' . $e->getMessage(), 'data' => []]); + } + } + + + /** + * 查看对话详情 + * @param $data + * @return false|string|\think\response\Json + */ + public function getConversationChat($data = []) + { + $conversation_id = !empty($data['conversation_id']) ? $data['conversation_id'] : ''; + $chat_id = !empty($data['chat_id']) ? $data['chat_id'] : ''; + $url = $this->apiUrl . '/v3/chat/retrieve?conversation_id='.$conversation_id.'&chat_id='.$chat_id; + $result = requestCurl($url, [], 'GET', $this->headers, 'json'); + $result = json_decode($result, true); + if ($result['code'] != 0) { + return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]); + } + return json_encode(['code' => 200, 'msg' => '获取成功','data' => $result['data']]); + } + + + /** + * 查看对话消息详情 + * @param $data + * @return false|string|\think\response\Json + */ + public function listConversationMessage($data = []) + { + $conversation_id = !empty($data['conversation_id']) ? $data['conversation_id'] : ''; + $chat_id = !empty($data['chat_id']) ? $data['chat_id'] : ''; + $url = $this->apiUrl . '/v3/chat/message/list?conversation_id='.$conversation_id.'&chat_id='.$chat_id; + $result = requestCurl($url, [], 'GET', $this->headers, 'json'); + $result = json_decode($result, true); + if ($result['code'] != 0) { + return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]); + } + return json_encode(['code' => 200, 'msg' => '获取成功','data' => $result['data']]); + } + + + /** + * 取消进行中的对话 + * @param $data + * @return false|string|\think\response\Json + */ + public function cancelConversationChat($data = []) + { + $conversation_id = !empty($data['conversation_id']) ? $data['conversation_id'] : ''; + $chat_id = !empty($data['chat_id']) ? $data['chat_id'] : ''; + + // 构建请求数据 + $params = [ + 'conversation_id' => (string) $conversation_id, + 'chat_id' => (string) $chat_id + ]; + + $url = $this->apiUrl . '/v3/chat/cancel'; + $result = requestCurl($url, $params, 'POST', $this->headers, 'json'); + $result = json_decode($result, true); + if ($result['code'] != 0) { + return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]); + } + return json_encode(['code' => 200, 'msg' => '取消成功', 'data' => []]); + } + +} \ No newline at end of file diff --git a/application/ai/controller/DouBaoAI.php b/application/ai/controller/DouBaoAI.php new file mode 100644 index 0000000..a3c942c --- /dev/null +++ b/application/ai/controller/DouBaoAI.php @@ -0,0 +1,68 @@ +apiUrl = Env::get('doubaoAi.api_url'); + $this->apiKey = Env::get('doubaoAi.api_key'); + + // 设置请求头 + $this->headers = [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . $this->apiKey + ]; + + if (empty($this->apiKey) || empty($this->apiUrl)) { + return json_encode(['code' => 500, 'msg' => '参数缺失']); + } + } + + + + public function text($params = []) + { + + if (empty($params)){ + $content = $this->request->param('content', ''); + $model = $this->request->param('model', 'doubao-seed-1-8-251215'); + if(empty($content)){ + return json_encode(['code' => 500, 'msg' => '提示词缺失']); + } + $params = [ + 'model' => $model, + 'messages' => [ + ['role' => 'system', 'content' => '你现在是存客宝的AI助理,你精通中国大陆的法律'], + ['role' => 'user', 'content' => $content], + ], + ]; + } + $result = requestCurl($this->apiUrl, $params, 'POST', $this->headers, 'json'); + $result = json_decode($result, true); + if(isset($result['error'])){ + $error = $result['error']; + return json_encode(['code' => 500, 'msg' => $error['message']]); + }else{ + $content = $result['choices'][0]['message']['content']; + $token = intval($result['usage']['total_tokens']) * 20; + + exit_data($content); + return json_encode(['code' => 200, 'msg' => '成功','data' => ['token' => $token,'content' => $content]]); + } + + } + + +} \ No newline at end of file diff --git a/application/ai/controller/OpenAi.php b/application/ai/controller/OpenAi.php new file mode 100644 index 0000000..ac0ff24 --- /dev/null +++ b/application/ai/controller/OpenAi.php @@ -0,0 +1,145 @@ +apiUrl = Env::get('openAi.apiUrl'); + $this->apiKey = Env::get('openAi.apiKey'); + + // 设置请求头 + $this->headers = [ + 'Content-Type: application/json', + 'Authorization: Bearer '.$this->apiKey + ]; + } + + + public function text() + { + + $params = [ + 'model' => 'gpt-3.5-turbo-0125', + 'input' => 'DHA 从孕期到出生到老年都需要,助力大脑发育🧠/减缓脑压力有助记忆/给大脑动力#贝蒂喜藻油DHA 双标认证每粒 150毫克,高含量、高性价比从小吃到老,长期吃更健康 重写这条朋友圈 要求: 1、原本的字数和意思不要修改超过10% 2、出现品牌名或个人名字就去除' + ]; + $result = $this->httpRequest( $this->apiUrl, 'POST', $params,$this->headers); + exit_data($result); + } + + /** + * 示例:调用OpenAI API生成睡前故事 + * 对应curl命令: + * curl "https://api.ai.com/v1/responses" \ + * -H "Content-Type: application/json" \ + * -H "Authorization: Bearer $OPENAI_API_KEY" \ + * -d '{ + * "model": "gpt-5", + * "input": "Write a one-sentence bedtime story about a unicorn." + * }' + */ + public function bedtimeStory() + { + + // API请求参数 + $params = [ + 'model' => 'gpt-5', + 'input' => 'Write a one-sentence bedtime story about a unicorn.' + ]; + + // 发送请求到OpenAI API + $url = 'https://api.openai.com/v1/responses'; + $result = $this->httpRequest($url, 'POST', $params, $this->headers); + + // 返回结果 + exit_data($result); + } + + + + + /** + * CURL请求 - 专门用于JSON API请求 + * + * @param $url 请求url地址 + * @param $method 请求方法 get post + * @param null $postfields post数据数组 + * @param array $headers 请求header信息 + * @param int $timeout 超时时间 + * @param bool|false $debug 调试开启 默认false + * @return mixed + */ + protected function httpRequest($url, $method = "GET", $postfields = null, $headers = array(), $timeout = 30, $debug = false) + { + $method = strtoupper($method); + $ci = curl_init(); + + /* Curl settings */ + curl_setopt($ci, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0"); + curl_setopt($ci, CURLOPT_CONNECTTIMEOUT, 60); /* 在发起连接前等待的时间,如果设置为0,则无限等待 */ + curl_setopt($ci, CURLOPT_TIMEOUT, $timeout); /* 设置cURL允许执行的最长秒数 */ + curl_setopt($ci, CURLOPT_RETURNTRANSFER, true); + + switch ($method) { + case "POST": + curl_setopt($ci, CURLOPT_POST, true); + if (!empty($postfields)) { + // 对于JSON API,直接将数组转换为JSON字符串 + if (is_array($postfields)) { + $tmpdatastr = json_encode($postfields); + } else { + $tmpdatastr = $postfields; + } + curl_setopt($ci, CURLOPT_POSTFIELDS, $tmpdatastr); + } + break; + default: + curl_setopt($ci, CURLOPT_CUSTOMREQUEST, $method); /* //设置请求方式 */ + break; + } + + $ssl = preg_match('/^https:\/\//i', $url) ? TRUE : FALSE; + curl_setopt($ci, CURLOPT_URL, $url); + if ($ssl) { + curl_setopt($ci, CURLOPT_SSL_VERIFYPEER, FALSE); // https请求 不验证证书和hosts + curl_setopt($ci, CURLOPT_SSL_VERIFYHOST, FALSE); // 不从证书中检查SSL加密算法是否存在 + } + + if (ini_get('open_basedir') == '' && ini_get('safe_mode' == 'Off')) { + curl_setopt($ci, CURLOPT_FOLLOWLOCATION, 1); + } + curl_setopt($ci, CURLOPT_MAXREDIRS, 2);/*指定最多的HTTP重定向的数量,这个选项是和CURLOPT_FOLLOWLOCATION一起使用的*/ + curl_setopt($ci, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ci, CURLINFO_HEADER_OUT, true); + + $response = curl_exec($ci); + $requestinfo = curl_getinfo($ci); + $http_code = curl_getinfo($ci, CURLINFO_HTTP_CODE); + + if ($debug) { + echo "=====post data======\r\n"; + var_dump($postfields); + echo "=====info===== \r\n"; + print_r($requestinfo); + echo "=====response=====\r\n"; + print_r($response); + } + + curl_close($ci); + return $response; + } + + + +} \ No newline at end of file diff --git a/application/api/config/route.php b/application/api/config/route.php new file mode 100644 index 0000000..8bee16d --- /dev/null +++ b/application/api/config/route.php @@ -0,0 +1,109 @@ +request->header('authorization', $this->authorization)); + if (empty($authorization)) { + if ($isInner) { + return json_encode(['code' => 500, 'msg' => '缺少授权信息']); + } else { + return errorJson('缺少授权信息'); + } + } + + try { + // 构建请求参数 + $params = [ + 'showNormalAccount' => $showNormalAccount, + 'keyword' => $keyword, + 'departmentId' => $departmentId, + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize + ]; + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'plain'); + + // 发送请求获取公司账号列表 + $result = requestCurl($this->baseUrl . 'api/Account/myTenantPageAccounts', $params, 'GET', $header); + $response = handleApiResponse($result); + + // 保存数据到数据库 + if (!empty($response['results'])) { + foreach ($response['results'] as $item) { + $this->saveAccount($item); + } + } + + if ($isInner) { + return json_encode(['code' => 200, 'msg' => '获取公司账号列表成功', 'data' => $response]); + } else { + + return successJson($response); + } + } catch (\Exception $e) { + if ($isInner) { + return json_encode(['code' => 500, 'msg' => '获取公司账号列表失败:' . $e->getMessage()]); + } else { + return errorJson('获取公司账号列表失败:' . $e->getMessage()); + } + } + } + + /** + * 创建新账号 + * @return \think\response\Json + */ + public function createAccount() + { + // 获取授权token + $authorization = trim($this->request->header('authorization', $this->authorization)); + if (empty($authorization)) { + return errorJson('缺少授权信息'); + } + + try { + // 获取并验证请求参数 + $userName = $this->request->param('userName', ''); + $password = $this->request->param('password', ''); + $realName = $this->request->param('realName', ''); + $nickname = $this->request->param('nickname', ''); + $memo = $this->request->param('memo', ''); + $departmentId = $this->request->param('departmentId', 0); + + + // 参数验证 + if (empty($userName)) { + return errorJson('用户名不能为空'); + } + // if (!preg_match('/^[a-zA-Z][a-zA-Z0-9]{5,9}$/', $userName)) { + // return errorJson('用户名必须以字母开头,只能包含字母和数字,长度6-10位'); + // } + if (empty($password)) { + return errorJson('密码不能为空'); + } + + if (empty($realName)) { + return errorJson('真实姓名不能为空'); + } + if (empty($departmentId)) { + return errorJson('公司ID不能为空'); + } + + // 检查账号是否已存在 + $existingAccount = CompanyAccountModel::where('userName', $userName)->find(); + if (!empty($existingAccount)) { + return errorJson('账号已存在'); + } + + + // 构建请求参数 + $params = [ + 'userName' => $userName, + 'password' => $password, + 'realName' => $realName, + 'nickname' => $nickname, + 'memo' => $memo, + 'departmentId' => $departmentId, + 'departmentIdArr' => empty($departmentId) ? [914] : [914, $departmentId] + ]; + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求创建账号 + $result = requestCurl($this->baseUrl . 'api/account/newAccount', $params, 'POST', $header, 'json'); + + if (is_numeric($result)) { + $res = CompanyAccountModel::create([ + 'id' => $result, + 'tenantId' => 242, + 'userName' => $userName, + 'realName' => $realName, + 'nickname' => $nickname, + 'passwordMd5' => md5($password), + 'passwordLocal' => localEncrypt($password), + 'memo' => $memo, + 'accountType' => 11, + 'departmentId' => $departmentId, + 'createTime' => time(), + 'privilegeIds' => json_encode([]) + ]); + $this->setPrivileges(['id' => $result]); + return successJson($res); + } else { + return errorJson($result); + } + } catch (\Exception $e) { + return errorJson('创建账号失败:' . $e->getMessage()); + } + } + + + /** + * 创建新账号(包含创建部门) + * @return \think\response\Json + */ + public function createNewAccount() + { + // 获取授权token + $authorization = trim($this->request->header('authorization', $this->authorization)); + if (empty($authorization)) { + return errorJson('缺少授权信息'); + } + + DB::startTrans(); + try { + // 获取参数 + $departmentName = $this->request->param('departmentName', ''); + $departmentMemo = $this->request->param('departmentMemo', ''); + $accountName = $this->request->param('accountName', ''); + $accountPassword = $this->request->param('accountPassword', ''); + $accountRealName = $this->request->param('accountRealName', ''); + $accountNickname = $this->request->param('accountNickname', ''); + $accountMemo = $this->request->param('accountMemo', ''); + + // 验证参数 + if (empty($departmentName)) { + return errorJson('部门名称不能为空'); + } + if (empty($accountName)) { + return errorJson('账号名称不能为空'); + } + if (empty($accountPassword)) { + return errorJson('账号密码不能为空'); + } + + // 检查部门是否已存在 + $existingDepartment = CompanyModel::where('name', $departmentName)->find(); + if (!empty($existingDepartment)) { + return errorJson('部门以存在'); + } + + // 检查账号是否已存在 + $existingAccount = CompanyAccountModel::where('userName', $accountName)->find(); + if (!empty($existingAccount)) { + return errorJson('账号已存在'); + } + + + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 1. 创建部门 + $departmentParams = [ + 'name' => $departmentName, + 'memo' => $departmentMemo, + 'departmentIdArr' => [914], + 'parentId' => 914 + ]; + + $departmentResult = requestCurl($this->baseUrl . 'api/Department/createDepartment', $departmentParams, 'POST', $header, 'json'); + if (is_numeric($departmentResult)) { + // 保存部门到数据库 + CompanyModel::create([ + 'id' => $departmentResult, + 'name' => $departmentName, + 'memo' => $departmentMemo, + 'tenantId' => 242, + 'isTop' => 0, + 'level' => 1, + 'parentId' => 914, + 'privileges' => '', + 'createTime' => time(), + 'lastUpdateTime' => 0 + ]); + + $this->setPrivileges(['id' => $departmentResult]); + + } else { + DB::rollback(); + return errorJson('创建部门失败:' . $departmentResult); + } + + // 2. 创建账号 + $accountParams = [ + 'userName' => $accountName, + 'password' => $accountPassword, + 'realName' => $accountRealName, + 'nickname' => $accountNickname, + 'memo' => $accountMemo, + 'departmentId' => $departmentResult, + 'departmentIdArr' => [914, $departmentResult] + ]; + + $accountResult = requestCurl($this->baseUrl . 'api/Account/newAccount', $accountParams, 'POST', $header, 'json'); + + if (is_numeric($accountResult)) { + $res = CompanyAccountModel::create([ + 'id' => $accountResult, + 'tenantId' => 242, + 'userName' => $accountName, + 'realName' => $accountRealName, + 'nickname' => $accountNickname, + 'passwordMd5' => md5($accountPassword), + 'passwordLocal' => localEncrypt($accountPassword), + 'memo' => $accountMemo, + 'accountType' => 11, + 'departmentId' => $departmentResult, + 'createTime' => time(), + 'privilegeIds' => json_encode([]) + ]); + DB::commit(); + return successJson($res, '账号创建成功'); + } else { + // 如果创建账号失败,删除已创建的部门 + $this->deleteDepartment($accountResult); + DB::rollback(); + return errorJson('创建账号失败:' . $accountResult); + } + + } catch (\Exception $e) { + DB::rollback(); + return errorJson('创建账号失败:' . $e->getMessage()); + } + } + + + + /************************ 部门管理相关接口 ************************/ + + /** + * 获取部门列表 + * @return \think\response\Json + */ + public function getDepartmentList($isInner = false) + { + // 获取授权token + $authorization = trim($this->request->header('authorization', $this->authorization)); + if (empty($authorization)) { + if ($isInner) { + return json_encode(['code' => 500, 'msg' => '缺少授权信息']); + } else { + return errorJson('缺少授权信息'); + } + } + + try { + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求获取部门列表 + $url = $this->baseUrl . 'api/Department/fetchMyAndSubordinateDepartment'; + $result = requestCurl($url, [], 'GET', $header, 'json'); + + // 处理返回结果 + $response = handleApiResponse($result); + + // 保存数据到数据库 + if (!empty($response)) { + CompanyModel::where('1=1')->delete(); + $this->processDepartments($response); + } + + if ($isInner) { + return json_encode(['code' => 200, 'msg' => '获取部门列表成功', 'data' => $response]); + } else { + return successJson($response, '获取部门列表成功'); + } + } catch (\Exception $e) { + if ($isInner) { + return json_encode(['code' => 500, 'msg' => '获取部门列表失败:' . $e->getMessage()]); + } else { + return errorJson('获取部门列表失败:' . $e->getMessage()); + } + } + } + + /** + * 创建部门 + * @return \think\response\Json + */ + public function createDepartment() + { + // 获取授权token + $authorization = trim($this->request->header('authorization', $this->authorization)); + if (empty($authorization)) { + return errorJson('缺少授权信息'); + } + + try { + // 获取并验证请求参数 + $name = $this->request->param('name', ''); + $memo = $this->request->param('memo', ''); + if (empty($name)) { + return errorJson('请输入公司名称'); + } + + // 检查部门名称是否已存在 + $departmentId = CompanyModel::where('name', $name)->find(); + if (!empty($departmentId)) { + return errorJson('部门已存在'); + } + + // 构建请求参数 + $params = [ + 'name' => $name, + 'memo' => $memo, + 'departmentIdArr' => [914], + 'parentId' => 914 + ]; + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求创建部门 + $result = requestCurl($this->baseUrl . 'api/Department/createDepartment', $params, 'POST', $header, 'json'); + + // 处理返回结果 + if (is_numeric($result)) { + $res = CompanyModel::create([ + 'id' => $result, + 'name' => $name, + 'memo' => $memo, + 'tenantId' => 242, + 'isTop' => 0, + 'level' => 1, + 'parentId' => 914, + 'privileges' => '', + 'createTime' => time(), + 'lastUpdateTime' => 0 + ]); + return successJson($res); + } else { + return errorJson($result); + } + } catch (\Exception $e) { + return errorJson('创建部门失败:' . $e->getMessage()); + } + } + + /** + * 修改部门信息 + * @return \think\response\Json + */ + public function updateDepartment() + { + // 获取授权token + $authorization = trim($this->request->header('authorization', $this->authorization)); + if (empty($authorization)) { + return errorJson('缺少授权信息'); + } + + try { + // 获取并验证请求参数 + $id = $this->request->param('id', 0); + $name = $this->request->param('name', ''); + $memo = $this->request->param('memo', ''); + + if (empty($id)) { + return errorJson('部门ID不能为空'); + } + if (empty($name)) { + return errorJson('部门名称不能为空'); + } + + // 验证部门是否存在 + $department = CompanyModel::where('id', $id)->find(); + if (empty($department)) { + return errorJson('部门不存在'); + } + + // 构建请求参数 + $departmentIdArr = $department->parentId == 914 ? [914] : [914, $department->parentId]; + $params = [ + 'id' => $id, + 'name' => $name, + 'memo' => $memo, + 'departmentIdArr' => $departmentIdArr, + 'tenantId' => 242, + 'createTime' => $department->createTime, + 'isTop' => $department->isTop, + 'level' => $department->level, + 'parentId' => $department->parentId, + 'lastUpdateTime' => $department->lastUpdateTime, + 'privileges' => $department->privileges + ]; + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求修改部门 + $result = requestCurl($this->baseUrl . 'api/Department/department', $params, 'PUT', $header, 'json'); + $response = handleApiResponse($result); + + // 更新本地数据库 + $department->name = $name; + $department->memo = $memo; + $department->save(); + + return successJson([], '部门修改成功'); + } catch (\Exception $e) { + return errorJson('修改部门失败:' . $e->getMessage()); + } + } + + /** + * 删除部门 + * @return \think\response\Json + */ + public function deleteDepartment($id = '') + { + // 获取授权token + $authorization = trim($this->request->header('authorization', $this->authorization)); + if (empty($authorization)) { + return errorJson('缺少授权信息'); + } + + try { + // 获取并验证部门ID + $id = !empty($id) ? $id : $this->request->param('id', ''); + if (empty($id)) { + return errorJson('部门ID不能为空'); + } + + // 验证部门是否存在 + $department = CompanyModel::where('id', $id)->find(); + if (empty($department)) { + return errorJson('部门不存在'); + } + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送删除请求 + $result = requestCurl($this->baseUrl . 'api/Department/del/' . $id, [], 'DELETE', $header); + + if ($result) { + return errorJson($result); + } else { + // 删除本地数据库记录 + $department->delete(); + return successJson([], '部门删除成功'); + } + + + } catch (\Exception $e) { + return errorJson('删除部门失败:' . $e->getMessage()); + } + } + + /** + * 修改部门权限 + * @return \think\response\Json + */ + public function setPrivileges($data = []) + { + // 获取授权token + $authorization = trim($this->request->header('authorization', $this->authorization)); + if (empty($authorization)) { + return errorJson('缺少授权信息'); + } + + try { + // 获取并验证请求参数 + $id = !empty($data['id']) ? $data['id'] : $this->request->param('id', 0); + if (empty($id)) { + return errorJson('部门ID不能为空'); + } + + $privilegeIds = !empty($data['privilegeIds']) ? $data['privilegeIds'] : '1001,1002,1004,1023,1406,20003,20021,20022,20023,20032,20041,20049,20054,20055,20060,20100,20102,20107'; + $privilegeIds = explode(',',$privilegeIds); + + // 验证部门是否存在 + $department = CompanyModel::where('id', $id)->find(); + if (empty($department)) { + return errorJson('部门不存在'); + } + + + + // 构建请求参数 + $params = [ + 'departmentId' => $id, + 'privilegeIds' => $privilegeIds, + 'syncPrivilege' => true + ]; + + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求修改部门 + $result = requestCurl($this->baseUrl . 'api/Department/privileges', $params, 'PUT', $header, 'json'); + $response = handleApiResponse($result); + + + return successJson([], '部门权限修改成功'); + } catch (\Exception $e) { + return errorJson('修改部门权限失败:' . $e->getMessage()); + } + } + + + + public function accountModify($data = []) + { + // 获取授权token + $authorization = $this->authorization; + if (empty($authorization)) { + return errorJson('缺少授权信息'); + } + + + $id = !empty($data['id']) ? $data['id'] : ''; + if (empty($id)) { + return errorJson('账号ID不能为空'); + } + + $account = CompanyAccountModel::where('id', $id)->find(); + + + + if (empty($account)) { + return errorJson('账号不存在'); + } + $privilegeIds = json_decode($account->privilegeIds,true); + $privilegeIds = !empty($privilegeIds) ? $privilegeIds : [1001,1002,1004,1023,1406,20003,20021,20022,20023,20032,20041,20049,20054,20055,20060,20100,20102,20107,20055]; + + // 构建请求参数 + $params = [ + 'accountType' => !empty($data['accountType']) ? $data['accountType'] : $account->accountType, + 'alive' => !empty($data['alive']) ? $data['alive'] : $account->alive, + 'avatar' => !empty($data['avatar']) ? $data['avatar'] : $account->avatar, + 'createTime' => !empty($data['createTime']) ? $data['createTime'] : $account->createTime, + 'creator' => !empty($data['creator']) ? $data['creator'] : $account->creator, + 'creatorRealName' => !empty($data['creatorRealName']) ? $data['creatorRealName'] : $account->creatorRealName, + 'creatorUserName' => !empty($data['creatorUserName']) ? $data['creatorUserName'] : $account->creatorUserName, + 'departmentId' => !empty($data['departmentId']) ? $data['departmentId'] : $account->departmentId, + 'departmentIdArr' => !empty($data['departmentIdArr']) ? $data['departmentIdArr'] : [914,$account->departmentId], + 'departmentName' => !empty($data['departmentName']) ? $data['departmentName'] : $account->departmentName, + 'hasXiakeAccount' => !empty($data['hasXiakeAccount']) ? $data['hasXiakeAccount'] : false, + 'id' => !empty($data['id']) ? $data['id'] : $account->id, + 'memo' => !empty($data['memo']) ? $data['memo'] : $account->memo, + 'nickname' => !empty($data['nickname']) ? $data['nickname'] : $account->nickname, + 'privilegeIds' => !empty($data['privilegeIds']) ? $data['privilegeIds'] : $privilegeIds, + 'realName' => !empty($data['realName']) ? $data['realName'] : $account->realName, + 'status' => !empty($data['status']) ? $data['status'] : $account->status, + 'tenantId' => !empty($data['tenantId']) ? $data['tenantId'] : $account->tenantId, + 'userName' => !empty($data['userName']) ? $data['userName'] : $account->userName, + ]; + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求修改部门 + $result = requestCurl($this->baseUrl . 'api/account/modify', $params, 'PUT', $header, 'json'); + $response = handleApiResponse($result); + + + if(empty($response)){ + $newData = [ + 'nickname' => $params['nickname'], + 'avatar' => $params['avatar'], + ]; + CompanyAccountModel::where('id', $id)->update($newData); + return json_encode(['code' => 200, 'msg' => '账号修改成功']); + }else{ + return json_encode(['code' => 500, 'msg' => $response]); + } + } + + + + + + + /************************ 私有辅助方法 ************************/ + + /** + * 递归处理部门列表 + * @param array $departments 部门数据 + */ + private function processDepartments($departments) + { + if (empty($departments) || !is_array($departments)) { + return; + } + + foreach ($departments as $item) { + // 保存当前部门 + $this->saveDepartment($item); + + // 递归处理子部门 + if (!empty($item['children']) && is_array($item['children'])) { + $this->processDepartments($item['children']); + } + } + } + + /** + * 保存部门数据到数据库 + * @param array $item 部门数据 + */ + private function saveDepartment($item) + { + $data = [ + 'id' => isset($item['id']) ? $item['id'] : 0, + 'name' => isset($item['name']) ? $item['name'] : '', + 'memo' => isset($item['memo']) ? $item['memo'] : '', + 'level' => isset($item['level']) ? $item['level'] : 0, + 'isTop' => isset($item['isTop']) ? $item['isTop'] : false, + 'parentId' => isset($item['parentId']) ? $item['parentId'] : 0, + 'tenantId' => isset($item['tenantId']) ? $item['tenantId'] : 0, + 'privileges' => isset($item['privileges']) ? (is_array($item['privileges']) ? json_encode($item['privileges']) : $item['privileges']) : '', + 'createTime' => isset($item['createTime']) ? strtotime($item['createTime']) : 0, + 'lastUpdateTime' => isset($item['lastUpdateTime']) ? ($item['lastUpdateTime'] == '0001-01-01T00:00:00' ? 0 : strtotime($item['lastUpdateTime'])) : 0 + ]; + + // 使用id作为唯一性判断 + $department = CompanyModel::where('id', $item['id'])->find(); + if ($department) { + $department->save($data); + } else { + CompanyModel::create($data); + } + } + + /** + * 保存账号数据到数据库 + * @param array $item 账号数据 + */ + private function saveAccount($item) + { + // 将日期时间字符串转换为时间戳 + $createTime = isset($item['createTime']) ? strtotime($item['createTime']) : null; + $deleteTime = isset($item['deleteTime']) ? strtotime($item['deleteTime']) : null; + + $data = [ + 'id' => $item['id'], + 'accountType' => isset($item['accountType']) ? $item['accountType'] : 0, + 'status' => isset($item['status']) ? $item['status'] : 0, + 'tenantId' => isset($item['tenantId']) ? $item['tenantId'] : 0, + 'userName' => isset($item['userName']) ? $item['userName'] : '', + 'realName' => isset($item['realName']) ? $item['realName'] : '', + 'nickname' => isset($item['nickname']) ? $item['nickname'] : '', + 'avatar' => isset($item['avatar']) ? $item['avatar'] : '', + 'phone' => isset($item['phone']) ? $item['phone'] : '', + 'memo' => isset($item['memo']) ? $item['memo'] : '', + 'createTime' => $createTime, + 'creator' => isset($item['creator']) ? $item['creator'] : 0, + 'creatorUserName' => isset($item['creatorUserName']) ? $item['creatorUserName'] : '', + 'creatorRealName' => isset($item['creatorRealName']) ? $item['creatorRealName'] : '', + 'departmentId' => isset($item['departmentId']) ? $item['departmentId'] : 0, + 'departmentName' => isset($item['departmentName']) ? $item['departmentName'] : '', + 'privilegeIds' => isset($item['privilegeIds']) ? json_encode($item['privilegeIds']) : json_encode([]), + 'alive' => isset($item['alive']) ? $item['alive'] : false, + 'hasXiakeAccount' => isset($item['hasXiakeAccount']) ? $item['hasXiakeAccount'] : false, + 'isDeleted' => isset($item['isDeleted']) ? $item['isDeleted'] : false, + 'deleteTime' => $deleteTime + ]; + + // 使用tenantId作为唯一性判断 + $account = CompanyAccountModel::where('id', $item['id'])->find(); + if ($account) { + $account->save($data); + } else { + CompanyAccountModel::create($data); + } + } +} \ No newline at end of file diff --git a/application/api/controller/AllotRuleController.php b/application/api/controller/AllotRuleController.php new file mode 100644 index 0000000..db590d6 --- /dev/null +++ b/application/api/controller/AllotRuleController.php @@ -0,0 +1,362 @@ +request->header('authorization', $this->authorization)); + if (empty($authorization)) { + if ($isInner) { + return json_encode(['code' => 500, 'msg' => '缺少授权信息']); + } else { + return errorJson('缺少授权信息'); + } + } + + try { + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'plain'); + + // 发送请求获取所有分配规则 + $result = requestCurl($this->baseUrl . 'api/AllotRule/all', [], 'GET', $header); + $response = handleApiResponse($result); + + // 保存数据到数据库 + if (!empty($response)) { + AllotRuleModel::where('1=1')->update(['isDel' => 1]); + foreach ($response as $item) { + $this->saveAllotRule($item); + } + } + + if ($isInner) { + return json_encode(['code' => 200, 'msg' => 'success', 'data' => $response]); + } else { + return successJson($response); + } + } catch (\Exception $e) { + if ($isInner) { + return json_encode(['code' => 500, 'msg' => '获取分配规则失败:' . $e->getMessage()]); + } else { + return errorJson('获取分配规则失败:' . $e->getMessage()); + } + } + } + + /** + * 手动触发分配规则同步任务 + * @return \think\response\Json + */ + public function startJob() + { + try { + $data = [ + 'time' => time() + ]; + + // 添加到队列,设置任务名为 allotrule_list + $isSuccess = Queue::push(AllotRuleListJob::class, $data, 'allotrule_list'); + + if ($isSuccess !== false) { + return successJson([], '分配规则同步任务已添加到队列'); + } else { + return errorJson('添加分配规则同步任务到队列失败'); + } + } catch (\Exception $e) { + return errorJson('触发分配规则同步任务失败:' . $e->getMessage()); + } + } + + /************************************ + * 分配规则CRUD操作方法 + ************************************/ + + /** + * 创建分配规则 + * @param array $data 请求数据 + * @param bool $isInner 是否为内部调用 + * @return \think\response\Json + */ + public function createRule($data = [], $isInner = false) + { + // 获取授权token + $authorization = trim($this->request->header('authorization', $this->authorization)); + if (empty($authorization)) { + if ($isInner) { + return json_encode(['code' => 500, 'msg' => '缺少授权信息']); + } else { + return errorJson('缺少授权信息'); + } + } + + try { + $wechatData = input('wechatData', '[]') ?: []; + $priorityStrategy = input('priorityStrategy', '[]') ?: []; + + // 构建请求数据 + $params = [ + 'allotType' => input('allotType', 1), + 'kefuRange' => input('kefuRange', 5), + 'wechatRange' => input('wechatRange',3), + 'kefuData' => input('kefuData', '[]') ?: [], + 'wechatData' => $wechatData, + 'labels' => input('labels', '[]') ?: [], + 'priorityStrategy' => json_encode($priorityStrategy,256) + ]; + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求到微信接口 + $result = requestCurl($this->baseUrl . 'api/AllotRule/new', $params, 'POST', $header, 'json'); + if (empty($result)) { + // 异步更新所有规则列表 + AllotRuleModel::where('1=1')->update(['isDel' => 1]); + $this->getAllRules(); + $res = AllotRuleModel::where('isDel',0)->order('id','DESC')->find(); + $res->departmentId = !empty($data['departmentId']) ? $data['departmentId'] : 0; + $res->save(); + return successJson($res, '创建分配规则成功'); + } else { + return errorJson($result); + } + } catch (\Exception $e) { + return errorJson('创建分配规则失败:' . $e->getMessage()); + } + } + + /** + * 更新分配规则 + * @param array $data 请求数据 + * @param bool $isInner 是否为内部调用 + * @return \think\response\Json + */ + public function updateRule($data = [], $isInner = false) + { + // 获取授权token + $authorization = trim($this->request->header('authorization', $this->authorization)); + if (empty($authorization)) { + return errorJson('缺少授权信息'); + } + + try { + // 获取请求参数 + $id = !empty($data['id']) ? $data['id'] : input('id',0); + if (empty($id)) { + return errorJson('规则ID不能为空'); + } + + $rule = AllotRuleModel::where('id', $id)->find(); + if (empty($rule)) { + return errorJson('规则不存在'); + } + + // 构建请求数据 + $params = [ + 'id' => $id, + 'tenantId' => $rule['tenantId'], + 'allotType' => !empty($data['allotType']) ? $data['allotType'] : input('allotType',1), + 'allotOnline' => !empty($data['allotOnline']) ? $data['allotOnline'] : input('allotOnline',false), + 'kefuRange' => !empty($data['kefuRange']) ? $data['kefuRange'] : input('kefuRange',5), + 'wechatRange' => !empty($data['wechatRange']) ? $data['wechatRange'] : input('wechatRange',3), + 'kefuData' => !empty($data['kefuData']) ? $data['kefuData'] : input('kefuData',[]), + 'wechatData' => !empty($data['wechatData']) ? $data['wechatData'] : input('wechatData',[]), + 'labels' => !empty($data['labels']) ? $data['labels'] : input('labels',[]), + 'priorityStrategy' => json_encode(!empty($data['priorityStrategy']) ? $data['priorityStrategy'] : input('priorityStrategy',[]),256), + ]; + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求到微信接口 + $result = requestCurl($this->baseUrl . 'api/AllotRule/update', $params, 'PUT', $header, 'json'); + + if (empty($result)) { + $this->getAllRules(); + return successJson([], '更新分配规则成功'); + } else { + return errorJson($result); + } + } catch (\Exception $e) { + return errorJson('更新分配规则失败:' . $e->getMessage()); + } + } + + /** + * 删除分配规则 + * @param array $data 请求数据 + * @param bool $isInner 是否为内部调用 + * @return \think\response\Json + */ + public function deleteRule($data = [], $isInner = false) + { + // 获取授权token + $authorization = trim($this->request->header('authorization', $this->authorization)); + if (empty($authorization)) { + if ($isInner) { + return json_encode(['code' => 500, 'msg' => '缺少授权信息']); + } else { + return errorJson('缺少授权信息'); + } + } + + try { + // 获取请求参数 + $id = !empty($data['id']) ? $data['id'] : input('id', 0); + if (empty($id)) { + return errorJson('规则ID不能为空'); + } + + // 检查规则是否存在 + $rule = AllotRuleModel::where('id', $id)->find(); + if (empty($rule)) { + return errorJson('规则不存在'); + } + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'plain'); + + // 发送请求到微信接口 + $result = requestCurl($this->baseUrl . 'api/AllotRule/delete?id=' . $id, [], 'DELETE', $header); + $response = handleApiResponse($result); + + if (empty($response)) { + // 删除成功,同步本地数据库 + AllotRuleModel::where('id', $id)->update(['isDel' => 1]); + return successJson([], '删除分配规则成功'); + } else { + return errorJson($response); + } + } catch (\Exception $e) { + return errorJson('删除分配规则失败:' . $e->getMessage()); + } + } + + /************************************ + * 数据查询相关方法 + ************************************/ + + /** + * 自动创建分配规则 + * 根据今日新增微信账号自动创建或更新分配规则 + * @param array $data 请求数据 + * @param bool $isInner 是否为内部调用 + * @return \think\response\Json|string + */ + public function autoCreateAllotRules($data = [], $isInner = false) + { + try { + // 获取今天的开始时间和结束时间 + $todayStart = strtotime(date('Y-m-d 00:00:00')); + $todayEnd = strtotime(date('Y-m-d 23:59:59')); + + // 查询今天新增的微信账号 + $newAccounts = Db::table('s2_wechat_account') + ->alias('wa') + ->join(['s2_company_account' => 'ca'], 'wa.deviceAccountId = ca.id', 'LEFT') + ->field([ + 'wa.id', + 'wa.wechatId', + 'wa.nickname', + 'wa.deviceAccountId', + 'wa.alias', + 'wa.createTime', + 'ca.departmentId', + ]) + ->where('wa.createTime', 'BETWEEN', [$todayStart, $todayEnd]) + ->where('wa.isDeleted', 0) + ->order('wa.createTime', 'DESC') + ->select(); + + // 没有今日新增微信账号,直接返回 + if (empty($newAccounts)) { + $result = ['code' => 200, 'msg' => '没有今日新增微信账号,无需创建分配规则', 'data' => []]; + return $isInner ? json_encode($result) : successJson([], '没有今日新增微信账号,无需创建分配规则'); + } + + // 获取所有分配规则 + foreach ($newAccounts as $key => $value) { + $rules = AllotRuleModel::where(['departmentId' => $value['departmentId'],'isDel' => 0])->order('id','DESC')->find(); + if (!empty($rules)) { + $wechatData = json_decode($rules['wechatData'], true); + if (!in_array($value['id'], $wechatData)) { + $wechatData[] = $value['id']; + $kefuData = [$value['deviceAccountId']]; + $this->updateRule(['id' => $rules['id'],'wechatData' => $wechatData,'kefuData' => $kefuData],true); + } + }else{ + $wechatData =[$value['id']]; + $kefuData = [$value['deviceAccountId']]; + $this->createRule(['wechatData' => $wechatData,'kefuData' => $kefuData,'departmentId' => $value['departmentId']],true); + } + } + $result = ['code' => 200, 'msg' => '自动分配规则成功', 'data' => []]; + return $isInner ? json_encode($result) : successJson([], '自动分配规则成功'); + } catch (\Exception $e) { + $error = '自动分配规则失败: ' . $e->getMessage(); + $result = ['code' => 500, 'msg' => $error]; + return $isInner ? json_encode($result) : errorJson($error); + } + } + + /************************************ + * 辅助方法 + ************************************/ + + /** + * 保存分配规则数据到数据库 + * @param array $item 分配规则数据 + */ + private function saveAllotRule($item) + { + $data = [ + 'id' => isset($item['id']) ? $item['id'] : '', + 'tenantId' => isset($item['tenantId']) ? $item['tenantId'] : 0, + 'allotType' => isset($item['allotType']) ? $item['allotType'] : 0, + 'allotOnline' => isset($item['allotOnline']) ? $item['allotOnline'] : false, + 'kefuRange' => isset($item['kefuRange']) ? $item['kefuRange'] : 0, + 'wechatRange' => isset($item['wechatRange']) ? $item['wechatRange'] : 0, + 'kefuData' => isset($item['kefuData']) ? json_encode($item['kefuData']) : json_encode([]), + 'wechatData' => isset($item['wechatData']) ? json_encode($item['wechatData']) : json_encode([]), + 'labels' => isset($item['labels']) ? json_encode($item['labels']) : json_encode([]), + 'priorityStrategy' => isset($item['priorityStrategy']) ? json_encode($item['priorityStrategy']) : json_encode([]), + 'sortIndex' => isset($item['sortIndex']) ? $item['sortIndex'] : 0, + 'creatorAccountId' => isset($item['creatorAccountId']) ? $item['creatorAccountId'] : 0, + 'createTime' => isset($item['createTime']) ? (strtotime($item['createTime']) ?: 0) : 0, + 'ruleName' => isset($item['ruleName']) ? $item['ruleName'] : '', + 'isDel' => 0, + ]; + + // 使用ID作为唯一性判断 + $rule = AllotRuleModel::where('id', $item['id'])->find(); + + if ($rule) { + $rule->save($data); + } else { + AllotRuleModel::create($data); + } + } +} \ No newline at end of file diff --git a/application/api/controller/AutomaticAssign.php b/application/api/controller/AutomaticAssign.php new file mode 100644 index 0000000..6a0953c --- /dev/null +++ b/application/api/controller/AutomaticAssign.php @@ -0,0 +1,346 @@ +request->header('authorization', $this->authorization)); + if (empty($authorization)) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'缺少授权信息']); + }else{ + return errorJson('缺少授权信息'); + } + } + + try { + // 获取请求参数 + $toAccountId = !empty($data['toAccountId']) ? $data['toAccountId'] : input('toAccountId', ''); + $wechatAccountKeyword = !empty($data['wechatAccountKeyword']) ? $data['wechatAccountKeyword'] : input('wechatAccountKeyword', ''); + $isDeleted = !empty($data['isDeleted']) ? $data['isDeleted'] : input('isDeleted', false); + + if (empty($toAccountId)) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'目标账号ID不能为空']); + }else{ + return errorJson('目标账号ID不能为空'); + } + } + + $params = [ + 'accountKeyword' => !empty($data['accountKeyword']) ? $data['accountKeyword'] : '', + 'addFrom' => !empty($data['addFrom']) ? $data['addFrom'] : [], + 'allotAccountId' => !empty($data['allotAccountId']) ? $data['allotAccountId'] : '', + 'containAllLabel' => !empty($data['containAllLabel']) ? $data['containAllLabel'] : false, + 'containSubDepartment' => !empty($data['containSubDepartment']) ? $data['containSubDepartment'] : false, + 'departmentId' => !empty($data['departmentId']) ? $data['departmentId'] : '', + 'extendFields' => !empty($data['extendFields']) ? $data['extendFields'] : [], + 'friendKeyword' => !empty($data['friendKeyword']) ? $data['friendKeyword'] : '', + 'friendPhoneKeyword' => !empty($data['friendPhoneKeyword']) ? $data['friendPhoneKeyword'] : '', + 'friendPinYinKeyword' => !empty($data['friendPinYinKeyword']) ? $data['friendPinYinKeyword'] : '', + 'friendRegionKeyword' => !empty($data['friendRegionKeyword']) ? $data['friendRegionKeyword'] : '', + 'friendRemarkKeyword' => !empty($data['friendRemarkKeyword']) ? $data['friendRemarkKeyword'] : '', + 'gender' => !empty($data['gender']) ? $data['gender'] : '', + 'groupId' => !empty($data['groupId']) ? $data['groupId'] : null, + 'isByRule' => !empty($data['isByRule']) ? $data['isByRule'] : false, + 'isDeleted' => $isDeleted, + 'isPass' => !empty($data['isPass']) ? $data['isPass'] : true, + 'keyword' => !empty($data['keyword']) ? $data['keyword'] : '', + 'labels' => !empty($data['labels']) ? $data['labels'] : [], + 'pageIndex' => !empty($data['pageIndex']) ? $data['pageIndex'] : 0, + 'pageSize' => !empty($data['pageSize']) ? $data['pageSize'] : 100, + 'preFriendId' => !empty($data['preFriendId']) ? $data['preFriendId'] : '', + 'toAccountId' => $toAccountId, + 'wechatAccountKeyword' => $wechatAccountKeyword + ]; + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求 + $result = requestCurl($this->baseUrl . 'api/WechatFriend/allotSearchResult', $params, 'PUT', $header,'json'); + $response = handleApiResponse($result); + + if($response){ + if($isInner){ + return json_encode(['code'=>200,'msg'=>'微信好友自动分配成功']); + }else{ + return successJson([],'微信好友自动分配成功'); + } + }else{ + if($isInner){ + return json_encode(['code'=>500,'msg'=>$response]); + }else{ + return errorJson($response); + } + } + + } catch (\Exception $e) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'微信好友自动分配失败:' . $e->getMessage()]); + }else{ + return errorJson('微信好友自动分配失败:' . $e->getMessage()); + } + } + } + + + /** + * 自动分配微信群聊 + * @param string $toAccountId 目标账号ID + * @param string $wechatAccountKeyword 微信账号关键字 + * @param bool $isDeleted 是否已删除 + * @return \think\response\Json + */ + public function autoAllotWechatChatroom($data = []) + { + // 获取授权token + $authorization = trim($this->request->header('authorization', $this->authorization)); + if (empty($authorization)) { + return errorJson('缺少授权信息'); + } + + try { + // 获取请求参数 + $toAccountId = !empty($data['toAccountId']) ? $data['toAccountId'] : input('toAccountId', ''); + $wechatAccountKeyword = !empty($data['wechatAccountKeyword']) ? $data['wechatAccountKeyword'] : input('wechatAccountKeyword', ''); + $isDeleted = !empty($data['isDeleted']) ? $data['isDeleted'] : input('isDeleted', false); + + if (empty($toAccountId)) { + return errorJson('目标账号ID不能为空'); + } + + $params = [ + 'AllotBySearch' => true, + 'byRule' => false, + 'comment' => '', + 'groupId' => null, + 'isDeleted' => $isDeleted, + 'keyword' => '', + 'memberKeyword' => '', + 'notifyReceiver' => false, + 'toAccountId' => $toAccountId, + 'wechatAccountKeyword' => $wechatAccountKeyword, + 'wechatChatroomId' => 0, + 'wechatChatroomIds' => [] + ]; + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求 + $result = requestCurl($this->baseUrl . 'api/WechatChatroom/allotChatroom', $params, 'PUT', $header, 'json'); + $response = handleApiResponse($result); + + if($response){ + return successJson([], '微信群聊自动分配成功'); + }else{ + return errorJson($response); + } + } catch (\Exception $e) { + return errorJson('微信群聊自动分配失败:' . $e->getMessage()); + } + } + + /** + * 指定微信好友分配到指定账号 + * @param int $wechatFriendId 微信好友ID + * @param int $toAccountId 目标账号ID + * @param string $comment 评论/备注 + * @param bool $notifyReceiver 是否通知接收者 + * @param int $optFrom 操作来源 + * @return \think\response\Json + */ + public function allotWechatFriend($data = [],$isInner = false,$errorNum = 0) + { + // 获取授权token + $authorization = $this->authorization; + + if (empty($authorization)) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'缺少授权信息']); + }else{ + return errorJson('缺少授权信息'); + } + } + + try { + // 获取请求参数 + $wechatFriendId = !empty($data['wechatFriendId']) ? $data['wechatFriendId'] : input('wechatFriendId', 0); + $toAccountId = !empty($data['toAccountId']) ? $data['toAccountId'] : input('toAccountId', 0); + $comment = !empty($data['comment']) ? $data['comment'] : input('comment', ''); + $notifyReceiver = !empty($data['notifyReceiver']) ? $data['notifyReceiver'] : input('notifyReceiver', 'false'); + $optFrom = !empty($data['optFrom']) ? $data['optFrom'] : input('optFrom', 4); // 默认操作来源为4 + + // 参数验证 + if (empty($wechatFriendId)) { + return json_encode(['code'=>500,'msg'=>'微信好友ID不能为空']); + + } + + if (empty($toAccountId)) { + return json_encode(['code'=>500,'msg'=>'目标账号ID不能为空']); + } + + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求 + $url = $this->baseUrl . 'api/WechatFriend/allot?wechatFriendId='.$wechatFriendId.'¬ifyReceiver='.$notifyReceiver.'&comment='.$comment.'&toAccountId='.$toAccountId.'&optFrom='.$optFrom; + $result = requestCurl($url, [], 'PUT', $header, 'json'); + $response = handleApiResponse($result); + if (empty($response)) { + if($isInner){ + return json_encode(['code'=>200,'msg'=>'微信好友分配成功']); + }else{ + return successJson([], '微信好友分配成功'); + } + } else { + if($isInner){ + return json_encode(['code'=>500,'msg'=>$result]); + }else{ + return errorJson($result); + } + } + } catch (\Exception $e) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'微信好友分配失败:' . $e->getMessage()]); + }else{ + Cache::rm('system_authorization_token'); + Cache::rm('system_refresh_token'); + $errorNum ++; + if ($errorNum <= 3) { + $this->allotWechatFriend($data,$isInner,$errorNum); + } + return json_encode(['code'=>500,'msg'=> $result]); + return errorJson('微信好友分配失败:' . $e->getMessage()); + } + } + } + + + public function multiAllotFriendToAccount($data = [],$errorNum = 0){ + // 获取授权token + $authorization = $this->authorization; + if (empty($authorization)) { + return json_encode(['code'=>500,'msg'=>'缺少授权信息']); + } + + $wechatFriendIds = !empty($data['wechatFriendIds']) ? $data['wechatFriendIds'] : input('wechatFriendIds', []); + $toAccountId = !empty($data['toAccountId']) ? $data['toAccountId'] : input('toAccountId', 0); + $notifyReceiver = !empty($data['notifyReceiver']) ? $data['notifyReceiver'] : input('notifyReceiver', 'false'); + // 参数验证 + if (empty($wechatFriendIds)) { + return json_encode(['code'=>500,'msg'=>'微信好友ID不能为空']); + } + + if (empty($toAccountId)) { + return json_encode(['code'=>500,'msg'=>'目标账号ID不能为空']); + } + + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求 + $url = $this->baseUrl . 'api/WechatFriend/multiAllotFriendToAccount?wechatFriendIds='.$wechatFriendIds.'&toAccountId='.$toAccountId.'¬ifyReceiver='.$notifyReceiver; + $result = requestCurl($url, [], 'PUT', $header, 'json'); + if (empty($result)) { + return json_encode(['code'=>200,'msg'=>'微信好友分配成功']); + } else { + Cache::rm('system_authorization_token'); + Cache::rm('system_refresh_token'); + $errorNum ++; + if ($errorNum <= 3) { + $this->multiAllotFriendToAccount($data,$errorNum); + } + return json_encode(['code'=>500,'msg'=> $result]); + } + + } + + + /** + * 分配搜索结果 + * @param array $data 请求参数 + * @return \think\response\Json + */ + public function allotSearchResult($data = []) + { + // 获取授权token + $authorization = trim($this->request->header('authorization', $this->authorization)); + if (empty($authorization)) { + return json_encode(['code'=>500,'msg'=>'缺少授权信息']); + } + + try { + + $params = [ + 'accountKeyword' => !empty($data['accountKeyword']) ? $data['accountKeyword'] : '', + 'addFrom' => !empty($data['addFrom']) ? $data['addFrom'] : [], + 'allotAccountId' => !empty($data['allotAccountId']) ? $data['allotAccountId'] : '', + 'containAllLabel' => !empty($data['containAllLabel']) ? $data['containAllLabel'] : false, + 'containSubDepartment' => !empty($data['containSubDepartment']) ? $data['containSubDepartment'] : false, + 'departmentId' => !empty($data['departmentId']) ? $data['departmentId'] : '', + 'extendFields' => !empty($data['extendFields']) ? json_encode($data['extendFields']) : json_encode([]), + 'friendKeyword' => !empty($data['friendKeyword']) ? $data['friendKeyword'] : '', + 'friendPhoneKeyword' => !empty($data['friendPhoneKeyword']) ? $data['friendPhoneKeyword'] : '', + 'friendPinYinKeyword' => !empty($data['friendPinYinKeyword']) ? $data['friendPinYinKeyword'] : '', + 'friendRegionKeyword' => !empty($data['friendRegionKeyword']) ? $data['friendRegionKeyword'] : '', + 'friendRemarkKeyword' => !empty($data['friendRemarkKeyword']) ? $data['friendRemarkKeyword'] : '', + 'gender' => !empty($data['gender']) ? $data['gender'] : '', + 'groupId' => !empty($data['groupId']) ? $data['groupId'] : null, + 'isByRule' => !empty($data['isByRule']) ? $data['isByRule'] : false, + 'isDeleted' => !empty($data['isDeleted']) ? $data['isDeleted'] : false, + 'isPass' => !empty($data['isPass']) ? $data['isPass'] : true, + 'keyword' => !empty($data['keyword']) ? $data['keyword'] : '', + 'labels' => !empty($data['labels']) ? $data['labels'] : [], + 'pageIndex' => !empty($data['pageIndex']) ? $data['pageIndex'] : 0, + 'pageSize' => !empty($data['pageSize']) ? $data['pageSize'] : 20, + 'preFriendId' => !empty($data['preFriendId']) ? $data['preFriendId'] : '', + 'toAccountId' => !empty($data['toAccountId']) ? $data['toAccountId'] : '', + 'wechatAccountKeyword' => !empty($data['wechatAccountKeyword']) ? $data['wechatAccountKeyword'] : '' + ]; + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求 + $result = requestCurl($this->baseUrl . 'api/WechatFriend/allotSearchResult', $params, 'POST', $header, 'json'); + $response = handleApiResponse($result); + if($response){ + return json_encode(['code'=>200,'msg'=>'分配成功']); + }else{ + return json_encode(['code'=>500,'msg'=>$response]); + } + } catch (\Exception $e) { + return json_encode(['code'=>500,'msg'=>'微信好友分配失败:' . $e->getMessage()]); + } + } + + +} \ No newline at end of file diff --git a/application/api/controller/BaseController.php b/application/api/controller/BaseController.php new file mode 100644 index 0000000..84f6993 --- /dev/null +++ b/application/api/controller/BaseController.php @@ -0,0 +1,26 @@ +baseUrl = Env::get('api.wechat_url'); + $this->authorization = AuthService::getSystemAuthorization(); + } +} diff --git a/application/api/controller/CallRecordingController.php b/application/api/controller/CallRecordingController.php new file mode 100644 index 0000000..a811261 --- /dev/null +++ b/application/api/controller/CallRecordingController.php @@ -0,0 +1,142 @@ +request->header('authorization', $this->authorization)); + if (empty($authorization)) { + if ($isInner) { + return json_encode(['code' => 500, 'msg' => '缺少授权信息']); + } else { + return errorJson('缺少授权信息'); + } + } + + try { + // 构建请求参数 + $params = [ + 'keyword' => $keyword, + 'isCallOut' => $isCallOut, + 'secondMin' => $secondMin, + 'secondMax' => $secondMax, + 'departmentIds' => $departmentIds, + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize, + 'from' => $from, + 'to' => $to, + 'departmentId' => $departmentId + ]; + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'plain'); + + // 发送请求获取通话记录列表 + $result = requestCurl($this->baseUrl . 'api/CallRecording/list', $params, 'GET', $header); + $response = handleApiResponse($result); + + // 保存数据到数据库 + if (!empty($response['results'])) { + foreach ($response['results'] as $item) { + $this->saveCallRecording($item); + } + } + + if ($isInner) { + return json_encode(['code' => 200, 'msg' => '获取通话记录列表成功', 'data' => $response]); + } else { + return successJson($response, '获取通话记录列表成功'); + } + } catch (\Exception $e) { + if ($isInner) { + return json_encode(['code' => 500, 'msg' => '获取通话记录列表失败:' . $e->getMessage()]); + } else { + return errorJson('获取通话记录列表失败:' . $e->getMessage()); + } + } + } + + /** + * 保存通话记录数据到数据库 + * @param array $item 通话记录数据 + */ + private function saveCallRecording($item) + { + // 将时间戳转换为秒级时间戳(API返回的是毫秒级) + $beginTime = isset($item['beginTime']) ? intval($item['beginTime'] / 1000) : 0; + $endTime = isset($item['endTime']) ? intval($item['endTime'] / 1000) : 0; + $callBeginTime = isset($item['callBeginTime']) ? intval($item['callBeginTime'] / 1000) : 0; + + // 将日期时间字符串转换为时间戳 + $createTime = isset($item['createTime']) ? strtotime($item['createTime']) : 0; + $lastUpdateTime = isset($item['lastUpdateTime']) ? strtotime($item['lastUpdateTime']) : 0; + + $data = [ + 'id' => isset($item['id']) ? $item['id'] : 0, + 'tenantId' => isset($item['tenantId']) ? $item['tenantId'] : 0, + 'deviceOwnerId' => isset($item['deviceOwnerId']) ? $item['deviceOwnerId'] : 0, + 'userName' => isset($item['userName']) ? $item['userName'] : '', + 'nickname' => isset($item['nickname']) ? $item['nickname'] : '', + 'realName' => isset($item['realName']) ? $item['realName'] : '', + 'deviceMemo' => isset($item['deviceMemo']) ? $item['deviceMemo'] : '', + 'fileName' => isset($item['fileName']) ? $item['fileName'] : '', + 'imei' => isset($item['imei']) ? $item['imei'] : '', + 'phone' => isset($item['phone']) ? $item['phone'] : '', + 'isCallOut' => isset($item['isCallOut']) ? $item['isCallOut'] : false, + 'beginTime' => $beginTime, + 'endTime' => $endTime, + 'audioUrl' => isset($item['audioUrl']) ? $item['audioUrl'] : '', + 'mp3AudioUrl' => isset($item['mp3AudioUrl']) ? $item['mp3AudioUrl'] : '', + 'callBeginTime' => $callBeginTime, + 'callLogId' => isset($item['callLogId']) ? $item['callLogId'] : 0, + 'callType' => isset($item['callType']) ? $item['callType'] : 0, + 'duration' => isset($item['duration']) ? $item['duration'] : 0, + 'skipReason' => isset($item['skipReason']) ? $item['skipReason'] : '', + 'skipUpload' => isset($item['skipUpload']) ? $item['skipUpload'] : false, + 'isDeleted' => isset($item['isDeleted']) ? $item['isDeleted'] : false, + 'createTime' => $createTime, + 'lastUpdateTime' => $lastUpdateTime + ]; + + // 使用id作为唯一性判断 + $callRecording = CallRecordingModel::where('id', $item['id'])->find(); + if ($callRecording) { + $callRecording->save($data); + } else { + CallRecordingModel::create($data); + } + } +} \ No newline at end of file diff --git a/application/api/controller/DeviceController.php b/application/api/controller/DeviceController.php new file mode 100644 index 0000000..33dd45d --- /dev/null +++ b/application/api/controller/DeviceController.php @@ -0,0 +1,769 @@ +authorization; + if (empty($authorization)) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'缺少授权信息']); + }else{ + return errorJson('缺少授权信息'); + } + } + + try { + // 根据isDel设置对应的deleteType值 + $deleteType = 'unDeleted'; // 默认值 + if ($isDel == 1) { + $deleteType = 'deleted'; + } elseif ($isDel == 2) { + $deleteType = 'deletedAndStop'; + } + + // 构建请求参数 + $params = [ + 'accountId' => !empty($data['accountId']) ? $data['accountId'] : $this->request->param('accountId', ''), + 'keyword' => $this->request->param('keyword', ''), + 'imei' => $this->request->param('imei', ''), + 'groupId' => $this->request->param('groupId', ''), + 'brand' => $this->request->param('brand', ''), + 'model' => $this->request->param('model', ''), + 'deleteType' => $this->request->param('deleteType', $deleteType), + 'operatingSystem' => $this->request->param('operatingSystem', ''), + 'softwareVersion' => $this->request->param('softwareVersion', ''), + 'phoneAppVersion' => $this->request->param('phoneAppVersion', ''), + 'recorderVersion' => $this->request->param('recorderVersion', ''), + 'contactsVersion' => $this->request->param('contactsVersion', ''), + 'rooted' => $this->request->param('rooted', ''), + 'xPosed' => $this->request->param('xPosed', ''), + 'alive' => $this->request->param('alive', ''), + 'hasWechat' => $this->request->param('hasWechat', ''), + 'departmentId' => $this->request->param('departmentId', ''), + 'pageIndex' => !empty($data['pageIndex']) ? $data['pageIndex'] : $this->request->param('pageIndex', 0), + 'pageSize' => !empty($data['pageSize']) ? $data['pageSize'] : $this->request->param('pageSize', 20) + ]; + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'plain'); + + // 发送请求获取设备列表 + $result = requestCurl($this->baseUrl . 'api/device/pageResult', $params, 'GET', $header); + $response = handleApiResponse($result); + + // 保存数据到数据库 + if (!empty($response['results'])) { + foreach ($response['results'] as $item) { + $this->saveDevice($item); + } + } + + if($isInner){ + return json_encode(['code'=>200,'msg'=>'success','data'=>$response]); + }else{ + return successJson($response); + } + } catch (\Exception $e) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'获取设备列表失败:' . $e->getMessage()]); + }else{ + return errorJson('获取设备列表失败:' . $e->getMessage()); + } + } + } + + /** + * 生成设备二维码 + * @param int $accountId 账号ID + * @return \think\response\Json + */ + public function addDevice($accountId = 0,$isInner = false) + { + if (empty($accountId)) { + $accountId = $this->request->param('accountId', ''); + } + + if (empty($accountId)) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'账号ID不能为空']); + }else{ + return errorJson('账号ID不能为空'); + } + } + + try { + // 获取环境配置 + $tenantGuid = Env::get('api.guid', ''); + $deviceSocketHost = Env::get('api.deviceSocketHost', ''); + + if (empty($tenantGuid) || empty($deviceSocketHost)) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'环境配置不完整,请检查api.guid和api.deviceSocketHost配置']); + }else{ + return errorJson('环境配置不完整,请检查api.guid和api.deviceSocketHost配置'); + } + } + + // 构建设备配置数据 + $data = [ + 'tenantGuid' => $tenantGuid, + 'deviceSocketHost' => $deviceSocketHost, + 'checkVersionUrl' => '', + 'accountId' => intval($accountId) + ]; + + // 将数据转换为JSON + $jsonData = json_encode($data); + + // 生成二维码图片 + $qrCode = $this->generateQrCodeImage($jsonData); + + return successJson([ + 'qrCode' => $qrCode, + 'config' => $data + ]); + } catch (\Exception $e) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'生成设备二维码失败:' . $e->getMessage()]); + }else{ + return errorJson('生成设备二维码失败:' . $e->getMessage()); + } + } + } + + /** + * 更新设备账号 + * @return \think\response\Json + */ + public function updateaccount($data = [],$isInner = false) + { + // 获取授权token + $authorization = trim($this->request->header('authorization', $this->authorization)); + if (empty($authorization)) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'缺少授权信息']); + }else{ + return errorJson('缺少授权信息'); + } + } + + try { + // 获取参数 + $id = !empty($data['id']) ? $data['id'] : $this->request->param('id', ''); + $accountId = !empty($data['accountId']) ? $data['accountId'] : $this->request->param('accountId', ''); + + if (empty($id)) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'设备ID不能为空']); + }else{ + return errorJson('设备ID不能为空'); + } + } + + if (empty($accountId)) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'账号id不能为空']); + }else{ + return errorJson('账号id不能为空'); + } + } + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'plain'); + + // 发送请求 + $result = requestCurl($this->baseUrl . 'api/device/updateaccount?accountId=' . $accountId . '&deviceId=' . $id, [], 'PUT', $header); + $response = handleApiResponse($result); + + if(empty($response)){ + return successJson([],'操作成功'); + }else{ + return errorJson([],$response); + } + } catch (\Exception $e) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'更新设备账号失败:' . $e->getMessage()]); + }else{ + return errorJson('更新设备账号失败:' . $e->getMessage()); + } + } + } + + /** + * 更新设备所属分组 + * @param int $id 设备ID + * @param int $groupId 分组ID + * @return \think\response\Json + */ + public function updateDeviceToGroup($data = []) + { + // 获取授权token + $authorization = trim($this->request->header('authorization', $this->authorization)); + if (empty($authorization)) { + return errorJson('缺少授权信息'); + } + + try { + // 获取参数 + $id = !empty($data['id']) ? $data['id'] : $this->request->param('id', ''); + $groupId = !empty($data['groupId']) ? $data['groupId'] : $this->request->param('groupId', ''); + + if (empty($id)) { + return errorJson('设备ID不能为空'); + } + + if (empty($groupId)) { + return errorJson('分组ID不能为空'); + } + + // 验证设备是否存在 + $device = DeviceModel::where('id', $id)->find(); + if (empty($device)) { + return errorJson('设备不存在'); + } + + // 验证分组是否存在 + $group = DeviceGroupModel::where('id', $groupId)->find(); + if (empty($group)) { + return errorJson('分组不存在'); + } + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'plain'); + + // 发送请求到微信接口 + $result = requestCurl($this->baseUrl . 'api/device/updateDeviceGroup?id=' . $id . '&groupId=' . $groupId, [], 'PUT', $header); + $response = handleApiResponse($result); + + if (empty($response)) { + // 更新成功,更新本地数据库 + $device->groupId = $groupId; + $device->groupName = $group->groupName; + $device->save(); + + return successJson([], '设备分组更新成功'); + } else { + return errorJson([], $response); + } + } catch (\Exception $e) { + return errorJson('更新设备分组失败:' . $e->getMessage()); + } + } + + + /** + * 删除设备 + * + * @param $deviceId + * @return false|string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function delDevice($deviceId = '') + { + $authorization = $this->authorization; + if (empty($authorization)) { + return json_encode(['code'=>500,'msg'=>'缺少授权信息']); + } + + if (empty($deviceId)) { + return json_encode(['code'=>500,'msg'=>'删除的设备不能为空']); + } + + $device = Db::table('s2_device')->where('id', $deviceId)->find(); + if (empty($device)) { + return json_encode(['code'=>500,'msg'=>'设备不存在']); + } + + try { + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + // 发送请求 + $result = requestCurl($this->baseUrl . 'api/device/del/'.$deviceId, [], 'DELETE', $header,'json'); + if (empty($result)) { + Db::table('s2_device')->where('id', $deviceId)->update([ + 'isDeleted' => 1, + 'deleteTime' => time() + ]); + return json_encode(['code'=>200,'msg'=>'删除成功']); + }else{ + return json_encode(['code'=>200,'msg'=>'删除失败']); + } + } catch (\Exception $e) { + return json_encode(['code'=>500,'msg'=>'获取设备分组列表失败:' . $e->getMessage()]); + } + } + + /** + * 更新设备联系人 + * @param int $id 设备ID + * @param int $groupId 分组ID + * @return \think\response\Json + */ + public function importContact($data = [],$isInner = false) + { + // 获取授权token + $authorization = $this->authorization; + if (empty($authorization)) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'缺少授权信息']); + }else{ + return errorJson('缺少授权信息'); + } + } + + try { + // 获取参数 + $deviceId = !empty($data['deviceId']) ? $data['deviceId'] : $this->request->param('deviceId', ''); + $rawContactJson = !empty($data['contactJson']) ? $data['contactJson'] : $this->request->param('contactJson', ''); + $clearContact = !empty($data['clearContact']) ? $data['clearContact'] : $this->request->param('clearContact', false); + + + if (empty($deviceId)) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'设备ID不能为空']); + }else{ + return errorJson('设备ID不能为空'); + } + } + + $contacts = []; + if (!empty($rawContactJson)) { + if (is_string($rawContactJson)) { + $decodedContacts = json_decode($rawContactJson, true); + if (json_last_error() === JSON_ERROR_NONE) { + // It's a valid JSON string + $contacts = $decodedContacts; + } else { + // It's not a JSON string, treat as multi-line text + $lines = explode("\n", str_replace("\r\n", "\n", $rawContactJson)); + foreach ($lines as $line) { + $line = trim($line); + if (empty($line)) continue; + $parts = explode(',', $line); + if (count($parts) == 2) { + $contacts[] = ['name' => trim($parts[0]), 'phone' => trim($parts[1])]; + } + } + } + } elseif (is_array($rawContactJson)) { + // It's already an array + $contacts = $rawContactJson; + } + } + + + if (empty($contacts)){ + if($isInner){ + return json_encode(['code'=>500,'msg'=>'更新设备联系人失败:通讯录不能为空' ]); + }else{ + return errorJson('更新设备联系人失败:通讯录不能为空' ); + } + } + + + // Trim whitespace from name and phone in all cases + if (!empty($contacts)) { + foreach ($contacts as &$contact) { + if (isset($contact['name'])) { + $contact['name'] = trim($contact['name']); + } + if (isset($contact['phone'])) { + $contact['phone'] = trim($contact['phone']); + } + } + unset($contact); // Unset reference to the last element + } + + $contactJsonForApi = json_encode($contacts); + + // 构建请求参数 + $params = [ + 'deviceId' => $deviceId, + 'contactJson' => $contactJsonForApi, + 'clearContact' => $clearContact + ]; + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求 + $result = requestCurl($this->baseUrl . 'api/device/importContact', $params, 'POST', $header,'json'); + $response = handleApiResponse($result); + + if(empty($response)){ + if($isInner){ + return json_encode(['code'=>200,'msg'=>'更新设备联系人成功' ]); + }else{ + return successJson([],'更新设备联系人失败:通讯录不能为空' ); + } + }else{ + if($isInner){ + return json_encode(['code'=>200,'msg'=> $response ]); + }else{ + return successJson([],$response ); + } + } + } catch (\Exception $e) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'更新设备联系人失败:' . $e->getMessage()]); + }else{ + return errorJson('更新设备联系人失败:' . $e->getMessage()); + } + } + } + + + /************************ 设备分组相关接口 ************************/ + + /** + * 获取设备分组列表 + * @return \think\response\Json + */ + public function getGroupList($data = [],$isInner = false) + { + // 获取授权token + $authorization = trim($this->request->header('authorization', $this->authorization)); + if (empty($authorization)) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'缺少授权信息']); + }else{ + return errorJson('缺少授权信息'); + } + } + + try { + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求 + $result = requestCurl($this->baseUrl . 'api/DeviceGroup/list', [], 'GET', $header,'json'); + $response = handleApiResponse($result); + // 保存数据到数据库 + if (!empty($response)) { + foreach ($response as $item) { + $this->saveDeviceGroup($item); + } + } + if($isInner){ + return json_encode(['code'=>200,'msg'=>'success','data'=>$response]); + }else{ + return successJson($response); + } + } catch (\Exception $e) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'获取设备分组列表失败:' . $e->getMessage()]); + }else{ + return errorJson('获取设备分组列表失败:' . $e->getMessage()); + } + } + } + + /** + * 创建设备分组 + * @return \think\response\Json + */ + public function createGroup($data = [],$isInner = false) + { + // 获取授权token + $authorization = trim($this->request->header('authorization', $this->authorization)); + if (empty($authorization)) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'缺少授权信息']); + }else{ + return errorJson('缺少授权信息'); + } + } + + try { + // 获取参数 + $groupName = !empty($data['groupName']) ? $data['groupName'] : $this->request->param('groupName', ''); + $groupMemo = !empty($data['groupMemo']) ? $data['groupMemo'] : $this->request->param('groupMemo', ''); + + if (empty($groupName)) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'分组名称不能为空']); + }else{ + return errorJson('分组名称不能为空'); + } + } + + // 构建请求参数 + $params = [ + 'groupName' => $groupName, + 'groupMemo' => $groupMemo + ]; + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求 + $result = requestCurl($this->baseUrl . 'api/DeviceGroup/new', $params, 'POST', $header,'json'); + if(empty($result)){ + // $res = $this->getGroupList([],true); + // $res = json_decode($res,true); + // if(!empty($res['data'])){ + // $data = $res['data'][0]; + // } + + $data = []; + + if($isInner){ + return json_encode(['code'=>200,'msg'=>'success','data'=>$data]); + }else{ + return successJson($data,'操作成功'); + } + }else{ + if($isInner){ + return json_encode(['code'=>500,'msg'=> $result]); + }else{ + return errorJson($result); + } + } + } catch (\Exception $e) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'创建设备分组失败:' . $e->getMessage()]); + }else{ + return errorJson('创建设备分组失败:' . $e->getMessage()); + } + } + } + + /** + * 更新设备分组 + * @return \think\response\Json + */ + public function updateDeviceGroup($data = [],$isInner = false) + { + // 获取授权token + $authorization = trim($this->request->header('authorization', $this->authorization)); + if (empty($authorization)) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'缺少授权信息']); + }else{ + return errorJson('缺少授权信息'); + } + } + + try { + // 获取参数 + $id = !empty($data['id']) ? $data['id'] : $this->request->param('id', ''); + $groupName = !empty($data['groupName']) ? $data['groupName'] : $this->request->param('groupName', ''); + $groupMemo = !empty($data['groupMemo']) ? $data['groupMemo'] : $this->request->param('groupMemo', ''); + + if (empty($id)) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'分组ID不能为空']); + }else{ + return errorJson('分组ID不能为空'); + } + } + + if (empty($groupName)) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'分组名称不能为空']); + }else{ + return errorJson('分组名称不能为空'); + } + } + + $group = DeviceGroupModel::where('id', $id)->find(); + if(empty($group)){ + if($isInner){ + return json_encode(['code'=>500,'msg'=>'分组不存在']); + }else{ + return errorJson('分组不存在'); + } + } + + $isGroupName = DeviceGroupModel::where('groupName', $groupName)->find(); + if(!empty($isGroupName)){ + if($isInner){ + return json_encode(['code'=>500,'msg'=>'分组名称已存在']); + }else{ + return errorJson('分组名称已存在'); + } + } + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 从数据库获取对象后,创建一个正确格式的数组用于API请求 + $requestData = [ + 'id' => $group->id, + 'tenantId' => $group->tenantId, + 'groupName' => $groupName, + 'groupMemo' => $groupMemo + ]; + + // 发送请求 + $result = requestCurl($this->baseUrl . 'api/DeviceGroup/update', $requestData, 'PUT', $header, 'json'); + if(empty($result)){ + $group->groupName = $groupName; + $group->groupMemo = $groupMemo; + $group->save(); + if($isInner){ + return json_encode(['code'=>200,'msg'=>'success','data'=>$group]); + }else{ + return successJson($group,'操作成功'); + } + }else{ + if($isInner){ + return json_encode(['code'=>500,'msg'=> $result]); + }else{ + return errorJson($result); + } + } + } catch (\Exception $e) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'更新设备分组失败:' . $e->getMessage()]); + }else{ + return errorJson('更新设备分组失败:' . $e->getMessage()); + } + } + } + + /************************ 私有辅助方法 ************************/ + + /** + * 保存设备数据到数据库 + * @param array $item 设备数据 + */ + private function saveDevice($item) + { + $data = [ + 'id' => isset($item['id']) ? $item['id'] : '', + 'userName' => isset($item['userName']) ? $item['userName'] : '', + 'nickname' => isset($item['nickname']) ? $item['nickname'] : '', + 'realName' => isset($item['realName']) ? $item['realName'] : '', + 'groupName' => isset($item['groupName']) ? $item['groupName'] : '', + 'wechatAccounts' => isset($item['wechatAccounts']) ? json_encode($item['wechatAccounts']) : json_encode([]), + 'alive' => isset($item['alive']) ? $item['alive'] : false, + 'lastAliveTime' => isset($item['lastAliveTime']) ? $item['lastAliveTime'] : null, + 'tenantId' => isset($item['tenantId']) ? $item['tenantId'] : 0, + 'groupId' => isset($item['groupId']) ? $item['groupId'] : 0, + 'currentAccountId' => isset($item['currentAccountId']) ? $item['currentAccountId'] : 0, + 'imei' => $item['imei'], + 'memo' => isset($item['memo']) ? $item['memo'] : '', + 'createTime' => isset($item['createTime']) ? strtotime($item['createTime']) : 0, + 'isDeleted' => isset($item['isDeleted']) ? $item['isDeleted'] : false, + 'deletedAndStop' => isset($item['deletedAndStop']) ? $item['deletedAndStop'] : false, + 'deleteTime' => empty($item['isDeleted']) ? 0 : strtotime($item['deleteTime']), + 'rooted' => isset($item['rooted']) ? $item['rooted'] : false, + 'xPosed' => isset($item['xPosed']) ? $item['xPosed'] : false, + 'brand' => isset($item['brand']) ? $item['brand'] : '', + 'model' => isset($item['model']) ? $item['model'] : '', + 'operatingSystem' => isset($item['operatingSystem']) ? $item['operatingSystem'] : '', + 'softwareVersion' => isset($item['softwareVersion']) ? $item['softwareVersion'] : '', + 'extra' => isset($item['extra']) ? json_encode($item['extra']) : json_encode([]), + 'phone' => isset($item['phone']) ? $item['phone'] : '', + 'lastUpdateTime' => isset($item['lastUpdateTime']) ? ($item['lastUpdateTime'] == '0001-01-01T00:00:00' ? 0 : strtotime($item['lastUpdateTime'])) : 0 + ]; + + if (!empty($data['alive'])){ + $data['aliveTime'] = time(); + } + + + // 使用imei作为唯一性判断 + $device = DeviceModel::where('id', $item['id'])->find(); + + if ($device) { + $device->save($data); + } else { + + // autoLike:自动点赞 + // momentsSync:朋友圈同步 + // autoCustomerDev:自动开发客户 + // groupMessageDeliver:群消息推送 + // autoGroup:自动建群 + + $data['taskConfig'] = json_encode([ + 'autoLike' => true, + 'momentsSync' => true, + 'autoCustomerDev' => true, + 'groupMessageDeliver' => true, + 'autoGroup' => true, + ]); + DeviceModel::create($data); + } + } + + /** + * 保存设备分组数据到数据库 + * @param array $item 设备分组数据 + */ + private function saveDeviceGroup($item) + { + $data = [ + 'id' => $item['id'], + 'tenantId' => $item['tenantId'], + 'groupName' => $item['groupName'], + 'groupMemo' => $item['groupMemo'], + 'count' => isset($item['count']) ? $item['count'] : 0, + 'createTime' => $item['createTime'] == '0001-01-01T00:00:00' ? 0 : strtotime($item['createTime']) + ]; + + // 使用ID作为唯一性判断 + $group = DeviceGroupModel::where('id', $item['id'])->find(); + + if ($group) { + $group->save($data); + } else { + DeviceGroupModel::create($data); + } + } + + /** + * 生成二维码图片(base64格式) + * @param string $data 二维码数据 + * @return string base64编码的图片 + */ + private function generateQrCodeImage($data) + { + // 使用endroid/qr-code 2.5版本生成二维码 + $qrCode = new QrCode($data); + $qrCode->setSize(300); + $qrCode->setMargin(10); + $qrCode->setWriterByName('png'); + $qrCode->setEncoding('UTF-8'); + + // 使用枚举常量而不是字符串 + $qrCode->setErrorCorrectionLevel(ErrorCorrectionLevel::HIGH); + + // 直接获取base64内容 + $base64 = 'data:image/png;base64,' . base64_encode($qrCode->writeString()); + + return $base64; + } +} \ No newline at end of file diff --git a/application/api/controller/FriendTaskController.php b/application/api/controller/FriendTaskController.php new file mode 100644 index 0000000..893a810 --- /dev/null +++ b/application/api/controller/FriendTaskController.php @@ -0,0 +1,192 @@ +authorization; + if (empty($authorization)) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'缺少授权信息']); + }else{ + return errorJson('缺少授权信息'); + } + } + + try { + // 构建请求参数 + $params = [ + 'keyword' => $this->request->param('keyword', ''), + 'status' => $this->request->param('status', ''), + 'pageIndex' => !empty($pageIndex) ? $pageIndex : $this->request->param('pageIndex', 0), + 'pageSize' => !empty($pageSize) ? $pageSize : $this->request->param('pageSize', 20), + ]; + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求获取添加好友记录列表 + $result = requestCurl($this->baseUrl . 'api/AddFriendByPhoneTask/list', $params, 'GET', $header,'json'); + $response = handleApiResponse($result); + + + // 保存数据到数据库 + if (!empty($response['results'])) { + foreach ($response['results'] as $item) { + $this->saveFriendTask($item); + } + } + if($isInner){ + return json_encode(['code'=>200,'msg'=>'获取添加好友记录列表成功','data'=>$response]); + }else{ + return successJson($response); + } + } catch (\Exception $e) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'获取添加好友记录列表失败:' . $e->getMessage()]); + }else{ + return errorJson('获取添加好友记录列表失败:' . $e->getMessage()); + } + } + } + + /** + * 添加好友任务 + * @return \think\response\Json + */ + public function addFriendTask($data = []) + { + // 获取授权token + $authorization =$this->authorization; + if (empty($authorization)) { + return json_encode(['code'=>500,'msg'=>'缺少授权信息']); + } + + try { + // 获取请求参数 + $phone = !empty($data['phone']) ? $data['phone'] : ''; + $message = !empty($data['message']) ? $data['message'] : ''; + $remark = !empty($data['remark']) ? $data['remark'] : ''; + $labels = !empty($data['labels']) ? $data['labels'] : ''; + $wechatAccountId = !empty($data['wechatAccountId']) ? $data['wechatAccountId'] : ''; + + // 参数验证 + if (empty($phone)) { + return json_encode(['code'=>500,'msg'=>'手机号不能为空']); + } + + if (empty($wechatAccountId)) { + return json_encode(['code'=>500,'msg'=>'微信号不能为空']); + } + + // 构建请求参数 + $params = [ + 'phone' => $phone, + 'message' => $message, + 'remark' => $remark, + 'labels' => is_array($labels) ? $labels : [$labels], + 'wechatAccountId' => (int)$wechatAccountId + ]; + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求添加好友任务 + $result = requestCurl($this->baseUrl . 'api/AddFriendByPhoneTask/add', $params, 'POST', $header, 'json'); + + // 处理响应 + return json_encode(['code'=>200,'msg'=>'添加好友任务创建成功']); + } catch (\Exception $e) { + return json_encode(['code'=>500,'msg'=> '添加好友任务失败:' . $e->getMessage()]); + } + } + + /************************ 私有辅助方法 ************************/ + + /** + * 保存添加好友记录到数据库 + * @param array $item 添加好友记录数据 + */ + private function saveFriendTask($item) + { + // 将日期时间字符串转换为时间戳 + $createTime = isset($item['createTime']) ? strtotime($item['createTime']) : null; + + $data = [ + 'id' => $item['id'], + 'tenantId' => $item['tenantId'], + 'operatorAccountId' => $item['operatorAccountId'], + 'status' => $item['status'], + 'phone' => $item['phone'], + 'msgContent' => $item['msgContent'], + 'wechatAccountId' => $item['wechatAccountId'], + 'createTime' => $createTime, + 'remark' => $item['remark'], + 'extra' => $item['extra'], + 'labels' => $item['labels'], + 'from' => $item['from'], + 'alias' => $item['alias'], + 'wechatId' => $item['wechatId'], + 'wechatAvatar' => $item['wechatAvatar'], + 'wechatNickname' => $item['wechatNickname'], + 'accountNickname' => $item['accountNickname'], + 'accountRealName' => $item['accountRealName'], + 'accountUsername' => $item['accountUsername'] + ]; + + // 使用taskId作为唯一性判断 + $task = FriendTaskModel::where('id', $item['id'])->find(); + if ($task) { + $task->save($data); + } else { + FriendTaskModel::create($data); + } + + //创建非法记录 + if ($item['status'] == 2){ + $data = [ + 'level' => 2, + 'taskId' => $item['id'], + 'reason' => '', + 'memo' => '', + 'wechatId' => $item['wechatId'], + 'companyId' => '', + 'restrictTime' => time(), + 'recoveryTime' => time() + 3600 * 72, + ]; + if (strpos('操作过于频繁', $item['extra']) !== false){ + $data['reason'] = '频繁添加好友'; + $data['memo'] = '操作过于频繁,请稍后再试'; + } + + if (strpos('当前账号存在安全风险', $item['extra']) !== false){ + $data['reason'] = '账号风险'; + $data['memo'] = '当前账号存在安全风险,需先到「微信团队」进行安全验证后才能继续使用当前功能'; + } + $res = WechatRestricts::where('taskId', $item['id'])->find(); + if (empty($res)) { + WechatRestricts::create($data); + } + } + + + } +} \ No newline at end of file diff --git a/application/api/controller/MessageController.php b/application/api/controller/MessageController.php new file mode 100644 index 0000000..daedae1 --- /dev/null +++ b/application/api/controller/MessageController.php @@ -0,0 +1,584 @@ +request->header('authorization', $this->authorization)); + if (empty($authorization)) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'缺少授权信息']); + }else{ + return errorJson('缺少授权信息'); + } + } + + $fromTime = $this->request->param('fromTime', date('Y-m-d 00:00:00', strtotime('-1 days'))); + $toTime = $this->request->param('toTime', date('Y-m-d 23:59:59')); + + + try { + // 构建请求参数 + $params = [ + 'chatroomKeyword' => $this->request->param('chatroomKeyword', ''), + 'friendKeyword' => $this->request->param('friendKeyword', ''), + 'friendPhoneKeyword' => $this->request->param('friendPhoneKeyword', ''), + 'friendPinYinKeyword' => $this->request->param('friendPinYinKeyword', ''), + 'friendRegionKeyword' => $this->request->param('friendRegionKeyword', ''), + 'friendRemarkKeyword' => $this->request->param('friendRemarkKeyword', ''), + 'groupId' => $this->request->param('groupId', null), + 'kefuId' => $this->request->param('kefuId', null), + 'labels' => $this->request->param('labels', []), + 'msgFrom' => $fromTime, + 'msgKeyword' => $this->request->param('msgKeyword', ''), + 'msgTo' => $toTime, + 'msgType' => $this->request->param('msgType', ''), + 'pageIndex' => !empty($pageIndex) ? $pageIndex : input('pageIndex', 0), + 'pageSize' => !empty($pageSize) ? $pageSize : input('pageSize', 20), + 'reverse' => $this->request->param('reverse', false), + 'type' => $this->request->param('type', 'friend'), + 'wechatAccountIds' => $this->request->param('wechatAccountIds', []) + ]; + + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求获取好友列表 + $result = requestCurl($this->baseUrl . 'api/WechatFriend/listWechatFriendForMsgPagination', $params, 'POST', $header, 'json'); + $response = handleApiResponse($result); + // 获取同步消息标志 + $syncMessages = $this->request->param('syncMessages', true); + // 如果需要同步消息,则获取每个好友的消息 + if ($syncMessages && !empty($response['results'])) { + $from = strtotime($fromTime) * 1000; + $to = strtotime($toTime) * 1000; + + + foreach ($response['results'] as &$friend) { + // 构建获取消息的参数 + $messageParams = [ + 'keyword' => '', + 'msgType' => '', + 'accountId' => '', + 'count' => 20, + 'messageId' => '', + 'olderData' => true, + 'wechatAccountId' => $friend['wechatAccountId'], + 'wechatFriendId' => $friend['wechatFriendId'], + 'from' => $from, + 'to' => $to, + 'searchFrom' => 'admin' + ]; + + // 调用获取消息的接口 + $messageResult = requestCurl($this->baseUrl . 'api/FriendMessage/searchMessage', $messageParams, 'GET', $header, 'json'); + $messageResponse = handleApiResponse($messageResult); + // 保存消息到数据库 + if (!empty($messageResponse)) { + foreach ($messageResponse as $item) { + $this->saveMessage($item); + } + } + + // 将消息列表添加到好友数据中 + $friend['messages'] = $messageResponse ?? []; + } + unset($friend); + } + if($isInner){ + return json_encode(['code'=>200,'msg'=>'获取好友列表成功','data'=>$response]); + }else{ + return successJson($response); + } + } catch (\Exception $e) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'获取好友列表失败:' . $e->getMessage()]); + }else{ + return errorJson('获取好友列表失败:' . $e->getMessage()); + } + } + } + + /** + * 用户聊天记录 + * @return \think\response\Json + */ + public function getMessageList() + { + // 获取授权token + $authorization = trim($this->request->header('authorization', $this->authorization)); + if (empty($authorization)) { + return errorJson('缺少授权信息'); + } + + try { + // 构建请求参数 + $params = [ + 'keyword' => $this->request->param('keyword', ''), + 'msgType' => $this->request->param('msgType', ''), + 'accountId' => $this->request->param('accountId', ''), + 'count' => $this->request->param('count', 100), + 'messageId' => $this->request->param('messageId', ''), + 'olderData' => $this->request->param('olderData', true), + 'wechatAccountId' => $this->request->param('wechatAccountId', ''), + 'wechatFriendId' => $this->request->param('wechatFriendId', ''), + 'from' => $this->request->param('from', ''), + 'to' => $this->request->param('to', ''), + 'searchFrom' => $this->request->param('searchFrom', 'admin') + ]; + + // 参数验证 + if (empty($params['wechatAccountId'])) { + return errorJson('微信账号ID不能为空'); + } + if (empty($params['wechatFriendId'])) { + return errorJson('好友ID不能为空'); + } + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求获取聊天记录 + $result = requestCurl($this->baseUrl . 'api/FriendMessage/searchMessage', $params, 'GET', $header, 'json'); + $response = handleApiResponse($result); + + // 保存数据到数据库 + if (!empty($response)) { + foreach ($response as $item) { + $this->saveMessage($item); + } + } + + return successJson($response); + } catch (\Exception $e) { + return errorJson('获取聊天记录失败:' . $e->getMessage()); + } + } + + /************************ 群聊消息相关接口 ************************/ + + /** + * 获取微信群聊列表 + * @return \think\response\Json + */ + public function getChatroomList($pageIndex = '',$pageSize = '',$isInner = false) + { + // 获取授权token + $authorization = trim($this->request->header('authorization', $this->authorization)); + if (empty($authorization)) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'缺少授权信息']); + }else{ + return errorJson('缺少授权信息'); + } + } + + $fromTime = $this->request->param('fromTime', date('Y-m-d 00:00:00', strtotime('-1 days'))); + $toTime = $this->request->param('toTime', date('Y-m-d 23:59:59')); + + + try { + // 构建请求参数 + $params = [ + 'chatroomKeyword' => $this->request->param('chatroomKeyword', ''), + 'friendKeyword' => $this->request->param('friendKeyword', ''), + 'friendInKeyword' => $this->request->param('friendInKeyword', ''), + 'friendInTimeKeyword' => $this->request->param('friendInTimeKeyword', ''), + 'friendOutKeyword' => $this->request->param('friendOutKeyword', ''), + 'friendRemarkKeyword' => $this->request->param('friendRemarkKeyword', ''), + 'groupId' => $this->request->param('groupId', null), + 'kefuId' => $this->request->param('kefuId', null), + 'labels' => $this->request->param('labels', []), + 'msgFrom' => $fromTime, + 'msgKeyword' => $this->request->param('msgKeyword', ''), + 'msgTo' => $toTime, + 'msgType' => $this->request->param('msgType', ''), + 'pageIndex' => $this->request->param('pageIndex', 0), + 'pageSize' => $this->request->param('pageSize', 100), + 'reverse' => $this->request->param('reverse', false), + 'type' => $this->request->param('type', 'chatroom'), + 'wechatAccountIds' => $this->request->param('wechatAccountIds', []) + ]; + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求获取群聊列表 + $result = requestCurl($this->baseUrl . 'api/WechatChatroom/listWechatChatroomForMsgPagination', $params, 'POST', $header, 'json'); + $response = handleApiResponse($result); + + // 获取同步消息标志 + $syncMessages = $this->request->param('syncMessages', true); + + // 如果需要同步消息,则获取每个群的消息 + if ($syncMessages && !empty($response)) { + $from = strtotime($fromTime) * 1000; + $to = strtotime($toTime) * 1000; + foreach ($response['results'] as &$chatroom) { + + // 构建获取消息的参数 + $messageParams = [ + 'keyword' => '', + 'msgType' =>'', + 'accountId' => '', + 'count' => 20, + 'messageId' => '', + 'olderData' => true, + 'wechatId' => '', + 'wechatAccountId' => $chatroom['wechatAccountId'], + 'wechatChatroomId' => $chatroom['wechatChatroomId'], + 'from' => $from, + 'to' => $to, + 'searchFrom' => 'admin' + ]; + + // 调用获取消息的接口 + $messageResult = requestCurl($this->baseUrl . 'api/ChatroomMessage/searchMessage', $messageParams, 'GET', $header, 'json'); + $messageResponse = handleApiResponse($messageResult); + + // 保存消息到数据库 + if (!empty($messageResponse)) { + foreach ($messageResponse as $item) { + $this->saveChatroomMessage($item); + } + } + + // 将消息列表添加到群聊数据中 + $chatroom['messages'] = $messageResponse ?? []; + } + unset($chatroom); + } + if($isInner){ + return json_encode(['code'=>200,'msg'=>'获取群聊列表成功','data'=>$response]); + }else{ + return successJson($response); + } + } catch (\Exception $e) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'获取群聊列表失败:' . $e->getMessage()]); + }else{ + return errorJson('获取群聊列表失败:' . $e->getMessage()); + } + } + } + + /** + * 获取群聊消息列表 + * @return \think\response\Json + */ + public function getChatroomMessages() + { + // 获取授权token + $authorization = trim($this->request->header('authorization', $this->authorization)); + if (empty($authorization)) { + return errorJson('缺少授权信息'); + } + + try { + // 构建请求参数 + $params = [ + 'keyword' => $this->request->param('keyword', ''), + 'msgType' => $this->request->param('msgType', ''), + 'accountId' => $this->request->param('accountId', ''), + 'count' => $this->request->param('count', 100), + 'messageId' => $this->request->param('messageId', ''), + 'olderData' => $this->request->param('olderData', true), + 'wechatId' => $this->request->param('wechatId', ''), + 'wechatAccountId' => $this->request->param('wechatAccountId', ''), + 'wechatChatroomId' => $this->request->param('wechatChatroomId', ''), + 'from' => $this->request->param('from', strtotime(date('Y-m-d 00:00:00', strtotime('-1 days')))), + 'to' => $this->request->param('to', strtotime(date('Y-m-d 00:00:00'))), + 'searchFrom' => $this->request->param('searchFrom', 'admin') + ]; + + // 参数验证 + if (empty($params['wechatAccountId'])) { + return errorJson('微信账号ID不能为空'); + } + if (empty($params['wechatChatroomId'])) { + return errorJson('群聊ID不能为空'); + } + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求获取群聊消息 + $result = requestCurl($this->baseUrl . 'api/ChatroomMessage/searchMessage', $params, 'GET', $header, 'json'); + $response = handleApiResponse($result); + + // 保存数据到数据库 + if (!empty($response)) { + foreach ($response as $item) { + $res = $this->saveChatroomMessage($item); + if(!$res){ + return errorJson('保存群聊消息失败'); + } + } + } + + return successJson($response); + } catch (\Exception $e) { + return errorJson('获取群聊消息失败:' . $e->getMessage()); + } + } + + /************************ 私有辅助方法 ************************/ + + /** + * 保存消息记录到数据库 + * @param array $item 消息记录数据 + */ + public function saveMessage($item) + { + // 检查消息是否已存在 + $exists = WechatMessageModel::where('id', $item['id']) ->find(); + + if (!empty($exists) && $exists['sendStatus'] == 0){ + return true; + } + + + // 将毫秒时间戳转换为秒级时间戳 + $createTime = isset($item['createTime']) ? strtotime($item['createTime']) : null; + $deleteTime = !empty($item['isDeleted']) ? strtotime($item['deleteTime']) : null; + $wechatTime = isset($item['wechatTime']) ? floor($item['wechatTime'] / 1000) : null; + + $data = [ + 'id' => $item['id'], + 'type' => 1, + 'accountId' => $item['accountId'], + 'content' => $item['content'], + 'createTime' => $createTime, + 'deleteTime' => $deleteTime, + 'isDeleted' => $item['isDeleted'] ?? false, + 'isSend' => $item['isSend'] ?? true, + 'msgId' => $item['msgId'], + 'msgSubType' => $item['msgSubType'] ?? 0, + 'msgSvrId' => $item['msgSvrId'] ?? '', + 'msgType' => $item['msgType'], + 'origin' => $item['origin'] ?? 0, + 'recallId' => $item['recallId'] ?? false, + 'sendStatus' => $item['sendStatus'] ?? 0, + 'synergyAccountId' => $item['synergyAccountId'] ?? 0, + 'tenantId' => $item['tenantId'], + 'wechatAccountId' => $item['wechatAccountId'], + 'wechatFriendId' => $item['wechatFriendId'], + 'wechatTime' => $wechatTime + ]; + + + //已被删除 + if ($item['msgType'] == 10000 && strpos($item['content'],'开启了朋友验证') !== false) { + Db::table('s2_wechat_friend')->where('id',$item['wechatFriendId'])->update(['isDeleted'=> 1,'deleteTime' => $wechatTime]); + }else{ + //优先分配在线客服 + $friend = Db::table('s2_wechat_friend')->where('id',$item['wechatFriendId'])->find(); + if (!empty($friend)){ + $accountId = $item['accountId']; + $accountData = Db::table('s2_company_account')->where('id',$accountId)->find(); + if (!empty($accountData)){ + $account = new AccountController(); + $account->getlist(['pageIndex' => 0,'pageSize' => 100,'departmentId' => $accountData['departmentId']]); + $accountIds = Db::table('s2_company_account')->where(['departmentId' => $accountData['departmentId'],'alive' => 1])->column('id'); + if (!empty($accountIds)){ + if (!in_array($friend['accountId'],$accountIds)){ + // 执行切换好友命令 + $randomKey = array_rand($accountIds, 1); + $toAccountId = $accountIds[$randomKey]; + $toAccountData = Db::table('s2_company_account')->where('id',$toAccountId)->find(); + $automaticAssign = new AutomaticAssign(); + $automaticAssign->allotWechatFriend([ + 'wechatFriendId' => $friend['id'], + 'toAccountId' => $toAccountId + ], true); + Db::table('s2_wechat_friend') + ->where('id',$friend['id']) + ->update([ + 'accountId' => $toAccountId, + 'accountUserName' => $toAccountData['userName'], + 'accountRealName' => $toAccountData['realName'], + 'accountNickname' => $toAccountData['nickname'], + ]); + } + } + + } + } + } + + $id = ''; + if (empty($exists)){ + // 创建新记录 + $res = WechatMessageModel::create($data); + $id= $res['id']; + }else{ + $id = $data['id']; + unset($data['id']); + $res = $exists->save($data); + } + + + + // 1 文字 3图片 47动态图片 34语言 43视频 42名片 40/20链接 49文件 + if (!empty($res) && empty($item['isSend']) && in_array($item['msgType'],[1,3,20,34,40,42,43,47,49])){ + $friend = Db::name('wechat_friendship')->where('id',$item['wechatFriendId'])->find(); + if (!empty($friend)){ + $trafficPoolId = Db::name('traffic_pool')->where('identifier',$friend['wechatId'])->value('id'); + if (!empty($trafficPoolId)){ + $data = [ + 'type' => 4, + 'companyId' => $friend['companyId'], + 'trafficPoolId' => $trafficPoolId, + 'source' => 0, + 'uniqueId' => $id, + 'sourceData' => json_encode([]), + 'remark' => '用户发送了消息', + 'createTime' => time(), + 'updateTime' => time() + ]; + Db::name('user_portrait')->insert($data); + + } + } + } + return true; + } + + /** + * 保存群聊消息记录到数据库 + * @param array $item 消息记录数据 + * @return bool 是否保存成功 + */ + public function saveChatroomMessage($item) + { + // 检查消息是否已存在 + $exists = WechatMessageModel::where('id', $item['id'])->find(); + + if (!empty($exists) && $exists['sendStatus'] == 0){ + return true; + } + + // 处理发送者信息 + $sender = $item['sender'] ?? []; + + // 处理消息内容,提取发送者ID和消息内容 + $originalContent = $item['content'] ?? ''; + $processedResult = $this->processMessageContent($originalContent); + $senderId = $processedResult['senderId']; + $processedContent = $processedResult['content']; + + // 将毫秒时间戳转换为秒级时间戳 + $createTime = isset($item['createTime']) ? strtotime($item['createTime']) : null; + $deleteTime = !empty($item['isDeleted']) ? strtotime($item['deleteTime']) : null; + $wechatTime = isset($item['wechatTime']) ? floor($item['wechatTime'] / 1000) : null; + + $data = [ + 'id' => $item['id'], + 'type' => 2, + 'wechatChatroomId' => $item['wechatChatroomId'], + // sender信息,添加sender前缀 + 'senderNickname' => $sender['nickname'] ?? '', + 'senderWechatId' => $sender['wechatId'] ?? $senderId, // 使用提取的发送者ID作为备选 + 'senderIsAdmin' => $sender['isAdmin'] ?? false, + 'senderIsDeleted' => $sender['isDeleted'] ?? false, + 'senderChatroomNickname' => $sender['chatroomNickname'] ?? '', + 'senderWechatAccountId' => $sender['wechatAccountId'] ?? '', + // 其他字段 + 'wechatAccountId' => $item['wechatAccountId'], + 'tenantId' => $item['tenantId'], + 'accountId' => $item['accountId'], + 'synergyAccountId' => $item['synergyAccountId'] ?? 0, + 'content' => $processedContent, // 使用处理后的内容 + 'originalContent' => $originalContent, // 保存原始内容 + 'msgType' => $item['msgType'], + 'msgSubType' => $item['msgSubType'] ?? 0, + 'msgSvrId' => $item['msgSvrId'] ?? '', + 'isSend' => $item['isSend'] ?? true, + 'createTime' => $createTime, + 'isDeleted' => $item['isDeleted'] ?? false, + 'deleteTime' => $deleteTime, + 'sendStatus' => $item['sendStatus'] ?? 0, + 'wechatTime' => $wechatTime, + 'origin' => $item['origin'] ?? 0, + 'msgId' => $item['msgId'], + 'recallId' => $item['recallId'] ?? false + ]; + + // 创建新记录 + try { + if(empty($exists)){ + WechatMessageModel::create($data); + }else{ + unset($data['id']); + $exists->save($data); + } + return true; + } catch (\Exception $e) { + return false; + } + } + + /** + * 处理消息内容,提取发送者ID和消息内容 + * @param string $content 原始消息内容 + * @return array 包含senderId和content的数组 + */ + private function processMessageContent($content) + { + if (empty($content)) { + return [ + 'senderId' => '', + 'content' => '' + ]; + } + + // 处理消息格式:wxid_vr2qafb1vg0d22:\n安德玛儿童 + if (preg_match('/^([^:]+):\n(.+)$/s', $content, $matches)) { + $senderId = trim($matches[1]); + $messageContent = trim($matches[2]); + + // 检查消息内容是否为JSON格式 + if (substr($messageContent, 0, 1) === '{' && substr($messageContent, -1) === '}') { + try { + // 尝试解析JSON + $jsonData = json_decode($messageContent, true); + if (json_last_error() == JSON_ERROR_NONE && isset($jsonData['text'])) { + // 如果是合法的JSON且包含text字段,则提取text字段作为内容 + $messageContent = $jsonData['text']; + } + } catch (\Exception $e) { + // JSON解析出错,保持原内容不变 + } + } + + return [ + 'senderId' => $senderId, + 'content' => $messageContent + ]; + } + + // 如果没有匹配到格式,则返回原始内容 + return [ + 'senderId' => '', + 'content' => $content + ]; + } +} \ No newline at end of file diff --git a/application/api/controller/MomentsController.php b/application/api/controller/MomentsController.php new file mode 100644 index 0000000..812ec4b --- /dev/null +++ b/application/api/controller/MomentsController.php @@ -0,0 +1,153 @@ +authorization; + if (empty($authorization)) { + return json_encode(['msg' => '缺少授权信息','code' => 400]); + } + + try { + // 获取请求参数 + $text = $data['text'] ?? ''; // 朋友圈文本内容 + $picUrlList = $data['picUrlList'] ?? []; // 图片URL列表 + $videoUrl = $data['videoUrl'] ?? ''; // 视频URL + $immediately = $data['immediately'] ?? true; // 是否立即发布 + $timingTime = $data['timingTime'] ?? ''; // 定时发布时间 + $beginTime = $data['beginTime'] ?? ''; // 开始时间 + $endTime = $data['endTime'] ?? ''; // 结束时间 + $isUseLocation = $data['isUseLocation'] ?? false; // 是否使用位置信息 + $poiName = $data['poiName'] ?? ''; // 位置名称 + $poiAddress = $data['poiAddress'] ?? ''; // 位置地址 + $lat = $data['lat'] ?? 0; // 纬度 + $lng = $data['lng'] ?? 0; // 经度 + $momentContentType = $data['momentContentType'] ?? 1; // 朋友圈内容类型 + $publicMode = $data['publicMode'] ?? 0; // 发布模式 + $altList = $data['altList'] ?? ''; // 替代列表 + $link = $data['link'] ?? []; // 链接信息 + $jobPublishWechatMomentsItems = $data['jobPublishWechatMomentsItems'] ?? []; // 发布账号和评论信息 + + // 必填参数验证 + if (empty($jobPublishWechatMomentsItems) || !is_array($jobPublishWechatMomentsItems)) { + return json_encode(['msg' => '至少需要选择一个发布账号','code' => 400]); + } + + // 根据朋友圈类型验证必填字段 + if ($momentContentType == 1 && empty($text)) { // 纯文本 + return json_encode(['msg' => '朋友圈内容不能为空','code' => 400]); + } else if ($momentContentType == 2 && (empty($picUrlList) || empty($text))) { // 图片+文字 + return json_encode(['msg' => '朋友圈内容和图片不能为空','code' => 400]); + } else if ($momentContentType == 3 && (empty($videoUrl) || empty($text))) { // 视频+文字 + return json_encode(['msg' => '朋友圈内容和视频不能为空','code' => 400]); + } else if ($momentContentType == 4 && (empty($link) || empty($text))) { // 链接+文字 + return json_encode(['msg' => '朋友圈内容和链接不能为空','code' => 400]); + } + + // 构建请求参数 + $params = [ + 'text' => $text, + 'picUrlList' => $picUrlList, + 'videoUrl' => $videoUrl, + 'immediately' => $immediately, + 'timingTime' => $timingTime, + 'beginTime' => $beginTime, + 'endTime' => $endTime, + 'isUseLocation' => $isUseLocation, + 'poiName' => $poiName, + 'poiAddress' => $poiAddress, + 'lat' => $lat, + 'lng' => $lng, + 'momentContentType' => (int)$momentContentType, + 'publicMode' => (int)$publicMode, + 'altList' => $altList, + 'link' => $link, + 'jobPublishWechatMomentsItems' => $jobPublishWechatMomentsItems + ]; + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求发布朋友圈 + $result = requestCurl($this->baseUrl . 'api/JobPublishWechatMoments/addJob', $params, 'POST', $header, 'json'); + // 处理响应 + if (empty($result)) { + return json_encode(['msg' => '朋友圈任务创建成功','code' => 200]); + } else { + // 如果返回的是错误信息 + return json_encode(['msg' => $result,'code' => 400]); + } + } catch (\Exception $e) { + return json_encode(['msg' => '发布朋友圈失败','code' => 400]); + } + } + + /************************ 朋友圈任务管理相关接口 ************************/ + + /** + * 获取朋友圈任务列表 + * @return \think\response\Json + */ + public function getList() + { + // 获取授权token + $authorization = trim($this->request->header('authorization', $this->authorization)); + if (empty($authorization)) { + return errorJson('缺少授权信息'); + } + + try { + // 获取请求参数 + $keyword = $this->request->param('keyword', ''); // 关键词搜索 + $jobStatus = $this->request->param('jobStatus', ''); // 任务状态筛选 + $contentType = $this->request->param('contentType', ''); // 内容类型筛选 + $only = $this->request->param('only', 'false'); // 是否只查看自己的 + $pageIndex = $this->request->param('pageIndex', 0); // 当前页码 + $pageSize = $this->request->param('pageSize', 10); // 每页数量 + $from = $this->request->param('from', ''); // 开始日期 + $to = $this->request->param('to', ''); // 结束日期 + + // 构建请求参数 + $params = [ + 'keyword' => $keyword, + 'jobStatus' => $jobStatus, + 'contentType' => $contentType, + 'only' => $only, + 'pageIndex' => (int)$pageIndex, + 'pageSize' => (int)$pageSize + ]; + + // 添加日期筛选条件(如果有) + if (!empty($from)) { + $params['from'] = $from; + } + if (!empty($to)) { + $params['to'] = $to; + } + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求获取朋友圈任务列表 + $result = requestCurl($this->baseUrl . 'api/JobPublishWechatMoments/listPagination', $params, 'GET', $header, 'json'); + $response = handleApiResponse($result); + + return successJson($response); + } catch (\Exception $e) { + return errorJson('获取朋友圈任务列表失败:' . $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/application/api/controller/StatsController.php b/application/api/controller/StatsController.php new file mode 100644 index 0000000..8ae0bb1 --- /dev/null +++ b/application/api/controller/StatsController.php @@ -0,0 +1,75 @@ +request->header('authorization', $this->authorization)); + if (empty($authorization)) { + return errorJson('缺少授权信息'); + } + + $headerData = ['client:' . self::CLIENT_TYPE]; + $header = setHeader($headerData, $authorization, 'plain'); + + try { + $result = requestCurl($this->baseUrl . '/api/DashBoard/ListHomePageStatistics', ['refresh' => 10000], 'GET', $header); + return successJson($result); + } catch (\Exception $e) { + return errorJson('获取基础数据失败:' . $e->getMessage()); + } + } + + /** + * 好友统计 + * @return \think\response\Json + */ + public function FansStatistics(){ + /* 参数说明 + lidu 数据搜索类型 0 小时 1 天 2月 + from to 时间 当lidu为 0时(2025-03-12 09:54:42) 当lidu为 1时(2025-03-12) 当lidu为 2时(2025-03) + */ + + $authorization = trim($this->request->header('authorization', $this->authorization)); + $lidu = trim($this->request->param('lidu', '')); + $from = trim($this->request->param('from', '')); + $to = trim($this->request->param('to', '')); + + if (empty($authorization)) { + return errorJson('缺少授权信息'); + } + + $params = [ + 'lidu' => $lidu, + 'from' => $from, + 'to' => $to, + ]; + + $headerData = ['client:' . self::CLIENT_TYPE]; + $header = setHeader($headerData, $authorization, 'plain'); + + try { + $result = requestCurl($this->baseUrl . 'api/DashBoard/listStatisticsCountDTOByCreateTimeAsync', $params, 'GET', $header); + return successJson($result); + } catch (\Exception $e) { + return errorJson('获取粉丝统计数据失败:' . $e->getMessage()); + } + } + +} \ No newline at end of file diff --git a/application/api/controller/UserController.php b/application/api/controller/UserController.php new file mode 100644 index 0000000..ab4ce13 --- /dev/null +++ b/application/api/controller/UserController.php @@ -0,0 +1,365 @@ +validateLoginParams(); + if (!is_array($params)) { + return $params; + } + + // 验证账号是否存在 + $existingAccount = CompanyAccountModel::where('userName', $params['username'])->find(); + if (empty($existingAccount)) { + // 记录登录失败日志 + recordUserLog(0, $params['username'], 'LOGIN', '账号不存在', $params, 500, '账号不存在'); + return errorJson('账号不存在'); + } + + // 获取验证码会话ID和用户输入的验证码 + $verifySessionId = $this->request->param('verifySessionId', ''); + $verifyCode = $this->request->param('verifyCode', ''); + + // 设置请求头 + $headerData = ['client:' . self::CLIENT_TYPE]; + + // 如果存在验证码信息,添加到请求头 + if (!empty($verifySessionId) && !empty($verifyCode)) { + $headerData[] = 'verifysessionid:' . $verifySessionId; + $headerData[] = 'verifycode:' . $verifyCode; + } + + $header = setHeader($headerData, '', 'plain'); + + try { + // 请求登录接口 + $result = requestCurl($this->baseUrl . 'token', $params, 'POST', $header); + $result_array = handleApiResponse($result); + + if (is_array($result_array) && isset($result_array['error'])) { + // 记录登录失败日志 + recordUserLog(0, $params['username'], 'LOGIN', '登录失败', $params, 500, $result_array['error_description']); + return errorJson($result_array['error_description']); + } + + // 获取客户端IP地址 + $ip = $this->request->ip(); + + // 登录成功,更新密码信息和登录信息 + $updateData = [ + 'passwordMd5' => md5($params['password']), + 'passwordLocal' => localEncrypt($params['password']), + 'lastLoginIp' => $ip, + 'lastLoginTime' => time() + ]; + + // 更新密码信息 + CompanyAccountModel::where('userName', $params['username'])->update($updateData); + + // 记录登录成功日志 + recordUserLog($existingAccount['id'], $params['username'], 'LOGIN', '登录成功', [], 200, '登录成功'); + + return successJson($result_array); + } catch (\Exception $e) { + // 记录登录异常日志 + recordUserLog(0, $params['username'], 'LOGIN', '登录请求失败', $params, 500, $e->getMessage()); + return errorJson('登录请求失败:' . $e->getMessage()); + } + } + + /** + * 获取新的token + * @return \think\response\Json + */ + public function getNewToken() + { + $grant_type = $this->request->param('grant_type', 'refresh_token'); + $refresh_token = $this->request->param('refresh_token', ''); + $authorization = $this->request->header('authorization', $this->authorization); + + if (empty($grant_type) || empty($authorization)) { + return errorJson('参数错误'); + } + + $params = [ + 'grant_type' => $grant_type, + 'refresh_token' => $refresh_token, + ]; + + + + $headerData = ['client:' . self::CLIENT_TYPE]; + $header = setHeader($headerData, $authorization, 'system'); + + try { + $result = requestCurl($this->baseUrl . 'token', $params, 'POST', $header); + $result_array = handleApiResponse($result); + + if (is_array($result_array) && isset($result_array['error'])) { + recordUserLog(0, '', 'REFRESH_TOKEN', '刷新token失败', $params, 500, $result_array['error_description']); + return errorJson($result_array['error_description']); + } + + recordUserLog(0, '', 'REFRESH_TOKEN', '刷新token成功', $params, 200, '刷新成功'); + return successJson($result_array); + } catch (\Exception $e) { + recordUserLog(0, '', 'REFRESH_TOKEN', '刷新token异常', $params, 500, $e->getMessage()); + return errorJson('获取新token失败:' . $e->getMessage()); + } + } + + /** + * 获取商户基本信息 + * @return \think\response\Json + */ + public function getAccountInfo() + { + $authorization = trim($this->request->header('authorization', $this->authorization)); + if (empty($authorization)) { + return errorJson('缺少授权信息'); + } + + $headerData = ['client:' . self::CLIENT_TYPE]; + $header = setHeader($headerData, $authorization, 'json'); + + try { + $result = requestCurl($this->baseUrl . 'api/Account/self', [], 'GET', $header,'json'); + $response = handleApiResponse($result); + if (!empty($response['account'])) { + $accountData = $response['account']; + + // 准备数据库字段映射,保持驼峰命名 + $dbData = [ + 'tenantId' => $accountData['id'], + 'realName' => $accountData['realName'], + 'nickname' => $accountData['nickname'], + 'memo' => $accountData['memo'], + 'avatar' => $accountData['avatar'], + 'userName' => $accountData['userName'], + 'secret' => $accountData['secret'], + 'accountType' => $accountData['accountType'], + 'companyId' => $accountData['departmentId'], + 'useGoogleSecretKey' => $accountData['useGoogleSecretKey'], + 'hasVerifyGoogleSecret' => $accountData['hasVerifyGoogleSecret'], + 'updateTime' => time() + ]; + + + // 查找是否存在该账户 + $existingAccount = CompanyAccountModel::where('userName', $accountData['userName'])->find(); + if ($existingAccount) { + // 更新现有记录 + CompanyAccountModel::where('userName', $accountData['userName'])->update($dbData); + } else { + // 创建新记录 + $dbData['createTime'] = time(); + CompanyAccountModel::create($dbData); + } + return successJson($response['account']); + }else{ + return successJson($response); + } + + + } catch (\Exception $e) { + recordUserLog(0, '', 'GET_ACCOUNT_INFO', '获取账户信息异常', [], 500, $e->getMessage()); + return errorJson('获取账户信息失败:' . $e->getMessage()); + } + } + + /** + * 修改密码 + * @return \think\response\Json + */ + public function modifyPwd($data = []) + { + + if (empty($data)) { + return json_encode(['code' => 400,'msg' => '参数缺失']); + } + + if (!isset($data['id']) || !isset($data['pwd'])) { + return json_encode(['code' => 401,'msg' => '参数缺失']); + } + $authorization = $this->authorization; + + if (empty($authorization)) { + return json_encode(['code' => 400,'msg' => '缺少授权信息']); + } + + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + $params = [ + 'id' => $data['id'], + 'newPw' => $data['pwd'], + ]; + + try { + $result = requestCurl($this->baseUrl . 'api/Account/modifypw', $params, 'PUT', $header,'json'); + $response = handleApiResponse($result); + if (empty($response)) { + return json_encode(['code' => 200,'msg' => '修改成功']); + } + return json_encode(['code' => 400,'msg' => $response]); + } catch (\Exception $e) { + return json_encode(['code' => 400,'msg' => '修改密码失败:' . $e->getMessage()]); + } + } + + /** + * 登出 + * @return \think\response\Json + */ + public function logout() + { + $authorization = trim($this->request->header('authorization', $this->authorization)); + if (empty($authorization)) { + return errorJson('缺少授权信息'); + } + + $headerData = ['client:' . self::CLIENT_TYPE]; + $header = setHeader($headerData, $authorization, 'system'); + + try { + // 调用外部退出登录接口 + $result = requestCurl($this->baseUrl . 'api/Account/SignOut', [], 'GET', $header); + return successJson([] , '退出成功'); + } catch (\Exception $e) { + recordUserLog(0, '', 'LOGOUT', '退出登录异常', [], 500, $e->getMessage()); + return errorJson('退出登录失败:' . $e->getMessage()); + } + } + + /** + * 获取验证码 + * @return \think\response\Json + */ + public function getVerifyCode($isJson = false) + { + $headerData = ['client:' . self::CLIENT_TYPE]; + $header = setHeader($headerData, '', 'plain'); + + try { + $result = requestCurl($this->baseUrl . 'api/Account/getVerifyCode', [], 'GET', $header); + $response = handleApiResponse($result); + + // 检查返回的数据格式 + if (is_array($response)) { + // 如果verifyCodeImage和verifySessionId都不为null,返回它们 + if (!empty($response['verifyCodeImage']) && !empty($response['verifySessionId'])) { + $returnData = [ + 'verifyCodeImage' => $response['verifyCodeImage'], + 'verifySessionId' => $response['verifySessionId'] + ]; + return !empty($isJson) ? json_encode(['code' => 200,'data' => $returnData]) : successJson($returnData); + } + } + + // 如果不是预期的格式,返回原始数据 + return !empty($isJson) ? json_encode(['code' => 200,'msg' => '无需验证码','data' => ['verifyCodeImage' => '', 'verifySessionId' => '']]) : successJson(['verifyCodeImage' => '', 'verifySessionId' => ''],'无需验证码'); + } catch (\Exception $e) { + $msg = '获取验证码失败'. $e->getMessage(); + return !empty($isJson) ? json_encode(['code' => 400,'msg' => $msg]) : errorJson($msg); + } + } + + /** + * 验证登录参数 + * @return array|\think\response\Json + */ + private function validateLoginParams() + { + $username = trim($this->request->param('username', '')); + $password = trim($this->request->param('password', '')); + $verifyCode = trim($this->request->param('verifyCode', '')); + $verifySessionId = trim($this->request->param('verifySessionId', '')); + + if (empty($username) || empty($password)) { + return errorJson('用户名和密码不能为空'); + } + + // 验证密码格式 + $passwordValidation = validateString($password, 'password',['max_length' => 20]); + if (!$passwordValidation['status']) { + return errorJson($passwordValidation['message']); + } + + // 如果提供了验证码,验证格式 + if (!empty($verifyCode)) { + if (empty($verifySessionId)) { + return errorJson('验证码会话ID不能为空'); + } + // 验证码格式验证(假设是4位数字) + if (!preg_match('/^\d{4}$/', $verifyCode)) { + return errorJson('验证码格式不正确'); + } + } + + return [ + 'grant_type' => 'password', + 'username' => $username, + 'password' => $password, + ]; + } + + /** + * 验证修改密码参数 + * @return array|\think\response\Json + */ + private function validateModifyPwdParams() + { + $cPw = trim($this->request->param('cPw', '')); + $newPw = trim($this->request->param('newPw', '')); + $oldPw = trim($this->request->param('oldPw', '')); + + if (empty($cPw) || empty($newPw) || empty($oldPw)) { + return errorJson('密码参数不完整'); + } + + if ($newPw !== $cPw) { + return errorJson('两次输入的新密码不一致'); + } + + // 验证新密码格式 + $passwordValidation = validateString($newPw, 'password'); + if (!$passwordValidation['status']) { + return errorJson($passwordValidation['message']); + } + + return [ + 'cPw' => $cPw, + 'newPw' => $newPw, + 'oldPw' => $oldPw, + ]; + } +} diff --git a/application/api/controller/WebSocketController.php b/application/api/controller/WebSocketController.php new file mode 100644 index 0000000..69afb2e --- /dev/null +++ b/application/api/controller/WebSocketController.php @@ -0,0 +1,1157 @@ +initConnection($userData); + } + + /** + * 初始化WebSocket连接 + * @param array $userData 用户数据 + */ + protected function initConnection($userData = []) + { + if (!empty($userData) && count($userData)) { + if (empty($userData['userName']) || empty($userData['password'])) { + return json_encode(['code' => 400, 'msg' => '参数缺失']); + } + + // 检查缓存中是否存在有效的token + $cacheKey = 'websocket_token_' . $userData['userName']; + $cachedToken = Cache::get($cacheKey); + if ($cachedToken) { + $this->authorized = $cachedToken; + $this->accountId = $userData['accountId']; + } else { + $params = [ + 'grant_type' => 'password', + 'username' => $userData['userName'], + 'password' => $userData['password'] + ]; + // 调用登录接口获取token + $headerData = ['client:kefu-client']; + + $headerData[] = 'verifysessionid:2fbc51c9-db70-4e84-9568-21ef3667e1be'; + $headerData[] = 'verifycode:5bcd'; + $header = setHeader($headerData, '', 'plain'); + $result = requestCurl('https://s2.siyuguanli.com:9991/token', $params, 'POST', $header); + $result_array = handleApiResponse($result); + if (isset($result_array['access_token']) && !empty($result_array['access_token'])) { + $this->authorized = $result_array['access_token']; + $this->accountId = $userData['accountId']; + + // 将token存入缓存,有效期5分钟 + Cache::set($cacheKey, $this->authorized, 300); + } else { + return json_encode(['code' => 400, 'msg' => '获取系统授权信息失败']); + } + } + } else { + $this->authorized = $this->request->header('authorization', ''); + $this->accountId = $this->request->param('accountId', ''); + } + + if (empty($this->authorized) || empty($this->accountId)) { + return json_encode(['code' => 400, 'msg' => '缺失关键参数']); + } + + $this->connect(); + } + + /** + * 建立WebSocket连接 + */ + protected function connect() + { + try { + //证书 + $context = stream_context_create(); + stream_context_set_option($context, 'ssl', 'verify_peer', false); + stream_context_set_option($context, 'ssl', 'verify_peer_name', false); + + //开启WS链接 + $result = [ + "accessToken" => $this->authorized, + "accountId" => $this->accountId, + "client" => "kefu-client", + "cmdType" => "CmdSignIn", + "seq" => 1, + ]; + + $content = json_encode($result); + $this->client = new Client("wss://s2.siyuguanli.com:9993", + [ + 'filter' => ['text', 'binary', 'ping', 'pong', 'close', 'receive', 'send'], + 'context' => $context, + 'headers' => [ + 'Sec-WebSocket-Protocol' => 'soap', + 'origin' => 'localhost', + ], + 'timeout' => 86400, + ] + ); + + $this->client->send($content); + $this->isConnected = true; + $this->lastHeartbeatTime = time(); + + // 启动心跳检测 + //$this->startHeartbeat(); + + } catch (\Exception $e) { + Log::error("WebSocket连接失败:" . $e->getMessage()); + $this->isConnected = false; + } + } + + /** + * 启动心跳检测 + */ + protected function startHeartbeat() + { + // 使用定时器发送心跳 + \Swoole\Timer::tick($this->heartbeatInterval * 1000, function () { + if ($this->isConnected) { + $this->sendHeartbeat(); + } + }); + } + + /** + * 发送心跳包 + */ + protected function sendHeartbeat() + { + try { + $heartbeat = [ + "cmdType" => "CmdHeartbeat", + "seq" => time() + ]; + + $this->client->send(json_encode($heartbeat)); + $this->lastHeartbeatTime = time(); + + } catch (\Exception $e) { + Log::error("发送心跳包失败:" . $e->getMessage()); + $this->reconnect(); + } + } + + /** + * 重连机制 + */ + protected function reconnect() + { + try { + if ($this->client) { + $this->client->close(); + } + $this->isConnected = false; + $this->connect(); + } catch (\Exception $e) { + Log::error("WebSocket重连失败:" . $e->getMessage()); + } + } + + /** + * 检查连接状态 + */ + protected function checkConnection() + { + if (!$this->isConnected || (time() - $this->lastHeartbeatTime) > $this->heartbeatInterval * 2) { + $this->reconnect(); + } + } + + /** + * 发送消息 + * @param array $data 消息数据 + * @return array + */ + protected function sendMessage($data,$receive = true) + { + $this->checkConnection(); + + try { + $this->client->send(json_encode($data)); + if ($receive){ + $response = $this->client->receive(); + return json_decode($response, true); + }else{ + return ['code' => 200, 'msg' => '成功']; + } + } catch (\Exception $e) { + Log::error("发送消息失败:" . $e->getMessage()); + $this->reconnect(); + return ['code' => 500, 'msg' => '发送消息失败']; + } + } + + /************************************ + * 朋友圈相关功能 + ************************************/ + + /** + * 获取指定账号朋友圈信息 + * @param array $data 请求参数 + * @return \think\response\Json + */ + public function getMoments($data = []) + { + $count = !empty($data['count']) ? $data['count'] : 10; + $wechatAccountId = !empty($data['wechatAccountId']) ? $data['wechatAccountId'] : ''; + $wechatFriendId = !empty($data['wechatFriendId']) ? $data['wechatFriendId'] : 0; + $prevSnsId = !empty($data['prevSnsId']) ? $data['prevSnsId'] : 0; + $maxPages = 1; // 最大页数限制为20 + $currentPage = 1; // 当前页码 + $allMoments = []; // 存储所有朋友圈数据 + + //过滤消息 + if (empty($wechatAccountId)) { + return json_encode(['code' => 400, 'msg' => '指定账号不能为空']); + } + + try { + do { + $params = [ + "cmdType" => "CmdFetchMoment", + "count" => $count, + "createTimeSec" => time(), + "isTimeline" => false, + "prevSnsId" => $prevSnsId, + "wechatAccountId" => $wechatAccountId, + "wechatFriendId" => $wechatFriendId, + "seq" => time(), + ]; + $message = $this->sendMessage($params); + + + // 检查是否遇到频率限制 + if (isset($message['extra']) && strpos($message['extra'], '朋友圈太频繁了') !== false) { + sleep(10); + continue; + } + + + // 检查返回结果 + if (!isset($message['result']) || empty($message['result']) || !is_array($message['result'])) { + break; + } + + + // 检查是否遇到旧数据 + $hasOldData = false; + foreach ($message['result'] as $moment) { + $momentId = WechatMoments::where('snsId', $moment['snsId']) + ->where('wechatAccountId', $wechatAccountId) + ->value('id'); + + if (!empty($momentId)) { + $hasOldData = true; + break; + } + } + + // 如果遇到旧数据,结束本次任务 + if ($hasOldData) { + // Log::info('遇到旧数据,结束本次任务'); + // break; + } + + // 合并朋友圈数据 + $allMoments = array_merge($allMoments, $message['result']); + // 存储当前页的朋友圈数据到数据库 + $this->saveMomentsToDatabase($message['result'], $wechatAccountId, $wechatFriendId); + + // 获取最后一条数据的snsId,用于下次查询 + $lastMoment = end($message['result']); + if (!$lastMoment || !isset($lastMoment['snsId'])) { + break; + } + + $prevSnsId = $lastMoment['snsId']; + $currentPage++; + + // 如果已经达到最大页数,退出循环 + if ($currentPage > $maxPages) { + break; + } + + // 如果返回的数据少于请求的数量,说明没有更多数据了 + if (count($message['result']) < $count) { + break; + } + + } while (true); + + // 构建返回数据 + $result = [ + 'code' => 200, + 'msg' => '获取朋友圈信息成功', + 'data' => [ + 'list' => $allMoments, + 'total' => count($allMoments), + 'nextPrevSnsId' => $prevSnsId + ] + ]; + + return json_encode($result); + } catch (\Exception $e) { + return json_encode(['code' => 500, 'msg' => $e->getMessage()]); + } + } + + /** + * 朋友圈点赞 + * @return \think\response\Json + */ + public function momentInteract($data = []) + { + + $snsId = !empty($data['snsId']) ? $data['snsId'] : ''; + $wechatAccountId = !empty($data['wechatAccountId']) ? $data['wechatAccountId'] : ''; + $wechatFriendId = !empty($data['wechatFriendId']) ? $data['wechatFriendId'] : 0; + + + //过滤消息 + if (empty($snsId)) { + return json_encode(['code' => 400, 'msg' => 'snsId不能为空']); + } + if (empty($wechatAccountId)) { + return json_encode(['code' => 400, 'msg' => '微信id不能为空']); + } + + try { + $result = [ + "cmdType" => "CmdMomentInteract", + "momentInteractType" => 1, + "seq" => time(), + "snsId" => $snsId, + "wechatAccountId" => $wechatAccountId, + "wechatFriendId" => $wechatFriendId, + ]; + + $message = $this->sendMessage($result); + return json_encode(['code' => 200, 'msg' => '点赞成功', 'data' => $message]); + } catch (\Exception $e) { + return json_encode(['code' => 500, 'msg' => $e->getMessage()]); + } + } + + /** + * 朋友圈取消点赞 + * @return \think\response\Json + */ + public function momentCancelInteract() + { + if ($this->request->isPost()) { + $data = $this->request->param(); + + if (empty($data)) { + return json_encode(['code' => 400, 'msg' => '参数缺失']); + } + + //过滤消息 + if (empty($data['snsId'])) { + return json_encode(['code' => 400, 'msg' => 'snsId不能为空']); + } + if (empty($data['wechatAccountId'])) { + return json_encode(['code' => 400, 'msg' => '微信id不能为空']); + } + + try { + $result = [ + "CommentId2" => '', + "CommentTime" => 0, + "cmdType" => "CmdMomentCancelInteract", + "optType" => 1, + "seq" => time(), + "snsId" => $data['snsId'], + "wechatAccountId" => $data['wechatAccountId'], + "wechatFriendId" => 0, + ]; + + $message = $this->sendMessage($result); + return json_encode(['code' => 200, 'msg' => '取消点赞成功', 'data' => $message]); + } catch (\Exception $e) { + return json_encode(['code' => 500, 'msg' => $e->getMessage()]); + } + } else { + return json_encode(['code' => 400, 'msg' => '非法请求']); + } + } + + /** + * 获取指定账号朋友圈图片地址 + * @param array $data 请求参数 + * @return string JSON响应 + */ + public function getMomentSourceRealUrl($data = []) + { + try { + // 参数验证 + if (empty($data)) { + return json_encode(['code' => 400, 'msg' => '参数缺失']); + } + + // 验证必要参数 + $requiredParams = ['snsId', 'snsUrls', 'wechatAccountId']; + foreach ($requiredParams as $param) { + if (empty($data[$param])) { + return json_encode(['code' => 400, 'msg' => "参数 {$param} 不能为空"]); + } + } + + // 验证snsUrls是否为数组 + if (!is_array($data['snsUrls'])) { + return json_encode(['code' => 400, 'msg' => '资源信息格式错误,应为数组']); + } + + // 判断snsUrls是否已经是远程图片(http/https) + $allRemoteUrls = true; + foreach ($data['snsUrls'] as $url) { + if (empty($url)) { + continue; + } + // 检查URL是否以http://或https://开头 + if (!preg_match('/^https?:\/\//i', $url)) { + $allRemoteUrls = false; + break; + } + } + + // 如果全部都是远程URL,直接使用,不需要通过WebSocket下载 + if ($allRemoteUrls && !empty($data['snsUrls'])) { + $urls = json_encode($data['snsUrls'], 256); + + // 上传图片到OSS + $ossUrls = $this->uploadMomentImagesToOss($data['snsUrls'], $data['snsId']); + + // 更新数据库:保存原始URL和OSS URL,并标记已上传 + $updateData = [ + 'resUrls' => $urls, + 'isOssUploaded' => 1, // 标识已上传到OSS + 'update_time' => time() + ]; + + // 如果有OSS URL,保存到ossUrls字段 + if (!empty($ossUrls)) { + $updateData['ossUrls'] = json_encode($ossUrls, 256); + } + Db::table('s2_wechat_moments')->where('snsId', $data['snsId'])->update($updateData); + + return json_encode(['code' => 200, 'msg' => '获取朋友圈资源链接成功(已为远程URL)', 'data' => ['urls' => $data['snsUrls']]]); + } + + // 如果不是远程URL,需要通过WebSocket下载 + // 检查连接状态 + if (!$this->isConnected) { + $this->connect(); + if (!$this->isConnected) { + return json_encode(['code' => 500, 'msg' => 'WebSocket连接失败']); + } + } + + // 构建请求参数 + $params = [ + "cmdType" => 'CmdDownloadMomentImages', + "snsId" => $data['snsId'], + "urls" => $data['snsUrls'], + "wechatAccountId" => $data['wechatAccountId'], + "seq" => time(), + ]; + + $message = $this->sendMessage($params); + + if (empty($message)) { + return json_encode(['code' => 500, 'msg' => '获取朋友圈资源链接失败']); + } + if ($message['cmdType'] == 'CmdDownloadMomentImagesResult' && is_array($message['urls']) && count($message['urls']) > 0) { + $urls = json_encode($message['urls'], 256); + + // 上传图片到OSS + $ossUrls = $this->uploadMomentImagesToOss($message['urls'], $data['snsId']); + + // 更新数据库:保存原始URL和OSS URL,并标记已上传 + $updateData = [ + 'resUrls' => $urls, + 'isOssUploaded' => 1, // 标识已上传到OSS + 'update_time' => time() + ]; + + // 如果有OSS URL,保存到ossUrls字段 + if (!empty($ossUrls)) { + $updateData['ossUrls'] = json_encode($ossUrls, 256); + } + Db::table('s2_wechat_moments')->where('snsId', $data['snsId'])->update($updateData); + + } + return json_encode(['code' => 200, 'msg' => '获取朋友圈资源链接成功', 'data' => $message]); + } catch (\Exception $e) { + // 记录错误日志 + Log::error('获取朋友圈资源链接异常:' . $e->getMessage()); + Log::error('异常堆栈:' . $e->getTraceAsString()); + + // 尝试重连 + try { + $this->reconnect(); + } catch (\Exception $reconnectError) { + Log::error('WebSocket重连失败:' . $reconnectError->getMessage()); + } + + return json_encode([ + 'code' => 500, + 'msg' => '获取朋友圈资源链接失败:' . $e->getMessage() + ]); + } + } + + /** + * 上传朋友圈图片到OSS + * @param array $urls 图片URL数组 + * @param string $snsId 朋友圈ID + * @return array OSS URL数组 + */ + protected function uploadMomentImagesToOss($urls, $snsId) + { + $ossUrls = []; + + if (empty($urls) || !is_array($urls)) { + return $ossUrls; + } + + try { + // 创建临时目录(兼容无 runtime_path() 辅助函数的环境) + if (function_exists('runtime_path')) { + $baseRuntimePath = rtrim(runtime_path(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + } elseif (defined('RUNTIME_PATH')) { + $baseRuntimePath = rtrim(RUNTIME_PATH, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + } else { + // 兜底:使用项目根目录下的 runtime 目录 + $baseRuntimePath = rtrim(ROOT_PATH, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'runtime' . DIRECTORY_SEPARATOR; + } + + $tempDir = $baseRuntimePath . 'temp' . DIRECTORY_SEPARATOR . 'moments' . DIRECTORY_SEPARATOR . date('Y' . DIRECTORY_SEPARATOR . 'm' . DIRECTORY_SEPARATOR . 'd') . DIRECTORY_SEPARATOR; + + if (!is_dir($tempDir)) { + mkdir($tempDir, 0755, true); + } + + foreach ($urls as $index => $url) { + if (empty($url)) { + continue; + } + + try { + // 下载图片到临时文件 + $tempFile = $tempDir . md5($url . $snsId . $index) . '.jpg'; + + // 使用curl下载图片 + $ch = curl_init($url); + $fp = fopen($tempFile, 'wb'); + curl_setopt($ch, CURLOPT_FILE, $fp); + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + fclose($fp); + + if ($httpCode != 200 || !file_exists($tempFile) || filesize($tempFile) == 0) { + Log::warning('下载朋友圈图片失败:' . $url . ', HTTP Code: ' . $httpCode); + @unlink($tempFile); + continue; + } + + // 生成OSS对象名称 + $objectName = 'moments/' . date('Y/m/d/') . md5($snsId . $index . time()) . '.jpg'; + + // 上传到OSS + $result = AliyunOSS::uploadFile($tempFile, $objectName); + if ($result['success']) { + $ossUrls[] = $result['url']; + } else { + Log::error('朋友圈图片上传OSS失败:' . $url . ', 错误:' . ($result['error'] ?? '未知错误')); + } + + // 删除临时文件 + @unlink($tempFile); + + } catch (\Exception $e) { + Log::error('上传朋友圈图片到OSS异常:' . $e->getMessage() . ', URL: ' . $url); + if (isset($tempFile) && file_exists($tempFile)) { + @unlink($tempFile); + } + } + } + + } catch (\Exception $e) { + Log::error('上传朋友圈图片到OSS异常:' . $e->getMessage()); + } + + return $ossUrls; + } + + /** + * 保存朋友圈数据到数据库 + * @param array $momentList 朋友圈数据列表 + * @param int $wechatAccountId 微信账号ID + * @param string $wechatFriendId 微信好友ID + * @return bool + */ + protected function saveMomentsToDatabase($momentList, $wechatAccountId, $wechatFriendId) + { + if (empty($momentList) || !is_array($momentList)) { + return false; + } + + + try { + foreach ($momentList as $moment) { + // 提取momentEntity中的数据 + $momentEntity = $moment['momentEntity'] ?? []; + + // 检查朋友圈数据是否已存在,并获取isOssUploaded状态 + $existingMoment = Db::table('s2_wechat_moments') + ->where('snsId', $moment['snsId']) + ->where('wechatAccountId', $wechatAccountId) + ->find(); + + $momentId = $existingMoment['id'] ?? null; + $isOssUploaded = isset($existingMoment['isOssUploaded']) ? (int)$existingMoment['isOssUploaded'] : 0; + + $dataToSave = [ + 'commentList' => json_encode($moment['commentList'] ?? [], 256), + 'createTime' => $moment['createTime'] ?? 0, + 'likeList' => json_encode($moment['likeList'] ?? [], 256), + 'content' => $momentEntity['content'] ?? '', + 'lat' => $momentEntity['lat'] ?? 0, + 'lng' => $momentEntity['lng'] ?? 0, + 'location' => $momentEntity['location'] ?? '', + 'picSize' => $momentEntity['picSize'] ?? 0, + 'resUrls' => json_encode($momentEntity['resUrls'] ?? [], 256), + 'urls' => json_encode($momentEntity['urls'] ?? [], 256), + 'userName' => $momentEntity['userName'] ?? '', + 'snsId' => $moment['snsId'] ?? '', + 'type' => $moment['type'] ?? 0, + 'title' => $momentEntity['title'] ?? '', + 'coverImage' => $momentEntity['coverImage'] ?? '', + 'update_time' => time() + ]; + + if (!empty($momentId)) { + // 如果已存在,则更新数据(保留isOssUploaded和ossUrls字段,不覆盖) + Db::table('s2_wechat_moments')->where('id', $momentId)->update($dataToSave); + } else { + if (empty($wechatFriendId)) { + $wechatFriendId = WechatFriend::where('wechatAccountId', $wechatAccountId)->where('wechatId', $momentEntity['userName'])->value('id'); + } + // 如果不存在,则插入新数据 + $dataToSave['wechatAccountId'] = $wechatAccountId; + $dataToSave['wechatFriendId'] = $wechatFriendId ?? 0; + $dataToSave['create_time'] = time(); + $dataToSave['ossUrls'] = json_encode([],256); + $dataToSave['isOssUploaded'] = 0; // 新记录默认为未上传 + $res = WechatMoments::create($dataToSave); + } + + // 获取资源链接(检查是否已上传到OSS,如果已上传则跳过) + if(empty($momentEntity['urls']) || $moment['type'] != 1) { + // 如果没有urls或类型不是1,跳过 + } elseif ($isOssUploaded == 1) { + // 如果已上传到OSS,跳过采集 + } else { + // 未上传到OSS,执行采集 + $snsData = [ + 'snsId' => $moment['snsId'], + 'snsUrls' => $momentEntity['urls'], + 'wechatAccountId' => $wechatAccountId, + ]; + $this->getMomentSourceRealUrl($snsData); + } + + } + //Log::write('朋友圈数据已存入数据库,共' . count($momentList) . '条'); + return true; + } catch (\Exception $e) { + //Log::write('保存朋友圈数据失败:' . $e->getMessage(), 'error'); + return false; + } + } + + + /** + * 修改好友标签 + * @param array $data 请求参数 + * @return string JSON响应 + */ + public function modifyFriendLabel($data = []) + { + // 获取请求参数 + $wechatFriendId = !empty($data['wechatFriendId']) ? $data['wechatFriendId'] : 0; + $wechatAccountId = !empty($data['wechatAccountId']) ? $data['wechatAccountId'] : ''; + $labels = !empty($data['labels']) ? $data['labels'] : []; + + // 验证必要参数 + if (empty($wechatFriendId)) { + return json_encode(['code' => 400, 'msg' => '好友ID不能为空']); + } + + if (empty($wechatAccountId)) { + return json_encode(['code' => 400, 'msg' => '微信账号ID不能为空']); + } + + if (empty($labels)) { + return json_encode(['code' => 400, 'msg' => '标签不能为空']); + } + + try { + // 构建请求参数 + $params = [ + "cmdType" => "CmdModifyFriendLabel", + "labels" => $labels, + "seq" => time(), + "wechatAccountId" => $wechatAccountId, + "wechatFriendId" => $wechatFriendId, + ]; + + // 发送请求并获取响应 + $message = $this->sendMessage($params); + + // 返回成功响应 + return json_encode(['code' => 200, 'msg' => '修改标签成功', 'data' => $message]); + } catch (\Exception $e) { + // 记录错误日志 + Log::error('修改好友标签失败:' . $e->getMessage()); + + // 返回错误响应 + return json_encode(['code' => 500, 'msg' => '修改标签失败:' . $e->getMessage()]); + } + } + + /************************************ + * 消息发送相关功能 + ************************************/ + + /** + * 个人消息发送 + * @return \think\response\Json + */ + public function sendPersonal(array $dataArray) + { + //过滤消息 + if (empty($dataArray['content'])) { + return json_encode(['code' => 400, 'msg' => '内容缺失']); + } + if (empty($dataArray['wechatAccountId'])) { + return json_encode(['code' => 400, 'msg' => '微信id不能为空']); + } + if (empty($dataArray['wechatFriendId'])) { + return json_encode(['code' => 400, 'msg' => '接收人不能为空']); + } + + if (empty($dataArray['msgType'])) { + return json_encode(['code' => 400, 'msg' => '类型缺失']); + } + + // 消息拼接 msgType(1:文本 3:图片 43:视频 47:动图表情包(gif、其他表情包) 49:小程序/其他:图文、文件) + // 当前,type 为文本、图片、动图表情包的时候,content为string, 其他情况为对象 {type: 'file/link/...', url: '', title: '', thunmbPath: '', desc: ''} + $params = [ + "cmdType" => "CmdSendMessage", + "content" => $dataArray['content'], + "msgSubType" => 0, + "msgType" => $dataArray['msgType'], + "seq" => time(), + "wechatAccountId" => $dataArray['wechatAccountId'], + "wechatChatroomId" => 0, + "wechatFriendId" => $dataArray['wechatFriendId'], + ]; + // 发送请求 + $this->client->send(json_encode($params)); + // 接收响应 + $response = $this->client->receive(); + $message = json_decode($response, true); + if (!empty($message)) { + return json_encode(['code' => 200, 'msg' => '信息发送成功', 'data' => $message]); + } + } + + /** + * 发送群消息 + * @return \think\response\Json + */ + public function sendCommunity($dataArray = []) + { + if (!is_array($dataArray)) { + return json_encode(['code' => 400, 'msg' => '数据格式错误']); + } + + //过滤消息 + if (empty($dataArray['content'])) { + return json_encode(['code' => 400, 'msg' => '内容缺失']); + } + if (empty($dataArray['wechatAccountId'])) { + return json_encode(['code' => 400, 'msg' => '微信id不能为空']); + } + + if (empty($dataArray['msgType'])) { + return json_encode(['code' => 400, 'msg' => '类型缺失']); + } + if (empty($dataArray['wechatChatroomId'])) { + return json_encode(['code' => 400, 'msg' => '群id不能为空']); + } + + $message = []; + try { + //消息拼接 msgType(1:文本 3:图片 43:视频 47:动图表情包 49:小程序) + $params = [ + "cmdType" => "CmdSendMessage", + "content" => htmlspecialchars_decode($dataArray['content']), + "msgSubType" => 0, + "msgType" => $dataArray['msgType'], + "seq" => time(), + "wechatAccountId" => $dataArray['wechatAccountId'], + "wechatChatroomId" => $dataArray['wechatChatroomId'], + "wechatFriendId" => 0, + ]; + + // 发送请求 + $this->client->send(json_encode($params)); + // 接收响应 + $response = $this->client->receive(); + $message = json_decode($response, true); + if (!empty($message)) { + return json_encode(['code' => 200, 'msg' => '信息发送成功', 'data' => $message]); + } + } catch (\Exception $e) { + $msg = $e->getMessage(); + return json_encode(['code' => 400, 'msg' => $msg, 'data' => $message]); + } + + + } + + /** + * 发送群消息(内部调用版) + * @param array $data 消息数据 + * @return \think\response\Json + */ + public function sendCommunitys($data = []) + { + if (empty($data)) { + return json_encode(['code' => 400, 'msg' => '参数缺失']); + } + $dataArray = $data; + if (!is_array($dataArray)) { + return json_encode(['code' => 400, 'msg' => '数据格式错误']); + } + + //过滤消息 + if (empty($dataArray['content'])) { + return json_encode(['code' => 400, 'msg' => '内容缺失']); + } + if (empty($dataArray['wechatAccountId'])) { + return json_encode(['code' => 400, 'msg' => '微信id不能为空']); + } + + if (empty($dataArray['msgType'])) { + return json_encode(['code' => 400, 'msg' => '类型缺失']); + } + if (empty($dataArray['wechatChatroomId'])) { + return json_encode(['code' => 400, 'msg' => '群id不能为空']); + } + + $msg = '消息成功发送'; + $message = []; + try { + //消息拼接 msgType(1:文本 3:图片 43:视频 47:动图表情包 49:小程序) + $result = [ + "cmdType" => "CmdSendMessage", + "content" => $dataArray['content'], + "msgSubType" => 0, + "msgType" => $dataArray['msgType'], + "seq" => time(), + "wechatAccountId" => $dataArray['wechatAccountId'], + "wechatChatroomId" => $dataArray['wechatChatroomId'], + "wechatFriendId" => 0, + ]; + + $result = json_encode($result); + $this->client->send($result); + $message = $this->client->receive(); + //关闭WS链接 + $this->client->close(); + //Log::write('WS群消息发送'); + //Log::write($message); + $message = json_decode($message, 1); + } catch (\Exception $e) { + $msg = $e->getMessage(); + } + + return json_encode(['code' => 200, 'msg' => $msg, 'data' => $message]); + } + + + /** + * 添加群好友 + * @param $data + * @return false|string + */ + public function CmdChatroomOperate($data = []) + { + try { + // 参数验证 + if (empty($data)) { + return json_encode(['code' => 400, 'msg' => '参数缺失']); + } + + // 验证必要参数 + if (empty($data['wechatId'])) { + return json_encode(['code' => 400, 'msg' => 'wechatId不能为空']); + } + + if (empty($data['sendWord'])) { + return json_encode(['code' => 400, 'msg' => '添加的招呼语不能为空']); + } + + if (empty($data['wechatAccountId'])) { + return json_encode(['code' => 400, 'msg' => '微信账号ID不能为空']); + } + if (empty($data['wechatChatroomId'])) { + return json_encode(['code' => 400, 'msg' => '群ID不能为空']); + } + + + // 构建请求参数 + $params = [ + "chatroomOperateType" => 1, + "cmdType" => "CmdChatroomOperate", + "extra" => [ + 'wechatId' => $data['wechatId'], + 'sendWord' => $data['sendWord'] + ], + "seq" => time(), + "wechatAccountId" => $data['wechatAccountId'], + "wechatChatroomId" => $data['wechatChatroomId'], + ]; + $message = $this->sendMessage($params); + return json_encode(['code' => 200, 'msg' => '添加好友请求发送成功', 'data' => $message]); + } catch (\Exception $e) { + // 返回错误响应 + return json_encode(['code' => 500, 'msg' => '添加群好友异常:' . $e->getMessage()]); + } + } + + /** + * 创建群聊 + * @param array $data 请求参数 + * @return string JSON响应 + */ + public function CmdChatroomCreate($data = []) + { + try { + // 参数验证 + if (empty($data)) { + return json_encode(['code' => 400, 'msg' => '参数缺失']); + } + + // 验证必要参数 + if (empty($data['chatroomName'])) { + return json_encode(['code' => 400, 'msg' => '群名称不能为空']); + } + + if (empty($data['wechatFriendIds']) || !is_array($data['wechatFriendIds'])) { + return json_encode(['code' => 400, 'msg' => '好友ID列表不能为空且必须为数组']); + } + + if (count($data['wechatFriendIds']) < 2) { + return json_encode(['code' => 400, 'msg' => '创建群聊至少需要2个好友']); + } + + if (empty($data['wechatAccountId'])) { + return json_encode(['code' => 400, 'msg' => '微信账号ID不能为空']); + } + + // 构建请求参数 + $params = [ + "cmdType" => "CmdChatroomCreate", + "seq" => time(), + "wechatAccountId" => $data['wechatAccountId'], + "chatroomName" => $data['chatroomName'], +// "wechatFriendIds" => $data['wechatFriendIds'] + "wechatFriendIds" => [17453051,17453058] + ]; + $message = $this->sendMessage($params,false); + return json_encode(['code' => 200, 'msg' => '群聊创建成功', 'data' => $message]); + } catch (\Exception $e) { + // 记录错误日志 + Log::error('创建群聊异常:' . $e->getMessage()); + // 返回错误响应 + return json_encode(['code' => 500, 'msg' => '创建群聊异常:' . $e->getMessage()]); + } + } + + + /** + * 邀请好友入群 + * @param array $data 请求参数 + * @return string JSON响应 + */ + public function CmdChatroomInvite($data = []) + { + try { + // 参数验证 + if (empty($data)) { + return json_encode(['code' => 400, 'msg' => '参数缺失']); + } + + // 验证必要参数 + if (empty($data['wechatChatroomId'])) { + return json_encode(['code' => 400, 'msg' => '群ID不能为空']); + } + if (empty($data['wechatFriendIds'])) { + return json_encode(['code' => 400, 'msg' => '好友ID不能为空']); + } + + if (!is_array($data['wechatFriendIds'])) { + return json_encode(['code' => 400, 'msg' => '好友数据格式必须为数组']); + } + + + // 构建请求参数 + $params = [ + "cmdType" => "CmdChatroomInvite", + "seq" => time(), + "wechatChatroomId" => $data['wechatChatroomId'], + "wechatFriendIds" => $data['wechatFriendIds'] + ]; + + $message = $this->sendMessage($params,false); + return json_encode(['code' => 200, 'msg' => '邀请成功', 'data' => $message]); + } catch (\Exception $e) { + // 记录错误日志 + Log::error('邀请好友入群异常:' . $e->getMessage()); + // 返回错误响应 + return json_encode(['code' => 500, 'msg' => '邀请好友入群异常:' . $e->getMessage()]); + } + } + + + + /** + * 修改群信息(群昵称和群公告) + * @param array $data 请求参数 + * @return string JSON响应 + */ + public function CmdChatroomModifyInfo($data = []) + { + try { + // 参数验证 + if (empty($data)) { + return json_encode(['code' => 400, 'msg' => '参数缺失']); + } + + // 验证必要参数 + if (empty($data['wechatChatroomId'])) { + return json_encode(['code' => 400, 'msg' => '群ID不能为空']); + } + + if (empty($data['wechatAccountId'])) { + return json_encode(['code' => 400, 'msg' => '微信账号ID不能为空']); + } + + // 检查是否至少提供了一个修改项 + if (empty($data['chatroomName']) && !isset($data['announce'])) { + return json_encode(['code' => 400, 'msg' => '请至少提供群昵称或群公告中的一个参数']); + } + + + if (!empty($data['chatroomName'])) { + $extra = [ + "chatroomName" => $data['chatroomName'] + ]; + } else { + $extra = [ + "announce" => $data['announce'] + ]; + } + + //chatroomOperateType 4退群 6群公告 5群名称 + $params = [ + "chatroomOperateType" => !empty($data['chatroomName']) ? 6 : 5, + "cmdType" => "CmdChatroomOperate", + "extra" => json_encode($extra,256), + "seq" => time(), + "wechatAccountId" => $data['wechatAccountId'], + "wechatChatroomId" => $data['wechatChatroomId'] + ]; + $message = $this->sendMessage($params,false); + return json_encode(['code' => 200, 'msg' => '修改群信息成功', 'data' => $message]); + } catch (\Exception $e) { + Log::error('修改群信息异常: ' . $e->getMessage()); + return json_encode(['code' => 500, 'msg' => '修改群信息失败: ' . $e->getMessage()]); + } + } + + + + + public function ttttt($data = []) + { + try { + $params = [ + "chatroomOperateType" => $data['chatroomOperateType'], + "cmdType" => "CmdChatroomOperate", + "seq" => time(), + "wechatAccountId" => $data['wechatAccountId'], + "wechatChatroomId" => $data['wechatChatroomId'] + ]; + $message = $this->sendMessage($params,false); + return json_encode(['code' => 200, 'msg' => '234', 'data' => $message]); + } catch (\Exception $e) { + Log::error('修改群信息异常: ' . $e->getMessage()); + return json_encode(['code' => 500, 'msg' => '567567: ' . $e->getMessage()]); + } + } + + +} \ No newline at end of file diff --git a/application/api/controller/WebSocketControllerCopy.php b/application/api/controller/WebSocketControllerCopy.php new file mode 100644 index 0000000..12c516b --- /dev/null +++ b/application/api/controller/WebSocketControllerCopy.php @@ -0,0 +1,559 @@ +400,'msg'=>'参数缺失']); + } + $params = [ + 'grant_type' => 'password', + 'username' => $userData['userName'], + 'password' => $userData['password'] + ]; + + // 调用登录接口获取token + // 设置请求头 + $headerData = ['client:kefu-client']; + $header = setHeader($headerData, '', 'plain'); + $result = requestCurl('https://s2.siyuguanli.com:9991/token', $params, 'POST',$header); + $result_array = handleApiResponse($result); + + if (isset($result_array['access_token']) && !empty($result_array['access_token'])) { + $authorization = $result_array['access_token']; + $this->authorized = $authorization; + $this->accountId = $userData['accountId']; + + } else { + return json_encode(['code'=>400,'msg'=>'获取系统授权信息失败']); + } + }else{ + $this->authorized = $this->request->header('authorization', ''); + $this->accountId = $this->request->param('accountId', ''); + } + + + if (empty($this->authorized) || empty($this->accountId)) { + $data['authorized'] = $this->authorized; + $data['accountId'] = $this->accountId; + return json_encode(['code'=>400,'msg'=>'缺失关键参数']); + } + + //证书 + $context = stream_context_create(); + stream_context_set_option($context, 'ssl', 'verify_peer', false); + stream_context_set_option($context, 'ssl', 'verify_peer_name', false); + //开启WS链接 + $result = [ + "accessToken" => $this->authorized, + "accountId" => $this->accountId, + "client" => "kefu-client", + "cmdType" => "CmdSignIn", + "seq" => 1, + ]; + + + $content = json_encode($result); + $this->client = new Client("wss://s2.siyuguanli.com:9993", + [ + 'filter' => ['text', 'binary', 'ping', 'pong', 'close','receive', 'send'], + 'context' => $context, + 'headers' => [ + 'Sec-WebSocket-Protocol' => 'soap', + 'origin' => 'localhost', + ], + 'timeout' => 86400, + ] + ); + $this->client->send($content); + } + + /************************************ + * 朋友圈相关功能 + ************************************/ + + /** + * 获取指定账号朋友圈信息 + * @param array $data 请求参数 + * @return \think\response\Json + */ + public function getMoments($data = []) + { + + $count = !empty($data['count']) ? $data['count'] : 10; + $wechatAccountId = !empty($data['wechatAccountId']) ? $data['wechatAccountId'] : ''; + $wechatFriendId = !empty($data['id']) ? $data['id'] : ''; + + //过滤消息 + if (empty($wechatAccountId)) { + return json_encode(['code'=>400,'msg'=>'指定账号不能为空']); + } + if (empty($wechatFriendId)) { + return json_encode(['code'=>400,'msg'=>'指定好友不能为空']); + } + $msg = '获取朋友圈信息成功'; + $message = []; + try { + $params = [ + "cmdType" => "CmdFetchMoment", + "count" => $count, + "createTimeSec" => time(), + "isTimeline" => false, + "prevSnsId" => 0, + "wechatAccountId" => $wechatAccountId, + "wechatFriendId" => $wechatFriendId, + "seq" => time(), + ]; + $params = json_encode($params); + //Log::write('WS获取朋友圈信息参数:' . json_encode($params, 256)); + $this->client->send($params); + $message = $this->client->receive(); + //Log::write('WS获取朋友圈信息成功,结果:' . $message); + $message = json_decode($message, 1); + + // 存储朋友圈数据到数据库 + if (isset($message['result']) && !empty($message['result'])) { + $this->saveMomentsToDatabase($message['result'], $wechatAccountId, $wechatFriendId); + } + + //关闭WS链接 + $this->client->close(); + } catch (\Exception $e) { + $msg = $e->getMessage(); + } + + return json_encode(['code'=>200,'msg'=>$msg,'data'=>$message]); + } + + /** + * 朋友圈点赞 + * @return \think\response\Json + */ + public function momentInteract() + { + if ($this->request->isPost()) { + $data = $this->request->param(); + + if (empty($data)) { + return json_encode(['code'=>400,'msg'=>'参数缺失']); + } + $dataArray = $data; + if (!is_array($dataArray)) { + return json_encode(['code'=>400,'msg'=>'数据格式错误']); + } + + //过滤消息 + if (empty($dataArray['snsId'])) { + return json_encode(['code'=>400,'msg'=>'snsId不能为空']); + } + if (empty($dataArray['wechatAccountId'])) { + return json_encode(['code'=>400,'msg'=>'微信id不能为空']); + } + + + $result = [ + "cmdType" => "CmdMomentInteract", + "momentInteractType" => 1, + "seq" => time(), + "snsId" => $dataArray['snsId'], + "wechatAccountId" => $dataArray['wechatAccountId'], + "wechatFriendId" => 0, + ]; + + $result = json_encode($result); + $this->client->send($result); + $message = $this->client->receive(); + $message = json_decode($message, 1); + //关闭WS链接 + $this->client->close(); + //Log::write('WS个人消息发送'); + return json_encode(['code'=>200,'msg'=>'点赞成功','data'=>$message]); + } else { + return json_encode(['code'=>400,'msg'=>'非法请求']); + } + } + + /** + * 朋友圈取消点赞 + * @return \think\response\Json + */ + public function momentCancelInteract() + { + if ($this->request->isPost()) { + $data = $this->request->param(); + + if (empty($data)) { + return json_encode(['code'=>400,'msg'=>'参数缺失']); + } + $dataArray = $data; + if (!is_array($dataArray)) { + return json_encode(['code'=>400,'msg'=>'数据格式错误']); + } + + //过滤消息 + if (empty($dataArray['snsId'])) { + return json_encode(['code'=>400,'msg'=>'snsId不能为空']); + } + if (empty($dataArray['wechatAccountId'])) { + return json_encode(['code'=>400,'msg'=>'微信id不能为空']); + } + + + $result = [ + "CommentId2" => '', + "CommentTime" => 0, + "cmdType" => "CmdMomentCancelInteract", + "optType" => 1, + "seq" => time(), + "snsId" => $dataArray['snsId'], + "wechatAccountId" => $dataArray['wechatAccountId'], + "wechatFriendId" => 0, + ]; + + $result = json_encode($result); + $this->client->send($result); + $message = $this->client->receive(); + $message = json_decode($message, 1); + //关闭WS链接 + $this->client->close(); + //Log::write('WS个人消息发送'); + return json_encode(['code'=>200,'msg'=>'取消点赞成功','data'=>$message]); + } else { + return json_encode(['code'=>400,'msg'=>'非法请求']); + } + } + + /** + * 获取指定账号朋友圈图片地址 + * @return \think\response\Json + */ + public function getMomentSourceRealUrl() + { + if ($this->request->isPost()) { + $data = $this->request->param(); + + if (empty($data)) { + return json_encode(['code'=>400,'msg'=>'参数缺失']); + } + $dataArray = $data; + if (!is_array($dataArray)) { + return json_encode(['code'=>400,'msg'=>'数据格式错误']); + } + //获取数据条数 +// $count = isset($dataArray['count']) ? $dataArray['count'] : 10; + //过滤消息 + if (empty($dataArray['wechatAccountId'])) { + return json_encode(['code'=>400,'msg'=>'指定账号不能为空']); + } + if (empty($dataArray['snsId'])) { + return json_encode(['code'=>400,'msg'=>'指定消息ID不能为空']); + } + if (empty($dataArray['snsUrls'])) { + return json_encode(['code'=>400,'msg'=>'资源信息不能为空']); + } + $msg = '获取朋友圈资源链接成功'; + $message = []; + try { + $params = [ + "cmdType" => $dataArray['type'], + "snsId" => $dataArray['snsId'], + "urls" => $dataArray['snsUrls'], + "wechatAccountId" => $dataArray['wechatAccountId'], + "seq" => time(), + ]; + $params = json_encode($params); + $this->client->send($params); + $message = $this->client->receive(); + //Log::write('WS获取朋友圈图片/视频链接成功,结果:' . json_encode($message, 256)); + //关闭WS链接 + $this->client->close(); + } catch (\Exception $e) { + $msg = $e->getMessage(); + } + + return json_encode(['code'=>200,'msg'=>$msg,'data'=>$message]); + } else { + return json_encode(['code'=>400,'msg'=>'非法请求']); + } + } + + /** + * 保存朋友圈数据到数据库 + * @param array $momentList 朋友圈数据列表 + * @param int $wechatAccountId 微信账号ID + * @param string $wechatFriendId 微信好友ID + * @return bool + */ + protected function saveMomentsToDatabase($momentList, $wechatAccountId, $wechatFriendId) + { + if (empty($momentList) || !is_array($momentList)) { + return false; + } + + try { + foreach ($momentList as $moment) { + // 提取momentEntity中的数据 + $momentEntity = $moment['momentEntity'] ?? []; + + // 检查朋友圈数据是否已存在 + $momentId = Db::table('s2_wechat_moments') + ->where('snsId', $moment['snsId']) + ->where('wechatAccountId', $wechatAccountId) + ->value('id'); + + $dataToSave = [ + 'commentList' => json_encode($moment['commentList'] ?? [], 256), + 'createTime' => $moment['createTime'] ?? 0, + 'likeList' => json_encode($moment['likeList'] ?? [], 256), + 'content' => $momentEntity['content'] ?? '', + 'lat' => $momentEntity['lat'] ?? 0, + 'lng' => $momentEntity['lng'] ?? 0, + 'location' => $momentEntity['location'] ?? '', + 'picSize' => $momentEntity['picSize'] ?? 0, + 'resUrls' => json_encode($momentEntity['resUrls'] ?? [], 256), + 'userName' => $momentEntity['userName'] ?? '', + 'snsId' => $moment['snsId'] ?? '', + 'type' => $moment['type'] ?? 0, + 'title' => $moment['title'] ?? '', + 'coverImage' => $moment['coverImage'] ?? '', + 'update_time' => time() + ]; + + if ($momentId) { + // 如果已存在,则更新数据 + Db::table('s2_wechat_moments')->where('id', $momentId)->update($dataToSave); + } else { + if(empty($wechatFriendId)){ + $wechatFriendId = WechatFriend::where('wechatAccountId', $wechatAccountId)->where('wechatId', $momentEntity['userName'])->value('id'); + } + // 如果不存在,则插入新数据 + $dataToSave['wechatAccountId'] = $wechatAccountId; + $dataToSave['wechatFriendId'] = $wechatFriendId; + $dataToSave['create_time'] = time(); + Db::table('s2_wechat_moments')->insert($dataToSave); + } + } + + //Log::write('朋友圈数据已存入数据库,共' . count($momentList) . '条'); + return true; + } catch (\Exception $e) { + //Log::write('保存朋友圈数据失败:' . $e->getMessage(), 'error'); + return false; + } + } + + /************************************ + * 消息发送相关功能 + ************************************/ + + /** + * 个人消息发送 + * @return \think\response\Json + */ + public function sendPersonal() + { + if ($this->request->isPost()) { + $data = $this->request->param(); + + if (empty($data)) { + return json_encode(['code'=>400,'msg'=>'参数缺失']); + } + $dataArray = $data; + if (!is_array($dataArray)) { + return json_encode(['code'=>400,'msg'=>'数据格式错误']); + } + + //过滤消息 + if (empty($dataArray['content'])) { + return json_encode(['code'=>400,'msg'=>'内容缺失']); + } + if (empty($dataArray['wechatAccountId'])) { + return json_encode(['code'=>400,'msg'=>'微信id不能为空']); + } + if (empty($dataArray['wechatFriendId'])) { + return json_encode(['code'=>400,'msg'=>'接收人不能为空']); + } + + if (empty($dataArray['msgType'])) { + return json_encode(['code'=>400,'msg'=>'类型缺失']); + } + + //消息拼接 msgType(1:文本 3:图片 43:视频 47:动图表情包 49:小程序) + $result = [ + "cmdType" => "CmdSendMessage", + "content" => $dataArray['content'], + "msgSubType" => 0, + "msgType" => $dataArray['msgType'], + "seq" => time(), + "wechatAccountId" => $dataArray['wechatAccountId'], + "wechatChatroomId" => 0, + "wechatFriendId" => $dataArray['wechatFriendId'], + ]; + + $result = json_encode($result); + $this->client->send($result); + $message = $this->client->receive(); + $message = json_decode($message, 1); + //关闭WS链接 + $this->client->close(); + //Log::write('WS个人消息发送'); + return json_encode(['code'=>200,'msg'=>'消息成功发送','data'=>$message]); + //return successJson($message, '消息成功发送'); + } else { + return json_encode(['code'=>400,'msg'=>'非法请求']); + //return errorJson('非法请求'); + } + } + + /** + * 发送群消息 + * @return \think\response\Json + */ + public function sendCommunity() + { + if ($this->request->isPost()) { + $data = $this->request->post(); + if (empty($data)) { + return json_encode(['code'=>400,'msg'=>'参数缺失']); + } + $dataArray = $data; + if (!is_array($dataArray)) { + return json_encode(['code'=>400,'msg'=>'数据格式错误']); + } + + //过滤消息 + if (empty($dataArray['content'])) { + return json_encode(['code'=>400,'msg'=>'内容缺失']); + } + if (empty($dataArray['wechatAccountId'])) { + return json_encode(['code'=>400,'msg'=>'微信id不能为空']); + } + + if (empty($dataArray['msgType'])) { + return json_encode(['code'=>400,'msg'=>'类型缺失']); + } + if (empty($dataArray['wechatChatroomId'])) { + return json_encode(['code'=>400,'msg'=>'群id不能为空']); + } + + $msg = '消息成功发送'; + $message = []; + try { + //消息拼接 msgType(1:文本 3:图片 43:视频 47:动图表情包 49:小程序) + $result = [ + "cmdType" => "CmdSendMessage", + "content" => htmlspecialchars_decode($dataArray['content']), + "msgSubType" => 0, + "msgType" => $dataArray['msgType'], + "seq" => time(), + "wechatAccountId" => $dataArray['wechatAccountId'], + "wechatChatroomId" => $dataArray['wechatChatroomId'], + "wechatFriendId" => 0, + ]; + + $result = json_encode($result); + $this->client->send($result); + $message = $this->client->receive(); + //关闭WS链接 + $this->client->close(); + //Log::write('WS群消息发送'); + //Log::write($message); + $message = json_decode($message, 1); + } catch (\Exception $e) { + $msg = $e->getMessage(); + } + return json_encode(['code'=>200,'msg'=>$msg,'data'=>$message]); + + } else { + return json_encode(['code'=>400,'msg'=>'非法请求']); + //return errorJson('非法请求'); + } + } + + /** + * 发送群消息(内部调用版) + * @param array $data 消息数据 + * @return \think\response\Json + */ + public function sendCommunitys($data = []) + { + if (empty($data)) { + return json_encode(['code'=>400,'msg'=>'参数缺失']); + } + $dataArray = $data; + if (!is_array($dataArray)) { + return json_encode(['code'=>400,'msg'=>'数据格式错误']); + } + + //过滤消息 + if (empty($dataArray['content'])) { + return json_encode(['code'=>400,'msg'=>'内容缺失']); + } + if (empty($dataArray['wechatAccountId'])) { + return json_encode(['code'=>400,'msg'=>'微信id不能为空']); + } + + if (empty($dataArray['msgType'])) { + return json_encode(['code'=>400,'msg'=>'类型缺失']); + } + if (empty($dataArray['wechatChatroomId'])) { + return json_encode(['code'=>400,'msg'=>'群id不能为空']); + } + + $msg = '消息成功发送'; + $message = []; + try { + //消息拼接 msgType(1:文本 3:图片 43:视频 47:动图表情包 49:小程序) + $result = [ + "cmdType" => "CmdSendMessage", + "content" => $dataArray['content'], + "msgSubType" => 0, + "msgType" => $dataArray['msgType'], + "seq" => time(), + "wechatAccountId" => $dataArray['wechatAccountId'], + "wechatChatroomId" => $dataArray['wechatChatroomId'], + "wechatFriendId" => 0, + ]; + + $result = json_encode($result); + $this->client->send($result); + $message = $this->client->receive(); + //关闭WS链接 + $this->client->close(); + //Log::write('WS群消息发送'); + //Log::write($message); + $message = json_decode($message, 1); + } catch (\Exception $e) { + $msg = $e->getMessage(); + } + + return json_encode(['code'=>200,'msg'=>$msg,'data'=>$message]); + } +} \ No newline at end of file diff --git a/application/api/controller/WechatChatroomController.php b/application/api/controller/WechatChatroomController.php new file mode 100644 index 0000000..a1a0f9d --- /dev/null +++ b/application/api/controller/WechatChatroomController.php @@ -0,0 +1,255 @@ +authorization; + if (empty($authorization)) { + return json_encode(['code'=>500,'msg'=>'缺少授权信息']); + } + + try { + // 根据isDel设置对应的isDeleted值 + $isDeleted = ''; + if ($isDel === '0' || $isDel === 0) { + $isDeleted = false; + } elseif ($isDel === '1' || $isDel === 1) { + $isDeleted = true; + } + + // 构建请求参数 + $params = [ + 'keyword' => $data['keyword'] ?? '', + 'wechatAccountKeyword' => $data['wechatAccountKeyword'] ?? '', + 'isDeleted' => $data['isDeleted'] ?? $isDeleted , + 'allotAccountId' => $data['allotAccountId'] ?? '', + 'groupId' => $data['groupId'] ?? '', + 'wechatChatroomId' => $data['wechatChatroomId'] ?? '', + 'memberKeyword' => $data['memberKeyword'] ?? '', + 'pageIndex' => $data['pageIndex'] ?? 0, + 'pageSize' => $data['pageSize'] ?? 20 + ]; + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + // 发送请求获取群聊列表 + $result = requestCurl($this->baseUrl . 'api/WechatChatroom/pagelist', $params, 'GET', $header,'json'); + $response = handleApiResponse($result); + + // 保存数据到数据库 + if (!empty($response['results'])) { + $isUpdate = false; + foreach ($response['results'] as $item) { + $updated = $this->saveChatroom($item); + if($updated && $isDel == 0){ + $isUpdate = true; + } + } + } + + if($isInner){ + return json_encode(['code'=>200,'msg'=>'success','data'=>$response,'isUpdate'=>$isUpdate]); + }else{ + return successJson($response); + } + } catch (\Exception $e) { + if($isInner){ + return json_encode(['code'=>200,'msg'=>'获取微信群聊列表失败' . $e->getMessage()]); + }else{ + return errorJson('获取微信群聊列表失败:' . $e->getMessage()); + } + } + } + + /** + * 保存群聊数据到数据库 + * @param array $item 群聊数据 + */ + private function saveChatroom($item) + { + $data = [ + 'id' => $item['id'], + 'wechatAccountId' => $item['wechatAccountId'], + 'wechatAccountAlias' => $item['wechatAccountAlias'], + 'wechatAccountWechatId' => $item['wechatAccountWechatId'], + 'wechatAccountAvatar' => $item['wechatAccountAvatar'], + 'wechatAccountNickname' => $item['wechatAccountNickname'], + 'chatroomId' => $item['chatroomId'], + 'hasMe' => $item['hasMe'], + 'chatroomOwnerNickname' => isset($item['chatroomOwnerNickname']) ? $item['chatroomOwnerNickname'] : '', + 'chatroomOwnerAvatar' => isset($item['chatroomOwnerAvatar']) ? $item['chatroomOwnerAvatar'] : '', + 'conRemark' => isset($item['conRemark']) ? $item['conRemark'] : '', + 'nickname' => isset($item['nickname']) ? $item['nickname'] : '', + 'pyInitial' => isset($item['pyInitial']) ? $item['pyInitial'] : '', + 'quanPin' => isset($item['quanPin']) ? $item['quanPin'] : '', + 'chatroomAvatar' => isset($item['chatroomAvatar']) ? $item['chatroomAvatar'] : '', + 'members' => is_array($item['members']) ? json_encode($item['members']) : json_encode([]), + 'isDeleted' => isset($item['isDeleted']) ? $item['isDeleted'] : 0, + 'deleteTime' => !empty($item['isDeleted']) ? strtotime($item['deleteTime']) : 0, + 'createTime' => isset($item['createTime']) ? strtotime($item['createTime']) : 0, + 'accountId' => isset($item['accountId']) ? $item['accountId'] : 0, + 'accountUserName' => isset($item['accountUserName']) ? $item['accountUserName'] : '', + 'accountRealName' => isset($item['accountRealName']) ? $item['accountRealName'] : '', + 'accountNickname' => isset($item['accountNickname']) ? $item['accountNickname'] : '', + 'groupId' => isset($item['groupId']) ? $item['groupId'] : 0, + 'updateTime' => time() + ]; + + // 使用chatroomId和wechatAccountId的组合作为唯一性判断 + $chatroom = WechatChatroomModel::where('id',$item['id'])->find(); + + if ($chatroom) { + $chatroom->save($data); + return true; + } else { + WechatChatroomModel::create($data); + return false; + } + + // // 同时保存群成员数据 + // if (!empty($item['members'])) { + // foreach ($item['members'] as $member) { + // $this->saveChatroomMember($member, $item['chatroomId']); + // } + // } + } + + /** + * 获取群成员列表 + * @param string $wechatChatroomId 微信群ID + * @return \think\response\Json + */ + public function listChatroomMember($wechatChatroomId = '',$chatroomId = '',$isInner = false) + { + // 获取授权token + $authorization = trim($this->request->header('authorization', $this->authorization)); + $wechatChatroomId = !empty($wechatChatroomId) ? $wechatChatroomId : $this->request->param('id', ''); + $chatroomId = !empty($chatroomId) ? $chatroomId : $this->request->param('chatroomId', ''); + + + if (empty($authorization)) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'缺少授权信息']); + }else{ + return errorJson('缺少授权信息'); + } + } + + if (empty($wechatChatroomId)) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'群ID不能为空']); + }else{ + return errorJson('群ID不能为空'); + } + } + + try { + // 构建请求参数 + $params = [ + 'wechatChatroomId' => $wechatChatroomId + ]; + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'plain'); + + // 发送请求获取群成员列表 + $result = requestCurl($this->baseUrl . 'api/WechatChatroom/listChatroomMember', $params, 'GET', $header); + $response = handleApiResponse($result); + + // 保存数据到数据库 + if (!empty($response)) { + foreach ($response as $item) { + $this->saveChatroomMember($item, $chatroomId); + } + } + + if($isInner){ + return json_encode(['code'=>200,'msg'=>'success','data'=>$response]); + }else{ + return successJson($response); + } + } catch (\Exception $e) { + if($isInner){ + return json_encode(['code'=>500,'msg'=>'获取群成员列表失败:' . $e->getMessage()]); + }else{ + return errorJson('获取群成员列表失败:' . $e->getMessage()); + } + } + } + + /** + * 保存群成员数据到数据库 + * @param array $item 群成员数据 + * @param string $wechatChatroomId 微信群ID + */ + private function saveChatroomMember($item, $wechatChatroomId) + { + $data = [ + 'chatroomId' => $wechatChatroomId, + 'wechatId' => isset($item['wechatId']) ? $item['wechatId'] : '', + 'nickname' => isset($item['nickname']) ? $item['nickname'] : '', + 'avatar' => isset($item['avatar']) ? $item['avatar'] : '', + 'conRemark' => isset($item['conRemark']) ? $item['conRemark'] : '', + 'alias' => isset($item['alias']) ? $item['alias'] : '', + 'friendType' => isset($item['friendType']) ? $item['friendType'] : false, + 'updateTime' => time() + ]; + + // 使用chatroomId和wechatId的组合作为唯一性判断 + $member = WechatChatroomMemberModel::where([ + ['chatroomId', '=', $wechatChatroomId], + ['wechatId', '=', $item['wechatId']] + ])->find(); + + if ($member) { + $member->savea($data); + } else { + $data['createTime'] = time(); + WechatChatroomMemberModel::create($data); + } + } + + /** + * 同步微信群聊数据 + * 此方法用于手动触发微信群聊数据同步任务 + * @return \think\response\Json + */ + public function syncChatrooms() + { + try { + // 获取请求参数 + $pageIndex = $this->request->param('pageIndex', 0); + $pageSize = $this->request->param('pageSize', 100); + $keyword = $this->request->param('keyword', ''); + $wechatAccountKeyword = $this->request->param('wechatAccountKeyword', ''); + $isDeleted = $this->request->param('isDeleted', ''); + + // 添加同步任务到队列 + $result = WechatChatroomJob::addSyncTask($pageIndex, $pageSize, $keyword, $wechatAccountKeyword, $isDeleted); + + if ($result) { + return successJson([], '微信群聊同步任务已添加到队列'); + } else { + return errorJson('添加同步任务失败'); + } + } catch (\Exception $e) { + return errorJson('添加同步任务异常:' . $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/application/api/controller/WechatController.php b/application/api/controller/WechatController.php new file mode 100644 index 0000000..b3903bc --- /dev/null +++ b/application/api/controller/WechatController.php @@ -0,0 +1,295 @@ +request->header('authorization', $this->authorization)); + if (empty($authorization)) { + if ($isInner) { + return json_encode(['code' => 500, 'msg' => '缺少授权信息']); + } else { + return errorJson('缺少授权信息'); + } + } + + try { + // 构建请求参数 + $params = [ + 'wechatAlive' => $this->request->param('wechatAlive', ''), + 'keyword' => $this->request->param('keyword', ''), + 'groupId' => $this->request->param('groupId', ''), + 'departmentId' => $this->request->param('departmentId', ''), + 'hasDevice' => $this->request->param('hasDevice', ''), + 'deviceGroupId' => $this->request->param('deviceGroupId', ''), + 'containSubDepartment' => $this->request->param('containSubDepartment', 'false'), + 'pageIndex' => !empty($pageIndex) ? $pageIndex : $this->request->param('pageIndex', 0), + 'pageSize' => !empty($pageSize) ? $pageSize : $this->request->param('pageSize', 10) + ]; + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'plain'); + + // 发送请求获取基本信息 + $result = requestCurl($this->baseUrl . 'api/WechatAccount/list', $params, 'GET', $header); + $response = handleApiResponse($result); + // 保存基本数据到数据库 + if (!empty($response['results'])) { + foreach ($response['results'] as $item) { + $this->saveWechatAccount($item); + } + + // 获取并更新微信账号状态信息 + $this->getListTenantWechatPartial($authorization); + } + + + + if ($isInner) { + return json_encode(['code' => 200, 'msg' => '获取微信账号列表成功', 'data' => $response]); + } else { + return successJson($response); + } + } catch (\Exception $e) { + if ($isInner) { + return json_encode(['code' => 500, 'msg' => '获取微信账号列表失败:' . $e->getMessage()]); + } else { + return errorJson('获取微信账号列表失败:' . $e->getMessage()); + } + } + } + + /** + * 获取微信账号状态信息 + * + * @param string $authorization 授权token + * @param int $pageIndex 页码,默认为1 + * @param int $pageSize 每页数量,默认为40 + * @return \think\response\Json|void + */ + public function getListTenantWechatPartial($authorization = '', $pageIndex = 1, $pageSize = 40) + { + // 获取授权token(如果未传入) + if (empty($authorization)) { + $authorization = trim($this->request->header('authorization', $this->authorization)); + if (empty($authorization)) { + return errorJson('缺少授权信息'); + } + } + + try { + // 从数据库获取微信账号和设备信息 + $wechatList = Db::table('s2_wechat_account') + ->where('imei', 'not null') + ->page($pageIndex, $pageSize) + ->select(); + if (empty($wechatList)) { + return; + } + + // 构造请求参数 + $wechatAccountIds = []; + $deviceIds = []; + $accountIds = []; + + foreach ($wechatList as $item) { + $wechatAccountIds[] = $item['id']; + $deviceIds[] = $item['currentDeviceId'] ?: 0; + $accountIds[] = $item['deviceAccountId'] ?: 0; + } + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + $params = [ + 'wechatAccountIdsStr' => json_encode($wechatAccountIds), + 'deviceIdsStr' => json_encode($deviceIds), + 'accountIdsStr' => json_encode($accountIds), + 'groupId' => '' + ]; + // 发送请求获取状态信息 + $result = requestCurl($this->baseUrl . 'api/WechatAccount/listTenantWechatPartial', $params, 'GET', $header,'json'); + $response = handleApiResponse($result); + // 如果请求成功并返回数据,则更新数据库 + if (!empty($response)) { + $this->batchUpdateWechatAccounts($response); + } + + // 递归调用获取下一页数据 + $this->getListTenantWechatPartial($authorization, $pageIndex + 1, $pageSize); + + } catch (\Exception $e) { + if (empty($authorization)) { // 只有作为独立API调用时才返回 + return json(['code' => 500, 'msg' => '获取失败:' . $e->getMessage()]); + } + } + } + + /** + * 批量更新微信账号数据 + * + * @param array $data 接口返回的数据 + */ + private function batchUpdateWechatAccounts($data) + { + // 更新微信账号信息 + if (!empty($data['totalFriend'])) { + // 遍历所有微信账号ID + $wechatIds = array_keys($data['totalFriend']); + foreach ($wechatIds as $wechatId) { + // 构建更新数据 + $updateData = [ + 'maleFriend' => $data['maleFriend'][$wechatId] ?? 0, + 'femaleFriend' => $data['femaleFriend'][$wechatId] ?? 0, + 'unknowFriend' => $data['unknowFriend'][$wechatId] ?? 0, + 'totalFriend' => $data['totalFriend'][$wechatId] ?? 0, + 'yesterdayMsgCount' => $data['yesterdayMsgCount'][$wechatId] ?? 0, + 'sevenDayMsgCount' => $data['sevenDayMsgCount'][$wechatId] ?? 0, + 'thirtyDayMsgCount' => $data['thirtyDayMsgCount'][$wechatId] ?? 0, + 'wechatAlive' => isset($data['wechatAlive'][$wechatId]) ? (int)$data['wechatAlive'][$wechatId] : 0, + 'updateTime' => time() + ]; + + if (!empty($updateData['wechatAlive'])) { + $updateData['wechatAliveTime'] = time(); + } + + + // 更新数据库 + Db::table('s2_wechat_account') + ->where('id', $wechatId) + ->update($updateData); + } + } + + // 更新设备状态 + if (!empty($data['deviceAlive'])) { + foreach ($data['deviceAlive'] as $deviceId => $isAlive) { + // 更新微信账号的设备状态 + Db::table('s2_wechat_account') + ->where('currentDeviceId', $deviceId) + ->update([ + 'deviceAlive' => (int)$isAlive, + 'updateTime' => time() + ]); + + // 更新设备表的状态 + Db::table('s2_device') + ->where('id', $deviceId) + ->update([ + 'alive' => (int)$isAlive, + 'updateTime' => time() + ]); + } + } + } + + /** + * 保存微信账号基本数据到数据库 + * + * @param array $item 微信账号数据 + */ + private function saveWechatAccount($item) + { + // 处理时间字段 + $createTime = isset($item['createTime']) ? strtotime($item['createTime']) : 0; + $deleteTime = !empty($item['isDeleted']) ? strtotime($item['deleteTime']) : 0; + + // 构建数据 + $data = [ + 'id' => $item['id'], + 'wechatId' => $item['wechatId'] ?? '', + 'deviceAccountId' => $item['deviceAccountId'] ?? 0, + 'imei' => $item['imei'] ?? '', + 'deviceMemo' => $item['deviceMemo'] ?? '', + 'accountUserName' => $item['accountUserName'] ?? '', + 'accountRealName' => $item['accountRealName'] ?? '', + 'accountNickname' => $item['accountNickname'] ?? '', + 'wechatGroupName' => $item['wechatGroupName'] ?? '', + 'alias' => $item['alias'] ?? '', + 'tenantId' => $item['tenantId'] ?? 0, + 'nickname' => $item['nickname'] ?? '', + 'avatar' => $item['avatar'] ?? '', + 'gender' => $item['gender'] ?? 0, + 'region' => $item['region'] ?? '', + 'signature' => $item['signature'] ?? '', + 'bindQQ' => $item['bindQQ'] ?? '', + 'bindEmail' => $item['bindEmail'] ?? '', + 'bindMobile' => $item['bindMobile'] ?? '', + 'currentDeviceId' => $item['currentDeviceId'] ?? 0, + 'isDeleted' => $item['isDeleted'] ?? 0, + 'groupId' => $item['groupId'] ?? 0, + 'memo' => $item['memo'] ?? '', + 'wechatVersion' => $item['wechatVersion'] ?? '', + 'labels' => !empty($item['labels']) ? json_encode($item['labels']) : json_encode([]), + 'createTime' => $createTime, + 'deleteTime' => $deleteTime, + 'updateTime' => time() + ]; + + // 保存或更新数据 + $account = WechatAccountModel::where('id', $item['id'])->find(); + if ($account) { + $account->save($data); + } else { + WechatAccountModel::create($data); + } + } + + + public function chatroomCreate($data = []) + { + + $authorization = $this->authorization; + + if (empty($authorization)) { + return json_encode(['code' => 500, 'msg' => '缺少授权信息']); + } + + try { + // 设置请求头 + $headerData = ['Client:system']; + $header = setHeader($headerData, $authorization,'json'); + $params = [ + "chatroomOperateType" => 7, + "extra" => "{chatroomName:{$data['chatroomName']}}", + "wechatAccountId" => $data['wechatAccountId'], + "wechatChatroomId" => 0, + "wechatFriendIds" => $data['wechatFriendIds'] + ]; + + // 发送请求获取状态信息 + $result = requestCurl($this->baseUrl . 'api/WechatChatroom/chatroomOperate', $params, 'POST', $header,'json'); + $response = handleApiResponse($result); + if (!empty($response)) { + return json_encode(['code' => 500, 'msg' =>$response]); + }else{ + return json_encode(['code' => 200, 'msg' =>'成功']); + } + } catch (\Exception $e) { + if (empty($authorization)) { // 只有作为独立API调用时才返回 + return json_encode(['code' => 500, 'msg' => '获取失败:' . $e->getMessage()]); + } + } + } + + +} \ No newline at end of file diff --git a/application/api/controller/WechatFriendController.php b/application/api/controller/WechatFriendController.php new file mode 100644 index 0000000..18fbe43 --- /dev/null +++ b/application/api/controller/WechatFriendController.php @@ -0,0 +1,165 @@ +request->header('authorization', $this->authorization)); + if (empty($authorization)) { + if ($isInner) { + return json_encode(['code' => 500, 'msg' => '缺少授权信息']); + } else { + return errorJson('缺少授权信息'); + } + } + + $pageIndex = !empty($data['pageIndex']) ? $data['pageIndex'] : ''; + $pageSize = !empty($data['pageSize']) ? $data['pageSize'] : ''; + $preFriendId = !empty($data['preFriendId']) ? $data['preFriendId'] : ''; + $friendKeyword = !empty($data['friendKeyword']) ? $data['friendKeyword'] : ''; + $wechatAccountKeyword = !empty($data['wechatAccountKeyword']) ? $data['wechatAccountKeyword'] : ''; + + + try { + // 初始化isUpdate标志为false + $isUpdate = false; + + // 根据isDel设置对应的isDeleted值 + $isDeleted = null; // 默认值 + if ($isDel == '0' || $isDel == 0) { + $isDeleted = false; + } elseif ($isDel == '1' || $isDel == 1) { + $isDeleted = true; + } + + // 构建请求参数 + $params = [ + 'accountKeyword' => '', + 'addFrom' => '[]', + 'allotAccountId' => input('allotAccountId', ''), + 'containSubDepartment' => false, + 'departmentId' => '', + 'extendFields' => '{}', + 'gender' => '', + 'groupId' => null, + 'isDeleted' => $isDeleted, + 'isPass' => null, + 'keyword' => input('keyword', ''), + 'labels' => '[]', + 'pageIndex' => !empty($pageIndex) ? $pageIndex : input('pageIndex', 0), + 'pageSize' => !empty($pageSize) ? $pageSize : input('pageSize', 20), + 'preFriendId' => !empty($preFriendId) ? $preFriendId : input('preFriendId', ''), + 'friendKeyword' => !empty($friendKeyword) ? $friendKeyword : input('friendKeyword', ''), + 'wechatAccountKeyword' => !empty($wechatAccountKeyword) ? $wechatAccountKeyword : input('wechatAccountKeyword', '') + ]; + + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization); + + // 发送请求获取好友列表 + $result = requestCurl($this->baseUrl . 'api/WechatFriend/friendlistData', $params, 'POST', $header, 'json'); + $response = handleApiResponse($result); + + // 保存数据到数据库 + if (is_array($response)) { + $isUpdate = false; + foreach ($response as $item) { + $updated = $this->saveFriend($item); + if($updated && $isDel == 0){ + $isUpdate = true; + } + } + } + + if ($isInner) { + return json_encode(['code' => 200, 'msg' => 'success', 'data' => $response, 'isUpdate' => $isUpdate]); + } else { + return successJson($response); + } + + } catch (\Exception $e) { + if ($isInner) { + return json_encode(['code' => 500, 'msg' => '获取微信好友列表失败:' . $e->getMessage()]); + } else { + return errorJson('获取微信好友列表失败:' . $e->getMessage()); + } + } + } + + /** + * 保存微信好友数据到数据库 + * @param array $item 微信好友数据 + * @return bool 是否创建或更新了记录 + */ + private function saveFriend($item) + { + $data = [ + 'id' => $item['id'], + 'wechatAccountId' => $item['wechatAccountId'], + 'alias' => $item['alias'], + 'wechatId' => $item['wechatId'], + 'conRemark' => $item['conRemark'], + 'nickname' => $item['nickname'], + 'pyInitial' => $item['pyInitial'], + 'quanPin' => $item['quanPin'], + 'avatar' => $item['avatar'], + 'gender' => $item['gender'], + 'region' => $item['region'], + 'addFrom' => $item['addFrom'], + 'labels' => is_array($item['labels']) ? json_encode($item['labels']) : json_encode([]), + 'siteLabels' => json_encode([]), + 'signature' => $item['signature'], + 'isDeleted' => $item['isDeleted'], + 'isPassed' => $item['isPassed'], + 'deleteTime' => !empty($item['isDeleted']) ? strtotime($item['deleteTime']) : 0, + 'accountId' => $item['accountId'], + 'extendFields' => is_array($item['extendFields']) ? json_encode($item['extendFields']) : json_encode([]), + 'accountUserName' => $item['accountUserName'], + 'accountRealName' => $item['accountRealName'], + 'accountNickname' => $item['accountNickname'], + 'ownerAlias' => $item['ownerAlias'], + 'ownerWechatId' => $item['ownerWechatId'], + 'ownerNickname' => $item['ownerNickname'], + 'ownerAvatar' => $item['ownerAvatar'], + 'phone' => $item['phone'], + 'thirdParty' => is_array($item['thirdParty']) ? json_encode($item['thirdParty']) : json_encode([]), + 'groupId' => $item['groupId'], + 'passTime' => !empty($item['isPassed']) && $item['passTime'] != '0001-01-01T00:00:00' ? strtotime($item['passTime']) : 0, + 'additionalPicture' => $item['additionalPicture'], + 'desc' => $item['desc'], + 'country' => $item['country'], + 'privince' => isset($item['privince']) ? $item['privince'] : '', + 'city' => isset($item['city']) ? $item['city'] : '', + 'createTime' => isset($item['createTime']) ? strtotime($item['createTime']) : 0, + 'updateTime' => time() + ]; + + // 使用ID作为唯一性判断 + $friend = WechatFriendModel::where('id', $item['id'])->find(); + + if ($friend) { + unset($data['siteLabels']); + $friend->save($data); + return true; + } else { + WechatFriendModel::create($data); + return false; + } + } +} \ No newline at end of file diff --git a/application/api/model/AllotRuleModel.php b/application/api/model/AllotRuleModel.php new file mode 100644 index 0000000..5fa4b6d --- /dev/null +++ b/application/api/model/AllotRuleModel.php @@ -0,0 +1,11 @@ + 'integer', + 'tenantId' => 'integer', + 'count' => 'integer', + 'createTime' => 'integer', + 'updateTime' => 'integer' + ]; +} \ No newline at end of file diff --git a/application/api/model/DeviceModel.php b/application/api/model/DeviceModel.php new file mode 100644 index 0000000..168b129 --- /dev/null +++ b/application/api/model/DeviceModel.php @@ -0,0 +1,10 @@ + ['ownerWechatId', 'wechatId','wechatAccountId'] // uk_owner_wechat_account 是数据库中组合唯一键的名称 + ];*/ +} \ No newline at end of file diff --git a/application/api/model/WechatMessageModel.php b/application/api/model/WechatMessageModel.php new file mode 100644 index 0000000..59c937e --- /dev/null +++ b/application/api/model/WechatMessageModel.php @@ -0,0 +1,11 @@ +middleware(['jwt']); + + +// 客服登录 +Route::group('v1/kefu', function () { + Route::post('login', 'app\chukebao\controller\LoginController@index'); // 登录 +}); + + + + + +return []; \ No newline at end of file diff --git a/application/chukebao/controller/AccountsController.php b/application/chukebao/controller/AccountsController.php new file mode 100644 index 0000000..d65e998 --- /dev/null +++ b/application/chukebao/controller/AccountsController.php @@ -0,0 +1,71 @@ +getUserInfo('companyId'); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 401); + } + + if (empty($companyId)) { + return ResponseHelper::error('请先登录', 401); + } + + $page = max(1, intval($this->request->param('page', 1))); + $limit = max(1, intval($this->request->param('limit', 10))); + $keyword = trim((string)$this->request->param('keyword', '')); + + $query = Db::table('s2_company_account') + ->alias('a') + ->join('users u', 'a.id = u.s2_accountId') + ->where([ + ['a.departmentId', '=', $companyId], + ['a.status', '=', 0], + ]) + ->whereNotLike('a.userName', '%_offline') + ->whereNotLike('a.userName', '%_delete'); + + if ($keyword !== '') { + $query->where(function ($subQuery) use ($keyword) { + $likeKeyword = '%' . $keyword . '%'; + $subQuery->whereLike('a.userName', $likeKeyword) + ->whereOrLike('a.realName', $likeKeyword) + ->whereOrLike('a.nickname', $likeKeyword); + }); + } + + $total = (clone $query)->count(); + $list = $query->field([ + 'a.id', + 'u.id as uid', + 'a.userName', + 'a.realName', + 'a.nickname', + 'a.departmentId', + 'a.departmentName', + 'a.avatar' + ]) + ->order('a.id', 'desc') + ->page($page, $limit) + ->select(); + + + + return ResponseHelper::success([ + 'total' => $total, + 'list' => $list, + ]); + } +} \ No newline at end of file diff --git a/application/chukebao/controller/AiChatController.php b/application/chukebao/controller/AiChatController.php new file mode 100644 index 0000000..7b4dd78 --- /dev/null +++ b/application/chukebao/controller/AiChatController.php @@ -0,0 +1,832 @@ +validateAndInitParams(); + + if ($params === false) { + return ResponseHelper::error('参数验证失败'); + } + + // 并发控制:检查并处理同一用户的重复请求 + $this->requestKey = "aichat_{$params['friendId']}_{$params['wechatAccountId']}"; + $this->requestId = uniqid('req_', true); + + $concurrentCheck = $this->handleConcurrentRequest($params); + if ($concurrentCheck !== true) { + return $concurrentCheck; // 返回错误响应 + } + + $this->currentStep = 1; + + // 2. 验证Tokens余额 + $this->updateRequestStep(2); + if ($this->isRequestCanceled()) { + return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消'); + } + $hasBalance = $this->checkTokensBalance($params['companyId']); + + if (!$hasBalance) { + $this->clearRequestCache(); + return ResponseHelper::error('Tokens余额不足,请充值后再试'); + } + + // 3. 获取AI配置 + $this->updateRequestStep(3); + if ($this->isRequestCanceled()) { + return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消'); + } + $setting = $this->getAiSettings($params['companyId']); + + if (!$setting) { + $this->clearRequestCache(); + return ResponseHelper::error('未找到AI配置信息,请先配置AI策略'); + } + + // 4. 获取好友AI设置 + $this->updateRequestStep(4); + if ($this->isRequestCanceled()) { + return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消'); + } + $friendSettings = $this->getFriendSettings($params['companyId'], $params['friendId']); + + if (!$friendSettings) { + $this->clearRequestCache(); + return ResponseHelper::error('该好友未配置或未开启AI功能'); + } + + // 5. 确保会话存在 + $this->updateRequestStep(5); + if ($this->isRequestCanceled()) { + return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消'); + } + $conversationId = $this->ensureConversation($friendSettings, $setting, $params); + + if (empty($conversationId)) { + $this->clearRequestCache(); + return ResponseHelper::error('创建会话失败'); + } + + // 6. 获取历史消息 + $this->updateRequestStep(6); + if ($this->isRequestCanceled()) { + return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消'); + } + $msgData = $this->getHistoryMessages($params['friendId'], $friendSettings); + + // 7. 创建AI对话(从这步开始需要保存对话ID以便取消) + $this->updateRequestStep(7); + if ($this->isRequestCanceled($conversationId, null)) { + return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消'); + } + $chatId = $this->createAiChat($setting, $friendSettings, $msgData); + + if (empty($chatId)) { + $this->clearRequestCache(); + return ResponseHelper::error('创建对话失败'); + } + + // 保存对话ID到缓存,以便新请求可以取消 + $this->updateRequestStep(7, $conversationId, $chatId); + + // 8. 等待AI处理完成(轮询) + $this->updateRequestStep(8, $conversationId, $chatId); + if ($this->isRequestCanceled($conversationId, $chatId)) { + return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消'); + } + $chatResult = $this->waitForChatCompletion($conversationId, $chatId); + + if (!$chatResult['success']) { + $this->clearRequestCache(); + return ResponseHelper::error($chatResult['error']); + } + + $chatResult = $chatResult['data']; + + // 9. 扣除Tokens + $this->updateRequestStep(9, $conversationId, $chatId); + if ($this->isRequestCanceled($conversationId, $chatId)) { + return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消'); + } + $this->consumeTokens($chatResult, $params, $friendSettings); + + // 10. 获取对话消息 + $this->updateRequestStep(10, $conversationId, $chatId); + if ($this->isRequestCanceled($conversationId, $chatId)) { + return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消'); + } + $messages = $this->getChatMessages($conversationId, $chatId); + + if (!$messages) { + return ResponseHelper::error('获取对话消息失败'); + } + + // 筛选type为answer的消息(AI回复的内容) + $answerContent = ''; + foreach ($messages as $msg) { + if (isset($msg['type']) && $msg['type'] === 'answer') { + $answerContent = $msg['content'] ?? ''; + break; + } + } + + if (empty($answerContent)) { + Log::warning('未找到AI回复内容,messages: ' . json_encode($messages)); + return ResponseHelper::error('未获取到AI回复内容'); + } + + // 清理请求缓存 + $this->clearRequestCache(); + + // 返回结果 + return ResponseHelper::success(['content' => $answerContent], '对话成功'); + + } catch (\Exception $e) { + Log::error('AI聊天异常:' . $e->getMessage()); + + // 清理请求缓存 + $this->clearRequestCache(); + + return ResponseHelper::error('系统异常:' . $e->getMessage()); + } + } + + /** + * 取消AI对话 + * 取消当前正在进行的AI对话请求 + * + * @return \think\response\Json + */ + public function cancel() + { + try { + // 获取参数 + $friendId = $this->request->param('friendId', ''); + $wechatAccountId = $this->request->param('wechatAccountId', ''); + + if (empty($wechatAccountId) || empty($friendId)) { + return ResponseHelper::error('参数缺失'); + } + + // 生成缓存键 + $requestKey = "aichat_{$friendId}_{$wechatAccountId}"; + + // 获取缓存数据 + $cacheData = Cache::get($requestKey); + + if (!$cacheData) { + return ResponseHelper::error('当前没有正在进行的AI对话'); + } + + $requestId = $cacheData['request_id'] ?? ''; + $step = $cacheData['step'] ?? 0; + $conversationId = $cacheData['conversation_id'] ?? ''; + $chatId = $cacheData['chat_id'] ?? ''; + + Log::info("手动取消AI对话 - 请求ID: {$requestId}, 步骤: {$step}"); + + // 如果已经到达步骤7或之后,需要调用取消API + if ($step >= 7 && !empty($conversationId) && !empty($chatId)) { + try { + $cozeAI = new CozeAI(); + $cancelResult = $cozeAI->cancelConversationChat([ + 'conversation_id' => $conversationId, + 'chat_id' => $chatId, + ]); + + $result = json_decode($cancelResult, true); + if ($result['code'] != 200) { + Log::error("调用取消API失败 - conversation_id: {$conversationId}, chat_id: {$chatId}, 错误: " . ($result['msg'] ?? '未知错误')); + } else { + Log::info("成功调用取消API - conversation_id: {$conversationId}, chat_id: {$chatId}"); + } + } catch (\Exception $e) { + Log::error("调用取消API异常:" . $e->getMessage()); + } + } + + // 清理缓存 + Cache::rm($requestKey); + Log::info("已清理AI对话缓存 - 请求ID: {$requestId}"); + + return ResponseHelper::success([ + 'canceled_request_id' => $requestId, + 'step' => $step + ], 'AI对话已取消'); + + } catch (\Exception $e) { + Log::error('取消AI对话异常:' . $e->getMessage()); + return ResponseHelper::error('系统异常:' . $e->getMessage()); + } + } + + /** + * 验证和初始化参数 + * + * @return array|false + */ + private function validateAndInitParams() + { + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + $friendId = $this->request->param('friendId', ''); + $wechatAccountId = $this->request->param('wechatAccountId', ''); + + if (empty($wechatAccountId) || empty($friendId)) { + return false; + } + + return [ + 'userId' => $userId, + 'companyId' => $companyId, + 'friendId' => $friendId, + 'wechatAccountId' => $wechatAccountId + ]; + } + + /** + * 检查Tokens余额 + * + * @param int $companyId 公司ID + * @return bool + */ + private function checkTokensBalance($companyId) + { + $tokens = TokensCompany::where(['companyId' => $companyId])->value('tokens'); + return !empty($tokens) && $tokens > 1000; + } + + /** + * 获取AI配置 + * + * @param int $companyId 公司ID + * @return AiSettings|null + */ + private function getAiSettings($companyId) + { + return AiSettings::where(['companyId' => $companyId])->find(); + } + + /** + * 获取好友AI设置 + * + * @param int $companyId 公司ID + * @param string $friendId 好友ID + * @return FriendSettings|null + */ + private function getFriendSettings($companyId, $friendId) + { + $friendSettings = FriendSettings::where([ + 'companyId' => $companyId, + 'friendId' => $friendId + ])->find(); + + if (empty($friendSettings) || $friendSettings->type == 0) { + return null; + } + + return $friendSettings; + } + + /** + * 确保会话存在 + * + * @param FriendSettings $friendSettings 好友设置 + * @param AiSettings $setting AI设置 + * @param array $params 参数 + * @return string|null 会话ID + */ + private function ensureConversation($friendSettings, $setting, $params) + { + if (!empty($friendSettings->conversationId)) { + return $friendSettings->conversationId; + } + + // 创建新会话 + $cozeAI = new CozeAI(); + $data = [ + 'bot_id' => $setting->botId, + 'name' => '与好友' . $params['friendId'] . '的对话', + 'meta_data' => [ + 'friendId' => (string)$friendSettings->friendId, + 'wechatAccountId' => (string)$params['wechatAccountId'], + ], + ]; + + $res = $cozeAI->createConversation($data); + $res = json_decode($res, true); + + if ($res['code'] != 200) { + Log::error('创建会话失败:' . ($res['msg'] ?? '未知错误')); + return null; + } + + // 保存会话ID + $conversationId = $res['data']['id']; + $friendSettings->conversationId = $conversationId; + $friendSettings->conversationTime = time(); + $friendSettings->save(); + return $conversationId; + } + + /** + * 获取历史消息 + * + * @param string $friendId 好友ID + * @param FriendSettings $friendSettings 好友设置 + * @return array + */ + private function getHistoryMessages($friendId, $friendSettings) + { + $msgData = []; + + // 会话创建时间小于1分钟,加载最近10条消息 + if ($friendSettings->conversationTime >= time() - 60) { + $messages = Db::table('s2_wechat_message') + ->where('wechatFriendId', $friendId) + ->where('msgType', '<', 50) + ->order('wechatTime desc') + ->field('id,content,msgType,isSend,wechatTime') + ->limit(10) + ->select(); + + // 按时间正序排列 + usort($messages, function ($a, $b) { + return $a['wechatTime'] <=> $b['wechatTime']; + }); + + // 处理聊天数据 + foreach ($messages as $val) { + if (empty($val['content'])) { + continue; + } + + $msg = [ + 'role' => empty($val['isSend']) ? 'user' : 'assistant', + 'content' => $val['content'], + 'type' => empty($val['isSend']) ? 'question' : 'answer', + 'content_type' => 'text' + ]; + $msgData[] = $msg; + } + } else { + // 只加载最新一条用户消息 + $message = Db::table('s2_wechat_message') + ->where('wechatFriendId', $friendId) + ->where('msgType', '<', 50) + ->where('isSend', 0) + ->order('wechatTime desc') + ->field('id,content,msgType,isSend,wechatTime') + ->find(); + + if (!empty($message) && !empty($message['content'])) { + $msgData[] = [ + 'role' => 'user', + 'content' => $message['content'], + 'type' => 'question', + 'content_type' => 'text' + ]; + } + } + + return $msgData; + } + + /** + * 创建AI对话 + * + * @param AiSettings $setting AI设置 + * @param FriendSettings $friendSettings 好友设置 + * @param array $msgData 消息数据 + * @return string|null 对话ID + */ + private function createAiChat($setting, $friendSettings, $msgData) + { + $cozeAI = new CozeAI(); + $data = [ + 'bot_id' => $setting->botId, + 'uid' => $friendSettings->friendId, + 'conversation_id' => $friendSettings->conversationId, + 'question' => $msgData, + ]; + + $res = $cozeAI->createChat($data); + $res = json_decode($res, true); + + if ($res['code'] != 200) { + Log::error('创建对话失败:' . ($res['msg'] ?? '未知错误')); + return null; + } + + return $res['data']['id']; + } + + /** + * 等待AI处理完成(轮询机制) + * + * @param string $conversationId 会话ID + * @param string $chatId 对话ID + * @return array ['success' => bool, 'data' => array|null, 'error' => string] + */ + private function waitForChatCompletion($conversationId, $chatId) + { + $cozeAI = new CozeAI(); + $retryCount = 0; + + while ($retryCount < self::MAX_RETRY_TIMES) { + // 获取对话状态 + $res = $cozeAI->getConversationChat([ + 'conversation_id' => $conversationId, + 'chat_id' => $chatId, + ]); + $res = json_decode($res, true); + + if ($res['code'] != 200) { + $errorMsg = 'AI接口调用失败:' . ($res['msg'] ?? '未知错误'); + Log::error($errorMsg); + return ['success' => false, 'data' => null, 'error' => $errorMsg]; + } + + $status = $res['data']['status'] ?? ''; + + // 处理不同的状态 + switch ($status) { + case self::STATUS_COMPLETED: + // 对话完成,返回结果 + return ['success' => true, 'data' => $res['data'], 'error' => '']; + + case self::STATUS_IN_PROGRESS: + case self::STATUS_CREATED: + // 继续等待 + $retryCount++; + usleep(self::RETRY_INTERVAL); + break; + + case self::STATUS_FAILED: + $errorMsg = 'AI对话处理失败'; + Log::error($errorMsg . ',chat_id: ' . $chatId); + return ['success' => false, 'data' => null, 'error' => $errorMsg]; + + case self::STATUS_CANCELED: + $errorMsg = 'AI对话已被取消'; + Log::error($errorMsg . ',chat_id: ' . $chatId); + return ['success' => false, 'data' => null, 'error' => $errorMsg]; + + case self::STATUS_REQUIRES_ACTION: + $errorMsg = 'AI对话需要进一步处理'; + Log::warning($errorMsg . ',chat_id: ' . $chatId); + return ['success' => false, 'data' => null, 'error' => $errorMsg]; + + default: + $errorMsg = 'AI返回未知状态:' . $status; + Log::error($errorMsg); + return ['success' => false, 'data' => null, 'error' => $errorMsg]; + } + } + + // 超时 + $errorMsg = 'AI对话处理超时,已等待' . (self::MAX_RETRY_TIMES * self::RETRY_INTERVAL / 1000000) . '秒'; + Log::error($errorMsg . ',chat_id: ' . $chatId); + return ['success' => false, 'data' => null, 'error' => $errorMsg]; + } + + /** + * 扣除Tokens + * + * @param array $chatResult 对话结果 + * @param array $params 参数 + * @param FriendSettings $friendSettings 好友设置 + */ + private function consumeTokens($chatResult, $params, $friendSettings) + { + $tokenCount = $chatResult['usage']['token_count'] ?? 0; + + if (empty($tokenCount)) { + return; + } + + // 获取好友昵称 + $nickname = WechatFriendModel::where('id', $friendSettings->friendId)->value('nickname'); + $remarks = !empty($nickname) ? '与好友【' . $nickname . '】聊天' : '与好友聊天'; + + // 扣除Tokens + $tokensRecord = new tokensRecord(); + $data = [ + 'tokens' => $tokenCount * 20, + 'type' => 0, + 'form' => 13, + 'wechatAccountId' => $params['wechatAccountId'], + 'friendIdOrGroupId' => $params['friendId'], + 'remarks' => $remarks, + ]; + + $tokensRecord->consumeTokens($data); + } + + /** + * 获取对话消息 + * + * @param string $conversationId 会话ID + * @param string $chatId 对话ID + * @return array|null + */ + private function getChatMessages($conversationId, $chatId) + { + $cozeAI = new CozeAI(); + $res = $cozeAI->listConversationMessage([ + 'conversation_id' => $conversationId, + 'chat_id' => $chatId, + ]); + $res = json_decode($res, true); + + if ($res['code'] != 200) { + Log::error('获取对话消息失败:' . ($res['msg'] ?? '未知错误')); + return null; + } + + return $res['data'] ?? []; + } + + /** + * 处理并发请求 + * 检查是否有同一用户的旧请求正在处理,如果有则取消旧请求 + * + * @param array $params 请求参数 + * @return true|\think\response\Json true表示可以继续,否则返回错误响应 + */ + private function handleConcurrentRequest($params) + { + $cacheData = Cache::get($this->requestKey); + + if ($cacheData) { + // 有旧请求正在处理 + $oldRequestId = $cacheData['request_id'] ?? ''; + $oldStep = $cacheData['step'] ?? 0; + $oldConversationId = $cacheData['conversation_id'] ?? ''; + $oldChatId = $cacheData['chat_id'] ?? ''; + + Log::info("检测到并发请求 - 旧请求: {$oldRequestId} (步骤{$oldStep}), 新请求: {$this->requestId}"); + + // 如果旧请求已经到达步骤7或之后,需要调用取消API + if ($oldStep >= 7 && !empty($oldConversationId) && !empty($oldChatId)) { + try { + $cozeAI = new CozeAI(); + $cancelResult = $cozeAI->cancelConversationChat([ + 'conversation_id' => $oldConversationId, + 'chat_id' => $oldChatId, + ]); + Log::info("已调用取消API取消旧请求的对话 - conversation_id: {$oldConversationId}, chat_id: {$oldChatId}"); + } catch (\Exception $e) { + Log::error("取消旧请求对话失败:" . $e->getMessage()); + } + } + + // 标记旧请求为已取消(通过更新缓存的 canceled 标志) + $cacheData['canceled'] = true; + $cacheData['canceled_by'] = $this->requestId; + Cache::set($this->requestKey, $cacheData, self::CACHE_EXPIRE); + } + + // 设置当前请求为活动请求 + $newCacheData = [ + 'request_id' => $this->requestId, + 'step' => 1, + 'start_time' => time(), + 'canceled' => false, + 'conversation_id' => '', + 'chat_id' => '', + ]; + Cache::set($this->requestKey, $newCacheData, self::CACHE_EXPIRE); + + return true; + } + + /** + * 检查当前请求是否被新请求取消 + * + * @param string $conversationId 会话ID(可选,用于取消对话) + * @param string $chatId 对话ID(可选,用于取消对话) + * @return bool + */ + private function isRequestCanceled($conversationId = '', $chatId = '') + { + $cacheData = Cache::get($this->requestKey); + + if (!$cacheData) { + // 缓存不存在,说明被清理或过期,视为被取消 + return true; + } + + $currentRequestId = $cacheData['request_id'] ?? ''; + $isCanceled = $cacheData['canceled'] ?? false; + + // 如果缓存中的请求ID与当前请求ID不一致,或者被标记为取消 + if ($currentRequestId !== $this->requestId || $isCanceled) { + Log::info("当前请求已被取消 - 请求ID: {$this->requestId}, 缓存请求ID: {$currentRequestId}, 取消标志: " . ($isCanceled ? 'true' : 'false')); + + // 如果提供了对话ID,尝试取消对话 + if (!empty($conversationId) && !empty($chatId) && $this->currentStep >= 7) { + try { + $cozeAI = new CozeAI(); + $cancelResult = $cozeAI->cancelConversationChat([ + 'conversation_id' => $conversationId, + 'chat_id' => $chatId, + ]); + Log::info("已取消当前请求的对话 - conversation_id: {$conversationId}, chat_id: {$chatId}"); + } catch (\Exception $e) { + Log::error("取消当前请求对话失败:" . $e->getMessage()); + } + } + + return true; + } + + return false; + } + + /** + * 更新请求步骤 + * + * @param int $step 当前步骤 + * @param string $conversationId 会话ID(可选) + * @param string $chatId 对话ID(可选) + */ + private function updateRequestStep($step, $conversationId = '', $chatId = '') + { + $this->currentStep = $step; + + $cacheData = Cache::get($this->requestKey); + + if ($cacheData && $cacheData['request_id'] === $this->requestId) { + $cacheData['step'] = $step; + $cacheData['update_time'] = time(); + + if (!empty($conversationId)) { + $cacheData['conversation_id'] = $conversationId; + } + if (!empty($chatId)) { + $cacheData['chat_id'] = $chatId; + } + + Cache::set($this->requestKey, $cacheData, self::CACHE_EXPIRE); + } + } + + /** + * 清理请求缓存 + */ + private function clearRequestCache() + { + if (!empty($this->requestKey)) { + $cacheData = Cache::get($this->requestKey); + + // 只有当前请求才能清理自己的缓存 + if ($cacheData && isset($cacheData['request_id']) && $cacheData['request_id'] === $this->requestId) { + Cache::rm($this->requestKey); + Log::info("已清理请求缓存 - 请求ID: {$this->requestId}"); + } + } + } + + + public function index2222() + { + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + $friendId = $this->request->param('friendId', ''); + $wechatAccountId = $this->request->param('wechatAccountId', ''); + $content = $this->request->param('content', ''); + + if (empty($wechatAccountId) || empty($friendId)) { + return ResponseHelper::error('参数缺失'); + } + + $tokens = TokensCompany::where(['companyId' => $companyId])->value('tokens'); + if (empty($tokens) || $tokens <= 0) { + return ResponseHelper::error('用户Tokens余额不足'); + } + + + //读取AI配置 + $setting = Db::name('ai_settings')->where(['companyId' => $companyId, 'userId' => $userId])->find(); + if (empty($setting)) { + return ResponseHelper::error('未找到配置信息,请先配置AI策略'); + } + $config = json_decode($setting['config'], true); + $modelSetting = $config['modelSetting']; + $round = isset($config['round']) ? $config['round'] : 10; + + + // 导出聊天 + $messages = Db::table('s2_wechat_message') + ->where('wechatFriendId', $friendId) + ->order('wechatTime desc') + ->field('id,content,msgType,isSend,wechatTime') + ->limit($round) + ->select(); + + usort($messages, function ($a, $b) { + return $a['wechatTime'] <=> $b['wechatTime']; + }); + + //处理聊天数据 + $msg = []; + foreach ($messages as $val) { + if (empty($val['content'])) { + continue; + } + if (!empty($val['isSend'])) { + $msg[] = '客服:' . $val['content']; + } else { + $msg[] = '用户:' . $val['content']; + } + } + $content = implode("\n", $msg); + + + $params = [ + 'model' => 'doubao-1-5-pro-32k-250115', + 'messages' => [ + // ['role' => 'system', 'content' => '请完成跟客户的对话'], + ['role' => 'system', 'content' => '角色设定:' . $modelSetting['role']], + ['role' => 'system', 'content' => '公司背景:' . $modelSetting['businessBackground']], + ['role' => 'system', 'content' => '对话风格:' . $modelSetting['dialogueStyle']], + ['role' => 'user', 'content' => $content], + ], + ]; + + //AI处理 + $ai = new DouBaoAI(); + $res = $ai->text($params); + $res = json_decode($res, true); + + if ($res['code'] == 200) { + //扣除Tokens + $tokensRecord = new tokensRecord(); + $nickname = Db::table('s2_wechat_friend')->where(['id' => $friendId])->value('nickname'); + $remarks = !empty($nickname) ? '与好友【' . $nickname . '】聊天' : '与好友聊天'; + $data = [ + 'tokens' => $res['data']['token'], + 'type' => 0, + 'form' => 13, + 'wechatAccountId' => $wechatAccountId, + 'friendIdOrGroupId' => $friendId, + 'remarks' => $remarks, + ]; + $tokensRecord->consumeTokens($data); + return ResponseHelper::success($res['data']['content']); + } else { + return ResponseHelper::error($res['msg']); + } + + + } +} \ No newline at end of file diff --git a/application/chukebao/controller/AiPushController.php b/application/chukebao/controller/AiPushController.php new file mode 100644 index 0000000..b1cecbd --- /dev/null +++ b/application/chukebao/controller/AiPushController.php @@ -0,0 +1,505 @@ +request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + $where = [ + ['companyId', '=', $companyId], + ['userId', '=', $userId], + ['isDel', '=', 0], + ]; + + if (!empty($keyword)) { + $where[] = ['name', 'like', '%' . $keyword . '%']; + } + + $query = AiPush::where($where); + $total = $query->count(); + $list = $query->where($where)->page($page, $limit)->order('id desc')->select(); + + // 处理数据 + $list = is_array($list) ? $list : $list->toArray(); + foreach ($list as &$item) { + // 解析标签数组 + $item['tags'] = json_decode($item['tags'], true); + if (!is_array($item['tags'])) { + $item['tags'] = []; + } + // 格式化推送时机显示文本 + $timingTypes = [ + 1 => '立即推送', + 2 => 'AI最佳时机', + 3 => '定时推送' + ]; + $item['timingText'] = $timingTypes[$item['pushTiming']] ?? '未知'; + // 处理定时推送时间 + if ($item['pushTiming'] == 3 && !empty($item['scheduledTime'])) { + $item['scheduledTime'] = date('Y-m-d H:i:s', $item['scheduledTime']); + } else { + $item['scheduledTime'] = ''; + } + // 从记录表计算实际成功率 + $pushId = $item['id']; + $totalCount = Db::name('kf_ai_push_record') + ->where('pushId', $pushId) + ->count(); + $sendCount = Db::name('kf_ai_push_record') + ->where('pushId', $pushId) + ->where('isSend', 1) + ->count(); + $item['successRate'] = $totalCount > 0 ? round(($sendCount * 100) / $totalCount, 1) : 0; + $item['totalPushCount'] = $totalCount; // 推送总数 + $item['sendCount'] = $sendCount; // 成功发送数 + } + unset($item); + + return ResponseHelper::success(['list' => $list, 'total' => $total]); + } + + /** + * 添加 + * @return \think\response\Json + * @throws \Exception + */ + public function add() + { + $name = $this->request->param('name', ''); + $tags = $this->request->param('tags', ''); // 标签,支持逗号分隔的字符串或数组 + $content = $this->request->param('content', ''); + $pushTiming = $this->request->param('pushTiming', 1); // 1=立即推送,2=最佳时机(AI决定),3=定时推送 + $scheduledTime = $this->request->param('scheduledTime', ''); // 定时推送的时间 + $status = $this->request->param('status', 1); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($name) || empty($content)) { + return ResponseHelper::error('推送名称和推送内容不能为空'); + } + + // 验证推送时机 + if (!in_array($pushTiming, [1, 2, 3])) { + return ResponseHelper::error('无效的推送时机类型'); + } + + // 如果是定时推送,需要验证时间 + if ($pushTiming == 3) { + if (empty($scheduledTime)) { + return ResponseHelper::error('定时推送需要设置推送时间'); + } + // 验证时间格式 + $timestamp = strtotime($scheduledTime); + if ($timestamp === false || $timestamp <= time()) { + return ResponseHelper::error('定时推送时间格式不正确或必须大于当前时间'); + } + } else { + $scheduledTime = ''; + } + + // 处理标签 + $tagsArray = []; + if (!empty($tags)) { + if (is_string($tags)) { + // 如果是字符串,按逗号分割 + $tagsArray = array_filter(array_map('trim', explode(',', $tags))); + } elseif (is_array($tags)) { + $tagsArray = array_filter(array_map('trim', $tags)); + } + } + + if (empty($tagsArray)) { + return ResponseHelper::error('目标用户标签不能为空'); + } + + Db::startTrans(); + try { + $aiPush = new AiPush(); + $aiPush->name = $name; + $aiPush->tags = json_encode($tagsArray, JSON_UNESCAPED_UNICODE); + $aiPush->content = $content; + $aiPush->pushTiming = $pushTiming; + $aiPush->scheduledTime = $pushTiming == 3 && !empty($scheduledTime) ? strtotime($scheduledTime) : 0; + $aiPush->status = $status; + $aiPush->successRate = 0; // 初始成功率为0 + $aiPush->userId = $userId; + $aiPush->companyId = $companyId; + $aiPush->createTime = time(); + $aiPush->updateTime = time(); + $aiPush->save(); + Db::commit(); + return ResponseHelper::success(['id' => $aiPush->id], '创建成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('创建失败:' . $e->getMessage()); + } + } + + /** + * 详情 + * @return \think\response\Json + * @throws \Exception + */ + public function details() + { + $id = $this->request->param('id', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($id)) { + return ResponseHelper::error('参数缺失'); + } + + $data = AiPush::where(['id' => $id, 'isDel' => 0, 'userId' => $userId, 'companyId' => $companyId])->find(); + if (empty($data)) { + return ResponseHelper::error('该推送已被删除或者不存在'); + } + + $data = $data->toArray(); + // 解析标签数组 + $data['tags'] = json_decode($data['tags'], true); + if (!is_array($data['tags'])) { + $data['tags'] = []; + } + // 标签转为逗号分隔的字符串(用于编辑时回显) + $data['tagsString'] = implode(',', $data['tags']); + + // 处理定时推送时间 + if ($data['pushTiming'] == 3 && !empty($data['scheduledTime'])) { + $data['scheduledTime'] = date('Y-m-d H:i:s', $data['scheduledTime']); + } else { + $data['scheduledTime'] = ''; + } + + // 成功率保留一位小数 + $data['successRate'] = isset($data['successRate']) ? round($data['successRate'], 1) : 0; + + return ResponseHelper::success($data, '获取成功'); + } + + /** + * 删除 + * @return \think\response\Json + * @throws \Exception + */ + public function del() + { + $id = $this->request->param('id', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($id)) { + return ResponseHelper::error('参数缺失'); + } + + $data = AiPush::where(['id' => $id, 'isDel' => 0, 'userId' => $userId, 'companyId' => $companyId])->find(); + if (empty($data)) { + return ResponseHelper::error('该推送已被删除或者不存在'); + } + + Db::startTrans(); + try { + $data->isDel = 1; + $data->delTime = time(); + $data->save(); + Db::commit(); + return ResponseHelper::success('', '删除成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('删除失败:' . $e->getMessage()); + } + } + + /** + * 更新 + * @return \think\response\Json + * @throws \Exception + */ + public function update() + { + $id = $this->request->param('id', ''); + $name = $this->request->param('name', ''); + $tags = $this->request->param('tags', ''); + $content = $this->request->param('content', ''); + $pushTiming = $this->request->param('pushTiming', 1); + $scheduledTime = $this->request->param('scheduledTime', ''); + $status = $this->request->param('status', 1); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($id) || empty($name) || empty($content)) { + return ResponseHelper::error('参数缺失'); + } + + // 验证推送时机 + if (!in_array($pushTiming, [1, 2, 3])) { + return ResponseHelper::error('无效的推送时机类型'); + } + + // 如果是定时推送,需要验证时间 + if ($pushTiming == 3) { + if (empty($scheduledTime)) { + return ResponseHelper::error('定时推送需要设置推送时间'); + } + // 验证时间格式 + $timestamp = strtotime($scheduledTime); + if ($timestamp === false || $timestamp <= time()) { + return ResponseHelper::error('定时推送时间格式不正确或必须大于当前时间'); + } + } else { + $scheduledTime = ''; + } + + // 处理标签 + $tagsArray = []; + if (!empty($tags)) { + if (is_string($tags)) { + $tagsArray = array_filter(array_map('trim', explode(',', $tags))); + } elseif (is_array($tags)) { + $tagsArray = array_filter(array_map('trim', $tags)); + } + } + + if (empty($tagsArray)) { + return ResponseHelper::error('目标用户标签不能为空'); + } + + $query = AiPush::where(['id' => $id, 'isDel' => 0, 'userId' => $userId, 'companyId' => $companyId])->find(); + if (empty($query)) { + return ResponseHelper::error('该推送已被删除或者不存在'); + } + + Db::startTrans(); + try { + $query->name = $name; + $query->tags = json_encode($tagsArray, JSON_UNESCAPED_UNICODE); + $query->content = $content; + $query->pushTiming = $pushTiming; + $query->scheduledTime = $pushTiming == 3 && !empty($scheduledTime) ? strtotime($scheduledTime) : 0; + $query->status = $status; + $query->updateTime = time(); + $query->save(); + Db::commit(); + return ResponseHelper::success('', '修改成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('修改失败:' . $e->getMessage()); + } + } + + /** + * 修改状态 + * @return \think\response\Json + * @throws \Exception + */ + public function setStatus() + { + $id = $this->request->param('id', ''); + $status = $this->request->param('status', 1); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($id)) { + return ResponseHelper::error('参数缺失'); + } + + if (!in_array($status, [0, 1])) { + return ResponseHelper::error('状态值无效'); + } + + $data = AiPush::where(['id' => $id, 'isDel' => 0, 'userId' => $userId, 'companyId' => $companyId])->find(); + if (empty($data)) { + return ResponseHelper::error('该推送已被删除或者不存在'); + } + + Db::startTrans(); + try { + $data->status = $status; + $data->updateTime = time(); + $data->save(); + Db::commit(); + return ResponseHelper::success('', $status == 1 ? '启用成功' : '禁用成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('操作失败:' . $e->getMessage()); + } + } + + /** + * 统计概览(整合自动问候和AI推送) + * - 活跃规则(自动问候规则,近30天) + * - 总触发次数(自动问候记录总数) + * - AI推送成功率(AI推送的成功率) + * - AI智能推送(AI推送规则,近30天活跃) + * - 规则效果排行(自动问候规则,按使用次数排序) + * @return \think\response\Json + */ + public function stats() + { + $companyId = $this->getUserInfo('companyId'); + $userId = $this->getUserInfo('id'); + + $start30d = time() - 30 * 24 * 3600; + + try { + // 公司维度(用于除排行外的统计) + $companyWhere = [ + ['companyId', '=', $companyId], + ]; + // 排行维度(限定个人) + $rankingWhere = [ + ['companyId', '=', $companyId], + ['userId', '=', $userId], + ]; + + // ========== 自动问候统计 ========== + + // 1) 活跃规则(自动问候规则,近30天有记录的) + $activeRules = Db::name('kf_auto_greetings_record') + ->where($companyWhere) + ->where('createTime', '>=', $start30d) + ->distinct(true) + ->count('autoId'); + + // 2) 总触发次数(自动问候记录总数) + $totalTriggers = Db::name('kf_auto_greetings_record') + ->where($companyWhere) + ->count(); + + // ========== AI推送统计 ========== + + // 3) AI推送成功率 + $totalPushes = Db::name('kf_ai_push_record') + ->where($companyWhere) + ->count(); + $sendCount = Db::name('kf_ai_push_record') + ->where($companyWhere) + ->where('isSend', '=', 1) + ->count(); + // 成功率:百分比,保留整数(75%) + $aiPushSuccessRate = $totalPushes > 0 ? round(($sendCount * 100) / $totalPushes, 0) : 0; + + // 4) AI智能推送(AI推送规则,近30天活跃的) + $aiPushCount = Db::name('kf_ai_push_record') + ->where($companyWhere) + ->where('createTime', '>=', $start30d) + ->distinct(true) + ->count('pushId'); + + // ========== 规则效果排行(自动问候规则,按使用次数排序)========== + $ruleRanking = Db::name('kf_auto_greetings_record') + ->where($rankingWhere) + ->field([ + 'autoId AS id', + 'COUNT(*) AS usageCount' + ]) + ->group('autoId') + ->order('usageCount DESC') + ->limit(20) + ->select(); + + // 附加规则名称和触发类型 + $autoIds = array_values(array_unique(array_column($ruleRanking, 'id'))); + $autoIdToRule = []; + if (!empty($autoIds)) { + $rules = AutoGreetings::where([['id', 'in', $autoIds]]) + ->field('id,name,trigger') + ->select(); + foreach ($rules as $rule) { + $triggerTypes = [ + 1 => '新好友', + 2 => '首次发消息', + 3 => '时间触发', + 4 => '关键词', + 5 => '生日触发', + 6 => '自定义' + ]; + $autoIdToRule[$rule['id']] = [ + 'name' => $rule['name'], + 'trigger' => $rule['trigger'], + 'triggerText' => $triggerTypes[$rule['trigger']] ?? '未知', + ]; + } + } + + foreach ($ruleRanking as &$row) { + $row['usageCount'] = (int)($row['usageCount'] ?? 0); + $row['name'] = $autoIdToRule[$row['id']]['name'] ?? ''; + $row['trigger'] = $autoIdToRule[$row['id']]['trigger'] ?? null; + $row['triggerText'] = $autoIdToRule[$row['id']]['triggerText'] ?? ''; + // 格式化使用次数显示 + $row['usageCountText'] = $row['usageCount'] . ' 次'; + } + unset($row); + + // 更新主表中的成功率字段(异步或定期更新) + $this->updatePushSuccessRate($companyId); + + return ResponseHelper::success([ + 'activeRules' => (int)$activeRules, + 'totalTriggers' => (int)$totalTriggers, + 'aiPushSuccessRate' => (int)$aiPushSuccessRate, + 'aiPushCount' => (int)$aiPushCount, + 'ruleRanking' => $ruleRanking, + ], '统计成功'); + } catch (\Exception $e) { + return ResponseHelper::error('统计失败:' . $e->getMessage()); + } + } + + /** + * 更新推送表的成功率字段 + * @param int $companyId + * @return void + */ + private function updatePushSuccessRate($companyId) + { + try { + // 获取所有启用的推送 + $pushes = AiPush::where([ + ['companyId', '=', $companyId], + ['isDel', '=', 0] + ])->field('id')->select(); + + foreach ($pushes as $push) { + $pushId = $push['id']; + $totalCount = Db::name('kf_ai_push_record') + ->where('pushId', $pushId) + ->count(); + $sendCount = Db::name('kf_ai_push_record') + ->where('pushId', $pushId) + ->where('isSend', 1) + ->count(); + + $successRate = $totalCount > 0 ? round(($sendCount * 100) / $totalCount, 2) : 0.00; + + AiPush::where('id', $pushId)->update([ + 'successRate' => $successRate, + 'updateTime' => time() + ]); + } + } catch (\Exception $e) { + // 静默失败,不影响主流程 + } + } +} + diff --git a/application/chukebao/controller/AiSettingsController.php b/application/chukebao/controller/AiSettingsController.php new file mode 100644 index 0000000..bef43ac --- /dev/null +++ b/application/chukebao/controller/AiSettingsController.php @@ -0,0 +1,266 @@ + false, + 'round' => 10, + 'aiStopSetting' => [ + 'status' => true, + 'key' => ['好', '不错', '好的', '下次', '可以'] + ], + 'fileSetting' => [ + 'type' => 1, + 'content' => '' + ], + 'modelSetting' => [ + 'model' => 'GPT-4', + 'role' => '你是一名销售的AI助理,同时也是一个工智能技术专家,你的名字叫小灵,你是单身女性,出生于2003年10月10日,喜欢听音乐和看电影有着丰富的人生阅历,前成熟大方,分享用幽默风趣的语言和客户交流,顾客问起你的感情,回复内容中不要使用号,特别注意不要跟客户问题,不要更多选择发送的信息。', + 'businessBackground' => '灵销智能公司开发了多款AI营销智能技术产品,以提升销售GPT AI大模型为核心,接入打造的销售/营销/客服等AI智能应用,为企业AI办公,AI助理,AI销售,AI营销,AI直播等大AI应用产品。', + 'dialogueStyle' => '客户:你们的AI解决方案具体是怎么收费的?销售:嗯,朋友,我们的AI解决方案是根据项目需求来定的,这样吧,你能跟我说说你们的具体情况吗,不过这样一分钱,您看怎么样?我们可以给您做个详细的方案对比。', + ] + ]; + + const TYPE_DATA = ['audioSetting', 'round', 'aiStopSetting', 'fileSetting', 'modelSetting']; + + /** + * 获取配置信息 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function getSetting() + { + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + $data = Db::name('ai_settings')->where(['userId' => $userId, 'companyId' => $companyId])->find(); + if (empty($data)) { + $setting = self::SETTING_DEFAULT; + $data = [ + 'companyId' => $companyId, + 'userId' => $userId, + 'config' => json_encode($setting, 256), + 'createTime' => time(), + 'updateTime' => time() + ]; + Db::name('ai_settings')->insert($data); + + } else { + $setting = json_decode($data['config'], true); + } + + return ResponseHelper::success($setting, '获取成功'); + } + + + /** + * 配置 + * @return \think\response\Json + * @throws \Exception + */ + public function setSetting() + { + $key = $this->request->param('key', ''); + $value = $this->request->param('value', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($key) || empty($value)) { + return ResponseHelper::error('参数缺失'); + } + + + if (!in_array($key, self::TYPE_DATA)) { + return ResponseHelper::error('该类型不在配置项'); + } + + Db::startTrans(); + try { + $data = Db::name('ai_settings')->where(['userId' => $userId, 'companyId' => $companyId])->find(); + if (empty($data)) { + $setting = self::SETTING_DEFAULT; + } else { + $setting = json_decode($data['config'], true); + } + $setting[$key] = $value; + $setting = json_encode($setting, 256); + Db::name('ai_settings')->where(['id' => $data['id']])->update(['config' => $setting, 'updateTime' => time()]); + Db::commit(); + return ResponseHelper::success(' ', '配置成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('配置失败:' . $e->getMessage()); + } + } + + + public function getUserTokens() + { + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + $tokens = Db::name('users') + ->where('id', $userId) + ->where('companyId', $companyId) + ->value('tokens'); + + return ResponseHelper::success($tokens, '获取成功'); + } + + + + public function getFriend() + { + $friendId = $this->request->param('friendId', ''); + $wechatAccountId = $this->request->param('wechatAccountId', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + $aiType = FriendSettings::where(['userId' => $userId, 'companyId' => $companyId,'friendId' => $friendId,'wechatAccountId' => $wechatAccountId])->value('type'); + if (empty($aiType)) { + $aiType = 0; + } + return ResponseHelper::success($aiType, '获取成功'); + } + + + + + + public function setFriend() + { + $friendId = $this->request->param('friendId', ''); + $wechatAccountId = $this->request->param('wechatAccountId', ''); + $type = $this->request->param('type', 0); + + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($friendId) || empty($wechatAccountId)) { + return ResponseHelper::error('参数缺失'); + } + $friend = Db::table('s2_wechat_friend')->where(['id' => $friendId,'wechatAccountId' => $wechatAccountId])->find(); + + if (empty($friend)) { + return ResponseHelper::error('该好友不存在'); + } + + $friendSettings = FriendSettings::where(['userId' => $userId, 'companyId' => $companyId,'friendId' => $friendId,'wechatAccountId' => $wechatAccountId])->find(); + Db::startTrans(); + try { + if (empty($friendSettings)) { + $friendSettings = new FriendSettings(); + $friendSettings->companyId = $companyId; + $friendSettings->userId = $userId; + $friendSettings->type = $type; + $friendSettings->wechatAccountId = $wechatAccountId; + $friendSettings->friendId = $friendId; + $friendSettings->createTime = time(); + $friendSettings->updateTime = time(); + }else{ + $friendSettings->type = $type; + $friendSettings->updateTime = time(); + } + $friendSettings->save(); + Db::commit(); + return ResponseHelper::success(' ', '配置成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('配置失败:' . $e->getMessage()); + } + + } + + + + + public function setAllFriend() + { + $packageId = $this->request->param('packageId', []); + $type = $this->request->param('type', 0); + $isUpdata = $this->request->param('isUpdata', 0); + + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($packageId)) { + return ResponseHelper::error('参数缺失'); + } + //列出所有好友 + $row = Db::name('traffic_source_package_item')->alias('a') + ->join('wechat_friendship f','a.identifier = f.wechatId and f.companyId = '.$companyId) + ->join(['s2_wechat_account' => 'wa'],'f.ownerWechatId = wa.wechatId') + ->whereIn('a.packageId' , $packageId) + ->field('f.id as friendId,wa.id as wechatAccountId') + ->group('f.id') + ->select(); + + if (empty($row)) { + return ResponseHelper::error('`好友不存在'); + } + + + + + + + // 1000条为一组进行批量处理 + $batchSize = 1000; + $totalRows = count($row); + + for ($i = 0; $i < $totalRows; $i += $batchSize) { + $batchRows = array_slice($row, $i, $batchSize); + if (!empty($batchRows)) { + // 1. 提取当前批次的phone + $friendIds = array_column($batchRows, 'friendId'); + // 2. 批量查询已存在的phone + $existingPhones = []; + if (!empty($friendIds)) { + //强制更新 + if(!empty($isUpdata)){ + FriendSettings::whereIn('friendId',$friendIds)->update(['type' => $type,'updateTime' => time()]); + } + + $existing = FriendSettings::where('companyId', $companyId)->where('friendId', 'in', $friendIds)->field('friendId')->select()->toArray(); + $existingPhones = array_column($existing, 'friendId'); + } + + // 3. 过滤出新数据,批量插入 + $newData = []; + foreach ($batchRows as $row) { + if (!empty($friendIds) && !in_array($row['friendId'], $existingPhones)) { + $newData[] = [ + 'companyId' => $companyId, + 'userId' => $userId, + 'type' => $type, + 'wechatAccountId' => $row['wechatAccountId'], + 'friendId' => $row['friendId'], + 'createTime' => time(), + 'updateTime' => time(), + ]; + } + } + // 4. 批量插入新数据 + if (!empty($newData)) { + FriendSettings::insertAll($newData); + } + } + } + try { + return ResponseHelper::success(' ', '配置成功'); + } catch (\Exception $e) { + return ResponseHelper::error('配置失败:' . $e->getMessage()); + } + + } + +} \ No newline at end of file diff --git a/application/chukebao/controller/AutoGreetingsController.php b/application/chukebao/controller/AutoGreetingsController.php new file mode 100644 index 0000000..ef186b3 --- /dev/null +++ b/application/chukebao/controller/AutoGreetingsController.php @@ -0,0 +1,754 @@ +request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + $is_template = $this->request->param('is_template', 0); + $triggerType = $this->request->param('triggerType', ''); // 触发类型筛选 + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if($is_template == 1){ + $where = [ + ['is_template','=',1], + ['isDel' ,'=', 0], + ]; + }else{ + $where = [ + ['companyId','=',$companyId], + ['userId' ,'=', $userId], + ['isDel' ,'=', 0], + ]; + } + + if(!empty($keyword)){ + $where[] = ['name','like','%'.$keyword.'%']; + } + + if(!empty($triggerType)){ + $where[] = ['trigger','=',$triggerType]; + } + + $query = AutoGreetings::where($where); + $total = $query->count(); + $list = $query->where($where)->page($page,$limit)->order('level asc,id desc')->select(); + + // 获取使用次数 + $list = is_array($list) ? $list : $list->toArray(); + $ids = array_column($list, 'id'); + $usageCounts = []; + if (!empty($ids)) { + $counts = Db::name('kf_auto_greetings_record') + ->where('autoId', 'in', $ids) + ->field('autoId, COUNT(*) as count') + ->group('autoId') + ->select(); + foreach ($counts as $count) { + $usageCounts[$count['autoId']] = (int)$count['count']; + } + } + + foreach ($list as &$item) { + $item['condition'] = json_decode($item['condition'], true); + $item['usageCount'] = $usageCounts[$item['id']] ?? 0; + // 格式化触发类型显示文本 + $triggerTypes = [ + 1 => '新好友', + 2 => '首次发消息', + 3 => '时间触发', + 4 => '关键词触发', + 5 => '生日触发', + 6 => '自定义' + ]; + $item['triggerText'] = $triggerTypes[$item['trigger']] ?? '未知'; + } + unset($item); + + return ResponseHelper::success(['list'=>$list,'total'=>$total]); + } + + + /** + * 校验trigger类型对应的condition + * @param int $trigger 触发类型 + * @param mixed $condition 条件参数 + * @return array|string 返回处理后的condition数组,或错误信息字符串 + */ + private function validateTriggerCondition($trigger, $condition) + { + // trigger类型:1=新好友,2=首次发消息,3=时间触发,4=关键词触发,5=生日触发,6=自定义 + switch ($trigger) { + case 1: // 新好友 + // 不需要condition + return []; + + case 2: // 首次发消息 + // 不需要condition + return []; + + case 3: // 时间触发 + // 需要condition,格式为:{"type": "daily_time|yearly_datetime|fixed_range|workday", "value": "..."} + if (empty($condition)) { + return '时间触发类型需要配置具体的触发条件'; + } + $condition = is_array($condition) ? $condition : json_decode($condition, true); + if (empty($condition) || !is_array($condition)) { + return '时间触发类型的条件格式不正确,应为数组格式'; + } + + // 验证必须包含type字段 + if (!isset($condition['type']) || empty($condition['type'])) { + return '时间触发类型必须指定触发方式:daily_time(每天固定时间)、yearly_datetime(每年固定日期时间)、fixed_range(固定时间段)、workday(工作日)'; + } + + $timeType = $condition['type']; + $allowedTypes = ['daily_time', 'yearly_datetime', 'fixed_range', 'workday']; + // 兼容旧版本的 fixed_time,自动转换为 daily_time + if ($timeType === 'fixed_time') { + $timeType = 'daily_time'; + } + if (!in_array($timeType, $allowedTypes)) { + return '时间触发类型无效,必须为:daily_time(每天固定时间)、yearly_datetime(每年固定日期时间)、fixed_range(固定时间段)、workday(工作日)'; + } + + // 根据不同的type验证value + switch ($timeType) { + case 'daily_time': // 每天固定时间(每天的几点几分) + // value应该是时间字符串,格式:HH:mm,如 "14:30" + if (!isset($condition['value']) || empty($condition['value'])) { + return '每天固定时间类型需要配置具体时间,格式:HH:mm(如 14:30)'; + } + $timeValue = $condition['value']; + if (!preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $timeValue)) { + return '每天固定时间格式不正确,应为 HH:mm 格式(如 14:30)'; + } + return [ + 'type' => 'daily_time', + 'value' => $timeValue + ]; + + case 'yearly_datetime': // 每年固定日期时间(每年的几月几号几点几分) + // value应该是日期时间字符串,格式:MM-dd HH:mm,如 "12-25 14:30" + if (!isset($condition['value']) || empty($condition['value'])) { + return '每年固定日期时间类型需要配置具体日期和时间,格式:MM-dd HH:mm(如 12-25 14:30)'; + } + $datetimeValue = $condition['value']; + // 验证格式:MM-dd HH:mm + if (!preg_match('/^(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) ([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $datetimeValue)) { + return '每年固定日期时间格式不正确,应为 MM-dd HH:mm 格式(如 12-25 14:30)'; + } + // 进一步验证日期是否有效(例如2月30日不存在) + list($datePart, $timePart) = explode(' ', $datetimeValue); + list($month, $day) = explode('-', $datePart); + if (!checkdate((int)$month, (int)$day, 2000)) { // 使用2000年作为参考年份验证日期有效性 + return '日期无效,请检查月份和日期是否正确(如2月不能有30日)'; + } + return [ + 'type' => 'yearly_datetime', + 'value' => $datetimeValue + ]; + + case 'fixed_range': // 固定时间段 + // value应该是时间段数组,格式:["09:00", "18:00"] + if (!isset($condition['value']) || !is_array($condition['value'])) { + return '固定时间段类型需要配置时间段,格式:["开始时间", "结束时间"](如 ["09:00", "18:00"])'; + } + $rangeValue = $condition['value']; + if (count($rangeValue) !== 2) { + return '固定时间段应为包含两个时间点的数组,格式:["09:00", "18:00"]'; + } + // 验证时间格式 + foreach ($rangeValue as $time) { + if (!preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $time)) { + return '时间段格式不正确,应为 HH:mm 格式(如 09:00)'; + } + } + // 验证开始时间小于结束时间 + $startTime = strtotime('2000-01-01 ' . $rangeValue[0]); + $endTime = strtotime('2000-01-01 ' . $rangeValue[1]); + if ($startTime >= $endTime) { + return '开始时间必须小于结束时间'; + } + return [ + 'type' => 'fixed_range', + 'value' => $rangeValue + ]; + + case 'workday': // 工作日 + // 工作日需要配置时间,格式:HH:mm(如 09:00) + if (!isset($condition['value']) || empty($condition['value'])) { + return '工作日触发类型需要配置时间,格式:HH:mm(如 09:00)'; + } + $timeValue = trim($condition['value']); + // 验证格式:HH:mm + if (!preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $timeValue)) { + return '工作日时间格式不正确,应为 HH:mm 格式(如 09:00)'; + } + return [ + 'type' => 'workday', + 'value' => $timeValue + ]; + + default: + return '时间触发类型无效'; + } + + case 4: // 关键词触发 + // 需要condition,格式:{"keywords": ["关键词1", "关键词2"], "match_type": "exact|fuzzy"} + if (empty($condition)) { + return '关键词触发类型需要配置至少一个关键词'; + } + + // 如果是字符串,尝试解析JSON + if (is_string($condition)) { + $decoded = json_decode($condition, true); + if (json_last_error() === JSON_ERROR_NONE) { + $condition = $decoded; + } else { + return '关键词触发类型格式错误,应为对象格式:{"keywords": ["关键词1", "关键词2"], "match_type": "exact|fuzzy"}'; + } + } + + // 必须是对象格式 + if (!is_array($condition) || !isset($condition['keywords'])) { + return '关键词触发类型格式错误,应为对象格式:{"keywords": ["关键词1", "关键词2"], "match_type": "exact|fuzzy"}'; + } + + $keywords = $condition['keywords']; + $matchType = isset($condition['match_type']) ? $condition['match_type'] : 'fuzzy'; + + // 验证match_type + if (!in_array($matchType, ['exact', 'fuzzy'])) { + return '匹配类型无效,必须为:exact(精准匹配)或 fuzzy(模糊匹配)'; + } + + // 处理keywords + if (is_string($keywords)) { + $keywords = explode(',', $keywords); + } + if (!is_array($keywords)) { + return '关键词格式不正确,应为数组格式'; + } + + // 过滤空值并去重 + $keywords = array_filter(array_map('trim', $keywords)); + if (empty($keywords)) { + return '关键词触发类型需要配置至少一个关键词'; + } + + // 验证每个关键词不为空 + foreach ($keywords as $keyword) { + if (empty($keyword)) { + return '关键词不能为空'; + } + } + + return [ + 'keywords' => array_values($keywords), + 'match_type' => $matchType + ]; + + case 5: // 生日触发 + // 需要condition,格式支持: + // 1. 月日字符串:'10-10' 或 '10-10 09:00'(MM-DD格式,不包含年份) + // 2. 对象格式:{'month': 10, 'day': 10, 'time': '09:00'} 或 {'month': '10', 'day': '10', 'time_range': ['09:00', '10:00']} + if (empty($condition)) { + return '生日触发类型需要配置日期条件'; + } + + // 如果是字符串,只接受 MM-DD 格式(不包含年份) + if (is_string($condition)) { + // 检查是否包含时间部分 + if (preg_match('/^(\d{1,2})-(\d{1,2})\s+(\d{2}:\d{2})$/', $condition, $matches)) { + // 格式:'10-10 09:00' + $month = (int)$matches[1]; + $day = (int)$matches[2]; + if ($month < 1 || $month > 12 || $day < 1 || $day > 31) { + return '生日日期格式不正确,月份应为1-12,日期应为1-31'; + } + return [ + 'month' => $month, + 'day' => $day, + 'time' => $matches[3] + ]; + } elseif (preg_match('/^(\d{1,2})-(\d{1,2})$/', $condition, $matches)) { + // 格式:'10-10'(不指定时间,当天任何时间都可以触发) + $month = (int)$matches[1]; + $day = (int)$matches[2]; + if ($month < 1 || $month > 12 || $day < 1 || $day > 31) { + return '生日日期格式不正确,月份应为1-12,日期应为1-31'; + } + return [ + 'month' => $month, + 'day' => $day + ]; + } else { + return '生日日期格式不正确,应为 MM-DD 或 MM-DD HH:mm 格式(如 10-10 或 10-10 09:00),不包含年份'; + } + } + + // 如果是数组,可能是对象格式或旧格式 + if (is_array($condition)) { + // 检查是否是旧格式(仅兼容 MM-DD 格式的数组) + if (isset($condition[0]) && is_string($condition[0])) { + $dateStr = $condition[0]; + // 只接受 MM-DD 格式:'10-10' 或 '10-10 09:00' + if (preg_match('/^(\d{1,2})-(\d{1,2})(?:\s+(\d{2}:\d{2}))?$/', $dateStr, $matches)) { + $month = (int)$matches[1]; + $day = (int)$matches[2]; + if ($month < 1 || $month > 12 || $day < 1 || $day > 31) { + return '生日日期格式不正确,月份应为1-12,日期应为1-31'; + } + if (isset($matches[3])) { + return [ + 'month' => $month, + 'day' => $day, + 'time' => $matches[3] + ]; + } else { + return [ + 'month' => $month, + 'day' => $day + ]; + } + } else { + return '生日日期格式不正确,应为 MM-DD 格式(如 10-10),不包含年份'; + } + } + + // 新格式:{'month': 10, 'day': 10, 'time': '09:00'} + if (isset($condition['month']) && isset($condition['day'])) { + $month = (int)$condition['month']; + $day = (int)$condition['day']; + + if ($month < 1 || $month > 12) { + return '生日月份格式不正确,应为1-12'; + } + if ($day < 1 || $day > 31) { + return '生日日期格式不正确,应为1-31'; + } + + $result = [ + 'month' => $month, + 'day' => $day + ]; + + // 检查是否配置了时间 + if (isset($condition['time']) && !empty($condition['time'])) { + $time = trim($condition['time']); + if (!preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $time)) { + return '生日时间格式不正确,应为 HH:mm 格式(如 09:00)'; + } + $result['time'] = $time; + } + + // 检查是否配置了时间范围 + if (isset($condition['time_range']) && is_array($condition['time_range']) && count($condition['time_range']) === 2) { + $startTime = trim($condition['time_range'][0]); + $endTime = trim($condition['time_range'][1]); + if (!preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $startTime) || + !preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $endTime)) { + return '生日时间范围格式不正确,应为 ["HH:mm", "HH:mm"] 格式'; + } + $result['time_range'] = [$startTime, $endTime]; + } + + return $result; + } + + return '生日触发条件格式不正确,需要提供month和day字段'; + } + + return '生日触发条件格式不正确'; + + case 6: // 自定义 + // 自定义类型,condition可选,如果有则必须是数组格式 + if (!empty($condition)) { + $condition = is_array($condition) ? $condition : json_decode($condition, true); + if (!is_array($condition)) { + return '自定义类型的条件格式不正确,应为数组格式'; + } + return $condition; + } + return []; + + default: + return '无效的触发类型'; + } + } + + /** + * 添加 + * @return \think\response\Json + * @throws \Exception + */ + public function create(){ + $name = $this->request->param('name', ''); + $trigger = $this->request->param('trigger', 0); + $condition = $this->request->param('condition', ''); + $content = $this->request->param('content', ''); + $level = $this->request->param('level', 0); + $status = $this->request->param('status', 1); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($name) || empty($trigger) || empty($content)){ + return ResponseHelper::error('参数缺失'); + } + + // 校验trigger类型 + if (!in_array($trigger, [1, 2, 3, 4, 5, 6])) { + return ResponseHelper::error('无效的触发类型'); + } + + // 校验并处理condition + $conditionResult = $this->validateTriggerCondition($trigger, $condition); + if (is_string($conditionResult)) { + // 返回的是错误信息 + return ResponseHelper::error($conditionResult); + } + $condition = $conditionResult; + + + Db::startTrans(); + try { + $AutoGreetings = new AutoGreetings(); + $AutoGreetings->name = $name; + $AutoGreetings->trigger = $trigger; + $AutoGreetings->condition = json_encode($condition,256); + $AutoGreetings->content = $content; + $AutoGreetings->level = $level; + $AutoGreetings->status = $status; + $AutoGreetings->userId = $userId; + $AutoGreetings->companyId = $companyId; + $AutoGreetings->updateTime = time(); + $AutoGreetings->createTime = time(); + $AutoGreetings->usageCount = 0; // 初始化使用次数为0 + $AutoGreetings->save(); + Db::commit(); + return ResponseHelper::success(['id' => $AutoGreetings->id],'创建成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('创建失败:'.$e->getMessage()); + } + } + + + + + /** + * 详情 + * @return \think\response\Json + */ + public function details() + { + $id = $this->request->param('id', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + if (empty($id)){ + return ResponseHelper::error('参数缺失'); + } + $data = AutoGreetings::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find(); + if (empty($data)){ + return ResponseHelper::error('该内容已被删除或者不存在'); + } + + + $data['condition'] = json_decode($data['condition'],true); + + // 获取使用次数 + $usageCount = Db::name('kf_auto_greetings_record') + ->where('autoId', $id) + ->count(); + $data['usageCount'] = (int)$usageCount; + + return ResponseHelper::success($data,'获取成功'); + } + + /** + * 删除 + * @return \think\response\Json + */ + public function del() + { + $id = $this->request->param('id', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + if (empty($id)){ + return ResponseHelper::error('参数缺失'); + } + $data = AutoGreetings::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find(); + if (empty($data)){ + return ResponseHelper::error('该已被删除或者不存在'); + } + Db::startTrans(); + try { + $data->isDel = 1; + $data->delTime = time(); + $data->save(); + Db::commit(); + return ResponseHelper::success('','删除成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('删除失败:'.$e->getMessage()); + } + } + + + /** + * 更新 + * @return \think\response\Json + * @throws \Exception + */ + public function update(){ + $id = $this->request->param('id', ''); + $name = $this->request->param('name', ''); + $trigger = $this->request->param('trigger', 0); + $condition = $this->request->param('condition', ''); + $content = $this->request->param('content', ''); + $level = $this->request->param('level', 0); + $status = $this->request->param('status', 1); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($id) || empty($name) || empty($trigger) || empty($content)){ + return ResponseHelper::error('参数缺失'); + } + + // 校验trigger类型 + if (!in_array($trigger, [1, 2, 3, 4, 5, 6])) { + return ResponseHelper::error('无效的触发类型'); + } + + // 校验并处理condition + $conditionResult = $this->validateTriggerCondition($trigger, $condition); + if (is_string($conditionResult)) { + // 返回的是错误信息 + return ResponseHelper::error($conditionResult); + } + $condition = $conditionResult; + + + $query = AutoGreetings::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find(); + if (empty($query)){ + return ResponseHelper::error('该内容已被删除或者不存在'); + } + Db::startTrans(); + try { + $query->name = $name; + $query->trigger = $trigger; + $query->condition = !empty($condition) ? json_encode($condition,256) : json_encode([]); + $query->content = $content; + $query->level = $level; + $query->status = $status; + $query->userId = $userId; + $query->companyId = $companyId; + $query->updateTime = time(); + $query->createTime = time(); + $query->save(); + Db::commit(); + return ResponseHelper::success(' ','修改成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('修改失败:'.$e->getMessage()); + } + } + + /** + * 修改状态 + * @return \think\response\Json + * @throws \Exception + */ + public function setStatus(){ + $id = $this->request->param('id', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($id)){ + return ResponseHelper::error('参数缺失'); + } + + $query = AutoGreetings::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find(); + if (empty($query)){ + return ResponseHelper::error('该内容已被删除或者不存在'); + } + Db::startTrans(); + try { + $status = $this->request->param('status', ''); + if ($status !== '') { + $query->status = (int)$status; + } else { + $query->status = $query->status == 1 ? 0 : 1; + } + $query->updateTime = time(); + $query->save(); + Db::commit(); + return ResponseHelper::success(['status' => $query->status],'修改成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('修改失败:'.$e->getMessage()); + } + } + + + + + /** + * 拷贝 + * @return \think\response\Json + * @throws \Exception + */ + public function copy(){ + $id = $this->request->param('id', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($id) ){ + return ResponseHelper::error('参数缺失'); + } + + $data = AutoGreetings::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find(); + if (empty($data)){ + return ResponseHelper::error('该内容已被删除或者不存在'); + } + Db::startTrans(); + try { + $query = new AutoGreetings(); + $query->name = $data['name'] . '_copy'; + $query->trigger = $data['trigger']; + $query->condition = $data['condition']; + $query->content = $data['content']; + $query->level = $data['level']; + $query->status = $data['status']; + $query->userId = $userId; + $query->companyId = $companyId; + $query->updateTime = time(); + $query->createTime = time(); + $query->save(); + Db::commit(); + return ResponseHelper::success(' ','拷贝成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('拷贝失败:'.$e->getMessage()); + } + } + + + /** + * 统计概览 + * - 总触发次数 + * - 活跃规则(近一个月) + * - 发送成功率 + * - 平均响应时间(秒) + * - 规则效果排行(按发送次数降序、平均响应时间升序) + * @return \think\response\Json + */ + public function stats() + { + $companyId = $this->getUserInfo('companyId'); + $userId = $this->getUserInfo('id'); + + $start30d = time() - 30 * 24 * 3600; + + try { + // 公司维度(用于除排行外的统计) + $companyWhere = [ + ['companyId', '=', $companyId], + ]; + // 排行维度(限定个人) + $rankingWhere = [ + ['companyId', '=', $companyId], + ['userId', '=', $userId], + ]; + + // 1) 总触发次数 + $totalTriggers = Db::name('kf_auto_greetings_record') + ->where($companyWhere) + ->count(); + + // 2) 近30天活跃规则(仅返回数量,按公司维度,distinct autoId) + $activeRulesCount = Db::name('kf_auto_greetings_record') + ->where($companyWhere) + ->where('createTime', '>=', $start30d) + ->distinct(true) + ->count('autoId'); + + // 3) 发送成功率 + $sendCount = Db::name('kf_auto_greetings_record') + ->where($companyWhere) + ->where('isSend', '=', 1) + ->count(); + // 成功率:百分比,保留两位小数 + $sendRate = $totalTriggers > 0 ? round(($sendCount * 100) / $totalTriggers, 2) : 0.00; + + // 4) 平均响应时间(receiveTime - sendTime,单位秒) + $avgResponse = Db::name('kf_auto_greetings_record') + ->where($companyWhere) + ->whereRaw('sendTime IS NOT NULL AND receiveTime IS NOT NULL AND receiveTime >= sendTime') + ->avg(Db::raw('(receiveTime - sendTime)')); + $avgResponse = $avgResponse ? (int)round($avgResponse) : 0; + + // 5) 规则效果排行(按发送次数降序、平均响应时间升序) + $ranking = Db::name('kf_auto_greetings_record') + ->where($rankingWhere) + ->field([ + 'autoId AS id', + 'COUNT(*) AS totalCount', + 'SUM(CASE WHEN isSend = 1 THEN 1 ELSE 0 END) AS sendCount', + 'AVG(CASE WHEN sendTime IS NOT NULL AND receiveTime IS NOT NULL AND receiveTime >= sendTime THEN (receiveTime - sendTime) END) AS avgResp' + ]) + ->group('autoId') + ->orderRaw('sendCount DESC, avgResp ASC') + ->limit(20) + ->select(); + + // 附加规则名称(如存在) + $autoIds = array_values(array_unique(array_column($ranking, 'id'))); + $autoIdToRule = []; + if (!empty($autoIds)) { + $rules = AutoGreetings::where([['id', 'in', $autoIds]]) + ->field('id,name,trigger') + ->select(); + foreach ($rules as $rule) { + $autoIdToRule[$rule['id']] = [ + 'name' => $rule['name'], + 'trigger' => $rule['trigger'], + ]; + } + } + + foreach ($ranking as &$row) { + $row['avgResp'] = isset($row['avgResp']) && $row['avgResp'] !== null ? (int)round($row['avgResp']) : 0; + // 百分比,两位小数 + $row['sendRate'] = ($row['totalCount'] ?? 0) > 0 ? round((($row['sendCount'] ?? 0) * 100) / $row['totalCount'], 2) : 0.00; + $row['name'] = $autoIdToRule[$row['id']]['name'] ?? ''; + $row['trigger'] = $autoIdToRule[$row['id']]['trigger'] ?? null; + } + unset($row); + + return ResponseHelper::success([ + 'totalTriggers' => (int)$totalTriggers, + 'activeRules' => (int)$activeRulesCount, + 'sendSuccessRate' => $sendRate, + 'avgResponseSeconds' => $avgResponse, + 'ruleRanking' => $ranking, + ], '统计成功'); + } catch (\Exception $e) { + return ResponseHelper::error('统计失败:' . $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/application/chukebao/controller/BaseController.php b/application/chukebao/controller/BaseController.php new file mode 100644 index 0000000..2c87750 --- /dev/null +++ b/application/chukebao/controller/BaseController.php @@ -0,0 +1,29 @@ +request->userInfo; + + if (!$user) { + throw new \Exception('未授权访问,缺少有效的身份凭证', 401); + } + + return $column ? $user[$column] : $user; + } +} \ No newline at end of file diff --git a/application/chukebao/controller/ContentController.php b/application/chukebao/controller/ContentController.php new file mode 100644 index 0000000..92e49b2 --- /dev/null +++ b/application/chukebao/controller/ContentController.php @@ -0,0 +1,665 @@ +request->param('keyword', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + $query = Material::where(['userId' => $userId,'companyId' => $companyId,'isDel' => 0,'status' => 1]) + ->field('id,title,cover') + ->order('id desc'); + + $list = $query->select()->toArray(); + + return ResponseHelper::success($list); + } + + + /** + * 素材列表 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function getMaterial(){ + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + $query = Material::where(['userId' => $userId,'companyId' => $companyId,'isDel' => 0]) + ->order('id desc'); + if (!empty($keyword)){ + $query->where('title', 'like', '%'.$keyword.'%'); + } + $list = $query->page($page, $limit)->select()->toArray(); + $total = $query->count(); + + foreach ($list as $k => &$v){ + $user = Db::name('users')->where(['id' => $v['userId']])->field('username,account')->find(); + if (!empty($user)){ + $v['userName'] = !empty($user['username']) ? $user['username'] : $user['account']; + }else{ + $v['userName'] = ''; + } + } + unset($v); + return ResponseHelper::success(['list'=>$list,'total'=>$total]); + } + + + /** + * 素材添加 + * @return \think\response\Json + * @throws \Exception + */ + public function createMaterial(){ + $title = $this->request->param('title', ''); + $content = $this->request->param('content', []); + $cover = $this->request->param('cover', ''); + $status = $this->request->param('status', 0); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($title) || empty($content) || empty($cover)){ + return ResponseHelper::error('参数缺失'); + } + $newContent = []; + foreach ($content as $k => $v){ + if (in_array($v['type'],['text','image','video','audio','file','link'])){ + $newContent[] = $v; + } + } + + Db::startTrans(); + try { + $query = new Material(); + $query->title = $title; + $query->content = !empty($newContent) ? json_encode($newContent,256) : json_encode([],256); + $query->cover = $cover; + $query->status = $status; + $query->userId = $userId; + $query->companyId = $companyId; + $query->createTime = time(); + $query->updateTime = time(); + $query->save(); + Db::commit(); + return ResponseHelper::success(' ','创建成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('创建失败:'.$e->getMessage()); + } + } + + /** + * 素材详情 + * @return \think\response\Json + */ + public function detailsMaterial() + { + $id = $this->request->param('id', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + if (empty($id)){ + return ResponseHelper::error('参数缺失'); + } + $data = Material::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find(); + if (empty($data)){ + return ResponseHelper::error('该素材已被删除或者不存在'); + } + $data['content'] = json_decode($data['content'],true); + unset($data['createTime'],$data['updateTime'],$data['isDel'],$data['delTime']); + return ResponseHelper::success($data,'获取成功'); + } + + /** + * 删除素材 + * @return \think\response\Json + */ + public function delMaterial() + { + $id = $this->request->param('id', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + if (empty($id)){ + return ResponseHelper::error('参数缺失'); + } + $data = Material::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find(); + if (empty($data)){ + return ResponseHelper::error('该素材已被删除或者不存在'); + } + Db::startTrans(); + try { + $data->isDel = 1; + $data->delTime = time(); + $data->save(); + Db::commit(); + return ResponseHelper::success('','删除成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('删除失败:'.$e->getMessage()); + } + } + + + /** + * 修改素材 + * @return \think\response\Json + * @throws \Exception + */ + public function updateMaterial(){ + $id = $this->request->param('id', ''); + $title = $this->request->param('title', ''); + $content = $this->request->param('content', []); + $cover = $this->request->param('cover', ''); + $status = $this->request->param('status', 0); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($id) || empty($title) || empty($content) || empty($cover)){ + return ResponseHelper::error('参数缺失'); + } + $newContent = []; + foreach ($content as $k => $v){ + if (in_array($v['type'],['text','image','video','audio','file','link'])){ + $newContent[] = $v; + } + } + $query = Material::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find(); + if (empty($query)){ + return ResponseHelper::error('该素材已被删除或者不存在'); + } + Db::startTrans(); + try { + $query->title = $title; + $query->content = !empty($newContent) ? json_encode($newContent,256) : json_encode([],256); + $query->cover = $cover; + $query->status = $status; + $query->updateTime = time(); + $query->save(); + Db::commit(); + return ResponseHelper::success(' ','修改成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('修改失败:'.$e->getMessage()); + } + } + //===================================================== 素材管理 ===================================================== + + + + + + //==================================================== 违禁词管理 ==================================================== + + /** + * 违禁词列表 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function getSensitiveWord(){ + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + $query = SensitiveWord::where(['userId' => $userId,'companyId' => $companyId,'isDel' => 0]) + ->order('id desc'); + if (!empty($keyword)){ + $query->where('title', 'like', '%'.$keyword.'%'); + } + $total = $query->count(); + $list = $query->page($page, $limit)->select()->toArray(); + + + foreach ($list as $k => &$v){ + $user = Db::name('users')->where(['id' => $v['userId']])->field('username,account')->find(); + if (!empty($user)){ + $v['userName'] = !empty($user['username']) ? $user['username'] : $user['account']; + }else{ + $v['userName'] = ''; + } + } + unset($v); + return ResponseHelper::success(['list'=>$list,'total'=>$total]); + } + + + /** + * 违禁词添加 + * @return \think\response\Json + * @throws \Exception + */ + public function createSensitiveWord(){ + $title = $this->request->param('title', ''); + $keywords = $this->request->param('keywords', ''); + $content = $this->request->param('content', ''); + $status = $this->request->param('status', 0); + $operation = $this->request->param('operation', 0); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($title) || empty($keywords)){ + return ResponseHelper::error('参数缺失'); + } + + $keywords = explode(',',$keywords); + + Db::startTrans(); + try { + $query = new SensitiveWord(); + $query->title = $title; + $query->keywords = $keywords; + $query->content = $content; + $query->status = $status; + $query->operation = $operation; + $query->userId = $userId; + $query->companyId = $companyId; + $query->createTime = time(); + $query->updateTime = time(); + $query->save(); + Db::commit(); + return ResponseHelper::success(' ','创建成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('创建失败:'.$e->getMessage()); + } + } + + + + /** + * 违禁词详情 + * @return \think\response\Json + */ + public function detailsSensitiveWord() + { + $id = $this->request->param('id', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + if (empty($id)){ + return ResponseHelper::error('参数缺失'); + } + $data = SensitiveWord::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find(); + if (empty($data)){ + return ResponseHelper::error('该素材已被删除或者不存在'); + } + $data['keywords'] = json_decode($data['keywords'],true); + $data['keywords'] = implode(',',$data['keywords']); + unset($data['createTime'],$data['updateTime'],$data['isDel'],$data['delTime']); + return ResponseHelper::success($data,'获取成功'); + } + + /** + * 违禁词删除 + * @return \think\response\Json + */ + public function delSensitiveWord() + { + $id = $this->request->param('id', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + if (empty($id)){ + return ResponseHelper::error('参数缺失'); + } + $data = SensitiveWord::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find(); + if (empty($data)){ + return ResponseHelper::error('该素材已被删除或者不存在'); + } + Db::startTrans(); + try { + $data->isDel = 1; + $data->delTime = time(); + $data->save(); + Db::commit(); + return ResponseHelper::success('','删除成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('删除失败:'.$e->getMessage()); + } + } + + + /** + * 更新违禁词 + * @return \think\response\Json + * @throws \Exception + */ + public function updateSensitiveWord(){ + $id = $this->request->param('id', ''); + $title = $this->request->param('title', ''); + $keywords = $this->request->param('keywords', ''); + $content = $this->request->param('content', ''); + $status = $this->request->param('status', 0); + $operation = $this->request->param('operation', 0); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($id) || empty($title) || empty($keywords)){ + return ResponseHelper::error('参数缺失'); + } + + $keywords = explode(',',$keywords); + + $query = SensitiveWord::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find(); + if (empty($query)){ + return ResponseHelper::error('该素材已被删除或者不存在'); + } + Db::startTrans(); + try { + $query->title = $title; + $query->keywords = $keywords; + $query->content = $content; + $query->status = $status; + $query->operation = $operation; + $query->updateTime = time(); + $query->save(); + Db::commit(); + return ResponseHelper::success(' ','修改成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('修改失败:'.$e->getMessage()); + } + } + + /** + * 修改违禁词状态 + * @return \think\response\Json + * @throws \Exception + */ + public function setSensitiveWordStatus(){ + $id = $this->request->param('id', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($id)){ + return ResponseHelper::error('参数缺失'); + } + + $query = SensitiveWord::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find(); + if (empty($query)){ + return ResponseHelper::error('该素材已被删除或者不存在'); + } + Db::startTrans(); + try { + $query->status = !empty($query['status']) ? 0 : 1;; + $query->updateTime = time(); + $query->save(); + Db::commit(); + return ResponseHelper::success(' ','修改成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('修改失败:'.$e->getMessage()); + } + } + + + //==================================================== 违禁词管理 ==================================================== + + + + + + //=================================================== 关键词词管理 ==================================================== + + /** + * 关键词列表 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function getKeywords(){ + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + $query = Keywords::where(['userId' => $userId,'companyId' => $companyId,'isDel' => 0]) + ->order('id desc'); + if (!empty($keyword)){ + $query->where('title', 'like', '%'.$keyword.'%'); + } + $total = $query->count(); + $list = $query->page($page, $limit)->select()->toArray(); + + + foreach ($list as $k => &$v){ + $v['metailGroups'] = json_decode($v['metailGroups'],true); + $v['content'] = json_decode($v['content'],true); + $v['keywords'] = json_decode($v['keywords'],true); + + $metailData = Material::where(['isDel' => 0,'userId' => $userId,'companyId' => $companyId]) + ->whereIn('id',$v['metailGroups']) + ->select()->toArray(); + $v['metailGroupsOptions'] = $metailData; + + + $user = Db::name('users')->where(['id' => $v['userId']])->field('username,account')->find(); + if (!empty($user)){ + $v['userName'] = !empty($user['username']) ? $user['username'] : $user['account']; + }else{ + $v['userName'] = ''; + } + } + unset($v); + return ResponseHelper::success(['list'=>$list,'total'=>$total]); + } + + + /** + * 关键词添加 + * @return \think\response\Json + * @throws \Exception + */ + public function createKeywords(){ + $title = $this->request->param('title', ''); + $type = $this->request->param('type', 0); + $keywords = $this->request->param('keywords', ''); + $replyType = $this->request->param('replyType', 0); + $content = $this->request->param('content',''); + $metailGroups = $this->request->param('metailGroups',[]); + $status = $this->request->param('status', 0); + $level = $this->request->param('level', 50); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($title) || empty($keywords) || (empty(metailGroups) && empty($content))){ + return ResponseHelper::error('参数缺失'); + } + + $keywords = explode(',',$keywords); + + Db::startTrans(); + try { + $query = new Keywords(); + $query->title = $title; + $query->type = $type; + $query->keywords = !empty($keywords) ? json_encode($keywords,256) : json_encode([]); + $query->replyType = $replyType; + $query->content = !empty($content) ? json_encode($content,256) : json_encode([]);; + $query->metailGroups = !empty($metailGroups) ? json_encode($metailGroups,256) : json_encode([]);; + $query->status = $status; + $query->level = $level; + $query->userId = $userId; + $query->companyId = $companyId; + $query->createTime = time(); + $query->updateTime = time(); + $query->save(); + Db::commit(); + return ResponseHelper::success(' ','创建成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('创建失败:'.$e->getMessage()); + } + } + + + + /** + * 关键词详情 + * @return \think\response\Json + */ + public function detailsKeywords() + { + $id = $this->request->param('id', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + if (empty($id)){ + return ResponseHelper::error('参数缺失'); + } + $data = Keywords::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find(); + if (empty($data)){ + return ResponseHelper::error('该素材已被删除或者不存在'); + } + + $data['metailGroups'] = json_decode($data['metailGroups'],true); + $metailData = Material::where(['isDel' => 0,'userId' => $userId,'companyId' => $companyId]) + ->whereIn('id',$data['metailGroups']) + ->select()->toArray(); + $data['metailGroupsOptions'] = $metailData; + + $data['content'] = json_decode($data['content'],true); + $data['keywords'] = json_decode($data['keywords'],true); + $data['keywords'] = implode(',',$data['keywords']); + unset($data['createTime'],$data['updateTime'],$data['isDel'],$data['delTime']); + return ResponseHelper::success($data,'获取成功'); + } + + /** + * 关键词删除 + * @return \think\response\Json + */ + public function delKeywords() + { + $id = $this->request->param('id', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + if (empty($id)){ + return ResponseHelper::error('参数缺失'); + } + $data = Keywords::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find(); + if (empty($data)){ + return ResponseHelper::error('该关键词已被删除或者不存在'); + } + Db::startTrans(); + try { + $data->isDel = 1; + $data->delTime = time(); + $data->save(); + Db::commit(); + return ResponseHelper::success('','删除成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('删除失败:'.$e->getMessage()); + } + } + + + /** + * 更新关键词 + * @return \think\response\Json + * @throws \Exception + */ + public function updateKeywords(){ + $id = $this->request->param('id', ''); + $title = $this->request->param('title', ''); + $type = $this->request->param('type', 0); + $keywords = $this->request->param('keywords', ''); + $replyType = $this->request->param('replyType', 0); + $content = $this->request->param('content',''); + $metailGroups = $this->request->param('metailGroups',''); + $status = $this->request->param('status', 0); + $level = $this->request->param('level', 50); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($title) || empty($keywords) || (empty($metailGroups) && empty($content))){ + return ResponseHelper::error('参数缺失'); + } + + $keywords = explode(',',$keywords); + + $query = Keywords::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find(); + if (empty($query)){ + return ResponseHelper::error('该素材已被删除或者不存在'); + } + Db::startTrans(); + try { + $query->title = $title; + $query->type = $type; + $query->keywords = !empty($keywords) ? json_encode($keywords,256) : json_encode([]); + $query->replyType = $replyType; + $query->content = !empty($content) ? json_encode($content,256) : json_encode([]);; + $query->metailGroups = !empty($metailGroups) ? json_encode($metailGroups,256) : json_encode([]);;; + $query->status = $status; + $query->level = $level; + $query->save(); + Db::commit(); + return ResponseHelper::success(' ','修改成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('修改失败:'.$e->getMessage()); + } + } + + /** + * 修改关键词状态 + * @return \think\response\Json + * @throws \Exception + */ + public function setKeywordStatus(){ + $id = $this->request->param('id', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($id)){ + return ResponseHelper::error('参数缺失'); + } + + $query = Keywords::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find(); + if (empty($query)){ + return ResponseHelper::error('该素材已被删除或者不存在'); + } + Db::startTrans(); + try { + $query->status = !empty($query['status']) ? 0 : 1; + $query->updateTime = time(); + $query->save(); + Db::commit(); + return ResponseHelper::success(' ','修改成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('修改失败:'.$e->getMessage()); + } + } + + + + //=================================================== 关键词词管理 ==================================================== + + + + + + +} \ No newline at end of file diff --git a/application/chukebao/controller/CustomerServiceController.php b/application/chukebao/controller/CustomerServiceController.php new file mode 100644 index 0000000..d0eefb6 --- /dev/null +++ b/application/chukebao/controller/CustomerServiceController.php @@ -0,0 +1,51 @@ +getUserInfo('s2_accountId'); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + if (empty($accountId)){ + return ResponseHelper::error('请先登录'); + } + + $accountIds1= Db::table('s2_wechat_friend')->where(['accountId' => $accountId,'isDeleted' => 0])->group('wechatAccountId')->column('wechatAccountId'); + $accountIds2 = Db::table('s2_wechat_chatroom')->where(['accountId' => $accountId,'isDeleted' => 0])->group('wechatAccountId')->column('wechatAccountId'); + // 确保即使有空数组也不会报错,并且去除重复值 + $accountIds = array_unique(array_merge($accountIds1 ?: [], $accountIds2 ?: [])); + + + + $wechatAliveTime = time() - 86400 * 30; + $list = Db::table('s2_wechat_account') + ->whereIn('id',$accountIds) + ->where('wechatAliveTime','>',$wechatAliveTime) + ->order('id desc') + ->group('id') + ->select(); + foreach ($list as $k=>&$v){ + $v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s',$v['createTime']) : ''; + $v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s',$v['updateTime']) : ''; + $v['labels'] = json_decode($v['labels'],true); + $momentsSetting = Db::name('kf_moments_settings')->where(['userId' => $userId,'companyId' => $companyId,'wechatId' =>$v['id']])->find(); + $v['momentsMax'] = !empty($momentsSetting['max']) ? $momentsSetting['max'] : 5; + $v['momentsNum'] = !empty($momentsSetting['sendNum']) ? $momentsSetting['sendNum'] : 0; + + unset( + $v['accountUserName'], + $v['accountRealName'], + $v['accountNickname'], + ); + } + unset($v); + + return ResponseHelper::success($list); + } +} \ No newline at end of file diff --git a/application/chukebao/controller/DataProcessing.php b/application/chukebao/controller/DataProcessing.php new file mode 100644 index 0000000..af8070c --- /dev/null +++ b/application/chukebao/controller/DataProcessing.php @@ -0,0 +1,206 @@ +getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + $type = $this->request->param('type', ''); + $wechatAccountId = $this->request->param('wechatAccountId', ''); + //微信好友 + $toAccountId = $this->request->param('toAccountId', ''); + $wechatFriendId = $this->request->param('wechatFriendId', ''); + $newRemark = $this->request->param('newRemark', ''); + $labels = $this->request->param('labels', []); + //微信群 + $wechatChatroomId = $this->request->param('wechatChatroomId', ''); + + //新消息 + $friendMessage = $this->request->param('friendMessage', ''); + $chatroomMessage = $this->request->param('chatroomMessage', ''); + + $typeData = [ + 'CmdModifyFriendRemark', //好友修改备注 {newRemark、wechatAccountId、wechatFriendId} + 'CmdModifyFriendLabel', //好友修改标签 {labels、wechatAccountId、wechatFriendId} + 'CmdAllotFriend', //转让好友 {labels、wechatAccountId、wechatFriendId} + 'CmdChatroomOperate', //修改群信息 {chatroomName(群名)、announce(公告)、extra(公告)、wechatAccountId、wechatChatroomId} + 'CmdNewMessage', //接收消息 + 'CmdSendMessageResult', //更新消息状态 + 'CmdPinToTop', //置顶 + ]; + + if (empty($type) || empty($wechatAccountId)) { + return ResponseHelper::error('参数缺失'); + } + + if (!in_array($type, $typeData)) { + return ResponseHelper::error('类型错误'); + } + $msg = ''; + $codee = 200; + switch ($type) { + case 'CmdModifyFriendRemark': //修改好友备注 + if(empty($wechatFriendId) || empty($newRemark)){ + return ResponseHelper::error('参数缺失'); + } + $friend = WechatFriendModel::where(['id' => $wechatFriendId,'wechatAccountId' => $wechatAccountId])->find(); + if(empty($friend)){ + return ResponseHelper::error('好友不存在'); + } + $friend->conRemark = $newRemark; + $friend->updateTime = time(); + $friend->save(); + $msg = '修改备成功'; + break; + case 'CmdModifyFriendLabel': //修改好友标签 + if(empty($wechatFriendId)){ + return ResponseHelper::error('参数缺失'); + } + $friend = WechatFriendModel::where(['id' => $wechatFriendId,'wechatAccountId' => $wechatAccountId])->find(); + if(empty($friend)){ + return ResponseHelper::error('好友不存在'); + } + $friend->labels = json_encode($labels,256); + $friend->updateTime = time(); + $friend->save(); + $msg = '修标签成功'; + break; + case 'CmdAllotFriend': //迁移好友 + if(empty($toAccountId)){ + return ResponseHelper::error('参数缺失'); + } + if(empty($wechatFriendId) && empty($wechatChatroomId)){ + return ResponseHelper::error('参数缺失'); + } + + + if (!empty($wechatFriendId)){ + $data = WechatFriendModel::where(['id' => $wechatFriendId,'wechatAccountId' => $wechatAccountId])->find(); + $msg = '好友转移成功'; + if(empty($data)){ + return ResponseHelper::error('好友不存在'); + } + } + + + if (!empty($wechatChatroomId)){ + $data = WechatChatroomModel::where(['id' => $wechatChatroomId,'wechatAccountId' => $wechatAccountId])->find(); + $msg = '群聊转移成功'; + if(empty($data)){ + return ResponseHelper::error('群聊不存在'); + } + } + + $data->accountId = $toAccountId; + $data->updateTime = time(); + $data->save(); + break; + case 'CmdNewMessage': + if(empty($friendMessage) && empty($chatroomMessage)){ + return ResponseHelper::error('参数缺失'); + } + + if(is_array($friendMessage) && is_array($chatroomMessage)){ + return ResponseHelper::error('数据类型错误'); + } + + + $messageController = new MessageController(); + if (!empty($friendMessage)){ + $res = $messageController->saveMessage($friendMessage[0]); + }else{ + $res = $messageController->saveChatroomMessage($chatroomMessage[0]); + } + if (!empty($res)){ + $msg = '消息记录成功'; + }else{ + $msg = '消息记录失败'; + $codee = 200; + } + break; + case 'CmdSendMessageResult': + $friendMessageId = $this->request->param('friendMessageId', 0); + $chatroomMessageId = $this->request->param('chatroomMessageId', 0); + $sendStatus = $this->request->param('sendStatus', null); + $wechatTime = $this->request->param('wechatTime', 0); + + if ($sendStatus === null) { + return ResponseHelper::error('sendStatus不能为空'); + } + + if (empty($friendMessageId) && empty($chatroomMessageId)) { + return ResponseHelper::error('friendMessageId或chatroomMessageId至少提供一个'); + } + + $messageId = $friendMessageId ?: $chatroomMessageId; + $update = [ + 'sendStatus' => (int)$sendStatus, + ]; + + if (!empty($wechatTime)) { + $update['wechatTime'] = strlen((string)$wechatTime) > 10 + ? intval($wechatTime / 1000) + : (int)$wechatTime; + } + + $affected = WechatMessageModel::where('id', $messageId)->update($update); + + if ($affected === false) { + return ResponseHelper::success('','更新消息状态失败'); + } + + if ($affected === 0) { + return ResponseHelper::success('','消息不存在'); + } + + $msg = '更新消息状态成功'; + break; + case 'CmdPinToTop': //置顶 + $wechatFriendId = $this->request->param('wechatFriendId', 0); + $wechatChatroomId = $this->request->param('wechatChatroomId', 0); + $isTop = $this->request->param('isTop', null); + + if ($isTop === null) { + return ResponseHelper::error('isTop不能为空'); + } + + if (empty($wechatFriendId) && empty($wechatChatroomId)) { + return ResponseHelper::error('wechatFriendId或chatroomId至少提供一个'); + } + + + if (!empty($wechatFriendId)){ + $data = WechatFriendModel::where(['id' => $wechatFriendId,'wechatAccountId' => $wechatAccountId])->find(); + $msg = $isTop == 1 ? '已置顶' : '取消置顶'; + if(empty($data)){ + return ResponseHelper::error('好友不存在'); + } + } + + + if (!empty($wechatChatroomId)){ + $data = WechatChatroomModel::where(['id' => $wechatChatroomId,'wechatAccountId' => $wechatAccountId])->find(); + $msg = $isTop == 1 ? '已置顶' : '取消置顶'; + if(empty($data)){ + return ResponseHelper::error('群聊不存在'); + } + } + + $data->updateTime = time(); + $data->isTop = $isTop; + $data->save(); + break; + } + return ResponseHelper::success('',$msg,$codee); + } +} \ No newline at end of file diff --git a/application/chukebao/controller/FollowUpController.php b/application/chukebao/controller/FollowUpController.php new file mode 100644 index 0000000..35d7d56 --- /dev/null +++ b/application/chukebao/controller/FollowUpController.php @@ -0,0 +1,144 @@ +request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + $isRemind = $this->request->param('isRemind', ''); + $isProcess = $this->request->param('isProcess', ''); + $type = $this->request->param('type', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + + $where = [ + ['companyId','=',$companyId], + ['userId' ,'=', $userId] + ]; + + if ($isRemind != '') { + $where[] = ['isRemind','=',$isRemind]; + } + if ($type != '') { + $where[] = ['type','=',$type]; + } + if ($isProcess != '') { + $where[] = ['isProcess','=',$isProcess]; + } + + if(!empty($keyword)){ + $where[] = ['title|description','like','%'.$keyword.'%']; + } + + $query = FollowUp::where($where); + + $total = $query->count(); + $list = $query->where($where)->page($page,$limit)->order('id desc')->select(); + + + foreach ($list as &$item) { + $nickname = Db::table('s2_wechat_friend')->where(['id' => $item['friendId']])->value('nickname'); + $item['nickname'] = !empty($nickname) ? $nickname : '-'; + $item['reminderTime'] = date('Y-m-d H:i:s',$item['reminderTime']); + } + unset($item); + + return ResponseHelper::success(['list'=>$list,'total'=>$total]); + } + + + /** + * 添加 + * @return \think\response\Json + * @throws \Exception + */ + public function create(){ + $type = $this->request->param('type', 0); + $title = $this->request->param('title', ''); + $reminderTime = $this->request->param('reminderTime', ''); + $description = $this->request->param('description', ''); + $friendId = $this->request->param('friendId', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($title) || empty($reminderTime) || empty($description) || empty($friendId)){ + return ResponseHelper::error('参数缺失'); + } + $friend = Db::table('s2_wechat_friend')->where(['id' => $friendId])->find(); + if (empty($friend)) { + return ResponseHelper::error('好友不存在'); + } + + + Db::startTrans(); + try { + $FollowUp = new FollowUp(); + $FollowUp->type = $type; + $FollowUp->title = $title; + $FollowUp->friendId = $friendId; + $FollowUp->reminderTime = !empty($reminderTime) ? strtotime($reminderTime) : time(); + $FollowUp->description = $description; + $FollowUp->userId = $userId; + $FollowUp->companyId = $companyId; + $FollowUp->updateTime = time(); + $FollowUp->createTime = time(); + $FollowUp->save(); + Db::commit(); + return ResponseHelper::success(' ','创建成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('创建失败:'.$e->getMessage()); + } + } + + + /** + * 处理代办事项 + * @return \think\response\Json + * @throws \Exception + */ + public function process(){ + $ids = $this->request->param('ids',''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($ids)){ + return ResponseHelper::error('参数缺失'); + } + $ids = explode(',',$ids); + + if (!is_array($ids)){ + return ResponseHelper::error('格式错误'); + } + + $FollowUpIds = FollowUp::where(['userId' => $userId,'companyId' => $companyId,'isProcess' => 0])->whereIn('id',$ids)->column('id'); + if (empty($FollowUpIds)){ + return ResponseHelper::error('代办事项不存在'); + } + + Db::startTrans(); + try { + FollowUp::whereIn('id',$FollowUpIds)->update(['isProcess' => 1,'isRemind' => 1,'updateTime' => time()]); + Db::commit(); + return ResponseHelper::success(' ','已处理'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('处理失败:'.$e->getMessage()); + } + + + + + + } + +} \ No newline at end of file diff --git a/application/chukebao/controller/LoginController.php b/application/chukebao/controller/LoginController.php new file mode 100644 index 0000000..832d055 --- /dev/null +++ b/application/chukebao/controller/LoginController.php @@ -0,0 +1,141 @@ +request->param('account', ''); + $password = !empty($password) ? $password : $this->request->param('password', ''); + $verifySessionId =!empty($verifySessionId) ? $verifySessionId : $this->request->param('verifySessionId', ''); + $verifyCode = !empty($verifyCode) ? $verifyCode : $this->request->param('verifyCode', ''); + $token = JwtUtil::getRequestToken(); + $payload = ''; + if (!empty($token)){ + $payload = JwtUtil::verifyToken($token); + } + + if ((empty($username) || empty($password)) && empty($payload)){ + return ResponseHelper::error('请输入账号密码'); + } + + // 验证账号是否存在(支持账号或手机号登录) + if (empty($payload11)){ + $user = Db::name('users') + ->where(function ($query) use ($username) { + $query->where('account', $username)->whereOr('phone', $username); + }) + ->where(function ($query2) use ($password) { + $query2->where('passwordMd5', md5($password))->whereOr('passwordLocal', localEncrypt($password)); + }) + ->find(); + }else{ + $user = $payload; + } + + if (empty($user)) { + return ResponseHelper::error('账号不存在或密码错误'); + } + + if($user['status'] != 1){ + return ResponseHelper::error('账号已禁用'); + } + + //登录参数 + $params = [ + 'grant_type' => 'password', + 'username' => $user['account'], + 'password' => !empty($user['passwordLocal']) ? localDecrypt($user['passwordLocal']) : $password + ]; + try { + // 调用登录接口获取token + $headerData = ['client:kefu-client']; + if (!empty($verifySessionId) && !empty($verifyCode)){ + $headerData[] = 'verifysessionid:'.$verifySessionId; + $headerData[] = 'verifycode:'.$verifyCode; + } + $header = setHeader($headerData, '', 'plain'); + $result = requestCurl('https://s2.siyuguanli.com:9991/token', $params, 'POST', $header); + $result = handleApiResponse($result); + if (isset($result['access_token']) && !empty($result['access_token'])) { + $kefuData['token'] = $result; + $headerData = ['client:kefu-client']; + $header = setHeader($headerData, $result['access_token']); + $result2 = requestCurl('https://s2.siyuguanli.com:9991/api/account/self', [], 'GET', $header, 'json'); + $self = handleApiResponse($result2); + $kefuData['self'] = $self; + Db::name('users')->where('id', $user['id'])->update(['passwordLocal' => localEncrypt($params['password']),'updateTime' => time()]); + }else{ + $kefuData = [ + 'token' => [ + "access_token"=> "27gINKZqGux6V4j9QLawOcTKlWXg-j4zxQjKvScvDTq-YlLcwIrDP2AFaNZKnOo9zLzepOBC8qrdXh4z9GxxkwE9TKGRQI1FjITRlMZzrim13IbSEbJUoywGs_BhDmIZnnPhfjqxDB1vjZgVtT2Kp4bxbUCV3i2uO_FTv_DT2G7NUFFLjq8oIuUrd_c1YXeYkH8m8Fw1AM4yPZJZyfdaHSSMOpJ2Bk2LAghnB6OaZCYWNFQcwWARsmh1BSAANUOAoadjkztZC7Fme-GGOm2sLo0WL6Mf26NfeLmnkluewTiPMyacD7RYclAR2LZ_8Mhwr3pwRg", + "token_type"=> "bearer", + "expires_in"=> 195519999, + "refresh_token"=> "a9545daa-d1c4-4c87-8c4c-b713631d4f0d" + ], + 'self' => [ + 'account' => [ + "id"=> 5538, + "realName"=> "测试", + "nickname"=> "", + "memo"=> "", + "avatar"=> "", + "userName"=> "wz_02", + "secret"=> "8f6f743395ad4198b6a4c0e6ca0e452f", + "accountType"=> 10, + "departmentId"=> 2130, + "useGoogleSecretKey"=> false, + "hasVerifyGoogleSecret"=> true + ], + 'tenant' => [ + "id" => 242, + "name"=> "泉州市卡若网络技术有限公司", + "guid"=> "5E2C38F5A275450D935F3ECEC076124E", + "thirdParty"=> null, + "tenantType"=> 0, + "deployName"=> "deploy-s2" + ] + ] + ]; + //return ResponseHelper::error($result['error_description']); + } + + + unset($user['passwordMd5'],$user['deleteTime']); + $userData['member'] = $user; + + // 生成JWT令牌 + $expired = 86400 * 30; + $token = JwtUtil::createToken($user, $expired); + $token_expired = time() + $expired; + + $userData['token'] = $token; + $userData['token_expired'] = $token_expired; + $userData['kefuData'] = $kefuData; + + return ResponseHelper::success($userData, '登录成功'); + } catch (Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/chukebao/controller/MessageController.php b/application/chukebao/controller/MessageController.php new file mode 100644 index 0000000..3ffeb9f --- /dev/null +++ b/application/chukebao/controller/MessageController.php @@ -0,0 +1,476 @@ +baseUrl = Env::get('api.wechat_url'); + $this->authorization = AuthService::getSystemAuthorization(); + } + + public function getList() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $accountId = $this->getUserInfo('s2_accountId'); + if (empty($accountId)) { + return ResponseHelper::error('请先登录'); + } + + $friends = Db::table('s2_wechat_friend') + ->where(['accountId' => $accountId, 'isDeleted' => 0]) + ->column('id,nickname,avatar,conRemark,labels,groupId,wechatAccountId,wechatId,extendFields,phone,region,isTop'); + + + // 构建好友子查询 + $friendSubQuery = Db::table('s2_wechat_friend') + ->where(['accountId' => $accountId, 'isDeleted' => 0]) + ->field('id') + ->buildSql(); + + // 优化后的查询:使用MySQL兼容的查询方式 + $unionQuery = " + (SELECT m.id, m.content, m.wechatFriendId, m.wechatChatroomId, m.createTime, m.wechatTime,m.wechatAccountId, 2 as msgType, wc.nickname, wc.chatroomAvatar as avatar, wc.chatroomId, wc.isTop + FROM s2_wechat_chatroom wc + INNER JOIN s2_wechat_message m ON wc.id = m.wechatChatroomId AND m.type = 2 + INNER JOIN ( + SELECT wechatChatroomId, MAX(wechatTime) as maxTime, MAX(id) as maxId + FROM s2_wechat_message + WHERE type = 2 + GROUP BY wechatChatroomId + ) latest ON m.wechatChatroomId = latest.wechatChatroomId AND m.wechatTime = latest.maxTime AND m.id = latest.maxId + WHERE wc.accountId = {$accountId} AND wc.isDeleted = 0 + ) + UNION ALL + (SELECT m.id, m.content, m.wechatFriendId, m.wechatChatroomId, m.createTime, m.wechatTime, 1 as msgType, 1 as nickname, 1 as avatar, 1 as chatroomId, 1 as wechatAccountId, 0 as isTop + FROM s2_wechat_message m + INNER JOIN ( + SELECT wechatFriendId, MAX(wechatTime) as maxTime, MAX(id) as maxId + FROM s2_wechat_message + WHERE type = 1 AND wechatFriendId IN {$friendSubQuery} + GROUP BY wechatFriendId + ) latest ON m.wechatFriendId = latest.wechatFriendId AND m.wechatTime = latest.maxTime AND m.id = latest.maxId + WHERE m.type = 1 AND m.wechatFriendId IN {$friendSubQuery} + ) + ORDER BY wechatTime DESC + LIMIT " . (($page - 1) * $limit) . ", {$limit} + "; + + $list = Db::query($unionQuery); + + // 对分页后的结果进行排序(按wechatTime降序) + usort($list, function ($a, $b) { + return $b['wechatTime'] <=> $a['wechatTime']; + }); + + // 批量统计未读数量(isRead=0),按好友/群聊分别聚合 + $friendIds = []; + $chatroomIds = []; + foreach ($list as $row) { + if (!empty($row['wechatFriendId'])) { + $friendIds[] = $row['wechatFriendId']; + } + if (!empty($row['wechatChatroomId'])) { + $chatroomIds[] = $row['wechatChatroomId']; + } + } + $friendIds = array_values(array_unique(array_filter($friendIds))); + $chatroomIds = array_values(array_unique(array_filter($chatroomIds))); + + $friendUnreadMap = []; + if (!empty($friendIds)) { + // 获取未读消息数量 + $friendUnreadMap = Db::table('s2_wechat_message') + ->where(['isRead' => 0]) + ->whereIn('wechatFriendId', $friendIds) + ->group('wechatFriendId') + ->column('COUNT(*) AS cnt', 'wechatFriendId'); + } + + $chatroomUnreadMap = []; + if (!empty($chatroomIds)) { + // 获取未读消息数量 + $chatroomUnreadMap = Db::table('s2_wechat_message') + ->where(['isRead' => 0]) + ->whereIn('wechatChatroomId', $chatroomIds) + ->group('wechatChatroomId') + ->column('COUNT(*) AS cnt', 'wechatChatroomId'); + } + + $aiTypeData = []; + if (!empty($friendIds)) { + $aiTypeData = FriendSettings::where('friendId', 'in', $friendIds)->column('friendId,type'); + } + + foreach ($list as $k => &$v) { + + $createTime = !empty($v['createTime']) ? date('Y-m-d H:i:s', $v['createTime']) : ''; + $wechatTime = !empty($v['wechatTime']) ? date('Y-m-d H:i:s', $v['wechatTime']) : ''; + + + $unreadCount = 0; + $v['aiType'] = 0; + if (!empty($v['wechatFriendId'])) { + $v['nickname'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['nickname'] : ''; + $v['avatar'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['avatar'] : ''; + $v['conRemark'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['conRemark'] : ''; + $v['groupId'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['groupId'] : ''; + $v['wechatAccountId'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['wechatAccountId'] : ''; + $v['wechatId'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['wechatId'] : ''; + $v['extendFields'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['extendFields'] : []; + $v['region'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['region'] : ''; + $v['phone'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['phone'] : ''; + $v['isTop'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['isTop'] : 0; + $v['labels'] = !empty($friends[$v['wechatFriendId']]) ? json_decode($friends[$v['wechatFriendId']]['labels'], true) : []; + + $unreadCount = isset($friendUnreadMap[$v['wechatFriendId']]) ? (int)$friendUnreadMap[$v['wechatFriendId']] : 0; + $v['aiType'] = isset($aiTypeData[$v['wechatFriendId']]) ? $aiTypeData[$v['wechatFriendId']] : 0; + unset($v['chatroomId']); + } + + if (!empty($v['wechatChatroomId'])) { + $v['conRemark'] = ''; + $unreadCount = isset($chatroomUnreadMap[$v['wechatChatroomId']]) ? (int)$chatroomUnreadMap[$v['wechatChatroomId']] : 0; + } + + $v['id'] = !empty($v['wechatFriendId']) ? $v['wechatFriendId'] : $v['wechatChatroomId']; + $v['config'] = [ + 'top' => !empty($v['isTop']) ? true : false, + 'unreadCount' => $unreadCount, + 'chat' => true, + 'msgTime' => $v['wechatTime'], + ]; + $v['createTime'] = $createTime; + $v['lastUpdateTime'] = $wechatTime; + + // 最新消息内容已经在UNION查询中获取,直接使用 + $v['latestMessage'] = [ + 'content' => $v['content'], + 'wechatTime' => $wechatTime + ]; + + unset($v['wechatFriendId'], $v['wechatChatroomId'],$v['isTop']); + + } + unset($v); + return ResponseHelper::success($list); + } + + + public function readMessage() + { + $wechatFriendId = $this->request->param('wechatFriendId', ''); + $wechatChatroomId = $this->request->param('wechatChatroomId', ''); + $accountId = $this->getUserInfo('s2_accountId'); + if (empty($accountId)) { + return ResponseHelper::error('请先登录'); + } + if (empty($wechatChatroomId) && empty($wechatFriendId)) { + return ResponseHelper::error('参数缺失'); + } + + $where = []; + if (!empty($wechatChatroomId)) { + $where[] = ['wechatChatroomId', '=', $wechatChatroomId]; + } + + if (!empty($wechatFriendId)) { + $where[] = ['wechatFriendId', '=', $wechatFriendId]; + } + + Db::table('s2_wechat_message')->where($where)->update(['isRead' => 1]); + return ResponseHelper::success([]); + } + + + /** + * 获取单条消息发送状态(带轮询功能) + * @return \think\response\Json + */ + public function getMessageStatus() + { + $messageId = $this->request->param('messageId', 0); + $wechatAccountId = $this->request->param('wechatAccountId', ''); + $accountId = $this->getUserInfo('s2_accountId'); + $wechatFriendId = $this->request->param('wechatFriendId', ''); + $wechatChatroomId = $this->request->param('wechatChatroomId', ''); + + if (empty($accountId)) { + return ResponseHelper::error('请先登录'); + } + + if (empty($messageId)) { + return ResponseHelper::error('消息ID不能为空'); + } + + if(empty($wechatFriendId) && empty($wechatChatroomId)) { + return ResponseHelper::error('消息类型不能为空'); + } + + // 查询单条消息的基本信息(只需要发送状态相关字段) + $message = Db::table('s2_wechat_message') + ->where('id', $messageId) + ->field('id,wechatAccountId,wechatFriendId,wechatChatroomId,sendStatus') + ->find(); + + if (empty($message)) { + $message = [ + 'id' => $messageId, + 'wechatAccountId' => $wechatAccountId, + 'wechatFriendId' => $wechatFriendId, + 'wechatChatroomId' => $wechatChatroomId, + 'sendStatus' => 0, + ]; + } + + $sendStatus = isset($message['sendStatus']) ? (int)$message['sendStatus'] : 0; + $isUpdated = false; + $pollCount = 0; + $maxPollCount = 10; // 最多轮询10次 + + // 如果sendStatus不为0,开始轮询 + if ($sendStatus != 0) { + $messageRequest = [ + 'id' => $message['id'], + 'wechatAccountId' => !empty($wechatAccountId) ? $wechatAccountId : $message['wechatAccountId'], + 'wechatFriendId' => !empty($message['wechatFriendId']) ? $message['wechatFriendId'] : '', + 'wechatChatroomId' => !empty($message['wechatChatroomId']) ? $message['wechatChatroomId'] : '', + 'from' => '', + 'to' => '', + ]; + + + // 轮询逻辑:最多10次 + while ($pollCount < $maxPollCount && $sendStatus != 0) { + $pollCount++; + + // 请求线上接口获取最新状态 + $newData = $this->fetchLatestMessageFromApi($messageRequest); + + if (!empty($newData)) { + // 重新查询消息状态(可能已更新) + $updatedMessage = Db::table('s2_wechat_message') + ->where('id', $messageId) + ->field('sendStatus') + ->find(); + + if (!empty($updatedMessage)) { + $newSendStatus = isset($updatedMessage['sendStatus']) ? (int)$updatedMessage['sendStatus'] : 0; + + // 如果状态已更新为0(已发送),停止轮询 + if ($newSendStatus == 0) { + $sendStatus = 0; + $isUpdated = true; + break; + } + + // 如果状态仍然是1,继续轮询(但需要等待一下,避免请求过快) + if ($newSendStatus != 0 && $pollCount < $maxPollCount) { + // 每次轮询间隔500毫秒(0.5秒) + usleep(500000); + } + } + } else { + // 如果请求失败,等待后继续尝试 + if ($pollCount < $maxPollCount) { + usleep(500000); + } + } + } + } + + // 返回发送状态信息 + return ResponseHelper::success([ + 'messageId' => $messageId, + 'sendStatus' => $sendStatus, + 'statusText' => $sendStatus == 0 ? '已发送' : '发送中' + ]); + } + + public function details() + { + $wechatFriendId = $this->request->param('wechatFriendId', ''); + $wechatChatroomId = $this->request->param('wechatChatroomId', ''); + $wechatAccountId = $this->request->param('wechatAccountId', ''); + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $from = $this->request->param('From', ''); + $to = $this->request->param('To', ''); + $olderData = $this->request->param('olderData', false); + $accountId = $this->getUserInfo('s2_accountId'); + if (empty($accountId)) { + return ResponseHelper::error('请先登录'); + } + if (empty($wechatChatroomId) && empty($wechatFriendId)) { + return ResponseHelper::error('参数缺失'); + } + + $where = []; + if (!empty($wechatChatroomId)) { + $where[] = ['wechatChatroomId', '=', $wechatChatroomId]; + } + + if (!empty($wechatFriendId)) { + $where[] = ['wechatFriendId', '=', $wechatFriendId]; + } + + if (!empty($From) && !empty($To)) { + $where[] = ['wechatTime', 'between', [$from, $to]]; + } + + $total = Db::table('s2_wechat_message')->where($where)->count(); + $list = Db::table('s2_wechat_message')->where($where)->page($page, $limit)->order('id DESC')->select(); + + // 检查消息是否有sendStatus字段,如果有且不为0,则请求线上最新接口 + foreach ($list as $k => &$item) { + // 检查是否存在sendStatus字段且不为0(0表示已发送成功) + if (isset($item['sendStatus']) && $item['sendStatus'] != 0) { + // 需要请求新的数据 + $messageRequest = [ + 'id' => $item['id'], + 'wechatAccountId' => $wechatAccountId, + 'wechatFriendId' => $wechatFriendId, + 'wechatChatroomId' => $wechatChatroomId, + 'from' => '', + 'to' => '', + ]; + $newData = $this->fetchLatestMessageFromApi($messageRequest); + if (!empty($newData)){ + $item['sendStatus'] = 0; + } + } + // 格式化时间 + $item['wechatTime'] = !empty($item['wechatTime']) ? date('Y-m-d H:i:s', $item['wechatTime']) : ''; + } + unset($item); + + + return ResponseHelper::success(['total' => $total, 'list' => $list]); + } + + + + + + + + /** + * 从线上接口获取最新消息 + * @param array $messageRequest 消息项(包含wechatAccountId、wechatFriendId或wechatChatroomId、id等) + * @return array|null 最新消息数据,失败返回null + */ + private function fetchLatestMessageFromApi($messageRequest) + { + if (empty($this->baseUrl) || empty($this->authorization)) { + return null; + } + + try { + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $this->authorization, 'json'); + + // 判断是好友消息还是群聊消息 + if (!empty($messageRequest['wechatFriendId'])) { + // 好友消息接口 + $params = [ + 'keyword' => '', + 'msgType' => '', + 'accountId' => '', + 'count' => 20, // 获取多条消息以便找到对应的消息 + 'messageId' => isset($messageRequest['id']) ? $messageRequest['id'] : '', + 'olderData' => true, + 'wechatAccountId' => $messageRequest['wechatAccountId'], + 'wechatFriendId' => $messageRequest['wechatFriendId'], + 'from' => $messageRequest['from'], + 'to' => $messageRequest['to'], + 'searchFrom' => 'admin' + ]; + $result = requestCurl($this->baseUrl . 'api/FriendMessage/searchMessage', $params, 'GET', $header, 'json'); + $response = handleApiResponse($result); + // 查找对应的消息 + if (!empty($response) && is_array($response)) { + $data = $response[0]; + if ($data['sendStatus'] == 0){ + WechatMessageModel::where(['id' => $data['id']])->update(['sendStatus' => 0]); + return true; + } + } + return false; + } elseif (!empty($messageRequest['wechatChatroomId'])) { + // 群聊消息接口 + $params = [ + 'keyword' => '', + 'msgType' => '', + 'accountId' => '', + 'count' => 20, // 获取多条消息以便找到对应的消息 + 'messageId' => isset($messageRequest['id']) ? $messageRequest['id'] : '', + 'olderData' => true, + 'wechatId' => '', + 'wechatAccountId' => $messageRequest['wechatAccountId'], + 'wechatChatroomId' => $messageRequest['wechatChatroomId'], + 'from' => $messageRequest['from'], + 'to' => $messageRequest['to'], + 'searchFrom' => 'admin' + ]; + + $result = requestCurl($this->baseUrl . 'api/ChatroomMessage/searchMessage', $params, 'GET', $header, 'json'); + $response = handleApiResponse($result); + + // 查找对应的消息 + if (!empty($response) && is_array($response)) { + $data = $response[0]; + if ($data['sendStatus'] == 0){ + WechatMessageModel::where(['id' => $data['id']])->update(['sendStatus' => 0]); + return true; + } + } + return false; + } + } catch (\Exception $e) { + // 记录错误日志,但不影响主流程 + \think\facade\Log::error('获取线上最新消息失败:' . $e->getMessage()); + } + + return null; + } + + /** + * 更新数据库中的消息 + * @param array $latestMessage 线上获取的最新消息 + * @param array $oldMessage 旧消息数据 + */ + private function updateMessageInDatabase($latestMessage, $oldMessage) + { + try { + // 使用API模块的MessageController来保存消息 + $apiMessageController = new \app\api\controller\MessageController(); + + // 判断是好友消息还是群聊消息 + if (!empty($oldMessage['wechatFriendId'])) { + // 保存好友消息 + $apiMessageController->saveMessage($latestMessage); + } elseif (!empty($oldMessage['wechatChatroomId'])) { + // 保存群聊消息 + $apiMessageController->saveChatroomMessage($latestMessage); + } + } catch (\Exception $e) { + // 记录错误日志,但不影响主流程 + \think\facade\Log::error('更新数据库消息失败:' . $e->getMessage()); + } + } + +} \ No newline at end of file diff --git a/application/chukebao/controller/MomentsController.php b/application/chukebao/controller/MomentsController.php new file mode 100644 index 0000000..f0d6a4d --- /dev/null +++ b/application/chukebao/controller/MomentsController.php @@ -0,0 +1,504 @@ +getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + // 获取请求参数 + $text = $this->request->param('content', ''); // 朋友圈内容 + $picUrlList = $this->request->param('picUrlList', []); // 图片列表 + $videoUrl = $this->request->param('videoUrl', ''); // 视频链接 + $link = $this->request->param('link', []); // 链接信息 + $momentContentType = (int)$this->request->param('type', 1); // 内容类型 1文本 2图文 3视频 4链接 + $publicMode = (int)$this->request->param('publicMode', 0); // 公开模式 + $wechatIds = $this->request->param('wechatIds', []); // 微信账号ID列表 + $labels = $this->request->param('labels', []); // 标签列表 + $timingTime = $this->request->param('timingTime', date('Y-m-d H:i:s')); // 定时发布时间 + $immediately = $this->request->param('immediately', false); // 是否立即发布 + + // 格式化时间字符串为统一格式 + $timingTime = $this->normalizeTimingTime($timingTime); + if ($timingTime === false) { + return ResponseHelper::error('定时发布时间格式不正确'); + } + + // 参数验证 + if (empty($text) && empty($picUrlList) && empty($videoUrl)) { + return ResponseHelper::error('朋友圈内容不能为空'); + } + + if (empty($wechatIds)) { + return ResponseHelper::error('请选择发布账号'); + } + + // 校验内容类型 + if (!in_array($momentContentType, [1, 2, 3, 4])) { + return ResponseHelper::error('内容类型不合法,支持:1文本 2图文 3视频 4链接'); + } + + if(!empty($labels)){ + $publicMode = 2; + } + + // 根据内容类型校验必要参数 + switch ($momentContentType) { + case 1: // 文本 + if (empty($text)) { + return ResponseHelper::error('文本类型必须填写内容'); + } + break; + case 2: // 图文 + if (empty($text) || empty($picUrlList)) { + return ResponseHelper::error('图文类型必须填写内容和上传图片'); + } + break; + case 3: // 视频 + if (empty($videoUrl)) { + return ResponseHelper::error('视频类型必须上传视频'); + } + break; + case 4: // 链接 + if (empty($link)) { + return ResponseHelper::error('链接类型必须填写链接信息'); + } + if (empty($link['url'])) { + return ResponseHelper::error('链接类型必须填写链接地址'); + } + if (empty($link['desc'])) { + return ResponseHelper::error('链接类型必须填写链接描述'); + } + if (empty($link['image'])) { + return ResponseHelper::error('链接类型必须填写链接图片'); + } + break; + } + + // 处理链接信息 - 所有链接都必须验证 + if (!empty($link)) { + $link = [ + 'desc' => $link['desc'] ?? '', + 'image' => $link['image'] ?? '', + 'url' => $link['url'] ?? '' + ]; + + // 验证链接URL格式 + if (!empty($link['url']) && !filter_var($link['url'], FILTER_VALIDATE_URL)) { + return ResponseHelper::error('链接地址格式不正确'); + } + } else { + $link = ['desc' => '', 'image' => '', 'url' => '']; + } + + // 构建发布账号列表 + $jobPublishWechatMomentsItems = $this->buildJobPublishWechatMomentsItems($wechatIds, $labels); + if (empty($jobPublishWechatMomentsItems)) { + return ResponseHelper::error('无法获取有效的发布账号信息'); + } + + try { + // 构建发送数据 + $sendData = [ + 'altList' => '', + 'beginTime' => $timingTime, + 'endTime' => date('Y-m-d H:i:s', strtotime($timingTime) + 3600), + 'immediately' => $immediately, + 'isUseLocation' => false, + 'jobPublishWechatMomentsItems' => $jobPublishWechatMomentsItems, + 'lat' => 0, + 'lng' => 0, + 'link' => $link, + 'momentContentType' => $momentContentType, + 'picUrlList' => $picUrlList, + 'poiAddress' => '', + 'poiName' => '', + 'publicMode' => $publicMode, + 'text' => $text, + 'timingTime' => $timingTime ?: date('Y-m-d H:i:s'), + 'videoUrl' => $videoUrl + ]; + + // 保存到数据库 + $moments = new KfMoments(); + $moments->companyId = $companyId; + $moments->userId = $userId; + $moments->sendData = json_encode($sendData, 256); + $nowTs = time(); + $moments->createTime = $nowTs; + $moments->updateTime = $nowTs; + $moments->isDel = 0; + $moments->delTime = null; + $moments->isSend = $immediately ? 1 : 0; + $moments->sendTime = $immediately ? $nowTs : strtotime($timingTime); + $moments->save(); + + // 如果立即发布,调用发布接口 + if ($immediately) { + $this->publishMoments($sendData); + } + return ResponseHelper::success('', '朋友圈创建成功'); + } catch (\Exception $e) { + return ResponseHelper::error('创建失败:' . $e->getMessage()); + } + } + + /** + * 编辑朋友圈 + * @return \think\response\Json + */ + public function update() + { + $id = (int)$this->request->param('id', 0); + if ($id <= 0) { + return ResponseHelper::error('ID不合法'); + } + + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + // 获取请求参数(与创建一致的字段名) + $text = $this->request->param('content', ''); + $picUrlList = $this->request->param('picUrlList', []); + $videoUrl = $this->request->param('videoUrl', ''); + $link = $this->request->param('link', []); + $momentContentType = (int)$this->request->param('type', 1); + $publicMode = (int)$this->request->param('publicMode', 0); + $wechatIds = $this->request->param('wechatIds', []); + $labels = $this->request->param('labels', []); + $timingTime = $this->request->param('timingTime', date('Y-m-d H:i:s')); + $immediately = $this->request->param('immediately', false); + + // 格式化时间字符串为统一格式 + $timingTime = $this->normalizeTimingTime($timingTime); + if ($timingTime === false) { + return ResponseHelper::error('定时发布时间格式不正确'); + } + + // 读取待编辑记录 + /** @var KfMoments|null $moments */ + $moments = KfMoments::where(['id' => $id, 'companyId' => $companyId, 'userId' => $userId, 'isDel' => 0])->find(); + if (empty($moments)) { + return ResponseHelper::error('朋友圈不存在'); + } + + // 参数校验 + if (empty($text) && empty($picUrlList) && empty($videoUrl)) { + return ResponseHelper::error('朋友圈内容不能为空'); + } + if (empty($wechatIds)) { + return ResponseHelper::error('请选择发布账号'); + } + if (!in_array($momentContentType, [1, 2, 3, 4])) { + return ResponseHelper::error('内容类型不合法,支持:1文本 2图文 3视频 4链接'); + } + if (!empty($labels)) { + $publicMode = 2; + } + switch ($momentContentType) { + case 1: + if (empty($text)) { + return ResponseHelper::error('文本类型必须填写内容'); + } + break; + case 2: + if (empty($text) || empty($picUrlList)) { + return ResponseHelper::error('图文类型必须填写内容和上传图片'); + } + break; + case 3: + if (empty($videoUrl)) { + return ResponseHelper::error('视频类型必须上传视频'); + } + break; + case 4: + if (empty($link)) { + return ResponseHelper::error('链接类型必须填写链接信息'); + } + if (empty($link['url'])) { + return ResponseHelper::error('链接类型必须填写链接地址'); + } + if (empty($link['desc'])) { + return ResponseHelper::error('链接类型必须填写链接描述'); + } + if (empty($link['image'])) { + return ResponseHelper::error('链接类型必须填写链接图片'); + } + break; + } + if (!empty($link)) { + $link = [ + 'desc' => $link['desc'] ?? '', + 'image' => $link['image'] ?? '', + 'url' => $link['url'] ?? '' + ]; + if (!empty($link['url']) && !filter_var($link['url'], FILTER_VALIDATE_URL)) { + return ResponseHelper::error('链接地址格式不正确'); + } + } else { + $link = ['desc' => '', 'image' => '', 'url' => '']; + } + + // 构建账号列表 + $jobPublishWechatMomentsItems = $this->buildJobPublishWechatMomentsItems($wechatIds, $labels); + if (empty($jobPublishWechatMomentsItems)) { + return ResponseHelper::error('无法获取有效的发布账号信息'); + } + + try { + $sendData = [ + 'altList' => '', + 'beginTime' => $timingTime, + 'endTime' => date('Y-m-d H:i:s', strtotime($timingTime) + 1800), + 'immediately' => $immediately, + 'isUseLocation' => false, + 'jobPublishWechatMomentsItems' => $jobPublishWechatMomentsItems, + 'lat' => 0, + 'lng' => 0, + 'link' => $link, + 'momentContentType' => $momentContentType, + 'picUrlList' => $picUrlList, + 'poiAddress' => '', + 'poiName' => '', + 'publicMode' => $publicMode, + 'text' => $text, + 'timingTime' => $timingTime ?: date('Y-m-d H:i:s'), + 'videoUrl' => $videoUrl + ]; + + $moments->sendData = json_encode($sendData, 256); + $moments->isSend = $immediately ? 1 : 0; + $moments->sendTime = $immediately ? time() : strtotime($timingTime); + $moments->updateTime = time(); + $moments->save(); + + if ($immediately) { + $this->publishMoments($sendData); + } + + return ResponseHelper::success('', '朋友圈更新成功'); + } catch (\Exception $e) { + return ResponseHelper::error('更新失败:' . $e->getMessage()); + } + } + + /** + * 构建发布账号列表 + * @param array $wechatIds 微信账号ID列表 + * @param array $labels 标签列表 + * @return array + */ + private function buildJobPublishWechatMomentsItems($wechatIds, $labels) + { + try { + // 查询微信账号信息 + $wechatAccounts = Db::table('s2_wechat_account') + ->whereIn('id', $wechatIds) + ->field('id,labels') + ->select(); + if (empty($wechatAccounts)) { + return []; + } + + $result = []; + foreach ($wechatAccounts as $account) { + $accountLabels = []; + + // 如果账号有标签,解析标签 + if (!empty($account['labels'])) { + $accountLabels = is_string($account['labels']) + ? json_decode($account['labels'], true) + : $account['labels']; + } + + // 取传入标签与账号标签的交集 + $finalLabels = array_intersect($labels, $accountLabels); + + $result[] = [ + 'wechatAccountId' => $account['id'], + 'labels' => array_values($finalLabels), // 重新索引数组 + 'comments' => [] + ]; + } + + return $result; + + } catch (\Exception $e) { + \think\facade\Log::error('构建发布账号列表失败:' . $e->getMessage()); + return []; + } + } + + /** + * 发布朋友圈到微信 + * @param array $sendData + * @return bool + */ + private function publishMoments($sendData) + { + try { + // 这里调用实际的朋友圈发布接口 + // 根据您的系统架构,可能需要调用 WebSocket 或其他服务 + // 示例:调用 MomentsController 的 addJob 方法 + $moments = new \app\api\controller\MomentsController(); + return $moments->addJob($sendData); + } catch (\Exception $e) { + // 记录错误日志 + \think\facade\Log::error('朋友圈发布失败:' . $e->getMessage()); + return false; + } + } + + /** + * 获取朋友圈列表 + * @return \think\response\Json + */ + public function getList() + { + $page = (int)$this->request->param('page', 1); + $limit = (int)$this->request->param('limit', 10); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + try { + $list = KfMoments::where(['companyId' => $companyId, 'userId' => $userId, 'isDel' => 0]) + ->order('createTime desc') + ->page($page, $limit) + ->select(); + + $total = KfMoments::where(['companyId' => $companyId, 'userId' => $userId, 'isDel' => 0])->count(); + + // 处理数据 + $data = []; + foreach ($list as $item) { + $sendData = json_decode($item->sendData,true); + $data[] = [ + 'id' => $item->id, + 'content' => $sendData['text'] ?? '', + 'momentContentType' => $sendData['momentContentType'] ?? 1, + 'picUrlList' => $sendData['picUrlList'] ?? [], + 'videoUrl' => $sendData['videoUrl'] ?? '', + 'link' => $sendData['link'] ?? [], + 'publicMode' => $sendData['publicMode'] ?? 2, + 'isSend' => $item->isSend, + 'sendTime' => date('Y-m-d H:i:s',$item->sendTime), + 'accountCount' => count($sendData['jobPublishWechatMomentsItems'] ?? []) + ]; + } + + return ResponseHelper::success([ + 'list' => $data, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ], '获取成功'); + + } catch (\Exception $e) { + return ResponseHelper::error('获取失败:' . $e->getMessage()); + } + } + + /** + * 删除朋友圈 + * @return \think\response\Json + */ + public function delete() + { + $id = (int)$this->request->param('id', 0); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if ($id <= 0) { + return ResponseHelper::error('ID不合法'); + } + + try { + $moments = KfMoments::where(['id' => $id, 'companyId' => $companyId, 'userId' => $userId, 'isDel' => 0])->find(); + if (empty($moments)) { + return ResponseHelper::error('朋友圈不存在'); + } + + $moments->isDel = 1; + $moments->delTime = time(); + $moments->updateTime = time(); + $moments->save(); + return ResponseHelper::success([], '删除成功'); + + } catch (\Exception $e) { + return ResponseHelper::error('删除失败:' . $e->getMessage()); + } + } + + /** + * 规范化时间字符串为 Y-m-d H:i:s 格式 + * 支持多种时间格式: + * - "2026年1月5日15:43:00" + * - "2026-01-05 15:43:00" + * - "2026/01/05 15:43:00" + * - 时间戳 + * @param string|int $timingTime 时间字符串或时间戳 + * @return string|false 格式化后的时间字符串,失败返回false + */ + private function normalizeTimingTime($timingTime) + { + if (empty($timingTime)) { + return date('Y-m-d H:i:s'); + } + + // 如果是时间戳 + if (is_numeric($timingTime) && strlen($timingTime) == 10) { + return date('Y-m-d H:i:s', $timingTime); + } + + // 如果是毫秒时间戳 + if (is_numeric($timingTime) && strlen($timingTime) == 13) { + return date('Y-m-d H:i:s', intval($timingTime / 1000)); + } + + // 如果已经是标准格式,直接返回 + if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $timingTime)) { + return $timingTime; + } + + // 处理中文日期格式:2026年1月5日15:43:00 或 2026年01月05日15:43:00 + if (preg_match('/^(\d{4})年(\d{1,2})月(\d{1,2})日(\d{1,2}):(\d{1,2}):(\d{1,2})$/', $timingTime, $matches)) { + $year = $matches[1]; + $month = str_pad($matches[2], 2, '0', STR_PAD_LEFT); + $day = str_pad($matches[3], 2, '0', STR_PAD_LEFT); + $hour = str_pad($matches[4], 2, '0', STR_PAD_LEFT); + $minute = str_pad($matches[5], 2, '0', STR_PAD_LEFT); + $second = str_pad($matches[6], 2, '0', STR_PAD_LEFT); + return "{$year}-{$month}-{$day} {$hour}:{$minute}:{$second}"; + } + + // 处理中文日期格式(无秒):2026年1月5日15:43 + if (preg_match('/^(\d{4})年(\d{1,2})月(\d{1,2})日(\d{1,2}):(\d{1,2})$/', $timingTime, $matches)) { + $year = $matches[1]; + $month = str_pad($matches[2], 2, '0', STR_PAD_LEFT); + $day = str_pad($matches[3], 2, '0', STR_PAD_LEFT); + $hour = str_pad($matches[4], 2, '0', STR_PAD_LEFT); + $minute = str_pad($matches[5], 2, '0', STR_PAD_LEFT); + return "{$year}-{$month}-{$day} {$hour}:{$minute}:00"; + } + + // 尝试使用 strtotime 解析其他格式 + $timestamp = strtotime($timingTime); + if ($timestamp !== false) { + return date('Y-m-d H:i:s', $timestamp); + } + + // 如果所有方法都失败,返回 false + return false; + } +} \ No newline at end of file diff --git a/application/chukebao/controller/NoticeController.php b/application/chukebao/controller/NoticeController.php new file mode 100644 index 0000000..d755eb9 --- /dev/null +++ b/application/chukebao/controller/NoticeController.php @@ -0,0 +1,116 @@ +request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + $accountId = $this->getUserInfo('s2_accountId'); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($accountId)) { + return ResponseHelper::error('请先登录'); + } + + $noRead = NoticeModel::where(['userId' => $userId, 'companyId' => $companyId,'isRead' => 0])->count(); + + $query = NoticeModel::where(['userId' => $userId, 'companyId' => $companyId]) + ->order('id desc'); + if (!empty($keyword)) { + $query->where('title|message', 'like', '%' . $keyword . '%'); + } + $total = $query->count(); + $list = $query->page($page, $limit)->order('isRead ASC,id DESC')->select()->toArray(); + + + foreach ($list as $k => &$v) { + if ($v['type'] == 1) { + $friendId = ToDo::where(['id' => $v['bindId']])->value('friendId'); + } elseif ($v['type'] == 2) { + $friendId = FollowUp::where(['id' => $v['bindId']])->value('friendId'); + } + if (!empty($friendId)) { + $friend = Db::table('s2_wechat_friend')->where(['id' => $friendId])->field('nickname,avatar')->find(); + } else { + $friend = ['nickname' => '', 'avatar' => '']; + } + $v['friendData'] = $friend; + + $v['readTime'] = !empty($v['readTime']) ? date('Y-m-d H:i:s', $v['readTime']) : ''; + } + unset($v); + return ResponseHelper::success(['list' => $list, 'total' => $total,'noRead' => $noRead]); + } + + + public function readMessage() + { + $id = $this->request->param('id', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($id)) { + return ResponseHelper::error('参数缺失'); + } + Db::startTrans(); + try { + $notice = NoticeModel::where(['userId' => $userId, 'companyId' => $companyId, 'id' => $id, 'isRead' => 0])->find(); + if (empty($notice)) { + return ResponseHelper::error('该消息不存在或标记已读'); + } + $notice->isRead = 1; + $notice->readTime = time(); + $notice->save(); + if ($notice->type == 1) { + ToDo::where(['userId' => $userId, 'companyId' => $companyId, 'id' => $notice->bindId])->update(['isProcess' => 1, 'updateTime' => time()]); + } elseif ($notice->type == 2) { + FollowUp::where(['userId' => $userId, 'companyId' => $companyId, 'id' => $notice->bindId])->update(['isProcess' => 1, 'updateTime' => time()]); + } + Db::commit(); + return ResponseHelper::success(' ', '处理成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('处理失败:' . $e->getMessage()); + } + } + + + public function readAll() + { + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + Db::startTrans(); + try { + $noticeData = NoticeModel::where(['userId' => $userId, 'companyId' => $companyId, 'isRead' => 0])->select()->toArray(); + if (empty($noticeData)) { + return ResponseHelper::error('暂未有新消息'); + } + NoticeModel::where(['userId' => $userId, 'companyId' => $companyId, 'isRead' => 0])->update(['isRead' => 1, 'readTime' => time()]); + FollowUp::where(['userId' => $userId, 'companyId' => $companyId, 'isProcess' => 0])->update(['isProcess' => 1, 'updateTime' => time()]); + ToDo::where(['userId' => $userId, 'companyId' => $companyId, 'isProcess' => 0])->update(['isProcess' => 1, 'updateTime' => time()]); + Db::commit(); + return ResponseHelper::success(' ', '全部已读'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('处理失败:' . $e->getMessage()); + } + } + +} \ No newline at end of file diff --git a/application/chukebao/controller/QuestionsController.php b/application/chukebao/controller/QuestionsController.php new file mode 100644 index 0000000..21096bb --- /dev/null +++ b/application/chukebao/controller/QuestionsController.php @@ -0,0 +1,233 @@ +request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + $accountId = $this->getUserInfo('s2_accountId'); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($accountId)){ + return ResponseHelper::error('请先登录'); + } + $query = Questions::where(['userId' => $userId,'companyId' => $companyId,'isDel' => 0]) + ->order('id desc'); + if (!empty($keyword)){ + $query->where('questions|answers', 'like', '%'.$keyword.'%'); + } + $total = $query->count(); + $list = $query->page($page, $limit)->select()->toArray(); + + + foreach ($list as $k => &$v){ + $user = Db::name('users')->where(['id' => $v['userId']])->field('username,account')->find(); + if (!empty($user)){ + $v['userName'] = !empty($user['username']) ? $user['username'] : $user['account']; + }else{ + $v['userName'] = ''; + } + $v['answers'] = json_decode($v['answers'],true); + } + unset($v); + return ResponseHelper::success(['list'=>$list,'total'=>$total]); + } + + + /** + * 新增 + * @return \think\response\Json + * @throws \Exception + */ + public function create(){ + + $type = $this->request->param('type', 0); + $questions = $this->request->param('questions', ''); + $answers = $this->request->param('answers', []); + $status = $this->request->param('status', 0); + $accountId = $this->getUserInfo('s2_accountId'); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($accountId)){ + return ResponseHelper::error('请先登录'); + } + + if (empty($questions) || empty($answers)){ + return ResponseHelper::error('问题和答案不能为空'); + } + + Db::startTrans(); + try { + $questionsModel = new Questions(); + $questionsModel->type = $type; + $questionsModel->questions = $questions; + $questionsModel->answers = !empty($answers) ? json_encode($answers,256) : json_encode([],256); + $questionsModel->status = $status; + $questionsModel->accountId = $accountId; + $questionsModel->userId = $userId; + $questionsModel->companyId = $companyId; + $questionsModel->createTime = time(); + $questionsModel->updateTime = time(); + $questionsModel->save(); + Db::commit(); + return ResponseHelper::success(' ','创建成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('创建失败:'.$e->getMessage()); + } + } + + + /** + * 更新 + * @return \think\response\Json + * @throws \Exception + */ + public function update(){ + + $id = $this->request->param('id', 0); + $accountId = $this->getUserInfo('s2_accountId'); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + $type = $this->request->param('type', 0); + $questions = $this->request->param('questions', ''); + $answers = $this->request->param('answers', []); + $status = $this->request->param('status', 0); + $accountId = $this->getUserInfo('s2_accountId'); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($accountId)){ + return ResponseHelper::error('请先登录'); + } + + if (empty($id)){ + return ResponseHelper::error('参数缺失'); + } + + if (empty($questions) || empty($answers)){ + return ResponseHelper::error('问题和答案不能为空'); + } + Db::startTrans(); + try { + $questionsData = Questions::where(['id' => $id,'userId' => $userId,'companyId' => $companyId,'isDel' => 0])->find(); + $questionsData->type = $type; + $questionsData->questions = $questions; + $questionsData->answers = !empty($answers) ? json_encode($answers,256) : json_encode([],256); + $questionsData->status = $status; + $questionsData->accountId = $accountId; + $questionsData->userId = $userId; + $questionsData->companyId = $companyId; + $questionsData->updateTime = time(); + $questionsData->save(); + Db::commit(); + return ResponseHelper::success(' ','更新成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('更新失败:'.$e->getMessage()); + } + } + + + + + + /** + * 删除 + * @return \think\response\Json + * @throws \Exception + */ + public function delete(){ + + $id = $this->request->param('id', 0); + $accountId = $this->getUserInfo('s2_accountId'); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($accountId)){ + return ResponseHelper::error('请先登录'); + } + + if (empty($id)){ + return ResponseHelper::error('参数缺失'); + } + $questions = Questions::where(['id' => $id,'userId' => $userId,'companyId' => $companyId,'isDel' => 0])->find(); + + if (empty($questions)){ + return ResponseHelper::error('该问题不存在或者已删除'); + } + $res = Questions::where(['id' => $id])->update(['isDel' => 1,'deleteTime' => time()]); + + if (!empty($res)){ + return ResponseHelper::success('','已删除'); + }else{ + return ResponseHelper::error('删除失败'); + } + + } + + + /** + * 详情 + * @return \think\response\Json + * @throws \Exception + */ + public function detail(){ + + $id = $this->request->param('id', 0); + $accountId = $this->getUserInfo('s2_accountId'); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($accountId)){ + return ResponseHelper::error('请先登录'); + } + + if (empty($id)){ + return ResponseHelper::error('参数缺失'); + } + $questions = Questions::where(['id' => $id,'userId' => $userId,'companyId' => $companyId,'isDel' => 0])->find(); + + if (empty($questions)){ + return ResponseHelper::error('该问题不存在或者已删除'); + } + + $questions['answers'] = json_decode($questions['answers'],true); + $user = Db::name('users')->where(['id' => $questions['userId']])->field('username,account')->find(); + if (!empty($user)){ + $questions['userName'] = !empty($user['username']) ? $user['username'] : $user['account']; + }else{ + $questions['userName'] = ''; + } + + unset( + $questions['isDel'], + $questions['deleteTime'], + $questions['createTime'], + $questions['updateTime'] + ); + + return ResponseHelper::success($questions,'获取成功'); + + + } + + + + +} \ No newline at end of file diff --git a/application/chukebao/controller/ReplyController.php b/application/chukebao/controller/ReplyController.php new file mode 100644 index 0000000..fb1dea7 --- /dev/null +++ b/application/chukebao/controller/ReplyController.php @@ -0,0 +1,371 @@ +request->param('replyType', 0); + $keyword = $this->request->param('keyword', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + try { + // 构建分组查询条件 + $groupWhere = [ + ['isDel','=',0] + ]; + switch ($replyType) { + case 0: + //公共快捷语 + $groupWhere[] = ['replyType', '=', 0]; + break; + case 1: + //私有快捷语 + $groupWhere[] = ['companyId', '=', $companyId]; + $groupWhere[] = ['userId', '=', $userId]; + $groupWhere[] = ['replyType', '=', 1]; + break; + case 2: + //公司快捷语 + $groupWhere[] = ['companyId', '=', $companyId]; + $groupWhere[] = ['replyType', '=', 2]; + break; + default: + $groupWhere[] = ['replyType', '=', 0]; + break; + } + + + if (!empty($keyword)) { + $groupWhere[] = ['groupName','like', '%' . $keyword . '%']; + } + + // 获取所有分组 + $allGroups = ReplyGroup::where($groupWhere) + ->order('sortIndex asc,id DESC') + ->select(); + // 构建树形结构 + $result = $this->buildGroupTree($allGroups, $keyword); + + return ResponseHelper::success($result, '获取成功'); + + } catch (\Exception $e) { + return ResponseHelper::error('获取失败:' . $e->getMessage()); + } + } + + /** + * 新增快捷语分组 + * @return \think\response\Json + */ + public function addGroup() + { + $groupName = $this->request->param('groupName', ''); + $parentId = (int)$this->request->param('parentId', 0); + $replyType = (int)$this->request->param('replyType', 0); // 0公共 1私有 2公司 + $sortIndex = (string)$this->request->param('sortIndex', 50); + + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + $accountId = $this->getUserInfo('s2_accountId'); + + if ($groupName === '') { + return ResponseHelper::error('分组名称不能为空'); + } + + try { + $data = [ + 'groupName' => $groupName, + 'parentId' => $parentId, + 'replyType' => $replyType, + 'sortIndex' => $sortIndex, + // 兼容现有程序中使用到的字段 + 'companyId' => $companyId, + 'userId' => $userId, + ]; + + /** @var ReplyGroup $group */ + $group = new ReplyGroup(); + $group->save($data); + + return ResponseHelper::success($group->toArray(), '创建成功'); + } catch (\Exception $e) { + return ResponseHelper::error('创建失败:' . $e->getMessage()); + } + } + + /** + * 新增快捷语 + * @return \think\response\Json + */ + public function addReply() + { + $groupId = (int)$this->request->param('groupId', 0); + $title = $this->request->param('title', ''); + $msgType = (int)$this->request->param('msgType', 1); // 1文本 3图片 43视频 49链接 等 + $content = $this->request->param('content', ''); + $sortIndex = (string)$this->request->param('sortIndex', 50); + + $accountId = $this->getUserInfo('s2_accountId'); + $companyId = $this->getUserInfo('companyId'); + $userId = $this->getUserInfo('id'); + + if ($groupId <= 0) { + return ResponseHelper::error('分组ID不合法'); + } + if ($title === '') { + return ResponseHelper::error('标题不能为空'); + } + + try { + $now = time(); + $data = [ + 'tenantId' => $companyId, + 'groupId' => $groupId, + 'accountId' => $accountId, + 'title' => $title, + 'msgType' => $msgType, + 'content' => $content, + 'sortIndex' => $sortIndex, + 'createTime' => $now, + 'lastUpdateTime' => $now, + 'userId' => $userId, + ]; + + /** @var Reply $reply */ + $reply = new Reply(); + $reply->save($data); + + return ResponseHelper::success($reply->toArray(), '创建成功'); + } catch (\Exception $e) { + return ResponseHelper::error('创建失败:' . $e->getMessage()); + } + } + + /** + * 编辑快捷语分组 + * @return \think\response\Json + */ + public function updateGroup() + { + $id = (int)$this->request->param('id', 0); + if ($id <= 0) { + return ResponseHelper::error('分组ID不合法'); + } + + $data = []; + $groupName = $this->request->param('groupName', null); + $parentId = $this->request->param('parentId', null); + $replyType = $this->request->param('replyType', null); + $sortIndex = $this->request->param('sortIndex', null); + + if ($groupName !== null) $data['groupName'] = $groupName; + if ($parentId !== null) $data['parentId'] = (int)$parentId; + if ($replyType !== null) $data['replyType'] = (int)$replyType; + if ($sortIndex !== null) $data['sortIndex'] = (string)$sortIndex; + + if (empty($data)) { + return ResponseHelper::error('无可更新字段'); + } + + try { + $group = ReplyGroup::where(['id' => $id,'isDel' => 0])->find(); + if (empty($group)) { + return ResponseHelper::error('分组不存在'); + } + $group->save($data); + return ResponseHelper::success($group->toArray(), '更新成功'); + } catch (\Exception $e) { + return ResponseHelper::error('更新失败:' . $e->getMessage()); + } + } + + /** + * 假删除快捷语分组 + * @return \think\response\Json + */ + public function deleteGroup() + { + $id = (int)$this->request->param('id', 0); + if ($id <= 0) { + return ResponseHelper::error('分组ID不合法'); + } + try { + $group = ReplyGroup::where(['id' => $id,'isDel' => 0])->find(); + if (empty($group)) { + return ResponseHelper::error('分组不存在'); + } + $group->save(['isDel' => 1,'delTime' => time()]); + return ResponseHelper::success([], '删除成功'); + } catch (\Exception $e) { + return ResponseHelper::error('删除失败:' . $e->getMessage()); + } + } + + /** + * 编辑快捷语 + * @return \think\response\Json + */ + public function updateReply() + { + $id = (int)$this->request->param('id', 0); + if ($id <= 0) { + return ResponseHelper::error('快捷语ID不合法'); + } + + $data = []; + $groupId = $this->request->param('groupId', null); + $title = $this->request->param('title', null); + $msgType = $this->request->param('msgType', null); + $content = $this->request->param('content', null); + $sortIndex = $this->request->param('sortIndex', null); + + if ($groupId !== null) $data['groupId'] = (int)$groupId; + if ($title !== null) $data['title'] = $title; + if ($msgType !== null) $data['msgType'] = (int)$msgType; + if ($content !== null) $data['content'] = $content; + if ($sortIndex !== null) $data['sortIndex'] = (string)$sortIndex; + if (!empty($data)) { + $data['lastUpdateTime'] = time(); + } + + if (empty($data)) { + return ResponseHelper::error('无可更新字段'); + } + + try { + $reply = Reply::where(['id' => $id,'isDel' => 0])->find(); + if (empty($reply)) { + return ResponseHelper::error('快捷语不存在'); + } + $reply->save($data); + return ResponseHelper::success($reply->toArray(), '更新成功'); + } catch (\Exception $e) { + return ResponseHelper::error('更新失败:' . $e->getMessage()); + } + } + + /** + * 假删除快捷语 + * @return \think\response\Json + */ + public function deleteReply() + { + $id = (int)$this->request->param('id', 0); + if ($id <= 0) { + return ResponseHelper::error('快捷语ID不合法'); + } + try { + $reply = Reply::where(['id' => $id,'isDel' => 0])->find(); + if (empty($reply)) { + return ResponseHelper::error('快捷语不存在'); + } + $reply->save(['isDel' => 1, 'delTime' => time()]); + return ResponseHelper::success([], '删除成功'); + } catch (\Exception $e) { + return ResponseHelper::error('删除失败:' . $e->getMessage()); + } + } + + /** + * 构建分组树形结构 + * @param array $groups 所有分组数据 + * @param string $keyword 搜索关键词 + * @return array + */ + private function buildGroupTree($groups, $keyword = '') + { + $tree = []; + $groupMap = []; + + // 先构建分组映射 + foreach ($groups as $group) { + $groupMap[$group->id] = $group->toArray(); + } + + // 构建树形结构 + foreach ($groups as $group) { + $groupData = $this->buildGroupData($group, $keyword); + + if ($group->parentId == null || $group->parentId == 0) { + // 顶级分组 + $tree[] = $groupData; + } else { + // 子分组,需要找到父分组并添加到其children中 + $this->addToParentGroup($tree, $group->parentId, $groupData); + } + } + + return $tree; + } + + /** + * 构建单个分组数据 + * @param object $group 分组对象 + * @param string $keyword 搜索关键词 + * @return array + */ + private function buildGroupData($group, $keyword = '') + { + // 构建快捷回复查询条件 + $replyWhere[] =[ + ['groupId' ,'=', $group->id], + ['isDel','=',0] + ]; + if (!empty($keyword)) { + $replyWhere[] = ['title','like', '%' . $keyword . '%']; + } + + // 获取该分组下的快捷回复 + $replies = Reply::where($replyWhere) + ->order('sortIndex asc, id desc + ') + ->select(); + + return [ + 'id' => $group->id, + 'groupName' => $group->groupName, + 'sortIndex' => $group->sortIndex, + 'parentId' => $group->parentId, + 'replyType' => $group->replyType, + 'replys' => $group->replys, + 'companyId' => $group->companyId, + 'userId' => $group->userId, + 'replies' => $replies->toArray(), + 'children' => [] // 子分组 + ]; + } + + /** + * 将子分组添加到父分组中 + * @param array $tree 树形结构 + * @param int $parentId 父分组ID + * @param array $childGroup 子分组数据 + */ + private function addToParentGroup(&$tree, $parentId, $childGroup) + { + foreach ($tree as &$group) { + if ($group['id'] == $parentId) { + $group['children'][] = $childGroup; + return; + } + + // 递归查找子分组 + if (!empty($group['children'])) { + $this->addToParentGroup($group['children'], $parentId, $childGroup); + } + } + } + +} \ No newline at end of file diff --git a/application/chukebao/controller/ToDoController.php b/application/chukebao/controller/ToDoController.php new file mode 100644 index 0000000..6914141 --- /dev/null +++ b/application/chukebao/controller/ToDoController.php @@ -0,0 +1,143 @@ +request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + $isRemind = $this->request->param('isRemind', ''); + $isProcess = $this->request->param('isProcess', ''); + $level = $this->request->param('level', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + + $where = [ + ['companyId','=',$companyId], + ['userId' ,'=', $userId] + ]; + + if ($isRemind != '') { + $where[] = ['isRemind','=',$isRemind]; + } + if ($level != '') { + $where[] = ['level','=',$level]; + } + if ($isProcess != '') { + $where[] = ['isProcess','=',$isProcess]; + } + + if(!empty($keyword)){ + $where[] = ['title|description','like','%'.$keyword.'%']; + } + + $query = ToDo::where($where); + $total = $query->count(); + $list = $query->where($where)->page($page,$limit)->order('id desc')->select(); + + + foreach ($list as &$item) { + $nickname = Db::table('s2_wechat_friend')->where(['id' => $item['friendId']])->value('nickname'); + $item['nickname'] = !empty($nickname) ? $nickname : '-'; + $item['reminderTime'] = date('Y-m-d H:i:s',$item['reminderTime']); + } + unset($item); + + return ResponseHelper::success(['list'=>$list,'total'=>$total]); + } + + + /** + * 添加 + * @return \think\response\Json + * @throws \Exception + */ + public function create(){ + $level = $this->request->param('level', 0); + $title = $this->request->param('title', ''); + $reminderTime = $this->request->param('reminderTime', ''); + $description = $this->request->param('description', ''); + $friendId = $this->request->param('friendId', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($title) || empty($reminderTime) || empty($description) || empty($friendId)){ + return ResponseHelper::error('参数缺失'); + } + $friend = Db::table('s2_wechat_friend')->where(['id' => $friendId])->find(); + if (empty($friend)) { + return ResponseHelper::error('好友不存在'); + } + + + Db::startTrans(); + try { + $todo = new ToDo(); + $todo->level = $level; + $todo->title = $title; + $todo->friendId = $friendId; + $todo->reminderTime = !empty($reminderTime) ? strtotime($reminderTime) : time(); + $todo->description = $description; + $todo->userId = $userId; + $todo->companyId = $companyId; + $todo->updateTime = time(); + $todo->createTime = time(); + $todo->save(); + Db::commit(); + return ResponseHelper::success(' ','创建成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('创建失败:'.$e->getMessage()); + } + } + + + /** + * 处理代办事项 + * @return \think\response\Json + * @throws \Exception + */ + public function process(){ + $ids = $this->request->param('ids',''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($ids)){ + return ResponseHelper::error('参数缺失'); + } + $ids = explode(',',$ids); + + if (!is_array($ids)){ + return ResponseHelper::error('格式错误'); + } + + $todoIds = ToDo::where(['userId' => $userId,'companyId' => $companyId,'isProcess' => 0])->whereIn('id',$ids)->column('id'); + if (empty($todoIds)){ + return ResponseHelper::error('代办事项不存在'); + } + + Db::startTrans(); + try { + ToDo::whereIn('id',$todoIds)->update(['isProcess' => 1,'isRemind' => 1,'updateTime' => time()]); + Db::commit(); + return ResponseHelper::success(' ','已处理'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('处理失败:'.$e->getMessage()); + } + + + + + + } + +} \ No newline at end of file diff --git a/application/chukebao/controller/TokensRecordController.php b/application/chukebao/controller/TokensRecordController.php new file mode 100644 index 0000000..03b7a2e --- /dev/null +++ b/application/chukebao/controller/TokensRecordController.php @@ -0,0 +1,192 @@ +request->param('page', 1); + $limit = $this->request->param('limit', 10); + $type = $this->request->param('type', ''); + $form = $this->request->param('form', ''); + $startTime = $this->request->param('startTime', ''); + $endTime = $this->request->param('endTime', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + + $where = [ + ['companyId','=',$companyId], + ['userId' ,'=', $userId] + ]; + + if ($type != '') { + $where[] = ['type','=',$type]; + } + if ($form != '') { + $where[] = ['form','=',$form]; + } + + // 时间筛选 + if (!empty($startTime)) { + // 支持时间戳或日期字符串格式 + $startTimestamp = is_numeric($startTime) ? intval($startTime) : strtotime($startTime); + if ($startTimestamp !== false) { + $where[] = ['createTime', '>=', $startTimestamp]; + } + } + + if (!empty($endTime)) { + // 支持时间戳或日期字符串格式 + $endTimestamp = is_numeric($endTime) ? intval($endTime) : strtotime($endTime); + if ($endTimestamp !== false) { + // 如果是日期字符串,自动设置为当天的23:59:59 + if (!is_numeric($endTime)) { + $endTimestamp = strtotime(date('Y-m-d 23:59:59', $endTimestamp)); + } + $where[] = ['createTime', '<=', $endTimestamp]; + } + } + + $query = TokensRecord::where($where); + $total = $query->count(); + $list = $query->where($where)->page($page,$limit)->order('id desc')->select(); + + + foreach ($list as &$item) { + if (in_array($item['type'],[1])){ + $nickname = Db::table('s2_wechat_friend')->where(['id' => $item['friendIdOrGroupId']])->value('nickname'); + $item['nickname'] = !empty($nickname) ? $nickname : '-'; + } + if (in_array($item['type'],[2,3])){ + $nickname = Db::table('s2_wechat_chatroom')->where(['id' => $item['friendIdOrGroupId']])->value('nickname'); + $item['nickname'] = !empty($nickname) ? $nickname : '-'; + } + } + unset($item); + + return ResponseHelper::success(['list'=>$list,'total'=>$total]); + } + + + public function consumeTokens($data = []) + { + if (empty($data)){ + return ResponseHelper::error('数据缺失'); + } + + $tokens = isset($data['tokens']) ? intval($data['tokens']) : 0; + $type = isset($data['type']) ? intval($data['type']) : 0; + $form = isset($data['form']) ? intval($data['form']) : 0; + $wechatAccountId = isset($data['wechatAccountId']) ? intval($data['wechatAccountId']) : 0; + $friendIdOrGroupId = isset($data['friendIdOrGroupId']) ? intval($data['friendIdOrGroupId']) : 0; + $remarks = isset($data['remarks']) ? $data['remarks'] : ''; + $companyId = isset($data['companyId']) ? intval($data['companyId']) : $this->getUserInfo('companyId'); + $userId = isset($data['userId']) ? intval($data['userId']) : $this->getUserInfo('id'); + + // 验证必要参数 + if ($tokens <= 0) { + return ResponseHelper::error('tokens数量必须大于0'); + } + + if (!in_array($type, [0, 1])) { + return ResponseHelper::error('类型参数错误,0为减少,1为增加'); + } + + + // 重试机制,最多重试3次 + $maxRetries = 3; + $retryCount = 0; + while ($retryCount < $maxRetries) { + try { + return $this->doConsumeTokens($userId, $companyId, $tokens, $type, $form, $wechatAccountId, $friendIdOrGroupId, $remarks); + } catch (\Exception $e) { + $retryCount++; + if ($retryCount >= $maxRetries) { + return ResponseHelper::error('操作失败,请稍后重试:' . $e->getMessage()); + } + // 短暂延迟后重试 + usleep(100000); // 100ms + } + } + } + + /** + * 执行tokens消费的核心方法 + */ + private function doConsumeTokens($userId, $companyId, $tokens, $type, $form, $wechatAccountId, $friendIdOrGroupId, $remarks) + { + // 开启数据库事务 + Db::startTrans(); + try { + // 使用悲观锁获取用户当前tokens余额,确保并发安全 + $userInfo = TokensCompany::where(['companyId'=> $companyId,'userId' => $userId])->lock(true)->find(); + if (!$userInfo) { + throw new \Exception('用户不存在'); + } + + $currentTokens = intval($userInfo['tokens']); + + // 计算新的余额 + $newBalance = $type == 1 ? ($currentTokens + $tokens) : ($currentTokens - $tokens); + + // 使用原子更新操作,基于当前值进行更新,防止并发覆盖 + $updateResult = TokensCompany::where('companyId', $companyId) + ->where('companyId', $companyId) + ->update([ + 'tokens' => $newBalance, + 'updateTime' => time() + ]); + + if (!$updateResult) { + // 如果更新失败,说明tokens值已被其他事务修改,需要重新获取 + throw new \Exception('tokens余额已被其他操作修改,请重试'); + } + + // 记录tokens变动 + $recordData = [ + 'companyId' => $companyId, + 'userId' => $userId, + 'wechatAccountId' => $wechatAccountId, + 'friendIdOrGroupId' => $friendIdOrGroupId, + 'form' => $form, + 'type' => $type, + 'tokens' => $tokens, + 'balanceTokens' => $newBalance, + 'remarks' => $remarks, + 'createTime' => time() + ]; + + $recordId = Db::name('tokens_record')->insertGetId($recordData); + + if (!$recordId) { + throw new \Exception('记录tokens变动失败'); + } + + // 提交事务 + Db::commit(); + + return ResponseHelper::success([ + 'recordId' => $recordId, + 'oldBalance' => $currentTokens, + 'newBalance' => $newBalance, + 'changeAmount' => $type == 1 ? $tokens : -$tokens + ], 'tokens变动记录成功'); + + } catch (\Exception $e) { + // 回滚事务 + Db::rollback(); + throw $e; // 重新抛出异常,让重试机制处理 + } + } + + + +} \ No newline at end of file diff --git a/application/chukebao/controller/WechatChatroomController.php b/application/chukebao/controller/WechatChatroomController.php new file mode 100644 index 0000000..936c837 --- /dev/null +++ b/application/chukebao/controller/WechatChatroomController.php @@ -0,0 +1,269 @@ +request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + $groupIds = $this->request->param('groupId', ''); + $ownerWechatId = $this->request->param('ownerWechatId', ''); + $accountId = $this->getUserInfo('s2_accountId'); + if (empty($accountId)){ + return ResponseHelper::error('请先登录'); + } + $query = Db::table('s2_wechat_chatroom') + ->where(['accountId' => $accountId,'isDeleted' => 0]); + + // 关键字搜索:群昵称、微信号(这里使用chatroomId作为群标识) + if ($keyword !== '' && $keyword !== null) { + $query->where(function ($q) use ($keyword) { + $like = '%' . $keyword . '%'; + $q->whereLike('nickname', $like) + ->whereOr('conRemark', 'like', $like); + }); + } + + // 分组筛选:groupIds(单个分组ID) + if ($groupIds !== '' && $groupIds !== null) { + $query->where('groupIds', $groupIds); + } + + if (!empty($ownerWechatId)) { + $query->where('ownerWechatId', $ownerWechatId); + } + + $query->order('id desc'); + $total = $query->count(); + $list = $query->page($page, $limit)->select(); + + + + // 提取所有聊天室ID,用于批量查询 + $chatroomIds = array_column($list, 'id'); + + + // 一次性查询所有聊天室的未读消息数量 + $unreadCounts = []; + if (!empty($chatroomIds)) { + $unreadResults = Db::table('s2_wechat_message') + ->field('wechatChatroomId, COUNT(*) as count') + ->where('wechatChatroomId', 'in', $chatroomIds) + ->where('isRead', 0) + ->group('wechatChatroomId') + ->select(); + + foreach ($unreadResults as $result) { + $unreadCounts[$result['wechatChatroomId']] = $result['count']; + } + } + // 一次性查询所有聊天室的最新消息 + $latestMessages = []; + if (!empty($chatroomIds)) { + // 使用子查询获取每个聊天室的最新消息ID + $subQuery = Db::table('s2_wechat_message') + ->field('MAX(id) as max_id, wechatChatroomId') + ->where('wechatChatroomId', 'in', $chatroomIds) + ->group('wechatChatroomId') + ->buildSql(); + + // 查询最新消息的详细信息 + $messageResults = Db::table('s2_wechat_message') + ->alias('m') + ->join([$subQuery => 'sub'], 'm.id = sub.max_id') + ->field('m.*, sub.wechatChatroomId') + ->select(); + + foreach ($messageResults as $message) { + $latestMessages[$message['wechatChatroomId']] = $message; + } + } + + // 处理每个聊天室的数据 + foreach ($list as $k => &$v) { + $v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s', $v['createTime']) : ''; + $v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s', $v['updateTime']) : ''; + + $config = [ + 'unreadCount' => isset($unreadCounts[$v['id']]) ? $unreadCounts[$v['id']] : 0, + 'chat' => isset($latestMessages[$v['id']]), + 'msgTime' => isset($latestMessages[$v['id']]) ? $latestMessages[$v['id']]['wechatTime'] : 0 + ]; + $v['config'] = $config; + } + unset($v); + + return ResponseHelper::success(['list'=>$list,'total'=>$total]); + } + + public function getDetail(){ + $id = input('id', 0); + + if (!$id) { + return ResponseHelper::error('聊天室ID不能为空'); + } + + $accountId = $this->getUserInfo('s2_accountId'); + if (empty($accountId)){ + return ResponseHelper::error('请先登录'); + } + + $detail = Db::table('s2_wechat_chatroom') + ->where(['accountId' => $accountId, 'id' => $id, 'isDeleted' => 0]) + ->find(); + + if (!$detail) { + return ResponseHelper::error('聊天室不存在或无权限访问'); + } + + // 处理时间格式 + $detail['createTime'] = !empty($detail['createTime']) ? date('Y-m-d H:i:s', $detail['createTime']) : ''; + $detail['updateTime'] = !empty($detail['updateTime']) ? date('Y-m-d H:i:s', $detail['updateTime']) : ''; + + // 查询未读消息数量 + $unreadCount = Db::table('s2_wechat_message') + ->where('wechatChatroomId', $id) + ->where('isRead', 0) + ->count(); + + // 查询最新消息 + $latestMessage = Db::table('s2_wechat_message') + ->where('wechatChatroomId', $id) + ->order('id desc') + ->find(); + + $config = [ + 'unreadCount' => $unreadCount, + 'chat' => !empty($latestMessage), + 'msgTime' => isset($latestMessage['wechatTime']) ? $latestMessage['wechatTime'] : 0 + ]; + $detail['config'] = $config; + + return ResponseHelper::success($detail); + } + + public function getMembers() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $groupId = $this->request->param('groupId', ''); + $keyword = $this->request->param('keyword', ''); + + $accountId = $this->getUserInfo('s2_accountId'); + if (empty($accountId)) { + return ResponseHelper::error('请先登录'); + } + + // 验证群组ID必填 + if (empty($groupId)) { + return ResponseHelper::error('群组ID不能为空'); + } + + // 验证群组是否属于当前账号 + $chatroom = Db::table('s2_wechat_chatroom') + ->where(['id' => $groupId, 'isDeleted' => 0]) + ->find(); + + if (!$chatroom) { + return ResponseHelper::error('群组不存在或无权限访问'); + } + + // 获取群组的chatroomId(微信群聊ID) + $chatroomId = $chatroom['chatroomId'] ?? $chatroom['id']; + + // 如果chatroomId为空,使用id作为chatroomId + if (empty($chatroomId)) { + $chatroomId = $chatroom['id']; + } + + // 构建查询 + $query = Db::table('s2_wechat_chatroom_member') + ->where('chatroomId', $chatroomId); + + // 关键字搜索:昵称、备注、别名 + if ($keyword !== '' && $keyword !== null) { + $query->where(function ($q) use ($keyword) { + $like = '%' . $keyword . '%'; + $q->whereLike('nickname', $like) + ->whereOr('conRemark', 'like', $like) + ->whereOr('alias', 'like', $like); + }); + } + + $query->order('id desc'); + $total = $query->count(); + $list = $query->page($page, $limit)->select(); + + // 处理时间格式 + foreach ($list as $k => &$v) { + $v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s', $v['createTime']) : ''; + $v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s', $v['updateTime']) : ''; + } + unset($v); + + return ResponseHelper::success(['list' => $list, 'total' => $total]); + } + + public function aiAnnouncement() + { + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + $wechatAccountId = $this->request->param('wechatAccountId', ''); + $groupId = $this->request->param('groupId', ''); + $content = $this->request->param('content', ''); + + if (empty($groupId) || empty($content)|| empty($wechatAccountId)){ + return ResponseHelper::error('参数缺失'); + } + + $tokens = TokensCompany::where(['companyId' => $companyId])->value('tokens'); + if (empty($tokens) || $tokens <= 0){ + return ResponseHelper::error('用户Tokens余额不足'); + } + + $params = [ + 'model' => 'doubao-1-5-pro-32k-250115', + 'messages' => [ + ['role' => 'system', 'content' => '你现在是存客宝的AI助理,你精通中国大陆的法律'], + ['role' => 'user', 'content' => $content], + ], + ]; + + //AI处理 + $ai = new DouBaoAI(); + $res = $ai->text($params); + $res = json_decode($res,true); + + + if ($res['code'] == 200) { + //扣除Tokens + $tokensRecord = new tokensRecord(); + $nickname = Db::table('s2_wechat_chatroom')->where(['id' => $groupId])->value('nickname'); + $remarks = !empty($nickname) ? '生成【'.$nickname.'】群公告' : '生成群公告'; + $data = [ + 'tokens' => $res['data']['token'], + 'type' => 0, + 'form' => 14, + 'wechatAccountId' => $wechatAccountId, + 'friendIdOrGroupId' => $groupId, + 'remarks' => $remarks, + ]; + $tokensRecord->consumeTokens($data); + return ResponseHelper::success($res['data']['content']); + }else{ + return ResponseHelper::error($res['msg']); + } + + + } + +} \ No newline at end of file diff --git a/application/chukebao/controller/WechatFriendController.php b/application/chukebao/controller/WechatFriendController.php new file mode 100644 index 0000000..aaf541f --- /dev/null +++ b/application/chukebao/controller/WechatFriendController.php @@ -0,0 +1,302 @@ +request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + $groupIds = $this->request->param('groupId', ''); + $ownerWechatId = $this->request->param('ownerWechatId', ''); + $accountId = $this->getUserInfo('s2_accountId'); + if (empty($accountId)) { + return ResponseHelper::error('请先登录'); + } + $query = Db::table('s2_wechat_friend') + ->where(['accountId' => $accountId, 'isDeleted' => 0]); + + // 关键字搜索:昵称、备注、微信号 + if ($keyword !== '' && $keyword !== null) { + $query->where(function ($q) use ($keyword) { + $like = '%' . $keyword . '%'; + $q->whereLike('nickname', $like) + ->whereOr('conRemark', 'like', $like) + ->whereOr('alias', 'like', $like) + ->whereOr('wechatId', 'like', $like); + }); + } + + // 分组筛选:groupIds(单个分组ID) + if ($groupIds !== '' && $groupIds !== null) { + $query->where('groupIds', $groupIds); + } + + if (!empty($ownerWechatId)) { + $query->where('ownerWechatId', $ownerWechatId); + } + + $query->order('id desc'); + $total = $query->count(); + $list = $query->page($page, $limit)->select(); + + // 提取所有好友ID + $friendIds = array_column($list, 'id'); + + $aiTypeData = []; + if (!empty($friendIds)) { + $aiTypeData = FriendSettings::where('friendId', 'in', $friendIds)->column('friendId,type'); + } + + + // 处理每个好友的数据 + foreach ($list as $k => &$v) { + $v['labels'] = json_decode($v['labels'], true); + $v['siteLabels'] = json_decode($v['siteLabels'], true); + $v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s', $v['createTime']) : ''; + $v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s', $v['updateTime']) : ''; + $v['passTime'] = !empty($v['passTime']) ? date('Y-m-d H:i:s', $v['passTime']) : ''; + $v['aiType'] = isset($aiTypeData[$v['id']]) ? $aiTypeData[$v['id']] : 0; + } + unset($v); + + return ResponseHelper::success(['list' => $list, 'total' => $total]); + } + + /** + * 获取单个好友详情 + * @return \think\response\Json + */ + public function getDetail() + { + $friendId = $this->request->param('id'); + $accountId = $this->getUserInfo('s2_accountId'); + + if (empty($accountId)) { + return ResponseHelper::error('请先登录'); + } + + if (empty($friendId)) { + return ResponseHelper::error('好友ID不能为空'); + } + + // 查询好友详情 + $friend = Db::table('s2_wechat_friend') + ->where(['id' => $friendId, 'isDeleted' => 0]) + ->find(); + + if (empty($friend)) { + return ResponseHelper::error('好友不存在'); + } + + // 处理好友数据 + $friend['labels'] = json_decode($friend['labels'], true); + $friend['siteLabels'] = json_decode($friend['siteLabels'], true); + $friend['createTime'] = !empty($friend['createTime']) ? date('Y-m-d H:i:s', $friend['createTime']) : ''; + $friend['updateTime'] = !empty($friend['updateTime']) ? date('Y-m-d H:i:s', $friend['updateTime']) : ''; + $friend['passTime'] = !empty($friend['passTime']) ? date('Y-m-d H:i:s', $friend['passTime']) : ''; + + // 获取AI类型设置 + $aiTypeSetting = FriendSettings::where('friendId', $friendId)->find(); + $friend['aiType'] = $aiTypeSetting ? $aiTypeSetting['type'] : 0; + + return ResponseHelper::success(['detail' => $friend]); + } + + /** + * 更新好友资料(公司、姓名、手机号等字段可单独更新) + * @return \think\response\Json + */ + public function updateFriendInfo() + { + $friendId = $this->request->param('id'); + $accountId = $this->getUserInfo('s2_accountId'); + + if (empty($accountId)) { + return ResponseHelper::error('请先登录'); + } + + if (empty($friendId)) { + return ResponseHelper::error('好友ID不能为空'); + } + + $friend = Db::table('s2_wechat_friend') + ->where(['id' => $friendId, 'accountId' => $accountId, 'isDeleted' => 0]) + ->find(); + + if (empty($friend)) { + return ResponseHelper::error('好友不存在或无权限操作'); + } + + $requestData = $this->request->param(); + $updatableColumns = [ + 'phone', + 'conRemark', + ]; + $columnUpdates = []; + + foreach ($updatableColumns as $field) { + if (array_key_exists($field, $requestData)) { + $columnUpdates[$field] = $requestData[$field]; + } + } + + $extendFieldsData = []; + if (!empty($friend['extendFields'])) { + $decodedExtend = json_decode($friend['extendFields'], true); + $extendFieldsData = is_array($decodedExtend) ? $decodedExtend : []; + } + + $extendFieldKeys = [ + 'company', + 'name', + 'position', + 'email', + 'address', + 'wechat', + 'qq', + 'remark' + ]; + $extendFieldsUpdated = false; + + foreach ($extendFieldKeys as $key) { + if (array_key_exists($key, $requestData)) { + $extendFieldsData[$key] = $requestData[$key]; + $extendFieldsUpdated = true; + } + } + + if ($extendFieldsUpdated) { + $columnUpdates['extendFields'] = json_encode($extendFieldsData, JSON_UNESCAPED_UNICODE); + } + + if (empty($columnUpdates)) { + return ResponseHelper::error('没有可更新的字段'); + } + + $columnUpdates['updateTime'] = time(); + + try { + Db::table('s2_wechat_friend')->where('id', $friendId)->update($columnUpdates); + } catch (\Exception $e) { + return ResponseHelper::error('更新失败:' . $e->getMessage()); + } + + return ResponseHelper::success(['id' => $friendId]); + } + + /** + * 获取添加好友任务记录列表(全新功能) + * 返回当前账号的所有添加好友任务记录,无论是否通过都展示 + * 包含:添加者头像、昵称、微信号、添加状态、添加时间、通过时间等信息 + * @return \think\response\Json + */ + public function getAddTaskList() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $status = $this->request->param('status', ''); // 可选:筛选状态 0执行中,1执行成功,2执行失败 + $accountId = $this->getUserInfo('s2_accountId'); + + if (empty($accountId)) { + return ResponseHelper::error('请先登录'); + } + + // 直接使用operatorAccountId查询添加好友任务记录 + $query = Db::table('s2_friend_task') + ->where('operatorAccountId', $accountId) + ->order('createTime desc'); + + // 如果指定了状态筛选 + if ($status !== '' && $status !== null) { + $query->where('status', $status); + } + + $total = $query->count(); + $tasks = $query->page($page, $limit)->select(); + + + // 处理任务数据 + $list = []; + foreach ($tasks as $task) { + // 提取所有任务的phone、wechatId,用于查询好友信息(获取通过时间) + $friendInfo = Db::table('s2_wechat_friend') + ->where(['isDeleted' => 0, 'ownerWechatId' => $task['wechatId']]) + ->where(function ($query) use ($task) { + $query->whereLike('phone', '%'.$task['phone'].'%')->whereOr('alias', $task['phone'])->whereOr('wechatId', $task['phone']); + })->field('phone,wechatId,alias,passTime,nickname')->find(); + + + + $item = [ + 'taskId' => $task['id'] ?? 0, + 'phone' => $task['phone'] ?? '', + 'wechatId' => $task['wechatId'] ?? '', + 'alias' => $task['alias'] ?? '', + // 添加者信息 + 'adder' => [ + 'avatar' => $task['wechatAvatar'] ?? '', // 添加者头像 + 'nickname' => $task['wechatNickname'] ?? '', // 添加者昵称 + 'username' => $task['accountUsername'] ?? '', // 添加者微信号 + 'accountNickname' => $task['accountNickname'] ?? '', // 账号昵称 + 'accountRealName' => $task['accountRealName'] ?? '', // 账号真实姓名 + ], + // 添加状态 + 'status' => [ + 'code' => $task['status'] ?? 0, // 状态码:0执行中,1执行成功,2执行失败 + 'text' => $this->getTaskStatusText($task['status'] ?? 0), // 状态文本 + 'extra' => '' + ], + // 时间信息 + 'time' => [ + 'addTime' => !empty($task['createTime']) ? date('Y-m-d H:i:s', $task['createTime']) : '', // 添加时间 + 'addTimeStamp' => $task['createTime'] ?? 0, // 添加时间戳 + 'updateTime' => !empty($task['updateTime']) ? date('Y-m-d H:i:s', $task['updateTime']) : '', // 更新时间 + 'updateTimeStamp' => $task['updateTime'] ?? 0, // 更新时间戳 + 'passTime' => !empty($friendInfo['passTime']) ? date('Y-m-d H:i:s', $friendInfo['passTime']) : '', // 通过时间 + 'passTimeStamp' => $friendInfo['passTime'] ?? 0, // 通过时间戳 + ], + // 好友信息(如果已通过) + 'friend' => [ + 'nickname' => $friendInfo['nickname'] ?? '', // 好友昵称 + 'isPassed' => !empty($friendInfo['passTime']), // 是否已通过 + ], + // 其他信息 + 'other' => [ + 'msgContent' => $task['msgContent'] ?? '', // 验证消息 + 'remark' => $task['remark'] ?? '', // 备注 + 'from' => $task['from'] ?? '', // 来源 + 'labels' => !empty($task['labels']) ? explode(',', $task['labels']) : [], // 标签 + ] + ]; + + $list[] = $item; + } + + return ResponseHelper::success(['list' => $list, 'total' => $total]); + } + + + /** + * 获取任务状态文本 + * @param int $status 状态码 + * @return string 状态文本 + */ + private function getTaskStatusText($status) + { + $statusMap = [ + 0 => '执行中', + 1 => '执行成功', + 2 => '执行失败', + ]; + + return isset($statusMap[$status]) ? $statusMap[$status] : '未知状态'; + } +} \ No newline at end of file diff --git a/application/chukebao/controller/WechatGroupController.php b/application/chukebao/controller/WechatGroupController.php new file mode 100644 index 0000000..8ff0b75 --- /dev/null +++ b/application/chukebao/controller/WechatGroupController.php @@ -0,0 +1,273 @@ +getUserInfo('companyId'); + + $query = ChatGroups::where([ + 'companyId' => $companyId, + 'isDel' => 0, + ]) + ->order('groupType desc,sort desc,id desc'); + + $total = $query->count(); + $list = $query->select(); + + // 处理每个分组的数据 + $list = is_array($list) ? $list : $list->toArray(); + foreach ($list as $k => &$v) { + $v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s', $v['createTime']) : ''; + } + unset($v); + + return ResponseHelper::success(['list'=>$list,'total'=>$total]); + } + + /** + * 新增分组 + * @return \think\response\Json + * @throws \Exception + */ + public function create() + { + $groupName = $this->request->param('groupName', ''); + $groupMemo = $this->request->param('groupMemo', ''); + $groupType = $this->request->param('groupType', 1); + $sort = $this->request->param('sort', 0); + $companyId = $this->getUserInfo('companyId'); + + // 只校验公司维度 + if (empty($companyId)) { + return ResponseHelper::error('请先登录'); + } + + if (empty($groupName)) { + return ResponseHelper::error('分组名称不能为空'); + } + + // 验证分组类型 + if (!in_array($groupType, [1, 2])) { + return ResponseHelper::error('无效的分组类型'); + } + + Db::startTrans(); + try { + $chatGroup = new ChatGroups(); + $chatGroup->groupName = $groupName; + $chatGroup->groupMemo = $groupMemo; + $chatGroup->groupType = $groupType; + $chatGroup->sort = $sort; + $chatGroup->userId = $this->getUserInfo('id'); + $chatGroup->companyId = $companyId; + $chatGroup->createTime = time(); + $chatGroup->isDel = 0; + $chatGroup->save(); + + Db::commit(); + return ResponseHelper::success(['id' => $chatGroup->id], '创建成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('创建失败:' . $e->getMessage()); + } + } + + /** + * 更新分组 + * @return \think\response\Json + * @throws \Exception + */ + public function update() + { + $id = $this->request->param('id', 0); + $groupName = $this->request->param('groupName', ''); + $groupMemo = $this->request->param('groupMemo', ''); + $groupType = $this->request->param('groupType', 1); + $sort = $this->request->param('sort', 0); + $companyId = $this->getUserInfo('companyId'); + + if (empty($companyId)) { + return ResponseHelper::error('请先登录'); + } + + if (empty($id)) { + return ResponseHelper::error('参数缺失'); + } + + if (empty($groupName)) { + return ResponseHelper::error('分组名称不能为空'); + } + + // 验证分组类型 + if (!in_array($groupType, [1, 2])) { + return ResponseHelper::error('无效的分组类型'); + } + + // 检查分组是否存在 + $chatGroup = ChatGroups::where([ + 'id' => $id, + 'companyId' => $companyId, + 'isDel' => 0, + ])->find(); + + if (empty($chatGroup)) { + return ResponseHelper::error('该分组不存在或已删除'); + } + + Db::startTrans(); + try { + $chatGroup->groupName = $groupName; + $chatGroup->groupMemo = $groupMemo; + $chatGroup->groupType = $groupType; + $chatGroup->sort = $sort; + $chatGroup->save(); + + Db::commit(); + return ResponseHelper::success('', '更新成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('更新失败:' . $e->getMessage()); + } + } + + /** + * 删除分组(假删除) + * @return \think\response\Json + * @throws \Exception + */ + public function delete() + { + $id = $this->request->param('id', 0); + $companyId = $this->getUserInfo('companyId'); + + if (empty($companyId)) { + return ResponseHelper::error('请先登录'); + } + + if (empty($id)) { + return ResponseHelper::error('参数缺失'); + } + + // 检查分组是否存在 + $chatGroup = ChatGroups::where([ + 'id' => $id, + 'companyId' => $companyId, + 'isDel' => 0, + ])->find(); + + if (empty($chatGroup)) { + return ResponseHelper::error('该分组不存在或已删除'); + } + + Db::startTrans(); + try { + // 1. 假删除当前分组 + $chatGroup->isDel = 1; + $chatGroup->deleteTime = time(); + $chatGroup->save(); + + // 2. 重置该分组下所有好友的分组ID(s2_wechat_friend.groupIds -> 0) + Db::table('s2_wechat_friend') + ->where('groupIds', $id) + ->update(['groupIds' => 0]); + + // 3. 重置该分组下所有微信群的分组ID(s2_wechat_chatroom.groupIds -> 0) + Db::table('s2_wechat_chatroom') + ->where('groupIds', $id) + ->update(['groupIds' => 0]); + + Db::commit(); + return ResponseHelper::success('', '删除成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('删除失败:' . $e->getMessage()); + } + } + + /** + * 移动分组(将好友或群移动到指定分组) + * @return \think\response\Json + * @throws \Exception + */ + public function move() + { + // type: friend 好友, chatroom 群 + $type = $this->request->param('type', 'friend'); + $targetId = (int)$this->request->param('groupId', 0); + // 仅支持单个ID移动 + $idParam = $this->request->param('id', 0); + $companyId = $this->getUserInfo('companyId'); + + if (empty($companyId)) { + return ResponseHelper::error('请先登录'); + } + + if (empty($targetId)) { + return ResponseHelper::error('目标分组ID不能为空'); + } + + // 仅允许单个 ID,禁止批量 + $moveId = (int)$idParam; + if (empty($moveId)) { + return ResponseHelper::error('需要移动的ID不能为空'); + } + + // 校验目标分组是否存在且属于当前公司 + $targetGroup = ChatGroups::where([ + 'id' => $targetId, + 'companyId' => $companyId, + 'isDel' => 0, + ])->find(); + + if (empty($targetGroup)) { + return ResponseHelper::error('目标分组不存在或已删除'); + } + + // 校验分组类型与移动对象类型是否匹配 + // groupType: 1=好友分组, 2=群分组 + if ($type === 'friend' && (int)$targetGroup->groupType !== 1) { + return ResponseHelper::error('目标分组类型错误(需要好友分组)'); + } + if ($type === 'chatroom' && (int)$targetGroup->groupType !== 2) { + return ResponseHelper::error('目标分组类型错误(需要群分组)'); + } + + Db::startTrans(); + try { + if ($type === 'friend') { + // 移动单个好友到指定分组:更新 s2_wechat_friend.groupIds + Db::table('s2_wechat_friend') + ->where('id', $moveId) + ->update(['groupIds' => $targetId]); + } elseif ($type === 'chatroom') { + // 移动单个群到指定分组:更新 s2_wechat_chatroom.groupIds + Db::table('s2_wechat_chatroom') + ->where('id', $moveId) + ->update(['groupIds' => $targetId]); + } else { + Db::rollback(); + return ResponseHelper::error('无效的类型参数'); + } + + Db::commit(); + return ResponseHelper::success('', '移动成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('移动失败:' . $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/application/chukebao/model/AiKnowledgeBase.php b/application/chukebao/model/AiKnowledgeBase.php new file mode 100644 index 0000000..2e08c8f --- /dev/null +++ b/application/chukebao/model/AiKnowledgeBase.php @@ -0,0 +1,52 @@ +belongsTo(AiKnowledgeBaseType::class, 'typeId', 'id'); + } + + /** + * 获取有效的知识库列表(未删除) + */ + public static function getValidList($companyId, $typeId = null) + { + $where = [ + ['isDel', '=', 0], + ['companyId', '=', $companyId] + ]; + + if ($typeId !== null) { + $where[] = ['typeId', '=', $typeId]; + } + + return self::where($where) + ->with(['type']) + ->order('createTime', 'desc') + ->select(); + } +} + diff --git a/application/chukebao/model/AiKnowledgeBaseType.php b/application/chukebao/model/AiKnowledgeBaseType.php new file mode 100644 index 0000000..80fa9f2 --- /dev/null +++ b/application/chukebao/model/AiKnowledgeBaseType.php @@ -0,0 +1,57 @@ +order('createTime', 'desc') + ->select(); + } + + /** + * 检查是否为系统类型 + */ + public function isSystemType() + { + return $this->type == self::TYPE_SYSTEM; + } +} + diff --git a/application/chukebao/model/AiPush.php b/application/chukebao/model/AiPush.php new file mode 100644 index 0000000..567c132 --- /dev/null +++ b/application/chukebao/model/AiPush.php @@ -0,0 +1,17 @@ + +// +---------------------------------------------------------------------- + +return [ + 'device:list' => 'app\command\DeviceListCommand', // 设备列表 √ + 'wechatFriends:list' => 'app\command\WechatFriendCommand', // 微信好友列表 √ + 'wechatChatroom:list' => 'app\command\WechatChatroomCommand', // 微信群列表 √ + 'friendTask:list' => 'app\command\FriendTaskCommand', // 添加好友任务列表 √ + 'wechatList:list' => 'app\command\WechatListCommand', // 微信客服列表 √ + 'account:list' => 'app\command\AccountListCommand', // 公司账号列表 √ + 'message:friendsList' => 'app\command\MessageFriendsListCommand', // 微信好友消息列表 √ + 'message:chatroomList' => 'app\command\MessageChatroomListCommand', // 微信群聊消息列表 √ + 'department:list' => 'app\command\DepartmentListCommand', // 部门列表 √ + 'content:sync' => 'app\command\SyncContentCommand', // 同步内容库 XXXXXXXX + 'groupFriends:list' => 'app\command\GroupFriendsCommand', // 微信群好友列表 + // 'allotFriends:run' => 'app\command\AllotFriendCommand', // 自动分配微信好友 + // 'allotChatroom:run' => 'app\command\AllotChatroomCommand', // 自动分配微信群聊 + 'allotrule:list' => 'app\command\AllotRuleListCommand', // 分配规则列表 √ + 'allotrule:autocreate' => 'app\command\AutoCreateAllotRulesCommand', // 自动创建分配规则 √ + 'content:collect' => 'app\command\ContentCollectCommand', // 内容采集任务 √ + 'moments:collect' => 'app\command\WechatMomentsCommand', // 朋友圈采集任务 + 'own:moments:collect' => 'app\command\OwnMomentsCollectCommand', // 采集在线微信账号自己的朋友圈 + 'switch:friends' => 'app\command\SwitchFriendsCommand', + 'call-recording:list' => 'app\command\CallRecordingListCommand', // 通话记录列表 √ + 'sync:wechatData' => 'app\command\SyncWechatDataToCkbTask', // 同步微信数据到存客宝 + 'sync:allFriends' => 'app\command\SyncAllFriendsCommand', // 同步所有在线好友 + + 'workbench:autoLike' => 'app\command\WorkbenchAutoLikeCommand', // 工作台自动点赞任务 + 'workbench:moments' => 'app\command\WorkbenchMomentsCommand', // 工作台朋友圈同步任务 + 'workbench:trafficDistribute' => 'app\command\WorkbenchTrafficDistributeCommand', // 工作台流量分发任务 + 'workbench:groupPush' => 'app\command\WorkbenchGroupPushCommand', // 工作台群推送任务 + 'workbench:groupCreate' => 'app\command\WorkbenchGroupCreateCommand', // 工作台群创建任务 + 'workbench:import-contact' => 'app\command\WorkbenchImportContactCommand', // 工作台通讯录导入任务 + 'kf:notice' => 'app\command\KfNoticeCommand', // 客服端消息通知 + + 'wechat:calculate-score' => 'app\command\CalculateWechatAccountScoreCommand', // 统一计算微信账号健康分 + 'wechat:update-score' => 'app\command\UpdateWechatAccountScoreCommand', // 更新微信账号评分记录 + + // 统一任务调度器 + 'scheduler:run' => 'app\command\TaskSchedulerCommand', // 统一任务调度器,支持多进程并发执行 +]; diff --git a/application/command/AccountListCommand.php b/application/command/AccountListCommand.php new file mode 100644 index 0000000..7a7fabd --- /dev/null +++ b/application/command/AccountListCommand.php @@ -0,0 +1,57 @@ +setName('account:list') + ->setDescription('获取公司账号列表,并根据分页自动处理下一页'); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln('开始处理公司账号列表任务...'); + + try { + // 初始页码 + $pageIndex = 0; + $pageSize = 100; // 每页获取100条记录 + + // 将第一页任务添加到队列 + $this->addToQueue($pageIndex, $pageSize); + + $output->writeln('公司账号列表任务已添加到队列'); + } catch (\Exception $e) { + Log::error('公司账号列表任务添加失败:' . $e->getMessage()); + $output->writeln('公司账号列表任务添加失败:' . $e->getMessage()); + return false; + } + + return true; + } + + /** + * 添加任务到队列 + * @param int $pageIndex 页码 + * @param int $pageSize 每页大小 + */ + protected function addToQueue($pageIndex, $pageSize) + { + $data = [ + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize + ]; + + // 添加到队列,设置任务名为 account_list + Queue::push(AccountListJob::class, $data, 'account_list'); + } +} \ No newline at end of file diff --git a/application/command/AllotChatroomCommand.php b/application/command/AllotChatroomCommand.php new file mode 100644 index 0000000..e0a7684 --- /dev/null +++ b/application/command/AllotChatroomCommand.php @@ -0,0 +1,102 @@ +setName('allotChatroom:run') + ->setDescription('自动分配微信群聊') + ->addOption('toAccountId', null, Option::VALUE_REQUIRED, '目标账号ID') + ->addOption('wechatAccountKeyword', null, Option::VALUE_REQUIRED, '微信账号关键字') + ->addOption('isDeleted', null, Option::VALUE_OPTIONAL, '是否已删除状态: 0=未删除(false), 1=已删除(true)', 0) + ->addOption('jobId', null, Option::VALUE_OPTIONAL, '任务ID,用于区分不同实例', date('YmdHis') . rand(1000, 9999)); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln('开始处理微信群聊自动分配任务...'); + + try { + // 获取命令参数 + $toAccountId = $input->getOption('toAccountId'); + $wechatAccountKeyword = $input->getOption('wechatAccountKeyword'); + $isDeleted = $input->getOption('isDeleted'); + $jobId = $input->getOption('jobId'); + + // 验证必填参数 + if (empty($toAccountId)) { + $output->writeln('错误: 目标账号ID不能为空'); + return false; + } + + if (empty($wechatAccountKeyword)) { + $output->writeln('错误: 微信账号关键字不能为空'); + return false; + } + + $output->writeln('目标账号ID: ' . $toAccountId); + $output->writeln('微信账号关键字: ' . $wechatAccountKeyword); + $output->writeln('删除状态: ' . ($isDeleted ? '已删除' : '未删除')); + $output->writeln('任务ID: ' . $jobId); + + // 检查队列是否已经在运行 + $queueLockKey = "queue_lock:{$this->queueName}:{$wechatAccountKeyword}"; + if (Cache::get($queueLockKey)) { + $output->writeln("队列 {$this->queueName} 已经在运行中,wechatAccountKeyword:{$wechatAccountKeyword},跳过执行"); + Log::warning("队列 {$this->queueName} 已经在运行中,wechatAccountKeyword:{$wechatAccountKeyword},跳过执行"); + return false; + } + + // 设置队列运行锁,有效期1小时 + Cache::set($queueLockKey, $jobId, 3600); + $output->writeln("已设置队列运行锁,键名:{$queueLockKey},值:{$jobId},有效期:1小时"); + + // 将任务添加到队列 + $this->addToQueue($toAccountId, $wechatAccountKeyword, $isDeleted, $jobId, $queueLockKey); + + $output->writeln('微信群聊自动分配任务已添加到队列'); + } catch (\Exception $e) { + Log::error('微信群聊自动分配任务添加失败:' . $e->getMessage()); + $output->writeln('微信群聊自动分配任务添加失败:' . $e->getMessage()); + return false; + } + + return true; + } + + /** + * 添加任务到队列 + * @param string $toAccountId 目标账号ID + * @param string $wechatAccountKeyword 微信账号关键字 + * @param bool $isDeleted 是否已删除状态 + * @param string $jobId 任务ID + * @param string $queueLockKey 队列锁键名 + */ + public function addToQueue($toAccountId, $wechatAccountKeyword, $isDeleted = false, $jobId = '', $queueLockKey = '') + { + $data = [ + 'toAccountId' => $toAccountId, + 'wechatAccountKeyword' => $wechatAccountKeyword, + 'isDeleted' => $isDeleted, + 'jobId' => $jobId, + 'queueLockKey' => $queueLockKey + ]; + + // 添加到队列 + Queue::push(AllotChatroomJob::class, $data, $this->queueName); + } +} \ No newline at end of file diff --git a/application/command/AllotFriendCommand.php b/application/command/AllotFriendCommand.php new file mode 100644 index 0000000..d8d34b8 --- /dev/null +++ b/application/command/AllotFriendCommand.php @@ -0,0 +1,102 @@ +setName('allotFriends:run') + ->setDescription('自动分配微信好友') + ->addOption('toAccountId', null, Option::VALUE_REQUIRED, '目标账号ID') + ->addOption('wechatAccountKeyword', null, Option::VALUE_REQUIRED, '微信账号关键字') + ->addOption('isDeleted', null, Option::VALUE_OPTIONAL, '是否已删除状态: 0=未删除(false), 1=已删除(true)', 0) + ->addOption('jobId', null, Option::VALUE_OPTIONAL, '任务ID,用于区分不同实例', date('YmdHis') . rand(1000, 9999)); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln('开始处理微信好友自动分配任务...'); + + try { + // 获取命令参数 + $toAccountId = $input->getOption('toAccountId'); + $wechatAccountKeyword = $input->getOption('wechatAccountKeyword'); + $isDeleted = $input->getOption('isDeleted'); + $jobId = $input->getOption('jobId'); + + // 验证必填参数 + if (empty($toAccountId)) { + $output->writeln('错误: 目标账号ID不能为空'); + return false; + } + + if (empty($wechatAccountKeyword)) { + $output->writeln('错误: 微信账号关键字不能为空'); + return false; + } + + $output->writeln('目标账号ID: ' . $toAccountId); + $output->writeln('微信账号关键字: ' . $wechatAccountKeyword); + $output->writeln('删除状态: ' . ($isDeleted ? '已删除' : '未删除')); + $output->writeln('任务ID: ' . $jobId); + + // 检查队列是否已经在运行 + $queueLockKey = "queue_lock:{$this->queueName}:{$wechatAccountKeyword}"; + if (Cache::get($queueLockKey)) { + $output->writeln("队列 {$this->queueName} 已经在运行中,wechatAccountKeyword:{$wechatAccountKeyword},跳过执行"); + Log::warning("队列 {$this->queueName} 已经在运行中,wechatAccountKeyword:{$wechatAccountKeyword},跳过执行"); + return false; + } + + // 设置队列运行锁,有效期1小时 + Cache::set($queueLockKey, $jobId, 3600); + $output->writeln("已设置队列运行锁,键名:{$queueLockKey},值:{$jobId},有效期:1小时"); + + // 将任务添加到队列 + $this->addToQueue($toAccountId, $wechatAccountKeyword, $isDeleted, $jobId, $queueLockKey); + + $output->writeln('微信好友自动分配任务已添加到队列'); + } catch (\Exception $e) { + Log::error('微信好友自动分配任务添加失败:' . $e->getMessage()); + $output->writeln('微信好友自动分配任务添加失败:' . $e->getMessage()); + return false; + } + + return true; + } + + /** + * 添加任务到队列 + * @param string $toAccountId 目标账号ID + * @param string $wechatAccountKeyword 微信账号关键字 + * @param bool $isDeleted 是否已删除状态 + * @param string $jobId 任务ID + * @param string $queueLockKey 队列锁键名 + */ + public function addToQueue($toAccountId, $wechatAccountKeyword, $isDeleted = false, $jobId = '', $queueLockKey = '') + { + $data = [ + 'toAccountId' => $toAccountId, + 'wechatAccountKeyword' => $wechatAccountKeyword, + 'isDeleted' => $isDeleted, + 'jobId' => $jobId, + 'queueLockKey' => $queueLockKey + ]; + + // 添加到队列 + Queue::push(AllotFriendJob::class, $data, $this->queueName); + } +} \ No newline at end of file diff --git a/application/command/AllotRuleListCommand.php b/application/command/AllotRuleListCommand.php new file mode 100644 index 0000000..4ef93a5 --- /dev/null +++ b/application/command/AllotRuleListCommand.php @@ -0,0 +1,50 @@ +setName('allotrule:list') + ->setDescription('获取分配规则列表,自动同步到数据库'); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln('开始处理分配规则列表任务...'); + + try { + // 将任务添加到队列 + $this->addToQueue(); + + $output->writeln('分配规则列表任务已添加到队列'); + } catch (\Exception $e) { + Log::error('分配规则列表任务添加失败:' . $e->getMessage()); + $output->writeln('分配规则列表任务添加失败:' . $e->getMessage()); + return false; + } + + return true; + } + + /** + * 添加任务到队列 + */ + protected function addToQueue() + { + $data = [ + 'time' => time() + ]; + + // 添加到队列,设置任务名为 allotrule_list + Queue::push(AllotRuleListJob::class, $data, 'allotrule_list'); + } +} \ No newline at end of file diff --git a/application/command/AutoCreateAllotRulesCommand.php b/application/command/AutoCreateAllotRulesCommand.php new file mode 100644 index 0000000..3f54726 --- /dev/null +++ b/application/command/AutoCreateAllotRulesCommand.php @@ -0,0 +1,50 @@ +setName('allotrule:autocreate') + ->setDescription('自动创建微信分配规则'); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln('开始处理自动创建分配规则任务...'); + + try { + // 将任务添加到队列 + $this->addToQueue(); + + $output->writeln('自动创建分配规则任务已添加到队列'); + } catch (\Exception $e) { + Log::error('自动创建分配规则任务添加失败:' . $e->getMessage()); + $output->writeln('自动创建分配规则任务添加失败:' . $e->getMessage()); + return false; + } + + return true; + } + + /** + * 添加任务到队列 + */ + protected function addToQueue() + { + $data = [ + 'time' => time() + ]; + + // 添加到队列,设置任务名为 autocreate_allotrule + Queue::push(AutoCreateAllotRulesJob::class, $data, 'autocreate_allotrule'); + } +} \ No newline at end of file diff --git a/application/command/CalculateWechatAccountScoreCommand.php b/application/command/CalculateWechatAccountScoreCommand.php new file mode 100644 index 0000000..2b1c996 --- /dev/null +++ b/application/command/CalculateWechatAccountScoreCommand.php @@ -0,0 +1,554 @@ +setName('wechat:calculate-score') + ->setDescription('统一计算微信账号健康分(包含初始化、更新评分记录、批量计算)') + ->addOption('only-init', null, \think\console\input\Option::VALUE_NONE, '仅执行初始化步骤') + ->addOption('only-update', null, \think\console\input\Option::VALUE_NONE, '仅执行更新评分记录步骤') + ->addOption('only-batch', null, \think\console\input\Option::VALUE_NONE, '仅执行批量更新健康分步骤') + ->addOption('account-id', 'a', \think\console\input\Option::VALUE_OPTIONAL, '指定账号ID,仅处理该账号') + ->addOption('batch-size', 'b', \think\console\input\Option::VALUE_OPTIONAL, '批处理大小', 50) + ->addOption('force-recalculate', 'f', \think\console\input\Option::VALUE_NONE, '强制重新计算基础分'); + } + + /** + * 执行命令 + * + * @param Input $input 输入对象 + * @param Output $output 输出对象 + * @return int 命令执行状态码(0表示成功) + */ + protected function execute(Input $input, Output $output) + { + // 解析命令行参数 + $onlyInit = $input->getOption('only-init'); + $onlyUpdate = $input->getOption('only-update'); + $onlyBatch = $input->getOption('only-batch'); + $accountId = $input->getOption('account-id'); + $batchSize = (int)$input->getOption('batch-size'); + $forceRecalculate = $input->getOption('force-recalculate'); + + // 参数验证 + if ($batchSize <= 0) { + $batchSize = 50; // 默认批处理大小 + } + + // 显示执行参数 + $output->writeln("=========================================="); + $output->writeln("开始统一计算微信账号健康分..."); + $output->writeln("=========================================="); + + if ($accountId) { + $output->writeln("指定账号ID: {$accountId}"); + } + + if ($onlyInit) { + $output->writeln("仅执行初始化步骤"); + } elseif ($onlyUpdate) { + $output->writeln("仅执行更新评分记录步骤"); + } elseif ($onlyBatch) { + $output->writeln("仅执行批量更新健康分步骤"); + } + + if ($forceRecalculate) { + $output->writeln("强制重新计算基础分"); + } + + $output->writeln("批处理大小: {$batchSize}"); + + // 记录命令开始执行的日志(仅在非交互模式下记录) + if (!$output->isVerbose()) { + Log::info('开始执行微信账号健康分计算命令', [ + 'accountId' => $accountId, + 'onlyInit' => $onlyInit ? 'true' : 'false', + 'onlyUpdate' => $onlyUpdate ? 'true' : 'false', + 'onlyBatch' => $onlyBatch ? 'true' : 'false', + 'batchSize' => $batchSize, + 'forceRecalculate' => $forceRecalculate ? 'true' : 'false' + ]); + + $startTime = time(); + + try { + // 实例化服务 + $service = new WechatAccountHealthScoreService(); + } catch (\Exception $e) { + $errorMsg = "实例化WechatAccountHealthScoreService失败: " . $e->getMessage(); + $output->writeln("{$errorMsg}"); + 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数据库错误: " . $errorMsg . ""); + $output->writeln($e->getTraceAsString()); + + // 记录数据库错误 + Log::error("数据库错误: " . $errorMsg); + Log::error("错误堆栈: " . $e->getTraceAsString()); + + return 2; // 数据库错误状态码 + } catch (\Exception $e) { + // 一般异常 + $errorMsg = "命令执行失败: " . $e->getMessage(); + $output->writeln("\n错误: " . $errorMsg . ""); + $output->writeln($e->getTraceAsString()); + + // 记录严重错误 + Log::error($errorMsg); + Log::error("错误堆栈: " . $e->getTraceAsString()); + + return 1; // 一般错误状态码 + } catch (\Throwable $e) { + // 其他所有错误 + $errorMsg = "严重错误: " . $e->getMessage(); + $output->writeln("\n严重错误: " . $errorMsg . ""); + $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; + } + } +} + diff --git a/application/command/CallRecordingListCommand.php b/application/command/CallRecordingListCommand.php new file mode 100644 index 0000000..0077083 --- /dev/null +++ b/application/command/CallRecordingListCommand.php @@ -0,0 +1,57 @@ +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'); + } +} diff --git a/application/command/CleanExpiredGroupMessages.php b/application/command/CleanExpiredGroupMessages.php new file mode 100644 index 0000000..78942cc --- /dev/null +++ b/application/command/CleanExpiredGroupMessages.php @@ -0,0 +1,100 @@ +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("Running in dry-run mode. No data will be deleted."); + } + + $cutoffDate = date('Y-m-d H:i:s', strtotime("-{$days} days")); + $output->writeln("Cleaning group messages older than {$cutoffDate} (keeping last {$days} days)"); + + // 清理微信群组消息 + $this->cleanWechatGroupMessages($cutoffDate, $dryRun, $batchSize, $output); + + $output->writeln("Group message cleanup completed successfully."); + } + + 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(" No expired group messages found."); + return; + } + + $output->writeln(" Found {$totalCount} group messages to clean up."); + + if ($dryRun) { + $output->writeln(" Dry run mode: would delete {$totalCount} group messages."); + 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(" Successfully deleted {$deletedCount} expired group messages."); + + // 优化表 + $output->writeln(" Optimizing table..."); + Db::execute("OPTIMIZE TABLE s2_wechat_group_message"); + $output->writeln(" Table optimization completed."); + } +} \ No newline at end of file diff --git a/application/command/CleanExpiredMessages.php b/application/command/CleanExpiredMessages.php new file mode 100644 index 0000000..d499f65 --- /dev/null +++ b/application/command/CleanExpiredMessages.php @@ -0,0 +1,100 @@ +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("Running in dry-run mode. No data will be deleted."); + } + + $cutoffDate = date('Y-m-d H:i:s', strtotime("-{$days} days")); + $output->writeln("Cleaning messages older than {$cutoffDate} (keeping last {$days} days)"); + + // 清理微信消息 + $this->cleanWechatMessages($cutoffDate, $dryRun, $batchSize, $output); + + $output->writeln("Message cleanup completed successfully."); + } + + 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(" No expired messages found."); + return; + } + + $output->writeln(" Found {$totalCount} messages to clean up."); + + if ($dryRun) { + $output->writeln(" Dry run mode: would delete {$totalCount} messages."); + 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(" Successfully deleted {$deletedCount} expired messages."); + + // 优化表 + $output->writeln(" Optimizing table..."); + Db::execute("OPTIMIZE TABLE s2_wechat_message"); + $output->writeln(" Table optimization completed."); + } +} \ No newline at end of file diff --git a/application/command/ContentCollectCommand.php b/application/command/ContentCollectCommand.php new file mode 100644 index 0000000..f7c79f8 --- /dev/null +++ b/application/command/ContentCollectCommand.php @@ -0,0 +1,51 @@ +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'); + } +} \ No newline at end of file diff --git a/application/command/DepartmentListCommand.php b/application/command/DepartmentListCommand.php new file mode 100644 index 0000000..262deb5 --- /dev/null +++ b/application/command/DepartmentListCommand.php @@ -0,0 +1,57 @@ +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'); + } +} \ No newline at end of file diff --git a/application/command/DeviceListCommand.php b/application/command/DeviceListCommand.php new file mode 100644 index 0000000..ec83682 --- /dev/null +++ b/application/command/DeviceListCommand.php @@ -0,0 +1,98 @@ +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); + } +} \ No newline at end of file diff --git a/application/command/FriendTaskCommand.php b/application/command/FriendTaskCommand.php new file mode 100644 index 0000000..7f3c71b --- /dev/null +++ b/application/command/FriendTaskCommand.php @@ -0,0 +1,60 @@ +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'); + } +} \ No newline at end of file diff --git a/application/command/GroupFriendsCommand.php b/application/command/GroupFriendsCommand.php new file mode 100644 index 0000000..fa9c396 --- /dev/null +++ b/application/command/GroupFriendsCommand.php @@ -0,0 +1,60 @@ +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'); + } +} \ No newline at end of file diff --git a/application/command/InitDatabase.php b/application/command/InitDatabase.php new file mode 100644 index 0000000..1107076 --- /dev/null +++ b/application/command/InitDatabase.php @@ -0,0 +1,50 @@ +setName('init:database') + ->setDescription('初始化数据库,创建必要的表结构'); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln('开始初始化数据库...'); + + try { + // 读取SQL文件 + $sqlFile = app()->getAppPath() . 'common/database/tk_users.sql'; + + if (!file_exists($sqlFile)) { + $output->error('SQL文件不存在: ' . $sqlFile); + return; + } + + $sql = file_get_contents($sqlFile); + + // 分割SQL语句 + $sqlArr = explode(';', $sql); + + // 执行SQL语句 + foreach ($sqlArr as $statement) { + $statement = trim($statement); + if ($statement) { + Db::execute($statement); + $output->writeln('执行SQL: ' . mb_substr($statement, 0, 100) . '...'); + } + } + + $output->info('数据库初始化完成!'); + } catch (\Exception $e) { + $output->error('数据库初始化失败: ' . $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/application/command/KfNoticeCommand.php b/application/command/KfNoticeCommand.php new file mode 100644 index 0000000..4c48582 --- /dev/null +++ b/application/command/KfNoticeCommand.php @@ -0,0 +1,118 @@ +setName('kfNotice:run') + ->setDescription('消息通知'); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln('开始处理消息通知任务...'); + $where = [ + ['isRemind', '=', 0], + ['reminderTime', '<=', time()], + ]; + $notice = []; + Db::startTrans(); + try { + $followUp = FollowUp::where($where)->alias('a') + ->field('a.*,f.nickname,f.avatar,f.alias,f.wechatId') + ->join(['s2_wechat_friend f'], 'a.friendId = f.id') + ->where('a.isRemind',0) + ->select(); + if (!empty($followUp)) { + foreach ($followUp as $k => $v) { + switch ($v['type']) { + case 1: + $title = '电话回访'; + break; + case 2: + $title = '发送消息'; + break; + case 3: + $title = '安排会议'; + break; + case 4: + $title = '发送邮件'; + break; + default: + $title = '其他'; + break; + } + + $wechatId = !empty($v['alias']) ? $v['alias'] : $v['wechatId']; + $nickname = $v['nickname'] . '(' . $wechatId . ')'; + $message = $nickname . ':' . $v['description']; + $notice[] = [ + 'type' => 2, + 'userId' => $v['userId'], + 'companyId' => $v['companyId'], + 'bindId' => $v['id'], + 'title' => $title, + 'message' => $message, + 'createTime' => $v['reminderTime'], + ]; + } + FollowUp::where($where)->update(['isRemind' => 1]); + } + + $toDo = ToDo::where($where)->alias('a') + ->field('a.*,f.nickname,f.avatar,f.alias,f.wechatId') + ->join(['s2_wechat_friend f'], 'a.friendId = f.id') + ->where('a.isRemind',0) + ->select(); + if (!empty($toDo)) { + foreach ($toDo as $k => $v) { + + $wechatId = !empty($v['alias']) ? $v['alias'] : $v['wechatId']; + $nickname = $v['nickname'] . '(' . $wechatId . ')'; + $message = $nickname . ':' . $v['description']; + + + $notice[] = [ + 'type' => 1, + 'userId' => $v['userId'], + 'companyId' => $v['companyId'], + 'bindId' => $v['id'], + 'title' => $v['title'], + 'message' => $message, + 'createTime' => $v['reminderTime'], + ]; + } + ToDo::where($where)->update(['isRemind' => 1]); + } + + $noticeModel = new NoticeModel(); + $noticeModel->insertAll($notice); + Db::commit(); + return true; + } catch (\Exception $e) { + Db::rollback(); + return false; + } + + } + +} \ No newline at end of file diff --git a/application/command/MessageChatroomListCommand.php b/application/command/MessageChatroomListCommand.php new file mode 100644 index 0000000..7ee0cf4 --- /dev/null +++ b/application/command/MessageChatroomListCommand.php @@ -0,0 +1,57 @@ +setName('message:chatroomList') + ->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 + ]; + + // 添加到队列,设置任务名为 friend_task + Queue::push(MessageChatroomListJob::class, $data, 'message_chatroom_list'); + } +} \ No newline at end of file diff --git a/application/command/MessageFriendsListCommand.php b/application/command/MessageFriendsListCommand.php new file mode 100644 index 0000000..e7df57b --- /dev/null +++ b/application/command/MessageFriendsListCommand.php @@ -0,0 +1,57 @@ +setName('message:friendsList') + ->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 + ]; + + // 添加到队列,设置任务名为 friend_task + Queue::push(MessageFriendsListJob::class, $data, 'message_friends_list'); + } +} \ No newline at end of file diff --git a/application/command/OptimizeMessageIndexes.php b/application/command/OptimizeMessageIndexes.php new file mode 100644 index 0000000..a604191 --- /dev/null +++ b/application/command/OptimizeMessageIndexes.php @@ -0,0 +1,112 @@ +setName('optimize:message_indexes') + ->setDescription('Optimize database indexes for message-related tables'); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln("Starting index optimization for message-related tables..."); + + // 优化 s2_wechat_message 表索引 + $this->optimizeWechatMessageIndexes($output); + + // 优化 s2_wechat_chatroom 表索引 + $this->optimizeWechatChatroomIndexes($output); + + // 优化 s2_wechat_friend 表索引 + $this->optimizeWechatFriendIndexes($output); + + $output->writeln("Index optimization completed successfully."); + } + + protected function optimizeWechatMessageIndexes(Output $output) + { + $output->writeln("Optimizing s2_wechat_message table indexes..."); + + // 检查并添加 wechatChatroomId 索引 + $this->addIndexIfNotExists('s2_wechat_message', 'idx_chatroom_id', 'wechatChatroomId', $output); + + // 检查并添加 wechatFriendId 索引 + $this->addIndexIfNotExists('s2_wechat_message', 'idx_friend_id', 'wechatFriendId', $output); + + // 检查并添加 isRead 索引 + $this->addIndexIfNotExists('s2_wechat_message', 'idx_is_read', 'isRead', $output); + + // 检查并添加 type 索引 + $this->addIndexIfNotExists('s2_wechat_message', 'idx_type', 'type', $output); + + // 检查并添加 createTime 索引 + $this->addIndexIfNotExists('s2_wechat_message', 'idx_create_time', 'createTime', $output); + + // 检查并添加组合索引 (wechatChatroomId, isRead) + $this->addIndexIfNotExists('s2_wechat_message', 'idx_chatroom_read', 'wechatChatroomId,isRead', $output); + + // 检查并添加组合索引 (wechatFriendId, isRead) + $this->addIndexIfNotExists('s2_wechat_message', 'idx_friend_read', 'wechatFriendId,isRead', $output); + } + + protected function optimizeWechatChatroomIndexes(Output $output) + { + $output->writeln("Optimizing s2_wechat_chatroom table indexes..."); + + // 检查并添加 accountId 索引 + $this->addIndexIfNotExists('s2_wechat_chatroom', 'idx_account_id', 'accountId', $output); + + // 检查并添加 isDeleted 索引 + $this->addIndexIfNotExists('s2_wechat_chatroom', 'idx_is_deleted', 'isDeleted', $output); + + // 检查并添加组合索引 (accountId, isDeleted) + $this->addIndexIfNotExists('s2_wechat_chatroom', 'idx_account_deleted', 'accountId,isDeleted', $output); + } + + protected function optimizeWechatFriendIndexes(Output $output) + { + $output->writeln("Optimizing s2_wechat_friend table indexes..."); + + // 检查并添加 accountId 索引 + $this->addIndexIfNotExists('s2_wechat_friend', 'idx_account_id', 'accountId', $output); + + // 检查并添加 isDeleted 索引 + $this->addIndexIfNotExists('s2_wechat_friend', 'idx_is_deleted', 'isDeleted', $output); + + // 检查并添加组合索引 (accountId, isDeleted) + $this->addIndexIfNotExists('s2_wechat_friend', 'idx_account_deleted', 'accountId,isDeleted', $output); + } + + protected function addIndexIfNotExists($table, $indexName, $columns, Output $output) + { + try { + // 检查索引是否已存在 + $indexExists = false; + $indexes = Db::query("SHOW INDEX FROM {$table}"); + + foreach ($indexes as $index) { + if ($index['Key_name'] === $indexName) { + $indexExists = true; + break; + } + } + + if (!$indexExists) { + // 添加索引 + Db::execute("ALTER TABLE {$table} ADD INDEX {$indexName} ({$columns})"); + $output->writeln(" - Added index {$indexName} on {$table}({$columns})"); + } else { + $output->writeln(" - Index {$indexName} already exists on {$table}"); + } + } catch (\Exception $e) { + $output->writeln(" - Error adding index {$indexName} to {$table}: " . $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/application/command/OwnMomentsCollectCommand.php b/application/command/OwnMomentsCollectCommand.php new file mode 100644 index 0000000..846f011 --- /dev/null +++ b/application/command/OwnMomentsCollectCommand.php @@ -0,0 +1,51 @@ +setName('own:moments: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 = [ + 'timestamp' => time() + ]; + + // 添加到队列,设置任务名为 own_moments_collect + Queue::push(OwnMomentsCollectJob::class, $data, 'own_moments_collect'); + } +} + diff --git a/application/command/ScheduleMessageMaintenance.php b/application/command/ScheduleMessageMaintenance.php new file mode 100644 index 0000000..359faf9 --- /dev/null +++ b/application/command/ScheduleMessageMaintenance.php @@ -0,0 +1,121 @@ +setName('schedule:message_maintenance') + ->setDescription('Schedule and run message maintenance tasks') + ->addOption('optimize-indexes', null, Option::VALUE_NONE, 'Run index optimization') + ->addOption('clean-messages', null, Option::VALUE_NONE, 'Clean expired messages') + ->addOption('days', 'd', Option::VALUE_OPTIONAL, 'Number of days to keep messages (default: 90)', 90) + ->addOption('batch-size', 'b', Option::VALUE_OPTIONAL, 'Batch size for deletion (default: 1000)', 1000) + ->addOption('dry-run', null, Option::VALUE_NONE, 'Perform a dry run without deleting any data'); + } + + protected function execute(Input $input, Output $output) + { + $optimizeIndexes = $input->getOption('optimize-indexes'); + $cleanMessages = $input->getOption('clean-messages'); + $days = (int)$input->getOption('days'); + $batchSize = (int)$input->getOption('batch-size'); + $dryRun = $input->getOption('dry-run'); + + // 如果没有指定任何选项,则运行所有维护任务 + if (!$optimizeIndexes && !$cleanMessages) { + $optimizeIndexes = true; + $cleanMessages = true; + } + + $output->writeln("Starting scheduled message maintenance tasks..."); + $startTime = microtime(true); + + // 运行索引优化 + if ($optimizeIndexes) { + $this->runCommand($output, 'optimize:message_indexes'); + } + + // 清理过期消息 + if ($cleanMessages) { + $options = []; + + if ($days !== 90) { + $options[] = "--days={$days}"; + } + + if ($batchSize !== 1000) { + $options[] = "--batch-size={$batchSize}"; + } + + if ($dryRun) { + $options[] = "--dry-run"; + } + + $this->runCommand($output, 'clean:expired_messages', $options); + $this->runCommand($output, 'clean:expired_group_messages', $options); + } + + $endTime = microtime(true); + $executionTime = round($endTime - $startTime, 2); + $output->writeln("All maintenance tasks completed in {$executionTime} seconds."); + } + + protected function runCommand(Output $output, $command, array $options = []) + { + $output->writeln("\nRunning command: {$command}"); + + $optionsStr = implode(' ', $options); + $fullCommand = "php think {$command} {$optionsStr}"; + + $output->writeln("Executing: {$fullCommand}"); + $output->writeln("\nCommand output:"); + + // 执行命令并实时输出结果 + $descriptorSpec = [ + 0 => ["pipe", "r"], // stdin + 1 => ["pipe", "w"], // stdout + 2 => ["pipe", "w"] // stderr + ]; + + $process = proc_open($fullCommand, $descriptorSpec, $pipes); + + if (is_resource($process)) { + // 关闭标准输入 + fclose($pipes[0]); + + // 读取标准输出 + while (!feof($pipes[1])) { + $line = fgets($pipes[1]); + if ($line !== false) { + $output->write($line); + } + } + fclose($pipes[1]); + + // 读取标准错误 + $errorOutput = stream_get_contents($pipes[2]); + fclose($pipes[2]); + + // 获取命令执行结果 + $exitCode = proc_close($process); + + if ($exitCode !== 0) { + $output->writeln("\nCommand failed with exit code {$exitCode}"); + if (!empty($errorOutput)) { + $output->writeln("Error output:"); + $output->writeln($errorOutput); + } + } else { + $output->writeln("\nCommand completed successfully."); + } + } else { + $output->writeln("Failed to execute command."); + } + } +} \ No newline at end of file diff --git a/application/command/SwitchFriendsCommand.php b/application/command/SwitchFriendsCommand.php new file mode 100644 index 0000000..2ea7751 --- /dev/null +++ b/application/command/SwitchFriendsCommand.php @@ -0,0 +1,270 @@ +setName('switch:friends') + ->setDescription('切换好友命令'); + } + + protected function execute(Input $input, Output $output) + { + // 清理可能损坏的缓存数据 + $this->clearCorruptedCache($output); + + //处理流量分过期数据 + $expUserData = Db::name('workbench_traffic_config_item') + ->where('expTime','<=',time()) + ->where('isRecycle',0) + ->select(); + + // 根据accountId对数组进行归类 + $groupedByAccount = []; + foreach ($expUserData as $friend) { + $accountId = $friend['wechatAccountId']; + if (!isset($groupedByAccount[$accountId])) { + $groupedByAccount[$accountId] = []; + } + $friendId = $friend['wechatFriendId']; + $groupedByAccount[$accountId][] = $friendId; + } + + // 对每个账号的好友进行20个为一组的分组 + foreach ($groupedByAccount as $accountId => $accountFriends) { + //检索主账号 + $account = Db::name('users')->where('s2_accountId',$accountId)->find(); + if (empty($account)) { + continue; + } + $account2 = Db::name('users') + ->where('s2_accountId','>',0) + ->where('companyId',$account['companyId']) + ->order('s2_accountId ASC') + ->find(); + if (empty($account2)) { + continue; + } + $newaAccountId = $account2['s2_accountId']; + + $chunks = array_chunk($accountFriends, 20); + $output->writeln('账号 ' . $newaAccountId . ' 共有 ' . count($accountFriends) . ' 个好友,分为 ' . count($chunks) . ' 组'); + + $automaticAssign = new AutomaticAssign(); + foreach ($chunks as $chunkIndex => $chunk) { + $output->writeln('处理账号 ' . $newaAccountId . ' 第 ' . ($chunkIndex + 1) . ' 组,共 ' . count($chunk) . ' 个好友'); + try { + $friendIds = implode(',', $chunk); + $res = $automaticAssign->multiAllotFriendToAccount([ + 'wechatFriendIds' => $friendIds, + 'toAccountId' => $newaAccountId, + ]); + $res = json_decode($res, true); + if ($res['code'] == 200){ + //修改数据库 + Db::table('s2_wechat_friend') + ->where('id',$friendIds) + ->update([ + 'accountId' => $account2['s2_accountId'], + 'accountUserName' => $account2['account'], + 'accountRealName' => $account2['username'], + 'accountNickname' => $account2['username'], + ]); + + Db::name('workbench_traffic_config_item') + ->whereIn('wechatFriendId',$friendIds) + ->where('wechatAccountId',$accountId) + ->update([ + 'isRecycle' => 1, + 'recycleTime' => time(), + ]); + $output->writeln('✓ 成功切换好友:' . $friendIds . ' 到账号:' . $newaAccountId); + } else { + $output->writeln('✗ 切换失败 - 好友:' . $friendIds . ' 到账号:' . $newaAccountId . ' 结果:' . $res['msg']); + } + } catch (\Exception $e) { + $output->writeln('✗ 切换异常 - 好友:' . implode(',', $chunk) . ' 到账号:' . $newaAccountId . ' 错误:' . $e->getMessage()); + } + + // 每组处理完后稍作延迟,避免请求过于频繁 + if ($chunkIndex < count($chunks) - 1) { + sleep(1); + } + } + } + + + + + + $cacheKey = 'allotWechatFriend'; + $now = time(); + $maxRetry = 5; + $retry = 0; + $switchedIds = []; + $totalProcessed = 0; + $totalSuccess = 0; + $totalFailed = 0; + + $output->writeln('开始执行好友切换任务...'); + + do { + try { + $friends = Cache::get($cacheKey, []); + } catch (\Exception $e) { + // 如果缓存数据损坏,清空缓存并记录错误 + $output->writeln('缓存数据损坏,正在清空缓存: ' . $e->getMessage()); + Cache::rm($cacheKey); + $friends = []; + } + + $toSwitch = []; + foreach ($friends as $friend) { + if (isset($friend['time']) && $friend['time'] < $now) { + $toSwitch[] = $friend; + } + } + + if (empty($toSwitch)) { + $output->writeln('没有需要切换的好友'); + return; + } + + $output->writeln('找到 ' . count($toSwitch) . ' 个需要切换的好友'); + + $automaticAssign = new AutomaticAssign(); + + // 根据accountId对数组进行归类 + $groupedByAccount = []; + foreach ($toSwitch as $friend) { + $accountId = $friend['accountId']; + if (!isset($groupedByAccount[$accountId])) { + $groupedByAccount[$accountId] = []; + } + $friendId = !empty($friend['friendId']) ? $friend['friendId'] : $friend['id']; + $groupedByAccount[$accountId][] = $friendId; + } + + + // 对每个账号的好友进行20个为一组的分组 + foreach ($groupedByAccount as $accountId => $accountFriends) { + $chunks = array_chunk($accountFriends, 20); + $output->writeln('账号 ' . $accountId . ' 共有 ' . count($accountFriends) . ' 个好友,分为 ' . count($chunks) . ' 组'); + $accountSuccess = 0; + $accountFailed = 0; + + foreach ($chunks as $chunkIndex => $chunk) { + $output->writeln('处理账号 ' . $accountId . ' 第 ' . ($chunkIndex + 1) . ' 组,共 ' . count($chunk) . ' 个好友'); + try { + $friendIds = implode(',', $chunk); + $res = $automaticAssign->multiAllotFriendToAccount([ + 'wechatFriendIds' => $friendIds, + 'toAccountId' => $accountId, + ]); + $res = json_decode($res, true); + if ($res['code'] == 200){ + $output->writeln('✓ 成功切换好友:' . $friendIds . ' 到账号:' . $accountId); + $switchedIds = array_merge($switchedIds, $chunk); + $accountSuccess += count($chunk); + $totalSuccess += count($chunk); + } else { + $output->writeln('✗ 切换失败 - 好友:' . $friendIds . ' 到账号:' . $accountId . ' 结果:' . $res['msg']); + $accountFailed += count($chunk); + $totalFailed += count($chunk); + } + } catch (\Exception $e) { + $output->writeln('✗ 切换异常 - 好友:' . implode(',', $chunk) . ' 到账号:' . $accountId . ' 错误:' . $e->getMessage()); + Log::error('切换好友异常: ' . $e->getMessage() . ' 好友IDs: ' . implode(',', $chunk) . ' 账号ID: ' . $accountId); + $accountFailed += count($chunk); + $totalFailed += count($chunk); + } + + $totalProcessed += count($chunk); + + // 每组处理完后稍作延迟,避免请求过于频繁 + if ($chunkIndex < count($chunks) - 1) { + sleep(1); + } + } + + $output->writeln('账号 ' . $accountId . ' 处理完成 - 成功:' . $accountSuccess . ',失败:' . $accountFailed); + } + + // 过滤掉已切换的,保留未切换和新进来的 + try { + $newFriends = Cache::get($cacheKey, []); + } catch (\Exception $e) { + // 如果缓存数据损坏,清空缓存并记录错误 + $output->writeln('缓存数据损坏,正在清空缓存: ' . $e->getMessage()); + Cache::rm($cacheKey); + $newFriends = []; + } + + $updated = []; + foreach ($newFriends as $friend) { + $friendId = !empty($friend['friendId']) ? $friend['friendId'] : $friend['id']; + if (!in_array($friendId, $switchedIds)) { + $updated[] = $friend; + } + } + + // 按time升序排序 + usort($updated, function($a, $b) { + return ($a['time'] ?? 0) <=> ($b['time'] ?? 0); + }); + + try { + $success = Cache::set($cacheKey, $updated); + } catch (\Exception $e) { + // 如果缓存设置失败,记录错误并继续 + $output->writeln('缓存设置失败: ' . $e->getMessage()); + $success = false; + } + $retry++; + } while (!$success && $retry < $maxRetry); + + $output->writeln('=== 切换任务完成 ==='); + $output->writeln('总处理数量:' . $totalProcessed); + $output->writeln('成功切换:' . $totalSuccess); + $output->writeln('切换失败:' . $totalFailed); + $output->writeln('成功率:' . ($totalProcessed > 0 ? round(($totalSuccess / $totalProcessed) * 100, 2) : 0) . '%'); + $output->writeln('缓存已更新并排序'); + } + + /** + * 清理损坏的缓存数据 + * @param Output $output + */ + private function clearCorruptedCache(Output $output) + { + $cacheKey = 'allotWechatFriend'; + try { + // 尝试读取缓存,如果失败则清空 + $testData = Cache::get($cacheKey, []); + if (!is_array($testData)) { + $output->writeln('缓存数据格式错误,正在清空缓存'); + Cache::rm($cacheKey); + } + } catch (\Exception $e) { + $output->writeln('检测到损坏的缓存数据,正在清空: ' . $e->getMessage()); + Cache::rm($cacheKey); + } + } + +} \ No newline at end of file diff --git a/application/command/SyncAllFriendsCommand.php b/application/command/SyncAllFriendsCommand.php new file mode 100644 index 0000000..e4b82d8 --- /dev/null +++ b/application/command/SyncAllFriendsCommand.php @@ -0,0 +1,67 @@ +setName('sync:allFriends') + ->setDescription('同步所有好友(自动分页队列)') + ->addOption('jobId', null, Option::VALUE_OPTIONAL, '任务ID', date('YmdHis') . rand(1000, 9999)); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln('开始同步所有好友...'); + try { + $jobId = $input->getOption('jobId'); + $queueLockKey = "queue_lock:{$this->queueName}"; + Cache::rm($queueLockKey); + if (Cache::get($queueLockKey)) { + $output->writeln("队列 {$this->queueName} 已经在运行中,跳过执行"); + Log::warning("队列 {$this->queueName} 已经在运行中,跳过执行"); + return false; + } + Cache::set($queueLockKey, $jobId, 3600); + $output->writeln("已设置队列运行锁,键名:{$queueLockKey},值:{$jobId},有效期:1小时"); + + $pageSize = 1000; + $accounts = Db::table('s2_wechat_account')->where('wechatAlive', 1)->select(); + foreach ($accounts as $account) { + $this->addToQueue($account['wechatId'], 0, $pageSize, '', $jobId, $queueLockKey); + } + + $output->writeln('同步所有好友任务已添加到队列'); + } catch (\Exception $e) { + Log::error('同步所有好友任务添加失败:' . $e->getMessage()); + $output->writeln('同步所有好友任务添加失败:' . $e->getMessage()); + return false; + } + return true; + } + + public function addToQueue($wechatId, $pageIndex, $pageSize, $preFriendId, $jobId, $queueLockKey) + { + $data = [ + 'wechatId' => $wechatId, + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize, + 'preFriendId' => $preFriendId, + 'jobId' => $jobId, + 'queueLockKey' => $queueLockKey + ]; + Queue::push(SyncAllFriendsJob::class, $data, $this->queueName); + } +} \ No newline at end of file diff --git a/application/command/SyncContentCommand.php b/application/command/SyncContentCommand.php new file mode 100644 index 0000000..401b007 --- /dev/null +++ b/application/command/SyncContentCommand.php @@ -0,0 +1,34 @@ +setName('content:sync') + ->setDescription('同步内容库数据'); + } + + protected function execute(Input $input, Output $output) + { + // 将任务推送到队列 + $jobHandlerClassName = 'app\job\SyncContentJob'; + $jobData = [ + 'time' => time(), + 'type' => 'sync_content' + ]; + + $isPushed = Queue::push($jobHandlerClassName, $jobData); + + if ($isPushed !== false) { + $output->writeln("同步任务已推送到队列"); + } else { + $output->writeln("同步任务推送失败"); + } + } +} \ No newline at end of file diff --git a/application/command/SyncWechatDataToCkbTask.php b/application/command/SyncWechatDataToCkbTask.php new file mode 100644 index 0000000..688f4fd --- /dev/null +++ b/application/command/SyncWechatDataToCkbTask.php @@ -0,0 +1,128 @@ +> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/sync_wechat_data.log 2>&1 +class SyncWechatDataToCkbTask extends Command +{ + protected $lockFile; + + public function __construct() + { + parent::__construct(); + $this->lockFile = App::getRuntimePath() . 'sync_wechat_to_ckb.lock'; + } + + + protected function configure() + { + $this->setName('sync:wechatData') + ->setDescription('同步微信数据到存客宝'); + } + + + protected function execute(Input $input, Output $output) + { + // 检查锁文件 + if (file_exists($this->lockFile)) { + $lockTime = filectime($this->lockFile); + if (time() - $lockTime < 3600) { + Log::info('微信好友同步任务已在运行中,跳过本次执行'); + return false; + } + unlink($this->lockFile); + } + + file_put_contents($this->lockFile, time()); + + try { + + $output->writeln("同步任务 sync_wechat_to_ckb 开始"); + $ChuKeBaoAdapter = new ChuKeBaoAdapter(); + $this->syncWechatAccount($ChuKeBaoAdapter); + $this->syncWechatFriend($ChuKeBaoAdapter); + $this->syncWechatDeviceLoginLog($ChuKeBaoAdapter); + $this->syncWechatDevice($ChuKeBaoAdapter); + $this->syncWechatCustomer($ChuKeBaoAdapter); + $this->syncWechatGroup($ChuKeBaoAdapter); + $this->syncWechatGroupCustomer($ChuKeBaoAdapter); + $this->syncWechatFriendToTrafficPoolBatch($ChuKeBaoAdapter); + $this->syncTrafficSourceUser($ChuKeBaoAdapter); + $this->syncTrafficSourceGroup($ChuKeBaoAdapter); + $this->syncCallRecording($ChuKeBaoAdapter); + + $output->writeln("同步任务 sync_wechat_to_ckb 已结束"); + return true; + } catch (\Exception $e) { + Log::error('微信好友同步任务异常:' . $e->getMessage()); + return false; + } finally { + if (file_exists($this->lockFile)) { + unlink($this->lockFile); + } + } + } + + protected function syncWechatFriend(ChuKeBaoAdapter $ChuKeBaoAdapter) + { + return $ChuKeBaoAdapter->syncFriendship(); + } + + protected function syncWechatAccount(ChuKeBaoAdapter $ChuKeBaoAdapter) + { + return $ChuKeBaoAdapter->syncWechatAccount(); + } + + protected function syncWechatDeviceLoginLog(ChuKeBaoAdapter $ChuKeBaoAdapter) + { + return $ChuKeBaoAdapter->syncWechatDeviceLoginLog(); + } + + // syncDevice + protected function syncWechatDevice(ChuKeBaoAdapter $ChuKeBaoAdapter) + { + return $ChuKeBaoAdapter->syncDevice(); + } + + // syncWechatCustomer + protected function syncWechatCustomer(ChuKeBaoAdapter $ChuKeBaoAdapter) + { + return $ChuKeBaoAdapter->syncWechatCustomer(); + } + + protected function syncWechatFriendToTrafficPoolBatch(ChuKeBaoAdapter $ChuKeBaoAdapter) + { + return $ChuKeBaoAdapter->syncWechatFriendToTrafficPoolBatch(); + } + protected function syncTrafficSourceUser(ChuKeBaoAdapter $ChuKeBaoAdapter) + { + return $ChuKeBaoAdapter->syncTrafficSourceUser(); + } + + protected function syncTrafficSourceGroup(ChuKeBaoAdapter $ChuKeBaoAdapter) + { + return $ChuKeBaoAdapter->syncTrafficSourceGroup(); + } + + protected function syncWechatGroup(ChuKeBaoAdapter $ChuKeBaoAdapter) + { + return $ChuKeBaoAdapter->syncWechatGroup(); + } + protected function syncWechatGroupCustomer(ChuKeBaoAdapter $ChuKeBaoAdapter) + { + return $ChuKeBaoAdapter->syncWechatGroupCustomer(); + } + protected function syncCallRecording(ChuKeBaoAdapter $ChuKeBaoAdapter) + { + return $ChuKeBaoAdapter->syncCallRecording(); + } + + +} \ No newline at end of file diff --git a/application/command/TaskSchedulerCommand.php b/application/command/TaskSchedulerCommand.php new file mode 100644 index 0000000..9cad5c1 --- /dev/null +++ b/application/command/TaskSchedulerCommand.php @@ -0,0 +1,478 @@ +> /path/to/log/scheduler.log 2>&1 + */ +class TaskSchedulerCommand extends Command +{ + /** + * 任务配置 + */ + protected $tasks = []; + + /** + * 最大并发进程数 + */ + protected $maxConcurrent = 10; + + /** + * 当前运行的进程数 + */ + protected $runningProcesses = []; + + /** + * 日志目录 + */ + protected $logDir = ''; + + protected function configure() + { + $this->setName('scheduler:run') + ->setDescription('统一任务调度器,支持多进程并发执行所有定时任务'); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln('=========================================='); + $output->writeln('任务调度器启动'); + $output->writeln('时间: ' . date('Y-m-d H:i:s')); + $output->writeln('=========================================='); + + // 检查是否支持 pcntl 扩展 + if (!function_exists('pcntl_fork')) { + $output->writeln('错误:系统不支持 pcntl 扩展,无法使用多进程功能'); + $output->writeln('提示:将使用单进程顺序执行任务'); + $this->maxConcurrent = 1; + } + + // 加载任务配置(优先使用框架配置,其次直接引入配置文件,避免加载失败) + $this->tasks = Config::get('task_scheduler', []); + + // 如果通过 Config 没有读到,再尝试直接 include 配置文件 + if (empty($this->tasks)) { + // 以项目根目录为基准查找 config/task_scheduler.php + $configFile = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'task_scheduler.php'; + if (is_file($configFile)) { + $config = include $configFile; + if (is_array($config) && !empty($config)) { + $this->tasks = $config; + } + } + } + + if (empty($this->tasks)) { + $output->writeln('错误:未找到任务配置(task_scheduler),请检查 config/task_scheduler.php 是否存在且返回数组'); + return false; + } + + // 设置日志目录(ThinkPHP5 中无 runtime_path 辅助函数,直接使用 ROOT_PATH/runtime/log) + if (!defined('ROOT_PATH')) { + // CLI 下正常情况下 ROOT_PATH 已在入口脚本 define,这里兜底一次 + define('ROOT_PATH', dirname(__DIR__, 2)); + } + $this->logDir = ROOT_PATH . DIRECTORY_SEPARATOR . 'runtime' . DIRECTORY_SEPARATOR . 'log' . DIRECTORY_SEPARATOR; + if (!is_dir($this->logDir)) { + mkdir($this->logDir, 0755, true); + } + + // 获取当前时间 + $currentTime = time(); + $currentMinute = date('i', $currentTime); + $currentHour = date('H', $currentTime); + $currentDay = date('d', $currentTime); + $currentMonth = date('m', $currentTime); + $currentWeekday = date('w', $currentTime); // 0=Sunday, 6=Saturday + + $output->writeln("当前时间: {$currentHour}:{$currentMinute}"); + $output->writeln("已加载 " . count($this->tasks) . " 个任务配置"); + + // 筛选需要执行的任务 + $tasksToRun = []; + foreach ($this->tasks as $taskId => $task) { + if (!isset($task['enabled']) || !$task['enabled']) { + continue; + } + + if ($this->shouldRun($task['schedule'], $currentMinute, $currentHour, $currentDay, $currentMonth, $currentWeekday)) { + $tasksToRun[$taskId] = $task; + } + } + + if (empty($tasksToRun)) { + $output->writeln('当前时间没有需要执行的任务'); + return true; + } + + $output->writeln("找到 " . count($tasksToRun) . " 个需要执行的任务"); + + // 执行任务 + if ($this->maxConcurrent > 1 && function_exists('pcntl_fork')) { + $this->executeConcurrent($tasksToRun, $output); + } else { + $this->executeSequential($tasksToRun, $output); + } + + // 清理僵尸进程 + $this->cleanupZombieProcesses(); + + $output->writeln('=========================================='); + $output->writeln('任务调度器执行完成'); + $output->writeln('=========================================='); + + return true; + } + + /** + * 判断任务是否应该执行 + * + * @param string $schedule cron表达式,格式:分钟 小时 日 月 星期 + * @param int $minute 当前分钟 + * @param int $hour 当前小时 + * @param int $day 当前日期 + * @param int $month 当前月份 + * @param int $weekday 当前星期 + * @return bool + */ + protected function shouldRun($schedule, $minute, $hour, $day, $month, $weekday) + { + $parts = preg_split('/\s+/', trim($schedule)); + if (count($parts) < 5) { + return false; + } + + list($scheduleMinute, $scheduleHour, $scheduleDay, $scheduleMonth, $scheduleWeekday) = $parts; + + // 解析分钟 + if (!$this->matchCronField($scheduleMinute, $minute)) { + return false; + } + + // 解析小时 + if (!$this->matchCronField($scheduleHour, $hour)) { + return false; + } + + // 解析日期 + if (!$this->matchCronField($scheduleDay, $day)) { + return false; + } + + // 解析月份 + if (!$this->matchCronField($scheduleMonth, $month)) { + return false; + } + + // 解析星期(注意:cron中0和7都表示星期日) + if ($scheduleWeekday !== '*') { + $scheduleWeekday = str_replace('7', '0', $scheduleWeekday); + if (!$this->matchCronField($scheduleWeekday, $weekday)) { + return false; + } + } + + return true; + } + + /** + * 匹配cron字段 + * + * @param string $field cron字段表达式 + * @param int $value 当前值 + * @return bool + */ + protected function matchCronField($field, $value) + { + // 通配符 + if ($field === '*') { + return true; + } + + // 列表(逗号分隔) + if (strpos($field, ',') !== false) { + $values = explode(',', $field); + foreach ($values as $v) { + if ($this->matchCronField(trim($v), $value)) { + return true; + } + } + return false; + } + + // 范围(如 1-5) + if (strpos($field, '-') !== false) { + list($start, $end) = explode('-', $field); + return $value >= (int)$start && $value <= (int)$end; + } + + // 步长(如 */5 或 0-59/5) + if (strpos($field, '/') !== false) { + $parts = explode('/', $field); + $base = $parts[0]; + $step = (int)$parts[1]; + + if ($base === '*') { + return $value % $step === 0; + } else { + // 处理范围步长,如 0-59/5 + if (strpos($base, '-') !== false) { + list($start, $end) = explode('-', $base); + if ($value >= (int)$start && $value <= (int)$end) { + return ($value - (int)$start) % $step === 0; + } + return false; + } else { + return $value % $step === 0; + } + } + } + + // 精确匹配 + return (int)$field === $value; + } + + /** + * 并发执行任务(多进程) + * + * @param array $tasks 任务列表 + * @param Output $output 输出对象 + */ + protected function executeConcurrent($tasks, Output $output) + { + $output->writeln('使用多进程并发执行任务(最大并发数:' . $this->maxConcurrent . ')'); + + foreach ($tasks as $taskId => $task) { + // 等待可用进程槽 + while (count($this->runningProcesses) >= $this->maxConcurrent) { + $this->waitForProcesses(); + usleep(100000); // 等待100ms + } + + // 检查任务是否已经在运行(防止重复执行) + $lockKey = "scheduler_task_lock:{$taskId}"; + $lockTime = Cache::get($lockKey); + if ($lockTime && (time() - $lockTime) < 300) { // 5分钟内不重复执行 + $output->writeln("任务 {$taskId} 正在运行中,跳过"); + continue; + } + + // 创建子进程 + $pid = pcntl_fork(); + + if ($pid == -1) { + // 创建进程失败 + $output->writeln("创建子进程失败:{$taskId}"); + Log::error("任务调度器:创建子进程失败", ['task' => $taskId]); + continue; + } elseif ($pid == 0) { + // 子进程:执行任务 + $this->runTask($taskId, $task); + exit(0); + } else { + // 父进程:记录子进程PID + $this->runningProcesses[$pid] = [ + 'task_id' => $taskId, + 'start_time' => time(), + ]; + $output->writeln("启动任务:{$taskId} (PID: {$pid})"); + + // 设置任务锁 + Cache::set($lockKey, time(), 600); // 10分钟过期 + } + } + + // 等待所有子进程完成 + while (!empty($this->runningProcesses)) { + $this->waitForProcesses(); + usleep(500000); // 等待500ms + } + } + + /** + * 顺序执行任务(单进程) + * + * @param array $tasks 任务列表 + * @param Output $output 输出对象 + */ + protected function executeSequential($tasks, Output $output) + { + $output->writeln('使用单进程顺序执行任务'); + + foreach ($tasks as $taskId => $task) { + $output->writeln("执行任务:{$taskId}"); + $this->runTask($taskId, $task); + } + } + + /** + * 执行单个任务 + * + * @param string $taskId 任务ID + * @param array $task 任务配置 + */ + protected function runTask($taskId, $task) + { + $startTime = microtime(true); + $logFile = $this->logDir . ($task['log_file'] ?? "scheduler_{$taskId}.log"); + + // 确保日志目录存在 + $logDir = dirname($logFile); + if (!is_dir($logDir)) { + mkdir($logDir, 0755, true); + } + + // 构建命令 + // 使用项目根目录下的 think 脚本(同命令行 php think) + if (!defined('ROOT_PATH')) { + define('ROOT_PATH', dirname(__DIR__, 2)); + } + $thinkPath = ROOT_PATH . DIRECTORY_SEPARATOR . 'think'; + $command = "php {$thinkPath} {$task['command']}"; + if (!empty($task['options'])) { + foreach ($task['options'] as $option) { + $command .= ' ' . escapeshellarg($option); + } + } + + // 添加日志重定向 + $command .= " >> " . escapeshellarg($logFile) . " 2>&1"; + + // 记录任务开始 + $logMessage = "\n" . str_repeat('=', 60) . "\n"; + $logMessage .= "任务开始执行: {$taskId}\n"; + $logMessage .= "执行时间: " . date('Y-m-d H:i:s') . "\n"; + $logMessage .= "命令: {$command}\n"; + $logMessage .= str_repeat('=', 60) . "\n"; + file_put_contents($logFile, $logMessage, FILE_APPEND); + + // 执行命令 + $descriptorspec = [ + 0 => ['file', (PHP_OS_FAMILY === 'Windows' ? 'NUL' : '/dev/null'), 'r'], // stdin + 1 => ['file', $logFile, 'a'], // stdout + 2 => ['file', $logFile, 'a'], // stderr + ]; + + $process = @proc_open($command, $descriptorspec, $pipes, ROOT_PATH); + + if (is_resource($process)) { + // 关闭管道 + if (isset($pipes[0])) @fclose($pipes[0]); + if (isset($pipes[1])) @fclose($pipes[1]); + if (isset($pipes[2])) @fclose($pipes[2]); + + // 设置超时 + $timeout = $task['timeout'] ?? 3600; + $startWaitTime = time(); + + // 等待进程完成或超时 + while (true) { + $status = proc_get_status($process); + + if (!$status['running']) { + break; + } + + // 检查超时 + if ((time() - $startWaitTime) > $timeout) { + if (function_exists('proc_terminate')) { + proc_terminate($process, SIGTERM); + // 等待进程终止 + sleep(2); + $status = proc_get_status($process); + if ($status['running']) { + // 强制终止 + proc_terminate($process, SIGKILL); + } + } + Log::warning("任务执行超时", [ + 'task' => $taskId, + 'timeout' => $timeout, + ]); + break; + } + + usleep(500000); // 等待500ms + } + + // 关闭进程 + proc_close($process); + } else { + // 如果 proc_open 失败,尝试直接执行(后台执行) + if (PHP_OS_FAMILY === 'Windows') { + pclose(popen("start /B " . $command, "r")); + } else { + exec($command . ' > /dev/null 2>&1 &'); + } + } + + $endTime = microtime(true); + $duration = round($endTime - $startTime, 2); + + // 记录任务完成 + $logMessage = "\n" . str_repeat('=', 60) . "\n"; + $logMessage .= "任务执行完成: {$taskId}\n"; + $logMessage .= "完成时间: " . date('Y-m-d H:i:s') . "\n"; + $logMessage .= "执行时长: {$duration} 秒\n"; + $logMessage .= str_repeat('=', 60) . "\n"; + file_put_contents($logFile, $logMessage, FILE_APPEND); + + Log::info("任务执行完成", [ + 'task' => $taskId, + 'duration' => $duration, + ]); + } + + /** + * 等待进程完成 + */ + protected function waitForProcesses() + { + foreach ($this->runningProcesses as $pid => $info) { + $status = 0; + $result = pcntl_waitpid($pid, $status, WNOHANG); + + if ($result == $pid || $result == -1) { + // 进程已结束 + unset($this->runningProcesses[$pid]); + + $duration = time() - $info['start_time']; + Log::info("子进程执行完成", [ + 'pid' => $pid, + 'task' => $info['task_id'], + 'duration' => $duration, + ]); + } + } + } + + /** + * 清理僵尸进程 + */ + protected function cleanupZombieProcesses() + { + if (!function_exists('pcntl_waitpid')) { + return; + } + + $status = 0; + while (($pid = pcntl_waitpid(-1, $status, WNOHANG)) > 0) { + // 清理僵尸进程 + } + } +} + diff --git a/application/command/UpdateWechatAccountScoreCommand.php b/application/command/UpdateWechatAccountScoreCommand.php new file mode 100644 index 0000000..13ded8d --- /dev/null +++ b/application/command/UpdateWechatAccountScoreCommand.php @@ -0,0 +1,168 @@ +setName('wechat:update-score') + ->setDescription('更新微信账号评分记录,根据wechatId和alias不一致情况更新isModifiedAlias字段(仅用于评分)'); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln("开始更新微信账号评分记录..."); + + try { + // 1. 查找所有需要更新的账号 + $output->writeln("步骤1: 查找需要更新的账号..."); + + // 查找wechatId和alias不一致的账号 + $inconsistentAccounts = Db::table('s2_wechat_account') + ->where('isDeleted', 0) + ->where('wechatId', '<>', '') + ->where('alias', '<>', '') + ->whereRaw('wechatId != alias') + ->field('id, wechatId, alias') + ->select(); + + // 查找wechatId和alias一致的账号 + $consistentAccounts = Db::table('s2_wechat_account') + ->where('isDeleted', 0) + ->where('wechatId', '<>', '') + ->where('alias', '<>', '') + ->whereRaw('wechatId = alias') + ->field('id, wechatId, alias') + ->select(); + + $output->writeln("发现 " . count($inconsistentAccounts) . " 条不一致记录(已修改微信号)"); + $output->writeln("发现 " . count($consistentAccounts) . " 条一致记录(未修改微信号)"); + + // 2. 更新评分记录表中的isModifiedAlias字段 + $output->writeln("步骤2: 更新评分记录表..."); + $updatedCount = 0; + $healthScoreService = new WechatAccountHealthScoreService(); + + // 更新不一致的记录 + foreach ($inconsistentAccounts as $account) { + $this->updateScoreRecord($account['id'], true, $healthScoreService); + $updatedCount++; + } + + // 更新一致的记录 + foreach ($consistentAccounts as $account) { + $this->updateScoreRecord($account['id'], false, $healthScoreService); + $updatedCount++; + } + + $output->writeln("已更新 " . $updatedCount . " 条评分记录"); + + // 3. 重新计算健康分(只更新基础信息分,不重新计算基础分) + $output->writeln("步骤3: 重新计算健康分..."); + $allAccountIds = array_merge( + array_column($inconsistentAccounts, 'id'), + array_column($consistentAccounts, 'id') + ); + + if (!empty($allAccountIds)) { + $stats = $healthScoreService->batchCalculateAndUpdate($allAccountIds, 100, false); + $output->writeln("健康分计算完成:成功 " . $stats['success'] . " 条,失败 " . $stats['failed'] . " 条"); + + if (!empty($stats['errors'])) { + $output->writeln("错误详情:"); + foreach ($stats['errors'] as $error) { + $output->writeln(" 账号ID {$error['accountId']}: {$error['error']}"); + } + } + } + + $output->writeln("任务完成!"); + + } catch (\Exception $e) { + $output->writeln("错误: " . $e->getMessage()); + $output->writeln($e->getTraceAsString()); + } + } + + /** + * 更新评分记录 + * + * @param int $accountId 账号ID + * @param bool $isModifiedAlias 是否已修改微信号 + * @param WechatAccountHealthScoreService $service 评分服务 + */ + private function updateScoreRecord($accountId, $isModifiedAlias, $service) + { + // 获取或创建评分记录 + $accountData = Db::table('s2_wechat_account') + ->where('id', $accountId) + ->find(); + + if (empty($accountData)) { + return; + } + + // 确保评分记录存在 + $scoreRecord = Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) + ->find(); + + if (empty($scoreRecord)) { + // 如果记录不存在,创建并计算基础分 + $service->calculateAndUpdate($accountId); + $scoreRecord = Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) + ->find(); + } + + if (empty($scoreRecord)) { + return; + } + + // 更新isModifiedAlias字段 + $updateData = [ + 'isModifiedAlias' => $isModifiedAlias ? 1 : 0, + 'updateTime' => time() + ]; + + // 如果基础分已计算,需要更新基础信息分和基础分 + if ($scoreRecord['baseScoreCalculated']) { + $oldBaseInfoScore = $scoreRecord['baseInfoScore'] ?? 0; + $newBaseInfoScore = $isModifiedAlias ? 10 : 0; // 已修改微信号得10分 + + if ($oldBaseInfoScore != $newBaseInfoScore) { + $oldBaseScore = $scoreRecord['baseScore'] ?? 60; + $newBaseScore = $oldBaseScore - $oldBaseInfoScore + $newBaseInfoScore; + + $updateData['baseInfoScore'] = $newBaseInfoScore; + $updateData['baseScore'] = $newBaseScore; + + // 重新计算健康分 + $dynamicScore = $scoreRecord['dynamicScore'] ?? 0; + $healthScore = $newBaseScore + $dynamicScore; + $healthScore = max(0, min(100, $healthScore)); + $updateData['healthScore'] = $healthScore; + $updateData['maxAddFriendPerDay'] = (int)floor($healthScore * 0.2); + } + } else { + // 基础分未计算,只更新标记和基础信息分 + $updateData['baseInfoScore'] = $isModifiedAlias ? 10 : 0; + } + + Db::table('s2_wechat_account_score') + ->where('accountId', $accountId) + ->update($updateData); + } +} + diff --git a/application/command/WechatChatroomCommand.php b/application/command/WechatChatroomCommand.php new file mode 100644 index 0000000..e770b4b --- /dev/null +++ b/application/command/WechatChatroomCommand.php @@ -0,0 +1,99 @@ +setName('wechatChatroom:list') + ->setDescription('获取微信聊天室列表,并根据分页自动处理下一页') + ->addOption('isDel', null, Option::VALUE_OPTIONAL, '删除状态: 0=未删除(false), 1=已删除(true)', '') + ->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 ? '未删除' : '已删除'))); + $output->writeln('任务ID: ' . $jobId); + + // 检查队列是否已经在运行 + $queueLockKey = "queue_lock:{$this->queueName}:{$isDel}"; + Cache::rm($queueLockKey); + 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 = "chatroomPage:{$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 + ]; + + // 添加到队列,设置任务名为 wechat_chatroom + Queue::push(WechatChatroomJob::class, $data, $this->queueName); + } +} \ No newline at end of file diff --git a/application/command/WechatFriendCommand.php b/application/command/WechatFriendCommand.php new file mode 100644 index 0000000..71a20e6 --- /dev/null +++ b/application/command/WechatFriendCommand.php @@ -0,0 +1,107 @@ +setName('wechatFriends:list') + ->setDescription('获微信列表,并根据分页自动处理下一页') + ->addOption('isDel', null, Option::VALUE_OPTIONAL, '删除状态: 0=未删除(false), 1=已删除(true)', '') + ->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 ? '未删除' : '已删除'))); + $output->writeln('任务ID: ' . $jobId); + + // 检查队列是否已经在运行 + $queueLockKey = "queue_lock:{$this->queueName}:{$isDel}"; + Cache::rm($queueLockKey); + 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 = "friendsPage:{$jobId}"; + $cacheKeySuffix = $isDel === '' ? '' : ":{$isDel}"; + $pageIndexCacheKey = $cacheKeyPrefix . $cacheKeySuffix; + $preFriendIdCacheKey = "preFriendId:{$jobId}" . $cacheKeySuffix; + + // 从缓存获取初始页码和上次处理的好友ID + $pageIndex = Cache::get($pageIndexCacheKey, 0); + $preFriendId = Cache::get($preFriendIdCacheKey, ''); + + $output->writeln("从缓存获取页码: {$pageIndex}, 上次处理的好友ID: {$preFriendId}"); + $output->writeln("缓存键: {$pageIndexCacheKey}, {$preFriendIdCacheKey}"); + + $pageSize = 100; // 每页获取100条记录 + + // 将任务添加到队列 + $this->addToQueue($pageIndex, $pageSize, $preFriendId, $isDel, $jobId, $pageIndexCacheKey, $preFriendIdCacheKey, $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 $preFriendId 上一个好友ID + * @param string $isDel 删除状态 + * @param string $jobId 任务ID + * @param string $pageIndexCacheKey 页码缓存键名 + * @param string $preFriendIdCacheKey 好友ID缓存键名 + * @param string $queueLockKey 队列锁键名 + */ + public function addToQueue($pageIndex, $pageSize, $preFriendId = '', $isDel = '', $jobId = '', $pageIndexCacheKey = '', $preFriendIdCacheKey = '', $queueLockKey = '') + { + $data = [ + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize, + 'preFriendId' => $preFriendId, + 'isDel' => $isDel, + 'jobId' => $jobId, + 'pageIndexCacheKey' => $pageIndexCacheKey, + 'preFriendIdCacheKey' => $preFriendIdCacheKey, + 'queueLockKey' => $queueLockKey + ]; + + // 添加到队列,设置任务名为 wechat_friends + Queue::push(WechatFriendJob::class, $data, $this->queueName); + } +} \ No newline at end of file diff --git a/application/command/WechatListCommand.php b/application/command/WechatListCommand.php new file mode 100644 index 0000000..6a7049f --- /dev/null +++ b/application/command/WechatListCommand.php @@ -0,0 +1,57 @@ +setName('wechat:list') + ->setDescription('获取微信客服列表,并根据分页自动处理下一页'); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln('开始处理微信客服列表任务...'); + + try { + // 初始页码 + $pageIndex = 0; + $pageSize = 500; // 每页获取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 + ]; + + // 添加到队列,设置任务名为 wechat_list + Queue::push(WechatListJob::class, $data, 'wechat_list'); + } +} \ No newline at end of file diff --git a/application/command/WechatMomentsCommand.php b/application/command/WechatMomentsCommand.php new file mode 100644 index 0000000..d305334 --- /dev/null +++ b/application/command/WechatMomentsCommand.php @@ -0,0 +1,99 @@ +setName('wechatMoments:list') + ->setDescription('获取朋友圈列表,并根据分页自动处理下一页') + ->addOption('jobId', null, Option::VALUE_OPTIONAL, '任务ID,用于区分不同实例', date('YmdHis') . rand(1000, 9999)); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln('开始处理朋友圈列表任务...'); + + try { + // 获取任务ID + $jobId = $input->getOption('jobId'); + + $output->writeln('任务ID: ' . $jobId); + + // 检查队列是否已经在运行 + $queueLockKey = "queue_lock:{$this->queueName}"; + if (Cache::get($queueLockKey)) { + $output->writeln("队列 {$this->queueName} 已经在运行中,跳过执行"); + Log::warning("队列 {$this->queueName} 已经在运行中,跳过执行"); + return false; + } + + // 设置队列运行锁,有效期1小时 + Cache::set($queueLockKey, $jobId, 3600); + $output->writeln("已设置队列运行锁,键名:{$queueLockKey},值:{$jobId},有效期:1小时"); + + // 为不同的任务ID使用不同的缓存键名 + $pageIndexCacheKey = "momentsPage:{$jobId}"; + $preMomentIdCacheKey = "preMomentId:{$jobId}"; + + // 从缓存获取初始页码和上次处理的朋友圈ID + $pageIndex = Cache::get($pageIndexCacheKey, 1); + $preMomentId = Cache::get($preMomentIdCacheKey, ''); + + $output->writeln("从缓存获取页码: {$pageIndex}, 上次处理的朋友圈ID: {$preMomentId}"); + $output->writeln("缓存键: {$pageIndexCacheKey}, {$preMomentIdCacheKey}"); + + $pageSize = 100; // 每页获取100条记录 + + // 将任务添加到队列 + $this->addToQueue($pageIndex, $pageSize, $preMomentId, $jobId, $pageIndexCacheKey, $preMomentIdCacheKey, $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 $preMomentId 上一个朋友圈ID + * @param string $jobId 任务ID + * @param string $pageIndexCacheKey 页码缓存键名 + * @param string $preMomentIdCacheKey 朋友圈ID缓存键名 + * @param string $queueLockKey 队列锁键名 + */ + public function addToQueue($pageIndex, $pageSize, $preMomentId = '', $jobId = '', $pageIndexCacheKey = '', $preMomentIdCacheKey = '', $queueLockKey = '') + { + $data = [ + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize, + 'preMomentId' => $preMomentId, + 'jobId' => $jobId, + 'pageIndexCacheKey' => $pageIndexCacheKey, + 'preMomentIdCacheKey' => $preMomentIdCacheKey, + 'queueLockKey' => $queueLockKey + ]; + + // 添加到队列,设置任务名为 wechat_moments + Queue::push(WechatMomentsJob::class, $data, $this->queueName); + } +} \ No newline at end of file diff --git a/application/command/WorkbenchAutoLikeCommand.php b/application/command/WorkbenchAutoLikeCommand.php new file mode 100644 index 0000000..0d6e785 --- /dev/null +++ b/application/command/WorkbenchAutoLikeCommand.php @@ -0,0 +1,77 @@ +setName('workbench:autoLike') + ->setDescription('工作台自动点赞任务队列') + ->addOption('jobId', null, Option::VALUE_OPTIONAL, '任务ID,用于区分不同实例', date('YmdHis') . rand(1000, 9999)); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln('开始处理工作台自动点赞任务...'); + + try { + // 获取任务ID + $jobId = $input->getOption('jobId'); + + $output->writeln('任务ID: ' . $jobId); + + // 检查队列是否已经在运行 + $queueLockKey = "queue_lock:{$this->queueName}"; + //Cache::rm($queueLockKey); + if (Cache::get($queueLockKey)) { + $output->writeln("队列 {$this->queueName} 已经在运行中,跳过执行"); + Log::warning("队列 {$this->queueName} 已经在运行中,跳过执行"); + return false; + } + + // 设置队列运行锁,有效期1小时 + Cache::set($queueLockKey, $jobId, 3600); + $output->writeln("已设置队列运行锁,键名:{$queueLockKey},值:{$jobId},有效期:1小时"); + + // 将任务添加到队列 + $this->addToQueue($jobId, $queueLockKey); + + $output->writeln('工作台自动点赞任务已添加到队列'); + } catch (\Exception $e) { + Log::error('工作台自动点赞任务添加失败:' . $e->getMessage()); + $output->writeln('工作台自动点赞任务添加失败:' . $e->getMessage()); + return false; + } + + return true; + } + + /** + * 添加任务到队列 + * @param string $jobId 任务ID + * @param string $queueLockKey 队列锁键名 + */ + public function addToQueue($jobId = '', $queueLockKey = '') + { + $data = [ + 'jobId' => $jobId, + 'queueLockKey' => $queueLockKey + ]; + + // 添加到队列,设置任务名为 workbench_auto_like + Queue::push(WorkbenchAutoLikeJob::class, $data, $this->queueName); + } +} \ No newline at end of file diff --git a/application/command/WorkbenchGroupCreateCommand.php b/application/command/WorkbenchGroupCreateCommand.php new file mode 100644 index 0000000..daa7030 --- /dev/null +++ b/application/command/WorkbenchGroupCreateCommand.php @@ -0,0 +1,76 @@ +setName('workbench:groupCreate') + ->setDescription('工作台群创建同步任务队列') + ->addOption('jobId', null, Option::VALUE_OPTIONAL, '任务ID,用于区分不同实例', date('YmdHis') . rand(1000, 9999)); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln('开始处理工作台群创建同步任务...'); + + try { + // 获取任务ID + $jobId = $input->getOption('jobId'); + + $output->writeln('任务ID: ' . $jobId); + + // 检查队列是否已经在运行 + $queueLockKey = "queue_lock:{$this->queueName}"; + Cache::rm($queueLockKey); + if (Cache::get($queueLockKey)) { + $output->writeln("队列 {$this->queueName} 已经在运行中,跳过执行"); + Log::warning("队列 {$this->queueName} 已经在运行中,跳过执行"); + return false; + } + + // 设置队列运行锁,有效期1小时 + Cache::set($queueLockKey, $jobId, 3600); + $output->writeln("已设置队列运行锁,键名:{$queueLockKey},值:{$jobId},有效期:1小时"); + + // 将任务添加到队列 + $this->addToQueue($jobId, $queueLockKey); + + $output->writeln('工作台群发同步任务已添加到队列'); + } catch (\Exception $e) { + Log::error('工作台群发同步任务添加失败:' . $e->getMessage()); + $output->writeln('工作台群发同步任务添加失败:' . $e->getMessage()); + return false; + } + + return true; + } + + /** + * 添加任务到队列 + * @param string $jobId 任务ID + * @param string $queueLockKey 队列锁键名 + */ + public function addToQueue($jobId = '', $queueLockKey = '') + { + $data = [ + 'jobId' => $jobId, + 'queueLockKey' => $queueLockKey + ]; + + // 添加到队列,设置任务名为 workbench_groupCreate + Queue::push(WorkbenchGroupCreateJob::class, $data, $this->queueName); + } +} \ No newline at end of file diff --git a/application/command/WorkbenchGroupPushCommand.php b/application/command/WorkbenchGroupPushCommand.php new file mode 100644 index 0000000..59c70dd --- /dev/null +++ b/application/command/WorkbenchGroupPushCommand.php @@ -0,0 +1,76 @@ +setName('workbench:groupPush') + ->setDescription('工作台群发同步任务队列') + ->addOption('jobId', null, Option::VALUE_OPTIONAL, '任务ID,用于区分不同实例', date('YmdHis') . rand(1000, 9999)); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln('开始处理工作台群发同步任务...'); + + try { + // 获取任务ID + $jobId = $input->getOption('jobId'); + + $output->writeln('任务ID: ' . $jobId); + + // 检查队列是否已经在运行 + $queueLockKey = "queue_lock:{$this->queueName}"; + Cache::rm($queueLockKey); + if (Cache::get($queueLockKey)) { + $output->writeln("队列 {$this->queueName} 已经在运行中,跳过执行"); + Log::warning("队列 {$this->queueName} 已经在运行中,跳过执行"); + return false; + } + + // 设置队列运行锁,有效期1小时 + Cache::set($queueLockKey, $jobId, 3600); + $output->writeln("已设置队列运行锁,键名:{$queueLockKey},值:{$jobId},有效期:1小时"); + + // 将任务添加到队列 + $this->addToQueue($jobId, $queueLockKey); + + $output->writeln('工作台群发同步任务已添加到队列'); + } catch (\Exception $e) { + Log::error('工作台群发同步任务添加失败:' . $e->getMessage()); + $output->writeln('工作台群发同步任务添加失败:' . $e->getMessage()); + return false; + } + + return true; + } + + /** + * 添加任务到队列 + * @param string $jobId 任务ID + * @param string $queueLockKey 队列锁键名 + */ + public function addToQueue($jobId = '', $queueLockKey = '') + { + $data = [ + 'jobId' => $jobId, + 'queueLockKey' => $queueLockKey + ]; + + // 添加到队列,设置任务名为 workbench_groupPush + Queue::push(WorkbenchGroupPushJob::class, $data, $this->queueName); + } +} \ No newline at end of file diff --git a/application/command/WorkbenchImportContactCommand.php b/application/command/WorkbenchImportContactCommand.php new file mode 100644 index 0000000..55593cc --- /dev/null +++ b/application/command/WorkbenchImportContactCommand.php @@ -0,0 +1,125 @@ +setName('workbench:import-contact') + ->setDescription('执行工作台通讯录导入任务'); + } + + /** + * 执行命令 + * @param Input $input + * @param Output $output + * @return int + */ + protected function execute(Input $input, Output $output) + { + $output->writeln('开始执行工作台通讯录导入任务...'); + + try { + // 检查是否有任务正在执行 + $lockKey = 'workbench_import_contact_lock'; + if (Cache::has($lockKey)) { + $output->writeln('通讯录导入任务正在执行中,跳过本次执行'); + return 0; + } + + // 设置执行锁,防止重复执行 + Cache::set($lockKey, time(), 3600); // 1小时锁定时间 + + // 生成任务ID + $jobId = 'workbench_import_contact_' . date('YmdHis') . '_' . mt_rand(1000, 9999); + + // 准备任务数据 + $jobData = [ + 'jobId' => $jobId, + 'queueLockKey' => $lockKey, + 'executeTime' => time() + ]; + // 判断是否使用队列 + if ($this->shouldUseQueue()) { + // 推送到队列 + Queue::push(WorkbenchImportContactJob::class, $jobData, 'workbench_import_contact'); + $output->writeln("通讯录导入任务已推送到队列,任务ID: {$jobId}"); + } else { + // 直接执行 + $job = new WorkbenchImportContactJob(); + $result = $job->execute(); + + // 释放锁 + Cache::rm($lockKey); + + if ($result !== false) { + $output->writeln('通讯录导入任务执行成功'); + } else { + $output->writeln('通讯录导入任务执行失败'); + return 1; + } + } + + } catch (\Exception $e) { + // 释放锁 + Cache::rm($lockKey ?? ''); + + $errorMsg = '通讯录导入任务执行异常: ' . $e->getMessage(); + $output->writeln($errorMsg); + Log::error($errorMsg); + return 1; + } + + return 0; + } + + /** + * 判断是否应该使用队列 + * @return bool + */ + protected function shouldUseQueue() + { + // 检查队列配置是否启用 + $queueConfig = config('queue'); + if (empty($queueConfig) || !isset($queueConfig['default'])) { + return false; + } + + // 检查队列连接是否可用 + try { + $connection = $queueConfig['connections'][$queueConfig['default']] ?? []; + if (empty($connection)) { + return false; + } + + // 如果是数据库队列,检查表是否存在 + if ($connection['type'] === 'database') { + $tableName = $connection['table'] ?? 'jobs'; + $exists = \think\Db::query("SHOW TABLES LIKE '{$tableName}'"); + return !empty($exists); + } + + return true; + } catch (\Exception $e) { + Log::warning('队列检查失败,将使用同步执行: ' . $e->getMessage()); + return false; + } + } +} \ No newline at end of file diff --git a/application/command/WorkbenchMomentsCommand.php b/application/command/WorkbenchMomentsCommand.php new file mode 100644 index 0000000..760b5a0 --- /dev/null +++ b/application/command/WorkbenchMomentsCommand.php @@ -0,0 +1,76 @@ +setName('workbench:moments') + ->setDescription('工作台朋友圈同步任务队列') + ->addOption('jobId', null, Option::VALUE_OPTIONAL, '任务ID,用于区分不同实例', date('YmdHis') . rand(1000, 9999)); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln('开始处理工作台朋友圈同步任务...'); + + try { + // 获取任务ID + $jobId = $input->getOption('jobId'); + + $output->writeln('任务ID: ' . $jobId); + + // 检查队列是否已经在运行 + $queueLockKey = "queue_lock:{$this->queueName}"; + Cache::rm($queueLockKey); + if (Cache::get($queueLockKey)) { + $output->writeln("队列 {$this->queueName} 已经在运行中,跳过执行"); + Log::warning("队列 {$this->queueName} 已经在运行中,跳过执行"); + return false; + } + + // 设置队列运行锁,有效期1小时 + Cache::set($queueLockKey, $jobId, 3600); + $output->writeln("已设置队列运行锁,键名:{$queueLockKey},值:{$jobId},有效期:1小时"); + + // 将任务添加到队列 + $this->addToQueue($jobId, $queueLockKey); + + $output->writeln('工作台朋友圈同步任务已添加到队列'); + } catch (\Exception $e) { + Log::error('工作台朋友圈同步任务添加失败:' . $e->getMessage()); + $output->writeln('工作台朋友圈同步任务添加失败:' . $e->getMessage()); + return false; + } + + return true; + } + + /** + * 添加任务到队列 + * @param string $jobId 任务ID + * @param string $queueLockKey 队列锁键名 + */ + public function addToQueue($jobId = '', $queueLockKey = '') + { + $data = [ + 'jobId' => $jobId, + 'queueLockKey' => $queueLockKey + ]; + + // 添加到队列,设置任务名为 workbench_moments + Queue::push(WorkbenchMomentsJob::class, $data, $this->queueName); + } +} \ No newline at end of file diff --git a/application/command/WorkbenchTrafficDistributeCommand.php b/application/command/WorkbenchTrafficDistributeCommand.php new file mode 100644 index 0000000..c3da23c --- /dev/null +++ b/application/command/WorkbenchTrafficDistributeCommand.php @@ -0,0 +1,61 @@ +setName('workbench:trafficDistribute') + ->setDescription('工作台流量分发任务队列') + ->addOption('jobId', null, Option::VALUE_OPTIONAL, '任务ID', date('YmdHis') . rand(1000, 9999)); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln('开始处理流量分发任务...'); + try { + $jobId = $input->getOption('jobId'); + $output->writeln('任务ID: ' . $jobId); + + $queueLockKey = "queue_lock:{$this->queueName}"; + Cache::rm($queueLockKey); + if (Cache::get($queueLockKey)) { + $output->writeln("队列 {$this->queueName} 已经在运行中,跳过执行"); + Log::warning("队列 {$this->queueName} 已经在运行中,跳过执行"); + return false; + } + Cache::set($queueLockKey, $jobId, 3600); + $output->writeln("已设置队列运行锁,键名:{$queueLockKey},值:{$jobId},有效期:1小时"); + + $this->addToQueue($jobId, $queueLockKey); + + $output->writeln('流量分发任务已添加到队列'); + } catch (\Exception $e) { + Log::error('流量分发任务添加失败:' . $e->getMessage()); + $output->writeln('流量分发任务添加失败:' . $e->getMessage()); + return false; + } + return true; + } + + public function addToQueue($jobId = '', $queueLockKey = '') + { + $data = [ + 'jobId' => $jobId, + 'queueLockKey' => $queueLockKey + ]; + Queue::push(WorkbenchTrafficDistributeJob::class, $data, $this->queueName); + } +} \ No newline at end of file diff --git a/application/common.php b/application/common.php new file mode 100644 index 0000000..8785f12 --- /dev/null +++ b/application/common.php @@ -0,0 +1,599 @@ + +// +---------------------------------------------------------------------- + +// 应用公共文件 +use app\common\service\AuthService; +use think\facade\Cache; + +if (!function_exists('requestCurl')) { + /** + * @param string $url 请求的链接 + * @param array $params 请求附带的参数 + * @param string $method 请求的方式, 支持GET, POST, PUT, DELETE等 + * @param array $header 头部 + * @param string $type 数据类型,支持dataBuild、json等 + * @return bool|string + */ + function requestCurl($url, $params = [], $method = 'GET', $header = [], $type = 'dataBuild') + { + $str = ''; + if (!empty($url)) { + try { + $ch = curl_init(); + + // 处理GET请求的参数 + if (strtoupper($method) == 'GET' && !empty($params)) { + $url = $url . '?' . dataBuild($params); + } + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); //30秒超时 + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + + // 处理不同的请求方法 + if (strtoupper($method) != 'GET') { + // 设置请求方法 + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($method)); + + // 处理参数格式 + if ($type == 'dataBuild') { + $params = dataBuild($params); + } elseif ($type == 'json') { + $params = json_encode($params); + } else { + $params = dataBuild($params); + } + + // 设置请求体 + curl_setopt($ch, CURLOPT_POSTFIELDS, $params); + } + + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); //是否验证对等证书,1则验证,0则不验证 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); + $str = curl_exec($ch); + curl_close($ch); + } catch (Exception $e) { + $str = ''; + } + } + return $str; + } +} + + +if (!function_exists('dataBuild')) { + function dataBuild($array) + { + if (!is_array($array)) { + return $array; + } + + // 处理嵌套数组 + foreach ($array as $key => $value) { + if (is_array($value)) { + $array[$key] = json_encode($value); + } + } + + return http_build_query($array); + } +} + + +if (!function_exists('setHeader')) { + /** + * 设置头部 + * + * @param array $headerData 头部数组 + * @param string $authorization + * @param string $type 类型 默认json (json,plain) + * @return array + */ + function setHeader($headerData = [], $authorization = '', $type = '') + { + $header = $headerData; + + switch ($type) { + case 'json': + $header[] = 'Content-Type:application/json'; + break; + case 'html' : + $header[] = 'Content-Type:text/html'; + break; + case 'plain' : + $header[] = 'Content-Type:text/plain'; + break; + default: + $header[] = 'Content-Type:application/json'; + } +// $header[] = $type == 'plain' ? 'Content-Type:text/plain' : 'Content-Type: application/json'; + if ($authorization !== "") $header[] = 'authorization:bearer ' . $authorization; + return $header; + } +} + +if (!function_exists('errorJson')) { + function errorJson($error = '', $code = 500) + { + return json([ + 'code' => $code, + 'msg' => $error, + ]); + } +} + + +if (!function_exists('successJson')) { + function successJson($data = [] ,$msg = '操作成功', $code = 200) + { + return json([ + 'data' => $data, + 'code' => $code, + 'msg' => $msg, + ]); + } +} + +if (!function_exists('validateString')) { + /** + * 通用字符串验证 + * @param string $string 待验证的字符串 + * @param string $type 验证类型 (password|email|mobile|nickname|url|idcard|bankcard|ip|date|username|chinese|english|number|zip|qq) + * @param array $options 额外的验证选项 + * @return array ['status' => bool, 'message' => string] + */ + function validateString($string, $type, $options = []) + { + // 默认配置 + $config = [ + 'password' => [ + 'min_length' => 6, + 'max_length' => 20, + 'pattern' => '/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z0-9]+$/', + 'error' => '密码必须包含英文和数字,不能包含特殊符号' + ], + 'email' => [ + 'pattern' => '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/', + 'error' => '邮箱格式不正确' + ], + 'mobile' => [ + 'pattern' => '/^1[3456789]\d{9}$/', + 'error' => '手机号格式不正确' + ], + 'nickname' => [ + 'min_length' => 2, + 'max_length' => 20, + 'pattern' => '/^[a-zA-Z0-9\x{4e00}-\x{9fa5}]+$/u', + 'error' => '昵称只能包含中文、英文、数字,长度2-20位' + ], + 'url' => [ + 'pattern' => '/^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/', + 'error' => '网址格式不正确' + ], + 'idcard' => [ + 'pattern' => '/(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/', + 'error' => '身份证号码格式不正确' + ], + 'bankcard' => [ + 'pattern' => '/^[1-9]\d{9,29}$/', + 'error' => '银行卡号格式不正确' + ], + 'ip' => [ + 'pattern' => '/^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/', + 'error' => 'IP地址格式不正确' + ], + 'date' => [ + 'pattern' => '/^\d{4}(-)(0[1-9]|1[0-2])(-)([0-2][1-9]|3[0-1])$/', + 'error' => '日期格式不正确(YYYY-MM-DD)' + ], + 'username' => [ + 'min_length' => 4, + 'max_length' => 20, + 'pattern' => '/^[a-zA-Z][a-zA-Z0-9_]{3,19}$/', + 'error' => '用户名必须以字母开头,只能包含字母、数字和下划线,长度4-20位' + ], + 'chinese' => [ + 'pattern' => '/^[\x{4e00}-\x{9fa5}]+$/u', + 'error' => '只能输入中文汉字' + ], + 'english' => [ + 'pattern' => '/^[a-zA-Z]+$/', + 'error' => '只能输入英文字母' + ], + 'number' => [ + 'pattern' => '/^[0-9]+$/', + 'error' => '只能输入数字' + ], + 'zip' => [ + 'pattern' => '/^\d{6}$/', + 'error' => '邮政编码格式不正确' + ], + 'qq' => [ + 'pattern' => '/^[1-9][0-9]{4,}$/', + 'error' => 'QQ号格式不正确' + ], + 'age' => [ + 'pattern' => '/^(?:[1-9][0-9]?|1[01][0-9]|120)$/', + 'error' => '年龄必须在1-120之间' + ], + 'phone' => [ + 'pattern' => '/^([0-9]{3,4}-)?[0-9]{7,8}$/', + 'error' => '固定电话格式不正确' + ], + 'money' => [ + 'pattern' => '/^[0-9]+\.?[0-9]{0,2}$/', + 'error' => '金额格式不正确(最多保留两位小数)' + ], + 'color' => [ + 'pattern' => '/^#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/', + 'error' => '颜色格式不正确(#RGB或#RRGGBB)' + ] + ]; + + // 合并自定义配置 + if (!empty($options)) { + $config[$type] = array_merge($config[$type], $options); + } + + // 空值检查 + if (empty($string)) { + return ['status' => false, 'message' => '不能为空']; + } + + switch ($type) { + case 'password': + case 'username': + // 长度检查 + if (strlen($string) < $config[$type]['min_length']) { + return [ + 'status' => false, + 'message' => '长度不能小于' . $config[$type]['min_length'] . '位' + ]; + } + if (strlen($string) > $config[$type]['max_length']) { + return [ + 'status' => false, + 'message' => '长度不能大于' . $config[$type]['max_length'] . '位' + ]; + } + // 格式检查 + if (!preg_match($config[$type]['pattern'], $string)) { + return [ + 'status' => false, + 'message' => $config[$type]['error'] + ]; + } + break; + + case 'nickname': + // 长度检查 + $length = mb_strlen($string, 'UTF-8'); + if ($length < $config['nickname']['min_length'] || $length > $config['nickname']['max_length']) { + return [ + 'status' => false, + 'message' => '昵称长度必须在' . $config['nickname']['min_length'] . '-' . $config['nickname']['max_length'] . '位之间' + ]; + } + // 格式检查 + if (!preg_match($config['nickname']['pattern'], $string)) { + return [ + 'status' => false, + 'message' => $config['nickname']['error'] + ]; + } + break; + + case 'idcard': + // 身份证号验证 + if (!preg_match($config['idcard']['pattern'], $string)) { + return [ + 'status' => false, + 'message' => $config['idcard']['error'] + ]; + } + // 进一步验证18位身份证的最后一位校验码 + if (strlen($string) == 18) { + $idCardBase = substr($string, 0, 17); + $verify = substr($string, 17, 1); + if (!checkIdCardVerify($idCardBase, $verify)) { + return [ + 'status' => false, + 'message' => '身份证校验码错误' + ]; + } + } + break; + + default: + if (!isset($config[$type])) { + return [ + 'status' => false, + 'message' => '不支持的验证类型' + ]; + } + if (!preg_match($config[$type]['pattern'], $string)) { + return [ + 'status' => false, + 'message' => $config[$type]['error'] + ]; + } + break; + } + + return ['status' => true, 'message' => '验证通过']; + } +} + +if (!function_exists('checkIdCardVerify')) { + /** + * 验证身份证校验码 + * @param string $idCardBase 身份证前17位 + * @param string $verify 校验码 + * @return bool + */ + function checkIdCardVerify($idCardBase, $verify) + { + if (strlen($idCardBase) != 17) { + return false; + } + + // 加权因子 + $factor = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; + + // 校验码对应值 + $verifyNumberList = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']; + + // 根据前17位计算校验码 + $sum = 0; + for ($i = 0; $i < 17; $i++) { + $sum += substr($idCardBase, $i, 1) * $factor[$i]; + } + + $mod = $sum % 11; + $verifyNumber = $verifyNumberList[$mod]; + + return strtoupper($verify) == $verifyNumber; + } +} + +if (!function_exists('handleApiResponse')) { + /** + * 处理API响应 + * @param string $response 响应内容 + * @param bool $returnRaw 是否返回原始数据 + * @return mixed + */ + function handleApiResponse($response, $returnRaw = false) + { + if (empty($response)) { + return $response; + } + + // 检查是否为JSON格式 + $decoded = json_decode($response, true); + if (json_last_error() === JSON_ERROR_NONE) { + return $decoded; + } + + + // 不是JSON格式,直接返回原始数据 + if($response == '无效路径或登录状态失效'){ + Cache::rm('system_refresh_token'); + Cache::rm('system_authorization_token'); + //AuthService::getSystemAuthorization(); + } + + return $response; + + } +} + +if (!function_exists('localEncrypt')) { + /** + * 本地密码加密 + * @param string $password 待加密的密码 + * @param string $key 加密密钥 + * @return string + */ + function localEncrypt($password, $key = 'karuo') + { + return openssl_encrypt($password, 'AES-256-CBC', $key, 0, substr(md5($key), 0, 16)); + } +} + +if (!function_exists('localDecrypt')) { + /** + * 本地密码解密 + * @param string $encrypted 已加密的密码 + * @param string $key 加密密钥 + * @return string + */ + function localDecrypt($encrypted, $key = 'karuo') + { + return openssl_decrypt($encrypted, 'AES-256-CBC', $key, 0, substr(md5($key), 0, 16)); + } +} + +if (!function_exists('recordUserLog')) { + /** + * 记录用户操作日志 + * @param int $userId 用户ID + * @param string $userName 用户名 + * @param string $action 操作类型 + * @param string $description 操作描述 + * @param array $requestData 请求数据 + * @param int $responseCode 响应状态码 + * @param string $responseMsg 响应消息 + * @return bool + */ + function recordUserLog($userId, $userName, $action, $description = '', $requestData = [], $responseCode = 200, $responseMsg = '操作成功') + { + try { + $request = request(); + + // 获取用户代理信息 + $userAgent = $request->header('user-agent'); + + // 准备日志数据 + $logData = [ + 'userId' => $userId, + 'userName' => $userName, + 'action' => $action, + 'description' => $description, + 'ip' => $request->ip(), + 'userAgent' => $userAgent, + 'requestMethod' => $request->method(), + 'requestUrl' => $request->url(true), + 'requestData' => !empty($requestData) ? json_encode($requestData, JSON_UNESCAPED_UNICODE) : '', + 'responseCode' => $responseCode, + 'responseMsg' => $responseMsg, + 'createTime' => time() + ]; + + // 写入日志 + \think\Db::name('user_log')->insert($logData); + return true; + } catch (\Exception $e) { + // 记录日志失败不影响主业务 + \think\facade\Log::error('记录用户日志失败:' . $e->getMessage()); + return false; + } + } +} + +if (!function_exists('getUserAction')) { + /** + * 获取用户操作类型常量 + * @return array + */ + function getUserAction() + { + return [ + 'LOGIN' => '登录', + 'LOGOUT' => '退出登录', + 'MODIFY_PASSWORD' => '修改密码', + 'GET_VERIFY_CODE' => '获取验证码', + 'UPDATE_PROFILE' => '更新个人信息', + 'REFRESH_TOKEN' => '刷新令牌', + 'API_REQUEST' => 'API请求', + 'FILE_UPLOAD' => '文件上传', + 'FILE_DOWNLOAD' => '文件下载', + 'DATA_EXPORT' => '数据导出', + 'DATA_IMPORT' => '数据导入', + 'CREATE' => '创建', + 'UPDATE' => '更新', + 'DELETE' => '删除', + 'QUERY' => '查询' + ]; + } +} + +if (!function_exists('formatRelativeTime')) { + /** + * 将时间戳格式化为相对时间(中文) + * 例:半年前 / 1个月前 / 3周前 / 1天前 / 5小时前 / 5分钟前 / 刚刚 + * @param int $timestamp Unix 时间戳(秒) + * @return string + */ + function formatRelativeTime($timestamp) + { + if (empty($timestamp) || !is_numeric($timestamp)) { + return ''; + } + $now = time(); + $diff = max(0, $now - (int)$timestamp); + + $minute = 60; + $hour = 60 * $minute; + $day = 24 * $hour; + $week = 7 * $day; + $month = 30 * $day; // 近似 + $halfYear = 6 * $month; // 近似 + + if ($diff >= $halfYear) return '半年前'; + if ($diff >= $month) return floor($diff / $month) . '个月前'; + if ($diff >= $week) return floor($diff / $week) . '周前'; + if ($diff >= $day) return floor($diff / $day) . '天前'; + if ($diff >= $hour) return floor($diff / $hour) . '小时前'; + if ($diff >= $minute) return floor($diff / $minute) . '分钟前'; + return '刚刚'; + } +} + + +if (!function_exists('exit_data')) { + + /** + * 截断输出 + * @param array $data + * @param string $type + * @param bool $exit + */ + function exit_data($data = [], $type = 'pr', $exit = true) + { + switch ($type) { + case 'pr': + $func = 'print_r'; + break; + case 'vd': + $func = 'var_dump'; + break; + default: + $func = 'print_r'; + break; + } + if ($func == 'print_r') { + echo '
';
+        }
+        call_user_func($func, $data);
+        if ($exit)
+            exit();
+    }
+}
+if (!function_exists('dump')) {
+    /**
+     * 调试打印变量但不终止程序
+     * @return void
+     */
+    function dump()
+    {
+        call_user_func_array(['app\\common\\helper\\Debug', 'dump'], func_get_args());
+    }
+}
+
+if (!function_exists('artificialAllotWechatFriend')) {
+    function artificialAllotWechatFriend($friend = [])
+    {
+        if (empty($friend)) {
+            return false;
+        }
+
+        //记录切换好友
+        $cacheKey = 'allotWechatFriend';
+        $cacheFriend = $friend;
+        $cacheFriend['time'] = time() + 120;
+        $maxRetry = 5;
+        $retry = 0;
+        do {
+            $cacheFriendData = Cache::get($cacheKey, []);
+            // 去重:移除同friendId的旧数据
+            $cacheFriendData = array_filter($cacheFriendData, function($item) use ($cacheFriend) {
+                return $item['friendId'] !== $cacheFriend['friendId'];
+            });
+            $cacheFriendData[] = $cacheFriend;
+            $success = Cache::set($cacheKey, $cacheFriendData);
+            $retry++;
+        } while (!$success && $retry < $maxRetry);
+    }
+}
\ No newline at end of file
diff --git a/application/common/Fork.php b/application/common/Fork.php
new file mode 100644
index 0000000..8154ad8
--- /dev/null
+++ b/application/common/Fork.php
@@ -0,0 +1,122 @@
+ $taskFunc) {
+            $pid = pcntl_fork();
+            if ($pid == -1) {
+                exit('Could not fork.');
+            } elseif ($pid == 0) {
+                call_user_func_array($taskFunc, [$i]);
+                exit(0);
+            }
+        }
+        foreach ($taskFuncs as $taskFunc) {
+            pcntl_wait($status);
+        }
+    }
+
+    /**
+     * 并发执行多条任务
+     *
+     * @param array $taskFuncs
+     */
+    static public function invoke(array $taskFuncs) {
+        $taskRuns = [];
+        $signal   = FALSE;
+        if (function_exists('pcntl_async_signals')) {
+            pcntl_async_signals(TRUE);
+        }
+        while (TRUE) {
+            foreach ($taskFuncs as $i => $taskFunc) {
+                if (!in_array($i, $taskRuns)) {
+                    $pid = pcntl_fork();
+                    if ($pid == -1) {
+                        exit('Could not fork.');
+                    } elseif ($pid > 0) {
+                        $taskRuns[$pid] = $i;
+                        if (!$signal) {
+                            $signal = TRUE;
+                            static::signalHandler();
+                        }
+                    } else {
+                        call_user_func_array($taskFunc, [$i]);
+                        exit(0);
+                    }
+                }
+            }
+            if ($pid = pcntl_wait($status, WNOHANG)) {
+                unset($taskRuns[$pid]);
+            } else {
+                sleep(1);
+            }
+        }
+    }
+
+    /**
+     * 按指定数量并发执行相同任务
+     *
+     * @param $count
+     * @param $taskFunc
+     * @param int $destroy
+     */
+    static public function execute($count, $taskFunc, $destroy = 100) {
+        $runSum  = 0;
+        $runNum  = 0;
+        $signal  = FALSE;
+        if (function_exists('pcntl_async_signals')) {
+            pcntl_async_signals(TRUE);
+        }
+        while (TRUE) {
+            if ($runNum >= $count) {
+                if (pcntl_wait($status, WNOHANG) > 0) {
+                    $runNum --;
+                } else {
+                    sleep(1);
+                    continue;
+                }
+            }
+
+            $pid = pcntl_fork();
+            if ($pid == -1) {
+                exit('Could not fork.');
+            } elseif ($pid > 0) {
+                $runSum ++;
+                $runNum ++;
+                if (!$signal) {
+                    $signal = TRUE;
+                    static::signalHandler();
+                }
+            } else {
+                for ($i = 0; $i < $destroy; $i ++) {
+                    call_user_func_array($taskFunc, [$runSum % $count]);
+                }
+
+                exit;
+            }
+        }
+    }
+
+    /**
+     * 设置信息处理器
+     *
+     */
+    static public function signalHandler() {
+        $handler = function () {
+            posix_kill(0, SIGKILL);
+            exit(0);
+        };
+        pcntl_signal(SIGINT, $handler);
+        pcntl_signal(SIGTERM, $handler);
+        pcntl_signal(SIGUSR1, $handler);
+        pcntl_signal(SIGUSR2, $handler);
+    }
+}
\ No newline at end of file
diff --git a/application/common/Logger.php b/application/common/Logger.php
new file mode 100644
index 0000000..57b9877
--- /dev/null
+++ b/application/common/Logger.php
@@ -0,0 +1,90 @@
+name = $name;
+    }
+
+    /**
+     * 写入信息
+     *
+     * @param $message
+     * @param array $params
+     */
+    public function info($message, array $params = []) {
+        $this->write(static::INFO, $message, $params);
+    }
+
+    /**
+     * 写入警告
+     *
+     * @param $message
+     * @param array $params
+     */
+    public function warn($message, array $params = []) {
+        $this->write(static::WARN, $message, $params);
+    }
+
+    /**
+     * 写入错误
+     *
+     * @param $message
+     * @param array $params
+     */
+    public function error($message, array $params = []) {
+        $this->write(static::ERROR, $message, $params);
+    }
+
+    /**
+     * 写入日志
+     *
+     * @param $type
+     * @param $message
+     * @param array $params
+     */
+    public function write($type, $message, array $params = []) {
+        foreach ($params as $key => $value) {
+            $message = str_replace('{' . $key . '}', $value, $message);
+        }
+
+        $data = '[' . date('Y-m-d H:i:s') . '][' . $type . '] ' . $message . PHP_EOL;
+        $file = ROOT_PATH . DS . 'runtime' . DS . $this->name . '_' . date('Ymd') . '.txt';
+
+        file_put_contents($file, $data, FILE_APPEND);
+
+        if ($this->print) {
+            echo '[' . date('Y-m-d H:i:s') . '][' . $this->name . '][' . $type . '] ' . $message . PHP_EOL;;
+        }
+    }
+}
\ No newline at end of file
diff --git a/application/common/Server.rar b/application/common/Server.rar
new file mode 100644
index 0000000..d21f54d
Binary files /dev/null and b/application/common/Server.rar differ
diff --git a/application/common/TaskServer.php b/application/common/TaskServer.php
new file mode 100644
index 0000000..50d2d63
--- /dev/null
+++ b/application/common/TaskServer.php
@@ -0,0 +1,93 @@
+ self::PROCESS_COUNT,
+        'name' => 'ckb_task_server'
+    ];
+
+    /**
+     * 当客户端的连接上发生错误时触发
+     * @param $connection
+     * @param $code
+     * @param $msg
+     */
+    public function onError($connection, $code, $msg)
+    {
+        Log::record("error $code $msg");
+    }
+
+    public function onMessage($connection, $data)
+    {
+    }
+
+    public function onClose($connection)
+    {
+    }
+
+    public function onConnect($connection)
+    {
+    }
+
+    public function onWorkerStart($worker)
+    {
+
+        $current_worker_id = $worker->id;
+
+        $process_count_for_status_0 = self::PROCESS_COUNT - 1;
+
+        $adapter = new ChuKeBaoAdapter();
+
+
+        Log::info('Workerman进程:' . $current_worker_id);
+
+
+        // 在一个进程里处理获客任务新是数据
+        if ($current_worker_id == 4) {
+            Timer::add(60, function () use ($adapter) {
+                $adapter->handleCustomerTaskNewUser();
+            });
+        }
+
+
+        // 在一个进程里处理获客任务添加后的相关逻辑
+        if ($current_worker_id == 3) {
+            Timer::add(60, function () use ($adapter) {
+                $adapter->handleCustomerTaskWithStatusIsCreated();
+            });
+        }
+
+        // 进程处理获客新任务
+        if ($current_worker_id  == 2) {
+            Timer::add(1, function () use ($current_worker_id, $process_count_for_status_0, $adapter) {
+                $adapter->handleCustomerTaskWithStatusIsNew($current_worker_id, $process_count_for_status_0);
+            });
+        }
+
+        // 在一个进程里处理自动问候任务
+        if ($current_worker_id == 1) {
+            // 每60秒检查一次自动问候规则
+            Timer::add(60, function () use ($adapter) {
+                //$adapter->handleAutoGreetings();
+            });
+        }
+
+        // 更多其他后台任务
+        // ......
+
+    }
+}
diff --git a/application/common/Utils.php b/application/common/Utils.php
new file mode 100644
index 0000000..c222f05
--- /dev/null
+++ b/application/common/Utils.php
@@ -0,0 +1,36 @@
+ 0 OR $number2 > 0) {
+             $number1 = $total * ($number1 / ($number1 + $number2));
+             $number2 = $total - $number1;
+         }
+     }
+}
\ No newline at end of file
diff --git a/application/common/Video.php b/application/common/Video.php
new file mode 100644
index 0000000..561bad6
--- /dev/null
+++ b/application/common/Video.php
@@ -0,0 +1,101 @@
+ $image) {
+            if (!file_exists($image)) {
+                throw new \Exception('图片不存在:' . $image);
+            }
+            $tempImage = $tempImagesDir . DS . sprintf('%04d.jpg', $index);
+            copy($image, $tempImage);
+            $tempImages[] = $tempImage;
+        }
+
+        // 构建 FFmpeg 命令
+        $filterComplex = [];
+        
+        // 处理所有输入流的格式和缩放
+        foreach ($tempImages as $i => $image) {
+            $filterComplex[] = sprintf('[%d:v]format=yuv420p,scale=%d:%d:force_original_aspect_ratio=decrease,pad=%d:%d:(ow-iw)/2:(oh-ih)/2[v%d]', 
+                $i, $width, $height, $width, $height, $i);
+        }
+
+        // 构建转场效果链
+        $lastOutput = 'v0';
+        for ($i = 1; $i < count($tempImages); $i++) {
+            $filterComplex[] = sprintf('[%s][v%d]xfade=transition=fade:duration=1:offset=%d[fade%d]', 
+                $lastOutput, $i, ($i * 3) - 1, $i);
+            $lastOutput = sprintf('fade%d', $i);
+        }
+
+        $inputFiles = '';
+        foreach ($tempImages as $image) {
+            $inputFiles .= sprintf('-loop 1 -t %d -i "%s" ', count($tempImages) * 3, $image);
+        }
+
+        $duration = count($tempImages) * 3 - (count($tempImages) - 1); // 总时长计算
+        
+        $cmd = sprintf(
+            'ffmpeg5 %s -filter_complex "%s" -map "[%s]" -t %d -c:v libx264 -pix_fmt yuv420p -r %d "%s" 2>&1',
+            $inputFiles,
+            implode(';', $filterComplex),
+            $lastOutput,
+            $duration,
+            $frameRate,
+            $outputFile
+        );
+
+        // 执行命令
+        exec($cmd, $output, $returnCode);
+
+        // 清理临时文件
+        array_map('unlink', glob($tempImagesDir . DS . '*'));
+        rmdir($tempImagesDir);
+        
+        if ($returnCode !== 0) {
+            throw new \Exception('视频生成失败:' . implode("\n", $output));
+        }
+
+        return $outputFile;
+    }
+}
\ No newline at end of file
diff --git a/application/common/config/route.php b/application/common/config/route.php
new file mode 100644
index 0000000..54001bd
--- /dev/null
+++ b/application/common/config/route.php
@@ -0,0 +1,33 @@
+middleware(['jwt']); // 获取用户信息
+    Route::post('refresh', 'app\common\controller\Auth@refresh')->middleware(['jwt']); // 刷新令牌
+});
+
+// 附件上传相关路由
+Route::group('v1/', function () {
+    Route::post('attachment/upload', 'app\common\controller\Attachment@upload');  // 上传附件
+    Route::get('attachment/:id', 'app\common\controller\Attachment@info');        // 获取附件信息
+})->middleware(['jwt']);
+
+
+
+Route::group('v1/pay', function () {
+    Route::post('', 'app\cunkebao\controller\Pay@createOrder')->middleware(['jwt']); 
+    Route::any('notify', 'app\common\controller\PaymentService@notify');
+});
+
+
+
+
+Route::get('v1/app/update', 'app\common\controller\Api@uploadApp'); //检测app是否需要更新
\ No newline at end of file
diff --git a/application/common/controller/Api.php b/application/common/controller/Api.php
new file mode 100644
index 0000000..459e8ee
--- /dev/null
+++ b/application/common/controller/Api.php
@@ -0,0 +1,151 @@
+requestType = Request::method();
+
+        // 控制器初始化
+        $this->_initialize();
+
+        // 跨域请求检测
+        $this->_checkCors();
+    }
+
+    /**
+     * 初始化操作
+     */
+    protected function _initialize()
+    {
+        // 初始化操作
+    }
+
+    /**
+     * 跨域检测
+     * @deprecated 已由全局中间件 AllowCrossDomain 处理,此方法保留用于兼容
+     */
+    protected function _checkCors()
+    {
+        // 由全局中间件处理跨域,此处不再处理
+        if ($this->requestType === 'OPTIONS') {
+            Response::create()->send();
+            exit;
+        }
+    }
+
+    /**
+     * 操作成功返回的数据
+     * @param string $msg 提示信息
+     * @param mixed $data 返回的数据
+     * @param int $code 错误码,默认为1
+     * @param string $type 输出类型
+     * @param array $header 发送的header信息
+     */
+    protected function success($msg = '', $data = null, $code = 1, $type = null, array $header = [])
+    {
+        $this->result($msg, $data, $code, $type, $header);
+    }
+
+    /**
+     * 操作失败返回的数据
+     * @param string $msg 提示信息
+     * @param mixed $data 返回的数据
+     * @param int $code 错误码,默认为0
+     * @param string $type 输出类型
+     * @param array $header 发送的header信息
+     */
+    protected function error($msg = '', $data = null, $code = 0, $type = null, array $header = [])
+    {
+        $this->result($msg, $data, $code, $type, $header);
+    }
+
+    /**
+     * 返回封装后的API数据
+     * @param string $msg 提示信息
+     * @param mixed $data 要返回的数据
+     * @param int $code 错误码,默认为0
+     * @param string $type 输出类型,支持json/xml/jsonp
+     * @param array $header 发送的header信息
+     */
+    protected function result($msg, $data = null, $code = 0, $type = null, array $header = [])
+    {
+        $result = [
+            'code' => $code,
+            'msg'  => $msg,
+            'time' => time(),
+            'data' => $data,
+        ];
+        
+        // 返回数据格式
+        $type = $type ?: $this->responseType;
+        
+        // 发送响应
+        $response = Response::create($result, $type)->header($header);
+        $response->send();
+        exit;
+    }
+
+
+    public function uploadApp()
+    {
+        $type = $this->request->param('type', '');
+        if (empty($type)){
+            return ResponseHelper::error('参数缺失');
+        }
+
+        if (!in_array($type,['ckb','aiStore'])){
+            return ResponseHelper::error('参数错误');
+        }
+
+        $data = Db::name('app_version')
+            ->field('version,downloadUrl,updateContent,forceUpdate')
+            ->where(['type'=>$type])
+            ->order('id DESC')
+            ->find();
+        return ResponseHelper::success($data, '获取成功');
+
+    }
+} 
\ No newline at end of file
diff --git a/application/common/controller/Attachment.php b/application/common/controller/Attachment.php
new file mode 100644
index 0000000..b3ffabb
--- /dev/null
+++ b/application/common/controller/Attachment.php
@@ -0,0 +1,144 @@
+ 400,
+                    'msg' => '请选择要上传的文件'
+                ]);
+            }
+            
+            // 验证文件
+            $validate = \think\facade\Validate::rule([
+                'file' => [
+                    'fileSize' => 50485760, // 50MB
+                    'fileExt' => 'jpg,jpeg,png,gif,doc,docx,pdf,zip,rar,mp4,mp3,csv,xlsx,xls,ppt,pptx,txt',
+                ]
+            ]);
+            
+            if (!$validate->check(['file' => $file])) {
+                return json([
+                    'code' => 400,
+                    'msg' => $validate->getError()
+                ]);
+            }
+            
+            // 生成文件hash
+            $hashKey = md5_file($file->getRealPath());
+            
+            // 检查文件是否已存在
+            $existFile = AttachmentModel::getByHashKey($hashKey);
+            if ($existFile) {
+                return json([
+                    'code' => 200,
+                    'msg' => '文件已存在',
+                    'data' => [
+                        'id' => $existFile['id'],
+                        'name' => $existFile['name'],
+                        'url' => $existFile['source'],
+                        'size' => isset($existFile['size']) ? $existFile['size'] : 0
+                    ]
+                ]);
+            }
+
+       
+            
+            // 生成OSS对象名称
+            $objectName = AliyunOSS::generateObjectName($file->getInfo('name'));
+            
+            // 上传到OSS
+            $result = AliyunOSS::uploadFile($file->getRealPath(), $objectName);
+            
+    
+            if (!$result['success']) {
+                return json([
+                    'code' => 500,
+                    'msg' => '文件上传失败:' . $result['error']
+                ]);
+            }
+            
+            // 保存到数据库
+            $attachmentData = [
+                'name' => Request::param('name') ?: $file->getInfo('name'),
+                'hash_key' => $hashKey,
+                'server' => 'aliyun_oss',
+                'source' => $result['url'],
+                'size' => $result['size'],
+                'suffix' => pathinfo($file->getInfo('name'), PATHINFO_EXTENSION)
+            ];
+            
+            $attachment = AttachmentModel::addAttachment($attachmentData);
+            
+            if (!$attachment) {
+                return json([
+                    'code' => 500,
+                    'msg' => '保存附件信息失败'
+                ]);
+            }
+            
+            return json([
+                'code' => 200,
+                'msg' => '上传成功',
+                'data' => [
+                    'id' => $attachment->id,
+                    'name' => $attachmentData['name'],
+                    'url' => $attachmentData['source'],
+                    'size' => $attachmentData['size']
+                ]
+            ]);
+            
+        } catch (\Exception $e) {
+            return json([
+                'code' => 500,
+                'msg' => '上传失败:' . $e->getMessage()
+            ]);
+        }
+    }
+    
+    /**
+     * 获取附件信息
+     * @param int $id 附件ID
+     * @return \think\response\Json
+     */
+    public function info($id)
+    {
+        try {
+            $attachment = AttachmentModel::find($id);
+            
+            if (!$attachment) {
+                return json([
+                    'code' => 404,
+                    'msg' => '附件不存在'
+                ]);
+            }
+            
+            return json([
+                'code' => 200,
+                'msg' => '获取成功',
+                'data' => $attachment
+            ]);
+            
+        } catch (\Exception $e) {
+            return json([
+                'code' => 500,
+                'msg' => '获取失败:' . $e->getMessage()
+            ]);
+        }
+    }
+} 
\ No newline at end of file
diff --git a/application/common/controller/Auth.php b/application/common/controller/Auth.php
new file mode 100644
index 0000000..73f7d84
--- /dev/null
+++ b/application/common/controller/Auth.php
@@ -0,0 +1,157 @@
+authService = new AuthService();
+    }
+    
+    /**
+     * 用户登录
+     * @return \think\response\Json
+     */
+    public function login()
+    {
+        // 获取登录参数
+        $params = Request::only(['account', 'password', 'typeId']);
+
+        // 参数验证
+        $validate = validate('common/Auth');
+        if (!$validate->scene('login')->check($params)) {
+            return ResponseHelper::error($validate->getError());
+        }
+        
+        try {
+            // 调用登录服务
+            $result = $this->authService->login(
+                $params['account'],
+                $params['password'],
+                $params['typeId'],
+                Request::ip()
+            );
+
+            return ResponseHelper::success($result, '登录成功');
+        } catch (\Exception $e) {
+            return ResponseHelper::error($e->getMessage());
+        }
+    }
+    
+    /**
+     * 手机号验证码登录
+     * @return \think\response\Json
+     */
+    public function mobileLogin()
+    {
+        // 获取登录参数
+        $params = Request::only(['account', 'code', 'typeId']);
+        
+        // 参数验证
+        $validate = validate('common/Auth');
+        if (!$validate->scene('mobile_login')->check($params)) {
+            return ResponseHelper::error($validate->getError());
+        }
+
+        try {
+            // 判断验证码是否已加密
+            $isEncrypted = isset($params['is_encrypted']) && $params['is_encrypted'] === true;
+            
+            // 调用手机号登录服务
+            $result = $this->authService->mobileLogin(
+                $params['account'],
+                $params['code'],
+                Request::ip(),
+                $isEncrypted
+            );
+            
+            return ResponseHelper::success($result, '登录成功');
+        } catch (\Exception $e) {
+            return ResponseHelper::error($e->getMessage());
+        }
+    }
+    
+    /**
+     * 发送验证码
+     * @return \think\response\Json
+     */
+    public function sendCode()
+    {
+        // 获取参数
+        $params = Request::only(['account', 'type']);
+        
+        // 参数验证
+        $validate = validate('common/Auth');
+        if (!$validate->scene('send_code')->check($params)) {
+            return ResponseHelper::error($validate->getError());
+        }
+        
+        try {
+            // 调用发送验证码服务
+            $result = $this->authService->sendLoginCode(
+                $params['account'],
+                $params['type']
+            );
+            return ResponseHelper::success($result, '验证码发送成功');
+        } catch (\Exception $e) {
+            return ResponseHelper::error($e->getMessage());
+        }
+    }
+    
+    /**
+     * 获取用户信息
+     * @return \think\response\Json
+     */
+    public function info()
+    {
+        try {
+            $result = $this->authService->getUserInfo(request()->userInfo);
+            return ResponseHelper::success($result);
+        } catch (\Exception $e) {
+            return ResponseHelper::unauthorized($e->getMessage());
+        }
+    }
+    
+    /**
+     * 刷新令牌
+     * @return \think\response\Json
+     */
+    public function refresh()
+    {
+        try {
+            $result = $this->authService->refreshToken(request()->userInfo);
+            return ResponseHelper::success($result, '刷新成功');
+        } catch (\Exception $e) {
+            return ResponseHelper::unauthorized($e->getMessage());
+        }
+    }
+} 
\ No newline at end of file
diff --git a/application/common/controller/BaseController.php b/application/common/controller/BaseController.php
new file mode 100644
index 0000000..81dcf83
--- /dev/null
+++ b/application/common/controller/BaseController.php
@@ -0,0 +1,12 @@
+ 需要在请求结束时清理的临时文件
+     */
+    protected static $tempFiles = [];
+
+    /**
+     * 导出 Excel(支持指定列插入图片)
+     *
+     * @param string $fileName      输出文件名(可不带扩展名)
+     * @param array  $headers       列定义,例如 ['name' => '姓名', 'phone' => '电话']
+     * @param array  $rows          数据行,需与 $headers 的 key 对应
+     * @param array  $imageColumns  需要渲染为图片的列 key 列表
+     * @param string $sheetName     工作表名称
+     * @param array  $options       额外选项:
+     *                               - imageWidth(图片宽度,默认100)
+     *                               - imageHeight(图片高度,默认100)
+     *                               - imageColumnWidth(图片列宽,默认15)
+     *                               - titleRow(标题行内容,支持多行文本数组)
+     *
+     * @throws Exception
+     */
+    public static function exportExcelWithImages(
+        $fileName,
+        array $headers,
+        array $rows,
+        array $imageColumns = [],
+        $sheetName = 'Sheet1',
+        array $options = []
+    ) {
+        if (empty($headers)) {
+            throw new Exception('导出列定义不能为空');
+        }
+        if (empty($rows)) {
+            throw new Exception('导出数据不能为空');
+        }
+
+        // 抑制 PHPExcel 库中已废弃的大括号语法警告(PHP 7.4+)
+        $oldErrorReporting = error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);
+
+        // 默认选项
+        $imageWidth = isset($options['imageWidth']) ? (int)$options['imageWidth'] : 100;
+        $imageHeight = isset($options['imageHeight']) ? (int)$options['imageHeight'] : 100;
+        $imageColumnWidth = isset($options['imageColumnWidth']) ? (float)$options['imageColumnWidth'] : 15;
+        $rowHeight = isset($options['rowHeight']) ? (int)$options['rowHeight'] : ($imageHeight + 10);
+
+        $excel = new PHPExcel();
+        $sheet = $excel->getActiveSheet();
+        $sheet->setTitle($sheetName);
+
+        $columnKeys = array_keys($headers);
+        $totalColumns = count($columnKeys);
+        $lastColumnLetter = self::columnLetter($totalColumns - 1);
+
+        // 定义特定列的固定宽度(如果未指定则使用默认值)
+        $columnWidths = isset($options['columnWidths']) ? $options['columnWidths'] : [];
+        
+        // 检查是否有标题行
+        $titleRow = isset($options['titleRow']) ? $options['titleRow'] : null;
+        $dataStartRow = 1; // 数据开始行(表头行)
+        
+        // 如果有标题行,先写入标题行(支持数组或字符串)
+        if (!empty($titleRow)) {
+            $dataStartRow = 2; // 数据从第2行开始(第1行是标题,第2行是表头)
+            
+            // 合并标题行单元格(从第一列到最后一列)
+            $titleRange = 'A1:' . $lastColumnLetter . '1';
+            $sheet->mergeCells($titleRange);
+            
+            // 构建标题内容(支持多行数组或字符串)
+            $titleContent = '';
+            if (is_array($titleRow)) {
+                $titleContent = implode("\n", $titleRow);
+            } else {
+                $titleContent = (string)$titleRow;
+            }
+            
+            // 写入标题
+            $sheet->setCellValue('A1', $titleContent);
+            
+            // 设置标题行样式
+            $sheet->getStyle('A1')->applyFromArray([
+                'font' => ['bold' => true, 'size' => 16],
+                'alignment' => [
+                    'horizontal' => \PHPExcel_Style_Alignment::HORIZONTAL_CENTER,
+                    'vertical' => \PHPExcel_Style_Alignment::VERTICAL_CENTER,
+                    'wrap' => true
+                ],
+                'fill' => [
+                    'type' => \PHPExcel_Style_Fill::FILL_SOLID,
+                    'color' => ['rgb' => 'FFF8DC'] // 浅黄色背景
+                ],
+                'borders' => [
+                    'allborders' => [
+                        'style' => \PHPExcel_Style_Border::BORDER_THIN,
+                        'color' => ['rgb' => '000000']
+                    ]
+                ]
+            ]);
+            $sheet->getRowDimension(1)->setRowHeight(80); // 标题行高度
+        }
+        
+        // 写入表头并设置列宽
+        $headerRow = $dataStartRow;
+        foreach ($columnKeys as $index => $key) {
+            $columnLetter = self::columnLetter($index);
+            $sheet->setCellValue($columnLetter . $headerRow, $headers[$key]);
+            
+            // 如果是图片列,设置固定列宽
+            if (in_array($key, $imageColumns, true)) {
+                $sheet->getColumnDimension($columnLetter)->setWidth($imageColumnWidth);
+            } elseif (isset($columnWidths[$key])) {
+                // 如果指定了该列的宽度,使用指定宽度
+                $sheet->getColumnDimension($columnLetter)->setWidth($columnWidths[$key]);
+            } else {
+                // 否则自动调整
+                $sheet->getColumnDimension($columnLetter)->setAutoSize(true);
+            }
+        }
+
+        // 设置表头样式
+        $headerRange = 'A' . $headerRow . ':' . $lastColumnLetter . $headerRow;
+        $sheet->getStyle($headerRange)->applyFromArray([
+            'font' => ['bold' => true, 'size' => 11],
+            'alignment' => [
+                'horizontal' => \PHPExcel_Style_Alignment::HORIZONTAL_CENTER, 
+                'vertical' => \PHPExcel_Style_Alignment::VERTICAL_CENTER,
+                'wrap' => true
+            ],
+            'fill' => [
+                'type' => \PHPExcel_Style_Fill::FILL_SOLID,
+                'color' => ['rgb' => 'FFF8DC']
+            ],
+            'borders' => [
+                'allborders' => [
+                    'style' => \PHPExcel_Style_Border::BORDER_THIN,
+                    'color' => ['rgb' => '000000']
+                ]
+            ]
+        ]);
+        $sheet->getRowDimension($headerRow)->setRowHeight(30); // 增加表头行高以确保文本完整显示
+
+        // 写入数据与图片
+        $dataRowStart = $dataStartRow + 1; // 数据从表头行下一行开始
+        foreach ($rows as $rowIndex => $rowData) {
+            $excelRow = $dataRowStart + $rowIndex; // 数据行
+            $maxRowHeight = $rowHeight; // 记录当前行的最大高度
+            
+            foreach ($columnKeys as $colIndex => $key) {
+                $columnLetter = self::columnLetter($colIndex);
+                $cell = $columnLetter . $excelRow;
+                $value = isset($rowData[$key]) ? $rowData[$key] : '';
+
+                if (in_array($key, $imageColumns, true) && !empty($value)) {
+                    $imagePath = self::resolveImagePath($value);
+                    if ($imagePath) {
+                        // 获取图片实际尺寸并等比例缩放
+                        $imageSize = @getimagesize($imagePath);
+                        if ($imageSize) {
+                            $originalWidth = $imageSize[0];
+                            $originalHeight = $imageSize[1];
+                            
+                            // 计算等比例缩放后的尺寸
+                            $ratio = min($imageWidth / $originalWidth, $imageHeight / $originalHeight);
+                            $scaledWidth = $originalWidth * $ratio;
+                            $scaledHeight = $originalHeight * $ratio;
+                            
+                            // 确保不超过最大尺寸
+                            if ($scaledWidth > $imageWidth) {
+                                $scaledWidth = $imageWidth;
+                                $scaledHeight = $originalHeight * ($imageWidth / $originalWidth);
+                            }
+                            if ($scaledHeight > $imageHeight) {
+                                $scaledHeight = $imageHeight;
+                                $scaledWidth = $originalWidth * ($imageHeight / $originalHeight);
+                            }
+                            
+                            $drawing = new PHPExcel_Worksheet_Drawing();
+                            $drawing->setPath($imagePath);
+                            $drawing->setCoordinates($cell);
+                            
+                            // 居中显示图片(Excel列宽1单位≈7像素,行高1单位≈0.75像素)
+                            $cellWidthPx = $imageColumnWidth * 7;
+                            $cellHeightPx = $maxRowHeight * 0.75;
+                            $offsetX = max(2, ($cellWidthPx - $scaledWidth) / 2);
+                            $offsetY = max(2, ($cellHeightPx - $scaledHeight) / 2);
+                            
+                            $drawing->setOffsetX((int)$offsetX);
+                            $drawing->setOffsetY((int)$offsetY);
+                            $drawing->setWidth((int)$scaledWidth);
+                            $drawing->setHeight((int)$scaledHeight);
+                            $drawing->setWorksheet($sheet);
+                            
+                            // 更新行高以适应图片(留出一些边距)
+                            $neededHeight = (int)($scaledHeight / 0.75) + 10;
+                            if ($neededHeight > $maxRowHeight) {
+                                $maxRowHeight = $neededHeight;
+                            }
+                        } else {
+                            // 如果无法获取图片尺寸,使用默认尺寸
+                            $drawing = new PHPExcel_Worksheet_Drawing();
+                            $drawing->setPath($imagePath);
+                            $drawing->setCoordinates($cell);
+                            $drawing->setOffsetX(5);
+                            $drawing->setOffsetY(5);
+                            $drawing->setWidth($imageWidth);
+                            $drawing->setHeight($imageHeight);
+                            $drawing->setWorksheet($sheet);
+                        }
+                    } else {
+                        $sheet->setCellValue($cell, '');
+                    }
+                } else {
+                    $sheet->setCellValue($cell, $value);
+                    // 设置文本对齐和换行
+                    $style = $sheet->getStyle($cell);
+                    $style->getAlignment()->setVertical(\PHPExcel_Style_Alignment::VERTICAL_CENTER);
+                    $style->getAlignment()->setWrapText(true);
+                    // 根据列类型设置水平对齐
+                    if (in_array($key, ['date', 'postTime'])) {
+                        $style->getAlignment()->setHorizontal(\PHPExcel_Style_Alignment::HORIZONTAL_CENTER);
+                    } else {
+                        $style->getAlignment()->setHorizontal(\PHPExcel_Style_Alignment::HORIZONTAL_LEFT);
+                    }
+                }
+            }
+            
+            // 设置行高
+            $sheet->getRowDimension($excelRow)->setRowHeight($maxRowHeight);
+        }
+
+        $safeName = preg_replace('/[^\w\-]/', '_', $fileName ?: 'export_' . date('Ymd_His'));
+        if (stripos($safeName, '.xlsx') === false) {
+            $safeName .= '.xlsx';
+        }
+
+        if (ob_get_length()) {
+            ob_end_clean();
+        }
+
+        header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
+        header('Cache-Control: max-age=0');
+        header('Content-Disposition: attachment;filename="' . $safeName . '"');
+
+        try {
+            $writer = PHPExcel_IOFactory::createWriter($excel, 'Excel2007');
+            $writer->save('php://output');
+        } catch (\Exception $e) {
+            // 恢复错误报告级别
+            error_reporting($oldErrorReporting);
+            throw $e;
+        }
+        
+        // 恢复错误报告级别
+        error_reporting($oldErrorReporting);
+        self::cleanupTempFiles();
+        exit;
+    }
+
+    /**
+     * 根据列序号生成 Excel 列字母
+     *
+     * @param int $index
+     * @return string
+     */
+    protected static function columnLetter($index)
+    {
+        $letters = '';
+        do {
+            $letters = chr($index % 26 + 65) . $letters;
+            $index = intval($index / 26) - 1;
+        } while ($index >= 0);
+
+        return $letters;
+    }
+
+    /**
+     * 将远程或本地图片路径转换为可用的本地文件路径
+     *
+     * @param string $path
+     * @return string|null
+     */
+    protected static function resolveImagePath($path)
+    {
+        if (empty($path)) {
+            return null;
+        }
+
+        if (preg_match('/^https?:\/\//i', $path)) {
+            $tempFile = tempnam(sys_get_temp_dir(), 'export_img_');
+            $stream = @file_get_contents($path);
+            if ($stream === false) {
+                return null;
+            }
+            file_put_contents($tempFile, $stream);
+            self::$tempFiles[] = $tempFile;
+            return $tempFile;
+        }
+
+        if (file_exists($path)) {
+            return $path;
+        }
+
+        return null;
+    }
+
+    /**
+     * 清理所有临时文件
+     */
+    protected static function cleanupTempFiles()
+    {
+        foreach (self::$tempFiles as $file) {
+            if (file_exists($file)) {
+                @unlink($file);
+            }
+        }
+        self::$tempFiles = [];
+    }
+}
\ No newline at end of file
diff --git a/application/common/controller/GetOpenid.php b/application/common/controller/GetOpenid.php
new file mode 100644
index 0000000..b33c81d
--- /dev/null
+++ b/application/common/controller/GetOpenid.php
@@ -0,0 +1,52 @@
+  Env::get('weChat.appid'),
+            'secret' =>  Env::get('weChat.secret'),
+            'response_type' => 'array'
+        ];
+        $this->app = Factory::officialAccount($config);
+    }
+
+
+
+    public function index()
+    {
+        $app = $this->app;
+        $oauth = $app->oauth;
+
+        // 未登录
+        if (empty($_SESSION['wechat_user'])) {
+
+            $_SESSION['target_url'] = 'user/profile';
+
+            $redirectUrl = $oauth->redirect();
+
+            exit_data($redirectUrl);
+            header("Location: {$redirectUrl}");
+            exit;
+        }
+
+        // 已经登录过
+        $user = $_SESSION['wechat_user'];
+
+        exit_data($user);
+
+        return 'Hello, World!';
+    }
+
+}
\ No newline at end of file
diff --git a/application/common/controller/PasswordLoginController.php b/application/common/controller/PasswordLoginController.php
new file mode 100644
index 0000000..232e4e6
--- /dev/null
+++ b/application/common/controller/PasswordLoginController.php
@@ -0,0 +1,157 @@
+where('phone', $account)->whereOr('account', $account);
+            }
+        )
+            ->where(
+                function ($query) use ($typeId) {
+                    $query->where('status', 1)->where('typeId', $typeId);
+                }
+            )->find();
+
+       if(!empty($user)){
+           return $user;
+       }else{
+           return '';
+       }
+    }
+
+    /**
+     * 获取用户信息
+     *
+     * @param string $account 账号(手机号)
+     * @param string $password 密码(可能是加密后的)
+     * @param int $typeId 身份信息
+     * @return array|null
+     */
+    protected function getUser(string $account, string $password, int $typeId): array
+    {
+        $user = $this->getUserProfileWithAccountAndType($account, $typeId);
+        if (!$user) {
+            throw new \Exception('用户不存在或已禁用', 403);
+        }
+
+        $password = md5($password);
+        if ($user->passwordMd5 !== $password) {
+            throw new \Exception('账号或密码错误', 403);
+        }
+
+        return array_merge($user->toArray(), [
+            'lastLoginIp' => $this->request->ip(),
+            'lastLoginTime' => time()
+        ]);
+    }
+
+    /**
+     * 数据验证
+     *
+     * @param array $params
+     * @return $this
+     * @throws \Exception
+     */
+    protected function dataValidate(array $params): self
+    {
+        $validate = Validate::make([
+            'account' => 'require',
+            'password' => 'require|length:6,64',
+            'typeId' => 'require|in:1,2',
+        ], [
+            'account.require' => '账号不能为空',
+            'password.require' => '密码不能为空',
+            'password.length' => '密码长度必须在6-64个字符之间',
+            'typeId.require' => '用户类型不能为空',
+            'typeId.in' => '用户类型错误',
+        ]);
+
+        if (!$validate->check($params)) {
+            throw new \Exception($validate->getError(), 400);
+        }
+
+        return $this;
+    }
+
+    /**
+     * 用户登录
+     *
+     * @param string $account 账号(手机号)
+     * @param string $password 密码(可能是加密后的)
+     * @param string $typeId 登录IP
+     * @param string $deviceId 本地设备imei
+     * @return array
+     * @throws \Exception
+     */
+    protected function doLogin(string $account, string $password, int $typeId, string $deviceId): array
+    {
+        // 获取用户信息
+        $member = $this->getUser($account, $password, $typeId);
+        $deviceTotal = Db::name('device')->where(['companyId' => $member['companyId'],'deleteTime' => 0])->count();
+
+        //更新设备imei
+        if ($typeId == 2 && !empty($deviceId)){
+            $deviceUser = Db::name('device_user')->where(['companyId' => $member['companyId'],'userId' => $member['id'],'deleteTime' => 0])->find();
+            if (!empty($deviceUser)){
+                $device = Db::name('device')->where(['companyId' => $member['companyId'],'deleteTime' => 0,'id' => $deviceUser['deviceId']])->find();
+                if (!empty($device) && empty($device['deviceImei'])){
+                    Db::table('s2_device')->where(['id' => $device['id']])->update(['deviceImei' => $deviceId,'updateTime' => time()]);
+                    Db::name('device')->where(['id' => $device['id']])->update(['deviceImei' => $deviceId,'updateTime' => time()]);
+                }
+            }
+        }
+
+
+        // 生成JWT令牌
+        $token = JwtUtil::createToken($member, 86400 * 30);
+        $token_expired = time() + 86400 * 30;
+        return compact('member', 'token', 'token_expired','deviceTotal');
+    }
+
+    /**
+     * 用户登录
+     *
+     * @return \think\response\Json
+     */
+    public function index()
+    {
+        $params = $this->request->only(['account', 'password', 'typeId','deviceId']);
+        try {
+            $deviceId = isset($params['deviceId']) ? $params['deviceId'] : '';
+            $userData = $this->dataValidate($params)->doLogin(
+                $params['account'],
+                $params['password'],
+                $params['typeId'],
+                $deviceId
+            );
+
+
+            return ResponseHelper::success($userData, '登录成功');
+        } catch (Exception $e) {
+            return ResponseHelper::error($e->getMessage(), $e->getCode());
+        }
+    }
+} 
\ No newline at end of file
diff --git a/application/common/controller/PaymentService.php b/application/common/controller/PaymentService.php
new file mode 100644
index 0000000..c8a030e
--- /dev/null
+++ b/application/common/controller/PaymentService.php
@@ -0,0 +1,543 @@
+ $service,
+            'sign_type' => PaymentUtil::SIGN_TYPE_MD5,
+            'mch_id' => Env::get('payment.mchId'),
+            'out_trade_no' => $order['orderNo'],
+            'body' => $order['goodsName'] ?? '',
+            'total_fee' => $order['money'] ?? 0,
+            'mch_create_ip' => Request::ip(),
+            'notify_url' => $order['notify_url'] ?? Env::get('payment.notify_url', '127.0.0.1'),
+            'nonce_str' => PaymentUtil::generateNonceStr(),
+        ];
+
+         // 微信JSAPI支付需要openid
+         if ($service == 'pay.weixin.jspay') {
+            // $params['sub_openid'] = 'oB44Yw1T6bfVAZwjj729P-6CUSPE';
+             $params['is_raw'] = 0;
+             $params['mch_app_name'] = '存客宝';
+             $params['mch_app_id'] = 'https://kr-op.quwanzhi.com';
+         }
+
+         // 支付宝JSAPI支付需要buyer_id(可选)
+         if ($service == 'pay.alipay.jspay') {
+             $params['is_raw'] = 0;
+             $params['quit_url'] = $params['notify_url'];
+             $params['buyer_id'] = '';
+         }
+
+        Db::startTrans();
+        try {
+            // 签名
+            $secret = Env::get('payment.key');
+            $params['sign_type'] = 'MD5';
+            $params['sign'] = PaymentUtil::generateSign($params, $secret, 'MD5');
+
+            $url = Env::get('payment.url');
+            if (empty($url)) {
+                throw new \Exception('支付网关地址未配置');
+            }
+
+            // 创建订单
+            Order::create([
+                'mchId' => $params['mch_id'],
+                'companyId' => isset($order['companyId']) ? $order['companyId'] : 0,
+                'userId' => isset($order['userId']) ? $order['userId'] : 0,
+                'orderType' => isset($order['orderType']) ? $order['orderType'] : 1,
+                'status' => 0,
+                'goodsId' => isset($order['goodsId']) ? $order['goodsId'] : 0,
+                'goodsName' => isset($order['goodsName']) ? $order['goodsName'] : '',
+                'money' => isset($order['money']) ? $order['money'] : 0,
+                'goodsSpecs' => isset($order['goodsSpecs']) ? json_encode($order['goodsSpecs'], 256) : json_encode([]),
+                'orderNo' => isset($order['orderNo']) ? $order['orderNo'] : '',
+                'ip' => Request::ip(),
+                'nonceStr' => isset($order['nonceStr']) ? $order['nonceStr'] : '',
+                'createTime' => time(),
+            ]);
+
+            // XML POST 请求
+            $xmlBody = $this->arrayToXml($params);
+            $response = $this->postXml($url, $xmlBody);
+            $parsed = $this->parseXmlOrRaw($response);
+
+
+            if ($parsed['status'] == 0 && $parsed['result_code'] == 0) {
+                Db::commit();
+
+                // 根据service类型返回不同的数据格式(仅返回接口文档中的字段)
+                $responseData = null;
+                if ($service == 'unified.trade.native') {
+                    // 扫码支付返回二维码URL
+                    $responseData = $parsed['code_img_url'] ?? '';
+                } elseif ($service == 'pay.weixin.jspay') {
+                    // 微信JSAPI支付返回支付参数(仅返回接口文档中存在的字段)
+                    $responseData = [];
+                    if (isset($parsed['appid'])) $responseData['appid'] = $parsed['appid'];
+                    if (isset($parsed['time_stamp'])) $responseData['time_stamp'] = $parsed['time_stamp'];
+                    if (isset($parsed['nonce_str'])) $responseData['nonce_str'] = $parsed['nonce_str'];
+                    if (isset($parsed['package'])) $responseData['package'] = $parsed['package'];
+                    if (isset($parsed['sign_type'])) $responseData['sign_type'] = $parsed['sign_type'];
+                    if (isset($parsed['pay_sign'])) $responseData['pay_sign'] = $parsed['pay_sign'];
+                } elseif ($service == 'pay.alipay.jspay') {
+                    // 支付宝JSAPI支付返回订单信息(仅返回接口文档中存在的字段)
+                    $responseData = [];
+                    if (isset($parsed['order_info'])) $responseData['order_info'] = $parsed['order_info'];
+                    if (isset($parsed['order_string'])) $responseData['order_string'] = $parsed['order_string'];
+                }
+
+                return json_encode(['code' => 200, 'msg' => '订单创建成功', 'data' => $responseData]);
+            } else {
+                Db::rollback();
+                return json_encode(['code' => 500, 'msg' => '订单创建失败:' . ($parsed['err_msg'] ?? '未知错误')]);
+            }
+
+        } catch (\Exception $e) {
+            Db::rollback();
+            return json_encode(['code' => 500, 'msg' => '订单创建失败:' . $e->getMessage()]);
+        }
+    }
+
+
+    /**
+     * POST 请求(x-www-form-urlencoded)
+     */
+    protected function httpPost(string $url, array $params, array $headers = [])
+    {
+        if (!function_exists('requestCurl')) {
+            throw new \RuntimeException('requestCurl 未定义');
+        }
+        return requestCurl($url, $params, 'POST', $headers, 'dataBuild');
+    }
+
+    /**
+     * 解析响应
+     */
+    protected function parseResponse($response)
+    {
+        if ($response === '' || $response === null) {
+            return '';
+        }
+        $decoded = json_decode($response, true);
+        if (json_last_error() === JSON_ERROR_NONE) {
+            return $decoded;
+        }
+        if (strpos($response, '=') !== false && strpos($response, '&') !== false) {
+            $arr = [];
+            foreach (explode('&', $response) as $pair) {
+                if ($pair === '') continue;
+                $kv = explode('=', $pair, 2);
+                $arr[$kv[0]] = $kv[1] ?? '';
+            }
+            return $arr;
+        }
+        return $response;
+    }
+
+    /**
+     * 以 XML 方式 POST(text/xml)
+     */
+    protected function postXml(string $url, string $xml)
+    {
+        $ch = curl_init();
+        curl_setopt($ch, CURLOPT_URL, $url);
+        curl_setopt($ch, CURLOPT_POST, 1);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
+        curl_setopt($ch, CURLOPT_HTTPHEADER, [
+            'Content-Type: text/xml; charset=UTF-8'
+        ]);
+        curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
+        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
+        $res = curl_exec($ch);
+        curl_close($ch);
+        return $res;
+    }
+
+    /**
+     * 数组转 XML(按 ASCII 升序,字符串走 CDATA)
+     */
+    protected function arrayToXml(array $data): string
+    {
+        // 过滤空值
+        $filtered = [];
+        foreach ($data as $k => $v) {
+            if ($v === '' || $v === null) continue;
+            $filtered[$k] = $v;
+        }
+        ksort($filtered, SORT_STRING);
+
+        $xml = '';
+        foreach ($filtered as $key => $value) {
+            if (is_numeric($value)) {
+                $xml .= "<{$key}>{$value}";
+            } else {
+                $xml .= "<{$key}>";
+            }
+        }
+        $xml .= '';
+        return $xml;
+    }
+
+    /**
+     * 解析 XML 响应
+     */
+    protected function parseXmlOrRaw($response)
+    {
+        if (!is_string($response) || $response === '') {
+            return $response;
+        }
+        libxml_use_internal_errors(true);
+        $xml = simplexml_load_string($response, 'SimpleXMLElement', LIBXML_NOCDATA);
+        if ($xml !== false) {
+            $json = json_encode($xml, JSON_UNESCAPED_UNICODE);
+            return json_decode($json, true);
+        }
+        return $response;
+    }
+
+    /**
+     * 支付结果异步通知
+     * - 威富通回调为 XML;需校验签名与业务字段并更新订单
+     * - 支持扫码付款、微信支付、支付宝支付的通知
+     * - 回应:成功返回XML格式SUCCESS,失败返回XML格式FAIL
+     * @return string XML响应
+     */
+    public function notify()
+    {
+        $rawBody = file_get_contents('php://input');
+        $payload = $this->parseXmlOrRaw($rawBody);
+        if (!is_array($payload) || empty($payload)) {
+            \think\facade\Log::error('支付通知:XML解析错误', ['rawBody' => $rawBody]);
+            return '';
+        }
+
+        // 验证签名
+        $secret = Env::get('payment.key');
+        if (!empty($secret) && isset($payload['sign'])) {
+            $signType = $payload['sign_type'] ?? 'MD5';
+            if (!PaymentUtil::verifySign($payload, $secret, $signType)) {
+                \think\facade\Log::error('支付通知:签名验证失败', ['payload' => $payload]);
+                return '';
+            }
+        }
+
+        // 检查通信状态
+        if (isset($payload['status']) && $payload['status'] != 0) {
+            $errMsg = $payload['err_msg'] ?? '通信失败';
+            \think\facade\Log::error('支付通知:通信失败', ['payload' => $payload]);
+            return '';
+        }
+
+        // 检查业务结果
+        if (isset($payload['result_code']) && $payload['result_code'] != 0) {
+            $errMsg = $payload['err_msg'] ?? '业务处理失败';
+            \think\facade\Log::error('支付通知:业务处理失败', ['payload' => $payload]);
+            return '';
+        }
+
+
+        // 业务处理:更新订单
+        Db::startTrans();
+        try {
+            $outTradeNo = $payload['out_trade_no'] ?? '';
+            $pay_result = $payload['pay_result'] ?? 0;
+            $time_end = $payload['time_end'] ?? '';
+
+            if (empty($outTradeNo)) {
+                Db::rollback();
+                \think\facade\Log::error('支付通知:订单号为空', ['payload' => $payload]);
+                return '';
+            }
+
+            $order = Order::where('orderNo', $outTradeNo)->find();
+            if (!$order) {
+                Db::rollback();
+                \think\facade\Log::error('支付通知:订单不存在', ['out_trade_no' => $outTradeNo]);
+                return '';
+            }
+
+            // 如果订单已支付,直接返回成功(防止重复处理)
+            if ($order->status == 1) {
+                Db::rollback();
+                return '';
+            }
+
+            if ($pay_result != 0) {
+                $order->payInfo = $payload['pay_info'] ?? '支付失败';
+                $order->status = 3;
+                $order->save();
+                Db::commit();
+                \think\facade\Log::error('支付通知:支付失败', ['orderNo' => $outTradeNo, 'pay_info' => $payload['pay_info'] ?? '']);
+                return '';
+            }
+
+            // 根据trade_type判断支付方式
+            $tradeType = $payload['trade_type'] ?? '';
+            if (strpos($tradeType, 'wechat') !== false || strpos($tradeType, 'weixin') !== false) {
+                $order->payType = 1; // 微信支付
+            } elseif (strpos($tradeType, 'alipay') !== false) {
+                $order->payType = 2; // 支付宝支付
+            } else {
+                // 默认根据原有逻辑判断
+                $order->payType = $tradeType == 'pay.wechat.jspay' ? 1 : 2;
+            }
+
+            $order->status = 1;
+            $order->payTime = $this->parsePayTime($time_end);
+            $order->transactionId = $payload['transaction_id'] ?? '';
+            $order->save();
+            //订单处理
+            $this->processOrder($order);
+            Db::commit();
+
+            // 返回成功响应(XML格式)
+            return '';
+        } catch (\Exception $e) {
+            Db::rollback();
+            \think\facade\Log::error('支付通知:处理异常', ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
+            return '';
+        }
+    }
+
+    /**
+     * 解析威富通时间(yyyyMMddHHmmss)为时间戳
+     */
+    protected function parsePayTime(string $timeEnd)
+    {
+        if ($timeEnd === '') {
+            return 0;
+        }
+        // 期望格式:20250102153045
+        if (preg_match('/^\\d{14}$/', $timeEnd) !== 1) {
+            return 0;
+        }
+        $dt = \DateTime::createFromFormat('YmdHis', $timeEnd, new \DateTimeZone('Asia/Shanghai'));
+        return $dt ? $dt->getTimestamp() : 0;
+    }
+
+    /**
+     * 查询订单(威富通 unified.trade.query)
+     * - 入参:商户订单号或平台交易号
+     * - 出参:统一 JSON 格式,包含交易状态与关键信息
+     * @param array $query
+     *  - out_trade_no: string 商户订单号(与 transaction_id 二选一)
+     *  - transaction_id: string 平台交易号(与 out_trade_no 二选一)
+     * @return \think\response\Json
+     */
+    public function queryOrder($orderNo = '')
+    {
+        if (empty($orderNo)) {
+            return json(['code' => 422, 'msg' => '订单号缺失']);
+        }
+
+        $params = [
+            'service' => 'unified.trade.query',
+            'mch_id' => Env::get('payment.mchId'),
+            'out_trade_no' => $orderNo ?: null,
+            'nonce_str' => PaymentUtil::generateNonceStr(),
+            'sign_type' => 'MD5',
+        ];
+
+        // 过滤空值后签名
+        $secret = Env::get('payment.key');
+        if (empty($secret)) {
+            return json_encode(['code' => 500, 'msg' => '支付密钥未配置']);
+        }
+
+        $filtered = [];
+        foreach ($params as $k => $v) {
+            if ($v === '' || $v === null) continue;
+            $filtered[$k] = $v;
+        }
+        $filtered['sign'] = PaymentUtil::generateSign($filtered, $secret, $filtered['sign_type']);
+
+        $url = Env::get('payment.url');
+        if (empty($url)) {
+            return json_encode(['code' => 500, 'msg' => '支付网关地址未配置']);
+        }
+
+        // 请求网关
+        $xmlBody = $this->arrayToXml($filtered);
+        $response = $this->postXml($url, $xmlBody);
+        $parsed = $this->parseXmlOrRaw($response);
+
+        if (!is_array($parsed)) {
+            return json_encode(['code' => 500, 'msg' => '响应解析失败', 'data' => $response]);
+        }
+
+        if ($parsed['status'] != 0) {
+            return json_encode(['code' => 500, 'msg' => '通信失败']);
+        }
+
+
+        if ($parsed['result_code'] == 0) {
+            $order = Order::where('orderNo', $orderNo)->lock(true)->find();
+            if (empty($order)) {
+                return json_encode(['code' => 500, 'msg' => '订单不存在']);
+            }
+
+            if ($order['status'] == 1) {
+                return json_encode(['code' => 200, 'msg' => '支付成功']);
+            }
+
+
+            $tradeState = $parsed['trade_state'] ?? '';
+            $resp = [
+                'trade_state' => $tradeState,
+                'trade_state_desc' => $parsed['trade_state_desc'] ?? '',
+                'transaction_id' => $parsed['transaction_id'] ?? '',
+                'out_trade_no' => $parsed['out_trade_no'] ?? $orderNo,
+                'total_fee' => isset($parsed['total_fee']) ? (int)$parsed['total_fee'] : null,
+                'time_end' => $parsed['time_end'] ?? '',
+                'buyer_logon_id' => $parsed['buyer_logon_id'] ?? '',
+                'bank_type' => $parsed['bank_type'] ?? '',
+            ];
+
+            // 若已支付,同步本地订单
+            if ($tradeState == 'SUCCESS') {
+                Db::startTrans();
+                try {
+                    /** @var Order|null $order */
+                    $order = Order::where('orderNo', $resp['out_trade_no'])->lock(true)->find();
+                    if ($order) {
+                        $paidAt = $this->parsePayTime($resp['time_end'] ?? '') ?: time();
+                        if ($order['status'] != 1) {
+                            $order->save([
+                                'status' => 1,
+                                'transactionId' => $resp['transaction_id'] ?? '',
+                                'payTime' => $paidAt,
+                                'updateTime' => time(),
+                            ]);
+                        }
+                    }
+                    //订单处理
+                    $this->processOrder($order);
+                    Db::commit();
+                    return json_encode(['code' => 200, 'msg' => '支付成功']);
+                } catch (\Exception $e) {
+                    Db::rollback();
+                    return json_encode(['code' => 500, 'msg' => '付款失败' . $e->getMessage()]);
+                }
+            } else {
+                $order = Order::where('orderNo', $resp['out_trade_no'])->lock(true)->find();
+                if ($order) {
+                    $order->status = 3;
+                    $order->payInfo = $resp['trade_state_desc'] ?? '';
+                    $order->save();
+                }
+                return json_encode(['code' => 500, 'msg' => '支付失败', 'data' => $resp]);
+            }
+
+        } else {
+            return json_encode(['code' => 500, 'msg' => '通信失败']);
+        }
+    }
+
+
+    public function processOrder($order = [])
+    {
+        if (empty($order)) {
+            return false;
+        }
+
+        switch ($order['orderType']) {
+            case 1:
+                // 处理购买算力
+                // 查询用户信息,判断是否为管理员(需要同时匹配userId和companyId)
+                $user = User::where([
+                    'id' => $order->userId,
+                    'companyId' => $order->companyId
+                ])->find();
+                $isAdmin = (!empty($user) && isset($user->isAdmin) && $user->isAdmin == 1) ? 1 : 0;
+                
+                $token = TokensCompany::where(['companyId' => $order->companyId,'userId' => $order->userId])->find();
+                $goodsSpecs = json_decode($order->goodsSpecs, true);
+                if (!empty($token)) {
+                    $token->tokens = $token->tokens + $goodsSpecs['tokens'];
+                    $token->updateTime = time();
+                    $token->save();
+                    $newTokens = $token->tokens;
+                } else {
+                    $tokensCompany = new TokensCompany();
+                    $tokensCompany->userId = $order->userId;
+                    $tokensCompany->companyId = $order->companyId;
+                    $tokensCompany->tokens = $goodsSpecs['tokens'];
+                    $tokensCompany->isAdmin = $isAdmin;
+                    $tokensCompany->createTime = time();
+                    $tokensCompany->updateTime = time();
+                    $tokensCompany->save();
+                    $newTokens = $tokensCompany->tokens;
+                }
+                //添加记录
+                $record = new TokensRecord();
+                $record->companyId = $order->companyId;
+                $record->userId = $order->userId;
+                $record->type = 1;
+                $record->form = 5;
+                $record->wechatAccountId = 0;
+                $record->friendIdOrGroupId = 0;
+                $record->remarks = '购买算力【' . $goodsSpecs['name'] . '】';
+                $record->tokens = $goodsSpecs['tokens'];
+                $record->balanceTokens = $newTokens;
+                $record->createTime = time();
+                $record->save();
+                break;
+        }
+
+        return true;
+    }
+
+}
\ No newline at end of file
diff --git a/application/common/controller/SendCodeController.php b/application/common/controller/SendCodeController.php
new file mode 100644
index 0000000..7dc58d2
--- /dev/null
+++ b/application/common/controller/SendCodeController.php
@@ -0,0 +1,39 @@
+request->only(['account', 'type']);
+
+        // 参数验证
+        $validate = validate('common/Auth');
+        if (!$validate->scene('send_code')->check($params)) {
+            return ResponseHelper::error($validate->getError());
+        }
+
+        try {
+            // 调用发送验证码服务
+            $result = $this->authService->sendLoginCode(
+                $params['account'],
+                $params['type']
+            );
+            return ResponseHelper::success($result, '验证码发送成功');
+        } catch (\Exception $e) {
+            return ResponseHelper::error($e->getMessage());
+        }
+    }
+} 
\ No newline at end of file
diff --git a/application/common/middleware/AllowCrossDomain.php b/application/common/middleware/AllowCrossDomain.php
new file mode 100644
index 0000000..096ce7c
--- /dev/null
+++ b/application/common/middleware/AllowCrossDomain.php
@@ -0,0 +1,59 @@
+header('origin');
+//
+//        // 当请求使用 credentials 模式时,不能使用通配符
+//        // 必须指定具体的域名或提取请求中的 Origin
+//        $allowOrigin = '*';
+//        if ($origin) {
+//            // 如果需要限制特定域名,可以在这里判断
+//            // 以下是允许的域名列表,如果请求来自这些域名之一,则允许跨域
+//            $allowDomains = [ /*  */ ];
+//
+//            // 如果请求来源在允许列表中,直接使用该源
+//            if (in_array($origin, $allowDomains)) {
+//                $allowOrigin = $origin;
+//            }
+//        }
+//
+//        // 设置允许的请求头信息
+//        $allowHeaders = [
+//            'Authorization', 'Content-Type', 'If-Match', 'If-Modified-Since',
+//            'If-None-Match', 'If-Unmodified-Since', 'X-Requested-With',
+//            'X-Token', 'X-Api-Token', 'Accept', 'Origin'
+//        ];
+//
+//        $response = $next($request);
+//
+//        // 添加跨域响应头
+//        $response->header([
+//            'Access-Control-Allow-Origin' => $allowOrigin,
+//            'Access-Control-Allow-Headers' => implode(', ', $allowHeaders),
+//            'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS',
+//            'Access-Control-Allow-Credentials' => 'true',
+//            'Access-Control-Max-Age' => '86400',
+//        ]);
+//
+//        // 对于预检请求,直接返回成功响应
+//        if ($request->method(true) == 'OPTIONS') {
+//            return response()->code(200);
+//        }
+//
+//        return $response;
+    }
+} 
\ No newline at end of file
diff --git a/application/common/middleware/jwt.php b/application/common/middleware/jwt.php
new file mode 100644
index 0000000..09331b1
--- /dev/null
+++ b/application/common/middleware/jwt.php
@@ -0,0 +1,49 @@
+ 401,
+                'msg'  => '未授权访问,缺少有效的身份凭证',
+                'data' => null
+            ])->header(['Content-Type' => 'application/json; charset=utf-8']);
+        }
+        
+        $payload = JwtUtil::verifyToken($token);
+        if (!$payload) {
+            return json([
+                'code' => 401,
+                'msg'  => '授权已过期或无效',
+                'data' => null
+            ])->header(['Content-Type' => 'application/json; charset=utf-8']);
+        }
+        
+        // 将用户信息附加到请求中
+        $request->userInfo = $payload;
+        
+        // 写入日志
+        Log::info('JWT认证通过', ['user_id' => $payload['id'] ?? 0, 'username' => $payload['username'] ?? '']);
+        
+        return $next($request);
+    }
+} 
\ No newline at end of file
diff --git a/application/common/model/Administrator.php b/application/common/model/Administrator.php
new file mode 100644
index 0000000..9a2ef17
--- /dev/null
+++ b/application/common/model/Administrator.php
@@ -0,0 +1,31 @@
+allowField(true)->save($data);
+
+        return $log->id;
+    }
+} 
\ No newline at end of file
diff --git a/application/common/model/DeviceTaskconf.php b/application/common/model/DeviceTaskconf.php
new file mode 100644
index 0000000..a4f2c68
--- /dev/null
+++ b/application/common/model/DeviceTaskconf.php
@@ -0,0 +1,24 @@
+whereIn('deviceId', $deviceIds)
+            ->group('deviceId')
+            ->select()
+            ->toArray();
+    }
+}
\ No newline at end of file
diff --git a/application/common/model/Menu.php b/application/common/model/Menu.php
new file mode 100644
index 0000000..acd26dc
--- /dev/null
+++ b/application/common/model/Menu.php
@@ -0,0 +1,17 @@
+ 'integer',
+        'baseScore' => 'integer',
+        'baseScoreCalculated' => 'integer',
+        'baseInfoScore' => 'integer',
+        'friendCountScore' => 'integer',
+        'friendCount' => 'integer',
+        'dynamicScore' => 'integer',
+        'frequentCount' => 'integer',
+        'frequentPenalty' => 'integer',
+        'consecutiveNoFrequentDays' => 'integer',
+        'noFrequentBonus' => 'integer',
+        'banPenalty' => 'integer',
+        'healthScore' => 'integer',
+        'maxAddFriendPerDay' => 'integer',
+        'isModifiedAlias' => 'integer',
+        'isBanned' => 'integer',
+        'lastFrequentTime' => 'integer',
+        'lastNoFrequentTime' => 'integer',
+        'baseScoreCalcTime' => 'integer',
+        'createTime' => 'integer',
+        'updateTime' => 'integer',
+    ];
+}
+
diff --git a/application/common/model/WechatCustomer.php b/application/common/model/WechatCustomer.php
new file mode 100644
index 0000000..c467338
--- /dev/null
+++ b/application/common/model/WechatCustomer.php
@@ -0,0 +1,21 @@
+where('phone', $account)->whereOr('account', $account);
+        })
+            ->where(function ($query) use ($typeId) {
+                $query->where('status', 1)->where('typeId', $typeId);
+            })->find();
+
+        return $user;
+    }
+
+    /**
+     * 获取用户信息
+     *
+     * @param string $account 账号(手机号)
+     * @param string $password 密码(可能是加密后的)
+     * @param int $typeId 身份信息
+     * @return array|null
+     */
+    protected function getUser(string $account, string $password, int $typeId): array
+    {
+        $user = $this->getUserProfileWithAccountAndType($account, $typeId);
+
+        if (!$user) {
+            throw new \Exception('用户不存在或已禁用', 403);
+        }
+
+        if ($user->passwordMd5 !== md5($password)) {
+            throw new \Exception('账号或密码错误', 403);
+        }
+
+        return $user->toArray();
+    }
+
+    /**
+     * 构造函数
+     */
+    public function __construct()
+    {
+        $this->smsService = new SmsService();
+    }
+
+    /**
+     * 用户登录
+     *
+     * @param string $account 账号(手机号)
+     * @param string $password 密码(可能是加密后的)
+     * @param string $ip 登录IP
+     * @return array
+     * @throws \Exception
+     */
+    public function login(string $account, string $password, int $typeId, string $ip)
+    {
+        // 获取用户信息
+        $member = $this->getUser($account, $password, $typeId);
+
+        // 生成JWT令牌
+        $token = JwtUtil::createToken($user, self::TOKEN_EXPIRE);
+        $token_expired = time() + self::TOKEN_EXPIRE;
+
+        return compact('member', 'token', 'token_expired');
+    }
+
+    /**
+     * 手机号验证码登录
+     *
+     * @param string $account 手机号
+     * @param string $code 验证码(可能是加密后的)
+     * @param string $ip 登录IP
+     * @param bool $isEncrypted 验证码是否已加密
+     * @return array
+     * @throws \Exception
+     */
+    public function mobileLogin($account, $code, $ip, $isEncrypted = false)
+    {
+        // 验证验证码
+        if (!$this->smsService->verifyCode($account, $code, 'login', $isEncrypted)) {
+            Log::info('验证码验证失败', ['account' => $account, 'ip' => $ip, 'is_encrypted' => $isEncrypted]);
+            throw new \Exception('验证码错误或已过期', 404);
+        }
+
+        // 获取用户信息
+        $user = User::getUserByMobile($account);
+        if (empty($user)) {
+            Log::info('用户不存在', ['account' => $account, 'ip' => $ip]);
+            throw new \Exception('用户不存在', 404);
+        }
+
+        // 生成JWT令牌
+        $token = JwtUtil::createToken($user, self::TOKEN_EXPIRE);
+        $expireTime = time() + self::TOKEN_EXPIRE;
+
+        // 记录登录成功
+        Log::info('手机号登录成功', ['account' => $account, 'ip' => $ip]);
+
+        return [
+            'token' => $token,
+            'token_expired' => $expireTime,
+            'member' => $user
+        ];
+    }
+
+    /**
+     * 发送登录验证码
+     *
+     * @param string $account 手机号
+     * @param string $type 验证码类型
+     * @return array
+     * @throws \Exception
+     */
+    public function sendLoginCode($account, $type)
+    {
+        return $this->smsService->sendCode($account, $type);
+    }
+
+    /**
+     * 获取用户信息
+     *
+     * @param array $userInfo JWT中的用户信息
+     * @return array
+     * @throws \Exception
+     */
+    public function getUserInfo($userInfo)
+    {
+        if (empty($userInfo)) {
+            throw new \Exception('获取用户信息失败');
+        }
+
+        // 移除不需要返回的字段
+        unset($userInfo['exp']);
+        unset($userInfo['iat']);
+
+        return $userInfo;
+    }
+
+    /**
+     * 刷新令牌
+     *
+     * @param array $userInfo JWT中的用户信息
+     * @return array
+     * @throws \Exception
+     */
+    public function refreshToken($userInfo)
+    {
+        if (empty($userInfo)) {
+            throw new \Exception('刷新令牌失败');
+        }
+
+        // 移除过期时间信息
+        unset($userInfo['exp']);
+        unset($userInfo['iat']);
+
+        // 生成新令牌
+        $token = JwtUtil::createToken($userInfo, self::TOKEN_EXPIRE);
+        $expireTime = time() + self::TOKEN_EXPIRE;
+
+        return [
+            'token' => $token,
+            'token_expired' => $expireTime
+        ];
+    }
+
+    /**
+     * 获取系统授权信息,使用缓存存储10分钟
+     *
+     * @param bool $useCache 是否使用缓存
+     * @return string
+     */
+    public static function getSystemAuthorization($useCache = true)
+    {
+        // 定义缓存键名
+        $cacheKey = 'system_authorization_token';
+
+        // 尝试从缓存获取授权信息
+        $authorization = Cache::get($cacheKey);
+        //$authorization = '';
+        // 如果缓存中没有或已过期,则重新获取
+        if (empty($authorization) || !$useCache) {
+            try {
+                // 从环境变量中获取API用户名和密码
+                $username = Env::get('api.username', '');
+                $password = Env::get('api.password', '');
+
+                if (empty($username) || empty($password)) {
+                    Log::error('缺少API用户名或密码配置');
+                    return '';
+                }
+
+                // 构建登录参数
+                $params = [
+                    'grant_type' => 'password',
+                    'username' => $username,
+                    'password' => $password
+                ];
+
+                // 获取API基础URL
+                $baseUrl = Env::get('api.wechat_url', '');
+                if (empty($baseUrl)) {
+                    Log::error('缺少API基础URL配置');
+                    return '';
+                }
+
+                // 调用登录接口获取token
+                // 设置请求头
+                $headerData = ['client:system'];
+                $header = setHeader($headerData, '', 'plain');
+                $result = requestCurl($baseUrl . 'token', $params, 'POST', $header);
+                $result_array = handleApiResponse($result);
+
+                if (isset($result_array['access_token']) && !empty($result_array['access_token'])) {
+                    $authorization = $result_array['access_token'];
+
+                    // 存入缓存,有效期10分钟(600秒)
+                    Cache::set($cacheKey, $authorization, 600);
+                    Cache::set('system_refresh_token', $result_array['refresh_token'], 600);
+
+                    Log::info('已重新获取系统授权信息并缓存');
+                    return $authorization;
+                } else {
+                    Log::error('获取系统授权信息失败:' . ($response['message'] ?? '未知错误'));
+                    return '';
+                }
+            } catch (\Exception $e) {
+                Log::error('获取系统授权信息异常:' . $e->getMessage());
+                return '';
+            }
+        }
+
+        return $authorization;
+    }
+} 
\ No newline at end of file
diff --git a/application/common/service/ClassTableService.php b/application/common/service/ClassTableService.php
new file mode 100644
index 0000000..365e882
--- /dev/null
+++ b/application/common/service/ClassTableService.php
@@ -0,0 +1,82 @@
+app = $app;
+        $this->classTable = ClassTable::getSelfInstance();
+    }
+
+    /**
+     * 绑定实例或类到容器
+     * @param string|array $alias
+     * @param mixed $instance
+     * @param string|null $tag
+     */
+    public function bind($alias, $instance = null, string $tag = null)
+    {
+        $this->classTable->bind($alias, $instance, $tag);
+        return $this;
+    }
+
+    /**
+     * 获取实例
+     * @param string|object $class
+     * @param array $parameters
+     * @return object
+     */
+    public function getInstance($class, ...$parameters)
+    {
+        return $this->classTable->getInstance($class, ...$parameters);
+    }
+
+    /**
+     * 获取共享实例
+     * @param string $alias
+     * @param array $parameters
+     * @return object|null
+     */
+    public function getShared($alias, array $parameters = [])
+    {
+        return $this->classTable->getShared($alias, $parameters);
+    }
+
+    /**
+     * 根据标签获取类
+     * @param string $tag
+     * @return array|null
+     */
+    public function getClassByTag(string $tag)
+    {
+        return $this->classTable->getClassByTag($tag);
+    }
+
+    /**
+     * 检查别名是否存在
+     * @param string $alias
+     * @return bool
+     */
+    public function has(string $alias)
+    {
+        return $this->classTable->has($alias);
+    }
+
+    /**
+     * 复制实例
+     * @param mixed $class
+     * @param string|null $name
+     * @return object
+     */
+    public function copy($class, string $name = null)
+    {
+        return $this->classTable->copy($class, $name);
+    }
+} 
\ No newline at end of file
diff --git a/application/common/service/SmsService.php b/application/common/service/SmsService.php
new file mode 100644
index 0000000..e0552f1
--- /dev/null
+++ b/application/common/service/SmsService.php
@@ -0,0 +1,203 @@
+checkSendLimit($mobile, $type);
+
+        // 生成验证码
+        $code = $this->generateCode();
+
+        // 缓存验证码
+        $this->saveCode($mobile, $code, $type);
+
+        // 发送验证码(实际项目中对接短信平台)
+        $this->doSend($mobile, $code, $type);
+
+        // 记录日志
+        Log::info('发送验证码', [
+            'mobile' => $mobile,
+            'type' => $type,
+            'code' => $code
+        ]);
+
+        return [
+            'mobile' => $mobile,
+            'expire' => self::CODE_EXPIRE,
+            // 测试环境返回验证码,生产环境不应返回
+            'code' => $code
+        ];
+    }
+
+    /**
+     * 验证验证码
+     * @param string $mobile 手机号
+     * @param string $code 验证码(可能是加密后的)
+     * @param string $type 验证码类型
+     * @param bool $isEncrypted 验证码是否已加密
+     * @return bool
+     */
+    public function verifyCode($mobile, $code, $type, $isEncrypted = false)
+    {
+        $cacheKey = $this->getCodeCacheKey($mobile, $type);
+        $cacheCode = Cache::get($cacheKey);
+
+        if (!$cacheCode) {
+            Log::info('验证码不存在或已过期', [
+                'mobile' => $mobile,
+                'type' => $type
+            ]);
+            return false;
+        }
+
+        // 验证码是否匹配
+        $isValid = false;
+
+        if ($isEncrypted) {
+            // 前端已加密,需要对缓存中的验证码进行相同的加密处理
+            $encryptedCacheCode = $this->encryptCode($cacheCode);
+            $isValid = hash_equals($encryptedCacheCode, $code);
+
+            // 记录日志
+            Log::info('加密验证码验证', [
+                'mobile' => $mobile,
+                'cache_code' => $cacheCode,
+                'encrypted_cache_code' => $encryptedCacheCode,
+                'input_code' => $code,
+                'is_valid' => $isValid
+            ]);
+        } else {
+            // 未加密,直接比较
+            $isValid = ($cacheCode === $code);
+
+            // 记录日志
+            Log::info('明文验证码验证', [
+                'mobile' => $mobile,
+                'cache_code' => $cacheCode,
+                'input_code' => $code,
+                'is_valid' => $isValid
+            ]);
+        }
+
+        // 验证成功后删除缓存
+        if ($isValid) {
+            Cache::rm($cacheKey);
+        }
+
+        return $isValid;
+    }
+
+    /**
+     * 检查发送频率限制
+     * @param string $mobile 手机号
+     * @param string $type 验证码类型
+     * @throws \Exception
+     */
+    protected function checkSendLimit($mobile, $type)
+    {
+        $cacheKey = $this->getCodeCacheKey($mobile, $type);
+
+        // 检查是否存在未过期的验证码
+        if (Cache::has($cacheKey)) {
+            throw new \Exception('验证码已发送,请稍后再试');
+        }
+
+        // 检查当日发送次数限制
+        $limitKey = "sms_limit:{$mobile}:" . date('Ymd');
+        $sendCount = Cache::get($limitKey, 0);
+
+        if ($sendCount >= 10) {
+            throw new \Exception('今日发送次数已达上限');
+        }
+
+        // 更新发送次数
+        Cache::set($limitKey, $sendCount + 1, 86400);
+    }
+
+    /**
+     * 生成随机验证码
+     * @return string
+     */
+    protected function generateCode()
+    {
+        // 生成4位数字验证码
+        return sprintf("%0" . self::CODE_LENGTH . "d", mt_rand(0, pow(10, self::CODE_LENGTH) - 1));
+    }
+
+    /**
+     * 保存验证码到缓存
+     * @param string $mobile 手机号
+     * @param string $code 验证码
+     * @param string $type 验证码类型
+     */
+    protected function saveCode($mobile, $code, $type)
+    {
+        $cacheKey = $this->getCodeCacheKey($mobile, $type);
+        Cache::set($cacheKey, $code, self::CODE_EXPIRE);
+    }
+
+    /**
+     * 执行发送验证码
+     * @param string $mobile 手机号
+     * @param string $code 验证码
+     * @param string $type 验证码类型
+     * @return bool
+     */
+    protected function doSend($mobile, $code, $type)
+    {
+        // 实际项目中对接短信平台API
+        // 这里仅做模拟,返回成功
+        return true;
+    }
+
+    /**
+     * 获取验证码缓存键名
+     * @param string $mobile 手机号
+     * @param string $type 验证码类型
+     * @return string
+     */
+    protected function getCodeCacheKey($mobile, $type)
+    {
+        return "sms_code:{$mobile}:{$type}";
+    }
+
+    /**
+     * 加密验证码
+     * 使用与前端相同的加密算法
+     * @param string $code 原始验证码
+     * @return string 加密后的验证码
+     */
+    protected function encryptCode($code)
+    {
+        // 使用与前端相同的加密算法
+        $salt = 'yishi_salt_2024'; // 与前端相同的盐值
+        return hash('sha256', $code . $salt);
+    }
+} 
\ No newline at end of file
diff --git a/application/common/service/WechatAccountHealthScoreService.php b/application/common/service/WechatAccountHealthScoreService.php
new file mode 100644
index 0000000..22f82e4
--- /dev/null
+++ b/application/common/service/WechatAccountHealthScoreService.php
@@ -0,0 +1,1506 @@
+where('id', $accountId)
+                    ->find();
+                
+                // 减少不必要的日志记录
+            }
+            
+            if (empty($accountData)) {
+                $errorMsg = "账号不存在:{$accountId}";
+                Log::error($errorMsg);
+                throw new Exception($errorMsg);
+            }
+            
+            $wechatId = $accountData['wechatId'] ?? '';
+            if (empty($wechatId)) {
+                $errorMsg = "账号wechatId为空:{$accountId}";
+                Log::error($errorMsg);
+                throw new Exception($errorMsg);
+            }
+            
+            // 减少不必要的日志记录
+            
+            // 获取或创建评分记录
+            $scoreRecord = $this->getOrCreateScoreRecord($accountId, $wechatId);
+            $scoreSnapshotBefore = $this->buildScoreSnapshotForLogging($scoreRecord);
+            // 减少不必要的日志记录
+            
+            // 计算基础分(只计算一次,除非强制重新计算)
+            if (!$scoreRecord['baseScoreCalculated'] || $forceRecalculateBase) {
+                
+                $baseScoreData = $this->calculateBaseScore($accountData, $scoreRecord);
+                $this->updateBaseScore($accountId, $baseScoreData);
+                
+                // 减少不必要的日志记录
+                
+                // 重新获取记录以获取最新数据
+                $scoreRecord = $this->getScoreRecord($accountId);
+            }
+            
+            // 计算动态分(每次都要重新计算)
+            $dynamicScoreData = $this->calculateDynamicScore($accountData, $scoreRecord);
+            
+            // 计算总分
+            $baseScore = $scoreRecord['baseScore'];
+            $dynamicScore = $dynamicScoreData['total'];
+            $healthScore = $baseScore + $dynamicScore;
+            
+            // 确保健康分在合理范围内(0-100)
+            $healthScore = max(0, min(100, $healthScore));
+            
+            // 计算每日最大加人次数
+            $maxAddFriendPerDay = $this->getMaxAddFriendPerDay($healthScore);
+            
+            // 更新评分记录
+            $updateData = [
+                'dynamicScore' => $dynamicScore,
+                'frequentPenalty' => $dynamicScoreData['frequentPenalty'],
+                'noFrequentBonus' => $dynamicScoreData['noFrequentBonus'],
+                'banPenalty' => $dynamicScoreData['banPenalty'],
+                'lastFrequentTime' => $dynamicScoreData['lastFrequentTime'],
+                'frequentCount' => $dynamicScoreData['frequentCount'],
+                'lastNoFrequentTime' => $dynamicScoreData['lastNoFrequentTime'],
+                'consecutiveNoFrequentDays' => $dynamicScoreData['consecutiveNoFrequentDays'],
+                'isBanned' => $dynamicScoreData['isBanned'],
+                'lastBanTime' => $dynamicScoreData['lastBanTime'],
+                'healthScore' => $healthScore,
+                'maxAddFriendPerDay' => $maxAddFriendPerDay,
+                'updateTime' => time()
+            ];
+            
+            $updateResult = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)
+                ->where('accountId', $accountId)
+                ->update($updateData);
+                
+            // 更新成功后,清除缓存
+            if ($updateResult !== false) {
+                $this->logScoreChangesIfNeeded(
+                    $accountId,
+                    $wechatId,
+                    $scoreSnapshotBefore,
+                    [
+                        'frequentPenalty' => $dynamicScoreData['frequentPenalty'],
+                        'banPenalty' => $dynamicScoreData['banPenalty'],
+                        'noFrequentBonus' => $dynamicScoreData['noFrequentBonus'],
+                        'dynamicScore' => $dynamicScore,
+                        'healthScore' => $healthScore
+                    ],
+                    $dynamicScoreData
+                );
+                $this->clearScoreCache($accountId);
+            }
+            
+            $result = [
+                'accountId' => $accountId,
+                'wechatId' => $wechatId,
+                'healthScore' => $healthScore,
+                'baseScore' => $baseScore,
+                'baseInfoScore' => $scoreRecord['baseInfoScore'],
+                'friendCountScore' => $scoreRecord['friendCountScore'],
+                'dynamicScore' => $dynamicScore,
+                'frequentPenalty' => $dynamicScoreData['frequentPenalty'],
+                'noFrequentBonus' => $dynamicScoreData['noFrequentBonus'],
+                'banPenalty' => $dynamicScoreData['banPenalty'],
+                'maxAddFriendPerDay' => $maxAddFriendPerDay
+            ];
+            
+            // 减少不必要的日志记录
+            return $result;
+            
+        } catch (\PDOException $e) {
+            // 数据库异常
+            $errorMsg = "数据库操作失败,accountId: {$accountId}, 错误: " . $e->getMessage();
+            Log::error($errorMsg);
+            throw new Exception($errorMsg, $e->getCode(), $e);
+        } catch (\Throwable $e) {
+            // 其他所有异常
+            $errorMsg = "计算健康分失败,accountId: {$accountId}, 错误: " . $e->getMessage();
+            Log::error($errorMsg);
+            throw new Exception($errorMsg, $e->getCode(), $e);
+        }
+    }
+    
+    /**
+     * 获取或创建评分记录
+     * 优化:使用事务和锁避免并发问题,减少重复查询
+     * 
+     * @param int $accountId 账号ID
+     * @param string $wechatId 微信ID
+     * @return array 评分记录
+     */
+    private function getOrCreateScoreRecord($accountId, $wechatId)
+    {
+        // 尝试获取现有记录
+        $record = $this->getScoreRecord($accountId);
+        
+        // 如果记录不存在,创建新记录
+        if (empty($record)) {
+            // 使用事务避免并发问题
+            Db::startTrans();
+            try {
+                // 再次检查记录是否存在(避免并发问题)
+                $record = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)
+                    ->where('accountId', $accountId)
+                    ->lock(true)  // 加锁防止并发插入
+                    ->find();
+                
+                if (empty($record)) {
+                    Log::info("为账号 {$accountId} 创建新的评分记录");
+                    
+                    // 检查表中是否存在lastBanTime字段
+                    $this->ensureScoreTableFields();
+                    
+                    // 创建新记录
+                    $data = [
+                        'accountId' => $accountId,
+                        'wechatId' => $wechatId,
+                        'baseScore' => 0,
+                        'baseScoreCalculated' => 0,
+                        'baseInfoScore' => 0,
+                        'friendCountScore' => 0,
+                        'dynamicScore' => 0,
+                        'frequentCount' => 0,
+                        'consecutiveNoFrequentDays' => 0,
+                        'healthScore' => 0,
+                        'maxAddFriendPerDay' => 0,
+                        'lastBanTime' => null,
+                        'createTime' => time(),
+                        'updateTime' => time()
+                    ];
+                    
+                    Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)->insert($data);
+                    $record = $data;
+                }
+                
+                Db::commit();
+            } catch (\Exception $e) {
+                Db::rollback();
+                Log::error("创建评分记录失败: " . $e->getMessage());
+                throw $e;
+            }
+        }
+        
+        return $record;
+    }
+    
+    /**
+     * 确保评分表有所需字段
+     * 优化:使用静态变量缓存结果,避免重复检查
+     * 
+     * @return void
+     */
+    private function ensureScoreTableFields()
+    {
+        // 使用静态变量缓存检查结果,避免重复检查
+        static $fieldsChecked = false;
+        
+        if ($fieldsChecked) {
+            return;
+        }
+        
+        try {
+            // 检查表中是否存在lastBanTime字段
+            $hasLastBanTimeField = false;
+            $tableFields = Db::query("SHOW COLUMNS FROM " . self::TABLE_WECHAT_ACCOUNT_SCORE);
+            foreach ($tableFields as $field) {
+                if ($field['Field'] == 'lastBanTime') {
+                    $hasLastBanTimeField = true;
+                    break;
+                }
+            }
+            
+            // 如果字段不存在,添加字段
+            if (!$hasLastBanTimeField) {
+                Log::info("添加lastBanTime字段到" . self::TABLE_WECHAT_ACCOUNT_SCORE . "表");
+                Db::execute("ALTER TABLE " . self::TABLE_WECHAT_ACCOUNT_SCORE . " ADD COLUMN lastBanTime INT(11) DEFAULT NULL COMMENT '最后一次封号时间'");
+            }
+            
+            $fieldsChecked = true;
+        } catch (\Exception $e) {
+            Log::error("检查或添加字段失败: " . $e->getMessage());
+            // 出错时不影响后续逻辑,继续执行
+        }
+    }
+    
+    /**
+     * 获取评分记录
+     * 优化:使用多级缓存策略,提高缓存命中率
+     * 
+     * @param int $accountId 账号ID
+     * @param bool $useCache 是否使用缓存(默认true)
+     * @return array 评分记录,如果不存在则返回空数组
+     */
+    private function getScoreRecord($accountId, $useCache = true)
+    {
+        // 生成缓存键
+        $cacheKey = self::CACHE_PREFIX . 'score:' . $accountId;
+        
+        // 如果使用缓存且缓存存在,则直接返回缓存数据
+        if ($useCache && Cache::has($cacheKey)) {
+            $cachedData = Cache::get($cacheKey);
+            // 减少日志记录,提高性能
+            return $cachedData ?: [];
+        }
+        
+        // 从数据库获取记录
+        $record = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)
+            ->where('accountId', $accountId)
+            ->find();
+            
+        // 如果记录存在且使用缓存,则缓存记录
+        if ($record && $useCache) {
+            // 根据数据更新频率设置不同的缓存时间
+            // 如果记录最近更新过(1小时内),使用短期缓存
+            $updateTime = $record['updateTime'] ?? 0;
+            $cacheTime = (time() - $updateTime < 3600) ? self::CACHE_TTL_SHORT : self::CACHE_TTL;
+            
+            Cache::set($cacheKey, $record, $cacheTime);
+            Log::debug("缓存评分记录,accountId: {$accountId}, 缓存时间: {$cacheTime}秒");
+        }
+        
+        return $record ?: [];
+    }
+    
+    /**
+     * 计算基础分(只计算一次)
+     * 基础分 = 默认60分 + 基础信息分(10分) + 好友数量分(3-12分)
+     * 
+     * @param array $accountData 账号数据
+     * @param array $scoreRecord 现有评分记录
+     * @return array 基础分数据
+     */
+    private function calculateBaseScore($accountData, $scoreRecord = [])
+    {
+        $baseScore = self::DEFAULT_BASE_SCORE;
+        
+        // 基础信息分(已修改微信号得10分)
+        $baseInfoScore = $this->getBaseInfoScore($accountData);
+        $baseScore += $baseInfoScore;
+        
+        // 好友数量分(特殊处理:使用快照值,避免同步问题)
+        $friendCountScore = 0;
+        $friendCount = 0;
+        $friendCountSource = 'manual';
+        
+        // 如果已有评分记录且好友数量分已计算,使用历史值
+        if (!empty($scoreRecord['friendCountScore']) && $scoreRecord['friendCountScore'] > 0) {
+            $friendCountScore = $scoreRecord['friendCountScore'];
+            $friendCount = $scoreRecord['friendCount'] ?? 0;
+            $friendCountSource = $scoreRecord['friendCountSource'] ?? 'manual';
+        } else {
+            // 首次计算:使用当前好友数量,但标记为手动计算
+            $totalFriend = $accountData['totalFriend'] ?? 0;
+            $friendCountScore = $this->getFriendCountScore($totalFriend);
+            $friendCount = $totalFriend;
+            $friendCountSource = 'manual';
+        }
+        
+        $baseScore += $friendCountScore;
+        
+        // 检查是否已修改微信号
+        $isModifiedAlias = $this->checkIsModifiedAlias($accountData);
+        
+        return [
+            'baseScore' => $baseScore,
+            'baseInfoScore' => $baseInfoScore,
+            'friendCountScore' => $friendCountScore,
+            'friendCount' => $friendCount,
+            'friendCountSource' => $friendCountSource,
+            'isModifiedAlias' => $isModifiedAlias ? 1 : 0,
+            'baseScoreCalculated' => 1,
+            'baseScoreCalcTime' => time()
+        ];
+    }
+    
+    /**
+     * 更新基础分
+     * 
+     * @param int $accountId 账号ID
+     * @param array $baseScoreData 基础分数据
+     * @return bool 更新是否成功
+     */
+    private function updateBaseScore($accountId, $baseScoreData)
+    {
+        try {
+            $result = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)
+                ->where('accountId', $accountId)
+                ->update($baseScoreData);
+                
+            // 减少不必要的日志记录
+            
+            // 更新成功后,清除缓存
+            if ($result !== false) {
+                $this->clearScoreCache($accountId);
+            }
+            
+            return $result !== false;
+        } catch (Exception $e) {
+            Log::error("更新基础分失败,accountId: {$accountId}, 错误: " . $e->getMessage());
+            return false;
+        }
+    }
+    
+    /**
+     * 清除评分记录缓存
+     * 
+     * @param int $accountId 账号ID
+     * @return bool 是否成功清除缓存
+     */
+    private function clearScoreCache($accountId)
+    {
+        $cacheKey = self::CACHE_PREFIX . 'score:' . $accountId;
+        $result = Cache::rm($cacheKey);
+        // 减少不必要的日志记录
+        return $result;
+    }
+    
+    /**
+     * 获取基础信息分
+     * 已修改微信号:10分
+     * 
+     * @param array $accountData 账号数据
+     * @return int 基础信息分
+     */
+    private function getBaseInfoScore($accountData)
+    {
+        if ($this->checkIsModifiedAlias($accountData)) {
+            return self::BASE_INFO_SCORE;
+        }
+        return 0;
+    }
+    
+    /**
+     * 检查是否已修改微信号
+     * 判断标准:wechatId和alias不一致且都不为空,则认为已修改微信号
+     * 注意:这里只用于评分,不修复数据
+     * 
+     * @param array $accountData 账号数据
+     * @return bool
+     */
+    private function checkIsModifiedAlias($accountData)
+    {
+        $wechatId = trim($accountData['wechatId'] ?? '');
+        $alias = trim($accountData['alias'] ?? '');
+        
+        // 如果wechatId和alias不一致且都不为空,则认为已修改微信号(用于评分)
+        if (!empty($wechatId) && !empty($alias) && $wechatId !== $alias) {
+            return true;
+        }
+        
+        return false;
+    }
+    
+    /**
+     * 获取好友数量分
+     * 根据好友数量区间得分(最高12分)
+     * 
+     * @param int $totalFriend 总好友数
+     * @return int 好友数量分
+     */
+    private function getFriendCountScore($totalFriend)
+    {
+        if ($totalFriend <= 50) {
+            return self::FRIEND_COUNT_SCORE_0_50;
+        } elseif ($totalFriend <= 500) {
+            return self::FRIEND_COUNT_SCORE_51_500;
+        } elseif ($totalFriend <= 3000) {
+            return self::FRIEND_COUNT_SCORE_501_3000;
+        } else {
+            return self::FRIEND_COUNT_SCORE_3001_PLUS;
+        }
+    }
+    
+    /**
+     * 手动更新好友数量分(用于处理同步问题)
+     * 
+     * @param int $accountId 账号ID
+     * @param int $friendCount 好友数量
+     * @param string $source 来源(manual=手动,sync=同步)
+     * @return bool 更新是否成功
+     * @throws Exception 如果参数无效或更新过程中出现错误
+     */
+    public function updateFriendCountScore($accountId, $friendCount, $source = 'manual')
+    {
+        // 参数验证
+        if (empty($accountId) || !is_numeric($accountId)) {
+            $errorMsg = "无效的账号ID: " . (is_scalar($accountId) ? $accountId : gettype($accountId));
+            Log::error($errorMsg);
+            throw new Exception($errorMsg);
+        }
+        
+        if (!is_numeric($friendCount) || $friendCount < 0) {
+            $errorMsg = "无效的好友数量: {$friendCount}";
+            Log::error($errorMsg);
+            throw new Exception($errorMsg);
+        }
+        
+        if (!in_array($source, ['manual', 'sync'])) {
+            $errorMsg = "无效的来源: {$source},必须是 'manual' 或 'sync'";
+            Log::error($errorMsg);
+            throw new Exception($errorMsg);
+        }
+        
+        try {
+            $scoreRecord = $this->getScoreRecord($accountId);
+            
+            // 如果基础分已计算,不允许修改好友数量分(除非是手动更新)
+            if (!empty($scoreRecord['baseScoreCalculated']) && $source === 'sync') {
+                // 同步数据不允许修改已计算的基础分
+                Log::warning("同步数据不允许修改已计算的基础分,accountId: {$accountId}");
+                return false;
+            }
+        }
+        catch (\Exception $e) {
+            $errorMsg = "获取评分记录失败,accountId: {$accountId}, 错误: " . $e->getMessage();
+            Log::error($errorMsg);
+            throw new Exception($errorMsg, $e->getCode(), $e);
+        }
+        
+        $friendCountScore = $this->getFriendCountScore($friendCount);
+        
+        // 重新计算基础分
+        $oldBaseScore = $scoreRecord['baseScore'] ?? self::DEFAULT_BASE_SCORE;
+        $oldFriendCountScore = $scoreRecord['friendCountScore'] ?? 0;
+        $baseInfoScore = $scoreRecord['baseInfoScore'] ?? 0;
+        
+        $newBaseScore = self::DEFAULT_BASE_SCORE + $baseInfoScore + $friendCountScore;
+        
+        $updateData = [
+            'friendCountScore' => $friendCountScore,
+            'friendCount' => $friendCount,
+            'friendCountSource' => $source,
+            'baseScore' => $newBaseScore,
+            'updateTime' => time()
+        ];
+        
+        // 如果基础分已计算,需要更新总分
+        if (!empty($scoreRecord['baseScoreCalculated'])) {
+            $dynamicScore = $scoreRecord['dynamicScore'] ?? 0;
+            $healthScore = $newBaseScore + $dynamicScore;
+            $healthScore = max(0, min(100, $healthScore));
+            $updateData['healthScore'] = $healthScore;
+            $updateData['maxAddFriendPerDay'] = $this->getMaxAddFriendPerDay($healthScore);
+        }
+        
+        try {
+            $result = Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE)
+                ->where('accountId', $accountId)
+                ->update($updateData);
+                
+            // 更新成功后,清除缓存
+            if ($result !== false) {
+                $this->clearScoreCache($accountId);
+                $this->clearHealthScoreCache($accountId);
+                Log::info("更新好友数量分成功,accountId: {$accountId}, friendCount: {$friendCount}, source: {$source}");
+            } else {
+                Log::warning("更新好友数量分失败,accountId: {$accountId}, friendCount: {$friendCount}, source: {$source}");
+            }
+            
+            return $result !== false;
+        } catch (\PDOException $e) {
+            $errorMsg = "数据库操作失败,accountId: {$accountId}, 错误: " . $e->getMessage();
+            Log::error($errorMsg);
+            throw new Exception($errorMsg, $e->getCode(), $e);
+        } catch (\Throwable $e) {
+            $errorMsg = "更新好友数量分失败,accountId: {$accountId}, 错误: " . $e->getMessage();
+            Log::error($errorMsg);
+            throw new Exception($errorMsg, $e->getCode(), $e);
+        }
+    }
+    
+    /**
+     * 计算动态分
+     * 动态分 = 扣分 + 加分
+     * 如果添加好友记录表没有记录,则动态分为0
+     * 
+     * @param array $accountData 账号数据
+     * @param array $scoreRecord 现有评分记录
+     * @return array 动态分数据
+     */
+    private function calculateDynamicScore($accountData, $scoreRecord)
+    {
+        $accountId = $accountData['id'] ?? 0;
+        $wechatId = $accountData['wechatId'] ?? '';
+        
+        Log::debug("开始计算动态分,accountId: {$accountId}, wechatId: {$wechatId}");
+        
+        $result = [
+            'total' => 0,
+            'frequentPenalty' => 0,
+            'noFrequentBonus' => 0,
+            'banPenalty' => 0,
+            'lastFrequentTime' => null,
+            'frequentCount' => 0,
+            'lastNoFrequentTime' => null,
+            'consecutiveNoFrequentDays' => 0,
+            'isBanned' => 0,
+            'lastBanTime' => null,
+            'frequentTaskIds' => [],
+            'banMessageId' => null
+        ];
+        
+        if (empty($accountId) || empty($wechatId)) {
+            Log::warning("计算动态分失败: accountId或wechatId为空");
+            return $result;
+        }
+        
+        // 不再使用30天限制
+        
+        // 检查添加好友记录表是否有记录,如果没有记录则动态分为0
+        // 使用EXISTS子查询优化性能,只检查是否存在记录,不需要计数
+        $hasFriendTask = Db::table(self::TABLE_FRIEND_TASK)
+            ->where('wechatAccountId', $accountId)
+            ->where(function($query) use ($wechatId) {
+                if (!empty($wechatId)) {
+                    $query->where('wechatId', $wechatId);
+                }
+            })
+            ->value('id'); // 只获取ID,比count()更高效
+        
+        // 如果添加好友记录表没有记录,则动态分为0
+        if (empty($hasFriendTask)) {
+            Log::info("账号没有添加好友记录,动态分为0,accountId: {$accountId}");
+            return $result;
+        }
+        
+        Log::debug("账号有添加好友记录,继续计算动态分,accountId: {$accountId}");
+        
+        // 继承现有数据
+        if (!empty($scoreRecord)) {
+            $result['lastFrequentTime'] = $scoreRecord['lastFrequentTime'] ?? null;
+            $result['frequentCount'] = $scoreRecord['frequentCount'] ?? 0;
+            $result['lastNoFrequentTime'] = $scoreRecord['lastNoFrequentTime'] ?? null;
+            $result['consecutiveNoFrequentDays'] = $scoreRecord['consecutiveNoFrequentDays'] ?? 0;
+            $result['frequentPenalty'] = $scoreRecord['frequentPenalty'] ?? 0;
+            $result['noFrequentBonus'] = $scoreRecord['noFrequentBonus'] ?? 0;
+            $result['banPenalty'] = $scoreRecord['banPenalty'] ?? 0;
+            $result['lastBanTime'] = $scoreRecord['lastBanTime'] ?? null;
+        }
+        
+        // 1. 检查频繁记录(从s2_friend_task表查询,不限制时间)
+        $frequentData = $this->checkFrequentFromFriendTask($accountId, $wechatId, $scoreRecord);
+        $result['lastFrequentTime'] = $frequentData['lastFrequentTime'] ?? null;
+        $result['frequentCount'] = $frequentData['frequentCount'] ?? 0;
+        $result['frequentPenalty'] = $frequentData['frequentPenalty'] ?? 0;
+        $result['frequentTaskIds'] = $frequentData['taskIds'] ?? [];
+        
+        // 2. 检查封号记录(从s2_wechat_message表查询,不限制时间)
+        $banData = $this->checkBannedFromMessage($accountId, $wechatId);
+        if (!empty($banData)) {
+            $result['isBanned'] = $banData['isBanned'];
+            $result['banPenalty'] = $banData['banPenalty'];
+            $result['lastBanTime'] = $banData['lastBanTime'];
+            $result['banMessageId'] = $banData['messageId'] ?? null;
+        }
+        
+        // 3. 计算不频繁加分(基于频繁记录,反向参考频繁规则)
+        $noFrequentData = $this->calculateNoFrequentBonus($accountId, $wechatId, $frequentData);
+        $result['noFrequentBonus'] = $noFrequentData['bonus'] ?? 0;
+        $result['consecutiveNoFrequentDays'] = $noFrequentData['consecutiveDays'] ?? 0;
+        $result['lastNoFrequentTime'] = $noFrequentData['lastNoFrequentTime'] ?? null;
+        
+        // 计算总分
+        $result['total'] = $result['frequentPenalty'] + $result['noFrequentBonus'] + $result['banPenalty'];
+        
+        Log::debug("动态分计算结果,accountId: {$accountId}, frequentPenalty: {$result['frequentPenalty']}, " . 
+            "noFrequentBonus: {$result['noFrequentBonus']}, banPenalty: {$result['banPenalty']}, " . 
+            "total: {$result['total']}");
+        
+        return $result;
+    }
+    
+    /**
+     * 从s2_friend_task表检查频繁记录
+     * extra字段包含"操作过于频繁"即需要扣分
+     * 统计所有时间的数据(不限制30天)
+     * 每条记录只统计一次,使用is_counted字段标记
+     * 
+     * @param int $accountId 账号ID
+     * @param string $wechatId 微信ID
+     * @param array $scoreRecord 现有评分记录
+     * @param int $thirtyDaysAgo 已废弃参数,保留是为了兼容性
+     * @return array|null
+     */
+    private function checkFrequentFromFriendTask($accountId, $wechatId, $scoreRecord, $thirtyDaysAgo = null)
+    {
+        // 不再使用30天限制
+        
+        // 减少不必要的日志记录
+        
+        // 查询包含"操作过于频繁"的记录(统计所有时间且未被统计过的记录)
+        // extra字段可能是文本或JSON格式,使用LIKE查询
+        // 优化查询:只查询必要的字段,减少数据传输量
+        // 添加is_counted条件,只查询未被统计过的记录
+        $frequentTasks = Db::table(self::TABLE_FRIEND_TASK)
+            ->where('wechatAccountId', $accountId)
+            ->where(function($query) use ($wechatId) {
+                if (!empty($wechatId)) {
+                    $query->where('wechatId', $wechatId);
+                }
+            })
+            ->where(function($query) {
+                // 检查extra字段是否包含"操作过于频繁"(可能是文本或JSON)
+                $query->where('extra', 'like', '%操作过于频繁%')
+                      ->whereOr('extra', 'like', '%"当前账号存在安全风险"%');
+            })
+            ->where(function($query) {
+                // 只查询未被统计过的记录
+                // 注意:需要兼容is_counted字段不存在的情况
+                $query->where('is_counted', 0)
+                      ->whereOr('is_counted', null);
+            })
+            ->order('createTime', 'desc')
+            ->field('id, createTime, extra')
+            ->select();
+        
+        // 获取最新的频繁时间
+        $latestFrequentTime = !empty($frequentTasks) ? $frequentTasks[0]['createTime'] : null;
+        
+        // 计算频繁次数(统计近30天内包含"操作过于频繁"的记录)
+        $frequentCount = count($frequentTasks);
+        
+        Log::info("找到 {$frequentCount} 条未统计的频繁记录,accountId: {$accountId}, wechatId: {$wechatId}");
+        
+        // 标记这些记录为已统计
+        if (!empty($frequentTasks)) {
+            $taskIds = array_column($frequentTasks, 'id');
+            try {
+                // 检查表中是否存在is_counted字段
+                $hasIsCountedField = false;
+                $tableFields = Db::query("SHOW COLUMNS FROM " . self::TABLE_FRIEND_TASK);
+                foreach ($tableFields as $field) {
+                    if ($field['Field'] == 'is_counted') {
+                        $hasIsCountedField = true;
+                        break;
+                    }
+                }
+                
+                // 如果字段不存在,添加字段
+                if (!$hasIsCountedField) {
+                    Log::info("添加is_counted字段到" . self::TABLE_FRIEND_TASK . "表");
+                    Db::execute("ALTER TABLE " . self::TABLE_FRIEND_TASK . " ADD COLUMN is_counted TINYINT(1) DEFAULT 0 COMMENT '是否已统计(0=未统计,1=已统计)'");
+                }
+                
+                // 更新记录为已统计
+                Db::table(self::TABLE_FRIEND_TASK)
+                    ->where('id', 'in', $taskIds)
+                    ->update(['is_counted' => 1]);
+                
+                // 减少不必要的日志记录
+            } catch (\Exception $e) {
+                Log::error("标记频繁记录失败: " . $e->getMessage());
+                // 出错时不影响后续逻辑,继续执行
+            }
+        }
+        
+        // 如果30天内没有频繁记录,清除扣分
+        if (empty($frequentTasks)) {
+            return [
+                'lastFrequentTime' => null,
+                'frequentCount' => 0,
+                'frequentPenalty' => 0,
+                'taskIds' => []
+            ];
+        }
+        
+        // 根据30天内的频繁次数计算扣分
+        $penalty = 0;
+        if ($frequentCount == 1) {
+            $penalty = self::PENALTY_FIRST_FREQUENT;  // 首次频繁-15分
+            Log::info("首次频繁,扣除 " . abs(self::PENALTY_FIRST_FREQUENT) . " 分,accountId: {$accountId}");
+        } elseif ($frequentCount >= 2) {
+            $penalty = self::PENALTY_SECOND_FREQUENT;  // 再次频繁-25分
+            Log::info("再次频繁,扣除 " . abs(self::PENALTY_SECOND_FREQUENT) . " 分,accountId: {$accountId}");
+        }
+        
+        return [
+            'lastFrequentTime' => $latestFrequentTime,
+            'frequentCount' => $frequentCount,
+            'frequentPenalty' => $penalty,
+            'taskIds' => $taskIds
+        ];
+    }
+    
+    /**
+     * 从s2_wechat_message表检查封号记录
+     * content包含"你的账号被限制"且msgType为10000
+     * 统计所有时间的数据(不限制30天)
+     * 每条记录只统计一次,使用is_counted字段标记
+     * 
+     * @param int $accountId 账号ID
+     * @param string $wechatId 微信ID
+     * @param int $thirtyDaysAgo 已废弃参数,保留是为了兼容性
+     * @return array|null
+     */
+    private function checkBannedFromMessage($accountId, $wechatId, $thirtyDaysAgo = null)
+    {
+        // 不再使用30天限制
+        
+        // 减少不必要的日志记录
+        
+        // 查询封号消息(统计所有时间且未被统计过的记录)
+        // 优化查询:只查询必要的字段,减少数据传输量
+        $banMessage = Db::table(self::TABLE_WECHAT_MESSAGE)
+            ->where('wechatAccountId', $accountId)
+            ->where('msgType', 10000)
+            ->where('content', 'like', '%你的账号被限制%')
+            ->where('isDeleted', 0)
+            ->where(function($query) {
+                // 只查询未被统计过的记录
+                // 注意:需要兼容is_counted字段不存在的情况
+                $query->where('is_counted', 0)
+                      ->whereOr('is_counted', null);
+            })
+            ->field('id, createTime')  // 只查询必要的字段
+            ->order('createTime', 'desc')
+            ->find();
+        
+        if (!empty($banMessage)) {
+            try {
+                // 检查表中是否存在is_counted字段
+                $hasIsCountedField = false;
+                $tableFields = Db::query("SHOW COLUMNS FROM " . self::TABLE_WECHAT_MESSAGE);
+                foreach ($tableFields as $field) {
+                    if ($field['Field'] == 'is_counted') {
+                        $hasIsCountedField = true;
+                        break;
+                    }
+                }
+                
+                // 如果字段不存在,添加字段
+                if (!$hasIsCountedField) {
+                    Log::info("添加is_counted字段到" . self::TABLE_WECHAT_MESSAGE . "表");
+                    Db::execute("ALTER TABLE " . self::TABLE_WECHAT_MESSAGE . " ADD COLUMN is_counted TINYINT(1) DEFAULT 0 COMMENT '是否已统计(0=未统计,1=已统计)'");
+                }
+                
+                // 更新记录为已统计
+                Db::table(self::TABLE_WECHAT_MESSAGE)
+                    ->where('id', $banMessage['id'])
+                    ->update(['is_counted' => 1]);
+                
+                // 减少不必要的日志记录
+                Log::info("发现封号记录,扣除 " . abs(self::PENALTY_BANNED) . " 分,accountId: {$accountId}");
+            } catch (\Exception $e) {
+                Log::error("标记封号记录失败: " . $e->getMessage());
+                // 出错时不影响后续逻辑,继续执行
+            }
+            
+            return [
+                'isBanned' => 1,
+                'banPenalty' => self::PENALTY_BANNED,  // 封号-60分
+                'lastBanTime' => $banMessage['createTime'],
+                'messageId' => $banMessage['id']
+            ];
+        }
+        
+        return [
+            'isBanned' => 0,
+            'banPenalty' => 0,
+            'lastBanTime' => null,
+            'messageId' => null
+        ];
+    }
+    
+    /**
+     * 计算不频繁加分
+     * 反向参考频繁规则:计算连续不频繁天数
+     * 规则:连续不频繁的,只要有一次频繁就得重新计算(重置连续不频繁天数)
+     * 如果连续3天没有频繁,则每天+5分
+     * 
+     * @param int $accountId 账号ID
+     * @param string $wechatId 微信ID
+     * @param array $frequentData 频繁数据(包含lastFrequentTime和frequentCount)
+     * @param int $thirtyDaysAgo 已废弃参数,保留是为了兼容性
+     * @return array 包含bonus、consecutiveDays、lastNoFrequentTime
+     */
+    private function calculateNoFrequentBonus($accountId, $wechatId, $frequentData, $thirtyDaysAgo = null)
+    {
+        $result = [
+            'bonus' => 0,
+            'consecutiveDays' => 0,
+            'lastNoFrequentTime' => null
+        ];
+        
+        if (empty($accountId) || empty($wechatId)) {
+            return $result;
+        }
+        
+        $currentTime = time();
+        
+        // 获取最后一次频繁时间
+        $lastFrequentTime = $frequentData['lastFrequentTime'] ?? null;
+        
+        // 规则:连续不频繁的,只要有一次频繁就得重新计算(重置连续不频繁天数)
+        if (empty($lastFrequentTime)) {
+            // 情况1:没有频繁记录,说明一直连续不频繁
+            // 默认给30天的连续不频繁天数(可以根据需要调整)
+            $consecutiveDays = 30;
+        } else {
+            // 情况2:有频繁记录,从最后一次频繁时间开始重新计算连续不频繁天数
+            // 只要有一次频繁,连续不频繁天数就从最后一次频繁时间开始重新计算
+            // 计算从最后一次频繁时间到现在,连续多少天没有频繁
+            $timeDiff = $currentTime - $lastFrequentTime;
+            $consecutiveDays = floor($timeDiff / 86400); // 向下取整,得到完整的天数
+        }
+        
+        // 如果连续3天或以上没有频繁,则每天+5分
+        if ($consecutiveDays >= 3) {
+            $bonus = $consecutiveDays * self::BONUS_NO_FREQUENT_PER_DAY;
+            $result['bonus'] = $bonus;
+            $result['consecutiveDays'] = $consecutiveDays;
+            $result['lastNoFrequentTime'] = $currentTime;
+        } else {
+            $result['consecutiveDays'] = $consecutiveDays;
+        }
+        
+        return $result;
+    }
+    
+    /**
+     * 构建日志快照(用于对比前后分值)
+     * 
+     * @param array $scoreRecord
+     * @return array
+     */
+    private function buildScoreSnapshotForLogging($scoreRecord)
+    {
+        $baseScore = $scoreRecord['baseScore'] ?? self::DEFAULT_BASE_SCORE;
+        $dynamicScore = $scoreRecord['dynamicScore'] ?? 0;
+        $healthScore = $scoreRecord['healthScore'] ?? ($baseScore + $dynamicScore);
+        
+        return [
+            'frequentPenalty' => $scoreRecord['frequentPenalty'] ?? 0,
+            'banPenalty' => $scoreRecord['banPenalty'] ?? 0,
+            'noFrequentBonus' => $scoreRecord['noFrequentBonus'] ?? 0,
+            'dynamicScore' => $dynamicScore,
+            'healthScore' => $healthScore
+        ];
+    }
+    
+    /**
+     * 根据前后快照写加减分日志
+     * 
+     * @param int   $accountId
+     * @param string $wechatId
+     * @param array $before
+     * @param array $after
+     * @param array $context
+     * @return void
+     */
+    private function logScoreChangesIfNeeded($accountId, $wechatId, array $before, array $after, array $context = [])
+    {
+        $healthBefore = $before['healthScore'] ?? 0;
+        $healthAfter = $after['healthScore'] ?? 0;
+        
+        $this->recordScoreLog($accountId, $wechatId, 'frequentPenalty', $before['frequentPenalty'] ?? 0, $after['frequentPenalty'] ?? 0, [
+            'category' => 'penalty',
+            'source' => 'friend_task',
+            'sourceId' => !empty($context['frequentTaskIds']) ? $context['frequentTaskIds'][0] : null,
+            'extra' => [
+                'taskIds' => $context['frequentTaskIds'] ?? [],
+                'frequentCount' => $context['frequentCount'] ?? 0,
+                'lastFrequentTime' => $context['lastFrequentTime'] ?? null
+            ],
+            'totalScoreBefore' => $healthBefore,
+            'totalScoreAfter' => $healthAfter
+        ]);
+        
+        $this->recordScoreLog($accountId, $wechatId, 'banPenalty', $before['banPenalty'] ?? 0, $after['banPenalty'] ?? 0, [
+            'category' => 'penalty',
+            'source' => 'wechat_message',
+            'sourceId' => $context['banMessageId'] ?? null,
+            'extra' => [
+                'lastBanTime' => $context['lastBanTime'] ?? null
+            ],
+            'totalScoreBefore' => $healthBefore,
+            'totalScoreAfter' => $healthAfter
+        ]);
+        
+        $this->recordScoreLog($accountId, $wechatId, 'noFrequentBonus', $before['noFrequentBonus'] ?? 0, $after['noFrequentBonus'] ?? 0, [
+            'category' => 'bonus',
+            'source' => 'system',
+            'extra' => [
+                'consecutiveDays' => $context['consecutiveNoFrequentDays'] ?? 0,
+                'lastNoFrequentTime' => $context['lastNoFrequentTime'] ?? null
+            ],
+            'totalScoreBefore' => $healthBefore,
+            'totalScoreAfter' => $healthAfter
+        ]);
+        
+        $this->recordScoreLog($accountId, $wechatId, 'dynamicScore', $before['dynamicScore'] ?? 0, $after['dynamicScore'] ?? 0, [
+            'category' => 'dynamic_total',
+            'source' => 'system',
+            'totalScoreBefore' => $healthBefore,
+            'totalScoreAfter' => $healthAfter
+        ]);
+        
+        $this->recordScoreLog($accountId, $wechatId, 'healthScore', $before['healthScore'] ?? 0, $after['healthScore'] ?? 0, [
+            'category' => 'health_total',
+            'source' => 'system',
+            'totalScoreBefore' => $healthBefore,
+            'totalScoreAfter' => $healthAfter
+        ]);
+    }
+    
+    /**
+     * 插入健康分加减分日志
+     * 
+     * @param int $accountId
+     * @param string $wechatId
+     * @param string $field
+     * @param int|null $beforeValue
+     * @param int|null $afterValue
+     * @param array $context
+     * @return void
+     */
+    private function recordScoreLog($accountId, $wechatId, $field, $beforeValue, $afterValue, array $context = [])
+    {
+        $beforeValue = (int)($beforeValue ?? 0);
+        $afterValue = (int)($afterValue ?? 0);
+        
+        if ($beforeValue === $afterValue) {
+            return;
+        }
+        
+        $extraPayload = $context['extra'] ?? null;
+        if (is_array($extraPayload)) {
+            $extraPayload = json_encode($extraPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+        } elseif (!is_string($extraPayload)) {
+            $extraPayload = null;
+        }
+        
+        $sourceId = null;
+        if (array_key_exists('sourceId', $context)) {
+            $sourceId = $context['sourceId'];
+        }
+        
+        $totalScoreBefore = null;
+        if (array_key_exists('totalScoreBefore', $context)) {
+            $totalScoreBefore = $context['totalScoreBefore'];
+        }
+        
+        $totalScoreAfter = null;
+        if (array_key_exists('totalScoreAfter', $context)) {
+            $totalScoreAfter = $context['totalScoreAfter'];
+        }
+        
+        $data = [
+            'accountId' => $accountId,
+            'wechatId' => $wechatId,
+            'field' => $field,
+            'changeValue' => $afterValue - $beforeValue,
+            'valueBefore' => $beforeValue,
+            'valueAfter' => $afterValue,
+            'category' => $context['category'] ?? null,
+            'source' => $context['source'] ?? null,
+            'sourceId' => $sourceId,
+            'extra' => $extraPayload,
+            'totalScoreBefore' => $totalScoreBefore,
+            'totalScoreAfter' => $totalScoreAfter,
+            'createTime' => time()
+        ];
+        
+        try {
+            Db::table(self::TABLE_WECHAT_ACCOUNT_SCORE_LOG)->insert($data);
+        } catch (\Exception $e) {
+            Log::error("记录健康分加减分日志失败,accountId: {$accountId}, field: {$field}, 错误: " . $e->getMessage());
+        }
+    }
+    
+    /**
+     * 根据健康分计算每日最大加人次数
+     * 公式:每日最大加人次数 = 健康分 * 0.2
+     * 
+     * @param int $healthScore 健康分
+     * @return int 每日最大加人次数
+     */
+    public function getMaxAddFriendPerDay($healthScore)
+    {
+        return (int)floor($healthScore * 0.2);
+    }
+    
+    /**
+     * 批量计算并更新多个账号的健康分
+     * 优化:使用多线程处理、优化批处理逻辑、减少日志记录
+     * 
+     * @param array $accountIds 账号ID数组(为空则处理所有账号)
+     * @param int $batchSize 每批处理数量
+     * @param bool $forceRecalculateBase 是否强制重新计算基础分
+     * @param bool $useMultiThread 是否使用多线程处理(需要pcntl扩展支持)
+     * @return array 处理结果统计
+     * @throws Exception 如果参数无效或批量处理过程中出现严重错误
+     */
+    public function batchCalculateAndUpdate($accountIds = [], $batchSize = 50, $forceRecalculateBase = false, $useMultiThread = false)
+    {
+        // 参数验证
+        if (!is_array($accountIds)) {
+            $errorMsg = "无效的账号ID数组: " . gettype($accountIds);
+            Log::error($errorMsg);
+            throw new Exception($errorMsg);
+        }
+        
+        if (!is_numeric($batchSize) || $batchSize <= 0) {
+            $errorMsg = "无效的批处理大小: {$batchSize}";
+            Log::error($errorMsg);
+            throw new Exception($errorMsg);
+        }
+        
+        // 检查是否支持多线程
+        if ($useMultiThread && !function_exists('pcntl_fork')) {
+            $useMultiThread = false;
+            Log::warning("系统不支持pcntl扩展,无法使用多线程处理,将使用单线程模式");
+        }
+        
+        try {
+            $startTime = microtime(true);
+            // 去除开始日志,减少日志空间消耗 
+               
+            
+            $stats = [
+                'total' => 0,
+                'success' => 0,
+                'failed' => 0,
+                'errors' => []
+            ];
+            
+            // 如果没有指定账号ID,则处理所有账号
+            if (empty($accountIds)) {
+                $accountIds = Db::table(self::TABLE_WECHAT_ACCOUNT)
+                    ->where('isDeleted', 0)
+                    ->column('id');
+            }
+        
+            $stats['total'] = count($accountIds);
+            
+            // 优化:减小批次大小,提高并行处理效率
+            $batchSize = min($batchSize, 50);
+            
+            // 分批处理
+            $batches = array_chunk($accountIds, $batchSize);
+            $batchCount = count($batches);
+            Log::info("分批处理,共 {$batchCount} 批");
+            
+            // 多线程处理
+            if ($useMultiThread && $batchCount > 1) {
+                $childPids = [];
+                $maxProcesses = 4; // 最大并行进程数
+                $runningProcesses = 0;
+                
+                for ($i = 0; $i < $batchCount; $i++) {
+                    // 如果达到最大进程数,等待某个子进程结束
+                    if ($runningProcesses >= $maxProcesses) {
+                        $pid = pcntl_wait($status);
+                        $runningProcesses--;
+                    }
+                    
+                    // 创建子进程
+                    $pid = pcntl_fork();
+                    
+                    if ($pid == -1) {
+                        // 创建进程失败
+                        Log::error("创建子进程失败");
+                        continue;
+                    } elseif ($pid == 0) {
+                        // 子进程
+                        $this->processBatch($batches[$i], $i, $batchCount, $forceRecalculateBase);
+                        exit(0);
+                    } else {
+                        // 父进程
+                        $childPids[] = $pid;
+                        $runningProcesses++;
+                    }
+                }
+                
+                // 等待所有子进程结束
+                foreach ($childPids as $pid) {
+                    pcntl_waitpid($pid, $status);
+                }
+                
+                Log::info("所有批次处理完成");
+            } else {
+                // 单线程处理
+                foreach ($batches as $batchIndex => $batch) {
+                    $batchStats = $this->processBatch($batch, $batchIndex, $batchCount, $forceRecalculateBase);
+                    $stats['success'] += $batchStats['success'];
+                    $stats['failed'] += $batchStats['failed'];
+                    $stats['errors'] = array_merge($stats['errors'], $batchStats['errors']);
+                }
+            }
+            
+            $endTime = microtime(true);
+            $totalDuration = round($endTime - $startTime, 2);
+            // 只在有失败时记录日志
+            if ($stats['failed'] > 0) {
+                Log::warning("批量计算健康分完成,总耗时: {$totalDuration}秒,成功: {$stats['success']},失败: {$stats['failed']}");
+            }
+            
+            return $stats;
+        } catch (\PDOException $e) {
+            $errorMsg = "批量计算健康分过程中数据库操作失败: " . $e->getMessage();
+            Log::error($errorMsg);
+            throw new Exception($errorMsg, $e->getCode(), $e);
+        } catch (\Throwable $e) {
+            $errorMsg = "批量计算健康分过程中发生严重错误: " . $e->getMessage();
+            Log::error($errorMsg);
+            throw new Exception($errorMsg, $e->getCode(), $e);
+        }
+    }
+    
+    /**
+     * 处理单个批次的账号
+     * 
+     * @param array $batch 批次账号ID数组
+     * @param int $batchIndex 批次索引
+     * @param int $batchCount 总批次数
+     * @param bool $forceRecalculateBase 是否强制重新计算基础分
+     * @return array 处理结果统计
+     */
+    private function processBatch($batch, $batchIndex, $batchCount, $forceRecalculateBase)
+    {
+        $batchStartTime = microtime(true);
+        // 去除批次开始日志,减少日志空间消耗
+        
+        $stats = [
+            'success' => 0,
+            'failed' => 0,
+            'errors' => []
+        ];
+        
+        // 优化:预先获取账号数据,减少重复查询
+        $accountIds = implode(',', $batch);
+        $accountDataMap = [];
+        if (!empty($batch)) {
+            $accountDataList = Db::table(self::TABLE_WECHAT_ACCOUNT)
+                ->where('id', 'in', $batch)
+                ->select();
+                
+            foreach ($accountDataList as $accountData) {
+                $accountDataMap[$accountData['id']] = $accountData;
+            }
+        }
+        
+        // 批量处理账号
+        foreach ($batch as $accountId) {
+            try {
+                $accountData = $accountDataMap[$accountId] ?? null;
+                $this->calculateAndUpdate($accountId, $accountData, $forceRecalculateBase);
+                $stats['success']++;
+                
+                // 减少日志记录,每10个账号记录一次进度
+                if ($stats['success'] % 10 == 0) {
+                    Log::debug("批次 " . ($batchIndex + 1) . " 已处理 {$stats['success']} 个账号");
+                }
+            } catch (Exception $e) {
+                $stats['failed']++;
+                $stats['errors'][] = [
+                    'accountId' => $accountId,
+                    'error' => $e->getMessage()
+                ];
+                Log::error("账号 {$accountId} 计算失败: " . $e->getMessage());
+            }
+        }
+        
+        $batchEndTime = microtime(true);
+        $batchDuration = round($batchEndTime - $batchStartTime, 2);
+        Log::info("第 " . ($batchIndex + 1) . "/" . $batchCount . " 批处理完成,耗时: {$batchDuration}秒," . 
+            "成功: {$stats['success']},失败: {$stats['failed']}");
+            
+        return $stats;
+    }
+    
+    /**
+     * 记录频繁事件(已废弃,改为从s2_friend_task表自动检测)
+     * 保留此方法以兼容旧代码,实际频繁检测在calculateDynamicScore中完成
+     * 
+     * @param int $accountId 账号ID
+     * @return bool
+     */
+    public function recordFrequent($accountId)
+    {
+        // 频繁检测已改为从s2_friend_task表自动检测
+        // 直接重新计算健康分即可
+        try {
+            $this->calculateAndUpdate($accountId);
+            return true;
+        } catch (\Exception $e) {
+            return false;
+        }
+    }
+    
+    /**
+     * 记录不频繁事件(用于加分)
+     * 
+     * @param int $accountId 账号ID
+     * @return bool
+     */
+    public function recordNoFrequent($accountId)
+    {
+        $scoreRecord = $this->getScoreRecord($accountId);
+        
+        if (empty($scoreRecord)) {
+            // 如果记录不存在,先创建
+            $accountData = Db::table(self::TABLE_WECHAT_ACCOUNT)
+                ->where('id', $accountId)
+                ->find();
+            
+            if (empty($accountData)) {
+                return false;
+            }
+            
+            $this->getOrCreateScoreRecord($accountId, $accountData['wechatId']);
+            $scoreRecord = $this->getScoreRecord($accountId);
+        }
+        
+        $lastNoFrequentTime = $scoreRecord['lastNoFrequentTime'] ?? null;
+        $consecutiveNoFrequentDays = $scoreRecord['consecutiveNoFrequentDays'] ?? 0;
+        $currentTime = time();
+        
+        // 如果上次不频繁时间是昨天或更早,则增加连续天数
+        if (empty($lastNoFrequentTime) || ($currentTime - $lastNoFrequentTime) >= 86400) {
+            // 如果间隔超过2天,重置为1天
+            if (!empty($lastNoFrequentTime) && ($currentTime - $lastNoFrequentTime) > 86400 * 2) {
+                $consecutiveNoFrequentDays = 1;
+            } else {
+                $consecutiveNoFrequentDays++;
+            }
+        }
+        
+        // 计算加分(连续3天及以上才加分)
+        $bonus = 0;
+        if ($consecutiveNoFrequentDays >= 3) {
+            $bonus = $consecutiveNoFrequentDays * self::BONUS_NO_FREQUENT_PER_DAY;
+        }
+        
+        $updateData = [
+            'lastNoFrequentTime' => $currentTime,
+            'consecutiveNoFrequentDays' => $consecutiveNoFrequentDays,
+            'noFrequentBonus' => $bonus,
+            'updateTime' => $currentTime
+        ];
+        
+        Db::table('s2_wechat_account_score')
+            ->where('accountId', $accountId)
+            ->update($updateData);
+        
+        // 重新计算健康分
+        $this->calculateAndUpdate($accountId);
+        
+        return true;
+    }
+    
+    /**
+     * 获取账号健康分信息
+     * 优化:使用多级缓存策略,提高缓存命中率
+     * 
+     * @param int $accountId 账号ID
+     * @param bool $useCache 是否使用缓存(默认true)
+     * @param bool $forceRecalculate 是否强制重新计算(默认false)
+     * @return array|null
+     */
+    public function getHealthScore($accountId, $useCache = true, $forceRecalculate = false)
+    {
+        // 如果强制重新计算,则不使用缓存
+        if ($forceRecalculate) {
+            Log::info("强制重新计算健康分,accountId: {$accountId}");
+            return $this->calculateAndUpdate($accountId, null, false);
+        }
+        
+        // 生成缓存键
+        $cacheKey = self::CACHE_PREFIX . 'health:' . $accountId;
+        
+        // 如果使用缓存且缓存存在,则直接返回缓存数据
+        if ($useCache && !$forceRecalculate && Cache::has($cacheKey)) {
+            $cachedData = Cache::get($cacheKey);
+            // 减少日志记录,提高性能
+            return $cachedData;
+        }
+        
+        // 从数据库获取记录
+        $scoreRecord = $this->getScoreRecord($accountId, $useCache);
+        
+        if (empty($scoreRecord)) {
+            return null;
+        }
+        
+        $healthScoreInfo = [
+            'accountId' => $scoreRecord['accountId'],
+            'wechatId' => $scoreRecord['wechatId'],
+            'healthScore' => $scoreRecord['healthScore'] ?? 0,
+            'baseScore' => $scoreRecord['baseScore'] ?? 0,
+            'baseInfoScore' => $scoreRecord['baseInfoScore'] ?? 0,
+            'friendCountScore' => $scoreRecord['friendCountScore'] ?? 0,
+            'friendCount' => $scoreRecord['friendCount'] ?? 0,
+            'dynamicScore' => $scoreRecord['dynamicScore'] ?? 0,
+            'frequentPenalty' => $scoreRecord['frequentPenalty'] ?? 0,
+            'noFrequentBonus' => $scoreRecord['noFrequentBonus'] ?? 0,
+            'banPenalty' => $scoreRecord['banPenalty'] ?? 0,
+            'maxAddFriendPerDay' => $scoreRecord['maxAddFriendPerDay'] ?? 0,
+            'baseScoreCalculated' => $scoreRecord['baseScoreCalculated'] ?? 0,
+            'lastFrequentTime' => $scoreRecord['lastFrequentTime'] ?? null,
+            'frequentCount' => $scoreRecord['frequentCount'] ?? 0,
+            'isBanned' => $scoreRecord['isBanned'] ?? 0,
+            'lastBanTime' => $scoreRecord['lastBanTime'] ?? null
+        ];
+        
+        // 如果使用缓存,则缓存健康分信息
+        if ($useCache) {
+            // 根据数据更新频率设置不同的缓存时间
+            // 如果有频繁记录或封号记录,使用短期缓存
+            $cacheTime = (!empty($scoreRecord['lastFrequentTime']) || !empty($scoreRecord['isBanned'])) 
+                ? self::CACHE_TTL_SHORT 
+                : self::CACHE_TTL;
+                
+            Cache::set($cacheKey, $healthScoreInfo, $cacheTime);
+            Log::debug("缓存健康分信息,accountId: {$accountId}, 缓存时间: {$cacheTime}秒");
+        }
+        
+        return $healthScoreInfo;
+    }
+    
+    /**
+     * 清除健康分信息缓存
+     * 
+     * @param int $accountId 账号ID
+     * @return bool 是否成功清除缓存
+     */
+    public function clearHealthScoreCache($accountId)
+    {
+        $cacheKey = self::CACHE_PREFIX . 'health:' . $accountId;
+        $result = Cache::rm($cacheKey);
+        
+        // 同时清除评分记录缓存
+        $this->clearScoreCache($accountId);
+        
+        // 减少不必要的日志记录
+        return $result;
+    }
+}
diff --git a/application/common/socket/Events.php b/application/common/socket/Events.php
new file mode 100644
index 0000000..0418e46
--- /dev/null
+++ b/application/common/socket/Events.php
@@ -0,0 +1,126 @@
+info('连接成功: {id}', [
+                'id' => $clientId,
+            ]);
+        } catch (\Exception $ex) {
+            Gateway::closeClient($clientId);
+
+            Logger::_(static::LOGGER)->info('连接错误: {id}|{error}', [
+                'id'    => $clientId,
+                'error' => $ex->getMessage() . '<' . $ex->getCode() . '>',
+            ]);
+        }
+    }
+
+    /**
+     * onMessage 事件回调
+     * 当客户端发来数据(Gateway进程收到数据)后触发
+     *
+     * @access public
+     * @param int $client_id
+     * @param mixed $data
+     * @return void
+     */
+    public static function onMessage($clientId, $data) {
+        $json = @json_decode($data, TRUE);
+        if (!empty($json)
+                AND !empty($json['type'])
+                AND is_array($json['data'])) {
+            $session = Gateway::getSession($clientId);
+            if (empty($session)) {
+                throw new \Exception('获取SESSION失败: ' . $clientId);
+            }
+
+        } else {
+            Gateway::closeClient($clientId);
+
+            Logger::_(static::LOGGER)->error('消息错误: {id}|{data}', [
+                'id'    => $clientId,
+                'data'  => $data,
+            ]);
+        }
+    }
+
+    /**
+     * onClose 事件回调 当用户断开连接时触发的方法
+     *
+     * @param integer $clientId 断开连接的客户端client_id
+     * @return void
+     */
+    public static function onClose($clientId) {
+        Gateway::setSession($clientId, []);
+        Logger::_(static::LOGGER)->info('连接关闭: {id}', [
+            'id' => $clientId,
+        ]);
+    }
+
+    /**
+     * onWorkerStart 事件回调
+     * 当businessWorker进程启动时触发。每个进程生命周期内都只会触发一次
+     *
+     * @access public
+     * @param \Workerman\Worker $businessWorker
+     * @return void
+     */
+    public static function onWorkerStart(Worker $businessWorker) {
+        Logger::_(static::LOGGER)->info('进程启动: {id}', [
+            'id' => $businessWorker->id,
+        ]);
+    }
+
+    /**
+     * onWorkerStop 事件回调
+     * 当businessWorker进程退出时触发。每个进程生命周期内都只会触发一次。
+     *
+     * @param \Workerman\Worker $businessWorker
+     * @return void
+     */
+    public static function onWorkerStop(Worker $businessWorker) {
+        Logger::_(static::LOGGER)->info('进程关闭: {id}', [
+            'id' => $businessWorker->id,
+        ]);
+    }
+}
\ No newline at end of file
diff --git a/application/common/util/AliyunOSS.php b/application/common/util/AliyunOSS.php
new file mode 100644
index 0000000..d25cf7a
--- /dev/null
+++ b/application/common/util/AliyunOSS.php
@@ -0,0 +1,79 @@
+getMessage());
+        }
+    }
+    
+    /**
+     * 上传文件到OSS
+     * @param string $filePath 本地文件路径
+     * @param string $objectName OSS对象名称
+     * @return array
+     * @throws OssException
+     */
+    public static function uploadFile($filePath, $objectName)
+    {
+        try {
+            $client = self::getClient();
+            
+            // 上传文件
+            $result = $client->uploadFile(self::BUCKET, $objectName, $filePath);
+            
+            // 获取文件访问URL
+            $url = !empty($result['oss-request-url']) ? $result['oss-request-url'] : $client->signUrl(self::BUCKET, $objectName, 3600);
+            
+            return [
+                'success' => true,
+                'url' => $url,
+                'object_name' => $objectName,
+                'size' => filesize($filePath),
+                'mime_type' => mime_content_type($filePath)
+            ];
+        } catch (OssException $e) {
+            return [
+                'success' => false,
+                'error' => $e->getMessage()
+            ];
+        }
+    }
+    
+    /**
+     * 生成OSS对象名称
+     * @param string $originalName 原始文件名
+     * @return string
+     */
+    public static function generateObjectName($originalName)
+    {
+        $ext = pathinfo($originalName, PATHINFO_EXTENSION);
+        $name = md5(uniqid(mt_rand(), true));
+        return date('Y/m/d/') . $name . '.' . $ext;
+    }
+} 
\ No newline at end of file
diff --git a/application/common/util/AliyunSMS.php b/application/common/util/AliyunSMS.php
new file mode 100644
index 0000000..e742591
--- /dev/null
+++ b/application/common/util/AliyunSMS.php
@@ -0,0 +1,65 @@
+ static::ACCESS_KEY_ID,
+            // AccessKey Secret
+            'accessKeySecret' => static::ACCESS_KEY_SECRET
+        ]);
+
+        // 访问的域名
+        $config->endpoint = 'dysmsapi.aliyuncs.com';
+
+        return new Dysmsapi($config);
+    }
+
+    /**
+     * 发送验证码
+     *
+     * @param $phoneNumbers
+     * @param $templateCode
+     * @param array $templateParam
+     * @return bool
+     */
+    static public function send($phoneNumbers, $templateCode, array $templateParam = array()) {
+        $client = static::createClient();
+        $sendSmsRequest = new SendSmsRequest([
+            'phoneNumbers'  => $phoneNumbers,
+            'signName'      => static::SIGN_NAME,
+            'templateCode'  => $templateCode,
+            'templateParam' => json_encode($templateParam)
+        ]);
+        $runtime = new RuntimeOptions([]);
+        $logFile = ROOT_PATH . DS . 'aliyun-sms.txt';
+        try {
+            $data = $client->sendSmsWithOptions($sendSmsRequest, $runtime);
+            if ($data->body->code === 'OK') {
+                return TRUE;
+            }
+
+            $logData = print_r($data, TRUE);
+        } catch (\Exception $ex) {
+            $logData = print_r($ex, TRUE);
+        }
+
+        file_put_contents($logFile, '[' . date('Y-m-d H:i:s') . ']' . PHP_EOL . $logData . PHP_EOL . PHP_EOL, FILE_APPEND);
+
+        return FALSE;
+    }
+} 
\ No newline at end of file
diff --git a/application/common/util/JwtUtil.php b/application/common/util/JwtUtil.php
new file mode 100644
index 0000000..8285fd5
--- /dev/null
+++ b/application/common/util/JwtUtil.php
@@ -0,0 +1,137 @@
+ 'HS256', // 加密算法
+        'typ' => 'JWT'    // 类型
+    ];
+
+    /**
+     * 创建JWT令牌
+     * @param array $payload 载荷信息
+     * @param int $expire 过期时间(秒),默认2小时
+     * @return string
+     */
+    public static function createToken($payload, $expire = 7200)
+    {
+        $header = self::base64UrlEncode(json_encode(self::$header, JSON_UNESCAPED_UNICODE));
+        
+        // 附加过期时间
+        $payload['exp'] = time() + $expire;
+        $payload['iat'] = time(); // 签发时间
+
+        unset($payload['passwordMd5']);
+
+        $payload = self::base64UrlEncode(json_encode($payload, JSON_UNESCAPED_UNICODE));
+        $signature = self::signature($header . '.' . $payload, self::$secret);
+        
+        return $header . '.' . $payload . '.' . $signature;
+    }
+
+    /**
+     * 验证令牌
+     * @param string $token 令牌
+     * @return array|bool 验证通过返回载荷信息,失败返回false
+     */
+    public static function verifyToken($token)
+    {
+        if (empty($token)) {
+            return false;
+        }
+
+        $tokenArray = explode('.', $token);
+        if (count($tokenArray) != 3) {
+            return false;
+        }
+
+        list($header, $payload, $signature) = $tokenArray;
+        
+        // 验证签名
+        if (self::signature($header . '.' . $payload, self::$secret) !== $signature) {
+            return false;
+        }
+
+        // 解码载荷
+        $payload = json_decode(self::base64UrlDecode($payload), true);
+        
+        // 验证是否过期
+        if (isset($payload['exp']) && $payload['exp'] < time()) {
+            return false;
+        }
+
+        return $payload;
+    }
+
+    /**
+     * 生成签名
+     * @param string $input 输入
+     * @param string $key 密钥
+     * @return string
+     */
+    private static function signature($input, $key)
+    {
+        return self::base64UrlEncode(hash_hmac('sha256', $input, $key, true));
+    }
+
+    /**
+     * URL安全的Base64编码
+     * @param string $input
+     * @return string
+     */
+    private static function base64UrlEncode($input)
+    {
+        return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($input));
+    }
+
+    /**
+     * URL安全的Base64解码
+     * @param string $input
+     * @return string
+     */
+    private static function base64UrlDecode($input)
+    {
+        $remainder = strlen($input) % 4;
+        if ($remainder) {
+            $input .= str_repeat('=', 4 - $remainder);
+        }
+        return base64_decode(str_replace(['-', '_'], ['+', '/'], $input));
+    }
+
+    /**
+     * 从请求头中获取Token
+     * @return string|null
+     */
+    public static function getRequestToken()
+    {
+        $authorization = Request::header('Authorization');
+        if (!$authorization) {
+            return null;
+        }
+        
+        // 检查Bearer前缀
+        if (strpos($authorization, 'Bearer ') !== 0) {
+            return null;
+        }
+        
+        return substr($authorization, 7);
+    }
+} 
\ No newline at end of file
diff --git a/application/common/util/PaymentUtil.php b/application/common/util/PaymentUtil.php
new file mode 100644
index 0000000..9c1dd9d
--- /dev/null
+++ b/application/common/util/PaymentUtil.php
@@ -0,0 +1,255 @@
+ $value) {
+            $pairs[] = $key . '=' . $value;
+        }
+        return implode('&', $pairs);
+    }
+
+    /**
+     * 生成MD5签名
+     * 
+     * @param string $queryString 待签名字符串
+     * @param string $secretKey 密钥
+     * @return string MD5签名
+     */
+    private static function generateMd5Sign(string $queryString, string $secretKey): string
+    {
+        $signString = $queryString . '&key=' . $secretKey;
+        return strtoupper(md5($signString));
+    }
+
+    /**
+     * 生成RSA256签名
+     * 
+     * @param string $queryString 待签名字符串
+     * @param string $privateKey 私钥
+     * @return string RSA256签名
+     */
+    private static function generateRsa256Sign(string $queryString, string $privateKey): string
+    {
+        $privateKey = self::formatPrivateKey($privateKey);
+        $key = openssl_pkey_get_private($privateKey);
+        if (!$key) {
+            throw new \Exception('RSA私钥格式错误');
+        }
+        
+        $signature = '';
+        $result = openssl_sign($queryString, $signature, $key, OPENSSL_ALGO_SHA256);
+        openssl_pkey_free($key);
+        
+        if (!$result) {
+            throw new \Exception('RSA256签名失败');
+        }
+        
+        return base64_encode($signature);
+    }
+
+    /**
+     * 生成RSA1签名
+     * 
+     * @param string $queryString 待签名字符串
+     * @param string $privateKey 私钥
+     * @return string RSA1签名
+     */
+    private static function generateRsa1Sign(string $queryString, string $privateKey): string
+    {
+        $privateKey = self::formatPrivateKey($privateKey);
+        $key = openssl_pkey_get_private($privateKey);
+        if (!$key) {
+            throw new \Exception('RSA私钥格式错误');
+        }
+        
+        $signature = '';
+        $result = openssl_sign($queryString, $signature, $key, OPENSSL_ALGO_SHA1);
+        openssl_pkey_free($key);
+        
+        if (!$result) {
+            throw new \Exception('RSA1签名失败');
+        }
+        
+        return base64_encode($signature);
+    }
+
+    /**
+     * 格式化私钥
+     * 
+     * @param string $privateKey 原始私钥
+     * @return string 格式化后的私钥
+     */
+    private static function formatPrivateKey(string $privateKey): string
+    {
+        $privateKey = str_replace(['-----BEGIN PRIVATE KEY-----', '-----END PRIVATE KEY-----', "\n", "\r"], '', $privateKey);
+        $privateKey = chunk_split($privateKey, 64, "\n");
+        return "-----BEGIN PRIVATE KEY-----\n" . $privateKey . "-----END PRIVATE KEY-----";
+    }
+
+    /**
+     * 格式化公钥
+     * 
+     * @param string $publicKey 原始公钥
+     * @return string 格式化后的公钥
+     */
+    private static function formatPublicKey(string $publicKey): string
+    {
+        $publicKey = str_replace(['-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----', "\n", "\r"], '', $publicKey);
+        $publicKey = chunk_split($publicKey, 64, "\n");
+        return "-----BEGIN PUBLIC KEY-----\n" . $publicKey . "-----END PUBLIC KEY-----";
+    }
+
+    /**
+     * 验证RSA签名
+     * 
+     * @param string $queryString 原始字符串
+     * @param string $signature 签名
+     * @param string $publicKey 公钥
+     * @param string $signType 签名类型
+     * @return bool 验证结果
+     */
+    public static function verifyRsaSign(string $queryString, string $signature, string $publicKey, string $signType = self::SIGN_TYPE_RSA_1_256): bool
+    {
+        $publicKey = self::formatPublicKey($publicKey);
+        $key = openssl_pkey_get_public($publicKey);
+        if (!$key) {
+            return false;
+        }
+        
+        $algorithm = $signType === self::SIGN_TYPE_RSA_1_1 ? OPENSSL_ALGO_SHA1 : OPENSSL_ALGO_SHA256;
+        $result = openssl_verify($queryString, base64_decode($signature), $key, $algorithm);
+        openssl_pkey_free($key);
+        
+        return $result === 1;
+    }
+
+    /**
+     * 生成随机字符串
+     * 
+     * @param int $length 长度
+     * @return string 随机字符串
+     */
+    public static function generateNonceStr(int $length = 32): string
+    {
+        $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
+        $str = '';
+        for ($i = 0; $i < $length; $i++) {
+            $str .= $chars[mt_rand(0, strlen($chars) - 1)];
+        }
+        return $str;
+    }
+
+    /**
+     * 生成时间戳
+     * 
+     * @return int 时间戳
+     */
+    public static function generateTimestamp(): int
+    {
+        return time();
+    }
+
+    /**
+     * 格式化金额(分转元)
+     * 
+     * @param int $amount 金额(分)
+     * @return string 格式化后的金额(元)
+     */
+    public static function formatAmount(int $amount): string
+    {
+        return number_format($amount / 100, 2, '.', '');
+    }
+
+    /**
+     * 解析金额(元转分)
+     * 
+     * @param string $amount 金额(元)
+     * @return int 金额(分)
+     */
+    public static function parseAmount(string $amount): int
+    {
+        return (int) round(floatval($amount) * 100);
+    }
+}
diff --git a/application/common/util/Signer.php b/application/common/util/Signer.php
new file mode 100644
index 0000000..9b68840
--- /dev/null
+++ b/application/common/util/Signer.php
@@ -0,0 +1,135 @@
+ $value) {
+            if ($key === 'sign') {
+                continue;
+            }
+            if ($value === '' || $value === null) {
+                continue;
+            }
+            $filtered[$key] = $value;
+        }
+
+        ksort($filtered, SORT_STRING);
+
+        $pairs = [];
+        foreach ($filtered as $key => $value) {
+            // 原始值拼接,不做 urlencode
+            $pairs[] = $key . '=' . (is_bool($value) ? ($value ? '1' : '0') : (string)$value);
+        }
+
+        return implode('&', $pairs);
+    }
+
+    /**
+     * MD5 签名
+     * - 若提供 secret,则原串末尾追加 &key=SECRET
+     * - 返回 32 位小写
+     *
+     * @param string $signString
+     * @param string|null $secret
+     * @return string
+     */
+    protected static function signMd5($signString, $secret = null)
+    {
+        if ($secret !== null && $secret !== '') {
+            $signString .= '&key=' . $secret;
+        }
+        return strtolower(md5($signString));
+    }
+
+    /**
+     * RSA 签名
+     *
+     * @param string $signString
+     * @param array $options 必填:private_key,可选:passphrase
+     * @param string $hashAlgo sha256|sha1
+     * @return string base64 签名
+     * @throws \InvalidArgumentException
+     */
+    protected static function signRsa($signString, array $options, $hashAlgo = 'sha256')
+    {
+        if (empty($options['private_key'])) {
+            throw new \InvalidArgumentException('RSA signing requires private_key.');
+        }
+
+        $privateKey = $options['private_key'];
+        $passphrase = isset($options['passphrase']) ? (string)$options['passphrase'] : '';
+
+        // 兼容无头尾私钥,自动包裹为 PEM
+        if (strpos($privateKey, 'BEGIN') === false) {
+            $privateKey = "-----BEGIN PRIVATE KEY-----\n" . trim(chunk_split(str_replace(["\r", "\n"], '', $privateKey), 64, "\n")) . "\n-----END PRIVATE KEY-----";
+        }
+
+        $pkeyId = openssl_pkey_get_private($privateKey, $passphrase);
+        if ($pkeyId === false) {
+            throw new \InvalidArgumentException('Invalid RSA private key or passphrase.');
+        }
+
+        $signature = '';
+        $algoConst = $hashAlgo === 'sha1' ? OPENSSL_ALGO_SHA1 : OPENSSL_ALGO_SHA256;
+        $ok = openssl_sign($signString, $signature, $pkeyId, $algoConst);
+        openssl_free_key($pkeyId);
+
+        if (!$ok) {
+            throw new \InvalidArgumentException('OpenSSL sign failed.');
+        }
+
+        return base64_encode($signature);
+    }
+}
+
+
diff --git a/application/common/validate/Auth.php b/application/common/validate/Auth.php
new file mode 100644
index 0000000..3fc8364
--- /dev/null
+++ b/application/common/validate/Auth.php
@@ -0,0 +1,46 @@
+ 'require',
+        'password' => 'require|length:6,64',
+        'code' => 'require|length:4,6',
+        'typeId' => 'require|in:1,2',
+    ];
+
+    /**
+     * 错误信息
+     * @var array
+     */
+    protected $message = [
+        'account.require' => '账号不能为空',
+        'password.require' => '密码不能为空',
+        'password.length' => '密码长度必须在6-64个字符之间',
+        'code.require' => '验证码不能为空',
+        'code.length' => '验证码长度必须在4-6个字符之间',
+        'typeId.require' => '用户类型不能为空',
+        'typeId.in' => '用户类型错误',
+    ];
+
+    /**
+     * 验证场景
+     * @var array
+     */
+    protected $scene = [
+        'login' => ['account', 'password', 'typeId'],
+        'mobile_login' => ['account', 'code', 'typeId'],
+        'refresh' => [],
+        'send_code' => ['account', 'type'],
+    ];
+} 
\ No newline at end of file
diff --git a/application/common/view/tpl/dispatch_jump.tpl b/application/common/view/tpl/dispatch_jump.tpl
new file mode 100644
index 0000000..50d63d7
--- /dev/null
+++ b/application/common/view/tpl/dispatch_jump.tpl
@@ -0,0 +1,93 @@
+{__NOLAYOUT__}
+
+
+    
+    
+    跳转提示
+    
+
+
+    
+ + +

:)

+

+ + +

:(

+

+ + +

+

+ 页面自动 跳转 等待时间: +

+
+ + + \ No newline at end of file diff --git a/application/cozeai/config/route.php b/application/cozeai/config/route.php new file mode 100644 index 0000000..ca6c702 --- /dev/null +++ b/application/cozeai/config/route.php @@ -0,0 +1,24 @@ +middleware(['jwt']); \ No newline at end of file diff --git a/application/cozeai/controller/BaseController.php b/application/cozeai/controller/BaseController.php new file mode 100644 index 0000000..20d1dc7 --- /dev/null +++ b/application/cozeai/controller/BaseController.php @@ -0,0 +1,110 @@ +apiUrl = Env::get('cozeAi.api_url'); + $this->accessToken = Env::get('cozeAi.token'); + + // 设置请求头 + $this->headers = [ + 'Authorization: Bearer ' . $this->accessToken, + 'Content-Type: application/json' + ]; + } + + + +/** + * CURL请求 + * + * @param $url 请求url地址 + * @param $method 请求方法 get post + * @param null $postfields post数据数组 + * @param array $headers 请求header信息 + * @param bool|false $debug 调试开启 默认false + * @return mixed + */ + protected function httpRequest($url, $method = "GET", $postfields = null, $headers = array(), $timeout = 30, $debug = false) + { + $method = strtoupper($method); + $ci = curl_init(); + /* Curl settings */ + // curl_setopt($ci, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); // 使用哪个版本 + curl_setopt($ci, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0"); + curl_setopt($ci, CURLOPT_CONNECTTIMEOUT, 60); /* 在发起连接前等待的时间,如果设置为0,则无限等待 */ + // curl_setopt($ci, CURLOPT_TIMEOUT, 7); /* 设置cURL允许执行的最长秒数 */ + curl_setopt($ci, CURLOPT_TIMEOUT, $timeout); /* 设置cURL允许执行的最长秒数 */ + curl_setopt($ci, CURLOPT_RETURNTRANSFER, true); + switch ($method) { + case "POST": + curl_setopt($ci, CURLOPT_POST, true); + if (!empty($postfields)) { + if (is_string($postfields) && preg_match('/^([\w\-]+=[\w\-]+(&[\w\-]+=[\w\-]+)*)$/', $postfields)) { + parse_str($postfields, $output); + $postfields = $output; + } + if (is_array($postfields)) { + $tmpdatastr = http_build_query($postfields); + } else { + $tmpdatastr = $postfields; + } + curl_setopt($ci, CURLOPT_POSTFIELDS, $tmpdatastr); + } + break; + default: + curl_setopt($ci, CURLOPT_CUSTOMREQUEST, $method); /* //设置请求方式 */ + break; + } + + $ssl = preg_match('/^https:\/\//i', $url) ? TRUE : FALSE; + curl_setopt($ci, CURLOPT_URL, $url); + if ($ssl) { + curl_setopt($ci, CURLOPT_SSL_VERIFYPEER, FALSE); // https请求 不验证证书和hosts + curl_setopt($ci, CURLOPT_SSL_VERIFYHOST, FALSE); // 不从证书中检查SSL加密算法是否存在 + // curl_setopt($ci, CURLOPT_SSLVERSION, 4); //因为之前的POODLE 病毒爆发,许多网站禁用了sslv3(nginx默认是禁用的,ssl_protocols 默认值为TLSv1 TLSv1.1 TLSv1.2;),最新使用sslv4 + } + //curl_setopt($ci, CURLOPT_HEADER, true); /*启用时会将头文件的信息作为数据流输出*/ + if (ini_get('open_basedir') == '' && ini_get('safe_mode' == 'Off')) { + curl_setopt($ci, CURLOPT_FOLLOWLOCATION, 1); + } + curl_setopt($ci, CURLOPT_MAXREDIRS, 2);/*指定最多的HTTP重定向的数量,这个选项是和CURLOPT_FOLLOWLOCATION一起使用的*/ + curl_setopt($ci, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ci, CURLINFO_HEADER_OUT, true); + /*curl_setopt($ci, CURLOPT_COOKIE, $Cookiestr); * *COOKIE带过去** */ + $response = curl_exec($ci); + $requestinfo = curl_getinfo($ci); + $http_code = curl_getinfo($ci, CURLINFO_HTTP_CODE); + if ($debug) { + echo "=====post data======\r\n"; + var_dump($postfields); + echo "=====info===== \r\n"; + print_r($requestinfo); + echo "=====response=====\r\n"; + print_r($response); + } + curl_close($ci); + return $response; + //return array($http_code, $response,$requestinfo); + } + + + +} \ No newline at end of file diff --git a/application/cozeai/controller/ConversationController.php b/application/cozeai/controller/ConversationController.php new file mode 100644 index 0000000..8882113 --- /dev/null +++ b/application/cozeai/controller/ConversationController.php @@ -0,0 +1,413 @@ +find(); + $meta_data = $conversation['meta_data'] ?? []; + if (!$exists) { + // 不存在则插入 + $data = [ + 'conversation_id' => $conversation['id'], + 'bot_id' => $bot_id, + 'created_at' => $conversation['created_at'], + 'meta_data' => $meta_data, + 'create_time' => time(), + 'update_time' => time() + ]; + + if(isset($meta_data['uid']) && !empty($meta_data['uid'])){ + $data['userId'] = $meta_data['uid']; + } + + if(isset($meta_data['companyId']) && !empty($meta_data['companyId'])){ + $data['companyId'] = $meta_data['companyId']; + } + return ConversationModel::create($data); + } else { + // 存在则更新 + return $exists->save([ + 'meta_data' => json_encode($meta_data), + 'update_time' => time() + ]); + } + } + + /** + * 获取会话列表 + */ + public function list() + { + try { + $bot_id = input('bot_id',''); + if(empty($bot_id)){ + if($is_internal){ + return json_encode([ + 'code' => 400, + 'msg' => '智能体ID不能为空', + 'data' => [] + ]); + }else{ + return errorJson('智能体ID不能为空'); + } + } + $page = input('page',1); + $limit = input('limit',20); + + $params = [ + 'bot_id' => $bot_id, + 'page_num' => $page, + 'page_size' => $limit, + 'sort_order' => 'desc' + ]; + + $result = requestCurl($this->apiUrl . "/v1/conversations", $params, 'GET', $this->headers); + $result = json_decode($result, true); + + if ($result['code'] != 0) { + if($is_internal){ + return json_encode([ + 'code' => $result['code'], + 'msg' => $result['msg'], + 'data' => [] + ]); + }else{ + return errorJson($result['msg'], $result['code']); + } + } + + // 处理返回的数据并存入数据库 + if (!empty($result['data']['conversations'])) { + foreach ($result['data']['conversations'] as $item) { + $this->saveConversation($item, $bot_id); + } + } + + return successJson($result['data'], '获取成功'); + + } catch (\Exception $e) { + return errorJson('获取对话列表失败:' . $e->getMessage()); + } + } + + /** + * 创建会话 + */ + public function create($is_internal = false) + { + try { + $bot_id = Env::get('cozeAi.bot_id'); + $userInfo = request()->userInfo; + $uid = $userInfo['id']; + $companyId = $userInfo['companyId']; + + if(empty($bot_id)){ + if($is_internal){ + return json_encode([ + 'code' => 400, + 'msg' => '智能体ID不能为空', + 'data' => [] + ]); + }else{ + return errorJson('智能体ID不能为空'); + } + } + + // 构建元数据和消息 + $meta_data = [ + 'uid' => strval($uid), + 'companyId' => strval($companyId), + ]; + $messages[] = [ + 'role' => 'assistant', + 'content' => Env::get('cozeAi.content'), + 'type' => 'answer', + 'content_type' => 'text', + ]; + + $params = [ + 'bot_id' => strval($bot_id), + 'meta_data' => $meta_data, + 'messages' => $messages, + ]; + $url = $this->apiUrl . '/v1/conversation/create'; + $result = $this->httpRequest($url, 'POST', json_encode($params,256), $this->headers); + $result = json_decode($result, true); + if ($result['code'] != 0) { + if($is_internal){ + return json_encode([ + 'code' => $result['code'], + 'msg' => $result['msg'], + 'data' => [] + ]); + }else{ + return errorJson($result['msg'], $result['code']); + } + } + + // 获取返回的对话数据并保存 + $conversation = $result['data'] ?? []; + if (!empty($conversation)) { + $this->saveConversation($conversation, $bot_id); + + + // 保存用户发送的消息 + $userMessageData = [ + 'chat_id' => $conversation['id'], + 'conversation_id' => $conversation['id'], + 'bot_id' => $bot_id, + 'content' => Env::get('cozeAi.content'), + 'content_type' => 'text', + 'role' => 'assistant', + 'type' => 'answer', + 'created_at' => time(), + 'updated_at' => time() + ]; + MessageModel::create($userMessageData); + + } + + if($is_internal){ + return json_encode([ + 'code' => 200, + 'data' => $conversation, + 'msg' => '创建成功' + ]); + }else{ + return successJson($conversation, '创建成功'); + } + + } catch (\Exception $e) { + if($is_internal){ + return json_encode([ + 'code' => 500, + 'msg' => '创建对话失败:' . $e->getMessage(), + 'data' => [] + ]); + }else{ + return errorJson('创建对话失败:' . $e->getMessage()); + } + } + } + + /** + * 创建对话 + */ + public function createChat() + { + try { + $bot_id = Env::get('cozeAi.bot_id'); + $conversation_id = input('conversation_id',''); + $question = input('question',''); + + if(empty($bot_id)){ + return errorJson('智能体ID不能为空'); + } + + if(empty($conversation_id)){ + return errorJson('会话ID不能为空'); + } + + if(empty($question)){ + return errorJson('问题不能为空'); + } + + $userInfo = request()->userInfo; + $uid = $userInfo['id']; + $companyId = $userInfo['companyId']; + + // 构建请求数据 + $params = [ + 'bot_id' => strval($bot_id), + 'user_id' => strval($uid), + 'additional_messages' => [ + [ + 'role' => 'user', + 'content' => $question, + 'type' => 'question', + 'content_type' => 'text' + ] + ], + 'stream' => false, + 'auto_save_history' => true + ]; + + $url = $this->apiUrl . '/v3/chat?conversation_id='.$conversation_id; + $result = $this->httpRequest($url, 'POST', json_encode($params,256), $this->headers); + $result = json_decode($result, true); + if ($result['code'] != 0) { + return errorJson($result['msg'], $result['code']); + } + + // 保存用户发送的消息 + $userMessageData = [ + 'chat_id' => $result['data']['id'], + 'conversation_id' => $conversation_id, + 'bot_id' => $bot_id, + 'content' => $question, + 'content_type' => 'text', + 'role' => 'user', + 'type' => 'question', + 'created_at' => $result['data']['created_at'], + 'updated_at' => $result['data']['created_at'] + ]; + MessageModel::create($userMessageData); + + + return successJson($result['data'], '发送成功'); + + } catch (\Exception $e) { + return errorJson('创建对话失败:' . $e->getMessage()); + } + } + + /** + * 查看对话详情 + */ + public function chatRetrieve() + { + $conversation_id = input('conversation_id',''); + $chat_id = input('chat_id',''); + if(empty($conversation_id) && empty($chat_id)){ + return errorJson('参数缺失'); + } + $conversation = ConversationModel::where('conversation_id', $conversation_id)->find(); + if(empty($conversation)){ + return errorJson('会话不存在'); + } + + $params = [ + 'conversation_id' => $conversation_id, + 'chat_id' => $chat_id + ]; + + $url = $this->apiUrl . '/v3/chat/retrieve?' . dataBuild($params); + + $result = $this->httpRequest($url, 'GET', [], $this->headers); + $result = json_decode($result, true); + + + if ($result['code'] != 0) { + return errorJson($result['msg'], $result['code']); + } + $status = [ + 'created' => '对话已创建', + 'in_progress' => '智能体正在处理中', + 'completed' => '智能体已完成处理,本次对话结束', + 'failed' => '对话失败', + 'requires_action' => '对话中断,需要进一步处理', + 'canceled' => '对话已取消', + ]; + + $status_msg = $status[$result['data']['status']] ?? '未知状态'; + if($result['data']['status'] == 'failed'){ + $last_error = $result['data']['last_error']; + $error_msg = $status_msg . ',错误信息:' . $last_error['msg']; + return errorJson($error_msg); + }else{ + return successJson($result['data'],$status_msg); + } + } + + + + /** + * 获取对话消息详情 + */ + public function chatMessage(){ + $conversation_id = input('conversation_id',''); + $chat_id = input('chat_id',''); + if(empty($conversation_id) && empty($chat_id)){ + return errorJson('参数缺失'); + } + $conversation = ConversationModel::where('conversation_id', $conversation_id)->find(); + if(empty($conversation)){ + return errorJson('会话不存在'); + } + + $params = [ + 'conversation_id' => $conversation_id, + 'chat_id' => $chat_id + ]; + $url = $this->apiUrl . '/v3/chat/message/list?' . dataBuild($params); + $result = $this->httpRequest($url, 'GET', [], $this->headers); + $result = json_decode($result, true); + + if ($result['code'] != 0) { + return errorJson($result['msg'], $result['code']); + } + + $data = $result['data']; + $list = []; + foreach($data as $item){ + if($item['type'] == 'answer'){ + $timestamp = $item['updated_at']; + $now = time(); + $today = strtotime(date('Y-m-d')); + $yesterday = strtotime('-1 day', $today); + $thisYear = strtotime(date('Y-01-01')); + + // 格式化时间 + if($timestamp >= $today) { + $time = date('H:i', $timestamp); + } elseif($timestamp >= $yesterday) { + $time = '昨天 ' . date('H:i', $timestamp); + } elseif($timestamp >= $thisYear) { + $time = date('m-d H:i', $timestamp); + } else { + $time = date('Y-m-d H:i', $timestamp); + } + + // 保存消息记录 + $messageData = [ + 'chat_id' => $item['id'], + 'conversation_id' => $conversation_id, + 'bot_id' => $item['bot_id'], + 'content' => $item['content'], + 'content_type' => $item['content_type'], + 'role' => $item['role'], + 'type' => $item['type'], + 'created_at' => $item['created_at'], + 'updated_at' => $item['updated_at'] + ]; + + // 检查消息是否已存在 + $exists = MessageModel::where('chat_id', $item['id'])->find(); + if (!$exists) { + MessageModel::create($messageData); + } + + $list = [ + 'id' => $item['id'], + 'type' => 'assistant', + 'content' => $item['content'], + 'time' => $time + ]; + break; + } + } + + return successJson($list, '获取成功'); + } +} \ No newline at end of file diff --git a/application/cozeai/controller/MessageController.php b/application/cozeai/controller/MessageController.php new file mode 100644 index 0000000..8c3fbd1 --- /dev/null +++ b/application/cozeai/controller/MessageController.php @@ -0,0 +1,119 @@ +userInfo; + $uid = $userInfo['id']; + $companyId = $userInfo['companyId']; + + // 获取会话ID + $conversation_id = input('conversation_id', ''); + + // 如果没有传入会话ID,则查询用户最新的会话 + if (empty($conversation_id)) { + // 查询用户是否有会话记录 + $conversation = ConversationModel::where([ + ['userId', '=', $uid], + ['companyId', '=', $companyId] + ])->order('create_time', 'desc')->find(); + + // 如果没有会话记录,创建新会话 + if (empty($conversation)) { + $conversationController = new ConversationController(); + $result = $conversationController->create(true); + $resultData = json_decode($result, true); + if ($resultData['code'] != 200) { + return errorJson('创建会话失败:' . $resultData['msg']); + } + $conversation_id = $resultData['data']['id']; + } else { + $conversation_id = $conversation['conversation_id']; + } + } else { + // 验证会话是否属于当前用户 + $conversation = ConversationModel::where([ + ['conversation_id', '=', $conversation_id], + ['userId', '=', $uid], + ['companyId', '=', $companyId] + ])->find(); + + if (empty($conversation)) { + return errorJson('会话不存在或无权访问'); + } + } + + + + // 分页参数 + $page = input('page', 1); + $limit = input('limit', 20); + + // 查询消息记录 + $messages = MessageModel::where('conversation_id', $conversation_id) + ->order('id', 'DESC') + ->page($page, $limit) + ->select() + ->each(function($item) { + // 格式化时间显示 + $timestamp = $item['created_at']; + $today = strtotime(date('Y-m-d')); + $yesterday = strtotime('-1 day', $today); + $thisYear = strtotime(date('Y-01-01')); + + if($timestamp >= $today) { + $item['show_time'] = date('H:i', $timestamp); + } elseif($timestamp >= $yesterday) { + $item['show_time'] = '昨天 ' . date('H:i', $timestamp); + } elseif($timestamp >= $thisYear) { + $item['show_time'] = date('m-d H:i', $timestamp); + } else { + $item['show_time'] = date('Y-m-d H:i', $timestamp); + } + + // 根据role设置type + if ($item['role'] == 'assistant') { + $item['type'] = 'assistant'; + } else { + $item['type'] = 'user'; + } + unset($item['role']); + + return $item; + }); + + // 对消息进行倒序处理 + $messages = array_reverse($messages->toArray()); + + // 获取总记录数 + $total = MessageModel::where('conversation_id', $conversation_id)->count(); + + $data = [ + 'list' => $messages, + 'total' => $total, + 'conversation_id' => $conversation_id + ]; + + return successJson($data, '获取成功'); + + } catch (\Exception $e) { + return errorJson('获取对话记录失败:' . $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/application/cozeai/controller/WorkspaceController.php b/application/cozeai/controller/WorkspaceController.php new file mode 100644 index 0000000..133563a --- /dev/null +++ b/application/cozeai/controller/WorkspaceController.php @@ -0,0 +1,67 @@ + $page, + 'page_size' => $limit + ]; + + $result =requestCurl($this->apiUrl . '/v1/workspaces', $params, 'GET', $this->headers); + $result = json_decode($result, true); + if ($result['code'] != 0) { + return errorJson($result['msg'],$result['code']); + } + + return successJson($result['data'], '获取成功'); + } catch (\Exception $e) { + return errorJson('获取工作区列表失败:' . $e->getMessage()); + } + } + + + + /** + * 获取智能体列表 + */ + public function getBotsList() + { + try { + $space_id = input('space_id',''); + if(empty($space_id)){ + return errorJson('Space ID不能为空'); + } + $page = input('page',1); + $limit = input('limit',20); + + $params = [ + 'space_id' => $space_id, + 'page_index' => $page, + 'page_size' => $limit + ]; + + $result = requestCurl($this->apiUrl . '/v1/space/published_bots_list', $params, 'GET', $this->headers); + $result = json_decode($result, true); + if ($result['code'] != 0) { + return errorJson($result['msg'],$result['code']); + } + return successJson($result['data'], '获取成功'); + } catch (\Exception $e) { + return errorJson('获取智能体列表失败:'.$e->getMessage()); + } + } +} \ No newline at end of file diff --git a/application/cozeai/model/Conversation.php b/application/cozeai/model/Conversation.php new file mode 100644 index 0000000..fd563f8 --- /dev/null +++ b/application/cozeai/model/Conversation.php @@ -0,0 +1,60 @@ + 'int', + 'conversation_id' => 'string', + 'workspace_id' => 'string', + 'bot_id' => 'string', + 'title' => 'string', + 'create_time' => 'datetime', + 'update_time' => 'datetime' + ]; + + // 自动写入时间戳 + protected $autoWriteTimestamp = true; + + /** + * 根据对话ID获取对话信息 + */ + public function getByConversationId($conversationId) + { + return $this->where('conversation_id', $conversationId)->find(); + } + + /** + * 保存对话信息 + */ + public function saveConversation($data) + { + $conversation = $this->getByConversationId($data['conversation_id']); + if ($conversation) { + return $this->where('conversation_id', $data['conversation_id'])->update($data); + } else { + return $this->save($data); + } + } + + /** + * 删除对话 + */ + public function deleteConversation($conversationId) + { + return $this->where('conversation_id', $conversationId)->delete(); + } +} \ No newline at end of file diff --git a/application/cozeai/model/Message.php b/application/cozeai/model/Message.php new file mode 100644 index 0000000..4befe5a --- /dev/null +++ b/application/cozeai/model/Message.php @@ -0,0 +1,24 @@ + 'integer', + 'updated_at' => 'integer', + 'create_time' => 'integer', + 'update_time' => 'integer' + ]; +} \ No newline at end of file diff --git a/application/cozeai/model/Workspace.php b/application/cozeai/model/Workspace.php new file mode 100644 index 0000000..fbf838a --- /dev/null +++ b/application/cozeai/model/Workspace.php @@ -0,0 +1,64 @@ + 'int', + 'workspace_id' => 'string', + 'name' => 'string', + 'description' => 'string', + 'create_time' => 'datetime', + 'update_time' => 'datetime' + ]; + + // 自动写入时间戳 + protected $autoWriteTimestamp = true; + protected $createTime = 'create_time'; + protected $updateTime = 'update_time'; + + /** + * 根据工作区ID获取工作区信息 + */ + public function getByWorkspaceId($workspaceId) + { + return $this->where('workspace_id', $workspaceId)->find(); + } + + /** + * 保存工作区信息 + */ + public function saveWorkspace($data) + { + $workspace = $this->getByWorkspaceId($data['workspace_id']); + + if ($workspace) { + // 更新 + return $this->where('workspace_id', $data['workspace_id'])->update($data); + } else { + // 新增 + return $this->save($data); + } + } + + /** + * 删除工作区 + */ + public function deleteWorkspace($workspaceId) + { + return $this->where('workspace_id', $workspaceId)->delete(); + } +} \ No newline at end of file diff --git a/application/cunkebao/config/route.php b/application/cunkebao/config/route.php new file mode 100644 index 0000000..07a9e8f --- /dev/null +++ b/application/cunkebao/config/route.php @@ -0,0 +1,275 @@ +middleware(['jwt']); + + + + +Route::group('v1/api/scenarios', function () { + Route::any('', 'app\cunkebao\controller\plan\PostExternalApiV1Controller@index'); +}); + + +//小程序 +Route::group('v1/frontend', function () { + Route::group('business/poster', function () { + Route::post('getone', 'app\cunkebao\controller\plan\PosterWeChatMiniProgram@getPosterTaskData'); + Route::post('decryptphone', 'app\cunkebao\controller\plan\PosterWeChatMiniProgram@getPhoneNumber'); + //Route::post('decryptphones', 'app\cunkebao\controller\plan\PosterWeChatMiniProgram@decryptphones'); + }); + Route::post('business/form/importsave', 'app\cunkebao\controller\plan\PosterWeChatMiniProgram@decryptphones'); + + // 分销渠道注册(H5扫码) + Route::group('distribution/channel', function () { + Route::get('register', 'app\cunkebao\controller\distribution\ChannelController@registerByQrCode'); // H5页面(GET显示表单) + Route::post('register', 'app\cunkebao\controller\distribution\ChannelController@registerByQrCode'); // 提交渠道信息(POST) + }); + + // 分销渠道用户端(无需JWT认证,通过渠道编码访问) + Route::group('distribution/user', function () { + Route::post('login', 'app\cunkebao\controller\distribution\ChannelUserController@login'); // 渠道登录 + Route::get('home', 'app\cunkebao\controller\distribution\ChannelUserController@index'); // 获取渠道首页数据 + Route::get('revenue-records', 'app\cunkebao\controller\distribution\ChannelUserController@revenueRecords'); // 获取收益明细列表 + Route::get('withdrawal-records', 'app\cunkebao\controller\distribution\ChannelUserController@withdrawalRecords'); // 获取提现明细列表 + Route::post('change-password', 'app\cunkebao\controller\distribution\ChannelUserController@changePassword'); // 修改密码 + }); +}); + + + + +return []; \ No newline at end of file diff --git a/application/cunkebao/controller/AiKnowledgeBaseController.php b/application/cunkebao/controller/AiKnowledgeBaseController.php new file mode 100644 index 0000000..06a95c8 --- /dev/null +++ b/application/cunkebao/controller/AiKnowledgeBaseController.php @@ -0,0 +1,707 @@ +getUserInfo('companyId'); + if (empty($companyId)) { + return ResponseHelper::error('公司信息获取失败'); + } + + // 获取分页参数 + $page = $this->request->param('page', 1); + $pageSize = $this->request->param('pageSize', 20); + $includeSystem = $this->request->param('includeSystem', 1); // 是否包含系统类型 + + // 构建查询条件 + $where = [['isDel', '=', 0]]; + + if ($includeSystem == 1) { + // 包含系统类型和本公司创建的类型 + $where[] = ['companyId', 'in', [$companyId, 0]]; + } else { + // 只显示本公司创建的类型 + $where[] = ['companyId', '=', $companyId]; + $where[] = ['type', '=', AiKnowledgeBaseType::TYPE_USER]; + } + + // 统计开启的类型总数 + $enabledCountWhere = $where; + $enabledCountWhere[] = ['status', '=', 1]; + $enabledCount = AiKnowledgeBaseType::where($enabledCountWhere)->count(); + + // 查询数据 + $list = AiKnowledgeBaseType::where($where) + ->order('type', 'asc') // 系统类型排在前面 + ->order('createTime', 'desc') + ->paginate($pageSize, false, ['page' => $page]); + + // 为每个类型添加素材数量统计 + $listData = $list->toArray(); + foreach ($listData['data'] as &$item) { + // 统计该类型下的知识库数量(素材数量) + $item['materialCount'] = AiKnowledgeBase::where([ + ['typeId', '=', $item['id']], + ['isDel', '=', 0] + ])->count(); + } + + // 重新构造返回数据 + $result = [ + 'total' => $listData['total'], + 'data' => $listData['data'], + 'enabledCount' => $enabledCount, // 开启的类型总数 + ]; + + return ResponseHelper::success($result, '获取成功'); + + } catch (\Exception $e) { + return ResponseHelper::error('系统异常:' . $e->getMessage()); + } + } + + /** + * 添加知识库类型 + * + * @return \think\response\Json + */ + public function addType() + { + try { + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($companyId)) { + return ResponseHelper::error('公司信息获取失败'); + } + + // 获取参数 + $name = $this->request->param('name', ''); + $description = $this->request->param('description', ''); + $label = $this->request->param('label', []); + $prompt = $this->request->param('prompt', ''); + $status = $this->request->param('status', 1); // 默认启用 + + // 参数验证 + if (empty($name)) { + return ResponseHelper::error('类型名称不能为空'); + } + + // 检查名称是否重复 + $exists = AiKnowledgeBaseType::where([ + ['companyId', '=', $companyId], + ['name', '=', $name], + ['isDel', '=', 0] + ])->find(); + + if ($exists) { + return ResponseHelper::error('该类型名称已存在'); + } + + // 创建类型 + $typeModel = new AiKnowledgeBaseType(); + $data = [ + 'type' => AiKnowledgeBaseType::TYPE_USER, + 'name' => $name, + 'description' => $description, + 'label' => json_encode($label,256), + 'prompt' => $prompt, + 'status' => $status, + 'companyId' => $companyId, + 'userId' => $userId, + 'createTime' => time(), + 'updateTime' => time(), + 'isDel' => 0 + ]; + + if ($typeModel->save($data)) { + return ResponseHelper::success(['id' => $typeModel->id], '添加成功'); + } else { + return ResponseHelper::error('添加失败'); + } + + } catch (\Exception $e) { + return ResponseHelper::error('系统异常:' . $e->getMessage()); + } + } + + /** + * 编辑知识库类型 + * + * @return \think\response\Json + */ + public function editType() + { + try { + $companyId = $this->getUserInfo('companyId'); + if (empty($companyId)) { + return ResponseHelper::error('公司信息获取失败'); + } + + // 获取参数 + $id = $this->request->param('id', 0); + $name = $this->request->param('name', ''); + $description = $this->request->param('description', ''); + $label = $this->request->param('label', []); + $prompt = $this->request->param('prompt', ''); + $status = $this->request->param('status', ''); + + // 参数验证 + if (empty($id)) { + return ResponseHelper::error('类型ID不能为空'); + } + + if (empty($name)) { + return ResponseHelper::error('类型名称不能为空'); + } + + // 查找类型 + $typeModel = AiKnowledgeBaseType::where([ + ['id', '=', $id], + ['isDel', '=', 0] + ])->find(); + + if (!$typeModel) { + return ResponseHelper::error('类型不存在'); + } + + // 检查是否为系统类型 + if ($typeModel->isSystemType()) { + return ResponseHelper::error('系统类型不允许编辑'); + } + + // 检查权限(只能编辑本公司的类型) + if ($typeModel->companyId != $companyId) { + return ResponseHelper::error('无权限编辑该类型'); + } + + // 检查名称是否重复(排除自己) + $exists = AiKnowledgeBaseType::where([ + ['companyId', '=', $companyId], + ['name', '=', $name], + ['id', '<>', $id], + ['isDel', '=', 0] + ])->find(); + + if ($exists) { + return ResponseHelper::error('该类型名称已存在'); + } + + // 更新数据 + $typeModel->name = $name; + $typeModel->description = $description; + $typeModel->label = json_encode($label,256); + $typeModel->prompt = $prompt; + if ($status !== '') { + $typeModel->status = $status; + } + $typeModel->updateTime = time(); + + if ($typeModel->save()) { + return ResponseHelper::success([], '更新成功'); + } else { + return ResponseHelper::error('更新失败'); + } + + } catch (\Exception $e) { + return ResponseHelper::error('系统异常:' . $e->getMessage()); + } + } + + /** + * 修改知识库类型状态 + * + * @return \think\response\Json + */ + public function updateTypeStatus() + { + try { + $companyId = $this->getUserInfo('companyId'); + if (empty($companyId)) { + return ResponseHelper::error('公司信息获取失败'); + } + + // 获取参数 + $id = $this->request->param('id', 0); + $status = $this->request->param('status', -1); + + // 参数验证 + if (empty($id)) { + return ResponseHelper::error('类型ID不能为空'); + } + + if ($status != 0 && $status != 1) { + return ResponseHelper::error('状态参数错误'); + } + + // 查找类型 + $typeModel = AiKnowledgeBaseType::where([ + ['id', '=', $id], + ['isDel', '=', 0] + ])->find(); + + if (!$typeModel) { + return ResponseHelper::error('类型不存在'); + } + + // 检查是否为系统类型 + if ($typeModel->isSystemType()) { + return ResponseHelper::error('系统类型不允许修改状态'); + } + + // 检查权限(只能修改本公司的类型) + if ($typeModel->companyId != $companyId) { + return ResponseHelper::error('无权限修改该类型'); + } + + // 更新状态 + $typeModel->status = $status; + $typeModel->updateTime = time(); + + if ($typeModel->save()) { + $message = $status == 0 ? '禁用成功' : '启用成功'; + return ResponseHelper::success([], $message); + } else { + return ResponseHelper::error('操作失败'); + } + + } catch (\Exception $e) { + return ResponseHelper::error('系统异常:' . $e->getMessage()); + } + } + + /** + * 获取知识库类型详情 + * + * @return \think\response\Json + */ + public function detailType() + { + try { + $companyId = $this->getUserInfo('companyId'); + if (empty($companyId)) { + return ResponseHelper::error('公司信息获取失败'); + } + + // 获取参数 + $id = $this->request->param('id', 0); + + // 参数验证 + if (empty($id)) { + return ResponseHelper::error('类型ID不能为空'); + } + + // 查找类型 + $typeModel = AiKnowledgeBaseType::where([ + ['id', '=', $id], + ['isDel', '=', 0] + ])->find(); + + if (!$typeModel) { + return ResponseHelper::error('类型不存在'); + } + + // 检查权限(系统类型或本公司的类型都可以查看) + if ($typeModel->companyId != 0 && $typeModel->companyId != $companyId) { + return ResponseHelper::error('无权限查看该类型'); + } + + return ResponseHelper::success($typeModel, '获取成功'); + + } catch (\Exception $e) { + return ResponseHelper::error('系统异常:' . $e->getMessage()); + } + } + + /** + * 删除知识库类型 + * + * @return \think\response\Json + */ + public function deleteType() + { + try { + $companyId = $this->getUserInfo('companyId'); + if (empty($companyId)) { + return ResponseHelper::error('公司信息获取失败'); + } + + // 获取参数 + $id = $this->request->param('id', 0); + + // 参数验证 + if (empty($id)) { + return ResponseHelper::error('类型ID不能为空'); + } + + // 查找类型 + $typeModel = AiKnowledgeBaseType::where([ + ['id', '=', $id], + ['isDel', '=', 0] + ])->find(); + + if (!$typeModel) { + return ResponseHelper::error('类型不存在'); + } + + // 检查是否为系统类型 + if ($typeModel->isSystemType()) { + return ResponseHelper::error('系统类型不允许删除'); + } + + // 检查权限(只能删除本公司的类型) + if ($typeModel->companyId != $companyId) { + return ResponseHelper::error('无权限删除该类型'); + } + + // 检查是否有关联的知识库 + $hasKnowledge = AiKnowledgeBase::where([ + ['typeId', '=', $id], + ['isDel', '=', 0] + ])->count(); + + if ($hasKnowledge > 0) { + return ResponseHelper::error('该类型下存在知识库,无法删除'); + } + + // 软删除 + $typeModel->isDel = 1; + $typeModel->delTime = time(); + + if ($typeModel->save()) { + return ResponseHelper::success([], '删除成功'); + } else { + return ResponseHelper::error('删除失败'); + } + + } catch (\Exception $e) { + return ResponseHelper::error('系统异常:' . $e->getMessage()); + } + } + + // ==================== 知识库管理 ==================== + + /** + * 获取知识库列表 + * + * @return \think\response\Json + */ + public function getList() + { + try { + $companyId = $this->getUserInfo('companyId'); + if (empty($companyId)) { + return ResponseHelper::error('公司信息获取失败'); + } + + // 获取分页参数 + $page = $this->request->param('page', 1); + $pageSize = $this->request->param('pageSize', 20); + $typeId = $this->request->param('typeId', 0); // 类型筛选 + $keyword = $this->request->param('keyword', ''); // 关键词搜索 + + // 构建查询条件 + $where = [ + ['isDel', '=', 0], + ['companyId', '=', $companyId] + ]; + + if ($typeId > 0) { + $where[] = ['typeId', '=', $typeId]; + } + + if (!empty($keyword)) { + $where[] = ['name', 'like', '%' . $keyword . '%']; + } + + // 查询数据 + $list = AiKnowledgeBase::where($where) + ->with(['type']) + ->order('createTime', 'desc') + ->paginate($pageSize, false, ['page' => $page]); + + foreach ($list as &$v){ + $v['size'] = 0; + } + unset($v); + + return ResponseHelper::success($list, '获取成功'); + + } catch (\Exception $e) { + return ResponseHelper::error('系统异常:' . $e->getMessage()); + } + } + + /** + * 添加知识库 + * + * @return \think\response\Json + */ + public function add() + { + try { + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($companyId)) { + return ResponseHelper::error('公司信息获取失败'); + } + + $datasetId = AiSettingsModel::where(['companyId' => $companyId])->value('datasetId'); + + + // 获取参数 + $typeId = $this->request->param('typeId', 0); + $name = $this->request->param('name', ''); + $label = $this->request->param('label', []); + $fileUrl = $this->request->param('fileUrl', ''); + + // 参数验证 + if (empty($typeId)) { + return ResponseHelper::error('请选择知识库类型'); + } + + if (empty($name)) { + return ResponseHelper::error('知识库名称不能为空'); + } + + if (empty($fileUrl)) { + return ResponseHelper::error('文件地址不能为空'); + } + + // 检查类型是否存在 + $typeExists = AiKnowledgeBaseType::where([ + ['id', '=', $typeId], + ['isDel', '=', 0] + ])->find(); + + if (!$typeExists) { + return ResponseHelper::error('知识库类型不存在'); + } + + // 创建知识库 + $knowledgeModel = new AiKnowledgeBase(); + $data = [ + 'typeId' => $typeId, + 'name' => $name, + 'label' => json_encode($label, 256), + 'fileUrl' => $fileUrl, + 'companyId' => $companyId, + 'userId' => $userId, + 'createTime' => time(), + 'updateTime' => time(), + 'isDel' => 0 + ]; + + if ($knowledgeModel->save($data)) { + if (!empty($datasetId)) { + $createDocumentData = [ + 'filePath' => $fileUrl, + 'fileName' => $name, + 'dataset_id' => $datasetId + ]; + $cozeAI = new CozeAI(); + $result = $cozeAI->createDocument($createDocumentData); + $result = json_decode($result, true); + if ($result['code'] == 200) { + $documentId = $result['data'][0]['document_id']; + AiKnowledgeBase::where('id', $knowledgeModel->id)->update(['documentId' => $documentId, 'updateTime' => time()]); + AiSettingsModel::where(['companyId' => $companyId])->update(['isRelease' => 0,'updateTime' => time()]); + } + } + return ResponseHelper::success(['id' => $knowledgeModel->id], '添加成功'); + } else { + return ResponseHelper::error('添加失败'); + } + + } catch (\Exception $e) { + return ResponseHelper::error('系统异常:' . $e->getMessage()); + } + } + + /** + * 编辑知识库 + * + * @return \think\response\Json + */ + public function edit() + { + try { + $companyId = $this->getUserInfo('companyId'); + if (empty($companyId)) { + return ResponseHelper::error('公司信息获取失败'); + } + + // 获取参数 + $id = $this->request->param('id', 0); + $typeId = $this->request->param('typeId', 0); + $name = $this->request->param('name', ''); + $label = $this->request->param('label', []); + $fileUrl = $this->request->param('fileUrl', ''); + + // 参数验证 + if (empty($id)) { + return ResponseHelper::error('知识库ID不能为空'); + } + + if (empty($typeId)) { + return ResponseHelper::error('请选择知识库类型'); + } + + if (empty($name)) { + return ResponseHelper::error('知识库名称不能为空'); + } + + // 查找知识库 + $knowledgeModel = AiKnowledgeBase::where([ + ['id', '=', $id], + ['companyId', '=', $companyId], + ['isDel', '=', 0] + ])->find(); + + if (!$knowledgeModel) { + return ResponseHelper::error('知识库不存在或无权限编辑'); + } + + // 检查类型是否存在 + $typeExists = AiKnowledgeBaseType::where([ + ['id', '=', $typeId], + ['isDel', '=', 0] + ])->find(); + + if (!$typeExists) { + return ResponseHelper::error('知识库类型不存在'); + } + + // 更新数据 + $knowledgeModel->typeId = $typeId; + $knowledgeModel->name = $name; + $knowledgeModel->label = json_encode($label, 256); + if (!empty($fileUrl)) { + $knowledgeModel->fileUrl = $fileUrl; + } + $knowledgeModel->updateTime = time(); + + if ($knowledgeModel->save()) { + return ResponseHelper::success([], '更新成功'); + } else { + return ResponseHelper::error('更新失败'); + } + + } catch (\Exception $e) { + return ResponseHelper::error('系统异常:' . $e->getMessage()); + } + } + + /** + * 删除知识库 + * + * @return \think\response\Json + */ + public function delete() + { + try { + $companyId = $this->getUserInfo('companyId'); + if (empty($companyId)) { + return ResponseHelper::error('公司信息获取失败'); + } + + // 获取参数 + $id = $this->request->param('id', 0); + + // 参数验证 + if (empty($id)) { + return ResponseHelper::error('知识库ID不能为空'); + } + + // 查找知识库 + $knowledgeModel = AiKnowledgeBase::where([ + ['id', '=', $id], + ['companyId', '=', $companyId], + ['isDel', '=', 0] + ])->find(); + + if (!$knowledgeModel) { + return ResponseHelper::error('知识库不存在或无权限删除'); + } + + // 软删除 + $knowledgeModel->isDel = 1; + $knowledgeModel->delTime = time(); + + if ($knowledgeModel->save()) { + if (!empty($knowledgeModel->documentId)){ + $cozeAI = new CozeAI(); + $cozeAI->deleteDocument([$knowledgeModel->documentId]); + AiSettingsModel::where(['companyId' => $companyId])->update(['isRelease' => 0,'updateTime' => time()]); + } + return ResponseHelper::success([], '删除成功'); + } else { + return ResponseHelper::error('删除失败'); + } + + } catch (\Exception $e) { + return ResponseHelper::error('系统异常:' . $e->getMessage()); + } + } + + /** + * 获取知识库详情 + * + * @return \think\response\Json + */ + public function detail() + { + try { + $companyId = $this->getUserInfo('companyId'); + if (empty($companyId)) { + return ResponseHelper::error('公司信息获取失败'); + } + + // 获取参数 + $id = $this->request->param('id', 0); + + // 参数验证 + if (empty($id)) { + return ResponseHelper::error('知识库ID不能为空'); + } + + // 查找知识库 + $knowledge = AiKnowledgeBase::where([ + ['id', '=', $id], + ['companyId', '=', $companyId], + ['isDel', '=', 0] + ])->with(['type'])->find(); + + if (!$knowledge) { + return ResponseHelper::error('知识库不存在或无权限查看'); + } + + return ResponseHelper::success($knowledge, '获取成功'); + + } catch (\Exception $e) { + return ResponseHelper::error('系统异常:' . $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/AiSettingsController.php b/application/cunkebao/controller/AiSettingsController.php new file mode 100644 index 0000000..429410b --- /dev/null +++ b/application/cunkebao/controller/AiSettingsController.php @@ -0,0 +1,433 @@ +getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($companyId)) { + return ResponseHelper::error('公司信息获取失败'); + } + + // 查找公司AI设置 + $settings = $this->getOrCreateAiSettings($companyId, $userId); + + if (!$settings) { + return ResponseHelper::error('AI设置初始化失败'); + } + + // 确保智能体已创建 + if (empty($settings->botId)) { + $settings->releaseTime = 0; + $botCreated = $this->createBot($settings); + if (!$botCreated) { + return ResponseHelper::error('智能体创建失败'); + } + } + + // 确保知识库已创建 + if (empty($settings->datasetId)) { + $settings->releaseTime = 0; + $knowledgeCreated = $this->createKnowledge($settings); + if (!$knowledgeCreated) { + return ResponseHelper::error('知识库创建失败'); + } + } + if (!empty($settings->botId) && !empty($settings->datasetId) && $settings->releaseTime <= 0) { + $cozeAI = new CozeAI(); + $config = json_decode($settings->config,true); + $config['bot_id'] = $settings->botId; + $config['dataset_ids'] = [$settings->datasetId]; + $cozeAI->updateBot($config); + } + + // 解析配置信息 + $settings->config = json_decode($settings->config, true); + + return ResponseHelper::success($settings, 'AI设置初始化成功'); + + } catch (\Exception $e) { + return ResponseHelper::error('系统异常:' . $e->getMessage()); + } + } + + /** + * 获取或创建AI设置 + * + * @param int $companyId 公司ID + * @param int $userId 用户ID + * @return AiSettingsModel|false + */ + private function getOrCreateAiSettings($companyId, $userId) + { + // 查找现有设置 + $settings = AiSettingsModel::where(['companyId' => $companyId])->find(); + + if (empty($settings)) { + // 获取公司信息 + $company = CompanyModel::where('id', $companyId)->find(); + if (empty($company)) { + return false; + } + + // 创建默认配置 + $config = $this->getDefaultConfig($company['name']); + + // 保存AI设置 + $settings = $this->saveAiSettings($companyId, $userId, $config); + } + + return $settings; + } + + /** + * 获取默认AI配置 + * + * @param string $companyName 公司名称 + * @return array + */ + private function getDefaultConfig($companyName) + { + return [ + 'name' => $companyName, + 'model_id' => '1737521813', // 默认模型ID + 'prompt_info' => $this->getDefaultPrompt() + ]; + } + + /** + * 获取默认提示词 + * + * @return string + */ + private function getDefaultPrompt() + { + return '# 角色 +你是一位全能知识客服,作为专业的客服智能体,具备全面的知识储备,能够回答用户提出的各类问题。在回答问题前,会仔细查阅知识库内容,并且始终严格遵守中国法律法规。 + +## 技能 +### 技能 1: 回答用户问题 +1. 当用户提出问题时,首先在知识库中进行搜索查找相关信息。 +2. 依据知识库中的内容,为用户提供准确、清晰、完整的回答。 + +## 限制 +- 仅依据知识库内容回答问题,对于知识库中没有的信息,如实告知用户无法回答。 +- 回答必须严格遵循中国法律法规,不得出现任何违法违规内容。 +- 回答需简洁明了,避免冗长复杂的表述(尽量在100字内)。 +- 适当加些表情点缀。'; + } + + /** + * 保存AI设置到数据库 + * + * @param int $companyId 公司ID + * @param int $userId 用户ID + * @param array $config 配置信息 + * @return AiSettingsModel|false + */ + private function saveAiSettings($companyId, $userId, $config) + { + $data = [ + 'companyId' => $companyId, + 'userId' => $userId, + 'config' => json_encode($config, JSON_UNESCAPED_UNICODE), + 'createTime' => time(), + 'updateTime' => time(), + 'botId' => 0, + 'datasetId' => 0, + ]; + + $aiSettingsModel = new AiSettingsModel(); + $result = $aiSettingsModel->save($data); + + if ($result) { + return AiSettingsModel::where(['companyId' => $companyId])->find(); + } + + return false; + } + + /** + * 创建AI智能体 + * + * @param AiSettingsModel $settings AI设置对象 + * @return bool + */ + private function createBot($settings) + { + if (empty($settings)) { + return false; + } + + try { + $config = json_decode($settings->config, true); + if (empty($config)) { + return false; + } + + // 调用CozeAI创建智能体 + $cozeAI = new CozeAI(); + $result = $cozeAI->createBot($config); + $result = json_decode($result, true); + + if ($result['code'] != 200) { + \think\facade\Log::error('智能体创建失败:' . ($result['msg'] ?? '未知错误')); + return false; + } + + // 更新智能体ID + $settings->botId = $result['data']['bot_id']; + $settings->updateTime = time(); + + return $settings->save(); + + } catch (\Exception $e) { + \think\facade\Log::error('创建智能体异常:' . $e->getMessage()); + return false; + } + } + + /** + * 创建知识库 + * + * @param AiSettingsModel $settings AI设置对象 + * @return bool + */ + private function createKnowledge($settings) + { + if (empty($settings)) { + return false; + } + + try { + $config = json_decode($settings->config, true); + if (empty($config)) { + return false; + } + + // 调用CozeAI创建知识库 + $cozeAI = new CozeAI(); + $result = $cozeAI->createKnowledge(['name' => $config['name']]); + $result = json_decode($result, true); + + if ($result['code'] != 200) { + \think\facade\Log::error('知识库创建失败:' . ($result['msg'] ?? '未知错误')); + return false; + } + + // 更新知识库ID + $settings->datasetId = $result['data']['dataset_id']; + $settings->updateTime = time(); + + return $settings->save(); + + } catch (\Exception $e) { + \think\facade\Log::error('创建知识库异常:' . $e->getMessage()); + return false; + } + } + + /** + * 更新AI配置 + * + * @return \think\response\Json + */ + public function updateConfig() + { + try { + $companyId = $this->getUserInfo('companyId'); + if (empty($companyId)) { + return ResponseHelper::error('公司信息获取失败'); + } + + // 获取请求参数 + $config = $this->request->param('config', []); + if (empty($config)) { + return ResponseHelper::error('配置参数不能为空'); + } + + // 查找现有设置 + $settings = AiSettingsModel::where(['companyId' => $companyId])->find(); + if (empty($settings)) { + return ResponseHelper::error('AI设置不存在,请先初始化'); + } + + // 更新配置 + $settings->config = json_encode($config, JSON_UNESCAPED_UNICODE); + $settings->updateTime = time(); + + if ($settings->save()) { + return ResponseHelper::success([], '配置更新成功'); + } else { + return ResponseHelper::error('配置更新失败'); + } + + } catch (\Exception $e) { + return ResponseHelper::error('系统异常:' . $e->getMessage()); + } + } + + /** + * 获取AI设置详情 + * + * @return \think\response\Json + */ + public function getSettings() + { + try { + $companyId = $this->getUserInfo('companyId'); + if (empty($companyId)) { + return ResponseHelper::error('公司信息获取失败'); + } + + $settings = AiSettingsModel::where(['companyId' => $companyId])->find(); + if (empty($settings)) { + return ResponseHelper::error('AI设置不存在'); + } + + // 解析配置信息 + $settings->config = json_decode($settings->config, true); + + return ResponseHelper::success($settings, '获取成功'); + + } catch (\Exception $e) { + return ResponseHelper::error('系统异常:' . $e->getMessage()); + } + } + + + /** + * 发布智能体 + * @return \think\response\Json + * @throws \Exception + */ + public function release() + { + $companyId = $this->getUserInfo('companyId'); + $settings = AiSettingsModel::where(['companyId' => $companyId])->find(); + if (!empty($settings->isRelease)) { + return ResponseHelper::success('', '已发布,无需重复发布'); + } + + $cozeAI = new CozeAI(); + $res = $cozeAI->botPublish(['bot_id' => $settings->botId]); + $res = json_decode($res, true); + + if ($res['code'] != 200) { + $msg = '发布失败失败:' . ($res['msg'] ?? '未知错误'); + return ResponseHelper::error($msg); + } + $settings->isRelease = 1; + $settings->releaseTime = time(); + $settings->save(); + return ResponseHelper::success('', '发布成功'); + } + + /** + * 保存统一提示词 + * 先更新数据库,再调用CozeAI接口更新智能体 + * + * @return \think\response\Json + */ + public function savePrompt() + { + try { + $companyId = $this->getUserInfo('companyId'); + if (empty($companyId)) { + return ResponseHelper::error('公司信息获取失败'); + } + + // 获取提示词参数 + $promptInfo = $this->request->param('promptInfo', ''); + if (empty($promptInfo)) { + return ResponseHelper::error('提示词内容不能为空'); + } + + // 查找AI设置 + $settings = AiSettingsModel::where(['companyId' => $companyId])->find(); + if (empty($settings)) { + return ResponseHelper::error('AI设置不存在,请先初始化'); + } + + // 检查智能体是否已创建 + if (empty($settings->botId)) { + return ResponseHelper::error('智能体未创建,请先初始化AI设置'); + } + + // 解析现有配置 + $config = json_decode($settings->config, true); + if (!is_array($config)) { + $config = []; + } + + // 更新提示词 + $config['prompt_info'] = $promptInfo; + + // 第一步:更新数据库 + $settings->config = json_encode($config, JSON_UNESCAPED_UNICODE); + $settings->isRelease = 0; // 标记为未发布状态 + $settings->updateTime = time(); + + if (!$settings->save()) { + return ResponseHelper::error('数据库更新失败'); + } + + // 第二步:调用CozeAI接口更新智能体 + try { + $cozeAI = new CozeAI(); + + // 参考 init 方法的参数格式,传递完整的 config + $updateData = $config; + $updateData['bot_id'] = $settings->botId; + + // 如果有知识库,也一并传入 + if (!empty($settings->datasetId)) { + $updateData['dataset_ids'] = [$settings->datasetId]; + } + + $result = $cozeAI->updateBot($updateData); + $result = json_decode($result, true); + + if ($result['code'] != 200) { + \think\facade\Log::error('更新智能体提示词失败:' . json_encode($result)); + return ResponseHelper::error('更新智能体失败:' . ($result['msg'] ?? '未知错误')); + } + + return ResponseHelper::success([ + 'prompt_info' => $promptInfo, + 'isRelease' => 0 + ], '提示词保存成功,请重新发布智能体'); + + } catch (\Exception $e) { + \think\facade\Log::error('调用CozeAI更新接口异常:' . $e->getMessage()); + return ResponseHelper::error('更新智能体接口调用失败:' . $e->getMessage()); + } + + } catch (\Exception $e) { + \think\facade\Log::error('保存提示词异常:' . $e->getMessage()); + return ResponseHelper::error('系统异常:' . $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/BaseController.php b/application/cunkebao/controller/BaseController.php new file mode 100644 index 0000000..f3875d5 --- /dev/null +++ b/application/cunkebao/controller/BaseController.php @@ -0,0 +1,163 @@ +classTable = $classTable; + + parent::__construct(); + } + + /** + * 初始化 + */ + protected function initialize() + { + parent::initialize(); + + date_default_timezone_set('Asia/Shanghai'); + } + + /** + * 获取用户信息 + * + * @param string $column + * @return mixed + * @throws \Exception + */ + protected function getUserInfo(?string $column = null) + { + $user = $this->request->userInfo; + + if (!$user) { + throw new \Exception('未授权访问,缺少有效的身份凭证', 401); + } + + return $column ? $user[$column] : $user; + } + + + public function editUserInfo() + { + $userId = $this->request->param('userId', ''); + $nickname = $this->request->param('nickname', ''); + $avatar = $this->request->param('avatar', ''); + $phone = $this->request->param('phone', ''); + $companyId = $this->getUserInfo('companyId'); + if (empty($userId)) { + return ResponseHelper::error('用户id不能为空'); + } + + if (empty($nickname) && empty($avatar) && empty($phone)) { + return ResponseHelper::error('修改的用户信息不能为空'); + } + + $user = Db::name('users')->where(['id' => $userId, 'companyId' => $companyId])->find(); + if (empty($user)) { + return ResponseHelper::error('用户不存在'); + } + + $user2 = Db::name('users')->where(['phone' => $phone])->find(); + if (!empty($user2) && $user2['id'] != $userId) { + return ResponseHelper::error('修改的手机号已存在'); + } + + $data = [ + 'id' => $user['s2_accountId'], + ]; + + if (!empty($nickname)) { + $data['nickname'] = $nickname; + } + if (!empty($avatar)) { + $data['avatar'] = $avatar; + } + if (!empty($phone)) { + $data['phone'] = $phone; + } + + $AccountControllel = new AccountController(); + $res = $AccountControllel->accountModify($data); + $res = json_decode($res, true); + if ($res['code'] == 200) { + unset($data['id']); + if (!empty($nickname)) { + $data['username'] = $nickname; + unset($data['nickname']); + } + Db::name('users')->where(['id' => $userId, 'companyId' => $companyId])->update($data); + return ResponseHelper::success('更新成功'); + } else { + return ResponseHelper::error($res['msg']); + } + } + + + public function editPassWord() + { + $userId = $this->request->param('userId', ''); + $passWord = $this->request->param('passWord', ''); + $companyId = $this->getUserInfo('companyId'); + if (empty($userId)) { + return ResponseHelper::error('用户id不能为空'); + } + + if (empty($passWord)) { + return ResponseHelper::error('密码不能为空'); + } + + $user = Db::name('users')->where(['id' => $userId, 'companyId' => $companyId])->find(); + if (empty($user)) { + return ResponseHelper::error('用户不存在'); + } + if ($user['passwordMd5'] == md5($passWord)) { + return ResponseHelper::error('新密码与旧密码一致'); + } + + $data = [ + 'passwordMd5' => md5($passWord), + 'passwordLocal' => localEncrypt($passWord), + 'updateTime' => time() + ]; + + $res = Db::name('users')->where(['id' => $userId, 'companyId' => $companyId])->update($data); + if (!empty($res)) { + if ($user['typeId'] == 1 && !empty($user['s2_accountId'])) { + $UserController = new UserController(); + $UserController->modifyPwd(['id' => $user['s2_accountId'],'pwd' => $passWord]); + } + + + return ResponseHelper::success('密码修改成功'); + } else { + return ResponseHelper::error('密码修改失败'); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/ContentLibraryController.php b/application/cunkebao/controller/ContentLibraryController.php new file mode 100644 index 0000000..8cdb213 --- /dev/null +++ b/application/cunkebao/controller/ContentLibraryController.php @@ -0,0 +1,2913 @@ +request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + // 获取请求参数 + $param = $this->request->post(); + + // 验证参数 + if (empty($param['name'])) { + return json(['code' => 400, 'msg' => '内容库名称不能为空']); + } + + + // 检查内容库名称是否已存在 + $where = [ + ['name', '=', $param['name']], + ['companyId', '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + // 查询内容库是否存在 + $exists = ContentLibrary::where($where)->find(); + + + if ($exists) { + return json(['code' => 400, 'msg' => '内容库名称已存在']); + } + + Db::startTrans(); + try { + + $keywordInclude = isset($param['keywordInclude']) ? json_encode($param['keywordInclude'], 256) : json_encode([]); + $keywordExclude = isset($param['keywordExclude']) ? json_encode($param['keywordExclude'], 256) : json_encode([]); + $devices = isset($param['devices']) ? json_encode($param['devices'], 256) : json_encode([]); + $sourceType = isset($param['sourceType']) ? $param['sourceType'] : 1; + + + // 构建数据 + $data = [ + 'name' => $param['name'], + // 数据来源配置 + 'sourceFriends' => $sourceType == 1 && isset($param['friendsGroups']) ? json_encode($param['friendsGroups']) : json_encode([]), // 选择的微信好友 + 'sourceGroups' => $sourceType == 2 && isset($param['wechatGroups']) ? json_encode($param['wechatGroups']) : json_encode([]), // 选择的微信群 + 'groupMembers' => $sourceType == 2 && isset($param['groupMembers']) ? json_encode($param['groupMembers']) : json_encode([]), // 群组成员 + 'catchType' => isset($param['catchType']) ? json_encode($param['catchType']) : json_encode([]), // 采集类型 + 'devices' => $devices, + // 关键词配置 + 'keywordInclude' => $keywordInclude, // 包含的关键词 + 'keywordExclude' => $keywordExclude, // 排除的关键词 + // AI配置 + 'aiEnabled' => isset($param['aiEnabled']) ? $param['aiEnabled'] : 0, // 是否启用AI + 'aiPrompt' => isset($param['aiPrompt']) ? $param['aiPrompt'] : '', // AI提示词 + // 时间配置 + 'timeEnabled' => isset($param['timeEnabled']) ? $param['timeEnabled'] : 0, // 是否启用时间限制 + 'timeStart' => isset($param['startTime']) ? strtotime($param['startTime']) : 0, // 开始时间(转换为时间戳) + 'timeEnd' => isset($param['endTime']) ? strtotime($param['endTime']) : 0, // 结束时间(转换为时间戳) + // 来源类型 + 'sourceType' => $sourceType, // 1=好友,2=群,3=好友和群 + // 表单类型 + 'formType' => isset($param['formType']) ? intval($param['formType']) : 1, // 表单类型,默认为0 + // 基础信息 + 'status' => isset($param['status']) ? $param['status'] : 0, // 状态:0=禁用,1=启用 + 'userId' => $this->request->userInfo['id'], + 'companyId' => $this->request->userInfo['companyId'], + 'createTime' => time(), + 'updateTime' => time() + ]; + + // 创建内容库 + $library = new ContentLibrary; + $result = $library->save($data); + + if (!$result) { + Db::rollback(); + return json(['code' => 500, 'msg' => '创建内容库失败']); + } + + Db::commit(); + return json(['code' => 200, 'msg' => '创建成功', 'data' => ['id' => $library->id]]); + } catch (\Exception $e) { + Db::rollback(); + return json(['code' => 500, 'msg' => '创建失败:' . $e->getMessage()]); + } + } + + /** + * 获取内容库列表 + * @return \think\response\Json + */ + public function getList() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + $sourceType = $this->request->param('sourceType', ''); // 来源类型,1=好友,2=群 + $formType = $this->request->param('formType', 0); // 表单类型筛选 + $companyId = $this->request->userInfo['companyId']; + $userId = $this->request->userInfo['id']; + $isAdmin = !empty($this->request->userInfo['isAdmin']); + + // 构建基础查询条件 + $where = [ + ['companyId', '=', $companyId], + ['isDel', '=', 0] // 只查询未删除的记录 + ]; + + // 非管理员只能查看自己的内容库 + if (!$isAdmin) { + $where[] = ['userId', '=', $userId]; + } + + // 添加名称模糊搜索 + if ($keyword !== '') { + $where[] = ['name', 'like', '%' . $keyword . '%']; + } + + // 添加来源类型筛选 + if (!empty($sourceType)) { + $where[] = ['sourceType', '=', $sourceType]; + } + + // 添加表单类型筛选 + if ($formType !== '') { + $where[] = ['formType', '=', $formType]; + } + + // 获取总记录数 + $total = ContentLibrary::where($where)->count(); + + // 获取分页数据 + $list = ContentLibrary::where($where) + ->field('id,name,sourceFriends,sourceGroups,keywordInclude,keywordExclude,aiEnabled,aiPrompt,timeEnabled,timeStart,timeEnd,status,sourceType,formType,userId,createTime,updateTime') + ->with(['user' => function ($query) { + $query->field('id,username'); + }]) + ->order('id', 'desc') + ->page($page, $limit) + ->select(); + + // 收集所有需要查询的ID + $libraryIds = []; + $friendIdsByLibrary = []; + $groupIdsByLibrary = []; + + foreach ($list as $item) { + $libraryIds[] = $item['id']; + + // 解析JSON字段 + $item['sourceFriends'] = json_decode($item['sourceFriends'] ?: '[]', true); + $item['sourceGroups'] = json_decode($item['sourceGroups'] ?: '[]', true); + $item['keywordInclude'] = json_decode($item['keywordInclude'] ?: '[]', true); + $item['keywordExclude'] = json_decode($item['keywordExclude'] ?: '[]', true); + + // 收集好友和群组ID + if (!empty($item['sourceFriends']) && $item['sourceType'] == 1) { + $friendIdsByLibrary[$item['id']] = $item['sourceFriends']; + } + + if (!empty($item['sourceGroups']) && $item['sourceType'] == 2) { + $groupIdsByLibrary[$item['id']] = $item['sourceGroups']; + } + } + + // 批量查询内容项数量 + $itemCounts = []; + if (!empty($libraryIds)) { + $counts = Db::name('content_item') + ->field('libraryId, COUNT(*) as count') + ->whereIn('libraryId', $libraryIds) + ->where('isDel', 0) + ->group('libraryId') + ->select(); + + foreach ($counts as $count) { + $itemCounts[$count['libraryId']] = $count['count']; + } + } + + // 批量查询好友信息 + $friendsInfoMap = []; + $allFriendIds = []; + foreach ($friendIdsByLibrary as $libraryId => $friendIds) { + if (!empty($friendIds)) { + $allFriendIds = array_merge($allFriendIds, $friendIds); + } + } + + if (!empty($allFriendIds)) { + $allFriendIds = array_unique($allFriendIds); + $friendsInfo = Db::name('wechat_friendship')->alias('wf') + ->field('wf.id,wf.wechatId, wa.nickname, wa.avatar') + ->join('wechat_account wa', 'wf.wechatId = wa.wechatId') + ->whereIn('wf.id', $allFriendIds) + ->select(); + + foreach ($friendsInfo as $friend) { + $friendsInfoMap[$friend['id']] = $friend; + } + } + + // 批量查询群组信息 + $groupsInfoMap = []; + $allGroupIds = []; + foreach ($groupIdsByLibrary as $libraryId => $groupIds) { + if (!empty($groupIds)) { + $allGroupIds = array_merge($allGroupIds, $groupIds); + } + } + + if (!empty($allGroupIds)) { + $allGroupIds = array_unique($allGroupIds); + $groupsInfo = Db::name('wechat_group')->alias('g') + ->field('g.id, g.chatroomId, g.name, g.avatar, g.ownerWechatId') + ->whereIn('g.id', $allGroupIds) + ->select(); + + foreach ($groupsInfo as $group) { + $groupsInfoMap[$group['id']] = $group; + } + } + + // 处理每个内容库的数据 + foreach ($list as &$item) { + // 添加创建人名称 + $item['creatorName'] = $item['user']['username'] ?? ''; + + // 添加内容项数量 + $item['itemCount'] = $itemCounts[$item['id']] ?? 0; + + // 处理好友信息 + if (!empty($friendIdsByLibrary[$item['id']])) { + $selectedFriends = []; + foreach ($friendIdsByLibrary[$item['id']] as $friendId) { + if (isset($friendsInfoMap[$friendId])) { + $selectedFriends[] = $friendsInfoMap[$friendId]; + } + } + $item['selectedFriends'] = $selectedFriends; + } + + // 处理群组信息 + if (!empty($groupIdsByLibrary[$item['id']])) { + $selectedGroups = []; + foreach ($groupIdsByLibrary[$item['id']] as $groupId) { + if (isset($groupsInfoMap[$groupId])) { + $selectedGroups[] = $groupsInfoMap[$groupId]; + } + } + $item['selectedGroups'] = $selectedGroups; + } + + unset($item['user']); // 移除关联数据 + } + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + ] + ]); + } + + /** + * 获取内容库详情 + * @return \think\response\Json + */ + public function detail() + { + $id = $this->request->param('id', 0); + $companyId = $this->request->userInfo['companyId']; + $userId = $this->request->userInfo['id']; + $isAdmin = !empty($this->request->userInfo['isAdmin']); + + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + // 构建查询条件 + $where = [ + ['id', '=', $id], + ['companyId', '=', $companyId], + ['isDel', '=', 0] // 只查询未删除的记录 + ]; + + if (!$isAdmin) { + $where[] = ['userId', '=', $userId]; + } + + // 查询内容库信息 + $library = ContentLibrary::where($where) + ->field('id,name,sourceType,formType,devices ,sourceFriends,sourceGroups,keywordInclude,keywordExclude,aiEnabled,aiPrompt,timeEnabled,timeStart,timeEnd,status,userId,companyId,createTime,updateTime,groupMembers,catchType') + ->find(); + + if (empty($library)) { + return json(['code' => 500, 'msg' => '内容库不存在']); + } + + // 处理JSON字段转数组 + $library['friendsGroups'] = json_decode($library['sourceFriends'] ?: [], true); + $library['wechatGroups'] = json_decode($library['sourceGroups'] ?: [], true); + $library['keywordInclude'] = json_decode($library['keywordInclude'] ?: [], true); + $library['keywordExclude'] = json_decode($library['keywordExclude'] ?: [], true); + $library['groupMembers'] = json_decode($library['groupMembers'] ?: [], true); + $library['catchType'] = json_decode($library['catchType'] ?: [], true); + $library['deviceGroups'] = json_decode($library['devices'] ?: [], true); + unset($library['sourceFriends'], $library['sourceGroups'], $library['devices']); + + // 将时间戳转换为日期格式(精确到日) + if (!empty($library['timeStart'])) { + $library['timeStart'] = date('Y-m-d', $library['timeStart']); + } + if (!empty($library['timeEnd'])) { + $library['timeEnd'] = date('Y-m-d', $library['timeEnd']); + } + + // 初始化选项数组 + $library['friendsGroupsOptions'] = []; + $library['wechatGroupsOptions'] = []; + $library['groupMembersOptions'] = []; + + // 批量查询好友信息 + if (!empty($library['friendsGroups'])) { + $friendIds = $library['friendsGroups']; + if (!empty($friendIds)) { + // 查询好友信息,使用wechat_friendship表 + $library['friendsGroupsOptions'] = Db::name('wechat_friendship')->alias('wf') + ->field('wf.id,wf.wechatId, wa.nickname, wa.avatar') + ->join('wechat_account wa', 'wf.wechatId = wa.wechatId') + ->order('wa.id DESC') + ->whereIn('wf.id', $friendIds) + ->select(); + } + } + + // 批量查询群组信息 + if (!empty($library['wechatGroups'])) { + $groupIds = $library['wechatGroups']; + if (!empty($groupIds)) { + // 查询群组信息 + $library['wechatGroupsOptions'] = Db::name('wechat_group')->alias('g') + ->field('g.id, g.chatroomId, g.name, g.avatar, g.ownerWechatId, wa.nickname as ownerNickname, wa.avatar as ownerAvatar, wa.alias as ownerAlias') + ->join('wechat_account wa', 'g.ownerWechatId = wa.wechatId', 'LEFT') // 使用LEFT JOIN避免因群主不存在导致查询失败 + ->whereIn('g.id', $groupIds) + ->select(); + } + } + + // 批量查询群成员信息 + if (!empty($library['groupMembers'])) { + // groupMembers格式: {"826825": ["413771", "413769"], "840818": ["496300", "496302"]} + // 键是群组ID,值是成员ID数组 + $allMemberIds = []; + $groupMembersMap = []; + + if (is_array($library['groupMembers'])) { + foreach ($library['groupMembers'] as $groupId => $memberIds) { + if (is_array($memberIds) && !empty($memberIds)) { + $allMemberIds = array_merge($allMemberIds, $memberIds); + // 保存群组ID和成员ID的映射关系 + $groupMembersMap[$groupId] = $memberIds; + } + } + } + + if (!empty($allMemberIds)) { + // 去重 + $allMemberIds = array_unique($allMemberIds); + + // 查询群成员信息 + $members = Db::table('s2_wechat_chatroom_member') + ->field('id, chatroomId, wechatId, nickname, avatar, conRemark, alias, friendType, createTime, updateTime') + ->whereIn('id', $allMemberIds) + ->select(); + + // 将成员数据按ID建立索引 + $membersById = []; + foreach ($members as $member) { + // 格式化时间字段 + $member['createTime'] = !empty($member['createTime']) ? date('Y-m-d H:i:s', $member['createTime']) : ''; + $member['updateTime'] = !empty($member['updateTime']) ? date('Y-m-d H:i:s', $member['updateTime']) : ''; + $membersById[$member['id']] = $member; + } + + // 按照群组ID分组返回 + $groupMembersOptions = []; + foreach ($groupMembersMap as $groupId => $memberIds) { + $groupMembersOptions[$groupId] = []; + foreach ($memberIds as $memberId) { + if (isset($membersById[$memberId])) { + $groupMembersOptions[$groupId][] = $membersById[$memberId]; + } + } + } + + $library['groupMembersOptions'] = $groupMembersOptions; + } else { + $library['groupMembersOptions'] = []; + } + } else { + $library['groupMembersOptions'] = []; + } + + //获取设备信息 + if (!empty($library['deviceGroups'])) { + $deviceList = DeviceModel::alias('d') + ->field([ + 'd.id', 'd.imei', 'd.memo', 'd.alive', + 'l.wechatId', + 'a.nickname', 'a.alias', 'a.avatar', 'a.alias', '0 totalFriend' + ]) + ->leftJoin('device_wechat_login l', 'd.id = l.deviceId and l.alive =' . DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE . ' and l.companyId = d.companyId') + ->leftJoin('wechat_account a', 'l.wechatId = a.wechatId') + ->whereIn('d.id', $library['deviceGroups']) + ->order('d.id desc') + ->select(); + + foreach ($deviceList as &$device) { + $curstomer = WechatCustomerModel::field('friendShip')->where(['wechatId' => $device['wechatId']])->find(); + $device['totalFriend'] = $curstomer->friendShip->totalFriend ?? 0; + } + unset($device); + $library['deviceGroupsOptions'] = $deviceList; + } else { + $library['deviceGroupsOptions'] = []; + } + + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => $library + ]); + } + + /** + * 更新内容库 + * @return \think\response\Json + */ + public function update() + { + if (!$this->request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + // 获取请求参数 + $param = $this->request->post(); + + // 简单验证 + if (empty($param['id'])) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + if (empty($param['name'])) { + return json(['code' => 400, 'msg' => '内容库名称不能为空']); + } + + + $where = [ + ['companyId', '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0], // 只查询未删除的记录 + ['id', '=', $param['id']] + ]; + + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + // 查询内容库是否存在 + $library = ContentLibrary::where($where)->find(); + + if (!$library) { + return json(['code' => 500, 'msg' => '内容库不存在']); + } + + Db::startTrans(); + try { + + $keywordInclude = isset($param['keywordInclude']) ? json_encode($param['keywordInclude'], 256) : json_encode([]); + $keywordExclude = isset($param['keywordExclude']) ? json_encode($param['keywordExclude'], 256) : json_encode([]); + $devices = isset($param['devices']) ? json_encode($param['devices'], 256) : json_encode([]); + + // 更新内容库基本信息 + $library->name = $param['name']; + $library->sourceType = isset($param['sourceType']) ? $param['sourceType'] : 1; + $library->sourceFriends = $param['sourceType'] == 1 && isset($param['friendsGroups']) ? json_encode($param['friendsGroups']) : json_encode([]); + $library->sourceGroups = $param['sourceType'] == 2 && isset($param['wechatGroups']) ? json_encode($param['wechatGroups']) : json_encode([]); + $library->groupMembers = $param['sourceType'] == 2 && isset($param['groupMembers']) ? json_encode($param['groupMembers']) : json_encode([]); + $library->catchType = isset($param['catchType']) ? json_encode($param['catchType']) : json_encode([]);// 采集类型 + $library->devices = $devices; + $library->keywordInclude = $keywordInclude; + $library->keywordExclude = $keywordExclude; + $library->aiEnabled = isset($param['aiEnabled']) ? $param['aiEnabled'] : 0; + $library->aiPrompt = isset($param['aiPrompt']) ? $param['aiPrompt'] : ''; + $library->timeEnabled = isset($param['timeEnabled']) ? $param['timeEnabled'] : 0; + $library->timeStart = isset($param['startTime']) ? strtotime($param['startTime']) : 0; + $library->timeEnd = isset($param['endTime']) ? strtotime($param['endTime']) : 0; + $library->formType = isset($param['formType']) ? intval($param['formType']) : $library->formType; // 表单类型,如果未传则保持原值 + $library->status = isset($param['status']) ? $param['status'] : 0; + $library->updateTime = time(); + $library->save(); + + Db::commit(); + return json(['code' => 200, 'msg' => '更新成功']); + } catch (\Exception $e) { + Db::rollback(); + return json(['code' => 500, 'msg' => '更新失败:' . $e->getMessage()]); + } + } + + /** + * 删除内容库 + * @return \think\response\Json + */ + public function delete() + { + $id = $this->request->param('id', 0); + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + + $where = [ + ['id', '=', $id], + ['companyId', '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + $library = ContentLibrary::where($where)->find(); + + if (empty($library)) { + return json(['code' => 500, 'msg' => '内容库不存在']); + } + + try { + // 软删除 + $library->isDel = 1; + $library->deleteTime = time(); + $library->save(); + + return json(['code' => 200, 'msg' => '删除成功']); + } catch (\Exception $e) { + return json(['code' => 500, 'msg' => '删除失败:' . $e->getMessage()]); + } + } + + /************************************ + * 内容项目管理功能 + ************************************/ + + /** + * 获取内容库素材列表 + * @return \think\response\Json + */ + public function getItemList() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $libraryId = $this->request->param('libraryId', 0); + $keyword = $this->request->param('keyword', ''); // 搜索关键词 + $companyId = $this->request->userInfo['companyId']; + $userId = $this->request->userInfo['id']; + $isAdmin = !empty($this->request->userInfo['isAdmin']); + + if (empty($libraryId)) { + return json(['code' => 400, 'msg' => '内容库ID不能为空']); + } + + // 验证内容库权限 + $libraryWhere = [ + ['id', '=', $libraryId], + ['companyId', '=', $companyId], + ['isDel', '=', 0] + ]; + + + if (!$isAdmin) { + $libraryWhere[] = ['userId', '=', $userId]; + } + + $library = ContentLibrary::where($libraryWhere)->find(); + + if (empty($library)) { + return json(['code' => 500, 'msg' => '内容库不存在或无权限访问']); + } + + // 构建查询条件 + $where = [ + ['libraryId', '=', $libraryId], + ['isDel', '=', 0] + ]; + + // 关键词搜索 + if (!empty($keyword)) { + $where[] = ['content|title', 'like', '%' . $keyword . '%']; + } + + // 获取总数 + $total = ContentItem::where($where)->count(); + + // 查询数据 + $list = ContentItem::where($where) + ->field('id,libraryId,type,title,content,contentAi,contentType,resUrls,urls,friendId,wechatId,wechatChatroomId,createTime,createMomentTime,createMessageTime,coverImage,ossUrls') + ->order('createMomentTime DESC,createMessageTime DESC,createTime DESC') + ->page($page, $limit) + ->select(); + + // 收集需要查询的ID + $friendIds = []; + $chatroomIds = []; + $wechatIds = []; + + foreach ($list as $item) { + if ($item['type'] == 'moment' && !empty($item['friendId'])) { + $friendIds[] = $item['friendId']; + } else if ($item['type'] == 'group_message' && !empty($item['wechatChatroomId'])) { + $chatroomIds[] = $item['wechatChatroomId']; + if (!empty($item['wechatId'])) { + $wechatIds[] = $item['wechatId']; + } + } + } + + // 批量查询好友信息 + $friendInfoMap = []; + if (!empty($friendIds)) { + $friendIds = array_unique($friendIds); + $friendInfos = Db::table('s2_wechat_friend') + ->whereIn('id', $friendIds) + ->field('id, nickname, avatar') + ->select(); + + foreach ($friendInfos as $info) { + $friendInfoMap[$info['id']] = $info; + } + } + + // 批量查询群成员信息 + $memberInfoMap = []; + if (!empty($wechatIds)) { + $wechatIds = array_unique($wechatIds); + $memberInfos = Db::table('s2_wechat_chatroom_member') + ->whereIn('wechatId', $wechatIds) + ->field('wechatId, nickname, avatar') + ->select(); + + foreach ($memberInfos as $info) { + $memberInfoMap[$info['wechatId']] = $info; + } + } + + + // 处理数据 + foreach ($list as &$item) { + // 使用AI内容(如果有) + $item['content'] = !empty($item['contentAi']) ? $item['contentAi'] : $item['content']; + + // 处理JSON字段 + $item['resUrls'] = json_decode($item['resUrls'] ?: [], true); + $item['urls'] = json_decode($item['urls'] ?: [], true); + $item['ossUrls'] = json_decode($item['ossUrls'] ?: [], true); + + if (!empty($item['ossUrls']) && count($item['ossUrls']) > 0) { + $item['resUrls'] = $item['ossUrls']; + } + + + // 格式化时间 + if (!empty($item['createMomentTime']) && is_numeric($item['createMomentTime'])) { + $item['time'] = date('Y-m-d H:i:s', (int)$item['createMomentTime']); + } elseif (!empty($item['createMessageTime']) && is_numeric($item['createMessageTime'])) { + $item['time'] = date('Y-m-d H:i:s', (int)$item['createMessageTime']); + } elseif (!empty($item['createTime']) && is_numeric($item['createTime'])) { + $item['time'] = date('Y-m-d H:i:s', (int)$item['createTime']); + } else { + $item['time'] = date('Y-m-d H:i:s'); // 如果没有有效的时间戳,使用当前时间 + } + + // 设置发送者信息 + $item['senderNickname'] = ''; + $item['senderAvatar'] = ''; + + // 从映射表获取发送者信息 + if ($item['type'] == 'moment' && !empty($item['friendId'])) { + if (isset($friendInfoMap[$item['friendId']])) { + $friendInfo = $friendInfoMap[$item['friendId']]; + $item['senderNickname'] = $friendInfo['nickname'] ?? ''; + $item['senderAvatar'] = $friendInfo['avatar'] ?? ''; + } + } else if ($item['type'] == 'group_message' && !empty($item['wechatId'])) { + if (isset($memberInfoMap[$item['wechatId']])) { + $memberInfo = $memberInfoMap[$item['wechatId']]; + $item['senderNickname'] = $memberInfo['nickname'] ?? ''; + $item['senderAvatar'] = $memberInfo['avatar'] ?? ''; + } + } + + unset($item['contentAi'], $item['ossUrls']); + } + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + + /** + * 添加内容项目 + * @return \think\response\Json + */ + public function addItem() + { + if (!$this->request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + // 获取请求参数 + $param = $this->request->post(); + + // A简单验证 + if (empty($param['libraryId'])) { + return json(['code' => 400, 'msg' => '内容库ID不能为空']); + } + + if (empty($param['type'])) { + return json(['code' => 400, 'msg' => '内容类型不能为空']); + } + + if (empty($param['content'])) { + return json(['code' => 400, 'msg' => '内容数据不能为空']); + } + + // 当类型为群消息时,限制图片只能上传一张 + if ($param['type'] == 'group_message') { + $images = isset($param['images']) ? $param['images'] : []; + if (is_string($images)) { + $images = json_decode($images, true); + } + + if (count($images) > 1) { + return json(['code' => 400, 'msg' => '群消息类型只能上传一张图片']); + } + } + + $where = [ + ['id', '=', $param['libraryId']], + ['companyId', '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + // 查询内容库是否存在 + $library = ContentLibrary::where($where)->find(); + + if (!$library) { + return json(['code' => 500, 'msg' => '内容库不存在']); + } + + try { + // 创建内容项目 + $item = new ContentItem; + $item->libraryId = $param['libraryId']; + $item->contentType = $param['type']; + $item->type = 'diy'; + $item->title = $param['title'] ?? '自定义内容'; + $item->content = $param['content']; + $item->comment = $param['comment'] ?? ''; + $item->sendTime = strtotime($param['sendTime']); + $item->resUrls = json_encode($param['resUrls'] ?? [], 256); + $item->urls = json_encode($param['urls'] ?? [], 256); + $item->ossUrls = json_encode($param['ossUrls'] ?? [], 256); + $item->senderNickname = '系统创建'; + $item->coverImage = $param['coverImage'] ?? ''; + $item->save(); + + return json(['code' => 200, 'msg' => '添加成功', 'data' => ['id' => $item->id]]); + } catch (\Exception $e) { + return json(['code' => 500, 'msg' => '添加失败:' . $e->getMessage()]); + } + } + + /** + * 删除内容项目 + * @param int $id 内容项目ID + * @return \think\response\Json + */ + public function deleteItem() + { + + $id = $this->request->param('id', 0); + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + + $where = [ + ['i.id', '=', $id], + ['l.companyId', '=', $this->request->userInfo['companyId']] + ]; + + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['l.userId', '=', $this->request->userInfo['id']]; + } + + // 查询内容项目是否存在并检查权限 + $item = ContentItem::alias('i') + ->join('content_library l', 'i.libraryId = l.id') + ->where($where) + ->find(); + + + if (empty($item)) { + return json(['code' => 500, 'msg' => '内容项目不存在或无权限操作']); + } + + try { + // 删除内容项目 + $service = new \app\cunkebao\service\ContentItemService(); + $result = $service->deleteItem($id); + if ($result['code'] != 200) { + return json($result); + } + + return json(['code' => 200, 'msg' => '删除成功']); + } catch (\Exception $e) { + return json(['code' => 500, 'msg' => '删除失败:' . $e->getMessage()]); + } + } + + /** + * 获取内容项目详情 + * @return \think\response\Json + */ + public function getItemDetail() + { + $id = $this->request->param('id', 0); + $userId = $this->request->userInfo['id']; + $isAdmin = !empty($this->request->userInfo['isAdmin']); + + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + // 构建查询条件 + $where = [ + ['i.id', '=', $id], + ['i.isDel', '=', 0] + ]; + + // 非管理员只能查看自己的内容项 + if (!$isAdmin) { + $where[] = ['l.userId', '=', $userId]; + } + + // 查询内容项目是否存在并检查权限 + $item = ContentItem::alias('i') + ->join('content_library l', 'i.libraryId = l.id') + ->where($where) + ->field('i.*') + ->find(); + + if (empty($item)) { + return json(['code' => 500, 'msg' => '内容项目不存在或无权限访问']); + } + + // 处理JSON字段 + $item['resUrls'] = json_decode($item['resUrls'] ?: [], true); + $item['urls'] = json_decode($item['urls'] ?: [], true); + + // 添加内容类型的文字描述 + $contentTypeMap = [ + 0 => '未知', + 1 => '图片', + 2 => '链接', + 3 => '视频', + 4 => '文本', + 5 => '小程序', + 6 => '图文' + ]; + $item['contentTypeName'] = $contentTypeMap[$item['contentType'] ?? 0] ?? '未知'; + + // 格式化时间 + if (!empty($item['createMomentTime']) && is_numeric($item['createMomentTime'])) { + $item['createMomentTimeFormatted'] = date('Y-m-d H:i:s', (int)$item['createMomentTime']); + } + if (!empty($item['createMessageTime']) && is_numeric($item['createMessageTime'])) { + $item['createMessageTimeFormatted'] = date('Y-m-d H:i:s', (int)$item['createMessageTime']); + } + if (!empty($item['createTime']) && is_numeric($item['createTime'])) { + $item['createTimeFormatted'] = date('Y-m-d H:i:s', (int)$item['createTime']); + } + + // 格式化发送时间 + if (!empty($item['sendTime']) && is_numeric($item['sendTime'])) { + $item['sendTimeFormatted'] = date('Y-m-d H:i:s', (int)$item['sendTime']); + // 保持原字段兼容 + $item['sendTime'] = $item['sendTimeFormatted']; + } + + // 初始化发送者和群组信息 + $item['senderInfo'] = []; + $item['groupInfo'] = []; + + // 批量获取关联信息 + if ($item['type'] == 'moment' && !empty($item['friendId'])) { + // 获取朋友圈发送者信息 + $friendInfo = Db::name('wechat_friendship') + ->alias('wf') + ->join('wechat_account wa', 'wf.wechatId = wa.wechatId', 'LEFT') + ->where('wf.id', $item['friendId']) + ->field('wf.id, wf.wechatId, wa.nickname, wa.avatar') + ->find(); + + if ($friendInfo) { + $item['senderInfo'] = $friendInfo; + } + } elseif ($item['type'] == 'group_message' && !empty($item['wechatChatroomId'])) { + // 获取群组信息 + $groupInfo = Db::name('wechat_group') + ->where('id', $item['wechatChatroomId']) + ->field('id, chatroomId, name, avatar, ownerWechatId') + ->find(); + + if ($groupInfo) { + $item['groupInfo'] = $groupInfo; + + // 如果有发送者信息,也获取发送者详情 + if (!empty($item['wechatId'])) { + $senderInfo = Db::table('s2_wechat_chatroom_member') + ->where([ + 'chatroomId' => $groupInfo['chatroomId'], + 'wechatId' => $item['wechatId'] + ]) + ->field('wechatId, nickname, avatar') + ->find(); + + if ($senderInfo) { + $item['senderInfo'] = $senderInfo; + } + } + } + } + + // 如果有AI内容,添加到返回数据中 + if (!empty($item['contentAi'])) { + $item['contentOriginal'] = $item['content']; + $item['content'] = $item['contentAi']; + } + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => $item + ]); + } + + /** + * 更新内容项目 + * @return \think\response\Json + */ + public function updateItem() + { + if (!$this->request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + // 获取请求参数 + $param = $this->request->post(); + + // 简单验证 + if (empty($param['id'])) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + // 查询内容项目是否存在并检查权限 + $item = ContentItem::where([ + ['id', '=', $param['id']], + ['isDel', '=', 0] + ])->find(); + + if (!$item) { + return json(['code' => 500, 'msg' => '内容项目不存在或无权限操作']); + } + + try { + // 更新内容项目 + $item->title = $param['title'] ?? $item->title; + $item->content = $param['content'] ?? $item->content; + $item->comment = $param['comment'] ?? $item->comment; + + // 处理发送时间 + if (!empty($param['sendTime'])) { + $item->sendTime = strtotime($param['sendTime']); + } + + // 处理内容类型 + if (isset($param['contentType'])) { + $item->contentType = $param['contentType']; + } + + // 处理资源URL + if (isset($param['resUrls'])) { + $resUrls = is_string($param['resUrls']) ? json_decode($param['resUrls'], true) : $param['resUrls']; + $item->resUrls = json_encode($resUrls, JSON_UNESCAPED_UNICODE); + + // 设置封面图片 + if (!empty($resUrls[0])) { + $item->coverImage = $resUrls[0]; + } + } + + // 处理链接URL + if (isset($param['urls'])) { + $urls = is_string($param['urls']) ? json_decode($param['urls'], true) : $param['urls']; + $item->urls = json_encode($urls, JSON_UNESCAPED_UNICODE); + } + + // 处理地理位置信息 + if (isset($param['location'])) { + $item->location = $param['location']; + } + if (isset($param['lat'])) { + $item->lat = $param['lat']; + } + if (isset($param['lng'])) { + $item->lng = $param['lng']; + } + + // 更新修改时间 + $item->updateTime = time(); + // 保存更新 + $item->save(); + + return json(['code' => 200, 'msg' => '更新成功']); + } catch (\Exception $e) { + return json(['code' => 500, 'msg' => '更新失败:' . $e->getMessage()]); + } + } + + + public function aiEditContent() + { + + $id = Request::param('id', ''); + $aiPrompt = Request::param('aiPrompt', ''); + $content = Request::param('content', ''); + $companyId = $this->request->userInfo['companyId']; + // 简单验证 + if (empty($id) && empty($content)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + if (!empty($id)) { + // 查询内容项目是否存在并检查权限 + $item = ContentItem::alias('ci') + ->join('content_library cl', 'ci.libraryId = cl.id') + ->where(['ci.id' => $id, 'ci.isDel' => 0, 'cl.isDel' => 0, 'cl.companyId' => $companyId]) + ->field('ci.*') + ->find(); + } else { + $item['content'] = $content; + } + + if (empty($item)) { + return json(['code' => 500, 'msg' => '内容项目不存在或无权限操作']); + } + + if (empty($item['content'])) { + return json(['code' => 500, 'msg' => '内容不能为空']); + } + $contentFront = !empty($item['contentAi']) ? $item['contentAi'] : $item['content']; + if (!$this->request->isPost()) { + try { + $contentAi = $this->aiRewrite(['aiEnabled' => true, 'aiPrompt' => $aiPrompt], $contentFront); + if (!empty($contentAi)) { + return json(['code' => 200, 'msg' => 'ai编写成功', 'data' => ['contentAfter' => $contentAi, 'contentFront' => $contentFront]]); + } else { + return json(['code' => 500, 'msg' => 'ai编写失败']); + } + } catch (\Exception $e) { + return json(['code' => 500, 'msg' => 'ai编写失败:' . $e->getMessage()]); + } + } else { + if (empty($content)) { + return json(['code' => 500, 'msg' => '新内容不能为空']); + } + $res = ContentItem::where(['id' => $item['id']])->update(['contentAi' => $content, 'updateTime' => time()]); + if (!empty($res)) { + return json(['code' => 200, 'msg' => '更新成功']); + } else { + return json(['code' => 500, 'msg' => '更新失败']); + } + } + } + + + /************************************ + * 数据采集相关功能 + ************************************/ + + function getExternalPageDetails($url) + { + $html = file_get_contents($url); + $dom = new \DOMDocument(); + @$dom->loadHTML($html); + $xpath = new \DOMXPath($dom); + + // 获取标题 + $titleNode = $xpath->query('//title'); + $title = $titleNode->length > 0 ? $titleNode->item(0)->nodeValue : ''; + + // 获取图标链接 + $iconNode = $xpath->query('//link[@rel="shortcut icon"]/@href'); + $icon = $iconNode->length > 0 ? $iconNode->item(0)->nodeValue : ''; + + return ['title' => $title, 'icon' => $icon]; + } + + + /** + * 执行朋友圈采集任务 + * @return \think\response\Json + */ + public function collectMoments() + { + // 查询条件:未删除且已开启的内容库 + $where = [ + ['isDel', '=', 0], // 未删除 + ['status', '=', 1], // 已开启 + ]; + + // 查询符合条件的内容库 + $libraries = ContentLibrary::where($where) + ->field('id,name,sourceType,sourceFriends,sourceGroups,keywordInclude,keywordExclude,aiEnabled,aiPrompt,timeEnabled,timeStart,timeEnd,status,userId,companyId,createTime,updateTime,groupMembers,catchType') + ->order('id', 'desc') + ->select()->toArray(); + + if (empty($libraries)) { + return json_encode(['code' => 200, 'msg' => '没有可用的内容库配置'], 256); + } + + $successCount = 0; + $failCount = 0; + $results = []; + $processedLibraries = 0; + $totalLibraries = count($libraries); + + // 预处理内容库数据 + foreach ($libraries as &$library) { + // 解析JSON字段 + $library['sourceFriends'] = json_decode($library['sourceFriends'] ?: [], true); + $library['sourceGroups'] = json_decode($library['sourceGroups'] ?: [], true); + $library['keywordInclude'] = json_decode($library['keywordInclude'] ?: [], true); + $library['keywordExclude'] = json_decode($library['keywordExclude'] ?: [], true); + $library['groupMembers'] = json_decode($library['groupMembers'] ?: [], true); + $library['catchType'] = json_decode($library['catchType'] ?: [], true); + } + unset($library); // 解除引用 + + // 处理每个内容库的采集任务 + foreach ($libraries as $library) { + try { + $processedLibraries++; + + // 根据数据来源类型执行不同的采集逻辑 + $collectResult = [ + 'status' => 'skipped', + 'message' => '没有配置数据来源' + ]; + + switch ($library['sourceType']) { + case 1: // 好友类型 + if (!empty($library['sourceFriends'])) { + $collectResult = $this->collectFromFriends($library); + } + break; + + case 2: // 群类型 + if (!empty($library['sourceGroups'])) { + $collectResult = $this->collectFromGroups($library); + } + break; + + default: + $collectResult = [ + 'status' => 'failed', + 'message' => '不支持的数据来源类型' + ]; + } + + // 统计成功和失败数量 + if ($collectResult['status'] == 'success') { + $successCount++; + } elseif ($collectResult['status'] == 'failed' || $collectResult['status'] == 'error') { + $failCount++; + } + + // 记录结果 + $results[] = [ + 'library_id' => $library['id'], + 'library_name' => $library['name'], + 'source_type' => $library['sourceType'] == 1 ? '好友' : ($library['sourceType'] == 2 ? '群组' : '未知'), + 'status' => $collectResult['status'], + 'message' => $collectResult['message'] ?? '', + 'data' => $collectResult['data'] ?? [] + ]; + + // 每处理5个内容库,释放一次内存 + if ($processedLibraries % 5 == 0 && $processedLibraries < $totalLibraries) { + gc_collect_cycles(); + } + } catch (\Exception $e) { + $failCount++; + $results[] = [ + 'library_id' => $library['id'], + 'library_name' => $library['name'], + 'source_type' => $library['sourceType'] == 1 ? '好友' : ($library['sourceType'] == 2 ? '群组' : '未知'), + 'status' => 'error', + 'message' => $e->getMessage() + ]; + + // 记录错误日志 + \think\facade\Log::error('内容库采集错误: ' . $e->getMessage() . ' [库ID: ' . $library['id'] . ']'); + } + } + + // 返回采集结果 + return json_encode([ + 'code' => 200, + 'msg' => '采集任务执行完成', + 'data' => [ + 'total' => $totalLibraries, + 'success' => $successCount, + 'fail' => $failCount, + 'skipped' => $totalLibraries - $successCount - $failCount, + 'results' => $results + ] + ], 256); + } + + /** + * 从好友采集朋友圈内容 + * @param array $library 内容库配置 + * @return array 采集结果 + */ + private function collectFromFriends($library) + { + $friendIds = $library['sourceFriends']; + if (empty($friendIds)) { + return [ + 'status' => 'failed', + 'message' => '没有指定要采集的好友' + ]; + } + + try { + // 获取API配置 + $toAccountId = ''; + $username = Env::get('api.username2', ''); + $password = Env::get('api.password2', ''); + $needFetch = false; + + // 检查是否需要主动获取朋友圈 + if (!empty($username) && !empty($password)) { + $toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId'); + $needFetch = !empty($toAccountId); + } + + // 批量查询好友信息 + $friends = Db::table('s2_wechat_friend') + ->field('id, wechatAccountId, wechatId, accountId, nickname, avatar') + ->whereIn('id', $friendIds) + ->where('isDeleted', 0) + ->select(); + + if (empty($friends)) { + return [ + 'status' => 'failed', + 'message' => '未找到有效的好友信息' + ]; + } + + // 从朋友圈采集内容 + $collectedData = []; + $totalMomentsCount = 0; + $processedFriends = 0; + $totalFriends = count($friends); + + // 获取采集类型限制 + $catchTypes = $library['catchType'] ?? []; + + foreach ($friends as $friend) { + $processedFriends++; + + // 如果配置了API并且需要主动获取朋友圈 + if ($needFetch) { + try { + // 执行切换好友命令 + $automaticAssign = new AutomaticAssign(); + $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['id'], 'toAccountId' => $toAccountId], true); + + // 存入缓存 + $friendData = $friend; + $friendData['friendId'] = $friend['id']; + artificialAllotWechatFriend($friendData); + + // 执行采集朋友圈命令 + $webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]); + $webSocket->getMoments(['wechatFriendId' => $friend['id'], 'wechatAccountId' => $friend['wechatAccountId']]); + + // 采集完毕切换回原账号 + $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['id'], 'toAccountId' => $friend['accountId']], true); + } catch (\Exception $e) { + \think\facade\Log::error('采集朋友圈失败: ' . $e->getMessage() . ' [好友ID: ' . $friend['id'] . ']'); + // 继续处理下一个好友,不中断整个流程 + } + } + + // 从s2_wechat_moments表获取朋友圈数据 + $query = Db::table('s2_wechat_moments') + ->where([ + //'wechatAccountId' => $friend['wechatAccountId'], + 'userName' => $friend['wechatId'], + ]) + ->order('createTime', 'desc') + ->group('snsId'); + + // 如果启用了时间限制 + if ($library['timeEnabled'] && $library['timeStart'] > 0 && $library['timeEnd'] > 0) { + $query->whereBetween('createTime', [$library['timeStart'], $library['timeEnd']]); + } + + // 如果指定了采集类型,进行过滤 + /*if (!empty($catchTypes)) { + $query->whereIn('type', $catchTypes); + }*/ + + // 获取最近20条朋友圈 + $moments = $query->page(1, 100)->select(); + if (empty($moments)) { + continue; + } + + $nickname = $friend['nickname'] ?? '未知好友'; + $friendMomentsCount = 0; + $filteredMoments = []; + + // 处理每条朋友圈数据 + foreach ($moments as $moment) { + // 处理关键词过滤 + $content = $moment['content'] ?? ''; + + // 应用关键词过滤 + if (!$this->passKeywordFilter($content, $library['keywordInclude'], $library['keywordExclude'])) { + continue; + } + + /* // 如果启用了AI处理 + if (!empty($library['aiEnabled']) && !empty($content)) { + try { + $contentAi = $this->aiRewrite($library, $content); + if (!empty($contentAi)) { + $moment['contentAi'] = $contentAi; + } + } catch (\Exception $e) { + \think\facade\Log::error('AI处理失败: ' . $e->getMessage() . ' [朋友圈ID: ' . ($moment['id'] ?? 'unknown') . ']'); + $moment['contentAi'] = ''; + } + }*/ + + // 保存到内容库的content_item表 + if ($this->saveMomentToContentItem($moment, $library['id'], $friend, $nickname)) { + $friendMomentsCount++; + $filteredMoments[] = [ + 'id' => $moment['id'] ?? '', + 'content' => mb_substr($content, 0, 50) . (mb_strlen($content) > 50 ? '...' : ''), + 'time' => date('Y-m-d H:i:s', $moment['createTime'] ?? time()) + ]; + } + } + + if ($friendMomentsCount > 0) { + // 记录采集结果 + $collectedData[$friend['wechatId']] = [ + 'friendId' => $friend['id'], + 'nickname' => $nickname, + 'count' => $friendMomentsCount, + 'samples' => array_slice($filteredMoments, 0, 3) // 只保留前3条示例 + ]; + + $totalMomentsCount += $friendMomentsCount; + } + + // 每处理5个好友,释放一次内存 + if ($processedFriends % 5 == 0 && $processedFriends < $totalFriends) { + gc_collect_cycles(); + } + } + + if (empty($collectedData)) { + return [ + 'status' => 'warning', + 'message' => '未采集到任何朋友圈内容' + ]; + } + + return [ + 'status' => 'success', + 'message' => '成功采集到' . count($collectedData) . '位好友的' . $totalMomentsCount . '条朋友圈内容', + 'data' => [ + 'friend_count' => count($collectedData), + 'collected_count' => $totalMomentsCount, + 'details' => $collectedData + ] + ]; + + } catch (\Exception $e) { + \think\facade\Log::error('从好友采集朋友圈失败: ' . $e->getMessage() . ' [内容库ID: ' . ($library['id'] ?? 'unknown') . ']'); + return [ + 'status' => 'error', + 'message' => '采集过程发生错误: ' . $e->getMessage() + ]; + } + } + + /** + * 应用关键词过滤规则 + * @param string $content 内容文本 + * @param array $includeKeywords 包含关键词 + * @param array $excludeKeywords 排除关键词 + * @return bool 是否通过过滤 + */ + private function passKeywordFilter($content, $includeKeywords, $excludeKeywords) + { + // 如果内容为空,跳过 + if (empty($content)) { + return false; + } + + // 检查是否包含必须关键词 + $includeMatch = empty($includeKeywords); + if (!empty($includeKeywords)) { + foreach ($includeKeywords as $keyword) { + if (strpos($content, $keyword) !== false) { + $includeMatch = true; + break; + } + } + } + + // 如果不满足包含条件,跳过 + if (!$includeMatch) { + return false; + } + + // 检查是否包含排除关键词 + if (!empty($excludeKeywords)) { + foreach ($excludeKeywords as $keyword) { + if (strpos($content, $keyword) !== false) { + return false; // 包含排除关键词,不通过过滤 + } + } + } + + return true; // 通过所有过滤条件 + } + + /** + * 从群组采集消息内容 + * @param array $library 内容库配置 + * @return array 采集结果 + */ + private function collectFromGroups($library) + { + $groupIds = $library['sourceGroups']; + if (empty($groupIds)) { + return [ + 'status' => 'failed', + 'message' => '没有指定要采集的群组' + ]; + } + + try { + // 查询群组信息 + $groups = Db::table('s2_wechat_chatroom')->alias('g') + ->field('g.id, g.chatroomId, g.nickname as name, g.wechatAccountWechatId as ownerWechatId') + ->whereIn('g.id', $groupIds) + ->where('g.deleteTime', 0) + ->select(); + + if (empty($groups)) { + return [ + 'status' => 'failed', + 'message' => '未找到有效的群组信息' + ]; + } + + // 获取群成员信息 + $groupMembers = $library['groupMembers']; + if (empty($groupMembers)) { + // 如果没有指定群成员,则尝试获取所有群成员 + return [ + 'status' => 'failed', + 'message' => '未找到有效的群成员信息' + ]; + } + + // groupMembers格式: {"826825": ["413771", "413769"], "840818": ["496300", "496302"]} + // 键是群组ID,值是该群组的成员ID数组 + // 需要按群组分组处理,确保每个群组只采集该群组配置的成员 + + // 建立群组ID到成员ID数组的映射 + $groupIdToMemberIds = []; + if (is_array($groupMembers)) { + foreach ($groupMembers as $groupId => $memberIds) { + if (is_array($memberIds) && !empty($memberIds)) { + $groupIdToMemberIds[$groupId] = $memberIds; + } + } + } + if (empty($groupIdToMemberIds)) { + return [ + 'status' => 'failed', + 'message' => '未找到有效的群成员ID' + ]; + } + + // 为每个群组查询成员信息,建立群组ID到成员wechatId数组的映射 + $groupIdToMemberWechatIds = []; + foreach ($groupIdToMemberIds as $groupId => $memberIds) { + // 查询该群组的成员信息,获取wechatId + $members = Db::table('s2_wechat_chatroom_member') + ->field('id, wechatId') + ->whereIn('id', $memberIds) + ->select(); + + $wechatIds = []; + foreach ($members as $member) { + if (!empty($member['wechatId'])) { + $wechatIds[] = $member['wechatId']; + } + } + + if (!empty($wechatIds)) { + $groupIdToMemberWechatIds[$groupId] = array_unique($wechatIds); + } + } + if (empty($groupIdToMemberWechatIds)) { + return [ + 'status' => 'failed', + 'message' => '未找到有效的群成员微信ID' + ]; + } + + // 从群组采集内容 + $collectedData = []; + $totalMessagesCount = 0; + $chatroomIds = array_column($groups, 'id'); + + // 获取群消息 - 支持时间范围过滤(先不添加群成员过滤,后面按群组分别过滤) + $messageWhere = [ + ['wechatChatroomId', 'in', $chatroomIds], + ['type', '=', 2] + ]; + + // 如果启用时间限制 + if ($library['timeEnabled'] && $library['timeStart'] > 0 && $library['timeEnd'] > 0) { + $messageWhere[] = ['createTime', 'between', [$library['timeStart'], $library['timeEnd']]]; + } + + // 查询群消息(先查询所有消息,后面按群组和成员过滤) + $groupMessages = Db::table('s2_wechat_message') + ->where($messageWhere) + ->order('createTime', 'desc') + ->limit(500) // 限制最大消息数量 + ->select(); + if (empty($groupMessages)) { + return [ + 'status' => 'warning', + 'message' => '未找到符合条件的群消息' + ]; + } + // 按群组分组处理消息 + $groupedMessages = []; + foreach ($groupMessages as $message) { + $chatroomId = $message['wechatChatroomId']; + $senderWechatId = $message['senderWechatId'] ?? ''; + + // 找到对应的群组信息 + $groupInfo = null; + foreach ($groups as $group) { + if ($group['id'] == $chatroomId) { + $groupInfo = $group; + break; + } + } + + if (!$groupInfo) { + continue; + } + + // 检查该消息的发送者是否在该群组的配置成员列表中 + $groupId = $groupInfo['id']; + if (!isset($groupIdToMemberWechatIds[$groupId])) { + // 该群组没有配置成员,跳过 + continue; + } + + // 检查发送者是否在配置的成员列表中 + if (!in_array($senderWechatId, $groupIdToMemberWechatIds[$groupId])) { + // 发送者不在该群组的配置成员列表中,跳过 + continue; + } + + if (!isset($groupedMessages[$chatroomId])) { + $groupedMessages[$chatroomId] = [ + 'count' => 0, + 'messages' => [] + ]; + } + + // 处理消息内容 + $content = $message['content'] ?? ''; + + // 如果启用了关键词过滤 + $includeKeywords = $library['keywordInclude']; + $excludeKeywords = $library['keywordExclude']; + + + // 检查是否包含必须关键词 + $includeMatch = empty($includeKeywords); + if (!empty($includeKeywords)) { + foreach ($includeKeywords as $keyword) { + if (strpos($content, $keyword) !== false) { + $includeMatch = true; + break; + } + } + } + + // 如果不满足包含条件,跳过 + if (!$includeMatch) { + continue; + } + + // 检查是否包含排除关键词 + $excludeMatch = false; + if (!empty($excludeKeywords)) { + foreach ($excludeKeywords as $keyword) { + if (strpos($content, $keyword) !== false) { + $excludeMatch = true; + break; + } + } + } + + // 如果满足排除条件,跳过 + if ($excludeMatch) { + continue; + } + + + // 如果启用了AI处理 + if (!empty($library['aiEnabled']) && !empty($content)) { + $contentAi = $this->aiRewrite($library, $content); + if (!empty($contentAi)) { + $message['contentAi'] = $contentAi; + } else { + $message['contentAi'] = ''; + } + } + + + // 保存消息到内容库 + $this->saveMessageToContentItem($message, $library['id'], $groupInfo); + + // 累计计数 + $groupedMessages[$chatroomId]['count']++; + $groupedMessages[$chatroomId]['messages'][] = [ + 'id' => $message['id'], + 'content' => mb_substr($content, 0, 50) . (mb_strlen($content) > 50 ? '...' : ''), + 'sender' => $message['senderNickname'], + 'time' => date('Y-m-d H:i:s', $message['createTime']) + ]; + + $totalMessagesCount++; + } + + // 构建结果数据 + foreach ($groups as $group) { + $chatroomId = $group['chatroomId']; + if (isset($groupedMessages[$chatroomId]) && $groupedMessages[$chatroomId]['count'] > 0) { + $collectedData[$chatroomId] = [ + 'groupId' => $group['id'], + 'groupName' => $group['name'], + 'count' => $groupedMessages[$chatroomId]['count'], + 'messages' => $groupedMessages[$chatroomId]['messages'] + ]; + } + } + + if (empty($collectedData)) { + return [ + 'status' => 'warning', + 'message' => '未采集到符合条件的群消息内容' + ]; + } + + return [ + 'status' => 'success', + 'message' => '成功采集到' . count($collectedData) . '个群的' . $totalMessagesCount . '条消息', + 'data' => [ + 'group_count' => count($collectedData), + 'collected_count' => $totalMessagesCount, + 'details' => $collectedData + ] + ]; + + } catch (\Exception $e) { + return [ + 'status' => 'error', + 'message' => '采集过程发生错误: ' . $e->getMessage() + ]; + } + } + + /** + * 判断内容类型 + * @param string $content 内容文本 + * @param array $resUrls 资源URL数组 + * @param array $urls URL数组 + * @return int 内容类型: 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序 + */ + private function determineContentType($content, $resUrls = [], $urls = []) + { + // 判断是否为空 + if (empty($content) && empty($resUrls) && empty($urls)) { + return 0; // 未知类型 + } + + // 分析内容中可能包含的链接或图片地址 + if (!empty($content)) { + // 检查内容中是否有链接 + $urlPattern = '/https?:\/\/[-A-Za-z0-9+&@#\/%?=~_|!:,.;]+[-A-Za-z0-9+&@#\/%=~_|]/'; + preg_match_all($urlPattern, $content, $contentUrlMatches); + + if (!empty($contentUrlMatches[0])) { + // 将内容中的链接添加到urls数组中(去重) + foreach ($contentUrlMatches[0] as $url) { + if (!in_array($url, $urls)) { + $urls[] = $url; + } + } + } + + // 检查内容中是否包含图片或视频链接 + foreach ($contentUrlMatches[0] ?? [] as $url) { + // 检查是否为图片文件 + if (stripos($url, '.jpg') !== false || + stripos($url, '.jpeg') !== false || + stripos($url, '.png') !== false || + stripos($url, '.gif') !== false || + stripos($url, '.webp') !== false || + stripos($url, '.bmp') !== false || + stripos($url, 'image') !== false) { + if (!in_array($url, $resUrls)) { + $resUrls[] = $url; + } + } + + // 检查是否为视频文件 + if (stripos($url, '.mp4') !== false || + stripos($url, '.mov') !== false || + stripos($url, '.avi') !== false || + stripos($url, '.wmv') !== false || + stripos($url, '.flv') !== false || + stripos($url, 'video') !== false) { + if (!in_array($url, $resUrls)) { + $resUrls[] = $url; + } + } + } + } + + // 判断是否有小程序信息 + if (strpos($content, '小程序') !== false || strpos($content, 'appid') !== false) { + return 5; // 小程序 + } + + // 检查资源URL中是否有视频或图片 + $hasVideo = false; + $hasImage = false; + + if (!empty($resUrls)) { + foreach ($resUrls as $url) { + // 检查是否为视频文件 + if (stripos($url, '.mp4') !== false || + stripos($url, '.mov') !== false || + stripos($url, '.avi') !== false || + stripos($url, '.wmv') !== false || + stripos($url, '.flv') !== false || + stripos($url, 'video') !== false) { + $hasVideo = true; + break; // 一旦发现视频文件,立即退出循环 + } + + // 检查是否为图片文件 + if (stripos($url, '.jpg') !== false || + stripos($url, '.jpeg') !== false || + stripos($url, '.png') !== false || + stripos($url, '.gif') !== false || + stripos($url, '.webp') !== false || + stripos($url, '.bmp') !== false || + stripos($url, 'image') !== false) { + $hasImage = true; + // 不退出循环,继续检查是否有视频(视频优先级更高) + } + } + } + + // 如果发现视频文件,判定为视频类型 + if ($hasVideo) { + return 3; // 视频 + } + + // 判断内容是否纯链接 + $isPureLink = false; + if (!empty($content) && !empty($urls)) { + $contentWithoutUrls = $content; + foreach ($urls as $url) { + $contentWithoutUrls = str_replace($url, '', $contentWithoutUrls); + } + // 如果去除链接后内容为空,则认为是纯链接 + if (empty(trim($contentWithoutUrls))) { + $isPureLink = true; + } + } + + // 如果内容是纯链接,判定为链接类型 + if ($isPureLink) { + return 2; // 链接 + } + + // 优先判断内容文本 + // 如果有文本内容(不仅仅是链接) + if (!empty($content) && !$isPureLink) { + // 如果有图片,则为图文类型 + if ($hasImage) { + return 1; // 图文 + } else { + return 4; // 纯文本 + } + } + + // 判断是否为图片类型 + if ($hasImage) { + return 1; // 图片 + } + + // 判断是否为链接类型 + if (!empty($urls)) { + return 2; // 链接 + } + + // 默认为文本类型 + return 4; // 文本 + } + + /** + * 保存朋友圈数据到内容项目表 + * @param array $moment 朋友圈数据 + * @param int $libraryId 内容库ID + * @param array $friend 好友信息 + * @param string $nickname 好友昵称 + * @return bool 是否保存成功 + */ + private function saveMomentToContentItem($moment, $libraryId, $friend, $nickname) + { + if (empty($moment) || empty($libraryId)) { + return false; + } + + + try { + + // 检查朋友圈数据是否已存在于内容项目中 + $exists = ContentItem::where('libraryId', $libraryId) + ->where('snsId', $moment['snsId'] ?? '') + ->find(); + + + // 解析资源URL (可能是JSON字符串) + $resUrls = $moment['resUrls']; + if (is_string($resUrls)) { + $resUrls = json_decode($resUrls, true); + } + + // 处理urls字段 + $urls = $moment['urls'] ?? []; + if (is_string($urls)) { + $urls = json_decode($urls, true); + } + + // 构建封面图片 + $coverImage = ''; + if (!empty($resUrls) && is_array($resUrls) && count($resUrls) > 0) { + $coverImage = $resUrls[0]; + } + + // 判断内容类型 (0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序) + if ($moment['type'] == 1) { + //图文 + $contentType = 1; + } elseif ($moment['type'] == 3) { + //链接 + $contentType = 2; + $urls = []; + $url = is_string($moment['urls']) ? json_decode($moment['urls'], true) : $moment['urls'] ?? []; + $url = $url[0]; + + //兼容链接采集不到标题及图标 + if (empty($moment['title'])) { + // 检查是否是飞书链接 + if (strpos($url, 'feishu.cn') !== false) { + // 飞书文档需要登录,无法直接获取内容,返回默认信息 + $urls[] = [ + 'url' => $url, + 'image' => 'http://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/2025/07/09/3db2a5d7fe49011ab68175a42a5094ce.jpeg', + 'desc' => '飞书文档' + ]; + } else { + $getUrlDetails = $this->getExternalPageDetails($url); + $icon = 'http://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/2025/07/09/ec039d96fad6eab1d960f207d3d9ca9f.jpeg'; + if (!empty($getUrlDetails['title'])) { + $urls[] = [ + 'url' => $url, + 'image' => $icon, + 'desc' => '点击查看详情' + ]; + } else { + $urls[] = [ + 'url' => $url, + 'image' => !empty($getUrlDetails['icon']) ? $getUrlDetails['icon'] : $icon, + 'desc' => $getUrlDetails['title'] + ]; + } + } + } else { + if (strpos($url, 'feishu.cn') !== false) { + $coverImage = 'http://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/2025/07/09/3db2a5d7fe49011ab68175a42a5094ce.jpeg'; + } else { + $coverImage = 'http://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/2025/07/09/ec039d96fad6eab1d960f207d3d9ca9f.jpeg'; + } + + $urls[] = [ + 'url' => $url, + 'image' => !empty($moment['coverImage']) ? $moment['coverImage'] : $coverImage, + 'desc' => $moment['title'] + ]; + } + $moment['urls'] = $urls; + } elseif ($moment['type'] == 15) { + //视频 + $contentType = 3; + } elseif ($moment['type'] == 2) { + //纯文本 + $contentType = 4; + } elseif ($moment['type'] == 30) { + //小程序 + $contentType = 5; + } else { + $contentType = 1; + } + + // 如果不存在,则创建新的内容项目 + + if (empty($exists)) { + $exists = new ContentItem(); + } + + $exists->libraryId = $libraryId; + $exists->type = 'moment'; // 朋友圈类型 + $exists->title = '来自 ' . $nickname . ' 的朋友圈'; + $exists->contentData = json_encode($moment, JSON_UNESCAPED_UNICODE); + $exists->snsId = $moment['snsId'] ?? ''; // 存储snsId便于后续查询 + $exists->createTime = time(); + $exists->wechatId = $friend['wechatId']; + $exists->friendId = $friend['id']; + $exists->createMomentTime = $moment['createTime'] ?? 0; + $exists->content = $moment['content'] ?? ''; + $exists->contentAi = $moment['contentAi'] ?? ''; + $exists->coverImage = $coverImage; + $exists->contentType = $contentType; // 设置内容类型 + $exists->ossUrls = $moment['ossUrls'] ?? json_decode([]); + + // 独立存储resUrls和urls字段 + $exists->resUrls = is_string($moment['resUrls']) ? $moment['resUrls'] : json_encode($resUrls, JSON_UNESCAPED_UNICODE); + $exists->urls = is_string($moment['urls']) ? $moment['urls'] : json_encode($urls, JSON_UNESCAPED_UNICODE); + + // 保存地理位置信息 + $exists->location = $moment['location'] ?? ''; + $exists->lat = $moment['lat'] ?? 0; + $exists->lng = $moment['lng'] ?? 0; + $exists->save(); + + + return true; + } catch (\Exception $e) { + // 记录错误日志 + \think\facade\Log::error('保存朋友圈数据失败: ' . $e->getMessage()); + return false; + } + } + + /** + * 保存群聊消息到内容项目表 + * @param array $message 消息数据 + * @param int $libraryId 内容库ID + * @param array $group 群组信息 + * @return bool 是否保存成功 + */ + private function saveMessageToContentItem($message, $libraryId, $group) + { + if (empty($message) || empty($libraryId)) { + return false; + } + + try { + // 检查消息是否已存在于内容项目中 + $exists = ContentItem::where('libraryId', $libraryId) + ->where('msgId', $message['msgSvrId'] ?? '') + ->find(); + + if ($exists) { + return true; + } + + $resUrls = []; + $links = []; + $contentType = 4; + $content = ''; + switch ($message['msgType']) { + case 1: // 文字 + $content = $message['content']; + $contentType = 4; + break; + case 3: //图片 + $resUrls[] = $message['content']; + $contentType = 1; + break; + case 47: //动态图片 + $resUrls[] = $message['content']; + $contentType = 1; + break; + case 34: //语言 + return false; + case 43: //视频 + $resUrls[] = $message['content']; + $contentType = 3; + break; + case 42: //名片 + return false; + case 49: //文件 链接 + $link = json_decode($message['content'], true); + switch ($link['type']) { + case 'link': + $links[] = [ + 'desc' => $link['desc'], + 'image' => $link['thumbPath'], + 'url' => $link['url'], + ]; + $contentType = 2; + break; + default: + return false; + } + break; + default: + return false; + } + + // 创建新的内容项目 + $item = new ContentItem(); + $item->libraryId = $libraryId; + $item->type = 'group_message'; // 群消息类型 + $item->title = '来自 ' . ($group['name'] ?? '未知群组') . ' 的消息'; + $item->contentData = json_encode($message, JSON_UNESCAPED_UNICODE); + $item->msgId = $message['msgSvrId'] ?? ''; // 存储msgSvrId便于后续查询 + $item->createTime = time(); + $item->content = $content; + $item->contentType = $contentType; // 设置内容类型 + + // 设置发送者信息 + $item->wechatId = $message['senderWechatId'] ?? ''; + $item->wechatChatroomId = $message['wechatChatroomId'] ?? ''; + $item->senderNickname = $message['senderNickname'] ?? ''; + $item->createMessageTime = $message['createTime'] ?? 0; + + // 处理资源URL + if (!empty($resUrls)) { + $item->resUrls = json_encode($resUrls, JSON_UNESCAPED_UNICODE); + // 设置封面图片 + if (!empty($resUrls[0])) { + $item->coverImage = $resUrls[0]; + } + } else { + $item->resUrls = json_encode([], JSON_UNESCAPED_UNICODE); + } + + // 处理链接 + if (!empty($links)) { + $item->urls = json_encode($links, JSON_UNESCAPED_UNICODE); + } else { + $item->urls = json_encode([], JSON_UNESCAPED_UNICODE); + } + $item->ossUrls = json_encode([], JSON_UNESCAPED_UNICODE); + + // 设置商品信息(需根据消息内容解析) + $this->extractProductInfo($item, $content); + + $item->save(); + return true; + } catch (\Exception $e) { + // 记录错误日志 + \think\facade\Log::error('保存群消息数据失败: ' . $e->getMessage()); + return false; + } + } + + /** + * 从消息内容中提取商品信息 + * @param ContentItem $item 内容项目对象 + * @param string $content 消息内容 + * @return void + */ + private function extractProductInfo($item, $content) + { + // 尝试提取商品名称 + $titlePatterns = [ + '/【(.+?)】/', // 匹配【】中的内容 + '/《(.+?)》/', // 匹配《》中的内容 + '/商品名称[::](.+?)[\r\n]/' // 匹配"商品名称:"后的内容 + ]; + + foreach ($titlePatterns as $pattern) { + preg_match($pattern, $content, $matches); + if (!empty($matches[1])) { + $item->productTitle = trim($matches[1]); + break; + } + } + + // 如果没有找到商品名称,尝试使用内容的前部分作为标题 + if (empty($item->productTitle)) { + // 获取第一行非空内容作为标题 + $lines = explode("\n", $content); + foreach ($lines as $line) { + $line = trim($line); + if (!empty($line) && mb_strlen($line) > 2) { + $item->productTitle = mb_substr($line, 0, 30); + break; + } + } + } + } + + /** + * 获取朋友圈数据 + * @param string $wechatId 微信ID + * @return array 朋友圈数据 + */ + private function getMomentsData($wechatId) + { + // 这里应该是实际从API或数据库获取朋友圈数据的逻辑 + // 这里仅作示例返回 + return [ + // 示例数据 + ['id' => 1, 'content' => '今天天气真好!', 'createTime' => time() - 3600], + ['id' => 2, 'content' => '分享一个有趣的项目', 'createTime' => time() - 7200], + ]; + } + + /** + * 根据关键词过滤朋友圈内容 + * @param array $moments 朋友圈内容 + * @param array $includeKeywords 包含关键词 + * @param array $excludeKeywords 排除关键词 + * @return array 过滤后的内容 + */ + private function filterMomentsByKeywords($moments, $includeKeywords, $excludeKeywords) + { + if (empty($moments)) { + return []; + } + + $filtered = []; + foreach ($moments as $moment) { + $content = $moment['content'] ?? ''; + + // 如果内容为空,跳过 + if (empty($content)) { + continue; + } + + // 检查是否包含必须关键词 + $includeMatch = empty($includeKeywords); + if (!empty($includeKeywords)) { + foreach ($includeKeywords as $keyword) { + if (strpos($content, $keyword) !== false) { + $includeMatch = true; + break; + } + } + } + + // 如果不满足包含条件,跳过 + if (!$includeMatch) { + continue; + } + + // 检查是否包含排除关键词 + $excludeMatch = false; + if (!empty($excludeKeywords)) { + foreach ($excludeKeywords as $keyword) { + if (strpos($content, $keyword) !== false) { + $excludeMatch = true; + break; + } + } + } + + // 如果满足排除条件,跳过 + if ($excludeMatch) { + continue; + } + + // 通过所有过滤,添加到结果中 + $filtered[] = $moment; + } + + return $filtered; + } + + /** + * 使用AI处理采集的数据 + * @param array $data 采集的数据 + * @param string $prompt AI提示词 + * @return array 处理后的数据 + */ + private function processWithAI($data, $prompt) + { + // 这里应该是调用AI处理数据的逻辑 + // 实际实现需要根据具体的AI API + return $data; + } + + /** + * 保存采集的数据到内容项目 + * @param array $data 采集的数据 + * @param int $libraryId 内容库ID + * @return bool 是否保存成功 + */ + private function saveCollectedData($data, $libraryId) + { + if (empty($data) || empty($libraryId)) { + return false; + } + + try { + foreach ($data as $wechatId => $userData) { + foreach ($userData['moments'] as $moment) { + // 创建内容项目 + $item = new ContentItem; + $item->libraryId = $libraryId; + $item->type = 'moment'; // 朋友圈类型 + $item->title = '来自 ' . $userData['nickname'] . ' 的朋友圈'; + $item->contentData = json_encode($moment); + $item->createTime = time(); + $item->save(); + } + } + return true; + } catch (\Exception $e) { + // 记录错误日志 + \think\facade\Log::error('保存采集数据失败: ' . $e->getMessage()); + return false; + } + } + + /** + * 获取所有群成员 + * @param array $groupIds 群组ID列表 + * @return array 群成员列表 + */ + private function getAllGroupMembers($groupIds) + { + if (empty($groupIds)) { + return []; + } + + try { + // 查询群成员信息 + $members = Db::name('wechat_group_member')->alias('gm') + ->field('gm.id, gm.memberId, gm.groupId, wa.nickname') + ->join('wechat_account wa', 'gm.memberId = wa.wechatId') + ->whereIn('gm.groupId', $groupIds) + ->where('gm.isDel', 0) + ->select(); + + return $members; + } catch (\Exception $e) { + \think\facade\Log::error('获取群成员失败: ' . $e->getMessage()); + return []; + } + } + + + /** + * 解析URL获取网页信息(内部调用) + * @param string $url 要解析的URL + * @return array 包含title、icon的数组,失败返回空数组 + */ + public function parseUrl($url) + { + if (empty($url) || !filter_var($url, FILTER_VALIDATE_URL)) { + return []; + } + + try { + // 设置请求头,模拟浏览器访问 + $context = stream_context_create([ + 'http' => [ + 'method' => 'GET', + 'header' => [ + 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8', + 'Accept-Encoding: gzip, deflate', + 'Connection: keep-alive', + 'Upgrade-Insecure-Requests: 1' + ], + 'timeout' => 10, + 'follow_location' => true, + 'max_redirects' => 3 + ] + ]); + + // 获取网页内容 + $html = @file_get_contents($url, false, $context); + + if ($html === false) { + return []; + } + + // 检测编码并转换为UTF-8 + $encoding = mb_detect_encoding($html, ['UTF-8', 'GBK', 'GB2312', 'BIG5', 'ASCII']); + if ($encoding && $encoding !== 'UTF-8') { + $html = mb_convert_encoding($html, 'UTF-8', $encoding); + } + + // 解析HTML + $dom = new \DOMDocument(); + @$dom->loadHTML($html, LIBXML_NOERROR | LIBXML_NOWARNING); + $xpath = new \DOMXPath($dom); + + $result = [ + 'title' => '', + 'icon' => '', + 'url' => $url + ]; + + // 提取标题 + $titleNodes = $xpath->query('//title'); + if ($titleNodes->length > 0) { + $result['title'] = trim($titleNodes->item(0)->textContent); + } + + // 提取图标 - 优先获取favicon + $iconNodes = $xpath->query('//link[@rel="icon"]/@href | //link[@rel="shortcut icon"]/@href | //link[@rel="apple-touch-icon"]/@href'); + if ($iconNodes->length > 0) { + $iconUrl = trim($iconNodes->item(0)->value); + $result['icon'] = $this->makeAbsoluteUrl($iconUrl, $url); + } else { + // 尝试获取Open Graph图片 + $ogImageNodes = $xpath->query('//meta[@property="og:image"]/@content'); + if ($ogImageNodes->length > 0) { + $result['icon'] = trim($ogImageNodes->item(0)->value); + } else { + // 默认favicon路径 + $result['icon'] = $this->makeAbsoluteUrl('/favicon.ico', $url); + } + } + + // 清理和验证数据 + $result['title'] = $this->cleanText($result['title']); + + return $result; + + } catch (\Exception $e) { + // 记录错误日志但不抛出异常 + \think\facade\Log::error('URL解析失败: ' . $e->getMessage() . ' URL: ' . $url); + return []; + } + } + + + /** + * 将相对URL转换为绝对URL + * @param string $relativeUrl 相对URL + * @param string $baseUrl 基础URL + * @return string 绝对URL + */ + private function makeAbsoluteUrl($relativeUrl, $baseUrl) + { + if (empty($relativeUrl)) { + return ''; + } + + // 如果已经是绝对URL,直接返回 + if (filter_var($relativeUrl, FILTER_VALIDATE_URL)) { + return $relativeUrl; + } + + // 解析基础URL + $baseParts = parse_url($baseUrl); + if (!$baseParts) { + return $relativeUrl; + } + + // 处理以/开头的绝对路径 + if (strpos($relativeUrl, '/') === 0) { + return $baseParts['scheme'] . '://' . $baseParts['host'] . + (isset($baseParts['port']) ? ':' . $baseParts['port'] : '') . + $relativeUrl; + } + + // 处理相对路径 + $basePath = isset($baseParts['path']) ? dirname($baseParts['path']) : '/'; + if ($basePath === '.') { + $basePath = '/'; + } + + return $baseParts['scheme'] . '://' . $baseParts['host'] . + (isset($baseParts['port']) ? ':' . $baseParts['port'] : '') . + $basePath . '/' . $relativeUrl; + } + + /** + * 清理文本内容 + * @param string $text 要清理的文本 + * @return string 清理后的文本 + */ + private function cleanText($text) + { + if (empty($text)) { + return ''; + } + + // 移除HTML实体 + $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + + // 移除多余的空白字符 + $text = preg_replace('/\s+/', ' ', $text); + + // 移除控制字符 + $text = preg_replace('/[\x00-\x1F\x7F]/', '', $text); + + return trim($text); + } + + + public function aiRewrite($library = [], $content = '') + { + if (empty($library['aiEnabled']) && empty($content)) { + return false; + } + + // 此处实现AI处理逻辑,暂未实现 + $utl = Env::get('doubaoAi.api_url', ''); + $apiKey = Env::get('doubaoAi.api_key', ''); + $model = Env::get('doubaoAi.model', 'doubao-1-5-pro-32k-250115'); + if (empty($apiKey)) { + return false; + } + + if (!empty($library['aiPrompt'])) { + $aiPrompt = $library['aiPrompt']; + } else { + $aiPrompt = '重写这条朋友圈 要求: +1、原本的字数和意思不要修改超过10% +2、出现品牌名或个人名字就去除'; + } + + $content = $aiPrompt . ' ' . $content; + $headerData = ['Authorization:Bearer ' . $apiKey]; + $header = setHeader($headerData); + + // 发送请求 + $params = [ + 'model' => $model, + 'messages' => [ + ['role' => 'system', 'content' => '你是人工智能助手.'], + ['role' => 'user', 'content' => $content], + ] + ]; + $result = requestCurl($utl, $params, 'POST', $header, 'json'); + $result = json_decode($result, true); + if (!empty($result['choices'])) { + $contentAI = $result['choices'][0]['message']['content']; + return $contentAI; + } else { + return false; + } + } + + /** + * 导入Excel表格(支持图片导入) + * @return \think\response\Json + */ + public function importExcel() + { + try { + $libraryId = $this->request->param('id', 0); + $companyId = $this->request->userInfo['companyId']; + $userId = $this->request->userInfo['id']; + $isAdmin = !empty($this->request->userInfo['isAdmin']); + + if (empty($libraryId)) { + return json(['code' => 400, 'msg' => '内容库ID不能为空']); + } + + // 验证内容库权限 + $libraryWhere = [ + ['id', '=', $libraryId], + ['companyId', '=', $companyId], + ['isDel', '=', 0] + ]; + + if (!$isAdmin) { + $libraryWhere[] = ['userId', '=', $userId]; + } + + $library = ContentLibrary::where($libraryWhere)->find(); + if (empty($library)) { + return json(['code' => 500, 'msg' => '内容库不存在或无权限访问']); + } + + // 获取文件(可能是上传的文件或远程URL) + $fileUrl = $this->request->param('fileUrl', ''); + $file = Request::file('file'); + $tmpFile = ''; + + if (!empty($fileUrl)) { + // 处理远程URL + if (!preg_match('/^https?:\/\//i', $fileUrl)) { + return json(['code' => 400, 'msg' => '无效的文件URL']); + } + + // 验证文件扩展名 + $urlExt = strtolower(pathinfo(parse_url($fileUrl, PHP_URL_PATH), PATHINFO_EXTENSION)); + if (!in_array($urlExt, ['xls', 'xlsx'])) { + return json(['code' => 400, 'msg' => '只支持Excel文件(.xls, .xlsx)']); + } + + // 下载远程文件到临时目录 + $tmpFile = tempnam(sys_get_temp_dir(), 'excel_import_') . '.' . $urlExt; + $fileContent = @file_get_contents($fileUrl); + + if ($fileContent === false) { + return json(['code' => 400, 'msg' => '下载远程文件失败,请检查URL是否可访问']); + } + + file_put_contents($tmpFile, $fileContent); + + } elseif ($file) { + // 处理上传的文件 + $ext = strtolower($file->getExtension()); + if (!in_array($ext, ['xls', 'xlsx'])) { + return json(['code' => 400, 'msg' => '只支持Excel文件(.xls, .xlsx)']); + } + + // 保存临时文件 + $tmpFile = $file->getRealPath(); + if (empty($tmpFile)) { + $savePath = $file->move(sys_get_temp_dir()); + $tmpFile = $savePath->getRealPath(); + } + } else { + return json(['code' => 400, 'msg' => '请上传Excel文件或提供文件URL']); + } + + if (empty($tmpFile) || !file_exists($tmpFile)) { + return json(['code' => 400, 'msg' => '文件不存在或无法访问']); + } + + // 加载Excel文件 + $excel = PHPExcel_IOFactory::load($tmpFile); + $sheet = $excel->getActiveSheet(); + + // 获取所有图片 + $images = []; + try { + $drawings = $sheet->getDrawingCollection(); + foreach ($drawings as $drawing) { + if ($drawing instanceof \PHPExcel_Worksheet_Drawing) { + $coordinates = $drawing->getCoordinates(); + $imagePath = $drawing->getPath(); + + // 如果是嵌入的图片(zip://格式),提取到临时文件 + if (strpos($imagePath, 'zip://') === 0) { + $zipEntry = str_replace('zip://', '', $imagePath); + $zipEntry = explode('#', $zipEntry); + $zipFile = $zipEntry[0]; + $imageEntry = isset($zipEntry[1]) ? $zipEntry[1] : ''; + + if (!empty($imageEntry)) { + $zip = new \ZipArchive(); + if ($zip->open($zipFile) === true) { + $imageContent = $zip->getFromName($imageEntry); + if ($imageContent !== false) { + $tempImageFile = tempnam(sys_get_temp_dir(), 'excel_img_'); + file_put_contents($tempImageFile, $imageContent); + $images[$coordinates] = $tempImageFile; + } + $zip->close(); + } + } + } elseif (file_exists($imagePath)) { + // 如果是外部文件路径 + $images[$coordinates] = $imagePath; + } + } elseif ($drawing instanceof \PHPExcel_Worksheet_MemoryDrawing) { + // 处理内存中的图片 + $coordinates = $drawing->getCoordinates(); + $imageResource = $drawing->getImageResource(); + + if ($imageResource) { + $tempImageFile = tempnam(sys_get_temp_dir(), 'excel_img_') . '.png'; + $imageType = $drawing->getMimeType(); + + switch ($imageType) { + case 'image/png': + imagepng($imageResource, $tempImageFile); + break; + case 'image/jpeg': + case 'image/jpg': + imagejpeg($imageResource, $tempImageFile); + break; + case 'image/gif': + imagegif($imageResource, $tempImageFile); + break; + default: + imagepng($imageResource, $tempImageFile); + } + + $images[$coordinates] = $tempImageFile; + } + } + } + } catch (\Exception $e) { + \think\facade\Log::error('提取Excel图片失败:' . $e->getMessage()); + } + + // 读取数据(实际内容从第三行开始,前两行是标题和说明) + $data = $sheet->toArray(); + if (count($data) < 3) { + return json(['code' => 400, 'msg' => 'Excel文件数据为空']); + } + + // 移除前两行(标题行和说明行) + array_shift($data); // 移除第1行 + array_shift($data); // 移除第2行 + + $successCount = 0; + $failCount = 0; + $errors = []; + + Db::startTrans(); + try { + foreach ($data as $rowIndex => $row) { + $rowNum = $rowIndex + 3; // Excel行号(从3开始,因为前两行是标题和说明) + + // 跳过空行 + if (empty(array_filter($row))) { + continue; + } + + try { + // 解析数据(根据图片中的表格结构) + // A:日期, B:投放时间, C:作用分类, D:朋友圈文案, E:自回评内容, F:朋友圈展示形式, G-O:配图1-9 + $date = isset($row[0]) ? trim($row[0]) : ''; + $placementTime = isset($row[1]) ? trim($row[1]) : ''; + $functionCategory = isset($row[2]) ? trim($row[2]) : ''; + $content = isset($row[3]) ? trim($row[3]) : ''; + $selfReply = isset($row[4]) ? trim($row[4]) : ''; + $displayForm = isset($row[5]) ? trim($row[5]) : ''; + + // 如果没有朋友圈文案,跳过 + if (empty($content)) { + continue; + } + + // 提取配图(G-O列,索引6-14) + $imageUrls = []; + for ($colIndex = 6; $colIndex <= 14; $colIndex++) { + $columnLetter = $this->columnLetter($colIndex); + $cellCoordinate = $columnLetter . $rowNum; + + // 检查是否有图片 + if (isset($images[$cellCoordinate])) { + $imagePath = $images[$cellCoordinate]; + + // 上传图片到OSS + $imageExt = 'jpg'; + if (file_exists($imagePath)) { + $imageInfo = @getimagesize($imagePath); + if ($imageInfo) { + $imageExt = image_type_to_extension($imageInfo[2], false); + if ($imageExt === 'jpeg') { + $imageExt = 'jpg'; + } + } + } + + $objectName = AliyunOSS::generateObjectName('excel_img_' . $rowNum . '_' . ($colIndex - 5) . '.' . $imageExt); + $uploadResult = AliyunOSS::uploadFile($imagePath, $objectName); + + if ($uploadResult['success']) { + $imageUrls[] = $uploadResult['url']; + } + } + } + + // 解析日期和时间 + $createMomentTime = 0; + if (!empty($date)) { + // 尝试解析日期格式:2025年11月25日 或 2025-11-25 + $dateStr = $date; + if (preg_match('/(\d{4})[年\-](\d{1,2})[月\-](\d{1,2})/', $dateStr, $matches)) { + $year = $matches[1]; + $month = str_pad($matches[2], 2, '0', STR_PAD_LEFT); + $day = str_pad($matches[3], 2, '0', STR_PAD_LEFT); + + // 解析时间 + $hour = 0; + $minute = 0; + if (!empty($placementTime) && preg_match('/(\d{1,2}):(\d{2})/', $placementTime, $timeMatches)) { + $hour = intval($timeMatches[1]); + $minute = intval($timeMatches[2]); + } + + $createMomentTime = strtotime("{$year}-{$month}-{$day} {$hour}:{$minute}:00"); + } + } + + if ($createMomentTime == 0) { + $createMomentTime = time(); + } + + // 判断内容类型 + $contentType = 4; // 默认文本 + if (!empty($imageUrls)) { + $contentType = 1; // 图文 + } + + // 创建内容项 + $item = new ContentItem(); + $item->libraryId = $libraryId; + $item->type = 'diy'; // 自定义类型 + $item->title = !empty($date) ? $date . ' ' . $placementTime : '导入的内容'; + $item->content = $content; + $item->comment = $selfReply; // 自回评内容 + $item->contentType = $contentType; + $item->resUrls = json_encode($imageUrls, JSON_UNESCAPED_UNICODE); + $item->urls = json_encode([], JSON_UNESCAPED_UNICODE); + $item->createMomentTime = $createMomentTime; + $item->createTime = time(); + + // 设置封面图片 + if (!empty($imageUrls[0])) { + $item->coverImage = $imageUrls[0]; + } + + // 保存其他信息到contentData + $contentData = [ + 'date' => $date, + 'placementTime' => $placementTime, + 'functionCategory' => $functionCategory, + 'displayForm' => $displayForm, + 'selfReply' => $selfReply + ]; + $item->contentData = json_encode($contentData, JSON_UNESCAPED_UNICODE); + + $item->save(); + $successCount++; + + } catch (\Exception $e) { + $failCount++; + $errors[] = "第{$rowNum}行处理失败:" . $e->getMessage(); + \think\facade\Log::error('导入Excel第' . $rowNum . '行失败:' . $e->getMessage()); + } + } + + Db::commit(); + + // 清理临时图片文件 + foreach ($images as $imagePath) { + if (file_exists($imagePath) && strpos($imagePath, sys_get_temp_dir()) === 0) { + @unlink($imagePath); + } + } + + // 清理临时Excel文件 + if (file_exists($tmpFile) && strpos($tmpFile, sys_get_temp_dir()) === 0) { + @unlink($tmpFile); + } + + return json([ + 'code' => 200, + 'msg' => '导入完成', + 'data' => [ + 'success' => $successCount, + 'fail' => $failCount, + 'errors' => $errors + ] + ]); + + } catch (\Exception $e) { + Db::rollback(); + + // 清理临时文件 + foreach ($images as $imagePath) { + if (file_exists($imagePath) && strpos($imagePath, sys_get_temp_dir()) === 0) { + @unlink($imagePath); + } + } + if (file_exists($tmpFile) && strpos($tmpFile, sys_get_temp_dir()) === 0) { + @unlink($tmpFile); + } + + return json(['code' => 500, 'msg' => '导入失败:' . $e->getMessage()]); + } + + } catch (\Exception $e) { + return json(['code' => 500, 'msg' => '导入失败:' . $e->getMessage()]); + } + } + + /** + * 根据列序号生成Excel列字母 + * @param int $index 列索引(从0开始) + * @return string 列字母(如A, B, C, ..., Z, AA, AB等) + */ + private function columnLetter($index) + { + $letters = ''; + do { + $letters = chr($index % 26 + 65) . $letters; + $index = intval($index / 26) - 1; + } while ($index >= 0); + return $letters; + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/Pay.php b/application/cunkebao/controller/Pay.php new file mode 100644 index 0000000..99553f0 --- /dev/null +++ b/application/cunkebao/controller/Pay.php @@ -0,0 +1,27 @@ + 111, + 'userId' => 111, + 'orderNo' => date('YmdHis') . rand(100000, 999999), + 'goodsId' => 34, + 'goodsName' => '测试测试', + 'orderType' => 1, + 'money' => 1 + ]; + + $paymentService = new PaymentService(); + $res = $paymentService->createOrder($order); + return $res; + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/Plan.php b/application/cunkebao/controller/Plan.php new file mode 100644 index 0000000..2c5f147 --- /dev/null +++ b/application/cunkebao/controller/Plan.php @@ -0,0 +1,254 @@ + Request::post('name', ''), + 'sceneId' => Request::post('sceneId', 0), + 'status' => Request::post('status', 0), + 'reqConf' => Request::post('reqConf', ''), + 'msgConf' => Request::post('msgConf', ''), + 'tagConf' => Request::post('tagConf', ''), + 'createTime' => time(), + 'updateTime' => time() + ]; + + // 验证必填字段 + if (empty($data['name'])) { + return ResponseHelper::error('计划名称不能为空', 400); + } + + if (empty($data['sceneId'])) { + return ResponseHelper::error('场景ID不能为空', 400); + } + + // 验证数据格式 + if (!$this->validateJson($data['reqConf'])) { + return ResponseHelper::error('好友申请设置格式不正确', 400); + } + + if (!$this->validateJson($data['msgConf'])) { + return ResponseHelper::error('消息设置格式不正确', 400); + } + + if (!$this->validateJson($data['tagConf'])) { + return ResponseHelper::error('标签设置格式不正确', 400); + } + + // 插入数据库 + $result = Db::name('friend_plan')->insert($data); + + if ($result) { + return ResponseHelper::success([], '添加计划任务成功'); + } else { + return ResponseHelper::error('添加计划任务失败', 500); + } + } catch (\Exception $e) { + return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500); + } + } + + /** + * 获取计划任务列表 + * + * @return \think\response\Json + */ + public function getList() + { + try { + // 获取分页参数 + $id = Request::param('id', 1); + $page = Request::param('page', 1); + $pageSize = Request::param('pageSize', 10); + + // 构建查询条件 + $where = []; + + // 过滤已删除的记录 + $where[] = ['deleteTime', 'null']; + + // 查询总数 + $total = Db::name('friend_plan')->where('sceneId', $id)->count(); + + // 查询列表数据 + $list = Db::name('friend_plan') + ->where('sceneId', $id) + ->field('id, name, status, createTime, updateTime, sceneId') + ->order('createTime desc') + ->page($page, $pageSize) + ->select(); + // 遍历列表,获取每个计划的统计信息 + foreach ($list as &$item) { + // 获取计划的统计信息 + $stats = $this->getPlanStats($item['id']); + + // 合并统计信息到结果中 + $item = array_merge($item, $stats); + + // 格式化状态为文字描述 + $item['statusText'] = $item['status'] == 1 ? '进行中' : '已暂停'; + + // 格式化时间 + $item['createTimeFormat'] = date('Y-m-d H:i', $item['createTime']); + + // 获取最近一次执行时间 + $lastExecution = $this->getLastExecution($item['id']); + $item['lastExecutionTime'] = $lastExecution['lastTime'] ?? ''; + $item['nextExecutionTime'] = $lastExecution['nextTime'] ?? ''; + } + + // 返回结果 + $result = [ + 'total' => $total, + 'list' => $list, + 'page' => $page, + 'pageSize' => $pageSize + ]; + + return ResponseHelper::success($result); + } catch (\Exception $e) { + return ResponseHelper::error('获取数据失败: ' . $e->getMessage(), 500); + } + } + + /** + * 获取计划的统计信息 + * + * @param int $planId 计划ID + * @return array + */ + private function getPlanStats($planId) + { + try { + // 获取设备数 + $deviceCount = $this->getDeviceCount($planId); + + // 获取已获客数 + $customerCount = $this->getCustomerCount($planId); + + // 获取已添加数 + $addedCount = 1; //$this->getAddedCount($planId); + + // 计算通过率 + $passRate = $customerCount > 0 ? round(($addedCount / $customerCount) * 100) : 0; + + return [ + 'deviceCount' => $deviceCount, + 'customerCount' => $customerCount, + 'addedCount' => $addedCount, + 'passRate' => $passRate + ]; + } catch (\Exception $e) { + return [ + 'deviceCount' => 0, + 'customerCount' => 0, + 'addedCount' => 0, + 'passRate' => 0 + ]; + } + } + + /** + * 获取计划使用的设备数 + * + * @param int $planId 计划ID + * @return int + */ + private function getDeviceCount($planId) + { + try { + // 获取计划 + $plan = Db::name('friend_plan')->where('id', $planId)->find(); + if (!$plan) { + return 0; + } + + // 解析reqConf + $reqConf = json_decode($plan['reqConf'], true); + + // 返回设备数量 + return isset($reqConf['selectedDevices']) ? count($reqConf['selectedDevices']) : 0; + } catch (\Exception $e) { + return 0; + } + } + + /** + * 获取计划的已获客数 + * + * @param int $planId 计划ID + * @return int + */ + private function getCustomerCount($planId) + { + // 模拟数据,实际应从相关表获取 + return rand(10, 50); + } + + /** + * 获取计划的已添加数 + * + * @param int $planId 计划ID + * @return int + */ + private function getAddedCount($planId) + { + // 模拟数据,实际应从相关表获取 + $customerCount = $this->getCustomerCount($planId); + return rand(5, $customerCount); + } + + /** + * 获取计划的最近一次执行时间 + * + * @param int $planId 计划ID + * @return array + */ + private function getLastExecution($planId) + { + // 模拟数据,实际应从执行记录表获取 + $now = time(); + $lastTime = $now - rand(3600, 86400); + $nextTime = $now + rand(3600, 86400); + + return [ + 'lastTime' => date('Y-m-d H:i', $lastTime), + 'nextTime' => date('Y-m-d H:i:s', $nextTime) + ]; + } + + /** + * 验证JSON格式是否正确 + * + * @param string $string + * @return bool + */ + private function validateJson($string) + { + if (empty($string)) { + return true; + } + + json_decode($string); + return (json_last_error() == JSON_ERROR_NONE); + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/RFMController.php b/application/cunkebao/controller/RFMController.php new file mode 100644 index 0000000..f9163db --- /dev/null +++ b/application/cunkebao/controller/RFMController.php @@ -0,0 +1,401 @@ +=', $startTime], + ['createTime', '<', $endTime], + ]; + + // identifier 条件 + if (!empty($identifier)) { + $where[] = ['identifier', '=', $identifier]; + } + + // ownerWechatId 条件 + if (!empty($ownerWechatId)) { + $where[] = ['ownerWechatId', '=', $ownerWechatId]; + } + + // 1. 数据过滤和聚合 - 获取每个客户的R、F、M原始值 + $orderModel = new TrafficOrderModel(); + $customers = $orderModel + ->where($where) + ->where(function ($query) { + // 只统计有效订单(actualPay大于0) + $query->where('actualPay', '>', 0); + }) + ->field('identifier, MAX(createTime) as lastOrderTime, COUNT(DISTINCT id) as orderCount, SUM(CAST(actualPay AS DECIMAL(18,2))) as totalAmount') + ->group('identifier') + ->select(); + + if (empty($customers)) { + return [ + 'code' => 200, + 'msg' => '暂无数据', + 'data' => [] + ]; + } + + // 2. 计算每个客户的R值(最近消费天数) + $customerData = []; + foreach ($customers as $customer) { + $recencyDays = floor(($endTime - $customer['lastOrderTime']) / (24 * 3600)); + $customerData[] = [ + 'identifier' => $customer['identifier'], + 'R' => $recencyDays, + 'F' => (int)$customer['orderCount'], + 'M' => (float)$customer['totalAmount'], + ]; + } + + // 3. 异常值处理 - 剔除大额异常订单 + $mValues = array_column($customerData, 'M'); + if (!empty($mValues)) { + sort($mValues); + $m99Percentile = $this->percentile($mValues, 0.99); + $abnormalThreshold = $m99Percentile * $abnormalMoneyRatio; + + // 标记异常客户(但不删除,仅在计算M维度区间时考虑) + foreach ($customerData as &$customer) { + $customer['isAbnormal'] = $customer['M'] > $abnormalThreshold; + } + } + + // 4. 使用五分位法计算各维度的区间阈值 + $rThresholds = $this->calculatePercentiles(array_column($customerData, 'R'), true); // R是反向的 + $fThresholds = $this->calculatePercentiles(array_column($customerData, 'F'), false); + // M维度排除异常值计算区间 + $mValuesForPercentile = array_filter(array_column($customerData, 'M'), function($m) use ($abnormalThreshold) { + return isset($abnormalThreshold) ? $m <= $abnormalThreshold : true; + }); + $mThresholds = $this->calculatePercentiles(array_values($mValuesForPercentile), false); + + // 5. 计算每个客户的RFM分项得分 + $results = []; + foreach ($customerData as $customer) { + $rScore = $this->scoreByPercentile($customer['R'], $rThresholds, true); // R是反向的 + $fScore = $this->scoreByPercentile($customer['F'], $fThresholds, false); + $mScore = $customer['isAbnormal'] ? 5 : $this->scoreByPercentile($customer['M'], $mThresholds, false); // 异常值给最高分 + + // 计算RFM总分(加权求和) + $rfmScore = $rScore * $weightR + $fScore * $weightF + $mScore * $weightM; + + // 可选:标准化为1-100分 + $standardScore = null; + if ($scoreScale == 100) { + $rfmMin = $weightR * 1 + $weightF * 1 + $weightM * 1; + $rfmMax = $weightR * 5 + $weightF * 5 + $weightM * 5; + $standardScore = (int)round(($rfmScore - $rfmMin) / ($rfmMax - $rfmMin) * 99 + 1); + } + + $results[] = [ + 'identifier' => $customer['identifier'], + 'R_raw' => $customer['R'], + 'R_score' => $rScore, + 'F_raw' => $customer['F'], + 'F_score' => $fScore, + 'M_raw' => round($customer['M'], 2), + 'M_score' => $mScore, + 'RFM_score' => round($rfmScore, 2), + 'RFM_standard_score' => $standardScore, + 'cycle_start' => date('Y-m-d H:i:s', $startTime), + 'cycle_end' => date('Y-m-d H:i:s', $endTime), + 'calculate_time' => date('Y-m-d H:i:s'), + ]; + } + + // 按RFM总分降序排序 + usort($results, function($a, $b) { + return $b['RFM_score'] <=> $a['RFM_score']; + }); + + // 6. 更新 ck_traffic_source 和 s2_wechat_friend 表的RFM值 + $this->updateRfmToTables($results, $ownerWechatId); + + return [ + 'code' => 200, + 'msg' => '计算成功', + 'data' => [ + 'results' => $results, + 'config' => [ + 'cycle_days' => $cycleDays, + 'weight_R' => $weightR, + 'weight_F' => $weightF, + 'weight_M' => $weightM, + 'score_scale' => $scoreScale, + ], + 'statistics' => [ + 'total_customers' => count($results), + 'avg_rfm_score' => round(array_sum(array_column($results, 'RFM_score')) / count($results), 2), + ] + ] + ]; + + } catch (\Exception $e) { + return [ + 'code' => 500, + 'msg' => '计算失败:' . $e->getMessage(), + 'data' => [] + ]; + } + } + + /** + * 计算 RFM 评分(兼容旧方法,使用固定阈值) + * @param int|null $recencyDays 最近购买天数 + * @param int $frequency 购买次数 + * @param float $monetary 购买金额 + * @return array{R:int,F:int,M:int} + */ + public static function calcRfmScores($recencyDays = 30, $frequency, $monetary) + { + $recencyDays = is_numeric($recencyDays) ? (int)$recencyDays : 9999; + $frequency = max(0, (int)$frequency); + $monetary = max(0, (float)$monetary); + return [ + 'R' => self::scoreR_Default($recencyDays), + 'F' => self::scoreF_Default($frequency), + 'M' => self::scoreM_Default($monetary), + ]; + } + + /** + * 使用固定阈值计算R得分(保留兼容性) + */ + protected static function scoreR_Default(int $days): int + { + if ($days <= 30) return 5; + if ($days <= 60) return 4; + if ($days <= 90) return 3; + if ($days <= 120) return 2; + return 1; + } + + /** + * 使用固定阈值计算F得分(保留兼容性) + */ + protected static function scoreF_Default(int $times): int + { + if ($times >= 10) return 5; + if ($times >= 6) return 4; + if ($times >= 3) return 3; + if ($times >= 2) return 2; + if ($times >= 1) return 1; + return 0; + } + + /** + * 使用固定阈值计算M得分(保留兼容性) + */ + protected static function scoreM_Default(float $amount): int + { + if ($amount >= 2000) return 5; + if ($amount >= 1000) return 4; + if ($amount >= 500) return 3; + if ($amount >= 200) return 2; + if ($amount > 0) return 1; + return 0; + } + + /** + * 计算百分位数(五分位法) + * @param array $values 数值数组 + * @param bool $reverse 是否反向(R维度需要反向,值越小得分越高) + * @return array 返回[0.2, 0.4, 0.6, 0.8]分位数的阈值数组 + */ + private function calculatePercentiles($values, $reverse = false) + { + if (empty($values)) { + return [0, 0, 0, 0]; + } + + // 去重并排序 + $uniqueValues = array_unique($values); + sort($uniqueValues); + + // 如果所有值相同,强制均分5个区间 + if (count($uniqueValues) == 1) { + $singleValue = $uniqueValues[0]; + if ($reverse) { + return [$singleValue, $singleValue, $singleValue, $singleValue]; + } else { + return [$singleValue, $singleValue, $singleValue, $singleValue]; + } + } + + $percentiles = [0.2, 0.4, 0.6, 0.8]; + $thresholds = []; + + foreach ($percentiles as $p) { + $thresholds[] = $this->percentile($uniqueValues, $p); + } + + return $thresholds; + } + + /** + * 计算百分位数 + * @param array $sortedArray 已排序的数组 + * @param float $percentile 百分位数(0-1之间) + * @return float + */ + private function percentile($sortedArray, $percentile) + { + if (empty($sortedArray)) { + return 0; + } + + $count = count($sortedArray); + $index = ($count - 1) * $percentile; + $floor = floor($index); + $ceil = ceil($index); + + if ($floor == $ceil) { + return $sortedArray[(int)$index]; + } + + $weight = $index - $floor; + return $sortedArray[(int)$floor] * (1 - $weight) + $sortedArray[(int)$ceil] * $weight; + } + + /** + * 根据五分位法阈值计算得分 + * @param float $value 当前值 + * @param array $thresholds 阈值数组[T1, T2, T3, T4] + * @param bool $reverse 是否反向(R维度反向:值越小得分越高) + * @return int 得分1-5 + */ + private function scoreByPercentile($value, $thresholds, $reverse = false) + { + if (empty($thresholds) || count($thresholds) < 4) { + return 1; + } + + list($t1, $t2, $t3, $t4) = $thresholds; + + if ($reverse) { + // R维度:值越小得分越高 + if ($value <= $t1) return 5; + if ($value <= $t2) return 4; + if ($value <= $t3) return 3; + if ($value <= $t4) return 2; + return 1; + } else { + // F和M维度:值越大得分越高 + if ($value >= $t4) return 5; + if ($value >= $t3) return 4; + if ($value >= $t2) return 3; + if ($value >= $t1) return 2; + return 1; + } + } + + /** + * 更新RFM值到 ck_traffic_source 和 s2_wechat_friend 表 + * + * @param array $results RFM计算结果数组 + * @param string|null $ownerWechatId 微信ID,用于过滤更新范围 + */ + private function updateRfmToTables($results, $ownerWechatId = null) + { + try { + foreach ($results as $result) { + $identifier = $result['identifier']; + $rScore = (string)$result['R_score']; + $fScore = (string)$result['F_score']; + $mScore = (string)$result['M_score']; + + // 更新 ck_traffic_source 表 + // 根据 identifier 更新所有匹配的记录 + $trafficSourceUpdate = [ + 'R' => $rScore, + 'F' => $fScore, + 'M' => $mScore, + 'updateTime' => time() + ]; + TrafficSource::where('identifier', $identifier)->update($trafficSourceUpdate); + + // 更新 s2_wechat_friend 表 + // wechatId 对应 identifier + $wechatFriendUpdate = [ + 'R' => $rScore, + 'F' => $fScore, + 'M' => $mScore, + 'updateTime' => time() + ]; + $wechatFriendWhere = ['wechatId' => $identifier]; + if (!empty($ownerWechatId)) { + $wechatFriendWhere['ownerWechatId'] = $ownerWechatId; + } + WechatFriendModel::where($wechatFriendWhere)->update($wechatFriendUpdate); + } + + } catch (\Exception $e) { + // 记录错误但不影响主流程 + \think\Log::error('更新RFM值失败:' . $e->getMessage()); + } + } +} + + diff --git a/application/cunkebao/controller/StatsController.php b/application/cunkebao/controller/StatsController.php new file mode 100644 index 0000000..45ec6ff --- /dev/null +++ b/application/cunkebao/controller/StatsController.php @@ -0,0 +1,438 @@ + '周日', + 1 => '周一', + 2 => '周二', + 3 => '周三', + 4 => '周四', + 5 => '周五', + 6 => '周六', + ]; + + /** + * 基础信息 + * @return \think\response\Json + */ + public function baseInfoStats() + { + + $where = [ + ['departmentId','=',$this->request->userInfo['companyId']] + ]; + if (empty($this->request->userInfo['isAdmin'])){ + $where[] = ['id','=',$this->request->userInfo['s2_accountId']]; + } + $accounts = Db::table('s2_company_account')->where($where)->column('id'); + + $deviceNum = Db::table('s2_device')->whereIn('currentAccountId',$accounts)->where(['isDeleted' => 0])->count(); + $wechatNum = Db::table('s2_wechat_account')->whereIn('deviceAccountId',$accounts)->count(); + $aliveWechatNum = Db::table('s2_wechat_account')->whereIn('deviceAccountId',$accounts)->where(['wechatAlive' => 1])->count(); + $data = [ + 'deviceNum' => $deviceNum, + 'wechatNum' => $wechatNum, + 'aliveWechatNum' => $aliveWechatNum, + ]; + return successJson($data, '获取成功'); + } + + /** + * 场景获客统计 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function planStats() + { + + $num = $this->request->param('num', 4); + $planScene = Db::name('plan_scene') + ->field('id,name,image') + ->where(['status' => 1]) + ->order('sort DESC') + ->page(1, $num) + ->select(); + + if (empty($planScene)) { + return successJson([], '获取成功'); + } + + $sceneIds = array_column($planScene, 'id'); + $companyId = $this->request->userInfo['companyId']; + + $stats = Db::name('customer_acquisition_task')->alias('ac') + ->join('task_customer tc', 'tc.task_id = ac.id') + ->where([ + ['ac.companyId', '=', $companyId], + ['ac.deleteTime', '=', 0], + ['ac.sceneId', 'in', $sceneIds], + ]) + ->field([ + 'ac.sceneId', + Db::raw('COUNT(1) as allNum'), + Db::raw("SUM(CASE WHEN tc.status IN (1,2,3,4) THEN 1 ELSE 0 END) as addNum"), + Db::raw("SUM(CASE WHEN tc.status = 4 THEN 1 ELSE 0 END) as passNum"), + ]) + ->group('ac.sceneId') + ->select(); + + $statsMap = []; + foreach ($stats as $row) { + $sceneId = is_array($row) ? ($row['sceneId'] ?? 0) : ($row->sceneId ?? 0); + if (!$sceneId) { + continue; + } + $statsMap[$sceneId] = [ + 'allNum' => (int)(is_array($row) ? ($row['allNum'] ?? 0) : ($row->allNum ?? 0)), + 'addNum' => (int)(is_array($row) ? ($row['addNum'] ?? 0) : ($row->addNum ?? 0)), + 'passNum' => (int)(is_array($row) ? ($row['passNum'] ?? 0) : ($row->passNum ?? 0)), + ]; + } + + foreach ($planScene as &$item) { + $sceneStats = $statsMap[$item['id']] ?? ['allNum' => 0, 'addNum' => 0, 'passNum' => 0]; + $item['allNum'] = $sceneStats['allNum']; + $item['addNum'] = $sceneStats['addNum']; + $item['passNum'] = $sceneStats['passNum']; + } + unset($item); + + return successJson($planScene, '获取成功'); + } + + + public function todayStats() + { + $date = date('Y-m-d',time()); + $start = strtotime($date . ' 00:00:00'); + $end = strtotime($date . ' 23:59:59'); + $companyId = $this->request->userInfo['companyId']; + + + $momentsNum = Db::name('workbench')->alias('w') + ->join('workbench_moments_sync_item wi', 'w.id = wi.workbenchId') + ->where(['w.companyId' => $companyId]) + ->where('wi.createTime', 'between', [$start, $end]) + ->count(); + + $groupPushNum = Db::name('workbench')->alias('w') + ->join('workbench_group_push_item wi', 'w.id = wi.workbenchId') + ->where(['w.companyId' => $companyId]) + ->where('wi.createTime', 'between', [$start, $end]) + ->count(); + + + $addNum = Db::name('customer_acquisition_task')->alias('ac') + ->join('task_customer tc', 'tc.task_id = ac.id') + ->where(['ac.companyId' => $companyId, 'ac.deleteTime' => 0]) + ->where('tc.updateTime', 'between', [$start, $end]) + ->whereIn('tc.status', [1, 2, 3, 4]) + ->count(); + + // 通过量 + $passNum = Db::name('customer_acquisition_task')->alias('ac') + ->join('task_customer tc', 'tc.task_id = ac.id') + ->where(['ac.companyId' => $companyId, 'ac.deleteTime' => 0]) + ->where('tc.updateTime', 'between', [$start, $end]) + ->whereIn('tc.status', [4]) + ->count(); + + if (!empty($passNum)){ + $passRate = number_format(($addNum / $passNum) * 100,2) ; + }else{ + $passRate = '0%'; + } + + $sysActive = '90%'; + $data = [ + 'momentsNum' => $momentsNum, + 'groupPushNum' => $groupPushNum, + 'addNum' => $addNum, + 'passNum' => $passNum, + 'passRate' => $passRate, + 'sysActive' => $sysActive, + ]; + return successJson($data, '获取成功'); + } + + + + /** + * 近7天获客统计 + * @return \think\response\Json + */ + public function customerAcquisitionStats7Days() + { + $companyId = $this->request->userInfo['companyId']; + $days = 7; + + $endTime = strtotime(date('Y-m-d 23:59:59')); + $startTime = strtotime(date('Y-m-d 00:00:00', strtotime('-' . ($days - 1) . ' day'))); + + $dateMap = []; + $dateLabels = []; + for ($i = 0; $i < $days; $i++) { + $currentDate = date('Y-m-d', strtotime("-" . ($days - 1 - $i) . " day")); + $weekIndex = date("w", strtotime($currentDate)); + $dateMap[$currentDate] = self::WEEK[$weekIndex]; + $dateLabels[] = self::WEEK[$weekIndex]; + } + + $baseWhere = [ + ['ac.companyId', '=', $companyId], + ['ac.deleteTime', '=', 0], + ]; + + $fetchCounts = function (string $timeField, array $status = []) use ($baseWhere, $startTime, $endTime) { + $query = Db::name('customer_acquisition_task')->alias('ac') + ->join('task_customer tc', 'tc.task_id = ac.id') + ->where($baseWhere) + ->whereBetween('tc.' . $timeField, [$startTime, $endTime]); + if (!empty($status)) { + $query->whereIn('tc.status', $status); + } + $rows = $query->field([ + "FROM_UNIXTIME(tc.{$timeField}, '%Y-%m-%d')" => 'day', + 'COUNT(1)' => 'total' + ])->group('day')->select(); + + $result = []; + foreach ($rows as $row) { + $day = is_array($row) ? ($row['day'] ?? '') : ($row->day ?? ''); + $total = (int)(is_array($row) ? ($row['total'] ?? 0) : ($row->total ?? 0)); + if ($day) { + $result[$day] = $total; + } + } + return $result; + }; + + $allNumDict = $fetchCounts('createTime'); + $addNumDict = $fetchCounts('updateTime', [1, 2, 3, 4]); + $passNumDict = $fetchCounts('updateTime', [4]); + + $allNum = []; + $addNum = []; + $passNum = []; + foreach (array_keys($dateMap) as $dateKey) { + $allNum[] = $allNumDict[$dateKey] ?? 0; + $addNum[] = $addNumDict[$dateKey] ?? 0; + $passNum[] = $passNumDict[$dateKey] ?? 0; + } + + $data = [ + 'date' => $dateLabels, + 'allNum' => $allNum, + 'addNum' => $addNum, + 'passNum' => $passNum, + ]; + + return successJson($data, '获取成功'); + } + + + /** + * 场景获客数据统计 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function getFriendRequestTaskStats() + { + $companyId = $this->request->userInfo['companyId']; + $taskId = $this->request->param('taskId', ''); + if(empty($taskId)){ + return errorJson('任务id不能为空'); + } + + $task = Db::name('customer_acquisition_task')->where(['id' => $taskId, 'companyId' => $companyId,'deleteTime' => 0])->find(); + if(empty($task)){ + return errorJson('任务不存在或已删除'); + } + + + // 1. 获取startTime和endTime,格式是日期 + $startTime = $this->request->param('startTime', ''); + $endTime = $this->request->param('endTime', ''); + + // 如果获取不到则默认为7天的跨度 + if (empty($startTime)) { + $startTime = date('Y-m-d', time() - 86400 * 6); + } + if (empty($endTime)) { + $endTime = date('Y-m-d', time()); + } + + // 转换成时间戳格式 + $startTimestamp = strtotime($startTime . ' 00:00:00'); + $endTimestamp = strtotime($endTime . ' 23:59:59'); + + // 同时生成日期数组和时间戳二维数组 + $dateArray = []; + $timestampArray = []; + $currentTimestamp = $startTimestamp; + + while ($currentTimestamp <= $endTimestamp) { + // 生成日期格式数组 + $dateArray[] = date('m-d', $currentTimestamp); + + // 生成时间戳二维数组 + $dayStart = $currentTimestamp; + $dayEnd = strtotime('+1 day', $currentTimestamp) - 1; // 23:59:59 + $timestampArray[] = [$dayStart, $dayEnd]; + + $currentTimestamp = strtotime('+1 day', $currentTimestamp); + } + + + // 使用分组聚合统计,减少 SQL 次数 + $allRows = Db::name('task_customer') + ->field("FROM_UNIXTIME(createTime, '%m-%d') AS d, COUNT(*) AS c") + ->where(['task_id' => $taskId]) + ->where('createTime', 'between', [$startTimestamp, $endTimestamp]) + ->group('d') + ->select(); + + $successRows = Db::name('task_customer') + ->field("FROM_UNIXTIME(addTime, '%m-%d') AS d, COUNT(*) AS c") + ->where(['task_id' => $taskId]) + ->where('addTime', 'between', [$startTimestamp, $endTimestamp]) + ->whereIn('status', [1, 2, 4, 5]) + ->group('d') + ->select(); + + $passRows = Db::name('task_customer') + ->field("FROM_UNIXTIME(passTime, '%m-%d') AS d, COUNT(*) AS c") + ->where(['task_id' => $taskId]) + ->where('passTime', 'between', [$startTimestamp, $endTimestamp]) + ->group('d') + ->select(); + + $errorRows = Db::name('task_customer') + ->field("FROM_UNIXTIME(updateTime, '%m-%d') AS d, COUNT(*) AS c") + ->where(['task_id' => $taskId, 'status' => 3]) + ->where('updateTime', 'between', [$startTimestamp, $endTimestamp]) + ->group('d') + ->select(); + + // 将分组结果映射到连续日期数组 + $mapToSeries = function(array $rows) use ($dateArray) { + $dict = []; + foreach ($rows as $row) { + // 兼容对象/数组两种返回 + $d = is_array($row) ? ($row['d'] ?? '') : ($row->d ?? ''); + $c = (int)(is_array($row) ? ($row['c'] ?? 0) : ($row->c ?? 0)); + if ($d !== '') { + $dict[$d] = $c; + } + } + $series = []; + foreach ($dateArray as $d) { + $series[] = $dict[$d] ?? 0; + } + return $series; + }; + + $allNumArray = $mapToSeries($allRows); + $successNumArray = $mapToSeries($successRows); + $passNumArray = $mapToSeries($passRows); + $errorNumArray = $mapToSeries($errorRows); + + // 计算通过率和成功率 + $passRateArray = []; + $successRateArray = []; + + for ($i = 0; $i < count($dateArray); $i++) { + // 通过率 = 通过数 / 总数 + $passRate = ($allNumArray[$i] > 0) ? round(($passNumArray[$i] / $allNumArray[$i]) * 100, 2) : 0; + $passRateArray[] = $passRate; + + // 成功率 = 成功数 / 总数 + $successRate = ($allNumArray[$i] > 0) ? round(($successNumArray[$i] / $allNumArray[$i]) * 100, 2) : 0; + $successRateArray[] = $successRate; + } + + // 计算总体统计 + $totalAll = array_sum($allNumArray); + $totalSuccess = array_sum($successNumArray); + $totalPass = array_sum($passNumArray); + $totalError = array_sum($errorNumArray); + + $totalPassRate = ($totalAll > 0) ? round(($totalPass / $totalAll) * 100, 2) : 0; + $totalSuccessRate = ($totalAll > 0) ? round(($totalSuccess / $totalAll) * 100, 2) : 0; + + // 返回结果 + $result = [ + 'startTime' => $startTime, + 'endTime' => $endTime, + 'dateArray' => $dateArray, + 'allNumArray' => $allNumArray, + 'successNumArray' => $successNumArray, + 'passNumArray' => $passNumArray, + 'errorNumArray' => $errorNumArray, + 'passRateArray' => $passRateArray, + 'successRateArray' => $successRateArray, + 'totalStats' => [ + 'totalAll' => $totalAll, + 'totalSuccess' => $totalSuccess, + 'totalPass' => $totalPass, + 'totalError' => $totalError, + 'totalPassRate' => $totalPassRate, + 'totalSuccessRate' => $totalSuccessRate + ] + ]; + + return successJson($result, '获取成功'); + } + + + public function userInfoStats() + { + $companyId = $this->request->userInfo['companyId']; + $userId = $this->request->userInfo['id']; + $isAdmin = $this->request->userInfo['isAdmin']; + + + $where = [ + ['departmentId','=',$companyId] + ]; + if (empty($this->request->userInfo['isAdmin'])){ + $where[] = ['id','=',$this->request->userInfo['s2_accountId']]; + } + $accounts = Db::table('s2_company_account')->where($where)->column('id'); + + + $userNum = Db::table('s2_wechat_friend')->whereIn('accountId',$accounts)->where(['isDeleted' => 0])->count(); + $deviceNum = Db::table('s2_device')->whereIn('currentAccountId',$accounts)->where(['isDeleted' => 0])->count(); + $wechatNum = Db::table('s2_wechat_account')->whereIn('deviceAccountId',$accounts)->count(); + + + $contentLibrary = Db::name('content_library')->where(['companyId' => $companyId,'isDel' => 0]); + if(empty($isAdmin)){ + $contentLibrary = $contentLibrary->where(['userId' => $userId]); + } + $contentLibraryNum = $contentLibrary->count(); + + + $data = [ + 'deviceNum' => $deviceNum, + 'wechatNum' => $wechatNum, + 'contentLibraryNum' => $contentLibraryNum, + 'userNum' => $userNum, + ]; + return successJson($data, '获取成功'); + } + + +} \ No newline at end of file diff --git a/application/cunkebao/controller/StoreAccountController.php b/application/cunkebao/controller/StoreAccountController.php new file mode 100644 index 0000000..f0b4464 --- /dev/null +++ b/application/cunkebao/controller/StoreAccountController.php @@ -0,0 +1,404 @@ +request->param('account', ''); + $username = $this->request->param('username', ''); + $phone = $this->request->param('phone', ''); + $password = $this->request->param('password', ''); + $deviceId = $this->request->param('deviceId', 0); + + $companyId = $this->getUserInfo('companyId'); + + // 参数验证 + if (empty($account)) { + return ResponseHelper::error('账号不能为空'); + } + if (empty($username)) { + return ResponseHelper::error('昵称不能为空'); + } + if (empty($phone)) { + return ResponseHelper::error('手机号不能为空'); + } + if (!preg_match('/^1[3-9]\d{9}$/', $phone)) { + return ResponseHelper::error('手机号格式不正确'); + } + if (empty($password)) { + return ResponseHelper::error('密码不能为空'); + } + if (strlen($password) < 6 || strlen($password) > 20) { + return ResponseHelper::error('密码长度必须在6-20个字符之间'); + } + if (empty($deviceId)) { + return ResponseHelper::error('请选择设备'); + } + + // 检查账号是否已存在(同一 typeId 和 companyId 下不能重复) + $existUser = Db::name('users')->where(['account' => $account, 'companyId' => $companyId, 'typeId' => 2, 'deleteTime' => 0]) + ->find(); + if ($existUser) { + return ResponseHelper::error('账号已存在'); + } + + // 检查手机号是否已存在(同一 typeId 和 companyId 下不能重复) + $existPhone = Db::name('users')->where(['phone' => $phone, 'companyId' => $companyId, 'typeId' => 2, 'deleteTime' => 0]) + ->find(); + if ($existPhone) { + return ResponseHelper::error('手机号已被使用'); + } + + // 检查设备是否存在且属于当前公司 + $device = Device::where('id', $deviceId) + ->where('companyId', $companyId) + ->find(); + if (!$device) { + return ResponseHelper::error('设备不存在或没有权限'); + } + + // 开始事务 + Db::startTrans(); + try { + // 创建用户 + $userData = [ + 'account' => $account, + 'username' => $username, + 'phone' => $phone, + 'passwordMd5' => md5($password), + 'passwordLocal' => localEncrypt($password), + 'avatar' => '', + 'isAdmin' => 0, + 'companyId' => $companyId, + 'typeId' => 2, // 门店端固定为2 + 'status' => 1, // 默认可用 + 'balance' => 0, + 'tokens' => 0, + 'createTime' => time(), + ]; + + $userId = Db::name('users')->insertGetId($userData); + + // 绑定设备 + Db::name('device_user')->insert([ + 'companyId' => $companyId, + 'userId' => $userId, + 'deviceId' => $deviceId, + 'deleteTime' => 0, + ]); + + // 提交事务 + Db::commit(); + + return ResponseHelper::success('创建账号成功'); + } catch (\Exception $e) { + Db::rollback(); + throw $e; + } + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500); + } + } + + /** + * 编辑账号 + * @return \think\response\Json + */ + public function update() + { + try { + $userId = $this->request->param('userId', 0); + $account = $this->request->param('account', ''); + $username = $this->request->param('username', ''); + $phone = $this->request->param('phone', ''); + $password = $this->request->param('password', ''); + $deviceId = $this->request->param('deviceId', 0); + + $companyId = $this->getUserInfo('companyId'); + + // 参数验证 + if (empty($userId)) { + return ResponseHelper::error('用户ID不能为空'); + } + + // 检查用户是否存在且属于当前公司 + $user = Db::name('users')->where(['id' => $userId, 'companyId' => $companyId, 'typeId' => 2])->find(); + if (!$user) { + return ResponseHelper::error('用户不存在或没有权限'); + } + + $updateData = []; + + // 更新账号 + if (!empty($account)) { + // 检查账号是否已被其他用户使用(同一 typeId 下) + $existUser = Db::name('users')->where(['account' => $account, 'companyId' => $companyId, 'typeId' => 2, 'deleteTime' => 0]) + ->where('id', '<>', $userId) + ->find(); + if ($existUser) { + return ResponseHelper::error('账号已被使用'); + } + $updateData['account'] = $account; + } + + // 更新昵称 + if (!empty($username)) { + $updateData['username'] = $username; + } + + // 更新手机号 + if (!empty($phone)) { + if (!preg_match('/^1[3-9]\d{9}$/', $phone)) { + return ResponseHelper::error('手机号格式不正确'); + } + // 检查手机号是否已被其他用户使用(同一 typeId 下) + $existPhone = Db::name('users')->where(['phone' => $phone, 'companyId' => $companyId, 'typeId' => 2, 'deleteTime' => 0]) + ->where('id', '<>', $userId) + ->find(); + if ($existPhone) { + return ResponseHelper::error('手机号已被使用'); + } + $updateData['phone'] = $phone; + } + + // 更新密码 + if (!empty($password)) { + if (strlen($password) < 6 || strlen($password) > 20) { + return ResponseHelper::error('密码长度必须在6-20个字符之间'); + } + $updateData['passwordMd5'] = md5($password); + $updateData['passwordLocal'] = localEncrypt($password); + } + + // 更新设备绑定 + if (!empty($deviceId)) { + // 检查设备是否存在且属于当前公司 + $device = Device::where('id', $deviceId) + ->where('companyId', $companyId) + ->find(); + if (!$device) { + return ResponseHelper::error('设备不存在或没有权限'); + } + } + + // 开始事务 + Db::startTrans(); + try { + // 更新用户信息 + if (!empty($updateData)) { + $updateData['updateTime'] = time(); + Db::name('users')->where(['id' => $userId])->update($updateData); + } + + // 更新设备绑定 + if (!empty($deviceId)) { + // 删除旧的设备绑定 + Db::name('device_user')->where(['userId' => $userId, 'companyId' => $companyId])->delete(); + + // 添加新的设备绑定 + Db::name('device_user')->insert([ + 'companyId' => $companyId, + 'userId' => $userId, + 'deviceId' => $deviceId, + 'deleteTime' => 0, + ]); + } + + // 提交事务 + Db::commit(); + + return ResponseHelper::success('更新账号成功'); + } catch (\Exception $e) { + Db::rollback(); + throw $e; + } + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500); + } + } + + /** + * 删除账号 + * @return \think\response\Json + */ + public function delete() + { + try { + $userId = $this->request->param('userId', 0); + $companyId = $this->getUserInfo('companyId'); + + if (empty($userId)) { + return ResponseHelper::error('用户ID不能为空'); + } + + // 检查用户是否存在且属于当前公司 + $user = Db::name('users')->where(['id' => $userId, 'companyId' => $companyId, 'typeId' => 2])->find(); + if (!$user) { + return ResponseHelper::error('用户不存在或没有权限'); + } + + // 检查是否是管理账号 + if ($user['isAdmin'] == 1) { + return ResponseHelper::error('管理账号无法删除'); + } + + // 软删除用户 + Db::name('users')->where(['id' => $userId])->update([ + 'deleteTime' => time(), + 'updateTime' => time() + ]); + + // 软删除设备绑定关系 + Db::name('device_user')->where(['userId' => $userId, 'companyId' => $companyId])->update([ + 'deleteTime' => time() + ]); + + return ResponseHelper::success('删除账号成功'); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500); + } + } + + /** + * 禁用/启用账号 + * @return \think\response\Json + */ + public function disable() + { + try { + $userId = $this->request->param('userId', 0); + $status = $this->request->param('status', -1); // 0-禁用 1-启用 + $companyId = $this->getUserInfo('companyId'); + + if (empty($userId)) { + return ResponseHelper::error('用户ID不能为空'); + } + + if ($status != 0 && $status != 1) { + return ResponseHelper::error('状态参数错误'); + } + + // 检查用户是否存在且属于当前公司 + $user = Db::name('users')->where(['id' => $userId, 'companyId' => $companyId, 'typeId' => 2])->find(); + if (!$user) { + return ResponseHelper::error('用户不存在或没有权限'); + } + + // 检查是否是管理账号 + if ($user['isAdmin'] == 1 && $status == 0) { + return ResponseHelper::error('管理账号无法禁用'); + } + + // 更新状态 + Db::name('users')->where(['id' => $userId])->update([ + 'status' => $status, + 'updateTime' => time() + ]); + + $message = $status == 0 ? '禁用账号成功' : '启用账号成功'; + return ResponseHelper::success($message); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500); + } + } + + /** + * 获取账号列表 + * @return \think\response\Json + */ + public function index() + { + try { + $keyword = $this->request->param('keyword', ''); + $status = $this->request->param('status', ''); + $page = $this->request->param('page/d', 1); + $limit = $this->request->param('limit/d', 10); + + $companyId = $this->getUserInfo('companyId'); + + // 构建查询条件 + $where = [ + ['companyId', '=', $companyId], + ['typeId', '=', 2], // 只查询门店端账号 + ['deleteTime', '=', 0] + ]; + + // 关键词搜索(账号、昵称、手机号) + if (!empty($keyword)) { + $where[] = ['account|username|phone', "LIKE", '%'.$keyword.'%']; + } + + // 状态筛选 + if ($status !== '') { + $where[] = ['status', '=', $status]; + } + + // 分页查询 + $query = Db::name('users')->where($where); + $total = $query->count(); + + $list = $query->field('id,account,username,phone,avatar,isAdmin,status,balance,tokens,createTime') + ->order('id desc') + ->page($page, $limit) + ->select(); + + + // 获取每个账号绑定的设备(单个设备) + if (!empty($list)) { + $userIds = array_column($list, 'id'); + $deviceBindings = Db::name('device_user') + ->alias('du') + ->join('device d', 'd.id = du.deviceId', 'left') + ->where([ + ['du.userId', 'in', $userIds], + ['du.companyId', '=', $companyId], + ['du.deleteTime', '=', 0] + ]) + ->field('du.userId,du.deviceId,d.imei,d.memo') + ->order('du.id desc') + ->select(); + + // 组织设备数据(单个设备对象) + $deviceMap = []; + foreach ($deviceBindings as $binding) { + $deviceMap[$binding['userId']] = [ + 'deviceId' => $binding['deviceId'], + 'imei' => $binding['imei'], + 'memo' => $binding['memo'] + ]; + } + + // 将设备信息添加到用户数据中 + foreach ($list as &$item) { + $item['device'] = $deviceMap[$item['id']] ?? null; + } + } + + return ResponseHelper::success([ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ]); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500); + } + } +} diff --git a/application/cunkebao/controller/Task.php b/application/cunkebao/controller/Task.php new file mode 100644 index 0000000..6ffcffb --- /dev/null +++ b/application/cunkebao/controller/Task.php @@ -0,0 +1,369 @@ +scene ? $task->scene->toArray() : null; + $task['device'] = $task->device ? $task->device->toArray() : null; + } + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => $result + ]); + } + + /** + * 获取任务详情 + * + * @param int $id + * @return \think\response\Json + */ + public function read($id) + { + $task = PlanTask::get($id, ['scene', 'device']); + if (!$task) { + return json([ + 'code' => 404, + 'msg' => '任务不存在' + ]); + } + + // 获取执行记录 + $executions = PlanExecution::where('plan_id', $id) + ->order('createTime DESC') + ->select(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'task' => $task, + 'executions' => $executions + ] + ]); + } + + /** + * 创建任务 + * + * @return \think\response\Json + */ + public function save() + { + $data = Request::post(); + + // 数据验证 + $validate = validate('app\cunkebao\validate\Task'); + if (!$validate->check($data)) { + return json([ + 'code' => 400, + 'msg' => $validate->getError() + ]); + } + + // 添加任务 + $task = new PlanTask; + $task->save([ + 'name' => $data['name'], + 'device_id' => $data['device_id'] ?? null, + 'scene_id' => $data['scene_id'] ?? null, + 'scene_config' => $data['scene_config'] ?? [], + 'status' => $data['status'] ?? 0, + 'current_step' => 0, + 'priority' => $data['priority'] ?? 5, + 'created_by' => $data['created_by'] ?? 0 + ]); + + return json([ + 'code' => 200, + 'msg' => '创建成功', + 'data' => $task->id + ]); + } + + /** + * 更新任务 + * + * @param int $id + * @return \think\response\Json + */ + public function update($id) + { + $data = Request::put(); + + // 检查任务是否存在 + $task = PlanTask::get($id); + if (!$task) { + return json([ + 'code' => 404, + 'msg' => '任务不存在' + ]); + } + + // 准备更新数据 + $updateData = []; + + // 只允许更新特定字段 + $allowedFields = ['name', 'device_id', 'scene_id', 'scene_config', 'status', 'priority']; + foreach ($allowedFields as $field) { + if (isset($data[$field])) { + $updateData[$field] = $data[$field]; + } + } + + // 更新任务 + $task->save($updateData); + + return json([ + 'code' => 200, + 'msg' => '更新成功' + ]); + } + + /** + * 删除任务 + * + * @param int $id + * @return \think\response\Json + */ + public function delete($id) + { + // 检查任务是否存在 + $task = PlanTask::get($id); + if (!$task) { + return json([ + 'code' => 404, + 'msg' => '任务不存在' + ]); + } + + // 软删除任务 + $task->delete(); + + return json([ + 'code' => 200, + 'msg' => '删除成功' + ]); + } + + /** + * 启动任务 + * + * @param int $id + * @return \think\response\Json + */ + public function start($id) + { + // 检查任务是否存在 + $task = PlanTask::get($id); + if (!$task) { + return json([ + 'code' => 404, + 'msg' => '任务不存在' + ]); + } + + // 更新状态为启用 + $task->save([ + 'status' => 1, + 'current_step' => 0 + ]); + + return json([ + 'code' => 200, + 'msg' => '任务已启动' + ]); + } + + /** + * 停止任务 + * + * @param int $id + * @return \think\response\Json + */ + public function stop($id) + { + // 检查任务是否存在 + $task = PlanTask::get($id); + if (!$task) { + return json([ + 'code' => 404, + 'msg' => '任务不存在' + ]); + } + + // 更新状态为停用 + $task->save([ + 'status' => 0 + ]); + + return json([ + 'code' => 200, + 'msg' => '任务已停止' + ]); + } + + /** + * 执行定时任务(供外部调用) + * + * @return \think\response\Json + */ + public function cron() + { + // 获取密钥 + $key = Request::param('key', ''); + + // 验证密钥(实际生产环境应当使用更安全的验证方式) + if ($key !== config('task.cron_key')) { + return json([ + 'code' => 403, + 'msg' => '访问密钥无效' + ]); + } + + try { + // 获取待执行的任务 + $tasks = PlanTask::getPendingTasks(5); + if ($tasks->isEmpty()) { + return json([ + 'code' => 200, + 'msg' => '没有需要执行的任务', + 'data' => [] + ]); + } + + $results = []; + + // 逐一执行任务 + foreach ($tasks as $task) { + $runner = new TaskRunner($task); + $result = $runner->run(); + + $results[] = [ + 'task_id' => $task->id, + 'name' => $task->name, + 'result' => $result + ]; + + // 记录执行信息 + Log::info('任务执行', [ + 'task_id' => $task->id, + 'name' => $task->name, + 'result' => $result + ]); + } + + return json([ + 'code' => 200, + 'msg' => '任务执行完成', + 'data' => $results + ]); + + } catch (\Exception $e) { + Log::error('任务执行异常', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return json([ + 'code' => 500, + 'msg' => '任务执行异常:' . $e->getMessage() + ]); + } + } + + /** + * 手动执行任务 + * + * @param int $id + * @return \think\response\Json + */ + public function execute($id) + { + // 检查任务是否存在 + $task = PlanTask::get($id); + if (!$task) { + return json([ + 'code' => 404, + 'msg' => '任务不存在' + ]); + } + + try { + // 执行任务 + $runner = new TaskRunner($task); + $result = $runner->run(); + + // 记录执行信息 + Log::info('手动执行任务', [ + 'task_id' => $task->id, + 'name' => $task->name, + 'result' => $result + ]); + + return json([ + 'code' => 200, + 'msg' => '任务执行完成', + 'data' => $result + ]); + + } catch (\Exception $e) { + Log::error('手动执行任务异常', [ + 'task_id' => $task->id, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return json([ + 'code' => 500, + 'msg' => '任务执行异常:' . $e->getMessage() + ]); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/TokensController.php b/application/cunkebao/controller/TokensController.php new file mode 100644 index 0000000..8934c3c --- /dev/null +++ b/application/cunkebao/controller/TokensController.php @@ -0,0 +1,535 @@ +request->param('page', 1); + $limit = $this->request->param('limit', 10); + $where = [ + ['isDel', '=', 0], + ['status', '=', 1], + ]; + $query = TokensPackage::where($where); + $total = $query->count(); + $list = $query->where($where)->page($page, $limit)->order('sort ASC,id desc')->select(); + foreach ($list as &$item) { + $item['description'] = json_decode($item['description'], true); + $item['discount'] = round(((($item['originalPrice'] - $item['price']) / $item['originalPrice']) * 100), 2); + $item['price'] = round($item['price'], 2); + $item['unitPrice'] = round($item['price'] / $item['tokens'], 6); + $item['originalPrice'] = round($item['originalPrice'] / 100, 2); + $item['tokens'] = number_format($item['tokens']); + } + unset($item); + return ResponseHelper::success(['list' => $list, 'total' => $total]); + } + + + public function pay() + { + $id = $this->request->param('id', ''); + $price = $this->request->param('price', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + $payType = $this->request->param('payType', 'qrCode'); + + if (!in_array($payType, ['wechat', 'alipay', 'qrCode'])) { + return ResponseHelper::error('付款类型不正确'); + } + + + if (empty($id) && empty($price)) { + return ResponseHelper::error('套餐和自定义购买金额必须选一个'); + } + + if (!empty($id)) { + $package = TokensPackage::where(['id' => $id, 'status' => 1, 'isDel' => 0])->find(); + if (empty($package)) { + return ResponseHelper::error('套餐不存在或者已禁用'); + } + + if ($package['price'] <= 0) { + return ResponseHelper::error('套餐金额异常'); + } + + $specs = [ + 'id' => $package['id'], + 'name' => $package['name'], + 'price' => $package['price'], + 'tokens' => $package['tokens'], + ]; + + } else { + //获取配置的tokens比例 + $tokens_multiple = Env::get('payment.tokens_multiple', 20); + $specs = [ + 'id' => 0, + 'name' => '自定义购买算力', + 'price' => intval($price * 100), + 'tokens' => intval($price * $tokens_multiple), + ]; + } + + + $orderNo = date('YmdHis') . rand(100000, 999999); + $order = [ + 'companyId' => $companyId, + 'userId' => $userId, + 'orderNo' => $orderNo, + 'goodsId' => $specs['id'], + 'goodsName' => $specs['name'], + 'goodsSpecs' => $specs, + 'orderType' => 1, + 'money' => $specs['price'], + 'service' => $payType + ]; + $paymentService = new PaymentService(); + $res = $paymentService->createOrder($order); + $res = json_decode($res, true); + if ($res['code'] == 200) { + return ResponseHelper::success(['orderNo' => $orderNo, 'code_url' => $res['data']], '订单创建成功'); + } else { + return ResponseHelper::error($res['msg']); + } + } + + public function queryOrder() + { + $orderNo = $this->request->param('orderNo', ''); + $order = Order::where('orderNo', $orderNo)->find(); + if (!$order) { + return ResponseHelper::error('该订单不存在'); + } + if ($order->status != 1) { + $paymentService = new PaymentService(); + $res = $paymentService->queryOrder($orderNo); + $res = json_decode($res, true); + if ($res['code'] == 200) { + return ResponseHelper::success($order, '订单已支付'); + } else { + $errorMsg = !empty($order['payInfo']) ? $order['payInfo'] : '订单未支付'; + return ResponseHelper::success($order,$errorMsg); + } + } else { + return ResponseHelper::success($order, '订单已支付'); + } + } + + + + /** + * 获取订单列表 + * @return \think\response\Json + */ + public function getOrderList() + { + try { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $status = $this->request->param('status', ''); // 订单状态筛选 + $keyword = $this->request->param('keyword', ''); // 关键词搜索(订单号) + $orderType = $this->request->param('orderType', ''); // 订单类型筛选 + $payType = $this->request->param('payType', ''); // 支付类型筛选 + $startTime = $this->request->param('startTime', ''); // 开始时间 + $endTime = $this->request->param('endTime', ''); // 结束时间 + + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + // 构建查询条件 + $where = [ + ['userId', '=', $userId], + ['companyId', '=', $companyId] + ]; + + // 关键词搜索(订单号、商品名称) + if (!empty($keyword)) { + $where[] = ['orderNo|goodsName', 'like', '%' . $keyword . '%']; + } + + // 状态筛选 (0-待支付 1-已付款 2-已退款 3-付款失败) + if ($status !== '') { + $where[] = ['status', '=', $status]; + } + + // 订单类型筛选 + if ($orderType !== '') { + $where[] = ['orderType', '=', $orderType]; + } + + // 支付类型筛选 + if($payType !== '') { + $where[] = ['payType', '=', $payType]; + } + + // 时间范围筛选 + if (!empty($startTime)) { + $where[] = ['createTime', '>=', strtotime($startTime)]; + } + if (!empty($endTime)) { + $where[] = ['createTime', '<=', strtotime($endTime . ' 23:59:59')]; + } + + // 分页查询 + $query = Order::where($where) + ->where(function ($query) { + $query->whereNull('deleteTime')->whereOr('deleteTime', 0); + }); + $total = $query->count(); + + $list = $query->field('id,orderNo,goodsId,goodsName,goodsSpecs,orderType,money,status,payType,payTime,createTime') + ->order('id desc') + ->page($page, $limit) + ->select(); + + // 格式化数据 + foreach ($list as &$item) { + // 金额转换(分转元) + $item['money'] = round($item['money'] / 100, 2); + + // 解析商品规格 + if (!empty($item['goodsSpecs'])) { + $specs = is_string($item['goodsSpecs']) ? json_decode($item['goodsSpecs'], true) : $item['goodsSpecs']; + $item['goodsSpecs'] = $specs; + + // 添加算力数量 + if (isset($specs['tokens'])) { + $item['tokens'] = number_format($specs['tokens']); + } + } + + // 状态文本 + $statusText = [ + 0 => '待支付', + 1 => '已付款', + 2 => '已退款', + 3 => '付款失败' + ]; + $item['statusText'] = $statusText[$item['status']] ?? '未知'; + + // 订单类型文本 + $orderTypeText = [ + 1 => '购买算力' + ]; + $item['orderTypeText'] = $orderTypeText[$item['orderType']] ?? '其他'; + + // 支付类型文本 + $payTypeText = [ + 1 => '微信支付', + 2 => '支付宝' + ]; + $item['payTypeText'] = !empty($item['payType']) ? ($payTypeText[$item['payType']] ?? '未知') : ''; + + // 格式化时间 + $item['createTime'] = $item['createTime'] ? date('Y-m-d H:i:s', $item['createTime']) : ''; + $item['payTime'] = $item['payTime'] ? date('Y-m-d H:i:s', $item['payTime']) : ''; + } + unset($item); + + return ResponseHelper::success([ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ]); + + } catch (\Exception $e) { + return ResponseHelper::error('获取订单列表失败:' . $e->getMessage()); + } + } + + /** + * 获取公司算力统计信息 + * 包括:总算力、今日使用、本月使用、剩余算力 + * + * @return \think\response\Json + */ + public function getTokensStatistics() + { + try { + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + if (empty($companyId)) { + return ResponseHelper::error('公司信息获取失败'); + } + + // 获取公司算力余额 + $tokensCompany = TokensCompany::where(['companyId' => $companyId,'userId' => $userId])->find(); + $remainingTokens = $tokensCompany ? intval($tokensCompany->tokens) : 0; + + // 获取今日开始和结束时间戳 + $todayStart = strtotime(date('Y-m-d 00:00:00')); + $todayEnd = strtotime(date('Y-m-d 23:59:59')); + + // 获取本月开始和结束时间戳 + $monthStart = strtotime(date('Y-m-01 00:00:00')); + $monthEnd = strtotime(date('Y-m-t 23:59:59')); + + // 统计今日消费(type=0表示消费) + $todayUsed = TokensRecord::where([ + ['userId', '=', $userId], + ['companyId', '=', $companyId], + ['type', '=', 0], // 0为减少(消费) + ['createTime', '>=', $todayStart], + ['createTime', '<=', $todayEnd] + ])->sum('tokens'); + $todayUsed = intval($todayUsed); + + // 统计本月消费 + $monthUsed = TokensRecord::where([ + ['userId', '=', $userId], + ['companyId', '=', $companyId], + ['type', '=', 0], // 0为减少(消费) + ['createTime', '>=', $monthStart], + ['createTime', '<=', $monthEnd] + ])->sum('tokens'); + $monthUsed = intval($monthUsed); + + // 计算总算力(当前剩余 + 历史总消费) + $totalConsumed = TokensRecord::where([ + ['userId', '=', $userId], + ['companyId', '=', $companyId], + ['type', '=', 0] + ])->sum('tokens'); + $totalConsumed = intval($totalConsumed); + + // 总充值算力 + $totalRecharged = TokensRecord::where([ + ['userId', '=', $userId], + ['companyId', '=', $companyId], + ['type', '=', 1] // 1为增加(充值) + ])->sum('tokens'); + $totalRecharged = intval($totalRecharged); + + // 计算预计可用天数(基于过去一个月的平均消耗) + $estimatedDays = $this->calculateEstimatedDays($userId,$companyId, $remainingTokens); + + return ResponseHelper::success([ + 'totalTokens' => $totalRecharged, // 总算力(累计充值) + 'todayUsed' => $todayUsed, // 今日使用 + 'monthUsed' => $monthUsed, // 本月使用 + 'remainingTokens' => $remainingTokens, // 剩余算力 + 'totalConsumed' => $totalConsumed, // 累计消费 + 'estimatedDays' => $estimatedDays, // 预计可用天数 + ], '获取成功'); + + } catch (\Exception $e) { + return ResponseHelper::error('获取算力统计失败:' . $e->getMessage()); + } + } + + /** + * 计算预计可用天数(基于过去一个月的平均消耗) + * @param int $userId 用户ID + * @param int $companyId 公司ID + * @param int $remainingTokens 当前剩余算力 + * @return int 预计可用天数,-1表示无法计算(无消耗记录或余额为0) + */ + private function calculateEstimatedDays($userId,$companyId, $remainingTokens) + { + // 如果余额为0或负数,无法计算 + if ($remainingTokens <= 0) { + return -1; + } + + // 计算过去30天的消耗总量(只统计减少的记录,type=0) + $oneMonthAgo = time() - (30 * 24 * 60 * 60); // 30天前的时间戳 + + $totalConsumed = TokensRecord::where([ + ['userId', '=', $userId], + ['companyId', '=', $companyId], + ['type', '=', 0], // 只统计减少的记录 + ['createTime', '>=', $oneMonthAgo] + ])->sum('tokens'); + + $totalConsumed = intval($totalConsumed); + + // 如果过去30天没有消耗记录,无法计算 + if ($totalConsumed <= 0) { + return -1; + } + + // 计算平均每天消耗量 + $avgDailyConsumption = $totalConsumed / 30; + + // 如果平均每天消耗为0,无法计算 + if ($avgDailyConsumption <= 0) { + return -1; + } + + // 计算预计可用天数 = 当前余额 / 平均每天消耗量 + $estimatedDays = floor($remainingTokens / $avgDailyConsumption); + + return $estimatedDays; + } + + /** + * 分配token(仅管理员可用) + * @return \think\response\Json + */ + public function allocateTokens() + { + try { + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + $targetUserId = (int)$this->request->param('targetUserId', 0); + $tokens = (int)$this->request->param('tokens', 0); + $remarks = $this->request->param('remarks', ''); + + // 验证参数 + if (empty($targetUserId)) { + return ResponseHelper::error('目标用户ID不能为空'); + } + + if ($tokens <= 0) { + return ResponseHelper::error('分配的token数量必须大于0'); + } + + if (empty($companyId)) { + return ResponseHelper::error('公司信息获取失败'); + } + + // 验证当前用户是否为管理员 + $currentUser = User::where([ + 'id' => $userId, + 'companyId' => $companyId + ])->find(); + + if (empty($currentUser)) { + return ResponseHelper::error('用户信息不存在'); + } + + if (empty($currentUser->isAdmin) || $currentUser->isAdmin != 1) { + return ResponseHelper::error('只有管理员才能分配token'); + } + + // 验证目标用户是否存在且属于同一公司 + $targetUser = User::where([ + 'id' => $targetUserId, + 'companyId' => $companyId + ])->find(); + + if (empty($targetUser)) { + return ResponseHelper::error('目标用户不存在或不属于同一公司'); + } + + // 检查分配者的token余额 + $allocatorTokens = TokensCompany::where([ + 'companyId' => $companyId, + 'userId' => $userId + ])->find(); + + $allocatorBalance = $allocatorTokens ? intval($allocatorTokens->tokens) : 0; + + if ($allocatorBalance < $tokens) { + return ResponseHelper::error('token余额不足,当前余额:' . $allocatorBalance); + } + + // 开始事务 + Db::startTrans(); + + try { + // 1. 减少分配者的token + if (!empty($allocatorTokens)) { + $allocatorTokens->tokens = $allocatorBalance - $tokens; + $allocatorTokens->updateTime = time(); + $allocatorTokens->save(); + $allocatorNewBalance = $allocatorTokens->tokens; + } else { + // 如果分配者没有记录,创建一条(余额为0) + $allocatorTokens = new TokensCompany(); + $allocatorTokens->userId = $userId; + $allocatorTokens->companyId = $companyId; + $allocatorTokens->tokens = 0; + $allocatorTokens->isAdmin = 1; + $allocatorTokens->createTime = time(); + $allocatorTokens->updateTime = time(); + $allocatorTokens->save(); + $allocatorNewBalance = 0; + } + + // 2. 记录分配者的减少记录 + $targetUserAccount = $targetUser->account ?? $targetUser->phone ?? '用户ID[' . $targetUserId . ']'; + $allocatorRecord = new TokensRecord(); + $allocatorRecord->companyId = $companyId; + $allocatorRecord->userId = $userId; + $allocatorRecord->type = 0; // 0为减少 + $allocatorRecord->form = 1001; // 1001表示分配 + $allocatorRecord->wechatAccountId = 0; + $allocatorRecord->friendIdOrGroupId = $targetUserId; + $allocatorRecord->remarks = !empty($remarks) ? $remarks : '分配给' . $targetUserAccount; + $allocatorRecord->tokens = $tokens; + $allocatorRecord->balanceTokens = $allocatorNewBalance; + $allocatorRecord->createTime = time(); + $allocatorRecord->save(); + + // 3. 增加接收者的token + $receiverTokens = TokensCompany::where([ + 'companyId' => $companyId, + 'userId' => $targetUserId + ])->find(); + + if (!empty($receiverTokens)) { + $receiverTokens->tokens = intval($receiverTokens->tokens) + $tokens; + $receiverTokens->updateTime = time(); + $receiverTokens->save(); + $receiverNewBalance = $receiverTokens->tokens; + } else { + // 如果接收者没有记录,创建一条 + $receiverTokens = new TokensCompany(); + $receiverTokens->userId = $targetUserId; + $receiverTokens->companyId = $companyId; + $receiverTokens->tokens = $tokens; + $receiverTokens->isAdmin = (!empty($targetUser->isAdmin) && $targetUser->isAdmin == 1) ? 1 : 0; + $receiverTokens->createTime = time(); + $receiverTokens->updateTime = time(); + $receiverTokens->save(); + $receiverNewBalance = $tokens; + } + + // 4. 记录接收者的增加记录 + $adminAccount = $currentUser->account ?? $currentUser->phone ?? '管理员'; + $receiverRecord = new TokensRecord(); + $receiverRecord->companyId = $companyId; + $receiverRecord->userId = $targetUserId; + $receiverRecord->type = 1; // 1为增加 + $receiverRecord->form = 1001; // 1001表示分配 + $receiverRecord->wechatAccountId = 0; + $receiverRecord->friendIdOrGroupId = $userId; + $receiverRecord->remarks = !empty($remarks) ? '管理员分配:' . $remarks : '管理员分配'; + $receiverRecord->tokens = $tokens; + $receiverRecord->balanceTokens = $receiverNewBalance; + $receiverRecord->createTime = time(); + $receiverRecord->save(); + + Db::commit(); + + return ResponseHelper::success([ + 'allocatorBalance' => $allocatorNewBalance, + 'receiverBalance' => $receiverNewBalance, + 'allocatedTokens' => $tokens + ], '分配成功'); + + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('分配失败:' . $e->getMessage()); + } + + } catch (\Exception $e) { + return ResponseHelper::error('分配失败:' . $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/TrafficController.php b/application/cunkebao/controller/TrafficController.php new file mode 100644 index 0000000..4ceefc9 --- /dev/null +++ b/application/cunkebao/controller/TrafficController.php @@ -0,0 +1,321 @@ +request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + + $companyId = $this->getUserInfo('companyId'); + $package = Db::name('traffic_source_package')->alias('tsp') + ->join('traffic_source_package_item tspi', 'tspi.packageId=tsp.id', 'left') + ->whereIn('tsp.companyId', [$companyId, 0]) + ->field('tsp.id,tsp.name,tsp.description,tsp.pic,tsp.isSys as type,tsp.createTime,count(tspi.id) as num') + ->group('tsp.id'); + + if (!empty($keyword)) { + $package->where('tsp.name|tsp.description', 'like', '%' . $keyword . '%'); + } + + $list = $package->page($page, $limit)->order('isSys ASC,id DESC')->select(); + $total = $package->count(); + + $rfmRule = 'default'; + foreach ($list as $k => &$v) { + if ($v['type'] != 1) { + $v['createTime'] = !empty($v['createTime']) ? formatRelativeTime($v['createTime']) : ''; + } else { + $v['createTime'] = ''; + } + + // RFM 评分(示例:以创建时间近似最近活跃,num 近似频次;金额若无则为 0) + $recencyDays = isset($v['createTime']) && is_numeric($v['createTime']) ? floor((time() - (int)$v['createTime']) / 86400) : null; + // 如果上方被格式化为文本,则尝试从原始结果集取原值 + if (!is_numeric($recencyDays) || $recencyDays === null) { + $rawCreate = isset($list[$k]['createTime']) ? $list[$k]['createTime'] : null; + $recencyDays = is_numeric($rawCreate) ? floor((time() - (int)$rawCreate) / 86400) : 9999; + } + $frequency = (int)($v['num'] ?? 0); + $monetary = (float)($v['monetary'] ?? 0); + + $scores = RFMController::calcRfmScores($recencyDays, $frequency, $monetary); + $v['R'] = $scores['R']; + $v['F'] = $scores['F']; + $v['M'] = $scores['M']; + $v['RFM'] = $scores['R'] + $scores['F'] + $scores['M']; + } + unset($v); + + $data = [ + 'total' => $total, + 'list' => $list, + ]; + + return ResponseHelper::success($data); + } + + /** + * 添加流量池 + * @return \think\response\Json + * @throws \Exception + */ + public function addPackage() + { + $packageName = $this->request->param('packageName', ''); + $description = $this->request->param('description', ''); + $pic = $this->request->param('pic', ''); + $companyId = $this->getUserInfo('companyId'); + $userId = $this->getUserInfo('id'); + + if (empty($packageName)) { + return ResponseHelper::error('流量池名称不能为空'); + } + + $package = TrafficSourcePackage::where(['isDel' => 0, 'name' => $packageName]) + ->whereIn('companyId', [$companyId, 0]) + ->field('id,name') + ->find(); + if (!empty($package)) { + return ResponseHelper::error('该流量池名称已存在'); + } + $packageId = TrafficSourcePackage::insertGetId([ + 'userId' => $userId, + 'companyId' => $companyId, + 'name' => $packageName, + 'description' => $description, + 'pic' => $pic, + 'matchingRules' => json_encode([]), + 'createTime' => time(), + 'isDel' => 0, + ]); + + if (!empty($packageId)) { + return ResponseHelper::success($packageId, '该流量添加成功'); + } else { + return ResponseHelper::error('该流量添加失败'); + } + } + + /** + * 编辑流量池 + * @return \think\response\Json + * @throws \Exception + */ + public function editPackage() + { + $packageId = $this->request->param('packageId', ''); + $packageName = $this->request->param('packageName', ''); + $description = $this->request->param('description', ''); + $pic = $this->request->param('pic', ''); + $companyId = $this->getUserInfo('companyId'); + $userId = $this->getUserInfo('id'); + + if (empty($packageId)) { + return ResponseHelper::error('流量池ID不能为空'); + } + + if (empty($packageName)) { + return ResponseHelper::error('流量池名称不能为空'); + } + + // 检查流量池是否存在且属于当前公司 + $package = TrafficSourcePackage::where(['id' => $packageId, 'isDel' => 0]) + ->whereIn('companyId', [$companyId, 0]) + ->find(); + if (empty($package)) { + return ResponseHelper::error('流量池不存在或已删除'); + } + + // 检查系统流量池是否可编辑 + if ($package['isSys'] == 1) { + return ResponseHelper::error('系统流量池不允许编辑'); + } + + // 检查名称是否重复(排除当前记录) + $existPackage = TrafficSourcePackage::where(['isDel' => 0, 'name' => $packageName]) + ->whereIn('companyId', [$companyId, 0]) + ->where('id', '<>', $packageId) + ->field('id,name') + ->find(); + if (!empty($existPackage)) { + return ResponseHelper::error('该流量池名称已存在'); + } + + // 更新流量池信息 + $updateData = [ + 'name' => $packageName, + 'updateTime' => time(), + ]; + + // 更新描述字段(允许为空) + $updateData['description'] = $description; + + // 更新图片字段(允许为空) + $updateData['pic'] = $pic; + + $result = TrafficSourcePackage::where('id', $packageId)->update($updateData); + + if ($result !== false) { + return ResponseHelper::success($packageId, '流量池编辑成功'); + } else { + return ResponseHelper::error('流量池编辑失败'); + } + } + + /** + * 删除流量池(假删除) + * @return \think\response\Json + * @throws \Exception + */ + public function deletePackage() + { + $packageId = $this->request->param('packageId', ''); + $companyId = $this->getUserInfo('companyId'); + + if (empty($packageId)) { + return ResponseHelper::error('流量池ID不能为空'); + } + + // 检查流量池是否存在且属于当前公司 + $package = TrafficSourcePackage::where(['id' => $packageId, 'isDel' => 0]) + ->whereIn('companyId', [$companyId, 0]) + ->find(); + if (empty($package)) { + return ResponseHelper::error('流量池不存在或已删除'); + } + + // 检查系统流量池是否可删除 + if ($package['isSys'] == 1) { + return ResponseHelper::error('系统流量池不允许删除'); + } + + // 开启事务 + Db::startTrans(); + try { + // 执行流量池假删除 + $result = TrafficSourcePackage::where('id', $packageId)->update([ + 'isDel' => 1, + 'deleteTime' => time() + ]); + + if ($result === false) { + throw new \Exception('流量池删除失败'); + } + + // 删除流量池内容(TrafficSourcePackageItem)假删除 + $itemResult = TrafficSourcePackageItem::where([ + 'packageId' => $packageId, + 'companyId' => $companyId, + 'isDel' => 0 + ])->update([ + 'isDel' => 1, + 'deleteTime' => time() + ]); + + // 提交事务 + Db::commit(); + + return ResponseHelper::success($packageId, '流量池及内容删除成功'); + + } catch (\Exception $e) { + // 回滚事务 + Db::rollback(); + return ResponseHelper::error('删除失败:' . $e->getMessage()); + } + } + + + /** + * 流量池列表 + * @return \think\response\Json + * @throws \Exception + */ + public function getTrafficPoolList() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + $packageId = $this->request->param('packageId', ''); + $companyId = $this->getUserInfo('companyId'); + $userId = $this->getUserInfo('id'); + + if (empty($packageId)) { + return ResponseHelper::error('流量包id不能为空'); + } + + $trafficSourcePackage = TrafficSourcePackage::where(['id' => $packageId, 'isDel' => 0])->whereIn('companyId', [$companyId, 0])->find(); + if (empty($trafficSourcePackage)) { + return ResponseHelper::error('流量包不存在或已删除'); + } + $where = [ + ['tspi.companyId', '=', $companyId], + ['tspi.packageId', '=', $packageId], + ]; + + if (empty($keyword)) { + $where[] = ['wa.nickname|wa.phone|wa.alias|wa.wechatId|p.mobile|p.identifier', 'like', '%' . $keyword . '%']; + } + + $query = TrafficSourcePackageItem::alias('tspi') + ->field( + [ + 'p.id', 'p.identifier', 'p.mobile', 'p.wechatId', 'tspi.companyId', + 'wa.nickname', 'wa.avatar', 'wa.gender', 'wa.phone', 'wa.alias' + ] + ) + ->join('traffic_pool p', 'p.identifier=tspi.identifier', 'left') + ->join('wechat_account wa', 'tspi.identifier=wa.wechatId', 'left') + ->where($where); + + $query->order('tspi.id DESC,p.id DESC')->group('p.identifier'); + + $list = $query->page($page, $limit)->select()->toArray(); + $total = $query->count(); + + foreach ($list as $k => &$v) { + //流量池筛选 + $package = TrafficSourcePackageItem::alias('tspi') + ->join('traffic_source_package p', 'tspi.packageId=p.id AND tspi.companyId=p.companyId') + ->where(['tspi.identifier' => $v['identifier']]) + ->whereIn('tspi.companyId', [0, $v['companyId']]) + ->column('p.name'); + $v['packages'] = $package; + $v['phone'] = !empty($v['phone']) ? $v['phone'] : $v['mobile']; + unset($v['mobile']); + + + $scores = RFMController::calcRfmScores(30, 30, 30); + $v['R'] = $scores['R']; + $v['F'] = $scores['F']; + $v['M'] = $scores['M']; + $v['RFM'] = $scores['R'] + $scores['F'] + $scores['M']; + $v['money'] = 2222; + $v['msgCount'] = 2222; + $v['tag'] = ['test', 'test2']; + } + unset($v); + + + $data = ['list' => $list, 'total' => $total]; + return ResponseHelper::success($data); + } + +} \ No newline at end of file diff --git a/application/cunkebao/controller/chatroom/GetChatroomListV1Controller.php b/application/cunkebao/controller/chatroom/GetChatroomListV1Controller.php new file mode 100644 index 0000000..d9b11a0 --- /dev/null +++ b/application/cunkebao/controller/chatroom/GetChatroomListV1Controller.php @@ -0,0 +1,152 @@ +request->param('page', 1); + $limit = $this->request->param('limit', 20); + $keyword = $this->request->param('keyword', ''); + try { + + $companyId = (int)$this->getUserInfo('companyId'); + $wechatIds = Db::name('device')->alias('d') + // 仅关联每个设备在 device_wechat_login 中的最新一条记录 + ->join('(SELECT MAX(id) AS id, deviceId FROM ck_device_wechat_login WHERE companyId='.$companyId.' GROUP BY deviceId) dwl_max','dwl_max.deviceId = d.id') + ->join('device_wechat_login dwl','dwl.id = dwl_max.id') + ->where(['d.companyId' => $companyId,'d.deleteTime' => 0]) + ->column('dwl.wechatId'); + + + /* $wechatIds = Db::name('device')->alias('d') + ->join('device_wechat_login dwl','dwl.deviceId=d.id AND dwl.companyId='.$this->getUserInfo('companyId')) + ->where(['d.companyId' => $this->getUserInfo('companyId'),'d.deleteTime' => 0]) + ->column('dwl.wechatId');*/ + + + $where = []; + if ($this->getUserInfo('isAdmin') == 1) { + $where[] = ['gg.isDeleted', '=', 0]; + $where[] = ['g.ownerWechatId', 'in', $wechatIds]; + } else { + $where[] = ['gg.isDeleted', '=', 0]; + $where[] = ['g.ownerWechatId', 'in', $wechatIds]; + //$where[] = ['g.userId', '=', $this->getUserInfo('id')]; + } + + if(!empty($keyword)){ + $where[] = ['g.name', 'like', '%'.$keyword.'%']; + } + + $data = WechatChatroom::alias('g') + ->field(['g.id', 'g.chatroomId', 'g.name', 'g.avatar','g.ownerWechatId', 'g.identifier', 'g.createTime', + 'wa.nickname as ownerNickname','wa.avatar as ownerAvatar','wa.alias as ownerAlias']) + ->join('wechat_account wa', 'g.ownerWechatId = wa.wechatId', 'LEFT') + ->join(['s2_wechat_chatroom' => 'gg'], 'g.id = gg.id', 'LEFT') + ->where($where); + + $total = $data->count(); + $list = $data->page($page, $limit)->order('g.id DESC')->select(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + ] + ]); + } catch (\Exception $e) { + return json([ + 'code' => $e->getCode(), + 'msg' => $e->getMessage() + ]); + } + } + + /** + * 获取群成员列表 + * @return \think\response\Json + */ + public function getMemberList() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 20); + $keyword = $this->request->param('keyword', ''); + $groupId = $this->request->param('groupId', 0); + + if (empty($groupId)) { + return json([ + 'code' => 400, + 'msg' => '群ID不能为空' + ]); + } + + try { + $where = []; + $where[] = ['m.groupId', '=', $groupId]; + $where[] = ['m.deleteTime', '<=', 0]; + + // 如果有搜索关键词 + if (!empty($keyword)) { + $where[] = ['wa.nickname|m.identifier', 'like', '%'.$keyword.'%']; + } + + $data = Db::name('wechat_group_member') + ->alias('m') + ->field([ + 'm.id', + 'm.identifier', + 'm.customerIs', + 'wa.nickname', + 'wa.avatar', + 'm.groupId', + 'm.createTime', + 'g.name as groupName', + 'g.chatroomId' + ]) + ->join('wechat_group g', 'm.groupId = g.id', 'LEFT') + ->join('wechat_account wa', 'wa.wechatId = m.identifier', 'LEFT') + ->where($where); + + $total = $data->count(); + $list = $data->page($page, $limit) + ->order('m.id DESC') + ->select(); + + // 格式化时间 + foreach ($list as &$item) { + if (!empty($item['createTime'])) { + $item['createTime'] = date('Y-m-d H:i:s', $item['createTime']); + } + } + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + ] + ]); + } catch (\Exception $e) { + return json([ + 'code' => $e->getCode() ?: 500, + 'msg' => $e->getMessage() + ]); + } + } +} diff --git a/application/cunkebao/controller/device/DeleteDeviceV1Controller.php b/application/cunkebao/controller/device/DeleteDeviceV1Controller.php new file mode 100644 index 0000000..cb1b4a2 --- /dev/null +++ b/application/cunkebao/controller/device/DeleteDeviceV1Controller.php @@ -0,0 +1,145 @@ +getUserInfo('companyId'); + $deviceUser = DeviceUserModel::where(compact('companyId', 'deviceId'))->find(); + + // 有关联数据则删除 + if ($deviceUser) { + if (!$deviceUser->delete()) { + throw new \Exception('设备用户关联数据删除失败', 402); + } + } + } + + /** + * 删除设备任务配置记录 + * + * @param int $deviceId + * @return void + * @throws \Exception + */ + protected function deleteDeviceConf(int $deviceId): void + { + $companyId = $this->getUserInfo('companyId'); + $deviceConf = DeviceTaskconfModel::where(compact('companyId', 'deviceId'))->find(); + + // 有配置信息则删除 + if ($deviceConf) { + if (!$deviceConf->delete()) { + throw new \Exception('设备设置信息删除失败', 402); + } + } + } + + /** + * 删除主设备信息 + * + * @param int $id + * @return DeviceModel + * @throws \Exception + */ + protected function deleteDevice(int $id): void + { + $device = DeviceModel::where('companyId', $this->getUserInfo('companyId'))->find($id); + + if (!$device) { + throw new \Exception('设备不存在或无权限操作', 404); + } + + if (!$device->delete()) { + throw new \Exception('设备删除失败', 402); + } + } + + /** + * 删除存客宝设备数据 + * + * @param int $id + * @return $this + * @throws \Exception + */ + protected function deleteCkbAbout(int $id): self + { + $apiDevice = new ApiDevice(); + $res = $apiDevice->delDevice($id); + $res = json_decode($res, true); + if ($res['code'] == 200){ + $this->deleteDevice($id); + $this->deleteDeviceConf($id); + $this->deleteDeviceUser($id); + return $this; + }else{ + return false; + } + } + + /** + * TODO 删除存客宝设备数据 + * + * @return self + */ + protected function deleteS2About(): self + { + return $this; + } + + /** + * 检查用户权限,只有操盘手可以删除设备 + * + * @return $this + */ + protected function checkPermission(): self + { + if ($this->getUserInfo('typeId') != UserModel::MASTER_USER) { + throw new \Exception('您没有权限删除设备', 403); + } + + return $this; + } + + /** + * 删除设备 + * + * @return \think\response\Json + */ + public function index() + { + try { + $id = $this->request->param('id/d'); + + Db::startTrans(); + $this->checkPermission(); + $this->deleteCkbAbout($id)->deleteS2About($id); + Db::commit(); + + return ResponseHelper::success(); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/device/GetAddResultedV1Controller.php b/application/cunkebao/controller/device/GetAddResultedV1Controller.php new file mode 100644 index 0000000..a2c78b0 --- /dev/null +++ b/application/cunkebao/controller/device/GetAddResultedV1Controller.php @@ -0,0 +1,165 @@ +value('companyId'); + } + + /** + * 获取项目下的所有设备。 + * + * @param int $companyId + * @return array + */ + protected function getAllDevicesIdWithInCompany(int $companyId): array + { + return DeviceModel::where('companyId', $companyId)->column('id') ?: [0]; + } + + /** + * 执行数据迁移。 + * + * @param int $accountId + * @return void + */ + protected function migrateData(int $accountId): void + { + $companyId = $this->getCompanyIdByAccountId($accountId); + $deviceIds = $this->getAllDevicesIdWithInCompany($companyId) ?: [0]; + + // 从 s2_device 导入数据。 + $this->getNewDeviceFromS2_device($deviceIds, $companyId); + } + + /** + * 从 s2_device 导入数据。 + * + * @param array $ids + * @param int $companyId + * @return void + */ + protected function getNewDeviceFromS2_device(array $ids, int $companyId): void + { + $ids = implode(',', $ids); + + $sql = "INSERT INTO ck_device(`id`, `imei`, `model`, phone, operatingSystem, memo, alive, brand, rooted, xPosed, softwareVersion, extra, createTime, updateTime, deleteTime, companyId) + SELECT + d.id, d.imei, d.model, d.phone, d.operatingSystem, d.memo, d.alive, d.brand, d.rooted, d.xPosed, d.softwareVersion, d.extra, d.createTime, d.lastUpdateTime, d.deleteTime, a.departmentId AS companyId + FROM s2_device d + JOIN s2_company_account a ON d.currentAccountId = a.id + WHERE isDeleted = 0 AND deletedAndStop = 0 AND d.id NOT IN ({$ids}) AND a.departmentId = {$companyId} + ON DUPLICATE KEY UPDATE + imei = VALUES(imei), + model = VALUES(model), + phone = VALUES(phone), + operatingSystem = VALUES(operatingSystem), + memo = VALUES(memo), + alive = VALUES(alive), + brand = VALUES(brand), + rooted = VALUES(rooted), + xPosed = VALUES(xPosed), + softwareVersion = VALUES(softwareVersion), + extra = VALUES(extra), + updateTime = VALUES(updateTime), + deleteTime = VALUES(deleteTime), + companyId = VALUES(companyId)"; + + Db::query($sql); + } + + /** + * 获取当前设备数量 + * + * @return int + */ + protected function getCkbDeviceCount(): int + { + $companyId = $this->getUserInfo('companyId'); + $cacheKey = 'deviceNum_'.$companyId; + $deviceNum = Cache::get($cacheKey); + if (empty($deviceNum)) { + $deviceNum = DeviceModel::where(['companyId' => $companyId])->count('*'); + Cache::set($cacheKey,$deviceNum,120); + } + return $deviceNum; + } + + /** + * 获取添加的关联设备结果。 + * + * @param int $accountId + * @return bool + */ + protected function getAddResulted(int $accountId): bool + { + $deviceNum = $this->getCkbDeviceCount(); + $result = (new ApiDeviceController())->getlist( + [ + 'accountId' => $accountId, + 'pageIndex' => 0, + 'pageSize' => 100 + ], + true + ); + $result = json_decode($result, true); + $result = $result['data']['results'] ?? false; + + if (empty($result)){ + return false; + }else{ + if (count($result) > $deviceNum){ + $companyId = $this->getUserInfo('companyId'); + $cacheKey = 'deviceNum_'.$companyId; + Cache::rm($cacheKey); + return true; + }else{ + return false; + } + } + + } + + /** + * 获取基础统计信息 + * + * @return \think\response\Json + */ + public function index() + { + $accountId = $this->request->param('accountId/d'); + + if (empty($accountId)){ + return ResponseHelper::error('参数缺失'); + } + + $isAdded = $this->getAddResulted($accountId); + $isAdded && $this->migrateData($accountId); + + return ResponseHelper::success( + [ + 'added' => $isAdded + ] + ); + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/device/GetDeviceDetailV1Controller.php b/application/cunkebao/controller/device/GetDeviceDetailV1Controller.php new file mode 100644 index 0000000..d61bad5 --- /dev/null +++ b/application/cunkebao/controller/device/GetDeviceDetailV1Controller.php @@ -0,0 +1,188 @@ + $deviceId, + 'userId' => $this->getUserInfo('id'), + 'companyId' => $this->getUserInfo('companyId') + ] + ) + ->count() > 0; + + if (!$hasPermission) { + throw new \Exception('您没有权限查看该设备', 403); + } + } + + /** + * 解析设备额外信息 + * + * @param string $extra + * @return int + */ + protected function parseExtraForBattery(string $extra): int + { + if (!empty($extra)) { + $extra = json_decode($extra); + + if ($extra && isset($extra->battery)) { + return intval($extra->battery); + } + } + + return 0; + } + + /** + * 获取设备最新登录微信的 wechatId + * + * @param int $deviceId + * @return string|null + * @throws \Exception + */ + protected function getDeviceLatestWechatLogin(int $deviceId): ?string + { + return DeviceWechatLoginModel::where( + [ + 'companyId' => $this->getUserInfo('companyId'), + 'deviceId' => $deviceId, + 'alive' => DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE + ] + ) + ->value('wechatId'); + } + + /** + * 获取设备绑定的客服信息 + * + * @param int $deviceId + * @return array + * @throws \Exception + */ + protected function getWechatCustomerInfo(int $deviceId): array + { + $curstomer = WechatCustomerModel::field('activity,friendShip') + ->where( + [ + 'companyId' => $this->getUserInfo('companyId'), + 'wechatId' => $this->getDeviceLatestWechatLogin($deviceId) + ] + ) + ->find(); + + return $curstomer ? [ + 'lastUpdateTime' => $curstomer->activity->lastActivityTime ?? '', + 'thirtyDayMsgCount' => $curstomer->activity->totalMsgCount ?? 0, + 'totalFriend' => $curstomer->friendShip->totalFriend ?? 0, + ] : [ + 'lastUpdateTime' => '', + 'thirtyDayMsgCount' => 0, + 'totalFriend' => 0, + ]; + } + + /** + * 获取设备详情 + * + * @param int $id + * @return array + */ + protected function getDeviceInfo(int $id): array + { + // 查询设备基础信息与关联的微信账号信息 + $device = DeviceModel::alias('d') + ->field([ + 'd.id', 'd.imei', 'd.memo', 'd.alive', 'd.extra' + ]) + ->find($id); + + if (empty($device)) { + throw new \Exception('设备不存在', 404); + } + + $device->battery = $this->parseExtraForBattery($device->extra); + + // 删除冗余字段 + unset($device->extra); + + return $device->toArray(); + } + + /** + * 获取设备详情 + * + * @return \think\response\Json + */ + public function index() + { + try { + $id = $this->request->param('id/d'); + + if ($this->getUserInfo('isAdmin') != UserModel::ADMIN_STP) { + $this->checkUserDevicePermission($id); + } + + return ResponseHelper::success( + $this->getDeviceInfo($id) + $this->getWechatCustomerInfo($id) + ); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } + + + public function isUpdataWechat() + { + $id = $this->request->param('id/d'); + $companyId = $this->getUserInfo('companyId'); + $newWechat = DeviceWechatLoginModel::alias('a') + ->field('b.*') + ->join('wechat_account b', 'a.wechatId = b.wechatId') + ->where(['a.deviceId' => $id,'a.isTips' => 0,'a.companyId' => $companyId]) + ->order('a.id', 'desc') + ->find(); + if (empty($newWechat)){ + return ResponseHelper::success('','该设备绑定的微信无需迁移',201); + } + + $oldWechat = DeviceWechatLoginModel::alias('a') + ->field('b.*') + ->join('wechat_account b', 'a.wechatId = b.wechatId') + ->where(['a.companyId' => $companyId]) + ->where('a.deviceId' ,'<>', $id) + ->order('a.id', 'desc') + ->find(); + if (empty($oldWechat)){ + return ResponseHelper::success('','该设备绑定的微信无需迁移',201); + }else{ + DeviceWechatLoginModel::where(['deviceId' => $id,'isTips' => 0,'companyId' => $companyId])->update(['isTips' => 1]);; + return ResponseHelper::success(['newWechat' => $newWechat,'oldWechat' => $oldWechat]); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/device/GetDeviceHandleLogsV1Controller.php b/application/cunkebao/controller/device/GetDeviceHandleLogsV1Controller.php new file mode 100644 index 0000000..ccd031c --- /dev/null +++ b/application/cunkebao/controller/device/GetDeviceHandleLogsV1Controller.php @@ -0,0 +1,82 @@ + $deviceId, + 'userId' => $this->getUserInfo('id'), + 'companyId' => $this->getUserInfo('companyId') + ]; + + $hasPermission = DeviceUserModel::where($where)->count() > 0; + + if (!$hasPermission) { + throw new \Exception('您没有权限查看该设备', 403); + } + } + + /** + * 查询设备操作记录,并关联用户表获取操作人信息 + * + * @param int $deviceId + * @return \think\Paginator + */ + protected function getHandleLogs(int $deviceId): \think\Paginator + { + return DeviceHandleLog::alias('l') + ->field([ + 'l.id', 'l.content', 'l.createTime', + 'u.username' + ]) + ->leftJoin('users u', 'l.userId = u.id') + ->where('l.deviceId', $deviceId) + ->order('l.createTime desc') + ->paginate($this->request->param('limit/d', 10), false, ['page' => $this->request->param('page/d', 1)]); + } + + /** + * 获取设备操作记录 + * + * @return \think\response\Json + */ + public function index() + { + try { + $deviceId = $this->request->param('id/d'); + + if ($this->getUserInfo('isAdmin') != UserModel::ADMIN_STP) { + $this->checkUserDevicePermission($deviceId); + } + + $logs = $this->getHandleLogs($deviceId); + + return ResponseHelper::success( + [ + 'total' => $logs->total(), + 'list' => $logs->items() + ] + ); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/device/GetDeviceListV1Controller.php b/application/cunkebao/controller/device/GetDeviceListV1Controller.php new file mode 100644 index 0000000..f55310f --- /dev/null +++ b/application/cunkebao/controller/device/GetDeviceListV1Controller.php @@ -0,0 +1,186 @@ +request->param('keyword'))) { + $where[] = ['exp', "d.imei LIKE '%{$keyword}%' OR d.memo LIKE '%{$keyword}%'"]; + } + + // 设备在线状态 + if (is_numeric($alive = $this->request->param('alive'))) { + $where['d.alive'] = $alive; + } + + $where['d.companyId'] = $this->getUserInfo('companyId'); + + return array_merge($params, $where); + } + + /** + * 获取指定用户的所有设备ID + * + * @return array + */ + protected function makeDeviceIdsWhere(): array + { + $deviceIds = DeviceUserModel::where( + [ + 'userId' => $this->getUserInfo('id'), + 'companyId' => $this->getUserInfo('companyId') + ] + ) + ->column('deviceId'); + + if (empty($deviceIds)) { + throw new \Exception('请联系管理员绑定设备', 403); + } + + $where['d.id'] = array('in', $deviceIds); + + return $where; + } + + /** + * 获取设备列表 + * + * @param array $where 查询条件 + * @return \think\Paginator 分页对象 + */ + protected function getDeviceList(array $where): \think\Paginator + { + + $companyId = $this->getUserInfo('companyId'); + $query = DeviceModel::alias('d') + ->field([ + 'd.id', 'd.imei', 'd.memo', 'd.alive', + 'l.wechatId', + 'a.nickname', 'a.alias', 'a.avatar', '0 totalFriend' + ]) + ->join('(SELECT MAX(id) AS id, deviceId FROM ck_device_wechat_login WHERE companyId='.$companyId.' GROUP BY deviceId) dwl_max','dwl_max.deviceId = d.id') + ->join('device_wechat_login l','l.id = dwl_max.id') + ->join('wechat_account a', 'l.wechatId = a.wechatId') + ->order('d.id desc'); + + foreach ($where as $key => $value) { + if (is_numeric($key) && is_array($value) && isset($value[0]) && $value[0] === 'exp') { + $query->whereExp('', $value[1]); + continue; + } + + $query->where($key, $value); + } + + return $query->paginate($this->request->param('limit/d', 10), false, ['page' => $this->request->param('page/d', 1)]); + } + + /** + * 获取设备最新登录微信的 wechatId + * + * @param int $deviceId + * @return string|null + * @throws \Exception + */ + protected function getDeviceLatestWechatLogin(int $deviceId): ?string + { + return DeviceWechatLoginModel::where( + [ + 'companyId' => $this->getUserInfo('companyId'), + 'deviceId' => $deviceId, + 'alive' => DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE + ] + ) + ->value('wechatId'); + } + + /** + * 获取设备绑定的客服信息 + * + * @param string $wechatId + * @return int + * @throws \Exception + */ + protected function getWechatCustomerInfo(string $wechatId): int + { + $curstomer = WechatCustomerModel::field('friendShip') + ->where( + [ + //'companyId' => $this->getUserInfo('companyId'), + 'wechatId' => $wechatId + ] + ) + ->find(); + + return $curstomer->friendShip->totalFriend ?? 0; + } + + /** + * 统计微信好友 + * + * @param \think\Paginator $list + * @return array + */ + protected function countFriend(\think\Paginator $list): array + { + $resultSets = []; + + foreach ($list->items() as $item) { + $wechatId = $this->getDeviceLatestWechatLogin($item->id); + + $item->totalFriend = $wechatId ? $this->getWechatCustomerInfo($wechatId) : 0; + + array_push($resultSets, $item->toArray()); + } + + return $resultSets; + } + + /** + * 获取设备列表 + * @return \think\response\Json + */ + public function index() + { + try { + if ($this->getUserInfo('isAdmin') == UserModel::ADMIN_STP) { + $where = $this->makeWhere(); + $result = $this->getDeviceList($where); + }else { + //$where = $this->makeWhere( $this->makeDeviceIdsWhere() ); + $where = $this->makeWhere(); + $result = $this->getDeviceList($where); + } + + return ResponseHelper::success( + [ + 'list' => $this->countFriend($result), + 'total' => $result->total(), + ] + ); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/device/GetDeviceTaskConfigV1Controller.php b/application/cunkebao/controller/device/GetDeviceTaskConfigV1Controller.php new file mode 100644 index 0000000..5cacb6a --- /dev/null +++ b/application/cunkebao/controller/device/GetDeviceTaskConfigV1Controller.php @@ -0,0 +1,85 @@ + $deviceId, + 'userId' => $this->getUserInfo('id'), + 'companyId' => $this->getUserInfo('companyId') + ] + ) + ->count() > 0; + + if (!$hasPermission) { + throw new \Exception('您没有权限查看该设备', 403); + } + } + + /** + * 解析taskConfig字段获取功能开关 + * + * @param int $deviceId + * @return int[] + * @throws \Exception + */ + protected function getTaskConfig(int $deviceId): array + { + $conf = DeviceTaskconfModel::alias('c') + ->field([ + 'c.autoAddFriend', 'c.autoReply', 'c.momentsSync', 'c.aiChat' + ]) + ->where( + [ + 'companyId' => $this->getUserInfo('companyId'), + 'deviceId' => $deviceId + ] + ) + ->find(); + + // 未配置时赋予默认关闭的状态 + return !is_null($conf) ? $conf->toArray() : ArrHelper::getValue('autoAddFriend,autoReply,momentsSync,aiChat', [], 0); + } + + /** + * 获取设备详情 + * + * @return \think\response\Json + */ + public function index() + { + try { + $id = $this->request->param('id/d'); + + if ($this->getUserInfo('isAdmin') != UserModel::ADMIN_STP) { + $this->checkUserDevicePermission($id); + } + + return ResponseHelper::success( + $this->getTaskConfig($id) + ); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/device/PostAddDeviceV1Controller.php b/application/cunkebao/controller/device/PostAddDeviceV1Controller.php new file mode 100644 index 0000000..5a8477c --- /dev/null +++ b/application/cunkebao/controller/device/PostAddDeviceV1Controller.php @@ -0,0 +1,20 @@ +getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/device/UpdateDeviceTaskConfigV1Controller.php b/application/cunkebao/controller/device/UpdateDeviceTaskConfigV1Controller.php new file mode 100644 index 0000000..97c326a --- /dev/null +++ b/application/cunkebao/controller/device/UpdateDeviceTaskConfigV1Controller.php @@ -0,0 +1,140 @@ + $deviceId, + 'companyId' => $this->getUserInfo('companyId'), + ]; + + $device = DeviceModel::find($where); + + if (!$device) { + throw new \Exception('设备不存在或已删除', 404); + } + } + + /** + * 检查用户是否有权限操作指定设备 + * + * @param int $deviceId + * @return void + */ + protected function checkUserDevicePermission(int $deviceId): void + { + $where = [ + 'deviceId' => $deviceId, + 'userId' => $this->getUserInfo('id'), + 'companyId' => $this->getUserInfo('companyId') + ]; + + $hasPermission = DeviceUserModel::where($where)->count() > 0; + + if (!$hasPermission) { + throw new \Exception('您没有权限操作该设备', 403); + } + } + + /** + * 添加设备操作日志 + * + * @param int $deviceId + * @return void + * @throws \Exception + */ + protected function addHandleLog(int $deviceId): void + { + $data = $this->request->post(); + $content = null; + + if (isset($data['autoAddFriend']))/**/ $content = $data['autoAddFriend'] ? '开启自动添加好友' : '关闭自动添加好友'; + if (isset($data['autoReply']))/* */ $content = $data['autoReply'] ? '开启自动回复' : '关闭自动回复'; + if (isset($data['momentsSync']))/* */ $content = $data['momentsSync'] ? '开启朋友圈同步' : '关闭朋友圈同步'; + if (isset($data['aiChat']))/* */ $content = $data['aiChat'] ? '开启AI会话' : '关闭AI会话'; + + if (empty($content)) { + throw new \Exception('参数错误', 400); + } + + DeviceHandleLogModel::addLog( + [ + 'deviceId' => $deviceId, + 'content' => $content, + 'userId' => $this->getUserInfo('id'), + 'companyId' => $this->getUserInfo('companyId'), + ] + ); + } + + /** + * 更新设备taskConfig字段 + * + * @param int $deviceId + * @return void + */ + protected function setTaskconf(int $deviceId): void + { + $data = $this->request->post(); + $conf = DeviceTaskconf::where('deviceId', $deviceId)->find(); + + if ($conf) { + DeviceTaskconf::where('deviceId', $deviceId)->update($data); + } else { + DeviceTaskconf::create(array_merge($data, [ + 'companyId' => $this->getUserInfo('companyId'), + ])); + } + } + + /** + * 更新设备任务配置 + * @return \think\response\Json + */ + public function index() + { + $id = $this->request->param('deviceId/d'); + + $this->checkDeviceExists($id); + + if ($this->getUserInfo('isAdmin') != UserModel::ADMIN_STP) { + $this->checkUserDevicePermission($id); + } + + try { + Db::startTrans(); + + $this->addHandleLog($id); + $this->setTaskconf($id); + + Db::commit(); + + return ResponseHelper::success(); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/distribution/ChannelController.php b/application/cunkebao/controller/distribution/ChannelController.php new file mode 100644 index 0000000..6df58d1 --- /dev/null +++ b/application/cunkebao/controller/distribution/ChannelController.php @@ -0,0 +1,1454 @@ +request->param('name', ''); + $phone = $this->request->param('phone', ''); + $wechatId = $this->request->param('wechatId', ''); + $remarks = $this->request->param('remarks', ''); + $createType = $this->request->param('createType', DistributionChannel::CREATE_TYPE_MANUAL); // 默认为手动创建 + + $companyId = $this->getUserInfo('companyId'); + $userId = $this->getUserInfo('id'); + + // 参数验证 + if (empty($name)) { + return ResponseHelper::error('渠道名称不能为空', 400); + } + + // 验证渠道名称长度 + if (mb_strlen($name) > 50) { + return ResponseHelper::error('渠道名称长度不能超过50个字符', 400); + } + + // 验证手机号格式(如果提供) + if (!empty($phone)) { + if (!preg_match('/^1[3-9]\d{9}$/', $phone)) { + return ResponseHelper::error('手机号格式不正确,请输入11位数字且以1开头', 400); + } + + // 检查手机号是否已存在(排除已删除的渠道) + $existChannel = Db::name('distribution_channel') + ->where([ + ['companyId', '=', $companyId], + ['phone', '=', $phone], + ['deleteTime', '=', 0] + ]) + ->find(); + + if ($existChannel) { + return ResponseHelper::error('手机号已被使用', 400); + } + } + + // 验证微信号长度(如果提供) + if (!empty($wechatId) && mb_strlen($wechatId) > 50) { + return ResponseHelper::error('微信号长度不能超过50个字符', 400); + } + + // 验证备注长度(如果提供) + if (!empty($remarks) && mb_strlen($remarks) > 200) { + return ResponseHelper::error('备注信息长度不能超过200个字符', 400); + } + + // 验证创建类型 + if (!in_array($createType, [DistributionChannel::CREATE_TYPE_MANUAL, DistributionChannel::CREATE_TYPE_AUTO])) { + $createType = DistributionChannel::CREATE_TYPE_MANUAL; + } + + // 生成渠道编码 + $code = DistributionChannel::generateChannelCode(); + + // 准备插入数据 + $data = [ + 'companyId' => $companyId, + 'userId' => $userId, + 'name' => $name, + 'code' => $code, + 'phone' => $phone ?: '', + 'password' => md5('123456'), // 默认密码123456,MD5加密 + 'wechatId' => $wechatId ?: '', + 'remarks' => $remarks ?: '', + 'createType' => $createType, + 'status' => DistributionChannel::STATUS_ENABLED, + 'totalCustomers' => 0, + 'todayCustomers' => 0, + 'totalFriends' => 0, + 'todayFriends' => 0, + 'withdrawableAmount' => 0, // 以分为单位存储 + 'createTime' => time(), + 'updateTime' => time(), + ]; + + // 插入数据库 + $channelId = Db::name('distribution_channel')->insertGetId($data); + + if (!$channelId) { + return ResponseHelper::error('创建渠道失败', 500); + } + + // 获取创建的数据 + $channel = Db::name('distribution_channel')->where('id', $channelId)->find(); + + // 格式化返回数据 + $result = [ + 'id' => $channel['id'], + 'name' => $channel['name'], + 'code' => $channel['code'], + 'phone' => $channel['phone'] ?: '', + 'wechatId' => $channel['wechatId'] ?: '', + 'companyId' => (int)$companyId, // 返回companyId,方便小程序自动跳转 + 'userId' => (int)($channel['userId'] ?? 0), + 'createType' => $channel['createType'], + 'status' => $channel['status'], + 'totalCustomers' => (int)$channel['totalCustomers'], + 'todayCustomers' => (int)$channel['todayCustomers'], + 'totalFriends' => (int)$channel['totalFriends'], + 'todayFriends' => (int)$channel['todayFriends'], + 'withdrawableAmount' => round(($channel['withdrawableAmount'] ?? 0) / 100, 2), // 分转元,保留2位小数 + 'createTime' => !empty($channel['createTime']) ? date('Y-m-d H:i:s', $channel['createTime']) : date('Y-m-d H:i:s'), + ]; + + // 返回符合需求的格式(包含success字段) + return json([ + 'code' => 200, + 'success' => true, + 'msg' => '创建成功', + 'data' => $result + ]); + + } catch (Exception $e) { + return ResponseHelper::error('创建渠道失败:' . $e->getMessage(), $e->getCode() ?: 500); + } + } + + /** + * 获取渠道列表 + * @return \think\response\Json + */ + public function index() + { + try { + // 获取参数 + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 20); + $keyword = $this->request->param('keyword', ''); + $status = $this->request->param('status', 'all'); // all、enabled、disabled + + $companyId = $this->getUserInfo('companyId'); + + // 参数验证 + $page = max(1, intval($page)); + $limit = max(1, min(100, intval($limit))); // 限制最大100 + + // 验证状态参数 + $validStatuses = ['all', DistributionChannel::STATUS_ENABLED, DistributionChannel::STATUS_DISABLED]; + if (!in_array($status, $validStatuses)) { + $status = 'all'; + } + + // 构建查询条件 + $where = []; + $where[] = ['companyId', '=', $companyId]; + $where[] = ['deleteTime', '=', 0]; + + // 如果不是管理员,只能查看自己创建的数据 + if (!$this->getUserInfo('isAdmin')) { + $where[] = ['userId', '=', $this->getUserInfo('id')]; + } + + // 状态筛选 + if ($status !== 'all') { + $where[] = ['status', '=', $status]; + } + + // 关键词搜索(模糊匹配 name、code、phone、wechatId) + if (!empty($keyword)) { + $keyword = trim($keyword); + // 使用 | 分隔字段表示OR关系(ThinkPHP语法) + $where[] = ['name|code|phone|wechatId', 'like', '%' . $keyword . '%']; + } + + // 查询总数 + $total = Db::name('distribution_channel') + ->where($where) + ->count(); + + // 查询列表(按创建时间倒序) + $list = Db::name('distribution_channel') + ->where($where) + ->order('createTime DESC') + ->page($page, $limit) + ->select(); + + // 格式化数据 + $formattedList = []; + foreach ($list as $item) { + $formattedItem = [ + 'id' => (int)$item['id'], + 'name' => $item['name'] ?? '', + 'code' => $item['code'] ?? '', + 'phone' => !empty($item['phone']) ? $item['phone'] : null, + 'wechatId' => !empty($item['wechatId']) ? $item['wechatId'] : null, + 'companyId' => (int)($item['companyId'] ?? 0), + 'userId' => (int)($item['userId'] ?? 0), + 'createType' => $item['createType'] ?? 'manual', + 'status' => $item['status'] ?? 'enabled', + 'totalCustomers' => (int)($item['totalCustomers'] ?? 0), + 'todayCustomers' => (int)($item['todayCustomers'] ?? 0), + 'totalFriends' => (int)($item['totalFriends'] ?? 0), + 'todayFriends' => (int)($item['todayFriends'] ?? 0), + 'withdrawableAmount' => round(($item['withdrawableAmount'] ?? 0) / 100, 2), // 分转元,保留2位小数 + 'createTime' => !empty($item['createTime']) ? date('Y-m-d H:i:s', $item['createTime']) : date('Y-m-d H:i:s'), + ]; + $formattedList[] = $formattedItem; + } + + // 返回结果 + return json([ + 'code' => 200, + 'success' => true, + 'msg' => '获取成功', + 'data' => [ + 'list' => $formattedList, + 'total' => (int)$total + ] + ]); + + } catch (Exception $e) { + return json([ + 'code' => $e->getCode() ?: 500, + 'success' => false, + 'msg' => '获取渠道列表失败:' . $e->getMessage(), + 'data' => null + ]); + } + } + + /** + * 编辑渠道 + * @return \think\response\Json + */ + public function update() + { + try { + // 获取参数 + $id = $this->request->param('id', 0); + $name = $this->request->param('name', ''); + $phone = $this->request->param('phone', ''); + $wechatId = $this->request->param('wechatId', ''); + $remarks = $this->request->param('remarks', ''); + + $companyId = $this->getUserInfo('companyId'); + + // 参数验证 + if (empty($id)) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '渠道ID不能为空', + 'data' => null + ]); + } + + // 检查渠道是否存在且属于当前公司 + $channel = Db::name('distribution_channel') + ->where(['id' => $id, 'companyId' => $companyId, 'deleteTime' => 0]) + ->find(); + + if (!$channel) { + return json([ + 'code' => 404, + 'success' => false, + 'msg' => '渠道不存在或没有权限', + 'data' => null + ]); + } + + // 准备更新数据 + $updateData = []; + $updateData['updateTime'] = time(); + + // 更新渠道名称 + if (!empty($name)) { + if (mb_strlen($name) > 50) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '渠道名称长度不能超过50个字符', + 'data' => null + ]); + } + $updateData['name'] = $name; + } + + // 更新手机号 + if (isset($phone)) { // 允许设置为空 + if (!empty($phone)) { + if (!preg_match('/^1[3-9]\d{9}$/', $phone)) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '手机号格式不正确,请输入11位数字且以1开头', + 'data' => null + ]); + } + + // 检查手机号是否已被其他渠道使用(排除当前渠道和已删除的渠道) + $existChannel = Db::name('distribution_channel') + ->where([ + ['companyId', '=', $companyId], + ['phone', '=', $phone], + ['id', '<>', $id], + ['deleteTime', '=', 0] + ]) + ->find(); + + if ($existChannel) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '手机号已被其他渠道使用', + 'data' => null + ]); + } + } + $updateData['phone'] = $phone ?: ''; + } + + // 更新微信号 + if (isset($wechatId)) { // 允许设置为空 + if (!empty($wechatId) && mb_strlen($wechatId) > 50) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '微信号长度不能超过50个字符', + 'data' => null + ]); + } + $updateData['wechatId'] = $wechatId ?: ''; + } + + // 更新备注 + if (isset($remarks)) { // 允许设置为空 + if (!empty($remarks) && mb_strlen($remarks) > 200) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '备注信息长度不能超过200个字符', + 'data' => null + ]); + } + $updateData['remarks'] = $remarks ?: ''; + } + + // 如果没有要更新的数据 + if (count($updateData) <= 1) { // 只有updateTime + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '没有要更新的数据', + 'data' => null + ]); + } + + // 更新数据库 + $result = Db::name('distribution_channel') + ->where(['id' => $id, 'companyId' => $companyId]) + ->update($updateData); + + if ($result === false) { + return json([ + 'code' => 500, + 'success' => false, + 'msg' => '更新渠道失败', + 'data' => null + ]); + } + + // 获取更新后的数据 + $updatedChannel = Db::name('distribution_channel') + ->where('id', $id) + ->find(); + + // 格式化返回数据 + $resultData = [ + 'id' => (int)$updatedChannel['id'], + 'name' => $updatedChannel['name'], + 'code' => $updatedChannel['code'], + 'phone' => !empty($updatedChannel['phone']) ? $updatedChannel['phone'] : null, + 'wechatId' => !empty($updatedChannel['wechatId']) ? $updatedChannel['wechatId'] : null, + 'companyId' => (int)($updatedChannel['companyId'] ?? 0), + 'userId' => (int)($updatedChannel['userId'] ?? 0), + 'createType' => $updatedChannel['createType'], + 'status' => $updatedChannel['status'], + 'totalCustomers' => (int)$updatedChannel['totalCustomers'], + 'todayCustomers' => (int)$updatedChannel['todayCustomers'], + 'totalFriends' => (int)$updatedChannel['totalFriends'], + 'todayFriends' => (int)$updatedChannel['todayFriends'], + 'withdrawableAmount' => round(($updatedChannel['withdrawableAmount'] ?? 0) / 100, 2), // 分转元,保留2位小数 + 'createTime' => !empty($updatedChannel['createTime']) ? date('Y-m-d H:i:s', $updatedChannel['createTime']) : date('Y-m-d H:i:s'), + ]; + + return json([ + 'code' => 200, + 'success' => true, + 'msg' => '更新成功', + 'data' => $resultData + ]); + + } catch (Exception $e) { + return json([ + 'code' => $e->getCode() ?: 500, + 'success' => false, + 'msg' => '更新渠道失败:' . $e->getMessage(), + 'data' => null + ]); + } + } + + /** + * 删除渠道(软删除) + * @return \think\response\Json + */ + public function delete() + { + try { + // 获取参数 + $id = $this->request->param('id', 0); + + $companyId = $this->getUserInfo('companyId'); + + // 参数验证 + if (empty($id)) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '渠道ID不能为空', + 'data' => null + ]); + } + + // 检查渠道是否存在且属于当前公司 + $channel = Db::name('distribution_channel') + ->where(['id' => $id, 'companyId' => $companyId, 'deleteTime' => 0]) + ->find(); + + if (!$channel) { + return json([ + 'code' => 404, + 'success' => false, + 'msg' => '渠道不存在或没有权限', + 'data' => null + ]); + } + + // 软删除 + $result = Db::name('distribution_channel') + ->where(['id' => $id, 'companyId' => $companyId]) + ->update([ + 'deleteTime' => time(), + 'updateTime' => time() + ]); + + if ($result === false) { + return json([ + 'code' => 500, + 'success' => false, + 'msg' => '删除渠道失败', + 'data' => null + ]); + } + + return json([ + 'code' => 200, + 'success' => true, + 'msg' => '删除成功', + 'data' => null + ]); + + } catch (Exception $e) { + return json([ + 'code' => $e->getCode() ?: 500, + 'success' => false, + 'msg' => '删除渠道失败:' . $e->getMessage(), + 'data' => null + ]); + } + } + + /** + * 禁用/启用渠道 + * @return \think\response\Json + */ + public function toggleStatus() + { + try { + // 获取参数 + $id = $this->request->param('id', 0); + $status = $this->request->param('status', ''); // enabled 或 disabled + + $companyId = $this->getUserInfo('companyId'); + + // 参数验证 + if (empty($id)) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '渠道ID不能为空', + 'data' => null + ]); + } + + if (!in_array($status, [DistributionChannel::STATUS_ENABLED, DistributionChannel::STATUS_DISABLED])) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '状态参数错误,必须为 enabled 或 disabled', + 'data' => null + ]); + } + + // 检查渠道是否存在且属于当前公司 + $channel = Db::name('distribution_channel') + ->where(['id' => $id, 'companyId' => $companyId, 'deleteTime' => 0]) + ->find(); + + if (!$channel) { + return json([ + 'code' => 404, + 'success' => false, + 'msg' => '渠道不存在或没有权限', + 'data' => null + ]); + } + + // 如果状态相同,直接返回成功 + if ($channel['status'] === $status) { + $msg = $status === DistributionChannel::STATUS_ENABLED ? '渠道已启用' : '渠道已禁用'; + return json([ + 'code' => 200, + 'success' => true, + 'msg' => $msg, + 'data' => null + ]); + } + + // 更新状态 + $result = Db::name('distribution_channel') + ->where(['id' => $id, 'companyId' => $companyId]) + ->update([ + 'status' => $status, + 'updateTime' => time() + ]); + + if ($result === false) { + return json([ + 'code' => 500, + 'success' => false, + 'msg' => '更新状态失败', + 'data' => null + ]); + } + + $msg = $status === DistributionChannel::STATUS_ENABLED ? '启用成功' : '禁用成功'; + + return json([ + 'code' => 200, + 'success' => true, + 'msg' => $msg, + 'data' => [ + 'id' => (int)$id, + 'status' => $status + ] + ]); + + } catch (Exception $e) { + return json([ + 'code' => $e->getCode() ?: 500, + 'success' => false, + 'msg' => '更新状态失败:' . $e->getMessage(), + 'data' => null + ]); + } + } + + /** + * 获取渠道统计数据 + * @return \think\response\Json + */ + public function statistics() + { + try { + $companyId = $this->getUserInfo('companyId'); + + // 获取今日开始和结束时间戳 + $todayStart = strtotime(date('Y-m-d 00:00:00')); + $todayEnd = strtotime(date('Y-m-d 23:59:59')); + + // 构建基础查询条件 + $baseWhere = [ + ['companyId', '=', $companyId], + ['deleteTime', '=', 0] + ]; + + // 如果不是管理员,只能查看自己创建的数据 + if (!$this->getUserInfo('isAdmin')) { + $baseWhere[] = ['userId', '=', $this->getUserInfo('id')]; + } + + // 1. 总渠道数 + $totalChannels = Db::name('distribution_channel') + ->where($baseWhere) + ->count(); + + // 2. 今日新增渠道数 + $todayChannels = Db::name('distribution_channel') + ->where($baseWhere) + ->where('createTime', 'between', [$todayStart, $todayEnd]) + ->count(); + + // 3. 统计所有渠道的获客数和加好友数(使用聚合函数) + $statistics = Db::name('distribution_channel') + ->where($baseWhere) + ->field([ + 'SUM(totalCustomers) as totalCustomers', + 'SUM(todayCustomers) as todayCustomers', + 'SUM(totalFriends) as totalFriends', + 'SUM(todayFriends) as todayFriends' + ]) + ->find(); + + // 格式化统计数据 + $data = [ + 'totalChannels' => (int)$totalChannels, + 'todayChannels' => (int)$todayChannels, + 'totalCustomers' => (int)($statistics['totalCustomers'] ?? 0), + 'todayCustomers' => (int)($statistics['todayCustomers'] ?? 0), + 'totalFriends' => (int)($statistics['totalFriends'] ?? 0), + 'todayFriends' => (int)($statistics['todayFriends'] ?? 0), + ]; + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => $data + ]); + + } catch (Exception $e) { + return json([ + 'code' => $e->getCode() ?: 500, + 'msg' => '获取统计数据失败:' . $e->getMessage(), + 'data' => null + ]); + } + } + + /** + * 获取渠道收益统计(全局统计) + * @return \think\response\Json + */ + public function revenueStatistics() + { + try { + $companyId = $this->getUserInfo('companyId'); + + // 构建基础查询条件 + $baseWhere = [ + ['companyId', '=', $companyId] + ]; + + // 如果不是管理员,只能查看自己创建的提现申请 + if (!$this->getUserInfo('isAdmin')) { + $baseWhere[] = ['userId', '=', $this->getUserInfo('id')]; + } + + // 1. 总支出:所有已打款的提现申请金额总和(状态为paid) + $totalExpenditure = Db::name('distribution_withdrawal') + ->where($baseWhere) + ->where('status', DistributionWithdrawal::STATUS_PAID) + ->sum('amount'); + $totalExpenditure = intval($totalExpenditure ?? 0); + + // 2. 已提现:所有已打款的提现申请金额总和(状态为paid) + $withdrawn = $totalExpenditure; // 已提现 = 总支出 + + // 3. 待审核:所有待审核的提现申请金额总和(状态为pending) + $pendingReview = Db::name('distribution_withdrawal') + ->where($baseWhere) + ->where('status', DistributionWithdrawal::STATUS_PENDING) + ->sum('amount'); + $pendingReview = intval($pendingReview ?? 0); + + // 格式化返回数据(分转元) + $data = [ + 'totalExpenditure' => round($totalExpenditure / 100, 2), // 总支出(元) + 'withdrawn' => round($withdrawn / 100, 2), // 已提现(元) + 'pendingReview' => round($pendingReview / 100, 2), // 待审核(元) + ]; + + return json([ + 'code' => 200, + 'success' => true, + 'msg' => '获取成功', + 'data' => $data + ]); + + } catch (Exception $e) { + return json([ + 'code' => $e->getCode() ?: 500, + 'success' => false, + 'msg' => '获取收益统计失败:' . $e->getMessage(), + 'data' => null + ]); + } + } + + /** + * 获取渠道收益明细列表(多个渠道的统计) + * @return \think\response\Json + */ + public function revenueDetail() + { + try { + // 获取参数 + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 20); + $keyword = $this->request->param('keyword', ''); + + $companyId = $this->getUserInfo('companyId'); + + // 参数验证 + $page = max(1, intval($page)); + $limit = max(1, min(100, intval($limit))); // 限制最大100 + + // 构建查询条件 + $where = []; + $where[] = ['companyId', '=', $companyId]; + $where[] = ['deleteTime', '=', 0]; + + // 如果不是管理员,只能查看自己创建的数据 + if (!$this->getUserInfo('isAdmin')) { + $where[] = ['userId', '=', $this->getUserInfo('id')]; + } + + // 关键词搜索(模糊匹配 name、code) + if (!empty($keyword)) { + $keyword = trim($keyword); + $where[] = ['name|code', 'like', '%' . $keyword . '%']; + } + + // 查询总数 + $total = Db::name('distribution_channel') + ->where($where) + ->count(); + + // 查询渠道列表(按创建时间倒序) + $channels = Db::name('distribution_channel') + ->where($where) + ->order('createTime DESC') + ->page($page, $limit) + ->select(); + + // 批量获取所有渠道的提现统计数据(提高性能) + $channelIds = array_column($channels, 'id'); + $withdrawalStats = []; + if (!empty($channelIds)) { + // 构建提现查询条件 + $withdrawalWhere = [ + ['companyId', '=', $companyId], + ['channelId', 'in', $channelIds] + ]; + + // 如果不是管理员,只能查看自己创建的提现申请 + if (!$this->getUserInfo('isAdmin')) { + $withdrawalWhere[] = ['userId', '=', $this->getUserInfo('id')]; + } + + // 按渠道ID和状态分组统计提现金额 + $stats = Db::name('distribution_withdrawal') + ->where($withdrawalWhere) + ->field([ + 'channelId', + 'status', + 'SUM(amount) as totalAmount' + ]) + ->group('channelId, status') + ->select(); + + // 组织统计数据 + foreach ($stats as $stat) { + $cid = $stat['channelId']; + if (!isset($withdrawalStats[$cid])) { + $withdrawalStats[$cid] = [ + 'totalRevenue' => 0, // 总收益(不包括驳回的) + 'withdrawn' => 0, // 已打款(paid) + 'pendingReview' => 0 // 待审核(pending) + ]; + } + $amount = intval($stat['totalAmount'] ?? 0); + $status = $stat['status']; + + // totalRevenue 不包括驳回(rejected)状态的金额 + if ($status !== DistributionWithdrawal::STATUS_REJECTED) { + $withdrawalStats[$cid]['totalRevenue'] += $amount; + } + + if ($status === DistributionWithdrawal::STATUS_PAID) { + $withdrawalStats[$cid]['withdrawn'] += $amount; + } elseif ($status === DistributionWithdrawal::STATUS_PENDING) { + $withdrawalStats[$cid]['pendingReview'] += $amount; + } + } + } + + // 格式化数据 + $formattedList = []; + foreach ($channels as $channel) { + $channelId = $channel['id']; + $stats = $withdrawalStats[$channelId] ?? [ + 'totalRevenue' => 0, + 'withdrawn' => 0, + 'pendingReview' => 0 + ]; + + // 可提现金额:渠道的withdrawableAmount + $withdrawableAmount = intval($channel['withdrawableAmount'] ?? 0); + + $formattedItem = [ + 'channelId' => (string)$channelId, + 'channelName' => $channel['name'] ?? '', + 'channelCode' => $channel['code'] ?? '', + 'totalRevenue' => round($stats['totalRevenue'] / 100, 2), // 总收益(元) + 'withdrawable' => round($withdrawableAmount / 100, 2), // 可提现(元) + 'withdrawn' => round($stats['withdrawn'] / 100, 2), // 已提现(元) + 'pendingReview' => round($stats['pendingReview'] / 100, 2), // 待审核(元) + ]; + $formattedList[] = $formattedItem; + } + + // 返回结果 + return json([ + 'code' => 200, + 'success' => true, + 'msg' => '获取成功', + 'data' => [ + 'list' => $formattedList, + 'total' => (int)$total + ] + ]); + + } catch (Exception $e) { + return json([ + 'code' => $e->getCode() ?: 500, + 'success' => false, + 'msg' => '获取收益明细失败:' . $e->getMessage(), + 'data' => null + ]); + } + } + + /** + * 生成渠道二维码(H5或小程序码) + * @return \think\response\Json + */ + public function generateQrCode() + { + try { + // 获取参数 + $type = $this->request->param('type', 'h5'); // h5 或 miniprogram + + $companyId = $this->getUserInfo('companyId'); + $userId = $this->getUserInfo('id'); + + // 参数验证 + if (!in_array($type, ['h5', 'miniprogram'])) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '类型参数错误,必须为 h5 或 miniprogram', + 'data' => null + ]); + } + + // 生成临时token(包含公司ID和用户ID,有效期24小时) + // 用户扫码后需要自己填写所有信息 + $tokenData = [ + 'companyId' => $companyId, + 'userId' => $userId, + 'expireTime' => time() + 86400 // 24小时后过期 + ]; + $token = base64_encode(json_encode($tokenData)); + + // 如果是小程序码,提前计算scene并存储映射关系到数据库 + if ($type === 'miniprogram') { + $scene = substr(md5($token), 0, 32); + + // 使用数据库存储映射关系(更可靠) + try { + Db::name('distribution_channel_scene_token')->insert([ + 'scene' => $scene, + 'token' => $token, + 'companyId' => $companyId, + 'expireTime' => time() + 86400, + 'createTime' => time() + ]); + } catch (\Exception $e) { + // 如果表不存在,尝试创建表 + $this->createSceneTokenTable(); + // 重试一次 + try { + Db::name('distribution_channel_scene_token')->insert([ + 'scene' => $scene, + 'token' => $token, + 'companyId' => $companyId, + 'expireTime' => time() + 86400, + 'createTime' => time() + ]); + } catch (\Exception $e2) { + // 静默失败,不影响主流程 + } + } + + // 同时存储到缓存(双重保险) + $sceneCacheKey = 'channel_register_scene_' . $scene; + Cache::set($sceneCacheKey, $token, 86400); + } + + if ($type === 'h5') { + // 生成H5二维码 + // 获取H5页面URL(需要根据实际项目配置) + $h5BaseUrl = Env::get('rpc.H5_FORM_URL', 'https://h5.ckb.quwanzhi.com/#'); + // 确保URL格式正确(去除末尾斜杠) + $h5BaseUrl = rtrim($h5BaseUrl, '/'); + $h5Url = $h5BaseUrl . '/pages/channel/add?token=' . urlencode($token); + + // 生成二维码 + $qrCode = new QrCode($h5Url); + $qrCode->setSize(300); + $qrCode->setMargin(10); + $qrCode->setWriterByName('png'); + $qrCode->setEncoding('UTF-8'); + $qrCode->setErrorCorrectionLevel(ErrorCorrectionLevel::HIGH); + + // 转换为base64 + $qrCodeBase64 = 'data:image/png;base64,' . base64_encode($qrCode->writeString()); + + return json([ + 'code' => 200, + 'success' => true, + 'msg' => '生成H5二维码成功', + 'data' => [ + 'type' => 'h5', + 'qrCode' => $qrCodeBase64, + 'url' => $h5Url + ] + ]); + + } else { + // 生成小程序码 + try { + // 从环境变量获取小程序配置 + $miniProgramConfig = [ + 'app_id' => Env::get('weChat.appidMiniApp', 'wx789850448e26c91d'), + 'secret' => Env::get('weChat.secretMiniApp', 'd18f75b3a3623cb40da05648b08365a1'), + 'response_type' => 'array' + ]; + + $app = Factory::miniProgram($miniProgramConfig); + + // scene参数长度限制为32位,使用token的MD5值 + $scene = substr(md5($token), 0, 32); + + // 再次确保映射关系已存储到数据库和缓存(双重保险) + $sceneCacheKey = 'channel_register_scene_' . $scene; + Cache::set($sceneCacheKey, $token, 86400); + + + // 调用接口生成小程序码 + // 注意:page 必须是小程序里已经存在且发布过的页面路径 + // 根据你的前端约定,改为和 H5 一致的添加渠道页面 + $response = $app->app_code->getUnlimit($scene, [ + 'page' => 'pages/channel/add', // 请确保小程序里存在该页面 + 'width' => 430, // 二维码的宽度 + ]); + + // 成功时返回的是 StreamResponse,失败时通常返回数组(包含 errcode/errmsg) + if ($response instanceof \EasyWeChat\Kernel\Http\StreamResponse) { + $img = $response->getBody()->getContents(); + $imgBase64 = 'data:image/png;base64,' . base64_encode($img); + + return json([ + 'code' => 200, + 'success' => true, + 'msg' => '生成小程序码成功', + 'data' => [ + 'type' => 'miniprogram', + 'qrCode' => $imgBase64, + 'scene' => $scene, // 返回scene供小程序端使用 + 'token' => $token // 返回token,小程序端可以通过scene查询 + ] + ]); + } + + // 如果不是流响应,而是数组(错误信息),则解析错误返回 + if (is_array($response) && isset($response['errcode']) && $response['errcode'] != 0) { + $errMsg = isset($response['errmsg']) ? $response['errmsg'] : '微信接口返回错误'; + return json([ + 'code' => 500, + 'success' => false, + 'msg' => '生成小程序码失败:' . $errMsg, + 'data' => $response + ]); + } + + // 其他未知格式 + return json([ + 'code' => 500, + 'success' => false, + 'msg' => '生成小程序码失败:响应格式错误', + 'data' => $response + ]); + } catch (\Exception $e) { + return json([ + 'code' => 500, + 'success' => false, + 'msg' => '生成小程序码失败:' . $e->getMessage(), + 'data' => null + ]); + } + } + + } catch (Exception $e) { + return json([ + 'code' => $e->getCode() ?: 500, + 'success' => false, + 'msg' => '生成二维码失败:' . $e->getMessage(), + 'data' => null + ]); + } + } + + /** + * 生成渠道登录二维码(H5或小程序码) + * 通用登录二维码,不绑定特定渠道,用户扫码后输入手机号和密码登录 + * @return \think\response\Json + */ + public function generateLoginQrCode() + { + try { + // 获取参数 + $type = $this->request->param('type', 'h5'); // h5 或 miniprogram + + $companyId = $this->getUserInfo('companyId'); + + // 参数验证 + if (!in_array($type, ['h5', 'miniprogram'])) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '类型参数错误,必须为 h5 或 miniprogram', + 'data' => null + ]); + } + + if ($type === 'h5') { + // 生成H5登录二维码 + $h5BaseUrl = Env::get('rpc.H5_FORM_URL', 'https://h5.ckb.quwanzhi.com/#'); + $h5BaseUrl = rtrim($h5BaseUrl, '/'); + // H5登录页面路径,需要根据实际项目调整 + // 登录入口只需要携带 companyId 参数 + $h5Url = $h5BaseUrl . '/pages/channel/login?companyId=' . urlencode($companyId); + + // 生成二维码 + $qrCode = new QrCode($h5Url); + $qrCode->setSize(300); + $qrCode->setMargin(10); + $qrCode->setWriterByName('png'); + $qrCode->setEncoding('UTF-8'); + $qrCode->setErrorCorrectionLevel(ErrorCorrectionLevel::HIGH); + + // 转换为base64 + $qrCodeBase64 = 'data:image/png;base64,' . base64_encode($qrCode->writeString()); + + return json([ + 'code' => 200, + 'success' => true, + 'msg' => '生成H5登录二维码成功', + 'data' => [ + 'type' => 'h5', + 'qrCode' => $qrCodeBase64, + 'url' => $h5Url + ] + ]); + + } else { + // 生成小程序登录码 + try { + // 从环境变量获取小程序配置 + $miniProgramConfig = [ + 'app_id' => Env::get('weChat.appidMiniApp', 'wx789850448e26c91d'), + 'secret' => Env::get('weChat.secretMiniApp', 'd18f75b3a3623cb40da05648b08365a1'), + 'response_type' => 'array' + ]; + + $app = Factory::miniProgram($miniProgramConfig); + + // scene 参数直接使用 companyId(字符串),并确保长度不超过32 + $scene = (string)$companyId; + if (strlen($scene) > 32) { + $scene = substr($scene, 0, 32); + } + + // 调用接口生成小程序码 + // 小程序登录页面路径,需要根据实际项目调整 + $response = $app->app_code->getUnlimit($scene, [ + 'page' => 'pages/channel/login', // 请确保小程序里存在该页面 + 'width' => 430, + ]); + + // 成功时返回的是 StreamResponse + if ($response instanceof \EasyWeChat\Kernel\Http\StreamResponse) { + $img = $response->getBody()->getContents(); + $imgBase64 = 'data:image/png;base64,' . base64_encode($img); + + return json([ + 'code' => 200, + 'success' => true, + 'msg' => '生成小程序登录码成功', + 'data' => [ + 'type' => 'miniprogram', + 'qrCode' => $imgBase64, + 'scene' => $scene + ] + ]); + } + + // 如果不是流响应,而是数组(错误信息),则解析错误返回 + if (is_array($response) && isset($response['errcode']) && $response['errcode'] != 0) { + $errMsg = isset($response['errmsg']) ? $response['errmsg'] : '微信接口返回错误'; + return json([ + 'code' => 500, + 'success' => false, + 'msg' => '生成小程序登录码失败:' . $errMsg, + 'data' => $response + ]); + } + + // 其他未知格式 + return json([ + 'code' => 500, + 'success' => false, + 'msg' => '生成小程序登录码失败:响应格式错误', + 'data' => $response + ]); + } catch (\Exception $e) { + return json([ + 'code' => 500, + 'success' => false, + 'msg' => '生成小程序登录码失败:' . $e->getMessage(), + 'data' => null + ]); + } + } + + } catch (Exception $e) { + return json([ + 'code' => $e->getCode() ?: 500, + 'success' => false, + 'msg' => '生成登录二维码失败:' . $e->getMessage(), + 'data' => null + ]); + } + } + + /** + * 扫码提交渠道信息(H5和小程序共用) + * GET请求:返回预填信息 + * POST请求:提交渠道信息 + * @return \think\response\Json + */ + public function registerByQrCode() + { + try { + // 获取参数 + $token = $this->request->param('token', ''); + + // 参数验证 + if (empty($token)) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => 'token不能为空', + 'data' => null + ]); + } + + // 判断传入的是scene(32位MD5)还是token(base64编码) + // 如果是32位MD5字符串,则从数据库或缓存中查找对应的token + if (strlen($token) == 32 && preg_match('/^[a-f0-9]{32}$/i', $token)) { + // 这是scene,先从数据库查找对应的token + $sceneData = Db::name('distribution_channel_scene_token') + ->where('scene', $token) + ->where('expireTime', '>', time()) + ->find(); + + if ($sceneData && !empty($sceneData['token'])) { + $realToken = $sceneData['token']; + } else { + // 如果数据库中没有,尝试从缓存获取 + $sceneCacheKey = 'channel_register_scene_' . $token; + $realToken = Cache::get($sceneCacheKey); + + if (empty($realToken)) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '二维码已过期,请重新生成', + 'data' => null + ]); + } + } + + $token = $realToken; + } + + // 解析token + $tokenData = json_decode(base64_decode($token), true); + if (!$tokenData || !isset($tokenData['companyId'])) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => 'token无效', + 'data' => null + ]); + } + + // 检查token是否过期 + if (isset($tokenData['expireTime']) && $tokenData['expireTime'] < time()) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '二维码已过期,请重新生成', + 'data' => null + ]); + } + + $companyId = $tokenData['companyId']; + $userId = isset($tokenData['userId']) ? $tokenData['userId'] : 0; // 兼容旧token,如果没有userId则默认为0 + + // GET请求:返回token验证成功信息(前端可以显示表单) + if ($this->request->isGet()) { + return json([ + 'code' => 200, + 'success' => true, + 'msg' => 'token验证成功', + 'data' => [ + 'valid' => true + ] + ]); + } + + // POST请求:提交渠道信息(所有信息都需要用户填写) + $name = $this->request->param('name', ''); + $phone = $this->request->param('phone', ''); + $wechatId = $this->request->param('wechatId', ''); + $remarks = $this->request->param('remarks', ''); + + // 参数验证 + if (empty($name)) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '渠道名称不能为空', + 'data' => null + ]); + } + + // 验证渠道名称长度 + if (mb_strlen($name) > 50) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '渠道名称长度不能超过50个字符', + 'data' => null + ]); + } + + // 验证手机号格式(如果提供) + if (!empty($phone)) { + if (!preg_match('/^1[3-9]\d{9}$/', $phone)) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '手机号格式不正确,请输入11位数字且以1开头', + 'data' => null + ]); + } + + // 检查手机号是否已存在(排除已删除的渠道) + $existChannel = Db::name('distribution_channel') + ->where([ + ['companyId', '=', $companyId], + ['phone', '=', $phone], + ['deleteTime', '=', 0] + ]) + ->find(); + + if ($existChannel) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '手机号已被使用', + 'data' => null + ]); + } + } + + // 验证微信号长度(如果提供) + if (!empty($wechatId) && mb_strlen($wechatId) > 50) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '微信号长度不能超过50个字符', + 'data' => null + ]); + } + + // 验证备注长度(如果提供) + if (!empty($remarks) && mb_strlen($remarks) > 200) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '备注信息长度不能超过200个字符', + 'data' => null + ]); + } + + // 生成渠道编码 + $code = DistributionChannel::generateChannelCode(); + + // 准备插入数据(从token中获取userId,记录是哪个用户生成的二维码) + $data = [ + 'companyId' => $companyId, + 'userId' => $userId, // 从token中获取userId,记录生成二维码的用户 + 'name' => $name, + 'code' => $code, + 'phone' => $phone ?: '', + 'password' => md5('123456'), // 默认密码123456,MD5加密 + 'wechatId' => $wechatId ?: '', + 'remarks' => $remarks ?: '', + 'createType' => DistributionChannel::CREATE_TYPE_AUTO, // 扫码创建 + 'status' => DistributionChannel::STATUS_ENABLED, + 'totalCustomers' => 0, + 'todayCustomers' => 0, + 'totalFriends' => 0, + 'todayFriends' => 0, + 'withdrawableAmount' => 0, // 以分为单位存储 + 'createTime' => time(), + 'updateTime' => time(), + ]; + + // 插入数据库 + $channelId = Db::name('distribution_channel')->insertGetId($data); + + if (!$channelId) { + return json([ + 'code' => 500, + 'success' => false, + 'msg' => '创建渠道失败', + 'data' => null + ]); + } + + // 获取创建的数据 + $channel = Db::name('distribution_channel')->where('id', $channelId)->find(); + + // 格式化返回数据 + $result = [ + 'id' => (string)$channel['id'], + 'name' => $channel['name'], + 'code' => $channel['code'], + 'phone' => $channel['phone'] ?: '', + 'wechatId' => $channel['wechatId'] ?: '', + 'companyId' => (int)$companyId, // 返回companyId,方便小程序自动跳转 + 'userId' => (int)($channel['userId'] ?? 0), + 'createType' => $channel['createType'], + 'status' => $channel['status'], + 'totalCustomers' => (int)$channel['totalCustomers'], + 'todayCustomers' => (int)$channel['todayCustomers'], + 'totalFriends' => (int)$channel['totalFriends'], + 'todayFriends' => (int)$channel['todayFriends'], + 'withdrawableAmount' => round(($channel['withdrawableAmount'] ?? 0) / 100, 2), // 分转元,保留2位小数 + 'createTime' => !empty($channel['createTime']) ? date('Y-m-d H:i:s', $channel['createTime']) : date('Y-m-d H:i:s'), + ]; + + return json([ + 'code' => 200, + 'success' => true, + 'msg' => '渠道注册成功', + 'data' => $result + ]); + + } catch (Exception $e) { + return json([ + 'code' => $e->getCode() ?: 500, + 'success' => false, + 'msg' => '渠道注册失败:' . $e->getMessage(), + 'data' => null + ]); + } + } + + /** + * 创建scene和token映射表(如果不存在) + */ + protected function createSceneTokenTable() + { + try { + $sql = "CREATE TABLE IF NOT EXISTS `ck_distribution_channel_scene_token` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `scene` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '小程序scene参数(MD5值)', + `token` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '原始token(base64编码)', + `companyId` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '公司ID', + `expireTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '过期时间戳', + `createTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_scene` (`scene`) USING BTREE, + INDEX `idx_companyId` (`companyId`) USING BTREE, + INDEX `idx_expireTime` (`expireTime`) USING BTREE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分销渠道小程序码scene和token映射表';"; + + Db::execute($sql); + } catch (\Exception $e) { + // 静默失败 + } + } +} + diff --git a/application/cunkebao/controller/distribution/ChannelUserController.php b/application/cunkebao/controller/distribution/ChannelUserController.php new file mode 100644 index 0000000..4c27dc0 --- /dev/null +++ b/application/cunkebao/controller/distribution/ChannelUserController.php @@ -0,0 +1,722 @@ +request->method(true) == 'OPTIONS') { + $origin = $this->request->header('origin', '*'); + header("Access-Control-Allow-Origin: " . $origin); + header("Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization, Cookie"); + header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, PATCH'); + header("Access-Control-Allow-Credentials: true"); + header("Access-Control-Max-Age: 86400"); + exit; + } + } + + /** + * 设置跨域响应头 + * @param \think\response\Json $response + * @return \think\response\Json + */ + protected function setCorsHeaders($response) + { + $origin = $this->request->header('origin', '*'); + $response->header([ + 'Access-Control-Allow-Origin' => $origin, + 'Access-Control-Allow-Headers' => 'Origin, X-Requested-With, Content-Type, Accept, Authorization, Cookie', + 'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS, PATCH', + 'Access-Control-Allow-Credentials' => 'true', + 'Access-Control-Max-Age' => '86400', + ]); + return $response; + } + + /** + * 渠道登录 + * @return \think\response\Json + */ + public function login() + { + try { + // 获取参数 + $phone = $this->request->param('phone', ''); + $password = $this->request->param('password', ''); + $companyId = $this->request->param('companyId', 0); + // 参数验证 + if (empty($phone)) { + return $this->setCorsHeaders(json([ + 'code' => 400, + 'success' => false, + 'msg' => '手机号不能为空', + 'data' => null + ])); + } + + if (empty($password)) { + return $this->setCorsHeaders(json([ + 'code' => 400, + 'success' => false, + 'msg' => '密码不能为空', + 'data' => null + ])); + } + + // 查询渠道信息(通过手机号) + $channel = Db::name('distribution_channel') + ->where([ + ['phone', '=', $phone], + ['companyId', '=', $companyId], + ['deleteTime', '=', 0] + ]) + ->find(); + + + if (!$channel) { + return $this->setCorsHeaders(json([ + 'code' => 404, + 'success' => false, + 'msg' => '渠道不存在', + 'data' => null + ])); + } + + // 检查渠道状态 + if ($channel['status'] !== DistributionChannel::STATUS_ENABLED) { + return $this->setCorsHeaders(json([ + 'code' => 403, + 'success' => false, + 'msg' => '渠道已被禁用', + 'data' => null + ])); + } + + // 验证密码(MD5加密) + $passwordMd5 = md5($password); + if ($channel['password'] !== $passwordMd5) { + return $this->setCorsHeaders(json([ + 'code' => 401, + 'success' => false, + 'msg' => '密码错误', + 'data' => null + ])); + } + + // 准备token载荷(不包含密码) + $payload = [ + 'id' => $channel['id'], + 'channelId' => $channel['id'], + 'channelCode' => $channel['code'], + 'channelName' => $channel['name'], + 'companyId' => $channel['companyId'], + 'type' => 'channel', // 标识这是渠道登录 + ]; + + // 生成JWT令牌(30天有效期) + $expire = 86400 * 30; + $token = JwtUtil::createToken($payload, $expire); + $tokenExpired = time() + $expire; + + // 更新最后登录时间(可选) + Db::name('distribution_channel') + ->where('id', $channel['id']) + ->update([ + 'updateTime' => time() + ]); + + // 返回数据(不包含密码) + $data = [ + 'token' => $token, + 'tokenExpired' => $tokenExpired, + 'channelInfo' => [ + 'id' => (string)$channel['id'], + 'channelCode' => $channel['code'], + 'channelName' => $channel['name'], + 'phone' => $channel['phone'] ?: '', + 'wechatId' => $channel['wechatId'] ?: '', + 'companyId' => (int)$channel['companyId'], // 返回companyId,方便小程序自动跳转 + 'status' => $channel['status'], + 'totalCustomers' => (int)$channel['totalCustomers'], + 'todayCustomers' => (int)$channel['todayCustomers'], + 'totalFriends' => (int)$channel['totalFriends'], + 'todayFriends' => (int)$channel['todayFriends'], + 'withdrawableAmount' => round(($channel['withdrawableAmount'] ?? 0) / 100, 2), // 分转元 + ] + ]; + + return $this->setCorsHeaders(json([ + 'code' => 200, + 'success' => true, + 'msg' => '登录成功', + 'data' => $data + ])); + + } catch (Exception $e) { + return $this->setCorsHeaders(json([ + 'code' => $e->getCode() ?: 500, + 'success' => false, + 'msg' => '登录失败:' . $e->getMessage(), + 'data' => null + ])); + } + } + + /** + * 获取渠道首页数据 + * @return \think\response\Json + */ + public function index() + { + try { + // 获取参数 + $channelCode = $this->request->param('channelCode', ''); + + // 参数验证 + if (empty($channelCode)) { + return $this->setCorsHeaders(json([ + 'code' => 400, + 'success' => false, + 'msg' => '渠道编码不能为空', + 'data' => null + ])); + } + + // 查询渠道信息 + $channel = Db::name('distribution_channel') + ->where([ + ['code', '=', $channelCode], + ['status', '=', DistributionChannel::STATUS_ENABLED], + ['deleteTime', '=', 0] + ]) + ->find(); + + if (!$channel) { + return $this->setCorsHeaders(json([ + 'code' => 404, + 'success' => false, + 'msg' => '渠道不存在或已被禁用', + 'data' => null + ])); + } + + $channelId = $channel['id']; + $companyId = $channel['companyId']; + + // 1. 渠道基本信息 + $channelInfo = [ + 'channelName' => $channel['name'] ?? '', + 'channelCode' => $channel['code'] ?? '', + 'phone' => $channel['phone'] ?? '', + 'wechatId' => $channel['wechatId'] ?? '', + 'remark' => $channel['remark'] ?? '', + 'createTime' => !empty($channel['createTime']) ? date('Y-m-d H:i:s', $channel['createTime']) : '', + 'createType' => $channel['createType'] ?? '', + ]; + + // 2. 财务统计 + // 当前可提现金额 + $withdrawableAmount = round(($channel['withdrawableAmount'] ?? 0) / 100, 2); // 分转元 + + // 已提现金额(已打款的提现申请) + $withdrawnAmount = Db::name('distribution_withdrawal') + ->where([ + ['companyId', '=', $companyId], + ['channelId', '=', $channelId], + ['status', '=', DistributionWithdrawal::STATUS_PAID] + ]) + ->sum('amount'); + $withdrawnAmount = round(($withdrawnAmount ?? 0) / 100, 2); // 分转元 + + // 待审核金额(待审核的提现申请) + $pendingReviewAmount = Db::name('distribution_withdrawal') + ->where([ + ['companyId', '=', $companyId], + ['channelId', '=', $channelId], + ['status', '=', DistributionWithdrawal::STATUS_PENDING] + ]) + ->sum('amount'); + $pendingReviewAmount = round(($pendingReviewAmount ?? 0) / 100, 2); // 分转元 + + // 总收益(所有收益记录的总和) + $totalRevenue = Db::name('distribution_revenue_record') + ->where([ + ['companyId', '=', $companyId], + ['channelId', '=', $channelId] + ]) + ->sum('amount'); + $totalRevenue = round(($totalRevenue ?? 0) / 100, 2); // 分转元 + + $financialStats = [ + 'withdrawableAmount' => $withdrawableAmount, // 当前可提现金额 + 'totalRevenue' => $totalRevenue, // 总收益 + 'pendingReview' => $pendingReviewAmount, // 待审核 + 'withdrawn' => $withdrawnAmount, // 已提现 + ]; + + // 3. 客户和好友统计 + $customerStats = [ + 'totalFriends' => (int)($channel['totalFriends'] ?? 0), // 总加好友数 + 'todayFriends' => (int)($channel['todayFriends'] ?? 0), // 今日加好友数 + 'totalCustomers' => (int)($channel['totalCustomers'] ?? 0), // 总获客数 + 'todayCustomers' => (int)($channel['todayCustomers'] ?? 0), // 今日获客数 + ]; + + // 返回数据 + $data = [ + 'channelInfo' => $channelInfo, + 'financialStats' => $financialStats, + 'customerStats' => $customerStats, + ]; + + return $this->setCorsHeaders(json([ + 'code' => 200, + 'success' => true, + 'msg' => '获取成功', + 'data' => $data + ])); + + } catch (Exception $e) { + return $this->setCorsHeaders(json([ + 'code' => $e->getCode() ?: 500, + 'success' => false, + 'msg' => '获取数据失败:' . $e->getMessage(), + 'data' => null + ])); + } + } + + /** + * 获取收益明细列表 + * @return \think\response\Json + */ + public function revenueRecords() + { + try { + // 获取参数 + $channelCode = $this->request->param('channelCode', ''); + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $type = $this->request->param('type', 'all'); // all, customer_acquisition, add_friend, order, poster, phone, other + $date = $this->request->param('date', ''); // 日期筛选,格式:Y-m-d + + // 参数验证 + if (empty($channelCode)) { + return $this->setCorsHeaders(json([ + 'code' => 400, + 'success' => false, + 'msg' => '渠道编码不能为空', + 'data' => null + ])); + } + + $page = max(1, intval($page)); + $limit = max(1, min(100, intval($limit))); + + // 查询渠道信息 + $channel = Db::name('distribution_channel') + ->where([ + ['code', '=', $channelCode], + ['status', '=', DistributionChannel::STATUS_ENABLED], + ['deleteTime', '=', 0] + ]) + ->find(); + + if (!$channel) { + return $this->setCorsHeaders(json([ + 'code' => 404, + 'success' => false, + 'msg' => '渠道不存在或已被禁用', + 'data' => null + ])); + } + + $channelId = $channel['id']; + $companyId = $channel['companyId']; + + // 构建查询条件 + $where = [ + ['companyId', '=', $companyId], + ['channelId', '=', $channelId] + ]; + + // 类型筛选 + if ($type !== 'all') { + $where[] = ['type', '=', $type]; + } + + // 日期筛选 + if (!empty($date)) { + $dateStart = strtotime($date . ' 00:00:00'); + $dateEnd = strtotime($date . ' 23:59:59'); + if ($dateStart && $dateEnd) { + $where[] = ['createTime', 'between', [$dateStart, $dateEnd]]; + } + } + + // 查询总数 + $total = Db::name('distribution_revenue_record') + ->where($where) + ->count(); + + // 查询列表(按创建时间倒序) + $list = Db::name('distribution_revenue_record') + ->where($where) + ->order('createTime DESC') + ->page($page, $limit) + ->select(); + + // 从活动表(customer_acquisition_task)获取类型标签映射(使用 sourceId 关联活动ID) + $formattedList = []; + if (!empty($list)) { + // 收集本页涉及到的活动ID + $taskIds = []; + foreach ($list as $row) { + if (!empty($row['sourceId'])) { + $taskIds[] = (int)$row['sourceId']; + } + } + $taskIds = array_values(array_unique($taskIds)); + + // 获取活动名称映射:taskId => name + $taskNameMap = []; + if (!empty($taskIds)) { + $taskNameMap = Db::name('customer_acquisition_task') + ->whereIn('id', $taskIds) + ->column('name', 'id'); + } + + // 格式化数据 + foreach ($list as $item) { + $taskId = !empty($item['sourceId']) ? (int)$item['sourceId'] : 0; + $taskName = $taskId && isset($taskNameMap[$taskId]) ? $taskNameMap[$taskId] : null; + + $formattedItem = [ + 'id' => (string)$item['id'], + 'sourceType' => $item['sourceType'] ?? '其他', + 'type' => $item['type'] ?? 'other', + // 类型标签优先取活动名称,没有则回退为 sourceType 或 “其他” + 'typeLabel' => $taskName ?: (!empty($item['sourceType']) ? $item['sourceType'] : '其他'), + 'amount' => round($item['amount'] / 100, 2), // 分转元 + 'remark' => isset($item['remark']) && $item['remark'] !== '' ? $item['remark'] : null, + 'createTime' => !empty($item['createTime']) ? date('Y-m-d H:i', $item['createTime']) : '', + ]; + $formattedList[] = $formattedItem; + } + } + + return $this->setCorsHeaders(json([ + 'code' => 200, + 'success' => true, + 'msg' => '获取成功', + 'data' => [ + 'list' => $formattedList, + 'total' => (int)$total, + 'page' => $page, + 'limit' => $limit + ] + ])); + + } catch (Exception $e) { + return $this->setCorsHeaders(json([ + 'code' => $e->getCode() ?: 500, + 'success' => false, + 'msg' => '获取收益明细失败:' . $e->getMessage(), + 'data' => null + ])); + } + } + + /** + * 获取提现明细列表 + * @return \think\response\Json + */ + public function withdrawalRecords() + { + try { + // 获取参数 + $channelCode = $this->request->param('channelCode', ''); + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $status = $this->request->param('status', 'all'); // all, pending, approved, rejected, paid + $payType = $this->request->param('payType', 'all'); // all, wechat, alipay, bankcard + $date = $this->request->param('date', ''); // 日期筛选,格式:Y-m-d + + // 参数验证 + if (empty($channelCode)) { + return $this->setCorsHeaders(json([ + 'code' => 400, + 'success' => false, + 'msg' => '渠道编码不能为空', + 'data' => null + ])); + } + + $page = max(1, intval($page)); + $limit = max(1, min(100, intval($limit))); + + // 校验到账方式参数 + $validPayTypes = ['all', 'wechat', 'alipay', 'bankcard']; + if (!in_array($payType, $validPayTypes)) { + $payType = 'all'; + } + + // 查询渠道信息 + $channel = Db::name('distribution_channel') + ->where([ + ['code', '=', $channelCode], + ['status', '=', DistributionChannel::STATUS_ENABLED], + ['deleteTime', '=', 0] + ]) + ->find(); + + if (!$channel) { + return $this->setCorsHeaders(json([ + 'code' => 404, + 'success' => false, + 'msg' => '渠道不存在或已被禁用', + 'data' => null + ])); + } + + $channelId = $channel['id']; + $companyId = $channel['companyId']; + + // 构建查询条件 + $where = [ + ['companyId', '=', $companyId], + ['channelId', '=', $channelId] + ]; + + // 状态筛选 + if ($status !== 'all') { + $where[] = ['status', '=', $status]; + } + + // 到账方式筛选 + if ($payType !== 'all') { + $where[] = ['payType', '=', $payType]; + } + + // 日期筛选 + if (!empty($date)) { + $dateStart = strtotime($date . ' 00:00:00'); + $dateEnd = strtotime($date . ' 23:59:59'); + if ($dateStart && $dateEnd) { + $where[] = ['applyTime', 'between', [$dateStart, $dateEnd]]; + } + } + + // 查询总数 + $total = Db::name('distribution_withdrawal') + ->where($where) + ->count(); + + // 查询列表(按申请时间倒序) + $list = Db::name('distribution_withdrawal') + ->where($where) + ->order('applyTime DESC') + ->page($page, $limit) + ->select(); + + // 格式化数据 + $formattedList = []; + foreach ($list as $item) { + // 状态标签映射 + $statusLabels = [ + 'pending' => '待审核', + 'approved' => '已通过', + 'rejected' => '已拒绝', + 'paid' => '已打款' + ]; + + // 支付类型标签映射 + $payTypeLabels = [ + 'wechat' => '微信', + 'alipay' => '支付宝', + 'bankcard' => '银行卡' + ]; + + $payType = !empty($item['payType']) ? $item['payType'] : null; + + $formattedItem = [ + 'id' => (string)$item['id'], + 'amount' => round($item['amount'] / 100, 2), // 分转元 + 'status' => $item['status'] ?? 'pending', + 'statusLabel' => $statusLabels[$item['status'] ?? 'pending'] ?? '待审核', + 'payType' => $payType, + 'payTypeLabel' => $payType && isset($payTypeLabels[$payType]) ? $payTypeLabels[$payType] : null, + 'applyTime' => !empty($item['applyTime']) ? date('Y-m-d H:i', $item['applyTime']) : '', + 'reviewTime' => !empty($item['reviewTime']) ? date('Y-m-d H:i', $item['reviewTime']) : null, + 'reviewer' => !empty($item['reviewer']) ? $item['reviewer'] : null, + 'remark' => !empty($item['remark']) ? $item['remark'] : null, + ]; + $formattedList[] = $formattedItem; + } + + return $this->setCorsHeaders(json([ + 'code' => 200, + 'success' => true, + 'msg' => '获取成功', + 'data' => [ + 'list' => $formattedList, + 'total' => (int)$total, + 'page' => $page, + 'limit' => $limit + ] + ])); + + } catch (Exception $e) { + return $this->setCorsHeaders(json([ + 'code' => $e->getCode() ?: 500, + 'success' => false, + 'msg' => '获取提现明细失败:' . $e->getMessage(), + 'data' => null + ])); + } + } + + /** + * 修改渠道分销员密码 + * @return \think\response\Json + */ + public function changePassword() + { + try { + // 获取参数并去除首尾空格 + $channelCode = trim($this->request->param('channelCode', '')); + $oldPassword = trim($this->request->param('oldPassword', '')); + $newPassword = trim($this->request->param('newPassword', '')); + + // 参数验证 + if (empty($channelCode)) { + return $this->setCorsHeaders(json([ + 'code' => 400, + 'success' => false, + 'msg' => '渠道编码不能为空', + 'data' => null + ])); + } + + if (empty($oldPassword)) { + return $this->setCorsHeaders(json([ + 'code' => 400, + 'success' => false, + 'msg' => '原密码不能为空', + 'data' => null + ])); + } + + if (empty($newPassword)) { + return $this->setCorsHeaders(json([ + 'code' => 400, + 'success' => false, + 'msg' => '新密码不能为空', + 'data' => null + ])); + } + + // 验证新密码长度(至少6位) + if (mb_strlen($newPassword) < 6) { + return $this->setCorsHeaders(json([ + 'code' => 400, + 'success' => false, + 'msg' => '新密码长度至少为6位', + 'data' => null + ])); + } + + // 查询渠道信息 + $channel = Db::name('distribution_channel') + ->where([ + ['code', '=', $channelCode], + ['status', '=', DistributionChannel::STATUS_ENABLED], + ['deleteTime', '=', 0] + ]) + ->find(); + + if (!$channel) { + return $this->setCorsHeaders(json([ + 'code' => 404, + 'success' => false, + 'msg' => '渠道不存在或已被禁用', + 'data' => null + ])); + } + + // 验证原密码(MD5加密) + $oldPasswordMd5 = md5($oldPassword); + if ($channel['password'] !== $oldPasswordMd5) { + return $this->setCorsHeaders(json([ + 'code' => 401, + 'success' => false, + 'msg' => '原密码错误', + 'data' => null + ])); + } + + // 检查新密码是否与原密码相同 + $newPasswordMd5 = md5($newPassword); + if ($channel['password'] === $newPasswordMd5) { + return $this->setCorsHeaders(json([ + 'code' => 400, + 'success' => false, + 'msg' => '新密码不能与原密码相同', + 'data' => null + ])); + } + + // 更新密码 + $updateResult = Db::name('distribution_channel') + ->where('id', $channel['id']) + ->update([ + 'password' => $newPasswordMd5, + 'updateTime' => time() + ]); + + if ($updateResult === false) { + return $this->setCorsHeaders(json([ + 'code' => 500, + 'success' => false, + 'msg' => '密码修改失败,请稍后重试', + 'data' => null + ])); + } + + return $this->setCorsHeaders(json([ + 'code' => 200, + 'success' => true, + 'msg' => '密码修改成功', + 'data' => null + ])); + + } catch (Exception $e) { + return $this->setCorsHeaders(json([ + 'code' => $e->getCode() ?: 500, + 'success' => false, + 'msg' => '密码修改失败:' . $e->getMessage(), + 'data' => null + ])); + } + } +} + diff --git a/application/cunkebao/controller/distribution/WithdrawalController.php b/application/cunkebao/controller/distribution/WithdrawalController.php new file mode 100644 index 0000000..4cdf7fb --- /dev/null +++ b/application/cunkebao/controller/distribution/WithdrawalController.php @@ -0,0 +1,692 @@ +request->param('page', 1); + $limit = $this->request->param('limit', 20); + $status = $this->request->param('status', 'all'); + $date = $this->request->param('date', ''); + $keyword = $this->request->param('keyword', ''); + + $companyId = $this->getUserInfo('companyId'); + + // 参数验证 + $page = max(1, intval($page)); + $limit = max(1, min(100, intval($limit))); // 限制最大100 + + // 验证状态参数 + $validStatuses = ['all', DistributionWithdrawal::STATUS_PENDING, DistributionWithdrawal::STATUS_APPROVED, DistributionWithdrawal::STATUS_REJECTED, DistributionWithdrawal::STATUS_PAID]; + if (!in_array($status, $validStatuses)) { + $status = 'all'; + } + + // 构建查询条件 + $where = []; + $where[] = ['w.companyId', '=', $companyId]; + + // 如果不是管理员,只能查看自己创建的提现申请 + if (!$this->getUserInfo('isAdmin')) { + $where[] = ['w.userId', '=', $this->getUserInfo('id')]; + } + + // 状态筛选 + if ($status !== 'all') { + $where[] = ['w.status', '=', $status]; + } + + // 日期筛选(格式:YYYY/MM/DD) + if (!empty($date)) { + // 转换日期格式 YYYY/MM/DD 为时间戳范围 + $dateParts = explode('/', $date); + if (count($dateParts) === 3) { + $dateStr = $dateParts[0] . '-' . $dateParts[1] . '-' . $dateParts[2]; + $dateStart = strtotime($dateStr . ' 00:00:00'); + $dateEnd = strtotime($dateStr . ' 23:59:59'); + if ($dateStart && $dateEnd) { + $where[] = ['w.applyTime', 'between', [$dateStart, $dateEnd]]; + } + } + } + + // 关键词搜索(模糊匹配渠道名称、渠道编码) + if (!empty($keyword)) { + $keyword = trim($keyword); + // 需要关联渠道表进行搜索 + } + + // 构建查询(关联渠道表获取渠道名称和编码,只关联未删除的渠道) + $query = Db::name('distribution_withdrawal') + ->alias('w') + ->join('distribution_channel c', 'w.channelId = c.id AND c.deleteTime = 0', 'left') + ->where($where); + + // 关键词搜索(如果有关键词,添加渠道表关联条件) + if (!empty($keyword)) { + $query->where(function ($query) use ($keyword) { + $query->where('c.name', 'like', '%' . $keyword . '%') + ->whereOr('c.code', 'like', '%' . $keyword . '%'); + }); + } + + // 查询总数 + $total = $query->count(); + + // 查询列表(按申请时间倒序) + $list = $query->field([ + 'w.id', + 'w.channelId', + 'w.userId', + 'w.amount', + 'w.status', + 'w.payType', + 'w.applyTime', + 'w.reviewTime', + 'w.reviewer', + 'w.remark', + 'c.name as channelName', + 'c.code as channelCode' + ]) + ->order('w.applyTime DESC') + ->page($page, $limit) + ->select(); + + // 格式化数据 + $formattedList = []; + foreach ($list as $item) { + // 格式化申请日期为 YYYY/MM/DD + $applyDate = ''; + if (!empty($item['applyTime'])) { + $applyDate = date('Y/m/d', $item['applyTime']); + } + + // 格式化审核日期 + $reviewDate = null; + if (!empty($item['reviewTime'])) { + $reviewDate = date('Y-m-d H:i:s', $item['reviewTime']); + } + + $formattedItem = [ + 'id' => (string)$item['id'], + 'channelId' => (string)$item['channelId'], + 'channelName' => $item['channelName'] ?? '', + 'channelCode' => $item['channelCode'] ?? '', + 'userId' => (int)($item['userId'] ?? 0), + 'amount' => round($item['amount'] / 100, 2), // 分转元,保留2位小数 + 'status' => $item['status'] ?? DistributionWithdrawal::STATUS_PENDING, + 'payType' => !empty($item['payType']) ? $item['payType'] : null, // 支付类型 + 'applyDate' => $applyDate, + 'reviewDate' => $reviewDate, + 'reviewer' => !empty($item['reviewer']) ? $item['reviewer'] : null, + 'remark' => !empty($item['remark']) ? $item['remark'] : null, + ]; + $formattedList[] = $formattedItem; + } + + // 返回结果 + return json([ + 'code' => 200, + 'success' => true, + 'msg' => '获取成功', + 'data' => [ + 'list' => $formattedList, + 'total' => (int)$total + ] + ]); + + } catch (Exception $e) { + return json([ + 'code' => $e->getCode() ?: 500, + 'success' => false, + 'msg' => '获取提现申请列表失败:' . $e->getMessage(), + 'data' => null + ]); + } + } + + /** + * 创建提现申请 + * @return \think\response\Json + */ + public function create() + { + try { + // 获取参数(接口接收的金额单位为元) + // 原先使用 channelId,现在改为使用渠道编码 channelCode + $channelCode = $this->request->param('channelCode', ''); + $amount = $this->request->param('amount', 0); // 金额单位:元 + + $companyId = $this->getUserInfo('companyId'); + + // 参数验证 + if (empty($channelCode)) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '渠道编码不能为空', + 'data' => null + ]); + } + + // 验证金额(转换为浮点数进行验证) + $amount = floatval($amount); + if (empty($amount) || $amount <= 0) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '提现金额必须大于0', + 'data' => null + ]); + } + + // 验证金额格式(最多2位小数) + if (!preg_match('/^\d+(\.\d{1,2})?$/', (string)$amount)) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '提现金额格式不正确,最多保留2位小数', + 'data' => null + ]); + } + + // 检查渠道是否存在且属于当前公司(通过渠道编码查询) + $channel = Db::name('distribution_channel') + ->where([ + ['code', '=', $channelCode], + ['companyId', '=', $companyId], + ['deleteTime', '=', 0] + ]) + ->find(); + + if (!$channel) { + return json([ + 'code' => 404, + 'success' => false, + 'msg' => '渠道不存在或没有权限', + 'data' => null + ]); + } + + // 统一使用渠道ID变量,后续逻辑仍然基于 channelId + $channelId = $channel['id']; + // 从渠道获取创建者的userId,而不是当前登录用户的userId + $userId = intval($channel['userId'] ?? 0); + + // 检查渠道状态 + if ($channel['status'] !== 'enabled') { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '渠道已禁用,无法申请提现', + 'data' => null + ]); + } + + // 检查可提现金额 + // 数据库存储的是分,接口接收的是元,需要统一单位进行比较 + $withdrawableAmountInFen = intval($channel['withdrawableAmount'] ?? 0); // 数据库中的分 + $withdrawableAmountInYuan = round($withdrawableAmountInFen / 100, 2); // 转换为元用于提示 + $amountInFen = intval(round($amount * 100)); // 将接口接收的元转换为分 + + if ($amountInFen > $withdrawableAmountInFen) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '提现金额不能超过可提现金额(' . number_format($withdrawableAmountInYuan, 2) . '元)', + 'data' => null + ]); + } + + // 检查是否有待审核的申请 + $pendingWithdrawal = Db::name('distribution_withdrawal') + ->where([ + ['channelId', '=', $channelId], + ['companyId', '=', $companyId], + ['status', '=', DistributionWithdrawal::STATUS_PENDING] + ]) + ->find(); + + if ($pendingWithdrawal) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '该渠道已有待审核的提现申请,请等待审核完成后再申请', + 'data' => null + ]); + } + + // 开始事务 + Db::startTrans(); + try { + // 创建提现申请(金额以分存储) + $withdrawalData = [ + 'companyId' => $companyId, + 'channelId' => $channelId, + 'userId' => $userId, + 'amount' => $amountInFen, // 存储为分 + 'status' => DistributionWithdrawal::STATUS_PENDING, + 'applyTime' => time(), + 'createTime' => time(), + 'updateTime' => time(), + ]; + + $withdrawalId = Db::name('distribution_withdrawal')->insertGetId($withdrawalData); + + if (!$withdrawalId) { + throw new Exception('创建提现申请失败'); + } + + // 扣除渠道可提现金额(以分为单位) + Db::name('distribution_channel') + ->where('id', $channelId) + ->setDec('withdrawableAmount', $amountInFen); + + // 提交事务 + Db::commit(); + + // 获取创建的申请数据 + $withdrawal = Db::name('distribution_withdrawal') + ->alias('w') + ->join('distribution_channel c', 'w.channelId = c.id', 'left') + ->where('w.id', $withdrawalId) + ->field([ + 'w.id', + 'w.channelId', + 'w.userId', + 'w.amount', + 'w.status', + 'w.payType', + 'w.applyTime', + 'c.name as channelName', + 'c.code as channelCode' + ]) + ->find(); + + // 格式化返回数据(分转元) + $result = [ + 'id' => (string)$withdrawal['id'], + 'channelId' => (string)$withdrawal['channelId'], + 'channelName' => $withdrawal['channelName'] ?? '', + 'channelCode' => $withdrawal['channelCode'] ?? '', + 'userId' => (int)($withdrawal['userId'] ?? 0), + 'amount' => round($withdrawal['amount'] / 100, 2), // 分转元,保留2位小数 + 'status' => $withdrawal['status'], + 'payType' => !empty($withdrawal['payType']) ? $withdrawal['payType'] : null, // 支付类型:wechat、alipay、bankcard(创建时为null) + 'applyDate' => !empty($withdrawal['applyTime']) ? date('Y/m/d', $withdrawal['applyTime']) : '', + 'reviewDate' => null, + 'reviewer' => null, + 'remark' => null, + ]; + + return json([ + 'code' => 200, + 'success' => true, + 'msg' => '提现申请提交成功', + 'data' => $result + ]); + + } catch (Exception $e) { + Db::rollback(); + throw $e; + } + + } catch (Exception $e) { + return json([ + 'code' => $e->getCode() ?: 500, + 'success' => false, + 'msg' => '提交提现申请失败:' . $e->getMessage(), + 'data' => null + ]); + } + } + + /** + * 审核提现申请(通过/拒绝) + * @return \think\response\Json + */ + public function review() + { + try { + // 获取参数 + $id = $this->request->param('id', 0); + $action = $this->request->param('action', ''); // approve 或 reject + $remark = $this->request->param('remark', ''); + + $companyId = $this->getUserInfo('companyId'); + $reviewer = $this->getUserInfo('username') ?: $this->getUserInfo('account') ?: '系统管理员'; + + // 参数验证 + if (empty($id)) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '申请ID不能为空', + 'data' => null + ]); + } + + if (!in_array($action, ['approve', 'reject'])) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '审核操作参数错误,必须为 approve 或 reject', + 'data' => null + ]); + } + + // 如果是拒绝,备注必填 + if ($action === 'reject' && empty($remark)) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '拒绝申请时,拒绝理由不能为空', + 'data' => null + ]); + } + + // 检查申请是否存在且属于当前公司 + $withdrawal = Db::name('distribution_withdrawal') + ->where([ + ['id', '=', $id], + ['companyId', '=', $companyId] + ]) + ->find(); + + if (!$withdrawal) { + return json([ + 'code' => 404, + 'success' => false, + 'msg' => '提现申请不存在或没有权限', + 'data' => null + ]); + } + + // 检查申请状态 + if ($withdrawal['status'] !== DistributionWithdrawal::STATUS_PENDING) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '该申请已审核,无法重复审核', + 'data' => null + ]); + } + + // 开始事务 + Db::startTrans(); + try { + $updateData = [ + 'reviewTime' => time(), + 'reviewer' => $reviewer, + 'remark' => $remark ?: '', + 'updateTime' => time(), + ]; + + if ($action === 'approve') { + // 审核通过 + $updateData['status'] = DistributionWithdrawal::STATUS_APPROVED; + } else { + // 审核拒绝,退回可提现金额(金额以分存储) + $updateData['status'] = DistributionWithdrawal::STATUS_REJECTED; + + // 退回渠道可提现金额(以分为单位) + Db::name('distribution_channel') + ->where('id', $withdrawal['channelId']) + ->setInc('withdrawableAmount', intval($withdrawal['amount'])); + } + + // 更新申请状态 + Db::name('distribution_withdrawal') + ->where('id', $id) + ->update($updateData); + + // 提交事务 + Db::commit(); + + $msg = $action === 'approve' ? '审核通过成功' : '审核拒绝成功'; + + return json([ + 'code' => 200, + 'success' => true, + 'msg' => $msg, + 'data' => [ + 'id' => (string)$id, + 'status' => $updateData['status'] + ] + ]); + + } catch (Exception $e) { + Db::rollback(); + throw $e; + } + + } catch (Exception $e) { + return json([ + 'code' => $e->getCode() ?: 500, + 'success' => false, + 'msg' => '审核失败:' . $e->getMessage(), + 'data' => null + ]); + } + } + + /** + * 打款(标记为已打款) + * @return \think\response\Json + */ + public function markPaid() + { + try { + // 获取参数 + $id = $this->request->param('id', 0); + $payType = $this->request->param('payType', ''); // 支付类型:wechat、alipay、bankcard + $remark = $this->request->param('remark', ''); + + $companyId = $this->getUserInfo('companyId'); + + // 参数验证 + if (empty($id)) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '申请ID不能为空', + 'data' => null + ]); + } + + // 验证支付类型 + $validPayTypes = [ + DistributionWithdrawal::PAY_TYPE_WECHAT, + DistributionWithdrawal::PAY_TYPE_ALIPAY, + DistributionWithdrawal::PAY_TYPE_BANKCARD + ]; + if (empty($payType) || !in_array($payType, $validPayTypes)) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '支付类型不能为空,必须为:wechat(微信)、alipay(支付宝)、bankcard(银行卡)', + 'data' => null + ]); + } + + // 检查申请是否存在且属于当前公司 + $withdrawal = Db::name('distribution_withdrawal') + ->where([ + ['id', '=', $id], + ['companyId', '=', $companyId] + ]) + ->find(); + + if (!$withdrawal) { + return json([ + 'code' => 404, + 'success' => false, + 'msg' => '提现申请不存在或没有权限', + 'data' => null + ]); + } + + // 检查申请状态(只有已通过的申请才能打款) + if ($withdrawal['status'] !== DistributionWithdrawal::STATUS_APPROVED) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '只有已通过的申请才能标记为已打款', + 'data' => null + ]); + } + + // 更新状态为已打款 + $result = Db::name('distribution_withdrawal') + ->where('id', $id) + ->update([ + 'status' => DistributionWithdrawal::STATUS_PAID, + 'payType' => $payType, + 'remark' => !empty($remark) ? $remark : $withdrawal['remark'], + 'updateTime' => time() + ]); + + if ($result === false) { + return json([ + 'code' => 500, + 'success' => false, + 'msg' => '标记打款失败', + 'data' => null + ]); + } + + return json([ + 'code' => 200, + 'success' => true, + 'msg' => '标记打款成功', + 'data' => [ + 'id' => (string)$id, + 'status' => DistributionWithdrawal::STATUS_PAID, + 'payType' => $payType + ] + ]); + + } catch (Exception $e) { + return json([ + 'code' => $e->getCode() ?: 500, + 'success' => false, + 'msg' => '标记打款失败:' . $e->getMessage(), + 'data' => null + ]); + } + } + + /** + * 获取提现申请详情 + * @return \think\response\Json + */ + public function detail() + { + try { + // 获取参数 + $id = $this->request->param('id', 0); + + $companyId = $this->getUserInfo('companyId'); + + // 参数验证 + if (empty($id)) { + return json([ + 'code' => 400, + 'success' => false, + 'msg' => '申请ID不能为空', + 'data' => null + ]); + } + + // 构建查询条件 + $where = [ + ['w.id', '=', $id], + ['w.companyId', '=', $companyId] + ]; + + // 如果不是管理员,只能查看自己创建的提现申请 + if (!$this->getUserInfo('isAdmin')) { + $where[] = ['w.userId', '=', $this->getUserInfo('id')]; + } + + // 查询申请详情(关联渠道表) + $withdrawal = Db::name('distribution_withdrawal') + ->alias('w') + ->join('distribution_channel c', 'w.channelId = c.id AND c.deleteTime = 0', 'left') + ->where($where) + ->field([ + 'w.id', + 'w.channelId', + 'w.userId', + 'w.amount', + 'w.status', + 'w.payType', + 'w.applyTime', + 'w.reviewTime', + 'w.reviewer', + 'w.remark', + 'c.name as channelName', + 'c.code as channelCode' + ]) + ->find(); + + if (!$withdrawal) { + return json([ + 'code' => 404, + 'success' => false, + 'msg' => '提现申请不存在或没有权限', + 'data' => null + ]); + } + + // 格式化返回数据(分转元) + $result = [ + 'id' => (string)$withdrawal['id'], + 'channelId' => (string)$withdrawal['channelId'], + 'channelName' => $withdrawal['channelName'] ?? '', + 'channelCode' => $withdrawal['channelCode'] ?? '', + 'userId' => (int)($withdrawal['userId'] ?? 0), + 'amount' => round($withdrawal['amount'] / 100, 2), // 分转元,保留2位小数 + 'status' => $withdrawal['status'], + 'payType' => !empty($withdrawal['payType']) ? $withdrawal['payType'] : null, // 支付类型:wechat、alipay、bankcard + 'applyDate' => !empty($withdrawal['applyTime']) ? date('Y/m/d', $withdrawal['applyTime']) : '', + 'reviewDate' => !empty($withdrawal['reviewTime']) ? date('Y-m-d H:i:s', $withdrawal['reviewTime']) : null, + 'reviewer' => !empty($withdrawal['reviewer']) ? $withdrawal['reviewer'] : null, + 'remark' => !empty($withdrawal['remark']) ? $withdrawal['remark'] : null, + ]; + + return json([ + 'code' => 200, + 'success' => true, + 'msg' => '获取成功', + 'data' => $result + ]); + + } catch (Exception $e) { + return json([ + 'code' => $e->getCode() ?: 500, + 'success' => false, + 'msg' => '获取详情失败:' . $e->getMessage(), + 'data' => null + ]); + } + } +} + diff --git a/application/cunkebao/controller/friend/GetFriendListV1Controller.php b/application/cunkebao/controller/friend/GetFriendListV1Controller.php new file mode 100644 index 0000000..4ba300f --- /dev/null +++ b/application/cunkebao/controller/friend/GetFriendListV1Controller.php @@ -0,0 +1,211 @@ +request->param('page',1); + $limit = $this->request->param('limit',20); + $keyword = $this->request->param('keyword',''); + $deviceIds = $this->request->param('deviceIds',''); + + if(!empty($deviceIds)){ + $deviceIds = explode(',',$deviceIds); + } + + try { + + $where = []; + if ($this->getUserInfo('isAdmin') == 1) { + $where[] = ['isDeleted','=',0]; + } else { + $where[] = ['isDeleted','=',0]; + } + + if(!empty($keyword)){ + $where[] = ['nickname|alias|wechatId','like','%'.$keyword.'%']; + } + + /* $wechatIds = Db::name('device')->alias('d') + ->join('device_wechat_login dwl','dwl.deviceId=d.id AND dwl.companyId='.$this->getUserInfo('companyId')) + ->where(['d.companyId' => $this->getUserInfo('companyId'),'d.deleteTime' => 0]) + ->group('dwl.deviceId') + ->order('dwl.id desc');*/ + + + $companyId = $this->getUserInfo('companyId'); + + $wechatIds = Db::name('device')->alias('d') + // 仅关联每个设备在 device_wechat_login 中的最新一条记录 + ->join('(SELECT MAX(id) AS id, deviceId FROM ck_device_wechat_login WHERE companyId='.$companyId.' GROUP BY deviceId) dwl_max','dwl_max.deviceId = d.id') + ->join('device_wechat_login dwl','dwl.id = dwl_max.id') + ->where(['d.companyId' => $companyId,'d.deleteTime' => 0]); + + + if (!empty($deviceIds)){ + $wechatIds = $wechatIds->where('d.id','in',$deviceIds); + } + $wechatIds = $wechatIds->column('dwl.wechatId'); + + $where[] = ['ownerWechatId','in',$wechatIds]; + + $data = Db::table('s2_wechat_friend') + ->field([ + 'id', 'nickname', 'avatar', 'alias', 'wechatId', + 'gender', 'phone', 'createTime', 'updateTime', 'deleteTime', + 'ownerNickname', 'ownerAlias', 'ownerWechatId', + 'accountUserName', 'accountNickname', 'accountRealName' + ]) + ->where($where); + $total = $data->count(); + $list = $data->page($page, $limit)->order('id DESC')->select(); + + // 格式化时间字段和处理数据 + $formattedList = []; + foreach ($list as $item) { + $formattedItem = [ + 'id' => $item['id'], + 'nickname' => $item['nickname'] ?? '', + 'avatar' => $item['avatar'] ?? '', + 'alias' => $item['alias'] ?? '', + 'wechatId' => $item['wechatId'] ?? '', + 'gender' => $item['gender'] ?? 0, + 'phone' => $item['phone'] ?? '', + 'account' => $item['accountUserName'] ?? '', + 'username' => $item['accountRealName'] ?? '', + 'createTime' => !empty($item['createTime']) ? date('Y-m-d H:i:s', $item['createTime']) : '1970-01-01 08:00:00', + 'updateTime' => !empty($item['updateTime']) ? date('Y-m-d H:i:s', $item['updateTime']) : '1970-01-01 08:00:00', + 'deleteTime' => !empty($item['deleteTime']) ? date('Y-m-d H:i:s', $item['deleteTime']) : '1970-01-01 08:00:00', + 'ownerNickname' => $item['ownerNickname'] ?? '', + 'ownerAlias' => $item['ownerAlias'] ?? '', + 'ownerWechatId' => $item['ownerWechatId'] ?? '', + 'accountNickname' => $item['accountNickname'] ?? '' + ]; + $formattedList[] = $formattedItem; + } + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $formattedList, + 'total' => $total, + 'companyId' => $this->getUserInfo('companyId') + ] + ]); + } catch (\Exception $e) { + return json([ + 'code' => $e->getCode(), + 'msg' => $e->getMessage() + ]); + } + } + + /** + * 好友转移 + * @return \think\response\Json + */ + public function transfer() + { + $friendId = $this->request->param('friendId', 0); + $toAccountId = $this->request->param('toAccountId', ''); + $comment = $this->request->param('comment', ''); + $companyId = $this->getUserInfo('companyId'); + + // 参数验证 + if (empty($friendId)) { + return json([ + 'code' => 400, + 'msg' => '好友ID不能为空' + ]); + } + + if (empty($toAccountId)) { + return json([ + 'code' => 400, + 'msg' => '目标账号ID不能为空' + ]); + } + + try { + // 验证目标账号是否存在且属于当前公司 + $accountInfo = Db::table('s2_company_account') + ->where('id', $toAccountId) + ->where('departmentId', $companyId) + ->field('id as accountId, userName as accountUserName, realName as accountRealName, nickname as accountNickname, tenantId') + ->find(); + + if (empty($accountInfo)) { + return json([ + 'code' => 404, + 'msg' => '目标账号不存在' + ]); + } + + + // 调用 AutomaticAssign 进行好友转移 + $automaticAssign = new AutomaticAssign(); + $result = $automaticAssign->allotWechatFriend([ + 'wechatFriendId' => $friendId, + 'toAccountId' => $toAccountId, + 'comment' => $comment, + 'notifyReceiver' => false, + 'optFrom' => 4 + ], true); + + $resultData = json_decode($result, true); + + if (!empty($resultData) && $resultData['code'] == 200) { + // 转移成功后更新数据库 + $updateData = [ + 'accountId' => $accountInfo['accountId'], + 'accountUserName' => $accountInfo['accountUserName'], + 'accountRealName' => $accountInfo['accountRealName'], + 'accountNickname' => $accountInfo['accountNickname'], + 'updateTime' => time() + ]; + + Db::table('s2_wechat_friend') + ->where('id', $friendId) + ->update($updateData); + + return json([ + 'code' => 200, + 'msg' => '好友转移成功', + 'data' => [ + 'friendId' => $friendId, + 'toAccountId' => $toAccountId + ] + ]); + } else { + return json([ + 'code' => 500, + 'msg' => '好友转移失败:' . ($resultData['msg'] ?? '未知错误') + ]); + } + + } catch (\Exception $e) { + return json([ + 'code' => 500, + 'msg' => '好友转移失败:' . $e->getMessage() + ]); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/plan/GetAddFriendPlanDetailV1Controller.php b/application/cunkebao/controller/plan/GetAddFriendPlanDetailV1Controller.php new file mode 100644 index 0000000..3d262ab --- /dev/null +++ b/application/cunkebao/controller/plan/GetAddFriendPlanDetailV1Controller.php @@ -0,0 +1,231 @@ + '测试客户', + 'phone' => '18888888888', + 'apiKey' => $apiKey, + 'timestamp' => time() + ]; + + // 生成签名 + $sign = $this->generateSignature($testParams, $apiKey); + $testParams['sign'] = $sign; + + // 构建签名过程说明 + $signParams = $testParams; + unset($signParams['sign'], $signParams['apiKey']); + ksort($signParams); + $signStr = implode('', array_values($signParams)); + + // 构建完整URL参数,不对中文进行编码 + $urlParams = []; + foreach ($testParams as $key => $value) { + $urlParams[] = $key . '=' . $value; + } + $fullUrl = implode('&', $urlParams); + + return [ + 'apiKey' => $apiKey, + 'originalString' => $signStr, + 'sign' => $sign, + 'fullUrl' => $fullUrl + ]; + + } catch (\Exception $e) { + return []; + } + } + + /** + * 获取计划详情 + * + * @return \think\response\Json + */ + public function index() + { + try { + $planId = $this->request->param('planId'); + + if (empty($planId)) { + return ResponseHelper::error('计划ID不能为空', 400); + } + + // 查询计划详情 + $plan = Db::name('customer_acquisition_task') + ->where('id', $planId) + ->find(); + + if (!$plan) { + return ResponseHelper::error('计划不存在', 404); + } + + // 解析JSON字段 + $sceneConf = json_decode($plan['sceneConf'], true) ?: []; + $reqConf = json_decode($plan['reqConf'], true) ?: []; + $reqConf['deviceGroups'] = $reqConf['device']; + $msgConf = json_decode($plan['msgConf'], true) ?: []; + $tagConf = json_decode($plan['tagConf'], true) ?: []; + + // 处理分销配置 + $distributionConfig = $sceneConf['distribution'] ?? [ + 'enabled' => false, + 'channels' => [], + 'customerRewardAmount' => 0, + 'addFriendRewardAmount' => 0, + ]; + + // 格式化分销配置(分转元,并获取渠道详情) + $distributionEnabled = !empty($distributionConfig['enabled']); + $distributionChannels = []; + if ($distributionEnabled && !empty($distributionConfig['channels'])) { + $channels = Db::name('distribution_channel') + ->where([ + ['id', 'in', $distributionConfig['channels']], + ['deleteTime', '=', 0] + ]) + ->field('id,code,name') + ->select(); + $distributionChannels = array_map(function($channel) { + return [ + 'id' => (int)$channel['id'], + 'code' => $channel['code'], + 'name' => $channel['name'] + ]; + }, $channels); + } + + // 将分销配置添加到返回数据中 + $sceneConf['distributionEnabled'] = $distributionEnabled; + $sceneConf['distributionChannels'] = $distributionChannels; + $sceneConf['customerRewardAmount'] = round(($distributionConfig['customerRewardAmount'] ?? 0) / 100, 2); // 分转元 + $sceneConf['addFriendRewardAmount'] = round(($distributionConfig['addFriendRewardAmount'] ?? 0) / 100, 2); // 分转元 + + + + if(!empty($sceneConf['wechatGroups'])){ + $groupList = Db::name('wechat_group')->alias('wg') + ->join('wechat_account wa', 'wa.wechatId = wg.ownerWechatId') + ->where('wg.id', 'in', $sceneConf['wechatGroups']) + ->order('wg.id', 'desc') + ->field('wg.id,wg.name,wg.chatroomId,wg.ownerWechatId,wa.nickName as ownerNickName,wa.avatar as ownerAvatar,wa.alias as ownerAlias,wg.avatar') + ->select(); + $sceneConf['wechatGroupsOptions'] = $groupList; + }else{ + $sceneConf['wechatGroupsOptions'] = []; + } + + + if (!empty($reqConf['deviceGroups'])){ + $deviceGroupsOptions = DeviceModel::alias('d') + ->field([ + 'd.id', 'd.imei', 'd.memo', 'd.alive', + 'l.wechatId', + 'a.nickname', 'a.alias', '0 totalFriend', '0 totalFriend' + ]) + ->leftJoin('device_wechat_login l', 'd.id = l.deviceId and l.alive =' . DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE . ' and l.companyId = d.companyId') + ->leftJoin('wechat_account a', 'l.wechatId = a.wechatId') + ->order('d.id desc') + ->whereIn('d.id',$reqConf['deviceGroups']) + ->select(); + foreach ($deviceGroupsOptions as &$device) { + $curstomer = WechatCustomerModel::field('friendShip')->where(['wechatId' => $device['wechatId']])->find(); + $device['totalFriend'] = $curstomer->friendShip->totalFriend ?? 0; + } + unset($device); + $reqConf['deviceGroupsOptions'] = $deviceGroupsOptions; + }else{ + $reqConf['deviceGroupsOptions'] = []; + } + + + + + unset( + $reqConf['device'], + $sceneConf['groupSelected'], + ); + + // 合并数据 + $newData['messagePlans'] = $msgConf; + $newData = array_merge($newData, $sceneConf, $reqConf, $tagConf, $plan); + + // 移除不需要的字段 + unset( + $newData['sceneConf'], + $newData['reqConf'], + $newData['msgConf'], + $newData['tagConf'], + $newData['userInfo'], + $newData['createTime'], + $newData['updateTime'], + $newData['deleteTime'] + ); + + // 生成测试URL + $newData['textUrl'] = $this->testUrl($newData['apiKey']); + + return ResponseHelper::success($newData, '获取计划详情成功'); + + } catch (\Exception $e) { + return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/plan/GetCreateAddFriendPlanV1Controller.php b/application/cunkebao/controller/plan/GetCreateAddFriendPlanV1Controller.php new file mode 100644 index 0000000..6941a8d --- /dev/null +++ b/application/cunkebao/controller/plan/GetCreateAddFriendPlanV1Controller.php @@ -0,0 +1,135 @@ + 0 ? '-' : '') . $segment; + } + + // 检查是否已存在 + $exists = Db::name('customer_acquisition_task') + ->where('apiKey', $apiKey) + ->find(); + + if ($exists) { + // 如果已存在,递归重新生成 + return $this->generateApiKey(); + } + + return $apiKey; + } + + /** + * 拷贝计划任务 + * + * @return \think\response\Json + */ + public function copy() + { + try { + $params = $this->request->param(); + $planId = isset($params['planId']) ? intval($params['planId']) : 0; + + if ($planId <= 0) { + return ResponseHelper::error('计划ID不能为空', 400); + } + + $plan = Db::name('customer_acquisition_task')->where('id', $planId)->find(); + if (!$plan) { + return ResponseHelper::error('计划不存在', 404); + } + + unset($plan['id']); + $plan['name'] = $plan['name'] . ' (拷贝)'; + $plan['createTime'] = time(); + $plan['updateTime'] = time(); + $plan['apiKey'] = $this->generateApiKey(); // 生成新的API密钥 + + $newPlanId = Db::name('customer_acquisition_task')->insertGetId($plan); + if (!$newPlanId) { + return ResponseHelper::error('拷贝计划失败', 500); + } + + return ResponseHelper::success(['planId' => $newPlanId], '拷贝计划任务成功'); + } catch (\Exception $e) { + return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500); + } + } + + /** + * 删除计划任务 + * + * @return \think\response\Json + */ + public function delete() + { + try { + $params = $this->request->param(); + $planId = isset($params['planId']) ? intval($params['planId']) : 0; + + if ($planId <= 0) { + return ResponseHelper::error('计划ID不能为空', 400); + } + + $result = Db::name('customer_acquisition_task')->where('id', $planId)->update(['deleteTime' => time()]); + if (!$result) { + return ResponseHelper::error('删除计划失败', 500); + } + + return ResponseHelper::success([], '删除计划任务成功'); + } catch (\Exception $e) { + return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500); + } + } + + /** + * 修改计划任务状态 + * + * @return \think\response\Json + */ + public function updateStatus() + { + try { + $params = $this->request->param(); + $planId = isset($params['planId']) ? intval($params['planId']) : 0; + $status = isset($params['status']) ? intval($params['status']) : 0; + + if ($planId <= 0) { + return ResponseHelper::error('计划ID不能为空', 400); + } + + $result = Db::name('customer_acquisition_task')->where('id', $planId)->update(['status' => $status, 'updateTime' => time()]); + if (!$result) { + return ResponseHelper::error('修改计划状态失败', 500); + } + + return ResponseHelper::success([], '修改计划任务状态成功'); + } catch (\Exception $e) { + return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/plan/GetPlanSceneListV1Controller.php b/application/cunkebao/controller/plan/GetPlanSceneListV1Controller.php new file mode 100644 index 0000000..887bfee --- /dev/null +++ b/application/cunkebao/controller/plan/GetPlanSceneListV1Controller.php @@ -0,0 +1,294 @@ + PlansSceneModel::STATUS_ACTIVE]; + + // 搜索条件 + if (!empty($params['keyword'])) { + $where[] = ['name', 'like', '%' . $params['keyword'] . '%']; + } + + // 标签筛选 + if (!empty($params['tag'])) { + $where[] = ['scenarioTags', 'like', '%' . $params['tag'] . '%']; + } + + // 查询数据 + $query = PlansSceneModel::where($where); + + // 获取分页数据 + $list = $query->order('sort DESC')->select()->toArray(); + + if (empty($list)) { + return []; + } + + $sceneIds = array_column($list, 'id'); + $companyId = $this->getUserInfo('companyId'); + $statsMap = $this->buildSceneStats($sceneIds, (int)$companyId); + + // 处理数据 + foreach($list as &$val) { + $val['scenarioTags'] = json_decode($val['scenarioTags'], true) ?: []; + $sceneStats = $statsMap[$val['id']] ?? ['count' => 0, 'growth' => '0%']; + $val['count'] = $sceneStats['count']; + $val['growth'] = $sceneStats['growth']; + } + unset($val); + + return $list; + + } catch (\Exception $e) { + throw new \Exception('获取场景列表失败:' . $e->getMessage()); + } + } + + /** + * 获取场景列表 + * + * @return \think\response\Json + */ + public function index() + { + try { + $params = $this->request->param(); + $result = $this->getSceneList($params); + return ResponseHelper::success($result); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), 500); + } + } + + /** + * 获取场景详情 + * + * @return \think\response\Json + */ + public function detail() + { + try { + $id = $this->request->param('id', ''); + if(empty($id)) { + return ResponseHelper::error('参数缺失'); + } + + $data = PlansSceneModel::where([ + 'status' => PlansSceneModel::STATUS_ACTIVE, + 'id' => $id + ])->find(); + + if(empty($data)) { + return ResponseHelper::error('场景不存在'); + } + + $data['scenarioTags'] = json_decode($data['scenarioTags'], true) ?: []; + $data['count'] = $this->getPlanCount($id); + $data['growth'] = $this->calculateGrowth($id); + + return ResponseHelper::success($data); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), 500); + } + } + + /** + * 获取计划数量 + * + * @param int $sceneId 场景ID + * @return int + */ + private function getPlanCount(int $sceneId): int + { + return Db::name('customer_acquisition_task') + ->where('sceneId', $sceneId) + ->where('companyId',$this->getUserInfo('companyId')) + ->where('deleteTime', 0) + ->count(); + } + + /** + * 计算增长率 + * + * @param int $sceneId 场景ID + * @return string + */ + private function calculateGrowth(int $sceneId): string + { + $companyId = $this->getUserInfo('companyId'); + $currentStart = strtotime(date('Y-m-01 00:00:00')); + $nextMonthStart = strtotime(date('Y-m-01 00:00:00', strtotime('+1 month'))); + $lastMonthStart = strtotime(date('Y-m-01 00:00:00', strtotime('-1 month'))); + + $currentMonth = $this->getSceneMonthlyCount($sceneId, $companyId, $currentStart, $nextMonthStart - 1); + $lastMonth = $this->getSceneMonthlyCount($sceneId, $companyId, $lastMonthStart, $currentStart - 1); + + return $this->formatGrowthPercentage($currentMonth, $lastMonth); + } + + /** + * 批量构建场景统计数据 + * @param array $sceneIds + * @param int $companyId + * @return array + */ + private function buildSceneStats(array $sceneIds, int $companyId): array + { + if (empty($sceneIds)) { + return []; + } + + $totalCounts = $this->getSceneTaskCounts($sceneIds, $companyId); + + $currentStart = strtotime(date('Y-m-01 00:00:00')); + $nextMonthStart = strtotime(date('Y-m-01 00:00:00', strtotime('+1 month'))); + $lastMonthStart = strtotime(date('Y-m-01 00:00:00', strtotime('-1 month'))); + + $currentMonthCounts = $this->getSceneMonthlyCounts($sceneIds, $companyId, $currentStart, $nextMonthStart - 1); + $lastMonthCounts = $this->getSceneMonthlyCounts($sceneIds, $companyId, $lastMonthStart, $currentStart - 1); + + $stats = []; + foreach ($sceneIds as $sceneId) { + $current = $currentMonthCounts[$sceneId] ?? 0; + $last = $lastMonthCounts[$sceneId] ?? 0; + $stats[$sceneId] = [ + 'count' => $totalCounts[$sceneId] ?? 0, + 'growth' => $this->formatGrowthPercentage($current, $last), + ]; + } + + return $stats; + } + + /** + * 获取场景计划总数 + * @param array $sceneIds + * @param int $companyId + * @return array + */ + private function getSceneTaskCounts(array $sceneIds, int $companyId): array + { + if (empty($sceneIds)) { + return []; + } + + + $where = [ + ['companyId', '=', $companyId], + ['deleteTime', '=', 0], + ['sceneId', 'in', $sceneIds], + ]; + if(!$this->getUserInfo('isAdmin')){ + $where[] = ['userId', '=', $this->getUserInfo('id')]; + } + + + $rows = Db::name('customer_acquisition_task') + ->where($where) + ->field('sceneId, COUNT(*) as total') + ->group('sceneId') + ->select(); + + $result = []; + foreach ($rows as $row) { + $sceneId = is_array($row) ? ($row['sceneId'] ?? 0) : ($row->sceneId ?? 0); + if (!$sceneId) { + continue; + } + $result[$sceneId] = (int)(is_array($row) ? ($row['total'] ?? 0) : ($row->total ?? 0)); + } + + return $result; + } + + /** + * 获取场景月度数据 + * @param array $sceneIds + * @param int $companyId + * @param int $startTime + * @param int $endTime + * @return array + */ + private function getSceneMonthlyCounts(array $sceneIds, int $companyId, int $startTime, int $endTime): array + { + if (empty($sceneIds)) { + return []; + } + + $rows = Db::name('customer_acquisition_task') + ->whereIn('sceneId', $sceneIds) + ->where('companyId', $companyId) + ->where('status', 1) + ->where('deleteTime', 0) + ->whereBetween('createTime', [$startTime, $endTime]) + ->field('sceneId, COUNT(*) as total') + ->group('sceneId') + ->select(); + + $result = []; + foreach ($rows as $row) { + $sceneId = is_array($row) ? ($row['sceneId'] ?? 0) : ($row->sceneId ?? 0); + if (!$sceneId) { + continue; + } + $result[$sceneId] = (int)(is_array($row) ? ($row['total'] ?? 0) : ($row->total ?? 0)); + } + + return $result; + } + + /** + * 获取单个场景的月度数据 + * @param int $sceneId + * @param int $companyId + * @param int $startTime + * @param int $endTime + * @return int + */ + private function getSceneMonthlyCount(int $sceneId, int $companyId, int $startTime, int $endTime): int + { + return Db::name('customer_acquisition_task') + ->where('sceneId', $sceneId) + ->where('companyId', $companyId) + ->where('status', 1) + ->where('deleteTime', 0) + ->whereBetween('createTime', [$startTime, $endTime]) + ->count(); + } + + /** + * 计算增长百分比 + * @param int $current + * @param int $last + * @return string + */ + private function formatGrowthPercentage(int $current, int $last): string + { + if ($last == 0) { + return $current > 0 ? '100%' : '0%'; + } + + $growth = round(($current - $last) / $last * 100, 2); + return $growth . '%'; + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/plan/PlanSceneV1Controller.php b/application/cunkebao/controller/plan/PlanSceneV1Controller.php new file mode 100644 index 0000000..3812e56 --- /dev/null +++ b/application/cunkebao/controller/plan/PlanSceneV1Controller.php @@ -0,0 +1,554 @@ +request->param(); + $page = isset($params['page']) ? intval($params['page']) : 1; + $limit = isset($params['limit']) ? intval($params['limit']) : 10; + $keyword = isset($params['keyword']) ? trim($params['keyword']) : ''; + $sceneId = $this->request->param('sceneId',''); + $where = [ + 'deleteTime' => 0, + 'companyId' => $this->getUserInfo('companyId'), + ]; + + if(!$this->getUserInfo('isAdmin')){ + $where['userId'] = $this->getUserInfo('id'); + } + + if(!empty($sceneId)){ + $where['sceneId'] = $sceneId; + } + + if(!empty($keyword)){ + $where[] = ['name', 'like', '%' . $keyword . '%']; + } + + + $total = Db::name('customer_acquisition_task')->where($where)->count(); + $list = Db::name('customer_acquisition_task') + ->where($where) + ->order('createTime', 'desc') + ->page($page, $limit) + ->select(); + + if (!empty($list)) { + $taskIds = array_column($list, 'id'); + $statsMap = $this->buildTaskStats($taskIds); + + foreach($list as &$val){ + $val['createTime'] = !empty($val['createTime']) ? date('Y-m-d H:i:s', $val['createTime']) : ''; + $val['updateTime'] = !empty($val['updateTime']) ? date('Y-m-d H:i:s', $val['updateTime']) : ''; + $val['sceneConf'] = json_decode($val['sceneConf'],true) ?: []; + $val['reqConf'] = json_decode($val['reqConf'],true) ?: []; + $val['msgConf'] = json_decode($val['msgConf'],true) ?: []; + $val['tagConf'] = json_decode($val['tagConf'],true) ?: []; + + $stats = $statsMap[$val['id']] ?? [ + 'acquiredCount' => 0, + 'addedCount' => 0, + 'passCount' => 0, + 'lastUpdated' => 0 + ]; + + $val['acquiredCount'] = $stats['acquiredCount']; + $val['addedCount'] = $stats['addedCount']; + $val['passCount'] = $stats['passCount']; + $val['passRate'] = ($stats['addedCount'] > 0 && $stats['passCount'] > 0) + ? number_format(($stats['passCount'] / $stats['addedCount']) * 100, 2) + : 0; + $val['lastUpdated'] = !empty($stats['lastUpdated']) ? date('Y-m-d H:i', $stats['lastUpdated']) : '--'; + } + unset($val); + } + return ResponseHelper::success([ + 'total' => $total, + 'list' => $list + ], '获取计划任务列表成功'); + } catch (\Exception $e) { + return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500); + } + } + + + /** + * 删除计划任务 + * + * @return \think\response\Json + */ + public function delete() + { + try { + $params = $this->request->param(); + $planId = isset($params['planId']) ? intval($params['planId']) : 0; + + if ($planId <= 0) { + return ResponseHelper::error('计划ID不能为空', 400); + } + + $result = Db::name('customer_acquisition_task')->where('id', $planId)->update(['deleteTime' => time()]); + if (!$result) { + return ResponseHelper::error('删除计划失败', 500); + } + + return ResponseHelper::success([], '删除计划任务成功'); + } catch (\Exception $e) { + return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500); + } + } + + /** + * 修改计划任务状态 + * + * @return \think\response\Json + */ + public function updateStatus() + { + try { + $params = $this->request->param(); + $planId = isset($params['planId']) ? intval($params['planId']) : 0; + $status = isset($params['status']) ? intval($params['status']) : 0; + + if ($planId <= 0) { + return ResponseHelper::error('计划ID不能为空', 400); + } + + $result = Db::name('customer_acquisition_task')->where('id', $planId)->update(['status' => $status, 'updateTime' => time()]); + if (!$result) { + return ResponseHelper::error('修改计划状态失败', 500); + } + + return ResponseHelper::success([], '修改计划任务状态成功'); + } catch (\Exception $e) { + return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500); + } + } + + /** + * 获取获客计划设备列表 + * + * @return \think\response\Json + */ + public function getPlanDevices() + { + try { + $params = $this->request->param(); + $planId = isset($params['planId']) ? intval($params['planId']) : 0; + $page = isset($params['page']) ? intval($params['page']) : 1; + $limit = isset($params['limit']) ? intval($params['limit']) : 10; + $deviceStatus = isset($params['deviceStatus']) ? $params['deviceStatus'] : ''; + $searchKeyword = isset($params['searchKeyword']) ? trim($params['searchKeyword']) : ''; + + // 验证计划ID + if ($planId <= 0) { + return ResponseHelper::error('计划ID不能为空', 400); + } + + // 验证计划是否存在且用户有权限 + $plan = Db::name('customer_acquisition_task') + ->where([ + 'id' => $planId, + 'deleteTime' => 0, + 'companyId' => $this->getUserInfo('companyId') + ]) + ->find(); + + if (!$plan) { + return ResponseHelper::error('计划不存在或无权限访问', 404); + } + + // 如果是管理员,需要验证用户权限 + if (!$this->getUserInfo('isAdmin')) { + $userPlan = Db::name('customer_acquisition_task') + ->where([ + 'id' => $planId, + 'userId' => $this->getUserInfo('id') + ]) + ->find(); + + if (!$userPlan) { + return ResponseHelper::error('您没有权限访问该计划', 403); + } + } + + // 构建查询条件 + $where = [ + 'pt.plan_id' => $planId, + 'd.deleteTime' => 0, + 'd.companyId' => $this->getUserInfo('companyId') + ]; + + // 设备状态筛选 + if (!empty($deviceStatus)) { + $where['d.alive'] = $deviceStatus; + } + + // 搜索关键词 + $searchWhere = []; + if (!empty($searchKeyword)) { + $searchWhere[] = ['d.imei', 'like', "%{$searchKeyword}%"]; + $searchWhere[] = ['d.memo', 'like', "%{$searchKeyword}%"]; + } + + // 查询设备总数 + $totalQuery = Db::name('plan_task_device')->alias('pt') + ->join('device d', 'pt.device_id = d.id') + ->where($where); + + if (!empty($searchWhere)) { + $totalQuery->where(function ($query) use ($searchWhere) { + foreach ($searchWhere as $condition) { + $query->whereOr($condition[0], $condition[1], $condition[2]); + } + }); + } + + $total = $totalQuery->count(); + + // 查询设备列表 + $listQuery = Db::name('plan_task_device')->alias('pt') + ->join('device d', 'pt.device_id = d.id') + ->field([ + 'd.id', + 'd.imei', + 'd.memo', + 'd.alive', + 'd.extra', + 'd.createTime', + 'd.updateTime', + 'pt.status as plan_device_status', + 'pt.createTime as assign_time' + ]) + ->where($where) + ->order('pt.createTime', 'desc'); + + if (!empty($searchWhere)) { + $listQuery->where(function ($query) use ($searchWhere) { + foreach ($searchWhere as $condition) { + $query->whereOr($condition[0], $condition[1], $condition[2]); + } + }); + } + + $list = $listQuery->page($page, $limit)->select(); + + // 处理设备数据 + foreach ($list as &$device) { + // 格式化时间 + $device['createTime'] = date('Y-m-d H:i:s', $device['createTime']); + $device['updateTime'] = date('Y-m-d H:i:s', $device['updateTime']); + $device['assign_time'] = date('Y-m-d H:i:s', $device['assign_time']); + + // 解析设备额外信息 + if (!empty($device['extra'])) { + $extra = json_decode($device['extra'], true); + $device['battery'] = isset($extra['battery']) ? intval($extra['battery']) : 0; + $device['device_info'] = $extra; + } else { + $device['battery'] = 0; + $device['device_info'] = []; + } + + // 设备状态文本 + $device['alive_text'] = $this->getDeviceStatusText($device['alive']); + $device['plan_device_status_text'] = $this->getPlanDeviceStatusText($device['plan_device_status']); + + // 获取设备当前微信登录信息 + $wechatLogin = Db::name('device_wechat_login') + ->where([ + 'deviceId' => $device['id'], + 'companyId' => $this->getUserInfo('companyId'), + 'alive' => 1 + ]) + ->order('createTime', 'desc') + ->find(); + + $device['current_wechat'] = $wechatLogin ? [ + 'wechatId' => $wechatLogin['wechatId'], + 'nickname' => $wechatLogin['nickname'] ?? '', + 'loginTime' => date('Y-m-d H:i:s', $wechatLogin['createTime']) + ] : null; + + // 获取设备在该计划中的任务统计 + $device['task_stats'] = $this->getDeviceTaskStats($device['id'], $planId); + + // 移除原始extra字段 + unset($device['extra']); + } + unset($device); + + return ResponseHelper::success([ + 'total' => $total, + 'list' => $list, + 'plan_info' => [ + 'id' => $plan['id'], + 'name' => $plan['name'], + 'status' => $plan['status'] + ] + ], '获取计划设备列表成功'); + + } catch (\Exception $e) { + return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500); + } + } + + /** + * 获取设备状态文本 + * + * @param int $status + * @return string + */ + private function getDeviceStatusText($status) + { + $statusMap = [ + 0 => '离线', + 1 => '在线', + 2 => '忙碌', + 3 => '故障' + ]; + return isset($statusMap[$status]) ? $statusMap[$status] : '未知'; + } + + /** + * 获取计划设备状态文本 + * + * @param int $status + * @return string + */ + private function getPlanDeviceStatusText($status) + { + $statusMap = [ + 0 => '待分配', + 1 => '已分配', + 2 => '执行中', + 3 => '已完成', + 4 => '已暂停', + 5 => '已取消' + ]; + return isset($statusMap[$status]) ? $statusMap[$status] : '未知'; + } + + /** + * 获取设备在指定计划中的任务统计 + * + * @param int $deviceId + * @param int $planId + * @return array + */ + private function getDeviceTaskStats($deviceId, $planId) + { + // 获取该设备在计划中的任务总数 + $totalTasks = Db::name('task_customer') + ->where([ + 'task_id' => $planId, + 'device_id' => $deviceId + ]) + ->count(); + + // 获取已完成的任务数 + $completedTasks = Db::name('task_customer') + ->where([ + 'task_id' => $planId, + 'device_id' => $deviceId, + 'status' => 4 + ]) + ->count(); + + // 获取进行中的任务数 + $processingTasks = Db::name('task_customer') + ->where([ + 'task_id' => $planId, + 'device_id' => $deviceId, + 'status' => ['in', [1, 2, 3]] + ]) + ->count(); + + return [ + 'total_tasks' => $totalTasks, + 'completed_tasks' => $completedTasks, + 'processing_tasks' => $processingTasks, + 'completion_rate' => $totalTasks > 0 ? round(($completedTasks / $totalTasks) * 100, 2) : 0 + ]; + } + + /** + * 构建任务统计 + * @param array $taskIds + * @return array + */ + private function buildTaskStats(array $taskIds): array + { + if (empty($taskIds)) { + return []; + } + + $rows = Db::name('task_customer') + ->whereIn('task_id', $taskIds) + ->field([ + 'task_id as taskId', + 'COUNT(1) as acquiredCount', + "SUM(CASE WHEN status IN (1,2,3,4,5) THEN 1 ELSE 0 END) as addedCount", + "SUM(CASE WHEN status IN (4,5) THEN 1 ELSE 0 END) as passCount", + 'MAX(updateTime) as lastUpdated' + ]) + ->group('task_id') + ->select(); + + $stats = []; + foreach ($rows as $row) { + $taskId = is_array($row) ? ($row['taskId'] ?? 0) : ($row->taskId ?? 0); + if (!$taskId) { + continue; + } + $stats[$taskId] = [ + 'acquiredCount' => (int)(is_array($row) ? ($row['acquiredCount'] ?? 0) : ($row->acquiredCount ?? 0)), + 'addedCount' => (int)(is_array($row) ? ($row['addedCount'] ?? 0) : ($row->addedCount ?? 0)), + 'passCount' => (int)(is_array($row) ? ($row['passCount'] ?? 0) : ($row->passCount ?? 0)), + 'lastUpdated' => (int)(is_array($row) ? ($row['lastUpdated'] ?? 0) : ($row->lastUpdated ?? 0)), + ]; + } + + return $stats; + } + + + /** + * 获取微信小程序码 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function getWxMinAppCode() + { + $params = $this->request->param(); + $taskId = isset($params['taskId']) ? intval($params['taskId']) : 0; + $channelId = isset($params['channelId']) ? intval($params['channelId']) : 0; + + if($taskId <= 0) { + return ResponseHelper::error('任务ID或场景ID不能为空', 400); + } + + $task = Db::name('customer_acquisition_task')->where(['id' => $taskId, 'deleteTime' => 0])->find(); + if(!$task) { + return ResponseHelper::error('任务不存在', 400); + } + + // 如果提供了channelId,验证渠道是否存在且有效 + if ($channelId > 0) { + $channel = Db::name('distribution_channel') + ->where([ + ['id', '=', $channelId], + ['companyId', '=', $task['companyId']], + ['status', '=', 'enabled'], + ['deleteTime', '=', 0] + ]) + ->find(); + + if (!$channel) { + return ResponseHelper::error('分销渠道不存在或已被禁用', 400); + } + } + + $posterWeChatMiniProgram = new PosterWeChatMiniProgram(); + $result = $posterWeChatMiniProgram->generateMiniProgramCodeWithScene($taskId, $channelId); + $result = json_decode($result, true); + if ($result['code'] == 200){ + return ResponseHelper::success($result['data'], '获取小程序码成功'); + }else{ + return ResponseHelper::error('获取小程序失败:' . $result['msg']); + } + + } + + + /** + * 获取已获客/已添加用户 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function getUserList(){ + $type = $this->request->param('type',1); + $planId = $this->request->param('planId',''); + $page = $this->request->param('page',1); + $pageSize = $this->request->param('pageSize',10); + $keyword = $this->request->param('keyword',''); + + if (!in_array($type, [1, 2])) { + return ResponseHelper::error('类型错误'); + } + + if (empty($planId)){ + return ResponseHelper::error('获客场景id不能为空'); + } + + $task = Db::name('customer_acquisition_task') + ->where(['id' => $planId, 'deleteTime' => 0,'companyId' => $this->getUserInfo('companyId')]) + ->find(); + if(empty($task)) { + return ResponseHelper::error('活动不存在'); + } + $query = Db::name('task_customer')->where(['task_id' => $task['id']]); + + if ($type == 2){ + $query = $query->whereIn('status',[4,5]); + } + + if (!empty($keyword)) { + $query = $query->where('name|phone|tags|siteTags', 'like', '%' . $keyword . '%'); + } + + $total = $query->count(); + $list = $query->page($page, $pageSize)->order('id', 'desc')->select(); + foreach ($list as &$item) { + unset($item['processed_wechat_ids'],$item['task_id']); + $userinfo = Db::table('s2_wechat_friend') + ->field('alias,wechatId,nickname,avatar') + ->where('alias|wechatId|phone|conRemark','like','%'.$item['phone'].'%') + ->order('id DESC') + ->find(); + + if (!empty($userinfo)) { + $item['userinfo'] = $userinfo; + }else{ + $item['userinfo'] = []; + } + + $item['tags'] = json_decode($item['tags'], true); + $item['siteTags'] = json_decode($item['siteTags'], true); + $item['createTime'] = !empty($item['createTime']) ? date('Y-m-d H:i:s', $item['createTime']) : ''; + $item['updateTime'] = !empty($item['updateTime']) ? date('Y-m-d H:i:s', $item['updateTime']) : ''; + } + + + + $data = [ + 'total' => $total, + 'list' => $list, + ]; + return ResponseHelper::success($data,'获取成功'); + } + + + +} \ No newline at end of file diff --git a/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php b/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php new file mode 100644 index 0000000..e7bdb52 --- /dev/null +++ b/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php @@ -0,0 +1,412 @@ + 0 ? '-' : '') . $segment; + } + + // 检查是否已存在 + $exists = Db::name('customer_acquisition_task') + ->where('apiKey', $apiKey) + ->find(); + + if ($exists) { + // 如果已存在,递归重新生成 + return $this->generateApiKey(); + } + + return $apiKey; + } + + /** + * 添加计划任务 + * + * @return \think\response\Json + */ + public function index() + { + try { + $params = $this->request->param(); + + // 验证必填字段 + if (empty($params['name'])) { + return ResponseHelper::error('计划名称不能为空', 400); + } + + if (empty($params['sceneId'])) { + return ResponseHelper::error('场景ID不能为空', 400); + } + + if (empty($params['deviceGroups'])) { + return ResponseHelper::error('请选择设备', 400); + } + + $companyId = $this->getUserInfo('companyId'); + + // 处理分销配置 + $distributionConfig = $this->processDistributionConfig($params, $companyId); + + // 归类参数 + $msgConf = isset($params['messagePlans']) ? $params['messagePlans'] : []; + $tagConf = [ + 'scenarioTags' => $params['scenarioTags'] ?? [], + 'customTags' => $params['customTags'] ?? [], + ]; + $reqConf = [ + 'device' => $params['deviceGroups'] ?? [], + 'remarkType' => $params['remarkType'] ?? '', + 'greeting' => $params['greeting'] ?? '', + 'addFriendInterval' => $params['addFriendInterval'] ?? '', + 'startTime' => $params['startTime'] ?? '', + 'endTime' => $params['endTime'] ?? '', + ]; + // 其余参数归为sceneConf + $sceneConf = $params; + unset( + $sceneConf['id'], + $sceneConf['apiKey'], + $sceneConf['userId'], + $sceneConf['status'], + $sceneConf['planId'], + $sceneConf['name'], + $sceneConf['sceneId'], + $sceneConf['messagePlans'], + $sceneConf['scenarioTags'], + $sceneConf['customTags'], + $sceneConf['device'], + $sceneConf['orderTableFileName'], + $sceneConf['userInfo'], + $sceneConf['textUrl'], + $sceneConf['remarkType'], + $sceneConf['greeting'], + $sceneConf['addFriendInterval'], + $sceneConf['startTime'], + $sceneConf['orderTableFile'], + $sceneConf['endTime'], + $sceneConf['distributionEnabled'], + $sceneConf['distributionChannels'], + $sceneConf['customerRewardAmount'], + $sceneConf['addFriendRewardAmount'] + ); + + // 将分销配置添加到sceneConf中 + $sceneConf['distribution'] = $distributionConfig; + + // 构建数据 + $data = [ + 'name' => $params['name'], + 'sceneId' => $params['sceneId'], + 'sceneConf' => json_encode($sceneConf, JSON_UNESCAPED_UNICODE), + 'reqConf' => json_encode($reqConf, JSON_UNESCAPED_UNICODE), + 'msgConf' => json_encode($msgConf, JSON_UNESCAPED_UNICODE), + 'tagConf' => json_encode($tagConf, JSON_UNESCAPED_UNICODE), + 'userId' => $this->getUserInfo('id'), + 'companyId' => $this->getUserInfo('companyId'), + 'status' => !empty($params['status']) ? 1 : 0, + 'apiKey' => $this->generateApiKey(), // 生成API密钥 + 'createTime' => time(), + 'updateTime' => time(), + ]; + + + try { + Db::startTrans(); + // 插入数据 + $planId = Db::name('customer_acquisition_task')->insertGetId($data); + + if (!$planId) { + throw new \Exception('添加计划失败'); + } + + + //订单 + if ($params['sceneId'] == 2) { + if (!empty($params['orderFileUrl'])) { + // 先下载到本地临时文件,再分析,最后删除 + $originPath = $params['orderFileUrl']; + $tmpFile = tempnam(sys_get_temp_dir(), 'order_'); + // 判断是否为远程文件 + if (preg_match('/^https?:\/\//i', $originPath)) { + // 远程URL,下载到本地 + $fileContent = file_get_contents($originPath); + if ($fileContent === false) { + exit('远程文件下载失败: ' . $originPath); + } + file_put_contents($tmpFile, $fileContent); + } else { + // 本地文件,直接copy + if (!file_exists($originPath)) { + exit('文件不存在: ' . $originPath); + } + copy($originPath, $tmpFile); + } + // 解析临时文件 + $ext = strtolower(pathinfo($originPath, PATHINFO_EXTENSION)); + $rows = []; + if (in_array($ext, ['xls', 'xlsx'])) { + // 直接用composer自动加载的PHPExcel + $excel = \PHPExcel_IOFactory::load($tmpFile); + $sheet = $excel->getActiveSheet(); + $data = $sheet->toArray(); + if (count($data) > 1) { + array_shift($data); // 去掉表头 + } + + foreach ($data as $cols) { + $rows[] = [ + 'name' => isset($cols[0]) ? trim($cols[0]) : '', + 'phone' => isset($cols[1]) ? trim($cols[1]) : '', + 'wechatId' => isset($cols[2]) ? trim($cols[2]) : '', + 'source' => isset($cols[3]) ? trim($cols[3]) : '', + 'orderAmount' => isset($cols[4]) ? trim($cols[4]) : '', + 'orderDate' => isset($cols[5]) ? trim($cols[5]) : '', + ]; + } + } elseif ($ext === 'csv') { + $content = file_get_contents($tmpFile); + $lines = preg_split('/\r\n|\r|\n/', $content); + if (count($lines) > 1) { + array_shift($lines); // 去掉表头 + foreach ($lines as $line) { + if (trim($line) === '') continue; + $cols = str_getcsv($line); + if (count($cols) >= 6) { + $rows[] = [ + 'name' => isset($cols[0]) ? trim($cols[0]) : '', + 'phone' => isset($cols[1]) ? trim($cols[1]) : '', + 'wechatId' => isset($cols[2]) ? trim($cols[2]) : '', + 'source' => isset($cols[3]) ? trim($cols[3]) : '', + 'orderAmount' => isset($cols[4]) ? trim($cols[4]) : '', + 'orderDate' => isset($cols[5]) ? trim($cols[5]) : '', + ]; + } + } + } + } else { + unlink($tmpFile); + exit('暂不支持的文件类型: ' . $ext); + } + // 删除临时文件 + unlink($tmpFile); + } + } + + //电话获客 + if ($params['sceneId'] == 5) { + $rows = Db::name('call_recording') + ->where('companyId', $this->getUserInfo('companyId')) + ->group('phone') + ->field('id,phone') + ->select(); + } + + + //群获客 + if ($params['sceneId'] == 7) { + if (!empty($params['wechatGroups']) && is_array($params['wechatGroups'])) { + $rows = Db::name('wechat_group_member')->alias('gm') + ->join('wechat_account wa', 'gm.identifier = wa.wechatId') + ->whereIn('gm.groupId', $params['wechatGroups']) + ->group('gm.identifier') + ->column('wa.id,wa.wechatId,wa.alias,wa.phone'); + } + } + + + if (in_array($params['sceneId'], [2, 5, 7]) && !empty($rows) && is_array($rows)) { + // 1000条为一组进行批量处理 + $batchSize = 1000; + $totalRows = count($rows); + + for ($i = 0; $i < $totalRows; $i += $batchSize) { + $batchRows = array_slice($rows, $i, $batchSize); + + if (!empty($batchRows)) { + // 1. 提取当前批次的phone + $phones = []; + foreach ($batchRows as $row) { + if (!empty($row['phone'])) { + $phone = $row['phone']; + } elseif (!empty($row['alias'])) { + $phone = $row['alias']; + } else { + $phone = $row['wechatId']; + } + if (!empty($phone)) { + $phones[] = $phone; + } + } + + // 2. 批量查询已存在的phone + $existingPhones = []; + if (!empty($phones)) { + $existing = Db::name('task_customer') + ->where('task_id', $planId) + ->where('phone', 'in', $phones) + ->field('phone') + ->select(); + $existingPhones = array_column($existing, 'phone'); + } + + // 3. 过滤出新数据,批量插入 + $newData = []; + foreach ($batchRows as $row) { + if (!empty($row['phone'])) { + $phone = $row['phone']; + } elseif (!empty($row['alias'])) { + $phone = $row['alias']; + } else { + $phone = $row['wechatId']; + } + if (!empty($phone) && !in_array($phone, $existingPhones)) { + $newData[] = [ + 'task_id' => $planId, + 'name' => '', + 'source' => '场景获客_' . $params['name'] ?? '', + 'phone' => $phone, + 'tags' => json_encode([], JSON_UNESCAPED_UNICODE), + 'siteTags' => json_encode([], JSON_UNESCAPED_UNICODE), + 'createTime' => time(), + ]; + } + } + + // 4. 批量插入新数据 + if (!empty($newData)) { + Db::name('task_customer')->insertAll($newData); + } + } + } + } + + + Db::commit(); + + return ResponseHelper::success(['planId' => $planId], '添加计划任务成功'); + + } catch (\Exception $e) { + // 回滚事务 + Db::rollback(); + throw $e; + } + + } catch (\Exception $e) { + return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500); + } + } + + /** + * 验证JSON格式是否正确 + * + * @param string $string + * @return bool + */ + private function validateJson($string) + { + if (empty($string)) { + return true; + } + + json_decode($string); + return (json_last_error() == JSON_ERROR_NONE); + } + + /** + * 处理分销配置 + * + * @param array $params 请求参数 + * @param int $companyId 公司ID + * @return array 分销配置 + */ + private function processDistributionConfig($params, $companyId) + { + $distributionEnabled = !empty($params['distributionEnabled']) ? true : false; + + $config = [ + 'enabled' => $distributionEnabled, + 'channels' => [], + 'customerRewardAmount' => 0, // 获客奖励金额(分) + 'addFriendRewardAmount' => 0, // 添加奖励金额(分) + ]; + + // 如果未开启分销,直接返回默认配置 + if (!$distributionEnabled) { + return $config; + } + + // 验证渠道ID + $channelIds = $params['distributionChannels'] ?? []; + if (empty($channelIds) || !is_array($channelIds)) { + throw new \Exception('请选择至少一个分销渠道'); + } + + // 查询有效的渠道(只保留存在且已启用的渠道) + $channels = Db::name('distribution_channel') + ->where([ + ['id', 'in', $channelIds], + ['companyId', '=', $companyId], + ['status', '=', 'enabled'], + ['deleteTime', '=', 0] + ]) + ->field('id,code,name') + ->select(); + + // 如果没有有效渠道,才报错 + if (empty($channels)) { + throw new \Exception('所选的分销渠道均不存在或已被禁用,请重新选择'); + } + + // 只保留有效的渠道ID + $config['channels'] = array_column($channels, 'id'); + + // 验证获客奖励金额(元转分) + $customerRewardAmount = isset($params['customerRewardAmount']) ? floatval($params['customerRewardAmount']) : 0; + if ($customerRewardAmount < 0) { + throw new \Exception('获客奖励金额不能为负数'); + } + if ($customerRewardAmount > 0 && !preg_match('/^\d+(\.\d{1,2})?$/', (string)$customerRewardAmount)) { + throw new \Exception('获客奖励金额格式不正确,最多保留2位小数'); + } + $config['customerRewardAmount'] = intval(round($customerRewardAmount * 100)); // 元转分 + + // 验证添加奖励金额(元转分) + $addFriendRewardAmount = isset($params['addFriendRewardAmount']) ? floatval($params['addFriendRewardAmount']) : 0; + if ($addFriendRewardAmount < 0) { + throw new \Exception('添加奖励金额不能为负数'); + } + if ($addFriendRewardAmount > 0 && !preg_match('/^\d+(\.\d{1,2})?$/', (string)$addFriendRewardAmount)) { + throw new \Exception('添加奖励金额格式不正确,最多保留2位小数'); + } + $config['addFriendRewardAmount'] = intval(round($addFriendRewardAmount * 100)); // 元转分 + + return $config; + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/plan/PostExternalApiV1Controller.php b/application/cunkebao/controller/plan/PostExternalApiV1Controller.php new file mode 100644 index 0000000..4edd546 --- /dev/null +++ b/application/cunkebao/controller/plan/PostExternalApiV1Controller.php @@ -0,0 +1,288 @@ +request->param(); + + // 验证必填参数 + if (empty($params['apiKey'])) { + return ResponseHelper::error('apiKey不能为空', 400); + } + + if (empty($params['sign'])) { + return ResponseHelper::error('sign不能为空', 400); + } + + if (empty($params['timestamp'])) { + return ResponseHelper::error('timestamp不能为空', 400); + } + + // 验证时间戳(允许5分钟误差) + if (abs(time() - intval($params['timestamp'])) > 300) { + return ResponseHelper::error('请求已过期', 400); + } + + // 查询API密钥是否存在 + $plan = Db::name('customer_acquisition_task') + ->where('apiKey', $params['apiKey']) + ->where('status', 1) + ->find(); + + if (!$plan) { + return ResponseHelper::error('无效的apiKey', 401); + } + + // 验证签名 + if (!$this->validateSign($params,$params['apiKey'], $params['sign'])) { + return ResponseHelper::error('签名验证失败', 401); + } + + $identifier = !empty($params['wechatId']) ? $params['wechatId'] : $params['phone']; + + // 渠道ID(cid),对应 distribution_channel.id + $channelId = !empty($params['cid']) ? intval($params['cid']) : 0; + + + $trafficPool = Db::name('traffic_pool')->where('identifier', $identifier)->find(); + if (!$trafficPool) { + $trafficPoolId =Db::name('traffic_pool')->insertGetId([ + 'identifier' => $identifier, + 'mobile' => !empty($params['phone']) ? $params['phone'] : '', + 'createTime' => time() + ]); + }else{ + $trafficPoolId = $trafficPool['id']; + } + + $taskCustomer = Db::name('task_customer') + ->where('task_id', $plan['id']) + ->where('phone', $identifier) + ->find(); + + // 处理用户画像 + if(!empty($params['portrait']) && is_array($params['portrait'])){ + $this->updatePortrait($params['portrait'],$trafficPoolId,$plan['companyId']); + } + if (!$taskCustomer) { + $tags = !empty($params['tags']) ? explode(',', $params['tags']) : []; + $siteTags = !empty($params['siteTags']) ? explode(',', $params['siteTags']) : []; + + // 处理渠道ID:只有在分销配置中允许、且渠道本身正常时,才记录到task_customer + $finalChannelId = 0; + if ($channelId > 0) { + $sceneConf = json_decode($plan['sceneConf'], true) ?: []; + $distributionConfig = $sceneConf['distribution'] ?? null; + $allowedChannelIds = $distributionConfig['channels'] ?? []; + if (!empty($distributionConfig) && !empty($distributionConfig['enabled']) && in_array($channelId, $allowedChannelIds)) { + // 验证渠道是否存在且正常 + $channel = Db::name('distribution_channel') + ->where([ + ['id', '=', $channelId], + ['companyId', '=', $plan['companyId']], + ['status', '=', 'enabled'], + ['deleteTime', '=', 0] + ]) + ->find(); + if ($channel) { + $finalChannelId = intval($channelId); + } + } + } + + $customerId = Db::name('task_customer')->insertGetId([ + 'task_id' => $plan['id'], + 'channelId' => $finalChannelId, + 'phone' => $identifier, + 'name' => !empty($params['name']) ? $params['name'] : '', + 'source' => !empty($params['source']) ? $params['source'] : '', + 'remark' => !empty($params['remark']) ? $params['remark'] : '', + 'tags' => json_encode($tags,256), + 'siteTags' => json_encode($siteTags,256), + 'createTime' => time(), + ]); + + // 记录获客奖励(异步处理,不影响主流程) + if ($customerId) { + try { + // 只有在存在有效渠道ID时才触发分佣 + if ($finalChannelId > 0) { + DistributionRewardService::recordCustomerReward($plan['id'], $customerId, $identifier, $finalChannelId); + } + } catch (\Exception $e) { + // 记录错误但不影响主流程 + \think\facade\Log::error('记录获客奖励失败:' . $e->getMessage()); + } + } + + return json([ + 'code' => 200, + 'message' => '新增成功', + 'data' => $identifier + ]); + }else{ + $siteTags = !empty($params['siteTags']) ? explode(',',$params['siteTags']) : []; + + // 更新新老标签数据,实现去重 + $this->updateSiteTags($taskCustomer['id'], $siteTags); + + return json([ + 'code' => 200, + 'message' => '已存在', + 'data' => $identifier + ]); + } + } catch (\Exception $e) { + return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500); + } + } + + /** + * 用户画像 + * @param array $data 用户画像数据 + * @param int $trafficPoolId 流量池id + */ + public function updatePortrait($data,$trafficPoolId,$companyId) + { + if(empty($data) || empty($trafficPoolId) || !is_array($data)){ + return; + } + + $type = !empty($data['type']) ? $data['type'] : 0; + $source = !empty($data['source']) ? $data['source'] : 0; + $sourceData = !empty($data['sourceData']) ? $data['sourceData'] : []; + $remark = !empty($data['remark']) ? $data['remark'] : ''; + $uniqueId = !empty($data['uniqueId']) ? $data['uniqueId'] : 0; + ksort($sourceData); + $sourceData = json_encode($sourceData,256); + + + $data = [ + 'companyId' => $companyId, + 'trafficPoolId' => $trafficPoolId, + 'type' => $type, + 'source' => $source, + 'sourceData' => $sourceData, + 'remark' => $remark, + 'uniqueId' => $uniqueId, + 'count' => 1, + 'createTime' => time(), + 'updateTime' => time(), + ]; + + $res= Db::name('user_portrait') + ->where(['trafficPoolId'=>$trafficPoolId,'type'=>$type,'source'=>$source,'uniqueId'=>$uniqueId]) + ->where('createTime','>',time()-1800) + ->find(); + if($res){ + $count = $res['count'] + 1; + Db::name('user_portrait')->where(['id'=>$res['id']])->update(['count'=>$count,'updateTime'=>time()]); + }else{ + Db::name('user_portrait')->insert($data); + } + + } + + /** + * 更新站点标签数据,实现去重 + * @param int $taskCustomerId 任务客户ID + * @param array $newSiteTags 新的站点标签数组 + */ + private function updateSiteTags($taskCustomerId, $newSiteTags) + { + + if (empty($taskCustomerId) || empty($newSiteTags) || !is_array($newSiteTags)) { + return; + } + + try { + // 获取当前任务客户的站点标签 + $taskCustomer = Db::name('task_customer')->where('id', $taskCustomerId)->find(); + + if (!$taskCustomer) { + return; + } + + // 解析现有的站点标签 + $existingSiteTags = []; + if (!empty($taskCustomer['siteTags'])) { + $existingSiteTags = json_decode($taskCustomer['siteTags'], true); + if (!is_array($existingSiteTags)) { + $existingSiteTags = []; + } + } + + // 合并新老标签并去重 + $mergedSiteTags = array_merge($existingSiteTags, $newSiteTags); + $uniqueSiteTags = array_unique($mergedSiteTags); + + // 过滤空值并重新索引数组 + $uniqueSiteTags = array_values(array_filter($uniqueSiteTags, function($tag) { + return !empty(trim($tag)); + })); + + + // 更新数据库中的站点标签 + Db::name('task_customer')->where('id', $taskCustomerId)->update([ + 'siteTags' => json_encode($uniqueSiteTags, JSON_UNESCAPED_UNICODE), + 'updateTime' => time() + ]); + + } catch (\Exception $e) { + // 记录错误日志,但不影响主流程 + \think\facade\Log::error('更新站点标签失败: ' . $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/plan/PostUpdateAddFriendPlanV1Controller.php b/application/cunkebao/controller/plan/PostUpdateAddFriendPlanV1Controller.php new file mode 100644 index 0000000..95a4468 --- /dev/null +++ b/application/cunkebao/controller/plan/PostUpdateAddFriendPlanV1Controller.php @@ -0,0 +1,369 @@ +request->param(); + + // 验证必填字段 + if (empty($params['planId'])) { + return ResponseHelper::error('计划ID不能为空', 400); + } + + if (empty($params['name'])) { + return ResponseHelper::error('计划名称不能为空', 400); + } + + if (empty($params['sceneId'])) { + return ResponseHelper::error('场景ID不能为空', 400); + } + + if (empty($params['deviceGroups'])) { + return ResponseHelper::error('请选择设备', 400); + } + + // 检查计划是否存在 + $plan = Db::name('customer_acquisition_task') + ->where('id', $params['planId']) + ->find(); + + if (!$plan) { + return ResponseHelper::error('计划不存在', 404); + } + + $companyId = $this->getUserInfo('companyId'); + + // 处理分销配置 + $distributionConfig = $this->processDistributionConfig($params, $companyId); + + // 归类参数 + $msgConf = isset($params['messagePlans']) ? $params['messagePlans'] : []; + $tagConf = [ + 'scenarioTags' => $params['scenarioTags'] ?? [], + 'customTags' => $params['customTags'] ?? [], + ]; + $reqConf = [ + 'device' => $params['deviceGroups'] ?? [], + 'remarkType' => $params['remarkType'] ?? '', + 'greeting' => $params['greeting'] ?? '', + 'addFriendInterval' => $params['addFriendInterval'] ?? '', + 'startTime' => $params['startTime'] ?? '', + 'endTime' => $params['endTime'] ?? '', + ]; + + // 其余参数归为sceneConf + $sceneConf = $params; + unset( + $sceneConf['id'], + $sceneConf['apiKey'], + $sceneConf['userId'], + $sceneConf['status'], + $sceneConf['planId'], + $sceneConf['name'], + $sceneConf['sceneId'], + $sceneConf['messagePlans'], + $sceneConf['scenarioTags'], + $sceneConf['customTags'], + $sceneConf['deviceGroups'], + $sceneConf['orderTableFileName'], + $sceneConf['userInfo'], + $sceneConf['textUrl'], + $sceneConf['remarkType'], + $sceneConf['greeting'], + $sceneConf['addFriendInterval'], + $sceneConf['startTime'], + $sceneConf['orderTableFile'], + $sceneConf['endTime'], + $sceneConf['distributionEnabled'], + $sceneConf['distributionChannels'], + $sceneConf['customerRewardAmount'], + $sceneConf['addFriendRewardAmount'] + ); + + // 将分销配置添加到sceneConf中 + $sceneConf['distribution'] = $distributionConfig; + + // 构建更新数据 + $data = [ + 'name' => $params['name'], + 'sceneId' => $params['sceneId'], + 'sceneConf' => json_encode($sceneConf, JSON_UNESCAPED_UNICODE), + 'reqConf' => json_encode($reqConf, JSON_UNESCAPED_UNICODE), + 'msgConf' => json_encode($msgConf, JSON_UNESCAPED_UNICODE), + 'tagConf' => json_encode($tagConf, JSON_UNESCAPED_UNICODE), + 'status' => !empty($params['status']) ? 1 : 0, + 'updateTime' => time(), + ]; + + + try { + // 更新数据 + $result = Db::name('customer_acquisition_task') + ->where('id', $params['planId']) + ->update($data); + + if ($result === false) { + throw new \Exception('更新计划失败'); + } + + //订单 + if ($params['sceneId'] == 2) { + if (!empty($params['orderFileUrl'])) { + // 先下载到本地临时文件,再分析,最后删除 + $originPath = $params['orderFileUrl']; + $tmpFile = tempnam(sys_get_temp_dir(), 'order_'); + // 判断是否为远程文件 + if (preg_match('/^https?:\/\//i', $originPath)) { + // 远程URL,下载到本地 + $fileContent = file_get_contents($originPath); + if ($fileContent === false) { + exit('远程文件下载失败: ' . $originPath); + } + file_put_contents($tmpFile, $fileContent); + } else { + // 本地文件,直接copy + if (!file_exists($originPath)) { + exit('文件不存在: ' . $originPath); + } + copy($originPath, $tmpFile); + } + // 解析临时文件 + $ext = strtolower(pathinfo($originPath, PATHINFO_EXTENSION)); + $rows = []; + if (in_array($ext, ['xls', 'xlsx'])) { + // 直接用composer自动加载的PHPExcel + $excel = \PHPExcel_IOFactory::load($tmpFile); + $sheet = $excel->getActiveSheet(); + $data = $sheet->toArray(); + if (count($data) > 1) { + array_shift($data); // 去掉表头 + } + + foreach ($data as $cols) { + $rows[] = [ + 'name' => isset($cols[0]) ? trim($cols[0]) : '', + 'phone' => isset($cols[1]) ? trim($cols[1]) : '', + 'wechatId' => isset($cols[2]) ? trim($cols[2]) : '', + 'source' => isset($cols[3]) ? trim($cols[3]) : '', + 'orderAmount' => isset($cols[4]) ? trim($cols[4]) : '', + 'orderDate' => isset($cols[5]) ? trim($cols[5]) : '', + ]; + } + } elseif ($ext === 'csv') { + $content = file_get_contents($tmpFile); + $lines = preg_split('/\r\n|\r|\n/', $content); + if (count($lines) > 1) { + array_shift($lines); // 去掉表头 + foreach ($lines as $line) { + if (trim($line) === '') continue; + $cols = str_getcsv($line); + if (count($cols) >= 6) { + $rows[] = [ + 'name' => isset($cols[0]) ? trim($cols[0]) : '', + 'phone' => isset($cols[1]) ? trim($cols[1]) : '', + 'wechatId' => isset($cols[2]) ? trim($cols[2]) : '', + 'source' => isset($cols[3]) ? trim($cols[3]) : '', + 'orderAmount' => isset($cols[4]) ? trim($cols[4]) : '', + 'orderDate' => isset($cols[5]) ? trim($cols[5]) : '', + ]; + } + } + } + } else { + unlink($tmpFile); + exit('暂不支持的文件类型: ' . $ext); + } + // 删除临时文件 + unlink($tmpFile); + } + } + + + //电话获客 + if ($params['sceneId'] == 5) { + $rows = Db::name('call_recording') + ->where('companyId', $this->getUserInfo('companyId')) + ->group('phone') + ->field('id,phone') + ->select(); + } + + //群获客 + if ($params['sceneId'] == 7) { + if (!empty($params['wechatGroups']) && is_array($params['wechatGroups'])) { + $rows = Db::name('wechat_group_member')->alias('gm') + ->join('wechat_account wa', 'gm.identifier = wa.wechatId') + ->whereIn('gm.groupId', $params['wechatGroups']) + ->group('gm.identifier') + ->column('wa.id,wa.wechatId,wa.alias,wa.phone'); + } + } + + + if (in_array($params['sceneId'], [2, 5, 7]) && !empty($rows) && is_array($rows)) { + // 1000条为一组进行批量处理 + $batchSize = 1000; + $totalRows = count($rows); + + for ($i = 0; $i < $totalRows; $i += $batchSize) { + $batchRows = array_slice($rows, $i, $batchSize); + if (!empty($batchRows)) { + // 1. 提取当前批次的phone + // 1. 提取当前批次的phone + $phones = []; + foreach ($batchRows as $row) { + if (!empty($row['phone'])) { + $phone = $row['phone']; + } elseif (!empty($row['alias'])) { + $phone = $row['alias']; + } else { + $phone = $row['wechatId']; + } + if (!empty($phone)) { + $phones[] = $phone; + } + } + // 2. 批量查询已存在的phone + $existingPhones = []; + if (!empty($phones)) { + $existing = Db::name('task_customer') + ->where('task_id', $params['planId']) + ->where('phone', 'in', $phones) + ->field('phone') + ->select(); + $existingPhones = array_column($existing, 'phone'); + } + + // 3. 过滤出新数据,批量插入 + $newData = []; + foreach ($batchRows as $row) { + if (!empty($row['phone'])) { + $phone = $row['phone']; + } elseif (!empty($row['alias'])) { + $phone = $row['alias']; + } else { + $phone = $row['wechatId']; + } + if (!empty($phone) && !in_array($phone, $existingPhones)) { + $newData[] = [ + 'task_id' => $params['planId'], + 'name' => !empty($row['name']) ? $row['name'] : '', + 'source' => '场景获客_' . $params['name'] ?? '', + 'phone' => $phone, + 'tags' => json_encode([], JSON_UNESCAPED_UNICODE), + 'siteTags' => json_encode([], JSON_UNESCAPED_UNICODE), + 'createTime' => time(), + ]; + } + } + + // 4. 批量插入新数据 + if (!empty($newData)) { + Db::name('task_customer')->insertAll($newData); + } + } + } + } + + + return ResponseHelper::success(['planId' => $params['planId']], '更新计划任务成功'); + + } catch (\Exception $e) { + // 回滚事务 + Db::rollback(); + throw $e; + } + + } catch (\Exception $e) { + return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500); + } + } + + /** + * 处理分销配置 + * + * @param array $params 请求参数 + * @param int $companyId 公司ID + * @return array 分销配置 + */ + private function processDistributionConfig($params, $companyId) + { + $distributionEnabled = !empty($params['distributionEnabled']) ? true : false; + + $config = [ + 'enabled' => $distributionEnabled, + 'channels' => [], + 'customerRewardAmount' => 0, // 获客奖励金额(分) + 'addFriendRewardAmount' => 0, // 添加奖励金额(分) + ]; + + // 如果未开启分销,直接返回默认配置 + if (!$distributionEnabled) { + return $config; + } + + // 验证渠道ID + $channelIds = $params['distributionChannels'] ?? []; + if (empty($channelIds) || !is_array($channelIds)) { + throw new \Exception('请选择至少一个分销渠道'); + } + + // 查询有效的渠道(只保留存在且已启用的渠道) + $channels = Db::name('distribution_channel') + ->where([ + ['id', 'in', $channelIds], + ['companyId', '=', $companyId], + ['status', '=', 'enabled'], + ['deleteTime', '=', 0] + ]) + ->field('id,code,name') + ->select(); + + // 如果没有有效渠道,才报错 + if (empty($channels)) { + throw new \Exception('所选的分销渠道均不存在或已被禁用,请重新选择'); + } + + // 只保留有效的渠道ID + $config['channels'] = array_column($channels, 'id'); + + // 验证获客奖励金额(元转分) + $customerRewardAmount = isset($params['customerRewardAmount']) ? floatval($params['customerRewardAmount']) : 0; + if ($customerRewardAmount < 0) { + throw new \Exception('获客奖励金额不能为负数'); + } + if ($customerRewardAmount > 0 && !preg_match('/^\d+(\.\d{1,2})?$/', (string)$customerRewardAmount)) { + throw new \Exception('获客奖励金额格式不正确,最多保留2位小数'); + } + $config['customerRewardAmount'] = intval(round($customerRewardAmount * 100)); // 元转分 + + // 验证添加奖励金额(元转分) + $addFriendRewardAmount = isset($params['addFriendRewardAmount']) ? floatval($params['addFriendRewardAmount']) : 0; + if ($addFriendRewardAmount < 0) { + throw new \Exception('添加奖励金额不能为负数'); + } + if ($addFriendRewardAmount > 0 && !preg_match('/^\d+(\.\d{1,2})?$/', (string)$addFriendRewardAmount)) { + throw new \Exception('添加奖励金额格式不正确,最多保留2位小数'); + } + $config['addFriendRewardAmount'] = intval(round($addFriendRewardAmount * 100)); // 元转分 + + return $config; + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php b/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php new file mode 100644 index 0000000..a9b5d4f --- /dev/null +++ b/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php @@ -0,0 +1,403 @@ +config = [ + 'app_id' => Env::get('weChat.appidMiniApp','wx789850448e26c91d'), + 'secret' => Env::get('weChat.secretMiniApp','d18f75b3a3623cb40da05648b08365a1'), + 'response_type' => 'array' + ]; + } + + + public function index() + { + return 'Hello, World!'; + } + + + // 生成小程序码,存客宝-操盘手调用 + public function generateMiniProgramCodeWithScene($taskId = '', $channelId = 0) + { + + if (empty($taskId)) { + return json_encode(['code' => 500, 'data' => '', 'msg' => '任务id不能为空']); + } + + + try { + $app = Factory::miniProgram($this->config); + // scene参数长度限制为32位 + // 如果提供了channelId,格式为:taskId,channelId + // 如果没有channelId,格式为:taskId + if (!empty($channelId) && $channelId > 0) { + $scene = sprintf("%s,%s", $taskId, $channelId); + } else { + $scene = sprintf("%s", $taskId); + } + + // 确保scene长度不超过32位 + if (strlen($scene) > 32) { + $scene = substr($scene, 0, 32); + } + + // 调用接口生成小程序码 + $response = $app->app_code->getUnlimit($scene, [ + 'page' => 'pages/poster/index2', // 必须是已经发布的小程序页面 + 'width' => 430, // 二维码的宽度,默认430 + // 'auto_color' => false, // 自动配置线条颜色 + // 'line_color' => ['r' => 0, 'g' => 0, 'b' => 0], // 颜色设置 + // 'is_hyaline' => false, // 是否需要透明底色 + ]); + // 保存小程序码到文件 + if ($response instanceof StreamResponse) { + // $filename = 'minicode_' . $taskId . '.png'; + // $response->saveAs('path/to/codes', $filename); + // return 'path/to/codes/' . $filename; + + $img = $response->getBody()->getContents();//获取图片二进制流 + $img_base64 = 'data:image/png;base64,' . base64_encode($img);//转化base64 + return json_encode(['code' => 200, 'data' => $img_base64]); + } + } catch (\Exception $e) { + return json_encode(['code' => 500, 'data' => '', 'msg' => $e->getMessage()]); + } + } + + // getPhoneNumber + public function getPhoneNumber() + { + + $taskId = request()->param('id'); + $code = request()->param('code'); + // code 不能为空 + if (!$code) { + return json([ + 'code' => 400, + 'message' => 'code不能为空' + ]); + } + + $task = Db::name('customer_acquisition_task')->where('id', $taskId)->find(); + if (!$task) { + return json([ + 'code' => 400, + 'message' => '任务不存在' + ]); + } + + $app = Factory::miniProgram($this->config); + + $result = $app->phone_number->getUserPhoneNumber($code); + + if ($result['errcode'] == 0 && isset($result['phone_info']['phoneNumber'])) { + + // TODO 拿到手机号之后的后续操作: + // 1. 先写入 ck_traffic_pool 表 identifier mobile 都是 用 phone字段的值 + $trafficPool = Db::name('traffic_pool')->where('identifier', $result['phone_info']['phoneNumber'])->find(); + if (!$trafficPool) { + Db::name('traffic_pool')->insert([ + 'identifier' => $result['phone_info']['phoneNumber'], + 'mobile' => $result['phone_info']['phoneNumber'], + 'createTime' => time() + ]); + } + // 2. 写入 ck_task_customer: 以 task_id ~~identifier~~ phone 为条件,如果存在则忽略,使用类似laravel的firstOrcreate(但我不知道thinkphp5.1里的写法) + // $taskCustomer = Db::name('task_customer')->where('task_id', $taskId)->where('identifier', $result['phone_info']['phoneNumber'])->find(); + $taskCustomer = Db::name('task_customer') + ->where('task_id', $taskId) + ->where('phone', $result['phone_info']['phoneNumber']) + ->find(); + if (!$taskCustomer) { + // 渠道ID(cid),对应 distribution_channel.id + $channelId = intval($this->request->param('cid', 0)); + + $finalChannelId = 0; + if ($channelId > 0) { + // 获取任务信息,解析分销配置 + $sceneConf = json_decode($task['sceneConf'] ?? '[]', true) ?: []; + $distributionConfig = $sceneConf['distribution'] ?? null; + $allowedChannelIds = $distributionConfig['channels'] ?? []; + if (!empty($distributionConfig) && !empty($distributionConfig['enabled']) && in_array($channelId, $allowedChannelIds)) { + // 验证渠道是否存在且正常 + $channel = Db::name('distribution_channel') + ->where([ + ['id', '=', $channelId], + ['companyId', '=', $task['companyId']], + ['status', '=', 'enabled'], + ['deleteTime', '=', 0] + ]) + ->find(); + if ($channel) { + $finalChannelId = $channelId; + } + } + } + + $customerId = Db::name('task_customer')->insertGetId([ + 'task_id' => $taskId, + 'channelId' => $finalChannelId, + // 'identifier' => $result['phone_info']['phoneNumber'], + 'phone' => $result['phone_info']['phoneNumber'], + 'source' => $task['name'], + 'createTime' => time(), + 'tags' => json_encode([]), + 'siteTags' => json_encode([]), + ]); + + // 记录获客奖励(异步处理,不影响主流程) + if ($customerId) { + try { + if ($finalChannelId > 0) { + \app\cunkebao\service\DistributionRewardService::recordCustomerReward( + $taskId, + $customerId, + $result['phone_info']['phoneNumber'], + $finalChannelId + ); + } + } catch (\Exception $e) { + // 记录错误但不影响主流程 + \think\facade\Log::error('记录获客奖励失败:' . $e->getMessage()); + } + } + } + // return $result['phone_info']['phoneNumber']; + return json([ + 'code' => 200, + 'message' => '获取手机号成功', + 'data' => $result['phone_info']['phoneNumber'] + ]); + } else { + // return null; + return json([ + 'code' => 400, + 'message' => '获取手机号失败: ' . $result['errmsg'] ?? '未知错误' + ]); + } + + // return $result; + + } + + + public function decryptphones() + { + + $taskId = request()->param('id'); + $rawInput = trim((string)request()->param('phone', '')); + // 渠道ID(cid),对应 distribution_channel.id + $channelId = intval(request()->param('cid', 0)); + if ($rawInput === '') { + return json([ + 'code' => 400, + 'message' => '手机号或微信号不能为空' + ]); + } + $task = Db::name('customer_acquisition_task')->where('id', $taskId)->find(); + + if (!$task) { + return json([ + 'code' => 400, + 'message' => '任务不存在' + ]); + } + + // 预先根据任务的分销配置校验渠道是否有效(仅当传入了cid时) + $finalChannelId = 0; + if ($channelId > 0) { + $sceneConf = json_decode($task['sceneConf'] ?? '[]', true) ?: []; + $distributionConfig = $sceneConf['distribution'] ?? null; + $allowedChannelIds = $distributionConfig['channels'] ?? []; + if (!empty($distributionConfig) && !empty($distributionConfig['enabled']) && in_array($channelId, $allowedChannelIds)) { + // 验证渠道是否存在且正常 + $channel = Db::name('distribution_channel') + ->where([ + ['id', '=', $channelId], + ['companyId', '=', $task['companyId']], + ['status', '=', 'enabled'], + ['deleteTime', '=', 0] + ]) + ->find(); + if ($channel) { + $finalChannelId = $channelId; + } + } + } + + + $lines = preg_split('/\r\n|\r|\n/', $rawInput); + foreach ($lines as $line) { + $line = trim($line); + if ($line === '') { + continue; + } + $parts = array_map('trim', explode(',', $line, 2)); + $identifier = $parts[0] ?? ''; + $remark = $parts[1] ?? ''; + if ($identifier === '') { + continue; + } + $isPhone = preg_match('/^\+?\d{6,}$/', $identifier); + $trafficPool = Db::name('traffic_pool')->where('identifier', $identifier)->find(); + if (!$trafficPool) { + $insertData = [ + 'identifier' => $identifier, + 'createTime' => time() + ]; + if ($isPhone) { + $insertData['mobile'] = $identifier; + } else { + $insertData['wechatId'] = $identifier; + } + Db::name('traffic_pool')->insert($insertData); + } else { + $updates = []; + if ($isPhone && empty($trafficPool['mobile'])) { + $updates['mobile'] = $identifier; + } + if (!$isPhone && empty($trafficPool['wechatId'])) { + $updates['wechatId'] = $identifier; + } + if (!empty($updates)) { + $updates['updateTime'] = time(); + Db::name('traffic_pool')->where('id', $trafficPool['id'])->update($updates); + } + } + + $taskCustomer = Db::name('task_customer') + ->where('task_id', $taskId) + ->where('phone', $identifier) + ->find(); + if (empty($taskCustomer)) { + $insertCustomer = [ + 'task_id' => $taskId, + 'channelId' => $finalChannelId, // 记录本次导入归属的分销渠道(如有) + 'phone' => $identifier, + 'source' => $task['name'], + 'createTime'=> time(), + 'tags' => json_encode([]), + 'siteTags' => json_encode([]), + ]; + if ($remark !== '') { + $insertCustomer['remark'] = $remark; + } + // 使用 insertGetId 以便在需要时记录获客奖励 + $customerId = Db::name('task_customer')->insertGetId($insertCustomer); + + // 表单录入成功即视为一次获客: + // 仅在存在有效渠道ID时,记录获客奖励(谁的cid谁获客) + if (!empty($customerId) && $finalChannelId > 0) { + try { + \app\cunkebao\service\DistributionRewardService::recordCustomerReward( + $taskId, + $customerId, + $identifier, + $finalChannelId + ); + } catch (\Exception $e) { + // 记录错误但不影响主流程 + \think\facade\Log::error('记录获客奖励失败:' . $e->getMessage()); + } + } + } elseif ($remark !== '' && $taskCustomer['remark'] !== $remark) { + Db::name('task_customer') + ->where('id', $taskCustomer['id']) + ->update([ + 'remark' => $remark, + 'updateTime' => time() + ]); + } + + } + + // return $phone; + return json([ + 'code' => 200, + 'message' => '操作成功', + ]); + + } + + // return $result; + + +// todo 获取海报获客任务的任务/海报数据 -- 表还没设计好,不急 ck_customer_acquisition_task + public + function getPosterTaskData() + { + $id = request()->param('id'); + $task = Db::name('customer_acquisition_task') + ->where(['id' => $id, 'deleteTime' => 0]) + ->field('id,name,sceneConf,status') + ->find(); + if (!$task) { + return json([ + 'code' => 400, + 'message' => '任务不存在' + ]); + } + + if ($task['status'] == 0) { + return json([ + 'code' => 400, + 'message' => '任务已结束' + ]); + } + + $sceneConf = json_decode($task['sceneConf'], true); + + if (isset($sceneConf['posters']['url'])) { + $posterUrl = !empty($sceneConf['posters']['url']); + } else { + $posterUrl = 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%92%A8%E8%AF%A2-FTiyAMAPop2g9LvjLOLDz0VwPg3KVu.gif'; + } + + + if (isset($sceneConf['tips'])) { + $sTip = $sceneConf['tips']; + } else { + $sTip = ''; + } + + unset($task['sceneConf']); + $task['sTip'] = $sTip; + + $data = [ + 'id' => $task['id'], + 'name' => $task['name'], + 'poster' => ['sUrl' => $posterUrl], + 'task' => $task, + ]; + + + // todo 只需 返回 poster_url success_tip + return json([ + 'code' => 200, + 'message' => '获取海报获客任务数据成功', + 'data' => $data + ]); + } + + +} \ No newline at end of file diff --git a/application/cunkebao/controller/traffic/GetConvertedListWithInCompanyV1Controller.php b/application/cunkebao/controller/traffic/GetConvertedListWithInCompanyV1Controller.php new file mode 100644 index 0000000..b152c8d --- /dev/null +++ b/application/cunkebao/controller/traffic/GetConvertedListWithInCompanyV1Controller.php @@ -0,0 +1,112 @@ +items() as $item) { + $item->tags = json_decode($item->tags); + + array_push($resultSets, $item->toArray()); + } + + return $resultSets; + } + + /** + * 构建查询条件 + * + * @param array $params + * @return array + */ + protected function makeWhere(array $params = []): array + { + if (!empty($keyword = $this->request->param('keyword'))) { + $where[] = ['exp', "w.alias LIKE '%{$keyword}%' OR w.nickname LIKE '%{$keyword}%'"]; + } + + // 来源的筛选 + if ($fromd = $this->request->param('fromd')) { + $where['s.fromd'] = $fromd; + } + + $where['s.companyId'] = $this->getUserInfo('companyId'); + $where['s.status'] = TrafficSourceModel::STATUS_PASSED; + + return array_merge($where, $params); + } + + /** + * 获取流量池列表 + * + * @param array $where + * @return \think\Paginator + */ + protected function getPoolListByCompanyId(array $where): \think\Paginator + { + $query = TrafficSourceModel::alias('s') + ->field( + [ + 'w.id', 'w.nickname', 'w.avatar', + 'CASE WHEN w.alias IS NULL OR w.alias = "" THEN w.wechatId ELSE w.alias END AS wechatId', + 's.fromd', + 'f.tags', 'f.createTime', TrafficSourceModel::STATUS_PASSED . ' status' + ] + ) + ->join('traffic_pool p', 'p.identifier=s.identifier') + ->join('wechat_account w', 'p.wechatId=w.wechatId') + ->join('wechat_friendship f', 'w.wechatId=f.wechatId and f.deleteTime=0') + ->order('s.id desc'); + + foreach ($where as $key => $value) { + if (is_numeric($key) && is_array($value) && isset($value[0]) && $value[0] === 'exp') { + $query->whereExp('', $value[1]); + continue; + } + + $query->where($key, $value); + } + + return $query->paginate($this->request->param('limit/d', 10), false, ['page' => $this->request->param('page/d', 1)]); + } + + /** + * 获取流量池列表 + * + * @return \think\response\Json + */ + public function index() + { + try { + $result = $this->getPoolListByCompanyId( $this->makeWhere() ); + + return ResponseHelper::success( + [ + 'list' => $this->makeResultedSet($result), + 'total' => $result->total(), + ] + ); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/traffic/GetPoolStatisticsV1Controller.php b/application/cunkebao/controller/traffic/GetPoolStatisticsV1Controller.php new file mode 100644 index 0000000..eed3f48 --- /dev/null +++ b/application/cunkebao/controller/traffic/GetPoolStatisticsV1Controller.php @@ -0,0 +1,70 @@ + $this->getUserInfo('companyId'), + 'status' => TrafficSourceModel::STATUS_PASSED, + ] + ) + ->whereBetween('updateTime', + [ + strtotime(date('Y-m-d 00:00:00')), + strtotime(date('Y-m-d 23:59:59')) + ] + ) + ->count('*'); + } + + /** + * 获取流量池总数 + * + * @return int + * @throws \Exception + */ + protected function getTotalCount(): int + { + return TrafficSourceModel::where( + [ + 'companyId' => $this->getUserInfo('companyId') + ] + ) + ->count('*'); + } + + /** + * 获取流量池数据统计 + * + * @return \think\response\Json + */ + public function index() + { + try { + return ResponseHelper::success( + [ + 'totalCount' => $this->getTotalCount(), + 'todayAddCount' => $this->getTodayAddedCount(), + ] + ); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/traffic/GetPotentialListWithInCompanyV1Controller.php b/application/cunkebao/controller/traffic/GetPotentialListWithInCompanyV1Controller.php new file mode 100644 index 0000000..434a128 --- /dev/null +++ b/application/cunkebao/controller/traffic/GetPotentialListWithInCompanyV1Controller.php @@ -0,0 +1,655 @@ +request->param('keyword', ''); + $device = $this->request->param('deviceId'); + $status = $this->request->param('addStatus', ''); + $taskId = $this->request->param('taskId', ''); + $packageId = $this->request->param('packageId', ''); + $where = []; + if (!empty($keyword)) { + $where[] = ['p.identifier|wa.nickname|wa.phone|wa.wechatId|wa.alias', 'like', '%' . $keyword . '%']; + } + + // 状态筛选 + if (!empty($status)) { + if ($status == 1) { + $where[] = ['s.status', '=', 4]; + } elseif ($status == 2) { + $where[] = ['s.status', '=', 0]; + } elseif ($status == -1) { + $where[] = ['s.status', '=', 2]; + } elseif ($status == 3) { + $where[] = ['s.status', '=', 2]; + } + } + + // 来源的筛选 + if ($packageId) { + if ($packageId != -1) { + $where[] = ['tsp.id', '=', $packageId]; + } else { + $where[] = ['tsp.id', '=', null]; + } + + } + + if (!empty($device)) { +// $where[] = ['d.deviceId', '=', $device]; + } + + if (!empty($taskId)) { + //$where[] = ['t.sceneId', '=', $taskId]; + } + $where[] = ['s.companyId', '=', $this->getUserInfo('companyId')]; + + return $where; + } + + /** + * 获取流量池列表 + * + * @param array $where + * @return \think\Paginator + */ + protected function getPoolListByCompanyId(array $where, $isPage = true) + { + $query = TrafficPoolModel::alias('p') + ->field( + [ + 'p.id', 'p.identifier', 'p.mobile', 'p.wechatId', 'p.identifier', + 's.fromd', 's.status', 's.createTime', 's.companyId', 's.sourceId', 's.type', + 'wa.nickname', 'wa.avatar', 'wa.gender', 'wa.phone', 'wa.alias' + ] + ) + ->join('traffic_source s', 'p.identifier=s.identifier') + ->join('wechat_account wa', 'p.identifier=wa.wechatId', 'left') + ->join('traffic_source_package_item tspi', 'p.identifier = tspi.identifier AND s.companyId = tspi.companyId', 'left') + ->join('traffic_source_package tsp', 'tspi.packageId=tsp.id', 'left') + ->join('device_wechat_login d', 's.sourceId=d.wechatId', 'left') + ->where($where); + + + $result = $query->order('p.id DESC,s.id DESC')->group('p.identifier'); + + if ($isPage) { + $result = $query->paginate($this->request->param('limit/d', 10), false, ['page' => $this->request->param('page/d', 1)]); + $list = $result->items(); + $total = $result->total(); + } else { + $list = $result->select(); + $total = ''; + } + + + if ($isPage) { + foreach ($list as &$item) { + //流量池筛选 + $package = Db::name('traffic_source_package_item')->alias('tspi') + ->join('traffic_source_package p', 'tspi.packageId=p.id AND tspi.companyId=p.companyId') + ->where(['tspi.identifier' => $item->identifier]) + ->whereIn('tspi.companyId', [0, $item->companyId]) + ->column('p.name'); + $item['packages'] = $package; + if ($item->type == 1) { + $tag = Db::name('wechat_friendship')->where(['wechatId' => $item->wechatId])->column('tags'); + $tags = []; + foreach ($tag as $k => $v) { + $v = json_decode($v, true); + if (!empty($v)) { + $tags = array_merge($tags, $v); + } + } + $item['tags'] = $tags; + } + + } + } + unset($item); + $data = ['list' => $list, 'total' => $total]; + return json_encode($data, JSON_UNESCAPED_UNICODE); + } + + /** + * 获取流量池列表 + * + * @return \think\response\Json + */ + public function index() + { + try { + $result = $this->getPoolListByCompanyId($this->makeWhere()); + $result = json_decode($result, true); + return ResponseHelper::success( + [ + 'list' => $result['list'], + 'total' => $result['total'], + ] + ); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } + + public function getUser() + { + + $wechatId = $this->request->param('wechatId', ''); + $companyId = $this->getUserInfo('companyId'); + + if (empty($wechatId)) { + return json_encode(['code' => 500, 'msg' => '微信id不能为空']); + } + + $total = [ + 'msg' => 0, + 'money' => 0, + 'isFriend' => false, + 'percentage' => '0.00%', + ]; + + + $data = TrafficPoolModel::alias('p') + ->field(['p.id', 'p.identifier', 'p.wechatId', + 'wa.nickname', 'wa.avatar', 'wa.gender', 'wa.phone', 'wa.alias']) + ->join('wechat_account wa', 'p.identifier=wa.wechatId', 'left') + ->order('p.id DESC') + ->where(['p.identifier' => $wechatId]) + ->group('p.identifier') + ->find(); + $data['lastMsgTime'] = ''; + + //来源 + $source = Db::name('traffic_source')->alias('ts') + ->field(['wa.nickname', 'wa.avatar', 'wa.gender', 'wa.phone', 'wa.wechatId', 'wa.alias', + 'ts.createTime', + 'wf.id as friendId', 'wf.wechatAccountId']) + ->join('wechat_account wa', 'ts.sourceId=wa.wechatId', 'left') + ->join(['s2_wechat_friend' => 'wf'], 'wa.wechatId=wf.ownerWechatId', 'left') + ->where(['ts.companyId' => $companyId, 'ts.identifier' => $data['identifier'], 'wf.wechatId' => $data['wechatId']]) + ->order('ts.createTime DESC') + ->select(); + + $wechatFriendId = []; + if (!empty($source)) { + $total['isFriend'] = true; + foreach ($source as &$v) { + $wechatFriendId[] = $v['friendId']; + //最后消息 + $v['createTime'] = date('Y-m-d H:i:s', $v['createTime']); + $lastMsgTime = Db::table('s2_wechat_message') + ->where(['wechatFriendId' => $v['friendId'], 'wechatAccountId' => $v['wechatAccountId']]) + ->value('wechatTime'); + $v['lastMsgTime'] = !empty($lastMsgTime) ? date('Y-m-d H:i:s', $lastMsgTime) : ''; + + //设备信息 + $device = Db::name('device_wechat_login')->alias('dwl') + ->join('device d', 'd.id=dwl.deviceId') + ->where(['dwl.wechatId' => $v['wechatId']]) + ->field('d.id,d.memo,d.imei,d.brand,d.extra,d.alive') + ->order('dwl.id DESC') + ->find(); + $extra = json_decode($device['extra'], true); + unset($device['extra']); + $device['address'] = !empty($extra['address']) ? $extra['address'] : ''; + $v['device'] = $device; + } + unset($v); + } + $data['source'] = $source; + + + //流量池 + $package = Db::name('traffic_source_package_item')->alias('tspi') + ->join('traffic_source_package p', 'tspi.packageId=p.id AND tspi.companyId=p.companyId') + ->where(['tspi.companyId' => $companyId, 'tspi.identifier' => $data['identifier']]) + ->column('p.name'); + $package2 = Db::name('traffic_source_package_item')->alias('tspi') + ->join('traffic_source_package p', 'tspi.packageId=p.id') + ->where(['tspi.companyId' => $companyId, 'tspi.identifier' => $data['identifier']]) + ->column('p.name'); + $packages = array_merge($package, $package2); + $data['packages'] = $packages; + + + if (!empty($wechatFriendId)) { + //消息统计 + $msgTotal = Db::table('s2_wechat_message') + ->whereIn('wechatFriendId', $wechatFriendId) + ->count(); + $total['msg'] = $msgTotal; + + //金额计算 + $money = Db::table('s2_wechat_message') + ->whereIn('wechatFriendId', $wechatFriendId) + ->where(['isSend' => 1, 'msgType' => 419430449]) + ->select(); + if (!empty($money)) { + foreach ($money as $v) { + $content = json_decode($v['content'], true); + if ($content['paysubtype'] == 1) { + $number = number_format(str_replace("¥", "", $content['feedesc']), 2); + $floatValue = floatval($number); + $total['money'] += $floatValue; + } + } + } + } + + $taskNum = Db::name('task_customer')->alias('tc') + ->join('customer_acquisition_task t', 'tc.task_id=t.id') + ->where(['t.companyId' => $companyId, 't.deleteTime' => 0]) + ->whereIn('tc.phone', [$data['phone'], $data['wechatId'], $data['alias']]) + ->count(); + + $passNum = Db::name('task_customer')->alias('tc') + ->join('customer_acquisition_task t', 'tc.task_id=t.id') + ->where(['t.companyId' => $companyId, 't.deleteTime' => 0, 'tc.status' => 4]) + ->whereIn('tc.phone', [$data['phone'], $data['wechatId'], $data['alias']]) + ->count(); + + if (!empty($taskNum) && !empty($passNum)) { + $percentage = number_format(($taskNum / $passNum) * 100, 2); + $total['percentage'] = $percentage; + } + + + $data['total'] = $total; + $data['rmm'] = [ + 'r' => 0, + 'f' => 0, + 'm' => 0, + ]; + return ResponseHelper::success($data); + } + + + /** + * 用户旅程 + * @return false|string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function getUserJourney() + { + $page = $this->request->param('page', 1); + $pageSize = $this->request->param('pageSize', 10); + $userId = $this->request->param('userId', ''); + if (empty($userId)) { + return json_encode(['code' => 500, 'msg' => '用户id不能为空']); + } + + $query = Db::name('user_portrait') + ->field('id,type,trafficPoolId,remark,count,createTime,updateTime') + ->where(['trafficPoolId' => $userId]); + + $total = $query->count(); + + $list = $query->order('createTime desc') + ->page($page, $pageSize) + ->select(); + + + foreach ($list as $k => $v) { + $list[$k]['createTime'] = date('Y-m-d H:i:s', $v['createTime']); + $list[$k]['updateTime'] = date('Y-m-d H:i:s', $v['updateTime']); + } + return ResponseHelper::success(['list' => $list, 'total' => $total]); + + } + + + public function getUserTags() + { + $userId = $this->request->param('userId', ''); + $companyId = $this->getUserInfo('companyId'); + if (empty($userId)) { + return json_encode(['code' => 500, 'msg' => '用户id不能为空']); + } + $data = Db::name('traffic_pool')->alias('tp') + ->join('wechat_friendship f', 'tp.wechatId=f.wechatId AND f.companyId=' . $companyId, 'left') + ->join(['s2_wechat_friend' => 'wf'], 'f.wechatId=wf.wechatId', 'left') + ->where(['tp.id' => $userId]) + ->order('tp.createTime desc') + ->column('wf.id,wf.labels,wf.siteLabels'); + if (empty($data)) { + return ResponseHelper::success(['wechat' => [], 'siteLabels' => []]); + } + + + $tags = []; + $siteLabels = []; + foreach ($data as $k => $v) { + $tag = json_decode($v['labels'], true); + $tag2 = json_decode($v['siteLabels'], true); + if (!empty($tag)) { + $tags = array_merge($tags, $tag); + } + if (!empty($tag2)) { + $siteLabels = array_merge($siteLabels, $tag2); + } + } + $tags = array_unique($tags); + $tags = array_values($tags); + $siteLabels = array_unique($siteLabels); + $siteLabels = array_values($siteLabels); + return ResponseHelper::success(['wechat' => $tags, 'siteLabels' => $siteLabels]); + } + + + + + + public function addPackage() + { + try { + $type = $this->request->param('type', ''); + $addPackageId = $this->request->param('addPackageId', ''); + $packageName = $this->request->param('packageName', ''); + $userIds = $this->request->param('userIds', []); + $tableFile = $this->request->param('tableFile', ''); + + + $companyId = $this->getUserInfo('companyId'); + $userId = $this->getUserInfo('id'); + if (empty($addPackageId) && empty($packageName)) { + return ResponseHelper::error('存储的流量池不能为空'); + } + + if (empty($type)) { + return ResponseHelper::error('请选择类型'); + } + + if (!empty($addPackageId)) { + $package = Db::name('traffic_source_package') + ->where(['id' => $addPackageId, 'isDel' => 0]) + ->whereIn('companyId', [$companyId, 0]) + ->field('id,name') + ->find(); + if (empty($package)) { + return ResponseHelper::error('该流量池不存在'); + } + $packageId = $package['id']; + } else { + $package = Db::name('traffic_source_package') + ->where(['isDel' => 0, 'name' => $packageName]) + ->whereIn('companyId', [$companyId, 0]) + ->field('id,name') + ->find(); + if (!empty($package)) { + return ResponseHelper::error('该流量池名称已存在'); + } + $packageId = Db::name('traffic_source_package')->insertGetId([ + 'userId' => $userId, + 'companyId' => $companyId, + 'name' => $packageName, + 'matchingRules' => json_encode($this->makeWhere()), + 'createTime' => time(), + 'isDel' => 0, + ]); + } + + + if ($type == 1) { + $result = $this->getPoolListByCompanyId($this->makeWhere(), false); + $result = json_decode($result, true); + $result = array_column($result['list'], 'identifier'); + } elseif ($type == 2) { + if (empty($packageId)) { + return ResponseHelper::error('选择的用户'); + } + //================== 表格数据处理 ================== + if (!is_array($userIds)) { + return ResponseHelper::error('选择的用户类型错误'); + } + $result = Db::name('traffic_pool')->alias('tp') + ->join('traffic_source tc', 'tp.identifier=tc.identifier') + ->whereIn('tp.id', $userIds) + ->where(['companyId' => $companyId]) + ->group('tp.identifier') + ->column('tc.identifier'); + } else { + /*if (empty($tableFile)){ + return ResponseHelper::error('请上传用户文件'); + } + + // 先下载到本地临时文件,再分析,最后删除 + $originPath = $tableFile; + $tmpFile = tempnam(sys_get_temp_dir(), 'user_'); + // 判断是否为远程文件 + if (preg_match('/^https?:\/\//i', $originPath)) { + // 远程URL,下载到本地 + $fileContent = file_get_contents($originPath); + if ($fileContent === false) { + exit('远程文件下载失败: ' . $originPath); + } + file_put_contents($tmpFile, $fileContent); + } else { + // 本地文件,直接copy + if (!file_exists($originPath)) { + exit('文件不存在: ' . $originPath); + } + copy($originPath, $tmpFile); + } + // 解析临时文件 + $ext = strtolower(pathinfo($originPath, PATHINFO_EXTENSION)); + $rows = []; + if (in_array($ext, ['xls', 'xlsx'])) { + // 直接用composer自动加载的PHPExcel + $excel = \PHPExcel_IOFactory::load($tmpFile); + $sheet = $excel->getActiveSheet(); + $data = $sheet->toArray(); + if (count($data) > 1) { + array_shift($data); // 去掉表头 + } + + foreach ($data as $cols) { + $rows[] = [ + 'name' => isset($cols[0]) ? trim($cols[0]) : '', + 'phone' => isset($cols[1]) ? trim($cols[1]) : '', + 'source' => isset($cols[2]) ? trim($cols[2]) : '', + ]; + } + } elseif ($ext === 'csv') { + $content = file_get_contents($tmpFile); + $lines = preg_split('/\r\n|\r|\n/', $content); + if (count($lines) > 1) { + array_shift($lines); // 去掉表头 + foreach ($lines as $line) { + if (trim($line) === '') continue; + $cols = str_getcsv($line); + if (count($cols) >= 6) { + $rows[] = [ + 'name' => isset($cols[0]) ? trim($cols[0]) : '', + 'phone' => isset($cols[1]) ? trim($cols[1]) : '', + 'source' => isset($cols[2]) ? trim($cols[2]) : '', + ]; + } + } + } + } else { + unlink($tmpFile); + exit('暂不支持的文件类型: ' . $ext); + } + // 删除临时文件 + unlink($tmpFile);*/ + //================== 表格数据处理 ================== + } + + $rows = [ + ['name' => '张三', 'phone' => '18883458888', 'source' => '234'], + ['name' => '李四', 'phone' => '18878988889', 'source' => '456'], + ]; + + + if (in_array($type, [1, 2])) { + // 1000条为一组进行批量处理 + $batchSize = 1000; + $totalRows = count($result); + + for ($i = 0; $i < $totalRows; $i += $batchSize) { + $batchRows = array_slice($result, $i, $batchSize); + if (!empty($batchRows)) { + // 2. 批量查询已存在的手机 + $existing = Db::name('traffic_source_package_item') + ->where(['companyId' => $companyId, 'packageId' => $packageId]) + ->whereIn('identifier', $batchRows) + ->field('identifier') + ->select(); + $existingPhones = array_column($existing, 'identifier'); + // 3. 过滤出新数据,批量插入 + $newData = []; + foreach ($batchRows as $row) { + if (!in_array($row, $existingPhones)) { + $newData[] = [ + 'packageId' => $packageId, + 'companyId' => $companyId, + 'identifier' => $row, + 'createTime' => time(), + ]; + } + } + // 4. 批量插入新数据 + if (!empty($newData)) { + Db::name('traffic_source_package_item')->insertAll($newData); + } + } + } + } else { + // 1000条为一组进行批量处理 + $batchSize = 1000; + $totalRows = count($rows); + + try { + for ($i = 0; $i < $totalRows; $i += $batchSize) { + Db::startTrans(); + $batchRows = array_slice($rows, $i, $batchSize); + if (!empty($batchRows)) { + $identifiers = array_column($batchRows, 'phone'); + //流量池处理 + $existing = Db::name('traffic_pool') + ->whereIn('identifier', $identifiers) + ->column('identifier'); + + $newData = []; + foreach ($batchRows as $row) { + if (!in_array($row['phone'], $existing)) { + $newData[] = [ + 'identifier' => $row['phone'], + 'mobile' => $row['phone'], + 'createTime' => time(), + ]; + } + } + if (!empty($newData)) { + Db::name('traffic_pool')->insertAll($newData); + } + + //流量池来源处理 + $newData2 = []; + $existing2 = Db::name('traffic_source') + ->where(['companyId' => $companyId]) + ->whereIn('identifier', $identifiers) + ->column('identifier'); + foreach ($batchRows as $row) { + if (!in_array($row['phone'], $existing2)) { + $newData2[] = [ + 'type' => 0, + 'name' => $row['name'], + 'identifier' => $row['phone'], + 'fromd' => $row['source'], + 'companyId' => $companyId, + 'createTime' => time(), + 'updateTime' => time(), + ]; + } + } + if (!empty($newData2)) { + Db::name('traffic_source')->insertAll($newData2); + } + + //流量池包数据处理 + $newData3 = []; + $existing3 = Db::name('traffic_source_package_item') + ->where(['companyId' => $companyId, 'packageId' => $packageId]) + ->whereIn('identifier', $identifiers) + ->field('identifier') + ->select(); + foreach ($batchRows as $row) { + if (!in_array($row['phone'], $existing3)) { + $newData3[] = [ + 'packageId' => $packageId, + 'companyId' => $companyId, + 'identifier' => $row['phone'], + 'createTime' => time(), + ]; + } + } + if (!empty($newData3)) { + Db::name('traffic_source_package_item')->insertAll($newData3); + } + + Db::commit(); + } + } + } catch (\Exception $e) { + DB::rollback(); + } + } + + + return ResponseHelper::success('添加成功'); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } + + + /* public function editUserTags() + { + $userId = $this->request->param('userId', ''); + if (empty($userId)) { + return json_encode(['code' => 500, 'msg' => '用户id不能为空']); + } + $tags = $this->request->param('tags', []); + $tags = $this->request->param('tags', []); + $isWechat = $this->request->param('isWechat', false); + $companyId = $this->getUserInfo('companyId'); + + $friend = Db::name('traffic_pool')->alias('tp') + ->join('wechat_friendship f', 'tp.wechatId=f.wechatId AND f.companyId='.$companyId, 'left') + ->join(['s2_wechat_friend' => 'wf'], 'f.wechatId=wf.wechatId', 'left') + ->where(['tp.id' => $userId]) + ->order('tp.createTime desc') + ->column('wf.id,wf.accountId,wf.labels,wf.siteLabels'); + if (empty($data)) { + return ResponseHelper::error('该用户不存在'); + } + + + }*/ + + +} \ No newline at end of file diff --git a/application/cunkebao/controller/traffic/GetPotentialTypeSectionV1Controller.php b/application/cunkebao/controller/traffic/GetPotentialTypeSectionV1Controller.php new file mode 100644 index 0000000..c1ccfb4 --- /dev/null +++ b/application/cunkebao/controller/traffic/GetPotentialTypeSectionV1Controller.php @@ -0,0 +1,62 @@ + TrafficSourceModel::STATUS_PENDING, + 'name' => '待处理' + ], + [ + 'id' => TrafficSourceModel::STATUS_WORKING, + 'name' => '处理中' + ], + [ + 'id' => TrafficSourceModel::STATUS_REFUSED, + 'name' => '已拒绝' + ], + [ + 'id' => TrafficSourceModel::STATUS_EXPIRED, + 'name' => '已过期' + ], + [ + 'id' => TrafficSourceModel::STATUS_CANCELED, + 'name' => '已取消' + ] + ]; + } + + /** + * 获取流量池状态筛选列表 + * + * @return \think\response\Json + */ + public function index() + { + try { + return ResponseHelper::success( + $this->getTypeSectionCols() + ); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/traffic/GetTrafficSourceSectionV1Controller.php b/application/cunkebao/controller/traffic/GetTrafficSourceSectionV1Controller.php new file mode 100644 index 0000000..273e395 --- /dev/null +++ b/application/cunkebao/controller/traffic/GetTrafficSourceSectionV1Controller.php @@ -0,0 +1,45 @@ + $this->getUserInfo('companyId') + ] + ) + ->field('fromd name,id')->group('fromd')->select()->toArray(); + } + + /** + * 获取流量来源筛选列表 + * + * @return \think\response\Json + */ + public function index() + { + try { + return ResponseHelper::success( + $this->getSourceSectionCols() + ); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/wechat/GetWechatController.php b/application/cunkebao/controller/wechat/GetWechatController.php new file mode 100644 index 0000000..23a4b60 --- /dev/null +++ b/application/cunkebao/controller/wechat/GetWechatController.php @@ -0,0 +1,189 @@ +WechatCustomerModel)) { + $this->WechatCustomerModel = WechatCustomerModel::where( + [ + 'wechatId' => $wechatId, + 'companyId' => $this->getUserInfo('companyId') + ] + ) + ->find(); + } + + return $this->WechatCustomerModel; + } + + + /** + * 获取昨日聊天次数 + * + * @param WechatCustomerModel $customer + * @return int + */ + protected function getChatTimesPerDay(?WechatCustomerModel $customer): int + { + return $customer->activity->yesterdayMsgCount ?? 0; + } + + /** + * 总聊天数量 + * + * @param WechatCustomerModel $customer + * @return int + */ + protected function getChatTimesTotal(?WechatCustomerModel $customer): int + { + return $customer->activity->totalMsgCount ?? 0; + } + + /** + * 计算活跃程度(根据消息数) + * + * @param string $wechatId + * @return string + */ + protected function getActivityLevel(string $wechatId): array + { + $customer = $this->getWechatCustomerModel($wechatId); + + return [ + 'allTimes' => $this->getChatTimesTotal($customer), + 'dayTimes' => $this->getChatTimesPerDay($customer), + ]; + } + + /** + * 获取限制记录 + * + * @param string $wechatId + * @return array + */ + protected function getRestrict(string $wechatId): array + { + return WechatRestrictsModel::alias('r') + ->field( + [ + 'r.id', 'r.restrictTime date', 'r.level', 'r.reason' + ] + ) + ->where('r.wechatId', $wechatId)->select() + ->toArray(); + } + + /** + * 获取账号权重 + * + * @param string $wechatId + * @return array + */ + protected function getAccountWeight(string $wechatId): array + { + $customer = $this->getWechatCustomerModel($wechatId); + $seeders = $customer ? (array)$customer->weight : array(); + + // 严谨返回 + return ArrHelper::getValue('ageWeight,activityWeigth,restrictWeight,realNameWeight,scope', $seeders, 0); + } + + /** + * 获取当日最高添加好友记录 + * + * @param string $wechatId + * @return int + * @throws \Exception + */ + protected function getAccountWeightAddLimit(string $wechatId): int + { + return $this->getWechatCustomerModel($wechatId)->weight->addLimit ?? 20; + } + + /** + * 计算今日新增好友数量 + * + * @param string $ownerWechatId + * @return int + */ + protected function getTodayNewFriendCount(string $ownerWechatId): int + { + + return Db::table('s2_friend_task') + ->where('wechatId',$ownerWechatId) + ->whereBetween('createTime', [strtotime(date('Y-m-d 00:00:00')), strtotime(date('Y-m-d 23:59:59'))]) + ->count(); + } + + /** + * 获取账号加友统计数据. + * + * @param string $wechatId + * @return array + */ + protected function getStatistics(string $wechatId): array + { + return [ + 'todayAdded' => $this->getTodayNewFriendCount($wechatId), + 'addLimit' => $this->getAccountWeightAddLimit($wechatId) + ]; + } + + + + + public function getWechatInfo() + { + $wechatId = $this->request->param('wechatId',''); + if (empty($wechatId)) { + return ResponseHelper::error('微信id不能为空'); + } + $userInfo = Db::name('wechat_customer')->alias('wc') + ->join('wechat_account wa','wc.wechatId = wa.wechatId') + ->where(['wc.wechatId' => $wechatId]) + ->field('wc.*,wa.nickname,wa.alias,wa.avatar,wa.gender') + ->find(); + + if (empty($userInfo)){ + return ResponseHelper::error('该微信不存在'); + } + $accountAge= !empty($userInfo['createTime']) ? date('Y-m-d H:i:s',$userInfo['createTime']) : date('Y-m-d H:i:s'); + unset($userInfo['basic'],$userInfo['companyId'],$userInfo['createTime'],$userInfo['updateTime'],$userInfo['id']); + + $userInfo['weight'] = json_decode($userInfo['weight'],true); + $userInfo['activity'] = json_decode($userInfo['activity'],true); + $userInfo['friendShip'] = json_decode($userInfo['friendShip'],true); + + + $newData = [ + 'userInfo' => $userInfo, + 'accountAge' => $accountAge, + 'activityLevel' => $this->getActivityLevel($wechatId), + 'accountWeight' => $this->getAccountWeight($wechatId), + 'statistics' => $this->getStatistics($wechatId), +// 'restrictions' => $this->getRestrict($wechatId), + ]; + + + + return ResponseHelper::success($newData); + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/wechat/GetWechatMomentsV1Controller.php b/application/cunkebao/controller/wechat/GetWechatMomentsV1Controller.php new file mode 100644 index 0000000..32dae25 --- /dev/null +++ b/application/cunkebao/controller/wechat/GetWechatMomentsV1Controller.php @@ -0,0 +1,364 @@ +getUserInfo('companyId')) + ->column('id'); + } + + /** + * 非主操盘手仅可查看分配到的设备 + * + * @return array + */ + protected function getUserDevicesId(): array + { + return DeviceUserModel::where([ + 'userId' => $this->getUserInfo('id'), + 'companyId' => $this->getUserInfo('companyId'), + ])->column('deviceId'); + } + + /** + * 获取当前用户可访问的设备ID + * + * @return array + */ + protected function getDevicesId(): array + { + return ($this->getUserInfo('isAdmin') == UserModel::ADMIN_STP) + ? $this->getCompanyDevicesId() + : $this->getUserDevicesId(); + } + + /** + * 获取用户可访问的微信ID集合 + * + * @return array + * @throws \Exception + */ + protected function getAccessibleWechatIds(): array + { + $deviceIds = $this->getDevicesId(); + if (empty($deviceIds)) { + throw new \Exception('暂无可用设备', 200); + } + + return DeviceWechatLoginModel::distinct(true) + ->where('companyId', $this->getUserInfo('companyId')) + ->whereIn('deviceId', $deviceIds) + ->column('wechatId'); + } + + /** + * 查看朋友圈列表 + * + * @return \think\response\Json + */ + public function index() + { + try { + $wechatId = $this->request->param('wechatId/s', ''); + if (empty($wechatId)) { + return ResponseHelper::error('wechatId不能为空'); + } + + // 权限校验:只能查看当前账号可访问的微信 + $accessibleWechatIds = $this->getAccessibleWechatIds(); + if (!in_array($wechatId, $accessibleWechatIds, true)) { + return ResponseHelper::error('无权查看该微信的朋友圈', 403); + } + + // 获取对应的微信账号ID + $accountId = Db::table('s2_wechat_account') + ->where('wechatId', $wechatId) + ->value('id'); + + if (empty($accountId)) { + return ResponseHelper::error('微信账号不存在或尚未同步', 404); + } + + $query = Db::table('s2_wechat_moments') + ->where('wechatAccountId', $accountId) + ->where('userName', $wechatId); + + // 关键词搜索 + if ($keyword = trim((string)$this->request->param('keyword', ''))) { + $query->whereLike('content', '%' . $keyword . '%'); + } + + // 类型筛选 + $type = $this->request->param('type', ''); + if ($type !== '' && $type !== null) { + $query->where('type', (int)$type); + } + + // 时间筛选 + $startTime = $this->request->param('startTime', ''); + $endTime = $this->request->param('endTime', ''); + if ($startTime || $endTime) { + $start = $startTime ? strtotime($startTime) : 0; + $end = $endTime ? strtotime($endTime) : time(); + if ($start && $end && $end < $start) { + return ResponseHelper::error('结束时间不能早于开始时间'); + } + $query->whereBetween('createTime', [$start ?: 0, $end ?: time()]); + } + + $page = (int)$this->request->param('page', 1); + $limit = (int)$this->request->param('limit', 10); + + $paginator = $query->order('createTime', 'desc') + ->paginate($limit, false, ['page' => $page]); + + $list = array_map(function ($item) { + return $this->formatMomentRow($item); + }, $paginator->items()); + + return ResponseHelper::success([ + 'list' => $list, + 'total' => $paginator->total(), + 'page' => $page, + 'limit' => $limit, + ]); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500); + } + } + + /** + * 导出朋友圈数据到Excel + * + * @return void + */ + public function export() + { + try { + $wechatId = $this->request->param('wechatId/s', ''); + if (empty($wechatId)) { + return ResponseHelper::error('wechatId不能为空'); + } + + // 权限校验:只能查看当前账号可访问的微信 + $accessibleWechatIds = $this->getAccessibleWechatIds(); + if (!in_array($wechatId, $accessibleWechatIds, true)) { + return ResponseHelper::error('无权查看该微信的朋友圈', 403); + } + + // 获取对应的微信账号ID + $accountId = Db::table('s2_wechat_account') + ->where('wechatId', $wechatId) + ->value('id'); + + if (empty($accountId)) { + return ResponseHelper::error('微信账号不存在或尚未同步', 404); + } + + $query = Db::table('s2_wechat_moments') + ->where('wechatAccountId', $accountId); + + // 关键词搜索 + if ($keyword = trim((string)$this->request->param('keyword', ''))) { + $query->whereLike('content', '%' . $keyword . '%'); + } + + // 类型筛选 + $type = $this->request->param('type', ''); + if ($type !== '' && $type !== null) { + $query->where('type', (int)$type); + } + + // 时间筛选 + $startTime = $this->request->param('startTime', ''); + $endTime = $this->request->param('endTime', ''); + if ($startTime || $endTime) { + $start = $startTime ? strtotime($startTime) : 0; + $end = $endTime ? strtotime($endTime) : time(); + if ($start && $end && $end < $start) { + return ResponseHelper::error('结束时间不能早于开始时间'); + } + $query->whereBetween('createTime', [$start ?: 0, $end ?: time()]); + } + + // 获取所有数据(不分页) + $moments = $query->order('createTime', 'desc')->select(); + + if (empty($moments)) { + return ResponseHelper::error('暂无数据可导出'); + } + + // 定义表头 + $headers = [ + 'date' => '日期', + 'postTime' => '投放时间', + 'functionCategory' => '作用分类', + 'content' => '朋友圈文案', + 'selfReply' => '自回评内容', + 'displayForm' => '朋友圈展示形式', + 'image1' => '配图1', + 'image2' => '配图2', + 'image3' => '配图3', + 'image4' => '配图4', + 'image5' => '配图5', + 'image6' => '配图6', + 'image7' => '配图7', + 'image8' => '配图8', + 'image9' => '配图9', + ]; + + // 格式化数据 + $rows = []; + foreach ($moments as $moment) { + $resUrls = $this->decodeJson($moment['resUrls'] ?? null); + $imageUrls = is_array($resUrls) ? $resUrls : []; + + // 格式化日期和时间 + $createTime = !empty($moment['createTime']) + ? (is_numeric($moment['createTime']) ? $moment['createTime'] : strtotime($moment['createTime'])) + : 0; + $date = $createTime ? date('Y年m月d日', $createTime) : ''; + $postTime = $createTime ? date('H:i', $createTime) : ''; + + // 判断展示形式 + $displayForm = ''; + if (!empty($moment['content']) && !empty($imageUrls)) { + $displayForm = '文字+图片'; + } elseif (!empty($moment['content'])) { + $displayForm = '文字'; + } elseif (!empty($imageUrls)) { + $displayForm = '图片'; + } + + $row = [ + 'date' => $date, + 'postTime' => $postTime, + 'functionCategory' => '', // 暂时放空 + 'content' => $moment['content'] ?? '', + 'selfReply' => '', // 暂时放空 + 'displayForm' => $displayForm, + ]; + + // 分配图片到配图1-9列 + for ($i = 1; $i <= 9; $i++) { + $imageKey = 'image' . $i; + $row[$imageKey] = isset($imageUrls[$i - 1]) ? $imageUrls[$i - 1] : ''; + } + + $rows[] = $row; + } + + // 定义图片列(配图1-9) + $imageColumns = ['image1', 'image2', 'image3', 'image4', 'image5', 'image6', 'image7', 'image8', 'image9']; + + // 生成文件名 + $fileName = '朋友圈投放_' . date('Ymd_His'); + + // 调用导出方法,优化图片显示效果 + ExportController::exportExcelWithImages( + $fileName, + $headers, + $rows, + $imageColumns, + '朋友圈投放', + [ + 'imageWidth' => 120, // 图片宽度(像素) + 'imageHeight' => 120, // 图片高度(像素) + 'imageColumnWidth' => 18, // 图片列宽(Excel单位) + 'rowHeight' => 130, // 行高(像素) + 'columnWidths' => [ // 特定列的固定宽度 + 'date' => 15, // 日期列宽 + 'postTime' => 12, // 投放时间列宽 + 'functionCategory' => 15, // 作用分类列宽 + 'content' => 40, // 朋友圈文案列宽(自动调整可能不够) + 'selfReply' => 30, // 自回评内容列宽 + 'displayForm' => 18, // 朋友圈展示形式列宽 + ], + 'titleRow' => [ // 标题行内容(第一行) + '朋友圈投放', + '我能提供什么价值? (40%) 有谁正在和我合作 (20%) 如何和我合作? (20%) 你找我合作需要付多少钱? (20%)' + ] + ] + ); + } catch (\Exception $e) { + return ResponseHelper::error('导出失败:' . $e->getMessage(), 500); + } + } + + /** + * 格式化朋友圈数据 + * + * @param array $row + * @return array + */ + protected function formatMomentRow(array $row): array + { + $formatTime = function ($timestamp) { + if (empty($timestamp)) { + return ''; + } + return is_numeric($timestamp) + ? date('Y-m-d H:i:s', $timestamp) + : date('Y-m-d H:i:s', strtotime($timestamp)); + }; + + return [ + 'id' => (int)$row['id'], + 'snsId' => $row['snsId'] ?? '', + 'type' => (int)($row['type'] ?? 0), + 'content' => $row['content'] ?? '', + 'commentList' => $this->decodeJson($row['commentList'] ?? null), + 'likeList' => $this->decodeJson($row['likeList'] ?? null), + 'resUrls' => $this->decodeJson($row['resUrls'] ?? null), + 'createTime' => $formatTime($row['createTime'] ?? null), + 'momentEntity' => [ + 'lat' => $row['lat'] ?? 0, + 'lng' => $row['lng'] ?? 0, + 'location' => $row['location'] ?? '', + 'picSize' => $row['picSize'] ?? 0, + 'userName' => $row['userName'] ?? '', + ], + ]; + } + + /** + * JSON字段解析 + * + * @param mixed $value + * @return array + */ + protected function decodeJson($value): array + { + if (empty($value)) { + return []; + } + + if (is_array($value)) { + return $value; + } + + $decoded = json_decode($value, true); + return $decoded ?: []; + } +} + diff --git a/application/cunkebao/controller/wechat/GetWechatOnDeviceFriendsV1Controller.php b/application/cunkebao/controller/wechat/GetWechatOnDeviceFriendsV1Controller.php new file mode 100644 index 0000000..cd544c4 --- /dev/null +++ b/application/cunkebao/controller/wechat/GetWechatOnDeviceFriendsV1Controller.php @@ -0,0 +1,105 @@ +items() as $item) { + $item->tags = json_decode($item->tags); + array_push($resultSets, $item->toArray()); + } + + return $resultSets; + } + + /** + * 根据微信账号ID获取好友列表 + * + * @param array $where + * @return \think\Paginator 分页对象 + */ + protected function getFriendsByWechatIdAndQueryParams(array $where): \think\Paginator + { + $query = WechatFriendShipModel::alias('f') + ->field( + [ + 'w.id', 'w.nickname', 'w.avatar', 'w.wechatId', + 'CASE WHEN w.alias IS NULL OR w.alias = "" THEN w.wechatId ELSE w.alias END AS wechatAccount', + 'f.memo', 'f.tags', + 'ff.accountUserName', 'ff.accountRealName','ff.id AS friendId' + ] + ) + ->join('wechat_account w', 'w.wechatId = f.wechatId') + ->join(['s2_wechat_friend' => 'ff'], 'ff.id = f.id'); + + foreach ($where as $key => $value) { + if (is_numeric($key) && is_array($value) && isset($value[0]) && $value[0] === 'exp') { + $query->whereExp('', $value[1]); + continue; + } + + $query->where($key, $value); + } + + return $query->paginate($this->request->param('limit/d', 10), false, ['page' => $this->request->param('page/d', 1)]); + } + + /** + * 构建查询条件 + * + * @param array $params + * @return array + */ + protected function makeWhere(array $params = []): array + { + // 关键词搜索(同时搜索好友备注和标签) + if (!empty($keyword = $this->request->param('keyword'))) { + $where[] = ['exp', "f.memo LIKE '%{$keyword}%' OR f.tags LIKE '%{$keyword}%'"]; + } + + $where['f.ownerWechatId'] = $this->request->param('id/s') ?: 'x_x'; + + return array_merge($where, $params); + } + + /** + * 获取微信好友列表 + * + * @return \think\response\Json + */ + public function index() + { + try { + $result = $this->getFriendsByWechatIdAndQueryParams( + $this->makeWhere() + ); + + return ResponseHelper::success( + [ + 'list' => $this->makeResultedSet($result), + 'total' => $result->total(), + ] + ); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/wechat/GetWechatOnDeviceSummarizeV1Controller.php b/application/cunkebao/controller/wechat/GetWechatOnDeviceSummarizeV1Controller.php new file mode 100644 index 0000000..c3eb6c6 --- /dev/null +++ b/application/cunkebao/controller/wechat/GetWechatOnDeviceSummarizeV1Controller.php @@ -0,0 +1,190 @@ +WechatCustomerModel)) { + $this->WechatCustomerModel = WechatCustomerModel::where( + [ + 'wechatId' => $wechatId, + 'companyId' => $this->getUserInfo('companyId') + ] + ) + ->find(); + } + + return $this->WechatCustomerModel; + } + + /** + * 计算账号年龄(从创建时间到现在) + * + * @param string $wechatId + * @return string + */ + protected function getRegisterDate(string $wechatId): string + { + return $this->getWechatCustomerModel($wechatId)->basic->registerDate ?? date('Y-m-d', time()); + } + + /** + * 获取昨日聊天次数 + * + * @param WechatCustomerModel $customer + * @return int + */ + protected function getChatTimesPerDay(?WechatCustomerModel $customer): int + { + return $customer->activity->yesterdayMsgCount ?? 0; + } + + /** + * 总聊天数量 + * + * @param WechatCustomerModel $customer + * @return int + */ + protected function getChatTimesTotal(?WechatCustomerModel $customer): int + { + return $customer->activity->totalMsgCount ?? 0; + } + + /** + * 计算活跃程度(根据消息数) + * + * @param string $wechatId + * @return string + */ + protected function getActivityLevel(string $wechatId): array + { + $customer = $this->getWechatCustomerModel($wechatId); + + return [ + 'allTimes' => $this->getChatTimesTotal($customer), + 'dayTimes' => $this->getChatTimesPerDay($customer), + ]; + } + + /** + * 获取限制记录 + * + * @param string $wechatId + * @return array + */ + protected function getRestrict(string $wechatId): array + { + return WechatRestrictsModel::alias('r') + ->field( + [ + 'r.id', 'r.restrictTime date', 'r.level', 'r.reason' + ] + ) + ->where('r.wechatId', $wechatId)->select() + ->toArray(); + } + + /** + * 获取账号权重 + * + * @param string $wechatId + * @return array + */ + protected function getAccountWeight(string $wechatId): array + { + $customer = $this->getWechatCustomerModel($wechatId); + $seeders = $customer ? (array)$customer->weight : array(); + + // 严谨返回 + return ArrHelper::getValue('ageWeight,activityWeigth,restrictWeight,realNameWeight,scope', $seeders, 0); + } + + /** + * 获取当日最高添加好友记录 + * + * @param string $wechatId + * @return int + * @throws \Exception + */ + protected function getAccountWeightAddLimit(string $wechatId): int + { + return $this->getWechatCustomerModel($wechatId)->weight->addLimit ?? 0; + } + + /** + * 计算今日新增好友数量 + * + * @param string $ownerWechatId + * @return int + */ + protected function getTodayNewFriendCount(string $ownerWechatId): int + { + return WechatFriendShipModel::where(compact('ownerWechatId')) + ->whereBetween('createTime', + [ + strtotime(date('Y-m-d 00:00:00')), + strtotime(date('Y-m-d 23:59:59')) + ] + ) + ->count('*'); + } + + /** + * 获取账号加友统计数据. + * + * @param string $wechatId + * @return array + */ + protected function getStatistics(string $wechatId): array + { + return [ + 'todayAdded' => $this->getTodayNewFriendCount($wechatId), + 'addLimit' => $this->getAccountWeightAddLimit($wechatId) + ]; + } + + /** + * 获取微信号详情 + * + * @return \think\response\Json + */ + public function index() + { + try { + $wechatId = $this->request->param('id/s'); + + return ResponseHelper::success( + [ + 'accountAge' => $this->getRegisterDate($wechatId), + 'activityLevel' => $this->getActivityLevel($wechatId), + 'accountWeight' => $this->getAccountWeight($wechatId), + 'statistics' => $this->getStatistics($wechatId), + 'restrictions' => $this->getRestrict($wechatId), + ] + ); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/wechat/GetWechatOverviewV1Controller.php b/application/cunkebao/controller/wechat/GetWechatOverviewV1Controller.php new file mode 100644 index 0000000..a48156d --- /dev/null +++ b/application/cunkebao/controller/wechat/GetWechatOverviewV1Controller.php @@ -0,0 +1,411 @@ +request->param('wechatId', ''); + + if (empty($wechatId)) { + return ResponseHelper::error('微信ID不能为空'); + } + + $companyId = $this->getUserInfo('companyId'); + + // 获取微信账号ID(accountId) + $account = Db::table('s2_wechat_account') + ->where('wechatId', $wechatId) + ->find(); + + if (empty($account)) { + return ResponseHelper::error('微信账号不存在'); + } + + $accountId = $account['id']; + + // 1. 健康分评估 + $healthScoreData = $this->getHealthScoreAssessment($accountId, $wechatId); + + // 2. 账号价值(模拟数据) + $accountValue = $this->getAccountValue($accountId); + + // 3. 今日价值变化(模拟数据) + $todayValueChange = $this->getTodayValueChange($accountId); + + // 4. 好友总数 + $totalFriends = $this->getTotalFriends($wechatId, $companyId); + + // 5. 今日新增好友 + $todayNewFriends = $this->getTodayNewFriends($wechatId); + + // 6. 高价群聊 + $highValueChatrooms = $this->getHighValueChatrooms($wechatId, $companyId); + + // 7. 今日新增群聊 + $todayNewChatrooms = $this->getTodayNewChatrooms($wechatId, $companyId); + + $result = [ + 'healthScoreAssessment' => $healthScoreData, + 'accountValue' => $accountValue, + 'todayValueChange' => $todayValueChange, + 'totalFriends' => $totalFriends, + 'todayNewFriends' => $todayNewFriends, + 'highValueChatrooms' => $highValueChatrooms, + 'todayNewChatrooms' => $todayNewChatrooms, + ]; + + return ResponseHelper::success($result); + + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500); + } + } + + /** + * 获取健康分评估数据 + * + * @param int $accountId 账号ID + * @param string $wechatId 微信ID + * @return array + */ + protected function getHealthScoreAssessment($accountId, $wechatId) + { + // 获取健康分信息 + $healthScoreService = new WechatAccountHealthScoreService(); + $healthScoreInfo = $healthScoreService->getHealthScore($accountId); + + $healthScore = $healthScoreInfo['healthScore'] ?? 0; + $maxAddFriendPerDay = $healthScoreInfo['maxAddFriendPerDay'] ?? 0; + + // 获取今日已加好友数 + $todayAdded = $this->getTodayAddedCount($wechatId); + + // 获取最后添加时间 + $lastAddTime = $this->getLastAddTime($wechatId); + + // 判断状态标签 + $statusTag = $todayAdded > 0 ? '已添加加人' : ''; + + // 获取基础构成 + $baseComposition = $this->getBaseComposition($healthScoreInfo); + + // 获取动态记录 + $dynamicRecords = $this->getDynamicRecords($healthScoreInfo); + + return [ + 'score' => $healthScore, + 'dailyLimit' => $maxAddFriendPerDay, + 'todayAdded' => $todayAdded, + 'lastAddTime' => $lastAddTime, + 'statusTag' => $statusTag, + 'baseComposition' => $baseComposition, + 'dynamicRecords' => $dynamicRecords, + ]; + } + + /** + * 获取基础构成数据 + * + * @param array $healthScoreInfo 健康分信息 + * @return array + */ + protected function getBaseComposition($healthScoreInfo) + { + $baseScore = $healthScoreInfo['baseScore'] ?? 0; + $baseInfoScore = $healthScoreInfo['baseInfoScore'] ?? 0; + $friendCountScore = $healthScoreInfo['friendCountScore'] ?? 0; + $friendCount = $healthScoreInfo['friendCount'] ?? 0; + + // 账号基础分(默认60分) + $accountBaseScore = 60; + + // 已修改微信号(如果baseInfoScore > 0,说明已修改) + $isModifiedAlias = $baseInfoScore > 0; + + $composition = [ + [ + 'name' => '账号基础分', + 'score' => $accountBaseScore, + 'formatted' => '+' . $accountBaseScore, + ] + ]; + + // 如果已修改微信号,添加基础信息分 + if ($isModifiedAlias) { + $composition[] = [ + 'name' => '已修改微信号', + 'score' => $baseInfoScore, + 'formatted' => '+' . $baseInfoScore, + ]; + } + + // 好友数量加成 + if ($friendCountScore > 0) { + $composition[] = [ + 'name' => '好友数量加成', + 'score' => $friendCountScore, + 'formatted' => '+' . $friendCountScore, + 'friendCount' => $friendCount, // 显示好友总数 + ]; + } + + return $composition; + } + + /** + * 获取动态记录数据 + * + * @param array $healthScoreInfo 健康分信息 + * @return array + */ + protected function getDynamicRecords($healthScoreInfo) + { + $records = []; + + $frequentPenalty = $healthScoreInfo['frequentPenalty'] ?? 0; + $frequentCount = $healthScoreInfo['frequentCount'] ?? 0; + $banPenalty = $healthScoreInfo['banPenalty'] ?? 0; + $isBanned = $healthScoreInfo['isBanned'] ?? 0; + $noFrequentBonus = $healthScoreInfo['noFrequentBonus'] ?? 0; + $consecutiveNoFrequentDays = $healthScoreInfo['consecutiveNoFrequentDays'] ?? 0; + $lastFrequentTime = $healthScoreInfo['lastFrequentTime'] ?? null; + + // 频繁扣分记录 + // 根据frequentCount判断是首次还是再次 + // frequentPenalty存储的是当前状态的扣分(-15或-25),不是累计值 + if ($frequentCount > 0 && $frequentPenalty < 0) { + if ($frequentCount == 1) { + // 首次频繁:-15分 + $records[] = [ + 'name' => '首次触发限额', + 'score' => $frequentPenalty, + 'formatted' => (string)$frequentPenalty, + 'type' => 'penalty', + 'time' => $lastFrequentTime ? date('Y-m-d H:i:s', $lastFrequentTime) : null, + ]; + } else { + // 再次频繁:-25分 + $records[] = [ + 'name' => '再次触发限额', + 'score' => $frequentPenalty, + 'formatted' => (string)$frequentPenalty, + 'type' => 'penalty', + 'time' => $lastFrequentTime ? date('Y-m-d H:i:s', $lastFrequentTime) : null, + ]; + } + } + + // 封号扣分记录 + if ($isBanned && $banPenalty < 0) { + $lastBanTime = $healthScoreInfo['lastBanTime'] ?? null; + $records[] = [ + 'name' => '封号', + 'score' => $banPenalty, + 'formatted' => (string)$banPenalty, + 'type' => 'penalty', + 'time' => $lastBanTime ? date('Y-m-d H:i:s', $lastBanTime) : null, + ]; + } + + // 不频繁加分记录 + if ($noFrequentBonus > 0 && $consecutiveNoFrequentDays >= 3) { + $lastNoFrequentTime = $healthScoreInfo['lastNoFrequentTime'] ?? null; + $records[] = [ + 'name' => '连续' . $consecutiveNoFrequentDays . '天不触发频繁', + 'score' => $noFrequentBonus, + 'formatted' => '+' . $noFrequentBonus, + 'type' => 'bonus', + 'time' => $lastNoFrequentTime ? date('Y-m-d H:i:s', $lastNoFrequentTime) : null, + ]; + } + + return $records; + } + + /** + * 获取今日已加好友数 + * + * @param string $wechatId 微信ID + * @return int + */ + protected function getTodayAddedCount($wechatId) + { + $start = strtotime(date('Y-m-d 00:00:00')); + $end = strtotime(date('Y-m-d 23:59:59')); + + return Db::table('s2_friend_task') + ->where('wechatId', $wechatId) + ->whereBetween('createTime', [$start, $end]) + ->count(); + } + + /** + * 获取最后添加时间 + * + * @param string $wechatId 微信ID + * @return string + */ + protected function getLastAddTime($wechatId) + { + $lastTask = Db::table('s2_friend_task') + ->where('wechatId', $wechatId) + ->order('createTime', 'desc') + ->find(); + + if (empty($lastTask) || empty($lastTask['createTime'])) { + return ''; + } + + return date('H:i:s', $lastTask['createTime']); + } + + /** + * 获取账号价值(模拟数据) + * + * @param int $accountId 账号ID + * @return array + */ + protected function getAccountValue($accountId) + { + // TODO: 后续替换为真实计算逻辑 + // 模拟数据:¥29,800 + $value = 29800; + + return [ + 'value' => $value, + 'formatted' => '¥' . number_format($value, 0, '.', ','), + ]; + } + + /** + * 获取今日价值变化(模拟数据) + * + * @param int $accountId 账号ID + * @return array + */ + protected function getTodayValueChange($accountId) + { + // TODO: 后续替换为真实计算逻辑 + // 模拟数据:+500 + $change = 500; + + return [ + 'change' => $change, + 'formatted' => $change > 0 ? '+' . $change : (string)$change, + 'isPositive' => $change > 0, + ]; + } + + /** + * 获取好友总数 + * + * @param string $wechatId 微信ID + * @param int $companyId 公司ID + * @return int + */ + protected function getTotalFriends($wechatId, $companyId) + { + // 优先从 s2_wechat_account 表获取 + $account = Db::table('s2_wechat_account') + ->where('wechatId', $wechatId) + ->field('totalFriend') + ->find(); + + if (!empty($account) && isset($account['totalFriend'])) { + return (int)$account['totalFriend']; + } + + // 如果 totalFriend 为空,则从 s2_wechat_friend 表统计 + return Db::table('s2_wechat_friend') + ->where('ownerWechatId', $wechatId) + ->where('isDeleted', 0) + ->count(); + } + + /** + * 获取今日新增好友数 + * + * @param string $wechatId 微信ID + * @return int + */ + protected function getTodayNewFriends($wechatId) + { + $start = strtotime(date('Y-m-d 00:00:00')); + $end = strtotime(date('Y-m-d 23:59:59')); + + // 从 s2_wechat_friend 表统计今日新增 + return Db::table('s2_wechat_friend') + ->where('ownerWechatId', $wechatId) + ->whereBetween('createTime', [$start, $end]) + ->where('isDeleted', 0) + ->count(); + } + + /** + * 获取高价群聊数量 + * 高价群聊定义:群成员数 >= 50 的群聊 + * + * @param string $wechatId 微信ID + * @param int $companyId 公司ID + * @return int + */ + protected function getHighValueChatrooms($wechatId, $companyId) + { + // 高价群聊定义:群成员数 >= 50 + $minMemberCount = 50; + + // 查询该微信账号下的高价群聊 + // 使用子查询统计每个群的成员数 + $result = Db::query(" + SELECT COUNT(DISTINCT c.chatroomId) as count + FROM s2_wechat_chatroom c + INNER JOIN ( + SELECT chatroomId, COUNT(*) as memberCount + FROM s2_wechat_chatroom_member + GROUP BY chatroomId + HAVING memberCount >= ? + ) m ON c.chatroomId = m.chatroomId + WHERE c.wechatAccountWechatId = ? + AND c.isDeleted = 0 + ", [$minMemberCount, $wechatId]); + + return !empty($result) ? (int)$result[0]['count'] : 0; + } + + /** + * 获取今日新增群聊数 + * + * @param string $wechatId 微信ID + * @param int $companyId 公司ID + * @return int + */ + protected function getTodayNewChatrooms($wechatId, $companyId) + { + $start = strtotime(date('Y-m-d 00:00:00')); + $end = strtotime(date('Y-m-d 23:59:59')); + + return Db::table('s2_wechat_chatroom') + ->where('wechatAccountWechatId', $wechatId) + ->whereBetween('createTime', [$start, $end]) + ->where('isDeleted', 0) + ->count(); + } +} + diff --git a/application/cunkebao/controller/wechat/GetWechatProfileV1Controller.php b/application/cunkebao/controller/wechat/GetWechatProfileV1Controller.php new file mode 100644 index 0000000..ad7a7f5 --- /dev/null +++ b/application/cunkebao/controller/wechat/GetWechatProfileV1Controller.php @@ -0,0 +1,115 @@ +field('t.id') + ->join('traffic_source s', 's.identifier = p.identifier') + ->where('p.wechatId', $wechatId) + ->value('fromd'); + } + + /** + * 获取微信账号 + * + * @param string $wechatId + * @return array + * @throws \Exception + */ + protected function getWechatAccountProfileByWechatId(string $wechatId): array + { + $account = WechatAccountModel::alias('w') + ->field( + [ + 'w.id', 'w.avatar', 'w.nickname', 'w.region', 'w.wechatId', + 'CASE WHEN w.alias IS NULL OR w.alias = "" THEN w.wechatId ELSE w.alias END AS wechatId', + 'f.createTime', 'f.tags', 'f.memo' + ] + ) + ->join('wechat_friendship f', 'w.wechatId=f.wechatId') + ->where('w.wechatId', $wechatId) + ->find(); + + if (is_null($account)) { + throw new \Exception('未获取到微信账号数据', 404); + } + + return $account->toArray(); + } + + /** + * 获取微信好友详情 + * + * @return \think\response\Json + */ + public function index() + { + try { + $results = $this->getWechatAccountProfileByWechatId( + $this->request->param('wechatId/s') + ); + + return ResponseHelper::success( + array_merge($results, [ + 'playDate' => $this->getLastPlayTime($results['wechatId']), + 'source' => $this->getTrafficSource($results['wechatId']), + 'tags' => $this->getWechatTags($results['tags']), + 'addDate' => $this->getAddShipDate($results['createTime']), + ]) + ); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/wechat/GetWechatsOnDevicesV1Controller.php b/application/cunkebao/controller/wechat/GetWechatsOnDevicesV1Controller.php new file mode 100644 index 0000000..0b50aa9 --- /dev/null +++ b/application/cunkebao/controller/wechat/GetWechatsOnDevicesV1Controller.php @@ -0,0 +1,422 @@ + $this->getUserInfo('companyId') + ] + ) + ->column('id'); + } + + /** + * 非主操盘手获取分配的设备 + * + * @return array + */ + protected function getUserDevicesId(): array + { + return DeviceUserModel::where( + [ + 'userId' => $this->getUserInfo('id'), + 'companyId' => $this->getUserInfo('companyId') + ] + ) + ->column('deviceId'); + } + + /** + * 根据不同角色,显示的设备数量不同 + * + * @return array + * @throws \Exception + */ + protected function getDevicesId(): array + { + return ($this->getUserInfo('isAdmin') == UserModel::ADMIN_STP) + ? $this->getCompanyDevicesId() // 主操盘手获取所有的设备 + : $this->getUserDevicesId(); // 非主操盘手获取分配的设备 + } + + /** + * 获取有登录设备的微信id + * 优化:使用索引字段,减少数据查询量 + * + * @return array + */ + protected function getWechatIdsOnDevices(): array + { + // 关联设备id查询,过滤掉已删除的设备 + if (empty($deviceIds = $this->getDevicesId())) { + throw new \Exception('暂无设备数据', 200); + } + + // 优化:直接使用DISTINCT减少数据传输量 + return DeviceWechatLoginModel::distinct(true) + ->where([ + 'companyId' => $this->getUserInfo('companyId'), +// 'alive' => DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE, + ]) + ->where('deviceId', 'in', $deviceIds) + ->column('wechatId'); + } + + /** + * 构建查询条件 + * + * @param array $params + * @return array + */ + protected function makeWhere(array $params = []): array + { + if (empty($wechatIds = $this->getWechatIdsOnDevices())) { + throw new \Exception('设备尚未有登录微信', 200); + } + + // 关键词搜索(同时搜索微信号和昵称) + if (!empty($keyword = $this->request->param('keyword'))) { + $where[] = ["w.wechatId|w.alias|w.nickname", 'LIKE', '%' . $keyword . '%']; + } + + $where['w.wechatId'] = array('in', implode(',', $wechatIds)); + + return array_merge($where, $params); + } + + /** + * 获取在线微信账号列表 + * 优化:减少查询字段,使用索引,优化JOIN条件 + * + * @param array $where + * @return \think\Paginator 分页对象 + */ + protected function getOnlineWechatList(array $where): \think\Paginator + { + // 获取微信在线状态筛选参数(1=在线,0=离线,不传=全部) + $wechatStatus = $this->request->param('wechatStatus'); + + // 优化:只查询必要字段,使用FORCE INDEX提示数据库使用索引 + $query = WechatAccountModel::alias('w') + ->field( + [ + 'w.id', 'w.nickname', 'w.avatar', 'w.wechatId', + 'CASE WHEN w.alias IS NULL OR w.alias = "" THEN w.wechatId ELSE w.alias END AS wechatAccount', + 'MAX(l.deviceId) as deviceId', 'MAX(l.alive) as alive' // 使用MAX确保GROUP BY时获取正确的在线状态 + ] + ) + // 优化:使用INNER JOIN代替LEFT JOIN,并添加索引提示 + ->join('device_wechat_login l', 'w.wechatId = l.wechatId AND l.companyId = '. $this->getUserInfo('companyId'), 'INNER') + // 添加s2_wechat_account表的LEFT JOIN,用于筛选微信在线状态 + ->join(['s2_wechat_account' => 'sa'], 'w.wechatId = sa.wechatId', 'LEFT') + ->group('w.wechatId') + // 优化:在线状态优先排序(alive=1的排在前面),然后按wechatId排序 + // 注意:ORDER BY使用SELECT中定义的别名alive,而不是聚合函数 + ->order('alive desc, w.wechatId desc'); + + // 根据wechatStatus参数筛选(1=在线,0=离线,不传=全部) + if ($wechatStatus !== null && $wechatStatus !== '') { + $wechatStatus = (int)$wechatStatus; + if ($wechatStatus === 1) { + // 筛选在线:wechatAlive = 1 + $query->where('sa.wechatAlive', 1); + } elseif ($wechatStatus === 0) { + // 筛选离线:wechatAlive = 0 或 NULL + $query->where(function($query) { + $query->where('sa.wechatAlive', 0) + ->whereOr('sa.wechatAlive', 'exp', 'IS NULL'); + }); + } + } + + // 应用查询条件 + foreach ($where as $key => $value) { + if (is_numeric($key) && is_array($value) && isset($value[0]) && $value[0] === 'exp') { + $query->whereExp('', $value[1]); + continue; + } + + if (is_array($value)) { + $query->where($key, ...$value); + continue; + } + + $query->where($key, $value); + } + + // 优化:使用简单计数查询 + return $query->paginate( + $this->request->param('limit/d', 10), + false, + ['page' => $this->request->param('page/d', 1)] + ); + } + + /** + * 构建返回数据 + * + * @param \think\Paginator $result + * @return array + */ + protected function makeResultedSet(\think\Paginator $result): array + { + $resultSets = []; + $items = $result->items(); + + if (empty($items)) { + return $resultSets; + } + + $wechatIds = array_values(array_unique(array_map(function ($item) { + return $item->wechatId ?? ($item['wechatId'] ?? ''); + }, $items))); + + $metrics = $this->collectWechatMetrics($wechatIds); + + foreach ($items as $item) { + $addLimit = $metrics['addLimit'][$item->wechatId] ?? 0; + $todayAdded = $metrics['todayAdded'][$item->wechatId] ?? 0; + // 计算今日可添加数量 = 可添加额度 - 今日已添加 + $todayCanAdd = max(0, $addLimit - $todayAdded); + + $sections = $item->toArray() + [ + 'times' => $addLimit, + 'addedCount' => $todayAdded, + 'todayCanAdd' => $todayCanAdd, // 今日可添加数量 + 'wechatStatus' => $metrics['wechatStatus'][$item->wechatId] ?? 0, + 'totalFriend' => $metrics['totalFriend'][$item->wechatId] ?? 0, + 'deviceMemo' => $metrics['deviceMemo'][$item->wechatId] ?? '', + 'activeTime' => $metrics['activeTime'][$item->wechatId] ?? '-', + ]; + + array_push($resultSets, $sections); + } + + return $resultSets; + } + + /** + * 批量收集微信账号的统计信息 + * 优化:合并查询,减少数据库访问次数,使用缓存 + * + * @param array $wechatIds + * @return array + */ + protected function collectWechatMetrics(array $wechatIds): array + { + $metrics = [ + 'addLimit' => [], + 'todayAdded' => [], + 'totalFriend' => [], + 'wechatStatus' => [], + 'deviceMemo' => [], + 'activeTime' => [], + ]; + + if (empty($wechatIds)) { + return $metrics; + } + + $companyId = $this->getUserInfo('companyId'); + + // 使用缓存键,避免短时间内重复查询 + $cacheKey = 'wechat_metrics_' . md5(implode(',', $wechatIds) . '_' . $companyId); + + // 尝试从缓存获取数据(缓存5分钟) + $cachedMetrics = cache($cacheKey); + if ($cachedMetrics) { + return $cachedMetrics; + } + + // 优化1:可添加好友额度 - 从s2_wechat_account_score表获取maxAddFriendPerDay + $scoreRows = Db::table('s2_wechat_account_score') + ->whereIn('wechatId', $wechatIds) + ->column('maxAddFriendPerDay', 'wechatId'); + foreach ($scoreRows as $wechatId => $maxAddFriendPerDay) { + $metrics['addLimit'][$wechatId] = (int)($maxAddFriendPerDay ?? 0); + } + + // 优化2:今日新增好友 - 使用索引字段和预计算 + $start = strtotime(date('Y-m-d 00:00:00')); + $end = strtotime(date('Y-m-d 23:59:59')); + + // 使用单次查询获取所有wechatIds的今日新增和总好友数 + // 根据数据库结构使用s2_wechat_friend表而不是wechat_friend_ship + $friendshipStats = Db::query(" + SELECT + ownerWechatId, + SUM(IF(createTime BETWEEN {$start} AND {$end}, 1, 0)) as today_added, + COUNT(*) as total_friend + FROM + s2_wechat_friend + WHERE + ownerWechatId IN ('" . implode("','", $wechatIds) . "') + AND isDeleted = 0 + GROUP BY + ownerWechatId + "); + + // 处理结果 + foreach ($friendshipStats as $row) { + $wechatId = $row['ownerWechatId'] ?? ''; + if ($wechatId) { + $metrics['todayAdded'][$wechatId] = (int)($row['today_added'] ?? 0); + $metrics['totalFriend'][$wechatId] = (int)($row['total_friend'] ?? 0); + } + } + + // 优化3:微信在线状态 - 从s2_wechat_account表获取wechatAlive + $wechatAccountRows = Db::table('s2_wechat_account') + ->whereIn('wechatId', $wechatIds) + ->field('wechatId, wechatAlive') + ->select(); + + foreach ($wechatAccountRows as $row) { + $wechatId = $row['wechatId'] ?? ''; + if (!empty($wechatId)) { + $metrics['wechatStatus'][$wechatId] = (int)($row['wechatAlive'] ?? 0); + } + } + + // 优化4:设备状态与备注 - 使用INNER JOIN和索引 + $loginRows = Db::name('device_wechat_login') + ->alias('l') + ->join('device d', 'd.id = l.deviceId', 'LEFT') + ->field('l.wechatId, l.alive, d.memo') + ->where('l.companyId', $companyId) + ->whereIn('l.wechatId', $wechatIds) + ->order('l.id', 'desc') + ->select(); + + // 使用临时数组避免重复处理 + $processedWechatIds = []; + foreach ($loginRows as $row) { + $wechatId = $row['wechatId'] ?? ''; + // 只处理每个wechatId的第一条记录(最新的) + if (!empty($wechatId) && !in_array($wechatId, $processedWechatIds)) { + // 如果s2_wechat_account表中没有wechatAlive,则使用device_wechat_login的alive作为备用 + if (!isset($metrics['wechatStatus'][$wechatId])) { + $metrics['wechatStatus'][$wechatId] = (int)($row['alive'] ?? 0); + } + $metrics['deviceMemo'][$wechatId] = $row['memo'] ?? ''; + $processedWechatIds[] = $wechatId; + } + } + + // 优化5:活跃时间 - 使用JOIN减少查询次数 + $activeTimeResults = Db::query(" + SELECT + a.wechatId, + MAX(m.wechatTime) as lastTime + FROM + s2_wechat_account a + LEFT JOIN + s2_wechat_message m ON a.id = m.wechatAccountId + WHERE + a.wechatId IN ('" . implode("','", $wechatIds) . "') + GROUP BY + a.wechatId + "); + + foreach ($activeTimeResults as $row) { + $wechatId = $row['wechatId'] ?? ''; + $lastTime = (int)($row['lastTime'] ?? 0); + if (!empty($wechatId) && $lastTime > 0) { + $metrics['activeTime'][$wechatId] = date('Y-m-d H:i:s', $lastTime); + } else { + $metrics['activeTime'][$wechatId] = '-'; + } + } + + // 确保所有wechatId都有wechatStatus值(默认0) + foreach ($wechatIds as $wechatId) { + if (!isset($metrics['wechatStatus'][$wechatId])) { + $metrics['wechatStatus'][$wechatId] = 0; + } + } + + // 存入缓存,有效期5分钟 + cache($cacheKey, $metrics, 300); + + return $metrics; + } + + /** + * 获取在线微信账号列表 + * 优化:添加缓存,优化分页逻辑 + * + * @return \think\response\Json + */ + public function index() + { + try { + // 获取分页参数 + $page = $this->request->param('page/d', 1); + $limit = $this->request->param('limit/d', 10); + $keyword = $this->request->param('keyword'); + $wechatStatus = $this->request->param('wechatStatus'); + + // 创建缓存键(基于用户、分页、搜索条件和在线状态筛选) + $cacheKey = 'wechat_list_' . $this->getUserInfo('id') . '_' . $page . '_' . $limit . '_' . md5($keyword ?? '') . '_' . ($wechatStatus ?? 'all'); + + // 尝试从缓存获取数据(缓存2分钟) + $cachedData = cache($cacheKey); + if ($cachedData) { + return ResponseHelper::success($cachedData); + } + + // 如果没有缓存,执行查询 + $result = $this->getOnlineWechatList( + $this->makeWhere() + ); + + $responseData = [ + 'list' => $this->makeResultedSet($result), + 'total' => $result->total(), + ]; + + // 存入缓存,有效期2分钟 + cache($cacheKey, $responseData, 120); + + return ResponseHelper::success($responseData); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/wechat/GetWechatsRelatedDeviceV1Controller.php b/application/cunkebao/controller/wechat/GetWechatsRelatedDeviceV1Controller.php new file mode 100644 index 0000000..92d20ce --- /dev/null +++ b/application/cunkebao/controller/wechat/GetWechatsRelatedDeviceV1Controller.php @@ -0,0 +1,180 @@ + $deviceId, + 'userId' => $this->getUserInfo('id'), + 'companyId' => $this->getUserInfo('companyId') + ] + ) + ->count() > 0; + + if (!$hasPermission) { + throw new \Exception('您没有权限查看该设备', 403); + } + } + + /** + * 查询设备关联的微信ID列表 + * + * @param int $deviceId + * @return array + */ + protected function getDeviceWechatIds(int $deviceId): array + { + return DeviceWechatLoginModel::where( + [ + 'deviceId' => $deviceId, + 'companyId' => $this->getUserInfo('companyId') + ] + ) + ->group('wechatId')->column('wechatId'); + } + + /** + * 通过设备关联的微信号列表获取微信账号 + * + * @param array $wechatIds + * @return ResultCollection + */ + protected function getWechatAccountsByIds(array $wechatIds): ResultCollection + { + return WechatAccountModel::alias('w') + ->field([ + 'w.wechatId', 'w.nickname', 'w.avatar', 'w.gender', 'w.createTime', + 'CASE WHEN w.alias IS NULL OR w.alias = "" THEN w.wechatId ELSE w.alias END AS wechatAccount', + ]) + ->whereIn('w.wechatId', $wechatIds) + ->select(); + } + + /** + * TODO 通过微信id获取微信最后活跃时间 + * + * @param int $time + * @return string + */ + protected function getWechatLastActiveTime(string $wechatId): string + { + return date('Y-m-d H:i:s', time()); + } + + /** + * TODO 加友状态 + * + * @param string $wechatId + * @return string + */ + protected function getWechatStatusText(string $wechatId): string + { + return 1 ? '可加友' : '已停用'; + } + + /** + * TODO 账号状态 + * + * @param string $wechatId + * @return string + */ + protected function getWechatAliveText(string $wechatId): string + { + return 1 ? '正常' : '异常'; + } + + /** + * 统计微信好友 + * + * @param string $ownerWechatId + * @return int + */ + protected function getCountFriend(string $ownerWechatId): int + { + return WechatFriendShipModel::where( + [ + 'ownerWechatId' => $ownerWechatId, + 'companyId' => $this->getUserInfo('companyId') + ] + ) + ->count(); + } + + /** + * 获取设备关联的微信账号信息 + * + * @param int $deviceId + * @return array + */ + protected function getDeviceRelatedAccounts(int $deviceId): array + { + // 获取设备关联的微信ID列表 + $wechatIds = $this->getDeviceWechatIds($deviceId); + + if (!empty($wechatIds)) { + $collection = $this->getWechatAccountsByIds($wechatIds); + + foreach ($collection as $account) { + $account->lastActive = $this->getWechatLastActiveTime($account->wechatId); + $account->statusText = $this->getWechatStatusText($account->wechatId); + $account->totalFriend = $this->getCountFriend($account->wechatId); + $account->wechatAliveText = $this->getWechatAliveText($account->wechatId); + } + + return $collection->toArray(); + } + + return []; + } + + /** + * 获取设备关联的微信账号 + * + * @return \think\response\Json + */ + public function index() + { + try { + $deviceId = $this->request->param('id/d'); + + if ($this->getUserInfo('isAdmin') != UserModel::ADMIN_STP) { + $this->checkUserDevicePermission($deviceId); + } + + // 获取设备关联的微信账号 + $wechatAccounts = $this->getDeviceRelatedAccounts($deviceId); + + return ResponseHelper::success( + [ + 'deviceId' => $deviceId, + 'accounts' => $wechatAccounts, + 'total' => count($wechatAccounts) + ] + ); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/wechat/PostTransferFriends.php b/application/cunkebao/controller/wechat/PostTransferFriends.php new file mode 100644 index 0000000..f408ae4 --- /dev/null +++ b/application/cunkebao/controller/wechat/PostTransferFriends.php @@ -0,0 +1,157 @@ +request->param('wechatId', ''); + $inherit = $this->request->param('inherit', ''); + $greeting = $this->request->param('greeting', ''); + $firstMessage = $this->request->param('firstMessage', ''); + $devices = $this->request->param('devices', []); + $companyId = $this->getUserInfo('companyId'); + + + if (empty($wechatId)){ + return ResponseHelper::error('迁移的微信不能为空'); + } + + if (empty($devices)){ + return ResponseHelper::error('迁移的设备不能为空'); + } + if (empty($greeting)){ + return ResponseHelper::error('打招呼不能为空'); + } + if (!is_array($devices)){ + return ResponseHelper::error('迁移的设备必须为数组'); + } + + $wechat = Db::name('wechat_customer')->alias('wc') + ->join('wechat_account wa', 'wc.wechatId = wa.wechatId') + ->where(['wc.wechatId' => $wechatId]) + ->field('wa.*') + ->find(); + + if (empty($wechat)) { + return ResponseHelper::error('该微信不存在'); + } + + $devices = Db::name('device') + ->where(['companyId' => $companyId,'deleteTime' => 0]) + ->whereIn('id', $devices) + ->column('id'); + + + + + + try { + $sceneConf = [ + 'enabled' => true, + 'posters' => [ + 'id' => 'poster-3', + 'name' => '点击咨询', + 'src' => 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%92%A8%E8%AF%A2-FTiyAMAPop2g9LvjLOLDz0VwPg3KVu.gif' + ] + ]; + $reqConf = [ + 'device' => $devices, + 'startTime' => '09:00', + 'endTime' => '18:00', + 'remarkType' => 'phone', + 'addFriendInterval' => 60, + 'greeting' => !empty($greeting) ? $greeting :'我是'. $wechat['nickname'] .'的新号,请通过' + ]; + + if (!empty($firstMessage)){ + $msgConf = [ + [ + 'day' => 0, + 'messages' => [ + [ + 'id' => 1, + 'type' => 'text', + 'content' => $firstMessage, + 'intervalUnit' => 'seconds', + 'sendInterval' => 5, + ] + ] + ] + ]; + }else{ + $msgConf = []; + } + + // 使用容器获取控制器实例,而不是直接实例化 + $createAddFriendPlan = app('app\cunkebao\controller\plan\PostCreateAddFriendPlanV1Controller'); + + $taskId = Db::name('customer_acquisition_task')->insertGetId([ + 'name' => '迁移好友('. $wechat['nickname'] .')', + 'sceneId' => 10, + 'sceneConf' => json_encode($sceneConf,256), + 'reqConf' => json_encode($reqConf,256), + 'tagConf' => json_encode([]), + 'msgConf' => json_encode($msgConf,256), + 'userId' => $this->getUserInfo('id'), + 'companyId' => $companyId, + 'status' => 0, + 'createTime' => time(), + 'apiKey' => $createAddFriendPlan->generateApiKey(), + ]); + + $friends = Db::table('s2_wechat_friend') + ->where(['ownerWechatId' => $wechatId]) + ->group('wechatId') + ->order('id DESC') + ->column('id', 'wechatId,alias,phone,labels,conRemark'); + + // 1000条为一组进行批量处理 + $batchSize = 1000; + $totalRows = count($friends); + + for ($i = 0; $i < $totalRows; $i += $batchSize) { + $batchRows = array_slice($friends, $i, $batchSize); + if (!empty($batchRows)) { + $newData = []; + foreach ($batchRows as $row) { + if (!empty($row['phone'])) { + $phone = $row['phone']; + } elseif (!empty($row['alias'])) { + $phone = $row['alias']; + } else { + $phone = $row['wechatId']; + } + + $tags = !empty($row['labels']) ? json_decode($row['labels'], true) : []; + $newData[] = [ + 'task_id' => $taskId, + 'name' => '', + 'source' => '迁移好友('. $wechat['nickname'] .')', + 'phone' => $phone, + 'remark' => !empty($inherit) ? $row['conRemark'] : '', + 'tags' => !empty($inherit) ? json_encode($tags, JSON_UNESCAPED_UNICODE) : json_encode([]), + 'siteTags' => json_encode([]), + 'status' => 0, + 'createTime' => time(), + ]; + } + Db::name('task_customer')->insertAll($newData); + } + } + return ResponseHelper::success('好友迁移创建成功' ); + } catch (\Exception $e) { + // 回滚事务 + Db::rollback(); + return ResponseHelper::error('好友迁移创建失败:' . $e->getMessage()); + } + + + } +} \ No newline at end of file diff --git a/application/cunkebao/controller/workbench/CommonFunctionsController.php b/application/cunkebao/controller/workbench/CommonFunctionsController.php new file mode 100644 index 0000000..d13d1dc --- /dev/null +++ b/application/cunkebao/controller/workbench/CommonFunctionsController.php @@ -0,0 +1,50 @@ +getUserInfo('companyId'); + + // 从数据库查询常用功能列表 + $functions = Db::name('workbench_function') + ->where('status', 1) + ->order('sort ASC, id ASC') + ->select(); + + + // 处理数据,判断是否显示New标签(创建时间近1个月) + $oneMonthAgo = time() - 30 * 24 * 60 * 60; // 30天前的时间戳 + foreach ($functions as &$function) { + // 判断是否显示New标签:创建时间在近1个月内 + $function['isNew'] = ($function['createTime'] >= $oneMonthAgo) ? true : false; + $function['labels'] = json_decode($function['labels'],true); + } + unset($function); + + return ResponseHelper::success([ + 'list' => $functions + ]); + + } catch (\Exception $e) { + return ResponseHelper::error('获取常用功能列表失败:' . $e->getMessage()); + } + } + + +} + diff --git a/application/cunkebao/controller/workbench/WorkbenchAutoLikeController.php b/application/cunkebao/controller/workbench/WorkbenchAutoLikeController.php new file mode 100644 index 0000000..de112ba --- /dev/null +++ b/application/cunkebao/controller/workbench/WorkbenchAutoLikeController.php @@ -0,0 +1,108 @@ +request->param('page', 1); + $limit = $this->request->param('limit', 10); + $workbenchId = $this->request->param('workbenchId', 0); + + $where = [ + ['wali.workbenchId', '=', $workbenchId] + ]; + + // 查询点赞记录 + $list = Db::name('workbench_auto_like_item')->alias('wali') + ->join(['s2_wechat_moments' => 'wm'], 'wali.snsId = wm.snsId') + ->field([ + 'wali.id', + 'wali.workbenchId', + 'wali.momentsId', + 'wali.snsId', + 'wali.wechatAccountId', + 'wali.wechatFriendId', + 'wali.createTime as likeTime', + 'wm.content', + 'wm.resUrls', + 'wm.createTime as momentTime', + 'wm.userName', + ]) + ->where($where) + ->order('wali.createTime', 'desc') + ->group('wali.id') + ->page($page, $limit) + ->select(); + + + // 处理数据 + foreach ($list as &$item) { + //处理用户信息 + $friend = Db::table('s2_wechat_friend') + ->where(['id' => $item['wechatFriendId']]) + ->field('nickName,avatar') + ->find(); + if (!empty($friend)) { + $item['friendName'] = $friend['nickName']; + $item['friendAvatar'] = $friend['avatar']; + } else { + $item['friendName'] = ''; + $item['friendAvatar'] = ''; + } + + + //处理客服 + $friend = Db::table('s2_wechat_account') + ->where(['id' => $item['wechatAccountId']]) + ->field('nickName,avatar') + ->find(); + if (!empty($friend)) { + $item['operatorName'] = $friend['nickName']; + $item['operatorAvatar'] = $friend['avatar']; + } else { + $item['operatorName'] = ''; + $item['operatorAvatar'] = ''; + } + + // 处理时间格式 + $item['likeTime'] = date('Y-m-d H:i:s', $item['likeTime']); + $item['momentTime'] = !empty($item['momentTime']) ? date('Y-m-d H:i:s', $item['momentTime']) : ''; + + // 处理资源链接 + if (!empty($item['resUrls'])) { + $item['resUrls'] = json_decode($item['resUrls'], true); + } else { + $item['resUrls'] = []; + } + } + + // 获取总记录数 + $total = Db::name('workbench_auto_like_item')->alias('wali') + ->where($where) + ->count(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ] + ]); + } +} + diff --git a/application/cunkebao/controller/workbench/WorkbenchController.php b/application/cunkebao/controller/workbench/WorkbenchController.php new file mode 100644 index 0000000..7f75653 --- /dev/null +++ b/application/cunkebao/controller/workbench/WorkbenchController.php @@ -0,0 +1,3353 @@ +request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + // 获取登录用户信息 + $userInfo = request()->userInfo; + + // 获取请求参数 + $param = $this->request->post(); + + + // 根据业务默认值补全参数 + if ( + isset($param['type']) && + intval($param['type']) === self::TYPE_GROUP_PUSH + ) { + if (empty($param['startTime'])) { + $param['startTime'] = '09:00'; + } + if (empty($param['endTime'])) { + $param['endTime'] = '21:00'; + } + } + + // 验证数据 + $validate = new WorkbenchValidate; + if (!$validate->scene('create')->check($param)) { + return json(['code' => 400, 'msg' => $validate->getError()]); + } + + Db::startTrans(); + try { + // 创建工作台基本信息 + $workbench = new Workbench; + $workbench->name = $param['name']; + $workbench->type = $param['type']; + $workbench->status = !empty($param['status']) ? 1 : 0; + $workbench->autoStart = !empty($param['autoStart']) ? 1 : 0; + $workbench->userId = $userInfo['id']; + $workbench->companyId = $userInfo['companyId']; + $workbench->createTime = time(); + $workbench->updateTime = time(); + $workbench->save(); + + // 根据类型创建对应的配置 + switch ($param['type']) { + case self::TYPE_AUTO_LIKE: // 自动点赞 + $config = new WorkbenchAutoLike; + $config->workbenchId = $workbench->id; + $config->interval = $param['interval']; + $config->maxLikes = $param['maxLikes']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->contentTypes = json_encode($param['contentTypes']); + $config->devices = json_encode($param['deviceGroups']); + $config->friends = json_encode($param['wechatFriends']); + // $config->targetGroups = json_encode($param['targetGroups']); + // $config->tagOperator = $param['tagOperator']; + $config->friendMaxLikes = $param['friendMaxLikes']; + $config->friendTags = $param['friendTags']; + $config->enableFriendTags = $param['enableFriendTags']; + $config->createTime = time(); + $config->updateTime = time(); + $config->save(); + break; + case self::TYPE_MOMENTS_SYNC: // 朋友圈同步 + $config = new WorkbenchMomentsSync; + $config->workbenchId = $workbench->id; + $config->syncInterval = $param['syncInterval']; + $config->syncCount = $param['syncCount']; + $config->syncType = $param['syncType']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->accountType = $param['accountType']; + $config->devices = json_encode($param['deviceGroups']); + $config->contentLibraries = json_encode($param['contentGroups'] ?? []); + $config->createTime = time(); + $config->updateTime = time(); + $config->save(); + break; + case self::TYPE_GROUP_PUSH: // 群消息推送 + $ownerWechatIds = $this->normalizeOwnerWechatIds($param['ownerWechatIds'] ?? []); + $groupPushData = $this->prepareGroupPushData($param, $ownerWechatIds); + $groupPushData['workbenchId'] = $workbench->id; + $groupPushData['createTime'] = time(); + $groupPushData['updateTime'] = time(); + $config = new WorkbenchGroupPush; + $config->save($groupPushData); + break; + case self::TYPE_GROUP_CREATE: // 自动建群 + $config = new WorkbenchGroupCreate; + $config->workbenchId = $workbench->id; + $config->planType = !empty($param['planType']) ? $param['planType'] : 0; + $config->executorId = !empty($param['executorId']) ? $param['executorId'] : 0; + + $config->devices = json_encode($param['deviceGroups'] ?? [], JSON_UNESCAPED_UNICODE); + $config->startTime = $param['startTime'] ?? ''; + $config->endTime = $param['endTime'] ?? ''; + $config->groupSizeMin = intval($param['groupSizeMin'] ?? 3); + $config->groupSizeMax = intval($param['groupSizeMax'] ?? 38); + $config->maxGroupsPerDay = intval($param['maxGroupsPerDay'] ?? 20); + $config->groupNameTemplate = $param['groupNameTemplate'] ?? ''; + $config->groupDescription = $param['groupDescription'] ?? ''; + $config->poolGroups = json_encode($param['poolGroups'] ?? [], JSON_UNESCAPED_UNICODE); + $config->wechatGroups = json_encode($param['wechatGroups'] ?? [], JSON_UNESCAPED_UNICODE); + + // 处理群管理员:如果启用了群管理员且有指定管理员,则保存到admins字段 + $admins = []; + if (!empty($param['groupAdminEnabled']) && !empty($param['groupAdminWechatId'])) { + // 如果groupAdminWechatId是数组,取第一个;如果是单个值,直接使用 + $adminWechatId = is_array($param['groupAdminWechatId']) ? $param['groupAdminWechatId'][0] : $param['groupAdminWechatId']; + // 如果是好友ID,直接添加到admins;如果是wechatId,需要转换为好友ID + if (is_numeric($adminWechatId)) { + $admins[] = intval($adminWechatId); + } else { + // 如果是wechatId字符串,需要查询对应的好友ID + $friend = Db::table('s2_wechat_friend')->where('wechatId', $adminWechatId)->find(); + if ($friend) { + $admins[] = intval($friend['id']); + } + } + } + // 如果传入了admins参数,优先使用(兼容旧逻辑) + if (!empty($param['admins']) && is_array($param['admins'])) { + $admins = array_merge($admins, $param['admins']); + } + $config->admins = json_encode(array_unique($admins), JSON_UNESCAPED_UNICODE); + + $config->fixedWechatIds = json_encode($param['fixedWechatIds'] ?? [], JSON_UNESCAPED_UNICODE); + $config->createTime = time(); + $config->updateTime = time(); + $config->save(); + break; + case self::TYPE_TRAFFIC_DISTRIBUTION: // 流量分发 + $config = new WorkbenchTrafficConfig; + $config->workbenchId = $workbench->id; + $config->distributeType = $param['distributeType']; + $config->maxPerDay = $param['maxPerDay']; + $config->timeType = $param['timeType']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->devices = json_encode($param['deviceGroups'], JSON_UNESCAPED_UNICODE); + $config->pools = json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE); + $config->account = json_encode($param['accountGroups'], JSON_UNESCAPED_UNICODE); + $config->createTime = time(); + $config->updateTime = time(); + $config->save(); + break; + case self::TYPE_IMPORT_CONTACT: //联系人导入 + $config = new WorkbenchImportContact; + $config->workbenchId = $workbench->id; + $config->devices = json_encode($param['deviceGroups'], JSON_UNESCAPED_UNICODE); + $config->pools = json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE); + $config->num = $param['num']; + $config->clearContact = $param['clearContact']; + $config->remark = $param['remark']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->createTime = time(); + $config->save(); + break; + } + + Db::commit(); + return json(['code' => 200, 'msg' => '创建成功', 'data' => ['id' => $workbench->id]]); + } catch (\Exception $e) { + Db::rollback(); + return json(['code' => 500, 'msg' => '创建失败:' . $e->getMessage()]); + } + } + + /** + * 获取工作台列表 + * @return \think\response\Json + */ + public function getList() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $type = $this->request->param('type', ''); + $keyword = $this->request->param('keyword', ''); + + $where = [ + ['companyId', '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0] + ]; + + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + + + // 添加类型筛选 + if ($type !== '') { + $where[] = ['type', '=', $type]; + } + + // 添加名称模糊搜索 + if ($keyword !== '') { + $where[] = ['name', 'like', '%' . $keyword . '%']; + } + + // 定义关联关系 + $with = [ + 'autoLike' => function ($query) { + $query->field('workbenchId,interval,maxLikes,startTime,endTime,contentTypes,devices,friends'); + }, + 'momentsSync' => function ($query) { + $query->field('workbenchId,syncInterval,syncCount,syncType,startTime,endTime,accountType,devices,contentLibraries'); + }, + 'trafficConfig' => function ($query) { + $query->field('workbenchId,distributeType,maxPerDay,timeType,startTime,endTime,devices,pools,account'); + }, + 'groupPush' => function ($query) { + $query->field('workbenchId,pushType,targetType,groupPushSubType,startTime,endTime,maxPerDay,pushOrder,isLoop,status,groups,friends,ownerWechatIds,trafficPools,contentLibraries,friendIntervalMin,friendIntervalMax,messageIntervalMin,messageIntervalMax,isRandomTemplate,postPushTags,announcementContent,enableAiRewrite,aiRewritePrompt'); + }, + 'groupCreate' => function ($query) { + $query->field('workbenchId,devices,startTime,endTime,groupSizeMin,groupSizeMax,maxGroupsPerDay,groupNameTemplate,groupDescription,poolGroups,wechatGroups,admins'); + }, + 'importContact' => function ($query) { + $query->field('workbenchId,devices,pools,num,remarkType,remark,clearContact,startTime,endTime'); + }, + 'user' => function ($query) { + $query->field('id,username'); + } + ]; + + $list = Workbench::where($where) + ->with($with) + ->field('id,companyId,name,type,status,autoStart,userId,createTime,updateTime') + ->order('id', 'desc') + ->page($page, $limit) + ->select() + ->each(function ($item) { + // 处理配置信息 + switch ($item->type) { + case self::TYPE_AUTO_LIKE: + if (!empty($item->autoLike)) { + $item->config = $item->autoLike; + $item->config->devices = json_decode($item->config->devices, true); + $item->config->contentTypes = json_decode($item->config->contentTypes, true); + $item->config->friends = json_decode($item->config->friends, true); + + // 添加今日点赞数 + $startTime = strtotime(date('Y-m-d') . ' 00:00:00'); + $endTime = strtotime(date('Y-m-d') . ' 23:59:59'); + $todayLikeCount = Db::name('workbench_auto_like_item') + ->where('workbenchId', $item->id) + ->whereTime('createTime', 'between', [$startTime, $endTime]) + ->count(); + + // 添加总点赞数 + $totalLikeCount = Db::name('workbench_auto_like_item') + ->where('workbenchId', $item->id) + ->count(); + + $item->config->todayLikeCount = $todayLikeCount; + $item->config->totalLikeCount = $totalLikeCount; + } + unset($item->autoLike, $item->auto_like); + break; + case self::TYPE_MOMENTS_SYNC: + if (!empty($item->momentsSync)) { + $item->config = $item->momentsSync; + $item->config->devices = json_decode($item->config->devices, true); + $item->config->contentGroups = json_decode($item->config->contentLibraries, true); + //同步记录 + $sendNum = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $item->id])->count(); + $item->syncCount = $sendNum; + $lastTime = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $item->id])->order('id DESC')->value('createTime'); + $item->lastSyncTime = !empty($lastTime) ? date('Y-m-d H:i', $lastTime) : '--'; + + + // 获取内容库名称 + if (!empty($item->config->contentGroups)) { + $libraryNames = ContentLibrary::where('id', 'in', $item->config->contentGroups)->select(); + $item->config->contentGroupsOptions = $libraryNames; + } else { + $item->config->contentGroupsOptions = []; + } + } + unset($item->momentsSync, $item->moments_sync, $item->config->contentLibraries); + break; + case self::TYPE_GROUP_PUSH: + if (!empty($item->groupPush)) { + $item->config = $item->groupPush; + $item->config->pushType = $item->config->pushType; + $item->config->targetType = isset($item->config->targetType) ? intval($item->config->targetType) : 1; // 默认1=群推送 + $item->config->groupPushSubType = isset($item->config->groupPushSubType) ? intval($item->config->groupPushSubType) : 1; // 默认1=群群发 + $item->config->startTime = $item->config->startTime; + $item->config->endTime = $item->config->endTime; + $item->config->maxPerDay = $item->config->maxPerDay; + $item->config->pushOrder = $item->config->pushOrder; + $item->config->isLoop = $item->config->isLoop; + $item->config->status = $item->config->status; + $item->config->ownerWechatIds = json_decode($item->config->ownerWechatIds ?? '[]', true) ?: []; + // 根据targetType解析不同的数据 + if ($item->config->targetType == 1) { + // 群推送 + $item->config->wechatGroups = json_decode($item->config->groups, true) ?: []; + $item->config->wechatFriends = []; + // 群推送不需要devices字段 + // 群公告相关字段 + if ($item->config->groupPushSubType == 2) { + $item->config->announcementContent = isset($item->config->announcementContent) ? $item->config->announcementContent : ''; + $item->config->enableAiRewrite = isset($item->config->enableAiRewrite) ? intval($item->config->enableAiRewrite) : 0; + $item->config->aiRewritePrompt = isset($item->config->aiRewritePrompt) ? $item->config->aiRewritePrompt : ''; + } + $item->config->trafficPools = []; + } else { + // 好友推送 + $item->config->wechatFriends = json_decode($item->config->friends, true) ?: []; + $item->config->wechatGroups = []; + $item->config->trafficPools = json_decode($item->config->trafficPools ?? '[]', true) ?: []; + } + $item->config->contentLibraries = json_decode($item->config->contentLibraries, true); + $item->config->postPushTags = json_decode($item->config->postPushTags ?? '[]', true) ?: []; + $item->config->lastPushTime = ''; + if (!empty($item->config->ownerWechatIds)) { + $ownerWechatOptions = Db::name('wechat_account') + ->whereIn('id', $item->config->ownerWechatIds) + ->field('id,wechatId,nickName,avatar,alias') + ->select(); + $item->config->ownerWechatOptions = $ownerWechatOptions; + } else { + $item->config->ownerWechatOptions = []; + } + } + unset($item->groupPush, $item->group_push); + break; + case self::TYPE_GROUP_CREATE: + if (!empty($item->groupCreate)) { + $item->config = $item->groupCreate; + $item->config->devices = json_decode($item->config->devices, true); + $item->config->poolGroups = json_decode($item->config->poolGroups, true); + $item->config->wechatGroups = json_decode($item->config->wechatGroups, true); + $item->config->admins = json_decode($item->config->admins ?? '[]', true) ?: []; + + // 处理群管理员相关字段 + $item->config->groupAdminEnabled = !empty($item->config->admins) ? 1 : 0; + + if (!empty($item->config->admins)) { + $adminOptions = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->where('wf.id', 'in', $item->config->admins) + ->order('wf.id', 'desc') + ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar') + ->select(); + $item->config->adminsOptions = $adminOptions; + // 如果有管理员,设置groupAdminWechatId为第一个管理员的ID(用于前端回显) + $item->config->groupAdminWechatId = !empty($item->config->admins) ? $item->config->admins[0] : null; + } else { + $item->config->adminsOptions = []; + $item->config->groupAdminWechatId = null; + } + } + unset($item->groupCreate, $item->group_create); + break; + case self::TYPE_TRAFFIC_DISTRIBUTION: + if (!empty($item->trafficConfig)) { + $item->config = $item->trafficConfig; + $item->config->devices = json_decode($item->config->devices, true); + $item->config->poolGroups = json_decode($item->config->pools, true); + $item->config->account = json_decode($item->config->account, true); + $config_item = Db::name('workbench_traffic_config_item')->where(['workbenchId' => $item->id])->order('id DESC')->find(); + $item->config->lastUpdated = !empty($config_item) ? date('Y-m-d H:i', $config_item['createTime']) : '--'; + + //统计 + $labels = $item->config->poolGroups; + $totalUsers = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->where([ + ['wf.isDeleted', '=', 0], + ['sa.departmentId', '=', $item->companyId] + ]) + ->whereIn('wa.currentDeviceId', $item->config->devices); + + if (!empty($labels) && count($labels) > 0) { + $totalUsers = $totalUsers->where(function ($q) use ($labels) { + foreach ($labels as $label) { + $q->whereOrRaw("JSON_CONTAINS(wf.labels, '\"{$label}\"')"); + } + }); + } + + $totalUsers = $totalUsers->count(); + $totalAccounts = count($item->config->account); + $dailyAverage = Db::name('workbench_traffic_config_item') + ->where('workbenchId', $item->id) + ->count(); + $day = (time() - strtotime($item->createTime)) / 86400; + $day = intval($day); + if ($dailyAverage > 0 && $totalAccounts > 0 && $day > 0) { + $dailyAverage = $dailyAverage / $totalAccounts / $day; + } + $item->config->total = [ + 'dailyAverage' => intval($dailyAverage), + 'totalAccounts' => $totalAccounts, + 'deviceCount' => count($item->config->devices), + 'poolCount' => !empty($item->config->poolGroups) ? count($item->config->poolGroups) : 'ALL', + 'totalUsers' => $totalUsers >> 0 + ]; + } + unset($item->trafficConfig, $item->traffic_config); + break; + + case self::TYPE_IMPORT_CONTACT: + if (!empty($item->importContact)) { + $item->config = $item->importContact; + $item->config->devices = json_decode($item->config->devices, true); + $item->config->poolGroups = json_decode($item->config->pools, true); + } + unset($item->importContact, $item->import_contact); + break; + } + // 添加创建人名称 + $item['creatorName'] = $item->user ? $item->user->username : ''; + unset($item['user']); // 移除关联数据 + return $item; + }); + + $total = Workbench::where($where)->count(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + + /** + * 获取工作台详情 + * @param int $id 工作台ID + * @return \think\response\Json + */ + public function detail() + { + $id = $this->request->param('id', ''); + + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + // 定义关联关系 + $with = [ + 'autoLike' => function ($query) { + $query->field('workbenchId,interval,maxLikes,startTime,endTime,contentTypes,devices,friends,friendMaxLikes,friendTags,enableFriendTags'); + }, + 'momentsSync' => function ($query) { + $query->field('workbenchId,syncInterval,syncCount,syncType,startTime,endTime,accountType,devices,contentLibraries'); + }, + 'trafficConfig' => function ($query) { + $query->field('workbenchId,distributeType,maxPerDay,timeType,startTime,endTime,devices,pools,account'); + }, + 'groupPush' => function ($query) { + $query->field('workbenchId,pushType,targetType,groupPushSubType,startTime,endTime,maxPerDay,pushOrder,isLoop,status,groups,friends,ownerWechatIds,trafficPools,contentLibraries,friendIntervalMin,friendIntervalMax,messageIntervalMin,messageIntervalMax,isRandomTemplate,postPushTags,announcementContent,enableAiRewrite,aiRewritePrompt'); + }, + 'groupCreate' => function ($query) { + $query->field('workbenchId,devices,startTime,endTime,groupSizeMin,groupSizeMax,maxGroupsPerDay,groupNameTemplate,groupDescription,poolGroups,wechatGroups,admins'); + }, + 'importContact' => function ($query) { + $query->field('workbenchId,devices,pools,num,remarkType,remark,clearContact,startTime,endTime'); + }, + ]; + + $where = [ + ['id', '=', $id], + ['companyId', '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + + $workbench = Workbench::where($where) + ->field('id,name,type,status,autoStart,createTime,updateTime,companyId') + ->with($with) + ->find(); + + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + + // 处理配置信息 + switch ($workbench->type) { + //自动点赞 + case self::TYPE_AUTO_LIKE: + if (!empty($workbench->autoLike)) { + $workbench->config = $workbench->autoLike; + $workbench->config->deviceGroups = json_decode($workbench->config->devices, true); + $workbench->config->wechatFriends = json_decode($workbench->config->friends, true); + $workbench->config->targetType = 2; + //$workbench->config->targetGroups = json_decode($workbench->config->targetGroups, true); + $workbench->config->contentTypes = json_decode($workbench->config->contentTypes, true); + + // 添加今日点赞数 + $startTime = strtotime(date('Y-m-d') . ' 00:00:00'); + $endTime = strtotime(date('Y-m-d') . ' 23:59:59'); + $todayLikeCount = Db::name('workbench_auto_like_item') + ->where('workbenchId', $workbench->id) + ->whereTime('createTime', 'between', [$startTime, $endTime]) + ->count(); + + // 添加总点赞数 + $totalLikeCount = Db::name('workbench_auto_like_item') + ->where('workbenchId', $workbench->id) + ->count(); + + $workbench->config->todayLikeCount = $todayLikeCount; + $workbench->config->totalLikeCount = $totalLikeCount; + + unset($workbench->autoLike, $workbench->auto_like); + } + break; + //自动同步朋友圈 + case self::TYPE_MOMENTS_SYNC: + if (!empty($workbench->momentsSync)) { + $workbench->config = $workbench->momentsSync; + $workbench->config->deviceGroups = json_decode($workbench->config->devices, true); + $workbench->config->contentGroups = json_decode($workbench->config->contentLibraries, true); + + //同步记录 + $sendNum = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $workbench->id])->count(); + $workbench->syncCount = $sendNum; + $lastTime = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $workbench->id])->order('id DESC')->value('createTime'); + $workbench->lastSyncTime = !empty($lastTime) ? date('Y-m-d H:i', $lastTime) : '--'; + unset($workbench->momentsSync, $workbench->moments_sync); + } + break; + //群推送 + case self::TYPE_GROUP_PUSH: + if (!empty($workbench->groupPush)) { + $workbench->config = $workbench->groupPush; + $workbench->config->targetType = isset($workbench->config->targetType) ? intval($workbench->config->targetType) : 1; // 默认1=群推送 + $workbench->config->groupPushSubType = isset($workbench->config->groupPushSubType) ? intval($workbench->config->groupPushSubType) : 1; // 默认1=群群发 + $workbench->config->ownerWechatIds = json_decode($workbench->config->ownerWechatIds ?? '[]', true) ?: []; + // 根据targetType解析不同的数据 + if ($workbench->config->targetType == 1) { + // 群推送 + $workbench->config->wechatGroups = json_decode($workbench->config->groups, true) ?: []; + $workbench->config->wechatFriends = []; + $workbench->config->trafficPools = []; + // 群推送不需要devices字段 + // 群公告相关字段 + if ($workbench->config->groupPushSubType == 2) { + $workbench->config->announcementContent = isset($workbench->config->announcementContent) ? $workbench->config->announcementContent : ''; + $workbench->config->enableAiRewrite = isset($workbench->config->enableAiRewrite) ? intval($workbench->config->enableAiRewrite) : 0; + $workbench->config->aiRewritePrompt = isset($workbench->config->aiRewritePrompt) ? $workbench->config->aiRewritePrompt : ''; + } + } else { + // 好友推送 + $workbench->config->wechatFriends = json_decode($workbench->config->friends, true) ?: []; + $workbench->config->wechatGroups = []; + $workbench->config->trafficPools = json_decode($workbench->config->trafficPools ?? '[]', true) ?: []; + } + $workbench->config->contentLibraries = json_decode($workbench->config->contentLibraries, true); + $workbench->config->postPushTags = json_decode($workbench->config->postPushTags ?? '[]', true) ?: []; + unset($workbench->groupPush, $workbench->group_push); + } + break; + //建群助手 + case self::TYPE_GROUP_CREATE: + if (!empty($workbench->groupCreate)) { + $workbench->config = $workbench->groupCreate; + $workbench->config->deviceGroups = json_decode($workbench->config->devices, true); + $workbench->config->poolGroups = json_decode($workbench->config->poolGroups, true); + $workbench->config->wechatGroups = json_decode($workbench->config->wechatGroups, true); + $workbench->config->admins = json_decode($workbench->config->admins ?? '[]', true) ?: []; + + // 处理群管理员相关字段 + $workbench->config->groupAdminEnabled = !empty($workbench->config->admins) ? 1 : 0; + + // 如果有管理员,设置groupAdminWechatId为第一个管理员的ID(用于前端回显) + $workbench->config->groupAdminWechatId = !empty($workbench->config->admins) ? $workbench->config->admins[0] : null; + + // 统计已建群数(状态为成功且groupId不为空的记录,按groupId分组去重) + $createdGroupsCount = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbench->id) + ->where('status', 2) // STATUS_SUCCESS = 2 + ->where('groupId', '<>', null) + ->group('groupId') + ->count(); + + // 统计总人数(该工作台的所有记录数) + $totalMembersCount = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbench->id) + ->count(); + + // 添加统计信息 + $workbench->config->stats = [ + 'createdGroupsCount' => $createdGroupsCount, + 'totalMembersCount' => $totalMembersCount + ]; + + unset($workbench->groupCreate, $workbench->group_create); + } + break; + //流量分发 + case self::TYPE_TRAFFIC_DISTRIBUTION: + if (!empty($workbench->trafficConfig)) { + $workbench->config = $workbench->trafficConfig; + $workbench->config->deviceGroups = json_decode($workbench->config->devices, true); + $workbench->config->accountGroups = json_decode($workbench->config->account, true); + $workbench->config->poolGroups = json_decode($workbench->config->pools, true); + $config_item = Db::name('workbench_traffic_config_item')->where(['workbenchId' => $workbench->id])->order('id DESC')->find(); + $workbench->config->lastUpdated = !empty($config_item) ? date('Y-m-d H:i', $config_item['createTime']) : '--'; + + //统计 + $labels = $workbench->config->poolGroups; + $totalUsers = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->where([ + ['wf.isDeleted', '=', 0], + ['sa.departmentId', '=', $workbench->companyId] + ]) + ->whereIn('wa.currentDeviceId', $workbench->config->deviceGroups) + ->field('wf.id,wf.wechatAccountId,wf.wechatId,wf.labels,sa.userName,wa.currentDeviceId as deviceId') + ->where(function ($q) use ($labels) { + foreach ($labels as $label) { + $q->whereOrRaw("JSON_CONTAINS(wf.labels, '\"{$label}\"')"); + } + })->count(); + + $totalAccounts = Db::table('s2_company_account') + ->alias('a') + ->where(['a.departmentId' => $workbench->companyId, 'a.status' => 0]) + ->whereNotLike('a.userName', '%_offline%') + ->whereNotLike('a.userName', '%_delete%') + ->group('a.id') + ->count(); + + $dailyAverage = Db::name('workbench_traffic_config_item') + ->where('workbenchId', $workbench->id) + ->count(); + $day = (time() - strtotime($workbench->createTime)) / 86400; + $day = intval($day); + + + if ($dailyAverage > 0) { + $dailyAverage = $dailyAverage / $totalAccounts / $day; + } + + $workbench->config->total = [ + 'dailyAverage' => intval($dailyAverage), + 'totalAccounts' => $totalAccounts, + 'deviceCount' => count($workbench->config->deviceGroups), + 'poolCount' => count($workbench->config->poolGroups), + 'totalUsers' => $totalUsers >> 0 + ]; + unset($workbench->trafficConfig, $workbench->traffic_config); + } + break; + case self::TYPE_IMPORT_CONTACT: + if (!empty($workbench->importContact)) { + $workbench->config = $workbench->importContact; + $workbench->config->deviceGroups = json_decode($workbench->config->devices, true); + $workbench->config->poolGroups = json_decode($workbench->config->pools, true); + } + unset($workbench->importContact, $workbench->import_contact); + break; + } + unset( + $workbench->autoLike, + $workbench->momentsSync, + $workbench->groupPush, + $workbench->groupCreate, + $workbench->config->devices, + $workbench->config->friends, + $workbench->config->groups, + $workbench->config->contentLibraries, + $workbench->config->account, + ); + + + //获取设备信息 + if (!empty($workbench->config->deviceGroups)) { + $deviceList = DeviceModel::alias('d') + ->field([ + 'd.id', 'd.imei', 'd.memo', 'd.alive', + 'l.wechatId', + 'a.nickname', 'a.alias', 'a.avatar', 'a.alias', '0 totalFriend' + ]) + ->leftJoin('device_wechat_login l', 'd.id = l.deviceId and l.alive =' . DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE . ' and l.companyId = d.companyId') + ->leftJoin('wechat_account a', 'l.wechatId = a.wechatId') + ->whereIn('d.id', $workbench->config->deviceGroups) + ->order('d.id desc') + ->select(); + + foreach ($deviceList as &$device) { + $curstomer = WechatCustomerModel::field('friendShip')->where(['wechatId' => $device['wechatId']])->find(); + $device['totalFriend'] = $curstomer->friendShip->totalFriend ?? 0; + } + unset($device); + + $workbench->config->deviceGroupsOptions = $deviceList; + } else { + $workbench->config->deviceGroupsOptions = []; + } + + + // 获取群(当targetType=1时) + if (!empty($workbench->config->wechatGroups) && isset($workbench->config->targetType) && $workbench->config->targetType == 1) { + $groupList = Db::name('wechat_group')->alias('wg') + ->join('wechat_account wa', 'wa.wechatId = wg.ownerWechatId') + ->where('wg.id', 'in', $workbench->config->wechatGroups) + ->order('wg.id', 'desc') + ->field('wg.id,wg.name as groupName,wg.ownerWechatId,wa.nickName,wa.avatar,wa.alias,wg.avatar as groupAvatar') + ->select(); + $workbench->config->wechatGroupsOptions = $groupList; + } else { + $workbench->config->wechatGroupsOptions = []; + } + + // 获取好友(当targetType=2时) + if (!empty($workbench->config->wechatFriends) && isset($workbench->config->targetType) && $workbench->config->targetType == 2) { + $friendList = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->where('wf.id', 'in', $workbench->config->wechatFriends) + ->order('wf.id', 'desc') + ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar') + ->select(); + $workbench->config->wechatFriendsOptions = $friendList; + } else { + $workbench->config->wechatFriendsOptions = []; + } + + // 获取流量池(当targetType=2时) + if (!empty($workbench->config->trafficPools) && isset($workbench->config->targetType) && $workbench->config->targetType == 2) { + $poolList = Db::name('traffic_source_package')->alias('tsp') + ->leftJoin('traffic_source_package_item tspi', 'tspi.packageId = tsp.id and tspi.isDel = 0') + ->whereIn('tsp.id', $workbench->config->trafficPools) + ->where('tsp.isDel', 0) + ->whereIn('tsp.companyId', [$this->request->userInfo['companyId'], 0]) + ->field('tsp.id,tsp.name,tsp.description,tsp.pic,COUNT(tspi.id) as itemCount') + ->group('tsp.id') + ->order('tsp.id', 'desc') + ->select(); + $workbench->config->trafficPoolsOptions = $poolList; + } else { + $workbench->config->trafficPoolsOptions = []; + } + + // 获取内容库名称 + if (!empty($workbench->config->contentGroups)) { + $libraryNames = ContentLibrary::where('id', 'in', $workbench->config->contentGroups)->select(); + $workbench->config->contentGroupsOptions = $libraryNames; + } else { + $workbench->config->contentGroupsOptions = []; + } + + //账号 + if (!empty($workbench->config->accountGroups)) { + $account = Db::table('s2_company_account')->alias('a') + ->where(['a.departmentId' => $this->request->userInfo['companyId'], 'a.status' => 0]) + ->whereIn('a.id', $workbench->config->accountGroups) + ->whereNotLike('a.userName', '%_offline%') + ->whereNotLike('a.userName', '%_delete%') + ->field('a.id,a.userName,a.realName,a.nickname,a.memo') + ->select(); + $workbench->config->accountGroupsOptions = $account; + } else { + $workbench->config->accountGroupsOptions = []; + } + + if (!empty($workbench->config->poolGroups)) { + $poolGroupsOptions = Db::name('traffic_source_package')->alias('tsp') + ->join('traffic_source_package_item tspi', 'tspi.packageId=tsp.id', 'left') + ->whereIn('tsp.companyId', [$this->request->userInfo['companyId'], 0]) + ->whereIn('tsp.id', $workbench->config->poolGroups) + ->field('tsp.id,tsp.name,tsp.description,tsp.createTime,count(tspi.id) as num') + ->group('tsp.id') + ->select(); + $workbench->config->poolGroupsOptions = $poolGroupsOptions; + } else { + $workbench->config->poolGroupsOptions = []; + } + + if (!empty($workbench->config->ownerWechatIds)) { + $ownerWechatOptions = Db::name('wechat_account') + ->whereIn('id', $workbench->config->ownerWechatIds) + ->field('id,wechatId,nickName,avatar,alias') + ->select(); + $workbench->config->ownerWechatOptions = $ownerWechatOptions; + } else { + $workbench->config->ownerWechatOptions = []; + } + + // 获取群组选项(自动建群) + if ($workbench->type == self::TYPE_GROUP_CREATE && !empty($workbench->config->wechatGroups)) { + // 分离数字ID(好友ID)和字符串ID(手动创建的群组) + $friendIds = []; + $manualGroupIds = []; + + foreach ($workbench->config->wechatGroups as $groupId) { + if (is_numeric($groupId)) { + $friendIds[] = intval($groupId); + } else { + $manualGroupIds[] = $groupId; + } + } + + $wechatGroupsOptions = []; + + // 查询好友信息(数字ID) + if (!empty($friendIds)) { + $friendList = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->join(['s2_company_account' => 'ca'], 'ca.id = wf.accountId', 'left') + ->where('wf.id', 'in', $friendIds) + ->order('wf.id', 'desc') + ->field('wf.id,wf.wechatId,wf.nickname,wf.avatar,wf.alias,wf.gender,wf.phone,wa.nickName as accountNickname,ca.userName as account,ca.realName as username,wf.createTime,wf.updateTime,wf.deleteTime,wf.ownerWechatId') + ->select(); + + // 获取群主信息 + foreach ($friendList as &$friend) { + if (!empty($friend['ownerWechatId'])) { + $owner = Db::name('wechat_account') + ->where('wechatId', $friend['ownerWechatId']) + ->field('nickName,alias') + ->find(); + $friend['ownerNickname'] = $owner['nickName'] ?? ''; + $friend['ownerAlias'] = $owner['alias'] ?? ''; + } else { + $friend['ownerNickname'] = ''; + $friend['ownerAlias'] = ''; + } + $friend['isManual'] = ''; + // 格式化时间 + $friend['createTime'] = !empty($friend['createTime']) ? date('Y-m-d H:i:s', $friend['createTime']) : ''; + $friend['updateTime'] = !empty($friend['updateTime']) ? date('Y-m-d H:i:s', $friend['updateTime']) : ''; + $friend['deleteTime'] = !empty($friend['deleteTime']) ? date('Y-m-d H:i:s', $friend['deleteTime']) : ''; + } + unset($friend); + + $wechatGroupsOptions = array_merge($wechatGroupsOptions, $friendList); + } + + // 处理手动创建的群组(字符串ID) + if (!empty($manualGroupIds)) { + foreach ($manualGroupIds as $groupId) { + // 手动创建的群组,只返回基本信息 + $wechatGroupsOptions[] = [ + 'id' => $groupId, + 'wechatId' => $groupId, + 'nickname' => $groupId, + 'avatar' => '', + 'isManual' => 1 + ]; + } + } + + $workbench->config->wechatGroupsOptions = $wechatGroupsOptions; + } else { + $workbench->config->wechatGroupsOptions = []; + } + + // 获取管理员选项(自动建群) + if ($workbench->type == self::TYPE_GROUP_CREATE && !empty($workbench->config->admins)) { + $adminOptions = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->where('wf.id', 'in', $workbench->config->admins) + ->order('wf.id', 'desc') + ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar') + ->select(); + $workbench->config->adminsOptions = $adminOptions; + } else { + $workbench->config->adminsOptions = []; + } + + return json(['code' => 200, 'msg' => '获取成功', 'data' => $workbench]); + } + + /** + * 更新工作台 + * @return \think\response\Json + */ + public function update() + { + if (!$this->request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + // 获取请求参数 + $param = $this->request->post(); + + // 验证数据 + $validate = new WorkbenchValidate; + if (!$validate->scene('update')->check($param)) { + return json(['code' => 400, 'msg' => $validate->getError()]); + } + + + $where = [ + ['id', '=', $param['id']], + ['companyId', '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + // 查询工作台是否存在 + $workbench = Workbench::where($where)->find(); + if (!$workbench) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + + Db::startTrans(); + try { + // 更新工作台基本信息 + $workbench->name = $param['name']; + $workbench->status = !empty($param['status']) ? 1 : 0; + $workbench->autoStart = !empty($param['autoStart']) ? 1 : 0; + $workbench->updateTime = time(); + $workbench->save(); + + // 根据类型更新对应的配置 + switch ($workbench->type) { + case self::TYPE_AUTO_LIKE: + $config = WorkbenchAutoLike::where('workbenchId', $param['id'])->find(); + if ($config) { + $config->interval = $param['interval']; + $config->maxLikes = $param['maxLikes']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->contentTypes = json_encode($param['contentTypes']); + $config->devices = json_encode($param['deviceGroups']); + $config->friends = json_encode($param['wechatFriends']); + // $config->targetGroups = json_encode($param['targetGroups']); + // $config->tagOperator = $param['tagOperator']; + $config->friendMaxLikes = $param['friendMaxLikes']; + $config->friendTags = $param['friendTags']; + $config->enableFriendTags = $param['enableFriendTags']; + $config->updateTime = time(); + $config->save(); + } + break; + + case self::TYPE_MOMENTS_SYNC: + $config = WorkbenchMomentsSync::where('workbenchId', $param['id'])->find(); + if ($config) { + if (!empty($param['contentGroups'])) { + foreach ($param['contentGroups'] as $library) { + if (isset($library['id']) && !empty($library['id'])) { + $contentLibraries[] = $library['id']; + } else { + $contentLibraries[] = $library; + } + } + } else { + $contentLibraries = []; + } + + $config->syncInterval = $param['syncInterval']; + $config->syncCount = $param['syncCount']; + $config->syncType = $param['syncType']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->accountType = $param['accountType']; + $config->devices = json_encode($param['deviceGroups']); + $config->contentLibraries = json_encode($contentLibraries); + $config->updateTime = time(); + $config->save(); + } + break; + + case self::TYPE_GROUP_PUSH: + $config = WorkbenchGroupPush::where('workbenchId', $param['id'])->find(); + if ($config) { + $ownerWechatIds = $this->normalizeOwnerWechatIds($param['ownerWechatIds'] ?? null, $config); + $groupPushData = $this->prepareGroupPushData($param, $ownerWechatIds, $config); + $groupPushData['updateTime'] = time(); + $config->save($groupPushData); + } + break; + + case self::TYPE_GROUP_CREATE: + $config = WorkbenchGroupCreate::where('workbenchId', $param['id'])->find(); + if ($config) { + $config->devices = json_encode($param['deviceGroups'] ?? [], JSON_UNESCAPED_UNICODE); + $config->startTime = $param['startTime'] ?? ''; + $config->endTime = $param['endTime'] ?? ''; + $config->groupSizeMin = intval($param['groupSizeMin'] ?? 3); + $config->groupSizeMax = intval($param['groupSizeMax'] ?? 38); + $config->maxGroupsPerDay = intval($param['maxGroupsPerDay'] ?? 20); + $config->groupNameTemplate = $param['groupNameTemplate'] ?? ''; + $config->groupDescription = $param['groupDescription'] ?? ''; + $config->poolGroups = json_encode($param['poolGroups'] ?? [], JSON_UNESCAPED_UNICODE); + $config->wechatGroups = json_encode($param['wechatGroups'] ?? [], JSON_UNESCAPED_UNICODE); + + // 处理群管理员:如果启用了群管理员且有指定管理员,则保存到admins字段 + $admins = []; + if (!empty($param['groupAdminEnabled']) && !empty($param['groupAdminWechatId'])) { + // 如果groupAdminWechatId是数组,取第一个;如果是单个值,直接使用 + $adminWechatId = is_array($param['groupAdminWechatId']) ? $param['groupAdminWechatId'][0] : $param['groupAdminWechatId']; + // 如果是好友ID,直接添加到admins;如果是wechatId,需要转换为好友ID + if (is_numeric($adminWechatId)) { + $admins[] = intval($adminWechatId); + } else { + // 如果是wechatId字符串,需要查询对应的好友ID + $friend = Db::table('s2_wechat_friend')->where('wechatId', $adminWechatId)->find(); + if ($friend) { + $admins[] = intval($friend['id']); + } + } + } + // 如果传入了admins参数,优先使用(兼容旧逻辑) + if (!empty($param['admins']) && is_array($param['admins'])) { + $admins = array_merge($admins, $param['admins']); + } + $config->admins = json_encode(array_unique($admins), JSON_UNESCAPED_UNICODE); + + $config->updateTime = time(); + $config->save(); + } + break; + case self::TYPE_TRAFFIC_DISTRIBUTION: + $config = WorkbenchTrafficConfig::where('workbenchId', $param['id'])->find(); + if ($config) { + $config->distributeType = $param['distributeType']; + $config->maxPerDay = $param['maxPerDay']; + $config->timeType = $param['timeType']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->devices = json_encode($param['deviceGroups']); + $config->pools = json_encode($param['poolGroups']); + $config->account = json_encode($param['accountGroups']); + $config->updateTime = time(); + $config->save(); + } + break; + case self::TYPE_IMPORT_CONTACT: //联系人导入 + $config = WorkbenchImportContact::where('workbenchId', $param['id'])->find();; + if ($config) { + $config->devices = json_encode($param['deviceGroups']); + $config->pools = json_encode($param['poolGroups']); + $config->num = $param['num']; + $config->clearContact = $param['clearContact']; + $config->remark = $param['remark']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->save(); + } + break; + } + + Db::commit(); + return json(['code' => 200, 'msg' => '更新成功']); + } catch (\Exception $e) { + Db::rollback(); + return json(['code' => 500, 'msg' => '更新失败:' . $e->getMessage()]); + } + } + + /** + * 更新工作台状态 + * @return \think\response\Json + */ + public function updateStatus() + { + if (!$this->request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + $id = $this->request->param('id', ''); + + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + + + $where = [ + ['id', '=', $id], + ['companyId', '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + $workbench = Workbench::where($where)->find(); + + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + + $workbench->status = !$workbench['status']; + $workbench->save(); + + return json(['code' => 200, 'msg' => '更新成功']); + } + + /** + * 删除工作台(软删除) + */ + public function delete() + { + $id = $this->request->param('id'); + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + $where = [ + ['id', '=', $id], + ['companyId', '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + $workbench = Workbench::where($where)->find(); + + if (!$workbench) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + + // 软删除 + $workbench->isDel = 1; + $workbench->deleteTime = time(); + $workbench->save(); + + return json(['code' => 200, 'msg' => '删除成功']); + } + + /** + * 拷贝工作台 + * @return \think\response\Json + */ + public function copy() + { + if (!$this->request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + $id = $this->request->post('id'); + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + // 验证权限并获取原数据 + $workbench = Workbench::where([ + ['id', '=', $id], + ['userId', '=', $this->request->userInfo['id']] + ])->find(); + + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + + Db::startTrans(); + try { + // 创建新的工作台基本信息 + $newWorkbench = new Workbench; + $newWorkbench->name = $workbench->name . ' copy'; + $newWorkbench->type = $workbench->type; + $newWorkbench->status = 1; // 新拷贝的默认启用 + $newWorkbench->autoStart = $workbench->autoStart; + $newWorkbench->userId = $this->request->userInfo['id']; + $newWorkbench->companyId = $this->request->userInfo['companyId']; + $newWorkbench->save(); + + // 根据类型拷贝对应的配置 + switch ($workbench->type) { + case self::TYPE_AUTO_LIKE: + $config = WorkbenchAutoLike::where('workbenchId', $id)->find(); + if ($config) { + $newConfig = new WorkbenchAutoLike; + $newConfig->workbenchId = $newWorkbench->id; + $newConfig->interval = $config->interval; + $newConfig->maxLikes = $config->maxLikes; + $newConfig->startTime = $config->startTime; + $newConfig->endTime = $config->endTime; + $newConfig->contentTypes = $config->contentTypes; + $newConfig->devices = $config->devices; + $newConfig->friends = $config->friends; + $newConfig->createTime = time(); + $newConfig->updateTime = time(); + $newConfig->save(); + } + break; + case self::TYPE_MOMENTS_SYNC: + $config = WorkbenchMomentsSync::where('workbenchId', $id)->find(); + if ($config) { + $newConfig = new WorkbenchMomentsSync; + $newConfig->workbenchId = $newWorkbench->id; + $newConfig->syncInterval = $config->syncInterval; + $newConfig->syncCount = $config->syncCount; + $newConfig->syncType = $config->syncType; + $newConfig->startTime = $config->startTime; + $newConfig->endTime = $config->endTime; + $newConfig->accountType = $config->accountType; + $newConfig->devices = $config->devices; + $newConfig->contentLibraries = $config->contentLibraries; + $newConfig->createTime = time(); + $newConfig->updateTime = time(); + $newConfig->save(); + } + break; + case self::TYPE_GROUP_PUSH: + $config = WorkbenchGroupPush::where('workbenchId', $id)->find(); + if ($config) { + $newConfig = new WorkbenchGroupPush; + $newConfig->workbenchId = $newWorkbench->id; + $newConfig->pushType = $config->pushType; + $newConfig->targetType = isset($config->targetType) ? $config->targetType : 1; // 默认1=群推送 + $newConfig->startTime = $config->startTime; + $newConfig->endTime = $config->endTime; + $newConfig->maxPerDay = $config->maxPerDay; + $newConfig->pushOrder = $config->pushOrder; + $newConfig->isLoop = $config->isLoop; + $newConfig->status = $config->status; + $newConfig->groups = $config->groups; + $newConfig->friends = $config->friends; + $newConfig->contentLibraries = $config->contentLibraries; + $newConfig->trafficPools = property_exists($config, 'trafficPools') ? $config->trafficPools : json_encode([], JSON_UNESCAPED_UNICODE); + $newConfig->socialMediaId = $config->socialMediaId; + $newConfig->promotionSiteId = $config->promotionSiteId; + $newConfig->ownerWechatIds = $config->ownerWechatIds; + $newConfig->createTime = time(); + $newConfig->updateTime = time(); + $newConfig->save(); + } + break; + case self::TYPE_GROUP_CREATE: + $config = WorkbenchGroupCreate::where('workbenchId', $id)->find(); + if ($config) { + $newConfig = new WorkbenchGroupCreate; + $newConfig->workbenchId = $newWorkbench->id; + $newConfig->devices = $config->devices; + $newConfig->startTime = $config->startTime; + $newConfig->endTime = $config->endTime; + $newConfig->groupSizeMin = $config->groupSizeMin; + $newConfig->groupSizeMax = $config->groupSizeMax; + $newConfig->maxGroupsPerDay = $config->maxGroupsPerDay; + $newConfig->groupNameTemplate = $config->groupNameTemplate; + $newConfig->groupDescription = $config->groupDescription; + $newConfig->poolGroups = $config->poolGroups; + $newConfig->wechatGroups = $config->wechatGroups; + $newConfig->admins = $config->admins ?? json_encode([], JSON_UNESCAPED_UNICODE); + $newConfig->createTime = time(); + $newConfig->updateTime = time(); + $newConfig->save(); + } + break; + case self::TYPE_IMPORT_CONTACT: //联系人导入 + $config = WorkbenchImportContact::where('workbenchId', $id)->find(); + if ($config) { + $newConfig = new WorkbenchImportContact; + $newConfig->workbenchId = $newWorkbench->id; + $newConfig->devices = $config->devices; + $newConfig->pools = $config->pools; + $newConfig->num = $config->num; + $newConfig->clearContact = $config->clearContact; + $newConfig->remark = $config->remark; + $newConfig->startTime = $config->startTime; + $newConfig->endTime = $config->endTime; + $newConfig->createTime = time(); + $newConfig->save(); + } + break; + } + + Db::commit(); + return json(['code' => 200, 'msg' => '拷贝成功', 'data' => ['id' => $newWorkbench->id]]); + } catch (\Exception $e) { + Db::rollback(); + return json(['code' => 500, 'msg' => '拷贝失败:' . $e->getMessage()]); + } + } + + /** + * 获取点赞记录列表 + * @return \think\response\Json + */ + public function getLikeRecords() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $workbenchId = $this->request->param('workbenchId', 0); + + $where = [ + ['wali.workbenchId', '=', $workbenchId] + ]; + + // 查询点赞记录 + $list = Db::name('workbench_auto_like_item')->alias('wali') + ->join(['s2_wechat_moments' => 'wm'], 'wali.snsId = wm.snsId') + ->field([ + 'wali.id', + 'wali.workbenchId', + 'wali.momentsId', + 'wali.snsId', + 'wali.wechatAccountId', + 'wali.wechatFriendId', + 'wali.createTime as likeTime', + 'wm.content', + 'wm.resUrls', + 'wm.createTime as momentTime', + 'wm.userName', + ]) + ->where($where) + ->order('wali.createTime', 'desc') + ->group('wali.id') + ->page($page, $limit) + ->select(); + + // 处理数据 + foreach ($list as &$item) { + //处理用户信息 + $friend = Db::table('s2_wechat_friend') + ->where(['id' => $item['wechatFriendId']]) + ->field('nickName,avatar') + ->find(); + if (!empty($friend)) { + $item['friendName'] = $friend['nickName']; + $item['friendAvatar'] = $friend['avatar']; + } else { + $item['friendName'] = ''; + $item['friendAvatar'] = ''; + } + + //处理客服 + $friend = Db::table('s2_wechat_account') + ->where(['id' => $item['wechatAccountId']]) + ->field('nickName,avatar') + ->find(); + if (!empty($friend)) { + $item['operatorName'] = $friend['nickName']; + $item['operatorAvatar'] = $friend['avatar']; + } else { + $item['operatorName'] = ''; + $item['operatorAvatar'] = ''; + } + + // 处理时间格式 + $item['likeTime'] = date('Y-m-d H:i:s', $item['likeTime']); + $item['momentTime'] = !empty($item['momentTime']) ? date('Y-m-d H:i:s', $item['momentTime']) : ''; + + // 处理资源链接 + if (!empty($item['resUrls'])) { + $item['resUrls'] = json_decode($item['resUrls'], true); + } else { + $item['resUrls'] = []; + } + } + + // 获取总记录数 + $total = Db::name('workbench_auto_like_item')->alias('wali') + ->where($where) + ->count(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + + /** + * 获取朋友圈发布记录列表 + * @return \think\response\Json + */ + public function getMomentsRecords() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $workbenchId = $this->request->param('workbenchId', 0); + + $where = [ + ['wmsi.workbenchId', '=', $workbenchId] + ]; + + // 查询发布记录 + $list = Db::name('workbench_moments_sync_item')->alias('wmsi') + ->join('content_item ci', 'ci.id = wmsi.contentId', 'left') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wmsi.wechatAccountId', 'left') + ->field([ + 'wmsi.id', + 'wmsi.workbenchId', + 'wmsi.createTime as publishTime', + 'ci.contentType', + 'ci.content', + 'ci.resUrls', + 'ci.urls', + 'wa.nickName as operatorName', + 'wa.avatar as operatorAvatar' + ]) + ->where($where) + ->order('wmsi.createTime', 'desc') + ->page($page, $limit) + ->select(); + + foreach ($list as &$item) { + $item['resUrls'] = json_decode($item['resUrls'], true); + $item['urls'] = json_decode($item['urls'], true); + } + + // 获取总记录数 + $total = Db::name('workbench_moments_sync_item')->alias('wmsi') + ->where($where) + ->count(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + + /** + * 获取朋友圈发布统计 + * @return \think\response\Json + */ + public function getMomentsStats() + { + $workbenchId = $this->request->param('workbenchId', 0); + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + // 获取今日数据 + $todayStart = strtotime(date('Y-m-d') . ' 00:00:00'); + $todayEnd = strtotime(date('Y-m-d') . ' 23:59:59'); + + $todayStats = Db::name('workbench_moments_sync_item') + ->where([ + ['workbenchId', '=', $workbenchId], + ['createTime', 'between', [$todayStart, $todayEnd]] + ]) + ->field([ + 'COUNT(*) as total', + 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success', + 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed' + ]) + ->find(); + + // 获取总数据 + $totalStats = Db::name('workbench_moments_sync_item') + ->where('workbenchId', $workbenchId) + ->field([ + 'COUNT(*) as total', + 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success', + 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed' + ]) + ->find(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'today' => [ + 'total' => intval($todayStats['total']), + 'success' => intval($todayStats['success']), + 'failed' => intval($todayStats['failed']) + ], + 'total' => [ + 'total' => intval($totalStats['total']), + 'success' => intval($totalStats['success']), + 'failed' => intval($totalStats['failed']) + ] + ] + ]); + } + + /** + * 获取流量分发记录列表 + * @return \think\response\Json + */ + public function getTrafficDistributionRecords() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $workbenchId = $this->request->param('workbenchId', 0); + + $where = [ + ['wtdi.workbenchId', '=', $workbenchId] + ]; + + // 查询分发记录 + $list = Db::name('workbench_traffic_distribution_item')->alias('wtdi') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wtdi.wechatAccountId', 'left') + ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wtdi.wechatFriendId', 'left') + ->field([ + 'wtdi.id', + 'wtdi.workbenchId', + 'wtdi.wechatAccountId', + 'wtdi.wechatFriendId', + 'wtdi.createTime as distributeTime', + 'wtdi.status', + 'wtdi.errorMsg', + 'wa.nickName as operatorName', + 'wa.avatar as operatorAvatar', + 'wf.nickName as friendName', + 'wf.avatar as friendAvatar', + 'wf.gender', + 'wf.province', + 'wf.city' + ]) + ->where($where) + ->order('wtdi.createTime', 'desc') + ->page($page, $limit) + ->select(); + + // 处理数据 + foreach ($list as &$item) { + // 处理时间格式 + $item['distributeTime'] = date('Y-m-d H:i:s', $item['distributeTime']); + + // 处理性别 + $genderMap = [ + 0 => '未知', + 1 => '男', + 2 => '女' + ]; + $item['genderText'] = $genderMap[$item['gender']] ?? '未知'; + + // 处理状态文字 + $statusMap = [ + 0 => '待分发', + 1 => '分发成功', + 2 => '分发失败' + ]; + $item['statusText'] = $statusMap[$item['status']] ?? '未知状态'; + } + + // 获取总记录数 + $total = Db::name('workbench_traffic_distribution_item')->alias('wtdi') + ->where($where) + ->count(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + + /** + * 获取流量分发统计 + * @return \think\response\Json + */ + public function getTrafficDistributionStats() + { + $workbenchId = $this->request->param('workbenchId', 0); + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + // 获取今日数据 + $todayStart = strtotime(date('Y-m-d') . ' 00:00:00'); + $todayEnd = strtotime(date('Y-m-d') . ' 23:59:59'); + + $todayStats = Db::name('workbench_traffic_distribution_item') + ->where([ + ['workbenchId', '=', $workbenchId], + ['createTime', 'between', [$todayStart, $todayEnd]] + ]) + ->field([ + 'COUNT(*) as total', + 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success', + 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed' + ]) + ->find(); + + // 获取总数据 + $totalStats = Db::name('workbench_traffic_distribution_item') + ->where('workbenchId', $workbenchId) + ->field([ + 'COUNT(*) as total', + 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success', + 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed' + ]) + ->find(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'today' => [ + 'total' => intval($todayStats['total']), + 'success' => intval($todayStats['success']), + 'failed' => intval($todayStats['failed']) + ], + 'total' => [ + 'total' => intval($totalStats['total']), + 'success' => intval($totalStats['success']), + 'failed' => intval($totalStats['failed']) + ] + ] + ]); + } + + /** + * 获取流量分发详情 + * @return \think\response\Json + */ + public function getTrafficDistributionDetail() + { + $id = $this->request->param('id', 0); + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + $detail = Db::name('workbench_traffic_distribution_item')->alias('wtdi') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wtdi.wechatAccountId', 'left') + ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wtdi.wechatFriendId', 'left') + ->field([ + 'wtdi.id', + 'wtdi.workbenchId', + 'wtdi.wechatAccountId', + 'wtdi.wechatFriendId', + 'wtdi.createTime as distributeTime', + 'wtdi.status', + 'wtdi.errorMsg', + 'wa.nickName as operatorName', + 'wa.avatar as operatorAvatar', + 'wf.nickName as friendName', + 'wf.avatar as friendAvatar', + 'wf.gender', + 'wf.province', + 'wf.city', + 'wf.signature', + 'wf.remark' + ]) + ->where('wtdi.id', $id) + ->find(); + + if (empty($detail)) { + return json(['code' => 404, 'msg' => '记录不存在']); + } + + // 处理数据 + $detail['distributeTime'] = date('Y-m-d H:i:s', $detail['distributeTime']); + + // 处理性别 + $genderMap = [ + 0 => '未知', + 1 => '男', + 2 => '女' + ]; + $detail['genderText'] = $genderMap[$detail['gender']] ?? '未知'; + + // 处理状态文字 + $statusMap = [ + 0 => '待分发', + 1 => '分发成功', + 2 => '分发失败' + ]; + $detail['statusText'] = $statusMap[$detail['status']] ?? '未知状态'; + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => $detail + ]); + } + + /** + * 创建流量分发计划 + * @return \think\response\Json + */ + public function createTrafficPlan() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchTrafficController(); + $controller->request = $this->request; + return $controller->createTrafficPlan(); + } + + /** + * 获取流量列表 + * @return \think\response\Json + */ + public function getTrafficList() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchTrafficController(); + $controller->request = $this->request; + return $controller->getTrafficList(); + } + + /** + * 获取所有微信好友标签及数量统计 + * @return \think\response\Json + */ + public function getDeviceLabels() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController(); + $controller->request = $this->request; + return $controller->getDeviceLabels(); + } + + /** + * 获取群列表 + * @return \think\response\Json + */ + public function getGroupList() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController(); + $controller->request = $this->request; + return $controller->getGroupList(); + } + + /** + * 获取流量池列表 + * @return \think\response\Json + */ + public function getTrafficPoolList() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController(); + $controller->request = $this->request; + return $controller->getTrafficPoolList(); + } + + /** + * 获取账号列表 + * @return \think\response\Json + */ + public function getAccountList() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController(); + $controller->request = $this->request; + return $controller->getAccountList(); + } + + /** + * 获取京东联盟导购媒体 + * @return \think\response\Json + */ + public function getJdSocialMedia() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController(); + $controller->request = $this->request; + return $controller->getJdSocialMedia(); + } + + /** + * 获取京东联盟广告位 + * @return \think\response\Json + */ + public function getJdPromotionSite() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController(); + $controller->request = $this->request; + return $controller->getJdPromotionSite(); + } + + /** + * 京东转链-京推推 + * @param string $content + * @param string $positionid + * @return string + */ + public function changeLink($content = '', $positionid = '') + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController(); + $controller->request = $this->request; + return $controller->changeLink($content, $positionid); + } + + /** + * 规范化客服微信ID列表 + * @param mixed $ownerWechatIds + * @param WorkbenchGroupPush|null $originalConfig + * @return array + * @throws \Exception + */ + private function normalizeOwnerWechatIds($ownerWechatIds, WorkbenchGroupPush $originalConfig = null): array + { + if ($ownerWechatIds === null) { + $existing = $originalConfig ? $this->decodeJsonArray($originalConfig->ownerWechatIds ?? []) : []; + if (empty($existing)) { + throw new \Exception('请至少选择一个客服微信'); + } + return $existing; + } + + if (!is_array($ownerWechatIds)) { + throw new \Exception('客服参数格式错误'); + } + + $normalized = $this->extractIdList($ownerWechatIds, '客服参数格式错误'); + if (empty($normalized)) { + throw new \Exception('请至少选择一个客服微信'); + } + return $normalized; + } + + /** + * 构建群推送配置数据 + * @param array $param + * @param array $ownerWechatIds + * @param WorkbenchGroupPush|null $originalConfig + * @return array + * @throws \Exception + */ + private function prepareGroupPushData(array $param, array $ownerWechatIds, WorkbenchGroupPush $originalConfig = null): array + { + $targetTypeDefault = $originalConfig ? intval($originalConfig->targetType) : 1; + $targetType = intval($this->getParamValue($param, 'targetType', $targetTypeDefault)) ?: 1; + + $groupPushSubTypeDefault = $originalConfig ? intval($originalConfig->groupPushSubType) : 1; + $groupPushSubType = intval($this->getParamValue($param, 'groupPushSubType', $groupPushSubTypeDefault)) ?: 1; + if (!in_array($groupPushSubType, [1, 2], true)) { + $groupPushSubType = 1; + } + + $data = [ + 'pushType' => $this->toBoolInt($this->getParamValue($param, 'pushType', $originalConfig->pushType ?? 0)), + 'targetType' => $targetType, + 'startTime' => $this->getParamValue($param, 'startTime', $originalConfig->startTime ?? ''), + 'endTime' => $this->getParamValue($param, 'endTime', $originalConfig->endTime ?? ''), + 'maxPerDay' => intval($this->getParamValue($param, 'maxPerDay', $originalConfig->maxPerDay ?? 0)), + 'pushOrder' => $this->getParamValue($param, 'pushOrder', $originalConfig->pushOrder ?? 1), + 'groupPushSubType' => $groupPushSubType, + 'status' => $this->toBoolInt($this->getParamValue($param, 'status', $originalConfig->status ?? 0)), + 'socialMediaId' => $this->getParamValue($param, 'socialMediaId', $originalConfig->socialMediaId ?? ''), + 'promotionSiteId' => $this->getParamValue($param, 'promotionSiteId', $originalConfig->promotionSiteId ?? ''), + 'friendIntervalMin' => intval($this->getParamValue($param, 'friendIntervalMin', $originalConfig->friendIntervalMin ?? 10)), + 'friendIntervalMax' => intval($this->getParamValue($param, 'friendIntervalMax', $originalConfig->friendIntervalMax ?? 20)), + 'messageIntervalMin' => intval($this->getParamValue($param, 'messageIntervalMin', $originalConfig->messageIntervalMin ?? 1)), + 'messageIntervalMax' => intval($this->getParamValue($param, 'messageIntervalMax', $originalConfig->messageIntervalMax ?? 12)), + 'isRandomTemplate' => $this->toBoolInt($this->getParamValue($param, 'isRandomTemplate', $originalConfig->isRandomTemplate ?? 0)), + 'ownerWechatIds' => json_encode($ownerWechatIds, JSON_UNESCAPED_UNICODE), + ]; + + if ($data['friendIntervalMin'] > $data['friendIntervalMax']) { + throw new \Exception('目标间最小间隔不能大于最大间隔'); + } + if ($data['messageIntervalMin'] > $data['messageIntervalMax']) { + throw new \Exception('消息间最小间隔不能大于最大间隔'); + } + + $contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : []; + $contentGroupsParam = $this->getParamValue($param, 'contentGroups', null); + $contentGroups = $contentGroupsParam !== null + ? $this->extractIdList($contentGroupsParam, '内容库参数格式错误') + : $contentGroupsExisting; + $data['contentLibraries'] = json_encode($contentGroups, JSON_UNESCAPED_UNICODE); + + $postPushTagsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->postPushTags ?? []) : []; + $postPushTagsParam = $this->getParamValue($param, 'postPushTags', null); + $postPushTags = $postPushTagsParam !== null + ? $this->extractIdList($postPushTagsParam, '推送标签参数格式错误') + : $postPushTagsExisting; + $data['postPushTags'] = json_encode($postPushTags, JSON_UNESCAPED_UNICODE); + + if ($targetType === 1) { + $data['isLoop'] = $this->toBoolInt($this->getParamValue($param, 'isLoop', $originalConfig->isLoop ?? 0)); + + $groupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->groups ?? []) : []; + $wechatGroups = array_key_exists('wechatGroups', $param) + ? $this->extractIdList($param['wechatGroups'], '群参数格式错误') + : $groupsExisting; + if (empty($wechatGroups)) { + throw new \Exception('群推送必须选择微信群'); + } + $data['groups'] = json_encode($wechatGroups, JSON_UNESCAPED_UNICODE); + $data['friends'] = json_encode([], JSON_UNESCAPED_UNICODE); + $data['trafficPools'] = json_encode([], JSON_UNESCAPED_UNICODE); + + if ($groupPushSubType === 2) { + $announcementContent = $this->getParamValue($param, 'announcementContent', $originalConfig->announcementContent ?? ''); + if (empty($announcementContent)) { + throw new \Exception('群公告必须输入公告内容'); + } + $enableAiRewrite = $this->toBoolInt($this->getParamValue($param, 'enableAiRewrite', $originalConfig->enableAiRewrite ?? 0)); + $aiRewritePrompt = trim((string)$this->getParamValue($param, 'aiRewritePrompt', $originalConfig->aiRewritePrompt ?? '')); + if ($enableAiRewrite === 1 && $aiRewritePrompt === '') { + throw new \Exception('启用AI智能话术改写时,必须输入改写提示词'); + } + $data['announcementContent'] = $announcementContent; + $data['enableAiRewrite'] = $enableAiRewrite; + $data['aiRewritePrompt'] = $aiRewritePrompt; + } else { + $data['groupPushSubType'] = 1; + $data['announcementContent'] = ''; + $data['enableAiRewrite'] = 0; + $data['aiRewritePrompt'] = ''; + } + } else { + $data['isLoop'] = 0; + $friendsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->friends ?? []) : []; + $trafficPoolsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->trafficPools ?? []) : []; + + $friendTargets = array_key_exists('wechatFriends', $param) + ? $this->extractIdList($param['wechatFriends'], '好友参数格式错误') + : $friendsExisting; + $trafficPools = array_key_exists('trafficPools', $param) + ? $this->extractIdList($param['trafficPools'], '流量池参数格式错误') + : $trafficPoolsExisting; + + if (empty($friendTargets) && empty($trafficPools)) { + throw new \Exception('好友推送需至少选择好友或流量池'); + } + + $data['friends'] = json_encode($friendTargets, JSON_UNESCAPED_UNICODE); + $data['trafficPools'] = json_encode($trafficPools, JSON_UNESCAPED_UNICODE); + $data['groups'] = json_encode([], JSON_UNESCAPED_UNICODE); + $data['groupPushSubType'] = 1; + $data['announcementContent'] = ''; + $data['enableAiRewrite'] = 0; + $data['aiRewritePrompt'] = ''; + } + + return $data; + } + + /** + * 获取参数值,若不存在则返回默认值 + * @param array $param + * @param string $key + * @param mixed $default + * @return mixed + */ + private function getParamValue(array $param, string $key, $default) + { + return array_key_exists($key, $param) ? $param[$key] : $default; + } + + /** + * 将值转换为整型布尔 + * @param mixed $value + * @return int + */ + private function toBoolInt($value): int + { + return empty($value) ? 0 : 1; + } + + /** + * 从参数中提取ID列表 + * @param mixed $items + * @param string $errorMessage + * @return array + * @throws \Exception + */ + private function extractIdList($items, string $errorMessage = '参数格式错误'): array + { + if (!is_array($items)) { + throw new \Exception($errorMessage); + } + + $ids = []; + foreach ($items as $item) { + if (is_array($item) && isset($item['id'])) { + $item = $item['id']; + } + if ($item === '' || $item === null) { + continue; + } + $ids[] = $item; + } + + return array_values(array_unique($ids)); + } + + /** + * 解码JSON数组 + * @param mixed $value + * @return array + */ + private function decodeJsonArray($value): array + { + if (empty($value)) { + return []; + } + if (is_array($value)) { + return $value; + } + $decoded = json_decode($value, true); + return is_array($decoded) ? $decoded : []; + } + + /** + * 验证内容是否包含链接 + * @param string $content 要检测的内容 + * @return bool + */ + private function containsLink($content) + { + // 定义各种链接的正则表达式模式 + $patterns = [ + // HTTP/HTTPS链接 + '/https?:\/\/[^\s]+/i', + // 京东商品链接 + '/item\.jd\.com\/\d+/i', + // 京东短链接 + '/u\.jd\.com\/[a-zA-Z0-9]+/i', + // 淘宝商品链接 + '/item\.taobao\.com\/item\.htm\?id=\d+/i', + // 天猫商品链接 + '/detail\.tmall\.com\/item\.htm\?id=\d+/i', + // 淘宝短链接 + '/m\.tb\.cn\/[a-zA-Z0-9]+/i', + // 拼多多链接 + '/mobile\.yangkeduo\.com\/goods\.html\?goods_id=\d+/i', + // 苏宁易购链接 + '/product\.suning\.com\/\d+\/\d+\.html/i', + // 通用域名模式(包含常见电商域名) + '/(?:jd|taobao|tmall|yangkeduo|suning|amazon|dangdang)\.com[^\s]*/i', + // 通用短链接模式 + '/[a-zA-Z0-9-]+\.[a-zA-Z]{2,}\/[a-zA-Z0-9\-._~:\/?#\[\]@!$&\'()*+,;=]+/i' + ]; + + // 遍历所有模式进行匹配 + foreach ($patterns as $pattern) { + if (preg_match($pattern, $content)) { + return true; + } + } + + return false; + } + + + /** + * 获取通讯录导入记录列表 + * @return \think\response\Json + */ + public function getImportContact() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $workbenchId = $this->request->param('workbenchId', 0); + + $where = [ + ['wici.workbenchId', '=', $workbenchId] + ]; + + // 查询发布记录 + $list = Db::name('workbench_import_contact_item')->alias('wici') + ->join('traffic_pool tp', 'tp.id = wici.poolId', 'left') + ->join('traffic_source tc', 'tc.identifier = tp.identifier', 'left') + ->join('wechat_account wa', 'wa.wechatId = tp.wechatId', 'left') + ->field([ + 'wici.id', + 'wici.workbenchId', + 'wici.createTime', + 'tp.identifier', + 'tp.mobile', + 'tp.wechatId', + 'tc.name', + 'wa.nickName', + 'wa.avatar', + 'wa.alias', + ]) + ->where($where) + ->order('tc.name DESC,wici.createTime DESC') + ->group('tp.identifier') + ->page($page, $limit) + ->select(); + + foreach ($list as &$item) { + $item['createTime'] = date('Y-m-d H:i:s', $item['createTime']); + } + + + // 获取总记录数 + $total = Db::name('workbench_import_contact_item')->alias('wici') + ->where($where) + ->count(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + ] + ]); + } + + /** + * 获取群发统计数据 + * @return \think\response\Json + */ + public function getGroupPushStats() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchGroupPushController(); + $controller->request = $this->request; + return $controller->getGroupPushStats(); + } + + /** + * 计算基础统计数据 + */ + private function calculateBasicStats($workbenchId, $where, $startTime, $endTime, $contentIds = null) + { + // 获取工作台配置,计算计划发送数 + // 如果 workbenchId 为空,则查询所有工作台的配置 + $configQuery = WorkbenchGroupPush::alias('wgp') + ->join('workbench w', 'w.id = wgp.workbenchId', 'left') + ->where('w.type', self::TYPE_GROUP_PUSH) + ->where('w.isDel', 0); + + if (!empty($workbenchId)) { + $configQuery->where('wgp.workbenchId', $workbenchId); + } else { + // 如果没有指定工作台ID,需要从 where 条件中获取 workbenchId 列表 + $workbenchIdCondition = null; + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') { + if ($condition[1] === 'in' && is_array($condition[2])) { + $workbenchIdCondition = $condition[2]; + break; + } elseif ($condition[1] === '=') { + $workbenchIdCondition = [$condition[2]]; + break; + } + } + } + if ($workbenchIdCondition) { + $configQuery->whereIn('wgp.workbenchId', $workbenchIdCondition); + } + } + + $configs = $configQuery->select(); + $targetType = 1; // 默认值 + if (!empty($configs)) { + // 如果只有一个配置,使用它的 targetType;如果有多个,默认使用1 + $targetType = intval($configs[0]->targetType ?? 1); + } + + // 计划发送数(根据配置计算) + $plannedSend = 0; + if (!empty($configs)) { + $days = ceil(($endTime - $startTime) / 86400); + foreach ($configs as $config) { + $maxPerDay = intval($config->maxPerDay ?? 0); + $configTargetType = intval($config->targetType ?? 1); + if ($configTargetType == 1) { + // 群推送:计划发送数 = 每日推送次数 * 天数 * 群数量 + $groups = $this->decodeJsonArray($config->groups ?? []); + $plannedSend += $maxPerDay * $days * count($groups); + } else { + // 好友推送:计划发送数 = 每日推送人数 * 天数 + $plannedSend += $maxPerDay * $days; + } + } + } + + // 构建查询条件 + $queryWhere = $where; + if ($contentIds !== null) { + $queryWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + + // 实际成功发送数(从推送记录表统计) + $successSend = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($queryWhere) + ->count(); + + // 触达率 = 成功发送数 / 计划发送数 + $reachRate = $plannedSend > 0 ? round(($successSend / $plannedSend) * 100, 1) : 0; + + // 获取发送记录列表,用于查询回复 + $sentItemIds = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($queryWhere) + ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId') + ->select(); + + // 回复统计(通过消息表查询) + $replyStats = $this->calculateReplyStats($sentItemIds, $targetType, $startTime, $endTime); + + // 链接点击统计 + $clickStats = $this->calculateClickStats($sentItemIds, $targetType, $startTime, $endTime); + + // 计算本月对比数据(简化处理,实际应该查询上个月同期数据) + $currentMonthStart = strtotime(date('Y-m-01 00:00:00')); + $lastMonthStart = strtotime(date('Y-m-01 00:00:00', strtotime('-1 month'))); + $lastMonthEnd = $currentMonthStart - 1; + + // 获取本月统计数据(避免递归调用) + $currentMonthWhere = [ + ['wgpi.createTime', '>=', $currentMonthStart] + ]; + // 复制 workbenchId 条件 + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') { + $currentMonthWhere[] = $condition; + break; + } + } + if ($contentIds !== null) { + $currentMonthWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + $currentMonthSend = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($currentMonthWhere) + ->count(); + + // 获取本月配置 + $currentMonthConfigQuery = WorkbenchGroupPush::alias('wgp') + ->join('workbench w', 'w.id = wgp.workbenchId', 'left') + ->where('w.type', self::TYPE_GROUP_PUSH) + ->where('w.isDel', 0); + if (!empty($workbenchId)) { + $currentMonthConfigQuery->where('wgp.workbenchId', $workbenchId); + } else { + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId' && $condition[1] === 'in') { + $currentMonthConfigQuery->whereIn('wgp.workbenchId', $condition[2]); + break; + } + } + } + $currentMonthConfigs = $currentMonthConfigQuery->select(); + $currentMonthPlanned = 0; + if (!empty($currentMonthConfigs)) { + $currentMonthDays = ceil(($endTime - $currentMonthStart) / 86400); + foreach ($currentMonthConfigs as $currentMonthConfig) { + $currentMonthMaxPerDay = intval($currentMonthConfig->maxPerDay ?? 0); + $currentMonthTargetType = intval($currentMonthConfig->targetType ?? 1); + if ($currentMonthTargetType == 1) { + $currentMonthGroups = $this->decodeJsonArray($currentMonthConfig->groups ?? []); + $currentMonthPlanned += $currentMonthMaxPerDay * $currentMonthDays * count($currentMonthGroups); + } else { + $currentMonthPlanned += $currentMonthMaxPerDay * $currentMonthDays; + } + } + } + $currentMonthReachRate = $currentMonthPlanned > 0 ? round(($currentMonthSend / $currentMonthPlanned) * 100, 1) : 0; + + // 获取上个月统计数据 + $lastMonthWhere = [ + ['wgpi.createTime', '>=', $lastMonthStart], + ['wgpi.createTime', '<=', $lastMonthEnd] + ]; + // 复制 workbenchId 条件 + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') { + $lastMonthWhere[] = $condition; + break; + } + } + if ($contentIds !== null) { + $lastMonthWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + $lastMonthSend = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($lastMonthWhere) + ->count(); + + // 获取上个月配置 + $lastMonthConfigQuery = WorkbenchGroupPush::alias('wgp') + ->join('workbench w', 'w.id = wgp.workbenchId', 'left') + ->where('w.type', self::TYPE_GROUP_PUSH) + ->where('w.isDel', 0); + if (!empty($workbenchId)) { + $lastMonthConfigQuery->where('wgp.workbenchId', $workbenchId); + } else { + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId' && $condition[1] === 'in') { + $lastMonthConfigQuery->whereIn('wgp.workbenchId', $condition[2]); + break; + } + } + } + $lastMonthConfigs = $lastMonthConfigQuery->select(); + + $lastMonthPlanned = 0; + if (!empty($lastMonthConfigs)) { + $lastMonthDays = ceil(($lastMonthEnd - $lastMonthStart) / 86400); + foreach ($lastMonthConfigs as $lastMonthConfig) { + $lastMonthMaxPerDay = intval($lastMonthConfig->maxPerDay ?? 0); + $lastMonthTargetType = intval($lastMonthConfig->targetType ?? 1); + if ($lastMonthTargetType == 1) { + $lastMonthGroups = $this->decodeJsonArray($lastMonthConfig->groups ?? []); + $lastMonthPlanned += $lastMonthMaxPerDay * $lastMonthDays * count($lastMonthGroups); + } else { + $lastMonthPlanned += $lastMonthMaxPerDay * $lastMonthDays; + } + } + } + $lastMonthReachRate = $lastMonthPlanned > 0 ? round(($lastMonthSend / $lastMonthPlanned) * 100, 1) : 0; + + // 获取上个月的回复和点击统计(简化处理) + $lastMonthSentItems = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($lastMonthWhere) + ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId') + ->select(); + $lastMonthReplyStats = $this->calculateReplyStats($lastMonthSentItems, $targetType, $lastMonthStart, $lastMonthEnd); + $lastMonthClickStats = $this->calculateClickStats($lastMonthSentItems, $targetType, $lastMonthStart, $lastMonthEnd); + + return [ + 'reachRate' => [ + 'value' => $reachRate, + 'trend' => round($reachRate - $lastMonthReachRate, 1), + 'unit' => '%', + 'description' => '成功发送/计划发送' + ], + 'replyRate' => [ + 'value' => $replyStats['replyRate'], + 'trend' => round($replyStats['replyRate'] - $lastMonthReplyStats['replyRate'], 1), + 'unit' => '%', + 'description' => '收到回复/成功发送' + ], + 'avgReplyTime' => [ + 'value' => $replyStats['avgReplyTime'], + 'trend' => round($lastMonthReplyStats['avgReplyTime'] - $replyStats['avgReplyTime'], 0), + 'unit' => '分钟', + 'description' => '从发送到回复的平均时长' + ], + 'clickRate' => [ + 'value' => $clickStats['clickRate'], + 'trend' => round($clickStats['clickRate'] - $lastMonthClickStats['clickRate'], 1), + 'unit' => '%', + 'description' => '点击链接/成功发送' + ], + 'plannedSend' => $plannedSend, + 'successSend' => $successSend, + 'replyCount' => $replyStats['replyCount'], + 'clickCount' => $clickStats['clickCount'] + ]; + } + + /** + * 计算回复统计 + */ + private function calculateReplyStats($sentItems, $targetType, $startTime, $endTime) + { + if (empty($sentItems)) { + return ['replyRate' => 0, 'avgReplyTime' => 0, 'replyCount' => 0]; + } + + $replyCount = 0; + $totalReplyTime = 0; + $replyTimes = []; + + foreach ($sentItems as $item) { + $itemArray = is_array($item) ? $item : (array)$item; + $sendTime = $itemArray['createTime'] ?? 0; + $accountId = $itemArray['wechatAccountId'] ?? 0; + + if ($targetType == 1) { + // 群推送:查找群内回复消息 + $groupId = $itemArray['groupId'] ?? 0; + $group = Db::name('wechat_group')->where('id', $groupId)->find(); + if ($group) { + $replyMsg = Db::table('s2_wechat_message') + ->where('wechatChatroomId', $group['chatroomId']) + ->where('wechatAccountId', $accountId) + ->where('isSend', 0) // 接收的消息 + ->where('wechatTime', '>', $sendTime) + ->where('wechatTime', '<=', $sendTime + 86400) // 24小时内回复 + ->order('wechatTime', 'asc') + ->find(); + + if ($replyMsg) { + $replyCount++; + $replyTime = $replyMsg['wechatTime'] - $sendTime; + $replyTimes[] = $replyTime; + $totalReplyTime += $replyTime; + } + } + } else { + // 好友推送:查找好友回复消息 + $friendId = $itemArray['friendId'] ?? 0; + $friend = Db::table('s2_wechat_friend')->where('id', $friendId)->find(); + if ($friend) { + $replyMsg = Db::table('s2_wechat_message') + ->where('wechatFriendId', $friendId) + ->where('wechatAccountId', $accountId) + ->where('isSend', 0) // 接收的消息 + ->where('wechatTime', '>', $sendTime) + ->where('wechatTime', '<=', $sendTime + 86400) // 24小时内回复 + ->order('wechatTime', 'asc') + ->find(); + + if ($replyMsg) { + $replyCount++; + $replyTime = $replyMsg['wechatTime'] - $sendTime; + $replyTimes[] = $replyTime; + $totalReplyTime += $replyTime; + } + } + } + } + + $successSend = count($sentItems); + $replyRate = $successSend > 0 ? round(($replyCount / $successSend) * 100, 1) : 0; + $avgReplyTime = $replyCount > 0 ? round(($totalReplyTime / $replyCount) / 60, 0) : 0; // 转换为分钟 + + return [ + 'replyRate' => $replyRate, + 'avgReplyTime' => $avgReplyTime, + 'replyCount' => $replyCount + ]; + } + + /** + * 计算链接点击统计 + */ + private function calculateClickStats($sentItems, $targetType, $startTime, $endTime) + { + if (empty($sentItems)) { + return ['clickRate' => 0, 'clickCount' => 0]; + } + + $clickCount = 0; + $linkContentIds = []; + + // 获取所有发送的内容ID + foreach ($sentItems as $item) { + $itemArray = is_array($item) ? $item : (array)$item; + $contentId = $itemArray['contentId'] ?? 0; + if ($contentId > 0) { + $linkContentIds[] = $contentId; + } + } + + if (empty($linkContentIds)) { + return ['clickRate' => 0, 'clickCount' => 0]; + } + + // 查询包含链接的内容 + $linkContents = Db::name('content_item') + ->whereIn('id', array_unique($linkContentIds)) + ->where('contentType', 2) // 链接类型 + ->column('id'); + + // 统计发送了链接内容的记录数 + $linkSendCount = 0; + foreach ($sentItems as $item) { + $itemArray = is_array($item) ? $item : (array)$item; + $contentId = $itemArray['contentId'] ?? 0; + if (in_array($contentId, $linkContents)) { + $linkSendCount++; + } + } + + // 简化处理:假设点击率基于链接消息的发送(实际应该从点击追踪系统获取) + // 这里可以根据业务需求调整,比如通过消息中的链接点击事件统计 + $clickCount = $linkSendCount; // 简化处理,实际需要真实的点击数据 + + $successSend = count($sentItems); + $clickRate = $successSend > 0 ? round(($clickCount / $successSend) * 100, 1) : 0; + + return [ + 'clickRate' => $clickRate, + 'clickCount' => $clickCount + ]; + } + + /** + * 获取话术组对比数据 + */ + private function getContentLibraryComparison($workbenchId, $where, $startTime, $endTime, $contentIds = null) + { + $queryWhere = $where; + if ($contentIds !== null) { + $queryWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + + $comparison = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->join('content_item ci', 'ci.id = wgpi.contentId', 'left') + ->join('content_library cl', 'cl.id = ci.libraryId', 'left') + ->where($queryWhere) + ->where('cl.id', '<>', null) + ->field([ + 'cl.id as libraryId', + 'cl.name as libraryName', + 'COUNT(DISTINCT wgpi.id) as pushCount' + ]) + ->group('cl.id, cl.name') + ->select(); + + $result = []; + foreach ($comparison as $item) { + $libraryId = $item['libraryId']; + $pushCount = intval($item['pushCount']); + + // 获取该内容库的详细统计 + $libraryContentIds = Db::name('content_item') + ->where('libraryId', $libraryId) + ->column('id'); + if (empty($libraryContentIds)) { + $libraryContentIds = [-1]; + } + + $libraryWhere = array_merge($where, [['wgpi.contentId', 'in', $libraryContentIds]]); + $librarySentItems = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($libraryWhere) + ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId') + ->select(); + + $config = WorkbenchGroupPush::where('workbenchId', $workbenchId)->find(); + $targetType = $config ? intval($config->targetType) : 1; + + $replyStats = $this->calculateReplyStats($librarySentItems, $targetType, $startTime, $endTime); + $clickStats = $this->calculateClickStats($librarySentItems, $targetType, $startTime, $endTime); + + // 计算转化率(简化处理,实际需要根据业务定义) + $conversionRate = $pushCount > 0 ? round(($replyStats['replyCount'] / $pushCount) * 100, 1) : 0; + + $result[] = [ + 'libraryId' => $libraryId, + 'libraryName' => $item['libraryName'], + 'pushCount' => $pushCount, + 'reachRate' => 100, // 简化处理,实际应该计算 + 'replyRate' => $replyStats['replyRate'], + 'clickRate' => $clickStats['clickRate'], + 'conversionRate' => $conversionRate, + 'avgReplyTime' => $replyStats['avgReplyTime'], + 'level' => $this->getPerformanceLevel($replyStats['replyRate'], $clickStats['clickRate'], $conversionRate) + ]; + } + + // 按回复率排序 + usort($result, function($a, $b) { + return $b['replyRate'] <=> $a['replyRate']; + }); + + return $result; + } + + /** + * 获取性能等级 + */ + private function getPerformanceLevel($replyRate, $clickRate, $conversionRate) + { + $score = ($replyRate * 0.4) + ($clickRate * 0.3) + ($conversionRate * 0.3); + + if ($score >= 40) { + return '优秀'; + } elseif ($score >= 25) { + return '良好'; + } elseif ($score >= 15) { + return '一般'; + } else { + return '待提升'; + } + } + + /** + * 获取时段分析数据 + */ + private function getTimePeriodAnalysis($workbenchId, $where, $startTime, $endTime, $contentIds = null) + { + $queryWhere = $where; + if ($contentIds !== null) { + $queryWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + + $analysis = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($queryWhere) + ->field([ + 'FROM_UNIXTIME(wgpi.createTime, "%H") as hour', + 'COUNT(*) as count' + ]) + ->group('hour') + ->order('hour', 'asc') + ->select(); + + $result = []; + foreach ($analysis as $item) { + $result[] = [ + 'hour' => intval($item['hour']), + 'count' => intval($item['count']) + ]; + } + + return $result; + } + + /** + * 获取互动深度数据 + */ + private function getInteractionDepth($workbenchId, $where, $startTime, $endTime, $contentIds = null) + { + // 简化处理,实际需要更复杂的统计逻辑 + return [ + 'singleReply' => 0, // 单次回复 + 'multipleReply' => 0, // 多次回复 + 'deepInteraction' => 0 // 深度互动 + ]; + } + + /** + * 获取推送历史记录列表 + * @return \think\response\Json + */ + public function getGroupPushHistory() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchGroupPushController(); + $controller->request = $this->request; + return $controller->getGroupPushHistory(); + } + + /** + * 获取状态文本 + * @param string $status 状态码 + * @return string 状态文本 + */ + private function getStatusText($status) + { + $statusMap = [ + 'success' => '已完成', + 'partial' => '进行中', + 'pending' => '进行中', + 'failed' => '失败' + ]; + return $statusMap[$status] ?? '未知'; + } + + /** + * 获取已创建的群列表(自动建群) + * @return \think\response\Json + */ + public function getCreatedGroupsList() + { + $workbenchId = $this->request->param('workbenchId', 0); + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 100); + $keyword = $this->request->param('keyword', ''); + + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '工作台ID不能为空']); + } + + // 验证工作台权限 + $where = [ + ['id', '=', $workbenchId], + ['companyId', '=', $this->request->userInfo['companyId']], + ['type', '=', self::TYPE_GROUP_CREATE], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + $workbench = Workbench::where($where)->find(); + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在或无权限']); + } + + // 获取已创建的群ID列表(状态为成功且groupId不为空) + $groupIds = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('status', 2) // STATUS_SUCCESS = 2 + ->where('groupId', '<>', null) + ->group('groupId') + ->column('groupId'); + + if (empty($groupIds)) { + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => [], + 'total' => 0, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + + // 查询群组详细信息(从s2_wechat_chatroom表查询) + $query = Db::table('s2_wechat_chatroom')->alias('wc') + ->join('wechat_account wa', 'wa.wechatId = wc.wechatAccountWechatId', 'left') + ->where('wc.id', 'in', $groupIds) + ->where('wc.isDeleted', 0); + + // 关键字搜索 + if (!empty($keyword)) { + $query->where(function ($q) use ($keyword) { + $q->where('wc.nickname', 'like', '%' . $keyword . '%') + ->whereOr('wc.chatroomId', 'like', '%' . $keyword . '%') + ->whereOr('wa.nickName', 'like', '%' . $keyword . '%'); + }); + } + + $total = $query->count(); + $list = $query->field('wc.id,wc.nickname as groupName,wc.chatroomId,wc.chatroomAvatar as groupAvatar,wc.wechatAccountWechatId as ownerWechatId,wc.createTime,wc.chatroomOwnerNickname as ownerNickname,wc.chatroomOwnerAvatar as ownerAvatar,wa.alias as ownerAlias') + ->order('wc.createTime', 'desc') + ->page($page, $limit) + ->select(); + + // 统计每个群的成员数量和成员信息 + foreach ($list as &$item) { + // 统计该群的成员数量(从workbench_group_create_item表统计) + $memberCount = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('groupId', $item['id']) + ->where('status', 'in', [2, 4]) // 创建成功和管理员好友已拉入 + ->count(); + + // 获取成员列表(用于显示成员头像) + $memberList = Db::name('workbench_group_create_item')->alias('wgci') + ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wgci.friendId', 'left') + ->where('wgci.workbenchId', $workbenchId) + ->where('wgci.groupId', $item['id']) + ->where('wgci.status', 'in', [2, 4]) + ->field('wf.avatar, wf.wechatId, wf.nickname') + ->order('wgci.createTime', 'asc') + ->limit(10) // 最多显示10个成员头像 + ->select(); + + // 格式化成员头像列表 + $memberAvatars = []; + foreach ($memberList as $member) { + if (!empty($member['avatar'])) { + $memberAvatars[] = [ + 'avatar' => $member['avatar'], + 'wechatId' => $member['wechatId'] ?? '', + 'nickname' => $member['nickname'] ?? '' + ]; + } + } + + // 计算剩余成员数(用于显示"+XX") + $remainingCount = $memberCount > count($memberAvatars) ? $memberCount - count($memberAvatars) : 0; + + // 格式化返回数据 + $item['memberCount'] = $memberCount; + $item['memberCountText'] = $memberCount . '人'; // 格式化为"XX人" + $item['createTime'] = !empty($item['createTime']) ? date('Y-m-d', $item['createTime']) : ''; // 格式化为"YYYY-MM-DD" + $item['memberAvatars'] = $memberAvatars; // 成员头像列表(最多10个) + $item['remainingCount'] = $remainingCount; // 剩余成员数(用于显示"+XX") + + // 保留原有字段,但调整格式 + $item['groupName'] = $item['groupName'] ?? ''; + $item['groupAvatar'] = $item['groupAvatar'] ?? ''; + } + unset($item); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + + /** + * 获取已创建群的详情(自动建群) + * @return \think\response\Json + */ + public function getCreatedGroupDetail() + { + $workbenchId = $this->request->param('workbenchId', 0); + $groupId = $this->request->param('groupId', 0); + + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '工作台ID不能为空']); + } + + if (empty($groupId)) { + return json(['code' => 400, 'msg' => '群ID不能为空']); + } + + // 验证工作台权限 + $where = [ + ['id', '=', $workbenchId], + ['companyId', '=', $this->request->userInfo['companyId']], + ['type', '=', self::TYPE_GROUP_CREATE], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + $workbench = Workbench::where($where)->find(); + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在或无权限']); + } + + // 验证该群是否属于该工作台 + $groupItem = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('groupId', $groupId) + ->where('status', 2) // STATUS_SUCCESS = 2 + ->find(); + + if (empty($groupItem)) { + return json(['code' => 404, 'msg' => '群不存在或不属于该工作台']); + } + + // 查询群基本信息(从s2_wechat_chatroom表查询) + $group = Db::table('s2_wechat_chatroom')->alias('wc') + ->join('wechat_account wa', 'wa.wechatId = wc.wechatAccountWechatId', 'left') + ->where('wc.id', $groupId) + ->where('wc.isDeleted', 0) + ->field('wc.id,wc.nickname as groupName,wc.chatroomId,wc.chatroomAvatar as groupAvatar,wc.wechatAccountWechatId as ownerWechatId,wc.createTime,wc.chatroomOwnerNickname as ownerNickname,wc.chatroomOwnerAvatar as ownerAvatar,wa.alias as ownerAlias,wc.announce') + ->find(); + + if (empty($group)) { + return json(['code' => 404, 'msg' => '群不存在']); + } + + // 获取chatroomId + $chatroomId = $group['chatroomId'] ?? ''; + if (empty($chatroomId)) { + return json(['code' => 400, 'msg' => '群聊ID不存在']); + } + + // 从s2_wechat_chatroom_member表查询所有成员列表(不限制数量) + $memberList = Db::table('s2_wechat_chatroom_member')->alias('wcm') + ->join(['s2_wechat_friend' => 'wf'], 'wf.wechatId = wcm.wechatId', 'left') + ->where('wcm.chatroomId', $chatroomId) + ->field('wcm.wechatId,wcm.nickname as memberNickname,wcm.avatar as memberAvatar,wcm.conRemark as memberRemark,wcm.alias as memberAlias,wcm.createTime as joinTime,wcm.updateTime,wf.id as friendId,wf.nickname as friendNickname,wf.avatar as friendAvatar') + ->order('wcm.createTime', 'asc') + ->select(); + + // 去重:按wechatId去重,保留第一条记录 + $memberMap = []; + foreach ($memberList as $member) { + $wechatId = $member['wechatId'] ?? ''; + if (!empty($wechatId) && !isset($memberMap[$wechatId])) { + $memberMap[$wechatId] = $member; + } + } + $memberList = array_values($memberMap); // 重新索引数组 + + // 获取在群中的成员wechatId列表(用于判断是否已退群) + $inGroupWechatIds = array_column($memberList, 'wechatId'); + $inGroupWechatIds = array_filter($inGroupWechatIds); // 过滤空值 + + // 获取通过自动建群加入的成员信息(用于判断入群状态和已退群成员) + $autoJoinMemberList = Db::name('workbench_group_create_item') + ->alias('wgci') + ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wgci.friendId', 'left') + ->where('wgci.workbenchId', $workbenchId) + ->where('wgci.groupId', $groupId) + ->where('wgci.status', 'in', [2, 4]) // 创建成功和管理员好友已拉入 + ->field('wf.wechatId,wf.id as friendId,wf.nickname as friendNickname,wf.avatar as friendAvatar,wgci.createTime as autoJoinTime') + ->select(); + + // 去重:按wechatId去重,保留第一条记录 + $autoJoinMemberMap = []; + foreach ($autoJoinMemberList as $autoMember) { + $wechatId = $autoMember['wechatId'] ?? ''; + if (!empty($wechatId) && !isset($autoJoinMemberMap[$wechatId])) { + $autoJoinMemberMap[$wechatId] = $autoMember; + } + } + $autoJoinMemberList = array_values($autoJoinMemberMap); // 重新索引数组 + + // 统计成员总数(包括在群中的成员和已退群的成员) + // 先统计在群中的成员数 + $inGroupCount = count($memberList); + + // 统计已退群的成员数(在自动建群记录中但不在群中的成员) + $quitCount = 0; + if (!empty($autoJoinMemberList)) { + foreach ($autoJoinMemberList as $autoMember) { + $wechatId = $autoMember['wechatId'] ?? ''; + if (!empty($wechatId) && !in_array($wechatId, $inGroupWechatIds)) { + $quitCount++; + } + } + } + + // 总成员数 = 在群中的成员数 + 已退群的成员数 + $memberCount = $inGroupCount + $quitCount; + + // 获取自动建群加入的成员wechatId列表(用于判断入群状态) + $autoJoinWechatIds = array_column($autoJoinMemberList, 'wechatId'); + $autoJoinWechatIds = array_filter($autoJoinWechatIds); // 过滤空值 + + // 格式化在群中的成员列表 + $members = []; + $addedWechatIds = []; // 用于记录已添加的成员wechatId,避免重复 + $ownerWechatId = $group['ownerWechatId'] ?? ''; + foreach ($memberList as $member) { + $wechatId = $member['wechatId'] ?? ''; + + // 跳过空wechatId和已添加的成员 + if (empty($wechatId) || in_array($wechatId, $addedWechatIds)) { + continue; + } + + // 标记为已添加 + $addedWechatIds[] = $wechatId; + + // 根据wechatId判断是否为群主 + $isOwner = (!empty($ownerWechatId) && $wechatId == $ownerWechatId) ? 1 : 0; + + // 判断入群状态:如果在自动建群记录中,说明是通过自动建群加入的;否则是其他方式加入的 + $joinStatus = in_array($wechatId, $autoJoinWechatIds) ? 'auto' : 'manual'; + + // 判断是否已退群:如果成员在s2_wechat_chatroom_member表中存在,说明在群中;否则已退群 + // 由于我们已经从s2_wechat_chatroom_member表查询,所以这里都是"在群中"状态 + $isQuit = 0; // 0=在群中,1=已退群 + + // 优先使用friend表的昵称和头像,如果没有则使用member表的 + $nickname = !empty($member['friendNickname']) ? $member['friendNickname'] : ($member['memberNickname'] ?? ''); + $avatar = !empty($member['friendAvatar']) ? $member['friendAvatar'] : ($member['memberAvatar'] ?? ''); + + $members[] = [ + 'friendId' => $member['friendId'] ?? 0, + 'wechatId' => $wechatId, + 'nickname' => $nickname, + 'avatar' => $avatar, + 'alias' => $member['memberAlias'] ?? '', + 'remark' => $member['memberRemark'] ?? '', + 'isOwner' => $isOwner, // 标记群主 + 'joinStatus' => $joinStatus, // 入群状态:auto=自动建群加入,manual=其他方式加入 + 'isQuit' => $isQuit, // 是否已退群:0=在群中,1=已退群 + 'joinTime' => !empty($member['joinTime']) ? date('Y-m-d H:i:s', $member['joinTime']) : '', // 入群时间 + ]; + } + + // 添加已退群的成员(在自动建群记录中但不在群中的成员) + foreach ($autoJoinMemberList as $autoMember) { + $wechatId = $autoMember['wechatId'] ?? ''; + + // 跳过空wechatId、已在群中的成员和已添加的成员 + if (empty($wechatId) || in_array($wechatId, $inGroupWechatIds) || in_array($wechatId, $addedWechatIds)) { + continue; + } + + // 标记为已添加 + $addedWechatIds[] = $wechatId; + + // 根据wechatId判断是否为群主 + $isOwner = (!empty($ownerWechatId) && $wechatId == $ownerWechatId) ? 1 : 0; + + $members[] = [ + 'friendId' => $autoMember['friendId'] ?? 0, + 'wechatId' => $wechatId, + 'nickname' => $autoMember['friendNickname'] ?? '', + 'avatar' => $autoMember['friendAvatar'] ?? '', + 'alias' => '', + 'remark' => '', + 'isOwner' => $isOwner, // 标记群主 + 'joinStatus' => 'auto', // 入群状态:auto=自动建群加入 + 'isQuit' => 1, // 是否已退群:1=已退群 + 'joinTime' => !empty($autoMember['autoJoinTime']) ? date('Y-m-d H:i:s', $autoMember['autoJoinTime']) : '', // 入群时间 + ]; + } + + // 将群主排在第一位 + usort($members, function($a, $b) { + if ($a['isOwner'] == $b['isOwner']) { + return 0; + } + return $a['isOwner'] > $b['isOwner'] ? -1 : 1; + }); + + // 格式化返回数据 + $result = [ + 'id' => $group['id'], + 'groupName' => $group['groupName'] ?? '', + 'chatroomId' => $group['chatroomId'] ?? '', + 'groupAvatar' => $group['groupAvatar'] ?? '', + 'ownerWechatId' => $group['ownerWechatId'] ?? '', + 'ownerNickname' => $group['ownerNickname'] ?? '', + 'ownerAvatar' => $group['ownerAvatar'] ?? '', + 'ownerAlias' => $group['ownerAlias'] ?? '', + 'announce' => $group['announce'] ?? '', + 'createTime' => !empty($group['createTime']) ? date('Y-m-d H:i', $group['createTime']) : '', // 格式化为"YYYY-MM-DD HH:MM" + 'memberCount' => $memberCount, + 'memberCountText' => $memberCount . '人', // 格式化为"XX人" + 'workbenchName' => $workbench->name ?? '', // 任务名称(工作台名称) + 'members' => $members // 所有成员列表 + ]; + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => $result + ]); + } + + /** + * 同步群最新信息(包括群成员) + * @return \think\response\Json + */ + public function syncGroupInfo() + { + $workbenchId = $this->request->param('workbenchId', 0); + $groupId = $this->request->param('groupId', 0); + + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '工作台ID不能为空']); + } + + if (empty($groupId)) { + return json(['code' => 400, 'msg' => '群ID不能为空']); + } + + // 验证工作台权限 + $where = [ + ['id', '=', $workbenchId], + ['companyId', '=', $this->request->userInfo['companyId']], + ['type', '=', self::TYPE_GROUP_CREATE], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + $workbench = Workbench::where($where)->find(); + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在或无权限']); + } + + // 验证该群是否属于该工作台 + $groupItem = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('groupId', $groupId) + ->where('status', 2) // STATUS_SUCCESS = 2 + ->find(); + + if (empty($groupItem)) { + return json(['code' => 404, 'msg' => '群不存在或不属于该工作台']); + } + + // 查询群基本信息,获取chatroomId和wechatAccountWechatId + $group = Db::table('s2_wechat_chatroom') + ->where('id', $groupId) + ->where('isDeleted', 0) + ->field('id,chatroomId,wechatAccountWechatId') + ->find(); + + if (empty($group)) { + return json(['code' => 404, 'msg' => '群不存在']); + } + + $chatroomId = $group['chatroomId'] ?? ''; + $wechatAccountWechatId = $group['wechatAccountWechatId'] ?? ''; + + if (empty($chatroomId)) { + return json(['code' => 400, 'msg' => '群聊ID不存在']); + } + + try { + // 实例化WechatChatroomController + $chatroomController = new \app\api\controller\WechatChatroomController(); + + // 1. 同步群信息(调用getlist方法) + $syncData = [ + 'wechatChatroomId' => $chatroomId, // 使用chatroomId作为wechatChatroomId来指定要同步的群 + 'wechatAccountKeyword' => $wechatAccountWechatId, // 通过群主微信ID筛选 + 'isDeleted' => false, + 'pageIndex' => 1, + 'pageSize' => 100 // 获取足够多的数据 + ]; + $syncResult = $chatroomController->getlist($syncData, true, 0); // isInner = true + $syncResponse = json_decode($syncResult, true); + + if (empty($syncResponse['code']) || $syncResponse['code'] != 200) { + return json(['code' => 500, 'msg' => '同步群信息失败:' . ($syncResponse['msg'] ?? '未知错误')]); + } + + // 2. 同步群成员信息(调用listChatroomMember方法) + // wechatChatroomId 使用 s2_wechat_chatroom 表的 id(即groupId) + // chatroomId 使用群聊ID(chatroomId) + $wechatChatroomId = $groupId; // s2_wechat_chatroom表的id + $memberSyncResult = $chatroomController->listChatroomMember($wechatChatroomId, $chatroomId, true); // isInner = true + $memberSyncResponse = json_decode($memberSyncResult, true); + + if (empty($memberSyncResponse['code']) || $memberSyncResponse['code'] != 200) { + // 成员同步失败不影响整体结果,记录警告即可 + \think\facade\Log::warning("同步群成员失败。群ID: {$groupId}, 群聊ID: {$chatroomId}, 错误: " . ($memberSyncResponse['msg'] ?? '未知错误')); + } + + return json([ + 'code' => 200, + 'msg' => '同步成功', + 'data' => [ + 'groupId' => $groupId, + 'chatroomId' => $chatroomId, + 'groupInfoSynced' => true, + 'memberInfoSynced' => !empty($memberSyncResponse['code']) && $memberSyncResponse['code'] == 200 + ] + ]); + } catch (\Exception $e) { + \think\facade\Log::error("同步群信息异常。群ID: {$groupId}, 错误: " . $e->getMessage()); + return json(['code' => 500, 'msg' => '同步失败:' . $e->getMessage()]); + } + } + + /** + * 修改群名称、群公告 + * @return \think\response\Json + */ + public function modifyGroupInfo() + { + $workbenchId = $this->request->param('workbenchId', 0); + $groupId = $this->request->param('groupId', 0); + $chatroomName = $this->request->param('chatroomName', ''); + $announce = $this->request->param('announce', ''); + + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '工作台ID不能为空']); + } + + if (empty($groupId)) { + return json(['code' => 400, 'msg' => '群ID不能为空']); + } + + // 至少需要提供一个修改项 + if (empty($chatroomName) && empty($announce)) { + return json(['code' => 400, 'msg' => '请至少提供群名称或群公告中的一个参数']); + } + + // 查询群基本信息 + $group = Db::table('s2_wechat_chatroom') + ->where('id', $groupId) + ->where('isDeleted', 0) + ->field('id,chatroomId,wechatAccountWechatId,accountId,wechatAccountId') + ->find(); + + if (empty($group)) { + return json(['code' => 404, 'msg' => '群不存在']); + } + + $chatroomId = $group['id'] ?? ''; + + if (empty($chatroomId)) { + return json(['code' => 400, 'msg' => '群聊ID不存在']); + } + + try { + // 直接使用群表中的账号信息 + $executeAccountId = $group['accountId'] ?? 0; + $executeWechatAccountId = $group['wechatAccountId'] ?? 0; + $executeWechatId = $group['wechatAccountWechatId'] ?? ''; + + // 确保 wechatId 不为空 + if (empty($executeWechatId)) { + return json(['code' => 400, 'msg' => '无法获取微信账号ID']); + } + + // 调用 WebSocketController 修改群信息 + // 获取系统API账号信息(用于WebSocket连接) + $username = Env::get('api.username2', ''); + $password = Env::get('api.password2', ''); + + if (empty($username) || empty($password)) { + return json(['code' => 500, 'msg' => '系统API账号配置缺失']); + } + + // 获取系统账号ID + $systemAccountId = Db::name('users')->where('account', $username)->value('s2_accountId'); + if (empty($systemAccountId)) { + return json(['code' => 500, 'msg' => '未找到系统账号ID']); + } + + $webSocketController = new WebSocketController([ + 'userName' => $username, + 'password' => $password, + 'accountId' => $systemAccountId + ]); + // 构建修改参数 + $modifyData = [ + 'wechatChatroomId' => $chatroomId, + 'wechatAccountId' => $executeWechatAccountId, + ]; + if (!empty($chatroomName)) { + $modifyData['chatroomName'] = $chatroomName; + } + if (!empty($announce)) { + $modifyData['announce'] = $announce; + } + + + $modifyResult = $webSocketController->CmdChatroomModifyInfo($modifyData); + $modifyResponse = json_decode($modifyResult, true); + if (empty($modifyResponse['code']) || $modifyResponse['code'] != 200) { + return json(['code' => 500, 'msg' => '修改群信息失败:' . ($modifyResponse['msg'] ?? '未知错误')]); + } + + // 修改成功后更新数据库 + $updateData = [ + 'updateTime' => time() + ]; + + // 如果修改了群名称,更新数据库 + if (!empty($chatroomName)) { + $updateData['nickname'] = $chatroomName; + } + + // 如果修改了群公告,更新数据库 + if (!empty($announce)) { + $updateData['announce'] = $announce; + } + + // 更新数据库 + Db::table('s2_wechat_chatroom') + ->where('id', $groupId) + ->update($updateData); + + return json([ + 'code' => 200, + 'msg' => '修改成功', + 'data' => [ + 'groupId' => $groupId, + 'chatroomId' => $chatroomId, + 'chatroomName' => $chatroomName, + 'announce' => $announce + ] + ]); + } catch (\Exception $e) { + \think\facade\Log::error("修改群信息异常。群ID: {$groupId}, 错误: " . $e->getMessage()); + return json(['code' => 500, 'msg' => '修改失败:' . $e->getMessage()]); + } + } + + /** + * 转移群聊到指定账号 + * @param int $groupId 群ID(s2_wechat_chatroom表的id) + * @param string $chatroomId 群聊ID + * @param int $toAccountId 目标账号ID(s2_company_account表的id) + * @param int $toWechatAccountId 目标微信账号ID(s2_wechat_account表的id) + * @return array ['success' => bool, 'msg' => string] + */ + protected function transferChatroomToAccount($groupId, $chatroomId, $toAccountId, $toWechatAccountId) + { + try { + // 查询目标账号信息 + $targetAccount = Db::table('s2_company_account') + ->where('id', $toAccountId) + ->field('id,userName,realName,nickname') + ->find(); + + if (empty($targetAccount)) { + return ['success' => false, 'msg' => '目标账号不存在']; + } + + // 查询目标微信账号信息 + $targetWechatAccount = Db::table('s2_wechat_account') + ->where('id', $toWechatAccountId) + ->field('id,wechatId,deviceAccountId') + ->find(); + + if (empty($targetWechatAccount)) { + return ['success' => false, 'msg' => '目标微信账号不存在']; + } + + // 调用 AutomaticAssign 进行群聊转移 + $automaticAssign = new \app\api\controller\AutomaticAssign(); + + // 构建转移参数(通过 API 调用) + $transferData = [ + 'wechatChatroomId' => $chatroomId, // 使用群聊ID + 'toAccountId' => $toAccountId, + 'wechatAccountKeyword' => $targetWechatAccount['wechatId'] ?? '', + 'isDeleted' => false + ]; + + // 直接更新数据库(因为 API 可能不支持指定单个群聊ID转移) + // 更新 s2_wechat_chatroom 表的 accountId 和 wechatAccountId + Db::table('s2_wechat_chatroom') + ->where('id', $groupId) + ->update([ + 'accountId' => $toAccountId, + 'accountUserName' => $targetAccount['userName'] ?? '', + 'accountRealName' => $targetAccount['realName'] ?? '', + 'accountNickname' => $targetAccount['nickname'] ?? '', + 'wechatAccountId' => $toWechatAccountId, + 'wechatAccountWechatId' => $targetWechatAccount['wechatId'] ?? '', + 'updateTime' => time() + ]); + + return ['success' => true, 'msg' => '转移成功']; + } catch (\Exception $e) { + \think\facade\Log::error("转移群聊异常。群ID: {$groupId}, 目标账号ID: {$toAccountId}, 错误: " . $e->getMessage()); + return ['success' => false, 'msg' => $e->getMessage()]; + } + } + + /** + * 退群功能(自动建群) + * @return \think\response\Json + */ + public function quitGroup() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchGroupCreateController(); + $controller->request = $this->request; + return $controller->quitGroup(); + } + + +} \ No newline at end of file diff --git a/application/cunkebao/controller/workbench/WorkbenchController_new.php b/application/cunkebao/controller/workbench/WorkbenchController_new.php new file mode 100644 index 0000000..8363ea6 --- /dev/null +++ b/application/cunkebao/controller/workbench/WorkbenchController_new.php @@ -0,0 +1,3006 @@ +request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + // 获取登录用户信息 + $userInfo = request()->userInfo; + + // 获取请求参数 + $param = $this->request->post(); + + + // 根据业务默认值补全参数 + if ( + isset($param['type']) && + intval($param['type']) === self::TYPE_GROUP_PUSH + ) { + if (empty($param['startTime'])) { + $param['startTime'] = '09:00'; + } + if (empty($param['endTime'])) { + $param['endTime'] = '21:00'; + } + } + + // 验证数据 + $validate = new WorkbenchValidate; + if (!$validate->scene('create')->check($param)) { + return json(['code' => 400, 'msg' => $validate->getError()]); + } + + Db::startTrans(); + try { + // 创建工作台基本信息 + $workbench = new Workbench; + $workbench->name = $param['name']; + $workbench->type = $param['type']; + $workbench->status = !empty($param['status']) ? 1 : 0; + $workbench->autoStart = !empty($param['autoStart']) ? 1 : 0; + $workbench->userId = $userInfo['id']; + $workbench->companyId = $userInfo['companyId']; + $workbench->createTime = time(); + $workbench->updateTime = time(); + $workbench->save(); + + // 根据类型创建对应的配置 + switch ($param['type']) { + case self::TYPE_AUTO_LIKE: // 自动点赞 + $config = new WorkbenchAutoLike; + $config->workbenchId = $workbench->id; + $config->interval = $param['interval']; + $config->maxLikes = $param['maxLikes']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->contentTypes = json_encode($param['contentTypes']); + $config->devices = json_encode($param['deviceGroups']); + $config->friends = json_encode($param['wechatFriends']); + // $config->targetGroups = json_encode($param['targetGroups']); + // $config->tagOperator = $param['tagOperator']; + $config->friendMaxLikes = $param['friendMaxLikes']; + $config->friendTags = $param['friendTags']; + $config->enableFriendTags = $param['enableFriendTags']; + $config->createTime = time(); + $config->updateTime = time(); + $config->save(); + break; + case self::TYPE_MOMENTS_SYNC: // 朋友圈同步 + $config = new WorkbenchMomentsSync; + $config->workbenchId = $workbench->id; + $config->syncInterval = $param['syncInterval']; + $config->syncCount = $param['syncCount']; + $config->syncType = $param['syncType']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->accountType = $param['accountType']; + $config->devices = json_encode($param['deviceGroups']); + $config->contentLibraries = json_encode($param['contentGroups'] ?? []); + $config->createTime = time(); + $config->updateTime = time(); + $config->save(); + break; + case self::TYPE_GROUP_PUSH: // 群消息推送 + $ownerWechatIds = $this->normalizeOwnerWechatIds($param['ownerWechatIds'] ?? []); + $groupPushData = $this->prepareGroupPushData($param, $ownerWechatIds); + $groupPushData['workbenchId'] = $workbench->id; + $groupPushData['createTime'] = time(); + $groupPushData['updateTime'] = time(); + $config = new WorkbenchGroupPush; + $config->save($groupPushData); + break; + case self::TYPE_GROUP_CREATE: // 自动建群 + $config = new WorkbenchGroupCreate; + $config->workbenchId = $workbench->id; + $config->planType = !empty($param['planType']) ? $param['planType'] : 0; + $config->executorId = !empty($param['executorId']) ? $param['executorId'] : 0; + + $config->devices = json_encode($param['deviceGroups'] ?? [], JSON_UNESCAPED_UNICODE); + $config->startTime = $param['startTime'] ?? ''; + $config->endTime = $param['endTime'] ?? ''; + $config->groupSizeMin = intval($param['groupSizeMin'] ?? 3); + $config->groupSizeMax = intval($param['groupSizeMax'] ?? 38); + $config->maxGroupsPerDay = intval($param['maxGroupsPerDay'] ?? 20); + $config->groupNameTemplate = $param['groupNameTemplate'] ?? ''; + $config->groupDescription = $param['groupDescription'] ?? ''; + $config->poolGroups = json_encode($param['poolGroups'] ?? [], JSON_UNESCAPED_UNICODE); + $config->wechatGroups = json_encode($param['wechatGroups'] ?? [], JSON_UNESCAPED_UNICODE); + + // 处理群管理员:如果启用了群管理员且有指定管理员,则保存到admins字段 + $admins = []; + if (!empty($param['groupAdminEnabled']) && !empty($param['groupAdminWechatId'])) { + // 如果groupAdminWechatId是数组,取第一个;如果是单个值,直接使用 + $adminWechatId = is_array($param['groupAdminWechatId']) ? $param['groupAdminWechatId'][0] : $param['groupAdminWechatId']; + // 如果是好友ID,直接添加到admins;如果是wechatId,需要转换为好友ID + if (is_numeric($adminWechatId)) { + $admins[] = intval($adminWechatId); + } else { + // 如果是wechatId字符串,需要查询对应的好友ID + $friend = Db::table('s2_wechat_friend')->where('wechatId', $adminWechatId)->find(); + if ($friend) { + $admins[] = intval($friend['id']); + } + } + } + // 如果传入了admins参数,优先使用(兼容旧逻辑) + if (!empty($param['admins']) && is_array($param['admins'])) { + $admins = array_merge($admins, $param['admins']); + } + $config->admins = json_encode(array_unique($admins), JSON_UNESCAPED_UNICODE); + + $config->fixedWechatIds = json_encode($param['fixedWechatIds'] ?? [], JSON_UNESCAPED_UNICODE); + $config->createTime = time(); + $config->updateTime = time(); + $config->save(); + break; + case self::TYPE_TRAFFIC_DISTRIBUTION: // 流量分发 + $config = new WorkbenchTrafficConfig; + $config->workbenchId = $workbench->id; + $config->distributeType = $param['distributeType']; + $config->maxPerDay = $param['maxPerDay']; + $config->timeType = $param['timeType']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->devices = json_encode($param['deviceGroups'], JSON_UNESCAPED_UNICODE); + $config->pools = json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE); + $config->account = json_encode($param['accountGroups'], JSON_UNESCAPED_UNICODE); + $config->createTime = time(); + $config->updateTime = time(); + $config->save(); + break; + case self::TYPE_IMPORT_CONTACT: //联系人导入 + $config = new WorkbenchImportContact; + $config->workbenchId = $workbench->id; + $config->devices = json_encode($param['deviceGroups'], JSON_UNESCAPED_UNICODE); + $config->pools = json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE); + $config->num = $param['num']; + $config->clearContact = $param['clearContact']; + $config->remark = $param['remark']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->createTime = time(); + $config->save(); + break; + } + + Db::commit(); + return json(['code' => 200, 'msg' => '创建成功', 'data' => ['id' => $workbench->id]]); + } catch (\Exception $e) { + Db::rollback(); + return json(['code' => 500, 'msg' => '创建失败:' . $e->getMessage()]); + } + } + + /** + * 获取工作台列表 + * @return \think\response\Json + */ + public function getList() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $type = $this->request->param('type', ''); + $keyword = $this->request->param('keyword', ''); + + $where = [ + ['companyId', '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0] + ]; + + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + + + // 添加类型筛选 + if ($type !== '') { + $where[] = ['type', '=', $type]; + } + + // 添加名称模糊搜索 + if ($keyword !== '') { + $where[] = ['name', 'like', '%' . $keyword . '%']; + } + + // 定义关联关系 + $with = [ + 'autoLike' => function ($query) { + $query->field('workbenchId,interval,maxLikes,startTime,endTime,contentTypes,devices,friends'); + }, + 'momentsSync' => function ($query) { + $query->field('workbenchId,syncInterval,syncCount,syncType,startTime,endTime,accountType,devices,contentLibraries'); + }, + 'trafficConfig' => function ($query) { + $query->field('workbenchId,distributeType,maxPerDay,timeType,startTime,endTime,devices,pools,account'); + }, + 'groupPush' => function ($query) { + $query->field('workbenchId,pushType,targetType,groupPushSubType,startTime,endTime,maxPerDay,pushOrder,isLoop,status,groups,friends,ownerWechatIds,trafficPools,contentLibraries,friendIntervalMin,friendIntervalMax,messageIntervalMin,messageIntervalMax,isRandomTemplate,postPushTags,announcementContent,enableAiRewrite,aiRewritePrompt'); + }, + 'groupCreate' => function ($query) { + $query->field('workbenchId,devices,startTime,endTime,groupSizeMin,groupSizeMax,maxGroupsPerDay,groupNameTemplate,groupDescription,poolGroups,wechatGroups,admins'); + }, + 'importContact' => function ($query) { + $query->field('workbenchId,devices,pools,num,remarkType,remark,clearContact,startTime,endTime'); + }, + 'user' => function ($query) { + $query->field('id,username'); + } + ]; + + $list = Workbench::where($where) + ->with($with) + ->field('id,companyId,name,type,status,autoStart,userId,createTime,updateTime') + ->order('id', 'desc') + ->page($page, $limit) + ->select() + ->each(function ($item) { + // 处理配置信息 + switch ($item->type) { + case self::TYPE_AUTO_LIKE: + if (!empty($item->autoLike)) { + $item->config = $item->autoLike; + $item->config->devices = json_decode($item->config->devices, true); + $item->config->contentTypes = json_decode($item->config->contentTypes, true); + $item->config->friends = json_decode($item->config->friends, true); + + // 添加今日点赞数 + $startTime = strtotime(date('Y-m-d') . ' 00:00:00'); + $endTime = strtotime(date('Y-m-d') . ' 23:59:59'); + $todayLikeCount = Db::name('workbench_auto_like_item') + ->where('workbenchId', $item->id) + ->whereTime('createTime', 'between', [$startTime, $endTime]) + ->count(); + + // 添加总点赞数 + $totalLikeCount = Db::name('workbench_auto_like_item') + ->where('workbenchId', $item->id) + ->count(); + + $item->config->todayLikeCount = $todayLikeCount; + $item->config->totalLikeCount = $totalLikeCount; + } + unset($item->autoLike, $item->auto_like); + break; + case self::TYPE_MOMENTS_SYNC: + if (!empty($item->momentsSync)) { + $item->config = $item->momentsSync; + $item->config->devices = json_decode($item->config->devices, true); + $item->config->contentGroups = json_decode($item->config->contentLibraries, true); + //同步记录 + $sendNum = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $item->id])->count(); + $item->syncCount = $sendNum; + $lastTime = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $item->id])->order('id DESC')->value('createTime'); + $item->lastSyncTime = !empty($lastTime) ? date('Y-m-d H:i', $lastTime) : '--'; + + + // 获取内容库名称 + if (!empty($item->config->contentGroups)) { + $libraryNames = ContentLibrary::where('id', 'in', $item->config->contentGroups)->select(); + $item->config->contentGroupsOptions = $libraryNames; + } else { + $item->config->contentGroupsOptions = []; + } + } + unset($item->momentsSync, $item->moments_sync, $item->config->contentLibraries); + break; + case self::TYPE_GROUP_PUSH: + if (!empty($item->groupPush)) { + $item->config = $item->groupPush; + $item->config->pushType = $item->config->pushType; + $item->config->targetType = isset($item->config->targetType) ? intval($item->config->targetType) : 1; // 默认1=群推送 + $item->config->groupPushSubType = isset($item->config->groupPushSubType) ? intval($item->config->groupPushSubType) : 1; // 默认1=群群发 + $item->config->startTime = $item->config->startTime; + $item->config->endTime = $item->config->endTime; + $item->config->maxPerDay = $item->config->maxPerDay; + $item->config->pushOrder = $item->config->pushOrder; + $item->config->isLoop = $item->config->isLoop; + $item->config->status = $item->config->status; + $item->config->ownerWechatIds = json_decode($item->config->ownerWechatIds ?? '[]', true) ?: []; + // 根据targetType解析不同的数据 + if ($item->config->targetType == 1) { + // 群推送 + $item->config->wechatGroups = json_decode($item->config->groups, true) ?: []; + $item->config->wechatFriends = []; + // 群推送不需要devices字段 + // 群公告相关字段 + if ($item->config->groupPushSubType == 2) { + $item->config->announcementContent = isset($item->config->announcementContent) ? $item->config->announcementContent : ''; + $item->config->enableAiRewrite = isset($item->config->enableAiRewrite) ? intval($item->config->enableAiRewrite) : 0; + $item->config->aiRewritePrompt = isset($item->config->aiRewritePrompt) ? $item->config->aiRewritePrompt : ''; + } + $item->config->trafficPools = []; + } else { + // 好友推送 + $item->config->wechatFriends = json_decode($item->config->friends, true) ?: []; + $item->config->wechatGroups = []; + $item->config->trafficPools = json_decode($item->config->trafficPools ?? '[]', true) ?: []; + } + $item->config->contentLibraries = json_decode($item->config->contentLibraries, true); + $item->config->postPushTags = json_decode($item->config->postPushTags ?? '[]', true) ?: []; + $item->config->lastPushTime = ''; + if (!empty($item->config->ownerWechatIds)) { + $ownerWechatOptions = Db::name('wechat_account') + ->whereIn('id', $item->config->ownerWechatIds) + ->field('id,wechatId,nickName,avatar,alias') + ->select(); + $item->config->ownerWechatOptions = $ownerWechatOptions; + } else { + $item->config->ownerWechatOptions = []; + } + } + unset($item->groupPush, $item->group_push); + break; + case self::TYPE_GROUP_CREATE: + if (!empty($item->groupCreate)) { + $item->config = $item->groupCreate; + $item->config->devices = json_decode($item->config->devices, true); + $item->config->poolGroups = json_decode($item->config->poolGroups, true); + $item->config->wechatGroups = json_decode($item->config->wechatGroups, true); + $item->config->admins = json_decode($item->config->admins ?? '[]', true) ?: []; + + // 处理群管理员相关字段 + $item->config->groupAdminEnabled = !empty($item->config->admins) ? 1 : 0; + + if (!empty($item->config->admins)) { + $adminOptions = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->where('wf.id', 'in', $item->config->admins) + ->order('wf.id', 'desc') + ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar') + ->select(); + $item->config->adminsOptions = $adminOptions; + // 如果有管理员,设置groupAdminWechatId为第一个管理员的ID(用于前端回显) + $item->config->groupAdminWechatId = !empty($item->config->admins) ? $item->config->admins[0] : null; + } else { + $item->config->adminsOptions = []; + $item->config->groupAdminWechatId = null; + } + } + unset($item->groupCreate, $item->group_create); + break; + case self::TYPE_TRAFFIC_DISTRIBUTION: + if (!empty($item->trafficConfig)) { + $item->config = $item->trafficConfig; + $item->config->devices = json_decode($item->config->devices, true); + $item->config->poolGroups = json_decode($item->config->pools, true); + $item->config->account = json_decode($item->config->account, true); + $config_item = Db::name('workbench_traffic_config_item')->where(['workbenchId' => $item->id])->order('id DESC')->find(); + $item->config->lastUpdated = !empty($config_item) ? date('Y-m-d H:i', $config_item['createTime']) : '--'; + + //统计 + $labels = $item->config->poolGroups; + $totalUsers = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->where([ + ['wf.isDeleted', '=', 0], + ['sa.departmentId', '=', $item->companyId] + ]) + ->whereIn('wa.currentDeviceId', $item->config->devices); + + if (!empty($labels) && count($labels) > 0) { + $totalUsers = $totalUsers->where(function ($q) use ($labels) { + foreach ($labels as $label) { + $q->whereOrRaw("JSON_CONTAINS(wf.labels, '\"{$label}\"')"); + } + }); + } + + $totalUsers = $totalUsers->count(); + $totalAccounts = count($item->config->account); + $dailyAverage = Db::name('workbench_traffic_config_item') + ->where('workbenchId', $item->id) + ->count(); + $day = (time() - strtotime($item->createTime)) / 86400; + $day = intval($day); + if ($dailyAverage > 0 && $totalAccounts > 0 && $day > 0) { + $dailyAverage = $dailyAverage / $totalAccounts / $day; + } + $item->config->total = [ + 'dailyAverage' => intval($dailyAverage), + 'totalAccounts' => $totalAccounts, + 'deviceCount' => count($item->config->devices), + 'poolCount' => !empty($item->config->poolGroups) ? count($item->config->poolGroups) : 'ALL', + 'totalUsers' => $totalUsers >> 0 + ]; + } + unset($item->trafficConfig, $item->traffic_config); + break; + + case self::TYPE_IMPORT_CONTACT: + if (!empty($item->importContact)) { + $item->config = $item->importContact; + $item->config->devices = json_decode($item->config->devices, true); + $item->config->poolGroups = json_decode($item->config->pools, true); + } + unset($item->importContact, $item->import_contact); + break; + } + // 添加创建人名称 + $item['creatorName'] = $item->user ? $item->user->username : ''; + unset($item['user']); // 移除关联数据 + return $item; + }); + + $total = Workbench::where($where)->count(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + + /** + * 获取工作台详情 + * @param int $id 工作台ID + * @return \think\response\Json + */ + public function detail() + { + $id = $this->request->param('id', ''); + + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + // 定义关联关系 + $with = [ + 'autoLike' => function ($query) { + $query->field('workbenchId,interval,maxLikes,startTime,endTime,contentTypes,devices,friends,friendMaxLikes,friendTags,enableFriendTags'); + }, + 'momentsSync' => function ($query) { + $query->field('workbenchId,syncInterval,syncCount,syncType,startTime,endTime,accountType,devices,contentLibraries'); + }, + 'trafficConfig' => function ($query) { + $query->field('workbenchId,distributeType,maxPerDay,timeType,startTime,endTime,devices,pools,account'); + }, + 'groupPush' => function ($query) { + $query->field('workbenchId,pushType,targetType,groupPushSubType,startTime,endTime,maxPerDay,pushOrder,isLoop,status,groups,friends,ownerWechatIds,trafficPools,contentLibraries,friendIntervalMin,friendIntervalMax,messageIntervalMin,messageIntervalMax,isRandomTemplate,postPushTags,announcementContent,enableAiRewrite,aiRewritePrompt'); + }, + 'groupCreate' => function ($query) { + $query->field('workbenchId,devices,startTime,endTime,groupSizeMin,groupSizeMax,maxGroupsPerDay,groupNameTemplate,groupDescription,poolGroups,wechatGroups,admins'); + }, + 'importContact' => function ($query) { + $query->field('workbenchId,devices,pools,num,remarkType,remark,clearContact,startTime,endTime'); + }, + ]; + + $where = [ + ['id', '=', $id], + ['companyId', '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + + $workbench = Workbench::where($where) + ->field('id,name,type,status,autoStart,createTime,updateTime,companyId') + ->with($with) + ->find(); + + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + + // 处理配置信息 + switch ($workbench->type) { + //自动点赞 + case self::TYPE_AUTO_LIKE: + if (!empty($workbench->autoLike)) { + $workbench->config = $workbench->autoLike; + $workbench->config->deviceGroups = json_decode($workbench->config->devices, true); + $workbench->config->wechatFriends = json_decode($workbench->config->friends, true); + $workbench->config->targetType = 2; + //$workbench->config->targetGroups = json_decode($workbench->config->targetGroups, true); + $workbench->config->contentTypes = json_decode($workbench->config->contentTypes, true); + + // 添加今日点赞数 + $startTime = strtotime(date('Y-m-d') . ' 00:00:00'); + $endTime = strtotime(date('Y-m-d') . ' 23:59:59'); + $todayLikeCount = Db::name('workbench_auto_like_item') + ->where('workbenchId', $workbench->id) + ->whereTime('createTime', 'between', [$startTime, $endTime]) + ->count(); + + // 添加总点赞数 + $totalLikeCount = Db::name('workbench_auto_like_item') + ->where('workbenchId', $workbench->id) + ->count(); + + $workbench->config->todayLikeCount = $todayLikeCount; + $workbench->config->totalLikeCount = $totalLikeCount; + + unset($workbench->autoLike, $workbench->auto_like); + } + break; + //自动同步朋友圈 + case self::TYPE_MOMENTS_SYNC: + if (!empty($workbench->momentsSync)) { + $workbench->config = $workbench->momentsSync; + $workbench->config->deviceGroups = json_decode($workbench->config->devices, true); + $workbench->config->contentGroups = json_decode($workbench->config->contentLibraries, true); + + //同步记录 + $sendNum = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $workbench->id])->count(); + $workbench->syncCount = $sendNum; + $lastTime = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $workbench->id])->order('id DESC')->value('createTime'); + $workbench->lastSyncTime = !empty($lastTime) ? date('Y-m-d H:i', $lastTime) : '--'; + unset($workbench->momentsSync, $workbench->moments_sync); + } + break; + //群推送 + case self::TYPE_GROUP_PUSH: + if (!empty($workbench->groupPush)) { + $workbench->config = $workbench->groupPush; + $workbench->config->targetType = isset($workbench->config->targetType) ? intval($workbench->config->targetType) : 1; // 默认1=群推送 + $workbench->config->groupPushSubType = isset($workbench->config->groupPushSubType) ? intval($workbench->config->groupPushSubType) : 1; // 默认1=群群发 + $workbench->config->ownerWechatIds = json_decode($workbench->config->ownerWechatIds ?? '[]', true) ?: []; + // 根据targetType解析不同的数据 + if ($workbench->config->targetType == 1) { + // 群推送 + $workbench->config->wechatGroups = json_decode($workbench->config->groups, true) ?: []; + $workbench->config->wechatFriends = []; + $workbench->config->trafficPools = []; + // 群推送不需要devices字段 + // 群公告相关字段 + if ($workbench->config->groupPushSubType == 2) { + $workbench->config->announcementContent = isset($workbench->config->announcementContent) ? $workbench->config->announcementContent : ''; + $workbench->config->enableAiRewrite = isset($workbench->config->enableAiRewrite) ? intval($workbench->config->enableAiRewrite) : 0; + $workbench->config->aiRewritePrompt = isset($workbench->config->aiRewritePrompt) ? $workbench->config->aiRewritePrompt : ''; + } + } else { + // 好友推送 + $workbench->config->wechatFriends = json_decode($workbench->config->friends, true) ?: []; + $workbench->config->wechatGroups = []; + $workbench->config->trafficPools = json_decode($workbench->config->trafficPools ?? '[]', true) ?: []; + } + $workbench->config->contentLibraries = json_decode($workbench->config->contentLibraries, true); + $workbench->config->postPushTags = json_decode($workbench->config->postPushTags ?? '[]', true) ?: []; + unset($workbench->groupPush, $workbench->group_push); + } + break; + //建群助手 + case self::TYPE_GROUP_CREATE: + if (!empty($workbench->groupCreate)) { + $workbench->config = $workbench->groupCreate; + $workbench->config->deviceGroups = json_decode($workbench->config->devices, true); + $workbench->config->poolGroups = json_decode($workbench->config->poolGroups, true); + $workbench->config->wechatGroups = json_decode($workbench->config->wechatGroups, true); + $workbench->config->admins = json_decode($workbench->config->admins ?? '[]', true) ?: []; + + // 处理群管理员相关字段 + $workbench->config->groupAdminEnabled = !empty($workbench->config->admins) ? 1 : 0; + + // 如果有管理员,设置groupAdminWechatId为第一个管理员的ID(用于前端回显) + $workbench->config->groupAdminWechatId = !empty($workbench->config->admins) ? $workbench->config->admins[0] : null; + + // 统计已建群数(状态为成功且groupId不为空的记录,按groupId分组去重) + $createdGroupsCount = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbench->id) + ->where('status', 2) // STATUS_SUCCESS = 2 + ->where('groupId', '<>', null) + ->group('groupId') + ->count(); + + // 统计总人数(该工作台的所有记录数) + $totalMembersCount = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbench->id) + ->count(); + + // 添加统计信息 + $workbench->config->stats = [ + 'createdGroupsCount' => $createdGroupsCount, + 'totalMembersCount' => $totalMembersCount + ]; + + unset($workbench->groupCreate, $workbench->group_create); + } + break; + //流量分发 + case self::TYPE_TRAFFIC_DISTRIBUTION: + if (!empty($workbench->trafficConfig)) { + $workbench->config = $workbench->trafficConfig; + $workbench->config->deviceGroups = json_decode($workbench->config->devices, true); + $workbench->config->accountGroups = json_decode($workbench->config->account, true); + $workbench->config->poolGroups = json_decode($workbench->config->pools, true); + $config_item = Db::name('workbench_traffic_config_item')->where(['workbenchId' => $workbench->id])->order('id DESC')->find(); + $workbench->config->lastUpdated = !empty($config_item) ? date('Y-m-d H:i', $config_item['createTime']) : '--'; + + //统计 + $labels = $workbench->config->poolGroups; + $totalUsers = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->where([ + ['wf.isDeleted', '=', 0], + ['sa.departmentId', '=', $workbench->companyId] + ]) + ->whereIn('wa.currentDeviceId', $workbench->config->deviceGroups) + ->field('wf.id,wf.wechatAccountId,wf.wechatId,wf.labels,sa.userName,wa.currentDeviceId as deviceId') + ->where(function ($q) use ($labels) { + foreach ($labels as $label) { + $q->whereOrRaw("JSON_CONTAINS(wf.labels, '\"{$label}\"')"); + } + })->count(); + + $totalAccounts = Db::table('s2_company_account') + ->alias('a') + ->where(['a.departmentId' => $workbench->companyId, 'a.status' => 0]) + ->whereNotLike('a.userName', '%_offline%') + ->whereNotLike('a.userName', '%_delete%') + ->group('a.id') + ->count(); + + $dailyAverage = Db::name('workbench_traffic_config_item') + ->where('workbenchId', $workbench->id) + ->count(); + $day = (time() - strtotime($workbench->createTime)) / 86400; + $day = intval($day); + + + if ($dailyAverage > 0) { + $dailyAverage = $dailyAverage / $totalAccounts / $day; + } + + $workbench->config->total = [ + 'dailyAverage' => intval($dailyAverage), + 'totalAccounts' => $totalAccounts, + 'deviceCount' => count($workbench->config->deviceGroups), + 'poolCount' => count($workbench->config->poolGroups), + 'totalUsers' => $totalUsers >> 0 + ]; + unset($workbench->trafficConfig, $workbench->traffic_config); + } + break; + case self::TYPE_IMPORT_CONTACT: + if (!empty($workbench->importContact)) { + $workbench->config = $workbench->importContact; + $workbench->config->deviceGroups = json_decode($workbench->config->devices, true); + $workbench->config->poolGroups = json_decode($workbench->config->pools, true); + } + unset($workbench->importContact, $workbench->import_contact); + break; + } + unset( + $workbench->autoLike, + $workbench->momentsSync, + $workbench->groupPush, + $workbench->groupCreate, + $workbench->config->devices, + $workbench->config->friends, + $workbench->config->groups, + $workbench->config->contentLibraries, + $workbench->config->account, + ); + + + //获取设备信息 + if (!empty($workbench->config->deviceGroups)) { + $deviceList = DeviceModel::alias('d') + ->field([ + 'd.id', 'd.imei', 'd.memo', 'd.alive', + 'l.wechatId', + 'a.nickname', 'a.alias', 'a.avatar', 'a.alias', '0 totalFriend' + ]) + ->leftJoin('device_wechat_login l', 'd.id = l.deviceId and l.alive =' . DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE . ' and l.companyId = d.companyId') + ->leftJoin('wechat_account a', 'l.wechatId = a.wechatId') + ->whereIn('d.id', $workbench->config->deviceGroups) + ->order('d.id desc') + ->select(); + + foreach ($deviceList as &$device) { + $curstomer = WechatCustomerModel::field('friendShip')->where(['wechatId' => $device['wechatId']])->find(); + $device['totalFriend'] = $curstomer->friendShip->totalFriend ?? 0; + } + unset($device); + + $workbench->config->deviceGroupsOptions = $deviceList; + } else { + $workbench->config->deviceGroupsOptions = []; + } + + + // 获取群(当targetType=1时) + if (!empty($workbench->config->wechatGroups) && isset($workbench->config->targetType) && $workbench->config->targetType == 1) { + $groupList = Db::name('wechat_group')->alias('wg') + ->join('wechat_account wa', 'wa.wechatId = wg.ownerWechatId') + ->where('wg.id', 'in', $workbench->config->wechatGroups) + ->order('wg.id', 'desc') + ->field('wg.id,wg.name as groupName,wg.ownerWechatId,wa.nickName,wa.avatar,wa.alias,wg.avatar as groupAvatar') + ->select(); + $workbench->config->wechatGroupsOptions = $groupList; + } else { + $workbench->config->wechatGroupsOptions = []; + } + + // 获取好友(当targetType=2时) + if (!empty($workbench->config->wechatFriends) && isset($workbench->config->targetType) && $workbench->config->targetType == 2) { + $friendList = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->where('wf.id', 'in', $workbench->config->wechatFriends) + ->order('wf.id', 'desc') + ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar') + ->select(); + $workbench->config->wechatFriendsOptions = $friendList; + } else { + $workbench->config->wechatFriendsOptions = []; + } + + // 获取流量池(当targetType=2时) + if (!empty($workbench->config->trafficPools) && isset($workbench->config->targetType) && $workbench->config->targetType == 2) { + $poolList = Db::name('traffic_source_package')->alias('tsp') + ->leftJoin('traffic_source_package_item tspi', 'tspi.packageId = tsp.id and tspi.isDel = 0') + ->whereIn('tsp.id', $workbench->config->trafficPools) + ->where('tsp.isDel', 0) + ->whereIn('tsp.companyId', [$this->request->userInfo['companyId'], 0]) + ->field('tsp.id,tsp.name,tsp.description,tsp.pic,COUNT(tspi.id) as itemCount') + ->group('tsp.id') + ->order('tsp.id', 'desc') + ->select(); + $workbench->config->trafficPoolsOptions = $poolList; + } else { + $workbench->config->trafficPoolsOptions = []; + } + + // 获取内容库名称 + if (!empty($workbench->config->contentGroups)) { + $libraryNames = ContentLibrary::where('id', 'in', $workbench->config->contentGroups)->select(); + $workbench->config->contentGroupsOptions = $libraryNames; + } else { + $workbench->config->contentGroupsOptions = []; + } + + //账号 + if (!empty($workbench->config->accountGroups)) { + $account = Db::table('s2_company_account')->alias('a') + ->where(['a.departmentId' => $this->request->userInfo['companyId'], 'a.status' => 0]) + ->whereIn('a.id', $workbench->config->accountGroups) + ->whereNotLike('a.userName', '%_offline%') + ->whereNotLike('a.userName', '%_delete%') + ->field('a.id,a.userName,a.realName,a.nickname,a.memo') + ->select(); + $workbench->config->accountGroupsOptions = $account; + } else { + $workbench->config->accountGroupsOptions = []; + } + + if (!empty($workbench->config->poolGroups)) { + $poolGroupsOptions = Db::name('traffic_source_package')->alias('tsp') + ->join('traffic_source_package_item tspi', 'tspi.packageId=tsp.id', 'left') + ->whereIn('tsp.companyId', [$this->request->userInfo['companyId'], 0]) + ->whereIn('tsp.id', $workbench->config->poolGroups) + ->field('tsp.id,tsp.name,tsp.description,tsp.createTime,count(tspi.id) as num') + ->group('tsp.id') + ->select(); + $workbench->config->poolGroupsOptions = $poolGroupsOptions; + } else { + $workbench->config->poolGroupsOptions = []; + } + + if (!empty($workbench->config->ownerWechatIds)) { + $ownerWechatOptions = Db::name('wechat_account') + ->whereIn('id', $workbench->config->ownerWechatIds) + ->field('id,wechatId,nickName,avatar,alias') + ->select(); + $workbench->config->ownerWechatOptions = $ownerWechatOptions; + } else { + $workbench->config->ownerWechatOptions = []; + } + + // 获取群组选项(自动建群) + if ($workbench->type == self::TYPE_GROUP_CREATE && !empty($workbench->config->wechatGroups)) { + // 分离数字ID(好友ID)和字符串ID(手动创建的群组) + $friendIds = []; + $manualGroupIds = []; + + foreach ($workbench->config->wechatGroups as $groupId) { + if (is_numeric($groupId)) { + $friendIds[] = intval($groupId); + } else { + $manualGroupIds[] = $groupId; + } + } + + $wechatGroupsOptions = []; + + // 查询好友信息(数字ID) + if (!empty($friendIds)) { + $friendList = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->join(['s2_company_account' => 'ca'], 'ca.id = wf.accountId', 'left') + ->where('wf.id', 'in', $friendIds) + ->order('wf.id', 'desc') + ->field('wf.id,wf.wechatId,wf.nickname,wf.avatar,wf.alias,wf.gender,wf.phone,wa.nickName as accountNickname,ca.userName as account,ca.realName as username,wf.createTime,wf.updateTime,wf.deleteTime,wf.ownerWechatId') + ->select(); + + // 获取群主信息 + foreach ($friendList as &$friend) { + if (!empty($friend['ownerWechatId'])) { + $owner = Db::name('wechat_account') + ->where('wechatId', $friend['ownerWechatId']) + ->field('nickName,alias') + ->find(); + $friend['ownerNickname'] = $owner['nickName'] ?? ''; + $friend['ownerAlias'] = $owner['alias'] ?? ''; + } else { + $friend['ownerNickname'] = ''; + $friend['ownerAlias'] = ''; + } + $friend['isManual'] = ''; + // 格式化时间 + $friend['createTime'] = !empty($friend['createTime']) ? date('Y-m-d H:i:s', $friend['createTime']) : ''; + $friend['updateTime'] = !empty($friend['updateTime']) ? date('Y-m-d H:i:s', $friend['updateTime']) : ''; + $friend['deleteTime'] = !empty($friend['deleteTime']) ? date('Y-m-d H:i:s', $friend['deleteTime']) : ''; + } + unset($friend); + + $wechatGroupsOptions = array_merge($wechatGroupsOptions, $friendList); + } + + // 处理手动创建的群组(字符串ID) + if (!empty($manualGroupIds)) { + foreach ($manualGroupIds as $groupId) { + // 手动创建的群组,只返回基本信息 + $wechatGroupsOptions[] = [ + 'id' => $groupId, + 'wechatId' => $groupId, + 'nickname' => $groupId, + 'avatar' => '', + 'isManual' => 1 + ]; + } + } + + $workbench->config->wechatGroupsOptions = $wechatGroupsOptions; + } else { + $workbench->config->wechatGroupsOptions = []; + } + + // 获取管理员选项(自动建群) + if ($workbench->type == self::TYPE_GROUP_CREATE && !empty($workbench->config->admins)) { + $adminOptions = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->where('wf.id', 'in', $workbench->config->admins) + ->order('wf.id', 'desc') + ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar') + ->select(); + $workbench->config->adminsOptions = $adminOptions; + } else { + $workbench->config->adminsOptions = []; + } + + return json(['code' => 200, 'msg' => '获取成功', 'data' => $workbench]); + } + + /** + * 更新工作台 + * @return \think\response\Json + */ + public function update() + { + if (!$this->request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + // 获取请求参数 + $param = $this->request->post(); + + // 验证数据 + $validate = new WorkbenchValidate; + if (!$validate->scene('update')->check($param)) { + return json(['code' => 400, 'msg' => $validate->getError()]); + } + + + $where = [ + ['id', '=', $param['id']], + ['companyId', '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + // 查询工作台是否存在 + $workbench = Workbench::where($where)->find(); + if (!$workbench) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + + Db::startTrans(); + try { + // 更新工作台基本信息 + $workbench->name = $param['name']; + $workbench->status = !empty($param['status']) ? 1 : 0; + $workbench->autoStart = !empty($param['autoStart']) ? 1 : 0; + $workbench->updateTime = time(); + $workbench->save(); + + // 根据类型更新对应的配置 + switch ($workbench->type) { + case self::TYPE_AUTO_LIKE: + $config = WorkbenchAutoLike::where('workbenchId', $param['id'])->find(); + if ($config) { + $config->interval = $param['interval']; + $config->maxLikes = $param['maxLikes']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->contentTypes = json_encode($param['contentTypes']); + $config->devices = json_encode($param['deviceGroups']); + $config->friends = json_encode($param['wechatFriends']); + // $config->targetGroups = json_encode($param['targetGroups']); + // $config->tagOperator = $param['tagOperator']; + $config->friendMaxLikes = $param['friendMaxLikes']; + $config->friendTags = $param['friendTags']; + $config->enableFriendTags = $param['enableFriendTags']; + $config->updateTime = time(); + $config->save(); + } + break; + + case self::TYPE_MOMENTS_SYNC: + $config = WorkbenchMomentsSync::where('workbenchId', $param['id'])->find(); + if ($config) { + if (!empty($param['contentGroups'])) { + foreach ($param['contentGroups'] as $library) { + if (isset($library['id']) && !empty($library['id'])) { + $contentLibraries[] = $library['id']; + } else { + $contentLibraries[] = $library; + } + } + } else { + $contentLibraries = []; + } + + $config->syncInterval = $param['syncInterval']; + $config->syncCount = $param['syncCount']; + $config->syncType = $param['syncType']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->accountType = $param['accountType']; + $config->devices = json_encode($param['deviceGroups']); + $config->contentLibraries = json_encode($contentLibraries); + $config->updateTime = time(); + $config->save(); + } + break; + + case self::TYPE_GROUP_PUSH: + $config = WorkbenchGroupPush::where('workbenchId', $param['id'])->find(); + if ($config) { + $ownerWechatIds = $this->normalizeOwnerWechatIds($param['ownerWechatIds'] ?? null, $config); + $groupPushData = $this->prepareGroupPushData($param, $ownerWechatIds, $config); + $groupPushData['updateTime'] = time(); + $config->save($groupPushData); + } + break; + + case self::TYPE_GROUP_CREATE: + $config = WorkbenchGroupCreate::where('workbenchId', $param['id'])->find(); + if ($config) { + $config->devices = json_encode($param['deviceGroups'] ?? [], JSON_UNESCAPED_UNICODE); + $config->startTime = $param['startTime'] ?? ''; + $config->endTime = $param['endTime'] ?? ''; + $config->groupSizeMin = intval($param['groupSizeMin'] ?? 3); + $config->groupSizeMax = intval($param['groupSizeMax'] ?? 38); + $config->maxGroupsPerDay = intval($param['maxGroupsPerDay'] ?? 20); + $config->groupNameTemplate = $param['groupNameTemplate'] ?? ''; + $config->groupDescription = $param['groupDescription'] ?? ''; + $config->poolGroups = json_encode($param['poolGroups'] ?? [], JSON_UNESCAPED_UNICODE); + $config->wechatGroups = json_encode($param['wechatGroups'] ?? [], JSON_UNESCAPED_UNICODE); + + // 处理群管理员:如果启用了群管理员且有指定管理员,则保存到admins字段 + $admins = []; + if (!empty($param['groupAdminEnabled']) && !empty($param['groupAdminWechatId'])) { + // 如果groupAdminWechatId是数组,取第一个;如果是单个值,直接使用 + $adminWechatId = is_array($param['groupAdminWechatId']) ? $param['groupAdminWechatId'][0] : $param['groupAdminWechatId']; + // 如果是好友ID,直接添加到admins;如果是wechatId,需要转换为好友ID + if (is_numeric($adminWechatId)) { + $admins[] = intval($adminWechatId); + } else { + // 如果是wechatId字符串,需要查询对应的好友ID + $friend = Db::table('s2_wechat_friend')->where('wechatId', $adminWechatId)->find(); + if ($friend) { + $admins[] = intval($friend['id']); + } + } + } + // 如果传入了admins参数,优先使用(兼容旧逻辑) + if (!empty($param['admins']) && is_array($param['admins'])) { + $admins = array_merge($admins, $param['admins']); + } + $config->admins = json_encode(array_unique($admins), JSON_UNESCAPED_UNICODE); + + $config->updateTime = time(); + $config->save(); + } + break; + case self::TYPE_TRAFFIC_DISTRIBUTION: + $config = WorkbenchTrafficConfig::where('workbenchId', $param['id'])->find(); + if ($config) { + $config->distributeType = $param['distributeType']; + $config->maxPerDay = $param['maxPerDay']; + $config->timeType = $param['timeType']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->devices = json_encode($param['deviceGroups']); + $config->pools = json_encode($param['poolGroups']); + $config->account = json_encode($param['accountGroups']); + $config->updateTime = time(); + $config->save(); + } + break; + case self::TYPE_IMPORT_CONTACT: //联系人导入 + $config = WorkbenchImportContact::where('workbenchId', $param['id'])->find();; + if ($config) { + $config->devices = json_encode($param['deviceGroups']); + $config->pools = json_encode($param['poolGroups']); + $config->num = $param['num']; + $config->clearContact = $param['clearContact']; + $config->remark = $param['remark']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->save(); + } + break; + } + + Db::commit(); + return json(['code' => 200, 'msg' => '更新成功']); + } catch (\Exception $e) { + Db::rollback(); + return json(['code' => 500, 'msg' => '更新失败:' . $e->getMessage()]); + } + } + + /** + * 更新工作台状态 + * @return \think\response\Json + */ + public function updateStatus() + { + if (!$this->request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + $id = $this->request->param('id', ''); + + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + + + $where = [ + ['id', '=', $id], + ['companyId', '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + $workbench = Workbench::where($where)->find(); + + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + + $workbench->status = !$workbench['status']; + $workbench->save(); + + return json(['code' => 200, 'msg' => '更新成功']); + } + + /** + * 删除工作台(软删除) + */ + public function delete() + { + $id = $this->request->param('id'); + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + $where = [ + ['id', '=', $id], + ['companyId', '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + $workbench = Workbench::where($where)->find(); + + if (!$workbench) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + + // 软删除 + $workbench->isDel = 1; + $workbench->deleteTime = time(); + $workbench->save(); + + return json(['code' => 200, 'msg' => '删除成功']); + } + + /** + * 拷贝工作台 + * @return \think\response\Json + */ + public function copy() + { + if (!$this->request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + $id = $this->request->post('id'); + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + // 验证权限并获取原数据 + $workbench = Workbench::where([ + ['id', '=', $id], + ['userId', '=', $this->request->userInfo['id']] + ])->find(); + + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + + Db::startTrans(); + try { + // 创建新的工作台基本信息 + $newWorkbench = new Workbench; + $newWorkbench->name = $workbench->name . ' copy'; + $newWorkbench->type = $workbench->type; + $newWorkbench->status = 1; // 新拷贝的默认启用 + $newWorkbench->autoStart = $workbench->autoStart; + $newWorkbench->userId = $this->request->userInfo['id']; + $newWorkbench->companyId = $this->request->userInfo['companyId']; + $newWorkbench->save(); + + // 根据类型拷贝对应的配置 + switch ($workbench->type) { + case self::TYPE_AUTO_LIKE: + $config = WorkbenchAutoLike::where('workbenchId', $id)->find(); + if ($config) { + $newConfig = new WorkbenchAutoLike; + $newConfig->workbenchId = $newWorkbench->id; + $newConfig->interval = $config->interval; + $newConfig->maxLikes = $config->maxLikes; + $newConfig->startTime = $config->startTime; + $newConfig->endTime = $config->endTime; + $newConfig->contentTypes = $config->contentTypes; + $newConfig->devices = $config->devices; + $newConfig->friends = $config->friends; + $newConfig->createTime = time(); + $newConfig->updateTime = time(); + $newConfig->save(); + } + break; + case self::TYPE_MOMENTS_SYNC: + $config = WorkbenchMomentsSync::where('workbenchId', $id)->find(); + if ($config) { + $newConfig = new WorkbenchMomentsSync; + $newConfig->workbenchId = $newWorkbench->id; + $newConfig->syncInterval = $config->syncInterval; + $newConfig->syncCount = $config->syncCount; + $newConfig->syncType = $config->syncType; + $newConfig->startTime = $config->startTime; + $newConfig->endTime = $config->endTime; + $newConfig->accountType = $config->accountType; + $newConfig->devices = $config->devices; + $newConfig->contentLibraries = $config->contentLibraries; + $newConfig->createTime = time(); + $newConfig->updateTime = time(); + $newConfig->save(); + } + break; + case self::TYPE_GROUP_PUSH: + $config = WorkbenchGroupPush::where('workbenchId', $id)->find(); + if ($config) { + $newConfig = new WorkbenchGroupPush; + $newConfig->workbenchId = $newWorkbench->id; + $newConfig->pushType = $config->pushType; + $newConfig->targetType = isset($config->targetType) ? $config->targetType : 1; // 默认1=群推送 + $newConfig->startTime = $config->startTime; + $newConfig->endTime = $config->endTime; + $newConfig->maxPerDay = $config->maxPerDay; + $newConfig->pushOrder = $config->pushOrder; + $newConfig->isLoop = $config->isLoop; + $newConfig->status = $config->status; + $newConfig->groups = $config->groups; + $newConfig->friends = $config->friends; + $newConfig->contentLibraries = $config->contentLibraries; + $newConfig->trafficPools = property_exists($config, 'trafficPools') ? $config->trafficPools : json_encode([], JSON_UNESCAPED_UNICODE); + $newConfig->socialMediaId = $config->socialMediaId; + $newConfig->promotionSiteId = $config->promotionSiteId; + $newConfig->ownerWechatIds = $config->ownerWechatIds; + $newConfig->createTime = time(); + $newConfig->updateTime = time(); + $newConfig->save(); + } + break; + case self::TYPE_GROUP_CREATE: + $config = WorkbenchGroupCreate::where('workbenchId', $id)->find(); + if ($config) { + $newConfig = new WorkbenchGroupCreate; + $newConfig->workbenchId = $newWorkbench->id; + $newConfig->devices = $config->devices; + $newConfig->startTime = $config->startTime; + $newConfig->endTime = $config->endTime; + $newConfig->groupSizeMin = $config->groupSizeMin; + $newConfig->groupSizeMax = $config->groupSizeMax; + $newConfig->maxGroupsPerDay = $config->maxGroupsPerDay; + $newConfig->groupNameTemplate = $config->groupNameTemplate; + $newConfig->groupDescription = $config->groupDescription; + $newConfig->poolGroups = $config->poolGroups; + $newConfig->wechatGroups = $config->wechatGroups; + $newConfig->admins = $config->admins ?? json_encode([], JSON_UNESCAPED_UNICODE); + $newConfig->createTime = time(); + $newConfig->updateTime = time(); + $newConfig->save(); + } + break; + case self::TYPE_IMPORT_CONTACT: //联系人导入 + $config = WorkbenchImportContact::where('workbenchId', $id)->find(); + if ($config) { + $newConfig = new WorkbenchImportContact; + $newConfig->workbenchId = $newWorkbench->id; + $newConfig->devices = $config->devices; + $newConfig->pools = $config->pools; + $newConfig->num = $config->num; + $newConfig->clearContact = $config->clearContact; + $newConfig->remark = $config->remark; + $newConfig->startTime = $config->startTime; + $newConfig->endTime = $config->endTime; + $newConfig->createTime = time(); + $newConfig->save(); + } + break; + } + + Db::commit(); + return json(['code' => 200, 'msg' => '拷贝成功', 'data' => ['id' => $newWorkbench->id]]); + } catch (\Exception $e) { + Db::rollback(); + return json(['code' => 500, 'msg' => '拷贝失败:' . $e->getMessage()]); + } + } + + /** + * 获取点赞记录列表 + * @return \think\response\Json + */ + public function getLikeRecords() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchAutoLikeController(); + $controller->request = $this->request; + return $controller->getLikeRecords(); + } + + /** + * 获取朋友圈发布记录列表 + * @return \think\response\Json + */ + public function getMomentsRecords() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchMomentsController(); + $controller->request = $this->request; + return $controller->getMomentsRecords(); + } + + /** + * 获取朋友圈发布统计 + * @return \think\response\Json + */ + public function getMomentsStats() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchMomentsController(); + $controller->request = $this->request; + return $controller->getMomentsStats(); + } + + /** + * 获取流量分发记录列表 + * @return \think\response\Json + */ + public function getTrafficDistributionRecords() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchTrafficController(); + $controller->request = $this->request; + return $controller->getTrafficDistributionRecords(); + } + + /** + * 获取流量分发统计 + * @return \think\response\Json + */ + public function getTrafficDistributionStats() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchTrafficController(); + $controller->request = $this->request; + return $controller->getTrafficDistributionStats(); + } + + /** + * 获取流量分发详情 + * @return \think\response\Json + */ + public function getTrafficDistributionDetail() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchTrafficController(); + $controller->request = $this->request; + return $controller->getTrafficDistributionDetail(); + } + + /** + * 创建流量分发计划 + * @return \think\response\Json + */ + public function createTrafficPlan() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchTrafficController(); + $controller->request = $this->request; + return $controller->createTrafficPlan(); + } + + /** + * 获取流量列表 + * @return \think\response\Json + */ + public function getTrafficList() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchTrafficController(); + $controller->request = $this->request; + return $controller->getTrafficList(); + } + + /** + * 获取所有微信好友标签及数量统计 + * @return \think\response\Json + */ + public function getDeviceLabels() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController(); + $controller->request = $this->request; + return $controller->getDeviceLabels(); + } + + /** + * 获取群列表 + * @return \think\response\Json + */ + public function getGroupList() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController(); + $controller->request = $this->request; + return $controller->getGroupList(); + } + + /** + * 获取流量池列表 + * @return \think\response\Json + */ + public function getTrafficPoolList() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController(); + $controller->request = $this->request; + return $controller->getTrafficPoolList(); + } + + /** + * 获取账号列表 + * @return \think\response\Json + */ + public function getAccountList() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController(); + $controller->request = $this->request; + return $controller->getAccountList(); + } + + /** + * 获取京东联盟导购媒体 + * @return \think\response\Json + */ + public function getJdSocialMedia() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController(); + $controller->request = $this->request; + return $controller->getJdSocialMedia(); + } + + /** + * 获取京东联盟广告位 + * @return \think\response\Json + */ + public function getJdPromotionSite() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController(); + $controller->request = $this->request; + return $controller->getJdPromotionSite(); + } + + /** + * 京东转链-京推推 + * @param string $content + * @param string $positionid + * @return string + */ + public function changeLink($content = '', $positionid = '') + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchHelperController(); + $controller->request = $this->request; + return $controller->changeLink($content, $positionid); + } + + /** + * 规范化客服微信ID列表 + * @param mixed $ownerWechatIds + * @param WorkbenchGroupPush|null $originalConfig + * @return array + * @throws \Exception + */ + private function normalizeOwnerWechatIds($ownerWechatIds, WorkbenchGroupPush $originalConfig = null): array + { + if ($ownerWechatIds === null) { + $existing = $originalConfig ? $this->decodeJsonArray($originalConfig->ownerWechatIds ?? []) : []; + if (empty($existing)) { + throw new \Exception('请至少选择一个客服微信'); + } + return $existing; + } + + if (!is_array($ownerWechatIds)) { + throw new \Exception('客服参数格式错误'); + } + + $normalized = $this->extractIdList($ownerWechatIds, '客服参数格式错误'); + if (empty($normalized)) { + throw new \Exception('请至少选择一个客服微信'); + } + return $normalized; + } + + /** + * 构建群推送配置数据 + * @param array $param + * @param array $ownerWechatIds + * @param WorkbenchGroupPush|null $originalConfig + * @return array + * @throws \Exception + */ + private function prepareGroupPushData(array $param, array $ownerWechatIds, WorkbenchGroupPush $originalConfig = null): array + { + $targetTypeDefault = $originalConfig ? intval($originalConfig->targetType) : 1; + $targetType = intval($this->getParamValue($param, 'targetType', $targetTypeDefault)) ?: 1; + + $groupPushSubTypeDefault = $originalConfig ? intval($originalConfig->groupPushSubType) : 1; + $groupPushSubType = intval($this->getParamValue($param, 'groupPushSubType', $groupPushSubTypeDefault)) ?: 1; + if (!in_array($groupPushSubType, [1, 2], true)) { + $groupPushSubType = 1; + } + + $data = [ + 'pushType' => $this->toBoolInt($this->getParamValue($param, 'pushType', $originalConfig->pushType ?? 0)), + 'targetType' => $targetType, + 'startTime' => $this->getParamValue($param, 'startTime', $originalConfig->startTime ?? ''), + 'endTime' => $this->getParamValue($param, 'endTime', $originalConfig->endTime ?? ''), + 'maxPerDay' => intval($this->getParamValue($param, 'maxPerDay', $originalConfig->maxPerDay ?? 0)), + 'pushOrder' => $this->getParamValue($param, 'pushOrder', $originalConfig->pushOrder ?? 1), + 'groupPushSubType' => $groupPushSubType, + 'status' => $this->toBoolInt($this->getParamValue($param, 'status', $originalConfig->status ?? 0)), + 'socialMediaId' => $this->getParamValue($param, 'socialMediaId', $originalConfig->socialMediaId ?? ''), + 'promotionSiteId' => $this->getParamValue($param, 'promotionSiteId', $originalConfig->promotionSiteId ?? ''), + 'friendIntervalMin' => intval($this->getParamValue($param, 'friendIntervalMin', $originalConfig->friendIntervalMin ?? 10)), + 'friendIntervalMax' => intval($this->getParamValue($param, 'friendIntervalMax', $originalConfig->friendIntervalMax ?? 20)), + 'messageIntervalMin' => intval($this->getParamValue($param, 'messageIntervalMin', $originalConfig->messageIntervalMin ?? 1)), + 'messageIntervalMax' => intval($this->getParamValue($param, 'messageIntervalMax', $originalConfig->messageIntervalMax ?? 12)), + 'isRandomTemplate' => $this->toBoolInt($this->getParamValue($param, 'isRandomTemplate', $originalConfig->isRandomTemplate ?? 0)), + 'ownerWechatIds' => json_encode($ownerWechatIds, JSON_UNESCAPED_UNICODE), + ]; + + if ($data['friendIntervalMin'] > $data['friendIntervalMax']) { + throw new \Exception('目标间最小间隔不能大于最大间隔'); + } + if ($data['messageIntervalMin'] > $data['messageIntervalMax']) { + throw new \Exception('消息间最小间隔不能大于最大间隔'); + } + + $contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : []; + $contentGroupsParam = $this->getParamValue($param, 'contentGroups', null); + $contentGroups = $contentGroupsParam !== null + ? $this->extractIdList($contentGroupsParam, '内容库参数格式错误') + : $contentGroupsExisting; + $data['contentLibraries'] = json_encode($contentGroups, JSON_UNESCAPED_UNICODE); + + $postPushTagsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->postPushTags ?? []) : []; + $postPushTagsParam = $this->getParamValue($param, 'postPushTags', null); + $postPushTags = $postPushTagsParam !== null + ? $this->extractIdList($postPushTagsParam, '推送标签参数格式错误') + : $postPushTagsExisting; + $data['postPushTags'] = json_encode($postPushTags, JSON_UNESCAPED_UNICODE); + + if ($targetType === 1) { + $data['isLoop'] = $this->toBoolInt($this->getParamValue($param, 'isLoop', $originalConfig->isLoop ?? 0)); + + $groupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->groups ?? []) : []; + $wechatGroups = array_key_exists('wechatGroups', $param) + ? $this->extractIdList($param['wechatGroups'], '群参数格式错误') + : $groupsExisting; + if (empty($wechatGroups)) { + throw new \Exception('群推送必须选择微信群'); + } + $data['groups'] = json_encode($wechatGroups, JSON_UNESCAPED_UNICODE); + $data['friends'] = json_encode([], JSON_UNESCAPED_UNICODE); + $data['trafficPools'] = json_encode([], JSON_UNESCAPED_UNICODE); + + if ($groupPushSubType === 2) { + $announcementContent = $this->getParamValue($param, 'announcementContent', $originalConfig->announcementContent ?? ''); + if (empty($announcementContent)) { + throw new \Exception('群公告必须输入公告内容'); + } + $enableAiRewrite = $this->toBoolInt($this->getParamValue($param, 'enableAiRewrite', $originalConfig->enableAiRewrite ?? 0)); + $aiRewritePrompt = trim((string)$this->getParamValue($param, 'aiRewritePrompt', $originalConfig->aiRewritePrompt ?? '')); + if ($enableAiRewrite === 1 && $aiRewritePrompt === '') { + throw new \Exception('启用AI智能话术改写时,必须输入改写提示词'); + } + $data['announcementContent'] = $announcementContent; + $data['enableAiRewrite'] = $enableAiRewrite; + $data['aiRewritePrompt'] = $aiRewritePrompt; + } else { + $data['groupPushSubType'] = 1; + $data['announcementContent'] = ''; + $data['enableAiRewrite'] = 0; + $data['aiRewritePrompt'] = ''; + } + } else { + $data['isLoop'] = 0; + $friendsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->friends ?? []) : []; + $trafficPoolsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->trafficPools ?? []) : []; + + $friendTargets = array_key_exists('wechatFriends', $param) + ? $this->extractIdList($param['wechatFriends'], '好友参数格式错误') + : $friendsExisting; + $trafficPools = array_key_exists('trafficPools', $param) + ? $this->extractIdList($param['trafficPools'], '流量池参数格式错误') + : $trafficPoolsExisting; + + if (empty($friendTargets) && empty($trafficPools)) { + throw new \Exception('好友推送需至少选择好友或流量池'); + } + + $data['friends'] = json_encode($friendTargets, JSON_UNESCAPED_UNICODE); + $data['trafficPools'] = json_encode($trafficPools, JSON_UNESCAPED_UNICODE); + $data['groups'] = json_encode([], JSON_UNESCAPED_UNICODE); + $data['groupPushSubType'] = 1; + $data['announcementContent'] = ''; + $data['enableAiRewrite'] = 0; + $data['aiRewritePrompt'] = ''; + } + + return $data; + } + + /** + * 获取参数值,若不存在则返回默认值 + * @param array $param + * @param string $key + * @param mixed $default + * @return mixed + */ + private function getParamValue(array $param, string $key, $default) + { + return array_key_exists($key, $param) ? $param[$key] : $default; + } + + /** + * 将值转换为整型布尔 + * @param mixed $value + * @return int + */ + private function toBoolInt($value): int + { + return empty($value) ? 0 : 1; + } + + /** + * 从参数中提取ID列表 + * @param mixed $items + * @param string $errorMessage + * @return array + * @throws \Exception + */ + private function extractIdList($items, string $errorMessage = '参数格式错误'): array + { + if (!is_array($items)) { + throw new \Exception($errorMessage); + } + + $ids = []; + foreach ($items as $item) { + if (is_array($item) && isset($item['id'])) { + $item = $item['id']; + } + if ($item === '' || $item === null) { + continue; + } + $ids[] = $item; + } + + return array_values(array_unique($ids)); + } + + /** + * 解码JSON数组 + * @param mixed $value + * @return array + */ + private function decodeJsonArray($value): array + { + if (empty($value)) { + return []; + } + if (is_array($value)) { + return $value; + } + $decoded = json_decode($value, true); + return is_array($decoded) ? $decoded : []; + } + + /** + * 验证内容是否包含链接 + * @param string $content 要检测的内容 + * @return bool + */ + private function containsLink($content) + { + // 定义各种链接的正则表达式模式 + $patterns = [ + // HTTP/HTTPS链接 + '/https?:\/\/[^\s]+/i', + // 京东商品链接 + '/item\.jd\.com\/\d+/i', + // 京东短链接 + '/u\.jd\.com\/[a-zA-Z0-9]+/i', + // 淘宝商品链接 + '/item\.taobao\.com\/item\.htm\?id=\d+/i', + // 天猫商品链接 + '/detail\.tmall\.com\/item\.htm\?id=\d+/i', + // 淘宝短链接 + '/m\.tb\.cn\/[a-zA-Z0-9]+/i', + // 拼多多链接 + '/mobile\.yangkeduo\.com\/goods\.html\?goods_id=\d+/i', + // 苏宁易购链接 + '/product\.suning\.com\/\d+\/\d+\.html/i', + // 通用域名模式(包含常见电商域名) + '/(?:jd|taobao|tmall|yangkeduo|suning|amazon|dangdang)\.com[^\s]*/i', + // 通用短链接模式 + '/[a-zA-Z0-9-]+\.[a-zA-Z]{2,}\/[a-zA-Z0-9\-._~:\/?#\[\]@!$&\'()*+,;=]+/i' + ]; + + // 遍历所有模式进行匹配 + foreach ($patterns as $pattern) { + if (preg_match($pattern, $content)) { + return true; + } + } + + return false; + } + + + /** + * 获取通讯录导入记录列表 + * @return \think\response\Json + */ + public function getImportContact() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $workbenchId = $this->request->param('workbenchId', 0); + + $where = [ + ['wici.workbenchId', '=', $workbenchId] + ]; + + // 查询发布记录 + $list = Db::name('workbench_import_contact_item')->alias('wici') + ->join('traffic_pool tp', 'tp.id = wici.poolId', 'left') + ->join('traffic_source tc', 'tc.identifier = tp.identifier', 'left') + ->join('wechat_account wa', 'wa.wechatId = tp.wechatId', 'left') + ->field([ + 'wici.id', + 'wici.workbenchId', + 'wici.createTime', + 'tp.identifier', + 'tp.mobile', + 'tp.wechatId', + 'tc.name', + 'wa.nickName', + 'wa.avatar', + 'wa.alias', + ]) + ->where($where) + ->order('tc.name DESC,wici.createTime DESC') + ->group('tp.identifier') + ->page($page, $limit) + ->select(); + + foreach ($list as &$item) { + $item['createTime'] = date('Y-m-d H:i:s', $item['createTime']); + } + + + // 获取总记录数 + $total = Db::name('workbench_import_contact_item')->alias('wici') + ->where($where) + ->count(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + ] + ]); + } + + /** + * 获取群发统计数据 + * @return \think\response\Json + */ + public function getGroupPushStats() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchGroupPushController(); + $controller->request = $this->request; + return $controller->getGroupPushStats(); + } + + /** + * 计算基础统计数据 + */ + private function calculateBasicStats($workbenchId, $where, $startTime, $endTime, $contentIds = null) + { + // 获取工作台配置,计算计划发送数 + // 如果 workbenchId 为空,则查询所有工作台的配置 + $configQuery = WorkbenchGroupPush::alias('wgp') + ->join('workbench w', 'w.id = wgp.workbenchId', 'left') + ->where('w.type', self::TYPE_GROUP_PUSH) + ->where('w.isDel', 0); + + if (!empty($workbenchId)) { + $configQuery->where('wgp.workbenchId', $workbenchId); + } else { + // 如果没有指定工作台ID,需要从 where 条件中获取 workbenchId 列表 + $workbenchIdCondition = null; + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') { + if ($condition[1] === 'in' && is_array($condition[2])) { + $workbenchIdCondition = $condition[2]; + break; + } elseif ($condition[1] === '=') { + $workbenchIdCondition = [$condition[2]]; + break; + } + } + } + if ($workbenchIdCondition) { + $configQuery->whereIn('wgp.workbenchId', $workbenchIdCondition); + } + } + + $configs = $configQuery->select(); + $targetType = 1; // 默认值 + if (!empty($configs)) { + // 如果只有一个配置,使用它的 targetType;如果有多个,默认使用1 + $targetType = intval($configs[0]->targetType ?? 1); + } + + // 计划发送数(根据配置计算) + $plannedSend = 0; + if (!empty($configs)) { + $days = ceil(($endTime - $startTime) / 86400); + foreach ($configs as $config) { + $maxPerDay = intval($config->maxPerDay ?? 0); + $configTargetType = intval($config->targetType ?? 1); + if ($configTargetType == 1) { + // 群推送:计划发送数 = 每日推送次数 * 天数 * 群数量 + $groups = $this->decodeJsonArray($config->groups ?? []); + $plannedSend += $maxPerDay * $days * count($groups); + } else { + // 好友推送:计划发送数 = 每日推送人数 * 天数 + $plannedSend += $maxPerDay * $days; + } + } + } + + // 构建查询条件 + $queryWhere = $where; + if ($contentIds !== null) { + $queryWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + + // 实际成功发送数(从推送记录表统计) + $successSend = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($queryWhere) + ->count(); + + // 触达率 = 成功发送数 / 计划发送数 + $reachRate = $plannedSend > 0 ? round(($successSend / $plannedSend) * 100, 1) : 0; + + // 获取发送记录列表,用于查询回复 + $sentItemIds = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($queryWhere) + ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId') + ->select(); + + // 回复统计(通过消息表查询) + $replyStats = $this->calculateReplyStats($sentItemIds, $targetType, $startTime, $endTime); + + // 链接点击统计 + $clickStats = $this->calculateClickStats($sentItemIds, $targetType, $startTime, $endTime); + + // 计算本月对比数据(简化处理,实际应该查询上个月同期数据) + $currentMonthStart = strtotime(date('Y-m-01 00:00:00')); + $lastMonthStart = strtotime(date('Y-m-01 00:00:00', strtotime('-1 month'))); + $lastMonthEnd = $currentMonthStart - 1; + + // 获取本月统计数据(避免递归调用) + $currentMonthWhere = [ + ['wgpi.createTime', '>=', $currentMonthStart] + ]; + // 复制 workbenchId 条件 + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') { + $currentMonthWhere[] = $condition; + break; + } + } + if ($contentIds !== null) { + $currentMonthWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + $currentMonthSend = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($currentMonthWhere) + ->count(); + + // 获取本月配置 + $currentMonthConfigQuery = WorkbenchGroupPush::alias('wgp') + ->join('workbench w', 'w.id = wgp.workbenchId', 'left') + ->where('w.type', self::TYPE_GROUP_PUSH) + ->where('w.isDel', 0); + if (!empty($workbenchId)) { + $currentMonthConfigQuery->where('wgp.workbenchId', $workbenchId); + } else { + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId' && $condition[1] === 'in') { + $currentMonthConfigQuery->whereIn('wgp.workbenchId', $condition[2]); + break; + } + } + } + $currentMonthConfigs = $currentMonthConfigQuery->select(); + $currentMonthPlanned = 0; + if (!empty($currentMonthConfigs)) { + $currentMonthDays = ceil(($endTime - $currentMonthStart) / 86400); + foreach ($currentMonthConfigs as $currentMonthConfig) { + $currentMonthMaxPerDay = intval($currentMonthConfig->maxPerDay ?? 0); + $currentMonthTargetType = intval($currentMonthConfig->targetType ?? 1); + if ($currentMonthTargetType == 1) { + $currentMonthGroups = $this->decodeJsonArray($currentMonthConfig->groups ?? []); + $currentMonthPlanned += $currentMonthMaxPerDay * $currentMonthDays * count($currentMonthGroups); + } else { + $currentMonthPlanned += $currentMonthMaxPerDay * $currentMonthDays; + } + } + } + $currentMonthReachRate = $currentMonthPlanned > 0 ? round(($currentMonthSend / $currentMonthPlanned) * 100, 1) : 0; + + // 获取上个月统计数据 + $lastMonthWhere = [ + ['wgpi.createTime', '>=', $lastMonthStart], + ['wgpi.createTime', '<=', $lastMonthEnd] + ]; + // 复制 workbenchId 条件 + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') { + $lastMonthWhere[] = $condition; + break; + } + } + if ($contentIds !== null) { + $lastMonthWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + $lastMonthSend = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($lastMonthWhere) + ->count(); + + // 获取上个月配置 + $lastMonthConfigQuery = WorkbenchGroupPush::alias('wgp') + ->join('workbench w', 'w.id = wgp.workbenchId', 'left') + ->where('w.type', self::TYPE_GROUP_PUSH) + ->where('w.isDel', 0); + if (!empty($workbenchId)) { + $lastMonthConfigQuery->where('wgp.workbenchId', $workbenchId); + } else { + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId' && $condition[1] === 'in') { + $lastMonthConfigQuery->whereIn('wgp.workbenchId', $condition[2]); + break; + } + } + } + $lastMonthConfigs = $lastMonthConfigQuery->select(); + + $lastMonthPlanned = 0; + if (!empty($lastMonthConfigs)) { + $lastMonthDays = ceil(($lastMonthEnd - $lastMonthStart) / 86400); + foreach ($lastMonthConfigs as $lastMonthConfig) { + $lastMonthMaxPerDay = intval($lastMonthConfig->maxPerDay ?? 0); + $lastMonthTargetType = intval($lastMonthConfig->targetType ?? 1); + if ($lastMonthTargetType == 1) { + $lastMonthGroups = $this->decodeJsonArray($lastMonthConfig->groups ?? []); + $lastMonthPlanned += $lastMonthMaxPerDay * $lastMonthDays * count($lastMonthGroups); + } else { + $lastMonthPlanned += $lastMonthMaxPerDay * $lastMonthDays; + } + } + } + $lastMonthReachRate = $lastMonthPlanned > 0 ? round(($lastMonthSend / $lastMonthPlanned) * 100, 1) : 0; + + // 获取上个月的回复和点击统计(简化处理) + $lastMonthSentItems = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($lastMonthWhere) + ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId') + ->select(); + $lastMonthReplyStats = $this->calculateReplyStats($lastMonthSentItems, $targetType, $lastMonthStart, $lastMonthEnd); + $lastMonthClickStats = $this->calculateClickStats($lastMonthSentItems, $targetType, $lastMonthStart, $lastMonthEnd); + + return [ + 'reachRate' => [ + 'value' => $reachRate, + 'trend' => round($reachRate - $lastMonthReachRate, 1), + 'unit' => '%', + 'description' => '成功发送/计划发送' + ], + 'replyRate' => [ + 'value' => $replyStats['replyRate'], + 'trend' => round($replyStats['replyRate'] - $lastMonthReplyStats['replyRate'], 1), + 'unit' => '%', + 'description' => '收到回复/成功发送' + ], + 'avgReplyTime' => [ + 'value' => $replyStats['avgReplyTime'], + 'trend' => round($lastMonthReplyStats['avgReplyTime'] - $replyStats['avgReplyTime'], 0), + 'unit' => '分钟', + 'description' => '从发送到回复的平均时长' + ], + 'clickRate' => [ + 'value' => $clickStats['clickRate'], + 'trend' => round($clickStats['clickRate'] - $lastMonthClickStats['clickRate'], 1), + 'unit' => '%', + 'description' => '点击链接/成功发送' + ], + 'plannedSend' => $plannedSend, + 'successSend' => $successSend, + 'replyCount' => $replyStats['replyCount'], + 'clickCount' => $clickStats['clickCount'] + ]; + } + + /** + * 计算回复统计 + */ + private function calculateReplyStats($sentItems, $targetType, $startTime, $endTime) + { + if (empty($sentItems)) { + return ['replyRate' => 0, 'avgReplyTime' => 0, 'replyCount' => 0]; + } + + $replyCount = 0; + $totalReplyTime = 0; + $replyTimes = []; + + foreach ($sentItems as $item) { + $itemArray = is_array($item) ? $item : (array)$item; + $sendTime = $itemArray['createTime'] ?? 0; + $accountId = $itemArray['wechatAccountId'] ?? 0; + + if ($targetType == 1) { + // 群推送:查找群内回复消息 + $groupId = $itemArray['groupId'] ?? 0; + $group = Db::name('wechat_group')->where('id', $groupId)->find(); + if ($group) { + $replyMsg = Db::table('s2_wechat_message') + ->where('wechatChatroomId', $group['chatroomId']) + ->where('wechatAccountId', $accountId) + ->where('isSend', 0) // 接收的消息 + ->where('wechatTime', '>', $sendTime) + ->where('wechatTime', '<=', $sendTime + 86400) // 24小时内回复 + ->order('wechatTime', 'asc') + ->find(); + + if ($replyMsg) { + $replyCount++; + $replyTime = $replyMsg['wechatTime'] - $sendTime; + $replyTimes[] = $replyTime; + $totalReplyTime += $replyTime; + } + } + } else { + // 好友推送:查找好友回复消息 + $friendId = $itemArray['friendId'] ?? 0; + $friend = Db::table('s2_wechat_friend')->where('id', $friendId)->find(); + if ($friend) { + $replyMsg = Db::table('s2_wechat_message') + ->where('wechatFriendId', $friendId) + ->where('wechatAccountId', $accountId) + ->where('isSend', 0) // 接收的消息 + ->where('wechatTime', '>', $sendTime) + ->where('wechatTime', '<=', $sendTime + 86400) // 24小时内回复 + ->order('wechatTime', 'asc') + ->find(); + + if ($replyMsg) { + $replyCount++; + $replyTime = $replyMsg['wechatTime'] - $sendTime; + $replyTimes[] = $replyTime; + $totalReplyTime += $replyTime; + } + } + } + } + + $successSend = count($sentItems); + $replyRate = $successSend > 0 ? round(($replyCount / $successSend) * 100, 1) : 0; + $avgReplyTime = $replyCount > 0 ? round(($totalReplyTime / $replyCount) / 60, 0) : 0; // 转换为分钟 + + return [ + 'replyRate' => $replyRate, + 'avgReplyTime' => $avgReplyTime, + 'replyCount' => $replyCount + ]; + } + + /** + * 计算链接点击统计 + */ + private function calculateClickStats($sentItems, $targetType, $startTime, $endTime) + { + if (empty($sentItems)) { + return ['clickRate' => 0, 'clickCount' => 0]; + } + + $clickCount = 0; + $linkContentIds = []; + + // 获取所有发送的内容ID + foreach ($sentItems as $item) { + $itemArray = is_array($item) ? $item : (array)$item; + $contentId = $itemArray['contentId'] ?? 0; + if ($contentId > 0) { + $linkContentIds[] = $contentId; + } + } + + if (empty($linkContentIds)) { + return ['clickRate' => 0, 'clickCount' => 0]; + } + + // 查询包含链接的内容 + $linkContents = Db::name('content_item') + ->whereIn('id', array_unique($linkContentIds)) + ->where('contentType', 2) // 链接类型 + ->column('id'); + + // 统计发送了链接内容的记录数 + $linkSendCount = 0; + foreach ($sentItems as $item) { + $itemArray = is_array($item) ? $item : (array)$item; + $contentId = $itemArray['contentId'] ?? 0; + if (in_array($contentId, $linkContents)) { + $linkSendCount++; + } + } + + // 简化处理:假设点击率基于链接消息的发送(实际应该从点击追踪系统获取) + // 这里可以根据业务需求调整,比如通过消息中的链接点击事件统计 + $clickCount = $linkSendCount; // 简化处理,实际需要真实的点击数据 + + $successSend = count($sentItems); + $clickRate = $successSend > 0 ? round(($clickCount / $successSend) * 100, 1) : 0; + + return [ + 'clickRate' => $clickRate, + 'clickCount' => $clickCount + ]; + } + + /** + * 获取话术组对比数据 + */ + private function getContentLibraryComparison($workbenchId, $where, $startTime, $endTime, $contentIds = null) + { + $queryWhere = $where; + if ($contentIds !== null) { + $queryWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + + $comparison = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->join('content_item ci', 'ci.id = wgpi.contentId', 'left') + ->join('content_library cl', 'cl.id = ci.libraryId', 'left') + ->where($queryWhere) + ->where('cl.id', '<>', null) + ->field([ + 'cl.id as libraryId', + 'cl.name as libraryName', + 'COUNT(DISTINCT wgpi.id) as pushCount' + ]) + ->group('cl.id, cl.name') + ->select(); + + $result = []; + foreach ($comparison as $item) { + $libraryId = $item['libraryId']; + $pushCount = intval($item['pushCount']); + + // 获取该内容库的详细统计 + $libraryContentIds = Db::name('content_item') + ->where('libraryId', $libraryId) + ->column('id'); + if (empty($libraryContentIds)) { + $libraryContentIds = [-1]; + } + + $libraryWhere = array_merge($where, [['wgpi.contentId', 'in', $libraryContentIds]]); + $librarySentItems = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($libraryWhere) + ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId') + ->select(); + + $config = WorkbenchGroupPush::where('workbenchId', $workbenchId)->find(); + $targetType = $config ? intval($config->targetType) : 1; + + $replyStats = $this->calculateReplyStats($librarySentItems, $targetType, $startTime, $endTime); + $clickStats = $this->calculateClickStats($librarySentItems, $targetType, $startTime, $endTime); + + // 计算转化率(简化处理,实际需要根据业务定义) + $conversionRate = $pushCount > 0 ? round(($replyStats['replyCount'] / $pushCount) * 100, 1) : 0; + + $result[] = [ + 'libraryId' => $libraryId, + 'libraryName' => $item['libraryName'], + 'pushCount' => $pushCount, + 'reachRate' => 100, // 简化处理,实际应该计算 + 'replyRate' => $replyStats['replyRate'], + 'clickRate' => $clickStats['clickRate'], + 'conversionRate' => $conversionRate, + 'avgReplyTime' => $replyStats['avgReplyTime'], + 'level' => $this->getPerformanceLevel($replyStats['replyRate'], $clickStats['clickRate'], $conversionRate) + ]; + } + + // 按回复率排序 + usort($result, function($a, $b) { + return $b['replyRate'] <=> $a['replyRate']; + }); + + return $result; + } + + /** + * 获取性能等级 + */ + private function getPerformanceLevel($replyRate, $clickRate, $conversionRate) + { + $score = ($replyRate * 0.4) + ($clickRate * 0.3) + ($conversionRate * 0.3); + + if ($score >= 40) { + return '优秀'; + } elseif ($score >= 25) { + return '良好'; + } elseif ($score >= 15) { + return '一般'; + } else { + return '待提升'; + } + } + + /** + * 获取时段分析数据 + */ + private function getTimePeriodAnalysis($workbenchId, $where, $startTime, $endTime, $contentIds = null) + { + $queryWhere = $where; + if ($contentIds !== null) { + $queryWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + + $analysis = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($queryWhere) + ->field([ + 'FROM_UNIXTIME(wgpi.createTime, "%H") as hour', + 'COUNT(*) as count' + ]) + ->group('hour') + ->order('hour', 'asc') + ->select(); + + $result = []; + foreach ($analysis as $item) { + $result[] = [ + 'hour' => intval($item['hour']), + 'count' => intval($item['count']) + ]; + } + + return $result; + } + + /** + * 获取互动深度数据 + */ + private function getInteractionDepth($workbenchId, $where, $startTime, $endTime, $contentIds = null) + { + // 简化处理,实际需要更复杂的统计逻辑 + return [ + 'singleReply' => 0, // 单次回复 + 'multipleReply' => 0, // 多次回复 + 'deepInteraction' => 0 // 深度互动 + ]; + } + + /** + * 获取推送历史记录列表 + * @return \think\response\Json + */ + public function getGroupPushHistory() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchGroupPushController(); + $controller->request = $this->request; + return $controller->getGroupPushHistory(); + } + + /** + * 获取状态文本 + * @param string $status 状态码 + * @return string 状态文本 + */ + private function getStatusText($status) + { + $statusMap = [ + 'success' => '已完成', + 'partial' => '进行中', + 'pending' => '进行中', + 'failed' => '失败' + ]; + return $statusMap[$status] ?? '未知'; + } + + /** + * 获取已创建的群列表(自动建群) + * @return \think\response\Json + */ + public function getCreatedGroupsList() + { + $workbenchId = $this->request->param('workbenchId', 0); + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 100); + $keyword = $this->request->param('keyword', ''); + + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '工作台ID不能为空']); + } + + // 验证工作台权限 + $where = [ + ['id', '=', $workbenchId], + ['companyId', '=', $this->request->userInfo['companyId']], + ['type', '=', self::TYPE_GROUP_CREATE], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + $workbench = Workbench::where($where)->find(); + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在或无权限']); + } + + // 获取已创建的群ID列表(状态为成功且groupId不为空) + $groupIds = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('status', 2) // STATUS_SUCCESS = 2 + ->where('groupId', '<>', null) + ->group('groupId') + ->column('groupId'); + + if (empty($groupIds)) { + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => [], + 'total' => 0, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + + // 查询群组详细信息(从s2_wechat_chatroom表查询) + $query = Db::table('s2_wechat_chatroom')->alias('wc') + ->join('wechat_account wa', 'wa.wechatId = wc.wechatAccountWechatId', 'left') + ->where('wc.id', 'in', $groupIds) + ->where('wc.isDeleted', 0); + + // 关键字搜索 + if (!empty($keyword)) { + $query->where(function ($q) use ($keyword) { + $q->where('wc.nickname', 'like', '%' . $keyword . '%') + ->whereOr('wc.chatroomId', 'like', '%' . $keyword . '%') + ->whereOr('wa.nickName', 'like', '%' . $keyword . '%'); + }); + } + + $total = $query->count(); + $list = $query->field('wc.id,wc.nickname as groupName,wc.chatroomId,wc.chatroomAvatar as groupAvatar,wc.wechatAccountWechatId as ownerWechatId,wc.createTime,wc.chatroomOwnerNickname as ownerNickname,wc.chatroomOwnerAvatar as ownerAvatar,wa.alias as ownerAlias') + ->order('wc.createTime', 'desc') + ->page($page, $limit) + ->select(); + + // 统计每个群的成员数量和成员信息 + foreach ($list as &$item) { + // 统计该群的成员数量(从workbench_group_create_item表统计) + $memberCount = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('groupId', $item['id']) + ->where('status', 'in', [2, 4]) // 创建成功和管理员好友已拉入 + ->count(); + + // 获取成员列表(用于显示成员头像) + $memberList = Db::name('workbench_group_create_item')->alias('wgci') + ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wgci.friendId', 'left') + ->where('wgci.workbenchId', $workbenchId) + ->where('wgci.groupId', $item['id']) + ->where('wgci.status', 'in', [2, 4]) + ->field('wf.avatar, wf.wechatId, wf.nickname') + ->order('wgci.createTime', 'asc') + ->limit(10) // 最多显示10个成员头像 + ->select(); + + // 格式化成员头像列表 + $memberAvatars = []; + foreach ($memberList as $member) { + if (!empty($member['avatar'])) { + $memberAvatars[] = [ + 'avatar' => $member['avatar'], + 'wechatId' => $member['wechatId'] ?? '', + 'nickname' => $member['nickname'] ?? '' + ]; + } + } + + // 计算剩余成员数(用于显示"+XX") + $remainingCount = $memberCount > count($memberAvatars) ? $memberCount - count($memberAvatars) : 0; + + // 格式化返回数据 + $item['memberCount'] = $memberCount; + $item['memberCountText'] = $memberCount . '人'; // 格式化为"XX人" + $item['createTime'] = !empty($item['createTime']) ? date('Y-m-d', $item['createTime']) : ''; // 格式化为"YYYY-MM-DD" + $item['memberAvatars'] = $memberAvatars; // 成员头像列表(最多10个) + $item['remainingCount'] = $remainingCount; // 剩余成员数(用于显示"+XX") + + // 保留原有字段,但调整格式 + $item['groupName'] = $item['groupName'] ?? ''; + $item['groupAvatar'] = $item['groupAvatar'] ?? ''; + } + unset($item); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + + /** + * 获取已创建群的详情(自动建群) + * @return \think\response\Json + */ + public function getCreatedGroupDetail() + { + $workbenchId = $this->request->param('workbenchId', 0); + $groupId = $this->request->param('groupId', 0); + + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '工作台ID不能为空']); + } + + if (empty($groupId)) { + return json(['code' => 400, 'msg' => '群ID不能为空']); + } + + // 验证工作台权限 + $where = [ + ['id', '=', $workbenchId], + ['companyId', '=', $this->request->userInfo['companyId']], + ['type', '=', self::TYPE_GROUP_CREATE], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + $workbench = Workbench::where($where)->find(); + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在或无权限']); + } + + // 验证该群是否属于该工作台 + $groupItem = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('groupId', $groupId) + ->where('status', 2) // STATUS_SUCCESS = 2 + ->find(); + + if (empty($groupItem)) { + return json(['code' => 404, 'msg' => '群不存在或不属于该工作台']); + } + + // 查询群基本信息(从s2_wechat_chatroom表查询) + $group = Db::table('s2_wechat_chatroom')->alias('wc') + ->join('wechat_account wa', 'wa.wechatId = wc.wechatAccountWechatId', 'left') + ->where('wc.id', $groupId) + ->where('wc.isDeleted', 0) + ->field('wc.id,wc.nickname as groupName,wc.chatroomId,wc.chatroomAvatar as groupAvatar,wc.wechatAccountWechatId as ownerWechatId,wc.createTime,wc.chatroomOwnerNickname as ownerNickname,wc.chatroomOwnerAvatar as ownerAvatar,wa.alias as ownerAlias,wc.announce') + ->find(); + + if (empty($group)) { + return json(['code' => 404, 'msg' => '群不存在']); + } + + // 获取chatroomId + $chatroomId = $group['chatroomId'] ?? ''; + if (empty($chatroomId)) { + return json(['code' => 400, 'msg' => '群聊ID不存在']); + } + + // 从s2_wechat_chatroom_member表查询所有成员列表(不限制数量) + $memberList = Db::table('s2_wechat_chatroom_member')->alias('wcm') + ->join(['s2_wechat_friend' => 'wf'], 'wf.wechatId = wcm.wechatId', 'left') + ->where('wcm.chatroomId', $chatroomId) + ->field('wcm.wechatId,wcm.nickname as memberNickname,wcm.avatar as memberAvatar,wcm.conRemark as memberRemark,wcm.alias as memberAlias,wcm.createTime as joinTime,wcm.updateTime,wf.id as friendId,wf.nickname as friendNickname,wf.avatar as friendAvatar') + ->order('wcm.createTime', 'asc') + ->select(); + + // 去重:按wechatId去重,保留第一条记录 + $memberMap = []; + foreach ($memberList as $member) { + $wechatId = $member['wechatId'] ?? ''; + if (!empty($wechatId) && !isset($memberMap[$wechatId])) { + $memberMap[$wechatId] = $member; + } + } + $memberList = array_values($memberMap); // 重新索引数组 + + // 获取在群中的成员wechatId列表(用于判断是否已退群) + $inGroupWechatIds = array_column($memberList, 'wechatId'); + $inGroupWechatIds = array_filter($inGroupWechatIds); // 过滤空值 + + // 获取通过自动建群加入的成员信息(用于判断入群状态和已退群成员) + $autoJoinMemberList = Db::name('workbench_group_create_item') + ->alias('wgci') + ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wgci.friendId', 'left') + ->where('wgci.workbenchId', $workbenchId) + ->where('wgci.groupId', $groupId) + ->where('wgci.status', 'in', [2, 4]) // 创建成功和管理员好友已拉入 + ->field('wf.wechatId,wf.id as friendId,wf.nickname as friendNickname,wf.avatar as friendAvatar,wgci.createTime as autoJoinTime') + ->select(); + + // 去重:按wechatId去重,保留第一条记录 + $autoJoinMemberMap = []; + foreach ($autoJoinMemberList as $autoMember) { + $wechatId = $autoMember['wechatId'] ?? ''; + if (!empty($wechatId) && !isset($autoJoinMemberMap[$wechatId])) { + $autoJoinMemberMap[$wechatId] = $autoMember; + } + } + $autoJoinMemberList = array_values($autoJoinMemberMap); // 重新索引数组 + + // 统计成员总数(包括在群中的成员和已退群的成员) + // 先统计在群中的成员数 + $inGroupCount = count($memberList); + + // 统计已退群的成员数(在自动建群记录中但不在群中的成员) + $quitCount = 0; + if (!empty($autoJoinMemberList)) { + foreach ($autoJoinMemberList as $autoMember) { + $wechatId = $autoMember['wechatId'] ?? ''; + if (!empty($wechatId) && !in_array($wechatId, $inGroupWechatIds)) { + $quitCount++; + } + } + } + + // 总成员数 = 在群中的成员数 + 已退群的成员数 + $memberCount = $inGroupCount + $quitCount; + + // 获取自动建群加入的成员wechatId列表(用于判断入群状态) + $autoJoinWechatIds = array_column($autoJoinMemberList, 'wechatId'); + $autoJoinWechatIds = array_filter($autoJoinWechatIds); // 过滤空值 + + // 格式化在群中的成员列表 + $members = []; + $addedWechatIds = []; // 用于记录已添加的成员wechatId,避免重复 + $ownerWechatId = $group['ownerWechatId'] ?? ''; + foreach ($memberList as $member) { + $wechatId = $member['wechatId'] ?? ''; + + // 跳过空wechatId和已添加的成员 + if (empty($wechatId) || in_array($wechatId, $addedWechatIds)) { + continue; + } + + // 标记为已添加 + $addedWechatIds[] = $wechatId; + + // 根据wechatId判断是否为群主 + $isOwner = (!empty($ownerWechatId) && $wechatId == $ownerWechatId) ? 1 : 0; + + // 判断入群状态:如果在自动建群记录中,说明是通过自动建群加入的;否则是其他方式加入的 + $joinStatus = in_array($wechatId, $autoJoinWechatIds) ? 'auto' : 'manual'; + + // 判断是否已退群:如果成员在s2_wechat_chatroom_member表中存在,说明在群中;否则已退群 + // 由于我们已经从s2_wechat_chatroom_member表查询,所以这里都是"在群中"状态 + $isQuit = 0; // 0=在群中,1=已退群 + + // 优先使用friend表的昵称和头像,如果没有则使用member表的 + $nickname = !empty($member['friendNickname']) ? $member['friendNickname'] : ($member['memberNickname'] ?? ''); + $avatar = !empty($member['friendAvatar']) ? $member['friendAvatar'] : ($member['memberAvatar'] ?? ''); + + $members[] = [ + 'friendId' => $member['friendId'] ?? 0, + 'wechatId' => $wechatId, + 'nickname' => $nickname, + 'avatar' => $avatar, + 'alias' => $member['memberAlias'] ?? '', + 'remark' => $member['memberRemark'] ?? '', + 'isOwner' => $isOwner, // 标记群主 + 'joinStatus' => $joinStatus, // 入群状态:auto=自动建群加入,manual=其他方式加入 + 'isQuit' => $isQuit, // 是否已退群:0=在群中,1=已退群 + 'joinTime' => !empty($member['joinTime']) ? date('Y-m-d H:i:s', $member['joinTime']) : '', // 入群时间 + ]; + } + + // 添加已退群的成员(在自动建群记录中但不在群中的成员) + foreach ($autoJoinMemberList as $autoMember) { + $wechatId = $autoMember['wechatId'] ?? ''; + + // 跳过空wechatId、已在群中的成员和已添加的成员 + if (empty($wechatId) || in_array($wechatId, $inGroupWechatIds) || in_array($wechatId, $addedWechatIds)) { + continue; + } + + // 标记为已添加 + $addedWechatIds[] = $wechatId; + + // 根据wechatId判断是否为群主 + $isOwner = (!empty($ownerWechatId) && $wechatId == $ownerWechatId) ? 1 : 0; + + $members[] = [ + 'friendId' => $autoMember['friendId'] ?? 0, + 'wechatId' => $wechatId, + 'nickname' => $autoMember['friendNickname'] ?? '', + 'avatar' => $autoMember['friendAvatar'] ?? '', + 'alias' => '', + 'remark' => '', + 'isOwner' => $isOwner, // 标记群主 + 'joinStatus' => 'auto', // 入群状态:auto=自动建群加入 + 'isQuit' => 1, // 是否已退群:1=已退群 + 'joinTime' => !empty($autoMember['autoJoinTime']) ? date('Y-m-d H:i:s', $autoMember['autoJoinTime']) : '', // 入群时间 + ]; + } + + // 将群主排在第一位 + usort($members, function($a, $b) { + if ($a['isOwner'] == $b['isOwner']) { + return 0; + } + return $a['isOwner'] > $b['isOwner'] ? -1 : 1; + }); + + // 格式化返回数据 + $result = [ + 'id' => $group['id'], + 'groupName' => $group['groupName'] ?? '', + 'chatroomId' => $group['chatroomId'] ?? '', + 'groupAvatar' => $group['groupAvatar'] ?? '', + 'ownerWechatId' => $group['ownerWechatId'] ?? '', + 'ownerNickname' => $group['ownerNickname'] ?? '', + 'ownerAvatar' => $group['ownerAvatar'] ?? '', + 'ownerAlias' => $group['ownerAlias'] ?? '', + 'announce' => $group['announce'] ?? '', + 'createTime' => !empty($group['createTime']) ? date('Y-m-d H:i', $group['createTime']) : '', // 格式化为"YYYY-MM-DD HH:MM" + 'memberCount' => $memberCount, + 'memberCountText' => $memberCount . '人', // 格式化为"XX人" + 'workbenchName' => $workbench->name ?? '', // 任务名称(工作台名称) + 'members' => $members // 所有成员列表 + ]; + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => $result + ]); + } + + /** + * 同步群最新信息(包括群成员) + * @return \think\response\Json + */ + public function syncGroupInfo() + { + $workbenchId = $this->request->param('workbenchId', 0); + $groupId = $this->request->param('groupId', 0); + + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '工作台ID不能为空']); + } + + if (empty($groupId)) { + return json(['code' => 400, 'msg' => '群ID不能为空']); + } + + // 验证工作台权限 + $where = [ + ['id', '=', $workbenchId], + ['companyId', '=', $this->request->userInfo['companyId']], + ['type', '=', self::TYPE_GROUP_CREATE], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + $workbench = Workbench::where($where)->find(); + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在或无权限']); + } + + // 验证该群是否属于该工作台 + $groupItem = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('groupId', $groupId) + ->where('status', 2) // STATUS_SUCCESS = 2 + ->find(); + + if (empty($groupItem)) { + return json(['code' => 404, 'msg' => '群不存在或不属于该工作台']); + } + + // 查询群基本信息,获取chatroomId和wechatAccountWechatId + $group = Db::table('s2_wechat_chatroom') + ->where('id', $groupId) + ->where('isDeleted', 0) + ->field('id,chatroomId,wechatAccountWechatId') + ->find(); + + if (empty($group)) { + return json(['code' => 404, 'msg' => '群不存在']); + } + + $chatroomId = $group['chatroomId'] ?? ''; + $wechatAccountWechatId = $group['wechatAccountWechatId'] ?? ''; + + if (empty($chatroomId)) { + return json(['code' => 400, 'msg' => '群聊ID不存在']); + } + + try { + // 实例化WechatChatroomController + $chatroomController = new \app\api\controller\WechatChatroomController(); + + // 1. 同步群信息(调用getlist方法) + $syncData = [ + 'wechatChatroomId' => $chatroomId, // 使用chatroomId作为wechatChatroomId来指定要同步的群 + 'wechatAccountKeyword' => $wechatAccountWechatId, // 通过群主微信ID筛选 + 'isDeleted' => false, + 'pageIndex' => 1, + 'pageSize' => 100 // 获取足够多的数据 + ]; + $syncResult = $chatroomController->getlist($syncData, true, 0); // isInner = true + $syncResponse = json_decode($syncResult, true); + + if (empty($syncResponse['code']) || $syncResponse['code'] != 200) { + return json(['code' => 500, 'msg' => '同步群信息失败:' . ($syncResponse['msg'] ?? '未知错误')]); + } + + // 2. 同步群成员信息(调用listChatroomMember方法) + // wechatChatroomId 使用 s2_wechat_chatroom 表的 id(即groupId) + // chatroomId 使用群聊ID(chatroomId) + $wechatChatroomId = $groupId; // s2_wechat_chatroom表的id + $memberSyncResult = $chatroomController->listChatroomMember($wechatChatroomId, $chatroomId, true); // isInner = true + $memberSyncResponse = json_decode($memberSyncResult, true); + + if (empty($memberSyncResponse['code']) || $memberSyncResponse['code'] != 200) { + // 成员同步失败不影响整体结果,记录警告即可 + \think\facade\Log::warning("同步群成员失败。群ID: {$groupId}, 群聊ID: {$chatroomId}, 错误: " . ($memberSyncResponse['msg'] ?? '未知错误')); + } + + return json([ + 'code' => 200, + 'msg' => '同步成功', + 'data' => [ + 'groupId' => $groupId, + 'chatroomId' => $chatroomId, + 'groupInfoSynced' => true, + 'memberInfoSynced' => !empty($memberSyncResponse['code']) && $memberSyncResponse['code'] == 200 + ] + ]); + } catch (\Exception $e) { + \think\facade\Log::error("同步群信息异常。群ID: {$groupId}, 错误: " . $e->getMessage()); + return json(['code' => 500, 'msg' => '同步失败:' . $e->getMessage()]); + } + } + + /** + * 修改群名称、群公告 + * @return \think\response\Json + */ + public function modifyGroupInfo() + { + $workbenchId = $this->request->param('workbenchId', 0); + $groupId = $this->request->param('groupId', 0); + $chatroomName = $this->request->param('chatroomName', ''); + $announce = $this->request->param('announce', ''); + + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '工作台ID不能为空']); + } + + if (empty($groupId)) { + return json(['code' => 400, 'msg' => '群ID不能为空']); + } + + // 至少需要提供一个修改项 + if (empty($chatroomName) && empty($announce)) { + return json(['code' => 400, 'msg' => '请至少提供群名称或群公告中的一个参数']); + } + + // 查询群基本信息 + $group = Db::table('s2_wechat_chatroom') + ->where('id', $groupId) + ->where('isDeleted', 0) + ->field('id,chatroomId,wechatAccountWechatId,accountId,wechatAccountId') + ->find(); + + if (empty($group)) { + return json(['code' => 404, 'msg' => '群不存在']); + } + + $chatroomId = $group['id'] ?? ''; + + if (empty($chatroomId)) { + return json(['code' => 400, 'msg' => '群聊ID不存在']); + } + + try { + // 直接使用群表中的账号信息 + $executeAccountId = $group['accountId'] ?? 0; + $executeWechatAccountId = $group['wechatAccountId'] ?? 0; + $executeWechatId = $group['wechatAccountWechatId'] ?? ''; + + // 确保 wechatId 不为空 + if (empty($executeWechatId)) { + return json(['code' => 400, 'msg' => '无法获取微信账号ID']); + } + + // 调用 WebSocketController 修改群信息 + // 获取系统API账号信息(用于WebSocket连接) + $username = Env::get('api.username2', ''); + $password = Env::get('api.password2', ''); + + if (empty($username) || empty($password)) { + return json(['code' => 500, 'msg' => '系统API账号配置缺失']); + } + + // 获取系统账号ID + $systemAccountId = Db::name('users')->where('account', $username)->value('s2_accountId'); + if (empty($systemAccountId)) { + return json(['code' => 500, 'msg' => '未找到系统账号ID']); + } + + $webSocketController = new WebSocketController([ + 'userName' => $username, + 'password' => $password, + 'accountId' => $systemAccountId + ]); + // 构建修改参数 + $modifyData = [ + 'wechatChatroomId' => $chatroomId, + 'wechatAccountId' => $executeWechatAccountId, + ]; + if (!empty($chatroomName)) { + $modifyData['chatroomName'] = $chatroomName; + } + if (!empty($announce)) { + $modifyData['announce'] = $announce; + } + + + $modifyResult = $webSocketController->CmdChatroomModifyInfo($modifyData); + $modifyResponse = json_decode($modifyResult, true); + if (empty($modifyResponse['code']) || $modifyResponse['code'] != 200) { + return json(['code' => 500, 'msg' => '修改群信息失败:' . ($modifyResponse['msg'] ?? '未知错误')]); + } + + // 修改成功后更新数据库 + $updateData = [ + 'updateTime' => time() + ]; + + // 如果修改了群名称,更新数据库 + if (!empty($chatroomName)) { + $updateData['nickname'] = $chatroomName; + } + + // 如果修改了群公告,更新数据库 + if (!empty($announce)) { + $updateData['announce'] = $announce; + } + + // 更新数据库 + Db::table('s2_wechat_chatroom') + ->where('id', $groupId) + ->update($updateData); + + return json([ + 'code' => 200, + 'msg' => '修改成功', + 'data' => [ + 'groupId' => $groupId, + 'chatroomId' => $chatroomId, + 'chatroomName' => $chatroomName, + 'announce' => $announce + ] + ]); + } catch (\Exception $e) { + \think\facade\Log::error("修改群信息异常。群ID: {$groupId}, 错误: " . $e->getMessage()); + return json(['code' => 500, 'msg' => '修改失败:' . $e->getMessage()]); + } + } + + /** + * 转移群聊到指定账号 + * @param int $groupId 群ID(s2_wechat_chatroom表的id) + * @param string $chatroomId 群聊ID + * @param int $toAccountId 目标账号ID(s2_company_account表的id) + * @param int $toWechatAccountId 目标微信账号ID(s2_wechat_account表的id) + * @return array ['success' => bool, 'msg' => string] + */ + protected function transferChatroomToAccount($groupId, $chatroomId, $toAccountId, $toWechatAccountId) + { + try { + // 查询目标账号信息 + $targetAccount = Db::table('s2_company_account') + ->where('id', $toAccountId) + ->field('id,userName,realName,nickname') + ->find(); + + if (empty($targetAccount)) { + return ['success' => false, 'msg' => '目标账号不存在']; + } + + // 查询目标微信账号信息 + $targetWechatAccount = Db::table('s2_wechat_account') + ->where('id', $toWechatAccountId) + ->field('id,wechatId,deviceAccountId') + ->find(); + + if (empty($targetWechatAccount)) { + return ['success' => false, 'msg' => '目标微信账号不存在']; + } + + // 调用 AutomaticAssign 进行群聊转移 + $automaticAssign = new \app\api\controller\AutomaticAssign(); + + // 构建转移参数(通过 API 调用) + $transferData = [ + 'wechatChatroomId' => $chatroomId, // 使用群聊ID + 'toAccountId' => $toAccountId, + 'wechatAccountKeyword' => $targetWechatAccount['wechatId'] ?? '', + 'isDeleted' => false + ]; + + // 直接更新数据库(因为 API 可能不支持指定单个群聊ID转移) + // 更新 s2_wechat_chatroom 表的 accountId 和 wechatAccountId + Db::table('s2_wechat_chatroom') + ->where('id', $groupId) + ->update([ + 'accountId' => $toAccountId, + 'accountUserName' => $targetAccount['userName'] ?? '', + 'accountRealName' => $targetAccount['realName'] ?? '', + 'accountNickname' => $targetAccount['nickname'] ?? '', + 'wechatAccountId' => $toWechatAccountId, + 'wechatAccountWechatId' => $targetWechatAccount['wechatId'] ?? '', + 'updateTime' => time() + ]); + + return ['success' => true, 'msg' => '转移成功']; + } catch (\Exception $e) { + \think\facade\Log::error("转移群聊异常。群ID: {$groupId}, 目标账号ID: {$toAccountId}, 错误: " . $e->getMessage()); + return ['success' => false, 'msg' => $e->getMessage()]; + } + } + + +} \ No newline at end of file diff --git a/application/cunkebao/controller/workbench/WorkbenchGroupCreateController.php b/application/cunkebao/controller/workbench/WorkbenchGroupCreateController.php new file mode 100644 index 0000000..8057731 --- /dev/null +++ b/application/cunkebao/controller/workbench/WorkbenchGroupCreateController.php @@ -0,0 +1,3895 @@ +request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + // 获取登录用户信息 + $userInfo = request()->userInfo; + + // 获取请求参数 + $param = $this->request->post(); + + + // 根据业务默认值补全参数 + if ( + isset($param['type']) && + intval($param['type']) === self::TYPE_GROUP_PUSH + ) { + if (empty($param['startTime'])) { + $param['startTime'] = '09:00'; + } + if (empty($param['endTime'])) { + $param['endTime'] = '21:00'; + } + } + + // 验证数据 + $validate = new WorkbenchValidate; + if (!$validate->scene('create')->check($param)) { + return json(['code' => 400, 'msg' => $validate->getError()]); + } + + Db::startTrans(); + try { + // 创建工作台基本信息 + $workbench = new Workbench; + $workbench->name = $param['name']; + $workbench->type = $param['type']; + $workbench->status = !empty($param['status']) ? 1 : 0; + $workbench->autoStart = !empty($param['autoStart']) ? 1 : 0; + $workbench->userId = $userInfo['id']; + $workbench->companyId = $userInfo['companyId']; + $workbench->createTime = time(); + $workbench->updateTime = time(); + $workbench->save(); + + // 根据类型创建对应的配置 + switch ($param['type']) { + case self::TYPE_AUTO_LIKE: // 自动点赞 + $config = new WorkbenchAutoLike; + $config->workbenchId = $workbench->id; + $config->interval = $param['interval']; + $config->maxLikes = $param['maxLikes']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->contentTypes = json_encode($param['contentTypes']); + $config->devices = json_encode($param['deviceGroups']); + $config->friends = json_encode($param['wechatFriends']); + // $config->targetGroups = json_encode($param['targetGroups']); + // $config->tagOperator = $param['tagOperator']; + $config->friendMaxLikes = $param['friendMaxLikes']; + $config->friendTags = $param['friendTags']; + $config->enableFriendTags = $param['enableFriendTags']; + $config->createTime = time(); + $config->updateTime = time(); + $config->save(); + break; + case self::TYPE_MOMENTS_SYNC: // 朋友圈同步 + $config = new WorkbenchMomentsSync; + $config->workbenchId = $workbench->id; + $config->syncInterval = $param['syncInterval']; + $config->syncCount = $param['syncCount']; + $config->syncType = $param['syncType']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->accountType = $param['accountType']; + $config->devices = json_encode($param['deviceGroups']); + $config->contentLibraries = json_encode($param['contentGroups'] ?? []); + $config->createTime = time(); + $config->updateTime = time(); + $config->save(); + break; + case self::TYPE_GROUP_PUSH: // 群消息推送 + $ownerWechatIds = $this->normalizeOwnerWechatIds($param['ownerWechatIds'] ?? []); + $groupPushData = $this->prepareGroupPushData($param, $ownerWechatIds); + $groupPushData['workbenchId'] = $workbench->id; + $groupPushData['createTime'] = time(); + $groupPushData['updateTime'] = time(); + $config = new WorkbenchGroupPush; + $config->save($groupPushData); + break; + case self::TYPE_GROUP_CREATE: // 自动建群 + $config = new WorkbenchGroupCreate; + $config->workbenchId = $workbench->id; + $config->planType = !empty($param['planType']) ? $param['planType'] : 0; + $config->executorId = !empty($param['executorId']) ? $param['executorId'] : 0; + + $config->devices = json_encode($param['deviceGroups'] ?? [], JSON_UNESCAPED_UNICODE); + $config->startTime = $param['startTime'] ?? ''; + $config->endTime = $param['endTime'] ?? ''; + $config->groupSizeMin = intval($param['groupSizeMin'] ?? 3); + $config->groupSizeMax = intval($param['groupSizeMax'] ?? 38); + $config->maxGroupsPerDay = intval($param['maxGroupsPerDay'] ?? 20); + $config->groupNameTemplate = $param['groupNameTemplate'] ?? ''; + $config->groupDescription = $param['groupDescription'] ?? ''; + $config->poolGroups = json_encode($param['poolGroups'] ?? [], JSON_UNESCAPED_UNICODE); + $config->wechatGroups = json_encode($param['wechatGroups'] ?? [], JSON_UNESCAPED_UNICODE); + + // 处理群管理员:如果启用了群管理员且有指定管理员,则保存到admins字段 + $admins = []; + if (!empty($param['groupAdminEnabled']) && !empty($param['groupAdminWechatId'])) { + // 如果groupAdminWechatId是数组,取第一个;如果是单个值,直接使用 + $adminWechatId = is_array($param['groupAdminWechatId']) ? $param['groupAdminWechatId'][0] : $param['groupAdminWechatId']; + // 如果是好友ID,直接添加到admins;如果是wechatId,需要转换为好友ID + if (is_numeric($adminWechatId)) { + $admins[] = intval($adminWechatId); + } else { + // 如果是wechatId字符串,需要查询对应的好友ID + $friend = Db::table('s2_wechat_friend')->where('wechatId', $adminWechatId)->find(); + if ($friend) { + $admins[] = intval($friend['id']); + } + } + } + // 如果传入了admins参数,优先使用(兼容旧逻辑) + if (!empty($param['admins']) && is_array($param['admins'])) { + $admins = array_merge($admins, $param['admins']); + } + $config->admins = json_encode(array_unique($admins), JSON_UNESCAPED_UNICODE); + + $config->fixedWechatIds = json_encode($param['fixedWechatIds'] ?? [], JSON_UNESCAPED_UNICODE); + $config->createTime = time(); + $config->updateTime = time(); + $config->save(); + break; + case self::TYPE_TRAFFIC_DISTRIBUTION: // 流量分发 + $config = new WorkbenchTrafficConfig; + $config->workbenchId = $workbench->id; + $config->distributeType = $param['distributeType']; + $config->maxPerDay = $param['maxPerDay']; + $config->timeType = $param['timeType']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->devices = json_encode($param['deviceGroups'], JSON_UNESCAPED_UNICODE); + $config->pools = json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE); + $config->account = json_encode($param['accountGroups'], JSON_UNESCAPED_UNICODE); + $config->createTime = time(); + $config->updateTime = time(); + $config->save(); + break; + case self::TYPE_IMPORT_CONTACT: //联系人导入 + $config = new WorkbenchImportContact; + $config->workbenchId = $workbench->id; + $config->devices = json_encode($param['deviceGroups'], JSON_UNESCAPED_UNICODE); + $config->pools = json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE); + $config->num = $param['num']; + $config->clearContact = $param['clearContact']; + $config->remark = $param['remark']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->createTime = time(); + $config->save(); + break; + } + + Db::commit(); + return json(['code' => 200, 'msg' => '创建成功', 'data' => ['id' => $workbench->id]]); + } catch (\Exception $e) { + Db::rollback(); + return json(['code' => 500, 'msg' => '创建失败:' . $e->getMessage()]); + } + } + + /** + * 获取工作台列表 + * @return \think\response\Json + */ + public function getList() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $type = $this->request->param('type', ''); + $keyword = $this->request->param('keyword', ''); + + $where = [ + ['companyId', '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0] + ]; + + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + + + // 添加类型筛选 + if ($type !== '') { + $where[] = ['type', '=', $type]; + } + + // 添加名称模糊搜索 + if ($keyword !== '') { + $where[] = ['name', 'like', '%' . $keyword . '%']; + } + + // 定义关联关系 + $with = [ + 'autoLike' => function ($query) { + $query->field('workbenchId,interval,maxLikes,startTime,endTime,contentTypes,devices,friends'); + }, + 'momentsSync' => function ($query) { + $query->field('workbenchId,syncInterval,syncCount,syncType,startTime,endTime,accountType,devices,contentLibraries'); + }, + 'trafficConfig' => function ($query) { + $query->field('workbenchId,distributeType,maxPerDay,timeType,startTime,endTime,devices,pools,account'); + }, + 'groupPush' => function ($query) { + $query->field('workbenchId,pushType,targetType,groupPushSubType,startTime,endTime,maxPerDay,pushOrder,isLoop,status,groups,friends,ownerWechatIds,trafficPools,contentLibraries,friendIntervalMin,friendIntervalMax,messageIntervalMin,messageIntervalMax,isRandomTemplate,postPushTags,announcementContent,enableAiRewrite,aiRewritePrompt'); + }, + 'groupCreate' => function ($query) { + $query->field('workbenchId,devices,startTime,endTime,groupSizeMin,groupSizeMax,maxGroupsPerDay,groupNameTemplate,groupDescription,poolGroups,wechatGroups,admins'); + }, + 'importContact' => function ($query) { + $query->field('workbenchId,devices,pools,num,remarkType,remark,clearContact,startTime,endTime'); + }, + 'user' => function ($query) { + $query->field('id,username'); + } + ]; + + $list = Workbench::where($where) + ->with($with) + ->field('id,companyId,name,type,status,autoStart,userId,createTime,updateTime') + ->order('id', 'desc') + ->page($page, $limit) + ->select() + ->each(function ($item) { + // 处理配置信息 + switch ($item->type) { + case self::TYPE_AUTO_LIKE: + if (!empty($item->autoLike)) { + $item->config = $item->autoLike; + $item->config->devices = json_decode($item->config->devices, true); + $item->config->contentTypes = json_decode($item->config->contentTypes, true); + $item->config->friends = json_decode($item->config->friends, true); + + // 添加今日点赞数 + $startTime = strtotime(date('Y-m-d') . ' 00:00:00'); + $endTime = strtotime(date('Y-m-d') . ' 23:59:59'); + $todayLikeCount = Db::name('workbench_auto_like_item') + ->where('workbenchId', $item->id) + ->whereTime('createTime', 'between', [$startTime, $endTime]) + ->count(); + + // 添加总点赞数 + $totalLikeCount = Db::name('workbench_auto_like_item') + ->where('workbenchId', $item->id) + ->count(); + + $item->config->todayLikeCount = $todayLikeCount; + $item->config->totalLikeCount = $totalLikeCount; + } + unset($item->autoLike, $item->auto_like); + break; + case self::TYPE_MOMENTS_SYNC: + if (!empty($item->momentsSync)) { + $item->config = $item->momentsSync; + $item->config->devices = json_decode($item->config->devices, true); + $item->config->contentGroups = json_decode($item->config->contentLibraries, true); + //同步记录 + $sendNum = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $item->id])->count(); + $item->syncCount = $sendNum; + $lastTime = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $item->id])->order('id DESC')->value('createTime'); + $item->lastSyncTime = !empty($lastTime) ? date('Y-m-d H:i', $lastTime) : '--'; + + + // 获取内容库名称 + if (!empty($item->config->contentGroups)) { + $libraryNames = ContentLibrary::where('id', 'in', $item->config->contentGroups)->select(); + $item->config->contentGroupsOptions = $libraryNames; + } else { + $item->config->contentGroupsOptions = []; + } + } + unset($item->momentsSync, $item->moments_sync, $item->config->contentLibraries); + break; + case self::TYPE_GROUP_PUSH: + if (!empty($item->groupPush)) { + $item->config = $item->groupPush; + $item->config->pushType = $item->config->pushType; + $item->config->targetType = isset($item->config->targetType) ? intval($item->config->targetType) : 1; // 默认1=群推送 + $item->config->groupPushSubType = isset($item->config->groupPushSubType) ? intval($item->config->groupPushSubType) : 1; // 默认1=群群发 + $item->config->startTime = $item->config->startTime; + $item->config->endTime = $item->config->endTime; + $item->config->maxPerDay = $item->config->maxPerDay; + $item->config->pushOrder = $item->config->pushOrder; + $item->config->isLoop = $item->config->isLoop; + $item->config->status = $item->config->status; + $item->config->ownerWechatIds = json_decode($item->config->ownerWechatIds ?? '[]', true) ?: []; + // 根据targetType解析不同的数据 + if ($item->config->targetType == 1) { + // 群推送 + $item->config->wechatGroups = json_decode($item->config->groups, true) ?: []; + $item->config->wechatFriends = []; + // 群推送不需要devices字段 + // 群公告相关字段 + if ($item->config->groupPushSubType == 2) { + $item->config->announcementContent = isset($item->config->announcementContent) ? $item->config->announcementContent : ''; + $item->config->enableAiRewrite = isset($item->config->enableAiRewrite) ? intval($item->config->enableAiRewrite) : 0; + $item->config->aiRewritePrompt = isset($item->config->aiRewritePrompt) ? $item->config->aiRewritePrompt : ''; + } + $item->config->trafficPools = []; + } else { + // 好友推送 + $item->config->wechatFriends = json_decode($item->config->friends, true) ?: []; + $item->config->wechatGroups = []; + $item->config->trafficPools = json_decode($item->config->trafficPools ?? '[]', true) ?: []; + } + $item->config->contentLibraries = json_decode($item->config->contentLibraries, true); + $item->config->postPushTags = json_decode($item->config->postPushTags ?? '[]', true) ?: []; + $item->config->lastPushTime = ''; + if (!empty($item->config->ownerWechatIds)) { + $ownerWechatOptions = Db::name('wechat_account') + ->whereIn('id', $item->config->ownerWechatIds) + ->field('id,wechatId,nickName,avatar,alias') + ->select(); + $item->config->ownerWechatOptions = $ownerWechatOptions; + } else { + $item->config->ownerWechatOptions = []; + } + } + unset($item->groupPush, $item->group_push); + break; + case self::TYPE_GROUP_CREATE: + if (!empty($item->groupCreate)) { + $item->config = $item->groupCreate; + $item->config->devices = json_decode($item->config->devices, true); + $item->config->poolGroups = json_decode($item->config->poolGroups, true); + $item->config->wechatGroups = json_decode($item->config->wechatGroups, true); + $item->config->admins = json_decode($item->config->admins ?? '[]', true) ?: []; + + // 处理群管理员相关字段 + $item->config->groupAdminEnabled = !empty($item->config->admins) ? 1 : 0; + + if (!empty($item->config->admins)) { + $adminOptions = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->where('wf.id', 'in', $item->config->admins) + ->order('wf.id', 'desc') + ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar') + ->select(); + $item->config->adminsOptions = $adminOptions; + // 如果有管理员,设置groupAdminWechatId为第一个管理员的ID(用于前端回显) + $item->config->groupAdminWechatId = !empty($item->config->admins) ? $item->config->admins[0] : null; + } else { + $item->config->adminsOptions = []; + $item->config->groupAdminWechatId = null; + } + } + unset($item->groupCreate, $item->group_create); + break; + case self::TYPE_TRAFFIC_DISTRIBUTION: + if (!empty($item->trafficConfig)) { + $item->config = $item->trafficConfig; + $item->config->devices = json_decode($item->config->devices, true); + $item->config->poolGroups = json_decode($item->config->pools, true); + $item->config->account = json_decode($item->config->account, true); + $config_item = Db::name('workbench_traffic_config_item')->where(['workbenchId' => $item->id])->order('id DESC')->find(); + $item->config->lastUpdated = !empty($config_item) ? date('Y-m-d H:i', $config_item['createTime']) : '--'; + + //统计 + $labels = $item->config->poolGroups; + $totalUsers = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->where([ + ['wf.isDeleted', '=', 0], + ['sa.departmentId', '=', $item->companyId] + ]) + ->whereIn('wa.currentDeviceId', $item->config->devices); + + if (!empty($labels) && count($labels) > 0) { + $totalUsers = $totalUsers->where(function ($q) use ($labels) { + foreach ($labels as $label) { + $q->whereOrRaw("JSON_CONTAINS(wf.labels, '\"{$label}\"')"); + } + }); + } + + $totalUsers = $totalUsers->count(); + $totalAccounts = count($item->config->account); + $dailyAverage = Db::name('workbench_traffic_config_item') + ->where('workbenchId', $item->id) + ->count(); + $day = (time() - strtotime($item->createTime)) / 86400; + $day = intval($day); + if ($dailyAverage > 0 && $totalAccounts > 0 && $day > 0) { + $dailyAverage = $dailyAverage / $totalAccounts / $day; + } + $item->config->total = [ + 'dailyAverage' => intval($dailyAverage), + 'totalAccounts' => $totalAccounts, + 'deviceCount' => count($item->config->devices), + 'poolCount' => !empty($item->config->poolGroups) ? count($item->config->poolGroups) : 'ALL', + 'totalUsers' => $totalUsers >> 0 + ]; + } + unset($item->trafficConfig, $item->traffic_config); + break; + + case self::TYPE_IMPORT_CONTACT: + if (!empty($item->importContact)) { + $item->config = $item->importContact; + $item->config->devices = json_decode($item->config->devices, true); + $item->config->poolGroups = json_decode($item->config->pools, true); + } + unset($item->importContact, $item->import_contact); + break; + } + // 添加创建人名称 + $item['creatorName'] = $item->user ? $item->user->username : ''; + unset($item['user']); // 移除关联数据 + return $item; + }); + + $total = Workbench::where($where)->count(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + + /** + * 获取工作台详情 + * @param int $id 工作台ID + * @return \think\response\Json + */ + public function detail() + { + $id = $this->request->param('id', ''); + + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + // 定义关联关系 + $with = [ + 'autoLike' => function ($query) { + $query->field('workbenchId,interval,maxLikes,startTime,endTime,contentTypes,devices,friends,friendMaxLikes,friendTags,enableFriendTags'); + }, + 'momentsSync' => function ($query) { + $query->field('workbenchId,syncInterval,syncCount,syncType,startTime,endTime,accountType,devices,contentLibraries'); + }, + 'trafficConfig' => function ($query) { + $query->field('workbenchId,distributeType,maxPerDay,timeType,startTime,endTime,devices,pools,account'); + }, + 'groupPush' => function ($query) { + $query->field('workbenchId,pushType,targetType,groupPushSubType,startTime,endTime,maxPerDay,pushOrder,isLoop,status,groups,friends,ownerWechatIds,trafficPools,contentLibraries,friendIntervalMin,friendIntervalMax,messageIntervalMin,messageIntervalMax,isRandomTemplate,postPushTags,announcementContent,enableAiRewrite,aiRewritePrompt'); + }, + 'groupCreate' => function ($query) { + $query->field('workbenchId,devices,startTime,endTime,groupSizeMin,groupSizeMax,maxGroupsPerDay,groupNameTemplate,groupDescription,poolGroups,wechatGroups,admins'); + }, + 'importContact' => function ($query) { + $query->field('workbenchId,devices,pools,num,remarkType,remark,clearContact,startTime,endTime'); + }, + ]; + + $where = [ + ['id', '=', $id], + ['companyId', '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + + $workbench = Workbench::where($where) + ->field('id,name,type,status,autoStart,createTime,updateTime,companyId') + ->with($with) + ->find(); + + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + + // 处理配置信息 + switch ($workbench->type) { + //自动点赞 + case self::TYPE_AUTO_LIKE: + if (!empty($workbench->autoLike)) { + $workbench->config = $workbench->autoLike; + $workbench->config->deviceGroups = json_decode($workbench->config->devices, true); + $workbench->config->wechatFriends = json_decode($workbench->config->friends, true); + $workbench->config->targetType = 2; + //$workbench->config->targetGroups = json_decode($workbench->config->targetGroups, true); + $workbench->config->contentTypes = json_decode($workbench->config->contentTypes, true); + + // 添加今日点赞数 + $startTime = strtotime(date('Y-m-d') . ' 00:00:00'); + $endTime = strtotime(date('Y-m-d') . ' 23:59:59'); + $todayLikeCount = Db::name('workbench_auto_like_item') + ->where('workbenchId', $workbench->id) + ->whereTime('createTime', 'between', [$startTime, $endTime]) + ->count(); + + // 添加总点赞数 + $totalLikeCount = Db::name('workbench_auto_like_item') + ->where('workbenchId', $workbench->id) + ->count(); + + $workbench->config->todayLikeCount = $todayLikeCount; + $workbench->config->totalLikeCount = $totalLikeCount; + + unset($workbench->autoLike, $workbench->auto_like); + } + break; + //自动同步朋友圈 + case self::TYPE_MOMENTS_SYNC: + if (!empty($workbench->momentsSync)) { + $workbench->config = $workbench->momentsSync; + $workbench->config->deviceGroups = json_decode($workbench->config->devices, true); + $workbench->config->contentGroups = json_decode($workbench->config->contentLibraries, true); + + //同步记录 + $sendNum = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $workbench->id])->count(); + $workbench->syncCount = $sendNum; + $lastTime = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $workbench->id])->order('id DESC')->value('createTime'); + $workbench->lastSyncTime = !empty($lastTime) ? date('Y-m-d H:i', $lastTime) : '--'; + unset($workbench->momentsSync, $workbench->moments_sync); + } + break; + //群推送 + case self::TYPE_GROUP_PUSH: + if (!empty($workbench->groupPush)) { + $workbench->config = $workbench->groupPush; + $workbench->config->targetType = isset($workbench->config->targetType) ? intval($workbench->config->targetType) : 1; // 默认1=群推送 + $workbench->config->groupPushSubType = isset($workbench->config->groupPushSubType) ? intval($workbench->config->groupPushSubType) : 1; // 默认1=群群发 + $workbench->config->ownerWechatIds = json_decode($workbench->config->ownerWechatIds ?? '[]', true) ?: []; + // 根据targetType解析不同的数据 + if ($workbench->config->targetType == 1) { + // 群推送 + $workbench->config->wechatGroups = json_decode($workbench->config->groups, true) ?: []; + $workbench->config->wechatFriends = []; + $workbench->config->trafficPools = []; + // 群推送不需要devices字段 + // 群公告相关字段 + if ($workbench->config->groupPushSubType == 2) { + $workbench->config->announcementContent = isset($workbench->config->announcementContent) ? $workbench->config->announcementContent : ''; + $workbench->config->enableAiRewrite = isset($workbench->config->enableAiRewrite) ? intval($workbench->config->enableAiRewrite) : 0; + $workbench->config->aiRewritePrompt = isset($workbench->config->aiRewritePrompt) ? $workbench->config->aiRewritePrompt : ''; + } + } else { + // 好友推送 + $workbench->config->wechatFriends = json_decode($workbench->config->friends, true) ?: []; + $workbench->config->wechatGroups = []; + $workbench->config->trafficPools = json_decode($workbench->config->trafficPools ?? '[]', true) ?: []; + } + $workbench->config->contentLibraries = json_decode($workbench->config->contentLibraries, true); + $workbench->config->postPushTags = json_decode($workbench->config->postPushTags ?? '[]', true) ?: []; + unset($workbench->groupPush, $workbench->group_push); + } + break; + //建群助手 + case self::TYPE_GROUP_CREATE: + if (!empty($workbench->groupCreate)) { + $workbench->config = $workbench->groupCreate; + $workbench->config->deviceGroups = json_decode($workbench->config->devices, true); + $workbench->config->poolGroups = json_decode($workbench->config->poolGroups, true); + $workbench->config->wechatGroups = json_decode($workbench->config->wechatGroups, true); + $workbench->config->admins = json_decode($workbench->config->admins ?? '[]', true) ?: []; + + // 处理群管理员相关字段 + $workbench->config->groupAdminEnabled = !empty($workbench->config->admins) ? 1 : 0; + + // 如果有管理员,设置groupAdminWechatId为第一个管理员的ID(用于前端回显) + $workbench->config->groupAdminWechatId = !empty($workbench->config->admins) ? $workbench->config->admins[0] : null; + + // 统计已建群数(状态为成功且groupId不为空的记录,按groupId分组去重) + $createdGroupsCount = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbench->id) + ->where('status', 2) // STATUS_SUCCESS = 2 + ->where('groupId', '<>', null) + ->group('groupId') + ->count(); + + // 统计总人数(该工作台的所有记录数) + $totalMembersCount = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbench->id) + ->count(); + + // 添加统计信息 + $workbench->config->stats = [ + 'createdGroupsCount' => $createdGroupsCount, + 'totalMembersCount' => $totalMembersCount + ]; + + unset($workbench->groupCreate, $workbench->group_create); + } + break; + //流量分发 + case self::TYPE_TRAFFIC_DISTRIBUTION: + if (!empty($workbench->trafficConfig)) { + $workbench->config = $workbench->trafficConfig; + $workbench->config->deviceGroups = json_decode($workbench->config->devices, true); + $workbench->config->accountGroups = json_decode($workbench->config->account, true); + $workbench->config->poolGroups = json_decode($workbench->config->pools, true); + $config_item = Db::name('workbench_traffic_config_item')->where(['workbenchId' => $workbench->id])->order('id DESC')->find(); + $workbench->config->lastUpdated = !empty($config_item) ? date('Y-m-d H:i', $config_item['createTime']) : '--'; + + //统计 + $labels = $workbench->config->poolGroups; + $totalUsers = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->where([ + ['wf.isDeleted', '=', 0], + ['sa.departmentId', '=', $workbench->companyId] + ]) + ->whereIn('wa.currentDeviceId', $workbench->config->deviceGroups) + ->field('wf.id,wf.wechatAccountId,wf.wechatId,wf.labels,sa.userName,wa.currentDeviceId as deviceId') + ->where(function ($q) use ($labels) { + foreach ($labels as $label) { + $q->whereOrRaw("JSON_CONTAINS(wf.labels, '\"{$label}\"')"); + } + })->count(); + + $totalAccounts = Db::table('s2_company_account') + ->alias('a') + ->where(['a.departmentId' => $workbench->companyId, 'a.status' => 0]) + ->whereNotLike('a.userName', '%_offline%') + ->whereNotLike('a.userName', '%_delete%') + ->group('a.id') + ->count(); + + $dailyAverage = Db::name('workbench_traffic_config_item') + ->where('workbenchId', $workbench->id) + ->count(); + $day = (time() - strtotime($workbench->createTime)) / 86400; + $day = intval($day); + + + if ($dailyAverage > 0) { + $dailyAverage = $dailyAverage / $totalAccounts / $day; + } + + $workbench->config->total = [ + 'dailyAverage' => intval($dailyAverage), + 'totalAccounts' => $totalAccounts, + 'deviceCount' => count($workbench->config->deviceGroups), + 'poolCount' => count($workbench->config->poolGroups), + 'totalUsers' => $totalUsers >> 0 + ]; + unset($workbench->trafficConfig, $workbench->traffic_config); + } + break; + case self::TYPE_IMPORT_CONTACT: + if (!empty($workbench->importContact)) { + $workbench->config = $workbench->importContact; + $workbench->config->deviceGroups = json_decode($workbench->config->devices, true); + $workbench->config->poolGroups = json_decode($workbench->config->pools, true); + } + unset($workbench->importContact, $workbench->import_contact); + break; + } + unset( + $workbench->autoLike, + $workbench->momentsSync, + $workbench->groupPush, + $workbench->groupCreate, + $workbench->config->devices, + $workbench->config->friends, + $workbench->config->groups, + $workbench->config->contentLibraries, + $workbench->config->account, + ); + + + //获取设备信息 + if (!empty($workbench->config->deviceGroups)) { + $deviceList = DeviceModel::alias('d') + ->field([ + 'd.id', 'd.imei', 'd.memo', 'd.alive', + 'l.wechatId', + 'a.nickname', 'a.alias', 'a.avatar', 'a.alias', '0 totalFriend' + ]) + ->leftJoin('device_wechat_login l', 'd.id = l.deviceId and l.alive =' . DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE . ' and l.companyId = d.companyId') + ->leftJoin('wechat_account a', 'l.wechatId = a.wechatId') + ->whereIn('d.id', $workbench->config->deviceGroups) + ->order('d.id desc') + ->select(); + + foreach ($deviceList as &$device) { + $curstomer = WechatCustomerModel::field('friendShip')->where(['wechatId' => $device['wechatId']])->find(); + $device['totalFriend'] = $curstomer->friendShip->totalFriend ?? 0; + } + unset($device); + + $workbench->config->deviceGroupsOptions = $deviceList; + } else { + $workbench->config->deviceGroupsOptions = []; + } + + + // 获取群(当targetType=1时) + if (!empty($workbench->config->wechatGroups) && isset($workbench->config->targetType) && $workbench->config->targetType == 1) { + $groupList = Db::name('wechat_group')->alias('wg') + ->join('wechat_account wa', 'wa.wechatId = wg.ownerWechatId') + ->where('wg.id', 'in', $workbench->config->wechatGroups) + ->order('wg.id', 'desc') + ->field('wg.id,wg.name as groupName,wg.ownerWechatId,wa.nickName,wa.avatar,wa.alias,wg.avatar as groupAvatar') + ->select(); + $workbench->config->wechatGroupsOptions = $groupList; + } else { + $workbench->config->wechatGroupsOptions = []; + } + + // 获取好友(当targetType=2时) + if (!empty($workbench->config->wechatFriends) && isset($workbench->config->targetType) && $workbench->config->targetType == 2) { + $friendList = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->where('wf.id', 'in', $workbench->config->wechatFriends) + ->order('wf.id', 'desc') + ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar') + ->select(); + $workbench->config->wechatFriendsOptions = $friendList; + } else { + $workbench->config->wechatFriendsOptions = []; + } + + // 获取流量池(当targetType=2时) + if (!empty($workbench->config->trafficPools) && isset($workbench->config->targetType) && $workbench->config->targetType == 2) { + $poolList = Db::name('traffic_source_package')->alias('tsp') + ->leftJoin('traffic_source_package_item tspi', 'tspi.packageId = tsp.id and tspi.isDel = 0') + ->whereIn('tsp.id', $workbench->config->trafficPools) + ->where('tsp.isDel', 0) + ->whereIn('tsp.companyId', [$this->request->userInfo['companyId'], 0]) + ->field('tsp.id,tsp.name,tsp.description,tsp.pic,COUNT(tspi.id) as itemCount') + ->group('tsp.id') + ->order('tsp.id', 'desc') + ->select(); + $workbench->config->trafficPoolsOptions = $poolList; + } else { + $workbench->config->trafficPoolsOptions = []; + } + + // 获取内容库名称 + if (!empty($workbench->config->contentGroups)) { + $libraryNames = ContentLibrary::where('id', 'in', $workbench->config->contentGroups)->select(); + $workbench->config->contentGroupsOptions = $libraryNames; + } else { + $workbench->config->contentGroupsOptions = []; + } + + //账号 + if (!empty($workbench->config->accountGroups)) { + $account = Db::table('s2_company_account')->alias('a') + ->where(['a.departmentId' => $this->request->userInfo['companyId'], 'a.status' => 0]) + ->whereIn('a.id', $workbench->config->accountGroups) + ->whereNotLike('a.userName', '%_offline%') + ->whereNotLike('a.userName', '%_delete%') + ->field('a.id,a.userName,a.realName,a.nickname,a.memo') + ->select(); + $workbench->config->accountGroupsOptions = $account; + } else { + $workbench->config->accountGroupsOptions = []; + } + + if (!empty($workbench->config->poolGroups)) { + $poolGroupsOptions = Db::name('traffic_source_package')->alias('tsp') + ->join('traffic_source_package_item tspi', 'tspi.packageId=tsp.id', 'left') + ->whereIn('tsp.companyId', [$this->request->userInfo['companyId'], 0]) + ->whereIn('tsp.id', $workbench->config->poolGroups) + ->field('tsp.id,tsp.name,tsp.description,tsp.createTime,count(tspi.id) as num') + ->group('tsp.id') + ->select(); + $workbench->config->poolGroupsOptions = $poolGroupsOptions; + } else { + $workbench->config->poolGroupsOptions = []; + } + + if (!empty($workbench->config->ownerWechatIds)) { + $ownerWechatOptions = Db::name('wechat_account') + ->whereIn('id', $workbench->config->ownerWechatIds) + ->field('id,wechatId,nickName,avatar,alias') + ->select(); + $workbench->config->ownerWechatOptions = $ownerWechatOptions; + } else { + $workbench->config->ownerWechatOptions = []; + } + + // 获取群组选项(自动建群) + if ($workbench->type == self::TYPE_GROUP_CREATE && !empty($workbench->config->wechatGroups)) { + // 分离数字ID(好友ID)和字符串ID(手动创建的群组) + $friendIds = []; + $manualGroupIds = []; + + foreach ($workbench->config->wechatGroups as $groupId) { + if (is_numeric($groupId)) { + $friendIds[] = intval($groupId); + } else { + $manualGroupIds[] = $groupId; + } + } + + $wechatGroupsOptions = []; + + // 查询好友信息(数字ID) + if (!empty($friendIds)) { + $friendList = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->join(['s2_company_account' => 'ca'], 'ca.id = wf.accountId', 'left') + ->where('wf.id', 'in', $friendIds) + ->order('wf.id', 'desc') + ->field('wf.id,wf.wechatId,wf.nickname,wf.avatar,wf.alias,wf.gender,wf.phone,wa.nickName as accountNickname,ca.userName as account,ca.realName as username,wf.createTime,wf.updateTime,wf.deleteTime,wf.ownerWechatId') + ->select(); + + // 获取群主信息 + foreach ($friendList as &$friend) { + if (!empty($friend['ownerWechatId'])) { + $owner = Db::name('wechat_account') + ->where('wechatId', $friend['ownerWechatId']) + ->field('nickName,alias') + ->find(); + $friend['ownerNickname'] = $owner['nickName'] ?? ''; + $friend['ownerAlias'] = $owner['alias'] ?? ''; + } else { + $friend['ownerNickname'] = ''; + $friend['ownerAlias'] = ''; + } + $friend['isManual'] = ''; + // 格式化时间 + $friend['createTime'] = !empty($friend['createTime']) ? date('Y-m-d H:i:s', $friend['createTime']) : ''; + $friend['updateTime'] = !empty($friend['updateTime']) ? date('Y-m-d H:i:s', $friend['updateTime']) : ''; + $friend['deleteTime'] = !empty($friend['deleteTime']) ? date('Y-m-d H:i:s', $friend['deleteTime']) : ''; + } + unset($friend); + + $wechatGroupsOptions = array_merge($wechatGroupsOptions, $friendList); + } + + // 处理手动创建的群组(字符串ID) + if (!empty($manualGroupIds)) { + foreach ($manualGroupIds as $groupId) { + // 手动创建的群组,只返回基本信息 + $wechatGroupsOptions[] = [ + 'id' => $groupId, + 'wechatId' => $groupId, + 'nickname' => $groupId, + 'avatar' => '', + 'isManual' => 1 + ]; + } + } + + $workbench->config->wechatGroupsOptions = $wechatGroupsOptions; + } else { + $workbench->config->wechatGroupsOptions = []; + } + + // 获取管理员选项(自动建群) + if ($workbench->type == self::TYPE_GROUP_CREATE && !empty($workbench->config->admins)) { + $adminOptions = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->where('wf.id', 'in', $workbench->config->admins) + ->order('wf.id', 'desc') + ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar') + ->select(); + $workbench->config->adminsOptions = $adminOptions; + } else { + $workbench->config->adminsOptions = []; + } + + return json(['code' => 200, 'msg' => '获取成功', 'data' => $workbench]); + } + + /** + * 更新工作台 + * @return \think\response\Json + */ + public function update() + { + if (!$this->request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + // 获取请求参数 + $param = $this->request->post(); + + // 验证数据 + $validate = new WorkbenchValidate; + if (!$validate->scene('update')->check($param)) { + return json(['code' => 400, 'msg' => $validate->getError()]); + } + + + $where = [ + ['id', '=', $param['id']], + ['companyId', '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + // 查询工作台是否存在 + $workbench = Workbench::where($where)->find(); + if (!$workbench) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + + Db::startTrans(); + try { + // 更新工作台基本信息 + $workbench->name = $param['name']; + $workbench->status = !empty($param['status']) ? 1 : 0; + $workbench->autoStart = !empty($param['autoStart']) ? 1 : 0; + $workbench->updateTime = time(); + $workbench->save(); + + // 根据类型更新对应的配置 + switch ($workbench->type) { + case self::TYPE_AUTO_LIKE: + $config = WorkbenchAutoLike::where('workbenchId', $param['id'])->find(); + if ($config) { + $config->interval = $param['interval']; + $config->maxLikes = $param['maxLikes']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->contentTypes = json_encode($param['contentTypes']); + $config->devices = json_encode($param['deviceGroups']); + $config->friends = json_encode($param['wechatFriends']); + // $config->targetGroups = json_encode($param['targetGroups']); + // $config->tagOperator = $param['tagOperator']; + $config->friendMaxLikes = $param['friendMaxLikes']; + $config->friendTags = $param['friendTags']; + $config->enableFriendTags = $param['enableFriendTags']; + $config->updateTime = time(); + $config->save(); + } + break; + + case self::TYPE_MOMENTS_SYNC: + $config = WorkbenchMomentsSync::where('workbenchId', $param['id'])->find(); + if ($config) { + if (!empty($param['contentGroups'])) { + foreach ($param['contentGroups'] as $library) { + if (isset($library['id']) && !empty($library['id'])) { + $contentLibraries[] = $library['id']; + } else { + $contentLibraries[] = $library; + } + } + } else { + $contentLibraries = []; + } + + $config->syncInterval = $param['syncInterval']; + $config->syncCount = $param['syncCount']; + $config->syncType = $param['syncType']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->accountType = $param['accountType']; + $config->devices = json_encode($param['deviceGroups']); + $config->contentLibraries = json_encode($contentLibraries); + $config->updateTime = time(); + $config->save(); + } + break; + + case self::TYPE_GROUP_PUSH: + $config = WorkbenchGroupPush::where('workbenchId', $param['id'])->find(); + if ($config) { + $ownerWechatIds = $this->normalizeOwnerWechatIds($param['ownerWechatIds'] ?? null, $config); + $groupPushData = $this->prepareGroupPushData($param, $ownerWechatIds, $config); + $groupPushData['updateTime'] = time(); + $config->save($groupPushData); + } + break; + + case self::TYPE_GROUP_CREATE: + $config = WorkbenchGroupCreate::where('workbenchId', $param['id'])->find(); + if ($config) { + $config->devices = json_encode($param['deviceGroups'] ?? [], JSON_UNESCAPED_UNICODE); + $config->startTime = $param['startTime'] ?? ''; + $config->endTime = $param['endTime'] ?? ''; + $config->groupSizeMin = intval($param['groupSizeMin'] ?? 3); + $config->groupSizeMax = intval($param['groupSizeMax'] ?? 38); + $config->maxGroupsPerDay = intval($param['maxGroupsPerDay'] ?? 20); + $config->groupNameTemplate = $param['groupNameTemplate'] ?? ''; + $config->groupDescription = $param['groupDescription'] ?? ''; + $config->poolGroups = json_encode($param['poolGroups'] ?? [], JSON_UNESCAPED_UNICODE); + $config->wechatGroups = json_encode($param['wechatGroups'] ?? [], JSON_UNESCAPED_UNICODE); + + // 处理群管理员:如果启用了群管理员且有指定管理员,则保存到admins字段 + $admins = []; + if (!empty($param['groupAdminEnabled']) && !empty($param['groupAdminWechatId'])) { + // 如果groupAdminWechatId是数组,取第一个;如果是单个值,直接使用 + $adminWechatId = is_array($param['groupAdminWechatId']) ? $param['groupAdminWechatId'][0] : $param['groupAdminWechatId']; + // 如果是好友ID,直接添加到admins;如果是wechatId,需要转换为好友ID + if (is_numeric($adminWechatId)) { + $admins[] = intval($adminWechatId); + } else { + // 如果是wechatId字符串,需要查询对应的好友ID + $friend = Db::table('s2_wechat_friend')->where('wechatId', $adminWechatId)->find(); + if ($friend) { + $admins[] = intval($friend['id']); + } + } + } + // 如果传入了admins参数,优先使用(兼容旧逻辑) + if (!empty($param['admins']) && is_array($param['admins'])) { + $admins = array_merge($admins, $param['admins']); + } + $config->admins = json_encode(array_unique($admins), JSON_UNESCAPED_UNICODE); + + $config->updateTime = time(); + $config->save(); + } + break; + case self::TYPE_TRAFFIC_DISTRIBUTION: + $config = WorkbenchTrafficConfig::where('workbenchId', $param['id'])->find(); + if ($config) { + $config->distributeType = $param['distributeType']; + $config->maxPerDay = $param['maxPerDay']; + $config->timeType = $param['timeType']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->devices = json_encode($param['deviceGroups']); + $config->pools = json_encode($param['poolGroups']); + $config->account = json_encode($param['accountGroups']); + $config->updateTime = time(); + $config->save(); + } + break; + case self::TYPE_IMPORT_CONTACT: //联系人导入 + $config = WorkbenchImportContact::where('workbenchId', $param['id'])->find();; + if ($config) { + $config->devices = json_encode($param['deviceGroups']); + $config->pools = json_encode($param['poolGroups']); + $config->num = $param['num']; + $config->clearContact = $param['clearContact']; + $config->remark = $param['remark']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->save(); + } + break; + } + + Db::commit(); + return json(['code' => 200, 'msg' => '更新成功']); + } catch (\Exception $e) { + Db::rollback(); + return json(['code' => 500, 'msg' => '更新失败:' . $e->getMessage()]); + } + } + + /** + * 更新工作台状态 + * @return \think\response\Json + */ + public function updateStatus() + { + if (!$this->request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + $id = $this->request->param('id', ''); + + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + + + $where = [ + ['id', '=', $id], + ['companyId', '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + $workbench = Workbench::where($where)->find(); + + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + + $workbench->status = !$workbench['status']; + $workbench->save(); + + return json(['code' => 200, 'msg' => '更新成功']); + } + + /** + * 删除工作台(软删除) + */ + public function delete() + { + $id = $this->request->param('id'); + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + $where = [ + ['id', '=', $id], + ['companyId', '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + $workbench = Workbench::where($where)->find(); + + if (!$workbench) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + + // 软删除 + $workbench->isDel = 1; + $workbench->deleteTime = time(); + $workbench->save(); + + return json(['code' => 200, 'msg' => '删除成功']); + } + + /** + * 拷贝工作台 + * @return \think\response\Json + */ + public function copy() + { + if (!$this->request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + $id = $this->request->post('id'); + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + // 验证权限并获取原数据 + $workbench = Workbench::where([ + ['id', '=', $id], + ['userId', '=', $this->request->userInfo['id']] + ])->find(); + + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + + Db::startTrans(); + try { + // 创建新的工作台基本信息 + $newWorkbench = new Workbench; + $newWorkbench->name = $workbench->name . ' copy'; + $newWorkbench->type = $workbench->type; + $newWorkbench->status = 1; // 新拷贝的默认启用 + $newWorkbench->autoStart = $workbench->autoStart; + $newWorkbench->userId = $this->request->userInfo['id']; + $newWorkbench->companyId = $this->request->userInfo['companyId']; + $newWorkbench->save(); + + // 根据类型拷贝对应的配置 + switch ($workbench->type) { + case self::TYPE_AUTO_LIKE: + $config = WorkbenchAutoLike::where('workbenchId', $id)->find(); + if ($config) { + $newConfig = new WorkbenchAutoLike; + $newConfig->workbenchId = $newWorkbench->id; + $newConfig->interval = $config->interval; + $newConfig->maxLikes = $config->maxLikes; + $newConfig->startTime = $config->startTime; + $newConfig->endTime = $config->endTime; + $newConfig->contentTypes = $config->contentTypes; + $newConfig->devices = $config->devices; + $newConfig->friends = $config->friends; + $newConfig->createTime = time(); + $newConfig->updateTime = time(); + $newConfig->save(); + } + break; + case self::TYPE_MOMENTS_SYNC: + $config = WorkbenchMomentsSync::where('workbenchId', $id)->find(); + if ($config) { + $newConfig = new WorkbenchMomentsSync; + $newConfig->workbenchId = $newWorkbench->id; + $newConfig->syncInterval = $config->syncInterval; + $newConfig->syncCount = $config->syncCount; + $newConfig->syncType = $config->syncType; + $newConfig->startTime = $config->startTime; + $newConfig->endTime = $config->endTime; + $newConfig->accountType = $config->accountType; + $newConfig->devices = $config->devices; + $newConfig->contentLibraries = $config->contentLibraries; + $newConfig->createTime = time(); + $newConfig->updateTime = time(); + $newConfig->save(); + } + break; + case self::TYPE_GROUP_PUSH: + $config = WorkbenchGroupPush::where('workbenchId', $id)->find(); + if ($config) { + $newConfig = new WorkbenchGroupPush; + $newConfig->workbenchId = $newWorkbench->id; + $newConfig->pushType = $config->pushType; + $newConfig->targetType = isset($config->targetType) ? $config->targetType : 1; // 默认1=群推送 + $newConfig->startTime = $config->startTime; + $newConfig->endTime = $config->endTime; + $newConfig->maxPerDay = $config->maxPerDay; + $newConfig->pushOrder = $config->pushOrder; + $newConfig->isLoop = $config->isLoop; + $newConfig->status = $config->status; + $newConfig->groups = $config->groups; + $newConfig->friends = $config->friends; + $newConfig->contentLibraries = $config->contentLibraries; + $newConfig->trafficPools = property_exists($config, 'trafficPools') ? $config->trafficPools : json_encode([], JSON_UNESCAPED_UNICODE); + $newConfig->socialMediaId = $config->socialMediaId; + $newConfig->promotionSiteId = $config->promotionSiteId; + $newConfig->ownerWechatIds = $config->ownerWechatIds; + $newConfig->createTime = time(); + $newConfig->updateTime = time(); + $newConfig->save(); + } + break; + case self::TYPE_GROUP_CREATE: + $config = WorkbenchGroupCreate::where('workbenchId', $id)->find(); + if ($config) { + $newConfig = new WorkbenchGroupCreate; + $newConfig->workbenchId = $newWorkbench->id; + $newConfig->devices = $config->devices; + $newConfig->startTime = $config->startTime; + $newConfig->endTime = $config->endTime; + $newConfig->groupSizeMin = $config->groupSizeMin; + $newConfig->groupSizeMax = $config->groupSizeMax; + $newConfig->maxGroupsPerDay = $config->maxGroupsPerDay; + $newConfig->groupNameTemplate = $config->groupNameTemplate; + $newConfig->groupDescription = $config->groupDescription; + $newConfig->poolGroups = $config->poolGroups; + $newConfig->wechatGroups = $config->wechatGroups; + $newConfig->admins = $config->admins ?? json_encode([], JSON_UNESCAPED_UNICODE); + $newConfig->createTime = time(); + $newConfig->updateTime = time(); + $newConfig->save(); + } + break; + case self::TYPE_IMPORT_CONTACT: //联系人导入 + $config = WorkbenchImportContact::where('workbenchId', $id)->find(); + if ($config) { + $newConfig = new WorkbenchImportContact; + $newConfig->workbenchId = $newWorkbench->id; + $newConfig->devices = $config->devices; + $newConfig->pools = $config->pools; + $newConfig->num = $config->num; + $newConfig->clearContact = $config->clearContact; + $newConfig->remark = $config->remark; + $newConfig->startTime = $config->startTime; + $newConfig->endTime = $config->endTime; + $newConfig->createTime = time(); + $newConfig->save(); + } + break; + } + + Db::commit(); + return json(['code' => 200, 'msg' => '拷贝成功', 'data' => ['id' => $newWorkbench->id]]); + } catch (\Exception $e) { + Db::rollback(); + return json(['code' => 500, 'msg' => '拷贝失败:' . $e->getMessage()]); + } + } + + /** + * 获取点赞记录列表 + * @return \think\response\Json + */ + public function getLikeRecords() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchAutoLikeController(); + $controller->request = $this->request; + return $controller->getLikeRecords(); + } + + /** + * 获取朋友圈发布记录列表 + * @return \think\response\Json + */ + public function getMomentsRecords() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchMomentsController(); + $controller->request = $this->request; + return $controller->getMomentsRecords(); + } + + /** + * 获取朋友圈发布统计 + * @return \think\response\Json + */ + public function getMomentsStats() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchMomentsController(); + $controller->request = $this->request; + return $controller->getMomentsStats(); + } + + /** + * 获取流量分发记录列表 + * @return \think\response\Json + */ + public function getTrafficDistributionRecords() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $workbenchId = $this->request->param('workbenchId', 0); + + $where = [ + ['wtdi.workbenchId', '=', $workbenchId] + ]; + + // 查询分发记录 + $list = Db::name('workbench_traffic_distribution_item')->alias('wtdi') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wtdi.wechatAccountId', 'left') + ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wtdi.wechatFriendId', 'left') + ->field([ + 'wtdi.id', + 'wtdi.workbenchId', + 'wtdi.wechatAccountId', + 'wtdi.wechatFriendId', + 'wtdi.createTime as distributeTime', + 'wtdi.status', + 'wtdi.errorMsg', + 'wa.nickName as operatorName', + 'wa.avatar as operatorAvatar', + 'wf.nickName as friendName', + 'wf.avatar as friendAvatar', + 'wf.gender', + 'wf.province', + 'wf.city' + ]) + ->where($where) + ->order('wtdi.createTime', 'desc') + ->page($page, $limit) + ->select(); + + // 处理数据 + foreach ($list as &$item) { + // 处理时间格式 + $item['distributeTime'] = date('Y-m-d H:i:s', $item['distributeTime']); + + // 处理性别 + $genderMap = [ + 0 => '未知', + 1 => '男', + 2 => '女' + ]; + $item['genderText'] = $genderMap[$item['gender']] ?? '未知'; + + // 处理状态文字 + $statusMap = [ + 0 => '待分发', + 1 => '分发成功', + 2 => '分发失败' + ]; + $item['statusText'] = $statusMap[$item['status']] ?? '未知状态'; + } + + // 获取总记录数 + $total = Db::name('workbench_traffic_distribution_item')->alias('wtdi') + ->where($where) + ->count(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + + /** + * 获取流量分发统计 + * @return \think\response\Json + */ + public function getTrafficDistributionStats() + { + $workbenchId = $this->request->param('workbenchId', 0); + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + // 获取今日数据 + $todayStart = strtotime(date('Y-m-d') . ' 00:00:00'); + $todayEnd = strtotime(date('Y-m-d') . ' 23:59:59'); + + $todayStats = Db::name('workbench_traffic_distribution_item') + ->where([ + ['workbenchId', '=', $workbenchId], + ['createTime', 'between', [$todayStart, $todayEnd]] + ]) + ->field([ + 'COUNT(*) as total', + 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success', + 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed' + ]) + ->find(); + + // 获取总数据 + $totalStats = Db::name('workbench_traffic_distribution_item') + ->where('workbenchId', $workbenchId) + ->field([ + 'COUNT(*) as total', + 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success', + 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed' + ]) + ->find(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'today' => [ + 'total' => intval($todayStats['total']), + 'success' => intval($todayStats['success']), + 'failed' => intval($todayStats['failed']) + ], + 'total' => [ + 'total' => intval($totalStats['total']), + 'success' => intval($totalStats['success']), + 'failed' => intval($totalStats['failed']) + ] + ] + ]); + } + + /** + * 获取流量分发详情 + * @return \think\response\Json + */ + public function getTrafficDistributionDetail() + { + $id = $this->request->param('id', 0); + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + $detail = Db::name('workbench_traffic_distribution_item')->alias('wtdi') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wtdi.wechatAccountId', 'left') + ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wtdi.wechatFriendId', 'left') + ->field([ + 'wtdi.id', + 'wtdi.workbenchId', + 'wtdi.wechatAccountId', + 'wtdi.wechatFriendId', + 'wtdi.createTime as distributeTime', + 'wtdi.status', + 'wtdi.errorMsg', + 'wa.nickName as operatorName', + 'wa.avatar as operatorAvatar', + 'wf.nickName as friendName', + 'wf.avatar as friendAvatar', + 'wf.gender', + 'wf.province', + 'wf.city', + 'wf.signature', + 'wf.remark' + ]) + ->where('wtdi.id', $id) + ->find(); + + if (empty($detail)) { + return json(['code' => 404, 'msg' => '记录不存在']); + } + + // 处理数据 + $detail['distributeTime'] = date('Y-m-d H:i:s', $detail['distributeTime']); + + // 处理性别 + $genderMap = [ + 0 => '未知', + 1 => '男', + 2 => '女' + ]; + $detail['genderText'] = $genderMap[$detail['gender']] ?? '未知'; + + // 处理状态文字 + $statusMap = [ + 0 => '待分发', + 1 => '分发成功', + 2 => '分发失败' + ]; + $detail['statusText'] = $statusMap[$detail['status']] ?? '未知状态'; + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => $detail + ]); + } + + /** + * 创建流量分发计划 + * @return \think\response\Json + */ + public function createTrafficPlan() + { + $param = $this->request->post(); + Db::startTrans(); + try { + // 1. 创建主表 + $planId = Db::name('ck_workbench')->insertGetId([ + 'name' => $param['name'], + 'type' => self::TYPE_TRAFFIC_DISTRIBUTION, + 'status' => 1, + 'autoStart' => $param['autoStart'] ?? 0, + 'userId' => $this->request->userInfo['id'], + 'companyId' => $this->request->userInfo['companyId'], + 'createTime' => time(), + 'updateTime' => time() + ]); + // 2. 创建扩展表 + Db::name('ck_workbench_traffic_config')->insert([ + 'workbenchId' => $planId, + 'distributeType' => $param['distributeType'], + 'maxPerDay' => $param['maxPerDay'], + 'timeType' => $param['timeType'], + 'startTime' => $param['startTime'], + 'endTime' => $param['endTime'], + 'targets' => json_encode($param['targets'], JSON_UNESCAPED_UNICODE), + 'pools' => json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE), + 'createTime' => time(), + 'updateTime' => time() + ]); + Db::commit(); + return json(['code' => 200, 'msg' => '创建成功']); + } catch (\Exception $e) { + Db::rollback(); + return json(['code' => 500, 'msg' => '创建失败:' . $e->getMessage()]); + } + } + + /** + * 获取所有微信好友标签及数量统计 + * @return \think\response\Json + */ + public function getDeviceLabels() + { + $deviceIds = $this->request->param('deviceIds', ''); + $companyId = $this->request->userInfo['companyId']; + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + + $where = [ + ['wc.companyId', '=', $companyId], + ]; + + if (!empty($deviceIds)) { + $deviceIds = explode(',', $deviceIds); + $where[] = ['dwl.deviceId', 'in', $deviceIds]; + } + + $wechatAccounts = Db::name('wechat_customer')->alias('wc') + ->join('device_wechat_login dwl', 'dwl.wechatId = wc.wechatId AND dwl.companyId = wc.companyId AND dwl.alive = 1') + ->join(['s2_wechat_account' => 'wa'], 'wa.wechatId = wc.wechatId') + ->where($where) + ->field('wa.id,wa.wechatId,wa.nickName,wa.labels') + ->select(); + $labels = []; + $wechatIds = []; + foreach ($wechatAccounts as $account) { + $labelArr = json_decode($account['labels'], true); + if (is_array($labelArr)) { + foreach ($labelArr as $label) { + if ($label !== '' && $label !== null) { + $labels[] = $label; + } + } + } + $wechatIds[] = $account['wechatId']; + } + // 去重(只保留一个) + $labels = array_values(array_unique($labels)); + $wechatIds = array_unique($wechatIds); + + // 搜索过滤 + if (!empty($keyword)) { + $labels = array_filter($labels, function ($label) use ($keyword) { + return mb_stripos($label, $keyword) !== false; + }); + $labels = array_values($labels); // 重新索引数组 + } + + + // 分页处理 + $labels2 = array_slice($labels, ($page - 1) * $limit, $limit); + + // 统计数量 + $newLabel = []; + foreach ($labels2 as $label) { + $friendCount = Db::table('s2_wechat_friend') + ->whereIn('ownerWechatId', $wechatIds) + ->where('labels', 'like', '%"' . $label . '"%') + ->count(); + $newLabel[] = [ + 'label' => $label, + 'count' => $friendCount + ]; + } + + // 返回结果 + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $newLabel, + 'total' => count($labels), + ] + ]); + } + + + /** + * 获取群列表 + * @return \think\response\Json + */ + public function getGroupList() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + + $where = [ + ['wg.deleteTime', '=', 0], + ['wg.companyId', '=', $this->request->userInfo['companyId']], + ]; + + if (!empty($keyword)) { + $where[] = ['wg.name', 'like', '%' . $keyword . '%']; + } + + $query = Db::name('wechat_group')->alias('wg') + ->join('wechat_account wa', 'wa.wechatId = wg.ownerWechatId') + ->where($where); + + $total = $query->count(); + $list = $query->order('wg.id', 'desc') + ->field('wg.id,wg.name as groupName,wg.ownerWechatId,wa.nickName,wg.createTime,wa.avatar,wa.alias,wg.avatar as groupAvatar') + ->page($page, $limit) + ->select(); + + // 优化:格式化时间,头像兜底 + $defaultGroupAvatar = ''; + $defaultAvatar = ''; + foreach ($list as &$item) { + $item['createTime'] = $item['createTime'] ? date('Y-m-d H:i:s', $item['createTime']) : ''; + $item['groupAvatar'] = $item['groupAvatar'] ?: $defaultGroupAvatar; + $item['avatar'] = $item['avatar'] ?: $defaultAvatar; + } + + return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]); + } + + /** + * 获取流量池列表 + * @return \think\response\Json + */ + public function getTrafficPoolList() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + $companyId = $this->request->userInfo['companyId']; + + $baseQuery = Db::name('traffic_source_package')->alias('tsp') + ->where('tsp.isDel', 0) + ->whereIn('tsp.companyId', [$companyId, 0]); + + if (!empty($keyword)) { + $baseQuery->whereLike('tsp.name', '%' . $keyword . '%'); + } + + $total = (clone $baseQuery)->count(); + + $list = $baseQuery + ->leftJoin('traffic_source_package_item tspi', 'tspi.packageId = tsp.id and tspi.isDel = 0') + ->field('tsp.id,tsp.name,tsp.description,tsp.pic,tsp.companyId,COUNT(tspi.id) as itemCount,max(tspi.createTime) as latestImportTime') + ->group('tsp.id') + ->order('tsp.id', 'desc') + ->page($page, $limit) + ->select(); + + foreach ($list as &$item) { + $item['latestImportTime'] = !empty($item['latestImportTime']) ? date('Y-m-d H:i:s', $item['latestImportTime']) : ''; + } + unset($item); + + return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]); + } + + + public function getAccountList() + { + $companyId = $this->request->userInfo['companyId']; + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $query = Db::table('s2_company_account') + ->alias('a') + ->where(['a.departmentId' => $companyId, 'a.status' => 0]) + ->whereNotLike('a.userName', '%_offline%') + ->whereNotLike('a.userName', '%_delete%'); + + $total = $query->count(); + $list = $query->field('a.id,a.userName,a.realName,a.nickname,a.memo') + ->page($page, $limit) + ->select(); + + + return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]); + } + + + /** + * 获取京东联盟导购媒体 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function getJdSocialMedia() + { + $data = Db::name('jd_social_media')->order('id DESC')->select(); + return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]); + } + + /** + * 获取京东联盟广告位 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function getJdPromotionSite() + { + $id = $this->request->param('id', ''); + if (empty($id)) { + return json(['code' => 500, 'msg' => '参数缺失']); + } + + $data = Db::name('jd_promotion_site')->where('jdSocialMediaId', $id)->order('id DESC')->select(); + return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]); + } + + + //京东转链-京推推 + public function changeLink($content = '', $positionid = '') + { + $unionId = Env::get('jd.unionId', ''); + $jttAppId = Env::get('jd.jttAppId', ''); + $appKey = Env::get('jd.appKey', ''); + $apiUrl = Env::get('jd.apiUrl', ''); + + $content = !empty($content) ? $content : $this->request->param('content', ''); + $positionid = !empty($positionid) ? $positionid : $this->request->param('positionid', ''); + + + if (empty($content)) { + return json_encode(['code' => 500, 'msg' => '转链的内容为空']); + } + + // 验证是否包含链接 + if (!$this->containsLink($content)) { + return json_encode(['code' => 500, 'msg' => '内容中未检测到有效链接']); + } + + if (empty($unionId) || empty($jttAppId) || empty($appKey) || empty($apiUrl)) { + return json_encode(['code' => 500, 'msg' => '参数缺失']); + } + $params = [ + 'unionid' => $unionId, + 'content' => $content, + 'appid' => $jttAppId, + 'appkey' => $appKey, + 'v' => 'v2' + ]; + + if (!empty($positionid)) { + $params['positionid'] = $positionid; + } + + $res = requestCurl($apiUrl, $params, 'GET', [], 'json'); + $res = json_decode($res, true); + if (empty($res)) { + return json_encode(['code' => 500, 'msg' => '未知错误']); + } + $result = $res['result']; + if ($res['return'] == 0) { + return json_encode(['code' => 200, 'data' => $result['chain_content'], 'msg' => $result['msg']]); + } else { + return json_encode(['code' => 500, 'msg' => $result['msg']]); + } + } + + + public function getTrafficList() + { + $companyId = $this->request->userInfo['companyId']; + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + $workbenchId = $this->request->param('workbenchId', ''); + $isRecycle = $this->request->param('isRecycle', ''); + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + $workbench = Db::name('workbench')->where(['id' => $workbenchId, 'isDel' => 0, 'companyId' => $companyId, 'type' => 5])->find(); + + if (empty($workbench)) { + return json(['code' => 400, 'msg' => '该任务不存在或已删除']); + } + $query = Db::name('workbench_traffic_config_item')->alias('wtc') + ->join(['s2_wechat_friend' => 'wf'], 'wtc.wechatFriendId = wf.id') + ->join('users u', 'wtc.wechatAccountId = u.s2_accountId', 'left') + ->field([ + 'wtc.id', 'wtc.isRecycle', 'wtc.isRecycle', 'wtc.createTime','wtc.recycleTime', + 'wf.wechatId', 'wf.alias', 'wf.nickname', 'wf.avatar', 'wf.gender', 'wf.phone', + 'u.account', 'u.username' + ]) + ->where(['wtc.workbenchId' => $workbenchId]) + ->order('wtc.id DESC'); + + if (!empty($keyword)) { + $query->where('wf.wechatId|wf.alias|wf.nickname|wf.phone|u.account|u.username', 'like', '%' . $keyword . '%'); + } + + if ($isRecycle != '' || $isRecycle != null) { + $query->where('isRecycle',$isRecycle); + } + + + + $total = $query->count(); + $list = $query->page($page, $limit)->select(); + + foreach ($list as &$item) { + $item['createTime'] = date('Y-m-d H:i:s', $item['createTime']); + $item['recycleTime'] = date('Y-m-d H:i:s', $item['recycleTime']); + } + unset($item); + + + $data = [ + 'total' => $total, + 'list' => $list, + ]; + + return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]); + + } + + /** + * 规范化客服微信ID列表 + * @param mixed $ownerWechatIds + * @param WorkbenchGroupPush|null $originalConfig + * @return array + * @throws \Exception + */ + private function normalizeOwnerWechatIds($ownerWechatIds, WorkbenchGroupPush $originalConfig = null): array + { + if ($ownerWechatIds === null) { + $existing = $originalConfig ? $this->decodeJsonArray($originalConfig->ownerWechatIds ?? []) : []; + if (empty($existing)) { + throw new \Exception('请至少选择一个客服微信'); + } + return $existing; + } + + if (!is_array($ownerWechatIds)) { + throw new \Exception('客服参数格式错误'); + } + + $normalized = $this->extractIdList($ownerWechatIds, '客服参数格式错误'); + if (empty($normalized)) { + throw new \Exception('请至少选择一个客服微信'); + } + return $normalized; + } + + /** + * 构建群推送配置数据 + * @param array $param + * @param array $ownerWechatIds + * @param WorkbenchGroupPush|null $originalConfig + * @return array + * @throws \Exception + */ + private function prepareGroupPushData(array $param, array $ownerWechatIds, WorkbenchGroupPush $originalConfig = null): array + { + $targetTypeDefault = $originalConfig ? intval($originalConfig->targetType) : 1; + $targetType = intval($this->getParamValue($param, 'targetType', $targetTypeDefault)) ?: 1; + + $groupPushSubTypeDefault = $originalConfig ? intval($originalConfig->groupPushSubType) : 1; + $groupPushSubType = intval($this->getParamValue($param, 'groupPushSubType', $groupPushSubTypeDefault)) ?: 1; + if (!in_array($groupPushSubType, [1, 2], true)) { + $groupPushSubType = 1; + } + + $data = [ + 'pushType' => $this->toBoolInt($this->getParamValue($param, 'pushType', $originalConfig->pushType ?? 0)), + 'targetType' => $targetType, + 'startTime' => $this->getParamValue($param, 'startTime', $originalConfig->startTime ?? ''), + 'endTime' => $this->getParamValue($param, 'endTime', $originalConfig->endTime ?? ''), + 'maxPerDay' => intval($this->getParamValue($param, 'maxPerDay', $originalConfig->maxPerDay ?? 0)), + 'pushOrder' => $this->getParamValue($param, 'pushOrder', $originalConfig->pushOrder ?? 1), + 'groupPushSubType' => $groupPushSubType, + 'status' => $this->toBoolInt($this->getParamValue($param, 'status', $originalConfig->status ?? 0)), + 'socialMediaId' => $this->getParamValue($param, 'socialMediaId', $originalConfig->socialMediaId ?? ''), + 'promotionSiteId' => $this->getParamValue($param, 'promotionSiteId', $originalConfig->promotionSiteId ?? ''), + 'friendIntervalMin' => intval($this->getParamValue($param, 'friendIntervalMin', $originalConfig->friendIntervalMin ?? 10)), + 'friendIntervalMax' => intval($this->getParamValue($param, 'friendIntervalMax', $originalConfig->friendIntervalMax ?? 20)), + 'messageIntervalMin' => intval($this->getParamValue($param, 'messageIntervalMin', $originalConfig->messageIntervalMin ?? 1)), + 'messageIntervalMax' => intval($this->getParamValue($param, 'messageIntervalMax', $originalConfig->messageIntervalMax ?? 12)), + 'isRandomTemplate' => $this->toBoolInt($this->getParamValue($param, 'isRandomTemplate', $originalConfig->isRandomTemplate ?? 0)), + 'ownerWechatIds' => json_encode($ownerWechatIds, JSON_UNESCAPED_UNICODE), + ]; + + if ($data['friendIntervalMin'] > $data['friendIntervalMax']) { + throw new \Exception('目标间最小间隔不能大于最大间隔'); + } + if ($data['messageIntervalMin'] > $data['messageIntervalMax']) { + throw new \Exception('消息间最小间隔不能大于最大间隔'); + } + + $contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : []; + $contentGroupsParam = $this->getParamValue($param, 'contentGroups', null); + $contentGroups = $contentGroupsParam !== null + ? $this->extractIdList($contentGroupsParam, '内容库参数格式错误') + : $contentGroupsExisting; + $data['contentLibraries'] = json_encode($contentGroups, JSON_UNESCAPED_UNICODE); + + $postPushTagsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->postPushTags ?? []) : []; + $postPushTagsParam = $this->getParamValue($param, 'postPushTags', null); + $postPushTags = $postPushTagsParam !== null + ? $this->extractIdList($postPushTagsParam, '推送标签参数格式错误') + : $postPushTagsExisting; + $data['postPushTags'] = json_encode($postPushTags, JSON_UNESCAPED_UNICODE); + + if ($targetType === 1) { + $data['isLoop'] = $this->toBoolInt($this->getParamValue($param, 'isLoop', $originalConfig->isLoop ?? 0)); + + $groupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->groups ?? []) : []; + $wechatGroups = array_key_exists('wechatGroups', $param) + ? $this->extractIdList($param['wechatGroups'], '群参数格式错误') + : $groupsExisting; + if (empty($wechatGroups)) { + throw new \Exception('群推送必须选择微信群'); + } + $data['groups'] = json_encode($wechatGroups, JSON_UNESCAPED_UNICODE); + $data['friends'] = json_encode([], JSON_UNESCAPED_UNICODE); + $data['trafficPools'] = json_encode([], JSON_UNESCAPED_UNICODE); + + if ($groupPushSubType === 2) { + $announcementContent = $this->getParamValue($param, 'announcementContent', $originalConfig->announcementContent ?? ''); + if (empty($announcementContent)) { + throw new \Exception('群公告必须输入公告内容'); + } + $enableAiRewrite = $this->toBoolInt($this->getParamValue($param, 'enableAiRewrite', $originalConfig->enableAiRewrite ?? 0)); + $aiRewritePrompt = trim((string)$this->getParamValue($param, 'aiRewritePrompt', $originalConfig->aiRewritePrompt ?? '')); + if ($enableAiRewrite === 1 && $aiRewritePrompt === '') { + throw new \Exception('启用AI智能话术改写时,必须输入改写提示词'); + } + $data['announcementContent'] = $announcementContent; + $data['enableAiRewrite'] = $enableAiRewrite; + $data['aiRewritePrompt'] = $aiRewritePrompt; + } else { + $data['groupPushSubType'] = 1; + $data['announcementContent'] = ''; + $data['enableAiRewrite'] = 0; + $data['aiRewritePrompt'] = ''; + } + } else { + $data['isLoop'] = 0; + $friendsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->friends ?? []) : []; + $trafficPoolsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->trafficPools ?? []) : []; + + $friendTargets = array_key_exists('wechatFriends', $param) + ? $this->extractIdList($param['wechatFriends'], '好友参数格式错误') + : $friendsExisting; + $trafficPools = array_key_exists('trafficPools', $param) + ? $this->extractIdList($param['trafficPools'], '流量池参数格式错误') + : $trafficPoolsExisting; + + if (empty($friendTargets) && empty($trafficPools)) { + throw new \Exception('好友推送需至少选择好友或流量池'); + } + + $data['friends'] = json_encode($friendTargets, JSON_UNESCAPED_UNICODE); + $data['trafficPools'] = json_encode($trafficPools, JSON_UNESCAPED_UNICODE); + $data['groups'] = json_encode([], JSON_UNESCAPED_UNICODE); + $data['groupPushSubType'] = 1; + $data['announcementContent'] = ''; + $data['enableAiRewrite'] = 0; + $data['aiRewritePrompt'] = ''; + } + + return $data; + } + + /** + * 获取参数值,若不存在则返回默认值 + * @param array $param + * @param string $key + * @param mixed $default + * @return mixed + */ + private function getParamValue(array $param, string $key, $default) + { + return array_key_exists($key, $param) ? $param[$key] : $default; + } + + /** + * 将值转换为整型布尔 + * @param mixed $value + * @return int + */ + private function toBoolInt($value): int + { + return empty($value) ? 0 : 1; + } + + /** + * 从参数中提取ID列表 + * @param mixed $items + * @param string $errorMessage + * @return array + * @throws \Exception + */ + private function extractIdList($items, string $errorMessage = '参数格式错误'): array + { + if (!is_array($items)) { + throw new \Exception($errorMessage); + } + + $ids = []; + foreach ($items as $item) { + if (is_array($item) && isset($item['id'])) { + $item = $item['id']; + } + if ($item === '' || $item === null) { + continue; + } + $ids[] = $item; + } + + return array_values(array_unique($ids)); + } + + /** + * 解码JSON数组 + * @param mixed $value + * @return array + */ + private function decodeJsonArray($value): array + { + if (empty($value)) { + return []; + } + if (is_array($value)) { + return $value; + } + $decoded = json_decode($value, true); + return is_array($decoded) ? $decoded : []; + } + + /** + * 验证内容是否包含链接 + * @param string $content 要检测的内容 + * @return bool + */ + private function containsLink($content) + { + // 定义各种链接的正则表达式模式 + $patterns = [ + // HTTP/HTTPS链接 + '/https?:\/\/[^\s]+/i', + // 京东商品链接 + '/item\.jd\.com\/\d+/i', + // 京东短链接 + '/u\.jd\.com\/[a-zA-Z0-9]+/i', + // 淘宝商品链接 + '/item\.taobao\.com\/item\.htm\?id=\d+/i', + // 天猫商品链接 + '/detail\.tmall\.com\/item\.htm\?id=\d+/i', + // 淘宝短链接 + '/m\.tb\.cn\/[a-zA-Z0-9]+/i', + // 拼多多链接 + '/mobile\.yangkeduo\.com\/goods\.html\?goods_id=\d+/i', + // 苏宁易购链接 + '/product\.suning\.com\/\d+\/\d+\.html/i', + // 通用域名模式(包含常见电商域名) + '/(?:jd|taobao|tmall|yangkeduo|suning|amazon|dangdang)\.com[^\s]*/i', + // 通用短链接模式 + '/[a-zA-Z0-9-]+\.[a-zA-Z]{2,}\/[a-zA-Z0-9\-._~:\/?#\[\]@!$&\'()*+,;=]+/i' + ]; + + // 遍历所有模式进行匹配 + foreach ($patterns as $pattern) { + if (preg_match($pattern, $content)) { + return true; + } + } + + return false; + } + + + /** + * 获取通讯录导入记录列表 + * @return \think\response\Json + */ + public function getImportContact() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $workbenchId = $this->request->param('workbenchId', 0); + + $where = [ + ['wici.workbenchId', '=', $workbenchId] + ]; + + // 查询发布记录 + $list = Db::name('workbench_import_contact_item')->alias('wici') + ->join('traffic_pool tp', 'tp.id = wici.poolId', 'left') + ->join('traffic_source tc', 'tc.identifier = tp.identifier', 'left') + ->join('wechat_account wa', 'wa.wechatId = tp.wechatId', 'left') + ->field([ + 'wici.id', + 'wici.workbenchId', + 'wici.createTime', + 'tp.identifier', + 'tp.mobile', + 'tp.wechatId', + 'tc.name', + 'wa.nickName', + 'wa.avatar', + 'wa.alias', + ]) + ->where($where) + ->order('tc.name DESC,wici.createTime DESC') + ->group('tp.identifier') + ->page($page, $limit) + ->select(); + + foreach ($list as &$item) { + $item['createTime'] = date('Y-m-d H:i:s', $item['createTime']); + } + + + // 获取总记录数 + $total = Db::name('workbench_import_contact_item')->alias('wici') + ->where($where) + ->count(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + ] + ]); + } + + /** + * 获取群发统计数据 + * @return \think\response\Json + */ + public function getGroupPushStats() + { + $workbenchId = $this->request->param('workbenchId', 0); + $timeRange = $this->request->param('timeRange', '7'); // 默认最近7天 + $contentLibraryIds = $this->request->param('contentLibraryIds', ''); // 话术组筛选 + $userId = $this->request->userInfo['id']; + + // 如果指定了工作台ID,则验证权限 + if (!empty($workbenchId)) { + $workbench = Workbench::where([ + ['id', '=', $workbenchId], + ['userId', '=', $userId], + ['type', '=', self::TYPE_GROUP_PUSH], + ['isDel', '=', 0] + ])->find(); + + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + } + + // 计算时间范围 + $days = intval($timeRange); + $startTime = strtotime(date('Y-m-d 00:00:00', strtotime("-{$days} days"))); + $endTime = time(); + + // 构建查询条件 + $where = [ + ['wgpi.createTime', '>=', $startTime], + ['wgpi.createTime', '<=', $endTime] + ]; + + // 如果指定了工作台ID,则限制查询范围 + if (!empty($workbenchId)) { + $where[] = ['wgpi.workbenchId', '=', $workbenchId]; + } else { + // 如果没有指定工作台ID,则查询当前用户的所有群推送工作台 + $workbenchIds = Workbench::where([ + ['userId', '=', $userId], + ['type', '=', self::TYPE_GROUP_PUSH], + ['isDel', '=', 0] + ])->column('id'); + + if (empty($workbenchIds)) { + // 如果没有工作台,返回空结果 + $workbenchIds = [-1]; + } + $where[] = ['wgpi.workbenchId', 'in', $workbenchIds]; + } + + // 话术组筛选 - 先获取符合条件的内容ID列表 + $contentIds = null; + if (!empty($contentLibraryIds)) { + $libraryIds = is_array($contentLibraryIds) ? $contentLibraryIds : explode(',', $contentLibraryIds); + $libraryIds = array_filter(array_map('intval', $libraryIds)); + if (!empty($libraryIds)) { + // 查询符合条件的内容ID + $contentIds = Db::name('content_item') + ->whereIn('libraryId', $libraryIds) + ->column('id'); + if (empty($contentIds)) { + // 如果没有符合条件的内容,返回空结果 + $contentIds = [-1]; // 使用不存在的ID,确保查询结果为空 + } + } + } + + // 1. 基础统计:触达率、回复率、平均回复时间、链接点击率 + $stats = $this->calculateBasicStats($workbenchId, $where, $startTime, $endTime, $contentIds); + + // 2. 话术组对比 + $contentLibraryComparison = $this->getContentLibraryComparison($workbenchId, $where, $startTime, $endTime, $contentIds); + + // 3. 时段分析 + $timePeriodAnalysis = $this->getTimePeriodAnalysis($workbenchId, $where, $startTime, $endTime, $contentIds); + + // 4. 互动深度(可选,需要更多数据) + $interactionDepth = $this->getInteractionDepth($workbenchId, $where, $startTime, $endTime, $contentIds); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'basicStats' => $stats, + 'contentLibraryComparison' => $contentLibraryComparison, + 'timePeriodAnalysis' => $timePeriodAnalysis, + 'interactionDepth' => $interactionDepth + ] + ]); + } + + /** + * 计算基础统计数据 + */ + private function calculateBasicStats($workbenchId, $where, $startTime, $endTime, $contentIds = null) + { + // 获取工作台配置,计算计划发送数 + // 如果 workbenchId 为空,则查询所有工作台的配置 + $configQuery = WorkbenchGroupPush::alias('wgp') + ->join('workbench w', 'w.id = wgp.workbenchId', 'left') + ->where('w.type', self::TYPE_GROUP_PUSH) + ->where('w.isDel', 0); + + if (!empty($workbenchId)) { + $configQuery->where('wgp.workbenchId', $workbenchId); + } else { + // 如果没有指定工作台ID,需要从 where 条件中获取 workbenchId 列表 + $workbenchIdCondition = null; + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') { + if ($condition[1] === 'in' && is_array($condition[2])) { + $workbenchIdCondition = $condition[2]; + break; + } elseif ($condition[1] === '=') { + $workbenchIdCondition = [$condition[2]]; + break; + } + } + } + if ($workbenchIdCondition) { + $configQuery->whereIn('wgp.workbenchId', $workbenchIdCondition); + } + } + + $configs = $configQuery->select(); + $targetType = 1; // 默认值 + if (!empty($configs)) { + // 如果只有一个配置,使用它的 targetType;如果有多个,默认使用1 + $targetType = intval($configs[0]->targetType ?? 1); + } + + // 计划发送数(根据配置计算) + $plannedSend = 0; + if (!empty($configs)) { + $days = ceil(($endTime - $startTime) / 86400); + foreach ($configs as $config) { + $maxPerDay = intval($config->maxPerDay ?? 0); + $configTargetType = intval($config->targetType ?? 1); + if ($configTargetType == 1) { + // 群推送:计划发送数 = 每日推送次数 * 天数 * 群数量 + $groups = $this->decodeJsonArray($config->groups ?? []); + $plannedSend += $maxPerDay * $days * count($groups); + } else { + // 好友推送:计划发送数 = 每日推送人数 * 天数 + $plannedSend += $maxPerDay * $days; + } + } + } + + // 构建查询条件 + $queryWhere = $where; + if ($contentIds !== null) { + $queryWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + + // 实际成功发送数(从推送记录表统计) + $successSend = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($queryWhere) + ->count(); + + // 触达率 = 成功发送数 / 计划发送数 + $reachRate = $plannedSend > 0 ? round(($successSend / $plannedSend) * 100, 1) : 0; + + // 获取发送记录列表,用于查询回复 + $sentItemIds = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($queryWhere) + ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId') + ->select(); + + // 回复统计(通过消息表查询) + $replyStats = $this->calculateReplyStats($sentItemIds, $targetType, $startTime, $endTime); + + // 链接点击统计 + $clickStats = $this->calculateClickStats($sentItemIds, $targetType, $startTime, $endTime); + + // 计算本月对比数据(简化处理,实际应该查询上个月同期数据) + $currentMonthStart = strtotime(date('Y-m-01 00:00:00')); + $lastMonthStart = strtotime(date('Y-m-01 00:00:00', strtotime('-1 month'))); + $lastMonthEnd = $currentMonthStart - 1; + + // 获取本月统计数据(避免递归调用) + $currentMonthWhere = [ + ['wgpi.createTime', '>=', $currentMonthStart] + ]; + // 复制 workbenchId 条件 + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') { + $currentMonthWhere[] = $condition; + break; + } + } + if ($contentIds !== null) { + $currentMonthWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + $currentMonthSend = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($currentMonthWhere) + ->count(); + + // 获取本月配置 + $currentMonthConfigQuery = WorkbenchGroupPush::alias('wgp') + ->join('workbench w', 'w.id = wgp.workbenchId', 'left') + ->where('w.type', self::TYPE_GROUP_PUSH) + ->where('w.isDel', 0); + if (!empty($workbenchId)) { + $currentMonthConfigQuery->where('wgp.workbenchId', $workbenchId); + } else { + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId' && $condition[1] === 'in') { + $currentMonthConfigQuery->whereIn('wgp.workbenchId', $condition[2]); + break; + } + } + } + $currentMonthConfigs = $currentMonthConfigQuery->select(); + $currentMonthPlanned = 0; + if (!empty($currentMonthConfigs)) { + $currentMonthDays = ceil(($endTime - $currentMonthStart) / 86400); + foreach ($currentMonthConfigs as $currentMonthConfig) { + $currentMonthMaxPerDay = intval($currentMonthConfig->maxPerDay ?? 0); + $currentMonthTargetType = intval($currentMonthConfig->targetType ?? 1); + if ($currentMonthTargetType == 1) { + $currentMonthGroups = $this->decodeJsonArray($currentMonthConfig->groups ?? []); + $currentMonthPlanned += $currentMonthMaxPerDay * $currentMonthDays * count($currentMonthGroups); + } else { + $currentMonthPlanned += $currentMonthMaxPerDay * $currentMonthDays; + } + } + } + $currentMonthReachRate = $currentMonthPlanned > 0 ? round(($currentMonthSend / $currentMonthPlanned) * 100, 1) : 0; + + // 获取上个月统计数据 + $lastMonthWhere = [ + ['wgpi.createTime', '>=', $lastMonthStart], + ['wgpi.createTime', '<=', $lastMonthEnd] + ]; + // 复制 workbenchId 条件 + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') { + $lastMonthWhere[] = $condition; + break; + } + } + if ($contentIds !== null) { + $lastMonthWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + $lastMonthSend = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($lastMonthWhere) + ->count(); + + // 获取上个月配置 + $lastMonthConfigQuery = WorkbenchGroupPush::alias('wgp') + ->join('workbench w', 'w.id = wgp.workbenchId', 'left') + ->where('w.type', self::TYPE_GROUP_PUSH) + ->where('w.isDel', 0); + if (!empty($workbenchId)) { + $lastMonthConfigQuery->where('wgp.workbenchId', $workbenchId); + } else { + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId' && $condition[1] === 'in') { + $lastMonthConfigQuery->whereIn('wgp.workbenchId', $condition[2]); + break; + } + } + } + $lastMonthConfigs = $lastMonthConfigQuery->select(); + + $lastMonthPlanned = 0; + if (!empty($lastMonthConfigs)) { + $lastMonthDays = ceil(($lastMonthEnd - $lastMonthStart) / 86400); + foreach ($lastMonthConfigs as $lastMonthConfig) { + $lastMonthMaxPerDay = intval($lastMonthConfig->maxPerDay ?? 0); + $lastMonthTargetType = intval($lastMonthConfig->targetType ?? 1); + if ($lastMonthTargetType == 1) { + $lastMonthGroups = $this->decodeJsonArray($lastMonthConfig->groups ?? []); + $lastMonthPlanned += $lastMonthMaxPerDay * $lastMonthDays * count($lastMonthGroups); + } else { + $lastMonthPlanned += $lastMonthMaxPerDay * $lastMonthDays; + } + } + } + $lastMonthReachRate = $lastMonthPlanned > 0 ? round(($lastMonthSend / $lastMonthPlanned) * 100, 1) : 0; + + // 获取上个月的回复和点击统计(简化处理) + $lastMonthSentItems = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($lastMonthWhere) + ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId') + ->select(); + $lastMonthReplyStats = $this->calculateReplyStats($lastMonthSentItems, $targetType, $lastMonthStart, $lastMonthEnd); + $lastMonthClickStats = $this->calculateClickStats($lastMonthSentItems, $targetType, $lastMonthStart, $lastMonthEnd); + + return [ + 'reachRate' => [ + 'value' => $reachRate, + 'trend' => round($reachRate - $lastMonthReachRate, 1), + 'unit' => '%', + 'description' => '成功发送/计划发送' + ], + 'replyRate' => [ + 'value' => $replyStats['replyRate'], + 'trend' => round($replyStats['replyRate'] - $lastMonthReplyStats['replyRate'], 1), + 'unit' => '%', + 'description' => '收到回复/成功发送' + ], + 'avgReplyTime' => [ + 'value' => $replyStats['avgReplyTime'], + 'trend' => round($lastMonthReplyStats['avgReplyTime'] - $replyStats['avgReplyTime'], 0), + 'unit' => '分钟', + 'description' => '从发送到回复的平均时长' + ], + 'clickRate' => [ + 'value' => $clickStats['clickRate'], + 'trend' => round($clickStats['clickRate'] - $lastMonthClickStats['clickRate'], 1), + 'unit' => '%', + 'description' => '点击链接/成功发送' + ], + 'plannedSend' => $plannedSend, + 'successSend' => $successSend, + 'replyCount' => $replyStats['replyCount'], + 'clickCount' => $clickStats['clickCount'] + ]; + } + + /** + * 计算回复统计 + */ + private function calculateReplyStats($sentItems, $targetType, $startTime, $endTime) + { + if (empty($sentItems)) { + return ['replyRate' => 0, 'avgReplyTime' => 0, 'replyCount' => 0]; + } + + $replyCount = 0; + $totalReplyTime = 0; + $replyTimes = []; + + foreach ($sentItems as $item) { + $itemArray = is_array($item) ? $item : (array)$item; + $sendTime = $itemArray['createTime'] ?? 0; + $accountId = $itemArray['wechatAccountId'] ?? 0; + + if ($targetType == 1) { + // 群推送:查找群内回复消息 + $groupId = $itemArray['groupId'] ?? 0; + $group = Db::name('wechat_group')->where('id', $groupId)->find(); + if ($group) { + $replyMsg = Db::table('s2_wechat_message') + ->where('wechatChatroomId', $group['chatroomId']) + ->where('wechatAccountId', $accountId) + ->where('isSend', 0) // 接收的消息 + ->where('wechatTime', '>', $sendTime) + ->where('wechatTime', '<=', $sendTime + 86400) // 24小时内回复 + ->order('wechatTime', 'asc') + ->find(); + + if ($replyMsg) { + $replyCount++; + $replyTime = $replyMsg['wechatTime'] - $sendTime; + $replyTimes[] = $replyTime; + $totalReplyTime += $replyTime; + } + } + } else { + // 好友推送:查找好友回复消息 + $friendId = $itemArray['friendId'] ?? 0; + $friend = Db::table('s2_wechat_friend')->where('id', $friendId)->find(); + if ($friend) { + $replyMsg = Db::table('s2_wechat_message') + ->where('wechatFriendId', $friendId) + ->where('wechatAccountId', $accountId) + ->where('isSend', 0) // 接收的消息 + ->where('wechatTime', '>', $sendTime) + ->where('wechatTime', '<=', $sendTime + 86400) // 24小时内回复 + ->order('wechatTime', 'asc') + ->find(); + + if ($replyMsg) { + $replyCount++; + $replyTime = $replyMsg['wechatTime'] - $sendTime; + $replyTimes[] = $replyTime; + $totalReplyTime += $replyTime; + } + } + } + } + + $successSend = count($sentItems); + $replyRate = $successSend > 0 ? round(($replyCount / $successSend) * 100, 1) : 0; + $avgReplyTime = $replyCount > 0 ? round(($totalReplyTime / $replyCount) / 60, 0) : 0; // 转换为分钟 + + return [ + 'replyRate' => $replyRate, + 'avgReplyTime' => $avgReplyTime, + 'replyCount' => $replyCount + ]; + } + + /** + * 计算链接点击统计 + */ + private function calculateClickStats($sentItems, $targetType, $startTime, $endTime) + { + if (empty($sentItems)) { + return ['clickRate' => 0, 'clickCount' => 0]; + } + + $clickCount = 0; + $linkContentIds = []; + + // 获取所有发送的内容ID + foreach ($sentItems as $item) { + $itemArray = is_array($item) ? $item : (array)$item; + $contentId = $itemArray['contentId'] ?? 0; + if ($contentId > 0) { + $linkContentIds[] = $contentId; + } + } + + if (empty($linkContentIds)) { + return ['clickRate' => 0, 'clickCount' => 0]; + } + + // 查询包含链接的内容 + $linkContents = Db::name('content_item') + ->whereIn('id', array_unique($linkContentIds)) + ->where('contentType', 2) // 链接类型 + ->column('id'); + + // 统计发送了链接内容的记录数 + $linkSendCount = 0; + foreach ($sentItems as $item) { + $itemArray = is_array($item) ? $item : (array)$item; + $contentId = $itemArray['contentId'] ?? 0; + if (in_array($contentId, $linkContents)) { + $linkSendCount++; + } + } + + // 简化处理:假设点击率基于链接消息的发送(实际应该从点击追踪系统获取) + // 这里可以根据业务需求调整,比如通过消息中的链接点击事件统计 + $clickCount = $linkSendCount; // 简化处理,实际需要真实的点击数据 + + $successSend = count($sentItems); + $clickRate = $successSend > 0 ? round(($clickCount / $successSend) * 100, 1) : 0; + + return [ + 'clickRate' => $clickRate, + 'clickCount' => $clickCount + ]; + } + + /** + * 获取话术组对比数据 + */ + private function getContentLibraryComparison($workbenchId, $where, $startTime, $endTime, $contentIds = null) + { + $queryWhere = $where; + if ($contentIds !== null) { + $queryWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + + $comparison = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->join('content_item ci', 'ci.id = wgpi.contentId', 'left') + ->join('content_library cl', 'cl.id = ci.libraryId', 'left') + ->where($queryWhere) + ->where('cl.id', '<>', null) + ->field([ + 'cl.id as libraryId', + 'cl.name as libraryName', + 'COUNT(DISTINCT wgpi.id) as pushCount' + ]) + ->group('cl.id, cl.name') + ->select(); + + $result = []; + foreach ($comparison as $item) { + $libraryId = $item['libraryId']; + $pushCount = intval($item['pushCount']); + + // 获取该内容库的详细统计 + $libraryContentIds = Db::name('content_item') + ->where('libraryId', $libraryId) + ->column('id'); + if (empty($libraryContentIds)) { + $libraryContentIds = [-1]; + } + + $libraryWhere = array_merge($where, [['wgpi.contentId', 'in', $libraryContentIds]]); + $librarySentItems = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($libraryWhere) + ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId') + ->select(); + + $config = WorkbenchGroupPush::where('workbenchId', $workbenchId)->find(); + $targetType = $config ? intval($config->targetType) : 1; + + $replyStats = $this->calculateReplyStats($librarySentItems, $targetType, $startTime, $endTime); + $clickStats = $this->calculateClickStats($librarySentItems, $targetType, $startTime, $endTime); + + // 计算转化率(简化处理,实际需要根据业务定义) + $conversionRate = $pushCount > 0 ? round(($replyStats['replyCount'] / $pushCount) * 100, 1) : 0; + + $result[] = [ + 'libraryId' => $libraryId, + 'libraryName' => $item['libraryName'], + 'pushCount' => $pushCount, + 'reachRate' => 100, // 简化处理,实际应该计算 + 'replyRate' => $replyStats['replyRate'], + 'clickRate' => $clickStats['clickRate'], + 'conversionRate' => $conversionRate, + 'avgReplyTime' => $replyStats['avgReplyTime'], + 'level' => $this->getPerformanceLevel($replyStats['replyRate'], $clickStats['clickRate'], $conversionRate) + ]; + } + + // 按回复率排序 + usort($result, function($a, $b) { + return $b['replyRate'] <=> $a['replyRate']; + }); + + return $result; + } + + /** + * 获取性能等级 + */ + private function getPerformanceLevel($replyRate, $clickRate, $conversionRate) + { + $score = ($replyRate * 0.4) + ($clickRate * 0.3) + ($conversionRate * 0.3); + + if ($score >= 40) { + return '优秀'; + } elseif ($score >= 25) { + return '良好'; + } elseif ($score >= 15) { + return '一般'; + } else { + return '待提升'; + } + } + + /** + * 获取时段分析数据 + */ + private function getTimePeriodAnalysis($workbenchId, $where, $startTime, $endTime, $contentIds = null) + { + $queryWhere = $where; + if ($contentIds !== null) { + $queryWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + + $analysis = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($queryWhere) + ->field([ + 'FROM_UNIXTIME(wgpi.createTime, "%H") as hour', + 'COUNT(*) as count' + ]) + ->group('hour') + ->order('hour', 'asc') + ->select(); + + $result = []; + foreach ($analysis as $item) { + $result[] = [ + 'hour' => intval($item['hour']), + 'count' => intval($item['count']) + ]; + } + + return $result; + } + + /** + * 获取互动深度数据 + */ + private function getInteractionDepth($workbenchId, $where, $startTime, $endTime, $contentIds = null) + { + // 简化处理,实际需要更复杂的统计逻辑 + return [ + 'singleReply' => 0, // 单次回复 + 'multipleReply' => 0, // 多次回复 + 'deepInteraction' => 0 // 深度互动 + ]; + } + + /** + * 获取推送历史记录列表 + * @return \think\response\Json + */ + public function getGroupPushHistory() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $workbenchId = $this->request->param('workbenchId', 0); + $keyword = $this->request->param('keyword', ''); + $pushType = $this->request->param('pushType', ''); // 推送类型筛选:''=全部, 'friend'=好友消息, 'group'=群消息, 'announcement'=群公告 + $status = $this->request->param('status', ''); // 状态筛选:''=全部, 'success'=已完成, 'progress'=进行中, 'failed'=失败 + $userId = $this->request->userInfo['id']; + + // 构建工作台查询条件 + $workbenchWhere = [ + ['w.userId', '=', $userId], + ['w.type', '=', self::TYPE_GROUP_PUSH], + ['w.isDel', '=', 0] + ]; + + // 如果指定了工作台ID,则验证权限并限制查询范围 + if (!empty($workbenchId)) { + $workbench = Workbench::where([ + ['id', '=', $workbenchId], + ['userId', '=', $userId], + ['type', '=', self::TYPE_GROUP_PUSH], + ['isDel', '=', 0] + ])->find(); + + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + $workbenchWhere[] = ['w.id', '=', $workbenchId]; + } + + // 1. 先查询所有已执行的推送记录(按推送时间分组) + $pushHistoryQuery = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->join('workbench w', 'w.id = wgpi.workbenchId', 'left') + ->join('workbench_group_push wgp', 'wgp.workbenchId = wgpi.workbenchId', 'left') + ->join('content_item ci', 'ci.id = wgpi.contentId', 'left') + ->join('content_library cl', 'cl.id = ci.libraryId', 'left') + ->where($workbenchWhere) + ->field([ + 'wgpi.workbenchId', + 'w.name as workbenchName', + 'wgpi.contentId', + 'FROM_UNIXTIME(wgpi.createTime, "%Y-%m-%d %H:00:00") as pushTime', + 'wgpi.targetType', + 'wgp.groupPushSubType', + 'MIN(wgpi.createTime) as createTime', + 'COUNT(DISTINCT wgpi.id) as totalCount', + 'cl.name as contentLibraryName' + ]) + ->group('wgpi.workbenchId, wgpi.contentId, pushTime, wgpi.targetType, wgp.groupPushSubType'); + + if (!empty($keyword)) { + $pushHistoryQuery->where('w.name|cl.name|ci.content', 'like', '%' . $keyword . '%'); + } + + $pushHistoryList = $pushHistoryQuery->order('createTime', 'desc')->select(); + + // 2. 查询所有任务(包括未执行的) + $allTasksQuery = Db::name('workbench') + ->alias('w') + ->join('workbench_group_push wgp', 'wgp.workbenchId = w.id', 'left') + ->where($workbenchWhere) + ->field([ + 'w.id as workbenchId', + 'w.name as workbenchName', + 'w.createTime', + 'wgp.targetType', + 'wgp.groupPushSubType', + 'wgp.groups', + 'wgp.friends', + 'wgp.trafficPools' + ]); + + if (!empty($keyword)) { + $allTasksQuery->where('w.name', 'like', '%' . $keyword . '%'); + } + + $allTasks = $allTasksQuery->select(); + + // 3. 合并数据:已执行的推送记录 + 未执行的任务 + $resultList = []; + $executedWorkbenchIds = []; + + // 处理已执行的推送记录 + foreach ($pushHistoryList as $item) { + $itemWorkbenchId = $item['workbenchId']; + $contentId = $item['contentId']; + $pushTime = $item['pushTime']; + $targetType = intval($item['targetType']); + $groupPushSubType = isset($item['groupPushSubType']) ? intval($item['groupPushSubType']) : 1; + + // 标记该工作台已有执行记录 + if (!in_array($itemWorkbenchId, $executedWorkbenchIds)) { + $executedWorkbenchIds[] = $itemWorkbenchId; + } + + // 将时间字符串转换为时间戳范围(小时级别) + $pushTimeStart = strtotime($pushTime); + $pushTimeEnd = $pushTimeStart + 3600; // 一小时后 + + // 获取该次推送的详细统计 + $pushWhere = [ + ['wgpi.workbenchId', '=', $itemWorkbenchId], + ['wgpi.contentId', '=', $contentId], + ['wgpi.createTime', '>=', $pushTimeStart], + ['wgpi.createTime', '<', $pushTimeEnd], + ['wgpi.targetType', '=', $targetType] + ]; + + // 目标数量 + if ($targetType == 1) { + // 群推送:统计群数量 + $targetCount = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($pushWhere) + ->where('wgpi.groupId', '<>', null) + ->distinct(true) + ->count('wgpi.groupId'); + } else { + // 好友推送:统计好友数量 + $targetCount = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($pushWhere) + ->where('wgpi.friendId', '<>', null) + ->distinct(true) + ->count('wgpi.friendId'); + } + + // 成功数和失败数(简化处理,实际需要根据发送状态判断) + $successCount = intval($item['totalCount']); // 简化处理 + $failCount = 0; // 简化处理,实际需要从发送状态获取 + + // 状态判断 + $itemStatus = $successCount > 0 ? 'success' : 'failed'; + if ($failCount > 0 && $successCount > 0) { + $itemStatus = 'partial'; + } + + // 推送类型判断 + $pushTypeText = ''; + $pushTypeCode = ''; + if ($targetType == 1) { + // 群推送 + if ($groupPushSubType == 2) { + $pushTypeText = '群公告'; + $pushTypeCode = 'announcement'; + } else { + $pushTypeText = '群消息'; + $pushTypeCode = 'group'; + } + } else { + // 好友推送 + $pushTypeText = '好友消息'; + $pushTypeCode = 'friend'; + } + + $resultList[] = [ + 'workbenchId' => $itemWorkbenchId, + 'taskName' => $item['workbenchName'] ?? '', + 'pushType' => $pushTypeText, + 'pushTypeCode' => $pushTypeCode, + 'targetCount' => $targetCount, + 'successCount' => $successCount, + 'failCount' => $failCount, + 'status' => $itemStatus, + 'statusText' => $this->getStatusText($itemStatus), + 'createTime' => date('Y-m-d H:i:s', $item['createTime']), + 'contentLibraryName' => $item['contentLibraryName'] ?? '' + ]; + } + + // 处理未执行的任务 + foreach ($allTasks as $task) { + $taskWorkbenchId = $task['workbenchId']; + + // 如果该任务已有执行记录,跳过(避免重复) + if (in_array($taskWorkbenchId, $executedWorkbenchIds)) { + continue; + } + + $targetType = isset($task['targetType']) ? intval($task['targetType']) : 1; + $groupPushSubType = isset($task['groupPushSubType']) ? intval($task['groupPushSubType']) : 1; + + // 计算目标数量(从配置中获取) + $targetCount = 0; + if ($targetType == 1) { + // 群推送:统计配置的群数量 + $groups = json_decode($task['groups'] ?? '[]', true); + $targetCount = is_array($groups) ? count($groups) : 0; + } else { + // 好友推送:统计配置的好友数量或流量池数量 + $friends = json_decode($task['friends'] ?? '[]', true); + $trafficPools = json_decode($task['trafficPools'] ?? '[]', true); + $friendCount = is_array($friends) ? count($friends) : 0; + $poolCount = is_array($trafficPools) ? count($trafficPools) : 0; + // 如果配置了流量池,目标数量暂时显示为流量池数量(实际数量需要从流量池中统计) + $targetCount = $friendCount > 0 ? $friendCount : $poolCount; + } + + // 推送类型判断 + $pushTypeText = ''; + $pushTypeCode = ''; + if ($targetType == 1) { + // 群推送 + if ($groupPushSubType == 2) { + $pushTypeText = '群公告'; + $pushTypeCode = 'announcement'; + } else { + $pushTypeText = '群消息'; + $pushTypeCode = 'group'; + } + } else { + // 好友推送 + $pushTypeText = '好友消息'; + $pushTypeCode = 'friend'; + } + + $resultList[] = [ + 'workbenchId' => $taskWorkbenchId, + 'taskName' => $task['workbenchName'] ?? '', + 'pushType' => $pushTypeText, + 'pushTypeCode' => $pushTypeCode, + 'targetCount' => $targetCount, + 'successCount' => 0, + 'failCount' => 0, + 'status' => 'pending', + 'statusText' => '进行中', + 'createTime' => date('Y-m-d H:i:s', $task['createTime']), + 'contentLibraryName' => '' + ]; + } + + // 应用筛选条件 + $filteredList = []; + foreach ($resultList as $item) { + // 推送类型筛选 + if (!empty($pushType)) { + if ($pushType === 'friend' && $item['pushTypeCode'] !== 'friend') { + continue; + } + if ($pushType === 'group' && $item['pushTypeCode'] !== 'group') { + continue; + } + if ($pushType === 'announcement' && $item['pushTypeCode'] !== 'announcement') { + continue; + } + } + + // 状态筛选 + if (!empty($status)) { + if ($status === 'success' && $item['status'] !== 'success') { + continue; + } + if ($status === 'progress') { + // 进行中:包括 partial 和 pending + if ($item['status'] !== 'partial' && $item['status'] !== 'pending') { + continue; + } + } + if ($status === 'failed' && $item['status'] !== 'failed') { + continue; + } + } + + $filteredList[] = $item; + } + + // 按创建时间倒序排序 + usort($filteredList, function($a, $b) { + return strtotime($b['createTime']) - strtotime($a['createTime']); + }); + + // 分页处理 + $total = count($filteredList); + $offset = ($page - 1) * $limit; + $list = array_slice($filteredList, $offset, $limit); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + + /** + * 获取状态文本 + * @param string $status 状态码 + * @return string 状态文本 + */ + private function getStatusText($status) + { + $statusMap = [ + 'success' => '已完成', + 'partial' => '进行中', + 'pending' => '进行中', + 'failed' => '失败' + ]; + return $statusMap[$status] ?? '未知'; + } + + /** + * 获取已创建的群列表(自动建群) + * @return \think\response\Json + */ + public function getCreatedGroupsList() + { + $workbenchId = $this->request->param('workbenchId', 0); + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 100); + $keyword = $this->request->param('keyword', ''); + + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '工作台ID不能为空']); + } + + // 验证工作台权限 + $where = [ + ['id', '=', $workbenchId], + ['companyId', '=', $this->request->userInfo['companyId']], + ['type', '=', self::TYPE_GROUP_CREATE], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + $workbench = Workbench::where($where)->find(); + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在或无权限']); + } + + // 获取已创建的群ID列表(状态为成功且groupId不为空) + $groupIds = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('status', 2) // STATUS_SUCCESS = 2 + ->where('groupId', '<>', null) + ->group('groupId') + ->column('groupId'); + + if (empty($groupIds)) { + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => [], + 'total' => 0, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + + // 查询群组详细信息(从s2_wechat_chatroom表查询) + $query = Db::table('s2_wechat_chatroom')->alias('wc') + ->join('wechat_account wa', 'wa.wechatId = wc.wechatAccountWechatId', 'left') + ->where('wc.id', 'in', $groupIds) + ->where('wc.isDeleted', 0); + + // 关键字搜索 + if (!empty($keyword)) { + $query->where(function ($q) use ($keyword) { + $q->where('wc.nickname', 'like', '%' . $keyword . '%') + ->whereOr('wc.chatroomId', 'like', '%' . $keyword . '%') + ->whereOr('wa.nickName', 'like', '%' . $keyword . '%'); + }); + } + + $total = $query->count(); + $list = $query->field('wc.id,wc.nickname as groupName,wc.chatroomId,wc.chatroomAvatar as groupAvatar,wc.wechatAccountWechatId as ownerWechatId,wc.createTime,wc.chatroomOwnerNickname as ownerNickname,wc.chatroomOwnerAvatar as ownerAvatar,wa.alias as ownerAlias') + ->order('wc.createTime', 'desc') + ->page($page, $limit) + ->select(); + + // 统计每个群的成员数量和成员信息 + foreach ($list as &$item) { + // 统计该群的成员数量(从workbench_group_create_item表统计) + $memberCount = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('groupId', $item['id']) + ->where('status', 'in', [2, 4]) // 创建成功和管理员好友已拉入 + ->count(); + + // 获取成员列表(用于显示成员头像) + $memberList = Db::name('workbench_group_create_item')->alias('wgci') + ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wgci.friendId', 'left') + ->where('wgci.workbenchId', $workbenchId) + ->where('wgci.groupId', $item['id']) + ->where('wgci.status', 'in', [2, 4]) + ->field('wf.avatar, wf.wechatId, wf.nickname') + ->order('wgci.createTime', 'asc') + ->limit(10) // 最多显示10个成员头像 + ->select(); + + // 格式化成员头像列表 + $memberAvatars = []; + foreach ($memberList as $member) { + if (!empty($member['avatar'])) { + $memberAvatars[] = [ + 'avatar' => $member['avatar'], + 'wechatId' => $member['wechatId'] ?? '', + 'nickname' => $member['nickname'] ?? '' + ]; + } + } + + // 计算剩余成员数(用于显示"+XX") + $remainingCount = $memberCount > count($memberAvatars) ? $memberCount - count($memberAvatars) : 0; + + // 格式化返回数据 + $item['memberCount'] = $memberCount; + $item['memberCountText'] = $memberCount . '人'; // 格式化为"XX人" + $item['createTime'] = !empty($item['createTime']) ? date('Y-m-d', $item['createTime']) : ''; // 格式化为"YYYY-MM-DD" + $item['memberAvatars'] = $memberAvatars; // 成员头像列表(最多10个) + $item['remainingCount'] = $remainingCount; // 剩余成员数(用于显示"+XX") + + // 保留原有字段,但调整格式 + $item['groupName'] = $item['groupName'] ?? ''; + $item['groupAvatar'] = $item['groupAvatar'] ?? ''; + } + unset($item); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + + /** + * 获取已创建群的详情(自动建群) + * @return \think\response\Json + */ + public function getCreatedGroupDetail() + { + $workbenchId = $this->request->param('workbenchId', 0); + $groupId = $this->request->param('groupId', 0); + + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '工作台ID不能为空']); + } + + if (empty($groupId)) { + return json(['code' => 400, 'msg' => '群ID不能为空']); + } + + // 验证工作台权限 + $where = [ + ['id', '=', $workbenchId], + ['companyId', '=', $this->request->userInfo['companyId']], + ['type', '=', self::TYPE_GROUP_CREATE], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + $workbench = Workbench::where($where)->find(); + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在或无权限']); + } + + // 验证该群是否属于该工作台 + $groupItem = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('groupId', $groupId) + ->where('status', 2) // STATUS_SUCCESS = 2 + ->find(); + + if (empty($groupItem)) { + return json(['code' => 404, 'msg' => '群不存在或不属于该工作台']); + } + + // 查询群基本信息(从s2_wechat_chatroom表查询) + $group = Db::table('s2_wechat_chatroom')->alias('wc') + ->join('wechat_account wa', 'wa.wechatId = wc.wechatAccountWechatId', 'left') + ->where('wc.id', $groupId) + ->where('wc.isDeleted', 0) + ->field('wc.id,wc.nickname as groupName,wc.chatroomId,wc.chatroomAvatar as groupAvatar,wc.wechatAccountWechatId as ownerWechatId,wc.createTime,wc.chatroomOwnerNickname as ownerNickname,wc.chatroomOwnerAvatar as ownerAvatar,wa.alias as ownerAlias,wc.announce') + ->find(); + + if (empty($group)) { + return json(['code' => 404, 'msg' => '群不存在']); + } + + // 获取chatroomId + $chatroomId = $group['chatroomId'] ?? ''; + if (empty($chatroomId)) { + return json(['code' => 400, 'msg' => '群聊ID不存在']); + } + + // 从s2_wechat_chatroom_member表查询所有成员列表(不限制数量) + $memberList = Db::table('s2_wechat_chatroom_member')->alias('wcm') + ->join(['s2_wechat_friend' => 'wf'], 'wf.wechatId = wcm.wechatId', 'left') + ->where('wcm.chatroomId', $chatroomId) + ->field('wcm.wechatId,wcm.nickname as memberNickname,wcm.avatar as memberAvatar,wcm.conRemark as memberRemark,wcm.alias as memberAlias,wcm.createTime as joinTime,wcm.updateTime,wf.id as friendId,wf.nickname as friendNickname,wf.avatar as friendAvatar') + ->order('wcm.createTime', 'asc') + ->select(); + + // 去重:按wechatId去重,保留第一条记录 + $memberMap = []; + foreach ($memberList as $member) { + $wechatId = $member['wechatId'] ?? ''; + if (!empty($wechatId) && !isset($memberMap[$wechatId])) { + $memberMap[$wechatId] = $member; + } + } + $memberList = array_values($memberMap); // 重新索引数组 + + // 获取在群中的成员wechatId列表(用于判断是否已退群) + $inGroupWechatIds = array_column($memberList, 'wechatId'); + $inGroupWechatIds = array_filter($inGroupWechatIds); // 过滤空值 + + // 获取通过自动建群加入的成员信息(用于判断入群状态和已退群成员) + $autoJoinMemberList = Db::name('workbench_group_create_item') + ->alias('wgci') + ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wgci.friendId', 'left') + ->where('wgci.workbenchId', $workbenchId) + ->where('wgci.groupId', $groupId) + ->where('wgci.status', 'in', [2, 4]) // 创建成功和管理员好友已拉入 + ->field('wf.wechatId,wf.id as friendId,wf.nickname as friendNickname,wf.avatar as friendAvatar,wgci.createTime as autoJoinTime') + ->select(); + + // 去重:按wechatId去重,保留第一条记录 + $autoJoinMemberMap = []; + foreach ($autoJoinMemberList as $autoMember) { + $wechatId = $autoMember['wechatId'] ?? ''; + if (!empty($wechatId) && !isset($autoJoinMemberMap[$wechatId])) { + $autoJoinMemberMap[$wechatId] = $autoMember; + } + } + $autoJoinMemberList = array_values($autoJoinMemberMap); // 重新索引数组 + + // 统计成员总数(包括在群中的成员和已退群的成员) + // 先统计在群中的成员数 + $inGroupCount = count($memberList); + + // 统计已退群的成员数(在自动建群记录中但不在群中的成员) + $quitCount = 0; + if (!empty($autoJoinMemberList)) { + foreach ($autoJoinMemberList as $autoMember) { + $wechatId = $autoMember['wechatId'] ?? ''; + if (!empty($wechatId) && !in_array($wechatId, $inGroupWechatIds)) { + $quitCount++; + } + } + } + + // 总成员数 = 在群中的成员数 + 已退群的成员数 + $memberCount = $inGroupCount + $quitCount; + + // 获取自动建群加入的成员wechatId列表(用于判断入群状态) + $autoJoinWechatIds = array_column($autoJoinMemberList, 'wechatId'); + $autoJoinWechatIds = array_filter($autoJoinWechatIds); // 过滤空值 + + // 格式化在群中的成员列表 + $members = []; + $addedWechatIds = []; // 用于记录已添加的成员wechatId,避免重复 + $ownerWechatId = $group['ownerWechatId'] ?? ''; + foreach ($memberList as $member) { + $wechatId = $member['wechatId'] ?? ''; + + // 跳过空wechatId和已添加的成员 + if (empty($wechatId) || in_array($wechatId, $addedWechatIds)) { + continue; + } + + // 标记为已添加 + $addedWechatIds[] = $wechatId; + + // 根据wechatId判断是否为群主 + $isOwner = (!empty($ownerWechatId) && $wechatId == $ownerWechatId) ? 1 : 0; + + // 判断入群状态:如果在自动建群记录中,说明是通过自动建群加入的;否则是其他方式加入的 + $joinStatus = in_array($wechatId, $autoJoinWechatIds) ? 'auto' : 'manual'; + + // 判断是否已退群:如果成员在s2_wechat_chatroom_member表中存在,说明在群中;否则已退群 + // 由于我们已经从s2_wechat_chatroom_member表查询,所以这里都是"在群中"状态 + $isQuit = 0; // 0=在群中,1=已退群 + + // 优先使用friend表的昵称和头像,如果没有则使用member表的 + $nickname = !empty($member['friendNickname']) ? $member['friendNickname'] : ($member['memberNickname'] ?? ''); + $avatar = !empty($member['friendAvatar']) ? $member['friendAvatar'] : ($member['memberAvatar'] ?? ''); + + $members[] = [ + 'friendId' => $member['friendId'] ?? 0, + 'wechatId' => $wechatId, + 'nickname' => $nickname, + 'avatar' => $avatar, + 'alias' => $member['memberAlias'] ?? '', + 'remark' => $member['memberRemark'] ?? '', + 'isOwner' => $isOwner, // 标记群主 + 'joinStatus' => $joinStatus, // 入群状态:auto=自动建群加入,manual=其他方式加入 + 'isQuit' => $isQuit, // 是否已退群:0=在群中,1=已退群 + 'joinTime' => !empty($member['joinTime']) ? date('Y-m-d H:i:s', $member['joinTime']) : '', // 入群时间 + ]; + } + + // 添加已退群的成员(在自动建群记录中但不在群中的成员) + foreach ($autoJoinMemberList as $autoMember) { + $wechatId = $autoMember['wechatId'] ?? ''; + + // 跳过空wechatId、已在群中的成员和已添加的成员 + if (empty($wechatId) || in_array($wechatId, $inGroupWechatIds) || in_array($wechatId, $addedWechatIds)) { + continue; + } + + // 标记为已添加 + $addedWechatIds[] = $wechatId; + + // 根据wechatId判断是否为群主 + $isOwner = (!empty($ownerWechatId) && $wechatId == $ownerWechatId) ? 1 : 0; + + $members[] = [ + 'friendId' => $autoMember['friendId'] ?? 0, + 'wechatId' => $wechatId, + 'nickname' => $autoMember['friendNickname'] ?? '', + 'avatar' => $autoMember['friendAvatar'] ?? '', + 'alias' => '', + 'remark' => '', + 'isOwner' => $isOwner, // 标记群主 + 'joinStatus' => 'auto', // 入群状态:auto=自动建群加入 + 'isQuit' => 1, // 是否已退群:1=已退群 + 'joinTime' => !empty($autoMember['autoJoinTime']) ? date('Y-m-d H:i:s', $autoMember['autoJoinTime']) : '', // 入群时间 + ]; + } + + // 将群主排在第一位 + usort($members, function($a, $b) { + if ($a['isOwner'] == $b['isOwner']) { + return 0; + } + return $a['isOwner'] > $b['isOwner'] ? -1 : 1; + }); + + // 格式化返回数据 + $result = [ + 'id' => $group['id'], + 'groupName' => $group['groupName'] ?? '', + 'chatroomId' => $group['chatroomId'] ?? '', + 'groupAvatar' => $group['groupAvatar'] ?? '', + 'ownerWechatId' => $group['ownerWechatId'] ?? '', + 'ownerNickname' => $group['ownerNickname'] ?? '', + 'ownerAvatar' => $group['ownerAvatar'] ?? '', + 'ownerAlias' => $group['ownerAlias'] ?? '', + 'announce' => $group['announce'] ?? '', + 'createTime' => !empty($group['createTime']) ? date('Y-m-d H:i', $group['createTime']) : '', // 格式化为"YYYY-MM-DD HH:MM" + 'memberCount' => $memberCount, + 'memberCountText' => $memberCount . '人', // 格式化为"XX人" + 'workbenchName' => $workbench->name ?? '', // 任务名称(工作台名称) + 'members' => $members // 所有成员列表 + ]; + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => $result + ]); + } + + /** + * 同步群最新信息(包括群成员) + * @return \think\response\Json + */ + public function syncGroupInfo() + { + $workbenchId = $this->request->param('workbenchId', 0); + $groupId = $this->request->param('groupId', 0); + + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '工作台ID不能为空']); + } + + if (empty($groupId)) { + return json(['code' => 400, 'msg' => '群ID不能为空']); + } + + // 验证工作台权限 + $where = [ + ['id', '=', $workbenchId], + ['companyId', '=', $this->request->userInfo['companyId']], + ['type', '=', self::TYPE_GROUP_CREATE], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + $workbench = Workbench::where($where)->find(); + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在或无权限']); + } + + // 验证该群是否属于该工作台 + $groupItem = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('groupId', $groupId) + ->where('status', 2) // STATUS_SUCCESS = 2 + ->find(); + + if (empty($groupItem)) { + return json(['code' => 404, 'msg' => '群不存在或不属于该工作台']); + } + + // 查询群基本信息,获取chatroomId和wechatAccountWechatId + $group = Db::table('s2_wechat_chatroom') + ->where('id', $groupId) + ->where('isDeleted', 0) + ->field('id,chatroomId,wechatAccountWechatId') + ->find(); + + if (empty($group)) { + return json(['code' => 404, 'msg' => '群不存在']); + } + + $chatroomId = $group['chatroomId'] ?? ''; + $wechatAccountWechatId = $group['wechatAccountWechatId'] ?? ''; + + if (empty($chatroomId)) { + return json(['code' => 400, 'msg' => '群聊ID不存在']); + } + + try { + // 实例化WechatChatroomController + $chatroomController = new \app\api\controller\WechatChatroomController(); + + // 1. 同步群信息(调用getlist方法) + $syncData = [ + 'wechatChatroomId' => $chatroomId, // 使用chatroomId作为wechatChatroomId来指定要同步的群 + 'wechatAccountKeyword' => $wechatAccountWechatId, // 通过群主微信ID筛选 + 'isDeleted' => false, + 'pageIndex' => 1, + 'pageSize' => 100 // 获取足够多的数据 + ]; + $syncResult = $chatroomController->getlist($syncData, true, 0); // isInner = true + $syncResponse = json_decode($syncResult, true); + + if (empty($syncResponse['code']) || $syncResponse['code'] != 200) { + return json(['code' => 500, 'msg' => '同步群信息失败:' . ($syncResponse['msg'] ?? '未知错误')]); + } + + // 2. 同步群成员信息(调用listChatroomMember方法) + // wechatChatroomId 使用 s2_wechat_chatroom 表的 id(即groupId) + // chatroomId 使用群聊ID(chatroomId) + $wechatChatroomId = $groupId; // s2_wechat_chatroom表的id + $memberSyncResult = $chatroomController->listChatroomMember($wechatChatroomId, $chatroomId, true); // isInner = true + $memberSyncResponse = json_decode($memberSyncResult, true); + + if (empty($memberSyncResponse['code']) || $memberSyncResponse['code'] != 200) { + // 成员同步失败不影响整体结果,记录警告即可 + \think\facade\Log::warning("同步群成员失败。群ID: {$groupId}, 群聊ID: {$chatroomId}, 错误: " . ($memberSyncResponse['msg'] ?? '未知错误')); + } + + return json([ + 'code' => 200, + 'msg' => '同步成功', + 'data' => [ + 'groupId' => $groupId, + 'chatroomId' => $chatroomId, + 'groupInfoSynced' => true, + 'memberInfoSynced' => !empty($memberSyncResponse['code']) && $memberSyncResponse['code'] == 200 + ] + ]); + } catch (\Exception $e) { + \think\facade\Log::error("同步群信息异常。群ID: {$groupId}, 错误: " . $e->getMessage()); + return json(['code' => 500, 'msg' => '同步失败:' . $e->getMessage()]); + } + } + + /** + * 修改群名称、群公告 + * @return \think\response\Json + */ + public function modifyGroupInfo() + { + $workbenchId = $this->request->param('workbenchId', 0); + $groupId = $this->request->param('groupId', 0); + $chatroomName = $this->request->param('chatroomName', ''); + $announce = $this->request->param('announce', ''); + + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '工作台ID不能为空']); + } + + if (empty($groupId)) { + return json(['code' => 400, 'msg' => '群ID不能为空']); + } + + // 至少需要提供一个修改项 + if (empty($chatroomName) && empty($announce)) { + return json(['code' => 400, 'msg' => '请至少提供群名称或群公告中的一个参数']); + } + + // 查询群基本信息 + $group = Db::table('s2_wechat_chatroom') + ->where('id', $groupId) + ->where('isDeleted', 0) + ->field('id,chatroomId,wechatAccountWechatId,accountId,wechatAccountId') + ->find(); + + if (empty($group)) { + return json(['code' => 404, 'msg' => '群不存在']); + } + + $chatroomId = $group['id'] ?? ''; + + if (empty($chatroomId)) { + return json(['code' => 400, 'msg' => '群聊ID不存在']); + } + + try { + // 直接使用群表中的账号信息 + $executeAccountId = $group['accountId'] ?? 0; + $executeWechatAccountId = $group['wechatAccountId'] ?? 0; + $executeWechatId = $group['wechatAccountWechatId'] ?? ''; + + // 确保 wechatId 不为空 + if (empty($executeWechatId)) { + return json(['code' => 400, 'msg' => '无法获取微信账号ID']); + } + + // 调用 WebSocketController 修改群信息 + // 获取系统API账号信息(用于WebSocket连接) + $username = Env::get('api.username2', ''); + $password = Env::get('api.password2', ''); + + if (empty($username) || empty($password)) { + return json(['code' => 500, 'msg' => '系统API账号配置缺失']); + } + + // 获取系统账号ID + $systemAccountId = Db::name('users')->where('account', $username)->value('s2_accountId'); + if (empty($systemAccountId)) { + return json(['code' => 500, 'msg' => '未找到系统账号ID']); + } + + $webSocketController = new WebSocketController([ + 'userName' => $username, + 'password' => $password, + 'accountId' => $systemAccountId + ]); + // 构建修改参数 + $modifyData = [ + 'wechatChatroomId' => $chatroomId, + 'wechatAccountId' => $executeWechatAccountId, + ]; + if (!empty($chatroomName)) { + $modifyData['chatroomName'] = $chatroomName; + } + if (!empty($announce)) { + $modifyData['announce'] = $announce; + } + + + $modifyResult = $webSocketController->CmdChatroomModifyInfo($modifyData); + $modifyResponse = json_decode($modifyResult, true); + if (empty($modifyResponse['code']) || $modifyResponse['code'] != 200) { + return json(['code' => 500, 'msg' => '修改群信息失败:' . ($modifyResponse['msg'] ?? '未知错误')]); + } + + // 修改成功后更新数据库 + $updateData = [ + 'updateTime' => time() + ]; + + // 如果修改了群名称,更新数据库 + if (!empty($chatroomName)) { + $updateData['nickname'] = $chatroomName; + } + + // 如果修改了群公告,更新数据库 + if (!empty($announce)) { + $updateData['announce'] = $announce; + } + + // 更新数据库 + Db::table('s2_wechat_chatroom') + ->where('id', $groupId) + ->update($updateData); + + return json([ + 'code' => 200, + 'msg' => '修改成功', + 'data' => [ + 'groupId' => $groupId, + 'chatroomId' => $chatroomId, + 'chatroomName' => $chatroomName, + 'announce' => $announce + ] + ]); + } catch (\Exception $e) { + \think\facade\Log::error("修改群信息异常。群ID: {$groupId}, 错误: " . $e->getMessage()); + return json(['code' => 500, 'msg' => '修改失败:' . $e->getMessage()]); + } + } + + /** + * 退群功能 + * @return \think\response\Json + */ + public function quitGroup() + { + $workbenchId = $this->request->param('workbenchId', 0); + $groupId = $this->request->param('groupId', 0); + + if (empty($groupId)) { + return json(['code' => 400, 'msg' => '群ID不能为空']); + } + + // 查询群基本信息 + $group = Db::table('s2_wechat_chatroom') + ->where('id', $groupId) + ->where('isDeleted', 0) + ->field('id,chatroomId,wechatAccountWechatId,accountId,wechatAccountId') + ->find(); + + if (empty($group)) { + return json(['code' => 404, 'msg' => '群不存在']); + } + + $chatroomId = $group['chatroomId'] ?? ''; + if (empty($chatroomId)) { + return json(['code' => 400, 'msg' => '群聊ID不存在']); + } + + try { + // 直接使用群表中的账号信息 + $executeWechatId = $group['wechatAccountWechatId'] ?? ''; + + // 确保 wechatId 不为空 + if (empty($executeWechatId)) { + return json(['code' => 400, 'msg' => '无法获取微信账号ID']); + } + + // 调用 WebSocketController 退群 + // 获取系统API账号信息(用于WebSocket连接) + $username = Env::get('api.username2', ''); + $password = Env::get('api.password2', ''); + + if (empty($username) || empty($password)) { + return json(['code' => 500, 'msg' => '系统API账号配置缺失']); + } + + // 获取系统账号ID + $systemAccountId = Db::name('users')->where('account', $username)->value('s2_accountId'); + if (empty($systemAccountId)) { + return json(['code' => 500, 'msg' => '未找到系统账号ID']); + } + + $webSocketController = new WebSocketController([ + 'userName' => $username, + 'password' => $password, + 'accountId' => $systemAccountId + ]); + + // 使用和 modifyGroupInfo 相同的方式,但操作类型改为 4(退群) + $params = [ + "chatroomOperateType" => 4, // 4 表示退群 + "cmdType" => "CmdChatroomOperate", + "seq" => time(), + "wechatAccountId" => $executeWechatId, + "wechatChatroomId" => $chatroomId + ]; + + // 使用反射调用 protected 的 sendMessage 方法 + $reflection = new \ReflectionClass($webSocketController); + $method = $reflection->getMethod('sendMessage'); + $method->setAccessible(true); + $quitResult = $method->invoke($webSocketController, $params, false); + + // sendMessage 返回的是数组 + if (empty($quitResult) || (isset($quitResult['code']) && $quitResult['code'] != 200)) { + return json(['code' => 500, 'msg' => '退群失败:' . ($quitResult['msg'] ?? '未知错误')]); + } + + // 退群成功后更新数据库 + // 将群标记为已删除 + Db::table('s2_wechat_chatroom') + ->where('id', $groupId) + ->update([ + 'isDeleted' => 1, + 'deleteTime' => time(), + 'updateTime' => time() + ]); + + // 如果提供了 workbenchId,更新工作台建群记录的状态(标记为已退群) + if (!empty($workbenchId)) { + Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('groupId', $groupId) + ->update([ + 'status' => 5, // 可以定义一个新状态:5 = 已退群 + 'updateTime' => time() + ]); + } + + return json([ + 'code' => 200, + 'msg' => '退群成功', + 'data' => [ + 'groupId' => $groupId, + 'chatroomId' => $chatroomId + ] + ]); + } catch (\Exception $e) { + \think\facade\Log::error("退群异常。群ID: {$groupId}, 错误: " . $e->getMessage()); + return json(['code' => 500, 'msg' => '退群失败:' . $e->getMessage()]); + } + } + + /** + * 转移群聊到指定账号 + * @param int $groupId 群ID(s2_wechat_chatroom表的id) + * @param string $chatroomId 群聊ID + * @param int $toAccountId 目标账号ID(s2_company_account表的id) + * @param int $toWechatAccountId 目标微信账号ID(s2_wechat_account表的id) + * @return array ['success' => bool, 'msg' => string] + */ + protected function transferChatroomToAccount($groupId, $chatroomId, $toAccountId, $toWechatAccountId) + { + try { + // 查询目标账号信息 + $targetAccount = Db::table('s2_company_account') + ->where('id', $toAccountId) + ->field('id,userName,realName,nickname') + ->find(); + + if (empty($targetAccount)) { + return ['success' => false, 'msg' => '目标账号不存在']; + } + + // 查询目标微信账号信息 + $targetWechatAccount = Db::table('s2_wechat_account') + ->where('id', $toWechatAccountId) + ->field('id,wechatId,deviceAccountId') + ->find(); + + if (empty($targetWechatAccount)) { + return ['success' => false, 'msg' => '目标微信账号不存在']; + } + + // 调用 AutomaticAssign 进行群聊转移 + $automaticAssign = new \app\api\controller\AutomaticAssign(); + + // 构建转移参数(通过 API 调用) + $transferData = [ + 'wechatChatroomId' => $chatroomId, // 使用群聊ID + 'toAccountId' => $toAccountId, + 'wechatAccountKeyword' => $targetWechatAccount['wechatId'] ?? '', + 'isDeleted' => false + ]; + + // 直接更新数据库(因为 API 可能不支持指定单个群聊ID转移) + // 更新 s2_wechat_chatroom 表的 accountId 和 wechatAccountId + Db::table('s2_wechat_chatroom') + ->where('id', $groupId) + ->update([ + 'accountId' => $toAccountId, + 'accountUserName' => $targetAccount['userName'] ?? '', + 'accountRealName' => $targetAccount['realName'] ?? '', + 'accountNickname' => $targetAccount['nickname'] ?? '', + 'wechatAccountId' => $toWechatAccountId, + 'wechatAccountWechatId' => $targetWechatAccount['wechatId'] ?? '', + 'updateTime' => time() + ]); + + return ['success' => true, 'msg' => '转移成功']; + } catch (\Exception $e) { + \think\facade\Log::error("转移群聊异常。群ID: {$groupId}, 目标账号ID: {$toAccountId}, 错误: " . $e->getMessage()); + return ['success' => false, 'msg' => $e->getMessage()]; + } + } + + +} \ No newline at end of file diff --git a/application/cunkebao/controller/workbench/WorkbenchGroupPushController.php b/application/cunkebao/controller/workbench/WorkbenchGroupPushController.php new file mode 100644 index 0000000..52e3005 --- /dev/null +++ b/application/cunkebao/controller/workbench/WorkbenchGroupPushController.php @@ -0,0 +1,3782 @@ +request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + // 获取登录用户信息 + $userInfo = request()->userInfo; + + // 获取请求参数 + $param = $this->request->post(); + + + // 根据业务默认值补全参数 + if ( + isset($param['type']) && + intval($param['type']) === self::TYPE_GROUP_PUSH + ) { + if (empty($param['startTime'])) { + $param['startTime'] = '09:00'; + } + if (empty($param['endTime'])) { + $param['endTime'] = '21:00'; + } + } + + // 验证数据 + $validate = new WorkbenchValidate; + if (!$validate->scene('create')->check($param)) { + return json(['code' => 400, 'msg' => $validate->getError()]); + } + + Db::startTrans(); + try { + // 创建工作台基本信息 + $workbench = new Workbench; + $workbench->name = $param['name']; + $workbench->type = $param['type']; + $workbench->status = !empty($param['status']) ? 1 : 0; + $workbench->autoStart = !empty($param['autoStart']) ? 1 : 0; + $workbench->userId = $userInfo['id']; + $workbench->companyId = $userInfo['companyId']; + $workbench->createTime = time(); + $workbench->updateTime = time(); + $workbench->save(); + + // 根据类型创建对应的配置 + switch ($param['type']) { + case self::TYPE_AUTO_LIKE: // 自动点赞 + $config = new WorkbenchAutoLike; + $config->workbenchId = $workbench->id; + $config->interval = $param['interval']; + $config->maxLikes = $param['maxLikes']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->contentTypes = json_encode($param['contentTypes']); + $config->devices = json_encode($param['deviceGroups']); + $config->friends = json_encode($param['wechatFriends']); + // $config->targetGroups = json_encode($param['targetGroups']); + // $config->tagOperator = $param['tagOperator']; + $config->friendMaxLikes = $param['friendMaxLikes']; + $config->friendTags = $param['friendTags']; + $config->enableFriendTags = $param['enableFriendTags']; + $config->createTime = time(); + $config->updateTime = time(); + $config->save(); + break; + case self::TYPE_MOMENTS_SYNC: // 朋友圈同步 + $config = new WorkbenchMomentsSync; + $config->workbenchId = $workbench->id; + $config->syncInterval = $param['syncInterval']; + $config->syncCount = $param['syncCount']; + $config->syncType = $param['syncType']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->accountType = $param['accountType']; + $config->devices = json_encode($param['deviceGroups']); + $config->contentLibraries = json_encode($param['contentGroups'] ?? []); + $config->createTime = time(); + $config->updateTime = time(); + $config->save(); + break; + case self::TYPE_GROUP_PUSH: // 群消息推送 + $ownerWechatIds = $this->normalizeOwnerWechatIds($param['ownerWechatIds'] ?? []); + $groupPushData = $this->prepareGroupPushData($param, $ownerWechatIds); + $groupPushData['workbenchId'] = $workbench->id; + $groupPushData['createTime'] = time(); + $groupPushData['updateTime'] = time(); + $config = new WorkbenchGroupPush; + $config->save($groupPushData); + break; + case self::TYPE_GROUP_CREATE: // 自动建群 + $config = new WorkbenchGroupCreate; + $config->workbenchId = $workbench->id; + $config->planType = !empty($param['planType']) ? $param['planType'] : 0; + $config->executorId = !empty($param['executorId']) ? $param['executorId'] : 0; + + $config->devices = json_encode($param['deviceGroups'] ?? [], JSON_UNESCAPED_UNICODE); + $config->startTime = $param['startTime'] ?? ''; + $config->endTime = $param['endTime'] ?? ''; + $config->groupSizeMin = intval($param['groupSizeMin'] ?? 3); + $config->groupSizeMax = intval($param['groupSizeMax'] ?? 38); + $config->maxGroupsPerDay = intval($param['maxGroupsPerDay'] ?? 20); + $config->groupNameTemplate = $param['groupNameTemplate'] ?? ''; + $config->groupDescription = $param['groupDescription'] ?? ''; + $config->poolGroups = json_encode($param['poolGroups'] ?? [], JSON_UNESCAPED_UNICODE); + $config->wechatGroups = json_encode($param['wechatGroups'] ?? [], JSON_UNESCAPED_UNICODE); + + // 处理群管理员:如果启用了群管理员且有指定管理员,则保存到admins字段 + $admins = []; + if (!empty($param['groupAdminEnabled']) && !empty($param['groupAdminWechatId'])) { + // 如果groupAdminWechatId是数组,取第一个;如果是单个值,直接使用 + $adminWechatId = is_array($param['groupAdminWechatId']) ? $param['groupAdminWechatId'][0] : $param['groupAdminWechatId']; + // 如果是好友ID,直接添加到admins;如果是wechatId,需要转换为好友ID + if (is_numeric($adminWechatId)) { + $admins[] = intval($adminWechatId); + } else { + // 如果是wechatId字符串,需要查询对应的好友ID + $friend = Db::table('s2_wechat_friend')->where('wechatId', $adminWechatId)->find(); + if ($friend) { + $admins[] = intval($friend['id']); + } + } + } + // 如果传入了admins参数,优先使用(兼容旧逻辑) + if (!empty($param['admins']) && is_array($param['admins'])) { + $admins = array_merge($admins, $param['admins']); + } + $config->admins = json_encode(array_unique($admins), JSON_UNESCAPED_UNICODE); + + $config->fixedWechatIds = json_encode($param['fixedWechatIds'] ?? [], JSON_UNESCAPED_UNICODE); + $config->createTime = time(); + $config->updateTime = time(); + $config->save(); + break; + case self::TYPE_TRAFFIC_DISTRIBUTION: // 流量分发 + $config = new WorkbenchTrafficConfig; + $config->workbenchId = $workbench->id; + $config->distributeType = $param['distributeType']; + $config->maxPerDay = $param['maxPerDay']; + $config->timeType = $param['timeType']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->devices = json_encode($param['deviceGroups'], JSON_UNESCAPED_UNICODE); + $config->pools = json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE); + $config->account = json_encode($param['accountGroups'], JSON_UNESCAPED_UNICODE); + $config->createTime = time(); + $config->updateTime = time(); + $config->save(); + break; + case self::TYPE_IMPORT_CONTACT: //联系人导入 + $config = new WorkbenchImportContact; + $config->workbenchId = $workbench->id; + $config->devices = json_encode($param['deviceGroups'], JSON_UNESCAPED_UNICODE); + $config->pools = json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE); + $config->num = $param['num']; + $config->clearContact = $param['clearContact']; + $config->remark = $param['remark']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->createTime = time(); + $config->save(); + break; + } + + Db::commit(); + return json(['code' => 200, 'msg' => '创建成功', 'data' => ['id' => $workbench->id]]); + } catch (\Exception $e) { + Db::rollback(); + return json(['code' => 500, 'msg' => '创建失败:' . $e->getMessage()]); + } + } + + /** + * 获取工作台列表 + * @return \think\response\Json + */ + public function getList() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $type = $this->request->param('type', ''); + $keyword = $this->request->param('keyword', ''); + + $where = [ + ['companyId', '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0] + ]; + + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + + + // 添加类型筛选 + if ($type !== '') { + $where[] = ['type', '=', $type]; + } + + // 添加名称模糊搜索 + if ($keyword !== '') { + $where[] = ['name', 'like', '%' . $keyword . '%']; + } + + // 定义关联关系 + $with = [ + 'autoLike' => function ($query) { + $query->field('workbenchId,interval,maxLikes,startTime,endTime,contentTypes,devices,friends'); + }, + 'momentsSync' => function ($query) { + $query->field('workbenchId,syncInterval,syncCount,syncType,startTime,endTime,accountType,devices,contentLibraries'); + }, + 'trafficConfig' => function ($query) { + $query->field('workbenchId,distributeType,maxPerDay,timeType,startTime,endTime,devices,pools,account'); + }, + 'groupPush' => function ($query) { + $query->field('workbenchId,pushType,targetType,groupPushSubType,startTime,endTime,maxPerDay,pushOrder,isLoop,status,groups,friends,ownerWechatIds,trafficPools,contentLibraries,friendIntervalMin,friendIntervalMax,messageIntervalMin,messageIntervalMax,isRandomTemplate,postPushTags,announcementContent,enableAiRewrite,aiRewritePrompt'); + }, + 'groupCreate' => function ($query) { + $query->field('workbenchId,devices,startTime,endTime,groupSizeMin,groupSizeMax,maxGroupsPerDay,groupNameTemplate,groupDescription,poolGroups,wechatGroups,admins'); + }, + 'importContact' => function ($query) { + $query->field('workbenchId,devices,pools,num,remarkType,remark,clearContact,startTime,endTime'); + }, + 'user' => function ($query) { + $query->field('id,username'); + } + ]; + + $list = Workbench::where($where) + ->with($with) + ->field('id,companyId,name,type,status,autoStart,userId,createTime,updateTime') + ->order('id', 'desc') + ->page($page, $limit) + ->select() + ->each(function ($item) { + // 处理配置信息 + switch ($item->type) { + case self::TYPE_AUTO_LIKE: + if (!empty($item->autoLike)) { + $item->config = $item->autoLike; + $item->config->devices = json_decode($item->config->devices, true); + $item->config->contentTypes = json_decode($item->config->contentTypes, true); + $item->config->friends = json_decode($item->config->friends, true); + + // 添加今日点赞数 + $startTime = strtotime(date('Y-m-d') . ' 00:00:00'); + $endTime = strtotime(date('Y-m-d') . ' 23:59:59'); + $todayLikeCount = Db::name('workbench_auto_like_item') + ->where('workbenchId', $item->id) + ->whereTime('createTime', 'between', [$startTime, $endTime]) + ->count(); + + // 添加总点赞数 + $totalLikeCount = Db::name('workbench_auto_like_item') + ->where('workbenchId', $item->id) + ->count(); + + $item->config->todayLikeCount = $todayLikeCount; + $item->config->totalLikeCount = $totalLikeCount; + } + unset($item->autoLike, $item->auto_like); + break; + case self::TYPE_MOMENTS_SYNC: + if (!empty($item->momentsSync)) { + $item->config = $item->momentsSync; + $item->config->devices = json_decode($item->config->devices, true); + $item->config->contentGroups = json_decode($item->config->contentLibraries, true); + //同步记录 + $sendNum = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $item->id])->count(); + $item->syncCount = $sendNum; + $lastTime = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $item->id])->order('id DESC')->value('createTime'); + $item->lastSyncTime = !empty($lastTime) ? date('Y-m-d H:i', $lastTime) : '--'; + + + // 获取内容库名称 + if (!empty($item->config->contentGroups)) { + $libraryNames = ContentLibrary::where('id', 'in', $item->config->contentGroups)->select(); + $item->config->contentGroupsOptions = $libraryNames; + } else { + $item->config->contentGroupsOptions = []; + } + } + unset($item->momentsSync, $item->moments_sync, $item->config->contentLibraries); + break; + case self::TYPE_GROUP_PUSH: + if (!empty($item->groupPush)) { + $item->config = $item->groupPush; + $item->config->pushType = $item->config->pushType; + $item->config->targetType = isset($item->config->targetType) ? intval($item->config->targetType) : 1; // 默认1=群推送 + $item->config->groupPushSubType = isset($item->config->groupPushSubType) ? intval($item->config->groupPushSubType) : 1; // 默认1=群群发 + $item->config->startTime = $item->config->startTime; + $item->config->endTime = $item->config->endTime; + $item->config->maxPerDay = $item->config->maxPerDay; + $item->config->pushOrder = $item->config->pushOrder; + $item->config->isLoop = $item->config->isLoop; + $item->config->status = $item->config->status; + $item->config->ownerWechatIds = json_decode($item->config->ownerWechatIds ?? '[]', true) ?: []; + // 根据targetType解析不同的数据 + if ($item->config->targetType == 1) { + // 群推送 + $item->config->wechatGroups = json_decode($item->config->groups, true) ?: []; + $item->config->wechatFriends = []; + // 群推送不需要devices字段 + // 群公告相关字段 + if ($item->config->groupPushSubType == 2) { + $item->config->announcementContent = isset($item->config->announcementContent) ? $item->config->announcementContent : ''; + $item->config->enableAiRewrite = isset($item->config->enableAiRewrite) ? intval($item->config->enableAiRewrite) : 0; + $item->config->aiRewritePrompt = isset($item->config->aiRewritePrompt) ? $item->config->aiRewritePrompt : ''; + } + $item->config->trafficPools = []; + } else { + // 好友推送 + $item->config->wechatFriends = json_decode($item->config->friends, true) ?: []; + $item->config->wechatGroups = []; + $item->config->trafficPools = json_decode($item->config->trafficPools ?? '[]', true) ?: []; + } + $item->config->contentLibraries = json_decode($item->config->contentLibraries, true); + $item->config->postPushTags = json_decode($item->config->postPushTags ?? '[]', true) ?: []; + $item->config->lastPushTime = ''; + if (!empty($item->config->ownerWechatIds)) { + $ownerWechatOptions = Db::name('wechat_account') + ->whereIn('id', $item->config->ownerWechatIds) + ->field('id,wechatId,nickName,avatar,alias') + ->select(); + $item->config->ownerWechatOptions = $ownerWechatOptions; + } else { + $item->config->ownerWechatOptions = []; + } + } + unset($item->groupPush, $item->group_push); + break; + case self::TYPE_GROUP_CREATE: + if (!empty($item->groupCreate)) { + $item->config = $item->groupCreate; + $item->config->devices = json_decode($item->config->devices, true); + $item->config->poolGroups = json_decode($item->config->poolGroups, true); + $item->config->wechatGroups = json_decode($item->config->wechatGroups, true); + $item->config->admins = json_decode($item->config->admins ?? '[]', true) ?: []; + + // 处理群管理员相关字段 + $item->config->groupAdminEnabled = !empty($item->config->admins) ? 1 : 0; + + if (!empty($item->config->admins)) { + $adminOptions = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->where('wf.id', 'in', $item->config->admins) + ->order('wf.id', 'desc') + ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar') + ->select(); + $item->config->adminsOptions = $adminOptions; + // 如果有管理员,设置groupAdminWechatId为第一个管理员的ID(用于前端回显) + $item->config->groupAdminWechatId = !empty($item->config->admins) ? $item->config->admins[0] : null; + } else { + $item->config->adminsOptions = []; + $item->config->groupAdminWechatId = null; + } + } + unset($item->groupCreate, $item->group_create); + break; + case self::TYPE_TRAFFIC_DISTRIBUTION: + if (!empty($item->trafficConfig)) { + $item->config = $item->trafficConfig; + $item->config->devices = json_decode($item->config->devices, true); + $item->config->poolGroups = json_decode($item->config->pools, true); + $item->config->account = json_decode($item->config->account, true); + $config_item = Db::name('workbench_traffic_config_item')->where(['workbenchId' => $item->id])->order('id DESC')->find(); + $item->config->lastUpdated = !empty($config_item) ? date('Y-m-d H:i', $config_item['createTime']) : '--'; + + //统计 + $labels = $item->config->poolGroups; + $totalUsers = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->where([ + ['wf.isDeleted', '=', 0], + ['sa.departmentId', '=', $item->companyId] + ]) + ->whereIn('wa.currentDeviceId', $item->config->devices); + + if (!empty($labels) && count($labels) > 0) { + $totalUsers = $totalUsers->where(function ($q) use ($labels) { + foreach ($labels as $label) { + $q->whereOrRaw("JSON_CONTAINS(wf.labels, '\"{$label}\"')"); + } + }); + } + + $totalUsers = $totalUsers->count(); + $totalAccounts = count($item->config->account); + $dailyAverage = Db::name('workbench_traffic_config_item') + ->where('workbenchId', $item->id) + ->count(); + $day = (time() - strtotime($item->createTime)) / 86400; + $day = intval($day); + if ($dailyAverage > 0 && $totalAccounts > 0 && $day > 0) { + $dailyAverage = $dailyAverage / $totalAccounts / $day; + } + $item->config->total = [ + 'dailyAverage' => intval($dailyAverage), + 'totalAccounts' => $totalAccounts, + 'deviceCount' => count($item->config->devices), + 'poolCount' => !empty($item->config->poolGroups) ? count($item->config->poolGroups) : 'ALL', + 'totalUsers' => $totalUsers >> 0 + ]; + } + unset($item->trafficConfig, $item->traffic_config); + break; + + case self::TYPE_IMPORT_CONTACT: + if (!empty($item->importContact)) { + $item->config = $item->importContact; + $item->config->devices = json_decode($item->config->devices, true); + $item->config->poolGroups = json_decode($item->config->pools, true); + } + unset($item->importContact, $item->import_contact); + break; + } + // 添加创建人名称 + $item['creatorName'] = $item->user ? $item->user->username : ''; + unset($item['user']); // 移除关联数据 + return $item; + }); + + $total = Workbench::where($where)->count(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + + /** + * 获取工作台详情 + * @param int $id 工作台ID + * @return \think\response\Json + */ + public function detail() + { + $id = $this->request->param('id', ''); + + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + // 定义关联关系 + $with = [ + 'autoLike' => function ($query) { + $query->field('workbenchId,interval,maxLikes,startTime,endTime,contentTypes,devices,friends,friendMaxLikes,friendTags,enableFriendTags'); + }, + 'momentsSync' => function ($query) { + $query->field('workbenchId,syncInterval,syncCount,syncType,startTime,endTime,accountType,devices,contentLibraries'); + }, + 'trafficConfig' => function ($query) { + $query->field('workbenchId,distributeType,maxPerDay,timeType,startTime,endTime,devices,pools,account'); + }, + 'groupPush' => function ($query) { + $query->field('workbenchId,pushType,targetType,groupPushSubType,startTime,endTime,maxPerDay,pushOrder,isLoop,status,groups,friends,ownerWechatIds,trafficPools,contentLibraries,friendIntervalMin,friendIntervalMax,messageIntervalMin,messageIntervalMax,isRandomTemplate,postPushTags,announcementContent,enableAiRewrite,aiRewritePrompt'); + }, + 'groupCreate' => function ($query) { + $query->field('workbenchId,devices,startTime,endTime,groupSizeMin,groupSizeMax,maxGroupsPerDay,groupNameTemplate,groupDescription,poolGroups,wechatGroups,admins'); + }, + 'importContact' => function ($query) { + $query->field('workbenchId,devices,pools,num,remarkType,remark,clearContact,startTime,endTime'); + }, + ]; + + $where = [ + ['id', '=', $id], + ['companyId', '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + + $workbench = Workbench::where($where) + ->field('id,name,type,status,autoStart,createTime,updateTime,companyId') + ->with($with) + ->find(); + + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + + // 处理配置信息 + switch ($workbench->type) { + //自动点赞 + case self::TYPE_AUTO_LIKE: + if (!empty($workbench->autoLike)) { + $workbench->config = $workbench->autoLike; + $workbench->config->deviceGroups = json_decode($workbench->config->devices, true); + $workbench->config->wechatFriends = json_decode($workbench->config->friends, true); + $workbench->config->targetType = 2; + //$workbench->config->targetGroups = json_decode($workbench->config->targetGroups, true); + $workbench->config->contentTypes = json_decode($workbench->config->contentTypes, true); + + // 添加今日点赞数 + $startTime = strtotime(date('Y-m-d') . ' 00:00:00'); + $endTime = strtotime(date('Y-m-d') . ' 23:59:59'); + $todayLikeCount = Db::name('workbench_auto_like_item') + ->where('workbenchId', $workbench->id) + ->whereTime('createTime', 'between', [$startTime, $endTime]) + ->count(); + + // 添加总点赞数 + $totalLikeCount = Db::name('workbench_auto_like_item') + ->where('workbenchId', $workbench->id) + ->count(); + + $workbench->config->todayLikeCount = $todayLikeCount; + $workbench->config->totalLikeCount = $totalLikeCount; + + unset($workbench->autoLike, $workbench->auto_like); + } + break; + //自动同步朋友圈 + case self::TYPE_MOMENTS_SYNC: + if (!empty($workbench->momentsSync)) { + $workbench->config = $workbench->momentsSync; + $workbench->config->deviceGroups = json_decode($workbench->config->devices, true); + $workbench->config->contentGroups = json_decode($workbench->config->contentLibraries, true); + + //同步记录 + $sendNum = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $workbench->id])->count(); + $workbench->syncCount = $sendNum; + $lastTime = Db::name('workbench_moments_sync_item')->where(['workbenchId' => $workbench->id])->order('id DESC')->value('createTime'); + $workbench->lastSyncTime = !empty($lastTime) ? date('Y-m-d H:i', $lastTime) : '--'; + unset($workbench->momentsSync, $workbench->moments_sync); + } + break; + //群推送 + case self::TYPE_GROUP_PUSH: + if (!empty($workbench->groupPush)) { + $workbench->config = $workbench->groupPush; + $workbench->config->targetType = isset($workbench->config->targetType) ? intval($workbench->config->targetType) : 1; // 默认1=群推送 + $workbench->config->groupPushSubType = isset($workbench->config->groupPushSubType) ? intval($workbench->config->groupPushSubType) : 1; // 默认1=群群发 + $workbench->config->ownerWechatIds = json_decode($workbench->config->ownerWechatIds ?? '[]', true) ?: []; + // 根据targetType解析不同的数据 + if ($workbench->config->targetType == 1) { + // 群推送 + $workbench->config->wechatGroups = json_decode($workbench->config->groups, true) ?: []; + $workbench->config->wechatFriends = []; + $workbench->config->trafficPools = []; + // 群推送不需要devices字段 + // 群公告相关字段 + if ($workbench->config->groupPushSubType == 2) { + $workbench->config->announcementContent = isset($workbench->config->announcementContent) ? $workbench->config->announcementContent : ''; + $workbench->config->enableAiRewrite = isset($workbench->config->enableAiRewrite) ? intval($workbench->config->enableAiRewrite) : 0; + $workbench->config->aiRewritePrompt = isset($workbench->config->aiRewritePrompt) ? $workbench->config->aiRewritePrompt : ''; + } + } else { + // 好友推送 + $workbench->config->wechatFriends = json_decode($workbench->config->friends, true) ?: []; + $workbench->config->wechatGroups = []; + $workbench->config->trafficPools = json_decode($workbench->config->trafficPools ?? '[]', true) ?: []; + } + $workbench->config->contentLibraries = json_decode($workbench->config->contentLibraries, true); + $workbench->config->postPushTags = json_decode($workbench->config->postPushTags ?? '[]', true) ?: []; + unset($workbench->groupPush, $workbench->group_push); + } + break; + //建群助手 + case self::TYPE_GROUP_CREATE: + if (!empty($workbench->groupCreate)) { + $workbench->config = $workbench->groupCreate; + $workbench->config->deviceGroups = json_decode($workbench->config->devices, true); + $workbench->config->poolGroups = json_decode($workbench->config->poolGroups, true); + $workbench->config->wechatGroups = json_decode($workbench->config->wechatGroups, true); + $workbench->config->admins = json_decode($workbench->config->admins ?? '[]', true) ?: []; + + // 处理群管理员相关字段 + $workbench->config->groupAdminEnabled = !empty($workbench->config->admins) ? 1 : 0; + + // 如果有管理员,设置groupAdminWechatId为第一个管理员的ID(用于前端回显) + $workbench->config->groupAdminWechatId = !empty($workbench->config->admins) ? $workbench->config->admins[0] : null; + + // 统计已建群数(状态为成功且groupId不为空的记录,按groupId分组去重) + $createdGroupsCount = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbench->id) + ->where('status', 2) // STATUS_SUCCESS = 2 + ->where('groupId', '<>', null) + ->group('groupId') + ->count(); + + // 统计总人数(该工作台的所有记录数) + $totalMembersCount = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbench->id) + ->count(); + + // 添加统计信息 + $workbench->config->stats = [ + 'createdGroupsCount' => $createdGroupsCount, + 'totalMembersCount' => $totalMembersCount + ]; + + unset($workbench->groupCreate, $workbench->group_create); + } + break; + //流量分发 + case self::TYPE_TRAFFIC_DISTRIBUTION: + if (!empty($workbench->trafficConfig)) { + $workbench->config = $workbench->trafficConfig; + $workbench->config->deviceGroups = json_decode($workbench->config->devices, true); + $workbench->config->accountGroups = json_decode($workbench->config->account, true); + $workbench->config->poolGroups = json_decode($workbench->config->pools, true); + $config_item = Db::name('workbench_traffic_config_item')->where(['workbenchId' => $workbench->id])->order('id DESC')->find(); + $workbench->config->lastUpdated = !empty($config_item) ? date('Y-m-d H:i', $config_item['createTime']) : '--'; + + //统计 + $labels = $workbench->config->poolGroups; + $totalUsers = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->where([ + ['wf.isDeleted', '=', 0], + ['sa.departmentId', '=', $workbench->companyId] + ]) + ->whereIn('wa.currentDeviceId', $workbench->config->deviceGroups) + ->field('wf.id,wf.wechatAccountId,wf.wechatId,wf.labels,sa.userName,wa.currentDeviceId as deviceId') + ->where(function ($q) use ($labels) { + foreach ($labels as $label) { + $q->whereOrRaw("JSON_CONTAINS(wf.labels, '\"{$label}\"')"); + } + })->count(); + + $totalAccounts = Db::table('s2_company_account') + ->alias('a') + ->where(['a.departmentId' => $workbench->companyId, 'a.status' => 0]) + ->whereNotLike('a.userName', '%_offline%') + ->whereNotLike('a.userName', '%_delete%') + ->group('a.id') + ->count(); + + $dailyAverage = Db::name('workbench_traffic_config_item') + ->where('workbenchId', $workbench->id) + ->count(); + $day = (time() - strtotime($workbench->createTime)) / 86400; + $day = intval($day); + + + if ($dailyAverage > 0) { + $dailyAverage = $dailyAverage / $totalAccounts / $day; + } + + $workbench->config->total = [ + 'dailyAverage' => intval($dailyAverage), + 'totalAccounts' => $totalAccounts, + 'deviceCount' => count($workbench->config->deviceGroups), + 'poolCount' => count($workbench->config->poolGroups), + 'totalUsers' => $totalUsers >> 0 + ]; + unset($workbench->trafficConfig, $workbench->traffic_config); + } + break; + case self::TYPE_IMPORT_CONTACT: + if (!empty($workbench->importContact)) { + $workbench->config = $workbench->importContact; + $workbench->config->deviceGroups = json_decode($workbench->config->devices, true); + $workbench->config->poolGroups = json_decode($workbench->config->pools, true); + } + unset($workbench->importContact, $workbench->import_contact); + break; + } + unset( + $workbench->autoLike, + $workbench->momentsSync, + $workbench->groupPush, + $workbench->groupCreate, + $workbench->config->devices, + $workbench->config->friends, + $workbench->config->groups, + $workbench->config->contentLibraries, + $workbench->config->account, + ); + + + //获取设备信息 + if (!empty($workbench->config->deviceGroups)) { + $deviceList = DeviceModel::alias('d') + ->field([ + 'd.id', 'd.imei', 'd.memo', 'd.alive', + 'l.wechatId', + 'a.nickname', 'a.alias', 'a.avatar', 'a.alias', '0 totalFriend' + ]) + ->leftJoin('device_wechat_login l', 'd.id = l.deviceId and l.alive =' . DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE . ' and l.companyId = d.companyId') + ->leftJoin('wechat_account a', 'l.wechatId = a.wechatId') + ->whereIn('d.id', $workbench->config->deviceGroups) + ->order('d.id desc') + ->select(); + + foreach ($deviceList as &$device) { + $curstomer = WechatCustomerModel::field('friendShip')->where(['wechatId' => $device['wechatId']])->find(); + $device['totalFriend'] = $curstomer->friendShip->totalFriend ?? 0; + } + unset($device); + + $workbench->config->deviceGroupsOptions = $deviceList; + } else { + $workbench->config->deviceGroupsOptions = []; + } + + + // 获取群(当targetType=1时) + if (!empty($workbench->config->wechatGroups) && isset($workbench->config->targetType) && $workbench->config->targetType == 1) { + $groupList = Db::name('wechat_group')->alias('wg') + ->join('wechat_account wa', 'wa.wechatId = wg.ownerWechatId') + ->where('wg.id', 'in', $workbench->config->wechatGroups) + ->order('wg.id', 'desc') + ->field('wg.id,wg.name as groupName,wg.ownerWechatId,wa.nickName,wa.avatar,wa.alias,wg.avatar as groupAvatar') + ->select(); + $workbench->config->wechatGroupsOptions = $groupList; + } else { + $workbench->config->wechatGroupsOptions = []; + } + + // 获取好友(当targetType=2时) + if (!empty($workbench->config->wechatFriends) && isset($workbench->config->targetType) && $workbench->config->targetType == 2) { + $friendList = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->where('wf.id', 'in', $workbench->config->wechatFriends) + ->order('wf.id', 'desc') + ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar') + ->select(); + $workbench->config->wechatFriendsOptions = $friendList; + } else { + $workbench->config->wechatFriendsOptions = []; + } + + // 获取流量池(当targetType=2时) + if (!empty($workbench->config->trafficPools) && isset($workbench->config->targetType) && $workbench->config->targetType == 2) { + $poolList = Db::name('traffic_source_package')->alias('tsp') + ->leftJoin('traffic_source_package_item tspi', 'tspi.packageId = tsp.id and tspi.isDel = 0') + ->whereIn('tsp.id', $workbench->config->trafficPools) + ->where('tsp.isDel', 0) + ->whereIn('tsp.companyId', [$this->request->userInfo['companyId'], 0]) + ->field('tsp.id,tsp.name,tsp.description,tsp.pic,COUNT(tspi.id) as itemCount') + ->group('tsp.id') + ->order('tsp.id', 'desc') + ->select(); + $workbench->config->trafficPoolsOptions = $poolList; + } else { + $workbench->config->trafficPoolsOptions = []; + } + + // 获取内容库名称 + if (!empty($workbench->config->contentGroups)) { + $libraryNames = ContentLibrary::where('id', 'in', $workbench->config->contentGroups)->select(); + $workbench->config->contentGroupsOptions = $libraryNames; + } else { + $workbench->config->contentGroupsOptions = []; + } + + //账号 + if (!empty($workbench->config->accountGroups)) { + $account = Db::table('s2_company_account')->alias('a') + ->where(['a.departmentId' => $this->request->userInfo['companyId'], 'a.status' => 0]) + ->whereIn('a.id', $workbench->config->accountGroups) + ->whereNotLike('a.userName', '%_offline%') + ->whereNotLike('a.userName', '%_delete%') + ->field('a.id,a.userName,a.realName,a.nickname,a.memo') + ->select(); + $workbench->config->accountGroupsOptions = $account; + } else { + $workbench->config->accountGroupsOptions = []; + } + + if (!empty($workbench->config->poolGroups)) { + $poolGroupsOptions = Db::name('traffic_source_package')->alias('tsp') + ->join('traffic_source_package_item tspi', 'tspi.packageId=tsp.id', 'left') + ->whereIn('tsp.companyId', [$this->request->userInfo['companyId'], 0]) + ->whereIn('tsp.id', $workbench->config->poolGroups) + ->field('tsp.id,tsp.name,tsp.description,tsp.createTime,count(tspi.id) as num') + ->group('tsp.id') + ->select(); + $workbench->config->poolGroupsOptions = $poolGroupsOptions; + } else { + $workbench->config->poolGroupsOptions = []; + } + + if (!empty($workbench->config->ownerWechatIds)) { + $ownerWechatOptions = Db::name('wechat_account') + ->whereIn('id', $workbench->config->ownerWechatIds) + ->field('id,wechatId,nickName,avatar,alias') + ->select(); + $workbench->config->ownerWechatOptions = $ownerWechatOptions; + } else { + $workbench->config->ownerWechatOptions = []; + } + + // 获取群组选项(自动建群) + if ($workbench->type == self::TYPE_GROUP_CREATE && !empty($workbench->config->wechatGroups)) { + // 分离数字ID(好友ID)和字符串ID(手动创建的群组) + $friendIds = []; + $manualGroupIds = []; + + foreach ($workbench->config->wechatGroups as $groupId) { + if (is_numeric($groupId)) { + $friendIds[] = intval($groupId); + } else { + $manualGroupIds[] = $groupId; + } + } + + $wechatGroupsOptions = []; + + // 查询好友信息(数字ID) + if (!empty($friendIds)) { + $friendList = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->join(['s2_company_account' => 'ca'], 'ca.id = wf.accountId', 'left') + ->where('wf.id', 'in', $friendIds) + ->order('wf.id', 'desc') + ->field('wf.id,wf.wechatId,wf.nickname,wf.avatar,wf.alias,wf.gender,wf.phone,wa.nickName as accountNickname,ca.userName as account,ca.realName as username,wf.createTime,wf.updateTime,wf.deleteTime,wf.ownerWechatId') + ->select(); + + // 获取群主信息 + foreach ($friendList as &$friend) { + if (!empty($friend['ownerWechatId'])) { + $owner = Db::name('wechat_account') + ->where('wechatId', $friend['ownerWechatId']) + ->field('nickName,alias') + ->find(); + $friend['ownerNickname'] = $owner['nickName'] ?? ''; + $friend['ownerAlias'] = $owner['alias'] ?? ''; + } else { + $friend['ownerNickname'] = ''; + $friend['ownerAlias'] = ''; + } + $friend['isManual'] = ''; + // 格式化时间 + $friend['createTime'] = !empty($friend['createTime']) ? date('Y-m-d H:i:s', $friend['createTime']) : ''; + $friend['updateTime'] = !empty($friend['updateTime']) ? date('Y-m-d H:i:s', $friend['updateTime']) : ''; + $friend['deleteTime'] = !empty($friend['deleteTime']) ? date('Y-m-d H:i:s', $friend['deleteTime']) : ''; + } + unset($friend); + + $wechatGroupsOptions = array_merge($wechatGroupsOptions, $friendList); + } + + // 处理手动创建的群组(字符串ID) + if (!empty($manualGroupIds)) { + foreach ($manualGroupIds as $groupId) { + // 手动创建的群组,只返回基本信息 + $wechatGroupsOptions[] = [ + 'id' => $groupId, + 'wechatId' => $groupId, + 'nickname' => $groupId, + 'avatar' => '', + 'isManual' => 1 + ]; + } + } + + $workbench->config->wechatGroupsOptions = $wechatGroupsOptions; + } else { + $workbench->config->wechatGroupsOptions = []; + } + + // 获取管理员选项(自动建群) + if ($workbench->type == self::TYPE_GROUP_CREATE && !empty($workbench->config->admins)) { + $adminOptions = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->where('wf.id', 'in', $workbench->config->admins) + ->order('wf.id', 'desc') + ->field('wf.id,wf.wechatId,wf.nickname as friendName,wf.avatar as friendAvatar,wf.conRemark,wf.ownerWechatId,wa.nickName as accountName,wa.avatar as accountAvatar') + ->select(); + $workbench->config->adminsOptions = $adminOptions; + } else { + $workbench->config->adminsOptions = []; + } + + return json(['code' => 200, 'msg' => '获取成功', 'data' => $workbench]); + } + + /** + * 更新工作台 + * @return \think\response\Json + */ + public function update() + { + if (!$this->request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + // 获取请求参数 + $param = $this->request->post(); + + // 验证数据 + $validate = new WorkbenchValidate; + if (!$validate->scene('update')->check($param)) { + return json(['code' => 400, 'msg' => $validate->getError()]); + } + + + $where = [ + ['id', '=', $param['id']], + ['companyId', '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + // 查询工作台是否存在 + $workbench = Workbench::where($where)->find(); + if (!$workbench) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + + Db::startTrans(); + try { + // 更新工作台基本信息 + $workbench->name = $param['name']; + $workbench->status = !empty($param['status']) ? 1 : 0; + $workbench->autoStart = !empty($param['autoStart']) ? 1 : 0; + $workbench->updateTime = time(); + $workbench->save(); + + // 根据类型更新对应的配置 + switch ($workbench->type) { + case self::TYPE_AUTO_LIKE: + $config = WorkbenchAutoLike::where('workbenchId', $param['id'])->find(); + if ($config) { + $config->interval = $param['interval']; + $config->maxLikes = $param['maxLikes']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->contentTypes = json_encode($param['contentTypes']); + $config->devices = json_encode($param['deviceGroups']); + $config->friends = json_encode($param['wechatFriends']); + // $config->targetGroups = json_encode($param['targetGroups']); + // $config->tagOperator = $param['tagOperator']; + $config->friendMaxLikes = $param['friendMaxLikes']; + $config->friendTags = $param['friendTags']; + $config->enableFriendTags = $param['enableFriendTags']; + $config->updateTime = time(); + $config->save(); + } + break; + + case self::TYPE_MOMENTS_SYNC: + $config = WorkbenchMomentsSync::where('workbenchId', $param['id'])->find(); + if ($config) { + if (!empty($param['contentGroups'])) { + foreach ($param['contentGroups'] as $library) { + if (isset($library['id']) && !empty($library['id'])) { + $contentLibraries[] = $library['id']; + } else { + $contentLibraries[] = $library; + } + } + } else { + $contentLibraries = []; + } + + $config->syncInterval = $param['syncInterval']; + $config->syncCount = $param['syncCount']; + $config->syncType = $param['syncType']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->accountType = $param['accountType']; + $config->devices = json_encode($param['deviceGroups']); + $config->contentLibraries = json_encode($contentLibraries); + $config->updateTime = time(); + $config->save(); + } + break; + + case self::TYPE_GROUP_PUSH: + $config = WorkbenchGroupPush::where('workbenchId', $param['id'])->find(); + if ($config) { + $ownerWechatIds = $this->normalizeOwnerWechatIds($param['ownerWechatIds'] ?? null, $config); + $groupPushData = $this->prepareGroupPushData($param, $ownerWechatIds, $config); + $groupPushData['updateTime'] = time(); + $config->save($groupPushData); + } + break; + + case self::TYPE_GROUP_CREATE: + $config = WorkbenchGroupCreate::where('workbenchId', $param['id'])->find(); + if ($config) { + $config->devices = json_encode($param['deviceGroups'] ?? [], JSON_UNESCAPED_UNICODE); + $config->startTime = $param['startTime'] ?? ''; + $config->endTime = $param['endTime'] ?? ''; + $config->groupSizeMin = intval($param['groupSizeMin'] ?? 3); + $config->groupSizeMax = intval($param['groupSizeMax'] ?? 38); + $config->maxGroupsPerDay = intval($param['maxGroupsPerDay'] ?? 20); + $config->groupNameTemplate = $param['groupNameTemplate'] ?? ''; + $config->groupDescription = $param['groupDescription'] ?? ''; + $config->poolGroups = json_encode($param['poolGroups'] ?? [], JSON_UNESCAPED_UNICODE); + $config->wechatGroups = json_encode($param['wechatGroups'] ?? [], JSON_UNESCAPED_UNICODE); + + // 处理群管理员:如果启用了群管理员且有指定管理员,则保存到admins字段 + $admins = []; + if (!empty($param['groupAdminEnabled']) && !empty($param['groupAdminWechatId'])) { + // 如果groupAdminWechatId是数组,取第一个;如果是单个值,直接使用 + $adminWechatId = is_array($param['groupAdminWechatId']) ? $param['groupAdminWechatId'][0] : $param['groupAdminWechatId']; + // 如果是好友ID,直接添加到admins;如果是wechatId,需要转换为好友ID + if (is_numeric($adminWechatId)) { + $admins[] = intval($adminWechatId); + } else { + // 如果是wechatId字符串,需要查询对应的好友ID + $friend = Db::table('s2_wechat_friend')->where('wechatId', $adminWechatId)->find(); + if ($friend) { + $admins[] = intval($friend['id']); + } + } + } + // 如果传入了admins参数,优先使用(兼容旧逻辑) + if (!empty($param['admins']) && is_array($param['admins'])) { + $admins = array_merge($admins, $param['admins']); + } + $config->admins = json_encode(array_unique($admins), JSON_UNESCAPED_UNICODE); + + $config->updateTime = time(); + $config->save(); + } + break; + case self::TYPE_TRAFFIC_DISTRIBUTION: + $config = WorkbenchTrafficConfig::where('workbenchId', $param['id'])->find(); + if ($config) { + $config->distributeType = $param['distributeType']; + $config->maxPerDay = $param['maxPerDay']; + $config->timeType = $param['timeType']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->devices = json_encode($param['deviceGroups']); + $config->pools = json_encode($param['poolGroups']); + $config->account = json_encode($param['accountGroups']); + $config->updateTime = time(); + $config->save(); + } + break; + case self::TYPE_IMPORT_CONTACT: //联系人导入 + $config = WorkbenchImportContact::where('workbenchId', $param['id'])->find();; + if ($config) { + $config->devices = json_encode($param['deviceGroups']); + $config->pools = json_encode($param['poolGroups']); + $config->num = $param['num']; + $config->clearContact = $param['clearContact']; + $config->remark = $param['remark']; + $config->startTime = $param['startTime']; + $config->endTime = $param['endTime']; + $config->save(); + } + break; + } + + Db::commit(); + return json(['code' => 200, 'msg' => '更新成功']); + } catch (\Exception $e) { + Db::rollback(); + return json(['code' => 500, 'msg' => '更新失败:' . $e->getMessage()]); + } + } + + /** + * 更新工作台状态 + * @return \think\response\Json + */ + public function updateStatus() + { + if (!$this->request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + $id = $this->request->param('id', ''); + + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + + + $where = [ + ['id', '=', $id], + ['companyId', '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + $workbench = Workbench::where($where)->find(); + + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + + $workbench->status = !$workbench['status']; + $workbench->save(); + + return json(['code' => 200, 'msg' => '更新成功']); + } + + /** + * 删除工作台(软删除) + */ + public function delete() + { + $id = $this->request->param('id'); + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + $where = [ + ['id', '=', $id], + ['companyId', '=', $this->request->userInfo['companyId']], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + $workbench = Workbench::where($where)->find(); + + if (!$workbench) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + + // 软删除 + $workbench->isDel = 1; + $workbench->deleteTime = time(); + $workbench->save(); + + return json(['code' => 200, 'msg' => '删除成功']); + } + + /** + * 拷贝工作台 + * @return \think\response\Json + */ + public function copy() + { + if (!$this->request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + $id = $this->request->post('id'); + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + // 验证权限并获取原数据 + $workbench = Workbench::where([ + ['id', '=', $id], + ['userId', '=', $this->request->userInfo['id']] + ])->find(); + + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + + Db::startTrans(); + try { + // 创建新的工作台基本信息 + $newWorkbench = new Workbench; + $newWorkbench->name = $workbench->name . ' copy'; + $newWorkbench->type = $workbench->type; + $newWorkbench->status = 1; // 新拷贝的默认启用 + $newWorkbench->autoStart = $workbench->autoStart; + $newWorkbench->userId = $this->request->userInfo['id']; + $newWorkbench->companyId = $this->request->userInfo['companyId']; + $newWorkbench->save(); + + // 根据类型拷贝对应的配置 + switch ($workbench->type) { + case self::TYPE_AUTO_LIKE: + $config = WorkbenchAutoLike::where('workbenchId', $id)->find(); + if ($config) { + $newConfig = new WorkbenchAutoLike; + $newConfig->workbenchId = $newWorkbench->id; + $newConfig->interval = $config->interval; + $newConfig->maxLikes = $config->maxLikes; + $newConfig->startTime = $config->startTime; + $newConfig->endTime = $config->endTime; + $newConfig->contentTypes = $config->contentTypes; + $newConfig->devices = $config->devices; + $newConfig->friends = $config->friends; + $newConfig->createTime = time(); + $newConfig->updateTime = time(); + $newConfig->save(); + } + break; + case self::TYPE_MOMENTS_SYNC: + $config = WorkbenchMomentsSync::where('workbenchId', $id)->find(); + if ($config) { + $newConfig = new WorkbenchMomentsSync; + $newConfig->workbenchId = $newWorkbench->id; + $newConfig->syncInterval = $config->syncInterval; + $newConfig->syncCount = $config->syncCount; + $newConfig->syncType = $config->syncType; + $newConfig->startTime = $config->startTime; + $newConfig->endTime = $config->endTime; + $newConfig->accountType = $config->accountType; + $newConfig->devices = $config->devices; + $newConfig->contentLibraries = $config->contentLibraries; + $newConfig->createTime = time(); + $newConfig->updateTime = time(); + $newConfig->save(); + } + break; + case self::TYPE_GROUP_PUSH: + $config = WorkbenchGroupPush::where('workbenchId', $id)->find(); + if ($config) { + $newConfig = new WorkbenchGroupPush; + $newConfig->workbenchId = $newWorkbench->id; + $newConfig->pushType = $config->pushType; + $newConfig->targetType = isset($config->targetType) ? $config->targetType : 1; // 默认1=群推送 + $newConfig->startTime = $config->startTime; + $newConfig->endTime = $config->endTime; + $newConfig->maxPerDay = $config->maxPerDay; + $newConfig->pushOrder = $config->pushOrder; + $newConfig->isLoop = $config->isLoop; + $newConfig->status = $config->status; + $newConfig->groups = $config->groups; + $newConfig->friends = $config->friends; + $newConfig->contentLibraries = $config->contentLibraries; + $newConfig->trafficPools = property_exists($config, 'trafficPools') ? $config->trafficPools : json_encode([], JSON_UNESCAPED_UNICODE); + $newConfig->socialMediaId = $config->socialMediaId; + $newConfig->promotionSiteId = $config->promotionSiteId; + $newConfig->ownerWechatIds = $config->ownerWechatIds; + $newConfig->createTime = time(); + $newConfig->updateTime = time(); + $newConfig->save(); + } + break; + case self::TYPE_GROUP_CREATE: + $config = WorkbenchGroupCreate::where('workbenchId', $id)->find(); + if ($config) { + $newConfig = new WorkbenchGroupCreate; + $newConfig->workbenchId = $newWorkbench->id; + $newConfig->devices = $config->devices; + $newConfig->startTime = $config->startTime; + $newConfig->endTime = $config->endTime; + $newConfig->groupSizeMin = $config->groupSizeMin; + $newConfig->groupSizeMax = $config->groupSizeMax; + $newConfig->maxGroupsPerDay = $config->maxGroupsPerDay; + $newConfig->groupNameTemplate = $config->groupNameTemplate; + $newConfig->groupDescription = $config->groupDescription; + $newConfig->poolGroups = $config->poolGroups; + $newConfig->wechatGroups = $config->wechatGroups; + $newConfig->admins = $config->admins ?? json_encode([], JSON_UNESCAPED_UNICODE); + $newConfig->createTime = time(); + $newConfig->updateTime = time(); + $newConfig->save(); + } + break; + case self::TYPE_IMPORT_CONTACT: //联系人导入 + $config = WorkbenchImportContact::where('workbenchId', $id)->find(); + if ($config) { + $newConfig = new WorkbenchImportContact; + $newConfig->workbenchId = $newWorkbench->id; + $newConfig->devices = $config->devices; + $newConfig->pools = $config->pools; + $newConfig->num = $config->num; + $newConfig->clearContact = $config->clearContact; + $newConfig->remark = $config->remark; + $newConfig->startTime = $config->startTime; + $newConfig->endTime = $config->endTime; + $newConfig->createTime = time(); + $newConfig->save(); + } + break; + } + + Db::commit(); + return json(['code' => 200, 'msg' => '拷贝成功', 'data' => ['id' => $newWorkbench->id]]); + } catch (\Exception $e) { + Db::rollback(); + return json(['code' => 500, 'msg' => '拷贝失败:' . $e->getMessage()]); + } + } + + /** + * 获取点赞记录列表 + * @return \think\response\Json + */ + public function getLikeRecords() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchAutoLikeController(); + $controller->request = $this->request; + return $controller->getLikeRecords(); + } + + /** + * 获取朋友圈发布记录列表 + * @return \think\response\Json + */ + public function getMomentsRecords() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchMomentsController(); + $controller->request = $this->request; + return $controller->getMomentsRecords(); + } + + /** + * 获取朋友圈发布统计 + * @return \think\response\Json + */ + public function getMomentsStats() + { + $controller = new \app\cunkebao\controller\workbench\WorkbenchMomentsController(); + $controller->request = $this->request; + return $controller->getMomentsStats(); + } + + /** + * 获取流量分发记录列表 + * @return \think\response\Json + */ + public function getTrafficDistributionRecords() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $workbenchId = $this->request->param('workbenchId', 0); + + $where = [ + ['wtdi.workbenchId', '=', $workbenchId] + ]; + + // 查询分发记录 + $list = Db::name('workbench_traffic_distribution_item')->alias('wtdi') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wtdi.wechatAccountId', 'left') + ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wtdi.wechatFriendId', 'left') + ->field([ + 'wtdi.id', + 'wtdi.workbenchId', + 'wtdi.wechatAccountId', + 'wtdi.wechatFriendId', + 'wtdi.createTime as distributeTime', + 'wtdi.status', + 'wtdi.errorMsg', + 'wa.nickName as operatorName', + 'wa.avatar as operatorAvatar', + 'wf.nickName as friendName', + 'wf.avatar as friendAvatar', + 'wf.gender', + 'wf.province', + 'wf.city' + ]) + ->where($where) + ->order('wtdi.createTime', 'desc') + ->page($page, $limit) + ->select(); + + // 处理数据 + foreach ($list as &$item) { + // 处理时间格式 + $item['distributeTime'] = date('Y-m-d H:i:s', $item['distributeTime']); + + // 处理性别 + $genderMap = [ + 0 => '未知', + 1 => '男', + 2 => '女' + ]; + $item['genderText'] = $genderMap[$item['gender']] ?? '未知'; + + // 处理状态文字 + $statusMap = [ + 0 => '待分发', + 1 => '分发成功', + 2 => '分发失败' + ]; + $item['statusText'] = $statusMap[$item['status']] ?? '未知状态'; + } + + // 获取总记录数 + $total = Db::name('workbench_traffic_distribution_item')->alias('wtdi') + ->where($where) + ->count(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + + /** + * 获取流量分发统计 + * @return \think\response\Json + */ + public function getTrafficDistributionStats() + { + $workbenchId = $this->request->param('workbenchId', 0); + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + // 获取今日数据 + $todayStart = strtotime(date('Y-m-d') . ' 00:00:00'); + $todayEnd = strtotime(date('Y-m-d') . ' 23:59:59'); + + $todayStats = Db::name('workbench_traffic_distribution_item') + ->where([ + ['workbenchId', '=', $workbenchId], + ['createTime', 'between', [$todayStart, $todayEnd]] + ]) + ->field([ + 'COUNT(*) as total', + 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success', + 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed' + ]) + ->find(); + + // 获取总数据 + $totalStats = Db::name('workbench_traffic_distribution_item') + ->where('workbenchId', $workbenchId) + ->field([ + 'COUNT(*) as total', + 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success', + 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed' + ]) + ->find(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'today' => [ + 'total' => intval($todayStats['total']), + 'success' => intval($todayStats['success']), + 'failed' => intval($todayStats['failed']) + ], + 'total' => [ + 'total' => intval($totalStats['total']), + 'success' => intval($totalStats['success']), + 'failed' => intval($totalStats['failed']) + ] + ] + ]); + } + + /** + * 获取流量分发详情 + * @return \think\response\Json + */ + public function getTrafficDistributionDetail() + { + $id = $this->request->param('id', 0); + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + $detail = Db::name('workbench_traffic_distribution_item')->alias('wtdi') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wtdi.wechatAccountId', 'left') + ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wtdi.wechatFriendId', 'left') + ->field([ + 'wtdi.id', + 'wtdi.workbenchId', + 'wtdi.wechatAccountId', + 'wtdi.wechatFriendId', + 'wtdi.createTime as distributeTime', + 'wtdi.status', + 'wtdi.errorMsg', + 'wa.nickName as operatorName', + 'wa.avatar as operatorAvatar', + 'wf.nickName as friendName', + 'wf.avatar as friendAvatar', + 'wf.gender', + 'wf.province', + 'wf.city', + 'wf.signature', + 'wf.remark' + ]) + ->where('wtdi.id', $id) + ->find(); + + if (empty($detail)) { + return json(['code' => 404, 'msg' => '记录不存在']); + } + + // 处理数据 + $detail['distributeTime'] = date('Y-m-d H:i:s', $detail['distributeTime']); + + // 处理性别 + $genderMap = [ + 0 => '未知', + 1 => '男', + 2 => '女' + ]; + $detail['genderText'] = $genderMap[$detail['gender']] ?? '未知'; + + // 处理状态文字 + $statusMap = [ + 0 => '待分发', + 1 => '分发成功', + 2 => '分发失败' + ]; + $detail['statusText'] = $statusMap[$detail['status']] ?? '未知状态'; + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => $detail + ]); + } + + /** + * 创建流量分发计划 + * @return \think\response\Json + */ + public function createTrafficPlan() + { + $param = $this->request->post(); + Db::startTrans(); + try { + // 1. 创建主表 + $planId = Db::name('ck_workbench')->insertGetId([ + 'name' => $param['name'], + 'type' => self::TYPE_TRAFFIC_DISTRIBUTION, + 'status' => 1, + 'autoStart' => $param['autoStart'] ?? 0, + 'userId' => $this->request->userInfo['id'], + 'companyId' => $this->request->userInfo['companyId'], + 'createTime' => time(), + 'updateTime' => time() + ]); + // 2. 创建扩展表 + Db::name('ck_workbench_traffic_config')->insert([ + 'workbenchId' => $planId, + 'distributeType' => $param['distributeType'], + 'maxPerDay' => $param['maxPerDay'], + 'timeType' => $param['timeType'], + 'startTime' => $param['startTime'], + 'endTime' => $param['endTime'], + 'targets' => json_encode($param['targets'], JSON_UNESCAPED_UNICODE), + 'pools' => json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE), + 'createTime' => time(), + 'updateTime' => time() + ]); + Db::commit(); + return json(['code' => 200, 'msg' => '创建成功']); + } catch (\Exception $e) { + Db::rollback(); + return json(['code' => 500, 'msg' => '创建失败:' . $e->getMessage()]); + } + } + + /** + * 获取所有微信好友标签及数量统计 + * @return \think\response\Json + */ + public function getDeviceLabels() + { + $deviceIds = $this->request->param('deviceIds', ''); + $companyId = $this->request->userInfo['companyId']; + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + + $where = [ + ['wc.companyId', '=', $companyId], + ]; + + if (!empty($deviceIds)) { + $deviceIds = explode(',', $deviceIds); + $where[] = ['dwl.deviceId', 'in', $deviceIds]; + } + + $wechatAccounts = Db::name('wechat_customer')->alias('wc') + ->join('device_wechat_login dwl', 'dwl.wechatId = wc.wechatId AND dwl.companyId = wc.companyId AND dwl.alive = 1') + ->join(['s2_wechat_account' => 'wa'], 'wa.wechatId = wc.wechatId') + ->where($where) + ->field('wa.id,wa.wechatId,wa.nickName,wa.labels') + ->select(); + $labels = []; + $wechatIds = []; + foreach ($wechatAccounts as $account) { + $labelArr = json_decode($account['labels'], true); + if (is_array($labelArr)) { + foreach ($labelArr as $label) { + if ($label !== '' && $label !== null) { + $labels[] = $label; + } + } + } + $wechatIds[] = $account['wechatId']; + } + // 去重(只保留一个) + $labels = array_values(array_unique($labels)); + $wechatIds = array_unique($wechatIds); + + // 搜索过滤 + if (!empty($keyword)) { + $labels = array_filter($labels, function ($label) use ($keyword) { + return mb_stripos($label, $keyword) !== false; + }); + $labels = array_values($labels); // 重新索引数组 + } + + + // 分页处理 + $labels2 = array_slice($labels, ($page - 1) * $limit, $limit); + + // 统计数量 + $newLabel = []; + foreach ($labels2 as $label) { + $friendCount = Db::table('s2_wechat_friend') + ->whereIn('ownerWechatId', $wechatIds) + ->where('labels', 'like', '%"' . $label . '"%') + ->count(); + $newLabel[] = [ + 'label' => $label, + 'count' => $friendCount + ]; + } + + // 返回结果 + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $newLabel, + 'total' => count($labels), + ] + ]); + } + + + /** + * 获取群列表 + * @return \think\response\Json + */ + public function getGroupList() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + + $where = [ + ['wg.deleteTime', '=', 0], + ['wg.companyId', '=', $this->request->userInfo['companyId']], + ]; + + if (!empty($keyword)) { + $where[] = ['wg.name', 'like', '%' . $keyword . '%']; + } + + $query = Db::name('wechat_group')->alias('wg') + ->join('wechat_account wa', 'wa.wechatId = wg.ownerWechatId') + ->where($where); + + $total = $query->count(); + $list = $query->order('wg.id', 'desc') + ->field('wg.id,wg.name as groupName,wg.ownerWechatId,wa.nickName,wg.createTime,wa.avatar,wa.alias,wg.avatar as groupAvatar') + ->page($page, $limit) + ->select(); + + // 优化:格式化时间,头像兜底 + $defaultGroupAvatar = ''; + $defaultAvatar = ''; + foreach ($list as &$item) { + $item['createTime'] = $item['createTime'] ? date('Y-m-d H:i:s', $item['createTime']) : ''; + $item['groupAvatar'] = $item['groupAvatar'] ?: $defaultGroupAvatar; + $item['avatar'] = $item['avatar'] ?: $defaultAvatar; + } + + return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]); + } + + /** + * 获取流量池列表 + * @return \think\response\Json + */ + public function getTrafficPoolList() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + $companyId = $this->request->userInfo['companyId']; + + $baseQuery = Db::name('traffic_source_package')->alias('tsp') + ->where('tsp.isDel', 0) + ->whereIn('tsp.companyId', [$companyId, 0]); + + if (!empty($keyword)) { + $baseQuery->whereLike('tsp.name', '%' . $keyword . '%'); + } + + $total = (clone $baseQuery)->count(); + + $list = $baseQuery + ->leftJoin('traffic_source_package_item tspi', 'tspi.packageId = tsp.id and tspi.isDel = 0') + ->field('tsp.id,tsp.name,tsp.description,tsp.pic,tsp.companyId,COUNT(tspi.id) as itemCount,max(tspi.createTime) as latestImportTime') + ->group('tsp.id') + ->order('tsp.id', 'desc') + ->page($page, $limit) + ->select(); + + foreach ($list as &$item) { + $item['latestImportTime'] = !empty($item['latestImportTime']) ? date('Y-m-d H:i:s', $item['latestImportTime']) : ''; + } + unset($item); + + return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]); + } + + + public function getAccountList() + { + $companyId = $this->request->userInfo['companyId']; + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $query = Db::table('s2_company_account') + ->alias('a') + ->where(['a.departmentId' => $companyId, 'a.status' => 0]) + ->whereNotLike('a.userName', '%_offline%') + ->whereNotLike('a.userName', '%_delete%'); + + $total = $query->count(); + $list = $query->field('a.id,a.userName,a.realName,a.nickname,a.memo') + ->page($page, $limit) + ->select(); + + + return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]); + } + + + /** + * 获取京东联盟导购媒体 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function getJdSocialMedia() + { + $data = Db::name('jd_social_media')->order('id DESC')->select(); + return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]); + } + + /** + * 获取京东联盟广告位 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function getJdPromotionSite() + { + $id = $this->request->param('id', ''); + if (empty($id)) { + return json(['code' => 500, 'msg' => '参数缺失']); + } + + $data = Db::name('jd_promotion_site')->where('jdSocialMediaId', $id)->order('id DESC')->select(); + return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]); + } + + + //京东转链-京推推 + public function changeLink($content = '', $positionid = '') + { + $unionId = Env::get('jd.unionId', ''); + $jttAppId = Env::get('jd.jttAppId', ''); + $appKey = Env::get('jd.appKey', ''); + $apiUrl = Env::get('jd.apiUrl', ''); + + $content = !empty($content) ? $content : $this->request->param('content', ''); + $positionid = !empty($positionid) ? $positionid : $this->request->param('positionid', ''); + + + if (empty($content)) { + return json_encode(['code' => 500, 'msg' => '转链的内容为空']); + } + + // 验证是否包含链接 + if (!$this->containsLink($content)) { + return json_encode(['code' => 500, 'msg' => '内容中未检测到有效链接']); + } + + if (empty($unionId) || empty($jttAppId) || empty($appKey) || empty($apiUrl)) { + return json_encode(['code' => 500, 'msg' => '参数缺失']); + } + $params = [ + 'unionid' => $unionId, + 'content' => $content, + 'appid' => $jttAppId, + 'appkey' => $appKey, + 'v' => 'v2' + ]; + + if (!empty($positionid)) { + $params['positionid'] = $positionid; + } + + $res = requestCurl($apiUrl, $params, 'GET', [], 'json'); + $res = json_decode($res, true); + if (empty($res)) { + return json_encode(['code' => 500, 'msg' => '未知错误']); + } + $result = $res['result']; + if ($res['return'] == 0) { + return json_encode(['code' => 200, 'data' => $result['chain_content'], 'msg' => $result['msg']]); + } else { + return json_encode(['code' => 500, 'msg' => $result['msg']]); + } + } + + + public function getTrafficList() + { + $companyId = $this->request->userInfo['companyId']; + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + $workbenchId = $this->request->param('workbenchId', ''); + $isRecycle = $this->request->param('isRecycle', ''); + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + $workbench = Db::name('workbench')->where(['id' => $workbenchId, 'isDel' => 0, 'companyId' => $companyId, 'type' => 5])->find(); + + if (empty($workbench)) { + return json(['code' => 400, 'msg' => '该任务不存在或已删除']); + } + $query = Db::name('workbench_traffic_config_item')->alias('wtc') + ->join(['s2_wechat_friend' => 'wf'], 'wtc.wechatFriendId = wf.id') + ->join('users u', 'wtc.wechatAccountId = u.s2_accountId', 'left') + ->field([ + 'wtc.id', 'wtc.isRecycle', 'wtc.isRecycle', 'wtc.createTime','wtc.recycleTime', + 'wf.wechatId', 'wf.alias', 'wf.nickname', 'wf.avatar', 'wf.gender', 'wf.phone', + 'u.account', 'u.username' + ]) + ->where(['wtc.workbenchId' => $workbenchId]) + ->order('wtc.id DESC'); + + if (!empty($keyword)) { + $query->where('wf.wechatId|wf.alias|wf.nickname|wf.phone|u.account|u.username', 'like', '%' . $keyword . '%'); + } + + if ($isRecycle != '' || $isRecycle != null) { + $query->where('isRecycle',$isRecycle); + } + + + + $total = $query->count(); + $list = $query->page($page, $limit)->select(); + + foreach ($list as &$item) { + $item['createTime'] = date('Y-m-d H:i:s', $item['createTime']); + $item['recycleTime'] = date('Y-m-d H:i:s', $item['recycleTime']); + } + unset($item); + + + $data = [ + 'total' => $total, + 'list' => $list, + ]; + + return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]); + + } + + /** + * 规范化客服微信ID列表 + * @param mixed $ownerWechatIds + * @param WorkbenchGroupPush|null $originalConfig + * @return array + * @throws \Exception + */ + private function normalizeOwnerWechatIds($ownerWechatIds, WorkbenchGroupPush $originalConfig = null): array + { + if ($ownerWechatIds === null) { + $existing = $originalConfig ? $this->decodeJsonArray($originalConfig->ownerWechatIds ?? []) : []; + if (empty($existing)) { + throw new \Exception('请至少选择一个客服微信'); + } + return $existing; + } + + if (!is_array($ownerWechatIds)) { + throw new \Exception('客服参数格式错误'); + } + + $normalized = $this->extractIdList($ownerWechatIds, '客服参数格式错误'); + if (empty($normalized)) { + throw new \Exception('请至少选择一个客服微信'); + } + return $normalized; + } + + /** + * 构建群推送配置数据 + * @param array $param + * @param array $ownerWechatIds + * @param WorkbenchGroupPush|null $originalConfig + * @return array + * @throws \Exception + */ + private function prepareGroupPushData(array $param, array $ownerWechatIds, WorkbenchGroupPush $originalConfig = null): array + { + $targetTypeDefault = $originalConfig ? intval($originalConfig->targetType) : 1; + $targetType = intval($this->getParamValue($param, 'targetType', $targetTypeDefault)) ?: 1; + + $groupPushSubTypeDefault = $originalConfig ? intval($originalConfig->groupPushSubType) : 1; + $groupPushSubType = intval($this->getParamValue($param, 'groupPushSubType', $groupPushSubTypeDefault)) ?: 1; + if (!in_array($groupPushSubType, [1, 2], true)) { + $groupPushSubType = 1; + } + + $data = [ + 'pushType' => $this->toBoolInt($this->getParamValue($param, 'pushType', $originalConfig->pushType ?? 0)), + 'targetType' => $targetType, + 'startTime' => $this->getParamValue($param, 'startTime', $originalConfig->startTime ?? ''), + 'endTime' => $this->getParamValue($param, 'endTime', $originalConfig->endTime ?? ''), + 'maxPerDay' => intval($this->getParamValue($param, 'maxPerDay', $originalConfig->maxPerDay ?? 0)), + 'pushOrder' => $this->getParamValue($param, 'pushOrder', $originalConfig->pushOrder ?? 1), + 'groupPushSubType' => $groupPushSubType, + 'status' => $this->toBoolInt($this->getParamValue($param, 'status', $originalConfig->status ?? 0)), + 'socialMediaId' => $this->getParamValue($param, 'socialMediaId', $originalConfig->socialMediaId ?? ''), + 'promotionSiteId' => $this->getParamValue($param, 'promotionSiteId', $originalConfig->promotionSiteId ?? ''), + 'friendIntervalMin' => intval($this->getParamValue($param, 'friendIntervalMin', $originalConfig->friendIntervalMin ?? 10)), + 'friendIntervalMax' => intval($this->getParamValue($param, 'friendIntervalMax', $originalConfig->friendIntervalMax ?? 20)), + 'messageIntervalMin' => intval($this->getParamValue($param, 'messageIntervalMin', $originalConfig->messageIntervalMin ?? 1)), + 'messageIntervalMax' => intval($this->getParamValue($param, 'messageIntervalMax', $originalConfig->messageIntervalMax ?? 12)), + 'isRandomTemplate' => $this->toBoolInt($this->getParamValue($param, 'isRandomTemplate', $originalConfig->isRandomTemplate ?? 0)), + 'ownerWechatIds' => json_encode($ownerWechatIds, JSON_UNESCAPED_UNICODE), + ]; + + if ($data['friendIntervalMin'] > $data['friendIntervalMax']) { + throw new \Exception('目标间最小间隔不能大于最大间隔'); + } + if ($data['messageIntervalMin'] > $data['messageIntervalMax']) { + throw new \Exception('消息间最小间隔不能大于最大间隔'); + } + + $contentGroupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->contentLibraries ?? []) : []; + $contentGroupsParam = $this->getParamValue($param, 'contentGroups', null); + $contentGroups = $contentGroupsParam !== null + ? $this->extractIdList($contentGroupsParam, '内容库参数格式错误') + : $contentGroupsExisting; + $data['contentLibraries'] = json_encode($contentGroups, JSON_UNESCAPED_UNICODE); + + $postPushTagsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->postPushTags ?? []) : []; + $postPushTagsParam = $this->getParamValue($param, 'postPushTags', null); + $postPushTags = $postPushTagsParam !== null + ? $this->extractIdList($postPushTagsParam, '推送标签参数格式错误') + : $postPushTagsExisting; + $data['postPushTags'] = json_encode($postPushTags, JSON_UNESCAPED_UNICODE); + + if ($targetType === 1) { + $data['isLoop'] = $this->toBoolInt($this->getParamValue($param, 'isLoop', $originalConfig->isLoop ?? 0)); + + $groupsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->groups ?? []) : []; + $wechatGroups = array_key_exists('wechatGroups', $param) + ? $this->extractIdList($param['wechatGroups'], '群参数格式错误') + : $groupsExisting; + if (empty($wechatGroups)) { + throw new \Exception('群推送必须选择微信群'); + } + $data['groups'] = json_encode($wechatGroups, JSON_UNESCAPED_UNICODE); + $data['friends'] = json_encode([], JSON_UNESCAPED_UNICODE); + $data['trafficPools'] = json_encode([], JSON_UNESCAPED_UNICODE); + + if ($groupPushSubType === 2) { + $announcementContent = $this->getParamValue($param, 'announcementContent', $originalConfig->announcementContent ?? ''); + if (empty($announcementContent)) { + throw new \Exception('群公告必须输入公告内容'); + } + $enableAiRewrite = $this->toBoolInt($this->getParamValue($param, 'enableAiRewrite', $originalConfig->enableAiRewrite ?? 0)); + $aiRewritePrompt = trim((string)$this->getParamValue($param, 'aiRewritePrompt', $originalConfig->aiRewritePrompt ?? '')); + if ($enableAiRewrite === 1 && $aiRewritePrompt === '') { + throw new \Exception('启用AI智能话术改写时,必须输入改写提示词'); + } + $data['announcementContent'] = $announcementContent; + $data['enableAiRewrite'] = $enableAiRewrite; + $data['aiRewritePrompt'] = $aiRewritePrompt; + } else { + $data['groupPushSubType'] = 1; + $data['announcementContent'] = ''; + $data['enableAiRewrite'] = 0; + $data['aiRewritePrompt'] = ''; + } + } else { + $data['isLoop'] = 0; + $friendsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->friends ?? []) : []; + $trafficPoolsExisting = $originalConfig ? $this->decodeJsonArray($originalConfig->trafficPools ?? []) : []; + + $friendTargets = array_key_exists('wechatFriends', $param) + ? $this->extractIdList($param['wechatFriends'], '好友参数格式错误') + : $friendsExisting; + $trafficPools = array_key_exists('trafficPools', $param) + ? $this->extractIdList($param['trafficPools'], '流量池参数格式错误') + : $trafficPoolsExisting; + + if (empty($friendTargets) && empty($trafficPools)) { + throw new \Exception('好友推送需至少选择好友或流量池'); + } + + $data['friends'] = json_encode($friendTargets, JSON_UNESCAPED_UNICODE); + $data['trafficPools'] = json_encode($trafficPools, JSON_UNESCAPED_UNICODE); + $data['groups'] = json_encode([], JSON_UNESCAPED_UNICODE); + $data['groupPushSubType'] = 1; + $data['announcementContent'] = ''; + $data['enableAiRewrite'] = 0; + $data['aiRewritePrompt'] = ''; + } + + return $data; + } + + /** + * 获取参数值,若不存在则返回默认值 + * @param array $param + * @param string $key + * @param mixed $default + * @return mixed + */ + private function getParamValue(array $param, string $key, $default) + { + return array_key_exists($key, $param) ? $param[$key] : $default; + } + + /** + * 将值转换为整型布尔 + * @param mixed $value + * @return int + */ + private function toBoolInt($value): int + { + return empty($value) ? 0 : 1; + } + + /** + * 从参数中提取ID列表 + * @param mixed $items + * @param string $errorMessage + * @return array + * @throws \Exception + */ + private function extractIdList($items, string $errorMessage = '参数格式错误'): array + { + if (!is_array($items)) { + throw new \Exception($errorMessage); + } + + $ids = []; + foreach ($items as $item) { + if (is_array($item) && isset($item['id'])) { + $item = $item['id']; + } + if ($item === '' || $item === null) { + continue; + } + $ids[] = $item; + } + + return array_values(array_unique($ids)); + } + + /** + * 解码JSON数组 + * @param mixed $value + * @return array + */ + private function decodeJsonArray($value): array + { + if (empty($value)) { + return []; + } + if (is_array($value)) { + return $value; + } + $decoded = json_decode($value, true); + return is_array($decoded) ? $decoded : []; + } + + /** + * 验证内容是否包含链接 + * @param string $content 要检测的内容 + * @return bool + */ + private function containsLink($content) + { + // 定义各种链接的正则表达式模式 + $patterns = [ + // HTTP/HTTPS链接 + '/https?:\/\/[^\s]+/i', + // 京东商品链接 + '/item\.jd\.com\/\d+/i', + // 京东短链接 + '/u\.jd\.com\/[a-zA-Z0-9]+/i', + // 淘宝商品链接 + '/item\.taobao\.com\/item\.htm\?id=\d+/i', + // 天猫商品链接 + '/detail\.tmall\.com\/item\.htm\?id=\d+/i', + // 淘宝短链接 + '/m\.tb\.cn\/[a-zA-Z0-9]+/i', + // 拼多多链接 + '/mobile\.yangkeduo\.com\/goods\.html\?goods_id=\d+/i', + // 苏宁易购链接 + '/product\.suning\.com\/\d+\/\d+\.html/i', + // 通用域名模式(包含常见电商域名) + '/(?:jd|taobao|tmall|yangkeduo|suning|amazon|dangdang)\.com[^\s]*/i', + // 通用短链接模式 + '/[a-zA-Z0-9-]+\.[a-zA-Z]{2,}\/[a-zA-Z0-9\-._~:\/?#\[\]@!$&\'()*+,;=]+/i' + ]; + + // 遍历所有模式进行匹配 + foreach ($patterns as $pattern) { + if (preg_match($pattern, $content)) { + return true; + } + } + + return false; + } + + + /** + * 获取通讯录导入记录列表 + * @return \think\response\Json + */ + public function getImportContact() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $workbenchId = $this->request->param('workbenchId', 0); + + $where = [ + ['wici.workbenchId', '=', $workbenchId] + ]; + + // 查询发布记录 + $list = Db::name('workbench_import_contact_item')->alias('wici') + ->join('traffic_pool tp', 'tp.id = wici.poolId', 'left') + ->join('traffic_source tc', 'tc.identifier = tp.identifier', 'left') + ->join('wechat_account wa', 'wa.wechatId = tp.wechatId', 'left') + ->field([ + 'wici.id', + 'wici.workbenchId', + 'wici.createTime', + 'tp.identifier', + 'tp.mobile', + 'tp.wechatId', + 'tc.name', + 'wa.nickName', + 'wa.avatar', + 'wa.alias', + ]) + ->where($where) + ->order('tc.name DESC,wici.createTime DESC') + ->group('tp.identifier') + ->page($page, $limit) + ->select(); + + foreach ($list as &$item) { + $item['createTime'] = date('Y-m-d H:i:s', $item['createTime']); + } + + + // 获取总记录数 + $total = Db::name('workbench_import_contact_item')->alias('wici') + ->where($where) + ->count(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + ] + ]); + } + + /** + * 获取群发统计数据 + * @return \think\response\Json + */ + public function getGroupPushStats() + { + $workbenchId = $this->request->param('workbenchId', 0); + $timeRange = $this->request->param('timeRange', '7'); // 默认最近7天 + $contentLibraryIds = $this->request->param('contentLibraryIds', ''); // 话术组筛选 + $userId = $this->request->userInfo['id']; + + // 如果指定了工作台ID,则验证权限 + if (!empty($workbenchId)) { + $workbench = Workbench::where([ + ['id', '=', $workbenchId], + ['userId', '=', $userId], + ['type', '=', self::TYPE_GROUP_PUSH], + ['isDel', '=', 0] + ])->find(); + + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + } + + // 计算时间范围 + $days = intval($timeRange); + $startTime = strtotime(date('Y-m-d 00:00:00', strtotime("-{$days} days"))); + $endTime = time(); + + // 构建查询条件 + $where = [ + ['wgpi.createTime', '>=', $startTime], + ['wgpi.createTime', '<=', $endTime] + ]; + + // 如果指定了工作台ID,则限制查询范围 + if (!empty($workbenchId)) { + $where[] = ['wgpi.workbenchId', '=', $workbenchId]; + } else { + // 如果没有指定工作台ID,则查询当前用户的所有群推送工作台 + $workbenchIds = Workbench::where([ + ['userId', '=', $userId], + ['type', '=', self::TYPE_GROUP_PUSH], + ['isDel', '=', 0] + ])->column('id'); + + if (empty($workbenchIds)) { + // 如果没有工作台,返回空结果 + $workbenchIds = [-1]; + } + $where[] = ['wgpi.workbenchId', 'in', $workbenchIds]; + } + + // 话术组筛选 - 先获取符合条件的内容ID列表 + $contentIds = null; + if (!empty($contentLibraryIds)) { + $libraryIds = is_array($contentLibraryIds) ? $contentLibraryIds : explode(',', $contentLibraryIds); + $libraryIds = array_filter(array_map('intval', $libraryIds)); + if (!empty($libraryIds)) { + // 查询符合条件的内容ID + $contentIds = Db::name('content_item') + ->whereIn('libraryId', $libraryIds) + ->column('id'); + if (empty($contentIds)) { + // 如果没有符合条件的内容,返回空结果 + $contentIds = [-1]; // 使用不存在的ID,确保查询结果为空 + } + } + } + + // 1. 基础统计:触达率、回复率、平均回复时间、链接点击率 + $stats = $this->calculateBasicStats($workbenchId, $where, $startTime, $endTime, $contentIds); + + // 2. 话术组对比 + $contentLibraryComparison = $this->getContentLibraryComparison($workbenchId, $where, $startTime, $endTime, $contentIds); + + // 3. 时段分析 + $timePeriodAnalysis = $this->getTimePeriodAnalysis($workbenchId, $where, $startTime, $endTime, $contentIds); + + // 4. 互动深度(可选,需要更多数据) + $interactionDepth = $this->getInteractionDepth($workbenchId, $where, $startTime, $endTime, $contentIds); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'basicStats' => $stats, + 'contentLibraryComparison' => $contentLibraryComparison, + 'timePeriodAnalysis' => $timePeriodAnalysis, + 'interactionDepth' => $interactionDepth + ] + ]); + } + + /** + * 计算基础统计数据 + */ + private function calculateBasicStats($workbenchId, $where, $startTime, $endTime, $contentIds = null) + { + // 获取工作台配置,计算计划发送数 + // 如果 workbenchId 为空,则查询所有工作台的配置 + $configQuery = WorkbenchGroupPush::alias('wgp') + ->join('workbench w', 'w.id = wgp.workbenchId', 'left') + ->where('w.type', self::TYPE_GROUP_PUSH) + ->where('w.isDel', 0); + + if (!empty($workbenchId)) { + $configQuery->where('wgp.workbenchId', $workbenchId); + } else { + // 如果没有指定工作台ID,需要从 where 条件中获取 workbenchId 列表 + $workbenchIdCondition = null; + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') { + if ($condition[1] === 'in' && is_array($condition[2])) { + $workbenchIdCondition = $condition[2]; + break; + } elseif ($condition[1] === '=') { + $workbenchIdCondition = [$condition[2]]; + break; + } + } + } + if ($workbenchIdCondition) { + $configQuery->whereIn('wgp.workbenchId', $workbenchIdCondition); + } + } + + $configs = $configQuery->select(); + $targetType = 1; // 默认值 + if (!empty($configs)) { + // 如果只有一个配置,使用它的 targetType;如果有多个,默认使用1 + $targetType = intval($configs[0]->targetType ?? 1); + } + + // 计划发送数(根据配置计算) + $plannedSend = 0; + if (!empty($configs)) { + $days = ceil(($endTime - $startTime) / 86400); + foreach ($configs as $config) { + $maxPerDay = intval($config->maxPerDay ?? 0); + $configTargetType = intval($config->targetType ?? 1); + if ($configTargetType == 1) { + // 群推送:计划发送数 = 每日推送次数 * 天数 * 群数量 + $groups = $this->decodeJsonArray($config->groups ?? []); + $plannedSend += $maxPerDay * $days * count($groups); + } else { + // 好友推送:计划发送数 = 每日推送人数 * 天数 + $plannedSend += $maxPerDay * $days; + } + } + } + + // 构建查询条件 + $queryWhere = $where; + if ($contentIds !== null) { + $queryWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + + // 实际成功发送数(从推送记录表统计) + $successSend = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($queryWhere) + ->count(); + + // 触达率 = 成功发送数 / 计划发送数 + $reachRate = $plannedSend > 0 ? round(($successSend / $plannedSend) * 100, 1) : 0; + + // 获取发送记录列表,用于查询回复 + $sentItemIds = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($queryWhere) + ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId') + ->select(); + + // 回复统计(通过消息表查询) + $replyStats = $this->calculateReplyStats($sentItemIds, $targetType, $startTime, $endTime); + + // 链接点击统计 + $clickStats = $this->calculateClickStats($sentItemIds, $targetType, $startTime, $endTime); + + // 计算本月对比数据(简化处理,实际应该查询上个月同期数据) + $currentMonthStart = strtotime(date('Y-m-01 00:00:00')); + $lastMonthStart = strtotime(date('Y-m-01 00:00:00', strtotime('-1 month'))); + $lastMonthEnd = $currentMonthStart - 1; + + // 获取本月统计数据(避免递归调用) + $currentMonthWhere = [ + ['wgpi.createTime', '>=', $currentMonthStart] + ]; + // 复制 workbenchId 条件 + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') { + $currentMonthWhere[] = $condition; + break; + } + } + if ($contentIds !== null) { + $currentMonthWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + $currentMonthSend = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($currentMonthWhere) + ->count(); + + // 获取本月配置 + $currentMonthConfigQuery = WorkbenchGroupPush::alias('wgp') + ->join('workbench w', 'w.id = wgp.workbenchId', 'left') + ->where('w.type', self::TYPE_GROUP_PUSH) + ->where('w.isDel', 0); + if (!empty($workbenchId)) { + $currentMonthConfigQuery->where('wgp.workbenchId', $workbenchId); + } else { + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId' && $condition[1] === 'in') { + $currentMonthConfigQuery->whereIn('wgp.workbenchId', $condition[2]); + break; + } + } + } + $currentMonthConfigs = $currentMonthConfigQuery->select(); + $currentMonthPlanned = 0; + if (!empty($currentMonthConfigs)) { + $currentMonthDays = ceil(($endTime - $currentMonthStart) / 86400); + foreach ($currentMonthConfigs as $currentMonthConfig) { + $currentMonthMaxPerDay = intval($currentMonthConfig->maxPerDay ?? 0); + $currentMonthTargetType = intval($currentMonthConfig->targetType ?? 1); + if ($currentMonthTargetType == 1) { + $currentMonthGroups = $this->decodeJsonArray($currentMonthConfig->groups ?? []); + $currentMonthPlanned += $currentMonthMaxPerDay * $currentMonthDays * count($currentMonthGroups); + } else { + $currentMonthPlanned += $currentMonthMaxPerDay * $currentMonthDays; + } + } + } + $currentMonthReachRate = $currentMonthPlanned > 0 ? round(($currentMonthSend / $currentMonthPlanned) * 100, 1) : 0; + + // 获取上个月统计数据 + $lastMonthWhere = [ + ['wgpi.createTime', '>=', $lastMonthStart], + ['wgpi.createTime', '<=', $lastMonthEnd] + ]; + // 复制 workbenchId 条件 + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId') { + $lastMonthWhere[] = $condition; + break; + } + } + if ($contentIds !== null) { + $lastMonthWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + $lastMonthSend = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($lastMonthWhere) + ->count(); + + // 获取上个月配置 + $lastMonthConfigQuery = WorkbenchGroupPush::alias('wgp') + ->join('workbench w', 'w.id = wgp.workbenchId', 'left') + ->where('w.type', self::TYPE_GROUP_PUSH) + ->where('w.isDel', 0); + if (!empty($workbenchId)) { + $lastMonthConfigQuery->where('wgp.workbenchId', $workbenchId); + } else { + foreach ($where as $condition) { + if (is_array($condition) && isset($condition[0]) && $condition[0] === 'wgpi.workbenchId' && $condition[1] === 'in') { + $lastMonthConfigQuery->whereIn('wgp.workbenchId', $condition[2]); + break; + } + } + } + $lastMonthConfigs = $lastMonthConfigQuery->select(); + + $lastMonthPlanned = 0; + if (!empty($lastMonthConfigs)) { + $lastMonthDays = ceil(($lastMonthEnd - $lastMonthStart) / 86400); + foreach ($lastMonthConfigs as $lastMonthConfig) { + $lastMonthMaxPerDay = intval($lastMonthConfig->maxPerDay ?? 0); + $lastMonthTargetType = intval($lastMonthConfig->targetType ?? 1); + if ($lastMonthTargetType == 1) { + $lastMonthGroups = $this->decodeJsonArray($lastMonthConfig->groups ?? []); + $lastMonthPlanned += $lastMonthMaxPerDay * $lastMonthDays * count($lastMonthGroups); + } else { + $lastMonthPlanned += $lastMonthMaxPerDay * $lastMonthDays; + } + } + } + $lastMonthReachRate = $lastMonthPlanned > 0 ? round(($lastMonthSend / $lastMonthPlanned) * 100, 1) : 0; + + // 获取上个月的回复和点击统计(简化处理) + $lastMonthSentItems = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($lastMonthWhere) + ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId') + ->select(); + $lastMonthReplyStats = $this->calculateReplyStats($lastMonthSentItems, $targetType, $lastMonthStart, $lastMonthEnd); + $lastMonthClickStats = $this->calculateClickStats($lastMonthSentItems, $targetType, $lastMonthStart, $lastMonthEnd); + + return [ + 'reachRate' => [ + 'value' => $reachRate, + 'trend' => round($reachRate - $lastMonthReachRate, 1), + 'unit' => '%', + 'description' => '成功发送/计划发送' + ], + 'replyRate' => [ + 'value' => $replyStats['replyRate'], + 'trend' => round($replyStats['replyRate'] - $lastMonthReplyStats['replyRate'], 1), + 'unit' => '%', + 'description' => '收到回复/成功发送' + ], + 'avgReplyTime' => [ + 'value' => $replyStats['avgReplyTime'], + 'trend' => round($lastMonthReplyStats['avgReplyTime'] - $replyStats['avgReplyTime'], 0), + 'unit' => '分钟', + 'description' => '从发送到回复的平均时长' + ], + 'clickRate' => [ + 'value' => $clickStats['clickRate'], + 'trend' => round($clickStats['clickRate'] - $lastMonthClickStats['clickRate'], 1), + 'unit' => '%', + 'description' => '点击链接/成功发送' + ], + 'plannedSend' => $plannedSend, + 'successSend' => $successSend, + 'replyCount' => $replyStats['replyCount'], + 'clickCount' => $clickStats['clickCount'] + ]; + } + + /** + * 计算回复统计 + */ + private function calculateReplyStats($sentItems, $targetType, $startTime, $endTime) + { + if (empty($sentItems)) { + return ['replyRate' => 0, 'avgReplyTime' => 0, 'replyCount' => 0]; + } + + $replyCount = 0; + $totalReplyTime = 0; + $replyTimes = []; + + foreach ($sentItems as $item) { + $itemArray = is_array($item) ? $item : (array)$item; + $sendTime = $itemArray['createTime'] ?? 0; + $accountId = $itemArray['wechatAccountId'] ?? 0; + + if ($targetType == 1) { + // 群推送:查找群内回复消息 + $groupId = $itemArray['groupId'] ?? 0; + $group = Db::name('wechat_group')->where('id', $groupId)->find(); + if ($group) { + $replyMsg = Db::table('s2_wechat_message') + ->where('wechatChatroomId', $group['chatroomId']) + ->where('wechatAccountId', $accountId) + ->where('isSend', 0) // 接收的消息 + ->where('wechatTime', '>', $sendTime) + ->where('wechatTime', '<=', $sendTime + 86400) // 24小时内回复 + ->order('wechatTime', 'asc') + ->find(); + + if ($replyMsg) { + $replyCount++; + $replyTime = $replyMsg['wechatTime'] - $sendTime; + $replyTimes[] = $replyTime; + $totalReplyTime += $replyTime; + } + } + } else { + // 好友推送:查找好友回复消息 + $friendId = $itemArray['friendId'] ?? 0; + $friend = Db::table('s2_wechat_friend')->where('id', $friendId)->find(); + if ($friend) { + $replyMsg = Db::table('s2_wechat_message') + ->where('wechatFriendId', $friendId) + ->where('wechatAccountId', $accountId) + ->where('isSend', 0) // 接收的消息 + ->where('wechatTime', '>', $sendTime) + ->where('wechatTime', '<=', $sendTime + 86400) // 24小时内回复 + ->order('wechatTime', 'asc') + ->find(); + + if ($replyMsg) { + $replyCount++; + $replyTime = $replyMsg['wechatTime'] - $sendTime; + $replyTimes[] = $replyTime; + $totalReplyTime += $replyTime; + } + } + } + } + + $successSend = count($sentItems); + $replyRate = $successSend > 0 ? round(($replyCount / $successSend) * 100, 1) : 0; + $avgReplyTime = $replyCount > 0 ? round(($totalReplyTime / $replyCount) / 60, 0) : 0; // 转换为分钟 + + return [ + 'replyRate' => $replyRate, + 'avgReplyTime' => $avgReplyTime, + 'replyCount' => $replyCount + ]; + } + + /** + * 计算链接点击统计 + */ + private function calculateClickStats($sentItems, $targetType, $startTime, $endTime) + { + if (empty($sentItems)) { + return ['clickRate' => 0, 'clickCount' => 0]; + } + + $clickCount = 0; + $linkContentIds = []; + + // 获取所有发送的内容ID + foreach ($sentItems as $item) { + $itemArray = is_array($item) ? $item : (array)$item; + $contentId = $itemArray['contentId'] ?? 0; + if ($contentId > 0) { + $linkContentIds[] = $contentId; + } + } + + if (empty($linkContentIds)) { + return ['clickRate' => 0, 'clickCount' => 0]; + } + + // 查询包含链接的内容 + $linkContents = Db::name('content_item') + ->whereIn('id', array_unique($linkContentIds)) + ->where('contentType', 2) // 链接类型 + ->column('id'); + + // 统计发送了链接内容的记录数 + $linkSendCount = 0; + foreach ($sentItems as $item) { + $itemArray = is_array($item) ? $item : (array)$item; + $contentId = $itemArray['contentId'] ?? 0; + if (in_array($contentId, $linkContents)) { + $linkSendCount++; + } + } + + // 简化处理:假设点击率基于链接消息的发送(实际应该从点击追踪系统获取) + // 这里可以根据业务需求调整,比如通过消息中的链接点击事件统计 + $clickCount = $linkSendCount; // 简化处理,实际需要真实的点击数据 + + $successSend = count($sentItems); + $clickRate = $successSend > 0 ? round(($clickCount / $successSend) * 100, 1) : 0; + + return [ + 'clickRate' => $clickRate, + 'clickCount' => $clickCount + ]; + } + + /** + * 获取话术组对比数据 + */ + private function getContentLibraryComparison($workbenchId, $where, $startTime, $endTime, $contentIds = null) + { + $queryWhere = $where; + if ($contentIds !== null) { + $queryWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + + $comparison = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->join('content_item ci', 'ci.id = wgpi.contentId', 'left') + ->join('content_library cl', 'cl.id = ci.libraryId', 'left') + ->where($queryWhere) + ->where('cl.id', '<>', null) + ->field([ + 'cl.id as libraryId', + 'cl.name as libraryName', + 'COUNT(DISTINCT wgpi.id) as pushCount' + ]) + ->group('cl.id, cl.name') + ->select(); + + $result = []; + foreach ($comparison as $item) { + $libraryId = $item['libraryId']; + $pushCount = intval($item['pushCount']); + + // 获取该内容库的详细统计 + $libraryContentIds = Db::name('content_item') + ->where('libraryId', $libraryId) + ->column('id'); + if (empty($libraryContentIds)) { + $libraryContentIds = [-1]; + } + + $libraryWhere = array_merge($where, [['wgpi.contentId', 'in', $libraryContentIds]]); + $librarySentItems = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($libraryWhere) + ->field('wgpi.id, wgpi.groupId, wgpi.friendId, wgpi.wechatAccountId, wgpi.createTime, wgpi.targetType, wgpi.contentId') + ->select(); + + $config = WorkbenchGroupPush::where('workbenchId', $workbenchId)->find(); + $targetType = $config ? intval($config->targetType) : 1; + + $replyStats = $this->calculateReplyStats($librarySentItems, $targetType, $startTime, $endTime); + $clickStats = $this->calculateClickStats($librarySentItems, $targetType, $startTime, $endTime); + + // 计算转化率(简化处理,实际需要根据业务定义) + $conversionRate = $pushCount > 0 ? round(($replyStats['replyCount'] / $pushCount) * 100, 1) : 0; + + $result[] = [ + 'libraryId' => $libraryId, + 'libraryName' => $item['libraryName'], + 'pushCount' => $pushCount, + 'reachRate' => 100, // 简化处理,实际应该计算 + 'replyRate' => $replyStats['replyRate'], + 'clickRate' => $clickStats['clickRate'], + 'conversionRate' => $conversionRate, + 'avgReplyTime' => $replyStats['avgReplyTime'], + 'level' => $this->getPerformanceLevel($replyStats['replyRate'], $clickStats['clickRate'], $conversionRate) + ]; + } + + // 按回复率排序 + usort($result, function($a, $b) { + return $b['replyRate'] <=> $a['replyRate']; + }); + + return $result; + } + + /** + * 获取性能等级 + */ + private function getPerformanceLevel($replyRate, $clickRate, $conversionRate) + { + $score = ($replyRate * 0.4) + ($clickRate * 0.3) + ($conversionRate * 0.3); + + if ($score >= 40) { + return '优秀'; + } elseif ($score >= 25) { + return '良好'; + } elseif ($score >= 15) { + return '一般'; + } else { + return '待提升'; + } + } + + /** + * 获取时段分析数据 + */ + private function getTimePeriodAnalysis($workbenchId, $where, $startTime, $endTime, $contentIds = null) + { + $queryWhere = $where; + if ($contentIds !== null) { + $queryWhere[] = ['wgpi.contentId', 'in', $contentIds]; + } + + $analysis = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($queryWhere) + ->field([ + 'FROM_UNIXTIME(wgpi.createTime, "%H") as hour', + 'COUNT(*) as count' + ]) + ->group('hour') + ->order('hour', 'asc') + ->select(); + + $result = []; + foreach ($analysis as $item) { + $result[] = [ + 'hour' => intval($item['hour']), + 'count' => intval($item['count']) + ]; + } + + return $result; + } + + /** + * 获取互动深度数据 + */ + private function getInteractionDepth($workbenchId, $where, $startTime, $endTime, $contentIds = null) + { + // 简化处理,实际需要更复杂的统计逻辑 + return [ + 'singleReply' => 0, // 单次回复 + 'multipleReply' => 0, // 多次回复 + 'deepInteraction' => 0 // 深度互动 + ]; + } + + /** + * 获取推送历史记录列表 + * @return \think\response\Json + */ + public function getGroupPushHistory() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $workbenchId = $this->request->param('workbenchId', 0); + $keyword = $this->request->param('keyword', ''); + $pushType = $this->request->param('pushType', ''); // 推送类型筛选:''=全部, 'friend'=好友消息, 'group'=群消息, 'announcement'=群公告 + $status = $this->request->param('status', ''); // 状态筛选:''=全部, 'success'=已完成, 'progress'=进行中, 'failed'=失败 + $userId = $this->request->userInfo['id']; + + // 构建工作台查询条件 + $workbenchWhere = [ + ['w.userId', '=', $userId], + ['w.type', '=', self::TYPE_GROUP_PUSH], + ['w.isDel', '=', 0] + ]; + + // 如果指定了工作台ID,则验证权限并限制查询范围 + if (!empty($workbenchId)) { + $workbench = Workbench::where([ + ['id', '=', $workbenchId], + ['userId', '=', $userId], + ['type', '=', self::TYPE_GROUP_PUSH], + ['isDel', '=', 0] + ])->find(); + + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在']); + } + $workbenchWhere[] = ['w.id', '=', $workbenchId]; + } + + // 1. 先查询所有已执行的推送记录(按推送时间分组) + $pushHistoryQuery = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->join('workbench w', 'w.id = wgpi.workbenchId', 'left') + ->join('workbench_group_push wgp', 'wgp.workbenchId = wgpi.workbenchId', 'left') + ->join('content_item ci', 'ci.id = wgpi.contentId', 'left') + ->join('content_library cl', 'cl.id = ci.libraryId', 'left') + ->where($workbenchWhere) + ->field([ + 'wgpi.workbenchId', + 'w.name as workbenchName', + 'wgpi.contentId', + 'FROM_UNIXTIME(wgpi.createTime, "%Y-%m-%d %H:00:00") as pushTime', + 'wgpi.targetType', + 'wgp.groupPushSubType', + 'MIN(wgpi.createTime) as createTime', + 'COUNT(DISTINCT wgpi.id) as totalCount', + 'cl.name as contentLibraryName' + ]) + ->group('wgpi.workbenchId, wgpi.contentId, pushTime, wgpi.targetType, wgp.groupPushSubType'); + + if (!empty($keyword)) { + $pushHistoryQuery->where('w.name|cl.name|ci.content', 'like', '%' . $keyword . '%'); + } + + $pushHistoryList = $pushHistoryQuery->order('createTime', 'desc')->select(); + + // 2. 查询所有任务(包括未执行的) + $allTasksQuery = Db::name('workbench') + ->alias('w') + ->join('workbench_group_push wgp', 'wgp.workbenchId = w.id', 'left') + ->where($workbenchWhere) + ->field([ + 'w.id as workbenchId', + 'w.name as workbenchName', + 'w.createTime', + 'wgp.targetType', + 'wgp.groupPushSubType', + 'wgp.groups', + 'wgp.friends', + 'wgp.trafficPools' + ]); + + if (!empty($keyword)) { + $allTasksQuery->where('w.name', 'like', '%' . $keyword . '%'); + } + + $allTasks = $allTasksQuery->select(); + + // 3. 合并数据:已执行的推送记录 + 未执行的任务 + $resultList = []; + $executedWorkbenchIds = []; + + // 处理已执行的推送记录 + foreach ($pushHistoryList as $item) { + $itemWorkbenchId = $item['workbenchId']; + $contentId = $item['contentId']; + $pushTime = $item['pushTime']; + $targetType = intval($item['targetType']); + $groupPushSubType = isset($item['groupPushSubType']) ? intval($item['groupPushSubType']) : 1; + + // 标记该工作台已有执行记录 + if (!in_array($itemWorkbenchId, $executedWorkbenchIds)) { + $executedWorkbenchIds[] = $itemWorkbenchId; + } + + // 将时间字符串转换为时间戳范围(小时级别) + $pushTimeStart = strtotime($pushTime); + $pushTimeEnd = $pushTimeStart + 3600; // 一小时后 + + // 获取该次推送的详细统计 + $pushWhere = [ + ['wgpi.workbenchId', '=', $itemWorkbenchId], + ['wgpi.contentId', '=', $contentId], + ['wgpi.createTime', '>=', $pushTimeStart], + ['wgpi.createTime', '<', $pushTimeEnd], + ['wgpi.targetType', '=', $targetType] + ]; + + // 目标数量 + if ($targetType == 1) { + // 群推送:统计群数量 + $targetCount = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($pushWhere) + ->where('wgpi.groupId', '<>', null) + ->distinct(true) + ->count('wgpi.groupId'); + } else { + // 好友推送:统计好友数量 + $targetCount = Db::name('workbench_group_push_item') + ->alias('wgpi') + ->where($pushWhere) + ->where('wgpi.friendId', '<>', null) + ->distinct(true) + ->count('wgpi.friendId'); + } + + // 成功数和失败数(简化处理,实际需要根据发送状态判断) + $successCount = intval($item['totalCount']); // 简化处理 + $failCount = 0; // 简化处理,实际需要从发送状态获取 + + // 状态判断 + $itemStatus = $successCount > 0 ? 'success' : 'failed'; + if ($failCount > 0 && $successCount > 0) { + $itemStatus = 'partial'; + } + + // 推送类型判断 + $pushTypeText = ''; + $pushTypeCode = ''; + if ($targetType == 1) { + // 群推送 + if ($groupPushSubType == 2) { + $pushTypeText = '群公告'; + $pushTypeCode = 'announcement'; + } else { + $pushTypeText = '群消息'; + $pushTypeCode = 'group'; + } + } else { + // 好友推送 + $pushTypeText = '好友消息'; + $pushTypeCode = 'friend'; + } + + $resultList[] = [ + 'workbenchId' => $itemWorkbenchId, + 'taskName' => $item['workbenchName'] ?? '', + 'pushType' => $pushTypeText, + 'pushTypeCode' => $pushTypeCode, + 'targetCount' => $targetCount, + 'successCount' => $successCount, + 'failCount' => $failCount, + 'status' => $itemStatus, + 'statusText' => $this->getStatusText($itemStatus), + 'createTime' => date('Y-m-d H:i:s', $item['createTime']), + 'contentLibraryName' => $item['contentLibraryName'] ?? '' + ]; + } + + // 处理未执行的任务 + foreach ($allTasks as $task) { + $taskWorkbenchId = $task['workbenchId']; + + // 如果该任务已有执行记录,跳过(避免重复) + if (in_array($taskWorkbenchId, $executedWorkbenchIds)) { + continue; + } + + $targetType = isset($task['targetType']) ? intval($task['targetType']) : 1; + $groupPushSubType = isset($task['groupPushSubType']) ? intval($task['groupPushSubType']) : 1; + + // 计算目标数量(从配置中获取) + $targetCount = 0; + if ($targetType == 1) { + // 群推送:统计配置的群数量 + $groups = json_decode($task['groups'] ?? '[]', true); + $targetCount = is_array($groups) ? count($groups) : 0; + } else { + // 好友推送:统计配置的好友数量或流量池数量 + $friends = json_decode($task['friends'] ?? '[]', true); + $trafficPools = json_decode($task['trafficPools'] ?? '[]', true); + $friendCount = is_array($friends) ? count($friends) : 0; + $poolCount = is_array($trafficPools) ? count($trafficPools) : 0; + // 如果配置了流量池,目标数量暂时显示为流量池数量(实际数量需要从流量池中统计) + $targetCount = $friendCount > 0 ? $friendCount : $poolCount; + } + + // 推送类型判断 + $pushTypeText = ''; + $pushTypeCode = ''; + if ($targetType == 1) { + // 群推送 + if ($groupPushSubType == 2) { + $pushTypeText = '群公告'; + $pushTypeCode = 'announcement'; + } else { + $pushTypeText = '群消息'; + $pushTypeCode = 'group'; + } + } else { + // 好友推送 + $pushTypeText = '好友消息'; + $pushTypeCode = 'friend'; + } + + $resultList[] = [ + 'workbenchId' => $taskWorkbenchId, + 'taskName' => $task['workbenchName'] ?? '', + 'pushType' => $pushTypeText, + 'pushTypeCode' => $pushTypeCode, + 'targetCount' => $targetCount, + 'successCount' => 0, + 'failCount' => 0, + 'status' => 'pending', + 'statusText' => '进行中', + 'createTime' => date('Y-m-d H:i:s', $task['createTime']), + 'contentLibraryName' => '' + ]; + } + + // 应用筛选条件 + $filteredList = []; + foreach ($resultList as $item) { + // 推送类型筛选 + if (!empty($pushType)) { + if ($pushType === 'friend' && $item['pushTypeCode'] !== 'friend') { + continue; + } + if ($pushType === 'group' && $item['pushTypeCode'] !== 'group') { + continue; + } + if ($pushType === 'announcement' && $item['pushTypeCode'] !== 'announcement') { + continue; + } + } + + // 状态筛选 + if (!empty($status)) { + if ($status === 'success' && $item['status'] !== 'success') { + continue; + } + if ($status === 'progress') { + // 进行中:包括 partial 和 pending + if ($item['status'] !== 'partial' && $item['status'] !== 'pending') { + continue; + } + } + if ($status === 'failed' && $item['status'] !== 'failed') { + continue; + } + } + + $filteredList[] = $item; + } + + // 按创建时间倒序排序 + usort($filteredList, function($a, $b) { + return strtotime($b['createTime']) - strtotime($a['createTime']); + }); + + // 分页处理 + $total = count($filteredList); + $offset = ($page - 1) * $limit; + $list = array_slice($filteredList, $offset, $limit); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + + /** + * 获取状态文本 + * @param string $status 状态码 + * @return string 状态文本 + */ + private function getStatusText($status) + { + $statusMap = [ + 'success' => '已完成', + 'partial' => '进行中', + 'pending' => '进行中', + 'failed' => '失败' + ]; + return $statusMap[$status] ?? '未知'; + } + + /** + * 获取已创建的群列表(自动建群) + * @return \think\response\Json + */ + public function getCreatedGroupsList() + { + $workbenchId = $this->request->param('workbenchId', 0); + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 100); + $keyword = $this->request->param('keyword', ''); + + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '工作台ID不能为空']); + } + + // 验证工作台权限 + $where = [ + ['id', '=', $workbenchId], + ['companyId', '=', $this->request->userInfo['companyId']], + ['type', '=', self::TYPE_GROUP_CREATE], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + $workbench = Workbench::where($where)->find(); + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在或无权限']); + } + + // 获取已创建的群ID列表(状态为成功且groupId不为空) + $groupIds = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('status', 2) // STATUS_SUCCESS = 2 + ->where('groupId', '<>', null) + ->group('groupId') + ->column('groupId'); + + if (empty($groupIds)) { + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => [], + 'total' => 0, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + + // 查询群组详细信息(从s2_wechat_chatroom表查询) + $query = Db::table('s2_wechat_chatroom')->alias('wc') + ->join('wechat_account wa', 'wa.wechatId = wc.wechatAccountWechatId', 'left') + ->where('wc.id', 'in', $groupIds) + ->where('wc.isDeleted', 0); + + // 关键字搜索 + if (!empty($keyword)) { + $query->where(function ($q) use ($keyword) { + $q->where('wc.nickname', 'like', '%' . $keyword . '%') + ->whereOr('wc.chatroomId', 'like', '%' . $keyword . '%') + ->whereOr('wa.nickName', 'like', '%' . $keyword . '%'); + }); + } + + $total = $query->count(); + $list = $query->field('wc.id,wc.nickname as groupName,wc.chatroomId,wc.chatroomAvatar as groupAvatar,wc.wechatAccountWechatId as ownerWechatId,wc.createTime,wc.chatroomOwnerNickname as ownerNickname,wc.chatroomOwnerAvatar as ownerAvatar,wa.alias as ownerAlias') + ->order('wc.createTime', 'desc') + ->page($page, $limit) + ->select(); + + // 统计每个群的成员数量和成员信息 + foreach ($list as &$item) { + // 统计该群的成员数量(从workbench_group_create_item表统计) + $memberCount = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('groupId', $item['id']) + ->where('status', 'in', [2, 4]) // 创建成功和管理员好友已拉入 + ->count(); + + // 获取成员列表(用于显示成员头像) + $memberList = Db::name('workbench_group_create_item')->alias('wgci') + ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wgci.friendId', 'left') + ->where('wgci.workbenchId', $workbenchId) + ->where('wgci.groupId', $item['id']) + ->where('wgci.status', 'in', [2, 4]) + ->field('wf.avatar, wf.wechatId, wf.nickname') + ->order('wgci.createTime', 'asc') + ->limit(10) // 最多显示10个成员头像 + ->select(); + + // 格式化成员头像列表 + $memberAvatars = []; + foreach ($memberList as $member) { + if (!empty($member['avatar'])) { + $memberAvatars[] = [ + 'avatar' => $member['avatar'], + 'wechatId' => $member['wechatId'] ?? '', + 'nickname' => $member['nickname'] ?? '' + ]; + } + } + + // 计算剩余成员数(用于显示"+XX") + $remainingCount = $memberCount > count($memberAvatars) ? $memberCount - count($memberAvatars) : 0; + + // 格式化返回数据 + $item['memberCount'] = $memberCount; + $item['memberCountText'] = $memberCount . '人'; // 格式化为"XX人" + $item['createTime'] = !empty($item['createTime']) ? date('Y-m-d', $item['createTime']) : ''; // 格式化为"YYYY-MM-DD" + $item['memberAvatars'] = $memberAvatars; // 成员头像列表(最多10个) + $item['remainingCount'] = $remainingCount; // 剩余成员数(用于显示"+XX") + + // 保留原有字段,但调整格式 + $item['groupName'] = $item['groupName'] ?? ''; + $item['groupAvatar'] = $item['groupAvatar'] ?? ''; + } + unset($item); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + + /** + * 获取已创建群的详情(自动建群) + * @return \think\response\Json + */ + public function getCreatedGroupDetail() + { + $workbenchId = $this->request->param('workbenchId', 0); + $groupId = $this->request->param('groupId', 0); + + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '工作台ID不能为空']); + } + + if (empty($groupId)) { + return json(['code' => 400, 'msg' => '群ID不能为空']); + } + + // 验证工作台权限 + $where = [ + ['id', '=', $workbenchId], + ['companyId', '=', $this->request->userInfo['companyId']], + ['type', '=', self::TYPE_GROUP_CREATE], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + $workbench = Workbench::where($where)->find(); + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在或无权限']); + } + + // 验证该群是否属于该工作台 + $groupItem = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('groupId', $groupId) + ->where('status', 2) // STATUS_SUCCESS = 2 + ->find(); + + if (empty($groupItem)) { + return json(['code' => 404, 'msg' => '群不存在或不属于该工作台']); + } + + // 查询群基本信息(从s2_wechat_chatroom表查询) + $group = Db::table('s2_wechat_chatroom')->alias('wc') + ->join('wechat_account wa', 'wa.wechatId = wc.wechatAccountWechatId', 'left') + ->where('wc.id', $groupId) + ->where('wc.isDeleted', 0) + ->field('wc.id,wc.nickname as groupName,wc.chatroomId,wc.chatroomAvatar as groupAvatar,wc.wechatAccountWechatId as ownerWechatId,wc.createTime,wc.chatroomOwnerNickname as ownerNickname,wc.chatroomOwnerAvatar as ownerAvatar,wa.alias as ownerAlias,wc.announce') + ->find(); + + if (empty($group)) { + return json(['code' => 404, 'msg' => '群不存在']); + } + + // 获取chatroomId + $chatroomId = $group['chatroomId'] ?? ''; + if (empty($chatroomId)) { + return json(['code' => 400, 'msg' => '群聊ID不存在']); + } + + // 从s2_wechat_chatroom_member表查询所有成员列表(不限制数量) + $memberList = Db::table('s2_wechat_chatroom_member')->alias('wcm') + ->join(['s2_wechat_friend' => 'wf'], 'wf.wechatId = wcm.wechatId', 'left') + ->where('wcm.chatroomId', $chatroomId) + ->field('wcm.wechatId,wcm.nickname as memberNickname,wcm.avatar as memberAvatar,wcm.conRemark as memberRemark,wcm.alias as memberAlias,wcm.createTime as joinTime,wcm.updateTime,wf.id as friendId,wf.nickname as friendNickname,wf.avatar as friendAvatar') + ->order('wcm.createTime', 'asc') + ->select(); + + // 去重:按wechatId去重,保留第一条记录 + $memberMap = []; + foreach ($memberList as $member) { + $wechatId = $member['wechatId'] ?? ''; + if (!empty($wechatId) && !isset($memberMap[$wechatId])) { + $memberMap[$wechatId] = $member; + } + } + $memberList = array_values($memberMap); // 重新索引数组 + + // 获取在群中的成员wechatId列表(用于判断是否已退群) + $inGroupWechatIds = array_column($memberList, 'wechatId'); + $inGroupWechatIds = array_filter($inGroupWechatIds); // 过滤空值 + + // 获取通过自动建群加入的成员信息(用于判断入群状态和已退群成员) + $autoJoinMemberList = Db::name('workbench_group_create_item') + ->alias('wgci') + ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wgci.friendId', 'left') + ->where('wgci.workbenchId', $workbenchId) + ->where('wgci.groupId', $groupId) + ->where('wgci.status', 'in', [2, 4]) // 创建成功和管理员好友已拉入 + ->field('wf.wechatId,wf.id as friendId,wf.nickname as friendNickname,wf.avatar as friendAvatar,wgci.createTime as autoJoinTime') + ->select(); + + // 去重:按wechatId去重,保留第一条记录 + $autoJoinMemberMap = []; + foreach ($autoJoinMemberList as $autoMember) { + $wechatId = $autoMember['wechatId'] ?? ''; + if (!empty($wechatId) && !isset($autoJoinMemberMap[$wechatId])) { + $autoJoinMemberMap[$wechatId] = $autoMember; + } + } + $autoJoinMemberList = array_values($autoJoinMemberMap); // 重新索引数组 + + // 统计成员总数(包括在群中的成员和已退群的成员) + // 先统计在群中的成员数 + $inGroupCount = count($memberList); + + // 统计已退群的成员数(在自动建群记录中但不在群中的成员) + $quitCount = 0; + if (!empty($autoJoinMemberList)) { + foreach ($autoJoinMemberList as $autoMember) { + $wechatId = $autoMember['wechatId'] ?? ''; + if (!empty($wechatId) && !in_array($wechatId, $inGroupWechatIds)) { + $quitCount++; + } + } + } + + // 总成员数 = 在群中的成员数 + 已退群的成员数 + $memberCount = $inGroupCount + $quitCount; + + // 获取自动建群加入的成员wechatId列表(用于判断入群状态) + $autoJoinWechatIds = array_column($autoJoinMemberList, 'wechatId'); + $autoJoinWechatIds = array_filter($autoJoinWechatIds); // 过滤空值 + + // 格式化在群中的成员列表 + $members = []; + $addedWechatIds = []; // 用于记录已添加的成员wechatId,避免重复 + $ownerWechatId = $group['ownerWechatId'] ?? ''; + foreach ($memberList as $member) { + $wechatId = $member['wechatId'] ?? ''; + + // 跳过空wechatId和已添加的成员 + if (empty($wechatId) || in_array($wechatId, $addedWechatIds)) { + continue; + } + + // 标记为已添加 + $addedWechatIds[] = $wechatId; + + // 根据wechatId判断是否为群主 + $isOwner = (!empty($ownerWechatId) && $wechatId == $ownerWechatId) ? 1 : 0; + + // 判断入群状态:如果在自动建群记录中,说明是通过自动建群加入的;否则是其他方式加入的 + $joinStatus = in_array($wechatId, $autoJoinWechatIds) ? 'auto' : 'manual'; + + // 判断是否已退群:如果成员在s2_wechat_chatroom_member表中存在,说明在群中;否则已退群 + // 由于我们已经从s2_wechat_chatroom_member表查询,所以这里都是"在群中"状态 + $isQuit = 0; // 0=在群中,1=已退群 + + // 优先使用friend表的昵称和头像,如果没有则使用member表的 + $nickname = !empty($member['friendNickname']) ? $member['friendNickname'] : ($member['memberNickname'] ?? ''); + $avatar = !empty($member['friendAvatar']) ? $member['friendAvatar'] : ($member['memberAvatar'] ?? ''); + + $members[] = [ + 'friendId' => $member['friendId'] ?? 0, + 'wechatId' => $wechatId, + 'nickname' => $nickname, + 'avatar' => $avatar, + 'alias' => $member['memberAlias'] ?? '', + 'remark' => $member['memberRemark'] ?? '', + 'isOwner' => $isOwner, // 标记群主 + 'joinStatus' => $joinStatus, // 入群状态:auto=自动建群加入,manual=其他方式加入 + 'isQuit' => $isQuit, // 是否已退群:0=在群中,1=已退群 + 'joinTime' => !empty($member['joinTime']) ? date('Y-m-d H:i:s', $member['joinTime']) : '', // 入群时间 + ]; + } + + // 添加已退群的成员(在自动建群记录中但不在群中的成员) + foreach ($autoJoinMemberList as $autoMember) { + $wechatId = $autoMember['wechatId'] ?? ''; + + // 跳过空wechatId、已在群中的成员和已添加的成员 + if (empty($wechatId) || in_array($wechatId, $inGroupWechatIds) || in_array($wechatId, $addedWechatIds)) { + continue; + } + + // 标记为已添加 + $addedWechatIds[] = $wechatId; + + // 根据wechatId判断是否为群主 + $isOwner = (!empty($ownerWechatId) && $wechatId == $ownerWechatId) ? 1 : 0; + + $members[] = [ + 'friendId' => $autoMember['friendId'] ?? 0, + 'wechatId' => $wechatId, + 'nickname' => $autoMember['friendNickname'] ?? '', + 'avatar' => $autoMember['friendAvatar'] ?? '', + 'alias' => '', + 'remark' => '', + 'isOwner' => $isOwner, // 标记群主 + 'joinStatus' => 'auto', // 入群状态:auto=自动建群加入 + 'isQuit' => 1, // 是否已退群:1=已退群 + 'joinTime' => !empty($autoMember['autoJoinTime']) ? date('Y-m-d H:i:s', $autoMember['autoJoinTime']) : '', // 入群时间 + ]; + } + + // 将群主排在第一位 + usort($members, function($a, $b) { + if ($a['isOwner'] == $b['isOwner']) { + return 0; + } + return $a['isOwner'] > $b['isOwner'] ? -1 : 1; + }); + + // 格式化返回数据 + $result = [ + 'id' => $group['id'], + 'groupName' => $group['groupName'] ?? '', + 'chatroomId' => $group['chatroomId'] ?? '', + 'groupAvatar' => $group['groupAvatar'] ?? '', + 'ownerWechatId' => $group['ownerWechatId'] ?? '', + 'ownerNickname' => $group['ownerNickname'] ?? '', + 'ownerAvatar' => $group['ownerAvatar'] ?? '', + 'ownerAlias' => $group['ownerAlias'] ?? '', + 'announce' => $group['announce'] ?? '', + 'createTime' => !empty($group['createTime']) ? date('Y-m-d H:i', $group['createTime']) : '', // 格式化为"YYYY-MM-DD HH:MM" + 'memberCount' => $memberCount, + 'memberCountText' => $memberCount . '人', // 格式化为"XX人" + 'workbenchName' => $workbench->name ?? '', // 任务名称(工作台名称) + 'members' => $members // 所有成员列表 + ]; + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => $result + ]); + } + + /** + * 同步群最新信息(包括群成员) + * @return \think\response\Json + */ + public function syncGroupInfo() + { + $workbenchId = $this->request->param('workbenchId', 0); + $groupId = $this->request->param('groupId', 0); + + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '工作台ID不能为空']); + } + + if (empty($groupId)) { + return json(['code' => 400, 'msg' => '群ID不能为空']); + } + + // 验证工作台权限 + $where = [ + ['id', '=', $workbenchId], + ['companyId', '=', $this->request->userInfo['companyId']], + ['type', '=', self::TYPE_GROUP_CREATE], + ['isDel', '=', 0] + ]; + if (empty($this->request->userInfo['isAdmin'])) { + $where[] = ['userId', '=', $this->request->userInfo['id']]; + } + + $workbench = Workbench::where($where)->find(); + if (empty($workbench)) { + return json(['code' => 404, 'msg' => '工作台不存在或无权限']); + } + + // 验证该群是否属于该工作台 + $groupItem = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('groupId', $groupId) + ->where('status', 2) // STATUS_SUCCESS = 2 + ->find(); + + if (empty($groupItem)) { + return json(['code' => 404, 'msg' => '群不存在或不属于该工作台']); + } + + // 查询群基本信息,获取chatroomId和wechatAccountWechatId + $group = Db::table('s2_wechat_chatroom') + ->where('id', $groupId) + ->where('isDeleted', 0) + ->field('id,chatroomId,wechatAccountWechatId') + ->find(); + + if (empty($group)) { + return json(['code' => 404, 'msg' => '群不存在']); + } + + $chatroomId = $group['chatroomId'] ?? ''; + $wechatAccountWechatId = $group['wechatAccountWechatId'] ?? ''; + + if (empty($chatroomId)) { + return json(['code' => 400, 'msg' => '群聊ID不存在']); + } + + try { + // 实例化WechatChatroomController + $chatroomController = new \app\api\controller\WechatChatroomController(); + + // 1. 同步群信息(调用getlist方法) + $syncData = [ + 'wechatChatroomId' => $chatroomId, // 使用chatroomId作为wechatChatroomId来指定要同步的群 + 'wechatAccountKeyword' => $wechatAccountWechatId, // 通过群主微信ID筛选 + 'isDeleted' => false, + 'pageIndex' => 1, + 'pageSize' => 100 // 获取足够多的数据 + ]; + $syncResult = $chatroomController->getlist($syncData, true, 0); // isInner = true + $syncResponse = json_decode($syncResult, true); + + if (empty($syncResponse['code']) || $syncResponse['code'] != 200) { + return json(['code' => 500, 'msg' => '同步群信息失败:' . ($syncResponse['msg'] ?? '未知错误')]); + } + + // 2. 同步群成员信息(调用listChatroomMember方法) + // wechatChatroomId 使用 s2_wechat_chatroom 表的 id(即groupId) + // chatroomId 使用群聊ID(chatroomId) + $wechatChatroomId = $groupId; // s2_wechat_chatroom表的id + $memberSyncResult = $chatroomController->listChatroomMember($wechatChatroomId, $chatroomId, true); // isInner = true + $memberSyncResponse = json_decode($memberSyncResult, true); + + if (empty($memberSyncResponse['code']) || $memberSyncResponse['code'] != 200) { + // 成员同步失败不影响整体结果,记录警告即可 + \think\facade\Log::warning("同步群成员失败。群ID: {$groupId}, 群聊ID: {$chatroomId}, 错误: " . ($memberSyncResponse['msg'] ?? '未知错误')); + } + + return json([ + 'code' => 200, + 'msg' => '同步成功', + 'data' => [ + 'groupId' => $groupId, + 'chatroomId' => $chatroomId, + 'groupInfoSynced' => true, + 'memberInfoSynced' => !empty($memberSyncResponse['code']) && $memberSyncResponse['code'] == 200 + ] + ]); + } catch (\Exception $e) { + \think\facade\Log::error("同步群信息异常。群ID: {$groupId}, 错误: " . $e->getMessage()); + return json(['code' => 500, 'msg' => '同步失败:' . $e->getMessage()]); + } + } + + /** + * 修改群名称、群公告 + * @return \think\response\Json + */ + public function modifyGroupInfo() + { + $workbenchId = $this->request->param('workbenchId', 0); + $groupId = $this->request->param('groupId', 0); + $chatroomName = $this->request->param('chatroomName', ''); + $announce = $this->request->param('announce', ''); + + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '工作台ID不能为空']); + } + + if (empty($groupId)) { + return json(['code' => 400, 'msg' => '群ID不能为空']); + } + + // 至少需要提供一个修改项 + if (empty($chatroomName) && empty($announce)) { + return json(['code' => 400, 'msg' => '请至少提供群名称或群公告中的一个参数']); + } + + // 查询群基本信息 + $group = Db::table('s2_wechat_chatroom') + ->where('id', $groupId) + ->where('isDeleted', 0) + ->field('id,chatroomId,wechatAccountWechatId,accountId,wechatAccountId') + ->find(); + + if (empty($group)) { + return json(['code' => 404, 'msg' => '群不存在']); + } + + $chatroomId = $group['id'] ?? ''; + + if (empty($chatroomId)) { + return json(['code' => 400, 'msg' => '群聊ID不存在']); + } + + try { + // 直接使用群表中的账号信息 + $executeAccountId = $group['accountId'] ?? 0; + $executeWechatAccountId = $group['wechatAccountId'] ?? 0; + $executeWechatId = $group['wechatAccountWechatId'] ?? ''; + + // 确保 wechatId 不为空 + if (empty($executeWechatId)) { + return json(['code' => 400, 'msg' => '无法获取微信账号ID']); + } + + // 调用 WebSocketController 修改群信息 + // 获取系统API账号信息(用于WebSocket连接) + $username = Env::get('api.username2', ''); + $password = Env::get('api.password2', ''); + + if (empty($username) || empty($password)) { + return json(['code' => 500, 'msg' => '系统API账号配置缺失']); + } + + // 获取系统账号ID + $systemAccountId = Db::name('users')->where('account', $username)->value('s2_accountId'); + if (empty($systemAccountId)) { + return json(['code' => 500, 'msg' => '未找到系统账号ID']); + } + + $webSocketController = new WebSocketController([ + 'userName' => $username, + 'password' => $password, + 'accountId' => $systemAccountId + ]); + // 构建修改参数 + $modifyData = [ + 'wechatChatroomId' => $chatroomId, + 'wechatAccountId' => $executeWechatAccountId, + ]; + if (!empty($chatroomName)) { + $modifyData['chatroomName'] = $chatroomName; + } + if (!empty($announce)) { + $modifyData['announce'] = $announce; + } + + + $modifyResult = $webSocketController->CmdChatroomModifyInfo($modifyData); + $modifyResponse = json_decode($modifyResult, true); + if (empty($modifyResponse['code']) || $modifyResponse['code'] != 200) { + return json(['code' => 500, 'msg' => '修改群信息失败:' . ($modifyResponse['msg'] ?? '未知错误')]); + } + + // 修改成功后更新数据库 + $updateData = [ + 'updateTime' => time() + ]; + + // 如果修改了群名称,更新数据库 + if (!empty($chatroomName)) { + $updateData['nickname'] = $chatroomName; + } + + // 如果修改了群公告,更新数据库 + if (!empty($announce)) { + $updateData['announce'] = $announce; + } + + // 更新数据库 + Db::table('s2_wechat_chatroom') + ->where('id', $groupId) + ->update($updateData); + + return json([ + 'code' => 200, + 'msg' => '修改成功', + 'data' => [ + 'groupId' => $groupId, + 'chatroomId' => $chatroomId, + 'chatroomName' => $chatroomName, + 'announce' => $announce + ] + ]); + } catch (\Exception $e) { + \think\facade\Log::error("修改群信息异常。群ID: {$groupId}, 错误: " . $e->getMessage()); + return json(['code' => 500, 'msg' => '修改失败:' . $e->getMessage()]); + } + } + + /** + * 转移群聊到指定账号 + * @param int $groupId 群ID(s2_wechat_chatroom表的id) + * @param string $chatroomId 群聊ID + * @param int $toAccountId 目标账号ID(s2_company_account表的id) + * @param int $toWechatAccountId 目标微信账号ID(s2_wechat_account表的id) + * @return array ['success' => bool, 'msg' => string] + */ + protected function transferChatroomToAccount($groupId, $chatroomId, $toAccountId, $toWechatAccountId) + { + try { + // 查询目标账号信息 + $targetAccount = Db::table('s2_company_account') + ->where('id', $toAccountId) + ->field('id,userName,realName,nickname') + ->find(); + + if (empty($targetAccount)) { + return ['success' => false, 'msg' => '目标账号不存在']; + } + + // 查询目标微信账号信息 + $targetWechatAccount = Db::table('s2_wechat_account') + ->where('id', $toWechatAccountId) + ->field('id,wechatId,deviceAccountId') + ->find(); + + if (empty($targetWechatAccount)) { + return ['success' => false, 'msg' => '目标微信账号不存在']; + } + + // 调用 AutomaticAssign 进行群聊转移 + $automaticAssign = new \app\api\controller\AutomaticAssign(); + + // 构建转移参数(通过 API 调用) + $transferData = [ + 'wechatChatroomId' => $chatroomId, // 使用群聊ID + 'toAccountId' => $toAccountId, + 'wechatAccountKeyword' => $targetWechatAccount['wechatId'] ?? '', + 'isDeleted' => false + ]; + + // 直接更新数据库(因为 API 可能不支持指定单个群聊ID转移) + // 更新 s2_wechat_chatroom 表的 accountId 和 wechatAccountId + Db::table('s2_wechat_chatroom') + ->where('id', $groupId) + ->update([ + 'accountId' => $toAccountId, + 'accountUserName' => $targetAccount['userName'] ?? '', + 'accountRealName' => $targetAccount['realName'] ?? '', + 'accountNickname' => $targetAccount['nickname'] ?? '', + 'wechatAccountId' => $toWechatAccountId, + 'wechatAccountWechatId' => $targetWechatAccount['wechatId'] ?? '', + 'updateTime' => time() + ]); + + return ['success' => true, 'msg' => '转移成功']; + } catch (\Exception $e) { + \think\facade\Log::error("转移群聊异常。群ID: {$groupId}, 目标账号ID: {$toAccountId}, 错误: " . $e->getMessage()); + return ['success' => false, 'msg' => $e->getMessage()]; + } + } + + +} \ No newline at end of file diff --git a/application/cunkebao/controller/workbench/WorkbenchHelperController.php b/application/cunkebao/controller/workbench/WorkbenchHelperController.php new file mode 100644 index 0000000..285fcab --- /dev/null +++ b/application/cunkebao/controller/workbench/WorkbenchHelperController.php @@ -0,0 +1,313 @@ +request->param('deviceIds', ''); + $companyId = $this->request->userInfo['companyId']; + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + + $where = [ + ['wc.companyId', '=', $companyId], + ]; + + if (!empty($deviceIds)) { + $deviceIds = explode(',', $deviceIds); + $where[] = ['dwl.deviceId', 'in', $deviceIds]; + } + + $wechatAccounts = Db::name('wechat_customer')->alias('wc') + ->join('device_wechat_login dwl', 'dwl.wechatId = wc.wechatId AND dwl.companyId = wc.companyId AND dwl.alive = 1') + ->join(['s2_wechat_account' => 'wa'], 'wa.wechatId = wc.wechatId') + ->where($where) + ->field('wa.id,wa.wechatId,wa.nickName,wa.labels') + ->select(); + $labels = []; + $wechatIds = []; + foreach ($wechatAccounts as $account) { + $labelArr = json_decode($account['labels'], true); + if (is_array($labelArr)) { + foreach ($labelArr as $label) { + if ($label !== '' && $label !== null) { + $labels[] = $label; + } + } + } + $wechatIds[] = $account['wechatId']; + } + // 去重(只保留一个) + $labels = array_values(array_unique($labels)); + $wechatIds = array_unique($wechatIds); + + // 搜索过滤 + if (!empty($keyword)) { + $labels = array_filter($labels, function ($label) use ($keyword) { + return mb_stripos($label, $keyword) !== false; + }); + $labels = array_values($labels); // 重新索引数组 + } + + // 分页处理 + $labels2 = array_slice($labels, ($page - 1) * $limit, $limit); + + // 统计数量 + $newLabel = []; + foreach ($labels2 as $label) { + $friendCount = Db::table('s2_wechat_friend') + ->whereIn('ownerWechatId', $wechatIds) + ->where('labels', 'like', '%"' . $label . '"%') + ->count(); + $newLabel[] = [ + 'label' => $label, + 'count' => $friendCount + ]; + } + + // 返回结果 + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $newLabel, + 'total' => count($labels), + ] + ]); + } + + /** + * 获取群列表 + * @return \think\response\Json + */ + public function getGroupList() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + + $where = [ + ['wg.deleteTime', '=', 0], + ['wg.companyId', '=', $this->request->userInfo['companyId']], + ]; + + if (!empty($keyword)) { + $where[] = ['wg.name', 'like', '%' . $keyword . '%']; + } + + $query = Db::name('wechat_group')->alias('wg') + ->join('wechat_account wa', 'wa.wechatId = wg.ownerWechatId') + ->where($where); + + $total = $query->count(); + $list = $query->order('wg.id', 'desc') + ->field('wg.id,wg.name as groupName,wg.ownerWechatId,wa.nickName,wg.createTime,wa.avatar,wa.alias,wg.avatar as groupAvatar') + ->page($page, $limit) + ->select(); + + // 优化:格式化时间,头像兜底 + $defaultGroupAvatar = ''; + $defaultAvatar = ''; + foreach ($list as &$item) { + $item['createTime'] = $item['createTime'] ? date('Y-m-d H:i:s', $item['createTime']) : ''; + $item['groupAvatar'] = $item['groupAvatar'] ?: $defaultGroupAvatar; + $item['avatar'] = $item['avatar'] ?: $defaultAvatar; + } + + return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]); + } + + /** + * 获取流量池列表 + * @return \think\response\Json + */ + public function getTrafficPoolList() + { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + $companyId = $this->request->userInfo['companyId']; + + $baseQuery = Db::name('traffic_source_package')->alias('tsp') + ->where('tsp.isDel', 0) + ->whereIn('tsp.companyId', [$companyId, 0]); + + if (!empty($keyword)) { + $baseQuery->whereLike('tsp.name', '%' . $keyword . '%'); + } + + $total = (clone $baseQuery)->count(); + + $list = $baseQuery + ->leftJoin('traffic_source_package_item tspi', 'tspi.packageId = tsp.id and tspi.isDel = 0') + ->field('tsp.id,tsp.name,tsp.description,tsp.pic,tsp.companyId,COUNT(tspi.id) as itemCount,max(tspi.createTime) as latestImportTime') + ->group('tsp.id') + ->order('tsp.id', 'desc') + ->page($page, $limit) + ->select(); + + foreach ($list as &$item) { + $item['latestImportTime'] = !empty($item['latestImportTime']) ? date('Y-m-d H:i:s', $item['latestImportTime']) : ''; + } + unset($item); + + return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]); + } + + /** + * 获取账号列表 + * @return \think\response\Json + */ + public function getAccountList() + { + $companyId = $this->request->userInfo['companyId']; + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $query = Db::table('s2_company_account') + ->alias('a') + ->where(['a.departmentId' => $companyId, 'a.status' => 0]) + ->whereNotLike('a.userName', '%_offline%') + ->whereNotLike('a.userName', '%_delete%'); + + $total = $query->count(); + $list = $query->field('a.id,a.userName,a.realName,a.nickname,a.memo') + ->page($page, $limit) + ->select(); + + return json(['code' => 200, 'msg' => '获取成功', 'data' => ['total' => $total, 'list' => $list]]); + } + + /** + * 获取京东联盟导购媒体 + * @return \think\response\Json + */ + public function getJdSocialMedia() + { + $data = Db::name('jd_social_media')->order('id DESC')->select(); + return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]); + } + + /** + * 获取京东联盟广告位 + * @return \think\response\Json + */ + public function getJdPromotionSite() + { + $id = $this->request->param('id', ''); + if (empty($id)) { + return json(['code' => 500, 'msg' => '参数缺失']); + } + + $data = Db::name('jd_promotion_site')->where('jdSocialMediaId', $id)->order('id DESC')->select(); + return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]); + } + + /** + * 京东转链-京推推 + * @param string $content + * @param string $positionid + * @return string + */ + public function changeLink($content = '', $positionid = '') + { + $unionId = Env::get('jd.unionId', ''); + $jttAppId = Env::get('jd.jttAppId', ''); + $appKey = Env::get('jd.appKey', ''); + $apiUrl = Env::get('jd.apiUrl', ''); + + $content = !empty($content) ? $content : $this->request->param('content', ''); + $positionid = !empty($positionid) ? $positionid : $this->request->param('positionid', ''); + + if (empty($content)) { + return json_encode(['code' => 500, 'msg' => '转链的内容为空']); + } + + // 验证是否包含链接 + if (!$this->containsLink($content)) { + return json_encode(['code' => 500, 'msg' => '内容中未检测到有效链接']); + } + + if (empty($unionId) || empty($jttAppId) || empty($appKey) || empty($apiUrl)) { + return json_encode(['code' => 500, 'msg' => '参数缺失']); + } + $params = [ + 'unionid' => $unionId, + 'content' => $content, + 'appid' => $jttAppId, + 'appkey' => $appKey, + 'v' => 'v2' + ]; + + if (!empty($positionid)) { + $params['positionid'] = $positionid; + } + + $res = requestCurl($apiUrl, $params, 'GET', [], 'json'); + $res = json_decode($res, true); + if (empty($res)) { + return json_encode(['code' => 500, 'msg' => '未知错误']); + } + $result = $res['result']; + if ($res['return'] == 0) { + return json_encode(['code' => 200, 'data' => $result['chain_content'], 'msg' => $result['msg']]); + } else { + return json_encode(['code' => 500, 'msg' => $result['msg']]); + } + } + + /** + * 验证内容是否包含链接 + * @param string $content 要检测的内容 + * @return bool + */ + private function containsLink($content) + { + // 定义各种链接的正则表达式模式 + $patterns = [ + // HTTP/HTTPS链接 + '/https?:\/\/[^\s]+/i', + // 京东商品链接 + '/item\.jd\.com\/\d+/i', + // 京东短链接 + '/u\.jd\.com\/[a-zA-Z0-9]+/i', + // 淘宝商品链接 + '/item\.taobao\.com\/item\.htm\?id=\d+/i', + // 天猫商品链接 + '/detail\.tmall\.com\/item\.htm\?id=\d+/i', + // 淘宝短链接 + '/m\.tb\.cn\/[a-zA-Z0-9]+/i', + // 拼多多链接 + '/mobile\.yangkeduo\.com\/goods\.html\?goods_id=\d+/i', + // 苏宁易购链接 + '/product\.suning\.com\/\d+\/\d+\.html/i', + // 通用域名模式(包含常见电商域名) + '/(?:jd|taobao|tmall|yangkeduo|suning|amazon|dangdang)\.com[^\s]*/i', + // 通用短链接模式 + '/[a-zA-Z0-9-]+\.[a-zA-Z]{2,}\/[a-zA-Z0-9\-._~:\/?#\[\]@!$&\'()*+,;=]+/i' + ]; + + // 遍历所有模式进行匹配 + foreach ($patterns as $pattern) { + if (preg_match($pattern, $content)) { + return true; + } + } + + return false; + } +} + diff --git a/application/cunkebao/controller/workbench/WorkbenchImportContactController.php b/application/cunkebao/controller/workbench/WorkbenchImportContactController.php new file mode 100644 index 0000000..5c1d5ea --- /dev/null +++ b/application/cunkebao/controller/workbench/WorkbenchImportContactController.php @@ -0,0 +1,69 @@ +request->param('page', 1); + $limit = $this->request->param('limit', 10); + $workbenchId = $this->request->param('workbenchId', 0); + + $where = [ + ['wici.workbenchId', '=', $workbenchId] + ]; + + // 查询发布记录 + $list = Db::name('workbench_import_contact_item')->alias('wici') + ->join('traffic_pool tp', 'tp.id = wici.poolId', 'left') + ->join('traffic_source tc', 'tc.identifier = tp.identifier', 'left') + ->join('wechat_account wa', 'wa.wechatId = tp.wechatId', 'left') + ->field([ + 'wici.id', + 'wici.workbenchId', + 'wici.createTime', + 'tp.identifier', + 'tp.mobile', + 'tp.wechatId', + 'tc.name', + 'wa.nickName', + 'wa.avatar', + 'wa.alias', + ]) + ->where($where) + ->order('tc.name DESC,wici.createTime DESC') + ->group('tp.identifier') + ->page($page, $limit) + ->select(); + + foreach ($list as &$item) { + $item['createTime'] = date('Y-m-d H:i:s', $item['createTime']); + } + + // 获取总记录数 + $total = Db::name('workbench_import_contact_item')->alias('wici') + ->where($where) + ->count(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + ] + ]); + } +} + diff --git a/application/cunkebao/controller/workbench/WorkbenchMomentsController.php b/application/cunkebao/controller/workbench/WorkbenchMomentsController.php new file mode 100644 index 0000000..92d001a --- /dev/null +++ b/application/cunkebao/controller/workbench/WorkbenchMomentsController.php @@ -0,0 +1,125 @@ +request->param('page', 1); + $limit = $this->request->param('limit', 10); + $workbenchId = $this->request->param('workbenchId', 0); + + $where = [ + ['wmsi.workbenchId', '=', $workbenchId] + ]; + + // 查询发布记录 + $list = Db::name('workbench_moments_sync_item')->alias('wmsi') + ->join('content_item ci', 'ci.id = wmsi.contentId', 'left') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wmsi.wechatAccountId', 'left') + ->field([ + 'wmsi.id', + 'wmsi.workbenchId', + 'wmsi.createTime as publishTime', + 'ci.contentType', + 'ci.content', + 'ci.resUrls', + 'ci.urls', + 'wa.nickName as operatorName', + 'wa.avatar as operatorAvatar' + ]) + ->where($where) + ->order('wmsi.createTime', 'desc') + ->page($page, $limit) + ->select(); + + foreach ($list as &$item) { + $item['resUrls'] = json_decode($item['resUrls'], true); + $item['urls'] = json_decode($item['urls'], true); + } + + + // 获取总记录数 + $total = Db::name('workbench_moments_sync_item')->alias('wmsi') + ->where($where) + ->count(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + + /** + * 获取朋友圈发布统计 + * @return \think\response\Json + */ + public function getMomentsStats() + { + $workbenchId = $this->request->param('workbenchId', 0); + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + // 获取今日数据 + $todayStart = strtotime(date('Y-m-d') . ' 00:00:00'); + $todayEnd = strtotime(date('Y-m-d') . ' 23:59:59'); + + $todayStats = Db::name('workbench_moments_sync_item') + ->where([ + ['workbenchId', '=', $workbenchId], + ['createTime', 'between', [$todayStart, $todayEnd]] + ]) + ->field([ + 'COUNT(*) as total', + 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success', + 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed' + ]) + ->find(); + + // 获取总数据 + $totalStats = Db::name('workbench_moments_sync_item') + ->where('workbenchId', $workbenchId) + ->field([ + 'COUNT(*) as total', + 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success', + 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed' + ]) + ->find(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'today' => [ + 'total' => intval($todayStats['total']), + 'success' => intval($todayStats['success']), + 'failed' => intval($todayStats['failed']) + ], + 'total' => [ + 'total' => intval($totalStats['total']), + 'success' => intval($totalStats['success']), + 'failed' => intval($totalStats['failed']) + ] + ] + ]); + } +} + diff --git a/application/cunkebao/controller/workbench/WorkbenchTrafficController.php b/application/cunkebao/controller/workbench/WorkbenchTrafficController.php new file mode 100644 index 0000000..284f01c --- /dev/null +++ b/application/cunkebao/controller/workbench/WorkbenchTrafficController.php @@ -0,0 +1,309 @@ +request->param('page', 1); + $limit = $this->request->param('limit', 10); + $workbenchId = $this->request->param('workbenchId', 0); + + $where = [ + ['wtdi.workbenchId', '=', $workbenchId] + ]; + + // 查询分发记录 + $list = Db::name('workbench_traffic_distribution_item')->alias('wtdi') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wtdi.wechatAccountId', 'left') + ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wtdi.wechatFriendId', 'left') + ->field([ + 'wtdi.id', + 'wtdi.workbenchId', + 'wtdi.wechatAccountId', + 'wtdi.wechatFriendId', + 'wtdi.createTime as distributeTime', + 'wtdi.status', + 'wtdi.errorMsg', + 'wa.nickName as operatorName', + 'wa.avatar as operatorAvatar', + 'wf.nickName as friendName', + 'wf.avatar as friendAvatar', + 'wf.gender', + 'wf.province', + 'wf.city' + ]) + ->where($where) + ->order('wtdi.createTime', 'desc') + ->page($page, $limit) + ->select(); + + // 处理数据 + foreach ($list as &$item) { + // 处理时间格式 + $item['distributeTime'] = date('Y-m-d H:i:s', $item['distributeTime']); + + // 处理性别 + $genderMap = [ + 0 => '未知', + 1 => '男', + 2 => '女' + ]; + $item['genderText'] = $genderMap[$item['gender']] ?? '未知'; + + // 处理状态文字 + $statusMap = [ + 0 => '待分发', + 1 => '分发成功', + 2 => '分发失败' + ]; + $item['statusText'] = $statusMap[$item['status']] ?? '未知状态'; + } + + // 获取总记录数 + $total = Db::name('workbench_traffic_distribution_item')->alias('wtdi') + ->where($where) + ->count(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ] + ]); + } + + /** + * 获取流量分发统计 + * @return \think\response\Json + */ + public function getTrafficDistributionStats() + { + $workbenchId = $this->request->param('workbenchId', 0); + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + // 获取今日数据 + $todayStart = strtotime(date('Y-m-d') . ' 00:00:00'); + $todayEnd = strtotime(date('Y-m-d') . ' 23:59:59'); + + $todayStats = Db::name('workbench_traffic_distribution_item') + ->where([ + ['workbenchId', '=', $workbenchId], + ['createTime', 'between', [$todayStart, $todayEnd]] + ]) + ->field([ + 'COUNT(*) as total', + 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success', + 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed' + ]) + ->find(); + + // 获取总数据 + $totalStats = Db::name('workbench_traffic_distribution_item') + ->where('workbenchId', $workbenchId) + ->field([ + 'COUNT(*) as total', + 'SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as success', + 'SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as failed' + ]) + ->find(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'today' => [ + 'total' => intval($todayStats['total']), + 'success' => intval($todayStats['success']), + 'failed' => intval($todayStats['failed']) + ], + 'total' => [ + 'total' => intval($totalStats['total']), + 'success' => intval($totalStats['success']), + 'failed' => intval($totalStats['failed']) + ] + ] + ]); + } + + /** + * 获取流量分发详情 + * @return \think\response\Json + */ + public function getTrafficDistributionDetail() + { + $id = $this->request->param('id', 0); + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + $detail = Db::name('workbench_traffic_distribution_item')->alias('wtdi') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wtdi.wechatAccountId', 'left') + ->join(['s2_wechat_friend' => 'wf'], 'wf.id = wtdi.wechatFriendId', 'left') + ->field([ + 'wtdi.id', + 'wtdi.workbenchId', + 'wtdi.wechatAccountId', + 'wtdi.wechatFriendId', + 'wtdi.createTime as distributeTime', + 'wtdi.status', + 'wtdi.errorMsg', + 'wa.nickName as operatorName', + 'wa.avatar as operatorAvatar', + 'wf.nickName as friendName', + 'wf.avatar as friendAvatar', + 'wf.gender', + 'wf.province', + 'wf.city', + 'wf.signature', + 'wf.remark' + ]) + ->where('wtdi.id', $id) + ->find(); + + if (empty($detail)) { + return json(['code' => 404, 'msg' => '记录不存在']); + } + + // 处理数据 + $detail['distributeTime'] = date('Y-m-d H:i:s', $detail['distributeTime']); + + // 处理性别 + $genderMap = [ + 0 => '未知', + 1 => '男', + 2 => '女' + ]; + $detail['genderText'] = $genderMap[$detail['gender']] ?? '未知'; + + // 处理状态文字 + $statusMap = [ + 0 => '待分发', + 1 => '分发成功', + 2 => '分发失败' + ]; + $detail['statusText'] = $statusMap[$detail['status']] ?? '未知状态'; + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => $detail + ]); + } + + /** + * 创建流量分发计划 + * @return \think\response\Json + */ + public function createTrafficPlan() + { + $param = $this->request->post(); + Db::startTrans(); + try { + // 1. 创建主表 + $planId = Db::name('ck_workbench')->insertGetId([ + 'name' => $param['name'], + 'type' => 5, // TYPE_TRAFFIC_DISTRIBUTION + 'status' => 1, + 'autoStart' => $param['autoStart'] ?? 0, + 'userId' => $this->request->userInfo['id'], + 'companyId' => $this->request->userInfo['companyId'], + 'createTime' => time(), + 'updateTime' => time() + ]); + // 2. 创建扩展表 + Db::name('ck_workbench_traffic_config')->insert([ + 'workbenchId' => $planId, + 'distributeType' => $param['distributeType'], + 'maxPerDay' => $param['maxPerDay'], + 'timeType' => $param['timeType'], + 'startTime' => $param['startTime'], + 'endTime' => $param['endTime'], + 'targets' => json_encode($param['targets'], JSON_UNESCAPED_UNICODE), + 'pools' => json_encode($param['poolGroups'], JSON_UNESCAPED_UNICODE), + 'createTime' => time(), + 'updateTime' => time() + ]); + Db::commit(); + return json(['code' => 200, 'msg' => '创建成功']); + } catch (\Exception $e) { + Db::rollback(); + return json(['code' => 500, 'msg' => '创建失败:' . $e->getMessage()]); + } + } + + /** + * 获取流量列表 + * @return \think\response\Json + */ + public function getTrafficList() + { + $companyId = $this->request->userInfo['companyId']; + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + $workbenchId = $this->request->param('workbenchId', ''); + $isRecycle = $this->request->param('isRecycle', ''); + if (empty($workbenchId)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + $workbench = Db::name('workbench')->where(['id' => $workbenchId, 'isDel' => 0, 'companyId' => $companyId, 'type' => 5])->find(); + + if (empty($workbench)) { + return json(['code' => 400, 'msg' => '该任务不存在或已删除']); + } + $query = Db::name('workbench_traffic_config_item')->alias('wtc') + ->join(['s2_wechat_friend' => 'wf'], 'wtc.wechatFriendId = wf.id') + ->join('users u', 'wtc.wechatAccountId = u.s2_accountId', 'left') + ->field([ + 'wtc.id', 'wtc.isRecycle', 'wtc.isRecycle', 'wtc.createTime','wtc.recycleTime', + 'wf.wechatId', 'wf.alias', 'wf.nickname', 'wf.avatar', 'wf.gender', 'wf.phone', + 'u.account', 'u.username' + ]) + ->where(['wtc.workbenchId' => $workbenchId]) + ->order('wtc.id DESC'); + + if (!empty($keyword)) { + $query->where('wf.wechatId|wf.alias|wf.nickname|wf.phone|u.account|u.username', 'like', '%' . $keyword . '%'); + } + + if ($isRecycle != '' || $isRecycle != null) { + $query->where('isRecycle',$isRecycle); + } + + $total = $query->count(); + $list = $query->page($page, $limit)->select(); + + foreach ($list as &$item) { + $item['createTime'] = date('Y-m-d H:i:s', $item['createTime']); + $item['recycleTime'] = date('Y-m-d H:i:s', $item['recycleTime']); + } + unset($item); + + $data = [ + 'total' => $total, + 'list' => $list, + ]; + + return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]); + } +} + diff --git a/application/cunkebao/model/AiKnowledgeBase.php b/application/cunkebao/model/AiKnowledgeBase.php new file mode 100644 index 0000000..fbf0664 --- /dev/null +++ b/application/cunkebao/model/AiKnowledgeBase.php @@ -0,0 +1,11 @@ + $item) { + if (is_integer($key)) { + $temp[] = "{$item} = VALUES({$item})"; + } else { + $temp[] = "{$key} = {$item}"; + } + } + $duplicate = implode(',', $temp); + } + } + if (!empty($limit)) { + // 分批写入 自动启动事务支持 + $this->startTrans(); + + try { + // array_chunk 函数把数组分割为新的数组块。 + //其中每个数组的单元数目由 size 参数决定。最后一个数组的单元数目可能会少几个。 + //可选参数 preserve_key 是一个布尔值,它指定新数组的元素是否有和原数组相同的键(用于关联数组),还是从 0 开始的新数字键(用于索引数组)。默认是分配新的键。 + $array = array_chunk($data, $limit, true); + $count = 0; + + foreach ($array as $item) { + $sql = $this->fetchSql(true)->insertAll($item); + $sql = preg_replace("/INSERT\s*INTO\s*/", "INSERT IGNORE INTO ", $sql); + if (is_string($duplicate)) { + $sql = $sql . ' ON DUPLICATE KEY UPDATE ' . $duplicate; + } + $count += $this->execute($sql); // 获取影响函数 + } + // 提交事务 + $this->commit(); + } catch (\Exception $e) { + $this->rollback(); + throw $e; + } + return $count; + } else { + $sql = $this->fetchSql(true)->insertAll($data); + $sql = preg_replace("/INSERT\s*INTO\s*/", "INSERT IGNORE INTO ", $sql); + if (is_string($duplicate)) { + $sql = $sql . ' ON DUPLICATE KEY UPDATE ' . $duplicate; + } + return $this->execute($sql); + } + } +} \ No newline at end of file diff --git a/application/cunkebao/model/ContentItem.php b/application/cunkebao/model/ContentItem.php new file mode 100644 index 0000000..efc9202 --- /dev/null +++ b/application/cunkebao/model/ContentItem.php @@ -0,0 +1,20 @@ +belongsTo('User', 'userId', 'id'); + } + + // 定义关联的内容项目 + public function items() + { + return $this->hasMany('ContentItem', 'libraryId', 'id'); + } + + // 根据ID数组获取内容库列表 + public static function getByIds($ids) + { + if (empty($ids)) { + return []; + } + + return self::where('id', 'in', $ids)->select(); + } +} \ No newline at end of file diff --git a/application/cunkebao/model/DistributionChannel.php b/application/cunkebao/model/DistributionChannel.php new file mode 100644 index 0000000..500ada9 --- /dev/null +++ b/application/cunkebao/model/DistributionChannel.php @@ -0,0 +1,87 @@ + 'integer', + 'companyId' => 'integer', + 'totalCustomers' => 'integer', + 'todayCustomers' => 'integer', + 'totalFriends' => 'integer', + 'todayFriends' => 'integer', + 'withdrawableAmount' => 'integer', + 'createTime' => 'timestamp', + 'updateTime' => 'timestamp', + 'deleteTime' => 'timestamp', + ]; + + /** + * 生成渠道编码 + * 格式:QD + 时间戳 + 9位随机字符串 + * + * @return string + */ + public static function generateChannelCode() + { + $prefix = 'QD'; + $timestamp = time(); + $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + $randomStr = ''; + + // 生成9位随机字符串 + for ($i = 0; $i < 9; $i++) { + $randomStr .= $chars[mt_rand(0, strlen($chars) - 1)]; + } + + $code = $prefix . $timestamp . $randomStr; + + // 检查是否已存在 + $exists = self::where('code', $code)->find(); + if ($exists) { + // 如果已存在,递归重新生成 + return self::generateChannelCode(); + } + + return $code; + } + + /** + * 创建类型:manual(手动创建) + */ + const CREATE_TYPE_MANUAL = 'manual'; + + /** + * 创建类型:auto(扫码创建) + */ + const CREATE_TYPE_AUTO = 'auto'; + + /** + * 状态:enabled(启用) + */ + const STATUS_ENABLED = 'enabled'; + + /** + * 状态:disabled(禁用) + */ + const STATUS_DISABLED = 'disabled'; +} + diff --git a/application/cunkebao/model/DistributionWithdrawal.php b/application/cunkebao/model/DistributionWithdrawal.php new file mode 100644 index 0000000..64e0861 --- /dev/null +++ b/application/cunkebao/model/DistributionWithdrawal.php @@ -0,0 +1,71 @@ + 'integer', + 'companyId' => 'integer', + 'channelId' => 'integer', + 'amount' => 'integer', + 'reviewTime' => 'timestamp', + 'applyTime' => 'timestamp', + 'createTime' => 'timestamp', + 'updateTime' => 'timestamp', + ]; + + /** + * 状态:pending(待审核) + */ + const STATUS_PENDING = 'pending'; + + /** + * 状态:approved(已通过) + */ + const STATUS_APPROVED = 'approved'; + + /** + * 状态:rejected(已拒绝) + */ + const STATUS_REJECTED = 'rejected'; + + /** + * 状态:paid(已打款) + */ + const STATUS_PAID = 'paid'; + + /** + * 支付类型:wechat(微信) + */ + const PAY_TYPE_WECHAT = 'wechat'; + + /** + * 支付类型:alipay(支付宝) + */ + const PAY_TYPE_ALIPAY = 'alipay'; + + /** + * 支付类型:bankcard(银行卡) + */ + const PAY_TYPE_BANKCARD = 'bankcard'; +} + + + + diff --git a/application/cunkebao/model/FriendTask.php b/application/cunkebao/model/FriendTask.php new file mode 100644 index 0000000..0be5936 --- /dev/null +++ b/application/cunkebao/model/FriendTask.php @@ -0,0 +1,286 @@ + '待处理', + self::STATUS_PROCESSING => '处理中', + self::STATUS_APPROVED => '已通过', + self::STATUS_REJECTED => '已拒绝', + self::STATUS_EXPIRED => '已过期', + self::STATUS_CANCELLED => '已取消' + ]; + + return isset($statusMap[$status]) ? $statusMap[$status] : '未知状态'; + } + + /** + * 获取好友任务列表 + * @param array $where 查询条件 + * @param string $order 排序条件 + * @param int $page 页码 + * @param int $limit 每页数量 + * @return \think\Paginator + */ + public static function getTaskList($where = [], $order = 'createTime desc', $page = 1, $limit = 10) + { + return self::where($where) + ->order($order) + ->paginate($limit, false, ['page' => $page]); + } + + /** + * 获取任务详情 + * @param int $id 任务ID + * @return array|null + */ + public static function getTaskDetail($id) + { + return self::where('id', $id)->find(); + } + + /** + * 创建好友任务 + * @param array $data 任务数据 + * @return int|bool 任务ID或false + */ + public static function createTask($data) + { + // 确保必填字段存在 + if (!isset($data['id'])) { + return false; + } + + // 设置默认值 + if (!isset($data['status'])) { + $data['status'] = self::STATUS_PENDING; + } + + // 设置创建时间 + $data['createTime'] = time(); + $data['updateTime'] = time(); + + // 创建任务 + $task = new self; + $task->allowField(true)->save($data); + + return $task->id; + } + + /** + * 更新任务信息 + * @param int $id 任务ID + * @param array $data 更新数据 + * @return bool + */ + public static function updateTask($id, $data) + { + // 更新时间 + $data['updateTime'] = time(); + + return self::where('id', $id)->update($data); + } + + /** + * 更新任务状态 + * @param int $id 任务ID + * @param int $status 新状态 + * @param string $remark 备注 + * @return bool + */ + public static function updateTaskStatus($id, $status, $remark = '') + { + $data = [ + 'status' => $status, + 'updateTime' => time() + ]; + + if (!empty($remark)) { + $data['remark'] = $remark; + } + + return self::where('id', $id)->update($data); + } + + /** + * 取消任务 + * @param int $id 任务ID + * @param string $remark 取消原因 + * @return bool + */ + public static function cancelTask($id, $remark = '') + { + return self::updateTaskStatus($id, self::STATUS_CANCELLED, $remark); + } + + /** + * 任务审批通过 + * @param int $id 任务ID + * @param string $remark 备注信息 + * @return bool + */ + public static function approveTask($id, $remark = '') + { + return self::updateTaskStatus($id, self::STATUS_APPROVED, $remark); + } + + /** + * 任务拒绝 + * @param int $id 任务ID + * @param string $remark 拒绝原因 + * @return bool + */ + public static function rejectTask($id, $remark = '') + { + return self::updateTaskStatus($id, self::STATUS_REJECTED, $remark); + } + + /** + * 根据微信账号ID获取任务列表 + * @param int $wechatAccountId 微信账号ID + * @param array $status 状态数组,默认查询所有状态 + * @param int $page 页码 + * @param int $limit 每页数量 + * @return \think\Paginator + */ + public static function getTasksByWechatAccount($wechatAccountId, $status = [], $page = 1, $limit = 10) + { + $where = ['wechatAccountId' => $wechatAccountId]; + + if (!empty($status)) { + $where['status'] = ['in', $status]; + } + + return self::getTaskList($where, 'createTime desc', $page, $limit); + } + + /** + * 根据操作账号ID获取任务列表 + * @param int $operatorAccountId 操作账号ID + * @param array $status 状态数组,默认查询所有状态 + * @param int $page 页码 + * @param int $limit 每页数量 + * @return \think\Paginator + */ + public static function getTasksByOperator($operatorAccountId, $status = [], $page = 1, $limit = 10) + { + $where = ['operatorAccountId' => $operatorAccountId]; + + if (!empty($status)) { + $where['status'] = ['in', $status]; + } + + return self::getTaskList($where, 'createTime desc', $page, $limit); + } + + /** + * 根据手机号/微信号查询任务 + * @param string $phone 手机号/微信号 + * @param int $tenantId 租户ID + * @return array + */ + public static function getTasksByPhone($phone, $tenantId = null) + { + $where = ['phone' => $phone]; + + if ($tenantId !== null) { + $where['tenantId'] = $tenantId; + } + + return self::where($where)->select(); + } + + /** + * 获取统计数据 + * @param int $tenantId 租户ID + * @param int $timeRange 时间范围(秒) + * @return array + */ + public static function getTaskStats($tenantId = null, $timeRange = 86400) + { + $where = []; + + if ($tenantId !== null) { + $where['tenantId'] = $tenantId; + } + + // 时间范围 + $startTime = time() - $timeRange; + $where['createTime'] = ['>=', $startTime]; + + // 获取各状态的任务数量 + $stats = [ + 'total' => self::where($where)->count(), + 'pending' => self::where(array_merge($where, ['status' => self::STATUS_PENDING]))->count(), + 'processing' => self::where(array_merge($where, ['status' => self::STATUS_PROCESSING]))->count(), + 'approved' => self::where(array_merge($where, ['status' => self::STATUS_APPROVED]))->count(), + 'rejected' => self::where(array_merge($where, ['status' => self::STATUS_REJECTED]))->count(), + 'expired' => self::where(array_merge($where, ['status' => self::STATUS_EXPIRED]))->count(), + 'cancelled' => self::where(array_merge($where, ['status' => self::STATUS_CANCELLED]))->count() + ]; + + return $stats; + } + + /** + * 任务处理结果统计 + * @param int $tenantId 租户ID + * @param int $timeRange 时间范围(秒) + * @return array + */ + public static function getTaskResultStats($tenantId = null, $timeRange = 86400 * 30) + { + $where = []; + + if ($tenantId !== null) { + $where['tenantId'] = $tenantId; + } + + // 时间范围 + $startTime = time() - $timeRange; + $where['createTime'] = ['>=', $startTime]; + + // 获取处理结果数据 + $stats = [ + 'total' => self::where($where)->count(), + 'approved' => self::where(array_merge($where, ['status' => self::STATUS_APPROVED]))->count(), + 'rejected' => self::where(array_merge($where, ['status' => self::STATUS_REJECTED]))->count(), + 'pending' => self::where(array_merge($where, ['status' => ['in', [self::STATUS_PENDING, self::STATUS_PROCESSING]]]))->count(), + 'other' => self::where(array_merge($where, ['status' => ['in', [self::STATUS_EXPIRED, self::STATUS_CANCELLED]]]))->count() + ]; + + // 计算成功率 + $stats['approvalRate'] = $stats['total'] > 0 ? round($stats['approved'] / $stats['total'] * 100, 2) : 0; + + return $stats; + } +} \ No newline at end of file diff --git a/application/cunkebao/model/PlanTask.php b/application/cunkebao/model/PlanTask.php new file mode 100644 index 0000000..d4ebc9f --- /dev/null +++ b/application/cunkebao/model/PlanTask.php @@ -0,0 +1,13 @@ +belongsTo('Workbench', 'id', 'userId'); + } +} \ No newline at end of file diff --git a/application/cunkebao/model/WechatChatroom.php b/application/cunkebao/model/WechatChatroom.php new file mode 100644 index 0000000..4c07e96 --- /dev/null +++ b/application/cunkebao/model/WechatChatroom.php @@ -0,0 +1,15 @@ +hasOne('WorkbenchAutoLike', 'workbenchId', 'id'); + } + + // 朋友圈同步配置关联 + public function momentsSync() + { + return $this->hasOne('WorkbenchMomentsSync', 'workbenchId', 'id'); + } + + // 群消息推送配置关联 + public function groupPush() + { + return $this->hasOne('WorkbenchGroupPush', 'workbenchId', 'id'); + } + + // 自动建群配置关联 + public function groupCreate() + { + return $this->hasOne('WorkbenchGroupCreate', 'workbenchId', 'id'); + } + + // 流量分发配置关联 + public function trafficConfig() + { + return $this->hasOne('WorkbenchTrafficConfig', 'workbenchId', 'id'); + } + + public function importContact() + { + return $this->hasOne('WorkbenchImportContact', 'workbenchId', 'id'); + } + + + /** + * 用户关联 + */ + public function user() + { + return $this->belongsTo('User', 'userId', 'id'); + } +} \ No newline at end of file diff --git a/application/cunkebao/model/WorkbenchAutoLike.php b/application/cunkebao/model/WorkbenchAutoLike.php new file mode 100644 index 0000000..44fd263 --- /dev/null +++ b/application/cunkebao/model/WorkbenchAutoLike.php @@ -0,0 +1,25 @@ +belongsTo('Workbench', 'workbenchId', 'id'); + } +} \ No newline at end of file diff --git a/application/cunkebao/model/WorkbenchGroupCreate.php b/application/cunkebao/model/WorkbenchGroupCreate.php new file mode 100644 index 0000000..8721db9 --- /dev/null +++ b/application/cunkebao/model/WorkbenchGroupCreate.php @@ -0,0 +1,23 @@ +belongsTo('Workbench', 'workbenchId', 'id'); + } +} \ No newline at end of file diff --git a/application/cunkebao/model/WorkbenchGroupPush.php b/application/cunkebao/model/WorkbenchGroupPush.php new file mode 100644 index 0000000..b5e4447 --- /dev/null +++ b/application/cunkebao/model/WorkbenchGroupPush.php @@ -0,0 +1,23 @@ +belongsTo('Workbench', 'workbenchId', 'id'); + } +} \ No newline at end of file diff --git a/application/cunkebao/model/WorkbenchImportContact.php b/application/cunkebao/model/WorkbenchImportContact.php new file mode 100644 index 0000000..58b73b6 --- /dev/null +++ b/application/cunkebao/model/WorkbenchImportContact.php @@ -0,0 +1,25 @@ +belongsTo('Workbench', 'workbenchId', 'id'); + } +} \ No newline at end of file diff --git a/application/cunkebao/model/WorkbenchMomentsSync.php b/application/cunkebao/model/WorkbenchMomentsSync.php new file mode 100644 index 0000000..625f372 --- /dev/null +++ b/application/cunkebao/model/WorkbenchMomentsSync.php @@ -0,0 +1,59 @@ +belongsTo('Workbench', 'workbenchId', 'id'); + } + + // 定义关联的内容库 + public function contentLibraries() + { + return $this->belongsToMany('ContentLibrary', 'workbench_content_relation', 'contentLibraryId', 'workbenchId'); + } + + // 开始时间获取器 + public function getStartTimeAttr($value) + { + return $value ? date('H:i', strtotime($value)) : ''; + } + + // 结束时间获取器 + public function getEndTimeAttr($value) + { + return $value ? date('H:i', strtotime($value)) : ''; + } + + // 同步类型获取器 + public function getSyncTypeTextAttr($value, $data) + { + $types = [ + self::SYNC_TYPE_TEXT => '文本', + self::SYNC_TYPE_IMAGE => '图片', + self::SYNC_TYPE_VIDEO => '视频', + self::SYNC_TYPE_LINK => '链接' + ]; + return isset($types[$data['syncType']]) ? $types[$data['syncType']] : '未知'; + } +} \ No newline at end of file diff --git a/application/cunkebao/model/WorkbenchTrafficConfig.php b/application/cunkebao/model/WorkbenchTrafficConfig.php new file mode 100644 index 0000000..abf54be --- /dev/null +++ b/application/cunkebao/model/WorkbenchTrafficConfig.php @@ -0,0 +1,25 @@ +belongsTo('Workbench', 'workbenchId', 'id'); + } +} \ No newline at end of file diff --git a/application/cunkebao/service/ContentItemService.php b/application/cunkebao/service/ContentItemService.php new file mode 100644 index 0000000..b0a1df3 --- /dev/null +++ b/application/cunkebao/service/ContentItemService.php @@ -0,0 +1,158 @@ +save($this->prepareItemData($data, $libraryId)); + + if (!$result) { + Db::rollback(); + return ['code' => 500, 'msg' => '创建内容项目失败']; + } + + Db::commit(); + return ['code' => 200, 'msg' => '创建成功', 'data' => ['id' => $item->id]]; + } catch (\Exception $e) { + Db::rollback(); + return ['code' => 500, 'msg' => '创建失败:' . $e->getMessage()]; + } + } + + /** + * 准备内容项目数据 + * @param array $data 原始数据 + * @param int $libraryId 内容库ID + * @return array + */ + private function prepareItemData($data, $libraryId) + { + return [ + 'libraryId' => $libraryId, + 'title' => $data['title'] ?? '', + 'content' => $data['content'] ?? '', + 'images' => isset($data['images']) ? json_encode($data['images']) : json_encode([]), + 'videos' => isset($data['videos']) ? json_encode($data['videos']) : json_encode([]), + 'status' => $data['status'] ?? 0, + 'createTime' => time(), + 'updateTime' => time() + ]; + } + + /** + * 删除内容项目 + * @param int $itemId 内容项目ID + * @return array + */ + public function deleteItem($itemId) + { + try { + $result = ContentItem::where('id', $itemId) + ->update(['isDel' => 1, 'delTime' => time()]); + + if ($result === false) { + return ['code' => 500, 'msg' => '删除失败']; + } + + return ['code' => 200, 'msg' => '删除成功']; + } catch (\Exception $e) { + return ['code' => 500, 'msg' => '删除失败:' . $e->getMessage()]; + } + } + + // ==================== 查询相关 ==================== + + /** + * 获取内容项目列表 + * @param array $params 查询参数 + * @param int $libraryId 内容库ID + * @return array + */ + public function getItemList($params, $libraryId) + { + $where = [ + ['libraryId', '=', $libraryId], + ['isDel', '=', 0] + ]; + + if (!empty($params['keyword'])) { + $where[] = ['title', 'like', '%' . $params['keyword'] . '%']; + } + + if (isset($params['status'])) { + $where[] = ['status', '=', $params['status']]; + } + + $list = ContentItem::where($where) + ->field('id,title,content,images,videos,status,createTime,updateTime') + ->order('id', 'desc') + ->page($params['page'], $params['limit']) + ->select(); + + $this->processItemList($list); + + $total = ContentItem::where($where)->count(); + + return [ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + 'page' => $params['page'] + ] + ]; + } + + /** + * 处理内容项目列表数据 + * @param array $list 内容项目列表 + */ + private function processItemList(&$list) + { + foreach ($list as &$item) { + $item['images'] = json_decode($item['images'] ?: '[]', true); + $item['videos'] = json_decode($item['videos'] ?: '[]', true); + } + } + + // ==================== 状态管理 ==================== + + /** + * 更新内容项目状态 + * @param int $itemId 内容项目ID + * @param int $status 状态 + * @return array + */ + public function updateItemStatus($itemId, $status) + { + try { + $result = ContentItem::where('id', $itemId) + ->update(['status' => $status, 'updateTime' => time()]); + + if ($result === false) { + return ['code' => 500, 'msg' => '更新状态失败']; + } + + return ['code' => 200, 'msg' => '更新成功']; + } catch (\Exception $e) { + return ['code' => 500, 'msg' => '更新失败:' . $e->getMessage()]; + } + } +} \ No newline at end of file diff --git a/application/cunkebao/service/ContentLibraryService.php b/application/cunkebao/service/ContentLibraryService.php new file mode 100644 index 0000000..23f0d9c --- /dev/null +++ b/application/cunkebao/service/ContentLibraryService.php @@ -0,0 +1,185 @@ + $data['name'], + 'userId' => $userId, + 'isDel' => 0 + ])->find(); + + if ($exists) { + return ['code' => 400, 'msg' => '内容库名称已存在']; + } + + Db::startTrans(); + try { + $library = new ContentLibrary; + $result = $library->save($this->prepareLibraryData($data, $userId)); + + if (!$result) { + Db::rollback(); + return ['code' => 500, 'msg' => '创建内容库失败']; + } + + Db::commit(); + return ['code' => 200, 'msg' => '创建成功', 'data' => ['id' => $library->id]]; + } catch (\Exception $e) { + Db::rollback(); + return ['code' => 500, 'msg' => '创建失败:' . $e->getMessage()]; + } + } + + /** + * 准备内容库数据 + * @param array $data 原始数据 + * @param int $userId 用户ID + * @return array + */ + private function prepareLibraryData($data, $userId) + { + return [ + 'name' => $data['name'], + 'sourceFriends' => $data['sourceType'] == 1 ? json_encode($data['friends']) : json_encode([]), + 'sourceGroups' => $data['sourceType'] == 2 ? json_encode($data['groups']) : json_encode([]), + 'groupMembers' => $data['sourceType'] == 2 ? json_encode($data['groupMembers']) : json_encode([]), + 'keywordInclude' => isset($data['keywordInclude']) ? json_encode($data['keywordInclude'], 256) : json_encode([]), + 'keywordExclude' => isset($data['keywordExclude']) ? json_encode($data['keywordExclude'], 256) : json_encode([]), + 'aiEnabled' => $data['aiEnabled'] ?? 0, + 'aiPrompt' => $data['aiPrompt'] ?? '', + 'timeEnabled' => $data['timeEnabled'] ?? 0, + 'timeStart' => isset($data['startTime']) ? strtotime($data['startTime']) : 0, + 'timeEnd' => isset($data['endTime']) ? strtotime($data['endTime']) : 0, + 'sourceType' => $data['sourceType'] ?? 1, + 'status' => $data['status'] ?? 0, + 'userId' => $userId, + 'createTime' => time(), + 'updateTime' => time() + ]; + } + + // ==================== 查询相关 ==================== + + /** + * 获取内容库列表 + * @param array $params 查询参数 + * @param int $userId 用户ID + * @return array + */ + public function getLibraryList($params, $userId) + { + $where = [ + ['userId', '=', $userId], + ['isDel', '=', 0] + ]; + + if (!empty($params['keyword'])) { + $where[] = ['name', 'like', '%' . $params['keyword'] . '%']; + } + + if (!empty($params['sourceType'])) { + $where[] = ['sourceType', '=', $params['sourceType']]; + } + + $list = ContentLibrary::where($where) + ->field('id,name,sourceFriends,sourceGroups,keywordInclude,keywordExclude,aiEnabled,aiPrompt,timeEnabled,timeStart,timeEnd,status,sourceType,userId,createTime,updateTime') + ->with(['user' => function($query) { + $query->field('id,username'); + }]) + ->order('id', 'desc') + ->page($params['page'], $params['limit']) + ->select(); + + $this->processLibraryList($list); + + $total = ContentLibrary::where($where)->count(); + + return [ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + 'page' => $params['page'] + ] + ]; + } + + /** + * 处理内容库列表数据 + * @param array $list 内容库列表 + */ + private function processLibraryList(&$list) + { + foreach ($list as &$item) { + $item['sourceFriends'] = json_decode($item['sourceFriends'] ?: '[]', true); + $item['sourceGroups'] = json_decode($item['sourceGroups'] ?: '[]', true); + $item['keywordInclude'] = json_decode($item['keywordInclude'] ?: '[]', true); + $item['keywordExclude'] = json_decode($item['keywordExclude'] ?: '[]', true); + $item['creatorName'] = $item['user']['username'] ?? ''; + + if (!empty($item['sourceFriends']) && $item['sourceType'] == 1) { + $item['selectedFriends'] = $this->getFriendsInfo($item['sourceFriends']); + } + + if (!empty($item['sourceGroups']) && $item['sourceType'] == 2) { + $item['selectedGroups'] = $this->getGroupsInfo($item['sourceGroups']); + } + + unset($item['user']); + } + } + + // ==================== 数据关联查询 ==================== + + /** + * 获取好友信息 + * @param array $friendIds 好友ID列表 + * @return array + */ + private function getFriendsInfo($friendIds) + { + if (empty($friendIds)) { + return []; + } + + return Db::name('wechat_friend')->alias('wf') + ->field('wf.id,wf.wechatId, wa.nickname, wa.avatar') + ->join('wechat_account wa', 'wf.wechatId = wa.wechatId') + ->whereIn('wf.id', $friendIds) + ->select(); + } + + /** + * 获取群组信息 + * @param array $groupIds 群组ID列表 + * @return array + */ + private function getGroupsInfo($groupIds) + { + if (empty($groupIds)) { + return []; + } + + return Db::name('wechat_group')->alias('g') + ->field('g.id, g.chatroomId, g.name, g.avatar, g.ownerWechatId') + ->whereIn('g.id', $groupIds) + ->select(); + } +} \ No newline at end of file diff --git a/application/cunkebao/service/DistributionRewardService.php b/application/cunkebao/service/DistributionRewardService.php new file mode 100644 index 0000000..668f3f6 --- /dev/null +++ b/application/cunkebao/service/DistributionRewardService.php @@ -0,0 +1,276 @@ +where('id', $taskId) + ->find(); + + if (!$task) { + return false; + } + + // 解析分销配置 + $sceneConf = json_decode($task['sceneConf'], true) ?: []; + $distributionConfig = $sceneConf['distribution'] ?? null; + + // 检查是否开启分销 + if (empty($distributionConfig) || empty($distributionConfig['enabled'])) { + return false; + } + + // 检查是否有获客奖励 + $rewardAmount = intval($distributionConfig['customerRewardAmount'] ?? 0); + if ($rewardAmount <= 0) { + return false; + } + + // 获取渠道列表(从分销配置中获取允许分佣的渠道) + $channelIds = $distributionConfig['channels'] ?? []; + if (empty($channelIds) || !is_array($channelIds)) { + return false; + } + + $companyId = $task['companyId']; + $sceneId = $task['sceneId']; + + // 获取场景名称(用于展示来源类型) + $scene = Db::name('plan_scene') + ->where('id', $sceneId) + ->field('name') + ->find(); + $sceneName = $scene['name'] ?? '未知场景'; + + // 如果指定了 channelId(cid),仅允许该渠道获得分佣 + if (!empty($channelId)) { + // 必须在配置的渠道列表中,且是有效ID + if (!in_array($channelId, $channelIds)) { + // 该渠道不在本计划允许分佣的渠道列表中,直接返回 + return false; + } + $channelIds = [$channelId]; + } + + // 开始事务 + Db::startTrans(); + try { + // 为每个渠道记录收益并更新可提现金额 + foreach ($channelIds as $channelId) { + // 验证渠道是否存在 + $channel = Db::name('distribution_channel') + ->where([ + ['id', '=', $channelId], + ['companyId', '=', $companyId], + ['status', '=', 'enabled'], + ['deleteTime', '=', 0] + ]) + ->find(); + + if (!$channel) { + continue; // 跳过不存在的渠道 + } + + // 记录收益明细 + Db::name('distribution_revenue_record')->insert([ + 'companyId' => $companyId, + 'channelId' => $channelId, + 'channelCode' => $channel['code'], + 'type' => 'customer_acquisition', // 获客类型 + 'sourceType' => $sceneName, + 'sourceId' => $taskId, // 活动ID(获客任务ID) + 'amount' => $rewardAmount, // 金额(分) + 'remark' => '获客奖励:' . $phone, + 'createTime' => time(), + 'updateTime' => time(), + ]); + + // 更新渠道可提现金额 + Db::name('distribution_channel') + ->where('id', $channelId) + ->setInc('withdrawableAmount', $rewardAmount); + + // 更新渠道获客统计 + Db::name('distribution_channel') + ->where('id', $channelId) + ->setInc('totalCustomers', 1); + + // 更新今日获客统计(如果是今天) + $todayStart = strtotime(date('Y-m-d 00:00:00')); + $todayEnd = strtotime(date('Y-m-d 23:59:59')); + $createTime = time(); + if ($createTime >= $todayStart && $createTime <= $todayEnd) { + Db::name('distribution_channel') + ->where('id', $channelId) + ->setInc('todayCustomers', 1); + } + } + + Db::commit(); + return true; + + } catch (Exception $e) { + Db::rollback(); + throw $e; + } + + } catch (Exception $e) { + // 记录错误日志,但不影响主流程 + \think\Log::error('记录获客奖励失败:' . $e->getMessage()); + return false; + } + } + + /** + * 记录添加好友奖励 + * + * @param int $taskId 获客计划ID + * @param int $customerId 客户ID(task_customer表的id) + * @param string $phone 客户手机号 + * @param int|null $channelId 渠道ID(分销渠道ID,对应distribution_channel.id)。为空时按配置的所有渠道分配 + * @return bool + */ + public static function recordAddFriendReward($taskId, $customerId, $phone, $channelId = null) + { + try { + // 获取获客计划信息 + $task = Db::name('customer_acquisition_task') + ->where('id', $taskId) + ->find(); + + if (!$task) { + return false; + } + + // 解析分销配置 + $sceneConf = json_decode($task['sceneConf'], true) ?: []; + $distributionConfig = $sceneConf['distribution'] ?? null; + + // 检查是否开启分销 + if (empty($distributionConfig) || empty($distributionConfig['enabled'])) { + return false; + } + + // 检查是否有添加奖励 + $rewardAmount = intval($distributionConfig['addFriendRewardAmount'] ?? 0); + if ($rewardAmount <= 0) { + return false; + } + + // 获取渠道列表(从分销配置中获取允许分佣的渠道) + $channelIds = $distributionConfig['channels'] ?? []; + if (empty($channelIds) || !is_array($channelIds)) { + return false; + } + + $companyId = $task['companyId']; + $sceneId = $task['sceneId']; + + // 获取场景名称(用于展示来源类型) + $scene = Db::name('plan_scene') + ->where('id', $sceneId) + ->field('name') + ->find(); + $sceneName = $scene['name'] ?? '未知场景'; + + // 如果指定了 channelId(cid),仅允许该渠道获得分佣 + if (!empty($channelId)) { + // 必须在配置的渠道列表中,且是有效ID + if (!in_array($channelId, $channelIds)) { + // 该渠道不在本计划允许分佣的渠道列表中,直接返回 + return false; + } + $channelIds = [$channelId]; + } + + // 开始事务 + Db::startTrans(); + try { + // 为每个渠道记录收益并更新可提现金额 + foreach ($channelIds as $channelId) { + // 验证渠道是否存在 + $channel = Db::name('distribution_channel') + ->where([ + ['id', '=', $channelId], + ['companyId', '=', $companyId], + ['status', '=', 'enabled'], + ['deleteTime', '=', 0] + ]) + ->find(); + + if (!$channel) { + continue; // 跳过不存在的渠道 + } + + // 记录收益明细 + Db::name('distribution_revenue_record')->insert([ + 'companyId' => $companyId, + 'channelId' => $channelId, + 'channelCode' => $channel['code'], + 'type' => 'add_friend', // 添加好友类型 + 'sourceType' => $sceneName, + 'sourceId' => $taskId, // 活动ID(获客任务ID) + 'amount' => $rewardAmount, // 金额(分) + 'remark' => '添加好友奖励:' . $phone, + 'createTime' => time(), + 'updateTime' => time(), + ]); + + // 更新渠道可提现金额 + Db::name('distribution_channel') + ->where('id', $channelId) + ->setInc('withdrawableAmount', $rewardAmount); + + // 更新渠道好友统计 + Db::name('distribution_channel') + ->where('id', $channelId) + ->setInc('totalFriends', 1); + + // 更新今日好友统计(如果是今天) + $todayStart = strtotime(date('Y-m-d 00:00:00')); + $todayEnd = strtotime(date('Y-m-d 23:59:59')); + $createTime = time(); + if ($createTime >= $todayStart && $createTime <= $todayEnd) { + Db::name('distribution_channel') + ->where('id', $channelId) + ->setInc('todayFriends', 1); + } + } + + Db::commit(); + return true; + + } catch (Exception $e) { + Db::rollback(); + throw $e; + } + + } catch (Exception $e) { + // 记录错误日志,但不影响主流程 + \think\Log::error('记录添加好友奖励失败:' . $e->getMessage()); + return false; + } + } +} + diff --git a/application/cunkebao/validate/DistributionChannel.php b/application/cunkebao/validate/DistributionChannel.php new file mode 100644 index 0000000..11016e0 --- /dev/null +++ b/application/cunkebao/validate/DistributionChannel.php @@ -0,0 +1,31 @@ + 'require|length:1,50', + 'phone' => 'regex:^1[3-9]\d{9}$', + 'wechatId' => 'max:50', + 'remarks' => 'max:200', + ]; + + protected $message = [ + 'name.require' => '渠道名称不能为空', + 'name.length' => '渠道名称长度必须在1-50个字符之间', + 'phone.regex' => '手机号格式不正确,请输入11位数字且以1开头', + 'wechatId.max' => '微信号长度不能超过50个字符', + 'remarks.max' => '备注信息长度不能超过200个字符', + ]; + + protected $scene = [ + 'create' => ['name', 'phone', 'wechatId', 'remarks'], + ]; +} + diff --git a/application/cunkebao/validate/Task.php b/application/cunkebao/validate/Task.php new file mode 100644 index 0000000..b64f847 --- /dev/null +++ b/application/cunkebao/validate/Task.php @@ -0,0 +1,48 @@ + 'require|max:100', + 'device_id' => 'number', + 'scene_id' => 'number', + 'scene_config' => 'array', + 'status' => 'in:0,1,2,3', + 'priority' => 'between:1,10', + 'created_by' => 'number' + ]; + + /** + * 错误信息 + * @var array + */ + protected $message = [ + 'name.require' => '任务名称不能为空', + 'name.max' => '任务名称不能超过100个字符', + 'device_id.number' => '设备ID必须是数字', + 'scene_id.number' => '场景ID必须是数字', + 'scene_config.array'=> '场景配置必须是数组', + 'status.in' => '状态值无效', + 'priority.between' => '优先级必须在1到10之间', + 'created_by.number' => '创建者ID必须是数字' + ]; + + /** + * 验证场景 + * @var array + */ + protected $scene = [ + 'create' => ['name', 'device_id', 'scene_id', 'scene_config', 'status', 'priority', 'created_by'], + 'update' => ['name', 'device_id', 'scene_id', 'scene_config', 'status', 'priority'] + ]; +} \ No newline at end of file diff --git a/application/cunkebao/validate/Traffic.php b/application/cunkebao/validate/Traffic.php new file mode 100644 index 0000000..2c51c62 --- /dev/null +++ b/application/cunkebao/validate/Traffic.php @@ -0,0 +1,51 @@ + 'require|mobile', + 'gender' => 'in:0,1,2', + 'age' => 'number|between:0,120', + 'tags' => 'max:255', + 'province' => 'max:50', + 'city' => 'max:50', + 'source_channel' => 'max:50', + 'source_detail' => 'array' + ]; + + /** + * 错误信息 + * @var array + */ + protected $message = [ + 'mobile.require' => '手机号不能为空', + 'mobile.mobile' => '手机号格式不正确', + 'gender.in' => '性别值无效', + 'age.number' => '年龄必须是数字', + 'age.between' => '年龄必须在0到120之间', + 'tags.max' => '标签不能超过255个字符', + 'province.max' => '省份不能超过50个字符', + 'city.max' => '城市不能超过50个字符', + 'source_channel.max' => '来源渠道不能超过50个字符', + 'source_detail.array'=> '来源详情必须是数组' + ]; + + /** + * 验证场景 + * @var array + */ + protected $scene = [ + 'create' => ['mobile', 'gender', 'age', 'tags', 'province', 'city', 'source_channel', 'source_detail'], + 'update' => ['gender', 'age', 'tags', 'province', 'city'] + ]; +} \ No newline at end of file diff --git a/application/cunkebao/validate/Workbench.php b/application/cunkebao/validate/Workbench.php new file mode 100644 index 0000000..7225c5a --- /dev/null +++ b/application/cunkebao/validate/Workbench.php @@ -0,0 +1,386 @@ + 'require|max:100', + 'type' => 'require|in:1,2,3,4,5,6', + //'autoStart' => 'require|boolean', + // 自动点赞特有参数 + 'interval' => 'requireIf:type,1|number|min:1', + 'maxLikes' => 'requireIf:type,1|number|min:1', + 'startTime' => 'requireIf:type,1|dateFormat:H:i', + 'endTime' => 'requireIf:type,1|dateFormat:H:i', + 'contentTypes' => 'requireIf:type,1|array|contentTypeEnum:text,image,video', + //'targetGroups' => 'requireIf:type,1|array', + // 朋友圈同步特有参数 + //'syncInterval' => 'requireIf:type,2|number|min:1', + 'syncCount' => 'requireIf:type,2|number|min:1', + 'syncType' => 'requireIf:type,2|in:1,2,3,4', + 'startTime' => 'requireIf:type,2|dateFormat:H:i', + 'endTime' => 'requireIf:type,2|dateFormat:H:i', + 'accountGroups' => 'requireIf:type,2|in:1,2', + 'contentGroups' => 'requireIf:type,2|array', + // 群消息推送特有参数 + 'pushType' => 'requireIf:type,3|in:0,1', // 推送方式 0定时 1立即 + 'targetType' => 'requireIf:type,3|in:1,2', // 推送目标类型:1=群推送,2=好友推送 + 'groupPushSubType' => 'checkGroupPushSubType|in:1,2', // 群推送子类型:1=群群发,2=群公告(仅当targetType=1时有效) + 'maxPerDay' => 'requireIf:type,3|number|min:1', + 'pushOrder' => 'requireIf:type,3|in:1,2', // 1最早 2最新 + 'isLoop' => 'requireIf:type,3|in:0,1', + 'status' => 'requireIf:type,3|in:0,1', + 'wechatGroups' => 'checkGroupPushTarget|array|min:1', // 当targetType=1时必填 + 'wechatFriends' => 'checkFriendPushTarget|array', // 当targetType=2时可选(可以为空) + 'ownerWechatId' => 'checkFriendPushService', // 当targetType=2且未选择好友/流量池时必填 + 'contentGroups' => 'requireIf:type,3|array|min:1', + // 群公告特有参数 + 'announcementContent' => 'checkAnnouncementContent|max:5000', // 群公告内容(当groupPushSubType=2时必填) + 'enableAiRewrite' => 'checkEnableAiRewrite|in:0,1', // 是否启用AI智能话术改写 + 'aiRewritePrompt' => 'checkAiRewritePrompt|max:500', // AI改写提示词(当enableAiRewrite=1时必填) + // 自动建群特有参数 + 'groupNameTemplate' => 'requireIf:type,4|max:50', + 'maxGroupsPerDay' => 'requireIf:type,4|number|min:1', + 'groupSizeMin' => 'requireIf:type,4|number|min:1|max:50', + 'groupSizeMax' => 'requireIf:type,4|number|min:1|max:50', + // 流量分发特有参数 + 'distributeType' => 'requireIf:type,5|in:1,2', + 'maxPerDay' => 'requireIf:type,5|number|min:1', + 'timeType' => 'requireIf:type,5|in:1,2', + 'accountGroups' => 'requireIf:type,5|array|min:1', + // 通用参数 + 'deviceGroups' => 'requireIf:type,1,2,5|array', + 'trafficPools' => 'checkFriendPushPools', + ]; + + /** + * 错误信息 + */ + protected $message = [ + 'name.require' => '请输入任务名称', + 'name.max' => '任务名称最多100个字符', + 'type.require' => '请选择工作台类型', + 'type.in' => '工作台类型错误', + 'autoStart.require' => '请选择是否自动启动', + 'autoStart.boolean' => '自动启动参数必须为布尔值', + // 自动点赞相关提示 + 'interval.requireIf' => '请设置点赞间隔', + 'interval.number' => '点赞间隔必须为数字', + 'interval.min' => '点赞间隔必须大于0', + 'maxLikes.requireIf' => '请设置每日最大点赞数', + 'maxLikes.number' => '每日最大点赞数必须为数字', + 'maxLikes.min' => '每日最大点赞数必须大于0', + 'startTime.requireIf' => '请设置开始时间', + 'startTime.dateFormat' => '开始时间格式错误', + 'endTime.requireIf' => '请设置结束时间', + 'endTime.dateFormat' => '结束时间格式错误', + 'contentTypes.requireIf' => '请选择点赞内容类型', + 'contentTypes.array' => '点赞内容类型必须是数组', + 'contentTypes.contentTypeEnum' => '点赞内容类型只能是text、image、video', + // 朋友圈同步相关提示 + /* 'syncInterval.requireIf' => '请设置同步间隔', + 'syncInterval.number' => '同步间隔必须为数字', + 'syncInterval.min' => '同步间隔必须大于0',*/ + 'syncCount.requireIf' => '请设置同步数量', + 'syncCount.number' => '同步数量必须为数字', + 'syncCount.min' => '同步数量必须大于0', + 'syncType.requireIf' => '请选择同步类型', + 'syncType.in' => '同步类型错误', + 'startTime.requireIf' => '请设置发布开始时间', + 'startTime.dateFormat' => '发布开始时间格式错误', + 'endTime.requireIf' => '请设置发布结束时间', + 'endTime.dateFormat' => '发布结束时间格式错误', + 'accountGroups.requireIf' => '请选择账号类型', + 'accountGroups.in' => '账号类型错误', + 'contentGroups.requireIf' => '请选择内容库', + 'contentGroups.array' => '内容库格式错误', + // 群消息推送相关提示 + 'pushType.requireIf' => '请选择推送方式', + 'startTime.requireIf' => '请设置推送开始时间', + 'startTime.dateFormat' => '推送开始时间格式错误', + 'endTime.requireIf' => '请设置推送结束时间', + 'endTime.dateFormat' => '推送结束时间格式错误', + 'maxPerDay.requireIf' => '请设置每日最大推送数', + 'maxPerDay.number' => '每日最大推送数必须为数字', + 'maxPerDay.min' => '每日最大推送数必须大于0', + 'pushOrder.requireIf' => '请选择推送顺序', + 'pushOrder.in' => '推送顺序错误', + 'isLoop.requireIf' => '请选择是否循环推送', + 'isLoop.in' => '循环推送参数错误', + 'targetType.requireIf' => '请选择推送目标类型', + 'targetType.in' => '推送目标类型错误,只能选择群推送或好友推送', + 'wechatGroups.requireIf' => '请选择推送群组', + 'wechatGroups.checkGroupPushTarget' => '群推送时必须选择推送群组', + 'wechatGroups.array' => '推送群组格式错误', + 'wechatGroups.min' => '至少选择一个推送群组', + 'groupPushSubType.checkGroupPushSubType' => '群推送子类型错误', + 'groupPushSubType.in' => '群推送子类型只能是群群发或群公告', + 'announcementContent.checkAnnouncementContent' => '群公告必须输入公告内容', + 'announcementContent.max' => '公告内容最多5000个字符', + 'enableAiRewrite.checkEnableAiRewrite' => 'AI智能话术改写参数错误', + 'enableAiRewrite.in' => 'AI智能话术改写参数只能是0或1', + 'aiRewritePrompt.checkAiRewritePrompt' => '启用AI智能话术改写时,必须输入改写提示词', + 'aiRewritePrompt.max' => '改写提示词最多500个字符', + 'wechatFriends.requireIf' => '请选择推送好友', + 'wechatFriends.checkFriendPushTarget' => '好友推送时必须选择推送好友', + 'wechatFriends.array' => '推送好友格式错误', + 'deviceGroups.requireIf' => '请选择设备', + 'deviceGroups.array' => '设备格式错误', + 'ownerWechatId.checkFriendPushService' => '好友推送需选择客服或提供好友/流量池', + // 自动建群相关提示 + 'groupNameTemplate.requireIf' => '请设置群名称前缀', + 'groupNameTemplate.max' => '群名称前缀最多50个字符', + 'maxGroupsPerDay.requireIf' => '请设置最大建群数量', + 'maxGroupsPerDay.number' => '最大建群数量必须为数字', + 'maxGroupsPerDay.min' => '最大建群数量必须大于0', + 'groupSizeMin.requireIf' => '请设置每个群的人数', + 'groupSizeMin.number' => '每个群的人数必须为数字', + 'groupSizeMin.min' => '每个群的人数必须大于0', + 'groupSizeMin.max' => '每个群的人数最大50人', + 'groupSizeMax.requireIf' => '请设置每个群的人数', + 'groupSizeMax.number' => '每个群的人数必须为数字', + 'groupSizeMax.min' => '每个群的人数必须大于0', + 'groupSizeMax.max' => '每个群的人数最大50人', + // 流量分发相关提示 + 'distributeType.requireIf' => '请选择流量分发类型', + 'distributeType.in' => '流量分发类型错误', + 'maxPerDay.requireIf' => '请设置每日最大流量', + 'maxPerDay.number' => '每日最大流量必须为数字', + 'maxPerDay.min' => '每日最大流量必须大于0', + 'timeType.requireIf' => '请选择时间类型', + + // 通用提示 + 'deviceGroups.require' => '请选择设备', + 'deviceGroups.array' => '设备格式错误', + 'targetGroups.require' => '请选择目标用户组', + 'targetGroups.array' => '目标用户组格式错误', + 'accountGroups.requireIf' => '流量分发时必须选择分发账号', + 'accountGroups.array' => '分发账号格式错误', + 'accountGroups.min' => '至少选择一个分发账号', + 'trafficPools.checkFriendPushPools' => '好友推送时请选择好友或流量池', + ]; + + /** + * 验证场景 + */ + protected $scene = [ + 'create' => ['name', 'type', 'autoStart', 'deviceGroups', 'targetGroups', + 'interval', 'maxLikes', 'startTime', 'endTime', 'contentTypes', + 'syncCount', 'syncType', 'accountGroups', + 'pushType', 'targetType', 'groupPushSubType', 'startTime', 'endTime', 'maxPerDay', 'pushOrder', 'isLoop', 'status', 'wechatGroups', 'wechatFriends', 'trafficPools', 'ownerWechatId', 'contentGroups', + 'announcementContent', 'enableAiRewrite', 'aiRewritePrompt', + 'groupNameTemplate', 'maxGroupsPerDay', 'groupSizeMin', 'groupSizeMax', + 'distributeType', 'timeType', 'accountGroups', + ], + 'update_status' => ['id', 'status'], + 'update' => ['name', 'type', 'autoStart', 'deviceGroups', 'targetGroups', + 'interval', 'maxLikes', 'startTime', 'endTime', 'contentTypes', + 'syncCount', 'syncType', 'accountGroups', + 'pushType', 'targetType', 'groupPushSubType', 'startTime', 'endTime', 'maxPerDay', 'pushOrder', 'isLoop', 'status', 'wechatGroups', 'wechatFriends', 'trafficPools', 'ownerWechatId', 'contentGroups', + 'announcementContent', 'enableAiRewrite', 'aiRewritePrompt', + 'groupNameTemplate', 'maxGroupsPerDay', 'groupSizeMin', 'groupSizeMax', + 'distributeType', 'timeType', 'accountGroups', + ] + ]; + + /** + * 自定义验证规则 + */ + protected function contentTypeEnum($value, $rule, $data) + { + $allowTypes = explode(',', $rule); + foreach ($value as $type) { + if (!in_array($type, $allowTypes)) { + return false; + } + } + return true; + } + + /** + * 验证群推送目标(当targetType=1时,wechatGroups必填) + */ + protected function checkGroupPushTarget($value, $rule, $data) + { + // 如果是群消息推送类型 + if (isset($data['type']) && $data['type'] == self::TYPE_GROUP_PUSH) { + // 如果targetType=1(群推送),则wechatGroups必填 + $targetType = isset($data['targetType']) ? intval($data['targetType']) : 1; // 默认1 + if ($targetType == 1) { + // 检查值是否存在且有效 + if (!isset($value) || $value === null || $value === '') { + return false; + } + if (!is_array($value) || count($value) < 1) { + return false; + } + } + } + return true; + } + + /** + * 验证好友推送目标(当targetType=2时,wechatFriends可选,可以为空) + */ + protected function checkFriendPushTarget($value, $rule, $data) + { + // 如果是群消息推送类型 + if (isset($data['type']) && $data['type'] == self::TYPE_GROUP_PUSH) { + // 如果targetType=2(好友推送),wechatFriends可以为空数组 + $targetType = isset($data['targetType']) ? intval($data['targetType']) : 1; // 默认1 + if ($targetType == 2) { + // 如果提供了值,则必须是数组 + if (isset($value) && $value !== null && $value !== '') { + if (!is_array($value)) { + return false; + } + } + } + } + return true; + } + + /** + * 验证好友推送时设备必填(当targetType=2时,deviceGroups必填) + */ + protected function checkFriendPushService($value, $rule, $data) + { + if (isset($data['type']) && $data['type'] == self::TYPE_GROUP_PUSH) { + $targetType = isset($data['targetType']) ? intval($data['targetType']) : 1; // 默认1 + if ($targetType == 2) { + if ($value !== null && $value !== '' && !is_array($value)) { + return false; + } + + $hasFriends = isset($data['wechatFriends']) && is_array($data['wechatFriends']) && count($data['wechatFriends']) > 0; + $hasPools = isset($data['trafficPools']) && is_array($data['trafficPools']) && count($data['trafficPools']) > 0; + $hasServices = is_array($value) && count(array_filter($value, function ($item) { + if (is_array($item)) { + return !empty($item['ownerWechatId'] ?? $item['wechatId'] ?? $item['id']); + } + return $item !== null && $item !== ''; + })) > 0; + + if (!$hasFriends && !$hasPools && !$hasServices) { + return false; + } + } + } + return true; + } + + /** + * 验证好友推送时是否选择好友或流量池(至少其一) + */ + protected function checkFriendPushPools($value, $rule, $data) + { + if (isset($data['type']) && $data['type'] == self::TYPE_GROUP_PUSH) { + $targetType = isset($data['targetType']) ? intval($data['targetType']) : 1; // 默认1 + if ($targetType == 2) { + $hasFriends = isset($data['wechatFriends']) && !empty($data['wechatFriends']); + $hasPools = isset($value) && $value !== null && $value !== '' && is_array($value) && count($value) > 0; + if (!$hasFriends && !$hasPools) { + return false; + } + if (isset($value) && $value !== null && $value !== '') { + if (!is_array($value)) { + return false; + } + } + } + } + return true; + } + + /** + * 验证群推送子类型(当targetType=1时,groupPushSubType必填且只能是1或2) + */ + protected function checkGroupPushSubType($value, $rule, $data) + { + // 如果是群消息推送类型 + if (isset($data['type']) && $data['type'] == self::TYPE_GROUP_PUSH) { + // 如果targetType=1(群推送),则groupPushSubType必填 + $targetType = isset($data['targetType']) ? intval($data['targetType']) : 1; // 默认1 + if ($targetType == 1) { + // 检查值是否存在且有效 + if (!isset($value) || !in_array(intval($value), [1, 2])) { + return false; + } + } + } + return true; + } + + /** + * 验证群公告内容(当groupPushSubType=2时,announcementContent必填) + */ + protected function checkAnnouncementContent($value, $rule, $data) + { + // 如果是群消息推送类型 + if (isset($data['type']) && $data['type'] == self::TYPE_GROUP_PUSH) { + // 如果targetType=1且groupPushSubType=2(群公告),则announcementContent必填 + $targetType = isset($data['targetType']) ? intval($data['targetType']) : 1; // 默认1 + $groupPushSubType = isset($data['groupPushSubType']) ? intval($data['groupPushSubType']) : 1; // 默认1 + if ($targetType == 1 && $groupPushSubType == 2) { + // 检查值是否存在且有效 + if (!isset($value) || $value === null || trim($value) === '') { + return false; + } + } + } + return true; + } + + /** + * 验证AI智能话术改写(当enableAiRewrite=1时,aiRewritePrompt必填) + */ + protected function checkEnableAiRewrite($value, $rule, $data) + { + // 如果是群消息推送类型且是群公告 + if (isset($data['type']) && $data['type'] == self::TYPE_GROUP_PUSH) { + $targetType = isset($data['targetType']) ? intval($data['targetType']) : 1; // 默认1 + $groupPushSubType = isset($data['groupPushSubType']) ? intval($data['groupPushSubType']) : 1; // 默认1 + if ($targetType == 1 && $groupPushSubType == 2) { + // 检查值是否存在且有效 + if (!isset($value) || !in_array(intval($value), [0, 1])) { + return false; + } + } + } + return true; + } + + /** + * 验证AI改写提示词(当enableAiRewrite=1时,aiRewritePrompt必填) + */ + protected function checkAiRewritePrompt($value, $rule, $data) + { + // 如果是群消息推送类型且是群公告 + if (isset($data['type']) && $data['type'] == self::TYPE_GROUP_PUSH) { + $targetType = isset($data['targetType']) ? intval($data['targetType']) : 1; // 默认1 + $groupPushSubType = isset($data['groupPushSubType']) ? intval($data['groupPushSubType']) : 1; // 默认1 + $enableAiRewrite = isset($data['enableAiRewrite']) ? intval($data['enableAiRewrite']) : 0; // 默认0 + if ($targetType == 1 && $groupPushSubType == 2 && $enableAiRewrite == 1) { + // 如果启用AI改写,提示词必填 + if (!isset($value) || $value === null || trim($value) === '') { + return false; + } + } + } + return true; + } +} \ No newline at end of file diff --git a/application/http/middleware/Jwt.php b/application/http/middleware/Jwt.php new file mode 100644 index 0000000..4f2fb7d --- /dev/null +++ b/application/http/middleware/Jwt.php @@ -0,0 +1,49 @@ + 401, + 'msg' => '未授权访问,缺少有效的身份凭证', + 'data' => null + ])->header(['Content-Type' => 'application/json; charset=utf-8']); + } + + $payload = JwtUtil::verifyToken($token); + if (!$payload) { + return json([ + 'code' => 401, + 'msg' => '授权已过期或无效', + 'data' => null + ])->header(['Content-Type' => 'application/json; charset=utf-8']); + } + + // 将用户信息附加到请求中 + $request->userInfo = $payload; + + // 写入日志 + Log::info('JWT认证通过', ['user_id' => $payload['id'] ?? 0, 'username' => $payload['username'] ?? '']); + + return $next($request); + } +} diff --git a/application/job/AccountListJob.php b/application/job/AccountListJob.php new file mode 100644 index 0000000..6d76ab3 --- /dev/null +++ b/application/job/AccountListJob.php @@ -0,0 +1,115 @@ +processAccountList($data, $job->attempts())) { + $job->delete(); + Log::info('公司账号列表任务执行成功,页码:' . $data['pageIndex']); + } else { + if ($job->attempts() > 3) { + // 超过重试次数,删除任务 + Log::error('公司账号列表任务执行失败,已超过重试次数,页码:' . $data['pageIndex']); + $job->delete(); + } else { + // 任务失败,重新放回队列 + Log::warning('公司账号列表任务执行失败,重试次数:' . $job->attempts() . ',页码:' . $data['pageIndex']); + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } catch (\Exception $e) { + // 出现异常,记录日志 + Log::error('公司账号列表任务异常:' . $e->getMessage()); + if ($job->attempts() > 3) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } + + /** + * 处理公司账号列表获取 + * @param array $data 任务数据 + * @param int $attempts 重试次数 + * @return bool + */ + protected function processAccountList($data, $attempts) + { + // 获取参数 + $pageIndex = isset($data['pageIndex']) ? $data['pageIndex'] : 0; + $pageSize = isset($data['pageSize']) ? $data['pageSize'] : 100; + + Log::info('开始获取公司账号列表,页码:' . $pageIndex . ',页大小:' . $pageSize); + + // 实例化控制器 + $accountController = new AccountController(); + + // 构建请求参数 + $params = [ + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize + ]; + + // 设置请求信息 + $request = request(); + $request->withGet($params); + + // 调用公司账号列表获取方法 + $result = $accountController->getlist(['pageIndex' => $pageIndex,'pageSize' => $pageSize],true); + $response = json_decode($result,true); + + + // 判断是否成功 + if ($response['code'] == 200) { + $data = $response['data']; + + // 判断是否有下一页 + if (!empty($data) && count($data['results']) > 0) { + // 有下一页,将下一页任务添加到队列 + $nextPageIndex = $pageIndex + 1; + $this->addNextPageToQueue($nextPageIndex, $pageSize); + Log::info('添加下一页任务到队列,页码:' . $nextPageIndex); + } + + return true; + } else { + $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误'; + Log::error('获取公司账号列表失败:' . $errorMsg); + return false; + } + } + + /** + * 添加下一页任务到队列 + * @param int $pageIndex 页码 + * @param int $pageSize 每页大小 + */ + protected function addNextPageToQueue($pageIndex, $pageSize) + { + $data = [ + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize + ]; + + // 添加到队列,设置任务名为 account_list + Queue::push(self::class, $data, 'account_list'); + } +} \ No newline at end of file diff --git a/application/job/AllotChatroomJob.php b/application/job/AllotChatroomJob.php new file mode 100644 index 0000000..96c01b1 --- /dev/null +++ b/application/job/AllotChatroomJob.php @@ -0,0 +1,96 @@ + $toAccountId, + 'wechatAccountKeyword' => $wechatAccountKeyword, + 'isDeleted' => $isDeleted, + 'jobId' => $jobId, + 'queueLockKey' => $queueLockKey + ])); + + // 如果没有提供队列锁键,生成一个 + if (empty($queueLockKey)) { + $queueLockKey = "queue_lock:allot_chatroom:{$wechatAccountKeyword}"; + } + + // 实例化控制器 + $automaticAssignController = new AutomaticAssign(); + + // 调用微信群聊自动分配方法 + $result = $automaticAssignController->autoAllotWechatChatroom($toAccountId, $wechatAccountKeyword, $isDeleted); + $response = json_decode($result, true); + + // 判断是否成功 + if ($response['code'] == 1) { + Log::info("微信群聊自动分配成功: toAccountId={$toAccountId}, wechatAccountKeyword={$wechatAccountKeyword}"); + + // 释放队列锁 + Cache::rm($queueLockKey); + Log::info("任务完成,释放队列锁: {$queueLockKey}"); + + $job->delete(); + return true; + } else { + // API调用出错,记录错误 + $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误'; + Log::error("微信群聊自动分配失败: {$errorMsg}"); + + if ($job->attempts() > 3) { + // 超过重试次数,删除任务并释放队列锁 + Cache::rm($queueLockKey); + Log::info("由于错误多次尝试失败,释放队列锁: {$queueLockKey}"); + $job->delete(); + } else { + // 任务失败,重新放回队列 + Log::warning("微信群聊自动分配任务执行失败,重试次数: " . $job->attempts()); + $job->release(10); // 延迟10秒后重试 + } + + return false; + } + } catch (\Exception $e) { + // 出现异常,记录错误并释放队列锁 + Log::error('微信群聊自动分配任务异常: ' . $e->getMessage()); + + if (!empty($queueLockKey)) { + Cache::rm($queueLockKey); + Log::info("由于异常释放队列锁: {$queueLockKey}"); + } + + if ($job->attempts() > 3) { + $job->delete(); + } else { + $job->release(10); + } + + return false; + } + } +} \ No newline at end of file diff --git a/application/job/AllotFriendJob.php b/application/job/AllotFriendJob.php new file mode 100644 index 0000000..baeeb6e --- /dev/null +++ b/application/job/AllotFriendJob.php @@ -0,0 +1,96 @@ + $toAccountId, + 'wechatAccountKeyword' => $wechatAccountKeyword, + 'isDeleted' => $isDeleted, + 'jobId' => $jobId, + 'queueLockKey' => $queueLockKey + ])); + + // 如果没有提供队列锁键,生成一个 + if (empty($queueLockKey)) { + $queueLockKey = "queue_lock:allot_friends:{$wechatAccountKeyword}"; + } + + // 实例化控制器 + $automaticAssignController = new AutomaticAssign(); + + // 调用微信好友自动分配方法 + $result = $automaticAssignController->autoAllotWechatFriend($toAccountId, $wechatAccountKeyword, $isDeleted); + $response = json_decode($result, true); + + // 判断是否成功 + if ($response['code'] == 1) { + Log::info("微信好友自动分配成功: toAccountId={$toAccountId}, wechatAccountKeyword={$wechatAccountKeyword}"); + + // 释放队列锁 + Cache::rm($queueLockKey); + Log::info("任务完成,释放队列锁: {$queueLockKey}"); + + $job->delete(); + return true; + } else { + // API调用出错,记录错误 + $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误'; + Log::error("微信好友自动分配失败: {$errorMsg}"); + + if ($job->attempts() > 3) { + // 超过重试次数,删除任务并释放队列锁 + Cache::rm($queueLockKey); + Log::info("由于错误多次尝试失败,释放队列锁: {$queueLockKey}"); + $job->delete(); + } else { + // 任务失败,重新放回队列 + Log::warning("微信好友自动分配任务执行失败,重试次数: " . $job->attempts()); + $job->release(10); // 延迟10秒后重试 + } + + return false; + } + } catch (\Exception $e) { + // 出现异常,记录错误并释放队列锁 + Log::error('微信好友自动分配任务异常: ' . $e->getMessage()); + + if (!empty($queueLockKey)) { + Cache::rm($queueLockKey); + Log::info("由于异常释放队列锁: {$queueLockKey}"); + } + + if ($job->attempts() > 3) { + $job->delete(); + } else { + $job->release(10); + } + + return false; + } + } +} \ No newline at end of file diff --git a/application/job/AllotRuleListJob.php b/application/job/AllotRuleListJob.php new file mode 100644 index 0000000..52a6437 --- /dev/null +++ b/application/job/AllotRuleListJob.php @@ -0,0 +1,75 @@ +processAllotRuleList($data, $job->attempts())) { + $job->delete(); + Log::info('分配规则列表任务执行成功'); + } else { + if ($job->attempts() > 3) { + // 超过重试次数,删除任务 + Log::error('分配规则列表任务执行失败,已超过重试次数'); + $job->delete(); + } else { + // 任务失败,重新放回队列 + Log::warning('分配规则列表任务执行失败,重试次数:' . $job->attempts()); + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } catch (\Exception $e) { + // 出现异常,记录日志 + Log::error('分配规则列表任务异常:' . $e->getMessage()); + if ($job->attempts() > 3) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } + + /** + * 处理分配规则列表获取 + * @param array $data 任务数据 + * @param int $attempts 重试次数 + * @return bool + */ + protected function processAllotRuleList($data, $attempts) + { + Log::info('开始获取分配规则列表'); + + // 实例化控制器 + $allotRuleController = new AllotRuleController(); + + // 调用分配规则列表获取方法 + $result = $allotRuleController->getAllRules([], true); + $response = json_decode($result, true); + + // 判断是否成功 + if ($response['code'] == 200) { + Log::info('获取分配规则列表成功,共获取 ' . (isset($response['data']) ? count($response['data']) : 0) . ' 条记录'); + return true; + } else { + $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误'; + Log::error('获取分配规则列表失败:' . $errorMsg); + return false; + } + } +} \ No newline at end of file diff --git a/application/job/AutoCreateAllotRulesJob.php b/application/job/AutoCreateAllotRulesJob.php new file mode 100644 index 0000000..5129eba --- /dev/null +++ b/application/job/AutoCreateAllotRulesJob.php @@ -0,0 +1,75 @@ +processAutoCreateAllotRules($data, $job->attempts())) { + $job->delete(); + Log::info('自动创建分配规则任务执行成功,时间:' . date('Y-m-d H:i:s')); + } else { + if ($job->attempts() > 3) { + // 超过重试次数,删除任务 + Log::error('自动创建分配规则任务执行失败,已超过重试次数'); + $job->delete(); + } else { + // 任务失败,重新放回队列 + Log::warning('自动创建分配规则任务执行失败,重试次数:' . $job->attempts()); + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } catch (\Exception $e) { + // 出现异常,记录日志 + Log::error('自动创建分配规则任务异常:' . $e->getMessage()); + if ($job->attempts() > 3) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } + + /** + * 处理自动创建分配规则 + * @param array $data 任务数据 + * @param int $attempts 重试次数 + * @return bool + */ + protected function processAutoCreateAllotRules($data, $attempts) + { + Log::info('开始执行自动创建分配规则任务,重试次数:' . $attempts); + + // 实例化控制器 + $allotRuleController = new AllotRuleController(); + + // 调用自动创建分配规则方法 + $result = $allotRuleController->autoCreateAllotRules([], true); + $response = json_decode($result, true); + + // 判断是否成功 + if (isset($response['code']) && $response['code'] == 200) { + Log::info('自动创建分配规则成功:' . json_encode($response['data'])); + return true; + } else { + $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误'; + Log::error('自动创建分配规则失败:' . $errorMsg); + return false; + } + } +} \ No newline at end of file diff --git a/application/job/CallRecordingListJob.php b/application/job/CallRecordingListJob.php new file mode 100644 index 0000000..3ec8d6c --- /dev/null +++ b/application/job/CallRecordingListJob.php @@ -0,0 +1,118 @@ +processCallRecordingList($data, $job->attempts())) { + $job->delete(); + Log::info('通话记录列表任务执行成功,页码:' . $data['pageIndex']); + } else { + if ($job->attempts() > 3) { + // 超过重试次数,删除任务 + Log::error('通话记录列表任务执行失败,已超过重试次数,页码:' . $data['pageIndex']); + $job->delete(); + } else { + // 任务失败,重新放回队列 + Log::warning('通话记录列表任务执行失败,重试次数:' . $job->attempts() . ',页码:' . $data['pageIndex']); + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } catch (\Exception $e) { + // 出现异常,记录日志 + Log::error('通话记录列表任务异常:' . $e->getMessage()); + if ($job->attempts() > 3) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } + + /** + * 处理通话记录列表获取 + * @param array $data 任务数据 + * @param int $attempts 重试次数 + * @return bool + */ + protected function processCallRecordingList($data, $attempts) + { + // 获取参数 + $pageIndex = isset($data['pageIndex']) ? $data['pageIndex'] : 0; + $pageSize = isset($data['pageSize']) ? $data['pageSize'] : 100; + + Log::info('开始获取通话记录列表,页码:' . $pageIndex . ',页大小:' . $pageSize); + + // 实例化控制器 + $callRecordingController = new CallRecordingController(); + + // 构建请求参数 + $params = [ + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize, + 'keyword' => '', + 'isCallOut' => '', + 'secondMin' => 0, + 'secondMax' => 99999, + 'departmentIds' => '', + 'from' => date('Y-m-d 00:00:00', strtotime('-100 days')), + 'to' => date('Y-m-d 23:59:59'), + 'departmentId' => '' + ]; + + // 调用通话记录列表获取方法 + $result = $callRecordingController->getlist($params, true); + $response = json_decode($result, true); + + // 判断是否成功 + if ($response['code'] == 200) { + $data = $response['data']; + + // 判断是否有下一页 + if (!empty($data) && isset($data['results']) && count($data['results']) > 0) { + // 有下一页,将下一页任务添加到队列 + $nextPageIndex = $pageIndex + 1; + $this->addNextPageToQueue($nextPageIndex, $pageSize); + Log::info('添加下一页任务到队列,页码:' . $nextPageIndex); + } + + return true; + } else { + $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误'; + Log::error('获取通话记录列表失败:' . $errorMsg); + return false; + } + } + + /** + * 添加下一页任务到队列 + * @param int $pageIndex 页码 + * @param int $pageSize 每页大小 + */ + protected function addNextPageToQueue($pageIndex, $pageSize) + { + $data = [ + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize + ]; + + // 添加到队列,设置任务名为 call_recording_list + Queue::push(self::class, $data, 'call_recording_list'); + } +} diff --git a/application/job/ContentCollectJob.php b/application/job/ContentCollectJob.php new file mode 100644 index 0000000..fcdde95 --- /dev/null +++ b/application/job/ContentCollectJob.php @@ -0,0 +1,90 @@ +processContentCollect($data, $job->attempts())) { + $job->delete(); + // 去除成功日志,减少日志空间消耗 + } else { + if ($job->attempts() > 3) { + // 超过重试次数,删除任务 + Log::error('内容采集任务执行失败,已超过重试次数,内容库ID:' . $data['libraryId']); + $job->delete(); + } else { + // 任务失败,重新放回队列 + Log::warning('内容采集任务执行失败,重试次数:' . $job->attempts() . ',内容库ID:' . $data['libraryId']); + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } catch (\Exception $e) { + // 出现异常,记录日志 + Log::error('内容采集任务异常:' . $e->getMessage()); + if ($job->attempts() > 3) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } + + /** + * 处理内容采集 + * @param array $data 任务数据 + * @param int $attempts 重试次数 + * @return bool + */ + protected function processContentCollect($data, $attempts) + { + try { + $controller = new ContentLibraryController(); + $result = $controller->collectMoments(); + $response = json_decode($result, true); + + if ($response['code'] == 200) { + // 记录详细的采集结果 + if (!empty($response['data'])) { + foreach ($response['data'] as $result) { + if ($result['status'] == 'success') { + Log::info(sprintf( + '内容库[%s]采集成功: %s', + $result['library_name'], + $result['message'] + )); + } else { + Log::warning(sprintf( + '内容库[%s]采集失败: %s', + $result['library_name'], + $result['message'] + )); + } + } + } + return true; + } else { + Log::error('内容采集失败:' . ($response['msg'] ?? '未知错误')); + return false; + } + } catch (\Exception $e) { + Log::error('内容采集处理异常:' . $e->getMessage()); + return false; + } + } +} \ No newline at end of file diff --git a/application/job/DepartmentListJob.php b/application/job/DepartmentListJob.php new file mode 100644 index 0000000..852c121 --- /dev/null +++ b/application/job/DepartmentListJob.php @@ -0,0 +1,115 @@ +processDepartmentList($data, $job->attempts())) { + $job->delete(); + Log::info('部门列表任务执行成功,页码:' . $data['pageIndex']); + } else { + if ($job->attempts() > 3) { + // 超过重试次数,删除任务 + Log::error('部门列表任务执行失败,已超过重试次数,页码:' . $data['pageIndex']); + $job->delete(); + } else { + // 任务失败,重新放回队列 + Log::warning('部门列表任务执行失败,重试次数:' . $job->attempts() . ',页码:' . $data['pageIndex']); + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } catch (\Exception $e) { + // 出现异常,记录日志 + Log::error('部门列表任务异常:' . $e->getMessage()); + if ($job->attempts() > 3) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } + + /** + * 处理部门列表获取 + * @param array $data 任务数据 + * @param int $attempts 重试次数 + * @return bool + */ + protected function processDepartmentList($data, $attempts) + { + // 获取参数 + $pageIndex = isset($data['pageIndex']) ? $data['pageIndex'] : 0; + $pageSize = isset($data['pageSize']) ? $data['pageSize'] : 100; + + Log::info('开始获取部门列表,页码:' . $pageIndex . ',页大小:' . $pageSize); + + // 实例化控制器 + $accountController = new AccountController(); + + // 构建请求参数 + $params = [ + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize + ]; + + // 设置请求信息 + $request = request(); + $request->withGet($params); + + // 调用公司账号列表获取方法 + $result = $accountController->getDepartmentList(true); + $response = json_decode($result,true); + + + // 判断是否成功 + if ($response['code'] == 200) { + $data = $response['data']; + + // 判断是否有下一页 + if (!empty($data) && count($data['results']) > 0) { + // 有下一页,将下一页任务添加到队列 + $nextPageIndex = $pageIndex + 1; + $this->addNextPageToQueue($nextPageIndex, $pageSize); + Log::info('添加下一页任务到队列,页码:' . $nextPageIndex); + } + + return true; + } else { + $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误'; + Log::error('获取部门列表失败:' . $errorMsg); + return false; + } + } + + /** + * 添加下一页任务到队列 + * @param int $pageIndex 页码 + * @param int $pageSize 每页大小 + */ + protected function addNextPageToQueue($pageIndex, $pageSize) + { + $data = [ + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize + ]; + + // 添加到队列,设置任务名为 account_list + Queue::push(self::class, $data, 'department_list'); + } +} \ No newline at end of file diff --git a/application/job/DeviceListJob.php b/application/job/DeviceListJob.php new file mode 100644 index 0000000..1dd337b --- /dev/null +++ b/application/job/DeviceListJob.php @@ -0,0 +1,155 @@ + $pageIndex, + 'pageSize' => $pageSize, + 'isDel' => $isDel, + 'jobId' => $jobId, + 'cacheKey' => $cacheKey, + 'queueLockKey' => $queueLockKey + ])); + + // 如果没有提供缓存键,根据删除状态和任务ID生成一个 + if (empty($cacheKey)) { + $cacheKeyPrefix = "devicePage:" . ($jobId ?: date('YmdHis') . rand(1000, 9999)); + $cacheKeySuffix = $isDel === '' ? '' : ":{$isDel}"; + $cacheKey = $cacheKeyPrefix . $cacheKeySuffix; + } + + // 如果没有提供队列锁键,生成一个 + if (empty($queueLockKey)) { + $queueLockKey = "queue_lock:device_list:{$isDel}"; + } + + // 实例化控制器 + $deviceController = new DeviceController(); + + // 设置请求信息 + $request = request(); + $request->withGet([ + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize + ]); + + // 调用设备列表获取方法,传入isDel参数 + $result = $deviceController->getlist(['pageIndex' => $pageIndex, 'pageSize' => $pageSize], true, $isDel); + $response = json_decode($result, true); + + if ($response['code'] == 200) { + $data = $response['data']; + $dataCount = count($data['results']); + $totalCount = $data['total']; + + Log::info("设备列表获取成功,当前页:{$pageIndex},获取数量:{$dataCount},总数量:{$totalCount}"); + + // 计算是否还有下一页 + $hasNextPage = ($pageIndex + 1) * $pageSize < $totalCount; + + if ($hasNextPage) { + // 缓存页码信息,设置有效期1天 + $nextPageIndex = $pageIndex + 1; + Cache::set($cacheKey, $nextPageIndex, 600); + Log::info("更新缓存页码: {$nextPageIndex}, 缓存键: {$cacheKey}"); + + // 添加下一页任务到队列 + $command = new DeviceListCommand(); + $command->addToQueue($nextPageIndex, $pageSize, $isDel, $jobId, $cacheKey, $queueLockKey); + Log::info("已添加下一页任务到队列: 页码 {$nextPageIndex}"); + } else { + // 处理完所有页面,重置页码并释放队列锁 + Cache::set($cacheKey, 0, 600); + Cache::rm($queueLockKey); + Log::info("所有设备列表页面处理完毕,重置页码为0,释放队列锁: {$queueLockKey}"); + } + + $job->delete(); + return true; + } else { + // API调用出错,记录错误并释放队列锁 + $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误'; + Log::error("设备列表获取失败: " . $errorMsg); + + if ($job->attempts() > 3) { + // 超过重试次数,删除任务并释放队列锁 + Cache::rm($queueLockKey); + Log::info("由于错误释放队列锁: {$queueLockKey}"); + $job->delete(); + } else { + // 任务失败,重新放回队列 + Log::warning('设备列表任务执行失败,重试次数:' . $job->attempts() . ',页码:' . $pageIndex); + $job->release(Config::get('queue.failed_delay', 10)); + } + + return false; + } + } catch (\Exception $e) { + // 出现异常,记录错误并释放队列锁 + Log::error('设备列表任务处理异常: ' . $e->getMessage()); + + if (!empty($queueLockKey)) { + Cache::rm($queueLockKey); + Log::info("由于异常释放队列锁: {$queueLockKey}"); + } + + if ($job->attempts() > 3) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + + return false; + } + } + + /** + * 获取删除状态的文本描述 + * @param string $isDel 删除状态 + * @return string 删除状态文本 + */ + protected function getDeleteTypeText($isDel) + { + switch ($isDel) { + case '0': + case 0: + return '未删除(unDeleted)'; + case '1': + case 1: + return '已删除(deleted)'; + case '2': + case 2: + return '已停用(deletedAndStop)'; + default: + return '全部'; + } + } +} \ No newline at end of file diff --git a/application/job/FriendTaskJob.php b/application/job/FriendTaskJob.php new file mode 100644 index 0000000..03ec65b --- /dev/null +++ b/application/job/FriendTaskJob.php @@ -0,0 +1,123 @@ +processFriendTask($data, $job->attempts())) { + $job->delete(); + Log::info('添加好友任务执行成功,页码:' . $data['pageIndex']); + } else { + if ($job->attempts() > 3) { + // 超过重试次数,删除任务 + Log::error('添加好友任务执行失败,已超过重试次数,页码:' . $data['pageIndex']); + $job->delete(); + } else { + // 任务失败,重新放回队列 + Log::warning('添加好友任务执行失败,重试次数:' . $job->attempts() . ',页码:' . $data['pageIndex']); + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } catch (\Exception $e) { + // 出现异常,记录日志 + Log::error('添加好友任务异常:' . $e->getMessage()); + if ($job->attempts() > 3) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } + + /** + * 处理添加好友任务获取 + * @param array $data 任务数据 + * @param int $attempts 重试次数 + * @return bool + */ + protected function processFriendTask($data, $attempts) + { + // 获取参数 + $pageIndex = isset($data['pageIndex']) ? $data['pageIndex'] : 0; + $pageSize = isset($data['pageSize']) ? $data['pageSize'] : 100; + + Log::info('开始获取添加好友任务,页码:' . $pageIndex . ',页大小:' . $pageSize); + + // 实例化控制器 + $friendTaskController = new FriendTaskController(); + + // 构建请求参数 + $params = [ + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize + ]; + + // 设置请求信息 + $request = request(); + $request->withGet($params); + + // 调用添加好友任务获取方法 + $result = $friendTaskController->getlist($pageIndex, $pageSize, true); + $response = json_decode($result, true); + + // 判断是否成功 + if ($response['code'] == 200) { + $data = $response['data']; + + // 判断是否有下一页 + if (!empty($data) && count($data['results']) > 0 && $pageIndex < 2) { + // 更新缓存中的页码,设置10分钟过期 + Cache::set('friendTaskPage', $pageIndex + 1, 600); + Log::info('更新缓存,下一页页码:' . ($pageIndex + 1) . ',缓存时间:10分钟'); + + // 有下一页,将下一页任务添加到队列 + $nextPageIndex = $pageIndex + 1; + $this->addNextPageToQueue($nextPageIndex, $pageSize); + Log::info('添加下一页任务到队列,页码:' . $nextPageIndex); + } else { + // 没有下一页,重置缓存,设置10分钟过期 + Cache::set('friendTaskPage', 0, 600); + Log::info('获取完成,重置缓存,缓存时间:10分钟'); + } + + return true; + } else { + $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误'; + Log::error('获取添加好友任务失败:' . $errorMsg); + return false; + } + } + + /** + * 添加下一页任务到队列 + * @param int $pageIndex 页码 + * @param int $pageSize 每页大小 + */ + protected function addNextPageToQueue($pageIndex, $pageSize) + { + $data = [ + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize + ]; + + // 添加到队列,设置任务名为 friend_task + Queue::push(self::class, $data, 'friend_task'); + } +} \ No newline at end of file diff --git a/application/job/GroupFriendsJob.php b/application/job/GroupFriendsJob.php new file mode 100644 index 0000000..54cf56e --- /dev/null +++ b/application/job/GroupFriendsJob.php @@ -0,0 +1,145 @@ +processGroupFriendsList($data, $job->attempts())) { + $job->delete(); + Log::info('微信群好友列表任务执行成功,页码:' . $data['pageIndex']); + } else { + if ($job->attempts() > 3) { + // 超过重试次数,删除任务 + Log::error('微信群好友列表任务执行失败,已超过重试次数,页码:' . $data['pageIndex']); + $job->delete(); + } else { + // 任务失败,重新放回队列 + Log::warning('微信群好友列表任务执行失败,重试次数:' . $job->attempts() . ',页码:' . $data['pageIndex']); + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } catch (\Exception $e) { + // 出现异常,记录日志 + Log::error('微信群好友列表任务异常:' . $e->getMessage()); + if ($job->attempts() > 3) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } + + /** + * 处理微信群好友列表获取 + * @param array $data 任务数据 + * @param int $attempts 重试次数 + * @return bool + */ + protected function processGroupFriendsList($data, $attempts) + { + // 获取参数 + $pageIndex = isset($data['pageIndex']) ? $data['pageIndex'] : 0; + $pageSize = isset($data['pageSize']) ? $data['pageSize'] : 100; + + Log::info('开始获取微信群好友列表,页码:' . $pageIndex . ',页大小:' . $pageSize); + + try { + // 从数据库获取未删除的群聊列表 + $chatrooms = WechatChatroomModel::where('isDeleted', 0) + ->page($pageIndex + 1, $pageSize) + ->order('id', 'desc') + ->select(); + + if (empty($chatrooms)) { + Log::info('未找到需要处理的群聊数据,页码:' . $pageIndex); + return true; + } + + + // 实例化控制器 + $wechatChatroomController = new WechatChatroomController(); + + // 遍历每个群聊,获取其成员 + foreach ($chatrooms as $chatroom) { + try { + // 调用获取群成员列表方法 + $result = $wechatChatroomController->listChatroomMember($chatroom['id'],$chatroom['chatroomId'],true); + $response = is_string($result) ? json_decode($result, true) : $result; + + // 判断是否成功 + if (is_array($response) && isset($response['code']) && $response['code'] == 200) { + //Log::info('成功获取群 ' . $chatroom['chatroomId'] . ' 的成员列表'); + } else { + $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误'; + Log::error('获取群 ' . $chatroom['chatroomId'] . ' 的成员列表失败:' . $errorMsg); + } + } catch (\Exception $e) { + Log::error('获取群 ' . $chatroom['chatroomId'] . ' 的成员列表异常:' . $e->getMessage()); + } + + } + + //Log::info('群成员获取完成,成功:' . $successCount . ',失败:' . $failCount); + + // 计算总数量 + $totalCount = WechatChatroomModel::where('isDeleted', 0)->count(); + $processedCount = ($pageIndex + 1) * $pageSize; + + // 判断是否有下一页 + if ($processedCount < $totalCount) { + // 更新缓存中的页码,设置一天过期 + Cache::set('groupFriendsPage', $pageIndex + 1, 600); + //Log::info('更新缓存,下一页页码:' . ($pageIndex + 1) . ',缓存时间:1天'); + + // 有下一页,将下一页任务添加到队列 + $nextPageIndex = $pageIndex + 1; + $this->addNextPageToQueue($nextPageIndex, $pageSize); + Log::info('添加下一页任务到队列,页码:' . $nextPageIndex); + } else { + // 没有下一页,重置缓存,设置一天过期 + Cache::set('groupFriendsPage', 0, 600); + Log::info('获取完成,重置缓存,缓存时间:1天'); + } + + return true; + } catch (\Exception $e) { + Log::error('获取微信群好友列表处理失败:' . $e->getMessage()); + return false; + } + } + + /** + * 添加下一页任务到队列 + * @param int $pageIndex 页码 + * @param int $pageSize 每页大小 + */ + protected function addNextPageToQueue($pageIndex, $pageSize) + { + $data = [ + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize + ]; + + // 添加到队列,设置任务名为 group_friends + Queue::push(self::class, $data, 'group_friends'); + } +} \ No newline at end of file diff --git a/application/job/MessageChatroomListJob.php b/application/job/MessageChatroomListJob.php new file mode 100644 index 0000000..e3db929 --- /dev/null +++ b/application/job/MessageChatroomListJob.php @@ -0,0 +1,115 @@ +processMessageChatroomList($data, $job->attempts())) { + $job->delete(); + Log::info('微信群聊消息列表任务执行成功,页码:' . $data['pageIndex']); + } else { + if ($job->attempts() > 3) { + // 超过重试次数,删除任务 + Log::error('微信群聊消息列表任务执行失败,已超过重试次数,页码:' . $data['pageIndex']); + $job->delete(); + } else { + // 任务失败,重新放回队列 + Log::warning('微信群聊消息列表任务执行失败,重试次数:' . $job->attempts() . ',页码:' . $data['pageIndex']); + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } catch (\Exception $e) { + // 出现异常,记录日志 + Log::error('微信群聊消息列表任务异常:' . $e->getMessage()); + if ($job->attempts() > 3) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } + + /** + * 处理微信群聊消息列表获取 + * @param array $data 任务数据 + * @param int $attempts 重试次数 + * @return bool + */ + protected function processMessageChatroomList($data, $attempts) + { + // 获取参数 + $pageIndex = isset($data['pageIndex']) ? $data['pageIndex'] : 0; + $pageSize = isset($data['pageSize']) ? $data['pageSize'] : 100; + + Log::info('开始获取微信群聊消息列表,页码:' . $pageIndex . ',页大小:' . $pageSize); + + // 实例化控制器 + $messageController = new MessageController(); + + // 构建请求参数 + $params = [ + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize + ]; + + // 设置请求信息 + $request = request(); + $request->withGet($params); + + // 调用添加好友任务获取方法 + $result = $messageController->getChatroomList($pageIndex,$pageSize,true); + $response = json_decode($result,true); + + + // 判断是否成功 + if ($response['code'] == 200) { + $data = $response['data']; + + // 判断是否有下一页 + if (!empty($data) && count($data['results']) > 0) { + // 有下一页,将下一页任务添加到队列 + $nextPageIndex = $pageIndex + 1; + $this->addNextPageToQueue($nextPageIndex, $pageSize); + Log::info('添加下一页任务到队列,页码:' . $nextPageIndex); + } + + return true; + } else { + $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误'; + Log::error('获取微信群聊消息列表失败:' . $errorMsg); + return false; + } + } + + /** + * 添加下一页任务到队列 + * @param int $pageIndex 页码 + * @param int $pageSize 每页大小 + */ + protected function addNextPageToQueue($pageIndex, $pageSize) + { + $data = [ + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize + ]; + + // 添加到队列,设置任务名为 message_chatroom_list + Queue::push(self::class, $data, 'message_chatroom_list'); + } +} \ No newline at end of file diff --git a/application/job/MessageFriendsListJob.php b/application/job/MessageFriendsListJob.php new file mode 100644 index 0000000..ee7c309 --- /dev/null +++ b/application/job/MessageFriendsListJob.php @@ -0,0 +1,117 @@ +processMessageFriendsList($data, $job->attempts())) { + $job->delete(); + // 去除成功日志,减少日志空间消耗 + } else { + if ($job->attempts() > 3) { + // 超过重试次数,删除任务 + Log::error('好友消息列表任务执行失败,已超过重试次数,页码:' . $data['pageIndex']); + $job->delete(); + } else { + // 任务失败,重新放回队列 + Log::warning('好友消息列表任务执行失败,重试次数:' . $job->attempts() . ',页码:' . $data['pageIndex']); + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } catch (\Exception $e) { + // 出现异常,记录日志 + Log::error('好友消息列表任务异常:' . $e->getMessage()); + if ($job->attempts() > 3) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } + + /** + * 处理好友消息列表获取 + * @param array $data 任务数据 + * @param int $attempts 重试次数 + * @return bool + */ + protected function processMessageFriendsList($data, $attempts) + { + // 获取参数 + $pageIndex = isset($data['pageIndex']) ? $data['pageIndex'] : 0; + $pageSize = isset($data['pageSize']) ? $data['pageSize'] : 100; + + // 去除开始日志,减少日志空间消耗 + + // 实例化控制器 + $messageController = new MessageController(); + + // 构建请求参数 + $params = [ + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize + ]; + + // 设置请求信息 + $request = request(); + $request->withGet($params); + + + + // 调用添加好友任务获取方法 + $result = $messageController->getFriendsList($pageIndex,$pageSize,true); + $response = json_decode($result,true); + + + // 判断是否成功 + if ($response['code'] == 200) { + $data = $response['data']; + + // 判断是否有下一页 + if (!empty($data) && count($data) > 0) { + // 有下一页,将下一页任务添加到队列 + $nextPageIndex = $pageIndex + 1; + $this->addNextPageToQueue($nextPageIndex, $pageSize); + Log::info('添加下一页任务到队列,页码:' . $nextPageIndex); + } + + return true; + } else { + $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误'; + Log::error('获取好友消息列表失败:' . $errorMsg); + return false; + } + } + + /** + * 添加下一页任务到队列 + * @param int $pageIndex 页码 + * @param int $pageSize 每页大小 + */ + protected function addNextPageToQueue($pageIndex, $pageSize) + { + $data = [ + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize + ]; + + // 添加到队列,设置任务名为 message_friends_list + Queue::push(self::class, $data, 'message_friends_list'); + } +} \ No newline at end of file diff --git a/application/job/OwnMomentsCollectJob.php b/application/job/OwnMomentsCollectJob.php new file mode 100644 index 0000000..9b8987c --- /dev/null +++ b/application/job/OwnMomentsCollectJob.php @@ -0,0 +1,152 @@ +processOwnMomentsCollect($data, $job->attempts())) { + $job->delete(); + } else { + if ($job->attempts() > 3) { + // 超过重试次数,删除任务 + Log::error('自己朋友圈采集任务执行失败,已超过重试次数'); + $job->delete(); + } else { + // 任务失败,重新放回队列 + Log::warning('自己朋友圈采集任务执行失败,重试次数:' . $job->attempts()); + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } catch (\Exception $e) { + // 出现异常,记录日志 + Log::error('自己朋友圈采集任务异常:' . $e->getMessage()); + if ($job->attempts() > 3) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } + + /** + * 处理自己朋友圈采集 + * @param array $data 任务数据 + * @param int $attempts 重试次数 + * @return bool + */ + protected function processOwnMomentsCollect($data, $attempts) + { + try { + // 获取在线微信账号列表(只获取在线微信) + $onlineWechatAccounts = $this->getOnlineWechatAccounts(); + + if (empty($onlineWechatAccounts)) { + return true; + } + + // 获取API账号配置 + $username = Env::get('api.username2', ''); + $password = Env::get('api.password2', ''); + + if (empty($username) || empty($password)) { + Log::error('API账号配置缺失,无法执行朋友圈采集'); + return false; + } + + // 获取账号ID + $toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId'); + if (empty($toAccountId)) { + Log::error('未找到API账号对应的账号ID'); + return false; + } + + $successCount = 0; + $failCount = 0; + + // 遍历每个在线微信账号,采集自己的朋友圈 + foreach ($onlineWechatAccounts as $account) { + try { + $wechatAccountId = $account['id']; + $wechatId = $account['wechatId']; + + // 创建WebSocket控制器实例 + $webSocket = new WebSocketController([ + 'userName' => $username, + 'password' => $password, + 'accountId' => $toAccountId + ]); + + // 采集自己的朋友圈(wechatFriendId传0或空,表示采集自己的朋友圈) + $result = $webSocket->getMoments([ + 'wechatAccountId' => $wechatAccountId, + 'wechatFriendId' => 0, // 0表示采集自己的朋友圈 + 'count' => 10 // 每次采集10条 + ]); + + $resultData = json_decode($result, true); + if (!empty($resultData) && $resultData['code'] == 200) { + $successCount++; + } else { + $failCount++; + Log::warning("微信账号 {$wechatId} 朋友圈采集失败:" . ($resultData['msg'] ?? '未知错误')); + } + + // 避免请求过于频繁,每个账号之间稍作延迟 + usleep(500000); // 延迟0.5秒 + + } catch (\Exception $e) { + $failCount++; + Log::error("采集微信账号 {$account['wechatId']} 朋友圈异常:" . $e->getMessage()); + continue; + } + } + + Log::info("自己朋友圈采集任务完成,成功:{$successCount},失败:{$failCount}"); + return true; + + } catch (\Exception $e) { + Log::error('自己朋友圈采集处理异常:' . $e->getMessage()); + return false; + } + } + + /** + * 获取在线微信账号列表 + * @return array + */ + protected function getOnlineWechatAccounts() + { + try { + // 查询在线微信账号(deviceAlive=1 且 wechatAlive=1) + $accounts = Db::table('s2_wechat_account') + ->where('deviceAlive', 1) + ->where('wechatAlive', 1) + ->field('id, wechatId, nickname, alias') + ->select(); + + return $accounts ?: []; + } catch (\Exception $e) { + Log::error('获取在线微信账号列表失败:' . $e->getMessage()); + return []; + } + } +} + diff --git a/application/job/SyncAllFriendsJob.php b/application/job/SyncAllFriendsJob.php new file mode 100644 index 0000000..fc82bd5 --- /dev/null +++ b/application/job/SyncAllFriendsJob.php @@ -0,0 +1,70 @@ +getlist([ + 'wechatAccountKeyword' => $wechatId, + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize, + 'preFriendId' => $preFriendId + ], true); + + if (is_string($result)) { + $result = json_decode($result, true); + } + + if ($result['code'] == 200) { + $friends = $result['data']['list'] ?? $result['data']; + if (is_array($friends) && count($friends) == $pageSize) { + $lastFriendId = $friends[count($friends) - 1]['id']; + // 还有下一页,重新入队 + $nextPageIndex = $pageIndex + 1; + \think\Queue::push(self::class, [ + 'wechatId' => $wechatId, + 'pageIndex' => $nextPageIndex, + 'pageSize' => $pageSize, + 'preFriendId' => $lastFriendId, + 'jobId' => $jobId, + 'queueLockKey' => $queueLockKey + ], $job->getQueue()); + Log::info("微信id: {$wechatId} 下一页任务已入队,pageIndex: {$nextPageIndex},preFriendId: {$lastFriendId}"); + } + } + + $job->delete(); + Log::info('同步微信id: ' . $wechatId . ' 第' . $pageIndex . '页任务执行成功'); + // 释放锁逻辑可在所有账号所有分页都完成后处理 + return true; + } catch (\Exception $e) { + Log::error('同步所有好友任务异常:' . $e->getMessage()); + if (!empty($data['queueLockKey'])) { + \think\facade\Cache::rm($data['queueLockKey']); + } + if ($job->attempts() > 3) { + $job->delete(); + } else { + $job->release(\think\facade\Config::get('queue.failed_delay', 10)); + } + return false; + } + } +} \ No newline at end of file diff --git a/application/job/SyncContentJob.php b/application/job/SyncContentJob.php new file mode 100644 index 0000000..522368e --- /dev/null +++ b/application/job/SyncContentJob.php @@ -0,0 +1,66 @@ +where('ownerWechatId','wxid_h7nsh7vxseyn29')->select(); + foreach ($ddd as $v) { + $d = Db::table('ck_task_customer')->where('task_id','167')->where('phone',$v['wechatId'])->find(); + if (!empty($d) && !in_array($d['status'],[4,5])) { + Db::table('ck_task_customer')->where('id',$d['id'])->update(['status'=>5]); + } + } + + exit_data(111); + + return true; + /*$ddd= '[{"id":21909,"groupName":"私域运营招聘","sortIndex":240426546,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":8074,"groupName":"存客宝新客户介绍","sortIndex":230,"parentId":0,"replyType":1,"children":[{"id":8081,"groupName":"客户系统上线准备","sortIndex":1,"parentId":8074,"replyType":1,"children":[],"replys":null}],"replys":null},{"id":10441,"groupName":"BOSS直聘","sortIndex":229,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":15111,"groupName":"点了码新客户了解","sortIndex":228,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":12732,"groupName":"测试","sortIndex":219,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":8814,"groupName":"封单话术","sortIndex":176,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":8216,"groupName":"私域合伙人","sortIndex":172,"parentId":0,"replyType":1,"children":[],"replys":null},{"id":6476,"groupName":"新客户了解","sortIndex":119,"parentId":0,"replyType":1,"children":[],"replys":null}]'; + $ddd=json_decode($ddd,1);*/ + + $ddd = ReplyGroup::where('companyId',2778)->where('companyId','<>',21898)->where('isDel',0)->select()->toArray(); + + + $authorization = 'OE7kh6Dsw_0SqqH1FTAPCB2ewCQDhx7VvPw6PrsE_p9tcRKbtlFsZau8kjk2NQ829Yah90KhTh0C_35ek569uRQgM_gC0NtKzfRPDDoqMIUE5mI6AO_hm0dm-xDJqhAFYkXHCdXnJYzQZxWS5dleJCIwtQxgRuIzIbr-_G_5C-7DeLEOSt2vi1oGPleLt00QGQ1WYVYqoHYrbPGMghMQpWIbgk5qNcUCeANlLJ_s7QFC3QzArU95_YiK0HlhU81hZqr8kI_5lmdrRBoR-yNIlyhySLRCmEZYGzOxCiUHL3uFHYZA1VnLBAVbryNj5DElZjMgwA'; + // 设置请求头 + $headerData = ['client:system']; + $header = setHeader($headerData, $authorization, 'json'); + + + + foreach ($ddd as $key => $value) { + $data = []; + // 发送请求获取公司账号列表 + $result = requestCurl('https://s2.siyuguanli.com:9991/api/Reply/listReply?groupId='.$value['id'], '', 'GET', $header,'json'); + $response = handleApiResponse($result); + foreach ($response as $k => $v) { + $data[] = [ + 'groupId' => $v['groupId'], + 'userId' => $value['userId'], + 'title' => $v['title'], + 'msgType' => $v['msgType'], + 'content' => $v['content'], + 'createTime' => strtotime($v['createTime']), + 'lastUpdateTime' => strtotime($v['lastUpdateTime']), + 'sortIndex' => 50 + ]; + } + $Reply = new Reply(); + $Reply->insertAll($data); + } + + + exit_data(11111); + + + + + } +} \ No newline at end of file diff --git a/application/job/WechatChatroomJob.php b/application/job/WechatChatroomJob.php new file mode 100644 index 0000000..d5b25b0 --- /dev/null +++ b/application/job/WechatChatroomJob.php @@ -0,0 +1,101 @@ + $pageIndex, + 'pageSize' => $pageSize, + 'isDel' => $isDel, + 'jobId' => $jobId, + 'cacheKey' => $cacheKey, + 'queueLockKey' => $queueLockKey + ])); + + // 如果没有提供缓存键,根据删除状态和任务ID生成一个 + if (empty($cacheKey)) { + $cacheKeyPrefix = "chatroomPage:" . ($jobId ?: date('YmdHis') . rand(1000, 9999)); + $cacheKeySuffix = $isDel === '' ? '' : ":{$isDel}"; + $cacheKey = $cacheKeyPrefix . $cacheKeySuffix; + } + + // 如果没有提供队列锁键,生成一个 + if (empty($queueLockKey)) { + $queueLockKey = "queue_lock:wechat_chatroom:{$isDel}"; + } + + // 调用业务逻辑获取微信聊天室列表 + $logic = new WechatChatroomController(); + $result = $logic->getlist(['pageIndex' => $pageIndex, 'pageSize' => $pageSize],true, $isDel); + $response = json_decode($result, true); + $data = $response['data']; + // 判断是否有下一页 + if (!empty($data) && count($data['results']) > 0 && empty($response['isUpdate'])) { + $dataCount = count($data['results']); + $totalCount = $data['total']; + + // 计算是否还有下一页 + $hasNextPage = ($pageIndex + 1) * $pageSize < $totalCount; + + if ($hasNextPage) { + // 缓存页码信息,设置有效期1天 + $nextPageIndex = $pageIndex + 1; + Cache::set($cacheKey, $nextPageIndex, 600); + Log::info("更新缓存页码: {$nextPageIndex}, 缓存键: {$cacheKey}"); + + // 添加下一页任务到队列 + $command = new WechatChatroomCommand(); + $command->addToQueue($nextPageIndex, $pageSize, $isDel, $jobId, $cacheKey, $queueLockKey); + Log::info("已添加下一页任务到队列: 页码 {$nextPageIndex}"); + } else { + // 处理完所有页面,重置页码并释放队列锁 + Cache::set($cacheKey, 0, 600); + Cache::rm($queueLockKey); + Log::info("所有微信聊天室列表页面处理完毕,重置页码为0,释放队列锁: {$queueLockKey}"); + } + } else { + // API调用出错,记录错误并释放队列锁 + Log::error("微信聊天室列表获取失败: " . $response['msg']); + Cache::rm($queueLockKey); + Log::info("由于错误释放队列锁: {$queueLockKey}"); + } + + $job->delete(); + return true; + } catch (\Exception $e) { + // 出现异常,记录错误并释放队列锁 + Log::error('微信聊天室列表任务处理异常: ' . $e->getMessage()); + if (!empty($queueLockKey)) { + Cache::rm($queueLockKey); + Log::info("由于异常释放队列锁: {$queueLockKey}"); + } + + $job->delete(); + return false; + } + } +} \ No newline at end of file diff --git a/application/job/WechatFriendJob.php b/application/job/WechatFriendJob.php new file mode 100644 index 0000000..e6af5fd --- /dev/null +++ b/application/job/WechatFriendJob.php @@ -0,0 +1,169 @@ + $pageIndex, + 'pageSize' => $pageSize, + 'preFriendId' => $preFriendId, + 'isDel' => $isDel, + 'jobId' => $jobId, + 'pageIndexCacheKey' => $pageIndexCacheKey, + 'preFriendIdCacheKey' => $preFriendIdCacheKey, + 'queueLockKey' => $queueLockKey + ])); + + // 如果没有提供缓存键,根据删除状态和任务ID生成 + if (empty($pageIndexCacheKey)) { + $cacheKeyPrefix = "friendsPage:" . ($jobId ?: date('YmdHis') . rand(1000, 9999)); + $cacheKeySuffix = $isDel === '' ? '' : ":{$isDel}"; + $pageIndexCacheKey = $cacheKeyPrefix . $cacheKeySuffix; + } + + if (empty($preFriendIdCacheKey)) { + $cacheKeyPrefix = "preFriendId:" . ($jobId ?: date('YmdHis') . rand(1000, 9999)); + $cacheKeySuffix = $isDel === '' ? '' : ":{$isDel}"; + $preFriendIdCacheKey = $cacheKeyPrefix . $cacheKeySuffix; + } + + // 如果没有提供队列锁键,生成一个 + if (empty($queueLockKey)) { + $queueLockKey = "queue_lock:wechat_friends:{$isDel}"; + } + + // 实例化控制器 + $wechatFriendController = new WechatFriendController(); + + // 设置请求信息 + $request = request(); + $request->withGet([ + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize, + 'preFriendId' => $preFriendId + ]); + + // 调用微信好友列表获取方法,传入isDel参数 + $result = $wechatFriendController->getlist([ + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize, + 'preFriendId' => $preFriendId, + ], true, $isDel); + $response = json_decode($result, true); + + // 判断是否成功 + if ($response['code'] == 200) { + $data = $response['data']; + + // 判断是否有下一页 + if (!empty($data) && count($data) > 0 && empty($response['isUpdate'])) { + // 获取最后一条记录的ID + $lastFriendId = $data[count($data)-1]['id']; + + // 更新缓存中的页码和最后一个好友ID,设置1天过期 + $nextPageIndex = $pageIndex + 1; + Cache::set($pageIndexCacheKey, $nextPageIndex, 600); + Cache::set($preFriendIdCacheKey, $lastFriendId, 600); + + Log::info("更新缓存,下一页页码:{$nextPageIndex},最后好友ID:{$lastFriendId},缓存键: {$pageIndexCacheKey}, {$preFriendIdCacheKey}"); + + // 有下一页,将下一页任务添加到队列 + $command = new WechatFriendCommand(); + $command->addToQueue($nextPageIndex, $pageSize, $lastFriendId, $isDel, $jobId, $pageIndexCacheKey, $preFriendIdCacheKey, $queueLockKey); + Log::info("已添加下一页任务到队列: 页码 {$nextPageIndex}"); + } else { + // 没有下一页,重置缓存并释放队列锁 + Cache::set($pageIndexCacheKey, 0, 600); + Cache::set($preFriendIdCacheKey, '', 600); + Cache::rm($queueLockKey); + Log::info("所有微信好友列表页面处理完毕,重置页码为0,释放队列锁: {$queueLockKey}"); + } + + $job->delete(); + Log::info('微信好友列表任务执行成功,页码:' . $pageIndex . ',删除状态:' . $this->getDeleteStatusText($isDel)); + return true; + } else { + // API调用出错,记录错误 + $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误'; + Log::error('获取微信好友列表失败:' . $errorMsg); + + if ($job->attempts() > 3) { + // 超过重试次数,删除任务并释放队列锁 + Cache::rm($queueLockKey); + Log::info("由于错误释放队列锁: {$queueLockKey}"); + $job->delete(); + } else { + // 任务失败,重新放回队列 + Log::warning('微信好友列表任务执行失败,重试次数:' . $job->attempts() . ',页码:' . $pageIndex); + $job->release(Config::get('queue.failed_delay', 10)); + } + + return false; + } + } catch (\Exception $e) { + // 出现异常,记录错误并释放队列锁 + Log::error('微信好友列表任务异常:' . $e->getMessage()); + + if (!empty($queueLockKey)) { + Cache::rm($queueLockKey); + Log::info("由于异常释放队列锁: {$queueLockKey}"); + } + + if ($job->attempts() > 3) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + + return false; + } + } + + /** + * 获取删除状态的文本描述 + * @param string $isDel 删除状态 + * @return string 状态文本描述 + */ + protected function getDeleteStatusText($isDel) + { + switch ($isDel) { + case '0': + case 0: + return '未删除(false)'; + case '1': + case 1: + return '已删除(true)'; + default: + return '全部'; + } + } +} \ No newline at end of file diff --git a/application/job/WechatListJob.php b/application/job/WechatListJob.php new file mode 100644 index 0000000..01641fb --- /dev/null +++ b/application/job/WechatListJob.php @@ -0,0 +1,117 @@ +processWechatList($data, $job->attempts())) { + $job->delete(); + Log::info('微信客服列表任务执行成功,页码:' . $data['pageIndex']); + } else { + if ($job->attempts() > 3) { + // 超过重试次数,删除任务 + Log::error('微信客服列表任务执行失败,已超过重试次数,页码:' . $data['pageIndex']); + $job->delete(); + } else { + // 任务失败,重新放回队列 + Log::warning('微信客服列表任务执行失败,重试次数:' . $job->attempts() . ',页码:' . $data['pageIndex']); + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } catch (\Exception $e) { + // 出现异常,记录日志 + Log::error('微信客服列表任务异常:' . $e->getMessage()); + if ($job->attempts() > 3) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + } + } + + /** + * 处理微信客服列表获取 + * @param array $data 任务数据 + * @param int $attempts 重试次数 + * @return bool + */ + protected function processWechatList($data, $attempts) + { + // 获取参数 + $pageIndex = isset($data['pageIndex']) ? $data['pageIndex'] : 0; + $pageSize = isset($data['pageSize']) ? $data['pageSize'] : 1000; + + Log::info('开始获取微信客服列表,页码:' . $pageIndex . ',页大小:' . $pageSize); + + // 实例化控制器 + $wechatController = new WechatController(); + + // 构建请求参数 + $params = [ + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize + ]; + + // 设置请求信息 + $request = request(); + $request->withGet($params); + + + // 调用设备列表获取方法 + $result = $wechatController->getlist($pageIndex,$pageSize,true); + $response = json_decode($result,true); + + + + // 判断是否成功 + if ($response['code'] == 200) { + $data = $response['data']; + + // 判断是否有下一页 + if (!empty($data) && count($data['results']) > 0) { + // 有下一页,将下一页任务添加到队列 + $nextPageIndex = $pageIndex + 1; + $this->addNextPageToQueue($nextPageIndex, $pageSize); + Log::info('添加下一页任务到队列,页码:' . $nextPageIndex); + } + + return true; + } else { + $errorMsg = isset($response['msg']) ? $response['msg'] : '未知错误'; + Log::error('获取微信客服列表失败:' . $errorMsg); + return false; + } + } + + /** + * 添加下一页任务到队列 + * @param int $pageIndex 页码 + * @param int $pageSize 每页大小 + */ + protected function addNextPageToQueue($pageIndex, $pageSize) + { + $data = [ + 'pageIndex' => $pageIndex, + 'pageSize' => $pageSize + ]; + + // 添加到队列,设置任务名为 wechat_list + Queue::push(self::class, $data, 'wechat_list'); + } +} \ No newline at end of file diff --git a/application/job/WechatMomentsJob.php b/application/job/WechatMomentsJob.php new file mode 100644 index 0000000..2aabff4 --- /dev/null +++ b/application/job/WechatMomentsJob.php @@ -0,0 +1,131 @@ +where('account',$username)->value('s2_accountId'); + }else{ + Log::error("没有账号配置"); + return; + } + + try { + $jobId = $data['jobId'] ?? ''; + $queueLockKey = $data['queueLockKey'] ?? ''; + Log::info("开始处理朋友圈采集任务,任务ID:{$jobId}"); + + // 获取好友列表 + $friends = $this->getFriends($data['pageIndex'], $data['pageSize']); + if (empty($friends)) { + Log::info("没有更多好友数据,任务完成"); + Cache::rm($queueLockKey); + $job->delete(); + return; + } + + foreach ($friends as $friend) { + try { + // 执行切换好友命令 + $automaticAssign = new AutomaticAssign(); + $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['friendId'], 'toAccountId' => $toAccountId], true); + //存入缓存 + artificialAllotWechatFriend($friend); + + // 执行采集朋友圈命令 + $webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]); + $webSocket->getMoments(['wechatFriendId' => $friend['friendId'], 'wechatAccountId' => $friend['wechatAccountId']]); + + // 处理完毕切换回原账号 + $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['friendId'], 'toAccountId' => $friend['accountId']], true); + + + } catch (\Exception $e) { + // 发生异常时也要切换回原账号 + $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['friendId'], 'toAccountId' => $friend['accountId']], true); + Log::error("采集好友 {$friend['id']} 的朋友圈失败:" . $e->getMessage()); + continue; + } + } + + // 判断是否需要继续翻页 + if (count($friends) < $data['pageSize']) { + // 如果返回的数据少于页面大小,说明已经没有更多数据了 + Log::info("朋友圈采集任务完成,没有更多数据"); + Cache::rm($queueLockKey); + $job->delete(); + } else { + // 还有更多数据,继续处理下一页 + $data['pageIndex']++; + if ($data['pageIndex'] > $this->maxPages) { + Log::info("已达到最大页数限制 {$this->maxPages},任务完成"); + Cache::rm($data['pageIndexCacheKey']); + Cache::rm($queueLockKey); + $job->delete(); + } else { + // 处理下一页 + Cache::set($data['pageIndexCacheKey'], $data['pageIndex']); + + // 有下一页,将下一页任务添加到队列 + $command = new WechatMomentsCommand(); + $command->addToQueue($data['pageIndex'], $data['pageSize'], $jobId, $queueLockKey); + } + } + } catch (\Exception $e) { + $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['friendId'], 'toAccountId' => $friend['accountId']], true); + Log::error("朋友圈采集任务异常:" . $e->getMessage()); + Cache::rm($queueLockKey); + $job->delete(); + } + } + + + /** + * 获取账号的好友列表 + * @param int $accountId 账号ID + * @return array + */ + private function getFriends($page = 1 ,$pageSize = 100) + { + $list = Db::table('s2_company_account') + ->alias('ca') + ->join(['s2_wechat_account' => 'wa'], 'ca.id = wa.deviceAccountId') + ->join(['s2_wechat_friend' => 'wf'], 'ca.id = wf.accountId AND wf.wechatAccountId = wa.id') + ->where([ + 'ca.status' => 0, + 'wf.isDeleted' => 0, + 'wa.deviceAlive' => 1, + 'wa.wechatAlive' => 1 + ]) + ->field([ + 'ca.id as accountId', + 'ca.userName', + 'wf.id as friendId', + 'wf.wechatId', + 'wf.wechatAccountId', + 'wa.wechatId as wechatAccountWechatId', + 'wa.currentDeviceId as deviceId' + ])->group('wf.wechatId') + ->order('wf.id DESC') + ->page($page, $pageSize) + ->select(); + return $list; + } +} \ No newline at end of file diff --git a/application/job/WorkbenchAutoLikeJob.php b/application/job/WorkbenchAutoLikeJob.php new file mode 100644 index 0000000..11c8d84 --- /dev/null +++ b/application/job/WorkbenchAutoLikeJob.php @@ -0,0 +1,483 @@ +logJobStart($jobId, $queueLockKey); + $workbenches = $this->getActiveWorkbenches(); + if (empty($workbenches)) { + $this->handleEmptyWorkbenches($job, $queueLockKey); + return true; + } + $this->processWorkbenches($workbenches); + $this->handleJobSuccess($job, $queueLockKey); + return true; + + } catch (\Exception $e) { + return $this->handleJobError($e, $job, $queueLockKey); + } + } + + /** + * 获取活跃的工作台 + * @return \think\Collection + */ + protected function getActiveWorkbenches() + { + return Workbench::where([ + ['status', '=', 1], + ['isDel', '=', 0], + ['type', '=', 1] // 只获取自动点赞类型的工作台 + ])->order('id DESC')->select(); + } + + /** + * 处理工作台列表 + * @param \think\Collection $workbenches + */ + protected function processWorkbenches($workbenches) + { + foreach ($workbenches as $workbench) { + try { + $this->processSingleWorkbench($workbench); + } catch (\Exception $e) { + Log::error("处理工作台 {$workbench->id} 失败: " . $e->getMessage()); + } + } + } + + /** + * 处理单个工作台 + * @param Workbench $workbench + */ + protected function processSingleWorkbench($workbench) + { + $config = WorkbenchAutoLike::where('workbenchId', $workbench->id)->find(); + if (!$config) { + Log::error("工作台 {$workbench->id} 配置获取失败"); + return; + } + + $this->handleAutoLike($workbench, $config); + } + + /** + * 处理自动点赞任务 + * @param Workbench $workbench + * @param WorkbenchAutoLike $config + */ + protected function handleAutoLike($workbench, $config) + { + if (!$this->validateAutoLikeConfig($workbench, $config)) { + return; + } + + // 验证是否在点赞时间范围内 + if (!$this->isWithinLikeTimeRange($config)) { + return; + } + + // 处理分页获取好友列表 + $this->processAllFriends($workbench, $config); + } + + /** + * 处理所有好友分页 + * @param Workbench $workbench + * @param WorkbenchAutoLike $config + * @param int $page 当前页码 + * @param int $pageSize 每页大小 + */ + protected function processAllFriends($workbench, $config, $page = 1, $pageSize = 100) + { + $friendList = $this->getFriendList($config, $page, $pageSize); + + if (empty($friendList)) { + return; + } + + // 直接顺序处理所有好友 + foreach ($friendList as $friend) { + // 验证是否达到点赞次数上限 + $likeCount = $this->getTodayLikeCount($workbench, $config, $friend['deviceId']); + if ($likeCount >= $config['maxLikes']) { + Log::info("工作台 {$workbench->id} 点赞次数已达上限"); + continue; + } + + // 验证是否达到好友点赞次数上限 + $friendMaxLikes = Db::name('workbench_auto_like_item') + ->where('workbenchId', $workbench->id) + ->where('wechatFriendId', $friend['friendId']) + ->count(); + + if ($friendMaxLikes < $config['friendMaxLikes']) { + $this->processFriendMoments($workbench, $config, $friend); + } + } + + // 如果当前页数据量等于页大小,说明可能还有更多数据,继续处理下一页 + if (count($friendList) == $pageSize) { + $this->processAllFriends($workbench, $config, $page + 1, $pageSize); + } + } + + /** + * 获取好友列表 + * @param WorkbenchAutoLike $config 配置 + * @param int $page 页码 + * @param int $pageSize 每页大小 + * @return array + */ + protected function getFriendList($config, $page = 1, $pageSize = 100) + { + $friends = json_decode($config['friends'], true); + $devices = json_decode($config['devices'], true); + + $list = Db::table('s2_company_account') + ->alias('ca') + ->join(['s2_wechat_account' => 'wa'], 'ca.id = wa.deviceAccountId') + ->join(['s2_wechat_friend' => 'wf'], 'wf.wechatAccountId = wa.id') + ->join('workbench_auto_like_item wali', 'wali.wechatFriendId = wf.id AND wali.workbenchId = ' . $config['workbenchId'], 'left') + ->where([ + 'ca.status' => 0, + 'wf.isDeleted' => 0, + 'wa.deviceAlive' => 1, + 'wa.wechatAlive' => 1 + ]) + ->whereIn('wa.currentDeviceId', $devices) + ->field([ + 'ca.id as accountId', + 'ca.userName', + 'wf.id as friendId', + 'wf.wechatId', + 'wf.wechatAccountId', + 'wa.wechatId as wechatAccountWechatId', + 'wa.currentDeviceId as deviceId', + 'COUNT(wali.id) as like_count' + ]); + + if (!empty($friends) && is_array($friends) && count($friends) > 0) { + $list = $list->whereIn('wf.id', $friends); + } + + $list = $list->group('wf.wechatId') + ->having('like_count < ' . $config['friendMaxLikes']) + ->order('wf.id DESC') + ->page($page, $pageSize) + ->select(); + + return $list; + } + + /** + * 处理好友朋友圈 + * @param Workbench $workbench + * @param WorkbenchAutoLike $config + * @param array $friend + */ + protected function processFriendMoments($workbench, $config, $friend) + { + $toAccountId = ''; + $username = Env::get('api.username2', ''); + $password = Env::get('api.password2', ''); + if (!empty($username) || !empty($password)) { + $toAccountId = Db::name('users')->where('account',$username)->value('s2_accountId'); + } + + try { + // 执行切换好友命令 + $automaticAssign = new AutomaticAssign(); + $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['friendId'], 'toAccountId' => $toAccountId], true); + //存入缓存 + artificialAllotWechatFriend($friend); + // 创建WebSocket链接 + $webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]); + + // 查询未点赞的朋友圈 + $moments = $this->getUnlikedMoments($friend['wechatId']); + if (empty($moments) || count($moments) == 0) { + //采集最新朋友圈 + $webSocket->getMoments(['wechatFriendId' => $friend['friendId'], 'wechatAccountId' => $friend['wechatAccountId']]); + $moments = $this->getUnlikedMoments($friend['wechatId']); + } + + + if (empty($moments) || count($moments) == 0) { + // 处理完毕切换回原账号 + $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['friendId'], 'toAccountId' => $friend['accountId']], true); + Log::info("好友 {$friend['friendId']} 没有需要点赞的朋友圈"); + return; + } + + + foreach ($moments as $moment) { + // 点赞朋友圈 + $this->likeMoment($workbench, $config, $friend, $moment, $webSocket); + + if(!empty($config['enableFriendTags']) && !empty($config['friendTags'])){ + // 修改好友标签 + $labels = $this->getFriendLabels($friend); + $labels[] = $config['friendTags']; + $webSocket->modifyFriendLabel(['wechatFriendId' => $friend['friendId'], 'wechatAccountId' => $friend['wechatAccountId'], 'labels' => $labels]); + + //更新用户标签 + $friendData = Db::table('s2_wechat_friend')->where('id', $friend['friendId'])->find(); + Db::table('s2_wechat_friend')->where('id', $friend['friendId'])->update(['labels' => json_encode($labels,256)]); + } + break; + } + + // 处理完毕切换回原账号 + $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['friendId'], 'toAccountId' => $friend['accountId']], true); + } catch (\Exception $e) { + // 异常情况下也要确保切换回原账号 + $automaticAssign = new AutomaticAssign(); + $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['friendId'], 'toAccountId' => $friend['accountId']], true); + Log::error("处理好友 {$friend['friendId']} 朋友圈失败异常: " . $e->getMessage()); + } + } + + /** + * 获取未点赞的朋友圈 + * @param int $wechatId + * @return \think\Collection + */ + protected function getUnlikedMoments($wechatId) + { + return Db::table('s2_wechat_moments') + ->alias('wm') + ->join('workbench_auto_like_item wali', 'wali.momentsId = wm.id', 'left') + ->where([ + ['wm.userName', '=', $wechatId], + ['wali.id', 'null', null] + ]) + ->where('wm.update_time', '>=', time() - 86400) + ->field('wm.id, wm.snsId') + ->group('wali.wechatFriendId') + ->order('wm.createTime DESC') + ->page(1,1) + ->select(); + } + + /** + * 点赞朋友圈 + * @param Workbench $workbench + * @param WorkbenchAutoLike $config + * @param array $friend + * @param array $moment + * @param WebSocketController $webSocket + */ + protected function likeMoment($workbench, $config, $friend, $moment, $webSocket) + { + try { + $result = $webSocket->momentInteract([ + 'snsId' => $moment['snsId'], + 'wechatAccountId' => $friend['wechatAccountId'], + ]); + + $result = json_decode($result, true); + + if ($result['code'] == 200) { + $this->recordLike($workbench, $moment, $friend); + + // 添加间隔时间 + if (!empty($config['interval'])) { + sleep($config['interval']); + } + } else { + Log::error("工作台 {$workbench->id} 点赞失败: " . ($result['msg'] ?? '未知错误')); + } + } catch (\Exception $e) { + Log::error("工作台 {$workbench->id} 点赞异常: " . $e->getMessage()); + } + } + + /** + * 记录点赞 + * @param Workbench $workbench + * @param array $moment + * @param array $friend + */ + protected function recordLike($workbench, $moment, $friend) + { + Db::name('workbench_auto_like_item')->insert([ + 'workbenchId' => $workbench->id, + 'deviceId' => $friend['deviceId'], + 'momentsId' => $moment['id'], + 'snsId' => $moment['snsId'], + 'wechatAccountId' => $friend['wechatAccountId'], + 'wechatFriendId' => $friend['friendId'], + 'createTime' => time() + ]); + Log::info("工作台 {$workbench->id} 点赞成功: {$moment['snsId']}"); + } + + /** + * 获取好友标签 + * @param array $friend + * @return array + */ + protected function getFriendLabels($friend) + { + $wechatFriendController = new WechatFriendController(); + $result = $wechatFriendController->getlist([ + 'friendKeyword' => $friend['wechatId'], + 'wechatAccountKeyword' => $friend['wechatAccountWechatId'] + ], true); + + $result = json_decode($result, true); + $labels = []; + + if(!empty($result['data'])){ + foreach($result['data'] as $item){ + $labels = array_merge($labels, $item['labels']); + } + } + + return $labels; + } + + /** + * 验证自动点赞配置 + * @param Workbench $workbench + * @param WorkbenchAutoLike $config + * @return bool + */ + protected function validateAutoLikeConfig($workbench, $config) + { + $requiredFields = ['contentTypes', 'interval', 'maxLikes', 'startTime', 'endTime']; + foreach ($requiredFields as $field) { + if (empty($config[$field])) { + Log::error("工作台 {$workbench->id} 配置字段 {$field} 为空"); + return false; + } + } + return true; + } + + /** + * 获取今日点赞次数 + * @param Workbench $workbench + * @param WorkbenchAutoLike $config + * @return int + */ + protected function getTodayLikeCount($workbench, $config, $deviceId) + { + return Db::name('workbench_auto_like_item') + ->where('workbenchId', $workbench->id) + ->where('deviceId', $deviceId) + ->whereTime('createTime', 'between', [ + strtotime(date('Y-m-d') . ' ' . $config['startTime'] . ':00'), + strtotime(date('Y-m-d') . ' ' . $config['endTime'] . ':00') + ]) + ->count(); + } + + /** + * 检查是否在点赞时间范围内 + * @param WorkbenchAutoLike $config + * @return bool + */ + protected function isWithinLikeTimeRange($config) + { + $currentTime = date('H:i'); + if ($currentTime < $config['startTime'] || $currentTime > $config['endTime']) { + Log::info("当前时间 {$currentTime} 不在点赞时间范围内 ({$config['startTime']} - {$config['endTime']})"); + return false; + } + return true; + } + + /** + * 记录任务开始 + * @param string $jobId + * @param string $queueLockKey + */ + protected function logJobStart($jobId, $queueLockKey) + { + Log::info('开始处理工作台自动点赞任务: ' . json_encode([ + 'jobId' => $jobId, + 'queueLockKey' => $queueLockKey + ])); + } + + /** + * 处理任务成功 + * @param Job $job + * @param string $queueLockKey + */ + protected function handleJobSuccess($job, $queueLockKey) + { + $job->delete(); + Cache::rm($queueLockKey); + Log::info('工作台自动点赞任务执行成功'); + } + + /** + * 处理任务错误 + * @param \Exception $e + * @param Job $job + * @param string $queueLockKey + * @return bool + */ + protected function handleJobError(\Exception $e, $job, $queueLockKey) + { + Log::error('工作台自动点赞任务异常:' . $e->getMessage()); + + if (!empty($queueLockKey)) { + Cache::rm($queueLockKey); + Log::info("由于异常释放队列锁: {$queueLockKey}"); + } + + if ($job->attempts() > self::MAX_RETRY_ATTEMPTS) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + + return false; + } + + /** + * 处理空工作台情况 + * @param Job $job + * @param string $queueLockKey + */ + protected function handleEmptyWorkbenches(Job $job, $queueLockKey) + { + Log::info('没有需要处理的工作台自动点赞任务'); + $job->delete(); + Cache::rm($queueLockKey); + } +} \ No newline at end of file diff --git a/application/job/WorkbenchGroupCreateAdminFriendJob.php b/application/job/WorkbenchGroupCreateAdminFriendJob.php new file mode 100644 index 0000000..459db92 --- /dev/null +++ b/application/job/WorkbenchGroupCreateAdminFriendJob.php @@ -0,0 +1,155 @@ +delete(); + return true; + } + + // 获取管理员信息 + $adminFriends = Db::table('s2_wechat_friend') + ->where('id', 'in', $adminFriendIds) + ->column('id,wechatId,ownerWechatId'); + + if (empty($adminFriends)) { + Log::warning("未找到管理员好友信息。工作台ID: {$workbenchId}"); + $job->delete(); + return true; + } + + // 获取微信账号信息 + $wechatAccount = Db::table('s2_wechat_account')->where('id', $wechatAccountId)->find(); + if (empty($wechatAccount)) { + Log::error("未找到微信账号。微信账号ID: {$wechatAccountId}"); + $job->delete(); + return false; + } + + // 从流量池用户中查找每个管理员的好友 + // 管理员的好友:从s2_wechat_friend表中查找,ownerWechatId=管理员的wechatId,且wechatId在流量池用户中 + $allAdminFriendIds = []; + foreach ($adminFriends as $adminFriend) { + $adminWechatId = $adminFriend['wechatId']; + + // 从好友表中查找该管理员的好友(在流量池用户中) + $adminFriendsList = Db::table('s2_wechat_friend') + ->where('ownerWechatId', $adminWechatId) + ->whereIn('wechatId', $poolUsers) + ->column('id,wechatId'); + + if (!empty($adminFriendsList)) { + $allAdminFriendIds = array_merge($allAdminFriendIds, array_keys($adminFriendsList)); + } + } + + $allAdminFriendIds = array_unique($allAdminFriendIds); + + if (empty($allAdminFriendIds)) { + Log::info("未找到管理员的好友,跳过拉人。工作台ID: {$workbenchId}"); + $job->delete(); + return true; + } + + // 初始化WebSocket + $toAccountId = ''; + $username = Env::get('api.username2', ''); + $password = Env::get('api.password2', ''); + if (!empty($username) || !empty($password)) { + $toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId'); + } + $webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]); + + // 拉管理员好友进群 + $inviteResult = $webSocket->CmdChatroomInvite([ + 'wechatChatroomId' => $groupId, + 'wechatFriendIds' => $allAdminFriendIds + ]); + + // 记录管理员好友进群 + $installData = []; + foreach ($allAdminFriendIds as $friendId) { + $friendInfo = Db::table('s2_wechat_friend')->where('id', $friendId)->find(); + $installData[] = [ + 'workbenchId' => $workbenchId, + 'friendId' => $friendId, + 'wechatId' => $friendInfo['wechatId'] ?? '', + 'groupId' => $groupId, + 'wechatAccountId' => $wechatAccountId, + 'status' => self::STATUS_ADMIN_FRIEND_ADDED, + 'memberType' => self::MEMBER_TYPE_ADMIN_FRIEND, + 'retryCount' => 0, + 'chatroomId' => $chatroomId, + 'createTime' => time(), + ]; + } + + if (!empty($installData)) { + Db::name('workbench_group_create_item')->insertAll($installData); + Log::info("管理员好友已拉入群。工作台ID: {$workbenchId}, 群ID: {$groupId}, 好友数: " . count($installData)); + } + + $job->delete(); + return true; + } catch (\Exception $e) { + Log::error("拉管理员好友任务异常:{$e->getMessage()}"); + + if ($job->attempts() > self::MAX_RETRY_ATTEMPTS) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + + return false; + } + } +} + diff --git a/application/job/WorkbenchGroupCreateJob.php b/application/job/WorkbenchGroupCreateJob.php new file mode 100644 index 0000000..5359ab8 --- /dev/null +++ b/application/job/WorkbenchGroupCreateJob.php @@ -0,0 +1,852 @@ +logJobStart($jobId, $queueLockKey); + $this->execute(); + $this->handleJobSuccess($job, $queueLockKey); + return true; + } catch (\Exception $e) { + return $this->handleJobError($e, $job, $queueLockKey); + } + } + + /** + * 成员类型常量 + */ + const MEMBER_TYPE_OWNER = 1; // 群主成员 + const MEMBER_TYPE_ADMIN = 2; // 管理员 + const MEMBER_TYPE_OWNER_FRIEND = 3; // 群主好友 + const MEMBER_TYPE_ADMIN_FRIEND = 4; // 管理员好友 + + /** + * 状态常量 + */ + const STATUS_PENDING = 0; // 待创建 + const STATUS_CREATING = 1; // 创建中 + const STATUS_SUCCESS = 2; // 创建成功 + const STATUS_FAILED = 3; // 创建失败 + const STATUS_ADMIN_FRIEND_ADDED = 4; // 管理员好友已拉入 + + /** + * 执行任务 + * @throws \Exception + */ + public function execute() + { + try { + // 1. 查询启用了建群功能的数据 + $workbenches = Workbench::where(['status' => 0, 'type' => 4, 'isDel' => 0,'id' => 354])->order('id desc')->select(); + + foreach ($workbenches as $workbench) { + // 获取工作台配置 + $config = WorkbenchGroupCreate::where('workbenchId', $workbench->id)->find(); + if (!$config) { + continue; + } + + // 解析配置 + $config['poolGroups'] = json_decode($config['poolGroups'] ?? '[]', true) ?: []; + $config['devices'] = json_decode($config['devices'] ?? '[]', true) ?: []; + $config['wechatGroups'] = json_decode($config['wechatGroups'] ?? '[]', true) ?: []; + $config['admins'] = json_decode($config['admins'] ?? '[]', true) ?: []; + + // 检查时间限制 + if (!$this->isWithinTimeRange($config)) { + continue; + } + + // 检查每日建群数量限制 + if (!$this->checkDailyLimit($workbench->id, $config)) { + continue; + } + + // 检查是否有正在创建中的群,如果有则跳过(避免重复创建) + $creatingCount = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbench->id) + ->where('status', self::STATUS_CREATING) + ->where('groupId', '<>', null) // 有groupId的记录 + ->group('groupId') + ->count(); + if ($creatingCount > 0) { + Log::info("工作台ID: {$workbench->id} 有正在创建中的群({$creatingCount}个),跳过本次执行"); + continue; + } + + if (empty($config['devices'])) { + continue; + } + // 获取群主成员(从设备中获取) + $groupMember = []; + $wechatIds = Db::name('device_wechat_login') + ->whereIn('deviceId', $config['devices']) + ->where('alive', DeviceWechatLoginModel::ALIVE_WECHAT_ACTIVE) + ->order('id desc') + ->column('wechatId'); + + if (empty($wechatIds)) { + continue; + } + $groupMember = array_unique($wechatIds); + + // 获取群主好友ID映射(所有群主的好友) + $groupMemberWechatId = []; + $groupMemberId = []; + + foreach ($groupMember as $ownerWechatId) { + $friends = Db::table('s2_wechat_friend') + ->where('ownerWechatId', $ownerWechatId) + ->whereIn('wechatId', $groupMember) + ->where('isDeleted', 0) + ->field('id,wechatId') + ->select(); + + foreach ($friends as $friend) { + if (!isset($groupMemberWechatId[$friend['id']])) { + $groupMemberWechatId[$friend['id']] = $friend['wechatId']; + $groupMemberId[] = $friend['id']; + } + } + } + + // 如果配置了wechatGroups,从指定的群组中获取成员 + if (!empty($config['wechatGroups'])) { + $this->addGroupMembersFromWechatGroups($config['wechatGroups'], $groupMember, $groupMemberId, $groupMemberWechatId); + } + + if (empty($groupMemberId)) { + continue; + } + + // 获取流量池用户(如果配置了流量池) + $poolItem = []; + if (!empty($config['poolGroups'])) { + $poolItem = Db::name('traffic_source_package_item') + ->whereIn('packageId', $config['poolGroups']) + ->where('isDel', 0) + ->group('identifier') + ->column('identifier'); + } + + // 如果既没有流量池也没有指定群组,跳过 + if (empty($poolItem) && empty($config['wechatGroups'])) { + continue; + } + + // 获取已入群的用户(排除已成功入群的) + $groupUser = []; + if (!empty($poolItem)) { + $groupUser = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbench->id) + ->where('status', 'in', [self::STATUS_SUCCESS, self::STATUS_ADMIN_FRIEND_ADDED, self::STATUS_CREATING]) + ->whereIn('wechatId', $poolItem) + ->group('wechatId') + ->column('wechatId'); + } + + // 待入群的用户(从流量池中筛选) + $joinUser = !empty($poolItem) ? array_diff($poolItem, $groupUser) : []; + + // 如果流量池用户已用完或没有配置流量池,但配置了wechatGroups,至少创建一次(使用群主成员) + if (empty($joinUser) && !empty($config['wechatGroups'])) { + // 如果没有流量池用户,创建一个空批次,让processBatchUsers处理只有群主成员的情况 + $joinUser = []; // 空数组,但会继续执行 + } + + // 如果既没有流量池用户也没有配置wechatGroups,跳过 + if (empty($joinUser) && empty($config['wechatGroups'])) { + continue; + } + + // 计算随机群人数(不包含管理员,只减去群主成员数) + // 群主成员数 = 群主好友ID数量 + $minGroupSize = max(2, $config['groupSizeMin']); // 至少2人才能建群 + $maxGroupSize = max($minGroupSize, $config['groupSizeMax']); + $groupRandNum = mt_rand($minGroupSize, $maxGroupSize) - count($groupMemberId); + if ($groupRandNum <= 0) { + $groupRandNum = 1; // 至少需要1个成员 + } + + // 分批处理待入群用户 + $addGroupUser = []; + if (!empty($joinUser)) { + $totalRows = count($joinUser); + for ($i = 0; $i < $totalRows; $i += $groupRandNum) { + $batchRows = array_slice($joinUser, $i, $groupRandNum); + if (!empty($batchRows)) { + $addGroupUser[] = $batchRows; + } + } + } else { + // 如果没有流量池用户但配置了wechatGroups,创建一个空批次 + $addGroupUser[] = []; + } + // 初始化WebSocket + $toAccountId = ''; + $username = Env::get('api.username2', ''); + $password = Env::get('api.password2', ''); + if (!empty($username) || !empty($password)) { + $toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId'); + } + $webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]); + + // 遍历每批用户 + foreach ($addGroupUser as $batchUsers) { + $this->processBatchUsers($workbench, $config, $batchUsers, $groupMemberId, $groupMemberWechatId, $groupRandNum, $webSocket); + } + } + } catch (\Exception $e) { + Log::error("工作台建群任务异常: " . $e->getMessage()); + throw $e; + } + } + + /** + * 处理一批用户 + * @param Workbench $workbench 工作台 + * @param array $config 配置 + * @param array $batchUsers 批次用户(微信ID数组,来自流量池) + * @param array $groupMemberId 群主成员ID数组 + * @param array $groupMemberWechatId 群主成员微信ID映射 + * @param int $groupRandNum 随机群人数(不包含管理员) + * @param WebSocketController $webSocket WebSocket实例 + */ + protected function processBatchUsers($workbench, $config, $batchUsers, $groupMemberId, $groupMemberWechatId, $groupRandNum, $webSocket) + { + // 1. 获取群主微信ID列表(用于验证管理员) + // 从群主成员的好友记录中提取所有群主的微信ID(ownerWechatId) + $groupOwnerWechatIds = []; + foreach ($groupMemberId as $memberId) { + $member = Db::table('s2_wechat_friend')->where('id', $memberId)->find(); + if ($member && !in_array($member['ownerWechatId'], $groupOwnerWechatIds)) { + $groupOwnerWechatIds[] = $member['ownerWechatId']; + } + } + + + // 如果从好友表获取不到,使用群主成员微信ID列表(作为备用) + if (empty($groupOwnerWechatIds)) { + $groupOwnerWechatIds = array_values(array_unique($groupMemberWechatId)); + } + // 2. 验证并获取管理员好友ID(管理员必须是群主的好友) + $adminFriendIds = []; + $adminWechatIds = []; + if (!empty($config['admins'])) { + $adminFriends = Db::table('s2_wechat_friend') + ->where('id', 'in', $config['admins']) + ->field('id,wechatId,ownerWechatId') + ->select(); + + foreach ($adminFriends as $adminFriend) { + // 验证:管理员必须是群主的好友 + if (in_array($adminFriend['ownerWechatId'], $groupOwnerWechatIds)) { + $adminFriendIds[] = $adminFriend['id']; + $adminWechatIds[$adminFriend['id']] = $adminFriend['wechatId']; + } + } + } + + + // 3. 从流量池用户中筛选出是群主好友的用户(按微信账号分组) + $ownerFriendIdsByAccount = []; + $wechatIds = []; + + // 如果batchUsers为空,说明没有流量池用户,但可能配置了wechatGroups + // 这种情况下,使用群主成员作为基础,按账号分组 + if (empty($batchUsers)) { + // 按账号分组群主成员 + foreach ($groupMemberId as $memberId) { + $member = Db::table('s2_wechat_friend')->where('id', $memberId)->find(); + if ($member) { + $accountWechatId = $member['ownerWechatId']; + $account = Db::table('s2_wechat_account') + ->where('wechatId', $accountWechatId) + ->field('id') + ->find(); + + if ($account) { + $wechatAccountId = $account['id']; + if (!isset($ownerFriendIdsByAccount[$wechatAccountId])) { + $ownerFriendIdsByAccount[$wechatAccountId] = []; + } + $ownerFriendIdsByAccount[$wechatAccountId][] = $memberId; + $wechatIds[$memberId] = $groupMemberWechatId[$memberId] ?? ''; + } + } + } + } else { + // 获取群主的好友关系(从流量池中筛选) + $ownerFriends = Db::table('s2_wechat_friend')->alias('f') + ->join(['s2_wechat_account' => 'a'], 'f.wechatAccountId=a.id') + ->whereIn('f.wechatId', $batchUsers) + ->whereIn('a.wechatId', $groupOwnerWechatIds) + ->where('f.isDeleted', 0) + ->field('f.id,f.wechatId,a.id as wechatAccountId') + ->select(); + + if (empty($ownerFriends)) { + Log::warning("未找到群主的好友,跳过。工作台ID: {$workbench->id}"); + return; + } + + // 按微信账号分组群主好友 + foreach ($ownerFriends as $friend) { + $wechatAccountId = $friend['wechatAccountId']; + if (!isset($ownerFriendIdsByAccount[$wechatAccountId])) { + $ownerFriendIdsByAccount[$wechatAccountId] = []; + } + $ownerFriendIdsByAccount[$wechatAccountId][] = $friend['id']; + $wechatIds[$friend['id']] = $friend['wechatId']; + } + } + + // 如果没有找到任何好友,跳过 + if (empty($ownerFriendIdsByAccount)) { + Log::warning("未找到任何群主好友或成员,跳过。工作台ID: {$workbench->id}"); + return; + } + + // 4. 遍历每个微信账号,创建群 + foreach ($ownerFriendIdsByAccount as $wechatAccountId => $ownerFriendIds) { + // 4.1 获取当前账号的管理员好友ID + $currentAdminFriendIds = []; + $accountWechatId = Db::table('s2_wechat_account')->where('id', $wechatAccountId)->value('wechatId'); + foreach ($adminFriendIds as $adminFriendId) { + $adminFriend = Db::table('s2_wechat_friend')->where('id', $adminFriendId)->find(); + if ($adminFriend && $adminFriend['ownerWechatId'] == $accountWechatId) { + $currentAdminFriendIds[] = $adminFriendId; + $wechatIds[$adminFriendId] = $adminWechatIds[$adminFriendId]; + } + } + + // 4.2 获取当前账号的群主成员ID + $currentGroupMemberIds = []; + foreach ($groupMemberId as $memberId) { + $member = Db::table('s2_wechat_friend')->where('id', $memberId)->find(); + if ($member && $member['ownerWechatId'] == $accountWechatId) { + $currentGroupMemberIds[] = $memberId; + if (!isset($wechatIds[$memberId])) { + $wechatIds[$memberId] = $groupMemberWechatId[$memberId] ?? ''; + } + } + } + + // 4.3 限制群主好友数量(按随机群人数) + // 如果ownerFriendIds只包含群主成员(没有流量池用户),则不需要限制 + $limitedOwnerFriendIds = $ownerFriendIds; + if (count($ownerFriendIds) > $groupRandNum) { + $limitedOwnerFriendIds = array_slice($ownerFriendIds, 0, $groupRandNum); + } + + // 4.4 创建群:管理员 + 群主成员 + 群主好友(从流量池筛选) + // 合并时去重,避免重复添加群主成员 + $createFriendIds = array_merge($currentAdminFriendIds, $currentGroupMemberIds); + foreach ($limitedOwnerFriendIds as $friendId) { + if (!in_array($friendId, $createFriendIds)) { + $createFriendIds[] = $friendId; + } + } + + // 微信建群至少需要2个人 + if (count($createFriendIds) < 2) { + Log::warning("建群好友数量不足(至少需要2人),跳过。工作台ID: {$workbench->id}, 微信账号ID: {$wechatAccountId}, 当前人数: " . count($createFriendIds)); + continue; + } + + // 4.5 检查当前账号是否有正在创建中的群,如果有则跳过 + $creatingGroupCount = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbench->id) + ->where('wechatAccountId', $wechatAccountId) + ->where('status', self::STATUS_CREATING) + ->where('groupId', '<>', null) + ->group('groupId') + ->count(); + + if ($creatingGroupCount > 0) { + Log::info("工作台ID: {$workbench->id}, 微信账号ID: {$wechatAccountId} 有正在创建中的群({$creatingGroupCount}个),跳过本次创建"); + continue; + } + + // 4.6 生成群名称 + $existingGroupCount = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbench->id) + ->where('wechatAccountId', $wechatAccountId) + ->where('status', self::STATUS_SUCCESS) + ->where('groupId', '<>', null) // 排除groupId为NULL的记录 + ->group('groupId') + ->count(); + + $chatroomName = $existingGroupCount > 0 + ? $config['groupNameTemplate'] . ($existingGroupCount + 1) . '群' + : $config['groupNameTemplate']; + + // 4.7 调用建群接口 + $createTime = time(); + $createResult = $webSocket->CmdChatroomCreate([ + 'chatroomName' => $chatroomName, + 'wechatFriendIds' => $createFriendIds, + 'wechatAccountId' => $wechatAccountId + ]); + + $createResultData = json_decode($createResult, true); + + // 4.8 解析建群结果,获取群ID + // chatroomId: varchar(64) - 微信的群聊ID(字符串) + // groupId: int(10) - 数据库中的群组ID(整数) + $chatroomId = null; // 微信群聊ID(字符串) + $groupId = 0; // 数据库群组ID(整数) + $tempGroupId = null; // 临时群标识,用于轮询查询 + + if (!empty($createResultData) && isset($createResultData['code']) && $createResultData['code'] == 200) { + // 尝试从返回数据中获取群ID(根据实际API返回格式调整) + if (isset($createResultData['data']['chatroomId'])) { + // API返回的是chatroomId(字符串) + $chatroomId = (string)$createResultData['data']['chatroomId']; + // 通过chatroomId查询数据库获取groupId + $group = Db::name('wechat_group') + ->where('chatroomId', $chatroomId) + ->where('deleteTime', 0) + ->find(); + if ($group) { + $groupId = intval($group['id']); + } + } elseif (isset($createResultData['data']['id'])) { + // API返回的是数据库ID(整数) + $groupId = intval($createResultData['data']['id']); + // 通过groupId查询chatroomId + $group = Db::name('wechat_group') + ->where('id', $groupId) + ->where('deleteTime', 0) + ->find(); + if ($group && !empty($group['chatroomId'])) { + $chatroomId = (string)$group['chatroomId']; + } + } + // 如果有临时标识,保存用于轮询 + if (isset($createResultData['data']['tempId'])) { + $tempGroupId = $createResultData['data']['tempId']; + } + } + + // 4.9 如果建群接口没有立即返回群ID,进行同步轮询检查 + if ($groupId == 0) { + // 获取账号的微信ID(群主微信ID) + $accountWechatId = Db::table('s2_wechat_account') + ->where('id', $wechatAccountId) + ->value('wechatId'); + + if (!empty($accountWechatId)) { + $pollResult = $this->pollGroupCreation($chatroomName, $accountWechatId, $wechatAccountId, $tempGroupId); + if ($pollResult && is_array($pollResult)) { + $groupId = intval($pollResult['groupId'] ?? 0); + $chatroomId = !empty($pollResult['chatroomId']) ? (string)$pollResult['chatroomId'] : null; + } elseif ($pollResult > 0) { + // 兼容旧返回值(只返回groupId) + $groupId =0; + $chatroomId = null; + } + } + } + + // 4.10 记录创建请求 + $installData = []; + foreach ($createFriendIds as $friendId) { + $memberType = in_array($friendId, $currentAdminFriendIds) + ? self::MEMBER_TYPE_ADMIN + : (in_array($friendId, $currentGroupMemberIds) ? self::MEMBER_TYPE_OWNER : self::MEMBER_TYPE_OWNER_FRIEND); + + $installData[] = [ + 'workbenchId' => $workbench->id, + 'friendId' => $friendId, + 'wechatId' => $wechatIds[$friendId] ?? ($groupMemberWechatId[$friendId] ?? ''), + 'groupId' => $groupId > 0 ? $groupId : null, // int类型 + 'wechatAccountId' => $wechatAccountId, + 'status' => $groupId > 0 ? self::STATUS_SUCCESS : self::STATUS_FAILED, + 'memberType' => $memberType, + 'retryCount' => 0, + 'chatroomId' => $chatroomId, // varchar类型 + 'createTime' => $createTime, + ]; + } + Db::name('workbench_group_create_item')->insertAll($installData); + + // 5. 如果群创建成功,拉管理员的好友进群 + // 注意:拉人接口需要chatroomId(字符串),而不是groupId(整数) + if (!empty($chatroomId) && !empty($currentAdminFriendIds)) { + $this->inviteAdminFriends($workbench, $config, $batchUsers, $currentAdminFriendIds, $chatroomId, $groupId, $wechatAccountId, $wechatIds, $createTime, $webSocket); + } + } + } + + /** + * 拉管理员的好友进群 + * @param Workbench $workbench 工作台 + * @param array $config 配置 + * @param array $batchUsers 批次用户(流量池微信ID数组) + * @param array $adminFriendIds 管理员好友ID数组 + * @param string $chatroomId 群聊ID(字符串,用于API调用) + * @param int $groupId 数据库群组ID(整数) + * @param int $wechatAccountId 微信账号ID + * @param array $wechatIds 好友ID到微信ID的映射 + * @param int $createTime 创建时间 + * @param WebSocketController $webSocket WebSocket实例 + */ + protected function inviteAdminFriends($workbench, $config, $batchUsers, $adminFriendIds, $chatroomId, $groupId, $wechatAccountId, $wechatIds, $createTime, $webSocket) + { + // 获取管理员的微信ID列表 + $adminWechatIds = []; + foreach ($adminFriendIds as $adminFriendId) { + if (isset($wechatIds[$adminFriendId])) { + $adminWechatIds[] = $wechatIds[$adminFriendId]; + } + } + + if (empty($adminWechatIds)) { + return; + } + + // 从流量池用户中筛选出是管理员好友的用户 + $adminFriendsFromPool = Db::table('s2_wechat_friend')->alias('f') + ->join(['s2_wechat_account' => 'a'], 'f.wechatAccountId=a.id') + ->whereIn('f.wechatId', $batchUsers) + ->whereIn('a.wechatId', $adminWechatIds) + ->where('a.id', $wechatAccountId) + ->where('f.isDeleted', 0) + ->field('f.id,f.wechatId') + ->select(); + + if (empty($adminFriendsFromPool)) { + Log::info("未找到管理员的好友,跳过拉人。工作台ID: {$workbench->id}, 群ID: {$chatroomId}"); + return; + } + + // 提取好友ID列表 + $adminFriendIdsToInvite = []; + foreach ($adminFriendsFromPool as $friend) { + $adminFriendIdsToInvite[] = $friend['id']; + $wechatIds[$friend['id']] = $friend['wechatId']; + } + + // 调用拉人接口(使用chatroomId字符串) + $inviteResult = $webSocket->CmdChatroomInvite([ + 'wechatChatroomId' => $chatroomId, + 'wechatFriendIds' => $adminFriendIdsToInvite + ]); + + $inviteResultData = json_decode($inviteResult, true); + $inviteSuccess = !empty($inviteResultData) && isset($inviteResultData['code']) && $inviteResultData['code'] == 200; + + // 记录管理员好友拉入状态 + $adminFriendData = []; + foreach ($adminFriendIdsToInvite as $friendId) { + $adminFriendData[] = [ + 'workbenchId' => $workbench->id, + 'friendId' => $friendId, + 'wechatId' => $wechatIds[$friendId] ?? '', + 'groupId' => $groupId > 0 ? $groupId : null, // int类型 + 'wechatAccountId' => $wechatAccountId, + 'status' => $inviteSuccess ? self::STATUS_ADMIN_FRIEND_ADDED : self::STATUS_FAILED, + 'memberType' => self::MEMBER_TYPE_ADMIN_FRIEND, + 'retryCount' => 0, + 'chatroomId' => $chatroomId, // varchar类型 + 'createTime' => $createTime, + ]; + } + Db::name('workbench_group_create_item')->insertAll($adminFriendData); + + if ($inviteSuccess) { + // 去除成功日志,减少日志空间消耗 + } else { + Log::warning("管理员好友拉入失败。工作台ID: {$workbench->id}, 群组ID: {$groupId}, 群聊ID: {$chatroomId}"); + } + } + + + /** + * 轮询检查群是否创建成功 + * @param string $chatroomName 群名称 + * @param string $ownerWechatId 群主微信ID + * @param int $wechatAccountId 微信账号ID + * @param string|null $tempGroupId 临时群标识(如果有) + * @return array|int 返回数组包含groupId和chatroomId,或只返回groupId(兼容旧代码),如果未找到返回0 + */ + protected function pollGroupCreation($chatroomName, $ownerWechatId, $wechatAccountId, $tempGroupId = null) + { + $maxAttempts = 10; // 最多查询10次 + $interval = 5; // 每次间隔5秒 + + // 获取账号ID(accountId)和微信账号的微信ID(wechatAccountWechatId),用于查询s2_wechat_chatroom表 + $accountInfo = Db::table('s2_wechat_account') + ->where('id', $wechatAccountId) + ->field('id,wechatId') + ->find(); + + $accountId = $accountInfo['id'] ?? null; + $wechatAccountWechatId = $accountInfo['wechatId'] ?? null; + + if (empty($accountId) && empty($wechatAccountWechatId)) { + Log::warning("无法获取账号ID和微信账号ID,跳过轮询。微信账号ID: {$wechatAccountId}"); + return 0; + } + + // 获取授权信息(用于调用同步接口) + $username = Env::get('api.username2', ''); + $password = Env::get('api.password2', ''); + + for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) { + // 等待5秒(第一次立即查询,后续等待) + if ($attempt > 1) { + sleep($interval); + } + + // 1. 先调用接口同步最新的群组信息 + try { + $chatroomController = new \app\api\controller\WechatChatroomController(); + // 构建同步参数 + $syncData = [ + 'wechatAccountKeyword' => $ownerWechatId, // 通过群主微信ID筛选 + 'isDeleted' => false, + 'pageIndex' => 0, + 'pageSize' => 5 // 获取足够多的数据 + ]; + // 调用getlist方法同步数据(内部调用,isInner=true) + $chatroomController->getlist($syncData, true, 0); + } catch (\Exception $e) { + Log::warning("同步群组信息失败: " . $e->getMessage()); + // 即使同步失败,也继续查询本地数据 + } + + // 2. 查询本地表 s2_wechat_chatroom + // 计算5分钟前的时间戳 + $fiveMinutesAgo = time() - 300; // 5分钟 = 300秒 + $now = time(); + + // 查询群聊:通过群名称、账号ID或微信账号ID和创建时间查询 + // 如果accountId不为空,优先使用accountId查询;如果accountId为空,则使用wechatAccountWechatId查询 + $chatroom = Db::table('s2_wechat_chatroom') + ->where('nickname', $chatroomName) + ->where('isDeleted', 0) + ->where('createTime', '>=', $fiveMinutesAgo) // 创建时间在5分钟内 + ->where('createTime', '<=', $now) + ->where('wechatAccountWechatId', $wechatAccountWechatId) + ->order('createTime', 'desc') + ->find(); + + + // 如果找到了群聊,返回群ID和chatroomId + if ($chatroom && !empty($chatroom['id'])) { + $chatroomId = !empty($chatroom['chatroomId']) ? (string)$chatroom['chatroomId'] : null; + // 如果有chatroomId,尝试查询wechat_group表获取groupId + $groupId = $chatroom['id']; + Log::info("轮询检查群创建成功。群名称: {$chatroomName}, 群聊ID: {$chatroom['id']}, chatroomId: {$chatroomId}, 群组ID: {$groupId}, 尝试次数: {$attempt}"); + return [ + 'groupId' => $groupId > 0 ? $groupId : intval($chatroom['id']), // 如果没有groupId,使用chatroom的id + 'chatroomId' => $chatroomId ?: (string)$chatroom['id'] + ]; + } + + Log::debug("轮询检查群创建中。群名称: {$chatroomName}, 尝试次数: {$attempt}/{$maxAttempts}"); + } + + // 10次查询后仍未找到,返回0表示失败 + Log::warning("轮询检查群创建失败,已查询{$maxAttempts}次仍未找到群组。群名称: {$chatroomName}, 群主微信ID: {$ownerWechatId}, 账号ID: {$accountId}"); + return 0; + } + + /** + * 检查是否在时间范围内 + * @param array $config 配置 + * @return bool + */ + protected function isWithinTimeRange($config) + { + if (empty($config['startTime']) || empty($config['endTime'])) { + return true; // 如果没有配置时间,则允许执行 + } + + $today = date('Y-m-d'); + $startTimestamp = strtotime($today . ' ' . $config['startTime'] . ':00'); + $endTimestamp = strtotime($today . ' ' . $config['endTime'] . ':00'); + + $currentTime = time(); + + // 如果开始时间大于当前时间,还未到执行时间 + if ($startTimestamp > $currentTime) { + return false; + } + + // 如果结束时间小于当前时间,已过执行时间 + if ($endTimestamp < $currentTime) { + return false; + } + + return true; + } + + /** + * 检查每日建群数量限制 + * @param int $workbenchId 工作台ID + * @param array $config 配置 + * @return bool + */ + protected function checkDailyLimit($workbenchId, $config) + { + if (empty($config['maxGroupsPerDay']) || $config['maxGroupsPerDay'] <= 0) { + return true; // 如果没有配置限制,则允许执行 + } + + $today = date('Y-m-d'); + $startTimestamp = strtotime($today . ' 00:00:00'); + $endTimestamp = strtotime($today . ' 23:59:59'); + + // 查询今日已创建的群数量(状态为成功) + $todayCount = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('status', self::STATUS_SUCCESS) + ->where('groupId', '<>', null) // 排除groupId为NULL的记录 + ->where('createTime', 'between', [$startTimestamp, $endTimestamp]) + ->group('groupId') + ->count(); + + return $todayCount < $config['maxGroupsPerDay']; + } + + /** + * 从指定的微信群组中获取成员 + * @param array $wechatGroups 群组ID数组(可能是好友ID或群组ID) + * @param array $groupMember 群主成员微信ID数组(引用传递) + * @param array $groupMemberId 群主成员好友ID数组(引用传递) + * @param array $groupMemberWechatId 群主成员微信ID映射(引用传递) + */ + protected function addGroupMembersFromWechatGroups($wechatGroups, &$groupMember, &$groupMemberId, &$groupMemberWechatId) + { + foreach ($wechatGroups as $groupId) { + if (is_numeric($groupId)) { + // 数字ID:可能是好友ID,查询好友信息 + $friend = Db::table('s2_wechat_friend') + ->where('id', $groupId) + ->where('isDeleted', 0) + ->field('id,wechatId,ownerWechatId') + ->find(); + + if ($friend) { + // 添加到群主成员 + if (!in_array($friend['ownerWechatId'], $groupMember)) { + $groupMember[] = $friend['ownerWechatId']; + } + + if (!isset($groupMemberWechatId[$friend['id']])) { + $groupMemberWechatId[$friend['id']] = $friend['wechatId']; + $groupMemberId[] = $friend['id']; + } + } else { + // 如果不是好友ID,可能是群组ID,查询群组信息 + $group = Db::name('wechat_group') + ->where('id', $groupId) + ->where('deleteTime', 0) + ->field('ownerWechatId') + ->find(); + + if ($group && !in_array($group['ownerWechatId'], $groupMember)) { + $groupMember[] = $group['ownerWechatId']; + } + } + } else { + // 字符串ID:手动创建的群组,可能是wechatId + if (!in_array($groupId, $groupMember)) { + $groupMember[] = $groupId; + } + } + } + } + + + /** + * 记录任务开始 + * @param string $jobId + * @param string $queueLockKey + */ + protected function logJobStart($jobId, $queueLockKey) + { + // 去除开始日志,减少日志空间消耗 + } + + /** + * 处理任务成功 + * @param Job $job + * @param string $queueLockKey + */ + protected function handleJobSuccess($job, $queueLockKey) + { + $job->delete(); + Cache::rm($queueLockKey); + // 去除成功日志,减少日志空间消耗 + } + + /** + * 处理任务错误 + * @param \Exception $e + * @param Job $job + * @param string $queueLockKey + * @return bool + */ + protected function handleJobError(\Exception $e, $job, $queueLockKey) + { + Log::error('工作台消息群发任务异常:' . $e->getMessage()); + + if (!empty($queueLockKey)) { + Cache::rm($queueLockKey); + Log::info("由于异常释放队列锁: {$queueLockKey}"); + } + + if ($job->attempts() > self::MAX_RETRY_ATTEMPTS) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + + return false; + } + +} \ No newline at end of file diff --git a/application/job/WorkbenchGroupCreateOwnerFriendJob.php b/application/job/WorkbenchGroupCreateOwnerFriendJob.php new file mode 100644 index 0000000..6409720 --- /dev/null +++ b/application/job/WorkbenchGroupCreateOwnerFriendJob.php @@ -0,0 +1,109 @@ +delete(); + return true; + } + + // 初始化WebSocket + $toAccountId = ''; + $username = Env::get('api.username2', ''); + $password = Env::get('api.password2', ''); + if (!empty($username) || !empty($password)) { + $toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId'); + } + $webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]); + + // 拉群主好友进群 + $inviteResult = $webSocket->CmdChatroomInvite([ + 'wechatChatroomId' => $groupId, + 'wechatFriendIds' => $ownerFriendIds + ]); + + // 获取好友微信ID映射 + $friendWechatIds = Db::table('s2_wechat_friend') + ->where('id', 'in', $ownerFriendIds) + ->column('id,wechatId'); + + // 更新群主好友记录状态 + Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('wechatAccountId', $wechatAccountId) + ->where('status', 1) // 创建中 + ->where('memberType', self::MEMBER_TYPE_OWNER_FRIEND) + ->where('createTime', '>=', $createTime - 10) + ->where('createTime', '<=', $createTime + 10) + ->update([ + 'status' => self::STATUS_SUCCESS, + 'groupId' => $groupId, + 'chatroomId' => $chatroomId, + 'verifyTime' => time() + ]); + + Log::info("群主好友已拉入群。工作台ID: {$workbenchId}, 群ID: {$groupId}, 好友数: " . count($ownerFriendIds)); + + $job->delete(); + return true; + } catch (\Exception $e) { + Log::error("拉群主好友任务异常:{$e->getMessage()}"); + + if ($job->attempts() > self::MAX_RETRY_ATTEMPTS) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + + return false; + } + } +} + diff --git a/application/job/WorkbenchGroupCreateRetryJob.php b/application/job/WorkbenchGroupCreateRetryJob.php new file mode 100644 index 0000000..8f4ab9c --- /dev/null +++ b/application/job/WorkbenchGroupCreateRetryJob.php @@ -0,0 +1,179 @@ +find(); + if (!$workbench) { + Log::error("未找到工作台。工作台ID: {$workbenchId}"); + $job->delete(); + return false; + } + + $config = WorkbenchGroupCreate::where('workbenchId', $workbench->id)->find(); + if (!$config) { + Log::error("未找到工作台配置。工作台ID: {$workbenchId}"); + $job->delete(); + return false; + } + + // 获取失败记录 + $failedItems = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('wechatAccountId', $wechatAccountId) + ->where('createTime', '>=', $createTime - 10) + ->where('createTime', '<=', $createTime + 10) + ->where('status', 'in', [1, 3]) // 创建中或失败 + ->select(); + + if (empty($failedItems)) { + Log::info("未找到需要重试的记录。工作台ID: {$workbenchId}"); + $job->delete(); + return true; + } + + // 解析配置 + $config['poolGroups'] = json_decode($config['poolGroups'], true); + $config['devices'] = json_decode($config['devices'], true); + $config['admins'] = json_decode($config['admins'] ?? '[]', true) ?: []; + + // 获取群主成员 + $groupMember = Db::name('device_wechat_login')->alias('dwl') + ->join(['s2_wechat_account' => 'a'], 'dwl.wechatId = a.wechatId') + ->whereIn('dwl.deviceId', $config['devices']) + ->group('a.id') + ->column('a.wechatId'); + + $groupMemberWechatId = Db::table('s2_wechat_friend') + ->where('ownerWechatId', $groupMember[0]) + ->whereIn('wechatId', $groupMember) + ->column('id,wechatId'); + + $groupMemberId = array_keys($groupMemberWechatId); + + // 获取管理员好友ID + $adminFriendIds = []; + if (!empty($config['admins'])) { + $adminFriends = Db::table('s2_wechat_friend') + ->where('id', 'in', $config['admins']) + ->column('id,wechatId,ownerWechatId'); + + $accountWechatId = Db::table('s2_wechat_account')->where('id', $wechatAccountId)->value('wechatId'); + foreach ($adminFriends as $adminFriend) { + if ($adminFriend['ownerWechatId'] == $accountWechatId) { + $adminFriendIds[] = $adminFriend['id']; + } + } + } + + // 初始化WebSocket + $toAccountId = ''; + $username = Env::get('api.username2', ''); + $password = Env::get('api.password2', ''); + if (!empty($username) || !empty($password)) { + $toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId'); + } + $webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]); + + // 重新创建群 + $createFriendIds = array_merge($adminFriendIds, $groupMemberId); + + if (count($createFriendIds) < 2) { + Log::error("重试建群好友数量不足。工作台ID: {$workbenchId}"); + $job->delete(); + return false; + } + + // 生成群名称 + $existingGroupCount = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('wechatAccountId', $wechatAccountId) + ->where('status', 2) // 成功 + ->group('groupId') + ->count(); + + $chatroomName = $existingGroupCount > 0 + ? $config['groupNameTemplate'] . ($existingGroupCount + 1) . '群' + : $config['groupNameTemplate']; + + // 调用建群接口 + $createResult = $webSocket->CmdChatroomCreate([ + 'chatroomName' => $chatroomName, + 'wechatFriendIds' => $createFriendIds, + 'wechatAccountId' => $wechatAccountId + ]); + + // 更新记录状态为创建中 + Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('wechatAccountId', $wechatAccountId) + ->where('createTime', '>=', $createTime - 10) + ->where('createTime', '<=', $createTime + 10) + ->update([ + 'status' => 1, // 创建中 + 'createTime' => time() // 更新创建时间 + ]); + + // 创建新的轮询验证任务 + Queue::later(5, 'app\job\WorkbenchGroupCreateVerifyJob', [ + 'workbenchId' => $workbenchId, + 'wechatAccountId' => $wechatAccountId, + 'createTime' => time(), + 'adminFriendIds' => $adminFriendIds, + 'poolUsers' => [], // 重试时暂时不传poolUsers,后续可以优化 + ], 'default'); + + Log::info("重试建群任务已创建。工作台ID: {$workbenchId}, 微信账号ID: {$wechatAccountId}"); + + $job->delete(); + return true; + } catch (\Exception $e) { + Log::error("重试建群任务异常:{$e->getMessage()}"); + + if ($job->attempts() > self::MAX_RETRY_ATTEMPTS) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + + return false; + } + } +} + diff --git a/application/job/WorkbenchGroupCreateVerifyJob.php b/application/job/WorkbenchGroupCreateVerifyJob.php new file mode 100644 index 0000000..9c8b091 --- /dev/null +++ b/application/job/WorkbenchGroupCreateVerifyJob.php @@ -0,0 +1,248 @@ +attempts(); + + // 查询待验证的群记录 + $groupItems = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('wechatAccountId', $wechatAccountId) + ->where('status', self::STATUS_CREATING) + ->where('createTime', '>=', $createTime - 10) // 允许10秒误差 + ->where('createTime', '<=', $createTime + 10) + ->group('wechatAccountId') + ->select(); + + if (empty($groupItems)) { + // 去除信息日志,减少日志空间消耗 + $job->delete(); + return true; + } + + // 获取微信账号信息 + $wechatAccount = Db::table('s2_wechat_account')->where('id', $wechatAccountId)->find(); + if (empty($wechatAccount)) { + Log::error("未找到微信账号,任务失败。微信账号ID: {$wechatAccountId}"); + $job->delete(); + return false; + } + + // 调用接口查询群聊列表 + $chatroomController = new WechatChatroomController(); + $chatroomList = $chatroomController->getlist([ + 'wechatAccountKeyword' => $wechatAccount['wechatId'], + 'pageIndex' => 0, + 'pageSize' => 100 + ], true); + + $chatroomListData = json_decode($chatroomList, true); + + if (empty($chatroomListData['data']['results'])) { + // 如果超过最大重试次数,标记为失败并重试创建 + if ($attempts >= self::MAX_RETRY_ATTEMPTS) { + $this->handleCreateFailed($workbenchId, $wechatAccountId, $createTime, $job); + return false; + } + + // 继续轮询 + $job->release(self::POLL_INTERVAL); + return false; + } + + // 查找符合条件的群(chatroomOwnerAvatar和chatroomOwnerNickname不为空) + $successGroup = null; + foreach ($chatroomListData['data']['results'] as $chatroom) { + if (!empty($chatroom['chatroomOwnerAvatar']) && !empty($chatroom['chatroomOwnerNickname'])) { + // 检查创建时间是否匹配(允许30秒误差) + $chatroomCreateTime = isset($chatroom['createTime']) ? strtotime($chatroom['createTime']) : 0; + if (abs($chatroomCreateTime - $createTime) <= 30) { + $successGroup = $chatroom; + break; + } + } + } + + if ($successGroup) { + // 群创建成功,更新记录状态 + $groupId = $successGroup['id'] ?? 0; + $chatroomId = $successGroup['chatroomId'] ?? ''; + + // 更新管理员和群主成员的记录状态 + Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('wechatAccountId', $wechatAccountId) + ->where('status', self::STATUS_CREATING) + ->where('memberType', 'in', [1, 2]) // 群主成员和管理员 + ->where('createTime', '>=', $createTime - 10) + ->where('createTime', '<=', $createTime + 10) + ->update([ + 'status' => self::STATUS_SUCCESS, + 'groupId' => $groupId, + 'chatroomId' => $chatroomId, + 'verifyTime' => time() + ]); + + // 去除成功日志,减少日志空间消耗 + + // 3. 拉群主好友进群(在验证成功后执行) + $ownerFriendIds = $data['ownerFriendIds'] ?? []; + if (!empty($ownerFriendIds)) { + Queue::push('app\job\WorkbenchGroupCreateOwnerFriendJob', [ + 'workbenchId' => $workbenchId, + 'wechatAccountId' => $wechatAccountId, + 'groupId' => $groupId, + 'chatroomId' => $chatroomId, + 'ownerFriendIds' => $ownerFriendIds, + 'createTime' => $createTime + ], 'default'); + } + + // 5. 创建拉管理员好友的任务(在群主好友拉入后执行) + if (!empty($adminFriendIds) && !empty($poolUsers)) { + Queue::push('app\job\WorkbenchGroupCreateAdminFriendJob', [ + 'workbenchId' => $workbenchId, + 'wechatAccountId' => $wechatAccountId, + 'groupId' => $groupId, + 'chatroomId' => $chatroomId, + 'adminFriendIds' => $adminFriendIds, + 'poolUsers' => $poolUsers + ], 'default'); + } + + $job->delete(); + return true; + } else { + // 如果超过最大重试次数,标记为失败并重试创建 + if ($attempts >= self::MAX_RETRY_ATTEMPTS) { + $this->handleCreateFailed($workbenchId, $wechatAccountId, $createTime, $job); + return false; + } + + // 继续轮询 + $job->release(self::POLL_INTERVAL); + return false; + } + } catch (\Exception $e) { + Log::error("群创建验证任务异常:{$e->getMessage()}"); + + if ($job->attempts() >= self::MAX_RETRY_ATTEMPTS) { + $job->delete(); + } else { + $job->release(self::POLL_INTERVAL); + } + + return false; + } + } + + /** + * 处理创建失败的情况(重试创建) + * @param int $workbenchId 工作台ID + * @param int $wechatAccountId 微信账号ID + * @param int $createTime 创建时间 + * @param Job $job 队列任务 + */ + protected function handleCreateFailed($workbenchId, $wechatAccountId, $createTime, $job) + { + // 更新状态为失败 + Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('wechatAccountId', $wechatAccountId) + ->where('status', self::STATUS_CREATING) + ->where('createTime', '>=', $createTime - 10) + ->where('createTime', '<=', $createTime + 10) + ->update([ + 'status' => self::STATUS_FAILED, + 'verifyTime' => time() + ]); + + Log::warning("群创建失败,准备重试。工作台ID: {$workbenchId}, 微信账号ID: {$wechatAccountId}"); + + // 检查重试次数 + $failedItems = Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('wechatAccountId', $wechatAccountId) + ->where('createTime', '>=', $createTime - 10) + ->where('createTime', '<=', $createTime + 10) + ->select(); + + $maxRetryCount = 0; + foreach ($failedItems as $item) { + if ($item['retryCount'] >= 3) { + Log::error("群创建重试次数已达上限,放弃重试。工作台ID: {$workbenchId}, 微信账号ID: {$wechatAccountId}"); + $job->delete(); + return; + } + $maxRetryCount = max($maxRetryCount, $item['retryCount']); + } + + // 增加重试次数并重置状态 + Db::name('workbench_group_create_item') + ->where('workbenchId', $workbenchId) + ->where('wechatAccountId', $wechatAccountId) + ->where('createTime', '>=', $createTime - 10) + ->where('createTime', '<=', $createTime + 10) + ->update([ + 'status' => self::STATUS_CREATING, + 'retryCount' => Db::raw('retryCount + 1') + ]); + + // 重新创建建群任务(延迟10秒) + Queue::later(10, 'app\job\WorkbenchGroupCreateRetryJob', [ + 'workbenchId' => $workbenchId, + 'wechatAccountId' => $wechatAccountId, + 'createTime' => $createTime + ], 'default'); + + $job->delete(); + } +} + diff --git a/application/job/WorkbenchGroupPushJob.php b/application/job/WorkbenchGroupPushJob.php new file mode 100644 index 0000000..b14c266 --- /dev/null +++ b/application/job/WorkbenchGroupPushJob.php @@ -0,0 +1,1045 @@ +logJobStart($jobId, $queueLockKey); + $this->execute(); + $this->handleJobSuccess($job, $queueLockKey); + return true; + } catch (\Exception $e) { + return $this->handleJobError($e, $job, $queueLockKey); + } + } + + /** + * 执行任务 + * @throws \Exception + */ + public function execute() + { + try { + // 获取所有工作台 + $workbenches = Workbench::where(['status' => 1, 'type' => 3, 'isDel' => 0,'id' => 264])->order('id desc')->select(); + foreach ($workbenches as $workbench) { + // 获取工作台配置 + $configModel = WorkbenchGroupPush::where('workbenchId', $workbench->id)->find(); + if (!$configModel) { + continue; + } + + // 标准化配置 + $config = $this->normalizeConfig($configModel->toArray()); + if ($config === false) { + Log::warning("消息群发:配置无效,工作台ID: {$workbench->id}"); + continue; + } + + //判断是否推送 + $isPush = $this->isPush($workbench, $config); + if (empty($isPush)) { + continue; + } + + $targetType = intval($config['targetType']); + $groupPushSubType = intval($config['groupPushSubType']); + + // 如果是群推送且是群公告,暂时跳过(晚点处理) + if ($targetType == 1 && $groupPushSubType == 2) { + Log::info("群公告功能暂未实现,工作台ID: {$workbench->id}"); + continue; + } + + // 获取内容库(群群发需要内容库,好友推送也需要内容库) + $contentLibrary = $this->getContentLibrary($workbench, $config); + if (empty($contentLibrary)) { + continue; + } + // 处理内容发送 + $this->sendMsgToGroup($workbench, $config, $contentLibrary); + } + } catch (\Exception $e) { + Log::error("消息群发任务异常: " . $e->getMessage()); + throw $e; + } + } + + + // 发送消息(支持群推送和好友推送) + public function sendMsgToGroup($workbench, $config, $msgConf) + { + // 消息拼接 msgType(1:文本 3:图片 43:视频 47:动图表情包(gif、其他表情包) 49:小程序/其他:图文、文件) + // 当前,type 为文本、图片、动图表情包的时候,content为string, 其他情况为对象 {type: 'file/link/...', url: '', title: '', thunmbPath: '', desc: ''} + + $targetType = intval($config['targetType']); // 默认1=群推送 + + $toAccountId = ''; + $username = Env::get('api.username', ''); + $password = Env::get('api.password', ''); + if (!empty($username) || !empty($password)) { + $toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId'); + } + // 建立WebSocket + $wsController = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]); + $ownerWechatIds = $config['ownerWechatIds'] ?? $this->getOwnerWechatIds($config); + if ($targetType == 1) { + // 群推送 + $this->sendToGroups($workbench, $config, $msgConf, $wsController, $ownerWechatIds); + } else { + // 好友推送 + $this->sendToFriends($workbench, $config, $msgConf, $wsController, $ownerWechatIds); + } + } + + /** + * 发送群消息 + */ + protected function sendToGroups($workbench, $config, $msgConf, $wsController, array $ownerWechatIds = []) + { + // 获取群推送子类型:1=群群发,2=群公告 + $groupPushSubType = intval($config['groupPushSubType'] ?? 1); // 默认1=群群发 + + // 如果是群公告,暂时跳过(晚点处理) + if ($groupPushSubType == 2) { + Log::info("群公告功能暂未实现,工作台ID: {$workbench['id']}"); + return false; + } + + // 群群发:从groups字段获取群ID列表 + $groups = $config['groups'] ?? []; + if (empty($groups)) { + Log::warning("群群发:未选择微信群,工作台ID: {$workbench['id']}"); + return false; + } + + $query = Db::name('wechat_group') + ->whereIn('id', $groups); + + if (!empty($ownerWechatIds)) { + $query->whereIn('wechatAccountId', $ownerWechatIds); + } + + $groupsData = $query + ->field('id,wechatAccountId,chatroomId,companyId,ownerWechatId') + ->select(); + if (empty($groupsData)) { + Log::warning("群群发:未找到微信群数据,工作台ID: {$workbench['id']}"); + return false; + } + + foreach ($msgConf as $content) { + $sqlData = []; + + foreach ($groupsData as $group) { + // 构建发送数据 + $sendData = $this->buildSendData($content, $config, $group['wechatAccountId'], $group['id'], 'group'); + if (empty($sendData)) { + continue; + } + + //发送消息 + foreach ($sendData as $send) { + $wsController->sendCommunity($send); + } + + // 准备插入发送记录 + $sqlData[] = [ + 'workbenchId' => $workbench['id'], + 'contentId' => $content['id'], + 'groupId' => $group['id'], + 'friendId' => null, + 'targetType' => 1, + 'wechatAccountId' => $group['wechatAccountId'], + 'createTime' => time() + ]; + } + + // 批量插入发送记录 + if (!empty($sqlData)) { + Db::name('workbench_group_push_item')->insertAll($sqlData); + Log::info("群群发:推送了" . count($sqlData) . "个群,工作台ID: {$workbench['id']}"); + } + } + + return true; + } + + /** + * 发送好友消息 + */ + protected function sendToFriends($workbench, $config, $msgConf, $wsController, array $ownerWechatIds = []) + { + $friends = $config['friends'] ?? []; + $trafficPools = $config['trafficPools'] ?? []; + $devices = $config['devices'] ?? []; + + $friendsData = []; + + // 指定好友 + if (!empty($friends)) { + $friendsData = array_merge($friendsData, $this->getFriendsByIds($friends, $ownerWechatIds)); + } + + // 流量池好友 + if (!empty($trafficPools)) { + $friendsData = array_merge($friendsData, $this->getFriendsByTrafficPools($trafficPools, $workbench, $ownerWechatIds)); + } + + // 如果未选择好友或流量池,则根据设备查询所有好友 + if (empty($friendsData)) { + if (empty($devices)) { + Log::warning('好友推送:未选择好友或流量池,且未选择设备,无法推送'); + return false; + } + $friendsData = $this->getFriendsByDevices($devices, $ownerWechatIds); + } + $friendsData = $this->deduplicateFriends($friendsData); + if (empty($friendsData)) { + return false; + } + + // 获取已推送的好友ID列表(不限制时间范围,避免重复推送) + $sentFriendIds = Db::name('workbench_group_push_item') + ->where('workbenchId', $workbench->id) + ->where('targetType', 2) + ->column('friendId'); + $sentFriendIds = array_unique(array_filter($sentFriendIds)); + + // 过滤掉所有已推送的好友 + $friendsData = array_filter($friendsData, function($friend) use ($sentFriendIds) { + return !in_array($friend['id'], $sentFriendIds); + }); + + if (empty($friendsData)) { + Log::info('好友推送:所有好友都已推送过'); + return false; + } + + // 重新索引数组 + $friendsData = array_values($friendsData); + + // 计算剩余可推送人数(基于累计推送人数) + $sentFriendCount = count($sentFriendIds); + $maxPerDay = intval($config['maxPerDay']); + $remainingCount = $maxPerDay - $sentFriendCount; + + if ($remainingCount <= 0) { + Log::info('好友推送:累计推送人数已达上限'); + return false; + } + + // 限制本次推送人数(不超过剩余可推送人数) + $friendsData = array_slice($friendsData, 0, $remainingCount); + + // 批量处理:每批最多500人 + $batchSize = 500; + $batches = array_chunk($friendsData, $batchSize); + + foreach ($msgConf as $content) { + foreach ($batches as $batchIndex => $batch) { + $sqlData = []; + + foreach ($batch as $friend) { + // 构建发送数据 + $sendData = $this->buildSendData($content, $config, $friend['wechatAccountId'], $friend['id'], 'friend'); + + if (empty($sendData)) { + continue; + } + + // 发送个人消息 + foreach ($sendData as $send) { + if ($send['msgType'] == 49){ + $sendContent = json_encode($send['content'], 256); + } else { + $sendContent = $send['content']; + } + $wsController->sendPersonal([ + 'wechatFriendId' => $friend['id'], + 'wechatAccountId' => $friend['wechatAccountId'], + 'msgType' => $send['msgType'], + 'content' => $sendContent, + ]); + } + + // 准备插入发送记录 + $sqlData[] = [ + 'workbenchId' => $workbench['id'], + 'contentId' => $content['id'], + 'groupId' => null, + 'friendId' => $friend['id'], + 'targetType' => 2, + 'wechatAccountId' => $friend['wechatAccountId'], + 'createTime' => time() + ]; + } + + // 批量插入发送记录 + if (!empty($sqlData)) { + Db::name('workbench_group_push_item')->insertAll($sqlData); + Log::info("好友推送:第" . ($batchIndex + 1) . "批,推送了" . count($sqlData) . "个好友"); + } + + // 如果不是最后一批,等待一下再处理下一批(避免一次性推送太多) + if ($batchIndex < count($batches) - 1) { + sleep(1); // 等待1秒 + } + } + } + } + + /** + * 构建发送数据 + */ + protected function buildSendData($content, $config, $wechatAccountId, $targetId, $type = 'group') + { + $sendData = []; + + // 内容处理 + if (!empty($content['content'])) { + // 京东转链 + if (!empty($config['promotionSiteId'])) { + $WorkbenchController = new WorkbenchController(); + $jdLink = $WorkbenchController->changeLink($content['content'], $config['promotionSiteId']); + $jdLink = json_decode($jdLink, true); + if ($jdLink['code'] == 200) { + $content['content'] = $jdLink['data']; + } + } + + if ($type == 'group') { + $sendData[] = [ + 'content' => $content['content'], + 'msgType' => 1, + 'wechatAccountId' => $wechatAccountId, + 'wechatChatroomId' => $targetId, + ]; + } else { + $sendData[] = [ + 'content' => $content['content'], + 'msgType' => 1, + ]; + } + } + + // 根据内容类型处理 + switch ($content['contentType']) { + case 1: + // 图片解析 + $imgs = json_decode($content['resUrls'], true); + if (!empty($imgs)) { + foreach ($imgs as $img) { + if ($type == 'group') { + $sendData[] = [ + 'content' => $img, + 'msgType' => 3, + 'wechatAccountId' => $wechatAccountId, + 'wechatChatroomId' => $targetId, + ]; + } else { + $sendData[] = [ + 'content' => $img, + 'msgType' => 3, + ]; + } + } + } + break; + case 2: + // 链接解析 + $url = json_decode($content['urls'], true); + if (!empty($url[0])) { + $url = $url[0]; + $linkContent = [ + 'desc' => $url['desc'], + 'thumbPath' => $url['image'], + 'title' => $url['desc'], + 'type' => 'link', + 'url' => $url['url'], + ]; + if ($type == 'group') { + $sendData[] = [ + 'content' => $linkContent, + 'msgType' => 49, + 'wechatAccountId' => $wechatAccountId, + 'wechatChatroomId' => $targetId, + ]; + } else { + $sendData[] = [ + 'content' => $linkContent, + 'msgType' => 49, + ]; + } + } + break; + case 3: + // 视频解析 + $video = json_decode($content['resUrls'], true); + if (!empty($video)) { + $video = $video[0]; + } + if ($type == 'group') { + $sendData[] = [ + 'content' => $video, + 'msgType' => 43, + 'wechatAccountId' => $wechatAccountId, + 'wechatChatroomId' => $targetId, + ]; + } else { + $sendData[] = [ + 'content' => $video, + 'msgType' => 43, + ]; + } + break; + } + + return $sendData; + } + + /** + * 根据好友ID获取好友信息 + * @param array $friendIds + * @return array + */ + protected function getFriendsByIds(array $friendIds, array $ownerWechatIds = []) + { + if (empty($friendIds)) { + return []; + } + $query = Db::table('s2_wechat_friend') + ->whereIn('id', $friendIds) + ->where('isDeleted', 0); + + if (!empty($ownerWechatIds)) { + $query->whereIn('wechatAccountId', $ownerWechatIds); + } + + $friends = $query + ->field('id,wechatAccountId,wechatId,ownerWechatId') + ->select(); + if ($friends === false) { + return []; + } + + return $friends; + } + + /** + * 根据设备获取好友信息 + * @param array $deviceIds + * @return array + */ + protected function getFriendsByDevices(array $deviceIds, array $ownerWechatIds = []) + { + if (empty($deviceIds)) { + return []; + } + + $query = Db::table('s2_company_account') + ->alias('ca') + ->join(['s2_wechat_account' => 'wa'], 'ca.id = wa.deviceAccountId') + ->join(['s2_wechat_friend' => 'wf'], 'wf.wechatAccountId = wa.id') + ->where([ + 'ca.status' => 0, + 'wf.isDeleted' => 0, + 'wa.deviceAlive' => 1, + 'wa.wechatAlive' => 1 + ]) + ->whereIn('wa.currentDeviceId', $deviceIds); + + if (!empty($ownerWechatIds)) { + $query->whereIn('wf.wechatAccountId', $ownerWechatIds); + } + + $friends = $query + ->field('wf.id,wf.wechatAccountId,wf.wechatId,wf.ownerWechatId') + ->group('wf.id') + ->select(); + + if ($friends === false) { + return []; + } + + return $friends->toArray(); + } + + /** + * 根据流量池获取好友信息 + * @param array $trafficPools + * @param Workbench $workbench + * @return array + */ + protected function getFriendsByTrafficPools(array $trafficPools, $workbench, array $ownerWechatIds = []) + { + if (empty($trafficPools)) { + return []; + } + + $companyId = $workbench->companyId ?? 0; + + $query = Db::name('traffic_source_package_item') + ->alias('tspi') + ->leftJoin('traffic_source_package tsp', 'tsp.id = tspi.packageId') + ->leftJoin('traffic_pool tp', 'tp.identifier = tspi.identifier') + ->leftJoin(['s2_wechat_friend' => 'wf'], 'wf.wechatId = tp.wechatId') + ->leftJoin(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId') + ->whereIn('tspi.packageId', $trafficPools) + ->where('tsp.isDel', 0) + ->where('wf.isDeleted', 0) + ->whereNotNull('wf.id') + ->whereNotNull('wf.wechatAccountId') + ->where(function ($query) use ($companyId) { + $query->whereIn('tsp.companyId', [$companyId, 0]); + }) + ->where(function ($query) use ($companyId) { + $query->whereIn('tspi.companyId', [$companyId, 0]); + }); + + if (!empty($ownerWechatIds)) { + $query->whereIn('wf.wechatAccountId', $ownerWechatIds); + } + + $friends = $query + ->field('wf.id,wf.wechatAccountId,wf.wechatId,wf.ownerWechatId') + ->group('wf.id') + ->select(); + + if (empty($friends)) { + Log::info('好友推送:流量池未匹配到好友'); + return []; + } + + if ($friends === false) { + return []; + } + + return $friends; + } + + /** + * 标准化群推送配置 + * @param array $config + * @return array|false + */ + protected function normalizeConfig(array $config) + { + $config['targetType'] = intval($config['targetType'] ?? 1); + $config['groupPushSubType'] = intval($config['groupPushSubType'] ?? 1); + if (!in_array($config['groupPushSubType'], [1, 2], true)) { + $config['groupPushSubType'] = 1; + } + + $config['pushType'] = !empty($config['pushType']) ? 1 : 0; + $config['status'] = !empty($config['status']) ? 1 : 0; + $config['isLoop'] = !empty($config['isLoop']) ? 1 : 0; + + $config['startTime'] = $this->normalizeTimeString($config['startTime'] ?? '00:00'); + $config['endTime'] = $this->normalizeTimeString($config['endTime'] ?? '23:59'); + $config['maxPerDay'] = max(0, intval($config['maxPerDay'] ?? 0)); + + $config['friendIntervalMin'] = max(0, intval($config['friendIntervalMin'] ?? 0)); + $config['friendIntervalMax'] = max(0, intval($config['friendIntervalMax'] ?? $config['friendIntervalMin'])); + if ($config['friendIntervalMin'] > $config['friendIntervalMax']) { + $config['friendIntervalMax'] = $config['friendIntervalMin']; + } + + $config['messageIntervalMin'] = max(0, intval($config['messageIntervalMin'] ?? 0)); + $config['messageIntervalMax'] = max(0, intval($config['messageIntervalMax'] ?? $config['messageIntervalMin'])); + if ($config['messageIntervalMin'] > $config['messageIntervalMax']) { + $config['messageIntervalMax'] = $config['messageIntervalMin']; + } + + $config['ownerWechatIds'] = $this->deduplicateIds($this->jsonToArray($config['ownerWechatIds'] ?? [])); + $config['groups'] = $this->deduplicateIds($this->jsonToArray($config['groups'] ?? [])); + $config['friends'] = $this->deduplicateIds($this->jsonToArray($config['friends'] ?? [])); + $config['trafficPools'] = $this->deduplicateIds($this->jsonToArray($config['trafficPools'] ?? [])); + $config['devices'] = $this->deduplicateIds($this->jsonToArray($config['devices'] ?? [])); + $config['contentLibraries'] = $this->deduplicateIds($this->jsonToArray($config['contentLibraries'] ?? [])); + $config['postPushTags'] = $this->deduplicateIds($this->jsonToArray($config['postPushTags'] ?? [])); + + return $config; + } + + /** + * 将混合类型转换为数组 + * @param mixed $value + * @return array + */ + protected function jsonToArray($value): array + { + if (empty($value)) { + return []; + } + + if (is_array($value)) { + return $value; + } + + if (is_string($value)) { + $decoded = json_decode($value, true); + if (json_last_error() === JSON_ERROR_NONE) { + return is_array($decoded) ? $decoded : []; + } + } + + return []; + } + + /** + * 归一化时间字符串,保留到分钟 + * @param string $time + * @return string + */ + protected function normalizeTimeString(string $time): string + { + if (empty($time)) { + return '00:00'; + } + $parts = explode(':', $time); + $hour = str_pad(intval($parts[0] ?? 0), 2, '0', STR_PAD_LEFT); + $minute = str_pad(intval($parts[1] ?? 0), 2, '0', STR_PAD_LEFT); + return "{$hour}:{$minute}"; + } + + /** + * 对ID数组进行去重并清理无效值 + * @param array $ids + * @return array + */ + protected function deduplicateIds(array $ids) + { + if (empty($ids)) { + return []; + } + + $normalized = array_map(function ($value) { + if (is_array($value) && isset($value['id'])) { + return $value['id']; + } + if (is_object($value) && isset($value->id)) { + return $value->id; + } + return $value; + }, $ids); + + $filtered = array_filter($normalized, function ($value) { + return $value !== null && $value !== ''; + }); + + if (empty($filtered)) { + return []; + } + + return array_values(array_unique($filtered)); + } + + /** + * 对内容列表根据内容ID去重 + * @param mixed $contents + * @return array + */ + protected function deduplicateContentList($contents) + { + if (empty($contents)) { + return []; + } + + if ($contents instanceof \think\Collection || $contents instanceof \think\model\Collection) { + $contents = $contents->toArray(); + } elseif ($contents instanceof \think\Model) { + $contents = [$contents->toArray()]; + } + + if (!is_array($contents)) { + return []; + } + + $result = []; + $unique = []; + + foreach ($contents as $content) { + if ($content instanceof \think\Model) { + $content = $content->toArray(); + } elseif (is_object($content)) { + $content = (array)$content; + } + + if (!is_array($content)) { + continue; + } + + $contentId = $content['id'] ?? null; + if (empty($contentId) || isset($unique[$contentId])) { + continue; + } + + $unique[$contentId] = true; + $result[] = $content; + } + + return $result; + } + + /** + * 对好友数据进行去重 + * @param array $friends + * @return array + */ + protected function deduplicateFriends(array $friends) + { + if (empty($friends)) { + return []; + } + + $unique = []; + $result = []; + + foreach ($friends as $friend) { + if (empty($friend['id'])) { + continue; + } + if (isset($unique[$friend['id']])) { + continue; + } + $unique[$friend['id']] = true; + $result[] = $friend; + } + + return $result; + } + + /** + * 获取配置中的客服微信ID列表 + * @param array $config + * @return array + */ + protected function getOwnerWechatIds($config) + { + if (empty($config['ownerWechatIds'])) { + return []; + } + + $ownerWechatIds = $config['ownerWechatIds']; + + if (is_string($ownerWechatIds)) { + $decoded = json_decode($ownerWechatIds, true); + if (json_last_error() === JSON_ERROR_NONE) { + $ownerWechatIds = $decoded; + } + } + + if (!is_array($ownerWechatIds)) { + return []; + } + + $ownerWechatIds = array_map(function ($id) { + return is_numeric($id) ? intval($id) : $id; + }, $ownerWechatIds); + + return $this->deduplicateIds($ownerWechatIds); + } + + + /** + * 记录发送历史 + * @param Workbench $workbench + * @param array $devices + * @param array $contentLibrary + */ + protected function recordSendHistory($workbench, $devices, $contentLibrary) + { + $now = time(); + $data = []; + foreach ($devices as $device) { + $data = [ + 'workbenchId' => $workbench->id, + 'deviceId' => $device['deviceId'], + 'contentId' => $contentLibrary['id'], + 'wechatAccountId' => $device['wechatAccountId'], + 'createTime' => $now, + ]; + Db::name('workbench_group_push_item')->insert($data); + } + + } + + /** + * 判断是否推送 + * @param Workbench $workbench 工作台 + * @param array $config 配置 + * @return bool + */ + protected function isPush($workbench, $config) + { + // 检查发送间隔(新逻辑:根据startTime、endTime、maxPerDay动态计算) + $today = date('Y-m-d'); + $startTimestamp = strtotime($today . ' ' . $config['startTime'] . ':00'); + $endTimestamp = strtotime($today . ' ' . $config['endTime'] . ':00'); + + // 如果时间不符,则跳过 + if (($startTimestamp > time() || $endTimestamp < time()) && empty($config['pushType'])) { + return false; + } + + $totalSeconds = $endTimestamp - $startTimestamp; + if ($totalSeconds <= 0 || empty($config['maxPerDay'])) { + return false; + } + + $targetType = intval($config['targetType']); // 默认1=群推送 + + if ($targetType == 2) { + // 好友推送:maxPerDay表示每日推送人数 + // 查询已推送的好友ID列表(去重) + $sentFriendIds = Db::name('workbench_group_push_item') + ->where('workbenchId', $workbench->id) + ->where('targetType', 2) + ->column('friendId'); + $sentFriendIds = array_filter($sentFriendIds); // 过滤null值 + $count = count(array_unique($sentFriendIds)); // 去重后统计累计推送人数 + + if ($count >= $config['maxPerDay']) { + return false; + } + + // 计算本次同步的最早允许时间(基于好友/消息间隔配置) + $friendIntervalMin = max(0, intval($config['friendIntervalMin'] ?? 0)); + $messageIntervalMin = max(0, intval($config['messageIntervalMin'] ?? 0)); + $minInterval = max(1, $friendIntervalMin + $messageIntervalMin); + + $lastSendTime = Db::name('workbench_group_push_item') + ->where('workbenchId', $workbench->id) + ->where('targetType', 2) + ->order('id', 'desc') + ->value('createTime'); + + if (!empty($lastSendTime) && (time() - $lastSendTime) < $minInterval) { + return false; + } + } else { + // 群推送:maxPerDay表示每日推送次数 + $interval = floor($totalSeconds / $config['maxPerDay']); + + // 查询今日已同步次数 + $count = Db::name('workbench_group_push_item') + ->where('workbenchId', $workbench->id) + ->where('targetType', 1) + ->whereTime('createTime', 'between', [$startTimestamp, $endTimestamp]) + ->count(); + if ($count >= $config['maxPerDay']) { + return false; + } + + // 计算本次同步的最早允许时间 + $nextSyncTime = $startTimestamp + $count * $interval; + if (time() < $nextSyncTime) { + return false; + } + } + + return true; + } + + /** + * 获取内容库 + * @param Workbench $workbench 工作台 + * @param array $config 配置 + * @return array|bool + */ + protected function getContentLibrary($workbench, $config) + { + $targetType = intval($config['targetType']); // 默认1=群推送 + $groupPushSubType = intval($config['groupPushSubType']); // 默认1=群群发 + + // 如果是群公告,不需要内容库(晚点处理) + if ($targetType == 1 && $groupPushSubType == 2) { + return false; + } + + $contentids = $config['contentLibraries'] ?? []; + if (empty($contentids)) { + Log::warning("未选择内容库,工作台ID: {$workbench->id}"); + return false; + } + + if ($config['pushType'] == 1) { + $limit = 10; + } else { + $limit = 1; + } + + //推送顺序 + if ($config['pushOrder'] == 1) { + $order = 'ci.sendTime desc, ci.id asc'; + } else { + $order = 'ci.sendTime desc, ci.id desc'; + } + + // 基础查询,根据targetType过滤记录 + $query = Db::name('content_library')->alias('cl') + ->join('content_item ci', 'ci.libraryId = cl.id') + ->join('workbench_group_push_item wgpi', 'wgpi.contentId = ci.id and wgpi.workbenchId = ' . $workbench->id . ' and wgpi.targetType = ' . $targetType, 'left') + ->where(['cl.isDel' => 0, 'ci.isDel' => 0]) + ->where('ci.sendTime <= ' . (time() + 60)) + ->whereIn('cl.id', $contentids) + ->field([ + 'ci.id', + 'ci.libraryId', + 'ci.contentType', + 'ci.title', + 'ci.content', + 'ci.resUrls', + 'ci.urls', + 'ci.comment', + 'ci.sendTime' + ]); + // 复制 query + $query2 = clone $query; + $query3 = clone $query; + // 根据isLoop处理不同的发送逻辑 + if ($config['isLoop'] == 1) { + // 可以循环发送(只有群推送时才能为1) + // 1. 优先获取未发送的内容 + $unsentContent = $this->deduplicateContentList( + $query->where('wgpi.id', 'null') + ->order($order) + ->limit(0, $limit) + ->select() + ); + if (!empty($unsentContent)) { + return $unsentContent; + } + $lastSendData = Db::name('workbench_group_push_item') + ->where('workbenchId', $workbench->id) + ->where('targetType', $targetType) + ->order('id desc') + ->find(); + $fastSendData = Db::name('workbench_group_push_item') + ->where('workbenchId', $workbench->id) + ->where('targetType', $targetType) + ->order('id asc') + ->find(); + + if (empty($lastSendData) || empty($fastSendData)) { + return []; + } + + $sentContent = $this->deduplicateContentList( + $query2->where('wgpi.contentId', '<', $lastSendData['contentId']) + ->order('wgpi.id ASC') + ->group('wgpi.contentId') + ->limit(0, $limit) + ->select() + ); + + if (empty($sentContent)) { + $sentContent = $this->deduplicateContentList( + $query3->where('wgpi.contentId', '=', $fastSendData['contentId']) + ->order('wgpi.id ASC') + ->group('wgpi.contentId') + ->limit(0, $limit) + ->select() + ); + } + return $sentContent; + } else { + // 不能循环发送,只获取未发送的内容(好友推送时isLoop=0) + $list = $this->deduplicateContentList( + $query->where('wgpi.id', 'null') + ->order($order) + ->limit(0, $limit) + ->select() + ); + return $list; + } + } + + /** + * 记录任务开始 + * @param string $jobId + * @param string $queueLockKey + */ + protected function logJobStart($jobId, $queueLockKey) + { + Log::info('开始处理工作台消息群发任务: ' . json_encode([ + 'jobId' => $jobId, + 'queueLockKey' => $queueLockKey + ])); + } + + /** + * 处理任务成功 + * @param Job $job + * @param string $queueLockKey + */ + protected function handleJobSuccess($job, $queueLockKey) + { + $job->delete(); + Cache::rm($queueLockKey); + Log::info('工作台消息群发任务执行成功'); + } + + /** + * 处理任务错误 + * @param \Exception $e + * @param Job $job + * @param string $queueLockKey + * @return bool + */ + protected function handleJobError(\Exception $e, $job, $queueLockKey) + { + Log::error('工作台消息群发任务异常:' . $e->getMessage()); + + if (!empty($queueLockKey)) { + Cache::rm($queueLockKey); + Log::info("由于异常释放队列锁: {$queueLockKey}"); + } + + if ($job->attempts() > self::MAX_RETRY_ATTEMPTS) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + + return false; + } +} \ No newline at end of file diff --git a/application/job/WorkbenchImportContactJob.php b/application/job/WorkbenchImportContactJob.php new file mode 100644 index 0000000..a8edfba --- /dev/null +++ b/application/job/WorkbenchImportContactJob.php @@ -0,0 +1,399 @@ +logJobStart($jobId, $queueLockKey); + $this->execute(); + $this->handleJobSuccess($job, $queueLockKey); + return true; + } catch (\Exception $e) { + return $this->handleJobError($e, $job, $queueLockKey); + } + } + + /** + * 执行任务 + * @throws \Exception + */ + public function execute() + { + try { + // 获取所有启用的通讯录导入工作台 + $workbenches = Workbench::where(['status' => 1, 'type' => 6, 'isDel' => 0])->order('id desc')->select(); + foreach ($workbenches as $workbench) { + // 获取工作台配置 + $config = WorkbenchImportContact::where('workbenchId', $workbench->id)->find(); + if (!$config) { + continue; + } + + // 判断是否需要导入 + $shouldImport = $this->shouldImport($workbench, $config); + if (!$shouldImport) { + continue; + } + + // 获取需要导入的设备列表 + $devices = $this->getDeviceList($workbench, $config); + if (empty($devices)) { + continue; + } + + // 获取通讯录数据 + $contactData = $this->getContactFromDatabase($workbench, $config); + if (empty($contactData)) { + continue; + } + + // 执行通讯录导入 + $this->importContactToDevices($workbench, $config, $devices, $contactData); + } + } catch (\Exception $e) { + Log::error("通讯录导入任务异常: " . $e->getMessage()); + throw $e; + } + } + + /** + * 导入通讯录到设备 + * @param Workbench $workbench + * @param WorkbenchImportContact $config + * @param array $devices + * @param array $contactData + */ + public function importContactToDevices($workbench, $config, $devices, $contactData) + { + $deviceController = new DeviceController(); + + // 根据设备数量平分通讯录数据 + $deviceCount = count($devices); + if ($deviceCount == 0) { + Log::warning("没有可用设备进行通讯录导入"); + return; + } + + $contactCount = count($contactData); + if ($contactCount == 0) { + Log::warning("没有通讯录数据需要导入"); + return; + } + + // 计算每个设备分配的联系人数量 + $contactsPerDevice = ceil($contactCount / $deviceCount); + foreach ($devices as $index => $device) { + try { + // 计算当前设备的联系人数据范围 + $startIndex = $index * $contactsPerDevice; + $endIndex = min($startIndex + $contactsPerDevice, $contactCount); + + // 如果起始索引超出范围,跳过 + if ($startIndex >= $contactCount) { + continue; + } + + // 获取当前设备的联系人数据片段 + $deviceContactData = array_slice($contactData, $startIndex, $endIndex - $startIndex); + + if (empty($deviceContactData)) { + continue; + } + // 准备联系人数据 + $contactJson = $this->formatContactData($deviceContactData, $config); + + // 调用设备控制器的导入联系人方法 + $result = $deviceController->importContact([ + 'deviceId' => $device['deviceId'], + 'contactJson' => $contactJson, + 'clearContact' => $config['clearContact'] ?? false + ], true); + + $resultData = json_decode($result, true); + + // 记录导入历史 + $this->recordImportHistory($workbench, $device, $deviceContactData); + + if ($resultData['code'] == 200) { + Log::info("设备 {$device['deviceId']} 通讯录导入成功,导入联系人数量: " . count($deviceContactData)); + } else { + Log::error("设备 {$device['deviceId']} 通讯录导入失败: " . ($resultData['msg'] ?? '未知错误')); + } + + // 添加延迟,避免频繁请求 + if ($config['importInterval'] ?? 0 > 0) { + sleep($config['importInterval']); + } + + } catch (\Exception $e) { + Log::error("设备 {$device['deviceId']} 通讯录导入异常: " . $e->getMessage()); + } + } + } + + /** + * 格式化联系人数据 + * @param array $contactData + * @param WorkbenchImportContact $config + * @return array|string + */ + protected function formatContactData($contactData, $config) + { + $remarkType = $config['remarkType'] ?? 0; + $remark = $config['remark'] ?? ''; + + // 根据remarkType添加备注 + $suffix = ''; + switch ($remarkType) { + case 0: + // 不添加备注 + $suffix = ''; + break; + case 1: + // 添加年月日 + $suffix = date('Ymd') . '_'; + break; + case 2: + // 添加月日 + $suffix = date('md') . '_'; + break; + case 3: + // 自定义备注 + $suffix = $remark . '_'; + break; + default: + $suffix = ''; + break; + } + // 返回数组格式 + $contacts = []; + foreach ($contactData as $contact) { + $name = !empty($contact['name']) ? trim($contact['name']) : trim($contact['phone']); + if (!empty($suffix)) { + $name = $suffix . $name; + } + $contacts[] = [ + 'name' => $name, + 'phone' => trim($contact['phone']) + ]; + } + return $contacts; + + } + + /** + * 记录导入历史 + * @param Workbench $workbench + * @param array $device + * @param array $contactData + * @param array $result + */ + protected function recordImportHistory($workbench, $device, $contactData) + { + $data = []; + foreach ($contactData as $v){ + $data[] = [ + 'workbenchId' => $workbench->id, + 'deviceId' => $device['deviceId'], + 'packageId' => !empty($v['packageId']) ? $v['packageId'] : 0, + 'poolId' => !empty($v['id']) ? $v['id'] : 0, + 'createTime' => time(), + ]; + } + Db::name('workbench_import_contact_item')->insertAll($data); + } + + /** + * 获取设备列表 + * @param Workbench $workbench 工作台 + * @param WorkbenchImportContact $config 配置 + * @return array + */ + protected function getDeviceList($workbench, $config) + { + $deviceIds = json_decode($config['devices'], true); + if (empty($deviceIds)) { + return []; + } + + // 从数据库获取设备信息 + $devices = Db::table('s2_device') + ->whereIn('id', $deviceIds) + ->where('isDeleted', 0) + ->where('alive', 1) // 只选择在线设备 + ->field('id as deviceId, imei, nickname') + ->select(); + + return $devices; + } + + /** + * 判断是否需要导入 + * @param Workbench $workbench 工作台 + * @param WorkbenchImportContact $config 配置 + * @return bool + */ + protected function shouldImport($workbench, $config) + { + // 检查导入间隔 + $today = date('Y-m-d'); + $startTimestamp = strtotime($today . ' ' . $config['startTime'] . ':00'); + $endTimestamp = strtotime($today . ' ' . $config['endTime'] . ':00'); + // 如果不在指定时间范围内,则跳过 + if ($startTimestamp > time() || $endTimestamp < time()) { + return false; + } + + $maxPerDay = $config['num']; + if ($maxPerDay <= 0) { + return false; + } + + // 查询今日已导入次数 + $count = Db::name('workbench_import_contact_item') + ->where('workbenchId', $workbench->id) + ->whereTime('createTime', 'between', [$startTimestamp, $endTimestamp]) + ->count(); + + if ($count >= $maxPerDay) { + return false; + } + + // 计算导入间隔 + $totalSeconds = $endTimestamp - $startTimestamp; + $interval = floor($totalSeconds / $maxPerDay); + $nextImportTime = $startTimestamp + $count * $interval; + + if (time() < $nextImportTime) { + return false; + } + + return true; + } + + + + /** + * 从数据库读取通讯录 + * @param WorkbenchImportContact $config + * @return array + */ + protected function getContactFromDatabase($workbench,$config) + { + $pools = json_decode($config['pools'], true); + $deviceIds = json_decode($config['devices'], true); + if (empty($pools) || empty($deviceIds)) { + return false; + } + $deviceNum = count($deviceIds); + $contactNum = $deviceNum * $config['num']; + if (empty($contactNum)) { + return false; + } + //过滤已删除的数据 + $packageIds = Db::name('traffic_source_package') + ->where(['isDel' => 0]) + ->whereIn('id', $pools) + ->column('id'); + + if (empty($packageIds)) { + return false; + } + + $data = Db::name('traffic_source_package_item')->alias('tpi') + ->join('traffic_pool tp', 'tp.identifier = tpi.identifier') + ->join('traffic_source ts', 'ts.identifier = tpi.identifier','left') + ->join('workbench_import_contact_item wici', 'wici.poolId = tp.id AND wici.workbenchId = '.$workbench->id,'left') + ->where('tp.mobile', '>',0) + ->where('wici.id','null') + ->whereIn('tpi.packageId',$packageIds) + ->field('tp.id,tpi.packageId,tp.mobile as phone,ts.name') + ->order('tp.id DESC') + ->group('tpi.identifier') + ->limit($contactNum) + ->select(); + return $data; + } + + /** + * 记录任务开始 + * @param string $jobId + * @param string $queueLockKey + */ + protected function logJobStart($jobId, $queueLockKey) + { + Log::info('开始处理工作台通讯录导入任务: ' . json_encode([ + 'jobId' => $jobId, + 'queueLockKey' => $queueLockKey + ])); + } + + /** + * 处理任务成功 + * @param Job $job + * @param string $queueLockKey + */ + protected function handleJobSuccess($job, $queueLockKey) + { + $job->delete(); + Cache::rm($queueLockKey); + Log::info('工作台通讯录导入任务执行成功'); + } + + /** + * 处理任务错误 + * @param \Exception $e + * @param Job $job + * @param string $queueLockKey + * @return bool + */ + protected function handleJobError(\Exception $e, $job, $queueLockKey) + { + Log::error('工作台通讯录导入任务异常:' . $e->getMessage()); + + if (!empty($queueLockKey)) { + Cache::rm($queueLockKey); + Log::info("由于异常释放队列锁: {$queueLockKey}"); + } + + if ($job->attempts() > self::MAX_RETRY_ATTEMPTS) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + + return false; + } +} \ No newline at end of file diff --git a/application/job/WorkbenchMomentsJob.php b/application/job/WorkbenchMomentsJob.php new file mode 100644 index 0000000..62db79d --- /dev/null +++ b/application/job/WorkbenchMomentsJob.php @@ -0,0 +1,562 @@ + 1, // 未知 -> 文本 + 1 => 2, // 图片 -> 图文 + 2 => 4, // 链接 -> 链接 + 3 => 3, // 视频 -> 视频 + 4 => 1, // 文本 -> 文本 + 5 => 1, // 小程序 -> 文本 + 6 => 2, // 图文 -> 图文 + ]; + + /** + * 最大重试次数 + */ + const MAX_RETRY_ATTEMPTS = 3; + + /** + * 队列任务处理 + * @param Job $job 队列任务 + * @param array $data 任务数据 + * @return bool + */ + public function fire(Job $job, $data) + { + $jobId = $data['jobId'] ?? ''; + $queueLockKey = $data['queueLockKey'] ?? ''; + try { + $this->logJobStart($jobId, $queueLockKey); + $this->execute2(); + $this->execute(); + $this->handleJobSuccess($job, $queueLockKey); + return true; + } catch (\Exception $e) { + return $this->handleJobError($e, $job, $queueLockKey); + } + } + + /** + * 执行任务 + * @throws \Exception + */ + public function execute() + { + try { + // 获取所有工作台 + $workbenches = Workbench::where(['status' => 1, 'type' => 2, 'isDel' => 0])->order('id desc')->select(); + foreach ($workbenches as $workbench) { + // 获取工作台配置 + $config = WorkbenchMoments::where('workbenchId', $workbench->id)->find(); + if (!$config) { + continue; + } + $startTime = strtotime(date('Y-m-d ' . $config['startTime'])); + $endTime = strtotime(date('Y-m-d ' . $config['endTime'])); + // 如果时间不符,则跳过 + if ($startTime > time() || $endTime < time()) { + continue; + } + + // 获取设备 + $devices = $this->getDevice($workbench, $config); + if (empty($devices)) { + continue; + } + + // 获取内容库 + $contentLibrary = $this->getContentLibrary($workbench, $config); + if (empty($contentLibrary)) { + continue; + } + // 处理内容发送 + $this->handleContentSend($workbench, $config, $devices, $contentLibrary); + } + } catch (\Exception $e) { + Log::error("朋友圈同步任务异常: " . $e->getMessage()); + throw $e; + } + } + + public function execute2() + { + try { + // 1) 每日重置 + $this->resetDailyCountersIfNeeded(); + + // 2) 获取发送窗口内的任务 + [$nowTs, $kfMoments] = $this->getWindowTasks(); + foreach ($kfMoments as $val) { + $companyId = (int)($val['companyId'] ?? 0); + $userId = (int)($val['userId'] ?? 0); + + // 2.1) 数据规范化 + $sendData = json_decode($val->sendData, true); + $sendData = $this->normalizeSendData($sendData); + + // 2.2) 账号额度过滤 + $items = $sendData['jobPublishWechatMomentsItems'] ?? []; + if (empty($items)) { continue; } + $allowed = $this->filterAccountsByQuota($companyId, $userId, $items); + if (empty($allowed)) { continue; } + $sendData['jobPublishWechatMomentsItems'] = $allowed; + + // 3) 下发 + $moments = new Moments(); + $res = $moments->addJob($sendData); + $res = json_decode($res, true); + if ($res['code'] == 200){ + KfMoments::where(['id' => $val['id']])->update(['isSend' => 1]); + + // 4) 统计 + $this->incrementSendStats($companyId, $userId, $allowed); + } + } + } catch (\Exception $e) { + Log::error("朋友圈同步任务异常: " . $e->getMessage()); + throw $e; + } + } + + protected function resetDailyCountersIfNeeded() + { + $now = time(); + $todayStart = strtotime(date('Y-m-d 00:00:00')); + if ($now - $todayStart >= 0 && $now - $todayStart <= 600) { + $cacheKey = 'moments_settings_reset_' . date('Ymd'); + if (!Cache::has($cacheKey)) { + Db::table('ck_kf_moments_settings')->where('sendNum', '<>', 0) + ->update(['sendNum' => 0, 'updateTime' => $now]); + Cache::set($cacheKey, 1, 7200); + } + } + } + + protected function getWindowTasks() + { + $nowTs = time(); + $windowStart = $nowTs - 300; + $windowEnd = $nowTs + 300; + $kfMoments = KfMoments::where(['isSend' => 0, 'isDel' => 0]) + ->whereBetween('sendTime', [$windowStart, $windowEnd]) + ->order('id desc')->select(); + return [$nowTs, $kfMoments]; + } + + protected function normalizeSendData(array $sendData) + { + $endTime = strtotime($sendData['endTime'] ?? ''); + if ($endTime <= time() + 1800) { + $endTime = time() + 3600; + $sendData['endTime'] = date('Y-m-d H:i:s', $endTime); + } + switch ($sendData['momentContentType'] ?? 1) { + case 1: + $sendData['link'] = ['image' => '']; + $sendData['picUrlList'] = []; + $sendData['videoUrl'] = ''; + break; + case 2: + $sendData['link'] = ['image' => '']; + $sendData['videoUrl'] = ''; + break; + case 3: + $sendData['link'] = ['image' => '']; + $sendData['picUrlList'] = []; + break; + case 4: + $sendData['picUrlList'] = []; + $sendData['videoUrl'] = ''; + break; + default: + $sendData['link'] = ['image' => '']; + $sendData['picUrlList'] = []; + $sendData['videoUrl'] = ''; + break; + } + return $sendData; + } + + protected function filterAccountsByQuota(int $companyId, int $userId, array $items) + { + $wechatIds = array_values(array_filter(array_map(function($it){ return (int)($it['wechatAccountId'] ?? 0); }, $items))); + if (empty($wechatIds)) { return []; } + $settings = Db::table('ck_kf_moments_settings') + ->where('companyId', $companyId) + ->where('userId', $userId) + ->whereIn('wechatId', $wechatIds) + ->column('id,max,sendNum', 'wechatId'); + $allowed = []; + foreach ($items as $it) { + $wid = (int)($it['wechatAccountId'] ?? 0); + if ($wid <= 0) { continue; } + if (isset($settings[$wid])) { + $max = (int)$settings[$wid]['max']; + $sent = (int)$settings[$wid]['sendNum']; + if ($sent < ($max > 0 ? $max : 5)) { $allowed[] = $it; } + } else { + $allowed[] = $it; + } + } + return $allowed; + } + + protected function incrementSendStats(int $companyId, int $userId, array $items) + { + try { + $nowTs = time(); + foreach ($items as $it) { + $wechatId = (int)($it['wechatAccountId'] ?? 0); + if ($wechatId <= 0) { continue; } + $cond = ['companyId' => $companyId, 'userId' => $userId, 'wechatId' => $wechatId]; + $setting = Db::table('ck_kf_moments_settings')->where($cond)->find(); + if ($setting) { + Db::table('ck_kf_moments_settings')->where('id', $setting['id']) + ->update(['sendNum' => Db::raw('sendNum + 1'), 'updateTime' => $nowTs]); + } else { + Db::table('ck_kf_moments_settings')->insert([ + 'companyId' => $companyId, + 'userId' => $userId, + 'wechatId' => $wechatId, + 'max' => 5, + 'sendNum' => 1, + 'createTime' => $nowTs, + 'updateTime' => $nowTs, + ]); + } + } + } catch (\Throwable $e) { + Log::error('朋友圈发送统计失败: ' . $e->getMessage()); + } + } + + + /** + * 处理内容发送 + * @param Workbench $workbench + * @param WorkbenchMoments $config + * @param array $devices + * @param array $contentLibrary + */ + protected function handleContentSend($workbench, $config, $devices, $contentLibrary) + { + // 准备评论数据 + $comment = []; + if (!empty($contentLibrary['comment'])) { + $comment[] = $contentLibrary['comment']; + } + + // 准备发送数据 + $jobPublishWechatMomentsItems = []; + foreach ($devices as $device) { + $jobPublishWechatMomentsItems[] = [ + 'comments' => $comment, + 'labels' => [], + 'wechatAccountId' => $device['wechatAccountId'] + ]; + } + + // 转换内容类型 + $momentContentType = self::CONTENT_TYPE_MAP[$contentLibrary['contentType']] ?? 1; + $sendTime = !empty($contentLibrary['sendTime']) ? $contentLibrary['sendTime'] : time(); + + // 图片url + if ($momentContentType == 2) { + $picUrlList = json_decode($contentLibrary['resUrls'], true); + } else { + $picUrlList = []; + } + + // 视频url + if ($momentContentType == 3) { + $videoUrl = json_decode($contentLibrary['urls'], true); + $videoUrl = $videoUrl[0] ?? ''; + } else { + $videoUrl = ''; + } + + // 链接url + if ($momentContentType == 4) { + $urls = json_decode($contentLibrary['urls'], true); + $url = $urls[0] ?? []; + $link = [ + 'desc' => $url['desc'] ?? '', + 'image' => $url['image'] ?? '', + 'url' => $url['url'] ?? '' + ]; + } else { + $link = ['image' => '']; + } + + // 准备发送参数 + $data = [ + 'altList' => '', + 'immediately' => false, + 'isUseLocation' => false, + 'jobPublishWechatMomentsItems' => $jobPublishWechatMomentsItems, + 'lat' => 0, + 'lng' => 0, + 'link' => $link, + 'momentContentType' => $momentContentType, + 'picUrlList' => $picUrlList, + 'poiAddress' => '', + 'poiName' => '', + 'publicMode' => '', + 'text' => !empty($contentLibrary['contentAi']) ? $contentLibrary['contentAi'] : $contentLibrary['content'], + 'timingTime' => date('Y-m-d H:i:s', $sendTime), + 'beginTime' => date('Y-m-d H:i:s', $sendTime), + 'endTime' => date('Y-m-d H:i:s', $sendTime + 3600), + 'videoUrl' => $videoUrl, + ]; + // 发送朋友圈 + $moments = new Moments(); + $res = $moments->addJob($data); + $res = json_decode($res,true); + if ($res['code'] == 200){ + // 记录发送记录 + $this->recordSendHistory($workbench, $devices, $contentLibrary); + } + } + + + /** + * 记录发送历史 + * @param Workbench $workbench + * @param array $devices + * @param array $contentLibrary + */ + protected function recordSendHistory($workbench, $devices, $contentLibrary) + { + $now = time(); + $data = []; + foreach ($devices as $device) { + $data = [ + 'workbenchId' => $workbench->id, + 'deviceId' => $device['deviceId'], + 'contentId' => $contentLibrary['id'], + 'wechatAccountId' => $device['wechatAccountId'], + 'isLoop' => 0, // 初始状态为未完成循环 + 'createTime' => $now, + ]; + Db::name('workbench_moments_sync_item')->insert($data); + } + + } + + /** + * 获取设备列表 + * @param Workbench $workbench 工作台 + * @param WorkbenchMoments $config 配置 + * @return array|bool + */ + protected function getDevice($workbench, $config) + { + $devices = json_decode($config['devices'], true); + if (empty($devices)) { + return false; + } + + $list = Db::name('device')->alias('d') + ->join('device_wechat_login dw', 'dw.alive = 1 and dw.deviceId = d.id and dw.companyId = d.companyId') + ->join(['s2_wechat_account' => 'wa'], 'wa.wechatId = dw.wechatId') + ->where(['d.companyId' => $workbench->companyId, 'd.alive' => 1]) + ->whereIn('d.id', $devices) + ->field('d.id as deviceId, d.memo as deviceName, d.companyId, dw.wechatId, wa.id as wechatAccountId') + ->select(); + + $newList = []; + foreach ($list as $val) { + // 检查发送间隔(新逻辑:根据startTime、endTime、syncCount动态计算) + $today = date('Y-m-d'); + $startTimestamp = strtotime($today . ' ' . $config['startTime'] . ':00'); + $endTimestamp = strtotime($today . ' ' . $config['endTime'] . ':00'); + $totalSeconds = $endTimestamp - $startTimestamp; + if ($totalSeconds <= 0 || empty($config['syncCount'])) { + continue; + } + $interval = floor($totalSeconds / $config['syncCount']); + + // 查询今日已同步次数 + $count = Db::name('workbench_moments_sync_item') + ->where('workbenchId', $workbench->id) + ->where('deviceId', $val['deviceId']) + ->whereTime('createTime', 'between', [$startTimestamp, $endTimestamp]) + ->count(); + + if ($count >= $config['syncCount']) { + continue; + } + + // 计算本次同步的最早允许时间 + $nextSyncTime = $startTimestamp + $count * $interval; + if (time() < $nextSyncTime) { + continue; + } + + $newList[] = $val; + } + + return $newList; + } + + /** + * 获取内容库 + * @param Workbench $workbench 工作台 + * @param WorkbenchMoments $config 配置 + * @return array|bool + */ + protected function getContentLibrary($workbench, $config) + { + $contentids = json_decode($config['contentLibraries'], true); + // 清洗 contentids:去除 null/空字符串,并去重,保持原顺序 + if (is_array($contentids)) { + $contentids = array_values(array_unique(array_filter($contentids, function ($v) { + return $v !== null && $v !== ''; + }))); + } else { + $contentids = []; + } + if (empty($contentids)) { + return false; + } + // 基础查询 + $query = Db::name('content_library')->alias('cl') + ->join('content_item ci', 'ci.libraryId = cl.id') + ->where(['cl.isDel' => 0, 'ci.isDel' => 0]) + ->whereIn('cl.id', $contentids) + ->field([ + 'ci.id', + 'ci.libraryId', + 'ci.contentType', + 'ci.title', + 'ci.content', + 'ci.resUrls', + 'ci.urls', + 'ci.comment', + 'ci.sendTime' + ]); + // 复制 query + $query2 = clone $query; + $query3 = clone $query; + // 根据accountType处理不同的发送逻辑 + if ($config['accountType'] == 1) { + // 可以循环发送 + // 1. 优先获取未发送的内容 + $unsentContent = $query2->join('workbench_moments_sync_item wmsi', 'wmsi.contentId = ci.id and wmsi.workbenchId = ' . $workbench->id, 'left') + ->where('wmsi.id', 'null') + ->where('ci.sendTime <= ' . (time() + 60)) + ->order('ci.sendTime desc, ci.id desc') + ->find(); + + if (!empty($unsentContent)) { + return $unsentContent; + } + + // 获取下一个要发送的内容(从内容库中查询,排除isLoop为0的数据) + $isPushIds = Db::name('workbench_moments_sync_item') + ->where(['workbenchId' => $workbench->id, 'isLoop' => 0]) + ->column('contentId'); + + if (empty($isPushIds)) { + $isPushIds = [0]; + } + $sentContent = $query3 + ->whereNotIn('ci.id', $isPushIds) + ->group('ci.id') + ->order('ci.id asc') + ->find(); + // 4. 如果仍然没有内容,说明内容库为空,将所有记录的isLoop标记为1 + if (empty($sentContent)) { + // 将所有该工作台的记录标记为循环完成 + Db::name('workbench_moments_sync_item') + ->where('workbenchId', $workbench->id) + ->where('isLoop', 0) + ->update(['isLoop' => 1]); + return false; + } + + return $sentContent; + } else { + // 不能循环发送,只获取未发送的内容 + $list = $query2->join('workbench_moments_sync_item wmsi', 'wmsi.contentId = ci.id and wmsi.workbenchId = ' . $workbench->id, 'left') + ->where('wmsi.id', 'null') + ->order('ci.sendTime desc, ci.id desc') + ->find(); + return $list; + } + } + + /** + * 记录任务开始 + * @param string $jobId + * @param string $queueLockKey + */ + protected function logJobStart($jobId, $queueLockKey) + { + // 去除开始日志,减少日志空间消耗 + } + + /** + * 处理任务成功 + * @param Job $job + * @param string $queueLockKey + */ + protected function handleJobSuccess($job, $queueLockKey) + { + $job->delete(); + Cache::rm($queueLockKey); + // 去除成功日志,减少日志空间消耗 + } + + /** + * 处理任务错误 + * @param \Exception $e + * @param Job $job + * @param string $queueLockKey + * @return bool + */ + protected function handleJobError(\Exception $e, $job, $queueLockKey) + { + Log::error('工作台朋友圈同步任务异常:' . $e->getMessage()); + + if (!empty($queueLockKey)) { + Cache::rm($queueLockKey); + Log::info("由于异常释放队列锁: {$queueLockKey}"); + } + + if ($job->attempts() > self::MAX_RETRY_ATTEMPTS) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + + return false; + } +} \ No newline at end of file diff --git a/application/job/WorkbenchTrafficDistributeJob.php b/application/job/WorkbenchTrafficDistributeJob.php new file mode 100644 index 0000000..fb2ddaa --- /dev/null +++ b/application/job/WorkbenchTrafficDistributeJob.php @@ -0,0 +1,267 @@ +logJobStart($jobId, $queueLockKey); + $workbenches = $this->getActiveWorkbenches(); + if (empty($workbenches)) { + $this->handleEmptyWorkbenches($job, $queueLockKey); + return true; + } + $this->processWorkbenches($workbenches); + $this->handleJobSuccess($job, $queueLockKey); + return true; + } catch (\Exception $e) { + return $this->handleJobError($e, $job, $queueLockKey); + } + } + + protected function getActiveWorkbenches() + { + return Workbench::where([ + ['status', '=', 1], + ['isDel', '=', 0], + ['type', '=', 5] + ])->order('id DESC')->select(); + } + + protected function processWorkbenches($workbenches) + { + foreach ($workbenches as $workbench) { + try { + $this->processSingleWorkbench($workbench); + } catch (\Exception $e) { + Log::error("处理流量分发工作台 {$workbench->id} 失败: " . $e->getMessage()); + } + } + } + + protected function processSingleWorkbench($workbench) + { + $page = 1; + $pageSize = 20; + + $config = WorkbenchTrafficConfig::where('workbenchId', $workbench->id)->find(); + if (!$config) { + Log::error("流量分发工作台 {$workbench->id} 配置获取失败"); + return; + } + + // 验证是否在流量分发时间范围内 + if (!$this->isTimeRange($config) && $config['timeType'] == 2) { + return; + } + // 获取当天未超额的可用账号 + if(empty($config['account'])){ + Log::error("流量分发工作台 {$workbench->id} 未配置分发的客服"); + return; + } + $accountIds = json_decode($config['account'],true); + $todayStart = strtotime(date('Y-m-d 00:00:00')); + $todayEnd = strtotime(date('Y-m-d 23:59:59')); + $accounts = Db::table('s2_company_account') + ->alias('a') + ->where(['a.departmentId' => $workbench->companyId, 'a.status' => 0]) + ->whereIn('a.id',$accountIds) + ->whereNotLike('a.userName', '%_offline%') + ->whereNotLike('a.userName', '%_delete%') + ->leftJoin('workbench_traffic_config_item wti', "wti.wechatAccountId = a.id AND wti.workbenchId = {$workbench->id} AND wti.createTime BETWEEN {$todayStart} AND {$todayEnd}") + ->field('a.id,a.userName,a.realName,a.nickname,COUNT(wti.id) as todayCount') + ->group('a.id') + ->having('todayCount <= ' . $config['maxPerDay']) + ->select(); + $accountNum = count($accounts); + if ($accountNum < 1) { + Log::info("流量分发工作台 {$workbench->id} 可分配账号少于1个"); + return; + } + $automaticAssign = new AutomaticAssign(); + do { + $friends = $this->getFriendsByLabels($workbench, $config, $page, $pageSize); + + if (empty($friends) || count($friends) == 0) { + Log::info("流量分发工作台 {$workbench->id} 没有可分配的好友"); + break; + } + $i = 0; + $accountNum = count($accounts); + foreach ($friends as $friend) { + if ($accountNum == 0) { + Log::info("流量分发工作台 {$workbench->id} 所有账号今日分配已满"); + break 2; + } + if ($i >= $accountNum) { + $i = 0; + } + $account = $accounts[$i]; + + // 如果该账号今天分配的记录数加上本次分配的记录数超过最大限制 + if (($account['todayCount'] + $pageSize) >= $config['maxPerDay']) { + // 查询该客服账号当天分配记录数 + $todayCount = Db::name('workbench_traffic_config_item') + ->where('workbenchId', $workbench->id) + ->where('wechatAccountId', $account['id']) + ->whereBetween('createTime', [$todayStart, $todayEnd]) + ->count(); + if ($todayCount >= $config['maxPerDay']) { + unset($accounts[$i]); + $accounts = array_values($accounts); + $accountNum = count($accounts); + $i++; + continue; + } + } + + // 执行切换好友命令 + $res = $automaticAssign->allotWechatFriend([ + 'wechatFriendId' => $friend['id'], + 'toAccountId' => $account['id'] + ], true); + + $res = json_decode($res,true); + if ($res['code'] == 200){ + Db::table('s2_wechat_friend') + ->where('id',$friend['id']) + ->update([ + 'accountId' => $account['id'], + 'accountUserName' => $account['userName'], + 'accountRealName' => $account['realName'], + 'accountNickname' => $account['nickname'], + ]); + // 写入分配记录表 + Db::name('workbench_traffic_config_item')->insert([ + 'workbenchId' => $workbench->id, + 'deviceId' => $friend['deviceId'], + 'wechatFriendId' => $friend['id'], + 'wechatAccountId' => $account['id'], + 'createTime' => time(), + 'exp' => $config['exp'], + 'expTime' => time() + 86400 * $config['exp'], + ]); +- // 去除成功日志,减少日志空间消耗 + } + $i++; + } + break; + $page++; + } while (true); + // 去除完成日志,减少日志空间消耗 + } + + + /** + * 检查是否在流量分发时间范围内 + * @param WorkbenchAutoLike $config + * @return bool + */ + protected function isTimeRange($config) + { + $currentTime = date('H:i'); + if ($currentTime < $config['startTime'] || $currentTime > $config['endTime']) { + Log::info("当前时间 {$currentTime} 不在流量分发时间范围内 ({$config['startTime']} - {$config['endTime']})"); + return false; + } + return true; + } + + /** + * 一次性查出所有包含指定标签数组的好友(支持分页) + * @param object $workbench 工作台对象 + * @param object $config 配置对象 + * @param int $page 页码 + * @param int $pageSize 每页数量 + * @return array + */ + protected function getFriendsByLabels($workbench, $config, $page = 1, $pageSize = 20) + { + $labels = []; + if (!empty($config['pools'])) { + $labels = is_array($config['pools']) ? $config['pools'] : json_decode($config['pools'], true); + } + + $devices = []; + if (!empty($config['devices'])) { + $devices = is_array($config['devices']) ? $config['devices'] : json_decode($config['devices'], true); + } + if (empty($devices)) { + return []; + } + $query = Db::table('s2_wechat_friend')->alias('wf') + ->join(['s2_company_account' => 'sa'], 'sa.id = wf.accountId', 'left') + ->join(['s2_wechat_account' => 'wa'], 'wa.id = wf.wechatAccountId', 'left') + ->join('workbench_traffic_config_item wtci', 'wtci.isRecycle = 0 and wtci.wechatFriendId = wf.id AND wtci.workbenchId = ' . $config['workbenchId'], 'left') + ->where([ + ['wf.isDeleted', '=', 0], + ['wf.isPassed', '=', 1], + //['sa.departmentId', '=', $workbench->companyId], + ['wtci.id', 'null', null] + ]) + ->whereIn('wa.currentDeviceId', $devices) + ->field('wf.id,wf.wechatAccountId,wf.wechatId,wf.labels,sa.userName,wa.currentDeviceId as deviceId'); + + + if(!empty($labels)){ + $query->where(function ($q) use ($labels) { + foreach ($labels as $label) { + $q->whereOrRaw("JSON_CONTAINS(wf.labels, '\"{$label}\"')"); + } + }); + } + $list = $query->page($page, $pageSize)->order('wf.id DESC')->select(); + + return $list; + } + + protected function logJobStart($jobId, $queueLockKey) + { + // 去除开始日志,减少日志空间消耗 + } + + protected function handleJobSuccess($job, $queueLockKey) + { + $job->delete(); + Cache::rm($queueLockKey); + // 去除成功日志,减少日志空间消耗 + } + + protected function handleJobError(\Exception $e, $job, $queueLockKey) + { + Log::error('流量分发任务异常:' . $e->getMessage()); + if (!empty($queueLockKey)) { + Cache::rm($queueLockKey); + Log::info("由于异常释放队列锁: {$queueLockKey}"); + } + if ($job->attempts() > self::MAX_RETRY_ATTEMPTS) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + return false; + } + + protected function handleEmptyWorkbenches(Job $job, $queueLockKey) + { + Log::info('没有需要处理的流量分发任务'); + $job->delete(); + Cache::rm($queueLockKey); + } +} \ No newline at end of file diff --git a/application/provider.php b/application/provider.php new file mode 100644 index 0000000..d453b84 --- /dev/null +++ b/application/provider.php @@ -0,0 +1,16 @@ + +// +---------------------------------------------------------------------- + +// 应用容器绑定定义 +return [ + // 类的映射表 + 'ClassTable' => app\common\service\ClassTableService::class, +]; diff --git a/application/store/config/route.php b/application/store/config/route.php new file mode 100644 index 0000000..ae09c5d --- /dev/null +++ b/application/store/config/route.php @@ -0,0 +1,49 @@ +middleware(['jwt']); + +Route::get('v1/store/login', 'app\store\controller\LoginController@index'); \ No newline at end of file diff --git a/application/store/controller/BaseController.php b/application/store/controller/BaseController.php new file mode 100644 index 0000000..c8cec8b --- /dev/null +++ b/application/store/controller/BaseController.php @@ -0,0 +1,65 @@ +userInfo = request()->userInfo; + + // 生成缓存key + $cacheKey = 'device_info_' . $this->userInfo['id'] . '_' . $this->userInfo['companyId']; + + // 尝试从缓存获取设备信息 + $device = Cache::get($cacheKey); + // 如果缓存不存在,则从数据库获取 + if (!$device) { + $device = Db::name('device_user') + ->alias('du') + ->join('device d', 'd.id = du.deviceId','left') + ->join('device_wechat_login dwl', 'dwl.deviceId = du.deviceId','left') + ->join('wechat_account wa', 'dwl.wechatId = wa.wechatId','left') + ->where([ + 'du.userId' => $this->userInfo['id'], + 'du.companyId' => $this->userInfo['companyId'] + ]) + ->field('d.*,wa.wechatId,wa.alias,wa.s2_wechatAccountId as wechatAccountId') + ->find(); + // 将设备信息存入缓存 + if ($device) { + Cache::set($cacheKey, $device, $this->cacheExpire); + } + } + $this->device = $device; + } + + /** + * 清除设备信息缓存 + */ + protected function clearDeviceCache() + { + $cacheKey = 'device_info_' . $this->userInfo['id'] . '_' . $this->userInfo['companyId']; + Cache::rm($cacheKey); + } +} \ No newline at end of file diff --git a/application/store/controller/CustomerController.php b/application/store/controller/CustomerController.php new file mode 100644 index 0000000..e61e62e --- /dev/null +++ b/application/store/controller/CustomerController.php @@ -0,0 +1,93 @@ +request->param(); + + // 获取分页参数 + $page = isset($params['page']) ? intval($params['page']) : 1; + $pageSize = isset($params['pageSize']) ? intval($params['pageSize']) : 10; + $userInfo = request()->userInfo; + + $where = []; + // 必要的查询条件 + $userId = $userInfo['id']; + $companyId = $userInfo['companyId']; + + if (empty($userId) || empty($companyId)) { + return errorJson('缺少必要参数'); + } + + // 构建查询条件 + $deviceIds = Db::name('device_user')->where(['userId' => $userId, 'companyId' => $companyId])->order('id DESC')->column('deviceId'); + if (empty($deviceIds)) { + return errorJson('设备不存在'); + } + $wechatIds = []; + foreach ($deviceIds as $deviceId) { + $wechatIds[] = Db::name('device_wechat_login') + ->where(['deviceId' => $deviceId]) + ->order('id DESC') + ->value('wechatId'); + } + + + + // 搜索条件 + if (!empty($params['keyword'])) { + $where['alias|nickname|wechatId'] = ['like', '%' . $params['keyword'] . '%']; + } + // if (!empty($params['email'])) { + // $where['wa.bindEmail'] = ['like', '%' . $params['email'] . '%']; + // } + // if (!empty($params['name'])) { + // $where['wa.accountRealName|wa.accountUserName|wa.nickname'] = ['like', '%' . $params['name'] . '%']; + // } + + // 构建查询 + $query = Db::table('s2_wechat_friend') + ->where($where) + ->whereIn('ownerWechatId',$wechatIds) + ->group('wechatId'); // 防止重复数据 + + // 克隆查询对象,用于计算总数 + $countQuery = clone $query; + $total = $countQuery->count(); + + // 获取分页数据 + $list = $query->page($page, $pageSize) + ->order('id DESC') + ->select(); + + + // 格式化数据 + foreach ($list as &$item) { + $item['labels'] = json_decode($item['labels'], true); + $item['createTime'] = date('Y-m-d H:i:s', $item['createTime']); + } + unset($item); + + return successJson([ + 'list' => $list, + 'total' => $total + ], '获取成功'); + } +} \ No newline at end of file diff --git a/application/store/controller/FlowPackageController.php b/application/store/controller/FlowPackageController.php new file mode 100644 index 0000000..a4f4035 --- /dev/null +++ b/application/store/controller/FlowPackageController.php @@ -0,0 +1,295 @@ +request->param(); + + // 查询条件 + $where = []; + + // 只获取未删除的数据 + $where[] = ['isDel', '=', 0]; + + // 套餐模型 + $model = new FlowPackageModel(); + + // 查询数据 + $list = $model->where($where) + ->field('id, name, tag, originalPrice, price, monthlyFlow, duration, privileges') + ->order('sort', 'asc') + ->select(); + + // 格式化返回数据,添加计算字段 + $result = []; + foreach ($list as $item) { + $result[] = [ + 'id' => $item['id'], + 'name' => $item['name'], + 'tag' => $item['tag'], + 'originalPrice' => $item['originalPrice'], + 'price' => $item['price'], + 'monthlyFlow' => $item['monthlyFlow'], + 'duration' => $item['duration'], + 'discount' => $item->discount, + 'totalFlow' => $item->totalFlow, + 'privileges' => $item['privileges'], + ]; + } + + return successJson($result, '获取成功'); + } + + /** + * 获取流量套餐详情 + * + * @param int $id 套餐ID + * @return \think\Response + */ + public function detail($id) + { + if (empty($id)) { + return errorJson('参数错误'); + } + + // 套餐模型 + $model = new FlowPackageModel(); + + // 查询数据 + $info = $model->where('id', $id)->where('isDel', 0)->find(); + + if (empty($info)) { + return errorJson('套餐不存在'); + } + + // 格式化返回数据,添加计算字段 + $result = [ + 'id' => $info['id'], + 'name' => $info['name'], + 'tag' => $info['tag'], + 'originalPrice' => $info['originalPrice'], + 'price' => $info['price'], + 'monthlyFlow' => $info['monthlyFlow'], + 'duration' => $info['duration'], + 'discount' => $info->discount, + 'totalFlow' => $info->totalFlow, + 'privileges' => $info['privileges'], + ]; + + return successJson($result, '获取成功'); + } + + /** + * 展示用户流量套餐使用情况 + * + * @return \think\Response + */ + public function remainingFlow() + { + $params = $this->request->param(); + + $userInfo = request()->userInfo; + // 获取用户ID,通常应该从会话或令牌中获取 + $userId = $userInfo['id']; + + if (empty($userId)) { + return errorJson('请先登录'); + } + + // 获取用户当前有效的流量套餐 + $userPackage = UserFlowPackageModel::getUserActivePackage($userId); + + if (empty($userPackage)) { + return errorJson('您没有有效的流量套餐'); + } + + // 获取套餐详情 + $packageId = $userPackage['packageId']; + $flowPackage = FlowPackageModel::where('id', $packageId)->where('isDel', 0)->find(); + + if (empty($flowPackage)) { + return errorJson('套餐信息不存在'); + } + + // 计算剩余流量 + $totalFlow = $userPackage['totalFlow'] ?? $flowPackage->totalFlow; // 总流量 + $usedFlow = $userPackage['usedFlow'] ?? 0; // 已使用流量 + $remainingFlow = $totalFlow - $usedFlow; // 剩余流量 + $remainingFlow = $remainingFlow > 0 ? $remainingFlow : 0; // 确保不为负数 + + // 计算剩余天数 + $now = time(); + $expireTime = $userPackage['expireTime']; + $remainingDays = ceil(($expireTime - $now) / 86400); // 向上取整,剩余天数 + $remainingDays = $remainingDays > 0 ? $remainingDays : 0; // 确保不为负数 + + // 剩余百分比 + $flowPercentage = $totalFlow > 0 ? round(($remainingFlow / $totalFlow) * 100, 1) : 0; + $timePercentage = $userPackage['duration'] > 0 ? + round(($remainingDays / ($userPackage['duration'] * 30)) * 100, 1) : 0; + + // 返回数据 + $result = [ + 'packageName' => $flowPackage['name'], // 套餐名称 + 'remainingFlow' => $remainingFlow, // 剩余流量(人) + 'totalFlow' => $totalFlow, // 总流量(人) + 'flowPercentage' => $flowPercentage, // 剩余流量百分比 + 'remainingDays' => $remainingDays, // 剩余天数 + 'totalDays' => $userPackage['duration'] * 30, // 总天数(按30天/月计算) + 'timePercentage' => $timePercentage, // 剩余时间百分比 + 'expireTime' => date('Y-m-d', $expireTime), // 到期日期 + 'startTime' => date('Y-m-d', $userPackage['startTime']), // 开始日期 + ]; + + return successJson($result, '获取成功'); + } + + /** + * 创建流量采购订单 + * + * @return \think\Response + */ + public function createOrder() + { + $params = $this->request->param(); + + $userInfo = request()->userInfo; + // 获取用户ID,通常应该从会话或令牌中获取 + $userId = $userInfo['id']; + + if (empty($userId)) { + return errorJson('请先登录'); + } + + // 获取套餐ID + $packageId = isset($params['packageId']) ? intval($params['packageId']) : 0; + + if (empty($packageId)) { + return errorJson('请选择套餐'); + } + + // 查询套餐信息 + $flowPackage = FlowPackageModel::where('id', $packageId)->where('isDel', 0)->find(); + + if (empty($flowPackage)) { + return errorJson('套餐不存在'); + } + + // 获取支付方式(可选) + $payType = isset($params['payType']) ? $params['payType'] : 'wechat'; + + // 套餐价格和信息 + $amount = floatval($flowPackage['price']); + $packageName = $flowPackage['name']; + $duration = intval($flowPackage['duration']); + $remark = isset($params['remark']) ? $params['remark'] : ''; + + // 处理金额为0的特殊情况 + if ($amount <= 0) { + // 金额为0,无需支付,直接创建订单并设置为已支付 + $order = FlowPackageOrderModel::createOrder( + $userId, + $packageId, + $packageName, + 0, + $duration, + 'nopay', + $remark + ); + + if (!$order) { + return errorJson('订单创建失败'); + } + + // 创建用户流量套餐记录 + $this->createUserFlowPackage($userId, $packageId, $order['id']); + + // 返回成功信息 + return successJson(['orderNo' => $order['orderNo'],'status' => 'success'], '购买成功'); + } else { + // 创建正常需要支付的订单 + $order = FlowPackageOrderModel::createOrder( + $userId, + $packageId, + $packageName, + $amount, + $duration, + $payType, + $remark + ); + + if (!$order) { + return errorJson('订单创建失败'); + } + + // 返回订单信息,前端需要跳转到支付页面 + return successJson([ + 'orderNo' => $order['orderNo'], + 'amount' => $amount, + 'payType' => $payType, + 'status' => 'pending' + ], '订单创建成功'); + } + } + + /** + * 创建用户流量套餐记录 + * + * @param int $userId 用户ID + * @param int $packageId 套餐ID + * @param int $orderId 订单ID + * @return bool + */ + private function createUserFlowPackage($userId, $packageId, $orderId) + { + // 获取套餐信息 + $flowPackage = FlowPackageModel::where('id', $packageId)->where('isDel', 0)->find(); + + if (empty($flowPackage)) { + return false; + } + + // 计算到期时间(当前时间 + 套餐时长(月) * 30天) + $now = time(); + $expireTime = $now + (intval($flowPackage['duration']) * 30 * 86400); + + // 用户流量套餐数据 + $data = [ + 'userId' => $userId, + 'packageId' => $packageId, + 'orderId' => $orderId, + 'packageName' => $flowPackage['name'], + 'monthlyFlow' => $flowPackage['monthlyFlow'], + 'duration' => $flowPackage['duration'], + 'totalFlow' => $flowPackage->totalFlow, // 使用计算属性获取总流量 + 'usedFlow' => 0, + 'startTime' => $now, + 'expireTime' => $expireTime, + 'status' => 1, // 1:有效 0:无效 + 'isDel' => 0 + ]; + + // 创建用户流量套餐记录 + return UserFlowPackageModel::create($data) ? true : false; + } +} diff --git a/application/store/controller/LoginController.php b/application/store/controller/LoginController.php new file mode 100644 index 0000000..8dc571d --- /dev/null +++ b/application/store/controller/LoginController.php @@ -0,0 +1,43 @@ +request->param('deviceId', ''); + if (empty($deviceId)) { + return errorJson('缺少必要参数'); + } + + $user = Db::name('users')->alias('u') + ->field('u.*') + ->join('device_user du', 'u.id = du.userId and u.companyId = du.companyId') + ->join('device d', 'du.deviceId = d.id and u.companyId = du.companyId') + ->where(['d.deviceImei' => $deviceId, 'u.deleteTime' => 0, 'du.deleteTime' => 0, 'd.deleteTime' => 0]) + ->find(); + if (empty($user)) { + return errorJson('用户不存在'); + } + $member = array_merge($user, [ + 'lastLoginIp' => $this->request->ip(), + 'lastLoginTime' => time() + ]); + + // 生成JWT令牌 + $token = JwtUtil::createToken($user, 86400 * 30); + $token_expired = time() + 86400 * 30; + + $data = [ + 'member' => $member, + 'token' => $token, + 'token_expired' => $token_expired + ]; + return successJson($data, '登录成功'); + } +} \ No newline at end of file diff --git a/application/store/controller/StatisticsController.php b/application/store/controller/StatisticsController.php new file mode 100644 index 0000000..17c9226 --- /dev/null +++ b/application/store/controller/StatisticsController.php @@ -0,0 +1,482 @@ +userInfo['companyId']; + $userId = $this->userInfo['id']; + + // 构建查询条件 + $deviceIds = Db::name('device_user')->where(['userId' => $userId, 'companyId' => $companyId])->order('id DESC')->column('deviceId'); + if (empty($deviceIds)) { + return errorJson('设备不存在'); + } + $ownerWechatIds = []; + foreach ($deviceIds as $deviceId) { + $ownerWechatIds[] = Db::name('device_wechat_login') + ->where(['deviceId' => $deviceId]) + ->order('id DESC') + ->value('wechatId'); + } + + $wechatAccountIds = Db::table('s2_wechat_account')->whereIn('wechatId', $ownerWechatIds)->column('id'); + + + // 获取时间范围 + $timeRange = $this->getTimeRange(); + $startTime = $timeRange['start_time']; + $endTime = $timeRange['end_time']; + $lastStartTime = $timeRange['last_start_time']; + $lastEndTime = $timeRange['last_end_time']; + + + // 1. 总客户数 + $totalCustomers = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) + ->where('isDeleted', 0) + ->whereTime('createTime', '>=', $startTime) + ->whereTime('createTime', '<', $endTime) + ->count(); + + // 上期总客户数 + $lastTotalCustomers = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) + ->whereTime('createTime', '>=', $lastStartTime) + ->whereTime('createTime', '<', $lastEndTime) + ->count(); + + // 2. 新增客户数 + $newCustomers = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) + ->whereTime('createTime', '>=', $startTime) + ->whereTime('createTime', '<', $endTime) + ->count(); + + // 上期新增客户数 + $lastNewCustomers = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) + ->whereTime('createTime', '>=', $lastStartTime) + ->whereTime('createTime', '<', $lastEndTime) + ->count(); + + //3. 互动次数 + $interactionCount = WechatMessageModel::whereIn('wechatAccountId', $wechatAccountIds) + ->where('createTime', '>=', $startTime) + ->where('createTime', '<', $endTime) + ->count(); + + // 上期互动次数 + $lastInteractionCount = WechatMessageModel::whereIn('wechatAccountId', $wechatAccountIds) + ->where('createTime', '>=', $lastStartTime) + ->where('createTime', '<', $lastEndTime) + ->count(); + + // 4. RFM 平均值计算(不查询上期数据) + $rfmStats = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) + ->where('isDeleted', 0) + ->field('AVG(`R`) as avgR, AVG(`F`) as avgF, AVG(`M`) as avgM') + ->find(); + + // 处理查询结果,如果字段为null则默认为0 + $avgR = isset($rfmStats['avgR']) && $rfmStats['avgR'] !== null ? round((float)$rfmStats['avgR'], 2) : 0; + $avgF = isset($rfmStats['avgF']) && $rfmStats['avgF'] !== null ? round((float)$rfmStats['avgF'], 2) : 0; + $avgM = isset($rfmStats['avgM']) && $rfmStats['avgM'] !== null ? round((float)$rfmStats['avgM'], 2) : 0; + + // 计算三者的平均值 + $avgRFM = ($avgR + $avgF + $avgM) / 3; + $avgRFM = round($avgRFM, 2); + + // 计算环比增长率 + $customerGrowth = $this->calculateGrowth($totalCustomers, $lastTotalCustomers); + $newCustomerGrowth = $this->calculateGrowth($newCustomers, $lastNewCustomers); + $interactionGrowth = $this->calculateGrowth($interactionCount, $lastInteractionCount); + $data = [ + 'total_customers' => [ + 'value' => $totalCustomers, + 'growth' => $customerGrowth + ], + 'new_customers' => [ + 'value' => $newCustomers, + 'growth' => $newCustomerGrowth + ], + 'interaction_count' => [ + 'value' => $interactionCount, + 'growth' => $interactionGrowth + ], + 'conversion_rate' => [ + 'value' => 10, + 'growth' => 15 + ], + 'account_value' => [ + 'avg_r' => $avgR, + 'avg_f' => $avgF, + 'avg_m' => $avgM, + 'avg_rfm' => $avgRFM + ] + ]; + + return successJson($data); + } catch (\Exception $e) { + return errorJson('获取数据概览失败:' . $e->getMessage()); + } + } + + + /** + * 获取综合分析数据 + */ + public function getComprehensiveAnalysis() + { + try { + $companyId = $this->userInfo['companyId']; + $userId = $this->userInfo['id']; + + // 构建查询条件 + $deviceIds = Db::name('device_user')->where(['userId' => $userId, 'companyId' => $companyId])->order('id DESC')->column('deviceId'); + if (empty($deviceIds)) { + return errorJson('设备不存在'); + } + $ownerWechatIds = []; + foreach ($deviceIds as $deviceId) { + $ownerWechatIds[] = Db::name('device_wechat_login') + ->where(['deviceId' => $deviceId]) + ->order('id DESC') + ->value('wechatId'); + } + $wechatAccountIds = Db::table('s2_wechat_account')->whereIn('wechatId', $ownerWechatIds)->column('id'); + + // 获取时间范围 + $timeRange = $this->getTimeRange(); + $startTime = $timeRange['start_time']; + $endTime = $timeRange['end_time']; + $lastStartTime = $timeRange['last_start_time']; + $lastEndTime = $timeRange['last_end_time']; + + // ========== 1. 客户平均转化金额 ========== + // 获取有订单的客户数(去重) + $convertedCustomers = TrafficOrderModel::whereIn('ownerWechatId', $ownerWechatIds) + ->where('createTime', '>=', $startTime) + ->where('createTime', '<', $endTime) + ->group('identifier') + ->column('identifier'); + $convertedCustomerCount = count($convertedCustomers); + + // 总销售额 + $totalSales = TrafficOrderModel::whereIn('ownerWechatId', $ownerWechatIds) + ->where('createTime', '>=', $startTime) + ->where('createTime', '<', $endTime) + ->sum('actualPay'); + $totalSales = $totalSales ?: 0; + + // 客户平均转化金额 + $avgConversionAmount = $convertedCustomerCount > 0 ? round($totalSales / $convertedCustomerCount, 2) : 0; + + // ========== 2. 价值指标 ========== + // 销售总额(已计算) + + // 平均订单金额(总订单数) + $totalOrderCount = TrafficOrderModel::whereIn('ownerWechatId', $ownerWechatIds) + ->where('createTime', '>=', $startTime) + ->where('createTime', '<', $endTime) + ->count(); + $avgOrderAmount = $totalOrderCount > 0 ? round($totalSales / $totalOrderCount, 2) : 0; + + // 高价值客户(消费超过平均订单金额的客户) + // 先获取每个客户的消费总额 + $customerTotalSpend = TrafficOrderModel::whereIn('ownerWechatId', $ownerWechatIds) + ->where('createTime', '>=', $startTime) + ->where('createTime', '<', $endTime) + ->field('identifier, SUM(actualPay) as totalSpend') + ->group('identifier') + ->select(); + + $highValueCustomerCount = 0; + $avgCustomerSpend = $convertedCustomerCount > 0 ? ($totalSales / $convertedCustomerCount) : 0; + foreach ($customerTotalSpend as $customer) { + if ($customer['totalSpend'] > $avgCustomerSpend) { + $highValueCustomerCount++; + } + } + + // 高价值客户百分比 + $totalCustomersForCalc = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) + ->where('isDeleted', 0) + ->count(); + $highValueCustomerPercent = $totalCustomersForCalc > 0 ? round(($highValueCustomerCount / $totalCustomersForCalc) * 100, 1) : 0; + + // ========== 3. 增长趋势 ========== + // 上期销售额 + $lastTotalSales = TrafficOrderModel::whereIn('ownerWechatId', $ownerWechatIds) + ->where('createTime', '>=', $lastStartTime) + ->where('createTime', '<', $lastEndTime) + ->sum('actualPay'); + $lastTotalSales = $lastTotalSales ?: 0; + + // 周收益增长(金额差值) + $weeklyRevenueGrowth = round($totalSales - $lastTotalSales, 2); + + // 新客转化(新客户中有订单的人数) + $newCustomers = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) + ->where('createTime', '>=', $startTime) + ->where('createTime', '<', $endTime) + ->column('wechatId'); + + // 获取新客户中有订单的(identifier 对应 wechatId) + $newConvertedCustomers = 0; + if (!empty($newCustomers)) { + $newConvertedCustomers = TrafficOrderModel::whereIn('ownerWechatId', $ownerWechatIds) + ->where('createTime', '>=', $startTime) + ->where('createTime', '<', $endTime) + ->whereIn('identifier', $newCustomers) + ->group('identifier') + ->count(); + } + + // 活跃客户增长(有互动的客户) + $activeCustomers = WechatMessageModel::whereIn('wechatAccountId', $wechatAccountIds) + ->where('createTime', '>=', $startTime) + ->where('createTime', '<', $endTime) + ->group('wechatFriendId') + ->count(); + + $lastActiveCustomers = WechatMessageModel::whereIn('wechatAccountId', $wechatAccountIds) + ->where('createTime', '>=', $lastStartTime) + ->where('createTime', '<', $lastEndTime) + ->group('wechatFriendId') + ->count(); + + // 活跃客户增长(人数差值) + $activeCustomerGrowth = $activeCustomers - $lastActiveCustomers; + + // ========== 4. 客户活跃度 ========== + // 按天统计每个客户的互动次数,然后分类 + // 高频互动用户数(平均每天3次以上) + $days = max(1, ($endTime - $startTime) / 86400); // 计算天数 + $highFrequencyThreshold = $days * 3; // 高频阈值 + + $highFrequencyUsers = WechatMessageModel::whereIn('wechatAccountId', $wechatAccountIds) + ->where('createTime', '>=', $startTime) + ->where('createTime', '<', $endTime) + ->field('wechatFriendId, COUNT(*) as count') + ->group('wechatFriendId') + ->having('count > ' . $highFrequencyThreshold) + ->count(); + + // 中频互动用户数(平均每天1-3次) + $midFrequencyThreshold = $days * 1; + $midFrequencyUsers = WechatMessageModel::whereIn('wechatAccountId', $wechatAccountIds) + ->where('createTime', '>=', $startTime) + ->where('createTime', '<', $endTime) + ->field('wechatFriendId, COUNT(*) as count') + ->group('wechatFriendId') + ->having('count >= ' . $midFrequencyThreshold . ' AND count <= ' . $highFrequencyThreshold) + ->count(); + + // 低频互动用户数(少于平均每天1次) + $lowFrequencyUsers = WechatMessageModel::whereIn('wechatAccountId', $wechatAccountIds) + ->where('createTime', '>=', $startTime) + ->where('createTime', '<', $endTime) + ->field('wechatFriendId, COUNT(*) as count') + ->group('wechatFriendId') + ->having('count < ' . $midFrequencyThreshold) + ->count(); + + $frequency_analysis = [ + ['name' => '高频', 'value' => $highFrequencyUsers], + ['name' => '中频', 'value' => $midFrequencyUsers], + ['name' => '低频', 'value' => $lowFrequencyUsers] + ]; + + // ========== 5. 转化客户来源 ========== + // 只统计有订单的客户来源(identifier 对应 wechatId) + $convertedFriendIds = TrafficOrderModel::whereIn('ownerWechatId', $ownerWechatIds) + ->where('createTime', '>=', $startTime) + ->where('createTime', '<', $endTime) + ->group('identifier') + ->column('identifier'); + + $friendRecommend = 0; + $wechatSearch = 0; + $wechatGroup = 0; + + if (!empty($convertedFriendIds)) { + // 朋友推荐(有订单的) + $friendRecommend = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) + ->whereIn('wechatId', $convertedFriendIds) + ->whereIn('addFrom', [17, 1000017]) + ->count(); + + // 微信搜索(有订单的) + $wechatSearch = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) + ->whereIn('wechatId', $convertedFriendIds) + ->whereIn('addFrom', [3, 15, 1000003, 1000015]) + ->count(); + + // 微信群(有订单的) + $wechatGroup = WechatFriendModel::whereIn('ownerWechatId', $ownerWechatIds) + ->whereIn('wechatId', $convertedFriendIds) + ->whereIn('addFrom', [14, 1000014]) + ->count(); + } + + $totalConvertedCustomers = $convertedCustomerCount; + $otherSource = max(0, $totalConvertedCustomers - $friendRecommend - $wechatSearch - $wechatGroup); + + // 计算百分比 + $calculatePercentage = function ($value) use ($totalConvertedCustomers) { + if ($totalConvertedCustomers <= 0) return 0; + return round(($value / $totalConvertedCustomers) * 100, 2); + }; + + $sourceDistribution = [ + [ + 'name' => '朋友推荐', + 'value' => $calculatePercentage($friendRecommend) . '%', + 'count' => $friendRecommend + ], + [ + 'name' => '微信搜索', + 'value' => $calculatePercentage($wechatSearch) . '%', + 'count' => $wechatSearch + ], + [ + 'name' => '微信群', + 'value' => $calculatePercentage($wechatGroup) . '%', + 'count' => $wechatGroup + ] + ]; + + // 构建返回数据 + $data = [ + 'avg_conversion_amount' => $avgConversionAmount, // 客户平均转化金额 + 'value_indicators' => [ + 'total_sales' => round($totalSales, 2), // 销售总额 + 'avg_order_amount' => $avgOrderAmount, // 平均订单金额 + 'high_value_customers' => $highValueCustomerPercent . '%' // 高价值客户 + ], + 'growth_trend' => [ + 'weekly_revenue_growth' => $weeklyRevenueGrowth, // 周收益增长(金额) + 'new_customer_conversion' => $newConvertedCustomers, // 新客转化(人数) + 'active_customer_growth' => $activeCustomerGrowth // 活跃客户增长(人数差值) + ], + 'frequency_analysis' => $frequency_analysis, // 客户活跃度 + 'source_distribution' => $sourceDistribution // 转化客户来源 + ]; + + return successJson($data); + } catch (\Exception $e) { + return errorJson('获取互动分析数据失败:' . $e->getMessage()); + } + } + + /** + * 获取时间范围 + * + * @param bool $toTimestamp 是否将日期转为时间戳,默认为true + * @return array 时间范围数组 + */ + private function getTimeRange($toTimestamp = true) + { + // 可选:today, yesterday, this_week, last_week, this_month, this_quarter, this_year + $timeType = input('time_type', 'this_week'); + + switch ($timeType) { + case 'today': // 今日 + $startTime = date('Y-m-d'); + $endTime = date('Y-m-d', strtotime('+1 day')); + $lastStartTime = date('Y-m-d', strtotime('-1 day')); // 昨日 + $lastEndTime = $startTime; + break; + + case 'yesterday': // 昨日 + $startTime = date('Y-m-d', strtotime('-1 day')); + $endTime = date('Y-m-d'); + $lastStartTime = date('Y-m-d', strtotime('-2 day')); // 前日 + $lastEndTime = $startTime; + break; + + case 'this_week': // 本周 + $startTime = date('Y-m-d', strtotime('monday this week')); + $endTime = date('Y-m-d', strtotime('monday next week')); + $lastStartTime = date('Y-m-d', strtotime('monday last week')); // 上周一 + $lastEndTime = $startTime; + break; + + case 'last_week': // 上周 + $startTime = date('Y-m-d', strtotime('monday last week')); + $endTime = date('Y-m-d', strtotime('monday this week')); + $lastStartTime = date('Y-m-d', strtotime('monday last week', strtotime('last week'))); // 上上周一 + $lastEndTime = $startTime; + break; + + case 'this_month': // 本月 + $startTime = date('Y-m-01'); + $endTime = date('Y-m-d', strtotime(date('Y-m-01') . ' +1 month')); + $lastStartTime = date('Y-m-01', strtotime('-1 month')); // 上月初 + $lastEndTime = $startTime; + break; + + case 'this_quarter': // 本季度 + $month = date('n'); + $quarter = ceil($month / 3); + $startMonth = ($quarter - 1) * 3 + 1; + $startTime = date('Y-') . str_pad($startMonth, 2, '0', STR_PAD_LEFT) . '-01'; + $endTime = date('Y-m-d', strtotime($startTime . ' +3 month')); + // 上季度 + $lastStartTime = date('Y-m-d', strtotime($startTime . ' -3 month')); + $lastEndTime = $startTime; + break; + + case 'this_year': // 本年度 + $startTime = date('Y-01-01'); + $endTime = (date('Y') + 1) . '-01-01'; + $lastStartTime = (date('Y') - 1) . '-01-01'; // 去年初 + $lastEndTime = $startTime; + break; + + default: + $startTime = date('Y-m-d', strtotime('monday this week')); + $endTime = date('Y-m-d', strtotime('monday next week')); + $lastStartTime = date('Y-m-d', strtotime('monday last week')); + $lastEndTime = $startTime; + } + + // 如果需要转换为时间戳 + if ($toTimestamp) { + $startTime = strtotime($startTime); + $endTime = strtotime($endTime); + $lastStartTime = strtotime($lastStartTime); + $lastEndTime = strtotime($lastEndTime); + } + + return [ + 'start_time' => $startTime, + 'end_time' => $endTime, + 'last_start_time' => $lastStartTime, + 'last_end_time' => $lastEndTime + ]; + } + + /** + * 计算环比增长率 + */ + private function calculateGrowth($current, $last) + { + if ($last == 0) { + return $current > 0 ? 100 : 0; + } + return round((($current - $last) / $last) * 100, 1); + } +} \ No newline at end of file diff --git a/application/store/controller/SystemConfigController.php b/application/store/controller/SystemConfigController.php new file mode 100644 index 0000000..6eaf8cf --- /dev/null +++ b/application/store/controller/SystemConfigController.php @@ -0,0 +1,151 @@ +device['id'] ?? 0; + if (!$deviceId) { + return $this->error('设备不存在'); + } + + // 从新表中获取配置 + $config = Db::name('device_taskconf') + ->where('deviceId', $deviceId) + ->field('id,autoLike,autoCustomerDev,groupMessageDeliver,autoGroup,contentSync,aiChat,autoReply,momentsSync') + ->find(); + + // 如果没有找到配置,创建默认配置 + if (empty($config)) { + $taskConfig = [ + 'deviceId' => $deviceId, + 'autoLike' => 0, + 'autoCustomerDev' => 0, + 'groupMessageDeliver' => 0, + 'autoGroup' => 0, + 'contentSync' => 0, + 'aiChat' => 0, + 'autoReply' => 0, + 'momentsSync' => 0, + 'companyId' => $this->device['companyId'] ?? 0, + 'createTime' => time(), + 'updateTime' => time() + ]; + + // 添加到数据库 + Db::name('device_taskconf')->insert($taskConfig); + + // 返回默认配置 + return successJson($taskConfig); + } + + // 返回开关状态 + return successJson($config); + + } catch (\Exception $e) { + Log::error('获取开关状态异常:' . $e->getMessage()); + return $this->error('获取开关状态失败'); + } + } + + /** + * 更新系统开关状态 + * + * @return \think\Response + */ + public function updateSwitchStatus() + { + try { + // 获取参数 + if (empty($this->device)) { + return errorJson('设备不存在'); + } + + $switchName = $this->request->param('switchName'); + $deviceId = $this->device['id']; + + if (empty($switchName)) { + return errorJson('开关名称不能为空'); + } + + // 验证开关名称是否有效 + $validSwitches = ['autoLike', 'autoCustomerDev', 'groupMessageDeliver', 'autoGroup', 'contentSync', 'aiChat', 'autoReply', 'momentsSync']; + if (!in_array($switchName, $validSwitches)) { + return errorJson('无效的开关名称'); + } + + // 获取当前配置 + $taskConfig = Db::name('device_taskconf') + ->where('deviceId', $deviceId) + ->find(); + + // 如果没有找到配置,创建默认配置 + if (empty($taskConfig)) { + $taskConfig = [ + 'deviceId' => $deviceId, + 'autoLike' => 0, + 'autoCustomerDev' => 0, + 'groupMessageDeliver' => 0, + 'autoGroup' => 0, + 'contentSync' => 0, + 'aiChat' => 0, + 'autoReply' => 0, + 'momentsSync' => 0, + 'companyId' => $this->device['companyId'] ?? 0, + 'createTime' => time(), + 'updateTime' => time() + ]; + + // 设置要更新的开关 + $taskConfig[$switchName] = 1; + + // 添加到数据库 + Db::name('device_taskconf')->insert($taskConfig); + } else { + // 更新指定开关状态 + $updateData = [ + $switchName => !$taskConfig[$switchName], + 'updateTime' => time() + ]; + + // 更新数据库 + $result = Db::name('device_taskconf') + ->where('deviceId', $deviceId) + ->update($updateData); + + if ($result === false) { + Log::error("更新设备{$switchName}开关状态失败,设备ID:{$deviceId}"); + return errorJson('更新失败'); + } + } + + // 清除缓存 + $this->clearDeviceCache(); + + return successJson([], '更新成功'); + + } catch (\Exception $e) { + return errorJson('系统错误'. $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/application/store/controller/TrafficPackage.php b/application/store/controller/TrafficPackage.php new file mode 100644 index 0000000..664ce70 --- /dev/null +++ b/application/store/controller/TrafficPackage.php @@ -0,0 +1,97 @@ +field([ + 'id', + 'name', + 'tags', + 'originalPrice', + 'price', + 'monthlyTraffic', + 'duration', + 'privileges', + 'createTime' + ])->select(); + + // 处理数据 + $list = collection($list)->each(function($item) { + // 添加计算字段 + $item['discount'] = $item->discount; // 折扣 + $item['totalTraffic'] = $item->totalTraffic; // 总流量 + // 确保特权是数组格式 + $item['privileges'] = $item->privileges; // 使用模型的获取器处理特权 + // 格式化时间 + $item['createTime'] = date('Y-m-d H:i:s', strtotime($item['createTime'])); + return $item; + }); + return successJson($list,'获取成功'); + } + + + /** + * 获取当前套餐使用情况 + * @return \think\response\Json + */ + public function getUsage() + { + // 获取用户ID,可以从session或token中获取 + $userId = input('userId', 0, 'intval'); + if (empty($userId)) { + return errorJson('请先登录'); + } + + // 获取用户当前生效的套餐订单 + $order = model('TrafficPackageOrder') + ->where('userId', $userId) + ->where('status', 1) // 1表示生效中 + ->where('expireTime', '>', time()) // 未过期 + ->order('expireTime', 'desc') // 取最晚过期的 + ->find(); + + if (empty($order)) { + return errorJson('未找到有效的套餐'); + } + + // 获取套餐详情 + $package = TrafficPackageModel::get($order['packageId']); + if (empty($package)) { + return errorJson('套餐信息不存在'); + } + + // 计算套餐使用情况 + $totalUsers = $package['monthlyTraffic'] * $package['duration']; // 总人数 + $usedUsers = model('TrafficUsageLog') + ->where('orderId', $order['id']) + ->count(); // 已使用人数 + + // 计算剩余有效期(天数) + $remainDays = ceil(($order['expireTime'] - time()) / (60 * 60 * 24)); + $remainDays = max(0, $remainDays); // 确保不会出现负数 + + $data = [ + 'packageName' => $package['name'], // 套餐名称 + 'totalUsers' => $totalUsers, // 总人数 + 'usedUsers' => $usedUsers, // 已使用人数 + 'remainUsers' => $totalUsers - $usedUsers, // 剩余可用人数 + 'remainDays' => $remainDays, // 剩余有效期(天) + 'expireTime' => date('Y-m-d', $order['expireTime']), // 过期时间 + 'usagePercent' => $totalUsers > 0 ? round(($usedUsers / $totalUsers) * 100, 1) : 0, // 使用百分比 + ]; + + return successJson($data, '获取成功'); + } + +} \ No newline at end of file diff --git a/application/store/controller/VendorController.php b/application/store/controller/VendorController.php new file mode 100644 index 0000000..929bade --- /dev/null +++ b/application/store/controller/VendorController.php @@ -0,0 +1,534 @@ +request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + $status = $this->request->param('status', ''); + + $where = [ + ['isDel', '=', 0] + ]; + + // 关键词搜索 + if (!empty($keyword)) { + $where[] = ['name', 'like', "%{$keyword}%"]; + } + + // 状态筛选 + if ($status !== '') { + $where[] = ['status', '=', $status]; + } + + $list = VendorPackageModel::where($where) + ->order('id', 'desc') + ->page($page, $limit) + ->select(); + + $total = VendorPackageModel::where($where)->count(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + ] + ]); + } catch (\Exception $e) { + Log::error('获取套餐列表失败:' . $e->getMessage()); + return json(['code' => 500, 'msg' => '获取失败:' . $e->getMessage()]); + } + } + + /** + * 获取套餐详情 + * + * @return \think\response\Json + */ + public function detail() + { + try { + $id = $this->request->param('id', 0); + + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + // 查询套餐基本信息 + $package = VendorPackageModel::where([ + ['id', '=', $id], + ['isDel', '=', 0] + ])->find(); + + if (empty($package)) { + return json(['code' => 404, 'msg' => '套餐不存在']); + } + + // 查询项目列表 + $projects = VendorProjectModel::where([ + ['packageId', '=', $id], + ['isDel', '=', 0] + ])->select(); + + $package['projects'] = $projects; + + return json(['code' => 200, 'msg' => '获取成功', 'data' => $package]); + } catch (\Exception $e) { + Log::error('获取套餐详情失败:' . $e->getMessage()); + return json(['code' => 500, 'msg' => '获取失败:' . $e->getMessage()]); + } + } + + /** + * 添加套餐 + * + * @return \think\response\Json + */ + public function add() + { + try { + if (!$this->request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + $param = $this->request->post(); + + // 参数验证 + if (empty($param['name'])) { + return json(['code' => 400, 'msg' => '套餐名称不能为空']); + } + + // 检查名称是否已存在 + $exists = VendorPackageModel::where([ + ['name', '=', $param['name']], + ['isDel', '=', 0] + ])->find(); + + if ($exists) { + return json(['code' => 400, 'msg' => '该套餐名称已存在']); + } + + Db::startTrans(); + try { + // 创建套餐 + $package = new VendorPackageModel; + $package->name = $param['name']; + $package->originalPrice = $param['originalPrice'] ?? 0; + $package->price = $param['price'] ?? 0; + $package->discount = $param['discount'] ?? 0; + $package->advancePayment = $param['advancePayment'] ?? 0; + $package->tags = $param['tags'] ?? ''; + $package->description = $param['description'] ?? ''; + $package->cover = $param['cover'] ?? ''; + $package->status = $param['status'] ?? 1; + $package->createTime = time(); + $package->updateTime = time(); + $package->save(); + + // 处理项目信息 + if (!empty($param['projects']) && is_array($param['projects'])) { + foreach ($param['projects'] as $projectData) { + if (empty($projectData['name'])) { + continue; + } + + // 创建项目 + $project = new VendorProjectModel; + $project->packageId = $package->id; + $project->name = $projectData['name']; + $project->originalPrice = $projectData['originalPrice'] ?? 0; + $project->price = $projectData['price'] ?? 0; + $project->duration = $projectData['duration'] ?? 0; + $project->image = $projectData['image'] ?? ''; + $project->detail = $projectData['detail'] ?? ''; + $project->createTime = time(); + $project->updateTime = time(); + $project->save(); + } + } + + Db::commit(); + return json(['code' => 200, 'msg' => '添加成功', 'data' => ['id' => $package->id]]); + } catch (\Exception $e) { + Db::rollback(); + Log::error('添加套餐失败:' . $e->getMessage()); + return json(['code' => 500, 'msg' => '添加失败:' . $e->getMessage()]); + } + } catch (\Exception $e) { + Log::error('添加套餐异常:' . $e->getMessage()); + return json(['code' => 500, 'msg' => '添加异常:' . $e->getMessage()]); + } + } + + /** + * 编辑套餐 + * + * @return \think\response\Json + */ + public function edit() + { + try { + if (!$this->request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + $param = $this->request->post(); + + // 参数验证 + if (empty($param['id'])) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + if (empty($param['name'])) { + return json(['code' => 400, 'msg' => '套餐名称不能为空']); + } + + // 检查套餐是否存在 + $package = VendorPackageModel::where([ + ['id', '=', $param['id']], + ['isDel', '=', 0] + ])->find(); + + if (!$package) { + return json(['code' => 404, 'msg' => '套餐不存在']); + } + + // 检查名称是否已存在 + $exists = VendorPackageModel::where([ + ['name', '=', $param['name']], + ['id', '<>', $param['id']], + ['isDel', '=', 0] + ])->find(); + + if ($exists) { + return json(['code' => 400, 'msg' => '该套餐名称已存在']); + } + + Db::startTrans(); + try { + // 更新套餐 + $package->name = $param['name']; + $package->originalPrice = $param['originalPrice'] ?? $package->originalPrice; + $package->price = $param['price'] ?? $package->price; + $package->discount = $param['discount'] ?? $package->discount; + $package->advancePayment = $param['advancePayment'] ?? $package->advancePayment; + $package->tags = $param['tags'] ?? $package->tags; + $package->description = $param['description'] ?? $package->description; + $package->cover = $param['cover'] ?? $package->cover; + $package->status = $param['status'] ?? $package->status; + $package->updateTime = time(); + $package->save(); + + Db::commit(); + return json(['code' => 200, 'msg' => '更新成功']); + } catch (\Exception $e) { + Db::rollback(); + Log::error('更新套餐失败:' . $e->getMessage()); + return json(['code' => 500, 'msg' => '更新失败:' . $e->getMessage()]); + } + } catch (\Exception $e) { + Log::error('编辑套餐异常:' . $e->getMessage()); + return json(['code' => 500, 'msg' => '编辑异常:' . $e->getMessage()]); + } + } + + /** + * 删除套餐 + * + * @return \think\response\Json + */ + public function delete() + { + try { + $id = $this->request->param('id', 0); + + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + // 检查套餐是否存在 + $package = VendorPackageModel::where([ + ['id', '=', $id], + ['isDel', '=', 0] + ])->find(); + + if (!$package) { + return json(['code' => 404, 'msg' => '套餐不存在']); + } + + Db::startTrans(); + try { + // 软删除套餐 + $package->isDel = 1; + $package->updateTime = time(); + $package->save(); + + // 软删除关联的项目 + VendorProjectModel::where('packageId', $id) + ->update([ + 'isDel' => 1, + 'updateTime' => time() + ]); + + Db::commit(); + return json(['code' => 200, 'msg' => '删除成功']); + } catch (\Exception $e) { + Db::rollback(); + Log::error('删除套餐失败:' . $e->getMessage()); + return json(['code' => 500, 'msg' => '删除失败:' . $e->getMessage()]); + } + } catch (\Exception $e) { + Log::error('删除套餐异常:' . $e->getMessage()); + return json(['code' => 500, 'msg' => '删除异常:' . $e->getMessage()]); + } + } + + /** + * 添加项目 + * + * @return \think\response\Json + */ + public function addProject() + { + try { + if (!$this->request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + $param = $this->request->post(); + + // 参数验证 + if (empty($param['packageId'])) { + return json(['code' => 400, 'msg' => '套餐ID不能为空']); + } + + if (empty($param['name'])) { + return json(['code' => 400, 'msg' => '项目名称不能为空']); + } + + // 检查套餐是否存在 + $package = VendorPackageModel::where([ + ['id', '=', $param['packageId']], + ['isDel', '=', 0] + ])->find(); + + if (!$package) { + return json(['code' => 404, 'msg' => '套餐不存在']); + } + + try { + // 创建项目 + $project = new VendorProjectModel; + $project->packageId = $param['packageId']; + $project->name = $param['name']; + $project->originalPrice = $param['originalPrice'] ?? 0; + $project->price = $param['price'] ?? 0; + $project->duration = $param['duration'] ?? 0; + $project->image = $param['image'] ?? ''; + $project->detail = $param['detail'] ?? ''; + $project->createTime = time(); + $project->updateTime = time(); + $project->save(); + + return json(['code' => 200, 'msg' => '添加成功', 'data' => ['id' => $project->id]]); + } catch (\Exception $e) { + Log::error('添加项目失败:' . $e->getMessage()); + return json(['code' => 500, 'msg' => '添加失败:' . $e->getMessage()]); + } + } catch (\Exception $e) { + Log::error('添加项目异常:' . $e->getMessage()); + return json(['code' => 500, 'msg' => '添加异常:' . $e->getMessage()]); + } + } + + /** + * 编辑项目 + * + * @return \think\response\Json + */ + public function editProject() + { + try { + if (!$this->request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + $param = $this->request->post(); + + // 参数验证 + if (empty($param['id'])) { + return json(['code' => 400, 'msg' => '项目ID不能为空']); + } + + if (empty($param['name'])) { + return json(['code' => 400, 'msg' => '项目名称不能为空']); + } + + // 检查项目是否存在 + $project = VendorProjectModel::where([ + ['id', '=', $param['id']], + ['isDel', '=', 0] + ])->find(); + + if (!$project) { + return json(['code' => 404, 'msg' => '项目不存在']); + } + + try { + // 更新项目 + $project->name = $param['name']; + $project->originalPrice = $param['originalPrice'] ?? $project->originalPrice; + $project->price = $param['price'] ?? $project->price; + $project->duration = $param['duration'] ?? $project->duration; + $project->image = $param['image'] ?? $project->image; + $project->detail = $param['detail'] ?? $project->detail; + $project->updateTime = time(); + $project->save(); + + return json(['code' => 200, 'msg' => '更新成功']); + } catch (\Exception $e) { + Log::error('更新项目失败:' . $e->getMessage()); + return json(['code' => 500, 'msg' => '更新失败:' . $e->getMessage()]); + } + } catch (\Exception $e) { + Log::error('编辑项目异常:' . $e->getMessage()); + return json(['code' => 500, 'msg' => '编辑异常:' . $e->getMessage()]); + } + } + + /** + * 删除项目 + * + * @return \think\response\Json + */ + public function deleteProject() + { + try { + $id = $this->request->param('id', 0); + + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + // 检查项目是否存在 + $project = VendorProjectModel::where([ + ['id', '=', $id], + ['isDel', '=', 0] + ])->find(); + + if (!$project) { + return json(['code' => 404, 'msg' => '项目不存在']); + } + + try { + // 软删除项目 + $project->isDel = 1; + $project->updateTime = time(); + $project->save(); + + return json(['code' => 200, 'msg' => '删除成功']); + } catch (\Exception $e) { + Log::error('删除项目失败:' . $e->getMessage()); + return json(['code' => 500, 'msg' => '删除失败:' . $e->getMessage()]); + } + } catch (\Exception $e) { + Log::error('删除项目异常:' . $e->getMessage()); + return json(['code' => 500, 'msg' => '删除异常:' . $e->getMessage()]); + } + } + + /** + * 创建订单 + * + * @return \think\response\Json + */ + public function createOrder() + { + try { + if (!$this->request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + $param = $this->request->post(); + + // 参数验证 + if (empty($param['packageId'])) { + return json(['code' => 400, 'msg' => '套餐ID不能为空']); + } + + // 检查套餐是否存在 + $package = VendorPackageModel::where([ + ['id', '=', $param['packageId']], + ['isDel', '=', 0], + ['status', '=', 1] + ])->find(); + + if (!$package) { + return json(['code' => 404, 'msg' => '套餐不存在或已下架']); + } + + // 获取当前用户信息 + $userId = $this->request->userInfo['id']; + + if (empty($userId)) { + return json(['code' => 401, 'msg' => '请先登录']); + } + + Db::startTrans(); + try { + // 生成订单 + $order = new VendorOrderModel; + $order->orderNo = VendorOrderModel::generateOrderNo(); + $order->userId = $userId; + $order->packageId = $package->id; + $order->packageName = $package->name; + $order->totalAmount = $package->price; + $order->payAmount = $package->price; + $order->advancePayment = $package->advancePayment; + $order->status = VendorOrderModel::STATUS_UNPAID; + $order->remark = $param['remark'] ?? ''; + $order->createTime = time(); + $order->updateTime = time(); + $order->save(); + + Db::commit(); + return json([ + 'code' => 200, + 'msg' => '订单创建成功', + 'data' => [ + 'orderId' => $order->id, + 'orderNo' => $order->orderNo + ] + ]); + } catch (\Exception $e) { + Db::rollback(); + Log::error('创建订单失败:' . $e->getMessage()); + return json(['code' => 500, 'msg' => '创建订单失败:' . $e->getMessage()]); + } + } catch (\Exception $e) { + Log::error('创建订单异常:' . $e->getMessage()); + return json(['code' => 500, 'msg' => '创建订单异常:' . $e->getMessage()]); + } + } +} \ No newline at end of file diff --git a/application/store/controller/VendorOrderController.php b/application/store/controller/VendorOrderController.php new file mode 100644 index 0000000..f63e224 --- /dev/null +++ b/application/store/controller/VendorOrderController.php @@ -0,0 +1,229 @@ +request->param('page', 1); + $limit = $this->request->param('limit', 10); + $status = $this->request->param('status', ''); + $keyword = $this->request->param('keyword', ''); + + // 获取当前用户信息 + $userId = $this->request->userInfo['id']; + + $where = [ + ['userId', '=', $userId] + ]; + + // 关键词搜索 + if (!empty($keyword)) { + $where[] = ['orderNo|packageName', 'like', "%{$keyword}%"]; + } + + // 状态筛选 + if ($status !== '') { + $where[] = ['status', '=', $status]; + } + + $list = VendorOrderModel::with(['package']) + ->where($where) + ->order('id', 'desc') + ->page($page, $limit) + ->select(); + + $total = VendorOrderModel::where($where)->count(); + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ] + ]); + } catch (\Exception $e) { + Log::error('获取订单列表失败:' . $e->getMessage()); + return json(['code' => 500, 'msg' => '获取失败:' . $e->getMessage()]); + } + } + + /** + * 获取订单详情 + * + * @return \think\response\Json + */ + public function detail() + { + try { + $id = $this->request->param('id', 0); + + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + // 获取当前用户信息 + $userId = $this->request->userInfo['id']; + + // 查询订单 + $order = VendorOrderModel::with(['package']) + ->where([ + ['id', '=', $id], + ['userId', '=', $userId] + ])->find(); + + if (empty($order)) { + return json(['code' => 404, 'msg' => '订单不存在']); + } + + // 查询套餐项目 + if (!empty($order['package'])) { + $projects = VendorProjectModel::where([ + ['packageId', '=', $order['packageId']], + ['isDel', '=', 0] + ])->select(); + + $order['package']['projects'] = $projects; + } + + return json(['code' => 200, 'msg' => '获取成功', 'data' => $order]); + } catch (\Exception $e) { + Log::error('获取订单详情失败:' . $e->getMessage()); + return json(['code' => 500, 'msg' => '获取失败:' . $e->getMessage()]); + } + } + + /** + * 更新订单状态 + * + * @return \think\response\Json + */ + public function updateStatus() + { + try { + if (!$this->request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + $param = $this->request->post(); + + // 参数验证 + if (empty($param['id'])) { + return json(['code' => 400, 'msg' => '订单ID不能为空']); + } + + if (!isset($param['status'])) { + return json(['code' => 400, 'msg' => '订单状态不能为空']); + } + + // 检查订单是否存在 + $order = VendorOrderModel::where('id', $param['id'])->find(); + + if (!$order) { + return json(['code' => 404, 'msg' => '订单不存在']); + } + + // 检查状态是否有效 + $validStatus = [ + VendorOrderModel::STATUS_UNPAID, + VendorOrderModel::STATUS_PAID, + VendorOrderModel::STATUS_COMPLETED, + VendorOrderModel::STATUS_CANCELED + ]; + + if (!in_array($param['status'], $validStatus)) { + return json(['code' => 400, 'msg' => '无效的订单状态']); + } + + // 更新订单状态 + $updateData = [ + 'status' => $param['status'], + 'updateTime' => time() + ]; + + // 如果订单状态为已支付,记录支付时间 + if ($param['status'] == VendorOrderModel::STATUS_PAID) { + $updateData['payTime'] = time(); + } + + try { + $order->save($updateData); + return json(['code' => 200, 'msg' => '更新成功']); + } catch (\Exception $e) { + Log::error('更新订单状态失败:' . $e->getMessage()); + return json(['code' => 500, 'msg' => '更新失败:' . $e->getMessage()]); + } + } catch (\Exception $e) { + Log::error('更新订单状态异常:' . $e->getMessage()); + return json(['code' => 500, 'msg' => '更新异常:' . $e->getMessage()]); + } + } + + /** + * 取消订单 + * + * @return \think\response\Json + */ + public function cancel() + { + try { + if (!$this->request->isPost()) { + return json(['code' => 400, 'msg' => '请求方式错误']); + } + + $id = $this->request->param('id', 0); + + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + // 获取当前用户信息 + $userId = $this->request->userInfo['id']; + + // 检查订单是否存在 + $order = VendorOrderModel::where([ + ['id', '=', $id], + ['userId', '=', $userId], + ['status', '=', VendorOrderModel::STATUS_UNPAID] + ])->find(); + + if (!$order) { + return json(['code' => 404, 'msg' => '订单不存在或状态不允许取消']); + } + + try { + // 更新订单状态为已取消 + $order->status = VendorOrderModel::STATUS_CANCELED; + $order->updateTime = time(); + $order->save(); + + return json(['code' => 200, 'msg' => '取消成功']); + } catch (\Exception $e) { + Log::error('取消订单失败:' . $e->getMessage()); + return json(['code' => 500, 'msg' => '取消失败:' . $e->getMessage()]); + } + } catch (\Exception $e) { + Log::error('取消订单异常:' . $e->getMessage()); + return json(['code' => 500, 'msg' => '取消异常:' . $e->getMessage()]); + } + } +} \ No newline at end of file diff --git a/application/store/model/FlowPackageModel.php b/application/store/model/FlowPackageModel.php new file mode 100644 index 0000000..1977aa8 --- /dev/null +++ b/application/store/model/FlowPackageModel.php @@ -0,0 +1,64 @@ + 'array', + ]; + + /** + * 特权字段获取器 - 将多行文本转换为数组 + * @param $value + * @return array + */ + public function getPrivilegesAttr($value) + { + if (empty($value)) { + return []; + } + + // 如果已经是数组则直接返回 + if (is_array($value)) { + return $value; + } + + // 按行分割文本 + return array_filter(explode("\n", $value)); + } + + /** + * 折扣获取器 - 根据原价和售价计算折扣 + * @param $value + * @param $data + * @return string + */ + public function getDiscountAttr($value, $data) + { + if (empty($data['originalPrice']) || $data['originalPrice'] <= 0) { + return '原价'; + } + + $discount = round(($data['price'] / $data['originalPrice']) * 10, 1); + return $discount . '折'; + } + + /** + * 总流量获取器 - 计算套餐总流量 + * @param $value + * @param $data + * @return int + */ + public function getTotalFlowAttr($value, $data) + { + return isset($data['monthlyFlow']) && isset($data['duration']) ? + intval($data['monthlyFlow']) * intval($data['duration']) : 0; + } +} \ No newline at end of file diff --git a/application/store/model/FlowPackageOrderModel.php b/application/store/model/FlowPackageOrderModel.php new file mode 100644 index 0000000..4bbe378 --- /dev/null +++ b/application/store/model/FlowPackageOrderModel.php @@ -0,0 +1,93 @@ + 'integer', + 'userId' => 'integer', + 'packageId' => 'integer', + 'amount' => 'float', + 'duration' => 'integer', + 'createTime' => 'timestamp', + 'updateTime' => 'timestamp', + 'payTime' => 'timestamp', + 'status' => 'integer', + 'payStatus' => 'integer', + 'isDel' => 'integer', + ]; + + /** + * 生成订单号 + * 规则:LL + 年月日时分秒 + 5位随机数 + * + * @return string + */ + public static function generateOrderNo() + { + $prefix = 'LL'; + $date = date('YmdHis'); + $random = mt_rand(10000, 99999); + + return $prefix . $date . $random; + } + + /** + * 创建订单 + * + * @param int $userId 用户ID + * @param int $packageId 套餐ID + * @param string $packageName 套餐名称 + * @param float $amount 订单金额 + * @param int $duration 购买时长(月) + * @param string $payType 支付类型 (wechat|alipay|nopay) + * @param string $remark 备注 + * @return array|false + */ + public static function createOrder($userId, $packageId, $packageName, $amount, $duration, $payType = 'wechat', $remark = '') + { + // 生成订单号 + $orderNo = self::generateOrderNo(); + + // 订单数据 + $data = [ + 'userId' => $userId, + 'packageId' => $packageId, + 'packageName' => $packageName, + 'orderNo' => $orderNo, + 'amount' => $amount, + 'duration' => $duration, + 'payType' => $payType, + 'createTime' => time(), + 'status' => 0, // 0:待支付 1:已完成 2:已取消 3:已退款 + 'payStatus' => $payType == 'nopay' ? 10 : 0, // 0:未支付 1:已支付 10:无需支付 + 'remark' => $remark, + 'isDel' => 0, + ]; + + // 创建订单 + $model = new self(); + $result = $model->save($data); + + if ($result) { + return $model->toArray(); + } else { + return false; + } + } +} \ No newline at end of file diff --git a/application/store/model/TrafficOrderModel.php b/application/store/model/TrafficOrderModel.php new file mode 100644 index 0000000..4d51d66 --- /dev/null +++ b/application/store/model/TrafficOrderModel.php @@ -0,0 +1,11 @@ + 'timestamp', + 'updateTime' => 'timestamp', + 'expireTime' => 'timestamp', + ]; +} \ No newline at end of file diff --git a/application/store/model/TrafficUsageLog.php b/application/store/model/TrafficUsageLog.php new file mode 100644 index 0000000..8f5b46b --- /dev/null +++ b/application/store/model/TrafficUsageLog.php @@ -0,0 +1,14 @@ + 'timestamp', + 'updateTime' => 'timestamp', + ]; +} \ No newline at end of file diff --git a/application/store/model/UserFlowPackageModel.php b/application/store/model/UserFlowPackageModel.php new file mode 100644 index 0000000..5569bf0 --- /dev/null +++ b/application/store/model/UserFlowPackageModel.php @@ -0,0 +1,103 @@ +where('status', 1) // 1表示有效 + ->where('expireTime', '>', time()) // 未过期 + ->order('expireTime', 'asc') // 按到期时间排序,最先到期的排在前面 + ->find(); + } + + /** + * 创建用户套餐订阅记录 + * + * @param int $userId 用户ID + * @param int $packageId 套餐ID + * @param int $duration 套餐时长(月) + * @return bool 是否创建成功 + */ + public static function createSubscription($userId, $packageId, $duration = 0) + { + if (empty($userId) || empty($packageId)) { + return false; + } + + // 获取套餐信息 + $package = FlowPackageModel::where('id', $packageId)->where('isDel', 0)->find(); + if (empty($package)) { + return false; + } + + // 如果未指定时长,则使用套餐默认时长 + if (empty($duration)) { + $duration = $package['duration']; + } + + // 计算开始时间和到期时间 + $now = time(); + $startTime = $now; + $expireTime = strtotime("+{$duration} month", $now); + + // 创建新订阅 + $data = [ + 'userId' => $userId, + 'packageId' => $packageId, + 'duration' => $duration, + 'totalFlow' => $package->totalFlow, + 'usedFlow' => 0, + 'status' => 1, // 1表示有效 + 'startTime' => $startTime, + 'expireTime' => $expireTime, + 'createTime' => $now, + 'updateTime' => $now + ]; + + return self::create($data) ? true : false; + } + + /** + * 更新用户已使用流量 + * + * @param int $id 用户套餐ID + * @param int $usedFlow 已使用流量 + * @return bool 是否更新成功 + */ + public static function updateUsedFlow($id, $usedFlow) + { + if (empty($id)) { + return false; + } + + $userPackage = self::where('id', $id)->find(); + if (empty($userPackage)) { + return false; + } + + // 确保使用量不超过总量 + $maxFlow = $userPackage['totalFlow']; + $usedFlow = $usedFlow > $maxFlow ? $maxFlow : $usedFlow; + + return self::where('id', $id)->update([ + 'usedFlow' => $usedFlow, + 'updateTime' => time() + ]) ? true : false; + } +} \ No newline at end of file diff --git a/application/store/model/VendorModel.php b/application/store/model/VendorModel.php new file mode 100644 index 0000000..e28896a --- /dev/null +++ b/application/store/model/VendorModel.php @@ -0,0 +1,34 @@ +hasMany('VendorPackageModel', 'vendorId', 'id') + ->where('isDel', 0); + } +} \ No newline at end of file diff --git a/application/store/model/VendorOrderModel.php b/application/store/model/VendorOrderModel.php new file mode 100644 index 0000000..ab1fc89 --- /dev/null +++ b/application/store/model/VendorOrderModel.php @@ -0,0 +1,45 @@ +belongsTo('VendorPackageModel', 'packageId', 'id'); + } + + /** + * 生成唯一订单号 + * @return string + */ + public static function generateOrderNo() + { + return date('YmdHis') . rand(1000, 9999); + } +} \ No newline at end of file diff --git a/application/store/model/VendorPackageModel.php b/application/store/model/VendorPackageModel.php new file mode 100644 index 0000000..bccb054 --- /dev/null +++ b/application/store/model/VendorPackageModel.php @@ -0,0 +1,50 @@ +hasMany('VendorProjectModel', 'packageId', 'id') + ->where('isDel', 0); + } + + /** + * 标签获取器 + */ + public function getTagsAttr($value) + { + return $value ? explode(',', $value) : []; + } + + /** + * 标签修改器 + */ + public function setTagsAttr($value) + { + return is_array($value) ? implode(',', $value) : $value; + } +} \ No newline at end of file diff --git a/application/store/model/VendorProjectModel.php b/application/store/model/VendorProjectModel.php new file mode 100644 index 0000000..a9add85 --- /dev/null +++ b/application/store/model/VendorProjectModel.php @@ -0,0 +1,33 @@ +belongsTo('VendorPackageModel', 'packageId', 'id'); + } +} \ No newline at end of file diff --git a/application/store/model/WechatFriendModel.php b/application/store/model/WechatFriendModel.php new file mode 100644 index 0000000..0dcb758 --- /dev/null +++ b/application/store/model/WechatFriendModel.php @@ -0,0 +1,12 @@ + +// +---------------------------------------------------------------------- + +// +---------------------------------------------------------------------- +// | Cookie设置 +// +---------------------------------------------------------------------- +return [ + // cookie 名称前缀 + 'prefix' => '', + // cookie 保存时间 + 'expire' => 0, + // cookie 保存路径 + 'path' => '/', + // cookie 有效域名 + 'domain' => '', + // cookie 启用安全传输 + 'secure' => false, + // httponly设置 + 'httponly' => '', + // 是否使用 setcookie + 'setcookie' => true, + // 跨站需要 + 'SameSite' => 'None', +]; diff --git a/application/superadmin/config/route.php b/application/superadmin/config/route.php new file mode 100644 index 0000000..337cdfc --- /dev/null +++ b/application/superadmin/config/route.php @@ -0,0 +1,52 @@ +middleware(['app\superadmin\middleware\AdminAuth']); \ No newline at end of file diff --git a/application/superadmin/controller/BaseController.php b/application/superadmin/controller/BaseController.php new file mode 100644 index 0000000..bbd609e --- /dev/null +++ b/application/superadmin/controller/BaseController.php @@ -0,0 +1,46 @@ +request->adminInfo; + + if (!$admin) { + throw new \Exception('未授权访问,缺少有效的身份凭证', 401); + } + + return $column ? $admin[$column] : $admin; + } +} \ No newline at end of file diff --git a/application/superadmin/controller/Menu/GetMenuTreeController.php b/application/superadmin/controller/Menu/GetMenuTreeController.php new file mode 100644 index 0000000..1a62183 --- /dev/null +++ b/application/superadmin/controller/Menu/GetMenuTreeController.php @@ -0,0 +1,179 @@ +buildMenuTree($menus, $menu['id']); + + if (!empty($children)) { + $menu['children'] = $children; + } + + $tree[] = $menu; + } + } + + return $tree; + } + + /** + * 获取管理员权限 + * + * @return array + */ + protected function getPermissions(): array + { + $record = AdministratorPermissionsModel::where('adminId', $this->getAdminInfo('id'))->find(); + + if (!$record || empty($record->permissions)) { + return []; + } + + $permissions = $record->permissions ? json_decode($record->permissions, true) : []; + + if (isset($permissions['ids']) && !empty($permissions['ids'])) { + return is_string($permissions['ids']) ? explode(',', $permissions['ids']) : $permissions['ids']; + } + + return []; + } + + /** + * 获取所有菜单,并组织成树状结构 + * + * @return array + */ + protected function getMenuTree(): array + { + // 获取所有菜单 + $allMenus = MenuModel::where('status', MenuModel::STATUS_ACTIVE)->order('sort', 'asc')->select()->toArray(); + + // 组织成树状结构 + return $allMenus ? $this->buildMenuTree($allMenus) : []; + } + + /** + * 获取所有一级菜单(用户拥有权限的) + * + * @param array $permissionIds + * @return array + */ + protected function getTopMenusInPermissionIds(array $permissionIds): array + { + $where = [ + 'parentId' => MenuModel::TOP_LEVEL, + 'status' => MenuModel::STATUS_ACTIVE, + ]; + + return MenuModel::where($where)->whereIn('id', $permissionIds)->order('sort', 'asc')->select()->toArray(); + } + + /** + * 获取所有子菜单. + * + * @param array $topMenuIds + * @return array + */ + protected function getAllChildrenInPermissionIds(array $topMenuIds): array + { + return MenuModel::where('status', MenuModel::STATUS_ACTIVE)->whereIn('parentId', $topMenuIds)->order('sort', 'asc')->select()->toArray(); + } + + /** + * 获取用户菜单 + * + * @param array $permissionIds + * @return array + */ + protected function getUserMenus(array $permissionIds): array + { + $topMenus = $this->getTopMenusInPermissionIds($permissionIds); + + // 菜单ID集合,用于获取子菜单 + $childMenus = $this->getAllChildrenInPermissionIds( + array_column($topMenus, 'id') + ); + + return $this->_makeMenuTree($topMenus, $childMenus); + } + + /** + * 构建菜单树. + * + * @param array $topMenus + * @param array $childMenus + * @return array + */ + protected function _makeMenuTree(array $topMenus, array $childMenus): array + { + // 将子菜单按照父ID进行分组 + $childMenusGroup = []; + + foreach ($childMenus as $menu) { + $childMenusGroup[$menu['parentId']][] = $menu; + } + + foreach ($topMenus as $topMenu) { + if (isset($childMenusGroup[$topMenu['id']])) { + $topMenu['children'] = $childMenusGroup[$topMenu['id']]; + } + + $menuTree[] = $topMenu; + } + + return $menuTree ?? []; + } + + /** + * 根据权限ID获取相应的菜单树 + * + * @param array $permissionIds 权限ID数组 + * @return array + */ + protected function getMenuTreeByPermissions(array $permissionIds): array + { + // 如果没有权限,返回空数组 + return $permissionIds ? $this->getUserMenus($permissionIds) : []; + } + + /** + * 获取菜单列表(树状结构) + * @return \think\response\Json + */ + public function index() + { + if ($this->getAdminInfo('id') == AdministratorModel::MASTER_ID) { + $menuTree = $this->getMenuTree(); + } else { + $menuTree = $this->getMenuTreeByPermissions( + $this->getPermissions() + ); + } + + return ResponseHelper::success($menuTree); + } +} \ No newline at end of file diff --git a/application/superadmin/controller/Menu/GetTopLevelForPermissionController.php b/application/superadmin/controller/Menu/GetTopLevelForPermissionController.php new file mode 100644 index 0000000..20c0632 --- /dev/null +++ b/application/superadmin/controller/Menu/GetTopLevelForPermissionController.php @@ -0,0 +1,40 @@ + MenuModel::TOP_LEVEL, + 'status' => MenuModel::STATUS_NORMAL + ]; + + return MenuModel::where($where)->field('id, title')->order('sort', 'asc')->select()->toArray(); + } + + /** + * 获取一级菜单(供权限设置使用) + * + * @return \think\response\Json + */ + public function index() + { + $menus = $this->getTopLevelMenus(); + + return ResponseHelper::success($menus); + } +} \ No newline at end of file diff --git a/application/superadmin/controller/administrator/AddAdministratorController.php b/application/superadmin/controller/administrator/AddAdministratorController.php new file mode 100644 index 0000000..22a6a75 --- /dev/null +++ b/application/superadmin/controller/administrator/AddAdministratorController.php @@ -0,0 +1,150 @@ +count() > 0; + + if ($exists) { + throw new \Exception('账号已存在', 400); + } + } + + /** + * 数据验证 + * + * @param array $params + * @return $this + * @throws \Exception + */ + protected function dataValidate(array $params): self + { + $validate = Validate::make([ + 'account' => 'require|regex:^[a-zA-Z0-9]+$|/\S+/', + 'username' => 'require|/\S+/', + 'password' => 'require|/\S+/', + 'permissionIds' => 'require|array', + ], [ + 'account.require' => '账号不能为空', + 'account.regex' => '账号只能用数字或者字母或者数字字母组合', + 'username.require' => '用户名不能为空', + 'password.require' => '密码不能为空', + 'permissionIds.require' => '请至少分配一种权限', + ]); + + if (!$validate->check($params)) { + throw new \Exception($validate->getError(), 400); + } + + return $this; + } + + /** + * 判断是否有权限修改 + * + * @return $this + */ + protected function checkPermission(): self + { + if ($this->getAdminInfo('id') != AdministratorModel::MASTER_ID) { + throw new \Exception('您没有权限添加管理员', 403); + } + + return $this; + } + + /** + * 保存管理员权限 + * + * @param int $adminId 管理员ID + * @param array $permissionIds 权限ID数组 + * @return bool + */ + protected function savePermissions(int $adminId, array $permissionIds) + { + $record = AdministratorPermissionsModel::where('adminId', $adminId)->find(); + + $permissionData = [ + 'ids' => is_array($permissionIds) ? implode(',', $permissionIds) : $permissionIds + ]; + + if ($record) { + return $record->save([ + 'permissions' => json_encode($permissionData), + ]); + } else { + return AdministratorPermissionsModel::create([ + 'adminId' => $adminId, + 'permissions' => json_encode($permissionData), + ]); + } + } + + /** + * 添加管理员信息 + * + * @param array $params + * @return AdministratorModel + * @throws \Exception + */ + protected function addAdministrator(array $params): AdministratorModel + { + $result = AdministratorModel::create(array_merge($params, ['password' => md5($params['password'])])); + + if (!$result) { + throw new \Exception('添加管理员失败', 401); + } + + return $result; + } + + /** + * 添加管理员 + * + * @return \think\response\Json + */ + public function index() + { + try { + $params = $this->request->only(['account', 'username', 'password', 'permissionIds']); + + $this->dataValidate($params); + $this->checkPermission()->chekAdminIsExist($params['account']); + + Db::startTrans(); + $admin = $this->addAdministrator($params); + + // 保存权限 + if (!empty($params['permissionIds'])) { + $this->savePermissions($admin->id, $params['permissionIds']); + } + + Db::commit(); + return ResponseHelper::success(); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/superadmin/controller/administrator/DeleteAdministratorController.php b/application/superadmin/controller/administrator/DeleteAdministratorController.php new file mode 100644 index 0000000..0f7dea2 --- /dev/null +++ b/application/superadmin/controller/administrator/DeleteAdministratorController.php @@ -0,0 +1,128 @@ +find(); + + if (!$admin) { + throw new \Exception('管理员不存在', 404); + } + + if (!$admin->delete()) { + throw new \Exception('管理员删除失败', 400); + } + } + + /** + * 删除管理员权限 + * + * @param int $adminId + * @return void + * @throws \Exception + */ + protected function deletePermission(int $adminId): void + { + $permission = AdministratorPermissionsModel::where('adminId', $adminId)->find(); + + if (!$permission->delete()) { + throw new \Exception('管理员权限移除失败', 400); + } + } + + /** + * 删除账号的限制条件 + * + * @param int $adminId + * @return void + * @throws \Exception + */ + protected function canNotDeleteSelf(int $adminId) + { + // 不能删除自己的账号 + if ($this->getAdminInfo('id') == $adminId) { + throw new \Exception('不能删除自己的账号', 403); + } + + // 只有超级管理员(ID为1)可以删除管理员 + if ($this->getAdminInfo('id') != AdministratorModel::MASTER_ID) { + throw new \Exception('您没有权限删除管理员', 403); + } + + // 不能删除超级管理员账号 + if ($adminId == AdministratorModel::MASTER_ID) { + throw new \Exception('不能删除超级管理员账号', 403); + } + } + + /** + * 数据验证 + * + * @param array $params + * @return $this + * @throws \Exception + */ + protected function dataValidate(array $params): self + { + $validate = Validate::make([ + 'id' => 'require|regex:/^[1-9]\d*$/', + ], [ + 'id.regex' => '非法请求', + 'id.require' => '非法请求', + ]); + + if (!$validate->check($params)) { + throw new \Exception($validate->getError(), 400); + } + + return $this; + } + + /** + * 删除管理员 + * + * @return \think\response\Json + */ + public function index() + { + try { + $params = $this->request->only('id'); + $adminId = $params['id']; + + $this->dataValidate($params)->canNotDeleteSelf($adminId); + + Db::startTrans(); + + $this->deleteAdmin($adminId); + $this->deletePermission($adminId); + + Db::commit(); + + return ResponseHelper::success(); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/superadmin/controller/administrator/GetAdministratorDetailController.php b/application/superadmin/controller/administrator/GetAdministratorDetailController.php new file mode 100644 index 0000000..25200dd --- /dev/null +++ b/application/superadmin/controller/administrator/GetAdministratorDetailController.php @@ -0,0 +1,108 @@ +field([ + 'a.id', 'a.account', 'a.username', 'a.status', 'a.authId', 'a.createTime createdAt', 'a.lastLoginTime lastLogin', + 'p.permissions' + ]) + ->leftJoin('administrator_permissions p', 'a.id = p.adminId') + ->where('a.id', $adminId) + ->find(); + + if (!$admin) { + throw new \Exception('管理员不存在', 404); + } + + return $admin; + } + + /** + * 解析权限数据 + * + * @param string|null $permission + * @return array + */ + protected function parsePermissions(?string $permission): array + { + $permissionIds = []; + + if (!empty($permission)) { + $permissions = json_decode($permission, true); + $permissions = is_array($permissions) ? $permissions : json_decode($permissions, true); + + if (isset($permissions['ids'])) { + $permissionIds = is_string($permissions['ids']) ? explode(',', $permissions['ids']) : $permissions['ids']; + $permissionIds = array_map('intval', $permissionIds); + } + } + + return $permissionIds; + } + + /** + * 根据权限ID获取角色名称 + * + * @param int $authId + * @return string + */ + protected function getRoleName($authId): string + { + switch ($authId) { + case 1: + return '超级管理员'; + case 2: + return '项目管理员'; + case 3: + return '客户管理员'; + default: + return '普通管理员'; + } + } + + /** + * 获取详细信息 + * + * @param int $id 管理员ID + * @return \think\response\Json + */ + public function index($id) + { + try { + $admin = $this->getAdministrator($id); + $roleName = $this->getRoleName($admin->authId); + $permissionIds = $this->parsePermissions($admin->permissions); + + return ResponseHelper::success( + array_merge($admin->toArray(), [ + 'roleName' => $roleName, + 'permissions' => $permissionIds, + 'lastLogin' => $admin->lastLogin ? date('Y-m-d H:i', $admin->lastLogin) : '从未登录', + 'createdAt' => date('Y-m-d H:i', $admin->createdAt), + ]) + ); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/superadmin/controller/administrator/GetAdministratorListController.php b/application/superadmin/controller/administrator/GetAdministratorListController.php new file mode 100644 index 0000000..6bf4e7a --- /dev/null +++ b/application/superadmin/controller/administrator/GetAdministratorListController.php @@ -0,0 +1,175 @@ +request->param('keyword/s', ''))) { + $where[] = ['account|username', 'like', "%{$keyword}%"]; + } + + return array_merge($params, $where); + } + + /** + * 获取管理员列表 + * + * @param array $where 查询条件 + * @return \think\Paginator 分页对象 + */ + protected function getAdministratorList(array $where): \think\Paginator + { + $query = AdministratorModel::alias('a') + ->field([ + 'a.id', 'a.account', 'a.username', 'a.status', 'a.authId', 'a.createTime createdAt', 'a.lastLoginTime', 'a.lastLoginIp' + ]); + + foreach ($where as $key => $value) { + if (is_numeric($key) && is_array($value) && isset($value[0]) && $value[0] === 'exp') { + $query->whereExp('', $value[1]); + continue; + } + + $query->where($key, $value); + } + + return $query->paginate($this->request->param('limit/d', 10), false, ['page' => $this->request->param('page/d', 1)]); + } + + /** + * 根据权限ID获取角色名称 + * + * @param int $authId 权限ID + * @return string + */ + protected function getRoleName($authId): string + { + switch ($authId) { + case 1: + return '超级管理员'; + case 2: + return '项目管理员'; + case 3: + return '客户管理员'; + default: + return '普通管理员'; + } + } + + /** + * 获取管理员权限 + * + * @param int $adminId + * @return array + */ + protected function _getPermissions(int $adminId): array + { + $record = AdministratorPermissionsModel::where('adminId', $adminId)->find(); + + if (!$record || empty($record->permissions)) { + return []; + } + + $permissions = $record->permissions ? json_decode($record->permissions, true) : []; + + if (isset($permissions['ids']) && !empty($permissions['ids'])) { + return is_string($permissions['ids']) ? explode(',', $permissions['ids']) : $permissions['ids']; + } + + return []; + } + + /** + * 通过菜单的id获取菜单的名字 + * + * @param array $ids + * @return array + */ + protected function getMenusNameByIds(array $ids): array + { + return MenuModel::whereIn('id', $ids)->column('title'); + } + + /** + * 根据权限ID获取权限列表 + * + * @param int $authId 权限ID + * @return array + */ + protected function getPermissions(int $authId): array + { + $ids = $this->_getPermissions($authId); + + if ($ids) { + return $this->getMenusNameByIds($ids); + } + + return []; + } + + /** + * 构建返回数据 + * + * @param \think\Paginator $list + * @return array + */ + protected function makeReturnedResult(\think\Paginator $list): array + { + $result = []; + + foreach ($list->items() as $item) { + $section = [ + 'id' => $item->id, + 'account' => $item->account, + 'username' => $item->username, + 'status' => $item->status, + 'createdAt' => date('Y-m-d H:i:s', $item->createdAt), + 'lastLogin' => !empty($item->lastLoginTime) ? date('Y-m-d H:i:s', $item->lastLoginTime) : '从未登录', + 'role' => $this->getRoleName($item->authId), + 'permissions' => $this->getPermissions($item->id), + ]; + + array_push($result, $section); + } + + return $result; + } + + /** + * 获取管理员列表 + * + * @return \think\response\Json + */ + public function index() + { + $where = $this->makeWhere(); + $result = $this->getAdministratorList($where); + + return ResponseHelper::success( + [ + 'list' => $this->makeReturnedResult($result), + 'total' => $result->total(), + ] + ); + } +} \ No newline at end of file diff --git a/application/superadmin/controller/administrator/UpdateAdministratorController.php b/application/superadmin/controller/administrator/UpdateAdministratorController.php new file mode 100644 index 0000000..9e886c0 --- /dev/null +++ b/application/superadmin/controller/administrator/UpdateAdministratorController.php @@ -0,0 +1,154 @@ +save($params)) { + throw new \Exception('记录更新失败', 402); + } + } + + /** + * 数据验证 + * + * @param array $params + * @return $this + * @throws \Exception + */ + protected function dataValidate(array $params): self + { + $validate = Validate::make([ + 'id' => 'require|regex:/^[1-9]\d*$/', + 'account' => 'require|regex:^[a-zA-Z0-9]+$|/\S+/', + 'username' => 'require|/\S+/', + 'password' => '/\S+/', + 'permissionIds' => 'array', + ], [ + 'id.require' => '缺少必要参数', + 'account.require' => '账号不能为空', + 'account.regex' => '账号只能用数字或者字母或者数字字母组合', + 'username.require' => '用户名不能为空', + 'permissionIds.array' => '请至少分配一种权限', + ]); + + if (!$validate->check($params)) { + throw new \Exception($validate->getError(), 400); + } + + return $this; + } + + /** + * 判断是否有权限修改 + * + * @param int $adminId + * @param array $params + * @return $this + */ + protected function checkPermission(int $adminId, array $params): self + { + $currentAdminId = $this->getAdminInfo('id'); + + if ($currentAdminId != AdministratorModel::MASTER_ID && $currentAdminId != $adminId) { + throw new \Exception('您没有权限修改其他管理员', 403); + } + + if ($params['id'] != AdministratorModel::MASTER_ID && empty($params['permissionIds'])) { + throw new \Exception('请至少分配一种权限', 403); + } + + return $this; + } + + /** + * 保存管理员权限 + * + * @param int $adminId + * @param array $permissionIds + * @return bool + */ + protected function savePermissions(int $adminId, array $permissionIds) + { + $record = AdministratorPermissionsModel::where('adminId', $adminId)->find(); + + $permissionData = [ + 'ids' => is_array($permissionIds) ? implode(',', $permissionIds) : $permissionIds + ]; + + if ($record) { + return $record->save([ + 'permissions' => json_encode($permissionData), + ]); + } else { + return AdministratorPermissionsModel::create([ + 'adminId' => $adminId, + 'permissions' => json_encode($permissionData), + ]); + } + } + + /** + * 更新管理员信息 + * + * @return \think\response\Json + */ + public function index() + { + try { + $params = $this->request->only(['id', 'account', 'username', 'password', 'permissionIds']); + + // 被修改的管理员id + $adminId = $params['id'] ?? 0; + + $this->dataValidate($params)->checkPermission($adminId, $params); + + Db::startTrans(); + + $this->udpateAdministrator($params); + + // 如果当前是超级管理员(ID为1),并且修改的不是自己,则更新权限 + if ($this->getAdminInfo('id') == AdministratorModel::MASTER_ID + && $this->getAdminInfo('id') != $adminId + && !empty($params['permissionIds']) + ) { + $this->savePermissions($adminId, $params['permissionIds']); + } + + Db::commit(); + return ResponseHelper::success(); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/superadmin/controller/auth/AuthLoginController.php b/application/superadmin/controller/auth/AuthLoginController.php new file mode 100644 index 0000000..0c52e29 --- /dev/null +++ b/application/superadmin/controller/auth/AuthLoginController.php @@ -0,0 +1,166 @@ +id . '|' . $admin->account . 'cunkebao_admin_secret'); + } + + /** + * 数据验证 + * + * @param array $params + * @return $this + * @throws \Exception + */ + protected function dataValidate(array $params): self + { + $validate = Validate::make([ + 'account' => 'require|/\S+/', + 'password' => 'require|/\S+/', + ]); + + if (!$validate->check($params)) { + throw new \Exception($validate->getError(), 400); + } + + return $this; + } + + /** + * 获取管理员信息 + * + * @param array $params + * @return object|AdministratorModel + * @throws \Exception + */ + protected function getAdministrator(array $params): AdministratorModel + { + extract($params); + + $admin = AdministratorModel::where(['account' => $account])->find(); + + if (!$admin || + $admin->password !== $password || + $admin->deleteTime + ) { + throw new \Exception('账号不存在或密码错误', 404); + } + + if (!$admin->status) { + throw new \Exception('账号已禁用', 404); + } + + return $admin; + } + + /** + * 更新登录信息 + * + * @param AdministratorModel $admin + * @return $this + */ + protected function saveLoginInfo(AdministratorModel $admin): self + { + $admin->lastLoginTime = time(); + $admin->lastLoginIp = $this->request->ip(); + + if (!$admin->save()) { + throw new \Exception('拒绝登录', 403); + } + + return $this; + } + + /** + * 设置登录Cookie,有效期24小时 + * + * @param AdministratorModel $admin + * @return void + */ + protected function setCookie(AdministratorModel $admin): void + { + // 获取当前环境 + $env = app()->env->get('APP_ENV', 'production'); + + // 获取请求的域名 + $origin = $this->request->header('origin'); + $domain = ''; + + if ($origin) { + // 解析域名 + $parsedUrl = parse_url($origin); + if (isset($parsedUrl['host'])) { + // 如果是测试环境,使用完整的域名 + if ($env === 'testing') { + $domain = $parsedUrl['host']; + } else { + // 生产环境使用顶级域名 + $parts = explode('.', $parsedUrl['host']); + if (count($parts) > 1) { + $domain = '.' . $parts[count($parts) - 2] . '.' . $parts[count($parts) - 1]; + } + } + } + } + + // 设置cookie选项 + $options = [ + 'expire' => 86400, + 'path' => '/', + 'httponly' => true, + 'samesite' => 'None', // 允许跨域 + 'secure' => true // 仅 HTTPS 下有效 + ]; + + // 如果有域名,添加到选项 + if ($domain) { + $options['domain'] = $domain; + } + + // 设置cookies + Cookie::set('admin_id', $admin->id, $options); + Cookie::set('admin_token', $this->createToken($admin), $options); + } + + /** + * 管理员登录 + * + * @return \think\response\Json + */ + public function index() + { + try { + $params = $this->request->only(['account', 'password']); + + $admin = $this->dataValidate($params)->getAdministrator($params); + $this->saveLoginInfo($admin)->setCookie($admin); + + return ResponseHelper::success( + [ + 'id' => $admin->id, + 'name' => $admin->username, + 'account' => $admin->account, + 'token' => Cookie::get('admin_token') + ] + ); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/superadmin/controller/company/CreateCompanyController.php b/application/superadmin/controller/company/CreateCompanyController.php new file mode 100644 index 0000000..fcbbc3b --- /dev/null +++ b/application/superadmin/controller/company/CreateCompanyController.php @@ -0,0 +1,288 @@ +setBaseUrl(Env::get('rpc.API_BASE_URL')) + ->setMethod('post') + ->send('/v1/api/account/create', $params); + + $result = json_decode($response, true); + + if ($result['code'] != 200) { + throw new Exception($result['msg'], 210 . $result['code']); + } + + return $result['data'] ?: null; + } + + /** + * S2 创建部门并返回id + * + * @param array $params + * @return array + */ + protected function s2CreateDepartmentAndUser(array $params): ?array + { + $params = ArrHelper::getValue('name=departmentName,memo=departmentMemo,account=accountName,password=accountPassword,username=accountRealName,username=accountNickname,accountMemo', $params); + + // 创建公司部门 + $response = CurlHandle::getInstant() + ->setBaseUrl(Env::get('rpc.API_BASE_URL')) + ->setMethod('post') + ->send('/v1/api/account/createNewAccount', $params); + + $result = json_decode($response, true); + + if ($result['code'] != 200) { + throw new Exception($result['msg'], 210 . $result['code']); + } + + return $result['data'] ?: null; + } + + /** + * 数据验证 + * + * @param array $params + * @return $this + * @throws Exception + */ + protected function dataValidate(array $params): self + { + $validate = Validate::make([ + 'name' => 'require|max:50|/\S+/', + 'account' => 'require|regex:^[a-zA-Z0-9]+$|max:20|/\S+/', + 'username' => 'require|max:20|/\S+/', + 'phone' => 'require|regex:/^1[3-9]\d{9}$/', + 'status' => 'require|in:0,1', + 'password' => 'require|/\S+/', + 'memo' => '/\S+/', + ], [ + 'name.require' => '请输入项目名称', + 'account.require' => '请输入账号', + 'account.max' => '账号长度受限', + 'account.regex' => '账号只能用数字或者字母或者数字字母组合', + 'username.require' => '请输入用户昵称', + 'phone.require' => '请输入手机号', + 'phone.regex' => '手机号格式错误', + 'status.require' => '缺少重要参数', + 'status.in' => '非法参数', + 'password.require' => '请输入密码', + ]); + + if (!$validate->check($params)) { + throw new Exception($validate->getError(), 400); + } + + return $this; + } + + /** + * 设备创建分组 + * + * @param array $params + * @return void + * @throws Exception + */ + protected function s2CreateDeviceGroup(array $params): void + { + $respon = (new DeviceController())->createGroup($params, true); + $respon = json_decode($respon, true); + + if ($respon['code'] != 200) { + throw new Exception('设备分组添加错误', 210 . $respon['code']); + } + } + + /** + * S2 部分 + * + * @param array $params + * @return array + * @throws Exception + */ + protected function creatS2About(array $params): array + { + $department = $this->s2CreateDepartmentAndUser($params); + + if (!$department || !isset($department['id']) || !isset($department['departmentId'])) { + throw new Exception('S2返参异常', 210402); + } + + // 设备创建分组 + $this->s2CreateDeviceGroup(['groupName' => $params['name']]); + + return array_merge($params, [ + 'companyId' => $department['departmentId'], + 's2_accountId' => $department['id'], + ]); + } + + /** + * 存客宝创建项目 + * + * @param array $params + * @return void + * @throws Exception + */ + protected function ckbCreateCompany(array $params): void + { + $params = ArrHelper::getValue('companyId=id,companyId,name,memo,status', $params); + $result = CompanyModel::create($params); + + if (!$result) { + throw new Exception('创建公司记录失败', 402); + } + } + + /** + * 创建功能账号,不可登录,也非管理员,用户也不可见. + * + * @param array $params + * @return void + * @throws Exception + */ + protected function createFuncUsers(array $params): void + { + $seedCols = [ + ['account' => $params['account'] . '_01', 'username' => $params['username'] . '_子账号01', 'status' => UsersModel::ADMIN_STP, 'isAdmin' => UsersModel::ADMIN_OTP, 'typeId' => UsersModel::MASTER_USER], + ['account' => $params['account'] . '_02', 'username' => $params['username'] . '_子账号02', 'status' => UsersModel::ADMIN_STP, 'isAdmin' => UsersModel::ADMIN_OTP, 'typeId' => UsersModel::MASTER_USER], + ['account' => $params['account'] . '_03', 'username' => $params['username'] . '_子账号03', 'status' => UsersModel::ADMIN_STP, 'isAdmin' => UsersModel::ADMIN_OTP, 'typeId' => UsersModel::MASTER_USER], + ['account' => $params['account'] . '_offline', 'username' => $params['username'] . '_处理离线专用', 'status' => UsersModel::STATUS_STOP, 'isAdmin' => UsersModel::ADMIN_OTP, 'typeId' => UsersModel::NOT_USER], + ['account' => $params['account'] . '_delete', 'username' => $params['username'] . '_处理删除专用', 'status' => UsersModel::STATUS_STOP, 'isAdmin' => UsersModel::ADMIN_OTP, 'typeId' => UsersModel::NOT_USER], + ]; + + foreach ($seedCols as $seeds) { + $this->s2CreateUser(array_merge($params, ArrHelper::getValue('account,username', $seeds))); + $this->ckbCreateUser(array_merge($params, $seeds)); + } + } + + /** + * 存客宝创建账号 + * + * @param array $params + * @return void + * @throws Exception + */ + protected function ckbCreateUser(array $params): void + { + $params = ArrHelper::getValue('username,account,password,companyId,s2_accountId,status,phone,isAdmin,typeId', $params); + + $params = array_merge($params, [ + 'passwordLocal' => localEncrypt($params['password']), + 'passwordMd5' => md5($params['password']), + ]); + + if (!UsersModel::create($params)) { + throw new Exception('创建用户记录失败', 402); + } + } + + /** + * @param array $params + * @return void + * @throws Exception + */ + protected function createCkbAbout(array $params) + { + // 1. 存客宝创建项目 + $this->ckbCreateCompany($params); + + // 2. 存客宝创建操盘手总账号 + $this->ckbCreateUser(array_merge($params, [ + 'isAdmin' => UsersModel::ADMIN_STP, // 主要账号默认1 + 'typeId' => UsersModel::MASTER_USER, // 类型:运营后台/操盘手传1、 门店传2 + ])); + } + + /** + * 检查项目名称是否已存在 + * + * @param array $where + * @return void + * @throws Exception + */ + protected function checkCompanyNameOrAccountOrPhoneExists(array $where): void + { + extract($where); + + // 项目名称尽量不重名 + $exists = CompanyModel::where(compact('name'))->count() > 0; + if ($exists) { + throw new Exception('项目名称已存在', 403); + } + + // 账号不重名 + $exists = UsersModel::where(compact('account'))->count() > 0; + if ($exists) { + throw new Exception('用户账号已存在', 403); + } + + // 手机号不重名 + $exists = UsersModel::where(compact('phone'))->count() > 0; + if ($exists) { + throw new Exception('手机号已存在', 403); + } + } + + + + /** + * 创建新项目 + * + * @return Json + */ + public function index() + { + try { + $params = $this->request->only(['name', 'status', 'username', 'account', 'password', 'phone', 'memo']); + $params = $this->dataValidate($params)->creatS2About($params); + + Db::startTrans(); + + $this->checkCompanyNameOrAccountOrPhoneExists(ArrHelper::getValue('name,account,phone', $params)); + $this->createCkbAbout($params); + + // 创建功能账号,不可登录,也非管理员,用户也不可见 + $this->createFuncUsers($params); + Db::commit(); + + return ResponseHelper::success(); + } catch (Exception $e) { + Db::rollback(); + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/superadmin/controller/company/DeleteCompanyController.php b/application/superadmin/controller/company/DeleteCompanyController.php new file mode 100644 index 0000000..5334d4c --- /dev/null +++ b/application/superadmin/controller/company/DeleteCompanyController.php @@ -0,0 +1,126 @@ + 'require|regex:/^[1-9]\d*$/', + ], [ + 'id.regex' => '非法请求', + 'id.require' => '非法请求', + ]); + + if (!$validate->check($params)) { + throw new \Exception($validate->getError(), 400); + } + + return $this; + } + + /** + * 删除项目 + * + * @param int $id + * @throws \Exception + */ + protected function deleteCompany(int $id): void + { + $company = CompanyModel::where('id', $id)->find(); + + if (!$company) { + throw new \Exception('项目不存在', 404); + } + + if (!$company->delete()) { + throw new \Exception('项目删除失败', 400); + } + } + + /** + * 删除用户 + * + * @param int $companId + * @throws \Exception + */ + protected function deleteUsers(int $companId): void + { + $users = UserModel::where('companyId', $companId)->select(); + + foreach ($users as $user) { + if (!$user->delete()) { + throw new \Exception($user->username . ' 用户删除失败', 400); + } + } + } + + /** + * 删除存客宝数据 + * + * @param int $companId + * @return self + * @throws \Exception + */ + protected function delteCkbAbout(int $companId): self + { + // 1. 删除项目 + $this->deleteCompany($companId); + + // 2. 删除用户 + $this->deleteUsers($companId); + + return $this; + } + + /** + * 删除 s2 数据 + * + * @return void + */ + protected function deleteS2About() + { + + } + + /** + * 删除项目 + * + * @return \think\response\Json + */ + public function index() + { + try { + $params = $this->request->only('id'); + $companId = $params['id']; + + Db::startTrans(); + + $this->dataValidate($params)->delteCkbAbout($companId)->deleteS2About($companId); + + Db::commit(); + return ResponseHelper::success(); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/superadmin/controller/company/GetCompanyDetailForProfileController.php b/application/superadmin/controller/company/GetCompanyDetailForProfileController.php new file mode 100644 index 0000000..787abd6 --- /dev/null +++ b/application/superadmin/controller/company/GetCompanyDetailForProfileController.php @@ -0,0 +1,114 @@ +column('wechatId'); + + return array_unique($wechatIds); + } + + /** + * 统计微信好友数量 + * + * @param int $companyId + * @return int + */ + protected function getFriendCountByCompanyId(int $companyId): int + { + $wechatIds = $this->getDeveiceWechats($companyId); + + return WechatFriendModel::whereIn('ownerWechatId', $wechatIds)->count(); + } + + /** + * 根据 CompanyId 获取设备数量 + * + * @param int $companyId + * @return int + */ + protected function getDeviceCountByCompanyId(int $companyId): int + { + return DeviceModel::where('companyId', $companyId)->count(); + } + + /** + * 根据 CompanyId 获取子账号数量 + * + * @param int $companyId + * @return int + */ + protected function getUsersCountByCompanyId(int $companyId): int + { + $where = array_merge(compact('companyId'), array('isAdmin' => UserModel::ADMIN_OTP)); + + return UserModel::where($where)->count(); + } + + /** + * 获取项目详情 + * + * @param int $id + * @return CompanyModel + * @throws \Exception + */ + protected function getCompanyDetail(int $id): array + { + $detail = CompanyModel::alias('c') + ->field([ + 'c.id', 'c.name', 'c.memo', 'c.companyId', 'c.createTime', + 'u.account', 'u.phone' + ]) + ->leftJoin('users u', 'c.companyId = u.companyId and u.isAdmin = ' . UserModel::ADMIN_STP) + ->find($id); + + if (!$detail) { + throw new \Exception('项目不存在', 404); + } + + return $detail->toArray(); + } + + /** + * 获取项目详情 + * + * @param int $id + * @return \think\response\Json + */ + public function index($id) + { + try { + $data = $this->getCompanyDetail($id); + + $userCount = $this->getUsersCountByCompanyId($id); + $deviceCount = $this->getDeviceCountByCompanyId($id); + $friendCount = $this->getFriendCountByCompanyId($id); + + return ResponseHelper::success( + array_merge($data, compact('deviceCount', 'friendCount', 'userCount')) + ); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/superadmin/controller/company/GetCompanyDetailForUpdateController.php b/application/superadmin/controller/company/GetCompanyDetailForUpdateController.php new file mode 100644 index 0000000..ddf63ae --- /dev/null +++ b/application/superadmin/controller/company/GetCompanyDetailForUpdateController.php @@ -0,0 +1,76 @@ +field([ + 'd.id', 'd.memo', 'd.model', 'd.brand', 'd.phone', 'd.imei', 'd.createTime', 'd.alive', + ]) + ->where('companyId', $companyId) + ->select() + ->toArray() ?: []; + } + + /** + * 获取项目详情 + * + * @param int $id + * @return CompanyModel + * @throws \Exception + */ + protected function getCompanyDetail(int $id): array + { + $detail = CompanyModel::alias('c') + ->field([ + 'c.id', 'c.name', 'c.status', 'c.memo', 'c.companyId', + 'u.account', 'u.username', 'u.phone', 'u.s2_accountId' + ]) + ->leftJoin('users u', 'c.companyId = u.companyId and u.isAdmin = ' . UserModel::ADMIN_STP) + ->find($id); + + if (!$detail) { + throw new \Exception('项目不存在', 404); + } + + return $detail->toArray(); + } + + /** + * 获取项目详情 + * + * @param int $id + * @return \think\response\Json + */ + public function index($id) + { + try { + $data = $this->getCompanyDetail($id); + $devices = $this->getDevicesByCompanyId($data['companyId']); + + return ResponseHelper::success( + array_merge($data, compact('devices')) + ); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/superadmin/controller/company/GetCompanyDevicesForProfileController.php b/application/superadmin/controller/company/GetCompanyDevicesForProfileController.php new file mode 100644 index 0000000..de71b5f --- /dev/null +++ b/application/superadmin/controller/company/GetCompanyDevicesForProfileController.php @@ -0,0 +1,105 @@ +request->param('companyId/d', 0); + + $devices = DeviceModel::alias('d') + ->field([ + 'd.id', 'd.memo', 'd.imei', 'd.phone', 'd.model', 'd.brand', 'd.alive', 'd.id deviceId' + ]) + ->where(compact('companyId')) + ->select() + ->toArray(); + + if (empty($devices)) { + throw new \Exception('暂无设备', 200); + } + + return $devices; + } + + /** + * 查询设备与微信的关联关系. + * + * @return array + */ + protected function getDeviceWechatRelationsByDeviceIds(array $deviceIds): array + { + // 获取设备最新登录记录的id + $latestLogs = DeviceWechatLoginModel::getDevicesLatestLogin($deviceIds); + + // 获取最新登录记录id + $latestIds = array_column($latestLogs, 'lastedId'); + + return DeviceWechatLoginModel::alias('d') + ->field([ + 'd.deviceId', 'd.wechatId', 'd.alive wAlive' + ]) + ->whereIn('id', $latestIds) + ->select() + ->toArray(); + } + + /** + * 获取设备的微信好友统计 + * + * @param array $ownerWechatId + * @return void + */ + protected function getWechatFriendsCount(array $deviceIds): array + { + // 查询设备与微信的关联关系 + $relations = $this->getDeviceWechatRelationsByDeviceIds($deviceIds); + + // 统计微信好友数量 + $friendCounts = WechatFriendShipModel::alias('f') + ->field([ + 'f.ownerWechatId wechatId', 'count(*) friendCount' + ]) + ->whereIn('ownerWechatId', array_column($relations, 'wechatId')) + ->group('ownerWechatId') + ->select() + ->toArray(); + + return ArrHelper::leftJoin($relations, $friendCounts, 'wechatId'); + } + + /** + * 获取公司关联的设备列表 + * + * @return \think\response\Json + */ + public function index() + { + try { + $devices = $this->getDevicesWithCompanyId(); + $friendCount = $this->getWechatFriendsCount(array_column($devices, 'id')); + + $result = ArrHelper::leftJoin($devices, $friendCount, 'deviceId'); + + return ResponseHelper::success($result); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/superadmin/controller/company/GetCompanyListController.php b/application/superadmin/controller/company/GetCompanyListController.php new file mode 100644 index 0000000..74c8abc --- /dev/null +++ b/application/superadmin/controller/company/GetCompanyListController.php @@ -0,0 +1,126 @@ +request->param('keyword/s', ''))) { + $where[] = ['name', 'like', "%{$keyword}%"]; + } + + return array_merge($params, $where); + } + + /** + * 获取设备统计 + * + * @return array + */ + protected function getDevices() + { + $devices = DeviceModel::field('companyId, count(id) as numCount')->group('companyId')->select(); + $devices = $devices ? $devices->toArray() : array(); + + return ArrHelper::columnTokey('companyId', $devices); + } + + + /** + * 获取项目列表 + * + * @param array $where 查询条件 + * @param int $page 页码 + * @param int $limit 每页数量 + * @return \think\Paginator 分页对象 + */ + protected function getCompanyList(array $where): \think\Paginator + { + $query = CompanyModel::alias('c') + ->field([ + 'c.id', 'c.name', 'c.status', 'c.companyId', 'c.memo', 'c.createTime' + ]); + + foreach ($where as $key => $value) { + if (is_numeric($key) && is_array($value) && isset($value[0]) && $value[0] === 'exp') { + $query->whereExp('', $value[1]); + continue; + } + + $query->where($key, $value); + } + + return $query->order('id', 'desc') + ->paginate($this->request->param('limit/d', 10), false, ['page' => $this->request->param('page/d', 1)]); + } + + /** + * 统计项目下的用户数量 + * + * @param int $companyId + * @return int + */ + protected function countUserInCompany(int $companyId): int + { + return UsersModel::where('companyId', $companyId)->count('id'); + } + + /** + * 构建返回数据 + * + * @param \think\Paginator $Companylist + * @return array + */ + protected function makeReturnedResult(\think\Paginator $Companylist): array + { + $result = []; + $devices = $this->getDevices(); + + foreach ($Companylist->items() as $item) { + $item->userCount = $this->countUserInCompany($item->companyId); + $item->deviceCount = $devices[$item->companyId]['numCount'] ?? 0; + + array_push($result, $item->toArray()); + } + + return $result; + } + + /** + * 获取项目列表 + * + * @return \think\response\Json + */ + public function index() + { + $where = $this->makeWhere(); + $result = $this->getCompanyList($where); + + return ResponseHelper::success( + [ + 'list' => $this->makeReturnedResult($result), + 'total' => $result->total(), + ] + ); + } +} \ No newline at end of file diff --git a/application/superadmin/controller/company/GetCompanySubusersForProfileController.php b/application/superadmin/controller/company/GetCompanySubusersForProfileController.php new file mode 100644 index 0000000..d9a02f8 --- /dev/null +++ b/application/superadmin/controller/company/GetCompanySubusersForProfileController.php @@ -0,0 +1,51 @@ + $this->request->param('companyId/d', 0), + 'isAdmin' => UserModel::ADMIN_OTP + ]; + + return UserModel::alias('u') + ->field([ + 'u.id', 'u.account', 'u.phone', 'u.username', 'u.avatar', 'u.status', 'u.createTime', 'u.typeId' + ]) + ->where($where) + ->select() + ->toArray(); + } + + /** + * 获取公司关联的设备列表 + * + * @return \think\response\Json + */ + public function index() + { + $users = $this->getSubusers(); + + foreach ($users as &$user) { + $user['createTime'] = date('Y-m-d H:i:s', $user['createTime']); + } + + return ResponseHelper::success($users); + } +} \ No newline at end of file diff --git a/application/superadmin/controller/company/UpdateCompanyController.php b/application/superadmin/controller/company/UpdateCompanyController.php new file mode 100644 index 0000000..aa6fac0 --- /dev/null +++ b/application/superadmin/controller/company/UpdateCompanyController.php @@ -0,0 +1,228 @@ +request->post('id/d', 0) + ); + + if (!$company) { + throw new \Exception('项目不存在', 404); + } + + // 外部使用 + $this->companyId = $company->id; + + return $company; + } + + /** + * 通过账号获取用户信息 + * + * @return UsersModel + * @throws \Exception + */ + protected function getUserDetailByCompanyId(): ?UsersModel + { + $where = [ + 'isAdmin' => UsersModel::MASTER_USER, // 必须保证 isAdmin 有且只有一个 + 'companyId' => $this->companyId, + ]; + + $user = UsersModel::where($where)->find(); + + if (!$user) { + throw new \Exception('用户不存在', 404); + } + + return $user; + } + + /** + * 更新项目信息 + * + * @param array $params + * @return void + * @throws \Exception + */ + protected function updateCompany(array $params): void + { + $params = ArrHelper::getValue('name,status,memo', $params); + $params = ArrHelper::rmValue($params); + + $company = $this->getCompanyDetailById(); + if (!$company->save($params)) { + throw new \Exception('项目更新失败', 403); + } + } + + /** + * 更新账号信息 + * + * @param array $params + * @return void + */ + protected function updateUserAccount(array $params): void + { + $params = ArrHelper::getValue('username,account,password,phone,status', $params); + $params = ArrHelper::rmValue($params); + + if (isset($params['password'])) { + $params['passwordMd5'] = md5($params['password']); + $params['passwordLocal'] = localEncrypt($params['password']); + } + + $user = $this->getUserDetailByCompanyId(); + if (!$user->save($params)) { + throw new \Exception('用户账号更新失败', 403); + } + } + + /** + * 更新存客宝端数据 + * + * @param array $params + * @return self + * @throws \Exception + */ + protected function updateCkbAbout(array $params): self + { + // 1. 更新项目信息 + $this->updateCompany($params); + + // 2. 更新账号信息 + $this->updateUserAccount($params); + + return $this; + } + + /** + * 更新触客宝端数据 + * + * @param array $params + * @return self + * @throws \Exception + */ + protected function updateS2About(array $params): self + { + // 1. 更新项目信息 + $this->updateCompany($params); + + // 2. 更新账号信息 + $this->updateUserAccount($params); + + return $this; + } + + /** + * 检查项目名称是否已存在(排除自身) + * + * @param array $where + * @return void + * @throws \Exception + */ + protected function checkCompanyNameOrAccountOrPhoneExists(array $where): void + { + extract($where); + + // 项目名称尽量不重名 + $exists = CompanyModel::where(compact('name'))->where('id', '<>', $id)->count() > 0; + if ($exists) { + throw new \Exception('项目名称已存在', 403); + } + + // TODO(数据迁移时,存客宝,主账号先查询出id,通过id查询出S2的最新信息,然后更新。) + $exists = UsersModel::where(compact('account'))->where('companyId', '<>', $id)->count() > 0; + if ($exists) { + throw new \Exception('用户账号已存在', 403); + } + + // 手机号不重复 + $exists = UsersModel::where(compact('phone'))->where('companyId', '<>', $id)->count() > 0; + if ($exists) { + throw new \Exception('手机号已存在', 403); + } + } + + /** + * 数据验证 + * + * @param array $params + * @return $this + * @throws \Exception + */ + protected function dataValidate(array $params): self + { + $validate = Validate::make([ + 'id' => 'require', + 'name' => 'require|max:50|/\S+/', + 'username' => 'require|max:20|/\S+/', + 'account' => 'require|regex:^[a-zA-Z0-9]+$|max:20|/\S+/', + 'phone' => 'require|regex:/^1[3-9]\d{9}$/', + 'status' => 'require|in:0,1' + ], [ + 'id.require' => '非法请求', + 'name.require' => '请输入项目名称', + 'username.require' => '请输入用户昵称', + 'account.require' => '请输入账号', + 'account.regex' => '账号只能用数字或者字母或者数字字母组合', + 'account.max' => '账号长度受限', + 'phone.require' => '请输入手机号', + 'phone.regex' => '手机号格式错误', + 'status.require' => '缺少重要参数', + 'status.in' => '非法参数', + ]); + + if (!$validate->check($params)) { + throw new \Exception($validate->getError(), 400); + } + + return $this; + } + + /** + * 更新项目信息 + * + * @return \think\response\Json + */ + public function index() + { + try { + $params = $this->request->only(['id', 'name', 'status', 'username', 'account', 'password', 'phone', 'memo']); + + // 数据验证 + $this->dataValidate($params); + $this->checkCompanyNameOrAccountOrPhoneExists(ArrHelper::getValue('id,name,account,phone', $params)); + + Db::startTrans(); + $this->updateCkbAbout($params)->updateS2About($params); + Db::commit(); + + return ResponseHelper::success(); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error($e->getMessage(), $e->getCode()); + } + } +} \ No newline at end of file diff --git a/application/superadmin/controller/dashboard/GetBasestatisticsController.php b/application/superadmin/controller/dashboard/GetBasestatisticsController.php new file mode 100644 index 0000000..e4dfc37 --- /dev/null +++ b/application/superadmin/controller/dashboard/GetBasestatisticsController.php @@ -0,0 +1,61 @@ + $this->getCompanyCount(), + 'adminCount' => $this->getAdminCount(), + 'customerCount' => $this->getDeviceCount(), + ] + ); + } +} \ No newline at end of file diff --git a/application/superadmin/controller/devices/GetAddResultedDevicesController.php b/application/superadmin/controller/devices/GetAddResultedDevicesController.php new file mode 100644 index 0000000..b877fe7 --- /dev/null +++ b/application/superadmin/controller/devices/GetAddResultedDevicesController.php @@ -0,0 +1,149 @@ +value('companyId'); + } + + /** + * 获取项目下的所有设备。 + * + * @param int $companyId + * @return array + */ + protected function getAllDevicesIdWithInCompany(int $companyId): array + { + return DeviceModel::where('companyId', $companyId)->column('id') ?: [0]; + } + + /** + * 执行数据迁移。 + * + * @param int $accountId + * @return void + */ + protected function migrateData(int $accountId): void + { + $companyId = $this->getCompanyIdByAccountId($accountId); + $deviceIds = $this->getAllDevicesIdWithInCompany($companyId) ?: [0]; + + // 从 s2_device 导入数据。 + $this->getNewDeviceFromS2_device($deviceIds, $companyId); + } + + /** + * 获取当前设备数量 + * + * @param int $accountId + * @return int + */ + protected function getCkbDeviceCount(int $accountId): int + { + return DeviceModel::where( + [ + 'companyId' => $this->getCompanyIdByAccountId($accountId) + ] + ) + ->count('*'); + } + + /** + * 从 s2_device 导入数据。 + * + * @param array $ids + * @param int $companyId + * @return void + */ + protected function getNewDeviceFromS2_device(array $ids, int $companyId): void + { + $ids = implode(',', $ids); + + $sql = "INSERT INTO ck_device(`id`, `imei`, `model`, phone, operatingSystem, memo, alive, brand, rooted, xPosed, softwareVersion, extra, createTime, updateTime, deleteTime, companyId) + SELECT + d.id, d.imei, d.model, d.phone, d.operatingSystem, d.memo, d.alive, d.brand, d.rooted, d.xPosed, d.softwareVersion, d.extra, d.createTime, d.lastUpdateTime, d.deleteTime, a.departmentId AS companyId + FROM s2_device d + JOIN s2_company_account a ON d.currentAccountId = a.id + WHERE isDeleted = 0 AND deletedAndStop = 0 AND d.id NOT IN ({$ids}) AND a.departmentId = {$companyId} + ON DUPLICATE KEY UPDATE + imei = VALUES(imei), + model = VALUES(model), + phone = VALUES(phone), + operatingSystem = VALUES(operatingSystem), + memo = VALUES(memo), + alive = VALUES(alive), + brand = VALUES(brand), + rooted = VALUES(rooted), + xPosed = VALUES(xPosed), + softwareVersion = VALUES(softwareVersion), + extra = VALUES(extra), + updateTime = VALUES(updateTime), + deleteTime = VALUES(deleteTime), + companyId = VALUES(companyId)"; + + Db::query($sql); + } + + /** + * 获取添加的关联设备结果。 + * + * @param int $accountId + * @return bool + */ + protected function getAddResulted(int $accountId): bool + { + $result = (new ApiDeviceController())->getlist( + [ + 'accountId' => $accountId, + 'pageIndex' => 0, + 'pageSize' => 100 + ], + true + ); + + $result = json_decode($result, true); + $result = $result['data']['results'][0] ?? false; + + return $result ? ( + count($result) > $this->getCkbDeviceCount($accountId) + ) : false; + } + + /** + * 获取基础统计信息 + * + * @return \think\response\Json + */ + public function index() + { + $accountId = $this->request->param('accountId/d'); + + $isAdded = $this->getAddResulted($accountId); + $isAdded && $this->migrateData($accountId); + + return ResponseHelper::success( + [ + 'added' => $isAdded + ] + ); + } +} \ No newline at end of file diff --git a/application/superadmin/controller/traffic/GetPoolDetailController.php b/application/superadmin/controller/traffic/GetPoolDetailController.php new file mode 100644 index 0000000..f968c7d --- /dev/null +++ b/application/superadmin/controller/traffic/GetPoolDetailController.php @@ -0,0 +1,126 @@ + 400, 'msg' => '参数错误']); + } + + try { + // 查询流量来源信息 + $sourceInfo = TrafficSourceModel::alias('ts') + ->join('company c', 'ts.companyId = c.companyId', 'LEFT') + ->field([ + 'ts.fromd as source', + 'ts.createTime as addTime', + 'c.name as projectName', + 'ts.identifier' + ]) + ->where('ts.id', $id) + ->find(); + + if (!$sourceInfo) { + return json(['code' => 404, 'msg' => '记录不存在']); + } + + // 查询客户池信息 + $poolInfo = TrafficPoolModel::where('identifier', $sourceInfo['identifier']) + ->field('wechatId') + ->find(); + + $result = [ + 'source' => $sourceInfo['source'], + 'addTime' => $sourceInfo['addTime'] ? date('Y-m-d H:i:s', $sourceInfo['addTime']) : null, + 'projectName' => $sourceInfo['projectName'] + ]; + + // 如果存在微信ID,查询微信账号信息 + if ($poolInfo && $poolInfo['wechatId']) { + // 查询微信账号信息 + $wechatInfo = WechatAccountModel::where('wechatId', $poolInfo['wechatId']) + ->field('avatar,nickname,region,gender') + ->find(); + + if ($wechatInfo) { + $result = array_merge($result, [ + 'avatar' => $wechatInfo['avatar'], + 'nickname' => $wechatInfo['nickname'], + 'region' => $wechatInfo['region'], + 'gender' => $this->formatGender($wechatInfo['gender']) + ]); + + // 查询标签信息 + $tagInfo = WechatTagModel::where('wechatId', $poolInfo['wechatId']) + ->field('tags') + ->find(); + + if ($tagInfo) { + $result['tags'] = is_string($tagInfo['tags']) ? + json_decode($tagInfo['tags'], true) : + $tagInfo['tags']; + } else { + $result['tags'] = []; + } + } + } else { + $result = array_merge($result, [ + 'avatar' => '', + 'nickname' => '未知', + 'region' => '未知', + 'gender' => $this->formatGender(0), + 'tags' => [] + ]); + } + + return json([ + 'code' => 200, + 'msg' => '获取成功', + 'data' => $result + ]); + + } catch (\Exception $e) { + return json([ + 'code' => 500, + 'msg' => '系统错误:' . $e->getMessage() + ]); + } + } + + /** + * 格式化性别显示 + * @param int $gender + * @return string + */ + protected function formatGender($gender) + { + switch ($gender) { + case 1: + return '男'; + case 2: + return '女'; + default: + return '保密'; + } + } +} \ No newline at end of file diff --git a/application/superadmin/controller/traffic/GetPoolListController.php b/application/superadmin/controller/traffic/GetPoolListController.php new file mode 100644 index 0000000..d19ce5d --- /dev/null +++ b/application/superadmin/controller/traffic/GetPoolListController.php @@ -0,0 +1,113 @@ +each(function ($item) { + $item->gender = $this->formatGender($item->gender); + $item->addTime = $this->formatDate($item->addTime); + $item->tags = $this->handlTags($item->tags); + }); + + return $list; + } + + /** + * 构建查询. + * + * @return TrafficPoolModel|\think\Paginator + */ + protected function gePoolList(): \think\Paginator + { + $query = TrafficPoolModel::alias('tp') + ->field([ + 'tp.wechatId', + 'ts.id', 'ts.createTime as addTime', 'ts.fromd as source', + 'c.name as projectName', + 'wa.avatar', 'wa.gender', 'wa.nickname', 'wa.region', + 'wt.tags' + ]) + ->join('traffic_source ts', 'tp.identifier = ts.identifier', 'RIGHT') + ->join('company c', 'ts.companyId = c.companyId', 'LEFT') + ->join('wechat_account wa', 'tp.wechatId = wa.wechatId', 'LEFT') + ->join('wechat_tag wt', 'wa.wechatId = wt.wechatId', 'LEFT'); + + return $query->paginate($this->request->param('limit/d', 10), false, ['page' => $this->request->param('page/d', 1)]); + } + + /** + * 获取客户池列表 + * + * @return \think\response\Json + */ + public function index() + { + $list = $this->gePoolList(); + + return ResponseHelper::success( + [ + 'list' => $this->makeReturnedValue($list)->items(), + 'total' => $list->total(), + 'page' => $list->currentPage(), + 'limit' => $list->listRows() + ] + ); + } +} \ No newline at end of file diff --git a/application/superadmin/middleware/AdminAuth.php b/application/superadmin/middleware/AdminAuth.php new file mode 100644 index 0000000..dc61d93 --- /dev/null +++ b/application/superadmin/middleware/AdminAuth.php @@ -0,0 +1,81 @@ +method(true) == 'OPTIONS') { + return $next($request); + } + + // 获取Cookie中的管理员信息 + $adminId = cookie('admin_id'); + $adminToken = cookie('admin_token'); + + // 如果没有登录信息,返回401未授权 + if (empty($adminId) || empty($adminToken)) { + return json([ + 'code' => 401, + 'msg' => '请先登录', + 'data' => null + ]); + } + + // 获取管理员信息 + $admin = Administrator::where([ + ['id', '=', $adminId], + ['status', '=', 1] + ])->find(); + + // 如果管理员不存在,返回401未授权 + if (!$admin) { + return json([ + 'code' => 401, + 'msg' => '管理员账号不存在或已被禁用', + 'data' => null + ]); + } + + // 验证Token是否有效 + $expectedToken = $this->createToken($admin); + + if ($adminToken !== $expectedToken) { + return json([ + 'code' => 401, + 'msg' => '登录已过期,请重新登录', + 'data' => null + ]); + } + + // 将管理员信息绑定到请求对象,方便后续控制器使用 + $request->adminInfo = $admin; + + // 继续执行后续操作 + return $next($request); + } + + /** + * 创建登录令牌 + * + * @param Administrator $admin + * @return string + */ + private function createToken($admin) + { + $data = $admin->id . '|' . $admin->account; + return md5($data . 'cunkebao_admin_secret'); + } +} \ No newline at end of file diff --git a/application/tags.php b/application/tags.php new file mode 100644 index 0000000..4b18d10 --- /dev/null +++ b/application/tags.php @@ -0,0 +1,28 @@ + +// +---------------------------------------------------------------------- + +// 应用行为扩展定义文件 +return [ + // 应用初始化 + 'app_init' => [], + // 应用开始 + 'app_begin' => [], + // 模块初始化 + 'module_init' => [], + // 操作开始执行 + 'action_begin' => [], + // 视图内容过滤 + 'view_filter' => [], + // 日志写入 + 'log_write' => [], + // 应用结束 + 'app_end' => [], +]; diff --git a/build.php b/build.php new file mode 100644 index 0000000..34ba3c8 --- /dev/null +++ b/build.php @@ -0,0 +1,26 @@ + +// +---------------------------------------------------------------------- + +return [ + // 生成应用公共文件 + '__file__' => ['common.php'], + + // 定义demo模块的自动生成 (按照实际定义的文件名生成) + 'demo' => [ + '__file__' => ['common.php'], + '__dir__' => ['behavior', 'controller', 'model', 'view'], + 'controller' => ['Index', 'Test', 'UserType'], + 'model' => ['User', 'UserType'], + 'view' => ['index/index'], + ], + + // 其他更多的模块定义 +]; diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..ce1721e --- /dev/null +++ b/composer.json @@ -0,0 +1,120 @@ +{ + "name": "topthink/think", + "description": "the new thinkphp framework", + "type": "project", + "keywords": [ + "framework", + "thinkphp", + "ORM" + ], + "homepage": "http://thinkphp.cn/", + "license": "Apache-2.0", + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + }, + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "require": { + "php": ">=5.6.0", + "topthink/framework": "5.1.41", + "topthink/think-installer": "2.*", + "topthink/think-captcha": "^2.0", + "topthink/think-helper": "^3.0", + "topthink/think-image": "^1.0", + "topthink/think-queue": "^2.0", + "topthink/think-worker": "^2.0", + "textalk/websocket": "^1.5", + "aliyuncs/oss-sdk-php": "^2.6", + "monolog/monolog": "^1.27", + "guzzlehttp/guzzle": "^6.5", + "overtrue/wechat": "~4.6", + "endroid/qr-code": "^3.9", + "phpoffice/phpspreadsheet": "^1.29", + "workerman/workerman": "^3.5", + "workerman/gateway-worker": "^3.0", + "hashids/hashids": "^2.0", + "khanamiryan/qrcode-detector-decoder": "^1.0", + "lizhichao/word": "^2.0", + "adbario/php-dot-notation": "^2.2" + }, + "require-dev": { + "symfony/var-dumper": "^3.4|^4.4", + "topthink/think-migration": "^2.0", + "phpunit/phpunit": "^5.0|^6.0" + }, + "autoload": { + "psr-4": { + "app\\": "application", + "Eison\\": "extend/Eison" + }, + "files": [ + "application/common.php" + ], + "homepage": "http://thinkphp.cn/", + "license": "Apache-2.0", + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + }, + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "require": { + "php": ">=5.6.0", + "topthink/framework": "5.1.41", + "topthink/think-installer": "~1.0", + "topthink/think-captcha": "^2.0", + "topthink/think-helper": "^3.0", + "topthink/think-image": "^1.0", + "topthink/think-queue": "^2.0", + "topthink/think-worker": "^2.0", + "textalk/websocket": "^1.2", + "aliyuncs/oss-sdk-php": "^2.3", + "monolog/monolog": "^1.24", + "guzzlehttp/guzzle": "^6.3", + "overtrue/wechat": "~4.0", + "endroid/qr-code": "^3.5", + "phpoffice/phpspreadsheet": "^1.8", + "workerman/workerman": "^3.5", + "workerman/gateway-worker": "^3.0", + "hashids/hashids": "^2.0", + "khanamiryan/qrcode-detector-decoder": "^1.0", + "lizhichao/word": "^2.0", + "adbario/php-dot-notation": "^2.2" + }, + "require-dev": { + "symfony/var-dumper": "^3.4", + "topthink/think-migration": "^2.0" + }, + "autoload": { + "psr-4": { + "app\\": "application", + "Eison\\": "extend/Eison" + }, + "files": [ + "application/common.php" + ], + "classmap": [] + }, + "extra": { + "think-path": "thinkphp" + }, + "config": { + "preferred-install": "dist", + "allow-plugins": { + "topthink/think-installer": true, + "easywechat-composer/easywechat-composer": true + } + }, + "minimum-stability": "dev", + "prefer-stable": true + } +} \ No newline at end of file diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..56ff4a9 --- /dev/null +++ b/composer.lock @@ -0,0 +1,4367 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "5bca28194f84a7d13965d1851e6ce38a", + "packages": [ + { + "name": "adbario/php-dot-notation", + "version": "2.5.0", + "source": { + "type": "git", + "url": "https://github.com/adbario/php-dot-notation.git", + "reference": "081e2cca50c84bfeeea2e3ef9b2c8d206d80ccae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/adbario/php-dot-notation/zipball/081e2cca50c84bfeeea2e3ef9b2c8d206d80ccae", + "reference": "081e2cca50c84bfeeea2e3ef9b2c8d206d80ccae", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^5.5 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8|^5.7|^6.6|^7.5|^8.5|^9.5", + "squizlabs/php_codesniffer": "^3.6" + }, + "type": "library", + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Adbar\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Riku Särkinen", + "email": "riku@adbar.io" + } + ], + "description": "PHP dot notation access to arrays", + "homepage": "https://github.com/adbario/php-dot-notation", + "keywords": [ + "ArrayAccess", + "dotnotation" + ], + "support": { + "issues": "https://github.com/adbario/php-dot-notation/issues", + "source": "https://github.com/adbario/php-dot-notation/tree/2.5.0" + }, + "time": "2022-10-14T20:31:46+00:00" + }, + { + "name": "aliyuncs/oss-sdk-php", + "version": "v2.7.2", + "source": { + "type": "git", + "url": "https://github.com/aliyun/aliyun-oss-php-sdk.git", + "reference": "483dd0b8bff5d47f0e4ffc99f6077a295c5ccbb5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aliyun/aliyun-oss-php-sdk/zipball/483dd0b8bff5d47f0e4ffc99f6077a295c5ccbb5", + "reference": "483dd0b8bff5d47f0e4ffc99f6077a295c5ccbb5", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "php-coveralls/php-coveralls": "*", + "phpunit/phpunit": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "OSS\\": "src/OSS" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aliyuncs", + "homepage": "http://www.aliyun.com" + } + ], + "description": "Aliyun OSS SDK for PHP", + "homepage": "http://www.aliyun.com/product/oss/", + "support": { + "issues": "https://github.com/aliyun/aliyun-oss-php-sdk/issues", + "source": "https://github.com/aliyun/aliyun-oss-php-sdk/tree/v2.7.2" + }, + "time": "2024-10-28T10:41:12+00:00" + }, + { + "name": "bacon/bacon-qr-code", + "version": "2.0.8", + "source": { + "type": "git", + "url": "https://github.com/Bacon/BaconQrCode.git", + "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22", + "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22", + "shasum": "" + }, + "require": { + "dasprid/enum": "^1.0.3", + "ext-iconv": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phly/keep-a-changelog": "^2.1", + "phpunit/phpunit": "^7 | ^8 | ^9", + "spatie/phpunit-snapshot-assertions": "^4.2.9", + "squizlabs/php_codesniffer": "^3.4" + }, + "suggest": { + "ext-imagick": "to generate QR code images" + }, + "type": "library", + "autoload": { + "psr-4": { + "BaconQrCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "BaconQrCode is a QR code generator for PHP.", + "homepage": "https://github.com/Bacon/BaconQrCode", + "support": { + "issues": "https://github.com/Bacon/BaconQrCode/issues", + "source": "https://github.com/Bacon/BaconQrCode/tree/2.0.8" + }, + "time": "2022-12-07T17:46:57+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, + { + "name": "dasprid/enum", + "version": "1.0.7", + "source": { + "type": "git", + "url": "https://github.com/DASPRiD/Enum.git", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "shasum": "" + }, + "require": { + "php": ">=7.1 <9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "DASPRiD\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "PHP 7.1 enum implementation", + "keywords": [ + "enum", + "map" + ], + "support": { + "issues": "https://github.com/DASPRiD/Enum/issues", + "source": "https://github.com/DASPRiD/Enum/tree/1.0.7" + }, + "time": "2025-09-16T12:23:56+00:00" + }, + { + "name": "easywechat-composer/easywechat-composer", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/mingyoung/easywechat-composer.git", + "reference": "3fc6a7ab6d3853c0f4e2922539b56cc37ef361cd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mingyoung/easywechat-composer/zipball/3fc6a7ab6d3853c0f4e2922539b56cc37ef361cd", + "reference": "3fc6a7ab6d3853c0f4e2922539b56cc37ef361cd", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=7.0" + }, + "require-dev": { + "composer/composer": "^1.0 || ^2.0", + "phpunit/phpunit": "^6.5 || ^7.0" + }, + "type": "composer-plugin", + "extra": { + "class": "EasyWeChatComposer\\Plugin" + }, + "autoload": { + "psr-4": { + "EasyWeChatComposer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "张铭阳", + "email": "mingyoungcheung@gmail.com" + } + ], + "description": "The composer plugin for EasyWeChat", + "support": { + "issues": "https://github.com/mingyoung/easywechat-composer/issues", + "source": "https://github.com/mingyoung/easywechat-composer/tree/1.4.1" + }, + "time": "2021-07-05T04:03:22+00:00" + }, + { + "name": "endroid/qr-code", + "version": "3.9.7", + "source": { + "type": "git", + "url": "https://github.com/endroid/qr-code.git", + "reference": "94563d7b3105288e6ac53a67ae720e3669fac1f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/endroid/qr-code/zipball/94563d7b3105288e6ac53a67ae720e3669fac1f6", + "reference": "94563d7b3105288e6ac53a67ae720e3669fac1f6", + "shasum": "" + }, + "require": { + "bacon/bacon-qr-code": "^2.0", + "khanamiryan/qrcode-detector-decoder": "^1.0.5", + "myclabs/php-enum": "^1.5", + "php": "^7.3||^8.0", + "symfony/options-resolver": "^3.4||^4.4||^5.0", + "symfony/property-access": "^3.4||^4.4||^5.0" + }, + "require-dev": { + "endroid/quality": "^1.5.2", + "setasign/fpdf": "^1.8" + }, + "suggest": { + "ext-gd": "Required for generating PNG images", + "roave/security-advisories": "Avoids installation of package versions with vulnerabilities", + "setasign/fpdf": "Required to use the FPDF writer.", + "symfony/security-checker": "Checks your composer.lock for vulnerabilities" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Endroid\\QrCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeroen van den Enden", + "email": "info@endroid.nl" + } + ], + "description": "Endroid QR Code", + "homepage": "https://github.com/endroid/qr-code", + "keywords": [ + "bundle", + "code", + "endroid", + "php", + "qr", + "qrcode" + ], + "support": { + "issues": "https://github.com/endroid/qr-code/issues", + "source": "https://github.com/endroid/qr-code/tree/3.9.7" + }, + "funding": [ + { + "url": "https://github.com/endroid", + "type": "github" + } + ], + "time": "2021-04-20T19:10:54+00:00" + }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.19.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf", + "shasum": "" + }, + "require": { + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "cerdic/css-tidy": "^1.7 || ^2.0", + "simpletest/simpletest": "dev-master" + }, + "suggest": { + "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.", + "ext-bcmath": "Used for unit conversion and imagecrash protection", + "ext-iconv": "Converts text to and from non-UTF-8 encodings", + "ext-tidy": "Used for pretty-printing HTML" + }, + "type": "library", + "autoload": { + "files": [ + "library/HTMLPurifier.composer.php" + ], + "psr-0": { + "HTMLPurifier": "library/" + }, + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "support": { + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0" + }, + "time": "2025-10-17T16:34:55+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "6.5.8", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/a52f0440530b54fa079ce76e8c5d196a42cad981", + "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.9", + "php": ">=5.5", + "symfony/polyfill-intl-idn": "^1.17" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", + "psr/log": "^1.1" + }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.5-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/6.5.8" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2022-06-20T22:16:07+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "1.5.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/67ab6e18aaa14d753cc148911d273f6e6cb6721e", + "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.5.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2023-05-21T12:31:43+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.9.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "e4490cabc77465aaee90b20cfc9a770f8c04be6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/e4490cabc77465aaee90b20cfc9a770f8c04be6b", + "reference": "e4490cabc77465aaee90b20cfc9a770f8c04be6b", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/1.9.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2023-04-17T16:00:37+00:00" + }, + { + "name": "hashids/hashids", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/vinkla/hashids.git", + "reference": "7a945a5192d4a5c8888364970feece9bc26179df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vinkla/hashids/zipball/7a945a5192d4a5c8888364970feece9bc26179df", + "reference": "7a945a5192d4a5c8888364970feece9bc26179df", + "shasum": "" + }, + "require": { + "php": "^5.6.4 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.3" + }, + "suggest": { + "ext-bcmatch": "Required to use BC Math arbitrary precision mathematics (*).", + "ext-gmp": "Required to use GNU multiple precision mathematics (*)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "psr-4": { + "Hashids\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ivan Akimov", + "email": "ivan@barreleye.com", + "homepage": "https://twitter.com/IvanAkimov" + }, + { + "name": "Vincent Klaiber", + "email": "hello@vinkla.com", + "homepage": "https://vinkla.com" + } + ], + "description": "Generate short, unique, non-sequential ids (like YouTube and Bitly) from numbers", + "homepage": "http://hashids.org/php", + "keywords": [ + "bitly", + "decode", + "encode", + "hash", + "hashid", + "hashids", + "ids", + "obfuscate", + "youtube" + ], + "support": { + "issues": "https://github.com/vinkla/hashids/issues", + "source": "https://github.com/vinkla/hashids/tree/2.0.4" + }, + "time": "2017-10-28T11:24:20+00:00" + }, + { + "name": "khanamiryan/qrcode-detector-decoder", + "version": "1.0.6", + "source": { + "type": "git", + "url": "https://github.com/khanamiryan/php-qrcode-detector-decoder.git", + "reference": "45326fb83a2a375065dbb3a134b5b8a5872da569" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/khanamiryan/php-qrcode-detector-decoder/zipball/45326fb83a2a375065dbb3a134b5b8a5872da569", + "reference": "45326fb83a2a375065dbb3a134b5b8a5872da569", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 | ^7.5 | ^8.0 | ^9.0", + "rector/rector": "^0.13.6", + "symplify/easy-coding-standard": "^11.0" + }, + "type": "library", + "autoload": { + "files": [ + "lib/Common/customFunctions.php" + ], + "psr-4": { + "Zxing\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT", + "Apache-2.0" + ], + "authors": [ + { + "name": "Ashot Khanamiryan", + "email": "a.khanamiryan@gmail.com", + "homepage": "https://github.com/khanamiryan", + "role": "Developer" + } + ], + "description": "QR code decoder / reader", + "homepage": "https://github.com/khanamiryan/php-qrcode-detector-decoder/", + "keywords": [ + "barcode", + "qr", + "zxing" + ], + "support": { + "issues": "https://github.com/khanamiryan/php-qrcode-detector-decoder/issues", + "source": "https://github.com/khanamiryan/php-qrcode-detector-decoder/tree/1.0.6" + }, + "time": "2022-06-29T09:25:13+00:00" + }, + { + "name": "lizhichao/word", + "version": "v2.1", + "source": { + "type": "git", + "url": "https://github.com/lizhichao/VicWord.git", + "reference": "f17172d45f505e7140da0bde2103defc13255326" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lizhichao/VicWord/zipball/f17172d45f505e7140da0bde2103defc13255326", + "reference": "f17172d45f505e7140da0bde2103defc13255326", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Lizhichao\\Word\\": "Lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "tanszhe", + "email": "1018595261@qq.com" + } + ], + "description": "This is a participle library", + "support": { + "issues": "https://github.com/lizhichao/VicWord/issues", + "source": "https://github.com/lizhichao/VicWord/tree/master" + }, + "time": "2020-07-30T07:33:06+00:00" + }, + { + "name": "maennchen/zipstream-php", + "version": "2.2.6", + "source": { + "type": "git", + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "30ad6f93cf3efe4192bc7a4c9cad11ff8f4f237f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/30ad6f93cf3efe4192bc7a4c9cad11ff8f4f237f", + "reference": "30ad6f93cf3efe4192bc7a4c9cad11ff8f4f237f", + "shasum": "" + }, + "require": { + "myclabs/php-enum": "^1.5", + "php": "^7.4 || ^8.0", + "psr/http-message": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "require-dev": { + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.9", + "guzzlehttp/guzzle": "^6.5.3 || ^7.2.0", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.4", + "phpunit/phpunit": "^8.5.8 || ^9.4.2", + "vimeo/psalm": "^4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZipStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paul Duncan", + "email": "pabs@pablotron.org" + }, + { + "name": "Jonatan Männchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "András Kolesár", + "email": "kolesar@kolesar.hu" + } + ], + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "keywords": [ + "stream", + "zip" + ], + "support": { + "issues": "https://github.com/maennchen/ZipStream-PHP/issues", + "source": "https://github.com/maennchen/ZipStream-PHP/tree/2.2.6" + }, + "funding": [ + { + "url": "https://github.com/maennchen", + "type": "github" + }, + { + "url": "https://opencollective.com/zipstream", + "type": "open_collective" + } + ], + "time": "2022-11-25T18:57:19+00:00" + }, + { + "name": "markbaker/complex", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPComplex.git", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Complex\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" + } + ], + "description": "PHP Class for working with complex numbers", + "homepage": "https://github.com/MarkBaker/PHPComplex", + "keywords": [ + "complex", + "mathematics" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPComplex/issues", + "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2" + }, + "time": "2022-12-06T16:21:08+00:00" + }, + { + "name": "markbaker/matrix", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPMatrix.git", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "sebastian/phpcpd": "^4.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matrix\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@demon-angel.eu" + } + ], + "description": "PHP Class for working with matrices", + "homepage": "https://github.com/MarkBaker/PHPMatrix", + "keywords": [ + "mathematics", + "matrix", + "vector" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPMatrix/issues", + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1" + }, + "time": "2022-12-02T22:17:43+00:00" + }, + { + "name": "monolog/monolog", + "version": "1.27.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "904713c5929655dc9b97288b69cfeedad610c9a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/904713c5929655dc9b97288b69cfeedad610c9a1", + "reference": "904713c5929655dc9b97288b69cfeedad610c9a1", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/log": "~1.0" + }, + "provide": { + "psr/log-implementation": "1.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "graylog2/gelf-php": "~1.0", + "php-amqplib/php-amqplib": "~2.4", + "php-console/php-console": "^3.1.3", + "phpstan/phpstan": "^0.12.59", + "phpunit/phpunit": "~4.5", + "ruflin/elastica": ">=0.90 <3.0", + "sentry/sentry": "^0.13", + "swiftmailer/swiftmailer": "^5.3|^6.0" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-mongo": "Allow sending log messages to a MongoDB server", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "php-console/php-console": "Allow sending log messages to Google Chrome", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server", + "sentry/sentry": "Allow sending log messages to a Sentry server" + }, + "type": "library", + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "http://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/1.27.1" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2022-06-09T08:53:42+00:00" + }, + { + "name": "myclabs/php-enum", + "version": "1.8.5", + "source": { + "type": "git", + "url": "https://github.com/myclabs/php-enum.git", + "reference": "e7be26966b7398204a234f8673fdad5ac6277802" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/php-enum/zipball/e7be26966b7398204a234f8673fdad5ac6277802", + "reference": "e7be26966b7398204a234f8673fdad5ac6277802", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "1.*", + "vimeo/psalm": "^4.6.2 || ^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "MyCLabs\\Enum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP Enum contributors", + "homepage": "https://github.com/myclabs/php-enum/graphs/contributors" + } + ], + "description": "PHP Enum implementation", + "homepage": "https://github.com/myclabs/php-enum", + "keywords": [ + "enum" + ], + "support": { + "issues": "https://github.com/myclabs/php-enum/issues", + "source": "https://github.com/myclabs/php-enum/tree/1.8.5" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum", + "type": "tidelift" + } + ], + "time": "2025-01-14T11:49:03+00:00" + }, + { + "name": "overtrue/socialite", + "version": "2.0.24", + "source": { + "type": "git", + "url": "https://github.com/overtrue/socialite.git", + "reference": "ee7e7b000ec7d64f2b8aba1f6a2eec5cdf3f8bec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/overtrue/socialite/zipball/ee7e7b000ec7d64f2b8aba1f6a2eec5cdf3f8bec", + "reference": "ee7e7b000ec7d64f2b8aba1f6a2eec5cdf3f8bec", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/guzzle": "^5.0|^6.0|^7.0", + "php": ">=5.6", + "symfony/http-foundation": "^2.7|^3.0|^4.0|^5.0" + }, + "require-dev": { + "mockery/mockery": "~1.2", + "phpunit/phpunit": "^6.0|^7.0|^8.0|^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Overtrue\\Socialite\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "overtrue", + "email": "anzhengchao@gmail.com" + } + ], + "description": "A collection of OAuth 2 packages that extracts from laravel/socialite.", + "keywords": [ + "login", + "oauth", + "qq", + "social", + "wechat", + "weibo" + ], + "support": { + "issues": "https://github.com/overtrue/socialite/issues", + "source": "https://github.com/overtrue/socialite/tree/2.0.24" + }, + "funding": [ + { + "url": "https://www.patreon.com/overtrue", + "type": "patreon" + } + ], + "time": "2021-05-13T16:04:48+00:00" + }, + { + "name": "overtrue/wechat", + "version": "4.9.0", + "source": { + "type": "git", + "url": "https://github.com/w7corp/easywechat.git", + "reference": "92791f5d957269c633b9aa175f842f6006f945b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/w7corp/easywechat/zipball/92791f5d957269c633b9aa175f842f6006f945b1", + "reference": "92791f5d957269c633b9aa175f842f6006f945b1", + "shasum": "" + }, + "require": { + "easywechat-composer/easywechat-composer": "^1.1", + "ext-fileinfo": "*", + "ext-openssl": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^6.2 || ^7.0", + "monolog/monolog": "^1.22 || ^2.0", + "overtrue/socialite": "~2.0", + "php": ">=7.2", + "pimple/pimple": "^3.0", + "psr/simple-cache": "^1.0", + "symfony/cache": "^3.3 || ^4.3 || ^5.0", + "symfony/event-dispatcher": "^4.3 || ^5.0", + "symfony/http-foundation": "^2.7 || ^3.0 || ^4.0 || ^5.0", + "symfony/psr-http-message-bridge": "^0.3 || ^1.0 || ^2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.15", + "mikey179/vfsstream": "^1.6", + "mockery/mockery": "^1.2.3", + "phpstan/phpstan": "^0.12.0", + "phpunit/phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/Kernel/Support/Helpers.php", + "src/Kernel/Helpers.php" + ], + "psr-4": { + "EasyWeChat\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "overtrue", + "email": "anzhengchao@gmail.com" + } + ], + "description": "微信SDK", + "keywords": [ + "easywechat", + "sdk", + "wechat", + "weixin", + "weixin-sdk" + ], + "support": { + "issues": "https://github.com/w7corp/easywechat/issues", + "source": "https://github.com/w7corp/easywechat/tree/4.9.0" + }, + "funding": [ + { + "url": "https://github.com/overtrue", + "type": "github" + } + ], + "abandoned": "w7corp/easywechat", + "time": "2023-04-28T03:30:34+00:00" + }, + { + "name": "phpoffice/phpspreadsheet", + "version": "1.30.1", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "fa8257a579ec623473eabfe49731de5967306c4c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fa8257a579ec623473eabfe49731de5967306c4c", + "reference": "fa8257a579ec623473eabfe49731de5967306c4c", + "shasum": "" + }, + "require": { + "composer/pcre": "^1||^2||^3", + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "ezyang/htmlpurifier": "^4.15", + "maennchen/zipstream-php": "^2.1 || ^3.0", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "php": ">=7.4.0 <8.5.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-main", + "dompdf/dompdf": "^1.0 || ^2.0 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.2", + "mitoteam/jpgraph": "^10.3", + "mpdf/mpdf": "^8.1.1", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^8.5 || ^9.0", + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "ext-intl": "PHP Internationalization Functions", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + } + ], + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", + "keywords": [ + "OpenXML", + "excel", + "gnumeric", + "ods", + "php", + "spreadsheet", + "xls", + "xlsx" + ], + "support": { + "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.1" + }, + "time": "2025-10-26T16:01:04+00:00" + }, + { + "name": "phrity/net-uri", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-net-uri.git", + "reference": "3f458e0c4d1ddc0e218d7a5b9420127c63925f43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-net-uri/zipball/3f458e0c4d1ddc0e218d7a5b9420127c63925f43", + "reference": "3f458e0c4d1ddc0e218d7a5b9420127c63925f43", + "shasum": "" + }, + "require": { + "php": "^7.4 | ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 | ^2.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.0", + "phpunit/phpunit": "^9.0 | ^10.0", + "squizlabs/php_codesniffer": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Phrity\\Net\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "description": "PSR-7 Uri and PSR-17 UriFactory implementation", + "homepage": "https://phrity.sirn.se/net-uri", + "keywords": [ + "psr-17", + "psr-7", + "uri", + "uri factory" + ], + "support": { + "issues": "https://github.com/sirn-se/phrity-net-uri/issues", + "source": "https://github.com/sirn-se/phrity-net-uri/tree/1.3.0" + }, + "time": "2023-08-21T10:33:06+00:00" + }, + { + "name": "phrity/util-errorhandler", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sirn-se/phrity-util-errorhandler.git", + "reference": "483228156e06673963902b1cc1e6bd9541ab4d5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirn-se/phrity-util-errorhandler/zipball/483228156e06673963902b1cc1e6bd9541ab4d5e", + "reference": "483228156e06673963902b1cc1e6bd9541ab4d5e", + "shasum": "" + }, + "require": { + "php": "^7.4 | ^8.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.0", + "phpunit/phpunit": "^9.0 | ^10.0 | ^11.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Phrity\\Util\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sören Jensen", + "email": "sirn@sirn.se", + "homepage": "https://phrity.sirn.se" + } + ], + "description": "Inline error handler; catch and resolve errors for code block.", + "homepage": "https://phrity.sirn.se/util-errorhandler", + "keywords": [ + "error", + "warning" + ], + "support": { + "issues": "https://github.com/sirn-se/phrity-util-errorhandler/issues", + "source": "https://github.com/sirn-se/phrity-util-errorhandler/tree/1.1.1" + }, + "time": "2024-09-12T06:49:16+00:00" + }, + { + "name": "pimple/pimple", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/silexphp/Pimple.git", + "reference": "a70f552d338f9266eec6606c1f0b324da5514c96" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/silexphp/Pimple/zipball/a70f552d338f9266eec6606c1f0b324da5514c96", + "reference": "a70f552d338f9266eec6606c1f0b324da5514c96", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1 || ^2.0" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4.x-dev" + } + }, + "autoload": { + "psr-0": { + "Pimple": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Pimple, a simple Dependency Injection Container", + "homepage": "https://pimple.symfony.com", + "keywords": [ + "container", + "dependency injection" + ], + "support": { + "source": "https://github.com/silexphp/Pimple/tree/v3.6.0" + }, + "time": "2025-11-12T12:31:38+00:00" + }, + { + "name": "psr/cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/master" + }, + "time": "2016-08-06T20:24:11+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "1.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/1.1" + }, + "time": "2023-04-04T09:50:52+00:00" + }, + { + "name": "psr/log", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" + }, + { + "name": "psr/simple-cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/master" + }, + "time": "2017-10-23T01:57:42+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "symfony/cache", + "version": "v4.3.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "8794ccf68ac341fc19311919d2287f7557bfccba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/8794ccf68ac341fc19311919d2287f7557bfccba", + "reference": "8794ccf68ac341fc19311919d2287f7557bfccba", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "psr/cache": "~1.0", + "psr/log": "~1.0", + "symfony/cache-contracts": "^1.1", + "symfony/service-contracts": "^1.1", + "symfony/var-exporter": "^4.2" + }, + "conflict": { + "doctrine/dbal": "<2.5", + "symfony/dependency-injection": "<3.4", + "symfony/var-dumper": "<3.4" + }, + "provide": { + "psr/cache-implementation": "1.0", + "psr/simple-cache-implementation": "1.0", + "symfony/cache-implementation": "1.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/cache": "~1.6", + "doctrine/dbal": "~2.5", + "predis/predis": "~1.1", + "psr/simple-cache": "^1.0", + "symfony/config": "~4.2", + "symfony/dependency-injection": "~3.4|~4.1", + "symfony/var-dumper": "^4.1.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Cache component with PSR-6, PSR-16, and tags", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v4.3.11" + }, + "time": "2020-01-27T09:15:09+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v1.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "a872a66e0bf7bac179c89bc96c7098bef1949f81" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/a872a66e0bf7bac179c89bc96c7098bef1949f81", + "reference": "a872a66e0bf7bac179c89bc96c7098bef1949f81", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "psr/cache": "^1.0|^2.0|^3.0" + }, + "suggest": { + "symfony/cache-implementation": "" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v1.10.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:41:36+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.5.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918", + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "2.5-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v5.4.45", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "72982eb416f61003e9bb6e91f8b3213600dcf9e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/72982eb416f61003e9bb6e91f8b3213600dcf9e9", + "reference": "72982eb416f61003e9bb6e91f8b3213600dcf9e9", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/event-dispatcher-contracts": "^2|^3", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "symfony/dependency-injection": "<4.4" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/http-foundation": "^4.4|^5.0|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/stopwatch": "^4.4|^5.0|^6.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.45" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v2.5.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "e0fe3d79b516eb75126ac6fa4cbf19b79b08c99f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/e0fe3d79b516eb75126ac6fa4cbf19b79b08c99f", + "reference": "e0fe3d79b516eb75126ac6fa4cbf19b79b08c99f", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/event-dispatcher": "^1" + }, + "suggest": { + "symfony/event-dispatcher-implementation": "" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "2.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v5.4.50", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "1a0706e8b8041046052ea2695eb8aeee04f97609" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/1a0706e8b8041046052ea2695eb8aeee04f97609", + "reference": "1a0706e8b8041046052ea2695eb8aeee04f97609", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "predis/predis": "^1.0|^2.0", + "symfony/cache": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", + "symfony/mime": "^4.4|^5.0|^6.0", + "symfony/rate-limiter": "^5.2|^6.0" + }, + "suggest": { + "symfony/mime": "To use the file extension guesser" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v5.4.50" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-03T12:58:48+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v5.4.45", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "74e5b6f0db3e8589e6cfd5efb317a1fc2bb52fb6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/74e5b6f0db3e8589e6cfd5efb317a1fc2bb52fb6", + "reference": "74e5b6f0db3e8589e6cfd5efb317a1fc2bb52fb6", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php73": "~1.0", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v5.4.45" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-10T14:38:51+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "symfony/property-access", + "version": "v5.4.45", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "111e7ed617509f1a9139686055d234aad6e388e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-access/zipball/111e7ed617509f1a9139686055d234aad6e388e0", + "reference": "111e7ed617509f1a9139686055d234aad6e388e0", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php80": "^1.16", + "symfony/property-info": "^5.2|^6.0" + }, + "require-dev": { + "symfony/cache": "^4.4|^5.0|^6.0" + }, + "suggest": { + "psr/cache-implementation": "To cache access methods." + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" + ], + "support": { + "source": "https://github.com/symfony/property-access/tree/v5.4.45" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:11:13+00:00" + }, + { + "name": "symfony/property-info", + "version": "v5.4.48", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-info.git", + "reference": "a0396295ad585f95fccd690bc6a281e5bd303902" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-info/zipball/a0396295ad585f95fccd690bc6a281e5bd303902", + "reference": "a0396295ad585f95fccd690bc6a281e5bd303902", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php80": "^1.16", + "symfony/string": "^5.1|^6.0" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/dependency-injection": "<4.4" + }, + "require-dev": { + "doctrine/annotations": "^1.10.4|^2", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "symfony/cache": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/serializer": "^4.4|^5.0|^6.0" + }, + "suggest": { + "phpdocumentor/reflection-docblock": "To use the PHPDoc", + "psr/cache-implementation": "To cache results", + "symfony/doctrine-bridge": "To use Doctrine metadata", + "symfony/serializer": "To use Serializer metadata" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts information about PHP class' properties using metadata of popular sources", + "homepage": "https://symfony.com", + "keywords": [ + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" + ], + "support": { + "source": "https://github.com/symfony/property-info/tree/v5.4.48" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-25T16:14:41+00:00" + }, + { + "name": "symfony/psr-http-message-bridge", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/psr-http-message-bridge.git", + "reference": "581ca6067eb62640de5ff08ee1ba6850a0ee472e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/581ca6067eb62640de5ff08ee1ba6850a0ee472e", + "reference": "581ca6067eb62640de5ff08ee1ba6850a0ee472e", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/http-message": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/http-foundation": "^5.4 || ^6.0" + }, + "require-dev": { + "nyholm/psr7": "^1.1", + "psr/log": "^1.1 || ^2 || ^3", + "symfony/browser-kit": "^5.4 || ^6.0", + "symfony/config": "^5.4 || ^6.0", + "symfony/event-dispatcher": "^5.4 || ^6.0", + "symfony/framework-bundle": "^5.4 || ^6.0", + "symfony/http-kernel": "^5.4 || ^6.0", + "symfony/phpunit-bridge": "^6.2" + }, + "suggest": { + "nyholm/psr7": "For a super lightweight PSR-7/17 implementation" + }, + "type": "symfony-bridge", + "extra": { + "branch-alias": { + "dev-main": "2.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bridge\\PsrHttpMessage\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "description": "PSR HTTP message bridge", + "homepage": "http://symfony.com", + "keywords": [ + "http", + "http-message", + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/symfony/psr-http-message-bridge/issues", + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v2.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-26T11:53:26+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v1.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "191afdcb5804db960d26d8566b7e9a2843cab3a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/191afdcb5804db960d26d8566b7e9a2843cab3a0", + "reference": "191afdcb5804db960d26d8566b7e9a2843cab3a0", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "suggest": { + "psr/container": "", + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v1.1.2" + }, + "time": "2019-05-28T07:50:59+00:00" + }, + { + "name": "symfony/string", + "version": "v5.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "136ca7d72f72b599f2631aca474a4f8e26719799" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/136ca7d72f72b599f2631aca474a4f8e26719799", + "reference": "136ca7d72f72b599f2631aca474a4f8e26719799", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "~1.15" + }, + "conflict": { + "symfony/translation-contracts": ">=3.0" + }, + "require-dev": { + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/http-client": "^4.4|^5.0|^6.0", + "symfony/translation-contracts": "^1.1|^2", + "symfony/var-exporter": "^4.4|^5.0|^6.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v5.4.47" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-10T20:33:58+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v4.4.43", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "4a7a3a3d55c471d396e6d28011368b7b83cb518b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/4a7a3a3d55c471d396e6d28011368b7b83cb518b", + "reference": "4a7a3a3d55c471d396e6d28011368b7b83cb518b", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "symfony/var-dumper": "^4.4.9|^5.0.9" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v4.4.43" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-27T11:44:32+00:00" + }, + { + "name": "textalk/websocket", + "version": "1.6.3", + "source": { + "type": "git", + "url": "https://github.com/Textalk/websocket-php.git", + "reference": "67de79745b1a357caf812bfc44e0abf481cee012" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Textalk/websocket-php/zipball/67de79745b1a357caf812bfc44e0abf481cee012", + "reference": "67de79745b1a357caf812bfc44e0abf481cee012", + "shasum": "" + }, + "require": { + "php": "^7.4 | ^8.0", + "phrity/net-uri": "^1.0", + "phrity/util-errorhandler": "^1.0", + "psr/http-message": "^1.0", + "psr/log": "^1.0 | ^2.0 | ^3.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.0", + "phpunit/phpunit": "^9.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "WebSocket\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Fredrik Liljegren" + }, + { + "name": "Sören Jensen" + } + ], + "description": "WebSocket client and server", + "support": { + "issues": "https://github.com/Textalk/websocket-php/issues", + "source": "https://github.com/Textalk/websocket-php/tree/1.6.3" + }, + "time": "2022-11-07T18:59:33+00:00" + }, + { + "name": "topthink/framework", + "version": "v5.1.12", + "source": { + "type": "git", + "url": "https://github.com/top-think/framework.git", + "reference": "f879603ee321af8fde56d8855445cf98bc81b042" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/framework/zipball/f879603ee321af8fde56d8855445cf98bc81b042", + "reference": "f879603ee321af8fde56d8855445cf98bc81b042", + "shasum": "" + }, + "require": { + "php": ">=5.6.0", + "topthink/think-installer": "~1.0" + }, + "require-dev": { + "johnkary/phpunit-speedtrap": "^1.0", + "mikey179/vfsstream": "~1.6", + "phpdocumentor/reflection-docblock": "^2.0", + "phploc/phploc": "2.*", + "phpunit/phpunit": "^5.0|^6.0", + "sebastian/phpcpd": "2.*", + "squizlabs/php_codesniffer": "2.*" + }, + "type": "think-framework", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + }, + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "the new thinkphp framework", + "homepage": "http://thinkphp.cn/", + "keywords": [ + "framework", + "orm", + "thinkphp" + ], + "support": { + "issues": "https://github.com/top-think/framework/issues", + "source": "https://github.com/top-think/framework/tree/5.1" + }, + "time": "2018-04-26T03:46:41+00:00" + }, + { + "name": "topthink/think-captcha", + "version": "v2.0.2", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-captcha.git", + "reference": "54c8a51552f99ff9ea89ea9c272383a8f738ceee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-captcha/zipball/54c8a51552f99ff9ea89ea9c272383a8f738ceee", + "reference": "54c8a51552f99ff9ea89ea9c272383a8f738ceee", + "shasum": "" + }, + "require": { + "topthink/framework": "5.1.*" + }, + "type": "library", + "autoload": { + "files": [ + "src/helper.php" + ], + "psr-4": { + "think\\captcha\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "captcha package for thinkphp5", + "support": { + "issues": "https://github.com/top-think/think-captcha/issues", + "source": "https://github.com/top-think/think-captcha/tree/2.0" + }, + "time": "2017-12-31T16:37:49+00:00" + }, + { + "name": "topthink/think-helper", + "version": "v3.1.11", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-helper.git", + "reference": "1d6ada9b9f3130046bf6922fe1bd159c8d88a33c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-helper/zipball/1d6ada9b9f3130046bf6922fe1bd159c8d88a33c", + "reference": "1d6ada9b9f3130046bf6922fe1bd159c8d88a33c", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/helper.php" + ], + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "The ThinkPHP6 Helper Package", + "support": { + "issues": "https://github.com/top-think/think-helper/issues", + "source": "https://github.com/top-think/think-helper/tree/v3.1.11" + }, + "time": "2025-04-07T06:55:59+00:00" + }, + { + "name": "topthink/think-image", + "version": "v1.0.8", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-image.git", + "reference": "d1d748cbb2fe2f29fca6138cf96cb8b5113892f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-image/zipball/d1d748cbb2fe2f29fca6138cf96cb8b5113892f1", + "reference": "d1d748cbb2fe2f29fca6138cf96cb8b5113892f1", + "shasum": "" + }, + "require": { + "ext-gd": "*" + }, + "require-dev": { + "phpunit/phpunit": "4.8.*", + "topthink/framework": "^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "The ThinkPHP5 Image Package", + "support": { + "issues": "https://github.com/top-think/think-image/issues", + "source": "https://github.com/top-think/think-image/tree/v1.0.8" + }, + "time": "2024-08-07T10:06:35+00:00" + }, + { + "name": "topthink/think-installer", + "version": "v1.0.14", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-installer.git", + "reference": "eae1740ac264a55c06134b6685dfb9f837d004d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-installer/zipball/eae1740ac264a55c06134b6685dfb9f837d004d1", + "reference": "eae1740ac264a55c06134b6685dfb9f837d004d1", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0||^2.0" + }, + "require-dev": { + "composer/composer": "^1.0||^2.0" + }, + "type": "composer-plugin", + "extra": { + "class": "think\\composer\\Plugin" + }, + "autoload": { + "psr-4": { + "think\\composer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "support": { + "issues": "https://github.com/top-think/think-installer/issues", + "source": "https://github.com/top-think/think-installer/tree/v1.0.14" + }, + "time": "2021-03-25T08:34:02+00:00" + }, + { + "name": "topthink/think-queue", + "version": "v2.0.3", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-queue.git", + "reference": "465320c9cb7811df22d4ff8f29f58ead7d104348" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-queue/zipball/465320c9cb7811df22d4ff8f29f58ead7d104348", + "reference": "465320c9cb7811df22d4ff8f29f58ead7d104348", + "shasum": "" + }, + "require": { + "topthink/think-helper": ">=1.0.4", + "topthink/think-installer": ">=1.0.10" + }, + "type": "think-extend", + "extra": { + "think-config": { + "queue": "src/config.php" + } + }, + "autoload": { + "files": [ + "src/common.php" + ], + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "The ThinkPHP5 Queue Package", + "support": { + "issues": "https://github.com/top-think/think-queue/issues", + "source": "https://github.com/top-think/think-queue/tree/master" + }, + "time": "2018-05-04T05:29:53+00:00" + }, + { + "name": "topthink/think-worker", + "version": "v2.0.3", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-worker.git", + "reference": "7b7a64b2911cc11298b11677508f1a1936a2fb70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-worker/zipball/7b7a64b2911cc11298b11677508f1a1936a2fb70", + "reference": "7b7a64b2911cc11298b11677508f1a1936a2fb70", + "shasum": "" + }, + "require": { + "topthink/framework": "5.1.*", + "workerman/workerman": "^3.3.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/config.php" + ], + "psr-4": { + "think\\worker\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "description": "workerman extend for thinkphp5", + "support": { + "issues": "https://github.com/top-think/think-worker/issues", + "source": "https://github.com/top-think/think-worker/tree/2.0" + }, + "time": "2018-06-28T06:04:53+00:00" + }, + { + "name": "workerman/gateway-worker", + "version": "v3.0.22", + "source": { + "type": "git", + "url": "https://github.com/walkor/GatewayWorker.git", + "reference": "a615036c482d11f68b693998575e804752ef9068" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/walkor/GatewayWorker/zipball/a615036c482d11f68b693998575e804752ef9068", + "reference": "a615036c482d11f68b693998575e804752ef9068", + "shasum": "" + }, + "require": { + "workerman/workerman": ">=3.5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "GatewayWorker\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "homepage": "http://www.workerman.net", + "keywords": [ + "communication", + "distributed" + ], + "support": { + "issues": "https://github.com/walkor/GatewayWorker/issues", + "source": "https://github.com/walkor/GatewayWorker/tree/v3.0.22" + }, + "funding": [ + { + "url": "https://opencollective.com/walkor", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/walkor", + "type": "patreon" + } + ], + "time": "2021-12-23T13:13:09+00:00" + }, + { + "name": "workerman/workerman", + "version": "v3.5.35", + "source": { + "type": "git", + "url": "https://github.com/walkor/workerman.git", + "reference": "3cc0adae51ba36db38b11e7996c64250d356dbe7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/walkor/workerman/zipball/3cc0adae51ba36db38b11e7996c64250d356dbe7", + "reference": "3cc0adae51ba36db38b11e7996c64250d356dbe7", + "shasum": "" + }, + "require": { + "php": "^5.3||^7.0" + }, + "suggest": { + "ext-event": "For better performance. " + }, + "type": "library", + "autoload": { + "psr-4": { + "Workerman\\": "./" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "walkor", + "email": "walkor@workerman.net", + "homepage": "http://www.workerman.net", + "role": "Developer" + } + ], + "description": "An asynchronous event driven PHP framework for easily building fast, scalable network applications.", + "homepage": "http://www.workerman.net", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "email": "walkor@workerman.net", + "forum": "http://wenda.workerman.net/", + "issues": "https://github.com/walkor/workerman/issues", + "source": "https://github.com/walkor/workerman", + "wiki": "http://doc.workerman.net/" + }, + "funding": [ + { + "url": "https://opencollective.com/workerman", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/walkor", + "type": "patreon" + } + ], + "time": "2023-09-13T14:30:13+00:00" + } + ], + "packages-dev": [ + { + "name": "symfony/var-dumper", + "version": "v3.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "0719f6cf4633a38b2c1585140998579ce23b4b7d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0719f6cf4633a38b2c1585140998579ce23b4b7d", + "reference": "0719f6cf4633a38b2c1585140998579ce23b4b7d", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0" + }, + "require-dev": { + "ext-iconv": "*", + "twig/twig": "~1.34|~2.4" + }, + "suggest": { + "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", + "ext-intl": "To show region name in time zone dump", + "ext-symfony_debug": "" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony mechanism for exploring and dumping PHP variables", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v3.4.47" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T10:57:07+00:00" + }, + { + "name": "topthink/think-migration", + "version": "v2.0.3", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-migration.git", + "reference": "70c89850ca29c2eab988c7c3475d1d5331901bb8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-migration/zipball/70c89850ca29c2eab988c7c3475d1d5331901bb8", + "reference": "70c89850ca29c2eab988c7c3475d1d5331901bb8", + "shasum": "" + }, + "require": { + "topthink/framework": "5.1.*" + }, + "type": "library", + "autoload": { + "files": [ + "src/config.php" + ], + "psr-4": { + "Phinx\\": "phinx/src/Phinx", + "think\\migration\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "support": { + "issues": "https://github.com/top-think/think-migration/issues", + "source": "https://github.com/top-think/think-migration/tree/2.0" + }, + "time": "2017-12-31T16:32:22+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": ">=5.6.0" + }, + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/config/app.php b/config/app.php new file mode 100644 index 0000000..78410a0 --- /dev/null +++ b/config/app.php @@ -0,0 +1,146 @@ + +// +---------------------------------------------------------------------- + +// +---------------------------------------------------------------------- +// | 应用设置 +// +---------------------------------------------------------------------- +use think\facade\Env; +return [ + // 应用名称 + 'app_name' => '', + // 应用地址 + 'app_host' => '', + // 应用调试模式 + 'app_debug' => Env::get('app.debug', false), + // 应用Trace + 'app_trace' => Env::get('app.trace', false), + // 是否支持多模块 + 'app_multi_module' => true, + // 入口自动绑定模块 + 'auto_bind_module' => false, + // 注册的根命名空间 + 'root_namespace' => [], + // 默认输出类型 + 'default_return_type' => 'html', + // 默认AJAX 数据返回格式,可选json xml ... + 'default_ajax_return' => 'json', + // 默认JSONP格式返回的处理方法 + 'default_jsonp_handler' => 'jsonpReturn', + // 默认JSONP处理方法 + 'var_jsonp_handler' => 'callback', + // 默认时区 + 'default_timezone' => 'Asia/Shanghai', + // 是否开启多语言 + 'lang_switch_on' => false, + // 默认全局过滤方法 用逗号分隔多个 + 'default_filter' => '', + // 默认语言 + 'default_lang' => 'zh-cn', + // 应用类库后缀 + 'class_suffix' => false, + // 控制器类后缀 + 'controller_suffix' => false, + + // +---------------------------------------------------------------------- + // | 模块设置 + // +---------------------------------------------------------------------- + + // 默认模块名 + 'default_module' => 'index', + // 禁止访问模块 + 'deny_module_list' => [], + // 默认控制器名 + 'default_controller' => 'Index', + // 默认操作名 + 'default_action' => 'index', + // 默认验证器 + 'default_validate' => '', + // 默认的空模块名 + 'empty_module' => '', + // 默认的空控制器名 + 'empty_controller' => 'Error', + // 操作方法前缀 + 'use_action_prefix' => false, + // 操作方法后缀 + 'action_suffix' => '', + // 自动搜索控制器 + 'controller_auto_search' => false, + + // +---------------------------------------------------------------------- + // | URL设置 + // +---------------------------------------------------------------------- + + // PATHINFO变量名 用于兼容模式 + 'var_pathinfo' => 's', + // 兼容PATH_INFO获取 + 'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'], + // pathinfo分隔符 + 'pathinfo_depr' => '/', + // HTTPS代理标识 + 'https_agent_name' => '', + // IP代理获取标识 + 'http_agent_ip' => 'X-REAL-IP', + // URL伪静态后缀 + 'url_html_suffix' => 'html', + // URL普通方式参数 用于自动生成 + 'url_common_param' => false, + // URL参数方式 0 按名称成对解析 1 按顺序解析 + 'url_param_type' => 0, + // 是否开启路由延迟解析 + 'url_lazy_route' => false, + // 是否强制使用路由 + 'url_route_must' => false, + // 合并路由规则 + 'route_rule_merge' => false, + // 路由是否完全匹配 + 'route_complete_match' => true, + // 使用注解路由 + 'route_annotation' => false, + // 域名根,如thinkphp.cn + 'url_domain_root' => '', + // 是否自动转换URL中的控制器和操作名 + 'url_convert' => true, + // 默认的访问控制器层 + 'url_controller_layer' => 'controller', + // 表单请求类型伪装变量 + 'var_method' => '_method', + // 表单ajax伪装变量 + 'var_ajax' => '_ajax', + // 表单pjax伪装变量 + 'var_pjax' => '_pjax', + // 是否开启请求缓存 true自动缓存 支持设置请求缓存规则 + 'request_cache' => false, + // 请求缓存有效期 + 'request_cache_expire' => null, + // 全局请求缓存排除规则 + 'request_cache_except' => [], + // 是否开启路由缓存 + 'route_check_cache' => false, + // 路由缓存的Key自定义设置(闭包),默认为当前URL和请求类型的md5 + 'route_check_cache_key' => '', + // 路由缓存类型及参数 + 'route_cache_option' => [], + + // 默认跳转页面对应的模板文件 + 'dispatch_success_tmpl' => Env::get('think_path') . 'tpl/dispatch_jump.tpl', + 'dispatch_error_tmpl' => Env::get('think_path') . 'tpl/dispatch_jump.tpl', + + // 异常页面的模板文件 + 'exception_tmpl' => Env::get('think_path') . 'tpl/think_exception.tpl', + + // 错误显示信息,非调试模式有效 + 'error_message' => '页面错误!请稍后再试~', + // 显示错误信息 + 'show_error_msg' => false, + // 异常处理handle类 留空使用 \think\exception\Handle + 'exception_handle' => '', + +]; diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 0000000..985dbb1 --- /dev/null +++ b/config/cache.php @@ -0,0 +1,25 @@ + +// +---------------------------------------------------------------------- + +// +---------------------------------------------------------------------- +// | 缓存设置 +// +---------------------------------------------------------------------- + +return [ + // 驱动方式 + 'type' => 'File', + // 缓存保存目录 + 'path' => '', + // 缓存前缀 + 'prefix' => '', + // 缓存有效期 0表示永久缓存 + 'expire' => 0, +]; diff --git a/config/config.php b/config/config.php new file mode 100644 index 0000000..442e4ae --- /dev/null +++ b/config/config.php @@ -0,0 +1,5 @@ + 'http://192.168.3.75:8081' +]; diff --git a/config/console.php b/config/console.php new file mode 100644 index 0000000..a7fabca --- /dev/null +++ b/config/console.php @@ -0,0 +1,20 @@ + +// +---------------------------------------------------------------------- + +// +---------------------------------------------------------------------- +// | 控制台配置 +// +---------------------------------------------------------------------- +return [ + 'name' => 'Think Console', + 'version' => '0.1', + 'user' => null, + 'auto_path' => env('app_path') . 'command' . DIRECTORY_SEPARATOR, +]; diff --git a/config/cookie.php b/config/cookie.php new file mode 100644 index 0000000..ce3a2f5 --- /dev/null +++ b/config/cookie.php @@ -0,0 +1,32 @@ + +// +---------------------------------------------------------------------- + +// +---------------------------------------------------------------------- +// | Cookie设置 +// +---------------------------------------------------------------------- +return [ + // cookie 名称前缀 + 'prefix' => '', + // cookie 保存时间 + 'expire' => 0, + // cookie 保存路径 + 'path' => '/', + // cookie 有效域名 + 'domain' => '', + // cookie 启用安全传输 + 'secure' => false, + // httponly设置 + 'httponly' => '', + // 是否使用 setcookie + 'setcookie' => true, + // 跨站需要 + 'SameSite' => 'None', +]; diff --git a/config/cookies.php b/config/cookies.php new file mode 100644 index 0000000..9705b35 --- /dev/null +++ b/config/cookies.php @@ -0,0 +1,13 @@ + [ + 'cna=bjINIOOLP2QBASQOA6MRoWqz', + 'xlly_s=1', + '_samesite_flag_=true', + 't=9aa5a7ca9fc23837b5f62366fb0e3c57', + '_tb_token_=e834030feb9b3', + 'mtop_partitioned_detect=1', + 'tfstk=g0cKNh9DFFQLM2KvYXJgruGcYcYMId0e-DufZuqhFcntlqS3FXVn2Tnnz02WYWc-XD0jKJc-z_USP08UqeJi82PzNnxDoI0E8l7Ra4tgNUg_ZzQ98mhK82PP7NjWnAgF2BFTQWNSV5w_zrP5F76BWla4f6a5Ozs6Wlz7Nyw7FR6_orW7PkiS5FUzfuN7d0NswMOzfMZ5wxUi5afPUl15NfULCd0Qfs4K6yeac2GdN_6zJJEjRlK42WH4eDeKsBI4JAgxSynyg6PQeYG31Xt6N5gqIDatVnB8fxhEw-cpDtrITuoTnvKA9mnUP4USvI7YfX3-6JkGos3THfG3TAKAioMZPbes8nB4SYgmtYnHb_ZnHViTnfjGiSMqXbebGgJKijQhNiqYr9T9WTWzdPRiFilYSSQ0zPEDJ3WPURxaWoY9cTWzdPzTmFVNUTyMb' + ], +]; \ No newline at end of file diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..3666761 --- /dev/null +++ b/config/database.php @@ -0,0 +1,64 @@ + +// +---------------------------------------------------------------------- + +use think\facade\Env; +return [ + // 数据库类型 + 'type' => Env::get('database.type', 'mysql'), + // 服务器地址 + 'hostname' => Env::get('database.hostname', '127.0.0.1'), + // 数据库名 + 'database' => Env::get('database.database', 'database'), + // 用户名 + 'username' => Env::get('database.username', 'root'), + // 密码 + 'password' => Env::get('database.password', 'root'), + // 端口 + 'hostport' => Env::get('database.hostport', '3306'), + // 连接dsn + 'dsn' => '', + // 数据库连接参数 + 'params' => [], + // 数据库编码默认采用utf8 + 'charset' => env('database.charset', 'utf8mb4'), + // 数据库表前缀 + 'prefix' => Env::get('database.prefix', ''), + // 数据库调试模式 + 'debug' => env('database.debug', true), + // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) + 'deploy' => 0, + // 数据库读写是否分离 主从式有效 + 'rw_separate' => false, + // 读写分离后 主服务器数量 + 'master_num' => 1, + // 指定从服务器序号 + 'slave_no' => '', + // 自动读取主库数据 + 'read_master' => false, + // 是否严格检查字段是否存在 + 'fields_strict' => true, + // 数据集返回类型 + 'resultset_type' => 'array', + // 自动写入时间戳字段 + 'auto_timestamp' => true, + // 时间字段取出后的默认时间格式 + 'datetime_format' => 'Y-m-d H:i:s', + // 是否需要进行SQL性能分析 + 'sql_explain' => false, + // Builder类 + 'builder' => '', + // Query类 + 'query' => '\\think\\db\\Query', + // 是否需要断线重连 + 'break_reconnect' => true, + // 断线标识字符串 + 'break_match_str' => [], +]; diff --git a/config/gateway_worker.php b/config/gateway_worker.php new file mode 100644 index 0000000..e0c6731 --- /dev/null +++ b/config/gateway_worker.php @@ -0,0 +1,46 @@ + +// +---------------------------------------------------------------------- +// +---------------------------------------------------------------------- +// | Workerman设置 仅对 php think worker:gateway 指令有效 +// +---------------------------------------------------------------------- +return [ + // 扩展自身需要的配置 + 'protocol' => 'websocket', // 协议 支持 tcp udp unix http websocket text + 'host' => '0.0.0.0', // 监听地址 + 'port' => 2348, // 监听端口 + 'socket' => '', // 完整监听地址 + 'context' => [], // socket 上下文选项 + 'register_deploy' => true, // 是否需要部署register + 'businessWorker_deploy' => true, // 是否需要部署businessWorker + 'gateway_deploy' => true, // 是否需要部署gateway + + // Register配置 + 'registerAddress' => '127.0.0.1:1236', + + // Gateway配置 + 'name' => 'thinkphp', + 'count' => 1, + 'lanIp' => '127.0.0.1', + 'startPort' => 2000, + 'daemonize' => false, + 'pingInterval' => 30, + 'pingNotResponseLimit' => 0, + 'pingData' => '{"type":"ping"}', + + // BusinsessWorker配置 + 'businessWorker' => [ + 'name' => 'BusinessWorker', + 'count' => 1, + 'eventHandler' => '\app\common\socket\Events', + ], + + 'qdKey' => 'N2WhAkJ1pgDBM$Bq', +]; diff --git a/config/log.php b/config/log.php new file mode 100644 index 0000000..b3d87b4 --- /dev/null +++ b/config/log.php @@ -0,0 +1,30 @@ + +// +---------------------------------------------------------------------- + +// +---------------------------------------------------------------------- +// | 日志设置 +// +---------------------------------------------------------------------- +return [ + // 日志记录方式,内置 file socket 支持扩展 + 'type' => 'File', + // 日志保存目录 + 'path' => '', + // 日志记录级别 + 'level' => [], + // 单文件日志写入 + 'single' => false, + // 独立日志级别 + 'apart_level' => [], + // 最大日志文件数量 + 'max_files' => 0, + // 是否关闭日志写入 + 'close' => false, +]; diff --git a/config/middleware.php b/config/middleware.php new file mode 100644 index 0000000..b221b8a --- /dev/null +++ b/config/middleware.php @@ -0,0 +1,34 @@ + +// +---------------------------------------------------------------------- + +// +---------------------------------------------------------------------- +// | 中间件配置 +// +---------------------------------------------------------------------- +return [ + // 默认中间件命名空间 + 'default_namespace' => 'app\\common\\middleware\\', + + // 优先级设置,此数组中的中间件会按照数组中的顺序优先执行 + 'priority' => [ + 'app\\common\\middleware\\AllowCrossDomain' + ], + + // 全局中间件 + 'alias' => [ + 'cors' => 'app\\common\\middleware\\AllowCrossDomain', + 'jwt' => 'app\\http\\middleware\\Jwt' + ], + + // 应用中间件 + 'app' => [ + 'cors' + ], +]; diff --git a/config/queue.php b/config/queue.php new file mode 100644 index 0000000..777f844 --- /dev/null +++ b/config/queue.php @@ -0,0 +1,33 @@ + 'redis', + 'connections' => [ + 'sync' => [ + 'type' => 'sync', + ], + 'database' => [ + 'type' => 'database', + 'queue' => 'default', + 'table' => 'jobs', + ], + 'redis' => [ + 'type' => 'redis', + 'queue' => 'default', + 'host' => '127.0.0.1', + 'port' => 6379, + 'password' => '', + 'select' => 0, + 'timeout' => 0, + 'persistent' => false, + ], + ], + 'failed' => [ + 'type' => 'redis', + 'table' => 'failed_jobs', + ], + 'failed_delay' => 30, // 失败重试延迟时间(秒) +]; \ No newline at end of file diff --git a/config/robot.php b/config/robot.php new file mode 100644 index 0000000..ffacd6b --- /dev/null +++ b/config/robot.php @@ -0,0 +1,7 @@ + 'AKLTNzNiMzU2ZWMwMDYwNDNiZWJkMjFjOTFhYzFjY2JlZGU', + 'secretKey' => 'TXpjeVlqSTJNRFpoTUdZd05EY3lPRGsxWTJFMk0yRmpaalkyTWpFd05tUQ==', + 'model' => 'ep-20240522132057-rp6t4', +]; \ No newline at end of file diff --git a/config/session.php b/config/session.php new file mode 100644 index 0000000..1d7b6c6 --- /dev/null +++ b/config/session.php @@ -0,0 +1,26 @@ + +// +---------------------------------------------------------------------- + +// +---------------------------------------------------------------------- +// | 会话设置 +// +---------------------------------------------------------------------- + +return [ + 'id' => '', + // SESSION_ID的提交变量,解决flash上传跨域 + 'var_session_id' => '', + // SESSION 前缀 + 'prefix' => 'think', + // 驱动方式 支持redis memcache memcached + 'type' => '', + // 是否自动开启 SESSION + 'auto_start' => true, +]; diff --git a/config/task_scheduler.php b/config/task_scheduler.php new file mode 100644 index 0000000..7fa7ea3 --- /dev/null +++ b/config/task_scheduler.php @@ -0,0 +1,301 @@ + [ + // 'command' => '命令名称', // 必填:执行的 ThinkPHP 命令(见 application/command.php) + // 'schedule' => 'cron表达式', // 必填:cron 表达式,如 '*/5 * * * *' 表示每5分钟 + // 'options' => ['--option=value'], // 可选:命令参数(原来 crontab 里的 --xxx=yyy) + // 'enabled' => true, // 可选:是否启用,默认 true + // 'max_concurrent'=> 1, // 可选:单任务最大并发数(目前由调度器统一控制,可预留) + // 'timeout' => 3600, // 可选:超时时间(秒),默认 3600 + // 'log_file' => 'custom.log', // 可选:日志文件名,默认使用任务标识 + // ] + + // =========================== + // 高频任务(每分钟或更频繁) + // =========================== + + // 同步微信好友列表(未删除好友),用于保持系统中好友数据实时更新 + 'wechat_friends_active' => [ + 'command' => 'wechatFriends:list', + 'schedule' => '*/1 * * * *', // 每1分钟 + 'options' => ['--isDel=0'], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'crontab_wechatFriends_active.log', + ], + + // 拉取“添加好友任务”列表,驱动自动加好友的任务队列 + 'friend_task' => [ + 'command' => 'friendTask:list', + 'schedule' => '*/1 * * * *', // 每1分钟 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'crontab_friendTask.log', + ], + + // 同步微信好友私聊消息列表,写入消息表,供客服工作台使用 + 'message_friends' => [ + 'command' => 'message:friendsList', + 'schedule' => '*/1 * * * *', // 每1分钟 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'crontab_messageFriends.log', + ], + + // 同步微信群聊消息列表,写入消息表,供群聊记录与风控分析 + 'message_chatroom' => [ + 'command' => 'message:chatroomList', + 'schedule' => '*/1 * * * *', // 每1分钟 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'crontab_messageChatroom.log', + ], + + // 客服端消息提醒任务,负责给在线客服推送新消息通知 + 'kf_notice' => [ + 'command' => 'kf:notice', + 'schedule' => '*/1 * * * *', // 每1分钟 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'kf_notice.log', + ], + + // =========================== + // 中频任务(每 2-5 分钟) + // =========================== + + // 同步微信设备列表(未删除设备),用于设备管理与监控 + 'device_active' => [ + 'command' => 'device:list', + 'schedule' => '*/5 * * * *', // 每5分钟 + 'options' => ['--isDel=0'], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'crontab_device_active.log', + ], + + // 同步微信群聊列表(未删除群),用于群管理与后续任务分配 + 'wechat_chatroom_active' => [ + 'command' => 'wechatChatroom:list', + 'schedule' => '*/5 * * * *', // 每5分钟 + 'options' => ['--isDel=0'], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'crontab_wechatChatroom_active.log', + ], + + // 同步微信群成员列表(群好友),维持群成员明细数据 + 'group_friends' => [ + 'command' => 'groupFriends:list', + 'schedule' => '*/5 * * * *', // 每5分钟 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'crontab_groupFriends.log', + ], + + // 同步“微信客服列表”,获取绑定到公司的微信号,用于工作台与分配规则 + 'wechat_list' => [ + 'command' => 'wechatList:list', + 'schedule' => '*/5 * * * *', // 每5分钟 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'crontab_wechatList.log', + ], + + // 同步公司账号列表(企业/租户账号),供后台管理与统计 + 'account_list' => [ + 'command' => 'account:list', + 'schedule' => '*/5 * * * *', // 每5分钟 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'crontab_account.log', + ], + + // 内容采集任务,将外部或设备内容同步到系统内容库 + 'content_collect' => [ + 'command' => 'content:collect', + 'schedule' => '*/5 * * * *', // 每5分钟 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'crontab_contentCollect.log', + ], + + // 工作台:自动点赞好友/客户朋友圈,提高账号活跃度 + 'workbench_auto_like' => [ + 'command' => 'workbench:autoLike', + 'schedule' => '*/6 * * * *', // 每6分钟 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'crontab_workbench_autoLike.log', + ], + + // 工作台:自动建群任务,按规则批量创建微信群 + 'workbench_group_create' => [ + 'command' => 'workbench:groupCreate', + 'schedule' => '*/5 * * * *', // 每5分钟 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'workbench_groupCreate.log', + ], + + // 工作台:自动导入通讯录到系统,生成加粉/建群等任务 + 'workbench_import_contact' => [ + 'command' => 'workbench:import-contact', + 'schedule' => '*/5 * * * *', // 每5分钟 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'import_contact.log', + ], + + // =========================== + // 低频任务(每 2 分钟) + // =========================== + + // 清洗并同步微信原始数据到存客宝业务表(数据治理任务) + 'sync_wechat_data' => [ + 'command' => 'sync:wechatData', + 'schedule' => '*/2 * * * *', // 每2分钟 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'sync_wechat_data.log', + ], + + // 工作台:流量分发任务,把流量池中的线索按规则分配给微信号或员工 + 'workbench_traffic_distribute' => [ + 'command' => 'workbench:trafficDistribute', + 'schedule' => '*/2 * * * *', // 每2分钟 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'traffic_distribute.log', + ], + + // 工作台:朋友圈同步任务,拉取并落库朋友圈内容 + 'workbench_moments' => [ + 'command' => 'workbench:moments', + 'schedule' => '*/2 * * * *', // 每2分钟 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'workbench_moments.log', + ], + + // 预防性切换好友任务,监控频繁/风控风险,自动切换加人对象,保护微信号 + 'switch_friends' => [ + 'command' => 'switch:friends', + 'schedule' => '*/2 * * * *', // 每2分钟 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'switch_friends.log', + ], + + // =========================== + // 低频任务(每 30 分钟) + // =========================== + + // 拉取设备通话记录(语音/电话),用于质检、统计或标签打分 + 'call_recording' => [ + 'command' => 'call-recording:list', + 'schedule' => '*/30 * * * *', // 每30分钟 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'call_recording.log', + ], + + // =========================== + // 每日 / 每几天任务 + // =========================== + + // 每日 1:00 同步“已删除设备”列表,补齐历史状态 + 'device_deleted' => [ + 'command' => 'device:list', + 'schedule' => '0 1 * * *', // 每天1点 + 'options' => ['--isDel=1'], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'crontab_device_deleted.log', + ], + + // 每日 1:10 同步“已停用设备”列表,更新停用状态 + 'device_stopped' => [ + 'command' => 'device:list', + 'schedule' => '10 1 * * *', // 每天1:10 + 'options' => ['--isDel=2'], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'crontab_device_stopped.log', + ], + + // 每日 1:30 同步“已删除微信好友”,用于历史恢复与报表 + 'wechat_friends_deleted' => [ + 'command' => 'wechatFriends:list', + 'schedule' => '30 1 * * *', // 每天1:30 + 'options' => ['--isDel=1'], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'crontab_wechatFriends_deleted.log', + ], + + // 每日 1:30 同步“已删除微信群聊”,用于统计与留痕 + 'wechat_chatroom_deleted' => [ + 'command' => 'wechatChatroom:list', + 'schedule' => '30 1 * * *', // 每天1:30 + 'options' => ['--isDel=1'], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'crontab_wechatChatroom_deleted.log', + ], + + // 每日 2:00 统一计算所有微信账号健康分(基础分 + 动态分) + 'wechat_calculate_score' => [ + 'command' => 'wechat:calculate-score', + 'schedule' => '0 2 * * *', // 每天2点 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'calculate_score.log', + ], + + // 每 3 天执行的全量任务 + + // 每 3 天 3:00 全量同步所有在线好友,做一次大规模校准 + 'sync_all_friends' => [ + 'command' => 'sync:allFriends', + 'schedule' => '0 3 */3 * *', // 每3天的3点 + 'options' => [], + 'enabled' => true, + 'max_concurrent' => 1, + 'log_file' => 'all_friends.log', + ], + + // 已禁用的任务(注释掉的任务) + // 'workbench_group_push' => [ + // 'command' => 'workbench:groupPush', + // 'schedule' => '*/2 * * * *', + // 'options' => [], + // 'enabled' => false, + // 'log_file' => 'workbench_groupPush.log', + // ], +]; + diff --git a/config/template.php b/config/template.php new file mode 100644 index 0000000..2111deb --- /dev/null +++ b/config/template.php @@ -0,0 +1,38 @@ + +// +---------------------------------------------------------------------- + +// +---------------------------------------------------------------------- +// | 模板设置 +// +---------------------------------------------------------------------- + +return [ + // 模板引擎类型 支持 php think 支持扩展 + 'type' => 'php', + // 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 3 保持操作方法 + 'auto_rule' => 1, + // 模板路径 + 'view_path' => '', + // 模板后缀 + 'view_suffix' => 'php', + // 模板文件名分隔符 + 'view_depr' => DIRECTORY_SEPARATOR, + // 模板引擎普通标签开始标记 + 'tpl_begin' => '{{', + // 模板引擎普通标签结束标记 + 'tpl_end' => '}}', + // 标签库标签开始标记 + 'taglib_begin' => '{{', + // 标签库标签结束标记 + 'taglib_end' => '}}', + + 'layout_on' => true, + 'layout_name' => 'layout', +]; diff --git a/config/trace.php b/config/trace.php new file mode 100644 index 0000000..425d301 --- /dev/null +++ b/config/trace.php @@ -0,0 +1,18 @@ + +// +---------------------------------------------------------------------- + +// +---------------------------------------------------------------------- +// | Trace设置 开启 app_trace 后 有效 +// +---------------------------------------------------------------------- +return [ + // 内置Html Console 支持扩展 + 'type' => 'Html', +]; diff --git a/config/wechat_device_api.php b/config/wechat_device_api.php new file mode 100644 index 0000000..a248025 --- /dev/null +++ b/config/wechat_device_api.php @@ -0,0 +1,19 @@ + 'ChuKeBao', + + // 各个供应商适配器的配置 + 'adapters' => [ + 'ChuKeBao' => [ + 'driver' => \WeChatDeviceApi\Adapters\ChuKebao\Adapter::class, +// 'api_key' => env('ChuKebao_API_KEY', ''), +// 'api_secret' => env('ChuKebao_API_SECRET', ''), + 'base_url' => env('api.wechat_url'), + 'username' => env('api.username', ''), + 'password' => env('api.password', ''), + ], + // ... 更多供应商 + ], +]; \ No newline at end of file diff --git a/config/worker.php b/config/worker.php new file mode 100644 index 0000000..a385c80 --- /dev/null +++ b/config/worker.php @@ -0,0 +1,31 @@ + +// +---------------------------------------------------------------------- +use think\facade\Env; + +// +---------------------------------------------------------------------- +// | Workerman设置 仅对 php think worker 指令有效 +// +---------------------------------------------------------------------- +return [ + // 扩展自身需要的配置 + 'host' => '0.0.0.0', // 监听地址 + 'port' => 2346, // 监听端口 + 'root' => '', // WEB 根目录 默认会定位public目录 + 'app_path' => '', // 应用目录 守护进程模式必须设置(绝对路径) + 'file_monitor' => false, // 是否开启PHP文件更改监控(调试模式下自动开启) + 'file_monitor_interval' => 2, // 文件监控检测时间间隔(秒) + 'file_monitor_path' => [], // 文件监控目录 默认监控application和config目录 + + // 支持workerman的所有配置参数 + 'name' => 'thinkphp', + 'count' => 4, + 'daemonize' => false, + 'pidFile' => Env::get('runtime_path') . 'worker.pid', +]; diff --git a/config/worker_server.php b/config/worker_server.php new file mode 100644 index 0000000..a1ee5fd --- /dev/null +++ b/config/worker_server.php @@ -0,0 +1,59 @@ + +// +---------------------------------------------------------------------- +use think\facade\Env; + +// +---------------------------------------------------------------------- +// | Workerman设置 仅对 php think worker:server 指令有效 +// +---------------------------------------------------------------------- +return [ + // 扩展自身需要的配置 + 'protocol' => 'websocket', // 协议 支持 tcp udp unix http websocket text + 'host' => '0.0.0.0', // 监听地址 + 'port' => 2345, // 监听端口 + 'socket' => '', // 完整监听地址 + 'context' => [], // socket 上下文选项 + 'worker_class' => 'app\common\TaskServer', // 自定义Workerman服务类名 支持数组定义多个服务 + + // 支持workerman的所有配置参数 + 'name' => 'thinkphp', + 'count' => 4, + 'daemonize' => false, + 'pidFile' => __DIR__ . '/../runtime/worker.pid', + 'logFile' => __DIR__ . '/../runtime/workerman.log', + 'stdoutFile' => __DIR__ . '/../runtime/stdout.log', + 'daemonize' => true, // 你用 -d 时会自动变 true + + // 支持事件回调 + // onWorkerStart + 'onWorkerStart' => function ($worker) { + //\app\common\socket\MessageHandler::onWorkerStart($worker); + }, + // onWorkerReload + 'onWorkerReload' => function ($worker) { + //\app\common\socket\MessageHandler::onWorkerReload($worker); + }, + // onConnect + 'onConnect' => function ($connection) { + //\app\common\socket\MessageHandler::onConnect($connection); + }, + // onMessage + 'onMessage' => function ($connection, $data) { + //\app\common\socket\MessageHandler::onMessage($connection, $data); + }, + // onClose + 'onClose' => function ($connection) { + //\app\common\socket\MessageHandler::onClose($connection); + }, + // onError + 'onError' => function ($connection, $code, $msg) { + //\app\common\socket\MessageHandler::onError($connection); + }, +]; diff --git a/crontab_tasks.md b/crontab_tasks.md new file mode 100644 index 0000000..38e61ea --- /dev/null +++ b/crontab_tasks.md @@ -0,0 +1,181 @@ +# 新版微信服务器定时任务配置 + +以下为当前 command.php 注册的所有计划任务示例,按需调整执行频率和日志路径。 + +```bash +# 设备列表 +*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think device:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/device_list.log 2>&1 + +# 微信好友列表 +*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatFriends:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/wechat_friends_list.log 2>&1 + +# 微信群列表 +*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatChatroom:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/wechat_chatroom_list.log 2>&1 + +# 添加好友任务列表 +*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think friendTask:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/friend_task_list.log 2>&1 + +# 微信客服列表 +*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatList:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/wechat_list.log 2>&1 + +# 公司账号列表 +*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think account:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/account_list.log 2>&1 + +# 微信好友消息列表 +*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think message:friendsList >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/message_friends_list.log 2>&1 + +# 微信群聊消息列表 +*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think message:chatroomList >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/message_chatroom_list.log 2>&1 + +# 部门列表 +*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think department:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/department_list.log 2>&1 + +# 同步内容库 +0 2 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think content:sync >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/content_sync.log 2>&1 + +# 微信群好友列表 +*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think groupFriends:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/group_friends_list.log 2>&1 + +# 获取通话记录 +*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think call-recording:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/call_recording.log 2>&1 + + +# 分配规则列表 +0 3 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think allotrule:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/allot_rule_list.log 2>&1 + +# 自动创建分配规则 +0 4 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think allotrule:autocreate >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/allot_rule_autocreate.log 2>&1 + +# 内容采集任务 +0 5 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think content:collect >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/content_collect.log 2>&1 + +# 朋友圈采集任务 +0 6 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think moments:collect >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/moments_collect.log 2>&1 + +# 工作台自动点赞任务 +0 7 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:autoLike >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_auto_like.log 2>&1 + +# 工作台朋友圈同步任务 +0 8 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:moments >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_moments.log 2>&1 + +# 同步微信数据到存客宝 +0 9 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think sync:wechatData >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/sync_wechat_data.log 2>&1 + +# 工作台群发消息 +*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:groupPush >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_groupPush.log 2>&1 + +# 工作台建群 +*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:groupCreate >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_groupCreate.log 2>&1 + +# 工作台通讯录导入 +*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:import-contact >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/import_contact.log 2>&1 + +# 工作台流量分发 +0 9 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:trafficDistribute >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/traffic_distribute.log 2>&1 + + + +# 预防性切换好友 +*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think switch:friends >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/switch_friends.log 2>&1 + +# 消息提醒 +*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think kf:notice >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/kf_notice.log 2>&1 + +# 客服评分 +0 2 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechat:calculate-score >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/calculate_score.log 2>&1 + + + + + +``` + +## 说明 + +- 所有命令都在 `/www/wwwroot/mckb_quwanzhi_com/Server` 目录下执行 +- 默认只获取未删除(活跃)的设备、微信好友和群聊 +- 已注释的命令(以#开头)是获取已删除或已停用数据的任务,可根据需要取消注释启用 +- 每个命令的执行结果都会记录到对应的日志文件中 +- 日志文件名格式包含了数据状态(如 `_active`, `_deleted`, `_stopped`) +- 日志文件位于 `/www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/` 目录下 +- 大部分任务每5分钟执行一次(`*/5 * * * *` 表示每小时的第0,5,10,15...55分钟执行) +- 设备列表的未删除设备任务每天凌晨1点执行一次(`0 1 * * *`) +- 自动创建分配规则每小时整点执行一次(`0 * * * *`) +- 内容采集任务每5分钟执行一次(`*/5 * * * *`) + +## 检查定时任务 + +使用以下命令查看当前配置的 crontab 任务: + +```bash +crontab -l +``` + +```bash +- 本地: php think worker:server +- 线上: php think worker:server -d (自带守护进程,无需搭配Supervisor 之类的工具) +- php think worker:server stop php think worker:server status +``` + + + +# 设备列表 - 未删除设备(每半小时执行) +*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think device:list --isDel=0 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_device_active.log 2>&1 +# 设备列表 - 已删除设备(每天1点执行) +0 1 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think device:list --isDel=1 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_device_deleted.log 2>&1 +# 设备列表 - 已停用设备(每天1:10执行) +10 1 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think device:list --isDel=2 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_device_stopped.log 2>&1 +# 微信好友列表 - 未删除好友(每1分钟执行) +*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatFriends:list --isDel=0 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_wechatFriends_active.log 2>&1 +# 微信好友列表 - 已删除好友(每天1:30分执行) +30 1 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatFriends:list --isDel=1 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_wechatFriends_deleted.log 2>&1 +# 微信群列表 - 未删除群(每5分钟执行) +*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatChatroom:list --isDel=0 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_wechatChatroom_active.log 2>&1 +# 微信群列表 - 已删除群(每天1:30分执行) +30 1 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatChatroom:list --isDel=1 >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_wechatChatroom_deleted.log 2>&1 +# 微信群好友列表(没5分钟执行) +*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think groupFriends:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_groupFriends.log 2>&1 +# 添加好友任务列表(每1分钟执行) +*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think friendTask:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_friendTask.log 2>&1 +# 微信客服列表(每5分钟执行) +*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatList:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_wechatList.log 2>&1 +# 公司账号列表(每5分钟执行) +*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think account:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_account.log 2>&1 +# 微信好友消息列表(每30分钟执行) +*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think message:friendsList >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_messageFriends.log 2>&1 +# 微信群聊消息列表(每30分钟执行) +*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think message:chatroomList >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_messageChatroom.log 2>&1 +# 获取通话记录 +*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think call-recording:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/call_recording.log 2>&1 +# 清洗微信数据 +*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think sync:wechatData >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/sync_wechat_data.log 2>&1 +# 内容采集任务(每5分钟执行) +*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think content:collect >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_contentCollect.log 2>&1 +# 工作台任务_自动点赞(每10分钟执行) +*/6 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:autoLike >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/crontab_workbench_autoLike.log 2>&1 +# 每3天的3点同步所有好友 +0 3 */3 * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think sync:allFriends >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/all_friends.log 2>&1 +# 工作台流量分发 +*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:trafficDistribute >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/traffic_distribute.log 2>&1 +# 工作台朋友圈同步任务 +*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:moments >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_moments.log 2>&1 +# 工作台群发消息 +#*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:groupPush >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_groupPush.log 2>&1 +# 预防性切换好友 +*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think switch:friends >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/switch_friends.log 2>&1 +# 工作台建群 +*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:groupCreate >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_groupCreate.log 2>&1 +# 工作台通讯录导入 +*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:import-contact >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/import_contact.log 2>&1 +# 消息提醒 +*/1 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think kf:notice >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/kf_notice.log 2>&1 +# 客服评分 +0 2 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechat:calculate-score >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/calculate_score.log 2>&1 + +# 采集客服自己的朋友圈 +*/30 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think own:moments:collect >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/own_moments_collect.log 2>&1 + + + +# 每分钟执行一次调度器(调度器内部会自动判断哪些任务需要执行) +* * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think scheduler:run >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/scheduler.log 2>&1 diff --git a/extend/.gitignore b/extend/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/extend/AccountWeight/Exceptions/WechatAccountWeightAssessmentException.php b/extend/AccountWeight/Exceptions/WechatAccountWeightAssessmentException.php new file mode 100644 index 0000000..51cb167 --- /dev/null +++ b/extend/AccountWeight/Exceptions/WechatAccountWeightAssessmentException.php @@ -0,0 +1,16 @@ + $wechatId, + ] + ) + ->value('activity'); + + return json_decode($activity)->yesterdayMsgCount ?? 0; + } + + /** + * @inheritDoc + */ + public function settingFactor($wechatId): WechatAccountWeightResultSetInterface + { + $times = intval($this->getChatTimesPerDay($wechatId) / 50 * 100); + + // 规定每天发送50条消息起拥有最高权重 + $this->weight = $times > 100 ? 100 : $times; + + return $this; + } + + /** + * @inheritDoc + */ + public function getResult(): int + { + return $this->weight ?: 0; + } +} \ No newline at end of file diff --git a/extend/AccountWeight/UnitWeight/AgeWeight.php b/extend/AccountWeight/UnitWeight/AgeWeight.php new file mode 100644 index 0000000..eb40c83 --- /dev/null +++ b/extend/AccountWeight/UnitWeight/AgeWeight.php @@ -0,0 +1,69 @@ + $wechatId, + ] + ) + ->value('basic'); + + $basic = json_decode($basic); + + // 如果没有设置账号注册时间,则默认今天,即账号年龄为0 + return $basic && isset($basic->registerDate) ? $basic->registerDate : date('Y-m-d', time()); + } + + /** + * 计算两个时间相差几个月 + * + * @param string $wechatId + * @return int + * @throws \DateMalformedStringException + */ + private function getDateTimeDiff(string $wechatId): int + { + $currentData = new \DateTime(date('Y-m-d', time())); + $registerDate = new \DateTime($this->getRegisterDate($wechatId)); + + $interval = date_diff($currentData, $registerDate); + + return $interval->y * 12 + $interval->m; + } + + /** + * @inheritDoc + */ + public function settingFactor($wechatId): WechatAccountWeightResultSetInterface + { + $cha = ceil($this->getDateTimeDiff($wechatId) / 60) * 100; + + // 规定账号年龄五年起拥有最高权重 + $this->weight = $cha > 100 ? 100 : $cha; + + return $this; + } + + /** + * @inheritDoc + */ + public function getResult(): int + { + return $this->weight ?: 0; + } +} \ No newline at end of file diff --git a/extend/AccountWeight/UnitWeight/RealNameWeight.php b/extend/AccountWeight/UnitWeight/RealNameWeight.php new file mode 100644 index 0000000..e2dec3d --- /dev/null +++ b/extend/AccountWeight/UnitWeight/RealNameWeight.php @@ -0,0 +1,39 @@ +weight = $this->hereWeGo(); + + return $this; + } + + /** + * @inheritDoc + */ + public function getResult(): int + { + return $this->weight ?: 0; + } +} \ No newline at end of file diff --git a/extend/AccountWeight/UnitWeight/RestrictWeight.php b/extend/AccountWeight/UnitWeight/RestrictWeight.php new file mode 100644 index 0000000..df2eb7f --- /dev/null +++ b/extend/AccountWeight/UnitWeight/RestrictWeight.php @@ -0,0 +1,50 @@ +field( + [ + 'r.id', 'r.restrictTime date', 'r.level', 'r.reason' + ] + ) + ->where('r.wechatId', $wechatId)->select() + ->count('*'); + } + + /** + * @inheritDoc + */ + public function settingFactor($wechatId): WechatAccountWeightResultSetInterface + { + $restrict = 10 - $this->getRestrictCount($wechatId); + + // 规定没有限制记录拥有最高权重,10条以上权重为0 + $this->weight = ($restrict < 0 ? 0 : $restrict) * 10; + + return $this; + } + + /** + * @inheritDoc + */ + public function getResult(): int + { + return $this->weight ?: 0; + } +} \ No newline at end of file diff --git a/extend/AccountWeight/WechatAccountWeightAssessment.php b/extend/AccountWeight/WechatAccountWeightAssessment.php new file mode 100644 index 0000000..0d53b1f --- /dev/null +++ b/extend/AccountWeight/WechatAccountWeightAssessment.php @@ -0,0 +1,140 @@ +classTable = $classTable ?? app('ClassTable'); + } + + /** + * 获取言 + * @return string + * @throws WechatAccountWeightAssessmentException + */ + private function getWechatId(): string + { + if (empty($this->wechatId)) { + throw new WeightAssessmentException('缺少验证参数'); + } + + return $this->wechatId; + } + + /** + * @inheritDoc + */ + public function calculAgeWeight(): WechatAccountWeightResultSetInterface + { + $AgeWeight = $this->classTable->getInstance(UnitWeight\AgeWeight::class); + + if (!$this->ageWeight) { + $this->ageWeight = $AgeWeight->settingFactor( + $this->getWechatId() + ) + ->getResult(); + } + + return $AgeWeight; + } + + /** + * @inheritDoc + */ + public function calculActivityWeigth(): WechatAccountWeightResultSetInterface + { + $ActivityWeigth = $this->classTable->getInstance(UnitWeight\ActivityWeigth::class); + + if (!$this->activityWeigth) { + $this->activityWeigth = $ActivityWeigth->settingFactor( + $this->getWechatId() + ) + ->getResult(); + } + + return $ActivityWeigth; + } + + /** + * @inheritDoc + */ + public function calculRestrictWeigth(): WechatAccountWeightResultSetInterface + { + $RestrictWeight = $this->classTable->getInstance(UnitWeight\RestrictWeight::class); + + if (!$this->restrictWeight) { + $this->restrictWeight = $RestrictWeight->settingFactor( + $this->getWechatId() + ) + ->getResult(); + } + + return $RestrictWeight; + } + + /** + * @inheritDoc + */ + public function calculRealNameWeigth(): WechatAccountWeightResultSetInterface + { + $AccountWeight = $this->classTable->getInstance(UnitWeight\RealNameWeight::class); + + if (!$this->realNameWeight) { + $this->realNameWeight = $AccountWeight->settingFactor( + $this->getWechatId() + ) + ->getResult(); + } + + return $AccountWeight; + } + + /** + * @inheritDoc + */ + public function getWeightScope(): int + { + return ceil( + ( + $this->calculAgeWeight()->getResult() + + $this->calculActivityWeigth()->getResult() + + $this->calculRestrictWeigth()->getResult() + + $this->calculRealNameWeigth()->getResult() + ) / 4); + } + + /** + * @inheritDoc + */ + public function settingFactor($params): WechatAccountWeightAssessmentInterface + { + if (!is_string($params)) { + throw new WeightAssessmentException('参数错误,只能传微信ID'); + } + + $this->wechatId = $params; + + return $this; + } +} \ No newline at end of file diff --git a/extend/AccountWeight/WechatFriendAddLimitAssessment.php b/extend/AccountWeight/WechatFriendAddLimitAssessment.php new file mode 100644 index 0000000..49bd887 --- /dev/null +++ b/extend/AccountWeight/WechatFriendAddLimitAssessment.php @@ -0,0 +1,25 @@ +getWeightScope(); + $lastDigit = $scope % 10; + + if ($scope < 10) { + $adjusted = $lastDigit < 5 ? 5 : 10; + } + + // 每5权重=1好友,最多20个 + return min(20, floor($adjusted / 5)); + } +} \ No newline at end of file diff --git a/extend/Eison/Utils/Helper/ArrHelper.php b/extend/Eison/Utils/Helper/ArrHelper.php new file mode 100644 index 0000000..a659c82 --- /dev/null +++ b/extend/Eison/Utils/Helper/ArrHelper.php @@ -0,0 +1,125 @@ +config = $config ?: Config::get('wechat_device_api.'); + $this->config = $config ?: Config::get('wechat_device_api.adapters.ChuKeBao'); + // $this->config = $config; + // $this->apiClient = new ChuKeBaoApiClient($config['api_key'], $config['api_secret'], $config['base_url']); + // 校验配置等... + if (empty($this->config['base_url']) || empty($this->config['username']) || empty($this->config['password'])) { + throw new \InvalidArgumentException("ChuKeBao username and password are required."); + } + } + + public function addFriend(string $deviceId, string $targetWxId): bool + { + // 1. 构建请求参数 (ChuKeBao 特定的格式) + $params = [ + 'device_identifier' => $deviceId, + 'wechat_user_to_add' => $targetWxId, + 'username' => $this->config['username'], + 'password' => $this->config['password'], + // ... 其他 ChuKeBao 特定参数 + ]; + + // 2. 调用 ChuKeBao 的 API (例如使用 GuzzleHttp 或 cURL) + // $response = $this->apiClient->post('/friend/add', $params); + // 伪代码: + $url = $this->config['base_url'] . '/friend/add'; + // $httpClient = new \GuzzleHttp\Client(); + // $response = $httpClient->request('POST', $url, ['form_params' => $params]); + // $responseData = json_decode($response->getBody()->getContents(), true); + + // 模拟API调用 + echo "ChuKeBao: Adding friend {$targetWxId} using device {$deviceId}\n"; + $responseData = ['code' => 0, 'message' => 'Success']; // 假设的响应 + + // 3. 处理响应,转换为标准结果 + if (!isset($responseData['code'])) { + throw new ApiException("ChuKeBao: Invalid API response for addFriend."); + } + + if ($responseData['code'] !== 0) { + throw new ApiException("ChuKeBao: Failed to add friend - " . ($responseData['message'] ?? 'Unknown error')); + } + + return true; + } + + public function likeMoment(string $deviceId, string $momentId): bool + { + echo "ChuKeBao: Liking moment {$momentId} using device {$deviceId}\n"; + // 实现 VendorA 的点赞逻辑 + return true; + } + + public function getGroupList(string $deviceId): array + { + echo "ChuKeBao: Getting group list for device {$deviceId}\n"; + // 实现 VendorA 的获取群列表逻辑,并转换数据格式 + return [ + ['id' => 'group1_va', 'name' => 'ChuKeBao Group 1', 'member_count' => 10], + ]; + } + + public function getFriendList(string $deviceId): array + { + echo "VendorA: Getting friend list for device {$deviceId}\n"; + return [ + ['id' => 'friend1_va', 'nickname' => 'ChuKeBao Friend 1', 'remark' => 'VA-F1'], + ]; + } + + public function getDeviceInfo(string $deviceId): array + { + echo "ChuKeBao: Getting device info for device {$deviceId}\n"; + return ['id' => $deviceId, 'status' => 'online_va', 'battery' => '80%']; + } + + public function bindDeviceToCompany(string $deviceId, string $companyId): bool + { + echo "ChuKeBao: Binding device {$deviceId} to company {$companyId}\n"; + return true; + } + + /** + * 获取群成员列表 + * @param string $deviceId 设备ID + * @param string $chatroomId 群ID + * @return array 群成员列表 + */ + public function getChatroomMemberList(string $deviceId, string $chatroomId): array + { + echo "ChuKeBao: Getting chatroom member list for device {$deviceId}, chatroom {$chatroomId}\n"; + return [ + ['id' => 'member1_va', 'nickname' => 'VendorA Member 1', 'avatar' => ''], + ]; + } + + /** + * 获取指定微信的朋友圈内容/列表 + * @param string $deviceId 设备ID + * @param string $wxId 微信ID + * @return array 朋友圈列表 + */ + public function getMomentList(string $deviceId, string $wxId): array + { + echo "VendorA: Getting moment list for device {$deviceId}, wxId {$wxId}\n"; + return [ + ['id' => 'moment1_va', 'content' => 'VendorA Moment 1', 'created_at' => time()], + ]; + } + + + /** + * 发送微信朋友圈 + * @param string $deviceId 设备ID + * @param string $wxId 微信ID + * @param string $moment 朋友圈内容 + * @return bool 是否成功 + */ + public function sendMoment(string $deviceId, string $wxId, string $moment): bool + { + echo "VendorA: Sending moment for device {$deviceId}, wxId {$wxId}, content: {$moment}\n"; + return true; + } + + public function handleCustomerTaskWithStatusIsNew(int $current_worker_id, int $process_count_for_status_0) + { + $task = Db::name('customer_acquisition_task') + ->where(['status' => 1, 'deleteTime' => 0]) + /* ->whereRaw("id % $process_count_for_status_0 = {$current_worker_id}")*/ + ->order('id desc') + ->select(); + + if (empty($task)) { + return false; + } + + $taskData = []; + foreach ($task as $item) { + $reqConf = json_decode($item['reqConf'], true); + $device = $reqConf['device'] ?? []; + $deviceCount = count($device); + if ($deviceCount <= 0) { + continue; + } + $tasks = Db::name('task_customer') + ->where(['status' => 0, 'task_id' => $item['id']]) + ->order('id DESC') + ->limit($deviceCount) + ->select(); + $taskData = array_merge($taskData, $tasks); + } + if ($taskData) { + + foreach ($taskData as $task) { + $task_id = $task['task_id']; + $task_info = $this->getCustomerAcquisitionTask($task_id); + if (empty($task_info['status']) || empty($task_info['reqConf']) || empty($task_info['reqConf']['device'])) { + continue; + } + //筛选出设备在线微信在线 + $wechatIdAccountIdMap = $this->getWeChatIdsAccountIdsMapByDeviceIds($task_info['reqConf']['device']); + if (empty($wechatIdAccountIdMap)) { + continue; + } + + $friendAddTaskCreated = false; + foreach ($wechatIdAccountIdMap as $accountId => $wechatId) { + // 是否已经是好友的判断,如果已经是好友,直接break; 但状态还是维持1,让另外一个进程处理发消息的逻辑 + $wechatTags = json_decode($task['tags'], true); + $isFriend = $this->checkIfIsWeChatFriendByPhone($wechatId, $task['phone'], $task['siteTags']); + if (!empty($isFriend)) { + $friendAddTaskCreated = true; + $task['processed_wechat_ids'] = $task['processed_wechat_ids'] . ',' . $wechatId; // 处理失败任务用,用于过滤已处理的微信号 + break; + } + + // 判断时间间隔\时间段和最后一次的状态 + $canCreateFriendAddTask = $this->checkIfCanCreateFriendAddTask($wechatId, $task_info['reqConf']); + if (empty($canCreateFriendAddTask)) { + continue; + } + + // 根据健康分判断24h内加的好友数量限制 + $healthScoreService = new WechatAccountHealthScoreService(); + $healthScoreInfo = $healthScoreService->getHealthScore($accountId); + + // 如果健康分记录不存在,先计算一次 + if (empty($healthScoreInfo)) { + try { + $healthScoreService->calculateAndUpdate($accountId); + $healthScoreInfo = $healthScoreService->getHealthScore($accountId); + } catch (\Exception $e) { + Log::error("计算健康分失败 (accountId: {$accountId}): " . $e->getMessage()); + // 如果计算失败,使用默认值5作为兜底 + $maxAddFriendPerDay = 5; + } + } + + // 获取每日最大加人次数(基于健康分) + $maxAddFriendPerDay = $healthScoreInfo['maxAddFriendPerDay'] ?? 5; + + // 如果健康分为0或很低,不允许添加好友 + if ($maxAddFriendPerDay <= 0) { + Log::info("账号健康分过低,不允许添加好友 (accountId: {$accountId}, wechatId: {$wechatId}, healthScore: " . ($healthScoreInfo['healthScore'] ?? 0) . ")"); + continue; + } + + // 检查频繁暂停限制:首次频繁或再次频繁,暂停24小时 + $lastFrequentTime = $healthScoreInfo['lastFrequentTime'] ?? null; + $frequentCount = $healthScoreInfo['frequentCount'] ?? 0; + if (!empty($lastFrequentTime) && $frequentCount > 0) { + $frequentPauseHours = 24; // 频繁暂停24小时 + $frequentPauseTime = $lastFrequentTime + ($frequentPauseHours * 3600); + $currentTime = time(); + + if ($currentTime < $frequentPauseTime) { + $remainingHours = ceil(($frequentPauseTime - $currentTime) / 3600); + Log::info("账号频繁,暂停添加好友 (accountId: {$accountId}, wechatId: {$wechatId}, frequentCount: {$frequentCount}, 剩余暂停时间: {$remainingHours}小时)"); + continue; + } + } + + // 检查封号暂停限制:封号暂停72小时 + $isBanned = $healthScoreInfo['isBanned'] ?? 0; + if ($isBanned == 1) { + // 查询封号时间(从s2_wechat_message表查询最近一次封号消息) + $banMessage = Db::table('s2_wechat_message') + ->where('wechatAccountId', $accountId) + ->where('msgType', 10000) + ->where('content', 'like', '%你的账号被限制%') + ->where('isDeleted', 0) + ->order('createTime', 'desc') + ->find(); + + if (!empty($banMessage)) { + $banTime = $banMessage['createTime'] ?? 0; + $banPauseHours = 72; // 封号暂停72小时 + $banPauseTime = $banTime + ($banPauseHours * 3600); + $currentTime = time(); + + if ($currentTime < $banPauseTime) { + $remainingHours = ceil(($banPauseTime - $currentTime) / 3600); + Log::info("账号封号,暂停添加好友 (accountId: {$accountId}, wechatId: {$wechatId}, 剩余暂停时间: {$remainingHours}小时)"); + continue; + } + } + } + + // 判断今天添加的好友数量,使用健康分计算的每日最大加人次数 + // 优先使用今天添加的好友数量(更符合"每日"限制) + $todayAddedFriendsCount = $this->getTodayAddedFriendsCount($wechatId); + if ($todayAddedFriendsCount >= $maxAddFriendPerDay) { + Log::info("今天添加好友数量已达上限 (accountId: {$accountId}, wechatId: {$wechatId}, count: {$todayAddedFriendsCount}, max: {$maxAddFriendPerDay}, healthScore: " . ($healthScoreInfo['healthScore'] ?? 0) . ")"); + continue; + } + + // 如果今天添加数量未达上限,再检查24小时内的数量(作为额外保护) + $last24hAddedFriendsCount = $this->getLast24hAddedFriendsCount($wechatId); + // 24小时内的限制可以稍微宽松一些,设置为每日限制的1.2倍(防止跨天累积) + $max24hLimit = (int)ceil($maxAddFriendPerDay * 1.2); + if ($last24hAddedFriendsCount >= $max24hLimit) { + Log::info("24小时内添加好友数量已达上限 (accountId: {$accountId}, wechatId: {$wechatId}, count: {$last24hAddedFriendsCount}, max24h: {$max24hLimit}, maxDaily: {$maxAddFriendPerDay})"); + continue; + } + + // 采取乐观尝试的策略,假设第一个可以添加的人可以添加成功的; 回头再另外一个任务进程去判断 + + // 创建好友添加任务, 对接触客宝 + $tags = array_merge($task_info['tagConf']['customTags'], $task_info['tagConf']['scenarioTags']); + if (!empty($wechatTags)) { + $tags = array_merge($tags, $wechatTags); + } + $tags = array_unique($tags); + $tags = array_values($tags); + $conf = array_merge($task_info['reqConf'], ['task_name' => $task_info['name'], 'tags' => $tags]); + + + $this->createFriendAddTask($accountId, $task['phone'], $conf, $task['remark']); + $friendAddTaskCreated = true; + $task['processed_wechat_ids'] = $task['processed_wechat_ids'] . ',' . $wechatId; // 处理失败任务用,用于过滤已处理的微信号 + break; + } + if (!empty($friendAddTaskCreated)){ + Db::name('task_customer') + ->where('id', $task['id']) + ->update([ + 'status' => $friendAddTaskCreated ? 1 : 3, + 'fail_reason' => '', + 'processed_wechat_ids' => $task['processed_wechat_ids'], + 'addTime' => time(), + 'updateTime' => time() + ]); + } + // ~~不用管,回头再添加再判断即可~~ + // 失败一定是另一个进程/定时器在检查的 + + } + } + } + + // 处理添加中的获客任务, only run in workerman process! + public function handleCustomerTaskWithStatusIsCreated() + { + + $tasks = Db::name('task_customer') + ->whereIn('status', [1, 2]) + ->where('updateTime', '>=', (time() - 86400 * 3)) + ->limit(50) + ->order('updateTime DESC') + ->select(); + + if (empty($tasks)) { + return; + } + + foreach ($tasks as $task) { + $task_id = $task['task_id']; + $task_info = $this->getCustomerAcquisitionTask($task_id); + + + if (empty($task_info['status']) || empty($task_info['reqConf']) || empty($task_info['reqConf']['device'])) { + continue; + } + + if (empty($task['processed_wechat_ids'])) { + continue; + } + + $weChatIds = explode(',', $task['processed_wechat_ids']); + $passedWeChatId = ''; + foreach ($weChatIds as $wechatId) { + // 先是否是好友,如果不是好友,先查询执行状态,看是否还能以及需要换账号继续添加,还是直接更新状态为3 + // 如果添加成功,先更新为2,然后去发消息(先判断有无消息设置,发消息的log记录?) + if (!empty($wechatId)) { + $isFriend = $this->checkIfIsWeChatFriendByPhone($wechatId, $task['phone']); + if ($isFriend) { + // 更新状态为5(已通过未发消息) + Db::name('task_customer') + ->where('id', $task['id']) + ->update(['status' => 5,'passTime' => time(), 'updateTime' => time()]); + $passedWeChatId = $wechatId; + break; + } + } + } + + + if ($passedWeChatId && !empty($task_info['msgConf'])) { + + // 更新状态为4(已通过并已发消息) + Db::name('task_customer') + ->where('id', $task['id']) + ->update(['status' => 4,'passTime' => time(), 'updateTime' => time()]); + + // 记录添加好友奖励(如果之前没有记录过,status从其他状态变为4时) + // 注意:如果status已经是2,说明已经记录过奖励,这里不再重复记录 + if ($task['status'] != 2 && !empty($task['channelId'])) { + try { + DistributionRewardService::recordAddFriendReward( + $task['task_id'], + $task['id'], + $task['phone'], + intval($task['channelId']) + ); + } catch (\Exception $e) { + // 记录错误但不影响主流程 + Log::error('记录添加好友奖励失败:' . $e->getMessage()); + } + } + + $wechatFriendRecord = $this->getWeChatAccoutIdAndFriendIdByWeChatIdAndFriendPhone($passedWeChatId, $task['phone']); + $msgConf = is_string($task_info['msgConf']) ? json_decode($task_info['msgConf'], 1) : $task_info['msgConf']; + $wechatFriendRecord && $this->sendMsgToFriend($wechatFriendRecord['id'], $wechatFriendRecord['wechatAccountId'], $msgConf); + + } else { + + foreach ($weChatIds as $wechatId) { + + // 查询执行状态 + $latestFriendTask = $this->getLatestFriendTaskByPhoneAndWeChatId($task['phone'], $wechatId); + if (empty($latestFriendTask)) { + continue; + } + + // 已经执行成功的话,直接break,同时更新对应task_customer的状态为2(添加成功) + if (isset($latestFriendTask['status']) && $latestFriendTask['status'] == 1) { + // 更新状态 + Db::name('task_customer') + ->where('id', $task['id']) + ->update(['status' => 2, 'updateTime' => time()]); + + // 记录添加好友奖励(异步处理,不影响主流程) + if (!empty($task['channelId'])) { + try { + DistributionRewardService::recordAddFriendReward( + $task['task_id'], + $task['id'], + $task['phone'], + intval($task['channelId']) + ); + } catch (\Exception $e) { + // 记录错误但不影响主流程 + Log::error('记录添加好友奖励失败:' . $e->getMessage()); + } + } + + break; + } + + // todo 判断处理执行失败的情况 status=2,根据 extra 的描述去处理;-- 可以先直接更新为失败,然后 extra =》fail_reason -- 因为有专门的任务会处理失败的 + if (isset($latestFriendTask['status']) && $latestFriendTask['status'] == 2) { + Db::name('task_customer') + ->where('id', $task['id']) + ->update(['status' => 3, 'fail_reason' => $latestFriendTask['extra'] ?? '未知原因', 'updateTime' => time()]); + break; + } + } + } + } + } + + + public function handleCustomerTaskNewUser() + { + $task = Db::name('customer_acquisition_task') + ->where(['status' => 1, 'deleteTime' => 0]) + ->whereIn('sceneId', [5, 7]) + ->order('id desc') + ->select(); + + if (empty($task)) { + return false; + } + + foreach ($task as $item) { + $sceneConf = json_decode($item['sceneConf'], true); + //电话 + if ($item['sceneId'] == 5) { + $rows = Db::name('call_recording') + ->where('companyId', $item['companyId']) + ->group('phone') + ->field('id,phone') + ->order('id asc') + ->limit(0, 100) + ->select(); + } + + if ($item['sceneId'] == 7) { + if (!empty($sceneConf['groupSelected']) && is_array($sceneConf['groupSelected'])) { + $rows = Db::name('wechat_group_member')->alias('gm') + ->join('wechat_account wa', 'gm.identifier = wa.wechatId') + ->where('gm.companyId', $item['companyId']) + ->whereIn('gm.groupId', $sceneConf['groupSelected']) + ->group('gm.identifier') + ->column('wa.id,wa.wechatId,wa.alias,wa.phone'); + } + } + + + if (in_array($item['sceneId'], [5, 7]) && !empty($rows) && is_array($rows)) { + // 1000条为一组进行批量处理 + $batchSize = 1000; + $totalRows = count($rows); + + for ($i = 0; $i < $totalRows; $i += $batchSize) { + $batchRows = array_slice($rows, $i, $batchSize); + + if (!empty($batchRows)) { + // 1. 提取当前批次的phone + $phones = []; + foreach ($batchRows as $row) { + if (!empty($row['phone'])) { + $phone = !empty($row['phone']); + } elseif (!empty($row['alias'])) { + $phone = $row['alias']; + } else { + $phone = $row['wechatId']; + } + if (!empty($phone)) { + $phones[] = $phone; + } + } + + // 2. 批量查询已存在的phone + $existingPhones = []; + if (!empty($phones)) { + $existing = Db::name('task_customer') + ->where('task_id', $item['id']) + ->where('phone', 'in', $phones) + ->field('phone') + ->select(); + $existingPhones = array_column($existing, 'phone'); + } + + // 3. 过滤出新数据,批量插入 + $newData = []; + foreach ($batchRows as $row) { + if (!empty($row['phone'])) { + $phone = !empty($row['phone']); + } elseif (!empty($row['alias'])) { + $phone = $row['alias']; + } else { + $phone = $row['wechatId']; + } + if (!empty($phone) && !in_array($phone, $existingPhones)) { + $newData[] = [ + 'task_id' => $item['id'], + 'name' => '', + 'source' => '场景获客_' . $item['name'], + 'phone' => $phone, + 'tags' => json_encode([], JSON_UNESCAPED_UNICODE), + 'siteTags' => json_encode([], JSON_UNESCAPED_UNICODE), + 'createTime' => time(), + ]; + } + } + + // 4. 批量插入新数据 + if (!empty($newData)) { + Db::name('task_customer')->insertAll($newData); + } + } + } + } + + + } + } + + + // 发微信个人消息 + public function sendMsgToFriend(int $friendId, int $wechatAccountId, array $msgConf) + { + // 消息拼接 msgType(1:文本 3:图片 43:视频 47:动图表情包(gif、其他表情包) 49:小程序/其他:图文、文件) + // 当前,type 为文本、图片、动图表情包的时候,content为string, 其他情况为对象 {type: 'file/link/...', url: '', title: '', thunmbPath: '', desc: ''} + // $result = [ + // "content" => $dataArray['content'], + // "msgSubType" => 0, + // "msgType" => $dataArray['msgType'], + // "seq" => time(), + // "wechatAccountId" => $dataArray['wechatAccountId'], + // "wechatChatroomId" => 0, + // "wechatFriendId" => $dataArray['wechatFriendId'], + // ]; + $toAccountId = ''; + $username = Env::get('api.username', ''); + $password = Env::get('api.password', ''); + if (!empty($username) || !empty($password)) { + $toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId'); + } + + // 建立WebSocket + $wsController = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]); + + + $gap = 0; + foreach ($msgConf as $messages) { + foreach ($messages['messages'] as $content) { + + $msgType = 0; + $detail = ''; + switch ($content['type']) { + case 'text': + $msgType = 1; + $detail = $content['content']; + break; + case 'image': + $msgType = 3; + $detail = $content['content']; + break; + case 'video': + $msgType = 43; + $detail = $content['content']; + break; + + case 'file': + $msgType = 49; + + $detail = [ + 'type' => 'file', + 'title' => $content['content'][0]['name'], + 'url' => $content['content'][0]['url'], + ]; + $detail = json_encode($detail); + break; + + case 'miniprogram': + $msgType = 49; + $detail = ''; + break; + + case 'link': + $msgType = 49; + $detail = [ + 'type' => 'link', + 'title' => $content['title'], + 'url' => $content['linkUrl'], + 'thumbPath' => $content['cover'], + 'desc' => $content['description'], + ]; + $detail = json_encode($detail); + break; + + case 'group': + $msgType = 49; + $detail = ''; + break; + default : + $msgType = 47; + $detail = $content['content']; + break; + } + + + if (empty($detail)) { + continue; + } + + if ($gap) { + Timer::add($gap, function () use ($wsController, $friendId, $wechatAccountId, $msgType, $content, $detail) { + $wsController->sendPersonal([ + 'wechatFriendId' => $friendId, + 'wechatAccountId' => $wechatAccountId, + 'msgType' => $msgType, + 'content' => $detail, + ]); + }, [], false); + } else { + $wsController->sendPersonal([ + 'wechatFriendId' => $friendId, + 'wechatAccountId' => $wechatAccountId, + 'msgType' => $msgType, + 'content' => $detail, + ]); + } + + !empty($content['sendInterval']) && $gap += $content['sendInterval']; + } + } + + } + + // getCustomerAcquisitionTask + public function getCustomerAcquisitionTask($id) + { + // 先读取缓存 + $task_info = Db::name('customer_acquisition_task') + ->where('id', $id) + ->find(); + if ($task_info) { + $task_info['sceneConf'] = json_decode($task_info['sceneConf'], true); + $task_info['reqConf'] = json_decode($task_info['reqConf'], true); + $task_info['msgConf'] = json_decode($task_info['msgConf'], true); + $task_info['tagConf'] = json_decode($task_info['tagConf'], true); + } + return $task_info; + } + + // 检查是否是好友关系 + + public function checkIfIsWeChatFriendByPhone($wxId = '', $phone = '', $siteTags = '') + { + if (empty($wxId) || empty($phone)) { + return false; + } + + try { + $friend = Db::table('s2_wechat_friend') + ->where('ownerWechatId', $wxId) + ->where(['isPassed' => 1, 'isDeleted' => 0]) + ->where('phone|alias|wechatId', 'like', $phone . '%') + ->order('createTime', 'desc') + ->find(); + if (!empty($friend)) { + if (!empty($siteTags)) { + $siteTags = json_decode($siteTags, true); + $siteLabels = json_decode($friend['siteLabels'], true); + $tags = array_merge($siteTags, $siteLabels); + $tags = array_unique($tags); + $tags = array_values($tags); + if (empty($tags)) { + $tags = []; + } + $tags = json_encode($tags, 256); + Db::table('s2_wechat_friend')->where(['id' => $friend['id']])->update(['siteLabels' => $tags, 'updateTime' => time()]); + } + return true; + } else { + return false; + } + } catch (\Exception $e) { + Log::error("Error in checkIfIsWeChatFriendByPhone (wxId: {$wxId}, phone: {$phone}): " . $e->getMessage()); + return false; + } + } + + // getWeChatAccoutIdAndFriendIdByWeChatId + public function getWeChatAccoutIdAndFriendIdByWeChatIdAndFriendPhone(string $wechatId, string $phone): array + { + if (empty($wechatId) || empty($phone)) { + return []; + } + + return Db::table('s2_wechat_friend') + ->where('ownerWechatId', $wechatId) + ->where('phone|alias|wechatId', 'like', $phone . '%') + ->field('id,wechatAccountId,passTime,createTime') + ->find(); + } + + // 判断是否已添加某手机号为好友并返回添加时间 + public function getWeChatFriendPassTimeByPhone(string $wxId, string $phone): int + { + if (empty($wxId) || empty($phone)) { + return 0; + } + + try { + $record = Db::table('s2_wechat_friend') + ->where('ownerWechatId', $wxId) + ->where('phone|alias|wechatId', 'like', $phone . '%') + ->field('id,createTime,passTime') + ->find(); + + return $record['passTime'] ?? $record['createTime'] ?? 0; + } catch (\Exception $e) { + Log::error("Error in getWeChatFriendPassTimeByPhone (wxId: {$wxId}, phone: {$phone}): " . $e->getMessage()); + return 0; + } + } + + /** + * 查询某个微信今天添加了多少个好友 + * @param string $wechatId 微信ID + * @return int 好友数量 + */ + public function getTodayAddedFriendsCount(string $wechatId): int + { + if (empty($wechatId)) { + return 0; + } + try { + $count = Db::table('s2_friend_task') + ->where('wechatId', $wechatId) + ->whereRaw("FROM_UNIXTIME(createTime, '%Y-%m-%d') = CURDATE()") + ->count(); + return (int)$count; + } catch (\Exception $e) { + Log::error("Error in getTodayAddedFriendsCount (wechatId: {$wechatId}): " . $e->getMessage()); + return 0; + } + } + + /** + * 查询某个微信24小时内添加了多少个好友 + * @param string $wechatId 微信ID + * @return int 好友数量 + */ + public function getLast24hAddedFriendsCount(string $wechatId): int + { + if (empty($wechatId)) { + return 0; + } + try { + $twentyFourHoursAgo = time() - (24 * 60 * 60); + $count = Db::table('s2_friend_task') + ->where('wechatId', $wechatId) + ->where('createTime', '>=', $twentyFourHoursAgo) + ->count(); + return (int)$count; + } catch (\Exception $e) { + Log::error("Error in getLast24hAddedFriendsCount (wechatId: {$wechatId}): " . $e->getMessage()); + return 0; + } + } + + /** + * 查询某个微信最新的一条添加好友任务记录 + * @param string $wechatId 微信ID + * @return array|null 任务记录或null + */ + public function getLatestFriendTask(string $wechatId): ?array + { + if (empty($wechatId)) { + return null; + } + try { + $task = Db::table('s2_friend_task') + ->where('wechatId', $wechatId) + ->order('createTime', 'desc') + ->find(); + return $task; + } catch (\Exception $e) { + Log::error("Error in getLatestFriendTask (wechatId: {$wechatId}): " . $e->getMessage()); + return null; + } + } + + // 获取某微信最后一条添加好友任务 + public function getLatestFriendTaskByPhoneAndWeChatId(string $phone, string $wechatId): array + { + if (empty($phone) || empty($wechatId)) { + return []; + } + + $record = Db::table('s2_friend_task') + ->where('phone', $phone) + ->where('wechatId', $wechatId) + ->order('createTime', 'desc') + ->find(); + return $record ?: []; + } + + // 获取最新的一条添加好友任务记录的创建时间 + public function getLastCreateFriendTaskTime(string $wechatId): int + { + if (empty($wechatId)) { + return 0; + } + $record = Db::table('s2_friend_task') + ->where('wechatId', $wechatId) + ->order('createTime', 'desc') + ->find(); + return $record['createTime'] ?? 0; + } + + // 判断是否能够加好友 + public function checkIfCanCreateFriendAddTask(string $wechatId, $conf = []): bool + { + if (empty($wechatId)) { + return false; + } + //强制请求添加好友的列表 + $friendController = new FriendTaskController(); + $friendController->getlist(0, 50); + + + $record = $this->getLatestFriendTask($wechatId); + if (empty($record)) { + return true; + } + + if (!empty($conf['addFriendInterval']) && isset($record['createTime']) && $record['createTime'] > time() - $conf['addFriendInterval'] * 60) { + return false; + } + + if (!empty($conf['startTime']) && !empty($conf['endTime'])) { + $currentTime = date('H:i'); + $startTime = $conf['startTime']; + $endTime = $conf['endTime']; + + if ($currentTime >= $startTime && $currentTime <= $endTime) { + return true; + } else { + return false; + } + } + + if (isset($record['status'])) { + + if ($record['status'] == 2) { + + // 判断$record['extra'] 是否包含文字: 操作过于频繁;如果包含判断 updateTime 是否已经超过72min,updateTime是10位时间戳;如果包含指定文字且时间未超过72min,return false + if (isset($record['extra']) && strpos($record['extra'], '操作过于频繁') !== false) { + $updateTime = isset($record['updateTime']) ? (int)$record['updateTime'] : 0; + $now = time(); + $diff = $now - $updateTime; + + if ($diff < 24 * 60 * 60) { + return false; + } + } + } + } + + return true; + } + + // 获取触客宝系统的客服微信账号id,用于后续微信相关操作 + public function getWeChatAccountIdByWechatId(string $wechatId): string + { + if (empty($wechatId)) { + return ''; + } + $record = Db::table('s2_wechat_account') + ->where('wechatId', $wechatId) + ->field('id') + ->find(); + return $record['id'] ?? ''; + } + + // 获取在线的客服微信账号id列表 + public function getOnlineWeChatAccountIdsByWechatIds(array $wechatIds): array + { + if (empty($wechatIds)) { + return []; + } + $records = Db::table('s2_wechat_account') + ->where('deviceAlive', 1) + ->where('wechatAlive', 1) + ->where('wechatId', 'in', $wechatIds) + ->field('id,wechatId') + ->column('id', 'wechatId'); + + return $records; + } + + public function getWeChatIdsAccountIdsMapByDeviceIds(array $deviceIds): array + { + if (empty($deviceIds)) { + return []; + } + + $records = Db::table('s2_wechat_account') + ->where('deviceAlive', 1) + ->where('wechatAlive', 1) + ->where('currentDeviceId', 'in', $deviceIds) + ->field('id,wechatId') + ->column('id,wechatId'); + return $records; + } + + // 触客宝添加好友API + public function addFriendTaskApi(int $wechatAccountId, string $phone, string $message, string $remark, array $labels, $authorization = '') + { + + $authorization = $authorization ?: AuthService::getSystemAuthorization(); + + if (empty($authorization)) { + return [ + 'status_code' => 0, + 'body' => null, + 'error' => true, + ]; + } + + $params = [ + 'phone' => $phone, + 'message' => $message, + 'remark' => $remark, + 'labels' => $labels, + 'wechatAccountId' => $wechatAccountId + ]; + + //准备发起添加请求 + $friendController = new FriendTaskController(); + $result = $friendController->addFriendTask($params); + $result = json_decode($result, true); + if ($result['code'] == 200) { + return $result; + } else { + $authorization = AuthService::getSystemAuthorization(false); + return $this->addFriendTaskApi($wechatAccountId, $phone, $message, $remark, $labels, $authorization); + } + + + } + + // 创建添加好友任务/执行添加 + public function createFriendAddTask(int $wechatAccountId, string $phone, array $conf, $remark = '') + { + if (empty($wechatAccountId) || empty($phone) || empty($conf)) { + return; + } + + if (empty($remark)){ + switch ($conf['remarkType']) { + case 'phone': + $remark = $phone . '-' . $conf['task_name']; + break; + case 'nickname': + $remark = ''; + break; + case 'source': + $remark = $conf['task_name']; + break; + default: + $remark = ''; + break; + } + } + + $tags = []; + if (!empty($conf['tags'])) { + if (is_array($conf['tags'])) { + $tags = $conf['tags']; + } + } + $res = $this->addFriendTaskApi($wechatAccountId, $phone, $conf['greeting'] ?? '你好', $remark, $tags); + } + + /* TODO: 以上方法待实现,基于/参考 application/api/controller/WebSocketController.php 去实现;以下同步脚本用的方法转移到其他类 */ + + + // NOTE: run in background; 5min 同步一次 + public function syncFriendship() + { + $sql = "INSERT INTO ck_wechat_friendship(id,wechatId,tags,memo,ownerWechatId,createTime,updateTime,deleteTime,companyId) + SELECT + f.id,f.wechatId,f.labels as tags,f.conRemark as memo,f.ownerWechatId,f.createTime,f.updateTime,f.deleteTime, + c.departmentId + FROM s2_wechat_friend f + LEFT JOIN s2_wechat_account a on a.id = f.wechatAccountId + LEFT JOIN s2_company_account c on c.id = a.deviceAccountId + ORDER BY f.id DESC + LIMIT ?, ? + ON DUPLICATE KEY UPDATE + id=VALUES(id), + tags=VALUES(tags), + memo=VALUES(memo), + updateTime=VALUES(updateTime), + deleteTime=VALUES(deleteTime), + companyId=VALUES(companyId)"; + + $offset = 0; + $limit = 2000; + $usleepTime = 50000; + + do { + $affected = Db::execute($sql, [$offset, $limit]); + $offset += $limit; + if ($affected > 0) { + usleep($usleepTime); + } + } while ($affected > 0); + } + + + public function syncWechatAccount() + { + $pk = 'wechatId'; + $limit = 1000; + // $lastId = ''; + $lastId = null; // Or some other sentinel indicating "first run" + + + $totalAffected = 0; + $iterations = 0; + $maxIterations = 10000; + + do { + // Fetch a batch of distinct wechatIds + // Important: Order by wechatId for consistent pagination + $sourceDb = Db::connect()->table('s2_wechat_friend'); + // if ($lastId !== '') { // For subsequent iterations + if (!is_null($lastId)) { // Check if it's not the first iteration + $sourceDb->where($pk, '>', $lastId); + } + $distinctWechatIds = $sourceDb->order($pk, 'ASC') + ->distinct(true) + ->limit($limit) + ->column($pk); // Get an array of wechatIds + + if (empty($distinctWechatIds)) { + break; // No more wechatIds to process + } + + // Prepare the main IODKU query for this batch of wechatIds + $sql = "INSERT INTO ck_wechat_account(wechatId,alias,nickname,pyInitial,quanPin,avatar,gender,region,signature,phone,country,privince,city,createTime,updateTime) + SELECT + wechatId,alias,nickname,pyInitial,quanPin,avatar,gender,region,signature,phone,country,privince,city,createTime,updateTime + FROM + s2_wechat_friend + WHERE wechatId IN (" . implode(',', array_fill(0, count($distinctWechatIds), '?')) . ") + GROUP BY wechatId -- Grouping within the selected wechatIds + ON DUPLICATE KEY UPDATE + alias=VALUES(alias), + nickname=VALUES(nickname), + pyInitial=VALUES(pyInitial), + quanPin=VALUES(quanPin), + avatar=VALUES(avatar), + gender=VALUES(gender), + region=VALUES(region), + signature=VALUES(signature), + phone=VALUES(phone), + country=VALUES(country), + privince=VALUES(privince), + city=VALUES(city), + updateTime=VALUES(updateTime)"; + + // The parameters for the IN clause are the distinctWechatIds themselves + $bindings = $distinctWechatIds; + + try { + $affected = Db::execute($sql, $bindings); + $totalAffected += $affected; + // Log::info("syncWechatAccount: Processed batch of " . count($distinctWechatIds) . " distinct wechatIds. Affected rows: " . $affected); + + // Update lastId for the next iteration + $lastId = end($distinctWechatIds); + + if ($affected > 0) { + usleep(50000); + } + } catch (\Exception $e) { + Log::error("syncWechatAccount batch error: " . $e->getMessage() . " with wechatIds starting around " . $distinctWechatIds[0] . ". SQL: " . $sql . " Bindings: " . json_encode($bindings)); + // Decide if you want to break or continue with the next batch + break; // Example: break on error + } + $iterations++; + } while (count($distinctWechatIds) === $limit && $iterations < $maxIterations); // Continue if we fetched a full batch + + // Log::info("syncWechatAccount finished. Total affected rows: " . $totalAffected); + return $totalAffected; + } + + + public function syncWechatDeviceLoginLog() + { + try { + $cursor = Db::table('s2_wechat_account') + ->alias('a') + ->join(['s2_device' => 'd'], 'd.imei = a.imei') + ->join(['s2_company_account' => 'c'], 'c.id = d.currentAccountId') + ->field('d.id as deviceId, a.wechatId, a.wechatAlive as alive, c.departmentId as companyId, a.updateTime as updateTime') + ->cursor(); + + foreach ($cursor as $item) { + + if (empty($item['deviceId']) || empty($item['wechatId']) || empty($item['companyId'])) { + continue; + } + + $exists = Db::table('ck_device_wechat_login') + ->where('deviceId', $item['deviceId']) + ->where('wechatId', $item['wechatId']) + ->where('companyId', $item['companyId']) + ->find(); + + if ($exists) { + Db::table('ck_device_wechat_login') + ->where('deviceId', $item['deviceId']) + ->where('wechatId', $item['wechatId']) + ->where('companyId', $item['companyId']) + ->update(['alive' => $item['alive'], 'updateTime' => $item['updateTime']]); + } else { + $item['createTime'] = $item['updateTime']; + Db::table('ck_device_wechat_login')->insert($item); + } + + } + + return true; + } catch (\Exception $e) { + Log::error("微信好友同步任务异常: " . $e->getMessage() . ", 堆栈: " . $e->getTraceAsString()); + return false; + } + } + + /** + * 大数据量分批处理版本 + * 适用于数据源非常大的情况,避免一次性加载全部数据到内存 + * 独立脚本执行,30min 同步一次 和 流量来源的更新一起 + * + * @param int $batchSize 每批处理的数据量 + * @return int 影响的行数 + */ + public function syncWechatFriendToTrafficPoolBatch($batchSize = 5000) + { + Db::execute("CREATE TEMPORARY TABLE IF NOT EXISTS temp_wechat_ids ( + wechatId VARCHAR(64) PRIMARY KEY + ) ENGINE=MEMORY"); + + Db::execute("TRUNCATE TABLE temp_wechat_ids"); + + // 批量插入去重的wechatId + Db::execute("INSERT INTO temp_wechat_ids SELECT DISTINCT wechatId FROM s2_wechat_friend"); + + $total = Db::table('temp_wechat_ids')->count(); + + $batchCount = ceil($total / $batchSize); + $affectedRows = 0; + + try { + for ($i = 0; $i < $batchCount; $i++) { + $offset = $i * $batchSize; + + $sql = "INSERT IGNORE INTO ck_traffic_pool(`identifier`, `wechatId`, `mobile`) + SELECT t.wechatId AS identifier, t.wechatId, + (SELECT phone FROM s2_wechat_friend + WHERE wechatId = t.wechatId LIMIT 1) AS mobile + FROM ( + SELECT wechatId FROM temp_wechat_ids LIMIT {$offset}, {$batchSize} + ) AS t"; + + $currentAffected = Db::execute($sql); + $affectedRows += $currentAffected; + + if ($i % 5 == 0) { + gc_collect_cycles(); + } + + usleep(30000); // 30毫秒 + } + } catch (\Exception $e) { + \think\facade\Log::error("Error in traffic pool sync: " . $e->getMessage()); + throw $e; + } finally { + Db::execute("DROP TEMPORARY TABLE IF EXISTS temp_wechat_ids"); + } + + return $affectedRows; + } + + + /** + * 同步/更新微信客服信息到ck_wechat_customer表 + * + * @param int $batchSize 每批处理的数据量 + * @return int 影响的行数 + */ + public function syncWechatCustomer($batchSize = 1000) + { + try { + // 1. 获取要处理的wechatId和companyId列表 + $customerList = Db::table('ck_device_wechat_login') + ->field('DISTINCT wechatId, companyId') + ->order('id DESC') + ->select(); + + $totalAffected = 0; + $batchCount = ceil(count($customerList) / $batchSize); + + for ($i = 0; $i < $batchCount; $i++) { + $batch = array_slice($customerList, $i * $batchSize, $batchSize); + $insertData = []; + + foreach ($batch as $customer) { + $wechatId = $customer['wechatId']; + $companyId = $customer['companyId']; + + if (empty($wechatId)) continue; + + // 2. 获取s2_wechat_account数据 + $accountInfo = Db::table('s2_wechat_account') + ->where('wechatId', $wechatId) + ->find(); + + // 3. 获取群数量 (不包含 @openim 结尾的identifier) + $groupCount = Db::table('ck_wechat_group_member') + ->where('identifier', $wechatId) + ->where('customerIs', 1) + ->where('identifier', 'not like', '%@openim') + ->count(); + + // 4. 检查记录是否已存在 + $existingRecord = Db::table('ck_wechat_customer') + ->where('wechatId', $wechatId) + ->find(); + + // 5. 构建basic JSON数据 + $basic = []; + if ($existingRecord && !empty($existingRecord['basic'])) { + $basic = json_decode($existingRecord['basic'], true) ?: []; + } + + if (empty($basic['registerDate'])) { + $basic['registerDate'] = date('Y-m-d H:i:s', strtotime('-' . mt_rand(1, 150) . ' months')); + } + + // 6. 构建activity JSON数据 + $activity = []; + if ($existingRecord && !empty($existingRecord['activity'])) { + $activity = json_decode($existingRecord['activity'], true) ?: []; + } + + if ($accountInfo) { + $activity['yesterdayMsgCount'] = $accountInfo['yesterdayMsgCount'] ?? 0; + $activity['sevenDayMsgCount'] = $accountInfo['sevenDayMsgCount'] ?? 0; + $activity['thirtyDayMsgCount'] = $accountInfo['thirtyDayMsgCount'] ?? 0; + + // 计算totalMsgCount + if (empty($activity['totalMsgCount'])) { + $activity['totalMsgCount'] = $activity['thirtyDayMsgCount']; + } else { + $activity['totalMsgCount'] += $activity['yesterdayMsgCount']; + } + } + + // 7. 构建friendShip JSON数据 + $friendShip = []; + if ($existingRecord && !empty($existingRecord['friendShip'])) { + $friendShip = json_decode($existingRecord['friendShip'], true) ?: []; + } + + if ($accountInfo) { + $friendShip['totalFriend'] = $accountInfo['totalFriend'] ?? 0; + $friendShip['maleFriend'] = $accountInfo['maleFriend'] ?? 0; + $friendShip['unknowFriend'] = $accountInfo['unknowFriend'] ?? 0; + $friendShip['femaleFriend'] = $accountInfo['femaleFriend'] ?? 0; + } + $friendShip['groupNumber'] = $groupCount; + + // 8. 构建weight JSON数据 (每天只计算一次) + // $weight = []; + // if ($existingRecord && !empty($existingRecord['weight'])) { + // $weight = json_decode($existingRecord['weight'], true) ?: []; + + // // 如果不是今天更新的,重新计算权重 + // $lastUpdateDate = date('Y-m-d', $existingRecord['updateTime'] ?? 0); + // if ($lastUpdateDate !== date('Y-m-d')) { + // $weight = $this->calculateCustomerWeight($basic, $activity, $friendShip); + // } + // } else { + // $weight = $this->calculateCustomerWeight($basic, $activity, $friendShip); + // } + + // 9. 准备更新或插入的数据 + $data = [ + 'wechatId' => $wechatId, + 'companyId' => $companyId, + 'basic' => json_encode($basic), + 'activity' => json_encode($activity), + 'friendShip' => json_encode($friendShip), + // 'weight' => json_encode($weight), + 'createTime' => $accountInfo['createTime'], + 'updateTime' => time() + ]; + + if ($existingRecord) { + // 更新记录 + Db::table('ck_wechat_customer') + ->where('wechatId', $wechatId) + ->update($data); + } else { + // 插入记录 + Db::table('ck_wechat_customer')->insert($data); + } + + $totalAffected++; + } + + // 释放内存 + if ($i % 5 == 0) { + gc_collect_cycles(); + } + + usleep(50000); // 50毫秒短暂休息 + } + + return $totalAffected; + } catch (\Exception $e) { + Log::error("同步微信客服信息异常: " . $e->getMessage() . ", 堆栈: " . $e->getTraceAsString()); + throw $e; + } + } + + /** + * 计算客服权重 + * + * @param array $basic 基础信息 + * @param array $activity 活跃信息 + * @param array $friendShip 好友关系信息 + * @return array 权重信息 + */ + private function calculateCustomerWeight($basic, $activity, $friendShip) + { + // 1. 计算账号年龄权重(最大20分) + $ageWeight = 0; + if (!empty($basic['registerDate'])) { + $registerTime = strtotime($basic['registerDate']); + $accountAgeMonths = floor((time() - $registerTime) / (30 * 24 * 3600)); + $ageWeight = min(20, floor($accountAgeMonths / 12) * 4); + } + + // 2. 计算活跃度权重(最大30分) + $activityWeight = 0; + if (!empty($activity)) { + // 基于消息数计算活跃度 + $msgScore = 0; + if ($activity['thirtyDayMsgCount'] > 10000) $msgScore = 15; + elseif ($activity['thirtyDayMsgCount'] > 5000) $msgScore = 12; + elseif ($activity['thirtyDayMsgCount'] > 1000) $msgScore = 8; + elseif ($activity['thirtyDayMsgCount'] > 500) $msgScore = 5; + elseif ($activity['thirtyDayMsgCount'] > 100) $msgScore = 3; + + // 连续活跃天数加分(这里简化处理,实际可能需要更复杂逻辑) + $activeScore = min(15, $activity['yesterdayMsgCount'] > 10 ? 15 : floor($activity['yesterdayMsgCount'] / 2)); + + $activityWeight = $msgScore + $activeScore; + } + + // 3. 计算限制影响权重(最大15分) + $restrictWeight = 15; // 默认满分,无限制 + + // 4. 计算实名认证权重(最大10分) + $realNameWeight = 0; // 简化处理,默认未实名 + + // 5. 计算可加友数量限制(基于好友数量,最大为5000) + $addLimit = 0; + if (!empty($friendShip['totalFriend'])) { + $addLimit = max(0, min(5000 - $friendShip['totalFriend'], 5000)); + $addLimit = floor($addLimit / 1000); // 每1000个空位1分,最大5分 + } + + // 6. 计算总分(满分75+5分) + $scope = $ageWeight + $activityWeight + $restrictWeight + $realNameWeight; + + return [ + 'ageWeight' => $ageWeight, + 'activityWeight' => $activityWeight, // 注意这里修正了拼写错误 + 'restrictWeight' => $restrictWeight, + 'realNameWeight' => $realNameWeight, + 'scope' => $scope, + 'addLimit' => $addLimit + ]; + } + + /** + * 同步设备信息到ck_device表 + * 数据量不大,仅同步一次所有设备 + * + * @return int 影响的行数 + */ + public function syncDevice() + { + try { + $sql = "INSERT INTO ck_device(`id`, `imei`, `model`, phone, operatingSystem, memo, alive, brand, rooted, xPosed, softwareVersion, extra, createTime, updateTime, deleteTime, companyId) + SELECT + d.id, d.imei, d.model, d.phone, d.operatingSystem, d.memo, d.alive, d.brand, d.rooted, d.xPosed, d.softwareVersion, d.extra, d.createTime, d.lastUpdateTime, d.deleteTime, a.departmentId companyId + FROM s2_device d + JOIN s2_company_account a ON d.currentAccountId = a.id + ON DUPLICATE KEY UPDATE + `model` = VALUES(`model`), + `phone` = VALUES(`phone`), + `operatingSystem` = VALUES(`operatingSystem`), + `memo` = VALUES(`memo`), + `alive` = VALUES(`alive`), + `brand` = VALUES(`brand`), + `rooted` = VALUES(`rooted`), + `xPosed` = VALUES(`xPosed`), + `softwareVersion` = VALUES(`softwareVersion`), + `extra` = VALUES(`extra`), + `updateTime` = VALUES(`updateTime`), + `deleteTime` = VALUES(`deleteTime`), + `companyId` = VALUES(`companyId`)"; + + $affected = Db::execute($sql); + return $affected; + } catch (\Exception $e) { + Log::error("同步设备信息异常: " . $e->getMessage() . ", 堆栈: " . $e->getTraceAsString()); + return false; + } + } + + public function syncTrafficSourceUser() + { + $sql = "insert into ck_traffic_source(`identifier`,companyId,`fromd`,`sourceId`,`createTime`,`type`, `status`) + SELECT + f.wechatId identifier, + c.departmentId companyId, + f.ownerNickname fromd, + f.ownerWechatId sourceId, + f.createTime createTime, + 1 as type, + CASE WHEN f.isDeleted = 1 THEN -3 ELSE 3 END as status + FROM + s2_wechat_friend f + LEFT JOIN s2_wechat_account a ON f.wechatAccountId = a.id + LEFT JOIN s2_company_account c on c.id = a.deviceAccountId + ORDER BY f.id DESC + LIMIT ?, ? + ON DUPLICATE KEY UPDATE + identifier=VALUES(identifier), + companyId=VALUES(companyId), + sourceId=VALUES(sourceId)"; + + + $offset = 0; + $limit = 2000; + $usleepTime = 50000; + do { + $affected = Db::execute($sql, [$offset, $limit]); + $offset += $limit; + if ($affected > 0) { + usleep($usleepTime); + } + } while ($affected > 0); + } + + public function syncTrafficSourceGroup() + { + $sql = "insert into ck_traffic_source(`identifier`,companyId,`fromd`,`sourceId`,`createTime`,`type`, `status`) + SELECT + m.wechatId identifier, + c.departmentId companyId, + r.nickname fromd, + m.chatroomId sourceId, + m.createTime createTime, + 2 as type, + CASE WHEN m.friendType = 1 THEN 3 ELSE 1 END as status + FROM + s2_wechat_chatroom_member m + JOIN s2_wechat_chatroom r ON m.chatroomId = r.chatroomId + LEFT JOIN s2_wechat_account a ON a.id = r.wechatAccountId + LEFT JOIN s2_company_account c on c.id = a.deviceAccountId + GROUP BY m.wechatId + ORDER BY m.id DESC + LIMIT ?, ? + ON DUPLICATE KEY UPDATE + identifier=VALUES(identifier), + companyId=VALUES(companyId), + sourceId=VALUES(sourceId)"; + + + $offset = 0; + $limit = 2000; + $usleepTime = 50000; + do { + $affected = Db::execute($sql, [$offset, $limit]); + $offset += $limit; + if ($affected > 0) { + usleep($usleepTime); + } + } while ($affected > 0); + } + + public function syncWechatGroup() + { + $sql = "insert into ck_wechat_group(`id`,`wechatAccountId`,`chatroomId`,`name`,`avatar`,`companyId`,`ownerWechatId`,`createTime`,`updateTime`,`deleteTime`) + SELECT + g.id id, + g.wechatAccountId wechatAccountId, + g.chatroomId chatroomId, + g.nickname name, + g.chatroomAvatar avatar, + c.departmentId companyId, + g.wechatAccountWechatId ownerWechatId, + g.createTime createTime, + g.updateTime updateTime, + g.deleteTime deleteTime + FROM + s2_wechat_chatroom g + LEFT JOIN s2_company_account c ON g.accountId = c.id + ORDER BY g.id DESC + LIMIT ?, ? + ON DUPLICATE KEY UPDATE + chatroomId=VALUES(chatroomId), + companyId=VALUES(companyId), + ownerWechatId=VALUES(ownerWechatId)"; + + + $offset = 0; + $limit = 2000; + $usleepTime = 50000; + do { + $affected = Db::execute($sql, [$offset, $limit]); + $offset += $limit; + if ($affected > 0) { + usleep($usleepTime); + } + } while ($affected > 0); + } + + public function syncWechatGroupCustomer() + { + $sql = "insert into ck_wechat_group_member(`identifier`,`chatroomId`,`companyId`,`groupId`,`createTime`) + SELECT + m.wechatId identifier, + g.chatroomId chatroomId, + c.departmentId companyId, + g.id groupId, + m.createTime createTime + FROM + s2_wechat_chatroom_member m + LEFT JOIN s2_wechat_chatroom g ON g.chatroomId = m.chatroomId + LEFT JOIN s2_company_account c ON g.accountId = c.id + ORDER BY m.id DESC + LIMIT ?, ? + ON DUPLICATE KEY UPDATE + identifier=VALUES(identifier), + chatroomId=VALUES(chatroomId), + companyId=VALUES(companyId), + groupId=VALUES(groupId)"; + + $offset = 0; + $limit = 2000; + $usleepTime = 50000; + do { + $affected = Db::execute($sql, [$offset, $limit]); + $offset += $limit; + if ($affected > 0) { + usleep($usleepTime); + } + } while ($affected > 0); + } + + + public function syncCallRecording() + { + $sql = "insert into ck_call_recording(`id`,`phone`,`isCallOut`,`companyId`,`callType`,`beginTime`,`endTime`,`createTime`) + SELECT + c.id id, + c.phone phone, + c.isCallOut isCallOut, + a.departmentId companyId, + c.callType callType, + c.beginTime beginTime, + c.endTime endTime, + c.callBeginTime createTime + FROM + s2_call_recording c + LEFT JOIN s2_company_account a ON c.deviceOwnerId = a.id + ORDER BY c.id DESC + LIMIT ?, ? + ON DUPLICATE KEY UPDATE + id=VALUES(id), + phone=VALUES(phone), + isCallOut=VALUES(isCallOut), + companyId=VALUES(companyId)"; + + $offset = 0; + $limit = 2000; + $usleepTime = 50000; + do { + $affected = Db::execute($sql, [$offset, $limit]); + $offset += $limit; + if ($affected > 0) { + usleep($usleepTime); + } + } while ($affected > 0); + } + + /** + * 处理自动问候功能 + * 根据不同的触发类型检查并发送问候消息 + */ + public function handleAutoGreetings() + { + try { + // 获取所有启用的问候规则 + $rules = Db::name('kf_auto_greetings') + ->where(['status' => 1, 'isDel' => 0]) + ->order('level asc, id asc') + ->select(); + + if (empty($rules)) { + return; + } + + foreach ($rules as $rule) { + $trigger = $rule['trigger']; + $condition = json_decode($rule['condition'], true); + + switch ($trigger) { + case 1: // 新好友 + $this->handleNewFriendGreeting($rule); + break; + case 2: // 首次发消息 + $this->handleFirstMessageGreeting($rule); + break; + case 3: // 时间触发 + $this->handleTimeTriggerGreeting($rule, $condition); + break; + case 4: // 关键词触发 + $this->handleKeywordTriggerGreeting($rule, $condition); + break; + case 5: // 生日触发 + $this->handleBirthdayTriggerGreeting($rule, $condition); + break; + case 6: // 自定义 + $this->handleCustomTriggerGreeting($rule, $condition); + break; + } + } + } catch (\Exception $e) { + Log::error('自动问候处理失败:' . $e->getMessage()); + } + } + + /** + * 处理新好友触发 + */ + private function handleNewFriendGreeting($rule) + { + // 获取最近24小时内添加的好友(避免重复处理) + $last24h = time() - 24 * 3600; + + // 查询该用户/公司最近24小时内新添加的好友 + // 通过 s2_wechat_account -> s2_company_account 关联获取 companyId + $friends = Db::table('s2_wechat_friend') + ->alias('wf') + ->join(['s2_wechat_account' => 'wa'], 'wf.wechatAccountId = wa.id') + ->join(['s2_company_account' => 'ca'], 'wa.deviceAccountId = ca.id') + ->where([ + ['wf.isPassed', '=', 1], + ['wf.isDeleted', '=', 0], + ['wf.passTime', '>=', $last24h], + ['ca.departmentId', '=', $rule['companyId']], + ]) + ->field('wf.id, wf.wechatAccountId') + ->select(); + + foreach ($friends as $friend) { + // 检查是否已经发送过问候 + $exists = Db::name('kf_auto_greetings_record') + ->where([ + 'autoId' => $rule['id'], + 'friendIdOrGroupId' => $friend['id'], + 'wechatAccountId' => $friend['wechatAccountId'], + ]) + ->find(); + + if (!$exists) { + $this->sendGreetingMessage($rule, $friend['wechatAccountId'], $friend['id'], 0); + } + } + } + + /** + * 处理首次发消息触发 + */ + private function handleFirstMessageGreeting($rule) + { + // 获取最近1小时内收到的消息 + $last1h = time() - 3600; + + // 查询消息表,找出首次发消息的好友 + // 通过 s2_wechat_account -> s2_company_account 关联获取 companyId + $messages = Db::table('s2_wechat_message') + ->alias('wm') + ->join(['s2_wechat_account' => 'wa'], 'wm.wechatAccountId = wa.id') + ->join(['s2_company_account' => 'ca'], 'wa.deviceAccountId = ca.id') + ->where([ + ['wm.isSend', '=', 0], // 接收的消息 + ['wm.wechatChatroomId', '=', 0], // 个人消息 + ['wm.createTime', '>=', $last1h], + ['ca.departmentId', '=', $rule['companyId']], + ]) + ->group('wm.wechatFriendId, wm.wechatAccountId') + ->field('wm.wechatFriendId, wm.wechatAccountId, MIN(wm.createTime) as firstMsgTime') + ->select(); + + foreach ($messages as $msg) { + // 检查该好友是否之前发送过消息 + $previousMsg = Db::table('s2_wechat_message') + ->where([ + 'wechatFriendId' => $msg['wechatFriendId'], + 'wechatAccountId' => $msg['wechatAccountId'], + 'isSend' => 0, + ]) + ->where('createTime', '<', $msg['firstMsgTime']) + ->find(); + + // 如果是首次发消息,且没有发送过问候 + if (!$previousMsg) { + $exists = Db::name('kf_auto_greetings_record') + ->where([ + 'autoId' => $rule['id'], + 'friendIdOrGroupId' => $msg['wechatFriendId'], + 'wechatAccountId' => $msg['wechatAccountId'], + ]) + ->find(); + + if (!$exists) { + $this->sendGreetingMessage($rule, $msg['wechatAccountId'], $msg['wechatFriendId'], 0); + } + } + } + } + + /** + * 处理时间触发 + */ + private function handleTimeTriggerGreeting($rule, $condition) + { + if (empty($condition) || !isset($condition['type'])) { + return; + } + + $now = time(); + $currentTime = date('H:i', $now); + $currentDate = date('m-d', $now); + $currentDateTime = date('m-d H:i', $now); + $currentWeekday = date('w', $now); // 0=周日, 1=周一, ..., 6=周六 + + $shouldTrigger = false; + + switch ($condition['type']) { + case 'daily_time': // 每天固定时间 + if ($currentTime === $condition['value']) { + $shouldTrigger = true; + } + break; + + case 'yearly_datetime': // 每年固定日期时间 + if ($currentDateTime === $condition['value']) { + $shouldTrigger = true; + } + break; + + case 'fixed_range': // 固定时间段 + if (is_array($condition['value']) && count($condition['value']) === 2) { + $startTime = strtotime('2000-01-01 ' . $condition['value'][0]); + $endTime = strtotime('2000-01-01 ' . $condition['value'][1]); + $currentTimeStamp = strtotime('2000-01-01 ' . $currentTime); + + if ($currentTimeStamp >= $startTime && $currentTimeStamp <= $endTime) { + $shouldTrigger = true; + } + } + break; + + case 'workday': // 工作日 + // 周一到周五(1-5) + if ($currentWeekday >= 1 && $currentWeekday <= 5 && $currentTime === $condition['value']) { + $shouldTrigger = true; + } + break; + } + + if ($shouldTrigger) { + // 获取该用户/公司的所有好友 + // 通过 s2_wechat_account -> s2_company_account 关联获取 companyId + $friends = Db::table('s2_wechat_friend') + ->alias('wf') + ->join(['s2_wechat_account' => 'wa'], 'wf.wechatAccountId = wa.id') + ->join(['s2_company_account' => 'ca'], 'wa.deviceAccountId = ca.id') + ->where([ + ['wf.isPassed', '=', 1], + ['wf.isDeleted', '=', 0], + ['ca.departmentId', '=', $rule['companyId']], + ]) + ->field('wf.id, wf.wechatAccountId') + ->select(); + + foreach ($friends as $friend) { + // 检查今天是否已经发送过 + $todayStart = strtotime(date('Y-m-d 00:00:00')); + $exists = Db::name('kf_auto_greetings_record') + ->where([ + 'autoId' => $rule['id'], + 'friendIdOrGroupId' => $friend['id'], + 'wechatAccountId' => $friend['wechatAccountId'], + ]) + ->where('createTime', '>=', $todayStart) + ->find(); + + if (!$exists) { + $this->sendGreetingMessage($rule, $friend['wechatAccountId'], $friend['id'], 0); + } + } + } + } + + /** + * 处理关键词触发 + */ + private function handleKeywordTriggerGreeting($rule, $condition) + { + if (empty($condition) || empty($condition['keywords'])) { + return; + } + + $keywords = $condition['keywords']; + $matchType = $condition['match_type'] ?? 'fuzzy'; + + // 获取最近1小时内收到的消息 + $last1h = time() - 3600; + + // 通过 s2_wechat_account -> s2_company_account 关联获取 companyId + $messages = Db::table('s2_wechat_message') + ->alias('wm') + ->join(['s2_wechat_account' => 'wa'], 'wm.wechatAccountId = wa.id') + ->join(['s2_company_account' => 'ca'], 'wa.deviceAccountId = ca.id') + ->where([ + ['wm.isSend', '=', 0], // 接收的消息 + ['wm.wechatChatroomId', '=', 0], // 个人消息 + ['wm.msgType', '=', 1], // 文本消息 + ['wm.createTime', '>=', $last1h], + ['ca.departmentId', '=', $rule['companyId']], + ]) + ->field('wm.*') + ->select(); + + foreach ($messages as $msg) { + $content = $msg['content'] ?? ''; + + // 检查关键词匹配 + $matched = false; + foreach ($keywords as $keyword) { + if ($matchType === 'exact') { + // 精准匹配 + if ($content === $keyword) { + $matched = true; + break; + } + } else { + // 模糊匹配 + if (strpos($content, $keyword) !== false) { + $matched = true; + break; + } + } + } + + if ($matched) { + // 检查是否已经发送过问候(同一好友同一规则,1小时内只发送一次) + $last1h = time() - 3600; + $exists = Db::name('kf_auto_greetings_record') + ->where([ + 'autoId' => $rule['id'], + 'friendIdOrGroupId' => $msg['wechatFriendId'], + 'wechatAccountId' => $msg['wechatAccountId'], + ]) + ->where('createTime', '>=', $last1h) + ->find(); + + if (!$exists) { + $this->sendGreetingMessage($rule, $msg['wechatAccountId'], $msg['wechatFriendId'], 0); + } + } + } + } + + /** + * 处理生日触发 + */ + private function handleBirthdayTriggerGreeting($rule, $condition) + { + if (empty($condition)) { + return; + } + + // 解析condition格式 + // 支持格式: + // 1. {'month': 10, 'day': 10} - 当天任何时间都可以触发 + // 2. {'month': 10, 'day': 10, 'time': '09:00'} - 当天指定时间触发 + // 3. {'month': 10, 'day': 10, 'time_range': ['09:00', '10:00']} - 当天时间范围内触发 + // 兼容旧格式:['10-10'] 或 '10-10'(仅支持 MM-DD 格式,不包含年份) + + $birthdayMonth = null; + $birthdayDay = null; + $birthdayTime = null; + $timeRange = null; + + if (is_array($condition)) { + // 新格式:对象格式 {'month': 10, 'day': 10} + if (isset($condition['month']) && isset($condition['day'])) { + $birthdayMonth = (int)$condition['month']; + $birthdayDay = (int)$condition['day']; + $birthdayTime = $condition['time'] ?? null; + $timeRange = $condition['time_range'] ?? null; + } + // 兼容旧格式:['10-10'] 或 ['10-10 09:00'](仅支持 MM-DD 格式) + elseif (isset($condition[0])) { + $dateStr = $condition[0]; + // 只接受月日格式:'10-10' 或 '10-10 09:00' + if (preg_match('/^(\d{1,2})-(\d{1,2})(?:\s+(\d{2}:\d{2}))?$/', $dateStr, $matches)) { + $birthdayMonth = (int)$matches[1]; + $birthdayDay = (int)$matches[2]; + if (isset($matches[3])) { + $birthdayTime = $matches[3]; + } + } + } + } elseif (is_string($condition)) { + // 字符串格式:只接受 '10-10' 或 '10-10 09:00'(MM-DD 格式,不包含年份) + if (preg_match('/^(\d{1,2})-(\d{1,2})(?:\s+(\d{2}:\d{2}))?$/', $condition, $matches)) { + $birthdayMonth = (int)$matches[1]; + $birthdayDay = (int)$matches[2]; + if (isset($matches[3])) { + $birthdayTime = $matches[3]; + } + } + } + + if ($birthdayMonth === null || $birthdayDay === null || $birthdayMonth < 1 || $birthdayMonth > 12 || $birthdayDay < 1 || $birthdayDay > 31) { + return; + } + + $todayMonth = (int)date('m'); + $todayDay = (int)date('d'); + + // 检查今天是否是生日(只匹配月日,不匹配年份) + if ($todayMonth !== $birthdayMonth || $todayDay !== $birthdayDay) { + return; + } + + // 如果配置了时间,检查当前时间是否匹配 + $now = time(); + $currentTime = date('H:i', $now); + + if ($birthdayTime !== null) { + // 指定了具体时间,检查是否在指定时间(允许1分钟误差,避免定时任务执行时间不精确) + $birthdayTimestamp = strtotime('2000-01-01 ' . $birthdayTime); + $currentTimestamp = strtotime('2000-01-01 ' . $currentTime); + $diff = abs($currentTimestamp - $birthdayTimestamp); + + // 如果时间差超过2分钟,不触发(允许1分钟误差) + if ($diff > 120) { + return; + } + } elseif ($timeRange !== null && is_array($timeRange) && count($timeRange) === 2) { + // 指定了时间范围,检查当前时间是否在范围内 + $startTime = strtotime('2000-01-01 ' . $timeRange[0]); + $endTime = strtotime('2000-01-01 ' . $timeRange[1]); + $currentTimestamp = strtotime('2000-01-01 ' . $currentTime); + + if ($currentTimestamp < $startTime || $currentTimestamp > $endTime) { + return; + } + } + // 如果没有配置时间或时间范围,则当天任何时间都可以触发 + + // 获取该用户/公司的所有好友 + // 通过 s2_wechat_account -> s2_company_account 关联获取 companyId + $friends = Db::table('s2_wechat_friend') + ->alias('wf') + ->join(['s2_wechat_account' => 'wa'], 'wf.wechatAccountId = wa.id') + ->join(['s2_company_account' => 'ca'], 'wa.deviceAccountId = ca.id') + ->where([ + ['wf.isPassed', '=', 1], + ['wf.isDeleted', '=', 0], + ['ca.departmentId', '=', $rule['companyId']], + ]) + ->field('wf.id, wf.wechatAccountId') + ->select(); + + foreach ($friends as $friend) { + // 检查今天是否已经发送过 + $todayStart = strtotime(date('Y-m-d 00:00:00')); + $exists = Db::name('kf_auto_greetings_record') + ->where([ + 'autoId' => $rule['id'], + 'friendIdOrGroupId' => $friend['id'], + 'wechatAccountId' => $friend['wechatAccountId'], + ]) + ->where('createTime', '>=', $todayStart) + ->find(); + + if (!$exists) { + $this->sendGreetingMessage($rule, $friend['wechatAccountId'], $friend['id'], 0); + } + } + } + + /** + * 处理自定义触发 + */ + private function handleCustomTriggerGreeting($rule, $condition) + { + // 自定义类型需要根据具体业务需求实现 + // 这里提供一个基础框架,可根据实际需求扩展 + // 暂时不实现,留待后续扩展 + } + + /** + * 发送问候消息 + * @param array $rule 问候规则 + * @param int $wechatAccountId 微信账号ID + * @param int $friendId 好友ID + * @param int $groupId 群ID(0表示个人消息) + */ + private function sendGreetingMessage($rule, $wechatAccountId, $friendId, $groupId = 0) + { + try { + $content = $rule['content']; + + // 创建记录 + $recordId = Db::name('kf_auto_greetings_record')->insertGetId([ + 'autoId' => $rule['id'], + 'userId' => $rule['userId'], + 'companyId' => $rule['companyId'], + 'wechatAccountId' => $wechatAccountId, + 'friendIdOrGroupId' => $friendId, + 'isSend' => 0, + 'sendTime' => 0, + 'receiveTime' => 0, + 'createTime' => time(), + ]); + + // 发送消息(文本消息) + $username = Env::get('api.username', ''); + $password = Env::get('api.password', ''); + $toAccountId = ''; + if (!empty($username) || !empty($password)) { + $toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId'); + } + + $wsController = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]); + + $sendTime = time(); + $result = $wsController->sendPersonal([ + 'wechatFriendId' => $friendId, + 'wechatAccountId' => $wechatAccountId, + 'msgType' => 1, // 文本消息 + 'content' => $content, + ]); + + $isSend = 0; + $receiveTime = 0; + + // 解析返回结果 + $resultData = json_decode($result, true); + if (!empty($resultData) && $resultData['code'] == 200) { + $isSend = 1; + $receiveTime = time(); // 简化处理,实际应该从返回结果中获取 + } + + // 更新记录 + Db::name('kf_auto_greetings_record') + ->where('id', $recordId) + ->update([ + 'isSend' => $isSend, + 'sendTime' => $sendTime, + 'receiveTime' => $receiveTime, + ]); + + // 更新规则使用次数 + Db::name('kf_auto_greetings') + ->where('id', $rule['id']) + ->setInc('usageCount'); + + } catch (\Exception $e) { + Log::error('发送问候消息失败:' . $e->getMessage() . ',规则ID:' . $rule['id']); + } + } + +} diff --git a/extend/WeChatDeviceApi/Contracts/WeChatServiceInterface.php b/extend/WeChatDeviceApi/Contracts/WeChatServiceInterface.php new file mode 100644 index 0000000..182a18b --- /dev/null +++ b/extend/WeChatDeviceApi/Contracts/WeChatServiceInterface.php @@ -0,0 +1,67 @@ +config = $config ?: Config::get('wechat_device_api.'); + if (empty($this->config)) { + throw new \InvalidArgumentException("WeChat Device API configuration not found."); + } + } + + /** + * 获取指定的适配器实例 + * + * @param string|null $name 适配器名称 (例如 'vendor_a', 'vendor_b'),null 则使用默认 + * @return WeChatServiceInterface + * @throws \InvalidArgumentException + */ + public function adapter(string $name = null): WeChatServiceInterface + { + $name = $name ?: $this->getDefaultAdapterName(); + + if (!isset($this->config['adapters'][$name])) { + throw new \InvalidArgumentException("Adapter [{$name}] configuration not found."); + } + + if (!isset($this->adapters[$name])) { + $this->adapters[$name] = $this->createAdapter($name); + } + + return $this->adapters[$name]; + } + + /** + * 创建适配器实例 + * + * @param string $name + * @return WeChatServiceInterface + */ + protected function createAdapter(string $name): WeChatServiceInterface + { + $adapterConfig = $this->config['adapters'][$name]; + $driverClass = $adapterConfig['driver'] ?? null; + + if (!$driverClass || !class_exists($driverClass)) { + throw new \InvalidArgumentException("Driver class for adapter [{$name}] not found or not specified."); + } + + $adapterInstance = new $driverClass($adapterConfig); + + if (!$adapterInstance instanceof WeChatServiceInterface) { + throw new \LogicException("Driver class [{$driverClass}] must implement WeChatServiceInterface."); + } + + return $adapterInstance; + } + + /** + * 获取默认适配器名称 + * + * @return string + */ + public function getDefaultAdapterName(): string + { + return $this->config['default_adapter'] ?? ''; + } + + /** + * 动态调用默认适配器的方法 + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call(string $method, array $parameters) + { + return $this->adapter()->{$method}(...$parameters); + } +} \ No newline at end of file diff --git a/extend/library/ClassTable.php b/extend/library/ClassTable.php new file mode 100644 index 0000000..939eeb4 --- /dev/null +++ b/extend/library/ClassTable.php @@ -0,0 +1,243 @@ +{$tag} = (array)($this->{$tag} ?? []); + + // 将值追加到数组中。 + $this->{$tag} = array_merge($this->{$tag}, $collection); + } + + /** + * 返回标签的元素 + * + * @param string $tag + * @return array|null + */ + private function getTagElements(string $tag): ?array + { + return $this->{'__tag__' . $tag} ?? null; + } + + /** + * 创建并返回一个对象 + * + * @param object|string $class + * @param array $parameters + * @return object + * @throws \Throwable + */ + private function _getInstance($class, $parameters = []): object + { + if ($class instanceof \Closure) { + return $class(); + } + + if (is_object($class)) { + return $class; + } + + if (class_exists(strval($class))) { + // 创建该类的新实例 + return method_exists($class, 'newInstance') ? $class::newInstance(...$parameters) : new $class(...$parameters); + } + + throw new \Exception(static::class . ': 调用了未定义的函数', 532341); + } + + /** + * 单例 + * + * @param $container + * @return void + */ + public static function getSelfInstance(): CallMapInterface + { + if (!self::$selfInstance instanceof self) { + self::$selfInstance = new self(); + } + + return self::$selfInstance; + } + + /** + * @inheritDoc + */ + public function bind($alias, $instance = null, string $tag = null): void + { + if (is_array($alias)) { + foreach ($alias as $name => $instance) { + $this->hashTable[$name] = $instance; + } + + $this->latest = array_keys($alias); + + if ($tag) { + $this->tag($tag, $this->latest); + } + } else { + $this->hashTable[$alias] = $instance; + $this->latest = [$alias]; + + if ($tag) { + $this->tag($tag, $this->latest); + } + } + } + + /** + * @inheritDoc + */ + public function dealloc($alias): void + { + dealloc($this->hashTable, $alias); + + $insects = array_intersect((array)$alias, $this->latest); + + if ($insects) { + $this->latest = array_diff($this->latest, $insects); + } + } + + /** + * @inheritDoc + */ + public function revokeShared($alias): void + { + $instance = $this->alias($alias); + + $this->bind($alias, get_class($instance)); + } + + /** + * @inheritDoc + */ + public function tag(string $tag, array $lastElements = []): CallMapInterface + { + $this->_tag('__tag__' . $tag, ($lastElements ?: $this->latest)); + + return $this; + } + + /** + * @inheritDoc + */ + public function getInstance($class, ...$parameters): object + { + $name = is_object($class) ? get_class($class) : $class; + + if (!$this->has($name)) { + return $this->hashTable[$name] = $this->_getInstance($class, $parameters); + } + + return $this->_getInstance($this->hashTable[$name], $parameters); + } + + /** + * @inheritDoc + */ + public function copy($class, string $name = null): object + { + $instance = clone $this->getInstance($class); + + if (!is_null($name)) { + $this->bind($name, $instance); + } + + return $instance; + } + + /** + * @inheritDoc + */ + public function getClassByTag(string $tag): ?array + { + $elements = $this->getTagElements($tag); + + if ($elements) { + foreach ($elements as $alias) { + if ($this->has($alias)) { + $consequences[$alias] = $this->hashTable[$alias]; + } + } + } + + return $consequences ?? null; + } + + /** + * @inheritDoc + */ + public function has(string $alias): bool + { + return isset($this->hashTable[$alias]); + } + + /** + * @inheritDoc + */ + public function detect(string $precept, $class, string $alias = null): ?object + { + // Scheme 是临时对象,不会被保存 + if (!$this->has($precept)) { + // 如果想共享该对象,可以调用 bind 方法将其注册到哈希表中 + $instance = $this->_getInstance($class); + + if ($alias) { + $this->hashTable[$alias] = $instance; + } + + return $instance; + } + + return $this->hashTable[$precept]; + } + + /** + * @inheritDoc + */ + public function alias($alias, array $parameters = []): ?object + { + return $this->getInstance($alias, ...$parameters); + } + + /** + * @inheritDoc + */ + public function getShared($alias, array $parameters = []): ?object + { + $instance = $this->alias($alias, $parameters); + + $this->bind($alias, $instance); + + return $instance; + } +} diff --git a/extend/library/Interfaces/CallMap.php b/extend/library/Interfaces/CallMap.php new file mode 100644 index 0000000..f58d5bc --- /dev/null +++ b/extend/library/Interfaces/CallMap.php @@ -0,0 +1,106 @@ + $code, + 'msg' => $msg, + 'data' => $data + ]); + } + + /** + * 错误响应 + * + * @param string $msg + * @param int $code + * @param mixed $data + * @return JsonResponse + */ + public static function error(string $msg = 'fail', int $code = 400, $data = []): JsonResponse + { + return json([ + 'code' => $code, + 'msg' => $msg, + 'data' => $data + ]); + } + + /** + * 未授权响应 + * + * @param string $msg 错误消息 + * @return JsonResponse + */ + public static function unauthorized(string $msg = 'unauthorized access'): JsonResponse + { + return static::error($msg, 401); + } + + /** + * 禁止访问响应 + * + * @param string $msg 错误消息 + * @return JsonResponse + */ + public static function forbidden(string $msg = 'access denied'): JsonResponse + { + return static::error($msg, 403); + } +} \ No newline at end of file diff --git a/extend/library/s2/CurlHandle.php b/extend/library/s2/CurlHandle.php new file mode 100644 index 0000000..0d10d51 --- /dev/null +++ b/extend/library/s2/CurlHandle.php @@ -0,0 +1,156 @@ +baseUrl = Env::get('api.wechat_url'); + } + + /** + * CurlHandle constructor. + */ + public function __construct() + { + $this->getBaseUrl(); + } + + /** + * 设置头部 + * + * @param array $headerData 头部数组 + * @param string $authorization + * @param string $type 类型 默认json (json,plain) + * @return array + */ + public function setHeader($key, $value): CurlHandle + { + if (is_array($key)) { + $this->header = array_merge($this->header, $key); + } else { + $this->header[$key] = $value; + } + + return $this; + } + + private function getHearder(): array + { + $header = []; + + foreach ($this->header as $key => $value) { + $header[] = $key . ':' . $value; + } + + return $header; + } + + public function setMethod(string $method): CurlHandle + { + $this->method = $method; + + return $this; + } + + /** + * @param string $baseUrl + * @return $this + */ + public function setBaseUrl(string $baseUrl): CurlHandle + { + $this->baseUrl = $baseUrl; + + return $this; + } + + /** + * @param string $url 请求的链接 + * @param array $params 请求附带的参数 + * @param string $method 请求的方式, 支持GET, POST, PUT, DELETE等 + * @param array $header 头部 + * @param string $type 数据类型,支持dataBuild、json等 + * @return bool|string + */ + public function send($url, $params = [], $type = 'dataBuild') + { + $str = ''; + if (!empty($url)) { + try { + $ch = curl_init(); + $method = $this->method; + $url = $this->baseUrl . $url; + + // 处理GET请求的参数 + if (strtoupper($method) == 'GET' && !empty($params)) { + $url = $url . '?' . dataBuild($params); + } + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); //30秒超时 + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($ch, CURLOPT_HTTPHEADER, $this->getHearder()); + + // 处理不同的请求方法 + if (strtoupper($method) != 'GET') { + // 设置请求方法 + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($method)); + + // 处理参数格式 + if ($type == 'dataBuild') { + $params = dataBuild($params); + } elseif ($type == 'json') { + $params = json_encode($params); + } else { + $params = dataBuild($params); + } + + // 设置请求体 + curl_setopt($ch, CURLOPT_POSTFIELDS, $params); + } + + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); //是否验证对等证书,1则验证,0则不验证 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); + $str = curl_exec($ch); + if(curl_errno($ch)) { + echo 'Curl error: ' .curl_errno($ch) . ':' . curl_error($ch); + } + curl_close($ch); + } catch (Exception $e) { + $str = ''; + } + } + + return $str; + } + + /** + * 单例 + * + * @return static|null + */ + public static function getInstant() + { + if (self::$instant instanceof self) { + return self::$instant; + } + + return new static(); + } +} diff --git a/extend/library/s2/interfaces/AccountInterface.php b/extend/library/s2/interfaces/AccountInterface.php new file mode 100644 index 0000000..7fe2807 --- /dev/null +++ b/extend/library/s2/interfaces/AccountInterface.php @@ -0,0 +1,23 @@ + $params['accountId'] ?? '', + 'keyword' => $params['keyword'] ?? '', + 'imei' => $params['imei'] ?? '', + 'groupId' => $params['groupId'] ?? '', + 'brand' => $params['brand'] ?? '', + 'model' => $params['model'] ?? '', + 'deleteType' => $params['deleteType'] ?? 'unDeleted', + 'operatingSystem' => $params['operatingSystem'] ?? '', + 'softwareVersion' => $params['softwareVersion'] ?? '', + 'phoneAppVersion' => $params['phoneAppVersion'] ?? '', + 'recorderVersion' => $params['recorderVersion'] ?? '', + 'contactsVersion' => $params['contactsVersion'] ?? '', + 'rooted' => $params['rooted'] ?? '', + 'xPosed' => $params['xPosed'] ?? '', + 'alive' => $params['alive'] ?? '', + 'hasWechat' => $params['hasWechat'] ?? '', + 'departmentId' => $params['departmentId'] ?? '', + 'pageIndex' => $params['pageIndex'] ?? 0, + 'pageSize' => $params['pageSize'] ?? 20 + ]; + + $JWT = AuthService::getSystemAuthorization(); + $result = CurlHandle::getInstant() + ->setHeader('Content-Type', 'text/plain') + ->setHeader('authorization', 'bearer ' . $JWT) + ->setMethod('get') + ->send('api/Account/myTenantPageAccounts', $params); + + $response = handleApiResponse($result); + // 保存数据到数据库 + if (!empty($response['results'])) { + foreach ($response['results'] as $item) { + $this->saveData($item); + } + } + return json_encode(['code' => 200, 'msg' => '获取公司账号列表成功', 'data' => $response]); + + } catch (\Exception $e) { + return json_encode(['code' => 500, 'msg' => '获取公司账号列表失败:' . $e->getMessage()]); + } + } + + + private function saveData($item) + { + $data = [ + 'id' => isset($item['id']) ? $item['id'] : '', + 'userName' => isset($item['userName']) ? $item['userName'] : '', + 'nickname' => isset($item['nickname']) ? $item['nickname'] : '', + 'realName' => isset($item['realName']) ? $item['realName'] : '', + 'groupName' => isset($item['groupName']) ? $item['groupName'] : '', + 'wechatAccounts' => isset($item['wechatAccounts']) ? json_encode($item['wechatAccounts']) : json_encode([]), + 'alive' => isset($item['alive']) ? $item['alive'] : false, + 'lastAliveTime' => isset($item['lastAliveTime']) ? $item['lastAliveTime'] : null, + 'tenantId' => isset($item['tenantId']) ? $item['tenantId'] : 0, + 'groupId' => isset($item['groupId']) ? $item['groupId'] : 0, + 'currentAccountId' => isset($item['currentAccountId']) ? $item['currentAccountId'] : 0, + 'imei' => $item['imei'], + 'memo' => isset($item['memo']) ? $item['memo'] : '', + 'createTime' => isset($item['createTime']) ? strtotime($item['createTime']) : 0, + 'isDeleted' => isset($item['isDeleted']) ? $item['isDeleted'] : false, + 'deletedAndStop' => isset($item['deletedAndStop']) ? $item['deletedAndStop'] : false, + 'deleteTime' => empty($item['isDeleted']) ? 0 : strtotime($item['deleteTime']), + 'rooted' => isset($item['rooted']) ? $item['rooted'] : false, + 'xPosed' => isset($item['xPosed']) ? $item['xPosed'] : false, + 'brand' => isset($item['brand']) ? $item['brand'] : '', + 'model' => isset($item['model']) ? $item['model'] : '', + 'operatingSystem' => isset($item['operatingSystem']) ? $item['operatingSystem'] : '', + 'softwareVersion' => isset($item['softwareVersion']) ? $item['softwareVersion'] : '', + 'extra' => isset($item['extra']) ? json_encode($item['extra']) : json_encode([]), + 'phone' => isset($item['phone']) ? $item['phone'] : '', + 'lastUpdateTime' => isset($item['lastUpdateTime']) ? ($item['lastUpdateTime'] == '0001-01-01T00:00:00' ? 0 : strtotime($item['lastUpdateTime'])) : 0 + ]; + + // 使用imei作为唯一性判断 + $device = DeviceModel::where('id', $item['id'])->find(); + + if ($device) { + $device->save($data); + } else { + + // autoLike:自动点赞 + // momentsSync:朋友圈同步 + // autoCustomerDev:自动开发客户 + // groupMessageDeliver:群消息推送 + // autoGroup:自动建群 + + $data['taskConfig'] = json_encode([ + 'autoLike' => true, + 'momentsSync' => true, + 'autoCustomerDev' => true, + 'groupMessageDeliver' => true, + 'autoGroup' => true, + ]); + DeviceModel::create($data); + } + } + + /** + * 保存设备数据 + * @param array $item 设备数据 + * @return bool + */ + public function saveDevice($item) + { + try { + $data = [ + 'id' => isset($item['id']) ? $item['id'] : '', + 'userName' => isset($item['userName']) ? $item['userName'] : '', + 'nickname' => isset($item['nickname']) ? $item['nickname'] : '', + 'realName' => isset($item['realName']) ? $item['realName'] : '', + 'groupName' => isset($item['groupName']) ? $item['groupName'] : '', + 'wechatAccounts' => isset($item['wechatAccounts']) ? json_encode($item['wechatAccounts']) : json_encode([]), + 'alive' => isset($item['alive']) ? $item['alive'] : false, + 'lastAliveTime' => isset($item['lastAliveTime']) ? $item['lastAliveTime'] : null, + 'tenantId' => isset($item['tenantId']) ? $item['tenantId'] : 0, + 'groupId' => isset($item['groupId']) ? $item['groupId'] : 0, + 'currentAccountId' => isset($item['currentAccountId']) ? $item['currentAccountId'] : 0, + 'imei' => $item['imei'], + 'memo' => isset($item['memo']) ? $item['memo'] : '', + 'createTime' => isset($item['createTime']) ? strtotime($item['createTime']) : 0, + 'isDeleted' => isset($item['isDeleted']) ? $item['isDeleted'] : false, + 'deletedAndStop' => isset($item['deletedAndStop']) ? $item['deletedAndStop'] : false, + 'deleteTime' => empty($item['isDeleted']) ? 0 : strtotime($item['deleteTime']), + 'rooted' => isset($item['rooted']) ? $item['rooted'] : false, + 'xPosed' => isset($item['xPosed']) ? $item['xPosed'] : false, + 'brand' => isset($item['brand']) ? $item['brand'] : '', + 'model' => isset($item['model']) ? $item['model'] : '', + 'operatingSystem' => isset($item['operatingSystem']) ? $item['operatingSystem'] : '', + 'softwareVersion' => isset($item['softwareVersion']) ? $item['softwareVersion'] : '', + 'extra' => isset($item['extra']) ? json_encode($item['extra']) : json_encode([]), + 'phone' => isset($item['phone']) ? $item['phone'] : '', + 'lastUpdateTime' => isset($item['lastUpdateTime']) ? ($item['lastUpdateTime'] == '0001-01-01T00:00:00' ? 0 : strtotime($item['lastUpdateTime'])) : 0 + ]; + + // 使用ID作为唯一性判断 + $device = DeviceModel::where('id', $item['id'])->find(); + + if ($device) { + $device->save($data); + } else { + $data['taskConfig'] = json_encode([ + 'autoLike' => true, + 'momentsSync' => true, + 'autoCustomerDev' => true, + 'groupMessageDeliver' => true, + 'autoGroup' => true, + ]); + DeviceModel::create($data); + } + + return true; + } catch (\Exception $e) { + Log::error('保存设备数据失败:' . $e->getMessage()); + return false; + } + } + + /** + * 保存设备分组数据 + * @param array $item 设备分组数据 + * @return bool + */ + public function saveDeviceGroup($item) + { + try { + $data = [ + 'id' => $item['id'], + 'tenantId' => $item['tenantId'], + 'groupName' => $item['groupName'], + 'groupMemo' => $item['groupMemo'], + 'count' => isset($item['count']) ? $item['count'] : 0, + 'createTime' => $item['createTime'] == '0001-01-01T00:00:00' ? 0 : strtotime($item['createTime']) + ]; + + // 使用ID作为唯一性判断 + $group = DeviceGroupModel::where('id', $item['id'])->find(); + + if ($group) { + $group->save($data); + } else { + DeviceGroupModel::create($data); + } + + return true; + } catch (\Exception $e) { + Log::error('保存设备分组数据失败:' . $e->getMessage()); + return false; + } + } +} \ No newline at end of file diff --git a/fonts.ttf b/fonts.ttf new file mode 100644 index 0000000..807b78f Binary files /dev/null and b/fonts.ttf differ diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..e69de29 diff --git a/public/admin/.htaccess b/public/admin/.htaccess new file mode 100644 index 0000000..9c856c3 --- /dev/null +++ b/public/admin/.htaccess @@ -0,0 +1,8 @@ + + Options +FollowSymlinks -Multiviews + RewriteEngine On + + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^admin/(.*)$ /admin/index.html/$1 [QSA,PT,L] + diff --git a/public/api_test.php b/public/api_test.php new file mode 100644 index 0000000..9f5ccb7 --- /dev/null +++ b/public/api_test.php @@ -0,0 +1,76 @@ +API测试工具'; +echo '
'; +echo '

API路径:

'; +echo '

请求方法:

'; +echo '

请求数据 (JSON):

'; +echo '

Authorization Token:

'; +echo '

'; +echo '
'; + +// 如果有URL参数,发送API请求 +if (!empty($url)) { + // 构建完整URL + $fullUrl = 'http://' . $_SERVER['HTTP_HOST'] . '/' . $url; + + // 初始化cURL + $ch = curl_init($fullUrl); + + // 设置cURL选项 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + // 设置请求方法 + if ($method == 'POST') { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($jsonData)); + } + + // 设置请求头 + $headers = ['Content-Type: application/json']; + if (!empty($token)) { + $headers[] = 'Authorization: ' . $token; + } + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + // 执行请求 + $result = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + // 检查是否有错误 + if (curl_errno($ch)) { + echo '

请求错误

'; + echo '
' . htmlspecialchars(curl_error($ch)) . '
'; + } else { + echo '

响应结果 (HTTP状态码: ' . $httpCode . ')

'; + echo '
' . htmlspecialchars($result) . '
'; + + // 尝试解析JSON + $jsonResult = json_decode($result, true); + if (json_last_error() === JSON_ERROR_NONE) { + echo '

格式化JSON响应

'; + echo '
' . htmlspecialchars(json_encode($jsonResult, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) . '
'; + } + } + + // 关闭cURL资源 + curl_close($ch); +} \ No newline at end of file diff --git a/public/doc/api_v1.md b/public/doc/api_v1.md new file mode 100644 index 0000000..95c8643 --- /dev/null +++ b/public/doc/api_v1.md @@ -0,0 +1,413 @@ +# 对外获客线索上报接口文档(V1) + +## 一、接口概述 + +- **接口名称**:对外获客线索上报接口 +- **接口用途**:供第三方系统向【存客宝】上报客户线索(手机号 / 微信号等),用于后续的跟进、标签管理和画像分析。 +- **接口协议**:HTTP +- **请求方式**:`POST` +- **请求地址**: `http://ckbapi.quwanzhi.com/v1/api/scenarios` + +> 具体 URL 以实际环境配置为准。 + +- **数据格式**: + - 推荐:`application/json` + - 兼容:`application/x-www-form-urlencoded` +- **字符编码**:`UTF-8` + +--- + +## 二、鉴权与签名 + +### 2.1 必填鉴权字段 + +| 字段名 | 类型 | 必填 | 说明 | +|-------------|--------|------|---------------------------------------| +| `apiKey` | string | 是 | 分配给第三方的接口密钥(每个任务唯一)| +| `sign` | string | 是 | 签名值 | +| `timestamp` | int | 是 | 秒级时间戳(与服务器时间差不超过 5 分钟) | + +### 2.2 时间戳校验 + +服务器会校验 `timestamp` 是否在当前时间前后 **5 分钟** 内: + +- 通过条件:`|server_time - timestamp| <= 300` +- 超出范围则返回:`请求已过期` + +### 2.3 签名生成规则 + +接口采用自定义签名机制。**签名字段为 `sign`,生成步骤如下:** + +假设本次请求的所有参数为 `params`,其中包括业务参数 + `apiKey` + `timestamp` + `sign` + 可能存在的 `portrait` 对象。 + +#### 第一步:移除特定字段 + +从 `params` 中移除以下字段: + +- `sign` —— 自身不参与签名 +- `apiKey` —— 不参与参数拼接,仅在最后一步参与二次 MD5 +- `portrait` —— 整个画像对象不参与签名(即使内部还有子字段) + +> 说明:`portrait` 通常是一个 JSON 对象,字段较多,为避免签名实现复杂且双方难以对齐,统一不参与签名。 + +#### 第二步:移除空值字段 + +从剩余参数中,移除值为: + +- `null` +- 空字符串 `''` + +的字段,这些字段不参与签名。 + +#### 第三步:按参数名升序排序 + +对剩余参数按**参数名(键名)升序排序**,排序规则为标准的 ASCII 升序: + +```text +例如: name, phone, source, timestamp +``` + +#### 第四步:拼接参数值 + +将排序后的参数 **只取“值”**,按顺序直接拼接为一个字符串,中间不加任何分隔符: + +- 示例: + 排序后参数为: + + ```text + name = 张三 + phone = 13800000000 + source = 微信广告 + timestamp = 1710000000 + ``` + + 则拼接: + + ```text + stringToSign = "张三13800000000微信广告1710000000" + ``` + +#### 第五步:第一次 MD5 + +对上一步拼接得到的字符串做一次 MD5: + +\[ +\text{firstMd5} = \text{MD5}(\text{stringToSign}) +\] + +#### 第六步:拼接 apiKey 再次 MD5 + +将第一步的结果与 `apiKey` 直接拼接,再做一次 MD5,得到最终签名值: + +\[ +\text{sign} = \text{MD5}(\text{firstMd5} + \text{apiKey}) +\] + +#### 第七步:放入请求 + +将第六步得到的 `sign` 填入请求参数中的 `sign` 字段即可。 + +> 建议: +> - 使用小写 MD5 字符串(双方约定统一即可)。 +> - 请确保参与签名的参数与最终请求发送的参数一致(包括是否传空值)。 + +### 2.4 签名示例(PHP 伪代码) + +```php +$params = [ + 'apiKey' => 'YOUR_API_KEY', + 'timestamp' => '1710000000', + 'phone' => '13800000000', + 'name' => '张三', + 'source' => '微信广告', + 'remark' => '通过H5落地页留资', + // 'portrait' => [...], // 如有画像,这里会存在,但不参与签名 + // 'sign' => '待生成', +]; + +// 1. 去掉 sign、apiKey、portrait +unset($params['sign'], $params['apiKey'], $params['portrait']); + +// 2. 去掉空值 +$params = array_filter($params, function($value) { + return !is_null($value) && $value !== ''; +}); + +// 3. 按键名升序排序 +ksort($params); + +// 4. 拼接参数值 +$stringToSign = implode('', array_values($params)); + +// 5. 第一次 MD5 +$firstMd5 = md5($stringToSign); + +// 6. 第二次 MD5(拼接 apiKey) +$apiKey = 'YOUR_API_KEY'; +$sign = md5($firstMd5 . $apiKey); + +// 将 $sign 作为字段发送 +$params['sign'] = $sign; +``` + +--- + +## 三、请求参数说明 + +### 3.1 主标识字段(至少传一个) + +| 字段名 | 类型 | 必填 | 说明 | +|-----------|--------|------|-------------------------------------------| +| `wechatId`| string | 否 | 微信号,存在时优先作为主标识 | +| `phone` | string | 否 | 手机号,当 `wechatId` 为空时用作主标识 | + +### 3.2 基础信息字段 + +| 字段名 | 类型 | 必填 | 说明 | +|------------|--------|------|-------------------------| +| `name` | string | 否 | 客户姓名 | +| `source` | string | 否 | 线索来源描述,如“百度推广”、“抖音直播间” | +| `remark` | string | 否 | 备注信息 | +| `tags` | string | 否 | 逗号分隔的“微信标签”,如:`"高意向,电商,女装"` | +| `siteTags` | string | 否 | 逗号分隔的“站内标签”,用于站内进一步细分 | + + +### 3.3 用户画像字段 `portrait`(可选) + +`portrait` 为一个对象(JSON),用于记录用户的行为画像数据。 + +#### 3.3.1 基本示例 + +```json +"portrait": { + "type": 1, + "source": 1, + "sourceData": { + "age": 28, + "gender": "female", + "city": "上海", + "productId": "P12345", + "pageUrl": "https://example.com/product/123" + }, + "remark": "画像-基础属性", + "uniqueId": "user_13800000000_20250301_001" +} +``` + +#### 3.3.2 字段详细说明 + +| 字段名 | 类型 | 必填 | 说明 | +|-----------------------|--------|------|----------------------------------------| +| `portrait.type` | int | 否 | 画像类型,枚举值:
0-浏览
1-点击
2-下单/购买
3-注册
4-互动
默认值:0 | +| `portrait.source` | int | 否 | 画像来源,枚举值:
0-本站
1-老油条
2-老坑爹
默认值:0 | +| `portrait.sourceData` | object | 否 | 画像明细数据(键值对,会存储为 JSON 格式)
可包含任意业务相关的键值对,如:年龄、性别、城市、商品ID、页面URL等 | +| `portrait.remark` | string | 否 | 画像备注信息,最大长度100字符 | +| `portrait.uniqueId` | string | 否 | 画像去重用唯一 ID
用于防止重复记录,相同 `uniqueId` 的画像数据在半小时内会被合并统计(count字段累加)
建议格式:`{来源标识}_{用户标识}_{时间戳}_{序号}` | + +#### 3.3.3 画像类型(type)说明 + +| 值 | 类型 | 说明 | 适用场景 | +|---|------|------|---------| +| 0 | 浏览 | 用户浏览了页面或内容 | 页面访问、商品浏览、文章阅读等 | +| 1 | 点击 | 用户点击了某个元素 | 按钮点击、链接点击、广告点击等 | +| 2 | 下单/购买 | 用户完成了购买行为 | 订单提交、支付完成等 | +| 3 | 注册 | 用户完成了注册 | 账号注册、会员注册等 | +| 4 | 互动 | 用户进行了互动行为 | 点赞、评论、分享、咨询等 | + +#### 3.3.4 画像来源(source)说明 + +| 值 | 来源 | 说明 | +|---|------|------| +| 0 | 本站 | 来自本站的数据 | +| 1 | 老油条 | 来自"老油条"系统的数据 | +| 2 | 老坑爹 | 来自"老坑爹"系统的数据 | + +#### 3.3.5 sourceData 数据格式说明 + +`sourceData` 是一个 JSON 对象,可以包含任意业务相关的键值对。常见字段示例: + +```json +{ + "age": 28, + "gender": "female", + "city": "上海", + "province": "上海市", + "productId": "P12345", + "productName": "商品名称", + "category": "女装", + "price": 299.00, + "pageUrl": "https://example.com/product/123", + "referrer": "https://www.baidu.com", + "device": "mobile", + "browser": "WeChat" +} +``` + +> **注意**: +> - `sourceData` 中的数据类型可以是字符串、数字、布尔值等 +> - 嵌套对象会被序列化为 JSON 字符串存储 +> - 建议根据实际业务需求定义字段结构 + +#### 3.3.6 uniqueId 去重机制说明 + +- **作用**:防止重复记录相同的画像数据 +- **规则**:相同 `uniqueId` 的画像数据在 **半小时内** 会被合并统计,`count` 字段会自动累加 +- **建议格式**:`{来源标识}_{用户标识}_{时间戳}_{序号}` + - 示例:`site_13800000000_1710000000_001` + - 示例:`wechat_wxid_abc123_1710000000_001` +- **注意事项**: + - 如果不传 `uniqueId`,系统会为每条画像数据创建新记录 + - 如果需要在半小时内多次统计同一行为,应使用相同的 `uniqueId` + - 如果需要在半小时后重新统计,应使用不同的 `uniqueId`(建议修改时间戳部分) + +> **重要提示**:`portrait` **整体不参与签名计算**,但会参与业务处理。系统会根据 `uniqueId` 自动处理去重和统计。 + +--- + +## 四、请求示例 + +### 4.1 JSON 请求示例(无画像) + +```json +{ + "apiKey": "YOUR_API_KEY", + "timestamp": 1710000000, + "phone": "13800000000", + "name": "张三", + "source": "微信广告", + "remark": "通过H5落地页留资", + "tags": "高意向,电商", + "siteTags": "新客,女装", + "sign": "根据签名规则生成的MD5字符串" +} +``` + +### 4.2 JSON 请求示例(带微信号与画像) + +```json +{ + "apiKey": "YOUR_API_KEY", + "timestamp": 1710000000, + "wechatId": "wxid_abcdefg123", + "phone": "13800000001", + "name": "李四", + "source": "小程序落地页", + "remark": "点击【立即咨询】按钮", + "tags": "中意向,直播", + "siteTags": "复购,高客单", + "portrait": { + "type": 1, + "source": 0, + "sourceData": { + "age": 28, + "gender": "female", + "city": "上海", + "pageUrl": "https://example.com/product/123", + "productId": "P12345" + }, + "remark": "画像-点击行为", + "uniqueId": "site_13800000001_1710000000_001" + }, + "sign": "根据签名规则生成的MD5字符串" +} +``` + +### 4.3 JSON 请求示例(多种画像类型) + +#### 4.3.1 浏览行为画像 + +```json +{ + "apiKey": "YOUR_API_KEY", + "timestamp": 1710000000, + "phone": "13800000002", + "name": "王五", + "source": "百度推广", + "portrait": { + "type": 0, + "source": 0, + "sourceData": { + "pageUrl": "https://example.com/product/456", + "productName": "商品名称", + "category": "女装", + "stayTime": 120, + "device": "mobile" + }, + "remark": "商品浏览", + "uniqueId": "site_13800000002_1710000000_001" + }, + "sign": "根据签名规则生成的MD5字符串" +} +``` + + +``` + +--- + +## 五、响应说明 + +### 5.1 成功响应 + +**1)新增线索成功** + +```json +{ + "code": 200, + "message": "新增成功", + "data": "13800000000" +} +``` + +**2)线索已存在** + +```json +{ + "code": 200, + "message": "已存在", + "data": "13800000000" +} +``` + +> `data` 字段返回本次线索的主标识 `wechatId` 或 `phone`。 + +### 5.2 常见错误响应 + +```json +{ "code": 400, "message": "apiKey不能为空", "data": null } +{ "code": 400, "message": "sign不能为空", "data": null } +{ "code": 400, "message": "timestamp不能为空", "data": null } +{ "code": 400, "message": "请求已过期", "data": null } + +{ "code": 401, "message": "无效的apiKey", "data": null } +{ "code": 401, "message": "签名验证失败", "data": null } + +{ "code": 500, "message": "系统错误: 具体错误信息", "data": null } +``` + +--- + + +## 六、常见问题(FAQ) + +### Q1: 如果同一个用户多次上报相同的行为,会如何处理? + +**A**: 如果使用相同的 `uniqueId`,系统会在半小时内合并统计,`count` 字段会累加。如果使用不同的 `uniqueId`,会创建多条记录。 + +### Q2: portrait 字段是否必须传递? + +**A**: 不是必须的。`portrait` 字段是可选的,只有在需要记录用户画像数据时才传递。 + +### Q3: sourceData 中可以存储哪些类型的数据? + +**A**: `sourceData` 是一个 JSON 对象,可以存储任意键值对。支持字符串、数字、布尔值等基本类型,嵌套对象会被序列化为 JSON 字符串。 + +### Q4: uniqueId 的作用是什么? + +**A**: `uniqueId` 用于防止重复记录。相同 `uniqueId` 的画像数据在半小时内会被合并统计,避免重复数据。 + +### Q5: 画像数据如何与用户关联? + +**A**: 系统会根据请求中的 `wechatId` 或 `phone` 自动匹配 `traffic_pool` 表中的用户,并将画像数据关联到对应的 `trafficPoolId`。 + +--- diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..e71815a Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..eeaf5d0 --- /dev/null +++ b/public/index.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +// [ 应用入口文件 ] +namespace think; + +////处理跨域预检请求 +if($_SERVER['REQUEST_METHOD'] == 'OPTIONS'){ + header("Access-Control-Allow-Origin: " . (isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '*')); + header("Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization, Cookie"); + header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, PATCH'); + header("Access-Control-Allow-Credentials: true"); + exit; +} + +define('ROOT_PATH', dirname(__DIR__)); +define('DS', DIRECTORY_SEPARATOR); + +// 加载基础文件 +require __DIR__ . '/../thinkphp/base.php'; + +// 支持事先使用静态方法设置Request对象和Config对象 + +// 执行应用并响应 +Container::get('app')->run()->send(); \ No newline at end of file diff --git a/public/login.html b/public/login.html new file mode 100644 index 0000000..fabec67 --- /dev/null +++ b/public/login.html @@ -0,0 +1,123 @@ + + + + + + 登录测试 + + + +

JWT登录测试

+ +
+ + +
+ +
+ + +
+ + + + +
+

响应结果将显示在这里

+
+ + + + \ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/public/router.php b/public/router.php new file mode 100644 index 0000000..4f916b4 --- /dev/null +++ b/public/router.php @@ -0,0 +1,17 @@ + +// +---------------------------------------------------------------------- +// $Id$ + +if (is_file($_SERVER["DOCUMENT_ROOT"] . $_SERVER["SCRIPT_NAME"])) { + return false; +} else { + require __DIR__ . "/index.php"; +} diff --git a/public/test.php b/public/test.php new file mode 100644 index 0000000..6b9703c --- /dev/null +++ b/public/test.php @@ -0,0 +1,36 @@ + 'admin', + 'password' => '123456' +]); + +// 初始化cURL +$ch = curl_init($url); + +// 设置cURL选项 +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_POST, true); +curl_setopt($ch, CURLOPT_POSTFIELDS, $data); +curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Content-Length: ' . strlen($data) +]); + +// 执行请求 +$result = curl_exec($ch); + +// 检查是否有错误 +if (curl_errno($ch)) { + echo '请求错误: ' . curl_error($ch); +} else { + // 输出结果 + echo '
';
+    print_r(json_decode($result, true));
+    echo '
'; +} + +// 关闭cURL资源 +curl_close($ch); \ No newline at end of file diff --git a/route/route.php b/route/route.php new file mode 100644 index 0000000..9f6b71c --- /dev/null +++ b/route/route.php @@ -0,0 +1,45 @@ + +// +---------------------------------------------------------------------- + +use think\facade\Route; + + // 允许跨域 + header('Access-Control-Allow-Origin: ' . (isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '*')); + header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, PATCH'); + header('Access-Control-Allow-Headers: Cookie, Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With, X-Token, X-Api-Token'); + header('Access-Control-Max-Age: 1728000'); + header('Access-Control-Allow-Credentials: true'); + +// 加载Store模块路由配置 +include __DIR__ . '/../application/api/config/route.php'; + +// 加载Common模块路由配置 +include __DIR__ . '/../application/common/config/route.php'; + +// 加载Cunkebao模块路由配置 +include __DIR__ . '/../application/cunkebao/config/route.php'; + +// 加载Store模块路由配置 +include __DIR__ . '/../application/store/config/route.php'; + +// 加载Superadmin模块路由配置 +include __DIR__ . '/../application/superadmin/config/route.php'; + +// 加载CozeAI模块路由配置 +include __DIR__ . '/../application/cozeai/config/route.php'; + +// 加载AI模块路由配置 +include __DIR__ . '/../application/ai/config/route.php'; + +// 加载存客宝模块路由配置 +include __DIR__ . '/../application/chukebao/config/route.php'; + +return []; diff --git a/sql.sql b/sql.sql new file mode 100644 index 0000000..633756e --- /dev/null +++ b/sql.sql @@ -0,0 +1,2523 @@ +/* + Navicat Premium Data Transfer + + Source Server : kr_存客宝 + Source Server Type : MySQL + Source Server Version : 50736 + Source Host : 56b4c23f6853c.gz.cdb.myqcloud.com:14413 + Source Schema : cunkebao_v3 + + Target Server Type : MySQL + Target Server Version : 50736 + File Encoding : 65001 + + Date: 16/12/2025 16:39:24 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for ck_administrator_permissions +-- ---------------------------- +DROP TABLE IF EXISTS `ck_administrator_permissions`; +CREATE TABLE `ck_administrator_permissions` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自动ID', + `adminId` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '超管用户ID', + `permissions` json NULL COMMENT '权限对象', + `createTime` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '更新时间', + `deleteTime` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '超级管理员权限配置表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_administrators +-- ---------------------------- +DROP TABLE IF EXISTS `ck_administrators`; +CREATE TABLE `ck_administrators` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `username` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '管理员名字', + `account` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '登录账号', + `password` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '登录密码', + `status` tinyint(3) UNSIGNED NULL DEFAULT 1 COMMENT '1->可用,0->禁用', + `lastLoginTime` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '最近登录时间', + `lastLoginIp` char(18) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '最近登录ip', + `authId` int(10) UNSIGNED NULL DEFAULT 0 COMMENT '权限id', + `createTime` int(10) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(10) NULL DEFAULT NULL COMMENT '更新时间', + `deleteTime` int(11) NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '超级管理员表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_ai_knowledge_base +-- ---------------------------- +DROP TABLE IF EXISTS `ck_ai_knowledge_base`; +CREATE TABLE `ck_ai_knowledge_base` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `typeId` int(11) NULL DEFAULT 1 COMMENT '类型id', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称', + `label` json NULL COMMENT '标签', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + `isDel` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(11) NOT NULL DEFAULT 0 COMMENT '删除时间', + `documentId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '知识库文件id', + `fileUrl` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件地址', + `size` int(10) NULL DEFAULT NULL COMMENT '文件大小', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ai知识库' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_ai_knowledge_base_type +-- ---------------------------- +DROP TABLE IF EXISTS `ck_ai_knowledge_base_type`; +CREATE TABLE `ck_ai_knowledge_base_type` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `type` tinyint(2) NULL DEFAULT 1 COMMENT '类型 0系统 1用户创建', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述', + `label` json NULL COMMENT '标签', + `prompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '提示词', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + `isDel` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(11) NOT NULL DEFAULT 0 COMMENT '删除时间', + `status` tinyint(2) NULL DEFAULT 1 COMMENT '状态 1启用 0禁用', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ai知识库类型' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_ai_settings +-- ---------------------------- +DROP TABLE IF EXISTS `ck_ai_settings`; +CREATE TABLE `ck_ai_settings` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `config` json NULL COMMENT '配置信息', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + `isRelease` tinyint(2) NULL DEFAULT 0 COMMENT '是否发布 0未发布 1已发布', + `releaseTime` int(11) NULL DEFAULT NULL COMMENT '发布时间', + `botId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '智能体id', + `datasetId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '知识库id', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'AI配置' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_app_version +-- ---------------------------- +DROP TABLE IF EXISTS `ck_app_version`; +CREATE TABLE `ck_app_version` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `forceUpdate` tinyint(2) NULL DEFAULT 0 COMMENT '是否强制更新', + `version` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `downloadUrl` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `updateContent` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `createTime` int(11) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_attachments +-- ---------------------------- +DROP TABLE IF EXISTS `ck_attachments`; +CREATE TABLE `ck_attachments` ( + `id` int(10) NOT NULL AUTO_INCREMENT COMMENT '自增长ID', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '资源名', + `hash_key` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '资源hash校验值', + `server` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '存储服务商', + `source` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '资源地址', + `dl_count` int(10) NULL DEFAULT 0 COMMENT '下载次数', + `size` int(10) NULL DEFAULT 0 COMMENT '资源大小', + `suffix` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '资源类型', + `scene` tinyint(3) NOT NULL COMMENT '引用场景,获客海报1', + `create_at` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间', + `update_at` timestamp(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_at` timestamp(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_hash_key`(`hash_key`) USING BTREE, + INDEX `idx_server`(`server`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 580 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '附件表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_call_recording +-- ---------------------------- +DROP TABLE IF EXISTS `ck_call_recording`; +CREATE TABLE `ck_call_recording` ( + `id` int(11) NOT NULL, + `phone` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号', + `isCallOut` tinyint(2) NULL DEFAULT NULL COMMENT '是否外呼', + `companyId` int(11) NULL DEFAULT NULL, + `callType` tinyint(2) NULL DEFAULT NULL, + `beginTime` int(11) NULL DEFAULT NULL, + `endTime` int(11) NULL DEFAULT NULL, + `createTime` int(11) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_id_phone_isCallOut_companyId`(`id`, `phone`, `isCallOut`, `companyId`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_chat_groups +-- ---------------------------- +DROP TABLE IF EXISTS `ck_chat_groups`; +CREATE TABLE `ck_chat_groups` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `groupName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称', + `groupMemo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述', + `groupType` tinyint(2) NULL DEFAULT NULL COMMENT '类型 1好友 2群', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司ID', + `sort` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '排序', + `createTime` int(11) NULL DEFAULT NULL, + `isDel` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '聊天分组' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_company +-- ---------------------------- +DROP TABLE IF EXISTS `ck_company`; +CREATE TABLE `ck_company` ( + `id` int(11) UNSIGNED NOT NULL COMMENT '项目真实ID,非自增', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '项目名称', + `status` tinyint(1) UNSIGNED NULL DEFAULT 1 COMMENT '状态', + `tenantId` int(11) UNSIGNED NULL DEFAULT 242 COMMENT '触客宝租户ID', + `companyId` int(11) UNSIGNED NOT NULL COMMENT '触客宝部门ID', + `memo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `createTime` int(11) NULL DEFAULT NULL, + `updateTime` int(11) NULL DEFAULT NULL, + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '部门表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_content_item +-- ---------------------------- +DROP TABLE IF EXISTS `ck_content_item`; +CREATE TABLE `ck_content_item` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `libraryId` int(11) NOT NULL COMMENT '所属内容库ID', + `type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'moment' COMMENT '内容类型(moment:朋友圈)', + `contentType` tinyint(1) NULL DEFAULT 0 COMMENT '0:未知 1:图片 2:链接 3:视频 4:文本 5:小程序 ', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '内容标题', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '文本内容', + `contentAi` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '文本内容_Ai版', + `contentData` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '完整内容数据(JSON格式)', + `snsId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '朋友圈唯一标识', + `msgId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群消息唯一标识', + `wechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信ID', + `friendId` int(11) NULL DEFAULT NULL COMMENT '微信好友ID', + `createMomentTime` bigint(20) NULL DEFAULT 0 COMMENT '朋友圈创建时间', + `createTime` int(11) NULL DEFAULT NULL COMMENT '记录创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '记录更新时间', + `coverImage` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '封面图片URL', + `resUrls` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '资源URL列表(JSON格式)', + `urls` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '相对路径URL列表(JSON格式)', + `location` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '地理位置名称', + `lat` decimal(10, 6) NULL DEFAULT 0.000000 COMMENT '纬度', + `lng` decimal(10, 6) NULL DEFAULT 0.000000 COMMENT '经度', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态(0:禁用,1:启用)', + `isDel` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除(0:否,1:是)', + `delTime` int(11) NULL DEFAULT 0 COMMENT '删除时间', + `wechatChatroomId` int(11) NULL DEFAULT NULL, + `senderNickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `createMessageTime` int(11) NULL DEFAULT NULL, + `comment` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '评论', + `sendTime` int(11) NULL DEFAULT 0 COMMENT '预计发布时间', + `sendTimes` int(11) NULL DEFAULT 0 COMMENT '实际发布时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_library`(`libraryId`) USING BTREE, + INDEX `idx_snsid`(`snsId`) USING BTREE, + INDEX `idx_wechatid`(`wechatId`) USING BTREE, + INDEX `idx_friendid`(`friendId`) USING BTREE, + INDEX `idx_create_time`(`createTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 6090 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '内容项目表-存储朋友圈采集数据' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_content_library +-- ---------------------------- +DROP TABLE IF EXISTS `ck_content_library`; +CREATE TABLE `ck_content_library` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `formType` tinyint(2) NULL DEFAULT 0 COMMENT '0存客宝 1触客宝', + `sourceType` tinyint(2) NOT NULL DEFAULT 1 COMMENT '类型 1好友 2群 3自定义', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '内容库名称', + `devices` json NULL COMMENT '设备列表', + `catchType` json NULL COMMENT '采集类型', + `sourceFriends` json NULL COMMENT '选择的微信好友', + `sourceGroups` json NULL COMMENT '选择的微信群', + `groupMembers` json NULL COMMENT '选择的微信群的群成员', + `keywordInclude` json NULL COMMENT '包含的关键词', + `keywordExclude` json NULL COMMENT '排除的关键词', + `aiEnabled` tinyint(1) NULL DEFAULT 0 COMMENT '是否启用AI:0=禁用,1=启用', + `aiPrompt` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT 'AI提示词', + `timeEnabled` tinyint(1) NULL DEFAULT 0 COMMENT '是否启用时间限制:0=禁用,1=启用', + `timeStart` int(11) NULL DEFAULT NULL COMMENT '开始时间', + `timeEnd` int(11) NULL DEFAULT NULL COMMENT '结束时间', + `status` tinyint(1) NULL DEFAULT 0 COMMENT '状态:0=禁用,1=启用', + `userId` int(11) NOT NULL COMMENT '用户ID', + `companyId` int(11) NOT NULL COMMENT '公司ID', + `createTime` int(11) NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT 0 COMMENT '更新时间', + `isDel` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 135 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '内容库表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_coze_conversation +-- ---------------------------- +DROP TABLE IF EXISTS `ck_coze_conversation`; +CREATE TABLE `ck_coze_conversation` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户id', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `conversation_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '对话ID', + `bot_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '机器人ID', + `created_at` int(11) NOT NULL DEFAULT 0 COMMENT '会话创建时间戳', + `meta_data` json NULL COMMENT '元数据', + `create_time` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间戳', + `update_time` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间戳', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `idx_conversation_id`(`conversation_id`) USING BTREE, + INDEX `idx_bot_id`(`bot_id`) USING BTREE, + INDEX `idx_create_time`(`create_time`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 56 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'Coze AI 会话表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_coze_message +-- ---------------------------- +DROP TABLE IF EXISTS `ck_coze_message`; +CREATE TABLE `ck_coze_message` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `chat_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '消息ID', + `conversation_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '会话ID', + `bot_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '机器人ID', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '消息内容', + `content_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'text' COMMENT '内容类型', + `role` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色', + `type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '消息类型', + `created_at` int(11) NOT NULL COMMENT '消息创建时间', + `updated_at` int(11) NOT NULL COMMENT '消息更新时间', + `create_time` int(11) NOT NULL COMMENT '记录创建时间', + `update_time` int(11) NOT NULL COMMENT '记录更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_chat_id`(`chat_id`) USING BTREE, + INDEX `idx_conversation_id`(`conversation_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 184 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '消息记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_coze_workspace +-- ---------------------------- +DROP TABLE IF EXISTS `ck_coze_workspace`; +CREATE TABLE `ck_coze_workspace` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `workspace_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '工作区ID', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '工作区名称', + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '工作区描述', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `workspace_id`(`workspace_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'Coze空间表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_customer_acquisition_task +-- ---------------------------- +DROP TABLE IF EXISTS `ck_customer_acquisition_task`; +CREATE TABLE `ck_customer_acquisition_task` ( + `id` int(10) NOT NULL AUTO_INCREMENT COMMENT '自增长ID', + `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '计划名称', + `sceneId` int(11) NULL DEFAULT 1 COMMENT '场景ID', + `sceneConf` json NULL COMMENT '场景具体配置信息', + `reqConf` json NULL COMMENT '好友申请设置', + `msgConf` json NULL COMMENT '消息设置', + `tagConf` json NULL COMMENT '标签设置', + `userId` int(11) NULL DEFAULT 0 COMMENT '创建者', + `companyId` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '公司ID', + `status` tinyint(3) NOT NULL DEFAULT 0 COMMENT '状态 0禁用 1启用', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '修改时间', + `deleteTime` int(11) NULL DEFAULT 0 COMMENT '删除时间', + `apiKey` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 178 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '获客计划表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_device +-- ---------------------------- +DROP TABLE IF EXISTS `ck_device`; +CREATE TABLE `ck_device` ( + `id` int(11) UNSIGNED NOT NULL COMMENT '设备真实ID,非自增', + `memo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '设备名称', + `imei` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '设备IMEI', + `deviceImei` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '设备本地IMEI', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号', + `operatingSystem` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作系统版本', + `model` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '型号', + `brand` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '品牌', + `rooted` tinyint(1) NULL DEFAULT 0 COMMENT '是否root', + `xPosed` tinyint(1) NULL DEFAULT 0 COMMENT '是否安装xposed', + `softwareVersion` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '软件版本', + `extra` json NULL COMMENT '额外信息JSON', + `alive` tinyint(1) NULL DEFAULT 0 COMMENT '是否在线', + `companyId` int(11) NOT NULL COMMENT '公司ID', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_id_imei`(`imei`, `id`) USING BTREE, + INDEX `idx_group`(`companyId`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_device_handle_log +-- ---------------------------- +DROP TABLE IF EXISTS `ck_device_handle_log`; +CREATE TABLE `ck_device_handle_log` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `content` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作说明', + `deviceId` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '设备id', + `userId` int(11) NULL DEFAULT NULL COMMENT '用户id', + `companyId` int(11) NULL DEFAULT NULL COMMENT '租户id', + `createTime` int(11) NULL DEFAULT NULL COMMENT '操作时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 351 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_device_taskconf +-- ---------------------------- +DROP TABLE IF EXISTS `ck_device_taskconf`; +CREATE TABLE `ck_device_taskconf` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `deviceId` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '设备ID', + `autoLike` tinyint(3) NULL DEFAULT 0 COMMENT '自动点赞', + `momentsSync` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT '朋友圈同步', + `autoCustomerDev` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT '自动开发客户', + `groupMessageDeliver` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT '群消息推送', + `autoGroup` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT '自动建群', + `autoAddFriend` tinyint(3) NULL DEFAULT 0 COMMENT '自动加好友', + `contentSync` tinyint(255) UNSIGNED NULL DEFAULT 0 COMMENT '朋友圈同步', + `aiChat` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT 'AI 会话', + `autoReply` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT '自动回复', + `companyId` int(10) NULL DEFAULT NULL COMMENT '公司ID', + `createTime` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '更新时间', + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 31 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备任务配置表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_device_user +-- ---------------------------- +DROP TABLE IF EXISTS `ck_device_user`; +CREATE TABLE `ck_device_user` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `companyId` int(11) UNSIGNED NOT NULL COMMENT '公司id', + `userId` int(11) UNSIGNED NOT NULL COMMENT '用户id', + `deviceId` int(11) UNSIGNED NOT NULL COMMENT '设备id', + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 24 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备跟操盘手的关联关系' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_device_wechat_login +-- ---------------------------- +DROP TABLE IF EXISTS `ck_device_wechat_login`; +CREATE TABLE `ck_device_wechat_login` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `deviceId` int(11) NULL DEFAULT NULL COMMENT '设备ID', + `wechatId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信ID', + `alive` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT '微信在线否', + `companyId` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '租户ID', + `createTime` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '更新时间', + `isTips` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否提示迁移', + PRIMARY KEY (`id`) USING BTREE, + INDEX `wechatId`(`wechatId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 322 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备登录微信记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_distribution_channel +-- ---------------------------- +DROP TABLE IF EXISTS `ck_distribution_channel`; +CREATE TABLE `ck_distribution_channel` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '渠道ID', + `companyId` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '公司ID', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '渠道名称', + `code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '渠道编码(系统生成)', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '联系电话', + `password` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '密码(MD5加密)', + `wechatId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '微信号', + `remarks` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '备注信息', + `createType` enum('manual','auto') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'manual' COMMENT '创建类型:manual手动创建,auto扫码创建', + `status` enum('enabled','disabled') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'enabled' COMMENT '状态:enabled启用,disabled禁用', + `totalCustomers` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '总获客数', + `todayCustomers` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '今日获客数', + `totalFriends` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '总加好友数', + `todayFriends` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '今日加好友数', + `withdrawableAmount` bigint(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '可提现金额(分)', + `createTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新时间', + `deleteTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '删除时间(软删除)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_companyId`(`companyId`) USING BTREE, + INDEX `idx_code`(`code`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_deleteTime`(`deleteTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '分销渠道表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_distribution_revenue_record +-- ---------------------------- +DROP TABLE IF EXISTS `ck_distribution_revenue_record`; +CREATE TABLE `ck_distribution_revenue_record` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '收益记录ID', + `companyId` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '公司ID', + `channelId` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '渠道ID', + `channelCode` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '渠道编码(冗余字段,方便查询)', + `type` enum('customer_acquisition','add_friend','order','poster','phone','other') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'other' COMMENT '收益类型:customer_acquisition获客,add_friend加好友,order订单,poster海报,phone电话,other其他', + `sourceType` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '来源类型(如:海报获客、加好友任务等)', + `sourceId` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '来源ID(关联任务ID或其他业务ID)', + `amount` bigint(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '收益金额(分,整型,单位分)', + `remark` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '备注信息', + `createTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_companyId`(`companyId`) USING BTREE, + INDEX `idx_channelId`(`channelId`) USING BTREE, + INDEX `idx_channelCode`(`channelCode`) USING BTREE, + INDEX `idx_type`(`type`) USING BTREE, + INDEX `idx_createTime`(`createTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '分销渠道收益明细表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_distribution_withdrawal +-- ---------------------------- +DROP TABLE IF EXISTS `ck_distribution_withdrawal`; +CREATE TABLE `ck_distribution_withdrawal` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '提现申请ID', + `companyId` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '公司ID', + `channelId` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '渠道ID', + `amount` bigint(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '提现金额(分,整型,单位分)', + `payType` enum('wechat','alipay','bankcard') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'wechat' COMMENT '支付类型:wechat微信,alipay支付宝,bankcard银行卡', + `status` enum('pending','approved','rejected','paid') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'pending' COMMENT '状态:pending待审核,approved已通过,rejected已拒绝,paid已打款', + `reviewer` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '审核人', + `reviewTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '审核时间', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '备注/拒绝理由', + `applyTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '申请时间', + `createTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_companyId`(`companyId`) USING BTREE, + INDEX `idx_channelId`(`channelId`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_applyTime`(`applyTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '分销渠道提现申请表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_flow_package +-- ---------------------------- +DROP TABLE IF EXISTS `ck_flow_package`; +CREATE TABLE `ck_flow_package` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '套餐名称', + `tag` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '套餐标签', + `originalPrice` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '原价', + `price` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '售价', + `monthlyFlow` int(11) NOT NULL DEFAULT 0 COMMENT '每月流量(人/月)', + `duration` int(11) NOT NULL DEFAULT 1 COMMENT '套餐时长(月)', + `privileges` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '套餐特权,多行文本存储', + `sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序', + `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态: 0=禁用, 1=启用', + `isDel` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除: 0=否, 1=是', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_name`(`name`) USING BTREE, + INDEX `idx_tag`(`tag`) USING BTREE, + INDEX `idx_is_del`(`isDel`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量套餐表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_flow_package_order +-- ---------------------------- +DROP TABLE IF EXISTS `ck_flow_package_order`; +CREATE TABLE `ck_flow_package_order` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `orderNo` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '订单编号', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `packageId` int(11) NOT NULL DEFAULT 0 COMMENT '套餐ID', + `packageName` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '套餐名称', + `amount` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '订单金额', + `duration` int(11) NOT NULL DEFAULT 1 COMMENT '购买时长(月)', + `payStatus` tinyint(1) NOT NULL DEFAULT 0 COMMENT '支付状态: 0=未支付, 1=已支付,10=无需支付', + `payTime` int(11) NOT NULL DEFAULT 0 COMMENT '支付时间', + `payType` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '支付方式: wechat=微信, alipay=支付宝,nopay=无需支付', + `transactionId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '第三方支付交易号', + `status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '订单状态: 0=待支付, 1=已支付, 2=已取消, 3=已退款', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '备注', + `isDel` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除: 0=否, 1=是', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_order_no`(`orderNo`) USING BTREE, + INDEX `idx_user_id`(`userId`) USING BTREE, + INDEX `idx_package_id`(`packageId`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_pay_status`(`payStatus`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 37 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '套餐订单表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_flow_usage_record +-- ---------------------------- +DROP TABLE IF EXISTS `ck_flow_usage_record`; +CREATE TABLE `ck_flow_usage_record` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `packageId` int(11) NOT NULL DEFAULT 0 COMMENT '套餐ID', + `userPackageId` int(11) NOT NULL DEFAULT 0 COMMENT '用户套餐ID', + `taskId` int(11) NOT NULL DEFAULT 0 COMMENT '关联任务ID', + `phone` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '微信号', + `usageAmount` int(11) NOT NULL DEFAULT 0 COMMENT '使用量(人)', + `usageType` tinyint(1) NOT NULL DEFAULT 1 COMMENT '使用类型: 1=添加好友, 2=群发消息, 3=其他', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '备注', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_user_id`(`userId`) USING BTREE, + INDEX `idx_package_id`(`packageId`) USING BTREE, + INDEX `idx_user_package_id`(`userPackageId`) USING BTREE, + INDEX `idx_task_id`(`taskId`) USING BTREE, + INDEX `idx_phone`(`phone`) USING BTREE, + INDEX `idx_create_time`(`createTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量使用记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_jd_promotion_site +-- ---------------------------- +DROP TABLE IF EXISTS `ck_jd_promotion_site`; +CREATE TABLE `ck_jd_promotion_site` ( + `id` bigint(11) NOT NULL, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `jdSocialMediaId` bigint(11) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_jd_social_media +-- ---------------------------- +DROP TABLE IF EXISTS `ck_jd_social_media`; +CREATE TABLE `ck_jd_social_media` ( + `id` bigint(11) NOT NULL, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `appkey` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `secretkey` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_ai_push +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_ai_push`; +CREATE TABLE `ck_kf_ai_push` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '推送名称', + `tags` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '目标用户标签(JSON数组格式)', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '推送内容(支持变量:{客户名称}{产品功能}{核心价值}等)', + `pushTiming` tinyint(4) NOT NULL DEFAULT 1 COMMENT '推送时机:1=立即推送,2=最佳时机(AI决定),3=定时推送', + `scheduledTime` int(11) NOT NULL DEFAULT 0 COMMENT '定时推送时间(时间戳,仅当pushTiming=3时有效)', + `status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '启用状态:0=禁用,1=启用', + `successRate` decimal(5, 2) NOT NULL DEFAULT 0.00 COMMENT '成功率(百分比,保留两位小数)', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司ID', + `isDel` tinyint(4) NOT NULL DEFAULT 0 COMMENT '删除标记:0=未删除,1=已删除', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间(时间戳)', + `updateTime` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间(时间戳)', + `delTime` int(11) NOT NULL DEFAULT 0 COMMENT '删除时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_company_user`(`companyId`, `userId`) USING BTREE, + INDEX `idx_pushTiming`(`pushTiming`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_isDel`(`isDel`) USING BTREE, + INDEX `idx_scheduledTime`(`scheduledTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'AI智能推送表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_ai_push_record +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_ai_push_record`; +CREATE TABLE `ck_kf_ai_push_record` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `pushId` int(11) NOT NULL DEFAULT 0 COMMENT '推送ID', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司ID', + `wechatAccountId` int(11) NOT NULL DEFAULT 0 COMMENT '微信账号ID', + `friendIdOrGroupId` int(11) NOT NULL DEFAULT 0 COMMENT '好友ID或群ID', + `isSend` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否发送:0=未发送,1=已发送', + `sendTime` int(11) NOT NULL DEFAULT 0 COMMENT '发送时间(时间戳)', + `receiveTime` int(11) NOT NULL DEFAULT 0 COMMENT '接收时间(时间戳)', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_pushId`(`pushId`) USING BTREE, + INDEX `idx_company`(`companyId`) USING BTREE, + INDEX `idx_user`(`userId`) USING BTREE, + INDEX `idx_createTime`(`createTime`) USING BTREE, + INDEX `idx_isSend`(`isSend`) USING BTREE, + INDEX `idx_wechatAccount`(`wechatAccountId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'AI智能推送记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_auto_greetings +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_auto_greetings`; +CREATE TABLE `ck_kf_auto_greetings` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '规则名称', + `trigger` tinyint(4) NOT NULL DEFAULT 0 COMMENT '触发类型:1=好友首次添加,2=首次发消息,3=时间触发,4=关键词触发,5=生日触发,6=自定义', + `condition` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '具体条件(JSON格式)', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '问候内容', + `level` int(11) NOT NULL DEFAULT 0 COMMENT '优先级(数字越小优先级越高)', + `status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '启用状态:0=禁用,1=启用', + `usageCount` int(11) NOT NULL DEFAULT 0 COMMENT '使用次数', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司ID', + `is_template` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否模板:0=否,1=是', + `isDel` tinyint(4) NOT NULL DEFAULT 0 COMMENT '删除标记:0=未删除,1=已删除', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间(时间戳)', + `updateTime` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间(时间戳)', + `delTime` int(11) NOT NULL DEFAULT 0 COMMENT '删除时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_company_user`(`companyId`, `userId`) USING BTREE, + INDEX `idx_trigger`(`trigger`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_isDel`(`isDel`) USING BTREE, + INDEX `idx_level`(`level`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '问候规则表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_auto_greetings_record +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_auto_greetings_record`; +CREATE TABLE `ck_kf_auto_greetings_record` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `autoId` int(11) NOT NULL DEFAULT 0 COMMENT '规则ID', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司ID', + `wechatAccountId` int(11) NOT NULL DEFAULT 0 COMMENT '微信账号ID', + `friendIdOrGroupId` int(11) NOT NULL DEFAULT 0 COMMENT '好友ID或群ID', + `isSend` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否发送:0=未发送,1=已发送', + `sendTime` int(11) NOT NULL DEFAULT 0 COMMENT '发送时间(时间戳)', + `receiveTime` int(11) NOT NULL DEFAULT 0 COMMENT '接收时间(时间戳)', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间(时间戳)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_autoId`(`autoId`) USING BTREE, + INDEX `idx_company`(`companyId`) USING BTREE, + INDEX `idx_user`(`userId`) USING BTREE, + INDEX `idx_createTime`(`createTime`) USING BTREE, + INDEX `idx_isSend`(`isSend`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '问候规则使用记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_follow_up +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_follow_up`; +CREATE TABLE `ck_kf_follow_up` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标题', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述', + `friendId` int(12) NULL DEFAULT NULL COMMENT '好友id', + `type` tinyint(2) NULL DEFAULT 0 COMMENT '类型 0其他 1电话回访 2发送消息 3安排会议 4发送邮件', + `reminderTime` int(12) NULL DEFAULT NULL COMMENT '提醒时间', + `isRemind` tinyint(2) NULL DEFAULT 0 COMMENT '是否提醒', + `isProcess` tinyint(2) NULL DEFAULT 0 COMMENT '是否处理', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_companyId`(`companyId`) USING BTREE, + INDEX `idx_userId`(`userId`) USING BTREE, + INDEX `idx_level`(`type`) USING BTREE, + INDEX `idx_isRemind`(`isRemind`) USING BTREE, + INDEX `idx_isProcess`(`isProcess`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '跟进提醒' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_friend_settings +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_friend_settings`; +CREATE TABLE `ck_kf_friend_settings` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `type` tinyint(2) NULL DEFAULT 0 COMMENT '匹配类型 0人工接待 1AI辅助 2AI接管', + `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id', + `friendId` int(11) NULL DEFAULT NULL COMMENT '好友id', + `conversationId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '会话id', + `conversationTime` int(11) NULL DEFAULT NULL COMMENT '会话创建时间', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_companyId`(`companyId`) USING BTREE, + INDEX `idx_userId`(`userId`) USING BTREE, + INDEX `idx_wechatAccountId`(`wechatAccountId`) USING BTREE, + INDEX `idx_friendId`(`friendId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 51 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '好友AI配置' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_keywords +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_keywords`; +CREATE TABLE `ck_kf_keywords` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标题', + `keywords` json NULL COMMENT '关键词', + `type` tinyint(2) NULL DEFAULT NULL COMMENT '匹配类型 0模糊 1精确', + `replyType` tinyint(2) NULL DEFAULT NULL COMMENT '回复类型 0素材回复 1自定义', + `content` json NULL COMMENT '自定义内容', + `metailGroups` json NULL COMMENT '素材id', + `status` tinyint(2) NULL DEFAULT NULL COMMENT '状态 0停用 1启用', + `level` tinyint(2) NULL DEFAULT 0 COMMENT '等级 0低优先级 1中优先级 2高优先级', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '关键词管理' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_material +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_material`; +CREATE TABLE `ck_kf_material` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `title` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标题', + `content` json NULL COMMENT '内容', + `cover` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '封面', + `status` tinyint(2) NULL DEFAULT NULL COMMENT '状态 0停用 1启用', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 19 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '素材库管理' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_moments +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_moments`; +CREATE TABLE `ck_kf_moments` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `sendData` json NULL COMMENT '发送的具体信息', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `isSend` tinyint(2) NULL DEFAULT 0 COMMENT '是否发送 0否 1是', + `sendTime` int(11) NULL DEFAULT NULL COMMENT '发送时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '客服端发布朋友圈记录' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_moments_settings +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_moments_settings`; +CREATE TABLE `ck_kf_moments_settings` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `wechatId` int(12) NULL DEFAULT NULL COMMENT '微信客服id', + `max` int(11) NULL DEFAULT 5 COMMENT '每日上限', + `sendNum` int(11) NULL DEFAULT 0 COMMENT '今日发送次数', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '客服朋友圈配置信息' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_notice +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_notice`; +CREATE TABLE `ck_kf_notice` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `type` tinyint(2) NULL DEFAULT NULL COMMENT '通知类型 1代办事项 2跟进提醒 ', + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `bindId` int(11) NULL DEFAULT NULL COMMENT '绑定的id', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标题', + `message` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '通知消息', + `isRead` tinyint(2) NULL DEFAULT 0 COMMENT '是否读取', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `readTime` int(12) NULL DEFAULT NULL COMMENT '读取时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 252 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '通知消息' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_questions +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_questions`; +CREATE TABLE `ck_kf_questions` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `type` tinyint(2) NULL DEFAULT 0 COMMENT '匹配类型 0模糊 1精确', + `questions` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '问题', + `answers` json NULL COMMENT '答案', + `status` tinyint(2) NULL DEFAULT 1 COMMENT '状态 0禁用 1启用', + `isDel` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除', + `deleteTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_companyId`(`companyId`) USING BTREE, + INDEX `idx_userId`(`userId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'AI问答' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_reply +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_reply`; +CREATE TABLE `ck_kf_reply` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `groupId` int(11) NULL DEFAULT NULL, + `userId` int(11) NULL DEFAULT NULL, + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `msgType` tinyint(2) NULL DEFAULT NULL, + `content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `createTime` int(11) NULL DEFAULT NULL, + `lastUpdateTime` int(11) NULL DEFAULT NULL, + `sortIndex` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 130753 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '快捷回复' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_reply_group +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_reply_group`; +CREATE TABLE `ck_kf_reply_group` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `userId` int(11) NULL DEFAULT 0, + `companyId` int(11) NULL DEFAULT 0, + `groupName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `sortIndex` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `parentId` int(11) NULL DEFAULT NULL, + `replyType` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `replys` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 21898 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '快捷回复分组' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_sensitive_word +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_sensitive_word`; +CREATE TABLE `ck_kf_sensitive_word` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标题', + `keywords` json NULL COMMENT '关键词', + `content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '替换内容/警告内容', + `operation` tinyint(2) NULL DEFAULT NULL COMMENT '操作 0不操作 1替换 2删除 3警告 4禁止发送', + `status` tinyint(2) NULL DEFAULT NULL COMMENT '状态 0停用 1启用', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '敏感词管理' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_kf_to_do +-- ---------------------------- +DROP TABLE IF EXISTS `ck_kf_to_do`; +CREATE TABLE `ck_kf_to_do` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标题', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述', + `friendId` int(12) NULL DEFAULT NULL COMMENT '好友id', + `level` tinyint(2) NULL DEFAULT 0 COMMENT '提示等级 0低优先级 1中优先级 2高优先级 3紧急', + `reminderTime` int(12) NULL DEFAULT NULL COMMENT '提醒时间', + `isRemind` tinyint(2) NULL DEFAULT 0 COMMENT '是否提醒', + `isProcess` tinyint(2) NULL DEFAULT 0 COMMENT '是否处理', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_companyId`(`companyId`) USING BTREE, + INDEX `idx_userId`(`userId`) USING BTREE, + INDEX `idx_level`(`level`) USING BTREE, + INDEX `idx_isRemind`(`isRemind`) USING BTREE, + INDEX `idx_isProcess`(`isProcess`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '待办事项' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_menus +-- ---------------------------- +DROP TABLE IF EXISTS `ck_menus`; +CREATE TABLE `ck_menus` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '菜单ID', + `title` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '菜单名称', + `path` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '路由路径', + `icon` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '图标名称', + `parentId` int(11) NOT NULL DEFAULT 0 COMMENT '父菜单ID,0表示顶级菜单', + `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态:1启用,0禁用', + `sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序,数值越小越靠前', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_parent_id`(`parentId`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '系统菜单表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_order +-- ---------------------------- +DROP TABLE IF EXISTS `ck_order`; +CREATE TABLE `ck_order` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `mchId` int(11) NULL DEFAULT NULL COMMENT '门店号', + `companyId` int(11) UNSIGNED NOT NULL, + `userId` int(11) NULL DEFAULT NULL, + `orderType` tinyint(2) NULL DEFAULT NULL COMMENT '订单类型 1购买算力', + `status` tinyint(1) UNSIGNED NULL DEFAULT 0 COMMENT '支付状态 0待支付 1已付款 2已退款 3付款失败', + `goodsId` int(11) NULL DEFAULT 0 COMMENT '商品id', + `goodsName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品名称', + `goodsSpecs` json NULL COMMENT '商品规格', + `money` int(11) NULL DEFAULT 0 COMMENT '金额 单位分', + `orderNo` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单号', + `ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `nonceStr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '随机字符串', + `createTime` int(11) NULL DEFAULT NULL, + `payType` tinyint(2) NULL DEFAULT NULL COMMENT '支付类型 1微信 2支付宝', + `payTime` int(11) NULL DEFAULT NULL, + `payInfo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '错误信息', + `deleteTime` int(11) UNSIGNED NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 106 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_plan_scene +-- ---------------------------- +DROP TABLE IF EXISTS `ck_plan_scene`; +CREATE TABLE `ck_plan_scene` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `name` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '场景名称', + `description` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述', + `image` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '图片icon', + `status` tinyint(3) NULL DEFAULT NULL COMMENT '状态', + `sort` tinyint(3) NULL DEFAULT NULL, + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '修改时间', + `deleteTime` int(11) NULL DEFAULT 0 COMMENT '删除时间', + `scenarioTags` json NULL COMMENT '标签', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '获客场景' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_plan_tags +-- ---------------------------- +DROP TABLE IF EXISTS `ck_plan_tags`; +CREATE TABLE `ck_plan_tags` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `tagName` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签名', + `companyId` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '部门ID', + `createTime` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量标签表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_task_customer +-- ---------------------------- +DROP TABLE IF EXISTS `ck_task_customer`; +CREATE TABLE `ck_task_customer` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `task_id` int(11) NOT NULL, + `channelId` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '渠道ID(分销渠道ID,对应distribution_channel.id)', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户姓名', + `source` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '来源', + `phone` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `remark` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `tags` json NULL, + `siteTags` json NULL COMMENT '站内标签', + `processed_wechat_ids` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '0-未处理,1-已处理/添加中,2-~~添加成功~~ ~~已添加~~添加任务成功 3-添加失败 4-已通过-已发消息', + `fail_reason` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `addTime` int(11) NOT NULL DEFAULT 0 COMMENT '添加时间', + `passTime` int(11) NOT NULL DEFAULT 0 COMMENT '通过时间', + `createTime` int(11) NOT NULL DEFAULT 0, + `updateTime` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE, + INDEX `task_id`(`task_id`) USING BTREE, + INDEX `addTime`(`addTime`) USING BTREE, + INDEX `passTime`(`passTime`) USING BTREE, + INDEX `updateTime`(`updateTime`) USING BTREE, + INDEX `idx_channelId`(`channelId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 28222 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_tokens_company +-- ---------------------------- +DROP TABLE IF EXISTS `ck_tokens_company`; +CREATE TABLE `ck_tokens_company` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `userId` int(10) NULL DEFAULT 0, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `tokens` bigint(100) NULL DEFAULT NULL, + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + `isAdmin` tinyint(2) NULL DEFAULT 0 COMMENT '是否公司主号', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '公司算力账户' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_tokens_form +-- ---------------------------- +DROP TABLE IF EXISTS `ck_tokens_form`; +CREATE TABLE `ck_tokens_form` ( + `id` int(11) UNSIGNED NOT NULL, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称', + `tokens` int(12) NULL DEFAULT 0 COMMENT '消耗token', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `status` tinyint(2) NULL DEFAULT 0 COMMENT '状态', + `createTime` int(11) NULL DEFAULT 0, + `delTime` int(11) NULL DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_tokens_package +-- ---------------------------- +DROP TABLE IF EXISTS `ck_tokens_package`; +CREATE TABLE `ck_tokens_package` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称', + `tokens` int(12) NULL DEFAULT NULL, + `price` int(12) NULL DEFAULT NULL COMMENT '售价 单位分', + `originalPrice` int(12) NULL DEFAULT NULL COMMENT '原价 单位分', + `description` json NULL COMMENT '描述', + `sort` int(12) NULL DEFAULT 50 COMMENT '排序', + `isTrial` tinyint(2) NULL DEFAULT 0 COMMENT '是否试用', + `isRecommend` tinyint(2) NULL DEFAULT 0 COMMENT '是否推荐', + `isHot` tinyint(2) NULL DEFAULT 0 COMMENT '是否热门', + `isVip` tinyint(2) NULL DEFAULT 0 COMMENT '是否VIP', + `status` tinyint(2) NULL DEFAULT 0 COMMENT '状态 0停用 1启用', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `delTime` int(12) NULL DEFAULT NULL COMMENT '删除时间', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(12) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'token套餐' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_tokens_record +-- ---------------------------- +DROP TABLE IF EXISTS `ck_tokens_record`; +CREATE TABLE `ck_tokens_record` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(11) NOT NULL DEFAULT 0 COMMENT '公司id', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '创建用户ID', + `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id', + `friendIdOrGroupId` int(11) NULL DEFAULT NULL COMMENT '好友id或者群id', + `form` int(11) NULL DEFAULT 0 COMMENT '来源 \r\n0 未知\r\n1 点赞\r\n2 朋友圈同步\r\n3 朋友圈发布\r\n4 群发微信\r\n5 群发群消息\r\n6 群发群公告\r\n7 海报获客\r\n8 订单获客\r\n9 电话获客\r\n10 微信群获客\r\n11 API获客\r\n12 AI改写\r\n13 AI客服\r\n14 生成群公告\r\n\r\n1001 商家 \r\n1002 充值 \r\n1003 系统', + `type` tinyint(2) NULL DEFAULT 0 COMMENT '类型 0减少 1增加', + `tokens` int(11) NULL DEFAULT NULL COMMENT '消耗tokens', + `balanceTokens` int(11) NULL DEFAULT NULL COMMENT '剩余tokens', + `remarks` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `createTime` int(12) NULL DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 336 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '算力明细记录' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_traffic_order +-- ---------------------------- +DROP TABLE IF EXISTS `ck_traffic_order`; +CREATE TABLE `ck_traffic_order` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `companyId` int(10) UNSIGNED NULL DEFAULT NULL, + `identifier` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '流量池用户', + `createTime` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', + `isDel` tinyint(2) NULL DEFAULT 0, + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', + `orderno` varchar(0) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单编号', + `userId` int(11) NULL DEFAULT NULL, + `storeId` int(11) NULL DEFAULT NULL COMMENT '门店id', + `goddsName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品价格', + `price` int(10) NULL DEFAULT NULL COMMENT '商品价格', + `actualPay` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '实际支付', + `ownerWechatId` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_traffic_pool +-- ---------------------------- +DROP TABLE IF EXISTS `ck_traffic_pool`; +CREATE TABLE `ck_traffic_pool` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `identifier` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '流量标识,可以是手机号、微信号', + `mobile` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号', + `wechatId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信ID', + `createTime` int(10) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(10) NULL DEFAULT NULL COMMENT '修改时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_identifier`(`identifier`) USING BTREE, + INDEX `idx_wechatId`(`wechatId`) USING BTREE, + INDEX `idx_mobile`(`mobile`) USING BTREE, + INDEX `idx_create_time`(`createTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1201225 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量池' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_traffic_profile +-- ---------------------------- +DROP TABLE IF EXISTS `ck_traffic_profile`; +CREATE TABLE `ck_traffic_profile` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `identifier` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '流量标识,可以是手机号、微信号', + `nickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '平台昵称', + `avatar` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '平台头像', + `gender` tinyint(3) NULL DEFAULT 0 COMMENT '平台性别', + `phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '平台手机号', + `platformId` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '平台Id', + `createTime` int(10) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(10) NULL DEFAULT NULL COMMENT '修改时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_identifier`(`identifier`) USING BTREE, + INDEX `idx_mobile`(`phone`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 196606 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量池用户个人信息' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_traffic_source +-- ---------------------------- +DROP TABLE IF EXISTS `ck_traffic_source`; +CREATE TABLE `ck_traffic_source` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `type` tinyint(2) NULL DEFAULT 1 COMMENT '流量来源 0其他 1好友 2群 3场景', + `identifier` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '流量标识', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称', + `status` tinyint(3) NULL DEFAULT 1 COMMENT '1待处理,2处理中,3已通过,4已拒绝,5已过期,6已取消 -3已删除(同步 tk_friend_task 表的 status)', + `sourceId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '来源id(微信id或群id)', + `fromd` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '流量来源(群聊名称)', + `sceneId` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '场景ID', + `companyId` int(11) NULL DEFAULT 0 COMMENT '账号所属项目id', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '修改时间', + `R` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0', + `F` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0', + `M` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_identifier_sourceId_sceneId`(`identifier`, `sourceId`, `sceneId`) USING BTREE, + INDEX `idx_identifier`(`identifier`) USING BTREE, + INDEX `idx_companyId`(`companyId`) USING BTREE, + INDEX `idx_company_status_time`(`companyId`, `status`, `updateTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 586456 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量来源' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_traffic_source_package +-- ---------------------------- +DROP TABLE IF EXISTS `ck_traffic_source_package`; +CREATE TABLE `ck_traffic_source_package` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `userId` int(10) NULL DEFAULT NULL COMMENT '用户id', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述', + `pic` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '图标', + `companyId` int(11) NULL DEFAULT NULL COMMENT '账号所属项目id', + `matchingRules` json NULL COMMENT '匹配规则', + `isSys` tinyint(2) NULL DEFAULT 0 COMMENT '是否系统只有', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `updateTime` int(11) NULL DEFAULT NULL, + `createTime` int(11) NULL DEFAULT 0 COMMENT '创建时间', + `deleteTime` int(11) NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `companyId`(`companyId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量池包' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_traffic_source_package_item +-- ---------------------------- +DROP TABLE IF EXISTS `ck_traffic_source_package_item`; +CREATE TABLE `ck_traffic_source_package_item` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `packageId` int(10) NULL DEFAULT NULL COMMENT '流量包id', + `companyId` int(11) NULL DEFAULT NULL COMMENT '账号所属项目id', + `identifier` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '流量标识,可以是手机号、微信号', + `isDel` tinyint(2) NULL DEFAULT 0 COMMENT '是否删除', + `createTime` int(11) NULL DEFAULT 0 COMMENT '创建时间', + `deleteTime` int(10) NULL DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_packageId_companyId_identifier_isDel`(`packageId`, `companyId`, `identifier`, `isDel`) USING BTREE, + INDEX `packageId`(`packageId`) USING BTREE, + INDEX `companyId`(`companyId`) USING BTREE, + INDEX `identifier`(`identifier`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 34 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_traffic_tag +-- ---------------------------- +DROP TABLE IF EXISTS `ck_traffic_tag`; +CREATE TABLE `ck_traffic_tag` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `tagId` int(11) NULL DEFAULT NULL, + `tagName` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签名', + `tagType` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签值', + `tagValue` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签值', + `companyId` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '部门ID', + `trafficPoolId` int(10) NULL DEFAULT NULL COMMENT '流量池用户id traffic_pool的主键', + `createTime` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', + `isDel` tinyint(2) NULL DEFAULT NULL, + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量标签表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_user_flow_package +-- ---------------------------- +DROP TABLE IF EXISTS `ck_user_flow_package`; +CREATE TABLE `ck_user_flow_package` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `packageId` int(11) NOT NULL DEFAULT 0 COMMENT '套餐ID', + `orderId` int(11) NOT NULL DEFAULT 0 COMMENT '关联订单ID', + `duration` int(11) NOT NULL DEFAULT 1 COMMENT '套餐时长(月)', + `totalFlow` int(11) NOT NULL DEFAULT 0 COMMENT '总流量(人)', + `usedFlow` int(11) NOT NULL DEFAULT 0 COMMENT '已使用流量(人)', + `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态: 0=无效, 1=有效', + `startTime` int(11) NOT NULL DEFAULT 0 COMMENT '开始时间', + `expireTime` int(11) NOT NULL DEFAULT 0 COMMENT '到期时间', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_user_id`(`userId`) USING BTREE, + INDEX `idx_package_id`(`packageId`) USING BTREE, + INDEX `idx_order_id`(`orderId`) USING BTREE, + INDEX `idx_expire_time`(`expireTime`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户流量套餐表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_user_log +-- ---------------------------- +DROP TABLE IF EXISTS `ck_user_log`; +CREATE TABLE `ck_user_log` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `userId` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID', + `userName` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户名', + `action` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '操作类型', + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '操作描述', + `ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT 'IP地址', + `userAgent` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户设备信息', + `requestMethod` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '请求方法', + `requestUrl` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '请求URL', + `requestData` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '请求数据', + `responseCode` int(11) NULL DEFAULT 0 COMMENT '响应状态码', + `responseMsg` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '响应消息', + `createTime` int(11) NULL DEFAULT 0 COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_user_id`(`userId`) USING BTREE, + INDEX `idx_user_name`(`userName`) USING BTREE, + INDEX `idx_action`(`action`) USING BTREE, + INDEX `idx_create_time`(`createTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 45 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户操作日志表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_user_portrait +-- ---------------------------- +DROP TABLE IF EXISTS `ck_user_portrait`; +CREATE TABLE `ck_user_portrait` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `type` tinyint(2) NULL DEFAULT 0 COMMENT '类型 0浏览 1点击 2下单/购买 3注册 4互动', + `companyId` int(11) NULL DEFAULT 0, + `trafficPoolId` int(10) NULL DEFAULT NULL COMMENT '流量池用户id traffic_pool的主键', + `source` tinyint(2) NULL DEFAULT 0 COMMENT '来源 0本站 1老油条 2老坑爹', + `uniqueId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '来源网站唯一id', + `sourceData` json NULL COMMENT '来源网站数据', + `remark` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `count` int(10) NULL DEFAULT 1 COMMENT '统计次数(半小时内)', + `createTime` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '修改时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 22602 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户画像' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_users +-- ---------------------------- +DROP TABLE IF EXISTS `ck_users`; +CREATE TABLE `ck_users` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID', + `account` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号', + `username` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', + `phone` char(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '登录手机号', + `passwordMd5` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码', + `passwordLocal` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '本地密码', + `avatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'https://img.icons8.com/color/512/circled-user-male-skin-type-7.png' COMMENT '头像', + `isAdmin` tinyint(3) NULL DEFAULT 0 COMMENT '是否管理身份 1->是 0->否', + `companyId` int(10) UNSIGNED NOT NULL COMMENT '账号所属项目id', + `typeId` tinyint(3) NOT NULL DEFAULT -1 COMMENT '类型:运营后台/操盘手 传1 、 门店传2', + `status` tinyint(3) NULL DEFAULT 0 COMMENT '1->可用,0->禁用', + `s2_accountId` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'S2的用户账号id', + `balance` int(11) NULL DEFAULT 0 COMMENT '余额', + `tokens` int(11) NULL DEFAULT 0 COMMENT '算力余额', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '修改时间', + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1666 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_vendor_order +-- ---------------------------- +DROP TABLE IF EXISTS `ck_vendor_order`; +CREATE TABLE `ck_vendor_order` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '订单ID', + `orderNo` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '订单编号', + `userId` int(10) UNSIGNED NOT NULL COMMENT '用户ID', + `packageId` int(10) UNSIGNED NOT NULL COMMENT '套餐ID', + `packageName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '套餐名称', + `totalAmount` decimal(10, 2) NOT NULL COMMENT '订单总额', + `payAmount` decimal(10, 2) NOT NULL COMMENT '支付金额', + `advancePayment` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '预付款', + `status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '状态:0=待支付,1=已支付,2=已完成,3=已取消', + `payTime` int(11) NULL DEFAULT 0 COMMENT '支付时间', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `createTime` int(11) NOT NULL COMMENT '创建时间', + `updateTime` int(11) NOT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `orderNo`(`orderNo`) USING BTREE, + INDEX `userId`(`userId`) USING BTREE, + INDEX `packageId`(`packageId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '供应商订单表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_vendor_package +-- ---------------------------- +DROP TABLE IF EXISTS `ck_vendor_package`; +CREATE TABLE `ck_vendor_package` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '套餐ID', + `userId` int(11) NULL DEFAULT NULL COMMENT '用户id', + `companyId` int(11) NULL DEFAULT NULL COMMENT '公司id', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '套餐名称', + `originalPrice` decimal(10, 2) NOT NULL COMMENT '原价', + `price` decimal(10, 2) NOT NULL COMMENT '售价', + `discount` decimal(4, 2) NULL DEFAULT 0.00 COMMENT '折扣', + `advancePayment` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '预付款', + `tags` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签', + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '套餐描述', + `cover` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '封面图片', + `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态:0=下架,1=上架', + `createTime` int(11) NOT NULL COMMENT '创建时间', + `updateTime` int(11) NOT NULL COMMENT '更新时间', + `isDel` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '供应商套餐表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_vendor_project +-- ---------------------------- +DROP TABLE IF EXISTS `ck_vendor_project`; +CREATE TABLE `ck_vendor_project` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '项目ID', + `packageId` int(10) UNSIGNED NOT NULL COMMENT '套餐ID', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '项目名称', + `originalPrice` decimal(10, 2) NOT NULL COMMENT '原价', + `price` decimal(10, 2) NOT NULL COMMENT '售价', + `duration` int(11) NULL DEFAULT 0 COMMENT '项目时长(分钟)', + `image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '项目图片', + `detail` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '项目详情', + `createTime` int(11) NOT NULL COMMENT '创建时间', + `updateTime` int(11) NOT NULL COMMENT '更新时间', + `isDel` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE, + INDEX `packageId`(`packageId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '供应商套餐项目表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_wechat_account +-- ---------------------------- +DROP TABLE IF EXISTS `ck_wechat_account`; +CREATE TABLE `ck_wechat_account` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `s2_wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '微信账号id', + `wechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信ID', + `alias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信号', + `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', + `pyInitial` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '拼音首字母', + `quanPin` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '全拼', + `avatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像URL', + `gender` tinyint(1) NULL DEFAULT 0 COMMENT '性别 0->保密;1->男;2->女', + `region` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '地区', + `signature` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '个性签名', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '电话', + `country` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '国家', + `privince` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '省份', + `city` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '城市', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_wechatId`(`wechatId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4282931 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_wechat_customer +-- ---------------------------- +DROP TABLE IF EXISTS `ck_wechat_customer`; +CREATE TABLE `ck_wechat_customer` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增id', + `wechatId` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信id', + `basic` json NULL COMMENT '保存基础信息', + `weight` json NULL COMMENT '保存权重信息', + `activity` json NULL COMMENT '保存账号活跃信息', + `friendShip` json NULL COMMENT '保存朋友关系信息', + `companyId` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '公司id', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_wechatId`(`wechatId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 159 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信客服信息' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_wechat_friendship +-- ---------------------------- +DROP TABLE IF EXISTS `ck_wechat_friendship`; +CREATE TABLE `ck_wechat_friendship` ( + `id` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '好友id', + `wechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '微信ID', + `tags` json NULL COMMENT '好友标签', + `memo` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '好友备注', + `ownerWechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '所有者微信ID', + `companyId` int(11) NULL DEFAULT NULL COMMENT '公司ID', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + UNIQUE INDEX `uk_owner_wechat_account`(`ownerWechatId`, `wechatId`) USING BTREE, + INDEX `idx_wechat_id`(`wechatId`) USING BTREE, + INDEX `idx_owner_wechat_id`(`ownerWechatId`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信好友表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_wechat_group +-- ---------------------------- +DROP TABLE IF EXISTS `ck_wechat_group`; +CREATE TABLE `ck_wechat_group` ( + `id` int(11) UNSIGNED NOT NULL COMMENT 'S2微信群id', + `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '微信账号ID', + `chatroomId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信群聊id', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群名称', + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群头像', + `companyId` int(11) NULL DEFAULT NULL COMMENT '项目id', + `ownerWechatId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '所有者微信ID', + `identifier` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群主(流量标识,可以是手机号、微信号)', + `createTime` int(11) UNSIGNED NULL DEFAULT NULL, + `updateTime` int(11) UNSIGNED NULL DEFAULT NULL, + `deleteTime` int(11) UNSIGNED NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_owner_chatroomId`(`chatroomId`, `ownerWechatId`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_wechat_group_member +-- ---------------------------- +DROP TABLE IF EXISTS `ck_wechat_group_member`; +CREATE TABLE `ck_wechat_group_member` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `identifier` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群成员(流量标识,可以是手机号、微信号)', + `chatroomId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群真实id', + `customerIs` tinyint(3) NULL DEFAULT 0 COMMENT '是否客服', + `companyId` int(11) NULL DEFAULT NULL COMMENT '项目id', + `groupId` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '所属群ID', + `createTime` int(11) UNSIGNED NULL DEFAULT 0, + `deleteTime` int(11) UNSIGNED NULL DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_identifier_chatroomId_groupId`(`identifier`, `chatroomId`, `groupId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 561848 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群成员' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_wechat_restricts +-- ---------------------------- +DROP TABLE IF EXISTS `ck_wechat_restricts`; +CREATE TABLE `ck_wechat_restricts` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `taskId` int(11) NULL DEFAULT NULL COMMENT '任务id', + `level` tinyint(3) UNSIGNED NULL DEFAULT 1 COMMENT '风险类型 1 普通 2 警告 3 错误', + `reason` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '风险原因', + `memo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '记录更详细的风险信息', + `wechatId` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信id', + `companyId` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '项目id', + `restrictTime` int(11) NULL DEFAULT NULL COMMENT '限制日期', + `recoveryTime` int(11) NULL DEFAULT NULL COMMENT '恢复日期', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1416 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信风险受限记录' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_wechat_tag +-- ---------------------------- +DROP TABLE IF EXISTS `ck_wechat_tag`; +CREATE TABLE `ck_wechat_tag` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `tags` json NULL COMMENT '标签JSON', + `wechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '微信ID', + `companyId` int(11) NULL DEFAULT NULL COMMENT '公司ID', + `createTime` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_wechatId`(`wechatId`) USING BTREE, + INDEX `idx_companyId`(`companyId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 123366 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench`; +CREATE TABLE `ck_workbench` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `userId` int(11) NOT NULL COMMENT '创建用户ID', + `companyId` int(11) NULL DEFAULT 0 COMMENT '公司id', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '工作台名称', + `type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '工作台类型:1=自动点赞,2=朋友圈同步,3=群消息推送,4=自动建群,5=流量分发,6=通讯录导入', + `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态:0=禁用,1=启用', + `autoStart` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否自动启动:0=否,1=是', + `createTime` int(11) NOT NULL COMMENT '创建时间', + `updateTime` int(11) NOT NULL COMMENT '更新时间', + `isDel` tinyint(1) NULL DEFAULT 0, + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_user_id`(`userId`) USING BTREE, + INDEX `idx_type`(`type`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 330 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '工作台主表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_auto_like +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_auto_like`; +CREATE TABLE `ck_workbench_auto_like` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `interval` int(11) NOT NULL DEFAULT 60 COMMENT '点赞间隔(秒)', + `maxLikes` int(11) NOT NULL DEFAULT 100 COMMENT '最大点赞数', + `startTime` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '00:00:00' COMMENT '开始时间', + `endTime` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '23:59:59' COMMENT '结束时间', + `contentTypes` json NULL COMMENT '内容类型', + `devices` json NULL COMMENT '设备列表,JSON格式:[{\"id\":1,\"name\":\"设备1\"},{\"id\":2,\"name\":\"设备2\"}]', + `friends` json NULL COMMENT '用户列表', + `createTime` int(11) NOT NULL COMMENT '创建时间', + `updateTime` int(11) NOT NULL COMMENT '更新时间', + `targetGroups` json NULL COMMENT '目标用户组列表,JSON格式:[{\"id\":1,\"name\":\"用户组1\"},{\"id\":2,\"name\":\"用户组2\"}] 废除', + `tagOperator` tinyint(1) NULL DEFAULT 2 COMMENT '标签匹配规则 1:and 2:or 废除', + `friendMaxLikes` int(10) NULL DEFAULT NULL COMMENT '好友最大点赞数', + `friendTags` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '好友标签', + `enableFriendTags` tinyint(1) NULL DEFAULT 0 COMMENT '启用好友标签', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_workbench_id`(`workbenchId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 54 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '自动点赞配置表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_auto_like_item +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_auto_like_item`; +CREATE TABLE `ck_workbench_auto_like_item` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `deviceId` int(11) NULL DEFAULT 0 COMMENT '设备id', + `snsId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '自动点赞id', + `wechatFriendId` int(11) NULL DEFAULT NULL COMMENT '好友id', + `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id', + `momentsId` int(11) NULL DEFAULT NULL COMMENT '朋友圈id', + `createTime` int(11) NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `workbenchId`(`workbenchId`) USING BTREE, + INDEX `wechatFriendId`(`wechatFriendId`) USING BTREE, + INDEX `wechatAccountId`(`wechatAccountId`) USING BTREE, + INDEX `momentsId`(`momentsId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4653 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '工作台-自动点赞记录' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_group_create +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_group_create`; +CREATE TABLE `ck_workbench_group_create` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `workbenchId` int(11) NOT NULL COMMENT '计划ID', + `devices` json NULL COMMENT '目标设备/客服(JSON数组)', + `admins` json NULL COMMENT '管理员', + `poolGroups` json NULL COMMENT '流量池JSON', + `wechatGroups` json NULL COMMENT '微信客服JSON', + `startTime` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '开始时间', + `endTime` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '结束时间', + `groupSizeMin` int(10) NULL DEFAULT NULL COMMENT '群好友最小人数', + `groupSizeMax` int(10) NULL DEFAULT NULL COMMENT '群好友最大人数', + `maxGroupsPerDay` int(10) NULL DEFAULT NULL COMMENT '每日建群最大数量', + `groupNameTemplate` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群模板信息', + `groupDescription` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群描述', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 27 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_group_create_item +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_group_create_item`; +CREATE TABLE `ck_workbench_group_create_item` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `friendId` int(11) NULL DEFAULT NULL, + `wechatId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '微信id', + `groupId` int(10) NULL DEFAULT NULL COMMENT '群id', + `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id', + `status` tinyint(2) NOT NULL DEFAULT 0 COMMENT '状态:0=待创建,1=创建中,2=创建成功,3=创建失败,4=管理员好友已拉入', + `memberType` tinyint(2) NOT NULL DEFAULT 1 COMMENT '成员类型:1=群主成员,2=管理员,3=群主好友,4=管理员好友', + `retryCount` int(11) NOT NULL DEFAULT 0 COMMENT '重试次数', + `chatroomId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群聊ID(用于查询验证)', + `verifyTime` int(11) NULL DEFAULT NULL COMMENT '验证时间', + `createTime` int(11) NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_status_workbench`(`status`, `workbenchId`) USING BTREE, + INDEX `idx_chatroom_id`(`chatroomId`) USING BTREE, + INDEX `idx_member_type`(`memberType`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 66 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_group_push +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_group_push`; +CREATE TABLE `ck_workbench_group_push` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `pushType` tinyint(1) NOT NULL DEFAULT 1 COMMENT '推送方式 0定时 1立即', + `targetType` tinyint(1) NOT NULL DEFAULT 1 COMMENT '推送目标类型:1=群推送,2=好友推送', + `startTime` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '推送开始时间', + `endTime` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '推送结束时间', + `maxPerDay` int(11) NULL DEFAULT 0 COMMENT '每日推送条数', + `pushOrder` tinyint(1) NULL DEFAULT 1 COMMENT '推送顺序 1最早 2最新', + `isLoop` tinyint(1) NULL DEFAULT 0 COMMENT '是否循环推送 0否 1是', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '是否启用 0否 1是', + `groups` json NULL COMMENT '推送微信群组(JSON)', + `friends` json NULL COMMENT '推送好友列表(JSON)', + `ownerWechatIds` json NULL COMMENT '所属微信id', + `contentLibraries` json NULL COMMENT '内容库(JSON)', + `friendIntervalMin` int(11) NOT NULL DEFAULT 10 COMMENT '好友间最小间隔时间(秒)', + `friendIntervalMax` int(11) NOT NULL DEFAULT 20 COMMENT '好友间最大间隔时间(秒)', + `messageIntervalMin` int(11) NOT NULL DEFAULT 1 COMMENT '消息间最小间隔时间(秒)', + `messageIntervalMax` int(11) NOT NULL DEFAULT 12 COMMENT '消息间最大间隔时间(秒)', + `isRandomTemplate` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否随机选择话术组(0=否,1=是)', + `postPushTags` json NOT NULL COMMENT '推送完成后打标签', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + `socialMediaId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '京东导购媒体', + `promotionSiteId` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '京东广告位', + `trafficPools` json NULL COMMENT '流量池', + `devices` json NULL, + `groupPushSubType` tinyint(2) NULL DEFAULT 1 COMMENT '群推送子类型 1=群群发,2=群公告', + `announcementContent` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `enableAiRewrite` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `aiRewritePrompt` tinyint(2) NULL DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_workbench_id`(`workbenchId`) USING BTREE, + INDEX `idx_status_targetType`(`status`, `targetType`, `workbenchId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 34 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '群消息推送扩展表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_group_push_item +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_group_push_item`; +CREATE TABLE `ck_workbench_group_push_item` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `targetType` tinyint(1) NOT NULL DEFAULT 1 COMMENT '推送目标类型:1=群,2=好友', + `contentId` int(11) NULL DEFAULT 0 COMMENT '内容库is', + `groupId` int(10) NULL DEFAULT NULL COMMENT '群id', + `friendId` int(11) NULL DEFAULT NULL COMMENT '好友ID(当targetType=2时使用)', + `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id', + `isLoop` tinyint(2) NULL DEFAULT 0 COMMENT '是否循环完成', + `createTime` int(11) NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_workbench_target_time`(`workbenchId`, `targetType`, `createTime`) USING BTREE, + INDEX `idx_workbench_target_friend`(`workbenchId`, `targetType`, `friendId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 302 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_import_contact +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_import_contact`; +CREATE TABLE `ck_workbench_import_contact` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `devices` json NULL COMMENT '设备id', + `pools` json NULL COMMENT '流量池', + `num` int(11) NULL DEFAULT NULL COMMENT '分配数量', + `clearContact` tinyint(2) NULL DEFAULT 0 COMMENT '是否清除现有联系人', + `remarkType` tinyint(2) NOT NULL DEFAULT 0 COMMENT '备注类型 0不备注 1年月日 2月日 3自定义', + `remark` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `startTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '开始时间', + `endTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '结束时间', + `createTime` int(11) NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_import_contact_item +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_import_contact_item`; +CREATE TABLE `ck_workbench_import_contact_item` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `deviceId` int(11) NULL DEFAULT NULL COMMENT '设备id', + `packageId` int(11) NULL DEFAULT 0 COMMENT '流量包id', + `poolId` int(11) NULL DEFAULT NULL COMMENT '流量id', + `createTime` int(11) NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 140 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_moments_sync +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_moments_sync`; +CREATE TABLE `ck_workbench_moments_sync` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `syncInterval` int(11) NOT NULL DEFAULT 1 COMMENT '同步间隔(小时)', + `syncCount` int(11) NOT NULL DEFAULT 5 COMMENT '每日同步数量', + `syncType` tinyint(1) NOT NULL DEFAULT 1 COMMENT '同步类型:1=文本,2=图片,3=视频,4=链接', + `startTime` time(0) NULL DEFAULT '06:00:00' COMMENT '发布开始时间', + `endTime` time(0) NULL DEFAULT '23:59:00' COMMENT '发布结束时间', + `accountType` tinyint(1) NOT NULL DEFAULT 1 COMMENT '账号类型:1=业务号,2=个人号', + `devices` json NOT NULL COMMENT '设备列表,JSON格式', + `contentLibraries` json NULL COMMENT '内容库ID列表,JSON格式', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_workbench_id`(`workbenchId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 97 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '朋友圈同步配置' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_moments_sync_item +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_moments_sync_item`; +CREATE TABLE `ck_workbench_moments_sync_item` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '工作台ID', + `deviceId` int(11) NULL DEFAULT 0 COMMENT '设备id', + `contentId` int(10) NULL DEFAULT NULL COMMENT '内容库id', + `wechatAccountId` int(11) NULL DEFAULT NULL COMMENT '客服id', + `createTime` int(11) NOT NULL COMMENT '创建时间', + `isLoop` tinyint(2) NULL DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_workbench_time`(`workbenchId`, `createTime`) USING BTREE, + INDEX `idx_workbench_content`(`workbenchId`, `contentId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2308 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '朋友圈同步配置' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_traffic_config +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_traffic_config`; +CREATE TABLE `ck_workbench_traffic_config` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL COMMENT '流量分发计划ID', + `distributeType` tinyint(1) NOT NULL DEFAULT 1 COMMENT '分配方式 1均分 2优先级 3比例', + `maxPerDay` int(11) NOT NULL DEFAULT 0 COMMENT '每日最大分配量', + `timeType` tinyint(1) NOT NULL DEFAULT 1 COMMENT '时间限制 1全天 2自定义', + `startTime` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '开始时间', + `endTime` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '结束时间', + `account` json NULL COMMENT '分发的账号', + `devices` json NULL COMMENT '目标设备/客服(JSON数组)', + `pools` json NULL COMMENT '流量池(JSON数组)', + `exp` int(10) NULL DEFAULT 30 COMMENT '有效期 单位天', + `createTime` int(11) NOT NULL, + `updateTime` int(11) NOT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uniq_workbench`(`workbenchId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 32 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量分发计划扩展表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for ck_workbench_traffic_config_item +-- ---------------------------- +DROP TABLE IF EXISTS `ck_workbench_traffic_config_item`; +CREATE TABLE `ck_workbench_traffic_config_item` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `workbenchId` int(11) NOT NULL DEFAULT 0 COMMENT '工作台ID', + `deviceId` int(11) NULL DEFAULT 0 COMMENT '设备id', + `wechatFriendId` int(10) NULL DEFAULT NULL COMMENT '好友id', + `wechatAccountId` int(11) NULL DEFAULT 0 COMMENT '客服id', + `expTime` int(11) NULL DEFAULT 0 COMMENT '有效时间', + `exp` int(11) NULL DEFAULT 0 COMMENT '有效时间 天', + `isRecycle` tinyint(2) NULL DEFAULT 0 COMMENT '是否回收', + `recycleTime` int(11) NULL DEFAULT 0 COMMENT '回收时间', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `workbenchId`(`workbenchId`) USING BTREE, + INDEX `deviceId`(`deviceId`) USING BTREE, + INDEX `wechatFriendId`(`wechatFriendId`) USING BTREE, + INDEX `wechatAccountId`(`wechatAccountId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 58212 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '流量分发计划扩展表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_allot_rule +-- ---------------------------- +DROP TABLE IF EXISTS `s2_allot_rule`; +CREATE TABLE `s2_allot_rule` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '规则ID', + `departmentId` int(11) NULL DEFAULT 0 COMMENT '部门id', + `tenantId` int(11) NOT NULL DEFAULT 0 COMMENT '租户ID', + `allotType` tinyint(4) NOT NULL DEFAULT 0 COMMENT '分配类型', + `allotOnline` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否在线分配', + `kefuRange` tinyint(4) NOT NULL DEFAULT 0 COMMENT '客服范围', + `wechatRange` tinyint(4) NOT NULL DEFAULT 0 COMMENT '微信范围', + `kefuData` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '客服数据JSON', + `wechatData` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '微信ID列表JSON', + `labels` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '标签JSON', + `priorityStrategy` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '优先级策略JSON', + `sortIndex` int(11) NOT NULL DEFAULT 0 COMMENT '排序索引', + `creatorAccountId` int(11) NOT NULL DEFAULT 0 COMMENT '创建者账号ID', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `ruleName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '规则名称', + `isDel` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_tenant`(`tenantId`) USING BTREE, + INDEX `idx_sort`(`sortIndex`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2176 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '分配规则表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_call_recording +-- ---------------------------- +DROP TABLE IF EXISTS `s2_call_recording`; +CREATE TABLE `s2_call_recording` ( + `id` bigint(20) NOT NULL COMMENT '主键ID', + `tenantId` bigint(20) NOT NULL DEFAULT 0 COMMENT '租户ID', + `deviceOwnerId` bigint(20) NOT NULL DEFAULT 0 COMMENT '设备所有者ID', + `userName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户名', + `nickname` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '昵称', + `realName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '真实姓名', + `deviceMemo` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '设备备注', + `fileName` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '文件名', + `imei` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '设备IMEI', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '电话号码', + `isCallOut` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否为呼出电话(0:呼入,1:呼出)', + `beginTime` int(11) NOT NULL DEFAULT 0 COMMENT '通话开始时间戳', + `endTime` int(11) NOT NULL DEFAULT 0 COMMENT '通话结束时间戳', + `audioUrl` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '录音文件URL', + `mp3AudioUrl` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'MP3录音文件URL', + `callBeginTime` int(11) NOT NULL DEFAULT 0 COMMENT '呼叫开始时间戳', + `callLogId` bigint(20) NOT NULL DEFAULT 0 COMMENT '通话日志ID', + `callType` int(11) NOT NULL DEFAULT 0 COMMENT '通话类型', + `duration` int(11) NOT NULL DEFAULT 0 COMMENT '通话时长(秒)', + `skipReason` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '跳过原因', + `skipUpload` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否跳过上传', + `isDeleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已删除', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间戳', + `lastUpdateTime` int(11) NOT NULL DEFAULT 0 COMMENT '最后更新时间戳', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_tenant_id`(`tenantId`) USING BTREE, + INDEX `idx_device_owner_id`(`deviceOwnerId`) USING BTREE, + INDEX `idx_user_name`(`userName`) USING BTREE, + INDEX `idx_phone`(`phone`) USING BTREE, + INDEX `idx_begin_time`(`beginTime`) USING BTREE, + INDEX `idx_end_time`(`endTime`) USING BTREE, + INDEX `idx_call_begin_time`(`callBeginTime`) USING BTREE, + INDEX `idx_imei`(`imei`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '通话记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_company_account +-- ---------------------------- +DROP TABLE IF EXISTS `s2_company_account`; +CREATE TABLE `s2_company_account` ( + `id` int(11) NULL DEFAULT NULL COMMENT 'id', + `tenantId` int(11) NULL DEFAULT NULL, + `userName` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户名', + `realName` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '真实姓名', + `nickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '昵称', + `memo` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '备注', + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '头像', + `secret` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '密钥', + `accountType` int(11) NULL DEFAULT 0 COMMENT '账户类型', + `departmentId` int(11) NULL DEFAULT 0 COMMENT '部门ID', + `departmentName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门名称', + `useGoogleSecretKey` tinyint(1) NULL DEFAULT 0 COMMENT '是否使用谷歌密钥', + `hasVerifyGoogleSecret` tinyint(1) NULL DEFAULT 0 COMMENT '是否验证谷歌密钥', + `passwordMd5` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT 'MD5加密密码', + `passwordLocal` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '本地加密密码', + `lastLoginIp` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '最后登录IP', + `lastLoginTime` int(11) NULL DEFAULT 0 COMMENT '最后登录时间', + `createTime` int(11) NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT 0 COMMENT '更新时间', + `privilegeIds` json NULL COMMENT '权限', + `alive` tinyint(1) NULL DEFAULT NULL, + `creator` int(10) NULL DEFAULT NULL COMMENT '创建者', + `creatorRealName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建者真实姓名', + `creatorUserName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建者用户名', + `status` tinyint(1) NULL DEFAULT 0 COMMENT '状态 0正常 1禁用', + UNIQUE INDEX `idx_username`(`userName`) USING BTREE, + INDEX `idx_create_time`(`createTime`) USING BTREE, + INDEX `idx_update_time`(`updateTime`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '公司账户表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_department +-- ---------------------------- +DROP TABLE IF EXISTS `s2_department`; +CREATE TABLE `s2_department` ( + `id` int(11) NOT NULL, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称', + `memo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `tenantId` int(11) NULL DEFAULT NULL, + `isTop` tinyint(1) NULL DEFAULT 0, + `level` int(10) NULL DEFAULT 0, + `parentId` int(10) NULL DEFAULT 0, + `privileges` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, + `createTime` int(11) NULL DEFAULT NULL, + `lastUpdateTime` int(11) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '部门表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_device +-- ---------------------------- +DROP TABLE IF EXISTS `s2_device`; +CREATE TABLE `s2_device` ( + `id` int(11) NULL DEFAULT NULL COMMENT '设备真实ID', + `userName` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户名', + `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', + `realName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '真实姓名', + `groupName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分组名称', + `wechatAccounts` json NULL COMMENT '微信账号列表JSON', + `alive` tinyint(1) NULL DEFAULT 0 COMMENT '是否在线', + `aliveTime` int(11) NULL DEFAULT 0, + `lastAliveTime` int(11) NULL DEFAULT NULL COMMENT '最后在线时间', + `tenantId` int(11) NULL DEFAULT NULL COMMENT '租户ID', + `groupId` int(11) NULL DEFAULT NULL COMMENT '分组ID', + `currentAccountId` int(11) NULL DEFAULT NULL COMMENT '当前账号ID', + `imei` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '设备IMEI', + `deviceImei` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '设备本地IMEI', + `memo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `isDeleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除', + `deletedAndStop` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除并停止', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + `rooted` tinyint(1) NULL DEFAULT 0 COMMENT '是否root', + `xPosed` tinyint(1) NULL DEFAULT 0 COMMENT '是否安装xposed', + `brand` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '品牌', + `model` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '型号', + `operatingSystem` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作系统版本', + `softwareVersion` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '软件版本', + `extra` json NULL COMMENT '额外信息JSON', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号', + `lastUpdateTime` int(11) NULL DEFAULT NULL COMMENT '最后更新时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + `taskConfig` json NULL COMMENT '自动化任务开关 \r\nautoLike:自动点赞\r\nmomentsSync:朋友圈同步\r\nautoCustomerDev:自动开发客户\r\ngroupMessageDeliver:群消息推送\r\nautoGroup:自动建群', + UNIQUE INDEX `uk_imei`(`imei`) USING BTREE, + INDEX `idx_tenant`(`tenantId`) USING BTREE, + INDEX `idx_group`(`groupId`) USING BTREE, + INDEX `idx_current_account`(`currentAccountId`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_device_group +-- ---------------------------- +DROP TABLE IF EXISTS `s2_device_group`; +CREATE TABLE `s2_device_group` ( + `id` int(11) NOT NULL, + `tenantId` int(11) NOT NULL COMMENT '租户ID', + `groupName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '分组名称', + `groupMemo` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分组备注', + `count` int(11) NULL DEFAULT 0 COMMENT '设备数量', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + INDEX `idx_tenant`(`tenantId`) USING BTREE, + INDEX `idx_group_name`(`groupName`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备分组表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_friend_task +-- ---------------------------- +DROP TABLE IF EXISTS `s2_friend_task`; +CREATE TABLE `s2_friend_task` ( + `id` int(11) NOT NULL COMMENT '任务ID', + `tenantId` int(11) NULL DEFAULT 0 COMMENT '租户ID', + `operatorAccountId` int(11) NULL DEFAULT 0 COMMENT '操作账号ID', + `status` int(11) NULL DEFAULT 1 COMMENT '状态:0执行中,1执行成功,2执行失败', + `phone` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号/微信号', + `msgContent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '验证消息', + `wechatAccountId` int(11) NULL DEFAULT 0 COMMENT '微信账号ID', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间戳', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `extra` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '额外数据JSON', + `labels` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签,逗号分隔', + `from` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '来源', + `alias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信账号别名', + `wechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信ID', + `wechatAvatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信头像', + `wechatNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信昵称', + `accountNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号昵称', + `accountRealName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号真实姓名', + `accountUsername` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号用户名', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间戳', + `is_counted` tinyint(1) NULL DEFAULT 0 COMMENT '是否已统计(0=未统计,1=已统计)', + UNIQUE INDEX `uk_task_id`(`id`) USING BTREE, + INDEX `idx_tenant_id`(`tenantId`) USING BTREE, + INDEX `idx_operator_account_id`(`operatorAccountId`) USING BTREE, + INDEX `idx_wechat_account_id`(`wechatAccountId`) USING BTREE, + INDEX `idx_status`(`status`) USING BTREE, + INDEX `idx_phone`(`phone`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '添加好友任务记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_moments_item +-- ---------------------------- +DROP TABLE IF EXISTS `s2_moments_item`; +CREATE TABLE `s2_moments_item` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `task_id` int(11) NOT NULL COMMENT '朋友圈任务ID', + `temp_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '临时ID', + `wechat_account_id` int(11) NULL DEFAULT NULL COMMENT '微信账号ID', + `execute_count` int(11) NULL DEFAULT 0 COMMENT '执行次数', + `executed` tinyint(1) NULL DEFAULT 0 COMMENT '是否已执行', + `status` tinyint(1) NULL DEFAULT 0 COMMENT '状态', + `extra` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '额外信息', + `execute_time` int(11) NULL DEFAULT NULL COMMENT '执行时间', + `finished_time` int(11) NULL DEFAULT NULL COMMENT '完成时间', + `labels` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '标签', + `alt_list` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '替代列表', + `comments` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '评论', + `moment_content_type` tinyint(1) NULL DEFAULT 0 COMMENT '朋友圈内容类型', + `text` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '文本内容', + `pic_url_list` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '图片URL列表', + `video_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '视频URL', + `link` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '链接信息', + `is_use_location` tinyint(1) NULL DEFAULT 0 COMMENT '是否使用位置', + `lat` decimal(10, 6) NULL DEFAULT 0.000000 COMMENT '纬度', + `lng` decimal(10, 6) NULL DEFAULT 0.000000 COMMENT '经度', + `poi_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '位置名称', + `poi_address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '位置地址', + `video_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '视频编号', + `created_at` int(11) NULL DEFAULT NULL COMMENT '记录创建时间', + `updated_at` int(11) NULL DEFAULT NULL COMMENT '记录更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `idx_task_temp`(`task_id`, `temp_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 184 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '朋友圈任务项表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_moments_task +-- ---------------------------- +DROP TABLE IF EXISTS `s2_moments_task`; +CREATE TABLE `s2_moments_task` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `task_id` int(11) NOT NULL COMMENT '朋友圈任务ID', + `tenant_id` int(11) NULL DEFAULT NULL COMMENT '租户ID', + `operator_account_id` int(11) NULL DEFAULT NULL COMMENT '操作人账号ID', + `account_username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号用户名', + `account_nickname` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号昵称', + `account_real_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号真实姓名', + `public_mode` tinyint(1) NULL DEFAULT 0 COMMENT '发布模式', + `moment_content_type` tinyint(1) NULL DEFAULT 1 COMMENT '朋友圈内容类型:1纯文本,2图片,3视频,4链接', + `text` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '文本内容', + `pic_url_list` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '图片URL列表', + `video_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '视频URL', + `link` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '链接信息', + `job_status` tinyint(1) NULL DEFAULT 0 COMMENT '任务状态', + `job_origin_status` tinyint(1) NULL DEFAULT 0 COMMENT '任务原始状态', + `job_group` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '任务组', + `job_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '任务名称', + `begin_time` int(11) NULL DEFAULT NULL COMMENT '开始时间', + `end_time` int(11) NULL DEFAULT NULL COMMENT '结束时间', + `timing_time` int(11) NULL DEFAULT NULL COMMENT '定时发布时间', + `create_time` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `immediately` tinyint(1) NULL DEFAULT 1 COMMENT '是否立即发布', + `created_at` int(11) NULL DEFAULT NULL COMMENT '记录创建时间', + `updated_at` int(11) NULL DEFAULT NULL COMMENT '记录更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `idx_task_id`(`task_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 88 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '朋友圈任务表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_reply +-- ---------------------------- +DROP TABLE IF EXISTS `s2_reply`; +CREATE TABLE `s2_reply` ( + `id` int(11) NOT NULL, + `tenantId` int(255) NULL DEFAULT NULL, + `groupId` int(11) NULL DEFAULT NULL, + `accountId` int(11) NULL DEFAULT NULL, + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `msgType` tinyint(2) NULL DEFAULT NULL, + `content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `createTime` int(11) NULL DEFAULT NULL, + `lastUpdateTime` int(11) NULL DEFAULT NULL, + `sortIndex` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '快捷回复' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_reply_group +-- ---------------------------- +DROP TABLE IF EXISTS `s2_reply_group`; +CREATE TABLE `s2_reply_group` ( + `id` int(11) NOT NULL, + `groupName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `sortIndex` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `parentId` int(11) NULL DEFAULT NULL, + `replyType` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `replys` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `departmentId` int(11) NULL DEFAULT 2130, + `accountId` int(11) NULL DEFAULT 5150, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '快捷回复分组' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_account +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_account`; +CREATE TABLE `s2_wechat_account` ( + `id` int(11) NOT NULL COMMENT '微信账号ID', + `wechatId` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '微信ID', + `deviceAccountId` int(11) NULL DEFAULT 0 COMMENT '设备账号ID', + `imei` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'IMEI', + `deviceMemo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '设备备注', + `accountUserName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号用户名', + `accountRealName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号真实姓名', + `accountNickname` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号昵称', + `keFuAlive` tinyint(1) NULL DEFAULT 0 COMMENT '客服是否在线', + `deviceAlive` tinyint(1) NULL DEFAULT 0 COMMENT '设备是否在线', + `wechatAlive` tinyint(1) NULL DEFAULT 0 COMMENT '微信是否在线', + `wechatAliveTime` int(11) NULL DEFAULT 0 COMMENT '在线时间', + `yesterdayMsgCount` int(11) NULL DEFAULT 0 COMMENT '昨日消息数', + `sevenDayMsgCount` int(11) NULL DEFAULT 0 COMMENT '7天消息数', + `thirtyDayMsgCount` int(11) NULL DEFAULT 0 COMMENT '30天消息数', + `totalFriend` int(11) NULL DEFAULT 0 COMMENT '总好友数', + `maleFriend` int(11) NULL DEFAULT 0 COMMENT '男性好友数', + `unknowFriend` int(11) NULL DEFAULT NULL COMMENT '未知好友数', + `femaleFriend` int(11) NULL DEFAULT 0 COMMENT '女性好友数', + `wechatGroupName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信群组名称', + `tenantId` int(11) NULL DEFAULT NULL COMMENT '租户ID', + `nickname` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', + `alias` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '别名', + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像', + `gender` tinyint(1) NULL DEFAULT 0 COMMENT '性别', + `region` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '地区', + `signature` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '签名', + `bindQQ` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '绑定QQ', + `bindEmail` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '绑定邮箱', + `bindMobile` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '绑定手机', + `currentDeviceId` int(11) NULL DEFAULT 0 COMMENT '当前设备ID', + `isDeleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + `groupId` int(11) NULL DEFAULT 0 COMMENT '分组ID', + `memo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `wechatVersion` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信版本', + `labels` json NULL COMMENT '标签', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + `status` tinyint(3) NULL DEFAULT 1 COMMENT '状态值', + `healthScore` int(11) NULL DEFAULT 60 COMMENT '健康分总分(基础分+动态分)', + `baseScore` int(11) NULL DEFAULT 60 COMMENT '基础分(60-100分)', + `dynamicScore` int(11) NULL DEFAULT 0 COMMENT '动态分(扣分和加分)', + `isModifiedAlias` tinyint(1) NULL DEFAULT 0 COMMENT '是否已修改微信号(0=未修改,1=已修改)', + `lastFrequentTime` int(11) NULL DEFAULT NULL COMMENT '最后频繁时间(时间戳)', + `frequentCount` int(11) NULL DEFAULT 0 COMMENT '频繁次数(用于判断首次/再次频繁)', + `lastNoFrequentTime` int(11) NULL DEFAULT NULL COMMENT '最后不频繁时间(时间戳)', + `consecutiveNoFrequentDays` int(11) NULL DEFAULT 0 COMMENT '连续不频繁天数(用于加分)', + `scoreUpdateTime` int(11) NULL DEFAULT NULL COMMENT '评分更新时间', + INDEX `idx_wechat_id`(`wechatId`) USING BTREE, + INDEX `idx_health_score`(`healthScore`) USING BTREE, + INDEX `idx_is_modified_alias`(`isModifiedAlias`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_account_score +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_account_score`; +CREATE TABLE `s2_wechat_account_score` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `accountId` int(11) NOT NULL COMMENT '微信账号ID(s2_wechat_account.id)', + `wechatId` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '微信ID', + `baseScore` int(11) NOT NULL DEFAULT 0 COMMENT '基础分(60-100分)', + `baseScoreCalculated` tinyint(1) NOT NULL DEFAULT 0 COMMENT '基础分是否已计算(0=未计算,1=已计算)', + `baseScoreCalcTime` int(11) NULL DEFAULT NULL COMMENT '基础分计算时间', + `baseInfoScore` int(11) NOT NULL DEFAULT 0 COMMENT '基础信息分(0-10分)', + `isModifiedAlias` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否已修改微信号(0=未修改,1=已修改)', + `friendCountScore` int(11) NOT NULL DEFAULT 0 COMMENT '好友数量分(0-30分)', + `friendCount` int(11) NOT NULL DEFAULT 0 COMMENT '好友数量(评分时的快照)', + `friendCountSource` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '好友数量来源(manual=手动,sync=同步)', + `dynamicScore` int(11) NOT NULL DEFAULT 0 COMMENT '动态分(扣分和加分)', + `lastFrequentTime` int(11) NULL DEFAULT NULL COMMENT '最后频繁时间(时间戳)', + `frequentCount` int(11) NOT NULL DEFAULT 0 COMMENT '频繁次数(用于判断首次/再次频繁)', + `frequentPenalty` int(11) NOT NULL DEFAULT 0 COMMENT '频繁扣分(累计)', + `lastNoFrequentTime` int(11) NULL DEFAULT NULL COMMENT '最后不频繁时间(时间戳)', + `consecutiveNoFrequentDays` int(11) NOT NULL DEFAULT 0 COMMENT '连续不频繁天数', + `noFrequentBonus` int(11) NOT NULL DEFAULT 0 COMMENT '不频繁加分(累计)', + `isBanned` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否封号(0=否,1=是)', + `banPenalty` int(11) NOT NULL DEFAULT 0 COMMENT '封号扣分', + `healthScore` int(11) NOT NULL DEFAULT 0 COMMENT '健康分总分(基础分+动态分)', + `maxAddFriendPerDay` int(11) NOT NULL DEFAULT 0 COMMENT '每日最大加人次数', + `createTime` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', + `updateTime` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间', + `lastBanTime` int(11) NULL DEFAULT NULL COMMENT '最后一次封号时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_account_id`(`accountId`) USING BTREE, + INDEX `idx_wechat_id`(`wechatId`) USING BTREE, + INDEX `idx_health_score`(`healthScore`) USING BTREE, + INDEX `idx_base_score_calculated`(`baseScoreCalculated`) USING BTREE, + INDEX `idx_update_time`(`updateTime`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 368 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号评分记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_account_score_log +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_account_score_log`; +CREATE TABLE `s2_wechat_account_score_log` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', + `accountId` int(11) NOT NULL COMMENT '微信账号ID', + `wechatId` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '微信ID', + `field` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '变动字段(如frequentPenalty)', + `changeValue` int(11) NOT NULL DEFAULT 0 COMMENT '变动值(正加负减)', + `valueBefore` int(11) NULL DEFAULT NULL COMMENT '变更前的字段值', + `valueAfter` int(11) NULL DEFAULT NULL COMMENT '变更后的字段值', + `category` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分类:penalty/bonus/dynamic_total/health_total等', + `source` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '触发来源 friend_task/wechat_message/system', + `sourceId` bigint(20) NULL DEFAULT NULL COMMENT '关联记录ID(如任务/消息ID)', + `extra` json NULL COMMENT '附加信息(JSON)', + `totalScoreBefore` int(11) NULL DEFAULT NULL COMMENT '变更前健康总分', + `totalScoreAfter` int(11) NULL DEFAULT NULL COMMENT '变更后健康总分', + `createTime` int(11) NOT NULL COMMENT '记录时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_account_field`(`accountId`, `field`) USING BTREE, + INDEX `idx_wechat_id`(`wechatId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 39 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信账号健康分加减分日志' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_chatroom +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_chatroom`; +CREATE TABLE `s2_wechat_chatroom` ( + `id` int(11) NOT NULL, + `wechatAccountId` int(11) NOT NULL COMMENT '微信账号ID', + `wechatAccountAlias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信账号别名', + `wechatAccountWechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信账号微信ID', + `wechatAccountAvatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信账号头像', + `wechatAccountNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信账号昵称', + `chatroomId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '群聊ID', + `hasMe` tinyint(1) NULL DEFAULT 0 COMMENT '是否包含自己', + `chatroomOwnerNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群主昵称', + `chatroomOwnerAvatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群主头像', + `conRemark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群聊名称', + `pyInitial` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '拼音首字母', + `quanPin` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '全拼', + `chatroomAvatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '群头像', + `isDeleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `accountId` int(11) NULL DEFAULT 0 COMMENT '账号ID', + `accountUserName` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号用户名', + `accountRealName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号真实姓名', + `accountNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号昵称', + `groupId` int(11) NULL DEFAULT 0 COMMENT '分组ID', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + `isTop` tinyint(2) NULL DEFAULT 0 COMMENT '是否置顶', + `groupIds` int(11) NULL DEFAULT 0 COMMENT '新分组ID', + UNIQUE INDEX `uk_chatroom_account`(`chatroomId`, `wechatAccountId`) USING BTREE, + INDEX `wechatAccountId`(`wechatAccountId`) USING BTREE, + INDEX `chatroomId`(`chatroomId`) USING BTREE, + INDEX `wechatAccountWechatId`(`wechatAccountWechatId`) USING BTREE, + INDEX `idx_account_deleted`(`accountId`, `isDeleted`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_chatroom_member +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_chatroom_member`; +CREATE TABLE `s2_wechat_chatroom_member` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `chatroomId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '群聊ID', + `wechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '微信ID', + `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', + `avatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像', + `conRemark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + `alias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '别名', + `friendType` tinyint(11) NULL DEFAULT 0 COMMENT '好友类型', + `createTime` int(10) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(10) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_chatroom_wechat`(`chatroomId`, `wechatId`) USING BTREE, + INDEX `chatroomId`(`chatroomId`) USING BTREE, + INDEX `wechatId`(`wechatId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 496929 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群成员表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_friend +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_friend`; +CREATE TABLE `s2_wechat_friend` ( + `id` int(11) NULL DEFAULT NULL COMMENT '好友id', + `wechatAccountId` int(11) NOT NULL COMMENT '所有者微信账号ID', + `alias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '好友微信号', + `wechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '好友微信ID', + `conRemark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注名', + `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', + `pyInitial` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '拼音首字母', + `quanPin` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '全拼', + `avatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像URL', + `gender` tinyint(1) NULL DEFAULT 0 COMMENT '性别', + `region` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '地区', + `addFrom` int(11) NULL DEFAULT NULL COMMENT '添加来源', + `labels` json NULL COMMENT '标签JSON', + `siteLabels` json NULL COMMENT '站内标签JSON', + `signature` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '个性签名', + `isDeleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除', + `isPassed` tinyint(1) NULL DEFAULT 1 COMMENT '是否通过', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + `accountId` int(11) NULL DEFAULT 0 COMMENT '账号ID', + `extendFields` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '扩展字段JSON', + `accountUserName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号用户名', + `accountRealName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号真实姓名', + `accountNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号昵称', + `ownerAlias` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '所有者别名', + `ownerWechatId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '所有者微信ID', + `ownerNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '所有者昵称', + `ownerAvatar` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '所有者头像', + `phone` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '电话', + `thirdParty` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '第三方数据JSON', + `groupId` int(11) NULL DEFAULT 0 COMMENT '分组ID', + `passTime` int(11) NULL DEFAULT NULL COMMENT '通过时间', + `additionalPicture` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '附加图片', + `desc` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '描述', + `country` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '国家', + `privince` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '省份', + `city` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '城市', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `updateTime` int(11) NULL DEFAULT NULL COMMENT '更新时间', + `R` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0', + `F` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0', + `M` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0', + `realName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '姓名', + `company` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '公司', + `position` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '职位', + `isTop` tinyint(2) NULL DEFAULT 0 COMMENT '是否置顶', + `groupIds` int(11) NULL DEFAULT 0 COMMENT '新分组ID', + UNIQUE INDEX `uk_owner_wechat_account`(`ownerWechatId`, `wechatId`, `wechatAccountId`) USING BTREE, + INDEX `idx_wechat_account_id`(`wechatAccountId`) USING BTREE, + INDEX `idx_wechat_id`(`wechatId`) USING BTREE, + INDEX `idx_owner_wechat_id`(`ownerWechatId`) USING BTREE, + INDEX `idx_id`(`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信好友表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_group +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_group`; +CREATE TABLE `s2_wechat_group` ( + `id` int(11) NOT NULL, + `tenantId` int(11) NULL DEFAULT NULL, + `groupName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `groupMemo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `groupType` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `sortIndex` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `groupOwnerType` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `departmentId` int(11) NULL DEFAULT NULL, + `accountId` int(11) NULL DEFAULT NULL, + `createTime` int(11) NULL DEFAULT NULL, + `isDel` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除 0未删除 1已删除', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_message +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_message`; +CREATE TABLE `s2_wechat_message` ( + `id` bigint(20) NOT NULL COMMENT '消息ID', + `type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '消息类型 1好友 2群', + `wechatFriendId` bigint(20) NULL DEFAULT NULL COMMENT '微信好友ID', + `wechatChatroomId` bigint(20) NOT NULL COMMENT '微信群聊ID', + `senderNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '发送者昵称', + `senderWechatId` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '发送者微信ID', + `senderIsAdmin` tinyint(1) NULL DEFAULT 0 COMMENT '发送者是否管理员', + `senderIsDeleted` tinyint(1) NULL DEFAULT 0 COMMENT '发送者是否已删除', + `senderChatroomNickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '发送者群昵称', + `senderWechatAccountId` bigint(20) NULL DEFAULT NULL COMMENT '发送者微信账号ID', + `wechatAccountId` bigint(20) NULL DEFAULT NULL COMMENT '微信账号ID', + `tenantId` bigint(20) NULL DEFAULT NULL COMMENT '租户ID', + `accountId` bigint(20) NULL DEFAULT NULL COMMENT '账号ID', + `synergyAccountId` bigint(20) NULL DEFAULT 0 COMMENT '协同账号ID', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '消息内容', + `originalContent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '消息内容(原版)', + `msgType` int(11) NULL DEFAULT NULL COMMENT '消息类型 1 文字 3图片 47动态图片 34语言 43视频 42名片 40/20链接 49文件 419430449转账 436207665红包', + `msgSubType` int(11) NULL DEFAULT 0 COMMENT '消息子类型', + `msgSvrId` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '消息服务器ID', + `isSend` tinyint(1) NULL DEFAULT 1 COMMENT '是否发送', + `createTime` int(11) NULL DEFAULT NULL COMMENT '创建时间', + `isDeleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否已删除', + `deleteTime` int(11) NULL DEFAULT NULL COMMENT '删除时间', + `sendStatus` int(11) NULL DEFAULT 0 COMMENT '发送状态', + `wechatTime` int(11) NULL DEFAULT NULL COMMENT '微信时间', + `origin` int(11) NULL DEFAULT 0 COMMENT '来源', + `msgId` bigint(20) NULL DEFAULT NULL COMMENT '消息ID', + `recallId` tinyint(1) NULL DEFAULT 0 COMMENT '撤回ID', + `isRead` tinyint(1) NULL DEFAULT 0 COMMENT '是否读取', + `is_counted` tinyint(1) NULL DEFAULT 0 COMMENT '是否已统计(0=未统计,1=已统计)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_wechatChatroomId`(`wechatChatroomId`) USING BTREE, + INDEX `idx_wechatAccountId`(`wechatAccountId`) USING BTREE, + INDEX `idx_msgSvrId`(`msgSvrId`) USING BTREE, + INDEX `idx_type`(`type`) USING BTREE, + INDEX `idx_type_wechatTime`(`type`, `wechatTime`, `id`) USING BTREE, + INDEX `idx_friend_time`(`wechatFriendId`, `wechatTime`, `id`) USING BTREE, + INDEX `idx_chatroom_time`(`wechatChatroomId`, `wechatTime`, `id`) USING BTREE, + INDEX `idx_account_type`(`accountId`, `type`, `wechatTime`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信群聊消息记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for s2_wechat_moments +-- ---------------------------- +DROP TABLE IF EXISTS `s2_wechat_moments`; +CREATE TABLE `s2_wechat_moments` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `wechatAccountId` int(11) NOT NULL COMMENT '微信账号ID', + `wechatFriendId` int(11) NULL DEFAULT NULL COMMENT '微信好友ID', + `snsId` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '朋友圈消息ID', + `commentList` json NULL COMMENT '评论列表JSON', + `createTime` bigint(20) NULL DEFAULT 0 COMMENT '创建时间戳', + `likeList` json NULL COMMENT '点赞列表JSON', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '朋友圈内容', + `lat` decimal(10, 6) NULL DEFAULT 0.000000 COMMENT '纬度', + `lng` decimal(10, 6) NULL DEFAULT 0.000000 COMMENT '经度', + `location` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '位置信息', + `picSize` int(11) NULL DEFAULT 0 COMMENT '图片大小', + `resUrls` json NULL COMMENT '资源URL列表', + `userName` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户名', + `type` int(11) NULL DEFAULT 0 COMMENT '朋友圈类型', + `create_time` int(11) NULL DEFAULT NULL COMMENT '数据创建时间', + `update_time` int(11) NULL DEFAULT NULL COMMENT '数据更新时间', + `coverImage` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `urls` json NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `idx_sns_account`(`snsId`, `wechatAccountId`) USING BTREE, + INDEX `idx_account_friend`(`wechatAccountId`, `wechatFriendId`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 40159 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信朋友圈数据表' ROW_FORMAT = Dynamic; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/think b/think new file mode 100644 index 0000000..bd31715 --- /dev/null +++ b/think @@ -0,0 +1,28 @@ +#!/usr/bin/env php + +// +---------------------------------------------------------------------- + +namespace think; + +define('ROOT_PATH', __DIR__); +define('DS', DIRECTORY_SEPARATOR); + +$_SERVER['HTTPS'] = true; +$_SERVER['HTTP_HOST'] = ''; + +// 加载基础文件 +require __DIR__ . '/thinkphp/base.php'; + +// 应用初始化 +Container::get('app')->path(__DIR__ . '/application/')->initialize(); + +// 控制台初始化 +Console::init(); \ No newline at end of file diff --git a/依赖.rar b/依赖.rar new file mode 100644 index 0000000..3b7bd07 Binary files /dev/null and b/依赖.rar differ diff --git a/微信健康分规则v2.md b/微信健康分规则v2.md new file mode 100644 index 0000000..90c63f0 --- /dev/null +++ b/微信健康分规则v2.md @@ -0,0 +1,58 @@ +# 微信健康分规则 v2 + +## 一、定义 + +当客户收到手机设备后,登录了微信号,我们将对其微信号进行健康分的评估。 + +**健康分 = 基础分 + 动态分** + +健康分只与系统中的"每日自动添加好友次数"这个功能相关联。\ +通过健康分体系来定义一个微信号每日**最佳、最稳定的添加次数**。\ +后期还可将健康分作为标签属性,用于快速筛选微信号。 + +**公式:每日最大加人次数 = 健康分 × 0.2** + +## 二、基础分 + +基础分为 **60--100 分**。 + +由 `60 + 40(基础加成分)` 四个维度参数组成,每个参数具有不同权重。 + +### 基础分组成 + + 类型 权重 分数 + ------------ ------ ------ + 基础信息 0.2 10 + 好友数量 0.3 30 + 默认基础分 --- 60 + +### 1. 基础信息(权重 0.2,满分 10) + + 类型 权重 分数 + -------------- ------ ------ + 已修改微信号 1 10 + +### 2. 好友数量(权重 0.3,满分 30) + + 好友数量范围 权重 分数 + -------------- ------ ------ + 0--50 0.1 3 + 51--500 0.2 6 + 501--3000 0.3 8 + 3001 以上 0.4 12 + +## 三、动态分规则 + +### 扣分规则 + + 场景 扣分 处罚 + ---------- ------ -------------- + 首次频繁 15 暂停 24 小时 + 再次频繁 25 暂停 24 小时 + 封号 60 暂停 72 小时 + +### 加分规则 + + 场景 加分 + --------------------- ------ + 连续 3 天不触发频繁 5/日