Compare commits

...

1145 Commits

Author SHA1 Message Date
kuaifan
40ff85304b no message 2022-05-20 18:18:59 +08:00
kuaifan
02de3a1680 no message 2022-05-20 15:55:54 +08:00
kuaifan
7a8fcad27c perf: 聊天输入框支持粘贴文件 2022-05-20 15:23:10 +08:00
kuaifan
35daeb443c no message 2022-05-20 12:00:14 +08:00
kuaifan
22993782ed build 2022-05-20 11:13:02 +08:00
kuaifan
4c2b88a13f no message 2022-05-20 11:05:02 +08:00
kuaifan
7a168977da perf: 优化UserAvatar组件 2022-05-20 10:23:24 +08:00
kuaifan
2e03cee726 perf: 上传或发送图片太大时压缩显示 2022-05-20 10:22:38 +08:00
kuaifan
74b1194398 perf: 仪表盘任务数量、最近打开的任务 2022-05-20 10:22:08 +08:00
kuaifan
88163499fe perf: 优化消息移动端打开动画效果 2022-05-19 12:18:23 +08:00
kuaifan
c0ead0b891 更新onlyoffice版本 2022-05-19 11:35:27 +08:00
kuaifan
6b6cb7473e 更新onlyoffice样式 2022-05-19 11:27:40 +08:00
kuaifan
e0f41f9330 no message 2022-05-18 17:17:17 +08:00
kuaifan
95dcfc961f no message 2022-05-18 16:57:40 +08:00
kuaifan
0178f4b8b5 添加隐私政策 2022-05-18 15:35:29 +08:00
kuaifan
85c4eae2a8 no message 2022-05-18 15:08:28 +08:00
kuaifan
168e5cea45 no message 2022-05-18 13:40:53 +08:00
kuaifan
0f5e393340 no message 2022-05-18 13:39:28 +08:00
kuaifan
41faf72c37 no message 2022-05-18 13:38:25 +08:00
kuaifan
6cf2279d21 no message 2022-05-17 19:57:48 +08:00
kuaifan
6347454098 no message 2022-05-17 19:55:47 +08:00
kuaifan
3f17c0689c perf: 未读消息邮件提醒,提醒时把所有未读消息都加上,而不是只提示指定时间的 2022-05-17 19:30:20 +08:00
kuaifan
e4524ff2c5 fix: 未读消息邮件头像不显示的问题 2022-05-17 18:45:59 +08:00
kuaifan
94e4054cd4 no message 2022-05-17 18:27:20 +08:00
kuaifan
a4a7939eae perf: 优化modal内滚动会传播给其他组件的问题 2022-05-17 18:26:17 +08:00
kuaifan
63fa9eb9e4 perf: 优化任务过多加载卡的情况 2022-05-17 15:43:20 +08:00
kuaifan
9e449fa4ee no message 2022-05-17 14:57:48 +08:00
kuaifan
ca7cd2372a perf: 点击聊天输入框窗口跳动 2022-05-17 14:57:18 +08:00
kuaifan
f4502a5e9d no message 2022-05-17 13:44:14 +08:00
kuaifan
040f1e8c86 移动端取消Tooltip提示 2022-05-17 13:44:07 +08:00
kuaifan
a9c10a3e0b 优化移动客户端登录逻辑 2022-05-17 11:52:34 +08:00
kuaifan
8522b4610d 群聊每条信息显示发送人位置给默认高度 2022-05-17 11:52:01 +08:00
kuaifan
f9d7d4faec build 2022-05-16 20:21:22 +08:00
kuaifan
bc4477cd86 no message 2022-05-16 19:30:51 +08:00
kuaifan
4d6fb6f2ca no message 2022-05-16 17:16:38 +08:00
kuaifan
dafa00c895 build 2022-05-16 14:19:21 +08:00
kuaifan
e8dc71c71f no message 2022-05-16 14:01:06 +08:00
kuaifan
3755402f37 build 2022-05-15 23:02:13 +08:00
kuaifan
8d876c60b2 fix: 修复手机客户端无法预览文件的问题 2022-05-15 22:58:35 +08:00
kuaifan
157e89b5ea perf: 支持上传plist格式文件 2022-05-15 22:58:14 +08:00
kuaifan
5fcc4c730f no message 2022-05-15 22:46:56 +08:00
kuaifan
6e8ee60df0 perf: DrawerOverlay 使用 Model 2022-05-15 22:46:45 +08:00
kuaifan
6610a948c5 no message 2022-05-15 18:20:43 +08:00
kuaifan
5ff1f57017 fix: 客户端选择sso登录输入相同地址时提交无反应的问题 2022-05-15 18:13:19 +08:00
kuaifan
8a870a361d perf: 手机客户端登录页优化sso登录样式 2022-05-15 18:12:42 +08:00
kuaifan
72e0f9b798 perf: 优化手机客户端登录页切换主题提示 2022-05-15 18:11:52 +08:00
kuaifan
04bc5d7d17 fix: 推送收到的群组名称为空的情况 2022-05-14 22:14:05 +08:00
kuaifan
3e19f3991c build 2022-05-13 22:48:45 +08:00
kuaifan
5e324c69b6 no message 2022-05-13 20:38:47 +08:00
kuaifan
bba34ef55e no message 2022-05-13 19:35:26 +08:00
kuaifan
99f9431794 catch Exception 改为 Throwable 2022-05-13 15:58:48 +08:00
kuaifan
279f05fd11 fix: 任务开始邮件提醒错误的问题 2022-05-13 15:57:28 +08:00
kuaifan
8f58797256 no message 2022-05-13 15:25:29 +08:00
kuaifan
6dbbcff0b6 no message 2022-05-12 17:51:33 +08:00
kuaifan
37578cf4a0 no message 2022-05-12 15:10:28 +08:00
kuaifan
a13bc3f0bd perf: 优化消息列表 2022-05-12 15:10:21 +08:00
kuaifan
1d4602db0b 优化移动端滑动返回 2022-05-12 15:08:58 +08:00
kuaifan
ece17b2aee fix: ios键盘遮挡输入框的问题 2022-05-12 08:31:51 +08:00
kuaifan
117fd62c44 优化移动端 2022-05-12 00:15:09 +08:00
kuaifan
4da73dd144 优化移动端 2022-05-11 19:14:01 +08:00
kuaifan
05df135229 no message 2022-05-11 09:59:32 +08:00
kuaifan
8e2a649d57 优化移动端 2022-05-10 18:56:43 +08:00
kuaifan
d2428e43b2 perf: 优化移动端 2022-05-09 08:33:09 +08:00
kuaifan
d03a6f7b00 no message 2022-05-06 16:34:48 +08:00
kuaifan
c1b218c133 no message 2022-05-06 16:32:44 +08:00
kuaifan
b3286c8f74 no message 2022-05-06 12:14:11 +08:00
kuaifan
44b46c396b no message 2022-05-06 07:41:11 +08:00
kuaifan
042fb28ccc no message 2022-05-05 22:18:06 +08:00
kuaifan
2206e0f1e6 no message 2022-05-05 22:15:23 +08:00
kuaifan
632f68660b feat: 邮件通知未读消息 2022-05-05 19:24:17 +08:00
kuaifan
43531d9b8d no message 2022-05-05 08:20:04 +08:00
kuaifan
d28fb9a27a no message 2022-05-04 18:16:03 +08:00
kuaifan
c0fadde527 perf: 优化聊天输入框计算样式 2022-04-29 16:06:55 +08:00
kuaifan
663661c73c perf: 优化正则表达式 2022-04-29 16:06:38 +08:00
kuaifan
4304b66b06 Merge remote-tracking branch 'A_github_pro/master' 2022-04-28 14:52:29 +08:00
kuaifan
99b789bc11 增加php backtrack_limit,解决聊天粘贴大图发送失败的问题 2022-04-28 14:52:10 +08:00
kuaifan
4bbd20b8c0 build 2022-04-28 08:50:27 +08:00
kuaifan
43c77726bb no message 2022-04-28 07:46:32 +08:00
kuaifan
3ceb183d8b no message 2022-04-28 07:37:54 +08:00
kuaifan
46787b187f no message 2022-04-27 16:15:03 +08:00
kuaifan
7b8a6c2a4c perf: 移交项目和任务时记录被移交对象 2022-04-27 16:00:55 +08:00
kuaifan
7b757d68b4 no message 2022-04-27 15:48:09 +08:00
kuaifan
092506e9ab perf: 共享的文件禁止移动到另一个共享文件夹内 2022-04-27 15:47:53 +08:00
kuaifan
529b4bec31 no message 2022-04-27 08:06:38 +08:00
kuaifan
2e88e933c3 no message 2022-04-27 08:03:36 +08:00
kuaifan
031dac36e6 fix: 修复共享文件移动到共享文件夹内共享属性错乱的问题 2022-04-27 08:03:17 +08:00
kuaifan
dc3b666635 调整office映射文件 2022-04-27 07:20:11 +08:00
kuaifan
e45bcf34a3 perf: 优化自定义sso登录 2022-04-27 07:05:50 +08:00
kuaifan
0aa7679a71 build 2022-04-26 11:18:27 +08:00
kuaifan
a46ac38561 fix: 同时删除多个任务负责人或协助人员任务动态显示错误的问题 2022-04-26 11:12:52 +08:00
kuaifan
b1395377a1 fix: 修复 ETooltip 组件 disabled 取消后错位的问题 2022-04-26 10:27:50 +08:00
kuaifan
41e60ee990 perf: 优化文件历史查看 2022-04-26 09:11:01 +08:00
kuaifan
00f80e8db8 no message 2022-04-26 08:45:04 +08:00
kuaifan
f70da2c4a2 perf: 查看文件修改历史时文本编辑器、图表点击编辑历史窗口不隐藏 2022-04-25 22:04:58 +08:00
kuaifan
ee485020d1 fix: 添加任务窗口选择其他项目无效的问题 2022-04-25 21:54:12 +08:00
kuaifan
8cdbb969ff 一键编译所有generic 2022-04-25 21:39:53 +08:00
kuaifan
687b9ca8b1 no message 2022-04-25 09:53:28 +08:00
kuaifan
f486477a41 no message 2022-04-25 08:15:12 +08:00
kuaifan
6f7acc60b5 no message 2022-04-25 08:12:27 +08:00
kuaifan
c748d8f5f6 perf: 优化任务详情右键预览图片 2022-04-25 07:58:17 +08:00
kuaifan
5d1d108811 fix: 修复上传文件夹不立即显示的问题 2022-04-25 07:19:16 +08:00
kuaifan
d8d49d6b5a no message 2022-04-25 05:10:27 +08:00
kuaifan
ebb3ec6784 feat: 新增查看文件历史版本 2022-04-25 05:04:21 +08:00
kuaifan
ab66b70485 no message 2022-04-25 05:03:52 +08:00
kuaifan
a723c2a44a perf: 文件打开保存机制 2022-04-24 14:38:38 +08:00
kuaifan
d0c7ee5e47 fix: 修复已打开文件需刷新网页才显示最新内容的情况 2022-04-22 15:55:56 +08:00
kuaifan
3c386eeaa9 perf: 客户端升级日志 2022-04-22 15:18:40 +08:00
kuaifan
e5c4faf6ef no message 2022-04-22 09:11:15 +08:00
kuaifan
0274a7f6e6 no message 2022-04-22 08:38:30 +08:00
kuaifan
92632cf294 no message 2022-04-22 08:23:14 +08:00
kuaifan
864af174f7 office开启强制保存按钮 2022-04-22 08:23:03 +08:00
kuaifan
b1cbf6f893 优化头像上传 2022-04-22 07:43:47 +08:00
kuaifan
0b0e6951e5 优化高度计算 2022-04-22 07:05:20 +08:00
kuaifan
52392e2e0a perf: 发送消息未设置昵称的优化 2022-04-21 21:18:48 +08:00
kuaifan
493d05f807 build 2022-04-21 20:56:35 +08:00
kuaifan
4ae7f22293 Merge remote-tracking branch 'A_github_pro/master' 2022-04-21 20:55:00 +08:00
kuaifan
204bdcd2a9 新增manifest预取 URL声明 2022-04-21 20:10:33 +08:00
kuaifan
c185b4c9c2 perf: 优化共享文件夹图标 2022-04-21 19:50:23 +08:00
kuaifan
7d48f9bde5 fix: 修复打开pdf因为文件名内容出错的问题 2022-04-21 17:25:00 +08:00
kuaifan
a48a1210c8 perf: 优化重复共享提示 2022-04-21 16:31:28 +08:00
kuaifan
6b2b072e16 no message 2022-04-20 18:44:11 +08:00
kuaifan
8acf3b21c7 perf: 优化聊天窗口群聊已读列表 2022-04-20 16:13:12 +08:00
kuaifan
0ad32ff63d perf: 优化任务窗口 2022-04-20 16:12:19 +08:00
kuaifan
4c595affa5 feat: 新增聊天选择内容粗体、斜体、删除线、序号等工具 2022-04-20 08:42:19 +08:00
kuaifan
4f267c6d89 no message 2022-04-20 08:35:30 +08:00
kuaifan
fae46bc2b9 no message 2022-04-20 06:56:32 +08:00
kuaifan
81f8a01cc1 no message 2022-04-20 06:39:30 +08:00
kuaifan
a6be0dc079 no message 2022-04-19 20:39:41 +08:00
kuaifan
0e4017f69a build 2022-04-19 08:07:00 +08:00
kuaifan
505f3b8b05 perf: 优化暗黑模式 2022-04-19 08:03:21 +08:00
kuaifan
65f53e264c no message 2022-04-19 00:40:35 +08:00
kuaifan
7c5e9f9b75 优化聊天表情样式 2022-04-19 00:36:30 +08:00
kuaifan
87933c821f no message 2022-04-18 22:18:28 +08:00
kuaifan
b590bce3e1 no message 2022-04-18 22:02:25 +08:00
kuaifan
cb3174f7d8 no message 2022-04-18 21:09:36 +08:00
kuaifan
7b3d9d9402 perf: 客户端填写周报后保存关闭窗口 2022-04-18 21:07:50 +08:00
kuaifan
5b66d32f25 perf: 文件浏览保存排序 2022-04-18 20:54:48 +08:00
kuaifan
2686bfbf5e no message 2022-04-18 18:36:38 +08:00
kuaifan
9553e43fa8 no message 2022-04-18 17:37:03 +08:00
kuaifan
b5a46ca63a build 2022-04-18 17:11:07 +08:00
kuaifan
7435fa69f2 no message 2022-04-18 17:06:01 +08:00
kuaifan
ea7d889d99 no message 2022-04-18 08:18:26 +08:00
kuaifan
b4bd635dbb no message 2022-04-18 08:12:07 +08:00
kuaifan
a066fc7eae no message 2022-04-18 00:16:21 +08:00
kuaifan
11ae7b091a no message 2022-04-17 22:37:38 +08:00
kuaifan
8cbc3a9667 feat: 新增聊天表情 2022-04-17 22:14:33 +08:00
kuaifan
f5b663b900 no message 2022-04-16 15:45:31 +08:00
kuaifan
572415d089 no message 2022-04-16 12:57:31 +08:00
kuaifan
8893254664 perf: 优化@提醒 2022-04-16 09:57:09 +08:00
kuaifan
d5ac6fc0c7 no message 2022-04-15 23:23:46 +08:00
kuaifan
64e53b3dd0 perf: 群聊信息预览显示发言人昵称 2022-04-15 22:09:26 +08:00
kuaifan
1de42f5c72 no message 2022-04-15 21:12:39 +08:00
kuaifan
5c3d8727e0 no message 2022-04-15 17:08:55 +08:00
kuaifan
c6e693392c no message 2022-04-15 13:13:34 +08:00
kuaifan
522ca02b36 perf: 优化聊天输入框 2022-04-15 09:22:52 +08:00
kuaifan
8c05d8791d no message 2022-04-15 00:17:59 +08:00
kuaifan
8df3611e7d no message 2022-04-14 23:59:16 +08:00
kuaifan
428d2eb795 no message 2022-04-14 23:52:16 +08:00
kuaifan
74c20fc57e 优化群动作通知 2022-04-14 23:32:58 +08:00
kuaifan
6ca13bf263 优化群聊操作 2022-04-14 23:00:28 +08:00
kuaifan
fcd6b1ddec feat: 新增自定义创建群聊功能 2022-04-14 22:00:45 +08:00
kuaifan
ac1e7bc186 no message 2022-04-14 07:58:53 +08:00
kuaifan
8a0f53aaaf perf: 优化任务窗口标题偶尔出现显示不全的情况 2022-04-14 07:16:31 +08:00
kuaifan
473dbb8361 no message 2022-04-14 07:05:15 +08:00
kuaifan
7aa3989b11 perf: 调整操作自己的规则 2022-04-14 07:05:04 +08:00
kuaifan
04d121367a no message 2022-04-13 20:45:23 +08:00
kuaifan
7f79795c55 feat: 新增离职操作 2022-04-13 20:06:17 +08:00
kuaifan
f76016860f no message 2022-04-13 14:57:30 +08:00
kuaifan
6cd3449571 fix: 修复导出统计数据不准确的情况 2022-04-13 14:57:19 +08:00
kuaifan
c480be4e1d perf: 统一指定标签样式 2022-04-13 14:14:50 +08:00
kuaifan
e3583df260 perf: 群聊显示发件人昵称 2022-04-13 13:55:23 +08:00
kuaifan
fd40ae6b67 perf: 任务详情窗口通过附件传图片保存到附件列表 2022-04-13 12:01:31 +08:00
kuaifan
003ddb4016 no message 2022-04-12 15:59:28 +08:00
kuaifan
be755a6f4e no message 2022-04-12 15:45:57 +08:00
kuaifan
91c62ee55e no message 2022-04-12 14:50:19 +08:00
kuaifan
17dc434350 no message 2022-04-12 12:04:54 +08:00
kuaifan
ad46fd1aae perf: 客户端隐藏顶部加载进度条 2022-04-12 11:53:33 +08:00
kuaifan
9ffa46a878 fix: 修复文件协作不提醒的问题 2022-04-12 11:36:44 +08:00
kuaifan
b1ee3fe3cd perf: 优化文件浏览查看路径 2022-04-12 10:52:35 +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
1a65c4a579 修复客户端 2022-01-16 01:11:19 +08:00
kuaifan
7a28a8950c 修复前端编译 2022-01-16 00:43:05 +08:00
kuaifan
89e8b0d8b9 perf: 新增工作流初始化数据 2022-01-16 00:05:49 +08:00
kuaifan
74803df5bd no message 2022-01-15 23:37:29 +08:00
kuaifan
2a8e030fb4 perf: 优化客户端自动更新 2022-01-15 23:36:21 +08:00
kuaifan
c6ebe994cc build 2022-01-15 12:53:21 +08:00
kuaifan
be3677cfa8 优化DrawerOverlay组件支持底部显示动态改变尺寸 2022-01-15 12:16:50 +08:00
kuaifan
0c2e56271b 优化DrawerOverlay组件 2022-01-15 00:06:40 +08:00
kuaifan
b019d40009 fix: websocket获取链接失败 2022-01-14 23:15:59 +08:00
kuaifan
361484be95 build 2022-01-14 18:12:33 +08:00
kuaifan
f0ce6cc28f build 2022-01-14 17:59:07 +08:00
kuaifan
ede298a142 perf: 工作汇报样式 2022-01-14 17:57:51 +08:00
kuaifan
aca1a4d34c 优化ws返回的地址 2022-01-14 16:56:45 +08:00
韦荣超
e63c5c074c build: 0d0b5dd552 2022-01-14 16:46:30 +08:00
韦荣超
0d0b5dd552 fix: 【工作报告】填写'提交'按钮位置被挤压修改 2022-01-14 16:43:13 +08:00
kuaifan
08704e7f60 优化报错 2022-01-14 16:35:00 +08:00
韦荣超
d48ed18102 feat: 【工作报告】功能 2022-01-14 15:55:26 +08:00
kuaifan
3602acd187 build 2022-01-14 14:05:09 +08:00
kuaifan
ad477eb608 优化文案 2022-01-14 14:01:56 +08:00
kuaifan
0a393845e9 优化任务详情窗口聊天 2022-01-14 13:52:10 +08:00
kuaifan
59b98209ac 优化websocket重连后的逻辑 2022-01-14 13:46:22 +08:00
kuaifan
ac26713f86 优化查询代码 2022-01-14 12:19:50 +08:00
kuaifan
9250ef6f65 fix: 查看日历页面部分任务会不见 2022-01-14 10:23:43 +08:00
kuaifan
cabbeb07d0 优化websocket重连后的逻辑 2022-01-14 10:23:23 +08:00
kuaifan
43171645c0 build 2022-01-14 00:04:28 +08:00
kuaifan
79c1484288 新增优化websocket重连后刷新项目、对话、仪表盘数据 2022-01-13 23:59:38 +08:00
kuaifan
e730875db3 perf: 客户端更新提示 2022-01-13 23:55:07 +08:00
kuaifan
18e9fb99b5 更新nginx持续握手时间 2022-01-13 23:53:29 +08:00
kuaifan
fd82b9b555 build 2022-01-13 19:42:41 +08:00
kuaifan
5c3bf0067e 优化日志显示 2022-01-13 19:19:58 +08:00
kuaifan
ac6b90c986 fix: 任务流转会出错 2022-01-13 18:39:57 +08:00
kuaifan
7dcd8b29fc build 2022-01-13 11:50:54 +08:00
kuaifan
d951da2c02 fix: 修复客户端旧版本更新任务出现已完成列表中有状态为“进行中”的情况 2022-01-13 11:48:33 +08:00
kuaifan
605aee35c4 perf: 发送文件ws返回错误地址 2022-01-13 11:42:01 +08:00
kuaifan
dc1f1985e8 访问接口header加上version、platform 2022-01-13 11:22:46 +08:00
kuaifan
cebcc26baf 优化项目日志 2022-01-13 11:11:29 +08:00
kuaifan
75a12fc6f5 优化日志 2022-01-13 09:00:39 +08:00
kuaifan
a3e0d89eb0 修改任务状态判断是否项目负责人直接查表 2022-01-13 00:32:35 +08:00
kuaifan
9594cc674f no message 2022-01-13 00:30:36 +08:00
kuaifan
87cdee4fe8 pref: 优化任务日志的显示 2022-01-13 00:30:36 +08:00
kuaifan
1df55b78c2 perf: 项目页面点击搜索按钮再点击弹出的搜索框窗会隐藏 2022-01-13 00:30:36 +08:00
kuaifan
f7820a23be no message 2022-01-12 16:34:16 +08:00
kuaifan
f80b9cdca5 feat: 工作流程状态支持仅限状态负责人修改 2022-01-12 16:16:04 +08:00
kuaifan
f2180b22c7 build 2022-01-11 23:23:44 +08:00
kuaifan
a745088213 perf: 任务日志刷新 2022-01-11 23:17:59 +08:00
kuaifan
14be7bd2b1 no msg 2022-01-11 22:58:30 +08:00
kuaifan
d453ea66da 优化登录页按钮 2022-01-11 21:31:23 +08:00
kuaifan
e7ae86e261 feat: 任务流转自动负责人支持转让模式 2022-01-11 20:07:58 +08:00
kuaifan
b7417f41c5 perf: action中错误提醒弱化 2022-01-11 17:54:42 +08:00
kuaifan
9e7aa381ed perf: 手机版无法选择日期范围的问题 2022-01-11 17:47:34 +08:00
kuaifan
b02453e9f4 perf: 手机版无法聊天,输入页面跳动 2022-01-11 17:35:53 +08:00
kuaifan
6b7f8fd31c 优化完成任务标记 2022-01-11 14:18:49 +08:00
kuaifan
4652242d6b build 2022-01-11 12:16:01 +08:00
kuaifan
a47ca2f357 perf: 个人设置显示版本号 2022-01-11 12:13:26 +08:00
kuaifan
e5a1e58159 perf: 完成任务暂时继续显示在我的列表 2022-01-11 12:13:06 +08:00
kuaifan
01a1e34e99 no message 2022-01-11 10:59:47 +08:00
kuaifan
e711220a66 优化自定义服务器 2022-01-11 10:31:37 +08:00
kuaifan
1d20f529a0 no message 2022-01-11 00:28:58 +08:00
kuaifan
79a94d25bd perf: 客户端自动下载新版本更新 2022-01-10 19:27:43 +08:00
kuaifan
92793b8ff8 no message 2022-01-10 00:25:36 +08:00
kuaifan
982024f359 子任务时间在主任务时间内 2022-01-09 20:13:59 +08:00
kuaifan
e851bd4d61 子任务时间大于主任务时主任务自动修改 2022-01-09 18:56:30 +08:00
kuaifan
e4b9383e96 perf: 设置子任务时间时,如果主任务没有时间则自动设置 2022-01-09 18:20:36 +08:00
kuaifan
95ac9aac14 添加任务自动上工作流状态 2022-01-09 17:59:08 +08:00
kuaifan
b895eec69c 初步完成工作流 2022-01-09 17:52:46 +08:00
kuaifan
1fe4e80f82 no msg 2022-01-09 12:56:55 +08:00
kuaifan
eb1f5f2632 perf: 任务操作菜单组件化 2022-01-09 00:09:43 +08:00
kuaifan
114b792300 no message 2022-01-08 21:23:47 +08:00
kuaifan
56a352a9d2 perf: 调整仅项目负责人可以删除任务列表 2022-01-08 17:35:25 +08:00
kuaifan
4c4d60dd83 no message 2022-01-08 17:32:03 +08:00
kuaifan
134ab0fe98 no message 2022-01-08 17:27:30 +08:00
kuaifan
bbd394272f feat: 添加工作流 2022-01-08 17:24:48 +08:00
kuaifan
8a2571f514 no msg 2022-01-08 08:56:29 +08:00
kuaifan
bf15be8144 perf: 领取任务需要设置计划时间 2022-01-08 08:51:34 +08:00
kuaifan
bf6f8de7fa no msg 2022-01-08 00:35:01 +08:00
kuaifan
2e75e6ffb6 后台限制任务负责人、协助人员每项最多不能超过10个 2022-01-07 23:36:05 +08:00
kuaifan
b987be54bf 优化前端变量 2022-01-07 23:28:07 +08:00
kuaifan
533e8a3742 build 2022-01-07 19:12:38 +08:00
kuaifan
9d007e64f6 优化部分相同代码 2022-01-07 18:42:04 +08:00
kuaifan
8ec1578f50 左边底部新增添加任务选项 2022-01-07 18:23:30 +08:00
kuaifan
4e8cc36d3a 显示完成时间 2022-01-07 18:09:22 +08:00
kuaifan
5a64cb2323 替换Tooltip改成ETooltip 2022-01-07 18:09:05 +08:00
kuaifan
b2aba82a1b perf: 任务版本如果只有一个负责人时显示负责人的名字 2022-01-07 17:46:03 +08:00
kuaifan
9d9500ba1b fix: 其他人员添加任务会临时出现在自己的列表中 2022-01-07 17:26:22 +08:00
kuaifan
674c5a11c1 客户端pkg包安装前必须关闭已打开的应用 2022-01-07 10:56:07 +08:00
kuaifan
8e2b2947ae 版本号基于master分支生成 2022-01-07 10:16:35 +08:00
kuaifan
0413ca7cba 新增pkg安装包 2022-01-07 10:14:49 +08:00
kuaifan
15cf7800a4 日历选时间快速添加任务 2022-01-07 01:07:41 +08:00
kuaifan
db6114a4ee pref: 日历任务缓存 2022-01-07 00:56:03 +08:00
kuaifan
818495a697 build 2022-01-06 23:25:04 +08:00
kuaifan
7aa41c4050 no msg 2022-01-06 23:22:03 +08:00
kuaifan
1975b8af1d perf: 日历选择时间添加任务 2022-01-06 23:15:04 +08:00
kuaifan
b870f5f4d1 no msg 2022-01-06 22:24:45 +08:00
kuaifan
3ebc720934 no message 2022-01-06 20:24:31 +08:00
kuaifan
748976f393 feat: 创建任务窗口添加小时钟 2022-01-06 20:12:13 +08:00
kuaifan
83e5c7fadb 优化注释 2022-01-06 19:22:29 +08:00
kuaifan
f919a34166 优化任务、列表数量限制 2022-01-06 19:12:49 +08:00
kuaifan
77a9eca634 no msg 2022-01-06 18:56:50 +08:00
kuaifan
72b732a55d perf: 归档任务可以搜索 2022-01-06 18:53:34 +08:00
kuaifan
0236897d1f no msg 2022-01-06 17:17:23 +08:00
kuaifan
46f95e5e13 no msg 2022-01-06 16:57:13 +08:00
kuaifan
24287c0857 fix: 主任务被删除或归档时子任务应该也同步 2022-01-06 16:51:32 +08:00
kuaifan
c930e4dd92 perf: 归档任务列表显示完成时间 2022-01-06 16:30:45 +08:00
kuaifan
7d8856e4bc no msg 2022-01-06 15:41:58 +08:00
kuaifan
6d026bbf42 no message 2022-01-06 15:24:54 +08:00
kuaifan
713770a448 no message 2022-01-06 15:09:41 +08:00
kuaifan
ac873fa757 no msg 2022-01-06 14:40:25 +08:00
kuaifan
6b3513c1c4 feat: 限制个人最多500个未完成任务 2022-01-06 14:40:19 +08:00
kuaifan
840374c48c feat: 限制项目最多100个人参与 2022-01-06 14:39:56 +08:00
kuaifan
7f454f279c perf: 项目版面成员显示 2022-01-06 14:28:23 +08:00
kuaifan
267c09f20c no msg 2022-01-06 13:38:49 +08:00
kuaifan
e44e77a3a6 pref: 子任务默认起始时间与主任务一致 2022-01-06 13:28:47 +08:00
kuaifan
4d9dd13ffb pref: 禁用会员记录禁用时间 2022-01-06 13:28:10 +08:00
kuaifan
369e75cb7e perf: 安装项目时支持自定义端口 2022-01-06 11:33:51 +08:00
kuaifan
0a6fa2431e perf: 任务详情窗口光标在任务描述可使用ctrl+s(command+s)保存 2022-01-06 08:27:32 +08:00
kuaifan
0bc40d1748 perf: 文件快捷键保存 2022-01-06 08:17:37 +08:00
kuaifan
919dc3cdea feat: 思维导图支持无极缩放 2022-01-06 08:08:40 +08:00
kuaifan
7c64b27ef4 feat: 自动归档已完成任务 2022-01-06 01:19:44 +08:00
kuaifan
369577a2c8 no message 2022-01-06 00:32:27 +08:00
kuaifan
fbc25e5134 no message 2022-01-05 23:50:29 +08:00
kuaifan
aba1628d36 no message 2022-01-05 22:51:02 +08:00
kuaifan
90336e1edf 整理$A 2022-01-05 20:19:27 +08:00
kuaifan
dde32fcaee no message 2022-01-05 19:49:47 +08:00
kuaifan
6324e79aba no message 2022-01-05 15:04:55 +08:00
kuaifan
7a22f4b20f no message 2022-01-05 14:56:11 +08:00
kuaifan
aad621bd84 no message 2022-01-05 14:17:45 +08:00
kuaifan
0b4e1f3dee no message 2022-01-05 13:41:05 +08:00
kuaifan
78f88db560 将所有任务都取出全部显示,子任务判断显示主任务 2022-01-05 09:30:00 +08:00
kuaifan
cc1c425ecf no message 2022-01-04 23:42:29 +08:00
kuaifan
186290e355 未完成项目概况 2022-01-04 20:23:01 +08:00
kuaifan
f1f1d784ff 修复我的任务数据 2022-01-01 15:03:33 +08:00
623585344@qq.com
320f183b49 add:新增暗黑皮肤样式 2021-12-31 16:59:42 +08:00
kuaifan
a1c7efeb85 任务数据逻辑 2021-12-31 14:53:57 +08:00
kuaifan
ba5f635687 子任务默认时间与主任务相同 2021-12-31 11:07:44 +08:00
kuaifan
bae5afc0da no message 2021-12-31 09:59:11 +08:00
kuaifan
befd5c3b08 去除taskSubs 2021-12-31 07:48:55 +08:00
kuaifan
0b4e96a90f build 2021-12-30 15:35:16 +08:00
kuaifan
57edf38c1a fix: 任务有负责人后仅限项目或任务负责人修改 2021-12-30 15:32:36 +08:00
kuaifan
502dd1ec1f perf: 未被领取的任务标记完成之前先领取 2021-12-30 14:39:12 +08:00
kuaifan
02361ddfb2 perf: 快速添加任务0天的描述 2021-12-30 14:31:23 +08:00
kuaifan
bcf6cc1019 perf: 客户端标题优化 2021-12-30 14:18:25 +08:00
kuaifan
b97a2e7cf3 build 2021-12-30 11:04:16 +08:00
kuaifan
27158e1ee7 perf: office文件预览 2021-12-30 10:58:51 +08:00
kuaifan
28abad0276 no msg 2021-12-30 10:36:46 +08:00
kuaifan
b59549ebe9 perf: 文件预览不能滚动的问题 2021-12-30 10:34:52 +08:00
kuaifan
b9f788fbe8 build 2021-12-30 00:58:23 +08:00
kuaifan
9d89334cc5 perf: 优化文件权限 2021-12-30 00:52:34 +08:00
kuaifan
6c67ff3fe8 fix: 只读文件也能修改文件 2021-12-29 22:11:18 +08:00
kuaifan
6ef59f703a perf: 初始化数据 2021-12-29 21:40:13 +08:00
623585344@qq.com
c8f11578d6 mod:修改首页内容部分图片自适应 2021-12-29 17:47:45 +08:00
kuaifan
bbd055c798 build 2021-12-29 17:36:33 +08:00
kuaifan
53879fcefb feat: 文件分享查看链接 2021-12-29 17:23:53 +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
kuaifan
fd6e7f3096 feat: 新增主动退出共享文件 2021-12-29 14:11:33 +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
kuaifan
4d8cf41b7a feat: 共享给所有人 2021-12-29 13:08:30 +08:00
kuaifan
2b0467e00f feat: 文件支持只读、读/写细化设置 2021-12-29 08:38:45 +08:00
kuaifan
cef6646f50 perf: Windows客户端无法关闭的情况 2021-12-28 16:55:29 +08:00
test
ffc2c7dea3 add:首页功能开发 2021-12-28 15:38:40 +08:00
test
d7652a7a32 add:首页功能开发 2021-12-28 15:34:54 +08:00
kuaifan
b695f90ded build 2021-12-28 14:40:59 +08:00
kuaifan
d7f1246c32 perf: 任务等级支持设置0天表示默认不限时 2021-12-28 14:37:02 +08:00
kuaifan
739d1f2455 perf: 客户端子窗口 2021-12-28 14:12:24 +08:00
kuaifan
be7c6e700b fix: 聊天窗口不自动滚动的问题 2021-12-28 12:24:27 +08:00
kuaifan
320dd49a87 perf: 任务窗口 2021-12-28 11:42:59 +08:00
kuaifan
9bfa680fa4 feat: 客户端子窗口数据同步到主窗口 2021-12-28 09:00:44 +08:00
kuaifan
ed32f9994d feat: 任务新窗口打开 2021-12-27 23:20:37 +08:00
kuaifan
c6eb850abe no message 2021-12-27 21:04:35 +08:00
kuaifan
6c458b81b2 fix: 清除缓存导致自定义服务器失败的问题 2021-12-27 14:49:08 +08:00
kuaifan
6741f59aef fix: office无法修改的问题 2021-12-27 14:44:45 +08:00
kuaifan
6a0fd46fc4 perf: 链接说明 2021-12-27 14:37:21 +08:00
kuaifan
b2c8beae71 build 2021-12-27 14:21:05 +08:00
kuaifan
b376327438 style: 任务描述溢出的问题 2021-12-27 13:57:42 +08:00
kuaifan
9eb4fecbe8 feat: 客户端支持自定义服务器 2021-12-27 13:49:04 +08:00
kuaifan
f672f4d1bb style: 优化客户端更新窗口滚动 2021-12-27 12:26:43 +08:00
kuaifan
c8b085e963 perf: 发布窗口小屏幕不支持右键(隐藏右键提示) 2021-12-27 12:20:10 +08:00
kuaifan
8cd3daee9d style: 发布任务出口小屏幕下方按钮不居中 2021-12-27 12:20:10 +08:00
kuaifan
0b03aec038 fix: 邀请加入项目数据库迁移文件 2021-12-27 12:19:03 +08:00
kuaifan
b04fd1a937 fix: 系统设置保存后邀请码消失 2021-12-27 00:07:34 +08:00
kuaifan
bdf4222d70 build 2021-12-26 23:48:14 +08:00
kuaifan
261f7ebbc2 fix: 任务详情窗口不显示子任务 2021-12-26 23:42:38 +08:00
kuaifan
91f25e9ec3 feat: 注册邀请码模式 2021-12-26 23:38:47 +08:00
kuaifan
dd86bb88c6 feat: 通过链接邀请加入项目 2021-12-26 23:38:25 +08:00
kuaifan
0665f2de5f feat: 文件上传进度 2021-12-26 11:51:12 +08:00
kuaifan
364e5df974 fix: 全局loading 2021-12-26 02:22:26 +08:00
kuaifan
d3cdaccbc5 build 2021-12-26 01:52:43 +08:00
kuaifan
75dbc990e4 perf: 安装composer install失败时尝试使用国内源再试一次 2021-12-26 01:44:04 +08:00
kuaifan
5bdd6e15e4 fix: 图片媒体文件无法预览 2021-12-26 01:20:37 +08:00
kuaifan
f7b5a2e971 feat: 新注册自动创建个人项目 2021-12-26 01:11:27 +08:00
kuaifan
a486eefd81 perf: 优化office加载提示 2021-12-26 01:00:49 +08:00
kuaifan
1fae364e7d perf: 按需加载富文本静态资源 2021-12-26 00:43:57 +08:00
kuaifan
76db0c41d3 style: 登录成功加载效果 2021-12-26 00:11:44 +08:00
kuaifan
7318b8917d style: 客户端文件窗口样式 2021-12-25 19:12:55 +08:00
kuaifan
fefcd682b8 build 2021-12-25 16:39:03 +08:00
kuaifan
a32c0b028b perf: 客户端新窗口打开文件 2021-12-25 16:35:47 +08:00
kuaifan
ffe62b8f8e fix: 客户端新窗口 2021-12-25 15:19:03 +08:00
kuaifan
5076374b5d doc: Docker Compose v2.0+ 2021-12-25 14:50:04 +08:00
kuaifan
d368e24f75 perf: 客户端窗口标题 2021-12-25 14:49:44 +08:00
kuaifan
343c5eb587 feat: 客户端新窗口打开文件 2021-12-25 14:48:38 +08:00
kuaifan
f7fc379e56 perf: 领取任务流程 2021-12-25 11:43:29 +08:00
kuaifan
ccf4c4bbb3 fix: window客户端弹出错误 2021-12-25 11:11:57 +08:00
kuaifan
a32995abec perf: 到期时间格式化 2021-12-25 02:18:56 +08:00
kuaifan
abd87f3584 feat: 任务创建窗口自动选择上次添加的项目 2021-12-25 02:12:53 +08:00
kuaifan
bbf7277abc style: iview 2021-12-25 01:44:35 +08:00
kuaifan
b38b335fa1 perf: 小屏幕弹窗底部按钮铺全 2021-12-25 01:29:28 +08:00
kuaifan
cf41e71494 perf: 仪表盘完成任务 2021-12-25 01:06:32 +08:00
kuaifan
e75408d20d style: iview 2021-12-25 00:59:26 +08:00
kuaifan
d79f3c6a80 fix: 任务窗口详情描述内容溢出 2021-12-25 00:16:28 +08:00
kuaifan
1a5e196e4e fix: 任务聊天窗口领取任务按钮错位 2021-12-25 00:11:29 +08:00
kuaifan
138bfc8362 fix: 任务级别设置等级描述 2021-12-25 00:06:44 +08:00
kuaifan
464795779f fix: 手机版隐藏客户端下载按钮 2021-12-24 23:49:33 +08:00
kuaifan
e65c80962d perf: 添加任务窗口 2021-12-24 23:49:09 +08:00
kuaifan
7171d1d6b2 perf: 添加任务弹窗 2021-12-24 12:59:05 +08:00
kuaifan
162c6e95d3 style: 排序箭头颜色 2021-12-24 00:36:29 +08:00
kuaifan
63743656d4 fix: 按到期时间排序没有时间应该排到最后 2021-12-24 00:27:01 +08:00
kuaifan
a3a9032af7 build 2021-12-23 23:36:50 +08:00
kuaifan
194ef2b4ca fix: 分页 #31 2021-12-23 23:34:49 +08:00
kuaifan
89b8342ca0 perf: 客户端关闭 2021-12-23 23:26:33 +08:00
kuaifan
ba32df2fb8 perf: 客户端在项目页面支持快捷键添加任务 2021-12-23 23:22:10 +08:00
kuaifan
08234afe4f fix: 任务负责人无法修改子任务的问题 2021-12-23 23:20:04 +08:00
kuaifan
41eb28992e feat: mac客户端角标提示加上仪表盘内的任务数 2021-12-23 21:50:13 +08:00
kuaifan
20a6da4944 feat: 客户端快捷键关闭侧滑窗 2021-12-23 21:49:27 +08:00
kuaifan
544496a09b feat: 客户端快捷键关闭窗口先关网页内的弹窗 2021-12-23 19:35:35 +08:00
kuaifan
26e7d562aa docs: 添加QQ群号 2021-12-23 16:44:11 +08:00
kuaifan
d1814a4e0f feat: 项目任务支持优先级、到期时间排序 2021-12-23 15:41:48 +08:00
kuaifan
a11adad23f perf: 已完成任务的显示 2021-12-23 15:10:09 +08:00
kuaifan
834c8cc7d5 perf: 任务详细描述、添加任务 2021-12-23 14:55:29 +08:00
kuaifan
1801ea7873 perf: 仪表盘 2021-12-23 12:21:23 +08:00
kuaifan
b208634e40 feat: 仪表盘徽标数 2021-12-23 01:33:14 +08:00
kuaifan
7b3d071fd3 feat: 协助的任务 2021-12-23 01:09:05 +08:00
kuaifan
663a8bb06d perf: 时间范围任务 2021-12-23 01:07:47 +08:00
kuaifan
6c34f083e3 perf: 今天任务、我的任务 2021-12-22 22:39:08 +08:00
kuaifan
6d9237c399 perf: 日历日、周视图选择添加任务会进入all day 2021-12-22 21:37:07 +08:00
kuaifan
8a7186c1b1 feat: 记住最后登录账号 2021-12-22 21:19:22 +08:00
kuaifan
c120686fae perf: 清除缓存 2021-12-22 21:13:25 +08:00
kuaifan
75c83e4117 fix 只有一列的时候默认版面为表格模式 2021-12-22 20:28:40 +08:00
kuaifan
d16b846d4e build 2021-12-22 19:51:54 +08:00
kuaifan
fe44c35406 使用element notification 2021-12-22 17:49:44 +08:00
kuaifan
376bcc4a0b 新增客户端提示升级 2021-12-22 15:37:19 +08:00
kuaifan
26d9e63e83 优化electron脚本命令 2021-12-22 12:13:31 +08:00
kuaifan
a7ac2cee13 no message 2021-12-22 01:42:21 +08:00
kuaifan
7a850704e5 no message 2021-12-22 00:37:17 +08:00
kuaifan
d4cc561d90 add workflows 2021-12-21 23:47:08 +08:00
aipaw
1430935c7e build 2021-12-21 01:44:31 +08:00
aipaw
497289fe69 office 默认亮色 2021-12-21 01:41:18 +08:00
aipaw
f7d75b830c build 2021-12-21 01:25:55 +08:00
aipaw
a20222747d 可设置发送聊天前必须设置昵称 2021-12-21 01:22:05 +08:00
aipaw
75ac1e9770 新增密码策略 2021-12-20 23:53:32 +08:00
wang
64d606013d 只有一列的时候默认版面为表格模式 2021-12-20 23:23:44 +08:00
kuaifan
40e8e47398 添加任务后继续添加 2021-12-20 18:52:24 +08:00
kuaifan
6faf7e71ba 升级onlyoffice 2021-12-20 18:51:44 +08:00
kuaifan
b20abb824d fix office save error 2021-12-19 23:55:31 +08:00
kuaifan
fa8c209ea3 no message 2021-12-19 22:56:12 +08:00
kuaifan
5181010d22 优化清除缓存 2021-12-19 19:14:05 +08:00
kuaifan
5c2a82b7ee 优化缓存机制 2021-12-19 17:30:13 +08:00
kuaifan
f69de50ed1 no message 2021-12-19 15:32:52 +08:00
kuaifan
56e142ac05 add custom repassword 2021-12-14 19:51:41 +08:00
kuaifan
32c2d60f7e 优化electron命令 2021-12-14 18:16:56 +08:00
kuaifan
cb792cbb26 fix more project hidden 2021-12-14 10:16:16 +08:00
kuaifan
c36b10a416 fix office file ext 2021-12-13 13:06:10 +08:00
kuaifan
f1d87cfbb2 build 2021-12-13 12:50:24 +08:00
kuaifan
cde6530832 优化任务项目归档 2021-12-13 12:43:53 +08:00
kuaifan
a5b1fec6b1 优化缓存数据 2021-12-13 12:22:35 +08:00
kuaifan
7cf76d91a0 优化归档 2021-12-13 00:27:47 +08:00
kuaifan
28dc314397 优化任务查看权限 2021-12-13 00:13:10 +08:00
kuaifan
66b5e5dd3f 优化缓存机制 2021-12-12 17:38:19 +08:00
kuaifan
678af5f744 build 2021-12-11 23:47:44 +08:00
kuaifan
85fcc28662 优化文件类型 2021-12-11 23:38:45 +08:00
kuaifan
f6035de548 fix file share empty user 2021-12-11 23:30:37 +08:00
kuaifan
0ea82b9f98 fix userinput empty loading 2021-12-11 23:29:19 +08:00
kuaifan
3260e5127a fix js report errors 2021-12-11 23:19:08 +08:00
kuaifan
f260d83923 优化缓存机制 2021-12-11 23:09:22 +08:00
kuaifan
cece720f68 fix app office load error 2021-12-11 22:45:15 +08:00
kuaifan
03c0449a0b no message 2021-12-11 13:14:43 +08:00
kuaifan
4ded7987e2 优化接口速度 2021-12-11 12:59:58 +08:00
kuaifan
49bbf826b9 build 2021-12-11 11:26:48 +08:00
kuaifan
42fc52a067 联系人加载更多 2021-12-11 10:20:07 +08:00
kuaifan
72e97d57b2 优化脚本命令 2021-12-11 09:46:58 +08:00
kuaifan
dbd99e1f57 build 2021-12-11 02:34:38 +08:00
kuaifan
8e68824ac0 文件格式svg 2021-12-11 00:56:51 +08:00
kuaifan
5fe79c4bad 文件预览优化 2021-12-11 00:03:03 +08:00
kuaifan
0fe89c34b3 Merge branch 'master' of github.com:kuaifan/dootask into dev 2021-12-10 22:23:51 +08:00
kuaifan
4a451ad5ae 新增演示账号显示 2021-12-10 22:22:19 +08:00
kuaifan
27984ceef2 Merge branch 'master' of github.com:kuaifan/dootask
# Conflicts:
#	app/Exceptions/Handler.php
2021-12-10 22:21:21 +08:00
kuaifan
8987a2d177 no message 2021-12-10 19:56:11 +08:00
kuaifan
915b6a918c no message 2021-12-10 19:30:20 +08:00
kuaifan
40bf82552b no message 2021-12-10 19:16:25 +08:00
kuaifan
211d17b609 build 2021-12-10 19:15:48 +08:00
kuaifan
6290bbdd4e no message 2021-12-10 18:52:30 +08:00
kuaifan
aa9a3238a2 新增文件预览功能 2021-12-10 17:50:45 +08:00
kuaifan
a303a60c4e 重写report优雅记录 2021-12-10 10:29:09 +08:00
kuaifan
7e67fa3076 优化会员限制 2021-12-10 06:36:19 +08:00
kuaifan
7ef966536d update https to http md 2021-12-10 01:30:19 +08:00
kuaifan
4c3827158f 优化脚本命令 2021-12-09 15:49:35 +08:00
kuaifan
6c577daf5d 优化脚本命令 2021-12-09 15:39:57 +08:00
kuaifan
9e54c689a1 优化脚本命令 2021-12-09 13:27:57 +08:00
kuaifan
ee7a53ff89 更新升级说明 2021-12-09 01:21:51 +08:00
kuaifan
c3c3c6b996 build 2021-12-08 23:54:40 +08:00
kuaifan
bfd3d2937d 取消默认ssl 2021-12-08 23:21:30 +08:00
kuaifan
58099284f2 优化安装命令 2021-12-08 22:35:40 +08:00
kuaifan
fc3a67f811 build 2021-12-08 18:10:17 +08:00
kuaifan
f796c2c1e2 优化安装命令 2021-12-08 18:05:11 +08:00
kuaifan
43a5fb4bd3 修改php容器 2021-12-07 23:46:12 +08:00
kuaifan
b2da1c0f76 fix install 2021-12-01 18:17:47 +08:00
kuaifan
31664cbc4d Merge branch 'master' of gitee.com:aipaw/dootask 2021-11-28 22:43:39 +08:00
kuaifan
ab17736fbf build 2021-11-28 22:43:25 +08:00
kuaifan
42bbbc5f75 fix openUserDialog 2021-11-28 22:10:22 +08:00
kuaifan
86e968a346 fix 聊天会员搜索 2021-11-28 22:10:03 +08:00
kuaifan
d22ee4b34a fix 判断局域网ip 2021-11-28 22:09:18 +08:00
kuaifan
30d4e1eaa9 搜索会员新增分页参数 2021-11-28 22:08:53 +08:00
aipaw
15618dd988 add LICENSE. 2021-11-18 02:31:17 +00:00
2767 changed files with 208631 additions and 451032 deletions

View File

@@ -1,17 +1,19 @@
APP_NAME=Dootask
APP_NAME=DooTask
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_SCHEME=auto
APP_URL=http://localhost
APP_ID=
APP_IPPR=
APP_PORT=2222
APP_PORT_SSL=2223
LOG_CHANNEL=stack
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=10.22.22.5
DB_HOST="${APP_IPPR}.5"
DB_PORT=3306
DB_DATABASE=dootask
DB_USERNAME=dootask
@@ -28,7 +30,7 @@ SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=10.22.22.4
REDIS_HOST="${APP_IPPR}.4"
REDIS_PASSWORD=null
REDIS_PORT=6379
@@ -56,5 +58,3 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
LARAVELS_LISTEN_IP=0.0.0.0
LARAVELS_LISTEN_PORT=20000
DOCKER_ID=

View File

@@ -2,10 +2,10 @@ APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_SCHEME=auto
APP_URL=http://localhost
APP_PORT=2222
APP_PORT_SSL=2223
LOG_CHANNEL=stack
LOG_LEVEL=debug

85
.github/workflows/electron-generic.yml vendored Normal file
View File

@@ -0,0 +1,85 @@
name: Build Generic
on:
push:
tags:
- 'v*'
jobs:
build-mac:
strategy:
fail-fast: false
matrix:
id: [ "com.dootask.task", "com.hitosea.task" ]
runs-on: macos-latest
environment: build
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,build
- name: Use Node.js 16.x
uses: actions/setup-node@v1
with:
node-version: 16.x
- name: Build for MacOS
env:
APPID: ${{ matrix.id }}
PROVIDER: "generic"
APPLEID: ${{ secrets.APPLEID }}
APPLEIDPASS: ${{ secrets.APPLEIDPASS }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
CSC_LINK: ${{ secrets.CSC_LINK }}
RELEASE_BODY: ${{ steps.changelog.outputs.changes }}
run: ./cmd electron build-mac
build-win:
strategy:
fail-fast: false
matrix:
id: [ "com.dootask.task", "com.hitosea.task" ]
runs-on: windows-latest
environment: build
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,build
- name: Use Node.js 16.x
uses: actions/setup-node@v1
with:
node-version: 16.x
- name: Build for Windows
shell: powershell
env:
APPID: ${{ matrix.id }}
PROVIDER: "generic"
RELEASE_BODY: ${{ steps.changelog.outputs.changes }}
run: |
npm install
cd electron
npm install
cd ../
mkdir -p ./electron/public
cp ./electron/index.html ./electron/public/index.html
npx mix --production -- --env --electron
node ./electron/build.js build-win

97
.github/workflows/electron-github.yml vendored Normal file
View File

@@ -0,0 +1,97 @@
name: Build Github
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
if: ${{ (startsWith(github.event.ref, 'refs/tags/v')) && (github.repository == 'kuaifan/dootask') }}
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,build
- 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-mac:
strategy:
fail-fast: false
matrix:
id: [ "com.dootask.task", "com.hitosea.task" ]
runs-on: macos-latest
environment: build
if: ${{ (startsWith(github.event.ref, 'refs/tags/v')) && (github.repository == 'kuaifan/dootask') }}
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Use Node.js 16.x
uses: actions/setup-node@v1
with:
node-version: 16.x
- name: Build for MacOS
env:
APPID: ${{ matrix.id }}
PROVIDER: "github"
APPLEID: ${{ secrets.APPLEID }}
APPLEIDPASS: ${{ secrets.APPLEIDPASS }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
CSC_LINK: ${{ secrets.CSC_LINK }}
GH_TOKEN: ${{ secrets.GH_PAT }}
EP_PRE_RELEASE: true
run: ./cmd electron build-mac
build-win:
strategy:
fail-fast: false
matrix:
id: [ "com.dootask.task", "com.hitosea.task" ]
runs-on: windows-latest
environment: build
if: ${{ (startsWith(github.event.ref, 'refs/tags/v')) && (github.repository == 'kuaifan/dootask') }}
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Use Node.js 16.x
uses: actions/setup-node@v1
with:
node-version: 16.x
- name: Build for Windows
shell: powershell
env:
APPID: ${{ matrix.id }}
PROVIDER: "github"
GH_TOKEN: ${{ secrets.GH_PAT }}
EP_PRE_RELEASE: true
run: |
npm install
cd electron
npm install
cd ../
mkdir -p ./electron/public
cp ./electron/index.html ./electron/public/index.html
npx mix --production -- --env --electron
node ./electron/build.js build-win

2
.gitignore vendored
View File

@@ -8,6 +8,7 @@
/vendor
/build
/tmp
/CHANGELOG.md
._*
.env
.idea
@@ -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 = git@github.com:kuaifan/dootask-app.git

661
LICENSE Normal file
View File

@@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.

113
README.md
View File

@@ -5,41 +5,54 @@ English | **[中文文档](./README_CN.md)**
- [Screenshot Preview](README_PREVIEW.md)
- [Demo site](http://www.dootask.com/)
**QQ Group**
Group No.: `546574618`
## Setup
> `Docker` & `Docker Compose` 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
```bash
# 1、Clone the repository
# using ssh
git clone git@github.com:kuaifan/dootask.git
# or you can use https
git clone https://github.com/kuaifan/dootask.git
# Clone projects on github
git clone --depth=1 https://github.com/kuaifan/dootask.git
# Or you can use gitee
git clone --depth=1 https://gitee.com/aipaw/dootask.git
# 2、enter directory
# 2、Enter directory
cd dootask
# 3、Build project
# 3、InstallationCustom port installation: ./cmd install --port 2222
./cmd install
```
Installed, project url: **`http://IP:PORT`**`PORT`Default is`2222`)。
### Default Account
### Reset password
```text
account: admin@dootask.com
password: 123456
```bash
# Reset default account password
./cmd repassword
```
### Change port
```bash
./cmd php bin/run --port=2222
./cmd up -d
./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
@@ -51,18 +64,40 @@ password: 123456
./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 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 npm "your command" // To run a npm command
./cmd yarn "your command" // To run a yarn command
./cmd mysql "your command" // To run a mysql command (use `./cmd mysql bak` Backup 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
```bash
# 1、Nginx config add
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、Running commands in a project
./cmd https
```
## Upgrade
@@ -70,13 +105,41 @@ password: 123456
**Note: Please back up your data before upgrading!**
```bash
# Enter directory and run command
# Method 1: Running commands in a project
./cmd update
# Or method 2: use this method if method 1 fails
git pull
./cmd mysql backup
./cmd uninstall
./cmd install
./cmd mysql recovery
```
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
```

View File

@@ -5,41 +5,54 @@
- [截图预览](README_PREVIEW.md)
- [演示站点](http://www.dootask.com/)
**QQ交流群**
- QQ群号: `546574618`
## 安装程序
> 必须安装 `Docker` 和 `Docker Compose`
- 必须安装`Docker``Docker Compose v2.0+`
- 支持环境:`Centos/Debian/Ubuntu/macOS`
- 硬件建议2核4G以上
### 部署项目
```bash
# 1、克隆项目到您的本地或服务器
# 使用ssh
git clone git@github.com:kuaifan/dootask.git
# 或者你也可以使用https
git clone https://github.com/kuaifan/dootask.git
# 通过github克隆项目
git clone --depth=1 https://github.com/kuaifan/dootask.git
# 或者你也可以使用gitee
git clone --depth=1 https://gitee.com/aipaw/dootask.git
# 2、进入目录
cd dootask
# 3、一键构建项目
# 3、一键安装项目(自定义端口安装 ./cmd install --port 2222
./cmd install
```
安装完毕,项目地址为:**`http://IP:PORT`**`PORT`默认为`2222`)。
### 默认账号
### 重置密码
```text
account: admin@dootask.com
password: 123456
```bash
# 重置默认管理员密码
./cmd repassword
```
### 更换端口
```bash
./cmd php bin/run --port=2222
./cmd up -d
./cmd port 2222
```
### 更换URL
```bash
# 此地址仅影响邮件回复功能
./cmd url {域名地址}
# 例如:
./cmd url https://domain.com
```
### 停止服务
@@ -51,18 +64,41 @@ password: 123456
./cmd start
```
### 开发编译
```bash
# 开发模式仅限macOS
./cmd dev
# 编译项目仅限macOS
./cmd prod
```
### 运行命令的快捷方式
```bash
# 你可以使用以下命令来执行
./cmd artisan "your command" // 运行 artisan 命令
./cmd php "your command" // 运行 php 命令
./cmd composer "your command" // 运行 composer 命令
./cmd supervisorctl "your command" // 运行 supervisorctl 命令
./cmd test "your command" // 运行 phpunit 命令
./cmd npm "your command" // 运行 npm 命令
./cmd yarn "your command" // 运行 yarn 命令
./cmd mysql "your command" // 运行 mysql 命令 (可以使用 `./cmd mysql bak` 命令来备份数据库)
./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
```bash
# 1、Nginx 代理配置添加
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、在项目下运行命令
./cmd https
```
## 升级更新
@@ -70,13 +106,41 @@ password: 123456
**注意:在升级之前请备份好你的数据!**
```bash
# 进入项目所在目录,运行一下命令即可
# 方法1在项目下运行命令
./cmd update
# 或者方法2如果方法1失败请使用此方法
git pull
./cmd mysql backup
./cmd uninstall
./cmd install
./cmd mysql recovery
```
如果升级后出现502请运行 `./cmd restart` 重启服务即可。
## 迁移项目
在新项目安装好之后按照以下步骤完成项目迁移:
1、备份原数据库
```bash
# 在旧的项目下运行命令
./cmd mysql backup
```
2、将`数据库备份文件`及`public/uploads`目录拷贝至新项目
3、还原数据库至新项目
```bash
# 在新的项目下运行命令
./cmd mysql recovery
```
## 卸载项目
```bash
# 进入项目所在目录,运行一下命令即可
# 在项目下运行命令
./cmd uninstall
```

28
README_PUBLISH.md Normal file
View File

@@ -0,0 +1,28 @@
# 发布说明
## 发布前
1. 添加环境变量 `APPLEID``APPLEIDPASS``CSC_LINK`
2. 发布GitHub还需要添加 `GH_PAT`
## 通过 GitHub Actions 发布
1. 执行 `./cmd prod` 编译
2. 执行 `node ./version.js` 制作版本
3. 执行 `git commit` 相关操作
4. 制作标签
5. 推送标签
## 本地发布
1. 执行 `./cmd prod` 编译
2. 执行 `node ./version.js` 制作版本
3. 执行 `git commit` 相关操作
4. 制作标签
5. 执行 `./cmd electron` 相关操作
## 编译App
1. 执行 `./cmd appbuild` 编译
2. 进入 `resources/mobile` eeui框架内打包Android或iOS应用

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@ namespace App\Exceptions;
use App\Module\Base;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Support\Facades\Log;
use Throwable;
class Handler extends ExceptionHandler
@@ -57,4 +58,18 @@ class Handler extends ExceptionHandler
}
return parent::render($request, $e);
}
/**
* 重写report优雅记录
* @param Throwable $e
* @throws Throwable
*/
public function report(Throwable $e)
{
if ($e instanceof ApiException) {
Log::error($e->getMessage(), ['exception' => ' at ' . $e->getFile() .':' . $e->getLine()]);
} else {
parent::report($e);
}
}
}

View File

@@ -10,8 +10,15 @@ if (!function_exists('asset_main')) {
if (!function_exists('seeders_at')) {
function seeders_at($data)
{
$diff = time() - strtotime("2021-07-01");
$diff = time() - strtotime("2021-07-02");
$time = strtotime($data) + $diff;
return date("Y-m-d H:i:s", $time);
}
}
if (!function_exists('md5s')) {
function md5s($val, $len = 16)
{
return substr(md5($val), 32 - $len);
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api;
use App\Models\File;
use App\Models\ProjectTask;
use App\Models\ProjectTaskFile;
use App\Models\User;
@@ -10,7 +11,10 @@ use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogMsgRead;
use App\Models\WebSocketDialogUser;
use App\Module\Base;
use Carbon\Carbon;
use DB;
use Request;
use Response;
/**
* @apiDefine dialog
@@ -20,31 +24,50 @@ use Request;
class DialogController extends AbstractController
{
/**
* 对话列表
* @api {get} api/dialog/lists 01. 对话列表
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName lists
*
* @apiParam {Number} [page] 当前页,默认:1
* @apiParam {Number} [pagesize] 每页显示数量,默认:100最大:200
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function lists()
{
$user = User::auth();
//
$list = WebSocketDialog::select(['web_socket_dialogs.*'])
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->where('u.userid', $user->userid)
->orderByDesc('u.top_at')
->orderByDesc('web_socket_dialogs.last_at')
->paginate(Base::getPaginate(200, 100));
$list->transform(function (WebSocketDialog $item) use ($user) {
return WebSocketDialog::formatData($item, $user->userid);
return $item->formatData($user->userid);
});
//
return Base::retSuccess('success', $list);
}
/**
* 获取单个会话信息
* @api {get} api/dialog/one 02. 获取单个会话信息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName one
*
* @apiParam {Number} dialog_id 对话ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function one()
{
@@ -52,22 +75,31 @@ class DialogController extends AbstractController
//
$dialog_id = intval(Request::input('dialog_id'));
//
$item = WebSocketDialog::select(['web_socket_dialogs.*'])
$item = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->where('web_socket_dialogs.id', $dialog_id)
->where('u.userid', $user->userid)
->first();
if ($item) {
$item = WebSocketDialog::formatData($item, $user->userid);
$item = $item->formatData($user->userid);
}
//
return Base::retSuccess('success', $item);
}
/**
* 打开会话
* @api {get} api/dialog/msg/user 03. 打开会话
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName open__user
*
* @apiParam {Number} userid 对话会员ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function open__user()
{
@@ -82,7 +114,7 @@ class DialogController extends AbstractController
if (empty($dialog)) {
return Base::retError('打开会话失败');
}
$data = WebSocketDialog::formatData(WebSocketDialog::find($dialog->id), $user->userid);
$data = WebSocketDialog::find($dialog->id)?->formatData($user->userid);
if (empty($data)) {
return Base::retError('打开会话错误');
}
@@ -90,12 +122,21 @@ class DialogController extends AbstractController
}
/**
* 获取消息列表
* @api {get} api/dialog/msg/lists 04. 获取消息列表
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__lists
*
* @apiParam {Number} dialog_id 对话ID
*
* @apiParam {Number} [page] 当前页,默认:1
* @apiParam {Number} [pagesize] 每页显示数量,默认:50最大:100
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__lists()
{
@@ -105,80 +146,146 @@ class DialogController extends AbstractController
//
$dialog = WebSocketDialog::checkDialog($dialog_id);
//
$list = WebSocketDialogMsg::whereDialogId($dialog_id)->orderByDesc('id')->paginate(Base::getPaginate(100, 50));
$list->transform(function (WebSocketDialogMsg $item) use ($user) {
$item->is_read = $item->userid === $user->userid || WebSocketDialogMsgRead::whereMsgId($item->id)->whereUserid($user->userid)->value('read_at');
return $item;
});
$list = WebSocketDialogMsg::select([
'web_socket_dialog_msgs.*',
'read.mention',
'read.read_at',
])->leftJoin('web_socket_dialog_msg_reads as read', function ($leftJoin) use ($user) {
$leftJoin
->on('read.userid', '=', DB::raw($user->userid))
->on('read.msg_id', '=', 'web_socket_dialog_msgs.id');
})->where('web_socket_dialog_msgs.dialog_id', $dialog_id)->orderByDesc('web_socket_dialog_msgs.id')->paginate(Base::getPaginate(100, 50));
//
if ($dialog->type == 'group' && $dialog->group_type == 'task') {
$user->task_dialog_id = $dialog->id;
$user->save();
}
//去掉标记未读
$isMarkDialogUser = WebSocketDialogUser::whereDialogId($dialog->id)->whereUserid($user->userid)->whereMarkUnread(1)->first();
if ($isMarkDialogUser) {
$isMarkDialogUser->mark_unread = 0;
$isMarkDialogUser->save();
}
//
$data = $list->toArray();
if ($list->currentPage() === 1) {
$data['dialog'] = WebSocketDialog::formatData($dialog, $user->userid);
$data['dialog'] = $dialog->formatData($user->userid);
}
return Base::retSuccess('success', $data);
}
/**
* 未读消息
* @api {get} api/dialog/msg/unread 05. 获取未读消息数量
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__unread
*
* @apiParam {Number} [dialog_id] 对话ID留空获取总未读消息数量
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__unread()
{
$unread = WebSocketDialogMsgRead::whereUserid(User::userid())->whereReadAt(null)->count();
$dialog_id = intval(Request::input('dialog_id'));
//
$builder = WebSocketDialogMsgRead::whereUserid(User::userid())->whereReadAt(null);
if ($dialog_id > 0) {
$builder->whereDialogId($dialog_id);
}
$unread = $builder->count();
return Base::retSuccess('success', [
'unread' => $unread,
]);
}
/**
* 发送消息
* @api {post} api/dialog/msg/sendtext 06. 发送消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__sendtext
*
* @apiParam {Number} dialog_id 对话ID
* @apiParam {String} text 消息内容
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__sendtext()
{
Base::checkClientVersion('0.13.33');
$user = User::auth();
//
$dialog_id = intval(Request::input('dialog_id'));
$text = trim(Request::input('text'));
$chat_nickname = Base::settingFind('system', 'chat_nickname');
if ($chat_nickname == 'required') {
$nickname = User::select(['nickname as nickname_original'])->whereUserid($user->userid)->value('nickname_original');
if (empty($nickname)) {
return Base::retError('请设置昵称', [], -2);
}
}
//
$dialog_id = Base::getPostInt('dialog_id');
$text = trim(Base::getPostValue('text'));
//
WebSocketDialog::checkDialog($dialog_id);
//
$text = WebSocketDialogMsg::formatMsg($text, $dialog_id);
if (mb_strlen($text) < 1) {
return Base::retError('消息内容不能为空');
} elseif (mb_strlen($text) > 20000) {
return Base::retError('消息内容最大不能超过20000字');
}
if (mb_strlen($text) > 2000) {
$array = mb_str_split($text, 2000);
} else {
$array = [$text];
}
//
WebSocketDialog::checkDialog($dialog_id);
$list = [];
foreach ($array as $item) {
$res = WebSocketDialogMsg::sendMsg($dialog_id, 'text', ['text' => $item], $user->userid);
if (Base::isSuccess($res)) {
$list[] = $res['data'];
}
}
//
$msg = [
'text' => $text
];
//
return WebSocketDialogMsg::sendMsg($dialog_id, 'text', $msg, $user->userid);
return Base::retSuccess('发送成功', $list);
}
/**
* {post}文件上传
* @api {post} api/dialog/msg/sendfile 07. 文件上传
*
* @apiParam {Number} dialog_id 对话ID
* @apiParam {String} [filename] post-文件名称
* @apiParam {String} [image64] post-base64图片二选一
* @apiParam {File} [files] post-文件对象(二选一)
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__sendfile
*
* @apiParam {Number} dialog_id 对话ID
* @apiParam {Number} [image_attachment] 图片是否也存到附件
* @apiParam {String} [filename] post-文件名称
* @apiParam {String} [image64] post-base64图片二选一
* @apiParam {File} [files] post-文件对象(二选一)
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__sendfile()
{
$user = User::auth();
//
$dialog_id = Base::getPostInt('dialog_id');
$image_attachment = Base::getPostInt('image_attachment');
//
$dialog = WebSocketDialog::checkDialog($dialog_id);
//
$path = "uploads/chat/" . $user->userid . "/";
$path = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
$image64 = Base::getPostValue('image64');
$fileName = Base::getPostValue('filename');
if ($image64) {
@@ -190,7 +297,7 @@ class DialogController extends AbstractController
} else {
$data = Base::upload([
"file" => Request::file('files'),
"type" => 'file',
"type" => 'more',
"path" => $path,
"fileName" => $fileName,
]);
@@ -203,8 +310,8 @@ class DialogController extends AbstractController
$fileData['thumb'] = Base::unFillUrl($fileData['thumb']);
$fileData['size'] *= 1024;
//
if ($dialog->type === 'group') {
if ($dialog->group_type === 'task') {
if ($dialog->type === 'group' && $dialog->group_type === 'task') { // 任务群组保存文件
if ($image_attachment || !in_array($fileData['ext'], File::imageExt)) { // 如果是图片不保存
$task = ProjectTask::whereDialogId($dialog->id)->first();
if ($task) {
$file = ProjectTaskFile::createInstance([
@@ -233,9 +340,18 @@ class DialogController extends AbstractController
}
/**
* 获取消息阅读情况
* @api {get} api/dialog/msg/readlist 08. 获取消息阅读情况
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__readlist
*
* @apiParam {Number} msg_id 消息ID需要是消息的发送人
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__readlist()
{
@@ -251,4 +367,424 @@ class DialogController extends AbstractController
$read = WebSocketDialogMsgRead::whereMsgId($msg_id)->get();
return Base::retSuccess('success', $read ?: []);
}
/**
* @api {get} api/dialog/msg/detail 09. 消息详情
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__detail
*
* @apiParam {Number} msg_id 消息ID
* @apiParam {String} only_update_at 仅获取update_at字段
* - no (默认)
* - yes
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__detail()
{
User::auth();
//
$msg_id = intval(Request::input('msg_id'));
$only_update_at = Request::input('only_update_at', 'no');
//
$dialogMsg = WebSocketDialogMsg::whereId($msg_id)->first();
if (empty($dialogMsg)) {
return Base::retError("文件不存在");
}
//
if ($only_update_at == 'yes') {
return Base::retSuccess('success', [
'id' => $dialogMsg->id,
'update_at' => Carbon::parse($dialogMsg->updated_at)->toDateTimeString()
]);
}
//
$data = $dialogMsg->toArray();
//
if ($data['type'] == 'file') {
$msg = Base::json2array($dialogMsg->getRawOriginal('msg'));
$msg = File::formatFileData($msg);
$data['content'] = $msg['content'];
$data['file_mode'] = $msg['file_mode'];
}
//
return Base::retSuccess('success', $data);
}
/**
* @api {get} api/dialog/msg/download 10. 文件下载
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__download
*
* @apiParam {Number} msg_id 消息ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__download()
{
User::auth();
//
$msg_id = intval(Request::input('msg_id'));
//
$msg = WebSocketDialogMsg::whereId($msg_id)->first();
if (empty($msg)) {
abort(403, "This file not exist.");
}
if ($msg->type != 'file') {
abort(403, "This file not support download.");
}
$array = Base::json2array($msg->getRawOriginal('msg'));
//
return Response::download(public_path($array['path']), $array['name']);
}
/**
* @api {get} api/dialog/msg/withdraw 11. 聊天消息撤回
*
* @apiDescription 消息撤回限制24小时内需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__withdraw
*
* @apiParam {Number} msg_id 消息ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__withdraw()
{
$user = User::auth();
$msg_id = intval(Request::input("msg_id"));
$msg = WebSocketDialogMsg::whereId($msg_id)->whereUserid($user->userid)->first();
if (empty($msg)) {
return Base::retError("消息不存在或已被删除");
}
$msg->deleteMsg();
return Base::retSuccess("success");
}
/**
* @api {get} api/dialog/top 12. 会话置顶
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName top
*
* @apiParam {Number} dialog_id 会话ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function top()
{
$user = User::auth();
$dialogId = intval(Request::input('dialog_id'));
$dialogUser = WebSocketDialogUser::whereUserid($user->userid)->whereDialogId($dialogId)->first();
if (!$dialogUser) {
return Base::retError("会话不存在");
}
$dialogUser->top_at = $dialogUser->top_at ? null : Carbon::now();
$dialogUser->save();
return Base::retSuccess("success", [
'id' => $dialogUser->dialog_id,
'top_at' => $dialogUser->top_at?->toDateTimeString(),
]);
}
/**
* @api {get} api/dialog/msg/mark 13. 消息标记操作
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__mark
*
* @apiParam {Number} dialog_id 会话ID
* @apiParam {String} type 类型
* - read
* - unread
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__mark()
{
$user = User::auth();
$dialogId = intval(Request::input('dialog_id'));
$type = Request::input('type');
$dialogUser = WebSocketDialogUser::whereUserid($user->userid)->whereDialogId($dialogId)->first();
if (!$dialogUser) {
return Base::retError("会话不存在");
}
switch ($type) {
case 'read':
WebSocketDialogMsgRead::whereUserid($user->userid)
->whereReadAt(null)
->whereDialogId($dialogId)
->chunkById(100, function ($list) {
WebSocketDialogMsgRead::onlyMarkRead($list);
});
$dialogUser->mark_unread = 0;
$dialogUser->save();
break;
case 'unread':
$dialogUser->mark_unread = 1;
$dialogUser->save();
break;
default:
return Base::retError("参数错误");
}
return Base::retSuccess("success", [
'id' => $dialogId,
'mark_unread' => $dialogUser->mark_unread,
]);
}
/**
* @api {get} api/dialog/group/add 14. 新增群组
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName group__add
*
* @apiParam {Array} userids 群成员,格式: [userid1, userid2, userid3]
* @apiParam {String} chat_name 群名称
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function group__add()
{
$user = User::auth();
//
$userids = Request::input('userids');
$chatName = trim(Request::input('chat_name'));
//
if (!is_array($userids)) {
return Base::retError('请选择群成员');
}
$userids = array_merge([$user->userid], $userids);
$userids = array_values(array_filter(array_unique($userids)));
if (count($userids) < 2) {
return Base::retError('群成员至少2人');
}
//
if (empty($chatName)) {
$array = [];
foreach ($userids as $userid) {
$array[] = User::userid2nickname($userid);
if (count($array) >= 8 || strlen(implode(", ", $array)) > 100) {
$array[] = "...";
break;
}
}
$chatName = implode(", ", $array);
}
$dialog = WebSocketDialog::createGroup($chatName, $userids, 'user', $user->userid);
if (empty($dialog)) {
return Base::retError('创建群组失败');
}
$data = WebSocketDialog::find($dialog->id)?->formatData($user->userid);
$dialog->pushMsg("groupAdd", $data, $userids);
return Base::retSuccess('创建成功', $data);
}
/**
* @api {get} api/dialog/group/edit 15. 修改群组
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName group__edit
*
* @apiParam {Number} dialog_id 会话ID
* @apiParam {String} chat_name 群名称
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function group__edit()
{
$user = User::auth();
//
$dialog_id = intval(Request::input('dialog_id'));
$chatName = trim(Request::input('chat_name'));
//
if (mb_strlen($chatName) < 2) {
return Base::retError('群名称至少2个字');
}
if (mb_strlen($chatName) > 100) {
return Base::retError('群名称最长限制100个字');
}
//
$dialog = WebSocketDialog::checkDialog($dialog_id, true);
//
$dialog->name = $chatName;
$dialog->save();
return Base::retSuccess('修改成功', [
'id' => $dialog->id,
'name' => $dialog->name,
]);
}
/**
* @api {get} api/dialog/group/user 16. 获取群成员
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName group__user
*
* @apiParam {Number} dialog_id 会话ID
* @apiParam {Number} [getuser] 获取会员详情1: 返回会员昵称、邮箱等基本信息0: 默认不返回)
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function group__user()
{
User::auth();
//
$dialog_id = intval(Request::input('dialog_id'));
$getuser = intval(Request::input('getuser', 0));
//
$dialog = WebSocketDialog::checkDialog($dialog_id);
//
$data = $dialog->dialogUser->toArray();
if ($getuser === 1) {
$array = [];
foreach ($data as $item) {
$res = User::userid2basic($item['userid']);
if ($res) {
$array[] = array_merge($item, $res->toArray());
}
}
$data = $array;
}
return Base::retSuccess('success', $data);
}
/**
* @api {get} api/dialog/group/adduser 17. 添加群成员
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName group__adduser
*
* @apiParam {Number} dialog_id 会话ID
* @apiParam {Array} userids 新增的群成员,格式: [userid1, userid2, userid3]
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function group__adduser()
{
$user = User::auth();
//
$dialog_id = intval(Request::input('dialog_id'));
$userids = Request::input('userids');
//
if (!is_array($userids)) {
return Base::retError('请选择群成员');
}
//
$dialog = WebSocketDialog::checkDialog($dialog_id, true);
//
$dialog->checkGroup();
$dialog->joinGroup($userids);
$dialog->pushMsg("groupJoin", $dialog->formatData($user->userid), $userids);
return Base::retSuccess('添加成功');
}
/**
* @api {get} api/dialog/group/deluser 18. 移出(退出)群成员
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName group__adduser
*
* @apiParam {Number} dialog_id 会话ID
* @apiParam {Array} userids 移出的群成员,格式: [userid1, userid2, userid3]
* - 留空表示自己退出
* - 有值表示移出,仅限群主操作
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function group__deluser()
{
$user = User::auth();
//
$dialog_id = intval(Request::input('dialog_id'));
$userids = Request::input('userids');
//
$type = 'remove';
if (empty($userids)) {
$type = 'exit';
$userids = [$user->userid];
}
//
if (!is_array($userids)) {
return Base::retError('请选择群成员');
}
//
$dialog = WebSocketDialog::checkDialog($dialog_id, $type === 'remove');
//
$dialog->checkGroup();
$dialog->exitGroup($userids);
$dialog->pushMsg("groupExit", null, $userids);
return Base::retSuccess($type === 'remove' ? '移出成功' : '退出成功');
}
/**
* @api {get} api/dialog/group/disband 19. 解散群组
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName group__disband
*
* @apiParam {Number} dialog_id 会话ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function group__disband()
{
User::auth();
//
$dialog_id = intval(Request::input('dialog_id'));
//
$dialog = WebSocketDialog::checkDialog($dialog_id, true);
//
$dialog->checkGroup();
$dialog->deleteDialog();
$dialog->pushMsg("groupDelete");
return Base::retSuccess('解散成功');
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,474 @@
<?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\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] 汇报ID
* @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
{
$input = [
"id" => Base::getPostValue("id", 0),
"title" => Base::getPostValue("title"),
"type" => Base::getPostValue("type"),
"content" => Base::getPostValue("content"),
"receive" => Base::getPostValue("receive"),
// 以当前日期为基础的周期偏移量。例如选择了上一周那么就是 -1上一天同理。
"offset" => Base::getPostValue("offset", 0),
];
$validator = Validator::make($input, [
'id' => 'numeric',
'title' => 'required',
'type' => ['required', Rule::in([Report::WEEKLY, Report::DAILY])],
'content' => 'required',
'receive' => 'required',
'offset' => ['numeric', 'max:0'],
], [
'id.numeric' => 'ID只能是数字',
'title.required' => '请填写标题',
'type.required' => '请选择汇报类型',
'type.in' => '汇报类型错误',
'content.required' => '请填写汇报内容',
'receive.required' => '请选择接收人',
'offset.numeric' => '工作汇报周期格式错误,只能是数字',
'offset.max' => '只能提交当天/本周或者之前的的工作汇报',
]);
if ($validator->fails())
return Base::retError($validator->errors()->first());
$user = User::auth();
// 接收人
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 = Report::generateSign($input["type"], $input["offset"]);
// 检查唯一标识是否存在
if (empty($input["id"])) {
if (Report::query()->whereSign($sign)->whereType($input["type"])->count() > 0)
throw new ApiException("请勿重复提交工作汇报");
}
$report = Report::createInstance([
"title" => $input["title"],
"type" => $input["type"],
"content" => htmlspecialchars($input["content"]),
"userid" => $user->userid,
"sign" => $sign,
]);
}
$report->save();
if (!empty($input["receive_content"])) {
// 删除关联
$report->Receives()->delete();
// 保存接收人
$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', [
"content" => $one->content,
"title" => $one->title,
"id" => $one->id,
]);
}
// 已完成的任务
$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>[' . Base::Lang('周' . ['日', '一', '二', '三', '四', '五', '六'][$complete_at->dayOfWeek]) . ']</span>&nbsp;') : '';
$completeContent .= '<li>' . $pre . $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;">[' . Base::Lang('超期') . ']</span>&nbsp;' : '';
$unfinishedContent .= '<li>' . $pre . $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") . "]";
}
$data = [
"time" => $start_time->toDateTimeString(),
"complete_task" => $complete_task,
"unfinished_task" => $unfinished_task,
"content" => '<h2>' . Base::Lang('已完成工作') . '</h2><ol>' .
$completeContent . '</ol><h2>' .
Base::Lang('未完成的工作') . '</h2><ol>' .
$unfinishedContent . '</ol>',
"title" => $title,
];
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/last_submitter 06. 获取最后一次提交的接收人
*
* @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 07. 获取未读
*
* @apiVersion 1.0.0
* @apiGroup report
* @apiName unread
*
* @apiParam {Number} [userid] 用户id
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function unread(): array
{
$userid = intval(trim(Request::input("userid")));
$user = empty($userid) ? User::auth() : User::find($userid);
$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 08. 标记汇报已读,可批量
*
* @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 = explode(",", $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);
}
}

View File

@@ -4,6 +4,8 @@ namespace App\Http\Controllers\Api;
use App\Models\User;
use App\Module\Base;
use Guanguans\Notify\Factory;
use Guanguans\Notify\Messages\EmailMessage;
use Request;
use Response;
@@ -24,7 +26,8 @@ class SystemController extends AbstractController
*
* @apiParam {String} type
* - get: 获取(默认)
* - save: 保存设置参数reg、login_code
* - all: 获取所有(需要管理员权限
* - save: 保存设置(参数:['reg', 'reg_invite', 'login_code', 'password_policy', 'project_invite', 'chat_nickname', 'auto_archived', 'archived_day', 'start_home', 'home_footer']
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -37,32 +40,199 @@ class SystemController extends AbstractController
if (env("SYSTEM_SETTING") == 'disabled') {
return Base::retError('当前环境禁止修改');
}
$user = User::auth();
$user->isAdmin();
User::auth('admin');
$all = Request::input();
foreach ($all AS $key => $value) {
if (!in_array($key, ['reg', 'login_code'])) {
if (!in_array($key, [
'reg',
'reg_invite',
'login_code',
'password_policy',
'project_invite',
'chat_nickname',
'auto_archived',
'archived_day',
'start_home',
'home_footer'
])) {
unset($all[$key]);
}
}
$all['archived_day'] = floatval($all['archived_day']);
if ($all['auto_archived'] == 'open') {
if ($all['archived_day'] <= 0) {
return Base::retError('自动归档时间不可小于1天');
} elseif ($all['archived_day'] > 100) {
return Base::retError('自动归档时间不可大于100天');
}
}
$setting = Base::setting('system', Base::newTrim($all));
} else {
$setting = Base::setting('system');
}
//
if ($type == 'all' || $type == 'save') {
User::auth('admin');
$setting['reg_invite'] = $setting['reg_invite'] ?: Base::generatePassword(8);
} else {
if (isset($setting['reg_invite'])) unset($setting['reg_invite']);
}
//
$setting['reg'] = $setting['reg'] ?: 'open';
$setting['login_code'] = $setting['login_code'] ?: 'auto';
$setting['password_policy'] = $setting['password_policy'] ?: 'simple';
$setting['project_invite'] = $setting['project_invite'] ?: 'open';
$setting['chat_nickname'] = $setting['chat_nickname'] ?: 'optional';
$setting['auto_archived'] = $setting['auto_archived'] ?: 'close';
$setting['archived_day'] = floatval($setting['archived_day']) ?: 7;
$setting['start_home'] = $setting['start_home'] ?: 'close';
//
return Base::retSuccess('success', $setting ?: json_decode('{}'));
}
/**
* @api {post} api/system/priority 02. 获取优先级、保存优先级
* @api {get} api/system/setting/email 02. 获取邮箱设置、保存邮箱设置(限管理员)
*
* @apiVersion 1.0.0
* @apiGroup system
* @apiName setting__email
*
* @apiParam {String} type
* - get: 获取(默认)
* - save: 保存设置(参数:['smtp_server', 'port', 'account', 'password', 'reg_verify', 'notice', 'task_start_minute', 'task_remind_hours', 'task_remind_hours2', 'notice_msg', 'msg_unread_user_minute', 'msg_unread_group_minute']
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function setting__email()
{
User::auth('admin');
//
$type = trim(Request::input('type'));
if ($type == 'save') {
if (env("SYSTEM_SETTING") == 'disabled') {
return Base::retError('当前环境禁止修改');
}
$all = Request::input();
foreach ($all as $key => $value) {
if (!in_array($key, [
'smtp_server',
'port',
'account',
'password',
'reg_verify',
'notice',
'task_start_minute',
'task_remind_hours',
'task_remind_hours2',
'notice_msg',
'msg_unread_user_minute',
'msg_unread_group_minute'
])) {
unset($all[$key]);
}
}
$setting = Base::setting('emailSetting', Base::newTrim($all));
} else {
$setting = Base::setting('emailSetting');
}
//
$setting['smtp_server'] = $setting['smtp_server'] ?: '';
$setting['port'] = $setting['port'] ?: '';
$setting['account'] = $setting['account'] ?: '';
$setting['password'] = $setting['password'] ?: '';
$setting['reg_verify'] = $setting['reg_verify'] ?: 'close';
$setting['notice'] = $setting['notice'] ?: 'close';
$setting['task_start_minute'] = intval($setting['task_start_minute'] ?? -1);
$setting['task_remind_hours'] = floatval($setting['task_remind_hours'] ?? -1);
$setting['task_remind_hours2'] = floatval($setting['task_remind_hours2'] ?? -1);
$setting['notice_msg'] = $setting['notice_msg'] ?: 'close';
$setting['msg_unread_user_minute'] = intval($setting['msg_unread_user_minute'] ?? -1);
$setting['msg_unread_group_minute'] = intval($setting['msg_unread_group_minute'] ?? -1);
//
return Base::retSuccess('success', $setting ?: json_decode('{}'));
}
/**
* @api {get} api/system/setting/apppush 03. 获取APP推送设置、保存APP推送设置限管理员
*
* @apiVersion 1.0.0
* @apiGroup system
* @apiName setting__apppush
*
* @apiParam {String} type
* - get: 获取(默认)
* - save: 保存设置(参数:['push', 'ios_key', 'ios_secret', 'android_key', 'android_secret']
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function setting__apppush()
{
User::auth('admin');
//
$type = trim(Request::input('type'));
if ($type == 'save') {
if (env("SYSTEM_SETTING") == 'disabled') {
return Base::retError('当前环境禁止修改');
}
$all = Request::input();
foreach ($all as $key => $value) {
if (!in_array($key, [
'push',
'ios_key',
'ios_secret',
'android_key',
'android_secret'
])) {
unset($all[$key]);
}
}
$setting = Base::setting('appPushSetting', Base::newTrim($all));
} else {
$setting = Base::setting('appPushSetting');
}
//
$setting['push'] = $setting['push'] ?: 'close';
//
return Base::retSuccess('success', $setting ?: json_decode('{}'));
}
/**
* @api {get} api/system/demo 04. 获取演示帐号
*
* @apiVersion 1.0.0
* @apiGroup system
* @apiName demo
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function demo()
{
$demo_account = env('DEMO_ACCOUNT');
$demo_password = env('DEMO_PASSWORD');
if (empty($demo_account) || empty($demo_password)) {
return Base::retError('No demo account');
}
return Base::retSuccess('success', [
'account' => $demo_account,
'password' => $demo_password,
]);
}
/**
* @api {post} api/system/priority 05. 任务优先级
*
* @apiDescription 获取任务优先级、保存任务优先级
* @apiVersion 1.0.0
* @apiGroup system
* @apiName priority
*
* @apiParam {String} type
* - get: 获取(默认)
* - save: 保存(限管理员)
* @apiParam {Array} list 优先级数据,格式:[{name,color,days,priority}]
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
@@ -73,15 +243,14 @@ class SystemController extends AbstractController
{
$type = trim(Request::input('type'));
if ($type == 'save') {
$user = User::auth();
$user->isAdmin();
User::auth('admin');
$list = Base::getPostValue('list');
$array = [];
if (empty($list) || !is_array($list)) {
return Base::retError('参数错误');
}
foreach ($list AS $item) {
if (empty($item['name']) || empty($item['color']) || empty($item['days']) || empty($item['priority'])) {
if (empty($item['name']) || empty($item['color']) || empty($item['priority'])) {
continue;
}
$array[] = [
@@ -103,7 +272,54 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/info 03. 获取终端详细信息
* @api {post} api/system/column/template 06. 创建项目模板
*
* @apiDescription 获取创建项目模板、保存创建项目模板
* @apiVersion 1.0.0
* @apiGroup system
* @apiName column__template
*
* @apiParam {String} type
* - get: 获取(默认)
* - save: 保存(限管理员)
* @apiParam {Array} list 优先级数据,格式:[{name,columns}]
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function column__template()
{
$type = trim(Request::input('type'));
if ($type == 'save') {
User::auth('admin');
$list = Base::getPostValue('list');
$array = [];
if (empty($list) || !is_array($list)) {
return Base::retError('参数错误');
}
foreach ($list AS $item) {
if (empty($item['name']) || empty($item['columns'])) {
continue;
}
$array[] = [
'name' => $item['name'],
'columns' => array_values(array_filter(array_unique(explode(",", $item['columns']))))
];
}
if (empty($array)) {
return Base::retError('参数为空');
}
$setting = Base::setting('columnTemplate', $array);
} else {
$setting = Base::setting('columnTemplate');
}
//
return Base::retSuccess('success', $setting);
}
/**
* @api {get} api/system/get/info 07. 获取终端详细信息
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -132,7 +348,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/ip 04. 获取IP地址
* @api {get} api/system/get/ip 08. 获取IP地址
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -147,7 +363,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/cnip 05. 是否中国IP地址
* @api {get} api/system/get/cnip 09. 是否中国IP地址
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -164,7 +380,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/ipgcj02 06. 获取IP地址经纬度
* @api {get} api/system/get/ipgcj02 10. 获取IP地址经纬度
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -181,7 +397,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/ipinfo 07. 获取IP地址详细信息
* @api {get} api/system/get/ipinfo 11. 获取IP地址详细信息
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -198,90 +414,22 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/appinfo 08. 获取应用下载信息
*
* @apiVersion 1.0.0
* @apiGroup system
* @apiName get__appinfo
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function get__appinfo() {
$array = [
'name' => '',
'version' => '',
'list' => [],
];
//
$file = base_path("electron/package.json");
$dist = base_path("electron/dist");
if (file_exists($file)) {
$packageArray = json_decode(file_get_contents($file), true);
$array['name'] = $packageArray['name'] ?? 'No app';
$array['version'] = $packageArray['version'] ?? '';
//
$list = [
[
'icon' => 'logo-apple',
'name' => 'macOS Intel',
'file' => "{$array['name']}-{$array['version']}.dmg"
],
[
'icon' => 'logo-apple',
'name' => 'macOS M1',
'file' => "{$array['name']}-{$array['version']}-arm64.dmg"
],
[
'icon' => 'logo-windows',
'name' => 'Windows x64',
'file' => "{$array['name']} Setup {$array['version']}.exe"
]
];
foreach ($list as $item) {
if (file_exists("{$dist}/{$item['file']}")) {
$item['url'] = Base::fillUrl('api/system/get/appdown?file=' . urlencode($item['file']));
$item['size'] = filesize("{$dist}/{$item['file']}");
$array['list'][] = $item;
}
}
}
//
if (count($array['list']) == 0) {
return Base::retError('No file');
}
return Base::retSuccess('success', $array);
}
/**
* @api {get} api/system/get/appdown 09. 下载应用
*
* @apiVersion 1.0.0
* @apiGroup system
* @apiName get__appdown
*
* @apiParam {String} file 文件名称
*/
public function get__appdown() {
$file = Request::input("file");
$path = base_path("electron/dist/" . $file);
if (!file_exists($path)) {
return Base::ajaxError("No file");
}
return Response::download($path);
}
/**
* @api {post} api/system/imgupload 10. 上传图片
* @api {post} api/system/imgupload 12. 上传图片
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup system
* @apiName imgupload
*
* @apiParam {String} image64 图片base64
* @apiParam {String} filename 文件名
* @apiParam {File} image post-图片对象
* @apiParam {String} [image64] post-图片base64与'image'二选一)
* @apiParam {String} filename post-文件名
* @apiParam {Number} [width] 压缩图片宽默认0
* @apiParam {Number} [height] 压缩图片高默认0
* @apiParam {String} [whcut] 压缩方式
* - 1裁切默认宽、高非0有效
* - 0缩放
* - -1或'auto':保持等比裁切
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -292,11 +440,14 @@ class SystemController extends AbstractController
if (User::userid() === 0) {
return Base::retError('身份失效,等重新登录');
}
$scale = [intval(Request::input('width')), intval(Request::input('height'))];
if (!$scale[0] && !$scale[1]) {
$scale = [2160, 4160, -1];
$width = intval(Request::input('width'));
$height = intval(Request::input('height'));
$whcut = intval(Request::input('whcut', 1));
$scale = [2160, 4160, -1];
if ($width > 0 || $height > 0) {
$scale = [$width, $height, $whcut];
}
$path = "uploads/picture/" . User::userid() . "/" . date("Ym") . "/";
$path = "uploads/user/picture/" . User::userid() . "/" . date("Ym") . "/";
$image64 = trim(Base::getPostValue('image64'));
$fileName = trim(Base::getPostValue('filename'));
if ($image64) {
@@ -323,7 +474,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/imgview 11. 浏览图片空间
* @api {get} api/system/get/imgview 13. 浏览图片空间
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -341,7 +492,7 @@ class SystemController extends AbstractController
if (User::userid() === 0) {
return Base::retError('身份失效,等重新登录');
}
$publicPath = "uploads/picture/" . User::userid() . "/";
$publicPath = "uploads/user/picture/" . User::userid() . "/";
$dirPath = public_path($publicPath);
$dirs = $files = [];
//
@@ -419,7 +570,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/fileupload 12. 上传文件
* @api {post} api/system/fileupload 14. 上传文件
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -439,7 +590,7 @@ class SystemController extends AbstractController
if (User::userid() === 0) {
return Base::retError('身份失效,等重新登录');
}
$path = "uploads/files/" . User::userid() . "/" . date("Ym") . "/";
$path = "uploads/user/file/" . User::userid() . "/" . date("Ym") . "/";
$image64 = trim(Base::getPostValue('image64'));
$fileName = trim(Base::getPostValue('filename'));
if ($image64) {
@@ -459,4 +610,66 @@ class SystemController extends AbstractController
//
return $data;
}
/**
* @api {get} api/system/get/starthome 15. 启动首页设置信息
*
* @apiDescription 用于判断注册是否需要启动首页
* @apiVersion 1.0.0
* @apiGroup system
* @apiName get__starthome
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function get__starthome()
{
return Base::retSuccess('success', [
'need_start' => Base::settingFind('system', 'start_home') == 'open',
'home_footer' => Base::settingFind('system', 'home_footer')
]);
}
/**
* @api {get} api/system/email/check 16. 邮件发送测试(限管理员)
*
* @apiDescription 测试配置邮箱是否能发送邮件
* @apiVersion 1.0.0
* @apiGroup system
* @apiName email__check
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function email__check()
{
User::auth('admin');
//
$all = Request::input();
if (!Base::isEmail($all['to'])) {
return Base::retError('请输入正确的收件人地址');
}
try {
Factory::mailer()
->setDsn("smtp://{$all['account']}:{$all['password']}@{$all['smtp_server']}:{$all['port']}?verify_peer=0")
->setMessage(EmailMessage::create()
->from(env('APP_NAME', 'Task') . " <{$all['account']}>")
->to($all['to'])
->subject('Mail sending test')
->html('<p>收到此电子邮件意味着您的邮箱配置正确。</p><p>Receiving this email means that your mailbox is configured correctly.</p>'))
->send();
return Base::retSuccess('成功发送');
} catch (\Throwable $e) {
// 一般是请求超时
if (str_contains($e->getMessage(), "Timed Out")) {
return Base::retError("language.TimedOut");
} elseif ($e->getCode() === 550) {
return Base::retError('邮件内容被拒绝,请检查邮箱是否开启接收功能');
} else {
return Base::retError($e->getMessage());
}
}
}
}

View File

@@ -2,7 +2,11 @@
namespace App\Http\Controllers\Api;
use App\Models\AbstractModel;
use App\Models\UmengAlias;
use App\Models\User;
use App\Models\UserEmailVerification;
use App\Models\UserTransfer;
use App\Module\Base;
use Arr;
use Cache;
@@ -31,7 +35,7 @@ class UsersController extends AbstractController
* @apiParam {String} email 邮箱
* @apiParam {String} password 密码
* @apiParam {String} [code] 登录验证码
* @apiParam {String} [key] 登陆验证码key
* @apiParam {String} [invite] 注册邀请码
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -42,49 +46,56 @@ class UsersController extends AbstractController
$type = trim(Request::input('type'));
$email = trim(Request::input('email'));
$password = trim(Request::input('password'));
$isRegVerify = Base::settingFind('emailSetting', 'reg_verify') === 'open';
if ($type == 'reg') {
$setting = Base::setting('system');
if ($setting['reg'] == 'close') {
return Base::retError('未开放注册');
} elseif ($setting['reg'] == 'invite') {
$invite = trim(Request::input('invite'));
if (empty($invite) || $invite != $setting['reg_invite']) {
return Base::retError('请输入正确的邀请码');
}
}
$user = User::reg($email, $password);
if ($isRegVerify) {
UserEmailVerification::userEmailSend($user);
return Base::retError('注册成功,请验证邮箱后登录', ['code' => 'email']);
}
} else {
$needCode = !Base::isError(User::needCode($email));
if ($needCode) {
$code = trim(Request::input('code'));
$key = trim(Request::input('key'));
if (empty($code)) {
return Base::retError('请输入验证码', ['code' => 'need']);
}
if (empty($key)) {
if (!Captcha::check($code)) {
return Base::retError('请输入正确的验证码', ['code' => 'need']);
}
} else {
if (!Captcha::check_api($code, $key)) {
return Base::retError('请输入正确的验证码', ['code' => 'need']);
}
if (!Captcha::check($code)) {
return Base::retError('请输入正确的验证码', ['code' => 'need']);
}
}
//
$retError = function ($msg) use ($email) {
Cache::forever("code::" . $email, "need");
$needCode = !Base::isError(User::needCode($email));
$needData = [ 'code' => $needCode ? 'need' : 'no' ];
$needData = ['code' => $needCode ? 'need' : 'no'];
return Base::retError($msg, $needData);
};
$user = User::whereEmail($email)->first();
if (empty($user)) {
return $retError('号或密码错误');
return $retError('号或密码错误');
}
if ($user->password != Base::md52($password, $user->encrypt)) {
return $retError('号或密码错误');
return $retError('号或密码错误');
}
//
if (in_array('disable', $user->identity)) {
return $retError('帐号已停用...');
}
Cache::forget("code::" . $email);
if ($isRegVerify && $user->email_verity === 0) {
UserEmailVerification::userEmailSend($user);
return Base::retError('您还没有验证邮箱,请先登录邮箱通过验证邮件验证邮箱', ['code' => 'email']);
}
}
//
$array = [
@@ -127,8 +138,6 @@ class UsersController extends AbstractController
* @apiGroup users
* @apiName login__codeimg
*
* @apiParam {String} email 用户名
*
* @apiSuccess {Image} data 返回数据(直接输出图片)
*/
public function login__codeimg()
@@ -155,7 +164,26 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/info 05. 获取我的信息
* @api {get} api/users/reg/needinvite 05. 是否需要邀请码
*
* @apiDescription 用于判断注册是否需要邀请码
* @apiVersion 1.0.0
* @apiGroup users
* @apiName reg__needinvite
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function reg__needinvite()
{
return Base::retSuccess('success', [
'need' => Base::settingFind('system', 'reg') == 'invite'
]);
}
/**
* @api {get} api/users/info 06. 获取我的信息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -175,9 +203,9 @@ class UsersController extends AbstractController
"userimg": "",
"login_num": 10,
"changepass": 0,
"last_ip": "10.22.22.1",
"last_ip": "127.0.0.1",
"last_at": "2021-06-01 12:00:00",
"line_ip": "10.22.22.1",
"line_ip": "127.0.0.1",
"line_at": "2021-06-01 12:00:00",
"created_ip": "",
}
@@ -187,11 +215,13 @@ class UsersController extends AbstractController
$user = User::auth();
User::token($user);
//
return Base::retSuccess('success', $user);
$data = $user->toArray();
$data['nickname_original'] = $user->getRawOriginal('nickname');
return Base::retSuccess('success', $data);
}
/**
* @api {get} api/users/editdata 06. 修改自己的资料
* @api {get} api/users/editdata 07. 修改自己的资料
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -210,6 +240,7 @@ class UsersController extends AbstractController
{
$user = User::auth();
$data = Request::all();
$user->checkSystem(1);
//头像
if (Arr::exists($data, 'userimg')) {
$userimg = Request::input('userimg');
@@ -217,13 +248,13 @@ class UsersController extends AbstractController
$userimg = is_array($userimg) ? $userimg[0]['path'] : $userimg;
$user->userimg = Base::unFillUrl($userimg);
} else {
$user->userimg = '';
$user->userimg = $user->getUserimgAttribute(null);
}
}
//昵称
if (Arr::exists($data, 'nickname')) {
$nickname = trim(Request::input('nickname'));
if (mb_strlen($nickname) < 2) {
if ($nickname && mb_strlen($nickname) < 2) {
return Base::retError('昵称不可以少于2个字');
} elseif (mb_strlen($nickname) > 20) {
return Base::retError('昵称最多只能设置20个字');
@@ -234,7 +265,7 @@ class UsersController extends AbstractController
//职位/职称
if (Arr::exists($data, 'profession')) {
$profession = trim(Request::input('profession'));
if (mb_strlen($profession) < 2) {
if ($profession && mb_strlen($profession) < 2) {
return Base::retError('职位/职称不可以少于2个字');
} elseif (mb_strlen($profession) > 20) {
return Base::retError('职位/职称最多只能设置20个字');
@@ -250,7 +281,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/editpass 07. 修改自己的密码
* @api {get} api/users/editpass 08. 修改自己的密码
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -267,26 +298,14 @@ class UsersController extends AbstractController
public function editpass()
{
$user = User::auth();
$user->checkSystem();
//
$oldpass = trim(Request::input('oldpass'));
$newpass = trim(Request::input('newpass'));
if (strlen($newpass) < 6) {
return Base::retError('密码设置不能小于6位数');
} elseif (strlen($newpass) > 32) {
return Base::retError('密码最多只能设置32位数');
}
if ($oldpass == $newpass) {
return Base::retError('新旧密码一致');
}
//
if (env("PASSWORD_ADMIN") == 'disabled') {
if ($user->userid == 1) {
return Base::retError('当前环境禁止修改密码');
}
}
if (env("PASSWORD_OWNER") == 'disabled') {
return Base::retError('当前环境禁止修改密码');
}
User::passwordPolicy($newpass);
//
$verify = User::whereUserid($user->userid)->wherePassword(Base::md52($oldpass, User::token2encrypt()))->count();
if (empty($verify)) {
@@ -302,7 +321,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/search 08. 搜索会员列表
* @api {get} api/users/search 09. 搜索会员列表
*
* @apiDescription 搜索会员列表
* @apiVersion 1.0.0
@@ -310,10 +329,16 @@ class UsersController extends AbstractController
* @apiName searchinfo
*
* @apiParam {Object} keys 搜索条件
* - keys.key 昵称、邮箱
* - keys.project_id 在指定项目ID
* - keys.no_project_id 在指定项目ID
* - keys.key 昵称、邮箱关键字
* - keys.disable 0-排除禁止默认1-含禁止2-仅禁止
* - keys.project_id 在指定项目ID
* - keys.no_project_id 不在指定项目ID
* @apiParam {Object} sorts 排序方式
* - sorts.az 按字母asc|desc
*
* @apiParam {Number} [take] 获取数量10-100
* @apiParam {Number} [page] 当前页,默认:1赋值分页模式take参数无效
* @apiParam {Number} [pagesize] 每页显示数量,默认:10最大:100
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -324,38 +349,52 @@ class UsersController extends AbstractController
$builder = User::select(['userid', 'email', 'nickname', 'profession', 'userimg', 'az']);
//
$keys = Request::input('keys');
if (is_array($keys)) {
if ($keys['key']) {
$builder->where(function($query) use ($keys) {
$query->where("email", "like", "%{$keys['key']}%")
->orWhere("nickname", "like", "%{$keys['key']}%");
});
}
if (intval($keys['project_id']) > 0) {
$builder->whereIn('userid', function ($query) use ($keys) {
$query->select('userid')->from('project_users')->where('project_id', $keys['project_id']);
});
}
if (intval($keys['no_project_id']) > 0) {
$builder->whereNotIn('userid', function ($query) use ($keys) {
$query->select('userid')->from('project_users')->where('project_id', $keys['no_project_id']);
});
}
$sorts = Request::input('sorts');
$keys = is_array($keys) ? $keys : [];
$sorts = is_array($sorts) ? $sorts : [];
//
if ($keys['key']) {
$builder->where(function($query) use ($keys) {
$query->where("email", "like", "%{$keys['key']}%")
->orWhere("nickname", "like", "%{$keys['key']}%");
});
}
if (intval($keys['disable']) == 0) {
$builder->whereNull("disable_at");
} elseif (intval($keys['disable']) == 2) {
$builder->whereNotNull("disable_at");
}
if (intval($keys['project_id']) > 0) {
$builder->whereIn('userid', function ($query) use ($keys) {
$query->select('userid')->from('project_users')->where('project_id', $keys['project_id']);
});
}
if (intval($keys['no_project_id']) > 0) {
$builder->whereNotIn('userid', function ($query) use ($keys) {
$query->select('userid')->from('project_users')->where('project_id', $keys['no_project_id']);
});
}
if (in_array($sorts['az'], ['asc', 'desc'])) {
$builder->orderBy('az', $sorts['az']);
}
//
$list = $builder->orderBy('userid')->take(Base::getPaginate(100, 10, 'take'))->get();
if (Request::exists('page')) {
$list = $builder->orderBy('userid')->paginate(Base::getPaginate(100, 10));
} else {
$list = $builder->orderBy('userid')->take(Base::getPaginate(100, 10, 'take'))->get();
}
return Base::retSuccess('success', $list);
}
/**
* @api {get} api/users/basic 09. 获取指定会员基础信息
* @api {get} api/users/basic 10. 获取指定会员基础信息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup users
* @apiName basic
*
* @apiParam {Number} userid 会员ID(多个格式jsonArray一次最多30个)
* @apiParam {Number} userid 会员ID(多个格式jsonArray一次最多50个)
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -382,14 +421,34 @@ class UsersController extends AbstractController
}
/**
* 会员列表(限管理员)
* @api {get} api/users/lists 11. 会员列表(限管理员)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup users
* @apiName lists
*
* @apiParam {Object} [keys] 搜索条件
* - keys.key 邮箱/昵称/职位赋值后keys.email、keys.nickname、keys.profession失效
* - keys.email 邮箱
* - keys.nickname 昵称
* - keys.profession 职位
* - keys.identity 身份admin、noadmin
* - keys.disable 是否离职
* - yes: 仅离职
* - all: 全部
* - 其他值: 仅在职(默认)
* - keys.email_verity 邮箱是否认证
* - yes: 已认证
* - no: 未认证
* - 其他值: 全部(默认)
*
* @apiParam {Number} [page] 当前页,默认:1
* @apiParam {Number} [pagesize] 每页显示数量,默认:20最大:50
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function lists()
{
@@ -399,14 +458,26 @@ class UsersController extends AbstractController
//
$keys = Request::input('keys');
if (is_array($keys)) {
if ($keys['email']) {
$builder->where("email", "like", "%{$keys['email']}%");
}
if ($keys['nickname']) {
$builder->where("nickname", "like", "%{$keys['nickname']}%");
}
if ($keys['profession']) {
$builder->where("profession", "like", "%{$keys['profession']}%");
if ($keys['key']) {
if (str_contains($keys['key'], "@")) {
$builder->where("email", "like", "%{$keys['key']}%");
} else {
$builder->where(function($query) use ($keys) {
$query->where("email", "like", "%{$keys['key']}%")
->orWhere("nickname", "like", "%{$keys['key']}%")
->orWhere("profession", "like", "%{$keys['key']}%");
});
}
} else {
if ($keys['email']) {
$builder->where("email", "like", "%{$keys['email']}%");
}
if ($keys['nickname']) {
$builder->where("nickname", "like", "%{$keys['nickname']}%");
}
if ($keys['profession']) {
$builder->where("profession", "like", "%{$keys['profession']}%");
}
}
if ($keys['identity']) {
if (Base::leftExists($keys['identity'], "no")) {
@@ -415,6 +486,18 @@ class UsersController extends AbstractController
$builder->where("identity", "like", "%,{$keys['identity']},%");
}
}
if ($keys['disable'] === 'yes') {
$builder->whereNotNull('disable_at');
} elseif ($keys['disable'] !== 'all') {
$builder->whereNull('disable_at');
}
if ($keys['email_verity'] === 'yes') {
$builder->whereEmailVerity(1);
} elseif ($keys['email_verity'] === 'no') {
$builder->whereEmailVerity(0);
}
} else {
$builder->whereNull('disable_at');
}
$list = $builder->orderByDesc('userid')->paginate(Base::getPaginate(50, 20));
//
@@ -422,22 +505,33 @@ class UsersController extends AbstractController
}
/**
* 操作会员(限管理员)
* @api {get} api/users/operation 12. 操作会员(限管理员)
*
* @apiParam {Number} userid 会员ID
* @apiParam {String} [type] 操作
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup users
* @apiName operation
*
* @apiParam {Number} userid 会员ID
* @apiParam {String} [type] 操作
* - setadmin 设为管理员
* - clearadmin 取消管理员
* - setdisable 设为禁用
* - cleardisable 取消禁用
* - setdisable 设为离职(需要参数 disable_time、transfer_userid
* - cleardisable 取消离职
* - delete 删除会员
* @apiParam {String} [password] 新的密码
* @apiParam {String} [nickname] 昵称
* @apiParam {String} [profession] 职位
* @apiParam {String} [password] 新的密码
* @apiParam {String} [nickname] 昵称
* @apiParam {String} [profession] 职位
* @apiParam {String} [disable_time] 离职时间
* @apiParam {String} [transfer_userid] 离职交接人
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function operation()
{
User::auth('admin');
$user = User::auth('admin');
//
$data = Request::all();
$userid = intval($data['userid']);
@@ -447,8 +541,10 @@ class UsersController extends AbstractController
if (empty($userInfo)) {
return Base::retError('会员不存在或已被删除');
}
$userInfo->checkSystem(1);
//
$upArray = [];
$transferUser = null;
switch ($type) {
case 'setadmin':
$upArray['identity'] = array_diff($userInfo->identity, ['admin']);
@@ -460,16 +556,35 @@ class UsersController extends AbstractController
break;
case 'setdisable':
if ($userInfo->userid === $user->userid) {
return Base::retError('不能操作自己离职');
}
$upArray['identity'] = array_diff($userInfo->identity, ['disable']);
$upArray['identity'][] = 'disable';
$upArray['disable_at'] = Carbon::parse($data['disable_time']);
$transferUserid = is_array($data['transfer_userid']) ? $data['transfer_userid'][0] : $data['transfer_userid'];
$transferUser = User::find(intval($transferUserid));
if (empty($transferUser)) {
return Base::retError('请选择正确的交接人');
}
if ($transferUser->userid === $userInfo->userid) {
return Base::retError('不能移交给自己');
}
if (in_array('disable', $transferUser->identity)) {
return Base::retError('交接人已离职,请选择另一个交接人');
}
break;
case 'cleardisable':
$upArray['identity'] = array_diff($userInfo->identity, ['disable']);
$upArray['disable_at'] = null;
break;
case 'delete':
$userInfo->delete();
if ($userInfo->userid === $user->userid) {
return Base::retError('不能删除自己');
}
$userInfo->deleteUser();
break;
}
if (isset($upArray['identity'])) {
@@ -478,11 +593,7 @@ class UsersController extends AbstractController
// 密码
if (Arr::exists($data, 'password')) {
$password = trim($data['password']);
if (strlen($password) < 6) {
return Base::retError('密码设置不能小于6位数');
} elseif (strlen($password) > 32) {
return Base::retError('密码最多只能设置32位数');
}
User::passwordPolicy($password);
$upArray['encrypt'] = Base::generatePassword(6);
$upArray['password'] = Base::md52($password, $upArray['encrypt']);
$upArray['changepass'] = 1;
@@ -490,7 +601,7 @@ class UsersController extends AbstractController
// 昵称
if (Arr::exists($data, 'nickname')) {
$nickname = trim($data['nickname']);
if (mb_strlen($nickname) < 2) {
if ($nickname && mb_strlen($nickname) < 2) {
return Base::retError('昵称不可以少于2个字');
} elseif (mb_strlen($nickname) > 20) {
return Base::retError('昵称最多只能设置20个字');
@@ -501,7 +612,7 @@ class UsersController extends AbstractController
// 职位/职称
if (Arr::exists($data, 'profession')) {
$profession = trim($data['profession']);
if (mb_strlen($profession) < 2) {
if ($profession && mb_strlen($profession) < 2) {
return Base::retError('职位/职称不可以少于2个字');
} elseif (mb_strlen($profession) > 20) {
return Base::retError('职位/职称最多只能设置20个字');
@@ -510,10 +621,117 @@ class UsersController extends AbstractController
}
}
if ($upArray) {
$userInfo->updateInstance($upArray);
$userInfo->save();
AbstractModel::transaction(function() use ($type, $upArray, $userInfo, $transferUser) {
$userInfo->updateInstance($upArray);
$userInfo->save();
if ($type === 'setdisable') {
$userTransfer = UserTransfer::createInstance([
'original_userid' => $userInfo->userid,
'new_userid' => $transferUser->userid,
]);
$userTransfer->save();
$userTransfer->start();
}
});
}
//
return Base::retSuccess('修改成功', $userInfo);
}
/**
* @api {get} api/users/email/verification 13. 邮箱验证
*
* @apiDescription 不需要token身份
* @apiVersion 1.0.0
* @apiGroup users
* @apiName email__verification
*
* @apiParam {String} code 验证参数
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据(同"获取我的信息"接口)
*/
public function email__verification()
{
$data = Request::input();
// 表单验证
Base::validator($data, [
'code.required' => '验证码不能为空',
]);
//
$res = UserEmailVerification::whereCode($data['code'])->first();
if (empty($res)) {
return Base::retError('无效连接,请重新注册');
}
// 如果已经校验过
if (intval($res->status) === 1)
return Base::retError('链接已经使用过', ['code' => 2]);
$oldTime = Carbon::parse($res->created_at)->timestamp;
$time = Base::Time();
// 30分钟失效
if (abs($time - $oldTime) > 1800) {
return Base::retError("链接已失效,请重新登录/注册");
}
UserEmailVerification::whereCode($data['code'])->update([
'status' => 1
]);
User::whereUserid($res->userid)->update([
'email_verity' => 1
]);
return Base::retSuccess('绑定邮箱成功');
}
/**
* @api {get} api/users/umeng/alias 14. 设置友盟别名
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup users
* @apiName umeng__alias
*
* @apiParam {String} alias 别名
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据(同"获取我的信息"接口)
*/
public function umeng__alias()
{
$data = Request::input();
// 表单验证
Base::validator($data, [
'alias.required' => '别名不能为空',
'alias.between:2,20' => '别名的长度在2-20个字符',
]);
//
$agent = strtolower(Request::server('HTTP_USER_AGENT'));
if (str_contains($agent, 'android')) {
$platform = 'android';
} elseif (str_contains($agent, 'iphone') || str_contains($agent, 'ipad')) {
$platform = 'ios';
} else {
return Base::retError('设备类型错误');
}
//
$user = User::auth();
$inArray = [
'userid' => $user->userid,
'alias' => $data['alias'],
'platform' => $platform,
];
if (UmengAlias::where($inArray)->exists()) {
return Base::retSuccess('别名已存在');
}
$row = UmengAlias::createInstance($inArray);
if ($row->save()) {
return Base::retSuccess('添加成功');
} else {
return Base::retError('添加错误');
}
}
}

View File

@@ -4,7 +4,7 @@
* 给apidoc项目增加顺序编号
*/
error_reporting(E_ALL & ~E_NOTICE);
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
$path = dirname(__FILE__). '/';
$lists = scandir($path);

View File

@@ -3,9 +3,13 @@
namespace App\Http\Controllers;
use App\Module\Base;
use App\Tasks\AutoArchivedTask;
use App\Tasks\DeleteTmpTask;
use App\Tasks\EmailNoticeTask;
use Arr;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Redirect;
use Request;
/**
@@ -21,6 +25,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';
}
@@ -29,11 +37,88 @@ 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()]);
$hash = 'no';
$path = public_path('js/hash');
$murl = url('manifest.txt');
if (file_exists($path)) {
$hash = trim(file_get_contents(public_path('js/hash')));
if (strlen($hash) > 16) {
$hash = 'long';
}
}
return response()->view('main', [
'version' => Base::getVersion(),
'hash' => $hash
])->header('Link', "<{$murl}>; 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.1.0-215/web-apps/vendor/requirejs/require.js",
"office/7.1.0-215/web-apps/apps/api/documents/api.js",
"office/7.1.0-215/sdkjs/common/AllFonts.js",
"office/7.1.0-215/web-apps/vendor/xregexp/xregexp-all-min.js",
"office/7.1.0-215/web-apps/vendor/sockjs/sockjs.min.js",
"office/7.1.0-215/web-apps/vendor/jszip/jszip.min.js",
"office/7.1.0-215/web-apps/vendor/jszip-utils/jszip-utils.min.js",
"office/7.1.0-215/sdkjs/common/libfont/wasm/fonts.js",
"office/7.1.0-215/sdkjs/common/Charts/ChartStyles.js",
"office/7.1.0-215/sdkjs/slide/themes//themes.js",
"office/7.1.0-215/web-apps/apps/presentationeditor/main/app.js",
"office/7.1.0-215/sdkjs/slide/sdk-all-min.js",
"office/7.1.0-215/sdkjs/slide/sdk-all.js",
"office/7.1.0-215/web-apps/apps/documenteditor/main/app.js",
"office/7.1.0-215/sdkjs/word/sdk-all-min.js",
"office/7.1.0-215/sdkjs/word/sdk-all.js",
"office/7.1.0-215/web-apps/apps/spreadsheeteditor/main/app.js",
"office/7.1.0-215/sdkjs/cell/sdk-all-min.js",
"office/7.1.0-215/sdkjs/cell/sdk-all.js",
];
foreach ($array as &$item) {
$item = url($item);
}
return implode(PHP_EOL, $array);
}
/**
* 获取版本号
* @return array
*/
public function version()
{
$url = url('');
$package = Base::getPackage();
$array = [
'version' => Base::getVersion(),
'publish' => Arr::get($package, 'app.0.publish'),
];
if (is_array($package['app'])) {
foreach ($package['app'] as $item) {
$urls = $item['urls'] && is_array($item['urls']) ? $item['urls'] : $item['url'];
if (is_array($item['publish']) && Base::hostContrast($url, $urls)) {
$array['publish'] = $item['publish'];
}
}
}
return $array;
}
/**
@@ -51,10 +136,14 @@ class IndexController extends InvokeController
*/
public function crontab()
{
if (!Base::is_internal_ip()) {
if (!Base::is_internal_ip(Base::getIp())) {
// 限制内网访问
return "Forbidden Access";
}
// 自动归档
Task::deliver(new AutoArchivedTask());
// 邮件通知
Task::deliver(new EmailNoticeTask());
// 删除过期的临时表数据
Task::deliver(new DeleteTmpTask('wg_tmp_msgs', 1));
Task::deliver(new DeleteTmpTask('tmp', 24));
@@ -62,13 +151,73 @@ class IndexController extends InvokeController
return "success";
}
/**
* 桌面客户端发布
*/
public function desktop__publish($name = '')
{
$genericVersion = Request::header('generic-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+$/", $genericVersion)) {
if (version_compare($genericVersion, $latestVersion) > -1) { // 限制上传版本必须 ≥ 当前版本
$genericPath = "uploads/desktop/{$genericVersion}/";
$res = Base::upload([
"file" => Request::file('file'),
"type" => 'desktop',
"path" => $genericPath,
"fileName" => true
]);
if (Base::isSuccess($res)) {
file_put_contents($latestFile, $genericVersion);
}
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", fileatime($file)),
'size' => Base::readableBytes(filesize($file)),
'url' => Base::fillUrl($path . $fileName),
];
}
return view('desktop', ['version' => $name, 'files' => $files]);
}
// 下载
if ($name && file_exists($latestFile)) {
$genericVersion = file_get_contents($latestFile);
if (preg_match("/^\d+\.\d+\.\d+$/", $genericVersion)) {
$filePath = public_path("uploads/desktop/{$genericVersion}/{$name}");
if (file_exists($filePath)) {
return response()->download($filePath);
}
}
}
return abort(404);
}
/**
* 提取所有中文
* @return array|string
*/
public function allcn()
{
if (!Base::is_internal_ip()) {
if (!Base::is_internal_ip(Base::getIp())) {
// 限制内网访问
return "Forbidden Access";
}
@@ -92,7 +241,7 @@ class IndexController extends InvokeController
*/
public function allcn__php()
{
if (!Base::is_internal_ip()) {
if (!Base::is_internal_ip(Base::getIp())) {
// 限制内网访问
return "Forbidden Access";
}

View File

@@ -21,14 +21,20 @@ class VerifyCsrfToken extends Middleware
// 保存任务优先级
'api/system/priority/',
// 保存创建项目列表模板
'api/system/column/template/',
// 添加任务
'api/project/task/add/',
// 保存工作流
'api/project/flow/save/',
// 修改任务
'api/project/task/update/',
// 上传任务问题
'api/project/task/upload/',
// 聊天发文本
'api/dialog/msg/sendtext/',
// 聊天发文件
'api/dialog/msg/sendfile/',
@@ -41,5 +47,11 @@ class VerifyCsrfToken extends Middleware
// 保存文件内容(上传)
'api/file/content/upload/',
// 保存汇报
'api/report/store/',
// 发布桌面端
'desktop/publish/',
];
}

View File

@@ -6,7 +6,6 @@ namespace App\Http\Middleware;
use Closure;
use Request;
use URL;
class WebApi
{
@@ -28,13 +27,9 @@ class WebApi
header('Access-Control-Allow-Headers:Content-Type, platform, platform-channel, token, release, Access-Control-Allow-Origin');
}
$APP_FORCE_URL_SCHEME = env('APP_FORCE_URL_SCHEME', 'auto');
if ($APP_FORCE_URL_SCHEME == 'https' || $APP_FORCE_URL_SCHEME === true) {
URL::forceScheme('https');
} elseif ($APP_FORCE_URL_SCHEME == 'http' || $APP_FORCE_URL_SCHEME === false) {
URL::forceScheme('http');
} elseif (Request::header('x-forwarded-server-port', 80) == 443) {
URL::forceScheme('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);

View File

@@ -10,13 +10,15 @@ 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()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel query()
* @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 whereNotIn($column, $values, $boolean = 'and')
@@ -44,7 +46,7 @@ class AbstractModel extends Model
{
try {
return $this->save();
} catch (\Exception $e) {
} catch (\Throwable) {
return false;
}
}
@@ -62,6 +64,24 @@ class AbstractModel extends Model
return $this->$key;
}
/**
* 取消附加值
* @return static
*/
protected function scopeCancelAppend()
{
return $this->setAppends([]);
}
/**
* 取消隐藏值
* @return static
*/
protected function scopeCancelHidden()
{
return $this->setHidden([]);
}
/**
* 为数组 / JSON 序列化准备日期。
* @param DateTimeInterface $date
@@ -133,16 +153,23 @@ 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)) {
$row = new static;
$row->updateInstance(array_merge($where, $insert ?: $update));
$array = array_merge($where, $insert ?: $update);
if (isset($array[$row->primaryKey])) {
unset($array[$row->primaryKey]);
}
$row->updateInstance($array);
$isInsert = true;
} elseif ($update) {
$row->updateInstance($update);
$isInsert = false;
}
if (!$row->save()) {
return null;

View File

@@ -10,18 +10,19 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Request;
/**
* Class File
* App\Models\File
*
* @package App\Models
* @property int $id
* @property string|null $pids 上级ID递归
* @property int|null $pid 上级ID
* @property int|null $cid 复制ID
* @property string|null $name 名称
* @property string|null $type 类型
* @property string|null $ext 后缀名
* @property int|null $size 大小(B)
* @property int|null $userid 拥有者ID
* @property int|null $share 是否共享(1:共享所有人,2:指定成员)
* @property int|null $created_id 创建者ID
* @property int|null $share 是否共享
* @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
@@ -33,9 +34,11 @@ use Request;
* @method static \Illuminate\Database\Eloquent\Builder|File whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereCreatedId($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereExt($value)
* @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 whereShare($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereSize($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereType($value)
@@ -50,43 +53,68 @@ class File extends AbstractModel
use SoftDeletes;
/**
* 是否有访问权限(没有时抛出异常)
* @param $userid
* 文件文件
*/
public function exceAllow($userid)
{
if (!$this->chackAllow($userid)) {
throw new ApiException('没有访问权限');
}
}
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', 'png', 'gif', 'bmp'
];
/**
* 本地媒体文件
*/
const localExt = [
'jpg', 'jpeg', 'png', 'gif', 'bmp', 'ico', 'raw',
'tif', 'tiff',
'mp3', 'wav', 'mp4', 'flv',
'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm',
];
/**
* 是否有访问权限
* ① 自己的文件夹
* ② 共享所有人的文件夹
* ③ 在指定共享人员内
* @param $userid
* @return bool
* @return int -1:没有权限0:访问权限1:读写权限1000:所有者或创建者
*/
public function chackAllow($userid)
public function getPermission($userid)
{
if ($userid == $this->userid) {
// ① 自己的文件夹
return true;
if ($userid == $this->userid || $userid == $this->created_id) {
// ① 自己的文件夹 或 自己创建的文件夹
return 1000;
}
$row = $this->getShareInfo();
if ($row) {
if ($row->share == 1) {
// ② 共享所有人的文件夹
return true;
} elseif ($row->share == 2) {
// ③ 在指定共享人员内
if (FileUser::whereFileId($row->id)->whereUserid($userid)->exists()) {
return true;
}
$fileUser = FileUser::whereFileId($row->id)->where(function ($query) use ($userid) {
$query->where('userid', 0);
$query->orWhere('userid', $userid);
})->orderByDesc('permission')->first();
if ($fileUser) {
// ② 在指定共享成员内
return $fileUser->permission;
}
}
return false;
return -1;
}
/**
@@ -95,7 +123,7 @@ class File extends AbstractModel
*/
public function getShareInfo()
{
if ($this->share > 0) {
if ($this->share) {
return $this;
}
$pid = $this->pid;
@@ -104,7 +132,7 @@ class File extends AbstractModel
if (empty($row)) {
break;
}
if ($row->share > 0) {
if ($row->share) {
return $row;
}
$pid = $row->pid;
@@ -114,7 +142,7 @@ class File extends AbstractModel
/**
* 是否处于共享文件夹内(不含自身)
* @return bool
* @return File|false
*/
public function isNnShare()
{
@@ -124,31 +152,82 @@ class File extends AbstractModel
if (empty($row)) {
break;
}
if ($row->share > 0) {
return true;
if ($row->share) {
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)
public function updataShare($share = null)
{
AbstractModel::transaction(function () use ($share) {
$this->share = $share;
$this->save();
$list = self::wherePid($this->id)->get();
if ($list->isNotEmpty()) {
foreach ($list as $item) {
$item->setShare(0);
if ($share === null) {
$share = FileUser::whereFileId($this->id)->count() == 0 ? 0 : 1;
}
if ($this->share != $share) {
AbstractModel::transaction(function () use ($share) {
$this->share = $share;
$this->save();
if ($share === 0) {
FileUser::deleteFileAll($this->id, $this->userid);
}
}
});
$list = self::wherePid($this->id)->get();
if ($list->isNotEmpty()) {
foreach ($list as $item) {
$item->updataShare(0);
}
}
});
}
return true;
}
/**
* 保存前更新pids
* @return bool
*/
public function saveBeforePids()
{
$pid = $this->pid;
$array = [];
while ($pid > 0) {
$array[] = $pid;
$pid = intval(self::whereId($pid)->value('pid'));
}
$opids = $this->pids;
if ($array) {
$array = array_values(array_reverse($array));
$this->pids = ',' . implode(',', $array) . ',';
} else {
$this->pids = '';
}
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->saveBeforePids();
}
});
}
return true;
}
@@ -161,6 +240,7 @@ class File extends AbstractModel
AbstractModel::transaction(function () {
$this->delete();
$this->pushMsg('delete');
FileUser::deleteFileAll($this->id);
FileContent::whereFid($this->id)->delete();
$list = self::wherePid($this->id)->get();
if ($list->isNotEmpty()) {
@@ -186,19 +266,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;
}
@@ -221,19 +289,219 @@ class File extends AbstractModel
Task::deliver($task);
}
/**
* 获取推送会员
* @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;
}
/**
* 处理返回图片地址
* @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']);
}
}
return $item;
}
/**
* 获取文件并检测权限
* @param $id
* @param null $noExistTis
* @param int $limit 要求权限: 0-访问权限、1-读写权限、1000-所有者或创建者
* @param $permission
* @return File
*/
public static function allowFind($id, $noExistTis = null)
public static function permissionFind($id, $limit = 0, &$permission = -1)
{
$file = File::find($id);
if (empty($file)) {
throw new ApiException($noExistTis ?: '文件不存在或已被删除');
throw new ApiException('文件不存在或已被删除');
}
//
$permission = $file->getPermission(User::userid());
if ($permission < $limit) {
$msg = match ($limit) {
1000 => '仅限所有者或创建者操作',
1 => '没有修改写入权限',
default => '没有查看访问权限',
};
throw new ApiException($msg);
}
$file->exceAllow(User::userid());
return $file;
}
/**
* 格式化内容数据
* @param array $data [path, size, ext, name]
* @return array
*/
public static function formatFileData(array $data)
{
$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
{
// 其他预览
if (in_array($fileExt, File::localExt)) {
$url = Base::fillUrl($filePath);
} else {
$url = 'http://' . env('APP_IPPR') . '.3/' . $filePath;
}
if ($fileExt != 'pdf') {
$fileDotExt = ".{$fileExt}";
$fileName = Base::rightDelete($data['name'], $fileDotExt) . $fileDotExt;
$url = Base::urlAddparameter($url, [
'fullfilename' => $fileName
]);
}
$data['content'] = [
'preview' => true,
'url' => base64_encode($url),
];
$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->saveBeforePids();
// 移交文件
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->saveBeforePids();
}
});
// 移交文件权限
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

@@ -8,9 +8,8 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Response;
/**
* Class FileContent
* App\Models\FileContent
*
* @package App\Models
* @property int $id
* @property int|null $fid 文件ID
* @property string|null $content 内容
@@ -42,41 +41,53 @@ class FileContent extends AbstractModel
use SoftDeletes;
/**
* 获取格式内容
* @param $type
* 获取格式内容(或下载)
* @param File $file
* @param $content
* @param $download
* @return array|\Symfony\Component\HttpFoundation\BinaryFileResponse
*/
public static function formatContent($type, $content)
public static function formatContent($file, $content, $download = false)
{
$content = Base::json2array($content);
if (in_array($type, ['word', 'excel', 'ppt'])) {
$name = $file->ext ? "{$file->name}.{$file->ext}" : null;
$content = Base::json2array($content ?: []);
if (in_array($file->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)));
return Response::download(resource_path('assets/statics/office/empty.' . str_replace(['word', 'excel', 'ppt'], ['docx', 'xlsx', 'pptx'], $file->type)), $name);
}
return Response::download(public_path($content['url']));
return Response::download(public_path($content['url']), $name);
}
if (empty($content)) {
switch ($type) {
case 'document':
$content = [
"type" => "md",
"content" => "",
];
break;
case 'sheet':
$content = [
[
"name" => "Sheet1",
"config" => json_decode('{}'),
]
];
break;
default:
$content = json_decode('{}');
break;
$content = match ($file->type) {
'document' => [
"type" => $file->ext,
"content" => "",
],
default => json_decode('{}'),
};
if ($download) {
abort(403, "This file is empty.");
}
} else {
$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 Response::download($filePath, $name);
} else {
abort(403, "This file not support download.");
}
}
}
return Base::retSuccess('success', [ 'content' => $content ]);

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

@@ -0,0 +1,37 @@
<?php
namespace App\Models;
/**
* App\Models\FileLink
*
* @property int $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
* @method static \Illuminate\Database\Eloquent\Builder|FileLink newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|FileLink newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|FileLink query()
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereCode($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereFileId($value)
* @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
{
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function file(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(File::class, 'id', 'file_id');
}
}

View File

@@ -4,12 +4,12 @@ namespace App\Models;
/**
* Class FileUser
* App\Models\FileUser
*
* @package App\Models
* @property int $id
* @property int|null $file_id 项目ID
* @property int|null $userid 成员ID
* @property int|null $permission 权限0只读1读写
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|FileUser newModelQuery()
@@ -18,10 +18,41 @@ namespace App\Models;
* @method static \Illuminate\Database\Eloquent\Builder|FileUser whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileUser whereFileId($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileUser whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileUser wherePermission($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileUser whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileUser whereUserid($value)
* @mixin \Eloquent
*/
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();
});
}
}

View File

@@ -3,40 +3,36 @@
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use App\Tasks\PushTask;
use Carbon\Carbon;
use DB;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Database\Eloquent\SoftDeletes;
use Request;
/**
* Class Project
* App\Models\Project
*
* @package App\Models
* @property int $id
* @property string|null $name 名称
* @property string|null $desc 描述、备注
* @property int|null $userid 创建人
* @property int|mixed $dialog_id 聊天会话ID
* @property int|null $dialog_id 聊天会话ID
* @property string|null $archived_at 归档时间
* @property int|null $archived_userid 归档会员
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read int $owner_userid
* @property-read int $task_complete
* @property-read int $task_my_complete
* @property-read int $task_my_num
* @property-read int $task_my_percent
* @property-read int $task_num
* @property-read int $task_percent
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectColumn[] $projectColumn
* @property-read int|null $project_column_count
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectLog[] $projectLog
* @property-read int|null $project_log_count
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectUser[] $projectUser
* @property-read int|null $project_user_count
* @method static \Illuminate\Database\Eloquent\Builder|Project authData($userid = null)
* @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()
@@ -59,99 +55,14 @@ class Project extends AbstractModel
{
use SoftDeletes;
const projectSelect = [
'projects.*',
'project_users.owner',
protected $hidden = [
'deleted_at',
];
protected $appends = [
'task_num',
'task_complete',
'task_percent',
'task_my_num',
'task_my_complete',
'task_my_percent',
'owner_userid',
];
/**
* 生成任务数据
*/
private function generateTaskData()
{
if (!isset($this->appendattrs['task_num'])) {
$builder = ProjectTask::whereProjectId($this->id)->whereParentId(0)->whereNull('archived_at');
$this->appendattrs['task_num'] = $builder->count();
$this->appendattrs['task_complete'] = $builder->whereNotNull('complete_at')->count();
$this->appendattrs['task_percent'] = $this->appendattrs['task_num'] ? intval($this->appendattrs['task_complete'] / $this->appendattrs['task_num'] * 100) : 0;
//
$builder = ProjectTask::whereProjectId($this->id)->whereParentId(0)->authData(User::userid())->whereNull('archived_at');
$this->appendattrs['task_my_num'] = $builder->count();
$this->appendattrs['task_my_complete'] = $builder->whereNotNull('complete_at')->count();
$this->appendattrs['task_my_percent'] = $this->appendattrs['task_my_num'] ? intval($this->appendattrs['task_my_complete'] / $this->appendattrs['task_my_num'] * 100) : 0;
}
}
/**
* 任务数量
* @return int
*/
public function getTaskNumAttribute()
{
$this->generateTaskData();
return $this->appendattrs['task_num'];
}
/**
* 任务完成数量
* @return int
*/
public function getTaskCompleteAttribute()
{
$this->generateTaskData();
return $this->appendattrs['task_complete'];
}
/**
* 任务完成率
* @return int
*/
public function getTaskPercentAttribute()
{
$this->generateTaskData();
return $this->appendattrs['task_percent'];
}
/**
* 任务数量(我的)
* @return int
*/
public function getTaskMyNumAttribute()
{
$this->generateTaskData();
return $this->appendattrs['task_my_num'];
}
/**
* 任务完成数量(我的)
* @return int
*/
public function getTaskMyCompleteAttribute()
{
$this->generateTaskData();
return $this->appendattrs['task_my_complete'];
}
/**
* 任务完成率(我的)
* @return int
*/
public function getTaskMyPercentAttribute()
{
$this->generateTaskData();
return $this->appendattrs['task_my_percent'];
}
/**
* 负责人会员ID
* @return int
@@ -159,7 +70,7 @@ class Project extends AbstractModel
public function getOwnerUseridAttribute()
{
if (!isset($this->appendattrs['owner_userid'])) {
$ownerUser = $this->projectUser->where('owner', 1)->first();
$ownerUser = ProjectUser::whereProjectId($this->id)->whereOwner(1)->first();
$this->appendattrs['owner_userid'] = $ownerUser ? $ownerUser->userid : 0;
}
return $this->appendattrs['owner_userid'];
@@ -190,19 +101,73 @@ class Project extends AbstractModel
}
/**
* 查询自己的项目
* 查询所有项目与正常查询多返回owner字段
* @param self $query
* @param null $userid
* @return self
*/
public function scopeAuthData($query, $userid = null)
public function scopeAllData($query, $userid = null)
{
$userid = $userid ?: User::userid();
$query->join('project_users', 'projects.id', '=', 'project_users.project_id')
->where('project_users.userid', $userid);
$query
->select([
'projects.*',
'project_users.owner',
'project_users.top_at',
])
->leftJoin('project_users', function ($leftJoin) use ($userid) {
$leftJoin
->on('project_users.userid', '=', DB::raw($userid))
->on('projects.id', '=', 'project_users.project_id');
});
return $query;
}
/**
* 查询自己负责或参与的项目
* @param self $query
* @param null $userid
* @param null $owner
* @return self
*/
public function scopeAuthData($query, $userid = null, $owner = null)
{
$userid = $userid ?: User::userid();
$query
->select([
'projects.*',
'project_users.owner',
'project_users.top_at',
])
->join('project_users', 'projects.id', '=', 'project_users.project_id')
->where('project_users.userid', $userid);
if ($owner !== null) {
$query->where('project_users.owner', $owner);
}
return $query;
}
/**
* 获取任务统计数据
* @param $userid
* @return array
*/
public function getTaskStatistics($userid)
{
$array = [];
$builder = ProjectTask::whereProjectId($this->id)->whereNull('archived_at');
$array['task_num'] = $builder->count();
$array['task_complete'] = $builder->whereNotNull('complete_at')->count();
$array['task_percent'] = $array['task_num'] ? intval($array['task_complete'] / $array['task_num'] * 100) : 0;
//
$builder = ProjectTask::authData($userid, 1)->where('project_tasks.project_id', $this->id)->whereNull('project_tasks.archived_at');
$array['task_my_num'] = $builder->count();
$array['task_my_complete'] = $builder->whereNotNull('project_tasks.complete_at')->count();
$array['task_my_percent'] = $array['task_my_num'] ? intval($array['task_my_complete'] / $array['task_my_num'] * 100) : 0;
//
return $array;
}
/**
* 加入项目
* @param int $userid 加入的会员ID
@@ -249,7 +214,7 @@ class Project extends AbstractModel
*/
public function relationUserids()
{
return $this->projectUser->pluck('userid')->toArray();
return ProjectUser::whereProjectId($this->id)->orderBy('id')->pluck('userid')->toArray();
}
/**
@@ -267,7 +232,7 @@ class Project extends AbstractModel
}
/**
* 归档任务、取消归档
* 归档项目、取消归档
* @param Carbon|null $archived_at 归档时间
* @return bool
*/
@@ -277,14 +242,23 @@ class Project extends AbstractModel
if ($archived_at === null) {
// 取消归档
$this->archived_at = null;
$this->archived_userid = User::userid();
$this->addLog("项目取消归档");
$this->pushMsg('add', $this);
ProjectTask::whereProjectId($this->id)->whereArchivedFollow(1)->update([
'archived_at' => null,
'archived_follow' => 0
]);
} else {
// 归档任务
// 归档项目
$this->archived_at = $archived_at;
$this->archived_userid = User::userid();
$this->addLog("项目归档");
$this->pushMsg('archived');
ProjectTask::whereProjectId($this->id)->whereArchivedAt(null)->update([
'archived_at' => $archived_at,
'archived_follow' => 1
]);
}
$this->save();
});
@@ -299,9 +273,7 @@ class Project extends AbstractModel
{
AbstractModel::transaction(function () {
$dialog = WebSocketDialog::find($this->dialog_id);
if ($dialog) {
$dialog->deleteDialog();
}
$dialog?->deleteDialog();
$columns = ProjectColumn::whereProjectId($this->id)->get();
foreach ($columns as $column) {
$column->deleteColumn(false);
@@ -316,18 +288,23 @@ class Project extends AbstractModel
/**
* 添加项目日志
* @param string $detail
* @param array $record
* @param int $userid
* @return ProjectLog
*/
public function addLog($detail, $userid = 0)
public function addLog($detail, $record = [], $userid = 0)
{
$log = ProjectLog::createInstance([
$array = [
'project_id' => $this->id,
'column_id' => 0,
'task_id' => 0,
'userid' => $userid ?: User::userid(),
'detail' => $detail,
]);
];
if ($record) {
$array['record'] = $record;
}
$log = ProjectLog::createInstance($array);
$log->save();
return $log;
}
@@ -335,45 +312,179 @@ class Project extends AbstractModel
/**
* 推送消息
* @param string $action
* @param array $data 发送内容,默认为[id=>项目ID]
* @param array $userid 指定会员,默认为项目所有成员
* @param array|self $data 发送内容,默认为[id=>项目ID]
* @param array $userid 指定会员,默认为项目所有成员
*/
public function pushMsg($action, $data = null, $userid = null)
{
if ($data === null) {
$data = ['id' => $this->id];
} elseif ($data instanceof self) {
$data = $data->toArray();
}
//
$array = [$userid, []];
if ($userid === null) {
$userid = $this->relationUserids();
$array[0] = $this->relationUserids();
} elseif (!is_array($userid)) {
$array[0] = [$userid];
}
//
if (isset($data['owner'])) {
$owners = ProjectUser::whereProjectId($data['id'])->whereOwner(1)->pluck('userid')->toArray();
$array = [array_intersect($array[0], $owners), array_diff($array[0], $owners)];
}
//
foreach ($array as $index => $item) {
if ($index > 0) {
$data['owner'] = 0;
}
$params = [
'ignoreFd' => Request::header('fd'),
'userid' => array_values($item),
'msg' => [
'type' => 'project',
'action' => $action,
'data' => $data,
]
];
$task = new PushTask($params, false);
Task::deliver($task);
}
$params = [
'ignoreFd' => Request::header('fd'),
'userid' => $userid,
'msg' => [
'type' => 'project',
'action' => $action,
'data' => $data,
]
];
$task = new PushTask($params, false);
Task::deliver($task);
}
/**
* 根据用户获取项目信息(用于判断会员是否存在项目内)
* 添加工作流
* @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)->update([
'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 int $project_id
* @param bool $ignoreArchived 排除已归档
* @param null|bool $archived true:仅限未归档, false:仅限已归档, null:不限制
* @param null|bool $mustOwner true:仅限项目负责人, false:仅限非项目负责人, null:不限制
* @return self
*/
public static function userProject($project_id, $ignoreArchived = true)
public static function userProject($project_id, $archived = true, $mustOwner = null)
{
$builder = self::select(self::projectSelect)->authData()->where('projects.id', intval($project_id));
if ($ignoreArchived) {
$builder->whereNull('projects.archived_at');
}
$project = $builder->first();
$project = self::authData()->where('projects.id', intval($project_id))->first();
if (empty($project)) {
throw new ApiException('项目不存在或不在成员列表内');
throw new ApiException('项目不存在或不在成员列表内', [ 'project_id' => $project_id ], -4001);
}
if ($archived === true && $project->archived_at != null) {
throw new ApiException('项目已归档', [ 'project_id' => $project_id ], -4001);
}
if ($archived === false && $project->archived_at == null) {
throw new ApiException('项目未归档', [ 'project_id' => $project_id ]);
}
if ($mustOwner === true && !$project->owner) {
throw new ApiException('仅限项目负责人操作', [ 'project_id' => $project_id ]);
}
if ($mustOwner === false && $project->owner) {
throw new ApiException('禁止项目负责人操作', [ 'project_id' => $project_id ]);
}
return $project;
}

View File

@@ -9,9 +9,8 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Request;
/**
* Class ProjectColumn
* App\Models\ProjectColumn
*
* @package App\Models
* @property int $id
* @property int|null $project_id 项目ID
* @property string|null $name 列表名称

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Models;
use App\Module\Base;
/**
* App\Models\ProjectFlow
*
* @property int $id
* @property int|null $project_id 项目ID
* @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 int|null $project_flow_item_count
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow query()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow whereProjectId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlow whereUpdatedAt($value)
* @mixin \Eloquent
*/
class ProjectFlow extends AbstractModel
{
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function projectFlowItem(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(ProjectFlowItem::class, 'flow_id', 'id')->orderBy('sort');
}
/**
* @return mixed
*/
public function deleteFlow()
{
return AbstractModel::transaction(function() {
ProjectFlowItem::whereProjectId($this->project_id)->chunk(100, function($list) {
foreach ($list as $item) {
$item->deleteFlowItem();
}
});
return $this->delete();
});
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace App\Models;
use App\Module\Base;
/**
* App\Models\ProjectFlowItem
*
* @property int $id
* @property int|null $project_id 项目ID
* @property int|null $flow_id 流程ID
* @property string|null $name 名称
* @property string|null $status 状态
* @property array $turns 可流转
* @property array $userids 状态负责人ID
* @property string|null $usertype 流转模式
* @property int|null $userlimit 限制负责人
* @property int|null $sort 排序
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\ProjectFlow|null $projectFlow
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem query()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereFlowId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereProjectId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereSort($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereStatus($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereTurns($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereUserids($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereUserlimit($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectFlowItem whereUsertype($value)
* @mixin \Eloquent
*/
class ProjectFlowItem extends AbstractModel
{
protected $hidden = [
'created_at',
'updated_at',
];
/**
* @param $value
* @return array
*/
public function getTurnsAttribute($value)
{
if (is_array($value)) {
return $value;
}
return Base::json2array($value);
}
/**
* @param $value
* @return array
*/
public function getUseridsAttribute($value)
{
if (is_array($value)) {
return $value;
}
return Base::json2array($value);
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function projectFlow(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(ProjectFlow::class, 'id', 'flow_id');
}
/**
* @return bool|null
*/
public function deleteFlowItem()
{
ProjectTask::whereFlowItemId($this->id)->update([
'flow_item_id' => 0,
'flow_item_name' => "",
]);
return $this->delete();
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Models;
/**
* App\Models\ProjectInvite
*
* @property int $id
* @property int|null $project_id 项目ID
* @property int|null $num 累计邀请
* @property string|null $code 链接码
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read bool $already
* @property-read \App\Models\Project|null $project
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite query()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite whereCode($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite whereNum($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite whereProjectId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectInvite whereUpdatedAt($value)
* @mixin \Eloquent
*/
class ProjectInvite extends AbstractModel
{
protected $appends = [
'already',
];
/**
* 是否已加入
* @return bool
*/
public function getAlreadyAttribute()
{
if (!isset($this->appendattrs['already'])) {
$this->appendattrs['already'] = false;
if (User::userid()) {
$this->appendattrs['already'] = (bool)$this->project?->projectUser?->where('userid', User::userid())->count();
}
}
return $this->appendattrs['already'];
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function project(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(Project::class, 'id', 'project_id');
}
}

View File

@@ -2,18 +2,21 @@
namespace App\Models;
use App\Module\Base;
/**
* Class ProjectLog
* App\Models\ProjectLog
*
* @package App\Models
* @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 记录数据
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\ProjectTask|null $projectTask
* @property-read \App\Models\User|null $user
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog newQuery()
@@ -23,6 +26,7 @@ namespace App\Models;
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereDetail($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereProjectId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereRecord($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereTaskId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectLog whereUserid($value)
@@ -31,6 +35,18 @@ namespace App\Models;
class ProjectLog extends AbstractModel
{
/**
* @param $value
* @return array
*/
public function getRecordAttribute($value)
{
if (is_array($value)) {
return $value;
}
return Base::json2array($value);
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
@@ -39,4 +55,12 @@ class ProjectLog extends AbstractModel
return $this->hasOne(User::class, 'userid', 'userid');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function projectTask(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(ProjectTask::class, 'id', 'task_id');
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,10 +2,11 @@
namespace App\Models;
use App\Module\Base;
/**
* Class ProjectTaskContent
* App\Models\ProjectTaskContent
*
* @package App\Models
* @property int $id
* @property int|null $project_id 项目ID
* @property int|null $task_id 任务ID
@@ -29,4 +30,54 @@ 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);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 (file_put_contents(public_path($tmpPath), base64_decode($text))) {
$content = str_replace($matchs[0][$key], '<img src="{{RemoteURL}}' . $tmpPath . '"', $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

@@ -5,17 +5,16 @@ namespace App\Models;
use App\Module\Base;
/**
* Class ProjectTaskFile
* App\Models\ProjectTaskFile
*
* @package App\Models
* @property int $id
* @property int|null $project_id 项目ID
* @property int|null $task_id 任务ID
* @property string|null $name 文件名称
* @property int|null $size 文件大小(B)
* @property string|null $ext 文件格式
* @property string|null $path 文件地址
* @property string|null $thumb 缩略图
* @property string $path 文件地址
* @property string $thumb 缩略图
* @property int|null $userid 上传用户ID
* @property int|null $download 下载次数
* @property \Illuminate\Support\Carbon|null $created_at

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,45 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* App\Models\ProjectTaskMailLog
*
* @property int $id
* @property int|null $userid 用户id
* @property int|null $task_id 任务id
* @property string|null $email 电子邮箱
* @property int|null $type 提醒类型0 任务开始提醒1 距离到期提醒2到期超时提醒
* @property int|null $is_send 邮件发送是否成功0否1是
* @property string|null $send_error 邮件发送错误详情
* @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|ProjectTaskMailLog newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog newQuery()
* @method static \Illuminate\Database\Query\Builder|ProjectTaskMailLog onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog query()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereEmail($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereIsSend($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereSendError($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereTaskId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskMailLog whereUserid($value)
* @method static \Illuminate\Database\Query\Builder|ProjectTaskMailLog withTrashed()
* @method static \Illuminate\Database\Query\Builder|ProjectTaskMailLog withoutTrashed()
* @mixin \Eloquent
*/
class ProjectTaskMailLog extends AbstractModel
{
use SoftDeletes;
}

View File

@@ -3,9 +3,8 @@
namespace App\Models;
/**
* Class ProjectTaskTag
* App\Models\ProjectTaskTag
*
* @package App\Models
* @property int $id
* @property int|null $project_id 项目ID
* @property int|null $task_id 任务ID

View File

@@ -3,9 +3,8 @@
namespace App\Models;
/**
* Class ProjectTaskUser
* App\Models\ProjectTaskUser
*
* @package App\Models
* @property int $id
* @property int|null $project_id 项目ID
* @property int|null $task_id 任务ID
@@ -14,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()
@@ -30,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

@@ -5,13 +5,13 @@ namespace App\Models;
use App\Module\Base;
/**
* Class ProjectUser
* App\Models\ProjectUser
*
* @package App\Models
* @property int $id
* @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
@@ -22,6 +22,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
@@ -37,17 +38,54 @@ 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) {
$item->project->addLog("移交项目身份", ['userid' => [$originalUserid, ' => ',$newUserid]]);
$item->project->syncDialogUser();
}
}
});
}
/**
* 退出项目
*/
public function exitProject()
{
$tasks = ProjectTask::whereProjectId($this->project_id)->authData($this->userid)->get();
foreach ($tasks as $task) {
if (ProjectTaskUser::whereTaskId($task->id)->whereUserid($this->userid)->delete()) {
$task->syncDialogUser();
}
}
ProjectTaskUser::whereProjectId($this->project_id)
->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();
}
}
});
$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|\App\Models\ReportReceive[] $Receives
* @property-read int|null $receives_count
* @property-read mixed $receives
* @property-read \Illuminate\Database\Eloquent\Collection|\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

@@ -3,9 +3,8 @@
namespace App\Models;
/**
* Class Setting
* App\Models\Setting
*
* @package App\Models
* @property int $id
* @property string|null $name
* @property string|null $desc 参数描述、备注

View File

@@ -3,9 +3,8 @@
namespace App\Models;
/**
* Class Tmp
* App\Models\Tmp
*
* @package App\Models
* @property int $id
* @property string|null $name
* @property string|null $value

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

@@ -0,0 +1,165 @@
<?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,
'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']);
if (is_array($userid)) {
$builder->whereIn('userid', $userid);
} elseif (Base::isNumber($userid)) {
$builder->whereUserid($userid);
}
$builder
->orderByDesc('id')
->chunkById(100, function ($rows) use ($array) {
$lists = $rows->groupBy('platform');
foreach ($lists as $platform => $list) {
$alias = $list->pluck('alias')->implode(',');
self::pushMsgToAlias($alias, $platform, $array);
}
});
}
}

View File

@@ -9,9 +9,8 @@ use Cache;
use Carbon\Carbon;
/**
* Class User
* App\Models\User
*
* @package App\Models
* @property int $userid
* @property array $identity 身份
* @property string|null $az A-Z
@@ -29,8 +28,11 @@ 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 int|null $email_verity 邮箱是否已验证
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Database\Factories\UserFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Builder|User newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|User newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|User query()
@@ -38,7 +40,9 @@ use Carbon\Carbon;
* @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 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)
@@ -145,6 +149,34 @@ class User extends AbstractModel
}
}
/**
* 检查环境是否允许
* @param null $onlyUserid 仅指定会员
*/
public function checkSystem($onlyUserid = null)
{
if ($onlyUserid && $onlyUserid != $this->userid) {
return;
}
if (env("PASSWORD_ADMIN") == 'disabled') {
if ($this->userid == 1) {
throw new ApiException('当前环境禁止此操作');
}
}
if (env("PASSWORD_OWNER") == 'disabled') {
throw new ApiException('当前环境禁止此操作');
}
}
/**
* 删除会员
* @return bool|null
*/
public function deleteUser()
{
UserEmailVerification::whereEmail($this->email)->delete();
return $this->delete();
}
/** ***************************************************************************************** */
/** ***************************************************************************************** */
@@ -160,18 +192,20 @@ 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) {
$isRegVerify = Base::settingFind('emailSetting', 'reg_verify') === 'open';
$user = self::whereUserid(User::email2userid($email))->first();
if ($isRegVerify && $user->email_verity === 0) {
UserEmailVerification::userEmailSend($user);
throw new ApiException('您的帐号已注册过,请验证邮箱', ['code' => 'email']);
}
throw new ApiException('邮箱地址已存在');
}
//密码
if (strlen($password) < 6) {
throw new ApiException('密码设置不能小于6位数');
} elseif (strlen($password) > 32) {
throw new ApiException('密码最多只能设置32位数');
}
self::passwordPolicy($password);
//开始注册
$encrypt = Base::generatePassword(6);
$inArray = [
@@ -441,4 +475,35 @@ class User extends AbstractModel
}
}
}
/**
* 检测密码策略是否符合
* @param $password
* @return void
*/
public static function passwordPolicy($password)
{
if (strlen($password) < 6) {
throw new ApiException('密码设置不能小于6位数');
}
if (strlen($password) > 32) {
throw new ApiException('密码最多只能设置32位数');
}
// 复杂密码
$password_policy = Base::settingFind('system', 'password_policy');
if ($password_policy == 'complex') {
if (preg_match("/^[0-9]+$/", $password)) {
throw new ApiException('密码不能全是数字,请包含数字,字母大小写或者特殊字符');
}
if (preg_match("/^[a-zA-Z]+$/", $password)) {
throw new ApiException('密码不能全是字母,请包含数字,字母大小写或者特殊字符');
}
if (preg_match("/^[0-9A-Z]+$/", $password)) {
throw new ApiException('密码不能全是数字+大写字母,密码包含数字,字母大小写或者特殊字符');
}
if (preg_match("/^[0-9a-z]+$/", $password)) {
throw new ApiException('密码不能全是数字+小写字母,密码包含数字,字母大小写或者特殊字符');
}
}
}
}

View File

@@ -0,0 +1,80 @@
<?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 \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 whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereUserid($value)
* @mixin \Eloquent
*/
class UserEmailVerification extends AbstractModel
{
/**
* 发验证邮箱
* @param User $user
*/
public static function userEmailSend(User $user)
{
$res = self::whereUserid($user->userid)->where('created_at', '>', Carbon::now()->subMinutes(30))->first();
if ($res) return;
//删除
self::whereUserid($user->userid)->delete();
$userEmailVerification = self::createInstance([
'userid' => $user->userid,
'email' => $user->email,
'code' => Base::generatePassword(64),
'status' => 0,
]);
$userEmailVerification->save();
$setting = Base::setting('emailSetting');
$url = Base::fillUrl('single/valid/email') . '?code=' . $userEmailVerification->code;
try {
if (!Base::isEmail($user->email)) {
throw new \Exception("User email '{$user->email}' address error");
}
$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>";
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($user->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());
}
}
}
}

View File

@@ -0,0 +1,45 @@
<?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()
{
// 移交项目身份
ProjectUser::transfer($this->original_userid, $this->new_userid);
// 移交任务身份
ProjectTaskUser::transfer($this->original_userid, $this->new_userid);
// 移交文件
File::transfer($this->original_userid, $this->new_userid);
}
}

View File

@@ -4,14 +4,13 @@ namespace App\Models;
/**
* Class WebSocket
* App\Models\WebSocket
*
* @package App\Models
* @property int $id
* @property string $key
* @property string|null $fd
* @property int|null $userid
* @property string|null $path
* @property int|null $userid
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|WebSocket newModelQuery()

View File

@@ -3,18 +3,20 @@
namespace App\Models;
use App\Exceptions\ApiException;
use App\Tasks\PushTask;
use Carbon\Carbon;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* Class WebSocketDialog
* App\Models\WebSocketDialog
*
* @package App\Models
* @property int $id
* @property string|null $type 对话类型
* @property string|null $group_type 聊天室类型
* @property string|null $name 对话名称
* @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
@@ -30,6 +32,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @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()
@@ -48,6 +51,93 @@ class WebSocketDialog extends AbstractModel
return $this->hasMany(WebSocketDialogUser::class, 'dialog_id', 'id');
}
/**
* 格式化对话
* @param int $userid 会员ID
* @return $this
*/
public function formatData($userid)
{
// 最后消息
$last_msg = WebSocketDialogMsg::whereDialogId($this->id)->orderByDesc('id')->first();
$this->last_msg = $last_msg;
// 未读信息
$unreadBuilder = WebSocketDialogMsgRead::whereDialogId($this->id)->whereUserid($userid)->whereReadAt(null);
$this->unread = $unreadBuilder->count();
$this->mention = $unreadBuilder->whereMention(1)->count();
$this->mark_unread = $this->mark_unread ?? WebSocketDialogUser::whereDialogId($this->id)->whereUserid($userid)->value('mark_unread');
// 对话人数
$builder = WebSocketDialogUser::whereDialogId($this->id);
$this->people = $builder->count();
// 对方信息
$this->dialog_user = null;
$this->group_info = null;
$this->top_at = $this->top_at ?? WebSocketDialogUser::whereDialogId($this->id)->whereUserid($userid)->value('top_at');
switch ($this->type) {
case "user":
$dialog_user = $builder->where('userid', '!=', $userid)->first();
$this->name = User::userid2nickname($dialog_user->userid);
$this->dialog_user = $dialog_user;
break;
case "group":
if ($this->group_type === 'project') {
$this->group_info = Project::withTrashed()->select(['id', 'name', 'archived_at', 'deleted_at'])->whereDialogId($this->id)->first()?->cancelAppend()->cancelHidden();
$this->name = $this->group_info ? $this->group_info->name : '';
} elseif ($this->group_type === 'task') {
$this->group_info = ProjectTask::withTrashed()->select(['id', 'name', 'complete_at', 'archived_at', 'deleted_at'])->whereDialogId($this->id)->first()?->cancelAppend()->cancelHidden();
$this->name = $this->group_info ? $this->group_info->name : '';
}
break;
}
return $this;
}
/**
* 加入聊天室
* @param int|array $userid 加入的会员ID或会员ID组
* @return bool
*/
public function joinGroup($userid)
{
AbstractModel::transaction(function () use ($userid) {
foreach (is_array($userid) ? $userid : [$userid] as $value) {
if ($value > 0) {
WebSocketDialogUser::updateInsert([
'dialog_id' => $this->id,
'userid' => $value,
]);
}
}
});
return true;
}
/**
* 退出聊天室
* @param int|array $userid 加入的会员ID或会员ID组
* @return bool
*/
public function exitGroup($userid)
{
$builder = WebSocketDialogUser::whereDialogId($this->id);
if (is_array($userid)) {
$builder->whereIn('userid', $userid);
} else {
$builder->whereUserid($userid);
}
$builder->chunkById(100, function($list) {
/** @var WebSocketDialogUser $item */
foreach ($list as $item) {
if ($item->userid == $this->owner_id) {
// 群主不可退出
continue;
}
$item->delete();
}
});
return true;
}
/**
* 删除会话
* @return bool
@@ -55,27 +145,107 @@ 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();
});
return true;
}
/**
* 还原会话
* @return bool
*/
public function recoveryDialog()
{
$this->restore();
return true;
}
/**
* 检查群组类型
* @return void
*/
public function checkGroup($groupType = 'user')
{
if ($this->type !== 'group') {
throw new ApiException('仅限群组操作');
}
if ($this->group_type !== $groupType) {
throw new ApiException('操作的群组类型错误');
}
}
/**
* 获取群组名称
* @return mixed|string|null
*/
public function getGroupName()
{
if (!isset($this->appendattrs['groupName'])) {
$name = $this->name;
if ($this->type == "group") {
if ($this->group_type === 'project') {
$name = \DB::table('projects')->where('dialog_id', $this->id)->value('name');
} elseif ($this->group_type === 'task') {
$name = \DB::table('project_tasks')->where('dialog_id', $this->id)->value('name');
}
}
$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);
}
/**
* 获取对话(同时检验对话身份)
* @param $id
* @param $dialog_id
* @param bool $checkOwner 是否校验群组身份
* @return self
*/
public static function checkDialog($id)
public static function checkDialog($dialog_id, $checkOwner = false)
{
$dialog = WebSocketDialog::whereId($id)->first();
$dialog = WebSocketDialog::find($dialog_id);
if (empty($dialog)) {
throw new ApiException('对话不存在或已被删除');
throw new ApiException('对话不存在或已被删除', ['dialog_id' => $dialog_id], -4003);
}
//
$userid = User::userid();
if ($checkOwner === true && $dialog->owner_id != $userid) {
throw new ApiException('仅限群主操作');
}
//
if ($dialog->type === 'group' && $dialog->group_type === 'task') {
// 任务群对话校验是否在项目内
$project_id = intval(ProjectTask::whereDialogId($dialog->id)->value('project_id'));
@@ -86,48 +256,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;
}
@@ -135,17 +264,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' => $group_type === 'user' ? Carbon::now() : null,
]);
$dialog->save();
foreach (is_array($userid) ? $userid : [$userid] as $value) {
@@ -160,47 +292,6 @@ 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
@@ -210,12 +301,14 @@ class WebSocketDialog extends AbstractModel
public static function checkUserDialog($userid, $userid2)
{
$dialogUser = self::select(['web_socket_dialogs.*'])
->join('web_socket_dialog_users', 'web_socket_dialog_users.dialog_id', '=', 'web_socket_dialogs.id')
->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('web_socket_dialogs.type', 'user')
->whereIn('web_socket_dialog_users.userid', [$userid, $userid2])
->get();
if ($dialogUser->count() >= 2) {
return $dialogUser[0];
->first();
if ($dialogUser) {
return $dialogUser;
}
return AbstractModel::transaction(function () use ($userid2, $userid) {
$dialog = self::createInstance([

View File

@@ -8,13 +8,14 @@ use App\Tasks\PushTask;
use App\Tasks\WebSocketDialogMsgTask;
use Carbon\Carbon;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* Class WebSocketDialogMsg
* App\Models\WebSocketDialogMsg
*
* @package App\Models
* @property int $id
* @property int|null $dialog_id 对话ID
* @property string|null $dialog_type 对话类型
* @property int|null $userid 发送会员ID
* @property string|null $type 消息类型
* @property array|mixed $msg 详细消息
@@ -22,12 +23,17 @@ use Hhxsv5\LaravelS\Swoole\Task\Task;
* @property int|null $send 发送数量
* @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\WebSocketDialog|null $webSocketDialog
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg newQuery()
* @method static \Illuminate\Database\Query\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 whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereMsg($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereRead($value)
@@ -35,10 +41,14 @@ use Hhxsv5\LaravelS\Swoole\Task\Task;
* @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\Query\Builder|WebSocketDialogMsg withTrashed()
* @method static \Illuminate\Database\Query\Builder|WebSocketDialogMsg withoutTrashed()
* @mixin \Eloquent
*/
class WebSocketDialogMsg extends AbstractModel
{
use SoftDeletes;
protected $appends = [
'percentage',
];
@@ -47,6 +57,14 @@ class WebSocketDialogMsg extends AbstractModel
'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
@@ -54,11 +72,7 @@ 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'];
}
@@ -82,6 +96,22 @@ class WebSocketDialogMsg extends AbstractModel
return $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
@@ -111,13 +141,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,6 +159,161 @@ class WebSocketDialogMsg extends AbstractModel
return true;
}
/**
* 删除消息
* @return void
*/
public function deleteMsg()
{
$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();
//
$last_msg = null;
if ($this->webSocketDialog) {
$last_msg = WebSocketDialogMsg::whereDialogId($this->dialog_id)->orderByDesc('id')->first();
$this->webSocketDialog->last_at = $last_msg->created_at;
$this->webSocketDialog->save();
}
//
$dialog = WebSocketDialog::find($this->dialog_id);
if ($dialog) {
$userids = $dialog->dialogUser->pluck('userid')->toArray();
PushTask::push([
'userid' => $userids,
'msg' => [
'type' => 'dialog',
'mode' => 'delete',
'data' => [
'id' => $this->id,
'dialog_id' => $this->dialog_id,
'last_msg' => $last_msg,
'update_read' => $deleteRead ? 1 : 0
],
]
]);
}
});
}
/**
* 预览消息
* @param bool $preserveHtml 保留html格式
* @return string
*/
public function previewMsg($preserveHtml = false)
{
switch ($this->type) {
case 'text':
return $this->previewTextMsg($this->msg['text'], $preserveHtml);
case 'file':
if ($this->msg['type'] == 'img') {
return "[图片]";
}
return "[文件] {$this->msg['name']}";
default:
return "[未知的消息]";
}
}
/**
* 返回文本预览消息
* @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) {
return $text;
} else {
return strip_tags($text);
}
}
/**
* 处理文本消息内容,用于发送前
* @param $text
* @param $dialog_id
* @return mixed|string|string[]
*/
public static function formatMsg($text, $dialog_id)
{
@ini_set("pcre.backtrack_limit", 999999999);
// 图片 [:IMAGE:className:width:height:src:alt:]
preg_match_all("/<img\s+src=\"data:image\/(png|jpg|jpeg|gif);base64,(.*?)\"(.*?)>(<\/img>)*/s", $text, $matchs);
foreach ($matchs[2] as $key => $base64) {
$tmpPath = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
Base::makeDir(public_path($tmpPath));
$tmpPath .= md5s($base64) . "." . $matchs[1][$key];
if (file_put_contents(public_path($tmpPath), base64_decode($base64))) {
$imagesize = getimagesize(public_path($tmpPath));
if (Base::imgThumb(public_path($tmpPath), public_path($tmpPath) . "_thumb.jpg", 320, 0)) {
$tmpPath .= "_thumb.jpg";
}
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imagesize[0]}:{$imagesize[1]}:{$tmpPath}::]", $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);
if (file_exists(public_path($matchAsset[1]))) {
$imagesize = getimagesize(public_path($matchAsset[1]));
$text = str_replace($matchs[0][$key], "[:IMAGE:emoticon:{$imagesize[0]}:{$imagesize[1]}:{$matchAsset[1]}:{$matchName[1]}:]", $text);
}
}
// 其他网络图片
preg_match_all("/<img[^>]*?src=([\"'])(.*?\.(png|jpg|jpeg|gif))\\1[^>]*?>/is", $text, $matchs);
foreach ($matchs[2] as $key => $str) {
$tmpPath = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
Base::makeDir(public_path($tmpPath));
$tmpPath .= md5s($str) . "." . $matchs[3][$key];
if (file_exists(public_path($tmpPath))) {
$imagesize = getimagesize(public_path($tmpPath));
if (Base::imgThumb(public_path($tmpPath), public_path($tmpPath) . "_thumb.jpg", 320, 0)) {
$tmpPath .= "_thumb.jpg";
}
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imagesize[0]}:{$imagesize[1]}:{$tmpPath}::]", $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 (file_put_contents(public_path($tmpPath), $image)) {
$imagesize = getimagesize(public_path($tmpPath));
if (Base::imgThumb(public_path($tmpPath), public_path($tmpPath) . "_thumb.jpg", 320, 0)) {
$tmpPath .= "_thumb.jpg";
}
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imagesize[0]}:{$imagesize[1]}:{$tmpPath}::]", $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);
$text = str_replace($matchs[0][$key], "[:{$matchChar[1]}:{$matchId[1]}:{$matchValye[1]}:]", $text);
}
// 过滤标签
$text = strip_tags($text, '<blockquote> <strong> <pre> <ol> <ul> <li> <em> <p> <s> <u>');
$text = preg_replace("/\<(blockquote|strong|pre|ol|ul|li|em|p|s|u).*?\>/is", "<$1>", $text);
$text = preg_replace("/\[:IMAGE:(.*?):(.*?):(.*?):(.*?):(.*?):\]/i", "<img class=\"$1\" width=\"$2\" height=\"$3\" src=\"{{RemoteURL}}$4\" alt=\"$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);
return preg_replace("/^(<p><\/p>)+|(<p><\/p>)+$/i", "", $text);
}
/**
* 发送消息
* @param int $dialog_id 会话ID即 聊天室ID
@@ -150,6 +339,7 @@ class WebSocketDialogMsg extends AbstractModel
$dialog->save();
$dialogMsg->send = 1;
$dialogMsg->dialog_id = $dialog->id;
$dialogMsg->dialog_type = $dialog->type;
$dialogMsg->save();
});
Task::deliver(new WebSocketDialogMsgTask($dialogMsg->id));

View File

@@ -2,22 +2,28 @@
namespace App\Models;
use Carbon\Carbon;
/**
* Class WebSocketDialogMsgRead
* App\Models\WebSocketDialogMsgRead
*
* @package App\Models
* @property int $id
* @property int|null $dialog_id 对话ID
* @property int|null $msg_id 消息ID
* @property int|null $userid 发送会员ID
* @property int|null $mention 是否提及(被@
* @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 whereUserid($value)
@@ -30,4 +36,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

@@ -3,12 +3,13 @@
namespace App\Models;
/**
* Class WebSocketDialogUser
* App\Models\WebSocketDialogUser
*
* @package App\Models
* @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 \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser newModelQuery()
@@ -17,6 +18,8 @@ namespace App\Models;
* @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 whereMarkUnread($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

View File

@@ -3,9 +3,8 @@
namespace App\Models;
/**
* Class WebSocketTmpMsg
* App\Models\WebSocketTmpMsg
*
* @package App\Models
* @property int $id
* @property string|null $md5 MD5(会员ID-消息)
* @property string|null $msg 详细消息

View File

@@ -2,15 +2,16 @@
namespace App\Module;
use App\Exceptions\ApiException;
use App\Models\Setting;
use App\Models\Tmp;
use Cache;
use Exception;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
use Redirect;
use Request;
use Storage;
use Validator;
class Base
{
@@ -58,20 +59,55 @@ class Base
])->save();
}
/**
* 获取package配置文件
* @return array
*/
public static function getPackage()
{
return Cache::remember("Base::package", now()->addSeconds(10), function () {
$file = base_path('package.json');
if (file_exists($file)) {
$package = json_decode(file_get_contents($file), true);
return is_array($package) ? $package : [];
}
return [];
});
}
/**
* 获取版本号
* @return string
*/
public static function getVersion()
{
return Cache::remember("Base::version", now()->addSeconds(10), function () {
$file = base_path('package.json');
if (file_exists($file)) {
$packageArray = json_decode(file_get_contents($file), true);
return $packageArray['version'] ?? '1.0.0';
}
return '1.0.0';
});
$package = self::getPackage();
return $package['version'] ?? '1.0.0';
}
/**
* 获取客户端版本号
* @return string
*/
public static function getClientVersion()
{
global $_A;
if (!isset($_A["__static_client_version"])) {
$_A["__static_client_version"] = Request::header('version') ?: '0.0.1';
}
return $_A["__static_client_version"];
}
/**
* 检查客户端版本
* @param string $min 最小版本
* @return void
*/
public static function checkClientVersion($min)
{
if (version_compare(Base::getClientVersion(), $min, '<')) {
throw new ApiException('当前版本 (v' . Base::getClientVersion() . ') 过低');
}
}
/**
@@ -248,7 +284,7 @@ class Base
{
try {
Storage::makeDirectory($path);
} catch (Exception $e) {
} catch (\Throwable $e) {
}
if (!file_exists($path)) {
self::makeDir(dirname($path));
@@ -316,19 +352,15 @@ class Base
{
if (strtolower($charset) == 'utf-8') {
if (Base::getStrlen($string) <= $length) return $string;
$strcut = str_replace(array('&amp;', '&quot;', '&lt;', '&gt;'), array('&', '"', '<', '>'), $string);
$strcut = Base::utf8Substr($strcut, $length, $start);
$strcut = str_replace(array('&', '"', '<', '>'), array('&amp;', '&quot;', '&lt;', '&gt;'), $strcut);
$strcut = Base::utf8Substr($string, $length, $start);
return $strcut . $dot;
} else {
$length = $length * 2;
if (strlen($string) <= $length) return $string;
$string = str_replace(array('&amp;', '&quot;', '&lt;', '&gt;'), array('&', '"', '<', '>'), $string);
$strcut = '';
for ($i = 0; $i < $length; $i++) {
$strcut .= ord($string[$i]) > 127 ? $string[$i] . $string[++$i] : $string[$i];
}
$strcut = str_replace(array('&', '"', '<', '>'), array('&amp;', '&quot;', '&lt;', '&gt;'), $strcut);
}
return $strcut . $dot;
}
@@ -453,7 +485,7 @@ class Base
try {
$array = json_decode($string, true);
return is_array($array) ? $array : [];
} catch (Exception $e) {
} catch (\Throwable) {
return [];
}
}
@@ -471,7 +503,7 @@ class Base
}
try {
return json_encode($array, $options);
} catch (Exception $e) {
} catch (\Throwable) {
return '';
}
}
@@ -684,24 +716,20 @@ class Base
/**
* 判断两个地址域名是否相同
* @param string $var1
* @param string $var2
* @param string|array $var2
* @return bool
*/
public static function hostContrast($var1, $var2)
{
$arr1 = parse_url($var1);
$arr2 = parse_url($var2);
$host1 = $arr1['host'] ?? $var1;
//
$host1 = $var1;
if (isset($arr1['host'])) {
$host1 = $arr1['host'];
$host2 = [];
foreach (is_array($var2) ? $var2 : [$var2] as $url) {
$arr2 = parse_url($url);
$host2[] = $arr2['host'] ?? $url;
}
//
$host2 = $var2;
if (isset($arr2['host'])) {
$host2 = $arr2['host'];
}
return $host1 == $host2;
return in_array($host1, $host2);
}
/**
@@ -725,6 +753,7 @@ class Base
*/
public static function fillUrl($str = '')
{
global $_A;
if (is_array($str)) {
foreach ($str as $key => $item) {
$str[$key] = Base::fillUrl($item);
@@ -743,9 +772,12 @@ class Base
) {
return $str;
} else {
if ($_A['__fill_url_remote_url'] === true) {
return "{{RemoteURL}}" . $str;
}
try {
return url($str);
} catch (\Throwable $e) {
} catch (\Throwable) {
return self::getSchemeAndHost() . "/" . $str;
}
}
@@ -766,7 +798,7 @@ class Base
}
try {
$find = url('');
} catch (\Throwable $e) {
} catch (\Throwable) {
$find = self::getSchemeAndHost();
}
return Base::leftDelete($str, $find . '/');
@@ -782,6 +814,31 @@ class Base
return $scheme.($_SERVER['HTTP_HOST'] ?? '');
}
/**
* 地址后拼接参数
* @param $url
* @param $parames
* @return mixed|string
*/
public static function urlAddparameter($url, $parames)
{
if ($parames && is_array($parames)) {
$array = [];
foreach ($parames as $key => $val) {
$array[] = $key . "=" . $val;
}
if ($array) {
$query = implode("&", $array);
if (str_contains($url, "?")) {
$url .= "&" . $query;
} else {
$url .= "?" . $query;
}
}
}
return $url;
}
/**
* 格式化内容图片地址
* @param $content
@@ -830,13 +887,16 @@ class Base
/**
* 数组只保留数字的
* @param $array
* @param bool $int 是否格式化值
* @return array
*/
public static function arrayRetainInt($array)
public static function arrayRetainInt($array, $int = false)
{
foreach ($array as $k => $v) {
if (!is_numeric($v)) {
unset($array[$k]);
} elseif ($int === true) {
$array[$k] = intval($v);
}
}
return array_values($array);
@@ -931,13 +991,16 @@ class Base
/**
* 检测邮箱格式
* @param string $str 需要检测的字符串
* @return int
* @param $str
* @return bool
*/
public static function isMail($str)
public static function isEmail($str)
{
$RegExp = '/^[a-z0-9][a-z\.0-9-_]+@[a-z0-9_-]+(?:\.[a-z]{0,3}\.[a-z]{0,2}|\.[a-z]{0,3}|\.[a-z]{0,2})$/i';
return preg_match($RegExp, $str);
if (filter_var($str, FILTER_VALIDATE_EMAIL)) {
return true;
} else {
return false;
}
}
/**
@@ -1153,11 +1216,12 @@ class Base
/**
* 获取或设置
* @param $setname //配置名称
* @param bool $array //保存内容
* @param $setname // 配置名称
* @param bool $array // 保存内容
* @param false $isUpdate // 保存内容为更新模式,默认否
* @return array
*/
public static function setting($setname, $array = false)
public static function setting($setname, $array = false, $isUpdate = false)
{
global $_A;
if (empty($setname)) {
@@ -1168,15 +1232,19 @@ class Base
}
$setting = [];
$row = Setting::whereName($setname)->first();
if (!empty($row)) {
if ($row) {
$setting = Base::string2array($row->setting);
} else {
$row = Setting::createInstance(['name' => $setname]);
$row->save();
}
if ($array !== false) {
$setting = $array;
$row->updateInstance(['setting' => $array]);
if ($isUpdate && is_array($array)) {
$setting = array_merge($setting, $array);
} else {
$setting = $array;
}
$row->updateInstance(['setting' => $setting]);
$row->save();
}
$_A["__static_setting_" . $setname] = $setting;
@@ -1559,7 +1627,7 @@ class Base
}
/**
* 用户名、邮箱、手机号、银行卡号中间字符串以*隐藏
* 用户名、邮箱、手机号、银行卡号中间字符串以*隐藏
* @param $str
* @return string
*/
@@ -1658,24 +1726,46 @@ class Base
*/
public static function timeDiff($s, $e)
{
$d = $e - $s;
if ($d > 86400) {
$day = floor($d / 86400);
$hour = ceil(($d - ($day * 86400)) / 3600);
if ($hour > 0) {
return $day . '天' . $hour . '小时';
} else {
return $day . '天';
}
} elseif ($d > 3600) {
return ceil($d / 3600) . '小时';
} elseif ($d > 60) {
return ceil($d / 60) . '分钟';
} elseif ($d > 1) {
return '1分钟内';
} else {
return '0秒';
$time = $e - $s;
$days = 0;
if ($time >= 86400) { // 如果大于1天
$days = (int)($time / 86400);
$time = $time % 86400; // 计算天后剩余的毫秒数
}
$hours = 0;
if ($time >= 3600) { // 如果大于1小时
$hours = (int)($time / 3600);
$time = $time % 3600; // 计算小时后剩余的毫秒数
}
$minutes = ceil($time / 60); // 剩下的毫秒数都算作分
$daysStr = $days > 0 ? $days . '天' : '';
$hoursStr = ($hours > 0 || ($days > 0 && $minutes > 0)) ? $hours . '时' : '';
$minuteStr = ($minutes > 0) ? $minutes . '分' : '';
return $daysStr . $hoursStr . $minuteStr;
}
/**
* 时间秒数格式化
* @param int $time 时间秒数
* @return string
*/
public static function timeFormat($time)
{
$days = 0;
if ($time >= 86400) { // 如果大于1天
$days = (int)($time / 86400);
$time = $time % 86400; // 计算天后剩余的毫秒数
}
$hours = 0;
if ($time >= 3600) { // 如果大于1小时
$hours = (int)($time / 3600);
$time = $time % 3600; // 计算小时后剩余的毫秒数
}
$minutes = ceil($time / 60); // 剩下的毫秒数都算作分
$daysStr = $days > 0 ? $days . '天' : '';
$hoursStr = ($hours > 0 || ($days > 0 && $minutes > 0)) ? $hours . '时' : '';
$minuteStr = ($minutes > 0) ? $minutes . '分' : '';
return $daysStr . $hoursStr . $minuteStr;
}
/**
@@ -2105,7 +2195,7 @@ class Base
if ($width > 0 || $height > 0) {
$scaleName = "_{WIDTH}x{HEIGHT}";
if (isset($param['scale'][2])) {
$scaleName .= $param['scale'][2];
$scaleName .= "_c{$param['scale'][2]}";
}
}
}
@@ -2216,10 +2306,7 @@ class Base
$type = ['zip'];
break;
case 'file':
$type = ['jpg', 'jpeg', 'png', 'gif', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'esp', 'pdf', 'rar', 'zip', 'gz'];
break;
case 'office':
$type = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'];
$type = ['jpg', 'jpeg', 'png', 'gif', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'esp', 'pdf', 'rar', 'zip', 'gz', 'ai', 'avi', 'bmp', 'cdr', 'eps', 'mov', 'mp3', 'mp4', 'pr', 'psd', 'svg', 'tif'];
break;
case 'firmware':
$type = ['img', 'tar', 'bin'];
@@ -2227,8 +2314,34 @@ class Base
case 'md':
$type = ['md'];
break;
case 'node_template':
$type = ['csv'];
case 'desktop':
$type = ['yml', 'yaml', 'dmg', 'pkg', 'blockmap', 'zip', 'exe', 'msi'];
break;
case 'more':
$type = [
'text', 'md', 'markdown',
'drawio',
'mind',
'docx', 'wps', 'doc', 'xls', 'xlsx', 'ppt', 'pptx',
'jpg', 'jpeg', 'png', 'gif', 'bmp', 'ico', 'raw',
'rar', 'zip', 'jar', '7-zip', 'tar', 'gzip', '7z',
'tif', 'tiff',
'dwg', 'dxf',
'ofd',
'pdf',
'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',
'mp3', 'wav', 'mp4', 'flv',
'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm',
'xmind',
'rp',
];
break;
default:
return Base::retError('错误的类型参数');
@@ -2242,11 +2355,13 @@ class Base
if ($param['size'] > 0 && $fileSize > $param['size'] * 1024) {
return Base::retError('文件大小超限,最大限制:' . $param['size'] . 'KB');
}
} catch (Exception $e) {
} catch (\Throwable) {
$fileSize = 0;
}
$scaleName = "";
if ($param['fileName']) {
if ($param['fileName'] === true) {
$fileName = $file->getClientOriginalName();
} elseif ($param['fileName']) {
$fileName = $param['fileName'];
} else {
if ($param['scale'] && is_array($param['scale'])) {
@@ -2254,7 +2369,7 @@ class Base
if ($width > 0 || $height > 0) {
$scaleName = "_{WIDTH}x{HEIGHT}";
if (isset($param['scale'][2])) {
$scaleName .= $param['scale'][2];
$scaleName .= "_c{$param['scale'][2]}";
}
}
}
@@ -2349,6 +2464,37 @@ class Base
}
}
/**
* 上传文件移动
* @param array $uploadResult
* @param string $newPath "/" 结尾
* @return array
*/
public static function uploadMove($uploadResult, $newPath)
{
if (str_ends_with($newPath, "/") && file_exists($uploadResult['file'])) {
Base::makeDir(public_path($newPath));
$oldPath = dirname($uploadResult['path']) . "/";
$newFile = str_replace($oldPath, $newPath, $uploadResult['file']);
if (rename($uploadResult['file'], $newFile)) {
$oldUrl = $uploadResult['url'];
$uploadResult['file'] = $newFile;
$uploadResult['path'] = str_replace($oldPath, $newPath, $uploadResult['path']);
$uploadResult['url'] = str_replace($oldPath, $newPath, $uploadResult['url']);
if ($uploadResult['thumb'] == $oldUrl) {
$uploadResult['thumb'] = $uploadResult['url'];
} elseif ($uploadResult['thumb']) {
$oldThumb = substr($uploadResult['thumb'], strpos($uploadResult['thumb'], $newPath));
$newThumb = str_replace($oldPath, $newPath, $oldThumb);
if (file_exists(public_path($oldThumb)) && rename(public_path($oldThumb), public_path($newThumb))) {
$uploadResult['thumb'] = str_replace($oldPath, $newPath, $uploadResult['thumb']);
}
}
}
}
return $uploadResult;
}
/**
* 生成缩略图
* @param string $src_img 源图绝对完整地址{带文件名及后缀名}
@@ -2466,7 +2612,7 @@ class Base
try {
$white = imagecolorallocate($dst, 255, 255, 255);
imagefill($dst, 0, 0, $white);
} catch (Exception $e) {
} catch (\Throwable) {
}
if (function_exists('imagecopyresampled')) {
@@ -2620,16 +2766,19 @@ class Base
/**
* 遍历获取文件
* @param $dir
* @param bool $subdirectory 是否遍历子目录
* @return array
*/
public static function readDir($dir)
public static function readDir($dir, $subdirectory = true)
{
$files = array();
$dir_list = scandir($dir);
foreach ($dir_list as $file) {
if ($file != '..' && $file != '.') {
if (is_dir($dir . '/' . $file)) {
$files = array_merge($files, self::readDir($dir . '/' . $file));
if ($subdirectory) {
$files = array_merge($files, self::readDir($dir . '/' . $file, $subdirectory));
}
} else {
$files[] = $dir . "/" . $file;
}
@@ -2858,4 +3007,54 @@ class Base
$matrix = array_unique($matrix, SORT_REGULAR);
return array_merge($matrix);
}
/**
* 字节转格式
* @param $bytes
* @return string
*/
public static function readableBytes($bytes)
{
$i = floor(log($bytes) / log(1024));
$sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
return sprintf('%.02F', $bytes / pow(1024, $i)) * 1 . ' ' . $sizes[$i];
}
/**
* 去除emoji表情
* @param $str
* @return string|string[]|null
*/
public static function filterEmoji($str)
{
return preg_replace_callback(
'/./u',
function (array $match) {
return strlen($match[0]) >= 4 ? '' : $match[0];
},
$str);
}
/**
* 统一验证器
* @param $data
* @param $messages
*/
public static function validator($data, $messages) {
$rules = [];
foreach ($messages as $key => $item) {
$keys = explode(".", $key);
if (isset($keys[1])) {
if (isset($rules[$keys[0]])) {
$rules[$keys[0]] = $rules[$keys[0]] . '|' . $keys[1];
} else {
$rules[$keys[0]] = $keys[1];
}
}
}
$validator = Validator::make($data, $rules, $messages);
if ($validator->fails()) {
throw new ApiException($validator->errors()->first());
}
}
}

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

@@ -0,0 +1,144 @@
<?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
{
public $title;
public $headings = [];
public $data = [];
public $typeLists = [];
public $typeNumber = 0;
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 store($fileName = '') {
if (empty($fileName)) {
$fileName = date("YmdHis") . '.xls';
}
try {
return Excel::store($this, $fileName);
} catch (Exception $e) {
return "导出错误:" . $e->getMessage();
} catch (\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 $e) {
return "导出错误:" . $e->getMessage();
} catch (\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) {
$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

@@ -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([

View File

@@ -23,6 +23,14 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot()
{
//
\Illuminate\Database\Query\Builder::macro('rawSql', function(){
return array_reduce($this->getBindings(), function($sql, $binding){
return preg_replace('/\?/', is_numeric($binding) ? $binding : "'".$binding."'" , $sql, 1);
}, $this->toSql());
});
\Illuminate\Database\Eloquent\Builder::macro('rawSql', function(){
return ($this->getQuery()->rawSql());
});
}
}

View File

@@ -127,9 +127,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,10 +145,10 @@ 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);
}
}
@@ -204,7 +206,19 @@ class WebSocketService implements WebSocketHandlerInterface
*/
private function deleteUser($fd)
{
WebSocket::whereFd($fd)->delete();
$array = [];
WebSocket::whereFd($fd)->chunk(10, function($list) use (&$array) {
/** @var WebSocket $item */
foreach ($list as $item) {
$item->delete();
if ($item->path && str_starts_with($item->path, "/single/file/")) {
$array[$item->path] = $item->path;
}
}
});
foreach ($array as $path) {
$this->pushPath($path);
}
}
/**

View File

@@ -27,7 +27,7 @@ abstract class AbstractTask extends Task
{
try {
$this->start();
} catch (\Exception $e) {
} catch (\Throwable $e) {
$this->info($e);
$this->failed($e);
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Tasks;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Models\AbstractModel;
use App\Models\ProjectTask;
use App\Module\Base;
use Carbon\Carbon;
/**
* 完成的任务自动归档
* Class AutoArchivedTask
* @package App\Tasks
*/
class AutoArchivedTask extends AbstractTask
{
public function __construct()
{
//
}
public function start()
{
$setting = Base::setting('system');
if ($setting['auto_archived'] === 'open') {
$archivedDay = floatval($setting['archived_day']);
if ($archivedDay > 0) {
$archivedDay = min(100, $archivedDay);
$archivedTime = Carbon::now()->subDays($archivedDay);
//获取已完成未归档的任务
$taskLists = ProjectTask::whereNotNull('complete_at')
->where('complete_at', '<=', $archivedTime)
->where('archived_userid', 0)
->whereNull('archived_at')
->take(100)
->get();
foreach ($taskLists AS $task) {
$task->archivedTask(Carbon::now(), true);
}
}
}
}
}

View File

@@ -0,0 +1,264 @@
<?php
namespace App\Tasks;
use App\Models\ProjectTask;
use App\Models\ProjectTaskMailLog;
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()
{
//
}
public function start()
{
$setting = Base::setting('emailSetting');
// 任务通知
if ($setting['notice'] === 'open') {
$start = intval($setting['task_start_minute']);
$hours = floatval($setting['task_remind_hours']);
$hours2 = floatval($setting['task_remind_hours2']);
if ($start > -1) {
ProjectTask::whereNull("complete_at")
->whereNull("archived_at")
->whereBetween("start_at", [
Carbon::now()->subMinutes($start + 10),
Carbon::now()->subMinutes($start)
])->chunkById(100, function ($tasks) {
/** @var ProjectTask $task */
foreach ($tasks as $task) {
$this->taskEmail($task, 0);
}
});
}
if ($hours > -1) {
ProjectTask::whereNull("complete_at")
->whereNull("archived_at")
->whereBetween("end_at", [
Carbon::now()->addMinutes($hours * 60),
Carbon::now()->addMinutes($hours * 60 + 10)
])->chunkById(100, function ($tasks) {
/** @var ProjectTask $task */
foreach ($tasks as $task) {
$this->taskEmail($task, 1);
}
});
}
if ($hours2 > -1) {
ProjectTask::whereNull("complete_at")
->whereNull("archived_at")
->whereBetween("end_at", [
Carbon::now()->subMinutes($hours2 * 60 + 10),
Carbon::now()->subMinutes($hours2 * 60)
])->chunkById(100, function ($tasks) {
/** @var ProjectTask $task */
foreach ($tasks as $task) {
$this->taskEmail($task, 2);
}
});
}
}
// 消息通知
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.email", 0);
if ($userMinute > -1) {
$builder->clone()
->where("web_socket_dialog_msgs.dialog_type", "user")
->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")
->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");
});
}
}
}
/**
* 任务过期前、超期后提醒
* @param ProjectTask $task
* @param int $type
* @return void
*/
private function taskEmail(ProjectTask $task, int $type)
{
$userids = $task->taskUser->where('owner', 1)->pluck('userid')->toArray();
if (empty($userids)) {
return;
}
$users = User::whereIn('userid', $userids)->get();
if (empty($users)) {
return;
}
$setting = Base::setting('emailSetting');
/** @var User $user */
foreach ($users as $user) {
$data = [
'type' => $type,
'userid' => $user->userid,
'task_id' => $task->id,
];
$emailLog = ProjectTaskMailLog::where($data)->exists();
if ($emailLog) {
continue;
}
try {
if (!Base::isEmail($user->email)) {
throw new \Exception("User email '{$user->email}' address error");
}
$subject = match ($type) {
1 => env('APP_NAME') . " 任务提醒",
2 => env('APP_NAME') . " 任务过期提醒",
default => env('APP_NAME') . " 任务开始提醒",
};
$content = view('email.task', [
'type' => str_replace([0, 1, 2], ['start', 'before', 'after'], $type),
'user' => $user,
'task' => $task,
'setting' => $setting,
])->render();
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($user->email)
->subject($subject)
->html($content))
->send();
$data['is_send'] = 1;
} catch (\Throwable $e) {
$data['send_error'] = $e->getMessage();
}
$data['email'] = $user->email;
ProjectTaskMailLog::createInstance($data)->save();
}
}
/**
* 未读消息通知
* @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.email", 0)
->where("web_socket_dialog_msgs.dialog_type", $dialogType)
->take(100)
->get();
if (empty($data)) {
continue;
}
$user = User::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);
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/{$dialogId}",
'dialogName' => $dialogName,
'unread' => count($items),
'items' => $items,
])->render();
$content = str_replace("{{RemoteURL}}", config("app.url") . "/", $content);
}
try {
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($user->email)
->subject($subject)
->html($content))
->send();
} catch (\Throwable $e) {
info("unreadMsgEmail: " . $e->getMessage());
}
WebSocketDialogMsgRead::whereIn('id', $data->pluck('r_id'))->update([
'email' => 1
]);
}
}
}

View File

@@ -172,7 +172,7 @@ class PushTask extends AbstractTask
try {
$swoole->push($fid, Base::array2json($msg));
$tmpMsgId > 0 && WebSocketTmpMsg::whereId($tmpMsgId)->update(['send' => 1]);
} catch (\Exception $e) {
} catch (\Throwable) {
}
}

View File

@@ -0,0 +1,38 @@
<?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 = [])
{
$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);
}
}

View File

@@ -8,6 +8,7 @@ use App\Models\User;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogMsgRead;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Request;
@@ -33,6 +34,11 @@ class WebSocketDialogMsgTask extends AbstractTask
public function start()
{
global $_A;
$_A = [
'__fill_url_remote_url' => true,
];
//
$msg = WebSocketDialogMsg::find($this->id);
if (empty($msg)) {
return;
@@ -43,30 +49,51 @@ class WebSocketDialogMsgTask extends AbstractTask
}
// 推送目标①:群成员
$array = [];
$userids = $dialog->dialogUser->pluck('userid')->toArray();
foreach ($userids AS $userid) {
if ($userid == $msg->userid) {
continue;
}
$mention = preg_match("/<span class=\"mention user\" data-id=\"[0|{$userid}]\">/", $msg->type === 'text' ? $msg->msg['text'] : '');
WebSocketDialogMsgRead::createInstance([
'dialog_id' => $msg->dialog_id,
'msg_id' => $msg->id,
'userid' => $userid,
'mention' => $mention,
])->saveOrIgnore();
$array[$userid] = $mention;
}
// 更新已发送数量
$msg->send = WebSocketDialogMsgRead::whereMsgId($msg->id)->count();
$msg->save();
// 开始推送消息
PushTask::push([
'userid' => $userids,
'ignoreFd' => $this->ignoreFd,
'msg' => [
'type' => 'dialog',
'mode' => 'add',
'data' => $msg->toArray(),
]
foreach ($array as $userid => $mention) {
PushTask::push([
'userid' => $userid,
'ignoreFd' => $this->ignoreFd,
'msg' => [
'type' => 'dialog',
'mode' => 'add',
'data' => array_merge($msg->toArray(), [
'mention' => $mention,
]),
]
]);
}
// umeng推送app
$msgTitle = User::userid2nickname($msg->userid);
if ($dialog->type == 'group') {
$msgTitle = "{$dialog->getGroupName()} ($msgTitle)";
}
$umengMsg = new PushUmengMsg(array_keys($array), [
'title' => $msgTitle,
'body' => $msg->previewMsg(),
'description' => "消息推送-ID:{$msg->id}",
'seconds' => 3600,
'badge' => 1,
]);
Task::deliver($umengMsg);
// 推送目标②:正在打开这个任务会话的会员
if ($dialog->type == 'group' && $dialog->group_type == 'task') {

View File

@@ -73,7 +73,7 @@ class runLoader
}
}
$array = getopt('', ['port:', 'ssl:', 'mode:']);
$array = getopt('', ['port:', 'mode:']);
$loader = new runLoader();
if (isset($array['mode'])) {
@@ -84,9 +84,6 @@ $data = [];
if (isset($array['port'])) {
$data['APP_PORT'] = $array['port'];
}
if (isset($array['ssl'])) {
$data['APP_PORT_SSL'] = $array['ssl'];
}
if ($data) {
$loader->modifyEnv($data);
}

60
cliff.toml Normal file
View File

@@ -0,0 +1,60 @@
# configuration file for git-cliff (0.1.0)
[changelog]
# changelog header
header = """
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://tera.netlify.app/docs/#introduction
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}]
{% else %}\
## [Unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
"""
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "Features"},
{ message = "^fix", group = "Bug Fixes"},
{ message = "^doc", group = "Documentation"},
{ message = "^perf", group = "Performance"},
{ message = "^pref", group = "Performance"},
{ message = "^refactor", group = "Refactor"},
{ message = "^style", group = "Styling"},
{ message = "^test", group = "Testing"},
{ message = "^chore\\(release\\): prepare for", skip = true},
{ message = "^chore", group = "Miscellaneous Tasks"},
{ body = ".*security", group = "Security"},
]
# filter out the commits that are not matched by commit parsers
filter_commits = true
# glob pattern for matching git tags
tag_pattern = "v[0-9]*"
# regex for skipping tags
skip_tags = "v0.1.0-beta.1"
# regex for ignoring tags
ignore_tags = ""
# sort the tags chronologically
date_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"

391
cmd
View File

@@ -12,6 +12,8 @@ OK="${Green}[OK]${Font}"
Error="${Red}[错误]${Font}"
cur_path="$(pwd)"
cur_arg=$@
COMPOSE="docker-compose"
judge() {
if [[ 0 -eq $? ]]; then
@@ -23,10 +25,26 @@ judge() {
fi
}
rand() {
local min=$1
local max=$(($2-$min+1))
local num=$(($RANDOM+1000000000))
echo $(($num%$max+$min))
}
rand_string() {
local lan=$1
if [[ `uname` == 'Linux' ]]; then
echo "$(date +%s%N | md5sum | cut -c 1-${lan})"
else
echo "$(docker run -it --rm alpine sh -c "date +%s%N | md5sum | cut -c 1-${lan}")"
fi
}
supervisorctl_restart() {
RES=`docker-compose exec php /bin/bash -c "supervisorctl update $1"`
if [ -z "$RES" ];then
docker-compose exec php /bin/bash -c "supervisorctl restart $1"
local RES=`run_exec php "supervisorctl update $1"`
if [ -z "$RES" ]; then
run_exec php "supervisorctl restart $1"
else
echo -e "$RES"
fi
@@ -38,48 +56,199 @@ check_docker() {
echo -e "${Error} ${RedBG} 未安装 Docker${Font}"
exit 1
fi
docker-compose --version &> /dev/null
docker-compose version &> /dev/null
if [ $? -ne 0 ]; then
echo -e "${Error} ${RedBG} 未安装 Docker-compose${Font}"
docker compose version &> /dev/null
if [ $? -ne 0 ]; then
echo -e "${Error} ${RedBG} 未安装 Docker-compose${Font}"
exit 1
fi
COMPOSE="docker compose"
fi
if [[ -n `$COMPOSE version | grep -E "\sv*1"` ]]; then
$COMPOSE version
echo -e "${Error} ${RedBG} Docker-compose 版本过低请升级至v2+${Font}"
exit 1
fi
}
check_node() {
npm --version > /dev/null
npm --version &> /dev/null
if [ $? -ne 0 ]; then
echo -e "${Error} ${RedBG} 未安装nodejs${Font}"
exit 1
fi
}
docker_name() {
echo `$COMPOSE ps | awk '{print $1}' | grep "\-$1\-"`
}
run_compile() {
local type=$1
check_node
if [ ! -d "./node_modules" ]; then
npm install
fi
run_exec php "php bin/run --mode=$type"
supervisorctl_restart php
#
if [ "$type" = "prod" ]; then
rm -rf "./public/js/build"
npx mix --production
echo "$(rand_string 16)" > ./public/js/hash
else
npx mix watch --hot
fi
}
run_electron() {
local argv=$@
check_node
if [ ! -d "./node_modules" ]; then
npm install
fi
if [ ! -d "./electron/node_modules" ]; then
pushd electron
npm install
popd
fi
#
if [ -d "./electron/dist" ]; then
rm -rf "./electron/dist"
fi
if [ -d "./electron/public" ] && [ "$argv" != "--nobuild" ]; then
rm -rf "./electron/public"
fi
mkdir -p ./electron/public
cp ./electron/index.html ./electron/public/index.html
#
if [ "$argv" != "dev" ] && [ "$argv" != "--nobuild" ]; then
npx mix --production -- --env --electron
fi
if [ "$argv" == "dev" ]; then
run_exec php "php bin/run --mode=$argv"
supervisorctl_restart php
fi
node ./electron/build.js $argv
}
run_exec() {
local container=$1
local cmd=$2
local name=`docker_name $container`
if [ -z "$name" ]; then
echo -e "${Error} ${RedBG} 没有找到 $container 容器! ${Font}"
exit 1
fi
docker exec -it "$name" /bin/sh -c "$cmd"
}
run_mysql() {
if [ "$1" = "backup" ]; then
# 备份数据库
database=$(env_get DB_DATABASE)
username=$(env_get DB_USERNAME)
password=$(env_get DB_PASSWORD)
mkdir -p ${cur_path}/docker/mysql/backup
filename="${cur_path}/docker/mysql/backup/${database}_$(date "+%Y%m%d%H%M%S").sql.gz"
run_exec mariadb "exec mysqldump --databases $database -u$username -p$password" | gzip > $filename
judge "备份数据库"
[ -f "$filename" ] && echo -e "备份文件:$filename"
elif [ "$1" = "recovery" ]; then
# 还原数据库
database=$(env_get DB_DATABASE)
username=$(env_get DB_USERNAME)
password=$(env_get DB_PASSWORD)
mkdir -p ${cur_path}/docker/mysql/backup
list=`ls -1 "${cur_path}/docker/mysql/backup" | grep ".sql.gz"`
if [ -z "$list" ]; then
echo -e "${Error} ${RedBG} 没有备份文件!${Font}"
exit 1
fi
echo "$list"
read -rp "请输入备份文件名称还原:" inputname
filename="${cur_path}/docker/mysql/backup/${inputname}"
if [ ! -f "$filename" ]; then
echo -e "${Error} ${RedBG} 备份文件:${inputname} 不存在! ${Font}"
exit 1
fi
container_name=`docker_name mariadb`
if [ -z "$container_name" ]; then
echo -e "${Error} ${RedBG} 没有找到 mariadb 容器! ${Font}"
exit 1
fi
docker cp $filename $container_name:/
run_exec mariadb "gunzip < /$inputname | mysql -u$username -p$password $database"
run_exec php "php artisan migrate"
judge "还原数据库"
fi
}
env_get() {
key=$1
value=`cat ${cur_path}/.env | grep "^$key=" | awk -F '=' '{print $2}'`
local key=$1
local value=`cat ${cur_path}/.env | grep "^$key=" | awk -F '=' '{print $2}'`
echo "$value"
}
env_set() {
key=$1
val=$2
exist=`cat ${cur_path}/.env | grep "^$key="`
if [ -z "$exist" ];then
local key=$1
local val=$2
local exist=`cat ${cur_path}/.env | grep "^$key="`
if [ -z "$exist" ]; then
echo "$key=$val" >> $cur_path/.env
else
command="sed -i '/^$key=/c\\$key=$val' /www/.env"
docker run -it --rm -v ${cur_path}:/www alpine sh -c "$command"
if [[ `uname` == 'Linux' ]]; then
sed -i "/^${key}=/c\\${key}=${val}" ${cur_path}/.env
else
docker run -it --rm -v ${cur_path}:/www alpine sh -c "sed -i "/^${key}=/c\\${key}=${val}" /www/.env"
fi
if [ $? -ne 0 ]; then
echo -e "${Error} ${RedBG} 设置env参数失败${Font}"
exit 1
fi
fi
}
env_init() {
if [ ! -f ".env" ];then
if [ ! -f ".env" ]; then
cp .env.docker .env
fi
if [ -z "$(env_get DB_ROOT_PASSWORD)" ];then
env_set DB_ROOT_PASSWORD "$(docker run -it --rm alpine sh -c "date +%s%N | md5sum | cut -c 1-16")"
if [ -z "$(env_get DB_ROOT_PASSWORD)" ]; then
env_set DB_ROOT_PASSWORD "$(rand_string 16)"
fi
if [ -z "$(env_get DOCKER_ID)" ];then
env_set DOCKER_ID "$(docker run -it --rm alpine sh -c "date +%s%N | md5sum | cut -c 1-6")"
if [ -z "$(env_get APP_ID)" ]; then
env_set APP_ID "$(rand_string 6)"
fi
if [ -z "$(env_get APP_IPPR)" ]; then
env_set APP_IPPR "10.$(rand 50 100).$(rand 100 200)"
fi
}
arg_get() {
local find="n"
local value=""
for var in $cur_arg; do
if [[ "$find" == "y" ]]; then
if [[ ! $var =~ "--" ]]; then
value=$var
fi
break
fi
if [[ "--$1" == "$var" ]] || [[ "-$1" == "$var" ]]; then
find="y"
value="yes"
fi
done
echo $value
}
is_arm() {
local get_arch=`arch`
if [[ $get_arch =~ "aarch" ]] || [[ $get_arch =~ "arm" ]]; then
echo "yes"
else
echo "no"
fi
}
@@ -87,38 +256,79 @@ env_init() {
####################################################################################
####################################################################################
COMPOSE="docker-compose"
env_init
check_docker
if [[ "$1" != "electron" ]]; then
check_docker
env_init
fi
if [ $# -gt 0 ];then
if [ $# -gt 0 ]; then
if [[ "$1" == "init" ]] || [[ "$1" == "install" ]]; then
shift 1
networkid=`docker network ls | grep "dooteak-networks-" | awk '{print $1}'`
if [ -n "$networkid" ]; then
docker network rm "$networkid" > /dev/null
# 判断架构
if [[ "$(is_arm)" == "yes" ]] && [[ -z "$(arg_get force)" ]]; then
echo -e "${Error} ${RedBG}暂不支持arm架构强制安装请使用./cmd install --force${Font}"
exit 1
fi
rm -rf composer.lock
rm -rf package-lock.json
mkdir -p ${cur_path}/docker/mysql/data
chmod -R 777 ${cur_path}/docker/mysql/data
# 初始化文件
if [[ -n "$(arg_get relock)" ]]; then
rm -rf node_modules
rm -rf package-lock.json
rm -rf vendor
rm -rf composer.lock
fi
mkdir -p "${cur_path}/docker/log/supervisor"
mkdir -p "${cur_path}/docker/mysql/data"
chmod -R 775 "${cur_path}/docker/log/supervisor"
chmod -R 775 "${cur_path}/docker/mysql/data"
# 启动容器
[[ "$(arg_get port)" -gt 0 ]] && env_set APP_PORT "$(arg_get port)"
$COMPOSE up php -d
# 安装composer依赖
run_exec php "composer install"
if [ ! -f "${cur_path}/vendor/autoload.php" ]; then
run_exec php "composer config repo.packagist composer https://packagist.phpcomposer.com"
run_exec php "composer install"
run_exec php "composer config --unset repos.packagist"
fi
if [ ! -f "${cur_path}/vendor/autoload.php" ]; then
echo -e "${Error} ${RedBG}composer install 失败,请重试! ${Font}"
exit 1
fi
[[ -z "$(env_get APP_KEY)" ]] && run_exec php "php artisan key:generate"
run_exec php "php bin/run --mode=prod"
# 检查数据库
remaining=20
while [ ! -f "${cur_path}/docker/mysql/data/$(env_get DB_DATABASE)/db.opt" ]; do
((remaining=$remaining-1))
if [ $remaining -lt 0 ]; then
echo -e "${Error} ${RedBG} 数据库初始化失败! ${Font}"
exit 1
fi
chmod -R 775 "${cur_path}/docker/mysql/data"
sleep 3
done
run_exec php "php artisan migrate --seed"
if [ ! -f "${cur_path}/docker/mysql/data/$(env_get DB_DATABASE)/$(env_get DB_PREFIX)migrations.ibd" ]; then
echo -e "${Error} ${RedBG} 数据库安装失败! ${Font}"
exit 1
fi
# 设置初始化密码
res=`run_exec mariadb "sh /etc/mysql/repassword.sh"`
$COMPOSE up -d
$COMPOSE restart php
$COMPOSE exec php /bin/bash -c "composer install"
[ -z "$(env_get APP_KEY)" ] && $COMPOSE exec php /bin/bash -c "php artisan key:generate"
$COMPOSE exec php /bin/bash -c "php artisan migrate --seed"
$COMPOSE exec php /bin/bash -c "php bin/run --port=2222 --ssl=2223"
$COMPOSE exec php /bin/bash -c "php bin/run --mode=prod"
$COMPOSE stop
$COMPOSE start
supervisorctl_restart php
echo -e "${OK} ${GreenBG} 安装完成 ${Font}"
echo -e "地址: http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
echo -e "$res"
elif [[ "$1" == "update" ]]; then
shift 1
run_mysql backup
git fetch --all
git reset --hard origin/$(git branch | sed -n -e 's/^\* \(.*\)/\1/p')
git pull
$COMPOSE exec php /bin/bash -c "composer update"
$COMPOSE exec php /bin/bash -c "php artisan migrate"
run_exec php "composer update"
run_exec php "php artisan migrate"
supervisorctl_restart php
$COMPOSE up -d
elif [[ "$1" == "uninstall" ]]; then
shift 1
read -rp "确定要卸载(含:删除容器、数据库、日志)吗?(y/n): " uninstall
@@ -132,30 +342,56 @@ if [ $# -gt 0 ];then
exit 2
;;
esac
docker-compose rm -fs
$COMPOSE down
rm -rf "./docker/mysql/data"
rm -rf "./docker/log/supervisor"
find "./storage/logs" -name "*.log" | xargs rm -rf
echo -e "${OK} ${GreenBG} 卸载完成 ${Font}"
elif [[ "$1" == "dev" ]]; then
elif [[ "$1" == "reinstall" ]]; then
shift 1
check_node
$COMPOSE exec php /bin/bash -c "php bin/run --mode=dev"
./cmd uninstall $@
sleep 3
./cmd install $@
elif [[ "$1" == "port" ]]; then
shift 1
env_set APP_PORT "$1"
$COMPOSE up -d
echo -e "${OK} ${GreenBG} 修改成功 ${Font}"
echo -e "地址: http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
elif [[ "$1" == "url" ]]; then
shift 1
env_set APP_URL "$1"
supervisorctl_restart php
mix watch --hot
elif [[ "$1" == "prod" ]]; then
echo -e "${OK} ${GreenBG} 修改成功 ${Font}"
elif [[ "$1" == "env" ]]; then
shift 1
check_node
$COMPOSE exec php /bin/bash -c "php bin/run --mode=prod"
if [ -n "$1" ]; then
env_set $1 "$2"
fi
supervisorctl_restart php
rm -rf "./public/js/build"
mix --production
elif [[ "$1" == "super" ]]; then
echo -e "${OK} ${GreenBG} 修改成功 ${Font}"
elif [[ "$1" == "repassword" ]]; then
shift 1
supervisorctl_restart "$@"
run_exec mariadb "sh /etc/mysql/repassword.sh \"$@\""
elif [[ "$1" == "dev" ]] || [[ "$1" == "development" ]]; then
shift 1
run_compile dev
elif [[ "$1" == "prod" ]] || [[ "$1" == "production" ]]; then
shift 1
run_compile prod
elif [[ "$1" == "appbuild" ]] || [[ "$1" == "buildapp" ]]; then
shift 1
run_electron app
elif [[ "$1" == "electron" ]]; then
shift 1
run_electron $@
elif [[ "$1" == "doc" ]]; then
shift 1
run_exec php "php app/Http/Controllers/Api/apidoc.php"
docker run -it --rm -v ${cur_path}:/home/node/apidoc kuaifan/apidoc -i app/Http/Controllers/Api -o public/docs
elif [[ "$1" == "debug" ]]; then
shift 1
if [[ "$@" == "close" ]];then
if [[ "$@" == "close" ]]; then
env_set APP_DEBUG "false"
else
env_set APP_DEBUG "true"
@@ -163,45 +399,48 @@ if [ $# -gt 0 ];then
supervisorctl_restart php
elif [[ "$1" == "https" ]]; then
shift 1
if [[ "$@" == "auto" ]];then
env_set APP_FORCE_URL_SCHEME "auto"
if [[ "$@" == "auto" ]]; then
env_set APP_SCHEME "auto"
else
env_set APP_FORCE_URL_SCHEME "true"
env_set APP_SCHEME "true"
fi
supervisorctl_restart php
elif [[ "$1" == "artisan" ]]; then
shift 1
e="php artisan $@" && $COMPOSE exec php /bin/bash -c "$e"
e="php artisan $@" && run_exec php "$e"
elif [[ "$1" == "php" ]]; then
shift 1
e="php $@" && $COMPOSE exec php /bin/bash -c "$e"
e="php $@" && run_exec php "$e"
elif [[ "$1" == "nginx" ]]; then
shift 1
e="nginx $@" && run_exec nginx "$e"
elif [[ "$1" == "redis" ]]; then
shift 1
e="redis $@" && run_exec redis "$e"
elif [[ "$1" == "mysql" ]]; then
shift 1
if [[ "$@" == "bak" ]];then
database=$(env_get DB_DATABASE)
password=$(env_get DB_ROOT_PASSWORD)
filename="${cur_path}/docker/mysql/bak/${database}_$(date "+%Y%m%d%H%M%S").sql.gz"
$COMPOSE exec mariadb /bin/sh -c "exec mysqldump --databases $database -uroot -p\"$password\"" | gzip > $filename
judge "备份数据库"
[ -f "$filename" ] && echo -e "备份文件:$filename"
if [ "$1" = "backup" ]; then
run_mysql backup
elif [ "$1" = "recovery" ]; then
run_mysql recovery
else
e="mysql $@" && $COMPOSE exec mariadb /bin/sh -c "$e"
e="mysql $@" && run_exec mariadb "$e"
fi
elif [[ "$1" == "composer" ]]; then
shift 1
e="composer $@" && $COMPOSE exec php /bin/bash -c "$e"
elif [[ "$1" == "supervisorctl" ]]; then
e="composer $@" && run_exec php "$e"
elif [[ "$1" == "service" ]]; then
shift 1
e="supervisorctl $@" && $COMPOSE exec php /bin/bash -c "$e"
e="service $@" && run_exec php "$e"
elif [[ "$1" == "super" ]] || [[ "$1" == "supervisorctl" ]]; then
shift 1
e="supervisorctl $@" && run_exec php "$e"
elif [[ "$1" == "models" ]]; then
shift 1
run_exec php "php artisan ide-helper:models -W"
elif [[ "$1" == "test" ]]; then
shift 1
e="./vendor/bin/phpunit $@" && $COMPOSE exec php /bin/bash -c "$e"
elif [[ "$1" == "npm" ]]; then
shift 1
e="npm $@" && $COMPOSE exec php /bin/bash -c "$e"
elif [[ "$1" == "yarn" ]]; then
shift 1
e="yarn $@" && $COMPOSE exec php /bin/bash -c "$e"
e="./vendor/bin/phpunit $@" && run_exec php "$e"
elif [[ "$1" == "restart" ]]; then
shift 1
$COMPOSE stop "$@"

View File

@@ -16,7 +16,9 @@
"ext-simplexml": "*",
"fideloper/proxy": "^4.4.1",
"fruitcake/laravel-cors": "^2.0.4",
"guanguans/notify": "^1.20",
"guzzlehttp/guzzle": "^7.3.0",
"hedeqiang/umeng": "^2.1",
"laravel/framework": "^v8.48.1",
"laravel/tinker": "^v2.6.1",
"maatwebsite/excel": "^3.1.31",

9994
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -300,9 +300,9 @@ return [
'log_level' => 4,
'log_file' => storage_path(sprintf('logs/swoole-%s.log', date('Y-m'))),
'document_root' => base_path('public'),
'buffer_output_size' => 200 * 1024 * 1024,
'socket_buffer_size' => 256 * 1024 * 1024,
'package_max_length' => 200 * 1024 * 1024,
'buffer_output_size' => 512 * 1024 * 1024,
'socket_buffer_size' => 512 * 1024 * 1024,
'package_max_length' => 512 * 1024 * 1024,
'reload_async' => true,
'max_wait_time' => 60,
'enable_reuse_port' => true,

View File

@@ -17,7 +17,7 @@ class CreateProjectLogsTable extends Migration
$table->bigIncrements('id');
$table->bigInteger('project_id')->nullable()->default(0)->comment('项目ID');
$table->bigInteger('column_id')->nullable()->default(0)->comment('列表ID');
$table->bigInteger('task_id')->nullable()->default(0)->comment('项目ID');
$table->bigInteger('task_id')->nullable()->default(0)->comment('任务ID');
$table->bigInteger('userid')->nullable()->default(0)->comment('会员ID');
$table->string('detail', 500)->nullable()->default('')->comment('详细信息');
$table->timestamps();

View File

@@ -0,0 +1,47 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class FilesAddExt extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$isAdd = false;
Schema::table('files', function (Blueprint $table) use (&$isAdd) {
if (!Schema::hasColumn('files', 'ext')) {
$isAdd = true;
$table->string('ext', 20)->nullable()->default('')->after('type')->comment('后缀名');
}
});
if ($isAdd) {
// 更新数据
\App\Models\File::chunkById(100, function ($lists) {
foreach ($lists as $item) {
if (in_array($item->type, ['word', 'excel', 'ppt'])) {
$item->ext = str_replace(['word', 'excel', 'ppt'], ['docx', 'xlsx', 'pptx'], $item->type);
$item->save();
}
}
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('files', function (Blueprint $table) {
$table->dropColumn("ext");
});
}
}

View File

@@ -0,0 +1,50 @@
<?php
use App\Models\Project;
use App\Models\ProjectTask;
use Carbon\Carbon;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ProjectTasksAddArchivedFollow extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$isAdd = false;
Schema::table('project_tasks', function (Blueprint $table) use (&$isAdd) {
if (!Schema::hasColumn('project_tasks', 'archived_follow')) {
$isAdd = true;
$table->tinyInteger('archived_follow')->nullable()->default(0)->after('archived_userid')->comment('跟随项目归档(项目取消归档时任务也取消归档)');
}
});
if ($isAdd) {
// 更新数据
Project::whereNotNull('archived_at')->chunkById(100, function ($lists) {
foreach ($lists as $item) {
ProjectTask::whereProjectId($item->id)->whereArchivedAt(null)->update([
'archived_at' => $item->archived_at,
'archived_follow' => 1
]);
}
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('project_tasks', function (Blueprint $table) {
$table->dropColumn("archived_follow");
});
}
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateProjectInvitesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('project_invites', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('project_id')->nullable()->default(0)->comment('项目ID');
$table->integer('num')->nullable()->default(0)->comment('累计邀请');
$table->string('code')->nullable()->default('')->comment('链接码');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('project_invites');
}
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateFileLinksTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('file_links', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('file_id')->nullable()->default(0)->comment('文件ID');
$table->integer('num')->nullable()->default(0)->comment('累计访问');
$table->string('code')->nullable()->default('')->comment('链接码');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('file_links');
}
}

View File

@@ -0,0 +1,55 @@
<?php
use App\Models\File;
use App\Models\FileUser;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class FileUsersAddPermission extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$isAdd = false;
Schema::table('file_users', function (Blueprint $table) use (&$isAdd) {
if (!Schema::hasColumn('file_users', 'permission')) {
$isAdd = true;
$table->tinyInteger('permission')->nullable()->default(0)->after('userid')->comment('权限0只读1读写');
}
});
if ($isAdd) {
// 更新数据
File::whereShare(1)->chunkById(100, function ($lists) {
foreach ($lists as $file) {
FileUser::updateInsert([
'file_id' => $file->id,
'userid' => 0,
]);
}
});
File::whereShare(2)->update([
'share' => 1,
]);
FileUser::wherePermission(0)->update([
'permission' => 1,
]);
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('file_users', function (Blueprint $table) {
$table->dropColumn("permission");
});
}
}

View File

@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateReportsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if ( Schema::hasTable('reports') )
return;
Schema::create('reports', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->string("title")->default("")->comment("标题");
$table->enum("type", ["weekly", "daily"])->default("daily")->comment("汇报类型");
$table->unsignedBigInteger("userid")->default(0);
$table->longText("content")->nullable();
$table->index(["userid", "created_at"], "default");
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('reports');
}
}

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateReportReceivesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if ( Schema::hasTable('report_receives') )
return;
Schema::create('report_receives', function (Blueprint $table) {
$table->bigIncrements("id");
$table->unsignedInteger("rid")->default(0);
$table->timestamp("receive_time")->nullable()->comment("接收时间");
$table->unsignedBigInteger("userid")->default(0)->comment("接收人");
$table->unsignedTinyInteger("read")->default(0)->comment("是否已读");
$table->index(["userid", "receive_time"], "default");
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('report_receives');
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddReportSign extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('reports', function (Blueprint $table) {
$table->string("sign")->default("")->comment("汇报唯一标识");
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('reports', function (Blueprint $table) {
$table->dropColumn("sign");
});
}
}

View File

@@ -0,0 +1,43 @@
<?php
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class UsersAddDisableAt extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$isAdd = false;
Schema::table('users', function (Blueprint $table) use (&$isAdd) {
if (!Schema::hasColumn('users', 'disable_at')) {
$isAdd = true;
$table->timestamp('disable_at')->nullable()->after('created_ip')->comment('禁用时间');
}
});
if ($isAdd) {
User::where("identity", "like", "%,disable,%")->update([
'disable_at' => Carbon::now(),
]);
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn("disable_at");
});
}
}

View File

@@ -0,0 +1,39 @@
<?php
use App\Models\ProjectTask;
use Illuminate\Database\Migrations\Migration;
class ProjectTasksUpdateSubtaskTime extends Migration
{
/**
* 子任务同步主任务(任务时间)
*
* @return void
*/
public function up()
{
ProjectTask::where('parent_id', '>', 0)
->whereNull('end_at')
->chunkById(100, function ($lists) {
/** @var ProjectTask $task */
foreach ($lists as $task) {
$parent = ProjectTask::whereNotNull('end_at')->find($task->parent_id);
if ($parent) {
$task->start_at = $parent->start_at;
$task->end_at = $parent->end_at;
$task->save();
}
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@@ -0,0 +1,51 @@
<?php
use App\Models\ProjectTask;
use Illuminate\Database\Migrations\Migration;
class ProjectTasksUpdateSubtaskArchivedDelete extends Migration
{
/**
* 子任务同步主任务(归档、删除)
*
* @return void
*/
public function up()
{
// 归档
ProjectTask::whereParentId(0)
->whereNotNull('archived_at')
->chunkById(100, function ($lists) {
/** @var ProjectTask $task */
foreach ($lists as $task) {
ProjectTask::whereParentId($task->id)->update([
'archived_at' => $task->archived_at,
'archived_userid' => $task->archived_userid,
'archived_follow' => $task->archived_follow,
]);
}
});
// 删除
ProjectTask::onlyTrashed()
->whereParentId(0)
->chunkById(100, function ($lists) {
/** @var ProjectTask $task */
foreach ($lists as $task) {
ProjectTask::whereParentId($task->id)->update([
'deleted_at' => $task->deleted_at,
]);
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateProjectFlowItemsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('project_flow_items', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('project_id')->nullable()->default(0)->comment('项目ID');
$table->bigInteger('flow_id')->nullable()->default(0)->comment('流程ID');
$table->string('name', 50)->nullable()->default('')->comment('名称');
$table->string('status', 20)->nullable()->default('')->comment('状态');
$table->string('turns')->nullable()->default('')->comment('可流转');
$table->string('userids')->nullable()->default('')->comment('状态负责人ID');
$table->integer('sort')->nullable()->comment('排序');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('project_flow_items');
}
}

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateProjectFlowsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('project_flows', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('project_id')->nullable()->default(0)->comment('项目ID');
$table->string('name', 50)->nullable()->default('')->comment('流程名称');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('project_flows');
}
}

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ProjectTasksAddFlowItemIdFlowItemName extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('project_tasks', function (Blueprint $table) {
if (!Schema::hasColumn('project_tasks', 'flow_item_id')) {
$table->bigInteger('flow_item_id')->nullable()->default(0)->after('dialog_id')->comment('工作流状态ID');
$table->string('flow_item_name', 50)->nullable()->default('')->after('flow_item_id')->comment('工作流状态名称');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('project_tasks', function (Blueprint $table) {
$table->dropColumn("flow_item_id");
$table->dropColumn("flow_item_name");
});
}
}

View File

@@ -0,0 +1,42 @@
<?php
use App\Models\ProjectFlowItem;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ProjectFlowItemsAddUsertype extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$isAdd = false;
Schema::table('project_flow_items', function (Blueprint $table) use (&$isAdd) {
if (!Schema::hasColumn('project_flow_items', 'usertype')) {
$isAdd = true;
$table->string('usertype', 10)->nullable()->default('')->after('userids')->comment('流转模式');
}
});
if ($isAdd) {
ProjectFlowItem::where("usertype", "")->update([
'usertype' => 'add',
]);
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('project_flow_items', function (Blueprint $table) {
$table->dropColumn("usertype");
});
}
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ProjectLogsAddRecord extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('project_logs', function (Blueprint $table) {
if (!Schema::hasColumn('project_logs', 'record')) {
$table->text('record')->nullable()->after('detail')->comment('记录数据');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('project_logs', function (Blueprint $table) {
$table->dropColumn("record");
});
}
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ProjectFlowItemsAddUserlimit extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('project_flow_items', function (Blueprint $table) {
if (!Schema::hasColumn('project_flow_items', 'userlimit')) {
$table->tinyInteger('userlimit')->nullable()->default(0)->after('usertype')->comment('限制负责人');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('project_flow_items', function (Blueprint $table) {
$table->dropColumn("userlimit");
});
}
}

View File

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

View File

@@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class InsertSettingColumnTemplate extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$array = \App\Module\Base::setting('columnTemplate');
if (empty($array)) {
\App\Module\Base::setting('columnTemplate', [
[
'name' => '软件开发',
'columns' => ['产品规划', '前端开发', '后端开发', '测试', '发布', '其他'],
],
[
'name' => '产品开发',
'columns' => ['产品计划', '正在设计', '正在研发', '测试', '准备发布', '发布成功'],
],
]);
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
}
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class WebSocketDialogUsersAddTopAt extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('web_socket_dialog_users', function (Blueprint $table) {
if (!Schema::hasColumn('web_socket_dialog_users', 'top_at')) {
$table->timestamp('top_at')->nullable()->after('userid')->comment('置顶时间');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('web_socket_dialog_users', function (Blueprint $table) {
$table->dropColumn("top_at");
});
}
}

View File

@@ -0,0 +1,30 @@
<?php
use App\Models\File;
use Illuminate\Database\Migrations\Migration;
class FilesUpdateType extends Migration
{
/**
* 更改流程图文件类型
* @return void
*/
public function up()
{
File::whereType('flow')->update([
'type' => 'drawio'
]);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
File::whereType('drawio')->update([
'type' => 'flow'
]);
}
}

View File

@@ -0,0 +1,75 @@
<?php
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Models\File;
use App\Models\FileContent;
use App\Module\Base;
use Carbon\Carbon;
use Illuminate\Database\Migrations\Migration;
class FilesUpdateExt extends Migration
{
/**
* 更新后缀
* @return void
*/
public function up()
{
File::whereIn('type', ['mind', 'drawio', 'document'])->where('ext', '')->orderBy('id')->chunk(100, function($files) {
/** @var File $file */
foreach ($files as $file) {
$fileContent = FileContent::whereFid($file->id)->orderByDesc('id')->first();
$contentArray = Base::json2array($fileContent?->content);
$contentString = '';
//
switch ($file->type) {
case 'document':
$file->ext = $contentArray['type'] ?: 'md';
$contentString = $contentArray['content'];
break;
case 'drawio':
$file->ext = 'drawio';
$contentString = $contentArray['xml'];
break;
case 'mind':
$file->ext = 'mind';
$contentString = $fileContent?->content;
break;
}
$file->save();
//
$path = 'uploads/file/' . $file->type . '/' . date("Ym", Carbon::parse($file->created_at)->timestamp) . '/' . $file->id . '/' . md5($contentString);
$save = public_path($path);
Base::makeDir(dirname($save));
file_put_contents($save, $contentString);
$content = [
'type' => $file->ext,
'url' => $path
];
//
$content = FileContent::createInstance([
'fid' => $file->id,
'content' => $content,
'text' => $fileContent?->text,
'size' => $file->size,
'userid' => $file->userid,
]);
$content->save();
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
File::whereIn('ext', ['mind', 'drawio', 'md'])->update([
'ext' => ''
]);
// ... 退回去意义不大,文件内容不做回滚操作
}
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ProjectUsersAddTopAt extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('project_users', function (Blueprint $table) {
if (!Schema::hasColumn('project_users', 'top_at')) {
$table->timestamp('top_at')->nullable()->after('owner')->comment('置顶时间');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('project_users', function (Blueprint $table) {
$table->dropColumn("top_at");
});
}
}

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateProjectTaskFlowChangesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('project_task_flow_changes', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('task_id')->nullable()->default(0)->comment('任务ID');
$table->bigInteger('userid')->nullable()->default(0)->comment('会员ID');
$table->bigInteger('before_item_id')->nullable()->default(0)->comment('变化前工作流状态ID');
$table->string('before_item_name', 50)->nullable()->default('')->comment('(变化前)工作流状态名称');
$table->bigInteger('after_item_id')->nullable()->default(0)->comment('变化后工作流状态ID');
$table->string('after_item_name', 50)->nullable()->default('')->comment('(变化后)工作流状态名称');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('project_task_flow_changes');
}
}

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class RenamePreProjectTaskFlowChangesItem extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('project_task_flow_changes', function (Blueprint $table) {
if (Schema::hasColumn('project_task_flow_changes', 'before_item_id')) {
$table->renameColumn('before_item_id', 'before_flow_item_id');
$table->renameColumn('before_item_name', 'before_flow_item_name');
$table->renameColumn('after_item_id', 'after_flow_item_id');
$table->renameColumn('after_item_name', 'after_flow_item_name');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('project_task_flow_changes', function (Blueprint $table) {
//
});
}
}

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