Compare commits

...

405 Commits

Author SHA1 Message Date
kuaifan
40b637b16e build 2024-03-20 02:53:37 +09:00
kuaifan
6e68f399b4 perf: 优化数据结构 2024-03-20 02:49:44 +09:00
kuaifan
0be6c70e92 Merge commit '6c2d8fc16313234bbacb4ad4d7f8637b71025a26' into pro 2024-03-20 02:03:26 +09:00
weifs
6c2d8fc163 perf: 接龙优化为清空内容默认删除 2024-03-19 18:41:41 +08:00
kuaifan
a8193b8feb build 2024-03-19 16:32:39 +09:00
kuaifan
34159caf22 Merge commit 'd12c0c42072452de4c99ef55c5915edb108dd2ef' into pro 2024-03-19 16:04:26 +09:00
kuaifan
c75f406459 no message 2024-03-19 15:57:24 +09:00
kuaifan
99dca06d44 perf: 支持取消发送中的消息 2024-03-19 15:57:07 +09:00
weifs
d12c0c4207 perf: 1. 强化接龙接口本地时间戳问题 2. 接龙消息点展开按钮后做缓存处理 2024-03-19 14:38:15 +08:00
kuaifan
915a5ed7d5 fix: 关闭文件后无法再次打开 2024-03-19 12:07:54 +09:00
kuaifan
7bfc43c85f no message 2024-03-19 04:06:45 +09:00
kuaifan
77ea022ddf build 2024-03-19 03:23:09 +09:00
kuaifan
93578f93f4 Merge commit 'cbbd50a2e320ca0427474fb2921f2b93a5ad2c14' into pro 2024-03-19 03:17:19 +09:00
kuaifan
f129615ebe no message 2024-03-19 03:15:38 +09:00
kuaifan
0e5b44baad perf: 自动识别md格式发送 2024-03-19 03:15:19 +09:00
kuaifan
3596475790 fix: 消息太长导致菜单无法正常显示 2024-03-19 03:14:09 +09:00
kuaifan
6218521dea fix: 项目数量不正确的情况 2024-03-19 02:29:06 +09:00
kuaifan
65db8b5703 fix: 部分未读和待办信息不显示的情况 2024-03-19 02:21:48 +09:00
kuaifan
f5ff9a3648 perf: 优化回复、转发消息数据结构 2024-03-18 19:44:20 +09:00
weifs
cbbd50a2e3 fix: 审批中心修复loadIng效果 2024-03-18 14:12:19 +08:00
kuaifan
b04647e65a build 2024-03-17 17:50:27 +09:00
kuaifan
d34d94faa6 perf: 优化iOS端数据读取失败的情况 2024-03-17 17:21:29 +09:00
kuaifan
4038d9560f perf: 回复消息时自动@提及 2024-03-17 17:00:05 +09:00
kuaifan
006fc43498 perf: 优化会话数据结构 2024-03-17 16:23:57 +09:00
kuaifan
47c9b2e1b0 build 2024-03-15 12:51:10 +09:00
kuaifan
dc3e5f0a59 perf: 聊天文件发送进度 2024-03-15 12:48:33 +09:00
kuaifan
01bda83fcd build 2024-03-15 11:24:46 +09:00
kuaifan
9ecb9c68fb perf: 拨打电话确认提示 2024-03-15 11:16:20 +09:00
kuaifan
4612d5180a no message 2024-03-15 11:15:58 +09:00
kuaifan
cfb653796c perf: 优化预加载文件 2024-03-15 10:58:19 +09:00
kuaifan
d00cd5cb26 fix: 子窗口出现重新登录的情况 2024-03-15 10:19:24 +09:00
kuaifan
285a62c87e no message 2024-03-15 02:15:03 +09:00
kuaifan
bcb0c6bc77 build 2024-03-14 13:48:38 +09:00
kuaifan
d1ab2d98eb no message 2024-03-14 13:45:01 +09:00
kuaifan
c3d5328154 perf: 优化接口时间 2024-03-14 13:44:46 +09:00
kuaifan
fc30588014 build 2024-03-14 08:15:46 +09:00
kuaifan
65b02001b2 no message 2024-03-14 08:01:28 +09:00
kuaifan
cd011a172f perf: 优化审批对话按钮配色 2024-03-14 08:01:17 +09:00
kuaifan
bf913d9eff fix: 回复消息点击到原文无效 2024-03-14 07:26:19 +09:00
kuaifan
c2dd15fca1 no message 2024-03-14 02:55:42 +09:00
kuaifan
b267863b58 build 2024-03-13 14:46:32 +09:00
kuaifan
d189fb100a Merge commit 'c6568969c7b2d538d27cb4ca0ee412d4dbdceb56' into pro 2024-03-13 14:39:39 +09:00
kuaifan
dc6c5bef26 no message 2024-03-13 14:38:49 +09:00
kuaifan
7208d51644 perf: 优化文件功能按钮 2024-03-13 14:38:38 +09:00
kuaifan
16359a968d perf: 文件上传支持覆盖上传 2024-03-13 14:25:15 +09:00
kuaifan
d553f77533 perf: 优化app等比显示 2024-03-13 11:34:27 +09:00
kuaifan
bc25f5dfdf perf: 优化发送文件预览 2024-03-13 10:58:46 +09:00
kuaifan
d40028340c perf: 消息发送中禁止右键菜单 2024-03-13 08:03:58 +09:00
kuaifan
4194d1cddd perf: 部分搜索框图标抖动 2024-03-13 07:57:41 +09:00
kuaifan
1fdd532133 perf: 优化复制功能 2024-03-13 07:49:12 +09:00
kuaifan
71c62a3772 fix: 客户端无法打开excel文件 2024-03-13 07:32:16 +09:00
kuaifan
9be6cd5148 perf: 优化pdf文件预览 2024-03-13 07:14:27 +09:00
wfs
c6568969c7 style: 调整代码格式 2024-03-13 00:26:13 +08:00
wfs
f5b1a6ab05 fix: 修复投票实名逻辑 2024-03-13 00:25:10 +08:00
wfs
5efe659cf5 perf: 优化投票接口,加上事务锁 2024-03-13 00:17:23 +08:00
wfs
b254fd5ce2 perf: 优化接龙接口,加上事务锁 2024-03-12 23:49:05 +08:00
kuaifan
631a0ffff4 no message 2024-03-13 00:42:15 +09:00
kuaifan
8b11e9a19e perf: 优化转发消息样式 2024-03-13 00:42:06 +09:00
weifs
f6b006b000 feat: 升级okr 2024-03-11 18:07:37 +08:00
weifs
3a26f420b8 perf: 接龙接口-强化排序 2024-03-11 17:25:40 +08:00
weifs
0919e415ec perf: 审批按钮色微调 2024-03-11 16:57:45 +08:00
weifs
030a07698d perf: 统一审批中心的按钮色 2024-03-11 16:36:12 +08:00
weifs
a7f2582df7 perf: 转发会议亮色皮肤问题,转发文件宽度铺满 2024-03-11 10:39:53 +08:00
Pang
5f0a0e0371 no message 2024-03-10 22:13:21 +08:00
Pang
28717fd0c7 perf: 优化app数据交互 2024-03-10 14:44:55 +08:00
Pang
7014ea176a build 2024-03-10 12:12:30 +08:00
Pang
b4f2da66be fix: 修复搜索偶尔无效的情况 2024-03-10 11:03:34 +08:00
Pang
b53462cf6e build 2024-03-10 00:39:01 +08:00
Pang
8b40364722 no message 2024-03-10 00:36:07 +08:00
Pang
6ee1824410 perf: 优化文件预览 2024-03-09 17:38:33 +08:00
Pang
f63c2da37a no message 2024-03-09 10:44:36 +08:00
Pang
9be0642ba5 perf: 滑动列表自动隐藏键盘 2024-03-09 10:35:44 +08:00
Pang
55a922c7b3 perf: 优化时间格式 2024-03-09 10:16:09 +08:00
kuaifan
50893929d6 perf: 适配nodejs 20 2024-03-08 17:04:56 +08:00
weifs
03c94e791a feat: 升级okr 2024-03-08 11:03:16 +08:00
weifs
96bb554813 feat: 升级okr 2024-03-08 10:48:33 +08:00
Pang
1bc77de144 build 2024-03-07 23:13:44 +08:00
Pang
aa07c78fc8 perf: 修改消息换行优化 2024-03-07 23:13:24 +08:00
kuaifan
52dda88d40 build 2024-03-07 22:47:04 +08:00
kuaifan
c555b309bd feat: 新增不显示会话功能 2024-03-07 21:05:46 +08:00
kuaifan
0a51225762 Merge commit '3d725ddeef1f86eb02ff2b33435c23bc5e6da8de' into pro 2024-03-07 19:26:49 +08:00
kuaifan
fab49b1dda no message 2024-03-07 19:22:41 +08:00
kuaifan
57e422f2d3 perf: 优化预加载资源 2024-03-07 15:54:55 +08:00
kuaifan
50a1a3147e perf: 优化pdf文件预览 2024-03-07 15:54:26 +08:00
kuaifan
277115a30f perf: 优化签到消息 2024-03-07 15:53:31 +08:00
kuaifan
7464de3adc fix: 部分手机出现非正常滚动到底部的情况 2024-03-07 15:52:47 +08:00
HEXIANG
3d725ddeef fix:dootask官网标题 2024-03-07 15:17:25 +08:00
Pang
38d8f289e4 no message 2024-03-07 08:27:47 +08:00
Pang
edfd6e6de2 perf: 优化登录 2024-03-07 07:17:24 +08:00
Pang
a68ab6512e Merge commit '8383b88a448501cfdb32b79b6e2ccafedadf5e97' into pro 2024-03-06 22:03:34 +08:00
HEXIANG
8383b88a44 fix:dootask官网调整 2024-03-06 18:01:50 +08:00
kuaifan
27ff24f44e perf: 优化安装脚本 2024-03-06 17:18:12 +08:00
weifs
b111ecb227 fix: 修复导出任务统计没有按创建时间来的bug 2024-03-06 14:00:13 +08:00
kuaifan
ac17952cd3 perf: 优化消息时间格式 2024-03-06 12:01:18 +08:00
kuaifan
e24978fdd7 perf: 优化app功能 2024-03-06 11:47:05 +08:00
weifs
a4d7579e3f feat: 升级okr 2024-03-06 10:23:47 +08:00
Pang
52171b794a build 2024-03-06 02:44:16 +08:00
Pang
3c33f02e9d Merge commit '0968c43f61e0183aaf47e38a482d037bc33fc434' into pro 2024-03-06 01:08:59 +08:00
Pang
0a8823c40b no message 2024-03-06 01:05:30 +08:00
Pang
a3f7e71638 translate 2024-03-06 01:01:29 +08:00
Pang
7ebf4fb9ce perf: 默认映射443端口 2024-03-06 01:01:12 +08:00
kuaifan
c96bad3cdf perf: 优化子窗口 2024-03-06 00:30:30 +08:00
weifs
0968c43f61 feat: 发起投票功能添加缓存记录选中效果 2024-03-05 17:50:13 +08:00
weifs
ae147c76ff perf: 优化发布接口 删除目录的逻辑 2024-03-05 15:22:40 +08:00
weifs
0e916a2804 feat: 导出的签到数据和审批数据换成xlsx,因老版本的xls会出现兼容性问题 2024-03-05 15:19:12 +08:00
weifs
494565e131 perf: 优化发布接口 删除目录的逻辑 2024-03-04 09:51:35 +08:00
kuaifan
c4430e1a6c build 2024-03-02 14:56:12 +08:00
kuaifan
26adfa11bf Merge commit '6230bf94c5abfa7e67c6226e0b0dc088d80ff055' into pro 2024-03-02 14:39:47 +08:00
kuaifan
69ec57669e perf: 更新说明文档 2024-03-02 14:11:52 +08:00
kuaifan
3556133585 perf: 优化客户端 2024-03-02 14:11:33 +08:00
kuaifan
a65181757d perf: 默认关闭端到端加密传输 2024-03-02 13:21:01 +08:00
kuaifan
42d39a830e fix: 修改消息导致最后消息改变 2024-03-02 13:07:27 +08:00
kuaifan
2e70c9617c fix: 显示无关系的子任务、指定成员可见消息推送 2024-03-02 12:52:16 +08:00
zzw
6230bf94c5 fix:调整gemini机器人设置参数 2024-03-01 17:47:41 +08:00
kuaifan
47832ececb Merge commit '76570e2f1b72de50cb54c36229447eea69c59940' into pro 2024-03-01 16:02:36 +08:00
kuaifan
60e6003485 build 2024-02-29 19:47:45 +08:00
kuaifan
9133f289b4 perf: 优化ai机器人 2024-02-29 19:39:05 +08:00
gwok
76570e2f1b fix:修复左侧滚动菜单不跟随右侧正文自动上滑 2024-02-29 18:25:26 +08:00
weifs
c9234a4b49 feat: 升级okr 2024-02-29 16:22:25 +08:00
weifs
c1361fadda perf: okr和审批优化 2024-02-29 15:26:45 +08:00
zzw
ec7af94f71 fix:调整发送参数逻辑 2024-02-29 11:39:30 +08:00
kuaifan
81690d6ce9 build 2024-02-28 15:33:33 +08:00
kuaifan
236b57864b Merge commit 'e3ce3bcfbe1d251c791fddbf2627b35669bdd2df' into pro 2024-02-28 15:21:43 +08:00
kuaifan
22259ec34d build 2024-02-28 15:15:25 +08:00
kuaifan
5a4700753a perf: 优化pdf在线预览 2024-02-28 15:04:56 +08:00
kuaifan
cc862741dc fix: 推送标题存在换行时不显示 2024-02-28 15:04:31 +08:00
Pang
779b32e8ad feat: 优化内置浏览器 2024-02-28 15:03:56 +08:00
zzw
e3ce3bcfbe feat:gemini机器人添加代理参数 2024-02-28 14:37:18 +08:00
zzw
673053f181 feat:集成geminiAI 2024-02-27 18:23:07 +08:00
Pang
b6eb77ae63 no message 2024-02-24 09:18:12 +08:00
kuaifan
0e63255a7f build 2024-02-23 22:11:43 +08:00
kuaifan
f42408a363 no message 2024-02-23 22:11:43 +08:00
kuaifan
897fc51ce3 build 2024-02-23 22:11:43 +08:00
Pang
6848b126c5 perf: 优化客户端打开服务器链接 2024-02-23 22:11:43 +08:00
Pang
0ed9afd1bd no message 2024-02-23 22:11:43 +08:00
weifs
26cca8298f fix: 修复下载文件大小为0时报错 2024-02-23 13:53:03 +08:00
weifs
58407af2ba fix: 更改其他版本的链接 2024-02-23 09:40:00 +08:00
spylecym
3a0473a74f fix: 修复价格页面样式 2024-02-23 09:13:00 +08:00
weifs
6e5124fe22 fix: 发布接口,调整缓存时间为两小时 2024-02-22 19:19:50 +08:00
weifs
02bd022c62 feat: 发布接口只保留最近两个版本 2024-02-22 19:18:04 +08:00
weifs
f2538884ea perf: 签到设置,有些客户服务器安全体系会拦截 curl -sSL 关键字,优化为cmd不传值 2024-02-22 11:07:54 +08:00
weifs
8d121d4056 perf: 签到设置,有些客户服务器安全体系会拦截 curl -sSL 关键字,优化为base64返回 2024-02-22 11:02:32 +08:00
weifs
96438604ee perf: 升级okr 2024-02-21 17:30:59 +08:00
ganzizi
63ccd675d0 fix: 修复okr定时处理信息不发送 2024-02-21 16:05:32 +08:00
weifs
2c08145c40 fix: 项目已归档,任务面板也没有这三个任务,但是每次新增报告,都会弹任务出来 2024-02-21 14:42:30 +08:00
spylecym
12effb5738 fix: 修改下载页面按钮布局样式 2024-02-20 15:52:32 +08:00
weifs
91bfb989be feat: 官网添加其他版本的按钮 2024-02-20 15:05:45 +08:00
weifs
192de79fea feat: 统一表为utf8mb4_unicode_ci 2024-02-20 15:00:38 +08:00
Pang
82063f1b21 build 2024-02-06 07:27:43 +08:00
kuaifan
02b263439b perf: 升级okr容器 2024-02-05 21:26:53 +08:00
kuaifan
7efaf3bb32 perf: 新增禁止私聊、群聊功能 2024-02-05 21:24:45 +08:00
kuaifan
7dd5b082cf perf: 更换笑话和鸡汤接口 2024-02-05 13:33:42 +08:00
Pang
6320eaa3ac build 2024-01-25 21:52:57 +08:00
Pang
85b88b6b61 no message 2024-01-21 20:20:02 +08:00
Pang
f285665f90 build 2024-01-21 16:24:29 +08:00
Pang
8f4399dc2f perf: 修复一些问题 2024-01-21 16:12:24 +08:00
Pang
c8b8cc578d build 2024-01-19 14:38:01 +08:00
Pang
a142f52113 perf: iOS打开键盘时看不见通知的情况 2024-01-18 18:36:09 +08:00
Pang
aa666a9662 build 2024-01-18 13:16:08 +08:00
Pang
0a4ac6abb7 perf: 优化系统参数 2024-01-17 23:22:13 +08:00
Pang
96b0cb8aa0 perf: 优化菜单显示位置 2024-01-16 11:15:19 +08:00
Pang
b3a30720fa build 2024-01-16 00:56:30 +08:00
kuaifan
b711605bdc perf: 优化获取最近消息 2024-01-16 00:50:53 +08:00
kuaifan
31efee2e97 perf: 优化请求时间 2024-01-15 22:11:24 +08:00
kuaifan
569af135bd no message 2024-01-15 21:27:02 +08:00
kuaifan
2975a0eaf9 perf: 优化触摸长按和右键菜单共存 2024-01-15 21:06:14 +08:00
kuaifan
d4ee87f324 fix: 部分机型首次打开聊天窗口不显示聊天记录的问题 2024-01-15 21:05:40 +08:00
kuaifan
c676a3037c build 2024-01-15 14:26:53 +08:00
kuaifan
e4790062c8 no message 2024-01-15 14:15:35 +08:00
Pang
bb8a6982d0 build 2024-01-15 08:58:38 +08:00
Pang
80af98111b perf: 优化发送消息接口 2024-01-14 20:12:21 +08:00
Pang
9a69d20949 perf: 优化搜索提示 2024-01-14 15:07:43 +08:00
Pang
5e52996a9e pref: 优化消息列表 2024-01-14 14:28:29 +08:00
Pang
33d22d4970 build 2024-01-14 01:19:13 +08:00
Pang
170473fb2d Merge commit 'c1b63af5f5408d5ce9c1d45560480a6fd4bd52a9' into pro 2024-01-13 23:54:59 +08:00
Pang
67ccaea41e no message 2024-01-13 23:54:39 +08:00
Pang
67d7e81ffa no message 2024-01-13 23:54:30 +08:00
Pang
1788b40431 fix: 重复通知 2024-01-13 23:47:13 +08:00
Pang
7f432cefb9 perf: 优化消息保存覆盖 2024-01-13 22:42:30 +08:00
Pang
57e8c9c7cd pref: 优化消息列表 2024-01-13 22:26:48 +08:00
weifashi
c1b63af5f5 fix: 修复投票进度的算法 2024-01-13 22:12:06 +08:00
Pang
cf7f245a49 perf: 优化消息保存覆盖 2024-01-13 00:14:21 +08:00
Pang
4824f30950 no message 2024-01-13 00:07:06 +08:00
Pang
88fb1d8e62 perf: 优化消息保存覆盖 2024-01-12 23:48:22 +08:00
Pang
e67ce9a438 perf: 优化快捷表情发送消息时关闭延迟的问题 2024-01-12 23:48:22 +08:00
Pang
976b9690d2 pref: 优化消息列表 2024-01-12 23:48:22 +08:00
weifashi
36735ace50 perf: 升级okr 2024-01-12 10:04:13 +08:00
kuaifan
3aeea13526 build 2024-01-11 23:52:25 +08:00
kuaifan
6f33c3f5d6 Merge commit 'e53242613b634916bb2f7939bc260ee683c2236e' into pro 2024-01-11 23:48:39 +08:00
kuaifan
53aab1ed0f build 2024-01-11 23:43:59 +08:00
kuaifan
b209040978 no message 2024-01-11 22:45:10 +08:00
kuaifan
e74aeb9393 no message 2024-01-10 23:14:42 +08:00
weifashi
e53242613b perf: 年度报告接口 - 查询条件优化 2024-01-10 18:59:47 +08:00
kuaifan
bea7ba00f0 no message 2024-01-10 14:23:38 +08:00
weifashi
24d90b93e2 perf: 升级okr, ai 2024-01-10 01:20:54 +08:00
kuaifan
f380b0433d build 2024-01-09 20:21:12 +08:00
kuaifan
f7df6408ed no message 2024-01-09 20:19:56 +08:00
kuaifan
10a77ee2a9 Merge commit '5a44076859d807d61aa44a06364c619a6b877f07' into pro 2024-01-09 18:57:52 +08:00
kuaifan
d5db894891 no message 2024-01-09 18:57:36 +08:00
weifashi
5a44076859 perf: 年度汇报接口返回用户头像 2024-01-09 17:26:03 +08:00
kuaifan
e78513cb80 no message 2024-01-09 14:44:13 +08:00
Pang
2860c4cbe6 no message 2024-01-09 07:54:12 +08:00
weifashi
ebce9fa596 feat: 更新okr 2024-01-08 22:58:36 +08:00
kuaifan
8080d0bb4e no message 2024-01-08 19:10:29 +08:00
kuaifan
221e42d02b no message 2024-01-08 17:20:32 +08:00
kuaifan
e06fd21a4b no message 2024-01-08 16:29:08 +08:00
kuaifan
f42036c104 no message 2024-01-08 12:30:56 +08:00
weifashi
937bc4ead3 perf: 年度报告接口 - 增加用户信息字段返回 2024-01-08 09:32:28 +08:00
weifashi
322a855ba2 perf: 去掉未使用的引用 2024-01-08 01:26:37 +08:00
weifashi
7c4d537d67 perf: 去掉未使用的引用 2024-01-08 01:25:44 +08:00
weifashi
b78e4240cb feat: 添加年度报告接口 2024-01-08 01:22:58 +08:00
weifashi
4f663dd761 feat: 添加年度报告接口 2024-01-08 01:20:10 +08:00
Pang
b3bd5aded5 perf: 优化滑动返回动画效果 2024-01-08 00:25:37 +08:00
Pang
7714c53085 perf: 消息置顶滚动恢复 2024-01-08 00:15:35 +08:00
kuaifan
3a74cdc98b perf: 消息置顶滚动恢复 2024-01-07 14:26:42 +08:00
kuaifan
3631f511d4 perf: 优化消息Load效果 2024-01-07 14:25:35 +08:00
Pang
5f7d528d9d build 2024-01-05 07:02:46 +08:00
Pang
85ceb8b938 Merge commit 'fb8d759103d8468e6ef51f18d1c5f31a2b3e6a89' into pro 2024-01-05 06:55:29 +08:00
Pang
b4b268a4d7 no message 2024-01-05 06:55:15 +08:00
Pang
4b39f13fa9 build 2024-01-05 00:42:08 +08:00
Pang
4abcec08f4 perf: 消息首次加载数据优化 2024-01-05 00:38:52 +08:00
Pang
4144f92631 fix: 消息阅读回馈 2024-01-04 20:14:26 +08:00
weifashi
fb8d759103 perf: 优化打包下载 2024-01-04 12:02:29 +08:00
kuaifan
e215cda700 build 2024-01-04 00:07:27 +08:00
kuaifan
846fdcf145 perf: 优化打包下载 2024-01-03 20:48:49 +08:00
kuaifan
ecdabc668d Merge commit 'ada88a1c02740846547eb2c3eaf37639006b3b57' into pro 2024-01-03 19:34:27 +08:00
kuaifan
e8839974d4 no message 2024-01-03 19:23:18 +08:00
kuaifan
2a864b6617 no message 2024-01-03 18:50:27 +08:00
weifashi
ada88a1c02 perf: 去掉无用引用 2024-01-03 15:09:36 +08:00
weifashi
8fe16416f9 perf: 优化报告未读接口 2024-01-03 15:07:47 +08:00
kuaifan
0daf06c06d no message 2024-01-03 14:29:42 +08:00
Pang
3b697e7400 no message 2024-01-02 11:45:01 +08:00
Pang
a543f8716b perf: 优化图片显示 2023-12-30 16:16:58 +08:00
kuaifan
63703a029f perf: 优化代码 2023-12-30 02:11:32 +08:00
weifashi
22415e6c61 fix: 修复置顶人员 2023-12-29 17:05:57 +08:00
kuaifan
1a69e76fe7 build 2023-12-29 16:39:26 +08:00
kuaifan
7f916c4770 perf: 优化代码 2023-12-29 16:39:07 +08:00
weifashi
f76d36a74b perf: 代码整理 2023-12-29 11:45:42 +08:00
weifashi
ab0539a263 perf: 优化待审批流程数量接口 2023-12-29 00:35:35 +08:00
weifashi
4104dea68e fix: 修复高危bug 2023-12-28 23:15:22 +08:00
weifashi
5aded9daa3 perf: 代码优化 2023-12-28 22:10:56 +08:00
weifashi
91d5bd80ff Merge branch 'kuaifan/pro' into pro 2023-12-28 22:08:52 +08:00
kuaifan
40d56a0155 build 2023-12-28 21:09:30 +08:00
kuaifan
54117fe51a perf: 优化未读消息提示 2023-12-28 20:48:23 +08:00
kuaifan
fbd662e400 perf: 优化预览消息 2023-12-28 20:48:00 +08:00
kuaifan
ccb31a81f8 perf: 优化缓存数据 2023-12-28 17:44:17 +08:00
weifashi
dbb9366de6 perf: 代码优化 2023-12-28 12:02:39 +08:00
weifashi
6d7a4edae3 perf: 代码整理 2023-12-26 23:54:12 +08:00
weifashi
632068a74c perf: 任务可见性用户 - 分表优化 2023-12-26 23:28:32 +08:00
weifashi
4e78920f99 perf: 代码命名优化 2023-12-26 20:53:59 +08:00
weifashi
fdc85bbcbf fix: 1.修复可见效数据取值,2.修复设置可见效指定人员不成功 2023-12-26 20:52:03 +08:00
weifashi
67dafae9d6 perf: 移动任务后,对应项目路径也要更改显示 2023-12-26 17:16:57 +08:00
weifashi
989e5a5f9d perf: 升级okr容器 2023-12-26 17:06:35 +08:00
weifashi
a7e5bd0b80 perf: 导出任务统计 - 下载地址换成按钮 2023-12-26 16:58:12 +08:00
weifashi
da131746be Merge branch 'pro' into wfs-msg-top
# Conflicts:
#	resources/assets/js/pages/manage/components/DialogWrapper.vue
#	resources/assets/sass/components/user-select.scss
#	resources/assets/sass/pages/components/dialog-wrapper.scss
2023-12-26 16:29:08 +08:00
kuaifan
8a7e80fe86 build 2023-12-26 00:24:26 +08:00
kuaifan
865dc61cd1 fix: 修复已知问题 2023-12-25 22:19:42 +08:00
kuaifan
c8b96a8bce build 2023-12-25 14:15:52 +08:00
kuaifan
5546dbaa0e perf: 优化会话列表 2023-12-25 14:15:45 +08:00
Pang
fd6312408b build 2023-12-24 21:21:18 +08:00
Pang
4f4c6de8a2 perf: 优化录音load效果 2023-12-24 20:28:02 +08:00
Pang
4506ba8cd3 perf: 优化消息列表 2023-12-24 19:30:36 +08:00
Pang
9300e9fd9a perf: 优化应用图标 2023-12-24 11:36:20 +08:00
Pang
a4eb8317da fix: 撤回消息不删除消息的情况 2023-12-24 11:04:19 +08:00
Pang
0e819de1bc perf: 升级okr容器 2023-12-24 10:47:30 +08:00
Pang
9800f9e3da perf: 优化用户选择器 2023-12-24 10:47:07 +08:00
Pang
a0f6a17005 perf: 优化对话列表接口数据 2023-12-24 10:46:50 +08:00
Pang
6087c7fed0 perf: 优化未读消息提示动画 2023-12-23 19:24:30 +08:00
Pang
3fa0b472d2 perf: 优化消息更新机制 2023-12-23 19:20:41 +08:00
Pang
1ce96ddae6 perf: 优化缓存 2023-12-23 12:19:35 +08:00
kuaifan
d4ef140c8e perf: 优化用户选择器 2023-12-23 11:58:54 +08:00
kuaifan
7de575e236 no message 2023-12-22 12:26:51 +08:00
kuaifan
f0f0883a88 build 2023-12-22 11:24:36 +08:00
kuaifan
c1695a78d6 perf: 优化任务修改 2023-12-22 11:09:43 +08:00
weifashi
15e37eded3 fix: 更新导致的小问题 2023-12-22 10:01:27 +08:00
Pang
57cd91e6d4 no message 2023-12-22 09:07:32 +08:00
Pang
a178334d8e build 2023-12-21 23:56:11 +08:00
Pang
dd8ba7e8da fix: 更新导致的小问题 2023-12-21 23:43:50 +08:00
weifashi
d26df91960 fix: 版本验证有问题,先干掉 2023-12-21 20:29:56 +08:00
kuaifan
f249763d41 build 2023-12-21 19:54:24 +08:00
kuaifan
1bada9ab30 no message 2023-12-21 19:14:56 +08:00
kuaifan
a185ab2973 perf: 优化发送消息时闪现2条一样的情况 2023-12-21 19:14:56 +08:00
kuaifan
ce83bef0ed perf: 优化消息首页加载效果 2023-12-21 19:14:56 +08:00
kuaifan
66135d8222 perf: 优化Android长按事件 2023-12-21 19:14:56 +08:00
Pang
e99e069e55 fix: android 无法回删输入框内的@(mention)内容 2023-12-21 19:14:56 +08:00
Pang
327cdbc873 fix: android 长按重复事件 2023-12-21 19:14:56 +08:00
kuaifan
6eabba9679 perf: 优化输入框自动高度 2023-12-21 19:14:56 +08:00
kuaifan
c99f6cfcf2 perf: 点击消息页面会发生跳动的问题 2023-12-21 19:14:56 +08:00
kuaifan
0579a73c1c perf: 优化待办列表 2023-12-21 19:14:56 +08:00
kuaifan
12b3c14299 perf: 调整任务过多提示范围 2023-12-21 19:14:56 +08:00
kuaifan
c21da4292b perf: 优化消息阅读规则 2023-12-21 19:14:56 +08:00
weifashi
3f9cdfd887 perf: okr版本升级 2023-12-21 19:07:14 +08:00
weifashi
8dac2bc444 Merge branch 'okr' into pro 2023-12-21 18:39:00 +08:00
weifashi
13ec6ec323 perf: 1.数据库迁移文件修复 2.转发样式优化 2023-12-21 17:30:27 +08:00
weifashi
59aa854470 feat: 消息置顶功能 - 50% 2023-12-21 17:20:42 +08:00
ganzizi
e0c3ea4456 perf: 兼容okr1.1版本 2023-12-20 17:54:26 +08:00
ganzizi
d6a3727713 perf: 兼容okr1.1版本 2023-12-20 10:34:15 +08:00
weifashi
48cd32742c perf: 整体数据库索引和字段类型优化 2023-12-18 18:16:04 +08:00
weifashi
852ceba828 fix: 合并修复 2023-12-18 15:15:35 +08:00
weifashi
905c8be6eb Merge branch 'pro' into okr
# Conflicts:
#	resources/assets/js/pages/manage/application.vue
#	resources/assets/js/pages/manage/dashboard.vue
2023-12-18 15:09:13 +08:00
weifashi
fad98dcc9d Merge branch 'pro' into kuanfan-pro 2023-12-18 14:53:33 +08:00
weifashi
8b5409de5a perf: 项目列表数据库查询优化 2023-12-17 21:42:00 +08:00
Pang
bcf1ad0870 build 2023-12-17 16:37:12 +08:00
Pang
617e88e0b5 no message 2023-12-17 16:32:34 +08:00
Pang
c9e0840173 perf: 优化任务列表查询速度 2023-12-17 16:27:43 +08:00
Pang
5e4f99da6c perf: 优化消息输入框内选择文本 2023-12-17 16:26:58 +08:00
Pang
28bc303fcf perf: 移动端修改任务详情确认提示 2023-12-17 16:25:44 +08:00
Pang
91c63f281b perf: 优化发送录音消息抖动 2023-12-17 16:24:23 +08:00
Pang
7b3769b1db perf: 优化录音效果 2023-12-17 16:19:48 +08:00
Pang
211f9f0c15 perf: 优化快捷键设置 2023-12-17 16:19:32 +08:00
Pang
37ccf4dacb fix: 修复头像出现D的情况 2023-12-17 16:18:28 +08:00
Pang
971167cad3 doc: 更新文档 2023-12-16 23:39:03 +08:00
Pang
332bed3136 build 2023-12-16 23:01:19 +08:00
Pang
e2a9906de0 fix: 聊天输入框内容为空时仍可以长安发送显示发送菜单 2023-12-16 22:59:25 +08:00
Pang
c5879e4376 fix: 文件页移动端滑动返回失败情况 2023-12-16 22:58:28 +08:00
Pang
22324f4c16 build 2023-12-16 22:28:52 +08:00
Pang
fa9c3b4f2f perf: 优化输入空换行时的兼容问题 2023-12-16 22:19:55 +08:00
Pang
f411f17386 perf: 优化设置页面 2023-12-16 21:22:26 +08:00
Pang
ab3a82300c perf: 优化应用中心菜单排序 2023-12-16 21:22:06 +08:00
Pang
dbb9162267 perf: 更新录音插件 2023-12-16 21:11:20 +08:00
Pang
84d3e4f617 perf: 更换移动任务图标 2023-12-16 12:22:22 +08:00
Pang
6209b53321 perf: 优化设置返回跟滑动返回冲突 2023-12-16 12:05:57 +08:00
Pang
62a2bcf71d perf: 优化键盘设置 2023-12-16 12:05:19 +08:00
Pang
cdefe9d4a7 perf: 优化清除缓存数据 2023-12-16 10:36:33 +08:00
Pang
2bebad1112 perf: 优化阅读消息列表机制 2023-12-16 01:49:01 +08:00
Pang
9f186f1e9c perf: 优化项目页面任务加载速度 2023-12-15 22:51:20 +08:00
Pang
a6873302f3 fix: 会员头像显示错乱 2023-12-15 22:50:30 +08:00
weifashi
615f40d458 perf: 代码优化 2023-12-15 16:24:43 +08:00
kuaifan
c4b49b34b8 build 2023-12-15 13:25:43 +08:00
kuaifan
d12fb47902 perf: 优化消息阅读逻辑 2023-12-15 13:20:05 +08:00
weifashi
21132f475a perf: 微应用优化 2023-12-15 11:22:35 +08:00
weifashi
a55e0a457d perf: 微应用优化 2023-12-15 11:20:51 +08:00
kuaifan
fa7b049316 build 2023-12-15 00:17:16 +08:00
kuaifan
f8f5bc476b perf: 优化未读消息机制 2023-12-15 00:14:48 +08:00
kuaifan
e5c622cb89 perf: 优化重连时消息列表跳回第一页的情况 2023-12-14 22:29:52 +08:00
kuaifan
aa70c41041 build 2023-12-14 21:25:23 +08:00
kuaifan
3cce9b67d4 feat: 新增以下为新消息提示 2023-12-14 21:05:23 +08:00
kuaifan
c0342ea6d1 perf: 优化未读消息机制 2023-12-14 19:56:06 +08:00
weifashi
2813f4c062 feat: okr结果分析 - 部门负责人也可以看 2023-12-14 16:34:43 +08:00
kuaifan
9ca9de0d7e perf: 优化消息更新太快导致不更新数据的情况 2023-12-14 16:30:22 +08:00
kuaifan
603db9de7f fix: 修复重复SSE请求的问题 2023-12-14 16:00:54 +08:00
kuaifan
c4e72507e0 perf: 机器人添加清空上下文菜单 2023-12-14 15:59:04 +08:00
kuaifan
d06d1c177c fix: 部分pad设备横版和竖屏反过来 2023-12-14 15:18:02 +08:00
kuaifan
4ff1cf68fc perf: 优化翻译 2023-12-14 15:08:07 +08:00
kuaifan
8d92933e43 perf: 发送消息失败时再次编辑改为重新发送 2023-12-14 14:43:05 +08:00
kuaifan
9497fb1bb6 perf: 优化通过消息设置待办功能 2023-12-14 14:42:11 +08:00
kuaifan
fc65d56977 perf: 优化扫一扫登录功能 2023-12-14 13:55:11 +08:00
kuaifan
fe4cba61e2 perf: 优化头像 2023-12-14 13:17:49 +08:00
weifashi
8144bea613 fix: 用户选择组件,单选时不需要显示项目 2023-12-13 18:29:08 +08:00
weifashi
7a431d86d2 perf: 兼容okr1.1版本 2023-12-11 19:03:11 +08:00
ganzizi
7ecfd86ffa perf: 兼容okr1.1版本 2023-12-11 17:47:50 +08:00
weifashi
66b9e7e9b3 feat: okr1.1 兼容开发 2023-12-11 15:17:43 +08:00
ganzizi
6bed109f97 perf: 兼容okr1.1版本 2023-12-11 10:13:36 +08:00
weifashi
fbc5eed5c5 perf: 接龙和投票的样式优化 2023-12-11 09:28:02 +08:00
weifashi
43b665652e perf: 逻辑强化 2023-12-10 18:49:49 +08:00
weifashi
5760d3ef0f fix: 文件主题修复 2023-12-10 18:42:49 +08:00
weifashi
e712b99287 feat: 未读消息优化 2023-12-10 15:47:54 +08:00
weifashi
85d88b6800 perf: 未读消息优化 2023-12-08 22:09:47 +08:00
weifashi
1a62a47935 fix: 修复客户端版本更新按钮的显示问题 2023-12-08 21:14:49 +08:00
kuaifan
689d842d58 no message 2023-12-08 21:14:03 +08:00
kuaifan
8215e73a95 perf: 搜索消息时按esc取消搜索 2023-12-08 21:13:58 +08:00
weifashi
d3fc274f08 Merge branch 'price' into pro 2023-12-08 19:02:02 +08:00
weifashi
e4bcb8b518 feat: 翻译 2023-12-08 19:01:03 +08:00
weifashi
9a942c483d feat: 添加投票功能 - 100% 2023-12-08 18:05:40 +08:00
weifashi
e9fd223808 feat: 样式调优 2023-12-08 01:52:03 +08:00
weifashi
5dfc66fc21 feat: 添加投票功能 30% 2023-12-08 01:42:59 +08:00
weifashi
bab82dc290 perf: 接龙优化 2023-12-07 21:22:38 +08:00
weifashi
4c1125b9e1 perf: 接龙优化 2023-12-07 21:19:12 +08:00
weifashi
85ef2d9687 feat: 接龙功能 - 100% 2023-12-07 20:26:32 +08:00
kuaifan
b7fc815d58 perf: 移动设备优化消息输入框菜单 2023-12-07 18:31:25 +08:00
kuaifan
ad1cc964c9 perf: 优化消息输入框@所有人暗黑样式 2023-12-07 18:11:39 +08:00
kuaifan
96a2b250a3 perf: 优化@人名换行的情况 2023-12-07 18:11:14 +08:00
weifashi
d72ab58f98 feat: 添加接龙 2023-12-07 00:49:39 +08:00
weifashi
abd453f2f6 perf: 样式优化 2023-12-06 21:18:14 +08:00
weifashi
4b7283dbe8 feat: 1.任务移动功能优化,2.导航样式优化 2023-12-06 20:08:25 +08:00
ganzizi
f5a068fffc perf: 优化导出任务统计 2023-12-06 11:57:29 +08:00
ganzizi
faf5dec08a feat: 新增压缩下载完成后系统机器人提醒 2023-12-05 18:55:50 +08:00
weifashi
e4070e249d feat: 添加一个 @我的 消息标签 2023-12-05 11:11:06 +08:00
weifashi
fe5ec9677a feat: 转发消息 - 添加单选模式 2023-12-05 11:00:34 +08:00
weifashi
5fdd5adef8 feat: 转发消息 - 添加来源显示 2023-12-04 18:45:42 +08:00
weifashi
bc250ad4b8 perf: 样式调优 2023-12-04 11:28:14 +08:00
weifashi
22050b7488 feat: 翻译 2023-12-01 19:30:51 +08:00
weifashi
6df906aa24 perf: 客户端下载按钮,仪表盘不显示 2023-12-01 19:21:20 +08:00
weifashi
d2f20128bb perf: 未读消息优化 2023-12-01 18:17:12 +08:00
weifashi
cef19488d2 perf: 细节优化 2023-12-01 14:52:50 +08:00
weifashi
0ceb2de79d fix: 标注取值bug修复 2023-11-29 18:03:53 +08:00
weifashi
79feaaf801 feat: 1.修复任务详情有图片时,无修改点击离开依然会保存的bug
2.修复任务详情图片过大时保存不成功的bug
	3.添加全局标注
2023-11-29 16:57:02 +08:00
weifashi
34c005001d Merge branch 'gzc-userTaskPermission' into pro 2023-11-28 15:45:35 +08:00
weifashi
3508d7a472 fix: 项目权限 - 100% 2023-11-28 15:40:03 +08:00
weifashi
1e58587b1c fix: 1. lisence 具体项不对的时候提醒
2. 从日历页面修改任务时间,就算时间不变也提示修改了
3. android滑动返回有问题,会文件页面循环返回
4. SSEClient 连接失败后死循环
2023-11-27 14:55:12 +08:00
weifashi
e99f952c28 feat: 首页改版 - 100% 2023-11-24 18:53:10 +08:00
ganzizi
b0742021b6 feat: 新增项目任务创建权限功能 - 90% 2023-11-24 18:25:28 +08:00
weifashi
5e784f64a6 feat: 更换calendar 2023-11-24 01:50:51 +08:00
weifashi
32aae08ef2 feat: 首页改版 2023-11-23 19:12:14 +08:00
weifashi
af46fc501b fix: 修复安装项目报错 2023-11-21 18:45:57 +08:00
weifashi
07c3a958fa build 2023-11-20 17:27:28 +08:00
weifashi
ecdbf8765f feat: 添加项目权限功能 - 30% 2023-11-20 17:16:10 +08:00
weifashi
5682943c24 perf: 审批版本更新 2023-11-20 17:12:11 +08:00
weifashi
5fbc5d3164 perf: Okr 和 审批中心弄一些演示数据 2023-11-20 15:30:51 +08:00
ganzizi
90af11a842 fix: 修复打包下载问题 2023-11-20 15:29:23 +08:00
weifashi
b644a65f22 fix: 1. 修复 windows端 右键发送 是直接发送了,没有出现使用md格式发送 2.其他bug修复 2023-11-20 15:19:29 +08:00
ganzizi
6db82c8176 fix: 修复打包下载问题 2023-11-20 11:44:35 +08:00
ganzizi
fa149fcaa9 fix: 修复统一打包下载命名 2023-11-20 10:59:46 +08:00
weifashi
9772e4b48a perf: 项目邀请页 - 安卓用opne方式打开 2023-11-20 09:26:10 +08:00
OldTT
420e1a9d63 提交功能对比清单 2023-11-17 17:09:16 +08:00
560 changed files with 46570 additions and 6666 deletions

View File

@@ -10,6 +10,7 @@ APP_URL=http://localhost
APP_ID=
APP_IPPR=
APP_PORT=2222
APP_SSL_PORT=
APP_DEV_PORT=
LOG_CHANNEL=stack
@@ -56,9 +57,6 @@ PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
JUKE_KEY_JOKE=
JUKE_KEY_SOUP=
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

View File

@@ -1,4 +1,4 @@
name: Publish Desktop
name: Publish Mac
on:
push:
@@ -6,7 +6,7 @@ on:
- 'v*'
jobs:
build:
Build:
runs-on: macos-latest
environment: build
@@ -15,10 +15,10 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Use Node.js 16.x
- name: Use Node.js 20.x
uses: actions/setup-node@v1
with:
node-version: 16.x
node-version: 20.x
- name: Build
env:
@@ -29,5 +29,6 @@ jobs:
DP_KEY: ${{ secrets.DP_KEY }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GH_REPOSITORY: ${{ github.repository }}
run: ./cmd electron all
run: |
./cmd electron mac

View File

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

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
/node_modules
/public/hot
/public/tmp
/public/summary
/public/uploads/*
/public/.well-known
/public/.user.ini

27
.prefetch Normal file
View File

@@ -0,0 +1,27 @@
office/web-apps/apps/api/documents/api.js?hash={version}
office/7.5.1-23/sdkjs/cell/css/main.css
office/7.5.1-23/web-apps/apps/spreadsheeteditor/main/resources/css/app.css
office/7.5.1-23/web-apps/vendor/requirejs/require.js
office/7.5.1-23/web-apps/apps/spreadsheeteditor/main/app.js
office/7.5.1-23/sdkjs/common/AllFonts.js
office/7.5.1-23/web-apps/vendor/xregexp/xregexp-all-min.js
office/7.5.1-23/web-apps/vendor/socketio/socket.io.min.js
office/7.5.1-23/sdkjs/cell/sdk-all-min.js
office/7.5.1-23/sdkjs/cell/sdk-all.js
office/7.5.1-23/sdkjs/common/libfont/engine/fonts.js
office/7.5.1-23/sdkjs/common/Charts/ChartStyles.js
office/7.5.1-23/sdkjs/common/libfont/engine/fonts.wasm
office/7.5.1-23/web-apps/apps/presentationeditor/main/resources/css/app.css
office/7.5.1-23/web-apps/apps/presentationeditor/main/app.js
office/7.5.1-23/sdkjs/slide/sdk-all-min.js
office/7.5.1-23/sdkjs/slide/sdk-all.js
office/7.5.1-23/sdkjs/slide/themes//themes.js
office/7.5.1-23/web-apps/apps/documenteditor/main/resources/css/app.css
office/7.5.1-23/web-apps/apps/documenteditor/main/app.js
office/7.5.1-23/sdkjs/word/sdk-all-min.js
office/7.5.1-23/sdkjs/word/sdk-all.js
drawio/webapp/js/app.min.js
drawio/webapp/js/stencils.min.js
drawio/webapp/js/extensions.min.js
drawio/webapp/js/shapes-14-6-5.min.js
drawio/webapp/math/es5/output/svg/fonts/tex.js

View File

@@ -2,6 +2,477 @@
All notable changes to this project will be documented in this file.
## [0.36.26]
### Bug Fixes
- 关闭文件后无法再次打开
### Performance
- 优化数据结构
- 接龙优化为清空内容默认删除
- 支持取消发送中的消息
- 1. 强化接龙接口本地时间戳问题 2. 接龙消息点展开按钮后做缓存处理
## [0.36.15]
### Bug Fixes
- 消息太长导致菜单无法正常显示
- 项目数量不正确的情况
- 部分未读和待办信息不显示的情况
- 审批中心修复loadIng效果
### Performance
- 自动识别md格式发送
- 优化回复、转发消息数据结构
- 优化iOS端数据读取失败的情况
- 回复消息时自动@提及
- 优化会话数据结构
## [0.35.90]
### Bug Fixes
- 子窗口出现重新登录的情况
### Performance
- 聊天文件发送进度
- 拨打电话确认提示
- 优化预加载文件
## [0.35.84]
### Bug Fixes
- 回复消息点击到原文无效
### Performance
- 优化接口时间
- 优化审批对话按钮配色
## [0.35.76]
### Bug Fixes
- 客户端无法打开excel文件
- 修复投票实名逻辑
### Features
- 升级okr
### Performance
- 优化文件功能按钮
- 文件上传支持覆盖上传
- 优化app等比显示
- 优化发送文件预览
- 消息发送中禁止右键菜单
- 部分搜索框图标抖动
- 优化复制功能
- 优化pdf文件预览
- 优化投票接口,加上事务锁
- 优化接龙接口,加上事务锁
- 优化转发消息样式
- 接龙接口-强化排序
- 审批按钮色微调
- 统一审批中心的按钮色
- 转发会议亮色皮肤问题,转发文件宽度铺满
- 优化app数据交互
### Styling
- 调整代码格式
## [0.35.48]
### Bug Fixes
- 修复搜索偶尔无效的情况
### Features
- 升级okr
### Performance
- 优化文件预览
- 滑动列表自动隐藏键盘
- 优化时间格式
- 适配nodejs 20
## [0.35.40]
### Bug Fixes
- 部分手机出现非正常滚动到底部的情况
- Dootask官网标题
- Dootask官网调整
- 修复导出任务统计没有按创建时间来的bug
### Features
- 新增不显示会话功能
- 升级okr
### Performance
- 修改消息换行优化
- 优化预加载资源
- 优化pdf文件预览
- 优化签到消息
- 优化登录
- 优化安装脚本
- 优化消息时间格式
- 优化app功能
## [0.35.20]
### Features
- 发起投票功能添加缓存记录选中效果
- 导出的签到数据和审批数据换成xlsx因老版本的xls会出现兼容性问题
### Performance
- 默认映射443端口
- 优化子窗口
- 优化发布接口 删除目录的逻辑
## [0.35.10]
### Bug Fixes
- 修改消息导致最后消息改变
- 显示无关系的子任务、指定成员可见消息推送
- 调整gemini机器人设置参数
### Performance
- 更新说明文档
- 优化客户端
- 默认关闭端到端加密传输
## [0.34.95]
### Bug Fixes
- 调整发送参数逻辑
- 推送标题存在换行时不显示
- 修复下载文件大小为0时报错
- 更改其他版本的链接
- 修复价格页面样式
- 发布接口,调整缓存时间为两小时
- 修复okr定时处理信息不发送
- 项目已归档,任务面板也没有这三个任务,但是每次新增报告,都会弹任务出来
- 修改下载页面按钮布局样式
### Features
- 升级okr
- 优化内置浏览器
- Gemini机器人添加代理参数
- 发布接口只保留最近两个版本
- 官网添加其他版本的按钮
- 统一表为utf8mb4_unicode_ci
### Performance
- 优化ai机器人
- Okr和审批优化
- 优化pdf在线预览
- 优化客户端打开服务器链接
- 签到设置,有些客户服务器安全体系会拦截 curl -sSL 关键字优化为cmd不传值
- 签到设置,有些客户服务器安全体系会拦截 curl -sSL 关键字优化为base64返回
## [0.34.66]
### Performance
- 升级okr容器
- 新增禁止私聊、群聊功能
- 更换笑话和鸡汤接口
## [0.34.59]
### Bug Fixes
- 部分机型首次打开聊天窗口不显示聊天记录的问题
### Performance
- 修复一些问题
- IOS打开键盘时看不见通知的情况
- 优化系统参数
- 优化菜单显示位置
- 优化获取最近消息
- 优化请求时间
- 优化触摸长按和右键菜单共存
## [0.34.46]
### Bug Fixes
- 重复通知
- 修复投票进度的算法
### Performance
- 优化发送消息接口
- 优化搜索提示
- 优化消息列表
- 优化消息保存覆盖
- 优化快捷表情发送消息时关闭延迟的问题
- 升级okr
## [0.34.28]
### Features
- 更新okr
- 添加年度报告接口
### Performance
- 年度报告接口 - 查询条件优化
- 升级okr, ai
- 年度汇报接口返回用户头像
- 年度报告接口 - 增加用户信息字段返回
- 去掉未使用的引用
- 优化滑动返回动画效果
- 消息置顶滚动恢复
- 优化消息Load效果
## [0.33.98]
### Bug Fixes
- 消息阅读回馈
### Performance
- 消息首次加载数据优化
- 优化打包下载
## [0.33.91]
### Bug Fixes
- 修复置顶人员
- 修复高危bug
- 1.修复可见效数据取值2.修复设置可见效指定人员不成功
### Performance
- 优化打包下载
- 去掉无用引用
- 优化报告未读接口
- 优化图片显示
- 优化代码
- 代码整理
- 优化待审批流程数量接口
- 代码优化
- 优化未读消息提示
- 优化预览消息
- 优化缓存数据
- 任务可见性用户 - 分表优化
- 代码命名优化
- 移动任务后,对应项目路径也要更改显示
- 升级okr容器
- 导出任务统计 - 下载地址换成按钮
## [0.33.58]
### Bug Fixes
- 修复已知问题
### Performance
- 优化会话列表
## [0.33.54]
### Bug Fixes
- 撤回消息不删除消息的情况
### Performance
- 优化录音load效果
- 优化消息列表
- 优化应用图标
- 升级okr容器
- 优化用户选择器
- 优化对话列表接口数据
- 优化未读消息提示动画
- 优化消息更新机制
- 优化缓存
## [0.33.41]
### Bug Fixes
- 更新导致的小问题
### Performance
- 优化任务修改
## [0.33.34]
### Bug Fixes
- 版本验证有问题,先干掉
- Android 无法回删输入框内的@(mention)内容
- Android 长按重复事件
- 合并修复
### Features
- 消息置顶功能 - 50%
### Performance
- 优化发送消息时闪现2条一样的情况
- 优化消息首页加载效果
- 优化Android长按事件
- 优化输入框自动高度
- 点击消息页面会发生跳动的问题
- 优化待办列表
- 调整任务过多提示范围
- 优化消息阅读规则
- Okr版本升级
- 1.数据库迁移文件修复 2.转发样式优化
- 兼容okr1.1版本
- 整体数据库索引和字段类型优化
- 项目列表数据库查询优化
## [0.32.65]
### Bug Fixes
- 修复头像出现D的情况
### Documentation
- 更新文档
### Performance
- 优化任务列表查询速度
- 优化消息输入框内选择文本
- 移动端修改任务详情确认提示
- 优化发送录音消息抖动
- 优化录音效果
- 优化快捷键设置
## [0.32.55]
### Bug Fixes
- 聊天输入框内容为空时仍可以长安发送显示发送菜单
- 文件页移动端滑动返回失败情况
- 会员头像显示错乱
### Performance
- 优化输入空换行时的兼容问题
- 优化设置页面
- 优化应用中心菜单排序
- 更新录音插件
- 更换移动任务图标
- 优化设置返回跟滑动返回冲突
- 优化键盘设置
- 优化清除缓存数据
- 优化阅读消息列表机制
- 优化项目页面任务加载速度
- 代码优化
## [0.32.35]
### Bug Fixes
- 修复重复SSE请求的问题
- 部分pad设备横版和竖屏反过来
- 用户选择组件,单选时不需要显示项目
- 文件主题修复
- 修复客户端版本更新按钮的显示问题
- 标注取值bug修复
- 项目权限 - 100%
- 修复安装项目报错
### Features
- 新增以下为新消息提示
- Okr结果分析 - 部门负责人也可以看
- Okr1.1 兼容开发
- 未读消息优化
- 翻译
- 添加投票功能 - 100%
- 样式调优
- 添加投票功能 30%
- 接龙功能 - 100%
- 添加接龙
- 1.任务移动功能优化2.导航样式优化
- 新增压缩下载完成后系统机器人提醒
- 添加一个 @我的 消息标签
- 转发消息 - 添加单选模式
- 转发消息 - 添加来源显示
- 首页改版 - 100%
- 新增项目任务创建权限功能 - 90%
- 更换calendar
- 首页改版
### Performance
- 优化消息阅读逻辑
- 微应用优化
- 优化未读消息机制
- 优化重连时消息列表跳回第一页的情况
- 优化消息更新太快导致不更新数据的情况
- 机器人添加清空上下文菜单
- 优化翻译
- 发送消息失败时再次编辑改为重新发送
- 优化通过消息设置待办功能
- 优化扫一扫登录功能
- 优化头像
- 兼容okr1.1版本
- 接龙和投票的样式优化
- 逻辑强化
- 搜索消息时按esc取消搜索
- 接龙优化
- 移动设备优化消息输入框菜单
- 优化消息输入框@所有人暗黑样式
- 优化@人名换行的情况
- 样式优化
- 优化导出任务统计
- 客户端下载按钮,仪表盘不显示
- 细节优化
## [0.32.17]
### Bug Fixes
- 修复打包下载问题
- 1. 修复 windows端 右键发送 是直接发送了没有出现使用md格式发送 2.其他bug修复
- 修复统一打包下载命名
### Features
- 添加项目权限功能 - 30%
### Performance
- 审批版本更新
- Okr 和 审批中心弄一些演示数据
- 项目邀请页 - 安卓用opne方式打开
## [0.32.9]
### Bug Fixes
@@ -28,7 +499,6 @@ All notable changes to this project will be documented in this file.
- 文件共享只读禁止下载文件
- 保存任务详情至文件的方法 添加失败日志
- 翻译
- 翻译
- 新增实现文件夹下载以及多文件压缩下载功能
- 任务可以筛选未设置时间的
- 临时账号可以主动跟机器人聊天
@@ -37,14 +507,11 @@ All notable changes to this project will be documented in this file.
- Office只读模式隐藏下载按钮
- 优化实现文件夹下载以及多文件压缩下载功能
- 优化实现文件夹下载以及多文件压缩下载功能
- 机器人设置页面,点点点看不到内容,需要给弹窗看详细内容
- 文件选中后,移动端页面宽度放不下对应内容
- 文件选中后,移动端页面宽度放不下对应内容,没有滚动条
- 压缩下载改名打包下载
- 复制链接去除主题语言参数
- 优化实现文件夹下载以及多文件压缩下载功能
- 优化实现文件夹下载以及多文件压缩下载功能
## [0.31.75]
@@ -186,14 +653,12 @@ All notable changes to this project will be documented in this file.
- 去掉test信息
- 按照dootask启动原始尺寸截取使用说明的图
- 修改边栏目录滚动效果
- 修改边栏目录滚动效果
- 官网使用说明的图重新截取更换
- 优化官网布局与样式
- 修复下载英文页面跳转
### Features
- 新增创建聊天关联id
- 新增创建聊天关联id
- Okr信息面板新增"打开OKR"按钮
- 新增OKR信息推送
@@ -280,7 +745,6 @@ All notable changes to this project will be documented in this file.
- 优化会员选择器
- 优化图片压缩
- 回复图片显示图片搜略图
- 优化会员选择器
- 会员选择下拉框提示
## [0.27.26]
@@ -311,7 +775,6 @@ All notable changes to this project will be documented in this file.
- 修复获取聊天列表的接口
- 添加密码账号长度限制
- 添加密码账号长度限制
- Dootask对接系统分享 - 添加头像返回
- 兼容加密bug问题处理
- Dootask对接系统分享
@@ -361,7 +824,6 @@ All notable changes to this project will be documented in this file.
### Bug Fixes
- 审批流程静态页
- 审批流程静态页
### Performance
@@ -1646,7 +2108,6 @@ All notable changes to this project will be documented in this file.
- 优化甘特图
- 优化任务列表切换显示
- 更新icon图标库
- 更新icon图标库
- 已删除任务详情任务描述改为只读
- 已删除任务操作文案及显示优化
- 项目--删除任务查看详情页功能
@@ -1737,7 +2198,6 @@ All notable changes to this project will be documented in this file.
- 报表导出任务没有流程日志判断优化
- 修复登录页设置下拉显示不全的情况
- 处理回滚后异常代码
- 处理回滚后异常代码
- 【系统设置】邮件设置提前小时数双向绑定无效问题修改
### Features
@@ -1825,7 +2285,6 @@ All notable changes to this project will be documented in this file.
- 调整消息置顶标识位置
- 消息列表详情增加'置顶'标识
- 项目列表置顶优化
- 项目列表置顶优化
- 【文件】剪切后加'取消剪切'按钮
- 消息会话右键时隐藏滚动条
- 页面高度足够时只滚动项目部分
@@ -1926,7 +2385,6 @@ All notable changes to this project will be documented in this file.
- 该文件版本已经改变了。该页面将被重新加载
- 点击切换语言一级菜单出现的兼容问题
- 上传文件夹
- 该文件版本已经改变了。该页面将被重新加载
- 团队管理新增身份筛选项
- 任务文件支持更多格式上传
@@ -1964,7 +2422,6 @@ All notable changes to this project will be documented in this file.
- 任务中没有聊天记录时,发送图片无法成功
- 修复消息撤回文字提示在第一条时会被顶部遮住的问题
- 修复个人对话为空时无法重复打开该对话的问题
- 修复消息撤回文字提示在第一条时会被顶部遮住的问题
- MacOS客户端首次不加载角标的问题
- 工作流列表接口用作筛选时不用传多余参数
@@ -2065,7 +2522,6 @@ All notable changes to this project will be documented in this file.
- 工作报告优化
- 规范代码
- 工作报告优化
- 共享文件删除、移动改为仅限所有者或创建者操作
## [0.6.38]
@@ -2345,7 +2801,6 @@ All notable changes to this project will be documented in this file.
### Performance
- 客户端新窗口打开文件
- 客户端窗口标题
- 领取任务流程
- 到期时间格式化
@@ -2358,7 +2813,6 @@ All notable changes to this project will be documented in this file.
- 客户端文件窗口样式
- Iview
- Iview
- 排序箭头颜色
## [0.4.28]

View File

@@ -2,7 +2,7 @@
English | **[中文文档](./README_CN.md)**
- [Screenshot Preview](README_PREVIEW.md)
- [Screenshot preview](./README_PREVIEW.md)
- [Demo site](http://www.dootask.com/)
**QQ Group**
@@ -12,8 +12,9 @@ Group No.: `546574618`
## Setup
- `Docker v20.10+` & `Docker Compose v2.0+` must be installed
- System: `Centos/Debian/Ubuntu/macOS`
- 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
### Deployment (Pro Edition)
@@ -28,7 +29,7 @@ git clone -b pro --depth=1 https://gitee.com/aipaw/dootask.git
# 2、Enter directory
cd dootask
# 3、InstallationCustom port installation: ./cmd install --port 2222
# 3、InstallationCustom port installation, as: ./cmd install --port 80
./cmd install
```
@@ -42,7 +43,8 @@ cd dootask
### Change port
```bash
./cmd port 2222
# This method only replaces the HTTP port. To replace the HTTPS port, please read the SSL configuration below
./cmd port 80
```
### Change App Url
@@ -66,11 +68,13 @@ cd dootask
### Development compilation
- `NodeJs 20+` must be installed
```bash
# Development mode, Mac OS only
# Development
./cmd dev
# Production projects, macOS only
# Production (This is web client. For App/PC/Mac clients, Please read README-CLIENT.md)
./cmd prod
```
@@ -84,11 +88,19 @@ cd dootask
./cmd redis "your command" # To run a redis command
./cmd composer "your command" # To run a composer command
./cmd supervisorctl "your command" # To run a supervisorctl command
./cmd test "your command" # To run a phpunit command
./cmd mysql "your command" # To run a mysql command (backup: Backup database, recovery: Restore database)
```
### NGINX PROXY SSL
### SSL configuration
#### Method 1: Automatic configuration
```bash
# Running commands in a project
./cmd https
```
#### Or Method 2: Nginx Agent Configuration
```bash
# 1、Nginx config add
@@ -96,8 +108,8 @@ proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 2、Running commands in a project
./cmd https
# 2、Running commands in a project (If you unconfigure the NGINX agent, run: ./cmd https close)
./cmd https agent
```
## Upgrade

View File

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

View File

@@ -2,7 +2,7 @@
**[English](./README.md)** | 中文文档
- [截图预览](README_PREVIEW.md)
- [截图预览](./README_PREVIEW.md)
- [演示站点](http://www.dootask.com/)
**QQ交流群**
@@ -12,8 +12,9 @@
## 安装程序
- 必须安装:`Docker v20.10+``Docker Compose v2.0+`
- 支持环境:`Centos/Debian/Ubuntu/macOS`
- 支持环境:`Centos/Debian/Ubuntu/macOS/Windows`
- 硬件建议2核4G以上
- 特别说明Windows 用户请使用 `git bash` 或者 `cmder` 运行命令
### 部署项目Pro版
@@ -28,7 +29,7 @@ git clone -b pro --depth=1 https://gitee.com/aipaw/dootask.git
# 2、进入目录
cd dootask
# 3、一键安装项目自定义端口安装 ./cmd install --port 2222
# 3、一键安装项目自定义端口安装,如:./cmd install --port 80
./cmd install
```
@@ -42,7 +43,8 @@ cd dootask
### 更换端口
```bash
./cmd port 2222
# 此方法仅更换http端口更换https端口请阅读下面SSL配置
./cmd port 80
```
### 更换URL
@@ -66,11 +68,13 @@ cd dootask
### 开发编译
- 请确保你已经安装了 `NodeJs 20+`
```bash
# 开发模式仅限macOS
# 开发模式
./cmd dev
# 编译项目仅限macOS
# 编译项目这是网页端的App/Pc/Mac客户端请查看 README_CLIENT.md
./cmd prod
```
@@ -85,11 +89,19 @@ cd dootask
./cmd redis "your command" # 运行 redis 命令
./cmd composer "your command" # 运行 composer 命令
./cmd supervisorctl "your command" # 运行 supervisorctl 命令
./cmd test "your command" # 运行 phpunit 命令
./cmd mysql "your command" # 运行 mysql 命令 (backup: 备份数据库recovery: 还原数据库)
```
### NGINX 代理 SSL
### SSL 配置
#### 方法1自动配置
```bash
# 在项目下运行命令,根据提示执行即可
./cmd https
```
#### 或者方法2Nginx 代理配置
```bash
# 1、Nginx 代理配置添加
@@ -97,8 +109,8 @@ proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 2、在项目下运行命令
./cmd https
# 2、在项目下运行命令(如果取消 Nginx 代理配置请运行:./cmd https close
./cmd https agent
```
## 升级更新

View File

@@ -22,5 +22,5 @@
## 编译App
1. 执行 `./cmd appbuild``./cmd appbuild setting` 编译
1. 执行 `./cmd appbuild [setting]` 编译
2. 进入 `resources/mobile` eeui框架内打包Android或iOS应用

View File

@@ -19,6 +19,7 @@ use App\Models\UserDepartment;
use App\Models\WebSocketDialogMsg;
use App\Module\BillMultipleExport;
use Hhxsv5\LaravelS\Swoole\Task\Task;
/**
* @apiDefine approve
*
@@ -27,6 +28,7 @@ use Hhxsv5\LaravelS\Swoole\Task\Task;
class ApproveController extends AbstractController
{
private $flow_url = '';
public function __construct()
{
$this->flow_url = env('FLOW_URL') ?: 'http://approve';
@@ -71,7 +73,7 @@ class ApproveController extends AbstractController
{
User::auth();
$data['name'] = Request::input('name');
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/procdef/findAll', json_encode($data));
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/procdef/findAll', json_encode($data));
$procdef = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$procdef || $procdef['status'] != 200 || $ret['ret'] == 0) {
// info($ret);
@@ -98,7 +100,7 @@ class ApproveController extends AbstractController
{
User::auth('admin');
$data['id'] = Request::input('id');
$ret = Ihttp::ihttp_get($this->flow_url.'/api/v1/workflow/procdef/delById?'.http_build_query($data));
$ret = Ihttp::ihttp_get($this->flow_url . '/api/v1/workflow/procdef/delById?' . http_build_query($data));
$procdef = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$procdef || $procdef['status'] != 200) {
return Base::retError($procdef['message'] ?? '删除失败');
@@ -131,7 +133,7 @@ class ApproveController extends AbstractController
//
$var = json_decode(Request::input('var'), true);
$data['var'] = $var;
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/start', json_encode(Base::arrayKeyToCamel($data)));
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/start', json_encode(Base::arrayKeyToCamel($data)));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
return Base::retError($process['message'] ?? '启动失败');
@@ -193,7 +195,7 @@ class ApproveController extends AbstractController
$processInst = $this->getProcessById($data['proc_inst_id']);
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/addGlobalComment', json_encode(Base::arrayKeyToCamel($data)));
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/addGlobalComment', json_encode(Base::arrayKeyToCamel($data)));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
return Base::retError($process['message'] ?? '添加失败');
@@ -201,11 +203,11 @@ class ApproveController extends AbstractController
// 推送通知
$botUser = User::botGetOrCreate('approval-alert');
foreach ( $processInst['userids'] as $id) {
if($id != $user->userid){
foreach ($processInst['userids'] as $id) {
if ($id != $user->userid) {
$dialog = WebSocketDialog::checkUserDialog($botUser, $id);
$processInst['comment_user_id'] = $user->userid;
$processInst['comment_content'] = json_decode($data['content'],true)['content'];
$processInst['comment_content'] = json_decode($data['content'], true)['content'];
$this->approveMsg('approve_comment_notifier', $dialog, $botUser, $processInst, $processInst);
}
}
@@ -237,7 +239,7 @@ class ApproveController extends AbstractController
$data['task_id'] = intval(Request::input('task_id'));
$data['pass'] = Request::input('pass');
$data['comment'] = Request::input('comment');
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/task/complete', json_encode(Base::arrayKeyToCamel($data)));
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/task/complete', json_encode(Base::arrayKeyToCamel($data)));
$task = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$task || $task['status'] != 200) {
return Base::retError($task['message'] ?? '审批失败');
@@ -260,12 +262,12 @@ class ApproveController extends AbstractController
$this->approveMsg('approve_reviewer', $dialog, $botUser, $val, $process, $pass);
}
// 发起人
if($process['is_finished'] == true) {
if ($process['is_finished'] == true) {
$dialog = WebSocketDialog::checkUserDialog($botUser, $process['start_user_id']);
if (!empty($dialog)) {
$this->approveMsg('approve_submitter', $dialog, $botUser, ['userid' => $data['userid']], $process, $pass);
}
}else if ($process['candidate']) {
} else if ($process['candidate']) {
// 下个审批人
$userid = explode(',', $process['candidate']);
$toUser = User::whereIn('userid', $userid)->get()->toArray();
@@ -277,7 +279,7 @@ class ApproveController extends AbstractController
if (empty($dialog)) {
continue;
}
$this->approveMsg('approve_reviewer', $dialog, $botUser, $val, $process,'start');
$this->approveMsg('approve_reviewer', $dialog, $botUser, $val, $process, 'start');
}
}
@@ -291,7 +293,7 @@ class ApproveController extends AbstractController
}
}
}
return Base::retSuccess( $pass == 'pass' ? '已通过' : '已拒绝', $task);
return Base::retSuccess($pass == 'pass' ? '已通过' : '已拒绝', $task);
}
/**
@@ -315,7 +317,7 @@ class ApproveController extends AbstractController
$data['userid'] = (string)$user->userid;
$data['task_id'] = intval(Request::input('task_id'));
$data['proc_inst_id'] = intval(Request::input('proc_inst_id'));
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/task/withdraw', json_encode(Base::arrayKeyToCamel($data)));
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/task/withdraw', json_encode(Base::arrayKeyToCamel($data)));
$task = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$task || $task['status'] != 200) {
return Base::retError($task['message'] ?? '撤回失败');
@@ -365,7 +367,7 @@ class ApproveController extends AbstractController
$data['sort'] = Request::input('sort');
$data['pageIndex'] = intval(Request::input('page'));
$data['pageSize'] = intval(Request::input('page_size'));
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/findTask', json_encode(Base::arrayKeyToCamel($data)));
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/findTask', json_encode(Base::arrayKeyToCamel($data)));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
return Base::retError($process['message'] ?? '查询失败');
@@ -379,7 +381,7 @@ class ApproveController extends AbstractController
}
$val['userimg'] = User::getAvatar($info->userid, $info->userimg, $info->email, $info->nickname);
}
return Base::retSuccess('success',$res);
return Base::retSuccess('success', $res);
}
/**
@@ -403,12 +405,12 @@ class ApproveController extends AbstractController
{
$user = User::auth();
$data['userid'] = (string)$user->userid;
$data['username'] = Request::input('username');
$data['username'] = Request::input('username');
$data['procName'] = Request::input('proc_def_name'); //分类
$data['state'] = intval(Request::input('state')); //状态
$data['pageIndex'] = intval(Request::input('page'));
$data['pageSize'] = intval(Request::input('page_size'));
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/startByMyselfAll', json_encode($data));
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/startByMyselfAll', json_encode($data));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
return Base::retError($process['message'] ?? '查询失败');
@@ -446,7 +448,7 @@ class ApproveController extends AbstractController
$data['userid'] = (string)$user->userid;
$data['pageIndex'] = intval(Request::input('page'));
$data['pageSize'] = intval(Request::input('page_size'));
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/startByMyself', json_encode($data));
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/startByMyself', json_encode($data));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
return Base::retError($process['message'] ?? '查询失败');
@@ -490,7 +492,7 @@ class ApproveController extends AbstractController
$data['pageIndex'] = intval(Request::input('page'));
$data['pageSize'] = intval(Request::input('page_size'));
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/findProcNotify', json_encode($data));
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/findProcNotify', json_encode($data));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
return Base::retError($process['message'] ?? '查询失败');
@@ -525,7 +527,7 @@ class ApproveController extends AbstractController
{
User::auth();
$proc_inst_id = Request::input('proc_inst_id');
$ret = Ihttp::ihttp_get($this->flow_url.'/api/v1/workflow/identitylink/findParticipant?procInstId=' . $proc_inst_id);
$ret = Ihttp::ihttp_get($this->flow_url . '/api/v1/workflow/identitylink/findParticipant?procInstId=' . $proc_inst_id);
$identitylink = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$identitylink || $identitylink['status'] != 200) {
return Base::retError($identitylink['message'] ?? '查询失败');
@@ -568,7 +570,7 @@ class ApproveController extends AbstractController
$data['sort'] = Request::input('sort');
$data['pageIndex'] = intval(Request::input('page'));
$data['pageSize'] = intval(Request::input('page_size'));
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/procHistory/findTask', json_encode(Base::arrayKeyToCamel($data)));
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/procHistory/findTask', json_encode(Base::arrayKeyToCamel($data)));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
return Base::retError($process['message'] ?? '查询失败');
@@ -606,7 +608,7 @@ class ApproveController extends AbstractController
$data['userid'] = (string)$user->userid;
$data['pageIndex'] = intval(Request::input('page'));
$data['pageSize'] = intval(Request::input('page_size'));
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/procHistory/startByMyself', json_encode($data));
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/procHistory/startByMyself', json_encode($data));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
return Base::retError($process['message'] ?? '查询失败');
@@ -650,7 +652,7 @@ class ApproveController extends AbstractController
$data['pageIndex'] = intval(Request::input('page'));
$data['pageSize'] = intval(Request::input('page_size'));
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/procHistory/findProcNotify', json_encode($data));
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/procHistory/findProcNotify', json_encode($data));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
return Base::retError($process['message'] ?? '查询失败');
@@ -685,7 +687,7 @@ class ApproveController extends AbstractController
{
User::auth();
$proc_inst_id = Request::input('proc_inst_id');
$ret = Ihttp::ihttp_get($this->flow_url.'/api/v1/workflow/identitylinkHistory/findParticipant?procInstId=' . $proc_inst_id);
$ret = Ihttp::ihttp_get($this->flow_url . '/api/v1/workflow/identitylinkHistory/findParticipant?procInstId=' . $proc_inst_id);
$identitylink = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$identitylink || $identitylink['status'] != 200) {
return Base::retError($identitylink['message'] ?? '查询失败');
@@ -749,7 +751,7 @@ class ApproveController extends AbstractController
$data['isFinished'] = intval(Request::input('is_finished')); //是否完成
$date = Request::input('date');
$data['startTime'] = $date[0]; //开始时间
$data['endTime'] =Carbon::parse($date[1])->addDay()->toDateString(); //结束时间 + 1天
$data['endTime'] = Carbon::parse($date[1])->addDay()->toDateString(); //结束时间 + 1天
//
if (empty($name) || empty($date)) {
return Base::retError('参数错误');
@@ -761,7 +763,7 @@ class ApproveController extends AbstractController
return Base::retError('日期范围限制最大35天');
}
//
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/findAllProcIns', json_encode($data));
$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'] ?? '查询失败');
@@ -856,7 +858,7 @@ class ApproveController extends AbstractController
BillExport::create()->setTitle($title)->setHeadings($headings)->setData($datas)->setStyles(["A1:Y1" => ["font" => ["bold" => true]]])
];
//
$fileName = '审批记录_' . Base::time() . '.xls';
$fileName = '审批记录_' . Base::time() . '.xlsx';
$filePath = "temp/approve/export/" . date("Ym", Base::time());
$export = new BillMultipleExport($sheets);
$res = $export->store($filePath . "/" . $fileName);
@@ -864,7 +866,7 @@ class ApproveController extends AbstractController
return Base::retError('导出失败,' . $fileName . '');
}
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xls') . ".zip";
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xlsx') . ".zip";
$zipPath = storage_path($zipFile);
if (file_exists($zipPath)) {
Base::deleteDirAndFile($zipPath, true);
@@ -888,7 +890,8 @@ class ApproveController extends AbstractController
}
}
function getStateDescription($state) {
function getStateDescription($state)
{
$state_map = array(
0 => '全部',
1 => '审批中',
@@ -950,13 +953,13 @@ class ApproveController extends AbstractController
}
// 审批记录
$name = $val['username'] . '|';
$call = $val['step'] == 0 ? '发起审批'. '|' : '同意' . '|';
$time =$val['step'] == 0 ? $process['start_time'] . '|' : '';
$call = $val['step'] == 0 ? '发起审批' . '|' : '同意' . '|';
$time = $val['step'] == 0 ? $process['start_time'] . '|' : '';
$comment = $val['step'] == 0 ? '' : ($val['comment'] ?? '') . '|';
$res['approval_record'] .= $name . $call . $time . $comment;
}
}
$res['historical_approver'] = trim(implode(';', $historical_approver), ';');
$res['historical_approver'] = trim(implode(';', $historical_approver), ';');
$res['approved_node'] = $approved_node;
$res['approved_num'] = $approved_num;
$res['historical_agent'] = $res['historical_approver'];
@@ -975,9 +978,9 @@ class ApproveController extends AbstractController
'department' => $process['department'],
'type' => $process['var']['type'],
'start_time' => $process['var']['start_time'],
'start_day_of_week' => '周'.Base::getTimeWeek(Carbon::parse($process['var']['start_time'])->timestamp),
'start_day_of_week' => '周' . Base::getTimeWeek(Carbon::parse($process['var']['start_time'])->timestamp),
'end_time' => $process['var']['end_time'],
'end_day_of_week' => '周'.Base::getTimeWeek(Carbon::parse($process['var']['end_time'])->timestamp),
'end_day_of_week' => '周' . Base::getTimeWeek(Carbon::parse($process['var']['end_time'])->timestamp),
'description' => $process['var']['description'],
'comment_nickname' => $process['comment_user_id'] ? User::userid2nickname($process['comment_user_id']) : '',
'comment_content' => $process['comment_content'] ?? ''
@@ -988,11 +991,11 @@ class ApproveController extends AbstractController
$msg_action = null;
if ($action == 'withdraw' || $action == 'pass' || $action == 'refuse') {
// 任务完成,给发起人发送消息
if($type == 'approve_submitter' && $action != 'withdraw'){
if ($type == 'approve_submitter' && $action != 'withdraw') {
return WebSocketDialogMsg::sendMsg($msg_action, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, true);
}
// 查找最后一条消息msg_id
$msg_action = 'update-'.$toUser['msg_id'];
$msg_action = 'change-' . $toUser['msg_id'];
}
//
try {
@@ -1006,9 +1009,9 @@ class ApproveController extends AbstractController
$proc_msg->save();
}
// 更新工作报告 未读数量
if($type == 'approve_reviewer' && $toUser['userid']){
if ($type == 'approve_reviewer' && $toUser['userid']) {
$params = [
'userid' => [ $toUser['userid'], User::auth()->userid() ],
'userid' => [$toUser['userid'], User::auth()->userid()],
'msg' => [
'type' => 'approve',
'action' => 'unread',
@@ -1027,7 +1030,7 @@ class ApproveController extends AbstractController
public function getProcessById($id)
{
$data['id'] = intval($id);
$ret = Ihttp::ihttp_get($this->flow_url."/api/v1/workflow/process/findById?".http_build_query($data));
$ret = Ihttp::ihttp_get($this->flow_url . "/api/v1/workflow/process/findById?" . http_build_query($data));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
throw new ApiException($process['message'] ?? '查询失败');
@@ -1046,14 +1049,14 @@ class ApproveController extends AbstractController
$val['node_user_list'][$k]['userimg'] = User::getAvatar($info->userid, $info->userimg, $info->email, $info->nickname);
$res['userids'][] = $item['target_id'];
}
}else if($val['aprover_id']){
} else if ($val['aprover_id']) {
$info = User::whereUserid($val['aprover_id'])->first();
$val['userimg'] = $info ? User::getAvatar($info->userid, $info->userimg, $info->email, $info->nickname) : '';
$res['userids'][] = $val['aprover_id'];
}
}
// 全局评论
if(isset($res['global_comments'])){
if (isset($res['global_comments'])) {
foreach ($res['global_comments'] as $k => &$globalComment) {
$info = User::whereUserid($globalComment['user_id'])->first();
if (!$info) {
@@ -1094,7 +1097,7 @@ class ApproveController extends AbstractController
public function getUserProcessParticipantById($id)
{
$data['procInstId'] = intval($id);
$ret = Ihttp::ihttp_get($this->flow_url."/api/v1/workflow/identitylink/findParticipantAll?".http_build_query($data));
$ret = Ihttp::ihttp_get($this->flow_url . "/api/v1/workflow/identitylink/findParticipantAll?" . http_build_query($data));
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
throw new ApiException($process['message'] ?? '查询失败');
@@ -1110,20 +1113,44 @@ class ApproveController extends AbstractController
* @apiGroup system
* @apiName user__status
*
* @apiParam {String} userid
* @apiParam {String} userid
*
* @apiSuccess {String}
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function user__status()
{
$data['userid'] = intval(Request::input('userid'));
$ret = Ihttp::ihttp_get($this->flow_url.'/api/v1/workflow/process/getUserApprovalStatus?'.http_build_query($data));
$procdef = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
$ret = Ihttp::ihttp_get($this->flow_url . '/api/v1/workflow/process/getUserApprovalStatus?' . http_build_query($data));
$procdef = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (isset($procdef['status']) && $procdef['status'] == 200) {
return Base::retSuccess('success', isset($procdef['data']["proc_def_name"]) ? $procdef['data']["proc_def_name"] : '');
}
}
return Base::retSuccess('success', '');
}
/**
* @api {get} api/approve/process/doto 21. 查询需要我审批的流程数量
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup approve
* @apiName process__doto
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function process__doto()
{
$user = User::auth();
$ret = Ihttp::ihttp_get($this->flow_url . '/api/v1/workflow/process/findTaskTotal?userid=' . $user->userid);
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
if (!$process || $process['status'] != 200) {
return Base::retError($process['message'] ?? '查询失败');
}
return Base::retSuccess('success', $process['data']);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,8 @@
namespace App\Http\Controllers\Api;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialog;
use App\Exceptions\ApiException;
use App\Models\AbstractModel;
use App\Models\File;
@@ -11,6 +13,8 @@ use App\Models\FileUser;
use App\Models\User;
use App\Module\Base;
use App\Module\Ihttp;
use Response;
use Session;
use Swoole\Coroutine;
use Carbon\Carbon;
use Redirect;
@@ -695,6 +699,9 @@ class FileController extends AbstractController
* @apiName content__upload
*
* @apiParam {Number} [pid] 父级ID
* @apiParam {Number} [cover] 覆盖已存在的文件
* - 0不覆盖保留两者默认
* - 1覆盖
* @apiParam {String} [files] 文件名
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
@@ -705,8 +712,9 @@ class FileController extends AbstractController
{
$user = User::auth();
$pid = intval(Request::input('pid'));
$overwrite = intval(Request::input('cover'));
$webkitRelativePath = Request::input('webkitRelativePath');
$data = (new File)->contentUpload($user, $pid, $webkitRelativePath);
$data = (new File)->contentUpload($user, $pid, $webkitRelativePath, $overwrite);
return Base::retSuccess($data['data']['name'] . ' 上传成功', $data['addItem']);
}
@@ -973,49 +981,7 @@ class FileController extends AbstractController
}
/**
* @api {get} api/file/download/check 19. 检测下载
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup file
* @apiName download__check
*
* @apiParam {Array} [ids] 文件ID格式: [id, id2, id3]
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function download__check(){
$user = User::auth();
$ids = Request::input('ids');
if (!is_array($ids) || empty($ids)) {
return Base::retError('请选择下载的文件或文件夹');
}
if (count($ids) > 100) {
return Base::retError('一次最多只能下载100个文件或文件夹');
}
$files = [];
$totalSize = 0;
AbstractModel::transaction(function() use ($user, $ids, &$files, &$totalSize) {
foreach ($ids as $k => $id) {
$files[] = File::getFilesTree(intval($id), $user, 1);
$totalSize += $files[$k]->totalSize;
}
});
if ($totalSize > File::zipMaxSize) {
throw new ApiException('文件总大小已超过1GB请分批下载');
}
return Base::retSuccess('success');
}
/**
* @api {get} api/file/download/pack 20. 打包文件
* @api {get} api/file/download/pack 19. 打包文件
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1031,89 +997,110 @@ class FileController extends AbstractController
*/
public function download__pack()
{
$key = Request::input('key');
if ($key) {
$userid = Session::get('file::pack:userid');
if (empty($userid)) {
return Base::ajaxError("请求已过期,请重新导出!", [], 0, 502);
}
//
$array = Base::string2array(base64_decode(urldecode($key)));
$file = $array['file'];
if (empty($file) || !file_exists(storage_path($file))) {
return Base::ajaxError("文件不存在!", [], 0, 502);
}
return Response::download(storage_path($file));
}
$user = User::auth();
$ids = Request::input('ids');
$downName = Request::input('name');
$fileName = Request::input('name');
$fileName = preg_replace("/[\/\\\:\*\?\"\<\>\|]/", "", $fileName);
if (empty($fileName)) {
$fileName = 'Package_' . $user->userid;
}
$fileName .= '_' . Base::time() . '.zip';
$filePath = "temp/file/pack/" . date("Ym", Base::time());
$zipFile = "app/" . $filePath . "/" . $fileName;
$zipPath = storage_path($zipFile);
if (!is_array($ids) || empty($ids)) {
abort(403, "Please select the file or folder to download.");
return Base::retError('请选择下载的文件或文件夹');
}
if (count($ids) > 100) {
abort(403, "You can download a maximum of 100 files or folders at a time.");
return Base::retError('一次最多可以下载100个文件或文件夹');
}
$botUser = User::botGetOrCreate('system-msg');
if (empty($botUser)) {
return Base::retError('系统机器人不存在');
}
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
$files = [];
$totalSize = 0;
AbstractModel::transaction(function() use ($user, $ids, &$files, &$totalSize) {
foreach ($ids as $k => $id) {
$files[] = File::getFilesTree(intval($id), $user, 1);
$totalSize += $files[$k]->totalSize;
}
});
if ($totalSize > File::zipMaxSize) {
abort(403, "The total size of the file exceeds 1GB. Please download it in batches.");
foreach ($ids as $k => $id) {
$files[] = File::getFilesTree(intval($id), $user, 1);
$totalSize += $files[$k]->totalSize;
}
if ($totalSize > File::zipMaxSize) {
return Base::retError('文件总大小已超过1GB请分批下载');
}
$base64 = base64_encode(Base::array2string([
'file' => $zipFile,
]));
$fileUrl = Base::fillUrl('api/file/download/pack?key=' . urlencode($base64));
Session::put('file::pack:userid', $user->userid);
$zip = new \ZipArchive();
$zipName = 'temp/download/' . date("Ym") . '/' . $user->userid . '/' . $downName;
$zipPath = storage_path('app/'.$zipName);
Base::makeDir(dirname($zipPath));
if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
abort(403, "Failed to create compressed file.");
return Base::retError('创建压缩文件失败');
}
go(function() use ($zip, $files, $downName) {
go(function () use ($zipPath, $fileUrl, $zip, $files, $fileName, $botUser, $dialog) {
Coroutine::sleep(0.1);
// 压缩进度
$progress = 0;
$zip->registerProgressCallback(0.05, function($ratio) use ($downName, &$progress) {
$zip->registerProgressCallback(0.05, function ($ratio) use ($fileUrl, $fileName, &$progress) {
$progress = round($ratio * 100);
File::filePushMsg('compress', [
'name'=> $downName,
'name' => $fileName,
'url' => $fileUrl,
'progress' => $progress
]);
});
//
foreach ($files as $file) {
File::addFileTreeToZip($zip, $file);
}
$zip->close();
//
if ($progress < 100) {
File::filePushMsg('compress', [
'name'=> $downName,
'name' => $fileName,
'url' => $fileUrl,
'progress' => 100
]);
}
//
$text = "<b>文件下载打包已完成。</b>";
$text .= "\n\n";
$text .= "文件名:{$fileName}";
$text .= "\n";
$text .= "文件大小:".Base::twoFloat(filesize($zipPath) / 1024, true)."KB";
$text .= "\n";
$text .= '<a href="' . $fileUrl . '" target="_blank"><button type="button" class="ivu-btn ivu-btn-warning" style="margin-top: 10px;"><span>立即下载</span></button></a>';
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, true);
});
return Base::retSuccess('success');
}
/**
* @api {get} api/file/download/confirm 21. 确认下载
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup file
* @apiName download__confirm
*
* @apiParam {String} [name] 下载文件名
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function download__confirm()
{
$user = User::auth();
$downName = Request::input('name');
$zipName = 'temp/download/' . date("Ym") . '/' . $user->userid . '/' . $downName;
$zipPath = storage_path('app/'.$zipName);
if (!file_exists($zipPath)) {
abort(403, "The file does not exist.");
}
return response()->download($zipPath);
return Base::retSuccess('success', [
'name' => $fileName,
'url' => $fileUrl,
]);
}
}

View File

@@ -12,6 +12,7 @@ use App\Module\Doo;
use App\Models\File;
use App\Models\User;
use App\Module\Base;
use Swoole\Coroutine;
use App\Models\Deleted;
use App\Models\Project;
use App\Module\TimeRange;
@@ -30,9 +31,12 @@ use App\Models\ProjectTaskFile;
use App\Models\ProjectTaskUser;
use App\Models\WebSocketDialog;
use App\Exceptions\ApiException;
use App\Models\ProjectPermission;
use App\Models\WebSocketDialogMsg;
use App\Module\BillMultipleExport;
use Illuminate\Support\Facades\DB;
use App\Models\ProjectTaskFlowChange;
use App\Models\ProjectTaskVisibilityUser;
/**
* @apiDefine project
@@ -575,6 +579,8 @@ class ProjectController extends AbstractController
$project = Project::userProject($project_id);
//
if ($only_column) {
//
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_LIST_SORT);
// 排序列表
$index = 0;
foreach ($sort as $item) {
@@ -760,6 +766,8 @@ class ProjectController extends AbstractController
// 项目
$project = Project::userProject($project_id);
//
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_LIST_ADD);
//
if (empty($name)) {
return Base::retError('列表名称不能为空');
}
@@ -809,7 +817,9 @@ class ProjectController extends AbstractController
return Base::retError('列表不存在');
}
// 项目
Project::userProject($column->project_id);
$project = Project::userProject($column->project_id);
//
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_LIST_UPDATE);
//
if (Arr::exists($data, 'name') && $column->name != $data['name']) {
$column->addLog("修改列表名称:{$column->name} => {$data['name']}");
@@ -849,7 +859,9 @@ class ProjectController extends AbstractController
return Base::retError('列表不存在');
}
// 项目
Project::userProject($column->project_id, true, true);
$project = Project::userProject($column->project_id);
//
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_LIST_REMOVE);
//
$column->deleteColumn();
return Base::retSuccess('删除成功', ['id' => $column->id]);
@@ -876,10 +888,10 @@ class ProjectController extends AbstractController
public function column__one()
{
User::auth();
//
//
$column_id = intval(Request::input('column_id'));
$deleted = Request::input('deleted', 'no');
//
//
$builder = ProjectColumn::whereId($column_id);
if ($deleted == 'all') {
$builder->withTrashed();
@@ -890,10 +902,10 @@ class ProjectController extends AbstractController
if (empty($column)) {
return Base::retError('列表不存在');
}
//
//
return Base::retSuccess('success', $column);
}
/**
* @api {get} api/project/task/lists 19. 任务列表
@@ -1016,17 +1028,25 @@ class ProjectController extends AbstractController
$builder->orderBy('project_tasks.' . $column, $direction);
}
// 任务可见性条件
$builder->leftJoin('project_users', function ($query) {
$query->on('project_tasks.project_id', '=', 'project_users.project_id')->where('project_users.owner', 1);
$builder->leftJoin('project_users', function ($query) use($userid) {
$query->on('project_tasks.project_id', '=', 'project_users.project_id');
$query->where('project_users.owner', 1);
$query->where('project_users.userid', $userid);
});
$builder->leftJoin('project_task_users as project_p_task_users', function ($query) {
$query->on('project_p_task_users.task_pid', '=', 'project_tasks.parent_id');
$builder->leftJoin('project_task_users as project_sub_task_users', function ($query) use($userid) {
$query->on('project_sub_task_users.task_pid', '=', 'project_tasks.parent_id');
$query->where('project_sub_task_users.userid', $userid);
});
$builder->leftJoin('project_task_visibility_users', function ($query) use($userid) {
$query->on('project_task_visibility_users.task_id', '=', 'project_tasks.id');
$query->where('project_task_visibility_users.userid', $userid);
});
$builder->where(function ($query) use ($userid) {
$query->where("project_tasks.visibility", 1);
$query->orWhere("project_users.userid", $userid);
$query->orWhere("project_task_users.userid", $userid);
$query->orWhere("project_p_task_users.userid", $userid);
$query->orWhere("project_task_visibility_users.userid", $userid);
$query->orWhere("project_sub_task_users.userid", $userid);
});
// 优化子查询汇总
$builder->leftJoinSub(function ($query) {
@@ -1180,170 +1200,191 @@ class ProjectController extends AbstractController
if (Carbon::parse($time[1])->timestamp - Carbon::parse($time[0])->timestamp > 90 * 86400) {
return Base::retError('时间范围限制最大90天');
}
$headings = [];
$headings[] = '任务ID';
$headings[] = '父级任务ID';
$headings[] = '所属项目';
$headings[] = '任务标题';
$headings[] = '任务开始时间';
$headings[] = '任务结束时间';
$headings[] = '完成时间';
$headings[] = '归档时间';
$headings[] = '任务计划用时';
$headings[] = '实际完成用时';
$headings[] = '超时时间';
$headings[] = '开发用时';
$headings[] = '验收/测试用时';
$headings[] = '负责人';
$headings[] = '创建人';
$headings[] = '状态';
$datas = [];
$botUser = User::botGetOrCreate('system-msg');
if (empty($botUser)) {
return Base::retError('系统机器人不存在');
}
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
//
$builder = ProjectTask::select(['project_tasks.*', 'project_task_users.userid as ownerid'])
->join('project_task_users', 'project_tasks.id', '=', 'project_task_users.task_id')
->where('project_task_users.owner', 1)
->whereIn('project_task_users.userid', $userid)
->betweenTime(Carbon::parse($time[0])->startOfDay(), Carbon::parse($time[1])->endOfDay(), $type);
$builder->orderByDesc('project_tasks.id')->chunk(100, function ($tasks) use (&$datas) {
/** @var ProjectTask $task */
foreach ($tasks as $task) {
$flowChanges = ProjectTaskFlowChange::whereTaskId($task->id)->get();
$testTime = 0;//验收/测试时间
$taskStartTime = $task->start_at ? Carbon::parse($task->start_at)->timestamp : Carbon::parse($task->created_at)->timestamp;
$taskCompleteTime = $task->complete_at ? Carbon::parse($task->complete_at)->timestamp : time();
$totalTime = $taskCompleteTime - $taskStartTime; //开发测试总用时
foreach ($flowChanges as $change) {
if (!str_contains($change->before_flow_item_name, 'end')) {
$upOne = ProjectTaskFlowChange::where('id', '<', $change->id)->whereTaskId($task->id)->orderByDesc('id')->first();
if ($upOne) {
if (str_contains($change->before_flow_item_name, 'test') || str_contains($change->before_flow_item_name, '测试') || strpos($change->before_flow_item_name, '验收') !== false) {
$testCtime = Carbon::parse($change->created_at)->timestamp;
$tTime = Carbon::parse($upOne->created_at)->timestamp;
$tMinusNum = $testCtime - $tTime;
$testTime += $tMinusNum;
go(function () use ($user, $userid, $time, $type, $botUser, $dialog) {
Coroutine::sleep(0.1);
$headings = [];
$headings[] = '任务ID';
$headings[] = '父级任务ID';
$headings[] = '所属项目';
$headings[] = '任务标题';
$headings[] = '任务开始时间';
$headings[] = '任务结束时间';
$headings[] = '完成时间';
$headings[] = '归档时间';
$headings[] = '任务计划用时';
$headings[] = '实际完成用时';
$headings[] = '超时时间';
$headings[] = '开发用时';
$headings[] = '验收/测试用时';
$headings[] = '负责人';
$headings[] = '创建人';
$headings[] = '状态';
$datas = [];
//
$text = '<b>导出任务统计已完成。</b>';
$text .= "\n\n";
//
$builder = ProjectTask::select(['project_tasks.*', 'project_task_users.userid as ownerid'])
->join('project_task_users', 'project_tasks.id', '=', 'project_task_users.task_id')
->where('project_task_users.owner', 1)
->whereIn('project_task_users.userid', $userid)
->betweenTime(Carbon::parse($time[0])->startOfDay(), Carbon::parse($time[1])->endOfDay(), $type);
$builder->orderByDesc('project_tasks.id')->chunk(100, function ($tasks) use (&$datas) {
/** @var ProjectTask $task */
foreach ($tasks as $task) {
$flowChanges = ProjectTaskFlowChange::whereTaskId($task->id)->get();
$testTime = 0;//验收/测试时间
$taskStartTime = $task->start_at ? Carbon::parse($task->start_at)->timestamp : Carbon::parse($task->created_at)->timestamp;
$taskCompleteTime = $task->complete_at ? Carbon::parse($task->complete_at)->timestamp : time();
$totalTime = $taskCompleteTime - $taskStartTime; //开发测试总用时
foreach ($flowChanges as $change) {
if (!str_contains($change->before_flow_item_name, 'end')) {
$upOne = ProjectTaskFlowChange::where('id', '<', $change->id)->whereTaskId($task->id)->orderByDesc('id')->first();
if ($upOne) {
if (str_contains($change->before_flow_item_name, 'test') || str_contains($change->before_flow_item_name, '测试') || strpos($change->before_flow_item_name, '验收') !== false) {
$testCtime = Carbon::parse($change->created_at)->timestamp;
$tTime = Carbon::parse($upOne->created_at)->timestamp;
$tMinusNum = $testCtime - $tTime;
$testTime += $tMinusNum;
}
}
}
}
}
if (!$task->complete_at) {
$lastChange = ProjectTaskFlowChange::whereTaskId($task->id)->orderByDesc('id')->first();
$nowTime = time();
$unFinishTime = $nowTime - Carbon::parse($lastChange->created_at)->timestamp;
if (str_contains($lastChange->after_flow_item_name, 'test') || str_contains($lastChange->after_flow_item_name, '测试') || strpos($lastChange->after_flow_item_name, '验收') !== false) {
$testTime += $unFinishTime;
if (!$task->complete_at) {
$lastChange = ProjectTaskFlowChange::whereTaskId($task->id)->orderByDesc('id')->first();
$nowTime = time();
$unFinishTime = $nowTime - Carbon::parse($lastChange->created_at)->timestamp;
if (str_contains($lastChange->after_flow_item_name, 'test') || str_contains($lastChange->after_flow_item_name, '测试') || strpos($lastChange->after_flow_item_name, '验收') !== false) {
$testTime += $unFinishTime;
}
}
}
$developTime = $totalTime - $testTime;//开发时间
$planTime = '-';//任务计划用时
$overTime = '-';//超时时间
if ($task->end_at) {
$startTime = Carbon::parse($task->start_at)->timestamp;
$endTime = Carbon::parse($task->end_at)->timestamp;
$planTotalTime = $endTime - $startTime;
$residueTime = $planTotalTime - $totalTime;
if ($residueTime < 0) {
$overTime = Base::timeFormat(abs($residueTime));
$developTime = $totalTime - $testTime;//开发时间
$planTime = '-';//任务计划用时
$overTime = '-';//超时时间
if ($task->end_at) {
$startTime = Carbon::parse($task->start_at)->timestamp;
$endTime = Carbon::parse($task->end_at)->timestamp;
$planTotalTime = $endTime - $startTime;
$residueTime = $planTotalTime - $totalTime;
if ($residueTime < 0) {
$overTime = Base::timeFormat(abs($residueTime));
}
$planTime = Base::timeDiff($startTime, $endTime);
}
$planTime = Base::timeDiff($startTime, $endTime);
}
$actualTime = $task->complete_at ? $totalTime : 0;//实际完成用时
$statusText = '未完成';
if ($task->flow_item_name) {
if (str_contains($task->flow_item_name, '已取消')) {
$statusText = '已取消';
$actualTime = 0;
$testTime = 0;
$developTime = 0;
$overTime = '-';
} elseif (str_contains($task->flow_item_name, '已完成')) {
$actualTime = $task->complete_at ? $totalTime : 0;//实际完成用时
$statusText = '未完成';
if ($task->flow_item_name) {
if (str_contains($task->flow_item_name, '已取消')) {
$statusText = '已取消';
$actualTime = 0;
$testTime = 0;
$developTime = 0;
$overTime = '-';
} elseif (str_contains($task->flow_item_name, '已完成')) {
$statusText = '已完成';
}
} elseif ($task->complete_at) {
$statusText = '已完成';
}
} elseif ($task->complete_at) {
$statusText = '已完成';
}
if (!isset($datas[$task->ownerid])) {
$datas[$task->ownerid] = [
'index' => 1,
'nickname' => Base::filterEmoji(User::userid2nickname($task->ownerid)),
'styles' => ["A1:P1" => ["font" => ["bold" => true]]],
'data' => [],
if (!isset($datas[$task->ownerid])) {
$datas[$task->ownerid] = [
'index' => 1,
'nickname' => Base::filterEmoji(User::userid2nickname($task->ownerid)),
'styles' => ["A1:P1" => ["font" => ["bold" => true]]],
'data' => [],
];
}
$datas[$task->ownerid]['index']++;
if ($statusText === '未完成') {
$datas[$task->ownerid]['styles']["P{$datas[$task->ownerid]['index']}"] = ["font" => ["color" => ["rgb" => "ff0000"]]]; // 未完成
} elseif ($statusText === '已完成' && $task->end_at && Carbon::parse($task->complete_at)->gt($task->end_at)) {
$datas[$task->ownerid]['styles']["P{$datas[$task->ownerid]['index']}"] = ["font" => ["color" => ["rgb" => "436FF6"]]]; // 已完成超期
}
$datas[$task->ownerid]['data'][] = [
$task->id,
$task->parent_id ?: '-',
Base::filterEmoji($task->project?->name) ?: '-',
Base::filterEmoji($task->name),
$task->start_at ?: '-',
$task->end_at ?: '-',
$task->complete_at ?: '-',
$task->archived_at ?: '-',
$planTime ?: '-',
$actualTime ? Base::timeFormat($actualTime) : '-',
$overTime,
$developTime > 0 ? Base::timeFormat($developTime) : '-',
$testTime > 0 ? Base::timeFormat($testTime) : '-',
Base::filterEmoji(User::userid2nickname($task->ownerid)) . " (ID: {$task->ownerid})",
Base::filterEmoji(User::userid2nickname($task->userid)) . " (ID: {$task->userid})",
$statusText
];
}
$datas[$task->ownerid]['index']++;
if ($statusText === '未完成') {
$datas[$task->ownerid]['styles']["P{$datas[$task->ownerid]['index']}"] = ["font" => ["color" => ["rgb" => "ff0000"]]]; // 未完成
} elseif ($statusText === '已完成' && $task->end_at && Carbon::parse($task->complete_at)->gt($task->end_at)) {
$datas[$task->ownerid]['styles']["P{$datas[$task->ownerid]['index']}"] = ["font" => ["color" => ["rgb" => "436FF6"]]]; // 已完成超期
}
$datas[$task->ownerid]['data'][] = [
$task->id,
$task->parent_id ?: '-',
Base::filterEmoji($task->project?->name) ?: '-',
Base::filterEmoji($task->name),
$task->start_at ?: '-',
$task->end_at ?: '-',
$task->complete_at ?: '-',
$task->archived_at ?: '-',
$planTime ?: '-',
$actualTime ? Base::timeFormat($actualTime) : '-',
$overTime,
$developTime > 0 ? Base::timeFormat($developTime) : '-',
$testTime > 0 ? Base::timeFormat($testTime) : '-',
Base::filterEmoji(User::userid2nickname($task->ownerid)) . " (ID: {$task->ownerid})",
Base::filterEmoji(User::userid2nickname($task->userid)) . " (ID: {$task->userid})",
$statusText
];
});
if (empty($datas)) {
$text .= '没有任何数据';
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, true);
return;
}
});
if (empty($datas)) {
return Base::retError('没有任何数据');
}
//
$sheets = [];
foreach ($userid as $ownerid) {
$data = $datas[$ownerid] ?? [
//
$sheets = [];
foreach ($userid as $ownerid) {
$data = $datas[$ownerid] ?? [
'nickname' => Base::filterEmoji(User::userid2nickname($ownerid)),
'styles' => ["A1:P1" => ["font" => ["bold" => true]]],
'data' => [],
];
$title = (count($sheets) + 1) . "." . ($data['nickname'] ?: $ownerid);
$sheets[] = BillExport::create()->setTitle($title)->setHeadings($headings)->setData($data['data'])->setStyles($data['styles']);
}
//
$fileName = User::userid2nickname($userid[0]) ?: $userid[0];
if (count($userid) > 1) {
$fileName .= "" . count($userid) . "位成员";
}
$fileName .= '任务统计_' . Base::time() . '.xls';
$filePath = "temp/task/export/" . date("Ym", Base::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('打包失败,请稍后再试...');
}
$title = (count($sheets) + 1) . "." . ($data['nickname'] ?: $ownerid);
$sheets[] = BillExport::create()->setTitle($title)->setHeadings($headings)->setData($data['data'])->setStyles($data['styles']);
}
//
$fileName = User::userid2nickname($userid[0]) ?: $userid[0];
if (count($userid) > 1) {
$fileName .= '等' . count($userid) . '位成员任务统计';
}
$fileName .= '_' . Base::time() . '.xls';
$filePath = "temp/task/export/" . date("Ym", Base::time());
$export = new BillMultipleExport($sheets);
$res = $export->store($filePath . "/" . $fileName);
if ($res != 1) {
$text .= "导出失败,{$fileName}";
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, true);
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);
$text .= "文件名:{$fileName}";
$text .= "\n";
$text .= "文件大小:" . Base::twoFloat(filesize($zipPath) / 1024, true) . "KB";
$text .= "\n";
$text .= '<a href="' . $fileUrl . '" target="_blank"><button type="button" class="ivu-btn ivu-btn-warning" style="margin-top: 10px;"><span>立即下载</span></button></a>';
} else {
$text .= '打包失败,请稍后再试...';
}
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, true);
});
return Base::retSuccess('success', ['msg' => '正在打包,请留意系统消息。']);
}
/**
@@ -1507,13 +1548,15 @@ class ProjectController extends AbstractController
$archived = Request::input('archived', 'no');
//
$isArchived = str_replace(['all', 'yes', 'no'], [null, false, true], $archived);
$task = ProjectTask::userTask($task_id, $isArchived, true, false, ['taskUser', 'taskTag']);
$task = ProjectTask::userTask($task_id, $isArchived, true, ['taskUser', 'taskTag']);
// 项目可见性
$project_userid = ProjectUser::whereProjectId($task->project_id)->whereOwner(1)->value('userid'); // 项目负责人
if ($task->visibility != 1 && $user->userid != $project_userid) {
$visibleUserids = ProjectTaskUser::whereTaskId($task_id)->pluck('userid')->toArray(); // 是否任务负责人、协助人、可见人
$subVisibleUserids = ProjectTaskUser::whereTaskPid($task_id)->pluck('userid')->toArray(); // 是否子任务负责人、协助人
if (!in_array($user->userid, $visibleUserids) && !in_array($user->userid, $subVisibleUserids)) {
$taskUserids = ProjectTaskUser::whereTaskId($task_id)->pluck('userid')->toArray(); //任务负责人、协助人
$subTaskUserids = ProjectTaskUser::whereTaskPid($task_id)->pluck('userid')->toArray(); //子任务负责人、协助人
$visibleUserids = ProjectTaskVisibilityUser::whereTaskId($task_id)->pluck('userid')->toArray(); //可见人
$visibleUserids = array_merge($taskUserids, $subTaskUserids, $visibleUserids);
if (!in_array($user->userid, $visibleUserids)) {
return Base::retError('无任务权限');
}
}
@@ -1521,7 +1564,7 @@ class ProjectController extends AbstractController
$data = $task->toArray();
$data['project_name'] = $task->project?->name;
$data['column_name'] = $task->projectColumn?->name;
$data['visibility_appointor'] = $task->visibility == 1 ? [0] : ProjectTaskUser::whereTaskId($task_id)->whereOwner(2)->pluck('userid');
$data['visibility_appointor'] = $task->visibility == 1 ? [0] : ProjectTaskVisibilityUser::whereTaskId($task_id)->pluck('userid');
return Base::retSuccess('success', $data);
}
@@ -1603,7 +1646,9 @@ class ProjectController extends AbstractController
return Base::retError('文件不存在或已被删除');
}
//
$task = ProjectTask::userTask($file->task_id, true, true, true);
$task = ProjectTask::userTask($file->task_id);
//
ProjectPermission::userTaskPermission(Project::userProject($task->project_id), ProjectPermission::TASK_UPDATE, $task);
//
$task->pushMsg('filedelete', $file);
$file->delete();
@@ -1732,6 +1777,8 @@ class ProjectController extends AbstractController
$column_id = $data['column_id'];
// 项目
$project = Project::userProject($project_id);
//
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_ADD);
// 列表
$column = null;
$newColumn = null;
@@ -1808,11 +1855,13 @@ class ProjectController extends AbstractController
$task_id = intval(Request::input('task_id'));
$name = Request::input('name');
//
$task = ProjectTask::userTask($task_id, true, true, true);
$task = ProjectTask::userTask($task_id);
if ($task->complete_at) {
return Base::retError('主任务已完成无法添加子任务');
}
//
ProjectPermission::userTaskPermission(Project::userProject($task->project_id), ProjectPermission::TASK_ADD);
//
$task = ProjectTask::addTask([
'name' => $name,
'parent_id' => $task->id,
@@ -1867,18 +1916,26 @@ class ProjectController extends AbstractController
$param = Request::input();
$task_id = intval($param['task_id']);
//
$task = ProjectTask::userTask($task_id, true, true, 2);
$task = ProjectTask::userTask($task_id);
//
$project = Project::userProject($task->project_id);
if (Arr::exists($param, 'flow_item_id')) {
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_STATUS, $task);
}else{
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_UPDATE, $task);
}
//
$taskUser = ProjectTaskUser::select(['userid', 'owner'])->whereTaskId($task_id)->get();
$owners = $taskUser->where('owner', 1)->pluck('userid')->toArray(); // 负责人
$assists = $taskUser->where('owner', 0)->pluck('userid')->toArray(); // 协助人
$owners = $taskUser->where('owner', 1)->pluck('userid')->toArray();
$assists = $taskUser->where('owner', 0)->pluck('userid')->toArray();
$visible = ProjectTaskVisibilityUser::whereTaskId($task->id)->pluck('userid')->toArray();
// 更新任务
$updateMarking = [];
$task->updateTask($param, $updateMarking);
//
$data = ProjectTask::oneTask($task->id)->toArray();
$data['update_marking'] = $updateMarking ?: json_decode('{}');
$data['visibility_appointor'] = $data['visibility'] == 1 ? [] : ProjectTaskUser::whereTaskId($task->id)->whereOwner(2)->pluck('userid');
$data['visibility_appointor'] = $data['visibility'] == 1 ? [] : ProjectTaskVisibilityUser::whereTaskId($task->id)->pluck('userid');
$task->pushMsg('update', $data);
// 可见性推送
if ($task->parent_id == 0) {
@@ -1888,10 +1945,9 @@ class ProjectController extends AbstractController
$task->pushMsgVisibleAdd($data);
}
if ($param['visibility_appointor']) {
$oldVisibleUserIds = $taskUser->where('owner', 2)->pluck('userid')->toArray() ?? [];
$newVisibleUserIds = $param['visibility_appointor'] ?? [];
$deleteUserIds = array_diff($oldVisibleUserIds, $newVisibleUserIds, $subUserids);
$addUserIds = array_diff($newVisibleUserIds, $oldVisibleUserIds);
$deleteUserIds = array_diff($visible, $newVisibleUserIds, $subUserids);
$addUserIds = array_diff($newVisibleUserIds, $visible);
$task->pushMsgVisibleUpdate($data, $deleteUserIds, $addUserIds);
}
if ($data['visibility'] != 1 && empty($param['visibility_appointor'])) {
@@ -2003,12 +2059,15 @@ class ProjectController extends AbstractController
$task_id = intval(Request::input('task_id'));
$type = Request::input('type', 'add');
//
$task = ProjectTask::userTask($task_id, $type == 'add', true, true);
$task = ProjectTask::userTask($task_id, $type == 'add');
//
if ($task->parent_id > 0) {
return Base::retError('子任务不支持此功能');
}
//
$project = Project::userProject($task->project_id);
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_ARCHIVED, $task);
//
if ($type == 'recovery') {
$task->archivedTask(null);
} elseif ($type == 'add') {
@@ -2045,7 +2104,11 @@ class ProjectController extends AbstractController
$task_id = intval(Request::input('task_id'));
$type = Request::input('type', 'delete');
//
$task = ProjectTask::userTask($task_id, null, $type !== 'recovery', true);
$task = ProjectTask::userTask($task_id, null, $type !== 'recovery');
//
$project = Project::userProject($task->project_id);
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_REMOVE, $task);
//
if ($type == 'recovery') {
$task->restoreTask();
return Base::retSuccess('操作成功', ['id' => $task->id]);
@@ -2080,7 +2143,7 @@ class ProjectController extends AbstractController
return Base::retError('记录不存在');
}
//
$task = ProjectTask::userTask($projectLog->task_id, true, true, true);
$task = ProjectTask::userTask($projectLog->task_id);
//
$record = $projectLog->record;
if ($record['flow'] && is_array($record['flow'])) {
@@ -2123,6 +2186,7 @@ class ProjectController extends AbstractController
* @apiName task__flow
*
* @apiParam {Number} task_id 任务ID
* @apiParam {Number} project_id 项目ID - 存在时只返回这个项目的
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -2133,17 +2197,23 @@ class ProjectController extends AbstractController
User::auth();
//
$task_id = intval(Request::input('task_id'));
$project_id = intval(Request::input('project_id'));
//
$projectTask = ProjectTask::select(['id', 'project_id', 'complete_at', 'flow_item_id', 'flow_item_name'])->withTrashed()->find($task_id);
if (empty($projectTask)) {
return Base::retError('任务不存在', [ 'task_id' => $task_id ], -4002);
return Base::retError('任务不存在', ['task_id' => $task_id], -4002);
}
//
$projectFlowItem = $projectTask->flow_item_id ? ProjectFlowItem::with(['projectFlow'])->find($projectTask->flow_item_id) : null;
if ($projectFlowItem?->projectFlow) {
$projectFlow = $projectFlowItem->projectFlow;
$projectFlowItem = null;
if ($project_id) {
$projectFlow = ProjectFlow::whereProjectId($project_id)->orderByDesc('id')->first();
} else {
$projectFlow = ProjectFlow::whereProjectId($projectTask->project_id)->orderByDesc('id')->first();
$projectFlowItem = $projectTask->flow_item_id ? ProjectFlowItem::with(['projectFlow'])->find($projectTask->flow_item_id) : null;
if ($projectFlowItem?->projectFlow) {
$projectFlow = $projectFlowItem->projectFlow;
} else {
$projectFlow = ProjectFlow::whereProjectId($projectTask->project_id)->orderByDesc('id')->first();
}
}
if (empty($projectFlow)) {
return Base::retSuccess('success', [
@@ -2197,7 +2267,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/task/move 35. 任务移动
* @api {get} api/project/task/move 38. 任务移动
*
* @apiDescription 需要token身份项目、任务负责人
* @apiVersion 1.0.0
@@ -2207,6 +2277,9 @@ class ProjectController extends AbstractController
* @apiParam {Number} task_id 任务ID
* @apiParam {Number} project_id 项目ID
* @apiParam {Number} column_id 列ID
* @apiParam {Number} flow_item_id 工作流id
* @apiParam {Array} owner 负责人
* @apiParam {Array} assist 协助人
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -2219,26 +2292,40 @@ class ProjectController extends AbstractController
$task_id = intval(Request::input('task_id'));
$project_id = intval(Request::input('project_id'));
$column_id = intval(Request::input('column_id'));
//
$task = ProjectTask::userTask($task_id, true, true, 2);
//
$flow_item_id = intval(Request::input('flow_item_id'));
$owner = Request::input('owner', []);
$assist = Request::input('assist', []);
//
$task = ProjectTask::userTask($task_id);
//
$project = Project::userProject($task->project_id);
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_MOVE, $task);
//
if( $task->project_id == $project_id && $task->column_id == $column_id){
return Base::retSuccess('移动成功', ['id' => $task_id]);
}
//
//
$project = Project::userProject($project_id);
$column = ProjectColumn::whereProjectId($project->id)->whereId($column_id)->first();
if (empty($column)) {
return Base::retError('列表不存在');
}
//
$task->moveTask($project_id,$column_id);
//
return Base::retSuccess('移动成功', ['id' => $task_id]);
if($flow_item_id){
$flowItem = projectFlowItem::whereProjectId($project->id)->whereId($flow_item_id)->first();
if (empty($flowItem)) {
return Base::retError('任务状态不存在');
}
}
//
$task->moveTask($project_id, $column_id, $flow_item_id, $owner, $assist);
//
$task = ProjectTask::userTask($task_id);
//
return Base::retSuccess('移动成功', $task);
}
/**
* @api {get} api/project/flow/list 38. 工作流列表
* @api {get} api/project/flow/list 39. 工作流列表
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2264,7 +2351,7 @@ class ProjectController extends AbstractController
}
/**
* @api {post} api/project/flow/save 39. 保存工作流
* @api {post} api/project/flow/save 40. 保存工作流
*
* @apiDescription 需要token身份项目负责人
* @apiVersion 1.0.0
@@ -2298,7 +2385,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/flow/delete 40. 删除工作流
* @api {get} api/project/flow/delete 41. 删除工作流
*
* @apiDescription 需要token身份项目负责人
* @apiVersion 1.0.0
@@ -2330,7 +2417,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/log/lists 41. 获取项目、任务日志
* @api {get} api/project/log/lists 42. 获取项目、任务日志
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2383,7 +2470,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/top 42. 项目置顶
* @api {get} api/project/top 43. 项目置顶
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -2411,4 +2498,74 @@ class ProjectController extends AbstractController
'top_at' => $projectUser->top_at?->toDateTimeString(),
]);
}
/**
* @api {get} api/project/permission 44. 获取项目权限设置
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup project
* @apiName permission
*
* @apiParam {Number} project_id 项目ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function permission()
{
$user = User::auth();
$projectId = intval(Request::input('project_id'), 0);
$projectUser = ProjectUser::whereUserid($user->userid)->whereProjectId($projectId)->first();
if (!$projectUser) {
return Base::retError("项目不存在");
}
$projectPermission = ProjectPermission::initPermissions($projectId);
return Base::retSuccess("success", $projectPermission);
}
/**
* @api {get} api/project/permission/update 45. 项目权限设置
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup project
* @apiName permission__update
*
* @apiParam {Number} project_id 项目ID
* @apiParam {Array} task_add 添加任务权限
* @apiParam {Array} task_update 修改任务权限
* @apiParam {Array} task_remove 删除任务权限
* @apiParam {Array} task_update_complete 标记完成权限
* @apiParam {Array} task_archived 归档任务权限
* @apiParam {Array} task_move 移动任务权限
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function permission__update()
{
$user = User::auth();
$projectId = intval(Request::input('project_id'), 0);
$projectUser = ProjectUser::whereUserid($user->userid)->whereProjectId($projectId)->first();
if (!$projectUser) {
return Base::retError("项目不存在");
}
$permissions = Request::only([
ProjectPermission::TASK_LIST_ADD,
ProjectPermission::TASK_LIST_UPDATE,
ProjectPermission::TASK_LIST_REMOVE,
ProjectPermission::TASK_LIST_SORT,
ProjectPermission::TASK_ADD,
ProjectPermission::TASK_UPDATE,
ProjectPermission::TASK_REMOVE,
ProjectPermission::TASK_STATUS,
ProjectPermission::TASK_ARCHIVED,
ProjectPermission::TASK_MOVE,
]);
$projectPermission = ProjectPermission::updatePermissions($projectId, Base::newArrayRecursive('intval', $permissions));
return Base::retSuccess("success", $projectPermission);
}
}

View File

@@ -2,15 +2,8 @@
namespace App\Http\Controllers\Api;
use App\Models\User;
use App\Models\UserBot;
use App\Models\UserCheckinMac;
use App\Models\UserCheckinRecord;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Module\Base;
use Cache;
use Carbon\Carbon;
use Request;
/**

View File

@@ -13,7 +13,6 @@ use App\Module\Doo;
use App\Tasks\PushTask;
use Carbon\Carbon;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Validation\Rule;
use Request;
@@ -317,13 +316,16 @@ class ReportController extends AbstractController
// 未完成的任务
$unfinishedContent = "";
$unfinished_task = ProjectTask::query()
->whereNull("complete_at")
->whereNotNull("start_at")
->where("end_at", "<", $end_time->toDateTimeString())
->join("projects", "projects.id", "=", "project_tasks.project_id")
->whereNull("projects.archived_at")
->whereNull("project_tasks.complete_at")
->whereNotNull("project_tasks.start_at")
->where("project_tasks.end_at", "<", $end_time->toDateTimeString())
->whereHas("taskUser", function ($query) use ($user) {
$query->where("userid", $user->userid);
})
->orderByDesc("id")
->select("project_tasks.*")
->orderByDesc("project_tasks.id")
->get();
if ($unfinished_task->isNotEmpty()) {
foreach ($unfinished_task as $task) {
@@ -474,10 +476,13 @@ class ReportController extends AbstractController
{
$user = User::auth();
//
$data = Report::whereHas("Receives", function (Builder $query) use ($user) {
$query->where("userid", $user->userid)->where("read", 0);
})->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20));
return Base::retSuccess("success", $data);
$total = Report::select('reports.id')
->join('report_receives', 'report_receives.rid', '=', 'reports.id')
->where('report_receives.userid', $user->userid)
->where('report_receives.read', 0)
->count();
//
return Base::retSuccess("success", compact("total"));
}
/**

View File

@@ -40,7 +40,7 @@ class SystemController extends AbstractController
* @apiParam {String} type
* - get: 获取(默认)
* - all: 获取所有(需要管理员权限)
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'image_compress', 'image_save_local', 'start_home']
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'image_compress', 'image_save_local', 'start_home']
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -65,12 +65,15 @@ class SystemController extends AbstractController
'project_invite',
'chat_information',
'anon_message',
'e2e_message',
'auto_archived',
'archived_day',
'task_visible',
'task_default_time',
'all_group_mute',
'all_group_autoin',
'user_private_chat_mute',
'user_group_chat_mute',
'image_compress',
'image_save_local',
'start_home',
@@ -108,18 +111,22 @@ class SystemController extends AbstractController
$setting['project_invite'] = $setting['project_invite'] ?: 'open';
$setting['chat_information'] = $setting['chat_information'] ?: 'optional';
$setting['anon_message'] = $setting['anon_message'] ?: 'open';
$setting['e2e_message'] = $setting['e2e_message'] ?: 'close';
$setting['auto_archived'] = $setting['auto_archived'] ?: 'close';
$setting['archived_day'] = floatval($setting['archived_day']) ?: 7;
$setting['task_visible'] = $setting['task_visible'] ?: 'close';
$setting['task_default_time'] = $setting['task_default_time'] ? Base::json2array($setting['task_default_time']) : ['09:00', '18:00'];
$setting['all_group_mute'] = $setting['all_group_mute'] ?: 'open';
$setting['all_group_autoin'] = $setting['all_group_autoin'] ?: 'yes';
$setting['user_private_chat_mute'] = $setting['user_private_chat_mute'] ?: 'open';
$setting['user_group_chat_mute'] = $setting['user_group_chat_mute'] ?: 'open';
$setting['image_compress'] = $setting['image_compress'] ?: 'open';
$setting['image_save_local'] = $setting['image_save_local'] ?: '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_version'] = Base::getVersion();
//
return Base::retSuccess('success', $setting ?: json_decode('{}'));
}
@@ -267,7 +274,10 @@ class SystemController extends AbstractController
'wenxin_secret',
'wenxin_model',
'qianwen_key',
'qianwen_model'
'qianwen_model',
'gemini_key',
'gemini_model',
'gemini_agency',
];
if ($type == 'save') {
@@ -307,11 +317,18 @@ class SystemController extends AbstractController
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => "设置成功"], $botUser->userid, true, false, true);
}
}
if ($backup['gemini_key'] != $setting['gemini_key']) {
$botUser = User::botGetOrCreate('ai-gemini');
if ($botUser && $dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => "设置成功"], $botUser->userid, true, false, true);
}
}
}
//
$setting['openai_model'] = $setting['openai_model'] ?: 'gpt-3.5-turbo';
$setting['wenxin_model'] = $setting['wenxin_model'] ?: 'eb-instant';
$setting['qianwen_model'] = $setting['qianwen_model'] ?: 'qwen-v1';
$setting['gemini_model'] = $setting['gemini_model'] ?: 'gemini-1.0-pro';
if (env("SYSTEM_SETTING") == 'disabled') {
foreach ($keys as $item) {
if (strlen($setting[$item]) > 12) {
@@ -385,6 +402,9 @@ class SystemController extends AbstractController
$setting['edit'] = $setting['edit'] ?: 'close';
$setting['modes'] = is_array($setting['modes']) ? $setting['modes'] : [];
$setting['cmd'] = "curl -sSL '" . Base::fillUrl("api/public/checkin/install?key={$setting['key']}") . "' | sh";
if (Base::judgeClientVersion('0.34.67')) {
$setting['cmd'] = base64_encode($setting['cmd']);
}
//
return Base::retSuccess('success', $setting ?: json_decode('{}'));
}
@@ -1191,7 +1211,7 @@ class SystemController extends AbstractController
if (count($users) > 1) {
$fileName .= "" . count($userid) . "位成员";
}
$fileName .= '签到记录_' . Base::time() . '.xls';
$fileName .= '签到记录_' . Base::time() . '.xlsx';
$filePath = "temp/checkin/export/" . date("Ym", Base::time());
$export = new BillMultipleExport($sheets);
$res = $export->store($filePath . "/" . $fileName);
@@ -1199,7 +1219,7 @@ class SystemController extends AbstractController
return Base::retError('导出失败,' . $fileName . '');
}
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xls') . ".zip";
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xlsx') . ".zip";
$zipPath = storage_path($zipFile);
if (file_exists($zipPath)) {
Base::deleteDirAndFile($zipPath, true);
@@ -1256,8 +1276,14 @@ class SystemController extends AbstractController
* @apiGroup system
* @apiName version
*
* @apiSuccess {String} version
* @apiSuccess {String} publish
* @apiSuccessExample {json} Success-Response:
{
"version": "0.0.1",
"publish": {
"provider": "generic",
"url": ""
}
}
*/
public function version()
{
@@ -1279,4 +1305,52 @@ class SystemController extends AbstractController
}
return $array;
}
/**
* @api {get} api/system/prefetch 25. 预加载的资源
*
* @apiVersion 1.0.0
* @apiGroup system
* @apiName prefetch
*
* @apiSuccessExample {array} Success-Response:
[
"https://......",
"https://......",
"......",
]
*/
public function prefetch()
{
$userAgent = strtolower(Request::server('HTTP_USER_AGENT'));
$isMain = str_contains($userAgent, 'maintaskwindow');
$isApp = str_contains($userAgent, 'kuaifan_eeui');
$version = Base::getVersion();
$array = [];
if ($isMain || $isApp) {
$path = 'js/build/';
$list = Base::readDir(public_path($path), false);
foreach ($list as $item) {
if (is_file($item) && filesize($item) > 50 * 1024) {
$array[] = $path . basename($item);
}
}
}
if ($isMain) {
$file = base_path('.prefetch');
if (file_exists($file)) {
$content = file_get_contents($file);
$items = explode("\n", $content);
$array = array_merge($array, $items);
}
}
return array_map(function($item) use ($version) {
$url = trim($item);
$url = str_replace('{version}', $version, $url);
return url($url);
}, array_values(array_filter($array)));
}
}

View File

@@ -25,6 +25,7 @@ use App\Models\UserDepartment;
use App\Models\WebSocketDialog;
use App\Models\UserCheckinRecord;
use App\Models\WebSocketDialogMsg;
use Illuminate\Support\Facades\DB;
use App\Models\UserEmailVerification;
use App\Module\AgoraIO\AgoraTokenGenerator;
@@ -570,9 +571,9 @@ class UsersController extends AbstractController
$list->transform(function (User $userInfo) use ($user, $state) {
$tags = [];
$dep = $userInfo->getDepartmentName();
$dep = array_filter(explode(",", $dep), function($item) {
$dep = array_values(array_filter(explode(",", $dep), function($item) {
return preg_match("/\(M\)$/", $item);
});
}));
if ($dep) {
$tags[] = preg_replace("/\(M\)$/", "", trim($dep[0])) . Doo::translate("负责人");
}
@@ -1100,10 +1101,17 @@ class UsersController extends AbstractController
];
$row = UmengAlias::where($inArray);
if ($row->exists()) {
$row->update(['updated_at' => Carbon::now()]);
$row->update([
'ua' => $data['userAgent'],
'device' => $data['deviceModel'],
'updated_at' => Carbon::now()
]);
return Base::retSuccess('别名已存在');
}
$row = UmengAlias::createInstance($inArray);
$row = UmengAlias::createInstance(array_merge($inArray, [
'ua' => $data['userAgent'],
'device' => $data['deviceModel'],
]));
if ($row->save()) {
return Base::retSuccess('添加成功');
} else {
@@ -1971,7 +1979,7 @@ class UsersController extends AbstractController
'name' => Doo::translate('文件'),
];
}
$dialogList = (new WebSocketDialog)->getDialogList($user->userid);
$dialogList = WebSocketDialog::getDialogList($user->userid);
foreach ($dialogList['data'] as $dialog) {
if ($dialog['avatar']) {
$avatar = url($dialog['avatar']);
@@ -2002,4 +2010,176 @@ class UsersController extends AbstractController
// 返回
return Base::retSuccess('success', $lists);
}
/**
* @api {get} api/users/annual/report 34. 年度报告
*
* @apiVersion 1.0.0
* @apiGroup users
* @apiName annual__report
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function annual__report()
{
$user = User::auth();
//
global $_A;
if (!isset($_A["__annual__report_".$user->userid])) {
$year = '2023';
$time = '2300-01-01 00:00:01';
$prefix = \DB::getTablePrefix();
$hireTimestamp = strtotime($user->created_at);
DB::statement("SET SQL_MODE=''");
// 我的任务
$taskDb = DB::table('project_tasks as t')
->join('project_task_users as tu', 't.id', '=', 'tu.task_id')
->where('tu.owner', 1)
->whereYear('t.created_at', $year)
->where('tu.userid', $user->userid);
// 我的任务 - 时长(分钟)
$durationTaskDb = $taskDb->clone()
->selectRaw("
{$prefix}t.id,
{$prefix}t.flow_item_name,
{$prefix}t.name as task_name,
{$prefix}p.name as project_name,
{$prefix}c.name as project_column_name,
{$prefix}t.start_at,
{$prefix}t.end_at,
{$prefix}t.complete_at,
{$prefix}t.created_at,
ifnull(TIMESTAMPDIFF(MINUTE, {$prefix}t.start_at, {$prefix}t.complete_at), 0) as duration
")
->leftJoin('projects as p', 'p.id', '=', 't.project_id')
->leftJoin('project_columns as c', 'c.id', '=', 't.column_id')
->whereNotNull('t.start_at')
->whereNotNull('t.complete_at');
// 最多聊天用户
$longestChat = DB::table('web_socket_dialogs as d')
->selectRaw("
{$prefix}d.id,
{$prefix}d.name as dialog_name,
{$prefix}d.type as dialog_type,
{$prefix}d.group_type as dialog_group_type,
{$prefix}m.chat_num,
{$prefix}u.userid,
{$prefix}u.email as user_email,
{$prefix}u.nickname as user_nickname,
ifnull({$prefix}d.avatar, {$prefix}u.userimg) as avatar
")
->leftJoinSub(function ($query) use ($user, $year) {
$query->select('web_socket_dialog_msgs.dialog_id', DB::raw('count(*) as chat_num'))
->from('web_socket_dialog_msgs')
->where('web_socket_dialog_msgs.userid', $user->userid)
->whereYear('web_socket_dialog_msgs.created_at', $year)
->groupBy('web_socket_dialog_msgs.dialog_id');
}, 'm', 'm.dialog_id', '=', 'd.id')
->leftJoin('web_socket_dialog_users as du', function ($query) use ($user) {
$query->on('d.id', '=', 'du.dialog_id');
$query->where('du.userid', '!=', $user->userid);
$query->where('d.type', 'user');
})
->leftJoin('users as u', 'du.userid', '=', 'u.userid')
->where('d.type', '!=', 'user')
->orWhere('u.bot', 0)
->orderByDesc('m.chat_num')
->first();
if (!empty($longestChat)) {
if ($longestChat->avatar) {
$longestChat->avatar = url($longestChat->avatar);
} else if ($longestChat->dialog_type == 'user') {
$longestChat->avatar = User::getAvatar($longestChat->userid, $longestChat->avatar, $longestChat->user_email, $longestChat->user_nickname);
} else {
$longestChat->avatar = match ($longestChat->dialog_group_type) {
'department' => url("images/avatar/default_group_department.png"),
'project' => url("images/avatar/default_group_project.png"),
'task' => url("images/avatar/default_group_task.png"),
default => url("images/avatar/default_group_people.png"),
};
}
}
// 最晚在线时间
$timezone = config('app.timezone');
$latestOnline = UserCheckinRecord::whereUserid($user->userid)
->whereYear(DB::raw('from_unixtime(report_time)'), $year)
->orderByRaw("TIME_FORMAT(DATE_ADD(CONVERT_TZ(from_unixtime(report_time), 'UTC', '$timezone'), INTERVAL 18 HOUR), '%H%i%s') desc")
->first();
//
$_A["__annual__report_".$user->userid] = [
// 本人信息
'user' => [
'userid' => $user->userid,
'email' => $user->email,
'nickname' => $user->nickname,
'avatar' => User::getAvatar($user->userid, $user->userimg, $user->email, $user->nickname)
],
// 入职时间(年月日)
'hire_date' => date("Y-m-d", $hireTimestamp),
// 在职时间(天为单位)
'tenure_days' => floor((strtotime(date('Y-m-d')) - $hireTimestamp) / (24 * 60 * 60)),
// 最晚在线时间
'latest_online_time' => date("Y-m-d H:i:s", $latestOnline->report_time),
// 跟谁聊天最多(发消息的次数。可以是群、私聊、机器人除外)
'longest_chat_user' => $longestChat,
// 跟所有ai机器人聊天的次数
'chat_al_num' => DB::table('web_socket_dialog_msgs as m')
->join('web_socket_dialogs as d', 'd.id', '=', 'm.dialog_id')
->join('web_socket_dialog_users as du', 'd.id', '=', 'du.dialog_id')
->join('users as u', 'du.userid', '=', 'u.userid')
->where('u.email', 'like', "%ai-%")
->where('u.bot', 1)
->where('m.userid', $user->userid)
->whereYear('m.created_at', $year)
->count(),
// 文件创建数量
'file_created_num' => File::whereCreatedId($user->userid)->whereYear('created_at', $year)->count(),
// 参与过的项目
'projects' => DB::table('projects as p')
->select('p.id', 'p.name')
->join('project_users as pu', 'p.id', '=', 'pu.project_id')
->join('project_task_users as ptu', 'p.id', '=', 'ptu.project_id')
->where(function($query) use ($user,$year) {
$query->where('pu.userid', $user->userid);
$query->whereYear('pu.created_at', $year);
})
->orWhere(function($query) use ($user,$year) {
$query->where('ptu.userid', $user->userid);
$query->whereYear('ptu.created_at', $year);
})
->groupBy('p.id')
->take(100)
->get(),
// 任务统计
'tasks' => [
// 总数量
'total' => $taskDb->count(),
// 完成数量
'completed' => $taskDb->clone()->whereNotNUll('t.complete_at')->count(),
// 超时数量
'overtime' => $taskDb->clone()->whereRaw("ifnull({$prefix}t.complete_at,'$time') > ifnull({$prefix}t.end_at,'$time')")->count(),
// 做得最久的任务
'longest_task' => $durationTaskDb->clone()->orderByDesc('duration')->first(),
// 做得最快的任务
'fastest_task' => $durationTaskDb->clone()->orderBy('duration')->first(),
// 每个月完成多少个任务
'month_completed_task' => $taskDb->clone()
->selectRaw("MONTH({$prefix}t.complete_at) AS month, COUNT({$prefix}t.id) AS num")
->whereNotNUll('t.complete_at')
->whereYear('t.complete_at', $year)
->groupBy('month')
->get()
]
];
}
//
return Base::retSuccess('success', $_A["__annual__report_".$user->userid]);
}
}

View File

@@ -7,6 +7,7 @@ use Cache;
use Request;
use Redirect;
use Response;
use App\Module\Doo;
use App\Models\File;
use App\Module\Base;
use App\Tasks\LoopTask;
@@ -37,9 +38,8 @@ class IndexController extends InvokeController
if ($action) {
$app .= "__" . $action;
}
if ($app === 'manifest.txt') {
$app = 'manifest';
$child = 'txt';
if ($app == 'default') {
return '';
}
if (!method_exists($this, $app)) {
$app = method_exists($this, $method) ? $method : 'main';
@@ -68,49 +68,7 @@ class IndexController extends InvokeController
'version' => Base::getVersion(),
'style' => $style,
'script' => $script,
])->header('Link', "<" . url('manifest.txt') . ">; rel=\"prefetch\"");
}
/**
* Manifest
* @param $child
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response|string
*/
public function manifest($child = '')
{
if (empty($child)) {
$murl = url('manifest.txt');
return response($murl)->header('Link', "<{$murl}>; rel=\"prefetch\"");
}
$array = [
"office/web-apps/apps/api/documents/api.js?hash=" . Base::getVersion(),
"office/7.5.1-23/web-apps/vendor/requirejs/require.js",
"office/7.5.1-23/web-apps/apps/api/documents/api.js",
"office/7.5.1-23/sdkjs/common/AllFonts.js",
"office/7.5.1-23/web-apps/vendor/xregexp/xregexp-all-min.js",
"office/7.5.1-23/web-apps/vendor/sockjs/sockjs.min.js",
"office/7.5.1-23/web-apps/vendor/jszip/jszip.min.js",
"office/7.5.1-23/web-apps/vendor/jszip-utils/jszip-utils.min.js",
"office/7.5.1-23/sdkjs/common/libfont/wasm/fonts.js",
"office/7.5.1-23/sdkjs/common/Charts/ChartStyles.js",
"office/7.5.1-23/sdkjs/slide/themes//themes.js",
"office/7.5.1-23/web-apps/apps/presentationeditor/main/app.js",
"office/7.5.1-23/sdkjs/slide/sdk-all-min.js",
"office/7.5.1-23/sdkjs/slide/sdk-all.js",
"office/7.5.1-23/web-apps/apps/documenteditor/main/app.js",
"office/7.5.1-23/sdkjs/word/sdk-all-min.js",
"office/7.5.1-23/sdkjs/word/sdk-all.js",
"office/7.5.1-23/web-apps/apps/spreadsheeteditor/main/app.js",
"office/7.5.1-23/sdkjs/cell/sdk-all-min.js",
"office/7.5.1-23/sdkjs/cell/sdk-all.js",
];
foreach ($array as &$item) {
$item = url($item);
}
return implode(PHP_EOL, $array);
]);
}
/**
@@ -217,6 +175,7 @@ class IndexController extends InvokeController
public function desktop__publish($name = '')
{
$publishVersion = Request::header('publish-version');
$fileNum = Request::get('file_num', 1);
$latestFile = public_path("uploads/desktop/latest");
$latestVersion = file_exists($latestFile) ? trim(file_get_contents($latestFile)) : "0.0.1";
if (strtolower($name) === 'latest') {
@@ -224,6 +183,7 @@ class IndexController extends InvokeController
}
// 上传
if (preg_match("/^\d+\.\d+\.\d+$/", $publishVersion)) {
$uploadSuccessFileNum = (int)Cache::get($publishVersion, 0);
$publishKey = Request::header('publish-key');
if ($publishKey !== env('APP_KEY')) {
return Base::retError("key error");
@@ -238,6 +198,20 @@ class IndexController extends InvokeController
]);
if (Base::isSuccess($res)) {
file_put_contents($latestFile, $publishVersion);
$uploadSuccessFileNum = $uploadSuccessFileNum + 1;
Cache::set($publishVersion, $uploadSuccessFileNum, 7200);
}
if ($uploadSuccessFileNum >= $fileNum){
$directoryPath = public_path("uploads/desktop");
$files = array_filter(scandir($directoryPath), function($file) use($directoryPath) {
return preg_match("/^\d+\.\d+\.\d+$/", $file) && is_dir($directoryPath . '/' . $file) && $file != '.' && $file != '..';
});
sort($files);
foreach ($files as $key => $file) {
if ($file != $publishVersion && $key < count($files) - 2) {
Base::deleteDirAndFile($directoryPath . '/' . $file);
}
}
}
return $res;
}
@@ -249,14 +223,15 @@ class IndexController extends InvokeController
$lists = Base::readDir($dirPath);
$files = [];
foreach ($lists as $file) {
if (str_ends_with($file, '.yml') || str_ends_with($file, '.yaml')) {
if (str_ends_with($file, '.yml') || str_ends_with($file, '.yaml') || str_ends_with($file, '.blockmap')) {
continue;
}
$fileName = Base::leftDelete($file, $dirPath);
$fileSize = filesize($file);
$files[] = [
'name' => substr($fileName, 1),
'time' => date("Y-m-d H:i:s", filemtime($file)),
'size' => Base::readableBytes(filesize($file)),
'size' => $fileSize > 0 ? Base::readableBytes($fileSize) : 0,
'url' => Base::fillUrl($path . $fileName),
];
}
@@ -273,10 +248,11 @@ class IndexController extends InvokeController
continue;
}
$fileName = Base::leftDelete($file, $dirPath);
$fileSize = filesize($file);
$apkFile = [
'name' => substr($fileName, 1),
'time' => date("Y-m-d H:i:s", filemtime($file)),
'size' => Base::readableBytes(filesize($file)),
'size' => $fileSize > 0 ? Base::readableBytes($fileSize) : 0,
'url' => Base::fillUrl($path . $fileName),
];
}
@@ -321,45 +297,90 @@ class IndexController extends InvokeController
$data = parse_url($key);
$path = Arr::get($data, 'path');
$file = public_path($path);
// 防止 ../ 穿越获取到系统文件
if (!str_starts_with(realpath($file), public_path())) {
abort(404);
}
//
if (file_exists($file)) {
parse_str($data['query'], $query);
$name = Arr::get($query, 'name');
$ext = strtolower(Arr::get($query, 'ext'));
$userAgent = strtolower(Request::server('HTTP_USER_AGENT'));
if ($ext === 'pdf'
&& (str_contains($userAgent, 'electron') || str_contains($userAgent, 'chrome'))) {
if (!file_exists($file)) {
abort(404);
}
//
parse_str($data['query'], $query);
$name = Arr::get($query, 'name');
$ext = strtolower(Arr::get($query, 'ext'));
$userAgent = strtolower(Request::server('HTTP_USER_AGENT'));
if ($ext === 'pdf') {
// 文件超过 10m 不支持在线预览,提示下载
if (filesize($file) > 10 * 1024 * 1024) {
return view('download', [
'name' => $name,
'size' => Base::readableBytes(filesize($file)),
'url' => Base::fillUrl($path),
'button' => Doo::translate('点击下载'),
]);
}
// 浏览器类型
$browser = 'none';
if (str_contains($userAgent, 'chrome') || str_contains($userAgent, 'android_kuaifan_eeui')) {
$browser = str_contains($userAgent, 'android_kuaifan_eeui') ? 'android-mobile' : 'chrome-desktop';
} elseif (str_contains($userAgent, 'safari') || str_contains($userAgent, 'ios_kuaifan_eeui')) {
$browser = str_contains($userAgent, 'ios_kuaifan_eeui') ? 'safari-mobile' : 'safari-desktop';
}
// electron 直接在线预览查看
if (str_contains($userAgent, 'electron') || str_contains($browser, 'desktop')) {
return Response::download($file, $name, [
'Content-Type' => 'application/pdf'
], 'inline');
}
//
if (in_array($ext, File::localExt)) {
$url = Base::fillUrl($path);
} else {
$url = 'http://' . env('APP_IPPR') . '.3/' . $path;
// EEUI App 直接在线预览查看
if (str_contains($userAgent, 'eeui') && Base::judgeClientVersion("0.34.47")) {
if ($browser === 'safari-mobile') {
$redirectUrl = Base::fillUrl($path);
return <<<EOF
<script>
window.top.postMessage({
action: "eeuiAppSendMessage",
data: [
{
action: 'setPageData',
data: {
showProgress: true,
titleFixed: true,
urlFixed: true,
}
},
{
action: 'createTarget',
url: "{$redirectUrl}",
}
]
}, "*")
</script>
EOF;
}
}
if ($ext !== 'pdf') {
$url = Base::urlAddparameter($url, [
'fullfilename' => $name . '.' . $ext
]);
}
$toUrl = Base::fillUrl("fileview/onlinePreview?url=" . urlencode(base64_encode($url)));
return Redirect::to($toUrl, 301);
}
return abort(404);
//
if (in_array($ext, File::localExt)) {
$url = Base::fillUrl($path);
} else {
$url = 'http://' . env('APP_IPPR') . '.3/' . $path;
}
$url = Base::urlAddparameter($url, [
'fullfilename' => Base::rightDelete($name, '.' . $ext) . '_' . filemtime($file) . '.' . $ext
]);
$redirectUrl = Base::fillUrl("fileview/onlinePreview?url=" . urlencode(base64_encode($url)));
return Redirect::to($redirectUrl, 301);
}
/**
* 设置语言和皮肤
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
* 保存配置 (todo 已废弃)
* @return string
*/
public function setting__theme_language()
public function storage__synch()
{
return view('setting', [
'theme' => Request::input('theme'),
'language' => Request::input('language')
]);
return '<!-- Deprecated -->';
}
/**

View File

@@ -190,9 +190,10 @@ class File extends AbstractModel
* @param user $user
* @param int $pid
* @param string $webkitRelativePath
* @param bool $overwrite
* @return array
*/
public function contentUpload($user, int $pid, $webkitRelativePath)
public function contentUpload($user, int $pid, $webkitRelativePath, $overwrite = false)
{
$userid = $user->userid;
if ($pid > 0) {
@@ -283,17 +284,25 @@ class File extends AbstractModel
if ($data['ext'] == 'markdown') {
$data['ext'] = 'md';
}
$file = File::createInstance([
$file = null;
$params = [
'pid' => $pid,
'name' => Base::rightDelete($data['name'], '.' . $data['ext']),
'type' => $type,
'ext' => $data['ext'],
'userid' => $userid,
'created_id' => $user->userid,
]);
$file->handleDuplicateName();
];
if ($overwrite) {
$file = self::wherePid($params['pid'])->whereExt($params['ext'])->whereName($params['name'])->first();
}
if (!$file) {
$overwrite = false;
$file = File::createInstance($params);
$file->handleDuplicateName();
}
// 开始创建
return AbstractModel::transaction(function () use ($addItem, $webkitRelativePath, $type, $user, $data, $file) {
return AbstractModel::transaction(function () use ($overwrite, $addItem, $webkitRelativePath, $type, $user, $data, $file) {
$file->size = $data['size'] * 1024;
$file->saveBeforePP();
//
@@ -321,11 +330,12 @@ class File extends AbstractModel
$tmpRow->pushMsg('add', $tmpRow);
//
$data = File::handleImageUrl($tmpRow->toArray());
$data['full_name'] = $webkitRelativePath ?: $data['name'];
$data['full_name'] = $webkitRelativePath ?: ($data['name'] . '.' . $data['ext']);
$data['overwrite'] = $overwrite ? 1 : 0;
//
$addItem[] = $data;
return ['data'=>$data,'addItem'=>$addItem];
return ['data' => $data, 'addItem' => $addItem];
});
}
@@ -926,12 +936,10 @@ class File extends AbstractModel
*/
public static function filePushMsg($action, $data = null, $userid = null)
{
//
$userid = User::auth()->userid();
if (empty($userid)) {
return;
}
//
$msg = [
'type' => 'file',
'action' => $action,
@@ -941,7 +949,6 @@ class File extends AbstractModel
'userid' => $userid,
'msg' => $msg
];
$task = new PushTask($params, false);
Task::deliver($task);
Task::deliver(new PushTask($params));
}
}

View File

@@ -70,7 +70,7 @@ class FileContent extends AbstractModel
'name' => $name,
'ext' => $fileExt
]));
return Base::fillUrl("online/preview/{$name}?key={$key}");
return Base::fillUrl("online/preview/{$name}?key={$key}&version=" . Base::getVersion() . "&__=" . Base::msecTime());
}
/**

View File

@@ -8,7 +8,7 @@ use App\Module\Base;
* App\Models\FileLink
*
* @property int $id
* @property int|null $file_id 文件ID
* @property int|null $file_id 项目ID
* @property int|null $num 累计访问
* @property string|null $code 链接码
* @property int|null $userid 会员ID

View File

@@ -13,7 +13,7 @@ use App\Module\Base;
* @property string|null $name 名称
* @property string|null $status 状态
* @property array $turns 可流转
* @property array $userids 状态负责人ID
* @property array $userids 自动负责人ID
* @property string|null $usertype 流转模式
* @property int|null $userlimit 限制负责人
* @property int|null $columnid 对应的项目列表

View File

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

View File

@@ -0,0 +1,201 @@
<?php
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
/**
* App\Models\ProjectPermission
*
* @property int $id
* @property int|null $project_id 项目ID
* @property string $permissions 权限
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission query()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission wherePermissions($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission whereProjectId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission whereUpdatedAt($value)
* @mixin \Eloquent
*/
class ProjectPermission extends AbstractModel
{
const TASK_LIST_ADD = 'task_list_add'; // 添加列
const TASK_LIST_UPDATE = 'task_list_update'; // 修改列
const TASK_LIST_REMOVE = 'task_list_remove'; // 删除列
const TASK_LIST_SORT = 'task_list_sort'; // 列表排序
const TASK_ADD = 'task_add'; // 任务添加
const TASK_UPDATE = 'task_update'; // 任务更新
const TASK_STATUS = 'task_status'; // 任务状态
const TASK_REMOVE = 'task_remove'; // 任务删除
const TASK_ARCHIVED = 'task_archived'; // 任务归档
const TASK_MOVE = 'task_move'; // 任务移动
// 权限列表
const PERMISSIONS = [
'project_leader' => 1, // 项目负责人
'project_member' => 2, // 项目成员
'task_leader' => 3, // 任务负责人
'task_assist' => 4, // 任务协助人
];
// 权限描述
const PERMISSIONS_DESC = [
1 => "项目负责人",
2 => "项目成员",
3 => "任务负责人",
4 => "任务协助人",
];
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['project_id', 'permissions'];
/**
* 权限
* @param $value
* @return string
*/
public function getPermissionsAttribute($value)
{
return Base::json2array($value);
}
/**
* 获取权限值
*
* @param int $projectId
* @param string $key
* @return object|array
*/
public static function getPermission($projectId, $key = '')
{
$projectPermission = self::initPermissions($projectId);
$currentPermissions = $projectPermission->permissions;
if ($key) {
if (!isset($currentPermissions[$key])) {
throw new ApiException('项目权限设置不存在');
}
return $currentPermissions[$key];
}
return $projectPermission;
}
/**
* 初始化项目权限
*
* @param int $projectId
* @return ProjectPermission
*/
public static function initPermissions($projectId)
{
$permissions = [
self::TASK_LIST_ADD => $projectTaskList = [self::PERMISSIONS['project_leader'], self::PERMISSIONS['project_member']],
self::TASK_LIST_UPDATE => $projectTaskList,
self::TASK_LIST_REMOVE => [self::PERMISSIONS['project_leader']],
self::TASK_LIST_SORT => $projectTaskList,
self::TASK_ADD => $projectTaskList,
self::TASK_UPDATE => [self::PERMISSIONS['project_leader'], self::PERMISSIONS['task_leader'], self::PERMISSIONS['task_assist']],
self::TASK_STATUS => $taskStatus = [self::PERMISSIONS['project_leader'], self::PERMISSIONS['task_leader']],
self::TASK_REMOVE => $taskStatus,
self::TASK_ARCHIVED => $taskStatus,
self::TASK_MOVE => $taskStatus
];
return self::firstOrCreate(
['project_id' => $projectId],
['permissions' => Base::array2json($permissions)]
);
}
/**
* 更新项目权限
*
* @param int $projectId
* @param array $permissions
* @return ProjectPermission
*/
public static function updatePermissions($projectId, $newPermissions)
{
$projectPermission = self::initPermissions($projectId);
$currentPermissions = $projectPermission->permissions;
$mergedPermissions = empty($newPermissions) ? $currentPermissions : array_merge($currentPermissions, $newPermissions);
$projectPermission->permissions = Base::array2json($mergedPermissions);
$projectPermission->save();
return $projectPermission;
}
/**
* 检查用户是否有执行特定动作的权限
* @param string $action 动作名称
* @param Project $project 项目实例
* @param ProjectTask $task 任务实例
* @return bool
*/
public static function userTaskPermission(Project $project, $action, ProjectTask $task = null)
{
$userid = User::userid();
$permissions = self::getPermission($project->id, $action);
switch ($action) {
// 任务添加,任务更新, 任务状态, 任务删除, 任务完成, 任务归档, 任务移动
case self::TASK_LIST_ADD:
case self::TASK_LIST_UPDATE:
case self::TASK_LIST_REMOVE:
case self::TASK_LIST_SORT:
case self::TASK_ADD:
case self::TASK_UPDATE:
case self::TASK_STATUS:
case self::TASK_REMOVE:
case self::TASK_ARCHIVED:
case self::TASK_MOVE:
$verify = false;
// 项目负责人
if (in_array(self::PERMISSIONS['project_leader'], $permissions)) {
if ($project->owner) {
$verify = true;
}
}
// 项目成员
if (!$verify && in_array(self::PERMISSIONS['project_member'], $permissions)) {
$user = ProjectUser::whereProjectId($project->id)->whereUserid(intval($userid))->first();
if (!empty($user)) {
$verify = true;
}
}
// 任务负责人
if (!$verify && $task && in_array(self::PERMISSIONS['task_leader'], $permissions)) {
if ($task->isOwner()) {
$verify = true;
}
}
// 任务协助人
if (!$verify && $task && in_array(self::PERMISSIONS['task_assist'], $permissions)) {
if ($task->isAssister()) {
$verify = true;
}
}
//
if (!$verify) {
$desc = [];
rsort($permissions);
foreach ($permissions as $permission) {
$desc[] = self::PERMISSIONS_DESC[$permission];
}
$desc = array_reverse($desc);
throw new ApiException(sprintf("仅限%s操作", implode('、', $desc)));
}
break;
}
return true;
}
}

View File

@@ -2,15 +2,16 @@
namespace App\Models;
use App\Exceptions\ApiException;
use DB;
use Arr;
use Request;
use Carbon\Carbon;
use App\Module\Base;
use App\Tasks\PushTask;
use Arr;
use Carbon\Carbon;
use DB;
use App\Exceptions\ApiException;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use App\Models\ProjectTaskVisibilityUser;
use Illuminate\Database\Eloquent\SoftDeletes;
use Request;
/**
* App\Models\ProjectTask
@@ -32,7 +33,7 @@ use Request;
* @property int|null $archived_follow 跟随项目归档(项目取消归档时任务也取消归档)
* @property string|null $complete_at 完成时间
* @property int|null $userid 创建人
* @property int|null $is_all_visible 是否所有人可见
* @property int|null $visibility 任务可见性1-项目人员 2-任务人员 3-指定成员
* @property int|null $p_level 优先级
* @property string|null $p_name 优先级名称
* @property string|null $p_color 优先级颜色
@@ -81,7 +82,6 @@ use Request;
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereFlowItemId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereFlowItemName($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereIsAllVisible($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereLoop($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereLoopAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereName($value)
@@ -94,6 +94,7 @@ use Request;
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereStartAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereVisibility($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask withoutTrashed()
* @mixin \Eloquent
@@ -328,13 +329,7 @@ class ProjectTask extends AbstractModel
$query->where(function ($q1) use ($start, $end, $type) {
switch ($type) {
case 'createdTime':
$q1->where(function ($q2) use ($start) {
$q2->where('project_tasks.created_at', '>=', $start);
})->orWhere(function ($q2) use ($end) {
$q2->where('project_tasks.created_at', '<=', $end);
})->orWhere(function ($q2) use ($start, $end) {
$q2->where('project_tasks.created_at', '>', $start)->where('project_tasks.created_at', '<', $end);
});
$q1->where('project_tasks.created_at', '>=', $start)->where('project_tasks.created_at', '<=', $end);
break;
default:
@@ -494,13 +489,12 @@ class ProjectTask extends AbstractModel
])->save();
}
// 可见性
foreach ($visibility_userids as $uid) {
ProjectTaskUser::createInstance([
ProjectTaskVisibilityUser::createInstance([
'project_id' => $task->project_id,
'task_id' => $task->id,
'task_pid' => $task->parent_id ?: $task->id,
'userid' => $uid,
'owner' => 2,
'userid' => $uid
])->save();
}
@@ -549,15 +543,12 @@ class ProjectTask extends AbstractModel
*/
public function updateTask($data, &$updateMarking = [])
{
//
AbstractModel::transaction(function () use ($data, &$updateMarking) {
// 主任务
$mainTask = $this->parent_id > 0 ? self::find($this->parent_id) : null;
// 工作流
if (Arr::exists($data, 'flow_item_id')) {
$isProjectOwner = $this->useridInTheProject(User::userid()) === 2;
if (!$isProjectOwner && !$this->isOwner()) {
throw new ApiException('仅限项目或任务负责人修改任务状态');
}
if ($this->flow_item_id == $data['flow_item_id']) {
throw new ApiException('任务状态未发生改变');
}
@@ -578,6 +569,7 @@ class ProjectTask extends AbstractModel
throw new ApiException("当前状态[{$currentFlowItem->name}]不可流转到[{$newFlowItem->name}]");
}
if ($currentFlowItem->userlimit) {
$isProjectOwner = $this->useridInTheProject(User::userid()) === 2;
if (!$isProjectOwner && !in_array(User::userid(), $currentFlowItem->userids)) {
throw new ApiException("当前状态[{$currentFlowItem->name}]仅限状态负责人或项目负责人修改");
}
@@ -729,16 +721,14 @@ class ProjectTask extends AbstractModel
ProjectTask::whereId($data['task_id'])->update(['visibility' => $data["visibility"]]);
ProjectTask::whereParentId($data['task_id'])->update(['visibility' => $data["visibility"]]);
}
ProjectTaskUser::whereTaskId($data['task_id'])->whereOwner(2)->delete();
ProjectTaskVisibilityUser::whereTaskId($data['task_id'])->delete();
if (Arr::exists($data, 'visibility_appointor')) {
foreach ($data['visibility_appointor'] as $uid) {
if ($uid) {
ProjectTaskUser::createInstance([
ProjectTaskVisibilityUser::createInstance([
'project_id' => $this->project_id,
'task_id' => $this->id,
'task_pid' => $this->parent_id ?: $this->id,
'userid' => $uid,
'owner' => 2,
'userid' => $uid
])->save();
}
}
@@ -1434,53 +1424,62 @@ class ProjectTask extends AbstractModel
}
//
$array = [];
if (empty($data['parent_id'])) {
if (Arr::exists($data, 'owner') || Arr::exists($data, 'assist')) {
$taskUser = ProjectTaskUser::select(['userid', 'owner'])->whereTaskId($data['id'])->get();
// 负责人
$owners = $taskUser->where('owner', 1)->pluck('userid')->toArray();
$owners = array_intersect($userids, $owners);
if ($owners) {
$array[] = [
'userid' => array_values($owners),
'data' => array_merge($data, [
'owner' => 1,
'assist' => 1,
])
];
}
// 协助人
$assists = $taskUser->where('owner', 0)->pluck('userid')->toArray();
$assists = array_intersect($userids, $assists);
if ($assists) {
$array[] = [
'userid' => array_values($assists),
'data' => array_merge($data, [
'owner' => 0,
'assist' => 1,
])
];
}
// 项目成员(其他人)
if ($data['visibility'] == 1) {
// 全部可见
if (Arr::exists($data, 'owner') || Arr::exists($data, 'assist')) {
$taskUser = ProjectTaskUser::select(['userid', 'owner'])->whereTaskId($data['id'])->get();
// 负责人
$owners = $taskUser->where('owner', 1)->pluck('userid')->toArray();
$owners = array_intersect($userids, $owners);
if ($owners) {
$array[] = [
'userid' => array_values($owners),
'data' => array_merge($data, [
'owner' => 1,
'assist' => 1,
])
];
}
// 协助人
$assists = $taskUser->where('owner', 0)->pluck('userid')->toArray();
$assists = array_intersect($userids, $assists);
if ($assists) {
$array[] = [
'userid' => array_values($assists),
'data' => array_merge($data, [
'owner' => 0,
'assist' => 1,
])
];
}
// 其他人
switch ($data['visibility']) {
case 1:
// 项目人员,除了负责人、协助人项目其他人
$userids = array_diff($userids, $owners, $assists);
} else {
// 指定可见
$userids = $taskUser->pluck('userid')->toArray();
}
$data = array_merge($data, [
'owner' => 0,
'assist' => 0,
]);
break;
case 2:
// 任务人员,除了负责人、协助人
$userids = [];
break;
case 3:
// 指定成员
$specifys = ProjectTaskVisibilityUser::select(['userid'])->whereTaskId($data['id'])->pluck('userid')->toArray();
$userids = array_diff($specifys, $owners, $assists);
break;
default:
$userids = [];
break;
}
if ($userids) {
$array[] = [
'userid' => array_values($userids),
'data' => array_merge($data, [
'owner' => 0,
'assist' => 0,
])
];
}
}
//
$array[] = [
'userid' => array_values($userids),
'data' => $data
];
//
foreach ($array as $item) {
$params = [
'ignoreFd' => Request::header('fd'),
@@ -1521,7 +1520,10 @@ class ProjectTask extends AbstractModel
if ($pushUserIds) {
$userids = $pushUserIds;
} elseif ($this->visibility != 1) {
$userids = ProjectTaskUser::select(['userid', 'owner'])->whereTaskId($this->id)->orWhere('task_pid', '=', $this->id)->pluck('userid')->toArray();
$userids = ProjectTaskUser::whereTaskId($this->id)->orWhere('task_pid', '=', $this->id)->pluck('userid')->toArray();
if ($this->visibility == 3) {
$userids = array_merge($userids, ProjectTaskVisibilityUser::whereTaskId($this->id)->pluck('userid')->toArray());
}
} else {
$userids = ProjectUser::whereProjectId($this->project_id)->pluck('userid')->toArray(); // 项目成员
}
@@ -1667,13 +1669,21 @@ class ProjectTask extends AbstractModel
/**
* 移动任务
* @param int $project_id
* @param int $column_id
* @param int $project_id
* @param int $column_id
* @param int $flowItemId
* @param array $owner
* @param array $assist
* @return bool
*/
public function moveTask(int $projectId, int $columnId)
public function moveTask(int $projectId, int $columnId,int $flowItemId = 0,array $owner = [], array $assist = [])
{
AbstractModel::transaction(function () use($projectId, $columnId) {
AbstractModel::transaction(function () use($projectId, $columnId, $flowItemId, $owner, $assist) {
$newTaskUser = array_merge($owner, $assist);
//
$this->project_id = $projectId;
$this->column_id = $columnId;
$this->flow_item_id = $flowItemId;
// 任务内容
if($this->content){
$this->content->project_id = $projectId;
@@ -1690,15 +1700,24 @@ class ProjectTask extends AbstractModel
$taskTag->save();
}
// 任务用户
$this->updateTask(['owner' => $owner]);
$this->updateTask(['assist' => $assist]);
foreach ($this->taskUser as $taskUser){
$taskUser->project_id = $projectId;
$taskUser->save();
if( in_array($taskUser->id, $newTaskUser) ){
$taskUser->project_id = $projectId;
$taskUser->save();
}
}
//
$this->project_id = $projectId;
$this->column_id = $columnId;
//
if($flowItemId){
$flowItem = projectFlowItem::whereProjectId($projectId)->whereId($flowItemId)->first();
$this->flow_item_name = $flowItem->status . "|" . $flowItem->name;
}else{
$this->flow_item_name = '';
}
//
$this->save();
//
//
$this->addLog("移动{任务}");
});
$this->pushMsg('update');
@@ -1728,14 +1747,10 @@ class ProjectTask extends AbstractModel
* @param int $task_id
* @param bool $archived true:仅限未归档, false:仅限已归档, null:不限制
* @param bool $trashed true:仅限未删除, false:仅限已删除, null:不限制
* @param int|bool $permission
* - 0|false 限制:项目成员、任务成员、任务群聊成员(任务成员 = 任务创建人+任务协助人+任务负责人)
* - 1|true 限制:项目负责人、任务成员
* - 2 已有负责人才限制true (子任务时如果是主任务负责人也可以)
* @param array $with
* @return self
*/
public static function userTask($task_id, $archived = true, $trashed = true, $permission = false, $with = [])
public static function userTask($task_id, $archived = true, $trashed = true, $with = [])
{
$builder = self::with($with)->allData()->where("project_tasks.id", intval($task_id));
if ($trashed === false) {
@@ -1758,7 +1773,7 @@ class ProjectTask extends AbstractModel
try {
$project = Project::userProject($task->project_id);
} catch (\Throwable $e) {
if ($task->owner !== null || (!$permission && $task->permission(4))) {
if ($task->owner !== null || $task->permission(4)) {
$project = Project::find($task->project_id);
if (empty($project)) {
throw new ApiException('项目不存在或已被删除', [ 'task_id' => $task_id ], -4002);
@@ -1768,13 +1783,6 @@ class ProjectTask extends AbstractModel
}
}
//
if ($permission >= 2) {
$permission = $task->hasOwner() ? 1 : 0;
}
if ($permission && !$project->owner && !$task->permission(3)) {
throw new ApiException('仅限项目负责人、任务负责人、协助人员或任务创建者操作');
}
//
return $task;
}
}

View File

@@ -60,6 +60,8 @@ class ProjectTaskContent extends AbstractModel
*/
public static function saveContent($task_id, $content)
{
@ini_set("pcre.backtrack_limit", 999999999);
//
$oldContent = $content;
$path = 'uploads/task/content/' . date("Ym") . '/' . $task_id . '/';
//
@@ -80,10 +82,7 @@ class ProjectTaskContent extends AbstractModel
$publicPath = public_path($filePath);
Base::makeDir(dirname($publicPath));
$result = file_put_contents($publicPath, $content);
if(!$result){
info("保存任务详情至文件失败");
info($publicPath);
info($oldContent);
if(!$result && $oldContent){
throw new ApiException("保存任务详情至文件失败,请重试");
}
//

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Models;
/**
* App\Models\ProjectTaskVisibilityUser
*
* @property int $id
* @property int|null $project_id 项目ID
* @property int|null $task_id 任务ID
* @property int|null $userid 成员ID
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\ProjectTask|null $projectTask
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser query()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser whereProjectId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser whereTaskId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser whereUserid($value)
* @mixin \Eloquent
*/
class ProjectTaskVisibilityUser extends AbstractModel
{
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function projectTask(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(ProjectTask::class, 'id', 'task_id');
}
}

View File

@@ -31,6 +31,16 @@ class UmengAlias extends AbstractModel
{
protected $table = 'umeng_alias';
/**
* 推送内容处理
* @param $string
* @return string
*/
private static function specialCharacters($string)
{
return str_replace(["\r\n", "\r", "\n"], '', $string);
}
/**
* 获取推送配置
* @return array|false
@@ -73,9 +83,9 @@ class UmengAlias extends AbstractModel
return false;
}
//
$title = $array['title'] ?: ''; // 标题
$subtitle = $array['subtitle'] ?: ''; // 副标题iOS
$body = $array['body'] ?: ''; // 通知内容
$title = self::specialCharacters($array['title'] ?: ''); // 标题
$subtitle = self::specialCharacters($array['subtitle'] ?: ''); // 副标题iOS
$body = self::specialCharacters($array['body'] ?: ''); // 通知内容
$description = $array['description'] ?: 'no description'; // 描述
$extra = is_array($array['extra']) ? $array['extra'] : []; // 额外参数
$seconds = intval($array['seconds']) ?: 86400; // 有效时间(单位:秒)

View File

@@ -290,6 +290,26 @@ class User extends AbstractModel
});
}
/**
* 检查发送聊天内容前必须设置昵称、电话
* @return void
*/
public function checkChatInformation()
{
if ($this->bot) {
return;
}
$chatInformation = Base::settingFind('system', 'chat_information');
if ($chatInformation == 'required') {
if (empty($this->getRawOriginal('nickname'))) {
throw new ApiException('请设置昵称', [], -2);
}
if (empty($this->getRawOriginal('tel'))) {
throw new ApiException('请设置联系电话', [], -3);
}
}
}
/** ***************************************************************************************** */
/** ***************************************************************************************** */
/** ***************************************************************************************** */
@@ -539,6 +559,8 @@ class User extends AbstractModel
return url("images/avatar/default_openai.png");
case 'ai-claude@bot.system':
return url("images/avatar/default_claude.png");
case 'ai-gemini@bot.system':
return url("images/avatar/default_gemini.png");
case 'bot-manager@bot.system':
return url("images/avatar/default_bot.png");
case 'meeting-alert@bot.system':

View File

@@ -5,6 +5,7 @@ namespace App\Models;
use App\Module\Base;
use App\Module\Doo;
use App\Module\Extranet;
use App\Tasks\JokeSoupTask;
use Cache;
use Carbon\Carbon;
@@ -57,6 +58,7 @@ class UserBot extends AbstractModel
'ai-claude' => 'Claude',
'ai-wenxin' => '文心一言',
'ai-qianwen' => '通义千问',
'ai-gemini' => 'Gemini',
'bot-manager' => '机器人管理',
'meeting-alert' => '会议通知',
'okr-alert' => 'OKR提醒',
@@ -114,6 +116,16 @@ class UserBot extends AbstractModel
'label' => Doo::translate('我的机器人')
],
],
'ai-openai@bot.system',
'ai-claude@bot.system',
'ai-wenxin@bot.system',
'ai-gemini@bot.system',
'ai-qianwen@bot.system' => [
[
'key' => '%3A.clear',
'label' => Doo::translate('清空上下文')
]
],
default => [],
};
@@ -218,8 +230,8 @@ class UserBot extends AbstractModel
if ($checkins && $botUser = User::botGetOrCreate('check-in')) {
$getJokeSoup = function($type) {
$pre = $type == "up" ? "每日开心:" : "心灵鸡汤:";
$key = $type == "up" ? "JokeSoupTask:jokes" : "JokeSoupTask:soups";
$array = Base::json2array(Cache::get($key));
$key = $type == "up" ? "jokes" : "soups";
$array = Base::json2array(Cache::get(JokeSoupTask::keyName($key)));
if ($array) {
$item = $array[array_rand($array)];
if ($item) {

View File

@@ -10,6 +10,7 @@ use Cache;
use Carbon\Carbon;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\DB;
/**
* App\Models\WebSocketDialog
@@ -19,9 +20,10 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property string|null $group_type 聊天室类型
* @property string|null $name 对话名称
* @property string $avatar 头像(群)
* @property string|null $last_at 最后消息时间
* @property int|null $owner_id 群主用户ID
* @property int|null $link_id 关联id
* @property int|null $top_userid 置顶的用户ID
* @property int|null $top_msg_id 置顶的消息ID
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
@@ -36,10 +38,11 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereGroupType($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereLastAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereLinkId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereOwnerId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereTopMsgId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereTopUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog withTrashed()
@@ -76,9 +79,9 @@ class WebSocketDialog extends AbstractModel
* @param $deleted
* @return array
*/
public function getDialogList($userid, $updated = "", $deleted = "")
public static function getDialogList($userid, $updated = "", $deleted = "")
{
$builder = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread', 'u.silence', 'u.color', 'u.updated_at as user_at'])
$builder = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->where('u.userid', $userid);
if ($updated) {
@@ -86,7 +89,7 @@ class WebSocketDialog extends AbstractModel
}
$list = $builder
->orderByDesc('u.top_at')
->orderByDesc('web_socket_dialogs.last_at')
->orderByDesc('u.last_at')
->paginate(Base::getPaginate(100, 50));
$list->transform(function (WebSocketDialog $item) use ($userid) {
return $item->formatData($userid);
@@ -99,6 +102,74 @@ class WebSocketDialog extends AbstractModel
return $data;
}
/**
* 列表外的未读对话 和 列表外的待办对话
* @param $userid
* @param $unreadAt
* @param $todoAt
* @return WebSocketDialog[]
*/
public static function getDialogBeyond($userid, $unreadAt, $todoAt)
{
DB::statement("SET SQL_MODE=''");
$ids = [];
$array = [];
if ($unreadAt) {
// 未读对话
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->join('web_socket_dialog_msg_reads as r', 'web_socket_dialogs.id', '=', 'r.dialog_id')
->where('u.userid', $userid)
->where('r.userid', $userid)
->where('r.read_at')
->where('u.last_at', '<', $unreadAt)
->groupBy('u.dialog_id')
->take(20)
->get();
$list->transform(function (WebSocketDialog $item) use ($userid, &$ids, &$array) {
if (!in_array($item->id, $ids)) {
$ids[] = $item->id;
$array[] = $item->formatData($userid);
}
});
// 标记未读会话
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->where('u.userid', $userid)
->where('u.mark_unread', 1)
->where('u.last_at', '<', $unreadAt)
->take(20)
->get();
$list->transform(function (WebSocketDialog $item) use ($userid, &$ids, &$array) {
if (!in_array($item->id, $ids)) {
$ids[] = $item->id;
$array[] = $item->formatData($userid);
}
});
}
if ($todoAt) {
// 待办会话
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->join('web_socket_dialog_msg_todos as t', 'web_socket_dialogs.id', '=', 't.dialog_id')
->where('u.userid', $userid)
->where('t.userid', $userid)
->where('t.done_at')
->where('u.last_at', '<', $todoAt)
->groupBy('u.dialog_id')
->take(20)
->get();
$list->transform(function (WebSocketDialog $item) use ($userid, &$ids, &$array) {
if (!in_array($item->id, $ids)) {
$ids[] = $item->id;
$array[] = $item->formatData($userid);
}
});
}
return $array;
}
/**
* 格式化对话
* @param int $userid 会员ID
@@ -115,17 +186,23 @@ class WebSocketDialog extends AbstractModel
};
//
$time = Carbon::parse($this->user_at ?? $dialogUserFun('updated_at'));
$this->hide = $this->hide ?? $dialogUserFun('hide');
$this->top_at = $this->top_at ?? $dialogUserFun('top_at');
$this->last_at = $this->last_at ?? $dialogUserFun('last_at');
$this->user_at = $time->toDateTimeString('millisecond');
$this->user_ms = $time->valueOf();
//
if (isset($this->search_msg_id)) {
// 最后消息 (搜索预览消息)
$this->last_msg = WebSocketDialogMsg::whereDialogId($this->id)->find($this->search_msg_id);
$this->last_at = $this->last_msg?->created_at;
$this->last_at = $this->last_msg ? Carbon::parse($this->last_msg->created_at)->format('Y-m-d H:i:s') : null;
} else {
// 未读信息
$this->generateUnread($userid, $hasData);
if (Base::judgeClientVersion("0.34.0")) {
$this->generateUnread($userid);
} else {
$this->generateUnread_03398($userid, $hasData);
}
// 未读标记
$this->mark_unread = $this->mark_unread ?? $dialogUserFun('mark_unread');
// 是否免打扰
@@ -161,9 +238,13 @@ class WebSocketDialog extends AbstractModel
$this->dialog_delete = 1;
}
$this->dialog_user = $dialog_user;
$this->dialog_mute = Base::settingFind('system', 'user_private_chat_mute');
break;
case "group":
switch ($this->group_type) {
case 'user':
$this->dialog_mute = Base::settingFind('system', 'user_group_chat_mute');
break;
case 'project':
$this->group_info = Project::withTrashed()->select(['id', 'name', 'archived_at', 'deleted_at'])->whereDialogId($this->id)->first()?->cancelAppend()->cancelHidden();
if ($this->group_info) {
@@ -184,7 +265,7 @@ class WebSocketDialog extends AbstractModel
break;
case 'all':
$this->name = Doo::translate('全体成员');
$this->all_group_mute = Base::settingFind('system', 'all_group_mute');
$this->dialog_mute = Base::settingFind('system', 'all_group_mute');
break;
}
break;
@@ -192,10 +273,26 @@ class WebSocketDialog extends AbstractModel
if ($hasData === true) {
$msgBuilder = WebSocketDialogMsg::whereDialogId($this->id);
$this->has_tag = $msgBuilder->clone()->where('tag', '>', 0)->exists();
$this->has_todo = $msgBuilder->clone()->where('todo', '>', 0)->exists();
$this->has_image = $msgBuilder->clone()->whereMtype('image')->exists();
$this->has_file = $msgBuilder->clone()->whereMtype('file')->exists();
$this->has_link = $msgBuilder->clone()->whereLink(1)->exists();
$this->has_todo = $msgBuilder->clone()->where('todo', '>', 0)->exists();
Cache::forever("Dialog::tag:" . $this->id, Base::array2json([
'has_tag' => $this->has_tag,
'has_todo' => $this->has_todo,
'has_image' => $this->has_image,
'has_file' => $this->has_file,
'has_link' => $this->has_link,
]));
} else {
$tagData = Base::json2array(Cache::get("Dialog::tag:" . $this->id));
if ($tagData) {
$this->has_tag = !!$tagData['has_tag'];
$this->has_todo = !!$tagData['has_todo'];
$this->has_image = !!$tagData['has_image'];
$this->has_file = !!$tagData['has_file'];
$this->has_link = !!$tagData['has_link'];
}
}
return $this;
}
@@ -203,10 +300,29 @@ class WebSocketDialog extends AbstractModel
/**
* 生成未读数据
* @param $userid
* @return $this
*/
public function generateUnread($userid)
{
$builder = WebSocketDialogMsgRead::whereDialogId($this->id)->whereUserid($userid)->whereReadAt(null);
// 未读消息
$this->unread = $builder->count();
// 最早一条未读消息
$this->unread_one = $this->unread > 0 ? intval($builder->clone()->orderBy('msg_id')->value('msg_id')) : 0;
// @我的消息
$this->mention = $this->unread > 0 ? $builder->clone()->whereMention(1)->count() : 0;
// @我的消息id集合
$this->mention_ids = $this->mention > 0 ? $builder->clone()->whereMention(1)->orderByDesc('msg_id')->take(20)->pluck('msg_id')->toArray() : [];
return $this;
}
/**
* 生成未读数据 // todo: 旧版兼容,后续删除
* @param $userid
* @param $positionData
* @return $this
*/
public function generateUnread($userid, $positionData = false)
public function generateUnread_03398($userid, $positionData = false)
{
$builder = WebSocketDialogMsgRead::whereDialogId($this->id)->whereUserid($userid)->whereReadAt(null);
$this->unread = $builder->count();
@@ -214,12 +330,14 @@ class WebSocketDialog extends AbstractModel
if ($positionData) {
$array = [];
// @我的消息
if ($this->mention > 0
&& $mention_id = intval($builder->clone()->whereMention(1)->orderByDesc('msg_id')->value('msg_id'))) {
$array[] = [
'msg_id' => $mention_id,
'label' => Doo::translate('@我的消息'),
];
if ($this->mention > 0) {
$list = $builder->clone()->whereMention(1)->orderByDesc('msg_id')->take(20)->get();
foreach ($list as $item) {
$array[] = [
'msg_id' => $item->msg_id,
'label' => Doo::translate('@我的消息'),
];
}
}
// 最早一条未读消息
if ($this->unread > 0
@@ -386,17 +504,37 @@ class WebSocketDialog extends AbstractModel
*/
public function checkMute($userid)
{
if ($this->group_type === 'all') {
$allGroupMute = Base::settingFind('system', 'all_group_mute');
switch ($allGroupMute) {
case 'all':
throw new ApiException('当前会话全员禁言');
case 'user':
if (!User::find($userid)?->isAdmin()) {
throw new ApiException('当前会话禁言');
$muteMsgTip = null;
$systemConfig = Base::setting('system');
switch ($this->type) {
case 'user':
if ($systemConfig['user_private_chat_mute'] === 'close') {
$muteMsgTip = '个人会话禁言';
}
break;
case 'group':
if ($this->group_type === 'user') {
if ($systemConfig['user_group_chat_mute'] === 'close') {
$muteMsgTip = '个人群组禁言';
}
} elseif ($this->group_type === 'all') {
if ($systemConfig['all_group_mute'] === 'close') {
$muteMsgTip = '当前会话全员禁言';
}
}
break;
}
if ($muteMsgTip === null) {
return;
}
if ($userid) {
$user = User::find($userid);
if ($user?->bot || $user?->isAdmin()) { // 机器人或管理员不受禁言
return;
}
}
throw new ApiException($muteMsgTip);
}
/**
@@ -454,20 +592,6 @@ class WebSocketDialog extends AbstractModel
Task::deliver($task);
}
/**
* 更新对话最后消息时间
* @return WebSocketDialogMsg|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Query\Builder|object|null
*/
public function updateMsgLastAt()
{
$lastMsg = WebSocketDialogMsg::whereDialogId($this->id)->orderByDesc('id')->first();
if ($lastMsg) {
$this->last_at = $lastMsg->created_at;
$this->save();
}
return $lastMsg;
}
/**
* 获取对话(同时检验对话身份)
* @param $dialog_id
@@ -502,6 +626,7 @@ class WebSocketDialog extends AbstractModel
return $dialog;
}
if (!WebSocketDialogUser::whereDialogId($dialog->id)->whereUserid($userid)->exists()) {
WebSocketDialogMsgRead::forceRead($dialog_id, $userid);
throw new ApiException('不在成员列表内', ['dialog_id' => $dialog_id], -4003);
}
return $dialog;
@@ -523,7 +648,6 @@ class WebSocketDialog extends AbstractModel
'name' => $name ?: '',
'group_type' => $group_type,
'owner_id' => $owner_id,
'last_at' => in_array($group_type, ['user', 'department', 'all']) ? Carbon::now() : null,
]);
$dialog->save();
foreach (is_array($userid) ? $userid : [$userid] as $value) {
@@ -531,7 +655,8 @@ class WebSocketDialog extends AbstractModel
WebSocketDialogUser::createInstance([
'dialog_id' => $dialog->id,
'userid' => $value,
'important' => !in_array($group_type, ['user', 'all'])
'important' => !in_array($group_type, ['user', 'all']),
'last_at' => in_array($group_type, ['user', 'department', 'all']) ? Carbon::now() : null,
])->save();
}
}

View File

@@ -31,11 +31,12 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property int|null $modify 是否编辑
* @property int|null $reply_num 有多少条回复
* @property int|null $reply_id 回复ID
* @property int|null $forward_id 转发ID
* @property int|null $forward_num 被转发多少次
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read int|mixed $percentage
* @property-read \App\Models\WebSocketDialogMsg|null $reply_data
* @property-read \App\Models\WebSocketDialog|null $webSocketDialog
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg newQuery()
@@ -46,6 +47,8 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDialogType($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereEmoji($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereForwardId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereForwardNum($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereKey($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereLink($value)
@@ -71,7 +74,6 @@ class WebSocketDialogMsg extends AbstractModel
protected $appends = [
'percentage',
'reply_data',
];
protected $hidden = [
@@ -99,21 +101,6 @@ class WebSocketDialogMsg extends AbstractModel
return $this->appendattrs['percentage'];
}
/**
* 回复消息详情
* @return WebSocketDialogMsg|null
*/
public function getReplyDataAttribute()
{
if (!isset($this->appendattrs['reply_data'])) {
$this->appendattrs['reply_data'] = null;
if ($this->reply_id > 0) {
$this->appendattrs['reply_data'] = self::find($this->reply_id, ['id', 'userid', 'type', 'msg'])?->cancelAppend() ?: null;
}
}
return $this->appendattrs['reply_data'];
}
/**
* 消息格式化
* @param $value
@@ -124,13 +111,9 @@ class WebSocketDialogMsg extends AbstractModel
if (is_array($value)) {
return $value;
}
$value = Base::json2array($value);
if ($this->type === 'file') {
$value['type'] = in_array($value['ext'], ['jpg', 'jpeg', 'webp', 'png', 'gif']) ? 'img' : 'file';
$value['path'] = Base::fillUrl($value['path']);
$value['thumb'] = Base::fillUrl($value['thumb'] ?: Base::extIcon($value['ext']));
} else if ($this->type === 'record') {
$value['path'] = Base::fillUrl($value['path']);
$value = $this->formatDataMsg($this->type, $value);
if (isset($value['reply_data'])) {
$value['reply_data']['msg'] = $this->formatDataMsg($value['reply_data']['type'], $value['reply_data']['msg']);
}
return $value;
}
@@ -148,6 +131,27 @@ class WebSocketDialogMsg extends AbstractModel
return Base::json2array($value);
}
/**
* 处理消息数据
* @param $type
* @param $msg
* @return mixed
*/
private function formatDataMsg($type, $msg)
{
if (!is_array($msg)) {
$msg = Base::json2array($msg);
}
if ($type === 'file') {
$msg['type'] = in_array($msg['ext'], ['jpg', 'jpeg', 'webp', 'png', 'gif']) ? 'img' : 'file';
$msg['path'] = Base::fillUrl($msg['path']);
$msg['thumb'] = Base::fillUrl($msg['thumb'] ?: Base::extIcon($msg['ext']));
} else if ($type === 'record') {
$msg['path'] = Base::fillUrl($msg['path']);
}
return $msg;
}
/**
* 获取占比
* @param bool|int $increment 是否新增阅读数
@@ -368,13 +372,25 @@ class WebSocketDialogMsg extends AbstractModel
* 转发消息
* @param array|int $dialogids
* @param array|int $userids
* @param User $user 发送的会员
* @param User $user 发送的会员
* @param int $showSource 是否显示原发送者信息
* @param string $leaveMessage 转发留言
* @return mixed
*/
public function forwardMsg($dialogids, $userids, $user)
public function forwardMsg($dialogids, $userids, $user, $showSource = 1, $leaveMessage = '')
{
return AbstractModel::transaction(function() use ($dialogids, $user, $userids) {
$originalMsg = Base::json2array($this->getRawOriginal('msg'));
return AbstractModel::transaction(function () use ($dialogids, $user, $userids, $showSource, $leaveMessage) {
$msgData = Base::json2array($this->getRawOriginal('msg'));
$forwardData = is_array($msgData['forward_data']) ? $msgData['forward_data'] : [];
$forwardId = $forwardData['id'] ?: $this->id;
$forwardUserid = $forwardData['userid'] ?: $this->userid;
$msgData['forward_data'] = [
'id' => $forwardId, // 转发的消息ID原始
'userid' => $forwardUserid, // 转发的消息会员ID原始
'parent_id' => $this->id, // 转发的消息ID
'parent_userid' => $this->userid, // 转发的消息会员ID
'show' => $showSource, // 是否显示原发送者信息
];
$msgs = [];
$already = [];
if ($dialogids) {
@@ -382,11 +398,14 @@ class WebSocketDialogMsg extends AbstractModel
$dialogids = [$dialogids];
}
foreach ($dialogids as $dialogid) {
$res = self::sendMsg(null, $dialogid, $this->type, $originalMsg, $user->userid);
$res = self::sendMsg('forward-' . $forwardId, $dialogid, $this->type, $msgData, $user->userid);
if (Base::isSuccess($res)) {
$msgs[] = $res['data'];
$already[] = $dialogid;
}
if ($leaveMessage) {
self::sendMsg(null, $dialogid, 'text', ['text' => $leaveMessage], $user->userid);
}
}
}
if ($userids) {
@@ -399,13 +418,19 @@ class WebSocketDialogMsg extends AbstractModel
}
$dialog = WebSocketDialog::checkUserDialog($user, $userid);
if ($dialog && !in_array($dialog->id, $already)) {
$res = self::sendMsg(null, $dialog->id, $this->type, $originalMsg, $user->userid);
$res = self::sendMsg('forward-' . $forwardId, $dialog->id, $this->type, $msgData, $user->userid);
if (Base::isSuccess($res)) {
$msgs[] = $res['data'];
}
if ($leaveMessage) {
self::sendMsg(null, $dialog->id, 'text', ['text' => $leaveMessage], $user->userid);
}
}
}
}
if (count($msgs) > 0) {
$this->increment('forward_num', count($msgs));
}
return Base::retSuccess('转发成功', [
'msgs' => $msgs
]);
@@ -427,9 +452,8 @@ class WebSocketDialogMsg extends AbstractModel
WebSocketDialogMsgTodo::whereIn('msg_id', $ids)->delete();
self::whereIn('id', $ids)->delete();
//
$dialogDatas = WebSocketDialog::whereIn('id', $dialogIds)->get();
foreach ($dialogDatas as $dialogData) {
$dialogData->updateMsgLastAt();
foreach ($dialogIds as $dialogId) {
WebSocketDialogUser::updateMsgLastAt($dialogId);
}
foreach ($replyIds as $id) {
self::whereId($id)->update(['reply_num' => self::whereReplyId($id)->count()]);
@@ -470,7 +494,7 @@ class WebSocketDialogMsg extends AbstractModel
'data' => [
'id' => $this->id,
'dialog_id' => $this->dialog_id,
'last_msg' => $dialogData->updateMsgLastAt(),
'last_msg' => WebSocketDialogUser::updateMsgLastAt($this->dialog_id),
'update_read' => $deleteRead ? 1 : 0
],
]
@@ -497,6 +521,8 @@ class WebSocketDialogMsg extends AbstractModel
}
switch ($data['type']) {
case 'text':
case 'word-chain':
case 'vote':
return $this->previewTextMsg($data['msg']['text'], $preserveHtml);
case 'record':
return "[语音]";
@@ -510,6 +536,9 @@ class WebSocketDialogMsg extends AbstractModel
case 'tag':
$action = $data['msg']['action'] === 'remove' ? '取消标注' : '标注';
return "[{$action}] {$this->previewMsg(false, $data['msg']['data'])}";
case 'top':
$action = $data['msg']['action'] === 'remove' ? '取消置顶' : '置顶';
return "[{$action}] {$this->previewMsg(false, $data['msg']['data'])}";
case 'todo':
$action = $data['msg']['action'] === 'remove' ? '取消待办' : ($data['msg']['action'] === 'done' ? '完成' : '设待办');
return "[{$action}] {$this->previewMsg(false, $data['msg']['data'])}";
@@ -772,6 +801,7 @@ class WebSocketDialogMsg extends AbstractModel
* - reply-98回复消息ID=98
* - update-99更新消息ID=99标记修改
* - change-99更新消息ID=99不标记修改
* - forward-99转发消息ID=99
* @param int $dialog_id 会话ID即 聊天室ID
* @param string $type 消息类型
* @param array $msg 发送的消息
@@ -809,9 +839,10 @@ class WebSocketDialogMsg extends AbstractModel
$push_silence = !in_array($type, ["text", "file", "record", "meeting"]);
}
//
$update_id = preg_match("/^update-(\d+)$/", $action, $match) ? $match[1] : 0;
$change_id = preg_match("/^change-(\d+)$/", $action, $match) ? $match[1] : 0;
$reply_id = preg_match("/^reply-(\d+)$/", $action, $match) ? $match[1] : 0;
$update_id = intval(preg_match("/^update-(\d+)$/", $action, $match) ? $match[1] : 0);
$change_id = intval(preg_match("/^change-(\d+)$/", $action, $match) ? $match[1] : 0);
$reply_id = intval(preg_match("/^reply-(\d+)$/", $action, $match) ? $match[1] : 0);
$forward_id = intval(preg_match("/^forward-(\d+)$/", $action, $match) ? $match[1] : 0);
$sender = $sender === null ? User::userid() : $sender;
//
$dialog = WebSocketDialog::find($dialog_id);
@@ -833,10 +864,10 @@ class WebSocketDialogMsg extends AbstractModel
if (empty($dialogMsg)) {
throw new ApiException('消息不存在');
}
if ($dialogMsg->type !== 'text') {
if ($dialogMsg->type !== 'text' && $dialogMsg->type !== 'vote') {
throw new ApiException('此消息不支持此操作');
}
if ($dialogMsg->userid != $sender) {
if ($dialogMsg->userid != $sender && $dialogMsg->type !== 'vote') {
throw new ApiException('仅支持修改自己的消息');
}
//
@@ -850,6 +881,11 @@ class WebSocketDialogMsg extends AbstractModel
$dialogMsg->key = $dialogMsg->generateMsgKey();
$dialogMsg->save();
//
WebSocketDialogUser::whereDialogId($dialog->id)->whereUserid($sender)->whereHide(1)->change([
'hide' => 0, // 修改消息时,显示会话(仅自己)
'updated_at' => Carbon::now()->toDateTimeString('millisecond'),
]);
//
$dialogMsg->msgJoinGroup($dialog);
//
$dialog->pushMsg('update', array_merge($updateData, [
@@ -859,14 +895,28 @@ class WebSocketDialogMsg extends AbstractModel
return Base::retSuccess('修改成功', $dialogMsg);
} else {
// 发送
if ($reply_id && !self::whereId($reply_id)->increment('reply_num')) {
throw new ApiException('回复的消息不存在');
if ($reply_id) {
// 回复
$replyRow = self::whereId($reply_id)->whereDialogId($dialog_id)->first();
if (empty($replyRow)) {
throw new ApiException('回复的消息不存在');
}
$replyMsg = Base::json2array($replyRow->getRawOriginal('msg'));
unset($replyMsg['reply_data']);
$msg['reply_data'] = [
'id' => $replyRow->id,
'userid' => $replyRow->userid,
'type' => $replyRow->type,
'msg' => $replyMsg,
];
$replyRow->increment('reply_num');
}
//
$dialogMsg = self::createInstance([
'dialog_id' => $dialog_id,
'dialog_type' => $dialog->type,
'reply_id' => $reply_id,
'forward_id' => $forward_id,
'userid' => $sender,
'type' => $type,
'mtype' => $mtype,
@@ -874,13 +924,16 @@ class WebSocketDialogMsg extends AbstractModel
'msg' => $msg,
'read' => 0,
]);
AbstractModel::transaction(function () use ($dialog, $dialogMsg) {
$dialog->last_at = Carbon::now();
$dialog->save();
AbstractModel::transaction(function () use ($dialogMsg) {
$dialogMsg->send = 1;
$dialogMsg->key = $dialogMsg->generateMsgKey();
$dialogMsg->save();
WebSocketDialogUser::whereDialogId($dialog->id)->change(['updated_at' => Carbon::now()->toDateTimeString('millisecond')]);
//
WebSocketDialogUser::whereDialogId($dialogMsg->dialog_id)->change([
'hide' => 0, // 有新消息时,显示会话(会话内所有会员)
'last_at' => Carbon::now(),
'updated_at' => Carbon::now()->toDateTimeString('millisecond'),
]);
});
//
$task = new WebSocketDialogMsgTask($dialogMsg->id);

View File

@@ -47,6 +47,20 @@ class WebSocketDialogMsgRead extends AbstractModel
return $this->hasOne(WebSocketDialogMsg::class, 'id', 'msg_id');
}
/**
* 强制标记成阅读
* @param $dialogId
* @param $userId
* @return void
*/
public static function forceRead($dialogId, $userId)
{
self::whereDialogId($dialogId)
->whereUserid($userId)
->whereNull('read_at')
->update(['read_at' => Carbon::now()]);
}
/**
* 仅标记成阅读
* @param $list

View File

@@ -11,10 +11,12 @@ use Carbon\Carbon;
* @property int|null $dialog_id 对话ID
* @property int|null $userid 会员ID
* @property string|null $top_at 置顶时间
* @property string|null $last_at 最后消息时间
* @property int|null $mark_unread 是否标记为未读0否1是
* @property int|null $silence 是否免打扰0否1是
* @property int|null $hide 不显示会话0否1是
* @property int|null $inviter 邀请人
* @property int|null $important 是否不可移出(项目、任务、部门人员)
* @property int|null $important 是否不可移出(项目、任务人员)
* @property string|null $color 颜色
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
@@ -25,9 +27,11 @@ use Carbon\Carbon;
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereColor($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereHide($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereImportant($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereInviter($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereLastAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereMarkUnread($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereSilence($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereTopAt($value)
@@ -46,4 +50,17 @@ class WebSocketDialogUser extends AbstractModel
{
return $this->hasOne(WebSocketDialog::class, 'id', 'dialog_id');
}
/**
* 更新对话最后消息时间
* @return WebSocketDialogMsg|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Query\Builder|object|null
*/
public static function updateMsgLastAt($dialogId)
{
$lastMsg = WebSocketDialogMsg::whereDialogId($dialogId)->orderByDesc('id')->first();
if ($lastMsg) {
WebSocketDialogUser::whereDialogId($dialogId)->change(['last_at' => $lastMsg->created_at]);
}
return $lastMsg;
}
}

View File

@@ -133,7 +133,8 @@ class Base
/**
* 判断客户端版本
* @param $min
* @param $min // 最小版本满足此版本返回true
* @param null $clientVersion
* @return bool
*/
public static function judgeClientVersion($min, $clientVersion = null)
@@ -687,6 +688,22 @@ class Base
return $string;
}
/**
* 递归处理数组
*
* @param string $callback 如:'intval'、'trim'、'addslashes'、'stripslashes'、'htmlspecialchars'
* @param array $array
* @return array
*/
public static function newArrayRecursive($callback, $array)
{
$func = function ($item) use (&$func, &$callback) {
return is_array($item) ? array_map($func, $item) : call_user_func($callback, $item);
};
return array_map($func, $array);
}
/**
* 重MD5加密
* @param $text
@@ -2101,7 +2118,7 @@ class Base
}
$scaleName = "";
if ($param['fileName']) {
$fileName = $param['fileName'];
$fileName = basename($param['fileName']);
} else {
if ($param['scale'] && is_array($param['scale'])) {
list($width, $height) = $param['scale'];
@@ -2260,7 +2277,7 @@ class Base
if ($param['fileName'] === true) {
$fileName = $file->getClientOriginalName();
} elseif ($param['fileName']) {
$fileName = $param['fileName'];
$fileName = basename($param['fileName']);
} else {
if ($param['scale'] && is_array($param['scale'])) {
list($width, $height) = $param['scale'];
@@ -2456,13 +2473,18 @@ class Base
*/
public static function extIcon($ext)
{
return match ($ext) {
"docx" => 'images/ext/doc.png',
"xlsx" => 'images/ext/xls.png',
"pptx" => 'images/ext/ppt.png',
"ai", "avi", "bmp", "cdr", "doc", "eps", "gif", "mov", "mp3", "mp4", "pdf", "ppt", "pr", "psd", "rar", "svg", "tif", "txt", "xls", "zip" => 'images/ext/' . $ext . '.png',
default => 'images/ext/file.png',
};
if ($ext == "docx") {
$ext = 'doc';
} elseif ($ext == "xlsx") {
$ext = 'xls';
} elseif ($ext == "pptx") {
$ext = 'ppt';
}
if (in_array($ext, ["ai", "avi", "bmp", "cdr", "doc", "eps", "gif", "mov", "mp3", "mp4", "pdf", "ppt", "pr", "psd", "rar", "svg", "tif", "txt", "xls", "zip"])) {
return 'images/ext/' . $ext . '.png';
} else {
return 'images/ext/file.png';
}
}
/**

View File

@@ -174,19 +174,16 @@ class Extranet
/**
* 随机笑话接口
* @return array
* @return string
*/
public static function randJoke(): array
public static function randJoke(): string
{
$jokeKey = env("JUKE_KEY_JOKE");
if ($jokeKey) {
$data = self::curl("http://v.juhe.cn/joke/randJoke.php?key=" . $jokeKey);
$data = Base::json2array($data);
if ($data['reason'] === 'success') {
return $data['result'];
}
$data = self::curl("https://hmajax.itheima.net/api/randjoke");
$data = Base::json2array($data);
if ($data['message'] === '获取成功' && $text = trim($data['data'])) {
return $text;
}
return [];
return "";
}
/**
@@ -195,13 +192,10 @@ class Extranet
*/
public static function soups(): string
{
$soupKey = env("JUKE_KEY_SOUP");
if ($soupKey) {
$data = self::curl("https://apis.juhe.cn/fapig/soup/query?key=" . $soupKey);
$data = Base::json2array($data);
if ($data['reason'] === 'success' && $text = trim($data['result']['text'])) {
return $text;
}
$data = self::curl("https://hmajax.itheima.net/api/ambition");
$data = Base::json2array($data);
if ($data['message'] === '获取成功' && $text = trim($data['data'])) {
return $text;
}
return "";
}

View File

@@ -15,7 +15,7 @@ class TimeRange
public function __construct($data)
{
if (is_array($data)) {
$range = explode("-", str_replace([",", "|"], "-", $data['timerange']));
$range = $this->format($data['timerange']);
if ($data['updated_at'] || $data['at_after']) {
$range[0] = $data['updated_at'] ?: $data['at_after'];
}
@@ -23,14 +23,27 @@ class TimeRange
$range[1] = $data['deleted_at'];
}
} else {
$range = explode("-", str_replace([",", "|"], "-", $data));
$range = $this->format($data);
}
//
$updated = Base::isNumber($range[0]) ? intval($range[0]) : trim($range[0]);
$deleted = Base::isNumber($range[1]) ? intval($range[1]) : trim($range[1]);
//
$this->updated = $updated ? Carbon::parse($updated) : null;
$this->deleted = $deleted ? Carbon::parse($deleted) : null;
$timezone = config('app.timezone');
$this->updated = $updated ? Carbon::parse($updated)->setTimezone($timezone) : null;
$this->deleted = $deleted ? Carbon::parse($deleted)->setTimezone($timezone) : null;
}
/**
* @param $timerange
* - 格式12021-01-01 00:00:00,2021-01-01 23:59:59
* - 格式21612051200-1612137599
* @return array
*/
private function format($timerange)
{
$search = str_contains($timerange, ":") ? ["|"] : ["|", "-"];
return explode(",", str_replace($search, ",", $timerange));
}
/**

View File

@@ -4,6 +4,7 @@ namespace App\Observers;
use App\Models\Deleted;
use App\Models\WebSocketDialogUser;
use Carbon\Carbon;
class WebSocketDialogUserObserver
{
@@ -15,6 +16,18 @@ class WebSocketDialogUserObserver
*/
public function created(WebSocketDialogUser $webSocketDialogUser)
{
if (!$webSocketDialogUser->last_at) {
if (in_array($webSocketDialogUser->webSocketDialog?->group_type, ['user', 'department', 'all'])) {
$webSocketDialogUser->last_at = Carbon::now();
$webSocketDialogUser->save();
} else {
$item = WebSocketDialogUser::whereDialogId($webSocketDialogUser->dialog_id)->orderByDesc('last_at')->first();
if ($item?->last_at) {
$webSocketDialogUser->last_at = $item->last_at;
$webSocketDialogUser->save();
}
}
}
Deleted::forget('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
}

View File

@@ -172,8 +172,11 @@ class WebSocketService implements WebSocketHandlerInterface
*/
public function onClose(Server $server, $fd, $reactorId)
{
Task::deliver(new LineTask($this->getUserid($fd), false)); // 通知离线
$this->deleteUser($fd);
$userid = $this->getUserid($fd);
if($userid){
Task::deliver(new LineTask($userid, false)); // 通知离线
$this->deleteUser($fd);
}
}
/** ****************************************************************************** */

View File

@@ -71,6 +71,9 @@ class BotReceiveMsgTask extends AbstractTask
}
if (preg_match("/<span[^>]*?data-quick-key=([\"'])(.*?)\\1[^>]*?>(.*?)<\/span>/is", $original, $match)) {
$command = $match[2];
if (str_starts_with($command, '%3A.')) {
$command = ":" . substr($command, 4);
}
} else {
$command = trim(strip_tags($original));
}
@@ -313,12 +316,12 @@ class BotReceiveMsgTask extends AbstractTask
$nameKey = $isManager ? $array[2] : $array[1];
$data = $this->botManagerOne($botId, $msg->userid);
if ($data) {
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread', 'u.silence', 'u.color', 'u.updated_at as user_at'])
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->where('web_socket_dialogs.name', 'LIKE', "%{$nameKey}%")
->where('u.userid', $data->userid)
->orderByDesc('u.top_at')
->orderByDesc('web_socket_dialogs.last_at')
->orderByDesc('u.last_at')
->take(20)
->get();
if ($list->isEmpty()) {
@@ -377,6 +380,7 @@ class BotReceiveMsgTask extends AbstractTask
'openai_agency' => $setting['openai_agency'],
'openai_model' => $setting['openai_model'],
'server_url' => $serverUrl,
'chunk_size' => 7,
];
if (empty($extras['openai_key'])) {
$error = 'Robot disabled.';
@@ -434,6 +438,24 @@ class BotReceiveMsgTask extends AbstractTask
$error = 'The client version is low (required version ≥ v0.29.12).';
}
break;
// Gemini 机器人
case 'ai-gemini@bot.system':
$setting = Base::setting('aibotSetting');
$webhookUrl = "{$serverUrl}/ai/gemini/send";
$extras = [
'gemini_key' => $setting['gemini_key'],
'gemini_model' => $setting['gemini_model'],
'gemini_agency' => $setting['gemini_agency'],
'gemini_timeout' => 20,
'server_url' => $serverUrl,
];
if (empty($extras['gemini_key'])) {
$error = 'Robot disabled.';
} elseif (in_array($this->client['platform'], ['win', 'mac', 'web'])
&& !Base::judgeClientVersion("0.29.11", $this->client['version'])) {
$error = 'The client version is low (required version ≥ v0.29.12).';
}
break;
// 其他机器人
default:
$userBot = UserBot::whereBotId($botUser->userid)->first();
@@ -449,7 +471,7 @@ class BotReceiveMsgTask extends AbstractTask
}
//
try {
$res = Ihttp::ihttp_post($webhookUrl, [
$data = [
'text' => $command,
'token' => User::generateToken($botUser),
'dialog_id' => $dialog->id,
@@ -460,7 +482,8 @@ class BotReceiveMsgTask extends AbstractTask
'bot_uid' => $botUser->userid,
'version' => Base::getVersion(),
'extras' => Base::array2json($extras)
], 10);
];
$res = Ihttp::ihttp_post($webhookUrl, $data);
if ($userBot) {
$userBot->webhook_num++;
$userBot->save();

View File

@@ -7,6 +7,7 @@ use App\Models\TaskWorker;
use App\Models\Tmp;
use App\Models\WebSocketTmpMsg;
use Carbon\Carbon;
use Illuminate\Support\Facades\File as SupportFile;
/**
* 删除过期临时数据任务
@@ -102,10 +103,12 @@ class DeleteTmpTask extends AbstractTask
*/
case 'file_pack':
{
$path = storage_path('app/temp/download/');
$path = public_path('tmp/file/');
if (!SupportFile::exists($path)) {
return;
}
$dirIterator = new \RecursiveDirectoryIterator($path);
$iterator = new \RecursiveIteratorIterator($dirIterator);
foreach ($iterator as $file) {
if ($file->isFile()) {
$time = $file->getMTime();

View File

@@ -13,13 +13,15 @@ use Carbon\Carbon;
/**
* 获取笑话、心灵鸡汤
*
* 在.env添加笑话 JUKE_KEY_JOKE
* 在.env添加鸡汤 JUKE_KEY_SOUP
*
* 每日小时采集1次
* 每分钟采集1次
*/
class JokeSoupTask extends AbstractTask
{
public static function keyName($key)
{
return "JokeSoupTask-v2:{$key}";
}
public function __construct()
{
parent::__construct();
@@ -27,27 +29,25 @@ class JokeSoupTask extends AbstractTask
public function start()
{
// 判断每小时执行一次
if (Cache::get("JokeSoupTask:YmdH") == date("YmdH")) {
// 判断每分钟执行一次
if (Cache::get(self::keyName("YmdHi")) == date("YmdHi")) {
return;
}
Cache::put("JokeSoupTask:YmdH", date("YmdH"), Carbon::now()->addDay());
Cache::put(self::keyName("YmdHi"), date("YmdHi"), Carbon::now()->addDay());
//
$array = Base::json2array(Cache::get("JokeSoupTask:jokes"));
$array = Base::json2array(Cache::get(self::keyName("jokes")));
$data = Extranet::randJoke();
foreach ($data as $item) {
if ($text = trim($item['content'])) {
$array[] = $text;
}
if ($data) {
$array[] = $data;
}
Cache::forever("JokeSoupTask:jokes", Base::array2json(array_slice($array, -100)));
Cache::forever(self::keyName("jokes"), Base::array2json(array_slice($array, -200)));
//
$array = Base::json2array(Cache::get("JokeSoupTask:soups"));
$array = Base::json2array(Cache::get(self::keyName("soups")));
$data = Extranet::soups();
if ($data) {
$array[] = $data;
}
Cache::forever("JokeSoupTask:soups", Base::array2json(array_slice($array, -24)));
Cache::forever(self::keyName("soups"), Base::array2json(array_slice($array, -200)));
}
public function end()

View File

@@ -31,34 +31,34 @@ class UnclaimedTaskRemindTask extends AbstractTask
return;
}
//
$times = explode(':',date('H:i'));
$reminderTimes = explode(':',$setting['unclaimed_task_reminder_time']);
if( !isset($times[1]) || !isset($reminderTimes[1]) || $times[0] != $reminderTimes[0]){
$times = explode(':', date('H:i'));
$reminderTimes = explode(':', $setting['unclaimed_task_reminder_time']);
if (!isset($times[1]) || !isset($reminderTimes[1]) || $times[0] != $reminderTimes[0]) {
return;
}
// 执行一次
if (Cache::get("UnclaimedTaskRemindTask:His",0)) {
if (Cache::get("UnclaimedTaskRemindTask:His", 0)) {
return;
}
if( $times[1] >= $reminderTimes[1] - 1 && $times[1] <= $reminderTimes[1] + 1){
if ($times[1] >= intval($reminderTimes[1]) - 1 && $times[1] <= intval($reminderTimes[1]) + 1) {
//
Cache::put("UnclaimedTaskRemindTask:His", date('H:i:s'), Carbon::now()->addMinutes(5));
//
Project::whereNull('deleted_at')->whereNull('archived_at')->chunk(100,function($projects) {
Project::whereNull('deleted_at')->whereNull('archived_at')->chunk(100, function ($projects) {
foreach ($projects as $project) {
//
$projectTasks = ProjectTask::select('project_tasks.id','project_tasks.name')
$projectTasks = ProjectTask::select('project_tasks.id', 'project_tasks.name')
->leftJoin('project_task_users', function ($query) {
$query->on('project_tasks.id', '=', 'project_task_users.task_id');
})
->where('project_tasks.project_id',$project->id)
->where('project_tasks.project_id', $project->id)
->whereNull('project_tasks.deleted_at')
->whereNull('project_tasks.archived_at')
->whereNull('project_task_users.id')
->limit(10)
->get();
//
if( !$projectTasks->isEmpty() ){
if (!$projectTasks->isEmpty()) {
$botUser = User::botGetOrCreate('task-alert');
if (empty($botUser)) {
return;
@@ -69,10 +69,10 @@ class UnclaimedTaskRemindTask extends AbstractTask
}
//
$taskHtml = '<span style="line-height: 26px;">任务待领取</span> <br/>';
foreach($projectTasks as $projectTask){
foreach ($projectTasks as $projectTask) {
$taskHtml .= "<span class=\"mention task\" style=\"line-height: 26px;\" data-id=\"{$projectTask->id}\">#{$projectTask->name}</span> <br/>";
}
WebSocketDialogMsg::sendMsg(null, $project->dialog_id , 'text', [
WebSocketDialogMsg::sendMsg(null, $project->dialog_id, 'text', [
'text' => $taskHtml
], $botUser->userid);
}
@@ -83,7 +83,5 @@ class UnclaimedTaskRemindTask extends AbstractTask
public function end()
{
}
}

View File

@@ -38,12 +38,17 @@ class WebSocketDialogMsgTask extends AbstractTask
{
parent::__construct(...func_get_args());
$this->id = $id;
$this->ignoreFd = $ignoreFd === null ? Request::header('fd') : $ignoreFd;
$this->client = [
'version' => Base::headerOrInput('version'),
'language' => Base::headerOrInput('language'),
'platform' => Base::headerOrInput('platform'),
];
// 判断是否有Request方法兼容go协程请求
$this->ignoreFd = $ignoreFd;
$this->client = [];
if (method_exists(request(), "header")) {
$this->ignoreFd = $ignoreFd === null ? Request::header('fd') : $ignoreFd;
$this->client = [
'version' => Base::headerOrInput('version'),
'language' => Base::headerOrInput('language'),
'platform' => Base::headerOrInput('platform'),
];
}
}
/**

169
bin/https Normal file
View File

@@ -0,0 +1,169 @@
#!/bin/sh
#fonts color
Green="\033[32m"
Yellow="\033[33m"
Red="\033[31m"
GreenBG="\033[42;37m"
YellowBG="\033[43;37m"
RedBG="\033[41;37m"
Font="\033[0m"
#notification information
OK="${Green}[OK]${Font}"
Warn="${Yellow}[警告]${Font}"
Error="${Red}[错误]${Font}"
cd "$(
cd "$(dirname "$0")" || exit
pwd
)" || exit
#================================================================
#================================================================
success() {
echo -e "${OK} ${GreenBG}$1${Font}"
}
warning() {
echo -e "${Warn} ${YellowBG}$1${Font}"
}
error() {
echo -e "${Error} ${RedBG}$1${Font}"
}
info() {
echo -e "$1"
}
env_get() {
local key=$1
local value=`cat $(dirname "$PWD")/.env | grep "^$key=" | awk -F '=' '{print $2}'`
echo "$value"
}
env_set() {
local key=$1
local val=$2
local exist=`cat $(dirname "$PWD")/.env | grep "^$key="`
if [ -z "$exist" ]; then
echo "$key=$val" >> $(dirname "$PWD")/.env
else
sed -i "/^${key}=/c\\${key}=${val}" $(dirname "$PWD")/.env
fi
}
#================================================================
#================================================================
check() {
while [ -z "$domain" ]; do
read -rp "请输入你的域名: " domain
# 判断域名是否合法
if [ -z "$domain" ]; then
error "域名不能为空"
elif [ -z "$(echo "$domain" | grep -E '^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$')" ]; then
error "域名格式不正确"
domain=""
fi
done
success "正在域名DNS解析IP..."
local domain_ip=$(ping -c 1 "${domain}" 2>/dev/null | grep PING | cut -d "(" -f2 | cut -d ")" -f1)
local local_ip=$(curl -sk ip.sb)
info "域名DNS解析IP: ${domain_ip}"
info "本机IP: ${local_ip}"
sleep 2
if [[ "$(echo "${local_ip}" | tr '.' '+' | bc)" == "$(echo "${domain_ip}" | tr '.' '+' | bc)" ]]; then
success "域名DNS解析IP 与 本机IP 匹配"
sleep 2
else
warning "域名DNS解析IP 与 本机IP 不匹配,是否继续操作? [Y/n]"
read -r continue_next
[[ -z ${continue_next} ]] && continue_next="Y"
case $continue_next in
[yY][eE][sS] | [yY])
success "继续操作"
sleep 2
;;
*)
error "操作终止"
exit 2
;;
esac
fi
}
install() {
local sitePath="$(dirname "$PWD")/docker/nginx/site"
local sslPath="$sitePath/ssl"
if [[ -f "$sslPath/$domain.key" && -f "$sslPath/$domain.crt" ]]; then
warning "$domain 证书文件已存在,是否删除并继续操作? [Y/n]"
read -r continue_install
[[ -z ${continue_install} ]] && continue_install="Y"
case $continue_install in
[yY][eE][sS] | [yY])
rm -f "$sslPath/$domain.key"
rm -f "$sslPath/$domain.crt"
success "继续操作"
sleep 2
;;
*)
error "操作终止"
exit 2
;;
esac
fi
apk add --no-cache openssl socat
curl https://get.acme.sh | sh
if [[ 0 -ne $? ]]; then
error "安装证书生成脚本失败"
exit 1
fi
if /root/.acme.sh/acme.sh --issue -d "${domain}" -w "$(dirname "$PWD")/public" --standalone -k ec-256 --force --test; then
success "SSL 证书测试签发成功,开始正式签发"
rm -rf "/root/.acme.sh/${domain}_ecc"
sleep 2
else
error "SSL 证书测试签发失败"
rm -rf "root/.acme.sh/${domain}_ecc"
exit 1
fi
if /root/.acme.sh/acme.sh --issue -d "${domain}" -w "$(dirname "$PWD")/public" --server letsencrypt --standalone -k ec-256 --force; then
success "SSL 证书生成成功"
sleep 2
mkdir -p $sslPath
if /root/.acme.sh/acme.sh --installcert -d "${domain}" --fullchainpath "${sslPath}/${domain}.crt" --keypath "${sslPath}/${domain}.key" --ecc --force; then
success "SSL 证书配置成功"
sleep 2
fi
else
error "SSL 证书生成失败"
rm -rf "/root/.acme.sh/${domain}_ecc"
exit 1
fi
env_set "APP_URL" "https://${domain}"
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_protocols TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
error_page 497 https://\$host\$request_uri;
EOF
}
check
install

View File

@@ -17,12 +17,33 @@ function runExec(command, cb) {
});
}
function removeDuplicateLines(log) {
const logs = log.split(/(\n## \[.*?\])/)
if (logs) {
log = logs.map(str => {
const array = [];
const items = str.split("\n");
items.some(item => {
if (/^-/.test(item)) {
if (array.indexOf(item) === -1) {
array.push(item);
}
} else {
array.push(item);
}
})
return array.join("\n");
}).join('');
}
return log;
}
runExec("git rev-list --count HEAD $(git branch | sed -n -e 's/^\* \(.*\)/\1/p')", function (err, response) {
if (err) {
console.error(err);
return;
}
const num = 1238 + parseInt(response)
const num = 1250 + parseInt(response)
if (isNaN(num) || Math.floor(num % 100) < 0) {
console.error("get version error " + response);
return;
@@ -43,7 +64,7 @@ runExec("git rev-list --count HEAD $(git branch | sed -n -e 's/^\* \(.*\)/\1/p')
console.error("Change file does not exist");
return "";
}
let newContent = fs.readFileSync(changeFile, 'utf8');
let newContent = removeDuplicateLines(fs.readFileSync(changeFile, 'utf8'));
if (newContent.indexOf("## [Unreleased]") !== -1) {
newContent = newContent.replace("## [Unreleased]", `## [${ver}]`);
} else {

152
cmd
View File

@@ -2,13 +2,16 @@
#fonts color
Green="\033[32m"
Yellow="\033[33m"
Red="\033[31m"
GreenBG="\033[42;37m"
YellowBG="\033[43;37m"
RedBG="\033[41;37m"
Font="\033[0m"
#notification information
OK="${Green}[OK]${Font}"
Warn="${Yellow}[警告]${Font}"
Error="${Red}[错误]${Font}"
cur_path="$(pwd)"
@@ -17,14 +20,30 @@ COMPOSE="docker-compose"
judge() {
if [[ 0 -eq $? ]]; then
echo -e "${OK} ${GreenBG} $1 完成 ${Font}"
success "$1 完成"
sleep 1
else
echo -e "${Error} ${RedBG} $1 失败${Font}"
error "$1 失败"
exit 1
fi
}
success() {
echo -e "${OK} ${GreenBG}$1${Font}"
}
warning() {
echo -e "${Warn} ${YellowBG}$1${Font}"
}
error() {
echo -e "${Error} ${RedBG}$1${Font}"
}
info() {
echo -e "$1"
}
rand() {
local min=$1
local max=$(($2-$min+1))
@@ -51,7 +70,7 @@ restart_php() {
$COMPOSE stop php
$COMPOSE start php
else
echo -e "$RES"
info "$RES"
fi
}
@@ -69,21 +88,21 @@ switch_debug() {
check_docker() {
docker --version &> /dev/null
if [ $? -ne 0 ]; then
echo -e "${Error} ${RedBG} 未安装 Docker${Font}"
error "未安装 Docker"
exit 1
fi
docker-compose version &> /dev/null
if [ $? -ne 0 ]; then
docker compose version &> /dev/null
if [ $? -ne 0 ]; then
echo -e "${Error} ${RedBG} 未安装 Docker-compose${Font}"
error "未安装 Docker-compose"
exit 1
fi
COMPOSE="docker compose"
fi
if [[ -n `$COMPOSE version | grep -E "\sv*1"` ]]; then
if [[ -n `$COMPOSE version | grep -E "\sv1"` ]]; then
$COMPOSE version
echo -e "${Error} ${RedBG} Docker-compose 版本过低请升级至v2+${Font}"
error "Docker-compose 版本过低请升级至v2+"
exit 1
fi
}
@@ -91,7 +110,17 @@ check_docker() {
check_node() {
npm --version &> /dev/null
if [ $? -ne 0 ]; then
echo -e "${Error} ${RedBG} 未安装nodejs${Font}"
error "未安装 npm"
exit 1
fi
node --version &> /dev/null
if [ $? -ne 0 ]; then
error "未安装 Node.js"
exit 1
fi
if [[ -n `node --version | grep -E "v1"` ]]; then
node --version
error "Node.js 版本过低请升级至v20+"
exit 1
fi
}
@@ -154,7 +183,7 @@ run_exec() {
local cmd=$2
local name=`docker_name $container`
if [ -z "$name" ]; then
echo -e "${Error} ${RedBG} 没有找到 $container 容器! ${Font}"
error "没有找到 $container 容器!"
exit 1
fi
docker exec -it "$name" /bin/sh -c "$cmd"
@@ -170,7 +199,7 @@ run_mysql() {
filename="${cur_path}/docker/mysql/backup/${database}_$(date "+%Y%m%d%H%M%S").sql.gz"
run_exec mariadb "exec mysqldump --databases $database -u$username -p$password" | gzip > $filename
judge "备份数据库"
[ -f "$filename" ] && echo -e "备份文件:$filename"
[ -f "$filename" ] && info "备份文件:$filename"
elif [ "$1" = "recovery" ]; then
database=$(env_get DB_DATABASE)
username=$(env_get DB_USERNAME)
@@ -179,19 +208,19 @@ run_mysql() {
mkdir -p ${cur_path}/docker/mysql/backup
list=`ls -1 "${cur_path}/docker/mysql/backup" | grep ".sql.gz"`
if [ -z "$list" ]; then
echo -e "${Error} ${RedBG} 没有备份文件!${Font}"
error "没有备份文件!"
exit 1
fi
echo "$list"
read -rp "请输入备份文件名称还原:" inputname
filename="${cur_path}/docker/mysql/backup/${inputname}"
if [ ! -f "$filename" ]; then
echo -e "${Error} ${RedBG} 备份文件:${inputname} 不存在! ${Font}"
error "备份文件:${inputname} 不存在!"
exit 1
fi
container_name=`docker_name mariadb`
if [ -z "$container_name" ]; then
echo -e "${Error} ${RedBG} 没有找到 mariadb 容器! ${Font}"
error "没有找到 mariadb 容器!"
exit 1
fi
docker cp $filename $container_name:/
@@ -201,6 +230,49 @@ run_mysql() {
fi
}
https_auto() {
restart_nginx="n"
if [[ "$(env_get APP_PORT)" != "80" ]]; then
warning "HTTP服务端口不是80是否修改并继续操作 [Y/n]"
read -r continue_http
[[ -z ${continue_http} ]] && continue_http="Y"
case $continue_http in
[yY][eE][sS] | [yY])
success "继续操作"
env_set "APP_PORT" "80"
restart_nginx="y"
;;
*)
error "操作终止"
exit 1
;;
esac
fi
if [[ "$(env_get APP_SSL_PORT)" != "443" ]]; then
warning "HTTPS服务端口不是443是否修改并继续操作 [Y/n]"
read -r continue_https
[[ -z ${continue_https} ]] && continue_https="Y"
case $continue_https in
[yY][eE][sS] | [yY])
success "继续操作"
env_set "APP_SSL_PORT" "443"
restart_nginx="y"
;;
*)
error "操作终止"
exit 1
;;
esac
fi
if [[ "$restart_nginx" == "y" ]]; then
$COMPOSE up -d
fi
docker run -it --rm -v $(pwd):/work nginx:alpine sh "/work/bin/https"
if [[ 0 -eq $? ]]; then
run_exec nginx "nginx -s reload"
fi
}
env_get() {
local key=$1
local value=`cat ${cur_path}/.env | grep "^$key=" | awk -F '=' '{print $2}'`
@@ -220,7 +292,7 @@ env_set() {
docker run -it --rm -v ${cur_path}:/www alpine sh -c "sed -i "/^${key}=/c\\${key}=${val}" /www/.env"
fi
if [ $? -ne 0 ]; then
echo -e "${Error} ${RedBG} 设置env参数失败${Font}"
error "设置env参数失败"
exit 1
fi
fi
@@ -293,7 +365,7 @@ if [ $# -gt 0 ]; then
run_exec php "composer config --unset repos.packagist"
fi
if [ ! -f "${cur_path}/vendor/autoload.php" ]; then
echo -e "${Error} ${RedBG}composer install 失败,请重试! ${Font}"
error "composer install 失败,请重试!"
exit 1
fi
[[ -z "$(env_get APP_KEY)" ]] && run_exec php "php artisan key:generate"
@@ -303,7 +375,7 @@ if [ $# -gt 0 ]; then
while [ ! -f "${cur_path}/docker/mysql/data/$(env_get DB_DATABASE)/db.opt" ]; do
((remaining=$remaining-1))
if [ $remaining -lt 0 ]; then
echo -e "${Error} ${RedBG} 数据库初始化失败! ${Font}"
error "数据库初始化失败!"
exit 1
fi
chmod -R 775 "${cur_path}/docker/mysql/data"
@@ -311,19 +383,21 @@ if [ $# -gt 0 ]; then
done
run_exec php "php artisan migrate --seed"
if [ ! -f "${cur_path}/docker/mysql/data/$(env_get DB_DATABASE)/$(env_get DB_PREFIX)migrations.ibd" ]; then
echo -e "${Error} ${RedBG} 数据库安装失败! ${Font}"
error "数据库安装失败!"
exit 1
fi
# 设置初始化密码
res=`run_exec mariadb "sh /etc/mysql/repassword.sh"`
$COMPOSE up -d
restart_php
echo -e "${OK} ${GreenBG} 安装完成 ${Font}"
echo -e "地址: http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
echo -e "$res"
success "安装完成"
info "地址: http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
info "$res"
elif [[ "$1" == "update" ]]; then
shift 1
run_mysql backup
if [[ "$@" != "nobackup" ]]; then
run_mysql backup
fi
if [[ -z "$(arg_get local)" ]]; then
git fetch --all
git reset --hard origin/$(git branch | sed -n -e 's/^\* \(.*\)/\1/p')
@@ -340,10 +414,10 @@ if [ $# -gt 0 ]; then
[[ -z ${uninstall} ]] && uninstall="Y"
case $uninstall in
[yY][eE][sS] | [yY])
echo -e "${RedBG} 开始卸载... ${Font}"
info "${RedBG}开始卸载...${Font}"
;;
*)
echo -e "${GreenBG} 终止卸载。 ${Font}"
info "${GreenBG}终止卸载。${Font}"
exit 2
;;
esac
@@ -352,7 +426,7 @@ if [ $# -gt 0 ]; then
rm -rf "./docker/mysql/data"
rm -rf "./docker/log/supervisor"
find "./storage/logs" -name "*.log" | xargs rm -rf
echo -e "${OK} ${GreenBG} 卸载完成 ${Font}"
success "卸载完成"
elif [[ "$1" == "reinstall" ]]; then
shift 1
./cmd uninstall $@
@@ -362,20 +436,20 @@ if [ $# -gt 0 ]; then
shift 1
env_set APP_PORT "$1"
$COMPOSE up -d
echo -e "${OK} ${GreenBG} 修改成功 ${Font}"
echo -e "地址: http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
success "修改成功"
info "地址: http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
elif [[ "$1" == "url" ]]; then
shift 1
env_set APP_URL "$1"
restart_php
echo -e "${OK} ${GreenBG} 修改成功 ${Font}"
success "修改成功"
elif [[ "$1" == "env" ]]; then
shift 1
if [ -n "$1" ]; then
env_set $1 "$2"
fi
restart_php
echo -e "${OK} ${GreenBG} 修改成功 ${Font}"
success "修改成功"
elif [[ "$1" == "repassword" ]]; then
shift 1
run_exec mariadb "sh /etc/mysql/repassword.sh \"$@\""
@@ -391,6 +465,16 @@ if [ $# -gt 0 ]; then
elif [[ "$1" == "electron" ]]; then
shift 1
run_electron $@
elif [[ "$1" == "eeui" ]]; then
shift 1
cli="$@"
por=""
if [[ "$cli" == "build" ]]; then
cli="build --simple"
elif [[ "$cli" == "dev" ]]; then
por="-p 8880:8880"
fi
docker run -it --rm -v ${cur_path}/resources/mobile:/work -w /work ${por} kuaifan/eeui-cli:0.0.1 eeui ${cli}
elif [[ "$1" == "doc" ]]; then
shift 1
run_exec php "php app/Http/Controllers/Api/apidoc.php"
@@ -398,13 +482,15 @@ if [ $# -gt 0 ]; then
elif [[ "$1" == "debug" ]]; then
shift 1
switch_debug "$@"
echo "success"
info "success"
elif [[ "$1" == "https" ]]; then
shift 1
if [[ "$@" == "auto" ]]; then
if [[ "$1" == "agent" ]] || [[ "$1" == "true" ]]; then
env_set APP_SCHEME "true"
elif [[ "$1" == "close" ]] || [[ "$1" == "auto" ]]; then
env_set APP_SCHEME "auto"
else
env_set APP_SCHEME "true"
https_auto
fi
restart_php
elif [[ "$1" == "artisan" ]]; then
@@ -421,9 +507,9 @@ if [ $# -gt 0 ]; then
e="redis $@" && run_exec redis "$e"
elif [[ "$1" == "mysql" ]]; then
shift 1
if [ "$1" = "backup" ]; then
if [[ "$1" == "backup" ]] || [[ "$1" == "b" ]]; then
run_mysql backup
elif [ "$1" = "recovery" ]; then
elif [[ "$1" == "recovery" ]] || [[ "$1" == "r" ]]; then
run_mysql recovery
else
e="mysql $@" && run_exec mariadb "$e"

View File

@@ -16,6 +16,7 @@
"ext-libxml": "*",
"ext-openssl": "*",
"ext-simplexml": "*",
"ext-zip": "*",
"directorytree/ldaprecord-laravel": "^2.7",
"fideloper/proxy": "^4.4.1",
"firebase/php-jwt": "^6.9",

950
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -306,7 +306,7 @@ return [
'reload_async' => true,
'max_wait_time' => 60,
'enable_reuse_port' => true,
'enable_coroutine' => false,
'enable_coroutine' => true,
'upload_tmp_dir' => @is_writable('/dev/shm/') ? '/dev/shm' : '/tmp',
'http_compression' => false,
],

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateProjectPermissionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('project_permissions'))
return;
Schema::create('project_permissions', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('project_id')->nullable()->default(0)->comment('项目ID');
$table->text('permissions')->nullable()->comment('权限');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('project_permissions');
}
}

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class WebSocketDialogMsgsAddForwardId extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
if (!Schema::hasColumn('web_socket_dialog_msgs', 'forward_id')) {
$table->bigInteger('forward_id')->nullable()->default(0)->after('reply_id')->comment('转发ID');
$table->bigInteger('forward_num')->nullable()->default(0)->after('forward_id')->comment('被转发多少次');
$table->boolean('forward_show')->nullable()->default(1)->after('forward_num')->comment('是否显示转发的来源');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
$table->dropColumn("forward_id");
$table->dropColumn("forward_num");
$table->dropColumn("forward_show");
});
}
}

View File

@@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddIndexSome20231217 extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('project_task_users', function (Blueprint $table) {
$table->index('task_id');
$table->index('task_pid');
});
Schema::table('project_tasks', function (Blueprint $table) {
$table->index('visibility');
});
}
/**
* Reverse the migrations.
*
* @return voidw
*/
public function down()
{
Schema::table('project_task_users', function (Blueprint $table) {
$table->dropIndex(['task_id']);
$table->dropIndex(['task_pid']);
});
Schema::table('project_tasks', function (Blueprint $table) {
$table->dropIndex(['visibility']);
});
}
}

View File

@@ -0,0 +1,193 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class UpdateOwnerAddIndexSome20231217 extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// 项目相关
Schema::table('projects', function (Blueprint $table) {
$table->index('userid');
$table->index('dialog_id');
});
Schema::table('project_users', function (Blueprint $table) {
$table->index('userid');
$table->index('project_id');
$table->index(['project_id','userid']);
$table->index('owner');
$table->integer('owner')->change();
});
Schema::table('project_tasks', function (Blueprint $table) {
$table->index('parent_id');
$table->index('dialog_id');
$table->index('userid');
$table->integer('visibility')->change();
});
Schema::table('project_task_users', function (Blueprint $table) {
$table->index(['task_id','userid']);
$table->index('owner');
$table->integer('owner')->change();
});
Schema::table('project_task_files', function (Blueprint $table) {
$table->index('project_id');
$table->index('task_id');
});
Schema::table('project_task_tags', function (Blueprint $table) {
$table->index('project_id');
$table->index('task_id');
});
Schema::table('project_task_contents', function (Blueprint $table) {
$table->index('project_id');
$table->index('task_id');
});
Schema::table('project_task_push_logs', function (Blueprint $table) {
$table->index('userid');
$table->index('task_id');
});
Schema::table('project_task_flow_changes', function (Blueprint $table) {
$table->index('userid');
$table->index('task_id');
});
// 聊天相关
Schema::table('web_socket_dialogs', function (Blueprint $table) {
$table->index('owner_id');
$table->index('link_id');
});
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
$table->integer('link')->change();
$table->integer('modify')->change();
$table->integer('forward_show')->change();
});
Schema::table('web_socket_dialog_users', function (Blueprint $table) {
$table->index('dialog_id');
$table->index('userid');
$table->integer('mark_unread')->change();
$table->integer('silence')->change();
$table->integer('important')->change();
});
Schema::table('web_socket_dialog_msg_todos', function (Blueprint $table) {
$table->index('msg_id');
$table->index('userid');
});
Schema::table('web_socket_dialog_msg_reads', function (Blueprint $table) {
$table->index('dialog_id');
$table->integer('mention')->change();
$table->integer('silence')->change();
$table->integer('email')->change();
$table->integer('after')->change();
});
// 文件相关
Schema::table('files', function (Blueprint $table) {
$table->index('pid');
$table->index('cid');
$table->integer('share')->change();
});
Schema::table('file_users', function (Blueprint $table) {
$table->index('file_id');
$table->index('userid');
$table->integer('permission')->change();
});
Schema::table('file_links', function (Blueprint $table) {
$table->index('file_id');
$table->index('userid');
});
Schema::table('file_contents', function (Blueprint $table) {
$table->index('fid');
$table->index('userid');
});
}
/**
* Reverse the migrations.
*
* @return voidw
*/
public function down()
{
// 项目相关
Schema::table('projects', function (Blueprint $table) {
$table->dropIndex(['userid']);
$table->dropIndex(['dialog_id']);
});
Schema::table('project_users', function (Blueprint $table) {
$table->dropIndex(['userid']);
$table->dropIndex(['project_id']);
$table->dropIndex(['owner']);
$table->dropIndex(['project_id','userid']);
});
Schema::table('project_tasks', function (Blueprint $table) {
$table->dropIndex(['parent_id']);
$table->dropIndex(['dialog_id']);
$table->dropIndex(['userid']);
});
Schema::table('project_task_users', function (Blueprint $table) {
$table->dropIndex(['owner']);
$table->dropIndex(['task_id','userid']);
});
Schema::table('project_task_files', function (Blueprint $table) {
$table->dropIndex(['project_id']);
$table->dropIndex(['task_id']);
});
Schema::table('project_task_tags', function (Blueprint $table) {
$table->dropIndex(['project_id']);
$table->dropIndex(['task_id']);
});
Schema::table('project_task_contents', function (Blueprint $table) {
$table->dropIndex(['project_id']);
$table->dropIndex(['task_id']);
});
Schema::table('project_task_push_logs', function (Blueprint $table) {
$table->dropIndex(['userid']);
$table->dropIndex(['task_id']);
});
Schema::table('project_task_flow_changes', function (Blueprint $table) {
$table->dropIndex(['userid']);
$table->dropIndex(['task_id']);
});
// 聊天相关
Schema::table('web_socket_dialogs', function (Blueprint $table) {
$table->dropIndex(['owner_id']);
$table->dropIndex(['link_id']);
});
Schema::table('web_socket_dialog_users', function (Blueprint $table) {
$table->dropIndex(['dialog_id']);
$table->dropIndex(['userid']);
});
Schema::table('web_socket_dialog_msg_todos', function (Blueprint $table) {
$table->dropIndex(['msg_id']);
$table->dropIndex(['userid']);
});
Schema::table('web_socket_dialog_msg_reads', function (Blueprint $table) {
$table->dropIndex(['dialog_id']);
});
// 文件相关
Schema::table('files', function (Blueprint $table) {
$table->dropIndex(['pid']);
$table->dropIndex(['cid']);
});
Schema::table('file_users', function (Blueprint $table) {
$table->dropIndex(['file_id']);
$table->dropIndex(['userid']);
});
Schema::table('file_links', function (Blueprint $table) {
$table->dropIndex(['file_id']);
$table->dropIndex(['userid']);
});
Schema::table('file_contents', function (Blueprint $table) {
$table->dropIndex(['fid']);
$table->dropIndex(['userid']);
});
}
}

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddWebSocketDialogMsgsTopAt extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
if (!Schema::hasColumn('web_socket_dialog_msgs', 'top')) {
$table->bigInteger('top')->nullable()->default(0)->after('send')->comment('置顶的会员ID');
$table->timestamp('top_at')->nullable()->after('top')->comment('置顶时间');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
$table->dropColumn("top");
$table->dropColumn("top_at");
});
}
}

View File

@@ -0,0 +1,48 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateProjectTaskVisibilityUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('project_task_visibility_users', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('project_id')->index()->nullable()->default(0)->comment('项目ID');
$table->bigInteger('task_id')->index()->nullable()->default(0)->comment('任务ID');
$table->bigInteger('userid')->index()->nullable()->default(0)->comment('成员ID');
$table->timestamps();
});
// 迁移旧数据
DB::table('project_task_users')->where("owner",2)->orderBy('id')->chunk(100, function ($data) {
foreach ($data as $item) {
DB::table('project_task_visibility_users')->insert([
'project_id' => $item->project_id,
'task_id' => $item->task_id,
'userid' => $item->userid,
'created_at' => $item->created_at,
'updated_at' => $item->updated_at
]);
}
});
DB::table('project_task_users')->where("owner",2)->delete();
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('project_task_visibility_users');
}
}

View File

@@ -0,0 +1,52 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddWebSocketDialogsTop extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
if (Schema::hasColumn('web_socket_dialog_msgs', 'top')) {
$table->dropColumn("top");
$table->dropColumn("top_at");
}
});
//
Schema::table('web_socket_dialogs', function (Blueprint $table) {
$table->bigInteger('top_userid')->nullable()->default(0)->after('link_id')->comment('置顶的用户ID');
$table->bigInteger('top_msg_id')->nullable()->default(0)->after('top_userid')->comment('置顶的消息ID');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
if (!Schema::hasColumn('web_socket_dialog_msgs', 'top')) {
$table->bigInteger('top')->nullable()->default(0)->after('send')->comment('置顶的会员ID');
$table->timestamp('top_at')->nullable()->after('top')->comment('置顶时间');
}
});
//
Schema::table('web_socket_dialogs', function (Blueprint $table) {
if (Schema::hasColumn('web_socket_dialogs', 'top_msg_id')) {
$table->dropColumn("top_msg_id");
}
if (Schema::hasColumn('web_socket_dialogs', 'top_userid')) {
$table->dropColumn("top_userid");
}
});
}
}

View File

@@ -0,0 +1,35 @@
<?php
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
use App\Models\User;
use App\Models\WebSocketDialog;
use App\Module\Base;
use Carbon\Carbon;
use Illuminate\Database\Migrations\Migration;
class AllGroupMuteHandle extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$systemConfig = Base::setting('system');
if ($systemConfig['all_group_mute'] == 'user' || $systemConfig['all_group_mute'] == 'all') {
$systemConfig['all_group_mute'] = 'close';
Base::setting('system', $systemConfig);
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
}
}

View File

@@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Schema;
class ChangeCharsetToUtf8mb4 extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$pre = DB::connection()->getTablePrefix();
$tables = [
"approve_execution",
"approve_execution_history",
"approve_identitylink",
"approve_identitylink_history",
"approve_proc_inst",
"approve_proc_inst_history",
"approve_proc_msgs",
"approve_procdef",
"approve_procdef_history",
"approve_task",
"approve_task_history"
];
foreach ($tables as $table) {
if (Schema::hasTable($table)) {
Schema::getConnection()->getPdo()->exec("ALTER TABLE `{$pre}{$table}` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
}
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
}
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddWebSocketDialogUsersAddHide extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('web_socket_dialog_users', function (Blueprint $table) {
if (!Schema::hasColumn('web_socket_dialog_users', 'hide')) {
$table->integer('hide')->nullable()->default(0)->after('silence')->comment('不显示会话0否1是');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('web_socket_dialog_users', function (Blueprint $table) {
$table->dropColumn("hide");
});
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddIndexSome20240315 extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('web_socket_dialog_msg_reads', function (Blueprint $table) {
$table->index(['dialog_id', 'userid', 'read_at', 'msg_id'], 'IDEX_dialog_id_userid_read_at_msg_id');
});
}
/**
* Reverse the migrations.
*
* @return voidw
*/
public function down()
{
Schema::table('web_socket_dialog_msg_reads', function (Blueprint $table) {
$table->dropIndex('IDEX_dialog_id_userid_read_at_msg_id');
});
}
}

View File

@@ -0,0 +1,50 @@
<?php
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogUser;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddWebSocketDialogUsersLastAt extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$isAdd = false;
Schema::table('web_socket_dialog_users', function (Blueprint $table) use (&$isAdd) {
if (!Schema::hasColumn('web_socket_dialog_users', 'last_at')) {
$isAdd = true;
$table->timestamp('last_at')->nullable()->after('top_at')->comment('最后消息时间');
}
});
if ($isAdd) {
// 更新数据
WebSocketDialog::chunk(100, function ($dialogs) {
/** @var WebSocketDialog $dialog */
foreach ($dialogs as $dialog) {
WebSocketDialogUser::whereDialogId($dialog->id)->update(['last_at' => $dialog->last_at]);
}
});
// 删除表字段
Schema::table('web_socket_dialogs', function (Blueprint $table) {
$table->dropColumn("last_at");
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// 回滚数据 - 无法回滚
}
}

View File

@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddIndexSome20240317 extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('web_socket_dialog_users', function (Blueprint $table) {
$table->dropIndex(['userid']);
$table->index(['userid', 'dialog_id']);
});
}
/**
* Reverse the migrations.
*
* @return voidw
*/
public function down()
{
// 回滚数据 - 无法回滚
}
}

View File

@@ -0,0 +1,55 @@
<?php
use App\Module\Base;
use App\Models\WebSocketDialogMsg;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ChangeForwardData extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
if (Schema::hasColumn('web_socket_dialog_msgs', 'forward_show')) {
WebSocketDialogMsg::where("forward_id", ">", 0)->chunk(100, function ($items) {
/** @var WebSocketDialogMsg $item */
foreach ($items as $item) {
$msg = Base::json2array($item->getRawOriginal('msg'));
$msg['forward_data'] = [
'id' => $item->forward_id,
'userid' => 0,
'parent_id' => $item->forward_id,
'parent_userid' => 0,
'show' => 0,
];
$original = WebSocketDialogMsg::select(['id', 'userid', 'forward_show'])->whereId($item->forward_id)->withTrashed()->first();
if ($original) {
$msg['forward_data']['userid'] = $original->userid;
$msg['forward_data']['parent_userid'] = $original->userid;
$msg['forward_data']['show'] = $original->forward_show;
}
$item->msg = Base::array2json($msg);
$item->save();
}
});
$table->dropColumn("forward_show");
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// 回滚数据 - 无法回滚
}
}

View File

@@ -0,0 +1,49 @@
<?php
use App\Module\Base;
use App\Models\WebSocketDialogMsg;
use Illuminate\Database\Migrations\Migration;
class ChangeReplyData extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
WebSocketDialogMsg::where("reply_id", ">", 0)->chunk(100, function ($items) {
/** @var WebSocketDialogMsg $item */
foreach ($items as $item) {
$msg = Base::json2array($item->getRawOriginal('msg'));
$msg['reply_data'] = [
'id' => $item->reply_id,
'userid' => 0,
'type' => '',
'msg' => [],
];
$original = WebSocketDialogMsg::whereId($item->reply_id)->withTrashed()->first();
if ($original) {
$replyMsg = Base::json2array($original->getRawOriginal('msg'));
unset($replyMsg['reply_data']);
$msg['reply_data']['userid'] = $original->userid;
$msg['reply_data']['type'] = $original->type;
$msg['reply_data']['msg'] = $replyMsg;
}
$item->msg = Base::array2json($msg);
$item->save();
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// 回滚数据 - 无法回滚
}
}

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddUmengAliasDevice extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
//
Schema::table('umeng_alias', function (Blueprint $table) {
if (!Schema::hasColumn('umeng_alias', 'device')) {
$table->text('ua')->nullable()->after('platform')->comment('userAgent');
$table->string('device', 100)->nullable()->default('')->after('platform')->comment('设备类型');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
Schema::table('umeng_alias', function (Blueprint $table) {
$table->dropColumn("ua");
$table->dropColumn("device");
});
}
}

View File

@@ -26,6 +26,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 1,
'dialog_id' => 1,
'userid' => 1,
'last_at' => seeders_at('2021-07-01 10:46:37'),
'created_at' => seeders_at('2021-07-01 10:46:37'),
'updated_at' => seeders_at('2021-07-01 10:46:37'),
),
@@ -34,6 +35,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 2,
'dialog_id' => 1,
'userid' => 2,
'last_at' => seeders_at('2021-07-01 10:46:37'),
'created_at' => seeders_at('2021-07-01 10:46:37'),
'updated_at' => seeders_at('2021-07-01 10:46:37'),
),
@@ -42,6 +44,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 3,
'dialog_id' => 2,
'userid' => 1,
'last_at' => seeders_at('2021-07-01 10:46:47'),
'created_at' => seeders_at('2021-07-01 10:46:47'),
'updated_at' => seeders_at('2021-07-01 10:46:47'),
),
@@ -50,6 +53,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 4,
'dialog_id' => 3,
'userid' => 1,
'last_at' => seeders_at('2021-07-01 10:46:47'),
'created_at' => seeders_at('2021-07-01 10:46:47'),
'updated_at' => seeders_at('2021-07-01 10:46:47'),
),
@@ -58,6 +62,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 5,
'dialog_id' => 4,
'userid' => 1,
'last_at' => seeders_at('2021-07-01 10:47:45'),
'created_at' => seeders_at('2021-07-01 10:47:45'),
'updated_at' => seeders_at('2021-07-01 10:47:45'),
),
@@ -66,6 +71,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 6,
'dialog_id' => 5,
'userid' => 1,
'last_at' => seeders_at('2021-07-01 10:47:45'),
'created_at' => seeders_at('2021-07-01 10:47:45'),
'updated_at' => seeders_at('2021-07-01 10:47:45'),
),
@@ -74,6 +80,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 7,
'dialog_id' => 6,
'userid' => 1,
'last_at' => seeders_at('2021-07-01 11:02:57'),
'created_at' => seeders_at('2021-07-01 11:02:57'),
'updated_at' => seeders_at('2021-07-01 11:02:57'),
),
@@ -82,6 +89,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 8,
'dialog_id' => 7,
'userid' => 1,
'last_at' => seeders_at('2021-07-01 11:02:57'),
'created_at' => seeders_at('2021-07-01 11:02:57'),
'updated_at' => seeders_at('2021-07-01 11:02:57'),
),
@@ -90,6 +98,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 9,
'dialog_id' => 8,
'userid' => 1,
'last_at' => seeders_at('2021-07-01 11:43:01'),
'created_at' => seeders_at('2021-07-01 11:43:01'),
'updated_at' => seeders_at('2021-07-01 11:43:01'),
),
@@ -98,6 +107,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 10,
'dialog_id' => 9,
'userid' => 1,
'last_at' => seeders_at('2021-07-01 11:43:01'),
'created_at' => seeders_at('2021-07-01 11:43:01'),
'updated_at' => seeders_at('2021-07-01 11:43:01'),
),
@@ -106,6 +116,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 11,
'dialog_id' => 10,
'userid' => 1,
'last_at' => seeders_at('2021-07-01 15:33:23'),
'created_at' => seeders_at('2021-07-01 15:33:23'),
'updated_at' => seeders_at('2021-07-01 15:33:23'),
),
@@ -114,6 +125,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 12,
'dialog_id' => 11,
'userid' => 1,
'last_at' => seeders_at('2021-07-01 15:33:23'),
'created_at' => seeders_at('2021-07-01 15:33:23'),
'updated_at' => seeders_at('2021-07-01 15:33:23'),
),
@@ -122,6 +134,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 13,
'dialog_id' => 12,
'userid' => 1,
'last_at' => seeders_at('2021-07-01 15:37:06'),
'created_at' => seeders_at('2021-07-01 15:37:06'),
'updated_at' => seeders_at('2021-07-01 15:37:06'),
),
@@ -130,6 +143,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 14,
'dialog_id' => 13,
'userid' => 1,
'last_at' => seeders_at('2021-07-01 15:37:06'),
'created_at' => seeders_at('2021-07-01 15:37:06'),
'updated_at' => seeders_at('2021-07-01 15:37:06'),
),
@@ -138,6 +152,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 15,
'dialog_id' => 14,
'userid' => 1,
'last_at' => seeders_at('2021-07-01 16:15:28'),
'created_at' => seeders_at('2021-07-01 16:15:28'),
'updated_at' => seeders_at('2021-07-01 16:15:28'),
),
@@ -146,6 +161,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 16,
'dialog_id' => 15,
'userid' => 1,
'last_at' => seeders_at('2021-07-01 16:15:28'),
'created_at' => seeders_at('2021-07-01 16:15:28'),
'updated_at' => seeders_at('2021-07-01 16:15:28'),
),
@@ -154,6 +170,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 17,
'dialog_id' => 7,
'userid' => 2,
'last_at' => seeders_at('2021-07-01 16:22:42'),
'created_at' => seeders_at('2021-07-01 16:22:42'),
'updated_at' => seeders_at('2021-07-01 16:22:42'),
),
@@ -162,6 +179,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 18,
'dialog_id' => 5,
'userid' => 2,
'last_at' => seeders_at('2021-07-01 16:23:15'),
'created_at' => seeders_at('2021-07-01 16:23:15'),
'updated_at' => seeders_at('2021-07-01 16:23:15'),
),
@@ -170,6 +188,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 19,
'dialog_id' => 15,
'userid' => 2,
'last_at' => seeders_at('2021-07-01 16:23:40'),
'created_at' => seeders_at('2021-07-01 16:23:40'),
'updated_at' => seeders_at('2021-07-01 16:23:40'),
),
@@ -178,6 +197,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 20,
'dialog_id' => 11,
'userid' => 2,
'last_at' => seeders_at('2021-07-01 16:29:38'),
'created_at' => seeders_at('2021-07-01 16:29:38'),
'updated_at' => seeders_at('2021-07-01 16:29:38'),
),
@@ -186,6 +206,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 21,
'dialog_id' => 16,
'userid' => 2,
'last_at' => seeders_at('2021-07-01 16:56:05'),
'created_at' => seeders_at('2021-07-01 16:56:05'),
'updated_at' => seeders_at('2021-07-01 16:56:05'),
),
@@ -194,6 +215,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 22,
'dialog_id' => 16,
'userid' => 1,
'last_at' => seeders_at('2021-07-01 16:56:05'),
'created_at' => seeders_at('2021-07-01 16:56:05'),
'updated_at' => seeders_at('2021-07-01 16:56:05'),
),
@@ -202,6 +224,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 23,
'dialog_id' => 17,
'userid' => 2,
'last_at' => seeders_at('2021-07-01 16:56:35'),
'created_at' => seeders_at('2021-07-01 16:56:35'),
'updated_at' => seeders_at('2021-07-01 16:56:35'),
),
@@ -210,6 +233,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 24,
'dialog_id' => 17,
'userid' => 1,
'last_at' => seeders_at('2021-07-01 16:56:35'),
'created_at' => seeders_at('2021-07-01 16:56:35'),
'updated_at' => seeders_at('2021-07-01 16:56:35'),
),
@@ -218,6 +242,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 25,
'dialog_id' => 18,
'userid' => 2,
'last_at' => seeders_at('2021-07-01 16:56:57'),
'created_at' => seeders_at('2021-07-01 16:56:57'),
'updated_at' => seeders_at('2021-07-01 16:56:57'),
),
@@ -226,6 +251,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
'id' => 26,
'dialog_id' => 18,
'userid' => 1,
'last_at' => seeders_at('2021-07-01 16:56:57'),
'created_at' => seeders_at('2021-07-01 16:56:57'),
'updated_at' => seeders_at('2021-07-01 16:56:57'),
),

View File

@@ -4,6 +4,7 @@ namespace Database\Seeders;
use App\Models\User;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogUser;
use Carbon\Carbon;
use Illuminate\Database\Seeder;
@@ -30,7 +31,6 @@ class WebSocketDialogsTableSeeder extends Seeder
'type' => 'user',
'group_type' => '',
'name' => '',
'last_at' => seeders_at('2021-07-01 17:12:52'),
'created_at' => seeders_at('2021-07-01 10:46:37'),
'updated_at' => seeders_at('2021-07-01 17:12:52'),
'deleted_at' => NULL,
@@ -41,7 +41,6 @@ class WebSocketDialogsTableSeeder extends Seeder
'type' => 'group',
'group_type' => 'project',
'name' => '',
'last_at' => NULL,
'created_at' => seeders_at('2021-07-01 10:46:47'),
'updated_at' => seeders_at('2021-07-01 17:31:03'),
'deleted_at' => seeders_at('2021-07-01 17:31:03'),
@@ -52,7 +51,6 @@ class WebSocketDialogsTableSeeder extends Seeder
'type' => 'group',
'group_type' => 'project',
'name' => '',
'last_at' => NULL,
'created_at' => seeders_at('2021-07-01 10:46:47'),
'updated_at' => seeders_at('2021-07-01 10:46:55'),
'deleted_at' => seeders_at('2021-07-01 10:46:55'),
@@ -63,7 +61,6 @@ class WebSocketDialogsTableSeeder extends Seeder
'type' => 'group',
'group_type' => 'project',
'name' => '',
'last_at' => NULL,
'created_at' => seeders_at('2021-07-01 10:47:45'),
'updated_at' => seeders_at('2021-07-01 17:30:05'),
'deleted_at' => seeders_at('2021-07-01 17:30:05'),
@@ -74,7 +71,6 @@ class WebSocketDialogsTableSeeder extends Seeder
'type' => 'group',
'group_type' => 'project',
'name' => '',
'last_at' => seeders_at('2021-07-01 16:31:58'),
'created_at' => seeders_at('2021-07-01 10:47:45'),
'updated_at' => seeders_at('2021-07-01 16:31:58'),
'deleted_at' => NULL,
@@ -85,7 +81,6 @@ class WebSocketDialogsTableSeeder extends Seeder
'type' => 'group',
'group_type' => 'project',
'name' => '',
'last_at' => NULL,
'created_at' => seeders_at('2021-07-01 11:02:57'),
'updated_at' => seeders_at('2021-07-01 17:31:28'),
'deleted_at' => seeders_at('2021-07-01 17:31:28'),
@@ -96,7 +91,6 @@ class WebSocketDialogsTableSeeder extends Seeder
'type' => 'group',
'group_type' => 'project',
'name' => '',
'last_at' => seeders_at('2021-07-01 16:32:50'),
'created_at' => seeders_at('2021-07-01 11:02:57'),
'updated_at' => seeders_at('2021-07-01 16:32:50'),
'deleted_at' => NULL,
@@ -107,7 +101,6 @@ class WebSocketDialogsTableSeeder extends Seeder
'type' => 'group',
'group_type' => 'project',
'name' => '',
'last_at' => NULL,
'created_at' => seeders_at('2021-07-01 11:43:01'),
'updated_at' => seeders_at('2021-07-01 17:31:28'),
'deleted_at' => seeders_at('2021-07-01 17:31:28'),
@@ -118,7 +111,6 @@ class WebSocketDialogsTableSeeder extends Seeder
'type' => 'group',
'group_type' => 'project',
'name' => '',
'last_at' => seeders_at('2021-07-01 14:02:57'),
'created_at' => seeders_at('2021-07-01 11:43:01'),
'updated_at' => seeders_at('2021-07-01 14:02:57'),
'deleted_at' => NULL,
@@ -129,7 +121,6 @@ class WebSocketDialogsTableSeeder extends Seeder
'type' => 'group',
'group_type' => 'project',
'name' => '',
'last_at' => NULL,
'created_at' => seeders_at('2021-07-01 15:33:23'),
'updated_at' => seeders_at('2021-07-01 17:31:28'),
'deleted_at' => seeders_at('2021-07-01 17:31:28'),
@@ -140,7 +131,6 @@ class WebSocketDialogsTableSeeder extends Seeder
'type' => 'group',
'group_type' => 'project',
'name' => '',
'last_at' => seeders_at('2021-07-01 16:31:40'),
'created_at' => seeders_at('2021-07-01 15:33:23'),
'updated_at' => seeders_at('2021-07-01 16:31:40'),
'deleted_at' => NULL,
@@ -151,7 +141,6 @@ class WebSocketDialogsTableSeeder extends Seeder
'type' => 'group',
'group_type' => 'project',
'name' => '',
'last_at' => NULL,
'created_at' => seeders_at('2021-07-01 15:37:06'),
'updated_at' => seeders_at('2021-07-01 17:31:28'),
'deleted_at' => seeders_at('2021-07-01 17:31:28'),
@@ -162,7 +151,6 @@ class WebSocketDialogsTableSeeder extends Seeder
'type' => 'group',
'group_type' => 'project',
'name' => '',
'last_at' => seeders_at('2021-07-01 15:44:09'),
'created_at' => seeders_at('2021-07-01 15:37:06'),
'updated_at' => seeders_at('2021-07-01 15:44:09'),
'deleted_at' => NULL,
@@ -173,7 +161,6 @@ class WebSocketDialogsTableSeeder extends Seeder
'type' => 'group',
'group_type' => 'project',
'name' => '',
'last_at' => NULL,
'created_at' => seeders_at('2021-07-01 16:15:28'),
'updated_at' => seeders_at('2021-07-01 17:31:28'),
'deleted_at' => seeders_at('2021-07-01 17:31:28'),
@@ -184,7 +171,6 @@ class WebSocketDialogsTableSeeder extends Seeder
'type' => 'group',
'group_type' => 'project',
'name' => '',
'last_at' => seeders_at('2021-07-01 16:37:59'),
'created_at' => seeders_at('2021-07-01 16:15:28'),
'updated_at' => seeders_at('2021-07-01 16:37:59'),
'deleted_at' => NULL,
@@ -195,7 +181,6 @@ class WebSocketDialogsTableSeeder extends Seeder
'type' => 'group',
'group_type' => 'task',
'name' => '',
'last_at' => seeders_at('2021-07-01 16:58:02'),
'created_at' => seeders_at('2021-07-01 16:56:05'),
'updated_at' => seeders_at('2021-07-01 16:58:02'),
'deleted_at' => NULL,
@@ -206,7 +191,6 @@ class WebSocketDialogsTableSeeder extends Seeder
'type' => 'group',
'group_type' => 'task',
'name' => '',
'last_at' => seeders_at('2021-07-01 16:57:58'),
'created_at' => seeders_at('2021-07-01 16:56:35'),
'updated_at' => seeders_at('2021-07-01 16:57:58'),
'deleted_at' => NULL,
@@ -217,7 +201,6 @@ class WebSocketDialogsTableSeeder extends Seeder
'type' => 'group',
'group_type' => 'task',
'name' => '',
'last_at' => seeders_at('2021-07-01 16:57:52'),
'created_at' => seeders_at('2021-07-01 16:56:57'),
'updated_at' => seeders_at('2021-07-01 16:57:52'),
'deleted_at' => NULL,
@@ -228,8 +211,7 @@ class WebSocketDialogsTableSeeder extends Seeder
if ($botUser) {
$dialog = WebSocketDialog::checkUserDialog($botUser, 1);
if ($dialog) {
$dialog->last_at = Carbon::now();
$dialog->save();
WebSocketDialogUser::whereDialogId($dialog->id)->change(['last_at' => Carbon::now()->subSecond()]);
}
}
User::botGetOrCreate('ai-openai');

View File

@@ -36,6 +36,7 @@ services:
image: "nginx:alpine"
ports:
- "${APP_PORT}:80"
- "${APP_SSL_PORT:-}:443"
volumes:
- ./docker/nginx:/etc/nginx/conf.d
- ./public:/var/www/public
@@ -103,7 +104,7 @@ services:
fileview:
container_name: "dootask-fileview-${APP_ID}"
image: "kuaifan/fileview:4.2.0-SNAPSHOT-RC22"
image: "kuaifan/fileview:4.2.0-SNAPSHOT-RC25"
environment:
KK_CONTEXT_PATH: "/fileview"
KK_OFFICE_PREVIEW_SWITCH_DISABLED: true
@@ -150,7 +151,7 @@ services:
approve:
container_name: "dootask-approve-${APP_ID}"
image: "kuaifan/dooapprove:0.0.5"
image: "kuaifan/dooapprove:0.0.10"
environment:
TZ: "${TIMEZONE:-PRC}"
MYSQL_HOST: "${DB_HOST}"
@@ -159,6 +160,8 @@ services:
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"
@@ -168,7 +171,7 @@ services:
ai:
container_name: "dootask-ai-${APP_ID}"
image: "kuaifan/dooai:0.0.8"
image: "kuaifan/dooai:0.1.8"
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.12"
@@ -176,7 +179,7 @@ services:
okr:
container_name: "dootask-okr-${APP_ID}"
image: "kuaifan/doookr:0.0.22"
image: "kuaifan/doookr:0.4.2"
environment:
TZ: "${TIMEZONE:-PRC}"
DOO_TASK_URL: "http://${APP_IPPR}.3"
@@ -185,6 +188,9 @@ services:
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"

View File

@@ -12,7 +12,7 @@ default_storage_engine = InnoDB
performance_schema_max_table_instances = 400
table_definition_cache = 400
key_buffer_size = 1024M
max_allowed_packet = 10G
max_allowed_packet = 4096M
table_open_cache = 2048
sort_buffer_size = 4096K
net_buffer_length = 4K
@@ -47,7 +47,7 @@ innodb_write_io_threads = 2
[mysqldump]
quick
max_allowed_packet = 500M
max_allowed_packet = 4096M
[mysql]
no-auto-rehash

View File

@@ -1 +0,0 @@
site

View File

@@ -1 +1 @@
/*.conf
!.gitignore

1
docker/nginx/site/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
!.gitignore

View File

@@ -52,7 +52,6 @@
window._disableDownload = setInterval(function() {
var downloadBtn = document.getElementById("slot-hbtn-download")
if(downloadBtn) {
console.log(1)
clearInterval(window._disableDownload);
document.getElementById("slot-hbtn-download")?.remove()
document.getElementById("fm-btn-download")?.remove()

39
electron/build.js vendored
View File

@@ -126,6 +126,16 @@ function genericPublish({url, key, version, output}) {
if (err) {
console.warn(err)
} else {
let uploadFileNum = 0;
for (const filename of files) {
const localFile = path.join(filePath, filename)
if (fs.existsSync(localFile)) {
const fileStat = fs.statSync(localFile)
if (fileStat.isFile()) {
uploadFileNum += 1;
}
}
}
const uploadOras = {}
for (const filename of files) {
const localFile = path.join(filePath, filename)
@@ -135,6 +145,7 @@ function genericPublish({url, key, version, output}) {
uploadOras[filename] = ora(`Upload [0%] ${filename}`).start()
const formData = new FormData()
formData.append("file", fs.createReadStream(localFile));
formData.append("file_num", uploadFileNum);
await axiosAutoTry({
axios: {
method: 'post',
@@ -203,8 +214,6 @@ function startBuild(data) {
fs.writeFileSync(electronDir + "/config.js", "window.systemInfo = " + JSON.stringify(systemInfo), 'utf8');
fs.writeFileSync(nativeCachePath, utils.formatUrl(data.url));
fs.writeFileSync(devloadCachePath, "", 'utf8');
// default (fix "Failed to load resource: net::ERR_FILE_NOT_FOUND" report)
fs.writeFileSync(electronDir + "/default", "default", 'utf8');
// index.html
let manifestFile = path.resolve(electronDir, "manifest.json");
if (!fs.existsSync(manifestFile)) {
@@ -220,14 +229,16 @@ function startBuild(data) {
fs.writeFileSync(indexFile, indexString, 'utf8');
//
if (data.id === 'app') {
const eeuiDir = path.resolve(__dirname, "../resources/mobile");
const eeuiCli = "kuaifan/eeui-cli:0.0.1"
const publicDir = path.resolve(__dirname, "../resources/mobile/src/public");
fse.removeSync(publicDir)
fse.copySync(electronDir, publicDir)
if (argv[3] === "setting") {
child_process.spawnSync("eeui", ["setting"], {stdio: "inherit", cwd: "resources/mobile"});
child_process.spawnSync("docker", `run -it --rm -v ${eeuiDir}:/work -w /work ${eeuiCli} eeui setting`.split(" "), {stdio: "inherit", cwd: "resources/mobile"});
}
if (['setting', 'build'].includes(argv[3])) {
child_process.spawnSync("eeui", ["build", "--simple"], {stdio: "inherit", cwd: "resources/mobile"});
child_process.spawnSync("docker", `run -it --rm -v ${eeuiDir}:/work -w /work ${eeuiCli} eeui build --simple`.split(" "), {stdio: "inherit", cwd: "resources/mobile"});
} else {
[
path.resolve(publicDir, "../../platforms/ios/eeuiApp/bundlejs/eeui/public"),
@@ -262,8 +273,8 @@ function startBuild(data) {
econfig.build.nsis.artifactName = appName + "-v${version}-${os}-${arch}.${ext}";
// changelog
econfig.build.releaseInfo.releaseNotes = changeLog()
if (release) {
econfig.build.releaseInfo.releaseNotes = econfig.build.releaseInfo.releaseNotes.replace(`## [${config.version}]`, `## [${config.version}-Release]`)
if (!release) {
econfig.build.releaseInfo.releaseNotes = econfig.build.releaseInfo.releaseNotes.replace(`## [${config.version}]`, `## [${config.version}-Silence]`)
}
// darwin notarize
if (notarize && APPLEID && APPLEIDPASS) {
@@ -323,18 +334,20 @@ if (["dev"].includes(argv[2])) {
configure: {
platform: '',
publish: false,
release: false,
release: true,
notarize: false,
}
}, false, false)
} else if (["all"].includes(argv[2])) {
} else if (["all", "win", "mac"].includes(argv[2])) {
// 自动编译
platforms.forEach(platform => {
platforms.filter(p => {
return argv[2] === "all" || p.indexOf(argv[2]) !== -1
}).forEach(platform => {
config.app.forEach(data => {
data.configure = {
platform,
publish: true,
release: false,
release: true,
notarize: false,
}
startBuild(data)
@@ -375,11 +388,11 @@ if (["dev"].includes(argv[2])) {
name: 'release',
message: "选择是否弹出升级提示框",
choices: [{
name: "No",
value: false
}, {
name: "Yes",
value: true
}, {
name: "No",
value: false
}]
},
{

View File

@@ -72,6 +72,7 @@ contextBridge.exposeInMainWorld(
contextBridge.exposeInMainWorld(
'process', {
type: process.type,
versions: process.versions
versions: process.versions,
platform: process.platform,
}
);

508
electron/electron.js vendored
View File

@@ -1,9 +1,11 @@
const fs = require('fs')
const os = require("os");
const path = require('path')
const {app, BrowserWindow, ipcMain, dialog, clipboard, nativeImage, shell, Tray, Menu, globalShortcut, Notification} = require('electron')
const {app, BrowserWindow, ipcMain, dialog, clipboard, nativeImage, shell, Tray, Menu, globalShortcut, Notification, BrowserView, nativeTheme} = require('electron')
const {autoUpdater} = require("electron-updater")
const log = require("electron-log");
const electronConf = require('electron-config')
const userConf = new electronConf()
const fsProm = require('fs/promises');
const PDFDocument = require('pdf-lib').PDFDocument;
const Screenshots = require("electron-screenshots-tool").Screenshots;
@@ -17,13 +19,13 @@ const spawn = require("child_process").spawn;
const isMac = process.platform === 'darwin'
const isWin = process.platform === 'win32'
const allowedUrls = /^(?:https?|mailto|tel|callto):/i;
const allowedCalls = /^(?:mailto|tel|callto):/i;
let enableStoreBkp = true;
let dialogOpen = false;
let enablePlugins = false;
let mainWindow = null,
mainTray = null,
subWindow = [],
isReady = false,
willQuitApp = false,
devloadUrl = "",
@@ -32,6 +34,20 @@ let mainWindow = null,
let screenshotObj = null,
screenshotKey = null;
let childWindow = [],
webTabWindow = null,
webTabView = [],
webTabHeight = 38;
let showState = {},
onShowWindow = (win) => {
if (typeof showState[win.webContents.id] === 'undefined') {
showState[win.webContents.id] = true
win.setBackgroundColor('rgba(255, 255, 255, 0)')
win.show();
}
}
if (fs.existsSync(devloadCachePath)) {
devloadUrl = fs.readFileSync(devloadCachePath, 'utf8')
}
@@ -43,6 +59,8 @@ function createMainWindow() {
mainWindow = new BrowserWindow({
width: 1280,
height: 800,
minWidth: 360,
minHeight: 360,
center: true,
autoHideMenuBar: true,
webPreferences: {
@@ -56,19 +74,20 @@ function createMainWindow() {
const originalUA = mainWindow.webContents.session.getUserAgent() || mainWindow.webContents.getUserAgent()
mainWindow.webContents.setUserAgent(originalUA + " MainTaskWindow/" + process.platform + "/" + os.arch() + "/1.0");
mainWindow.webContents.setWindowOpenHandler(({url}) => {
openExternal(url)
if (allowedCalls.test(url)) {
return {action: 'allow'}
}
utils.onBeforeOpenWindow(mainWindow.webContents, url).then(() => {
openExternal(url)
})
return {action: 'deny'}
})
electronMenu.webContentsMenu(mainWindow.webContents)
if (devloadUrl) {
mainWindow.loadURL(devloadUrl).then(_ => {
})
mainWindow.loadURL(devloadUrl).then(_ => { }).catch(_ => { })
} else {
mainWindow.loadFile('./public/index.html').then(_ => {
})
mainWindow.loadFile('./public/index.html').then(_ => { }).catch(_ => { })
}
mainWindow.on('page-title-updated', (event, title) => {
@@ -96,7 +115,7 @@ function createMainWindow() {
* 创建子窗口
* @param args {path, hash, title, titleFixed, force, userAgent, config, webPreferences}
*/
function createSubWindow(args) {
function createChildWindow(args) {
if (!args) {
return;
}
@@ -106,7 +125,7 @@ function createSubWindow(args) {
}
let name = args.name || "auto_" + utils.randomString(6);
let item = subWindow.find(item => item.name == name);
let item = childWindow.find(item => item.name == name);
let browser = item ? item.browser : null;
if (browser) {
browser.focus();
@@ -119,7 +138,10 @@ function createSubWindow(args) {
browser = new BrowserWindow(Object.assign({
width: 1280,
height: 800,
minWidth: 360,
minHeight: 360,
center: true,
show: false,
parent: mainWindow,
autoHideMenuBar: true,
webPreferences: Object.assign({
@@ -146,34 +168,49 @@ function createSubWindow(args) {
})
browser.on('closed', () => {
let index = subWindow.findIndex(item => item.name == name);
let index = childWindow.findIndex(item => item.name == name);
if (index > -1) {
subWindow.splice(index, 1)
childWindow.splice(index, 1)
}
})
subWindow.push({ name, browser })
browser.once('ready-to-show', () => {
onShowWindow(browser);
})
browser.webContents.once('dom-ready', () => {
onShowWindow(browser);
})
childWindow.push({ name, browser })
}
const originalUA = browser.webContents.session.getUserAgent() || browser.webContents.getUserAgent()
browser.webContents.setUserAgent(originalUA + " SubTaskWindow/" + process.platform + "/" + os.arch() + "/1.0" + (args.userAgent ? (" " + args.userAgent) : ""));
browser.webContents.setWindowOpenHandler(({url}) => {
openExternal(url)
if (allowedCalls.test(url)) {
return {action: 'allow'}
}
utils.onBeforeOpenWindow(browser.webContents, url).then(() => {
openExternal(url)
})
return {action: 'deny'}
})
electronMenu.webContentsMenu(browser.webContents)
const hash = args.hash || args.path;
if (devloadUrl) {
browser.loadURL(devloadUrl + '#' + hash).then(_ => {
})
} else {
browser.loadFile('./public/index.html', {
hash
}).then(_ => {
})
if (/^https?:\/\//i.test(hash)) {
browser.loadURL(hash).then(_ => { }).catch(_ => { })
return;
}
if (devloadUrl) {
browser.loadURL(devloadUrl + '#' + hash).then(_ => { }).catch(_ => { })
return;
}
browser.loadFile('./public/index.html', {
hash
}).then(_ => {
})
}
/**
@@ -181,7 +218,7 @@ function createSubWindow(args) {
* @param browser
* @param args
*/
function updateSubWindow(browser, args) {
function updateChildWindow(browser, args) {
if (!args) {
return;
}
@@ -193,25 +230,331 @@ function updateSubWindow(browser, args) {
const hash = args.hash || args.path;
if (hash) {
if (devloadUrl) {
browser.loadURL(devloadUrl + '#' + hash).then(_ => {
})
browser.loadURL(devloadUrl + '#' + hash).then(_ => { }).catch(_ => { })
} else {
browser.loadFile('./public/index.html', {
hash
}).then(_ => {
})
}).then(_ => { }).catch(_ => { })
}
}
if (args.name) {
const er = subWindow.find(item => item.browser == browser);
const er = childWindow.find(item => item.browser == browser);
if (er) {
er.name = args.name;
}
}
}
/**
* 创建内置浏览器
* @param args {url, ?}
*/
function createWebTabWindow(args) {
if (!args) {
return;
}
if (!utils.isJson(args)) {
args = {url: args}
}
if (!allowedUrls.test(args.url)) {
return;
}
// 创建父级窗口
if (!webTabWindow) {
let config = Object.assign(args.config || {}, userConf.get('webTabWindow', {}));
let webPreferences = args.webPreferences || {};
const titleBarOverlay = {
height: webTabHeight
}
if (nativeTheme.shouldUseDarkColors) {
titleBarOverlay.color = '#3B3B3D'
titleBarOverlay.symbolColor = '#C5C5C5'
}
webTabWindow = new BrowserWindow(Object.assign({
x: mainWindow.getBounds().x + webTabHeight,
y: mainWindow.getBounds().y + webTabHeight,
width: 1280,
height: 800,
minWidth: 360,
minHeight: 360,
center: true,
show: false,
autoHideMenuBar: true,
titleBarStyle: 'hidden',
titleBarOverlay,
webPreferences: Object.assign({
preload: path.join(__dirname, 'electron-preload.js'),
webSecurity: true,
nodeIntegration: true,
contextIsolation: true,
nativeWindowOpen: true
}, webPreferences),
}, config))
webTabWindow.on('resize', () => {
resizeWebTab(0)
})
webTabWindow.on('enter-full-screen', () => {
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'enter-full-screen',
}).then(_ => { })
})
webTabWindow.on('leave-full-screen', () => {
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'leave-full-screen',
}).then(_ => { })
})
webTabWindow.on('close', event => {
if (!willQuitApp) {
closeWebTab(0)
event.preventDefault()
} else {
userConf.set('webTabWindow', webTabWindow.getBounds())
}
})
webTabWindow.on('closed', () => {
webTabView.forEach(({view}) => {
try {
view.webContents.close()
} catch (e) {
//
}
})
webTabView = []
webTabWindow = null
})
webTabWindow.once('ready-to-show', () => {
onShowWindow(webTabWindow);
})
webTabWindow.webContents.once('dom-ready', () => {
onShowWindow(webTabWindow);
})
webTabWindow.webContents.on('before-input-event', (event, input) => {
if (utils.isMetaOrControl(input) && input.key.toLowerCase() === 'r') {
reloadWebTab(0)
event.preventDefault()
} else if (utils.isMetaOrControl(input) && input.shift && input.key.toLowerCase() === 'i') {
devToolsWebTab(0)
}
})
webTabWindow.loadFile('./render/tabs/index.html', {}).then(_ => {
})
}
webTabWindow.focus();
// 创建子窗口
const browserView = new BrowserView({
useHTMLTitleAndIcon: true,
useLoadingView: true,
useErrorView: true,
webPreferences: {
preload: path.join(__dirname, 'electron-preload.js'),
}
})
if (nativeTheme.shouldUseDarkColors) {
browserView.setBackgroundColor('#575757')
} else {
browserView.setBackgroundColor('#FFFFFF')
}
browserView.setBounds({
x: 0,
y: webTabHeight,
width: webTabWindow.getContentBounds().width || 1280,
height: (webTabWindow.getContentBounds().height || 800) - webTabHeight,
})
browserView.webContents.on('destroyed', () => {
closeWebTab(browserView.webContents.id)
})
browserView.webContents.setWindowOpenHandler(({url}) => {
if (allowedCalls.test(url)) {
return {action: 'allow'}
}
createWebTabWindow({url})
return {action: 'deny'}
})
browserView.webContents.on('page-title-updated', (event, title) => {
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'title',
id: browserView.webContents.id,
title: title,
url: browserView.webContents.getURL(),
}).then(_ => { })
})
browserView.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL, isMainFrame) => {
if (!errorDescription) {
return
}
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'title',
id: browserView.webContents.id,
title: errorDescription,
url: browserView.webContents.getURL(),
}).then(_ => { })
})
browserView.webContents.on('page-favicon-updated', (event, favicons) => {
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'favicon',
id: browserView.webContents.id,
favicons
}).then(_ => { })
})
browserView.webContents.on('did-start-loading', _ => {
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'start-loading',
id: browserView.webContents.id,
}).then(_ => { })
})
browserView.webContents.on('did-stop-loading', _ => {
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'stop-loading',
id: browserView.webContents.id,
}).then(_ => { })
})
browserView.webContents.on('before-input-event', (event, input) => {
if (utils.isMetaOrControl(input) && input.key.toLowerCase() === 'r') {
browserView.webContents.reload()
event.preventDefault()
} else if (utils.isMetaOrControl(input) && input.shift && input.key.toLowerCase() === 'i') {
browserView.webContents.toggleDevTools()
}
})
browserView.webContents.loadURL(args.url).then(_ => { }).catch(_ => { })
webTabWindow.addBrowserView(browserView)
webTabView.push({
id: browserView.webContents.id,
view: browserView
})
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'create',
id: browserView.webContents.id,
url: args.url,
}).then(_ => { })
activateWebTab(browserView.webContents.id)
}
/**
* 获取当前内置浏览器标签
* @returns {Electron.BrowserView|undefined}
*/
function currentWebTab() {
const views = webTabWindow.getBrowserViews()
const view = views.length ? views[views.length - 1] : undefined
if (!view) {
return undefined
}
return webTabView.find(item => item.id == view.webContents.id)
}
/**
* 重新加载内置浏览器标签
* @param id
*/
function reloadWebTab(id) {
const item = id === 0 ? currentWebTab() : webTabView.find(item => item.id == id)
if (!item) {
return
}
item.view.webContents.reload()
}
/**
* 内置浏览器标签打开开发者工具
* @param id
*/
function devToolsWebTab(id) {
const item = id === 0 ? currentWebTab() : webTabView.find(item => item.id == id)
if (!item) {
return
}
item.view.webContents.toggleDevTools()
}
/**
* 调整内置浏览器标签尺寸
* @param id
*/
function resizeWebTab(id) {
const item = id === 0 ? currentWebTab() : webTabView.find(item => item.id == id)
if (!item) {
return
}
item.view.setBounds({
x: 0,
y: webTabHeight,
width: webTabWindow.getContentBounds().width || 1280,
height: (webTabWindow.getContentBounds().height || 800) - webTabHeight,
})
}
/**
* 切换内置浏览器标签
* @param id
*/
function activateWebTab(id) {
const item = id === 0 ? currentWebTab() : webTabView.find(item => item.id == id)
if (!item) {
return
}
resizeWebTab(item.id)
webTabWindow.setTopBrowserView(item.view)
item.view.webContents.focus()
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'switch',
id: item.id,
}).then(_ => { })
}
/**
* 关闭内置浏览器标签
* @param id
*/
function closeWebTab(id) {
const item = id === 0 ? currentWebTab() : webTabView.find(item => item.id == id)
if (!item) {
return
}
if (webTabView.length === 1) {
webTabWindow.hide()
}
webTabWindow.removeBrowserView(item.view)
try {
item.view.webContents.close()
} catch (e) {
//
}
const index = webTabView.findIndex(({id}) => item.id == id)
if (index > -1) {
webTabView.splice(index, 1)
}
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'close',
id: item.id,
}).then(_ => { })
if (webTabView.length === 0) {
userConf.set('webTabWindow', webTabWindow.getBounds())
webTabWindow.destroy()
} else {
activateWebTab(0)
}
}
const getTheLock = app.requestSingleInstanceLock()
if (!getTheLock) {
app.quit()
@@ -329,8 +672,8 @@ ipcMain.on('windowQuit', (event) => {
* 创建路由窗口
* @param args {path, ?}
*/
ipcMain.on('windowRouter', (event, args) => {
createSubWindow(args)
ipcMain.on('openChildWindow', (event, args) => {
createChildWindow(args)
event.returnValue = "ok"
})
@@ -338,9 +681,89 @@ ipcMain.on('windowRouter', (event, args) => {
* 更新路由窗口
* @param args {?name, ?path} // name: 不是要更改的窗口名,是要把窗口名改成什么, path: 地址
*/
ipcMain.on('updateChildWindow', (event, args) => {
const browser = BrowserWindow.fromWebContents(event.sender);
updateChildWindow(browser, args)
event.returnValue = "ok"
})
/**
* 创建路由窗口todo 已废弃)
* @param args {path, ?}
*/
ipcMain.on('windowRouter', (event, args) => {
createChildWindow(args)
event.returnValue = "ok"
})
/**
* 更新路由窗口todo 已废弃)
* @param args {?name, ?path} // name: 不是要更改的窗口名,是要把窗口名改成什么, path: 地址
*/
ipcMain.on('updateRouter', (event, args) => {
const browser = BrowserWindow.fromWebContents(event.sender);
updateSubWindow(browser, args)
updateChildWindow(browser, args)
event.returnValue = "ok"
})
/**
* 内置浏览器 - 打开创建
* @param args {url, ?}
*/
ipcMain.on('openWebTabWindow', (event, args) => {
createWebTabWindow(args)
event.returnValue = "ok"
})
/**
* 内置浏览器 - 激活标签
* @param id
*/
ipcMain.on('webTabActivate', (event, id) => {
activateWebTab(id)
event.returnValue = "ok"
})
/**
* 内置浏览器 - 关闭标签
* @param id
*/
ipcMain.on('webTabClose', (event, id) => {
closeWebTab(id)
event.returnValue = "ok"
})
/**
* 内置浏览器 - 在外部浏览器打开
*/
ipcMain.on('webTabExternal', (event) => {
const item = currentWebTab()
if (!item) {
return
}
openExternal(item.view.webContents.getURL())
event.returnValue = "ok"
})
/**
* 内置浏览器 - 打开开发者工具
*/
ipcMain.on('webTabOpenDevTools', (event) => {
const item = currentWebTab()
if (!item) {
return
}
item.view.webContents.openDevTools()
event.returnValue = "ok"
})
/**
* 内置浏览器 - 销毁所有标签及窗口
*/
ipcMain.on('webTabDestroyAll', (event) => {
if (webTabWindow) {
webTabWindow.destroy()
}
event.returnValue = "ok"
})
@@ -377,8 +800,8 @@ ipcMain.on('windowDestroy', (event) => {
/**
* 关闭所有子窗口
*/
ipcMain.on('subWindowCloseAll', (event) => {
subWindow.some(({browser}) => {
ipcMain.on('childWindowCloseAll', (event) => {
childWindow.some(({browser}) => {
browser && browser.close()
})
event.returnValue = "ok"
@@ -387,8 +810,8 @@ ipcMain.on('subWindowCloseAll', (event) => {
/**
* 销毁所有子窗口
*/
ipcMain.on('subWindowDestroyAll', (event) => {
subWindow.some(({browser}) => {
ipcMain.on('childWindowDestroyAll', (event) => {
childWindow.some(({browser}) => {
browser && browser.destroy()
})
event.returnValue = "ok"
@@ -644,6 +1067,9 @@ ipcMain.on('updateCheckAndDownload', (event, args) => {
autoUpdater.setFeedURL(args)
}
autoUpdater.checkForUpdates().then(info => {
if (!info) {
return
}
if (utils.compareVersion(config.version, info.updateInfo.version) >= 0) {
return
}
@@ -685,7 +1111,7 @@ ipcMain.on('mainWindowActive', (event) => {
ipcMain.on('updateQuitAndInstall', (event) => {
event.returnValue = "ok"
willQuitApp = true
subWindow.some(({browser}) => {
childWindow.some(({browser}) => {
browser && browser.destroy()
})
setTimeout(_ => {

View File

@@ -12,6 +12,7 @@
<!--style-->
<link rel="stylesheet" type="text/css" href="./css/iview.css">
<link rel="stylesheet" type="text/css" href="./css/loading.css">
<script src="./js/loading-theme.js"></script>
<script src="./js/jsencrypt.min.js"></script>
<script src="./js/scroll-into-view.min.js"></script>
<script src="./config.js"></script>
@@ -20,7 +21,7 @@
<div id="app">
<div class="app-view-loading">
<div class="app-view-loading no-dark-mode">
<div>
<div>PAGE LOADING</div>
<span></span>

28
electron/package.json Normal file → Executable file
View File

@@ -26,26 +26,27 @@
"url": "https://github.com/kuaifan/dootask.git"
},
"devDependencies": {
"@electron-forge/cli": "^6.4.2",
"@electron-forge/maker-deb": "^6.4.2",
"@electron-forge/maker-rpm": "^6.4.2",
"@electron-forge/maker-squirrel": "^6.4.2",
"@electron-forge/maker-zip": "^6.4.2",
"dotenv": "^16.0.3",
"electron": "^27.0.2",
"electron-builder": "^24.6.4",
"@electron-forge/cli": "^7.3.0",
"@electron-forge/maker-deb": "^7.3.0",
"@electron-forge/maker-rpm": "^7.3.0",
"@electron-forge/maker-squirrel": "^7.3.0",
"@electron-forge/maker-zip": "^7.3.0",
"dotenv": "^16.3.1",
"electron": "^29.1.0",
"electron-builder": "^24.12.0",
"electron-notarize": "^1.2.2",
"form-data": "^4.0.0",
"ora": "^4.1.1"
},
"dependencies": {
"axios": "^1.3.4",
"axios": "^1.6.2",
"crc": "^3.8.0",
"electron-log": "^5.0.0",
"electron-screenshots-tool": "^1.1.0",
"electron-config": "^2.0.0",
"electron-log": "^5.1.1",
"electron-screenshots-tool": "^1.1.2",
"electron-squirrel-startup": "^1.0.0",
"electron-updater": "^6.1.4",
"fs-extra": "^10.1.0",
"electron-updater": "^6.1.8",
"fs-extra": "^11.2.0",
"pdf-lib": "^1.17.1",
"request": "^2.88.2"
},
@@ -67,6 +68,7 @@
"output": "dist"
},
"files": [
"render/**/*",
"public/**/*",
"electron-menu.js",
"electron-preload.js",

View File

@@ -0,0 +1,269 @@
:root {
--tab-font-family: -apple-system, 'Segoe UI', roboto, oxygen-sans, ubuntu, cantarell, 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
--tab-font-size: 12px;
--tab-transition: background-color 200ms ease-out, color 200ms ease-out;
--tab-cursor: pointer; /* 设置鼠标指针为手型 */
--tab-color: #7f8792;
--tab-background: #EFF0F4;
--tab-active-color: #222529;
--tab-active-background: #FFFFFF;
--tab-close-color: #9DA3AC;
}
* {
margin: 0;
padding: 0;
}
html, body {
margin: 0;
padding: 0;
font-family: 'Roboto', sans-serif;
font-size: 16px;
color: #333;
}
.nav {
font-family: var(--tab-font-family);
font-feature-settings: 'clig', 'kern';
display: flex;
width: 100%;
cursor: default;
background-color: var(--tab-background);
-webkit-app-region: drag;
}
.nav ul {
display: flex;
height: 30px;
margin: 8px 46px 0 0;
user-select: none;
overflow-x: auto;
overflow-y: hidden;
}
.nav ul::-webkit-scrollbar {
display: none;
}
.nav ul li {
display: inline-flex;
position: relative;
box-sizing: border-box;
align-items: center;
height: 100%;
padding: 7px 8px;
margin: 0 8px 0 0;
min-width: 100px;
max-width: 240px;
scroll-margin: 12px;
font-size: var(--tab-font-size);
color: var(--tab-color);
cursor: var(--tab-cursor);
transition: var(--tab-transition);
-webkit-app-region: none;
}
.nav ul li:first-child {
margin-left: 8px;
border-left: none;
}
.nav ul li.active {
color: var(--tab-active-color);
background: var(--tab-active-background);
border-radius: 6px 6px 0 0;
}
.nav ul li.active::before {
position: absolute;
bottom: 0;
left: -6px;
width: 6px;
height: 6px;
background-image: url(../image/select_left.png);
background-repeat: no-repeat;
background-size: cover;
content: '';
}
.nav ul li.active::after {
position: absolute;
right: -6px;
bottom: 0;
width: 6px;
height: 6px;
background-image: url(../image/select_right.png);
background-repeat: no-repeat;
background-size: cover;
content: '';
}
.nav ul li.active .tab-icon.background {
background-image: url(../image/link_normal_selected_icon.png);
}
.nav ul li:not(.active)::after {
position: absolute;
right: 0;
width: 1px;
height: 16px;
background: rgba(0, 0, 0, 0.08);
content: '';
}
.nav ul li:not(.active):last-child::after {
content: none;
}
/* 浏览器打开 */
.browser {
position: absolute;
top: 0;
right: 0;
display: flex;
align-items: center;
height: 38px;
padding: 0 14px;
cursor: pointer;
-webkit-app-region: none;
}
.browser span {
display: inline-block;
width: 18px;
height: 18px;
background-size: cover;
background-image: url(../image/link_normal_selected_icon.png);
}
/* 图标 */
.tab-icon {
display: inline-block;
flex-shrink: 0;
width: 18px;
height: 18px;
background-size: cover;
}
.tab-icon.background {
background-image: url(../image/link_normal_icon.png);
}
.tab-icon.loading {
background-image: none !important;
}
.tab-icon .tab-icon-loading {
width: 18px;
height: 18px;
border: 2px solid #eeeeee;
border-bottom-color: #84C56A;
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
animation: spin 0.75s linear infinite;
}
.tab-icon:not(.loading) .tab-icon-loading {
display: none;
}
.tab-icon img {
width: 16px;
height: 16px;
border-radius: 4px;
}
@keyframes spin {
0% {
transform: scale(0.8) rotate(0deg);
}
100% {
transform: scale(0.8) rotate(360deg);
}
}
/* 标题 */
.tab-title {
display: inline-block;
flex: 1;
margin-right: 8px;
margin-left: 6px;
overflow: hidden;
line-height: 150%;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 关闭 */
.tab-close {
display: inline-block;
width: 14px;
height: 14px;
margin-right: 2px;
position: relative;
}
.tab-close::after,
.tab-close::before {
position: absolute;
top: 50%;
right: 50%;
transform: translate(50%, -50%) scale(0.9) rotate(45deg);
content: "";
width: 2px;
height: 11px;
border-radius: 3px;
background-color: var(--tab-close-color);
}
.tab-close::before {
transform: translate(50%, -50%) scale(0.9) rotate(-45deg);
}
/* 不同平台样式 */
body.win32 .nav ul {
margin-left: 8px;
margin-right: 186px;
}
body.win32 .browser {
right: 140px;
}
body.darwin .nav ul {
margin-left: 76px;
}
body.darwin.full-screen .nav ul {
margin-left: 8px;
}
/* 暗黑模式 */
@media (prefers-color-scheme: dark) {
:root {
--tab-color: #C5C5C5;
--tab-background: #3B3B3D;
--tab-active-color: #E1E1E1;
--tab-active-background: #575757;
--tab-close-color: #E3E3E3;
}
.nav ul li.active::before {
background-image: url(../image/dark/select_left.png);
}
.nav ul li.active::after {
background-image: url(../image/dark/select_right.png);
}
.nav ul li.active .tab-icon.background {
background-image: url(../image/dark/link_normal_selected_icon.png);
}
.browser span {
background-image: url(../image/dark/link_normal_selected_icon.png);
}
.tab-icon.background {
background-image: url(../image/dark/link_normal_icon.png);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Untitled</title>
<link rel="stylesheet" href="./assets/css/style.css">
<script src="./assets/js/vue.global.min.js"></script>
</head>
<body>
<div id="app" class="app">
<div class="nav">
<ul>
<li v-for="item in tabs" :data-id="item.id" :class="{active: activeId === item.id}" @click="onSwitch(item)">
<div v-if="item.state === 'loading'" class="tab-icon loading">
<div class="tab-icon-loading"></div>
</div>
<div v-else class="tab-icon background" :style="iconStyle(item)"></div>
<div class="tab-title" :title="item.title">{{tabTitle(item)}}</div>
<div class="tab-close" @click.stop="onClose(item)"></div>
</li>
</ul>
</div>
<div v-if="tabs.length > 0" class="browser" @click="onBrowser">
<span></span>
</div>
</div>
<script>
const App = {
data() {
return {
activeId: 0,
tabs: [],
stopTimer: null,
}
},
beforeCreate() {
document.body.classList.add(window.process.platform)
},
mounted() {
window.__onDispatchEvent = (detail) => {
const {id, event} = detail
switch (event) {
case 'create':
this.tabs.push(Object.assign({
id,
title: '',
url: '',
icon: '',
state: 'loading'
}, detail))
break
case 'close':
const closeIndex = this.tabs.findIndex(item => item.id === id)
if (closeIndex > -1) {
this.tabs.splice(closeIndex, 1)
}
break
case 'switch':
this.activeId = id
this.scrollTabActive()
break
case 'title':
if (["HitoseaTask", "DooTask", "about:blank"].includes(detail.title)) {
return
}
const titleItem = this.tabs.find(item => item.id === id)
if (titleItem) {
titleItem.title = detail.title
titleItem.url = detail.url
}
break
case 'favicon':
const faviconItem = this.tabs.find(item => item.id === id)
if (faviconItem) {
faviconItem.icon = detail.favicons[detail.favicons.length - 1]
}
break
case 'start-loading':
const startItem = this.tabs.find(item => item.id === id)
if (startItem) {
this.stopTimer && clearTimeout(this.stopTimer)
startItem.state = 'loading'
}
break
case 'stop-loading':
this.stopTimer = setTimeout(_ => {
const stopItem = this.tabs.find(item => item.id === id)
if (stopItem) {
stopItem.state = 'loaded'
}
}, 300)
break
case 'enter-full-screen':
document.body.classList.add('full-screen')
break
case 'leave-full-screen':
document.body.classList.remove('full-screen')
break
}
}
window.__openDevTools = () => {
this.sendMessage('webTabOpenDevTools')
}
},
computed: {
pageTitle() {
const activeItem = this.tabs.find(item => item.id === this.activeId)
return activeItem ? activeItem.title : 'Untitled'
}
},
watch: {
pageTitle(title) {
document.title = title;
}
},
methods: {
onSwitch(item) {
this.sendMessage('webTabActivate', item.id)
},
onClose(item) {
this.sendMessage('webTabClose', item.id);
},
onBrowser() {
this.sendMessage('webTabExternal')
},
iconStyle(item) {
return item.icon ? `background-image: url(${item.icon})` : ''
},
tabTitle(item) {
if (item.title) {
return item.title
}
if (item.state === 'loading') {
return 'Loading...'
}
if (item.url) {
return `${item.url}`.replace(/^https*:\/\//, '')
}
},
scrollTabActive() {
setTimeout(() => {
try {
const child = document.querySelector(`.nav ul li[data-id=${this.activeId}]`)
if (child) {
child.scrollIntoView({behavior: 'smooth', block: 'nearest'})
}
} catch (e) {
//
}
}, 0)
},
sendMessage(event, args) {
electron?.sendMessage(event, args)
}
},
}
Vue.createApp(App).mount('#app')
</script>
</body>
</html>

49
electron/utils.js vendored
View File

@@ -313,6 +313,42 @@ module.exports = {
})
},
/**
* 新窗口打开事件
* @param webContents
* @param url
* @returns {Promise<unknown>}
*/
onBeforeOpenWindow(webContents, url) {
return new Promise(resolve => {
const dataStr = JSON.stringify({url: url})
webContents.executeJavaScript(`if(typeof window.__onBeforeOpenWindow === 'function'){window.__onBeforeOpenWindow(${dataStr})}`, true).then(options => {
if (options !== true) {
resolve()
}
}).catch(_ => {
resolve()
})
})
},
/**
* 分发事件
* @param webContents
* @param data
* @returns {Promise<unknown>}
*/
onDispatchEvent(webContents, data) {
return new Promise(resolve => {
const dataStr = JSON.stringify(data)
webContents.executeJavaScript(`window.__onDispatchEvent(${dataStr})`, true).then(options => {
resolve(options)
}).catch(_ => {
resolve()
})
})
},
/**
* 版本比较
* @param version1
@@ -385,5 +421,18 @@ module.exports = {
}
callback({responseHeaders: details.responseHeaders});
});
},
/**
* win mac meta control
* @param input
* @returns {boolean | Point | HTMLElement}
*/
isMetaOrControl(input) {
if (process.platform === 'win32') {
return input.control
} else {
return input.meta
}
}
}

View File

@@ -479,4 +479,15 @@ Api接口文档
文件总大小已超过1GB请分批下载
保存任务详情至文件失败
保存任务详情至文件失败,请重试
移动成功
移动成功
不能重复投票
系统机器人不存在
个人会话禁言
个人群组禁言
点击下载
置顶会话无法隐藏

View File

@@ -14,7 +14,7 @@
如果必填,发送聊天前必须设置昵称、电话。
开启同步本地帐号登录后将同步到 LDAP 服务器
必填:发送聊天内容前必须设置昵称、电话。
进入路由器终端执行以下命令即可完成安装
进入路由器终端执行以下命令即可完成安装
设备连接上指定路由器WiFi后自动签到。
切换语言需要刷新后生效,是否确定刷新?
详细描述,选填...(点击右键使用工具栏)
@@ -58,7 +58,7 @@
密码错误,请输入正确密码
开放:所有人都可以发言。
强大易用的协同创作云文档
注销前,请确认一下事项
注销前,请确认一下事项
签到前后时间收到消息通知
签到提醒对象3天内有签到的成员法定工作日
该状态下任务自动标记完成
@@ -146,7 +146,7 @@
选择转发最近聊天
邮箱、昵称、职位
请输入会议频道ID
第二次任务提醒:
第二次任务提醒
请输入职位/职称
不在成员列表内
仅支持网页版。
@@ -364,7 +364,7 @@ html转markdown
验证码错误
会议频道ID
SMTP服务器
任务提醒:
任务提醒
职位/职称
验收/测试
上传图片
@@ -461,7 +461,7 @@ SMTP服务器
导出成员
尚未签到
工作报告
开启后
开启后
开启推送
开启通知
开始会议
@@ -642,7 +642,6 @@ LDAP 端口
MD编辑器
Word 文档
导出XLSX
APP推送
MAC地址
扫一扫
上个月
@@ -1054,7 +1053,8 @@ ID、任务名...
语音
群头像
修改头像
没有任何与(*)相关的会话
没有任何联系人
没有任何与(*)相关的结果
未读消息(*)条
帐号相关
项目相关
@@ -1106,7 +1106,8 @@ html转markdown
是否将消息中的网络图片保存到本地服务器。
保存网络图片
无声发送
Markdown 格式发送
MD 格式发送
正常发送
编辑消息
发送失败
再次编辑
@@ -1250,8 +1251,9 @@ Markdown 格式发送
退出
会议组件加载失败!
OKR管理
OKR结果分析
OKR 管理
OKR 结果
OKR 结果分析
计划时间冲突提示
忽略并继续
你确定要清除缓存吗?
@@ -1271,10 +1273,10 @@ OKR结果分析
审批类型
导出类型
未完成
AI机器人
AI 机器人
任务相关
请填写名称!
访问OpenAI网站查看
访问OpenAI网站查看
使用代理
支持 http 或 socks 代理
例如http://proxy.com 或 socks5://proxy.com
@@ -1354,21 +1356,25 @@ AI机器人
应用
机器人设置
聊天
开始聊天
返回
会议设置
我是一个人工智能助手,为用户提供问题解答和指导。我没有具体的身份,只是一个程序。您有什么问题可以问我哦?
我是由Google开发的生成式人工智能聊天机器人。
它基于同名的Gemini系列大型语言模型。
是应对OpenAI公司开发的ChatGPT聊天机器人的崛起而开发的。
我是Claude,一个由Anthropic公司创造出来的AI助手机器人。我的工作是帮助人类,与人对话并给出解答。
我是文心一言英文名是ERNIE Bot。我能够与人对话互动回答问题协助创作高效便捷地帮助人们获取信息、知识和灵感。
我是达摩院自主研发的超大规模语言模型,能够回答问题、创作文字,还能表达观点、撰写代码。
机器人暂未开启
创建一个全新的会议视频会议,与会者可以在实时中进行面对面的视听交流。通过视频会议平台,参与者可以分享屏幕、共享文档,并与其他与会人员进行讨论和协。
创建一个全新的会议视频会议,与会者可以在实时中进行面对面的视听交流。
通过视频会议平台,参与者可以分享屏幕、共享文档,并与其他与会人员进行讨论和协。
加入视频会议,参与已经创建的会议,在会议过程中与其他参会人员进行远程实时视听交流和协作。
新会议
新建会议
LDAP设置
LDAP 设置
邮件管理
APP推送
APP 推送
流程设置
邮件
签到
@@ -1390,8 +1396,8 @@ APP推送
包含消息发送的文件
特殊设置
打包列表
使用独立的发送按钮
开启后,键盘上的发送按钮会被替换成换行
发送按钮
开启后,发送消息时键盘上的发送按钮会被替换成换行
仅我的
未领任务提醒
开启后每天按设定的提醒时间在项目群聊中发送未领取任务通知。
@@ -1400,3 +1406,153 @@ APP推送
请输入时长
必须大于0
忍心拒绝
邮件通知
移动任务
请输入备注
任务延期
修改时间
请确认扫码的服务器与当前服务器一致
二维码服务器
当前服务器
你确定取消待办吗?
取消成功
请等待打包完成
选择一个项目查看更多任务
首页
无相关数据
当前环境
权限设置
任务列权限
添加列
修改列
删除列
排序列
任务权限
修改任务
修改状态
移动任务
任务协助人
搜索项目名称
服务器版本过低,请升级服务器。
不显示原发送者信息
转发给
留言
多选
@我的
移动前
移动后
状态
协助人
未变更移动项
接龙
参与接龙
发起接龙
发起接龙,参与接龙目前共(*)人
请输入接龙主题
请输入接龙内容
可填写接龙格式
重复内容将不再计入接龙结果
返回编辑
继续发送
接龙结果
选择群组发起接龙
来自
发起投票
投票结果
发起
请输入投票主题
请输入选项内容
允许多选
匿名投票
投票
匿名
实名
单选
多选
请选择后投票
立即投票
再次发送
再次发送投票?
结束投票
确定结束投票?
已发送
选择群组发起投票
以下为新消息
@我
签到打卡
在线会议
群接龙
群投票
LDAP
License Key
你确定要打包下载【(*)】文件夹吗?
正在打包,请留意系统消息。
发起,参与接龙目前共(*)人
选择会员
请选择移动后状态
移动成功
置顶人员
置顶了
你确定取消置顶吗?
打包下载(*)
数据已超过(*)条,是否继续加载?
版本过低
服务器((*))接口版本过低,部分功能可能无法正常使用。
禁言
私聊禁言
群聊禁言
默认不限制
开放:所有人都可以在全员群组发言。
开放:所有人都可以相互发起个人聊天。
开放:允许个人群组聊天发言。
禁言:除管理员外所有人都禁止在全员群组发言。
禁言:除管理员外所有人都禁止发起个人聊天。
除管理员外禁止个人群组聊天发言。
注意,仅禁止个人群组,其他类型的群组不禁止,比如:部门群聊、项目群聊等系统群聊。
浏览器打开
离最新版本只有一步之遥了!重新启动应用即可完成更新。
重新启动
欢迎使用本软件!
在您使用本软件前,请您认真阅读并了解相应的
以了解我们的服务内容和您相关个人信息的处理规则。
我们将严格的按照隐私服务协议为您提供服务,保护您的个人信息。
服务器((*))版本过低
不显示该会话
昨天
文件已存在
文件(*)已存在,是否替换?
保留两者
替换
端到端加密
关闭端到端加密传输数据。
等(*)个文件
你确定要删除【(*)】等2个文件吗
审批详情
天前
月前
请输入用户名
是否拨打电话给(*)
你确定要取消发送吗?
消息已发送,不可取消
取消发送失败

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,11 @@
{
"name": "DooTask",
"version": "0.32.9",
"version": "0.36.26",
"description": "DooTask is task management system.",
"scripts": {
"start": "./cmd dev",
"build": "./cmd prod",
"version": "node ./version.js",
"version": "node ./bin/version.js",
"translate": "./cmd translate"
},
"author": {
@@ -26,7 +26,7 @@
"css-loader": "^6.7.2",
"dexie": "^3.2.3",
"echarts": "^5.2.2",
"element-sea": "^2.15.10-6",
"element-sea": "^2.15.10-7",
"file-loader": "^6.2.0",
"highlight.js": "^11.7.0",
"inquirer": "^8.2.0",
@@ -41,21 +41,21 @@
"markdown-it": "^13.0.1",
"markdown-it-link-attributes": "^4.0.1",
"moment": "^2.29.1",
"node-sass": "^6.0.1",
"node-sass": "^9.0.0",
"notification-koro1": "^1.1.1",
"openpgp_hi": "^5.7.0-1",
"photoswipe": "^5.2.8",
"postcss": "^8.4.5",
"quill": "^1.3.7",
"quill-mention-hi": "^3.1.0-1",
"quill-mention-hi": "^3.1.0-4",
"resolve-url-loader": "^4.0.0",
"sass": "~1.32.13",
"sass-loader": "^12.6.0",
"sass": "^1.71.1",
"sass-loader": "^14.1.1",
"stylus": "^0.59.0",
"stylus-loader": "^7.1.0",
"tinymce": "^5.10.3",
"tui-calendar-hi": "^1.15.1-5",
"view-design-hi": "^4.7.0-48",
"view-design-hi": "^4.7.0-50",
"vite": "^2.9.15",
"vite-plugin-file-copy": "^1.0.0",
"vite-plugin-require": "^1.1.10",
@@ -68,7 +68,7 @@
"vue-resize-observer": "^2.0.16",
"vue-router": "^3.6.5",
"vue-template-compiler": "~2.6.14",
"vue-virtual-scroll-list-hi": "^2.3.5-3",
"vue-virtual-scroll-list-hi": "^2.3.5-10",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.2"
},

10888
public/docs/api_data.js vendored Normal file

File diff suppressed because it is too large Load Diff

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