Compare commits

...

818 Commits

Author SHA1 Message Date
kuaifan
905662e963 build 2023-05-31 15:26:01 +08:00
kuaifan
ac2dfcfce3 Merge pull request #159 from hitosea/pro-doo
打开审批设置页面路由修改
2023-05-31 15:22:31 +08:00
gwokwong
fd6fef4292 Merge branch 'pro' into pro-doo
* pro:
  fix: 打开审批设置页面 -  路由修改
2023-05-31 15:04:43 +08:00
weifashi
eee7b8f0f1 fix: 打开审批设置页面 - 路由修改 2023-05-31 14:34:50 +08:00
kuaifan
ef88924f36 build 2023-05-31 09:15:38 +08:00
kuaifan
c9378b0eab Merge pull request #157 from hitosea/pro-doo
Pro doo
2023-05-30 19:59:14 +08:00
gwokwong
cde96cb8b4 Merge branch 'pro' into pro-doo
* pro:
  fix: 更改审批流数据表名
2023-05-30 19:12:15 +08:00
weifashi
ffbd9b1f26 fix: 更改审批流数据表名 2023-05-30 18:38:39 +08:00
gwokwong
59524c78e6 调整统一命名 2023-05-30 18:04:33 +08:00
weifashi
1ad0c3a3ec fix: 调整统一命名 2023-05-30 17:58:43 +08:00
gwokwong
e85d4c24d3 build编译文件上传 2023-05-26 14:31:44 +08:00
gwokwong
18280b4ff9 工作流合并 2023-05-26 14:19:36 +08:00
weifashi
e504fb845f build 2023-05-24 11:08:27 +08:00
weifashi
003186edd9 fix: 添加评论功能 - 100% 2023-05-24 11:04:39 +08:00
ganzizi
61e958d757 perf 优化全局评论 2023-05-24 11:01:10 +08:00
ganzizi
2ee349605e feat 全局评论追加 2023-05-24 10:15:08 +08:00
ganzizi
7d200d9a73 feat 全局评论追加 2023-05-24 10:10:56 +08:00
ganzizi
0468178c43 feat 新增全局评论init 2023-05-23 19:12:54 +08:00
ganzizi
6f4a94c8d1 feat 新增全局评论init 2023-05-23 19:07:37 +08:00
weifashi
e83a65ec40 fix:工作流 - 添加上传图片功能 100% 2023-05-22 18:36:25 +08:00
weifashi
53ea6b00ab build - 去除加密 - 测试那边用 2023-05-17 14:54:46 +08:00
weifashi
45865c0c18 build 2023-05-11 17:51:27 +08:00
weifashi
979d8057a4 fix:添加流程 标题改名 2023-05-11 17:48:57 +08:00
weifashi
b81f75b794 build 2023-05-11 17:46:56 +08:00
weifashi
8f7578ab06 fix:假期类型去掉外出 2023-05-11 17:44:59 +08:00
weifashi
dd39a8b63f build 2023-05-11 17:27:59 +08:00
weifashi
7595309dc8 fix:翻译 2023-05-11 17:26:09 +08:00
weifashi
93abe72041 fix: 调整样式 2023-05-11 17:21:33 +08:00
kuaifan
60c513ea3b build 2023-05-10 19:53:58 +08:00
kuaifan
385abae741 fix: iOS16.4之前版本无法进入的问题 2023-05-10 19:48:56 +08:00
kuaifan
a680538f80 build 2023-05-08 23:19:13 +08:00
kuaifan
11571c2045 perf: Windows 10 以下不支持加密 2023-05-08 23:09:59 +08:00
kuaifan
f1d267e2b2 fix: 移动端无法长按录音的问题 2023-05-08 22:57:25 +08:00
kuaifan
003b52fbc9 perf: 优化图形验证码方式 2023-05-08 22:42:11 +08:00
weifs
6e2df6fcfc build:编译资源上传 2023-04-28 10:23:19 +08:00
weifs
cd151667c1 feat:1. 新增部门负责人标识并排第一位 - 深优化
2.  clearHelper.php 文件添加判断是cmd命令运行时才执行
2023-04-28 10:14:10 +08:00
ganzizi
1a3ecbb4e9 perf 新增部门负责人标识并排第一位 2023-04-28 09:28:44 +08:00
ganzizi
0b1f433fe4 feat 新增部门负责人标识并排第一位 2023-04-27 19:19:01 +08:00
kuaifan
7aa44b8623 perf: 兼容iPad键盘 2023-04-26 20:58:56 +08:00
weifs
6e68100f86 build:编译资源上传 2023-04-25 17:35:39 +08:00
weifs
c869e88c40 fix:兼容safari 时间处理 2023-04-25 14:18:55 +08:00
kuaifan
9771e1e741 perf: 优化移动端兼容 2023-04-25 12:03:59 +08:00
weifs
9034a70b8f fix:修复了bug - 成员已加入部门,无法发起请假申请 2023-04-25 10:08:10 +08:00
weifs
e58ebebb80 feat:解决bug - 发起流程提示用户所在部门departmentId不能为空 2023-04-25 09:44:57 +08:00
weifs
edab43504e build:编译资源上传 2023-04-24 17:11:11 +08:00
kuaifan
900b11280c perf: 优化iPad Pro页面布局 2023-04-24 14:48:35 +08:00
kuaifan
0f449f151a 网页端token过期时间调整成30天 2023-04-24 14:18:13 +08:00
kuaifan
8186649a9f fix: 终端用户数提示错误 2023-04-24 13:59:52 +08:00
weifs
364ef34248 feat:更改镜像 2023-04-24 10:51:25 +08:00
ganzizi
c66714d1f5 perf 优化导出 2023-04-21 11:58:24 +08:00
ganzizi
d42fcca81f perf 优化导出 2023-04-21 11:58:24 +08:00
weifs
16d1591675 feat:工作流-翻译 2023-04-21 11:09:22 +08:00
ganzizi
d7b2f93797 perf 优化抄送信息按钮显示 2023-04-21 10:00:34 +08:00
ganzizi
9203046d97 perf 优化发给抄送人信息 2023-04-21 09:25:20 +08:00
ganzizi
b860a749f3 perf 优化发给抄送人信息 2023-04-20 18:32:49 +08:00
ganzizi
6c689fe55f perf 优化发给抄送人信息 2023-04-20 18:31:37 +08:00
weifs
c18fd1ef20 feat:工作流 - 小优化 2023-04-20 18:25:34 +08:00
ganzizi
7e56389c34 perf 优化工作流容器时间 2023-04-20 17:50:23 +08:00
weifs
28771c38e7 feat:更新容器 2023-04-20 16:38:00 +08:00
weifs
de7e74ff19 feat:工作流优化 2023-04-20 16:10:39 +08:00
ganzizi
2cf661b54f feat 导出前端页 2023-04-20 14:55:20 +08:00
weifs
16ea92c970 feat: 工作流 - 前端bug修复,节点不显示提及名称 2023-04-20 14:37:06 +08:00
weifs
3ad55cf6d5 Merge branch 'pro' of github.com:hitosea/dootask into pro
# Conflicts:
#	app/Http/Controllers/Api/WorkflowController.php
2023-04-20 14:27:13 +08:00
weifs
d0951041eb feat:工作流 - 前端100% 2023-04-20 14:24:58 +08:00
ganzizi
150d9f18b4 feat 新增工作流导出 2023-04-20 11:51:39 +08:00
weifs
09f48c32c7 feat:工作流前端进度 - 95% 2023-04-19 17:19:28 +08:00
weifs
596594d8b3 feat:工作流 - 前端进度 90% 2023-04-18 18:57:22 +08:00
ganzizi
59d17aa950 feat 新增查询条及已发起所有记录 2023-04-18 16:10:21 +08:00
weifs
836d4f8209 feat:工作流 - 前端进度 70% 2023-04-18 15:54:36 +08:00
ganzizi
c13aa8d7a4 feat 新增查询条及已发起所有记录 2023-04-18 15:00:21 +08:00
kuaifan
5fb07b7aa7 build 2023-04-18 12:02:39 +08:00
kuaifan
ee12cc21c1 no message 2023-04-18 11:51:02 +08:00
weifs
6abfc7a2e2 feat:工作流 - 前端进度 60% 2023-04-18 10:45:49 +08:00
weifs
18758421fd feat:工作流 - 前端进度 50% 2023-04-17 18:46:57 +08:00
ganzizi
644a986747 feat 新增用户头像 2023-04-17 17:43:54 +08:00
ganzizi
ad164f3539 feat 新增用户头像 2023-04-17 15:20:50 +08:00
ganzizi
d5a7ae5fa3 fix 修复了bug 2023-04-17 14:24:55 +08:00
ganzizi
a845b7b892 fix 修复了bug 2023-04-17 14:16:28 +08:00
ganzizi
a29414a477 fix 修复了bug 2023-04-17 13:48:29 +08:00
kuaifan
1c3ce47dcc build 2023-04-17 12:42:58 +08:00
kuaifan
8f30693473 no message 2023-04-17 12:37:22 +08:00
kuaifan
0f5f7c515a perf: 优化移动端部门管理 2023-04-17 12:36:59 +08:00
kuaifan
8e76493cc6 perf: 移动端显示工作报告 2023-04-17 12:26:00 +08:00
ganzizi
8617c72aab fix 修复了bug 2023-04-17 10:18:28 +08:00
weifs
7f0a150b74 feat:更新前端路由 2023-04-17 10:03:02 +08:00
HEXIANG
a53aaff620 fix:审批流程静态页 2023-04-17 09:27:49 +08:00
ganzizi
cab81c5c04 perf 工作流接口优化 2023-04-13 14:35:54 +08:00
ganzizi
5923b6169a perf 工作流接口优化 2023-04-13 09:43:02 +08:00
weifs
0f5eb829c6 feat:工作流 - 前端设置页面 - 30% 2023-04-13 09:38:50 +08:00
ganzizi
d87c06de9a feat 初步融合工作流 2023-04-12 18:25:11 +08:00
ganzizi
76e8870738 feat 初步融合工作流 2023-04-12 18:22:25 +08:00
kuaifan
247b9ee0c1 下班静默推送 2023-04-12 18:17:11 +08:00
kuaifan
80dd76b078 ws仅加密对话消息 2023-04-12 17:51:45 +08:00
ganzizi
9163cee90e feat 初步融合工作流 2023-04-12 16:58:27 +08:00
weifs
fe46d63031 feat:初步融合工作流 2023-04-12 16:55:00 +08:00
HEXIANG
d14d5a5942 fix:审批流程静态页 2023-04-12 16:41:11 +08:00
ganzizi
7181652f01 feat 初步融合工作流 2023-04-12 16:25:26 +08:00
kuaifan
4024cfb7c7 no message 2023-04-10 12:36:41 +08:00
kuaifan
1cf5a24c26 perf: 导出数据支持搜索离职会员 2023-04-10 10:48:45 +08:00
kuaifan
06795ca32c build 2023-04-07 17:37:32 +08:00
kuaifan
c4fd5ee60a perf: 优化md消息图片预览 2023-04-07 17:02:29 +08:00
kuaifan
e30b66e5c1 no message 2023-04-07 15:22:35 +08:00
kuaifan
9ee8d9b828 build 2023-04-06 13:03:25 +08:00
kuaifan
6107bde666 license 提醒 2023-04-06 12:53:33 +08:00
kuaifan
e69a91feb3 fix: 修改个人头像缓存不更新的情况 2023-04-06 11:15:42 +08:00
kuaifan
174df30978 更新自启动说明 2023-04-06 10:56:49 +08:00
kuaifan
8ffac1376a perf: 对webp文件的支持 2023-04-06 10:34:23 +08:00
kuaifan
433a46bba9 fix: 无法查看已归档任务 2023-04-06 09:31:08 +08:00
kuaifan
ba3d5299a0 build 2023-04-04 21:18:50 +08:00
kuaifan
0aa6911159 perf: 右键或长按消息发送按钮可选无声发送、Markdown格式发送 2023-04-04 21:14:58 +08:00
kuaifan
1cef313689 perf: 系统设置新增图片优化、是否保存网络图片功能 2023-04-04 11:46:44 +08:00
kuaifan
4a198ccd8f no message 2023-04-03 23:49:52 +08:00
kuaifan
11e08fea73 build 2023-04-03 22:54:19 +08:00
kuaifan
a5be461021 perf: 消息api支持markdown 2023-04-03 22:47:23 +08:00
kuaifan
d2e04843a4 no message 2023-04-03 12:57:32 +08:00
kuaifan
d0276e63c6 build 2023-04-03 11:50:09 +08:00
kuaifan
2d4fa5735b no message 2023-04-03 11:32:02 +08:00
kuaifan
1c367eb687 perf: 优化图片消息 2023-04-03 11:01:38 +08:00
kuaifan
56cf534340 build 2023-04-02 23:15:02 +08:00
kuaifan
01fbc8b39d no message 2023-04-02 23:01:51 +08:00
kuaifan
91f3ea7c46 fix: 会议回音 2023-04-02 22:43:18 +08:00
kuaifan
d11a89d02c perf: 优化动态加载静态资源 2023-04-02 22:41:49 +08:00
kuaifan
b580a9f7ad no message 2023-04-02 11:18:09 +08:00
kuaifan
c2db186620 perf: 消息接口支持MARKDOWN 2023-04-02 10:53:21 +08:00
kuaifan
679a0002a7 no message 2023-04-01 23:45:21 +08:00
kuaifan
81fdcce40f no message 2023-04-01 23:09:13 +08:00
kuaifan
8af540f9b2 build 2023-04-01 21:33:14 +08:00
kuaifan
79898d87c0 fix: 文件搜索不到根目录的共享 2023-04-01 16:54:13 +08:00
kuaifan
3c400af559 perf: 优化会话搜索 2023-04-01 16:16:24 +08:00
kuaifan
c8375b846a build 2023-04-01 15:29:37 +08:00
kuaifan
765f74b619 no message 2023-04-01 15:25:11 +08:00
kuaifan
2fcb7fb59b perf: 通过页面修改机器人资料 2023-04-01 15:23:07 +08:00
kuaifan
d828ed83a5 no message 2023-04-01 13:03:16 +08:00
kuaifan
22993283f0 fix: 清除缓存导致获取不到数据的问题 2023-04-01 12:39:03 +08:00
kuaifan
69f0d3b2bc build 2023-03-31 20:31:02 +08:00
kuaifan
ab0a5898cb perf: WebSocket 数据传输加密 2023-03-31 20:24:23 +08:00
kuaifan
60a41cae41 Merge branch 'e2ee' into pro 2023-03-30 16:08:15 +08:00
kuaifan
6c7f7f9543 build 2023-03-30 15:56:17 +08:00
kuaifan
d571d6e121 no message 2023-03-30 15:45:44 +08:00
kuaifan
b7b933c89d fix: 无法查看已归档任务 2023-03-30 15:45:44 +08:00
kuaifan
7e98a78333 feat: 实现非对称加密关键接口 2023-03-30 15:44:51 +08:00
kuaifan
342b9d6fc6 upgrade php container 2023-03-28 00:11:48 +08:00
kuaifan
ef7ed58a22 no message 2023-03-25 23:41:48 +08:00
kuaifan
160c736b38 perf: 自动清空文件回收站 2023-03-25 23:25:50 +08:00
kuaifan
01bb72523e build 2023-03-25 20:39:52 +08:00
kuaifan
ec62d7be5a perf: 优化任务接口数据逻辑 2023-03-25 20:23:25 +08:00
kuaifan
f8cbd31f61 no message 2023-03-25 12:25:22 +08:00
kuaifan
8c04a432a8 no message 2023-03-24 11:04:02 +08:00
kuaifan
3a9001e091 Upgrade Professional Edition 2023-03-24 09:08:53 +08:00
kuaifan
a172909ddf build 2022-06-20 18:41:00 +08:00
kuaifan
54d695f851 perf: 指定mariadb:10.7.3解决部分出现初始化数据库失败的情况 2022-06-20 18:40:09 +08:00
kuaifan
0147e7f1e1 perf: 上传限制改为1G 2022-06-20 18:39:29 +08:00
kuaifan
86886ded16 no message 2022-04-08 08:05:52 +08:00
kuaifan
6c44abded9 build 2022-04-08 00:29:12 +08:00
kuaifan
f3fb777924 perf: 搜索后支持快速取消筛选 2022-04-07 23:23:04 +08:00
kuaifan
1323bba420 fix: 工作包括编辑内容不正确的问题 2022-04-07 22:51:43 +08:00
kuaifan
e55a1d8713 fix: 会员选择框偶尔出现默认值错误的情况 2022-04-07 22:51:12 +08:00
kuaifan
d951a6057d perf: 优化工作汇报的搜索 2022-04-07 19:45:03 +08:00
kuaifan
9755c59687 perf: 新增邮件发送测试 2022-04-07 14:27:54 +08:00
kuaifan
2918c55fa9 build 2022-04-07 11:15:32 +08:00
kuaifan
f01f5d7837 win版本更新提示文案不是重启,是立即安装 2022-04-07 10:19:40 +08:00
kuaifan
01313b16e1 perf: 优化用户邮箱验证 2022-04-07 09:05:32 +08:00
kuaifan
601d037201 perf: 优化任务到期前后邮件提醒 2022-04-07 08:29:49 +08:00
kuaifan
9bc56f5d17 no message 2022-04-07 07:44:56 +08:00
kuaifan
8237cd21ed no message 2022-04-02 22:45:58 +08:00
kuaifan
bb3f1f2c10 no message 2022-04-02 22:45:10 +08:00
kuaifan
f8463823d9 perf: 消息已完成图标布局优化 2022-04-02 08:45:04 +08:00
kuaifan
08c46fd66e fix: 部分客户端登录页面报错的问题 2022-04-02 08:44:28 +08:00
kuaifan
1939d42d09 fix: 退出登录仍出现未读数的情况 2022-04-02 08:43:55 +08:00
kuaifan
9a0a04ed76 perf: 优化websocket连接机制 2022-04-02 08:19:13 +08:00
kuaifan
813a49bf67 no message 2022-04-01 16:22:13 +08:00
kuaifan
77924ff248 no message 2022-04-01 15:22:48 +08:00
kuaifan
9dad51fa0b no message 2022-04-01 14:43:22 +08:00
kuaifan
8c39a81644 no message 2022-04-01 13:10:59 +08:00
kuaifan
7749fac683 优化更新提示 2022-04-01 12:27:12 +08:00
kuaifan
91f4b2fd9d no message 2022-04-01 11:40:35 +08:00
kuaifan
e0a3259765 no message 2022-04-01 10:13:41 +08:00
kuaifan
38c1a768fc feat: 客户端新增系统托盘图标 2022-04-01 09:18:48 +08:00
kuaifan
3c71af064c no message 2022-04-01 07:14:57 +08:00
kuaifan
7841f54db8 防止多开 2022-04-01 07:14:45 +08:00
kuaifan
936dee9da5 perf: 文件权限提示点击确定返回主目录 2022-04-01 06:26:52 +08:00
kuaifan
a10d5ee43b no message 2022-04-01 06:17:42 +08:00
kuaifan
a0b8529606 no message 2022-04-01 05:53:16 +08:00
kuaifan
6bf1eb5bde 优化暗黑样式 2022-03-31 23:08:31 +08:00
kuaifan
fb1b5969f5 build 2022-03-31 22:42:58 +08:00
kuaifan
8eca06fd5f perf: 优化聊天窗口显示头像 2022-03-31 22:26:47 +08:00
kuaifan
3c8642f30f perf: 优化聊天窗口滚动 2022-03-31 21:48:23 +08:00
kuaifan
3796a3dbde perf: 取消任务群聊发送图片同步到任务附件 2022-03-31 20:43:55 +08:00
kuaifan
637104643c 优化样式 2022-03-31 20:23:53 +08:00
kuaifan
9364cfe1cf no message 2022-03-31 19:13:56 +08:00
kuaifan
4ae66ee3aa no message 2022-03-31 19:11:30 +08:00
kuaifan
a6534518f8 perf: 优化项目页面聊天窗口 2022-03-31 19:10:03 +08:00
kuaifan
e327311477 perf: 优化网络重连聊天机制 2022-03-31 19:09:33 +08:00
kuaifan
78c766c52e fix: 已完成任务还可以拖动排序的问题 2022-03-31 18:47:24 +08:00
kuaifan
3c89ecb2c7 fix: 移动端不显示甘特图的问题 2022-03-31 17:58:28 +08:00
kuaifan
948972184e no message 2022-03-31 15:17:48 +08:00
kuaifan
8874a3fec7 no message 2022-03-31 15:15:23 +08:00
kuaifan
dde28136e1 perf: 文件查看图片直接弹窗浏览 2022-03-31 15:12:45 +08:00
kuaifan
ce03296078 整理下载文件跳转 2022-03-31 13:55:57 +08:00
kuaifan
457efa1c79 整理下载文件跳转 2022-03-31 13:53:15 +08:00
kuaifan
cfd1fc275b no message 2022-03-31 09:59:49 +08:00
kuaifan
9d56dd122f no message 2022-03-31 09:57:03 +08:00
kuaifan
b1e35e6824 build 2022-03-31 09:46:38 +08:00
kuaifan
775fdec0b8 perf: 文件共享成员支持分享链接 2022-03-31 09:44:52 +08:00
kuaifan
f403014ef6 no message 2022-03-31 09:42:41 +08:00
kuaifan
5982424864 no message 2022-03-30 18:01:00 +08:00
kuaifan
2ad5ccabec no msg 2022-03-30 17:53:55 +08:00
kuaifan
9ffc3520e7 优化下载提示 2022-03-30 17:29:12 +08:00
kuaifan
7ba28a9770 build 2022-03-30 08:28:12 +08:00
kuaifan
bec72dfd5f no msg 2022-03-30 08:20:41 +08:00
kuaifan
cf6b4f5432 perf: 优化提示此文件夹内已有共享文件夹 2022-03-30 08:20:24 +08:00
kuaifan
fe82c44687 perf: 支持上传golang文件 2022-03-30 07:40:13 +08:00
kuaifan
49e4f15bd1 perf: 文件新增pids(上级ID递归)字段 2022-03-30 07:35:25 +08:00
kuaifan
7dd85b2ba6 recovery appId 2022-03-30 06:25:20 +08:00
kuaifan
7b64793449 no message 2022-03-30 00:14:03 +08:00
kuaifan
5485f2013e no message 2022-03-29 22:41:48 +08:00
kuaifan
6ed24ec310 no message 2022-03-29 21:00:50 +08:00
kuaifan
e39335864d no message 2022-03-29 19:16:27 +08:00
kuaifan
27ec2c276a no message 2022-03-29 18:42:54 +08:00
kuaifan
b2f8da500b no message 2022-03-29 14:48:36 +08:00
kuaifan
80a21dbf4a no message 2022-03-25 10:48:31 +08:00
kuaifan
33bd39859d no msg 2022-03-25 10:47:15 +08:00
kuaifan
7367a769b1 fix: 设置分页10条每页无效的问题 2022-03-25 10:04:10 +08:00
kuaifan
1ca4c67f7e vue-kityminder-gg 更换 vue-kityminder-ggg 2022-03-25 10:03:28 +08:00
kuaifan
120a67159d no message 2022-03-24 18:32:36 +08:00
kuaifan
aa808587df no message 2022-03-24 11:56:45 +08:00
kuaifan
57df991dd3 优化 goForward 2022-03-24 11:55:54 +08:00
kuaifan
665184bfa2 fix: 客户端打开不自动登录的问题 2022-03-24 11:55:43 +08:00
kuaifan
ceee696443 no message 2022-03-24 11:48:58 +08:00
kuaifan
7fda376f19 perf: 优化一些前端 2022-03-24 11:48:04 +08:00
kuaifan
bd60cb3a18 build 2022-03-23 15:28:49 +08:00
kuaifan
534ed6ae6c no message 2022-03-23 15:27:37 +08:00
kuaifan
acac93bbd1 fix: 修复新增项目成员无法通过邮箱搜索的问题 2022-03-23 15:09:57 +08:00
kuaifan
070e4b8527 build 2022-03-23 12:00:25 +08:00
kuaifan
a4e8761add 客户端不需要显示首页 2022-03-23 11:59:19 +08:00
kuaifan
590b76a884 perf: 优化工作量配色 2022-03-23 11:52:03 +08:00
kuaifan
98729cfa7b fix: Public客户端打开空白的情况 2022-03-23 10:40:02 +08:00
kuaifan
2bd077136f perf: 优化路由重复提示的报错 2022-03-23 10:21:17 +08:00
kuaifan
6fba94594b fix: 所有可搜索列表在非第1页搜索时不返回第1页的问题 2022-03-23 10:12:37 +08:00
kuaifan
24e9ff4c86 fix: 查看已发送的工作汇报,汇报对象需横向显示 2022-03-23 10:11:31 +08:00
kuaifan
379357ee8e no message 2022-03-18 10:59:44 +08:00
kuaifan
ef7a64644b no msg 2022-03-18 10:06:11 +08:00
kuaifan
c163c20c3d no message 2022-03-17 23:06:45 +08:00
kuaifan
3ddbeac419 build 2022-03-17 22:06:59 +08:00
kuaifan
ef7a2ec123 no msg 2022-03-17 22:00:44 +08:00
kuaifan
8dd4cfa6b2 优化团队管理搜索功能 2022-03-17 22:00:36 +08:00
kuaifan
7491a6faac 优化字段 2022-03-17 21:41:17 +08:00
kuaifan
eaf1a59e89 优化预览图片 2022-03-17 21:15:53 +08:00
kuaifan
4cbc5040a2 perf: 任务详细描述右键新增预览图片 2022-03-17 21:15:18 +08:00
kuaifan
b90b129d98 no msg 2022-03-17 17:52:07 +08:00
kuaifan
64c0ae2734 优化首页 2022-03-17 17:51:56 +08:00
kuaifan
1c2a7a219a 优化路径命名 2022-03-17 14:03:34 +08:00
kuaifan
868b8e7206 perf: 优化图片预览,优化与弹窗esc按键冲突的问题 2022-03-17 10:53:30 +08:00
kuaifan
6115828dfe build 2022-03-16 17:18:19 +08:00
kuaifan
af9b5ab014 perf: 图片预览使用当前页组件,支持多图 2022-03-16 17:15:39 +08:00
kuaifan
5497bd97b2 优化甘特图 2022-03-16 16:13:01 +08:00
kuaifan
c53db63cc7 perf: 优化工作报告前端 2022-03-16 15:46:47 +08:00
kuaifan
1263603c7b 优化客户端工作报告 2022-03-16 10:24:36 +08:00
kuaifan
d1036c3be7 合并整理 2022-03-16 09:04:00 +08:00
kuaifan
5adbd6e8f1 perf: 优化甘特图 2022-03-16 07:21:21 +08:00
kuaifan
b83ce02849 perf: 优化任务列表切换显示 2022-03-15 17:38:02 +08:00
韦荣超
735a9c8a5d build: bf46881e3b 2022-03-15 16:46:32 +08:00
韦荣超
bf46881e3b fix: 项目-任务列表tab优化 2022-03-15 16:44:35 +08:00
韦荣超
f68226f4b4 fix: 项目-任务列表tab修改,甘特图数据优化 2022-03-15 15:46:45 +08:00
韦荣超
07d864e48b build: 0326864510 2022-03-15 10:45:07 +08:00
韦荣超
0326864510 fix: 项目--任务列表删除主任务后,子任务仍显示问题 2022-03-15 10:42:40 +08:00
韦荣超
2a97060b96 build: 6cffd81540 2022-03-14 18:19:29 +08:00
韦荣超
6cffd81540 fix: 项目任务甘特图图标及优化移动逻辑 2022-03-14 18:17:23 +08:00
Xuronglong
eb2b11ed3e perf: 更新icon图标库
perf: 更新icon图标库
2022-03-14 17:29:06 +08:00
Xuronglong
7cb52bebf5 perf: 更新icon图标库
perf: 更新icon图标库
2022-03-14 17:16:47 +08:00
韦荣超
3731f5d32c build: 7161b14631 2022-03-14 14:43:22 +08:00
韦荣超
7161b14631 fix: 项目任务列表甘特图鼠标悬停图标或文案时样式变为手指 2022-03-14 14:41:04 +08:00
韦荣超
2e7ff88be6 fix: 项目任务列表甘特图去掉'已完成'任务及样式优化 2022-03-14 14:34:29 +08:00
韦荣超
6368ee5c12 build: 42106e907d 2022-03-14 12:16:46 +08:00
韦荣超
42106e907d fix: 项目--删除任务详情删除前有聊天记录显示异常修改 2022-03-14 12:14:49 +08:00
韦荣超
3f71162e9d build: 008d8de24e 2022-03-14 09:36:01 +08:00
韦荣超
008d8de24e fix: 修改项目任务列表甘特图时长及颜色显示不对问题 2022-03-14 09:34:30 +08:00
韦荣超
e3000dcc7c build: 15ecd8d592 2022-03-11 19:13:16 +08:00
韦荣超
f98cf4cbfc Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	public/css/app.css
#	public/js/app.js
#	public/js/build/341.js.LICENSE.txt
#	public/js/build/712.js.LICENSE.txt
#	public/js/build/904.js.LICENSE.txt
2022-03-11 19:11:27 +08:00
韦荣超
807f885f92 build: 15ecd8d592 2022-03-11 19:06:27 +08:00
韦荣超
15ecd8d592 feat: 项目任务新增一个甘特图展示选项 2022-03-11 19:04:51 +08:00
mmppkka
588a6f1bf4 build:f633bc81cf874c71f45c7d6d0c3b10aa59316087 2022-03-11 18:34:19 +08:00
mmppkka
f633bc81cf fix:官网首页修改 2022-03-11 18:31:49 +08:00
韦荣超
3006dc6584 build: a7c63e22e5 2022-03-11 18:23:58 +08:00
韦荣超
a7c63e22e5 perf: 已删除任务详情任务描述改为只读 2022-03-11 18:21:52 +08:00
韦荣超
4304158088 build: c1fb67f143 2022-03-11 17:48:32 +08:00
韦荣超
0f446a1f77 Merge remote-tracking branch 'origin/develop' into develop 2022-03-11 17:44:22 +08:00
mmppkka
71639a91b9 build:b0395a6ed22e87064e3e4458b9573e8acaf5679d 2022-03-11 17:44:08 +08:00
韦荣超
c1fb67f143 perf: 已删除任务操作文案及显示优化 2022-03-11 17:42:47 +08:00
mmppkka
b0395a6ed2 fix:修改图片 2022-03-11 17:40:08 +08:00
mmppkka
7d0e6ac50b build:c6ea52482133f98312f6e82bbc624088e3096e03 2022-03-11 17:36:33 +08:00
mmppkka
c6ea524821 fix:首页重写 2022-03-11 17:34:53 +08:00
韦荣超
7c82769448 build: a4b69c3911 2022-03-11 12:08:12 +08:00
韦荣超
a4b69c3911 feat: 客户端登录,新增工作报告、修改工作报告、查看工作报告,全部直接在新窗口打开 2022-03-11 12:06:36 +08:00
韦荣超
e301abe9e3 build: d38ccba618 2022-03-11 10:00:49 +08:00
韦荣超
d38ccba618 perf: 项目--删除任务查看详情页功能 2022-03-11 09:59:29 +08:00
韦荣超
849ac56296 build: 67bb821a0e 2022-03-10 18:59:48 +08:00
韦荣超
67bb821a0e perf: 首页兼容暗黑模式及文案和查询优化 2022-03-10 18:57:53 +08:00
kuaifan
43c51d48d9 合并优化 2022-03-10 17:45:17 +08:00
kuaifan
48b7f4924e perf: 优化消息标记已读/未读 2022-03-10 14:41:29 +08:00
kuaifan
30c149be31 Merge branch 'develop' of ssh://git.gezi.vip:6006/gx/dootask
# Conflicts:
#	electron/package.json
#	package.json
#	public/css/app.css
#	public/js/app.js
#	public/js/build/189.js.LICENSE.txt
#	public/js/build/206.js
#	public/js/build/400.js.LICENSE.txt
#	public/js/build/486.js
#	public/js/build/496.js.LICENSE.txt
#	public/js/build/528.js
#	public/js/build/535.js
#	public/js/build/535.js.LICENSE.txt
#	public/js/build/578.js
#	public/js/build/78.js.LICENSE.txt
#	public/js/build/817.js
#	public/js/build/856.js
#	public/js/build/862.js
#	public/js/build/889.js
#	public/js/build/91.js.LICENSE.txt
#	public/js/build/954.js
#	public/js/build/956.js
#	resources/assets/js/pages/manage/components/TaskDetail.vue
#	resources/assets/js/pages/manage/dashboard.vue
#	resources/assets/sass/pages/common.scss
#	resources/assets/sass/pages/page-dashboard.scss
2022-03-10 11:27:00 +08:00
kuaifan
42d9271ea0 build 2022-03-09 21:26:05 +08:00
韦荣超
00e666e423 build: 6b29b686c7 2022-03-09 18:05:47 +08:00
韦荣超
6b29b686c7 feat: 在项目设置里新增一个“已删除任务”菜单 2022-03-09 18:02:49 +08:00
韦荣超
db99a21514 perf: 修改验证邮件有效期改回24小时 2022-03-09 15:04:04 +08:00
韦荣超
6fb1c474d2 build: f1c7c35e48 2022-03-09 12:07:04 +08:00
韦荣超
f1c7c35e48 perf: 优化邮件设置页面样式 2022-03-09 12:04:24 +08:00
韦荣超
923549bb5e perf: 修改验证邮件有效期为10分钟方便测试(验证完后改为24小时) 2022-03-09 11:51:42 +08:00
韦荣超
0aa18ded72 perf: 【邮箱验证】链接过期的提示文案为:链接已失效,请重新登录/注册 2022-03-09 11:45:11 +08:00
韦荣超
ee08d8d740 perf: 任务修改计划时间需要重置任务邮件提醒日志 2022-03-09 10:45:05 +08:00
韦荣超
0c23fa7c9d build: fe91765ab0 2022-03-09 09:53:34 +08:00
韦荣超
fe91765ab0 fix: 优化验证邮箱页面文案 2022-03-09 09:52:00 +08:00
韦荣超
8541e180cc fix: 修正验证邮箱页面文案 2022-03-09 09:39:22 +08:00
kuaifan
a3d950e2a3 fix: 修复自定义SSO自动升级版本出错的问题 2022-03-09 00:42:00 +08:00
kuaifan
e1c80636ba 显示最后一个加入的成员 2022-03-08 19:32:32 +08:00
kuaifan
4fb971a935 fix: 添加任务时开始时间和结束时间为同一天可能发生报错 2022-03-08 18:42:14 +08:00
kuaifan
117d0fbcef perf: 任务附件过多时仅显示最新50个 2022-03-08 18:34:20 +08:00
kuaifan
389cafc240 优化有时候任务详情标题未显示完全 2022-03-08 18:22:10 +08:00
kuaifan
4591b59465 fix: 拖动任务列表排序后会自动还原的情况 2022-03-08 18:05:16 +08:00
kuaifan
d4a082382d perf: 消息已读/未读人员优化 2022-03-08 16:24:52 +08:00
kuaifan
885437de8d feat: 优化表格分页样式 2022-03-08 16:05:25 +08:00
kuaifan
39cd9f4a44 feat: 优化TableAction组件 2022-03-08 14:36:20 +08:00
kuaifan
01a2244fed feat: 头像加载失败时显示名称首字 2022-03-08 14:11:07 +08:00
韦荣超
e0e3dd128e perf: 消息添加'未读标记'字段 2022-03-08 13:09:12 +08:00
韦荣超
0f6408d7f6 fix: 任务第二次邮件提醒判断错误修改 2022-03-08 12:25:36 +08:00
韦荣超
b610dc4969 build: bc6fa7fd27 2022-03-08 09:08:40 +08:00
韦荣超
bc6fa7fd27 feat: 消息右键对话新增:标记已读、标记未读 2022-03-08 09:06:38 +08:00
kuaifan
0e34cc49df no message 2022-03-08 08:15:34 +08:00
kuaifan
629881d16b no message 2022-03-08 07:05:18 +08:00
kuaifan
92569f5b3a no message 2022-03-08 00:37:17 +08:00
kuaifan
4ff32511b9 perf: 添加任务默认选中自己,如果不选则添加无负责人任务 2022-03-08 00:29:10 +08:00
kuaifan
5afed4b85e perf: 优化消息未读数 2022-03-07 18:07:02 +08:00
kuaifan
2aa687a40b perf: 优化置顶后数据请求 2022-03-07 17:48:34 +08:00
kuaifan
57567ebb1b perf: 优化未读信息数 2022-03-07 17:48:17 +08:00
kuaifan
6448169caa perf: 优化首页仪表盘样式 2022-03-07 17:47:59 +08:00
韦荣超
e5901f28e3 build: 2d3f0dd95a 2022-03-07 17:04:47 +08:00
韦荣超
2d3f0dd95a fix: '最近打开任务'数据没有根据用户区分问题修改 2022-03-07 17:03:29 +08:00
kuaifan
2e4e827887 新增focus方法 2022-03-07 15:24:48 +08:00
kuaifan
22704b32d6 perf: 优化关闭任务独立窗口点击取消后没有自动获取焦点 2022-03-07 15:24:19 +08:00
韦荣超
2839e595cc build: a9f28dfb47 2022-03-07 14:40:23 +08:00
韦荣超
a9f28dfb47 perf: 邮箱验证优化流程提示 2022-03-07 14:38:40 +08:00
韦荣超
c1bdd079cf build: 467d05a358 2022-03-07 11:56:37 +08:00
韦荣超
467d05a358 fix: 去掉调试信息 2022-03-07 11:54:30 +08:00
韦荣超
05c8a6e664 build: 59b2ecebda 2022-03-07 11:51:21 +08:00
韦荣超
59b2ecebda feat: 添加“最近打开的任务” 2022-03-07 11:49:29 +08:00
韦荣超
fe8cf38a7f perf: 任务提醒缩短邮件通知时间区间 2022-03-07 10:22:14 +08:00
kuaifan
e754ceace8 build 2022-03-07 01:42:30 +08:00
kuaifan
41ceb9bd82 perf: 项目、消息对话置顶后要滚动到可以看到它的位置 2022-03-07 01:35:12 +08:00
kuaifan
b0b7c63561 perf: 再次点击滚动到未读条目 2022-03-07 01:19:36 +08:00
kuaifan
c13f65611f perf: 优化新窗口打开的任务保存机制 2022-03-07 01:15:10 +08:00
kuaifan
b9feeabfb3 perf: 优化新消息等列表滚动 2022-03-07 00:35:00 +08:00
kuaifan
8e9ba7a2d2 no msg 2022-03-06 16:10:47 +08:00
kuaifan
b3e9b1c2be perf: 完成任务时任务暂时继续显示,直到路由发生改变 2022-03-06 13:57:56 +08:00
kuaifan
e969b5b7e4 perf: 优化仪表盘使用sticky方式 2022-03-06 13:56:18 +08:00
kuaifan
8e0a684a9a build 2022-03-04 19:45:10 +08:00
kuaifan
152aa0f92b fix: 清空子任务的时间报错闪现的问题 2022-03-04 19:43:54 +08:00
韦荣超
97fb0ba95a perf: 邮箱发送失败提示优化 2022-03-04 18:39:30 +08:00
韦荣超
01355b9a28 去掉查看符合发邮件数据(测试) 2022-03-04 17:55:24 +08:00
韦荣超
772922cefb 查看符合发邮件数据(测试) 2022-03-04 17:51:34 +08:00
kuaifan
95a3aa7290 no message 2022-03-04 17:40:15 +08:00
韦荣超
6f8a4af5eb fix: 任务提醒时间格式错误修改 2022-03-04 17:27:35 +08:00
韦荣超
b332c4e23d perf: 任务提醒时间区间放大,防止定时器出现意外发送不到问题 2022-03-04 16:57:13 +08:00
韦荣超
47c0c3fa3f fix: 任务统计导出完成时间为空时,不应出现实际完成用时 2022-03-04 16:38:08 +08:00
kuaifan
71cb8612d8 fix: 客户端任务独立窗口修改详情后没有同步到主窗口的问题 2022-03-04 16:35:59 +08:00
kuaifan
af12aecd36 perf: 网络不好连续按回车导致重复添加子任务 2022-03-04 16:18:06 +08:00
kuaifan
fec116f131 fix: 客户端任务独立窗口无法按command+s保存任务的问题 2022-03-04 16:17:18 +08:00
kuaifan
d1fdec0970 no message 2022-03-04 16:06:22 +08:00
kuaifan
77b6c53a42 perf: 优化任务详情数据结构 2022-03-04 15:50:38 +08:00
韦荣超
f2eaac50a6 fix: 任务提醒时间区间错误修改 2022-03-04 14:38:20 +08:00
韦荣超
ec8b0ada14 fix: 任务列表缺少参数报错修改 2022-03-04 14:27:45 +08:00
韦荣超
1e28be9671 build: 2975d550eb 2022-03-04 14:02:04 +08:00
韦荣超
8a0e813734 build: 2975d550eb 2022-03-04 14:01:55 +08:00
韦荣超
2975d550eb fix: 导出任务统计修改 2022-03-04 14:00:38 +08:00
韦荣超
248a9c4070 perf: 补全系统设置中的未翻译地方 2022-03-04 10:25:06 +08:00
韦荣超
327d0d20a9 fix: 登录账号密码错误时提示修改 2022-03-04 09:59:55 +08:00
韦荣超
1ffd2812dc perf: 修改导出报表文案 2022-03-04 09:57:54 +08:00
韦荣超
29da4d209d build: 35a369a953 2022-03-04 09:43:41 +08:00
韦荣超
35a369a953 feat: 任务到期提醒开启邮件通知 2022-03-04 09:41:36 +08:00
kuaifan
4785cae8f0 Merge branch 'master' into develop
# Conflicts:
#	electron/package.json
#	package.json
#	public/js/app.js
#	public/js/build/221.js
#	public/js/build/501.js
#	public/js/build/747.js
#	public/js/build/862.js
#	public/js/build/984.js
2022-03-04 08:59:28 +08:00
kuaifan
9eaa575d1a build 2022-03-04 08:53:42 +08:00
kuaifan
996ed78a0e perf: 优化仪表盘角标数 2022-03-04 08:51:22 +08:00
kuaifan
1759572b2e perf: 优化客户端任务详情按command+s保存 2022-03-04 08:50:22 +08:00
kuaifan
0a9f9eea90 build 2022-03-04 00:00:26 +08:00
kuaifan
223fb540b1 perf: 优化文件重命名,支持按esc取消编辑 2022-03-03 23:57:58 +08:00
kuaifan
c5f1c95f7b perf: 任务详情打开操作菜单时按esc任务窗口隐藏了但是菜单还看见 2022-03-03 23:06:54 +08:00
kuaifan
60086ead7c Merge branch 'master' of github.com:kuaifan/dootask into develop
# Conflicts:
#	electron/package.json
#	package.json
#	public/css/app.css
#	public/js/app.js
#	public/js/build/153.js
#	public/js/build/189.js
#	public/js/build/189.js.LICENSE.txt
#	public/js/build/206.js
#	public/js/build/486.js
#	public/js/build/501.js
#	public/js/build/528.js
#	public/js/build/544.js
#	public/js/build/601.js
#	public/js/build/747.js
#	public/js/build/756.js.LICENSE.txt
#	public/js/build/762.js
#	public/js/build/836.js
#	public/js/build/889.js
#	public/js/build/954.js
#	public/js/build/956.js.LICENSE.txt
2022-03-03 16:32:39 +08:00
kuaifan
3bbcbca926 build 2022-03-03 16:21:08 +08:00
kuaifan
63c1deb630 fix: md编辑器出现toc混乱的情况 2022-03-03 16:17:49 +08:00
kuaifan
424e2428fe pref: 上传文件名称过程显示错位的问题 2022-03-03 15:11:50 +08:00
kuaifan
2fdef45156 pref: 退出登录返回登录页而不是注册页 2022-03-03 15:11:34 +08:00
韦荣超
63ac99e906 perf: 报表导出列及剩余天数优化 2022-03-03 15:03:41 +08:00
kuaifan
4cd4550a36 pref: 网络异常的情况下需提示网络异常而不是系统出错 2022-03-03 14:53:01 +08:00
kuaifan
16af625aef no msg 2022-03-03 14:35:21 +08:00
kuaifan
8e72794c07 pref: 任务详情当任务倒计时结束时显示"超期未完成"标签 2022-03-03 14:35:12 +08:00
kuaifan
d9cf6d7e1b pref: 优化脚本,支持部分服务器是docker compose命令 2022-03-03 14:22:25 +08:00
韦荣超
57c4f935e2 fix: 邮箱验证模板页'你'改为'您' 2022-03-03 14:11:31 +08:00
kuaifan
f4e4252227 perf: 优化已读回执 2022-03-03 11:41:54 +08:00
韦荣超
84bbcf7753 fix: 邮箱设置异常报错修改 2022-03-03 11:20:09 +08:00
韦荣超
df129f0d20 build: a76da167eb 2022-03-03 10:49:23 +08:00
韦荣超
a76da167eb feat: 完成邮箱验证 2022-03-03 10:47:40 +08:00
韦荣超
1547b3f53b build: 68f5b30f7d 2022-03-02 18:13:29 +08:00
韦荣超
68f5b30f7d feat: 邮箱验证部分 2022-03-02 18:11:19 +08:00
韦荣超
d9b9c0f42c build: aa0597c27e 2022-03-02 15:46:23 +08:00
韦荣超
aa0597c27e perf: 补全任务'测试'状态样式 2022-03-02 15:45:05 +08:00
韦荣超
4d59cd1521 fix: 报表导出任务没有流程日志判断优化 2022-03-02 15:36:11 +08:00
韦荣超
6ba4170f08 build: 827bc97e8e 2022-03-02 15:21:36 +08:00
韦荣超
827bc97e8e perf: 导出报表调整 2022-03-02 15:19:48 +08:00
kuaifan
7107409b1b no message 2022-03-02 08:46:29 +08:00
kuaifan
6c086fab6f no message 2022-03-02 08:32:38 +08:00
kuaifan
54d215b8d1 Merge branch 'develop' of ssh://git.gezi.vip:6006/gx/dootask 2022-03-02 08:30:33 +08:00
kuaifan
4cd47a5c77 Merge branch 'master' into develop 2022-03-02 08:28:24 +08:00
kuaifan
d027d67e08 pref: 倒计时刚到到达0时会显示自定义才继续显示计时,且未显示超时标签 2022-03-02 08:25:38 +08:00
kuaifan
a7d9e635eb pref: 鼠标滑动至仪表盘中的待完成任务卡片时,卡片周围未显示光晕,且未显示为手指样式 2022-03-02 08:19:09 +08:00
kuaifan
e7196efaea pref: 优化任务详细描述显示 2022-03-02 08:17:30 +08:00
kuaifan
0a2a903c74 fix: 修复登录页设置下拉显示不全的情况 2022-03-02 07:59:53 +08:00
kuaifan
8104c26b19 处理任务简介出现"的情况 2022-03-02 07:48:29 +08:00
韦荣超
420245c630 build: 20cf83907d 2022-03-01 15:54:39 +08:00
韦荣超
20cf83907d fix: 处理回滚后异常代码 2022-03-01 15:52:46 +08:00
韦荣超
0a04d2ee83 fix: 处理回滚后异常代码 2022-03-01 15:47:36 +08:00
韦荣超
bc05781edd build: ce9d91e990 2022-03-01 15:08:02 +08:00
韦荣超
ce9d91e990 fix: 【系统设置】邮件设置提前小时数双向绑定无效问题修改 2022-03-01 15:06:39 +08:00
韦荣超
973d13b705 build: 51e4427108 2022-03-01 14:21:47 +08:00
韦荣超
51e4427108 build: 【系统设置】邮件设置任务提醒时间只能是0.5的倍数 2022-03-01 14:19:44 +08:00
韦荣超
d09ab541ab build: a9e1dc3cd5 2022-03-01 11:04:20 +08:00
韦荣超
a9e1dc3cd5 perf: 仪表盘当前激活的卡片不明显优化 2022-03-01 11:02:28 +08:00
韦荣超
1551e6c640 build: 03a1bdfc7f 2022-03-01 09:39:12 +08:00
韦荣超
03a1bdfc7f perf: 置顶样式优化 2022-03-01 09:37:25 +08:00
韦荣超
24545aa9cb build: cf7e7c1a06 2022-02-28 18:17:10 +08:00
韦荣超
c11426419c build: cf7e7c1a06 2022-02-28 18:17:01 +08:00
韦荣超
cf7e7c1a06 feat: 管理员系统设置新增:新增邮件设置 2022-02-28 18:15:17 +08:00
kuaifan
ab388f1ef5 Merge branch 'master' of github.com:kuaifan/dootask into develop
# Conflicts:
#	public/css/app.css
#	public/js/app.js
#	public/js/build/248.js
#	public/js/build/248.js.LICENSE.txt
#	public/js/build/501.js
#	public/js/build/510.js.LICENSE.txt
#	public/js/build/581.js.LICENSE.txt
#	public/js/build/601.js
#	public/js/build/712.js
#	public/js/build/747.js
#	resources/assets/js/pages/manage/file.vue
#	resources/assets/sass/pages/page-manage.scss
2022-02-28 10:26:12 +08:00
kuaifan
101d5c7eb0 build 2022-02-28 00:23:20 +08:00
kuaifan
cad253b85f feat: 导出任务功能 2022-02-28 00:21:48 +08:00
kuaifan
f17009a988 优化左上角菜单集合 2022-02-27 22:54:48 +08:00
kuaifan
1f76278d2b 优化客户端升级提示 2022-02-27 22:53:54 +08:00
kuaifan
e032d29c91 修复数据库字段填写错误 2022-02-27 21:12:11 +08:00
kuaifan
31d1b0c994 nomsg 2022-02-27 15:03:39 +08:00
kuaifan
611c6d415c perf: 记录任务工作流变化 2022-02-27 14:12:15 +08:00
kuaifan
e3b7ac00fd perf: 优化修改工作流的过程 2022-02-27 14:11:50 +08:00
kuaifan
7c952822db perf: 优化任务排序 2022-02-27 11:06:18 +08:00
kuaifan
b9e435c0e2 perf: 支持nodejs16+ 2022-02-27 10:59:38 +08:00
kuaifan
356d40e640 feat: 文件支持拖动到列表上传 2022-02-25 22:49:56 +08:00
kuaifan
123ffd4e66 no message 2022-02-25 22:47:56 +08:00
kuaifan
c952659620 fix: 修复无法预览pdf文件 2022-02-25 22:15:25 +08:00
韦荣超
ff59f31e84 build: 011fc8cc9e 2022-02-25 15:58:28 +08:00
韦荣超
011fc8cc9e perf: 首页判断是否登录优先于判断是否需要启动首页 2022-02-25 15:57:03 +08:00
韦荣超
7428022b2f build: 3815eb1983 2022-02-25 15:41:32 +08:00
韦荣超
3815eb1983 perf: 优化首页判断时间一闪而过问题 2022-02-25 15:40:17 +08:00
韦荣超
3778f10edb perf: 添加首页文字翻译 2022-02-25 15:31:26 +08:00
韦荣超
00a4b867ea build: 3a1024e848 2022-02-25 15:18:58 +08:00
韦荣超
3a1024e848 fix: 首页360浏览器图片函数摆错修改 2022-02-25 15:17:33 +08:00
kuaifan
ea8e1e9c57 优化样式 2022-02-25 15:14:20 +08:00
kuaifan
478d63893b fix: 无法浏览聊天图片的问题 2022-02-25 14:38:40 +08:00
韦荣超
3066631301 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	app/Http/Controllers/Api/SystemController.php
#	resources/assets/sass/pages/components/dialog-wrapper.scss
2022-02-25 10:57:28 +08:00
韦荣超
b0998d5f95 build: a074cb9664 2022-02-25 10:56:20 +08:00
韦荣超
a074cb9664 fix: 回滚代码后功能完善 2022-02-25 10:54:20 +08:00
韦荣超
43a3d1e5ac build: 24c8a4a9d1 2022-02-25 09:57:25 +08:00
韦荣超
24c8a4a9d1 fix: 项目列表处于置顶选中状态显示选中样式 2022-02-25 09:55:16 +08:00
kuaifan
b5ccba552f build 2022-02-25 09:12:28 +08:00
kuaifan
8d2ee364ba perf: 文件、聊天文件、任务文件预览优化(支持预览drawio、mind等) 2022-02-25 09:11:58 +08:00
kuaifan
14006068c8 优化样式 2022-02-25 00:13:15 +08:00
kuaifan
b4358ffc66 fix: 登录页重复填写sso地址无法保存的问题 2022-02-25 00:09:57 +08:00
kuaifan
33135b1df1 fix: 无法移动共享文件夹内创建的文件 2022-02-25 00:08:32 +08:00
kuaifan
9a66c38e01 fix: 客户端无法编辑office文件 2022-02-25 00:05:35 +08:00
kuaifan
81132ecab0 fix: 客户端无法下载文件 2022-02-25 00:05:12 +08:00
韦荣超
313fc09c8d build: 5610486a89 2022-02-24 18:16:15 +08:00
韦荣超
5610486a89 perf: 调整消息置顶标识位置 2022-02-24 18:14:45 +08:00
韦荣超
9f0231405a build: e650dd0503 2022-02-24 18:12:38 +08:00
韦荣超
e650dd0503 perf: 消息列表详情增加'置顶'标识 2022-02-24 18:10:50 +08:00
韦荣超
6fc6cb75e4 build: 184efc6f27 2022-02-24 17:38:00 +08:00
韦荣超
184efc6f27 fix: 合并异常代码修改 2022-02-24 17:35:55 +08:00
韦荣超
1117b57101 Merge remote-tracking branch 'origin/dev' into develop
# Conflicts:
#	app/Http/Controllers/Api/SystemController.php
#	public/js/language.all.js
#	resources/assets/js/pages/login.vue
#	resources/assets/js/pages/manage.vue
#	resources/assets/js/pages/manage/calendar.vue
#	resources/assets/js/pages/manage/components/Calendar.vue
#	resources/assets/js/pages/manage/setting/system/setting.vue
#	resources/assets/js/store/state.js
#	resources/assets/statics/public/js/language.all.js
2022-02-24 17:18:33 +08:00
韦荣超
0928ba71f2 feat: 首页启动设置 2022-02-24 17:11:05 +08:00
韦荣超
7170eded68 build: 32e891d6ba 2022-02-24 14:38:24 +08:00
韦荣超
32e891d6ba perf: 项目列表置顶优化 2022-02-24 14:37:17 +08:00
韦荣超
5a273676fb build: c3f6edf7c5 2022-02-24 14:03:29 +08:00
韦荣超
c3f6edf7c5 perf: 项目列表置顶优化 2022-02-24 14:02:12 +08:00
韦荣超
e0c0cc8613 build: fbeb3dd81e 2022-02-24 11:50:34 +08:00
韦荣超
fbeb3dd81e perf: 【文件】剪切后加'取消剪切'按钮 2022-02-24 11:48:59 +08:00
kuaifan
9d89af37be build 2022-02-24 09:04:03 +08:00
kuaifan
fbc8a36232 perf: 消息会话右键时隐藏滚动条 2022-02-24 09:03:15 +08:00
kuaifan
ea028ea1a1 perf: 页面高度足够时只滚动项目部分 2022-02-24 09:02:42 +08:00
kuaifan
35b1c12bb5 Merge branch 'develop' of ssh://git.gezi.vip:6006/gx/dootask
# Conflicts:
#	electron/package.json
#	package.json
#	public/js/app.js
2022-02-24 08:07:40 +08:00
kuaifan
fc89d96635 优化drawio不显示添加模板 2022-02-24 00:45:53 +08:00
kuaifan
27129652f2 no message 2022-02-23 23:31:59 +08:00
kuaifan
23ef992a7f 优化创建者和协助人机制 2022-02-23 23:30:33 +08:00
kuaifan
04533e17ec Merge branch 'master' of github.com:kuaifan/dootask into develop
# Conflicts:
#	app/Models/ProjectTask.php
#	public/css/app.css
#	public/js/app.js
#	public/js/build/361.js
#	public/js/build/412.js
#	public/js/build/499.js.LICENSE.txt
#	public/js/build/659.js
#	public/js/build/659.js.LICENSE.txt
#	public/js/build/726.js.LICENSE.txt
#	public/js/build/747.js
2022-02-23 23:18:50 +08:00
kuaifan
7f2fcba542 no message 2022-02-23 23:03:35 +08:00
kuaifan
c115e2f985 build 2022-02-23 22:57:33 +08:00
kuaifan
88642c2003 perf: 客户端版本更新提示关闭 2022-02-23 22:53:04 +08:00
kuaifan
2c678b5363 no message 2022-02-23 22:52:36 +08:00
kuaifan
bc3b72fafe no message 2022-02-23 20:58:04 +08:00
韦荣超
df3b8cf09c perf: 除了任务状态,任务创建人和协助人权限与负责人的保持一致 2022-02-23 17:25:54 +08:00
kuaifan
65393b7809 优化客户端升级提示 2022-02-23 16:45:38 +08:00
韦荣超
9782c849ad build: e7efaed08a 2022-02-23 16:27:57 +08:00
韦荣超
e7efaed08a perf: 议仪表盘添加'待完成任务'选项 2022-02-23 16:26:07 +08:00
kuaifan
337b3e5b5d 优化终端命令 2022-02-23 16:03:54 +08:00
kuaifan
7b1b7d1372 perf: 创建者及协助人可以修改任务但不能修改任务状态 2022-02-23 16:00:54 +08:00
韦荣超
e5b838a2b3 build: f9cc2ceb11 2022-02-23 15:55:41 +08:00
韦荣超
f9cc2ceb11 perf: 优化点击右键时选中框缺少右侧线条 2022-02-23 15:51:24 +08:00
kuaifan
4f107c5618 perf: 客户端修改文件未保存关闭窗口前提示 2022-02-23 15:16:36 +08:00
韦荣超
b1c5aaff43 build: d0a4473e2b 2022-02-23 15:04:21 +08:00
韦荣超
d0a4473e2b fix: 项目、消息置顶样式修改 2022-02-23 15:02:05 +08:00
kuaifan
1c79361094 fix: 修复客户端任务新窗口无法修改任务等级 2022-02-23 14:53:15 +08:00
kuaifan
f72114c223 优化electron通信 2022-02-23 14:52:39 +08:00
kuaifan
dbd59cd958 no message 2022-02-23 12:51:27 +08:00
韦荣超
d65f8a3c82 build: 96587a4e45 2022-02-23 12:09:41 +08:00
韦荣超
96587a4e45 perf: 修改项目及消息置顶样式 2022-02-23 12:06:43 +08:00
韦荣超
76ab47c82e perf: 任务创建人和协助人可修改任务内容和详情,但不可修改任务状态 2022-02-23 11:09:12 +08:00
kuaifan
af8344c555 fix: 修复邮箱大写报错的问题 2022-02-23 10:03:15 +08:00
kuaifan
10ff02b8a0 perf: 修改任务时间日志 2022-02-23 09:58:55 +08:00
kuaifan
cb6cf1e34b 优化drawio 2022-02-23 09:41:19 +08:00
kuaifan
953e924aa2 build 2022-02-23 00:26:44 +08:00
kuaifan
2e0c262d32 no message 2022-02-23 00:19:32 +08:00
kuaifan
cbf2e6a140 no message 2022-02-23 00:07:27 +08:00
kuaifan
49c5a9f621 取消 electron-renderer 2022-02-22 21:02:27 +08:00
kuaifan
fb8f63f305 取消drawio网页版未保存提示 2022-02-22 20:51:07 +08:00
韦荣超
49ff61ad65 build: 695fb60aa4 2022-02-22 17:43:08 +08:00
韦荣超
695fb60aa4 perf: 任务聊天中发送图片时,回车可确定发送 2022-02-22 17:39:22 +08:00
韦荣超
da20fafa39 build: d6a9ecd912 2022-02-22 17:20:47 +08:00
韦荣超
d6a9ecd912 fix: 项目列表滚动'置顶'框隐藏 2022-02-22 17:19:08 +08:00
韦荣超
f95d721c9c build: 69d6417985 2022-02-22 16:51:50 +08:00
韦荣超
69d6417985 feat: 项目列表添加置顶功能 2022-02-22 16:49:53 +08:00
kuaifan
ab2bbc28c8 no message 2022-02-22 14:24:27 +08:00
kuaifan
b0b39429ed build 2022-02-22 12:46:38 +08:00
kuaifan
ff14cbc752 feat: 支持文本、图表、思维导图下载上传 2022-02-22 12:24:16 +08:00
kuaifan
dd013aaaa3 优化drawio 2022-02-21 20:23:15 +08:00
kuaifan
119f61ef67 build 2022-02-21 18:49:38 +08:00
kuaifan
a99588c766 优化drawio路径 2022-02-21 18:11:41 +08:00
kuaifan
6f35fe9936 客户端drawio本地化 2022-02-21 17:46:00 +08:00
kuaifan
3112425e43 build 2022-02-20 21:27:52 +08:00
kuaifan
1ab3aefaa5 perf: 更新流程图表 2022-02-20 17:15:01 +08:00
kuaifan
be08732e6b 修复暗黑模式流程图无法正常浏览的问题 2022-02-19 14:43:11 +08:00
kuaifan
97b58b5f9a build 2022-02-19 14:31:10 +08:00
kuaifan
e4855875cf 优化文件多选部分 2022-02-19 14:29:22 +08:00
kuaifan
7a267cc07b 整理代码 2022-02-19 11:44:03 +08:00
kuaifan
f4f351cf9d Merge branch 'develop' of ssh://git.gezi.vip:6006/gx/dootask
# Conflicts:
#	public/css/app.css
#	public/js/app.js
#	public/js/build/208.js
#	public/js/build/389.js
#	public/js/build/406.js
#	public/js/build/406.js.LICENSE.txt
#	public/js/build/423.js
#	public/js/build/459.js
#	public/js/build/459.js.LICENSE.txt
#	public/js/build/688.js
#	public/js/build/688.js.LICENSE.txt
#	public/js/build/726.js
#	public/js/build/755.js
#	public/js/build/93.js
#	public/js/build/954.js
2022-02-19 09:30:13 +08:00
kuaifan
970b811ab9 no msg 2022-02-19 09:27:30 +08:00
kuaifan
99604dbe35 升级onlyoffice 2022-02-18 19:58:21 +08:00
韦荣超
09b1d89718 build: ccb889233c 2022-02-18 19:20:42 +08:00
韦荣超
9d1a9f3134 build: ccb889233c 2022-02-18 19:20:35 +08:00
韦荣超
ccb889233c fix: 【文件】右键多选所有文件复选框显示,取消所有选中消失 2022-02-18 19:19:05 +08:00
韦荣超
939f2cbf97 build: 4bd8835f23 2022-02-18 18:50:38 +08:00
韦荣超
580e0cb36a build: 4bd8835f23 2022-02-18 18:50:29 +08:00
韦荣超
4bd8835f23 fix: 消息:列表滚动隐藏'置顶'文案 2022-02-18 18:48:43 +08:00
韦荣超
22f32da0c5 build: aa8a094383 2022-02-18 18:42:49 +08:00
韦荣超
aa8a094383 fix: 文件:列表模式右键后已选内容会错乱修复 2022-02-18 18:40:39 +08:00
韦荣超
4a72b7f089 build: a33e4905cf 2022-02-18 17:03:18 +08:00
韦荣超
a33e4905cf fix: 消息:列表滚动右键Y轴值判断错误修复 2022-02-18 17:01:33 +08:00
韦荣超
40bd2f0742 build: 1fd73fe79e 2022-02-18 16:45:55 +08:00
韦荣超
1fd73fe79e fix: 消息:列表滚动在任意位置右键菜单错位问题修复 2022-02-18 16:43:27 +08:00
韦荣超
fd7d3e06f4 build: 7a4d27da69 2022-02-18 16:17:41 +08:00
韦荣超
7a4d27da69 perf: 【文件】多个选择剪切功能与右键剪切重复,数据处理应该合拼;方格列表默认不显示复选框,右键菜单新增一个多选菜单 2022-02-18 16:14:54 +08:00
韦荣超
afbadf7d81 build: 4f2c0e94d9 2022-02-18 14:02:22 +08:00
韦荣超
7b65c64431 build: 4f2c0e94d9 2022-02-18 14:02:00 +08:00
韦荣超
4f2c0e94d9 perf: 优化注册提示 2022-02-18 13:59:40 +08:00
韦荣超
7593d7a3e9 build: 3400d1e803 2022-02-18 11:54:29 +08:00
韦荣超
35ddc4a472 Merge remote-tracking branch 'origin/chao-flow' into develop 2022-02-18 11:52:22 +08:00
韦荣超
3400d1e803 fix: 为引入组件报错 2022-02-18 11:51:11 +08:00
韦荣超
f672428fde build: 1521d1e883 2022-02-18 11:29:33 +08:00
韦荣超
4171d993d0 Merge remote-tracking branch 'origin/chao-flow' into develop
# Conflicts:
#	public/js/app.js
2022-02-18 11:25:49 +08:00
韦荣超
d22310ea31 build: 1521d1e883 2022-02-18 11:23:38 +08:00
韦荣超
1521d1e883 fix: 【文件】流程图只读接入新组件及删除旧组件引入代码 2022-02-18 11:21:56 +08:00
韦荣超
0412615f6e build: 2bd666efe7 2022-02-18 10:11:48 +08:00
韦荣超
2bd666efe7 perf: 消息列表需支持多个置顶 2022-02-18 10:10:17 +08:00
kuaifan
541e1f760b build 2022-02-17 20:39:47 +08:00
kuaifan
f176bed436 隐藏浏览流程图的滚动条(流程图使用拖动浏览) 2022-02-17 19:12:29 +08:00
kuaifan
403545cd9b perf: 没有时间还显示时间倒计时的问题 2022-02-17 19:11:52 +08:00
kuaifan
54e4ed27ae perf: 优化修改任务时间记录 2022-02-17 19:11:23 +08:00
kuaifan
e5ddf5616a perf: 优化文件重命名 2022-02-17 18:48:18 +08:00
kuaifan
967c4f04d9 fix: 修复复制文件内容为空的问题 2022-02-17 18:34:27 +08:00
kuaifan
36d4d445a6 fix: 流程图预览暗黑模式下看不见文字的问题 2022-02-17 17:47:51 +08:00
kuaifan
5ed0ae2fa9 perf: 主任务归档时同步子任务归档 2022-02-17 17:27:11 +08:00
韦荣超
503a719609 build: 2dcbba63cb 2022-02-17 15:22:31 +08:00
韦荣超
2dcbba63cb fix: 【消息】置顶会话在子类tab中排序错误修改 2022-02-17 15:20:47 +08:00
韦荣超
5e86bdfda7 build: 4f74a0440c 2022-02-17 14:43:21 +08:00
韦荣超
4f74a0440c perf: 【注册】校验参数是否合法顺序优化 2022-02-17 14:41:49 +08:00
韦荣超
858709f610 build: 6689d48fad 2022-02-17 10:44:23 +08:00
韦荣超
6689d48fad perf: 【消息】列表置顶会话加背景颜色 2022-02-17 10:42:28 +08:00
韦荣超
8581a7c308 build: 9878efb198 2022-02-17 09:37:08 +08:00
韦荣超
9878efb198 perf: 【消息】列表取消置顶 2022-02-17 09:35:32 +08:00
韦荣超
9855c50367 build: 04c59041e0 2022-02-17 09:19:35 +08:00
韦荣超
04c59041e0 feat: 【消息】列表增加点击右键置顶该聊天功能 2022-02-17 09:17:19 +08:00
kuaifan
7fb1ecc9b0 build 2022-02-15 15:44:43 +08:00
kuaifan
ecd2cdd28e perf: 会员选择输入框不刷新的情况 2022-02-15 15:41:45 +08:00
kuaifan
4b0ad22f8d fix: 取消(完成状态)变为待测试(改变状态),如果有状态负责人应该把状态负责人加上 2022-02-15 15:30:35 +08:00
kuaifan
789558da6c perf: 未完成状态禁止归档 2022-02-15 15:21:11 +08:00
kuaifan
3f96117a57 no message 2022-02-15 15:11:29 +08:00
kuaifan
282d6b5746 升级electron 2022-02-15 15:11:20 +08:00
kuaifan
86d2bb9e2a 升级tinymce 2022-02-15 15:11:00 +08:00
韦荣超
ab4fbf0437 Merge remote-tracking branch 'origin/chao-flow' into develop 2022-02-15 15:09:49 +08:00
韦荣超
ae527a78ef perf: 【文件】流程图新组件添加切换皮肤主题功能 2022-02-15 15:02:32 +08:00
韦荣超
8218868681 Merge remote-tracking branch 'origin/chao-flow' into develop 2022-02-15 14:15:00 +08:00
韦荣超
251f4ca4ac fix: 【文件】流程图新组件添加缺失图片 2022-02-15 14:13:42 +08:00
韦荣超
5d5c8ced24 Merge remote-tracking branch 'origin/chao-flow' into develop 2022-02-15 11:12:20 +08:00
韦荣超
f0ec9b7826 fix: 【文件】流程图新组件复选框在360浏览器中显示异常修改 2022-02-15 11:05:48 +08:00
kuaifan
90b8e785a4 fix: 已完成子任务还出现时间跳动的情况 2022-02-15 10:39:19 +08:00
韦荣超
cb11a71ff5 build: 重新编译 2022-02-15 09:11:14 +08:00
韦荣超
54dd90fc4a Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	public/js/app.js
#	public/js/build/423.js
#	public/js/build/726.js
#	public/js/build/93.js
2022-02-15 09:05:20 +08:00
kuaifan
4bb080bd58 vue data内支持this.$L 2022-02-14 18:59:53 +08:00
韦荣超
5115a048a3 Merge remote-tracking branch 'origin/chao-flow' into develop
# Conflicts:
#	public/css/app.css
#	public/js/app.js
#	public/js/build/423.js
#	public/js/build/93.js
2022-02-14 18:17:09 +08:00
韦荣超
259351cdd2 fix: 【文件】流程图去掉ctr+s提示框 2022-02-14 18:13:16 +08:00
kuaifan
972b8f83bc 添加任务详细描述取消文件上传 2022-02-14 17:37:11 +08:00
kuaifan
9f6b1c1e25 perf: 优化iPad兼容 2022-02-14 17:34:07 +08:00
kuaifan
6229a103aa perf: 优化右下角、登录页主题设置 2022-02-14 17:02:51 +08:00
韦荣超
84116e531b build: 2c1b944b7c 2022-02-14 13:42:45 +08:00
韦荣超
2c1b944b7c fix: 【文件】新版流程图右侧及底部被隐藏问题修改 2022-02-14 13:40:37 +08:00
kuaifan
0e66ca148d build 2022-02-13 14:10:06 +08:00
kuaifan
ce70c1ca3a perf: 单条消息最长2000个字符,超过自动分割发送,总最长20000 2022-02-12 17:01:12 +08:00
韦荣超
56b91a59a2 Merge remote-tracking branch 'origin/develop' into develop 2022-02-12 16:02:04 +08:00
kuaifan
8f1fbcb19e no message 2022-02-12 15:10:30 +08:00
韦荣超
a4799a1bc0 “新版本流程图文件” 2022-02-12 15:04:11 +08:00
韦荣超
cc24ff22e9 perf: 去掉刷新提示及前端报错 2022-02-12 15:02:10 +08:00
kuaifan
72ca335c4c perf: 下载、查看任务文件权限改为所有项目成员 2022-02-12 14:24:36 +08:00
kuaifan
0ef6476e58 perf: 任务详细描述取消文件上传 2022-02-12 14:15:23 +08:00
kuaifan
6ba63d1466 fix: 修复聊天mp4文件无法预览的问题 2022-02-12 14:14:18 +08:00
kuaifan
f9d28e1b6b perf: 优化任务详情拖动发送文件 2022-02-12 13:49:30 +08:00
韦荣超
63534d3eb5 feat: 更新流程图组件 2022-02-12 11:54:56 +08:00
韦荣超
2ab2cf01db “新版本流程图文件” 2022-02-12 11:51:37 +08:00
kuaifan
2fc039dd70 perf: 优化通知 2022-02-12 09:50:02 +08:00
韦荣超
230d1a1f86 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	app/Http/Controllers/Api/FileController.php
#	public/js/build/208.js
#	public/js/build/252.js
#	public/js/build/400.js
#	public/js/build/76.js
2022-02-11 11:01:05 +08:00
kuaifan
f7921bf388 build 2022-02-10 21:18:18 +08:00
kuaifan
feed984ba8 perf: 兼容iPad端 2022-02-10 21:03:36 +08:00
kuaifan
5324861f16 perf: 手动切换账号提示“项目不存在或不在成员列表内”的情况 2022-02-10 20:34:13 +08:00
kuaifan
ae80939d2e perf: 任务详情 2022-02-10 20:33:35 +08:00
kuaifan
c117e4b087 fix: 修复文件上传一直出现loading的情况 2022-02-10 20:33:14 +08:00
kuaifan
96580e2284 perf: 该文件版本已经改变了。该页面将被重新加载 2022-02-10 20:12:17 +08:00
kuaifan
bc3932c8b8 perf: 点击切换语言一级菜单出现的兼容问题 2022-02-10 17:10:18 +08:00
kuaifan
f7cd4f34d3 fix: 已存在的任务新添加负责人不出现在任务群聊里 2022-02-10 17:09:44 +08:00
kuaifan
3a76f51707 fix: 初次安装失败的情况 2022-02-10 16:41:07 +08:00
kuaifan
041ba8f2ed 优化onlyoffice样式 2022-02-09 21:52:04 +08:00
kuaifan
17e5d15b1b build 2022-02-09 21:50:12 +08:00
kuaifan
a6ba59eac0 perf: 上传文件夹 2022-02-09 21:04:18 +08:00
kuaifan
e89ff02b59 fix: 客户端偶尔出现无法打开文件的情况 2022-02-09 21:04:03 +08:00
kuaifan
b28be29dc8 build 2022-02-09 17:29:36 +08:00
kuaifan
9ad85e01de perf: 该文件版本已经改变了。该页面将被重新加载 2022-02-09 17:27:59 +08:00
kuaifan
d49790ba78 perf: 团队管理新增身份筛选项 2022-02-09 17:09:40 +08:00
kuaifan
598f01de95 no message 2022-02-09 16:59:48 +08:00
kuaifan
992e137339 perf: 任务文件支持更多格式上传 2022-02-09 16:59:42 +08:00
kuaifan
124666cca6 优化系统设置路由菜单 2022-02-09 16:13:13 +08:00
kuaifan
848359bf7b feat: 添加项目支持默认工作流 2022-02-09 16:05:25 +08:00
kuaifan
fb24af1900 feat: 新增自定义添加项目时的项目模板 2022-02-09 16:05:02 +08:00
韦荣超
3d6df3cc09 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	app/Http/Controllers/Api/DialogController.php
#	app/Http/Controllers/Api/ProjectController.php
#	app/Models/Project.php
#	electron/package.json
#	package.json
#	public/css/app.css
#	public/js/app.js
#	public/js/build/146.js
#	public/js/build/188.js
#	public/js/build/188.js.LICENSE.txt
#	public/js/build/244.js
#	public/js/build/244.js.LICENSE.txt
#	public/js/build/278.js
#	public/js/build/278.js.LICENSE.txt
#	public/js/build/30.js
#	public/js/build/423.js
#	public/js/build/43.js
#	public/js/build/525.js
#	public/js/build/644.js.LICENSE.txt
#	public/js/build/660.js
#	public/js/build/766.js
#	public/js/build/831.js
#	public/js/build/893.js
#	public/js/build/919.js
#	public/js/build/919.js.LICENSE.txt
#	public/js/build/934.js
#	public/js/build/934.js.LICENSE.txt
#	resources/assets/js/pages/manage/components/DialogList.vue
#	resources/assets/js/pages/manage/components/DialogWrapper.vue
#	resources/assets/js/pages/manage/components/TaskAdd.vue
#	resources/assets/sass/components/report.scss
2022-02-07 10:50:38 +08:00
kuaifan
ea58ed46f2 doc 2022-01-29 16:10:31 +08:00
kuaifan
32817a4275 build 2022-01-29 16:04:28 +08:00
kuaifan
95033cd5b7 perf: 优化项目筛选工作流 2022-01-29 16:02:49 +08:00
kuaifan
9999548bc2 perf: 优化撤回消息 2022-01-29 13:39:16 +08:00
kuaifan
3d04bd4444 build 2022-01-28 18:29:15 +08:00
kuaifan
21e618cca2 优化了一些代码 2022-01-28 18:27:47 +08:00
kuaifan
02eb386155 撤回消息优化 2022-01-28 15:09:57 +08:00
kuaifan
35bd038802 Merge branch 'master' of github.com:kuaifan/dootask into develop
# Conflicts:
#	app/Http/Controllers/Api/DialogController.php
#	app/Http/Controllers/Api/ProjectController.php
#	electron/package.json
#	package.json
#	public/css/app.css
#	public/docs/assets/main.bundle.js
#	public/js/app.js
#	public/js/build/146.js.LICENSE.txt
#	public/js/build/161.js
#	public/js/build/199.js
#	public/js/build/199.js.LICENSE.txt
#	public/js/build/218.js
#	public/js/build/218.js.LICENSE.txt
#	public/js/build/244.js
#	public/js/build/244.js.LICENSE.txt
#	public/js/build/309.js
#	public/js/build/423.js
#	public/js/build/43.js
#	public/js/build/693.js
#	public/js/build/717.js
#	public/js/build/717.js.LICENSE.txt
#	public/js/build/79.js.LICENSE.txt
#	resources/assets/js/pages/manage/components/TaskAdd.vue
2022-01-28 14:39:43 +08:00
kuaifan
1ff59aee56 perf: 优化修改工作流未保存关闭提示 2022-01-28 13:25:51 +08:00
kuaifan
d64fae1832 perf: 查看工作报告图片显示不全的问题 2022-01-28 13:12:11 +08:00
kuaifan
ae169810d0 perf: 优化UserInput组件 2022-01-28 13:04:54 +08:00
kuaifan
3c7619098a build 2022-01-28 01:20:36 +08:00
kuaifan
a881bfd63b fix: 客户端编辑文件不显示协助成员 2022-01-28 01:12:22 +08:00
kuaifan
09d3131d46 fix: 添加任务时选择任务组无效 2022-01-28 00:12:51 +08:00
kuaifan
53550b8975 fix: 工作报告弹窗被遮挡的问题 2022-01-27 23:55:13 +08:00
kuaifan
569164ed56 fix: 客户端任务窗口首次聊天失败的问题 2022-01-27 23:44:06 +08:00
kuaifan
f61d79d53e 客户端office查看修改窗口隐藏自带的标题 2022-01-27 23:28:38 +08:00
kuaifan
bba28d6b57 fix: 文件右键打开失效 2022-01-27 23:17:41 +08:00
Mr.Huan
469f30044a build: 0993d87799 2022-01-27 17:12:58 +08:00
Mr.Huan
0993d87799 fix: 修复工作汇报正文被图片撑开页面的问题 2022-01-27 17:11:48 +08:00
Mr.Huan
09fa33236f build: 4e9abd6512 2022-01-27 16:33:25 +08:00
Mr.Huan
4e9abd6512 perf: 开放文件夹移动功能 2022-01-27 16:31:58 +08:00
Mr.Huan
2996c0b38e build: 36e366abe0 2022-01-27 15:58:26 +08:00
Mr.Huan
36e366abe0 perf: 优化文件模块用户体验 2022-01-27 15:55:44 +08:00
Mr.Huan
f9c6c6c127 build: 34772ef2bf 2022-01-27 14:35:17 +08:00
Mr.Huan
34772ef2bf perf: 文件增加取消选择按钮 2022-01-27 14:33:08 +08:00
Mr.Huan
305af935a7 feat: 文件支持批量移动 2022-01-27 14:24:01 +08:00
kuaifan
33f3c9acbf build 2022-01-27 12:25:10 +08:00
kuaifan
9e560c79ae 优化聊天消息附件支持预览 2022-01-27 12:17:30 +08:00
kuaifan
705d7f3da0 perf: 聊天消息附件支持预览 2022-01-27 00:44:49 +08:00
kuaifan
08372facd7 fix: 移动端无法上传任务文件的问题 2022-01-26 19:11:16 +08:00
kuaifan
886baa427b perf: 仪表盘鼠标滑过时间显示完整时间 2022-01-26 18:12:21 +08:00
kuaifan
8ff94bc138 fix: 任务中没有聊天记录时,发送图片无法成功 2022-01-26 18:05:53 +08:00
Mr.Huan
d0438390cc build: d22266a947 2022-01-26 17:27:42 +08:00
Mr.Huan
d22266a947 perf: 聊天页面图片尺寸缩小至180px 2022-01-26 17:26:29 +08:00
Mr.Huan
e34fb25759 fix: 修复消息撤回文字提示在第一条时会被顶部遮住的问题 2022-01-26 17:23:20 +08:00
Mr.Huan
675955b2e6 build: 4516bce0ee 2022-01-26 17:14:58 +08:00
Mr.Huan
4516bce0ee fix: 修复个人对话为空时无法重复打开该对话的问题 2022-01-26 17:13:20 +08:00
Mr.Huan
4853fbcec3 build: 72e5f9a83e 2022-01-26 15:04:03 +08:00
Mr.Huan
72e5f9a83e fix: 修复消息撤回文字提示在第一条时会被顶部遮住的问题 2022-01-26 15:02:56 +08:00
Mr.Huan
1a1ddc34a2 build: 40ebfb676c 2022-01-26 14:46:37 +08:00
Mr.Huan
40ebfb676c perf: 头像上传图片浏览组件增加空提醒 2022-01-26 14:43:29 +08:00
kuaifan
3bb7e958dc perf: macOS角标首次不显示的问题 2022-01-26 11:46:11 +08:00
Mr.Huan
19dd16fcf0 docs: 补充消息撤回功能API文档 2022-01-26 09:48:35 +08:00
kuaifan
f596749645 Merge branch 'master' of github.com:kuaifan/dootask into develop
# Conflicts:
#	electron/package.json
#	package.json
#	public/css/app.css
#	public/js/app.js
#	public/js/build/120.js
#	public/js/build/120.js.LICENSE.txt
#	public/js/build/146.js.LICENSE.txt
#	public/js/build/161.js
#	public/js/build/284.js
#	public/js/build/309.js
#	public/js/build/400.js
#	public/js/build/644.js
#	public/js/build/644.js.LICENSE.txt
#	public/js/build/79.js.LICENSE.txt
#	resources/assets/js/pages/manage/components/TaskAdd.vue
2022-01-26 09:35:16 +08:00
kuaifan
482813ea88 build 2022-01-26 01:33:53 +08:00
kuaifan
60e47e85a3 perf: 添加任务时如果自己不是任务负责人可选择加入协助人员列表 2022-01-26 01:12:00 +08:00
kuaifan
22c06fee5e 剔除模式代码优化 2022-01-26 00:02:15 +08:00
kuaifan
4a42ce87a2 feat: 工作流程负责人新增剔除模式(改变任务负责人并保留操作状态的人员) 2022-01-25 23:46:47 +08:00
kuaifan
a98e4dbcd4 perf: 文件列表模式右键重命名无效 2022-01-25 23:12:42 +08:00
kuaifan
7baa37ccd1 fix: macOS客户端首次不加载角标的问题 2022-01-25 23:06:08 +08:00
kuaifan
417017add9 优化编辑器皮肤 2022-01-25 23:04:51 +08:00
Mr.Huan
b5e3cc2503 build: 2e11fe2b58 2022-01-25 16:51:32 +08:00
Mr.Huan
2e11fe2b58 feat: 文件网格模式支持批量删除文件 2022-01-25 16:49:54 +08:00
Mr.Huan
70f1258bab feat: 文件表格支持批量删除文件 2022-01-25 16:49:54 +08:00
韦荣超
d91fa33330 fix: 工作流列表接口用作筛选时不用传多余参数 2022-01-25 16:28:15 +08:00
kuaifan
8f0b5dc049 Merge branch 'master' of github.com:kuaifan/dootask into develop
# Conflicts:
#	app/Http/Controllers/Api/DialogController.php
#	electron/package.json
#	package.json
#	public/css/app.css
#	public/js/app.js
#	public/js/build/146.js
#	public/js/build/146.js.LICENSE.txt
#	public/js/build/178.js
#	public/js/build/178.js.LICENSE.txt
#	public/js/build/199.js
#	public/js/build/199.js.LICENSE.txt
#	public/js/build/309.js
#	public/js/build/328.js.LICENSE.txt
#	public/js/build/388.js
#	public/js/build/43.js
#	public/js/build/46.js.LICENSE.txt
#	public/js/build/857.js
#	public/js/build/857.js.LICENSE.txt
#	public/js/build/893.js
2022-01-25 16:12:53 +08:00
kuaifan
7f14a82053 perf: 任务群聊中拖拽文件或者照片时并未有确认窗口 2022-01-25 16:06:26 +08:00
kuaifan
b05db6d458 fix: 主任务负责人无法修改子任务负责人的问题 2022-01-25 15:49:22 +08:00
kuaifan
ea87092d73 fix: 设置主任务时间跟子任务没有交集时子任务时间改为跟主任务一致 2022-01-25 15:28:42 +08:00
kuaifan
f468f7ae27 build 2022-01-25 15:05:46 +08:00
kuaifan
f12c79d292 pref: 待处理时进入动态中点击重置,重置提示中的参数为空,点击确定后,提示流程不存在或者删除 2022-01-25 15:04:03 +08:00
kuaifan
6e76514a24 pref: 添加子任务时输入框为空,回车时提示的图标错误 2022-01-25 15:03:43 +08:00
kuaifan
88f3d3821e fix: 搜索后,点击查看已归档的任务,仍然显示这个搜索框 2022-01-25 15:03:24 +08:00
kuaifan
bed2e20a91 fix: 修复归档任务无法查看日志的问题 2022-01-25 14:50:39 +08:00
kuaifan
c7700bdfef perf: 上传文件夹应该保持目录结构 2022-01-25 14:46:52 +08:00
Mr.Huan
778f6367f9 fix: 动态中点击重置按钮后流程的字体颜色和背景颜色并未正确显示 2022-01-25 11:00:59 +08:00
Mr.Huan
6115eea401 build: ff62ef729a 2022-01-25 09:39:03 +08:00
Mr.Huan
ff62ef729a perf: 支持xmind,rp格式文件上传 2022-01-25 09:37:45 +08:00
kuaifan
d0a432164d build 2022-01-25 02:06:27 +08:00
kuaifan
0f8d9e64ef no message 2022-01-25 01:30:55 +08:00
kuaifan
d1f5096a16 删除luckysheet 2022-01-25 00:32:51 +08:00
kuaifan
003de399dc 支持在线编辑代码 2022-01-25 00:24:47 +08:00
kuaifan
36989ce5ff feat: 新增文本文件编辑功能 2022-01-24 20:33:27 +08:00
Mr.Huan
fa987b4e30 build: b47c494240 2022-01-24 15:50:32 +08:00
Mr.Huan
b47c494240 fix: 修复任务负责人选择弹窗不随页面滚动的问题 2022-01-24 15:35:04 +08:00
kuaifan
1ab8a19f8e fix: 客户端无法下载聊天文件的问题 2022-01-24 15:18:35 +08:00
Mr.Huan
a31134195b build: 5580a4ee3d 2022-01-24 15:08:38 +08:00
Mr.Huan
5580a4ee3d fix: 修复添加任务变更栏目后无效果的问题 2022-01-24 15:07:01 +08:00
kuaifan
870276fa48 feat: 支持文件下载 2022-01-24 14:42:29 +08:00
Mr.Huan
608660a101 build: 25cb8015d0 2022-01-24 11:56:07 +08:00
Mr.Huan
25cb8015d0 perf: 注册页面增加密码格式提醒 2022-01-24 11:54:47 +08:00
Mr.Huan
e686e8f58c build: 14b699b5b1 2022-01-24 10:39:13 +08:00
Mr.Huan
14b699b5b1 perf: 消息撤回后直接删除消息 2022-01-24 10:37:18 +08:00
韦荣超
ce0c86c8e5 build: 11d1f26724 2022-01-24 10:22:45 +08:00
韦荣超
11d1f26724 feat: 加入【项目管理】任务列表流程筛选被回滚功能 2022-01-24 10:20:59 +08:00
Mr.Huan
a8c0978f0d build: 0a0227cca4 2022-01-24 09:31:52 +08:00
Mr.Huan
f643911014 feat: 聊天消息撤回-后端实现 2022-01-24 09:30:10 +08:00
Mr.Huan
0a0227cca4 feat: 聊天消息撤回-前端实现 2022-01-24 09:30:10 +08:00
kuaifan
8bacc3b6ba Merge branch 'master' of github.com:kuaifan/dootask into develop
# Conflicts:
#	electron/package.json
#	package.json
#	public/js/app.js
#	public/js/build/161.js
#	public/js/build/400.js
#	public/js/build/616.js
#	public/js/build/845.js
2022-01-24 01:45:08 +08:00
kuaifan
d516330a41 feat: 聊天粘贴发送文件、图片时预览确认 2022-01-24 01:30:10 +08:00
kuaifan
2d83faf144 build 2022-01-24 00:42:58 +08:00
kuaifan
c3fd6bf88f 优化日志显示 2022-01-24 00:40:14 +08:00
kuaifan
2a646becfd 优化项目简介样式 2022-01-24 00:33:31 +08:00
kuaifan
a825657516 fix: 修改主任务时间时未设置时间的子任务没有同步修改 2022-01-24 00:10:47 +08:00
kuaifan
b71dbe9832 perf: 任务详情窗口截止时间24小时倒计时 2022-01-24 00:05:36 +08:00
kuaifan
f06b4040bc 支持查看已归档任务 2022-01-23 23:55:56 +08:00
kuaifan
563cd4b843 优化已归档任务列表 2022-01-23 23:44:39 +08:00
kuaifan
834dc9bec9 支持查看已归档任务 2022-01-23 23:33:16 +08:00
kuaifan
e6e58a03a6 perf: 修改任务或修改项目后同步对话信息 2022-01-23 17:58:32 +08:00
kuaifan
d9f4adbe26 支持查看归档任务 2022-01-23 17:43:15 +08:00
kuaifan
1777153411 perf: 支持查看已归档任务详情 2022-01-23 17:36:19 +08:00
kuaifan
9b0ca581f1 perf: 消息列表显示任务基本状态 2022-01-23 16:17:11 +08:00
kuaifan
493cf7d46a perf: 优化消息对话排序 2022-01-23 15:01:58 +08:00
kuaifan
004bf36dc1 perf: 隐藏无聊天内容的对话 2022-01-23 14:51:32 +08:00
kuaifan
fcf7fb4b9f perf: 消息页再次点击类型定位到未读消息 2022-01-23 14:48:10 +08:00
kuaifan
130a85a5fc fix: flow文件路径引用错误导致无法编译的问题 2022-01-23 13:16:25 +08:00
Mr.Huan
d46eec568c build: 6529e45868 2022-01-22 15:39:25 +08:00
Mr.Huan
7dc6d0ffb2 build: 6529e45868 2022-01-22 15:28:37 +08:00
Mr.Huan
6529e45868 fix: 修复工作汇报选择接收人组件在调整窗体大小时发生位移的问题 2022-01-22 15:26:42 +08:00
Mr.Huan
3ba6ea9c7e perf: 调整周报签名的生成方法 2022-01-22 15:07:37 +08:00
kuaifan
b6ff6f5453 Merge branch 'master' of github.com:kuaifan/dootask into develop
# Conflicts:
#	public/js/app.js
#	public/js/build/657.js
#	public/js/build/782.js
#	public/js/build/845.js
2022-01-22 14:18:32 +08:00
Mr.Huan
cfa529b742 build: 2eabc76c1a 2022-01-22 13:53:13 +08:00
Mr.Huan
2eabc76c1a fix: 修复周报点击编辑关闭后再点击"新建汇报"按钮导致用户列表无法重新加载的问题 2022-01-22 13:51:03 +08:00
韦荣超
463e67d64c fix:【工作汇报】修复新增今天的周报,提示要覆盖昨天日报的问题 2022-01-22 12:06:21 +08:00
韦荣超
1f96af1024 build: 563aa92958 2022-01-22 10:37:27 +08:00
韦荣超
563aa92958 fix: 【工作汇报】收到的汇报中,未读时发送人删除掉汇报对象,汇报对象的未读数量仍然未减少 2022-01-22 10:35:45 +08:00
kuaifan
f09b864e30 perf: 仅客户端或Chrome浏览器支持主题功能 2022-01-22 09:07:13 +08:00
kuaifan
7ce15d5a71 优化代码 2022-01-21 23:31:01 +08:00
kuaifan
cc767b164e 优化工作报告 2022-01-21 23:16:50 +08:00
kuaifan
230ff75c0b 优化搜索栏前端 2022-01-21 21:56:27 +08:00
kuaifan
da39739fb5 优化工作报告前端 2022-01-21 21:49:27 +08:00
kuaifan
c33be22057 Merge branch 'develop' of ssh://git.gezi.vip:6006/gx/dootask into develop
# Conflicts:
#	resources/assets/js/pages/manage/components/ReportDetail.vue
#	resources/assets/js/pages/manage/components/ReportEdit.vue
2022-01-21 21:04:28 +08:00
kuaifan
902f35d21b 优化前端代码 2022-01-21 21:02:17 +08:00
韦荣超
d8426780d2 build: a1aaf90d2e 2022-01-21 21:01:08 +08:00
韦荣超
a1aaf90d2e fix: 【工作报告】新增报告初始化数据不导入已有时间点数据 2022-01-21 20:59:28 +08:00
韦荣超
53ed4d4072 build: da75554447 2022-01-21 19:59:44 +08:00
韦荣超
da75554447 fix: 【工作报告】详情页面前端报错修改 2022-01-21 19:58:03 +08:00
韦荣超
04998d6a60 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	public/js/app.js
2022-01-21 18:10:53 +08:00
韦荣超
3d88a28465 build: 2dd8f75d52 2022-01-21 18:10:24 +08:00
韦荣超
2dd8f75d52 fix: 【工作报告】填写内容--汇报类型切换提交提示错误问题 2022-01-21 18:08:48 +08:00
kuaifan
7a4bdd0ada no message 2022-01-21 17:59:08 +08:00
韦荣超
0ed0b5ee43 build: 2475ee90ee 2022-01-21 17:41:09 +08:00
韦荣超
2475ee90ee perf: 工作报告优化 2022-01-21 17:39:10 +08:00
kuaifan
511c19d5aa 优化前端代码 2022-01-21 14:49:20 +08:00
韦荣超
c4fed86f1e Merge remote-tracking branch 'origin/develop' into develop 2022-01-21 14:26:32 +08:00
韦荣超
18904ebbaa perf: 规范代码 2022-01-21 14:26:17 +08:00
kuaifan
da3672e6be 优化前端代码 2022-01-21 14:24:31 +08:00
kuaifan
52e9836bbf build 2022-01-21 14:07:14 +08:00
kuaifan
3201d90a53 格式化代码 2022-01-21 14:07:02 +08:00
kuaifan
cd9c1d9660 Merge branch 'master' of github.com:kuaifan/dootask into develop 2022-01-21 13:54:58 +08:00
kuaifan
0f54a1f638 no message 2022-01-21 13:50:41 +08:00
kuaifan
713316f87c 优化暗黑皮肤 2022-01-21 13:50:23 +08:00
kuaifan
88d38ba8d1 fix: 发送图片显示错误 2022-01-21 13:45:33 +08:00
kuaifan
164f3275f4 优化暗黑皮肤 2022-01-21 13:13:07 +08:00
韦荣超
a44d6b8b79 build: 942cf57c36 2022-01-21 12:10:48 +08:00
韦荣超
942cf57c36 perf: 工作报告优化 2022-01-21 12:06:26 +08:00
kuaifan
18bc75242b fix: 项目负责人还原已经回档的自己不是任务负责人的任务时,无法还原,且会提示错误 2022-01-21 11:10:29 +08:00
kuaifan
3b38c8b408 perf: 共享文件删除、移动改为仅限所有者或创建者操作 2022-01-21 10:00:31 +08:00
kuaifan
22e718423e 优化暗黑模式 2022-01-21 01:38:05 +08:00
kuaifan
0dc0e7226e 调整主题色 2022-01-20 22:59:51 +08:00
kuaifan
8069784198 no message 2022-01-20 00:38:30 +08:00
kuaifan
a9e71567fe Merge branch 'master' of github.com:kuaifan/dootask into develop
# Conflicts:
#	electron/package.json
#	package.json
#	public/css/app.css
#	public/js/app.js
#	public/js/build/146.js
#	public/js/build/169.js
#	public/js/build/631.js
#	public/js/build/679.js
#	public/js/build/700.js
#	public/js/build/728.js
#	public/js/build/802.js
#	public/js/build/823.js
#	public/js/build/954.js
#	resources/assets/js/pages/manage.vue
2022-01-20 00:33:05 +08:00
kuaifan
0fae151731 build 2022-01-20 00:20:06 +08:00
kuaifan
3bb0b55955 feat: 添加暗黑模式 2022-01-20 00:19:00 +08:00
kuaifan
071ca80bae fix: 无法读取 /www/.env:没有那个文件或目录 2022-01-18 22:52:43 +08:00
kuaifan
1abd78305f Merge branch 'master' of github.com:kuaifan/dootask into develop
# Conflicts:
#	public/css/app.css
#	public/js/app.js
#	public/js/build/120.js
#	public/js/build/120.js.LICENSE.txt
#	public/js/build/198.js
#	public/js/build/494.js
#	public/js/build/79.js
#	public/js/build/793.js
#	public/js/build/875.js
#	public/js/build/890.js
#	resources/assets/js/components/DrawerOverlay.vue
2022-01-17 17:13:12 +08:00
kuaifan
e1ef9a94af no message 2022-01-17 00:36:19 +08:00
kuaifan
9f62023175 build 2022-01-17 00:12:34 +08:00
kuaifan
c9a6d2f5a8 perf: 优化安装脚本 2022-01-16 22:03:50 +08:00
kuaifan
07fca8b895 fix: 最大只能上传64M的问题 2022-01-16 14:53:35 +08:00
kuaifan
f0ce6cc28f build 2022-01-14 17:59:07 +08:00
kuaifan
ede298a142 perf: 工作汇报样式 2022-01-14 17:57:51 +08:00
韦荣超
e63c5c074c build: 0d0b5dd552 2022-01-14 16:46:30 +08:00
韦荣超
0d0b5dd552 fix: 【工作报告】填写'提交'按钮位置被挤压修改 2022-01-14 16:43:13 +08:00
韦荣超
d48ed18102 feat: 【工作报告】功能 2022-01-14 15:55:26 +08:00
623585344@qq.com
320f183b49 add:新增暗黑皮肤样式 2021-12-31 16:59:42 +08:00
623585344@qq.com
c8f11578d6 mod:修改首页内容部分图片自适应 2021-12-29 17:47:45 +08:00
623585344@qq.com
f33be18be3 fix:修复首页内容部分间距问题 2021-12-29 16:28:30 +08:00
623585344@qq.com
513c7dbd85 add:新增自适应隐藏语言选择框文本 2021-12-29 16:20:59 +08:00
623585344@qq.com
02a9623310 fix:修复英文版本登录按钮无法点击 2021-12-29 14:13:51 +08:00
623585344@qq.com
58c1d52f52 fix:修复首页代码格式化 2021-12-29 13:58:12 +08:00
623585344@qq.com
b71c881b43 add:首页自适应 2021-12-29 13:48:52 +08:00
test
ffc2c7dea3 add:首页功能开发 2021-12-28 15:38:40 +08:00
test
d7652a7a32 add:首页功能开发 2021-12-28 15:34:54 +08:00
3240 changed files with 161537 additions and 386084 deletions

View File

@@ -1,13 +1,14 @@
APP_NAME=Dootask
APP_NAME=DooTask
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_DEBUG=false
APP_SCHEME=auto
APP_URL=http://localhost
APP_ID=
APP_IPPR=
APP_PORT=2222
APP_DEV_PORT=
LOG_CHANNEL=stack
LOG_LEVEL=debug
@@ -53,6 +54,9 @@ PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
JUKE_KEY_JOKE=
JUKE_KEY_SOUP=
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

View File

@@ -1,61 +0,0 @@
name: Build
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
if: startsWith(github.event.ref, 'refs/tags/v')
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Create changelog text
id: changelog
uses: loopwerk/tag-changelog@v1
with:
token: ${{ secrets.GH_PAT }}
exclude_types: other,chore
- name: Create release
uses: actions/create-release@latest
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
body: ${{ steps.changelog.outputs.changes }}
build:
runs-on: ${{ matrix.os }}
environment: build
strategy:
matrix:
os: [macos-11]
platform: [
build-mac,
build-mac-arm,
build-win
]
if: startsWith(github.event.ref, 'refs/tags/v')
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Use Node.js 14.x
uses: actions/setup-node@v1
with:
node-version: 14.x
- name: Build
env:
GH_TOKEN: ${{ secrets.GH_PAT }}
EP_PRE_RELEASE: true
run: ./cmd electron ${{ matrix.platform }}

33
.github/workflows/publish-desktop.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Publish Desktop
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: macos-latest
environment: build
if: startsWith(github.event.ref, 'refs/tags/v')
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Use Node.js 16.x
uses: actions/setup-node@v1
with:
node-version: 16.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 all

2
.gitignore vendored
View File

@@ -5,6 +5,7 @@
/public/.well-known
/public/.user.ini
/storage/*.key
/config/LICENSE
/vendor
/build
/tmp
@@ -19,7 +20,6 @@ Homestead.yaml
npm-debug.log
yarn-error.log
test.*
composer.lock
package-lock.json
laravels-timer-process.pid
.DS_Store

6
.gitmodules vendored Normal file
View File

@@ -0,0 +1,6 @@
[submodule "resources/drawio"]
path = resources/drawio
url = https://github.com/jgraph/drawio.git
[submodule "resources/mobile"]
path = resources/mobile
url = https://github.com/kuaifan/dootask-app.git

13
.gitpod.yml Normal file
View File

@@ -0,0 +1,13 @@
# This configuration file was automatically generated by Gitpod.
# Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file)
# and commit this file to your remote git repository to share the goodness with others.
tasks:
- init: sudo ./cmd install
command: ./cmd dev
ports:
- port: 2222
visibility: public
- port: 22222
visibility: public

2083
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -5,20 +5,25 @@ English | **[中文文档](./README_CN.md)**
- [Screenshot Preview](README_PREVIEW.md)
- [Demo site](http://www.dootask.com/)
**QQ Group**
Group No.: `546574618`
## Setup
> `Docker` & `Docker Compose v2.0+` must be installed
- `Docker` & `Docker Compose v2.0+` must be installed
- System: `Centos/Debian/Ubuntu/macOS`
- Hardware suggestion: 2 cores and above 4G memory
### Deployment project
### Deployment (Pro Edition)
```bash
# 1、Clone the repository
# Clone projects on github
git clone https://github.com/kuaifan/dootask.git
git clone -b pro --depth=1 https://github.com/kuaifan/dootask.git
# Or you can use gitee
git clone https://gitee.com/aipaw/dootask.git
git clone -b pro --depth=1 https://gitee.com/aipaw/dootask.git
# 2、Enter directory
cd dootask
@@ -40,6 +45,16 @@ cd dootask
./cmd port 2222
```
### Change App Url
```bash
# This URL only affects the email reply.
./cmd url {Your domain url}
# example:
./cmd url https://domain.com
```
### Stop server
```bash
@@ -49,18 +64,28 @@ cd dootask
./cmd start
```
### Development compilation
```bash
# Development mode, Mac OS only
./cmd dev
# Production projects, macOS only
./cmd prod
```
### Shortcuts for running command
```bash
# You can do this using the following command
./cmd artisan "your command" // To run a artisan command
./cmd php "your command" // To run a php command
./cmd nginx "your command" // To run a nginx command
./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 test "your command" // To run a phpunit command
./cmd mysql "your command" // To run a mysql command (backup: Backup database, recovery: Restore database)
./cmd artisan "your command" # To run a artisan command
./cmd php "your command" # To run a php command
./cmd nginx "your command" # To run a nginx command
./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 test "your command" # To run a phpunit command
./cmd mysql "your command" # To run a mysql command (backup: Backup database, recovery: Restore database)
```
### NGINX PROXY SSL
@@ -71,7 +96,7 @@ proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 2、Enter directory and run command
# 2、Running commands in a project
./cmd https
```
@@ -80,7 +105,7 @@ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
**Note: Please back up your data before upgrading!**
```bash
# Method 1: enter directory and run command
# Method 1: Running commands in a project
./cmd update
# Or method 2: use this method if method 1 fails
@@ -89,16 +114,33 @@ git pull
./cmd uninstall
./cmd install
./cmd mysql recovery
./cmd artisan migrate
```
* Please try again if the upgrade fails across a large version.
* If 502 after the upgrade please run `./cmd restart` restart the service.
## Transfer
Follow these steps to complete the project migration after the new project is installed:
1. Backup original database
```bash
# Run command under old project
./cmd mysql backup
```
2. Copy `database backup file` and `public/uploads` directory to the new project.
3. Restore database to new project
```bash
# Run command under new project
./cmd mysql recovery
```
## Uninstall
```bash
# Enter directory and run command
# Running commands in a project
./cmd uninstall
```
## Contact us
QQ Group: 546574618

21
README_CLIENT.md Normal file
View File

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

View File

@@ -5,20 +5,25 @@
- [截图预览](README_PREVIEW.md)
- [演示站点](http://www.dootask.com/)
**QQ交流群**
- QQ群号: `546574618`
## 安装程序
> 必须安装 `Docker` 和 `Docker Compose v2.0+`
- 必须安装`Docker``Docker Compose v2.0+`
- 支持环境:`Centos/Debian/Ubuntu/macOS`
- 硬件建议2核4G以上
### 部署项目
### 部署项目Pro版
```bash
# 1、克隆项目到您的本地或服务器
# 通过github克隆项目
git clone https://github.com/kuaifan/dootask.git
git clone -b pro --depth=1 https://github.com/kuaifan/dootask.git
# 或者你也可以使用gitee
git clone https://gitee.com/aipaw/dootask.git
git clone -b pro --depth=1 https://gitee.com/aipaw/dootask.git
# 2、进入目录
cd dootask
@@ -40,6 +45,16 @@ cd dootask
./cmd port 2222
```
### 更换URL
```bash
# 此地址仅影响邮件回复功能
./cmd url {域名地址}
# 例如:
./cmd url https://domain.com
```
### 停止服务
```bash
@@ -49,18 +64,29 @@ cd dootask
./cmd start
```
### 开发编译
```bash
# 开发模式仅限macOS
./cmd dev
# 编译项目仅限macOS
./cmd prod
```
### 运行命令的快捷方式
```bash
# 你可以使用以下命令来执行
./cmd artisan "your command" // 运行 artisan 命令
./cmd php "your command" // 运行 php 命令
./cmd nginx "your command" // 运行 nginx 命令
./cmd redis "your command" // 运行 redis 命令
./cmd composer "your command" // 运行 composer 命令
./cmd supervisorctl "your command" // 运行 supervisorctl 命令
./cmd test "your command" // 运行 phpunit 命令
./cmd mysql "your command" // 运行 mysql 命令 (backup: 备份数据库recovery: 还原数据库)
./cmd artisan "your command" # 运行 artisan 命令
./cmd php "your command" # 运行 php 命令
./cmd nginx "your command" # 运行 nginx 命令
./cmd redis "your command" # 运行 redis 命令
./cmd composer "your command" # 运行 composer 命令
./cmd supervisorctl "your command" # 运行 supervisorctl 命令
./cmd test "your command" # 运行 phpunit 命令
./cmd mysql "your command" # 运行 mysql 命令 (backup: 备份数据库recovery: 还原数据库)
```
### NGINX 代理 SSL
@@ -71,7 +97,7 @@ proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 2、进入项目所在目录,运行以下命令
# 2、在项目下运行命令
./cmd https
```
@@ -80,7 +106,7 @@ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
**注意:在升级之前请备份好你的数据!**
```bash
# 方法1进入项目所在目录,运行以下命令
# 方法1在项目下运行命令
./cmd update
# 或者方法2如果方法1失败请使用此方法
@@ -89,16 +115,33 @@ git pull
./cmd uninstall
./cmd install
./cmd mysql recovery
./cmd artisan migrate
```
* 跨越大版本升级失败时请重试执行一次。
* 如果升级后出现502请运行 `./cmd restart` 重启服务即可。
## 迁移项目
在新项目安装好之后按照以下步骤完成项目迁移:
1、备份原数据库
```bash
# 在旧的项目下运行命令
./cmd mysql backup
```
2、将`数据库备份文件`及`public/uploads`目录拷贝至新项目
3、还原数据库至新项目
```bash
# 在新的项目下运行命令
./cmd mysql recovery
```
## 卸载项目
```bash
# 进入项目所在目录,运行以下命令
# 在项目下运行命令
./cmd uninstall
```
## 联系我们
QQ群号: 546574618

26
README_PUBLISH.md Normal file
View File

@@ -0,0 +1,26 @@
# 发布说明
## 发布前
1. 添加环境变量 `APPLEID``APPLEIDPASS` 用于公证
2. 添加环境变量 `CSC_LINK``CSC_KEY_PASSWORD` 用于签名
3. 添加环境变量 `GH_TOKEN``GH_REPOSITORY` 用于发布到GitHub
4. 添加环境变量 `DP_KEY` 用于发布到私有服务器
## 通过 GitHub Actions 发布
1. 执行 `npm run version` 生成版本
2. 执行 `npm run build` 编译前端
3. 执行 `git commit` 提交并推送
4. 添加并推送标签
## 本地发布
1. 执行 `npm run version` 生成版本
2. 执行 `npm run build` 编译前端
3. 执行 `./cmd electron` 相关操作
## 编译App
1. 执行 `./cmd appbuild``./cmd appbuild setting` 编译
2. 进入 `resources/mobile` eeui框架内打包Android或iOS应用

File diff suppressed because it is too large Load Diff

View File

@@ -67,7 +67,13 @@ class Handler extends ExceptionHandler
public function report(Throwable $e)
{
if ($e instanceof ApiException) {
Log::error($e->getMessage(), ['exception' => ' at ' . $e->getFile() .':' . $e->getLine()]);
if ($e->getCode() !== -1) {
Log::error($e->getMessage(), [
'code' => $e->getCode(),
'data' => $e->getData(),
'exception' => ' at ' . $e->getFile() . ':' . $e->getLine()
]);
}
} else {
parent::report($e);
}

View File

@@ -15,3 +15,10 @@ if (!function_exists('seeders_at')) {
return date("Y-m-d H:i:s", $time);
}
}
if (!function_exists('md5s')) {
function md5s($val, $len = 16)
{
return substr(md5($val), 32 - $len);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
namespace App\Http\Controllers\Api;
use App\Exceptions\ApiException;
use App\Models\AbstractModel;
use App\Models\File;
use App\Models\FileContent;
@@ -11,7 +11,9 @@ use App\Models\FileUser;
use App\Models\User;
use App\Module\Base;
use App\Module\Ihttp;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Redirect;
use Request;
/**
@@ -43,11 +45,12 @@ class FileController extends AbstractController
$pid = intval($data['pid']);
//
$permission = 1000;
$userids = $user->isTemp() ? [$user->userid] : [0, $user->userid];
$builder = File::wherePid($pid);
if ($pid > 0) {
File::permissionFind($pid, 0, $permission);
$builder = File::wherePid($pid);
File::permissionFind($pid, $userids, 0, $permission);
} else {
$builder = File::whereUserid($user->userid);
$builder->whereUserid($user->userid);
}
//
$array = $builder->take(500)->get()->toArray();
@@ -64,9 +67,25 @@ class FileController extends AbstractController
}
$pid = $file->pid;
$temp = $file->toArray();
$temp['permission'] = $file->getPermission($user->userid);
$temp['permission'] = $file->getPermission($userids);
$array[] = $temp;
}
// 去除没有权限的文件
$isUnset = false;
foreach ($array as $index1 => $item1) {
if ($item1['permission'] === -1) {
foreach ($array as $index2 => $item2) {
if ($item2['pid'] === $item1['id']) {
$array[$index2]['pid'] = 0;
}
}
$isUnset = true;
unset($array[$index1]);
}
}
if ($isUnset) {
$array = array_values($array);
}
} else {
// 获取共享相关
DB::statement("SET SQL_MODE=''");
@@ -74,10 +93,7 @@ class FileController extends AbstractController
$list = File::select(["files.*", DB::raw("MAX({$pre}file_users.permission) as permission")])
->join('file_users', 'files.id', '=', 'file_users.file_id')
->where('files.userid', '!=', $user->userid)
->where(function ($query) use ($user) {
$query->where('file_users.userid', 0);
$query->orWhere('file_users.userid', $user->userid);
})
->whereIn('file_users.userid', $userids)
->groupBy('files.id')
->take(100)
->get();
@@ -89,6 +105,10 @@ class FileController extends AbstractController
}
}
}
// 图片直接返回预览地址
foreach ($array as &$item) {
$item = File::handleImageUrl($item);
}
return Base::retSuccess('success', $array);
}
@@ -114,13 +134,18 @@ class FileController extends AbstractController
//
$permission = 0;
if (Base::isNumber($id)) {
User::auth();
$file = File::permissionFind(intval($id), 0, $permission);
$user = User::auth();
$file = File::permissionFind(intval($id), $user, 0, $permission);
} elseif ($id) {
$fileLink = FileLink::whereCode($id)->first();
$file = $fileLink?->file;
if (empty($file)) {
return Base::retError('链接不存在');
$msg = '文件链接不存在';
$data = File::code2IdName($id);
if ($data) {
$msg = "{$data->name}{$msg}";
}
return Base::retError($msg, $data);
}
} else {
return Base::retError('参数错误');
@@ -139,6 +164,7 @@ class FileController extends AbstractController
* @apiGroup file
* @apiName search
*
* @apiParam {String} [link] 通过分享地址搜索https://t.hitosea.com/single/file/ODcwOCwzOSxpa0JBS2lmVQ==
* @apiParam {String} [key] 关键词
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
@@ -149,15 +175,57 @@ class FileController extends AbstractController
{
$user = User::auth();
//
$link = trim(Request::input('link'));
$key = trim(Request::input('key'));
if (empty($key)) {
return Base::retError('请输入关键词');
$id = 0;
$take = 50;
if (preg_match("/\/single\/file\/(.*?)$/i", $link, $match)) {
$id = intval(FileLink::whereCode($match[1])->value('file_id'));
$take = 1;
if (empty($id)) {
return Base::retSuccess('success', []);
}
}
// 搜索自己的
$builder = File::whereUserid($user->userid);
if ($id) {
$builder->where("id", $id);
}
if ($key) {
$builder->where("name", "like", "%{$key}%");
}
$array = $builder->take($take)->get()->toArray();
// 搜索共享的
$take = $take - count($array);
if ($take > 0 && ($id || $key)) {
$builder = File::whereIn('pshare', function ($queryA) use ($user) {
$queryA->select('files.id')
->from('files')
->join('file_users', 'files.id', '=', 'file_users.file_id')
->where('files.userid', '!=', $user->userid)
->where(function ($queryB) use ($user) {
$queryB->whereIn('file_users.userid', [0, $user->userid]);
});
});
if ($id) {
$builder->where("id", $id);
}
if ($key) {
$builder->where("name", "like", "%{$key}%");
}
$list = $builder->take($take)->get();
if ($list->isNotEmpty()) {
foreach ($list as $file) {
$temp = $file->toArray();
if ($file->pshare === $file->id) {
$temp['pid'] = 0;
}
$array[] = $temp;
}
}
}
//
$builder = File::whereUserid($user->userid)->where("name", "like", "%{$key}%");
$list = $builder->take(50)->get();
//
return Base::retSuccess('success', $list);
return Base::retSuccess('success', $array);
}
/**
@@ -190,44 +258,61 @@ class FileController extends AbstractController
} elseif (mb_strlen($name) > 32) {
return Base::retError('文件名称最多只能设置32个字');
}
$tmpName = preg_replace("/[\\\\\/:*?\"<>|]/", '', $name);
if ($tmpName != $name) {
return Base::retError("文件名称不能包含这些字符:\/:*?\"<>|");
}
//
if ($id > 0) {
// 修改
$file = File::permissionFind($id, 1);
$file = File::permissionFind($id, $user, 1);
//
$file->name = $name;
$file->handleDuplicateName();
$file->save();
$file->pushMsg('update', $file);
return Base::retSuccess('修改成功', $file);
$data = [
'id' => $file->id,
'name' => $file->name,
];
$file->pushMsg('update', $data);
return Base::retSuccess('修改成功', $data);
} else {
// 添加
if (!in_array($type, [
'folder',
'document',
'mind',
'sheet',
'flow',
'drawio',
'word',
'excel',
'ppt',
])) {
return Base::retError('类型错误');
}
$ext = '';
if (in_array($type, [
$ext = str_replace([
'folder',
'document',
'mind',
'drawio',
'word',
'excel',
'ppt',
])) {
$ext = str_replace(['word', 'excel', 'ppt'], ['docx', 'xlsx', 'pptx'], $type);
}
], [
'',
'md',
'mind',
'drawio',
'docx',
'xlsx',
'pptx',
], $type);
//
$userid = $user->userid;
if ($pid > 0) {
if (File::wherePid($pid)->count() >= 300) {
return Base::retError('每个文件夹里最多只能创建300个文件或文件夹');
}
$row = File::permissionFind($pid, 1);
$row = File::permissionFind($pid, $user, 1);
$userid = $row->userid;
} else {
if (File::whereUserid($user->userid)->wherePid(0)->count() >= 300) {
@@ -243,7 +328,8 @@ class FileController extends AbstractController
'userid' => $userid,
'created_id' => $user->userid,
]);
$file->save();
$file->handleDuplicateName();
$file->saveBeforePP();
//
$data = File::find($file->id);
$data->pushMsg('add', $data);
@@ -271,7 +357,7 @@ class FileController extends AbstractController
//
$id = intval(Request::input('id'));
//
$row = File::permissionFind($id);
$row = File::permissionFind($id, $user);
//
$userid = $user->userid;
if ($row->pid > 0) {
@@ -293,9 +379,20 @@ class FileController extends AbstractController
'userid' => $userid,
'created_id' => $user->userid,
]);
$file->save();
$file->handleDuplicateName();
$data = AbstractModel::transaction(function() use ($file) {
$content = FileContent::select(['content', 'text', 'size'])->whereFid($file->cid)->orderByDesc('id')->first();
$file->size = $content?->size ?: 0;
$file->saveBeforePP();
if ($content) {
$content = $content->toArray();
$content['fid'] = $file->id;
$content['userid'] = $file->userid;
FileContent::createInstance($content)->save();
}
return File::find($file->id);
});
//
$data = File::find($file->id);
$data->pushMsg('add', $data);
return Base::retSuccess('复制成功', $data);
}
@@ -308,7 +405,7 @@ class FileController extends AbstractController
* @apiGroup file
* @apiName move
*
* @apiParam {Number} id 文件ID
* @apiParam {Numbers} ids 文件ID(格式:[id1, id2]
* @apiParam {Number} pid 移动到的文件夹ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
@@ -319,30 +416,60 @@ class FileController extends AbstractController
{
$user = User::auth();
//
$id = intval(Request::input('id'));
$ids = Request::input('ids');
$pid = intval(Request::input('pid'));
//
$file = File::permissionFind($id, 1000);
//
if (!is_array($ids) || empty($ids)) {
return Base::retError('请选择移动的文件或文件夹');
}
if (count($ids) > 100) {
return Base::retError('一次最多只能移动100个文件或文件夹');
}
$toShareFile = false;
if ($pid > 0) {
if (!File::whereUserid($user->userid)->whereId($pid)->exists()) {
return Base::retError('参数错误');
}
$arr = [];
$tid = $pid;
while ($tid > 0) {
$arr[] = $tid;
$tid = intval(File::whereId($tid)->value('pid'));
}
if (in_array($id, $arr)) {
return Base::retError('位置错误');
}
$tmpFile = File::permissionFind($pid, $user, 1);
$toShareFile = $tmpFile->getShareInfo();
}
//
$file->pid = $pid;
$file->save();
$file->pushMsg('update', $file);
return Base::retSuccess('操作成功', $file);
$files = [];
AbstractModel::transaction(function() use ($user, $pid, $ids, $toShareFile, &$files) {
foreach ($ids as $id) {
$file = File::permissionFind($id, $user, 1000);
//
if ($pid > 0) {
if ($toShareFile) {
if ($file->share) {
throw new ApiException("{$file->name} 当前正在共享,无法移动到另一个共享文件夹内");
}
if ($file->isSubShare()) {
throw new ApiException("{$file->name} 内含有共享文件,无法移动到另一个共享文件夹内");
}
$file->userid = $toShareFile->userid;
File::where('pids', 'LIKE', "%,{$file->id},%")->update(['userid' => $toShareFile->userid]);
}
//
$tmpId = $pid;
while ($tmpId > 0) {
if ($id == $tmpId) {
throw new ApiException('移动位置错误');
}
$tmpId = intval(File::whereId($tmpId)->value('pid'));
}
} else {
$file->userid = $user->userid;
File::where('pids', 'LIKE', "%,{$file->id},%")->update(['userid' => $user->userid]);
}
//
$file->pid = $pid;
$file->handleDuplicateName();
$file->saveBeforePP();
$files[] = $file;
}
});
foreach ($files as $file) {
$file->pushMsg('update', $file);
}
return Base::retSuccess('操作成功', $files);
}
/**
@@ -353,7 +480,7 @@ class FileController extends AbstractController
* @apiGroup file
* @apiName remove
*
* @apiParam {Number} id 文件ID
* @apiParam {Numbers} ids 文件ID(格式:[id1, id2]
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -361,14 +488,27 @@ class FileController extends AbstractController
*/
public function remove()
{
User::auth();
$user = User::auth();
//
$id = intval(Request::input('id'));
$ids = Request::input('ids');
//
$file = File::permissionFind($id, 1000);
if (!is_array($ids) || empty($ids)) {
return Base::retError('请选择删除的文件或文件夹');
}
if (count($ids) > 100) {
return Base::retError('一次最多只能删除100个文件或文件夹');
}
//
$file->deleteFile();
return Base::retSuccess('删除成功', $file);
$files = [];
AbstractModel::transaction(function() use ($user, $ids, &$files) {
foreach ($ids as $id) {
$file = File::permissionFind($id, $user, 1000);
$file->deleteFile();
$files[] = $file;
}
});
//
return Base::retSuccess('删除成功', $files);
}
/**
@@ -380,8 +520,16 @@ class FileController extends AbstractController
* @apiName content
*
* @apiParam {Number|String} id
* - Number 文件ID需要登录
* - String 链接码(不需要登录,用于预览)
* - Number: 文件ID需要登录
* - String: 链接码(不需要登录,用于预览)
* @apiParam {String} only_update_at 仅获取update_at字段
* - no (默认)
* - yes
* @apiParam {String} down 直接下载
* - no: 浏览(默认)
* - yes: 下载office文件直接下载除非是preview
* - preview: 转预览地址
* @apiParam {Number} [history_id] 读取历史记录ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -390,22 +538,44 @@ class FileController extends AbstractController
public function content()
{
$id = Request::input('id');
$down = Request::input('down', 'no');
$only_update_at = Request::input('only_update_at', 'no');
$history_id = intval(Request::input('history_id'));
//
if (Base::isNumber($id)) {
User::auth();
$file = File::permissionFind(intval($id));
$user = User::auth();
$file = File::permissionFind(intval($id), $user);
} elseif ($id) {
$fileLink = FileLink::whereCode($id)->first();
$file = $fileLink?->file;
if (empty($file)) {
return Base::retError('链接不存在');
$msg = '文件链接不存在';
$data = File::code2IdName($id);
if ($data) {
$msg = "{$data->name}{$msg}";
}
return Base::retError($msg, $data);
}
} else {
return Base::retError('参数错误');
}
//
$content = FileContent::whereFid($file->id)->orderByDesc('id')->first();
return FileContent::formatContent($file->type, $content ? $content->content : []);
if ($only_update_at == 'yes') {
return Base::retSuccess('success', [
'id' => $file->id,
'update_at' => Carbon::parse($file->updated_at)->toDateTimeString()
]);
}
//
$builder = FileContent::whereFid($file->id);
if ($history_id > 0) {
$builder->whereId($history_id);
}
$content = $builder->orderByDesc('id')->first();
if ($down === 'preview') {
return Redirect::to(FileContent::formatPreview($file, $content?->content));
}
return FileContent::formatContent($file, $content?->content, $down == 'yes');
}
/**
@@ -416,7 +586,7 @@ class FileController extends AbstractController
* @apiGroup file
* @apiName content__save
*
* @apiParam {Number} id 文件ID
* @apiParam {Number} id 文件ID
* @apiParam {Object} [D] Request Payload 提交
* - content: 内容
*
@@ -428,23 +598,23 @@ class FileController extends AbstractController
{
$user = User::auth();
//
$id = Base::getPostInt('id');
$content = Base::getPostValue('content');
$id = intval(Request::input('id'));
$content = Request::input('content');
//
$file = File::permissionFind($id, 1);
$file = File::permissionFind($id, $user, 1);
//
$text = '';
if ($file->type == 'document') {
$data = Base::json2array($content);
$isRep = false;
preg_match_all("/<img\s*src=\"data:image\/(png|jpg|jpeg);base64,(.*?)\"/s", $data['content'], $matchs);
preg_match_all("/<img\s+src=\"data:image\/(png|jpg|jpeg|webp);base64,(.*?)\"/s", $data['content'], $matchs);
foreach ($matchs[2] as $key => $text) {
$p = "uploads/files/document/" . $id . "/";
Base::makeDir(public_path($p));
$p.= md5($text) . "." . $matchs[1][$key];
$r = file_put_contents(public_path($p), base64_decode($text));
if ($r) {
$data['content'] = str_replace($matchs[0][$key], '<img src="' . Base::fillUrl($p) . '"', $data['content']);
$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))) {
$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;
}
}
@@ -454,11 +624,42 @@ class FileController extends AbstractController
}
}
//
switch ($file->type) {
case 'document':
$contentArray = Base::json2array($content);
$contentString = $contentArray['content'];
$file->ext = $contentArray['type'] == 'md' ? 'md' : 'text';
break;
case 'drawio':
$contentArray = Base::json2array($content);
$contentString = $contentArray['xml'];
$file->ext = 'drawio';
break;
case 'mind':
$contentString = $content;
$file->ext = 'mind';
break;
case 'txt':
case 'code':
$contentArray = Base::json2array($content);
$contentString = $contentArray['content'];
break;
default:
return Base::retError('参数错误');
}
$path = "uploads/file/" . $file->type . "/" . date("Ym") . "/" . $id . "/" . md5($contentString);
$save = public_path($path);
Base::makeDir(dirname($save));
file_put_contents($save, $contentString);
//
$content = FileContent::createInstance([
'fid' => $file->id,
'content' => $content,
'content' => [
'type' => $file->ext,
'url' => $path
],
'text' => $text,
'size' => strlen($content),
'size' => filesize($save),
'userid' => $user->userid,
]);
$content->save();
@@ -493,12 +694,12 @@ class FileController extends AbstractController
$key = Request::input('key');
$url = Request::input('url');
//
$file = File::permissionFind($id, 1);
$file = File::permissionFind($id, $user, 1);
//
if ($status === 2) {
$parse = parse_url($url);
$from = 'http://' . env('APP_IPPR') . '.3' . $parse['path'] . '?' . $parse['query'];
$path = 'uploads/office/' . date("Ym") . '/' . $file->id . '/' . $user->userid . '-' . $key;
$path = 'uploads/file/' . $file->type . '/' . date("Ym") . '/' . $file->id . '/' . $key;
$save = public_path($path);
Base::makeDir(dirname($save));
$res = Ihttp::download($from, $save);
@@ -516,6 +717,7 @@ class FileController extends AbstractController
$content->save();
//
$file->size = $content->size;
$file->updated_at = Carbon::now();
$file->save();
$file->pushMsg('update', $file);
}
@@ -531,7 +733,7 @@ class FileController extends AbstractController
* @apiGroup file
* @apiName content__upload
*
* @apiParam {Number} [pid] 父级ID
* @apiParam {Number} [pid] 父级ID
* @apiParam {String} [files] 文件名
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
@@ -543,13 +745,14 @@ class FileController extends AbstractController
$user = User::auth();
//
$pid = intval(Request::input('pid'));
$webkitRelativePath = Request::input('webkitRelativePath');
//
$userid = $user->userid;
if ($pid > 0) {
if (File::wherePid($pid)->count() >= 300) {
return Base::retError('每个文件夹里最多只能创建300个文件或文件夹');
}
$row = File::permissionFind($pid, 1);
$row = File::permissionFind($pid, $user, 1);
$userid = $row->userid;
} else {
if (File::whereUserid($user->userid)->wherePid(0)->count() >= 300) {
@@ -557,7 +760,38 @@ class FileController extends AbstractController
}
}
//
$path = 'uploads/office/' . date("Ym") . '/u' . $user->userid . '/';
$dirs = explode("/", $webkitRelativePath);
$addItem = [];
while (count($dirs) > 1) {
$dirName = array_shift($dirs);
if ($dirName) {
AbstractModel::transaction(function () use ($dirName, $user, $userid, &$pid, &$addItem) {
$dirRow = File::wherePid($pid)->whereType('folder')->whereName($dirName)->lockForUpdate()->first();
if (empty($dirRow)) {
$dirRow = File::createInstance([
'pid' => $pid,
'type' => 'folder',
'name' => $dirName,
'userid' => $userid,
'created_id' => $user->userid,
]);
$dirRow->handleDuplicateName();
if ($dirRow->saveBeforePP()) {
$addItem[] = File::find($dirRow->id);
}
}
if (empty($dirRow)) {
throw new ApiException('创建文件夹失败');
}
$pid = $dirRow->id;
});
foreach ($addItem as $tmpRow) {
$tmpRow->pushMsg('add', $tmpRow);
}
}
}
//
$path = 'uploads/tmp/' . date("Ym") . '/';
$data = Base::upload([
"file" => Request::file('files'),
"type" => 'more',
@@ -570,23 +804,36 @@ class FileController extends AbstractController
$data = $data['data'];
//
$type = match ($data['ext']) {
'text', 'md', 'markdown' => 'document',
'drawio' => 'drawio',
'mind' => 'mind',
'doc', 'docx' => "word",
'xls', 'xlsx' => "excel",
'ppt', 'pptx' => "ppt",
'wps' => "wps",
'jpg', 'jpeg', 'png', 'gif', 'bmp', 'ico', 'raw' => "picture",
'rar', 'zip', 'jar', '7-zip', 'tar', 'gzip', '7z' => "archive",
'jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp', 'ico', 'raw', 'svg' => "picture",
'rar', 'zip', 'jar', '7-zip', 'tar', 'gzip', '7z', 'gz', 'apk', 'dmg' => "archive",
'tif', 'tiff' => "tif",
'dwg', 'dxf' => "cad",
'ofd' => "ofd",
'pdf' => "pdf",
'txt' => "txt",
'html', 'htm', 'asp', 'jsp', 'xml', 'json', 'properties', 'md', 'gitignore', 'log', 'java', 'py', 'c', 'cpp', 'sql', 'sh', 'bat', 'm', 'bas', 'prg', 'cmd',
'php', 'go', 'python', 'js', 'ftl', 'css', 'lua', 'rb', 'yaml', 'yml', 'h', 'cs', 'aspx' => "code",
'htaccess', 'htgroups', 'htpasswd', 'conf', 'bat', 'cmd', 'cpp', 'c', 'cc', 'cxx', 'h', 'hh', 'hpp', 'ino', 'cs', 'css',
'dockerfile', 'go', 'golang', 'html', 'htm', 'xhtml', 'vue', 'we', 'wpy', 'java', 'js', 'jsm', 'jsx', 'json', 'jsp', 'less', 'lua', 'makefile', 'gnumakefile',
'ocamlmakefile', 'make', 'mysql', 'nginx', 'ini', 'cfg', 'prefs', 'm', 'mm', 'pl', 'pm', 'p6', 'pl6', 'pm6', 'pgsql', 'php',
'inc', 'phtml', 'shtml', 'php3', 'php4', 'php5', 'phps', 'phpt', 'aw', 'ctp', 'module', 'ps1', 'py', 'r', 'rb', 'ru', 'gemspec', 'rake', 'guardfile', 'rakefile',
'gemfile', 'rs', 'sass', 'scss', 'sh', 'bash', 'bashrc', 'sql', 'sqlserver', 'swift', 'ts', 'typescript', 'str', 'vbs', 'vb', 'v', 'vh', 'sv', 'svh', 'xml',
'rdf', 'rss', 'wsdl', 'xslt', 'atom', 'mathml', 'mml', 'xul', 'xbl', 'xaml', 'yaml', 'yml',
'asp', 'properties', 'gitignore', 'log', 'bas', 'prg', 'python', 'ftl', 'aspx', 'plist' => "code",
'mp3', 'wav', 'mp4', 'flv',
'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm' => "media",
'xmind' => "xmind",
'rp' => "axure",
default => "",
};
if ($data['ext'] == 'markdown') {
$data['ext'] = 'md';
}
$file = File::createInstance([
'pid' => $pid,
'name' => Base::rightDelete($data['name'], '.' . $data['ext']),
@@ -595,35 +842,117 @@ class FileController extends AbstractController
'userid' => $userid,
'created_id' => $user->userid,
]);
$file->handleDuplicateName();
// 开始创建
return AbstractModel::transaction(function () use ($type, $user, $data, $file) {
$file->save();
return AbstractModel::transaction(function () use ($addItem, $webkitRelativePath, $type, $user, $data, $file) {
$file->size = $data['size'] * 1024;
$file->saveBeforePP();
//
$data = Base::uploadMove($data, "uploads/file/" . $file->type . "/" . date("Ym") . "/" . $file->id . "/");
$content = [
'from' => '',
'type' => $type,
'ext' => $data['ext'],
'url' => $data['path'],
];
if (isset($data['width'])) {
$content['width'] = $data['width'];
$content['height'] = $data['height'];
}
$content = FileContent::createInstance([
'fid' => $file->id,
'content' => [
'from' => '',
'type' => $type,
'ext' => $data['ext'],
'url' => $data['path']
],
'content' => $content,
'text' => '',
'size' => $data['size'] * 1024,
'size' => $file->size,
'userid' => $user->userid,
]);
$content->save();
//
$file->size = $content->size;
$file->save();
$tmpRow = File::find($file->id);
$tmpRow->pushMsg('add', $tmpRow);
//
$data = File::find($file->id);
$data->pushMsg('add', $data);
return Base::retSuccess($data['name'] . ' 上传成功', $data);
$data = File::handleImageUrl($tmpRow->toArray());
$data['full_name'] = $webkitRelativePath ?: $data['name'];
//
$addItem[] = $data;
return Base::retSuccess($data['name'] . ' 上传成功', $addItem);
});
}
/**
* @api {get} api/file/share 12. 获取共享信息
* @api {get} api/file/content/history 12. 获取内容历史
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup file
* @apiName content__history
*
* @apiParam {Number} id 文件ID
*
* @apiParam {Number} [page] 当前页,默认:1
* @apiParam {Number} [pagesize] 每页显示数量,默认:20最大:100
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function content__history()
{
$user = User::auth();
//
$id = Request::input('id');
//
$file = File::permissionFind(intval($id), $user);
//
$data = FileContent::select(['id', 'size', 'userid', 'created_at'])
->whereFid($file->id)
->orderByDesc('id')
->paginate(Base::getPaginate(100, 20));
return Base::retSuccess('success', $data);
}
/**
* @api {get} api/file/content/restore 13. 恢复文件历史
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup file
* @apiName content__restore
*
* @apiParam {Number} id 文件ID
* @apiParam {Number} history_id 历史数据ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function content__restore()
{
$user = User::auth();
//
$id = intval(Request::input('id'));
$history_id = intval(Request::input('history_id'));
//
$file = File::permissionFind($id, $user);
//
$history = FileContent::whereFid($file->id)->whereId($history_id)->first();
if (empty($history)) {
return Base::retError('历史数据不存在或已被删除');
}
//
$content = $history->replicate();
$content->userid = $user->userid;
$content->save();
//
$file->size = $content->size;
$file->save();
$file->pushMsg('content');
//
return Base::retSuccess('还原成功');
}
/**
* @api {get} api/file/share 14. 获取共享信息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -659,7 +988,7 @@ class FileController extends AbstractController
}
/**
* @api {get} api/file/share/update 13. 设置共享
* @api {get} api/file/share/update 15. 设置共享
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -672,6 +1001,9 @@ class FileController extends AbstractController
* - 0只读
* - 1读写
* - -1: 删除
* @apiParam {Number} [force] 设置共享时是否忽略提醒
* - 0如果子文件夹已存在共享则ret返回-3001默认
* - 1忽略提醒
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -684,6 +1016,7 @@ class FileController extends AbstractController
$id = intval(Request::input('id'));
$userids = Request::input('userids');
$permission = intval(Request::input('permission'));
$force = intval(Request::input('force'));
//
if (!in_array($permission, [-1, 0, 1])) {
return Base::retError('参数错误');
@@ -697,8 +1030,10 @@ class FileController extends AbstractController
return Base::retError('仅限所有者操作');
}
//
if ($file->isNnShare()) {
return Base::retError('已经处于共享文件夹中');
$share = $file->isNnShare();
if ($share) {
$typeCn = $file->type === 'folder' ? '文件夹' : '文件';
return Base::retError("{$typeCn}已经处于【{$share->name}】共享文件夹中,无法重复共享");
}
//
if (!is_array($userids) || empty($userids)) {
@@ -710,16 +1045,18 @@ class FileController extends AbstractController
// 取消共享
$action = "delete";
foreach ($userids as $userid) {
if (FileUser::where([
'file_id' => $file->id,
'userid' => $userid,
])->delete()) {
if (FileUser::deleteFileUser($file->id, $userid)) {
$array[] = $userid;
}
}
} else {
// 设置共享
$action = "update";
if ($force === 0) {
if ($file->isSubShare()) {
return Base::retError('此文件夹内已有共享文件夹', [], -3001);
}
}
if (FileUser::whereFileId($file->id)->count() + count($userids) > 100) {
return Base::retError('共享人数上限100个成员');
}
@@ -735,13 +1072,13 @@ class FileController extends AbstractController
}
}
//
$file->setShare();
$file->updataShare();
$file->pushMsg($action, $action == "delete" ? null : $file, $array);
return Base::retSuccess($action == "delete" ? "删除成功" : "设置成功", $file);
}
/**
* @api {get} api/file/share/out 14. 退出共享
* @api {get} api/file/share/out 16. 退出共享
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -760,28 +1097,22 @@ class FileController extends AbstractController
//
$id = intval(Request::input('id'));
//
$file = File::permissionFind($id);
$file = File::permissionFind($id, $user);
//
if ($file->userid == $user->userid) {
return Base::retError('不能退出自己共享的文件');
}
if (FileUser::where([
'file_id' => $file->id,
'userid' => 0,
])->exists()) {
if (FileUser::whereFileId($file->id)->whereUserid(0)->exists()) {
return Base::retError('无法退出共享所有人的文件或文件夹');
}
FileUser::where([
'file_id' => $file->id,
'userid' => $user->userid,
])->delete();
FileUser::deleteFileUser($file->id, $user->userid);
//
$file->setShare();
$file->updataShare();
return Base::retSuccess("退出成功");
}
/**
* @api {get} api/file/link 15. 获取链接
* @api {get} api/file/link 17. 获取链接
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -799,33 +1130,14 @@ class FileController extends AbstractController
*/
public function link()
{
User::auth();
$user = User::auth();
//
$id = intval(Request::input('id'));
$refresh = Request::input('refresh', 'no');
//
$file = File::permissionFind($id, 1000);
if ($file->type == 'folder') {
return Base::retError('文件夹暂不支持此功能');
}
$file = File::permissionFind($id, $user);
$fileLink = $file->getShareLink($user->userid, $refresh == 'yes');
//
$fileLink = FileLink::whereFileId($file->id)->first();
if (empty($fileLink)) {
$fileLink = FileLink::createInstance([
'file_id' => $file->id,
'code' => Base::generatePassword(64),
]);
$fileLink->save();
} else {
if ($refresh == 'yes') {
$fileLink->code = Base::generatePassword(64);
$fileLink->save();
}
}
return Base::retSuccess('success', [
'id' => $file->id,
'url' => Base::fillUrl('single/file/' . $fileLink->code),
'num' => $fileLink->num
]);
return Base::retSuccess('success', $fileLink);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,179 @@
<?php
namespace App\Http\Controllers\Api;
use App\Models\User;
use App\Models\UserCheckinMac;
use App\Models\UserCheckinRecord;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Module\Base;
use Cache;
use Carbon\Carbon;
use Request;
/**
* @apiDefine public
*
* 公开
*/
class PublicController extends AbstractController
{
/**
* 签到 - 路由器openwrt功能安装脚本
*
* @apiParam {String} key
*
* @return string
*/
public function checkin__install()
{
$key = trim(Request::input('key'));
//
$setting = Base::setting('checkinSetting');
if ($setting['open'] !== 'open') {
return <<<EOF
#!/bin/sh
echo "function off"
EOF;
}
if ($key != $setting['key']) {
return <<<EOF
#!/bin/sh
echo "key error"
EOF;
}
//
$reportUrl = Base::fillUrl("api/public/checkin/report");
return <<<EOE
#!/bin/sh
echo 'installing...'
cat > /etc/init.d/dootask-checkin-report <<EOF
#!/bin/sh
mac=\\\$(awk 'NR!=1&&\\\$3=="0x2" {print \\\$4}' /proc/net/arp | tr "\\n" ",")
tmp='{"key":"{$setting['key']}","mac":"'\\\${mac}'","time":"'\\\$(date +%s)'"}'
curl -4 -X POST "{$reportUrl}" -H "Content-Type: application/json" -d \\\${tmp}
EOF
chmod +x /etc/init.d/dootask-checkin-report
crontab -l >/tmp/cronbak
sed -i '/\/etc\/init.d\/dootask-checkin-report/d' /tmp/cronbak
sed -i '/^$/d' /tmp/cronbak
echo "* * * * * sh /etc/init.d/dootask-checkin-report" >>/tmp/cronbak
crontab /tmp/cronbak
rm -f /tmp/cronbak
/etc/init.d/cron enable
/etc/init.d/cron restart
echo 'installed'
EOE;
}
/**
* {post} 签到 - 路由器openwrt上报
*
* @apiParam {String} key
* @apiParam {String} mac 使用逗号分割多个
* @apiParam {String} time
*
* @return string
*/
public function checkin__report()
{
$key = trim(Request::input('key'));
$mac = trim(Request::input('mac'));
$time = intval(Request::input('time'));
//
$setting = Base::setting('checkinSetting');
if ($setting['open'] !== 'open') {
return 'function off';
}
if ($key != $setting['key']) {
return 'key error';
}
$times = $setting['time'] ? Base::json2array($setting['time']) : ['09:00', '18:00'];
$advance = (intval($setting['advance']) ?: 120) * 60;
$delay = (intval($setting['delay']) ?: 120) * 60;
//
$nowDate = date("Y-m-d");
$nowTime = date("H:i:s");
//
$timeStart = strtotime("{$nowDate} {$times[0]}");
$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 "not in valid time, valid time is " . date("H:i", $timeAdvance) . "-" . date("H:i", $timeDelay);
}
//
$macs = explode(",", $mac);
$checkins = [];
foreach ($macs as $mac) {
$mac = strtoupper($mac);
if (Base::isMac($mac) && $UserCheckinMac = UserCheckinMac::whereMac($mac)->first()) {
$checkins[] = $UserCheckinMac;
$array = [
'userid' => $UserCheckinMac->userid,
'mac' => $UserCheckinMac->mac,
'date' => $nowDate,
];
$record = UserCheckinRecord::where($array)->first();
if (empty($record)) {
$record = UserCheckinRecord::createInstance($array);
}
$record->times = Base::array2json(array_merge($record->times, [$nowTime]));
$record->report_time = $time;
$record->save();
}
}
//
if ($checkins && $botUser = User::botGetOrCreate('check-in')) {
$getJokeSoup = function($type) {
$pre = $type == "up" ? "每日开心:" : "心灵鸡汤:";
$key = $type == "up" ? "JokeSoupTask:jokes" : "JokeSoupTask:soups";
$array = Base::json2array(Cache::get($key));
if ($array) {
$item = $array[array_rand($array)];
if ($item) {
return $pre . $item;
}
}
return null;
};
$sendMsg = function($type, UserCheckinMac $checkin) use ($getJokeSoup, $botUser, $nowDate) {
$cacheKey = "Checkin::sendMsg-{$nowDate}-{$type}:" . $checkin->userid;
if (Cache::get($cacheKey) === "yes") {
return;
}
Cache::put($cacheKey, "yes", Carbon::now()->addDay());
//
$dialog = WebSocketDialog::checkUserDialog($botUser, $checkin->userid);
if ($dialog) {
$hi = date("H:i");
$pre = $type == "up" ? "上班" : "下班";
$remark = $checkin->remark ? " ({$checkin->remark})": "";
$text = "<p>{$pre}打卡成功,打卡时间: {$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");
}
};
if ($timeAdvance <= Base::time() && Base::time() < $timeEnd) {
// 上班打卡通知(从最早打卡时间 到 下班打卡时间)
foreach ($checkins as $checkin) {
$sendMsg('up', $checkin);
}
}
if ($timeEnd <= Base::time() && Base::time() <= $timeDelay) {
// 下班打卡通知(下班打卡时间 到 最晚打卡时间)
foreach ($checkins as $checkin) {
$sendMsg('down', $checkin);
}
}
}
return 'success';
}
}

View File

@@ -0,0 +1,521 @@
<?php
namespace App\Http\Controllers\Api;
use App\Exceptions\ApiException;
use App\Models\AbstractModel;
use App\Models\ProjectTask;
use App\Models\Report;
use App\Models\ReportReceive;
use App\Models\User;
use App\Module\Base;
use App\Module\Doo;
use App\Tasks\PushTask;
use Carbon\Carbon;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Validation\Rule;
use Request;
use Illuminate\Support\Facades\Validator;
/**
* @apiDefine report
*
* 汇报
*/
class ReportController extends AbstractController
{
/**
* @api {get} api/report/my 01. 我发送的汇报
*
* @apiVersion 1.0.0
* @apiGroup report
* @apiName my
*
* @apiParam {Object} [keys] 搜索条件
* - keys.type: 汇报类型weekly:周报daily:日报
* - keys.created_at: 汇报时间
* @apiParam {Number} [page] 当前页,默认:1
* @apiParam {Number} [pagesize] 每页显示数量,默认:20最大:50
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function my(): array
{
$user = User::auth();
//
$builder = Report::with(['receivesUser'])->whereUserid($user->userid);
$keys = Request::input('keys');
if (is_array($keys)) {
if (in_array($keys['type'], [Report::WEEKLY, Report::DAILY])) {
$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])));
}
}
$list = $builder->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20));
return Base::retSuccess('success', $list);
}
/**
* @api {get} api/report/receive 02. 我接收的汇报
*
* @apiVersion 1.0.0
* @apiGroup report
* @apiName receive
*
* @apiParam {Object} [keys] 搜索条件
* - keys.key: 关键词
* - keys.type: 汇报类型weekly:周报daily:日报
* - keys.created_at: 汇报时间
* @apiParam {Number} [page] 当前页,默认:1
* @apiParam {Number} [pagesize] 每页显示数量,默认:20最大:50
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function receive(): array
{
$user = User::auth();
$builder = Report::with(['receivesUser']);
$builder->whereHas("receivesUser", function ($query) use ($user) {
$query->where("report_receives.userid", $user->userid);
});
$keys = Request::input('keys');
if (is_array($keys)) {
if ($keys['key']) {
$builder->where(function($query) use ($keys) {
$query->whereHas('sendUser', function ($q2) use ($keys) {
$q2->where("users.email", "LIKE", "%{$keys['key']}%");
})->orWhere("title", "LIKE", "%{$keys['key']}%");
});
}
if (in_array($keys['type'], [Report::WEEKLY, Report::DAILY])) {
$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])));
}
}
$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");
}
}
return Base::retSuccess('success', $list);
}
/**
* @api {get} api/report/store 03. 保存并发送工作汇报
*
* @apiVersion 1.0.0
* @apiGroup report
* @apiName store
*
* @apiParam {Number} id 汇报ID0为新建
* @apiParam {String} [sign] 唯一签名,通过[api/report/template]接口返回
* @apiParam {String} title 汇报标题
* @apiParam {Array} type 汇报类型weekly:周报daily:日报
* @apiParam {Number} content 内容
* @apiParam {Number} [receive] 汇报对象
* @apiParam {Number} offset 时间偏移量
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function store(): array
{
$user = User::auth();
//
$input = [
"id" => Request::input("id", 0),
"sign" => Request::input("sign"),
"title" => Request::input("title"),
"type" => Request::input("type"),
"content" => Request::input("content"),
"receive" => Request::input("receive"),
// 以当前日期为基础的周期偏移量。例如选择了上一周那么就是 -1上一天同理。
"offset" => Request::input("offset", 0),
];
$validator = Validator::make($input, [
'id' => 'numeric',
'title' => 'required',
'type' => ['required', Rule::in([Report::WEEKLY, Report::DAILY])],
'content' => 'required',
'offset' => ['numeric', 'max:0'],
], [
'id.numeric' => 'ID只能是数字',
'title.required' => '请填写标题',
'type.required' => '请选择汇报类型',
'type.in' => '汇报类型错误',
'content.required' => '请填写汇报内容',
'offset.numeric' => '工作汇报周期格式错误,只能是数字',
'offset.max' => '只能提交当天/本周或者之前的的工作汇报',
]);
if ($validator->fails())
return Base::retError($validator->errors()->first());
// 接收人
if (is_array($input["receive"])) {
// 删除当前登录人
$input["receive"] = array_diff($input["receive"], [$user->userid]);
// 查询用户是否存在
if (count($input["receive"]) !== User::whereIn("userid", $input["receive"])->count())
return Base::retError("用户不存在");
foreach ($input["receive"] as $userid) {
$input["receive_content"][] = [
"receive_time" => Carbon::now()->toDateTimeString(),
"userid" => $userid,
"read" => 0,
];
}
}
// 在事务中运行
return AbstractModel::transaction(function () use ($input, $user) {
$id = $input["id"];
if ($id) {
// 编辑
$report = Report::getOne($id);
$report->updateInstance([
"title" => $input["title"],
"type" => $input["type"],
"content" => htmlspecialchars($input["content"]),
]);
} else {
// 生成唯一标识
$sign = Base::isNumber($input["sign"]) ? $input["sign"] : Report::generateSign($input["type"], $input["offset"]);
// 检查唯一标识是否存在
if (empty($input["id"]) && Report::query()->whereSign($sign)->whereType($input["type"])->count() > 0) {
throw new ApiException("请勿重复提交工作汇报");
}
$report = Report::createInstance([
"sign" => $sign,
"title" => $input["title"],
"type" => $input["type"],
"userid" => $user->userid,
"content" => htmlspecialchars($input["content"]),
]);
}
$report->save();
// 删除关联
$report->Receives()->delete();
if ($input["receive_content"]) {
// 保存接收人
$report->Receives()->createMany($input["receive_content"]);
}
// 推送消息
$userids = [];
foreach ($input["receive_content"] as $item) {
$userids[] = $item['userid'];
}
if ($userids) {
$params = [
'ignoreFd' => Request::header('fd'),
'userid' => $userids,
'msg' => [
'type' => 'report',
'action' => 'unreadUpdate',
]
];
Task::deliver(new PushTask($params, false));
}
//
return Base::retSuccess('保存成功', $report);
});
}
/**
* @api {get} api/report/template 04. 生成汇报模板
*
* @apiVersion 1.0.0
* @apiGroup report
* @apiName template
*
* @apiParam {Array} [type] 汇报类型weekly:周报daily:日报
* @apiParam {Number} [offset] 偏移量
* @apiParam {String} [date] 时间
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function template(): array
{
$user = User::auth();
$type = trim(Request::input("type"));
$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();
if ($offset > 0) {
// 将当前时间调整为偏移量当天结束
$now_dt->subDays($offset)->endOfDay();
// 开始时间偏移量计算
$start_time->subDays($offset);
}
$end_time = Carbon::instance($start_time)->endOfDay();
} else {
$start_time = Carbon::now();
if ($offset > 0) {
// 将当前时间调整为偏移量当周结束
$now_dt->subWeeks($offset)->endOfDay();
// 开始时间偏移量计算
$start_time->subWeeks($offset);
}
$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', [
"id" => $one->id,
"sign" => $one->sign,
"title" => $one->title,
"content" => $one->content,
]);
}
// 已完成的任务
$completeContent = "";
$complete_task = ProjectTask::query()
->whereNotNull("complete_at")
->whereBetween("complete_at", [$start_time->toDateTimeString(), $end_time->toDateTimeString()])
->whereHas("taskUser", function ($query) use ($user) {
$query->where("userid", $user->userid);
})
->orderByDesc("id")
->get();
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>";
}
} else {
$completeContent = '<li>&nbsp;</li>';
}
// 未完成的任务
$unfinishedContent = "";
$unfinished_task = ProjectTask::query()
->whereNull("complete_at")
->whereNotNull("start_at")
->where("end_at", "<", $end_time->toDateTimeString())
->whereHas("taskUser", function ($query) use ($user) {
$query->where("userid", $user->userid);
})
->orderByDesc("id")
->get();
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>";
}
} else {
$unfinishedContent = '<li>&nbsp;</li>';
}
// 生成标题
if ($type === Report::WEEKLY) {
$title = $user->nickname . "的周报[" . $start_time->format("m/d") . "-" . $end_time->format("m/d") . "]";
$title .= "[" . $start_time->month . "月第" . $start_time->weekOfMonth . "周]";
} else {
$title = $user->nickname . "的日报[" . $start_time->format("Y/m/d") . "]";
}
// 生成内容
$content = '<h2>' . Doo::translate('已完成工作') . '</h2><ol>' .
$completeContent . '</ol><h2>' .
Doo::translate('未完成的工作') . '</h2><ol>' .
$unfinishedContent . '</ol>';
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>";
}
$data = [
"time" => $start_time->toDateTimeString(),
"sign" => $sign,
"title" => $title,
"content" => $content,
"complete_task" => $complete_task,
"unfinished_task" => $unfinished_task,
];
if ($one) {
$data['id'] = $one->id;
}
return Base::retSuccess('success', $data);
}
/**
* @api {get} api/report/detail 05. 报告详情
*
* @apiVersion 1.0.0
* @apiGroup report
* @apiName detail
*
* @apiParam {Number} [id] 报告id
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function detail(): array
{
$user = User::auth();
$id = intval(trim(Request::input("id")));
if (empty($id))
return Base::retError("缺少ID参数");
$one = Report::getOne($id);
$one->type_val = $one->getRawOriginal("type");
// 标记为已读
if (!empty($one->receivesUser)) {
foreach ($one->receivesUser as $item) {
if ($item->userid === $user->userid && $item->pivot->read === 0) {
$one->receivesUser()->updateExistingPivot($user->userid, [
"read" => 1,
]);
}
}
}
return Base::retSuccess("success", $one);
}
/**
* @api {get} api/report/mark 06. 标记已读/未读
*
* @apiVersion 1.0.0
* @apiGroup report
* @apiName mark
*
* @apiParam {Number} id 报告id
* @apiParam {Number} action 操作
* - read: 标记已读(默认)
* - unread: 标记未读
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function mark(): array
{
$user = User::auth();
//
$id = Request::input('id');
$action = Request::input('action');
//
if (is_array($id)) {
if (count(Base::arrayRetainInt($id)) > 100) {
return Base::retError("最多只能操作100条数据");
}
$builder = Report::whereIn("id", Base::arrayRetainInt($id));
} else {
$builder = Report::whereId(intval($id));
}
$builder ->chunkById(100, function ($list) use ($action, $user) {
/** @var Report $item */
foreach ($list as $item) {
$item->receivesUser()->updateExistingPivot($user->userid, [
"read" => $action === 'unread' ? 0 : 1,
]);
}
});
return Base::retSuccess("操作成功");
}
/**
* @api {get} api/report/last_submitter 07. 获取最后一次提交的接收人
*
* @apiVersion 1.0.0
* @apiGroup report
* @apiName last_submitter
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function last_submitter(): array
{
$one = Report::getLastOne();
return Base::retSuccess("success", empty($one["receives"]) ? [] : $one["receives"]);
}
/**
* @api {get} api/report/unread 08. 获取未读
*
* @apiVersion 1.0.0
* @apiGroup report
* @apiName unread
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function unread(): array
{
$user = User::auth();
//
$data = Report::whereHas("Receives", function (Builder $query) use ($user) {
$query->where("userid", $user->userid)->where("read", 0);
})->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20));
return Base::retSuccess("success", $data);
}
/**
* @api {get} api/report/read 09. 标记汇报已读,可批量
*
* @apiVersion 1.0.0
* @apiGroup report
* @apiName read
*
* @apiParam {String} [ids] 报告id
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function read(): array
{
$user = User::auth();
$ids = Request::input("ids");
if (!is_array($ids) && !is_string($ids)) {
return Base::retError("请传入正确的工作汇报Id");
}
if (is_string($ids)) {
$ids = Base::explodeInt($ids);
}
$data = Report::with(["receivesUser" => function (BelongsToMany $query) use ($user) {
$query->where("report_receives.userid", $user->userid)->where("read", 0);
}])->whereIn("id", $ids)->get();
if ($data->isNotEmpty()) {
foreach ($data as $item) {
(!empty($item->receivesUser) && $item->receivesUser->isNotEmpty()) && $item->receivesUser()->updateExistingPivot($user->userid, [
"read" => 1,
]);
}
}
return Base::retSuccess("success", $data);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,11 +2,25 @@
namespace App\Http\Controllers;
use App\Models\File;
use App\Module\Base;
use App\Module\Extranet;
use App\Module\RandomColor;
use App\Tasks\AppPushTask;
use App\Tasks\AutoArchivedTask;
use App\Tasks\CheckinRemindTask;
use App\Tasks\DeleteBotMsgTask;
use App\Tasks\DeleteTmpTask;
use App\Tasks\EmailNoticeTask;
use App\Tasks\JokeSoupTask;
use App\Tasks\LoopTask;
use Arr;
use Cache;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use LasseRafn\InitialAvatarGenerator\InitialAvatar;
use Redirect;
use Request;
use Response;
/**
@@ -22,6 +36,10 @@ class IndexController extends InvokeController
if ($action) {
$app .= "__" . $action;
}
if ($app === 'manifest.txt') {
$app = 'manifest';
$child = 'txt';
}
if (!method_exists($this, $app)) {
$app = method_exists($this, $method) ? $method : 'main';
}
@@ -30,11 +48,121 @@ class IndexController extends InvokeController
/**
* 首页
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @return \Illuminate\Http\Response
*/
public function main()
{
return view('main', ['version' => Base::getVersion()]);
$hotFile = public_path('hot');
$manifestFile = public_path('manifest.json');
if (file_exists($hotFile)) {
$array = Base::json2array(file_get_contents($hotFile));
$style = null;
$script = preg_replace("/^(\/\/(.*?))(:\d+)?\//i", "$1:" . $array['APP_DEV_PORT'] . "/", asset_main("resources/assets/js/app.js"));
} else {
$array = Base::json2array(file_get_contents($manifestFile));
$style = asset_main($array['resources/assets/js/app.js']['css'][0]);
$script = asset_main($array['resources/assets/js/app.js']['file']);
}
return response()->view('main', [
'version' => Base::getVersion(),
'style' => $style,
'script' => $script,
])->header('Link', "<" . url('manifest.txt') . ">; rel=\"prefetch\"");
}
/**
* Manifest
* @param $child
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response|string
*/
public function manifest($child = '')
{
if (empty($child)) {
$murl = url('manifest.txt');
return response($murl)->header('Link', "<{$murl}>; rel=\"prefetch\"");
}
$array = [
"office/web-apps/apps/api/documents/api.js?hash=" . Base::getVersion(),
"office/7.3.2-8/web-apps/vendor/requirejs/require.js",
"office/7.3.2-8/web-apps/apps/api/documents/api.js",
"office/7.3.2-8/sdkjs/common/AllFonts.js",
"office/7.3.2-8/web-apps/vendor/xregexp/xregexp-all-min.js",
"office/7.3.2-8/web-apps/vendor/sockjs/sockjs.min.js",
"office/7.3.2-8/web-apps/vendor/jszip/jszip.min.js",
"office/7.3.2-8/web-apps/vendor/jszip-utils/jszip-utils.min.js",
"office/7.3.2-8/sdkjs/common/libfont/wasm/fonts.js",
"office/7.3.2-8/sdkjs/common/Charts/ChartStyles.js",
"office/7.3.2-8/sdkjs/slide/themes//themes.js",
"office/7.3.2-8/web-apps/apps/presentationeditor/main/app.js",
"office/7.3.2-8/sdkjs/slide/sdk-all-min.js",
"office/7.3.2-8/sdkjs/slide/sdk-all.js",
"office/7.3.2-8/web-apps/apps/documenteditor/main/app.js",
"office/7.3.2-8/sdkjs/word/sdk-all-min.js",
"office/7.3.2-8/sdkjs/word/sdk-all.js",
"office/7.3.2-8/web-apps/apps/spreadsheeteditor/main/app.js",
"office/7.3.2-8/sdkjs/cell/sdk-all-min.js",
"office/7.3.2-8/sdkjs/cell/sdk-all.js",
];
foreach ($array as &$item) {
$item = url($item);
}
return implode(PHP_EOL, $array);
}
/**
* 获取版本号
* @return \Illuminate\Http\RedirectResponse
*/
public function version()
{
return Redirect::to(Base::fillUrl('api/system/version'), 301);
}
/**
* 头像
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response
*/
public function avatar()
{
$segment = Request::segment(2);
if ($segment && preg_match('/.*?\.png$/i', $segment)) {
$name = substr($segment, 0, -4);
} else {
$name = Request::input('name', 'H');
}
$size = Request::input('size', 128);
$color = Request::input('color');
$background = Request::input('background');
//
if (preg_match('/^[\x{4e00}-\x{9fa5}]+$/u', $name)) {
$name = mb_substr($name, mb_strlen($name) - 2);
}
if (empty($color)) {
$color = '#ffffff';
$cacheKey = "avatarBackgroundColor::" . md5($name);
$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);
//
return response($content)
->header('Pragma', 'public')
->header('Cache-Control', 'max-age=1814400')
->header('Content-type', 'image/png')
->header('Expires', gmdate('D, d M Y H:i:s \G\M\T', time() + 1814400));
}
/**
@@ -56,15 +184,180 @@ class IndexController extends InvokeController
// 限制内网访问
return "Forbidden Access";
}
// 自动归档
Task::deliver(new AutoArchivedTask());
// 邮件通知
Task::deliver(new EmailNoticeTask());
// App推送
Task::deliver(new AppPushTask());
// 删除过期的临时表数据
Task::deliver(new DeleteTmpTask('wg_tmp_msgs', 1));
Task::deliver(new DeleteTmpTask('tmp', 24));
// 自动归档任务
Task::deliver(new AutoArchivedTask());
Task::deliver(new DeleteTmpTask('task_worker', 12));
Task::deliver(new DeleteTmpTask('tmp'));
Task::deliver(new DeleteTmpTask('file'));
// 删除机器人消息
Task::deliver(new DeleteBotMsgTask());
// 周期任务
Task::deliver(new LoopTask());
// 签到提醒
Task::deliver(new CheckinRemindTask());
// 获取笑话/心灵鸡汤
Task::deliver(new JokeSoupTask());
return "success";
}
/**
* 桌面客户端发布
*/
public function desktop__publish($name = '')
{
$publishVersion = Request::header('publish-version');
$latestFile = public_path("uploads/desktop/latest");
$latestVersion = file_exists($latestFile) ? trim(file_get_contents($latestFile)) : "0.0.1";
if (strtolower($name) === 'latest') {
$name = $latestVersion;
}
// 上传
if (preg_match("/^\d+\.\d+\.\d+$/", $publishVersion)) {
$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" => 'desktop',
"path" => $publishPath,
"fileName" => true
]);
if (Base::isSuccess($res)) {
file_put_contents($latestFile, $publishVersion);
}
return $res;
}
}
// 列表
if (preg_match("/^\d+\.\d+\.\d+$/", $name)) {
$path = "uploads/desktop/{$name}";
$dirPath = public_path($path);
$lists = Base::readDir($dirPath);
$files = [];
foreach ($lists as $file) {
if (str_ends_with($file, '.yml') || str_ends_with($file, '.yaml')) {
continue;
}
$fileName = Base::leftDelete($file, $dirPath);
$files[] = [
'name' => substr($fileName, 1),
'time' => date("Y-m-d H:i:s", filemtime($file)),
'size' => Base::readableBytes(filesize($file)),
'url' => Base::fillUrl($path . $fileName),
];
}
//
$path = "uploads/android";
$dirPath = public_path($path);
$lists = Base::readDir($dirPath);
$apkFile = null;
foreach ($lists as $file) {
if (!str_ends_with($file, '.apk')) {
continue;
}
if ($apkFile && strtotime($apkFile['time']) > filemtime($file)) {
continue;
}
$fileName = Base::leftDelete($file, $dirPath);
$apkFile = [
'name' => substr($fileName, 1),
'time' => date("Y-m-d H:i:s", filemtime($file)),
'size' => Base::readableBytes(filesize($file)),
'url' => Base::fillUrl($path . $fileName),
];
}
if ($apkFile) {
$files = array_merge([$apkFile], $files);
}
return view('desktop', ['version' => $name, 'files' => $files]);
}
// 下载
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);
}
}
}
return abort(404);
}
/**
* Drawio 图标搜索
* @return array|mixed
*/
public function drawio__iconsearch()
{
$query = trim(Request::input('q'));
$page = trim(Request::input('p'));
$size = trim(Request::input('c'));
return Extranet::drawioIconSearch($query, $page, $size);
}
/**
* 预览文件
* @return array|mixed
*/
public function online__preview()
{
$key = trim(Request::input('key'));
//
$data = parse_url($key);
$path = Arr::get($data, 'path');
$file = public_path($path);
//
if (file_exists($file)) {
parse_str($data['query'], $query);
$name = Arr::get($query, 'name');
$ext = strtolower(Arr::get($query, 'ext'));
$userAgent = strtolower(Request::server('HTTP_USER_AGENT'));
if ($ext === 'pdf'
&& (str_contains($userAgent, 'electron') || str_contains($userAgent, 'chrome'))) {
return Response::download($file, $name, [
'Content-Type' => 'application/pdf'
], 'inline');
}
//
if (in_array($ext, File::localExt)) {
$url = Base::fillUrl($path);
} else {
$url = 'http://' . env('APP_IPPR') . '.3/' . $path;
}
if ($ext !== 'pdf') {
$url = Base::urlAddparameter($url, [
'fullfilename' => $name . '.' . $ext
]);
}
$toUrl = Base::fillUrl("fileview/onlinePreview?url=" . urlencode(base64_encode($url)));
return Redirect::to($toUrl, 301);
}
return abort(404);
}
/**
* 设置语言和皮肤
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function setting__theme_language()
{
return view('setting', [
'theme' => Request::input('theme'),
'language' => Request::input('language')
]);
}
/**
* 提取所有中文
* @return array|string
@@ -126,9 +419,6 @@ class IndexController extends InvokeController
$list = array_merge(Base::readDir(app_path()), Base::readDir(resource_path()));
$array = [];
foreach ($list as $item) {
if (Base::rightExists($item, "language.all.js")) {
continue;
}
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);

View File

@@ -12,37 +12,10 @@ class VerifyCsrfToken extends Middleware
* @var array
*/
protected $except = [
// 上传图片
'api/system/imgupload/',
// 接口部分
'api/*',
// 上传文件
'api/system/fileupload/',
// 保存任务优先级
'api/system/priority/',
// 添加任务
'api/project/task/add/',
// 保存工作流
'api/project/flow/save/',
// 修改任务
'api/project/task/update/',
// 上传任务问题
'api/project/task/upload/',
// 聊天发文件
'api/dialog/msg/sendfile/',
// 保存文件内容
'api/file/content/save/',
// 保存文件内容office
'api/file/content/office/',
// 保存文件内容(上传)
'api/file/content/upload/',
// 发布桌面端
'desktop/publish/',
];
}

View File

@@ -4,8 +4,9 @@ namespace App\Http\Middleware;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Module\Base;
use App\Module\Doo;
use Closure;
use Request;
class WebApi
{
@@ -21,17 +22,41 @@ class WebApi
global $_A;
$_A = [];
if (Request::input('__Access-Control-Allow-Origin') || Request::header('__Access-Control-Allow-Origin')) {
header('Access-Control-Allow-Origin:*');
header('Access-Control-Allow-Methods:GET,POST,PUT,DELETE,OPTIONS');
header('Access-Control-Allow-Headers:Content-Type, platform, platform-channel, token, release, Access-Control-Allow-Origin');
Doo::load();
$encrypt = Doo::pgpParseStr($request->header('encrypt'));
if ($request->isMethod('post')) {
$version = $request->header('version');
if ($version && version_compare($version, '0.25.48', '<')) {
// 旧版本兼容 php://input
parse_str($request->getContent(), $content);
if ($content) {
$request->merge($content);
}
} elseif ($encrypt['encrypt_type'] === 'pgp' && $content = $request->input('encrypted')) {
// 新版本解密提交的内容
$content = Doo::pgpDecryptApi($content, $encrypt['encrypt_id']);
if ($content) {
$request->merge($content);
}
}
}
// 强制 https
$APP_SCHEME = env('APP_SCHEME', 'auto');
if (in_array(strtolower($APP_SCHEME), ['https', 'on', 'ssl', '1', 'true', 'yes'], true)) {
$request->setTrustedProxies([$request->getClientIp()], $request::HEADER_X_FORWARDED_PROTO);
}
return $next($request);
$response = $next($request);
// 加密返回内容
if ($encrypt['client_type'] === 'pgp' && $content = $response->getContent()) {
$response->setContent(json_encode([
'encrypted' => Doo::pgpEncryptApi($content, $encrypt['client_key'])
]));
}
return $response;
}
}

251
app/Ldap/LdapUser.php Normal file
View File

@@ -0,0 +1,251 @@
<?php
namespace App\Ldap;
use App\Models\User;
use App\Module\Base;
use LdapRecord\Configuration\ConfigurationException;
use LdapRecord\Container;
use LdapRecord\LdapRecordException;
use LdapRecord\Models\Model;
class LdapUser extends Model
{
protected static $init = null;
/**
* The object classes of the LDAP model.
*
* @var array
*/
public static $objectClasses = [
'inetOrgPerson',
'organizationalPerson',
'person',
'top',
'posixAccount',
];
/**
* @return mixed|null
*/
public function getPhoto()
{
return $this->jpegPhoto && is_array($this->jpegPhoto) ? $this->jpegPhoto[0] : null;
}
/**
* @return mixed|null
*/
public function getDisplayName()
{
$nickname = $this->displayName ?: $this->uid;
return is_array($nickname) ? $nickname[0] : $nickname;
}
/**
* @return LdapUser
*/
public static function static(): LdapUser
{
return new static;
}
/**
* 服务是否打开
* @return bool
*/
public static function isOpen(): bool
{
return Base::settingFind('thirdAccessSetting', 'ldap_open') === 'open';
}
/**
* 同步本地是否打开
* @return bool
*/
public static function isSyncLocal(): bool
{
return Base::settingFind('thirdAccessSetting', 'ldap_sync_local') === 'open';
}
/**
* 初始化配置
* @return bool
*/
public static function initConfig()
{
if (is_bool(self::$init)) {
return self::$init;
}
//
$setting = Base::setting('thirdAccessSetting');
if ($setting['ldap_open'] !== 'open') {
return self::$init = false;
}
//
$connection = Container::getDefaultConnection();
try {
$connection->setConfiguration([
"hosts" => [$setting['ldap_host']],
"port" => intval($setting['ldap_port']),
"base_dn" => $setting['ldap_base_dn'],
"username" => $setting['ldap_user_dn'],
"password" => $setting['ldap_password'],
]);
return self::$init = true;
} catch (ConfigurationException $e) {
info($e->getMessage());
return self::$init = false;
}
}
/**
* 获取
* @param $username
* @param $password
* @return Model|null
*/
public static function userFirst($username, $password): ?Model
{
if (!self::initConfig()) {
return null;
}
try {
return self::static()
->where([
'cn' => $username,
'userPassword' => $password
])->first();
} catch (\Exception) {
return null;
}
}
/**
* 登录
* @param $username
* @param $password
* @param User|null $user
* @return User|mixed|null
*/
public static function userLogin($username, $password, $user = null)
{
if (!self::initConfig()) {
return null;
}
$row = self::userFirst($username, $password);
if (!$row) {
return null;
}
if (empty($user)) {
$user = User::reg($username, $password);
}
if ($user) {
$userimg = $row->getPhoto();
if ($userimg) {
$path = "uploads/user/ldap/";
$file = "{$path}{$user->userid}.jpeg";
Base::makeDir(public_path($path));
if (Base::saveContentImage(public_path($file), $userimg)) {
$user->userimg = $file;
}
}
$user->nickname = $row->getDisplayName();
$user->save();
}
return $user;
}
/**
* 同步
* @param User $user
* @param $password
* @return void
*/
public static function userSync(User $user, $password)
{
if ($user->isLdap()) {
return;
}
//
if (!self::initConfig()) {
return;
}
//
if (self::isSyncLocal()) {
$row = self::userFirst($user->email, $password);
if ($row) {
return;
}
try {
$userimg = public_path($user->getRawOriginal('userimg'));
if (file_exists($userimg)) {
$userimg = file_get_contents($userimg);
} else {
$userimg = '';
}
self::static()->create([
'cn' => $user->email,
'gidNumber' => 0,
'homeDirectory' => '/home/ldap/dootask/' . env("APP_NAME"),
'sn' => $user->email,
'uid' => $user->email,
'uidNumber' => $user->userid,
'userPassword' => $password,
'displayName' => $user->nickname,
'jpegPhoto' => $userimg,
]);
$user->identity = Base::arrayImplode(array_merge(array_diff($user->identity, ['ldap']), ['ldap']));
$user->save();
} catch (LdapRecordException $e) {
info("[LDAP] sync fail: " . $e->getMessage());
}
}
}
/**
* 更新
* @param $username
* @param $array
* @return void
*/
public static function userUpdate($username, $array)
{
if (empty($array)) {
return;
}
if (!self::initConfig()) {
return;
}
try {
$row = self::static()
->where([
'cn' => $username,
])->first();
$row?->update($array);
} catch (\Exception $e) {
info("[LDAP] update fail: " . $e->getMessage());
}
}
/**
* 删除
* @param $username
* @return void
*/
public static function userDelete($username)
{
if (!self::initConfig()) {
return;
}
try {
$row = self::static()
->where([
'cn' => $username,
])->first();
$row?->delete();
} catch (\Exception $e) {
info("[LDAP] delete fail: " . $e->getMessage());
}
}
}

View File

@@ -10,7 +10,7 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
/**
* App\Model\AbstractModel
* App\Models\AbstractModel
*
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel newQuery()
@@ -18,9 +18,13 @@ use Illuminate\Support\Facades\DB;
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
* @method static \Illuminate\Database\Eloquent\Model|object|static|null cancelAppend()
* @method static \Illuminate\Database\Eloquent\Model|object|static|null cancelHidden()
* @method static \Illuminate\Database\Eloquent\Builder|static with($relations)
* @method static \Illuminate\Database\Query\Builder|static select($columns = [])
* @method static \Illuminate\Database\Query\Builder|static whereIn($column, $values, $boolean = 'and', $not = false)
* @method static \Illuminate\Database\Query\Builder|static whereNotIn($column, $values, $boolean = 'and')
* @method int change(array $array)
* @method int remove()
* @mixin \Eloquent
*/
class AbstractModel extends Model
@@ -37,6 +41,42 @@ class AbstractModel extends Model
protected $appendattrs = [];
/**
* 通过模型修改数据
* @param AbstractModel $builder
* @param $array
* @return int
*/
protected function scopeChange($builder, $array)
{
$line = 0;
$rows = $builder->get();
foreach ($rows as $row) {
$row->updateInstance($array);
if ($row->save()) {
$line++;
}
}
return $line;
}
/**
* 通过模型删除数据
* @param AbstractModel $builder
* @return int
*/
protected function scopeRemove($builder)
{
$line = 0;
$rows = $builder->get();
foreach ($rows as $row) {
if ($row->delete()) {
$line++;
}
}
return $line;
}
/**
* 保存数据忽略错误
* @return bool
@@ -45,7 +85,7 @@ class AbstractModel extends Model
{
try {
return $this->save();
} catch (\Exception $e) {
} catch (\Throwable) {
return false;
}
}
@@ -72,6 +112,15 @@ class AbstractModel extends Model
return $this->setAppends([]);
}
/**
* 取消隐藏值
* @return static
*/
protected function scopeCancelHidden()
{
return $this->setHidden([]);
}
/**
* 为数组 / JSON 序列化准备日期。
* @param DateTimeInterface $date
@@ -143,9 +192,10 @@ class AbstractModel extends Model
* @param $where
* @param array $update 存在时更新的内容
* @param array $insert 不存在时插入的内容,如果没有则插入更新内容
* @param bool $isInsert 是否是插入数据
* @return AbstractModel|\Illuminate\Database\Eloquent\Builder|Model|object|static|null
*/
public static function updateInsert($where, $update = [], $insert = [])
public static function updateInsert($where, $update = [], $insert = [], &$isInsert = true)
{
$row = static::where($where)->first();
if (empty($row)) {
@@ -155,8 +205,10 @@ class AbstractModel extends Model
unset($array[$row->primaryKey]);
}
$row->updateInstance($array);
$isInsert = true;
} elseif ($update) {
$row->updateInstance($update);
$isInsert = false;
}
if (!$row->save()) {
return null;

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Models;
/**
* App\Models\ApproveProcMsg
*
* @property int $id
* @property string|null $type 信息类型candidate-候选人、participant-参与人、notifier-抄送人)
* @property int|null $proc_inst_id 流程实例ID
* @property int|null $userid 会员ID
* @property int|null $msg_id 消息ID
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg query()
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereMsgId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereProcInstId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereUserid($value)
* @mixin \Eloquent
*/
class ApproveProcMsg extends AbstractModel
{
}

91
app/Models/Deleted.php Normal file
View File

@@ -0,0 +1,91 @@
<?php
namespace App\Models;
use Carbon\Carbon;
/**
* App\Models\Deleted
*
* @property int $id
* @property string|null $type 删除的数据类型project、task、dialog
* @property int|null $did 删除的数据ID
* @property int|null $userid 关系会员ID
* @property \Illuminate\Support\Carbon|null $created_at
* @method static \Illuminate\Database\Eloquent\Builder|Deleted newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Deleted newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Deleted query()
* @method static \Illuminate\Database\Eloquent\Builder|Deleted whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Deleted whereDid($value)
* @method static \Illuminate\Database\Eloquent\Builder|Deleted whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Deleted whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder|Deleted whereUserid($value)
* @mixin \Eloquent
*/
class Deleted extends AbstractModel
{
const UPDATED_AT = null;
/**
* 获取删除的ID
* @param $type
* @param $userid
* @param $time
* @return array
*/
public static function ids($type, $userid, $time): array
{
$builder = self::where([
'type' => $type,
'userid' => $userid
])->orderByDesc('id');
if (empty($time)) {
$builder = $builder->take(50);
} else {
$builder = $builder->where('created_at', '>=', Carbon::parse($time))->take(500);
}
return $builder->pluck('did')->toArray();
}
/**
* 忘记(恢复或添加数据时删除记录)
* @param $type
* @param $id
* @param $userid
* @return void
*/
public static function forget($type, $id, $userid): void
{
if (is_array($userid)) {
self::where([
'type' => $type,
'did' => $id,
])->whereIn('userid', $userid)->delete();
} else {
self::where([
'type' => $type,
'did' => $id,
'userid' => $userid,
])->delete();
}
}
/**
* 记录(删除数据时添加记录)
* @param $type
* @param $id
* @param $userid
* @return void
*/
public static function record($type, $id, $userid): void
{
$array = is_array($userid) ? $userid : [$userid];
foreach ($array as $value) {
self::updateInsert([
'type' => $type,
'did' => $id,
'userid' => $value,
]);
}
}
}

View File

@@ -14,6 +14,7 @@ use Request;
*
* @property int $id
* @property int|null $pid 上级ID
* @property string|null $pids 上级ID递归
* @property int|null $cid 复制ID
* @property string|null $name 名称
* @property string|null $type 类型
@@ -21,13 +22,14 @@ use Request;
* @property int|null $size 大小(B)
* @property int|null $userid 拥有者ID
* @property int|null $share 是否共享
* @property int|null $pshare 所属分享ID
* @property int|null $created_id 创建者
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @method static \Illuminate\Database\Eloquent\Builder|File newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|File newQuery()
* @method static \Illuminate\Database\Query\Builder|File onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|File onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|File query()
* @method static \Illuminate\Database\Eloquent\Builder|File whereCid($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereCreatedAt($value)
@@ -37,13 +39,15 @@ use Request;
* @method static \Illuminate\Database\Eloquent\Builder|File whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|File wherePid($value)
* @method static \Illuminate\Database\Eloquent\Builder|File wherePids($value)
* @method static \Illuminate\Database\Eloquent\Builder|File wherePshare($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereShare($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereSize($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereUserid($value)
* @method static \Illuminate\Database\Query\Builder|File withTrashed()
* @method static \Illuminate\Database\Query\Builder|File withoutTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|File withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|File withoutTrashed()
* @mixin \Eloquent
*/
class File extends AbstractModel
@@ -51,22 +55,59 @@ class File extends AbstractModel
use SoftDeletes;
/**
* 是否有访问权限
* @param $userid
* @return int -1:没有权限0:访问权限1:读写权限1000:所有者
* 文件文件
*/
public function getPermission($userid)
const codeExt = [
'txt',
'htaccess', 'htgroups', 'htpasswd', 'conf', 'bat', 'cmd', 'cpp', 'c', 'cc', 'cxx', 'h', 'hh', 'hpp', 'ino', 'cs', 'css',
'dockerfile', 'go', 'golang', 'html', 'htm', 'xhtml', 'vue', 'we', 'wpy', 'java', 'js', 'jsm', 'jsx', 'json', 'jsp', 'less', 'lua', 'makefile', 'gnumakefile',
'ocamlmakefile', 'make', 'mysql', 'nginx', 'ini', 'cfg', 'prefs', 'm', 'mm', 'pl', 'pm', 'p6', 'pl6', 'pm6', 'pgsql', 'php',
'inc', 'phtml', 'shtml', 'php3', 'php4', 'php5', 'phps', 'phpt', 'aw', 'ctp', 'module', 'ps1', 'py', 'r', 'rb', 'ru', 'gemspec', 'rake', 'guardfile', 'rakefile',
'gemfile', 'rs', 'sass', 'scss', 'sh', 'bash', 'bashrc', 'sql', 'sqlserver', 'swift', 'ts', 'typescript', 'str', 'vbs', 'vb', 'v', 'vh', 'sv', 'svh', 'xml',
'rdf', 'rss', 'wsdl', 'xslt', 'atom', 'mathml', 'mml', 'xul', 'xbl', 'xaml', 'yaml', 'yml',
'asp', 'properties', 'gitignore', 'log', 'bas', 'prg', 'python', 'ftl', 'aspx', 'plist'
];
/**
* office文件
*/
const officeExt = [
'doc', 'docx',
'xls', 'xlsx',
'ppt', 'pptx',
];
/**
* 图片文件
*/
const imageExt = [
'jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp'
];
/**
* 本地媒体文件
*/
const localExt = [
'jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp', 'ico', 'raw',
'tif', 'tiff',
'mp3', 'wav', 'mp4', 'flv',
'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm',
];
/**
* 是否有访问权限
* @param array $userids
* @return int -1:没有权限0:访问权限1:读写权限1000:所有者或创建者
*/
public function getPermission(array $userids)
{
if ($userid == $this->userid) {
// ① 自己的文件夹
if (in_array($this->userid, $userids) || in_array($this->created_id, $userids)) {
// ① 自己的文件夹 或 自己创建的文件夹
return 1000;
}
$row = $this->getShareInfo();
if ($row) {
$fileUser = FileUser::whereFileId($row->id)->where(function ($query) use ($userid) {
$query->where('userid', 0);
$query->orWhere('userid', $userid);
})->orderByDesc('permission')->first();
$fileUser = FileUser::whereFileId($row->id)->whereIn('userid', $userids)->orderByDesc('permission')->first();
if ($fileUser) {
// ② 在指定共享成员内
return $fileUser->permission;
@@ -100,7 +141,7 @@ class File extends AbstractModel
/**
* 是否处于共享文件夹内(不含自身)
* @return bool
* @return File|false
*/
public function isNnShare()
{
@@ -111,19 +152,28 @@ class File extends AbstractModel
break;
}
if ($row->share) {
return true;
return $row;
}
$pid = $row->pid;
}
return false;
}
/**
* 目录内是否存在共享文件或文件夹
* @return bool
*/
public function isSubShare()
{
return $this->type == 'folder' && File::where("pids", "like", "%,{$this->id},%")->whereShare(1)->exists();
}
/**
* 设置/关闭 共享(同时遍历取消里面的共享)
* @param $share
* @return bool
*/
public function setShare($share = null)
public function updataShare($share = null)
{
if ($share === null) {
$share = FileUser::whereFileId($this->id)->count() == 0 ? 0 : 1;
@@ -131,11 +181,16 @@ class File extends AbstractModel
if ($this->share != $share) {
AbstractModel::transaction(function () use ($share) {
$this->share = $share;
$this->pshare = $share ? $this->id : 0;
$this->save();
File::where("pids", "like", "%,{$this->id},%")->update(['pshare' => $share ? $this->id : 0]);
if ($share === 0) {
FileUser::deleteFileAll($this->id, $this->userid);
}
$list = self::wherePid($this->id)->get();
if ($list->isNotEmpty()) {
foreach ($list as $item) {
$item->setShare(0);
$item->updataShare(0);
}
}
});
@@ -143,6 +198,75 @@ class File extends AbstractModel
return true;
}
/**
* 处理重名
* @return void
*/
public function handleDuplicateName()
{
$builder = self::wherePid($this->pid)->whereUserid($this->userid)->whereExt($this->ext);
$exist = $builder->clone()->whereName($this->name)->exists();
if (!$exist) {
return; // 未重名,不需要处理
}
// 发现重名,自动重命名
$nextNum = 2;
if (preg_match("/(.*?)(\s+\(\d+\))*$/", $this->name)) {
$preName = preg_replace("/(.*?)(\s+\(\d+\))*$/", "$1", $this->name);
$nextNum = $builder->clone()->where("name", "LIKE", "{$preName}%")->count() + 1;
}
$newName = "{$this->name} ({$nextNum})";
if ($builder->clone()->whereName($newName)->exists()) {
$nextNum = rand(100, 9999);
$newName = "{$this->name} ({$nextNum})";
}
$this->name = $newName;
}
/**
* 保存前更新pids/pshare
* @return bool
*/
public function saveBeforePP()
{
$pid = $this->pid;
$pshare = $this->share ? $this->id : 0;
$array = [];
while ($pid > 0) {
$array[] = $pid;
$file = self::select(['id', 'pid', 'share'])->find($pid);
if ($file) {
$pid = $file->pid;
if ($file->share) {
$pshare = $file->id;
}
} else {
$pid = 0;
}
}
$opids = $this->pids;
if ($array) {
$array = array_values(array_reverse($array));
$this->pids = ',' . implode(',', $array) . ',';
} else {
$this->pids = '';
}
$this->pshare = $pshare;
if (!$this->save()) {
return false;
}
// 更新子文件(夹)
if ($opids != $this->pids) {
self::wherePid($this->id)->chunkById(100, function ($lists) {
/** @var self $item */
foreach ($lists as $item) {
$item->saveBeforePP();
}
});
}
return true;
}
/**
* 遍历删除文件(夹)
* @return bool
@@ -152,8 +276,7 @@ class File extends AbstractModel
AbstractModel::transaction(function () {
$this->delete();
$this->pushMsg('delete');
FileLink::whereFileId($this->id)->delete();
FileUser::whereFileId($this->id)->delete();
FileUser::deleteFileAll($this->id);
FileContent::whereFid($this->id)->delete();
$list = self::wherePid($this->id)->get();
if ($list->isNotEmpty()) {
@@ -165,6 +288,50 @@ class File extends AbstractModel
return true;
}
/**
* 强制删除文件
* @return true
*/
public function forceDeleteFile()
{
AbstractModel::transaction(function () {
$this->forceDelete();
FileContent::withTrashed()
->whereFid($this->id)
->orderBy('id')
->chunk(500, function ($contents) {
/** @var FileContent $content */
foreach ($contents as $content) {
$content->forceDeleteContent();
}
});
});
return true;
}
/**
* 获取文件分享链接
* @param $userid
* @param $refresh
* @return array
*/
public function getShareLink($userid, $refresh = false)
{
if ($this->type == 'folder') {
throw new ApiException('文件夹不支持分享');
}
return FileLink::generateLink($this->id, $userid, $refresh);
}
/**
* 获取文件名称加后缀
* @return string|null
*/
public function getNameAndExt()
{
return $this->ext ? "{$this->name}.{$this->ext}" : $this->name;
}
/**
* 推送消息
* @param $action
@@ -179,19 +346,7 @@ class File extends AbstractModel
];
}
//
if ($userid === null) {
$userid = [$this->userid];
if ($this->share == 1) {
$builder = WebSocket::select(['userid']);
if ($action == 'content') {
$builder->wherePath('file/content/' . $this->id);
}
$userid = array_merge($userid, $builder->pluck('userid')->toArray());
} elseif ($this->share == 2) {
$userid = array_merge($userid, FileUser::whereFileId($this->id)->pluck('userid')->toArray());
}
$userid = array_values(array_filter(array_unique($userid)));
}
$userid = $this->pushUserid($action, $userid);
if (empty($userid)) {
return;
}
@@ -215,28 +370,238 @@ class File extends AbstractModel
}
/**
* 获取文件并检测权限
* @param $id
* @param int $limit 要求权限: 0-访问权限、1-读写权限、1000-所有者
* @param $permission
* 获取推送会员
* @param $action
* @param $userid
* @return array|int[]|mixed|null[]
*/
public function pushUserid($action, $userid = null) {
$wherePath = "/manage/file";
if ($userid === null) {
$array = [$this->userid];
if ($action == 'add' && $this->pid == 0) {
return $array;
}
if ($action == 'content') {
$wherePath = "/single/file/{$this->id}";
} elseif ($this->pid > 0) {
$wherePath = "/manage/file/{$this->pid}";
} else {
$tmpArray = FileUser::whereFileId($this->id)->pluck('userid')->toArray();
if (empty($tmpArray)) {
return $array;
}
if (!in_array(0, $tmpArray)) {
return $tmpArray;
}
}
$tmpArray = WebSocket::wherePath($wherePath)->pluck('userid')->toArray();
if (empty($tmpArray)) {
return $array;
}
$array = array_values(array_filter(array_unique(array_merge($array, $tmpArray))));
} else {
$array = is_array($userid) ? $userid : [$userid];
if (in_array(0, $array)) {
return WebSocket::wherePath($wherePath)->pluck('userid')->toArray();
}
}
return $array;
}
/**
* code获取文件ID、名称
* @param $code
* @return File
*/
public static function permissionFind($id, $limit = 0, &$permission = -1)
public static function code2IdName($code) {
$arr = explode(",", base64_decode($code));
if (empty($arr)) {
return null;
}
$fileId = intval($arr[0]);
if (empty($fileId)) {
return null;
}
return File::select(['id', 'name'])->find($fileId);
}
/**
* 处理返回图片地址
* @param array $item
* @return array
*/
public static function handleImageUrl($item)
{
if (in_array($item['ext'], self::imageExt) ) {
$content = Base::json2array(FileContent::whereFid($item['id'])->orderByDesc('id')->value('content'));
if ($content) {
$item['image_url'] = Base::fillUrl($content['url']);
$item['image_width'] = intval($content['width']);
$item['image_height'] = intval($content['height']);
}
}
return $item;
}
/**
* 获取文件并检测权限
* @param int $id
* @param User|array|int $user 要求权限的用户,如:[0, 1]
* @param int $limit 要求权限: 0-访问权限、1-读写权限、1000-所有者或创建者
* @param int $permission
* @return File
*/
public static function permissionFind(int $id, $user, int $limit = 0, int &$permission = -1)
{
$file = File::find($id);
if (empty($file)) {
throw new ApiException('文件不存在或已被删除');
}
//
$permission = $file->getPermission(User::userid());
if ($user instanceof User) {
$userids = $user->isTemp() ? [$user->userid] : [0, $user->userid];
} else {
$userids = is_array($user) ? $user : [$user];
}
$permission = $file->getPermission($userids);
if ($permission < $limit) {
$msg = match ($limit) {
1000 => '仅限所有者操作',
1 => '没有读写权限',
default => '没有访问权限',
1000 => '仅限所有者或创建者操作',
1 => '没有修改写入权限',
default => '没有查看访问权限',
};
throw new ApiException($msg);
}
return $file;
}
/**
* 格式化内容数据
* @param array $data [path, size, ext, name]
* @return array
*/
public static function formatFileData(array $data)
{
$fileName = $data['name'];
$filePath = $data['path'];
$fileSize = $data['size'];
$fileExt = $data['ext'];
$publicPath = public_path($filePath);
//
switch ($fileExt) {
case 'md':
case 'text':
// 文本
$data['content'] = [
'type' => $fileExt,
'content' => file_get_contents($publicPath) ?: 'Content deleted',
];
$data['file_mode'] = $fileExt;
break;
case 'drawio':
// 图表
$data['content'] = [
'xml' => file_get_contents($publicPath)
];
$data['file_mode'] = $fileExt;
break;
case 'mind':
// 思维导图
$data['content'] = Base::json2array(file_get_contents($publicPath));
$data['file_mode'] = $fileExt;
break;
default:
if (in_array($fileExt, self::codeExt) && $fileSize < 2 * 1024 * 1024)
{
// 文本预览限制2M内的文件
$data['content'] = [
'content' => file_get_contents($publicPath) ?: 'Content deleted',
];
$data['file_mode'] = 'code';
}
elseif (in_array($fileExt, File::officeExt))
{
// office预览
$data['content'] = json_decode('{}');
$data['file_mode'] = 'office';
}
else
{
// 其他预览
$name = Base::rightDelete($fileName, ".{$fileExt}") . ".{$fileExt}";
$data['content'] = [
'preview' => true,
'name' => $name,
'key' => urlencode(Base::urlAddparameter($filePath, [
'name' => $name,
'ext' => $fileExt
])),
];
$data['file_mode'] = 'preview';
}
break;
}
return $data;
}
/**
* 移交文件
* @param $originalUserid
* @param $newUserid
* @return void
*/
public static function transfer($originalUserid, $newUserid)
{
if (!self::whereUserid($originalUserid)->exists()) {
return;
}
// 创建一个文件夹存放移交的文件
$name = User::userid2nickname($originalUserid) ?: ('ID:' . $originalUserid);
$file = File::createInstance([
'pid' => 0,
'name' => "{$name}】移交的文件",
'type' => "folder",
'ext' => "",
'userid' => $newUserid,
'created_id' => 0,
]);
$file->handleDuplicateName();
$file->saveBeforePP();
// 移交文件
self::whereUserid($originalUserid)->chunkById(100, function($list) use ($file, $newUserid) {
/** @var self $item */
foreach ($list as $item) {
if ($item->pid === 0) {
$item->pid = $file->id;
}
$item->userid = $newUserid;
$item->saveBeforePP();
}
});
// 移交文件权限
FileUser::whereUserid($originalUserid)->chunkById(100, function ($list) use ($newUserid) {
/** @var FileUser $item */
foreach ($list as $item) {
$row = FileUser::whereFileId($item->file_id)->whereUserid($newUserid)->first();
if ($row) {
// 已存在则删除原数据,判断改变已存在的数据
$row->permission = max($row->permission, $item->permission);
$row->save();
$item->delete();
} else {
// 不存在则改变原数据
$item->userid = $newUserid;
$item->save();
}
}
});
}
}

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use App\Module\Base;
use Illuminate\Database\Eloquent\SoftDeletes;
use Response;
/**
* App\Models\FileContent
@@ -21,7 +20,7 @@ use Response;
* @property \Illuminate\Support\Carbon|null $deleted_at
* @method static \Illuminate\Database\Eloquent\Builder|FileContent newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|FileContent newQuery()
* @method static \Illuminate\Database\Query\Builder|FileContent onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|FileContent onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|FileContent query()
* @method static \Illuminate\Database\Eloquent\Builder|FileContent whereContent($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileContent whereCreatedAt($value)
@@ -32,8 +31,8 @@ use Response;
* @method static \Illuminate\Database\Eloquent\Builder|FileContent whereText($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileContent whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileContent whereUserid($value)
* @method static \Illuminate\Database\Query\Builder|FileContent withTrashed()
* @method static \Illuminate\Database\Query\Builder|FileContent withoutTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|FileContent withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|FileContent withoutTrashed()
* @mixin \Eloquent
*/
class FileContent extends AbstractModel
@@ -41,43 +40,115 @@ class FileContent extends AbstractModel
use SoftDeletes;
/**
* 获取格式内容
* @param $type
* @param $content
* @return array|\Symfony\Component\HttpFoundation\BinaryFileResponse
* 强制删除文件内容
* @return void
*/
public static function formatContent($type, $content)
public function forceDeleteContent()
{
$content = Base::json2array($content);
if (in_array($type, ['word', 'excel', 'ppt'])) {
if (empty($content)) {
return Response::download(resource_path('assets/statics/office/empty.' . str_replace(['word', 'excel', 'ppt'], ['docx', 'xlsx', 'pptx'], $type)));
$this->forceDelete();
$content = Base::json2array($this->content ?: []);
if (str_starts_with($content['url'], 'uploads/')) {
$path = public_path($content['url']);
if (file_exists($path)) {
@unlink($path);
}
return Response::download(public_path($content['url']));
}
}
/**
* 转预览地址
* @param array $array
* @return string
*/
public static function toPreviewUrl($array)
{
$fileExt = $array['ext'];
$fileName = $array['name'];
$filePath = $array['path'];
$name = Base::rightDelete($fileName, ".{$fileExt}") . ".{$fileExt}";
$key = urlencode(Base::urlAddparameter($filePath, [
'name' => $name,
'ext' => $fileExt
]));
return Base::fillUrl("online/preview/{$name}?key={$key}");
}
/**
* 转预览地址
* @param File $file
* @param $content
* @return string
*/
public static function formatPreview($file, $content)
{
$content = Base::json2array($content ?: []);
$filePath = $content['url'];
if (in_array($file->type, ['word', 'excel', 'ppt'])) {
if (empty($content)) {
$filePath = 'assets/office/empty.' . str_replace(['word', 'excel', 'ppt'], ['docx', 'xlsx', 'pptx'], $file->type);
}
}
return self::toPreviewUrl([
'ext' => $file->ext,
'name' => $file->name,
'path' => $filePath,
]);
}
/**
* 获取格式内容(或下载)
* @param File $file
* @param $content
* @param $download
* @return array|\Symfony\Component\HttpFoundation\StreamedResponse
*/
public static function formatContent($file, $content, $download = false)
{
$name = $file->ext ? "{$file->name}.{$file->ext}" : null;
$content = Base::json2array($content ?: []);
if (in_array($file->type, ['word', 'excel', 'ppt'])) {
if (empty($content)) {
$filePath = public_path('assets/office/empty.' . str_replace(['word', 'excel', 'ppt'], ['docx', 'xlsx', 'pptx'], $file->type));
} else {
$filePath = public_path($content['url']);
}
return Base::streamDownload(function() use ($filePath) {
echo file_get_contents($filePath);
}, $name);
}
if (empty($content)) {
$content = match ($type) {
$content = match ($file->type) {
'document' => [
"type" => "md",
"type" => $file->ext,
"content" => "",
],
'sheet' => [
[
"name" => "Sheet1",
"config" => json_decode('{}'),
]
],
default => json_decode('{}'),
};
if ($download) {
abort(403, "This file is empty.");
}
} else {
$content['preview'] = false;
if ($content['ext'] && !in_array($content['ext'], ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'])) {
$url = 'http://' . env('APP_IPPR') . '.3/' . $content['url'];
if (in_array($type, ['picture', 'image', 'tif', 'media'])) {
$url = Base::fillUrl($content['url']);
$path = $content['url'];
if ($file->ext) {
$res = File::formatFileData([
'path' => $path,
'ext' => $file->ext,
'size' => $file->size,
'name' => $file->name,
]);
$content = $res['content'];
} else {
$content['preview'] = false;
}
if ($download) {
$filePath = public_path($path);
if (isset($filePath)) {
return Base::streamDownload(function() use ($filePath) {
echo file_get_contents($filePath);
}, $name);
} else {
abort(403, "This file not support download.");
}
$content['url'] = base64_encode($url);
$content['preview'] = true;
}
}
return Base::retSuccess('success', [ 'content' => $content ]);

View File

@@ -2,13 +2,16 @@
namespace App\Models;
use App\Module\Base;
/**
* App\Models\FileLink
*
* @property int $id
* @property int|null $file_id 项目ID
* @property int|null $file_id 文件ID
* @property int|null $num 累计访问
* @property string|null $code 链接码
* @property int|null $userid 会员ID
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\File|null $file
@@ -21,6 +24,7 @@ namespace App\Models;
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereNum($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereUserid($value)
* @mixin \Eloquent
*/
class FileLink extends AbstractModel
@@ -32,4 +36,35 @@ class FileLink extends AbstractModel
{
return $this->hasOne(File::class, 'id', 'file_id');
}
/**
* 生成链接
* @param $fileId
* @param $userid
* @param $refresh
* @return array
*/
public static function generateLink($fileId, $userid, $refresh = false)
{
$fileLink = FileLink::whereFileId($fileId)->whereUserid($userid)->first();
if (empty($fileLink)) {
$fileLink = FileLink::createInstance([
'file_id' => $fileId,
'userid' => $userid,
'code' => base64_encode("{$fileId},{$userid}," . Base::generatePassword()),
]);
$fileLink->save();
} else {
if ($refresh == 'yes') {
$fileLink->code = base64_encode("{$fileId},{$userid}," . Base::generatePassword());
$fileLink->save();
}
}
return [
'id' => $fileId,
'url' => Base::fillUrl('single/file/' . $fileLink->code),
'code' => $fileLink->code,
'num' => $fileLink->num
];
}
}

View File

@@ -25,4 +25,34 @@ namespace App\Models;
*/
class FileUser extends AbstractModel
{
/**
* 删除所有共享成员(同时删除成员分享的链接)
* @param $file_id
* @param int $retain_link_userid 保留指定会员的链接
* @return mixed
*/
public static function deleteFileAll($file_id, $retain_link_userid = 0)
{
return AbstractModel::transaction(function() use ($retain_link_userid, $file_id) {
if ($retain_link_userid > 0) {
FileLink::whereFileId($file_id)->where('userid', '!=', $retain_link_userid)->delete();
} else {
FileLink::whereFileId($file_id)->delete();
}
FileUser::whereFileId($file_id)->delete();
});
}
/**
* 删除指定共享成员(同时删除成员分享的链接)
* @param $file_id
* @param $userid
* @return mixed
*/
public static function deleteFileUser($file_id, $userid)
{
return AbstractModel::transaction(function() use ($userid, $file_id) {
FileLink::whereFileId($file_id)->whereUserid($userid)->delete();
return self::whereFileId($file_id)->whereUserid($userid)->delete();
});
}
}

34
app/Models/Meeting.php Normal file
View File

@@ -0,0 +1,34 @@
<?php
namespace App\Models;
/**
* App\Models\Meeting
*
* @property int $id
* @property string|null $meetingid 会议ID不是数字
* @property string|null $name 会议主题
* @property string|null $channel 频道
* @property int|null $userid 创建人
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $end_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @method static \Illuminate\Database\Eloquent\Builder|Meeting newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Meeting newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Meeting query()
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereChannel($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereEndAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereMeetingid($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereUserid($value)
* @mixin \Eloquent
*/
class Meeting extends AbstractModel
{
}

View File

@@ -3,7 +3,9 @@
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use App\Tasks\PushTask;
use Arr;
use Carbon\Carbon;
use DB;
use Hhxsv5\LaravelS\Swoole\Task\Task;
@@ -17,6 +19,8 @@ use Request;
* @property string|null $name 名称
* @property string|null $desc 描述、备注
* @property int|null $userid 创建人
* @property int|null $personal 是否个人项目
* @property string|null $user_simple 成员总数|1,2,3
* @property int|null $dialog_id 聊天会话ID
* @property string|null $archived_at 归档时间
* @property int|null $archived_userid 归档会员
@@ -24,17 +28,17 @@ use Request;
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read int $owner_userid
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectColumn[] $projectColumn
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectColumn> $projectColumn
* @property-read int|null $project_column_count
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectLog[] $projectLog
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectLog> $projectLog
* @property-read int|null $project_log_count
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectUser[] $projectUser
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectUser> $projectUser
* @property-read int|null $project_user_count
* @method static \Illuminate\Database\Eloquent\Builder|Project allData($userid = null)
* @method static \Illuminate\Database\Eloquent\Builder|Project authData($userid = null, $owner = null)
* @method static \Illuminate\Database\Eloquent\Builder|Project newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Project newQuery()
* @method static \Illuminate\Database\Query\Builder|Project onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|Project onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|Project query()
* @method static \Illuminate\Database\Eloquent\Builder|Project whereArchivedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereArchivedUserid($value)
@@ -44,10 +48,12 @@ use Request;
* @method static \Illuminate\Database\Eloquent\Builder|Project whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project wherePersonal($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereUserSimple($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereUserid($value)
* @method static \Illuminate\Database\Query\Builder|Project withTrashed()
* @method static \Illuminate\Database\Query\Builder|Project withoutTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|Project withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|Project withoutTrashed()
* @mixin \Eloquent
*/
class Project extends AbstractModel
@@ -112,6 +118,7 @@ class Project extends AbstractModel
->select([
'projects.*',
'project_users.owner',
'project_users.top_at',
])
->leftJoin('project_users', function ($leftJoin) use ($userid) {
$leftJoin
@@ -135,6 +142,7 @@ class Project extends AbstractModel
->select([
'projects.*',
'project_users.owner',
'project_users.top_at',
])
->join('project_users', 'projects.id', '=', 'project_users.project_id')
->where('project_users.userid', $userid);
@@ -199,9 +207,11 @@ class Project extends AbstractModel
WebSocketDialogUser::updateInsert([
'dialog_id' => $this->dialog_id,
'userid' => $userid,
], [
'important' => 1
]);
}
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->delete();
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->remove();
});
}
@@ -211,7 +221,7 @@ class Project extends AbstractModel
*/
public function relationUserids()
{
return $this->projectUser->pluck('userid')->toArray();
return ProjectUser::whereProjectId($this->id)->orderBy('id')->pluck('userid')->toArray();
}
/**
@@ -241,8 +251,8 @@ class Project extends AbstractModel
$this->archived_at = null;
$this->archived_userid = User::userid();
$this->addLog("项目取消归档");
$this->pushMsg('add', $this);
ProjectTask::whereProjectId($this->id)->whereArchivedFollow(1)->update([
$this->pushMsg('recovery', $this);
ProjectTask::whereProjectId($this->id)->whereArchivedFollow(1)->change([
'archived_at' => null,
'archived_follow' => 0
]);
@@ -252,7 +262,7 @@ class Project extends AbstractModel
$this->archived_userid = User::userid();
$this->addLog("项目归档");
$this->pushMsg('archived');
ProjectTask::whereProjectId($this->id)->whereArchivedAt(null)->update([
ProjectTask::whereProjectId($this->id)->whereArchivedAt(null)->change([
'archived_at' => $archived_at,
'archived_follow' => 1
]);
@@ -350,6 +360,201 @@ class Project extends AbstractModel
}
}
/**
* 添加工作流
* @param $flows
* @return mixed
*/
public function addFlow($flows)
{
return AbstractModel::transaction(function() use ($flows) {
$projectFlow = ProjectFlow::whereProjectId($this->id)->first();
if (empty($projectFlow)) {
$projectFlow = ProjectFlow::createInstance([
'project_id' => $this->id,
'name' => 'Default'
]);
if (!$projectFlow->save()) {
throw new ApiException('工作流创建失败');
}
}
//
$ids = [];
$idc = [];
$hasStart = false;
$hasEnd = false;
$upTaskList = [];
foreach ($flows as $item) {
$id = intval($item['id']);
$turns = Base::arrayRetainInt($item['turns'] ?: [], true);
$userids = Base::arrayRetainInt($item['userids'] ?: [], true);
$usertype = trim($item['usertype']);
$userlimit = intval($item['userlimit']);
if ($usertype == 'replace' && empty($userids)) {
throw new ApiException("状态[{$item['name']}]设置错误,设置流转模式时必须填写状态负责人");
}
if ($usertype == 'merge' && empty($userids)) {
throw new ApiException("状态[{$item['name']}]设置错误,设置剔除模式时必须填写状态负责人");
}
if ($userlimit && empty($userids)) {
throw new ApiException("状态[{$item['name']}]设置错误,设置限制负责人时必须填写状态负责人");
}
$flow = ProjectFlowItem::updateInsert([
'id' => $id,
'project_id' => $this->id,
'flow_id' => $projectFlow->id,
], [
'name' => trim($item['name']),
'status' => trim($item['status']),
'sort' => intval($item['sort']),
'turns' => $turns,
'userids' => $userids,
'usertype' => trim($item['usertype']),
'userlimit' => $userlimit,
], [], $isInsert);
if ($flow) {
$ids[] = $flow->id;
if ($flow->id != $id) {
$idc[$id] = $flow->id;
}
if ($flow->status == 'start') {
$hasStart = true;
}
if ($flow->status == 'end') {
$hasEnd = true;
}
if (!$isInsert) {
$upTaskList[$flow->id] = $flow->status . "|" . $flow->name;
}
}
}
if (!$hasStart) {
throw new ApiException('至少需要1个开始状态');
}
if (!$hasEnd) {
throw new ApiException('至少需要1个结束状态');
}
ProjectFlowItem::whereFlowId($projectFlow->id)->whereNotIn('id', $ids)->chunk(100, function($list) {
foreach ($list as $item) {
$item->deleteFlowItem();
}
});
//
foreach ($upTaskList as $id => $value) {
ProjectTask::whereFlowItemId($id)->change([
'flow_item_name' => $value
]);
}
//
$projectFlow = ProjectFlow::with(['projectFlowItem'])->whereProjectId($this->id)->find($projectFlow->id);
$itemIds = $projectFlow->projectFlowItem->pluck('id')->toArray();
foreach ($projectFlow->projectFlowItem as $item) {
$turns = $item->turns;
foreach ($idc as $oid => $nid) {
if (in_array($oid, $turns)) {
$turns = array_diff($turns, [$oid]);
$turns[] = $nid;
}
}
if (!in_array($item->id, $turns)) {
$turns[] = $item->id;
}
$turns = array_values(array_filter(array_unique(array_intersect($turns, $itemIds))));
sort($turns);
$item->turns = $turns;
ProjectFlowItem::whereId($item->id)->update([ 'turns' => Base::array2json($turns) ]);
}
return $projectFlow;
});
}
/**
* 创建项目
* @param $params
* - name 项目名称
* - desc
* - flow
* - personal
* - columns
* @return array
*/
public static function createProject($params, $userid)
{
$name = trim(Arr::get($params, 'name', ''));
$desc = trim(Arr::get($params, 'desc', ''));
$flow = trim(Arr::get($params, 'flow', 'close'));
$isPersonal = intval(Arr::get($params, 'personal'));
if (mb_strlen($name) < 2) {
return Base::retError('项目名称不可以少于2个字');
} elseif (mb_strlen($name) > 32) {
return Base::retError('项目名称最多只能设置32个字');
}
if (mb_strlen($desc) > 255) {
return Base::retError('项目介绍最多只能设置255个字');
}
// 列表
$columns = explode(",", Arr::get($params, 'columns'));
$insertColumns = [];
$sort = 0;
foreach ($columns AS $column) {
$column = trim($column);
if ($column) {
$insertColumns[] = [
'name' => $column,
'sort' => $sort++,
];
}
}
if (empty($insertColumns)) {
$insertColumns[] = [
'name' => 'Default',
'sort' => 0,
];
}
if (count($insertColumns) > 30) {
return Base::retError('项目列表最多不能超过30个');
}
// 开始创建
$project = Project::createInstance([
'name' => $name,
'desc' => $desc,
'userid' => $userid,
]);
if ($isPersonal) {
if (Project::whereUserid($userid)->wherePersonal(1)->exists()) {
return Base::retError('个人项目已存在,无须重复创建');
}
$project->personal = 1;
}
AbstractModel::transaction(function() use ($flow, $insertColumns, $project) {
$project->save();
ProjectUser::createInstance([
'project_id' => $project->id,
'userid' => $project->userid,
'owner' => 1,
])->save();
foreach ($insertColumns AS $column) {
$column['project_id'] = $project->id;
ProjectColumn::createInstance($column)->save();
}
$dialog = WebSocketDialog::createGroup($project->name, $project->userid, 'project');
if (empty($dialog)) {
throw new ApiException('创建项目聊天室失败');
}
$project->dialog_id = $dialog->id;
$project->save();
//
if ($flow == 'open') {
$project->addFlow(Base::json2array('[{"id":-10,"name":"待处理","status":"start","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0},{"id":-11,"name":"进行中","status":"progress","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0},{"id":-12,"name":"待测试","status":"test","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0},{"id":-13,"name":"已完成","status":"end","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0},{"id":-14,"name":"已取消","status":"end","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0}]'));
}
});
//
$data = Project::find($project->id);
$data->addLog("创建项目");
$data->pushMsg('add', $data);
return Base::retSuccess('添加成功', $data);
}
/**
* 获取项目信息(用于判断会员是否存在项目内)
* @param int $project_id

View File

@@ -20,11 +20,11 @@ use Request;
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read \App\Models\Project|null $project
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectTask[] $projectTask
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectTask> $projectTask
* @property-read int|null $project_task_count
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn newQuery()
* @method static \Illuminate\Database\Query\Builder|ProjectColumn onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn query()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn whereColor($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn whereCreatedAt($value)
@@ -34,8 +34,8 @@ use Request;
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn whereProjectId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn whereSort($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn whereUpdatedAt($value)
* @method static \Illuminate\Database\Query\Builder|ProjectColumn withTrashed()
* @method static \Illuminate\Database\Query\Builder|ProjectColumn withoutTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn withoutTrashed()
* @mixin \Eloquent
*/
class ProjectColumn extends AbstractModel

View File

@@ -12,7 +12,7 @@ use App\Module\Base;
* @property string|null $name 流程名称
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectFlowItem[] $projectFlowItem
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectFlowItem> $projectFlowItem
* @property-read int|null $project_flow_item_count
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow newQuery()

View File

@@ -13,7 +13,7 @@ use App\Module\Base;
* @property string|null $name 名称
* @property string|null $status 状态
* @property array $turns 可流转
* @property array $userids 自动负责人ID
* @property array $userids 状态负责人ID
* @property string|null $usertype 流转模式
* @property int|null $userlimit 限制负责人
* @property int|null $sort 排序
@@ -81,7 +81,7 @@ class ProjectFlowItem extends AbstractModel
*/
public function deleteFlowItem()
{
ProjectTask::whereFlowItemId($this->id)->update([
ProjectTask::whereFlowItemId($this->id)->change([
'flow_item_id' => 0,
'flow_item_name' => "",
]);

View File

@@ -10,7 +10,7 @@ use App\Module\Base;
* @property int $id
* @property int|null $project_id 项目ID
* @property int|null $column_id 列表ID
* @property int|null $task_id 项目ID
* @property int|null $task_id 任务ID
* @property int|null $userid 会员ID
* @property string|null $detail 详细信息
* @property array $record 记录数据

View File

@@ -8,7 +8,6 @@ use App\Tasks\PushTask;
use Arr;
use Carbon\Carbon;
use DB;
use Exception;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Database\Eloquent\SoftDeletes;
use Request;
@@ -37,9 +36,12 @@ use Request;
* @property string|null $p_name 优先级名称
* @property string|null $p_color 优先级颜色
* @property int|null $sort 排序(ASC)
* @property string|null $loop 重复周期
* @property string|null $loop_at 下一次重复时间
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property int|null $deleted_userid 删除会员
* @property-read \App\Models\ProjectTaskContent|null $content
* @property-read int $file_num
* @property-read int $msg_num
@@ -50,18 +52,18 @@ use Request;
* @property-read bool $today
* @property-read \App\Models\Project|null $project
* @property-read \App\Models\ProjectColumn|null $projectColumn
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectTaskFile[] $taskFile
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectTaskFile> $taskFile
* @property-read int|null $task_file_count
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectTaskTag[] $taskTag
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectTaskTag> $taskTag
* @property-read int|null $task_tag_count
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectTaskUser[] $taskUser
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectTaskUser> $taskUser
* @property-read int|null $task_user_count
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask allData($userid = null)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask authData($userid = null, $owner = null)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask betweenTime($start, $end)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask betweenTime($start, $end, $type = 'taskTime')
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask newQuery()
* @method static \Illuminate\Database\Query\Builder|ProjectTask onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask query()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereArchivedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereArchivedFollow($value)
@@ -71,12 +73,15 @@ use Request;
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereCompleteAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereDeletedUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereDesc($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereEndAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereFlowItemId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereFlowItemName($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereLoop($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereLoopAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask wherePColor($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask wherePLevel($value)
@@ -87,8 +92,8 @@ use Request;
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereStartAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereUserid($value)
* @method static \Illuminate\Database\Query\Builder|ProjectTask withTrashed()
* @method static \Illuminate\Database\Query\Builder|ProjectTask withoutTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask withoutTrashed()
* @mixin \Eloquent
*/
class ProjectTask extends AbstractModel
@@ -235,7 +240,7 @@ class ProjectTask extends AbstractModel
*/
public function content(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(ProjectTaskContent::class, 'task_id', 'id');
return $this->hasOne(ProjectTaskContent::class, 'task_id', 'id')->orderByDesc('id');
}
/**
@@ -243,7 +248,7 @@ class ProjectTask extends AbstractModel
*/
public function taskFile(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(ProjectTaskFile::class, 'task_id', 'id')->orderBy('id');
return $this->hasMany(ProjectTaskFile::class, 'task_id', 'id')->orderByDesc('id')->limit(50);
}
/**
@@ -299,6 +304,7 @@ class ProjectTask extends AbstractModel
'project_tasks.*',
'project_task_users.owner'
])
->selectRaw("1 AS assist")
->join('project_task_users', 'project_tasks.id', '=', 'project_task_users.task_id')
->where('project_task_users.userid', $userid);
if ($owner !== null) {
@@ -312,18 +318,33 @@ class ProjectTask extends AbstractModel
* @param $query
* @param $start
* @param $end
* @param $type
* @return mixed
*/
public function scopeBetweenTime($query, $start, $end)
public function scopeBetweenTime($query, $start, $end, $type = 'taskTime')
{
$query->where(function ($q1) use ($start, $end) {
$q1->where(function ($q2) use ($start) {
$q2->where('project_tasks.start_at', '<=', $start)->where('project_tasks.end_at', '>=', $start);
})->orWhere(function ($q2) use ($end) {
$q2->where('project_tasks.start_at', '<=', $end)->where('project_tasks.end_at', '>=', $end);
})->orWhere(function ($q2) use ($start, $end) {
$q2->where('project_tasks.start_at', '>', $start)->where('project_tasks.end_at', '<', $end);
});
$query->where(function ($q1) use ($start, $end, $type) {
switch ($type) {
case 'createdTime':
$q1->where(function ($q2) use ($start) {
$q2->where('project_tasks.created_at', '>=', $start);
})->orWhere(function ($q2) use ($end) {
$q2->where('project_tasks.created_at', '<=', $end);
})->orWhere(function ($q2) use ($start, $end) {
$q2->where('project_tasks.created_at', '>', $start)->where('project_tasks.created_at', '<', $end);
});
break;
default:
$q1->where(function ($q2) use ($start) {
$q2->where('project_tasks.start_at', '<=', $start)->where('project_tasks.end_at', '>=', $start);
})->orWhere(function ($q2) use ($end) {
$q2->where('project_tasks.start_at', '<=', $end)->where('project_tasks.end_at', '>=', $end);
})->orWhere(function ($q2) use ($start, $end) {
$q2->where('project_tasks.start_at', '>', $start)->where('project_tasks.end_at', '<', $end);
});
break;
}
});
return $query;
}
@@ -342,11 +363,13 @@ class ProjectTask extends AbstractModel
$content = $data['content'];
$times = $data['times'];
$owner = $data['owner'];
$add_assist = intval($data['add_assist']);
$subtasks = $data['subtasks'];
$p_level = intval($data['p_level']);
$p_name = $data['p_name'];
$p_color = $data['p_color'];
$top = intval($data['top']);
$userid = User::userid();
//
if (ProjectTask::whereProjectId($project_id)
->whereNull('project_tasks.complete_at')
@@ -411,8 +434,13 @@ class ProjectTask extends AbstractModel
$tmpArray[] = $uid;
}
$owner = $tmpArray;
// 协助人员
$assist = [];
if (!in_array($userid, $owner) && $add_assist) {
$assist = [$userid];
}
// 创建人
$task->userid = User::userid();
$task->userid = $userid;
// 排序位置
if ($top) {
$task->sort = intval(self::whereColumnId($task->column_id)->orderBy('sort')->value('sort')) - 1;
@@ -434,7 +462,7 @@ class ProjectTask extends AbstractModel
}
}
//
return AbstractModel::transaction(function() use ($times, $subtasks, $content, $owner, $task) {
return AbstractModel::transaction(function() use ($assist, $times, $subtasks, $content, $owner, $task) {
$task->save();
$owner = array_values(array_unique($owner));
foreach ($owner as $uid) {
@@ -446,11 +474,23 @@ class ProjectTask extends AbstractModel
'owner' => 1,
])->save();
}
$assist = array_values(array_unique(array_diff($assist, $owner)));
foreach ($assist as $uid) {
ProjectTaskUser::createInstance([
'project_id' => $task->project_id,
'task_id' => $task->id,
'task_pid' => $task->parent_id ?: $task->id,
'userid' => $uid,
'owner' => 0,
])->save();
}
if ($content) {
ProjectTaskContent::createInstance([
'project_id' => $task->project_id,
'task_id' => $task->id,
'content' => $content,
'content' => [
'url' => ProjectTaskContent::saveContent($task->id, $content)
],
])->save();
}
if ($task->parent_id == 0 && $subtasks && is_array($subtasks)) {
@@ -490,14 +530,14 @@ class ProjectTask extends AbstractModel
public function updateTask($data, &$updateMarking = [])
{
AbstractModel::transaction(function () use ($data, &$updateMarking) {
// 判断版本
if (version_compare(Base::getClientVersion(), '0.6.0', '<')) {
throw new ApiException('当前版本过低');
}
// 主任务
$mainTask = $this->parent_id > 0 ? self::find($this->parent_id) : null;
// 工作流
if (Arr::exists($data, 'flow_item_id')) {
$isProjectOwner = $this->useridInTheProject(User::userid()) === 2;
if (!$isProjectOwner && !$this->isOwner()) {
throw new ApiException('仅限项目或任务负责人修改任务状态');
}
if ($this->flow_item_id == $data['flow_item_id']) {
throw new ApiException('任务状态未发生改变');
}
@@ -518,8 +558,7 @@ class ProjectTask extends AbstractModel
throw new ApiException("当前状态[{$currentFlowItem->name}]不可流转到[{$newFlowItem->name}]");
}
if ($currentFlowItem->userlimit) {
if (!in_array(User::userid(), $currentFlowItem->userids)
&& !ProjectUser::whereProjectId($this->project_id)->whereOwner(1)->exists()) {
if (!$isProjectOwner && !in_array(User::userid(), $currentFlowItem->userids)) {
throw new ApiException("当前状态[{$currentFlowItem->name}]仅限状态负责人或项目负责人修改");
}
}
@@ -541,13 +580,17 @@ class ProjectTask extends AbstractModel
if ($newFlowItem->userids) {
// 判断自动添加负责人
$flowData['owner'] = $data['owner'] = $this->taskUser->where('owner', 1)->pluck('userid')->toArray();
if ($newFlowItem->usertype == "replace") {
// 流转模式
if (in_array($newFlowItem->usertype, ["replace", "merge"])) {
// 流转模式、剔除模式
if ($this->parent_id === 0) {
$flowData['assist'] = $data['assist'] = $this->taskUser->where('owner', 0)->pluck('userid')->toArray();
$data['assist'] = array_merge($data['assist'], $data['owner']);
}
$data['owner'] = $newFlowItem->userids;
// 判断剔除模式:保留操作状态的人员
if ($newFlowItem->usertype == "merge") {
$data['owner'][] = User::userid();
}
} else {
// 添加模式
$data['owner'] = array_merge($data['owner'], $newFlowItem->userids);
@@ -563,6 +606,14 @@ class ProjectTask extends AbstractModel
'flow' => $flowData,
'change' => [$currentFlowItem?->name, $newFlowItem->name]
]);
ProjectTaskFlowChange::createInstance([
'task_id' => $this->id,
'userid' => User::userid(),
'before_flow_item_id' => $flowData['flow_item_id'],
'before_flow_item_name' => $flowData['flow_item_name'],
'after_flow_item_id' => $this->flow_item_id,
'after_flow_item_name' => $this->flow_item_name,
])->save();
}
// 状态
if (Arr::exists($data, 'complete_at')) {
@@ -575,7 +626,7 @@ class ProjectTask extends AbstractModel
if ($this->complete_at) {
throw new ApiException('任务已完成');
}
$this->completeTask(Carbon::now());
$this->completeTask(Carbon::now(), isset($newFlowItem) ? $newFlowItem->name : null);
} else {
// 标记未完成
if (!$this->complete_at) {
@@ -584,7 +635,6 @@ class ProjectTask extends AbstractModel
$this->completeTask(null);
}
$updateMarking['is_update_project'] = true;
return;
}
// 标题
if (Arr::exists($data, 'name') && $this->name != $data['name']) {
@@ -597,10 +647,13 @@ class ProjectTask extends AbstractModel
'change' => [$this->name, $data['name']]
]);
$this->name = $data['name'];
if ($this->dialog_id) {
WebSocketDialog::updateData(['id' => $this->dialog_id], ['name' => $this->name]);
}
}
// 负责人
if (Arr::exists($data, 'owner')) {
$count = $this->taskUser->where('owner', 1)->count();
$older = $this->taskUser->where('owner', 1)->pluck('userid')->toArray();
$array = [];
$owner = is_array($data['owner']) ? $data['owner'] : [$data['owner']];
if (count($owner) > 10) {
@@ -619,20 +672,18 @@ class ProjectTask extends AbstractModel
'owner' => 1,
]);
$array[] = $uid;
if ($this->parent_id) {
break; // 子任务只能是一个负责人
}
}
if ($array) {
if ($count == 0 && count($array) == 1 && $array[0] == User::userid()) {
if (count($older) == 0 && count($array) == 1 && $array[0] == User::userid()) {
$this->addLog("认领{任务}");
} else {
$this->addLog("修改{任务}负责人", ['userid' => $array]);
}
$this->taskPush(array_values(array_diff($array, $older)), 0);
}
$rows = ProjectTaskUser::whereTaskId($this->id)->whereOwner(1)->whereNotIn('userid', $array)->get();
if ($rows->isNotEmpty()) {
$this->addLog("删除{任务}负责人", ['userid' => $rows->implode('userid', ',')]);
$this->addLog("删除{任务}负责人", ['userid' => $rows->pluck('userid')]);
foreach ($rows as $row) {
$row->delete();
}
@@ -643,10 +694,11 @@ 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()) : '';
$this->start_at = null;
$this->end_at = null;
$times = $data['times'];
list($start, $end) = is_string($times) ? explode(",", $times) : (is_array($times) ? $times : []);
list($start, $end, $desc) = is_string($times) ? explode(",", $times) : (is_array($times) ? $times : []);
if (Base::isDate($start) && Base::isDate($end) && $start != $end) {
$start_at = Carbon::parse($start);
$end_at = Carbon::parse($end);
@@ -686,14 +738,18 @@ class ProjectTask extends AbstractModel
$start_at = Carbon::parse($subTask->start_at);
$end_at = Carbon::parse($subTask->end_at);
$isUp = false;
if ($start_at->eq($oldAt[0]) || $start_at->lt(Carbon::parse($this->start_at))) {
if (empty($subTask->start_at) || $start_at->eq($oldAt[0]) || $start_at->lt(Carbon::parse($this->start_at))) {
$subTask->start_at = $this->start_at;
$isUp = true;
}
if ($end_at->eq($oldAt[1]) || $end_at->gt(Carbon::parse($this->end_at))) {
if (empty($subTask->end_at) || $end_at->eq($oldAt[1]) || $end_at->gt(Carbon::parse($this->end_at))) {
$subTask->end_at = $this->end_at;
$isUp = true;
}
if ($subTask->start_at && Carbon::parse($subTask->start_at)->gt($subTask->end_at)) {
$subTask->start_at = $this->start_at;
$isUp = true;
}
if ($isUp) {
$updateMarking['is_update_subtask'] = true;
$subTask->addLog("同步修改{任务}时间");
@@ -702,10 +758,39 @@ class ProjectTask extends AbstractModel
}
});
}
$this->addLog("修改{任务}时间");
$newStringAt = $this->start_at ? ($this->start_at->toDateTimeString() . '~' . $this->end_at->toDateTimeString()) : '';
$newDesc = $desc ? "(备注:{$desc}" : "";
$this->addLog("修改{任务}时间" . $newDesc, [
'change' => [$oldStringAt, $newStringAt]
]);
$this->taskPush(null, 3, $newDesc);
}
// 以下顶级任务可修改
// 以下顶级任务可修改
if ($this->parent_id === 0) {
// 重复周期
$loopAt = $this->loop_at;
$loopDesc = $this->loopDesc();
if (Arr::exists($data, 'loop')) {
$this->loop = $data['loop'];
if (!$this->refreshLoop()) {
throw new ApiException('重复周期选择错误');
}
} elseif (Arr::exists($data, 'times')) {
// 更新任务时间也要更新重复周期
$this->refreshLoop();
}
$oldLoop = $loopAt ? Carbon::parse($loopAt)->toDateTimeString() : null;
$newLoop = $this->loop_at ? Carbon::parse($this->loop_at)->toDateTimeString() : null;
if ($oldLoop != $newLoop) {
$this->addLog("修改{任务}下个周期", [
'change' => [$oldLoop, $newLoop]
]);
}
if ($loopDesc != $this->loopDesc()) {
$this->addLog("修改{任务}重复周期", [
'change' => [$loopDesc, $this->loopDesc()]
]);
}
// 协助人员
if (Arr::exists($data, 'assist')) {
$array = [];
@@ -732,7 +817,7 @@ class ProjectTask extends AbstractModel
}
$rows = ProjectTaskUser::whereTaskId($this->id)->whereOwner(0)->whereNotIn('userid', $array)->get();
if ($rows->isNotEmpty()) {
$this->addLog("删除{任务}协助人员", ['userid' => $rows->implode('userid', ',')]);
$this->addLog("删除{任务}协助人员", ['userid' => $rows->pluck('userid')]);
foreach ($rows as $row) {
$row->delete();
}
@@ -760,18 +845,20 @@ class ProjectTask extends AbstractModel
}
// 内容
if (Arr::exists($data, 'content')) {
ProjectTaskContent::updateInsert([
ProjectTaskContent::createInstance([
'project_id' => $this->project_id,
'task_id' => $this->id,
], [
'content' => $data['content'],
]);
'content' => [
'url' => ProjectTaskContent::saveContent($this->id, $data['content'])
],
])->save();
$this->desc = Base::getHtml($data['content'], 100);
$this->addLog("修改{任务}详细描述");
$updateMarking['is_update_content'] = true;
}
// 优先级
$p = false;
$oldPName = $this->p_name;
if (Arr::exists($data, 'p_level') && $this->p_level != $data['p_level']) {
$this->p_level = intval($data['p_level']);
$p = true;
@@ -785,7 +872,9 @@ class ProjectTask extends AbstractModel
$p = true;
}
if ($p) {
$this->addLog("修改{任务}优先级");
$this->addLog("修改{任务}优先级", [
'change' => [$oldPName, $this->p_name]
]);
}
}
$this->save();
@@ -795,6 +884,141 @@ class ProjectTask extends AbstractModel
return true;
}
/**
* 刷新重复周期时间
* @param bool $save 是否执行保存
* @return bool
*/
public function refreshLoop($save = false)
{
$success = true;
if ($this->start_at) {
$base = Carbon::parse($this->start_at);
if ($base->lt(Carbon::today())) {
// 如果任务开始时间小于今天则基数时间为今天
$base = Carbon::parse(date("Y-m-d {$base->toTimeString()}"));
}
} else {
// 未设置任务时间时基数时间为今天
$base = Carbon::today();
}
switch ($this->loop) {
case "day":
$this->loop_at = $base->addDay();
break;
case "weekdays":
$this->loop_at = $base->addWeekday();
break;
case "week":
$this->loop_at = $base->addWeek();
break;
case "twoweeks":
$this->loop_at = $base->addWeeks(2);
break;
case "month":
$this->loop_at = $base->addMonth();
break;
case "year":
$this->loop_at = $base->addYear();
break;
case "never":
$this->loop_at = null;
break;
default:
if (Base::isNumber($this->loop)) {
$this->loop_at = $base->addDays($this->loop);
} else {
$this->loop_at = null;
$success = false;
}
break;
}
if ($success && $save) {
$this->save();
}
return $success;
}
/**
* 获取周期描述
* @return string
*/
public function loopDesc() {
$loopDesc = "从不";
switch ($this->loop) {
case "day":
$loopDesc = "每天";
break;
case "weekdays":
$loopDesc = "每个工作日";
break;
case "week":
$loopDesc = "每周";
break;
case "twoweeks":
$loopDesc = "每两周";
break;
case "month":
$loopDesc = "每月";
break;
case "year":
$loopDesc = "每年";
break;
default:
if (Base::isNumber($this->loop)) {
$loopDesc = "{$this->loop}";
}
break;
}
return $loopDesc;
}
/**
* 复制任务
* @return self
*/
public function copyTask()
{
if ($this->parent_id > 0) {
throw new ApiException('子任务禁止复制');
}
return AbstractModel::transaction(function() {
// 复制任务
$task = $this->replicate();
$task->dialog_id = 0;
$task->archived_at = null;
$task->archived_userid = 0;
$task->archived_follow = 0;
$task->complete_at = null;
$task->created_at = Carbon::now();
$task->save();
// 复制任务内容
if ($this->content) {
$tmp = $this->content->replicate();
$tmp->task_id = $task->id;
$tmp->created_at = Carbon::now();
$tmp->save();
}
// 复制任务附件
foreach ($this->taskFile as $taskFile) {
$tmp = $taskFile->replicate();
$tmp->task_id = $task->id;
$tmp->created_at = Carbon::now();
$tmp->save();
}
// 复制任务成员
foreach ($this->taskUser as $taskUser) {
$tmp = $taskUser->replicate();
$tmp->task_id = $task->id;
$tmp->task_pid = $task->id;
$tmp->created_at = Carbon::now();
$tmp->save();
}
//
return $task;
});
}
/**
* 同步项目成员至聊天室
*/
@@ -814,9 +1038,11 @@ class ProjectTask extends AbstractModel
WebSocketDialogUser::updateInsert([
'dialog_id' => $this->dialog_id,
'userid' => $userid,
], [
'important' => 1
]);
}
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->delete();
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->remove();
});
}
@@ -826,7 +1052,7 @@ class ProjectTask extends AbstractModel
*/
public function relationUserids()
{
$userids = $this->taskUser->pluck('userid')->toArray();
$userids = ProjectTaskUser::whereTaskId($this->id)->orderByDesc('owner')->orderByDesc('id')->pluck('userid')->toArray();
$items = ProjectTask::with(['taskUser'])->where('parent_id', $this->id)->whereNull('archived_at')->get();
foreach ($items as $item) {
$userids = array_merge($userids, $item->taskUser->pluck('userid')->toArray());
@@ -862,6 +1088,78 @@ class ProjectTask extends AbstractModel
return $user->owner ? 2 : 1;
}
/**
* 权限版本
* @param int $level
* 1负责人
* 2协助人/负责人
* 3创建人/协助人/负责人
* 4任务群聊成员/3
* @return bool
*/
public function permission($level = 1)
{
if ($level >= 4) {
return $this->permission(3) || $this->existDialogUser();
}
if ($level >= 3 && $this->isCreater()) {
return true;
}
if ($level >= 2 && $this->isAssister()) {
return true;
}
return $this->isOwner();
}
/**
* 判断是否在任务对话里
* @return bool
*/
public function existDialogUser()
{
return $this->dialog_id && WebSocketDialogUser::whereDialogId($this->dialog_id)->whereUserid(User::userid())->exists();
}
/**
* 判断是否创建者
* @return bool
*/
public function isCreater()
{
return $this->userid == User::userid();
}
/**
* 判断是否协助人员
* @return bool
*/
public function isAssister()
{
$row = $this;
while ($row->parent_id > 0) {
$row = self::find($row->parent_id);
}
return ProjectTaskUser::whereTaskId($row->id)->whereUserid(User::userid())->whereOwner(0)->exists();
}
/**
* 判断是否负责人(或者是主任务的负责人)
* @return bool
*/
public function isOwner()
{
if ($this->owner) {
return true;
}
if ($this->parent_id > 0) {
$mainTask = self::allData()->find($this->parent_id);
if ($mainTask->owner) {
return true;
}
}
return false;
}
/**
* 是否有负责人
* @return bool
@@ -877,15 +1175,22 @@ class ProjectTask extends AbstractModel
/**
* 标记已完成、未完成
* @param Carbon|null $complete_at 完成时间
* @param String $complete_name 已完成名称(留空为:已完成)
* @return bool
*/
public function completeTask($complete_at)
public function completeTask($complete_at, $complete_name = null)
{
AbstractModel::transaction(function () use ($complete_at) {
AbstractModel::transaction(function () use ($complete_at, $complete_name) {
$addMsg = $this->parent_id == 0 && $this->dialog_id > 0;
if ($complete_at === null) {
// 标记未完成
$this->complete_at = null;
$this->addLog("标记{任务}未完成");
if ($addMsg) {
WebSocketDialogMsg::sendMsg(null, $this->dialog_id, 'notice', [
'notice' => "标记任务未完成"
], 0, true, true);
}
} else {
// 标记已完成
if ($this->parent_id == 0) {
@@ -896,8 +1201,16 @@ class ProjectTask extends AbstractModel
if (!$this->hasOwner()) {
throw new ApiException('请先领取任务');
}
if (empty($complete_name)) {
$complete_name = '已完成';
}
$this->complete_at = $complete_at;
$this->addLog("标记{任务}已完成");
$this->addLog("标记{任务}{$complete_name}");
if ($addMsg) {
WebSocketDialogMsg::sendMsg(null, $this->dialog_id, 'notice', [
'notice' => "标记任务{$complete_name}"
], 0, true, true);
}
}
$this->save();
});
@@ -911,6 +1224,16 @@ class ProjectTask extends AbstractModel
*/
public function archivedTask($archived_at, $isAuto = false)
{
if (!$this->complete_at) {
$flowItems = ProjectFlowItem::whereProjectId($this->project_id)->whereStatus('end')->pluck('name');
if ($flowItems) {
$flowItems = implode(",", array_values(array_unique($flowItems->toArray())));
}
if (empty($flowItems)) {
$flowItems = "已完成";
}
throw new ApiException('仅限【' . $flowItems . '】状态的任务归档');
}
AbstractModel::transaction(function () use ($isAuto, $archived_at) {
if ($archived_at === null) {
// 取消归档
@@ -918,7 +1241,6 @@ class ProjectTask extends AbstractModel
$this->archived_userid = User::userid();
$this->archived_follow = 0;
$this->addLog("任务取消归档");
$this->pushMsg('add', ProjectTask::oneTask($this->id));
} else {
// 归档任务
if ($isAuto === true) {
@@ -932,9 +1254,13 @@ class ProjectTask extends AbstractModel
$this->archived_userid = $userid;
$this->archived_follow = 0;
$this->addLog($logText, [], $userid);
$this->pushMsg('archived');
}
self::whereParentId($this->id)->update([
$this->pushMsg($archived_at === null ? 'recovery' : 'archived', [
'id' => $this->id,
'archived_at' => $this->archived_at,
'archived_userid' => $this->archived_userid,
]);
self::whereParentId($this->id)->change([
'archived_at' => $this->archived_at,
'archived_userid' => $this->archived_userid,
'archived_follow' => $this->archived_follow,
@@ -956,7 +1282,9 @@ class ProjectTask extends AbstractModel
$dialog = WebSocketDialog::find($this->dialog_id);
$dialog?->deleteDialog();
}
self::whereParentId($this->id)->delete();
self::whereParentId($this->id)->remove();
$this->deleted_userid = User::userid();
$this->save();
$this->addLog("删除{任务}");
$this->delete();
});
@@ -966,6 +1294,28 @@ class ProjectTask extends AbstractModel
return true;
}
/**
* 还原任务
* @param bool $pushMsg 是否推送
* @return bool
*/
public function restoreTask($pushMsg = true)
{
AbstractModel::transaction(function () {
if ($this->dialog_id) {
$dialog = WebSocketDialog::withTrashed()->find($this->dialog_id);
$dialog?->restoreDialog();
}
self::whereParentId($this->id)->withTrashed()->restore();
$this->addLog("还原{任务}");
$this->restore();
});
if ($pushMsg) {
$this->pushMsg('restore');
}
return true;
}
/**
* 添加任务日志
* @param string $detail
@@ -1017,28 +1367,62 @@ class ProjectTask extends AbstractModel
$data = $data->toArray();
}
//
$array = [$userid, []];
$userids = [];
if ($userid === null) {
$array[0] = $this->project->relationUserids();
$userids = $this->project->relationUserids();
} elseif (!is_array($userid)) {
$array[0] = [$userid];
$userids = [$userid];
}
//
if (isset($data['owner'])) {
$owners = ProjectTaskUser::whereTaskId($data['id'])->whereOwner(1)->pluck('userid')->toArray();
$array = [array_intersect($array[0], $owners), array_diff($array[0], $owners)];
}
foreach ($array as $index => $item) {
if ($index > 0) {
$data['owner'] = 0;
$array = [];
if (empty($data['parent_id'])) {
if (Arr::exists($data, 'owner') || Arr::exists($data, 'assist')) {
$taskUser = ProjectTaskUser::select(['userid', 'owner'])->whereTaskId($data['id'])->get();
// 负责人
$owners = $taskUser->where('owner', 1)->pluck('userid')->toArray();
$owners = array_intersect($userids, $owners);
if ($owners) {
$array[] = [
'userid' => array_values($owners),
'data' => array_merge($data, [
'owner' => 1,
'assist' => 1,
])
];
}
// 协助人
$assists = $taskUser->pluck('userid')->toArray();
$assists = array_intersect($userids, array_diff($assists, $owners));
if ($assists) {
$array[] = [
'userid' => array_values($assists),
'data' => array_merge($data, [
'owner' => 0,
'assist' => 1,
])
];
}
// 项目成员(其他人)
$userids = array_diff($userids, $owners, $assists);
$data = array_merge($data, [
'owner' => 0,
'assist' => 0,
]);
}
}
$array[] = [
'userid' => array_values($userids),
'data' => $data
];
//
foreach ($array as $item) {
$params = [
'ignoreFd' => Request::header('fd'),
'userid' => array_values($item),
'msg' => [
'type' => 'projectTask',
'action' => $action,
'data' => $data,
'data' => $item['data'],
]
];
$task = new PushTask($params, false);
@@ -1046,6 +1430,62 @@ class ProjectTask extends AbstractModel
}
}
/**
* 任务提醒
* @param $userids
* @param int $type 0-新任务、1-即将超时、2-已超时、3-修改时间
* @param string $suffix 描述后缀
* @return void
*/
public function taskPush($userids, int $type, string $suffix = "")
{
if ($userids === null) {
$userids = $this->taskUser->pluck('userid')->toArray();
}
if (empty($userids)) {
return;
}
$owners = $this->taskUser->pluck('owner', 'userid')->toArray();
$receivers = User::whereIn('userid', $userids)->whereNull('disable_at')->get();
if (empty($receivers)) {
return;
}
$botUser = User::botGetOrCreate('task-alert');
if (empty($botUser)) {
return;
}
$taskHtml = "<span class=\"mention task\" data-id=\"{$this->id}\">#{$this->name}</span>";
$text = match ($type) {
1 => "您的任务 {$taskHtml} 即将超时。",
2 => "您的任务 {$taskHtml} 已经超时。",
3 => "您的任务 {$taskHtml} 时间已修改。",
default => "您有一个新任务 {$taskHtml}",
};
/** @var User $user */
foreach ($receivers as $receiver) {
$data = [
'type' => $type,
'userid' => $receiver->userid,
'task_id' => $this->id,
];
if (in_array($type, [1, 2]) && ProjectTaskPushLog::where($data)->exists()) {
continue;
}
//
$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
], $botUser->userid);
}
}
}
/**
* 获取任务
* @param $task_id
@@ -1053,48 +1493,67 @@ class ProjectTask extends AbstractModel
*/
public static function oneTask($task_id)
{
return self::with(['taskUser', 'taskTag'])->allData()->where("project_tasks.id", intval($task_id))->first();
$data = self::with(['taskUser', 'taskTag'])->allData()->where("project_tasks.id", intval($task_id))->first();
if ($data && $data->parent_id === 0) {
if ($data->owner || ProjectTaskUser::select(['owner'])->whereTaskId($data->id)->whereUserid(User::userid())->exists()) {
$data->assist = 1;
} else {
$data->assist = 0;
}
}
return $data;
}
/**
* 获取任务(会员有任务权限 或 会员存在项目内)
* @param int $task_id
* @param bool $archived true:仅限未归档, false:不限制, null:不限制
* @param int|bool $mustOwner 0|false:不限制, 1|true:限制任务或项目负责人, 2:已有负责人才限制任务或项目负责人
* @param bool $archived true:仅限未归档, false:仅限已归档, null:不限制
* @param bool $trashed true:仅限未删除, false:仅限已删除, null:不限制
* @param int|bool $permission
* - 0|false 限制:项目成员、任务成员、任务群聊成员(任务成员 = 任务创建人+任务协助人+任务负责人)
* - 1|true 限制:项目负责人、任务成员
* - 2 已有负责人才限制true (子任务时如果是主任务负责人也可以)
* @param array $with
* @return self
*/
public static function userTask($task_id, $archived = true, $mustOwner = 0, $with = [])
public static function userTask($task_id, $archived = true, $trashed = true, $permission = false, $with = [])
{
$task = self::with($with)->allData()->where("project_tasks.id", intval($task_id))->first();
$builder = self::with($with)->allData()->where("project_tasks.id", intval($task_id));
if ($trashed === false) {
$builder->onlyTrashed();
} elseif ($trashed === null) {
$builder->withTrashed();
}
$task = $builder->first();
//
if (empty($task)) {
throw new ApiException('任务不存在', [ 'task_id' => $task_id ], -4002);
throw new ApiException('任务不存在', ['task_id' => $task_id], -4002);
}
if ($archived === true && $task->archived_at != null) {
throw new ApiException('任务已归档', [ 'task_id' => $task_id ], -4002);
throw new ApiException('任务已归档', ['task_id' => $task_id]);
}
if ($archived === false && $task->archived_at == null) {
throw new ApiException('任务未归档', [ 'task_id' => $task_id ]);
throw new ApiException('任务未归档', ['task_id' => $task_id]);
}
//
try {
$project = Project::userProject($task->project_id, $archived);
} catch (Exception $e) {
if ($task->owner === null) {
$project = Project::userProject($task->project_id);
} catch (\Throwable $e) {
if ($task->owner !== null || (!$permission && $task->permission(4))) {
$project = Project::find($task->project_id);
if (empty($project)) {
throw new ApiException('项目不存在或已被删除', [ 'task_id' => $task_id ], -4002);
}
} else {
throw new ApiException($e->getMessage(), [ 'task_id' => $task_id ], -4002);
}
$project = Project::find($task->project_id);
if (empty($project)) {
throw new ApiException('项目不存在或已被删除', [ 'task_id' => $task_id ], -4002);
}
}
//
if ($mustOwner === 2) {
$mustOwner = $task->hasOwner() ? 1 : 0;
if ($permission >= 2) {
$permission = $task->hasOwner() ? 1 : 0;
}
if (($mustOwner === 1 || $mustOwner === true) && !$task->owner && !$project->owner) {
throw new ApiException('仅限项目任务负责人操作');
if ($permission && !$project->owner && !$task->permission(3)) {
throw new ApiException('仅限项目负责人、任务负责人、协助人员或任务创建者操作');
}
//
return $task;

View File

@@ -2,6 +2,8 @@
namespace App\Models;
use App\Module\Base;
/**
* App\Models\ProjectTaskContent
*
@@ -28,4 +30,55 @@ class ProjectTaskContent extends AbstractModel
'created_at',
'updated_at',
];
/**
* 获取内容详情
* @return array
*/
public function getContentInfo()
{
$content = Base::json2array($this->content);
if (isset($content['url'])) {
$filePath = public_path($content['url']);
$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']);
}
return $array;
}
return $this->toArray();
}
/**
* 保存任务详情至文件并返回文件路径
* @param $task_id
* @param $content
* @return string
*/
public static function saveContent($task_id, $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);
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))) {
$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);
//
$filePath = $path . md5($content);
$publicPath = public_path($filePath);
Base::makeDir(dirname($publicPath));
file_put_contents($publicPath, $content);
//
return $filePath;
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Models;
use App\Module\Base;
use Cache;
/**
* App\Models\ProjectTaskFile
@@ -19,6 +20,8 @@ use App\Module\Base;
* @property int|null $download 下载次数
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read int $height
* @property-read int $width
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile query()
@@ -38,6 +41,11 @@ use App\Module\Base;
*/
class ProjectTaskFile extends AbstractModel
{
protected $appends = [
'width',
'height',
];
/**
* 地址
* @param $value
@@ -57,4 +65,50 @@ class ProjectTaskFile extends AbstractModel
{
return Base::fillUrl($value ?: Base::extIcon($this->ext));
}
/**
* 宽
* @return int
*/
public function getWidthAttribute()
{
$this->generateSizeData();
return $this->appendattrs['width'];
}
/**
* 高
* @return int
*/
public function getHeightAttribute()
{
$this->generateSizeData();
return $this->appendattrs['height'];
}
/**
* 生成尺寸数据
*/
private function generateSizeData()
{
if (!isset($this->appendattrs['width'])) {
$width = -1;
$height = -1;
if (in_array($this->ext, ['jpg', 'jpeg', 'webp', 'gif', 'png'])) {
$path = public_path($this->getRawOriginal('path'));
[$width, $height] = Cache::remember("File::size-" . md5($path), now()->addDays(7), function () use ($path) {
$width = -1;
$height = -1;
if (file_exists($path)) {
$paramet = getimagesize($path);
$width = $paramet[0];
$height = $paramet[1];
}
return [$width, $height];
});
}
$this->appendattrs['width'] = $width;
$this->appendattrs['height'] = $height;
}
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Models;
/**
* App\Models\ProjectTaskFlowChange
*
* @property int $id
* @property int|null $task_id 任务ID
* @property int|null $userid 会员ID
* @property int|null $before_flow_item_id 变化前工作流状态ID
* @property string|null $before_flow_item_name (变化前)工作流状态名称
* @property int|null $after_flow_item_id 变化后工作流状态ID
* @property string|null $after_flow_item_name (变化后)工作流状态名称
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange query()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereAfterFlowItemId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereAfterFlowItemName($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereBeforeFlowItemId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereBeforeFlowItemName($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereTaskId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFlowChange whereUserid($value)
* @mixin \Eloquent
*/
class ProjectTaskFlowChange extends AbstractModel
{
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* App\Models\ProjectTaskPushLog
*
* @property int $id
* @property int|null $userid 用户id
* @property int|null $task_id 任务id
* @property int|null $type 提醒类型0 任务开始提醒1 距离到期提醒2到期超时提醒
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog query()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereTaskId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog whereUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog withoutTrashed()
* @mixin \Eloquent
*/
class ProjectTaskPushLog extends AbstractModel
{
use SoftDeletes;
}

View File

@@ -13,6 +13,7 @@ namespace App\Models;
* @property int|null $owner 是否任务负责人
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\ProjectTask|null $projectTask
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskUser query()
@@ -29,4 +30,45 @@ namespace App\Models;
class ProjectTaskUser extends AbstractModel
{
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function projectTask(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(ProjectTask::class, 'id', 'task_id');
}
/**
* 移交任务身份
* @param $originalUserid
* @param $newUserid
* @return void
*/
public static function transfer($originalUserid, $newUserid)
{
self::whereUserid($originalUserid)->chunk(100, function ($list) use ($originalUserid, $newUserid) {
$tastIds = [];
/** @var self $item */
foreach ($list as $item) {
$row = self::whereTaskId($item->task_id)->whereUserid($newUserid)->first();
if ($row) {
// 已存在则删除原数据,判断改变已存在的数据
$row->owner = max($row->owner, $item->owner);
$row->save();
$item->delete();
} else {
// 不存在则改变原数据
$item->userid = $newUserid;
$item->save();
}
if ($item->projectTask) {
$item->projectTask->addLog("移交{任务}身份", ['userid' => [$originalUserid, ' => ',$newUserid]]);
if (!in_array($item->task_pid, $tastIds)) {
$tastIds[] = $item->task_pid;
$item->projectTask->syncDialogUser();
}
}
}
});
}
}

View File

@@ -2,8 +2,6 @@
namespace App\Models;
use App\Module\Base;
/**
* App\Models\ProjectUser
*
@@ -11,6 +9,7 @@ use App\Module\Base;
* @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 $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Project|null $project
@@ -21,6 +20,7 @@ use App\Module\Base;
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereOwner($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereProjectId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereTopAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereUserid($value)
* @mixin \Eloquent
@@ -36,6 +36,41 @@ class ProjectUser extends AbstractModel
return $this->hasOne(Project::class, 'id', 'project_id');
}
/**
* 移交项目身份
* @param $originalUserid
* @param $newUserid
* @return void
*/
public static function transfer($originalUserid, $newUserid)
{
self::whereUserid($originalUserid)->chunkById(100, function ($list) use ($originalUserid, $newUserid) {
/** @var self $item */
foreach ($list as $item) {
$row = self::whereProjectId($item->project_id)->whereUserid($newUserid)->first();
if ($row) {
// 已存在则删除原数据,判断改变已存在的数据
$row->owner = max($row->owner, $item->owner);
$row->save();
$item->delete();
} else {
// 不存在则改变原数据
$item->userid = $newUserid;
$item->save();
}
if ($item->project) {
if ($item->project->personal) {
$name = User::userid2nickname($originalUserid) ?: ('ID:' . $originalUserid);
$item->project->name = "{$name}{$item->project->name}";
$item->project->save();
}
$item->project->addLog("移交项目身份", ['userid' => [$originalUserid, ' => ', $newUserid]]);
$item->project->syncDialogUser();
}
}
});
}
/**
* 退出项目
*/
@@ -45,15 +80,13 @@ class ProjectUser extends AbstractModel
->whereUserid($this->userid)
->chunk(100, function ($list) {
$tastIds = [];
/** @var ProjectTaskUser $item */
foreach ($list as $item) {
$item->delete();
if (!in_array($item->task_pid, $tastIds)) {
$tastIds[] = $item->task_pid;
$item->projectTask?->syncDialogUser();
}
$item->delete();
}
$tasks = ProjectTask::whereIn('id', $tastIds)->get();
foreach ($tasks as $task) {
$task->syncDialogUser();
}
});
$this->delete();

156
app/Models/Report.php Normal file
View File

@@ -0,0 +1,156 @@
<?php
namespace App\Models;
use App\Exceptions\ApiException;
use Carbon\Carbon;
use Carbon\Traits\Creator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use JetBrains\PhpStorm\Pure;
/**
* App\Models\Report
*
* @property int $id
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string $title 标题
* @property string $type 汇报类型
* @property int $userid
* @property string $content
* @property string $sign 汇报唯一标识
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ReportReceive> $Receives
* @property-read int|null $receives_count
* @property-read mixed $receives
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $receivesUser
* @property-read int|null $receives_user_count
* @property-read \App\Models\User|null $sendUser
* @method static Builder|Report newModelQuery()
* @method static Builder|Report newQuery()
* @method static Builder|Report query()
* @method static Builder|Report whereContent($value)
* @method static Builder|Report whereCreatedAt($value)
* @method static Builder|Report whereId($value)
* @method static Builder|Report whereSign($value)
* @method static Builder|Report whereTitle($value)
* @method static Builder|Report whereType($value)
* @method static Builder|Report whereUpdatedAt($value)
* @method static Builder|Report whereUserid($value)
* @mixin \Eloquent
*/
class Report extends AbstractModel
{
use HasFactory;
const WEEKLY = "weekly";
const DAILY = "daily";
protected $fillable = [
"title",
"type",
"userid",
"content",
];
protected $appends = [
'receives',
];
public function Receives(): HasMany
{
return $this->hasMany(ReportReceive::class, "rid");
}
public function receivesUser(): BelongsToMany
{
return $this->belongsToMany(User::class, ReportReceive::class, "rid", "userid")
->withPivot("receive_time", "read");
}
public function sendUser()
{
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);
}
public function getReceivesAttribute()
{
if (!isset($this->appendattrs['receives'])) {
$this->appendattrs['receives'] = empty( $this->receivesUser ) ? [] : array_column($this->receivesUser->toArray(), "userid");
}
return $this->appendattrs['receives'];
}
/**
* 获取单条记录
* @param $id
* @return Report|Builder|Model|object|null
* @throw ApiException
*/
public static function getOne($id)
{
$one = self::whereId($id)->first();
if (empty($one))
throw new ApiException("记录不存在");
return $one;
}
/**
* 获取最后一条提交记录
* @param User|null $user
* @return Builder|Model|\Illuminate\Database\Query\Builder|object
*/
public static function getLastOne(User $user = null)
{
$user === null && $user = User::auth();
$one = self::whereUserid($user->userid)->orderByDesc("created_at")->first();
if ( empty($one) )
throw new ApiException("记录不存在");
return $one;
}
/**
* 生成唯一标识
* @param $type
* @param $offset
* @param Carbon|null $time
* @return string
*/
public static function generateSign($type, $offset, Carbon $time = null): string
{
$user = User::auth();
$now_dt = $time === null ? Carbon::now() : $time;
$time_s = match ($type) {
Report::WEEKLY => function() use ($now_dt, $offset) {
// 如果设置了周期偏移量
empty( $offset ) || $now_dt->subWeeks( abs( $offset ) );
$now_dt->startOfWeek(); // 设置为当周第一天
return $now_dt->year . $now_dt->weekOfYear;
},
Report::DAILY => function() use ($now_dt, $offset) {
// 如果设置了周期偏移量
empty( $offset ) || $now_dt->subDays( abs( $offset ) );
return $now_dt->format("Ymd");
},
default => "",
};
return $user->userid . ( is_callable($time_s) ? $time_s() : "" );
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* App\Models\ReportReceive
*
* @property int $id
* @property int $rid
* @property string|null $receive_time 接收时间
* @property int $userid 接收人
* @property int $read 是否已读
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive query()
* @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 whereRid($value)
* @method static \Illuminate\Database\Eloquent\Builder|ReportReceive whereUserid($value)
* @mixin \Eloquent
*/
class ReportReceive extends AbstractModel
{
use HasFactory;
// 关闭时间戳自动写入
public $timestamps = false;
protected $fillable = [
"rid",
"receive_time",
"userid",
"read",
];
}

View File

@@ -2,6 +2,8 @@
namespace App\Models;
use App\Module\Base;
/**
* App\Models\Setting
*
@@ -24,5 +26,40 @@ namespace App\Models;
*/
class Setting extends AbstractModel
{
/**
* 验证邮箱地址(过滤忽略地址)
* @param $array
* @param \Closure $resultClosure
* @param \Closure|null $emptyClosure
* @return array|mixed
*/
public static function validateAddr($array, $resultClosure, $emptyClosure = null)
{
if (!is_array($array)) {
$array = [$array];
}
$ignoreAddr = Base::settingFind('emailSetting', 'ignore_addr');
$ignoreAddr = explode("\n", $ignoreAddr);
$ignoreArray = ['admin@dootask.com', 'test@dootask.com'];
foreach ($ignoreAddr as $item) {
if (Base::isEmail($item)) {
$ignoreArray[] = trim($item);
}
}
if ($ignoreArray) {
$array = array_diff($array, $ignoreArray);
}
if ($array) {
if ($resultClosure instanceof \Closure) {
foreach ($array as $value) {
$resultClosure($value);
}
}
} else {
if ($emptyClosure instanceof \Closure) {
$emptyClosure();
}
}
return $array;
}
}

37
app/Models/TaskWorker.php Normal file
View File

@@ -0,0 +1,37 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* App\Models\TaskWorker
*
* @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 $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker query()
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereArgs($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereEndAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereError($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereStartAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker withoutTrashed()
* @mixin \Eloquent
*/
class TaskWorker extends AbstractModel
{
use SoftDeletes;
}

171
app/Models/UmengAlias.php Normal file
View File

@@ -0,0 +1,171 @@
<?php
namespace App\Models;
use App\Module\Base;
use Carbon\Carbon;
use Hedeqiang\UMeng\Android;
use Hedeqiang\UMeng\IOS;
/**
* App\Models\UmengAlias
*
* @property int $id
* @property int|null $userid 会员ID
* @property string|null $alias 别名
* @property string|null $platform 平台类型
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias query()
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereAlias($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias wherePlatform($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereUserid($value)
* @mixin \Eloquent
*/
class UmengAlias extends AbstractModel
{
protected $table = 'umeng_alias';
/**
* 获取推送配置
* @return array|false
*/
public static function getPushConfig()
{
$setting = Base::setting('appPushSetting');
if ($setting['push'] !== 'open') {
return false;
}
$config = [];
if ($setting['ios_key']) {
$config['iOS'] = [
'appKey' => $setting['ios_key'],
'appMasterSecret' => $setting['ios_secret'],
'production_mode' => true,
];
}
if ($setting['android_key']) {
$config['Android'] = [
'appKey' => $setting['android_key'],
'appMasterSecret' => $setting['android_secret'],
'production_mode' => true,
];
}
return $config;
}
/**
* 推送消息
* @param string $alias
* @param string $platform
* @param array $array [title, subtitle, body, description, extra, seconds, badge]
* @return array|false
*/
public static function pushMsgToAlias($alias, $platform, $array)
{
$config = self::getPushConfig();
if ($config === false) {
return false;
}
//
$title = $array['title'] ?: ''; // 标题
$subtitle = $array['subtitle'] ?: ''; // 副标题iOS
$body = $array['body'] ?: ''; // 通知内容
$description = $array['description'] ?: 'no description'; // 描述
$extra = is_array($array['extra']) ? $array['extra'] : []; // 额外参数
$seconds = intval($array['seconds']) ?: 86400; // 有效时间(单位:秒)
$badge = intval($array['badge']) ?: 0; // 角标数iOS
//
switch ($platform) {
case 'ios':
if (!isset($config['iOS'])) {
return false;
}
$ios = new IOS($config);
return $ios->send([
'description' => $description,
'payload' => array_merge([
'aps' => [
'alert' => [
'title' => $title,
'subtitle' => $subtitle,
'body' => $body,
],
'sound' => 'default',
'badge' => $badge,
],
], $extra),
'type' => 'customizedcast',
'alias_type' => 'userid',
'alias' => $alias,
'policy' => [
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
],
]);
case 'android':
if (!isset($config['Android'])) {
return false;
}
$android = new Android($config);
return $android->send([
'description' => $description,
'payload' => array_merge([
'display_type' => 'notification',
'body' => [
'ticker' => $title,
'text' => $body,
'title' => $title,
'after_open' => 'go_app',
'play_sound' => true,
],
], $extra),
'type' => 'customizedcast',
'alias_type' => 'userid',
'alias' => $alias,
'mipush' => true,
'mi_activity' => 'app.eeui.umeng.activity.MfrMessageActivity',
'policy' => [
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
]
]);
default:
return false;
}
}
/**
* 推送给指定会员
* @param array|int $userid
* @param array $array
* @return void
*/
public static function pushMsgToUserid($userid, $array)
{
$builder = self::select(['id', 'platform', 'alias', 'userid'])->where('updated_at', '>', Carbon::now()->subMonth());
if (is_array($userid)) {
$builder->whereIn('userid', $userid);
} elseif (Base::isNumber($userid)) {
$builder->whereUserid($userid);
}
$builder
->orderByDesc('updated_at')
->chunkById(100, function ($datas) use ($array) {
$uids = $datas->groupBy('userid');
foreach ($uids as $uid => $rows) {
$array['badge'] = WebSocketDialogMsgRead::whereUserid($uid)->whereSilence(0)->whereReadAt(null)->count();
$lists = $rows->take(5)->groupBy('platform'); // 每个会员最多推送5个别名
foreach ($lists as $platform => $list) {
$alias = $list->pluck('alias')->implode(',');
self::pushMsgToAlias($alias, $platform, $array);
}
}
});
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use App\Module\Doo;
use Cache;
use Carbon\Carbon;
@@ -13,8 +14,11 @@ use Carbon\Carbon;
*
* @property int $userid
* @property array $identity 身份
* @property array $department 所属部门
* @property string|null $az A-Z
* @property string|null $pinyin 拼音(主要用于搜索)
* @property string|null $email 邮箱
* @property string|null $tel 联系电话
* @property string $nickname 昵称
* @property string|null $profession 职位/职称
* @property string $userimg 头像
@@ -28,7 +32,9 @@ use Carbon\Carbon;
* @property string|null $line_at 最后在线时间(接口)
* @property int|null $task_dialog_id 最后打开的任务会话ID
* @property string|null $created_ip 注册IP
* @property string|null $disable_at 禁用时间
* @property string|null $disable_at 禁用时间(离职时间)
* @property int|null $email_verity 邮箱是否已验证
* @property int|null $bot 是否机器人
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Database\Factories\UserFactory factory(...$parameters)
@@ -36,11 +42,14 @@ use Carbon\Carbon;
* @method static \Illuminate\Database\Eloquent\Builder|User newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|User query()
* @method static \Illuminate\Database\Eloquent\Builder|User whereAz($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereBot($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereChangepass($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereCreatedIp($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereDepartment($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereDisableAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereEmail($value)
* @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 whereLastAt($value)
@@ -50,8 +59,10 @@ use Carbon\Carbon;
* @method static \Illuminate\Database\Eloquent\Builder|User whereLoginNum($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereNickname($value)
* @method static \Illuminate\Database\Eloquent\Builder|User wherePassword($value)
* @method static \Illuminate\Database\Eloquent\Builder|User wherePinyin($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereProfession($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereTaskDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereTel($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|User whereUserimg($value)
@@ -62,22 +73,14 @@ class User extends AbstractModel
protected $primaryKey = 'userid';
protected $hidden = [
'disable_at',
'updated_at',
];
/**
* 更新数据校验
* @param array $param
*/
public function updateInstance(array $param)
{
parent::updateInstance($param);
//
if (isset($param['line_at']) && $this->userid) {
Cache::put("User::online:" . $this->userid, time(), Carbon::now()->addSeconds(30));
}
}
// 默认头像类型auto自动生成system系统默认
public static $defaultAvatarMode = 'auto';
// 基本信息的字段
public static $basicField = ['userid', 'email', 'nickname', 'profession', 'department', 'userimg', 'bot', 'az', 'pinyin', 'line_at', 'disable_at'];
/**
* 昵称
@@ -96,11 +99,7 @@ class User extends AbstractModel
*/
public function getUserimgAttribute($value)
{
if ($value) {
return Base::fillUrl($value);
}
$name = ($this->userid - 1) % 21 + 1;
return url("images/avatar/default_{$name}.png");
return self::getAvatar($this->userid, $value, $this->email, $this->nickname);
}
/**
@@ -116,13 +115,71 @@ class User extends AbstractModel
return array_filter(is_array($value) ? $value : explode(",", trim($value, ",")));
}
/**
* 部门
* @param $value
* @return array
*/
public function getDepartmentAttribute($value)
{
if (empty($value)) {
return [];
}
return array_filter(is_array($value) ? $value : Base::explodeInt($value));
}
/**
* 获取所属部门名称
* @return string
*/
public function getDepartmentName()
{
if (empty($this->department)) {
return "";
}
$key = "UserDepartment::" . md5(Cache::get("UserDepartment::rand") . '-' . implode(',' , $this->department));
$list = Cache::remember($key, now()->addMonth(), function() {
$list = UserDepartment::select(['id', 'owner_userid', 'name'])->whereIn('id', $this->department)->take(10)->get();
return $list->toArray();
});
$array = [];
foreach ($list as $item) {
$array[] = $item['name'] . ($item['owner_userid'] === $this->userid ? '(M)' : '');
}
return implode(', ', $array);
}
/**
* 判断是否为部门负责人
*/
public function isDepartmentOwner()
{
return UserDepartment::where('owner_userid', $this->userid)->exists();
}
/**
* 获取机器人所有者
* @return int|mixed
*/
public function getBotOwner()
{
if (!$this->bot) {
return 0;
}
$key = "BotOwner::" . $this->userid;
return Cache::remember($key, now()->addMonth(), function() {
return intval(UserBot::whereBotId($this->userid)->value('userid'));
});
}
/**
* 是否在线
* @return bool
*/
public function getOnlineStatus()
{
$online = intval(Cache::get("User::online:" . $this->userid, 0));
$online = $this->bot || Cache::get("User::online:" . $this->userid) === "on";
if ($online) {
return true;
}
@@ -130,9 +187,45 @@ class User extends AbstractModel
}
/**
* 判断是否管理员
* 返回是否LDAP用户
* @return bool
*/
public function isLdap()
{
return in_array('ldap', $this->identity);
}
/**
* 返回是否临时帐号
* @return bool
*/
public function isTemp()
{
return in_array('temp', $this->identity);
}
/**
* 返回是否禁用帐号(离职)
* @return bool
*/
public function isDisable()
{
return in_array('disable', $this->identity);
}
/**
* 返回是否管理员
* @return bool
*/
public function isAdmin()
{
return in_array('admin', $this->identity);
}
/**
* 判断是否管理员
*/
public function checkAdmin()
{
$this->identity('admin');
}
@@ -167,6 +260,36 @@ class User extends AbstractModel
}
}
/**
* 删除会员
* @param $reason
* @return bool|null
*/
public function deleteUser($reason)
{
return AbstractModel::transaction(function () use ($reason) {
// 删除原因
$userDelete = UserDelete::createInstance([
'operator' => User::userid(),
'userid' => $this->userid,
'email' => $this->email,
'reason' => $reason,
'cache' => array_merge($this->getRawOriginal(), [
'department_name' => $this->getDepartmentName()
])
]);
$userDelete->save();
// 删除未读
WebSocketDialogMsgRead::whereUserid($this->userid)->delete();
// 删除待办
WebSocketDialogMsgTodo::whereUserid($this->userid)->delete();
// 删除邮箱验证记录
UserEmailVerification::whereEmail($this->email)->delete();
//
return $this->delete();
});
}
/** ***************************************************************************************** */
/** ***************************************************************************************** */
/** ***************************************************************************************** */
@@ -180,99 +303,47 @@ class User extends AbstractModel
*/
public static function reg($email, $password, $other = [])
{
//邮箱
if (!Base::isMail($email)) {
// 邮箱
if (!Base::isEmail($email)) {
throw new ApiException('请输入正确的邮箱地址');
}
if (User::email2userid($email) > 0) {
$user = self::whereEmail($email)->first();
if ($user) {
$isRegVerify = Base::settingFind('emailSetting', 'reg_verify') === 'open';
if ($isRegVerify && $user->email_verity === 0) {
UserEmailVerification::userEmailSend($user);
throw new ApiException('您的帐号已注册过,请验证邮箱', ['code' => 'email']);
}
throw new ApiException('邮箱地址已存在');
}
//密码
// 密码
self::passwordPolicy($password);
//开始注册
$encrypt = Base::generatePassword(6);
$inArray = [
'encrypt' => $encrypt,
'email' => $email,
'password' => Base::md52($password, $encrypt),
'created_ip' => Base::getIp(),
];
// 开始注册
$user = Doo::userCreate($email, $password);
if ($other) {
$inArray = array_merge($inArray, $other);
$user->updateInstance($other);
}
$user->az = Base::getFirstCharter($user->nickname);
$user->pinyin = Base::cn2pinyin($user->nickname);
$user->created_ip = Base::getIp();
if ($user->save()) {
$setting = Base::setting('system');
$reg_identity = $setting['reg_identity'] ?: 'normal';
$all_group_autoin = $setting['all_group_autoin'] ?: 'yes';
// 注册临时身份
if ($reg_identity === 'temp') {
$user->identity = Base::arrayImplode(array_merge(array_diff($user->identity, ['temp']), ['temp']));
$user->save();
}
// 加入全员群组
if ($all_group_autoin === 'yes') {
$dialog = WebSocketDialog::whereGroupType('all')->orderByDesc('id')->first();
$dialog?->joinGroup($user->userid, 0);
}
}
$user = User::createInstance($inArray);
$user->save();
User::AZUpdate($user->userid);
return $user->find($user->userid);
}
/**
* 邮箱获取userid
* @param $email
* @return int
*/
public static function email2userid($email)
{
if (empty($email)) {
return 0;
}
return intval(self::whereEmail($email)->value('userid'));
}
/**
* token获取会员userid
* @return int
*/
public static function token2userid()
{
return self::authFind('userid', Base::getToken());
}
/**
* token获取会员邮箱
* @return int
*/
public static function token2email()
{
return self::authFind('email', Base::getToken());
}
/**
* token获取encrypt
* @return mixed|string
*/
public static function token2encrypt()
{
return self::authFind('encrypt', Base::getToken());
}
/**
* 获取token身份信息
* @param $find
* @param null $token
* @return array|mixed|string
*/
public static function authFind($find, $token = null)
{
if ($token === null) {
$token = Base::getToken();
}
list($userid, $email, $encrypt, $timestamp) = explode("#$", base64_decode($token) . "#$#$#$#$");
$array = [
'userid' => intval($userid),
'email' => $email ?: '',
'encrypt' => $encrypt ?: '',
'timestamp' => intval($timestamp),
];
if (isset($array[$find])) {
return $array[$find];
}
if ($find == 'all') {
return $array;
}
return '';
}
/**
* 获取我的ID
* @return int
@@ -308,8 +379,7 @@ class User extends AbstractModel
{
$user = self::authInfo();
if (!$user) {
$authorization = Base::getToken();
if ($authorization) {
if (Base::headerOrInput('token')) {
throw new ApiException('身份已失效,请重新登录', [], -1);
} else {
throw new ApiException('请登录后继续...', [], -1);
@@ -334,57 +404,49 @@ class User extends AbstractModel
if (isset($_A["__static_auth"])) {
return $_A["__static_auth"];
}
$authorization = Base::getToken();
if ($authorization) {
$authInfo = self::authFind('all', $authorization);
if ($authInfo['userid'] > 0) {
$loginValid = floatval(Base::settingFind('system', 'loginValid')) ?: 720;
$loginValid *= 3600;
if ($authInfo['timestamp'] + $loginValid > time()) {
$row = self::whereUserid($authInfo['userid'])->whereEmail($authInfo['email'])->whereEncrypt($authInfo['encrypt'])->first();
if ($row) {
$upArray = [];
if (Base::getIp() && $row->line_ip != Base::getIp()) {
$upArray['line_ip'] = Base::getIp();
}
if (Carbon::parse($row->line_at)->addSeconds(30)->lt(Carbon::now())) {
$upArray['line_at'] = Carbon::now();
}
if ($upArray) {
$row->updateInstance($upArray);
$row->save();
}
return $_A["__static_auth"] = $row;
}
}
if (Doo::userId() > 0
&& !Doo::userExpired()
&& $user = self::whereUserid(Doo::userId())->whereEmail(Doo::userEmail())->whereEncrypt(Doo::userEncrypt())->first()) {
$upArray = [];
if (Base::getIp() && $user->line_ip != Base::getIp()) {
$upArray['line_ip'] = Base::getIp();
}
if (Carbon::parse($user->line_at)->addSeconds(30)->lt(Carbon::now())) {
$upArray['line_at'] = Carbon::now();
}
if ($upArray) {
$user->updateInstance($upArray);
$user->save();
}
return $_A["__static_auth"] = $user;
}
return $_A["__static_auth"] = false;
}
/**
* 生成token
* 生成 token
* @param self $userinfo
* @param bool $refresh 获取新的token
* @return string
*/
public static function token($userinfo)
public static function generateToken($userinfo, $refresh = false)
{
$userinfo->token = base64_encode($userinfo->userid . '#$' . $userinfo->email . '#$' . $userinfo->encrypt . '#$' . time() . '#$' . Base::generatePassword(6));
if (!$refresh) {
if (Doo::userId() != $userinfo->userid
|| Doo::userEmail() != $userinfo->email
|| Doo::userEncrypt() != $userinfo->encrypt) {
$refresh = true;
}
}
if ($refresh) {
$days = $userinfo->bot ? 0 : max(1, intval(Base::settingFind('system', 'token_valid_days', 30)));
$token = Doo::tokenEncode($userinfo->userid, $userinfo->email, $userinfo->encrypt, $days);
} else {
$token = Doo::userToken();
}
unset($userinfo->encrypt);
unset($userinfo->password);
return $userinfo->token;
}
/**
* 判断用户权限(身份)
* @param $identity
* @param $userIdentity
* @return bool
*/
public static function identityRaw($identity, $userIdentity)
{
$userIdentity = is_array($userIdentity) ? $userIdentity : explode(",", trim($userIdentity, ","));
return $identity && in_array($identity, $userIdentity);
return $userinfo->token = $token;
}
/**
@@ -402,10 +464,10 @@ class User extends AbstractModel
if (isset($_A["__static_userid2basic_" . $userid])) {
return $_A["__static_userid2basic_" . $userid];
}
$fields = ['userid', 'email', 'nickname', 'profession', 'userimg'];
$userInfo = self::whereUserid($userid)->select($fields)->first();
$userInfo = self::whereUserid($userid)->select(User::$basicField)->first();
if ($userInfo) {
$userInfo->online = $userInfo->getOnlineStatus();
$userInfo->department_name = $userInfo->getDepartmentName();
}
return $_A["__static_userid2basic_" . $userid] = ($userInfo ?: []);
}
@@ -418,21 +480,7 @@ class User extends AbstractModel
*/
public static function userid2nickname($userid)
{
$basic = self::userid2basic($userid);
return $basic ? $basic->nickname : '';
}
/**
* 更新首字母
* @param $userid
*/
public static function AZUpdate($userid)
{
$row = self::whereUserid($userid)->first();
if ($row) {
$row->az = Base::getFirstCharter($row->nickname);
$row->save();
}
return self::userid2basic($userid)?->nickname ?: '';
}
/**
@@ -459,6 +507,44 @@ class User extends AbstractModel
}
}
/**
* 获取头像
* @param $userid
* @param $userimg
* @param $email
* @param $nickname
* @return string
*/
public static function getAvatar($userid, $userimg, $email, $nickname)
{
// 自定义头像
if ($userimg && !str_contains($userimg, 'avatar/')) {
return Base::fillUrl($userimg);
}
// 机器人头像
switch ($email) {
case 'system-msg@bot.system':
return url("images/avatar/default_system.png");
case 'task-alert@bot.system':
return url("images/avatar/default_task.png");
case 'check-in@bot.system':
return url("images/avatar/default_checkin.png");
case 'anon-msg@bot.system':
return url("images/avatar/default_anon.png");
case 'approval-alert@bot.system':
return url("images/avatar/default_approval.png");
case 'bot-manager@bot.system':
return url("images/avatar/default_bot.png");
}
// 生成文字头像
if (self::$defaultAvatarMode === 'auto') {
return url("avatar/" . urlencode($nickname) . ".png");
}
// 系统默认头像
$name = ($userid - 1) % 21 + 1;
return url("images/avatar/default_{$name}.png");
}
/**
* 检测密码策略是否符合
* @param $password
@@ -489,4 +575,63 @@ class User extends AbstractModel
}
}
}
/**
* 获取机器人或创建
* @param $key
* @param $update
* @param $userid
* @return self|null
*/
public static function botGetOrCreate($key, $update = [], $userid = 0)
{
$email = "{$key}@bot.system";
$botUser = self::whereEmail($email)->first();
if (empty($botUser)) {
$botUser = Doo::userCreate($email, Base::generatePassword(32));
if (empty($botUser)) {
return null;
}
$botUser->updateInstance([
'created_ip' => Base::getIp(),
]);
$botUser->save();
if ($userid > 0) {
UserBot::createInstance([
'userid' => $userid,
'bot_id' => $botUser->userid,
])->save();
}
//
switch ($key) {
case 'system-msg':
$update['nickname'] = '系统消息';
break;
case 'task-alert':
$update['nickname'] = '任务提醒';
break;
case 'check-in':
$update['nickname'] = '签到打卡';
break;
case 'anon-msg':
$update['nickname'] = '匿名消息';
break;
case 'approval-alert':
$update['nickname'] = '审批';
break;
case 'bot-manager':
$update['nickname'] = '机器人管理';
break;
}
}
if ($update) {
$botUser->updateInstance($update);
if (isset($update['nickname'])) {
$botUser->az = Base::getFirstCharter($botUser->nickname);
$botUser->pinyin = Base::cn2pinyin($botUser->nickname);
}
$botUser->save();
}
return $botUser;
}
}

127
app/Models/UserBot.php Normal file
View File

@@ -0,0 +1,127 @@
<?php
namespace App\Models;
use App\Module\Doo;
use App\Module\Extranet;
use Cache;
use Carbon\Carbon;
/**
* App\Models\UserBot
*
* @property int $id
* @property int|null $userid 所属人ID
* @property int|null $bot_id 机器人ID
* @property int|null $clear_day 消息自动清理天数
* @property string|null $clear_at 下一次清理时间
* @property string|null $webhook_url 消息webhook地址
* @property int|null $webhook_num 消息webhook请求次数
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|UserBot newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserBot newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserBot query()
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereBotId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereClearAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereClearDay($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereWebhookNum($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereWebhookUrl($value)
* @mixin \Eloquent
*/
class UserBot extends AbstractModel
{
/**
* 机器人菜单
* @param $email
* @return array|array[]
*/
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('我的机器人')
],
],
default => [],
};
}
/**
* 签到机器人
* @param $command
* @param $userid
* @return string
*/
public static function checkinBotQuickMsg($command, $userid)
{
if (Cache::get("UserBot::checkinBotQuickMsg:{$userid}") === "yes") {
return "操作频繁!";
}
Cache::put("UserBot::checkinBotQuickMsg:{$userid}", "yes", Carbon::now()->addSecond());
//
$text = match ($command) {
"checkin" => "暂未开放手动签到。",
default => Extranet::checkinBotQuickMsg($command),
};
return $text ?: '维护中...';
}
/**
* 隐私机器人
* @param $command
* @return string
*/
public static function anonBotQuickMsg($command)
{
return match ($command) {
"help" => "使用说明:打开你想要发匿名消息的个人对话,点击输入框右边的 ⊕ 号,选择 <u>匿名消息</u> 即可输入你想要发送的匿名消息内容。",
"privacy" => "匿名消息将通过 <u>匿名消息(机器人)</u> 发送给对方,不会记录你的身份信息。",
default => '',
};
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
/**
* App\Models\UserCheckinMac
*
* @property int $id
* @property int|null $userid 会员id
* @property string|null $mac MAC地址
* @property string|null $remark 备注
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac query()
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac whereMac($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac whereRemark($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinMac whereUserid($value)
* @mixin \Eloquent
*/
class UserCheckinMac extends AbstractModel
{
/**
* 保存mac地址
* @param $userid
* @param $array
* @return mixed
*/
public static function saveMac($userid, $array)
{
return AbstractModel::transaction(function() use ($array, $userid) {
$ids = [];
$list = [];
foreach ($array as $item) {
if (self::whereMac($item['mac'])->where('userid', '!=', $userid)->exists()) {
throw new ApiException("{$item['mac']} 已被其他成员设置");
}
$update = [];
if ($item['remark']) {
$update = [
'remark' => $item['remark']
];
}
$row = self::updateInsert([
'userid' => $userid,
'mac' => $item['mac']
], $update);
if ($row) {
$ids[] = $row->id;
$list[] = $row;
}
}
self::whereUserid($userid)->whereNotIn('id', $ids)->delete();
//
return Base::retSuccess('修改成功', $list);
});
}
}

View File

@@ -0,0 +1,134 @@
<?php
namespace App\Models;
use App\Module\Base;
/**
* App\Models\UserCheckinRecord
*
* @property int $id
* @property int|null $userid 会员id
* @property string|null $mac MAC地址
* @property string|null $date 签到日期
* @property array $times 签到时间
* @property int|null $report_time 上报的时间戳
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord query()
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereDate($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereMac($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereReportTime($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereTimes($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserCheckinRecord whereUserid($value)
* @mixin \Eloquent
*/
class UserCheckinRecord extends AbstractModel
{
/**
* 签到记录
* @param $value
* @return array
*/
public function getTimesAttribute($value)
{
if (is_array($value)) {
return $value;
}
return Base::json2array($value);
}
/**
* 获取签到时间
* @param int $userid
* @param array $betweenTimes
* @return array
*/
public static function getTimes(int $userid, array $betweenTimes)
{
$array = [];
$records = self::whereUserid($userid)->whereBetween('created_at', $betweenTimes)->orderBy('id')->get();
/** @var self $record */
foreach ($records as $record) {
$times = array_map(function ($time) {
return preg_replace("/(\d+):(\d+):\d+$/", "$1:$2", $time);
}, $record->times);
if (isset($array[$record->date])) {
$array[$record->date] = array_merge($array[$record->date], $times);
} else {
$array[$record->date] = $times;
}
}
//
foreach ($array as $date => $times) {
$times = array_values(array_filter(array_unique($times)));
$inOrder = [];
foreach ($times as $key => $time) {
$inOrder[$key] = strtotime("2022-01-01 {$time}");
}
array_multisort($inOrder, SORT_ASC, $times);
$array[$date] = $times;
}
//
return $array;
}
/**
* 时间收集
* @param string $data
* @param array $times
* @return \Illuminate\Support\Collection
*/
public static function atCollect($data, $times)
{
$sameTimes = array_map(function($time) use ($data) {
return [
"datetime" => "{$data} {$time}",
"timestamp" => strtotime("{$data} {$time}")
];
}, $times);
return collect($sameTimes);
}
/**
* 签到时段
* @param array $times
* @param int $diff 多长未签到算失效(秒)
* @return array
*/
public static function atSection($times, $diff = 3600)
{
$start = "";
$end = "";
$array = [];
foreach ($times as $time) {
$time = preg_replace("/(\d+):(\d+):\d+$/", "$1:$2", $time);
if (empty($start)) {
$start = $time;
continue;
}
if (empty($end)) {
$end = $time;
continue;
}
if (strtotime("2022-01-01 {$time}") - strtotime("2022-01-01 {$end}") > $diff) {
$array[] = [$start, $end];
$start = $time;
$end = "";
continue;
}
$end = $time;
}
if ($start) {
$array[] = [$start, $end];
}
return $array;
}
}

71
app/Models/UserDelete.php Normal file
View File

@@ -0,0 +1,71 @@
<?php
namespace App\Models;
use App\Module\Base;
/**
* App\Models\UserDelete
*
* @property int $id
* @property int|null $operator 操作人员
* @property int|null $userid 用户id
* @property string|null $email 邮箱帐号
* @property string|null $reason 注销原因
* @property string $cache 会员资料缓存
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete query()
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete whereCache($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete whereEmail($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete whereOperator($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete whereReason($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDelete whereUserid($value)
* @mixin \Eloquent
*/
class UserDelete extends AbstractModel
{
/**
* 昵称
* @param $value
* @return string
*/
public function getCacheAttribute($value)
{
if (!is_array($value)) {
$value = Base::json2array($value);
// 昵称
if (!$value['nickname']) {
$value['nickname'] = Base::cardFormat($value['email']);
}
// 头像
$value['userimg'] = User::getAvatar($value['userid'], $value['userimg'], $value['email'], $value['nickname']);
// 部门
$value['department'] = array_filter(is_array($value['department']) ? $value['department'] : Base::explodeInt($value['department']));
}
return $value;
}
/**
* userid 获取 基础信息
* @param int $userid 会员ID
* @return array|null
*/
public static function userid2basic($userid)
{
$row = self::whereUserid($userid)->first();
if (empty($row) || empty($row->cache)) {
return null;
}
$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');
return $cache;
}
}

View File

@@ -0,0 +1,152 @@
<?php
namespace App\Models;
use App\Exceptions\ApiException;
/**
* App\Models\UserDepartment
*
* @property int $id
* @property string|null $name 部门名称
* @property int|null $dialog_id 聊天会话ID
* @property int|null $parent_id 上级部门
* @property int|null $owner_userid 部门负责人
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment query()
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereOwnerUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereParentId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserDepartment whereUpdatedAt($value)
* @mixin \Eloquent
*/
class UserDepartment extends AbstractModel
{
/**
* 保存部门
* @param $data
* @param $dialogUseid
*/
public function saveDepartment($data = [], $dialogUseid = 0) {
AbstractModel::transaction(function () use ($dialogUseid, $data) {
$oldUser = null;
$newUser = null;
if ($data['owner_userid'] !== $this->owner_userid) {
$oldUser = User::find($this->owner_userid);
$newUser = User::find($data['owner_userid']);
}
$this->updateInstance($data);
//
if ($this->dialog_id > 0) {
// 已有群
$dialog = WebSocketDialog::find($this->dialog_id);
if ($dialog) {
$dialog->name = $this->name;
$dialog->owner_id = $this->owner_userid;
if ($dialog->save()) {
$dialog->joinGroup($this->owner_userid, 0, true);
$dialog->pushMsg("groupUpdate", [
'id' => $dialog->id,
'name' => $dialog->name,
'owner_id' => $dialog->owner_id,
]);
}
}
} elseif ($dialogUseid > 0) {
// 使用现有群
$dialog = WebSocketDialog::whereType('group')->whereGroupType('user')->find($dialogUseid);
if (empty($dialog)) {
throw new ApiException("选择现有聊天群不存在");
}
$dialog->name = $this->name;
$dialog->owner_id = $this->owner_userid;
$dialog->group_type = 'department';
if ($dialog->save()) {
$dialog->joinGroup($this->owner_userid, 0, true);
$dialog->pushMsg("groupUpdate", [
'id' => $dialog->id,
'name' => $dialog->name,
'owner_id' => $dialog->owner_id,
'group_type' => $dialog->group_type,
]);
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'notice', [
'notice' => User::nickname() . " 将此群改为部门群"
], User::userid(), true, true);
}
$this->dialog_id = $dialog->id;
} else {
// 创建群
$dialog = WebSocketDialog::createGroup($this->name, [$this->owner_userid], 'department', $this->owner_userid);
if (empty($dialog)) {
throw new ApiException("创建群组失败");
}
$this->dialog_id = $dialog->id;
}
$this->save();
//
if ($oldUser) {
$oldUser->department = array_diff($oldUser->department, [$this->id]);
$oldUser->department = "," . implode(",", $oldUser->department) . ",";
$oldUser->save();
}
if ($newUser) {
$newUser->department = array_diff($newUser->department, [$this->id]);
$newUser->department = array_merge($newUser->department, [$this->id]);
$newUser->department = "," . implode(",", $newUser->department) . ",";
$newUser->save();
}
});
}
/**
* 删除部门
* @return void
*/
public function deleteDepartment() {
// 删除子部门
$list = self::whereParentId($this->id)->get();
foreach ($list as $item) {
$item->deleteDepartment();
}
// 移出成员
User::where("department", "like", "%,{$this->id},%")->chunk(100, function($items) {
/** @var User $user */
foreach ($items as $user) {
$user->department = array_diff($user->department, [$this->id]);
$user->department = "," . implode(",", $user->department) . ",";
$user->save();
}
});
// 解散群组
$dialog = WebSocketDialog::find($this->dialog_id);
if ($dialog) {
$dialog->deleteDialog();
}
//
$this->delete();
}
/**
* 移交部门身份
* @param $originalUserid
* @param $newUserid
* @return void
*/
public static function transfer($originalUserid, $newUserid)
{
self::whereOwnerUserid($originalUserid)->chunkById(100, function ($list) use ($originalUserid, $newUserid) {
/** @var self $item */
foreach ($list as $item) {
$item->saveDepartment([
'owner_userid' => $newUserid,
]);
}
});
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use Carbon\Carbon;
use Guanguans\Notify\Factory;
use Guanguans\Notify\Messages\EmailMessage;
/**
* App\Models\UserEmailVerification
*
* @property int $id
* @property int|null $userid 用户id
* @property string|null $code 验证参数
* @property string|null $email 电子邮箱
* @property int|null $status 0-未验证1-已验证
* @property int|null $type 邮件类型1-邮箱认证2-修改邮箱
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification query()
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereCode($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereEmail($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereStatus($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereUserid($value)
* @mixin \Eloquent
*/
class UserEmailVerification extends AbstractModel
{
/**
* 发验证邮箱
* @param User $user
* @param int $type
* @param null $email
*/
public static function userEmailSend(User $user, $type = 1, $email = null)
{
$email = $type == 1 ? $user->email : $email;
$res = self::whereEmail($email)->where('created_at', '>', Carbon::now()->subMinutes(30))->whereType($type)->first();
if ($res && $type == 1) return;
//删除
self::whereUserid($email)->delete();
$code = $type == 1 ? Base::generatePassword(64) : rand(100000, 999999);
$row = self::createInstance([
'userid' => $user->userid,
'email' => $email,
'code' => $code,
'status' => 0,
'type' => $type
]);
$row->save();
$setting = Base::setting('emailSetting');
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>";
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>";
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>";
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']}>")
->to($email)
->subject($subject)
->html($content))
->send();
} catch (\Throwable $e) {
if (str_contains($e->getMessage(), "Timed Out")) {
throw new ApiException("language.TimedOut");
} elseif ($e->getCode() === 550) {
throw new ApiException('邮件内容被拒绝,请检查邮箱是否开启接收功能');
} else {
throw new ApiException($e->getMessage());
}
}
}
/**
* 校验验证码
* @param $email
* @param $code
* @param int $type
* @return bool
*/
public static function verify($email, $code, $type = 1)
{
if (!$code) {
throw new ApiException('请输入验证码');
}
/** @var UserEmailVerification $emailVerify */
$emailVerify = self::whereEmail($email)->whereType($type)->orderByDesc('id')->first();
if (empty($emailVerify) || $emailVerify->code != $code) {
throw new ApiException('验证码错误');
}
$oldTime = Carbon::parse($emailVerify->created_at)->timestamp;
$time = Base::Time();
// 30分钟失效
if (abs($time - $oldTime) > 1800) {
throw new ApiException('验证码已失效');
}
self::whereEmail($email)->whereCode($code)->whereType($type)->update([
'status' => 1
]);
return true;
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use Carbon\Carbon;
use Guanguans\Notify\Factory;
use Guanguans\Notify\Messages\EmailMessage;
/**
* App\Models\UserTransfer
*
* @property int $id
* @property int|null $original_userid 原作者
* @property int|null $new_userid 交接人
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer query()
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer whereNewUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer whereOriginalUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer whereUpdatedAt($value)
* @mixin \Eloquent
*/
class UserTransfer extends AbstractModel
{
/**
* 开始移交
* @return void
*/
public function start()
{
// 移交部门
UserDepartment::transfer($this->original_userid, $this->new_userid);
// 移交项目身份
ProjectUser::transfer($this->original_userid, $this->new_userid);
// 移交任务身份
ProjectTaskUser::transfer($this->original_userid, $this->new_userid);
// 移交文件
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,
]);
}
}
}
});
}
}

View File

@@ -3,7 +3,12 @@
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use App\Module\Doo;
use App\Tasks\PushTask;
use Cache;
use Carbon\Carbon;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
@@ -13,32 +18,46 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property string|null $type 对话类型
* @property string|null $group_type 聊天室类型
* @property string|null $name 对话名称
* @property string $avatar 头像(群)
* @property string|null $last_at 最后消息时间
* @property int|null $owner_id 群主用户ID
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\WebSocketDialogUser[] $dialogUser
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\WebSocketDialogUser> $dialogUser
* @property-read int|null $dialog_user_count
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog newQuery()
* @method static \Illuminate\Database\Query\Builder|WebSocketDialog onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog query()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereAvatar($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereGroupType($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereLastAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereOwnerId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereUpdatedAt($value)
* @method static \Illuminate\Database\Query\Builder|WebSocketDialog withTrashed()
* @method static \Illuminate\Database\Query\Builder|WebSocketDialog withoutTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog withoutTrashed()
* @mixin \Eloquent
*/
class WebSocketDialog extends AbstractModel
{
use SoftDeletes;
/**
* 头像地址
* @param $value
* @return string
*/
public function getAvatarAttribute($value)
{
return $value ? Base::fillUrl($value) : $value;
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
@@ -47,6 +66,236 @@ class WebSocketDialog extends AbstractModel
return $this->hasMany(WebSocketDialogUser::class, 'dialog_id', 'id');
}
/**
* 格式化对话
* @param int $userid 会员ID
* @param bool $hasData
* @return $this
*/
public function formatData($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->top_at = $this->top_at ?? $dialogUserFun('top_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?->created_at;
} else {
// 未读信息
$this->generateUnread($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();
}
// 对方信息
$this->pinyin = Base::cn2pinyin($this->name);
$this->quick_msgs = [];
$this->dialog_user = null;
$this->group_info = null;
$this->bot = 0;
switch ($this->type) {
case "user":
$dialog_user = WebSocketDialogUser::whereDialogId($this->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->bot = $basic->getBotOwner();
$this->quick_msgs = UserBot::quickMsgs($basic->email);
} else {
$this->name = 'non-existent';
$this->dialog_delete = 1;
}
$this->dialog_user = $dialog_user;
break;
case "group":
switch ($this->group_type) {
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;
} else {
$this->name = '[Delete]';
$this->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;
} else {
$this->name = '[Delete]';
$this->dialog_delete = 1;
}
break;
case 'all':
$this->name = Doo::translate('全体成员');
$this->all_group_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_image = $msgBuilder->clone()->whereMtype('image')->exists();
$this->has_file = $msgBuilder->clone()->whereMtype('file')->exists();
$this->has_link = $msgBuilder->clone()->whereLink(1)->exists();
}
return $this;
}
/**
* 生成未读数据
* @param $userid
* @param $positionData
* @return $this
*/
public function generateUnread($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
&& $mention_id = intval($builder->clone()->whereMention(1)->orderByDesc('msg_id')->value('msg_id'))) {
$array[] = [
'msg_id' => $mention_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;
}
/**
* 加入聊天室
* @param int|array $userid 加入的会员ID或会员ID组
* @param int $inviter 邀请人
* @param bool|null $important 重要人员(null不修改、bool修改)
* @return bool
*/
public function joinGroup($userid, $inviter, $important = null)
{
AbstractModel::transaction(function () use ($important, $inviter, $userid) {
foreach (is_array($userid) ? $userid : [$userid] as $value) {
if ($value > 0) {
$updateData = [
'inviter' => $inviter,
];
if (is_bool($important)) {
$updateData['important'] = $important ? 1 : 0;
}
$isInsert = false;
WebSocketDialogUser::updateInsert([
'dialog_id' => $this->id,
'userid' => $value,
], $updateData, [], $isInsert);
if ($isInsert) {
WebSocketDialogMsg::sendMsg(null, $this->id, 'notice', [
'notice' => User::userid2nickname($value) . " 已加入群组"
], $inviter, true, true);
}
}
}
});
$this->pushMsg("groupUpdate", [
'id' => $this->id,
'people' => WebSocketDialogUser::whereDialogId($this->id)->count()
]);
return true;
}
/**
* 退出聊天室
* @param int|array $userid 退出的会员ID或会员ID组
* @param string $type exit|remove
* @param bool $checkDelete 是否检查删除
* @param bool $pushMsg 是否推送消息
*/
public function exitGroup($userid, $type = 'exit', $checkDelete = true, $pushMsg = true)
{
$typeDesc = $type === 'remove' ? '移出' : '退出';
AbstractModel::transaction(function () use ($pushMsg, $checkDelete, $typeDesc, $type, $userid) {
$builder = WebSocketDialogUser::whereDialogId($this->id);
if (is_array($userid)) {
$builder->whereIn('userid', $userid);
} else {
$builder->whereUserid($userid);
}
$builder->chunkById(100, function($list) use ($pushMsg, $checkDelete, $typeDesc, $type) {
/** @var WebSocketDialogUser $item */
foreach ($list as $item) {
if ($checkDelete) {
if ($type === 'remove') {
// 移出时:如果是全员群仅允许管理员操作,其他群仅群主或邀请人可以操作
if ($this->group_type === 'all') {
User::auth("admin");
} elseif (!in_array(User::userid(), [$this->owner_id, $item->inviter])) {
throw new ApiException('只有群主或邀请人可以移出成员');
}
}
if ($item->userid == $this->owner_id) {
throw new ApiException('群主不可' . $typeDesc);
}
if ($item->important) {
throw new ApiException('部门成员、项目人员或任务人员不可' . $typeDesc);
}
}
//
$item->delete();
//
if ($pushMsg) {
if ($type === 'remove') {
$notice = User::nickname() . "" . User::userid2nickname($item->userid) . " 移出群组";
} else {
$notice = User::userid2nickname($item->userid) . " 退出群组";
}
WebSocketDialogMsg::sendMsg(null, $this->id, 'notice', [
'notice' => $notice
], User::userid(), true, true);
}
}
});
});
//
$this->pushMsg("groupUpdate", [
'id' => $this->id,
'people' => WebSocketDialogUser::whereDialogId($this->id)->count()
]);
}
/**
* 删除会话
* @return bool
@@ -54,20 +303,142 @@ class WebSocketDialog extends AbstractModel
public function deleteDialog()
{
AbstractModel::transaction(function () {
WebSocketDialogMsgRead::whereDialogId($this->id)->whereNull('read_at')->update([
'read_at' => Carbon::now()
]);
WebSocketDialogMsgRead::whereDialogId($this->id)
->whereNull('read_at')
->chunkById(100, function ($list) {
WebSocketDialogMsgRead::onlyMarkRead($list);
});
$this->delete();
});
$this->pushMsg("groupDelete");
return true;
}
/**
* 还原会话
* @return bool
*/
public function restoreDialog()
{
$this->restore();
$this->pushMsg("groupRestore");
return true;
}
/**
* 检查群组类型
* @param string|array|null $groupType
* @return void
*/
public function checkGroup($groupType = null)
{
if ($this->type !== 'group') {
throw new ApiException('仅限群组操作');
}
if ($groupType) {
$groupTypes = is_array($groupType) ? $groupType : [$groupType];
if (!in_array($this->group_type, $groupTypes)) {
throw new ApiException('操作的群组类型错误');
}
}
}
/**
* 检查禁言
* @param $userid
* @return void
*/
public function checkMute($userid)
{
if ($this->group_type === 'all') {
$allGroupMute = Base::settingFind('system', 'all_group_mute');
switch ($allGroupMute) {
case 'all':
throw new ApiException('当前会话全员禁言');
case 'user':
if (!User::find($userid)?->checkAdmin()) {
throw new ApiException('当前会话禁言');
}
}
}
}
/**
* 获取群组名称
* @return mixed|string|null
*/
public function getGroupName()
{
if (!isset($this->appendattrs['groupName'])) {
$name = $this->name;
if ($this->type == "group") {
switch ($this->group_type) {
case 'project':
$name = \DB::table('projects')->where('dialog_id', $this->id)->value('name');
break;
case 'task':
$name = \DB::table('project_tasks')->where('dialog_id', $this->id)->value('name');
break;
case 'all':
$name = Doo::translate('全体成员');
break;
}
}
$this->appendattrs['groupName'] = $name;
}
return $this->appendattrs['groupName'];
}
/**
* 推送消息
* @param $action
* @param array $data 发送内容,默认为[id=>会话ID]
* @param array $userid 指定会员,默认为群组所有成员
* @return void
*/
public function pushMsg($action, $data = null, $userid = null)
{
if ($data === null) {
$data = ['id' => $this->id];
}
//
if ($userid === null) {
$userid = $this->dialogUser->pluck('userid')->toArray();
}
//
$params = [
'userid' => $userid,
'msg' => [
'type' => 'dialog',
'mode' => $action,
'data' => $data,
]
];
$task = new PushTask($params, false);
Task::deliver($task);
}
/**
* 更新对话最后消息时间
* @return WebSocketDialogMsg|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Query\Builder|object|null
*/
public function updateMsgLastAt()
{
$lastMsg = WebSocketDialogMsg::whereDialogId($this->id)->orderByDesc('id')->first();
if ($lastMsg) {
$this->last_at = $lastMsg->created_at;
$this->save();
}
return $lastMsg;
}
/**
* 获取对话(同时检验对话身份)
* @param $dialog_id
* @param bool|string $checkOwner 是否校验群组身份,'auto'时有群主为true无群主为false
* @return self
*/
public static function checkDialog($dialog_id)
public static function checkDialog($dialog_id, $checkOwner = false)
{
$dialog = WebSocketDialog::find($dialog_id);
if (empty($dialog)) {
@@ -75,7 +446,14 @@ class WebSocketDialog extends AbstractModel
}
//
$userid = User::userid();
if ($dialog->type === 'group' && $dialog->group_type === 'task') {
if ($checkOwner === 'auto') {
$checkOwner = $dialog->owner_id > 0;
}
if ($checkOwner === true && $dialog->owner_id != $userid) {
throw new ApiException('仅限群主操作');
}
//
if ($dialog->group_type === 'task') {
// 任务群对话校验是否在项目内
$project_id = intval(ProjectTask::whereDialogId($dialog->id)->value('project_id'));
if ($project_id > 0) {
@@ -85,48 +463,7 @@ class WebSocketDialog extends AbstractModel
}
}
if (!WebSocketDialogUser::whereDialogId($dialog->id)->whereUserid($userid)->exists()) {
throw new ApiException('不在成员列表内');
}
return $dialog;
}
/**
* 格式化对话
* @param WebSocketDialog $dialog
* @param int $userid 会员ID
* @return self|null
*/
public static function formatData(WebSocketDialog $dialog, $userid)
{
if (empty($dialog)) {
return null;
}
// 最后消息
$last_msg = WebSocketDialogMsg::whereDialogId($dialog->id)->orderByDesc('id')->first();
$dialog->last_msg = $last_msg;
// 未读信息
$dialog->unread = WebSocketDialogMsgRead::whereDialogId($dialog->id)->whereUserid($userid)->whereReadAt(null)->count();
// 对话人数
$builder = WebSocketDialogUser::whereDialogId($dialog->id);
$dialog->people = $builder->count();
// 对方信息
$dialog->dialog_user = null;
$dialog->group_info = null;
switch ($dialog->type) {
case "user":
$dialog_user = $builder->where('userid', '!=', $userid)->first();
$dialog->name = User::userid2nickname($dialog_user->userid);
$dialog->dialog_user = $dialog_user;
break;
case "group":
if ($dialog->group_type === 'project') {
$dialog->group_info = Project::withTrashed()->select(['id', 'name'])->whereDialogId($dialog->id)->first();
$dialog->name = $dialog->group_info ? $dialog->group_info->name : '';
} elseif ($dialog->group_type === 'task') {
$dialog->group_info = ProjectTask::withTrashed()->select(['id', 'name'])->whereDialogId($dialog->id)->first();
$dialog->name = $dialog->group_info ? $dialog->group_info->name : '';
}
break;
throw new ApiException('不在成员列表内', ['dialog_id' => $dialog_id], -4003);
}
return $dialog;
}
@@ -134,17 +471,20 @@ class WebSocketDialog extends AbstractModel
/**
* 创建聊天室
* @param string $name 聊天室名称
* @param int|array $userid 加入的会员ID或会员ID组
* @param int|array $userid 加入的会员ID(组)
* @param string $group_type 聊天室类型
* @param int $owner_id 群主会员ID
* @return self|null
*/
public static function createGroup($name, $userid, $group_type = '')
public static function createGroup($name, $userid, $group_type = '', $owner_id = 0)
{
return AbstractModel::transaction(function () use ($userid, $group_type, $name) {
return AbstractModel::transaction(function () use ($owner_id, $userid, $group_type, $name) {
$dialog = self::createInstance([
'type' => 'group',
'name' => $name ?: '',
'group_type' => $group_type,
'owner_id' => $owner_id,
'last_at' => in_array($group_type, ['user', 'department', 'all']) ? Carbon::now() : null,
]);
$dialog->save();
foreach (is_array($userid) ? $userid : [$userid] as $value) {
@@ -152,6 +492,7 @@ class WebSocketDialog extends AbstractModel
WebSocketDialogUser::createInstance([
'dialog_id' => $dialog->id,
'userid' => $value,
'important' => !in_array($group_type, ['user', 'all'])
])->save();
}
}
@@ -159,80 +500,44 @@ class WebSocketDialog extends AbstractModel
});
}
/**
* 加入聊天室
* @param int $dialog_id 会话ID即 聊天室ID
* @param int|array $userid 加入的会员ID或会员ID组
* @return bool
*/
public static function joinGroup($dialog_id, $userid)
{
$dialog = self::whereId($dialog_id)->whereType('group')->first();
if (empty($dialog)) {
return false;
}
AbstractModel::transaction(function () use ($dialog, $userid) {
foreach (is_array($userid) ? $userid : [$userid] as $value) {
if ($value > 0) {
WebSocketDialogUser::createInstance([
'dialog_id' => $dialog->id,
'userid' => $value,
])->save();
}
}
});
return true;
}
/**
* 退出聊天室
* @param int $dialog_id 会话ID即 聊天室ID
* @param int|array $userid 加入的会员ID或会员ID组
* @return bool
*/
public static function exitGroup($dialog_id, $userid)
{
if (is_array($userid)) {
WebSocketDialogUser::whereDialogId($dialog_id)->whereIn('userid', $userid)->delete();
} else {
WebSocketDialogUser::whereDialogId($dialog_id)->whereUserid($userid)->delete();
}
return true;
}
/**
* 获取会员对话(没有自动创建)
* @param int $userid 会员ID
* @param int $userid2 另一个会员ID
* @param User $user 发起会话的会员
* @param int $receiver 另一个会员ID
* @return self|null
*/
public static function checkUserDialog($userid, $userid2)
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', $userid)
->where('u2.userid', $userid2)
->where('u1.userid', $user->userid)
->where('u2.userid', $receiver)
->where('web_socket_dialogs.type', 'user')
->first();
if ($dialogUser) {
return $dialogUser;
}
return AbstractModel::transaction(function () use ($userid2, $userid) {
if ($receiver > 0 && $user->isTemp()) {
throw new ApiException('无法发起会话');
}
return AbstractModel::transaction(function () use ($receiver, $user) {
$dialog = self::createInstance([
'type' => 'user',
]);
$dialog->save();
WebSocketDialogUser::createInstance([
'dialog_id' => $dialog->id,
'userid' => $userid,
'userid' => $user->userid,
])->save();
WebSocketDialogUser::createInstance([
'dialog_id' => $dialog->id,
'userid' => $userid2,
'userid' => $receiver,
])->save();
return $dialog;
});
}
}

View File

@@ -2,50 +2,90 @@
namespace App\Models;
use App\Exceptions\ApiException;
use Carbon\Carbon;
use App\Module\Base;
use App\Tasks\PushTask;
use App\Exceptions\ApiException;
use App\Tasks\WebSocketDialogMsgTask;
use Carbon\Carbon;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* App\Models\WebSocketDialogMsg
*
* @property int $id
* @property int|null $dialog_id 对话ID
* @property string|null $dialog_type 对话类型
* @property int|null $userid 发送会员ID
* @property string|null $type 消息类型
* @property string|null $mtype 消息类型(用于搜索)
* @property array|mixed $msg 详细消息
* @property array|mixed $emoji emoji回复
* @property string|null $key 搜索关键词
* @property int|null $read 已阅数量
* @property int|null $send 发送数量
* @property int|null $tag 标注会员ID
* @property int|null $todo 设为待办会员ID
* @property int|null $link 是否存在链接
* @property int|null $modify 是否编辑
* @property int|null $reply_num 有多少条回复
* @property int|null $reply_id 回复ID
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read int|mixed $percentage
* @property-read \App\Models\WebSocketDialogMsg|null $reply_data
* @property-read \App\Models\WebSocketDialog|null $webSocketDialog
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg query()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDialogType($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereEmoji($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereKey($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereLink($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereModify($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereMsg($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereMtype($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereRead($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereReplyId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereReplyNum($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereSend($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereTag($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereTodo($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg withoutTrashed()
* @mixin \Eloquent
*/
class WebSocketDialogMsg extends AbstractModel
{
use SoftDeletes;
protected $appends = [
'percentage',
'reply_data',
];
protected $hidden = [
'key',
'updated_at',
];
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function webSocketDialog(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(WebSocketDialog::class, 'id', 'dialog_id');
}
/**
* 阅读占比
* @return int|mixed
@@ -53,15 +93,26 @@ class WebSocketDialogMsg extends AbstractModel
public function getPercentageAttribute()
{
if (!isset($this->appendattrs['percentage'])) {
if ($this->read > $this->send || empty($this->send)) {
$this->appendattrs['percentage'] = 100;
} else {
$this->appendattrs['percentage'] = intval($this->read / $this->send * 100);
}
$this->generatePercentage();
}
return $this->appendattrs['percentage'];
}
/**
* 回复消息详情
* @return WebSocketDialogMsg|null
*/
public function getReplyDataAttribute()
{
if (!isset($this->appendattrs['reply_data'])) {
$this->appendattrs['reply_data'] = null;
if ($this->reply_id > 0) {
$this->appendattrs['reply_data'] = self::find($this->reply_id, ['id', 'userid', 'type', 'msg'])?->cancelAppend() ?: null;
}
}
return $this->appendattrs['reply_data'];
}
/**
* 消息格式化
* @param $value
@@ -74,13 +125,44 @@ class WebSocketDialogMsg extends AbstractModel
}
$value = Base::json2array($value);
if ($this->type === 'file') {
$value['type'] = in_array($value['ext'], ['jpg', 'jpeg', 'png', 'gif']) ? 'img' : 'file';
$value['type'] = in_array($value['ext'], ['jpg', 'jpeg', 'webp', 'png', 'gif']) ? 'img' : 'file';
$value['path'] = Base::fillUrl($value['path']);
$value['thumb'] = Base::fillUrl($value['thumb'] ?: Base::extIcon($value['ext']));
} else if ($this->type === 'record') {
$value['path'] = Base::fillUrl($value['path']);
}
return $value;
}
/**
* emoji回复格式化
* @param $value
* @return array|mixed
*/
public function getEmojiAttribute($value)
{
if (is_array($value)) {
return $value;
}
return Base::json2array($value);
}
/**
* 获取占比
* @param bool|int $increment 是否新增阅读数
* @return int
*/
public function generatePercentage($increment = false) {
if ($increment) {
$this->increment('read', is_bool($increment) ? 1 : $increment);
}
if ($this->read > $this->send || empty($this->send)) {
return $this->appendattrs['percentage'] = 100;
} else {
return $this->appendattrs['percentage'] = intval($this->read / $this->send * 100);
}
}
/**
* 标记已送达 同时 告诉发送人已送达
* @param $userid
@@ -110,13 +192,17 @@ class WebSocketDialogMsg extends AbstractModel
if (!$msgRead->read_at) {
$msgRead->read_at = Carbon::now();
$msgRead->save();
$this->increment('read');
$this->generatePercentage(true);
PushTask::push([
'userid' => $this->userid,
'msg' => [
'type' => 'dialog',
'mode' => 'update',
'data' => $this->toArray(),
'mode' => 'readed',
'data' => [
'id' => $this->id,
'read' => $this->read,
'percentage' => $this->percentage,
],
]
]);
}
@@ -125,33 +211,679 @@ class WebSocketDialogMsg extends AbstractModel
}
/**
* 发送消息
* @param int $dialog_id 会话ID即 聊天室ID
* @param string $type 消息类型
* @param array $msg 发送的消息
* @param int $sender 发送的会员ID默认自己0为系统
* emoji回复
* @param $symbol
* @param int $sender 发送的会员ID
* @return mixed
*/
public function emojiMsg($symbol, $sender)
{
$exist = false;
$array = $this->emoji;
foreach ($array as $index => &$item) {
if ($item['symbol'] === $symbol) {
if (in_array($sender, $item['userids'])) {
// 已存在 去除
$item['userids'] = array_values(array_diff($item['userids'], [$sender]));
if (empty($item['userids'])) {
unset($array[$index]);
$array = array_values($array);
}
} else {
// 未存在 添加
array_unshift($item['userids'], $sender);
}
$exist = true;
break;
}
}
if (!$exist) {
array_unshift($array, [
'symbol' => $symbol,
'userids' => [$sender]
]);
}
//
$this->emoji = Base::array2json($array);
$this->save();
$resData = [
'id' => $this->id,
'emoji' => $array,
];
//
$dialog = WebSocketDialog::find($this->dialog_id);
$dialog?->pushMsg('update', $resData);
//
return Base::retSuccess('success', $resData);
}
/**
* 标注、取消标注
* @param int $sender 标注的会员ID
* @return mixed
*/
public function toggleTagMsg($sender)
{
if (in_array($this->type, ['tag', 'todo', 'notice'])) {
return Base::retError('此消息不支持标注');
}
$before = $this->tag;
$this->tag = $before ? 0 : $sender;
$this->save();
$resData = [
'id' => $this->id,
'tag' => $this->tag,
];
//
$data = [
'update' => $resData
];
$res = self::sendMsg(null, $this->dialog_id, 'tag', [
'action' => $this->tag ? 'add' : 'remove',
'data' => [
'id' => $this->id,
'type' => $this->type,
'msg' => $this->quoteTextMsg(),
]
], $sender);
if (Base::isSuccess($res)) {
$data['add'] = $res['data'];
$dialog = WebSocketDialog::find($this->dialog_id);
$dialog->pushMsg('update', $resData);
} else {
$this->tag = $before;
$this->save();
}
//
return Base::retSuccess($this->tag ? '标注成功' : '取消成功', $data);
}
/**
* 设待办、取消待办
* @param int $sender 设待办的会员ID
* @param array $userids 设置给指定会员
* @return mixed
*/
public function toggleTodoMsg($sender, $userids = [])
{
if (in_array($this->type, ['tag', 'todo', 'notice'])) {
return Base::retError('此消息不支持设待办');
}
if ($this->todo && $this->todo != $sender) {
return Base::retError('仅支持设此待办人员【' . User::userid2nickname($this->todo) . '】取消');
}
$before = $this->todo;
$this->todo = $before ? 0 : $sender;
$this->save();
$resData = [
'id' => $this->id,
'todo' => $this->todo,
];
//
$data = [
'update' => $resData
];
$res = self::sendMsg(null, $this->dialog_id, 'todo', [
'action' => $this->todo ? 'add' : 'remove',
'data' => [
'id' => $this->id,
'type' => $this->type,
'msg' => $this->quoteTextMsg(),
'userids' => implode(",", $userids),
]
], $sender);
if (Base::isSuccess($res)) {
$data['add'] = $res['data'];
$dialog = WebSocketDialog::find($this->dialog_id);
$dialog->pushMsg('update', array_merge($resData, ['dialog_id' => $this->dialog_id]));
//
if ($this->todo) {
$useridList = $dialog->dialogUser->pluck('userid')->toArray();
foreach ($useridList as $userid) {
if ($userids && !in_array($userid, $userids)) {
continue;
}
if (empty($userid)) {
continue;
}
WebSocketDialogMsgTodo::createInstance([
'dialog_id' => $this->dialog_id,
'msg_id' => $this->id,
'userid' => $userid,
])->saveOrIgnore();
}
} else {
WebSocketDialogMsgTodo::whereMsgId($this->id)->delete();
}
} else {
$this->todo = $before;
$this->save();
}
//
return Base::retSuccess($this->todo ? '设置成功' : '取消成功', $data);
}
/**
* 转发消息
* @param array|int $dialogids
* @param array|int $userids
* @param User $user 发送的会员
* @return mixed
*/
public function forwardMsg($dialogids, $userids, $user)
{
return AbstractModel::transaction(function() use ($dialogids, $user, $userids) {
$originalMsg = Base::json2array($this->getRawOriginal('msg'));
$msgs = [];
$already = [];
if ($dialogids) {
if (!is_array($dialogids)) {
$dialogids = [$dialogids];
}
foreach ($dialogids as $dialogid) {
$res = self::sendMsg(null, $dialogid, $this->type, $originalMsg, $user->userid);
if (Base::isSuccess($res)) {
$msgs[] = $res['data'];
$already[] = $dialogid;
}
}
}
if ($userids) {
if (!is_array($userids)) {
$userids = [$userids];
}
foreach ($userids as $userid) {
if (!User::whereUserid($userid)->exists()) {
continue;
}
$dialog = WebSocketDialog::checkUserDialog($user, $userid);
if ($dialog && !in_array($dialog->id, $already)) {
$res = self::sendMsg(null, $dialog->id, $this->type, $originalMsg, $user->userid);
if (Base::isSuccess($res)) {
$msgs[] = $res['data'];
}
}
}
}
return Base::retSuccess('转发成功', [
'msgs' => $msgs
]);
});
}
/**
* 删除消息
* @param array|int $ids
* @return void
*/
public static function deleteMsgs($ids) {
$ids = Base::arrayRetainInt(is_array($ids) ? $ids : [$ids], true);
AbstractModel::transaction(function() use ($ids) {
$dialogIds = WebSocketDialogMsg::select('dialog_id')->whereIn("id", $ids)->distinct()->get()->pluck('dialog_id');
$replyIds = WebSocketDialogMsg::select('reply_id')->whereIn("id", $ids)->distinct()->get()->pluck('reply_id');
//
WebSocketDialogMsgRead::whereIn('msg_id', $ids)->whereNull('read_at')->delete(); // 未阅读记录不需要软删除,直接删除即可
WebSocketDialogMsgTodo::whereIn('msg_id', $ids)->delete();
self::whereIn('id', $ids)->delete();
//
$dialogDatas = WebSocketDialog::whereIn('id', $dialogIds)->get();
foreach ($dialogDatas as $dialogData) {
$dialogData->updateMsgLastAt();
}
foreach ($replyIds as $id) {
self::whereId($id)->update(['reply_num' => self::whereReplyId($id)->count()]);
}
});
}
/**
* 撤回消息
* @return void
*/
public function withdrawMsg()
{
$send_dt = Carbon::parse($this->created_at)->addDay();
if ($send_dt->lt(Carbon::now())) {
throw new ApiException('已超过24小时此消息不能撤回');
}
AbstractModel::transaction(function() {
$deleteRead = WebSocketDialogMsgRead::whereMsgId($this->id)->whereNull('read_at')->delete(); // 未阅读记录不需要软删除,直接删除即可
$this->delete();
//
if ($this->reply_id > 0) {
self::whereId($this->reply_id)->decrement('reply_num');
}
//
$dialogData = $this->webSocketDialog;
if ($dialogData) {
foreach ($dialogData->dialogUser as $dialogUser) {
$dialogUser->updated_at = Carbon::now();
$dialogUser->save();
}
$userids = $dialogData->dialogUser->pluck('userid')->toArray();
PushTask::push([
'userid' => $userids,
'msg' => [
'type' => 'dialog',
'mode' => 'delete',
'data' => [
'id' => $this->id,
'dialog_id' => $this->dialog_id,
'last_msg' => $dialogData->updateMsgLastAt(),
'update_read' => $deleteRead ? 1 : 0
],
]
]);
}
//
WebSocketDialogMsgTodo::whereMsgId($this->id)->delete();
});
}
/**
* 预览消息
* @param bool $preserveHtml 保留html格式
* @param null|array $data
* @return string
*/
public function previewMsg($preserveHtml = false, $data = null)
{
if ($data === null) {
$data = [
'type' => $this->type,
'msg' => $this->msg,
];
}
switch ($data['type']) {
case 'text':
return $this->previewTextMsg($data['msg']['text'], $preserveHtml);
case 'record':
return "[语音]";
case 'meeting':
return "[会议] ${$data['msg']['name']}";
case 'file':
if ($data['msg']['type'] == 'img') {
return "[图片]";
}
return "[文件] {$data['msg']['name']}";
case 'tag':
$action = $data['msg']['action'] === 'remove' ? '取消标注' : '标注';
return "[{$action}] {$this->previewMsg(false, $data['msg']['data'])}";
case 'todo':
$action = $data['msg']['action'] === 'remove' ? '取消待办' : ($data['msg']['action'] === 'done' ? '完成' : '设待办');
return "[{$action}] {$this->previewMsg(false, $data['msg']['data'])}";
case 'notice':
return $data['msg']['notice'];
default:
return "[未知的消息]";
}
}
/**
* 生成关键词
* @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 bool $preserveHtml 保留html格式
* @return string|string[]|null
*/
private function previewTextMsg($text, $preserveHtml = false)
{
if (!$text) return '';
$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);
if (!$preserveHtml) {
$text = strip_tags($text);
$text = str_replace(["&nbsp;", "&amp;", "&lt;", "&gt;"], [" ", "&", "<", ">"], $text);
}
return $text;
}
/**
* 处理文本消息内容,用于发送前
* @param $text
* @param $dialog_id
* @return mixed|string|string[]
*/
public static function formatMsg($text, $dialog_id)
{
@ini_set("pcre.backtrack_limit", 999999999);
// 基础处理
$text = preg_replace("/<(\/[a-zA-Z]+)\s*>/s", "<$1>", $text);
// 图片 [:IMAGE:className:width:height:src:alt:]
preg_match_all("/<img\s+src=\"data:image\/(png|jpg|jpeg|webp|gif);base64,(.*?)\"(.*?)>(<\/img>)*/s", $text, $matchs);
foreach ($matchs[2] as $key => $base64) {
$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))) {
$imageSize = getimagesize(public_path($imagePath));
if (Base::imgThumb(public_path($imagePath), public_path($imagePath) . "_thumb.jpg", 320, 0)) {
$imagePath .= "_thumb.jpg";
}
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}::]", $text);
}
}
// 表情图片
preg_match_all("/<img\s+class=\"emoticon\"(.*?)>/s", $text, $matchs);
foreach ($matchs[1] as $key => $str) {
preg_match("/data-asset=\"(.*?)\"/", $str, $matchAsset);
preg_match("/data-name=\"(.*?)\"/", $str, $matchName);
$imageSize = null;
$imagePath = "";
$imageName = "";
if ($matchAsset[1] === "emosearch") {
preg_match("/src=\"(.*?)\"/", $str, $matchSrc);
if ($matchSrc) {
$srcMd5 = md5($matchSrc[1]);
$imagePath = "uploads/emosearch/" . substr($srcMd5, 0, 2) . "/" . substr($srcMd5, 32 - 2) . "/";
Base::makeDir(public_path($imagePath));
$imagePath .= md5s($matchSrc[1]);
if (file_exists(public_path($imagePath))) {
$imageSize = getimagesize(public_path($imagePath));
} else {
$image = file_get_contents($matchSrc[1]);
if ($image && file_put_contents(public_path($imagePath), $image)) {
$imageSize = getimagesize(public_path($imagePath));
// 添加后缀
if ($imageSize && !str_contains($imagePath, '.')) {
preg_match("/^image\/(png|jpg|jpeg|webp|gif)$/", $imageSize['mime'], $matchMine);
if ($matchMine) {
$imageNewPath = $imagePath . "." . $matchMine[1];
if (rename(public_path($imagePath), public_path($imageNewPath))) {
$imagePath = $imageNewPath;
}
}
}
}
}
}
} elseif (file_exists(public_path($matchAsset[1]))) {
$imagePath = $matchAsset[1];
$imageName = $matchName[1];
$imageSize = getimagesize(public_path($matchAsset[1]));
}
if ($imageSize) {
$text = str_replace($matchs[0][$key], "[:IMAGE:emoticon:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}:{$imageName}:]", $text);
} else {
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:90:90:images/other/imgerr.jpg::]", $text);
}
}
// 其他网络图片
$imageSaveLocal = Base::settingFind("system", "image_save_local");
preg_match_all("/<img[^>]*?src=([\"'])(.*?\.(png|jpg|jpeg|webp|gif))\\1[^>]*?>/is", $text, $matchs);
foreach ($matchs[2] as $key => $str) {
if ($imageSaveLocal === 'close') {
$imageSize = getimagesize($str);
if ($imageSize === false) {
$imageSize = ["auto", "auto"];
}
$imagePath = "base64-" . base64_encode($str);
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}::]", $text);
continue;
}
if (str_starts_with($str, "{{RemoteURL}}")) {
$imagePath = Base::leftDelete($str, "{{RemoteURL}}");
$imagePath = Base::rightDelete($imagePath, "_thumb.jpg");
} else {
$imagePath = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
Base::makeDir(public_path($imagePath));
$imagePath .= md5s($str) . "." . $matchs[3][$key];
}
if (file_exists(public_path($imagePath))) {
$imageSize = getimagesize(public_path($imagePath));
if (Base::imgThumb(public_path($imagePath), public_path($imagePath) . "_thumb.jpg", 320, 0)) {
$imagePath .= "_thumb.jpg";
}
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}::]", $text);
} else {
$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)) {
$imageSize = getimagesize(public_path($imagePath));
if (Base::imgThumb(public_path($imagePath), public_path($imagePath) . "_thumb.jpg", 320, 0)) {
$imagePath .= "_thumb.jpg";
}
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}::]", $text);
}
}
}
// @成员、#任务、~文件
preg_match_all("/<span\s+class=\"mention\"(.*?)>.*?<\/span>.*?<\/span>.*?<\/span>/s", $text, $matchs);
foreach ($matchs[1] as $key => $str) {
preg_match("/data-denotation-char=\"(.*?)\"/", $str, $matchChar);
preg_match("/data-id=\"(.*?)\"/", $str, $matchId);
preg_match("/data-value=\"(.*?)\"/", $str, $matchValye);
$keyId = $matchId[1];
if ($matchChar[1] === "~") {
if (Base::isNumber($keyId)) {
$file = File::permissionFind($keyId, User::auth());
if ($file->type == 'folder') {
throw new ApiException('文件夹不支持分享');
}
$fileLink = $file->getShareLink(User::userid());
$keyId = $fileLink['code'];
} else {
preg_match("/\/single\/file\/(.*?)$/i", $keyId, $match);
if ($match && strlen($match[1]) >= 8) {
$keyId = $match[1];
} else {
throw new ApiException('文件分享错误');
}
}
}
$text = str_replace($matchs[0][$key], "[:{$matchChar[1]}:{$keyId}:{$matchValye[1]}:]", $text);
}
// 处理快捷消息
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];
if ($quickKey && $quickLabel) {
$quickKey = str_replace(":", "", $quickKey);
$quickLabel = str_replace(":", "", $quickLabel);
$text = str_replace($str, "[:QUICK:{$quickKey}:{$quickLabel}:]", $text);
}
}
// 处理链接标签
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;
preg_match("/\/single\/file\/(.*?)$/i", strip_tags($title), $match);
if ($match && strlen($match[1]) >= 8) {
$file = File::select(['files.id', 'files.name', 'files.ext'])->join('file_links as L', 'files.id', '=', 'L.file_id')->where('L.code', $match[1])->first();
if ($file && $file->name) {
$name = $file->ext ? "{$file->name}.{$file->ext}" : $file->name;
$text = str_replace($str, "[:~:{$match[1]}:{$name}:]", $text);
continue;
}
}
$herf = base64_encode($herf);
$title = base64_encode($title);
$text = str_replace($str, "[:LINK:{$herf}:{$title}:]", $text);
}
// 文件分享链接
preg_match_all("/(https*:\/\/)((\w|=|\?|\.|\/|&|-|:|\+|%|;|#|@|,|!)+)/i", $text, $matchs);
if ($matchs) {
foreach ($matchs[0] as $str) {
preg_match("/\/single\/file\/(.*?)$/i", $str, $match);
if ($match && strlen($match[1]) >= 8) {
$file = File::select(['files.id', 'files.name', 'files.ext'])->join('file_links as L', 'files.id', '=', 'L.file_id')->where('L.code', $match[1])->first();
if ($file && $file->name) {
$name = $file->ext ? "{$file->name}.{$file->ext}" : $file->name;
$text = str_replace($str, "[:~:{$match[1]}:{$name}:]", $text);
}
}
}
}
// 过滤标签
$text = strip_tags($text, '<blockquote> <strong> <pre> <ol> <ul> <li> <em> <p> <s> <u> <a>');
$text = preg_replace("/\<(blockquote|strong|pre|ol|ul|li|em|p|s|u).*?\>/is", "<$1>", $text); // 不用去除a标签上面已经处理过了
$text = preg_replace_callback("/\[:LINK:(.*?):(.*?):\]/i", function (array $match) {
return "<a href=\"" . base64_decode($match[1]) . "\" target=\"_blank\">" . base64_decode($match[2]) . "</a>";
}, $text);
$text = preg_replace_callback("/\[:IMAGE:(.*?):(.*?):(.*?):(.*?):(.*?):\]/i", function (array $match) {
$wh = $match[2] === 'auto' ? "" : " width=\"{$match[2]}\" height=\"{$match[3]}\"";
$src = str_starts_with($match[4], "base64-") ? base64_decode(substr($match[4], 7)) : "{{RemoteURL}}{$match[4]}";
return "<img class=\"{$match[1]}\"{$wh} src=\"{$src}\" alt=\"{$match[5]}\"/>";
}, $text);
$text = preg_replace("/\[:@:(.*?):(.*?):\]/i", "<span class=\"mention user\" data-id=\"$1\">@$2</span>", $text);
$text = preg_replace("/\[:#:(.*?):(.*?):\]/i", "<span class=\"mention task\" data-id=\"$1\">#$2</span>", $text);
$text = preg_replace("/\[:~:(.*?):(.*?):\]/i", "<a class=\"mention file\" href=\"{{RemoteURL}}single/file/$1\" target=\"_blank\">~$2</a>", $text);
$text = preg_replace("/\[:QUICK:(.*?):(.*?):\]/i", "<span data-quick-key=\"$1\">$2</span>", $text);
return preg_replace("/^(<p><\/p>)+|(<p><\/p>)+$/i", "", $text);
}
/**
* 发送消息、修改消息
* @param string $action 动作
* - reply-98回复消息ID=98
* - update-99更新消息ID=99
* @param int $dialog_id 会话ID即 聊天室ID
* @param string $type 消息类型
* @param array $msg 发送的消息
* @param int $sender 发送的会员ID默认自己0为系统
* @param bool $push_self 推送-是否推给自己
* @param bool $push_retry 推送-失败后重试1次有时候在事务里执行数据还没生成时会出现找不到消息的情况
* @param bool|null $push_silence 推送-静默
* - type = [text|file|record|meeting] 默认为false
* @return array
*/
public static function sendMsg($dialog_id, $type, $msg, $sender = 0)
public static function sendMsg($action, $dialog_id, $type, $msg, $sender = 0, $push_self = false, $push_retry = false, $push_silence = null)
{
$dialogMsg = self::createInstance([
'userid' => $sender ?: User::userid(),
'type' => $type,
'msg' => $msg,
'read' => 0,
]);
AbstractModel::transaction(function () use ($dialog_id, $msg, $dialogMsg) {
$dialog = WebSocketDialog::find($dialog_id);
if (empty($dialog)) {
throw new ApiException('获取会话失败');
$link = 0;
$mtype = $type;
if ($type === 'text') {
if (str_contains($msg['text'], '<a ') || preg_match("/https*:\/\//", $msg['text'])) {
$link = 1;
}
$dialog->last_at = Carbon::now();
$dialog->save();
$dialogMsg->send = 1;
$dialogMsg->dialog_id = $dialog->id;
if (str_contains($msg['text'], '<img ')) {
$mtype = str_contains($msg['text'], '"emoticon"') ? 'emoticon' : 'image';
}
preg_match_all("/@([A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,6})/i", $msg['text'], $matchs);
foreach($matchs[0] as $key => $item) {
$aiUser = User::whereEmail($matchs[1][$key])->whereDisableAt(null)->first();
if ($aiUser) {
$msg['text'] = str_replace($item, "<span class=\"mention user\" data-id=\"{$aiUser->userid}\">@{$aiUser->nickname}</span>", $msg['text']);
}
}
} elseif ($type === 'file') {
if (in_array($msg['ext'], ['jpg', 'jpeg', 'webp', 'png', 'gif'])) {
$mtype = 'image';
}
}
if ($push_silence === null) {
$push_silence = !in_array($type, ["text", "file", "record", "meeting"]);
}
//
$update_id = preg_match("/^update-(\d+)$/", $action, $match) ? $match[1] : 0;
$reply_id = preg_match("/^reply-(\d+)$/", $action, $match) ? $match[1] : 0;
$sender = $sender ?: User::userid();
//
$dialog = WebSocketDialog::find($dialog_id);
if (empty($dialog)) {
throw new ApiException('获取会话失败');
}
$dialog->checkMute($sender);
//
if ($update_id) {
// 修改
$dialogMsg = self::whereId($update_id)->whereDialogId($dialog_id)->first();
if (empty($dialogMsg)) {
throw new ApiException('消息不存在');
}
if ($dialogMsg->type !== 'text') {
throw new ApiException('此消息不支持此操作');
}
if ($dialogMsg->userid != $sender) {
throw new ApiException('仅支持修改自己的消息');
}
//
$updateData = [
'mtype' => $mtype,
'link' => $link,
'msg' => $msg,
'modify' => 1,
];
$dialogMsg->updateInstance($updateData);
$dialogMsg->key = $dialogMsg->generateMsgKey();
$dialogMsg->save();
});
Task::deliver(new WebSocketDialogMsgTask($dialogMsg->id));
return Base::retSuccess('发送成功', $dialogMsg);
//
$dialog->pushMsg('update', array_merge($updateData, [
'id' => $dialogMsg->id
]));
//
return Base::retSuccess('修改成功', $dialogMsg);
} else {
// 发送
if ($reply_id && !self::whereId($reply_id)->increment('reply_num')) {
throw new ApiException('回复的消息不存在');
}
//
$dialogMsg = self::createInstance([
'dialog_id' => $dialog_id,
'dialog_type' => $dialog->type,
'reply_id' => $reply_id,
'userid' => $sender,
'type' => $type,
'mtype' => $mtype,
'link' => $link,
'msg' => $msg,
'read' => 0,
]);
AbstractModel::transaction(function () use ($dialog, $dialogMsg) {
$dialog->last_at = Carbon::now();
$dialog->save();
$dialogMsg->send = 1;
$dialogMsg->key = $dialogMsg->generateMsgKey();
$dialogMsg->save();
WebSocketDialogUser::whereDialogId($dialog->id)->change(['updated_at' => Carbon::now()->toDateTimeString('millisecond')]);
});
//
$task = new WebSocketDialogMsgTask($dialogMsg->id);
if ($push_self) {
$task->setIgnoreFd(null);
}
if ($push_retry) {
$task->setMsgNotExistRetry(true);
}
if ($push_silence) {
$task->setSilence($push_silence);
}
Task::deliver($task);
//
return Base::retSuccess('发送成功', $dialogMsg);
}
}
}

View File

@@ -2,23 +2,32 @@
namespace App\Models;
use Carbon\Carbon;
/**
* App\Models\WebSocketDialogMsgRead
*
* @property int $id
* @property int|null $dialog_id 对话ID
* @property int|null $msg_id 消息ID
* @property int|null $userid 发送会员ID
* @property int|null $userid 接收会员ID
* @property int|null $mention 是否提及(被@
* @property int|null $silence 是否免打扰0否1是
* @property int|null $email 是否发了邮件
* @property int|null $after 在阅读之后才添加的记录
* @property string|null $read_at 阅读时间
* @property-read \App\Models\WebSocketDialogMsg|null $webSocketDialogMsg
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead query()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereAfter($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereEmail($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereMention($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereMsgId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereReadAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereSilence($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgRead whereUserid($value)
* @mixin \Eloquent
*/
@@ -29,4 +38,38 @@ class WebSocketDialogMsgRead extends AbstractModel
parent::__construct($attributes);
$this->timestamps = false;
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function webSocketDialogMsg(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(WebSocketDialogMsg::class, 'id', 'msg_id');
}
/**
* 仅标记成阅读
* @param $list
* @return void
*/
public static function onlyMarkRead($list)
{
$dialogMsg = [];
/** @var WebSocketDialogMsgRead $item */
foreach ($list as $item) {
$item->read_at = Carbon::now();
$item->save();
if (isset($dialogMsg[$item->msg_id])) {
$dialogMsg[$item->msg_id]['readNum']++;
} else {
$dialogMsg[$item->msg_id] = [
'dialogMsg' => $item->webSocketDialogMsg,
'readNum' => 1
];
}
}
foreach ($dialogMsg as $item) {
$item['dialogMsg']?->generatePercentage($item['readNum']);
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Models;
/**
* App\Models\WebSocketDialogMsgTodo
*
* @property int $id
* @property int|null $dialog_id 对话ID
* @property int|null $msg_id 消息ID
* @property int|null $userid 接收会员ID
* @property string|null $done_at 完成时间
* @property-read array|mixed $msg_data
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo query()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo whereDoneAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo whereMsgId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTodo whereUserid($value)
* @mixin \Eloquent
*/
class WebSocketDialogMsgTodo extends AbstractModel
{
protected $appends = [
'msg_data',
];
function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->timestamps = false;
}
/**
* 消息详情
* @return array|mixed
*/
public function getMsgDataAttribute()
{
if (!isset($this->appendattrs['msgData'])) {
$this->appendattrs['msgData'] = WebSocketDialogMsg::select(['id', 'type', 'msg'])->whereId($this->msg_id)->first()?->cancelAppend();
}
return $this->appendattrs['msgData'];
}
}

View File

@@ -2,25 +2,46 @@
namespace App\Models;
use Carbon\Carbon;
/**
* App\Models\WebSocketDialogUser
*
* @property int $id
* @property int|null $dialog_id 对话ID
* @property int|null $userid 会员ID
* @property string|null $top_at 置顶时间
* @property int|null $mark_unread 是否标记为未读0否1是
* @property int|null $silence 是否免打扰0否1是
* @property int|null $inviter 邀请人
* @property int|null $important 是否不可移出(项目、任务、部门人员)
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\WebSocketDialog|null $webSocketDialog
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser query()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereImportant($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereInviter($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereMarkUnread($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereSilence($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereTopAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereUserid($value)
* @mixin \Eloquent
*/
class WebSocketDialogUser extends AbstractModel
{
protected $dateFormat = 'Y-m-d H:i:s.v';
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function webSocketDialog(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(WebSocketDialog::class, 'id', 'dialog_id');
}
}

29
app/Models/clearHelper.php Executable file
View File

@@ -0,0 +1,29 @@
<?php
/**
* 清除模型class注释
*/
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
// 判断是否通过cmd命令执行的
if (function_exists('info')){
echo "Success \n";
return;
}
$path = dirname(__FILE__). '/';
$lists = scandir($path);
//
foreach ($lists AS $item) {
$fillPath = $path . $item;
if (!in_array($item, ['AbstractModel.php', 'clearHelper.php']) && str_ends_with($item, '.php')) {
$content = file_get_contents($fillPath);
preg_match("/\/\*\*([\s\S]*?)class\s*(.*?)\s*extends\s*AbstractModel/i", $content, $matchs);
if ($matchs[0]) {
$content = str_replace($matchs[0], 'class ' . $matchs[2] . ' extends AbstractModel', $content);
file_put_contents($fillPath, $content);
}
}
}
echo "Success \n";

View File

@@ -0,0 +1,193 @@
<?php
/**
* @Description :
*
* @Date : 2019-03-14 13:22
* @Author : hmy940118@gmail.com
*/
namespace App\Module\AgoraIO;
class AccessToken
{
const Privileges = array(
"kJoinChannel" => 1,
"kPublishAudioStream" => 2,
"kPublishVideoStream" => 3,
"kPublishDataStream" => 4,
"kPublishAudioCdn" => 5,
"kPublishVideoCdn" => 6,
"kRequestPublishAudioStream" => 7,
"kRequestPublishVideoStream" => 8,
"kRequestPublishDataStream" => 9,
"kInvitePublishAudioStream" => 10,
"kInvitePublishVideoStream" => 11,
"kInvitePublishDataStream" => 12,
"kAdministrateChannel" => 101
);
public $appID, $appCertificate, $channelName, $uid;
public $message;
/**
* AccessToken constructor.
* @throws \Exception
*/
public function __construct()
{
$this->message = new Message();
}
/**
* @param $uid
*/
public function setUid($uid)
{
if ($uid === 0) {
$this->uid = "";
} else {
$this->uid = $uid . '';
}
}
/**
* @param $name
* @param $str
* @return bool
*/
public function is_nonempty_string($name, $str)
{
if (is_string($str) && $str !== "") {
return true;
}
echo $name . " check failed, should be a non-empty string";
return false;
}
/**
* @param $appID
* @param $appCertificate
* @param $channelName
* @param $uid
* @return AccessToken|null
* @throws \Exception
*/
public static function init($appID, $appCertificate, $channelName, $uid)
{
$accessToken = new AccessToken();
if (!$accessToken->is_nonempty_string("appID", $appID) ||
!$accessToken->is_nonempty_string("appCertificate", $appCertificate) ||
!$accessToken->is_nonempty_string("channelName", $channelName)) {
return null;
}
$accessToken->appID = $appID;
$accessToken->appCertificate = $appCertificate;
$accessToken->channelName = $channelName;
$accessToken->setUid($uid);
$accessToken->message = new Message();
return $accessToken;
}
/**
* @param $token
* @param $appCertificate
* @param $channel
* @param $uid
* @return AccessToken|null
* @throws \Exception
*/
public static function initWithToken($token, $appCertificate, $channel, $uid)
{
$accessToken = new AccessToken();
if (!$accessToken->extract($token, $appCertificate, $channel, $uid)) {
return null;
}
return $accessToken;
}
/**
* @param $key
* @param $expireTimestamp
* @return $this
*/
public function addPrivilege($key, $expireTimestamp)
{
$this->message->privileges[$key] = $expireTimestamp;
return $this;
}
/**
* @param $token
* @param $appCertificate
* @param $channelName
* @param $uid
* @return bool
* @throws \Exception
*/
protected function extract($token, $appCertificate, $channelName, $uid)
{
$ver_len = 3;
$appid_len = 32;
$version = substr($token, 0, $ver_len);
if ($version !== "006") {
echo 'invalid version ' . $version;
return false;
}
if (!$this->is_nonempty_string("token", $token) ||
!$this->is_nonempty_string("appCertificate", $appCertificate) ||
!$this->is_nonempty_string("channelName", $channelName)) {
return false;
}
$appid = substr($token, $ver_len, $appid_len);
$content = (base64_decode(substr($token, $ver_len + $appid_len, strlen($token) - ($ver_len + $appid_len))));
$pos = 0;
$len = unpack("v", $content . substr($pos, 2))[1];
$pos += 2;
$sig = substr($content, $pos, $len);
$pos += $len;
$crc_channel = unpack("V", substr($content, $pos, 4))[1];
$pos += 4;
$crc_uid = unpack("V", substr($content, $pos, 4))[1];
$pos += 4;
$msgLen = unpack("v", substr($content, $pos, 2))[1];
$pos += 2;
$msg = substr($content, $pos, $msgLen);
$this->appID = $appid;
$message = new Message();
$message->unpackContent($msg);
$this->message = $message;
//non reversable values
$this->appCertificate = $appCertificate;
$this->channelName = $channelName;
$this->setUid($uid);
return true;
}
/**
* @return string
*/
public function build()
{
$msg = $this->message->packContent();
$val = array_merge(unpack("C*", $this->appID), unpack("C*", $this->channelName), unpack("C*", $this->uid), $msg);
$sig = hash_hmac('sha256', implode(array_map("chr", $val)), $this->appCertificate, true);
$crc_channel_name = crc32($this->channelName) & 0xffffffff;
$crc_uid = crc32($this->uid) & 0xffffffff;
$content = array_merge(unpack("C*", $this->packString($sig)), unpack("C*", pack("V", $crc_channel_name)), unpack("C*", pack("V", $crc_uid)), unpack("C*", pack("v", count($msg))), $msg);
$version = "006";
$ret = $version . $this->appID . base64_encode(implode(array_map("chr", $content)));
return $ret;
}
/**
* @param $value
* @return string
*/
public function packString($value)
{
return pack("v", strlen($value)) . $value;
}
}

View File

@@ -0,0 +1,122 @@
<?php
/**
* @Description :
*
* @Date : 2019-03-14 13:20
* @Author : hmy940118@gmail.com
*/
namespace App\Module\AgoraIO;
class AgoraTokenGenerator
{
const AttendeePrivileges = array(
AccessToken::Privileges["kJoinChannel"] => 0,
AccessToken::Privileges["kPublishAudioStream"] => 0,
AccessToken::Privileges["kPublishVideoStream"] => 0,
AccessToken::Privileges["kPublishDataStream"] => 0
);
const PublisherPrivileges = array(
AccessToken::Privileges["kJoinChannel"] => 0,
AccessToken::Privileges["kPublishAudioStream"] => 0,
AccessToken::Privileges["kPublishVideoStream"] => 0,
AccessToken::Privileges["kPublishDataStream"] => 0,
AccessToken::Privileges["kPublishAudioCdn"] => 0,
AccessToken::Privileges["kPublishVideoCdn"] => 0,
AccessToken::Privileges["kInvitePublishAudioStream"] => 0,
AccessToken::Privileges["kInvitePublishVideoStream"] => 0,
AccessToken::Privileges["kInvitePublishDataStream"] => 0
);
const SubscriberPrivileges = array(
AccessToken::Privileges["kJoinChannel"] => 0,
AccessToken::Privileges["kRequestPublishAudioStream"] => 0,
AccessToken::Privileges["kRequestPublishVideoStream"] => 0,
AccessToken::Privileges["kRequestPublishDataStream"] => 0
);
const AdminPrivileges = array(
AccessToken::Privileges["kJoinChannel"] => 0,
AccessToken::Privileges["kPublishAudioStream"] => 0,
AccessToken::Privileges["kPublishVideoStream"] => 0,
AccessToken::Privileges["kPublishDataStream"] => 0,
AccessToken::Privileges["kAdministrateChannel"] => 0
);
const Role = array(
"kRoleAttendee" => 0, // for communication
"kRolePublisher" => 1, // for live broadcast
"kRoleSubscriber" => 2, // for live broadcast
"kRoleAdmin" => 101
);
const RolePrivileges = array(
self::Role["kRoleAttendee"] => self::AttendeePrivileges,
self::Role["kRolePublisher"] => self::PublisherPrivileges,
self::Role["kRoleSubscriber"] => self::SubscriberPrivileges,
self::Role["kRoleAdmin"] => self::AdminPrivileges
);
public $token;
/**
* AgoraTokenGenerator constructor.
* @param $appID
* @param $appCertificate
* @param $channelName
* @param $uid
* @throws \Exception
*/
public function __construct($appID, $appCertificate, $channelName, $uid){
$this->token = new AccessToken();
$this->token->appID = $appID;
$this->token->appCertificate = $appCertificate;
$this->token->channelName = $channelName;
$this->token->setUid($uid);
}
/**
* @param $token
* @param $appCertificate
* @param $channel
* @param $uid
* @throws \Exception
*/
public function initWithToken($token, $appCertificate, $channel, $uid){
$this->token = AccessToken::initWithToken($token, $appCertificate, $channel, $uid);
}
/**
* @param $role
*/
public function initPrivilege($role){
$p = self::RolePrivileges[$role];
foreach($p as $key => $value){
$this->setPrivilege($key, $value);
}
}
/**
* @param $privilege
* @param $expireTimestamp
*/
public function setPrivilege($privilege, $expireTimestamp){
$this->token->addPrivilege($privilege, $expireTimestamp);
}
/**
* @param $privilege
*/
public function removePrivilege($privilege){
unset($this->token->message->privileges[$privilege]);
}
/**
* @return string
*/
public function buildToken(){
return $this->token->build();
}
}

View File

@@ -0,0 +1,70 @@
<?php
/**
* @Description :
*
* @Date : 2019-03-14 13:27
* @Author : hmy940118@gmail.com
*/
namespace App\Module\AgoraIO;
class Message
{
public $salt;
public $ts;
public $privileges;
/**
* Message constructor.
* @throws \Exception
*/
public function __construct()
{
$this->salt = rand(0, 100000);
$date = new \DateTime("now", new \DateTimeZone('UTC'));
$this->ts = $date->getTimestamp() + 168 * 3600; // 7天时间
$this->privileges = array();
}
/**
* @return array
*/
public function packContent()
{
$buffer = unpack("C*", pack("V", $this->salt));
$buffer = array_merge($buffer, unpack("C*", pack("V", $this->ts)));
$buffer = array_merge($buffer, unpack("C*", pack("v", sizeof($this->privileges))));
foreach ($this->privileges as $key => $value) {
$buffer = array_merge($buffer, unpack("C*", pack("v", $key)));
$buffer = array_merge($buffer, unpack("C*", pack("V", $value)));
}
return $buffer;
}
/**
* @param $msg
*/
public function unpackContent($msg)
{
$pos = 0;
$salt = unpack("V", substr($msg, $pos, 4))[1];
$pos += 4;
$ts = unpack("V", substr($msg, $pos, 4))[1];
$pos += 4;
$size = unpack("v", substr($msg, $pos, 2))[1];
$pos += 2;
$privileges = array();
for ($i = 0; $i < $size; $i++) {
$key = unpack("v", substr($msg, $pos, 2));
$pos += 2;
$value = unpack("V", substr($msg, $pos, 4));
$pos += 4;
$privileges[$key[1]] = $value[1];
}
$this->salt = $salt;
$this->ts = $ts;
$this->privileges = $privileges;
}
}

File diff suppressed because it is too large Load Diff

152
app/Module/BillExport.php Normal file
View File

@@ -0,0 +1,152 @@
<?php
namespace App\Module;
use Excel;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithStrictNullComparison;
use Maatwebsite\Excel\Concerns\WithTitle;
use Maatwebsite\Excel\Events\AfterSheet;
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
use PhpOffice\PhpSpreadsheet\Writer\Exception;
class BillExport implements WithHeadings, WithEvents, FromCollection, WithTitle, WithStrictNullComparison
{
protected $title;
protected $headings = [];
protected $data = [];
protected $typeLists = [];
protected $typeNumber = 0;
protected $styles = [];
public function __construct($title, array $data)
{
$this->title = $title;
$this->data = $data;
}
public static function create($data = [], $title = "Sheet1") {
if (is_string($data)) {
list($title, $data) = [$data, $title];
}
if (!is_array($data)) {
$data = [];
}
return new BillExport($title, $data);
}
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function setHeadings(array $headings) {
$this->headings = $headings;
return $this;
}
public function setData(array $data) {
$this->data = $data;
return $this;
}
public function setTypeList(array $typeList, $number = 0) {
$this->typeLists = $typeList;
$this->typeNumber = $number;
return $this;
}
public function setStyles(array $styles)
{
$this->styles = $styles;
return $this;
}
public function store($fileName = '') {
if (empty($fileName)) {
$fileName = date("YmdHis") . '.xls';
}
try {
return Excel::store($this, $fileName);
} catch (Exception|\PhpOffice\PhpSpreadsheet\Exception $e) {
return "导出错误:" . $e->getMessage();
}
}
public function download($fileName = '') {
if (empty($fileName)) {
$fileName = date("YmdHis") . '.xls';
}
try {
return Excel::download($this, $fileName);
} catch (Exception|\PhpOffice\PhpSpreadsheet\Exception $e) {
return "导出错误:" . $e->getMessage();
}
}
/**
* 导出的文件标题
* @return string
*/
public function title(): string
{
return $this->title;
}
/**
* 标题行
* @return array
*/
public function headings(): array
{
return $this->headings;
}
/**
* 导出的内容
* @return \Illuminate\Support\Collection
*/
public function collection()
{
return collect($this->data);
}
/**
* 设置单元格事件
* @return array
*/
public function registerEvents(): array
{
return [
AfterSheet::Class => function (AfterSheet $event) {
if ($this->styles) {
foreach ($this->styles as $cell => $style) {
$event->sheet->getDelegate()->getStyle($cell)->applyFromArray($style);
}
}
$count = count($this->data);
foreach ($this->typeLists AS $cell => $typeList) {
if ($cell && $typeList) {
$p = $this->headings ? 1 : 0;
for ($i = 1 + $p; $i <= max($count, $this->typeNumber) + $p; $i++) {
$validation = $event->sheet->getDelegate()->getCell($cell . $i)->getDataValidation();
$validation->setType(DataValidation::TYPE_LIST);
$validation->setErrorStyle(DataValidation::STYLE_WARNING);
$validation->setAllowBlank(false);
$validation->setShowDropDown(true);
$validation->setShowInputMessage(true);
$validation->setShowErrorMessage(true);
$validation->setErrorTitle('输入的值不合法');
$validation->setError('选择的值不在列表中,请选择列表中的值');
$validation->setPromptTitle('从列表中选择');
$validation->setPrompt('请选择下拉列表中的值');
$validation->setFormula1('"' . implode(',', $typeList) . '"');
}
}
}
}
];
}
}

16
app/Module/BillImport.php Normal file
View File

@@ -0,0 +1,16 @@
<?php
namespace App\Module;
use Maatwebsite\Excel\Concerns\ToArray;
class BillImport implements ToArray
{
public function Array(Array $tables)
{
return $tables;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Module;
use Maatwebsite\Excel\Concerns\Exportable;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
class BillMultipleExport implements WithMultipleSheets
{
use Exportable;
protected $data = [];
public function __construct(array $data)
{
$this->data = $data;
}
public function sheets(): array
{
return $this->data;
}
}

419
app/Module/Doo.php Normal file
View File

@@ -0,0 +1,419 @@
<?php
namespace App\Module;
use App\Exceptions\ApiException;
use App\Models\User;
use Cache;
use Carbon\Carbon;
use FFI;
class Doo
{
private static $doo;
private static $passphrase = "LYHevk5n";
/**
* char转为字符串
* @param $text
* @return string
*/
private static function string($text): string
{
return FFI::string($text);
}
/**
* 装载
* @param $token
* @param $language
*/
public static function load($token = null, $language = null)
{
self::$doo = FFI::cdef(<<<EOF
void initialize(char* work, char* token, char* lang);
char* license();
char* licenseDecode(char* license);
char* licenseSave(char* license);
int userId();
char* userExpiredAt();
char* userEmail();
char* userEncrypt();
char* userToken();
char* userCreate(char* email, char* password);
char* tokenEncode(int userid, char* email, char* encrypt, int days);
char* tokenDecode(char* val);
char* translate(char* val, char* val);
char* md5s(char* text, char* password);
char* macs();
char* dooSN();
char* pgpGenerateKeyPair(char* name, char* email, char* passphrase);
char* pgpEncrypt(char* plainText, char* publicKey);
char* pgpDecrypt(char* cipherText, char* privateKey, char* passphrase);
EOF, "/usr/lib/doo/doo.so");
$token = $token ?: Base::headerOrInput('token');
$language = $language ?: Base::headerOrInput('language');
self::$doo->initialize("/var/www", $token, $language);
}
/**
* 获取实例
* @param $token
* @param $language
* @return mixed
*/
public static function doo($token = null, $language = null)
{
if (self::$doo == null) {
self::load($token, $language);
}
return self::$doo;
}
/**
* License
* @return array
*/
public static function license(): array
{
$array = Base::json2array(self::string(self::doo()->license()));
$ips = explode(",", $array['ip']);
$array['ip'] = [];
foreach ($ips as $ip) {
if (Base::is_ipv4($ip)) {
$array['ip'][] = $ip;
}
}
$domains = explode(",", $array['domain']);
$array['domain'] = [];
foreach ($domains as $domain) {
if (Base::is_domain($domain)) {
$array['domain'][] = $domain;
}
}
$macs = explode(",", $array['mac']);
$array['mac'] = [];
foreach ($macs as $mac) {
if (Base::isMac($mac)) {
$array['mac'][] = $mac;
}
}
$emails = explode(",", $array['email']);
$array['email'] = [];
foreach ($emails as $email) {
if (Base::isEmail($email)) {
$array['email'][] = $email;
}
}
return $array;
}
/**
* 获取License原文
* @return string
*/
public static function licenseContent(): string
{
if (env("SYSTEM_LICENSE") == 'hidden') {
return '';
}
$paths = [
config_path("LICENSE"),
config_path("license"),
app_path("LICENSE"),
app_path("license"),
];
$content = "";
foreach ($paths as $path) {
if (file_exists($path)) {
$content = file_get_contents($path);
break;
}
}
return $content;
}
/**
* 解析License
* @param $license
* @return array
*/
public static function licenseDecode($license): array
{
return Base::json2array(self::string(self::doo()->licenseDecode($license)));
}
/**
* 保存License
* @param $license
*/
public static function licenseSave($license): void
{
$res = self::string(self::doo()->licenseSave($license));
if ($res != 'success') {
throw new ApiException($res ?: 'LICENSE 保存失败');
}
}
/**
* 当前会员ID来自请求的token
* @return int
*/
public static function userId(): int
{
return intval(self::doo()->userId());
}
/**
* token是否过期来自请求的token
* @return bool
*/
public static function userExpired(): bool
{
$expiredAt = self::userExpiredAt();
return $expiredAt && Carbon::parse($expiredAt)->isBefore(Carbon::now());
}
/**
* token过期时间来自请求的token
* @return string
*/
public static function userExpiredAt(): string
{
$expiredAt = self::string(self::doo()->userExpiredAt());
return $expiredAt === 'forever' ? '' : $expiredAt;
}
/**
* 当前会员邮箱地址来自请求的token
* @return string
*/
public static function userEmail(): string
{
return self::string(self::doo()->userEmail());
}
/**
* 当前会员Encrypt来自请求的token
* @return string
*/
public static function userEncrypt(): string
{
return self::string(self::doo()->userEncrypt());
}
/**
* 当前会员token来自请求的token
* @return string
*/
public static function userToken(): string
{
return self::string(self::doo()->userToken());
}
/**
* 创建帐号
* @param $email
* @param $password
* @return User|null
*/
public static function userCreate($email, $password): User|null
{
$data = Base::json2array(self::string(self::doo()->userCreate($email, $password)));
if (Base::isError($data)) {
throw new ApiException($data['msg'] ?: '注册失败');
}
$user = User::whereEmail($email)->first();
if (empty($user)) {
throw new ApiException('注册失败');
}
return $user;
}
/**
* 生成token编码token
* @param $userid
* @param $email
* @param $encrypt
* @param int $days 有效时间(天)
* @return string
*/
public static function tokenEncode($userid, $email, $encrypt, int $days = 15): string
{
return self::string(self::doo()->tokenEncode($userid, $email, $encrypt, $days));
}
/**
* 解码token
* @param $token
* @return array
*/
public static function tokenDecode($token): array
{
return Base::json2array(self::string(self::doo()->tokenDecode($token)));
}
/**
* 翻译
* @param $text
* @param string $type
* @return string
*/
public static function translate($text, string $type = ""): string
{
return self::string(self::doo()->translate($text, $type));
}
/**
* md5防破解
* @param $text
* @param string $password
* @return string
*/
public static function md5s($text, string $password = ""): string
{
return self::string(self::doo()->md5s($text, $password));
}
/**
* 获取php容器mac地址组
* @return array
*/
public static function macs(): array
{
$macs = explode(",", self::string(self::doo()->macs()));
$array = [];
foreach ($macs as $mac) {
if (Base::isMac($mac)) {
$array[] = $mac;
}
}
return $array;
}
/**
* 获取当前SN
* @return string
*/
public static function dooSN(): string
{
return self::string(self::doo()->dooSN());
}
/**
* 生成PGP密钥对
* @param $name
* @param $email
* @param string $passphrase
* @return array
*/
public static function pgpGenerateKeyPair($name, $email, string $passphrase = ""): array
{
return Base::json2array(self::string(self::doo()->pgpGenerateKeyPair($name, $email, $passphrase)));
}
/**
* PGP加密
* @param $plaintext
* @param $publicKey
* @return string
*/
public static function pgpEncrypt($plaintext, $publicKey): string
{
if (strlen($publicKey) < 50) {
$keyCache = Base::json2array(Cache::get("KeyPair::" . $publicKey));
$publicKey = $keyCache['public_key'];
}
return self::string(self::doo()->pgpEncrypt($plaintext, $publicKey));
}
/**
* PGP解密
* @param $encryptedText
* @param $privateKey
* @param null $passphrase
* @return string
*/
public static function pgpDecrypt($encryptedText, $privateKey, $passphrase = null): string
{
if (strlen($privateKey) < 50) {
$keyCache = Base::json2array(Cache::get("KeyPair::" . $privateKey));
$privateKey = $keyCache['private_key'];
$passphrase = $keyCache['passphrase'];
}
return self::string(self::doo()->pgpDecrypt($encryptedText, $privateKey, $passphrase));
}
/**
* PGP加密API
* @param $plaintext
* @param $publicKey
* @return string
*/
public static function pgpEncryptApi($plaintext, $publicKey): string
{
$content = Base::array2json($plaintext);
$content = self::pgpEncrypt($content, $publicKey);
return preg_replace("/\s*-----(BEGIN|END) PGP MESSAGE-----\s*/i", "", $content);
}
/**
* PGP解密API
* @param $encryptedText
* @param null $privateKey
* @param null $passphrase
* @return array
*/
public static function pgpDecryptApi($encryptedText, $privateKey, $passphrase = null): array
{
$content = "-----BEGIN PGP MESSAGE-----\n\n" . $encryptedText . "\n-----END PGP MESSAGE-----";
$content = self::pgpDecrypt($content, $privateKey, $passphrase);
return Base::json2array($content);
}
/**
* 解析PGP参数
* @param $string
* @return string[]
*/
public static function pgpParseStr($string): array
{
$array = [
'encrypt_type' => '',
'encrypt_id' => '',
'client_type' => '',
'client_key' => '',
];
$string = str_replace(";", "&", $string);
parse_str($string, $params);
foreach ($params as $key => $value) {
$key = strtolower(trim($key));
if ($key) {
$array[$key] = trim($value);
}
}
if ($array['client_type'] === 'pgp' && $array['client_key']) {
$array['client_key'] = self::pgpPublicFormat($array['client_key']);
}
return $array;
}
/**
* 还原公钥格式
* @param $key
* @return string
*/
public static function pgpPublicFormat($key): string
{
$key = str_replace(["-", "_", "$"], ["+", "/", "\n"], $key);
if (!str_contains($key, '-----BEGIN PGP PUBLIC KEY BLOCK-----')) {
$key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n" . $key . "\n-----END PGP PUBLIC KEY BLOCK-----";
}
return $key;
}
}

321
app/Module/Extranet.php Normal file
View File

@@ -0,0 +1,321 @@
<?php
namespace App\Module;
use Cache;
use Carbon\Carbon;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
/**
* 外网资源请求
*/
class Extranet
{
/**
* 获取IP地址经纬度
* @param string $ip
* @return array
*/
public static function getIpGcj02(string $ip = ''): array
{
if (empty($ip)) {
$ip = Base::getIp();
}
$cacheKey = "getIpPoint::" . md5($ip);
$result = Cache::rememberForever($cacheKey, function () use ($ip) {
return Ihttp::ihttp_request("https://www.ifreesite.com/ipaddress/address.php?q=" . $ip, [], [], 12);
});
if (Base::isError($result)) {
Cache::forget($cacheKey);
return $result;
}
$data = $result['data'];
$lastPos = strrpos($data, ',');
$long = floatval(Base::getMiddle(substr($data, $lastPos + 1), null, ')'));
$lat = floatval(Base::getMiddle(substr($data, strrpos(substr($data, 0, $lastPos), ',') + 1), null, ','));
return Base::retSuccess("success", [
'long' => $long,
'lat' => $lat,
]);
}
/**
* 百度接口根据ip获取经纬度
* @param string $ip
* @return array
*/
public static function getIpGcj02ByBaidu(string $ip = ''): array
{
if (empty($ip)) {
$ip = Base::getIp();
}
$cacheKey = "getIpPoint::" . md5($ip);
$result = Cache::rememberForever($cacheKey, function () use ($ip) {
$ak = Config::get('app.baidu_app_key');
$url = 'http://api.map.baidu.com/location/ip?ak=' . $ak . '&ip=' . $ip . '&coor=bd09ll';
return Ihttp::ihttp_request($url, [], [], 12);
});
if (Base::isError($result)) {
Cache::forget($cacheKey);
return $result;
}
$data = json_decode($result['data'], true);
// x坐标纬度, y坐标经度
$long = Arr::get($data, 'content.point.x');
$lat = Arr::get($data, 'content.point.y');
return Base::retSuccess("success", [
'long' => $long,
'lat' => $lat,
]);
}
/**
* 获取IP地址详情
* @param string $ip
* @return array
*/
public static function getIpInfo(string $ip = ''): array
{
if (empty($ip)) {
$ip = Base::getIp();
}
$cacheKey = "getIpInfo::" . md5($ip);
$result = Cache::rememberForever($cacheKey, function () use ($ip) {
return Ihttp::ihttp_request("http://ip.taobao.com/service/getIpInfo.php?accessKey=alibaba-inc&ip=" . $ip, [], [], 12);
});
if (Base::isError($result)) {
Cache::forget($cacheKey);
return $result;
}
$data = json_decode($result['data'], true);
if (!is_array($data) || intval($data['code']) != 0) {
Cache::forget($cacheKey);
return Base::retError("error ip: -1");
}
$data = $data['data'];
if (!is_array($data) || !isset($data['country'])) {
return Base::retError("error ip: -2");
}
$data['text'] = $data['country'];
$data['textSmall'] = $data['country'];
if ($data['region'] && $data['region'] != $data['country'] && $data['region'] != "XX") {
$data['text'] .= " " . $data['region'];
$data['textSmall'] = $data['region'];
}
if ($data['city'] && $data['city'] != $data['region'] && $data['city'] != "XX") {
$data['text'] .= " " . $data['city'];
$data['textSmall'] .= " " . $data['city'];
}
if ($data['county'] && $data['county'] != $data['city'] && $data['county'] != "XX") {
$data['text'] .= " " . $data['county'];
$data['textSmall'] .= " " . $data['county'];
}
return Base::retSuccess("success", $data);
}
/**
* 判断是否工作日
* @param string $Ymd 年月日20220102
* @return int
* 0: 工作日
* 1: 非工作日
* 2: 获取不到远程数据的非工作日(周六、日)
* 所以可以用>0来判断是否工作日
*/
public static function isHoliday(string $Ymd): int
{
$time = strtotime($Ymd . " 00:00:00");
$holidayKey = "holiday::" . date("Ym", $time);
$holidayData = Cache::remember($holidayKey, now()->addMonth(), function () use ($time) {
$apiMonth = date("Ym", $time);
$apiResult = Ihttp::ihttp_request("https://api.apihubs.cn/holiday/get?field=date&month={$apiMonth}&workday=2&size=31", [], [], 20);
if (Base::isError($apiResult)) {
info('[holiday] get error');
return [];
}
$apiResult = Base::json2array($apiResult['data']);
if ($apiResult['code'] !== 0) {
info('[holiday] result error');
return [];
}
return array_map(function ($item) {
return $item['date'];
}, $apiResult['data']['list']);
});
if (empty($holidayData)) {
Cache::forget($holidayKey);
return in_array(date("w", $time), [0, 6]) ? 2 : 0;
}
return in_array($Ymd, $holidayData) ? 1 : 0;
}
/**
* Drawio 图标搜索
* @param $query
* @param $page
* @param $size
* @return array
*/
public static function drawioIconSearch($query, $page, $size): array
{
$result = self::curl("https://app.diagrams.net/iconSearch?q={$query}&p={$page}&c={$size}", 15 * 86400);
if ($result = Base::json2array($result)) {
return $result;
}
return [
'icons' => [],
'total_count' => 0
];
}
/**
* 随机笑话接口
* @return array
*/
public static function randJoke(): array
{
$jokeKey = env("JUKE_KEY_JOKE");
if ($jokeKey) {
$data = self::curl("http://v.juhe.cn/joke/randJoke.php?key=" . $jokeKey);
$data = Base::json2array($data);
if ($data['reason'] === 'success') {
return $data['result'];
}
}
return [];
}
/**
* 心灵鸡汤
* @return string
*/
public static function soups(): string
{
$soupKey = env("JUKE_KEY_SOUP");
if ($soupKey) {
$data = self::curl("https://apis.juhe.cn/fapig/soup/query?key=" . $soupKey);
$data = Base::json2array($data);
if ($data['reason'] === 'success' && $text = trim($data['result']['text'])) {
return $text;
}
}
return "";
}
/**
* 签到机器人网络内容
* @param $type
* @return string
*/
public static function checkinBotQuickMsg($type): string
{
$text = "维护中...";
switch ($type) {
case "it":
$data = self::curl('http://vvhan.api.hitosea.com/api/hotlist?type=itNews', 3600);
if ($data = Base::json2array($data)) {
$i = 1;
$array = array_map(function ($item) use (&$i) {
if ($item['title'] && $item['desc']) {
return "<p>" . ($i++) . ". <strong><a href='{$item['mobilUrl']}' target='_blank'>{$item['title']}</a></strong></p><p>{$item['desc']}</p>";
} else {
return null;
}
}, $data['data']);
$array = array_values(array_filter($array));
if ($array) {
array_unshift($array, "<p><strong>{$data['title']}</strong>{$data['update_time']}</p>");
$text = implode("<p>&nbsp;</p>", $array);
}
}
break;
case "36ke":
$data = self::curl('http://vvhan.api.hitosea.com/api/hotlist?type=36Ke', 3600);
if ($data = Base::json2array($data)) {
$i = 1;
$array = array_map(function ($item) use (&$i) {
if ($item['title'] && $item['desc']) {
return "<p>" . ($i++) . ". <strong><a href='{$item['mobilUrl']}' target='_blank'>{$item['title']}</a></strong></p><p>{$item['desc']}</p>";
} else {
return null;
}
}, $data['data']);
$array = array_values(array_filter($array));
if ($array) {
array_unshift($array, "<p><strong>{$data['title']}</strong>{$data['update_time']}</p>");
$text = implode("<p>&nbsp;</p>", $array);
}
}
break;
case "60s":
$data = self::curl('http://vvhan.api.hitosea.com/api/60s?type=json', 3600);
if ($data = Base::json2array($data)) {
$i = 1;
$array = array_map(function ($item) use (&$i) {
if ($item) {
return "<p>" . ($i++) . ". {$item}</p>";
} else {
return null;
}
}, $data['data']);
$array = array_values(array_filter($array));
if ($array) {
array_unshift($array, "<p><strong>{$data['name']}</strong>{$data['time'][0]}</p>");
$text = implode("<p>&nbsp;</p>", $array);
}
}
break;
case "joke":
$text = "笑话被掏空";
$data = self::curl('http://vvhan.api.hitosea.com/api/joke?type=json', 5);
if ($data = Base::json2array($data)) {
if ($data = trim($data['joke'])) {
$text = "开心笑话:{$data}";
}
}
break;
case "soup":
$text = "鸡汤分完了";
$data = self::curl('https://api.ayfre.com/jt/?type=bot', 5);
if ($data) {
$text = "心灵鸡汤:{$data}";
}
break;
}
return $text;
}
/**
* @param $url
* @param int $cacheSecond 缓存时间如果结果为空则缓存有效30秒
* @param int $timeout
* @return string
*/
private static function curl($url, int $cacheSecond = 0, int $timeout = 15): 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);
$content = Base::isSuccess($result) ? trim($result['data']) : '';
if (empty($content) && $cacheSecond > 30) {
Cache::put($key, "", Carbon::now()->addSeconds(30));
}
return $content;
});
} else {
$result = Ihttp::ihttp_request($url, [], [], $timeout);
$content = Base::isSuccess($result) ? trim($result['data']) : '';
}
//
return $content;
}
}

View File

@@ -2,8 +2,6 @@
namespace App\Module;
use Exception;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
class Ihttp
@@ -88,7 +86,7 @@ class Ihttp
return Base::retError($error);
} else {
if ($isGb2312) {
try { $data = iconv('GB2312', 'UTF-8', $data); }catch (Exception $e) { }
try { $data = iconv('GB2312', 'UTF-8', $data); }catch (\Throwable) { }
}
$response = self::ihttp_response_parse($data);
Base::addLog([
@@ -148,7 +146,7 @@ class Ihttp
$content .= fgets($fp, 512);
fclose($fp);
if ($isGb2312) {
try { $content = iconv('GB2312', 'UTF-8', $content); }catch (Exception $e) { }
try { $content = iconv('GB2312', 'UTF-8', $content); }catch (\Throwable) { }
}
$response = self::ihttp_response_parse($content, true);
Base::addLog([

170
app/Module/ImgCompress.php Normal file
View File

@@ -0,0 +1,170 @@
<?php
namespace App\Module;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
/**
* 图片压缩类:通过缩放来压缩。
* 如果要保持源图比例,把参数$percent保持为1即可。
* 即使原比例压缩也可大幅度缩小。数码相机4M图片。也可以缩为700KB左右。如果缩小比例则体积会更小。
*
* 结果:可保存、可直接显示。
*/
class ImgCompress
{
private $src;
private $image;
private $imageinfo;
private $percent;
/**
* 图片压缩
* @param string $src 源图
* @param float $percent 压缩比例
*/
public function __construct($src, $percent = 1)
{
$this->src = $src;
$this->percent = $percent;
}
/** 高清压缩图片
* @param string $saveName 提供图片名(可不带扩展名,用源图扩展名)用于保存。或不提供文件名直接显示
*/
public function compressImg($saveName = '')
{
if (!$this->_openImage()) { //打开图片
return;
}
if (!empty($saveName)) $this->_saveImage($saveName); //保存
else $this->_showImage();
}
/**
* 内部:打开图片
*/
private function _openImage()
{
list($width, $height, $type, $attr) = getimagesize($this->src);
$this->imageinfo = array(
'width' => $width,
'height' => $height,
'type' => image_type_to_extension($type, false),
'attr' => $attr
);
$fun = "imagecreatefrom" . $this->imageinfo['type'];
if (!function_exists($fun)) {
return false;
}
$this->image = $fun($this->src);
$this->_thumpImage();
return true;
}
/**
* 内部:操作图片
*/
private function _thumpImage()
{
$new_width = $this->imageinfo['width'] * $this->percent;
$new_height = $this->imageinfo['height'] * $this->percent;
$image_thump = imagecreatetruecolor($new_width, $new_height);
//将原图复制带图片载体上面,并且按照一定比例压缩,极大的保持了清晰度
imagecopyresampled($image_thump, $this->image, 0, 0, 0, 0, $new_width, $new_height, $this->imageinfo['width'], $this->imageinfo['height']);
imagedestroy($this->image);
$this->image = $image_thump;
}
/**
* 输出图片:保存图片则用saveImage()
*/
private function _showImage()
{
header('Content-Type: image/' . $this->imageinfo['type']);
$funcs = "image" . $this->imageinfo['type'];
$funcs($this->image);
}
/**
* 保存图片到硬盘:
* @param string $dstImgName 1、可指定字符串不带后缀的名称使用源图扩展名 。2、直接指定目标图片名带扩展名。
*/
private function _saveImage($dstImgName)
{
if (empty($dstImgName)) return false;
if (str_contains($dstImgName, '.')) {
$dstName = $dstImgName;
} else {
$allowImgs = ['.jpg', '.jpeg', '.webp', '.png', '.bmp', '.wbmp', '.gif']; //如果目标图片名有后缀就用目标图片扩展名 后缀,如果没有,则用源图的扩展名
$dstExt = strrchr($dstImgName, ".");
$sourseExt = strrchr($this->src, ".");
if (!empty($dstExt)) $dstExt = strtolower($dstExt);
if (!empty($sourseExt)) $sourseExt = strtolower($sourseExt);
//有指定目标名扩展名
if (!empty($dstExt) && in_array($dstExt, $allowImgs)) {
$dstName = $dstImgName;
} elseif (!empty($sourseExt) && in_array($sourseExt, $allowImgs)) {
$dstName = $dstImgName . $sourseExt;
} else {
$dstName = $dstImgName . $this->imageinfo['type'];
}
}
$funcs = "image" . $this->imageinfo['type'];
if (!function_exists($funcs)) {
return false;
}
$funcs($this->image, $dstName);
return true;
}
/**
* 销毁图片
*/
public function __destruct()
{
if ($this->image) {
imagedestroy($this->image);
}
}
/**
* 压缩图片静态方法
* @param string $src 原图地址
* @param float $percent 压缩比例
* @param float $minSize 最小压缩大小小于这个不压缩单位KB
* @return void
*/
public static function compress(string $src, float $percent = 1, float $minSize = 10): void
{
if (Base::settingFind("system", "image_compress") === 'close') {
return;
}
if (!file_exists($src)) {
return;
}
$allowImgs = ['.jpg', '.jpeg', '.webp', '.png', '.bmp', '.wbmp'];
if (!in_array(strrchr($src, "."), $allowImgs)) {
return;
}
$size = filesize($src);
if ($minSize > 0 && $size < $minSize * 1024) {
return;
}
try {
$img = new ImgCompress($src, $percent);
$tmp = $src . '.compress.tmp';
$img->compressImg($tmp);
if (file_exists($tmp)) {
if (filesize($tmp) > $size) {
unlink($tmp);
return;
}
unlink($src);
rename($tmp, $src);
}
} catch (\Exception) {
return;
}
}
}

390
app/Module/RandomColor.php Normal file
View File

@@ -0,0 +1,390 @@
<?php
/**
* RandomColor 1.0.5
*
* PHP port of David Merfield JavaScript randomColor
* https://github.com/davidmerfield/randomColor
*
*
* The MIT License (MIT)
*
* Copyright (c) 2014-2022 Damien "Mistic" Sorel
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
namespace App\Module;
class RandomColor
{
static public $dictionary = null;
private function __construct() {}
static public function one($options = array())
{
$h = self::_pickHue($options);
$s = self::_pickSaturation($h, $options);
$v = self::_pickBrightness($h, $s, $options);
return self::format(compact('h','s','v'), @$options['format']);
}
static public function many($count, $options = array())
{
$colors = array();
for ($i = 0; $i < $count; $i++)
{
$colors[] = self::one($options);
}
return $colors;
}
static public function format($hsv, $format='hex')
{
switch ($format)
{
case 'hsv':
return $hsv;
case 'hsl':
return self::hsv2hsl($hsv);
case 'hslCss':
$hsl = self::hsv2hsl($hsv);
return 'hsl(' . $hsl['h'] . ',' . $hsl['s'] . '%,' . $hsl['l'] . '%)';
case 'rgb':
return self::hsv2rgb($hsv);
case 'rgbCss':
return 'rgb(' . implode(',', self::hsv2rgb($hsv)) . ')';
case 'hex':
default:
return self::hsv2hex($hsv);
}
}
static private function _pickHue($options)
{
$range = self::_getHueRange($options);
if (empty($range))
{
return 0;
}
$hue = self::_rand($range, $options);
// Instead of storing red as two separate ranges,
// we group them, using negative numbers
if ($hue < 0)
{
$hue = 360 + $hue;
}
return $hue;
}
static private function _pickSaturation($h, $options)
{
if (@$options['hue'] === 'monochrome')
{
return 0;
}
if (@$options['luminosity'] === 'random')
{
return self::_rand(array(0, 100), $options);
}
$colorInfo = self::_getColorInfo($h);
$range = $colorInfo['s'];
switch (@$options['luminosity'])
{
case 'bright':
$range[0] = 55;
break;
case 'dark':
$range[0] = $range[1] - 10;
break;
case 'light':
$range[1] = 55;
break;
}
return self::_rand($range, $options);
}
static private function _pickBrightness($h, $s, $options)
{
if (@$options['luminosity'] === 'random')
{
$range = array(0, 100);
}
else
{
$range = array(
self::_getMinimumBrightness($h, $s),
100
);
switch (@$options['luminosity'])
{
case 'dark':
$range[1] = $range[0] + 20;
break;
case 'light':
$range[0] = round(($range[1] + $range[0]) / 2);
break;
}
}
return self::_rand($range, $options);
}
static private function _getHueRange($options)
{
$ranges = array();
if (isset($options['hue']))
{
if (!is_array($options['hue']))
{
$options['hue'] = array($options['hue']);
}
foreach ($options['hue'] as $hue)
{
if ($hue === 'random')
{
$ranges[] = array(0, 360);
}
else if (isset(self::$dictionary[$hue]))
{
$ranges[] = self::$dictionary[$hue]['h'];
}
else if (is_numeric($hue))
{
$hue = intval($hue);
if ($hue <= 360 && $hue >= 0)
{
$ranges[] = array($hue, $hue);
}
}
}
}
if (($l = count($ranges)) === 0)
{
return array(0, 360);
}
else if ($l === 1)
{
return $ranges[0];
}
else
{
return $ranges[self::_rand(array(0, $l-1), $options)];
}
}
static private function _getMinimumBrightness($h, $s)
{
$colorInfo = self::_getColorInfo($h);
$bounds = $colorInfo['bounds'];
for ($i = 0, $l = count($bounds); $i < $l - 1; $i++)
{
$s1 = $bounds[$i][0];
$v1 = $bounds[$i][1];
$s2 = $bounds[$i+1][0];
$v2 = $bounds[$i+1][1];
if ($s >= $s1 && $s <= $s2)
{
$m = ($v2 - $v1) / ($s2 - $s1);
$b = $v1 - $m * $s1;
return round($m * $s + $b);
}
}
return 0;
}
static private function _getColorInfo($h)
{
// Maps red colors to make picking hue easier
if ($h >= 334 && $h <= 360)
{
$h-= 360;
}
foreach (self::$dictionary as $color)
{
if ($color['h'] !== null && $h >= $color['h'][0] && $h <= $color['h'][1])
{
return $color;
}
}
}
static private function _rand($bounds, $options)
{
if (isset($options['prng']))
{
return $options['prng']($bounds[0], $bounds[1]);
}
else
{
return mt_rand($bounds[0], $bounds[1]);
}
}
static public function hsv2hex($hsv)
{
$rgb = self::hsv2rgb($hsv);
$hex = '#';
foreach ($rgb as $c)
{
$hex.= str_pad(dechex($c), 2, '0', STR_PAD_LEFT);
}
return $hex;
}
static public function hsv2hsl($hsv)
{
extract($hsv);
$s/= 100;
$v/= 100;
$k = (2-$s)*$v;
return array(
'h' => $h,
's' => round($s*$v / ($k < 1 ? $k : 2-$k), 4) * 100,
'l' => $k/2 * 100,
);
}
static public function hsv2rgb($hsv)
{
extract($hsv);
$h/= 360;
$s/= 100;
$v/= 100;
$i = floor($h * 6);
$f = $h * 6 - $i;
$m = $v * (1 - $s);
$n = $v * (1 - $s * $f);
$k = $v * (1 - $s * (1 - $f));
$r = 1;
$g = 1;
$b = 1;
switch ($i)
{
case 0:
list($r,$g,$b) = array($v,$k,$m);
break;
case 1:
list($r,$g,$b) = array($n,$v,$m);
break;
case 2:
list($r,$g,$b) = array($m,$v,$k);
break;
case 3:
list($r,$g,$b) = array($m,$n,$v);
break;
case 4:
list($r,$g,$b) = array($k,$m,$v);
break;
case 5:
case 6:
list($r,$g,$b) = array($v,$m,$n);
break;
}
return array(
'r' => floor($r*255),
'g' => floor($g*255),
'b' => floor($b*255),
);
}
}
/*
* h=hueRange
* s=saturationRange : bounds[0][0] ; bounds[-][0]
*/
RandomColor::$dictionary = array(
'monochrome' => array(
'bounds' => array(array(0,0), array(100,0)),
'h' => NULL,
's' => array(0,100)
),
'red' => array(
'bounds' => array(array(20,100),array(30,92),array(40,89),array(50,85),array(60,78),array(70,70),array(80,60),array(90,55),array(100,50)),
'h' => array(-26,18),
's' => array(20,100)
),
'orange' => array(
'bounds' => array(array(20,100),array(30,93),array(40,88),array(50,86),array(60,85),array(70,70),array(100,70)),
'h' => array(19,46),
's' => array(20,100)
),
'yellow' => array(
'bounds' => array(array(25,100),array(40,94),array(50,89),array(60,86),array(70,84),array(80,82),array(90,80),array(100,75)),
'h' => array(47,62),
's' => array(25,100)
),
'green' => array(
'bounds' => array(array(30,100),array(40,90),array(50,85),array(60,81),array(70,74),array(80,64),array(90,50),array(100,40)),
'h' => array(63,178),
's' => array(30,100)
),
'blue' => array(
'bounds' => array(array(20,100),array(30,86),array(40,80),array(50,74),array(60,60),array(70,52),array(80,44),array(90,39),array(100,35)),
'h' => array(179,257),
's' => array(20,100)
),
'purple' => array(
'bounds' => array(array(20,100),array(30,87),array(40,79),array(50,70),array(60,65),array(70,59),array(80,52),array(90,45),array(100,42)),
'h' => array(258,282),
's' => array(20,100)
),
'pink' => array(
'bounds' => array(array(20,100),array(30,90),array(40,86),array(60,84),array(80,80),array(90,75),array(100,73)),
'h' => array(283,334),
's' => array(20,100)
)
);

44
app/Module/TimeRange.php Normal file
View File

@@ -0,0 +1,44 @@
<?php
namespace App\Module;
use Carbon\Carbon;
class TimeRange
{
public ?Carbon $updated;
public ?Carbon $deleted;
/**
* @param $data
*/
public function __construct($data)
{
if (is_array($data)) {
$range = explode("-", str_replace([",", "|"], "-", $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 = explode("-", str_replace([",", "|"], "-", $data));
}
//
$updated = Base::isNumber($range[0]) ? intval($range[0]) : trim($range[0]);
$deleted = Base::isNumber($range[1]) ? intval($range[1]) : trim($range[1]);
//
$this->updated = $updated ? Carbon::parse($updated) : null;
$this->deleted = $deleted ? Carbon::parse($deleted) : null;
}
/**
* @param $data
* @return TimeRange
*/
public static function parse($data): TimeRange
{
return new self($data);
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace App\Observers;
use App\Models\Deleted;
use App\Models\Project;
use App\Models\ProjectUser;
class ProjectObserver
{
/**
* Handle the Project "created" event.
*
* @param \App\Models\Project $project
* @return void
*/
public function created(Project $project)
{
//
}
/**
* Handle the Project "updated" event.
*
* @param \App\Models\Project $project
* @return void
*/
public function updated(Project $project)
{
if ($project->isDirty('archived_at')) {
$userids = $this->userids($project);
if ($project->archived_at) {
Deleted::record('project', $project->id, $userids);
} else {
Deleted::forget('project', $project->id, $userids);
}
}
}
/**
* Handle the Project "deleted" event.
*
* @param \App\Models\Project $project
* @return void
*/
public function deleted(Project $project)
{
Deleted::record('project', $project->id, $this->userids($project));
}
/**
* Handle the Project "restored" event.
*
* @param \App\Models\Project $project
* @return void
*/
public function restored(Project $project)
{
Deleted::forget('project', $project->id, $this->userids($project));
}
/**
* Handle the Project "force deleted" event.
*
* @param \App\Models\Project $project
* @return void
*/
public function forceDeleted(Project $project)
{
//
}
/**
* @param Project $project
* @return array
*/
private function userids(Project $project)
{
return ProjectUser::whereProjectId($project->id)->pluck('userid')->toArray();
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace App\Observers;
use App\Models\Deleted;
use App\Models\ProjectTask;
use App\Models\ProjectTaskUser;
class ProjectTaskObserver
{
/**
* Handle the ProjectTask "created" event.
*
* @param \App\Models\ProjectTask $projectTask
* @return void
*/
public function created(ProjectTask $projectTask)
{
//
}
/**
* Handle the ProjectTask "updated" event.
*
* @param \App\Models\ProjectTask $projectTask
* @return void
*/
public function updated(ProjectTask $projectTask)
{
if ($projectTask->isDirty('archived_at')) {
$userids = $this->userids($projectTask);
if ($projectTask->archived_at) {
Deleted::record('projectTask', $projectTask->id, $userids);
} else {
Deleted::forget('projectTask', $projectTask->id, $userids);
}
}
}
/**
* Handle the ProjectTask "deleted" event.
*
* @param \App\Models\ProjectTask $projectTask
* @return void
*/
public function deleted(ProjectTask $projectTask)
{
Deleted::record('projectTask', $projectTask->id, $this->userids($projectTask));
}
/**
* Handle the ProjectTask "restored" event.
*
* @param \App\Models\ProjectTask $projectTask
* @return void
*/
public function restored(ProjectTask $projectTask)
{
Deleted::forget('projectTask', $projectTask->id, $this->userids($projectTask));
}
/**
* Handle the ProjectTask "force deleted" event.
*
* @param \App\Models\ProjectTask $projectTask
* @return void
*/
public function forceDeleted(ProjectTask $projectTask)
{
//
}
/**
* @param ProjectTask $projectTask
* @return array
*/
private function userids(ProjectTask $projectTask)
{
return ProjectTaskUser::whereTaskId($projectTask->id)->pluck('userid')->toArray();
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Observers;
use App\Models\Deleted;
use App\Models\ProjectTaskUser;
use App\Models\ProjectUser;
class ProjectTaskUserObserver
{
/**
* Handle the ProjectTaskUser "created" event.
*
* @param \App\Models\ProjectTaskUser $projectTaskUser
* @return void
*/
public function created(ProjectTaskUser $projectTaskUser)
{
Deleted::forget('projectTask', $projectTaskUser->task_id, $projectTaskUser->userid);
}
/**
* Handle the ProjectTaskUser "updated" event.
*
* @param \App\Models\ProjectTaskUser $projectTaskUser
* @return void
*/
public function updated(ProjectTaskUser $projectTaskUser)
{
//
}
/**
* Handle the ProjectTaskUser "deleted" event.
*
* @param \App\Models\ProjectTaskUser $projectTaskUser
* @return void
*/
public function deleted(ProjectTaskUser $projectTaskUser)
{
if (!ProjectUser::whereProjectId($projectTaskUser->project_id)->whereUserid($projectTaskUser->userid)->exists()) {
Deleted::record('projectTask', $projectTaskUser->task_id, $projectTaskUser->userid);
}
}
/**
* Handle the ProjectTaskUser "restored" event.
*
* @param \App\Models\ProjectTaskUser $projectTaskUser
* @return void
*/
public function restored(ProjectTaskUser $projectTaskUser)
{
//
}
/**
* Handle the ProjectTaskUser "force deleted" event.
*
* @param \App\Models\ProjectTaskUser $projectTaskUser
* @return void
*/
public function forceDeleted(ProjectTaskUser $projectTaskUser)
{
//
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Observers;
use App\Models\Deleted;
use App\Models\ProjectUser;
class ProjectUserObserver
{
/**
* Handle the ProjectUser "created" event.
*
* @param \App\Models\ProjectUser $projectUser
* @return void
*/
public function created(ProjectUser $projectUser)
{
Deleted::forget('project', $projectUser->project_id, $projectUser->userid);
}
/**
* Handle the ProjectUser "updated" event.
*
* @param \App\Models\ProjectUser $projectUser
* @return void
*/
public function updated(ProjectUser $projectUser)
{
//
}
/**
* Handle the ProjectUser "deleted" event.
*
* @param \App\Models\ProjectUser $projectUser
* @return void
*/
public function deleted(ProjectUser $projectUser)
{
Deleted::record('project', $projectUser->project_id, $projectUser->userid);
}
/**
* Handle the ProjectUser "restored" event.
*
* @param \App\Models\ProjectUser $projectUser
* @return void
*/
public function restored(ProjectUser $projectUser)
{
//
}
/**
* Handle the ProjectUser "force deleted" event.
*
* @param \App\Models\ProjectUser $projectUser
* @return void
*/
public function forceDeleted(ProjectUser $projectUser)
{
//
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Observers;
use App\Models\Deleted;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogUser;
class WebSocketDialogObserver
{
/**
* Handle the WebSocketDialog "created" event.
*
* @param \App\Models\WebSocketDialog $webSocketDialog
* @return void
*/
public function created(WebSocketDialog $webSocketDialog)
{
//
}
/**
* Handle the WebSocketDialog "updated" event.
*
* @param \App\Models\WebSocketDialog $webSocketDialog
* @return void
*/
public function updated(WebSocketDialog $webSocketDialog)
{
//
}
/**
* Handle the WebSocketDialog "deleted" event.
*
* @param \App\Models\WebSocketDialog $webSocketDialog
* @return void
*/
public function deleted(WebSocketDialog $webSocketDialog)
{
Deleted::record('dialog', $webSocketDialog->id, $this->userids($webSocketDialog));
}
/**
* Handle the WebSocketDialog "restored" event.
*
* @param \App\Models\WebSocketDialog $webSocketDialog
* @return void
*/
public function restored(WebSocketDialog $webSocketDialog)
{
Deleted::forget('dialog', $webSocketDialog->id, $this->userids($webSocketDialog));
}
/**
* Handle the WebSocketDialog "force deleted" event.
*
* @param \App\Models\WebSocketDialog $webSocketDialog
* @return void
*/
public function forceDeleted(WebSocketDialog $webSocketDialog)
{
//
}
/**
* @param WebSocketDialog $webSocketDialog
* @return array
*/
private function userids(WebSocketDialog $webSocketDialog)
{
return WebSocketDialogUser::whereDialogId($webSocketDialog->id)->pluck('userid')->toArray();
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Observers;
use App\Models\Deleted;
use App\Models\WebSocketDialogUser;
class WebSocketDialogUserObserver
{
/**
* Handle the WebSocketDialogUser "created" event.
*
* @param \App\Models\WebSocketDialogUser $webSocketDialogUser
* @return void
*/
public function created(WebSocketDialogUser $webSocketDialogUser)
{
Deleted::forget('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
}
/**
* Handle the WebSocketDialogUser "updated" event.
*
* @param \App\Models\WebSocketDialogUser $webSocketDialogUser
* @return void
*/
public function updated(WebSocketDialogUser $webSocketDialogUser)
{
//
}
/**
* Handle the WebSocketDialogUser "deleted" event.
*
* @param \App\Models\WebSocketDialogUser $webSocketDialogUser
* @return void
*/
public function deleted(WebSocketDialogUser $webSocketDialogUser)
{
Deleted::record('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
}
/**
* Handle the WebSocketDialogUser "restored" event.
*
* @param \App\Models\WebSocketDialogUser $webSocketDialogUser
* @return void
*/
public function restored(WebSocketDialogUser $webSocketDialogUser)
{
//
}
/**
* Handle the WebSocketDialogUser "force deleted" event.
*
* @param \App\Models\WebSocketDialogUser $webSocketDialogUser
* @return void
*/
public function forceDeleted(WebSocketDialogUser $webSocketDialogUser)
{
//
}
}

View File

@@ -2,10 +2,21 @@
namespace App\Providers;
use App\Models\Project;
use App\Models\ProjectTask;
use App\Models\ProjectTaskUser;
use App\Models\ProjectUser;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogUser;
use App\Observers\ProjectObserver;
use App\Observers\ProjectTaskObserver;
use App\Observers\ProjectTaskUserObserver;
use App\Observers\ProjectUserObserver;
use App\Observers\WebSocketDialogObserver;
use App\Observers\WebSocketDialogUserObserver;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
class EventServiceProvider extends ServiceProvider
{
@@ -27,6 +38,11 @@ class EventServiceProvider extends ServiceProvider
*/
public function boot()
{
//
Project::observe(ProjectObserver::class);
ProjectTask::observe(ProjectTaskObserver::class);
ProjectTaskUser::observe(ProjectTaskUserObserver::class);
ProjectUser::observe(ProjectUserObserver::class);
WebSocketDialog::observe(WebSocketDialogObserver::class);
WebSocketDialogUser::observe(WebSocketDialogUserObserver::class);
}
}

View File

@@ -8,6 +8,7 @@ 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\Tasks\PushTask;
use Cache;
@@ -39,55 +40,45 @@ class WebSocketService implements WebSocketHandlerInterface
*/
public function onOpen(Server $server, Request $request)
{
global $_A;
$_A = [
'__static_langdata' => [],
];
$fd = $request->fd;
$data = Base::newTrim($request->get);
$action = $data['action'];
$get = Base::newTrim($request->get);
$action = $get['action'];
Cache::forget("User::encrypt:" . $fd);
switch ($action) {
/**
* 网页访问
*/
case 'web':
{
// 判断token参数
$token = $data['token'];
$cacheKey = "ws::token:" . md5($token);
$userid = Cache::remember($cacheKey, now()->addSeconds(1), function () use ($token) {
$authInfo = User::authFind('all', $token);
if ($authInfo['userid'] > 0) {
if (User::whereUserid($authInfo['userid'])->whereEmail($authInfo['email'])->whereEncrypt($authInfo['encrypt'])->exists()) {
return $authInfo['userid'];
}
}
return 0;
});
if (empty($userid)) {
Cache::forget($cacheKey);
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事件
$server->push($fd, Base::array2json([
'type' => 'open',
'data' => [
'fd' => $fd,
],
]));
// 通知上线
Task::deliver(new LineTask($user->userid, true));
// 推送离线时收到的消息
Task::deliver(new PushTask("RETRY::" . $user->userid));
} else {
// 用户不存在
$server->push($fd, Base::array2json([
'type' => 'error',
'data' => [
'error' => '会员不存在'
'error' => 'No member'
],
]));
$server->close($fd);
$this->deleteUser($fd);
return;
}
// 保存用户、发送open事件
$this->saveUser($fd, $userid);
$server->push($fd, Base::array2json([
'type' => 'open',
'data' => [
'fd' => $fd,
],
]));
// 通知上线
Task::deliver(new LineTask($userid, true));
// 推送离线时收到的消息
Task::deliver(new PushTask("RETRY::" . $userid));
}
break;
@@ -103,11 +94,6 @@ class WebSocketService implements WebSocketHandlerInterface
*/
public function onMessage(Server $server, Frame $frame)
{
global $_A;
$_A = [
'__static_langdata' => [],
];
//
$msg = Base::json2array($frame->data);
$type = $msg['type']; // 消息类型
$msgId = $msg['msgId']; // 消息ID用于回调
@@ -127,9 +113,11 @@ class WebSocketService implements WebSocketHandlerInterface
case 'readMsg':
$ids = is_array($data['id']) ? $data['id'] : [$data['id']];
$userid = $this->getUserid($frame->fd);
$list = WebSocketDialogMsg::whereIn('id', $ids)->get();
$list->transform(function(WebSocketDialogMsg $item) use ($userid) {
$item->readSuccess($userid);
WebSocketDialogMsg::whereIn('id', $ids)->chunkById(20, function($list) use ($userid) {
/** @var WebSocketDialogMsg $item */
foreach ($list as $item) {
$item->readSuccess($userid);
}
});
return;
@@ -143,14 +131,24 @@ class WebSocketService implements WebSocketHandlerInterface
$pathOld = $row->path;
$row->path = $pathNew;
$row->save();
if (preg_match("/^file\/content\/\d+$/", $pathOld)) {
if (preg_match("/^\/single\/file\/\d+$/", $pathOld)) {
$this->pushPath($pathOld);
}
if (preg_match("/^file\/content\/\d+$/", $pathNew)) {
if (preg_match("/^\/single\/file\/\d+$/", $pathNew)) {
$this->pushPath($pathNew);
}
}
return;
/**
* 加密参数
*/
case 'encrypt':
if ($data['type'] === 'pgp') {
$data['key'] = Doo::pgpPublicFormat($data['key']);
}
Cache::put("User::encrypt:" . $frame->fd, Base::array2json($data), Carbon::now()->addDay());
return;
}
//
if ($msgId) {
@@ -189,13 +187,15 @@ class WebSocketService implements WebSocketHandlerInterface
*/
private function saveUser($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,
]);
Cache::put("User::online:" . $userid, time(), Carbon::now()->addSeconds(30));
}
/**
@@ -204,7 +204,28 @@ class WebSocketService implements WebSocketHandlerInterface
*/
private function deleteUser($fd)
{
WebSocket::whereFd($fd)->delete();
Cache::forget("User::fd:" . $fd);
//
$array = [];
WebSocket::whereFd($fd)->chunk(10, function($list) use (&$array) {
/** @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);
}
if ($item->path && str_starts_with($item->path, "/single/file/")) {
$array[$item->path] = $item->path;
}
}
});
foreach ($array as $path) {
$this->pushPath($path);
}
}
/**

View File

@@ -1,6 +1,9 @@
<?php
namespace App\Tasks;
use App\Models\TaskWorker;
use App\Module\Base;
use Carbon\Carbon;
use Hhxsv5\LaravelS\Swoole\Task\Task;
/**
@@ -9,27 +12,18 @@ use Hhxsv5\LaravelS\Swoole\Task\Task;
*/
abstract class AbstractTask extends Task
{
protected $newTask = [];
protected int $twid = 0;
/**
* 添加完成后执行的任务
* @param $task
*/
final protected function addTask($task)
public function __construct(...$params)
{
$this->newTask[] = $task;
}
/**
* 包装执行过程
*/
final public function handle()
{
try {
$this->start();
} catch (\Exception $e) {
$this->info($e);
$this->failed($e);
$row = TaskWorker::createInstance([
'args' => [
'params' => $params,
'class' => get_class($this)
],
]);
if ($row->save()) {
$this->twid = $row->id;
}
}
@@ -41,31 +35,53 @@ abstract class AbstractTask extends Task
/**
* 任务完成事件
*/
public function finish()
abstract public function end();
/**
* 重写执行过程
*/
final public function handle()
{
foreach ($this->newTask AS $task) {
Task::deliver($task);
TaskWorker::whereId($this->twid)->update(['start_at' => Carbon::now()]);
//
try {
$this->start();
} catch (\Throwable $e) {
$this->failed("start", $e);
}
}
/**
* 重写完成事件
*/
final public function finish()
{
TaskWorker::whereId($this->twid)->update(['end_at' => Carbon::now()]);
//
try {
$this->end();
TaskWorker::whereId($this->twid)->delete();
} catch (\Throwable $e) {
$this->failed("end", $e);
}
}
/**
* 任务失败事件
* @param $e
* @param string $type
* @param \Throwable $e
*/
public function failed($e)
public function failed(string $type, \Throwable $e)
{
info($type);
info($e);
//
}
/**
* 添加日志
* @param $var
*/
private function info($var)
{
if (!config('app.debug') || defined('DO_NOT_ADD_LOGS')) {
return;
}
info($var);
TaskWorker::whereId($this->twid)->update(['error' => Base::array2json([
'time' => Carbon::now(),
'type' => $type,
'code' => $e->getCode(),
'file' => $e->getFile(),
'message' => $e->getMessage(),
])]);
}
}

52
app/Tasks/AppPushTask.php Normal file
View File

@@ -0,0 +1,52 @@
<?php
namespace App\Tasks;
use App\Models\ProjectTask;
use App\Module\Base;
use Carbon\Carbon;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
class AppPushTask extends AbstractTask
{
public function __construct()
{
parent::__construct();
}
public function start()
{
$setting = Base::setting('appPushSetting');
if ($setting['push'] !== 'open') {
return;
}
ProjectTask::whereNull("complete_at")
->whereNull("archived_at")
->whereBetween("end_at", [
Carbon::now()->addMinutes(60),
Carbon::now()->addMinutes(60 + 10)
])->chunkById(100, function ($tasks) {
/** @var ProjectTask $task */
foreach ($tasks as $task) {
$task->taskPush(null, 1);
}
});
ProjectTask::whereNull("complete_at")
->whereNull("archived_at")
->whereBetween("end_at", [
Carbon::now()->subMinutes(60 + 10),
Carbon::now()->subMinutes(60)
])->chunkById(100, function ($tasks) {
/** @var ProjectTask $task */
foreach ($tasks as $task) {
$task->taskPush(null, 2);
}
});
}
public function end()
{
}
}

View File

@@ -18,7 +18,7 @@ class AutoArchivedTask extends AbstractTask
public function __construct()
{
//
parent::__construct();
}
public function start()
@@ -42,4 +42,9 @@ class AutoArchivedTask extends AbstractTask
}
}
}
public function end()
{
}
}

View File

@@ -0,0 +1,386 @@
<?php
namespace App\Tasks;
use App\Models\User;
use App\Models\UserBot;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Module\Base;
use App\Module\Doo;
use App\Module\Ihttp;
use Carbon\Carbon;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
/**
* 推送会话消息
* Class BotReceiveMsgTask
* @package App\Tasks
*/
class BotReceiveMsgTask extends AbstractTask
{
protected $userid;
protected $msgId;
public function __construct($userid, $msgId)
{
parent::__construct(...func_get_args());
$this->userid = $userid;
$this->msgId = $msgId;
}
public function start()
{
$botUser = User::whereUserid($this->userid)->whereBot(1)->first();
if (empty($botUser)) {
return;
}
$msg = WebSocketDialogMsg::find($this->msgId);
if (empty($msg)) {
return;
}
$msg->readSuccess($botUser->userid);
//
$dialog = WebSocketDialog::find($msg->dialog_id);
if (empty($dialog)) {
return;
}
if ($dialog->type !== 'user') {
return;
}
$this->botManagerReceive($msg, $botUser);
}
public function end()
{
}
/**
* 机器人管理处理消息
* @param WebSocketDialogMsg $msg
* @param User $botUser
* @return void
*/
private function botManagerReceive(WebSocketDialogMsg $msg, User $botUser)
{
if ($msg->type !== 'text') {
return;
}
$original = $msg->msg['text'];
if (preg_match("/<span[^>]*?data-quick-key=([\"'])(.*?)\\1[^>]*?>(.*?)<\/span>/is", $original, $match)) {
$command = $match[2];
} else {
$command = trim(strip_tags($original));
}
// 签到机器人
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事件来发送任务
}
}
// 隐私机器人
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事件来发送任务
}
}
// 管理机器人
if (str_starts_with($command, '/')) {
if ($botUser->email === 'bot-manager@bot.system') {
$isManager = true;
} 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事件来发送任务
return;
}
//
$array = Base::newTrim(explode(" ", "{$command} "));
$type = $array[0];
$data = [];
$notice = "";
if (!$isManager && in_array($type, ['/list', '/newbot'])) {
return; // 这些操作仅支持【机器人管理】机器人
}
switch ($type) {
/**
* 列表
*/
case '/list':
$data = User::select([
'users.*',
'user_bots.clear_day',
'user_bots.clear_at',
'user_bots.webhook_url',
'user_bots.webhook_num'
])
->join('user_bots', 'users.userid', '=', 'user_bots.bot_id')
->where('users.bot', 1)
->where('user_bots.userid', $msg->userid)
->take(50)
->orderByDesc('id')
->get();
if ($data->isEmpty()) {
$type = "notice";
$notice = "您没有创建机器人。";
}
break;
/**
* 详情
*/
case '/info':
$botId = $isManager ? $array[1] : $botUser->userid;
$data = $this->botManagerOne($botId, $msg->userid);
if (!$data) {
$type = "notice";
$notice = "机器人不存在。";
}
break;
/**
* 创建
*/
case '/newbot':
if (User::select(['users.*'])
->join('user_bots', 'users.userid', '=', 'user_bots.bot_id')
->where('users.bot', 1)
->where('user_bots.userid', $msg->userid)
->count() >= 50) {
$type = "notice";
$notice = "超过最大创建数量。";
break;
}
if (strlen($array[1]) < 2 || strlen($array[1]) > 20) {
$type = "notice";
$notice = "机器人名称由2-20个字符组成。";
break;
}
$data = User::botGetOrCreate("user-" . Base::generatePassword(), [
'nickname' => $array[1]
], $msg->userid);
if (empty($data)) {
$type = "notice";
$notice = "创建失败。";
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事件来发送任务
}
break;
/**
* 修改名字
*/
case '/setname':
$botId = $isManager ? $array[1] : $botUser->userid;
$nameString = $isManager ? $array[2] : $array[1];
if (strlen($nameString) < 2 || strlen($nameString) > 20) {
$type = "notice";
$notice = "机器人名称由2-20个字符组成。";
break;
}
$data = $this->botManagerOne($botId, $msg->userid);
if ($data) {
$data->nickname = $nameString;
$data->az = Base::getFirstCharter($nameString);
$data->pinyin = Base::cn2pinyin($nameString);
$data->save();
} else {
$type = "notice";
$notice = "机器人不存在。";
}
break;
/**
* 删除
*/
case '/deletebot':
$botId = $isManager ? $array[1] : $botUser->userid;
$data = $this->botManagerOne($botId, $msg->userid);
if ($data) {
$data->deleteUser('delete bot');
} else {
$type = "notice";
$notice = "机器人不存在。";
}
break;
/**
* 获取Token
*/
case '/token':
$botId = $isManager ? $array[1] : $botUser->userid;
$data = $this->botManagerOne($botId, $msg->userid);
if ($data) {
User::generateToken($data);
} else {
$type = "notice";
$notice = "机器人不存在。";
}
break;
/**
* 更新Token
*/
case '/revoke':
$botId = $isManager ? $array[1] : $botUser->userid;
$data = $this->botManagerOne($botId, $msg->userid);
if ($data) {
$data->encrypt = Base::generatePassword(6);
$data->password = Doo::md5s(Base::generatePassword(32), $data->encrypt);
$data->save();
} else {
$type = "notice";
$notice = "机器人不存在。";
}
break;
/**
* 设置保留消息时间
*/
case '/clearday':
$botId = $isManager ? $array[1] : $botUser->userid;
$clearDay = $isManager ? $array[2] : $array[1];
$data = $this->botManagerOne($botId, $msg->userid);
if ($data) {
$userBot = UserBot::whereBotId($botId)->whereUserid($msg->userid)->first();
if ($userBot) {
$userBot->clear_day = min(intval($clearDay) ?: 30, 999);
$userBot->clear_at = Carbon::now()->addDays($userBot->clear_day);
$userBot->save();
}
$data->clear_day = $userBot->clear_day;
$data->clear_at = $userBot->clear_at; // 这两个参数只是作为输出,所以不保存
} else {
$type = "notice";
$notice = "机器人不存在。";
}
break;
/**
* 设置webhook
*/
case '/webhook':
$botId = $isManager ? $array[1] : $botUser->userid;
$webhookUrl = $isManager ? $array[2] : $array[1];
$data = $this->botManagerOne($botId, $msg->userid);
if (strlen($webhookUrl) > 255) {
$type = "notice";
$notice = "webhook地址最长仅支持255个字符。";
} elseif ($data) {
$userBot = UserBot::whereBotId($botId)->whereUserid($msg->userid)->first();
if ($userBot) {
$userBot->webhook_url = $webhookUrl ?: "";
$userBot->webhook_num = 0;
$userBot->save();
}
$data->webhook_url = $userBot->webhook_url ?: '-';
$data->webhook_num = $userBot->webhook_num; // 这两个参数只是作为输出,所以不保存
} else {
$type = "notice";
$notice = "机器人不存在。";
}
break;
/**
* 会话搜索
*/
case '/dialog':
$botId = $isManager ? $array[1] : $botUser->userid;
$nameKey = $isManager ? $array[2] : $array[1];
$data = $this->botManagerOne($botId, $msg->userid);
if ($data) {
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread', 'u.silence', '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}%")
->where('u.userid', $data->userid)
->orderByDesc('u.top_at')
->orderByDesc('web_socket_dialogs.last_at')
->take(20)
->get();
if ($list->isEmpty()) {
$type = "notice";
$notice = "没有搜索到相关会话。";
} else {
$list->transform(function (WebSocketDialog $item) use ($data) {
return $item->formatData($data->userid);
});
$data->list = $list; // 这个参数只是作为输出,所以不保存
}
} else {
$type = "notice";
$notice = "机器人不存在。";
}
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);
}
$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事件来发送任务
return;
}
// 推送Webhook
if ($command) {
$userBot = UserBot::whereBotId($botUser->userid)->first();
if ($userBot && preg_match("/^https*:\/\//", $userBot->webhook_url)) {
Ihttp::ihttp_post($userBot->webhook_url, [
'text' => $command,
'token' => User::generateToken($botUser),
'dialog_id' => $msg->dialog_id,
'msg_id' => $msg->id,
'msg_uid' => $msg->userid,
'bot_uid' => $botUser->userid,
'version' => Base::getVersion(),
], 10);
$userBot->webhook_num++;
$userBot->save();
}
}
}
/**
* @param $botId
* @param $userid
* @return User
*/
private function botManagerOne($botId, $userid)
{
$botId = intval($botId);
$userid = intval($userid);
if ($botId > 0) {
return User::select([
'users.*',
'user_bots.clear_day',
'user_bots.clear_at',
'user_bots.webhook_url',
'user_bots.webhook_num'
])
->join('user_bots', 'users.userid', '=', 'user_bots.bot_id')
->where('users.bot', 1)
->where('user_bots.bot_id', $botId)
->where('user_bots.userid', $userid)
->first();
}
return null;
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Tasks;
use App\Models\User;
use App\Models\UserCheckinRecord;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Module\Base;
use App\Module\Extranet;
use Cache;
use Carbon\Carbon;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
class CheckinRemindTask extends AbstractTask
{
public function __construct()
{
parent::__construct();
}
public function start()
{
$setting = Base::setting('checkinSetting');
if ($setting['open'] !== 'open') {
return;
}
// 判断非工作日
if (Extranet::isHoliday(date("Ymd")) > 0) {
return;
}
//
$times = $setting['time'] ? Base::json2array($setting['time']) : ['09:00', '18:00'];
$remindin = (intval($setting['remindin']) ?: 5) * 60;
$remindexceed = (intval($setting['remindexceed']) ?: 10) * 60;
//
$nowDate = date("Y-m-d");
$timeStart = strtotime("{$nowDate} {$times[0]}");
//
if ($remindin > 0) {
$timeRemindin = $timeStart - $remindin;
if ($timeRemindin <= Base::time() && Base::time() <= $timeStart) {
// 签到打卡提醒
$this->remind('in');
}
}
if ($remindexceed > 0) {
$timeRemindexceed = $timeStart + $remindexceed;
if ($timeRemindexceed <= Base::time() && Base::time() <= $timeRemindexceed + 300) {
// 签到缺卡提醒
$this->remind('exceed');
}
}
}
public function end()
{
}
private function remind($type)
{
if (Cache::get("CheckinRemindTask:remind-" . $type) == date("Ymd")) {
return;
}
Cache::put("CheckinRemindTask:remind-" . $type, date("Ymd"), Carbon::now()->addDay());
//
$botUser = User::botGetOrCreate('check-in');
if (!$botUser) {
return;
}
// 提醒对象3天内有签到的成员在职
User::whereBot(0)->whereNull('disable_at')->chunk(100, function ($users) use ($type, $botUser) {
/** @var User $user */
foreach ($users as $user) {
if (UserCheckinRecord::whereUserid($user->userid)->where('date', date("Y-m-d"))->exists()) {
continue; // 已打卡
}
if (!UserCheckinRecord::whereUserid($user->userid)->where('created_at', '>', Carbon::now()->subDays(3))->exists()) {
continue; // 3天内没有打卡
}
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
if ($dialog) {
if ($type === 'exceed') {
$text = "<p><strong style='color:red'>缺卡提醒:</strong>上班时间到了,你还没有打卡哦~</p>";
} else {
$text = "<p><strong>打卡提醒:</strong>快到上班时间了,别忘了打卡哦~</p>";
}
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid);
}
}
});
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Tasks;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Models\UserBot;
use App\Models\WebSocketDialogMsg;
use Carbon\Carbon;
/**
* 删除机器人消息
*/
class DeleteBotMsgTask extends AbstractTask
{
public function __construct()
{
parent::__construct();
}
public function start()
{
$bots = UserBot::where('clear_at', '<=', Carbon::now())->take(100)->get();
foreach ($bots as $bot) {
WebSocketDialogMsg::whereUserid($bot->bot_id)
->where('created_at', '<=', Carbon::now()->subDays($bot->clear_day))
->orderBy('id')
->chunk(1000, function ($msgs) {
$ids = $msgs->pluck('id')->toArray();
if ($ids) {
WebSocketDialogMsg::deleteMsgs($ids);
}
});
$bot->clear_at = Carbon::now()->addDays($bot->clear_day);
$bot->save();
}
}
public function end()
{
}
}

View File

@@ -2,6 +2,8 @@
namespace App\Tasks;
use App\Models\File;
use App\Models\TaskWorker;
use App\Models\Tmp;
use App\Models\WebSocketTmpMsg;
use Carbon\Carbon;
@@ -16,8 +18,13 @@ class DeleteTmpTask extends AbstractTask
protected $data;
protected $hours; // 多久后删除,单位小时
public function __construct(string $data, int $hours)
/**
* @param string $data
* @param int $hours
*/
public function __construct(string $data, int $hours = 24)
{
parent::__construct(...func_get_args());
$this->data = $data;
$this->hours = $hours;
}
@@ -26,13 +33,14 @@ class DeleteTmpTask extends AbstractTask
{
switch ($this->data) {
/**
* 表pre_wg_tmp_msgs
* 表pre_tmp_msgs
*/
case 'wg_tmp_msgs':
{
WebSocketTmpMsg::where('created_at', '<', Carbon::now()->subHours($this->hours)->toDateTimeString())
WebSocketTmpMsg::where('created_at', '<', Carbon::now()->subHours($this->hours))
->orderBy('id')
->chunk(500, function ($msgs) {
/** @var WebSocketTmpMsg $msg */
foreach ($msgs as $msg) {
$msg->delete();
}
@@ -41,19 +49,58 @@ class DeleteTmpTask extends AbstractTask
break;
/**
* 表pre_wg_tmp
* 表pre_tmp
*/
case 'tmp':
{
Tmp::where('created_at', '<', Carbon::now()->subHours($this->hours)->toDateTimeString())
Tmp::where('created_at', '<', Carbon::now()->subHours($this->hours))
->orderBy('id')
->chunk(2000, function ($tmps) {
->chunk(500, function ($tmps) {
/** @var Tmp $tmp */
foreach ($tmps as $tmp) {
$tmp->delete();
}
});
}
break;
/**
* 表pre_task_worker
*/
case 'task_worker':
{
TaskWorker::onlyTrashed()
->where('deleted_at', '<', Carbon::now()->subHours($this->hours))
->orderBy('id')
->forceDelete();
}
break;
/**
* 表pre_file
*/
case 'file':
{
$day = intval(env("AUTO_EMPTY_FILE_RECYCLE", 365));
if ($day <= 0) {
return;
}
File::onlyTrashed()
->where('deleted_at', '<', Carbon::now()->subHours($day))
->orderBy('id')
->chunk(500, function ($files) {
/** @var File $file */
foreach ($files as $file) {
$file->forceDeleteFile();
}
});
}
break;
}
}
public function end()
{
}
}

View File

@@ -0,0 +1,170 @@
<?php
namespace App\Tasks;
use App\Models\Setting;
use App\Models\User;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogMsgRead;
use App\Module\Base;
use Carbon\Carbon;
use Guanguans\Notify\Factory;
use Guanguans\Notify\Messages\EmailMessage;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
class EmailNoticeTask extends AbstractTask
{
public function __construct()
{
parent::__construct();
}
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");
});
}
}
}
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

@@ -18,6 +18,7 @@ class IhttpTask extends AbstractTask
protected $extra;
protected $apiWebsocket;
protected $apiUserid;
protected $endPush = [];
/**
* IhttpTask constructor.
@@ -27,6 +28,7 @@ class IhttpTask extends AbstractTask
*/
public function __construct($url, $post = [], $extra = [])
{
parent::__construct(...func_get_args());
$this->url = $url;
$this->post = $post;
$this->extra = $extra;
@@ -53,7 +55,7 @@ class IhttpTask extends AbstractTask
$res = Ihttp::ihttp_request($this->url, $this->post, $this->extra);
if ($this->apiWebsocket && $this->apiUserid) {
$data = Base::isSuccess($res) ? Base::json2array($res['data']) : $res;
PushTask::push([
$this->endPush[] = [
'userid' => $this->apiUserid,
'msg' => [
'type' => 'apiWebsocket',
@@ -61,7 +63,13 @@ class IhttpTask extends AbstractTask
'apiSuccess' => Base::isSuccess($res),
'data' => $data,
]
]);
];
}
}
public function end()
{
PushTask::push($this->endPush);
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Tasks;
use App\Module\Base;
use App\Module\Extranet;
use Cache;
use Carbon\Carbon;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
/**
* 获取笑话、心灵鸡汤
*
* 在.env添加笑话 JUKE_KEY_JOKE
* 在.env添加鸡汤 JUKE_KEY_SOUP
*
* 每日小时采集1次
*/
class JokeSoupTask extends AbstractTask
{
public function __construct()
{
parent::__construct();
}
public function start()
{
// 判断每小时执行一次
if (Cache::get("JokeSoupTask:YmdH") == date("YmdH")) {
return;
}
Cache::put("JokeSoupTask:YmdH", date("YmdH"), Carbon::now()->addDay());
//
$array = Base::json2array(Cache::get("JokeSoupTask:jokes"));
$data = Extranet::randJoke();
foreach ($data as $item) {
if ($text = trim($item['content'])) {
$array[] = $text;
}
}
Cache::forever("JokeSoupTask:jokes", Base::array2json(array_slice($array, -100)));
//
$array = Base::json2array(Cache::get("JokeSoupTask:soups"));
$data = Extranet::soups();
if ($data) {
$array[] = $data;
}
Cache::forever("JokeSoupTask:soups", Base::array2json(array_slice($array, -24)));
}
public function end()
{
}
}

View File

@@ -16,6 +16,7 @@ class LineTask extends AbstractTask
{
protected $userid;
protected $online;
protected $endPush = [];
/**
* LineTask constructor.
@@ -24,6 +25,7 @@ class LineTask extends AbstractTask
*/
public function __construct($userid, bool $online)
{
parent::__construct(...func_get_args());
$this->userid = $userid;
$this->online = $online;
}
@@ -36,7 +38,7 @@ class LineTask extends AbstractTask
$fd[] = $ws->fd;
}
if ($fd) {
PushTask::push([
$this->endPush[] = [
'fd' => $fd,
'msg' => [
'type' => 'line',
@@ -45,8 +47,13 @@ class LineTask extends AbstractTask
'online' => $this->online,
],
]
]);
];
}
});
}
public function end()
{
PushTask::push($this->endPush);
}
}

84
app/Tasks/LoopTask.php Normal file
View File

@@ -0,0 +1,84 @@
<?php
namespace App\Tasks;
use App\Models\ProjectFlow;
use App\Models\ProjectFlowItem;
use App\Models\ProjectTask;
use App\Models\ProjectTaskUser;
use Carbon\Carbon;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
/**
* 任务重复周期
*/
class LoopTask extends AbstractTask
{
public function __construct()
{
parent::__construct();
}
public function start()
{
ProjectTask::whereBetween('loop_at', [
Carbon::now()->subMinutes(10),
Carbon::now()
])->chunkById(100, function ($list) {
/** @var ProjectTask $item */
foreach ($list as $item) {
try {
$task = $item->copyTask();
// 工作流
$projectFlow = ProjectFlow::whereProjectId($task->project_id)->orderByDesc('id')->first();
if ($projectFlow) {
$projectFlowItem = ProjectFlowItem::whereFlowId($projectFlow->id)->orderBy('sort')->get();
// 赋一个开始状态
foreach ($projectFlowItem as $flowItem) {
if ($flowItem->status == 'start') {
$task->flow_item_id = $flowItem->id;
$task->flow_item_name = $flowItem->status . "|" . $flowItem->name;
if ($flowItem->userids) {
$userids = array_values(array_unique($flowItem->userids));
foreach ($userids as $uid) {
ProjectTaskUser::updateInsert([
'task_id' => $task->id,
'userid' => $uid,
], [
'project_id' => $task->project_id,
'task_pid' => $task->id,
'owner' => 1,
]);
}
}
break;
}
}
}
// 新任务时间、周期
if ($task->start_at) {
$diffSecond = Carbon::parse($task->start_at)->diffInSeconds(Carbon::parse($task->end_at), true);
$task->start_at = Carbon::parse($task->loop_at);
$task->end_at = $task->start_at->clone()->addSeconds($diffSecond);
}
$task->refreshLoop(true);
$task->addLog("创建任务来自周期任务ID{$item->id}", [], $task->userid);
// 清空旧周期
$item->loop = '';
$item->loop_at = null;
$item->save();
$item->addLog("已创建新的周期任务ID{$task->id},此任务关闭周期", [], $task->userid);
} catch (\Throwable $e) {
$item->addLog("生成重复任务失败:" . $e->getMessage(), [], $item->userid);
}
}
});
}
public function end()
{
}
}

View File

@@ -6,6 +6,7 @@ namespace App\Tasks;
use App\Models\WebSocket;
use App\Models\WebSocketTmpMsg;
use App\Module\Base;
use App\Module\Doo;
use Cache;
use Carbon\Carbon;
use Hhxsv5\LaravelS\Swoole\Task\Task;
@@ -19,6 +20,7 @@ class PushTask extends AbstractTask
{
protected $params;
protected $retryOffline = true;
protected $endPush = [];
/**
* PushTask constructor.
@@ -26,6 +28,7 @@ class PushTask extends AbstractTask
*/
public function __construct($params = [], $retryOffline = true)
{
parent::__construct(...func_get_args());
$this->params = $params;
$this->retryOffline = $retryOffline;
}
@@ -42,12 +45,41 @@ class PushTask extends AbstractTask
}
// 根据会员ID推送离线时收到的消息
elseif (Base::leftExists($this->params, "RETRY::")) {
self::sendTmpMsgForUserid(intval(Base::leftDelete($this->params, "RETRY::")));
$this->sendTmpMsgForUserid(intval(Base::leftDelete($this->params, "RETRY::")));
}
}
is_array($this->params) && self::push($this->params, $this->retryOffline);
}
public function end()
{
self::push($this->endPush);
}
/**
* 根据会员ID推送离线时收到的消息
* @param $userid
*/
private function sendTmpMsgForUserid($userid)
{
if (empty($userid)) {
return;
}
WebSocketTmpMsg::whereCreateId($userid)
->whereSend(0)
->where('created_at', '>', Carbon::now()->subMinute()) // 1分钟内添加的数据
->orderBy('id')
->chunk(100, function($list) use ($userid) {
foreach ($list as $item) {
$this->endPush[] = [
'tmpMsgId' => $item->id,
'userid' => $userid,
'msg' => Base::json2array($item->msg),
];
}
});
}
/**
* 记录离线消息,等上线后重新发送
* @param array $userFail
@@ -71,30 +103,6 @@ class PushTask extends AbstractTask
}
}
/**
* 根据会员ID推送离线时收到的消息
* @param $userid
*/
private static function sendTmpMsgForUserid($userid)
{
if (empty($userid)) {
return;
}
WebSocketTmpMsg::whereCreateId($userid)
->whereSend(0)
->where('created_at', '>', Carbon::now()->subMinute()) // 1分钟内添加的数据
->orderBy('id')
->chunk(100, function($list) use ($userid) {
foreach ($list as $item) {
self::push([
'tmpMsgId' => $item->id,
'userid' => $userid,
'msg' => Base::json2array($item->msg),
]);
}
});
}
/**
* 推送消息
* @param array $lists 消息列表
@@ -104,7 +112,7 @@ class PushTask extends AbstractTask
*/
public static function push(array $lists, $retryOffline = true, $key = null, $delay = 1)
{
if (!is_array($lists) || empty($lists)) {
if (empty($lists)) {
return;
}
if (!Base::isTwoArray($lists)) {
@@ -170,10 +178,12 @@ class PushTask extends AbstractTask
Task::deliver($task);
} else {
try {
$swoole->push($fid, Base::array2json($msg));
$tmpMsgId > 0 && WebSocketTmpMsg::whereId($tmpMsgId)->update(['send' => 1]);
} catch (\Exception $e) {
$swoole->push($fid, self::pushMsgFormat($fid, $msg));
if ($tmpMsgId > 0) {
WebSocketTmpMsg::whereId($tmpMsgId)->update(['send' => 1]);
}
} catch (\Throwable) {
// 发送失败
}
}
}
@@ -184,4 +194,25 @@ class PushTask extends AbstractTask
}
}
}
/**
* 格式化推送消息
* @param $fid
* @param $msg
* @return string
*/
private static function pushMsgFormat($fid, $msg)
{
$encrypt = Base::json2array(Cache::get("User::encrypt:" . $fid));
if ($encrypt['type'] == 'pgp') {
if (is_array($msg) && $msg['type'] == 'dialog') {
// 仅加密对话消息
$msg = [
'type' => 'encrypt',
'encrypted' => Doo::pgpEncryptApi($msg, $encrypt['key']),
];
}
}
return Base::array2json($msg);
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Tasks;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Models\UmengAlias;
use App\Module\Base;
/**
* 推送友盟消息
*/
class PushUmengMsg extends AbstractTask
{
protected $userid = 0;
protected $array = [];
/**
* @param array|int $userid
* @param array $array
*/
public function __construct($userid, $array = [])
{
parent::__construct(...func_get_args());
$this->userid = $userid;
$this->array = is_array($array) ? $array : [];
}
public function start()
{
if (empty($this->userid) || empty($this->array)) {
return;
}
$setting = Base::setting('appPushSetting');
if ($setting['push'] !== 'open') {
return;
}
UmengAlias::pushMsgToUserid($this->userid, $this->array);
}
public function end()
{
}
}

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