Compare commits

...

211 Commits

Author SHA1 Message Date
kuaifan
958ef80602 build 2025-06-29 22:12:03 +08:00
kuaifan
124b63f325 perf: 优化客户端缓存 2025-06-29 21:58:43 +08:00
kuaifan
40f5ba5004 fix: 修复客户端无法打开部分应用的问题 2025-06-29 21:57:46 +08:00
kuaifan
32f30826b9 perf: 优化已知问题 2025-06-18 20:25:56 +08:00
kuaifan
b4aa8b37ea perf: 优化iPadOS兼容性 2025-06-18 15:35:37 +08:00
kuaifan
8368bbec47 perf: 优化iPadOS兼容性 2025-06-18 15:35:37 +08:00
kuaifan
618e482507 perf: 优化iPadOS兼容性 2025-06-18 15:35:30 +08:00
kuaifan
43711a1a59 perf: 优化iPadOS兼容性 2025-06-18 15:35:23 +08:00
kuaifan
bbe071545d perf: 优化设备登录 2025-06-17 00:17:04 +08:00
kuaifan
4710479b46 add tmp log 2025-06-16 21:08:42 +08:00
kuaifan
c28a375b5d build 2025-06-05 07:28:49 +08:00
kuaifan
750d3429e0 feat: 微应用支持iframe模式 2025-06-05 07:15:34 +08:00
kuaifan
4c34fe9b85 fix: 修复应用商店参数失效问题 2025-06-04 16:27:40 +08:00
kuaifan
34c56980d4 no message 2025-06-04 15:29:04 +08:00
kuaifan
a5b8609df1 perf: 优化导出签到功能 2025-06-04 15:14:31 +08:00
kuaifan
1fd7f0314a perf: 优化导出审批功能 2025-06-04 15:08:31 +08:00
kuaifan
25e82d690e perf: 优化导出任务功能 2025-06-04 14:58:54 +08:00
kuaifan
31879cb60b build 2025-06-04 07:22:43 +08:00
kuaifan
489e5b551c perf: 优化本地资源加载方式 2025-06-04 07:02:19 +08:00
kuaifan
e29bd01f68 no message 2025-06-03 21:38:53 +08:00
kuaifan
e5d9140aa0 perf: 优化微应用参数变量的支持 2025-06-03 21:38:22 +08:00
kuaifan
09f5cca948 fix: 修复已经在消息中打开项目对话时无法在其他地方打开项目沟通 2025-06-03 20:10:22 +08:00
kuaifan
405e09fdf4 fix: 修复搜索标签后搜索框消失的情况 2025-06-03 14:48:04 +08:00
kuaifan
43624e9b7b fix: 修复部分标签背景色不显示的情况 2025-06-03 14:47:42 +08:00
kuaifan
29ed0ad05a build 2025-06-02 19:34:59 +08:00
kuaifan
0aafe79c65 no message 2025-06-02 18:36:33 +08:00
kuaifan
802afd592c fix: 修复客户端无法打开工作报告 2025-06-02 18:25:19 +08:00
kuaifan
ac1644e32d fix: 修复部分机子无法打开OKR的情况 2025-06-02 18:14:12 +08:00
kuaifan
5a1f130bec fix: 修复客户端无法打开工作报告 2025-06-02 18:13:51 +08:00
kuaifan
678868153a fix: 修复客户端无法打开工作报告 2025-06-02 10:06:22 +08:00
kuaifan
991f050dbb build 2025-05-31 22:53:46 +08:00
kuaifan
be04355685 no message 2025-05-31 10:30:47 +08:00
kuaifan
762b6b3f3b no message 2025-05-30 19:11:19 +08:00
kuaifan
71d9cbdce6 no message 2025-05-30 15:10:13 +08:00
kuaifan
d995ef19b5 no message 2025-05-30 09:25:28 +08:00
Pang
bf80e4b02b feat: 桌面端使用web服务启动 2025-05-30 07:33:14 +08:00
Pang
64a047cd7c feat: 桌面端使用web服务启动 2025-05-30 07:13:19 +08:00
kuaifan
566421003c build 2025-05-29 08:52:24 +08:00
kuaifan
198da7608d no message 2025-05-29 08:49:37 +08:00
kuaifan
0a7edb219e no message 2025-05-29 02:02:21 +08:00
kuaifan
65c20f2211 fix: 修复移动端审批列表无法滚动到底部的情况 2025-05-29 01:51:24 +08:00
kuaifan
144767152a no message 2025-05-29 01:44:06 +08:00
kuaifan
82be52be52 no message 2025-05-29 01:35:28 +08:00
kuaifan
46c8caa627 no message 2025-05-29 01:35:28 +08:00
kuaifan
0027e838a0 no message 2025-05-29 01:35:28 +08:00
kuaifan
7e846e2a58 no message 2025-05-29 01:35:28 +08:00
kuaifan
de7f55bb97 no message 2025-05-29 01:35:28 +08:00
kuaifan
2ac9a59469 no message 2025-05-29 01:35:28 +08:00
kuaifan
bdb4014b94 no message 2025-05-29 01:35:28 +08:00
weifashi
9509bd1510 fix: 修复重复周期 子任务没有复制过去 2025-05-28 22:11:56 +08:00
kuaifan
3f11770baa no message 2025-05-28 15:23:59 +08:00
kuaifan
259a040b7e no message 2025-05-28 11:42:42 +08:00
kuaifan
ccb14630f7 no message 2025-05-28 10:13:05 +08:00
kuaifan
4aa865a60f no message 2025-05-28 07:23:20 +08:00
kuaifan
c7ea7b057c build 2025-05-28 07:05:27 +08:00
kuaifan
91dbbd46c0 no message 2025-05-27 16:34:02 +08:00
kuaifan
e5146077eb no message 2025-05-27 09:18:34 +08:00
kuaifan
76918bf973 no message 2025-05-27 07:32:48 +08:00
kuaifan
b7d3e69f87 no message 2025-05-26 23:44:16 +08:00
kuaifan
d7bccfd267 no message 2025-05-26 23:15:26 +08:00
kuaifan
2a25917e41 no message 2025-05-26 23:04:00 +08:00
kuaifan
d73239b274 build 2025-05-26 22:45:59 +08:00
kuaifan
9f6fffbe6b feat: 新增应用商店 2025-05-26 21:19:51 +08:00
kuaifan
72f7ff3df5 no message 2025-05-26 21:02:06 +08:00
kuaifan
2af1dba8dc no message 2025-05-26 20:54:06 +08:00
kuaifan
ca353d747b no message 2025-05-26 20:26:40 +08:00
kuaifan
1589d4df1c no message 2025-05-26 20:12:51 +08:00
kuaifan
b3abe8af9c no message 2025-05-26 17:45:16 +08:00
kuaifan
c178e36f9b no message 2025-05-26 15:55:42 +08:00
kuaifan
d93092de99 no message 2025-05-26 15:45:37 +08:00
kuaifan
790b05880a no message 2025-05-25 18:26:32 +08:00
kuaifan
f34766ade0 no message 2025-05-25 08:46:25 +08:00
kuaifan
0e1d5e802c no message 2025-05-25 08:09:57 +08:00
kuaifan
db526dfcc8 no message 2025-05-25 00:09:14 +08:00
kuaifan
c18db60e80 no message 2025-05-24 23:48:21 +08:00
kuaifan
b579a6ade2 no message 2025-05-24 21:04:13 +08:00
kuaifan
9d1d642734 no message 2025-05-24 20:28:36 +08:00
kuaifan
261c051052 no message 2025-05-24 19:40:36 +08:00
kuaifan
e499e2d0dc no message 2025-05-24 19:21:26 +08:00
kuaifan
b860b6f389 no message 2025-05-24 19:09:40 +08:00
kuaifan
05d5d5a967 no message 2025-05-24 19:06:14 +08:00
kuaifan
74ba1cc723 no message 2025-05-24 18:26:48 +08:00
kuaifan
f2042efdc2 no message 2025-05-24 18:10:34 +08:00
kuaifan
6b7e7fa1e4 no message 2025-05-24 17:32:40 +08:00
kuaifan
6677e6e74f no message 2025-05-24 16:53:48 +08:00
kuaifan
c3994ddbea no message 2025-05-24 16:05:25 +08:00
kuaifan
a981ff2f6c no message 2025-05-24 15:57:31 +08:00
kuaifan
3e0ba398d4 no message 2025-05-24 15:42:37 +08:00
kuaifan
aa4f7c8536 no message 2025-05-24 14:39:54 +08:00
kuaifan
959f9454d8 no message 2025-05-24 13:30:01 +08:00
kuaifan
6b72a309f5 no message 2025-05-24 07:49:15 +08:00
kuaifan
c388fe373d no message 2025-05-24 07:37:08 +08:00
kuaifan
270ddc6487 feat: 检查应用是否已安装 2025-05-23 13:39:57 +08:00
kuaifan
5ccaa8f106 perf: 更新AI默认模型列表 2025-05-23 13:03:52 +08:00
王昱
1d92c2668d feat: 检查应用是否已安装 2025-05-23 12:40:33 +08:00
kuaifan
6e03a05e6d no message 2025-05-23 11:30:31 +08:00
kuaifan
2905059947 no message 2025-05-23 11:17:05 +08:00
kuaifan
1df927f771 no message 2025-05-22 23:01:07 +08:00
kuaifan
bd8b6d0319 no message 2025-05-21 16:23:11 +08:00
kuaifan
fef39b2720 no message 2025-05-20 20:07:16 +08:00
kuaifan
7de433c5fc no message 2025-05-20 17:53:49 +08:00
kuaifan
0a711f2656 no message 2025-05-18 17:06:11 +08:00
kuaifan
04b6b8aa8a no message 2025-05-18 13:25:13 +08:00
kuaifan
58f286efe4 no message 2025-05-18 11:30:42 +08:00
kuaifan
a01b292eb3 no message 2025-05-18 11:00:06 +08:00
kuaifan
18c608ad7e no message 2025-05-18 00:20:23 +08:00
kuaifan
8d144f4e12 no message 2025-05-17 23:54:24 +08:00
kuaifan
7f895bfbec no message 2025-05-17 23:25:09 +08:00
kuaifan
b0c356fa9b no message 2025-05-17 23:12:47 +08:00
kuaifan
79defdc3f3 no message 2025-05-17 22:46:10 +08:00
kuaifan
b2d9568deb no message 2025-05-17 20:56:54 +08:00
kuaifan
a130c049bf no message 2025-05-17 13:53:07 +08:00
kuaifan
8904039515 no message 2025-05-17 09:52:52 +08:00
kuaifan
7d28181b16 no message 2025-05-17 09:09:59 +08:00
kuaifan
98e4c81b9b no message 2025-05-17 07:09:47 +08:00
kuaifan
10f5af5f09 no message 2025-05-17 02:03:49 +08:00
kuaifan
18ffad5de5 no message 2025-05-16 20:24:43 +08:00
kuaifan
428db42140 no message 2025-05-16 08:23:58 +08:00
kuaifan
6e5426764e no message 2025-05-15 23:16:10 +08:00
kuaifan
f3cfcc650c no message 2025-05-15 23:04:09 +08:00
kuaifan
fc88573c9d no message 2025-05-15 22:53:48 +08:00
kuaifan
5cf1c3d14f no message 2025-05-15 22:44:41 +08:00
kuaifan
79f15cc34d no message 2025-05-15 21:37:18 +08:00
kuaifan
775cab1080 no message 2025-05-15 21:26:14 +08:00
kuaifan
3e20e7d0ce no message 2025-05-15 21:02:42 +08:00
kuaifan
54407e0a60 no message 2025-05-15 20:00:17 +08:00
kuaifan
ef696391d8 no message 2025-05-15 19:42:16 +08:00
kuaifan
0c34df290e no message 2025-05-15 17:43:04 +08:00
kuaifan
04d31bd814 no message 2025-05-15 16:56:08 +08:00
kuaifan
9888d9f59e no message 2025-05-15 16:44:58 +08:00
kuaifan
3bb1bf0967 no message 2025-05-15 16:01:45 +08:00
kuaifan
dfbcb1f45c no message 2025-05-15 15:54:58 +08:00
kuaifan
12ecf4de40 no message 2025-05-15 15:39:03 +08:00
kuaifan
7be1171004 no message 2025-05-15 15:32:52 +08:00
kuaifan
2bb646d150 no message 2025-05-15 15:03:49 +08:00
kuaifan
e7749b2dff no message 2025-05-15 09:22:18 +08:00
kuaifan
434d8eabc8 no message 2025-05-15 08:09:56 +08:00
kuaifan
0a14219112 no message 2025-05-15 00:39:25 +08:00
kuaifan
5b811df8ee no message 2025-05-15 00:20:41 +08:00
kuaifan
bc264109f3 no message 2025-05-14 23:55:00 +08:00
kuaifan
9c29c1ca9b no message 2025-05-13 12:55:08 +08:00
kuaifan
fe4f62ff8d no message 2025-05-13 09:51:21 +08:00
kuaifan
35dfb9d1ff no message 2025-05-13 01:25:24 +08:00
kuaifan
3809aca09d no message 2025-05-12 14:15:42 +08:00
kuaifan
bcd5bb5009 no message 2025-05-12 14:08:53 +08:00
kuaifan
0d7cc6a386 no message 2025-05-12 13:47:05 +08:00
kuaifan
12265699b3 no message 2025-05-12 13:32:20 +08:00
kuaifan
783c0356c7 no message 2025-05-12 13:13:23 +08:00
kuaifan
7ef31bc0b5 no message 2025-05-12 12:14:30 +08:00
kuaifan
467f2368dd no message 2025-05-12 09:49:31 +08:00
kuaifan
2cfcb081a2 no message 2025-05-12 08:33:51 +08:00
kuaifan
1829ac851d no message 2025-05-11 10:07:38 +08:00
kuaifan
2e715004ae no message 2025-05-11 00:39:15 +08:00
kuaifan
45ee092593 no message 2025-05-10 21:31:48 +08:00
kuaifan
20e543f721 no message 2025-05-10 12:32:07 +08:00
kuaifan
b2148eb656 no message 2025-05-09 18:13:45 +08:00
kuaifan
efab4fb41b no message 2025-05-09 10:43:36 +08:00
kuaifan
9ac3fd3615 no message 2025-05-09 10:16:59 +08:00
kuaifan
14bc7a0f76 no message 2025-05-09 09:53:46 +08:00
kuaifan
26727fea17 no message 2025-05-09 07:33:24 +08:00
kuaifan
34f8d4c2a6 no message 2025-05-09 00:54:10 +08:00
kuaifan
02708807bd no message 2025-05-09 00:39:21 +08:00
kuaifan
176e5de531 no message 2025-05-08 23:38:02 +08:00
kuaifan
b9180a4426 no message 2025-05-08 23:02:31 +08:00
kuaifan
959b30c788 no message 2025-05-08 20:36:26 +08:00
kuaifan
df79ef59ea no message 2025-05-08 20:05:46 +08:00
kuaifan
4dc1f01cf0 no message 2025-05-08 20:01:16 +08:00
kuaifan
cb17110562 no message 2025-05-08 19:46:10 +08:00
kuaifan
4424e4f9be no message 2025-05-08 16:46:39 +08:00
kuaifan
fb641ac960 no message 2025-05-08 15:53:30 +08:00
kuaifan
a5faa378b0 no message 2025-05-08 13:07:59 +08:00
kuaifan
66ea277a59 no message 2025-05-08 10:34:47 +08:00
kuaifan
006bc6ceda no message 2025-05-08 07:11:58 +08:00
kuaifan
aef3e869dc no message 2025-05-07 20:28:52 +08:00
kuaifan
9c46d28871 no message 2025-05-07 17:53:21 +08:00
kuaifan
1fc141050f no message 2025-05-07 13:53:12 +08:00
kuaifan
1e45d199e2 no message 2025-05-07 13:32:50 +08:00
kuaifan
3018f3653c no message 2025-05-07 12:21:09 +08:00
kuaifan
1c5b856800 no message 2025-05-07 09:41:25 +08:00
kuaifan
f53a5ea6c1 no message 2025-05-07 08:27:22 +08:00
kuaifan
a608734be9 no message 2025-05-06 23:33:00 +08:00
kuaifan
e52523f903 no message 2025-05-06 22:41:43 +08:00
kuaifan
82778014b8 no message 2025-05-06 22:12:38 +08:00
kuaifan
cb4b9a361f no message 2025-05-06 16:13:37 +08:00
kuaifan
3a337940d1 no message 2025-05-06 16:13:09 +08:00
kuaifan
9abdafb905 no message 2025-05-06 09:55:42 +08:00
kuaifan
cd494b52a4 no message 2025-05-06 09:19:21 +08:00
kuaifan
5581d1431b no message 2025-05-06 04:36:11 +08:00
kuaifan
6539b14ecf no message 2025-05-06 03:37:28 +08:00
kuaifan
45b30e4a33 no message 2025-05-05 12:08:20 +08:00
kuaifan
ff48d543e7 no message 2025-05-05 11:19:47 +08:00
kuaifan
6562b74130 no message 2025-05-05 09:38:23 +08:00
kuaifan
23a68370b4 no message 2025-05-05 08:25:21 +08:00
kuaifan
cc9f346d49 no message 2025-05-05 08:04:09 +08:00
kuaifan
8c18865138 no message 2025-05-05 06:31:01 +08:00
kuaifan
db3a17a2c8 no message 2025-05-05 00:17:45 +08:00
kuaifan
5a0d1ac0c0 no message 2025-05-04 22:45:07 +08:00
kuaifan
5414accc6c no message 2025-05-04 12:57:04 +08:00
kuaifan
c415ace453 no message 2025-05-01 12:30:20 +08:00
kuaifan
bf34beec20 no message 2025-05-01 11:55:15 +08:00
kuaifan
f4d459af7f fix: 修复录音文件转文字后无法切换翻译的问题 2025-05-01 11:49:22 +08:00
kuaifan
508cc2bd91 no message 2025-04-24 22:40:33 +08:00
kuaifan
35b7e3a289 no message 2025-04-24 21:22:04 +08:00
kuaifan
fc907d23a7 no message 2025-04-24 20:40:52 +08:00
kuaifan
45e663fcf8 no message 2025-04-24 20:15:10 +08:00
kuaifan
b00c6a9268 no message 2025-04-24 09:16:18 +08:00
kuaifan
ad7b0cd834 no message 2025-04-24 09:11:41 +08:00
kuaifan
eccb3e2825 no message 2025-04-24 08:57:43 +08:00
kuaifan
f5a343f358 no message 2025-04-24 07:00:09 +08:00
kuaifan
f8b65a5546 no message 2025-04-23 22:46:52 +08:00
kuaifan
0f0b9c5551 no message 2025-04-23 14:50:28 +08:00
740 changed files with 6370 additions and 52875 deletions

View File

@@ -2,6 +2,81 @@
All notable changes to this project will be documented in this file.
## [1.0.61]
### Bug Fixes
- 修复客户端无法打开部分应用的问题
### Performance
- 优化客户端缓存
- 优化已知问题
- 优化iPadOS兼容性
- 优化设备登录
## [1.0.51]
### Bug Fixes
- 修复应用商店参数失效问题
### Features
- 微应用支持iframe模式
### Performance
- 优化导出签到功能
- 优化导出审批功能
- 优化导出任务功能
## [1.0.45]
### Bug Fixes
- 修复已经在消息中打开项目对话时无法在其他地方打开项目沟通
- 修复搜索标签后搜索框消失的情况
- 修复部分标签背景色不显示的情况
### Performance
- 优化本地资源加载方式
- 优化微应用参数变量的支持
## [1.0.37]
### Bug Fixes
- 修复客户端无法打开工作报告
- 修复部分机子无法打开OKR的情况
## [1.0.31]
### Bug Fixes
- 修复移动端审批列表无法滚动到底部的情况
- 修复重复周期 子任务没有复制过去
### Features
- 桌面端使用web服务启动
## [1.0.0]
### Bug Fixes
- 修复录音文件转文字后无法切换翻译的问题
### Features
- 新增应用商店
- 检查应用是否已安装
### Performance
- 更新AI默认模型列表
## [0.47.7]
### Bug Fixes

139
README.md
View File

@@ -1,148 +1,147 @@
# Install (Docker)
# DooTask - Open Source Task Management System
English | **[中文文档](./README_CN.md)**
- [Screenshot preview](./README_PREVIEW.md)
- [Demo site](http://www.dootask.com/)
- [Screenshot Preview](./README_PREVIEW.md)
- [Demo Site](http://www.dootask.com/)
**QQ Group**
Group No.: `546574618`
- Group Number: `546574618`
## Setup
## 📍 Migration from 0.x to 1.x
- `Docker v20.10+` & `Docker Compose v2.0+` must be installed
- System: `Centos/Debian/Ubuntu/macOS/Windows`
- Hardware suggestion: 2 cores and above 4G memory
- Special note: Windows users please use `git bash` or `cmder` to run the command
- Please ensure to back up your data before upgrading!
- If the upgrade fails, try running `./cmd update` multiple times.
- If you encounter "Container xxx not found" during upgrade, run `./cmd reup` and then execute `./cmd update`.
- If you see a 502 error after upgrading, run `./cmd reup` to restart the services.
- If you encounter "Application 'xxx' not installed" after upgrading, log in with the admin account and install the relevant applications from the App Store.
### Deployment (Pro Edition)
## Installation Requirements
- Required: `Docker v20.10+` and `Docker Compose v2.0+`
- Supported Systems: `CentOS/Debian/Ubuntu/macOS` and other Linux/Unix systems
- Hardware Recommendation: 2+ cores, 4GB+ memory
- Special Note: Windows users can install Linux environment using WSL2 before installing DooTask.
### Deploy Project
```bash
# 1、Clone the repository
# 1、Clone the project to your local machine or server
# Clone projects on github
git clone -b pro --depth=1 https://github.com/kuaifan/dootask.git
# Or you can use gitee
git clone -b pro --depth=1 https://gitee.com/aipaw/dootask.git
# Clone project from 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
cd dootask
# 3、InstallationCustom port installation, as: ./cmd install --port 80
# 3、One-click installation (Custom port installation: ./cmd install --port 80)
./cmd install
```
### Reset password
### Reset Password
```bash
# Reset default account password
# Reset default administrator password
./cmd repassword
```
### Change port
### Change Port
```bash
# This method only replaces the HTTP port. To replace the HTTPS port, please read the SSL configuration below
# This method only changes HTTP port. For HTTPS port, please read SSL configuration below
./cmd port 80
```
### Stop server
### Stop Service
```bash
./cmd stop
# P.S: Once application is set up, whenever you want to start the server (if it is stopped) run below command
./cmd start
./cmd down
```
### Development compilation
- `NodeJs 20+` must be installed
### Start Service
```bash
# Development
./cmd up
```
### Development & Build
Please ensure you have installed `NodeJs 20+`
```bash
# Development mode
./cmd dev
# Production (This is web client. For App/PC/Mac clients, Please read README-CLIENT.md)
# Build project (This is for web client. For desktop apps, refer to ".github/workflows/publish.yml")
./cmd prod
```
### Shortcuts for running command
### SSL Configuration
```bash
# You can do this using the following command
./cmd artisan "your command" # To run a artisan command
./cmd php "your command" # To run a php command
./cmd nginx "your command" # To run a nginx command
./cmd redis "your command" # To run a redis command
./cmd composer "your command" # To run a composer command
./cmd supervisorctl "your command" # To run a supervisorctl command
./cmd mysql "your command" # To run a mysql command (backup: Backup database, recovery: Restore database, open: Open database external port access, close: Close database external port access)
```
### SSL configuration
#### Method 1: Automatic configuration
#### Method 1: Automatic Configuration
```bash
# Running commands in a project
# Run command and follow the prompts
./cmd https
```
#### Or Method 2: Nginx Agent Configuration
#### Method 2: Nginx Proxy Configuration
```bash
# 1、Nginx config add
# 1、Add Nginx proxy configuration
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 (If you unconfigure the NGINX agent, run: ./cmd https close)
# 2、Run command (To cancel Nginx proxy configuration: ./cmd https close)
./cmd https agent
```
## Upgrade
## Upgrade & Update
**Note: Please back up your data before upgrading!**
**Note: Please backup your data before upgrading!**
```bash
# 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
```
* Please try again if the upgrade fails across a large version.
* If 502 after the upgrade please run `./cmd restart` restart the service.
* Please retry if upgrade fails across major versions.
* If you encounter 502 errors after upgrade, run `./cmd reup` to restart services.
## Transfer
## Project Migration
Follow these steps to complete the project migration after the new project is installed:
After installing the new project, follow these steps to complete migration:
1. Backup original database
1Backup original database
```bash
# Run command under old project
# Run command in the old project
./cmd mysql backup
```
2. Copy `database backup file` and `public/uploads` directory to the new project.
2Copy the following files and directories from old project to the same paths in new project
3. Restore database to new project
- `Database backup file`
- `docker/appstore`
- `public/uploads`
3、Restore database to new project
```bash
# Run command under new project
# Run command in the new project
./cmd mysql recovery
```
## Uninstall
## Uninstall Project
```bash
# Running commands in a project
./cmd uninstall
```
### More Commands
```bash
./cmd help
```

View File

@@ -1,4 +1,4 @@
# Install (Docker)
# DooTask - 开源任务管理系统
**[English](./README.md)** | 中文文档
@@ -9,22 +9,30 @@
- QQ群号: `546574618`
## 📍 0.x 迁移到 1.x
- 升级时请务必备份好数据!
- 如果升级失败请尝试执行 `./cmd update` 重试几次。
- 如果升级中出现 `没有找到 xxx 容器` 的提示,请运行 `./cmd reup` 后再执行 `./cmd update`
- 如果升级后出现502错误请运行 `./cmd reup` 重启服务即可。
- 如果升级后出现 `应用「xxx」未安装` 的提示,请使用管理员账号进入应用商店安装相关应用。
## 安装程序
- 必须安装:`Docker v20.10+``Docker Compose v2.0+`
- 支持环境:`Centos/Debian/Ubuntu/macOS/Windows`
- 支持环境:`Centos/Debian/Ubuntu/macOS` 等 linux/unix 系统
- 硬件建议2核4G以上
- 特别说明Windows 用户请使用 `git bash` 或者 `cmder` 运行命令
- 特别说明Windows 可以使用 WSL2 安装 Linux 环境后再安装 DooTask。
### 部署项目Pro版
### 部署项目
```bash
# 1、克隆项目到您的本地或服务器
# 通过github克隆项目
git clone -b pro --depth=1 https://github.com/kuaifan/dootask.git
git clone --depth=1 https://github.com/kuaifan/dootask.git
# 或者你也可以使用gitee
git clone -b pro --depth=1 https://gitee.com/aipaw/dootask.git
git clone --depth=1 https://gitee.com/aipaw/dootask.git
# 2、进入目录
cd dootask
@@ -50,48 +58,37 @@ cd dootask
### 停止服务
```bash
./cmd stop
./cmd down
```
# 一旦应用程序被设置,无论何时你想要启动服务器(如果它被停止)运行以下命令
./cmd start
### 启动服务
```bash
./cmd up
```
### 开发编译
- 请确保你已经安装了 `NodeJs 20+`
请确保你已经安装了 `NodeJs 20+`
```bash
# 开发模式
./cmd dev
# 编译项目(这是网页端的,App/Pc/Mac客户端请查看 README_CLIENT.md
# 编译项目(这是网页端的,客户端请参考“.github/workflows/publish.yml”文件
./cmd prod
```
### 运行命令的快捷方式
```bash
# 你可以使用以下命令来执行
./cmd artisan "your command" # 运行 artisan 命令
./cmd php "your command" # 运行 php 命令
./cmd nginx "your command" # 运行 nginx 命令
./cmd redis "your command" # 运行 redis 命令
./cmd composer "your command" # 运行 composer 命令
./cmd supervisorctl "your command" # 运行 supervisorctl 命令
./cmd mysql "your command" # 运行 mysql 命令 (backup: 备份数据库recovery: 还原数据库open: 开启数据库外部端口访问close: 关闭数据库外部端口访问)
```
### SSL 配置
#### 方法1自动配置
```bash
# 在项目下运行命令,根据提示执行即可
# 执行指令,根据提示执行即可
./cmd https
```
#### (或者)方法2Nginx 代理配置
#### 方法2Nginx 代理配置
```bash
# 1、Nginx 代理配置添加
@@ -99,7 +96,7 @@ proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 2、在项目下运行命令(如果取消 Nginx 代理配置请运行:./cmd https close
# 2、执行指令(如果取消 Nginx 代理配置请运行:./cmd https close
./cmd https agent
```
@@ -108,19 +105,11 @@ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
**注意:在升级之前请备份好你的数据!**
```bash
# 方法1在项目下运行命令
./cmd update
# 或者方法2如果方法1失败请使用此方法
git pull
./cmd mysql backup
./cmd uninstall
./cmd install
./cmd mysql recovery
```
* 跨越大版本升级失败时请重试执行一次。
* 如果升级后出现502请运行 `./cmd restart` 重启服务即可。
* 如果升级后出现502请运行 `./cmd reup` 重启服务即可。
## 迁移项目
@@ -129,21 +118,30 @@ git pull
1、备份原数据库
```bash
# 在旧的项目下运行命
# 在旧的项目下执行指
./cmd mysql backup
```
2、将`数据库备份文件`及`public/uploads`目录拷贝至新项目
2、将旧项目以下文件和目录拷贝至新项目同路径位置
- `数据库备份文件`
- `docker/appstore`
- `public/uploads`
3、还原数据库至新项目
```bash
# 在新的项目下运行命
# 在新的项目下执行指
./cmd mysql recovery
```
## 卸载项目
```bash
# 在项目下运行命令
./cmd uninstall
```
### 更多指令
```bash
./cmd help
```

View File

@@ -3,6 +3,7 @@
namespace App\Console\Commands;
use App\Models\WebSocketDialogMsg;
use App\Module\Apps;
use App\Module\ZincSearch\ZincSearchKeyValue;
use App\Module\ZincSearch\ZincSearchDialogMsg;
use Cache;
@@ -27,6 +28,11 @@ class SyncUserMsgToZincSearch extends Command
*/
public function handle(): int
{
if (!Apps::isInstalled("search")) {
$this->error("应用「ZincSearch」未安装");
return 1;
}
// 注册信号处理器仅在支持pcntl扩展的环境下
if (extension_loaded('pcntl')) {
pcntl_async_signals(true); // 启用异步信号处理

View File

@@ -10,13 +10,18 @@ class ApiException extends RuntimeException
*/
protected $data;
/**
* @var bool
*/
protected $writeLog = true;
/**
* ApiException constructor.
* @param string $msg
* @param string|array $msg
* @param array $data
* @param int $code
*/
public function __construct($msg = '', $data = [], $code = 0)
public function __construct($msg = '', $data = [], $code = 0, $writeLog = true)
{
if (is_array($msg) && isset($msg['code'])) {
$code = $msg['code'];
@@ -24,6 +29,7 @@ class ApiException extends RuntimeException
$msg = $msg['msg'];
}
$this->data = $data;
$this->writeLog = $writeLog && $code !== -1;
parent::__construct($msg, $code);
}
@@ -34,4 +40,12 @@ class ApiException extends RuntimeException
{
return $this->data;
}
/**
* @return bool
*/
public function isWriteLog(): bool
{
return $this->writeLog;
}
}

View File

@@ -74,7 +74,7 @@ class Handler extends ExceptionHandler
public function report(Throwable $e)
{
if ($e instanceof ApiException) {
if ($e->getCode() !== -1) {
if ($e->isWriteLog()) {
Log::error($e->getMessage(), [
'code' => $e->getCode(),
'data' => $e->getData(),

View File

@@ -20,8 +20,10 @@ use App\Models\ApproveProcInstHistory;
use App\Exceptions\ApiException;
use App\Models\UserDepartment;
use App\Models\WebSocketDialogMsg;
use App\Module\Apps;
use App\Module\BillMultipleExport;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Swoole\Coroutine;
/**
* @apiDefine approve
@@ -34,6 +36,7 @@ class ApproveController extends AbstractController
public function __construct()
{
Apps::isInstalledThrow('approve');
$this->flow_url = env('FLOW_URL') ?: 'http://approve';
}
@@ -766,131 +769,192 @@ class ApproveController extends AbstractController
if (Carbon::parse($date[1])->timestamp - Carbon::parse($date[0])->timestamp > 35 * 86400) {
return Base::retError('日期范围限制最大35天');
}
//
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/findAllProcIns', json_encode($data));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
return Base::retError($process['message'] ?? '查询失败');
$botUser = User::botGetOrCreate('system-msg');
if (empty($botUser)) {
return Base::retError('系统机器人不存在');
}
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
//
$res = Base::arrayKeyToUnderline($process['data']);
//
$headings = [];
$headings[] = Doo::translate('申请编号');
$headings[] = Doo::translate('标题');
$headings[] = Doo::translate('申请状态');
$headings[] = Doo::translate('发起时间');
$headings[] = Doo::translate('完成时间');
$headings[] = Doo::translate('发起人工号');
$headings[] = Doo::translate('发起人User ID');
$headings[] = Doo::translate('发起人姓名');
$headings[] = Doo::translate('发起人部门');
$headings[] = Doo::translate('发起人部门ID');
$headings[] = Doo::translate('部门负责人');
$headings[] = Doo::translate('历史审批人');
$headings[] = Doo::translate('历史办理人');
$headings[] = Doo::translate('审批记录');
$headings[] = Doo::translate('当前处理人');
$headings[] = Doo::translate('审批节点');
$headings[] = Doo::translate('审批人数');
$headings[] = Doo::translate('审批耗时');
$headings[] = Doo::translate('假期类型');
$headings[] = Doo::translate('开始时间');
$headings[] = Doo::translate('结束时间');
$headings[] = Doo::translate('时长');
$headings[] = Doo::translate('请假事由');
$headings[] = Doo::translate('请假单位');
//
$datas = [];
foreach ($res as $val) {
go(function () use ($data, $user, $botUser, $dialog) {
Coroutine::sleep(1);
//
$nickname = Base::filterEmoji($val['start_user_name']);
$participant = $this->getUserProcessParticipantById($val['id']); // 获取参与人
$participant = $this->handleParticipant($val, $participant['data']); // 处理参与人返回数据
//
$job_number = ''; // 发起人工号
$department_leader = User::userid2nickname(UserDepartment::find(1, ['owner_userid'])['owner_userid']); // 部门负责人
$historical_approver = $participant['historical_approver'] ?? ''; // 历史审批人
$historical_agent = ''; // 历史办理人
$approval_record = $participant['approval_record'] ?? ''; // 审批记录
$current_handler = !$val['is_finished'] ? implode(',', User::whereIn('userid', explode(';', $val['candidate']))->pluck('nickname')->toArray()) : ''; // 当前处理人
$approved_node = $participant['approved_node'] ?? 0; // 审批节点
$approved_num = $participant['approved_num'] ?? 0; // 审批人数
// 计算审批耗时
$startTime = Carbon::parse($val['start_time'])->timestamp;
$endTime = $val['end_time'] ? Carbon::parse($val['end_time'])->timestamp : time();
$approval_time = Doo::translate(Timer::timeDiff($startTime, $endTime)); // 审批耗时
// 计算时长
$varStartTime = Carbon::parse($val['var']['start_time']);
$varEndTime = Carbon::parse($val['var']['end_time']);
$duration = $varEndTime->floatDiffInHours($varStartTime);
$duration_unit = Doo::translate('小时'); // 时长单位
$datas[] = [
$val['id'], // 申请编号
$val['proc_def_name'], // 标题
$this->getStateDescription($val['state']), // 申请状态
$val['start_time'], // 发起时间
$val['end_time'], // 完成时间
$job_number, // 发起人工号
$val['start_user_id'], // 发起人User ID
$nickname, // 发起人姓名
$val['department'], // 发起人部门
$val['department_id'], // 发起人部门ID
$department_leader, // 部门负责人
$historical_approver, // 历史审批人
$historical_agent, // 历史办理人
$approval_record, // 审批记录
$current_handler, // 当前处理人
$approved_node, // 审批节点
$approved_num, // 审批人数
$approval_time, // 审批耗时
$val['var']['type'], // 假期类型
$val['var']['start_time'], // 开始时间
$val['var']['end_time'], // 结束时间
$duration, // 时长
$val['var']['description'], // 请假事由
$duration_unit, // 请假单位
$content = [];
$content[] = [
'content' => '导出审批数据已完成',
'style' => 'font-weight: bold;padding-bottom: 4px;',
];
}
if (empty($datas)) {
return Base::retError('没有任何数据');
}
//
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/findAllProcIns', json_encode($data));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
$content[] = [
'content' => $process['message'] ?? '查询失败',
'style' => 'color: #ff0000;',
];
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'title' => $content[0]['content'],
'content' => $content,
], $botUser->userid, true, false, true);
return;
}
//
$res = Base::arrayKeyToUnderline($process['data']);
//
$headings = [];
$headings[] = Doo::translate('申请编号');
$headings[] = Doo::translate('标题');
$headings[] = Doo::translate('申请状态');
$headings[] = Doo::translate('发起时间');
$headings[] = Doo::translate('完成时间');
$headings[] = Doo::translate('发起人工号');
$headings[] = Doo::translate('发起人User ID');
$headings[] = Doo::translate('发起人姓名');
$headings[] = Doo::translate('发起人部门');
$headings[] = Doo::translate('发起人部门ID');
$headings[] = Doo::translate('部门负责人');
$headings[] = Doo::translate('历史审批人');
$headings[] = Doo::translate('历史办理人');
$headings[] = Doo::translate('审批记录');
$headings[] = Doo::translate('当前处理人');
$headings[] = Doo::translate('审批节点');
$headings[] = Doo::translate('审批人数');
$headings[] = Doo::translate('审批耗时');
$headings[] = Doo::translate('假期类型');
$headings[] = Doo::translate('开始时间');
$headings[] = Doo::translate('结束时间');
$headings[] = Doo::translate('时长');
$headings[] = Doo::translate('请假事由');
$headings[] = Doo::translate('请假单位');
//
$datas = [];
foreach ($res as $val) {
//
$nickname = Base::filterEmoji($val['start_user_name']);
$participant = $this->getUserProcessParticipantById($val['id']); // 获取参与人
$participant = $this->handleParticipant($val, $participant['data']); // 处理参与人返回数据
//
$job_number = ''; // 发起人工号
$department_leader = User::userid2nickname(UserDepartment::find(1, ['owner_userid'])['owner_userid']); // 部门负责人
$historical_approver = $participant['historical_approver'] ?? ''; // 历史审批人
$historical_agent = ''; // 历史办理人
$approval_record = $participant['approval_record'] ?? ''; // 审批记录
$current_handler = !$val['is_finished'] ? implode(',', User::whereIn('userid', explode(';', $val['candidate']))->pluck('nickname')->toArray()) : ''; // 当前处理人
$approved_node = $participant['approved_node'] ?? 0; // 审批节点
$approved_num = $participant['approved_num'] ?? 0; // 审批人数
// 计算审批耗时
$startTime = Carbon::parse($val['start_time'])->timestamp;
$endTime = $val['end_time'] ? Carbon::parse($val['end_time'])->timestamp : time();
$approval_time = Doo::translate(Timer::timeDiff($startTime, $endTime)); // 审批耗时
// 计算时长
$varStartTime = Carbon::parse($val['var']['start_time']);
$varEndTime = Carbon::parse($val['var']['end_time']);
$duration = $varEndTime->floatDiffInHours($varStartTime);
$duration_unit = Doo::translate('小时'); // 时长单位
$datas[] = [
$val['id'], // 申请编号
$val['proc_def_name'], // 标题
$this->getStateDescription($val['state']), // 申请状态
$val['start_time'], // 发起时间
$val['end_time'], // 完成时间
$job_number, // 发起人工号
$val['start_user_id'], // 发起人User ID
$nickname, // 发起人姓名
$val['department'], // 发起人部门
$val['department_id'], // 发起人部门ID
$department_leader, // 部门负责人
$historical_approver, // 历史审批人
$historical_agent, // 历史办理人
$approval_record, // 审批记录
$current_handler, // 当前处理人
$approved_node, // 审批节点
$approved_num, // 审批人数
$approval_time, // 审批耗时
$val['var']['type'], // 假期类型
$val['var']['start_time'], // 开始时间
$val['var']['end_time'], // 结束时间
$duration, // 时长
$val['var']['description'], // 请假事由
$duration_unit, // 请假单位
];
}
if (empty($datas)) {
$content[] = [
'content' => '没有任何数据',
'style' => 'color: #ff0000;',
];
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'title' => $content[0]['content'],
'content' => $content,
], $botUser->userid, true, false, true);
return;
}
//
$title = Doo::translate("审批记录");
$sheets = [
BillExport::create()->setTitle($title)->setHeadings($headings)->setData($datas)->setStyles(["A1:Y1" => ["font" => ["bold" => true]]])
];
//
$fileName = $title . '_' . Timer::time() . '.xlsx';
$filePath = "temp/approve/export/" . date("Ym", Timer::time());
$export = new BillMultipleExport($sheets);
$res = $export->store($filePath . "/" . $fileName);
if ($res != 1) {
$content[] = [
'content' => "导出失败,{$fileName}",
'style' => 'color: #ff0000;',
];
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'title' => $content[0]['content'],
'content' => $content,
], $botUser->userid, true, false, true);
return;
}
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xlsx') . ".zip";
$zipPath = storage_path($zipFile);
if (file_exists($zipPath)) {
Base::deleteDirAndFile($zipPath, true);
}
try {
Madzipper::make($zipPath)->add($xlsPath)->close();
} catch (\Throwable) {
}
//
if (file_exists($zipPath)) {
$base64 = base64_encode(Base::array2string([
'file' => $zipFile,
]));
$fileUrl = Base::fillUrl('api/approve/down?key=' . urlencode($base64));
Session::put('approve::export:userid', $user->userid);
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'file_download',
'title' => '导出审批数据已完成',
'name' => $fileName,
'size' => filesize($zipPath),
'url' => $fileUrl,
], $botUser->userid, true, false, true);
} else {
$content[] = [
'content' => "打包失败,请稍后再试...",
'style' => 'color: #ff0000;',
];
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'title' => $content[0]['content'],
'content' => $content,
], $botUser->userid, true, false, true);
}
});
//
$title = Doo::translate("审批记录");
$sheets = [
BillExport::create()->setTitle($title)->setHeadings($headings)->setData($datas)->setStyles(["A1:Y1" => ["font" => ["bold" => true]]])
];
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'content' => '正在导出审批数据,请稍等...',
], $botUser->userid, true, false, true);
//
$fileName = $title . '_' . Timer::time() . '.xlsx';
$filePath = "temp/approve/export/" . date("Ym", Timer::time());
$export = new BillMultipleExport($sheets);
$res = $export->store($filePath . "/" . $fileName);
if ($res != 1) {
return Base::retError('导出失败,' . $fileName . '');
}
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xlsx') . ".zip";
$zipPath = storage_path($zipFile);
if (file_exists($zipPath)) {
Base::deleteDirAndFile($zipPath, true);
}
try {
Madzipper::make($zipPath)->add($xlsPath)->close();
} catch (\Throwable) {
}
//
if (file_exists($zipPath)) {
$base64 = base64_encode(Base::array2string([
'file' => $zipFile,
]));
Session::put('approve::export:userid', $user->userid);
return Base::retSuccess('success', [
'size' => Base::twoFloat(filesize($zipPath) / 1024, true),
'url' => Base::fillUrl('api/approve/down?key=' . urlencode($base64)),
]);
} else {
return Base::retError('打包失败,请稍后再试...');
}
return Base::retSuccess('success');
}
function getStateDescription($state)

View File

@@ -503,6 +503,10 @@ class FileController extends AbstractController
return Base::retError('参数错误');
}
//
if ($down == 'no') {
File::isNeedInstallApp($file->type);
}
//
if ($only_update_at == 'yes') {
return Base::retSuccess('success', [
'id' => $file->id,
@@ -577,10 +581,12 @@ class FileController extends AbstractController
$contentArray = Base::json2array($content);
$contentString = $contentArray['xml'];
$file->ext = 'drawio';
File::isNeedInstallApp($file->type);
break;
case 'mind':
$contentString = $content;
$file->ext = 'mind';
File::isNeedInstallApp($file->type);
break;
case 'txt':
case 'code':
@@ -632,6 +638,8 @@ class FileController extends AbstractController
{
User::auth();
//
File::isNeedInstallApp('office');
//
$config = Request::input('config');
$token = \Firebase\JWT\JWT::encode($config, env('APP_KEY') ,'HS256');
return Base::retSuccess('成功', [
@@ -657,6 +665,8 @@ class FileController extends AbstractController
{
$user = User::auth();
//
File::isNeedInstallApp('office');
//
$id = intval(Request::input('id'));
$status = intval(Request::input('status'));
$key = Request::input('key');
@@ -777,6 +787,8 @@ class FileController extends AbstractController
//
$file = File::permissionFind($id, $user);
//
File::isNeedInstallApp($file->type);
//
$history = FileContent::whereFid($file->id)->whereId($history_id)->first();
if (empty($history)) {
return Base::retError('历史数据不存在或已被删除');

View File

@@ -1258,7 +1258,7 @@ class ProjectController extends AbstractController
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
//
go(function () use ($user, $userid, $time, $type, $botUser, $dialog) {
Coroutine::sleep(0.1);
Coroutine::sleep(1);
$headings = [];
$headings[] = Doo::translate('任务ID');
$headings[] = Doo::translate('父级任务ID');
@@ -1394,7 +1394,7 @@ class ProjectController extends AbstractController
'type' => 'content',
'title' => $content[0]['content'],
'content' => $content,
], $botUser->userid, false, false, true);
], $botUser->userid, true, false, true);
return;
}
//
@@ -1428,7 +1428,7 @@ class ProjectController extends AbstractController
'type' => 'content',
'title' => $content[0]['content'],
'content' => $content,
], $botUser->userid, false, false, true);
], $botUser->userid, true, false, true);
return;
}
//
@@ -1455,7 +1455,7 @@ class ProjectController extends AbstractController
'name' => $fileName,
'size' => filesize($zipPath),
'url' => $fileUrl,
], $botUser->userid, false, false, true);
], $botUser->userid, true, false, true);
} else {
$content[] = [
'content' => "打包失败,请稍后再试...",
@@ -1465,9 +1465,15 @@ class ProjectController extends AbstractController
'type' => 'content',
'title' => $content[0]['content'],
'content' => $content,
], $botUser->userid, false, false, true);
], $botUser->userid, true, false, true);
}
});
//
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'content' => '正在导出任务统计,请稍等...',
], $botUser->userid, true, false, true);
//
return Base::retSuccess('success');
}
@@ -1487,103 +1493,156 @@ class ProjectController extends AbstractController
{
$user = User::auth('admin');
//
$headings = [];
$headings[] = Doo::translate('任务ID');
$headings[] = Doo::translate('父级任务ID');
$headings[] = Doo::translate('所属项目');
$headings[] = Doo::translate('任务标题');
$headings[] = Doo::translate('任务标签');
$headings[] = Doo::translate('任务开始时间');
$headings[] = Doo::translate('任务结束时间');
$headings[] = Doo::translate('任务计划用时');
$headings[] = Doo::translate('超时时间');
$headings[] = Doo::translate('负责人');
$headings[] = Doo::translate('创建人');
$data = [];
$botUser = User::botGetOrCreate('system-msg');
if (empty($botUser)) {
return Base::retError('系统机器人不存在');
}
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
//
ProjectTask::with(['taskTag'])
->whereNull('complete_at')
->whereNotNull('end_at')
->where('end_at', '<=', Carbon::now())
->orderBy('end_at')
->chunk(100, function ($tasks) use (&$data) {
/** @var ProjectTask $task */
foreach ($tasks as $task) {
$taskStartTime = Carbon::parse($task->start_at ?: $task->created_at)->timestamp;
$totalTime = time() - $taskStartTime; //开发测试总用时
$planTime = '-';//任务计划用时
$overTime = '-';//超时时间
if ($task->end_at) {
$startTime = Carbon::parse($task->start_at)->timestamp;
$endTime = Carbon::parse($task->end_at)->timestamp;
$planTotalTime = $endTime - $startTime;
$residueTime = $planTotalTime - $totalTime;
if ($residueTime < 0) {
$overTime = Doo::translate(Timer::timeFormat(abs($residueTime)));
go(function () use ($botUser, $dialog, $user) {
Coroutine::sleep(1);
//
$headings = [];
$headings[] = Doo::translate('任务ID');
$headings[] = Doo::translate('父级任务ID');
$headings[] = Doo::translate('所属项目');
$headings[] = Doo::translate('任务标题');
$headings[] = Doo::translate('任务标签');
$headings[] = Doo::translate('任务开始时间');
$headings[] = Doo::translate('任务结束时间');
$headings[] = Doo::translate('任务计划用时');
$headings[] = Doo::translate('超时时间');
$headings[] = Doo::translate('负责人');
$headings[] = Doo::translate('创建人');
$data = [];
//
$content = [];
$content[] = [
'content' => '导出超期任务已完成',
'style' => 'font-weight: bold;padding-bottom: 4px;',
];
//
ProjectTask::with(['taskTag'])
->whereNull('complete_at')
->whereNotNull('end_at')
->where('end_at', '<=', Carbon::now())
->orderBy('end_at')
->chunk(100, function ($tasks) use (&$data) {
/** @var ProjectTask $task */
foreach ($tasks as $task) {
$taskStartTime = Carbon::parse($task->start_at ?: $task->created_at)->timestamp;
$totalTime = time() - $taskStartTime; //开发测试总用时
$planTime = '-';//任务计划用时
$overTime = '-';//超时时间
if ($task->end_at) {
$startTime = Carbon::parse($task->start_at)->timestamp;
$endTime = Carbon::parse($task->end_at)->timestamp;
$planTotalTime = $endTime - $startTime;
$residueTime = $planTotalTime - $totalTime;
if ($residueTime < 0) {
$overTime = Doo::translate(Timer::timeFormat(abs($residueTime)));
}
$planTime = Doo::translate(Timer::timeDiff($startTime, $endTime));
}
$planTime = Doo::translate(Timer::timeDiff($startTime, $endTime));
$ownerIds = $task->taskUser->where('owner', 1)->pluck('userid')->toArray();
$ownerNames = [];
foreach ($ownerIds as $ownerId) {
$ownerNames[] = Base::filterEmoji(User::userid2nickname($ownerId)) . " (ID: {$ownerId})";
}
$data[] = [
$task->id,
$task->parent_id ?: '-',
Base::filterEmoji($task->project?->name) ?: '-',
Base::filterEmoji($task->name),
$task->taskTag->map(function ($tag) {
return Base::filterEmoji($tag->name);
})->join(', ') ?: '-',
$task->start_at ?: '-',
$task->end_at ?: '-',
$planTime,
$overTime,
implode(', ', $ownerNames),
Base::filterEmoji(User::userid2nickname($task->userid)) . " (ID: {$task->userid})",
];
}
$ownerIds = $task->taskUser->where('owner', 1)->pluck('userid')->toArray();
$ownerNames = [];
foreach ($ownerIds as $ownerId) {
$ownerNames[] = Base::filterEmoji(User::userid2nickname($ownerId)) . " (ID: {$ownerId})";
}
$data[] = [
$task->id,
$task->parent_id ?: '-',
Base::filterEmoji($task->project?->name) ?: '-',
Base::filterEmoji($task->name),
$task->taskTag->map(function ($tag) {
return Base::filterEmoji($tag->name);
})->join(', ') ?: '-',
$task->start_at ?: '-',
$task->end_at ?: '-',
$planTime,
$overTime,
implode(', ', $ownerNames),
Base::filterEmoji(User::userid2nickname($task->userid)) . " (ID: {$task->userid})",
];
}
});
if (empty($data)) {
return Base::retError('没有任何数据');
}
});
if (empty($data)) {
$content[] = [
'content' => '没有任何数据',
'style' => 'color: #ff0000;',
];
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'title' => $content[0]['content'],
'content' => $content,
], $botUser->userid, true, false, true);
return;
}
//
$title = Doo::translate('超期任务');
$sheets = [
BillExport::create()->setTitle($title)->setHeadings($headings)->setData($data)->setStyles(["A1:J1" => ["font" => ["bold" => true]]])
];
//
$fileName = $title . '_' . Timer::time() . '.xls';
$filePath = "temp/task/export/" . date("Ym", Timer::time());
$export = new BillMultipleExport($sheets);
$res = $export->store($filePath . "/" . $fileName);
if ($res != 1) {
$content[] = [
'content' => "导出失败,{$fileName}",
'style' => 'color: #ff0000;',
];
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'title' => $content[0]['content'],
'content' => $content,
], $botUser->userid, true, false, true);
return;
}
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xls') . ".zip";
$zipPath = storage_path($zipFile);
if (file_exists($zipPath)) {
Base::deleteDirAndFile($zipPath, true);
}
try {
Madzipper::make($zipPath)->add($xlsPath)->close();
} catch (\Throwable) {
}
//
if (file_exists($zipPath)) {
$base64 = base64_encode(Base::array2string([
'file' => $zipFile,
]));
$fileUrl = Base::fillUrl('api/project/task/down?key=' . urlencode($base64));
Session::put('task::export:userid', $user->userid);
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'file_download',
'title' => '导出超期任务已完成',
'name' => $fileName,
'size' => filesize($zipPath),
'url' => $fileUrl,
], $botUser->userid, true, false, true);
} else {
$content[] = [
'content' => "打包失败,请稍后再试...",
'style' => 'color: #ff0000;',
];
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'title' => $content[0]['content'],
'content' => $content,
], $botUser->userid, true, false, true);
}
});
//
$title = Doo::translate('超期任务');
$sheets = [
BillExport::create()->setTitle($title)->setHeadings($headings)->setData($data)->setStyles(["A1:J1" => ["font" => ["bold" => true]]])
];
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'content' => '正在导出超期任务,请稍等...',
], $botUser->userid, true, false, true);
//
$fileName = $title . '_' . Timer::time() . '.xls';
$filePath = "temp/task/export/" . date("Ym", Timer::time());
$export = new BillMultipleExport($sheets);
$res = $export->store($filePath . "/" . $fileName);
if ($res != 1) {
return Base::retError('导出失败,' . $fileName . '');
}
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xls') . ".zip";
$zipPath = storage_path($zipFile);
if (file_exists($zipPath)) {
Base::deleteDirAndFile($zipPath, true);
}
try {
Madzipper::make($zipPath)->add($xlsPath)->close();
} catch (\Throwable) {
}
//
if (file_exists($zipPath)) {
$base64 = base64_encode(Base::array2string([
'file' => $zipFile,
]));
Session::put('task::export:userid', $user->userid);
return Base::retSuccess('success', [
'size' => Base::twoFloat(filesize($zipPath) / 1024, true),
'url' => Base::fillUrl('api/project/task/down?key=' . urlencode($base64)),
]);
} else {
return Base::retError('打包失败,请稍后再试...');
}
return Base::retSuccess('success');
}
/**
@@ -1831,6 +1890,7 @@ class ProjectController extends AbstractController
'update_at' => Carbon::parse($file->updated_at)->toDateTimeString()
]);
}
File::isNeedInstallApp($file->ext);
//
$data = $file->toArray();
$data['path'] = $file->getRawOriginal('path');

View File

@@ -3,6 +3,8 @@
namespace App\Http\Controllers\Api;
use App\Models\UserDevice;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use Request;
use Session;
use Response;
@@ -18,9 +20,11 @@ use LdapRecord\Container;
use App\Module\BillExport;
use Guanguans\Notify\Factory;
use App\Models\UserCheckinRecord;
use App\Module\Apps;
use App\Module\BillMultipleExport;
use LdapRecord\LdapRecordException;
use Guanguans\Notify\Messages\EmailMessage;
use Swoole\Coroutine;
/**
* @apiDefine system
@@ -40,7 +44,7 @@ class SystemController extends AbstractController
* @apiParam {String} type
* - get: 获取(默认)
* - all: 获取所有(需要管理员权限)
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'voice2text', 'translation', 'convert_video', 'compress_video', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'system_alias', 'system_welcome', 'image_compress', 'image_quality', 'image_save_local', 'start_home']
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'voice2text', 'translation', 'convert_video', 'compress_video', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'system_alias', 'system_welcome', 'image_compress', 'image_quality', 'image_save_local']
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -87,7 +91,6 @@ class SystemController extends AbstractController
'image_compress',
'image_quality',
'image_save_local',
'start_home',
'file_upload_limit',
'unclaimed_task_reminder',
'unclaimed_task_reminder_time',
@@ -149,11 +152,9 @@ class SystemController extends AbstractController
$setting['all_group_autoin'] = $setting['all_group_autoin'] ?: 'yes';
$setting['user_private_chat_mute'] = $setting['user_private_chat_mute'] ?: 'open';
$setting['user_group_chat_mute'] = $setting['user_group_chat_mute'] ?: 'open';
$setting['start_home'] = $setting['start_home'] ?: 'close';
$setting['file_upload_limit'] = $setting['file_upload_limit'] ?: '';
$setting['unclaimed_task_reminder'] = $setting['unclaimed_task_reminder'] ?: 'close';
$setting['unclaimed_task_reminder_time'] = $setting['unclaimed_task_reminder_time'] ?: '';
$setting['server_closeai'] = env("SERVER_CLOSEAI") ?: 'open';
$setting['server_timezone'] = config('app.timezone');
$setting['server_version'] = Base::getVersion();
//
@@ -304,6 +305,8 @@ class SystemController extends AbstractController
{
User::auth('admin');
//
Apps::isInstalledThrow('ai');
//
$type = trim(Request::input('type'));
$filter = trim(Request::input('filter'));
$setting = Base::setting('aibotSetting');
@@ -453,16 +456,22 @@ class SystemController extends AbstractController
if (!$botUser) {
return Base::retError('创建签到机器人失败');
}
if (in_array('locat', $all['modes'])) {
if (empty($all['locat_bd_lbs_key'])) {
return Base::retError('请填写百度地图AK');
if (is_array($all['modes'])) {
if (in_array('locat', $all['modes'])) {
if (empty($all['locat_bd_lbs_key'])) {
return Base::retError('请填写百度地图AK');
}
if (!is_array($all['locat_bd_lbs_point'])) {
return Base::retError('请选择允许签到位置');
}
$all['locat_bd_lbs_point']['radius'] = intval($all['locat_bd_lbs_point']['radius']);
if (empty($all['locat_bd_lbs_point']['lng']) || empty($all['locat_bd_lbs_point']['lat']) || empty($all['locat_bd_lbs_point']['radius'])) {
return Base::retError('请选择有效的签到位置');
}
}
if (!is_array($all['locat_bd_lbs_point'])) {
return Base::retError('请选择允许签到位置');
}
$all['locat_bd_lbs_point']['radius'] = intval($all['locat_bd_lbs_point']['radius']);
if (empty($all['locat_bd_lbs_point']['lng']) || empty($all['locat_bd_lbs_point']['lat']) || empty($all['locat_bd_lbs_point']['radius'])) {
return Base::retError('请选择有效的签到位置');
// 人脸识别
if (in_array('face', $all['modes'])) {
Apps::isInstalledThrow('face');
}
}
}
@@ -812,6 +821,7 @@ class SystemController extends AbstractController
'info' => Doo::license(),
'macs' => Doo::macs(),
'doo_sn' => Doo::dooSN(),
'doo_version' => Doo::dooVersion(),
'user_count' => User::whereBot(0)->whereNull('disable_at')->count(),
'error' => []
];
@@ -820,7 +830,7 @@ class SystemController extends AbstractController
if ($data['info']['sn'] != $data['doo_sn']) {
$data['error'][] = '终端SN与License不匹配';
}
if ($data['info']['mac']) {
if ($data['info']['mac'] && $data['macs']) {
$approved = false;
foreach ($data['info']['mac'] as $mac) {
if (in_array($mac, $data['macs'])) {
@@ -1257,7 +1267,7 @@ class SystemController extends AbstractController
*/
public function checkin__export()
{
User::auth('admin');
$user = User::auth('admin');
//
$setting = Base::setting('checkinSetting');
if ($setting['open'] !== 'open') {
@@ -1287,126 +1297,179 @@ class SystemController extends AbstractController
$secondStart = strtotime("2000-01-01 {$time[0]}") - strtotime("2000-01-01 00:00:00");
$secondEnd = strtotime("2000-01-01 {$time[1]}") - strtotime("2000-01-01 00:00:00");
//
$headings = [];
$headings[] = Doo::translate('签到人');
$headings[] = Doo::translate('签到日期');
$headings[] = Doo::translate('班次时间');
$headings[] = Doo::translate('首次签到时间');
$headings[] = Doo::translate('首次签到结果');
$headings[] = Doo::translate('最后签到时间');
$headings[] = Doo::translate('最后签到结果');
$headings[] = Doo::translate('参数数据');
$botUser = User::botGetOrCreate('system-msg');
if (empty($botUser)) {
return Base::retError('系统机器人不存在');
}
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
//
$sheets = [];
$startD = Carbon::parse($date[0])->startOfDay();
$endD = Carbon::parse($date[1])->endOfDay();
$users = User::whereIn('userid', $userid)->take(100)->get();
/** @var User $user */
foreach ($users as $user) {
$recordTimes = UserCheckinRecord::getTimes($user->userid, [$startD, $endD]);
go(function () use ($secondStart, $secondEnd, $time, $userid, $date, $user, $botUser, $dialog) {
Coroutine::sleep(1);
//
$nickname = Base::filterEmoji($user->nickname);
$styles = ["A1:H1" => ["font" => ["bold" => true]]];
$datas = [];
$startT = $startD->timestamp;
$endT = $endD->timestamp;
$index = 1;
while ($startT < $endT) {
$index++;
$sameDate = date("Y-m-d", $startT);
$sameTimes = $recordTimes[$sameDate] ?? [];
$sameCollect = UserCheckinRecord::atCollect($sameDate, $sameTimes);
$firstBetween = [Carbon::createFromTimestamp($startT), Carbon::createFromTimestamp($startT + $secondEnd - 1)];
$lastBetween = [Carbon::createFromTimestamp($startT + $secondStart + 1), Carbon::createFromTimestamp($startT + 86400)];
$firstRecord = $sameCollect?->whereBetween("datetime", $firstBetween)->first();
$lastRecord = $sameCollect?->whereBetween("datetime", $lastBetween)->last();
$firstTimestamp = $firstRecord['timestamp'] ?: 0;
$lastTimestamp = $lastRecord['timestamp'] ?: 0;
if (Timer::time() < $startT + $secondStart) {
$firstResult = "-";
} else {
$firstResult = Doo::translate("正常");
if (empty($firstTimestamp)) {
$firstResult = Doo::translate("缺卡");
$styles["E{$index}"] = ["font" => ["color" => ["rgb" => "ff0000"]]];
} elseif ($firstTimestamp > $startT + $secondStart) {
$firstResult = Doo::translate("迟到");
$styles["E{$index}"] = ["font" => ["color" => ["rgb" => "436FF6"]]];
$headings = [];
$headings[] = Doo::translate('签到人');
$headings[] = Doo::translate('签到日期');
$headings[] = Doo::translate('班次时间');
$headings[] = Doo::translate('首次签到时间');
$headings[] = Doo::translate('首次签到结果');
$headings[] = Doo::translate('最后签到时间');
$headings[] = Doo::translate('最后签到结果');
$headings[] = Doo::translate('参数数据');
//
$content = [];
$content[] = [
'content' => '导出签到数据已完成',
'style' => 'font-weight: bold;padding-bottom: 4px;',
];
//
$sheets = [];
$startD = Carbon::parse($date[0])->startOfDay();
$endD = Carbon::parse($date[1])->endOfDay();
$users = User::whereIn('userid', $userid)->take(100)->get();
/** @var User $user */
foreach ($users as $user) {
$recordTimes = UserCheckinRecord::getTimes($user->userid, [$startD, $endD]);
//
$nickname = Base::filterEmoji($user->nickname);
$styles = ["A1:H1" => ["font" => ["bold" => true]]];
$datas = [];
$startT = $startD->timestamp;
$endT = $endD->timestamp;
$index = 1;
while ($startT < $endT) {
$index++;
$sameDate = date("Y-m-d", $startT);
$sameTimes = $recordTimes[$sameDate] ?? [];
$sameCollect = UserCheckinRecord::atCollect($sameDate, $sameTimes);
$firstBetween = [Carbon::createFromTimestamp($startT), Carbon::createFromTimestamp($startT + $secondEnd - 1)];
$lastBetween = [Carbon::createFromTimestamp($startT + $secondStart + 1), Carbon::createFromTimestamp($startT + 86400)];
$firstRecord = $sameCollect?->whereBetween("datetime", $firstBetween)->first();
$lastRecord = $sameCollect?->whereBetween("datetime", $lastBetween)->last();
$firstTimestamp = $firstRecord['timestamp'] ?: 0;
$lastTimestamp = $lastRecord['timestamp'] ?: 0;
if (Timer::time() < $startT + $secondStart) {
$firstResult = "-";
} else {
$firstResult = Doo::translate("正常");
if (empty($firstTimestamp)) {
$firstResult = Doo::translate("缺卡");
$styles["E{$index}"] = ["font" => ["color" => ["rgb" => "ff0000"]]];
} elseif ($firstTimestamp > $startT + $secondStart) {
$firstResult = Doo::translate("迟到");
$styles["E{$index}"] = ["font" => ["color" => ["rgb" => "436FF6"]]];
}
}
}
if (Timer::time() < $startT + $secondEnd) {
$lastResult = "-";
$lastTimestamp = 0;
} else {
$lastResult = Doo::translate("正常");
if (empty($lastTimestamp) || $lastTimestamp === $firstTimestamp) {
$lastResult = Doo::translate("缺卡");
$styles["G{$index}"] = ["font" => ["color" => ["rgb" => "ff0000"]]];
} elseif ($lastTimestamp < $startT + $secondEnd) {
$lastResult = Doo::translate("早退");
$styles["G{$index}"] = ["font" => ["color" => ["rgb" => "436FF6"]]];
if (Timer::time() < $startT + $secondEnd) {
$lastResult = "-";
$lastTimestamp = 0;
} else {
$lastResult = Doo::translate("正常");
if (empty($lastTimestamp) || $lastTimestamp === $firstTimestamp) {
$lastResult = Doo::translate("缺卡");
$styles["G{$index}"] = ["font" => ["color" => ["rgb" => "ff0000"]]];
} elseif ($lastTimestamp < $startT + $secondEnd) {
$lastResult = Doo::translate("早退");
$styles["G{$index}"] = ["font" => ["color" => ["rgb" => "436FF6"]]];
}
}
$firstTimestamp = $firstTimestamp ? date("H:i", $firstTimestamp) : "-";
$lastTimestamp = $lastTimestamp ? date("H:i", $lastTimestamp) : "-";
$section = array_map(function($item) {
return $item[0] . "-" . ($item[1] ?: "None");
}, UserCheckinRecord::atSection($sameTimes));
$datas[] = [
"{$nickname} (ID: {$user->userid})",
$sameDate,
implode("-", $time),
$firstTimestamp,
$firstResult,
$lastTimestamp,
$lastResult,
implode(", ", $section),
];
$startT += 86400;
}
$firstTimestamp = $firstTimestamp ? date("H:i", $firstTimestamp) : "-";
$lastTimestamp = $lastTimestamp ? date("H:i", $lastTimestamp) : "-";
$section = array_map(function($item) {
return $item[0] . "-" . ($item[1] ?: "None");
}, UserCheckinRecord::atSection($sameTimes));
$datas[] = [
"{$nickname} (ID: {$user->userid})",
$sameDate,
implode("-", $time),
$firstTimestamp,
$firstResult,
$lastTimestamp,
$lastResult,
implode(", ", $section),
];
$startT += 86400;
$title = (count($sheets) + 1) . "." . ($nickname ?: $user->userid);
$sheets[] = BillExport::create()->setTitle($title)->setHeadings($headings)->setData($datas)->setStyles($styles);
}
$title = (count($sheets) + 1) . "." . ($nickname ?: $user->userid);
$sheets[] = BillExport::create()->setTitle($title)->setHeadings($headings)->setData($datas)->setStyles($styles);
}
if (empty($sheets)) {
return Base::retError('没有任何数据');
}
if (empty($sheets)) {
$content[] = [
'content' => '没有任何数据',
'style' => 'color: #ff0000;',
];
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'title' => $content[0]['content'],
'content' => $content,
], $botUser->userid, true, false, true);
return;
}
//
$fileName = $users[0]->nickname;
if (count($users) > 1) {
$fileName .= "" . count($userid) . "位成员的签到记录";
} else {
$fileName .= '的签到记录';
}
$fileName = Doo::translate($fileName) . '_' . Timer::time() . '.xlsx';
$filePath = "temp/checkin/export/" . date("Ym", Timer::time());
$export = new BillMultipleExport($sheets);
$res = $export->store($filePath . "/" . $fileName);
if ($res != 1) {
$content[] = [
'content' => "导出失败,{$fileName}",
'style' => 'color: #ff0000;',
];
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'title' => $content[0]['content'],
'content' => $content,
], $botUser->userid, true, false, true);
return;
}
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xlsx') . ".zip";
$zipPath = storage_path($zipFile);
if (file_exists($zipPath)) {
Base::deleteDirAndFile($zipPath, true);
}
try {
Madzipper::make($zipPath)->add($xlsPath)->close();
} catch (\Throwable) {
}
//
if (file_exists($zipPath)) {
$base64 = base64_encode(Base::array2string([
'file' => $zipFile,
]));
$fileUrl = Base::fillUrl('api/system/checkin/down?key=' . urlencode($base64));
Session::put('checkin::export:userid', $user->userid);
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'file_download',
'title' => '导出签到数据已完成',
'name' => $fileName,
'size' => filesize($zipPath),
'url' => $fileUrl,
], $botUser->userid, true, false, true);
} else {
$content[] = [
'content' => "打包失败,请稍后再试...",
'style' => 'color: #ff0000;',
];
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'title' => $content[0]['content'],
'content' => $content,
], $botUser->userid, true, false, true);
}
});
//
$fileName = $users[0]->nickname;
if (count($users) > 1) {
$fileName .= "" . count($userid) . "位成员的签到记录";
} else {
$fileName .= '的签到记录';
}
$fileName = Doo::translate($fileName) . '_' . Timer::time() . '.xlsx';
$filePath = "temp/checkin/export/" . date("Ym", Timer::time());
$export = new BillMultipleExport($sheets);
$res = $export->store($filePath . "/" . $fileName);
if ($res != 1) {
return Base::retError('导出失败,' . $fileName . '');
}
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xlsx') . ".zip";
$zipPath = storage_path($zipFile);
if (file_exists($zipPath)) {
Base::deleteDirAndFile($zipPath, true);
}
try {
Madzipper::make($zipPath)->add($xlsPath)->close();
} catch (\Throwable) {
}
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
'type' => 'content',
'content' => '正在导出签到数据,请稍等...',
], $botUser->userid, true, false, true);
//
if (file_exists($zipPath)) {
$base64 = base64_encode(Base::array2string([
'file' => $zipFile,
]));
Session::put('checkin::export:userid', $user->userid);
return Base::retSuccess('success', [
'size' => Base::twoFloat(filesize($zipPath) / 1024, true),
'url' => Base::fillUrl('api/system/checkin/down?key=' . urlencode($base64)),
]);
} else {
return Base::retError('打包失败,请稍后再试...');
}
return Base::retSuccess('success');
}
/**
@@ -1522,11 +1585,13 @@ class SystemController extends AbstractController
}
// 添加office资源
$officePath = '';
$officeApi = 'http://office/web-apps/apps/api/documents/api.js';
$content = @file_get_contents($officeApi);
if ($content) {
if (preg_match("/const\s+ver\s*=\s*'\/*([^']+)'/", $content, $matches)) {
$officePath = $matches[1];
if (Apps::isInstalled('office')) {
$officeApi = 'http://office/web-apps/apps/api/documents/api.js';
$content = @file_get_contents($officeApi);
if ($content) {
if (preg_match("/const\s+ver\s*=\s*'\/*([^']+)'/", $content, $matches)) {
$officePath = $matches[1];
}
}
}
if ($officePath) {
@@ -1542,14 +1607,16 @@ class SystemController extends AbstractController
});
}
// 添加OKR资源
$okrContent = @file_get_contents("http://nginx/apps/okr/");
preg_match_all('/<script[^>]*src=["\']([^"\']+)["\'][^>]*>/i', $okrContent, $scriptMatches);
foreach ($scriptMatches[1] as $src) {
$array[] = $src;
}
preg_match_all('/<link[^>]*rel=["\']stylesheet["\'][^>]*href=["\']([^"\']+)["\'][^>]*>/i', $okrContent, $linkMatches);
foreach ($linkMatches[1] as $href) {
$array[] = $href;
if (Apps::isInstalled('okr')) {
$okrContent = @file_get_contents("http://nginx/apps/okr/");
preg_match_all('/<script[^>]*src=["\']([^"\']+)["\'][^>]*>/i', $okrContent, $scriptMatches);
foreach ($scriptMatches[1] as $src) {
$array[] = $src;
}
preg_match_all('/<link[^>]*rel=["\']stylesheet["\'][^>]*href=["\']([^"\']+)["\'][^>]*>/i', $okrContent, $linkMatches);
foreach ($linkMatches[1] as $href) {
$array[] = $href;
}
}
}

View File

@@ -155,7 +155,7 @@ class UsersController extends AbstractController
//
if (!Project::withTrashed()->whereUserid($user->userid)->wherePersonal(1)->exists()) {
Project::createProject([
'name' => Doo::translate('个人项目'),
'name' => "📝 " . Doo::translate('个人项目'),
'desc' => Doo::translate('注册时系统自动创建项目,你可以自由删除。'),
'personal' => 1,
], $user->userid);
@@ -942,8 +942,7 @@ class UsersController extends AbstractController
return UserCheckinMac::saveMac($userInfo->userid, $array);
case 'checkin_face':
$faceimg = $data['checkin_face'] ? $data['checkin_face'] : '';
$faceimg = $data['checkin_face'] ?: '';
return UserCheckinFace::saveFace($userInfo->userid, $userInfo->nickname, $faceimg, "管理员上传");
case 'department':

View File

@@ -8,8 +8,6 @@ use Request;
use Redirect;
use Response;
use App\Models\File;
use App\Models\User;
use App\Models\UserTransfer;
use App\Module\Doo;
use App\Module\Base;
use App\Module\Extranet;
@@ -27,7 +25,6 @@ use App\Tasks\ZincSearchSyncTask;
use App\Tasks\UnclaimedTaskRemindTask;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Laravolt\Avatar\Avatar;
use Swoole\Coroutine;
/**
@@ -86,6 +83,15 @@ class IndexController extends InvokeController
return Redirect::to(Base::fillUrl('api/system/version'), 301);
}
/**
* 健康检查
* @return string
*/
public function health()
{
return "ok";
}
/**
* 头像
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response|\Symfony\Component\HttpFoundation\BinaryFileResponse
@@ -502,43 +508,4 @@ class IndexController extends InvokeController
$redirectUrl = Base::fillUrl("fileview/onlinePreview?url=" . urlencode(base64_encode($url)));
return Redirect::to($redirectUrl, 301);
}
/**
* 修复操作离职后续操作(todo 临时,后期删除)
* @return array
*/
public function migration__userdialog()
{
if (Request::header('app-key') !== env('APP_KEY')) {
return Base::retError("key error");
}
go(function() {
Coroutine::sleep(3);
$handled = [];
UserTransfer::orderBy('id')->chunkById(10, function ($transfers) use ($handled) {
/** @var UserTransfer $transfer */
foreach ($transfers as $transfer) {
if (in_array($transfer->original_userid, $handled)) {
continue;
}
$handled[] = $transfer->original_userid;
//
$user = User::find($transfer->original_userid);
if ($user?->isDisable()) {
$transfer->exitDialog();
}
}
});
});
return Base::retSuccess('success');
}
/**
* 保存配置 (todo 已废弃)
* @return string
*/
public function storage__synch()
{
return '<!-- Deprecated -->';
}
}

View File

@@ -151,6 +151,25 @@ class AbstractModel extends Model
return $date->format($this->dateFormat ?: 'Y-m-d H:i:s');
}
/**
* 通过模型创建实例
* @param array $param
* @param bool $force
* @return static
*/
public static function fillInstance(array $param = [], bool $force = true)
{
$instance = new static;
if ($param) {
if ($force) {
$instance->forceFill($param);
} else {
$instance->fill($param);
}
}
return $instance;
}
/**
* 创建/更新数据
* @param array $param
@@ -209,15 +228,23 @@ class AbstractModel extends Model
/**
* 数据库更新或插入
* @param $where
* @param array|\Closure $update 存在时更新的内容
* @param array|\Closure $insert 不存在时插入的内容,如果没有则插入更新内容
* @param bool $isInsert 是否是插入数据
* @param array $where 查询条件
* @param array|\Closure $update 存在时更新的内容
* @param array|\Closure $insert 不存在时插入的内容,如果没有则插入更新内容
* @param bool $isInsert 是否是插入数据
* @param bool|null $lockForUpdate 是否加锁true:加锁false:不加锁null:在事务中会自动加锁)
* @return AbstractModel|\Illuminate\Database\Eloquent\Builder|Model|object|static|null
*/
public static function updateInsert($where, $update = [], $insert = [], &$isInsert = true)
public static function updateInsert($where, $update = [], $insert = [], &$isInsert = true, $lockForUpdate = null)
{
$row = static::where($where)->first();
$query = static::where($where);
if ($lockForUpdate === null) {
$lockForUpdate = \DB::transactionLevel() > 0;
}
if ($lockForUpdate) {
$query->lockForUpdate();
}
$row = $query->first();
if (empty($row)) {
$row = new static;
if ($insert instanceof \Closure) {

View File

@@ -3,6 +3,7 @@
namespace App\Models;
use Request;
use App\Module\Apps;
use App\Module\Base;
use App\Tasks\PushTask;
use App\Exceptions\ApiException;
@@ -978,4 +979,41 @@ class File extends AbstractModel
];
Task::deliver(new PushTask($params));
}
/**
* 根据文件类型判断是否需要安装应用
* @param $type
* @return void
*/
public static function isNeedInstallApp($type): void
{
// 文件类型与应用的映射配置
$fileTypeAppMapping = [
// Office 应用映射
[
'types' => ['word', 'excel', 'ppt', 'docx', 'xlsx', 'pptx'],
'app_id' => 'office',
'app_name' => 'OnlyOffice'
],
// Drawio 应用映射
[
'types' => ['drawio'],
'app_id' => 'drawio',
'app_name' => 'Drawio'
],
// Minder 应用映射
[
'types' => ['mind'],
'app_id' => 'minder',
'app_name' => 'Minder'
]
];
// 遍历配置检查是否需要安装应用
foreach ($fileTypeAppMapping as $config) {
if (in_array($type, $config['types'])) {
Apps::isInstalledThrow($config['app_id']);
}
}
}
}

View File

@@ -1144,9 +1144,6 @@ class ProjectTask extends AbstractModel
*/
public function copyTask()
{
if ($this->parent_id > 0) {
throw new ApiException('子任务禁止复制');
}
return AbstractModel::transaction(function() {
// 复制任务
$task = $this->replicate();

View File

@@ -124,50 +124,49 @@ class Setting extends AbstractModel
{
return match ($ai) {
'openai' => [
'gpt-4 | GPT-4',
'gpt-4-turbo | GPT-4 Turbo',
'gpt-4.1 | GPT-4.1',
'gpt-4o | GPT-4o',
'gpt-4 | GPT-4',
'gpt-4o-mini | GPT-4o Mini',
'gpt-4-turbo | GPT-4 Turbo',
'o3 (thinking) | GPT-o3',
'o1 | GPT-o1',
'o1-mini | GPT-o1 Mini',
'o4-mini | GPT-o4 Mini',
'o3-mini | GPT-o3 Mini',
'o1-mini | GPT-o1 Mini',
'gpt-3.5-turbo | GPT-3.5 Turbo',
'gpt-3.5-turbo-16k | GPT-3.5 Turbo 16K',
'gpt-3.5-turbo-0125 | GPT-3.5 Turbo 0125',
'gpt-3.5-turbo-1106 | GPT-3.5 Turbo 1106'
],
'claude' => [
'claude-3-5-sonnet-latest | Claude 3.5 Sonnet',
'claude-3-5-sonnet-20241022 | Claude 3.5 Sonnet 20241022',
'claude-3-5-haiku-latest | Claude 3.5 Haiku',
'claude-3-5-haiku-20241022 | Claude 3.5 Haiku 20241022',
'claude-3-opus-latest | Claude 3 Opus',
'claude-3-opus-20240229 | Claude 3 Opus 20240229',
'claude-3-haiku-20240307 | Claude 3 Haiku 20240307',
'claude-2.1 | Claude 2.1',
'claude-2.0 | Claude 2.0'
'claude-opus-4-0 (thinking) | Claude Opus 4',
'claude-sonnet-4-0 (thinking) | Claude Sonnet 4',
'claude-3-7-sonnet-latest (thinking) | Claude Sonnet 3.7',
'claude-3-5-sonnet-latest | Claude Sonnet 3.5',
'claude-3-5-haiku-latest | Claude Haiku 3.5',
'claude-3-opus-latest | Claude Opus 3'
],
'deepseek' => [
'deepseek-chat | DeepSeek V3',
'deepseek-reasoner | DeepSeek R1'
],
'gemini' => [
'gemini-2.5-pro-preview-05-06 (thinking) | Gemini 2.5 Pro Preview',
'gemini-2.0-flash | Gemini 2.0 Flash',
'gemini-2.0-flash-lite-preview-02-05 | Gemini 2.0 Flash-Lite Preview',
'gemini-2.0-flash-lite | Gemini 2.0 Flash-Lite',
'gemini-1.5-flash | Gemini 1.5 Flash',
'gemini-1.5-flash-8b | Gemini 1.5 Flash 8B',
'gemini-1.5-pro | Gemini 1.5 Pro',
'gemini-1.0-pro | Gemini 1.0 Pro'
],
'grok' => [
'grok-2-vision-1212 | Grok 2 Vision 1212',
'grok-2-vision | Grok 2 Vision',
'grok-2-vision-latest | Grok 2 Vision Latest',
'grok-2-1212 | Grok 2 1212',
'grok-2 | Grok 2',
'grok-2-latest | Grok 2 Latest',
'grok-vision-beta | Grok Vision Beta',
'grok-beta | Grok Beta',
'grok-3-latest | Grok 3',
'grok-3-fast-latest | Grok 3 Fast',
'grok-3-mini-latest | Grok 3 Mini',
'grok-3-mini-fast-latest | Grok 3 Mini Fast',
'grok-2-vision-latest | Grok 2 Vision',
'grok-2-latest | Grok 2',
],
'zhipu' => [
'glm-4 | GLM-4',

View File

@@ -47,6 +47,54 @@ class UmengAlias extends AbstractModel
{
protected $table = 'umeng_alias';
private static $waitSend = [];
/**
* 推送消息
* @param $push
* @return void
*/
private static function sendTask($push = null)
{
if ($push) {
self::$waitSend[] = $push;
}
if (!self::$waitSend) {
return;
}
$first = array_shift(self::$waitSend);
if (empty($first)) {
return;
}
try {
switch ($first['platform']) {
case 'ios':
$instance = new IOS($first['config']);
break;
case 'android':
$instance = new Android($first['config']);
break;
default:
return;
}
$instance->send($first['data']);
} catch (\Exception $e) {
$first['retry'] = intval($first['retry'] ?? 0) + 1;
if ($first['retry'] > 3) {
info("[PushMsg] fail: " . $e->getMessage());
} else {
info("[PushMsg] retry ({$first['retry']}): " . $e->getMessage());
self::$waitSend[] = $first;
}
} finally {
self::sendTask();
}
}
/**
* 推送内容处理
* @param $string
@@ -90,13 +138,13 @@ class UmengAlias extends AbstractModel
* @param string $alias
* @param string $platform
* @param array $array [title, subtitle, body, description, extra, seconds, badge]
* @return array|false
* @return void
*/
public static function pushMsgToAlias($alias, $platform, $array)
private static function pushMsgToAlias($alias, $platform, $array)
{
$config = self::getPushConfig();
if ($config === false) {
return false;
return;
}
//
$title = self::specialCharacters($array['title'] ?: ''); // 标题
@@ -110,65 +158,71 @@ class UmengAlias extends AbstractModel
switch ($platform) {
case 'ios':
if (!isset($config['iOS'])) {
return false;
return;
}
$ios = new IOS($config);
return $ios->send([
'description' => $description,
'payload' => array_merge([
'aps' => [
'alert' => [
'title' => $title,
'subtitle' => $subtitle,
'body' => $body,
self::sendTask([
'platform' => $platform,
'config' => $config,
'data' => [
'description' => $description,
'payload' => array_merge([
'aps' => [
'alert' => [
'title' => $title,
'subtitle' => $subtitle,
'body' => $body,
],
'sound' => 'default',
'badge' => $badge,
],
'sound' => 'default',
'badge' => $badge,
], $extra),
'type' => 'customizedcast',
'alias_type' => 'userid',
'alias' => $alias,
'policy' => [
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
],
], $extra),
'type' => 'customizedcast',
'alias_type' => 'userid',
'alias' => $alias,
'policy' => [
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
],
]
]);
break;
case 'android':
if (!isset($config['Android'])) {
return false;
return;
}
$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,
self::sendTask([
'platform' => $platform,
'config' => $config,
'data' => [
'description' => $description,
'payload' => array_merge([
'display_type' => 'notification',
'body' => [
'ticker' => $title,
'text' => $body,
'title' => $title,
'after_open' => 'go_app',
'play_sound' => true,
],
], $extra),
'type' => 'customizedcast',
'alias_type' => 'userid',
'alias' => $alias,
'mipush' => true,
'mi_activity' => 'app.eeui.umeng.activity.MfrMessageActivity',
'policy' => [
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
],
], $extra),
'type' => 'customizedcast',
'alias_type' => 'userid',
'alias' => $alias,
'mipush' => true,
'mi_activity' => 'app.eeui.umeng.activity.MfrMessageActivity',
'policy' => [
'expire_time' => Carbon::now()->addSeconds($seconds)->toDateTimeString(),
],
'channel_properties' => [
'vivo_category' => 'IM',
'huawei_channel_importance' => 'NORMAL',
'huawei_channel_category' => 'IM',
'channel_fcm' => 0,
],
'channel_properties' => [
'oppo_channel_id' => 'dootask',
'vivo_category' => 'IM',
'huawei_channel_importance' => 'NORMAL',
'huawei_channel_category' => 'IM',
'channel_fcm' => 0,
],
]
]);
default:
return false;
break;
}
}

View File

@@ -10,6 +10,7 @@ use App\Module\Table\OnlineData;
use App\Services\RequestContext;
use Cache;
use Carbon\Carbon;
use Request;
/**
* App\Models\User
@@ -435,6 +436,19 @@ class User extends AbstractModel
return $user->nickname;
}
/**
* 临时日志记录
* @param $message
* @return void
*/
private static function tmpLog($message)
{
if (Request::input('log') !== 'yes') {
return;
}
info("[User] [" . date("Y-m-d H:i:s") . "] " . $message);
}
/**
* 用户身份认证(获取用户信息)
* @param null $identity 判断身份
@@ -444,11 +458,20 @@ class User extends AbstractModel
{
$user = self::authInfo();
if (!$user) {
self::tmpLog('auth failed');
$token = Base::token();
if ($token) {
UserDevice::forget($token);
self::tmpLog('auth token found: ' . Base::array2json([
'header' => Request::header(),
'input' => Request::input(),
]));
throw new ApiException('身份已失效,请重新登录', [], -1);
} else {
self::tmpLog('auth no token found: ' . Base::array2json([
'header' => Request::header(),
'input' => Request::input(),
]));
throw new ApiException('请登录后继续...', [], -1);
}
}
@@ -467,25 +490,31 @@ class User extends AbstractModel
*/
private static function authInfo()
{
self::tmpLog('auth start');
if (RequestContext::has('auth')) {
// 缓存
self::tmpLog('auth from cache');
return RequestContext::get('auth');
}
if (Doo::userId() <= 0) {
// 没有登录
self::tmpLog('auth no login');
return RequestContext::save('auth', false);
}
if (Doo::userExpired()) {
// 登录过期
self::tmpLog('auth expired');
return RequestContext::save('auth', false);
}
if (!UserDevice::check()) {
// token 不存在
self::tmpLog('auth token not found');
return RequestContext::save('auth', false);
}
$user = self::whereUserid(Doo::userId())->whereEmail(Doo::userEmail())->whereEncrypt(Doo::userEncrypt())->first();
if (!$user) {
// 登录信息不匹配
self::tmpLog('auth user not found');
return RequestContext::save('auth', false);
}
@@ -507,6 +536,7 @@ class User extends AbstractModel
$user->updateInstance($upArray);
$user->save();
}
self::tmpLog('auth success: ' . $user->userid . ' - ' . $user->email);
return RequestContext::save('auth', $user);
}

View File

@@ -3,6 +3,7 @@
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Apps;
use App\Module\Base;
use App\Module\Ihttp;
@@ -37,8 +38,9 @@ use App\Module\Ihttp;
class UserCheckinFace extends AbstractModel
{
public static function saveFace($userid, $nickname, $faceimg, $remark='')
public static function saveFace($userid, $nickname, $faceimg, $remark = '')
{
Apps::isInstalledThrow('face');
// 取上传图片的URL
$faceimg = Base::unFillUrl($faceimg);
$record = "";
@@ -59,14 +61,14 @@ class UserCheckinFace extends AbstractModel
}
$res = Ihttp::ihttp_post($url, json_encode($data), 15);
if($res['data'] && $data = json_decode($res['data'])){
if($data->ret != 1 && $data->msg){
if ($res['data'] && $data = json_decode($res['data'])) {
if ($data->ret != 1 && $data->msg) {
throw new ApiException($data->msg);
}
}
return AbstractModel::transaction(function() use ($userid, $faceimg, $remark) {
return AbstractModel::transaction(function () use ($userid, $faceimg, $remark) {
$checkinFace = self::query()->whereUserid($userid)->first();
if ($checkinFace) {
self::updateData(['id' => $checkinFace->id], [
@@ -82,16 +84,14 @@ class UserCheckinFace extends AbstractModel
$checkinFace->save();
}
if ($faceimg == '') {
$res = UserCheckinFace::deleteDeviceUser($userid);
if ($res) {
return $res;
}
UserCheckinFace::deleteDeviceUser($userid);
}
return Base::retSuccess('设置成功');
});
}
public static function deleteDeviceUser($userid) {
private static function deleteDeviceUser($userid)
{
$url = "http://face:7788/user/delete";
$data = [
'enrollid' => $userid,
@@ -99,10 +99,9 @@ class UserCheckinFace extends AbstractModel
];
$res = Ihttp::ihttp_post($url, json_encode($data));
if($res['data'] && $data = json_decode($res['data'])){
if($data->ret != 1 && $data->msg){
if ($res['data'] && $data = json_decode($res['data'])) {
if ($data->ret != 1 && $data->msg) {
throw new ApiException($data->msg);
// return Base::retError($data->msg);
}
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use App\Module\Base;
use App\Module\Doo;
use App\Module\Lock;
use Cache;
use Carbon\Carbon;
use DeviceDetector\DeviceDetector;
@@ -218,13 +219,8 @@ class UserDevice extends AbstractModel
self::record();
return $hash;
}
// 没有记录,尝试创建一个(防止升级后所有登录都失效,保证留一个可以保持登录) // todo 后期删除
return AbstractModel::transaction(function () use ($hash, $userid) {
if (self::whereUserid($userid)->withoutTrashed()->lockForUpdate()->exists()) {
return null;
}
return self::record() ? $hash : null;
});
// 没有记录
return null;
}
/**
@@ -234,46 +230,48 @@ class UserDevice extends AbstractModel
*/
public static function record(string $token = null): ?self
{
return AbstractModel::transaction(function () use ($token) {
if (empty($token)) {
$token = Doo::userToken();
$userid = Doo::userId();
$expiredAt = Doo::userExpiredAt();
} else {
$info = Doo::tokenDecode($token);
$userid = $info['userid'] ?? 0;
$expiredAt = $info['expired_at'];
}
$hash = md5($token);
$row = self::whereHash($hash)->lockForUpdate()->first();
if (empty($row)) {
// 生成一个新的设备记录
$row = self::createInstance([
'userid' => $userid,
'hash' => $hash,
]);
if (!$row->save()) {
return null;
}
// 删除多余的设备记录
$currentDeviceCount = self::whereUserid($userid)->count();
if ($currentDeviceCount > self::$deviceLimit) {
$rows = self::whereUserid($userid)->orderBy('id')->take($currentDeviceCount - self::$deviceLimit)->get();
foreach ($rows as $row) {
UserDevice::forget($row);
if (empty($token)) {
$token = Doo::userToken();
$userid = Doo::userId();
$expiredAt = Doo::userExpiredAt();
} else {
$info = Doo::tokenDecode($token);
$userid = $info['userid'] ?? 0;
$expiredAt = $info['expired_at'];
}
$hash = md5($token);
//
return Lock::withLock("userDeviceRecord:{$hash}", function () use ($expiredAt, $userid, $hash, $token) {
return AbstractModel::transaction(function () use ($expiredAt, $userid, $hash, $token) {
$row = self::whereHash($hash)->first();
if (empty($row)) {
// 生成一个新的设备记录
$row = self::createInstance([
'userid' => $userid,
'hash' => $hash,
]);
if (!$row->save()) {
return null;
}
// 删除多余的设备记录
$currentDeviceCount = self::whereUserid($userid)->count();
if ($currentDeviceCount > self::$deviceLimit) {
$rows = self::whereUserid($userid)->orderBy('id')->take($currentDeviceCount - self::$deviceLimit)->get();
foreach ($rows as $row) {
UserDevice::forget($row);
}
}
}
}
$row->expired_at = $expiredAt;
if (Request::hasHeader('version')) {
$deviceInfo = array_merge(Base::json2array($row->detail), self::getDeviceInfo($_SERVER['HTTP_USER_AGENT'] ?? ''));
$row->detail = Base::array2json($deviceInfo);
}
$row->save();
$row->expired_at = $expiredAt;
if (Request::hasHeader('version')) {
$deviceInfo = array_merge(Base::json2array($row->detail), self::getDeviceInfo($_SERVER['HTTP_USER_AGENT'] ?? ''));
$row->detail = Base::array2json($deviceInfo);
}
$row->save();
Cache::put(self::ck($hash), $row->userid, now()->addHour());
return $row;
Cache::put(self::ck($hash), $row->userid, now()->addHour());
return $row;
});
});
}

View File

@@ -891,7 +891,7 @@ class WebSocketDialog extends AbstractModel
$data = [];
foreach ($dialogIds as $dialog_id) {
$dialog = WebSocketDialog::checkDialog($dialog_id);
//
$action = $replyId > 0 ? "reply-$replyId" : "";
$path = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
if ($image64) {
@@ -916,40 +916,41 @@ class WebSocketDialog extends AbstractModel
"compressVideo" => $setting['compress_video'] === 'open',
]);
}
//
if (Base::isError($data)) {
throw new ApiException($data['msg']);
} else {
$fileData = $data['data'];
$filePath = $fileData['file'];
$fileName = $fileData['name'];
$fileData['thumb'] = Base::unFillUrl($fileData['thumb']);
$fileData['size'] *= 1024;
//
if ($dialog->type === 'group' && $dialog->group_type === 'task') { // 任务群组保存文件
if ($imageAttachment || !in_array($fileData['ext'], File::imageExt)) { // 如果是图片不保存
$task = ProjectTask::whereDialogId($dialog->id)->first();
if ($task) {
$file = ProjectTaskFile::createInstance([
'project_id' => $task->project_id,
'task_id' => $task->id,
'name' => $fileData['name'],
'size' => $fileData['size'],
'ext' => $fileData['ext'],
'path' => $fileData['path'],
'thumb' => $fileData['thumb'],
'userid' => $user->userid,
]);
$file->save();
}
}
$fileData = $data['data'];
$filePath = $fileData['file'];
$fileName = $fileData['name'];
$fileData['thumb'] = Base::unFillUrl($fileData['thumb']);
$fileData['size'] *= 1024;
// 任务群组保存文件
if ($dialog->group_type === 'task') {
// 如果是图片不保存
if ($imageAttachment || !in_array($fileData['ext'], File::imageExt)) {
$task = ProjectTask::whereDialogId($dialog->id)->first();
if ($task) {
$file = ProjectTaskFile::createInstance([
'project_id' => $task->project_id,
'task_id' => $task->id,
'name' => $fileData['name'],
'size' => $fileData['size'],
'ext' => $fileData['ext'],
'path' => $fileData['path'],
'thumb' => $fileData['thumb'],
'userid' => $user->userid,
]);
$file->save();
}
}
//
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'file', $fileData, $user->userid);
if (Base::isSuccess($result)) {
if (isset($task)) {
$result['data']['task_id'] = $task->id;
}
}
// 发送消息
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'file', $fileData, $user->userid);
if (Base::isSuccess($result)) {
if (isset($task)) {
$result['data']['task_id'] = $task->id;
}
}
}

View File

@@ -3,8 +3,8 @@
namespace App\Models;
use App\Module\Base;
use App\Module\Extranet;
use Swoole\Coroutine;
use App\Tasks\UpdateSessionTitleViaAiTask;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Cache;
/**
@@ -82,18 +82,6 @@ class WebSocketDialogSession extends AbstractModel
$session->title = $title;
$session->save();
Cache::forever($cacheKey, true);
// 通过AI接口更新对话标题
go(function () use ($session, $title, $originalTitle) {
Coroutine::sleep(0.1);
$res = Extranet::openAIGenerateTitle($originalTitle);
if (Base::isError($res)) {
return;
}
$newTitle = $res['data'];
if ($newTitle && $newTitle != $title) {
$session->title = Base::cutStr($newTitle, 100);
$session->save();
}
});
Task::deliver(new UpdateSessionTitleViaAiTask($session->id, $originalTitle));
}
}

60
app/Module/Apps.php Normal file
View File

@@ -0,0 +1,60 @@
<?php
namespace App\Module;
use App\Exceptions\ApiException;
use App\Services\RequestContext;
use Symfony\Component\Yaml\Yaml;
class Apps
{
/**
* 判断应用是否已安装
*
* @param string $appId 应用ID名称
* @return bool 如果应用已安装返回 true否则返回 false
*/
public static function isInstalled(string $appId): bool
{
if ($appId === 'appstore') {
return true;
}
$key = 'app_installed_' . $appId;
if (RequestContext::has($key)) {
return RequestContext::get($key);
}
$configFile = base_path('docker/appstore/config/' . $appId . '/config.yml');
$installed = false;
if (file_exists($configFile)) {
$configData = Yaml::parseFile($configFile);
$installed = $configData['status'] === 'installed';
}
return RequestContext::save($key, $installed);
}
/**
* 判断应用是否已安装,如果未安装则抛出异常
* @param string $appId
* @return void
*/
public static function isInstalledThrow(string $appId): void
{
if (!self::isInstalled($appId)) {
$name = match ($appId) {
'ai' => 'AI Robot',
'face' => 'Face check-in',
'appstore' => 'AppStore',
'approve' => 'Approval',
'office' => 'OnlyOffice',
'drawio' => 'Drawio',
'minder' => 'Minder',
'search' => 'ZincSearch',
default => $appId,
};
throw new ApiException("应用「{$name}」未安装", [], 0, false);
}
}
}

View File

@@ -351,7 +351,7 @@ class Base
/**
* 删除文件夹及文件夹下所有的文件
* @param $dirName
* @param bool $undeleteDir 不删除文件夹(只删除文件)
* @param bool $undeleteDir 不删除文件夹本身(只删除文件夹里面的内容
*/
public static function deleteDirAndFile($dirName, $undeleteDir = false)
{
@@ -827,12 +827,19 @@ class Base
}
return $str;
}
try {
$find = url('');
} catch (\Throwable) {
$find = self::getSchemeAndHost();
if (empty($str)) {
return $str;
}
return Base::leftDelete($str, $find . '/');
$parsedUrl = parse_url($str);
if (isset($parsedUrl['scheme']) && isset($parsedUrl['host'])) {
$relativePath = $parsedUrl['path'] ?? '';
$relativePath = ltrim($relativePath, '/');
$absolutePath = public_path($relativePath);
if (file_exists($absolutePath) || file_exists(Base::thumbRestore($absolutePath))) {
return $relativePath;
}
}
return $str;
}
/**
@@ -2574,22 +2581,37 @@ class Base
/**
* 中文转拼音
* @param $str
* @param $delim
* @return string
*/
public static function cn2pinyin($str)
public static function cn2pinyin($str, $delim = '')
{
if (empty($str)) {
return '';
}
if (!preg_match("/^[a-zA-Z0-9_.]+$/", $str)) {
$str = Cache::rememberForever("cn2pinyin:" . md5($str), function() use ($str) {
$str = Cache::rememberForever("cn2pinyin:" . md5($str . '_' . $delim), function () use ($delim, $str) {
$pinyin = new Pinyin();
return $pinyin->permalink($str, '');
return $pinyin->permalink($str, $delim);
});
}
return $str;
}
/**
* 驼峰转下划线
* @param $str
* @return string
*/
public static function camel2snake($str)
{
if (empty($str)) {
return '';
}
$str = preg_replace('/([a-z])([A-Z])/', '$1_$2', $str);
return strtolower($str);
}
/**
* 缓存数据
* @param $name

View File

@@ -47,6 +47,7 @@ class Doo
char* md5s(char* text, char* password);
char* macs();
char* dooSN();
char* version();
char* pgpGenerateKeyPair(char* name, char* email, char* passphrase);
char* pgpEncrypt(char* plainText, char* publicKey);
char* pgpDecrypt(char* cipherText, char* privateKey, char* passphrase);
@@ -361,6 +362,15 @@ class Doo
return self::string(self::doo()->dooSN());
}
/**
* 获取当前版本
* @return string
*/
public static function dooVersion(): string
{
return self::string(self::doo()->version());
}
/**
* 生成PGP密钥对
* @param $name

52
app/Module/Lock.php Normal file
View File

@@ -0,0 +1,52 @@
<?php
namespace App\Module;
use Closure;
use Exception;
use Illuminate\Support\Facades\Redis;
class Lock
{
/**
* 使用Redis分布式锁执行闭包
* @param string $key 锁的key
* @param Closure $closure 要执行的闭包函数
* @param int $ttl 锁的过期时间毫秒默认1000010秒
* @param int $waitTimeout 等待锁的超时时间毫秒0表示不等待默认1000010秒
* @return mixed 闭包函数的返回值
* @throws Exception 如果获取锁失败或闭包执行异常
*/
public static function withLock(string $key, Closure $closure, int $ttl = 10000, int $waitTimeout = 10000)
{
$lockKey = "lock:{$key}";
$lockValue = uniqid('', true); // 生成唯一值,用于安全释放锁
// 尝试获取锁如果waitTimeout为0则直接返回false否则等待指定时间
$acquired = false;
if ($waitTimeout > 0) {
$end = microtime(true) + ($waitTimeout / 1000);
while (microtime(true) < $end) {
if (Redis::set($lockKey, $lockValue, 'PX', $ttl, 'NX')) {
$acquired = true;
break;
}
usleep(100000); // 休眠100ms后重试
}
} else {
$acquired = Redis::set($lockKey, $lockValue, 'PX', $ttl, 'NX');
}
if (!$acquired) {
throw new Exception("Failed to acquire lock for key: {$key}");
}
try {
// 执行闭包
return $closure();
} finally {
// 安全释放锁(仅当锁值未变时删除)
Redis::eval("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", 1, $lockKey, $lockValue);
}
}
}

View File

@@ -2,6 +2,9 @@
namespace App\Module\ZincSearch;
use App\Module\Apps;
use App\Module\Doo;
/**
* ZincSearch 公共类
*/
@@ -28,6 +31,13 @@ class ZincSearchBase
*/
private function request($path, $body = null, $method = 'POST')
{
if (!Apps::isInstalled("search")) {
return [
'success' => false,
'error' => Doo::translate("应用「ZincSearch」未安装")
];
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://{$this->host}:{$this->port}{$path}");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
@@ -38,7 +48,6 @@ class ZincSearchBase
$headers = ['Content-Type: application/json'];
if ($method === 'BULK') {
$headers = ['Content-Type: text/plain'];
$method = 'POST';
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

View File

@@ -4,9 +4,10 @@ namespace App\Module\ZincSearch;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogUser;
use App\Module\Apps;
use Carbon\Carbon;
use DB;
use Illuminate\Support\Facades\Log;
use Swoole\Coroutine;
/**
* ZincSearch 会话消息类
@@ -129,6 +130,11 @@ class ZincSearchDialogMsg
*/
public static function search(string $userid, string $keyword, int $from = 0, int $size = 20): array
{
if (!Apps::isInstalled("search")) {
// 如果搜索功能未安装,使用数据库查询
return self::searchByMysql($userid, $keyword, $from, $size);
}
$searchParams = [
'query' => [
'bool' => [
@@ -144,7 +150,6 @@ class ZincSearchDialogMsg
['updated_at' => 'desc']
]
];
try {
$result = ZincSearchBase::elasticSearch(self::$indexNameMsg, $searchParams);
$hits = $result['data']['hits']['hits'] ?? [];
@@ -184,6 +189,48 @@ class ZincSearchDialogMsg
}
}
/**
* 根据用户ID和消息关键词搜索会话MySQL 版本主要用于未安装ZincSearch的情况
*
* @param string $userid 用户ID
* @param string $keyword 消息关键词
* @param int $from 起始位置
* @param int $size 返回结果数量
* @return array
*/
private static function searchByMysql(string $userid, string $keyword, int $from = 0, int $size = 20): array
{
$items = DB::table('web_socket_dialog_users as u')
->select(['d.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at', 'm.id as search_msg_id'])
->join('web_socket_dialogs as d', 'u.dialog_id', '=', 'd.id')
->join('web_socket_dialog_msgs as m', 'm.dialog_id', '=', 'd.id')
->where('u.userid', $userid)
->where('m.bot', 0)
->whereNull('d.deleted_at')
->where('m.key', 'like', "%{$keyword}%")
->orderByDesc('m.id')
->offset($from)
->limit($size)
->get()
->all();
$msgs = [];
foreach ($items as $item) {
$msgs[] = [
'id' => $item->id,
'search_msg_id' => $item->search_msg_id,
'user_at' => Carbon::parse($item->user_at)->format('Y-m-d H:i:s'),
'mark_unread' => $item->mark_unread,
'silence' => $item->silence,
'hide' => $item->hide,
'color' => $item->color,
'top_at' => Carbon::parse($item->top_at)->format('Y-m-d H:i:s'),
'last_at' => Carbon::parse($item->last_at)->format('Y-m-d H:i:s'),
];
}
return $msgs;
}
/**
* 根据对话用户ID搜索用户信息
* @param array $dialogUserids
@@ -228,7 +275,7 @@ class ZincSearchDialogMsg
}
// ==============================
// 基本方法
// 生成内容
// ==============================
/**
@@ -287,8 +334,12 @@ class ZincSearchDialogMsg
];
}
// ==============================
// 基本方法
// ==============================
/**
* 同步消息
* 同步消息(建议在异步进程中使用)
*
* @param WebSocketDialogMsg $dialogMsg
* @return bool
@@ -345,7 +396,7 @@ class ZincSearchDialogMsg
}
/**
* 批量同步消息
* 批量同步消息(建议在异步进程中使用)
*
* @param WebSocketDialogMsg[] $dialogMsgs
* @return int 成功同步的消息数
@@ -423,7 +474,7 @@ class ZincSearchDialogMsg
}
/**
* 同步用户
* 同步用户(建议在异步进程中使用)
* @param WebSocketDialogUser $dialogUser
* @return bool
*/
@@ -459,32 +510,28 @@ class ZincSearchDialogMsg
// 用户不存在,同步消息
if (empty($hits)) {
go(function () use ($dialogUser) {
Coroutine::sleep(0.1);
$lastId = 0; // 上次同步的最后ID
$batchSize = 500; // 每批处理的消息数量
$lastId = 0; // 上次同步的最后ID
$batchSize = 500; // 每批处理的消息数量
// 分批同步消息
do {
// 获取一批
$dialogMsgs = WebSocketDialogMsg::whereDialogId($dialogUser->dialog_id)
->where('id', '>', $lastId)
->orderBy('id')
->limit($batchSize)
->get();
// 分批同步消息
do {
// 获取一批
$dialogMsgs = WebSocketDialogMsg::whereDialogId($dialogUser->dialog_id)
->where('id', '>', $lastId)
->orderBy('id')
->limit($batchSize)
->get();
if ($dialogMsgs->isEmpty()) {
break;
}
if ($dialogMsgs->isEmpty()) {
break;
}
// 同步数据
ZincSearchDialogMsg::batchSync($dialogMsgs);
// 同步数据
ZincSearchDialogMsg::batchSync($dialogMsgs);
// 更新最后ID
$lastId = $dialogMsgs->last()->id;
} while (count($dialogMsgs) == $batchSize);
});
// 更新最后ID
$lastId = $dialogMsgs->last()->id;
} while (count($dialogMsgs) == $batchSize);
}
return true;
@@ -495,7 +542,7 @@ class ZincSearchDialogMsg
}
/**
* 删除
* 删除(建议在异步进程中使用)
*
* @param WebSocketDialogMsg|WebSocketDialogUser|int $data
* @return int

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Observers;
use Hhxsv5\LaravelS\Swoole\Task\Task;
class AbstractObserver
{
/**
* @param $task
* @return void
*/
public static function taskDeliver($task)
{
if (app()->bound('swoole')) {
Task::deliver($task);
}
}
}

View File

@@ -3,9 +3,9 @@
namespace App\Observers;
use App\Models\WebSocketDialogMsg;
use App\Module\ZincSearch\ZincSearchDialogMsg;
use App\Tasks\ZincSearchSyncTask;
class WebSocketDialogMsgObserver
class WebSocketDialogMsgObserver extends AbstractObserver
{
/**
* Handle the WebSocketDialogMsg "created" event.
@@ -15,7 +15,7 @@ class WebSocketDialogMsgObserver
*/
public function created(WebSocketDialogMsg $webSocketDialogMsg)
{
ZincSearchDialogMsg::sync($webSocketDialogMsg);
self::taskDeliver(new ZincSearchSyncTask('sync', $webSocketDialogMsg->toArray()));
}
/**
@@ -26,7 +26,7 @@ class WebSocketDialogMsgObserver
*/
public function updated(WebSocketDialogMsg $webSocketDialogMsg)
{
ZincSearchDialogMsg::sync($webSocketDialogMsg);
self::taskDeliver(new ZincSearchSyncTask('sync', $webSocketDialogMsg->toArray()));
}
/**
@@ -37,7 +37,7 @@ class WebSocketDialogMsgObserver
*/
public function deleted(WebSocketDialogMsg $webSocketDialogMsg)
{
ZincSearchDialogMsg::delete($webSocketDialogMsg);
self::taskDeliver(new ZincSearchSyncTask('delete', $webSocketDialogMsg->toArray()));
}
/**

View File

@@ -4,10 +4,10 @@ namespace App\Observers;
use App\Models\Deleted;
use App\Models\WebSocketDialogUser;
use App\Module\ZincSearch\ZincSearchDialogMsg;
use App\Tasks\ZincSearchSyncTask;
use Carbon\Carbon;
class WebSocketDialogUserObserver
class WebSocketDialogUserObserver extends AbstractObserver
{
/**
* Handle the WebSocketDialogUser "created" event.
@@ -30,7 +30,7 @@ class WebSocketDialogUserObserver
}
}
Deleted::forget('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
ZincSearchDialogMsg::userSync($webSocketDialogUser);
self::taskDeliver(new ZincSearchSyncTask('userSync', $webSocketDialogUser->toArray()));
}
/**
@@ -41,7 +41,7 @@ class WebSocketDialogUserObserver
*/
public function updated(WebSocketDialogUser $webSocketDialogUser)
{
ZincSearchDialogMsg::userSync($webSocketDialogUser);
self::taskDeliver(new ZincSearchSyncTask('userSync', $webSocketDialogUser->toArray()));
}
/**
@@ -53,7 +53,7 @@ class WebSocketDialogUserObserver
public function deleted(WebSocketDialogUser $webSocketDialogUser)
{
Deleted::record('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
ZincSearchDialogMsg::delete($webSocketDialogUser);
self::taskDeliver(new ZincSearchSyncTask('deleteUser', $webSocketDialogUser->toArray()));
}
/**

View File

@@ -1,6 +1,8 @@
<?php
namespace App\Tasks;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Models\TaskWorker;
use App\Module\Base;
use Carbon\Carbon;

View File

@@ -6,8 +6,6 @@ use App\Models\ProjectTask;
use App\Module\Base;
use Carbon\Carbon;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
class AppPushTask extends AbstractTask
{
public function __construct()

View File

@@ -1,8 +1,6 @@
<?php
namespace App\Tasks;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Models\ProjectTask;
use App\Module\Base;
use Carbon\Carbon;

View File

@@ -12,6 +12,7 @@ use App\Models\UserDepartment;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogConfig;
use App\Models\WebSocketDialogMsg;
use App\Module\Apps;
use App\Module\Base;
use App\Module\Doo;
use App\Module\Ihttp;
@@ -21,9 +22,6 @@ use Exception;
use League\HTMLToMarkdown\HtmlConverter;
use DB;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
/**
* 推送会话消息
* Class BotReceiveMsgTask
@@ -429,15 +427,28 @@ class BotReceiveMsgTask extends AbstractTask
if ($msg->msg['model_name']) {
$extras['model_name'] = $msg->msg['model_name'];
}
if (preg_match("/(.*?)(\s+|\s*[_-]\s*)(think|thinking|reasoning)\s*$/", $extras['model_name'], $match)) {
$extras['model_name'] = $match[1];
// 提取模型“思考”参数
$thinkPatterns = [
"/^(.+?)(\s+|\s*[_-]\s*)(think|thinking|reasoning)\s*$/",
"/^(.+?)\s*\(\s*(think|thinking|reasoning)\s*\)\s*$/"
];
$thinkMatch = [];
foreach ($thinkPatterns as $pattern) {
if (preg_match($pattern, $extras['model_name'], $thinkMatch)) {
break;
}
}
if ($thinkMatch && !empty($thinkMatch[1])) {
$extras['model_name'] = $thinkMatch[1];
$extras['max_tokens'] = 20000;
$extras['thinking'] = 4096;
$extras['temperature'] = 1.0;
}
// 设定会话ID
if ($dialog->session_id) {
$extras['context_key'] = 'session_' . $dialog->session_id;
}
// 设置文心一言的API密钥
if ($type === 'wenxin') {
$extras['api_key'] .= ':' . $setting['wenxin_secret'];
}
@@ -455,7 +466,9 @@ class BotReceiveMsgTask extends AbstractTask
if (in_array($this->client['platform'], ['win', 'mac', 'web']) && !Base::judgeClientVersion("0.41.11", $this->client['version'])) {
$errorContent = '当前客户端版本低所需版本≥v0.41.11)。';
}
if (!Apps::isInstalled('ai')) {
$errorContent = '应用「AI Robot」未安装';
}
if ($msg->reply_id > 0) {
$replyCommand = $this->extractReplyCommand($msg->reply_id, $botUser);
if (Base::isError($replyCommand)) {

View File

@@ -13,8 +13,6 @@ use App\Module\Timer;
use Cache;
use Carbon\Carbon;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
class CheckinRemindTask extends AbstractTask
{
public function __construct()

View File

@@ -9,8 +9,6 @@ use Carbon\Carbon;
use App\Models\WebSocketDialogMsg;
use Illuminate\Support\Facades\Cache;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
class CloseMeetingRoomTask extends AbstractTask
{
public function __construct()

View File

@@ -2,8 +2,6 @@
namespace App\Tasks;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Models\UserBot;
use App\Models\WebSocketDialogMsg;
use Carbon\Carbon;

View File

@@ -1,8 +1,6 @@
<?php
namespace App\Tasks;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Models\Setting;
use App\Models\User;
use App\Models\WebSocketDialogMsg;

View File

@@ -7,9 +7,6 @@ use App\Module\Extranet;
use Cache;
use Carbon\Carbon;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
/**
* 获取笑话、心灵鸡汤
*

View File

@@ -4,9 +4,6 @@ namespace App\Tasks;
use App\Models\WebSocket;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
/**
* 上线、离线通知
* Class LineTask

View File

@@ -8,9 +8,6 @@ use App\Models\ProjectTask;
use App\Models\ProjectTaskUser;
use Carbon\Carbon;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
/**
* 任务重复周期
*/
@@ -29,6 +26,10 @@ class LoopTask extends AbstractTask
])->chunkById(100, function ($list) {
/** @var ProjectTask $item */
foreach ($list as $item) {
if ($item->parent_id > 0) {
// 如果是子任务则不处理
continue;
}
try {
$task = $item->copyTask();
// 工作流
@@ -63,6 +64,18 @@ class LoopTask extends AbstractTask
$task->start_at = Carbon::parse($task->loop_at);
$task->end_at = $task->start_at->clone()->addSeconds($diffSecond);
}
// 处理子任务
$subTasks = ProjectTask::whereParentId($item->id)->get();
if (!$subTasks->isEmpty()) {
foreach ($subTasks as $subTask) {
$newSubTask = $subTask->copyTask();
$newSubTask->parent_id = $task->id;
$newSubTask->start_at = $task->start_at;
$newSubTask->end_at = $task->end_at;
$newSubTask->save();
}
}
//
$task->refreshLoop(true);
$task->addLog("创建任务来自周期任务ID{$item->id}", [], $task->userid);
// 清空旧周期

View File

@@ -1,8 +1,6 @@
<?php
namespace App\Tasks;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Models\WebSocket;
use App\Models\WebSocketTmpMsg;
use App\Module\Base;

View File

@@ -1,8 +1,6 @@
<?php
namespace App\Tasks;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Models\UmengAlias;
use App\Module\Base;

View File

@@ -10,8 +10,6 @@ use Carbon\Carbon;
use App\Models\WebSocketDialogMsg;
use Illuminate\Support\Facades\Cache;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
class UnclaimedTaskRemindTask extends AbstractTask
{
public function __construct()

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Tasks;
use App\Models\WebSocketDialogSession;
use App\Module\Base;
use App\Module\Extranet;
/**
* 通过AI接口更新对话标题
*/
class UpdateSessionTitleViaAiTask extends AbstractTask
{
public function __construct($sessionId, $msgText)
{
parent::__construct();
$this->sessionId = $sessionId;
$this->msgText = $msgText;
}
public function start()
{
if (empty($this->sessionId) || empty($this->msgText)) {
return;
}
$session = WebSocketDialogSession::whereId($this->sessionId)->first();
if (!$session) {
return;
}
$res = Extranet::openAIGenerateTitle($this->msgText);
if (Base::isError($res)) {
return;
}
$newTitle = $res['data'];
if ($newTitle && $newTitle != $session->title) {
$session->title = Base::cutStr($newTitle, 100);
$session->save();
}
}
public function end()
{
}
}

View File

@@ -2,8 +2,6 @@
namespace App\Tasks;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Models\User;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;

View File

@@ -2,6 +2,10 @@
namespace App\Tasks;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogUser;
use App\Module\Apps;
use App\Module\ZincSearch\ZincSearchDialogMsg;
use Carbon\Carbon;
use Illuminate\Support\Facades\Cache;
@@ -10,12 +14,57 @@ use Illuminate\Support\Facades\Cache;
*/
class ZincSearchSyncTask extends AbstractTask
{
public function __construct()
private $action;
private $data;
public function __construct($action = null, $data = null)
{
parent::__construct();
parent::__construct(...func_get_args());
$this->action = $action;
$this->data = $data;
}
public function start()
{
if (!Apps::isInstalled("search")) {
// 如果没有安装搜索模块,则不执行
return;
}
switch ($this->action) {
case 'sync':
// 同步消息数据
ZincSearchDialogMsg::sync(WebSocketDialogMsg::fillInstance($this->data));
break;
case 'delete':
// 删除消息数据
ZincSearchDialogMsg::delete(WebSocketDialogMsg::fillInstance($this->data));
break;
case 'userSync':
// 同步用户数据
ZincSearchDialogMsg::userSync(WebSocketDialogUser::fillInstance($this->data));
break;
case 'deleteUser':
// 删除用户数据
ZincSearchDialogMsg::delete(WebSocketDialogUser::fillInstance($this->data));
break;
default:
// 增量更新
$this->incrementalUpdate();
break;
}
}
/**
* 增量更新
* @return void
*/
private function incrementalUpdate()
{
// 120分钟执行一次
$time = intval(Cache::get("ZincSearchSyncTask:Time"));

View File

@@ -155,8 +155,8 @@ install() {
cat >${sitePath}/ssl.conf <<EOF
server_name ${domain};
listen 443 ssl;
ssl_certificate /etc/nginx/conf.d/site/ssl/${domain}.crt;
ssl_certificate_key /etc/nginx/conf.d/site/ssl/${domain}.key;
ssl_certificate /var/www/docker/nginx/site/ssl/${domain}.crt;
ssl_certificate_key /var/www/docker/nginx/site/ssl/${domain}.key;
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
ssl_prefer_server_ciphers on;

2
bin/version.js vendored

File diff suppressed because one or more lines are too long

766
cmd

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,7 @@
"php": "^8.0",
"ext-curl": "*",
"ext-dom": "*",
"ext-ffi": "*",
"ext-fileinfo": "*",
"ext-gd": "*",
"ext-imagick": "*",

View File

@@ -22,7 +22,7 @@ class AddProjectsPersonal extends Migration
});
if ($isAdd) {
// 更新数据
\App\Models\Project::whereName('个人项目')->chunkById(100, function ($lists) {
\App\Models\Project::where('name','like', '%个人项目%')->chunkById(100, function ($lists) {
/** @var \App\Models\Project $item */
foreach ($lists as $item) {
if ($item->desc == '注册时系统自动创建项目,你可以自由删除。') {

View File

@@ -1,7 +1,7 @@
services:
php:
container_name: "dootask-php-${APP_ID}"
image: "kuaifan/php:swoole-8.0.rc18"
image: "kuaifan/php:swoole-8.0.rc20"
shm_size: 2G
ulimits:
core:
@@ -12,7 +12,7 @@ services:
- ./docker/crontab/crontab.conf:/etc/supervisor/conf.d/crontab.conf
- ./docker/php/php.conf:/etc/supervisor/conf.d/php.conf
- ./docker/php/php.ini:/usr/local/etc/php/php.ini
- ./docker/log/supervisor:/var/log/supervisor
- ./docker/logs/supervisor:/var/log/supervisor
- ./:/var/www
environment:
LANG: "C.UTF-8"
@@ -22,9 +22,13 @@ services:
MYSQL_DB_NAME: "${DB_DATABASE}"
MYSQL_USERNAME: "${DB_USERNAME}"
MYSQL_PASSWORD: "${DB_PASSWORD}"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:${LARAVELS_LISTEN_PORT}/health"]
interval: 5s
timeout: 5s
retries: 5
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.2"
- extnetwork
depends_on:
mariadb:
condition: service_healthy
@@ -37,13 +41,20 @@ services:
image: "nginx:alpine"
ports:
- "${APP_PORT}:80"
- "${APP_SSL_PORT:-}:443"
- "${APP_SSL_PORT:-0}:443"
volumes:
- ./docker/nginx:/etc/nginx/conf.d
- ./public:/var/www/public
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
- ./:/var/www
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 5s
timeout: 5s
retries: 5
depends_on:
php:
condition: service_healthy
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.3"
- extnetwork
restart: unless-stopped
redis:
@@ -57,8 +68,7 @@ services:
timeout: 5s
retries: 5
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.4"
- extnetwork
restart: unless-stopped
mariadb:
@@ -80,175 +90,34 @@ services:
timeout: 5s
retries: 5
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.5"
- extnetwork
restart: unless-stopped
office:
container_name: "dootask-office-${APP_ID}"
image: "onlyoffice/documentserver:8.2.2.1"
appstore:
container_name: "dootask-appstore-${APP_ID}"
privileged: true
image: "dootask/appstore:0.1.3"
volumes:
- ./docker/office/logs:/var/log/onlyoffice
- ./docker/office/data:/var/www/onlyoffice/Data
- ./docker/office/etc/documentserver/default.json:/etc/onlyoffice/documentserver/default.json
- ./docker/office/resources/require.js:/var/www/onlyoffice/documentserver/web-apps/vendor/requirejs/require.js
- ./docker/office/resources/common/main/resources/img/header:/var/www/onlyoffice/documentserver/web-apps/apps/common/main/resources/img/header
- ./docker/office/resources/documenteditor/main/resources/css/app.css:/var/www/onlyoffice/documentserver/web-apps/apps/documenteditor/main/resources/css/app.css
- ./docker/office/resources/documenteditor/mobile/css/526.caf35c11a8d72ca5ac85.css:/var/www/onlyoffice/documentserver/web-apps/apps/documenteditor/mobile/css/526.caf35c11a8d72ca5ac85.css
- ./docker/office/resources/presentationeditor/main/resources/css/app.css:/var/www/onlyoffice/documentserver/web-apps/apps/presentationeditor/main/resources/css/app.css
- ./docker/office/resources/presentationeditor/mobile/css/923.f9cf19de1a25c2e7bf8b.css:/var/www/onlyoffice/documentserver/web-apps/apps/presentationeditor/mobile/css/923.f9cf19de1a25c2e7bf8b.css
- ./docker/office/resources/spreadsheeteditor/main/resources/css/app.css:/var/www/onlyoffice/documentserver/web-apps/apps/spreadsheeteditor/main/resources/css/app.css
- ./docker/office/resources/spreadsheeteditor/mobile/css/611.1bef49f175e18fc085db.css:/var/www/onlyoffice/documentserver/web-apps/apps/spreadsheeteditor/mobile/css/611.1bef49f175e18fc085db.css
- shared_data:/usr/share/dootask
- /var/run/docker.sock:/var/run/docker.sock
- ./:/var/www
environment:
JWT_SECRET: ${APP_KEY}
HOST_PWD: "${PWD}"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 5s
timeout: 5s
retries: 5
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.6"
restart: unless-stopped
fileview:
container_name: "dootask-fileview-${APP_ID}"
image: "kuaifan/fileview:4.4.0-4"
environment:
KK_CONTEXT_PATH: "/fileview"
KK_OFFICE_PREVIEW_SWITCH_DISABLED: true
KK_MEDIA_CONVERT_DISABLE: true
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.7"
restart: unless-stopped
drawio-webapp:
container_name: "dootask-drawio-webapp-${APP_ID}"
image: "jgraph/drawio:24.7.17"
volumes:
- ./docker/drawio/webapp/index.html:/usr/local/tomcat/webapps/draw/index.html
- ./docker/drawio/webapp/stencils:/usr/local/tomcat/webapps/draw/stencils
- ./docker/drawio/webapp/js/app.min.js:/usr/local/tomcat/webapps/draw/js/app.min.js
- ./docker/drawio/webapp/js/croppie/croppie.min.css:/usr/local/tomcat/webapps/draw/js/croppie/croppie.min.css
- ./docker/drawio/webapp/js/diagramly/ElectronApp.js:/usr/local/tomcat/webapps/draw/js/diagramly/ElectronApp.js
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.8"
restart: unless-stopped
drawio-export:
container_name: "dootask-drawio-export-${APP_ID}"
image: "kuaifan/export-server:0.0.1"
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.9"
volumes:
- ./docker/drawio/export/fonts:/usr/share/fonts/drawio
restart: unless-stopped
minder:
container_name: "dootask-minder-${APP_ID}"
image: "kuaifan/minder:0.1.3"
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.10"
restart: unless-stopped
approve:
container_name: "dootask-approve-${APP_ID}"
image: "kuaifan/dooapprove:0.1.5"
environment:
TZ: "${TIMEZONE:-PRC}"
MYSQL_HOST: "${DB_HOST}"
MYSQL_PORT: "${DB_PORT}"
MYSQL_DBNAME: "${DB_DATABASE}"
MYSQL_USERNAME: "${DB_USERNAME}"
MYSQL_PASSWORD: "${DB_PASSWORD}"
MYSQL_Prefix: "${DB_PREFIX}approve_"
DEMO_DATA: true
KEY: ${APP_KEY}
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.11"
restart: unless-stopped
ai:
container_name: "dootask-ai-${APP_ID}"
image: "kuaifan/dootask-ai:0.3.5"
environment:
REDIS_HOST: "${REDIS_HOST}"
REDIS_PORT: "${REDIS_PORT}"
TIMEOUT: 600
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.12"
restart: unless-stopped
okr:
container_name: "dootask-okr-${APP_ID}"
image: "kuaifan/doookr:0.4.5"
environment:
TZ: "${TIMEZONE:-PRC}"
DOO_TASK_URL: "http://nginx"
MYSQL_HOST: "${DB_HOST}"
MYSQL_PORT: "${DB_PORT}"
MYSQL_DBNAME: "${DB_DATABASE}"
MYSQL_USERNAME: "${DB_USERNAME}"
MYSQL_PASSWORD: "${DB_PASSWORD}"
MYSQL_PREFIX: "${DB_PREFIX}"
DEMO_DATA: true
KEY: ${APP_KEY}
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.13"
restart: unless-stopped
face:
container_name: "dootask-face-${APP_ID}"
image: "hitosea2020/dooface:0.0.1"
ports:
- "7788:7788"
environment:
TZ: "${TIMEZONE:-PRC}"
STORAGE: mysql
MYSQL_HOST: "${DB_HOST}"
MYSQL_PORT: "${DB_PORT}"
MYSQL_USERNAME: "${DB_USERNAME}"
MYSQL_PASSWORD: "${DB_PASSWORD}"
MYSQL_DB_NAME: "${DB_DATABASE}"
DB_PREFIX: "${DB_PREFIX}"
REPORT_API: "http://nginx/api/public/checkin/report"
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.14"
restart: unless-stopped
search:
container_name: "dootask-search-${APP_ID}"
image: "public.ecr.aws/zinclabs/zincsearch:0.4.10"
volumes:
- search_data:/data
environment:
ZINC_DATA_PATH: "/data"
ZINC_FIRST_ADMIN_USER: "${DB_USERNAME}"
ZINC_FIRST_ADMIN_PASSWORD: "${DB_PASSWORD}"
deploy:
resources:
limits:
cpus: '2'
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.15"
- extnetwork
restart: unless-stopped
networks:
extnetwork:
name: "dootask-networks-${APP_ID}"
ipam:
config:
- subnet: "${APP_IPPR}.0/24"
gateway: "${APP_IPPR}.1"
volumes:
shared_data:
name: "dootask-shared-data-${APP_ID}"
redis_data:
name: "dootask-redis-data-${APP_ID}"
search_data:
name: "dootask-search-data-${APP_ID}"

5
docker/appstore/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
apps/*
config/*
log/*
temp/*
!.gitkeep

View File

View File

View File

View File

@@ -1 +0,0 @@

View File

@@ -1,3 +0,0 @@
# Change
diff https://github.com/jgraph/drawio/tree/acd938b1e42cff3be3b629e6239cdec9a9baddcc

View File

@@ -1,468 +0,0 @@
<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=5" ><![endif]-->
<!DOCTYPE html>
<html>
<head>
<title>Flowchart Maker &amp; Online Diagram Software</title>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="Description" content="draw.io is free online diagram software for making flowcharts, process diagrams, org charts, UML, ER and network diagrams">
<meta name="Keywords" content="drawio, diagram, online, flow chart, flowchart maker, uml, erd">
<meta itemprop="name" content="draw.io - free flowchart maker and diagrams online">
<meta itemprop="description" content="draw.io is a free online diagramming application and flowchart maker . You can use it to create UML, entity relationship,
org charts, BPMN and BPM, database schema and networks. Also possible are telecommunication network, workflow, flowcharts, maps overlays and GIS, electronic
circuit and social network diagrams.">
<meta itemprop="image" content="https://lh4.googleusercontent.com/-cLKEldMbT_E/Tx8qXDuw6eI/AAAAAAAAAAs/Ke0pnlk8Gpg/w500-h344-k/BPMN%2Bdiagram%2Brc2f.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="msapplication-config" content="images/browserconfig.xml">
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#d89000">
<script id="geBootstrap" type="text/javascript">
window.EXPORT_URL = window.location.origin + "/drawio/export/";
window.DRAWIO_LIGHTBOX_URL = window.location.origin + "/drawio/webapp";
setInterval(function() {window.ICONSEARCH_PATH = window.location.origin + "/drawio/iconsearch";}, 1000)
/**
* URL Parameters and protocol description are here:
*
* https://www.drawio.com/doc/faq/supported-url-parameters
*
* Parameters for developers:
*
* - dev=1: For developers only
* - test=1: For developers only
* - export=URL for export: For developers only
* - ignoremime=1: For developers only (see DriveClient.js). Use Cmd-S to override mime.
* - createindex=1: For developers only (see etc/build/README)
* - filesupport=0: For developers only (see Editor.js in core)
* - savesidebar=1: For developers only (see Sidebar.js)
* - pages=1: For developers only (see Pages.js)
* - lic=email: For developers only (see LicenseServlet.java)
* --
* - networkshapes=1: For testing network shapes (temporary)
*/
var urlParams = (function()
{
var result = new Object();
var params = window.location.search.slice(1).split('&');
for (var i = 0; i < params.length; i++)
{
var idx = params[i].indexOf('=');
if (idx > 0)
{
result[params[i].substring(0, idx)] = params[i].substring(idx + 1);
}
}
return result;
})();
// Forces CDN caches by passing URL parameters via URL hash
if (window.location.hash != null && window.location.hash.substring(0, 2) == '#P')
{
try
{
urlParams = JSON.parse(decodeURIComponent(window.location.hash.substring(2)));
if (urlParams.hash != null)
{
window.location.hash = urlParams.hash;
}
}
catch (e)
{
// ignore
}
}
// Global variable for desktop
var mxIsElectron = navigator.userAgent != null && navigator.userAgent.toLowerCase().indexOf(' electron/') > -1 &&
navigator.userAgent.indexOf(' draw.io/') > -1;
// Redirects page if required
if (urlParams['dev'] != '1')
{
(function()
{
var proto = window.location.protocol;
if (!mxIsElectron)
{
var host = window.location.host;
// Redirects apex, drive and rt to www
if (host === 'draw.io' || host === 'rt.draw.io' || host === 'drive.draw.io')
{
host = 'www.draw.io';
}
var href = proto + '//' + host + window.location.href.substring(
window.location.protocol.length +
window.location.host.length + 2);
// Redirects if href changes
if (href != window.location.href)
{
window.location.href = href;
}
}
})();
}
/**
* Adds meta tag to the page.
*/
function mxmeta(name, content, httpEquiv)
{
try
{
var s = document.createElement('meta');
if (name != null)
{
s.setAttribute('name', name);
}
s.setAttribute('content', content);
if (httpEquiv != null)
{
s.setAttribute('http-equiv', httpEquiv);
}
var t = document.getElementsByTagName('meta')[0];
t.parentNode.insertBefore(s, t);
}
catch (e)
{
// ignore
}
};
/**
* Synchronously adds scripts to the page.
*/
function mxscript(src, onLoad, id, dataAppKey, noWrite, onError)
{
var defer = onLoad == null && !noWrite;
if ((urlParams['dev'] != '1' && typeof document.createElement('canvas').getContext === "function") ||
onLoad != null || noWrite)
{
var s = document.createElement('script');
s.setAttribute('type', 'text/javascript');
s.setAttribute('defer', 'true');
s.setAttribute('src', src);
if (id != null)
{
s.setAttribute('id', id);
}
if (dataAppKey != null)
{
s.setAttribute('data-app-key', dataAppKey);
}
if (onLoad != null)
{
var r = false;
s.onload = s.onreadystatechange = function()
{
if (!r && (!this.readyState || this.readyState == 'complete'))
{
r = true;
onLoad();
}
};
}
if (onError != null)
{
s.onerror = function(e)
{
onError('Failed to load ' + src, e);
};
}
var t = document.getElementsByTagName('script')[0];
if (t != null)
{
t.parentNode.insertBefore(s, t);
}
}
else
{
document.write('<script src="' + src + '"' + ((id != null) ? ' id="' + id +'" ' : '') +
((dataAppKey != null) ? ' data-app-key="' + dataAppKey +'" ' : '') + '></scr' + 'ipt>');
}
};
/**
* Asynchronously adds scripts to the page.
*/
function mxinclude(src)
{
var g = document.createElement('script');
g.type = 'text/javascript';
g.async = true;
g.src = src;
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(g, s);
};
/**
* Adds meta tags with application name (depends on offline URL parameter)
*/
(function()
{
var name = 'diagrams.net';
mxmeta('apple-mobile-web-app-title', name);
mxmeta('application-name', name);
if (mxIsElectron)
{
mxmeta(null, 'default-src \'self\'; script-src \'self\' \'sha256-6g514VrT/cZFZltSaKxIVNFF46+MFaTSDTPB8WfYK+c=\'; connect-src \'self\' https://*.draw.io https://*.diagrams.net https://fonts.googleapis.com https://fonts.gstatic.com; img-src * data:; media-src *; font-src *; frame-src \'none\'; style-src \'self\' \'unsafe-inline\' https://fonts.googleapis.com; base-uri \'none\';child-src \'self\';object-src \'none\';', 'Content-Security-Policy');
}
})();
// Checks for local storage
var isLocalStorage = false;
try
{
isLocalStorage = urlParams['local'] != '1' && typeof(localStorage) != 'undefined';
}
catch (e)
{
// ignored
}
var mxScriptsLoaded = false, mxWinLoaded = false;
function checkAllLoaded()
{
if (mxScriptsLoaded && mxWinLoaded)
{
App.main();
}
};
var t0 = new Date();
// Changes paths for local development environment
if (urlParams['dev'] == '1')
{
// Used to request grapheditor/mxgraph sources in dev mode
var mxDevUrl = '';
// Used to request draw.io sources in dev mode
var drawDevUrl = '';
var geBasePath = 'js/grapheditor';
var mxBasePath = 'mxgraph/src';
if (document.location.protocol == 'file:')
{
// Forces includes for dev environment in node.js
mxForceIncludes = true;
}
mxForceIncludes = false;
mxscript(drawDevUrl + 'js/PreConfig.js');
mxscript(drawDevUrl + 'js/diagramly/Init.js');
mxscript(geBasePath + '/Init.js');
mxscript(mxBasePath + '/mxClient.js');
// Adds all JS code that depends on mxClient. This indirection via Devel.js is
// required in some browsers to make sure mxClient.js (and the files that it
// loads asynchronously) are available when the code loaded in Devel.js runs.
mxscript(drawDevUrl + 'js/diagramly/Devel.js');
// Electron
if (mxIsElectron)
{
mxscript('js/diagramly/DesktopLibrary.js');
mxscript('js/diagramly/ElectronApp.js');
}
mxscript(drawDevUrl + 'js/PostConfig.js');
}
else
{
(function()
{
var hostName = window.location.hostname;
// Supported domains are *.draw.io and the packaged version in Quip
var supportedDomain = (hostName.substring(hostName.length - 8, hostName.length) === '.draw.io') ||
(hostName.substring(hostName.length - 13, hostName.length) === '.diagrams.net');
function loadAppJS()
{
mxscript('js/app.min.js', function()
{
mxScriptsLoaded = true;
checkAllLoaded();
// Electron
if (mxIsElectron)
{
mxscript('js/diagramly/DesktopLibrary.js', function()
{
mxscript('js/diagramly/ElectronApp.js', function()
{
mxscript('js/extensions.min.js', function()
{
mxscript('js/stencils.min.js', function()
{
mxscript('js/shapes-14-6-5.min.js', function()
{
mxscript('js/PostConfig.js');
});
});
});
});
});
}
else if (!supportedDomain || navigator.onLine)
{
mxscript('js/PostConfig.js');
}
});
};
if (!supportedDomain || mxIsElectron || navigator.onLine)
{
mxscript('js/PreConfig.js', loadAppJS);
}
else
{
loadAppJS();
}
})();
}
// Adds basic error handling
window.onerror = function()
{
var status = document.getElementById('geStatus');
if (status != null)
{
status.innerHTML = 'Page could not be loaded. Please try refreshing.';
}
};
</script>
<link rel="icon" href="favicon.ico" sizes="any">
<link rel="apple-touch-icon" href="images/apple-touch-icon.png">
<link rel="stylesheet" type="text/css" href="styles/grapheditor.css">
<link rel="stylesheet" media="(forced-colors: active)" href="styles/high-contrast.css" id="high-contrast-stylesheet">
<link rel="canonical" href="https://app.diagrams.net">
<link rel="manifest" href="images/manifest.json">
<link rel="shortcut icon" href="favicon.ico">
<style type="text/css">
body { overflow:hidden; }
div.picker { z-index: 10007; }
.geSidebarContainer .geTitle input {
font-size:8pt;
color:#606060;
}
.geBlock {
display: none;
z-index:-3;
margin:100px;
margin-top:40px;
margin-bottom:30px;
padding:20px;
text-align:center;
min-width:50%;
}
.geBlock h1, .geBlock h2 {
margin-top:0px;
padding-top:0px;
}
.geEditor *:not(.geScrollable)::-webkit-scrollbar {
width:10px;
height:10px;
}
.geEditor ::-webkit-scrollbar-track {
background-clip:padding-box;
border:solid transparent;
border-width:1px;
}
.geEditor ::-webkit-scrollbar-corner {
background-color:transparent;
}
.geEditor ::-webkit-scrollbar-thumb {
background-color:rgba(0,0,0,.1);
background-clip:padding-box;
border:solid transparent;
border-radius:10px;
}
.geEditor ::-webkit-scrollbar-thumb:hover {
background-color:rgba(0,0,0,.4);
}
.geTemplate {
border:1px solid transparent;
display:inline-block;
_display:inline;
vertical-align:top;
border-radius:3px;
overflow:hidden;
font-size:14pt;
cursor:pointer;
margin:5px;
}
</style>
<!-- Workaround for binary XHR in IE 9/10, see App.loadUrl -->
<!--[if (IE 9)|(IE 10)]><!-->
<script type="text/vbscript">
Function mxUtilsBinaryToArray(Binary)
Dim i
ReDim byteArray(LenB(Binary))
For i = 1 To LenB(Binary)
byteArray(i-1) = AscB(MidB(Binary, i, 1))
Next
mxUtilsBinaryToArray = byteArray
End Function
</script>
<!--<![endif]-->
</head>
<body class="geEditor">
<div id="geInfo">
<div class="geBlock">
<h1>Flowchart Maker and Online Diagram Software</h1>
<p>
draw.io is free online diagram software. You can use it as a flowchart maker, network diagram software, to create UML online, as an ER diagram tool,
to design database schema, to build BPMN online, as a circuit diagram maker, and more. draw.io can import .vsdx, Gliffy&trade; and Lucidchart&trade; files .
</p>
<h2 id="geStatus">Loading...</h2>
<p>
Please ensure JavaScript is enabled.
</p>
</div>
</div>
<script id="geMain" type="text/javascript">
/**
* Main
*/
if (urlParams['dev'] != '1' && typeof document.createElement('canvas').getContext === "function")
{
window.addEventListener('load', function()
{
mxWinLoaded = true;
checkAllLoaded();
});
}
else
{
App.main();
}
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.croppie-container{width:100%;height:100%}.croppie-container .cr-image{z-index:-1;position:absolute;top:0;left:0;transform-origin:0 0;max-height:none;max-width:none}.croppie-container .cr-boundary{position:relative;overflow:hidden;margin:0 auto;z-index:1;width:100%;height:100%}.croppie-container .cr-resizer,.croppie-container .cr-viewport{position:absolute;border:2px solid #fff;margin:auto;top:0;bottom:0;right:0;left:0;box-shadow:0 0 2000px 2000px rgba(0,0,0,.5);z-index:0}.croppie-container .cr-resizer{z-index:2;box-shadow:none;pointer-events:none}.croppie-container .cr-resizer-horisontal,.croppie-container .cr-resizer-vertical{position:absolute;pointer-events:all}.croppie-container .cr-resizer-horisontal::after,.croppie-container .cr-resizer-vertical::after{display:block;position:absolute;box-sizing:border-box;border:1px solid #000;background:#fff;width:10px;height:10px;content:''}.croppie-container .cr-resizer-vertical{bottom:-5px;cursor:row-resize;width:100%;height:10px}.croppie-container .cr-resizer-vertical::after{left:50%;margin-left:-5px}.croppie-container .cr-resizer-horisontal{right:-5px;cursor:col-resize;width:10px;height:100%}.croppie-container .cr-resizer-horisontal::after{top:50%;margin-top:-5px}.croppie-container .cr-original-image{display:none}.croppie-container .cr-vp-circle{border-radius:50%}.croppie-container .cr-overlay{z-index:1;position:absolute;cursor:move;touch-action:none}.croppie-container .cr-slider-wrap{width:75%;margin:15px auto;text-align:center}.croppie-result{position:relative;overflow:hidden}.croppie-result img{position:absolute}.croppie-container .cr-image,.croppie-container .cr-overlay,.croppie-container .cr-viewport{-webkit-transform:translateZ(0);-moz-transform:translateZ(0);-ms-transform:translateZ(0);transform:translateZ(0)}.cr-slider{-webkit-appearance:none;width:300px;max-width:100%;padding-top:8px;padding-bottom:8px;background-color:transparent}.cr-slider::-webkit-slider-runnable-track{width:100%;height:3px;background:rgba(0,0,0,.5);border:0;border-radius:3px}.cr-slider::-webkit-slider-thumb{-webkit-appearance:none;border:none;height:16px;width:16px;border-radius:50%;background:#ddd;margin-top:-6px}.cr-slider:focus{outline:0}.cr-slider::-moz-range-track{width:100%;height:3px;background:rgba(0,0,0,.5);border:0;border-radius:3px}.cr-slider::-moz-range-thumb{border:none;height:16px;width:16px;border-radius:50%;background:#ddd;margin-top:-6px}.cr-slider:-moz-focusring{outline:1px solid #fff;outline-offset:-1px}.cr-slider::-ms-track{width:100%;height:5px;background:0 0;border-color:transparent;border-width:6px 0;color:transparent}.cr-slider::-ms-fill-lower{background:rgba(0,0,0,.5);border-radius:10px}.cr-slider::-ms-fill-upper{background:rgba(0,0,0,.5);border-radius:10px}.cr-slider::-ms-thumb{border:none;height:16px;width:16px;border-radius:50%;background:#ddd;margin-top:1px}.cr-slider:focus::-ms-fill-lower{background:rgba(0,0,0,.5)}.cr-slider:focus::-ms-fill-upper{background:rgba(0,0,0,.5)}.cr-rotate-controls{position:absolute;bottom:5px;left:5px;z-index:1}.cr-rotate-controls button{border:0;background:0 0}.cr-rotate-controls i:before{display:inline-block;font-style:normal;font-weight:900;font-size:22px}.cr-rotate-l i:before{content:'↺'}.cr-rotate-r i:before{content:'↻'}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

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