Compare commits

...

316 Commits

Author SHA1 Message Date
kuaifan
c0a0f34ff4 build 2024-11-25 04:00:32 +08:00
kuaifan
e983677e57 no message 2024-11-25 03:29:11 +08:00
kuaifan
a813809fc6 build 2024-11-23 09:06:19 +08:00
kuaifan
f28b99b516 fix: 修复退出群组不完全的问题 2024-11-23 09:03:35 +08:00
kuaifan
bab37530e4 perf: 优化会话成员列表查询 2024-11-23 00:11:24 +08:00
kuaifan
fb24c63e7f perf: 重复添加任务的情况 2024-11-22 23:35:10 +08:00
kuaifan
65b8e2270e perf: 重复添加任务列表的情况 2024-11-22 23:31:02 +08:00
kuaifan
bfb2db8a3f no message 2024-11-22 23:23:21 +08:00
kuaifan
fe8deb98a2 perf: 优化消息样式 2024-11-22 23:11:27 +08:00
kuaifan
cee2458370 no message 2024-11-22 17:43:00 +08:00
kuaifan
764bf6dd55 fix: 修复退出还能收到推送的情况 2024-11-22 17:23:54 +08:00
kuaifan
88aee1e3bf fix: 修复账号被禁用之后还能收到推送和邮件 2024-11-22 16:50:13 +08:00
kuaifan
bcc7d6d35c build 2024-11-21 21:21:21 +08:00
kuaifan
f3f0dec87b no message 2024-11-21 21:11:35 +08:00
kuaifan
bf4c4df939 fix: 任务首次聊天发表情失败的情况 2024-11-21 21:02:36 +08:00
kuaifan
51efb07c17 perf: 优化websocket消息 2024-11-21 20:02:02 +08:00
kuaifan
20e13ee9eb no message 2024-11-21 17:10:46 +08:00
kuaifan
b1b4ef926f perf: 优化快捷选择 2024-11-21 16:02:53 +08:00
kuaifan
43e6b4dc2f perf: 延期任务支持快选时间 2024-11-21 15:24:48 +08:00
kuaifan
906d87f43f no message 2024-11-21 09:30:08 +08:00
kuaifan
ec8d48292e perf: 优化消息阅读机制 2024-11-20 20:44:53 +08:00
kuaifan
1882a7baba build 2024-11-20 16:59:23 +08:00
kuaifan
d4cccbeb09 fix: AI聊天缺少最后一句话的情况 2024-11-20 16:57:35 +08:00
kuaifan
1f187ba8fb fix: 文件打包下载 2024-11-20 16:35:11 +08:00
kuaifan
9c4ff466a4 perf: 新增文件打包下载权限设置 2024-11-20 16:17:30 +08:00
kuaifan
e5c3cf6adb perf: 升级electron框架 2024-11-20 15:42:37 +08:00
kuaifan
02fd214b33 perf: 优化深色主题 2024-11-20 15:35:48 +08:00
kuaifan
1ddb88a3a6 perf: 优化表情包资源 2024-11-20 09:03:43 +08:00
kuaifan
6edd4451c5 no message 2024-11-19 21:33:03 +08:00
kuaifan
1e83c7442a perf: 优化客户端子窗口 2024-11-19 20:32:04 +08:00
kuaifan
91533c5cac perf: 优化项目列表 2024-11-19 19:45:14 +08:00
kuaifan
a2ee1135dd perf: 优化录制语音消息 2024-11-19 18:39:52 +08:00
kuaifan
0ec255ed60 perf: 优化任务内容 2024-11-19 18:39:24 +08:00
kuaifan
a19d11061f perf: 优化任务提交添加继续 2024-11-19 11:53:04 +08:00
kuaifan
a730c95492 perf: 移动端审批窗口点击人员头像直接进入会话 2024-11-19 11:35:10 +08:00
kuaifan
89a50fd389 no message 2024-11-19 11:22:00 +08:00
kuaifan
82a340d576 perf: 新增系统别名设置 2024-11-19 11:19:37 +08:00
kuaifan
c952da669c no message 2024-11-19 10:09:19 +08:00
kuaifan
24cec7016a build 2024-11-19 01:42:09 +08:00
kuaifan
2c407bea78 no message 2024-11-18 23:50:20 +08:00
kuaifan
fba98db7cb fix: 任务内容保存后图片消失的情况 2024-11-18 23:25:42 +08:00
kuaifan
00eb8f7b01 fix: 修复上传超大尺寸图片 2024-11-18 22:36:23 +08:00
kuaifan
1055daa0e3 perf: 优化窗口加载速度 2024-11-18 21:56:04 +08:00
kuaifan
928145214d perf: 优化窗口加载速度 2024-11-18 21:33:23 +08:00
kuaifan
56e52a7dfd perf: 优化国际化 2024-11-18 17:15:01 +08:00
kuaifan
479d3e3f39 no message 2024-11-18 16:59:54 +08:00
kuaifan
ad3e773f27 perf: 优化图片上传 2024-11-18 15:09:22 +08:00
kuaifan
42c77db1d4 perf: 优化用户在线状态 2024-11-18 13:33:32 +08:00
kuaifan
11ea2d3697 perf: 优化小屏幕登录页 2024-11-18 12:15:00 +08:00
kuaifan
d0b54ab27c perf: 优化本地资源 2024-11-18 11:35:38 +08:00
kuaifan
5b9b6ed966 perf: 优化本地资源 2024-11-18 10:50:06 +08:00
kuaifan
fc192891b7 build 2024-11-18 00:59:44 +08:00
kuaifan
14f54e9df4 perf: 优化iOS上传图片颠倒的问题 2024-11-17 23:59:55 +08:00
kuaifan
07a290dbf9 perf: 优化桌面端通知图标 2024-11-17 23:35:31 +08:00
kuaifan
694f9a37a5 perf: 优化资源预取 2024-11-17 17:14:26 +08:00
kuaifan
13e58c63f4 perf: 优化emoji表情回复的判断 2024-11-17 13:22:50 +08:00
kuaifan
67c79bf565 perf: 更新office组件 2024-11-17 11:25:55 +08:00
kuaifan
428b72ef3d perf: 优化审批功能 2024-11-17 10:56:50 +08:00
kuaifan
b78d92b387 perf: 优化客户端升级 2024-11-16 11:16:16 +08:00
kuaifan
a09f2038ee perf: 优化客户端升级 2024-11-15 11:58:47 +08:00
kuaifan
fbb74e09e8 perf: 优化客户端升级 2024-11-15 09:59:41 +08:00
kuaifan
ca1028921a perf: 优化客户端升级 2024-11-15 00:23:11 +08:00
kuaifan
0fd37e4c05 build 2024-11-14 22:04:18 +08:00
kuaifan
f3d9e3376e perf: 优化客户端升级 2024-11-14 21:46:52 +08:00
kuaifan
9296008ecc perf: 优化客户端升级 2024-11-14 20:09:27 +08:00
kuaifan
ee7a1bd99c perf: 优化客户端升级 2024-11-14 17:33:54 +08:00
kuaifan
21eab03684 perf: 优化客户端升级 2024-11-14 16:34:48 +08:00
kuaifan
da066e40ce perf: 优化客户端升级 2024-11-14 14:19:50 +08:00
kuaifan
a219b7b6ee perf: 优化客户端升级 2024-11-14 13:55:01 +08:00
kuaifan
85c4ed6399 perf: 优化客户端升级 2024-11-14 13:03:16 +08:00
kuaifan
fa42194d15 perf: 优化客户端升级 2024-11-14 12:53:18 +08:00
kuaifan
e574e728d4 perf: 优化客户端升级 2024-11-14 11:40:38 +08:00
kuaifan
2ca35e4458 perf: 优化客户端升级 2024-11-14 00:33:44 +08:00
kuaifan
99027858d9 perf: 优化客户端 2024-11-13 01:07:23 +08:00
kuaifan
e7fcb47e81 perf: 优化签到错误提示 2024-11-12 21:16:12 +08:00
kuaifan
02d6dcd592 perf: 优化图片选择器 2024-11-12 21:15:49 +08:00
kuaifan
6e0a575da9 no message 2024-11-12 20:05:29 +08:00
kuaifan
93387c289e no message 2024-11-12 19:52:19 +08:00
kuaifan
1227a05e2d perf: 优化邮件通知 2024-11-12 19:52:12 +08:00
kuaifan
9f00047fdd perf: 优化本地资源 2024-11-12 12:34:45 +08:00
kuaifan
9bc3e56c79 perf: 修复iOS下载中文名乱码的问题 2024-11-12 10:42:40 +08:00
kuaifan
508aaef303 build 2024-11-12 00:22:45 +08:00
kuaifan
efd44a5da1 no message 2024-11-11 23:37:19 +08:00
kuaifan
0c70613865 perf: 优化初始化数据 2024-11-11 23:31:20 +08:00
kuaifan
6fda0bd548 no message 2024-11-11 17:49:46 +08:00
kuaifan
77224c3726 perf: 优化一处定位签到的问题 2024-11-11 17:11:54 +08:00
kuaifan
f25d72e4f5 perf: 升级海豚表情包 2024-11-11 17:11:08 +08:00
kuaifan
34603ff96e fix: iOS16-无法打开定位签到的问题 2024-11-11 12:11:01 +08:00
kuaifan
812232b945 perf: 优化新窗口链接打开逻辑 2024-11-11 09:50:15 +08:00
kuaifan
bd7228a378 perf: 添加会议机器人快捷菜单 2024-11-11 09:49:53 +08:00
kuaifan
ab61715973 build 2024-11-11 00:53:13 +08:00
kuaifan
095f461cfd perf: 优化使用默认浏览器打开链接 2024-11-11 00:06:19 +08:00
kuaifan
047771e6f8 no message 2024-11-10 23:09:07 +08:00
kuaifan
e2cec420fa no message 2024-11-10 14:11:48 +08:00
kuaifan
35e55b8677 perf: 添加定位签到 2024-11-10 13:11:30 +08:00
kuaifan
1b0ec71d93 perf: 优化打开会议 2024-11-10 12:20:53 +08:00
kuaifan
c6c735bbe8 perf: 优化打开会议 2024-11-10 11:00:56 +08:00
kuaifan
d5bc7d4051 perf: 优化打开会话逻辑 2024-11-10 08:32:47 +08:00
kuaifan
74405f1a2a perf: 添加定位签到 2024-11-10 08:19:10 +08:00
kuaifan
016bc41180 no message 2024-11-09 09:47:22 +08:00
kuaifan
e5df3e6746 perf: 添加定位签到 2024-11-09 09:39:03 +08:00
kuaifan
13fb884387 perf: 添加定位签到 2024-11-09 08:35:14 +08:00
kuaifan
3b9c9872ca perf: 添加定位签到 2024-11-08 21:46:07 +08:00
kuaifan
2fc329a403 dev: 优化开发环境 2024-11-08 10:45:23 +08:00
kuaifan
8ca1ef3b50 fix: 翻译聊天内容参数错误 2024-11-08 10:45:09 +08:00
kuaifan
f7dd9f852f build 2024-11-07 23:10:15 +08:00
kuaifan
4a9ed730c6 no message 2024-11-07 23:07:37 +08:00
kuaifan
a023c0b8bf fix: 无法打开项目的情况 2024-11-07 22:26:21 +08:00
kuaifan
ff38be3187 no message 2024-11-07 20:58:04 +08:00
kuaifan
9ffb2de2c8 feat: 添加定位签到 2024-11-07 18:36:55 +08:00
kuaifan
dcd87f86f1 perf: 优化从审批点击头像发起会话 2024-11-07 09:06:13 +08:00
kuaifan
d149c16713 fix: 搜索特殊字符报错的情况 2024-11-07 08:53:47 +08:00
kuaifan
1d99022ca3 build 2024-11-07 01:55:28 +08:00
kuaifan
bc85da49e3 perf: 图片浏览 2024-11-07 01:44:42 +08:00
kuaifan
18e1240775 build 2024-11-06 23:29:07 +08:00
kuaifan
e149e276d5 perf: 优化会话搜索 2024-11-06 23:22:27 +08:00
kuaifan
02654c8327 perf: 优化国际化语言 2024-11-06 20:36:00 +08:00
kuaifan
dace1dd1f3 perf: 优化消息已读逻辑 2024-11-06 20:16:46 +08:00
kuaifan
c46fd080df perf: 优化国际化语言 2024-11-05 22:43:58 +08:00
kuaifan
ef2230a331 no message 2024-11-05 15:13:40 +08:00
kuaifan
2ecd0584aa build 2024-11-05 11:30:54 +08:00
kuaifan
65c398880b perf: 优化app新版本提示 2024-11-05 10:27:57 +08:00
kuaifan
5962a593da no message 2024-11-04 20:29:36 +08:00
kuaifan
67baddf7a8 perf: 优化文字头像 2024-11-04 20:11:18 +08:00
kuaifan
ceb4fc8292 perf: 优化修改任务load 2024-11-04 11:41:04 +08:00
kuaifan
c51135a4cc fix: 修复会话内加载待办为空的情况 2024-11-03 21:58:37 +08:00
kuaifan
b2b4f593ce build 2024-11-03 09:07:32 +08:00
kuaifan
a95504bbf1 perf: 优化预览消息 2024-11-03 08:23:41 +08:00
kuaifan
6ed0e14fe0 perf: 优化移动端输入法换行 2024-11-03 08:17:25 +08:00
kuaifan
257e69268b fix: 无法清理数据缓存的情况 2024-11-03 07:35:24 +08:00
kuaifan
7e951196bf no message 2024-11-02 11:00:52 +08:00
kuaifan
501872e8d2 perf: 审批消息预览图片 2024-11-02 10:50:20 +08:00
kuaifan
87e46ec5a5 perf: 删除冗余字段 2024-11-02 10:01:42 +08:00
kuaifan
ebe953cf63 perf: 优化索引 2024-11-02 09:44:47 +08:00
kuaifan
cbfcdbf836 perf: 优化国际化语言 2024-11-02 09:40:10 +08:00
kuaifan
bd15915648 perf: 优化预览消息 2024-11-02 08:21:29 +08:00
kuaifan
312acdab51 perf: 优化预览消息 2024-11-01 21:18:04 +08:00
kuaifan
4ba9cc88dd perf: 优化会话查询 2024-11-01 19:20:35 +08:00
kuaifan
239013a2cb perf: 优化国际化语言 2024-11-01 10:53:51 +08:00
kuaifan
85412ea4b7 no message 2024-10-31 23:29:28 +08:00
kuaifan
cfda858d87 fix: 目录拼错的情况 2024-10-31 23:07:13 +08:00
kuaifan
df8fdd56ba build 2024-10-31 20:44:13 +08:00
kuaifan
698d03f77e fix: 设置子任务时间主任务出现1970的情况 2024-10-31 19:42:41 +08:00
kuaifan
3e15a3341c no message 2024-10-31 19:26:02 +08:00
kuaifan
d8a25e75d7 perf: 优化国际化语言 2024-10-31 16:39:08 +08:00
kuaifan
42f69124aa fix: 消息溢出的情况 2024-10-30 20:28:31 +08:00
kuaifan
621726ab3b feat: 消息翻译支持切换语言 2024-10-30 20:22:35 +08:00
kuaifan
cce7523f45 perf: 审批支持点击头像进入私聊 2024-10-30 16:03:18 +08:00
kuaifan
5e6a62376a no message 2024-10-30 15:45:37 +08:00
kuaifan
b03fb9f1de perf: 优化删除临时文件 2024-10-30 15:41:33 +08:00
kuaifan
1a7591314f perf: 优化缩略图 2024-10-30 14:14:58 +08:00
kuaifan
b8852f821c perf: 优化缩略图 2024-10-30 13:57:38 +08:00
kuaifan
6ebca3befa no message 2024-10-30 12:53:14 +08:00
kuaifan
8db34c6ee6 perf: 优化缩略图 2024-10-30 12:50:35 +08:00
kuaifan
d799c06017 perf: 优化缩略图 2024-10-30 09:37:54 +08:00
kuaifan
50a7950ccd no message 2024-10-29 19:02:23 +08:00
kuaifan
a393dec0a0 perf: 优化缩略图 2024-10-29 19:02:11 +08:00
kuaifan
423aad4179 build 2024-10-28 22:57:17 +08:00
kuaifan
80d10051cf no message 2024-10-28 21:18:24 +08:00
kuaifan
b1776c82ad perf: 优化长按消息菜单显示逻辑 2024-10-28 21:18:24 +08:00
kuaifan
36f313380e no message 2024-10-28 21:18:24 +08:00
kuaifan
7df9c37850 perf: 优化会话全屏输入功能菜单固定下方 2024-10-28 21:18:24 +08:00
kuaifan
9001c51bea perf: 优化聊天输入时页面乱滚动的情况 2024-10-28 21:18:24 +08:00
kuaifan
99757fc947 no message 2024-10-28 21:18:24 +08:00
kuaifan
8e9ff1116a perf: 优化导出统计国际化 2024-10-28 21:18:24 +08:00
kuaifan
f1df4e07d2 no message 2024-10-28 21:18:24 +08:00
kuaifan
3e3799074a perf: 优化会话全屏输入功能菜单固定下方 2024-10-28 21:18:24 +08:00
kuaifan
ae0ee590e4 perf: 支持会员选择窗标题省略号点击查看全标题 2024-10-28 21:18:24 +08:00
kuaifan
988a9b0606 perf: 任务内容加载太久显示load 2024-10-28 21:18:24 +08:00
kuaifan
7a457e4364 perf: 任务日志显示子任务关联 2024-10-28 21:18:24 +08:00
kuaifan
2edbe4fb3f fix: 无法设置修改子任务时间的情况 2024-10-28 21:18:24 +08:00
kuaifan
8eaff830ad perf: 审批评论图片浏览可滑动连续查看 2024-10-28 21:18:24 +08:00
kuaifan
7fdc7a47e3 perf: 审批评论优化显示缩略图 2024-10-28 21:18:24 +08:00
kuaifan
0e821d1c84 perf: 任务变化通知加上任务标题 2024-10-28 21:18:24 +08:00
kuaifan
c23de08cf5 perf: 新任务提醒区分协助还是负责 2024-10-28 21:18:24 +08:00
kuaifan
a6acb7ea0d perf: 优化审批通知标题 2024-10-28 21:18:24 +08:00
kuaifan
0c64cf0546 faet: 新增文本消息长按翻译功能 2024-10-28 21:18:24 +08:00
kuaifan
a4a9ab8d2d perf: 优化审批通知标题 2024-10-28 21:18:23 +08:00
kuaifan
19a1ae9bec perf: 优化推送预览 2024-10-28 21:18:23 +08:00
kuaifan
36cb8290f4 perf: 优化md标题样式 2024-10-28 21:18:23 +08:00
kuaifan
244991e8e8 perf: 新增查看更新日志 2024-10-28 21:18:23 +08:00
gwok
d6a7c19cbf fix: 判断广告页逻辑错误 2024-10-28 21:13:30 +08:00
gwok
64906a827d fix: 下载页控制台报错处理 2024-10-28 21:05:07 +08:00
gwok
da53306a2c style: 推广页样式调整 2024-10-28 18:21:20 +08:00
kuaifan
48515d7caf build 2024-10-25 00:18:53 +08:00
kuaifan
1f6ef62499 perf: 优化国际化、优化显示 2024-10-24 23:42:20 +08:00
kuaifan
6b4b88aba7 no message 2024-10-24 12:31:51 +08:00
kuaifan
fadff146b4 build 2024-10-24 12:12:23 +08:00
kuaifan
01feacfe54 no message 2024-10-24 11:48:44 +08:00
kuaifan
d6ddc5ff88 perf: 优化人脸签到设置 2024-10-24 10:55:58 +08:00
kuaifan
287b6b396d perf: 优化消息搜索速度 2024-10-24 07:32:31 +08:00
kuaifan
b976f294f9 perf: 优化显示 2024-10-23 18:38:32 +08:00
kuaifan
dce48bd0cb fix: 周报默认内容已完成工作负责人不显示的情况 2024-10-23 16:31:53 +08:00
kuaifan
ab84235890 no message 2024-10-23 16:14:42 +08:00
kuaifan
7445ac3a39 perf: 优化图片压缩 2024-10-23 15:37:00 +08:00
kuaifan
f9ceb3e2d8 perf: 优化显示 2024-10-23 11:30:48 +08:00
kuaifan
8bb7b60055 perf: 优化显示 2024-10-23 10:56:02 +08:00
kuaifan
190211a467 no message 2024-10-23 00:06:20 +08:00
kuaifan
8a6868e811 perf: 优化显示 2024-10-22 22:37:12 +08:00
kuaifan
6aa868c8d8 fix: 无法清除计划时间 2024-10-22 21:52:21 +08:00
kuaifan
4dfa1c8efc fix: 选择时间起始不正确的问题 2024-10-22 21:32:58 +08:00
kuaifan
e2e7bc8d72 fix: 修复iOS日历无法正常显示的情况 2024-10-22 20:39:09 +08:00
kuaifan
a97d78bbf4 no message 2024-10-22 20:12:58 +08:00
kuaifan
22dbd288df perf: 优化cmd命令 2024-10-22 14:54:57 +08:00
kuaifan
4685cdcd3c fix: 签到信息预览错误 2024-10-22 14:51:02 +08:00
kuaifan
f792b3d983 build 2024-10-22 12:45:29 +08:00
kuaifan
adc94cef90 no message 2024-10-22 11:27:30 +08:00
kuaifan
e639cfbc2f perf: 优化显示效果 2024-10-22 11:27:24 +08:00
kuaifan
e520cd9020 build 2024-10-22 00:23:13 +08:00
kuaifan
daf8d15f45 perf: 升级onlyoffice 2024-10-21 22:40:50 +08:00
kuaifan
0e473ceacc no message 2024-10-21 20:19:04 +08:00
kuaifan
873bd0ed88 fix: 推送失败的情况 2024-10-21 19:05:24 +08:00
kuaifan
58b7853d63 perf: 优化人脸签到功能 2024-10-21 18:03:23 +08:00
kuaifan
2284788366 Merge commit '814a488801b328daf67f86c33ac422704303dceb' into pro
# Conflicts:
#	app/Http/Controllers/Api/SystemController.php
#	public/site/en/about.html
#	public/site/en/download.html
#	public/site/en/help.html
#	public/site/en/index.html
#	public/site/en/log.html
#	public/site/en/price.html
#	public/site/en/product.html
#	public/site/en/solutions.html
#	public/site/zh/about.html
#	public/site/zh/download.html
#	public/site/zh/help.html
#	public/site/zh/index.html
#	public/site/zh/log.html
#	public/site/zh/price.html
#	public/site/zh/product.html
#	public/site/zh/solutions.html
#	resources/mobile
2024-10-21 14:14:45 +08:00
kuaifan
d1766e52b6 Merge commit 'e3f5fb323ad00b6804acf265eec1fc5040d05a81' into pro 2024-10-21 13:33:44 +08:00
kuaifan
fdd5e36d19 no message 2024-10-21 12:53:03 +08:00
kuaifan
4fe4dc8c6e pref: 优化加载通讯录数量 2024-10-20 19:53:11 +08:00
kuaifan
a3202cbead no message 2024-10-20 18:39:18 +08:00
kuaifan
e8b03ae565 fix: 导出签到数据快速选择时间 2024-10-20 01:53:52 +08:00
kuaifan
829e3982d2 no message 2024-10-20 01:53:20 +08:00
kuaifan
07c5f586b0 no message 2024-10-19 23:49:28 +08:00
kuaifan
2ebaeb3453 no message 2024-10-19 22:13:44 +08:00
kuaifan
5660be12f6 no message 2024-10-19 17:15:19 +08:00
kuaifan
3cd00e1343 no message 2024-10-19 11:09:17 +08:00
kuaifan
f983146501 fix: 搜索区域无法回车搜索的问题 2024-10-18 22:10:48 +08:00
kuaifan
6cf64ce538 perf: 优化继续添加任务数据处理 2024-10-18 22:01:20 +08:00
kuaifan
47a7876505 fix: 未领任务提醒机器人无须加入项目 2024-10-18 22:00:15 +08:00
kuaifan
3f5ac55753 perf: 优化翻译 2024-10-18 21:58:55 +08:00
kuaifan
a33d95f2c1 perf: 更新gpt的一些模型 2024-10-18 15:44:14 +08:00
kuaifan
1128db184e no message 2024-10-17 14:03:51 +08:00
kuaifan
153fd6c569 perf: 优化消息组件 2024-10-17 13:06:55 +08:00
kuaifan
c9d002c1cd perf: 优化消息组件 2024-10-16 18:40:53 +08:00
kuaifan
e0a108eb2e perf: 优化消息组件 2024-10-16 14:49:33 +08:00
kuaifan
ae587950b9 perf: 优化消息组件 2024-10-15 21:23:42 +08:00
kuaifan
e956a03098 no message 2024-10-14 16:58:40 +08:00
kuaifan
1702aab538 no message 2024-10-14 16:24:47 +08:00
kuaifan
3c67b49d08 perf: 优化后端翻译 2024-10-11 16:49:45 +08:00
kuaifan
d58246b255 perf: 优化创建任务提示时间冲突的逻辑 2024-10-11 16:03:07 +08:00
yijixx
814a488801 docs: 更新docker-compose 2024-10-11 14:04:42 +08:00
yijixx
e029b39eb9 perf: 人脸打卡配置 2024-10-11 11:33:01 +08:00
yijixx
a8361299c7 perf: 签到设置保存 2024-10-11 11:14:13 +08:00
gwok
e3f5fb323a fix:修复导出签到数据中这个月和上个月时间显示不准确的问题 2024-10-08 11:24:50 +08:00
yijixx
be262c3a69 perf: 签到设备显示 2024-10-08 09:32:52 +08:00
kuaifan
a4525d4519 fix: 日历中总是显示时间相差一个月 2024-10-03 09:27:58 +08:00
yijixx
4f6034457f perf: 打卡标签页 2024-09-30 18:34:42 +08:00
yijixx
5413457b6b feat: 支持人脸打卡设备 2024-09-29 17:31:15 +08:00
gwokwong
977cf61b50 feat:推广页点击联系我们展示企业微信二维码 2024-09-24 17:12:26 +08:00
gwokwong
8c8c5b04d5 feat:新增推广页 2024-09-24 12:54:27 +08:00
kuaifan
620465d62a no message 2024-09-23 09:48:08 +03:00
kuaifan
a80e0d4c45 build 2024-09-23 01:37:55 +03:00
kuaifan
0ab6e6ca8d perf: 优化翻译 2024-09-23 01:34:06 +03:00
kuaifan
dcd41b4be2 perf: 优化时间组件 2024-09-22 23:19:03 +03:00
kuaifan
33cd9358c0 perf: 优化时间组件 2024-09-22 17:27:54 +03:00
kuaifan
51a3ad25d1 perf: 优化时间组件 2024-09-22 15:39:02 +03:00
kuaifan
f586938fe9 perf: 优化时间组件 2024-09-22 15:21:54 +03:00
kuaifan
912d229bdd perf: 优化时间组件 2024-09-22 11:16:50 +03:00
kuaifan
a93345afbd perf: 优化时间组件 2024-09-22 00:43:48 +03:00
kuaifan
a7bd0e0dac perf: 优化时间组件 2024-09-20 21:46:28 +03:00
kuaifan
e2fd37fe24 perf: 优化日历样式 2024-09-19 23:22:57 +03:00
kuaifan
6e6397fc91 no message 2024-09-19 15:55:55 +08:00
kuaifan
45c20dbed9 fix: 修复表情回应一处报错 2024-09-19 06:58:17 +08:00
kuaifan
594c19da03 perf: 手机端消息菜单居中 2024-09-19 06:47:16 +08:00
kuaifan
9251ccbb12 perf: 优化数据库外部访问方式 2024-09-19 06:33:17 +08:00
kuaifan
34305a1285 fix: 任务到期时间不变颜色 2024-09-19 00:42:57 +08:00
kuaifan
ccc60dfd77 perf: 优化消息选择文本 2024-09-18 23:17:18 +08:00
kuaifan
b7da689955 fix: 聊天提及内容错位的情况 2024-09-18 18:56:14 +08:00
kuaifan
0598a36b19 perf: 优化表情搜索 2024-09-18 18:55:17 +08:00
kuaifan
947e106f19 fix: 首次修改任务时间不提示时间冲突的问题 2024-09-18 17:38:14 +08:00
kuaifan
81957c9396 fix: 添加任务时不设置时间无须提示任务冲突 2024-09-18 17:13:24 +08:00
kuaifan
d54c86cec9 perf: 任务描述再次点击隐藏菜单 2024-09-18 13:24:24 +08:00
kuaifan
c17eca28fa fix: 负责人修改后不显示在仪表盘的情况 2024-09-17 00:02:26 +08:00
kuaifan
9a69f3b019 no message 2024-09-16 23:31:19 +08:00
kuaifan
c39fc80c02 fix: 添加任务选择今天时间无效的情况 2024-09-16 10:09:42 +03:00
kuaifan
b0eead121a perf: 优化工作包括模板 2024-09-16 09:43:41 +03:00
kuaifan
511b98ab5b perf: 仪表盘任务列表支持折叠 2024-09-16 08:51:26 +03:00
weifs
a69b01ecf5 fix: 修复url-token登录异常问题 2024-09-13 14:01:27 +08:00
kuaifan
a967493d77 perf: 新增修改任务时间权限 2024-09-13 06:41:20 +03:00
kuaifan
050c9702d8 perf: 优化子任务读取失败 2024-09-13 05:40:44 +03:00
spylecym
0d23b973de feat: 官网侧边导航按钮新增谷歌分析事件追踪 2024-09-06 16:44:16 +08:00
spylecym
fc3170369b feat: 官网侧边导航按钮新增谷歌分析事件追踪 2024-09-06 10:35:00 +08:00
kuaifan
647f7fdc7d build 2024-09-01 19:55:57 +08:00
kuaifan
8c3cd379a2 no message 2024-09-01 19:52:41 +08:00
wfs
cf9051412a fix: 修复任务可见性为非项目人员时项目负责人不可见的bug 2024-09-01 15:53:21 +08:00
wfs
6db0ff5647 Merge branch 'pro2' into pro
# Conflicts:
#	public/site/js/googleAds.js
#	public/site/js/googleAnalyze.js
2024-09-01 15:00:49 +08:00
spylecym
9ce127df86 feat: 网页右下角导航改为点击显示以及手机端点击拨打电话直接拨号 2024-08-26 17:24:28 +08:00
spylecym
20eec62fde feat: 网页右下角导航改为点击显示以及手机端点击拨打电话直接拨号 2024-08-22 11:30:30 +08:00
wfs
effc8ce43f feat: 更改审批版本 2024-08-14 01:10:22 +08:00
wfs
ced25e0cd2 perf: 1.优化审批流程-审批人审核过后自动通过 2. 优化审批评论图片可以左右滑动查看 2024-08-14 00:24:49 +08:00
wfs
72c70fe494 Merge branch 'pro2' into pro
# Conflicts:
#	public/site/en/about.html
#	public/site/en/cookie.html
#	public/site/en/download.html
#	public/site/en/help.html
#	public/site/en/index.html
#	public/site/en/log.html
#	public/site/en/price.html
#	public/site/en/privacy.html
#	public/site/en/product.html
#	public/site/en/solutions.html
#	public/site/zh/about.html
#	public/site/zh/cookie.html
#	public/site/zh/download.html
#	public/site/zh/help.html
#	public/site/zh/index.html
#	public/site/zh/log.html
#	public/site/zh/price.html
#	public/site/zh/privacy.html
#	public/site/zh/product.html
#	public/site/zh/solutions.html
#	resources/mobile
2024-08-13 21:39:57 +08:00
spylecym
dc062a44e1 fix: 修改谷歌分析以及谷歌推广文件命名 2024-08-13 15:06:45 +08:00
spylecym
dff22272b5 fix: 修改谷歌分析代码 2024-08-13 14:18:59 +08:00
spylecym
a0a1e03b53 fix: 修改谷歌分析代码 2024-08-13 11:51:47 +08:00
spylecym
3915c065fe feat: 官网新增谷歌分析代码 2024-08-12 10:06:44 +08:00
spylecym
dc71a779e0 fix: 删除打印 2024-08-12 10:06:44 +08:00
spylecym
b56ba93634 feat: 页面新增谷歌分析 2024-08-12 10:06:44 +08:00
spylecym
2926472f7d fix: 修改关于我们页面公司介绍文案 2024-08-12 10:06:44 +08:00
spylecym
a253d42f10 feat: 官网新增谷歌分析代码 2024-08-12 09:12:30 +08:00
spylecym
700d566255 fix: 删除打印 2024-08-09 16:42:26 +08:00
spylecym
fbbace90aa feat: 页面新增谷歌分析 2024-08-09 16:41:27 +08:00
kuaifan
6b5f7e780c build 2024-08-06 21:16:27 +08:00
kuaifan
79d4932bee perf: 优化聊天视频预览 2024-08-06 18:45:08 +08:00
kuaifan
e8af0f2ea6 fix: 无法下载大文件 2024-08-06 18:24:49 +08:00
kuaifan
f1ecf33ce7 perf: 优化打包下载 2024-08-06 13:23:28 +08:00
kuaifan
18eaf56ff9 perf: 支持上传mov、webm视频 2024-08-06 13:19:25 +08:00
spylecym
75f15ccc96 fix: 修改关于我们页面公司介绍文案 2024-06-13 16:51:27 +08:00
weifs
3f17e91f72 Merge branch 'pro' of github.com:hitosea/dootask into pro
# Conflicts:
#	resources/mobile
2024-06-06 09:37:01 +08:00
kuaifan
ee6eddf308 build 2024-06-04 22:12:51 +08:00
kuaifan
da84f15e9f no message 2024-06-04 21:08:10 +08:00
kuaifan
62f4d43bd9 perf: 通讯录菜单添加会议 2024-06-04 20:19:14 +08:00
kuaifan
376120b6d0 perf: 优化文件里预览图片 2024-06-04 20:11:48 +08:00
kuaifan
ff872c7dce perf: 优化消息描述 2024-06-04 19:50:12 +08:00
kuaifan
a834481d32 fix: 切换对话之后无法通过右键@ 2024-06-04 19:33:38 +08:00
kuaifan
4c5d3bd43e build 2024-06-03 19:39:37 +08:00
weifs
b737b841f5 Merge branch 'kuaifan-pro' into pro 2024-06-03 19:30:25 +08:00
weifs
0c5500edd4 Merge branch 'kuaifan-pro' into pro 2024-06-03 15:18:06 +08:00
weifs
990a40e4e4 feat: 操作人员离职对okr的移交处理 2024-06-03 15:14:19 +08:00
642 changed files with 60365 additions and 34927 deletions

View File

@@ -1,55 +0,0 @@
name: Publish Android
on:
push:
tags:
- 'v*'
jobs:
Build:
name: Build Android
runs-on: ubuntu-latest
environment: build
if: startsWith(github.event.ref, 'refs/tags/v')
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Use Node.js 20.x
uses: actions/setup-node@v1
with:
node-version: 20.x
- name: Build Js
run: |
git submodule init
git submodule update --remote "resources/mobile"
./cmd appbuild publish
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '11'
- name: Build Android
run: |
cd resources/mobile/platforms/android/eeuiApp
chmod +x ./gradlew
./gradlew assembleRelease --quiet
- name: Upload File
env:
DP_KEY: ${{ secrets.DP_KEY }}
run: |
node ./electron/build.js android-upload
- name: Release
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
with:
files: |
resources/mobile/platforms/android/eeuiApp/app/build/outputs/apk/release/*.apk

View File

@@ -1,35 +0,0 @@
name: Publish Mac
on:
push:
tags:
- 'v*'
jobs:
Build:
name: Build Mac
runs-on: macos-12
environment: build
if: startsWith(github.event.ref, 'refs/tags/v')
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Use Node.js 20.x
uses: actions/setup-node@v1
with:
node-version: 20.x
- name: Build
env:
APPLEID: ${{ secrets.APPLEID }}
APPLEIDPASS: ${{ secrets.APPLEIDPASS }}
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
DP_KEY: ${{ secrets.DP_KEY }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GH_REPOSITORY: ${{ github.repository }}
run: |
./cmd electron mac

View File

@@ -1,31 +0,0 @@
name: Publish Win
on:
push:
tags:
- 'v*'
jobs:
Build:
name: Build Windows
runs-on: windows-latest
environment: build
if: startsWith(github.event.ref, 'refs/tags/v')
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Use Node.js 20.x
uses: actions/setup-node@v1
with:
node-version: 20.x
- name: Build
env:
DP_KEY: ${{ secrets.DP_KEY }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GH_REPOSITORY: ${{ github.repository }}
shell: bash
run: |
./cmd electron win

261
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,261 @@
name: "Publish"
on:
push:
branches:
- "pro"
- "dev"
jobs:
check-version:
permissions:
contents: read
runs-on: ubuntu-latest
outputs:
should_release: ${{ steps.check-tag.outputs.should_release }}
version: ${{ steps.get-version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Get version from package.json
id: get-version
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Check if tag exists
id: check-tag
run: |
VERSION=${{ steps.get-version.outputs.version }}
if git ls-remote --tags origin | grep -q "refs/tags/v${VERSION}$"; then
echo "This version v${VERSION} has been released"
echo "should_release=false" >> $GITHUB_OUTPUT
else
echo "Version v${VERSION} has not been released, continue building"
echo "should_release=true" >> $GITHUB_OUTPUT
fi
create-release:
needs: check-version
if: needs.check-version.outputs.should_release == 'true'
permissions:
contents: write
runs-on: ubuntu-latest
outputs:
release_id: ${{ steps.create-release.outputs.result }}
steps:
- uses: actions/checkout@v4
- name: Create Release
id: create-release
uses: actions/github-script@v7
with:
script: |
// 获取最新的 tag
const { data: tags } = await github.rest.repos.listTags({
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 1
});
// 获取提交日志
let changelog = '';
if (tags.length > 0) {
const { data: commits } = await github.rest.repos.compareCommits({
owner: context.repo.owner,
repo: context.repo.repo,
base: tags[0].name,
head: 'HEAD'
});
// 按类型分组提交
const groups = {
'feat:': { title: '## Features', commits: new Set() },
'fix:': { title: '## Bug Fixes', commits: new Set() },
'perf:': { title: '## Performance Improvements', commits: new Set() }
};
// 分类收集提交,使用 Set 去重
commits.commits.forEach(commit => {
const message = commit.commit.message.split('\n')[0].trim();
for (const [prefix, group] of Object.entries(groups)) {
if (message.startsWith(prefix)) {
// 移除前缀后添加到对应分组
const cleanMessage = message.slice(prefix.length).trim();
group.commits.add(cleanMessage); // 使用 Set.add 自动去重
break;
}
}
});
// 生成更新日志
const sections = [];
for (const group of Object.values(groups)) {
if (group.commits.size > 0) {
sections.push(`${group.title}\n\n${Array.from(group.commits).map(msg => `- ${msg}`).join('\n')}`);
}
}
if (sections.length > 0) {
changelog = '# Changelog\n\n' + sections.join('\n\n');
}
}
// 创建 release
const { data } = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: `v${{ needs.check-version.outputs.version }}`,
name: `${{ needs.check-version.outputs.version }}`,
body: changelog || 'No significant changes in this release.',
draft: true,
prerelease: false
})
return data.id
build-client:
needs: [ check-version, create-release ]
if: needs.check-version.outputs.should_release == 'true'
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- platform: "macos-latest"
build_type: "mac"
- platform: "ubuntu-latest"
build_type: "android"
- platform: "windows-latest"
build_type: "windows"
runs-on: ${{ matrix.platform }}
environment: build
steps:
- uses: actions/checkout@v4
- name: Use Node.js 20.x
uses: actions/setup-node@v4
with:
node-version: 20.x
# Android 构建步骤
- name: (Android) Build Js
if: matrix.build_type == 'android'
uses: nick-fields/retry@v2
with:
timeout_minutes: 10
max_attempts: 3
command: |
git submodule init
git submodule update --remote "resources/mobile"
./cmd appbuild publish
- name: (Android) Setup JDK 11
if: matrix.build_type == 'android'
uses: actions/setup-java@v3
with:
distribution: "zulu"
java-version: "11"
- name: (Android) Build App
if: matrix.build_type == 'android'
uses: nick-fields/retry@v2
with:
timeout_minutes: 20
max_attempts: 5
command: |
cd resources/mobile/platforms/android/eeuiApp
chmod +x ./gradlew
./gradlew assembleRelease --quiet
- name: (Android) Upload File
if: matrix.build_type == 'android'
env:
PUBLISH_KEY: ${{ secrets.PUBLISH_KEY }}
run: |
node ./electron/build.js android-upload
- name: (Android) Upload Release
if: matrix.build_type == 'android'
uses: actions/github-script@v7
env:
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
with:
script: |
const fs = require('fs');
const path = require('path');
const globby = require('globby');
// 查找 APK 文件
const files = await globby('resources/mobile/platforms/android/eeuiApp/app/build/outputs/apk/release/*.apk');
for (const file of files) {
const data = await fs.promises.readFile(file);
await github.rest.repos.uploadReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: process.env.RELEASE_ID,
name: path.basename(file),
data: data
});
}
# Mac 构建步骤
- name: (Mac) Build Client
if: matrix.build_type == 'mac'
env:
APPLEID: ${{ secrets.APPLEID }}
APPLEIDPASS: ${{ secrets.APPLEIDPASS }}
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
PUBLISH_KEY: ${{ secrets.PUBLISH_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
./cmd electron mac
# Windows 构建步骤
- name: (Windows) Build Client
if: matrix.build_type == 'windows'
env:
PUBLISH_KEY: ${{ secrets.PUBLISH_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
shell: bash
run: |
./cmd electron win
publish-release:
needs: [ check-version, create-release, build-client ]
if: needs.check-version.outputs.should_release == 'true' && github.ref == 'refs/heads/pro'
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Publish Release
uses: actions/github-script@v7
env:
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
with:
script: |
github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: process.env.RELEASE_ID,
draft: false,
prerelease: false
})
- name: Publish Official
env:
PUBLISH_KEY: ${{ secrets.PUBLISH_KEY }}
run: |
node ./electron/build.js published

127
.prefetch
View File

@@ -1,27 +1,110 @@
office/web-apps/apps/api/documents/api.js?hash={version}
office/7.5.1-23/sdkjs/cell/css/main.css
office/7.5.1-23/web-apps/apps/spreadsheeteditor/main/resources/css/app.css
office/7.5.1-23/web-apps/vendor/requirejs/require.js
office/7.5.1-23/web-apps/apps/spreadsheeteditor/main/app.js
office/7.5.1-23/sdkjs/common/AllFonts.js
office/7.5.1-23/web-apps/vendor/xregexp/xregexp-all-min.js
office/7.5.1-23/web-apps/vendor/socketio/socket.io.min.js
office/7.5.1-23/sdkjs/cell/sdk-all-min.js
office/7.5.1-23/sdkjs/cell/sdk-all.js
office/7.5.1-23/sdkjs/common/libfont/engine/fonts.js
office/7.5.1-23/sdkjs/common/Charts/ChartStyles.js
office/7.5.1-23/sdkjs/common/libfont/engine/fonts.wasm
office/7.5.1-23/web-apps/apps/presentationeditor/main/resources/css/app.css
office/7.5.1-23/web-apps/apps/presentationeditor/main/app.js
office/7.5.1-23/sdkjs/slide/sdk-all-min.js
office/7.5.1-23/sdkjs/slide/sdk-all.js
office/7.5.1-23/sdkjs/slide/themes//themes.js
office/7.5.1-23/web-apps/apps/documenteditor/main/resources/css/app.css
office/7.5.1-23/web-apps/apps/documenteditor/main/app.js
office/7.5.1-23/sdkjs/word/sdk-all-min.js
office/7.5.1-23/sdkjs/word/sdk-all.js
office/{path}/fonts/000
office/{path}/fonts/020
office/{path}/fonts/020
office/{path}/fonts/020
office/{path}/fonts/022
office/{path}/fonts/022
office/{path}/fonts/022
office/{path}/fonts/023
office/{path}/fonts/023
office/{path}/fonts/023
office/{path}/fonts/024
office/{path}/fonts/024
office/{path}/fonts/024
office/{path}/fonts/027
office/{path}/fonts/027
office/{path}/fonts/028
office/{path}/fonts/028
office/{path}/fonts/029
office/{path}/fonts/029
office/{path}/fonts/030
office/{path}/fonts/030
office/{path}/fonts/036
office/{path}/fonts/036
office/{path}/fonts/037
office/{path}/fonts/037
office/{path}/fonts/038
office/{path}/fonts/038
office/{path}/fonts/039
office/{path}/fonts/039
office/{path}/fonts/058
office/{path}/fonts/058
office/{path}/fonts/058
office/{path}/fonts/059
office/{path}/fonts/059
office/{path}/fonts/059
office/{path}/fonts/060
office/{path}/fonts/060
office/{path}/fonts/060
office/{path}/fonts/061
office/{path}/fonts/061
office/{path}/fonts/061
office/{path}/fonts/063
office/{path}/fonts/065
office/{path}/fonts/066
office/{path}/fonts/081
office/{path}/fonts/081
office/{path}/fonts/081
office/{path}/fonts/138
office/{path}/fonts/184
office/{path}/fonts/184
office/{path}/sdkjs/cell/sdk-all-min.js
office/{path}/sdkjs/cell/sdk-all.js
office/{path}/sdkjs/common/AllFonts.js
office/{path}/sdkjs/common/AllFonts.js
office/{path}/sdkjs/common/AllFonts.js
office/{path}/sdkjs/common/Charts/ChartStyles.js
office/{path}/sdkjs/common/Charts/ChartStyles.js
office/{path}/sdkjs/common/Charts/ChartStyles.js
office/{path}/sdkjs/common/Images/fonts_thumbnail_ea@2x.png.bin
office/{path}/sdkjs/common/Images/fonts_thumbnail_ea@2x.png.bin
office/{path}/sdkjs/common/Images/fonts_thumbnail_ea@2x.png.bin
office/{path}/sdkjs/common/libfont/engine/fonts.js
office/{path}/sdkjs/common/libfont/engine/fonts.js
office/{path}/sdkjs/common/libfont/engine/fonts.js
office/{path}/sdkjs/common/libfont/engine/fonts.wasm
office/{path}/sdkjs/common/libfont/engine/fonts.wasm
office/{path}/sdkjs/common/libfont/engine/fonts.wasm
office/{path}/sdkjs/slide/sdk-all-min.js
office/{path}/sdkjs/slide/sdk-all.js
office/{path}/sdkjs/word/sdk-all-min.js
office/{path}/sdkjs/word/sdk-all.js
office/{path}/web-apps/apps/documenteditor/main/app.js
office/{path}/web-apps/apps/documenteditor/main/code.js
office/{path}/web-apps/apps/documenteditor/main/locale/zh.json
office/{path}/web-apps/apps/documenteditor/main/resources/css/app.css
office/{path}/web-apps/apps/documenteditor/main/resources/img/iconssmall@2.5x.svg
office/{path}/web-apps/apps/presentationeditor/main/app.js
office/{path}/web-apps/apps/presentationeditor/main/code.js
office/{path}/web-apps/apps/presentationeditor/main/locale/zh.json
office/{path}/web-apps/apps/presentationeditor/main/resources/css/app.css
office/{path}/web-apps/apps/presentationeditor/main/resources/img/iconsbig@2.5x.svg
office/{path}/web-apps/apps/presentationeditor/main/resources/img/iconsbig@2x.png
office/{path}/web-apps/apps/presentationeditor/main/resources/img/iconssmall@2.5x.svg
office/{path}/web-apps/apps/spreadsheeteditor/main/app.js
office/{path}/web-apps/apps/spreadsheeteditor/main/code.js
office/{path}/web-apps/apps/spreadsheeteditor/main/locale/zh.json
office/{path}/web-apps/apps/spreadsheeteditor/main/resources/css/app.css
office/{path}/web-apps/apps/spreadsheeteditor/main/resources/formula-lang/zh_desc.json
office/{path}/web-apps/apps/spreadsheeteditor/main/resources/img/iconssmall@2.5x.svg
office/{path}/web-apps/apps/spreadsheeteditor/main/resources/img/iconssmall@2x.png
office/{path}/web-apps/vendor/xregexp/xregexp-all-min.js
office/{path}/web-apps/vendor/xregexp/xregexp-all-min.js
office/{path}/web-apps/vendor/xregexp/xregexp-all-min.js
drawio/webapp/js/app.min.js
drawio/webapp/js/stencils.min.js
drawio/webapp/js/extensions.min.js
drawio/webapp/js/shapes-14-6-5.min.js
drawio/webapp/js/stencils.min.js
drawio/webapp/math/es5/core.js
drawio/webapp/math/es5/input/asciimath.js
drawio/webapp/math/es5/input/tex.js
drawio/webapp/math/es5/output/svg.js
drawio/webapp/math/es5/output/svg/fonts/tex.js
drawio/webapp/styles/grapheditor.css
minder/css/chunk-vendors.fe9c56c6.css
minder/js/app.aa385de3.js
minder/js/chunk-vendors.cc7455b8.js

View File

@@ -2,6 +2,338 @@
All notable changes to this project will be documented in this file.
## [0.40.78]
### Bug Fixes
- 修复退出群组不完全的问题
- 修复退出还能收到推送的情况
- 修复账号被禁用之后还能收到推送和邮件
- 任务首次聊天发表情失败的情况
- AI聊天缺少最后一句话的情况
- 文件打包下载
### Performance
- 优化会话成员列表查询
- 重复添加任务的情况
- 重复添加任务列表的情况
- 优化消息样式
- 优化websocket消息
- 优化快捷选择
- 延期任务支持快选时间
- 优化消息阅读机制
- 新增文件打包下载权限设置
- 升级electron框架
- 优化深色主题
- 优化表情包资源
- 优化客户端子窗口
- 优化项目列表
- 优化录制语音消息
- 优化任务内容
- 优化任务提交添加继续
- 移动端审批窗口点击人员头像直接进入会话
- 新增系统别名设置
## [0.40.40]
### Bug Fixes
- 任务内容保存后图片消失的情况
- 修复上传超大尺寸图片
### Performance
- 优化窗口加载速度
- 优化国际化
- 优化图片上传
- 优化用户在线状态
- 优化小屏幕登录页
- 优化本地资源
- 优化iOS上传图片颠倒的问题
- 优化桌面端通知图标
- 优化资源预取
- 优化emoji表情回复的判断
- 更新office组件
- 优化审批功能
- 优化客户端升级
- 优化客户端
- 优化签到错误提示
- 优化图片选择器
- 优化邮件通知
- 修复iOS下载中文名乱码的问题
## [0.39.97]
### Bug Fixes
- IOS16-无法打开定位签到的问题
### Performance
- 优化初始化数据
- 优化一处定位签到的问题
- 升级海豚表情包
- 优化新窗口链接打开逻辑
- 添加会议机器人快捷菜单
## [0.39.88]
### Bug Fixes
- 翻译聊天内容参数错误
### Performance
- 优化使用默认浏览器打开链接
- 添加定位签到
- 优化打开会议
- 优化打开会话逻辑
## [0.39.73]
### Bug Fixes
- 无法打开项目的情况
- 搜索特殊字符报错的情况
### Features
- 添加定位签到
### Performance
- 优化从审批点击头像发起会话
## [0.39.66]
### Bug Fixes
- 修复会话内加载待办为空的情况
### Performance
- 图片浏览
- 优化会话搜索
- 优化国际化语言
- 优化消息已读逻辑
- 优化app新版本提示
- 优化文字头像
- 优化修改任务load
## [0.39.52]
### Bug Fixes
- 无法清理数据缓存的情况
### Performance
- 优化预览消息
- 优化移动端输入法换行
- 审批消息预览图片
- 删除冗余字段
- 优化索引
- 优化国际化语言
- 优化会话查询
## [0.39.39]
### Bug Fixes
- 目录拼错的情况
- 设置子任务时间主任务出现1970的情况
- 消息溢出的情况
### Features
- 消息翻译支持切换语言
### Performance
- 优化国际化语言
- 审批支持点击头像进入私聊
- 优化删除临时文件
- 优化缩略图
## [0.39.21]
### Bug Fixes
- 无法设置修改子任务时间的情况
- 判断广告页逻辑错误
- 下载页控制台报错处理
### Performance
- 优化长按消息菜单显示逻辑
- 优化会话全屏输入功能菜单固定下方
- 优化聊天输入时页面乱滚动的情况
- 优化导出统计国际化
- 支持会员选择窗标题省略号点击查看全标题
- 任务内容加载太久显示load
- 任务日志显示子任务关联
- 审批评论图片浏览可滑动连续查看
- 审批评论优化显示缩略图
- 任务变化通知加上任务标题
- 新任务提醒区分协助还是负责
- 优化审批通知标题
- 优化推送预览
- 优化md标题样式
- 新增查看更新日志
### Styling
- 推广页样式调整
## [0.38.94]
### Performance
- 优化国际化、优化显示
## [0.38.91]
### Bug Fixes
- 周报默认内容已完成工作负责人不显示的情况
- 无法清除计划时间
- 选择时间起始不正确的问题
- 修复iOS日历无法正常显示的情况
- 签到信息预览错误
### Performance
- 优化人脸签到设置
- 优化消息搜索速度
- 优化显示
- 优化图片压缩
- 优化cmd命令
## [0.38.73]
### Performance
- 优化显示效果
## [0.38.70]
### Bug Fixes
- 推送失败的情况
- 导出签到数据快速选择时间
- 搜索区域无法回车搜索的问题
- 未领任务提醒机器人无须加入项目
- 日历中总是显示时间相差一个月
### Documentation
- 更新docker-compose
### Features
- 支持人脸打卡设备
### Performance
- 升级onlyoffice
- 优化人脸签到功能
- 优化加载通讯录数量
- 优化继续添加任务数据处理
- 优化翻译
- 更新gpt的一些模型
- 优化消息组件
- 优化后端翻译
- 优化创建任务提示时间冲突的逻辑
- 人脸打卡配置
- 签到设置保存
- 签到设备显示
- 打卡标签页
## [0.38.27]
### Bug Fixes
- 修复表情回应一处报错
- 任务到期时间不变颜色
- 聊天提及内容错位的情况
- 首次修改任务时间不提示时间冲突的问题
- 添加任务时不设置时间无须提示任务冲突
- 负责人修改后不显示在仪表盘的情况
- 添加任务选择今天时间无效的情况
- 修复url-token登录异常问题
### Features
- 官网侧边导航按钮新增谷歌分析事件追踪
### Performance
- 优化翻译
- 优化时间组件
- 优化日历样式
- 手机端消息菜单居中
- 优化数据库外部访问方式
- 优化消息选择文本
- 优化表情搜索
- 任务描述再次点击隐藏菜单
- 优化工作包括模板
- 仪表盘任务列表支持折叠
- 新增修改任务时间权限
- 优化子任务读取失败
## [0.37.98]
### Bug Fixes
- 修复任务可见性为非项目人员时项目负责人不可见的bug
- 修改谷歌分析以及谷歌推广文件命名
- 修改谷歌分析代码
- 删除打印
- 修改关于我们页面公司介绍文案
### Features
- 网页右下角导航改为点击显示以及手机端点击拨打电话直接拨号
- 更改审批版本
- 官网新增谷歌分析代码
- 页面新增谷歌分析
### Performance
- 1.优化审批流程-审批人审核过后自动通过 2. 优化审批评论图片可以左右滑动查看
## [0.37.76]
### Bug Fixes
- 无法下载大文件
- 修改关于我们页面公司介绍文案
### Performance
- 优化聊天视频预览
- 优化打包下载
- 支持上传mov、webm视频
## [0.37.71]
### Bug Fixes
- 切换对话之后无法通过右键@
### Performance
- 通讯录菜单添加会议
- 优化文件里预览图片
- 优化消息描述
## [0.37.65]
### Features
- 操作人员离职对okr的移交处理
## [0.37.62]
### Bug Fixes
@@ -517,6 +849,12 @@ All notable changes to this project will be documented in this file.
- 优化任务修改
## [0.33.37]
### Bug Fixes
- 更新导致的小问题
## [0.33.34]
### Bug Fixes

View File

@@ -88,7 +88,7 @@ cd dootask
./cmd redis "your command" # To run a redis command
./cmd composer "your command" # To run a composer command
./cmd supervisorctl "your command" # To run a supervisorctl command
./cmd mysql "your command" # To run a mysql command (backup: Backup database, recovery: Restore database)
./cmd mysql "your command" # To run a mysql command (backup: Backup database, recovery: Restore database, open: Open database external port access, close: Close database external port access)
```
### SSL configuration

View File

@@ -1,25 +0,0 @@
# 客户端说明
## 1、App客户端
#### 1.1、说明
目录 `resources/mobile`,使用`eeui.app`框架遵从eeui的开发文档进行打包开发app
#### 1.2、编译App
1. 在项目目录执行 `./cmd appbuild [build|setting]` 编译
2. 进入 `resources/mobile` eeui框架内打包Android或iOS应用
## 2、PC/Mac客户端
#### 2.1、说明
目录 `electron`,使用`electron`框架遵从electron的开发文档进行打包客户端
#### 2.2、编译客户端
在项目目录执行 `./cmd electron [dev]` 根据提示编译

View File

@@ -89,7 +89,7 @@ cd dootask
./cmd redis "your command" # 运行 redis 命令
./cmd composer "your command" # 运行 composer 命令
./cmd supervisorctl "your command" # 运行 supervisorctl 命令
./cmd mysql "your command" # 运行 mysql 命令 (backup: 备份数据库recovery: 还原数据库)
./cmd mysql "your command" # 运行 mysql 命令 (backup: 备份数据库recovery: 还原数据库open: 开启数据库外部端口访问close: 关闭数据库外部端口访问)
```
### SSL 配置

View File

@@ -1,26 +1,31 @@
# 发布说明
# 发布
## 发布前
## 准备工作
1. 添加环境变量 `APPLEID``APPLEIDPASS` 用于公证
2. 添加环境变量 `CSC_LINK``CSC_KEY_PASSWORD` 用于签名
3. 添加环境变量 `GH_TOKEN``GH_REPOSITORY` 用于发布到GitHub
4. 添加环境变量 `DP_KEY` 用于发布到私有服务器
3. 添加环境变量 `GITHUB_TOKEN``GITHUB_REPOSITORY` 用于发布到GitHubGitHub Actions 发布不需要)
4. 添加环境变量 `PUBLISH_KEY` 用于发布到私有服务器
## 通过 GitHub Actions 发布
## 发布版本
1. 执行 `npm run version` 生成版本
2. 执行 `npm run build` 编译前端
3. 执行 `git commit` 提交并推送
4. 添加并推送标签
```shell
npm run translate # 翻译(可选)
npm run version # 生成版本
npm run build # 编译前端
```
## 本地发布
说明:
1. 执行 `npm run version` 生成版本
2. 执行 `npm run build` 编译前端
3. 执行 `./cmd electron` 相关操作
- 执行 `npm run build` 作用是生成网页端;
- 客户端 (Windows、Mac、Android) 会通过 GitHub Actions 自动生成并发布所以如果要自动发布只需要提交git并推送即可
- 如果想手动生成客户端执行 `./cmd electron` 根据提示选择操作
## 编译App
1. 执行 `./cmd appbuild [setting]` 编译
2. 进入 `resources/mobile` eeui框架内打包Android或iOS应用
## 编译 App
```shell
./cmd appbuild publish # 编译生成App需要的资源
```
编译完后进入 `resources/mobile` EEUI框架目录内打包 Android 或 iOS 应用Android 以实现 GitHub Actions 自动发布)

View File

@@ -16045,7 +16045,7 @@
/**
*
*
* @see \Maatwebsite\Excel\Mixins\DownloadCollection::downloadExcel()
* @see \Maatwebsite\Excel\Mixins\DownloadCollectionMixin::downloadExcel()
* @param string $fileName
* @param string|null $writerType
* @param mixed $withHeadings
@@ -16059,7 +16059,7 @@
/**
*
*
* @see \Maatwebsite\Excel\Mixins\StoreCollection::storeExcel()
* @see \Maatwebsite\Excel\Mixins\StoreCollectionMixin::storeExcel()
* @param string $filePath
* @param string|null $disk
* @param string|null $writerType
@@ -16439,6 +16439,247 @@
}
}
namespace Laravolt\Avatar {
/**
*
*
*/
class Facade {
/**
*
*
* @static
*/
public static function setGenerator($generator)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->setGenerator($generator);
}
/**
*
*
* @static
*/
public static function create($name)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->create($name);
}
/**
*
*
* @static
*/
public static function applyTheme($config)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->applyTheme($config);
}
/**
*
*
* @static
*/
public static function addTheme($name, $config)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->addTheme($name, $config);
}
/**
*
*
* @static
*/
public static function toBase64()
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->toBase64();
}
/**
*
*
* @static
*/
public static function save($path, $quality = 90)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->save($path, $quality);
}
/**
*
*
* @static
*/
public static function toSvg()
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->toSvg();
}
/**
*
*
* @static
*/
public static function toGravatar($param = null)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->toGravatar($param);
}
/**
*
*
* @static
*/
public static function getInitial()
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->getInitial();
}
/**
*
*
* @static
*/
public static function getImageObject()
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->getImageObject();
}
/**
*
*
* @static
*/
public static function buildAvatar()
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->buildAvatar();
}
/**
*
*
* @static
*/
public static function getAttribute($key)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->getAttribute($key);
}
/**
*
*
* @static
*/
public static function setTheme($theme)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->setTheme($theme);
}
/**
*
*
* @static
*/
public static function setBackground($hex)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->setBackground($hex);
}
/**
*
*
* @static
*/
public static function setForeground($hex)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->setForeground($hex);
}
/**
*
*
* @static
*/
public static function setDimension($width, $height = null)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->setDimension($width, $height);
}
/**
*
*
* @static
*/
public static function setFontSize($size)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->setFontSize($size);
}
/**
*
*
* @static
*/
public static function setFontFamily($font)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->setFontFamily($font);
}
/**
*
*
* @static
*/
public static function setBorder($size, $color, $radius = 0)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->setBorder($size, $color, $radius);
}
/**
*
*
* @static
*/
public static function setBorderRadius($radius)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->setBorderRadius($radius);
}
/**
*
*
* @static
*/
public static function setShape($shape)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->setShape($shape);
}
/**
*
*
* @static
*/
public static function setChars($chars)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->setChars($chars);
}
/**
*
*
* @static
*/
public static function setFont($font)
{
/** @var \Laravolt\Avatar\Avatar $instance */
return $instance->setFont($font);
}
}
}
namespace Maatwebsite\Excel\Facades {
@@ -16467,9 +16708,10 @@
/**
*
*
* @param string|null $disk Fallback for usage with named properties
* @param object $export
* @param string $filePath
* @param string|null $disk
* @param string|null $diskName
* @param string $writerType
* @param mixed $diskOptions
* @return bool
@@ -16477,10 +16719,10 @@
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
* @static
*/
public static function store($export, $filePath, $diskName = null, $writerType = null, $diskOptions = [])
public static function store($export, $filePath, $diskName = null, $writerType = null, $diskOptions = [], $disk = null)
{
/** @var \Maatwebsite\Excel\Excel $instance */
return $instance->store($export, $filePath, $diskName, $writerType, $diskOptions);
return $instance->store($export, $filePath, $diskName, $writerType, $diskOptions, $disk);
}
/**
*
@@ -16698,7 +16940,7 @@
* @param $pathToFile string The file to open
* @param \Madnest\Madzipper\Repositories\RepositoryInterface|string $type The type of the archive, defaults to zip, possible are zip, phar
* @throws \RuntimeException
* @throws \Exception
* @throws Exception
* @throws \InvalidArgumentException
* @return \Madnest\Madzipper\Madzipper Madzipper instance
* @static
@@ -16712,7 +16954,7 @@
* Create a new zip archive or open an existing one.
*
* @param string $pathToFile
* @throws \Exception
* @throws Exception
* @return self
* @static
*/
@@ -16725,7 +16967,7 @@
* Create a new phar file or open one.
*
* @param string $pathToFile
* @throws \Exception
* @throws Exception
* @return self
* @static
*/
@@ -16738,7 +16980,7 @@
* Create a new rar file or open one.
*
* @param string $pathToFile
* @throws \Exception
* @throws Exception
* @return self
* @static
*/
@@ -16755,7 +16997,7 @@
* @param $path string The path to extract to
* @param array $files An array of files
* @param int $methodFlags The Method the files should be treated
* @throws \Exception
* @throws Exception
* @return void
* @static
*/
@@ -16782,7 +17024,7 @@
* Gets the content of a single file if available.
*
* @param $filePath string The full path (including all folders) of the file in the zip
* @throws \Exception
* @throws Exception
* @return mixed returns the content or throws an exception
* @static
*/
@@ -18774,6 +19016,64 @@ namespace {
/**
*
*
* @see \Maatwebsite\Excel\Mixins\DownloadQueryMacro::__invoke()
* @param string $fileName
* @param string|null $writerType
* @param mixed $withHeadings
* @static
*/
public static function downloadExcel($fileName, $writerType = null, $withHeadings = false)
{
return \Illuminate\Database\Eloquent\Builder::downloadExcel($fileName, $writerType, $withHeadings);
}
/**
*
*
* @see \Maatwebsite\Excel\Mixins\StoreQueryMacro::__invoke()
* @param string $filePath
* @param string|null $disk
* @param string|null $writerType
* @param mixed $withHeadings
* @static
*/
public static function storeExcel($filePath, $disk = null, $writerType = null, $withHeadings = false)
{
return \Illuminate\Database\Eloquent\Builder::storeExcel($filePath, $disk, $writerType, $withHeadings);
}
/**
*
*
* @see \Maatwebsite\Excel\Mixins\ImportMacro::__invoke()
* @param string $filename
* @param string|null $disk
* @param string|null $readerType
* @static
*/
public static function import($filename, $disk = null, $readerType = null)
{
return \Illuminate\Database\Eloquent\Builder::import($filename, $disk, $readerType);
}
/**
*
*
* @see \Maatwebsite\Excel\Mixins\ImportAsMacro::__invoke()
* @param string $filename
* @param callable $mapping
* @param string|null $disk
* @param string|null $readerType
* @static
*/
public static function importAs($filename, $mapping, $disk = null, $readerType = null)
{
return \Illuminate\Database\Eloquent\Builder::importAs($filename, $mapping, $disk, $readerType);
}
/**
*
*
* @see \App\Providers\AppServiceProvider::boot()
* @static
*/
@@ -20814,6 +21114,7 @@ namespace {
class View extends \Illuminate\Support\Facades\View {}
class Flare extends \Facade\Ignition\Facades\Flare {}
class Image extends \Intervention\Image\Facades\Image {}
class Avatar extends \Laravolt\Avatar\Facade {}
class Excel extends \Maatwebsite\Excel\Facades\Excel {}
class Madzipper extends \Madnest\Madzipper\Facades\Madzipper {}
class Captcha extends \Mews\Captcha\Facades\Captcha {}

View File

@@ -1,27 +0,0 @@
<?php
namespace App\Events;
use Hhxsv5\LaravelS\Swoole\Events\ServerStartInterface;
use Swoole\Http\Server;
class ServerStartEvent implements ServerStartInterface
{
public function __construct()
{
}
public function handle(Server $server)
{
$server->startMsecTime = $this->msecTime();
}
private function msecTime()
{
list($msec, $sec) = explode(' ', microtime());
$time = explode(".", $sec . ($msec * 1000));
return $time[0];
}
}

View File

@@ -3,7 +3,6 @@
namespace App\Events;
use App\Models\WebSocket;
use Cache;
use Hhxsv5\LaravelS\Swoole\Events\WorkerStartInterface;
use Swoole\Http\Server;
@@ -16,9 +15,15 @@ class WorkerStartEvent implements WorkerStartInterface
public function handle(Server $server, $workerId)
{
if (isset($server->startMsecTime) && Cache::get("swooleServerStartMsecTime") != $server->startMsecTime) {
Cache::forever("swooleServerStartMsecTime", $server->startMsecTime);
WebSocket::query()->delete();
// 仅在Worker进程启动时执行一次初始化代码
$initTable = app('swoole')->initFlagTable;
if ($initTable->incr('init_flag', 'value') === 1) {
$this->handleFirstWorkerTasks();
}
}
private function handleFirstWorkerTasks()
{
WebSocket::query()->delete();
}
}

View File

@@ -3,9 +3,11 @@
namespace App\Exceptions;
use App\Module\Base;
use App\Module\Image;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
class Handler extends ExceptionHandler
@@ -51,6 +53,11 @@ class Handler extends ExceptionHandler
*/
public function render($request, Throwable $e)
{
if ($e instanceof NotFoundHttpException) {
if ($result = $this->ImagePathHandler($request)) {
return $result;
}
}
if ($e instanceof ApiException) {
return response()->json(Base::retError($e->getMessage(), $e->getData(), $e->getCode()));
} elseif ($e instanceof ModelNotFoundException) {
@@ -78,4 +85,144 @@ class Handler extends ExceptionHandler
parent::report($e);
}
}
/**
* 图片路径处理
* @param $request
* @return \Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\BinaryFileResponse|null
*/
private function ImagePathHandler($request)
{
$path = $request->path();
// 处理图片
$patternCrop = '/^(uploads\/.*\.(png|jpg|jpeg))\/crop\/([^\/]+)$/';
$patternThumb = '/^(uploads\/.*)_thumb\.(png|jpg|jpeg)$/';
$matchesCrop = null;
$matchesThumb = null;
if (preg_match($patternCrop, $path, $matchesCrop) || preg_match($patternThumb, $path, $matchesThumb)) {
// 获取参数
if ($matchesCrop) {
$file = $matchesCrop[1];
$ext = $matchesCrop[2];
$rules = preg_replace('/\s+/', '', $matchesCrop[3]);
$rules = str_replace(['=', '&'], [':', ','], $rules);
$rules = explode(',', $rules);
} elseif ($matchesThumb) {
$file = $matchesThumb[1];
$ext = $matchesThumb[2];
$rules = ['percentage:320x0'];
} else {
return null;
}
if (empty($rules)) {
return null;
}
// 提取年月
$Ym = date("Ym");
if (preg_match('/\/(\d{6})\//', $file, $ms)) {
$Ym = $ms[1];
}
// 文件存在直接返回
$dirName = str_replace(['/', '.'], '_', $file);
$fileName = str_replace([':', ','], ['-', '_'], implode(',', $rules)) . '.' . $ext;
$savePath = public_path('uploads/tmp/crop/' . $Ym . '/' . $dirName . '/' . $fileName);
if (file_exists($savePath)) {
// 设置头部声明图片缓存
return response()->file($savePath, [
'Pragma' => 'public',
'Cache-Control' => 'max-age=1814400',
'Expires' => gmdate('D, d M Y H:i:s', time() + 1814400) . ' GMT',
'Last-Modified' => gmdate('D, d M Y H:i:s', filemtime($savePath)) . ' GMT',
'ETag' => md5_file($savePath)
]);
}
// 文件不存在处理
$sourcePath = public_path($file);
if (!file_exists($sourcePath)) {
return null;
}
// 判断删除多余文件
$saveDir = dirname($savePath);
if (is_dir($saveDir)) {
$items = glob($saveDir . '/*');
if (count($items) > 5) {
usort($items, function ($a, $b) {
return filemtime($b) - filemtime($a);
});
$itemsToDelete = array_slice($items, 5);
foreach ($itemsToDelete as $item) {
if (is_file($item)) {
unlink($item);
}
}
}
} else {
Base::makeDir($saveDir);
}
// 处理图片
try {
$handle = 0;
$image = new Image($sourcePath);
foreach ($rules as $rule) {
if (!str_contains($rule, ':')) {
continue;
}
[$type, $value] = explode(':', $rule);
if (!in_array($type, ['ratio', 'size', 'percentage', 'cover', 'contain'])) {
continue;
}
switch ($type) {
// 按比例裁剪
case 'ratio':
if (is_numeric($value)) {
$image->ratioCrop($value);
$handle++;
}
break;
// 按尺寸缩放
case 'size':
$size = Base::newIntval(explode('x', $value));
if (count($size) === 2) {
$image->resize($size[0], $size[1]);
$handle++;
}
break;
// 按尺寸缩放
case 'percentage':
case 'cover':
case 'contain':
$size = Base::newIntval(explode('x', $value));
if (count($size) === 2) {
$image->thumb($size[0], $size[1], $type);
$handle++;
}
break;
}
}
if ($handle > 0) {
$image->saveTo($savePath);
Image::compressImage($savePath, 80);
return response()->file($savePath, [
'Pragma' => 'public',
'Cache-Control' => 'max-age=1814400',
'Expires' => gmdate('D, d M Y H:i:s', time() + 1814400) . ' GMT',
'Last-Modified' => gmdate('D, d M Y H:i:s', filemtime($savePath)) . ' GMT',
'ETag' => md5_file($savePath)
]);
} else {
$image->destroy();
}
} catch (\ImagickException) { }
}
return null;
}
}

View File

@@ -3,7 +3,7 @@
if (!function_exists('asset_main')) {
function asset_main($path, $secure = null)
{
return preg_replace("/^https*:\/\//", "//", app('url')->asset($path, $secure));
return preg_replace("/^https?:\/\//", "//", app('url')->asset($path, $secure));
}
}

View File

@@ -9,6 +9,8 @@ use Madzipper;
use Carbon\Carbon;
use App\Models\User;
use App\Module\Base;
use App\Module\Doo;
use App\Module\Timer;
use App\Module\Ihttp;
use App\Tasks\PushTask;
use App\Module\BillExport;
@@ -37,8 +39,9 @@ class ApproveController extends AbstractController
/**
* @api {get} api/approve/verifyToken 01. 验证APi登录
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup users
* @apiGroup approve
* @apiName verifyToken
*
* @apiSuccess {String} version
@@ -262,7 +265,7 @@ class ApproveController extends AbstractController
$this->approveMsg('approve_reviewer', $dialog, $botUser, $val, $process, $pass);
}
// 发起人
if ($process['is_finished'] == true) {
if ($process['is_finished']) {
$dialog = WebSocketDialog::checkUserDialog($botUser, $process['start_user_id']);
if (!empty($dialog)) {
$this->approveMsg('approve_submitter', $dialog, $botUser, ['userid' => $data['userid']], $process, $pass);
@@ -284,7 +287,7 @@ class ApproveController extends AbstractController
}
// 抄送人
$notifier = $this->handleProcessNode($process, $task['step']);
$notifier = $this->handleProcessNode($process);
if ($notifier && $pass == 'pass') {
foreach ($notifier as $val) {
$dialog = WebSocketDialog::checkUserDialog($botUser, $val['target_id']);
@@ -756,7 +759,7 @@ class ApproveController extends AbstractController
if (empty($name) || empty($date)) {
return Base::retError('参数错误');
}
if (!(is_array($date) && Base::isDate($date[0]) && Base::isDate($date[1]))) {
if (!(is_array($date) && Timer::isDate($date[0]) && Timer::isDate($date[1]))) {
return Base::retError('日期选择错误');
}
if (Carbon::parse($date[1])->timestamp - Carbon::parse($date[0])->timestamp > 35 * 86400) {
@@ -772,32 +775,31 @@ class ApproveController extends AbstractController
$res = Base::arrayKeyToUnderline($process['data']);
//
$headings = [];
$headings[] = '申请编号';
$headings[] = '标题';
$headings[] = '申请状态';
$headings[] = '发起时间';
$headings[] = '完成时间';
$headings[] = '发起人工号';
$headings[] = '发起人User ID';
$headings[] = '发起人姓名';
$headings[] = '发起人部门';
$headings[] = '发起人部门ID';
$headings[] = '部门负责人';
$headings[] = '历史审批人';
$headings[] = '历史办理人';
$headings[] = '审批记录';
$headings[] = '当前处理人';
$headings[] = '审批节点';
$headings[] = '审批人数';
$headings[] = '审批耗时';
$headings[] = '假期类型';
$headings[] = '开始时间';
$headings[] = '结束时间';
$headings[] = '时长';
$headings[] = '请假事由';
$headings[] = '请假单位';
$headings[] = Doo::translate('申请编号');
$headings[] = Doo::translate('标题');
$headings[] = Doo::translate('申请状态');
$headings[] = Doo::translate('发起时间');
$headings[] = Doo::translate('完成时间');
$headings[] = Doo::translate('发起人工号');
$headings[] = Doo::translate('发起人User ID');
$headings[] = Doo::translate('发起人姓名');
$headings[] = Doo::translate('发起人部门');
$headings[] = Doo::translate('发起人部门ID');
$headings[] = Doo::translate('部门负责人');
$headings[] = Doo::translate('历史审批人');
$headings[] = Doo::translate('历史办理人');
$headings[] = Doo::translate('审批记录');
$headings[] = Doo::translate('当前处理人');
$headings[] = Doo::translate('审批节点');
$headings[] = Doo::translate('审批人数');
$headings[] = Doo::translate('审批耗时');
$headings[] = Doo::translate('假期类型');
$headings[] = Doo::translate('开始时间');
$headings[] = Doo::translate('结束时间');
$headings[] = Doo::translate('时长');
$headings[] = Doo::translate('请假事由');
$headings[] = Doo::translate('请假单位');
//
$sheets = [];
$datas = [];
foreach ($res as $val) {
//
@@ -816,12 +818,12 @@ class ApproveController extends AbstractController
// 计算审批耗时
$startTime = Carbon::parse($val['start_time'])->timestamp;
$endTime = $val['end_time'] ? Carbon::parse($val['end_time'])->timestamp : time();
$approval_time = Base::timeDiff($startTime, $endTime); // 审批耗时
$approval_time = Doo::translate(Timer::timeDiff($startTime, $endTime)); // 审批耗时
// 计算时长
$varStartTime = Carbon::parse($val['var']['start_time']);
$varEndTime = Carbon::parse($val['var']['end_time']);
$duration = $varEndTime->floatDiffInHours($varStartTime);
$duration_unit = '小时'; // 时长单位
$duration_unit = Doo::translate('小时'); // 时长单位
$datas[] = [
$val['id'], // 申请编号
$val['proc_def_name'], // 标题
@@ -853,13 +855,13 @@ class ApproveController extends AbstractController
return Base::retError('没有任何数据');
}
//
$title = "Sheet1";
$title = Doo::translate("审批记录");
$sheets = [
BillExport::create()->setTitle($title)->setHeadings($headings)->setData($datas)->setStyles(["A1:Y1" => ["font" => ["bold" => true]]])
];
//
$fileName = '审批记录_' . Base::time() . '.xlsx';
$filePath = "temp/approve/export/" . date("Ym", Base::time());
$fileName = $title . '_' . Timer::time() . '.xlsx';
$filePath = "temp/approve/export/" . date("Ym", Timer::time());
$export = new BillMultipleExport($sheets);
$res = $export->store($filePath . "/" . $fileName);
if ($res != 1) {
@@ -899,14 +901,14 @@ class ApproveController extends AbstractController
3 => '拒绝',
4 => '撤回'
);
return isset($state_map[$state]) ? $state_map[$state] : '';
return $state_map[$state] ?? '';
}
/**
* @api {get} api/approve/down 19. 下载导出的审批数据
*
* @apiVersion 1.0.0
* @apiGroup system
* @apiGroup approve
* @apiName down
*
* @apiParam {String} key 通过export接口得到的下载钥匙
@@ -966,7 +968,6 @@ class ApproveController extends AbstractController
return $res;
}
// 审批机器人消息
public function approveMsg($type, $dialog, $botUser, $toUser, $process, $action = null)
{
@@ -978,51 +979,71 @@ class ApproveController extends AbstractController
'department' => $process['department'],
'type' => $process['var']['type'],
'start_time' => $process['var']['start_time'],
'start_day_of_week' => '周' . Base::getTimeWeek(Carbon::parse($process['var']['start_time'])->timestamp),
'start_day_of_week' => '周' . Timer::getWeek(Carbon::parse($process['var']['start_time'])->timestamp),
'end_time' => $process['var']['end_time'],
'end_day_of_week' => '周' . Base::getTimeWeek(Carbon::parse($process['var']['end_time'])->timestamp),
'end_day_of_week' => '周' . Timer::getWeek(Carbon::parse($process['var']['end_time'])->timestamp),
'description' => $process['var']['description'],
'comment_nickname' => $process['comment_user_id'] ? User::userid2nickname($process['comment_user_id']) : '',
'comment_content' => $process['comment_contents']['content'] ?? '',
'comment_pictures' => $process['comment_contents']['pictures'] ?? []
];
$text = view('push.bot', ['type' => $type, 'action' => $action, 'is_finished' => $process['is_finished'], 'data' => (object)$data])->render();
$text = preg_replace("/^\x20+/", "", $text);
$text = preg_replace("/\n\x20+/", "\n", $text);
$msg_action = null;
$thumb = null;
if ($type === 'approve_reviewer') {
$thumb = $process['var']['other'];
} elseif ($type === 'approve_comment_notifier') {
$thumb = $data['comment_pictures'] ? $data['comment_pictures'][0] : null;
}
if ($thumb && file_exists(public_path($thumb))) {
$imageSize = getimagesize(public_path($thumb));
$data['thumb'] = [
'url' => $thumb,
'width' => $imageSize[0],
'height' => $imageSize[1]
];
}
$msgAction = null;
$msgData = [
'type' => $type,
'action' => $action,
'is_finished' => $process['is_finished'],
'data' => $data
];
$msgData['title'] = match ($type) {
'approve_reviewer' => $data['nickname'] . " 提交的「{$data['proc_def_name']}」待你审批",
'approve_notifier' => "抄送 {$data['nickname']} 提交的「{$data['proc_def_name']}」记录",
'approve_comment_notifier' => $data['comment_nickname'] . " 评论了 {$data['nickname']} 的「{$data['proc_def_name']}」审批",
'approve_submitter' => $action == 'pass' ? "您发起的「{$data['proc_def_name']}」已通过" : "您发起的「{$data['proc_def_name']}」被 {$data['nickname']} 拒绝",
default => '不支持的指令',
};
if ($action == 'withdraw' || $action == 'pass' || $action == 'refuse') {
// 任务完成,给发起人发送消息
if ($type == 'approve_submitter' && $action != 'withdraw') {
return WebSocketDialogMsg::sendMsg($msg_action, $dialog->id, 'text', ['text' => $text, 'approve_type' => $type], $botUser->userid, false, false, true);
return WebSocketDialogMsg::sendMsg($msgAction, $dialog->id, 'template', $msgData, $botUser->userid, false, false, true);
}
// 查找最后一条消息msg_id
$msg_action = 'change-' . $toUser['msg_id'];
$msgAction = 'change-' . $toUser['msg_id'];
}
//
try {
$msg = WebSocketDialogMsg::sendMsg($msg_action, $dialog->id, 'text', ['text' => $text, 'approve_type' => $type], $process['start_user_id'], false, false, true);
// 关联信息
if ($action == 'start') {
$proc_msg = new ApproveProcMsg();
$proc_msg->proc_inst_id = $process['id'];
$proc_msg->msg_id = $msg['data']->id;
$proc_msg->userid = $toUser['userid'];
$proc_msg->save();
}
// 更新工作报告 未读数量
if ($type == 'approve_reviewer' && $toUser['userid']) {
$params = [
'userid' => [$toUser['userid'], User::auth()->userid()],
'msg' => [
'type' => 'approve',
'action' => 'unread',
'userid' => $toUser['userid'],
]
];
Task::deliver(new PushTask($params, false));
}
} catch (\Throwable $th) {
//throw $th;
$msg = WebSocketDialogMsg::sendMsg($msgAction, $dialog->id, 'template', $msgData, $process['start_user_id'], false, false, true);
// 关联信息
if ($action == 'start') {
$proc_msg = new ApproveProcMsg();
$proc_msg->proc_inst_id = $process['id'];
$proc_msg->msg_id = $msg['data']->id;
$proc_msg->userid = $toUser['userid'];
$proc_msg->save();
}
// 更新审批 未读数量
if ($type == 'approve_reviewer' && $toUser['userid']) {
$params = [
'userid' => [$toUser['userid'], User::userid()],
'msg' => [
'type' => 'approve',
'action' => 'unread',
'userid' => $toUser['userid'],
]
];
Task::deliver(new PushTask($params, false));
}
return true;
}
@@ -1057,8 +1078,9 @@ class ApproveController extends AbstractController
}
}
// 全局评论
unset($res['global_comment']);
if (isset($res['global_comments'])) {
foreach ($res['global_comments'] as $k => &$globalComment) {
foreach ($res['global_comments'] as $k => $globalComment) {
$info = User::whereUserid($globalComment['user_id'])->first();
if (!$info) {
continue;
@@ -1066,6 +1088,8 @@ class ApproveController extends AbstractController
$res['global_comments'][$k]['userimg'] = User::getAvatar($info->userid, $info->userimg, $info->email, $info->nickname);
$res['global_comments'][$k]['nickname'] = $info->nickname;
}
} else {
$res['global_comments'] = [];
}
$info = User::whereUserid($res['start_user_id'])->first();
$res['userimg'] = $info ? User::getAvatar($info->userid, $info->userimg, $info->email, $info->nickname) : '';
@@ -1111,7 +1135,7 @@ class ApproveController extends AbstractController
* @api {get} api/approve/user/status 20. 获取用户审批状态
*
* @apiVersion 1.0.0
* @apiGroup system
* @apiGroup approve
* @apiName user__status
*
* @apiParam {String} userid
@@ -1126,7 +1150,7 @@ class ApproveController extends AbstractController
$ret = Ihttp::ihttp_get($this->flow_url . '/api/v1/workflow/process/getUserApprovalStatus?' . http_build_query($data));
$procdef = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (isset($procdef['status']) && $procdef['status'] == 200) {
return Base::retSuccess('success', isset($procdef['data']["proc_def_name"]) ? $procdef['data']["proc_def_name"] : '');
return Base::retSuccess('success', $procdef['data']["proc_def_name"] ?? '');
}
return Base::retSuccess('success', '');
}

View File

@@ -113,8 +113,11 @@ class ComplaintController extends AbstractController
->each(function ($adminUser) use ($reason, $botUser) {
$dialog = WebSocketDialog::checkUserDialog($botUser, $adminUser->userid);
if ($dialog) {
$text = "<p>收到新的举报信息:{$reason} (请前往应用查看详情)</p>";
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid); // todo 未能在任务end事件来发送任务
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'title' => '收到新的举报信息',
'content' => "收到新的举报信息:{$reason} (请前往应用查看详情)"
], $botUser->userid);
}
});
//

View File

@@ -2,15 +2,16 @@
namespace App\Http\Controllers\Api;
use App\Tasks\PushTask;
use DB;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Request;
use Redirect;
use Carbon\Carbon;
use App\Tasks\PushTask;
use App\Module\Doo;
use App\Models\File;
use App\Models\User;
use App\Module\Base;
use App\Module\Timer;
use App\Module\Extranet;
use App\Module\TimeRange;
use App\Models\FileContent;
@@ -20,6 +21,8 @@ use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogUser;
use App\Models\WebSocketDialogMsgRead;
use App\Models\WebSocketDialogMsgTodo;
use App\Models\WebSocketDialogMsgTranslate;
use Hhxsv5\LaravelS\Swoole\Task\Task;
/**
* @apiDefine dialog
@@ -82,13 +85,7 @@ class DialogController extends AbstractController
$unreadAt = Request::input('unread_at');
$todoAt = Request::input('todo_at');
//
$unreadAt = Base::isNumber($unreadAt) ? intval($unreadAt) : trim($unreadAt);
$unreadAt = Carbon::parse($unreadAt)->setTimezone(config('app.timezone'));
//
$todoAt = Base::isNumber($todoAt) ? intval($todoAt) : trim($todoAt);
$todoAt = Carbon::parse($todoAt)->setTimezone(config('app.timezone'));
//
$data = WebSocketDialog::getDialogBeyond($user->userid, $unreadAt, $todoAt);
$data = WebSocketDialog::getDialogBeyond($user->userid, Base::newCarbon($unreadAt), Base::newCarbon($todoAt));
//
return Base::retSuccess('success', $data);
}
@@ -116,18 +113,20 @@ class DialogController extends AbstractController
return Base::retError('请输入搜索关键词');
}
// 搜索会话
$dialogs = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->where('web_socket_dialogs.name', 'LIKE', "%{$key}%")
$list = DB::table('web_socket_dialog_users as u')
->select(['d.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
->join('web_socket_dialogs as d', 'u.dialog_id', '=', 'd.id')
->where('u.userid', $user->userid)
->where('d.name', 'LIKE', "%{$key}%")
->whereNull('d.deleted_at')
->orderByDesc('u.top_at')
->orderByDesc('u.last_at')
->take(20)
->get();
$dialogs->transform(function (WebSocketDialog $item) use ($user) {
return $item->formatData($user->userid);
});
$list = $dialogs->toArray();
->get()
->map(function($item) use ($user) {
return WebSocketDialog::synthesizeData($item, $user->userid);
})
->all();
// 搜索联系人
if (count($list) < 20 && Base::judgeClientVersion("0.21.60")) {
$users = User::select(User::$basicField)
@@ -140,31 +139,53 @@ class DialogController extends AbstractController
})->orderBy('userid')
->take(20 - count($list))
->get();
$users->transform(function (User $item) {
$users->transform(function (User $item) use ($user) {
$id = 'u:' . $item->userid;
$lastAt = null;
$lastMsg = null;
$dialog = WebSocketDialog::getUserDialog($user->userid, $item->userid, now()->addDay());
if ($dialog) {
$id = $dialog->id;
$row = WebSocketDialogMsg::whereDialogId($dialog->id)->orderByDesc('id')->first();
if ($row) {
$lastAt = Carbon::parse($row->created_at)->toDateTimeString();
$lastMsg = WebSocketDialog::lastMsgFormat($row->toArray());
}
}
return [
'id' => 'u:' . $item->userid,
'id' => $id,
'type' => 'user',
'name' => $item->nickname,
'dialog_user' => $item,
'last_msg' => null,
'last_at' => $lastAt,
'last_msg' => $lastMsg,
];
});
$list = array_merge($list, $users->toArray());
}
// 搜索消息会话
if (count($list) < 20) {
$msgs = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at', 'm.id as search_msg_id'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->join('web_socket_dialog_msgs as m', 'web_socket_dialogs.id', '=', 'm.dialog_id')
$prefix = DB::getTablePrefix();
if (preg_match('/[+\-><()~*"@]/', $key)) {
$against = "\"{$key}\"";
} else {
$against = "*{$key}*";
}
$msgs = DB::table('web_socket_dialog_users as u')
->select(['d.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at', 'm.id as search_msg_id'])
->join('web_socket_dialogs as d', 'u.dialog_id', '=', 'd.id')
->join('web_socket_dialog_msgs as m', 'm.dialog_id', '=', 'd.id')
->where('u.userid', $user->userid)
->where('m.key', 'LIKE', "%{$key}%")
->whereNull('d.deleted_at')
->whereRaw("MATCH({$prefix}m.key) AGAINST('{$against}' IN BOOLEAN MODE)")
->orderByDesc('m.id')
->take(20 - count($list))
->get();
$msgs->transform(function (WebSocketDialog $item) use ($user) {
return $item->formatData($user->userid);
});
$list = array_merge($list, $msgs->toArray());
->get()
->map(function($item) use ($user) {
return WebSocketDialog::synthesizeData($item, $user->userid);
})
->all();
$list = array_merge($list, $msgs);
}
//
return Base::retSuccess('success', $list);
@@ -186,19 +207,22 @@ class DialogController extends AbstractController
{
$user = User::auth();
// 搜索会话
$msgs = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at', 'm.id as search_msg_id'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->join('web_socket_dialog_msgs as m', 'web_socket_dialogs.id', '=', 'm.dialog_id')
$msgs = DB::table('web_socket_dialog_users as u')
->select(['d.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at', 'm.id as search_msg_id'])
->join('web_socket_dialogs as d', 'u.dialog_id', '=', 'd.id')
->join('web_socket_dialog_msgs as m', 'm.dialog_id', '=', 'd.id')
->where('u.userid', $user->userid)
->whereNull('d.deleted_at')
->where('m.tag', '>', 0)
->orderByDesc('m.id')
->take(50)
->get();
$msgs->transform(function (WebSocketDialog $item) use ($user) {
return $item->formatData($user->userid);
});
->get()
->map(function($item) use ($user) {
return WebSocketDialog::synthesizeData($item, $user->userid);
})
->all();
//
return Base::retSuccess('success', $msgs->toArray());
return Base::retSuccess('success', $msgs);
}
/**
@@ -221,16 +245,14 @@ class DialogController extends AbstractController
//
$dialog_id = intval(Request::input('dialog_id'));
//
$item = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->where('web_socket_dialogs.id', $dialog_id)
$item = DB::table('web_socket_dialog_users as u')
->select(['d.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
->join('web_socket_dialogs as d', 'u.dialog_id', '=', 'd.id')
->where('u.userid', $user->userid)
->where('d.id', $dialog_id)
->whereNull('d.deleted_at')
->first();
if (empty($item)) {
WebSocketDialogMsgRead::forceRead($dialog_id, $user->userid);
return Base::retError('会话不存在或已被删除', ['dialog_id' => $dialog_id], -4003);
}
return Base::retSuccess('success', $item->formatData($user->userid));
return Base::retSuccess('success', WebSocketDialog::synthesizeData($item, $user->userid));
}
/**
@@ -258,21 +280,15 @@ class DialogController extends AbstractController
$dialog = WebSocketDialog::checkDialog($dialog_id);
//
if ($getuser === 1) {
$data = $dialog->dialogUser->toArray();
$array = [];
foreach ($data as $item) {
$res = User::userid2basic($item['userid']);
if ($res) {
$array[] = array_merge($item, $res->toArray());
}
}
$array = array_filter($array, function ($item) {
$data = $dialog->dialogUserBuilder()->get();
$array = array_filter($data->toArray(), function ($item) {
return $item['userid'] > 0;
});
} else {
$data = WebSocketDialogUser::select(['web_socket_dialog_users.*', 'users.bot'])
->join('users', 'web_socket_dialog_users.userid', '=', 'users.userid')
->where('web_socket_dialog_users.dialog_id', $dialog_id)
->whereNull('users.disable_at')
->orderBy('web_socket_dialog_users.id')
->get();
$array = $data->toArray();
@@ -399,12 +415,11 @@ class DialogController extends AbstractController
if ($dialog->type !== 'user') {
return Base::retError("会话类型错误");
}
$dialogUser = $dialog->dialogUser->where('userid', '!=', $user->userid)->first();
$dialogUser = $dialog->dialogUserBuilder(['tel'])->where('users.userid', '!=', $user->userid)->first();
if (empty($dialogUser)) {
return Base::retError("会话对象不存在");
}
$callUser = User::find($dialogUser->userid);
if (empty($callUser) || empty($callUser->tel)) {
if (empty($dialogUser->tel)) {
return Base::retError("对方未设置联系电话");
}
if ($user->isTemp()) {
@@ -413,14 +428,14 @@ class DialogController extends AbstractController
//
$add = null;
$res = WebSocketDialogMsg::sendMsg(null, $dialog->id, 'notice', [
'notice' => $user->nickname . " 查看了 " . $callUser->nickname . " 的联系电话"
'notice' => $user->nickname . " 查看了 " . $dialogUser->nickname . " 的联系电话"
]);
if (Base::isSuccess($res)) {
$add = $res['data'];
}
//
return Base::retSuccess("success", [
'tel' => $callUser->tel,
'tel' => $dialogUser->tel,
'add' => $add ?: null
]);
}
@@ -452,7 +467,7 @@ class DialogController extends AbstractController
if (empty($dialog)) {
return Base::retError('打开会话失败');
}
$data = WebSocketDialog::find($dialog->id)?->formatData($user->userid);
$data = WebSocketDialog::synthesizeData($dialog->id, $user->userid);
return Base::retSuccess('success', $data);
}
@@ -570,7 +585,7 @@ class DialogController extends AbstractController
->value('id'));
}
$data['list'] = $list;
$data['time'] = Base::time();
$data['time'] = Timer::time();
// 记录当前打开的任务对话
if ($dialog->type == 'group' && $dialog->group_type == 'task') {
$user->task_dialog_id = $dialog->id;
@@ -584,8 +599,8 @@ class DialogController extends AbstractController
}
//
if ($reDialog) {
$data['dialog'] = $dialog->formatData($user->userid, true);
$data['todo'] = $data['dialog']->todo_num > 0 ? WebSocketDialogMsgTodo::whereDialogId($dialog->id)->whereUserid($user->userid)->whereDoneAt(null)->orderByDesc('id')->take(50)->get() : [];
$data['dialog'] = WebSocketDialog::synthesizeData($dialog, $user->userid, true);
$data['todo'] = $data['dialog']['todo_num'] > 0 ? WebSocketDialogMsgTodo::whereDialogId($dialog->id)->whereUserid($user->userid)->whereDoneAt(null)->orderByDesc('id')->take(50)->get() : [];
$data['top'] = $dialog->top_msg_id ? WebSocketDialogMsg::whereId($dialog->top_msg_id)->first() : null;
}
return Base::retSuccess('success', $data);
@@ -764,7 +779,7 @@ class DialogController extends AbstractController
*
* @apiParam {Object} id 消息ID
* - 1、多个ID用逗号分隔1,2,3
* - 2、另一种格式{"id": "[会话ID]"},如:{"2": 0, "3": 10}
* - 2、另一种格式{"id": "会话ID|0"},如:{"2": 0, "3": 10}
* -- 会话ID标记id之后的消息已读
* -- 其他:标记已读
*
@@ -811,13 +826,13 @@ class DialogController extends AbstractController
$dialogUser->updated_at = Carbon::now();
$dialogUser->save();
//
$dialogUser->webSocketDialog->generateUnread($user->userid);
$unreadData = WebSocketDialog::generateUnread($dialogUser->dialog_id, $user->userid);
$data[] = [
'id' => $dialogUser->webSocketDialog->id,
'unread' => $dialogUser->webSocketDialog->unread,
'unread_one' => $dialogUser->webSocketDialog->unread_one,
'mention' => $dialogUser->webSocketDialog->mention,
'mention_ids' => $dialogUser->webSocketDialog->mention_ids,
'id' => $dialogUser->dialog_id,
'unread' => $unreadData['unread'],
'unread_one' => $unreadData['unread_one'],
'mention' => $unreadData['mention'],
'mention_ids' => $unreadData['mention_ids'],
'user_at' => Carbon::parse($dialogUser->updated_at)->toDateTimeString('millisecond'),
'user_ms' => Carbon::parse($dialogUser->updated_at)->valueOf()
];
@@ -855,14 +870,14 @@ class DialogController extends AbstractController
if (empty($dialogUser?->webSocketDialog)) {
return Base::retError('会话不存在');
}
$dialogUser->webSocketDialog->generateUnread($dialogUser->userid);
$unreadData = WebSocketDialog::generateUnread($dialog_id, $dialogUser->userid);
//
return Base::retSuccess('success', [
'id' => $dialogUser->webSocketDialog->id,
'unread' => $dialogUser->webSocketDialog->unread,
'unread_one' => $dialogUser->webSocketDialog->unread_one,
'mention' => $dialogUser->webSocketDialog->mention,
'mention_ids' => $dialogUser->webSocketDialog->mention_ids,
'id' => $dialog_id,
'unread' => $unreadData['unread'],
'unread_one' => $unreadData['unread_one'],
'mention' => $unreadData['mention'],
'mention_ids' => $unreadData['mention_ids'],
'user_at' => Carbon::parse($dialogUser->updated_at)->toDateTimeString('millisecond'),
'user_ms' => Carbon::parse($dialogUser->updated_at)->valueOf()
]);
@@ -983,6 +998,7 @@ class DialogController extends AbstractController
*
* @apiParam {Number} dialog_id 对话ID
* @apiParam {String} text 消息内容
* @apiParam {String} [key] 搜索关键词 (不设置根据内容自动生成)
* @apiParam {String} [text_type] 消息类型
* - html: HTML默认
* - md: MARKDOWN
@@ -1010,6 +1026,7 @@ class DialogController extends AbstractController
$update_mark = !($user->bot && in_array(strtolower(trim(Request::input('update_mark'))), ['no', 'false', '0']));
$reply_id = intval(Request::input('reply_id'));
$text = trim(Request::input('text'));
$key = trim(Request::input('key'));
$text_type = strtolower(trim(Request::input('text_type')));
$silence = in_array(strtolower(trim(Request::input('silence'))), ['yes', 'true', '1']);
$markdown = in_array($text_type, ['md', 'markdown']);
@@ -1062,13 +1079,16 @@ class DialogController extends AbstractController
'height' => -1,
'ext' => $ext,
];
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'file', $fileData, $user->userid, false, false, $silence);
if (empty($key)) {
$key = mb_substr(strip_tags($text), 0, 200);
}
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'file', $fileData, $user->userid, false, false, $silence, $key);
} else {
$msgData = ['text' => $text];
if ($markdown) {
$msgData['type'] = 'md';
}
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'text', $msgData, $user->userid, false, false, $silence);
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'text', $msgData, $user->userid, false, false, $silence, $key);
}
}
return $result;
@@ -1330,11 +1350,74 @@ class DialogController extends AbstractController
if (empty($dialog)) {
return Base::retError('匿名机器人会话不存在');
}
return WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => "<p>{$text}</p>"], $botUser->userid, true);
return WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'content' => $text,
], $botUser->userid, true);
}
/**
* @api {get} api/dialog/msg/readlist 27. 获取消息阅读情况
* @api {post} api/dialog/msg/sendlocation 27. 发送位置消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__sendlocation
*
* @apiParam {Number} dialog_id 对话ID
* @apiParam {String} type 位置类型
* - bd: 百度地图
* @apiParam {Number} lng 经度
* @apiParam {Number} lat 纬度
* @apiParam {String} title 位置名称
* @apiParam {Number} [distance] 距离(米)
* @apiParam {String} [address] 位置地址
* @apiParam {String} [thumb] 预览图片url
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__sendlocation()
{
$user = User::auth();
//
$dialog_id = intval(Request::input('dialog_id'));
$type = strtolower(trim(Request::input('type')));
$lng = floatval(Request::input('lng'));
$lat = floatval(Request::input('lat'));
$title = trim(Request::input('title'));
$distance = intval(Request::input('distance'));
$address = trim(Request::input('address'));
$thumb = trim(Request::input('thumb'));
//
if (empty($lng) || $lng < -180 || $lng > 180
|| empty($lat) || $lat < -90 || $lat > 90) {
return Base::retError('经纬度错误');
}
if (empty($title)) {
return Base::retError('位置名称不能为空');
}
//
WebSocketDialog::checkDialog($dialog_id);
//
if ($type == 'bd') {
$msgData = [
'type' => $type,
'lng' => $lng,
'lat' => $lat,
'title' => $title,
'distance' => $distance,
'address' => $address,
'thumb' => $thumb,
];
return WebSocketDialogMsg::sendMsg(null, $dialog_id, 'location', $msgData, $user->userid);
}
return Base::retError('位置类型错误');
}
/**
* @api {get} api/dialog/msg/readlist 28. 获取消息阅读情况
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1363,7 +1446,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/detail 28. 消息详情
* @api {get} api/dialog/msg/detail 29. 消息详情
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1411,7 +1494,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/download 29. 文件下载
* @api {get} api/dialog/msg/download 30. 文件下载
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1448,11 +1531,11 @@ class DialogController extends AbstractController
}
//
$filePath = public_path($array['path']);
return Base::streamDownload($filePath, $array['name']);
return Base::BinaryFileResponse($filePath, $array['name']);
}
/**
* @api {get} api/dialog/msg/withdraw 30. 聊天消息撤回
* @api {get} api/dialog/msg/withdraw 31. 聊天消息撤回
*
* @apiDescription 消息撤回限制24小时内需要token身份
* @apiVersion 1.0.0
@@ -1478,7 +1561,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/voice2text 31. 语音消息转文字
* @api {get} api/dialog/msg/voice2text 32. 语音消息转文字
*
* @apiDescription 将语音消息转文字需要token身份
* @apiVersion 1.0.0
@@ -1530,7 +1613,66 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/mark 32. 消息标记操作
* @api {get} api/dialog/msg/translation 33. 翻译消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__translation
*
* @apiParam {Number} msg_id 消息ID
* @apiParam {String} [language] 目标语言,默认当前语言
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__translation()
{
User::auth();
//
$msg_id = intval(Request::input("msg_id"));
$language = Base::inputOrHeader('language');
$targetLanguage = Doo::getLanguages($language);
//
if (empty($targetLanguage)) {
return Base::retError("参数错误");
}
$msg = WebSocketDialogMsg::whereId($msg_id)->first();
if (empty($msg)) {
return Base::retError("消息不存在或已被删除");
}
if (!in_array($msg->type, ['text', 'record'])) {
return Base::retError("此消息不支持翻译");
}
WebSocketDialog::checkDialog($msg->dialog_id);
//
$row = WebSocketDialogMsgTranslate::whereMsgId($msg_id)->whereLanguage($language)->first();
if ($row) {
return Base::retSuccess("success", $row->only(['msg_id', 'language', 'content']));
}
//
$msgData = Base::json2array($msg->getRawOriginal('msg'));
if (empty($msgData['text'])) {
return Base::retError("消息内容为空");
}
$res = Extranet::openAItranslations($msgData['text'], $targetLanguage);
if (Base::isError($res)) {
return $res;
}
$row = WebSocketDialogMsgTranslate::createInstance([
'dialog_id' => $msg->dialog_id,
'msg_id' => $msg_id,
'language' => $language,
'content' => $res['data'],
]);
$row->save();
//
return Base::retSuccess("success", $row->only(['msg_id', 'language', 'content']));
}
/**
* @api {get} api/dialog/msg/mark 34. 消息标记操作
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1580,13 +1722,13 @@ class DialogController extends AbstractController
}
$dialogUser->mark_unread = $type == 'unread' ? 1 : 0;
$dialogUser->save();
$dialogUser->webSocketDialog->generateUnread($user->userid);
$unreadData = WebSocketDialog::generateUnread($dialog_id, $user->userid);
return Base::retSuccess("success", [
'id' => $dialogUser->webSocketDialog->id,
'unread' => $dialogUser->webSocketDialog->unread,
'unread_one' => $dialogUser->webSocketDialog->unread_one,
'mention' => $dialogUser->webSocketDialog->mention,
'mention_ids' => $dialogUser->webSocketDialog->mention_ids,
'id' => $dialog_id,
'unread' => $unreadData['unread'],
'unread_one' => $unreadData['unread_one'],
'mention' => $unreadData['mention'],
'mention_ids' => $unreadData['mention_ids'],
'user_at' => Carbon::parse($dialogUser->updated_at)->toDateTimeString('millisecond'),
'user_ms' => Carbon::parse($dialogUser->updated_at)->valueOf(),
'mark_unread' => $dialogUser->mark_unread,
@@ -1594,7 +1736,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/silence 33. 消息免打扰
* @api {get} api/dialog/msg/silence 35. 消息免打扰
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1657,7 +1799,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/forward 34. 转发消息给
* @api {get} api/dialog/msg/forward 36. 转发消息给
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1698,7 +1840,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/emoji 35. emoji回复
* @api {get} api/dialog/msg/emoji 37. emoji回复
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1719,7 +1861,7 @@ class DialogController extends AbstractController
$msg_id = intval(Request::input("msg_id"));
$symbol = Request::input("symbol");
//
if (!preg_match("/^[\u{d800}-\u{dbff}]|[\u{dc00}-\u{dfff}]$/", $symbol)) {
if (!preg_match('/[\x{1F300}-\x{1F9FF}]|[\x{1F000}-\x{1F02F}]|[\x{1F0A0}-\x{1F0FF}]|[\x{1F100}-\x{1F64F}]|[\x{1F680}-\x{1F6FF}]|[\x{2600}-\x{26FF}]|[\x{2700}-\x{27BF}]/u', $symbol)) {
return Base::retError("参数错误");
}
//
@@ -1733,7 +1875,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/tag 36. 标注/取消标注
* @api {get} api/dialog/msg/tag 38. 标注/取消标注
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1762,7 +1904,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/todo 37. 设待办/取消待办
* @api {get} api/dialog/msg/todo 39. 设待办/取消待办
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1805,7 +1947,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/todolist 38. 获取消息待办情况
* @api {get} api/dialog/msg/todolist 40. 获取消息待办情况
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1835,7 +1977,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/done 39. 完成待办
* @api {get} api/dialog/msg/done 41. 完成待办
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1888,7 +2030,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/color 40. 设置颜色
* @api {get} api/dialog/msg/color 42. 设置颜色
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1929,7 +2071,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/add 41. 新增群组
* @api {get} api/dialog/group/add 43. 新增群组
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1984,14 +2126,14 @@ class DialogController extends AbstractController
$dialog->avatar = $avatar;
$dialog->save();
}
$data = WebSocketDialog::find($dialog->id)?->formatData($user->userid);
$data = WebSocketDialog::synthesizeData($dialog, $user->userid);
$userids = array_values(array_diff($userids, [$user->userid]));
$dialog->pushMsg("groupAdd", null, $userids);
return Base::retSuccess('创建成功', $data);
}
/**
* @api {get} api/dialog/group/edit 42. 修改群组
* @api {get} api/dialog/group/edit 44. 修改群组
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2053,7 +2195,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/adduser 43. 添加群成员
* @api {get} api/dialog/group/adduser 45. 添加群成员
*
* @apiDescription 需要token身份
* - 有群主时:只有群主可以邀请
@@ -2089,7 +2231,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/deluser 44. 移出(退出)群成员
* @api {get} api/dialog/group/deluser 46. 移出(退出)群成员
*
* @apiDescription 需要token身份
* - 只有群主、邀请人可以踢人
@@ -2133,7 +2275,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/transfer 45. 转让群组
* @api {get} api/dialog/group/transfer 47. 转让群组
*
* @apiDescription 需要token身份
* - 只有群主且是个人类型群可以解散
@@ -2182,7 +2324,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/disband 46. 解散群组
* @api {get} api/dialog/group/disband 48. 解散群组
*
* @apiDescription 需要token身份
* - 只有群主且是个人类型群可以解散
@@ -2210,7 +2352,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/searchuser 47. 搜索个人群(仅限管理员)
* @api {get} api/dialog/group/searchuser 49. 搜索个人群(仅限管理员)
*
* @apiDescription 需要token身份用于创建部门搜索个人群组
* @apiVersion 1.0.0
@@ -2239,7 +2381,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/okr/add 48. 创建OKR评论会话
* @api {post} api/dialog/okr/add 50. 创建OKR评论会话
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2278,7 +2420,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/okr/push 49. 推送OKR相关信息
* @api {post} api/dialog/okr/push 51. 推送OKR相关信息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2314,7 +2456,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/wordchain 50. 发送接龙消息
* @api {post} api/dialog/msg/wordchain 52. 发送接龙消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2400,7 +2542,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/vote 51. 发起投票
* @api {post} api/dialog/msg/vote 53. 发起投票
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2516,7 +2658,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/top 52. 置顶/取消置顶
* @api {get} api/dialog/msg/top 54. 置顶/取消置顶
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2576,7 +2718,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/topinfo 53. 获取置顶消息
* @api {get} api/dialog/msg/topinfo 55. 获取置顶消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2601,4 +2743,28 @@ class DialogController extends AbstractController
//
return Base::retSuccess('success', $topMsg);
}
/**
* @api {get} api/dialog/sticker/search 56. 搜索在线表情
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName sticker__search
*
* @apiParam {String} key 关键词
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function sticker__search()
{
User::auth();
//
$key = trim(Request::input('key'));
return Base::retSuccess('success', [
'list' => Extranet::sticker($key)
]);
}
}

View File

@@ -12,6 +12,7 @@ use App\Models\FileLink;
use App\Models\FileUser;
use App\Models\User;
use App\Module\Base;
use App\Module\Timer;
use App\Module\Ihttp;
use Response;
use Session;
@@ -552,14 +553,14 @@ class FileController extends AbstractController
$tmpPath = "uploads/file/document/" . date("Ym") . "/" . $id . "/attached/";
Base::makeDir(public_path($tmpPath));
$tmpPath .= md5($text) . "." . $matchs[1][$key];
if (Base::saveContentImage(public_path($tmpPath), base64_decode($text))) {
if (Base::saveContentImage(public_path($tmpPath), base64_decode($text), 90)) {
$paramet = getimagesize(public_path($tmpPath));
$data['content'] = str_replace($matchs[0][$key], '<img src="' . Base::fillUrl($tmpPath) . '" original-width="' . $paramet[0] . '" original-height="' . $paramet[1] . '"', $data['content']);
$isRep = true;
}
}
$text = strip_tags($data['content']);
if ($isRep == true) {
if ($isRep) {
$content = Base::array2json($data);
}
}
@@ -1013,15 +1014,37 @@ class FileController extends AbstractController
}
$user = User::auth();
if ($user->isTemp()) {
return Base::retError('无法打包下载');
}
$setting = Base::setting('fileSetting');
switch ($setting['permission_pack_type']) {
case 'admin':
if (!$user->isAdmin()) {
return Base::retError('此功能仅管理员可用');
}
break;
case 'appointAllow':
if (!in_array($user->userid, $setting['permission_pack_userids'])) {
return Base::retError('此功能仅指定用户可用');
}
break;
case 'appointProhibit':
if (in_array($user->userid, $setting['permission_pack_userids'])) {
return Base::retError('此功能已禁止使用');
}
break;
}
$ids = Request::input('ids');
$fileName = Request::input('name');
$fileName = preg_replace("/[\/\\\:\*\?\"\<\>\|]/", "", $fileName);
if (empty($fileName)) {
$fileName = 'Package_' . $user->userid;
}
$fileName .= '_' . Base::time() . '.zip';
$fileName .= '_' . Timer::time() . '.zip';
$filePath = "temp/file/pack/" . date("Ym", Base::time());
$filePath = "temp/file/pack/" . date("Ym", Timer::time());
$zipFile = "app/" . $filePath . "/" . $fileName;
$zipPath = storage_path($zipFile);
@@ -1089,14 +1112,13 @@ class FileController extends AbstractController
]);
}
//
$text = "<b>文件下载打包已完成。</b>";
$text .= "\n\n";
$text .= "文件名:{$fileName}";
$text .= "\n";
$text .= "文件大小:".Base::twoFloat(filesize($zipPath) / 1024, true)."KB";
$text .= "\n";
$text .= '<a href="' . $fileUrl . '" target="_blank"><button type="button" class="ivu-btn ivu-btn-warning" style="margin-top: 10px;"><span>立即下载</span></button></a>';
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, true);
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'file_download',
'title' => '文件下载打包已完成',
'name' => $fileName,
'size' => filesize($zipPath),
'url' => $fileUrl,
], $botUser->userid, false, false, true);
});
return Base::retSuccess('success', [
'name' => $fileName,

View File

@@ -12,6 +12,7 @@ use App\Module\Doo;
use App\Models\File;
use App\Models\User;
use App\Module\Base;
use App\Module\Timer;
use Swoole\Coroutine;
use App\Models\Deleted;
use App\Models\Project;
@@ -323,7 +324,7 @@ class ProjectController extends AbstractController
}
$project->save();
});
$project->pushMsg('update', $project);
$project->pushMsg('update');
//
return Base::retSuccess('修改成功', $project);
}
@@ -1003,7 +1004,7 @@ class ProjectController extends AbstractController
}
//
if (is_array($time)) {
if (Base::isDateOrTime($time[0]) && Base::isDateOrTime($time[1])) {
if (Timer::isDateOrTime($time[0]) && Timer::isDateOrTime($time[1])) {
$builder->betweenTime(Carbon::parse($time[0])->startOfDay(), Carbon::parse($time[1])->endOfDay());
}
}
@@ -1111,7 +1112,7 @@ class ProjectController extends AbstractController
/**
* @api {get} api/project/task/easylists 20. 任务列表-简单的
*
* @apiDescription 需要token身份
* @apiDescription 需要token身份,主要用于判断是否有时间冲突的任务
* @apiVersion 1.0.0
* @apiGroup project
* @apiName task__easylists
@@ -1130,23 +1131,24 @@ class ProjectController extends AbstractController
//
$taskid = trim(Request::input('taskid'));
$userid = Request::input('userid');
$timerange = Request::input('timerange');
$timerange = TimeRange::parse(Request::input('timerange'));
//
$list = ProjectTask::with(['taskUser'])
->select('projects.name as project_name', 'project_tasks.id', 'project_tasks.name', 'project_tasks.start_at', 'project_tasks.end_at')
->select([
'projects.name as project_name',
'project_tasks.id',
'project_tasks.name',
'project_tasks.start_at',
'project_tasks.end_at'
])
->join('projects','project_tasks.project_id','=','projects.id')
->leftJoin('project_task_users', function ($query) {
$query->on('project_tasks.id', '=', 'project_task_users.task_id')->where('project_task_users.owner', '=', 1);
})
->whereIn('project_task_users.userid', is_array($userid) ? $userid : explode(',', $userid) )
->when(!empty($timerange), function ($query) use ($timerange) {
if (!is_array($timerange)) {
$timerange = explode(',', $timerange);
}
if (Base::isDateOrTime($timerange[0]) && Base::isDateOrTime($timerange[1])) {
$query->where('project_tasks.start_at', '<=', Carbon::parse($timerange[1])->endOfDay());
$query->where('project_tasks.end_at', '>=', Carbon::parse($timerange[0])->startOfDay());
}
->when($timerange->isExist(), function ($query) use ($timerange) {
$query->where('project_tasks.start_at', '<=', $timerange->lastTime()->endOfDay());
$query->where('project_tasks.end_at', '>=', $timerange->firstTime()->startOfDay());
})
->when(!empty($taskid), function ($query) use ($taskid) {
$query->where('project_tasks.id', "!=", $taskid);
@@ -1195,7 +1197,7 @@ class ProjectController extends AbstractController
if (count($userid) > 100) {
return Base::retError('导出成员限制最多100个');
}
if (!(is_array($time) && Base::isDateOrTime($time[0]) && Base::isDateOrTime($time[1]))) {
if (!(is_array($time) && Timer::isDateOrTime($time[0]) && Timer::isDateOrTime($time[1]))) {
return Base::retError('时间选择错误');
}
if (Carbon::parse($time[1])->timestamp - Carbon::parse($time[0])->timestamp > 90 * 86400) {
@@ -1210,26 +1212,29 @@ class ProjectController extends AbstractController
go(function () use ($user, $userid, $time, $type, $botUser, $dialog) {
Coroutine::sleep(0.1);
$headings = [];
$headings[] = '任务ID';
$headings[] = '父级任务ID';
$headings[] = '所属项目';
$headings[] = '任务标题';
$headings[] = '任务开始时间';
$headings[] = '任务结束时间';
$headings[] = '完成时间';
$headings[] = '归档时间';
$headings[] = '任务计划用时';
$headings[] = '实际完成用时';
$headings[] = '超时时间';
$headings[] = '开发用时';
$headings[] = '验收/测试用时';
$headings[] = '负责人';
$headings[] = '创建人';
$headings[] = '状态';
$headings[] = Doo::translate('任务ID');
$headings[] = Doo::translate('父级任务ID');
$headings[] = Doo::translate('所属项目');
$headings[] = Doo::translate('任务标题');
$headings[] = Doo::translate('任务开始时间');
$headings[] = Doo::translate('任务结束时间');
$headings[] = Doo::translate('完成时间');
$headings[] = Doo::translate('归档时间');
$headings[] = Doo::translate('任务计划用时');
$headings[] = Doo::translate('实际完成用时');
$headings[] = Doo::translate('超时时间');
$headings[] = Doo::translate('开发用时');
$headings[] = Doo::translate('验收/测试用时');
$headings[] = Doo::translate('负责人');
$headings[] = Doo::translate('创建人');
$headings[] = Doo::translate('状态');
$datas = [];
//
$text = '<b>导出任务统计已完成。</b>';
$text .= "\n\n";
$content = [];
$content[] = [
'content' => '导出任务统计已完成',
'style' => 'font-weight: bold;padding-bottom: 4px;',
];
//
$builder = ProjectTask::select(['project_tasks.*', 'project_task_users.userid as ownerid'])
->join('project_task_users', 'project_tasks.id', '=', 'project_task_users.task_id')
@@ -1240,20 +1245,21 @@ class ProjectController extends AbstractController
/** @var ProjectTask $task */
foreach ($tasks as $task) {
$flowChanges = ProjectTaskFlowChange::whereTaskId($task->id)->get();
$testTime = 0;//验收/测试时间
$testTime = 0; // 测试时间
$taskStartTime = $task->start_at ? Carbon::parse($task->start_at)->timestamp : Carbon::parse($task->created_at)->timestamp;
$taskCompleteTime = $task->complete_at ? Carbon::parse($task->complete_at)->timestamp : time();
$totalTime = $taskCompleteTime - $taskStartTime; //开发测试总用时
$totalTime = $taskCompleteTime - $taskStartTime; // 任务总用时
foreach ($flowChanges as $change) {
if (!str_contains($change->before_flow_item_name, 'end')) {
$upOne = ProjectTaskFlowChange::where('id', '<', $change->id)->whereTaskId($task->id)->orderByDesc('id')->first();
if ($upOne) {
if (str_contains($change->before_flow_item_name, 'test') || str_contains($change->before_flow_item_name, '测试') || strpos($change->before_flow_item_name, '验收') !== false) {
$testCtime = Carbon::parse($change->created_at)->timestamp;
$tTime = Carbon::parse($upOne->created_at)->timestamp;
$tMinusNum = $testCtime - $tTime;
$testTime += $tMinusNum;
}
if (str_starts_with($change->before_flow_item_name, 'end')) {
continue;
}
$upOne = ProjectTaskFlowChange::where('id', '<', $change->id)->whereTaskId($task->id)->orderByDesc('id')->first();
if ($upOne) {
if (str_starts_with($change->before_flow_item_name, 'test')) {
$testCtime = Carbon::parse($change->created_at)->timestamp;
$tTime = Carbon::parse($upOne->created_at)->timestamp;
$tMinusNum = $testCtime - $tTime;
$testTime += $tMinusNum;
}
}
}
@@ -1261,27 +1267,27 @@ class ProjectController extends AbstractController
$lastChange = ProjectTaskFlowChange::whereTaskId($task->id)->orderByDesc('id')->first();
$nowTime = time();
$unFinishTime = $nowTime - Carbon::parse($lastChange->created_at)->timestamp;
if (str_contains($lastChange->after_flow_item_name, 'test') || str_contains($lastChange->after_flow_item_name, '测试') || strpos($lastChange->after_flow_item_name, '验收') !== false) {
if (str_starts_with($lastChange->after_flow_item_name, 'test')) {
$testTime += $unFinishTime;
}
}
$developTime = $totalTime - $testTime;//开发时间
$planTime = '-';//任务计划用时
$overTime = '-';//超时时间
$developTime = $totalTime - $testTime; // 开发时间
$planTime = '-'; // 任务计划用时
$overTime = '-'; // 超时时间
if ($task->end_at) {
$startTime = Carbon::parse($task->start_at)->timestamp;
$endTime = Carbon::parse($task->end_at)->timestamp;
$planTotalTime = $endTime - $startTime;
$residueTime = $planTotalTime - $totalTime;
if ($residueTime < 0) {
$overTime = Base::timeFormat(abs($residueTime));
$overTime = Doo::translate(Timer::timeFormat(abs($residueTime)));
}
$planTime = Base::timeDiff($startTime, $endTime);
$planTime = Doo::translate(Timer::timeDiff($startTime, $endTime));
}
$actualTime = $task->complete_at ? $totalTime : 0;//实际完成用时
$actualTime = $task->complete_at ? $totalTime : 0; // 实际完成用时
$statusText = '未完成';
if ($task->flow_item_name) {
if (str_contains($task->flow_item_name, '已取消')) {
if (str_starts_with($task->flow_item_name, 'end')) {
if (preg_match('/已取消|Cancelled|취소됨|キャンセル済み|Abgebrochen|Annulé|Dibatalkan|Отменено/', $task->flow_item_name)) {
$statusText = '已取消';
$actualTime = 0;
$testTime = 0;
@@ -1316,20 +1322,27 @@ class ProjectController extends AbstractController
$task->end_at ?: '-',
$task->complete_at ?: '-',
$task->archived_at ?: '-',
$planTime ?: '-',
$actualTime ? Base::timeFormat($actualTime) : '-',
$planTime,
$actualTime ? Doo::translate(Timer::timeFormat($actualTime)) : '-',
$overTime,
$developTime > 0 ? Base::timeFormat($developTime) : '-',
$testTime > 0 ? Base::timeFormat($testTime) : '-',
$developTime > 0 ? Doo::translate(Timer::timeFormat($developTime)) : '-',
$testTime > 0 ? Doo::translate(Timer::timeFormat($testTime)) : '-',
Base::filterEmoji(User::userid2nickname($task->ownerid)) . " (ID: {$task->ownerid})",
Base::filterEmoji(User::userid2nickname($task->userid)) . " (ID: {$task->userid})",
$statusText
Doo::translate($statusText),
];
}
});
if (empty($datas)) {
$text .= '没有任何数据';
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, true);
$content[] = [
'content' => '没有任何数据',
'style' => 'color: #ff0000;',
];
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'title' => $content[0]['content'],
'content' => $content,
], $botUser->userid, false, false, true);
return;
}
//
@@ -1346,15 +1359,24 @@ class ProjectController extends AbstractController
//
$fileName = User::userid2nickname($userid[0]) ?: $userid[0];
if (count($userid) > 1) {
$fileName .= '等' . count($userid) . '位成员任务统计';
$fileName .= '等' . count($userid) . '位成员任务统计';
} else {
$fileName .= '的任务统计';
}
$fileName .= '_' . Base::time() . '.xls';
$filePath = "temp/task/export/" . date("Ym", Base::time());
$fileName = Doo::translate($fileName) . '_' . Timer::time() . '.xls';
$filePath = "temp/task/export/" . date("Ym", Timer::time());
$export = new BillMultipleExport($sheets);
$res = $export->store($filePath . "/" . $fileName);
if ($res != 1) {
$text .= "导出失败,{$fileName}";
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, true);
$content[] = [
'content' => "导出失败,{$fileName}",
'style' => 'color: #ff0000;',
];
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'title' => $content[0]['content'],
'content' => $content,
], $botUser->userid, false, false, true);
return;
}
//
@@ -1375,17 +1397,26 @@ class ProjectController extends AbstractController
]));
$fileUrl = Base::fillUrl('api/project/task/down?key=' . urlencode($base64));
Session::put('task::export:userid', $user->userid);
$text .= "文件名:{$fileName}";
$text .= "\n";
$text .= "文件大小:" . Base::twoFloat(filesize($zipPath) / 1024, true) . "KB";
$text .= "\n";
$text .= '<a href="' . $fileUrl . '" target="_blank"><button type="button" class="ivu-btn ivu-btn-warning" style="margin-top: 10px;"><span>立即下载</span></button></a>';
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'file_download',
'title' => '导出任务统计已完成',
'name' => $fileName,
'size' => filesize($zipPath),
'url' => $fileUrl,
], $botUser->userid, false, false, true);
} else {
$text .= '打包失败,请稍后再试...';
$content[] = [
'content' => "打包失败,请稍后再试...",
'style' => 'color: #ff0000;',
];
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'title' => $content[0]['content'],
'content' => $content,
], $botUser->userid, false, false, true);
}
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, true);
});
return Base::retSuccess('success', ['msg' => '正在打包,请留意系统消息。']);
return Base::retSuccess('success');
}
/**
@@ -1405,16 +1436,16 @@ class ProjectController extends AbstractController
$user = User::auth('admin');
//
$headings = [];
$headings[] = '任务ID';
$headings[] = '父级任务ID';
$headings[] = '所属项目';
$headings[] = '任务标题';
$headings[] = '任务开始时间';
$headings[] = '任务结束时间';
$headings[] = '任务计划用时';
$headings[] = '超时时间';
$headings[] = '负责人';
$headings[] = '创建人';
$headings[] = Doo::translate('任务ID');
$headings[] = Doo::translate('父级任务ID');
$headings[] = Doo::translate('所属项目');
$headings[] = Doo::translate('任务标题');
$headings[] = Doo::translate('任务开始时间');
$headings[] = Doo::translate('任务结束时间');
$headings[] = Doo::translate('任务计划用时');
$headings[] = Doo::translate('超时时间');
$headings[] = Doo::translate('负责人');
$headings[] = Doo::translate('创建人');
$data = [];
//
ProjectTask::whereNull('complete_at')
@@ -1434,9 +1465,9 @@ class ProjectController extends AbstractController
$planTotalTime = $endTime - $startTime;
$residueTime = $planTotalTime - $totalTime;
if ($residueTime < 0) {
$overTime = Base::timeFormat(abs($residueTime));
$overTime = Doo::translate(Timer::timeFormat(abs($residueTime)));
}
$planTime = Base::timeDiff($startTime, $endTime);
$planTime = Doo::translate(Timer::timeDiff($startTime, $endTime));
}
$ownerIds = $task->taskUser->where('owner', 1)->pluck('userid')->toArray();
$ownerNames = [];
@@ -1450,7 +1481,7 @@ class ProjectController extends AbstractController
Base::filterEmoji($task->name),
$task->start_at ?: '-',
$task->end_at ?: '-',
$planTime ?: '-',
$planTime,
$overTime,
implode("", $ownerNames),
Base::filterEmoji(User::userid2nickname($task->userid)) . " (ID: {$task->userid})",
@@ -1461,12 +1492,13 @@ class ProjectController extends AbstractController
return Base::retError('没有任何数据');
}
//
$title = Doo::translate('超期任务');
$sheets = [
BillExport::create()->setTitle("超期任务")->setHeadings($headings)->setData($data)->setStyles(["A1:J1" => ["font" => ["bold" => true]]])
BillExport::create()->setTitle($title)->setHeadings($headings)->setData($data)->setStyles(["A1:J1" => ["font" => ["bold" => true]]])
];
//
$fileName = '超期任务_' . Base::time() . '.xls';
$filePath = "temp/task/export/" . date("Ym", Base::time());
$fileName = $title . '_' . Timer::time() . '.xls';
$filePath = "temp/task/export/" . date("Ym", Timer::time());
$export = new BillMultipleExport($sheets);
$res = $export->store($filePath . "/" . $fileName);
if ($res != 1) {
@@ -1692,7 +1724,7 @@ class ProjectController extends AbstractController
//
$task = ProjectTask::userTask($file->task_id);
//
ProjectPermission::userTaskPermission(Project::userProject($task->project_id), ProjectPermission::TASK_UPDATE, $task);
ProjectPermission::userTaskPermission(Project::userProject($task->project_id), ProjectPermission::TASK_REMOVE, $task);
//
$task->pushMsg('filedelete', $file);
$file->delete();
@@ -1788,7 +1820,7 @@ class ProjectController extends AbstractController
}
//
$filePath = public_path($file->getRawOriginal('path'));
return Base::streamDownload($filePath, $file->name);
return Base::BinaryFileResponse($filePath, $file->name);
}
/**
@@ -1964,11 +1996,13 @@ class ProjectController extends AbstractController
$task = ProjectTask::userTask($task_id);
//
$project = Project::userProject($task->project_id);
if (Arr::exists($param, 'flow_item_id')) {
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_STATUS, $task);
}else{
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_UPDATE, $task);
$permissionKey = ProjectPermission::TASK_UPDATE;
if (Arr::exists($param, 'times')) {
$permissionKey = ProjectPermission::TASK_TIME;
} else if (Arr::exists($param, 'flow_item_id')) {
$permissionKey = ProjectPermission::TASK_STATUS;
}
ProjectPermission::userTaskPermission($project, $permissionKey, $task);
//
$taskUser = ProjectTaskUser::select(['userid', 'owner'])->whereTaskId($task_id)->get();
$owners = $taskUser->where('owner', 1)->pluck('userid')->toArray();
@@ -2072,7 +2106,7 @@ class ProjectController extends AbstractController
});
//
$task->pushMsg('dialog');
$dialogData = WebSocketDialog::find($task->dialog_id)?->formatData($user->userid);
$dialogData = WebSocketDialog::synthesizeData($task->dialog_id, $user->userid);
return Base::retSuccess('success', [
'id' => $task->id,
'dialog_id' => $task->dialog_id,
@@ -2508,10 +2542,10 @@ class ProjectController extends AbstractController
}
$log->detail = Doo::translate($log->detail);
$log->time = [
'ymd' => date(date("Y", $timestamp) == date("Y", Base::time()) ? "m-d" : "Y-m-d", $timestamp),
'ymd' => date(date("Y", $timestamp) == date("Y", Timer::time()) ? "m-d" : "Y-m-d", $timestamp),
'hi' => date("h:i", $timestamp) ,
'week' => Doo::translate("" . Base::getTimeWeek($timestamp)),
'segment' => Doo::translate(Base::getTimeDayeSegment($timestamp)),
'week' => Doo::translate("" . Timer::getWeek($timestamp)),
'segment' => Doo::translate(Timer::getDayeSegment($timestamp)),
];
$record = Base::json2array($log->record);
if (is_array($record['change'])) {
@@ -2558,6 +2592,10 @@ class ProjectController extends AbstractController
}
$projectUser->top_at = $projectUser->top_at ? null : Carbon::now();
$projectUser->save();
if ($projectUser->project) {
$projectUser->project->updated_at = Carbon::now();
$projectUser->project->save();
}
return Base::retSuccess("success", [
'id' => $projectUser->project_id,
'top_at' => $projectUser->top_at?->toDateTimeString(),
@@ -2625,8 +2663,9 @@ class ProjectController extends AbstractController
ProjectPermission::TASK_LIST_SORT,
ProjectPermission::TASK_ADD,
ProjectPermission::TASK_UPDATE,
ProjectPermission::TASK_REMOVE,
ProjectPermission::TASK_TIME,
ProjectPermission::TASK_STATUS,
ProjectPermission::TASK_REMOVE,
ProjectPermission::TASK_ARCHIVED,
ProjectPermission::TASK_MOVE,
]);

View File

@@ -65,7 +65,9 @@ class PublicController extends AbstractController
}
/**
* {post} 签到 - 路由器openwrt上报
* {post} 签到 - 上报
* - 1、路由器openwrt签到上报
* - 2、考勤机签到上报
*
* @apiParam {String} key
* @apiParam {String} mac 使用逗号分割多个
@@ -78,20 +80,30 @@ class PublicController extends AbstractController
$key = trim(Request::input('key'));
$mac = trim(Request::input('mac'));
$time = intval(Request::input('time'));
$type = trim(Request::input('type'));
//
$setting = Base::setting('checkinSetting');
if ($setting['open'] !== 'open') {
return 'function off';
}
if (!in_array('auto', $setting['modes'])) {
return 'mode off';
}
if ($key != $setting['key']) {
return 'key error';
}
if ($error = UserBot::checkinBotCheckin($mac, $time)) {
return $error;
$alreadyTip = false;
if ($type === 'face') {
if (!in_array('face', $setting['modes'])) {
return 'mode off';
}
if ($key != $setting['face_key']) {
return 'key error';
}
$alreadyTip = $setting['face_retip'] === 'open';
} else {
if (!in_array('auto', $setting['modes'])) {
return 'mode off';
}
if ($key != $setting['key']) {
return 'key error';
}
}
UserBot::checkinBotCheckin($mac, $time, $alreadyTip);
return 'success';
}
}

View File

@@ -53,8 +53,8 @@ class ReportController extends AbstractController
$builder->whereType($keys['type']);
}
if (is_array($keys['created_at'])) {
if ($keys['created_at'][0] > 0) $builder->where('created_at', '>=', date('Y-m-d H:i:s', Base::dayTimeF($keys['created_at'][0])));
if ($keys['created_at'][1] > 0) $builder->where('created_at', '<=', date('Y-m-d H:i:s', Base::dayTimeE($keys['created_at'][1])));
if ($keys['created_at'][0] > 0) $builder->where('created_at', '>=', Base::newCarbon($keys['created_at'][0])->startOfDay());
if ($keys['created_at'][1] > 0) $builder->where('created_at', '<=', Base::newCarbon($keys['created_at'][1])->endOfDay());
}
}
$list = $builder->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20));
@@ -99,14 +99,14 @@ class ReportController extends AbstractController
$builder->whereType($keys['type']);
}
if (is_array($keys['created_at'])) {
if ($keys['created_at'][0] > 0) $builder->where('created_at', '>=', date('Y-m-d H:i:s', Base::dayTimeF($keys['created_at'][0])));
if ($keys['created_at'][1] > 0) $builder->where('created_at', '<=', date('Y-m-d H:i:s', Base::dayTimeE($keys['created_at'][1])));
if ($keys['created_at'][0] > 0) $builder->where('created_at', '>=', Base::newCarbon($keys['created_at'][0])->startOfDay());
if ($keys['created_at'][1] > 0) $builder->where('created_at', '<=', Base::newCarbon($keys['created_at'][1])->endOfDay());
}
}
$list = $builder->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20));
if ($list->items()) {
foreach ($list->items() as $item) {
$item->receive_time = ReportReceive::query()->whereRid($item["id"])->whereUserid($user->userid)->value("receive_time");
$item->receive_at = ReportReceive::query()->whereRid($item["id"])->whereUserid($user->userid)->value("receive_at");
}
}
return Base::retSuccess('success', $list);
@@ -174,7 +174,7 @@ class ReportController extends AbstractController
foreach ($input["receive"] as $userid) {
$input["receive_content"][] = [
"receive_time" => Carbon::now()->toDateTimeString(),
"receive_at" => Carbon::now()->toDateTimeString(),
"userid" => $userid,
"read" => 0,
];
@@ -259,6 +259,7 @@ class ReportController extends AbstractController
$offset = abs(intval(Request::input("offset", 0)));
$id = intval(Request::input("offset", 0));
$now_dt = trim(Request::input("date")) ? Carbon::parse(Request::input("date")) : Carbon::now();
// 获取开始时间
if ($type === Report::DAILY) {
$start_time = Carbon::today();
@@ -280,9 +281,11 @@ class ReportController extends AbstractController
$start_time->startOfWeek();
$end_time = Carbon::instance($start_time)->endOfWeek();
}
// 生成唯一标识
$sign = Report::generateSign($type, 0, Carbon::instance($start_time));
$one = Report::whereSign($sign)->whereType($type)->first();
// 如果已经提交了相关汇报
if ($one && $id > 0) {
return Base::retSuccess('success', [
@@ -293,8 +296,16 @@ class ReportController extends AbstractController
]);
}
// 表格头部
$labels = [
Doo::translate('项目'),
Doo::translate('任务'),
Doo::translate('负责人'),
Doo::translate('备注'),
];
// 已完成的任务
$completeContent = "";
$completeDatas = [];
$complete_task = ProjectTask::query()
->whereNotNull("complete_at")
->whereBetween("complete_at", [$start_time->toDateTimeString(), $end_time->toDateTimeString()])
@@ -306,15 +317,20 @@ class ReportController extends AbstractController
if ($complete_task->isNotEmpty()) {
foreach ($complete_task as $task) {
$complete_at = Carbon::parse($task->complete_at);
$pre = $type == Report::WEEKLY ? ('<span>[' . Doo::translate('周' . ['日', '一', '二', '三', '四', '五', '六'][$complete_at->dayOfWeek]) . ']</span>&nbsp;') : '';
$completeContent .= "<li>{$pre}[{$task->project->name}] {$task->name}</li>";
$remark = $type == Report::WEEKLY ? ('<div style="text-align:center">[' . Doo::translate('周' . ['日', '一', '二', '三', '四', '五', '六'][$complete_at->dayOfWeek]) . ']</div>') : '&nbsp;';
$completeDatas[] = [
$task->project->name,
$task->name,
$task->taskUser->where("owner", 1)->map(function ($item) {
return User::userid2nickname($item->userid);
})->implode(", "),
$remark,
];
}
} else {
$completeContent = '<li>&nbsp;</li>';
}
// 未完成的任务
$unfinishedContent = "";
$unfinishedDatas = [];
$unfinished_task = ProjectTask::query()
->join("projects", "projects.id", "=", "project_tasks.project_id")
->whereNull("projects.archived_at")
@@ -330,12 +346,18 @@ class ReportController extends AbstractController
if ($unfinished_task->isNotEmpty()) {
foreach ($unfinished_task as $task) {
empty($task->end_at) || $end_at = Carbon::parse($task->end_at);
$pre = (!empty($end_at) && $end_at->lt($now_dt)) ? '<span style="color:#ff0000;">[' . Doo::translate('超期') . ']</span>&nbsp;' : '';
$unfinishedContent .= "<li>{$pre}[{$task->project->name}] {$task->name}</li>";
$remark = (!empty($end_at) && $end_at->lt($now_dt)) ? '<div style="color:#ff0000;text-align:center">[' . Doo::translate('超期') . ']</div>' : '&nbsp;';
$unfinishedDatas[] = [
$task->project->name,
$task->name,
$task->taskUser->where("owner", 1)->map(function ($item) {
return User::userid2nickname($item->userid);
})->implode(", "),
$remark,
];
}
} else {
$unfinishedContent = '<li>&nbsp;</li>';
}
// 生成标题
if ($type === Report::WEEKLY) {
$title = $user->nickname . "的周报[" . $start_time->format("m/d") . "-" . $end_time->format("m/d") . "]";
@@ -343,22 +365,43 @@ class ReportController extends AbstractController
} else {
$title = $user->nickname . "的日报[" . $start_time->format("Y/m/d") . "]";
}
$title = Doo::translate($title);
// 生成内容
$content = '<h2>' . Doo::translate('已完成工作') . '</h2><ol>' .
$completeContent . '</ol><h2>' .
Doo::translate('未完成的工作') . '</h2><ol>' .
$unfinishedContent . '</ol>';
$contents = [];
$contents[] = '<h2>' . Doo::translate('已完成工作') . '</h2>';
$contents[] = view('report', [
'labels' => $labels,
'datas' => $completeDatas,
])->render();
$contents[] = '<p>&nbsp;</p>';
$contents[] = '<h2>' . Doo::translate('未完成的工作') . '</h2>';
$contents[] = view('report', [
'labels' => $labels,
'datas' => $unfinishedDatas,
])->render();
if ($type === Report::WEEKLY) {
$content .= "<h2>" . Doo::translate("下周拟定计划") . "[" . $start_time->addWeek()->format("m/d") . "-" . $end_time->addWeek()->format("m/d") . "]</h2><ol><li>&nbsp;</li></ol>";
$contents[] = '<p>&nbsp;</p>';
$contents[] = "<h2>" . Doo::translate("下周拟定计划") . "[" . $start_time->addWeek()->format("m/d") . "-" . $end_time->addWeek()->format("m/d") . "]</h2>";
$contents[] = view('report', [
'labels' => [
Doo::translate('计划描述'),
Doo::translate('计划时间'),
Doo::translate('负责人'),
],
'datas' => [],
])->render();
}
$data = [
"time" => $start_time->toDateTimeString(),
"sign" => $sign,
"title" => $title,
"content" => $content,
"complete_task" => $complete_task,
"unfinished_task" => $unfinished_task,
"content" => implode("", $contents),
];
if ($one) {
$data['id'] = $one->id;
}

View File

@@ -12,6 +12,7 @@ use Carbon\Carbon;
use App\Module\Doo;
use App\Models\User;
use App\Module\Base;
use App\Module\Timer;
use App\Models\Setting;
use App\Module\Extranet;
use LdapRecord\Container;
@@ -40,7 +41,7 @@ class SystemController extends AbstractController
* @apiParam {String} type
* - get: 获取(默认)
* - all: 获取所有(需要管理员权限)
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'voice2text', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'image_compress', 'image_save_local', 'start_home']
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'voice2text', 'translation', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'system_alias', 'image_compress', 'image_save_local', 'start_home']
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -67,6 +68,7 @@ class SystemController extends AbstractController
'chat_information',
'anon_message',
'voice2text',
'translation',
'e2e_message',
'auto_archived',
'archived_day',
@@ -76,6 +78,7 @@ class SystemController extends AbstractController
'all_group_autoin',
'user_private_chat_mute',
'user_group_chat_mute',
'system_alias',
'image_compress',
'image_save_local',
'start_home',
@@ -97,6 +100,12 @@ class SystemController extends AbstractController
if ($all['voice2text'] == 'open' && empty(Base::settingFind('aibotSetting', 'openai_key'))) {
return Base::retError('开启语音转文字功能需要在应用中开启 ChatGPT AI 机器人。');
}
if ($all['translation'] == 'open' && empty(Base::settingFind('aibotSetting', 'openai_key'))) {
return Base::retError('开启翻译功能需要在应用中开启 ChatGPT AI 机器人。');
}
if ($all['system_alias'] == env('APP_NAME')) {
$all['system_alias'] = '';
}
$setting = Base::setting('system', Base::newTrim($all));
} else {
$setting = Base::setting('system');
@@ -118,11 +127,11 @@ class SystemController extends AbstractController
$setting['chat_information'] = $setting['chat_information'] ?: 'optional';
$setting['anon_message'] = $setting['anon_message'] ?: 'open';
$setting['voice2text'] = $setting['voice2text'] ?: 'close';
$setting['translation'] = $setting['translation'] ?: 'close';
$setting['e2e_message'] = $setting['e2e_message'] ?: 'close';
$setting['auto_archived'] = $setting['auto_archived'] ?: 'close';
$setting['archived_day'] = floatval($setting['archived_day']) ?: 7;
$setting['task_visible'] = $setting['task_visible'] ?: 'close';
$setting['task_default_time'] = $setting['task_default_time'] ? Base::json2array($setting['task_default_time']) : ['09:00', '18:00'];
$setting['all_group_mute'] = $setting['all_group_mute'] ?: 'open';
$setting['all_group_autoin'] = $setting['all_group_autoin'] ?: 'yes';
$setting['user_private_chat_mute'] = $setting['user_private_chat_mute'] ?: 'open';
@@ -133,6 +142,8 @@ class SystemController extends AbstractController
$setting['file_upload_limit'] = $setting['file_upload_limit'] ?: '';
$setting['unclaimed_task_reminder'] = $setting['unclaimed_task_reminder'] ?: 'close';
$setting['unclaimed_task_reminder_time'] = $setting['unclaimed_task_reminder_time'] ?: '';
$setting['server_closeai'] = env("SERVER_CLOSEAI") ?: 'open';
$setting['server_timezone'] = config('app.timezone');
$setting['server_version'] = Base::getVersion();
//
return Base::retSuccess('success', $setting ?: json_decode('{}'));
@@ -173,11 +184,18 @@ class SystemController extends AbstractController
'notice_msg',
'msg_unread_user_minute',
'msg_unread_group_minute',
'msg_unread_time_ranges',
'ignore_addr'
])) {
unset($all[$key]);
}
}
$ranges = array_map(function ($item) {
return !is_array($item) ? explode(',', $item) : $item;
}, is_array($all['msg_unread_time_ranges']) ? $all['msg_unread_time_ranges'] : []);
$all['msg_unread_time_ranges'] = array_values(array_filter($ranges, function ($item) {
return count($item) == 2 && Timer::isTime($item[0]) && Timer::isTime($item[1]);
}));
$setting = Base::setting('emailSetting', Base::newTrim($all));
} else {
$setting = Base::setting('emailSetting');
@@ -191,6 +209,7 @@ class SystemController extends AbstractController
$setting['notice_msg'] = $setting['notice_msg'] ?: 'close';
$setting['msg_unread_user_minute'] = intval($setting['msg_unread_user_minute'] ?? -1);
$setting['msg_unread_group_minute'] = intval($setting['msg_unread_group_minute'] ?? -1);
$setting['msg_unread_time_ranges'] = is_array($setting['msg_unread_time_ranges']) ? $setting['msg_unread_time_ranges'] : [[]];
$setting['ignore_addr'] = $setting['ignore_addr'] ?: '';
//
if ($type != 'save' && !in_array('admin', $user->identity)) {
@@ -305,41 +324,45 @@ class SystemController extends AbstractController
}
$backup = $setting;
$setting = Base::setting('aibotSetting', Base::newTrim($all));
$tempMsg = [
'type' => 'content',
'content' => '设置成功'
];
//
if ($backup['openai_key'] != $setting['openai_key']) {
$botUser = User::botGetOrCreate('ai-openai');
if ($botUser && $dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => "设置成功"], $botUser->userid, true, false, true);
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', $tempMsg, $botUser->userid, true, false, true);
}
}
if ($backup['claude_token'] != $setting['claude_token']) {
$botUser = User::botGetOrCreate('ai-claude');
if ($botUser && $dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => "设置成功"], $botUser->userid, true, false, true);
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', $tempMsg, $botUser->userid, true, false, true);
}
}
if ($backup['wenxin_key'] != $setting['wenxin_key']) {
$botUser = User::botGetOrCreate('ai-wenxin');
if ($botUser && $dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => "设置成功"], $botUser->userid, true, false, true);
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', $tempMsg, $botUser->userid, true, false, true);
}
}
if ($backup['qianwen_key'] != $setting['qianwen_key']) {
$botUser = User::botGetOrCreate('ai-qianwen');
if ($botUser && $dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => "设置成功"], $botUser->userid, true, false, true);
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', $tempMsg, $botUser->userid, true, false, true);
}
}
if ($backup['gemini_key'] != $setting['gemini_key']) {
$botUser = User::botGetOrCreate('ai-gemini');
if ($botUser && $dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => "设置成功"], $botUser->userid, true, false, true);
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', $tempMsg, $botUser->userid, true, false, true);
}
}
if ($backup['zhipu_key'] != $setting['zhipu_key']) {
$botUser = User::botGetOrCreate('ai-zhipu');
if ($botUser && $dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => "设置成功"], $botUser->userid, true, false, true);
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', $tempMsg, $botUser->userid, true, false, true);
}
}
}
@@ -393,6 +416,13 @@ class SystemController extends AbstractController
'remindin',
'remindexceed',
'edit',
'face_upload',
'face_remark',
'face_retip',
'locat_remark',
'locat_bd_lbs_key',
'locat_bd_lbs_point', // 格式:{"lng":116.404, "lat":39.915, "radius":500}
'manual_remark',
'modes',
'key',
])) {
@@ -401,8 +431,28 @@ class SystemController extends AbstractController
}
if ($all['open'] === 'close') {
$all['key'] = md5(Base::generatePassword(32));
$all['face_key'] = md5(Base::generatePassword(32));
} else {
$botUser = User::botGetOrCreate('check-in');
if (!$botUser) {
return Base::retError('创建签到机器人失败');
}
if (in_array('locat', $all['modes'])) {
if (empty($all['locat_bd_lbs_key'])) {
return Base::retError('请填写百度地图AK');
}
if (!is_array($all['locat_bd_lbs_point'])) {
return Base::retError('请选择允许签到位置');
}
$all['locat_bd_lbs_point']['radius'] = intval($all['locat_bd_lbs_point']['radius']);
if (empty($all['locat_bd_lbs_point']['lng']) || empty($all['locat_bd_lbs_point']['lat']) || empty($all['locat_bd_lbs_point']['radius'])) {
return Base::retError('请选择有效的签到位置');
}
}
}
if ($all['modes']) {
$all['modes'] = array_intersect($all['modes'], ['auto', 'manual', 'locat', 'face']);
}
$all['modes'] = array_intersect($all['modes'], ['auto', 'manual', 'location']);
$setting = Base::setting('checkinSetting', Base::newTrim($all));
} else {
$setting = Base::setting('checkinSetting');
@@ -412,8 +462,18 @@ class SystemController extends AbstractController
$setting['key'] = md5(Base::generatePassword(32));
Base::setting('checkinSetting', $setting);
}
if (empty($setting['face_key'])) {
$setting['face_key'] = md5(Base::generatePassword(32));
Base::setting('checkinSetting', $setting);
}
//
$setting['open'] = $setting['open'] ?: 'close';
$setting['face_upload'] = $setting['face_upload'] ?: 'close';
$setting['face_remark'] = $setting['face_remark'] ?: Doo::translate('考勤机');
$setting['face_retip'] = $setting['face_retip'] ?: 'open';
$setting['locat_remark'] = $setting['locat_remark'] ?: Doo::translate('定位签到');
$setting['locat_bd_lbs_point'] = is_array($setting['locat_bd_lbs_point']) ? $setting['locat_bd_lbs_point'] : ['radius' => 500];
$setting['manual_remark'] = $setting['manual_remark'] ?: Doo::translate('手动签到');
$setting['time'] = $setting['time'] ? Base::json2array($setting['time']) : ['09:00', '18:00'];
$setting['advance'] = intval($setting['advance']) ?: 120;
$setting['delay'] = intval($setting['delay']) ?: 120;
@@ -545,7 +605,47 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/demo 08. 获取演示帐号
* @api {get} api/system/setting/file 08. 文件设置(限管理员)
*
* @apiVersion 1.0.0
* @apiGroup system
* @apiName setting__file
*
* @apiParam {String} type
* - get: 获取(默认)
* - save: 保存设置(参数:['permission_pack_type', 'permission_pack_userids']
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function setting__file()
{
User::auth('admin');
//
$type = trim(Request::input('type'));
if ($type == 'save') {
if (env("SYSTEM_SETTING") == 'disabled') {
return Base::retError('当前环境禁止修改');
}
$all = Base::newTrim(Request::input());
foreach ($all as $key => $value) {
if (!in_array($key, [
'permission_pack_type',
'permission_pack_userids'
])) {
unset($all[$key]);
}
}
$setting = Base::setting('fileSetting', Base::newTrim($all));
} else {
$setting = Base::setting('fileSetting');
}
//
return Base::retSuccess('success', $setting ?: json_decode('{}'));
}
/**
* @api {get} api/system/demo 09. 获取演示帐号
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -569,7 +669,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/priority 09. 任务优先级
* @api {post} api/system/priority 10. 任务优先级
*
* @apiDescription 获取任务优先级、保存任务优先级
* @apiVersion 1.0.0
@@ -618,7 +718,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/column/template 10. 创建项目模板
* @api {post} api/system/column/template 11. 创建项目模板
*
* @apiDescription 获取创建项目模板、保存创建项目模板
* @apiVersion 1.0.0
@@ -665,7 +765,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/license 11. License
* @api {post} api/system/license 12. License
*
* @apiDescription 获取License信息、保存License限管理员
* @apiVersion 1.0.0
@@ -720,7 +820,7 @@ class SystemController extends AbstractController
if ($data['info']['people'] > 0 && $data['user_count'] > $data['info']['people']) {
$data['error'][] = '终端用户数超过License限制';
}
if ($data['info']['expired_at'] && strtotime($data['info']['expired_at']) <= Base::time()) {
if ($data['info']['expired_at'] && strtotime($data['info']['expired_at']) <= Timer::time()) {
$data['error'][] = '终端License已过期';
}
//
@@ -728,7 +828,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/info 12. 获取终端详细信息
* @api {get} api/system/get/info 13. 获取终端详细信息
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -757,7 +857,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/ip 13. 获取IP地址
* @api {get} api/system/get/ip 14. 获取IP地址
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -772,7 +872,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/cnip 14. 是否中国IP地址
* @api {get} api/system/get/cnip 15. 是否中国IP地址
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -789,7 +889,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/ipgcj02 15. 获取IP地址经纬度
* @api {get} api/system/get/ipgcj02 16. 获取IP地址经纬度
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -806,7 +906,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/ipinfo 16. 获取IP地址详细信息
* @api {get} api/system/get/ipinfo 17. 获取IP地址详细信息
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -823,7 +923,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/imgupload 17. 上传图片
* @api {post} api/system/imgupload 18. 上传图片
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -835,10 +935,10 @@ class SystemController extends AbstractController
* @apiParam {String} filename post-文件名
* @apiParam {Number} [width] 压缩图片宽默认0
* @apiParam {Number} [height] 压缩图片高默认0
* @apiParam {String} [whcut] 压缩方式
* - 1裁切默认宽、高非0有效
* - 0缩放
* - -1或'auto':保持等比裁切
* @apiParam {String} [whcut] 压缩方式(等比缩放)
* - cover完全覆盖容器可能图片部分不可见width、height必须大于0
* - contain完全装入容器可能容器部分显示空白width、height必须大于0
* - percentage完全装入容器可能容器有一边尺寸不足默认假如width=200、height=0则宽度最大不超过200、高度自动
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -851,11 +951,15 @@ class SystemController extends AbstractController
}
$width = intval(Request::input('width'));
$height = intval(Request::input('height'));
$whcut = intval(Request::input('whcut', 1));
$scale = [2160, 4160, -1];
if ($width > 0 || $height > 0) {
$scale = [$width, $height, $whcut];
}
$whcut = Request::input('whcut');
$whcut = match (strval($whcut)) {
'1' => 'cover',
'0' => 'contain',
'cover',
'contain' => $whcut,
default => 'percentage',
};
$scale = [$width ?: 2160, $height ?: 4160, $whcut];
$path = "uploads/user/picture/" . User::userid() . "/" . date("Ym") . "/";
$image64 = trim(Request::input('image64'));
$fileName = trim(Request::input('filename'));
@@ -864,7 +968,8 @@ class SystemController extends AbstractController
"image64" => $image64,
"path" => $path,
"fileName" => $fileName,
"scale" => $scale
"scale" => $scale,
"quality" => 85
]);
} else {
$data = Base::upload([
@@ -872,7 +977,8 @@ class SystemController extends AbstractController
"type" => 'image',
"path" => $path,
"fileName" => $fileName,
"scale" => $scale
"scale" => $scale,
"quality" => 100
]);
}
if (Base::isError($data)) {
@@ -883,7 +989,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/imgview 18. 浏览图片空间
* @api {get} api/system/get/imgview 19. 浏览图片空间
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -951,7 +1057,7 @@ class SystemController extends AbstractController
];
//
$extension = pathinfo($dirPath . $filename, PATHINFO_EXTENSION);
if (in_array($extension, array('gif', 'jpg', 'jpeg', 'webp', 'png', 'bmp'))) {
if (in_array($extension, ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'])) {
if ($extension = Base::getThumbExt($dirPath . $filename)) {
$array['thumb'] .= "_thumb.{$extension}";
} else {
@@ -980,7 +1086,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/fileupload 19. 上传文件
* @api {post} api/system/fileupload 20. 上传文件
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1008,6 +1114,7 @@ class SystemController extends AbstractController
"image64" => $image64,
"path" => $path,
"fileName" => $fileName,
"quality" => 85
]);
} else {
$data = Base::upload([
@@ -1015,6 +1122,7 @@ class SystemController extends AbstractController
"type" => 'file',
"path" => $path,
"fileName" => $fileName,
"quality" => 100
]);
}
//
@@ -1022,36 +1130,50 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/updatelog 20. 获取更新日志
* @api {get} api/system/get/updatelog 21. 获取更新日志
*
* @apiDescription 获取更新日志
* @apiVersion 1.0.0
* @apiGroup system
* @apiName get__updatelog
*
* @apiParam {Number} [take] 获取数量10-100留空默认50
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function get__updatelog()
{
$take = min(100, max(10, intval(Request::input('take', 50))));
$logPath = base_path('CHANGELOG.md');
$logContent = "";
$logVersion = "";
$logContent = "";
$logResults = [];
if (file_exists($logPath)) {
$logContent = file_get_contents($logPath);
preg_match("/## \[(.*?)\]/", $logContent, $matchs);
if ($matchs) {
$logVersion = $matchs[1] === "Unreleased" ? $matchs[1] : "v{$matchs[1]}";
$content = file_get_contents($logPath);
$sections = preg_split("/## \[(.*?)\]/", $content, -1, PREG_SPLIT_DELIM_CAPTURE);
for ($i = 1; $i < count($sections) && count($logResults) < $take; $i += 2) {
$logResults[] = [
'title' => $sections[$i],
'content' => $sections[$i + 1]
];
}
}
if ($logResults) {
$logVersion = $logResults[0]['title'];
$logContent = implode("\n", array_map(function($item) {
return "## [{$item['title']}]" . $item['content'];
}, $logResults));
}
return Base::retSuccess('success', [
'updateLog' => $logContent ?: false,
'logVersion' => $logVersion,
'updateLog' => $logContent,
]);
}
/**
* @api {get} api/system/email/check 21. 邮件发送测试(限管理员)
* @api {get} api/system/email/check 22. 邮件发送测试(限管理员)
*
* @apiDescription 测试配置邮箱是否能发送邮件
* @apiVersion 1.0.0
@@ -1075,10 +1197,10 @@ class SystemController extends AbstractController
Factory::mailer()
->setDsn("smtp://{$all['account']}:{$all['password']}@{$all['smtp_server']}:{$all['port']}?verify_peer=0")
->setMessage(EmailMessage::create()
->from(env('APP_NAME', 'Task') . " <{$all['account']}>")
->from(Base::settingFind('system', 'system_alias', 'Task') . " <{$all['account']}>")
->to($to)
->subject('Mail sending test')
->html('<p>收到此电子邮件意味着您的邮箱配置正确。</p><p>Receiving this email means that your mailbox is configured correctly.</p>'))
->html('<p>' . Doo::translate('收到此电子邮件意味着您的邮箱配置正确。') . '</p>'))
->send();
}, function () {
throw new \Exception("收件人地址错误或已被忽略");
@@ -1087,7 +1209,7 @@ class SystemController extends AbstractController
} catch (\Throwable $e) {
// 一般是请求超时
if (str_contains($e->getMessage(), "Timed Out")) {
return Base::retError("language.TimedOut");
return Base::retError("邮件发送超时,请检查邮箱配置是否正确");
} elseif ($e->getCode() === 550) {
return Base::retError('邮件内容被拒绝,请检查邮箱是否开启接收功能');
} else {
@@ -1097,7 +1219,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/checkin/export 22. 导出签到数据(限管理员)
* @api {get} api/system/checkin/export 23. 导出签到数据(限管理员)
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -1130,13 +1252,13 @@ class SystemController extends AbstractController
if (count($userid) > 100) {
return Base::retError('导出成员限制最多100个');
}
if (!(is_array($date) && Base::isDate($date[0]) && Base::isDate($date[1]))) {
if (!(is_array($date) && Timer::isDate($date[0]) && Timer::isDate($date[1]))) {
return Base::retError('日期选择错误');
}
if (Carbon::parse($date[1])->timestamp - Carbon::parse($date[0])->timestamp > 35 * 86400) {
return Base::retError('日期范围限制最大35天');
}
if (!(is_array($time) && Base::isTime($time[0]) && Base::isTime($time[1]))) {
if (!(is_array($time) && Timer::isTime($time[0]) && Timer::isTime($time[1]))) {
return Base::retError('时间选择错误');
}
//
@@ -1144,14 +1266,14 @@ class SystemController extends AbstractController
$secondEnd = strtotime("2000-01-01 {$time[1]}") - strtotime("2000-01-01 00:00:00");
//
$headings = [];
$headings[] = '签到人';
$headings[] = '签到日期';
$headings[] = '班次时间';
$headings[] = '首次签到时间';
$headings[] = '首次签到结果';
$headings[] = '最后签到时间';
$headings[] = '最后签到结果';
$headings[] = '参数数据';
$headings[] = Doo::translate('签到人');
$headings[] = Doo::translate('签到日期');
$headings[] = Doo::translate('班次时间');
$headings[] = Doo::translate('首次签到时间');
$headings[] = Doo::translate('首次签到结果');
$headings[] = Doo::translate('最后签到时间');
$headings[] = Doo::translate('最后签到结果');
$headings[] = Doo::translate('参数数据');
//
$sheets = [];
$startD = Carbon::parse($date[0])->startOfDay();
@@ -1178,28 +1300,28 @@ class SystemController extends AbstractController
$lastRecord = $sameCollect?->whereBetween("datetime", $lastBetween)->last();
$firstTimestamp = $firstRecord['timestamp'] ?: 0;
$lastTimestamp = $lastRecord['timestamp'] ?: 0;
if (Base::time() < $startT + $secondStart) {
if (Timer::time() < $startT + $secondStart) {
$firstResult = "-";
} else {
$firstResult = "正常";
$firstResult = Doo::translate("正常");
if (empty($firstTimestamp)) {
$firstResult = "缺卡";
$firstResult = Doo::translate("缺卡");
$styles["E{$index}"] = ["font" => ["color" => ["rgb" => "ff0000"]]];
} elseif ($firstTimestamp > $startT + $secondStart) {
$firstResult = "迟到";
$firstResult = Doo::translate("迟到");
$styles["E{$index}"] = ["font" => ["color" => ["rgb" => "436FF6"]]];
}
}
if (Base::time() < $startT + $secondEnd) {
if (Timer::time() < $startT + $secondEnd) {
$lastResult = "-";
$lastTimestamp = 0;
} else {
$lastResult = "正常";
$lastResult = Doo::translate("正常");
if (empty($lastTimestamp) || $lastTimestamp === $firstTimestamp) {
$lastResult = "缺卡";
$lastResult = Doo::translate("缺卡");
$styles["G{$index}"] = ["font" => ["color" => ["rgb" => "ff0000"]]];
} elseif ($lastTimestamp < $startT + $secondEnd) {
$lastResult = "早退";
$lastResult = Doo::translate("早退");
$styles["G{$index}"] = ["font" => ["color" => ["rgb" => "436FF6"]]];
}
}
@@ -1229,10 +1351,12 @@ class SystemController extends AbstractController
//
$fileName = $users[0]->nickname;
if (count($users) > 1) {
$fileName .= "" . count($userid) . "位成员";
$fileName .= "" . count($userid) . "位成员的签到记录";
} else {
$fileName .= '的签到记录';
}
$fileName .= '签到记录_' . Base::time() . '.xlsx';
$filePath = "temp/checkin/export/" . date("Ym", Base::time());
$fileName = Doo::translate($fileName) . '_' . Timer::time() . '.xlsx';
$filePath = "temp/checkin/export/" . date("Ym", Timer::time());
$export = new BillMultipleExport($sheets);
$res = $export->store($filePath . "/" . $fileName);
if ($res != 1) {
@@ -1264,7 +1388,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/checkin/down 23. 下载导出的签到数据
* @api {get} api/system/checkin/down 24. 下载导出的签到数据
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -1290,7 +1414,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/version 24. 获取版本号
* @api {get} api/system/version 25. 获取版本号
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -1327,7 +1451,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/prefetch 25. 预加载的资源
* @api {get} api/system/prefetch 26. 预加载的资源
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -1350,9 +1474,9 @@ class SystemController extends AbstractController
if ($isMain || $isApp) {
$path = 'js/build/';
$list = Base::readDir(public_path($path), false);
$list = Base::recursiveFiles(public_path($path), false);
foreach ($list as $item) {
if (is_file($item) && filesize($item) > 50 * 1024) {
if (is_file($item) && filesize($item) > 50 * 1024) { // 50KB
$array[] = $path . basename($item);
}
}
@@ -1365,6 +1489,27 @@ class SystemController extends AbstractController
$items = explode("\n", $content);
$array = array_merge($array, $items);
}
// 添加office资源
$officePath = '';
$officeApi = 'http://' . env('APP_IPPR') . '.6/web-apps/apps/api/documents/api.js';
$content = @file_get_contents($officeApi);
if ($content) {
if (preg_match("/const\s+ver\s*=\s*'\/*([^']+)'/", $content, $matches)) {
$officePath = $matches[1];
}
}
if ($officePath) {
$array = array_map(function($item) use ($officePath) {
if (str_starts_with($item, 'office/{path}/')) {
return preg_replace("/office\/{path}\//", '/office/' . $officePath . '/', $item);
}
return $item;
}, $array);
} else {
$array = array_filter($array, function($item) {
return !str_starts_with($item, 'office/{path}/');
});
}
}
return array_map(function($item) use ($version) {

View File

@@ -11,6 +11,7 @@ use App\Module\Doo;
use App\Models\File;
use App\Models\User;
use App\Module\Base;
use App\Module\Timer;
use App\Ldap\LdapUser;
use App\Models\Meeting;
use App\Models\Project;
@@ -20,6 +21,7 @@ use App\Models\UmengAlias;
use App\Models\UserDelete;
use App\Models\UserTransfer;
use App\Models\AbstractModel;
use App\Models\UserCheckinFace;
use App\Models\UserCheckinMac;
use App\Models\UserDepartment;
use App\Models\WebSocketDialog;
@@ -40,7 +42,6 @@ class UsersController extends AbstractController
/**
* @api {get} api/users/login 01. 登录、注册
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup users
* @apiName login
@@ -129,7 +130,7 @@ class UsersController extends AbstractController
return $retError('帐号或密码错误');
}
//
if (in_array('disable', $user->identity)) {
if ($user->isDisable()) {
return $retError('帐号已停用...');
}
Cache::forget("code::" . $email);
@@ -330,8 +331,7 @@ class UsersController extends AbstractController
$data = $user->toArray();
$data['nickname_original'] = $user->getRawOriginal('nickname');
$data['department_name'] = $user->getDepartmentName();
// 适用默认部门下第1级负责人才能添加部门OKR
$data['department_owner'] = UserDepartment::where('parent_id',0)->where('owner_userid', $user->userid())->exists();
$data['department_owner'] = UserDepartment::where('parent_id',0)->where('owner_userid', $user->userid)->exists(); // 适用默认部门下第1级负责人才能添加部门OKR
return Base::retSuccess('success', $data);
}
@@ -347,6 +347,7 @@ class UsersController extends AbstractController
* @apiParam {String} [tel] 电话
* @apiParam {String} [nickname] 昵称
* @apiParam {String} [profession] 职位/职称
* @apiParam {String} [lang] 语言比如zh/en
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -409,6 +410,15 @@ class UsersController extends AbstractController
$upLdap['employeeType'] = $profession;
}
}
// 语言
if (Arr::exists($data, 'lang')) {
$lang = trim(Request::input('lang'));
if (!Doo::checkLanguage($lang)) {
return Base::retError('语言错误');
} else {
$user->lang = $lang;
}
}
//
$user->save();
User::generateToken($user);
@@ -670,9 +680,13 @@ class UsersController extends AbstractController
* - all: 全部
* - 其他值: 非机器人(默认)
* - keys.department 部门ID0表示默认部门不赋值获取所有部门
* - keys.checkin_mac 签到mac地址get_checkin_mac=1时有效
* - keys.checkin_face 人脸图片get_checkin_data=1时有效
* - yes: 仅有人脸图片
* - no: 无人脸图片
* - all: 全部
* - keys.checkin_mac 签到mac地址get_checkin_data=1时有效
*
* @apiParam {Number} [get_checkin_mac] 获取签到mac地址
* @apiParam {Number} [get_checkin_data] 获取签到mac地址
* - 0: 不获取(默认)
* - 1: 获取
* @apiParam {Number} [page] 当前页,默认:1
@@ -689,7 +703,7 @@ class UsersController extends AbstractController
$builder = User::select(['*', 'nickname as nickname_original']);
//
$keys = Request::input('keys');
$getCheckinMac = intval(Request::input('get_checkin_mac')) === 1;
$getCheckinData = intval(Request::input('get_checkin_data')) === 1;
if (is_array($keys)) {
if ($keys['key']) {
if (str_contains($keys['key'], "@")) {
@@ -757,10 +771,17 @@ class UsersController extends AbstractController
$builder->orderBy("is_principal","desc");
}
}
if ($getCheckinMac && isset($keys['checkin_mac'])) {
$builder->whereIn('userid', function ($query) use ($keys) {
$query->select('userid')->from('user_checkin_macs')->where("mac", "like", "%{$keys['checkin_mac']}%");
});
if ($getCheckinData) {
if (isset($keys['checkin_face'])) {
$builder->whereIn('userid', function ($query) use ($keys) {
$query->select('userid')->from('user_checkin_faces')->whereNotNull("faceimg");
});
}
if (isset($keys['checkin_mac'])) {
$builder->whereIn('userid', function ($query) use ($keys) {
$query->select('userid')->from('user_checkin_macs')->where("mac", "like", "%{$keys['checkin_mac']}%");
});
}
}
} else {
$builder->whereNull('disable_at');
@@ -768,11 +789,11 @@ class UsersController extends AbstractController
}
$list = $builder->orderByDesc('userid')->paginate(Base::getPaginate(50, 20));
//
if ($getCheckinMac) {
$list->transform(function (User $user) use ($getCheckinMac) {
if ($getCheckinMac) {
$user->checkin_macs = UserCheckinMac::select(['id', 'mac', 'remark'])->whereUserid($user->userid)->orderBy('id')->get();
}
if ($getCheckinData) {
$list->transform(function (User $user) {
$checkinFace = UserCheckinFace::select(['faceimg'])->whereUserid($user->userid)->first();
$user->checkin_face = $checkinFace ? Base::fillUrl($checkinFace->faceimg) : '';
$user->checkin_macs = UserCheckinMac::select(['id', 'mac', 'remark'])->whereUserid($user->userid)->orderBy('id')->get();
return $user;
});
}
@@ -795,6 +816,7 @@ class UsersController extends AbstractController
* - settemp 设为临时帐号
* - cleartemp 取消临时身份(取消临时帐号)
* - checkin_macs 修改自动签到mac地址需要参数 checkin_macs
* - checkin_face 修改签到人脸图片(需要参数 checkin_face
* - department 修改部门(需要参数 department
* - setdisable 设为离职(需要参数 disable_time、transfer_userid
* - cleardisable 取消离职
@@ -805,6 +827,7 @@ class UsersController extends AbstractController
* @apiParam {String} [nickname] 昵称
* @apiParam {String} [profession] 职位
* @apiParam {String} [checkin_macs] 自动签到mac地址
* @apiParam {String} [checkin_face] 人脸图片地址
* @apiParam {String} [department] 部门
* @apiParam {String} [disable_time] 离职时间
* @apiParam {String} [transfer_userid] 离职交接人
@@ -869,6 +892,11 @@ class UsersController extends AbstractController
}
return UserCheckinMac::saveMac($userInfo->userid, $array);
case 'checkin_face':
$faceimg = $data['checkin_face'] ? $data['checkin_face'] : '';
return UserCheckinFace::saveFace($userInfo->userid, $userInfo->nickname, $faceimg, "管理员上传");
case 'department':
if (!is_array($data['department'])) {
$data['department'] = [];
@@ -900,7 +928,7 @@ class UsersController extends AbstractController
if ($transferUser->userid === $userInfo->userid) {
return Base::retError('不能移交给自己');
}
if (in_array('disable', $transferUser->identity)) {
if ($transferUser->isDisable()) {
return Base::retError('交接人已离职,请选择另一个交接人');
}
break;
@@ -1063,7 +1091,7 @@ class UsersController extends AbstractController
return Base::retError('链接已经使用过', ['code' => 2]);
$oldTime = Carbon::parse($res->created_at)->timestamp;
$time = Base::Time();
$time = Timer::Time();
// 30分钟失效
if (abs($time - $oldTime) > 1800) {
@@ -1087,6 +1115,9 @@ class UsersController extends AbstractController
* @apiGroup users
* @apiName umeng__alias
*
* @apiParam {String} action
* - update: 更新(默认)
* - remove: 删除
* @apiParam {String} alias 别名
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
@@ -1102,6 +1133,13 @@ class UsersController extends AbstractController
'alias.between:2,20' => '别名的长度在2-20个字符',
]);
//
if ($data['action'] === 'remove') {
if ($data['alias']) {
UmengAlias::whereAlias($data['alias'])->delete();
}
return Base::retSuccess('删除成功');
}
//
if (!in_array(Base::platform(), ['ios', 'android'])) {
return Base::retError('设备类型错误');
}
@@ -1147,6 +1185,7 @@ class UsersController extends AbstractController
* @apiParam {String} [name] 会话ID
* @apiParam {String} [sharekey] 分享的key
* @apiParam {String} [username] 用户名称
* @apiParam {String} [userimg] 用户头像
* @apiParam {Array} [userids] 邀请成员
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
@@ -1161,6 +1200,7 @@ class UsersController extends AbstractController
$userids = Request::input('userids');
$sharekey = trim(Request::input('sharekey'));
$username = trim(Request::input('username'));
$userimg = trim(Request::input('userimg')) ?: Base::fillUrl('avatar/' . $username . '.png');
$user = null;
if (!empty($sharekey) && $type === 'join') {
if (!Meeting::getShareInfo($sharekey)) {
@@ -1183,7 +1223,7 @@ class UsersController extends AbstractController
$meeting->save();
} elseif ($type === 'create') {
$meetingid = strtoupper(Base::generatePassword(11, 1));
$name = $name ?: "{$user?->nickname} 发起的会议";
$name = $name ?: Doo::translate("{$user?->nickname} 发起的会议");
$channel = "DooTask:" . substr(md5($meetingid . env("APP_KEY")), 16);
$meeting = Meeting::createInstance([
'meetingid' => $meetingid,
@@ -1238,7 +1278,7 @@ class UsersController extends AbstractController
//
$data['appid'] = $meetingSetting['appid'];
$data['uid'] = $uid;
$data['userimg'] = $sharekey ? Base::fillUrl('avatar/' . $username . '.png') : $user?->userimg;
$data['userimg'] = $sharekey ? $userimg : $user?->userimg;
$data['nickname'] = $sharekey ? $username : $user?->nickname;
$data['token'] = $token;
$data['msgs'] = $msgs;
@@ -1643,8 +1683,14 @@ class UsersController extends AbstractController
$user = User::auth();
//
$list = UserCheckinMac::whereUserid($user->userid)->orderBy('id')->get();
$userface = UserCheckinFace::whereUserid($user->userid)->first();
$data = [
'list' => $list,
'faceimg' => $userface ? Base::fillUrl($userface->faceimg) : ''
];
//
return Base::retSuccess('success', $list);
return Base::retSuccess('success', $data);
}
/**
@@ -1655,7 +1701,11 @@ class UsersController extends AbstractController
* @apiGroup users
* @apiName checkin__save
*
* @apiParam {Array} list 优先级数据,格式:[{mac,remark}]
* @apiParam {String} type 类型
* - face: 人脸识别设置
* - mac: MAC设置
* @apiParam {String} faceimg 人脸图片地址
* @apiParam {Array} list 优先级数据,格式:[{mac,remark}]
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -1669,30 +1719,53 @@ class UsersController extends AbstractController
if ($setting['open'] !== 'open') {
return Base::retError('此功能未开启,请联系管理员开启');
}
if ($setting['edit'] !== 'open') {
return Base::retError('未开放修改权限,请联系管理员');
}
//
$type = Request::input('type');
$list = Request::input('list');
$array = [];
if (empty($list) || !is_array($list)) {
return Base::retError('参数错误');
}
foreach ($list AS $item) {
$item = Base::newTrim($item);
if (Base::isMac($item['mac'])) {
$mac = strtoupper($item['mac']);
$array[$mac] = [
'mac' => $mac,
'remark' => substr($item['remark'], 0, 50),
];
}
}
if (count($array) > 3) {
return Base::retError('最多只能添加3个MAC地址');
$faceimg = Request::input('faceimg');
//
$data = [
'list' => $list,
'faceimg' => $faceimg
];
switch ($type) {
case 'face':
if ($setting['face_upload'] !== 'open') {
return Base::retError('未开放修改权限,请联系管理员');
}
UserCheckinFace::saveFace($user->userid, $user->nickname(), $faceimg, "用户上传");
break;
case 'mac':
if ($setting['edit'] !== 'open') {
return Base::retError('未开放修改权限,请联系管理员');
}
$array = [];
if (empty($list) || !is_array($list)) {
return Base::retError('参数错误');
}
foreach ($list as $item) {
$item = Base::newTrim($item);
if (Base::isMac($item['mac'])) {
$mac = strtoupper($item['mac']);
$array[$mac] = [
'mac' => $mac,
'remark' => substr($item['remark'], 0, 50),
];
}
}
if (count($array) > 3) {
return Base::retError('最多只能添加3个MAC地址');
}
$saveMacRes = UserCheckinMac::saveMac($user->userid, $array);
$data['list'] = $saveMacRes['data'];
break;
default:
return Base::retError('参数错误');
}
//
return UserCheckinMac::saveMac($user->userid, $array);
return Base::retSuccess('修改成功', $data);
}
/**
@@ -2044,24 +2117,22 @@ class UsersController extends AbstractController
{
$user = User::auth();
//
global $_A;
if (!isset($_A["__annual__report_".$user->userid])) {
$year = '2023';
$time = '2300-01-01 00:00:01';
$prefix = \DB::getTablePrefix();
$hireTimestamp = strtotime($user->created_at);
DB::statement("SET SQL_MODE=''");
$year = '2023';
$time = '2300-01-01 00:00:01';
$prefix = \DB::getTablePrefix();
$hireTimestamp = strtotime($user->created_at);
DB::statement("SET SQL_MODE=''");
// 我的任务
$taskDb = DB::table('project_tasks as t')
->join('project_task_users as tu', 't.id', '=', 'tu.task_id')
->where('tu.owner', 1)
->whereYear('t.created_at', $year)
->where('tu.userid', $user->userid);
// 我的任务
$taskDb = DB::table('project_tasks as t')
->join('project_task_users as tu', 't.id', '=', 'tu.task_id')
->where('tu.owner', 1)
->whereYear('t.created_at', $year)
->where('tu.userid', $user->userid);
// 我的任务 - 时长(分钟)
$durationTaskDb = $taskDb->clone()
->selectRaw("
// 我的任务 - 时长(分钟)
$durationTaskDb = $taskDb->clone()
->selectRaw("
{$prefix}t.id,
{$prefix}t.flow_item_name,
{$prefix}t.name as task_name,
@@ -2073,14 +2144,14 @@ class UsersController extends AbstractController
{$prefix}t.created_at,
ifnull(TIMESTAMPDIFF(MINUTE, {$prefix}t.start_at, {$prefix}t.complete_at), 0) as duration
")
->leftJoin('projects as p', 'p.id', '=', 't.project_id')
->leftJoin('project_columns as c', 'c.id', '=', 't.column_id')
->whereNotNull('t.start_at')
->whereNotNull('t.complete_at');
->leftJoin('projects as p', 'p.id', '=', 't.project_id')
->leftJoin('project_columns as c', 'c.id', '=', 't.column_id')
->whereNotNull('t.start_at')
->whereNotNull('t.complete_at');
// 最多聊天用户
$longestChat = DB::table('web_socket_dialogs as d')
->selectRaw("
// 最多聊天用户
$longestChat = DB::table('web_socket_dialogs as d')
->selectRaw("
{$prefix}d.id,
{$prefix}d.name as dialog_name,
{$prefix}d.type as dialog_type,
@@ -2091,113 +2162,112 @@ class UsersController extends AbstractController
{$prefix}u.nickname as user_nickname,
ifnull({$prefix}d.avatar, {$prefix}u.userimg) as avatar
")
->leftJoinSub(function ($query) use ($user, $year) {
$query->select('web_socket_dialog_msgs.dialog_id', DB::raw('count(*) as chat_num'))
->from('web_socket_dialog_msgs')
->where('web_socket_dialog_msgs.userid', $user->userid)
->whereYear('web_socket_dialog_msgs.created_at', $year)
->groupBy('web_socket_dialog_msgs.dialog_id');
}, 'm', 'm.dialog_id', '=', 'd.id')
->leftJoin('web_socket_dialog_users as du', function ($query) use ($user) {
$query->on('d.id', '=', 'du.dialog_id');
$query->where('du.userid', '!=', $user->userid);
$query->where('d.type', 'user');
})
->leftJoin('users as u', 'du.userid', '=', 'u.userid')
->where('d.type', '!=', 'user')
->orWhere('u.bot', 0)
->orderByDesc('m.chat_num')
->first();
if (!empty($longestChat)) {
if ($longestChat->avatar) {
$longestChat->avatar = url($longestChat->avatar);
} else if ($longestChat->dialog_type == 'user') {
$longestChat->avatar = User::getAvatar($longestChat->userid, $longestChat->avatar, $longestChat->user_email, $longestChat->user_nickname);
} else {
$longestChat->avatar = match ($longestChat->dialog_group_type) {
'department' => url("images/avatar/default_group_department.png"),
'project' => url("images/avatar/default_group_project.png"),
'task' => url("images/avatar/default_group_task.png"),
default => url("images/avatar/default_group_people.png"),
};
}
->leftJoinSub(function ($query) use ($user, $year) {
$query->select('web_socket_dialog_msgs.dialog_id', DB::raw('count(*) as chat_num'))
->from('web_socket_dialog_msgs')
->where('web_socket_dialog_msgs.userid', $user->userid)
->whereYear('web_socket_dialog_msgs.created_at', $year)
->groupBy('web_socket_dialog_msgs.dialog_id');
}, 'm', 'm.dialog_id', '=', 'd.id')
->leftJoin('web_socket_dialog_users as du', function ($query) use ($user) {
$query->on('d.id', '=', 'du.dialog_id');
$query->where('du.userid', '!=', $user->userid);
$query->where('d.type', 'user');
})
->leftJoin('users as u', 'du.userid', '=', 'u.userid')
->where('d.type', '!=', 'user')
->orWhere('u.bot', 0)
->orderByDesc('m.chat_num')
->first();
if (!empty($longestChat)) {
if ($longestChat->avatar) {
$longestChat->avatar = url($longestChat->avatar);
} else if ($longestChat->dialog_type == 'user') {
$longestChat->avatar = User::getAvatar($longestChat->userid, $longestChat->avatar, $longestChat->user_email, $longestChat->user_nickname);
} else {
$longestChat->avatar = match ($longestChat->dialog_group_type) {
'department' => url("images/avatar/default_group_department.png"),
'project' => url("images/avatar/default_group_project.png"),
'task' => url("images/avatar/default_group_task.png"),
default => url("images/avatar/default_group_people.png"),
};
}
// 最晚在线时间
$timezone = config('app.timezone');
$latestOnline = UserCheckinRecord::whereUserid($user->userid)
->whereYear(DB::raw('from_unixtime(report_time)'), $year)
->orderByRaw("TIME_FORMAT(DATE_ADD(CONVERT_TZ(from_unixtime(report_time), 'UTC', '$timezone'), INTERVAL 18 HOUR), '%H%i%s') desc")
->first();
//
$_A["__annual__report_".$user->userid] = [
// 本人信息
'user' => [
'userid' => $user->userid,
'email' => $user->email,
'nickname' => $user->nickname,
'avatar' => User::getAvatar($user->userid, $user->userimg, $user->email, $user->nickname)
],
// 入职时间(年月日)
'hire_date' => date("Y-m-d", $hireTimestamp),
// 在职时间(天为单位)
'tenure_days' => floor((strtotime(date('Y-m-d')) - $hireTimestamp) / (24 * 60 * 60)),
// 最晚在线时间
'latest_online_time' => date("Y-m-d H:i:s", $latestOnline->report_time),
// 跟谁聊天最多(发消息的次数。可以是群、私聊、机器人除外)
'longest_chat_user' => $longestChat,
// 跟所有ai机器人聊天的次数
'chat_al_num' => DB::table('web_socket_dialog_msgs as m')
->join('web_socket_dialogs as d', 'd.id', '=', 'm.dialog_id')
->join('web_socket_dialog_users as du', 'd.id', '=', 'du.dialog_id')
->join('users as u', 'du.userid', '=', 'u.userid')
->where('u.email', 'like', "%ai-%")
->where('u.bot', 1)
->where('m.userid', $user->userid)
->whereYear('m.created_at', $year)
->count(),
// 文件创建数量
'file_created_num' => File::whereCreatedId($user->userid)->whereYear('created_at', $year)->count(),
// 参与过的项目
'projects' => DB::table('projects as p')
->select('p.id', 'p.name')
->join('project_users as pu', 'p.id', '=', 'pu.project_id')
->join('project_task_users as ptu', 'p.id', '=', 'ptu.project_id')
->where(function($query) use ($user,$year) {
$query->where('pu.userid', $user->userid);
$query->whereYear('pu.created_at', $year);
})
->orWhere(function($query) use ($user,$year) {
$query->where('ptu.userid', $user->userid);
$query->whereYear('ptu.created_at', $year);
})
->groupBy('p.id')
->take(100)
->get(),
// 任务统计
'tasks' => [
// 总数量
'total' => $taskDb->count(),
// 完成数量
'completed' => $taskDb->clone()->whereNotNUll('t.complete_at')->count(),
// 超时数量
'overtime' => $taskDb->clone()->whereRaw("ifnull({$prefix}t.complete_at,'$time') > ifnull({$prefix}t.end_at,'$time')")->count(),
// 做得最久的任务
'longest_task' => $durationTaskDb->clone()->orderByDesc('duration')->first(),
// 做得最快的任务
'fastest_task' => $durationTaskDb->clone()->orderBy('duration')->first(),
// 每个月完成多少个任务
'month_completed_task' => $taskDb->clone()
->selectRaw("MONTH({$prefix}t.complete_at) AS month, COUNT({$prefix}t.id) AS num")
->whereNotNUll('t.complete_at')
->whereYear('t.complete_at', $year)
->groupBy('month')
->get()
]
];
}
// 最晚在线时间
$timezone = config('app.timezone');
$latestOnline = UserCheckinRecord::whereUserid($user->userid)
->whereYear(DB::raw('from_unixtime(report_time)'), $year)
->orderByRaw("TIME_FORMAT(DATE_ADD(CONVERT_TZ(from_unixtime(report_time), 'UTC', '$timezone'), INTERVAL 18 HOUR), '%H%i%s') desc")
->first();
//
return Base::retSuccess('success', $_A["__annual__report_".$user->userid]);
$data = [
// 本人信息
'user' => [
'userid' => $user->userid,
'email' => $user->email,
'nickname' => $user->nickname,
'avatar' => User::getAvatar($user->userid, $user->userimg, $user->email, $user->nickname)
],
// 入职时间(年月日)
'hire_date' => date("Y-m-d", $hireTimestamp),
// 在职时间(天为单位)
'tenure_days' => floor((strtotime(date('Y-m-d')) - $hireTimestamp) / (24 * 60 * 60)),
// 最晚在线时间
'latest_online_time' => date("Y-m-d H:i:s", $latestOnline->report_time),
// 跟谁聊天最多(发消息的次数。可以是群、私聊、机器人除外)
'longest_chat_user' => $longestChat,
// 跟所有ai机器人聊天的次数
'chat_al_num' => DB::table('web_socket_dialog_msgs as m')
->join('web_socket_dialogs as d', 'd.id', '=', 'm.dialog_id')
->join('web_socket_dialog_users as du', 'd.id', '=', 'du.dialog_id')
->join('users as u', 'du.userid', '=', 'u.userid')
->where('u.email', 'like', "%ai-%")
->where('u.bot', 1)
->where('m.userid', $user->userid)
->whereYear('m.created_at', $year)
->count(),
// 文件创建数量
'file_created_num' => File::whereCreatedId($user->userid)->whereYear('created_at', $year)->count(),
// 参与过的项目
'projects' => DB::table('projects as p')
->select('p.id', 'p.name')
->join('project_users as pu', 'p.id', '=', 'pu.project_id')
->join('project_task_users as ptu', 'p.id', '=', 'ptu.project_id')
->where(function($query) use ($user,$year) {
$query->where('pu.userid', $user->userid);
$query->whereYear('pu.created_at', $year);
})
->orWhere(function($query) use ($user,$year) {
$query->where('ptu.userid', $user->userid);
$query->whereYear('ptu.created_at', $year);
})
->groupBy('p.id')
->take(100)
->get(),
// 任务统计
'tasks' => [
// 总数量
'total' => $taskDb->count(),
// 完成数量
'completed' => $taskDb->clone()->whereNotNUll('t.complete_at')->count(),
// 超时数量
'overtime' => $taskDb->clone()->whereRaw("ifnull({$prefix}t.complete_at,'$time') > ifnull({$prefix}t.end_at,'$time')")->count(),
// 做得最久的任务
'longest_task' => $durationTaskDb->clone()->orderByDesc('duration')->first(),
// 做得最快的任务
'fastest_task' => $durationTaskDb->clone()->orderBy('duration')->first(),
// 每个月完成多少个任务
'month_completed_task' => $taskDb->clone()
->selectRaw("MONTH({$prefix}t.complete_at) AS month, COUNT({$prefix}t.id) AS num")
->whereNotNUll('t.complete_at')
->whereYear('t.complete_at', $year)
->groupBy('month')
->get()
]
];
//
return Base::retSuccess('success', $data);
}
}

View File

@@ -7,8 +7,10 @@ use Cache;
use Request;
use Redirect;
use Response;
use App\Module\Doo;
use App\Models\File;
use App\Models\User;
use App\Models\UserTransfer;
use App\Module\Doo;
use App\Module\Base;
use App\Module\Extranet;
use App\Module\RandomColor;
@@ -23,7 +25,8 @@ use App\Tasks\CheckinRemindTask;
use App\Tasks\CloseMeetingRoomTask;
use App\Tasks\UnclaimedTaskRemindTask;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use LasseRafn\InitialAvatarGenerator\InitialAvatar;
use Laravolt\Avatar\Avatar;
use Swoole\Coroutine;
/**
@@ -66,6 +69,7 @@ class IndexController extends InvokeController
$script = asset_main($array['resources/assets/js/app.js']['file']);
}
return response()->view('main', [
'system_alias' => Base::settingFind('system', 'system_alias', 'WebPage'),
'version' => Base::getVersion(),
'style' => $style,
'script' => $script,
@@ -83,7 +87,7 @@ class IndexController extends InvokeController
/**
* 头像
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response|\Symfony\Component\HttpFoundation\BinaryFileResponse
*/
public function avatar()
{
@@ -91,34 +95,120 @@ class IndexController extends InvokeController
if ($segment && preg_match('/.*?\.png$/i', $segment)) {
$name = substr($segment, 0, -4);
} else {
$name = Request::input('name', 'H');
$name = Request::input('name', 'D');
}
$size = Request::input('size', 128);
$color = Request::input('color');
$background = Request::input('background');
// 移除各种括号及其内容
$pattern = '/[(\[【{<<『「](.*?)[)\]】}>>』」]/u';
$name = preg_replace($pattern, '', $name) ?: preg_replace($pattern, '$1', $name);
// 移除常见标识词(不区分大小写)
$filterWords = [
// 测试相关
'测试', '测试号', '测试账号', '内测', '体验', '试用', 'test', 'testing', 'beta',
// 账号相关
'账号', '帐号', '账户', '帐户', 'account', 'acc', 'id', 'uid',
// 临时标识
'临时', '暂用', '备用', '主号', '副号', '小号', '大号', 'temp', 'temporary', 'backup',
// 系统相关
'系统', '管理员', 'admin', 'administrator', 'system', 'sys', 'root',
// 用户相关
'用户', 'user', '会员', 'member', 'vip', 'svip', 'mvip', 'premium',
// 官方相关
'官方', '正式', '认证', 'official', 'verified', 'auth',
// 客服相关
'客服', '售后', '服务', 'service', 'support', 'helper', 'assistant',
// 游戏相关
'game', 'gaming', 'player', 'gamer',
// 社交媒体相关
'ins', 'instagram', 'fb', 'facebook', 'tiktok', 'tweet', 'weibo', 'wechat',
// 常见后缀
'official', 'real', 'fake', 'copy', 'channel', 'studio', 'team', 'group',
// 职业相关
'dev', 'developer', 'designer', 'artist', 'writer', 'editor',
// 其他
'bot', 'robot', 'auto', 'anonymous', 'guest', 'default', 'new', 'old'
];
$filterWords = array_map(function ($word) {
return preg_quote($word, '/');
}, $filterWords);
$name = preg_replace('/' . implode('|', $filterWords) . '/iu', '', $name) ?: $name;
// 移除分隔符和特殊字符
$filterSymbols = [
// 常见分隔符
'-', '_', '=', '+', '/', '\\', '|',
'~', '@', '#', '$', '%', '^', '&', '*',
// 空格类字符
' ', ' ', "\t", "\n", "\r",
// 标点符号(中英文)
'。', '', '、', '', '', '', '',
'', '…', '‥', '', '″', '℃',
'.', ',', ';', ':', '?', '!',
// 引号类(修正版)
'"', "'", '', '', '“', '”', '`',
// 特殊符号
'★', '☆', '○', '●', '◎', '◇', '◆',
'□', '■', '△', '▲', '▽', '▼',
'♀', '♂', '♪', '♫', '♯', '♭', '♬',
'→', '←', '↑', '↓', '↖', '↗', '↙', '↘',
'√', '×', '÷', '±', '∵', '∴',
'♠', '♥', '♣', '♦',
// emoji 表情符号范围
'\x{1F300}-\x{1F9FF}',
'\x{2600}-\x{26FF}',
'\x{2700}-\x{27BF}',
'\x{1F900}-\x{1F9FF}',
'\x{1F600}-\x{1F64F}'
];
$filterSymbols = array_map(function ($symbol) {
return preg_quote($symbol, '/');
}, $filterSymbols);
$name = preg_replace('/[' . implode('', $filterSymbols) . ']/u', '', $name) ?: $name;
//
if (preg_match('/^[\x{4e00}-\x{9fa5}]+$/u', $name)) {
$name = mb_substr($name, mb_strlen($name) - 2);
}
if (empty($name)) {
$name = 'D';
}
if (empty($color)) {
$color = '#ffffff';
$cacheKey = "avatarBackgroundColor::" . md5($name);
$background = Cache::rememberForever($cacheKey, function() {
$background = Cache::rememberForever($cacheKey, function () {
return RandomColor::one(['luminosity' => 'dark']);
});
}
//
$avatar = new InitialAvatar();
$content = $avatar->name($name)
->size($size)
->color($color)
->background($background)
->fontSize(0.35)
->autoFont()
->generate()
->stream('png', 100);
$path = public_path('uploads/tmp/avatar/' . substr(md5($name), 0, 2));
$file = Base::joinPath($path, md5($name) . '.png');
if (file_exists($file)) {
return response()->file($file, [
'Pragma' => 'public',
'Cache-Control' => 'max-age=1814400',
'Content-type' => 'image/png',
'Expires' => gmdate('D, d M Y H:i:s \G\M\T', time() + 1814400),
]);
}
Base::makeDir($path);
//
return response($content)
$avatar = new Avatar([
'shape' => 'square',
'width' => $size,
'height' => $size,
'chars' => 2,
'fontSize' => $size / 2.9,
'uppercase' => true,
'fonts' => [resource_path('assets/statics/fonts/Source_Han_Sans_SC_Regular.otf')],
'foregrounds' => [$color],
'backgrounds' => [$background],
'border' => [
'size' => 0,
'color' => 'foreground',
'radius' => 0,
],
]);
return response($avatar->create($name)->save($file))
->header('Pragma', 'public')
->header('Cache-Control', 'max-age=1814400')
->header('Content-type', 'image/png')
@@ -155,7 +245,7 @@ class IndexController extends InvokeController
Task::deliver(new DeleteTmpTask('task_worker', 12));
Task::deliver(new DeleteTmpTask('tmp'));
Task::deliver(new DeleteTmpTask('file'));
Task::deliver(new DeleteTmpTask('file_pack'));
Task::deliver(new DeleteTmpTask('tmp_file', 24));
// 删除机器人消息
Task::deliver(new DeleteBotMsgTask());
// 周期任务
@@ -178,80 +268,135 @@ class IndexController extends InvokeController
public function desktop__publish($name = '')
{
$publishVersion = Request::header('publish-version');
$fileNum = Request::get('file_num', 1);
$latestFile = public_path("uploads/desktop/latest");
$latestVersion = file_exists($latestFile) ? trim(file_get_contents($latestFile)) : "0.0.1";
if (strtolower($name) === 'latest') {
$name = $latestVersion;
}
// 上传
// 上传header 中包含 publish-version
if (preg_match("/^\d+\.\d+\.\d+$/", $publishVersion)) {
$uploadSuccessFileNum = (int)Cache::get($publishVersion, 0);
// 判断密钥
$publishKey = Request::header('publish-key');
if ($publishKey !== env('APP_KEY')) {
return Base::retError("key error");
}
if (version_compare($publishVersion, $latestVersion) > -1) { // 限制上传版本必须 ≥ 当前版本
$publishPath = "uploads/desktop/{$publishVersion}/";
$res = Base::upload([
"file" => Request::file('file'),
"type" => 'publish',
"path" => $publishPath,
"fileName" => true
]);
if (Base::isSuccess($res)) {
file_put_contents($latestFile, $publishVersion);
$uploadSuccessFileNum = $uploadSuccessFileNum + 1;
Cache::set($publishVersion, $uploadSuccessFileNum, 7200);
// 判断版本
$action = Request::get('action');
$draftPath = "uploads/desktop-draft/{$publishVersion}/";
if ($action === 'release') {
// 将草稿版本发布为正式版本
$draftPath = public_path($draftPath);
$releasePath = public_path("uploads/desktop/{$publishVersion}/");
if (!file_exists($draftPath)) {
return Base::retError("draft version not exists");
}
if ($uploadSuccessFileNum >= $fileNum){
$directoryPath = public_path("uploads/desktop");
$files = array_filter(scandir($directoryPath), function($file) use($directoryPath) {
return preg_match("/^\d+\.\d+\.\d+$/", $file) && is_dir($directoryPath . '/' . $file) && $file != '.' && $file != '..';
});
sort($files);
foreach ($files as $key => $file) {
if ($file != $publishVersion && $key < count($files) - 2) {
Base::deleteDirAndFile($directoryPath . '/' . $file);
}
if (file_exists($releasePath)) {
Base::deleteDirAndFile($releasePath);
}
Base::copyDirectory($draftPath, $releasePath);
file_put_contents($latestFile, $publishVersion);
// 删除旧版本
Base::deleteDirAndFile(public_path("uploads/desktop-draft"));
$dirs = Base::recursiveDirs(public_path("uploads/desktop"), false);
sort($dirs);
$num = 0;
foreach ($dirs as $dir) {
if (!preg_match("/\/\d+\.\d+\.\d+$/", $dir)) {
continue;
}
$num++;
if ($num < 5) {
continue; // 保留最新的5个版本
}
if (filemtime($dir) > time() - 3600 * 24 * 30) {
continue; // 保留最近30天的版本
}
Base::deleteDirAndFile($dir);
}
return $res;
return Base::retSuccess('success');
}
// 上传草稿版本
return Base::upload([
"file" => Request::file('file'),
"type" => 'publish',
"path" => $draftPath,
"fileName" => true,
"quality" => 100
]);
}
// 列表
if (preg_match("/^\d+\.\d+\.\d+$/", $name)) {
$path = "uploads/desktop/{$name}";
$dirPath = public_path($path);
$lists = Base::readDir($dirPath);
// 列表(访问路径 desktop/publish/{version}
if (preg_match("/^v*(\d+\.\d+\.\d+)$/", $name, $match)) {
$paths = [
"uploads/desktop/{$match[1]}/",
"uploads/desktop/v{$match[1]}/",
"uploads/desktop-draft/{$match[1]}/",
"uploads/desktop-draft/v{$match[1]}/",
];
$avaiPath = null;
foreach ($paths as $path) {
$dirPath = public_path($path);
$isDraft = str_contains($path, 'draft');
if (is_dir($dirPath)) {
$avaiPath = $path;
break;
}
}
if (empty($avaiPath)) {
abort(404);
}
$lists = Base::recursiveFiles($dirPath, false);
$files = [];
foreach ($lists as $file) {
if (str_ends_with($file, '.yml') || str_ends_with($file, '.yaml') || str_ends_with($file, '.blockmap')) {
if (preg_match('/\.(zip|yml|yaml|blockmap)$/i', $file) || str_ends_with($file, '-win.exe')) {
continue;
}
$fileName = Base::leftDelete($file, $dirPath);
$fileName = basename($file, $dirPath);
$fileSize = filesize($file);
$files[] = [
'name' => substr($fileName, 1),
'name' => $fileName,
'time' => date("Y-m-d H:i:s", filemtime($file)),
'size' => $fileSize > 0 ? Base::readableBytes($fileSize) : 0,
'url' => Base::fillUrl($path . $fileName),
'url' => Base::fillUrl(Base::joinPath($avaiPath, $fileName)),
];
}
$otherVersion = [];
$dirs = Base::recursiveDirs(public_path("uploads/desktop"), false);
foreach ($dirs as $dir) {
if (!preg_match("/\/\d+\.\d+\.\d+$/", $dir)) {
continue;
}
$version = basename($dir);
if ($version === $match[1]) {
continue;
}
$otherVersion[] = [
'version' => $version,
'url' => Base::fillUrl("desktop/publish/{$version}"),
];
}
//
return view('desktop', ['version' => $name, 'files' => $files]);
return view('desktop', [
'system_alias' => Base::settingFind('system', 'system_alias', 'WebPage'),
'version' => $match[1],
'files' => $files,
'is_draft' => $isDraft,
'latest_version' => $latestVersion,
'other_version' => array_reverse($otherVersion),
]);
}
// 下载
if ($name && file_exists($latestFile)) {
$publishVersion = file_get_contents($latestFile);
if (preg_match("/^\d+\.\d+\.\d+$/", $publishVersion)) {
$filePath = public_path("uploads/desktop/{$publishVersion}/{$name}");
if (file_exists($filePath)) {
return Response::download($filePath);
}
// 下载Latest 版本内的文件,访问路径 desktop/publish/{fileName}
if ($name) {
$filePath = public_path("uploads/desktop/{$latestVersion}/{$name}");
if (file_exists($filePath)) {
return Response::download($filePath);
}
}
return abort(404);
// 404
abort(404);
}
/**
@@ -294,6 +439,7 @@ class IndexController extends InvokeController
// 文件超过 10m 不支持在线预览,提示下载
if (filesize($file) > 10 * 1024 * 1024) {
return view('download', [
'system_alias' => Base::settingFind('system', 'system_alias', 'WebPage'),
'name' => $name,
'size' => Base::readableBytes(filesize($file)),
'url' => Base::fillUrl($path),
@@ -314,7 +460,7 @@ class IndexController extends InvokeController
], 'inline');
}
// EEUI App 直接在线预览查看
if (str_contains($userAgent, 'eeui') && Base::judgeClientVersion("0.34.47")) {
if (Base::isEEUIApp() && Base::judgeClientVersion("0.34.47")) {
if ($browser === 'safari-mobile') {
$redirectUrl = Base::fillUrl($path);
return <<<EOF
@@ -354,6 +500,36 @@ class IndexController extends InvokeController
return Redirect::to($redirectUrl, 301);
}
/**
* 修复操作离职后续操作(todo 临时,后期删除)
* @return array
*/
public function migration__userdialog()
{
if (Request::header('app-key') !== env('APP_KEY')) {
return Base::retError("key error");
}
go(function() {
Coroutine::sleep(3);
$handled = [];
UserTransfer::orderBy('id')->chunkById(10, function ($transfers) use ($handled) {
/** @var UserTransfer $transfer */
foreach ($transfers as $transfer) {
if (in_array($transfer->original_userid, $handled)) {
continue;
}
$handled[] = $transfer->original_userid;
//
$user = User::find($transfer->original_userid);
if ($user?->isDisable()) {
$transfer->exitDialog();
}
}
});
});
return Base::retSuccess('success');
}
/**
* 保存配置 (todo 已废弃)
* @return string
@@ -362,85 +538,4 @@ class IndexController extends InvokeController
{
return '<!-- Deprecated -->';
}
/**
* 提取所有中文
* @return array|string
*/
public function allcn()
{
if (!Base::is_internal_ip(Base::getIp())) {
// 限制内网访问
return "Forbidden Access";
}
$list = Base::readDir(resource_path());
$array = [];
foreach ($list as $item) {
$content = file_get_contents($item);
preg_match_all("/\\\$L\((.*?)\)/", $content, $matchs);
if ($matchs) {
foreach ($matchs[1] as $text) {
$array[trim(trim($text, '"'), "'")] = trim(trim($text, '"'), "'");
}
}
}
return array_values($array);
}
/**
* 提取所有中文
* @return array|string
*/
public function allcn__php()
{
if (!Base::is_internal_ip(Base::getIp())) {
// 限制内网访问
return "Forbidden Access";
}
$list = Base::readDir(app_path());
$array = [];
foreach ($list as $item) {
$content = file_get_contents($item);
preg_match_all("/(retSuccess|retError|ApiException)\((.*?)[,|)]/", $content, $matchs);
if ($matchs) {
foreach ($matchs[2] as $text) {
$array[trim(trim($text, '"'), "'")] = trim(trim($text, '"'), "'");
}
}
}
return array_values($array);
}
/**
* 提取所有中文
* @return array|string
*/
public function allcn__all()
{
if (!Base::is_internal_ip(Base::getIp())) {
// 限制内网访问
return "Forbidden Access";
}
$list = array_merge(Base::readDir(app_path()), Base::readDir(resource_path()));
$array = [];
foreach ($list as $item) {
if (Base::rightExists($item, ".php") || Base::rightExists($item, ".vue") || Base::rightExists($item, ".js")) {
$content = file_get_contents($item);
preg_match_all("/(['\"])(.*?)[\u{4e00}-\u{9fa5}\u{FE30}-\u{FFA0}]+([\s\S]((?!\n).)*)\\1/u", $content, $matchs);
if ($matchs) {
foreach ($matchs[0] as $text) {
$tmp = preg_replace("/\/\/(.*?)$/", "", $text);
$tmp = preg_replace("/\/\/(.*?)\n/", "", $tmp);
$tmp = str_replace("", "", $tmp);
if (!preg_match("/[\u{4e00}-\u{9fa5}\u{FE30}-\u{FFA0}]/u", $tmp)){
continue; // 没有中文
}
$val = trim(trim($text, '"'), "'");
$array[md5($val)] = $val;
}
}
}
}
return implode("\n", array_values($array));
}
}

View File

@@ -2,10 +2,7 @@
namespace App\Http\Controllers;
use App\Models\User;
use App\Module\Base;
use App\Tasks\IhttpTask;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
@@ -32,24 +29,7 @@ class InvokeController extends BaseController
$msg = "404 not found (" . str_replace("__", "/", $app) . ").";
return Base::ajaxError($msg);
}
// 使用websocket请求
$apiWebsocket = Request::header('Api-Websocket');
if ($apiWebsocket) {
$userid = User::userid();
if ($userid > 0) {
$url = 'http://127.0.0.1:' . env('LARAVELS_LISTEN_PORT') . Request::getRequestUri();
$task = new IhttpTask($url, Request::post(), [
'Content-Type' => Request::header('Content-Type'),
'language' => Request::header('language'),
'token' => Request::header('token'),
]);
$task->setApiWebsocket($apiWebsocket);
$task->setApiUserid($userid);
Task::deliver($task);
return Base::retSuccess('wait');
}
}
// 正常请求
//
$res = $this->__before($method, $action);
if ($res === true || Base::isSuccess($res)) {
return $this->$app();

View File

@@ -5,6 +5,7 @@ namespace App\Http\Middleware;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Module\Doo;
use App\Services\RequestContext;
use Closure;
class WebApi
@@ -18,11 +19,15 @@ class WebApi
*/
public function handle($request, Closure $next)
{
global $_A;
$_A = [];
// 为每个请求生成唯一ID
$request->requestId = RequestContext::generateRequestId();
RequestContext::set('start_time', microtime(true));
RequestContext::set('header_language', $request->header('language'));
// 加载Doo类
Doo::load();
// 解密请求内容
$encrypt = Doo::pgpParseStr($request->header('encrypt'));
if ($request->isMethod('post')) {
$version = $request->header('version');
@@ -47,6 +52,7 @@ class WebApi
$request->setTrustedProxies([$request->getClientIp()], $request::HEADER_X_FORWARDED_PROTO);
}
// 执行下一个中间件
$response = $next($request);
// 加密返回内容
@@ -57,6 +63,16 @@ class WebApi
}
}
// 返回响应
return $response;
}
/**
* @return void
*/
public function terminate()
{
// 请求结束后清理上下文
RequestContext::clear();
}
}

View File

@@ -146,7 +146,7 @@ class LdapUser extends Model
$path = "uploads/user/ldap/";
$file = "{$path}{$user->userid}.jpeg";
Base::makeDir(public_path($path));
if (Base::saveContentImage(public_path($file), $userimg)) {
if (Base::saveContentImage(public_path($file), $userimg, 90)) {
$user->userimg = $file;
}
}

View File

@@ -34,6 +34,26 @@ class AbstractModel extends Model
const ID = 'id';
protected $dates = [
'top_at',
'last_at',
'start_at',
'end_at',
'archived_at',
'complete_at',
'loop_at',
'receive_at',
'line_at',
'disable_at',
'clear_at',
'read_at',
'done_at',
'created_at',
'updated_at',
'deleted_at',

View File

@@ -245,14 +245,13 @@ class File extends AbstractModel
}
}
//
$setting = Base::setting('system');
$path = 'uploads/tmp/' . date("Ym") . '/';
$path = 'uploads/tmp/file/' . date("Ym") . '/';
$data = Base::upload([
"file" => Request::file('files'),
"type" => 'more',
"autoThumb" => false,
"path" => $path,
"size" => ($setting['file_upload_limit'] ?: 0) * 1024
"quality" => 100
]);
if (Base::isError($data)) {
throw new ApiException($data['msg']);
@@ -942,7 +941,7 @@ class File extends AbstractModel
*/
public static function filePushMsg($action, $data = null, $userid = null)
{
$userid = User::auth()->userid();
$userid = User::userid();
if (empty($userid)) {
return;
}

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use App\Module\Base;
use App\Module\Timer;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
@@ -76,7 +77,7 @@ class FileContent extends AbstractModel
'name' => $name,
'ext' => $fileExt
]));
return Base::fillUrl("online/preview/{$name}?key={$key}&version=" . Base::getVersion() . "&__=" . Base::msecTime());
return Base::fillUrl("online/preview/{$name}?key={$key}&version=" . Base::getVersion() . "&__=" . Timer::msecTime());
}
/**
@@ -106,7 +107,7 @@ class FileContent extends AbstractModel
* @param File $file
* @param $content
* @param $download
* @return array|\Symfony\Component\HttpFoundation\StreamedResponse
* @return array|\Symfony\Component\HttpFoundation\BinaryFileResponse
*/
public static function formatContent($file, $content, $download = false)
{
@@ -118,7 +119,7 @@ class FileContent extends AbstractModel
} else {
$filePath = public_path($content['url']);
}
return Base::streamDownload($filePath, $name);
return Base::BinaryFileResponse($filePath, $name);
}
if (empty($content)) {
$content = match ($file->type) {
@@ -147,7 +148,7 @@ class FileContent extends AbstractModel
if ($download) {
$filePath = public_path($path);
if (isset($filePath)) {
return Base::streamDownload($filePath, $name);
return Base::BinaryFileResponse($filePath, $name);
} else {
abort(403, "This file not support download.");
}

View File

@@ -16,7 +16,7 @@ use Illuminate\Support\Carbon;
* @property int|null $userid 创建人
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property string|null $end_at
* @property Carbon|null $end_at
* @property Carbon|null $deleted_at
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()

View File

@@ -22,7 +22,7 @@ use Request;
* @property int|null $personal 是否个人项目
* @property string|null $user_simple 成员总数|1,2,3
* @property int|null $dialog_id 聊天会话ID
* @property string|null $archived_at 归档时间
* @property \Illuminate\Support\Carbon|null $archived_at 归档时间
* @property int|null $archived_userid 归档会员
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
@@ -325,44 +325,65 @@ class Project extends AbstractModel
/**
* 推送消息
* @param string $action
* @param array|self $data 送内容,默认为[id=>项目ID]
* @param array|self $data 送内容
* @param array $userid 指定会员,默认为项目所有成员
*/
public function pushMsg($action, $data = null, $userid = null)
{
if ($data === null) {
$data = ['id' => $this->id];
} elseif ($data instanceof self) {
// 处理数据
if ($data instanceof self) {
$data = $data->toArray();
}
//
$array = [$userid, []];
$data = is_array($data) ? $data : [];
$data['id'] = $this->id;
$data['name'] = $this->name;
$data['desc'] = $this->desc;
// 处理接收用户
$recipients = [$userid, []];
if ($userid === null) {
$array[0] = $this->relationUserids();
$recipients[0] = $this->relationUserids();
} elseif (!is_array($userid)) {
$array[0] = [$userid];
$recipients[0] = [$userid];
}
//
// 移除不需要的字段
unset($data['top_at']);
// 处理所有者权限
if (isset($data['owner'])) {
$owners = ProjectUser::whereProjectId($data['id'])->whereOwner(1)->pluck('userid')->toArray();
$array = [array_intersect($array[0], $owners), array_diff($array[0], $owners)];
$owners = ProjectUser::whereProjectId($data['id'])
->whereOwner(1)
->pluck('userid')
->toArray();
$recipients = [
array_intersect($recipients[0], $owners),
array_diff($recipients[0], $owners)
];
}
//
foreach ($array as $index => $item) {
// 发送推送
foreach ($recipients as $index => $userids) {
if (empty($userids)) {
continue;
}
if ($index > 0) {
$data['owner'] = 0;
}
$params = [
'ignoreFd' => Request::header('fd'),
'userid' => array_values($item),
'userid' => array_values($userids),
'msg' => [
'type' => 'project',
'action' => $action,
'data' => $data,
]
];
$task = new PushTask($params, false);
Task::deliver($task);
Task::deliver(new PushTask($params, false));
}
}

View File

@@ -4,13 +4,14 @@ namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use App\Module\Doo;
/**
* App\Models\ProjectPermission
*
* @property int $id
* @property int|null $project_id 项目ID
* @property string $permissions 权限
* @property array $permissions 权限
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
@@ -38,6 +39,7 @@ class ProjectPermission extends AbstractModel
const TASK_LIST_SORT = 'task_list_sort'; // 列表排序
const TASK_ADD = 'task_add'; // 任务添加
const TASK_UPDATE = 'task_update'; // 任务更新
const TASK_TIME = 'task_time'; // 任务时间
const TASK_STATUS = 'task_status'; // 任务状态
const TASK_REMOVE = 'task_remove'; // 任务删除
const TASK_ARCHIVED = 'task_archived'; // 任务归档
@@ -69,7 +71,7 @@ class ProjectPermission extends AbstractModel
/**
* 权限
* @param $value
* @return string
* @return array
*/
public function getPermissionsAttribute($value)
{
@@ -110,7 +112,8 @@ class ProjectPermission extends AbstractModel
self::TASK_LIST_REMOVE => [self::PERMISSIONS['project_leader']],
self::TASK_LIST_SORT => $projectTaskList,
self::TASK_ADD => $projectTaskList,
self::TASK_UPDATE => [self::PERMISSIONS['project_leader'], self::PERMISSIONS['task_leader'], self::PERMISSIONS['task_assist']],
self::TASK_UPDATE => $taskUpdate = [self::PERMISSIONS['project_leader'], self::PERMISSIONS['task_leader'], self::PERMISSIONS['task_assist']],
self::TASK_TIME => $taskUpdate,
self::TASK_STATUS => $taskStatus = [self::PERMISSIONS['project_leader'], self::PERMISSIONS['task_leader']],
self::TASK_REMOVE => $taskStatus,
self::TASK_ARCHIVED => $taskStatus,
@@ -153,13 +156,14 @@ class ProjectPermission extends AbstractModel
$userid = User::userid();
$permissions = self::getPermission($project->id, $action);
switch ($action) {
// 任务添加,任务更新, 任务状态, 任务删除, 任务完成, 任务归档, 任务移动
// 任务添加,任务更新, 任务状态, 任务删除, 任务完成, 任务归档, 任务移动
case self::TASK_LIST_ADD:
case self::TASK_LIST_UPDATE:
case self::TASK_LIST_REMOVE:
case self::TASK_LIST_SORT:
case self::TASK_ADD:
case self::TASK_UPDATE:
case self::TASK_TIME:
case self::TASK_STATUS:
case self::TASK_REMOVE:
case self::TASK_ARCHIVED:
@@ -195,7 +199,7 @@ class ProjectPermission extends AbstractModel
$desc = [];
rsort($permissions);
foreach ($permissions as $permission) {
$desc[] = self::PERMISSIONS_DESC[$permission];
$desc[] = Doo::translate(self::PERMISSIONS_DESC[$permission]);
}
$desc = array_reverse($desc);
throw new ApiException(sprintf("仅限%s操作", implode('、', $desc)));

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use App\Module\Timer;
use DB;
use Arr;
use Request;
@@ -26,12 +27,12 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property string|null $name 标题
* @property string|null $color 颜色
* @property string|null $desc 描述
* @property string|null $start_at 计划开始时间
* @property string|null $end_at 计划结束时间
* @property string|null $archived_at 归档时间
* @property \Illuminate\Support\Carbon|null $start_at 计划开始时间
* @property \Illuminate\Support\Carbon|null $end_at 计划结束时间
* @property \Illuminate\Support\Carbon|null $archived_at 归档时间
* @property int|null $archived_userid 归档会员
* @property int|null $archived_follow 跟随项目归档(项目取消归档时任务也取消归档)
* @property string|null $complete_at 完成时间
* @property \Illuminate\Support\Carbon|null $complete_at 完成时间
* @property int|null $userid 创建人
* @property int|null $visibility 任务可见性1-项目人员 2-任务人员 3-指定成员
* @property int|null $p_level 优先级
@@ -39,7 +40,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property string|null $p_color 优先级颜色
* @property int|null $sort 排序(ASC)
* @property string|null $loop 重复周期
* @property string|null $loop_at 下一次重复时间
* @property \Illuminate\Support\Carbon|null $loop_at 下一次重复时间
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
@@ -368,7 +369,7 @@ class ProjectTask extends AbstractModel
}
}, $matches[0]);
}, $content);
return Base::cutStr(strip_tags($content), 100, 0, "...");
return Base::cutStr(strip_tags($content), 100);
}
/**
@@ -438,7 +439,7 @@ class ProjectTask extends AbstractModel
// 时间
if ($times) {
list($start, $end) = is_string($times) ? explode(",", $times) : (is_array($times) ? $times : []);
if (Base::isDate($start) && Base::isDate($end) && $start != $end) {
if (Timer::isDate($start) && Timer::isDate($end) && $start != $end) {
$task->start_at = Carbon::parse($start);
$task->end_at = Carbon::parse($end);
}
@@ -537,7 +538,7 @@ class ProjectTask extends AbstractModel
if ($task->parent_id == 0 && $subtasks && is_array($subtasks)) {
foreach ($subtasks as $subtask) {
list($start, $end) = is_string($subtask['times']) ? explode(",", $subtask['times']) : (is_array($subtask['times']) ? $subtask['times'] : []);
if (Base::isDate($start) && Base::isDate($end) && $start != $end) {
if (Timer::isDate($start) && Timer::isDate($end) && $start != $end) {
if (Carbon::parse($start)->lt($task->start_at)) {
throw new ApiException('子任务开始时间不能小于主任务开始时间');
}
@@ -663,7 +664,7 @@ class ProjectTask extends AbstractModel
if ($mainTask?->complete_at) {
throw new ApiException('主任务已完成,无法修改子任务状态');
}
if (Base::isDate($data['complete_at'])) {
if (Timer::isDate($data['complete_at'])) {
// 标记已完成
if ($this->complete_at) {
throw new ApiException('任务已完成');
@@ -740,6 +741,7 @@ class ProjectTask extends AbstractModel
}
}
$updateMarking['is_update_project'] = true;
$this->updated_at = Carbon::now();
$this->syncDialogUser();
}
// 可见性
@@ -768,22 +770,23 @@ class ProjectTask extends AbstractModel
if (Arr::exists($data, 'times')) {
$oldAt = [Carbon::parse($this->start_at), Carbon::parse($this->end_at)];
$oldStringAt = $this->start_at ? ($oldAt[0]->toDateTimeString() . '~' . $oldAt[1]->toDateTimeString()) : '';
$clearSubTaskTime = false;
$this->start_at = null;
$this->end_at = null;
$times = $data['times'];
list($start, $end, $desc) = is_string($times) ? explode(",", $times) : (is_array($times) ? $times : []);
if (Base::isDate($start) && Base::isDate($end) && $start != $end) {
if (Timer::isDate($start) && Timer::isDate($end) && $start != $end) {
$start_at = Carbon::parse($start);
$end_at = Carbon::parse($end);
if ($this->parent_id > 0) {
// 判断同步主任务时间(子任务时间 超出 主任务)
if ($mainTask) {
$isUp = false;
if ($start_at->lt(Carbon::parse($mainTask->start_at))) {
if (empty($mainTask->start_at) || $start_at->lt(Carbon::parse($mainTask->start_at))) {
$mainTask->start_at = $start_at;
$isUp = true;
}
if ($end_at->gt(Carbon::parse($mainTask->end_at))) {
if (empty($mainTask->end_at) || $end_at->gt(Carbon::parse($mainTask->end_at))) {
$mainTask->end_at = $end_at;
$isUp = true;
}
@@ -801,6 +804,7 @@ class ProjectTask extends AbstractModel
// 清空子任务时间(子任务时间等于主任务时间)
$this->start_at = $mainTask->start_at;
$this->end_at = $mainTask->end_at;
$clearSubTaskTime = true;
}
}
if ($this->parent_id == 0) {
@@ -831,7 +835,7 @@ class ProjectTask extends AbstractModel
}
});
}
$newStringAt = $this->start_at ? ($this->start_at->toDateTimeString() . '~' . $this->end_at->toDateTimeString()) : '';
$newStringAt = $this->start_at && !$clearSubTaskTime ? ($this->start_at->toDateTimeString() . '~' . $this->end_at->toDateTimeString()) : '';
$newDesc = $desc ? "(备注:{$desc}" : "";
$this->addLog("修改{任务}时间" . $newDesc, [
'change' => [$oldStringAt, $newStringAt]
@@ -895,6 +899,7 @@ class ProjectTask extends AbstractModel
$row->delete();
}
}
$this->updated_at = Carbon::now();
$this->syncDialogUser();
}
// 背景色
@@ -961,8 +966,6 @@ class ProjectTask extends AbstractModel
}
}
$this->save();
if ($this->start_at instanceof \DateTimeInterface) $this->start_at = $this->start_at->format('Y-m-d H:i:s');
if ($this->end_at instanceof \DateTimeInterface) $this->end_at = $this->end_at->format('Y-m-d H:i:s');
});
return true;
}
@@ -1278,7 +1281,9 @@ class ProjectTask extends AbstractModel
// 标记已完成
if ($this->parent_id == 0) {
if (self::whereParentId($this->id)->whereCompleteAt(null)->exists()) {
throw new ApiException('子任务未完成');
throw new ApiException('子任务未完成', [
'task_id' => $this->id
], -4004);
}
}
if (!$this->hasOwner()) {
@@ -1424,7 +1429,11 @@ class ProjectTask extends AbstractModel
'detail' => $detail,
];
if ($this->parent_id) {
$record['subtitle'] = $this->name;
$record['subtask'] = [
'id' => $this->id,
'parent_id' => $this->parent_id,
'name' => $this->name,
];
}
if ($record) {
$array['record'] = $record;
@@ -1609,14 +1618,18 @@ class ProjectTask extends AbstractModel
'dialog_id' => $this->dialog_id,
];
//
$projectOwnerids = ProjectUser::whereProjectId($this->project_id)->whereOwner(1)->pluck('userid')->toArray(); // 项目负责人
//
$array = [];
if (empty($userids)) {
// 默认 项目成员 与 项目负责人,任务负责人、协助人的差集
$projectUserids = ProjectUser::whereProjectId($this->project_id)->pluck('userid')->toArray(); // 项目成员
$projectOwner = ProjectUser::whereProjectId($this->project_id)->whereOwner(1)->pluck('userid')->toArray(); // 项目负责人
$taskOwnerAndAssists = ProjectTaskUser::select(['userid', 'owner'])->whereIn('owner', [0, 1])->whereTaskId($this->id)->pluck('userid')->toArray();
$subUserids = ProjectTaskUser::whereTaskPid($this->id)->pluck('userid')->toArray();
$userids = array_diff($projectUserids, $projectOwner, $taskOwnerAndAssists, $subUserids);
$userids = array_diff($projectUserids, $projectOwnerids, $taskOwnerAndAssists, $subUserids);
} else {
// 保证项目负责人都能看到
$userids = array_diff($userids, $projectOwnerids);
}
//
$array[] = [
@@ -1680,16 +1693,6 @@ class ProjectTask extends AbstractModel
if (empty($botUser)) {
return;
}
$dataId = $this->parent_id ?: $this->id;
$taskHtml = "<span class=\"mention task\" data-id=\"{$dataId}\">#{$this->name}</span>";
$text = match ($type) {
1 => "您的任务 {$taskHtml} 即将超时。",
2 => "您的任务 {$taskHtml} 已经超时。",
3 => "您的任务 {$taskHtml} 时间已修改。",
default => "您有一个新任务 {$taskHtml}",
};
/** @var User $user */
foreach ($receivers as $receiver) {
$data = [
@@ -1700,13 +1703,34 @@ class ProjectTask extends AbstractModel
if (in_array($type, [1, 2]) && ProjectTaskPushLog::where($data)->exists()) {
continue;
}
if ($owners[$receiver->userid]) {
$title = match ($type) {
1 => "您负责的任务即将超时",
2 => "您负责的任务已经超时",
3 => "您负责的任务时间已修改",
default => "您有一个新任务",
};
} else {
$title = match ($type) {
1 => "您协助的任务即将超时",
2 => "您协助的任务已经超时",
3 => "您协助的任务时间已修改",
default => "您有一个新协助任务",
};
}
//
$replace = $owners[$receiver->userid] ? "您负责的任务" : "您协助的任务";
$dialog = WebSocketDialog::checkUserDialog($botUser, $receiver->userid);
if ($dialog) {
ProjectTaskPushLog::createInstance($data)->save();
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', [
'text' => str_replace("您的任务", $replace, $text) . $suffix
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'task_list',
'title' => $title . $suffix,
'list' => [
[
'id' => $this->parent_id ?: $this->id,
'name' => $this->name,
]
],
], in_array($type, [0, 3]) ? $userid : $botUser->userid);
}
}
@@ -1714,11 +1738,12 @@ class ProjectTask extends AbstractModel
/**
* 移动任务
* @param int $project_id
* @param int $column_id
* @param int $projectId
* @param int $columnId
* @param int $flowItemId
* @param array $owner
* @param array $assist
* @param string $completeAt
* @return bool
*/
public function moveTask(int $projectId, int $columnId,int $flowItemId = 0,array $owner = [], array $assist = [], string $completeAt='')

View File

@@ -53,8 +53,8 @@ class ProjectTaskContent extends AbstractModel
$array = $this->toArray();
$array['content'] = file_get_contents($filePath) ?: '';
if ($array['content']) {
$replace = Base::fillUrl('uploads/task');
$array['content'] = str_replace('{{RemoteURL}}uploads/task', $replace, $array['content']);
$replace = Base::fillUrl('uploads');
$array['content'] = str_replace('{{RemoteURL}}uploads', $replace, $array['content']);
}
return $array;
}
@@ -74,18 +74,24 @@ class ProjectTaskContent extends AbstractModel
$oldContent = $content;
$path = 'uploads/task/content/' . date("Ym") . '/' . $task_id . '/';
//
preg_match_all("/<img\s+src=\"data:image\/(png|jpg|jpeg|webp);base64,(.*?)\"/s", $content, $matchs);
preg_match_all('/<img[^>]*?src=\\\\?["\']data:image\/(png|jpg|jpeg|webp);base64,(.*?)\\\\?["\']/s', $content, $matchs);
foreach ($matchs[2] as $key => $text) {
$tmpPath = $path . 'attached/';
Base::makeDir(public_path($tmpPath));
$tmpPath .= md5($text) . "." . $matchs[1][$key];
if (Base::saveContentImage(public_path($tmpPath), base64_decode($text))) {
if (Base::saveContentImage(public_path($tmpPath), base64_decode($text), 90)) {
$paramet = getimagesize(public_path($tmpPath));
$content = str_replace($matchs[0][$key], '<img src="{{RemoteURL}}' . $tmpPath . '" original-width="' . $paramet[0] . '" original-height="' . $paramet[1] . '"', $content);
}
}
$pattern = '/<img(.*?)src=("|\')https*:\/\/(.*?)\/(uploads\/task\/content\/(.*?))\2/is';
$content = preg_replace($pattern, '<img$1src=$2{{RemoteURL}}$4$2', $content);
preg_match_all('/(<img[^>]*?src=\\\\?["\'])(https?:\/\/[^\/]+\/)(uploads\/[^\s"\'>]+)(\\\\?["\'][^>]*?>)/i', $content, $matches);
foreach ($matches[0] as $key => $fullMatch) {
$filePath = public_path($matches[3][$key]);
if (file_exists($filePath)) {
$replacement = $matches[1][$key] . '{{RemoteURL}}' . $matches[3][$key] . $matches[4][$key];
$content = str_replace($fullMatch, $replacement, $content);
}
}
//
$filePath = $path . md5($content);
$publicPath = public_path($filePath);

View File

@@ -9,7 +9,7 @@ namespace App\Models;
* @property int|null $project_id 项目ID
* @property int|null $userid 成员ID
* @property int|null $owner 是否负责人
* @property string|null $top_at 置顶时间
* @property \Illuminate\Support\Carbon|null $top_at 置顶时间
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Project|null $project

View File

@@ -74,7 +74,7 @@ class Report extends AbstractModel
public function receivesUser(): BelongsToMany
{
return $this->belongsToMany(User::class, ReportReceive::class, "rid", "userid")
->withPivot("receive_time", "read");
->withPivot("receive_at", "read");
}
public function sendUser()
@@ -82,15 +82,6 @@ class Report extends AbstractModel
return $this->hasOne(User::class, "userid", "userid");
}
public function getTypeAttribute($value): string
{
return match ($value) {
Report::WEEKLY => "周报",
Report::DAILY => "日报",
default => "",
};
}
public function getContentAttribute($value): string
{
return htmlspecialchars_decode($value);

View File

@@ -10,7 +10,7 @@ use Illuminate\Database\Eloquent\Model;
*
* @property int $id
* @property int $rid
* @property string|null $receive_time 接收时间
* @property \Illuminate\Support\Carbon|null $receive_at 接收时间
* @property int $userid 接收人
* @property int $read 是否已读
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
@@ -24,7 +24,7 @@ use Illuminate\Database\Eloquent\Model;
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereRead($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereReceiveTime($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereReceiveAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereRid($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereUserid($value)
* @mixin \Eloquent
@@ -38,7 +38,7 @@ class ReportReceive extends AbstractModel
protected $fillable = [
"rid",
"receive_time",
"receive_at",
"userid",
"read",
];

View File

@@ -3,6 +3,7 @@
namespace App\Models;
use App\Module\Base;
use App\Module\Timer;
/**
* App\Models\Setting
@@ -32,6 +33,32 @@ use App\Module\Base;
*/
class Setting extends AbstractModel
{
/**
* 格式化设置参数
* @param $value
* @return array|mixed
*/
public function getSettingAttribute($value)
{
if (is_array($value)) {
return $value;
}
$value = Base::json2array($value);
switch ($this->name) {
case 'system':
$value['system_alias'] = $value['system_alias'] ?: env('APP_NAME');
if (!is_array($value['task_default_time']) || count($value['task_default_time']) != 2 || !Timer::isTime($value['task_default_time'][0]) || !Timer::isTime($value['task_default_time'][1])) {
$value['task_default_time'] = ['09:00', '18:00'];
}
break;
case 'fileSetting':
$value['permission_pack_type'] = $value['permission_pack_type'] ?: 'all';
$value['permission_pack_userids'] = is_array($value['permission_pack_userids']) ? $value['permission_pack_userids'] : [];
break;
}
return $value;
}
/**
* 验证邮箱地址(过滤忽略地址)
* @param $array

View File

@@ -10,8 +10,8 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property int $id
* @property string|null $args
* @property string|null $error
* @property string|null $start_at 开始时间
* @property string|null $end_at 结束时间
* @property \Illuminate\Support\Carbon|null $start_at 开始时间
* @property \Illuminate\Support\Carbon|null $end_at 结束时间
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at

View File

@@ -157,7 +157,7 @@ class UmengAlias extends AbstractModel
'vivo_category' => 'IM',
'huawei_channel_importance' => 'NORMAL',
'huawei_channel_category' => 'IM',
'channel_fcm' => 1,
'channel_fcm' => 0,
],
]);

View File

@@ -6,6 +6,8 @@ namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use App\Module\Doo;
use App\Module\Table\OnlineData;
use App\Services\RequestContext;
use Cache;
use Carbon\Carbon;
@@ -13,28 +15,29 @@ use Carbon\Carbon;
* App\Models\User
*
* @property int $userid
* @property array $identity 身份
* @property array $department 所属部门
* @property array $identity
* @property array $department
* @property string|null $az A-Z
* @property string|null $pinyin 拼音(主要用于搜索)
* @property string|null $email 邮箱
* @property string|null $email
* @property string|null $tel 联系电话
* @property string $nickname 昵称
* @property string|null $profession 职位/职称
* @property string $userimg 头像
* @property string $nickname
* @property string|null $profession
* @property string $userimg
* @property string|null $encrypt
* @property string|null $password 登录密码
* @property int|null $changepass 登录需要修改密码
* @property int|null $login_num 累计登录次数
* @property string|null $last_ip 最后登录IP
* @property string|null $last_at 最后登录时间
* @property \Illuminate\Support\Carbon|null $last_at 最后登录时间
* @property string|null $line_ip 最后在线IP接口
* @property string|null $line_at 最后在线时间(接口)
* @property \Illuminate\Support\Carbon|null $line_at 最后在线时间(接口)
* @property int|null $task_dialog_id 最后打开的任务会话ID
* @property string|null $created_ip 注册IP
* @property string|null $disable_at 禁用时间(离职时间)
* @property \Illuminate\Support\Carbon|null $disable_at
* @property int|null $email_verity 邮箱是否已验证
* @property int|null $bot 是否机器人
* @property string|null $lang 语言首选项
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
@@ -58,6 +61,7 @@ use Carbon\Carbon;
* @method static \Illuminate\Database\Eloquent\Builder|User whereEmailVerity($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereEncrypt($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereIdentity($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereLang($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereLastAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereLastIp($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereLineAt($value)
@@ -95,7 +99,13 @@ class User extends AbstractModel
*/
public function getNicknameAttribute($value)
{
return $value ?: Base::cardFormat($this->email);
if ($value) {
if (UserBot::isSystemBot($this->email)) {
return Doo::translate($value);
}
return $value;
}
return Base::formatName($this->email);
}
/**
@@ -150,7 +160,7 @@ class User extends AbstractModel
});
$array = [];
foreach ($list as $item) {
$array[] = $item['name'] . ($item['owner_userid'] === $this->userid ? '(M)' : '');
$array[] = $item['name'] . ($item['owner_userid'] === $this->userid ? ' (M)' : '');
}
return implode(', ', $array);
}
@@ -185,7 +195,7 @@ class User extends AbstractModel
*/
public function getOnlineStatus()
{
$online = $this->bot || Cache::get("User::online:" . $this->userid) === "on";
$online = $this->bot || OnlineData::live($this->userid) > 0;
if ($online) {
return true;
}
@@ -411,7 +421,7 @@ class User extends AbstractModel
throw new ApiException('请登录后继续...', [], -1);
}
}
if (in_array('disable', $user->identity)) {
if ($user->isDisable()) {
throw new ApiException('帐号已停用...', [], -1);
}
if ($identity) {
@@ -426,9 +436,8 @@ class User extends AbstractModel
*/
private static function authInfo()
{
global $_A;
if (isset($_A["__static_auth"])) {
return $_A["__static_auth"];
if (RequestContext::has('auth')) {
return RequestContext::get('auth');
}
if (Doo::userId() > 0
&& !Doo::userExpired()
@@ -440,13 +449,19 @@ class User extends AbstractModel
if (Carbon::parse($user->line_at)->addSeconds(30)->lt(Carbon::now())) {
$upArray['line_at'] = Carbon::now();
}
$headerLanguage = RequestContext::get('header_language');
if (empty($user->lang) || $headerLanguage) {
if (Doo::checkLanguage($headerLanguage) && $user->lang != $headerLanguage) {
$upArray['lang'] = $headerLanguage;
}
}
if ($upArray) {
$user->updateInstance($upArray);
$user->save();
}
return $_A["__static_auth"] = $user;
return RequestContext::save('auth', $user);
}
return $_A["__static_auth"] = false;
return RequestContext::save('auth', false);
}
/**
@@ -480,22 +495,21 @@ class User extends AbstractModel
* @param int $userid 会员ID
* @return self
*/
public static function userid2basic($userid)
public static function userid2basic($userid, $addField = [])
{
global $_A;
if (empty($userid)) {
return null;
}
$userid = intval($userid);
if (isset($_A["__static_userid2basic_" . $userid])) {
return $_A["__static_userid2basic_" . $userid];
if (RequestContext::has("userid2basic_" . $userid)) {
return RequestContext::get("userid2basic_" . $userid);
}
$userInfo = self::whereUserid($userid)->select(User::$basicField)->first();
$userInfo = self::whereUserid($userid)->select(array_merge(User::$basicField, $addField))->first();
if ($userInfo) {
$userInfo->online = $userInfo->getOnlineStatus();
$userInfo->department_name = $userInfo->getDepartmentName();
}
return $_A["__static_userid2basic_" . $userid] = ($userInfo ?: []);
return RequestContext::save("userid2basic_" . $userid, $userInfo ?: []);
}
@@ -578,7 +592,7 @@ class User extends AbstractModel
case 'ai-gemini@bot.system':
return url("images/avatar/default_gemini.png");
case 'ai-zhipu@bot.system':
return url("images/avatar/default_zhipu.png");
return url("images/avatar/default_zhipu.png");
case 'bot-manager@bot.system':
return url("images/avatar/default_bot.png");
case 'meeting-alert@bot.system':
@@ -651,7 +665,9 @@ class User extends AbstractModel
])->save();
}
//
$update['nickname'] = UserBot::systemBotName($email);
if (empty($update['nickname'])) {
$update['nickname'] = UserBot::systemBotName($email);
}
}
if ($update) {
$botUser->updateInstance($update);

View File

@@ -5,6 +5,7 @@ namespace App\Models;
use App\Module\Base;
use App\Module\Doo;
use App\Module\Extranet;
use App\Module\Timer;
use App\Tasks\JokeSoupTask;
use Cache;
use Carbon\Carbon;
@@ -16,7 +17,7 @@ use Carbon\Carbon;
* @property int|null $userid 所属人ID
* @property int|null $bot_id 机器人ID
* @property int|null $clear_day 消息自动清理天数
* @property string|null $clear_at 下一次清理时间
* @property \Illuminate\Support\Carbon|null $clear_at 下一次清理时间
* @property string|null $webhook_url 消息webhook地址
* @property int|null $webhook_num 消息webhook请求次数
* @property \Illuminate\Support\Carbon|null $created_at
@@ -44,6 +45,16 @@ use Carbon\Carbon;
class UserBot extends AbstractModel
{
/**
* 判断是否系统机器人
* @param $email
* @return bool
*/
public static function isSystemBot($email)
{
return str_ends_with($email, '@bot.system') && self::systemBotName($email);
}
/**
* 系统机器人名称
* @param $name string 邮箱 或 邮箱前缀
@@ -54,7 +65,7 @@ class UserBot extends AbstractModel
if (str_contains($name, "@")) {
$name = explode("@", $name)[0];
}
return match ($name) {
$name = match ($name) {
'system-msg' => '系统消息',
'task-alert' => '任务提醒',
'check-in' => '签到打卡',
@@ -71,6 +82,7 @@ class UserBot extends AbstractModel
'okr-alert' => 'OKR提醒',
default => '', // 不是系统机器人时返回空(也可以拿来判断是否是系统机器人)
};
return Doo::translate($name);
}
/**
@@ -80,79 +92,118 @@ class UserBot extends AbstractModel
*/
public static function quickMsgs($email)
{
return match ($email) {
'check-in@bot.system' => [
[
'key' => 'checkin',
'label' => Doo::translate('我要打卡')
], [
'key' => 'it',
'label' => Doo::translate('IT资讯')
], [
'key' => '36ke',
'label' => Doo::translate('36氪')
], [
'key' => '60s',
'label' => Doo::translate('60s读世界')
], [
'key' => 'joke',
'label' => Doo::translate('开心笑话')
], [
'key' => 'soup',
'label' => Doo::translate('心灵鸡汤')
]
],
'anon-msg@bot.system' => [
[
'key' => 'help',
'label' => Doo::translate('使用说明')
], [
'key' => 'privacy',
'label' => Doo::translate('隐私说明')
],
],
'bot-manager@bot.system' => [
[
'key' => '/help',
'label' => Doo::translate('帮助指令')
], [
'key' => '/api',
'label' => Doo::translate('Api接口文档')
], [
'key' => '/list',
'label' => Doo::translate('我的机器人')
],
],
'ai-openai@bot.system',
'ai-claude@bot.system',
'ai-wenxin@bot.system',
'ai-gemini@bot.system',
'ai-zhipu@bot.system',
'ai-qianwen@bot.system' => [
[
'key' => '%3A.clear',
'label' => Doo::translate('清空上下文')
]
],
default => [],
};
switch ($email) {
case 'check-in@bot.system':
$menu = [
/*[
'key' => 'it',
'label' => Doo::translate('IT资讯')
], [
'key' => '36ke',
'label' => Doo::translate('36氪')
], [
'key' => '60s',
'label' => Doo::translate('60s读世界')
], [
'key' => 'joke',
'label' => Doo::translate('开心笑话')
], [
'key' => 'soup',
'label' => Doo::translate('心灵鸡汤')
]*/
];
$setting = Base::setting('checkinSetting');
if ($setting['open'] !== 'open') {
return $menu;
}
if (in_array('locat', $setting['modes']) && Base::isEEUIApp()) {
$menu[] = [
'key' => 'locat-checkin',
'label' => Doo::translate('定位签到'),
'config' => [
'key' => $setting['locat_bd_lbs_key'],
'lng' => $setting['locat_bd_lbs_point']['lng'],
'lat' => $setting['locat_bd_lbs_point']['lat'],
'radius' => $setting['locat_bd_lbs_point']['radius'],
]
];
}
if (in_array('manual', $setting['modes'])) {
$menu[] = [
'key' => 'manual-checkin',
'label' => Doo::translate('手动签到')
];
}
return $menu;
case 'anon-msg@bot.system':
return [
[
'key' => 'help',
'label' => Doo::translate('使用说明')
], [
'key' => 'privacy',
'label' => Doo::translate('隐私说明')
],
];
case 'meeting-alert@bot.system':
if (!Base::judgeClientVersion('0.39.89')) {
return [];
}
return [
[
'key' => 'meeting-create',
'label' => Doo::translate('新会议')
],
[
'key' => 'meeting-join',
'label' => Doo::translate('加入会议')
],
];
case 'bot-manager@bot.system':
return [
[
'key' => '/help',
'label' => Doo::translate('帮助指令')
], [
'key' => '/api',
'label' => Doo::translate('API接口文档')
], [
'key' => '/list',
'label' => Doo::translate('我的机器人')
],
];
default:
if (preg_match('/^ai-(.*?)@bot.system$/', $email)) {
return [
[
'key' => '%3A.clear',
'label' => Doo::translate('清空上下文')
]
];
}
return [];
}
}
/**
* 签到机器人
* @param $command
* @param $userid
* @param $extra
* @return string
*/
public static function checkinBotQuickMsg($command, $userid)
public static function checkinBotQuickMsg($command, $userid, $extra = [])
{
if (Cache::get("UserBot::checkinBotQuickMsg:{$userid}") === "yes") {
return "操作频繁!";
}
Cache::put("UserBot::checkinBotQuickMsg:{$userid}", "yes", Carbon::now()->addSecond());
//
if ($command === 'checkin') {
if ($command === 'manual-checkin') {
$setting = Base::setting('checkinSetting');
if ($setting['open'] !== 'open') {
return '暂未开启签到功能。';
@@ -160,9 +211,25 @@ class UserBot extends AbstractModel
if (!in_array('manual', $setting['modes'])) {
return '暂未开放手动签到。';
}
if ($error = UserBot::checkinBotCheckin($userid, Base::time(), true)) {
return $error;
UserBot::checkinBotCheckin('manual-' . $userid, Timer::time(), true);
return null;
} elseif ($command === 'locat-checkin') {
$setting = Base::setting('checkinSetting');
if ($setting['open'] !== 'open') {
return '暂未开启签到功能。';
}
if (!in_array('locat', $setting['modes'])) {
return '暂未开放定位签到。';
}
if (empty($extra)) {
return '当前客户端版本低所需版本≥v0.39.75)。';
}
if ($extra['type'] === 'bd') {
// todo 判断距离
} else {
return '错误的定位签到。';
}
UserBot::checkinBotCheckin('locat-' . $userid, Timer::time(), true);
return null;
} else {
return Extranet::checkinBotQuickMsg($command);
@@ -171,10 +238,11 @@ class UserBot extends AbstractModel
/**
* 签到机器人签到
* @param $mac
* @param mixed $mac
* - 多个使用,分隔
* - 支持mac地址、(manual|locat|face|checkin)-userid
* @param $time
* @param bool $alreadyTip 签到过是否提示
* @return string|null 返回string表示错误信息返回null表示签到成功
*/
public static function checkinBotCheckin($mac, $time, $alreadyTip = false)
{
@@ -190,18 +258,20 @@ class UserBot extends AbstractModel
$timeEnd = strtotime("{$nowDate} {$times[1]}");
$timeAdvance = max($timeStart - $advance, strtotime($nowDate));
$timeDelay = min($timeEnd + $delay, strtotime("{$nowDate} 23:59:59"));
if (Base::time() < $timeAdvance || $timeDelay < Base::time()) {
return "不在有效时间内,有效时间为:" . date("H:i", $timeAdvance) . "-" . date("H:i", $timeDelay);
$errorTime = false;
if (Timer::time() < $timeAdvance || $timeDelay < Timer::time()) {
$errorTime = "不在有效时间内,有效时间为:" . date("H:i", $timeAdvance) . "-" . date("H:i", $timeDelay);
}
//
$macs = explode(",", $mac);
$checkins = [];
$array = [];
foreach ($macs as $mac) {
$mac = strtoupper($mac);
$array = [];
if (Base::isMac($mac)) {
// 路由器签到
if ($UserCheckinMac = UserCheckinMac::whereMac($mac)->first()) {
$array = [
$array[] = [
'userid' => $UserCheckinMac->userid,
'mac' => $UserCheckinMac->mac,
'date' => $nowDate,
@@ -211,23 +281,34 @@ class UserBot extends AbstractModel
'remark' => $UserCheckinMac->remark,
];
}
} elseif (Base::isNumber($mac)) {
} elseif (preg_match('/^(manual|locat|face|checkin)-(\d+)$/i', $mac, $match)) {
// 机器签到、手动签到、定位签到
$type = str_replace('checkin', 'face', strtolower($match[1]));
$mac = intval($match[2]);
$remark = match ($type) {
'manual' => $setting['manual_remark'] ?: 'Manual',
'locat' => $setting['locat_remark'] ?: 'Location',
'face' => $setting['face_remark'] ?: 'Machine',
default => '',
};
if ($UserInfo = User::whereUserid($mac)->whereBot(0)->first()) {
$array = [
$array[] = [
'userid' => $UserInfo->userid,
'mac' => '00:00:00:00:00:00',
'date' => $nowDate,
];
$checkins[] = [
'userid' => $UserInfo->userid,
'remark' => '手动签到',
'remark' => $remark,
];
}
}
if ($array) {
$record = UserCheckinRecord::where($array)->first();
}
if (!$errorTime) {
foreach ($array as $item) {
$record = UserCheckinRecord::where($item)->first();
if (empty($record)) {
$record = UserCheckinRecord::createInstance($array);
$record = UserCheckinRecord::createInstance($item);
}
$record->times = Base::array2json(array_merge($record->times, [$nowTime]));
$record->report_time = $time;
@@ -236,68 +317,102 @@ class UserBot extends AbstractModel
}
//
if ($checkins && $botUser = User::botGetOrCreate('check-in')) {
$getJokeSoup = function($type) {
$getJokeSoup = function($type, $userid) {
$pre = $type == "up" ? "每日开心:" : "心灵鸡汤:";
$key = $type == "up" ? "jokes" : "soups";
$array = Base::json2array(Cache::get(JokeSoupTask::keyName($key)));
if ($array) {
$item = $array[array_rand($array)];
if ($item) {
return $pre . $item;
Doo::setLanguage($userid);
return Doo::translate($pre . $item);
}
}
return null;
};
$sendMsg = function($type, $checkin) use ($alreadyTip, $getJokeSoup, $botUser, $nowDate) {
$sendMsg = function($type, $checkin) use ($errorTime, $alreadyTip, $getJokeSoup, $botUser, $nowDate) {
$dialog = WebSocketDialog::checkUserDialog($botUser, $checkin['userid']);
if (!$dialog) {
return;
}
// 判断错误
if ($errorTime) {
if ($alreadyTip) {
$text = $errorTime;
$text .= $checkin['remark'] ? " ({$checkin['remark']})": "";
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'content' => $text,
], $botUser->userid, false, false, true);
}
return;
}
// 判断已打卡
$cacheKey = "Checkin::sendMsg-{$nowDate}-{$type}:" . $checkin['userid'];
$typeDesc = $type == "up" ? "上班" : "下班";
$typeContent = $type == "up" ? "上班" : "下班";
if (Cache::get($cacheKey) === "yes") {
if ($alreadyTip && $dialog = WebSocketDialog::checkUserDialog($botUser, $checkin['userid'])) {
$text = "<p>今日已{$typeDesc}打卡,无需重复打卡。</p>";
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, $type != "up");
if ($alreadyTip) {
$text = "今日已{$typeContent}打卡,无需重复打卡。";
$text .= $checkin['remark'] ? " ({$checkin['remark']})": "";
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'content' => $text,
], $botUser->userid, false, false, true);
}
return;
}
Cache::put($cacheKey, "yes", Carbon::now()->addDay());
//
if ($dialog = WebSocketDialog::checkUserDialog($botUser, $checkin['userid'])) {
$hi = date("H:i");
$remark = $checkin['remark'] ? " ({$checkin['remark']})": "";
$text = "<p>{$typeDesc}打卡成功,打卡时间: {$hi}{$remark}</p>";
$suff = $getJokeSoup($type);
if ($suff) {
$text = "{$text}<p>----------</p><p>{$suff}</p>";
}
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, $type != "up");
}
// 打卡成功
$hi = date("H:i");
$remark = $checkin['remark'] ? " ({$checkin['remark']})": "";
$subcontent = $getJokeSoup($type, $checkin['userid']);
$title = "{$typeContent}打卡成功,打卡时间: {$hi}{$remark}";
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'title' => $title,
'content' => [
[
'content' => $title
], [
'content' => $subcontent,
'language' => false,
'style' => 'padding-top:4px;opacity:0.6',
]
],
], $botUser->userid, false, false, $type != "up");
};
if ($timeAdvance <= Base::time() && Base::time() < $timeEnd) {
if ($timeAdvance <= Timer::time() && Timer::time() < $timeEnd) {
// 上班打卡通知(从最早打卡时间 到 下班打卡时间)
foreach ($checkins as $checkin) {
$sendMsg('up', $checkin);
}
}
if ($timeEnd <= Base::time() && Base::time() <= $timeDelay) {
if ($timeEnd <= Timer::time() && Timer::time() <= $timeDelay) {
// 下班打卡通知(下班打卡时间 到 最晚打卡时间)
foreach ($checkins as $checkin) {
$sendMsg('down', $checkin);
}
}
}
return null;
}
/**
* 隐私机器人
* @param $command
* @return string
* @return array
*/
public static function anonBotQuickMsg($command)
{
return match ($command) {
"help" => "使用说明:打开你想要发匿名消息的个人对话,点击输入框右边的 ⊕ 号,选择 <u>匿名消息</u> 即可输入你想要发送的匿名消息内容。",
"privacy" => "匿名消息将通过 <u>匿名消息(机器人)</u> 发送给对方,不会记录你的身份信息。",
default => '',
"help" => [
"title" => "匿名消息使用说明",
"content" => "使用说明:打开你想要发匿名消息的个人对话,点击输入框右边的 ⊕ 号,选择「匿名消息」即可输入你想要发送的匿名消息内容。"
],
"privacy" => [
"title" => "匿名消息隐私说明",
"content" => "匿名消息将通过「匿名消息(机器人)」发送给对方,不会记录你的身份信息。"
],
default => [],
};
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use App\Module\Ihttp;
/**
* App\Models\UserCheckinFace
*
* @property int $id
* @property int|null $userid 会员id
* @property string|null $faceimg 人脸图片
* @property int|null $status 状态
* @property string|null $remark 备注
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinFace newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinFace newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinFace query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinFace whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinFace whereFaceimg($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinFace whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinFace whereRemark($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinFace whereStatus($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinFace whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinFace whereUserid($value)
* @mixin \Eloquent
*/
class UserCheckinFace extends AbstractModel
{
public static function saveFace($userid, $nickname, $faceimg, $remark='')
{
// 取上传图片的URL
$faceimg = Base::unFillUrl($faceimg);
$record = "";
if ($faceimg != '') {
$faceFile = public_path($faceimg);
$record = base64_encode(file_get_contents($faceFile));
}
$url = 'http://' . env('APP_IPPR') . '.14' . ":7788/user";
$data = [
'name' => $nickname,
'enrollid' => $userid,
'admin' => 0,
'backupnum' => 50,
];
if ($record != '') {
$data['record'] = $record;
}
$res = Ihttp::ihttp_post($url, json_encode($data), 15);
if($res['data'] && $data = json_decode($res['data'])){
if($data->ret != 1 && $data->msg){
throw new ApiException($data->msg);
}
}
return AbstractModel::transaction(function() use ($userid, $faceimg, $remark) {
$checkinFace = self::query()->whereUserid($userid)->first();
if ($checkinFace) {
self::updateData(['id' => $checkinFace->id], [
'faceimg' => $faceimg,
'status' => 1,
'remark' => $remark
]);
} else {
$checkinFace = new UserCheckinFace();
$checkinFace->faceimg = $faceimg;
$checkinFace->userid = $userid;
$checkinFace->remark = $remark;
$checkinFace->save();
}
if ($faceimg == '') {
$res = UserCheckinFace::deleteDeviceUser($userid);
if ($res) {
return $res;
}
}
return Base::retSuccess('设置成功');
});
}
public static function deleteDeviceUser($userid) {
$url = 'http://' . env('APP_IPPR') . '.14' . ":7788/user/delete";
$data = [
'enrollid' => $userid,
'backupnum' => 50, // 13 删除整个用户 50 删除图片
];
$res = Ihttp::ihttp_post($url, json_encode($data));
if($res['data'] && $data = json_decode($res['data'])){
if($data->ret != 1 && $data->msg){
throw new ApiException($data->msg);
// return Base::retError($data->msg);
}
}
}
}

View File

@@ -48,7 +48,7 @@ class UserDelete extends AbstractModel
$value = Base::json2array($value);
// 昵称
if (!$value['nickname']) {
$value['nickname'] = Base::cardFormat($value['email']);
$value['nickname'] = Base::formatName($value['email']);
}
// 头像
$value['userimg'] = User::getAvatar($value['userid'], $value['userimg'], $value['email'], $value['nickname']);
@@ -71,7 +71,7 @@ class UserDelete extends AbstractModel
}
$cache = $row->cache;
$cache = array_intersect_key($cache, array_flip(array_merge(User::$basicField, ['department_name'])));
$cache['delete_at'] = $row->created_at->format($row->dateFormat ?: 'Y-m-d H:i:s');
$cache['delete_at'] = $row->created_at->toDateTimeString();
return $cache;
}
}

View File

@@ -4,6 +4,8 @@ namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use App\Module\Doo;
use App\Module\Timer;
use Carbon\Carbon;
use Guanguans\Notify\Factory;
use Guanguans\Notify\Messages\EmailMessage;
@@ -64,36 +66,48 @@ class UserEmailVerification extends AbstractModel
]);
$row->save();
$setting = Base::setting('emailSetting');
$alias = Base::settingFind('system', 'system_alias', 'Task');
try {
if (!Base::isEmail($email)) {
throw new \Exception("User email '{$email}' address error");
}
switch ($type) {
case 2:
$subject = env('APP_NAME') . "修改邮箱验证";
$content = "<p>{$user->nickname} 您好,您正在修改 " . env('APP_NAME') . " 的邮箱验证码如下。请在30分钟内输入验证码</p><p style='color: #0000DD;'><u>$code</u></p><p>如果不是本人操作,您的帐号可能存在风险,请及时修改密码!</p>";
$subject = Doo::translate($alias . "修改邮箱验证");
$content = sprintf("<p>%s</p><p style='color: #0000DD;'><u>%s</u></p><p>%s</p>",
Doo::translate($user->nickname . " 您好,您正在修改 " . $alias . " 的邮箱验证码如下。请在30分钟内输入验证码"),
$code,
Doo::translate("如果不是本人操作,您的帐号可能存在风险,请及时修改密码!")
);
break;
case 3:
$subject = env('APP_NAME') . "注销帐号验证";
$content = "<p>{$user->nickname} 您好,您正在注销 " . env('APP_NAME') . " 的帐号验证码如下。请在30分钟内输入验证码</p><p style='color: #0000DD;'><u>$code</u></p><p>如果不是本人操作,您的帐号可能存在风险,请及时修改密码!</p>";
$subject = Doo::translate($alias . "注销帐号验证");
$content = sprintf("<p>%s</p><p style='color: #0000DD;'><u>%s</u></p><p>%s</p>",
Doo::translate($user->nickname . " 您好,您正在注销 " . $alias . " 的帐号验证码如下。请在30分钟内输入验证码"),
$code,
Doo::translate("如果不是本人操作,您的帐号可能存在风险,请及时修改密码!")
);
break;
default:
$url = Base::fillUrl('single/valid/email') . '?code=' . $row->code;
$subject = env('APP_NAME') . "绑定邮箱验证";
$content = "<p>{$user->nickname} 您好,您正在绑定 " . env('APP_NAME') . " 的邮箱请于30分钟之内点击以下链接完成验证 :</p><p style='display: flex; justify-content: center;'><a href='{$url}' target='_blank'>{$url}</a></p>";
$subject = Doo::translate($alias . "绑定邮箱验证");
$content = sprintf("<p>%s</p><p style='display: flex; justify-content: center;'>%s</p>",
Doo::translate($user->nickname . " 您好,您正在绑定 " . $alias . " 的邮箱请于30分钟之内点击以下链接完成验证:"),
"<a href='{$url}' target='_blank'>{$url}</a>"
);
break;
}
Factory::mailer()
->setDsn("smtp://{$setting['account']}:{$setting['password']}@{$setting['smtp_server']}:{$setting['port']}?verify_peer=0")
->setMessage(EmailMessage::create()
->from(env('APP_NAME', 'Task') . " <{$setting['account']}>")
->from($alias . " <{$setting['account']}>")
->to($email)
->subject($subject)
->html($content))
->send();
} catch (\Throwable $e) {
if (str_contains($e->getMessage(), "Timed Out")) {
throw new ApiException("language.TimedOut");
throw new ApiException("邮件发送超时,请检查邮箱配置是否正确");
} elseif ($e->getCode() === 550) {
throw new ApiException('邮件内容被拒绝,请检查邮箱是否开启接收功能');
} else {
@@ -122,7 +136,7 @@ class UserEmailVerification extends AbstractModel
}
$oldTime = Carbon::parse($emailVerify->created_at)->timestamp;
$time = Base::Time();
$time = Timer::Time();
// 30分钟失效
if (abs($time - $oldTime) > 1800) {

View File

@@ -50,28 +50,58 @@ class UserTransfer extends AbstractModel
// 移交文件
File::transfer($this->original_userid, $this->new_userid);
// 离职移出群组
WebSocketDialog::select(['web_socket_dialogs.*'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->where('web_socket_dialogs.type', 'group')
->where('u.userid', $this->original_userid)
->orderByDesc('web_socket_dialogs.id')
->chunk(100, function($list) {
/** @var WebSocketDialog $dialog */
foreach ($list as $dialog) {
// 离职员工退出群
$dialog->exitGroup($this->original_userid, 'remove', false, false);
if ($dialog->owner_id === $this->original_userid) {
// 如果是群主则把交接人设为群主
$dialog->owner_id = $this->new_userid;
if ($dialog->save()) {
$dialog->joinGroup($this->new_userid, 0);
$dialog->pushMsg("groupUpdate", [
'id' => $dialog->id,
'owner_id' => $dialog->owner_id,
]);
}
$this->exitDialog();
}
/**
* 退出群组
* @return void
*/
public function exitDialog()
{
$lastId = 0;
$limit = 100;
while (true) {
$query = WebSocketDialog::select(['web_socket_dialogs.*'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->where('web_socket_dialogs.type', 'group')
->where('web_socket_dialogs.group_type', '!=', 'okr')
->where('u.userid', $this->original_userid)
->orderBy('web_socket_dialogs.id')
->limit($limit);
if ($lastId) {
$query->where('web_socket_dialogs.id', '>', $lastId);
}
$list = $query->get();
// 没有数据了就退出
if ($list->isEmpty()) {
break;
}
// 记录最后一条记录的ID
$lastId = $list->last()->id;
// 离职员工退出群
foreach ($list as $dialog) {
$dialog->exitGroup($this->original_userid, 'remove', false, false);
if ($dialog->owner_id === $this->original_userid) {
// 如果是群主则把交接人设为群主
$dialog->owner_id = $this->new_userid;
if ($dialog->save()) {
$dialog->joinGroup($this->new_userid, 0);
$dialog->pushMsg("groupUpdate", [
'id' => $dialog->id,
'owner_id' => $dialog->owner_id,
]);
}
}
});
}
// 如果返回的数据少于限制数,说明已经是最后一批
if ($list->count() < $limit) {
break;
}
}
}
}

View File

@@ -9,6 +9,7 @@ use App\Tasks\PushTask;
use Cache;
use Carbon\Carbon;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\DB;
@@ -77,6 +78,23 @@ class WebSocketDialog extends AbstractModel
return $this->hasMany(WebSocketDialogUser::class, 'dialog_id', 'id');
}
/**
* 获取对话成员(连表查)
* @param $addField
* @return User|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Query\Builder
*/
public function dialogUserBuilder($addField = [])
{
$columns = array_map(function ($column) {
return "users." . $column;
}, array_merge(User::$basicField, $addField));
$columns[] = "du.*";
return User::select($columns)
->join('web_socket_dialog_users as du', 'users.userid', '=', 'du.userid')
->where('du.dialog_id', $this->id)
->whereNull('users.disable_at');
}
/**
* 获取对话列表
@@ -87,9 +105,11 @@ class WebSocketDialog extends AbstractModel
*/
public static function getDialogList($userid, $updated = "", $deleted = "")
{
$builder = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->where('u.userid', $userid);
$builder = DB::table('web_socket_dialog_users as u')
->select(['d.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
->join('web_socket_dialogs as d', 'u.dialog_id', '=', 'd.id')
->where('u.userid', $userid)
->whereNull('d.deleted_at');
if ($updated) {
$builder->where('u.updated_at', '>', $updated);
}
@@ -97,8 +117,8 @@ class WebSocketDialog extends AbstractModel
->orderByDesc('u.top_at')
->orderByDesc('u.last_at')
->paginate(Base::getPaginate(100, 50));
$list->transform(function (WebSocketDialog $item) use ($userid) {
return $item->formatData($userid);
$list->transform(function ($item) use ($userid) {
return self::synthesizeData($item, $userid);
});
//
$data = $list->toArray();
@@ -122,241 +142,259 @@ class WebSocketDialog extends AbstractModel
$array = [];
if ($unreadAt) {
// 未读对话
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->join('web_socket_dialog_msg_reads as r', 'web_socket_dialogs.id', '=', 'r.dialog_id')
$list = DB::table('web_socket_dialog_users as u')
->select(['d.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
->join('web_socket_dialogs as d', 'u.dialog_id', '=', 'd.id')
->join('web_socket_dialog_msg_reads as r', 'd.id', '=', 'r.dialog_id')
->where('u.userid', $userid)
->where('u.last_at', '<', $unreadAt)
->whereNull('d.deleted_at')
->where('r.userid', $userid)
->where('r.read_at')
->where('u.last_at', '<', $unreadAt)
->groupBy('u.dialog_id')
->take(20)
->get();
$list->transform(function (WebSocketDialog $item) use ($userid, &$ids, &$array) {
$list->transform(function ($item) use ($userid, &$ids, &$array) {
if (!in_array($item->id, $ids)) {
$ids[] = $item->id;
$array[] = $item->formatData($userid);
$array[] = self::synthesizeData($item, $userid);
}
});
// 标记未读会话
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
$list = DB::table('web_socket_dialog_users as u')
->select(['d.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
->join('web_socket_dialogs as d', 'u.dialog_id', '=', 'd.id')
->where('u.userid', $userid)
->where('u.mark_unread', 1)
->where('u.last_at', '<', $unreadAt)
->whereNull('d.deleted_at')
->take(20)
->get();
$list->transform(function (WebSocketDialog $item) use ($userid, &$ids, &$array) {
$list->transform(function ($item) use ($userid, &$ids, &$array) {
if (!in_array($item->id, $ids)) {
$ids[] = $item->id;
$array[] = $item->formatData($userid);
$array[] = self::synthesizeData($item, $userid);
}
});
}
if ($todoAt) {
// 待办会话
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->join('web_socket_dialog_msg_todos as t', 'web_socket_dialogs.id', '=', 't.dialog_id')
$list = DB::table('web_socket_dialog_users as u')
->select(['d.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
->join('web_socket_dialogs as d', 'u.dialog_id', '=', 'd.id')
->join('web_socket_dialog_msg_todos as t', 'd.id', '=', 't.dialog_id')
->where('u.userid', $userid)
->where('u.last_at', '<', $todoAt)
->whereNull('d.deleted_at')
->where('t.userid', $userid)
->where('t.done_at')
->where('u.last_at', '<', $todoAt)
->groupBy('u.dialog_id')
->take(20)
->get();
$list->transform(function (WebSocketDialog $item) use ($userid, &$ids, &$array) {
$list->transform(function ($item) use ($userid, &$ids, &$array) {
if (!in_array($item->id, $ids)) {
$ids[] = $item->id;
$array[] = $item->formatData($userid);
$array[] = self::synthesizeData($item, $userid);
}
});
}
return $array;
}
/**
* 格式化对话
* 综合数据
* @param $data
* @param int $userid 会员ID
* @param bool $hasData
* @return $this
* @param bool $hasData 已存在的消息类型
* @return array
*/
public function formatData($userid, $hasData = false)
public static function synthesizeData($data, $userid, $hasData = false)
{
$dialogUserFun = function ($key, $default = null) use ($userid) {
$data = Cache::remember("Dialog::formatData-{$this->id}-{$userid}", now()->addSeconds(10), function () use ($userid) {
return WebSocketDialogUser::whereDialogId($this->id)->whereUserid($userid)->first()?->toArray();
});
return $data[$key] ?? $default;
};
//
$time = Carbon::parse($this->user_at ?? $dialogUserFun('updated_at'));
$this->hide = $this->hide ?? $dialogUserFun('hide');
$this->top_at = $this->top_at ?? $dialogUserFun('top_at');
$this->last_at = $this->last_at ?? $dialogUserFun('last_at');
$this->user_at = $time->toDateTimeString('millisecond');
$this->user_ms = $time->valueOf();
//
if (isset($this->search_msg_id)) {
// 最后消息 (搜索预览消息)
$this->last_msg = WebSocketDialogMsg::whereDialogId($this->id)->find($this->search_msg_id);
$this->last_at = $this->last_msg ? Carbon::parse($this->last_msg->created_at)->format('Y-m-d H:i:s') : null;
} else {
// 未读信息
if (Base::judgeClientVersion("0.34.0")) {
$this->generateUnread($userid);
} else {
$this->generateUnread_03398($userid, $hasData);
}
// 未读标记
$this->mark_unread = $this->mark_unread ?? $dialogUserFun('mark_unread');
// 是否免打扰
$this->silence = $this->silence ?? $dialogUserFun('silence');
// 对话人数
$this->people = WebSocketDialogUser::whereDialogId($this->id)->count();
// 有待办
$this->todo_num = WebSocketDialogMsgTodo::whereDialogId($this->id)->whereUserid($userid)->whereDoneAt(null)->count();
// 最后消息
$this->last_msg = WebSocketDialogMsg::whereDialogId($this->id)->orderByDesc('id')->first();
// 判断数据
if (is_numeric($data)) {
$data = WebSocketDialog::find($data)?->toArray();
} elseif ($data instanceof Model) {
$data = $data->toArray();
} elseif (is_object($data)) {
$data = (array)$data;
}
if (!is_array($data) || !isset($data['id'])) {
return $data;
}
// 会话必要字段
$fields = [
'id', 'type', 'group_type', 'name', 'avatar', 'owner_id', 'link_id', 'top_userid', 'top_msg_id', 'created_at', 'updated_at', 'deleted_at',
];
if (!empty(array_diff($fields, array_keys($data)))) {
// 补全数据
foreach ($fields as $field) {
$data[$field] = $data[$field] ?? null;
}
}
$data['avatar'] = Base::fillUrl($data['avatar']);
// 会员必要字段
$fields = [
'top_at', 'last_at', 'mark_unread', 'silence', 'hide', 'color', 'user_at',
];
if (!empty(array_diff($fields, array_keys($data)))) {
// 补全数据(查询数据库)
$array = WebSocketDialogUser::whereDialogId($data['id'])->whereUserid($userid)->first()?->toArray();
foreach ($fields as $field) {
if ($field === 'user_at') {
$data[$field] = $data[$field] ?? $array['updated_at'] ?? null;
} else {
$data[$field] = $data[$field] ?? $array[$field] ?? null;
}
}
}
// 会员数据处理
if (isset($data['user_at']) && !isset($data['user_ms'])) {
$time = Carbon::parse($data['user_at']);
$data['user_at'] = $time->toDateTimeString('millisecond');
$data['user_ms'] = $time->valueOf();
}
// 信息数据
if (isset($data['search_msg_id'])) {
// 最后消息 (搜索预览消息)
$data['last_msg'] = $data['last_msg'] ?? WebSocketDialogMsg::whereDialogId($data['id'])->find($data['search_msg_id'])?->toArray();
$data['last_at'] = $data['last_msg'] ? Carbon::parse($data['last_msg']['created_at'])->toDateTimeString() : null;
} else {
// 未读消息
$data = array_merge($data, self::generateUnread($data['id'], $userid));
// 对话人数
$data['people'] = $data['people'] ?? WebSocketDialogUser::whereDialogId($data['id'])->count();
// 有待办
$data['todo_num'] = $data['todo_num'] ?? WebSocketDialogMsgTodo::whereDialogId($data['id'])->whereUserid($userid)->whereDoneAt(null)->count();
// 最后消息
$data['last_msg'] = $data['last_msg'] ?? WebSocketDialogMsg::whereDialogId($data['id'])->orderByDesc('id')->first()?->toArray();
}
$data['last_msg'] = self::lastMsgFormat($data['last_msg']);
// 对方信息
$this->pinyin = Base::cn2pinyin($this->name);
$this->quick_msgs = [];
$this->dialog_user = null;
$this->group_info = null;
$this->bot = 0;
switch ($this->type) {
$data['pinyin'] = Base::cn2pinyin($data['name']);
$data['quick_msgs'] = [];
$data['dialog_user'] = null;
$data['group_info'] = null;
$data['bot'] = 0;
switch ($data['type']) {
case "user":
$dialog_user = WebSocketDialogUser::whereDialogId($this->id)->where('userid', '!=', $userid)->first();
$dialog_user = WebSocketDialogUser::whereDialogId($data['id'])->where('userid', '!=', $userid)->first();
if ($dialog_user->userid === 0) {
$dialog_user->userid = $userid;
}
$basic = User::userid2basic($dialog_user->userid);
if ($basic) {
$this->name = $basic->nickname;
$this->email = $basic->email;
$this->userimg = $basic->userimg;
$this->bot = $basic->getBotOwner();
$this->quick_msgs = UserBot::quickMsgs($basic->email);
$data['name'] = $basic->nickname;
$data['email'] = $basic->email;
$data['userimg'] = $basic->userimg;
$data['bot'] = $basic->getBotOwner();
$data['quick_msgs'] = UserBot::quickMsgs($basic->email);
} else {
$this->name = 'non-existent';
$this->dialog_delete = 1;
$data['name'] = 'non-existent';
$data['dialog_delete'] = 1;
}
$this->dialog_user = $dialog_user;
$this->dialog_mute = Base::settingFind('system', 'user_private_chat_mute');
$data['dialog_user'] = $dialog_user;
$data['dialog_mute'] = Base::settingFind('system', 'user_private_chat_mute');
break;
case "group":
switch ($this->group_type) {
switch ($data['group_type']) {
case 'user':
$this->dialog_mute = Base::settingFind('system', 'user_group_chat_mute');
$data['dialog_mute'] = Base::settingFind('system', 'user_group_chat_mute');
break;
case 'project':
$this->group_info = Project::withTrashed()->select(['id', 'name', 'archived_at', 'deleted_at'])->whereDialogId($this->id)->first()?->cancelAppend()->cancelHidden();
if ($this->group_info) {
$this->name = $this->group_info->name;
$data['group_info'] = Project::withTrashed()->select(['id', 'name', 'archived_at', 'deleted_at'])->whereDialogId($data['id'])->first()?->cancelAppend()->cancelHidden()->toArray();
if ($data['group_info']) {
$data['name'] = $data['group_info']['name'];
} else {
$this->name = '[Delete]';
$this->dialog_delete = 1;
$data['name'] = '[Delete]';
$data['dialog_delete'] = 1;
}
break;
case 'task':
$this->group_info = ProjectTask::withTrashed()->select(['id', 'name', 'complete_at', 'archived_at', 'deleted_at'])->whereDialogId($this->id)->first()?->cancelAppend()->cancelHidden();
if ($this->group_info) {
$this->name = $this->group_info->name;
$data['group_info'] = ProjectTask::withTrashed()->select(['id', 'name', 'complete_at', 'archived_at', 'deleted_at'])->whereDialogId($data['id'])->first()?->cancelAppend()->cancelHidden()->toArray();
if ($data['group_info']) {
$data['name'] = $data['group_info']['name'];
} else {
$this->name = '[Delete]';
$this->dialog_delete = 1;
$data['name'] = '[Delete]';
$data['dialog_delete'] = 1;
}
break;
case 'all':
$this->name = Doo::translate('全体成员');
$this->dialog_mute = Base::settingFind('system', 'all_group_mute');
$data['name'] = Doo::translate('全体成员');
$data['dialog_mute'] = Base::settingFind('system', 'all_group_mute');
break;
}
break;
}
// 已存在的消息类型
if ($hasData === true) {
$msgBuilder = WebSocketDialogMsg::whereDialogId($this->id);
$this->has_tag = $msgBuilder->clone()->where('tag', '>', 0)->exists();
$this->has_todo = $msgBuilder->clone()->where('todo', '>', 0)->exists();
$this->has_image = $msgBuilder->clone()->whereMtype('image')->exists();
$this->has_file = $msgBuilder->clone()->whereMtype('file')->exists();
$this->has_link = $msgBuilder->clone()->whereLink(1)->exists();
Cache::forever("Dialog::tag:" . $this->id, Base::array2json([
'has_tag' => $this->has_tag,
'has_todo' => $this->has_todo,
'has_image' => $this->has_image,
'has_file' => $this->has_file,
'has_link' => $this->has_link,
$msgBuilder = WebSocketDialogMsg::whereDialogId($data['id']);
$data['has_tag'] = $msgBuilder->clone()->where('tag', '>', 0)->exists();
$data['has_todo'] = $msgBuilder->clone()->where('todo', '>', 0)->exists();
$data['has_image'] = $msgBuilder->clone()->whereMtype('image')->exists();
$data['has_file'] = $msgBuilder->clone()->whereMtype('file')->exists();
$data['has_link'] = $msgBuilder->clone()->whereLink(1)->exists();
Cache::forever("Dialog::tag:" . $data['id'], Base::array2json([
'has_tag' => $data['has_tag'],
'has_todo' => $data['has_todo'],
'has_image' => $data['has_image'],
'has_file' => $data['has_file'],
'has_link' => $data['has_link'],
]));
} else {
$tagData = Base::json2array(Cache::get("Dialog::tag:" . $this->id));
$tagData = Base::json2array(Cache::get("Dialog::tag:" . $data['id']));
if ($tagData) {
$this->has_tag = !!$tagData['has_tag'];
$this->has_todo = !!$tagData['has_todo'];
$this->has_image = !!$tagData['has_image'];
$this->has_file = !!$tagData['has_file'];
$this->has_link = !!$tagData['has_link'];
$data['has_tag'] = !!$tagData['has_tag'];
$data['has_todo'] = !!$tagData['has_todo'];
$data['has_image'] = !!$tagData['has_image'];
$data['has_file'] = !!$tagData['has_file'];
$data['has_link'] = !!$tagData['has_link'];
}
}
return $this;
return $data;
}
/**
* 生成未读数据
* @param $userid
* @return $this
* 格式化最后消息
* @param array $lastMsg
* @return array
*/
public function generateUnread($userid)
public static function lastMsgFormat($lastMsg)
{
$builder = WebSocketDialogMsgRead::whereDialogId($this->id)->whereUserid($userid)->whereReadAt(null);
if ($lastMsg && $lastMsg['type'] != 'preview') {
$msgData = $lastMsg;
$msgData['emoji'] = Base::array_only_recursive($msgData['emoji'], ['symbol']);
$msgData['msg'] = ['preview' => WebSocketDialogMsg::previewMsg($msgData)];
$msgData['type'] = 'preview';
$lastMsg = array_intersect_key($msgData, array_flip(['id', 'type', 'msg', 'userid', 'percentage', 'emoji', 'created_at']));
}
return $lastMsg;
}
/**
* 获取未读数据
* @param $dialogId
* @param $userid
* @return array
*/
public static function generateUnread($dialogId, $userid)
{
$data = [];
// 未读消息
$this->unread = $builder->count();
$builder = WebSocketDialogMsgRead::whereDialogId($dialogId)->whereUserid($userid)->whereReadAt(null);
// 总未读消息
$data['unread'] = $builder->clone()->count();
// 最早一条未读消息
$this->unread_one = $this->unread > 0 ? intval($builder->clone()->orderBy('msg_id')->value('msg_id')) : 0;
$data['unread_one'] = $data['unread'] > 0 ? intval($builder->clone()->orderBy('msg_id')->value('msg_id')) : 0;
// @我的消息
$this->mention = $this->unread > 0 ? $builder->clone()->whereMention(1)->count() : 0;
$data['mention'] = $data['unread'] > 0 ? $builder->clone()->whereMention(1)->count() : 0;
// @我的消息id集合
$this->mention_ids = $this->mention > 0 ? $builder->clone()->whereMention(1)->orderByDesc('msg_id')->take(20)->pluck('msg_id')->toArray() : [];
return $this;
}
/**
* 生成未读数据 // todo: 旧版兼容,后续删除
* @param $userid
* @param $positionData
* @return $this
*/
public function generateUnread_03398($userid, $positionData = false)
{
$builder = WebSocketDialogMsgRead::whereDialogId($this->id)->whereUserid($userid)->whereReadAt(null);
$this->unread = $builder->count();
$this->mention = $this->unread > 0 ? $builder->clone()->whereMention(1)->count() : 0;
if ($positionData) {
$array = [];
// @我的消息
if ($this->mention > 0) {
$list = $builder->clone()->whereMention(1)->orderByDesc('msg_id')->take(20)->get();
foreach ($list as $item) {
$array[] = [
'msg_id' => $item->msg_id,
'label' => Doo::translate('@我的消息'),
];
}
}
// 最早一条未读消息
if ($this->unread > 0
&& $first_id = intval($builder->clone()->orderBy('msg_id')->value('msg_id'))) {
$array[] = [
'msg_id' => $first_id,
'label' => '{UNREAD}'
];
}
//
$this->position_msgs = $array;
}
return $this;
$data['mention_ids'] = $data['mention'] > 0 ? $builder->clone()->whereMention(1)->orderByDesc('msg_id')->take(20)->pluck('msg_id')->toArray() : [];
return $data;
}
/**
@@ -674,27 +712,22 @@ class WebSocketDialog extends AbstractModel
* 获取会员对话(没有自动创建)
* @param User $user 发起会话的会员
* @param int $receiver 另一个会员ID
* @return self|null
* @return WebSocketDialog|null
*/
public static function checkUserDialog($user, $receiver)
{
if ($user->userid == $receiver) {
$receiver = 0;
}
$dialogUser = self::select(['web_socket_dialogs.*'])
->join('web_socket_dialog_users as u1', 'web_socket_dialogs.id', '=', 'u1.dialog_id')
->join('web_socket_dialog_users as u2', 'web_socket_dialogs.id', '=', 'u2.dialog_id')
->where('u1.userid', $user->userid)
->where('u2.userid', $receiver)
->where('web_socket_dialogs.type', 'user')
->first();
$dialogUser = self::getUserDialog($user->userid, $receiver, 0, $cacheKey);
if ($dialogUser) {
return $dialogUser;
}
if ($receiver > 0 && $user->isTemp() && !User::whereUserid($receiver)->whereBot(1)->exists() ) {
throw new ApiException('无法发起会话,请联系管理员。');
}
return AbstractModel::transaction(function () use ($receiver, $user) {
return AbstractModel::transaction(function () use ($cacheKey, $receiver, $user) {
Cache::forget($cacheKey);
$dialog = self::createInstance([
'type' => 'user',
]);
@@ -711,12 +744,47 @@ class WebSocketDialog extends AbstractModel
});
}
/**
* 获取用户对话(支持缓存)
* @param $userid1
* @param $userid2
* @param $ttl
* @param null $cacheKey
* @return \Illuminate\Database\Eloquent\Builder|WebSocketDialog|null
*/
public static function getUserDialog($userid1, $userid2, $ttl, &$cacheKey = null)
{
$userids = [$userid1, $userid2];
sort($userids);
$cacheKey = "Dialog::user:" . implode('-', $userids);
if (empty($ttl)) {
return WebSocketDialog::query()
->whereType('user')
->whereExists(function ($query) use ($userids) {
$query->select(DB::raw(1))
->from('web_socket_dialog_users')
->whereColumn('web_socket_dialog_users.dialog_id', 'web_socket_dialogs.id')
->where('web_socket_dialog_users.userid', $userids[0]);
})
->whereExists(function ($query) use ($userids) {
$query->select(DB::raw(1))
->from('web_socket_dialog_users')
->whereColumn('web_socket_dialog_users.dialog_id', 'web_socket_dialogs.id')
->where('web_socket_dialog_users.userid', $userids[1]);
})
->first();
}
return Cache::remember($cacheKey, $ttl, function() use ($userids) {
return self::getUserDialog($userids[0], $userids[1], 0);
});
}
/**
* 发送消息文件
*
* @param User $user 发起会话的会员
* @param array $dialogIds 对话id
* @param file $files 文件对象
* @param file|mixed $files 文件对象
* @param string $image64 base64文件
* @param string $fileName 文件名称
* @param int $replyId 恢复id
@@ -727,6 +795,7 @@ class WebSocketDialog extends AbstractModel
{
$filePath = '';
$result = [];
$data = [];
foreach ($dialogIds as $dialog_id) {
$dialog = WebSocketDialog::checkDialog($dialog_id);
//
@@ -737,18 +806,19 @@ class WebSocketDialog extends AbstractModel
"image64" => $image64,
"path" => $path,
"fileName" => $fileName,
"quality" => 85
]);
} else if ($filePath) {
Base::makeDir(public_path($path));
copy($filePath, public_path($path) . basename($filePath));
} else {
$setting = Base::setting('system');
$data = Base::upload([
"file" => $files,
"type" => 'more',
"path" => $path,
"fileName" => $fileName,
"size" => ($setting['file_upload_limit'] ?: 0) * 1024
"quality" => 100,
"convertVideo" => true
]);
}
//

View File

@@ -149,19 +149,33 @@ class WebSocketDialogMsg extends AbstractModel
if (!is_array($msg)) {
$msg = Base::json2array($msg);
}
if ($type === 'file') {
$msg['type'] = in_array($msg['ext'], ['jpg', 'jpeg', 'webp', 'png', 'gif']) ? 'img' : 'file';
$msg['path'] = Base::fillUrl($msg['path']);
$msg['thumb'] = Base::fillUrl($msg['thumb'] ?: Base::extIcon($msg['ext']));
} else if ($type === 'record') {
$msg['path'] = Base::fillUrl($msg['path']);
$textUserid = is_array($msg['text_userid']) ? $msg['text_userid'] : [];
if (isset($msg['text_userid'])) {
unset($msg['text_userid']);
}
if ($msg['text'] && !in_array(Doo::userId(), $textUserid)) {
$msg['text'] = "";
}
switch ($type) {
case 'file':
$msg['type'] = in_array($msg['ext'], ['jpg', 'jpeg', 'webp', 'png', 'gif']) ? 'img' : 'file';
$msg['path'] = Base::fillUrl($msg['path']);
$msg['thumb'] = Base::fillUrl($msg['thumb'] ?: Base::extIcon($msg['ext']));
break;
case 'record':
$msg['path'] = Base::fillUrl($msg['path']);
$textUserid = is_array($msg['text_userid']) ? $msg['text_userid'] : [];
if (isset($msg['text_userid'])) {
unset($msg['text_userid']);
}
if ($msg['text'] && !in_array(Doo::userId(), $textUserid)) {
$msg['text'] = "";
}
break;
case 'location':
$msg['thumb'] = Base::fillUrl($msg['thumb'] ?: "images/other/location.jpg");
break;
case 'template':
if ($msg['data']['thumb']) {
$msg['data']['thumb']['url'] = Base::fillUrl($msg['data']['thumb']['url']);
}
break;
}
return $msg;
}
@@ -540,97 +554,195 @@ class WebSocketDialogMsg extends AbstractModel
/**
* 预览消息
* @param bool $preserveHtml 保留html格式
* @param null|array $data
* @param WebSocketDialogMsg|array $data 消息数据
* @param bool $preserveHtml 保留html格式
* @return string
*/
public function previewMsg($preserveHtml = false, $data = null)
public static function previewMsg($data, $preserveHtml = false)
{
if ($data === null) {
if ($data instanceof WebSocketDialogMsg) {
$data = [
'type' => $this->type,
'msg' => $this->msg,
'type' => $data->type,
'msg' => $data->msg,
];
}
if (!is_array($data)) {
return '';
}
switch ($data['type']) {
case 'text':
case 'word-chain':
return self::previewTextMsg($data['msg'], $preserveHtml);
case 'vote':
return $this->previewTextMsg($data['msg']['text'], $preserveHtml);
$action = Doo::translate("投票");
return "[{$action}] " . self::previewTextMsg($data['msg'], $preserveHtml);
case 'word-chain':
$action = Doo::translate("接龙");
return "[{$action}] " . self::previewTextMsg($data['msg'], $preserveHtml);
case 'record':
return "[语音]";
$action = Doo::translate("语音");
return "[{$action}]";
case 'location':
$action = Doo::translate("位置");
return "[{$action}] " . Base::cutStr($data['msg']['title'], 50);
case 'meeting':
return "[会议] ${$data['msg']['name']}";
$action = Doo::translate("会议");
return "[{$action}] " . Base::cutStr($data['msg']['name'], 50);
case 'file':
if ($data['msg']['type'] == 'img') {
return "[图片]";
}
return "[文件] {$data['msg']['name']}";
return self::previewFileMsg($data['msg']);
case 'tag':
$action = $data['msg']['action'] === 'remove' ? '取消标注' : '标注';
return "[{$action}] {$this->previewMsg(false, $data['msg']['data'])}";
$action = Doo::translate($data['msg']['action'] === 'remove' ? '取消标注' : '标注');
return "[{$action}] " . self::previewMsg($data['msg']['data']);
case 'top':
$action = $data['msg']['action'] === 'remove' ? '取消置顶' : '置顶';
return "[{$action}] {$this->previewMsg(false, $data['msg']['data'])}";
$action = Doo::translate($data['msg']['action'] === 'remove' ? '取消置顶' : '置顶');
return "[{$action}] " . self::previewMsg($data['msg']['data']);
case 'todo':
$action = $data['msg']['action'] === 'remove' ? '取消待办' : ($data['msg']['action'] === 'done' ? '完成' : '设待办');
return "[{$action}] {$this->previewMsg(false, $data['msg']['data'])}";
$action = Doo::translate($data['msg']['action'] === 'remove' ? '取消待办' : ($data['msg']['action'] === 'done' ? '完成' : '设待办'));
return "[{$action}] " . self::previewMsg($data['msg']['data']);
case 'notice':
return $data['msg']['notice'];
return Base::cutStr(Doo::translate($data['msg']['notice']), 50);
case 'template':
return self::previewTemplateMsg($data['msg']);
case 'preview':
return $data['msg']['preview'];
default:
return "[未知的消息]";
$action = Doo::translate("未知的消息");
return "[{$action}]";
}
}
/**
* 生成关键词
* @return string
*/
public function generateMsgKey()
{
return match ($this->type) {
'text' => str_replace("&nbsp;", " ", strip_tags($this->msg['text'])),
'meeting', 'file' => $this->msg['name'],
default => '',
};
}
/**
* 返回引用消息(如果是文本消息则截取)
* @param int $strlen
* @return array|mixed
*/
public function quoteTextMsg($strlen = 30)
{
$msg = $this->msg;
if ($this->type === 'text') {
$msg['text'] = $this->previewTextMsg($msg['text']);
if (mb_strlen($msg['text']) > $strlen) {
$msg['text'] = mb_substr($msg['text'], 0, $strlen - 3) . "...";
}
}
return $msg;
}
/**
* 返回文本预览消息
* @param $text
* @param array $msgData
* @param bool $preserveHtml 保留html格式
* @return string|string[]|null
*/
private function previewTextMsg($text, $preserveHtml = false)
private static function previewTextMsg($msgData, $preserveHtml = false)
{
$text = $msgData['text'] ?? '';
if (!$text) return '';
if ($msgData['type'] === 'md') {
$text = Base::markdown2html($text);
}
$text = preg_replace("/<img\s+class=\"emoticon\"[^>]*?alt=\"(\S+)\"[^>]*?>/", "[$1]", $text);
$text = preg_replace("/<img\s+class=\"emoticon\"[^>]*?>/", "[动画表情]", $text);
$text = preg_replace("/<img\s+class=\"browse\"[^>]*?>/", "[图片]", $text);
$text = preg_replace("/<img\s+class=\"emoticon\"[^>]*?>/", "[" . Doo::translate('动画表情') . "]", $text);
$text = preg_replace("/<img\s+class=\"browse\"[^>]*?>/", "[" . Doo::translate('图片') . "]", $text);
if (!$preserveHtml) {
$text = str_replace("</p><p>", "</p> <p>", $text);
$text = strip_tags($text);
$text = str_replace(["&nbsp;", "&amp;", "&lt;", "&gt;"], [" ", "&", "<", ">"], $text);
$text = str_replace(["&nbsp;", "&quot;", "&amp;", "&lt;", "&gt;"], [" ", '"', "&", "<", ">"], $text);
$text = preg_replace("/\s+/", " ", $text);
$text = Base::cutStr($text, 50);
}
return $text;
}
/**
* 预览文件消息
* @param $msg
* @return string
*/
private static function previewFileMsg($msg)
{
if ($msg['type'] == 'img') {
$action = Doo::translate("图片");
return "[{$action}]";
} elseif ($msg['ext'] == 'mp4') {
$action = Doo::translate("视频");
return "[{$action}]";
}
$action = Doo::translate("文件");
return "[{$action}] " . Base::cutStr($msg['name'], 50);
}
/**
* 预览模板消息
* @param $msg
* @return string
*/
private static function previewTemplateMsg($msg)
{
if (!empty($msg['title_raw'])) {
return $msg['title_raw'];
}
if ($msg['type'] === 'task_list' && count($msg['list']) === 1) {
return Doo::translate($msg['title']) . ": " . Base::cutStr($msg['list'][0]['name'], 50);
}
if (!empty($msg['title'])) {
return Doo::translate($msg['title']);
}
if ($msg['type'] === 'content' && is_string($msg['content']) && $msg['content'] !== '') {
return Base::cutStr(Doo::translate($msg['content']), 50);
}
return Doo::translate('未知的消息');
}
/**
* 生成关键词并保存
* @param string $key
* @return void
*/
public function generateKeyAndSave($key = ''): void
{
if (empty($key)) {
$key = '';
switch ($this->type) {
case 'text':
if (!preg_match("/<span[^>]*?data-quick-key=([\"'])([^\"']+?)\\1[^>]*?>/is", $this->msg['text'])) {
$key = strip_tags($this->msg['text']);
}
break;
case 'vote':
case 'word-chain':
$key = strip_tags($this->msg['text']);
break;
case 'file':
$key = $this->msg['name'];
$key = preg_replace("/^(image|\d+)\.(png|jpg|jpeg|webp|gif)$/i", "", $key);
$key = preg_replace("/^LongText-(.*?)/i", "", $key);
break;
case 'meeting':
$key = $this->msg['name'];
break;
}
}
$key = str_replace(["&quot;", "&amp;", "&lt;", "&gt;"], "", $key);
$key = str_replace(["\r", "\n", "\t", "&nbsp;"], " ", $key);
$key = preg_replace("/^\/[A-Za-z]+/", " ", $key);
$key = preg_replace("/\s+/", " ", $key);
$this->key = trim($key);
$this->save();
}
/**
* 返回引用消息(如果是文本只取预览)
* @return array|mixed
*/
public function quoteTextMsg()
{
$msg = $this->msg;
if ($this->type === 'text') {
$msg['text'] = self::previewTextMsg($msg);
}
return $msg;
}
/**
* 处理文本消息内容,用于发送前
* @param $text
@@ -648,9 +760,9 @@ class WebSocketDialogMsg extends AbstractModel
$imagePath = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
Base::makeDir(public_path($imagePath));
$imagePath .= md5s($base64) . "." . $matchs[1][$key];
if (Base::saveContentImage(public_path($imagePath), base64_decode($base64))) {
if (Base::saveContentImage(public_path($imagePath), base64_decode($base64), 90)) {
$imageSize = getimagesize(public_path($imagePath));
if ($extension = Image::thumbImage(public_path($imagePath), public_path($imagePath) . "_thumb.{*}", 320, 0)) {
if ($extension = Image::thumbImage(public_path($imagePath), public_path($imagePath) . "_thumb.{*}", 320, 0, 80)) {
$imagePath .= "_thumb.{$extension}";
}
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}::]", $text);
@@ -724,7 +836,7 @@ class WebSocketDialogMsg extends AbstractModel
}
if (file_exists(public_path($imagePath))) {
$imageSize = getimagesize(public_path($imagePath));
if ($extension = Image::thumbImage(public_path($imagePath), public_path($imagePath) . "_thumb.{*}", 320, 0)) {
if ($extension = Image::thumbImage(public_path($imagePath), public_path($imagePath) . "_thumb.{*}", 320, 0, 80)) {
$imagePath .= "_thumb.{$extension}";
}
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}::]", $text);
@@ -732,9 +844,9 @@ class WebSocketDialogMsg extends AbstractModel
$image = file_get_contents($str);
if (empty($image)) {
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:90:90:images/other/imgerr.jpg::]", $text);
} else if (Base::saveContentImage(public_path($imagePath), $image)) {
} else if (Base::saveContentImage(public_path($imagePath), $image, 90)) {
$imageSize = getimagesize(public_path($imagePath));
if ($extension = Image::thumbImage(public_path($imagePath), public_path($imagePath) . "_thumb.{*}", 320, 0)) {
if ($extension = Image::thumbImage(public_path($imagePath), public_path($imagePath) . "_thumb.{*}", 320, 0, 80)) {
$imagePath .= "_thumb.{$extension}";
}
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}::]", $text);
@@ -768,7 +880,7 @@ class WebSocketDialogMsg extends AbstractModel
$text = str_replace($matchs[0][$key], "[:{$matchChar[1]}:{$keyId}:{$matchValye[1]}:]", $text);
}
// 处理快捷消息
preg_match_all("/<span[^>]*?data-quick-key=([\"'])(.*?)\\1[^>]*?>(.*?)<\/span>/is", $text, $matchs);
preg_match_all("/<span[^>]*?data-quick-key=([\"'])([^\"']+?)\\1[^>]*?>(.*?)<\/span>/is", $text, $matchs);
foreach ($matchs[0] as $key => $str) {
$quickKey = $matchs[2][$key];
$quickLabel = $matchs[3][$key];
@@ -788,7 +900,7 @@ class WebSocketDialogMsg extends AbstractModel
}
}
// 处理链接标签
preg_match_all("/<a[^>]*?href=([\"'])(.*?)\\1[^>]*?>(.*?)<\/a>/is", $text, $matchs);
preg_match_all("/<a[^>]*?href=([\"'])([^\"']+?)\\1[^>]*?>(.*?)<\/a>/is", $text, $matchs);
foreach ($matchs[0] as $key => $str) {
$herf = $matchs[2][$key];
$title = $matchs[3][$key] ?: $herf;
@@ -806,7 +918,7 @@ class WebSocketDialogMsg extends AbstractModel
$text = str_replace($str, "[:LINK:{$herf}:{$title}:]", $text);
}
// 文件分享链接
preg_match_all("/(https*:\/\/)((\w|=|\?|\.|\/|&|-|:|\+|%|;|#|@|,|!)+)/i", $text, $matchs);
preg_match_all("/(https?:\/\/)((\w|=|\?|\.|\/|&|-|:|\+|%|;|#|@|,|!)+)/i", $text, $matchs);
if ($matchs) {
foreach ($matchs[0] as $str) {
preg_match("/\/single\/file\/(.*?)$/i", $str, $match);
@@ -853,6 +965,7 @@ class WebSocketDialogMsg extends AbstractModel
/**
* 发送消息、修改消息
* @param string $action 动作
* - null发送消息
* - reply-98回复消息ID=98
* - update-99更新消息ID=99标记修改
* - change-99更新消息ID=99不标记修改
@@ -865,14 +978,15 @@ class WebSocketDialogMsg extends AbstractModel
* @param bool $push_retry 推送-失败后重试1次有时候在事务里执行数据还没生成时会出现找不到消息的情况
* @param bool|null $push_silence 推送-静默
* - type = [text|file|record|meeting] 默认为false
* @param string|null $search_key 搜索关键词(用于搜索,留空则自动生成)
* @return array
*/
public static function sendMsg($action, $dialog_id, $type, $msg, $sender = null, $push_self = false, $push_retry = false, $push_silence = null)
public static function sendMsg($action, $dialog_id, $type, $msg, $sender = null, $push_self = false, $push_retry = false, $push_silence = null, $search_key = null)
{
$link = 0;
$mtype = $type;
if ($type === 'text') {
if (str_contains($msg['text'], '<a ') || preg_match("/https*:\/\//", $msg['text'])) {
if (str_contains($msg['text'], '<a ') || preg_match("/https?:\/\//", $msg['text'])) {
$link = 1;
}
if (str_contains($msg['text'], '<img ')) {
@@ -889,6 +1003,27 @@ class WebSocketDialogMsg extends AbstractModel
if (in_array($msg['ext'], ['jpg', 'jpeg', 'webp', 'png', 'gif'])) {
$mtype = 'image';
}
} elseif ($type === 'location') {
if (preg_match('/^https?:\/\//', $msg['thumb'])) {
$thumb = file_get_contents($msg['thumb']);
if (empty($thumb)) {
throw new ApiException('获取地图快照失败');
}
$fileUrl = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/" . md5s($msg['thumb']) . ".jpg";
$filePath = public_path($fileUrl);
Base::makeDir(dirname($filePath));
if (!Base::saveContentImage($filePath, $thumb, 90)) {
throw new ApiException('保存地图快照失败');
}
$imageSize = getimagesize($filePath);
if ($imageSize[0] < 20 || $imageSize[1] < 20) {
throw new ApiException('地图快照尺寸太小');
}
$msg['thumb_original'] = $msg['thumb'];
$msg['thumb'] = $fileUrl;
$msg['width'] = $imageSize[0];
$msg['height'] = $imageSize[1];
}
}
if ($push_silence === null) {
$push_silence = !in_array($type, ["text", "file", "record", "meeting"]);
@@ -927,7 +1062,7 @@ class WebSocketDialogMsg extends AbstractModel
];
}
} else {
if ($dialogMsg->type !== 'text') {
if (!in_array($dialogMsg->type, ['text', 'template'])) {
throw new ApiException('此消息不支持此操作');
}
if ($dialogMsg->userid != $sender) {
@@ -942,8 +1077,7 @@ class WebSocketDialogMsg extends AbstractModel
'modify' => $modify,
];
$dialogMsg->updateInstance($updateData);
$dialogMsg->key = $dialogMsg->generateMsgKey();
$dialogMsg->save();
$dialogMsg->generateKeyAndSave($search_key);
//
WebSocketDialogUser::whereDialogId($dialog->id)->whereUserid($sender)->whereHide(1)->change([
'hide' => 0, // 修改消息时,显示会话(仅自己)
@@ -988,10 +1122,9 @@ class WebSocketDialogMsg extends AbstractModel
'msg' => $msg,
'read' => 0,
]);
AbstractModel::transaction(function () use ($dialogMsg) {
AbstractModel::transaction(function () use ($search_key, $dialogMsg) {
$dialogMsg->send = 1;
$dialogMsg->key = $dialogMsg->generateMsgKey();
$dialogMsg->save();
$dialogMsg->generateKeyAndSave($search_key);
//
if ($dialogMsg->type === 'meeting') {
MeetingMsg::createInstance([

View File

@@ -16,7 +16,7 @@ use Carbon\Carbon;
* @property int|null $email 是否发了邮件
* @property int|null $after 在阅读之后才添加的记录
* @property int|null $dot 红点标记
* @property string|null $read_at 阅读时间
* @property \Illuminate\Support\Carbon|null $read_at 阅读时间
* @property-read \App\Models\WebSocketDialogMsg|null $webSocketDialogMsg
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()

View File

@@ -9,7 +9,7 @@ namespace App\Models;
* @property int|null $dialog_id 对话ID
* @property int|null $msg_id 消息ID
* @property int|null $userid 接收会员ID
* @property string|null $done_at 完成时间
* @property \Illuminate\Support\Carbon|null $done_at 完成时间
* @property-read array|mixed $msg_data
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Models;
/**
* App\Models\WebSocketDialogMsgTranslate
*
* @property int $id
* @property int|null $dialog_id 对话ID
* @property int|null $msg_id 消息ID
* @property string|null $language 语言
* @property string|null $content 翻译内容
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate whereContent($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate whereLanguage($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate whereMsgId($value)
* @mixin \Eloquent
*/
class WebSocketDialogMsgTranslate extends AbstractModel
{
function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->timestamps = false;
}
}

View File

@@ -10,8 +10,8 @@ use Carbon\Carbon;
* @property int $id
* @property int|null $dialog_id 对话ID
* @property int|null $userid 会员ID
* @property string|null $top_at 置顶时间
* @property string|null $last_at 最后消息时间
* @property \Illuminate\Support\Carbon|null $top_at 置顶时间
* @property \Illuminate\Support\Carbon|null $last_at 最后消息时间
* @property int|null $mark_unread 是否标记为未读0否1是
* @property int|null $silence 是否免打扰0否1是
* @property int|null $hide 不显示会话0否1是
@@ -47,7 +47,7 @@ use Carbon\Carbon;
*/
class WebSocketDialogUser extends AbstractModel
{
protected $dateFormat = 'Y-m-d H:i:s.v';
protected $dateFormat = 'Y-m-d H:i:s.u';
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@ use FFI;
class Doo
{
private static $doo;
private static $passphrase = "LYHevk5n";
private static $userLanguage = "";
/**
* char转为字符串
@@ -269,12 +269,58 @@ class Doo
/**
* 翻译
* @param $text
* @param string $type
* @param string $lang
* @return string
*/
public static function translate($text, string $type = ""): string
public static function translate($text, string $lang = ""): string
{
return self::string(self::doo()->translate($text, $type));
return self::string(self::doo()->translate($text, $lang ?: self::$userLanguage));
}
/**
* 设置语言
* @param string|integer $lang 语言 或 会员ID
* @return void
*/
public static function setLanguage($lang) {
if (Base::isNumber($lang)) {
$lang = User::find(intval($lang))?->lang ?: "";
}
self::$userLanguage = $lang;
}
/**
* 获取语言列表 或 语言名称
* @param string|false $lang
* @return string|string[]
*/
public static function getLanguages($lang = false)
{
$array = [
"zh" => "简体中文",
"zh-CHT" => "繁体中文",
"en" => "英语",
"ko" => "韩语",
"ja" => "日语",
"de" => "德语",
"fr" => "法语",
"id" => "印度尼西亚语",
"ru" => "俄语",
];
if ($lang !== false) {
return $array[$lang] ?? "";
}
return $array;
}
/**
* 检查语言是否存在
* @param $lang
* @return bool
*/
public static function checkLanguage($lang): bool
{
return array_key_exists($lang, self::getLanguages());
}
/**

View File

@@ -55,6 +55,59 @@ class Extranet
return Base::retSuccess("success", $resData['text']);
}
/**
* 通过 openAI 翻译
* @param $text
* @param $targetLanguage
* @return array
*/
public static function openAItranslations($text, $targetLanguage)
{
$systemSetting = Base::setting('system');
$aibotSetting = Base::setting('aibotSetting');
if ($systemSetting['translation'] !== 'open' || empty($aibotSetting['openai_key'])) {
return Base::retError("翻译功能未开启");
}
$extra = [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $aibotSetting['openai_key'],
];
if ($aibotSetting['openai_agency']) {
$extra['CURLOPT_PROXY'] = $aibotSetting['openai_agency'];
if (str_contains($aibotSetting['openai_agency'], 'socks')) {
$extra['CURLOPT_PROXYTYPE'] = CURLPROXY_SOCKS5;
} else {
$extra['CURLOPT_PROXYTYPE'] = CURLPROXY_HTTP;
}
}
$res = Ihttp::ihttp_request('https://api.openai.com/v1/chat/completions', json_encode([
"model" => "gpt-3.5-turbo",
"messages" => [
[
"role" => "system",
"content" => "你是一个专业的翻译器,翻译的结果尽量符合“项目任务管理系统”的使用,并且翻译的结果不用额外添加换行尽量保持原格式,将提供的文本翻译成“{$targetLanguage}”语言。"
],
[
"role" => "user",
"content" => $text
]
]
]), $extra, 15);
if (Base::isError($res)) {
return Base::retError("翻译失败", $res);
}
$resData = Base::json2array($res['data']);
if (empty($resData['choices'])) {
return Base::retError("翻译失败", $resData);
}
$result = $resData['choices'][0]['message']['content'];
$result = preg_replace('/^\"|\"$/', '', $result);
if (empty($result)) {
return Base::retError("翻译失败", $result);
}
return Base::retSuccess("success", $result);
}
/**
* 获取IP地址经纬度
* @param string $ip
@@ -326,22 +379,74 @@ class Extranet
$text = "心灵鸡汤:{$data}";
}
break;
default:
$text = "";
break;
}
return $text;
}
/**
* 获取搜狗表情包
* @param $keyword
* @return array
*/
public static function sticker($keyword)
{
$data = self::curl("https://pic.sogou.com/napi/wap/searchlist", 1800, 15, [], [
'CURLOPT_CUSTOMREQUEST' => 'POST',
'CURLOPT_POSTFIELDS' => json_encode([
"initQuery" => $keyword . " 表情",
"queryFrom" => "wap",
"ie" => "utf8",
"keyword" => $keyword . " 表情",
// "mode" => 20,
"showMode" => 0,
"start" => 1,
"reqType" => "client",
"reqFrom" => "wap_result",
"prevIsRedis" => "n",
"pagetype" => 0,
"amsParams" => []
]),
'CURLOPT_HTTPHEADER' => [
'Content-Type: application/json',
'Referer: https://pic.sogou.com/'
]
]);
$data = Base::json2array($data);
if ($data['status'] === 0 && $data['data']['picResult']['items']) {
$data = $data['data']['picResult']['items'];
$data = array_filter($data, function ($item) {
return intval($item['thumbHeight']) > 10 && intval($item['thumbWidth']) > 10;
});
return array_map(function ($item) {
return [
'name' => $item['title'],
'src' => $item['thumbUrl'],
'height' => $item['thumbHeight'],
'width' => $item['thumbWidth'],
];
}, $data);
}
return [];
}
/**
* @param $url
* @param int $cacheSecond 缓存时间如果结果为空则缓存有效30秒
* @param int $timeout
* @param array $post
* @param array $extra
* @return string
*/
private static function curl($url, int $cacheSecond = 0, int $timeout = 15): string
private static function curl($url, int $cacheSecond = 0, int $timeout = 15, array $post = [], array $extra = []): string
{
if ($cacheSecond > 0) {
$key = "curlCache::" . md5($url);
$content = Cache::remember($key, Carbon::now()->addSeconds($cacheSecond), function () use ($cacheSecond, $key, $timeout, $url) {
$result = Ihttp::ihttp_request($url, [], [], $timeout);
$key = "curlCache::" . md5($url) . "::" . md5(json_encode($post)) . "::" . md5(json_encode($extra));
$content = Cache::remember($key, Carbon::now()->addSeconds($cacheSecond), function () use ($extra, $post, $cacheSecond, $key, $timeout, $url) {
$result = Ihttp::ihttp_request($url, $post, $extra, $timeout);
$content = Base::isSuccess($result) ? trim($result['data']) : '';
if (empty($content) && $cacheSecond > 30) {
Cache::put($key, "", Carbon::now()->addSeconds(30));
@@ -349,7 +454,7 @@ class Extranet
return $content;
});
} else {
$result = Ihttp::ihttp_request($url, [], [], $timeout);
$result = Ihttp::ihttp_request($url, $post, $extra, $timeout);
$content = Base::isSuccess($result) ? trim($result['data']) : '';
}
//

View File

@@ -24,6 +24,15 @@ class Image
public function __construct($imagePath) {
$this->path = $imagePath;
$this->image = new Imagick($this->path);
$this->updateSize();
}
/**
* 更新图片尺寸
* @return void
* @throws \ImagickException
*/
private function updateSize() {
$geo = $this->image->getImageGeometry();
$this->height = $geo['height'];
$this->width = $geo['width'];
@@ -47,6 +56,36 @@ class Image
return $this->height;
}
/**
* 按比例裁剪
* @param float $ratio
* @return $this
* @throws \ImagickException
*/
public function ratioCrop(float $ratio = 0): static
{
if ($ratio === 0) {
return $this;
}
if ($ratio < 1) {
$ratio = 1 / $ratio;
}
$width = $this->width;
$height = $this->height;
if ($width > $height * $ratio) {
$newWidth = $height * $ratio;
$newHeight = $height;
} elseif ($height > $width * $ratio) {
$newWidth = $width;
$newHeight = $width * $ratio;
} else {
return $this;
}
$this->image->cropImage($newWidth, $newHeight, ($width - $newWidth) / 2, ($height - $newHeight) / 2);
$this->updateSize();
return $this;
}
/**
* 创建缩略图
* @param int $width
@@ -97,6 +136,7 @@ class Image
} else {
$this->image->thumbnailImage($width, $height);
}
$this->updateSize();
return $this;
}
@@ -161,6 +201,15 @@ class Image
$this->image->destroy();
}
/**
* 销毁对象
* @return void
*/
public function destroy()
{
$this->image->destroy();
}
/** ******************************************************************************/
/** ******************************************************************************/
/** ******************************************************************************/
@@ -171,10 +220,11 @@ class Image
* @param string $savePath 保存路径
* @param int $width 宽度
* @param int $height 高度
* @param int $quality 压缩质量0-100, 0 为不压缩
* @param string $mode 模式percentage|cover|contain
* @return string|null 成功返回图片后缀,失败返回 false
*/
public static function thumbImage(string $imagePath, string $savePath, int $width, int $height, string $mode = 'percentage'): ?string
public static function thumbImage(string $imagePath, string $savePath, int $width, int $height, int $quality = 0, string $mode = 'percentage'): ?string
{
if (!file_exists($imagePath)) {
return null;
@@ -187,6 +237,13 @@ class Image
$image = new Image($imagePath);
$image->thumb($width, $height, $mode);
$image->saveTo($savePath);
if ($quality > 0) {
Image::compressImage($savePath, $quality);
}
if ($savePath != $imagePath && filesize($savePath) >= filesize($imagePath)) {
unlink($savePath);
symlink(basename($imagePath), $savePath);
}
return $extension;
} catch (\ImagickException) {
return null;
@@ -194,33 +251,34 @@ class Image
}
/**
* 压缩图片
* @param string $imagePath 图片路径
* @param string|null $savePath 保存路径(默认覆盖原图
* @param int $quality 压缩质量0-100
* @param float $minSize 最小尺寸单位KB
* 压缩图片(如果压缩后的图片比原图还大那就直接使用原图)
* @param array|string $path 图片路径如果是数组第1个元素为原图路径第2个元素为保存路径
* @param int $quality 压缩质量0-100
* @param float $minSize 最小尺寸小于这个尺寸不压缩单位KB
* @return bool
*/
public static function compressImage(string $imagePath, string $savePath = null, int $quality = 100, float $minSize = 10): bool
public static function compressImage(array|string $path, int $quality = 100, float $minSize = 5): bool
{
if (Base::settingFind("system", "image_compress") === 'close') {
return false;
}
if (is_array($path)) {
$imagePath = $path[0];
$savePath = $path[1] ?? $imagePath;
} else {
$imagePath = $path;
$savePath = $path;
}
if (!file_exists($imagePath)) {
return false;
}
$quality = min(max($quality, 1), 100);
$imageSize = filesize($imagePath);
if ($minSize > 0 && $imageSize < $minSize * 1024) {
return false;
}
if (empty($savePath)) {
$savePath = $imagePath;
}
$tmpPath = $imagePath . '.compress.tmp';
try {
$image = new Image($imagePath);
$image->compress($quality);
$image->saveTo($tmpPath);
if (self::compressAuto($imagePath, $tmpPath, $quality)) {
if (filesize($tmpPath) >= $imageSize) {
copy($imagePath, $savePath);
unlink($tmpPath);
@@ -228,8 +286,89 @@ class Image
rename($tmpPath, $savePath);
}
return true;
}
return false;
}
/**
* 自动压缩图片仅限于compressImage方法使用
* @param string $imagePath
* @param string $savePath
* @param int $quality
* @return bool
*/
private static function compressAuto(string $imagePath, string $savePath, int $quality = 100): bool
{
if (strtolower(pathinfo($imagePath, PATHINFO_EXTENSION)) === 'png') {
$minQuality = $quality - 20;
$compressedContent = shell_exec("pngquant --quality={$minQuality}-{$quality} --strip - < " . $imagePath);
if ($compressedContent) {
file_put_contents($savePath, $compressedContent);
return true;
}
}
try {
$image = new Image($imagePath);
$image->compress($quality);
$image->saveTo($savePath);
return true;
} catch (\ImagickException) {
return false;
}
}
/** ******************************************************************************/
/** ******************************************************************************/
/** ******************************************************************************/
// ImageMagick 策略限制配置
private static $limits = [
'width' => 16384, // 16KP
'height' => 16384, // 16KP
'area' => 128000000, // 128MP (128 * 1000000 pixels)
'memory' => 256, // 256MiB
];
/**
* 验证上传的图片
* @param $file
* @return array
*/
public static function validateImage($file)
{
try {
// 获取图片信息
$imageInfo = getimagesize($file);
if ($imageInfo === false) {
return Base::retError('无法获取图片信息');
}
$width = $imageInfo[0];
$height = $imageInfo[1];
$area = $width * $height;
// 检查尺寸限制
if ($width > self::$limits['width']) {
return Base::retError(sprintf('图片宽度(%dpx)超过限制(%dpx)', $width, self::$limits['width']));
}
if ($height > self::$limits['height']) {
return Base::retError(sprintf('图片高度(%dpx)超过限制(%dpx)', $height, self::$limits['height']));
}
if ($area > self::$limits['area']) {
return Base::retError(sprintf('图片总像素(%dpx)超过限制(%dpx)', $area, self::$limits['area']));
}
// 估算内存使用每个像素约4字节
$estimatedMemory = ($area * 4) / (1024 * 1024); // 转换为 MB
if ($estimatedMemory > self::$limits['memory']) {
return Base::retError(sprintf('预计内存使用(%dMB)超过限制(%dMB)', $estimatedMemory, self::$limits['memory']));
}
return Base::retSuccess('success');
} catch (\Exception $e) {
return Base::retError('验证过程发生错误:' . $e->getMessage());
}
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace App\Module\Table;
use ReflectionClass;
use Swoole\Table;
abstract class AbstractData
{
/** @var self */
protected static $instance = null;
/** @var Table */
protected $table;
protected function getTableName(): string
{
$className = (new ReflectionClass(static::class))->getShortName();
return lcfirst($className) . 'Table';
}
private function __clone() {}
private function __wakeup() {}
protected function __construct()
{
$this->table = app('swoole')->{$this->getTableName()};
}
public function getTable()
{
return $this->table;
}
public static function instance()
{
if (static::$instance === null) {
static::$instance = new static();
}
return static::$instance;
}
public static function set($key, $value)
{
return self::instance()->table->set($key, ['value' => $value]);
}
public static function get($key, $default = null)
{
$data = self::instance()->table->get($key);
return $data ? $data['value'] : $default;
}
public static function del($key)
{
return self::instance()->table->del($key);
}
public static function exist($key)
{
return self::instance()->table->exist($key);
}
public static function setMultiple(array $items)
{
foreach ($items as $key => $value) {
self::set($key, $value);
}
}
public static function clear()
{
foreach (self::instance()->table as $key => $row) {
self::del($key);
}
}
public static function getAll()
{
$result = [];
foreach (self::instance()->table as $key => $row) {
$result[$key] = $row['value'];
}
return $result;
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace App\Module\Table;
class GlobalData extends AbstractData
{
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Module\Table;
use App\Models\User;
use App\Tasks\LineTask;
use App\Tasks\PushTask;
use Carbon\Carbon;
use Hhxsv5\LaravelS\Swoole\Task\Task;
class OnlineData extends AbstractData
{
/**
* 上线
* @param $userid
* @return float|int|mixed
*/
public static function online($userid)
{
$key = "online::" . $userid;
$value = self::instance()->getTable()->incr($key, 'value');
if ($value === 1) {
// 通知上线
Task::deliver(new LineTask($userid, true));
// 推送离线时收到的消息
Task::deliver(new PushTask("RETRY::" . $userid));
}
return $value;
}
/**
* 离线
* @param $userid
* @return float|int|mixed
*/
public static function offline($userid)
{
$key = "online::" . $userid;
$value = self::instance()->getTable()->decr($key, 'value');
if ($value === 0) {
// 更新最后在线时间
User::whereUserid($userid)->update([
'line_at' => Carbon::now()
]);
// 通知下线
Task::deliver(new LineTask($userid, false));
// 清除在线状态
self::instance()->getTable()->del($key);
}
return $value;
}
/**
* 获取在线状态
* @param $userid
* @return int
*/
public static function live($userid)
{
$key = "online::" . $userid;
return intval(self::instance()->getTable()->get($key));
}
}

View File

@@ -15,23 +15,48 @@ class TimeRange
public function __construct($data)
{
if (is_array($data)) {
$range = $this->format($data['timerange']);
if ($data['updated_at'] || $data['at_after']) {
$range[0] = $data['updated_at'] ?: $data['at_after'];
}
if ($data['deleted_at']) {
$range[1] = $data['deleted_at'];
$keys = array_keys($data);
if (count($keys) === 2 && $keys[0] === 0 && $keys[1] === 1) {
$range = $data;
} else {
$range = $this->format($data['timerange']);
if ($data['updated_at'] || $data['at_after']) {
$range[0] = $data['updated_at'] ?: $data['at_after'];
}
if ($data['deleted_at']) {
$range[1] = $data['deleted_at'];
}
}
} else {
$range = $this->format($data);
}
//
$updated = Base::isNumber($range[0]) ? intval($range[0]) : trim($range[0]);
$deleted = Base::isNumber($range[1]) ? intval($range[1]) : trim($range[1]);
//
$timezone = config('app.timezone');
$this->updated = $updated ? Carbon::parse($updated)->setTimezone($timezone) : null;
$this->deleted = $deleted ? Carbon::parse($deleted)->setTimezone($timezone) : null;
$this->updated = $range[0] ? Base::newCarbon($range[0]) : null;
$this->deleted = $range[1] ? Base::newCarbon($range[1]) : null;
}
/**
* @return Carbon|null
*/
public function firstTime(): ?Carbon
{
return $this->updated;
}
/**
* @return Carbon|null
*/
public function lastTime(): ?Carbon
{
return $this->deleted;
}
/**
* @return bool
*/
public function isExist(): bool
{
return $this->updated && $this->deleted;
}
/**
@@ -43,7 +68,7 @@ class TimeRange
private function format($timerange)
{
$search = str_contains($timerange, ":") ? ["|"] : ["|", "-"];
return explode(",", str_replace($search, ",", $timerange));
return Base::newTrim(explode(",", str_replace($search, ",", $timerange)));
}
/**

380
app/Module/Timer.php Normal file
View File

@@ -0,0 +1,380 @@
<?php
namespace App\Module;
use App\Services\RequestContext;
use Carbon\Carbon;
class Timer
{
/**
* 获取时间戳
* @return int
*/
public static function time()
{
return intval(RequestContext::get("start_time", time()));
}
/**
* 获取毫秒时间戳
* @return float
*/
public static function msecTime()
{
list($msec, $sec) = explode(' ', microtime());
$time = explode(".", $sec . ($msec * 1000));
return $time[0];
}
/**
* 时间差(不够1个小时算一个小时)
* @param int $s 开始时间戳
* @param int $e 结束时间戳
* @return string
*/
public static function timeDiff($s, $e)
{
$time = $e - $s;
$days = 0;
if ($time >= 86400) { // 如果大于1天
$days = (int)($time / 86400);
$time = $time % 86400; // 计算天后剩余的毫秒数
}
$hours = 0;
if ($time >= 3600) { // 如果大于1小时
$hours = (int)($time / 3600);
$time = $time % 3600; // 计算小时后剩余的毫秒数
}
$minutes = ceil($time / 60); // 剩下的毫秒数都算作分
$daysStr = $days > 0 ? $days . '天' : '';
$hoursStr = ($hours > 0 || ($days > 0 && $minutes > 0)) ? $hours . '时' : '';
$minuteStr = ($minutes > 0) ? $minutes . '分' : '';
return $daysStr . $hoursStr . $minuteStr;
}
/**
* 时间秒数格式化
* @param int $time 时间秒数
* @return string
*/
public static function timeFormat($time)
{
$days = 0;
if ($time >= 86400) { // 如果大于1天
$days = (int)($time / 86400);
$time = $time % 86400; // 计算天后剩余的毫秒数
}
$hours = 0;
if ($time >= 3600) { // 如果大于1小时
$hours = (int)($time / 3600);
$time = $time % 3600; // 计算小时后剩余的毫秒数
}
$minutes = ceil($time / 60); // 剩下的毫秒数都算作分
$daysStr = $days > 0 ? $days . '天' : '';
$hoursStr = ($hours > 0 || ($days > 0 && $minutes > 0)) ? $hours . '时' : '';
$minuteStr = ($minutes > 0) ? $minutes . '分' : '';
return $daysStr . $hoursStr . $minuteStr;
}
/**
* 检测日期格式
* @param string $str 需要检测的字符串
* @return bool
*/
public static function isDate($str)
{
$strArr = explode('-', $str);
if (empty($strArr) || count($strArr) != 3) {
return false;
} else {
list($year, $month, $day) = $strArr;
if (checkdate(intval($month), intval($day), intval($year))) {
return true;
} else {
return false;
}
}
}
/**
* 检测时间格式
* @param string $str 需要检测的字符串
* @return bool
*/
public static function isTime($str)
{
$strArr = explode(':', $str);
$count = count($strArr);
if ($count < 2 || $count > 3) {
return false;
}
$hour = $strArr[0];
if ($hour < 0 || $hour > 23) {
return false;
}
$minute = $strArr[1];
if ($minute < 0 || $minute > 59) {
return false;
}
if ($count == 3) {
$second = $strArr[2];
if ($second < 0 || $second > 59) {
return false;
}
}
return true;
}
/**
* 检测 日期格式 或 时间格式
* @param string $str 需要检测的字符串
* @return bool
*/
public static function isDateOrTime($str)
{
return self::isDate($str) || self::isTime($str);
}
/**
* 时间转毫秒时间戳
* @param $time
* @return float|int
*/
public static function strtotimeM($time)
{
if (str_contains($time, '.')) {
list($t, $m) = explode(".", $time);
if (is_string($t)) {
$t = strtotime($t);
}
$time = $t . str_pad($m, 3, "0", STR_PAD_LEFT);
}
if (is_numeric($time)) {
return (int) str_pad($time, 13, "0");
} else {
return strtotime($time) * 1000;
}
}
/**
* 时间格式化
* @param $date
* @return false|string
*/
public static function forumDate($date)
{
$dur = time() - $date;
if ($date > Carbon::now()->startOf('day')->timestamp) {
//今天
if ($dur < 60) {
return max($dur, 1) . '秒前';
} elseif ($dur < 3600) {
return floor($dur / 60) . '分钟前';
} elseif ($dur < 86400) {
return floor($dur / 3600) . '小时前';
} else {
return date("H:i", $date);
}
} elseif ($date > Carbon::now()->subDays()->startOf('day')->timestamp) {
//昨天
return '昨天';
} elseif ($date > Carbon::now()->subDays(2)->startOf('day')->timestamp) {
//前天
return '前天';
} elseif ($dur > 86400) {
//x天前
return floor($dur / 86400) . '天前';
}
return date("Y-m-d", $date);
}
/**
* 获取(时间戳转)今天是星期几,只返回(几)
* @param string|number $unixTime
* @return string
*/
public static function getWeek($unixTime = '')
{
$unixTime = is_numeric($unixTime) ? $unixTime : time();
$weekarray = ['日', '一', '二', '三', '四', '五', '六'];
return $weekarray[date('w', $unixTime)];
}
/**
* 获取(时间戳转)现在时间段:深夜、凌晨、早晨、上午.....
* @param string|number $unixTime
* @return string
*/
public static function getDayeSegment($unixTime = '')
{
$unixTime = is_numeric($unixTime) ? $unixTime : time();
$H = date('H', $unixTime);
if ($H >= 19) {
return '晚上';
} elseif ($H >= 18) {
return '傍晚';
} elseif ($H >= 13) {
return '下午';
} elseif ($H >= 12) {
return '中午';
} elseif ($H >= 8) {
return '上午';
} elseif ($H >= 5) {
return '早晨';
} elseif ($H >= 1) {
return '凌晨';
} elseif ($H >= 0) {
return '深夜';
} else {
return '';
}
}
/**
* 秒 (转) 年、天、时、分、秒
* @param $time
* @return array|bool
*/
public static function sec2time($time)
{
if (is_numeric($time)) {
$value = array(
"years" => 0, "days" => 0, "hours" => 0,
"minutes" => 0, "seconds" => 0,
);
if ($time >= 86400) {
$value["days"] = floor($time / 86400);
$time = ($time % 86400);
}
if ($time >= 3600) {
$value["hours"] = floor($time / 3600);
$time = ($time % 3600);
}
if ($time >= 60) {
$value["minutes"] = floor($time / 60);
$time = ($time % 60);
}
$value["seconds"] = floor($time);
return (array)$value;
} else {
return (bool)FALSE;
}
}
/**
* 年、天、时、分、秒 (转) 秒
* @param $value
* @return int
*/
public static function time2sec($value)
{
$time = intval($value["seconds"]);
$time += intval($value["minutes"] * 60);
$time += intval($value["hours"] * 3600);
$time += intval($value["days"] * 86400);
$time += intval($value["years"] * 31536000);
return $time;
}
/**
* 阿拉伯数字转化为中文
* @param $num
* @return string
*/
public static function chinaNum($num)
{
$china = array('零', '一', '二', '三', '四', '五', '六', '七', '八', '九');
$arr = str_split($num);
$txt = '';
for ($i = 0; $i < count($arr); $i++) {
$txt .= $china[$arr[$i]];
}
return $txt;
}
/**
* 阿拉伯数字转化为中文(用于星期,七改成日)
* @param $num
* @return string
*/
public static function chinaNumZ($num)
{
return str_replace("", "", Timer::chinaNum($num));
}
/**
* 时间是否在时间范围内
* @param array $timeRanges 如:['08:00', '12:00'] 或 [['08:00', '12:00'], ['14:00', '18:00']]
* @param string|null $currentTime
* @return bool
*/
public static function isTimeInRanges(array $timeRanges, ?string $currentTime = null): bool
{
// 如果没有传入当前时间,使用当前时间
$currentTime = $currentTime ?? date('H:i');
// 转换当前时间为分钟数,便于比较
$currentMinutes = self::timeToMinutes($currentTime);
if ($currentMinutes === false) {
return false;
}
// 将单个时间范围转换为数组格式
if (isset($timeRanges[0]) && !is_array($timeRanges[0])) {
$timeRanges = [$timeRanges];
}
// 过滤并检查有效的时间范围
foreach ($timeRanges as $range) {
if (!self::isValidTimeRange($range)) {
continue;
}
$startMinutes = self::timeToMinutes($range[0]);
$endMinutes = self::timeToMinutes($range[1]);
if ($startMinutes === false || $endMinutes === false) {
continue;
}
if ($startMinutes <= $currentMinutes && $currentMinutes <= $endMinutes) {
return true;
}
}
return false;
}
/**
* 辅助函数:检查时间范围是否有效
* @param $range
* @return bool
*/
private static function isValidTimeRange($range): bool
{
return is_array($range)
&& count($range) === 2
&& is_string($range[0])
&& is_string($range[1])
&& !empty($range[0])
&& !empty($range[1])
&& preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $range[0])
&& preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $range[1]);
}
/**
* 辅助函数:将时间转换为分钟数
* @param string $time
* @return false|float|int
*/
private static function timeToMinutes(string $time)
{
if (!preg_match('/^([01]?[0-9]|2[0-3]):([0-5][0-9])$/', $time, $matches)) {
return false;
}
return intval($matches[1]) * 60 + intval($matches[2]);
}
}

View File

@@ -87,6 +87,9 @@ class ProjectTaskObserver
if (in_array('project', $dataType)) {
return ProjectUser::whereProjectId($projectTask->project_id)->pluck('userid')->toArray();
}
if (in_array('projectOwnerUser', $dataType)) {
return ProjectUser::whereProjectId($projectTask->project_id)->where('owner', 1)->pluck('userid')->toArray();
}
$array = [];
if (in_array('task', $dataType)) {
$array = array_merge($array, ProjectTaskUser::whereTaskId($projectTask->id)->pluck('userid')->toArray());
@@ -112,7 +115,8 @@ class ProjectTaskObserver
case 3:
$dataType = $projectTask->visibility == 2 ? ['task'] : ['task', 'visibility'];
$forgetUserids = self::userids($projectTask, $dataType);
$recordUserids = array_diff($projectUserids, $forgetUserids);
$projectOwnerUserIds = self::userids($projectTask, 'projectOwnerUser');
$recordUserids = array_diff($projectUserids, $forgetUserids, $projectOwnerUserIds);
Deleted::record('projectTask', $projectTask->id, $recordUserids);
Deleted::forget('projectTask', $projectTask->id, $forgetUserids);
break;

View File

@@ -49,7 +49,8 @@ class WebSocketDialogObserver
*/
public function restored(WebSocketDialog $webSocketDialog)
{
Deleted::forget('dialog', $webSocketDialog->id, $this->userids($webSocketDialog));
$userids = $this->userids($webSocketDialog);
Deleted::forget('dialog', $webSocketDialog->id, $userids);
}
/**

View File

@@ -0,0 +1,142 @@
<?php
namespace App\Services;
use Illuminate\Http\Request;
class RequestContext
{
/** @var array<string, array<string, mixed>> */
private static array $context = [];
private const REQUEST_ID_PREFIX = 'req_';
/**
* 生成请求唯一ID
*/
public static function generateRequestId(): string
{
return self::REQUEST_ID_PREFIX . uniqid() . mt_rand(10000, 99999);
}
/**
* 获取当前请求ID
*/
private static function getCurrentRequestId(): ?string
{
/** @var Request $request */
$request = request();
return $request?->requestId;
}
/**
* 设置请求上下文
*
* @param string $key
* @param mixed $value
* @param string|null $requestId
* @return void
*/
public static function set(string $key, mixed $value, ?string $requestId = null): void
{
$requestId = $requestId ?? self::getCurrentRequestId();
if ($requestId === null) {
return;
}
self::$context[$requestId] ??= [];
self::$context[$requestId][$key] = $value;
}
// 与 set 方法的区别是save 方法会返回传入的 value 值
public static function save(string $key, mixed $value, ?string $requestId = null): mixed
{
self::set($key, $value, $requestId);
return $value;
}
/**
* 获取请求上下文
*
* @param string $key
* @param mixed $default
* @param string|null $requestId
* @return mixed
*/
public static function get(string $key, mixed $default = null, ?string $requestId = null): mixed
{
$requestId = $requestId ?? self::getCurrentRequestId();
if ($requestId === null) {
return $default;
}
return self::$context[$requestId][$key] ?? $default;
}
/**
* 判断请求上下文是否存在
*
* @param string $key
* @param string|null $requestId
* @return bool
*/
public static function has(string $key, ?string $requestId = null): bool
{
$requestId = $requestId ?? self::getCurrentRequestId();
if ($requestId === null) {
return false;
}
return isset(self::$context[$requestId][$key]);
}
/**
* 清理请求上下文
*
* @param string|null $requestId
* @return void
*/
public static function clear(?string $requestId = null): void
{
$requestId = $requestId ?? self::getCurrentRequestId();
if ($requestId === null) {
return;
}
unset(self::$context[$requestId]);
}
/**
* 获取当前请求的所有上下文数据
*
* @param string|null $requestId
* @return array<string, mixed>
*/
public static function getAll(?string $requestId = null): array
{
$requestId = $requestId ?? self::getCurrentRequestId();
if ($requestId === null) {
return [];
}
return self::$context[$requestId] ?? [];
}
/**
* 批量设置上下文数据
*
* @param array<string, mixed> $data
* @param string|null $requestId
* @return void
*/
public static function setMultiple(array $data, ?string $requestId = null): void
{
$requestId = $requestId ?? self::getCurrentRequestId();
if ($requestId === null) {
return;
}
self::$context[$requestId] ??= [];
self::$context[$requestId] = array_merge(self::$context[$requestId], $data);
}
}

View File

@@ -6,10 +6,9 @@ namespace App\Services;
use App\Models\User;
use App\Models\WebSocket;
use App\Models\WebSocketDialogMsg;
use App\Module\Base;
use App\Module\Doo;
use App\Tasks\LineTask;
use App\Module\Table\OnlineData;
use App\Tasks\PushTask;
use Cache;
use Carbon\Carbon;
@@ -25,7 +24,6 @@ use Swoole\WebSocket\Server;
class WebSocketService implements WebSocketHandlerInterface
{
/**
* 声明没有参数的构造函数
* WebSocketService constructor.
*/
public function __construct()
@@ -37,14 +35,14 @@ class WebSocketService implements WebSocketHandlerInterface
* 连接建立时触发
* @param Server $server
* @param Request $request
* @return void
*/
public function onOpen(Server $server, Request $request)
{
$fd = $request->fd;
$get = Base::newTrim($request->get);
$action = $get['action'];
Cache::forget("User::encrypt:" . $fd);
switch ($action) {
switch ($get['action']) {
/**
* 网页访问
*/
@@ -52,23 +50,21 @@ class WebSocketService implements WebSocketHandlerInterface
{
Doo::load($get['token'], $get['language']);
//
if (Doo::userId() > 0
&& !Doo::userExpired()
&& $user = User::whereUserid(Doo::userId())->whereEmail(Doo::userEmail())->whereEncrypt(Doo::userEncrypt())->first()) {
// 保存用户
$this->saveUser($fd, $user->userid);
// 发送open事件
$count = 0;
$userid = Doo::userId();
if ($userid > 0 && !Doo::userExpired()) {
$count = User::whereUserid($userid)->whereEmail(Doo::userEmail())->whereEncrypt(Doo::userEncrypt())->count();
}
if ($count) {
// 用户正常
$server->push($fd, Base::array2json([
'type' => 'open',
'data' => [
'fd' => $fd,
'ud' => $user->userid,
'ud' => $userid,
],
]));
// 通知上线
Task::deliver(new LineTask($user->userid, true));
// 推送离线时收到的消息
Task::deliver(new PushTask("RETRY::" . $user->userid));
$this->userOn($fd, $userid);
} else {
// 用户不存在
$server->push($fd, Base::array2json([
@@ -78,7 +74,6 @@ class WebSocketService implements WebSocketHandlerInterface
],
]));
$server->close($fd);
$this->deleteUser($fd);
}
}
break;
@@ -92,39 +87,23 @@ class WebSocketService implements WebSocketHandlerInterface
* 收到消息时触发
* @param Server $server
* @param Frame $frame
* @return void
*/
public function onMessage(Server $server, Frame $frame)
{
$msg = Base::json2array($frame->data);
$type = $msg['type']; // 消息类型
$msgId = $msg['msgId']; // 消息ID用于回调
$data = $msg['data']; // 消息详情
//
$type = $msg['type']; // 消息类型
$data = $msg['data']; // 消息详情
$msgId = $msg['msgId'] ?? $msg['msg_id']; // 消息ID用于回调
// 处理消息
$reData = [];
switch ($type) {
/**
* 收到回执
*/
// 收到回执
case 'receipt':
return;
/**
* 已阅消息
*/
case 'readMsg':
$ids = is_array($data['id']) ? $data['id'] : [$data['id']];
$userid = $this->getUserid($frame->fd);
WebSocketDialogMsg::whereIn('id', $ids)->chunkById(20, function($list) use ($userid) {
/** @var WebSocketDialogMsg $item */
foreach ($list as $item) {
$item->readSuccess($userid);
}
});
return;
/**
* 访问状态
*/
// 访问状态
case 'path':
$row = WebSocket::whereFd($frame->fd)->first();
if ($row) {
@@ -141,9 +120,7 @@ class WebSocketService implements WebSocketHandlerInterface
}
return;
/**
* 加密参数
*/
// 加密参数
case 'encrypt':
if ($data['type'] === 'pgp') {
$data['key'] = Doo::pgpPublicFormat($data['key']);
@@ -151,7 +128,8 @@ class WebSocketService implements WebSocketHandlerInterface
Cache::put("User::encrypt:" . $frame->fd, Base::array2json($data), Carbon::now()->addDay());
return;
}
//
// 返回消息
if ($msgId) {
PushTask::push([
'fd' => $frame->fd,
@@ -169,15 +147,11 @@ class WebSocketService implements WebSocketHandlerInterface
* @param Server $server
* @param $fd
* @param $reactorId
* @throws \Exception
* @return void
*/
public function onClose(Server $server, $fd, $reactorId)
{
$userid = $this->getUserid($fd);
if($userid){
Task::deliver(new LineTask($userid, false)); // 通知离线
$this->deleteUser($fd);
}
$this->userOff($fd);
}
/** ****************************************************************************** */
@@ -185,65 +159,49 @@ class WebSocketService implements WebSocketHandlerInterface
/** ****************************************************************************** */
/**
* 保存用户
* 用户上线
* @param $fd
* @param $userid
* @return void
*/
private function saveUser($fd, $userid)
private function userOn($fd, $userid)
{
Cache::put("User::fd:" . $fd, "on", Carbon::now()->addDay());
Cache::put("User::online:" . $userid, "on", Carbon::now()->addDay());
//
WebSocket::updateInsert([
'key' => md5($fd . '@' . $userid)
], [
'fd' => $fd,
'userid' => $userid,
]);
OnlineData::online($userid);
}
/**
* 清除用户
* 用户下线
* @param $fd
* @return void
*/
private function deleteUser($fd)
private function userOff($fd)
{
Cache::forget("User::fd:" . $fd);
//
$array = [];
WebSocket::whereFd($fd)->chunk(10, function($list) use (&$array) {
$paths = [];
WebSocket::whereFd($fd)->chunk(10, function($list) use (&$paths) {
/** @var WebSocket $item */
foreach ($list as $item) {
$item->delete();
if ($item->userid) {
// 离线时更新会员最后在线时间
User::whereUserid($item->userid)->update([
'line_at' => Carbon::now()
]);
Cache::forget("User::online:" . $item->userid);
OnlineData::offline($item->userid);
}
if ($item->path && str_starts_with($item->path, "/single/file/")) {
$array[$item->path] = $item->path;
$paths[$item->path] = $item->path;
}
}
});
foreach ($array as $path) {
foreach ($paths as $path) {
$this->pushPath($path);
}
}
/**
* 根据fd获取会员ID
* @param $fd
* @return int
*/
private function getUserid($fd)
{
return intval(WebSocket::whereFd($fd)->value('userid'));
}
/**
* 发送给相同访问状态的会员
* 通知相同访问路径的用户
* @param $path
*/
private function pushPath($path)

View File

@@ -3,7 +3,6 @@ namespace App\Tasks;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Models\AbstractModel;
use App\Models\ProjectTask;
use App\Module\Base;
use Carbon\Carbon;

View File

@@ -10,6 +10,7 @@ use App\Module\Base;
use App\Module\Doo;
use App\Module\Ihttp;
use Carbon\Carbon;
use DB;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
@@ -62,6 +63,22 @@ class BotReceiveMsgTask extends AbstractTask
*/
private function botManagerReceive(WebSocketDialogMsg $msg, User $botUser)
{
// 位置消息
if ($msg->type === 'location') {
// 签到机器人
if ($botUser->email === 'check-in@bot.system') {
$content = UserBot::checkinBotQuickMsg('locat-checkin', $msg->userid, $msg->msg);
if ($content) {
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'template', [
'type' => 'content',
'content' => $content,
], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
}
}
return;
}
// 文本消息
if ($msg->type !== 'text') {
return;
}
@@ -69,7 +86,7 @@ class BotReceiveMsgTask extends AbstractTask
if ($this->mention) {
$original = preg_replace("/<span class=\"mention user\" data-id=\"(\d+)\">(.*?)<\/span>/", "", $original);
}
if (preg_match("/<span[^>]*?data-quick-key=([\"'])(.*?)\\1[^>]*?>(.*?)<\/span>/is", $original, $match)) {
if (preg_match("/<span[^>]*?data-quick-key=([\"'])([^\"']+?)\\1[^>]*?>(.*?)<\/span>/is", $original, $match)) {
$command = $match[2];
if (str_starts_with($command, '%3A.')) {
$command = ":" . substr($command, 4);
@@ -93,16 +110,23 @@ class BotReceiveMsgTask extends AbstractTask
}
// 签到机器人
if ($botUser->email === 'check-in@bot.system') {
$text = UserBot::checkinBotQuickMsg($command, $msg->userid);
if ($text) {
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', ['text' => $text], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
$content = UserBot::checkinBotQuickMsg($command, $msg->userid);
if ($content) {
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'template', [
'type' => 'content',
'content' => $content,
], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
}
}
// 隐私机器人
if ($botUser->email === 'anon-msg@bot.system') {
$text = UserBot::anonBotQuickMsg($command);
if ($text) {
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', ['text' => $text], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
$array = UserBot::anonBotQuickMsg($command);
if ($array) {
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'template', [
'type' => 'content',
'title' => $array['title'],
'content' => $array['content'],
], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
}
}
// 管理机器人
@@ -112,15 +136,17 @@ class BotReceiveMsgTask extends AbstractTask
} elseif (UserBot::whereBotId($botUser->userid)->whereUserid($msg->userid)->exists()) {
$isManager = false;
} else {
$text = "非常抱歉,我不是你的机器人,无法完成你的指令。";
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', ['text' => $text], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'template', [
'type' => 'content',
'content' => "非常抱歉,我不是你的机器人,无法完成你的指令。",
], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
return;
}
//
$array = Base::newTrim(explode(" ", "{$command} "));
$type = $array[0];
$data = [];
$notice = "";
$content = "";
if (!$isManager && in_array($type, ['/list', '/newbot'])) {
return; // 这些操作仅支持【机器人管理】机器人
}
@@ -143,20 +169,19 @@ class BotReceiveMsgTask extends AbstractTask
->orderByDesc('id')
->get();
if ($data->isEmpty()) {
$type = "notice";
$notice = "您没有创建机器人。";
$content = "您没有创建机器人。";
}
break;
/**
* 详情
*/
case '/hello':
case '/info':
$botId = $isManager ? $array[1] : $botUser->userid;
$data = $this->botManagerOne($botId, $msg->userid);
if (!$data) {
$type = "notice";
$notice = "机器人不存在。";
$content = "机器人不存在。";
}
break;
@@ -169,27 +194,27 @@ class BotReceiveMsgTask extends AbstractTask
->where('users.bot', 1)
->where('user_bots.userid', $msg->userid)
->count() >= 50) {
$type = "notice";
$notice = "超过最大创建数量。";
$content = "超过最大创建数量。";
break;
}
if (strlen($array[1]) < 2 || strlen($array[1]) > 20) {
$type = "notice";
$notice = "机器人名称由2-20个字符组成。";
$content = "机器人名称由2-20个字符组成。";
break;
}
$data = User::botGetOrCreate("user-" . Base::generatePassword(), [
'nickname' => $array[1]
], $msg->userid);
if (empty($data)) {
$type = "notice";
$notice = "创建失败。";
$content = "创建失败。";
break;
}
$dialog = WebSocketDialog::checkUserDialog($data, $msg->userid);
if ($dialog) {
$text = "<p>您好,我是机器人:{$data->nickname}我的机器人ID是{$data->userid}</p><p>你可以发送 <u><b>/help</b></u> 查看我支持什么命令。</p>";
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $data->userid); // todo 未能在任务end事件来发送任务
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => '/hello',
'title' => '创建成功。',
'data' => $data,
], $data->userid); // todo 未能在任务end事件来发送任务
}
break;
@@ -200,8 +225,7 @@ class BotReceiveMsgTask extends AbstractTask
$botId = $isManager ? $array[1] : $botUser->userid;
$nameString = $isManager ? $array[2] : $array[1];
if (strlen($nameString) < 2 || strlen($nameString) > 20) {
$type = "notice";
$notice = "机器人名称由2-20个字符组成。";
$content = "机器人名称由2-20个字符组成。";
break;
}
$data = $this->botManagerOne($botId, $msg->userid);
@@ -211,8 +235,7 @@ class BotReceiveMsgTask extends AbstractTask
$data->pinyin = Base::cn2pinyin($nameString);
$data->save();
} else {
$type = "notice";
$notice = "机器人不存在。";
$content = "机器人不存在。";
}
break;
@@ -226,8 +249,7 @@ class BotReceiveMsgTask extends AbstractTask
if ($data) {
$data->deleteUser('delete bot');
} else {
$type = "notice";
$notice = "机器人不存在。";
$content = "机器人不存在。";
}
break;
@@ -240,8 +262,7 @@ class BotReceiveMsgTask extends AbstractTask
if ($data) {
User::generateToken($data);
} else {
$type = "notice";
$notice = "机器人不存在。";
$content = "机器人不存在。";
}
break;
@@ -256,8 +277,7 @@ class BotReceiveMsgTask extends AbstractTask
$data->password = Doo::md5s(Base::generatePassword(32), $data->encrypt);
$data->save();
} else {
$type = "notice";
$notice = "机器人不存在。";
$content = "机器人不存在。";
}
break;
@@ -278,8 +298,7 @@ class BotReceiveMsgTask extends AbstractTask
$data->clear_day = $userBot->clear_day;
$data->clear_at = $userBot->clear_at; // 这两个参数只是作为输出,所以不保存
} else {
$type = "notice";
$notice = "机器人不存在。";
$content = "机器人不存在。";
}
break;
@@ -291,8 +310,7 @@ class BotReceiveMsgTask extends AbstractTask
$webhookUrl = $isManager ? $array[2] : $array[1];
$data = $this->botManagerOne($botId, $msg->userid);
if (strlen($webhookUrl) > 255) {
$type = "notice";
$notice = "webhook地址最长仅支持255个字符。";
$content = "webhook地址最长仅支持255个字符。";
} elseif ($data) {
$userBot = UserBot::whereBotId($botId)->whereUserid($msg->userid)->first();
if ($userBot) {
@@ -303,8 +321,7 @@ class BotReceiveMsgTask extends AbstractTask
$data->webhook_url = $userBot->webhook_url ?: '-';
$data->webhook_num = $userBot->webhook_num; // 这两个参数只是作为输出,所以不保存
} else {
$type = "notice";
$notice = "机器人不存在。";
$content = "机器人不存在。";
}
break;
@@ -316,43 +333,65 @@ class BotReceiveMsgTask extends AbstractTask
$nameKey = $isManager ? $array[2] : $array[1];
$data = $this->botManagerOne($botId, $msg->userid);
if ($data) {
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->where('web_socket_dialogs.name', 'LIKE', "%{$nameKey}%")
$list = DB::table('web_socket_dialog_users as u')
->select(['d.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
->join('web_socket_dialogs as d', 'u.dialog_id', '=', 'd.id')
->where('u.userid', $data->userid)
->where('d.name', 'LIKE', "%{$nameKey}%")
->whereNull('d.deleted_at')
->orderByDesc('u.top_at')
->orderByDesc('u.last_at')
->take(20)
->get();
if ($list->isEmpty()) {
$type = "notice";
$notice = "没有搜索到相关会话。";
->get()
->map(function($item) use ($data) {
return WebSocketDialog::synthesizeData($item, $data->userid);
})
->all();
if (empty($list)) {
$content = "没有搜索到相关会话。";
} else {
$list->transform(function (WebSocketDialog $item) use ($data) {
return $item->formatData($data->userid);
});
$data->list = $list; // 这个参数只是作为输出,所以不保存
}
} else {
$type = "notice";
$notice = "机器人不存在。";
$content = "机器人不存在。";
}
break;
}
//
$text = view('push.bot', [
'type' => $type,
'data' => $data,
'notice' => $notice,
'manager' => $isManager,
'version' => Base::getVersion()
])->render();
if (!$isManager) {
$text = preg_replace("/\s*\{机器人ID\}/", "", $text);
if ($content) {
$msgData = [
'type' => 'content',
'content' => $content,
];
} else {
$msgData = [
'type' => $type,
'data' => $data,
];
$msgData['title'] = match ($type) {
'/hello' => '您好',
'/help' => '帮助指令',
'/list' => '我的机器人',
'/info' => '机器人信息',
'/newbot' => '新建机器人',
'/setname' => '设置名称',
'/deletebot' => '删除机器人',
'/token' => '机器人Token',
'/revoke' => '更新Token',
'/webhook' => '设置Webhook',
'/clearday' => '设置保留消息时间',
'/dialog' => '对话列表',
'/api' => 'API接口文档',
default => '不支持的指令',
};
if ($type == '/api') {
$msgData['version'] = Base::getVersion();
} elseif ($type == '/help') {
$msgData['manager'] = $isManager;
}
}
$text = preg_replace("/^\x20+/", "", $text);
$text = preg_replace("/\n\x20+/", "\n", $text);
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', ['text' => $text], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'template', $msgData, $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
}
}
@@ -369,7 +408,7 @@ class BotReceiveMsgTask extends AbstractTask
$serverUrl = 'http://' . env('APP_IPPR') . '.3';
$userBot = null;
$extras = [];
$error = null;
$errorContent = null;
switch ($botUser->email) {
// ChatGPT 机器人
case 'ai-openai@bot.system':
@@ -383,10 +422,10 @@ class BotReceiveMsgTask extends AbstractTask
'chunk_size' => 7,
];
if (empty($extras['openai_key'])) {
$error = 'Robot disabled.';
$errorContent = '机器人未启用。';
} elseif (in_array($this->client['platform'], ['win', 'mac', 'web'])
&& !Base::judgeClientVersion("0.29.11", $this->client['version'])) {
$error = 'The client version is low (required version ≥ v0.29.11).';
$errorContent = '当前客户端版本低(所需版本≥v0.29.11)。';
}
break;
// Claude 机器人
@@ -399,10 +438,10 @@ class BotReceiveMsgTask extends AbstractTask
'server_url' => $serverUrl,
];
if (empty($extras['claude_token'])) {
$error = 'Robot disabled.';
$errorContent = '机器人未启用。';
} elseif (in_array($this->client['platform'], ['win', 'mac', 'web'])
&& !Base::judgeClientVersion("0.29.11", $this->client['version'])) {
$error = 'The client version is low (required version ≥ v0.29.11).';
$errorContent = '当前客户端版本低(所需版本≥v0.29.11)。';
}
break;
// Wenxin 机器人
@@ -416,10 +455,10 @@ class BotReceiveMsgTask extends AbstractTask
'server_url' => $serverUrl,
];
if (empty($extras['wenxin_key'])) {
$error = 'Robot disabled.';
$errorContent = '机器人未启用。';
} elseif (in_array($this->client['platform'], ['win', 'mac', 'web'])
&& !Base::judgeClientVersion("0.29.11", $this->client['version'])) {
$error = 'The client version is low (required version ≥ v0.29.12).';
$errorContent = '当前客户端版本低(所需版本≥v0.29.12)。';
}
break;
// QianWen 机器人
@@ -432,10 +471,10 @@ class BotReceiveMsgTask extends AbstractTask
'server_url' => $serverUrl,
];
if (empty($extras['qianwen_key'])) {
$error = 'Robot disabled.';
$errorContent = '机器人未启用。';
} elseif (in_array($this->client['platform'], ['win', 'mac', 'web'])
&& !Base::judgeClientVersion("0.29.11", $this->client['version'])) {
$error = 'The client version is low (required version ≥ v0.29.12).';
$errorContent = '当前客户端版本低(所需版本≥v0.29.12)。';
}
break;
// Gemini 机器人
@@ -450,10 +489,10 @@ class BotReceiveMsgTask extends AbstractTask
'server_url' => $serverUrl,
];
if (empty($extras['gemini_key'])) {
$error = 'Robot disabled.';
$errorContent = '机器人未启用。';
} elseif (in_array($this->client['platform'], ['win', 'mac', 'web'])
&& !Base::judgeClientVersion("0.29.11", $this->client['version'])) {
$error = 'The client version is low (required version ≥ v0.29.12).';
$errorContent = '当前客户端版本低(所需版本≥v0.29.12)。';
}
break;
// 智谱清言 机器人
@@ -466,23 +505,26 @@ class BotReceiveMsgTask extends AbstractTask
'server_url' => $serverUrl,
];
if (empty($extras['zhipu_key'])) {
$error = 'Robot disabled.';
$errorContent = '机器人未启用。';
} elseif (in_array($this->client['platform'], ['win', 'mac', 'web'])
&& !Base::judgeClientVersion("0.29.11", $this->client['version'])) {
$error = 'The client version is low (required version ≥ v0.29.12).';
$errorContent = '当前客户端版本低(所需版本≥v0.29.12)。';
}
break;
break;
// 其他机器人
default:
$userBot = UserBot::whereBotId($botUser->userid)->first();
$webhookUrl = $userBot?->webhook_url;
break;
}
if ($error) {
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', ['text' => $error], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
if ($errorContent) {
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'template', [
'type' => 'content',
'content' => $errorContent,
], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
return;
}
if (!preg_match("/^https*:\/\//", $webhookUrl)) {
if (!preg_match("/^https?:\/\//", $webhookUrl)) {
return;
}
//
@@ -504,13 +546,19 @@ class BotReceiveMsgTask extends AbstractTask
$userBot->webhook_num++;
$userBot->save();
}
if($res['data'] && $data = json_decode($res['data'])){
if($data['code'] != 200 && $data['message']){
if ($res['data'] && $data = Base::json2array($res['data'])) {
if ($data['code'] != 200 && $data['message']) {
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', ['text' => $res['data']['message']], $botUser->userid, false, false, true);
}
}
} catch (\Throwable $th) {
//throw $th;
info(Base::array2json([
'bot_userid' => $botUser->userid,
'dialog' => $dialog->id,
'msg' => $msg->id,
'webhook_url' => $webhookUrl,
'error' => $th->getMessage(),
]));
}
}

View File

@@ -7,7 +7,9 @@ use App\Models\UserCheckinRecord;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Module\Base;
use App\Module\Doo;
use App\Module\Extranet;
use App\Module\Timer;
use Cache;
use Carbon\Carbon;
@@ -40,14 +42,14 @@ class CheckinRemindTask extends AbstractTask
//
if ($remindin > 0) {
$timeRemindin = $timeStart - $remindin;
if ($timeRemindin <= Base::time() && Base::time() <= $timeStart) {
if ($timeRemindin <= Timer::time() && Timer::time() <= $timeStart) {
// 签到打卡提醒
$this->remind('in');
}
}
if ($remindexceed > 0) {
$timeRemindexceed = $timeStart + $remindexceed;
if ($timeRemindexceed <= Base::time() && Base::time() <= $timeRemindexceed + 300) {
if ($timeRemindexceed <= Timer::time() && Timer::time() <= $timeRemindexceed + 300) {
// 签到缺卡提醒
$this->remind('exceed');
}
@@ -83,11 +85,28 @@ class CheckinRemindTask extends AbstractTask
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
if ($dialog) {
if ($type === 'exceed') {
$text = "<p><strong style='color:red'>缺卡提醒:</strong>上班时间到了,你还没有打卡哦~</p>";
$title = '缺卡提醒';
$style = 'color:#f55;';
$content = '上班时间到了,你还没有打卡哦~';
} else {
$text = "<p><strong>打卡提醒:</strong>快到上班时间了,别忘了打卡哦~</p>";
$title = '打卡提醒';
$style = '';
$content = '快到上班时间了,别忘了打卡哦~';
}
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid);
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'title' => $title,
'content' => [
[
'content' => $title,
'style' => $style . 'font-weight:bold',
],
[
'content' => $content,
'style' => 'padding-top:4px;opacity:0.6',
],
],
], $botUser->userid);
}
}
});

View File

@@ -6,8 +6,8 @@ use App\Models\File;
use App\Models\TaskWorker;
use App\Models\Tmp;
use App\Models\WebSocketTmpMsg;
use App\Module\Base;
use Carbon\Carbon;
use Illuminate\Support\Facades\File as SupportFile;
/**
* 删除过期临时数据任务
@@ -99,22 +99,19 @@ class DeleteTmpTask extends AbstractTask
break;
/**
* file_pack 临时压缩下载文件
* tmp_file 删除临时文件
*/
case 'file_pack':
case 'tmp_file':
{
$path = public_path('tmp/file/');
if (!SupportFile::exists($path)) {
$day = intval(env("AUTO_EMPTY_TEMP_FILE", 30));
if ($day <= 0) {
return;
}
$dirIterator = new \RecursiveDirectoryIterator($path);
$iterator = new \RecursiveIteratorIterator($dirIterator);
foreach ($iterator as $file) {
if ($file->isFile()) {
$time = $file->getMTime();
if ($time < time() - 3600 * 24) {
unlink($file->getPathname());
}
$files = Base::recursiveFiles(public_path('uploads/tmp'));
foreach ($files as $file) {
$time = @filemtime($file);
if ($time && $time < time() - 3600 * 24 * $day) {
unlink($file);
}
}
}

View File

@@ -7,164 +7,278 @@ use App\Models\User;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogMsgRead;
use App\Module\Base;
use App\Module\Doo;
use App\Module\Timer;
use Carbon\Carbon;
use Guanguans\Notify\Factory;
use Guanguans\Notify\Messages\EmailMessage;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
/**
* 未读消息邮件通知任务
* 根据设置的时间范围,将未读消息通过邮件发送给用户
*/
class EmailNoticeTask extends AbstractTask
{
/** @var array 允许发送通知的消息类型 */
private const ALLOWED_MSG_TYPES = ["text", "file", "record", "meeting"];
/** @var int 每批处理的数据量 */
private const CHUNK_SIZE = 100;
/** @var array 邮件相关设置 */
private array $emailSetting;
public function __construct()
{
parent::__construct();
$this->emailSetting = Base::setting('emailSetting');
}
public function start()
{
$setting = Base::setting('emailSetting');
// 消息通知
if ($setting['notice_msg'] === 'open') {
$userMinute = intval($setting['msg_unread_user_minute']);
$groupMinute = intval($setting['msg_unread_group_minute']);
\DB::statement("SET SQL_MODE=''");
$builder = WebSocketDialogMsg::select(['web_socket_dialog_msgs.*', 'r.id as r_id', 'r.userid as r_userid'])
->join('web_socket_dialog_msg_reads as r', 'web_socket_dialog_msgs.id', '=', 'r.msg_id')
->whereNull("r.read_at")
->where("r.silence", 0)
->where("r.email", 0);
if ($userMinute > -1) {
$builder->clone()
->where("web_socket_dialog_msgs.dialog_type", "user")
->whereIn("web_socket_dialog_msgs.type", ["text", "file", "record", "meeting"])
->whereBetween("web_socket_dialog_msgs.created_at", [
Carbon::now()->subMinutes($userMinute + 10),
Carbon::now()->subMinutes($userMinute)
])
->groupBy('r_userid')
->chunkById(100, function ($rows) {
$this->unreadMsgEmail($rows, "user");
});
}
if ($groupMinute > -1) {
$builder->clone()
->where("web_socket_dialog_msgs.dialog_type", "group")
->whereIn("web_socket_dialog_msgs.type", ["text", "file", "record", "meeting"])
->whereBetween("web_socket_dialog_msgs.created_at", [
Carbon::now()->subMinutes($groupMinute + 10),
Carbon::now()->subMinutes($groupMinute)
])
->groupBy('r_userid')
->chunkById(100, function ($rows) {
$this->unreadMsgEmail($rows, "group");
});
}
// 检查是否可以发送邮件
if (!$this->canSendEmails()) {
return;
}
\DB::statement("SET SQL_MODE=''");
// 分别处理用户消息和群组消息
$this->processMessages('user');
$this->processMessages('group');
}
/**
* 检查是否可以发送邮件通知
* 需要开启通知功能且在指定的时间范围内
*/
private function canSendEmails(): bool
{
if ($this->emailSetting['notice_msg'] !== 'open') {
return false;
}
$timeRanges = is_array($this->emailSetting['msg_unread_time_ranges'])
? $this->emailSetting['msg_unread_time_ranges']
: [];
return Timer::isTimeInRanges($timeRanges);
}
/**
* 处理指定类型的未读消息
* @param string $dialogType 对话类型user|group
*/
private function processMessages(string $dialogType): void
{
// 获取未读时间限制(分钟)
$minute = $dialogType === 'user'
? intval($this->emailSetting['msg_unread_user_minute'])
: intval($this->emailSetting['msg_unread_group_minute']);
if ($minute <= -1) {
return;
}
// 获取上次处理时间
$lastProcessKey = 'time' . ucfirst($dialogType);
$startTime = Base::settingFind('emailLastNotice', $lastProcessKey);
$startTime = $startTime ? Carbon::parse($startTime) : Carbon::today();
// 计算本次处理的结束时间(当前时间减去未读时间限制)
$endTime = Carbon::now()->subMinutes($minute);
// 如果开始时间晚于结束时间,则不处理
if ($startTime->isAfter($endTime)) {
return;
}
// 获取需要处理的用户列表
$query = WebSocketDialogMsgRead::select('web_socket_dialog_msg_reads.userid')
->join('web_socket_dialog_msgs as m', 'm.id', '=', 'web_socket_dialog_msg_reads.msg_id')
->whereNull('web_socket_dialog_msg_reads.read_at')
->where('web_socket_dialog_msg_reads.silence', 0)
->where('web_socket_dialog_msg_reads.email', 0)
->where('m.dialog_type', $dialogType)
->whereBetween('m.created_at', [$startTime, $endTime])
->whereIn('m.type', self::ALLOWED_MSG_TYPES)
->orderBy('web_socket_dialog_msg_reads.userid')
->groupBy('web_socket_dialog_msg_reads.userid');
// 分批处理用户的未读消息
$query->chunk(self::CHUNK_SIZE, function($users) use ($dialogType, $startTime, $endTime) {
foreach ($users as $userData) {
$this->sendUserEmail($userData->userid, $dialogType, $startTime, $endTime);
}
});
// 更新处理时间
Base::setting('emailLastNotice', [
$lastProcessKey => $endTime->toDateTimeString()
]);
}
/**
* 发送用户的未读消息邮件
*/
private function sendUserEmail(int $userId, string $dialogType, Carbon $startTime, Carbon $endTime): void
{
// 验证用户
$user = User::whereDisableAt(null)->find($userId);
if (!$user || $user->bot || !is_null($user->disable_at) || !Base::isEmail($user->email)) {
return;
}
// 获取未读消息
$messages = $this->getUnreadMessages($userId, $dialogType, $startTime, $endTime);
if ($messages->isEmpty()) {
return;
}
// 设置用户语言
Doo::setLanguage($user->lang);
// 按对话分组并生成邮件内容
$messagesByDialog = $messages->groupBy('dialog_id');
$emailContent = $this->generateEmailContent($user, $messagesByDialog, $dialogType);
try {
// 发送邮件
$this->sendEmail($user, $emailContent);
// 标记消息已发送邮件
WebSocketDialogMsgRead::whereIn('id', $messages->pluck('r_id'))
->update(['email' => 1]);
} catch (\Throwable $e) {
info("Email send failed for user {$userId}: " . $e->getMessage());
}
}
/**
* 获取用户的未读消息
*/
private function getUnreadMessages($userId, $dialogType, Carbon $startTime, Carbon $endTime)
{
return WebSocketDialogMsg::select([
'web_socket_dialog_msgs.*',
'r.id as r_id',
'r.userid as r_userid'
])
->join('web_socket_dialog_msg_reads as r', 'web_socket_dialog_msgs.id', '=', 'r.msg_id')
->where([
'r.userid' => $userId,
'r.silence' => 0,
'r.email' => 0,
'web_socket_dialog_msgs.dialog_type' => $dialogType
])
->whereNull('r.read_at')
->whereBetween('web_socket_dialog_msgs.created_at', [$startTime, $endTime])
->whereIn('web_socket_dialog_msgs.type', self::ALLOWED_MSG_TYPES)
->orderBy('web_socket_dialog_msgs.created_at')
->limit(self::CHUNK_SIZE)
->get();
}
/**
* 生成邮件内容
*/
private function generateEmailContent($user, $messagesByDialog, $dialogType)
{
$msgType = $dialogType === "group" ? "群聊" : "单聊";
// 生成邮件头部
$content = view('email.unread', [
'type' => 'head',
'title' => Doo::translate(sprintf('%s您好。', $user->nickname)),
'desc' => Doo::translate(sprintf('您有%d条未读%s消息请及时处理。', count($messagesByDialog), $msgType)),
])->render();
$subject = null;
// 处理每个对话的消息
foreach ($messagesByDialog as $items) {
$dialogId = 0;
$dialogName = null;
foreach ($items as $item) {
$item->cancelAppend();
$item->userInfo = User::userid2basic($item->userid, ['lang']);
Doo::setLanguage($item->userInfo->lang);
$item->preview = WebSocketDialogMsg::previewMsg($item, true);
$item->preview = str_replace('<p>', '<p style="margin:0;padding:0">', $item->preview);
if (empty($dialogId)) {
$dialogId = $item->dialog_id;
}
if ($dialogName === null) {
$dialogName = $this->getDialogName($item, $dialogType);
}
}
// 生成邮件主题
if ($subject === null) {
$subject = count($messagesByDialog) > 1
? sprintf('来自%d个%s未读消息提醒', count($messagesByDialog), $msgType)
: sprintf('来自%s未读消息提醒', $dialogName);
}
// 添加对话内容
$content .= view('email.unread', [
'type' => 'content',
'dialogUrl' => '', // 不显示回复消息按钮
// 'dialogUrl' => config("app.url") . "/manage/messenger?dialog_id={$dialogId}",
'dialogName' => trim($dialogName),
'title' => Doo::translate(sprintf('%d条未读信息', count($items))),
'button' => Doo::translate('回复消息'),
'unread' => count($items),
'items' => $items,
])->render();
}
$content = str_replace("{{RemoteURL}}", config("app.url") . "/", $content);
return [
'subject' => Doo::translate($subject),
'content' => $content
];
}
/**
* 获取对话名称
*/
private function getDialogName($message, $dialogType)
{
if ($dialogType === "user" && $message->userInfo) {
return $message->userInfo->profession
? sprintf('%s (%s) ', $message->userInfo->nickname, $message->userInfo->profession)
: $message->userInfo->nickname;
}
return $message->webSocketDialog?->getGroupName();
}
/**
* 发送邮件
*/
private function sendEmail($user, $emailData): void
{
Setting::validateAddr($user->email, function($to) use ($emailData) {
Factory::mailer()
->setDsn(sprintf(
'smtp://%s:%s@%s:%s?verify_peer=0',
$this->emailSetting['account'],
$this->emailSetting['password'],
$this->emailSetting['smtp_server'],
$this->emailSetting['port']
))
->setMessage(EmailMessage::create()
->from(sprintf('%s <%s>', Base::settingFind('system', 'system_alias', 'Task'), $this->emailSetting['account']))
->to($to)
->subject($emailData['subject'])
->html($emailData['content']))
->send();
});
}
public function end()
{
}
/**
* 未读消息通知
* @param $rows
* @param $dialogType
* @return void
*/
private function unreadMsgEmail($rows, $dialogType)
{
$array = $rows->groupBy('r_userid');
foreach ($array as $userid => $data) {
$data = WebSocketDialogMsg::select(['web_socket_dialog_msgs.*', 'r.id as r_id', 'r.userid as r_userid'])
->join('web_socket_dialog_msg_reads as r', 'web_socket_dialog_msgs.id', '=', 'r.msg_id')
->whereNull("r.read_at")
->where("r.silence", 0)
->where("r.email", 0)
->where("r.userid", $userid)
->where("web_socket_dialog_msgs.dialog_type", $dialogType)
->whereIn("web_socket_dialog_msgs.type", ["text", "file", "record", "meeting"])
->take(100)
->get();
if (empty($data)) {
continue;
}
$user = User::whereBot(0)->whereNull('disable_at')->find($userid);
if (empty($user)) {
continue;
}
if (!Base::isEmail($user->email)) {
continue;
}
$setting = Base::setting('emailSetting');
$msgType = $dialogType === "group" ? "群聊" : "成员";
$subject = null;
$content = view('email.unread', [
'type' => 'head',
'nickname' => $user->nickname,
'msgType' => $msgType,
'count' => count($data),
])->render();
$lists = $data->groupBy('dialog_id');
/** @var WebSocketDialogMsg[] $items */
foreach ($lists as $items) {
$dialogId = 0;
$dialogName = null;
foreach ($items as $item) {
$item->cancelAppend();
$item->userInfo = User::userid2basic($item->userid);
$item->preview = $item->previewMsg(true);
$item->preview = str_replace('<p>', '<p style="margin:0;padding:0">', $item->preview);
if (empty($dialogId)) {
$dialogId = $item->dialog_id;
}
if ($dialogName === null) {
if ($dialogType === "user" && $item->userInfo) {
if ($item->userInfo->profession) {
$dialogName = $item->userInfo->nickname . " ({$item->userInfo->profession})";
} else {
$dialogName = $item->userInfo->nickname;
}
} else {
$dialogName = $item->webSocketDialog?->getGroupName();
}
}
}
if ($subject === null) {
$count = count($lists);
if ($count > 1) {
$subject = "来自{$count}{$msgType}未读消息提醒";
} else {
$subject = "来自{$dialogName}未读消息提醒";
}
}
$content .= view('email.unread', [
'type' => 'content',
'dialogUrl' => config("app.url") . "/manage/messenger?dialog_id={$dialogId}",
'dialogName' => $dialogName,
'unread' => count($items),
'items' => $items,
])->render();
$content = str_replace("{{RemoteURL}}", config("app.url") . "/", $content);
}
try {
Setting::validateAddr($user->email, function($to) use ($content, $subject, $setting) {
Factory::mailer()
->setDsn("smtp://{$setting['account']}:{$setting['password']}@{$setting['smtp_server']}:{$setting['port']}?verify_peer=0")
->setMessage(EmailMessage::create()
->from(env('APP_NAME', 'Task') . " <{$setting['account']}>")
->to($to)
->subject($subject)
->html($content))
->send();
});
} catch (\Throwable $e) {
info("unreadMsgEmail: " . $e->getMessage());
}
WebSocketDialogMsgRead::whereIn('id', $data->pluck('r_id'))->update([
'email' => 1
]);
}
// 任务结束处理
}
}

View File

@@ -1,75 +0,0 @@
<?php
namespace App\Tasks;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Module\Base;
use App\Module\Ihttp;
/**
* Ihttp任务
* Class IhttpTask
* @package App\Tasks
*/
class IhttpTask extends AbstractTask
{
protected $url;
protected $post;
protected $extra;
protected $apiWebsocket;
protected $apiUserid;
protected $endPush = [];
/**
* IhttpTask constructor.
* @param $url
* @param array $post
* @param array $extra
*/
public function __construct($url, $post = [], $extra = [])
{
parent::__construct(...func_get_args());
$this->url = $url;
$this->post = $post;
$this->extra = $extra;
}
/**
* @param mixed $apiWebsocket
*/
public function setApiWebsocket($apiWebsocket): void
{
$this->apiWebsocket = $apiWebsocket;
}
/**
* @param mixed $apiUserid
*/
public function setApiUserid($apiUserid): void
{
$this->apiUserid = $apiUserid;
}
public function start()
{
$res = Ihttp::ihttp_request($this->url, $this->post, $this->extra);
if ($this->apiWebsocket && $this->apiUserid) {
$data = Base::isSuccess($res) ? Base::json2array($res['data']) : $res;
$this->endPush[] = [
'userid' => $this->apiUserid,
'msg' => [
'type' => 'apiWebsocket',
'apiWebsocket' => $this->apiWebsocket,
'apiSuccess' => Base::isSuccess($res),
'data' => $data,
]
];
}
}
public function end()
{
PushTask::push($this->endPush);
}
}

View File

@@ -6,7 +6,6 @@ use App\Models\User;
use App\Module\Base;
use App\Models\Project;
use App\Models\ProjectTask;
use App\Models\ProjectUser;
use Carbon\Carbon;
use App\Models\WebSocketDialogMsg;
use Illuminate\Support\Facades\Cache;
@@ -47,7 +46,7 @@ class UnclaimedTaskRemindTask extends AbstractTask
Project::whereNull('deleted_at')->whereNull('archived_at')->chunk(100, function ($projects) {
foreach ($projects as $project) {
//
$projectTasks = ProjectTask::select('project_tasks.id', 'project_tasks.name')
$projectTasks = ProjectTask::select(['project_tasks.id', 'project_tasks.name'])
->leftJoin('project_task_users', function ($query) {
$query->on('project_tasks.id', '=', 'project_task_users.task_id');
})
@@ -63,17 +62,15 @@ class UnclaimedTaskRemindTask extends AbstractTask
if (empty($botUser)) {
return;
}
if (!ProjectUser::whereUserid($botUser->userid)->whereProjectId($project->id)->exists()) {
$project->joinProject($botUser->userid);
$project->syncDialogUser();
}
//
$taskHtml = '<span style="line-height: 26px;">任务待领取</span> <br/>';
foreach ($projectTasks as $projectTask) {
$taskHtml .= "<span class=\"mention task\" style=\"line-height: 26px;\" data-id=\"{$projectTask->id}\">#{$projectTask->name}</span> <br/>";
}
WebSocketDialogMsg::sendMsg(null, $project->dialog_id, 'text', [
'text' => $taskHtml
WebSocketDialogMsg::sendMsg(null, $project->dialog_id, 'template', [
'type' => 'task_list',
'title' => '任务待领取',
'list' => $projectTasks->map(function ($item) {
return [
'id' => $item->id,
'name' => $item->name,
];
}),
], $botUser->userid);
}
}

View File

@@ -9,6 +9,8 @@ use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogMsgRead;
use App\Module\Base;
use App\Module\Doo;
use App\Services\RequestContext;
use Carbon\Carbon;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Request;
@@ -77,11 +79,7 @@ class WebSocketDialogMsgTask extends AbstractTask
public function start()
{
global $_A;
$_A = [
'__fill_url_remote_url' => true,
];
RequestContext::set('fill_url_remote_url', true);
//
$msg = WebSocketDialogMsg::find($this->id);
if (empty($msg)) {
@@ -191,13 +189,25 @@ class WebSocketDialogMsgTask extends AbstractTask
if ($dialog->type == 'group') {
$umengTitle = "{$dialog->getGroupName()} ($umengTitle)";
}
$this->endArray[] = new PushUmengMsg($umengUserid, [
'title' => $umengTitle,
'body' => $msg->previewMsg(),
'description' => "MID:{$msg->id}",
'seconds' => 3600,
'badge' => 1,
]);
$langs = User::select(['userid', 'lang'])
->whereIn('userid', $umengUserid)
->whereDisableAt(null)
->get()
->groupBy('lang')
->map(function($group) {
return $group->pluck('userid');
});
foreach ($langs as $lang => $uids) {
Doo::setLanguage($lang);
$umengMsg = [
'title' => $umengTitle,
'body' => WebSocketDialogMsg::previewMsg($msg),
'description' => "MID:{$msg->id}",
'seconds' => 3600,
'badge' => 1,
];
$this->endArray[] = new PushUmengMsg($uids->toArray(), $umengMsg);
}
}
}

2
bin/version.js vendored

File diff suppressed because one or more lines are too long

96
cmd
View File

@@ -168,14 +168,12 @@ run_electron() {
rm -rf "./electron/public"
fi
#
BUILD_FRONTEND="build"
if [ "$argv" == "dev" ]; then
switch_debug "$argv"
else
mkdir -p ./electron/public
cp ./electron/index.html ./electron/public/index.html
npx vite build -- fromcmd electronBuild
BUILD_FRONTEND="dev"
fi
node ./electron/build.js $argv
env BUILD_FRONTEND=$BUILD_FRONTEND node ./electron/build.js $argv
}
run_exec() {
@@ -227,6 +225,63 @@ run_mysql() {
run_exec mariadb "gunzip < /$inputname | mysql -u$username -p$password $database"
run_exec php "php artisan migrate"
judge "还原数据库"
elif [ "$1" = "open" ]; then
container_name=`docker_name mariadb`
if [ -z "$container_name" ]; then
error "没有找到 mariadb 容器!"
exit 1
fi
mkdir -p ${cur_path}/docker/mysql/tmp
cat > ${cur_path}/docker/mysql/tmp/${container_name}.conf <<EOF
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
stream {
upstream mysql {
server ${container_name}:3306 max_fails=1 fail_timeout=30s;
}
server {
listen 3306;
proxy_pass mysql;
proxy_connect_timeout 5s;
}
}
EOF
default_value="$(env_get DB_PORT_OPEN)"
if [ -n "$default_value" ]; then
read_tip="请输入代理端口 (3300-65500, 默认: ${default_value}): "
else
read_tip="请输入代理端口 (3300-65500): "
fi
read -rp "$read_tip" inputport
inputport=${inputport:-$default_value}
if [ $inputport -lt 3300 ] || [ $inputport -gt 65500 ]; then
error "端口范围不正确!"
exit 1
fi
env_set DB_PORT_OPEN $inputport
run_mysql rm-port
docker run --name ${container_name}-port \
--network dootask-networks-$(env_get APP_ID) \
-p ${inputport}:3306 \
-v ${cur_path}/docker/mysql/tmp/${container_name}.conf:/etc/nginx/nginx.conf \
-d nginx:alpine > /dev/null
judge "开启代理"
elif [ "$1" = "close" ]; then
container_name=`docker_name mariadb`
if [ -z "$container_name" ]; then
error "没有找到 mariadb 容器!"
exit 1
fi
docker stop ${container_name}-port > /dev/null
docker rm ${container_name}-port > /dev/null
judge "关闭代理"
elif [ "$1" = "rm-port" ]; then
docker rm -f $(docker_name mariadb)-port &> /dev/null
fi
}
@@ -379,13 +434,18 @@ if [ $# -gt 0 ]; then
exit 1
fi
chmod -R 775 "${cur_path}/docker/mysql/data"
sleep 3
done
run_exec php "php artisan migrate --seed"
if [ ! -f "${cur_path}/docker/mysql/data/$(env_get DB_DATABASE)/$(env_get DB_PREFIX)migrations.ibd" ]; then
error "数据库安装失败!"
exit 1
fi
# 数据库迁移
remaining=20
while [ ! -f "${cur_path}/docker/mysql/data/$(env_get DB_DATABASE)/$(env_get DB_PREFIX)migrations.ibd" ]; do
((remaining=$remaining-1))
if [ $remaining -lt 0 ]; then
error "数据库安装失败!"
exit 1
fi
sleep 3
run_exec php "php artisan migrate --seed"
done
# 设置初始化密码
res=`run_exec mariadb "sh /etc/mysql/repassword.sh"`
$COMPOSE up -d
@@ -421,6 +481,7 @@ if [ $# -gt 0 ]; then
exit 2
;;
esac
run_mysql rm-port
$COMPOSE down
env_set APP_DEBUG "false"
rm -rf "./docker/mysql/data"
@@ -505,7 +566,11 @@ if [ $# -gt 0 ]; then
e="php artisan $@" && run_exec php "$e"
elif [[ "$1" == "php" ]]; then
shift 1
e="php $@" && run_exec php "$e"
if [[ "$1" == "restart" ]] || [[ "$1" == "reboot" ]]; then
restart_php
else
e="php $@" && run_exec php "$e"
fi
elif [[ "$1" == "nginx" ]]; then
shift 1
e="nginx $@" && run_exec nginx "$e"
@@ -518,6 +583,10 @@ if [ $# -gt 0 ]; then
run_mysql backup
elif [[ "$1" == "recovery" ]] || [[ "$1" == "r" ]]; then
run_mysql recovery
elif [[ "$1" == "agent" ]] || [[ "$1" == "open" ]]; then
run_mysql open
elif [[ "$1" == "unagent" ]] || [[ "$1" == "close" ]]; then
run_mysql close
else
e="mysql $@" && run_exec mariadb "$e"
fi
@@ -545,6 +614,9 @@ if [ $# -gt 0 ]; then
$COMPOSE stop "$@"
$COMPOSE start "$@"
else
if [[ "$1" == "down" ]]; then
run_mysql rm-port
fi
$COMPOSE "$@"
fi
else

View File

@@ -26,7 +26,8 @@
"hedeqiang/umeng": "^2.1",
"laravel/framework": "^v8.83.27",
"laravel/tinker": "^v2.6.1",
"lasserafn/php-initial-avatar-generator": "^4.2",
"laravolt/avatar": "^5.1",
"league/commonmark": "^2.5",
"maatwebsite/excel": "^3.1.31",
"madnest/madzipper": "^v1.1.0",
"mews/captcha": "^3.2.6",

1114
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -134,7 +134,6 @@ return [
*/
'event_handlers' => [
'ServerStart' => \App\Events\ServerStartEvent::class,
'WorkerStart' => \App\Events\WorkerStartEvent::class,
],
@@ -222,7 +221,38 @@ return [
|
*/
'swoole_tables' => [],
'swoole_tables' => [
'initFlag' => [
'size' => 1,
'column' => [
[
'name' => 'value',
'type' => \Swoole\Table::TYPE_INT,
'size' => 8
],
],
],
'globalData' => [
'size' => 1024,
'column' => [
[
'name' => 'value',
'type' => \Swoole\Table::TYPE_STRING,
'size' => 1024
],
],
],
'onlineData' => [
'size' => 10240,
'column' => [
[
'name' => 'value',
'type' => \Swoole\Table::TYPE_INT,
'size' => 8
],
],
],
],
/*
|--------------------------------------------------------------------------

View File

@@ -26,11 +26,7 @@ class AddWebSocketDialogMsgsKey extends Migration
\App\Models\WebSocketDialogMsg::chunkById(100, function ($lists) {
/** @var \App\Models\WebSocketDialogMsg $item */
foreach ($lists as $item) {
$key = $item->generateMsgKey();
if ($key) {
$item->key = $key;
$item->save();
}
$item->generateKeyAndSave();
}
});
}

View File

@@ -23,7 +23,7 @@ class AddIndexToProjectTasksTable extends Migration
/**
* Reverse the migrations.
*
* @return voidw
* @return void
*/
public function down()
{

View File

@@ -23,7 +23,7 @@ class AddIndexToProjectTaskUsersTable extends Migration
/**
* Reverse the migrations.
*
* @return voidw
* @return void
*/
public function down()
{

View File

@@ -25,7 +25,7 @@ class AddIndexSome20231217 extends Migration
/**
* Reverse the migrations.
*
* @return voidw
* @return void
*/
public function down()
{

View File

@@ -110,7 +110,7 @@ class UpdateOwnerAddIndexSome20231217 extends Migration
/**
* Reverse the migrations.
*
* @return voidw
* @return void
*/
public function down()
{

View File

@@ -21,7 +21,7 @@ class AddIndexSome20240315 extends Migration
/**
* Reverse the migrations.
*
* @return voidw
* @return void
*/
public function down()
{

View File

@@ -22,7 +22,7 @@ class AddIndexSome20240317 extends Migration
/**
* Reverse the migrations.
*
* @return voidw
* @return void
*/
public function down()
{

View File

@@ -2,6 +2,7 @@
use App\Models\ProjectLog;
use App\Models\ProjectTaskUser;
use App\Models\User;
use App\Models\UserTransfer;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
@@ -35,7 +36,9 @@ class ProjectLogsAddTaskOnly extends Migration
UserTransfer::chunkById(100, function ($lists) {
/** @var UserTransfer $item */
foreach ($lists as $item) {
ProjectTaskUser::transfer($item->original_userid, $item->new_userid);
if (User::whereUserid($item->original_userid)->where("identity", "like", "%,disable,%")->exists()) {
ProjectTaskUser::transfer($item->original_userid, $item->new_userid);
}
}
});
});

View File

@@ -0,0 +1,37 @@
<?php
use App\Models\ProjectPermission;
use Illuminate\Database\Migrations\Migration;
class UpdateProjectPermissions extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
ProjectPermission::orderBy('id')->chunk(100, function ($rows) {
/** @var ProjectPermission $row */
foreach ($rows as $row) {
$permissions = $row->permissions;
if (!isset($permissions[ProjectPermission::TASK_TIME])) {
$permissions[ProjectPermission::TASK_TIME] = $permissions[ProjectPermission::TASK_UPDATE];
$row->permissions = \App\Module\Base::array2json($permissions);
$row->save();
}
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class RenamePreReportReceivesReceiveTime extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('report_receives', function (Blueprint $table) {
if (Schema::hasColumn('report_receives', 'receive_time')) {
$table->renameColumn('receive_time', 'receive_at');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('report_receives', function (Blueprint $table) {
//
});
}
}

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateUserCheckinFacesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('user_checkin_faces'))
return;
Schema::create('user_checkin_faces', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('userid')->nullable()->default(0)->comment('会员id');
$table->string('faceimg')->nullable()->default('')->comment('人脸图片');
$table->integer('status')->nullable()->default(0)->comment('状态');
$table->string('remark',100)->nullable()->default('')->comment('备注');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('user_checkin_faces');
}
}

View File

@@ -0,0 +1,130 @@
<?php
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogMsgRead;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddWebSocketDialogMsgReadsLive extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
info("update web_socket_dialog_msg_reads live field");
$isAdd = false;
Schema::table('web_socket_dialog_msg_reads', function (Blueprint $table) use (&$isAdd) {
if (!Schema::hasColumn('web_socket_dialog_msg_reads', 'live')) {
$isAdd = true;
$table->integer('live')->nullable()->default(0)->index()->after('dot')->comment('是否在会话里');
$table->index(['userid', 'live', 'msg_id']);
}
});
info("update web_socket_dialog_msgs deleted_at");
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
$table->index('deleted_at');
});
if ($isAdd) {
// 关键词包含
$contains = [
'您可以通过发送以下命令来控制我',
'我的机器人。',
'机器人名称:',
'已加入的会话:',
'你可以通过执行以下命令来请求我:',
'假期类型:',
'评论了此审批',
'我是你的机器人助理',
'打卡时间: ',
'匿名消息使用说明',
'匿名消息将通过',
'导出任务统计已完成',
'我不是你的机器人',
'机器人名称由',
'您没有创建机器人',
'缺卡提醒:',
'打卡提醒:',
'文件下载打包已完成',
'您有一个新任务 #',
'您协助的任务 #',
'您负责的任务 #',
];
info("update web_socket_dialog_msgs key contains");
foreach ($contains as $key) {
WebSocketDialogMsg::whereType('text')->where('key', 'like', "%{$key}%")->update(['key' => '']);
}
// 关键词开始以
$starts = [
'/hello',
'/help',
'/list',
'/info',
'/newbot',
'/setname',
'/deletebot',
'/token',
'/revoke',
'/webhook',
'/clearday',
'/dialog',
'/api',
];
info("update web_socket_dialog_msgs key starts");
foreach ($starts as $key) {
WebSocketDialogMsg::whereType('text')->where('key', 'like', "{$key}%")->update(['key' => '']);
}
// 关键词等于
$equals = [
'我要打卡',
'IT资讯',
'36氪',
'60s读世界',
'开心笑话',
'心灵鸡汤',
'使用说明',
'隐私说明',
'帮助指令',
'API接口文档',
'我的机器人',
'清空上下文',
'操作频繁!',
'暂未开启签到功能。',
'暂未开放手动签到。',
'设置成功',
'机器人不存在。',
];
info("update web_socket_dialog_msgs key equals");
foreach ($equals as $key) {
WebSocketDialogMsg::whereType('text')->whereKey($key)->update(['key' => '']);
}
// 更新是否在会话里面
info("update web_socket_dialog_msg_reads live value");
WebSocketDialog::chunk(100, function ($dialogs) {
/** @var WebSocketDialog $dialog */
foreach ($dialogs as $dialog) {
WebSocketDialogMsgRead::whereDialogId($dialog->id)->whereIn('userid', function ($query) use ($dialog) {
$query->select('userid')->from('web_socket_dialog_users')->whereDialogId($dialog->id);
})->update(['live' => 1]);
}
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@@ -0,0 +1,33 @@
<?php
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogMsgRead;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddWebSocketDialogMsgReadsLive2 extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
WebSocketDialogMsg::whereType('text')->where('key', 'like', "LongText-%")->update(['key' => '']);
WebSocketDialogMsg::whereType('file')->where('key', 'like', "image.%")->update(['key' => '']);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateWebSocketDialogMsgTranslatesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('web_socket_dialog_msg_translates'))
return;
Schema::create('web_socket_dialog_msg_translates', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('dialog_id')->nullable()->default(0)->comment('对话ID');
$table->bigInteger('msg_id')->nullable()->default(0)->comment('消息ID');
$table->string('language', 50)->nullable()->default('')->comment('语言');
$table->longText('content')->nullable()->comment('翻译内容');
$table->index(['msg_id', 'language']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('web_socket_dialog_msg_translates');
}
}

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddIndexSome20241102 extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
$table->index(['dialog_id', 'deleted_at']);
});
Schema::table('deleteds', function (Blueprint $table) {
$table->index(['type', 'userid']);
$table->index(['type', 'userid', 'created_at']);
});
Schema::table('report_receives', function (Blueprint $table) {
$table->index(['userid', 'read', 'rid']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// 回滚数据 - 无法回滚
}
}

View File

@@ -0,0 +1,36 @@
<?php
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogMsgRead;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class RemoveWebSocketDialogMsgReadsLive extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('web_socket_dialog_msg_reads', function (Blueprint $table) use (&$isAdd) {
if (Schema::hasColumn('web_socket_dialog_msg_reads', 'live')) {
$table->dropIndex(['userid', 'live', 'msg_id']);
$table->dropColumn('live');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@@ -0,0 +1,35 @@
<?php
use App\Models\User;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddUsersLang extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
if (!Schema::hasColumn('users', 'lang')) {
$table->string('lang', 20)->nullable()->default('')->after('bot')->comment('语言首选项');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn("lang");
});
}
}

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddIndexSome20241107 extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('web_socket_dialogs', function (Blueprint $table) {
$table->index('deleted_at');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// 回滚数据 - 无法回滚
}
}

View File

@@ -0,0 +1,35 @@
<?php
use App\Models\ProjectPermission;
use App\Module\Base;
use Illuminate\Database\Migrations\Migration;
class UpdateSettingEmailSettings extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$setting = Base::setting('emailSetting');
if (!isset($setting['msg_unread_time_ranges'])) {
$setting['msg_unread_time_ranges'] = [
['00:00', '09:00'],
['18:00', '23:59'],
];
Base::setting('emailSetting', $setting);
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

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