Compare commits

...

1968 Commits

Author SHA1 Message Date
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
weifashi
08cc4a4815 build 2023-11-17 19:23:01 +08:00
weifashi
aa554627fb perf: office只读模式,隐藏下载按钮 2023-11-17 19:01:42 +08:00
OldTT
420e1a9d63 提交功能对比清单 2023-11-17 17:09:16 +08:00
weifashi
b8ed8566ee fix: 移动打开分享链接时先关闭聊天窗口 2023-11-17 15:24:53 +08:00
weifashi
bbb3cee927 feat: 邀请加入项目的链接客户端直接打开 2023-11-17 15:00:15 +08:00
ganzizi
2737fa4697 feat: 新增临时压缩下载文件24小时自动清理 2023-11-17 11:56:22 +08:00
weifashi
14913ae312 fix: 修复未登录时,进入邀请项目的链接会弹出多次登录框的情况 2023-11-17 11:14:55 +08:00
weifashi
d2fe6217a6 feat: 任务加时功能 (模仿可见性的交互,任务延期和修改时间) - 100% 2023-11-17 11:04:10 +08:00
ganzizi
aef46d4c76 fix: 修复进度完成后再次完成 2023-11-17 10:23:04 +08:00
weifashi
ad494b86e3 feat: 任务加时功能 (模仿可见性的交互,任务延期和修改时间) - 70% 2023-11-16 19:22:22 +08:00
ganzizi
c0a594655d perf: 优化实现文件夹下载以及多文件压缩下载功能 2023-11-16 17:12:12 +08:00
ganzizi
ed4afa63f0 perf: 优化实现文件夹下载以及多文件压缩下载功能 2023-11-16 17:01:49 +08:00
weifashi
08fccf4adc perf: 机器人设置页面,点点点看不到内容,需要给弹窗看详细内容 2023-11-16 11:39:17 +08:00
weifashi
a52e9e152d feat: 添加功能 - 每天发出本项目未领取任务 2023-11-16 11:09:57 +08:00
weifashi
38befc94ca perf: 文件选中后,移动端页面宽度放不下对应内容 2023-11-15 17:19:18 +08:00
weifashi
df4c8cc352 perf: 文件选中后,移动端页面宽度放不下对应内容,没有滚动条 2023-11-15 17:13:03 +08:00
weifashi
2305d30d35 feat: 添加转移任务到别的项目功能 2023-11-15 16:28:19 +08:00
weifashi
cdc7e671ce feat: 键盘设置里面,支持设置回车发送还是按键发送 2023-11-15 11:19:50 +08:00
weifashi
d541397594 feat: 上传文件大小限制 2023-11-14 18:17:53 +08:00
weifashi
4205b68b8c perf: 压缩下载改名打包下载 2023-11-14 11:39:14 +08:00
weifashi
6ffb458ffc perf: 复制链接去除主题语言参数 2023-11-14 09:49:52 +08:00
kuaifan
e43dd53e4f fix: 华为手机弹出键盘时出现空白区域的问题 2023-11-13 22:05:38 +08:00
weifashi
5ef4516e3d feat: 任务标题有换行时聊天引用会失败 2023-11-13 18:31:23 +08:00
ganzizi
51b1d78d8a perf: 优化实现文件夹下载以及多文件压缩下载功能 2023-11-13 17:21:26 +08:00
weifashi
3fdcbf92b6 feat: 文件共享只读禁止下载文件 2023-11-13 17:13:17 +08:00
weifashi
742875d5eb feat: 保存任务详情至文件的方法 添加失败日志 2023-11-13 15:43:59 +08:00
weifashi
4a79eeb9df feat: 翻译 2023-11-13 15:15:17 +08:00
ganzizi
43cee0eb4a perf: 优化实现文件夹下载以及多文件压缩下载功能 2023-11-13 15:00:53 +08:00
weifashi
92beb20455 feat: 翻译 2023-11-13 14:25:14 +08:00
ganzizi
930f55b080 feat: 新增实现文件夹下载以及多文件压缩下载功能 2023-11-10 19:34:36 +08:00
weifashi
e7239b3c5c fix: 修复错误提示 2023-11-10 19:23:31 +08:00
weifashi
1e92ca5518 fix: 栏目被删除的时候,已归档任务不受影响 2023-11-10 19:17:36 +08:00
weifashi
c9e3e88443 feat: 任务可以筛选未设置时间的 2023-11-10 15:08:08 +08:00
weifashi
05f20eb761 feat: 临时账号可以主动跟机器人聊天 2023-11-10 14:11:45 +08:00
weifashi
a9033c610b fix: 直接拖文件会发送两次的bug修复 2023-11-10 11:30:48 +08:00
kuaifan
e9e1cd9028 build 2023-11-09 00:45:03 +08:00
kuaifan
4e8239377f fix: 思维导图缩放后无法触发手指移动 2023-11-09 00:38:16 +08:00
kuaifan
83a501b69f perf: 新增文心一言模型 2023-11-09 00:38:16 +08:00
kuaifan
efe80ce0eb perf: 补充优化 2023-11-09 00:38:16 +08:00
kuaifan
e0fc1c5ef7 perf: update documentserver 2023-11-09 00:38:16 +08:00
weifashi
a38ddf83f2 fix:1.优化字段is_all_visible,2.更新okr版本,3.修复删除任务列报错 2023-11-09 00:36:49 +08:00
weifashi
e7fbe8bb49 feat: 升级 office 到 7.5 版本 2023-11-08 18:37:08 +08:00
kuaifan
523166da48 build 2023-11-07 10:56:14 +08:00
kuaifan
a24990e06a perf: Openid supports gpt-4 model 2023-11-07 10:40:26 +08:00
weifashi
3f11451f3c fix:文件下载功能报500修复 2023-11-01 10:06:56 +08:00
kuaifan
0b888b9597 build 2023-11-01 00:51:01 +08:00
kuaifan
acddde8faa fix: 手机下载文件名出现html的情况 2023-11-01 00:13:24 +08:00
kuaifan
c587a50505 perf: 标记已读/未读等待效果 2023-10-31 23:34:03 +08:00
kuaifan
7befd4277e perf: app内转发没有等待效果 2023-10-31 23:28:47 +08:00
kuaifan
bbc29d5d5f fix: 桌面客户端内聊天复制图片模糊的情况 2023-10-31 23:23:30 +08:00
kuaifan
9dc7c08b61 fix: 桌面客户端截图功能 2023-10-31 23:21:48 +08:00
weifashi
0f690a99df fix:electron-screenshots-tool 最新版本不兼容,固定为1.0.4 2023-10-23 14:51:04 +08:00
weifashi
d8c8cca6fc fix:electron-screenshots-tool 最新版本不兼容,固定为1.0.4 2023-10-23 14:48:19 +08:00
weifashi
9bccc99228 build 2023-10-23 11:47:57 +08:00
weifashi
4f1016a939 feat:重置版本 2023-10-23 09:44:23 +08:00
weifashi
71fb50b4ce build:编译资源上传 2023-10-20 18:43:44 +08:00
weifashi
7359b04001 build:编译资源上传 2023-10-20 18:36:14 +08:00
weifashi
5700b97e98 fix:修复任务详情页面 - 有pre内容的时候 样式错落的问题 2023-10-20 18:24:43 +08:00
weifashi
19e50be276 fix:
1. 项目的栏的任务列表过多时,拖动排序时报错,原因是请求的url过长,已经超过浏览器的url最大长度了
2. 修复项目列表,文本错位
2023-10-18 15:23:57 +08:00
weifashi
14595d3a60 fix:修复报错信息 2023-10-17 14:45:19 +08:00
weifashi
eea9d1f3ce feat:兼容安卓手机,键盘唤起时会把底部导航往上推的bug 2023-10-07 14:18:34 +08:00
weifashi
157147ded7 feat:更新审批中心容器 2023-09-28 11:06:29 +08:00
weifashi
b8d51c6462 feat:添加关键信息日志 2023-09-27 17:20:10 +08:00
ganzizi
7fe3436819 perf: 优化审批信息推送内容(时间、事由) 2023-09-18 11:19:29 +08:00
weifashi
a2cd57641b feat:更新okr 2023-09-18 09:38:15 +08:00
weifashi
80bfc2c86f fix:用户状态- 过滤机器人 2023-09-17 23:24:44 +08:00
weifashi
e3bd895908 fix:审批中心 样式修复 2023-09-17 23:21:12 +08:00
weifashi
018ff205f5 build:编译资源上传 2023-09-15 19:19:11 +08:00
weifashi
9fee5c61a6 fix:修复bug - 获取用户审批状态 2023-09-15 18:57:00 +08:00
weifashi
ba8e7b3fbb feat:更新okr版本 2023-09-15 18:15:43 +08:00
weifashi
dbe53481f1 Merge branch 'kuaifan-pro' into pro 2023-09-15 17:09:26 +08:00
weifashi
8b9cfbcf29 feat:更新okr容器版本 2023-09-15 17:03:36 +08:00
weifashi
db67d662d1 feat:1.okr全局状态管理窗口的隐藏显示 2.聊天列表只缓存25条记录 3.审批中心-从设置返回取消动画 2023-09-15 16:43:24 +08:00
kuaifan
dcf947c498 no message 2023-09-15 10:31:43 +08:00
kuaifan
c94025013b build 2023-09-15 10:19:39 +08:00
weifashi
304b22b1c1 feat:扫一扫按钮显示优化 2023-09-14 18:15:40 +08:00
weifashi
f23ece5e9e feat:审批中心前端优化 2023-09-13 22:21:12 +08:00
weifashi
651194f12e feat:审批中心前端优化 2023-09-13 22:02:55 +08:00
weifashi
ae00f49b30 feat:审批中心前端优化 2023-09-13 22:00:49 +08:00
weifashi
60f5341a9a feat:okr更新 - 修复时间格式 2023-09-13 17:53:40 +08:00
weifashi
3b17ebcb09 feat:okr - 窗口 - 添加复合快捷键 2023-09-13 09:30:49 +08:00
weifashi
c861f49e29 feat:审批中心-逻辑优化 2023-09-12 14:50:59 +08:00
weifashi
1d2f50c896 fix:修复okr路由,修复okr卸载聊天组件的时候非常缓慢的问题 2023-09-11 18:39:12 +08:00
weifashi
4f676c0ccb feat:1. Okr 添加esc关闭窗口 2. Okr 页面返回错误修复 3. 修复安全漏洞-发送消息接口返回的响应数据,暴露了数据库信息 2023-09-11 16:09:49 +08:00
weifashi
5801d0fc14 feat:1.应用页面优化 2.审批中心优化 2023-09-08 19:57:21 +08:00
ganzizi
525ac41258 feat: 新增用户审批状态显示 2023-09-08 19:49:39 +08:00
ganzizi
39a0d001ef feat: 1 新增用户审批状态接口 2 新增审批用户查询条件 2023-09-08 19:14:31 +08:00
weifashi
0e9c1c19ce feat:应用页面 样式优化 2023-09-08 18:46:48 +08:00
weifashi
0011e04823 feat:应用页面去掉边框 2023-09-08 15:05:14 +08:00
weifashi
e919170ee8 feat:更新okr版本 2023-09-08 12:39:52 +08:00
weifashi
8024edc1b0 fix:工作流设置 - 太多的时候显示不全的bug修复 2023-09-08 00:03:00 +08:00
weifashi
984c68dc09 feat:适配移动端也可以编辑文件(.md ,.text) 2023-09-07 15:22:35 +08:00
kuaifan
c35a177ac1 fix: ai return error 2023-09-06 20:47:45 +08:00
kuaifan
ea82e2dbfe Merge commit '034bd7dcb81987368d9b8e7fd5e1ac1a8d9734f4' into pro 2023-09-06 20:46:30 +08:00
weifashi
034bd7dcb8 feat:更改官网备案号 2023-09-06 18:26:37 +08:00
ganzizi
343905362c fix: 修复审批数据导出时间以请假时间为准 2023-09-06 17:14:24 +08:00
spylecym
79d5b70364 fix: 修复服务价格页面文字 2023-09-06 15:12:06 +08:00
Pang
11a2fcf1f4 build 2023-09-05 20:32:00 +08:00
Pang
7c16f9f134 Merge commit 'fb0ef19158b6b035cf80fb1ac9564b0bdf2f3cc9' into pro 2023-09-05 19:41:58 +08:00
weifashi
fb0ef19158 fix:会议用户id恢复为之前的占位数,解决php更新了,app客户端没更新信息错误的bug 2023-09-05 15:03:31 +08:00
weifashi
3c7b7e021f fix:修复表情包箭头不显示的bug 2023-09-05 10:27:45 +08:00
weifashi
a38fa4625f fix:修复okr路由解析错误 2023-09-04 18:37:14 +08:00
kuaifan
46376121d0 build 2023-09-03 23:37:56 +09:00
weifashi
7b2b026bad style:调整代码格式 2023-09-01 17:52:04 +08:00
weifashi
9796e104a5 Merge branch 'kuaifan-pro' into pro 2023-09-01 17:42:17 +08:00
weifashi
0bf3020db7 fix:1. okr性能问题修复 2. 解决okr路由自动加上#/ 3.okr日期选择快速 4.okr其他bug修复 5.加强会议功能uid生成逻辑 6.okr打不开修复 7. 应用页面字体大小和布局调整 2023-09-01 17:37:24 +08:00
kuaifan
12e8f81a58 build 2023-09-01 01:08:22 +09:00
kuaifan
f073e61965 merge optimization 2023-09-01 00:47:54 +09:00
kuaifan
85e6066292 Merge commit 'f57d3cf02c50d8555471ec65091caef1dbdd4cc1' into pro 2023-08-31 23:57:27 +09:00
weifashi
f57d3cf02c feat:强化分享链接 - 适配手机端 2023-08-31 10:17:05 +08:00
weifashi
aa03c00dd5 feat:会议分享添加姓名验证 2023-08-31 09:47:49 +08:00
weifashi
1a3c4640ec feat: 会议邀请改为由机器人发送 2023-08-30 21:58:46 +08:00
weifashi
29d8396910 fix:样式整理 2023-08-30 18:27:44 +08:00
weifashi
d1e38910ef Merge branch 'wfs-0829' into pro 2023-08-30 18:23:34 +08:00
weifashi
0c4abb5db3 feat:添加会议分享功能 - 100% 2023-08-30 18:21:37 +08:00
weifashi
6f7b44cb08 feat:1. 更新 docker-compose 2. uodate 指令添加nginx重启功能 2023-08-30 10:54:16 +08:00
weifashi
3a2c40a43e feat:会议可分享 - 60% 2023-08-29 17:09:32 +08:00
weifashi
288e265aaa init:1. 添加清空数据库的指令 2. 修复一些前端控制台报错 2023-08-29 11:51:46 +08:00
weifashi
42dec0464e feat:审批优化 2023-08-24 16:05:59 +08:00
weifashi
5b09a111cd feat:审批优化 2023-08-24 15:43:13 +08:00
weifashi
769e2b0223 feat:审批中心 - 文案修改 2023-08-24 09:50:16 +08:00
weifashi
1fed482025 feat:更新审批容器版本 - 解决流程列表排序问题 2023-08-23 10:54:37 +08:00
weifashi
46705dd55f feat:聊天表情包 - 添加左右滚动条 2023-08-22 16:39:09 +08:00
weifashi
c11f946979 perf:代码优化 2023-08-22 02:06:36 +08:00
weifashi
e928ff1fce fix:1.修复任务详情英文不换行, 2.分享文件皮肤跟语言统一 ,3.聊天未读数修复 2023-08-21 20:42:45 +08:00
kuaifan
333d4517b5 perf: 优化滚动条导致页面抖动的情况 2023-08-19 15:29:07 +08:00
weifashi
d64d06a70a feat:更新okr版本 2023-08-19 01:12:44 +08:00
weifashi
39834f507c feat:更新容器okr版本 2023-08-19 01:06:27 +08:00
weifashi
5199609d54 feat:微应用 - 置入时间组件 2023-08-18 19:10:27 +08:00
weifashi
53a5a33fa1 feat:更新容器版本 2023-08-18 16:47:01 +08:00
weifashi
a397908bd4 fix:修复结果分析命名 2023-08-18 09:59:18 +08:00
weifashi
bd9e0bba9c fix:改文件名 2023-08-18 09:55:54 +08:00
weifashi
7179efd3bd fix:改文件名 2023-08-18 09:37:10 +08:00
weifashi
de0bca2076 perf:代码优化 2023-08-18 00:19:53 +08:00
weifashi
5f3f350e1b perf:代码优化 2023-08-18 00:11:49 +08:00
weifashi
7eeacc59db fix:修复扫一扫功能 2023-08-17 23:57:57 +08:00
weifashi
d6d96d2d2b perf:代码优化 2023-08-17 23:40:27 +08:00
weifashi
bb39b7db89 feat:样式强化 2023-08-17 19:00:01 +08:00
weifashi
706fd0d588 feat:样式优化 2023-08-17 17:30:07 +08:00
weifashi
97fc99c5e4 feat:样式优化 2023-08-17 17:07:29 +08:00
weifashi
666767539d feat:按钮优化 2023-08-17 17:02:46 +08:00
weifashi
28234f64ec feat:样式优化 2023-08-17 16:42:54 +08:00
weifashi
80bede367d feat:样式集合优化 2023-08-17 15:55:16 +08:00
weifashi
6722051e82 feat:翻译与优化 2023-08-17 10:26:43 +08:00
weifashi
c9fc9916b2 feat:融合okr到应用入口 2023-08-17 09:56:47 +08:00
weifashi
9ff7160870 fix:修复合并错误 2023-08-17 09:34:48 +08:00
weifashi
9eec44249c Merge branch 'okr' of github.com:hitosea/dootask into okr 2023-08-17 09:30:02 +08:00
weifashi
146fb3321a Merge branch 'pro' into okr
# Conflicts:
#	app/Http/Controllers/Api/DialogController.php
#	app/Models/User.php
#	docker-compose.yml
#	docker/nginx/default.conf
#	language/original-web.txt
#	language/translate.json
#	public/docs/assets/main.bundle.js
#	public/docs/index.html
#	resources/assets/js/pages/manage.vue
#	resources/assets/js/pages/manage/components/ChatInput/index.vue
#	resources/assets/sass/pages/_.scss
2023-08-17 09:28:38 +08:00
ganzizi
05fcb5cb0c fix: OKR组新增/删除人员传输格式兼容 2023-08-17 09:12:00 +08:00
weifashi
09cf62cadd fix:修复设置页面 返回错误的bug 2023-08-16 18:43:35 +08:00
weifashi
632e113d63 feat:样式调优 2023-08-16 18:35:20 +08:00
weifashi
c4743d3b26 fix:修复文件页面 返回错误 2023-08-16 16:30:58 +08:00
weifashi
10960eec59 feat:优化应用 2023-08-16 15:28:32 +08:00
weifashi
b3c79be4e7 Merge branch 'wfs-tmp-apply' into pro 2023-08-16 15:24:45 +08:00
weifashi
efe6c99199 feat:整体窗口样式优化 2023-08-16 15:21:08 +08:00
weifashi
521a0dbec6 feat:添加应用 - 100% 2023-08-15 18:59:47 +08:00
weifashi
916997d92a feat:添加应用 - 100% 2023-08-15 18:35:01 +08:00
kuaifan
0af07058df build 2023-08-15 12:14:46 +08:00
kuaifan
84c98dd5c1 fix: 任务列表更新数量不正确的情况 2023-08-15 12:12:13 +08:00
spylecym
a229c12baf fix:修复移动端cookie弹框样式以及下载页面点击问题 2023-08-15 12:05:46 +08:00
Pang
e0ecd0ad0a no message 2023-08-15 09:57:09 +08:00
weifashi
96ca57509f feat:整合应用入口 - 65% 2023-08-14 18:41:27 +08:00
Pang
e9e090fdf6 build 2023-08-13 08:26:47 +08:00
Pang
6af0922542 perf: 优化菜单颜色选择 2023-08-13 08:19:39 +08:00
kuaifan
1bb400b7dc build 2023-08-11 18:43:51 +08:00
kuaifan
bbb4550f10 fix: 消息列表过大导致无法查看图片 2023-08-11 18:38:05 +08:00
kuaifan
96b2af9cc0 Merge commit 'af33f473b5d2d95c03f14e01443f667612105ea9' into pro 2023-08-11 18:13:57 +08:00
kuaifan
e47f601a51 fix: 桌面端drawio版本错误 2023-08-11 17:31:56 +08:00
spylecym
af33f473b5 fix:修复下载页面btn点击、新增主页cookie弹框阴影 2023-08-11 11:12:08 +08:00
Pang
c73f1c0061 build 2023-08-11 09:33:22 +08:00
Pang
b5d63dfd12 Merge Optimization 2023-08-11 09:13:19 +08:00
Pang
952836e1f1 Merge commit '4912f9746115e3fe24bc1621076d08d73c20b552' into pro
# Conflicts:
#	public/js/emoticon.all.js
2023-08-11 08:19:04 +08:00
weifashi
4912f97461 fix:修复子任务可见性字段为null的数据 2023-08-10 23:35:39 +08:00
weifashi
e2a0b7a033 fix:修复子任务可见性字段为null的数据 2023-08-10 23:23:45 +08:00
weifashi
0e12a08076 fix:任务列表接口 - 消息数量错误修复 2023-08-10 22:43:51 +08:00
weifashi
3ae3acf705 feat:1.会话列表右键添加标记颜色的选项 2023-08-10 22:14:47 +08:00
kuaifan
b8770433d7 build 2023-08-10 21:49:44 +08:00
kuaifan
7030148a76 no message 2023-08-10 21:15:16 +08:00
kuaifan
4ed09dddcf perf: 升级客户端框架 2023-08-10 21:14:42 +08:00
kuaifan
aba74681ef fix: 无法在任务新窗口打开引用的任务 2023-08-10 20:44:56 +08:00
kuaifan
c768395094 fix: 在任务新窗口使用显示文件窗口错误的情况 2023-08-10 20:18:42 +08:00
kuaifan
d7620bf4ff fix: 部分iOS系统按录音时页面闪烁的情况 2023-08-10 20:17:25 +08:00
weifashi
3a9adfa089 feat:测试环境打开mysql执行日志 2023-08-10 16:41:53 +08:00
weifashi
c8bc67e7bf feat:调整打开okr明细的方法 2023-08-10 16:34:31 +08:00
zzb-zzb
7c72003b91 fix:修改cookie协议页面的标题及点击cookie协议链接跳转新页面 2023-08-10 16:21:07 +08:00
zzb-zzb
6a4392266d fix:修改cookie协议内容中公司名称,公司地址 2023-08-10 16:04:31 +08:00
zzb-zzb
5d1b805b93 feat:添加中文、英文cookie协议 2023-08-10 15:54:18 +08:00
Pang
dffd860d5c perf: 优化工作汇报提交表单 2023-08-10 08:31:27 +08:00
Pang
8d8777ba95 perf: 优化确认框按钮样式 2023-08-10 08:05:31 +08:00
Pang
00e255a4a8 perf: 优化时间冲突提示框 2023-08-10 07:46:08 +08:00
Pang
bba40830fb fix: Claude 机器人返回内容错误的情况 2023-08-10 01:15:26 +08:00
Pang
a3974195c2 no message 2023-08-10 01:12:42 +08:00
Pang
7c4c26e86e perf: 文件页面弹出菜单时误操作优化 2023-08-10 01:12:33 +08:00
Pang
1be70fd8f2 fix: 在文件页面编辑文本时选择已传图片缩列图不显示的情况 2023-08-10 01:11:57 +08:00
Pang
84e3b22357 no message 2023-08-10 00:37:48 +08:00
kuaifan
a3c509da83 perf: 优化任务描述编辑器 2023-08-09 21:58:22 +08:00
weifashi
9944dc4693 fix:1. 修改消息时@成员不会拉进群 2. @的时候,会话以外成员排序更改为前5名为最近联系的人 3. 审批中心时间后面加周几 4.审批拒绝不能输入表情的bug修复 2023-08-09 18:33:00 +08:00
weifashi
5b1fc8af84 feat:引入环境变量 - 时区 2023-08-09 14:52:43 +08:00
kuaifan
ee708d1d1b no message 2023-08-09 11:42:59 +08:00
zzb-zzb
d0df50705a fix:添加cookie弹窗 2023-08-09 11:05:31 +08:00
weifashi
cb2b762a6f feat:审批容器 - 添加时区 2023-08-09 09:10:27 +08:00
Pang
3971c63dda perf: 优化表情快捷提示框 2023-08-09 00:42:45 +08:00
weifashi
3fe9c60480 feat:退出登录时,清空微应用缓存以便重新加载 2023-08-09 00:28:09 +08:00
Pang
00050c9c6b fix: 桌面客户端提示request错误 2023-08-08 23:17:19 +08:00
Pang
afc0fd78c8 build 2023-08-08 23:17:18 +08:00
Pang
ac46e1ca22 fix: 客户端无法保存网络文件的情况 2023-08-08 23:17:18 +08:00
kuaifan
9efdeb6b97 Merge commit 'c6b9dfff220858e5faee143f76ec1bc9a57ef4d5' into pro 2023-08-08 20:30:40 +08:00
kuaifan
30ac03e0cc no message 2023-08-08 19:51:03 +08:00
kuaifan
89001bec0a fix: 可以发送空白md消息的情况 2023-08-08 19:16:05 +08:00
weifashi
c6b9dfff22 feat:优化小海豚的关联字和图片 2023-08-08 17:33:23 +08:00
weifashi
b8aa32dcaa fix:修复可见性任务,子任务不显示的bug 2023-08-08 15:53:50 +08:00
kuaifan
c589d5e0a5 perf: 优化移动端编辑任务详情 2023-08-08 12:10:37 +08:00
weifashi
1b090c1c6e fix:添加代办时 实时显示 2023-08-07 00:18:08 +08:00
weifashi
40474319e3 feat:聊天列表头部添加代办标签 2023-08-06 23:47:36 +08:00
Pang
b79fdbd7e0 fix: 桌面客户端缺失文件 2023-08-06 22:27:51 +08:00
kuaifan
f3f65fa99e build 2023-08-06 19:29:49 +08:00
kuaifan
6a9408a8bc no message 2023-08-06 18:05:04 +08:00
kuaifan
2b88764c7e perf: 优化桌面端邮件图片菜单 2023-08-06 17:58:54 +08:00
kuaifan
4a75844c98 build 2023-08-06 14:26:59 +08:00
kuaifan
9d9a3ebe54 Merge commit '79b9d412c143c51dc165ed9c229dd5a06b999986' into pro
# Conflicts:
#	public/js/emoticon.all.js
2023-08-06 14:08:59 +08:00
weifashi
79b9d412c1 feat:统一表情包名称格式 2023-08-05 21:36:26 +08:00
weifashi
579cf4ee3b feat:添加小海豚表情 2023-08-05 20:42:33 +08:00
kuaifan
554c847ea4 整理表情包 2023-08-05 19:39:44 +08:00
kuaifan
b7d9ac7436 Merge commit 'a951a719d1518883474f36579b63bb8f9c4f0494' into pro 2023-08-05 18:52:52 +08:00
weifashi
a951a719d1 feat:表情包重新整理 2023-08-05 18:25:39 +08:00
kuaifan
31b03beb2c perf: 优化表情关键词匹配 2023-08-05 17:54:49 +08:00
kuaifan
2b3d5ff223 fix: 打开工作流设置后无法关闭桌面客户端的问题 2023-08-05 16:56:25 +08:00
kuaifan
7e5bbb4bb7 perf: 工作流支持关联任务列表自动移动 2023-08-05 16:54:34 +08:00
kuaifan
fcecccc9b8 perf: 支持手动打卡 2023-08-05 15:35:33 +08:00
kuaifan
29ef080399 perf: 优化数据流推送消息页面滚动 2023-08-05 13:05:55 +08:00
kuaifan
459bce93c1 fix: 打不开已归档任务的情况 2023-08-05 12:41:39 +08:00
kuaifan
0ba161f4c5 perf: 优化再次点击消息定位到未读、待办、灰色未读 2023-08-05 11:57:54 +08:00
weifashi
cfb8736a1f feat:更新表情包排序 2023-08-04 22:48:40 +08:00
weifashi
9686df4898 feat:更新原版图片 2023-08-04 21:04:43 +08:00
weifashi
a613507835 feat:添加新的表情包 2023-08-04 19:40:07 +08:00
zzw
bc5046c04d feat:添加通义千问机器人 2023-08-04 18:14:02 +08:00
weifashi
77d3ba0c3b feat:审批中心列表 - 添加上拉加载功能,以及一些loadIng效果 2023-08-04 17:14:46 +08:00
kuaifan
68bd66089a no message 2023-08-04 16:58:18 +08:00
ganzizi
7a1c2c7f2d feat: 新增适用OKR评论不在成员列表内也能显示聊天记录 2023-08-04 16:47:49 +08:00
kuaifan
4f7259747c 替换字体图标 2023-08-04 11:11:13 +08:00
ganzizi
e3079ce6e0 feat: 新增适用默认部门下第1级负责人才能添加部门OKR 2023-08-04 10:57:35 +08:00
weifashi
e26d75c894 fiex:1. 加强发送消息接口的逻辑 , 2.解决在网页图片上直接右键复制图片(非jpg的)后,粘贴到消息框,发送后出现错误 2023-08-03 18:09:11 +08:00
kuaifan
e176600a5a perf: 优化复制链接 2023-08-03 17:21:10 +08:00
weifashi
7147db0ef2 feat:发送文本消息接口 - 添加群发字段 2023-08-03 17:16:44 +08:00
kuaifan
f0d4aa324e perf: 优化可见消息列表 2023-08-03 17:11:57 +08:00
weifashi
6870a29ec9 init:分享列表的接口 - 添加分享文本的功能 2023-08-03 16:12:17 +08:00
weifashi
50f044455a feat:添加预加载 2023-08-03 15:06:07 +08:00
weifashi
6cc20ce23f feat:微应用调整 2023-08-03 14:36:49 +08:00
kuaifan
defd0d2361 perf: 优化动画样式 2023-08-03 11:12:01 +08:00
kuaifan
f49f73409c perf: 优化菜单显示、选择复制 2023-08-02 22:54:57 +08:00
kuaifan
3d783c59c2 no message 2023-08-02 22:16:55 +08:00
weifashi
7582727753 fix:审批中心 - 添加loading效果 2023-08-02 18:36:19 +08:00
spylecym
b16b214099 feat:添加价格页面:离线部署-支持 2023-08-02 18:24:08 +08:00
weifashi
a101feff21 fix:更改okr版本 2023-08-02 17:44:23 +08:00
weifashi
7eef3e7989 feat:优化资源缓存 2023-08-02 17:04:52 +08:00
weifashi
d3182f278f fix:更改okr版本 2023-08-02 16:15:49 +08:00
weifashi
cf3abdf643 fix:修复微应用路由 2023-08-02 16:10:48 +08:00
spylecym
b8edd410b5 feat:增加底部备案 2023-08-02 15:25:16 +08:00
weifashi
d0122328b7 feat:添加okr容器 2023-08-02 14:54:26 +08:00
kuaifan
3b2460c5d1 Merge commit 'a8a2badc99e318319e93ad77aa92d1b3531da391' into pro 2023-08-02 10:58:19 +08:00
kuaifan
41742cd6df no message 2023-08-02 10:05:34 +08:00
weifashi
a8a2badc99 fix:修复文心一言 配置项的文本 2023-08-02 10:03:46 +08:00
kuaifan
423594e20c perf: ai聊天小概率出现重复推流的情况 2023-08-02 00:29:00 +08:00
kuaifan
bb251c8aee perf: 适配arm64 2023-08-01 23:28:23 +08:00
weifashi
d728e2d7c0 feat:添加文心一眼机器人配置 2023-08-01 18:35:17 +08:00
weifashi
6e2e915d95 feat:手机端菜单 添加okr 2023-07-31 17:57:58 +08:00
ganzizi
9d7d2b3fda feat: 新增OKR图标和task图标对等显示 2023-07-31 14:57:27 +08:00
kuaifan
600944fc22 ai model container 2023-07-31 14:09:17 +08:00
weifashi
142de587e4 feat:用户选择组件添加禁用选项 2023-07-31 09:53:03 +08:00
kuaifan
cda1c88434 build 2023-07-29 21:51:45 +08:00
kuaifan
b4bf8304ec no message 2023-07-29 18:14:48 +08:00
kuaifan
9ea1fa6df7 perf: 会话消息没有接收人时已读进度优化 2023-07-29 15:43:48 +08:00
kuaifan
829ed0a575 perf: 优化拖拽文件夹上传提示 2023-07-29 15:26:23 +08:00
kuaifan
bbdf4932e3 perf: 深色模式硬件加速 2023-07-29 15:16:07 +08:00
kuaifan
cc76e91e12 perf: 优化Android弹出键盘后聊天内容被覆盖的问题 2023-07-29 00:04:03 +08:00
kuaifan
b59aca0b5c no message 2023-07-28 23:15:54 +08:00
kuaifan
dd4d8d8880 build 2023-07-28 21:30:10 +08:00
kuaifan
479e9f83a9 no message 2023-07-28 21:28:31 +08:00
kuaifan
a8481e0ad5 no message 2023-07-28 20:38:04 +08:00
kuaifan
827dde4d1b build 2023-07-28 18:57:00 +08:00
weifashi
4a027cec20 feat:样式强化 2023-07-28 18:45:58 +08:00
weifashi
00fa06a274 fix:修复打开okr方法的bug 2023-07-28 17:09:21 +08:00
kuaifan
23cb7ffd6f Merge commit 'ffb05d5aabbc11fe523b88153abfa3bc4760655a' into pro
# Conflicts:
#	resources/views/push/bot.blade.php
2023-07-28 15:36:11 +08:00
kuaifan
2aab517e0c no message 2023-07-28 15:33:00 +08:00
kuaifan
25c2a0d90e no message 2023-07-28 15:30:01 +08:00
weifashi
a603211f1a feat:融合子应用调整 2023-07-28 15:04:38 +08:00
kuaifan
34745fb3e5 perf: 管理员可以修改系统机器人基本资料 2023-07-28 14:31:54 +08:00
kuaifan
05680ab152 perf: 优化深色模式 2023-07-28 13:48:49 +08:00
ganzizi
ffb05d5aab fix: 修复用户列表默认排序 2023-07-28 09:13:51 +08:00
Pang
fe4ab6e9d5 no message 2023-07-28 08:37:47 +08:00
Pang
1ea764c860 no message 2023-07-28 00:50:34 +08:00
Pang
be94c816ca no message 2023-07-27 23:43:57 +08:00
kuaifan
201c0e086b no message 2023-07-27 23:11:48 +08:00
kuaifan
569145196e perf: 添加ChatGPT、Claude智能机器人 2023-07-27 18:42:30 +08:00
ganzizi
7bae000a28 feat: 新增多部门用户查询 2023-07-27 18:18:05 +08:00
weifashi
0d57b8a163 fix: 1. 审批通知模版 - 按钮白色修复 2. 审批详情样式相等 3. 审批评论 - 0分钟换成刚刚 4. 没有加入部门也能发起 审批申请 5. 审批流程设置页 - 样式调整 2023-07-27 15:34:03 +08:00
gwok
34820cc395 Merge branch 'kuaifan-pro' into pro
* kuaifan-pro: (41 commits)
  no message
  build
  build
  隐私政策、私有化部署使用新标签页打开;删除接受cookies页面、二维码扫码页面;删除日志底部定义的线
  build
  no message
  perf: 优化审批机器人模板消息样式
  perf: 优化添加任务样式
  perf: 优化任务默认时间
  perf: 优化深色模式
  fix: 修复无法从任务消息对话中打开任务详情的情况
  perf: 任务详情发送文件时防止按esc关闭发送窗口
  perf: 深色模式下无法扫描登录二维码的情况
  perf: 优化iOS深色模式
  build
  perf: Safari支持暗黑模式
  perf: 优化任务时间冲突提示
  build
  perf: iOS部分点击事件存在阻塞的情况
  site
  ...

# Conflicts:
#	public/site/zh/index.html
2023-07-27 13:55:19 +08:00
kuaifan
efdc4c5229 perf: 机器人群聊消息被@到时发送到webhook 2023-07-27 13:46:20 +08:00
kuaifan
29c9ba56ff no message 2023-07-27 12:05:04 +08:00
kuaifan
e6be45516d Merge commit '2b5b522f3ec7c0bb8c2c82fbe754b194df550305' into pro
# Conflicts:
#	public/site/zh/index.html
2023-07-27 11:56:05 +08:00
weifashi
2b5b522f3e fix:审批中心 - 全文评论 - 时间样式调整 2023-07-27 09:31:05 +08:00
gwok
4ff310ea8b 修改首页
修改路径
2023-07-27 08:41:01 +08:00
Pang
ee4f894933 no message 2023-07-27 08:26:54 +08:00
Pang
482b9d43dd build 2023-07-27 08:03:25 +08:00
Pang
ff64a1a510 Merge commit 'f328a5a43934ce720c50f9ebd155c11e1277a065' into pro 2023-07-27 07:52:43 +08:00
weifashi
f328a5a439 feat:更新审批中心的镜像版本 2023-07-27 00:42:14 +08:00
kuaifan
d418a9e086 build 2023-07-26 22:26:24 +08:00
gwok
e87e69fa1d 隐私政策、私有化部署使用新标签页打开;删除接受cookies页面、二维码扫码页面;删除日志底部定义的线 2023-07-26 21:00:04 +08:00
kuaifan
5d52cb823f Merge commit 'c5114203b27d2ff530897843e0816a3c97990bfe' into tmp
# Conflicts:
#	public/site/css/log.css
#	public/site/en/about.html
#	public/site/en/download.html
#	public/site/en/help.html
#	public/site/en/index.html
#	public/site/en/log.html
#	public/site/en/price.html
#	public/site/en/product.html
#	public/site/en/solutions.html
#	public/site/zh/about.html
#	public/site/zh/download.html
#	public/site/zh/help.html
#	public/site/zh/index.html
#	public/site/zh/log.html
#	public/site/zh/price.html
#	public/site/zh/product.html
#	public/site/zh/solutions.html
2023-07-26 19:14:51 +08:00
kuaifan
5a18eccad5 build 2023-07-26 18:56:53 +08:00
weifashi
df627b0ad5 feat:用户选择组件 - 返回用户ids 2023-07-26 18:47:48 +08:00
kuaifan
395ccaad22 no message 2023-07-26 18:31:16 +08:00
kuaifan
cc96bbf17e perf: 优化审批机器人模板消息样式 2023-07-25 17:37:28 +08:00
kuaifan
a19ee35ca4 perf: 优化添加任务样式 2023-07-25 17:26:34 +08:00
kuaifan
39211297e2 perf: 优化任务默认时间 2023-07-25 17:11:28 +08:00
weifashi
46967f1e00 feat:微应用调整 2023-07-25 16:01:53 +08:00
kuaifan
40b52d8f3b perf: 优化深色模式 2023-07-25 15:15:21 +08:00
kuaifan
c5f7073cc1 fix: 修复无法从任务消息对话中打开任务详情的情况 2023-07-25 12:00:44 +08:00
kuaifan
a1a8e8a962 perf: 任务详情发送文件时防止按esc关闭发送窗口 2023-07-25 11:52:40 +08:00
kuaifan
3d30f4e6c2 perf: 深色模式下无法扫描登录二维码的情况 2023-07-25 11:45:21 +08:00
weigw
c5114203b2 隐私协议提交 2023-07-25 01:46:43 +08:00
kuaifan
1be7e67655 perf: 优化iOS深色模式 2023-07-24 18:19:54 +08:00
weifashi
b8eab81ad2 feat:聊天打开okr - 10% 2023-07-24 18:13:31 +08:00
ganzizi
75242f8844 feat: 新增创建聊天关联id 2023-07-24 16:50:50 +08:00
ganzizi
d423c2bd05 feat: 新增创建聊天关联id 2023-07-24 16:49:07 +08:00
ganzizi
89260dc751 feat: okr信息面板新增"打开OKR"按钮 2023-07-24 15:34:19 +08:00
weifashi
dd7e24850d feat:更改路由 2023-07-24 13:58:06 +08:00
weifashi
42f805d7cc fix:1.仪表盘显示可见性有我,但不是我负责的任务的bug ,2.审批评论间距样式调整与可评论emoj表情加评论通知 ,3,添加任务判断是否存在交集时间内任务调整 ,4. 审批流程撤回后显示错误修复, 5. 审批添加评论后自动滚动到下方 2023-07-21 20:59:08 +08:00
gwokwong
94f1e764bc 中文翻译优化 2023-07-21 18:34:31 +08:00
ganzizi
69fd97485d fix: 去掉test信息 2023-07-21 18:32:32 +08:00
gwokwong
68e8c4bb59 中文翻译 2023-07-21 18:21:46 +08:00
spylecym
7c7965a3f7 fix:修改隐私政策页面 2023-07-21 18:12:15 +08:00
kuaifan
189986dd88 build 2023-07-21 18:11:29 +08:00
kuaifan
b8c2b3a97a perf: Safari支持暗黑模式 2023-07-21 15:46:03 +08:00
kuaifan
5b079018e8 perf: 优化任务时间冲突提示 2023-07-21 12:15:07 +08:00
spylecym
3a834ae90d feat:增加隐私政策链接、页面、cookie弹框 2023-07-21 11:56:26 +08:00
kuaifan
c9d9afc72b build 2023-07-20 23:09:52 +08:00
kuaifan
dac34c8988 perf: iOS部分点击事件存在阻塞的情况 2023-07-20 22:59:13 +08:00
gwokwong
40f18764b9 私有化部署按钮 2023-07-20 17:42:50 +08:00
weifashi
5f95c23029 feat:okr代理路由更改 2023-07-20 17:26:11 +08:00
kuaifan
026dca2d84 Merge commit 'f73ee2c13bd3eff393a768ecd2e83a213a75b678' into pro
# Conflicts:
#	public/site/css/help.css
#	public/site/en/help.html
2023-07-20 16:58:34 +08:00
weifashi
11b152cfbb feat:一些配合okr微应用的改动 2023-07-20 16:31:20 +08:00
ganzizi
af5db70c07 feat: 新增OKR信息推送 2023-07-19 18:26:12 +08:00
zzb-zzb
b4d718126a fix:按照dootask启动原始尺寸截取使用说明的图 2023-07-19 15:37:00 +08:00
weifashi
f73ee2c13b style:整理代码格式 2023-07-19 10:50:40 +08:00
spylecym
aebcca76e4 Merge branch 'pro' of github.com:hitosea/dootask into pro 2023-07-19 10:47:03 +08:00
spylecym
e55fd2cede fix:修改中文日志页面请求地址 2023-07-19 10:46:53 +08:00
weifashi
e272c06028 fix:修改任务时间给提示功能,排除当前任务 2023-07-19 00:50:30 +08:00
weifashi
ada0de8dbc refactor:更改模板名称 2023-07-19 00:41:55 +08:00
weifashi
874bf4b051 feat:添加任务 - 提示功能 - 100% 2023-07-19 00:35:06 +08:00
weifashi
f8e70bd7f7 feat:添加任务 - 提示功能 - 60% 2023-07-18 19:03:09 +08:00
spylecym
04cb03e0b2 fix:修改帮助页面和日志页面中英文的锚点问题 2023-07-18 15:48:57 +08:00
HEXIANG
df287e7122 fix:修改边栏目录滚动效果 2023-07-18 12:55:00 +08:00
HEXIANG
bef48be571 fix:修改边栏目录滚动效果 2023-07-18 12:48:36 +08:00
spylecym
7e77ac1ecb fix:修改帮助页面锚点问题 2023-07-18 11:22:07 +08:00
zzb-zzb
f33faf66a6 fix:官网使用说明的图重新截取更换 2023-07-17 12:05:56 +08:00
weifashi
77ecb89533 style:整理代码格式 2023-07-17 11:46:37 +08:00
weifashi
02bd1deb2c style:整理代码格式 2023-07-17 11:44:30 +08:00
weifashi
8476ed2e51 feat:简单任务列表 - 过滤掉已完成任务 2023-07-17 11:28:18 +08:00
weifashi
a3e32f88b0 fix:修复时间范围的查询 2023-07-17 10:55:59 +08:00
weifashi
92f9240da7 feat:添加简单的任务列表 2023-07-17 10:41:30 +08:00
kuaifan
62cd82a9ec site 2023-07-15 14:22:30 +08:00
kuaifan
97ce98177f site 2023-07-15 12:15:14 +08:00
kuaifan
a614789baf Merge commit 'a6d9617e7fc64d4a3455f02d8ac3cea924800944' into pro
# Conflicts:
#	app/Http/Controllers/Api/SystemController.php
#	public/site/en/download.html
#	public/site/zh/download.html
#	public/site/zh/log.html
2023-07-15 11:17:46 +08:00
gwok
a6d9617e7f feat: 增加获取更新日志接口,更改前端页面默认请求地址 2023-07-14 21:46:57 +08:00
spylecym
4b62aa04aa fix:优化官网布局与样式 2023-07-14 19:39:52 +08:00
zzb-zzb
1ca7e257ee feat:使用说明提交 2023-07-14 18:48:48 +08:00
spylecym
24d7300f23 Merge branch 'pro' of github.com:hitosea/dootask into pro 2023-07-14 11:29:36 +08:00
spylecym
04038253cf fix:修复下载英文页面跳转 2023-07-14 11:29:26 +08:00
kuaifan
f5fc2301bc build 2023-07-14 00:08:40 +08:00
kuaifan
ce82cc8dd9 site 2023-07-14 00:05:05 +08:00
kuaifan
d7e823dfdd Merge commit '8831aa1ce10afa3e7abbf2416242fbf6f66d4e20' into pro
# Conflicts:
#	public/site/en/product.html
#	public/site/en/solutions.html
2023-07-13 23:00:07 +08:00
kuaifan
bbb69b3ec2 no message 2023-07-13 22:58:16 +08:00
gwokwong
8831aa1ce1 feat: 修改英文的下载单次手写字母大写 2023-07-13 21:47:23 +08:00
spylecym
db2e4edb27 fix:修改英文页面 2023-07-13 21:43:05 +08:00
spylecym
fa7a33cb9b fix:导航按钮英文修改 2023-07-13 20:59:02 +08:00
spylecym
fd5088836b fix:修复导航按钮 2023-07-13 20:54:09 +08:00
gwokwong
45746684d5 feat:全局displaynone统一使用comm.css 2023-07-13 20:31:24 +08:00
spylecym
d647a9d6a0 fix:修复官网问题 2023-07-13 19:06:12 +08:00
kuaifan
cbb5ab2ecc build 2023-07-13 00:56:21 +08:00
kuaifan
fc01f00915 Merge commit '986e2cf0b46144ae461825776eea00c3002a30d7' into pro 2023-07-13 00:32:53 +08:00
gwokwong
986e2cf0b4 feat:链接调整和价格页面调整 2023-07-12 20:05:33 +08:00
weifashi
ddc7aecd53 feat:微应用逻辑补充 2023-07-12 18:44:28 +08:00
kuaifan
bf845208db Merge commit 'dd582fef65d377d001e93f45e883d01f3f04b792' into pro 2023-07-12 17:29:20 +08:00
gwokwong
dd582fef65 feat:帮助中心图更换 2023-07-12 16:53:52 +08:00
weifashi
f6fd9a5edf feat:添加配置 2023-07-12 15:29:17 +08:00
weifashi
fead758660 feat:多应用配置 2023-07-12 14:58:52 +08:00
gwokwong
abfcc1020f fix:微调链接和翻译 2023-07-11 20:02:54 +08:00
gwokwong
c2215452d6 feat: 立即体验按钮、价格页面等样式调整 2023-07-11 19:52:07 +08:00
weifashi
10d68790e7 feat:组件共享研究 2023-07-11 18:59:34 +08:00
kuaifan
7a47cb9031 no message 2023-07-11 15:50:04 +08:00
kuaifan
56da2a5725 perf: 整理官网页面 2023-07-11 15:48:22 +08:00
weifashi
6185082311 fix:官网-更换qq群二维码图片 2023-07-11 15:46:03 +08:00
weifashi
7e5f350de5 fix:修复官网静态资源和路由 2023-07-11 15:28:58 +08:00
kuaifan
925667d840 perf: 任务详情页可见性选项默认不显示 2023-07-11 14:43:16 +08:00
kuaifan
87669010e3 perf: 避免删除后不关闭任务窗口 2023-07-11 13:59:31 +08:00
kuaifan
4703b03b82 perf: 添加任务支持自定义协助人 2023-07-11 13:39:38 +08:00
kuaifan
e27ea9e117 fix: 前端取消会议屏幕常亮 2023-07-11 11:31:24 +08:00
kuaifan
1df0a1dfb4 Merge commit '7b02bc006b397990c055e80a006e0d104387f40c' into pro
# Conflicts:
#	resources/assets/js/pages/manage/components/MeetingManager.vue
2023-07-11 11:29:11 +08:00
weifashi
7b02bc006b feat:修复加入会议的loadIng 2023-07-10 20:35:44 +08:00
weifashi
ccea4b5b1b feat:添加微应用配置 2023-07-10 18:41:15 +08:00
weifashi
3096a6be79 feat:1.补充翻译,2给原生传翻译好的文本,3加入会议时appid不对,给出提示 2023-07-10 18:21:20 +08:00
kuaifan
ab23311851 Merge commit '035fba3fe5dacaae8ea00d8a5d296233e947ebf4' into pro 2023-07-09 18:17:39 +08:00
weifashi
035fba3fe5 fix:修复声网 id 改动问题 2023-07-09 17:52:27 +08:00
kuaifan
7d388cb10a build 2023-07-09 17:07:01 +08:00
kuaifan
5152cee99e Merge commit '220ce21a4aec28adb018cf71a8f472bfd54da433' into dev
# Conflicts:
#	resources/assets/js/App.vue
#	resources/assets/js/store/actions.js
2023-07-09 16:33:05 +08:00
kuaifan
1a011fd971 build 2023-07-09 16:24:21 +08:00
weifashi
220ce21a4a fix:发送会议名称给原生那边 2023-07-07 19:49:34 +08:00
weifashi
01b50c442e feat:发送会议名称给原生那边 2023-07-07 17:40:31 +08:00
weifashi
371c87f5a4 fix:统一字段名称 2023-07-07 17:02:13 +08:00
weifashi
155f42dd37 fix:打开会议窗口更改 2023-07-07 16:54:50 +08:00
kuaifan
9289591ba0 整理代码 2023-07-07 12:03:32 +08:00
kuaifan
99d8559c56 build 2023-07-07 11:59:00 +08:00
kuaifan
0adbdbdf1b fix: 全员群禁言仅管理员可发言无效的问题 2023-07-07 11:59:00 +08:00
kuaifan
13560616c8 no message 2023-07-07 11:59:00 +08:00
kuaifan
621706b1ff build 2023-07-07 11:59:00 +08:00
kuaifan
a5ed59bd64 build 2023-07-07 11:59:00 +08:00
kuaifan
a6ce767532 fix: 发送消息失败再次编辑格式丢失的问题 2023-07-07 11:59:00 +08:00
kuaifan
19fd36b195 build 2023-07-07 11:58:59 +08:00
gwokwong
829612e720 feat:官网页面首版提交 2023-07-06 15:25:53 +08:00
weifashi
9ceb00bf54 feat:会议模块换成原生 - web端配合修改 2023-07-05 18:36:06 +08:00
ganzizi
560562755b fix: 1.修复审核导出缺少 2.修复审核导出小时计算误差 2023-07-05 17:54:42 +08:00
ganzizi
aebd47e535 fix: 请假表格导出sheeft里名称显示人名 2023-07-04 18:23:08 +08:00
weifashi
31f051a39e feat:优化任务列表 查询速度 2023-07-04 00:33:21 +08:00
weifashi
49f27ee4cd feat:项目任务用户表 添加索引 “userid” 2023-07-03 22:33:30 +08:00
weifashi
eabbe82b39 perf: 审批中心图片压缩优化 2023-07-03 11:09:12 +08:00
weifashi
01fd5f78e8 fix:审批中心 - 已知bug修复 2023-07-03 10:25:14 +08:00
weifashi
5869c8b6bc fix:修复添加子任务时推送的bug 2023-07-02 16:28:52 +08:00
weifashi
5142497c7a fix:修复任务列表查询不出数据的问题 2023-07-01 23:28:29 +08:00
weifashi
9adb825a88 fix:1.修复任务列表查询不出数据的问题 ,2.项目任务表添加项目id的索引 2, 修复任务详情-前端事件 2023-07-01 22:37:55 +08:00
kuaifan
cde9c819d1 Merge branch 'pro' into pro 2023-06-30 21:09:38 +08:00
weifashi
14195a2647 feat:翻译中文 2023-06-30 19:45:40 +08:00
kuaifan
a35f1505e3 整理代码 2023-06-30 17:24:06 +08:00
kuaifan
c444e2d3fa Merge pull request #173 from hitosea/pro
可见性样式调整
2023-06-30 16:45:30 +08:00
weifashi
287e3e378d style:调整代码格式,未修改代码逻辑 2023-06-30 16:15:05 +08:00
weifashi
41c2dabe26 style:调整代码格式,未修改代码逻辑 2023-06-30 15:47:24 +08:00
weifashi
5c00a28920 style:调整代码格式,未修改代码逻辑 2023-06-30 15:46:00 +08:00
kuaifan
854ed2147d Merge pull request #171 from hitosea/pro
可见性修复
2023-06-30 15:00:08 +08:00
weifashi
dc057b5ca7 fix:可见性下划线消失问题修复 2023-06-30 12:01:31 +08:00
gwok
a9585ed9cc Merge branch 'pro' into pro 2023-06-29 23:32:41 +08:00
weifashi
36499a36b1 feat:调整可见性 2023-06-29 23:18:36 +08:00
weifashi
ff2f311d43 feat:更改可见性样式 2023-06-29 19:15:07 +08:00
Meng
99403fad7d perf(任务可见性):修改可见性推送优化 2023-06-29 10:54:17 +08:00
weifashi
29a651b261 fix:去除无用代码 2023-06-29 01:28:29 +08:00
weifashi
2e92682df2 fix:1.审批中心时长显示优化修复,2.审批中心点击列表时滚动条不变 , 3调整可见性样式 2023-06-29 01:08:36 +08:00
kuaifan
b667184c35 Merge pull request #169 from hitosea/pro
Pro
2023-06-28 22:43:47 +08:00
Meng
5d57666cd4 feat(子任务可见性):优化子任务可见性 2023-06-28 20:45:52 +08:00
kuaifan
ee3c3a0497 Merge branch 'hitosea-pro' into pro
# Conflicts:
#	public/manifest.json
#	resources/assets/js/components/UserInput.vue
#	resources/assets/js/pages/manage/components/TaskDetail.vue
2023-06-27 18:49:07 +08:00
gwokwong
6c84319fca build:添加官网分支的build目录文件 2023-06-27 18:26:12 +08:00
gwokwong
a17991b25e Remove file '/public/js/build/' from history 2023-06-27 17:55:42 +08:00
gwokwong
cfb8818f7b Remove file '/public/js/build' from history 2023-06-27 17:52:35 +08:00
weifashi
d167efb52b fix:修复bug - 负责人已拒绝审批,不要显示“同意/拒绝”操作 2023-06-21 11:00:14 +08:00
weifashi
ad0ce38bc7 fix:修复bug - 负责人已拒绝审批,不要显示“同意/拒绝”操作 2023-06-21 10:54:51 +08:00
Meng
09dbdf1df7 buikd:前端编译 2023-06-20 18:33:15 +08:00
Meng
68aef19c2c buikd:前端编译 2023-06-20 18:26:13 +08:00
Meng
99b6198153 fix(任务详情):修正初始化可见性人员异常问题 2023-06-20 18:24:15 +08:00
Meng
76866e6292 Merge branch 'pro' of https://github.com/hitosea/dootask into pro 2023-06-20 17:08:08 +08:00
Meng
7393fdf6cf perf(消息推送):代码优化 2023-06-20 17:07:56 +08:00
ganzizi
0870ccf4d8 fix: 修复审批通过人员姓名显示不正确 2023-06-20 16:59:11 +08:00
Meng
e8675c9c14 perf:冗余代码去除 2023-06-20 16:25:23 +08:00
Meng
1114a74f94 perf(迁移文件):调整 2023-06-20 14:12:40 +08:00
Meng
3ea7d61bb7 build:前端编译 2023-06-20 13:13:02 +08:00
Meng
f077b41ffc feat(任务可见性功能):负责人、协助人更改可见性推送收回 2023-06-20 13:12:13 +08:00
Meng
ad5cb79d40 feat(任务可见性功能、子任务优化):新增任务可见性操作模块、任务详情子任务样式优化 2023-06-20 12:00:34 +08:00
kuaifan
1fdaecf7b4 build 2023-06-19 23:21:52 +08:00
kuaifan
dd2ff9359b perf: 优化移动端任务详情编辑 2023-06-19 23:18:52 +08:00
kuaifan
c8e3a5ee4c perf: 聊天输入框iOS输入第一个字母出现抖动的情况 2023-06-19 12:07:57 +08:00
kuaifan
5dddf25e5e perf: 优化iOS出现连续加载消息列表的情况 2023-06-19 12:06:51 +08:00
kuaifan
7ad8abce6b fix: 会议窗口恢复不显示的情况 2023-06-19 10:40:16 +08:00
kuaifan
175864403e Merge pull request #167 from hitosea/pro
fix: 修复审批的图片无法查看
2023-06-19 00:39:29 +08:00
kuaifan
2082fbc4dd build 2023-06-18 15:51:31 +08:00
kuaifan
9247860b50 perf: 移动端键盘发送 2023-06-18 15:47:13 +08:00
kuaifan
a4e41ffb24 build 2023-06-18 14:50:53 +08:00
kuaifan
19815415d0 fix: 修复已知bug 2023-06-17 06:01:27 +08:00
kuaifan
c940698933 build 2023-06-16 18:38:10 +08:00
kuaifan
6426c76bce fix: 打开会话面板报错 2023-06-16 17:45:57 +08:00
kuaifan
04632182b4 perf: 优化会员选择器 2023-06-16 17:45:42 +08:00
kuaifan
29bbbd804a perf: 优化图片压缩 2023-06-15 17:25:25 +08:00
kuaifan
fd33eb3d1c build 2023-06-14 17:22:03 +08:00
kuaifan
4845e2e6a6 fix: 子任务通知无法打开 2023-06-14 17:19:34 +08:00
kuaifan
414b423311 perf: 回复图片显示图片搜略图 2023-06-14 16:05:46 +08:00
kuaifan
e507c148ca perf: 优化会员选择器 2023-06-14 15:44:48 +08:00
kuaifan
ae4be9e08e no message 2023-06-13 23:16:08 +08:00
kuaifan
296ae69cf9 perf: 会员选择下拉框提示 2023-06-13 09:21:28 +08:00
weifashi
f613a8cfc6 fix: 修复审批的图片无法查看 2023-06-12 13:48:26 +08:00
kuaifan
b0369a3af1 build 2023-06-11 23:30:54 +08:00
kuaifan
27d69e90fa Merge pull request #166 from hitosea/pro
fix:分享功能,补充头像判断
2023-06-11 14:25:41 +08:00
weifashi
19a4f63ffa fin:分享功能,补充头像判断 2023-06-11 12:54:37 +08:00
kuaifan
a32b15e89c build 2023-06-11 09:27:14 +08:00
kuaifan
644d8e22a1 perf: 优化富文本输入框 2023-06-11 09:22:56 +08:00
kuaifan
722b3b4788 perf: 修改开发依赖 2023-06-11 07:24:31 +08:00
kuaifan
3b97c6ecd9 Merge pull request #165 from hitosea/pro
分享逻辑
2023-06-10 22:26:07 +08:00
gwok
20aa89dd6a Merge branch 'pro' into pro 2023-06-10 09:47:06 +08:00
kuaifan
0d2754ab95 build 2023-06-10 01:01:30 +08:00
weifashi
010e9c2fbe fix:提交图片到服务器测试 2023-06-09 19:32:05 +08:00
weifashi
8d4511e2b1 fix: 更换图片为png 2023-06-09 19:31:25 +08:00
kuaifan
77f1869e3c fix: 打开任务出现空白错误的概率 2023-06-09 18:01:36 +08:00
weifashi
ec032b91a3 fix: 去除没应用的导入 2023-06-09 17:53:54 +08:00
kuaifan
68d9d3a659 perf: 网络异常自动重试 2023-06-09 17:49:52 +08:00
kuaifan
c4cc7ea18c build 2023-06-09 16:54:20 +08:00
kuaifan
e75fbadd53 perf: 触屏设备实体键盘回车发送 2023-06-09 16:52:12 +08:00
kuaifan
4957f32d06 perf: 消息输入框支持全屏输入 2023-06-09 16:05:00 +08:00
kuaifan
c2715f9b5e perf: 优化大屏移动端长按菜单 2023-06-09 16:04:38 +08:00
weifashi
d3a3c34287 fix: 更该文件类型都为 children 2023-06-09 15:49:21 +08:00
weifashi
95ec53381b fix: 分享接口修改 2023-06-09 15:27:39 +08:00
weifashi
8c6f1120e4 fix: 修改分享逻辑 2023-06-09 15:05:09 +08:00
kuaifan
ea1c2a34e2 fix: safari 消息输入框焦点溢出的情况 2023-06-09 13:58:43 +08:00
kuaifan
a233c492e6 fix: 已知bug 2023-06-09 11:11:04 +08:00
kuaifan
aea2124d61 build 2023-06-08 21:04:02 +08:00
kuaifan
317970f010 perf: scrollbar 2023-06-08 20:34:29 +08:00
kuaifan
b0fda87923 no message 2023-06-07 09:50:23 +08:00
kuaifan
73cb7e9397 Merge pull request #161 from hitosea/pro
默认审核图片显示
2023-06-06 23:45:31 +08:00
weifashi
95d3c7dfce build:生成doc文档 2023-06-06 21:16:44 +08:00
weifashi
40d5c1f11e perf:代码整理 2023-06-06 19:42:48 +08:00
gwok
847d7f98fc Merge branch 'pro' into pro 2023-06-06 18:44:32 +08:00
weifashi
3a13b8bf30 fix: 修复获取聊天列表的接口 2023-06-06 14:25:34 +08:00
weifashi
11e30d5860 fix: 添加密码账号长度限制 2023-06-06 11:51:48 +08:00
weifashi
6f1a0d90b0 fix: 添加密码账号长度限制 2023-06-06 11:50:19 +08:00
weifashi
098269adf6 fix: dootask对接系统分享 - 添加头像返回 2023-06-06 11:26:45 +08:00
weifashi
0d63447987 fix:添加群发接口 2023-06-06 10:56:29 +08:00
weifashi
edfb32647b fix: 兼容加密bug问题处理 2023-06-05 19:03:57 +08:00
weifashi
20496b2edb build 2023-06-05 15:21:58 +08:00
weifashi
4739539722 fix: dootask对接系统分享 2023-06-05 15:11:40 +08:00
weifashi
dba2bdd9e7 fix: 更改默认审核的图片 2023-06-05 14:21:49 +08:00
kuaifan
905662e963 build 2023-05-31 15:26:01 +08:00
kuaifan
ac2dfcfce3 Merge pull request #159 from hitosea/pro-doo
打开审批设置页面路由修改
2023-05-31 15:22:31 +08:00
gwokwong
fd6fef4292 Merge branch 'pro' into pro-doo
* pro:
  fix: 打开审批设置页面 -  路由修改
2023-05-31 15:04:43 +08:00
weifashi
eee7b8f0f1 fix: 打开审批设置页面 - 路由修改 2023-05-31 14:34:50 +08:00
kuaifan
ef88924f36 build 2023-05-31 09:15:38 +08:00
kuaifan
c9378b0eab Merge pull request #157 from hitosea/pro-doo
Pro doo
2023-05-30 19:59:14 +08:00
gwokwong
cde96cb8b4 Merge branch 'pro' into pro-doo
* pro:
  fix: 更改审批流数据表名
2023-05-30 19:12:15 +08:00
weifashi
ffbd9b1f26 fix: 更改审批流数据表名 2023-05-30 18:38:39 +08:00
gwokwong
59524c78e6 调整统一命名 2023-05-30 18:04:33 +08:00
weifashi
1ad0c3a3ec fix: 调整统一命名 2023-05-30 17:58:43 +08:00
gwokwong
e85d4c24d3 build编译文件上传 2023-05-26 14:31:44 +08:00
gwokwong
18280b4ff9 工作流合并 2023-05-26 14:19:36 +08:00
weifashi
e504fb845f build 2023-05-24 11:08:27 +08:00
weifashi
003186edd9 fix: 添加评论功能 - 100% 2023-05-24 11:04:39 +08:00
ganzizi
61e958d757 perf 优化全局评论 2023-05-24 11:01:10 +08:00
ganzizi
2ee349605e feat 全局评论追加 2023-05-24 10:15:08 +08:00
ganzizi
7d200d9a73 feat 全局评论追加 2023-05-24 10:10:56 +08:00
ganzizi
0468178c43 feat 新增全局评论init 2023-05-23 19:12:54 +08:00
ganzizi
6f4a94c8d1 feat 新增全局评论init 2023-05-23 19:07:37 +08:00
weifashi
e83a65ec40 fix:工作流 - 添加上传图片功能 100% 2023-05-22 18:36:25 +08:00
weifashi
53ea6b00ab build - 去除加密 - 测试那边用 2023-05-17 14:54:46 +08:00
weifashi
45865c0c18 build 2023-05-11 17:51:27 +08:00
weifashi
979d8057a4 fix:添加流程 标题改名 2023-05-11 17:48:57 +08:00
weifashi
b81f75b794 build 2023-05-11 17:46:56 +08:00
weifashi
8f7578ab06 fix:假期类型去掉外出 2023-05-11 17:44:59 +08:00
weifashi
dd39a8b63f build 2023-05-11 17:27:59 +08:00
weifashi
7595309dc8 fix:翻译 2023-05-11 17:26:09 +08:00
weifashi
93abe72041 fix: 调整样式 2023-05-11 17:21:33 +08:00
kuaifan
60c513ea3b build 2023-05-10 19:53:58 +08:00
kuaifan
385abae741 fix: iOS16.4之前版本无法进入的问题 2023-05-10 19:48:56 +08:00
kuaifan
a680538f80 build 2023-05-08 23:19:13 +08:00
kuaifan
11571c2045 perf: Windows 10 以下不支持加密 2023-05-08 23:09:59 +08:00
kuaifan
f1d267e2b2 fix: 移动端无法长按录音的问题 2023-05-08 22:57:25 +08:00
kuaifan
003b52fbc9 perf: 优化图形验证码方式 2023-05-08 22:42:11 +08:00
weifs
6e2df6fcfc build:编译资源上传 2023-04-28 10:23:19 +08:00
weifs
cd151667c1 feat:1. 新增部门负责人标识并排第一位 - 深优化
2.  clearHelper.php 文件添加判断是cmd命令运行时才执行
2023-04-28 10:14:10 +08:00
ganzizi
1a3ecbb4e9 perf 新增部门负责人标识并排第一位 2023-04-28 09:28:44 +08:00
ganzizi
0b1f433fe4 feat 新增部门负责人标识并排第一位 2023-04-27 19:19:01 +08:00
kuaifan
7aa44b8623 perf: 兼容iPad键盘 2023-04-26 20:58:56 +08:00
weifs
6e68100f86 build:编译资源上传 2023-04-25 17:35:39 +08:00
weifs
c869e88c40 fix:兼容safari 时间处理 2023-04-25 14:18:55 +08:00
kuaifan
9771e1e741 perf: 优化移动端兼容 2023-04-25 12:03:59 +08:00
weifs
9034a70b8f fix:修复了bug - 成员已加入部门,无法发起请假申请 2023-04-25 10:08:10 +08:00
weifs
e58ebebb80 feat:解决bug - 发起流程提示用户所在部门departmentId不能为空 2023-04-25 09:44:57 +08:00
weifs
edab43504e build:编译资源上传 2023-04-24 17:11:11 +08:00
kuaifan
900b11280c perf: 优化iPad Pro页面布局 2023-04-24 14:48:35 +08:00
kuaifan
0f449f151a 网页端token过期时间调整成30天 2023-04-24 14:18:13 +08:00
kuaifan
8186649a9f fix: 终端用户数提示错误 2023-04-24 13:59:52 +08:00
weifs
364ef34248 feat:更改镜像 2023-04-24 10:51:25 +08:00
ganzizi
c66714d1f5 perf 优化导出 2023-04-21 11:58:24 +08:00
ganzizi
d42fcca81f perf 优化导出 2023-04-21 11:58:24 +08:00
weifs
16d1591675 feat:工作流-翻译 2023-04-21 11:09:22 +08:00
ganzizi
d7b2f93797 perf 优化抄送信息按钮显示 2023-04-21 10:00:34 +08:00
ganzizi
9203046d97 perf 优化发给抄送人信息 2023-04-21 09:25:20 +08:00
ganzizi
b860a749f3 perf 优化发给抄送人信息 2023-04-20 18:32:49 +08:00
ganzizi
6c689fe55f perf 优化发给抄送人信息 2023-04-20 18:31:37 +08:00
weifs
c18fd1ef20 feat:工作流 - 小优化 2023-04-20 18:25:34 +08:00
ganzizi
7e56389c34 perf 优化工作流容器时间 2023-04-20 17:50:23 +08:00
weifs
28771c38e7 feat:更新容器 2023-04-20 16:38:00 +08:00
weifs
de7e74ff19 feat:工作流优化 2023-04-20 16:10:39 +08:00
ganzizi
2cf661b54f feat 导出前端页 2023-04-20 14:55:20 +08:00
weifs
16ea92c970 feat: 工作流 - 前端bug修复,节点不显示提及名称 2023-04-20 14:37:06 +08:00
weifs
3ad55cf6d5 Merge branch 'pro' of github.com:hitosea/dootask into pro
# Conflicts:
#	app/Http/Controllers/Api/WorkflowController.php
2023-04-20 14:27:13 +08:00
weifs
d0951041eb feat:工作流 - 前端100% 2023-04-20 14:24:58 +08:00
ganzizi
150d9f18b4 feat 新增工作流导出 2023-04-20 11:51:39 +08:00
weifs
09f48c32c7 feat:工作流前端进度 - 95% 2023-04-19 17:19:28 +08:00
weifs
596594d8b3 feat:工作流 - 前端进度 90% 2023-04-18 18:57:22 +08:00
ganzizi
59d17aa950 feat 新增查询条及已发起所有记录 2023-04-18 16:10:21 +08:00
weifs
836d4f8209 feat:工作流 - 前端进度 70% 2023-04-18 15:54:36 +08:00
ganzizi
c13aa8d7a4 feat 新增查询条及已发起所有记录 2023-04-18 15:00:21 +08:00
kuaifan
5fb07b7aa7 build 2023-04-18 12:02:39 +08:00
kuaifan
ee12cc21c1 no message 2023-04-18 11:51:02 +08:00
weifs
6abfc7a2e2 feat:工作流 - 前端进度 60% 2023-04-18 10:45:49 +08:00
weifs
18758421fd feat:工作流 - 前端进度 50% 2023-04-17 18:46:57 +08:00
ganzizi
644a986747 feat 新增用户头像 2023-04-17 17:43:54 +08:00
ganzizi
ad164f3539 feat 新增用户头像 2023-04-17 15:20:50 +08:00
ganzizi
d5a7ae5fa3 fix 修复了bug 2023-04-17 14:24:55 +08:00
ganzizi
a845b7b892 fix 修复了bug 2023-04-17 14:16:28 +08:00
ganzizi
a29414a477 fix 修复了bug 2023-04-17 13:48:29 +08:00
kuaifan
1c3ce47dcc build 2023-04-17 12:42:58 +08:00
kuaifan
8f30693473 no message 2023-04-17 12:37:22 +08:00
kuaifan
0f5f7c515a perf: 优化移动端部门管理 2023-04-17 12:36:59 +08:00
kuaifan
8e76493cc6 perf: 移动端显示工作报告 2023-04-17 12:26:00 +08:00
ganzizi
8617c72aab fix 修复了bug 2023-04-17 10:18:28 +08:00
weifs
7f0a150b74 feat:更新前端路由 2023-04-17 10:03:02 +08:00
HEXIANG
a53aaff620 fix:审批流程静态页 2023-04-17 09:27:49 +08:00
ganzizi
cab81c5c04 perf 工作流接口优化 2023-04-13 14:35:54 +08:00
ganzizi
5923b6169a perf 工作流接口优化 2023-04-13 09:43:02 +08:00
weifs
0f5eb829c6 feat:工作流 - 前端设置页面 - 30% 2023-04-13 09:38:50 +08:00
ganzizi
d87c06de9a feat 初步融合工作流 2023-04-12 18:25:11 +08:00
ganzizi
76e8870738 feat 初步融合工作流 2023-04-12 18:22:25 +08:00
kuaifan
247b9ee0c1 下班静默推送 2023-04-12 18:17:11 +08:00
kuaifan
80dd76b078 ws仅加密对话消息 2023-04-12 17:51:45 +08:00
ganzizi
9163cee90e feat 初步融合工作流 2023-04-12 16:58:27 +08:00
weifs
fe46d63031 feat:初步融合工作流 2023-04-12 16:55:00 +08:00
HEXIANG
d14d5a5942 fix:审批流程静态页 2023-04-12 16:41:11 +08:00
ganzizi
7181652f01 feat 初步融合工作流 2023-04-12 16:25:26 +08:00
kuaifan
4024cfb7c7 no message 2023-04-10 12:36:41 +08:00
kuaifan
1cf5a24c26 perf: 导出数据支持搜索离职会员 2023-04-10 10:48:45 +08:00
kuaifan
06795ca32c build 2023-04-07 17:37:32 +08:00
kuaifan
c4fd5ee60a perf: 优化md消息图片预览 2023-04-07 17:02:29 +08:00
kuaifan
e30b66e5c1 no message 2023-04-07 15:22:35 +08:00
kuaifan
9ee8d9b828 build 2023-04-06 13:03:25 +08:00
kuaifan
6107bde666 license 提醒 2023-04-06 12:53:33 +08:00
kuaifan
e69a91feb3 fix: 修改个人头像缓存不更新的情况 2023-04-06 11:15:42 +08:00
kuaifan
174df30978 更新自启动说明 2023-04-06 10:56:49 +08:00
kuaifan
8ffac1376a perf: 对webp文件的支持 2023-04-06 10:34:23 +08:00
kuaifan
433a46bba9 fix: 无法查看已归档任务 2023-04-06 09:31:08 +08:00
kuaifan
ba3d5299a0 build 2023-04-04 21:18:50 +08:00
kuaifan
0aa6911159 perf: 右键或长按消息发送按钮可选无声发送、Markdown格式发送 2023-04-04 21:14:58 +08:00
kuaifan
1cef313689 perf: 系统设置新增图片优化、是否保存网络图片功能 2023-04-04 11:46:44 +08:00
kuaifan
4a198ccd8f no message 2023-04-03 23:49:52 +08:00
kuaifan
11e08fea73 build 2023-04-03 22:54:19 +08:00
kuaifan
a5be461021 perf: 消息api支持markdown 2023-04-03 22:47:23 +08:00
kuaifan
d2e04843a4 no message 2023-04-03 12:57:32 +08:00
kuaifan
d0276e63c6 build 2023-04-03 11:50:09 +08:00
kuaifan
2d4fa5735b no message 2023-04-03 11:32:02 +08:00
kuaifan
1c367eb687 perf: 优化图片消息 2023-04-03 11:01:38 +08:00
kuaifan
56cf534340 build 2023-04-02 23:15:02 +08:00
kuaifan
01fbc8b39d no message 2023-04-02 23:01:51 +08:00
kuaifan
91f3ea7c46 fix: 会议回音 2023-04-02 22:43:18 +08:00
kuaifan
d11a89d02c perf: 优化动态加载静态资源 2023-04-02 22:41:49 +08:00
kuaifan
b580a9f7ad no message 2023-04-02 11:18:09 +08:00
kuaifan
c2db186620 perf: 消息接口支持MARKDOWN 2023-04-02 10:53:21 +08:00
kuaifan
679a0002a7 no message 2023-04-01 23:45:21 +08:00
kuaifan
81fdcce40f no message 2023-04-01 23:09:13 +08:00
kuaifan
8af540f9b2 build 2023-04-01 21:33:14 +08:00
kuaifan
79898d87c0 fix: 文件搜索不到根目录的共享 2023-04-01 16:54:13 +08:00
kuaifan
3c400af559 perf: 优化会话搜索 2023-04-01 16:16:24 +08:00
kuaifan
c8375b846a build 2023-04-01 15:29:37 +08:00
kuaifan
765f74b619 no message 2023-04-01 15:25:11 +08:00
kuaifan
2fcb7fb59b perf: 通过页面修改机器人资料 2023-04-01 15:23:07 +08:00
kuaifan
d828ed83a5 no message 2023-04-01 13:03:16 +08:00
kuaifan
22993283f0 fix: 清除缓存导致获取不到数据的问题 2023-04-01 12:39:03 +08:00
kuaifan
69f0d3b2bc build 2023-03-31 20:31:02 +08:00
kuaifan
ab0a5898cb perf: WebSocket 数据传输加密 2023-03-31 20:24:23 +08:00
kuaifan
60a41cae41 Merge branch 'e2ee' into pro 2023-03-30 16:08:15 +08:00
kuaifan
6c7f7f9543 build 2023-03-30 15:56:17 +08:00
kuaifan
d571d6e121 no message 2023-03-30 15:45:44 +08:00
kuaifan
b7b933c89d fix: 无法查看已归档任务 2023-03-30 15:45:44 +08:00
kuaifan
7e98a78333 feat: 实现非对称加密关键接口 2023-03-30 15:44:51 +08:00
kuaifan
342b9d6fc6 upgrade php container 2023-03-28 00:11:48 +08:00
kuaifan
ef7ed58a22 no message 2023-03-25 23:41:48 +08:00
kuaifan
160c736b38 perf: 自动清空文件回收站 2023-03-25 23:25:50 +08:00
kuaifan
01bb72523e build 2023-03-25 20:39:52 +08:00
kuaifan
ec62d7be5a perf: 优化任务接口数据逻辑 2023-03-25 20:23:25 +08:00
kuaifan
f8cbd31f61 no message 2023-03-25 12:25:22 +08:00
kuaifan
8c04a432a8 no message 2023-03-24 11:04:02 +08:00
kuaifan
3a9001e091 Upgrade Professional Edition 2023-03-24 09:08:53 +08:00
kuaifan
a172909ddf build 2022-06-20 18:41:00 +08:00
kuaifan
54d695f851 perf: 指定mariadb:10.7.3解决部分出现初始化数据库失败的情况 2022-06-20 18:40:09 +08:00
kuaifan
0147e7f1e1 perf: 上传限制改为1G 2022-06-20 18:39:29 +08:00
kuaifan
86886ded16 no message 2022-04-08 08:05:52 +08:00
kuaifan
6c44abded9 build 2022-04-08 00:29:12 +08:00
kuaifan
f3fb777924 perf: 搜索后支持快速取消筛选 2022-04-07 23:23:04 +08:00
kuaifan
1323bba420 fix: 工作包括编辑内容不正确的问题 2022-04-07 22:51:43 +08:00
kuaifan
e55a1d8713 fix: 会员选择框偶尔出现默认值错误的情况 2022-04-07 22:51:12 +08:00
kuaifan
d951a6057d perf: 优化工作汇报的搜索 2022-04-07 19:45:03 +08:00
kuaifan
9755c59687 perf: 新增邮件发送测试 2022-04-07 14:27:54 +08:00
kuaifan
2918c55fa9 build 2022-04-07 11:15:32 +08:00
kuaifan
f01f5d7837 win版本更新提示文案不是重启,是立即安装 2022-04-07 10:19:40 +08:00
kuaifan
01313b16e1 perf: 优化用户邮箱验证 2022-04-07 09:05:32 +08:00
kuaifan
601d037201 perf: 优化任务到期前后邮件提醒 2022-04-07 08:29:49 +08:00
kuaifan
9bc56f5d17 no message 2022-04-07 07:44:56 +08:00
kuaifan
8237cd21ed no message 2022-04-02 22:45:58 +08:00
kuaifan
bb3f1f2c10 no message 2022-04-02 22:45:10 +08:00
kuaifan
f8463823d9 perf: 消息已完成图标布局优化 2022-04-02 08:45:04 +08:00
kuaifan
08c46fd66e fix: 部分客户端登录页面报错的问题 2022-04-02 08:44:28 +08:00
kuaifan
1939d42d09 fix: 退出登录仍出现未读数的情况 2022-04-02 08:43:55 +08:00
kuaifan
9a0a04ed76 perf: 优化websocket连接机制 2022-04-02 08:19:13 +08:00
kuaifan
813a49bf67 no message 2022-04-01 16:22:13 +08:00
kuaifan
77924ff248 no message 2022-04-01 15:22:48 +08:00
kuaifan
9dad51fa0b no message 2022-04-01 14:43:22 +08:00
kuaifan
8c39a81644 no message 2022-04-01 13:10:59 +08:00
kuaifan
7749fac683 优化更新提示 2022-04-01 12:27:12 +08:00
kuaifan
91f4b2fd9d no message 2022-04-01 11:40:35 +08:00
kuaifan
e0a3259765 no message 2022-04-01 10:13:41 +08:00
kuaifan
38c1a768fc feat: 客户端新增系统托盘图标 2022-04-01 09:18:48 +08:00
kuaifan
3c71af064c no message 2022-04-01 07:14:57 +08:00
kuaifan
7841f54db8 防止多开 2022-04-01 07:14:45 +08:00
kuaifan
936dee9da5 perf: 文件权限提示点击确定返回主目录 2022-04-01 06:26:52 +08:00
kuaifan
a10d5ee43b no message 2022-04-01 06:17:42 +08:00
kuaifan
a0b8529606 no message 2022-04-01 05:53:16 +08:00
kuaifan
6bf1eb5bde 优化暗黑样式 2022-03-31 23:08:31 +08:00
kuaifan
fb1b5969f5 build 2022-03-31 22:42:58 +08:00
kuaifan
8eca06fd5f perf: 优化聊天窗口显示头像 2022-03-31 22:26:47 +08:00
kuaifan
3c8642f30f perf: 优化聊天窗口滚动 2022-03-31 21:48:23 +08:00
kuaifan
3796a3dbde perf: 取消任务群聊发送图片同步到任务附件 2022-03-31 20:43:55 +08:00
kuaifan
637104643c 优化样式 2022-03-31 20:23:53 +08:00
kuaifan
9364cfe1cf no message 2022-03-31 19:13:56 +08:00
kuaifan
4ae66ee3aa no message 2022-03-31 19:11:30 +08:00
kuaifan
a6534518f8 perf: 优化项目页面聊天窗口 2022-03-31 19:10:03 +08:00
kuaifan
e327311477 perf: 优化网络重连聊天机制 2022-03-31 19:09:33 +08:00
kuaifan
78c766c52e fix: 已完成任务还可以拖动排序的问题 2022-03-31 18:47:24 +08:00
kuaifan
3c89ecb2c7 fix: 移动端不显示甘特图的问题 2022-03-31 17:58:28 +08:00
kuaifan
948972184e no message 2022-03-31 15:17:48 +08:00
kuaifan
8874a3fec7 no message 2022-03-31 15:15:23 +08:00
kuaifan
dde28136e1 perf: 文件查看图片直接弹窗浏览 2022-03-31 15:12:45 +08:00
kuaifan
ce03296078 整理下载文件跳转 2022-03-31 13:55:57 +08:00
kuaifan
457efa1c79 整理下载文件跳转 2022-03-31 13:53:15 +08:00
kuaifan
cfd1fc275b no message 2022-03-31 09:59:49 +08:00
kuaifan
9d56dd122f no message 2022-03-31 09:57:03 +08:00
kuaifan
b1e35e6824 build 2022-03-31 09:46:38 +08:00
kuaifan
775fdec0b8 perf: 文件共享成员支持分享链接 2022-03-31 09:44:52 +08:00
kuaifan
f403014ef6 no message 2022-03-31 09:42:41 +08:00
kuaifan
5982424864 no message 2022-03-30 18:01:00 +08:00
kuaifan
2ad5ccabec no msg 2022-03-30 17:53:55 +08:00
kuaifan
9ffc3520e7 优化下载提示 2022-03-30 17:29:12 +08:00
kuaifan
7ba28a9770 build 2022-03-30 08:28:12 +08:00
kuaifan
bec72dfd5f no msg 2022-03-30 08:20:41 +08:00
kuaifan
cf6b4f5432 perf: 优化提示此文件夹内已有共享文件夹 2022-03-30 08:20:24 +08:00
kuaifan
fe82c44687 perf: 支持上传golang文件 2022-03-30 07:40:13 +08:00
kuaifan
49e4f15bd1 perf: 文件新增pids(上级ID递归)字段 2022-03-30 07:35:25 +08:00
kuaifan
7dd85b2ba6 recovery appId 2022-03-30 06:25:20 +08:00
kuaifan
7b64793449 no message 2022-03-30 00:14:03 +08:00
kuaifan
5485f2013e no message 2022-03-29 22:41:48 +08:00
kuaifan
6ed24ec310 no message 2022-03-29 21:00:50 +08:00
kuaifan
e39335864d no message 2022-03-29 19:16:27 +08:00
kuaifan
27ec2c276a no message 2022-03-29 18:42:54 +08:00
kuaifan
b2f8da500b no message 2022-03-29 14:48:36 +08:00
kuaifan
80a21dbf4a no message 2022-03-25 10:48:31 +08:00
kuaifan
33bd39859d no msg 2022-03-25 10:47:15 +08:00
kuaifan
7367a769b1 fix: 设置分页10条每页无效的问题 2022-03-25 10:04:10 +08:00
kuaifan
1ca4c67f7e vue-kityminder-gg 更换 vue-kityminder-ggg 2022-03-25 10:03:28 +08:00
kuaifan
120a67159d no message 2022-03-24 18:32:36 +08:00
kuaifan
aa808587df no message 2022-03-24 11:56:45 +08:00
kuaifan
57df991dd3 优化 goForward 2022-03-24 11:55:54 +08:00
kuaifan
665184bfa2 fix: 客户端打开不自动登录的问题 2022-03-24 11:55:43 +08:00
kuaifan
ceee696443 no message 2022-03-24 11:48:58 +08:00
kuaifan
7fda376f19 perf: 优化一些前端 2022-03-24 11:48:04 +08:00
kuaifan
bd60cb3a18 build 2022-03-23 15:28:49 +08:00
kuaifan
534ed6ae6c no message 2022-03-23 15:27:37 +08:00
kuaifan
acac93bbd1 fix: 修复新增项目成员无法通过邮箱搜索的问题 2022-03-23 15:09:57 +08:00
kuaifan
070e4b8527 build 2022-03-23 12:00:25 +08:00
kuaifan
a4e8761add 客户端不需要显示首页 2022-03-23 11:59:19 +08:00
kuaifan
590b76a884 perf: 优化工作量配色 2022-03-23 11:52:03 +08:00
kuaifan
98729cfa7b fix: Public客户端打开空白的情况 2022-03-23 10:40:02 +08:00
kuaifan
2bd077136f perf: 优化路由重复提示的报错 2022-03-23 10:21:17 +08:00
kuaifan
6fba94594b fix: 所有可搜索列表在非第1页搜索时不返回第1页的问题 2022-03-23 10:12:37 +08:00
kuaifan
24e9ff4c86 fix: 查看已发送的工作汇报,汇报对象需横向显示 2022-03-23 10:11:31 +08:00
kuaifan
379357ee8e no message 2022-03-18 10:59:44 +08:00
kuaifan
ef7a64644b no msg 2022-03-18 10:06:11 +08:00
kuaifan
c163c20c3d no message 2022-03-17 23:06:45 +08:00
kuaifan
3ddbeac419 build 2022-03-17 22:06:59 +08:00
kuaifan
ef7a2ec123 no msg 2022-03-17 22:00:44 +08:00
kuaifan
8dd4cfa6b2 优化团队管理搜索功能 2022-03-17 22:00:36 +08:00
kuaifan
7491a6faac 优化字段 2022-03-17 21:41:17 +08:00
kuaifan
eaf1a59e89 优化预览图片 2022-03-17 21:15:53 +08:00
kuaifan
4cbc5040a2 perf: 任务详细描述右键新增预览图片 2022-03-17 21:15:18 +08:00
kuaifan
b90b129d98 no msg 2022-03-17 17:52:07 +08:00
kuaifan
64c0ae2734 优化首页 2022-03-17 17:51:56 +08:00
kuaifan
1c2a7a219a 优化路径命名 2022-03-17 14:03:34 +08:00
kuaifan
868b8e7206 perf: 优化图片预览,优化与弹窗esc按键冲突的问题 2022-03-17 10:53:30 +08:00
kuaifan
6115828dfe build 2022-03-16 17:18:19 +08:00
kuaifan
af9b5ab014 perf: 图片预览使用当前页组件,支持多图 2022-03-16 17:15:39 +08:00
kuaifan
5497bd97b2 优化甘特图 2022-03-16 16:13:01 +08:00
kuaifan
c53db63cc7 perf: 优化工作报告前端 2022-03-16 15:46:47 +08:00
kuaifan
1263603c7b 优化客户端工作报告 2022-03-16 10:24:36 +08:00
kuaifan
d1036c3be7 合并整理 2022-03-16 09:04:00 +08:00
kuaifan
5adbd6e8f1 perf: 优化甘特图 2022-03-16 07:21:21 +08:00
kuaifan
b83ce02849 perf: 优化任务列表切换显示 2022-03-15 17:38:02 +08:00
韦荣超
735a9c8a5d build: bf46881e3b 2022-03-15 16:46:32 +08:00
韦荣超
bf46881e3b fix: 项目-任务列表tab优化 2022-03-15 16:44:35 +08:00
韦荣超
f68226f4b4 fix: 项目-任务列表tab修改,甘特图数据优化 2022-03-15 15:46:45 +08:00
韦荣超
07d864e48b build: 0326864510 2022-03-15 10:45:07 +08:00
韦荣超
0326864510 fix: 项目--任务列表删除主任务后,子任务仍显示问题 2022-03-15 10:42:40 +08:00
韦荣超
2a97060b96 build: 6cffd81540 2022-03-14 18:19:29 +08:00
韦荣超
6cffd81540 fix: 项目任务甘特图图标及优化移动逻辑 2022-03-14 18:17:23 +08:00
Xuronglong
eb2b11ed3e perf: 更新icon图标库
perf: 更新icon图标库
2022-03-14 17:29:06 +08:00
Xuronglong
7cb52bebf5 perf: 更新icon图标库
perf: 更新icon图标库
2022-03-14 17:16:47 +08:00
韦荣超
3731f5d32c build: 7161b14631 2022-03-14 14:43:22 +08:00
韦荣超
7161b14631 fix: 项目任务列表甘特图鼠标悬停图标或文案时样式变为手指 2022-03-14 14:41:04 +08:00
韦荣超
2e7ff88be6 fix: 项目任务列表甘特图去掉'已完成'任务及样式优化 2022-03-14 14:34:29 +08:00
韦荣超
6368ee5c12 build: 42106e907d 2022-03-14 12:16:46 +08:00
韦荣超
42106e907d fix: 项目--删除任务详情删除前有聊天记录显示异常修改 2022-03-14 12:14:49 +08:00
韦荣超
3f71162e9d build: 008d8de24e 2022-03-14 09:36:01 +08:00
韦荣超
008d8de24e fix: 修改项目任务列表甘特图时长及颜色显示不对问题 2022-03-14 09:34:30 +08:00
韦荣超
e3000dcc7c build: 15ecd8d592 2022-03-11 19:13:16 +08:00
韦荣超
f98cf4cbfc Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	public/css/app.css
#	public/js/app.js
#	public/js/build/341.js.LICENSE.txt
#	public/js/build/712.js.LICENSE.txt
#	public/js/build/904.js.LICENSE.txt
2022-03-11 19:11:27 +08:00
韦荣超
807f885f92 build: 15ecd8d592 2022-03-11 19:06:27 +08:00
韦荣超
15ecd8d592 feat: 项目任务新增一个甘特图展示选项 2022-03-11 19:04:51 +08:00
mmppkka
588a6f1bf4 build:f633bc81cf874c71f45c7d6d0c3b10aa59316087 2022-03-11 18:34:19 +08:00
mmppkka
f633bc81cf fix:官网首页修改 2022-03-11 18:31:49 +08:00
韦荣超
3006dc6584 build: a7c63e22e5 2022-03-11 18:23:58 +08:00
韦荣超
a7c63e22e5 perf: 已删除任务详情任务描述改为只读 2022-03-11 18:21:52 +08:00
韦荣超
4304158088 build: c1fb67f143 2022-03-11 17:48:32 +08:00
韦荣超
0f446a1f77 Merge remote-tracking branch 'origin/develop' into develop 2022-03-11 17:44:22 +08:00
mmppkka
71639a91b9 build:b0395a6ed22e87064e3e4458b9573e8acaf5679d 2022-03-11 17:44:08 +08:00
韦荣超
c1fb67f143 perf: 已删除任务操作文案及显示优化 2022-03-11 17:42:47 +08:00
mmppkka
b0395a6ed2 fix:修改图片 2022-03-11 17:40:08 +08:00
mmppkka
7d0e6ac50b build:c6ea52482133f98312f6e82bbc624088e3096e03 2022-03-11 17:36:33 +08:00
mmppkka
c6ea524821 fix:首页重写 2022-03-11 17:34:53 +08:00
韦荣超
7c82769448 build: a4b69c3911 2022-03-11 12:08:12 +08:00
韦荣超
a4b69c3911 feat: 客户端登录,新增工作报告、修改工作报告、查看工作报告,全部直接在新窗口打开 2022-03-11 12:06:36 +08:00
韦荣超
e301abe9e3 build: d38ccba618 2022-03-11 10:00:49 +08:00
韦荣超
d38ccba618 perf: 项目--删除任务查看详情页功能 2022-03-11 09:59:29 +08:00
韦荣超
849ac56296 build: 67bb821a0e 2022-03-10 18:59:48 +08:00
韦荣超
67bb821a0e perf: 首页兼容暗黑模式及文案和查询优化 2022-03-10 18:57:53 +08:00
kuaifan
43c51d48d9 合并优化 2022-03-10 17:45:17 +08:00
kuaifan
48b7f4924e perf: 优化消息标记已读/未读 2022-03-10 14:41:29 +08:00
kuaifan
30c149be31 Merge branch 'develop' of ssh://git.gezi.vip:6006/gx/dootask
# Conflicts:
#	electron/package.json
#	package.json
#	public/css/app.css
#	public/js/app.js
#	public/js/build/189.js.LICENSE.txt
#	public/js/build/206.js
#	public/js/build/400.js.LICENSE.txt
#	public/js/build/486.js
#	public/js/build/496.js.LICENSE.txt
#	public/js/build/528.js
#	public/js/build/535.js
#	public/js/build/535.js.LICENSE.txt
#	public/js/build/578.js
#	public/js/build/78.js.LICENSE.txt
#	public/js/build/817.js
#	public/js/build/856.js
#	public/js/build/862.js
#	public/js/build/889.js
#	public/js/build/91.js.LICENSE.txt
#	public/js/build/954.js
#	public/js/build/956.js
#	resources/assets/js/pages/manage/components/TaskDetail.vue
#	resources/assets/js/pages/manage/dashboard.vue
#	resources/assets/sass/pages/common.scss
#	resources/assets/sass/pages/page-dashboard.scss
2022-03-10 11:27:00 +08:00
kuaifan
42d9271ea0 build 2022-03-09 21:26:05 +08:00
韦荣超
00e666e423 build: 6b29b686c7 2022-03-09 18:05:47 +08:00
韦荣超
6b29b686c7 feat: 在项目设置里新增一个“已删除任务”菜单 2022-03-09 18:02:49 +08:00
韦荣超
db99a21514 perf: 修改验证邮件有效期改回24小时 2022-03-09 15:04:04 +08:00
韦荣超
6fb1c474d2 build: f1c7c35e48 2022-03-09 12:07:04 +08:00
韦荣超
f1c7c35e48 perf: 优化邮件设置页面样式 2022-03-09 12:04:24 +08:00
韦荣超
923549bb5e perf: 修改验证邮件有效期为10分钟方便测试(验证完后改为24小时) 2022-03-09 11:51:42 +08:00
韦荣超
0aa18ded72 perf: 【邮箱验证】链接过期的提示文案为:链接已失效,请重新登录/注册 2022-03-09 11:45:11 +08:00
韦荣超
ee08d8d740 perf: 任务修改计划时间需要重置任务邮件提醒日志 2022-03-09 10:45:05 +08:00
韦荣超
0c23fa7c9d build: fe91765ab0 2022-03-09 09:53:34 +08:00
韦荣超
fe91765ab0 fix: 优化验证邮箱页面文案 2022-03-09 09:52:00 +08:00
韦荣超
8541e180cc fix: 修正验证邮箱页面文案 2022-03-09 09:39:22 +08:00
kuaifan
a3d950e2a3 fix: 修复自定义SSO自动升级版本出错的问题 2022-03-09 00:42:00 +08:00
kuaifan
e1c80636ba 显示最后一个加入的成员 2022-03-08 19:32:32 +08:00
kuaifan
4fb971a935 fix: 添加任务时开始时间和结束时间为同一天可能发生报错 2022-03-08 18:42:14 +08:00
kuaifan
117d0fbcef perf: 任务附件过多时仅显示最新50个 2022-03-08 18:34:20 +08:00
kuaifan
389cafc240 优化有时候任务详情标题未显示完全 2022-03-08 18:22:10 +08:00
kuaifan
4591b59465 fix: 拖动任务列表排序后会自动还原的情况 2022-03-08 18:05:16 +08:00
kuaifan
d4a082382d perf: 消息已读/未读人员优化 2022-03-08 16:24:52 +08:00
kuaifan
885437de8d feat: 优化表格分页样式 2022-03-08 16:05:25 +08:00
kuaifan
39cd9f4a44 feat: 优化TableAction组件 2022-03-08 14:36:20 +08:00
kuaifan
01a2244fed feat: 头像加载失败时显示名称首字 2022-03-08 14:11:07 +08:00
韦荣超
e0e3dd128e perf: 消息添加'未读标记'字段 2022-03-08 13:09:12 +08:00
韦荣超
0f6408d7f6 fix: 任务第二次邮件提醒判断错误修改 2022-03-08 12:25:36 +08:00
韦荣超
b610dc4969 build: bc6fa7fd27 2022-03-08 09:08:40 +08:00
韦荣超
bc6fa7fd27 feat: 消息右键对话新增:标记已读、标记未读 2022-03-08 09:06:38 +08:00
kuaifan
0e34cc49df no message 2022-03-08 08:15:34 +08:00
kuaifan
629881d16b no message 2022-03-08 07:05:18 +08:00
kuaifan
92569f5b3a no message 2022-03-08 00:37:17 +08:00
kuaifan
4ff32511b9 perf: 添加任务默认选中自己,如果不选则添加无负责人任务 2022-03-08 00:29:10 +08:00
kuaifan
5afed4b85e perf: 优化消息未读数 2022-03-07 18:07:02 +08:00
kuaifan
2aa687a40b perf: 优化置顶后数据请求 2022-03-07 17:48:34 +08:00
kuaifan
57567ebb1b perf: 优化未读信息数 2022-03-07 17:48:17 +08:00
kuaifan
6448169caa perf: 优化首页仪表盘样式 2022-03-07 17:47:59 +08:00
韦荣超
e5901f28e3 build: 2d3f0dd95a 2022-03-07 17:04:47 +08:00
韦荣超
2d3f0dd95a fix: '最近打开任务'数据没有根据用户区分问题修改 2022-03-07 17:03:29 +08:00
kuaifan
2e4e827887 新增focus方法 2022-03-07 15:24:48 +08:00
kuaifan
22704b32d6 perf: 优化关闭任务独立窗口点击取消后没有自动获取焦点 2022-03-07 15:24:19 +08:00
韦荣超
2839e595cc build: a9f28dfb47 2022-03-07 14:40:23 +08:00
韦荣超
a9f28dfb47 perf: 邮箱验证优化流程提示 2022-03-07 14:38:40 +08:00
韦荣超
c1bdd079cf build: 467d05a358 2022-03-07 11:56:37 +08:00
韦荣超
467d05a358 fix: 去掉调试信息 2022-03-07 11:54:30 +08:00
韦荣超
05c8a6e664 build: 59b2ecebda 2022-03-07 11:51:21 +08:00
韦荣超
59b2ecebda feat: 添加“最近打开的任务” 2022-03-07 11:49:29 +08:00
韦荣超
fe8cf38a7f perf: 任务提醒缩短邮件通知时间区间 2022-03-07 10:22:14 +08:00
kuaifan
e754ceace8 build 2022-03-07 01:42:30 +08:00
kuaifan
41ceb9bd82 perf: 项目、消息对话置顶后要滚动到可以看到它的位置 2022-03-07 01:35:12 +08:00
kuaifan
b0b7c63561 perf: 再次点击滚动到未读条目 2022-03-07 01:19:36 +08:00
kuaifan
c13f65611f perf: 优化新窗口打开的任务保存机制 2022-03-07 01:15:10 +08:00
kuaifan
b9feeabfb3 perf: 优化新消息等列表滚动 2022-03-07 00:35:00 +08:00
kuaifan
8e9ba7a2d2 no msg 2022-03-06 16:10:47 +08:00
kuaifan
b3e9b1c2be perf: 完成任务时任务暂时继续显示,直到路由发生改变 2022-03-06 13:57:56 +08:00
kuaifan
e969b5b7e4 perf: 优化仪表盘使用sticky方式 2022-03-06 13:56:18 +08:00
kuaifan
8e0a684a9a build 2022-03-04 19:45:10 +08:00
kuaifan
152aa0f92b fix: 清空子任务的时间报错闪现的问题 2022-03-04 19:43:54 +08:00
韦荣超
97fb0ba95a perf: 邮箱发送失败提示优化 2022-03-04 18:39:30 +08:00
韦荣超
01355b9a28 去掉查看符合发邮件数据(测试) 2022-03-04 17:55:24 +08:00
韦荣超
772922cefb 查看符合发邮件数据(测试) 2022-03-04 17:51:34 +08:00
kuaifan
95a3aa7290 no message 2022-03-04 17:40:15 +08:00
韦荣超
6f8a4af5eb fix: 任务提醒时间格式错误修改 2022-03-04 17:27:35 +08:00
韦荣超
b332c4e23d perf: 任务提醒时间区间放大,防止定时器出现意外发送不到问题 2022-03-04 16:57:13 +08:00
韦荣超
47c0c3fa3f fix: 任务统计导出完成时间为空时,不应出现实际完成用时 2022-03-04 16:38:08 +08:00
kuaifan
71cb8612d8 fix: 客户端任务独立窗口修改详情后没有同步到主窗口的问题 2022-03-04 16:35:59 +08:00
kuaifan
af12aecd36 perf: 网络不好连续按回车导致重复添加子任务 2022-03-04 16:18:06 +08:00
kuaifan
fec116f131 fix: 客户端任务独立窗口无法按command+s保存任务的问题 2022-03-04 16:17:18 +08:00
kuaifan
d1fdec0970 no message 2022-03-04 16:06:22 +08:00
kuaifan
77b6c53a42 perf: 优化任务详情数据结构 2022-03-04 15:50:38 +08:00
韦荣超
f2eaac50a6 fix: 任务提醒时间区间错误修改 2022-03-04 14:38:20 +08:00
韦荣超
ec8b0ada14 fix: 任务列表缺少参数报错修改 2022-03-04 14:27:45 +08:00
韦荣超
1e28be9671 build: 2975d550eb 2022-03-04 14:02:04 +08:00
韦荣超
8a0e813734 build: 2975d550eb 2022-03-04 14:01:55 +08:00
韦荣超
2975d550eb fix: 导出任务统计修改 2022-03-04 14:00:38 +08:00
韦荣超
248a9c4070 perf: 补全系统设置中的未翻译地方 2022-03-04 10:25:06 +08:00
韦荣超
327d0d20a9 fix: 登录账号密码错误时提示修改 2022-03-04 09:59:55 +08:00
韦荣超
1ffd2812dc perf: 修改导出报表文案 2022-03-04 09:57:54 +08:00
韦荣超
29da4d209d build: 35a369a953 2022-03-04 09:43:41 +08:00
韦荣超
35a369a953 feat: 任务到期提醒开启邮件通知 2022-03-04 09:41:36 +08:00
kuaifan
4785cae8f0 Merge branch 'master' into develop
# Conflicts:
#	electron/package.json
#	package.json
#	public/js/app.js
#	public/js/build/221.js
#	public/js/build/501.js
#	public/js/build/747.js
#	public/js/build/862.js
#	public/js/build/984.js
2022-03-04 08:59:28 +08:00
kuaifan
9eaa575d1a build 2022-03-04 08:53:42 +08:00
kuaifan
996ed78a0e perf: 优化仪表盘角标数 2022-03-04 08:51:22 +08:00
kuaifan
1759572b2e perf: 优化客户端任务详情按command+s保存 2022-03-04 08:50:22 +08:00
kuaifan
0a9f9eea90 build 2022-03-04 00:00:26 +08:00
kuaifan
223fb540b1 perf: 优化文件重命名,支持按esc取消编辑 2022-03-03 23:57:58 +08:00
kuaifan
c5f1c95f7b perf: 任务详情打开操作菜单时按esc任务窗口隐藏了但是菜单还看见 2022-03-03 23:06:54 +08:00
kuaifan
60086ead7c Merge branch 'master' of github.com:kuaifan/dootask into develop
# Conflicts:
#	electron/package.json
#	package.json
#	public/css/app.css
#	public/js/app.js
#	public/js/build/153.js
#	public/js/build/189.js
#	public/js/build/189.js.LICENSE.txt
#	public/js/build/206.js
#	public/js/build/486.js
#	public/js/build/501.js
#	public/js/build/528.js
#	public/js/build/544.js
#	public/js/build/601.js
#	public/js/build/747.js
#	public/js/build/756.js.LICENSE.txt
#	public/js/build/762.js
#	public/js/build/836.js
#	public/js/build/889.js
#	public/js/build/954.js
#	public/js/build/956.js.LICENSE.txt
2022-03-03 16:32:39 +08:00
kuaifan
3bbcbca926 build 2022-03-03 16:21:08 +08:00
kuaifan
63c1deb630 fix: md编辑器出现toc混乱的情况 2022-03-03 16:17:49 +08:00
kuaifan
424e2428fe pref: 上传文件名称过程显示错位的问题 2022-03-03 15:11:50 +08:00
kuaifan
2fdef45156 pref: 退出登录返回登录页而不是注册页 2022-03-03 15:11:34 +08:00
韦荣超
63ac99e906 perf: 报表导出列及剩余天数优化 2022-03-03 15:03:41 +08:00
kuaifan
4cd4550a36 pref: 网络异常的情况下需提示网络异常而不是系统出错 2022-03-03 14:53:01 +08:00
kuaifan
16af625aef no msg 2022-03-03 14:35:21 +08:00
kuaifan
8e72794c07 pref: 任务详情当任务倒计时结束时显示"超期未完成"标签 2022-03-03 14:35:12 +08:00
kuaifan
d9cf6d7e1b pref: 优化脚本,支持部分服务器是docker compose命令 2022-03-03 14:22:25 +08:00
韦荣超
57c4f935e2 fix: 邮箱验证模板页'你'改为'您' 2022-03-03 14:11:31 +08:00
kuaifan
f4e4252227 perf: 优化已读回执 2022-03-03 11:41:54 +08:00
韦荣超
84bbcf7753 fix: 邮箱设置异常报错修改 2022-03-03 11:20:09 +08:00
韦荣超
df129f0d20 build: a76da167eb 2022-03-03 10:49:23 +08:00
韦荣超
a76da167eb feat: 完成邮箱验证 2022-03-03 10:47:40 +08:00
韦荣超
1547b3f53b build: 68f5b30f7d 2022-03-02 18:13:29 +08:00
韦荣超
68f5b30f7d feat: 邮箱验证部分 2022-03-02 18:11:19 +08:00
韦荣超
d9b9c0f42c build: aa0597c27e 2022-03-02 15:46:23 +08:00
韦荣超
aa0597c27e perf: 补全任务'测试'状态样式 2022-03-02 15:45:05 +08:00
韦荣超
4d59cd1521 fix: 报表导出任务没有流程日志判断优化 2022-03-02 15:36:11 +08:00
韦荣超
6ba4170f08 build: 827bc97e8e 2022-03-02 15:21:36 +08:00
韦荣超
827bc97e8e perf: 导出报表调整 2022-03-02 15:19:48 +08:00
kuaifan
7107409b1b no message 2022-03-02 08:46:29 +08:00
kuaifan
6c086fab6f no message 2022-03-02 08:32:38 +08:00
kuaifan
54d215b8d1 Merge branch 'develop' of ssh://git.gezi.vip:6006/gx/dootask 2022-03-02 08:30:33 +08:00
kuaifan
4cd47a5c77 Merge branch 'master' into develop 2022-03-02 08:28:24 +08:00
kuaifan
d027d67e08 pref: 倒计时刚到到达0时会显示自定义才继续显示计时,且未显示超时标签 2022-03-02 08:25:38 +08:00
kuaifan
a7d9e635eb pref: 鼠标滑动至仪表盘中的待完成任务卡片时,卡片周围未显示光晕,且未显示为手指样式 2022-03-02 08:19:09 +08:00
kuaifan
e7196efaea pref: 优化任务详细描述显示 2022-03-02 08:17:30 +08:00
kuaifan
0a2a903c74 fix: 修复登录页设置下拉显示不全的情况 2022-03-02 07:59:53 +08:00
kuaifan
8104c26b19 处理任务简介出现"的情况 2022-03-02 07:48:29 +08:00
韦荣超
420245c630 build: 20cf83907d 2022-03-01 15:54:39 +08:00
韦荣超
20cf83907d fix: 处理回滚后异常代码 2022-03-01 15:52:46 +08:00
韦荣超
0a04d2ee83 fix: 处理回滚后异常代码 2022-03-01 15:47:36 +08:00
韦荣超
bc05781edd build: ce9d91e990 2022-03-01 15:08:02 +08:00
韦荣超
ce9d91e990 fix: 【系统设置】邮件设置提前小时数双向绑定无效问题修改 2022-03-01 15:06:39 +08:00
韦荣超
973d13b705 build: 51e4427108 2022-03-01 14:21:47 +08:00
韦荣超
51e4427108 build: 【系统设置】邮件设置任务提醒时间只能是0.5的倍数 2022-03-01 14:19:44 +08:00
韦荣超
d09ab541ab build: a9e1dc3cd5 2022-03-01 11:04:20 +08:00
韦荣超
a9e1dc3cd5 perf: 仪表盘当前激活的卡片不明显优化 2022-03-01 11:02:28 +08:00
韦荣超
1551e6c640 build: 03a1bdfc7f 2022-03-01 09:39:12 +08:00
韦荣超
03a1bdfc7f perf: 置顶样式优化 2022-03-01 09:37:25 +08:00
韦荣超
24545aa9cb build: cf7e7c1a06 2022-02-28 18:17:10 +08:00
韦荣超
c11426419c build: cf7e7c1a06 2022-02-28 18:17:01 +08:00
韦荣超
cf7e7c1a06 feat: 管理员系统设置新增:新增邮件设置 2022-02-28 18:15:17 +08:00
kuaifan
ab388f1ef5 Merge branch 'master' of github.com:kuaifan/dootask into develop
# Conflicts:
#	public/css/app.css
#	public/js/app.js
#	public/js/build/248.js
#	public/js/build/248.js.LICENSE.txt
#	public/js/build/501.js
#	public/js/build/510.js.LICENSE.txt
#	public/js/build/581.js.LICENSE.txt
#	public/js/build/601.js
#	public/js/build/712.js
#	public/js/build/747.js
#	resources/assets/js/pages/manage/file.vue
#	resources/assets/sass/pages/page-manage.scss
2022-02-28 10:26:12 +08:00
kuaifan
101d5c7eb0 build 2022-02-28 00:23:20 +08:00
kuaifan
cad253b85f feat: 导出任务功能 2022-02-28 00:21:48 +08:00
kuaifan
f17009a988 优化左上角菜单集合 2022-02-27 22:54:48 +08:00
kuaifan
1f76278d2b 优化客户端升级提示 2022-02-27 22:53:54 +08:00
kuaifan
e032d29c91 修复数据库字段填写错误 2022-02-27 21:12:11 +08:00
kuaifan
31d1b0c994 nomsg 2022-02-27 15:03:39 +08:00
kuaifan
611c6d415c perf: 记录任务工作流变化 2022-02-27 14:12:15 +08:00
kuaifan
e3b7ac00fd perf: 优化修改工作流的过程 2022-02-27 14:11:50 +08:00
kuaifan
7c952822db perf: 优化任务排序 2022-02-27 11:06:18 +08:00
kuaifan
b9e435c0e2 perf: 支持nodejs16+ 2022-02-27 10:59:38 +08:00
kuaifan
356d40e640 feat: 文件支持拖动到列表上传 2022-02-25 22:49:56 +08:00
kuaifan
123ffd4e66 no message 2022-02-25 22:47:56 +08:00
kuaifan
c952659620 fix: 修复无法预览pdf文件 2022-02-25 22:15:25 +08:00
韦荣超
ff59f31e84 build: 011fc8cc9e 2022-02-25 15:58:28 +08:00
韦荣超
011fc8cc9e perf: 首页判断是否登录优先于判断是否需要启动首页 2022-02-25 15:57:03 +08:00
韦荣超
7428022b2f build: 3815eb1983 2022-02-25 15:41:32 +08:00
韦荣超
3815eb1983 perf: 优化首页判断时间一闪而过问题 2022-02-25 15:40:17 +08:00
韦荣超
3778f10edb perf: 添加首页文字翻译 2022-02-25 15:31:26 +08:00
韦荣超
00a4b867ea build: 3a1024e848 2022-02-25 15:18:58 +08:00
韦荣超
3a1024e848 fix: 首页360浏览器图片函数摆错修改 2022-02-25 15:17:33 +08:00
kuaifan
ea8e1e9c57 优化样式 2022-02-25 15:14:20 +08:00
kuaifan
478d63893b fix: 无法浏览聊天图片的问题 2022-02-25 14:38:40 +08:00
韦荣超
3066631301 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	app/Http/Controllers/Api/SystemController.php
#	resources/assets/sass/pages/components/dialog-wrapper.scss
2022-02-25 10:57:28 +08:00
韦荣超
b0998d5f95 build: a074cb9664 2022-02-25 10:56:20 +08:00
韦荣超
a074cb9664 fix: 回滚代码后功能完善 2022-02-25 10:54:20 +08:00
韦荣超
43a3d1e5ac build: 24c8a4a9d1 2022-02-25 09:57:25 +08:00
韦荣超
24c8a4a9d1 fix: 项目列表处于置顶选中状态显示选中样式 2022-02-25 09:55:16 +08:00
kuaifan
b5ccba552f build 2022-02-25 09:12:28 +08:00
kuaifan
8d2ee364ba perf: 文件、聊天文件、任务文件预览优化(支持预览drawio、mind等) 2022-02-25 09:11:58 +08:00
kuaifan
14006068c8 优化样式 2022-02-25 00:13:15 +08:00
kuaifan
b4358ffc66 fix: 登录页重复填写sso地址无法保存的问题 2022-02-25 00:09:57 +08:00
kuaifan
33135b1df1 fix: 无法移动共享文件夹内创建的文件 2022-02-25 00:08:32 +08:00
kuaifan
9a66c38e01 fix: 客户端无法编辑office文件 2022-02-25 00:05:35 +08:00
kuaifan
81132ecab0 fix: 客户端无法下载文件 2022-02-25 00:05:12 +08:00
韦荣超
313fc09c8d build: 5610486a89 2022-02-24 18:16:15 +08:00
韦荣超
5610486a89 perf: 调整消息置顶标识位置 2022-02-24 18:14:45 +08:00
韦荣超
9f0231405a build: e650dd0503 2022-02-24 18:12:38 +08:00
韦荣超
e650dd0503 perf: 消息列表详情增加'置顶'标识 2022-02-24 18:10:50 +08:00
韦荣超
6fc6cb75e4 build: 184efc6f27 2022-02-24 17:38:00 +08:00
韦荣超
184efc6f27 fix: 合并异常代码修改 2022-02-24 17:35:55 +08:00
韦荣超
1117b57101 Merge remote-tracking branch 'origin/dev' into develop
# Conflicts:
#	app/Http/Controllers/Api/SystemController.php
#	public/js/language.all.js
#	resources/assets/js/pages/login.vue
#	resources/assets/js/pages/manage.vue
#	resources/assets/js/pages/manage/calendar.vue
#	resources/assets/js/pages/manage/components/Calendar.vue
#	resources/assets/js/pages/manage/setting/system/setting.vue
#	resources/assets/js/store/state.js
#	resources/assets/statics/public/js/language.all.js
2022-02-24 17:18:33 +08:00
韦荣超
0928ba71f2 feat: 首页启动设置 2022-02-24 17:11:05 +08:00
韦荣超
7170eded68 build: 32e891d6ba 2022-02-24 14:38:24 +08:00
韦荣超
32e891d6ba perf: 项目列表置顶优化 2022-02-24 14:37:17 +08:00
韦荣超
5a273676fb build: c3f6edf7c5 2022-02-24 14:03:29 +08:00
韦荣超
c3f6edf7c5 perf: 项目列表置顶优化 2022-02-24 14:02:12 +08:00
韦荣超
e0c0cc8613 build: fbeb3dd81e 2022-02-24 11:50:34 +08:00
韦荣超
fbeb3dd81e perf: 【文件】剪切后加'取消剪切'按钮 2022-02-24 11:48:59 +08:00
kuaifan
9d89af37be build 2022-02-24 09:04:03 +08:00
kuaifan
fbc8a36232 perf: 消息会话右键时隐藏滚动条 2022-02-24 09:03:15 +08:00
kuaifan
ea028ea1a1 perf: 页面高度足够时只滚动项目部分 2022-02-24 09:02:42 +08:00
kuaifan
35b1c12bb5 Merge branch 'develop' of ssh://git.gezi.vip:6006/gx/dootask
# Conflicts:
#	electron/package.json
#	package.json
#	public/js/app.js
2022-02-24 08:07:40 +08:00
kuaifan
fc89d96635 优化drawio不显示添加模板 2022-02-24 00:45:53 +08:00
kuaifan
27129652f2 no message 2022-02-23 23:31:59 +08:00
kuaifan
23ef992a7f 优化创建者和协助人机制 2022-02-23 23:30:33 +08:00
kuaifan
04533e17ec Merge branch 'master' of github.com:kuaifan/dootask into develop
# Conflicts:
#	app/Models/ProjectTask.php
#	public/css/app.css
#	public/js/app.js
#	public/js/build/361.js
#	public/js/build/412.js
#	public/js/build/499.js.LICENSE.txt
#	public/js/build/659.js
#	public/js/build/659.js.LICENSE.txt
#	public/js/build/726.js.LICENSE.txt
#	public/js/build/747.js
2022-02-23 23:18:50 +08:00
kuaifan
7f2fcba542 no message 2022-02-23 23:03:35 +08:00
kuaifan
c115e2f985 build 2022-02-23 22:57:33 +08:00
kuaifan
88642c2003 perf: 客户端版本更新提示关闭 2022-02-23 22:53:04 +08:00
kuaifan
2c678b5363 no message 2022-02-23 22:52:36 +08:00
kuaifan
bc3b72fafe no message 2022-02-23 20:58:04 +08:00
韦荣超
df3b8cf09c perf: 除了任务状态,任务创建人和协助人权限与负责人的保持一致 2022-02-23 17:25:54 +08:00
kuaifan
65393b7809 优化客户端升级提示 2022-02-23 16:45:38 +08:00
韦荣超
9782c849ad build: e7efaed08a 2022-02-23 16:27:57 +08:00
韦荣超
e7efaed08a perf: 议仪表盘添加'待完成任务'选项 2022-02-23 16:26:07 +08:00
kuaifan
337b3e5b5d 优化终端命令 2022-02-23 16:03:54 +08:00
kuaifan
7b1b7d1372 perf: 创建者及协助人可以修改任务但不能修改任务状态 2022-02-23 16:00:54 +08:00
韦荣超
e5b838a2b3 build: f9cc2ceb11 2022-02-23 15:55:41 +08:00
韦荣超
f9cc2ceb11 perf: 优化点击右键时选中框缺少右侧线条 2022-02-23 15:51:24 +08:00
kuaifan
4f107c5618 perf: 客户端修改文件未保存关闭窗口前提示 2022-02-23 15:16:36 +08:00
韦荣超
b1c5aaff43 build: d0a4473e2b 2022-02-23 15:04:21 +08:00
韦荣超
d0a4473e2b fix: 项目、消息置顶样式修改 2022-02-23 15:02:05 +08:00
kuaifan
1c79361094 fix: 修复客户端任务新窗口无法修改任务等级 2022-02-23 14:53:15 +08:00
kuaifan
f72114c223 优化electron通信 2022-02-23 14:52:39 +08:00
kuaifan
dbd59cd958 no message 2022-02-23 12:51:27 +08:00
韦荣超
d65f8a3c82 build: 96587a4e45 2022-02-23 12:09:41 +08:00
韦荣超
96587a4e45 perf: 修改项目及消息置顶样式 2022-02-23 12:06:43 +08:00
韦荣超
76ab47c82e perf: 任务创建人和协助人可修改任务内容和详情,但不可修改任务状态 2022-02-23 11:09:12 +08:00
kuaifan
af8344c555 fix: 修复邮箱大写报错的问题 2022-02-23 10:03:15 +08:00
kuaifan
10ff02b8a0 perf: 修改任务时间日志 2022-02-23 09:58:55 +08:00
kuaifan
cb6cf1e34b 优化drawio 2022-02-23 09:41:19 +08:00
kuaifan
953e924aa2 build 2022-02-23 00:26:44 +08:00
kuaifan
2e0c262d32 no message 2022-02-23 00:19:32 +08:00
kuaifan
cbf2e6a140 no message 2022-02-23 00:07:27 +08:00
kuaifan
49c5a9f621 取消 electron-renderer 2022-02-22 21:02:27 +08:00
kuaifan
fb8f63f305 取消drawio网页版未保存提示 2022-02-22 20:51:07 +08:00
韦荣超
49ff61ad65 build: 695fb60aa4 2022-02-22 17:43:08 +08:00
韦荣超
695fb60aa4 perf: 任务聊天中发送图片时,回车可确定发送 2022-02-22 17:39:22 +08:00
韦荣超
da20fafa39 build: d6a9ecd912 2022-02-22 17:20:47 +08:00
韦荣超
d6a9ecd912 fix: 项目列表滚动'置顶'框隐藏 2022-02-22 17:19:08 +08:00
韦荣超
f95d721c9c build: 69d6417985 2022-02-22 16:51:50 +08:00
韦荣超
69d6417985 feat: 项目列表添加置顶功能 2022-02-22 16:49:53 +08:00
kuaifan
ab2bbc28c8 no message 2022-02-22 14:24:27 +08:00
kuaifan
b0b39429ed build 2022-02-22 12:46:38 +08:00
kuaifan
ff14cbc752 feat: 支持文本、图表、思维导图下载上传 2022-02-22 12:24:16 +08:00
kuaifan
dd013aaaa3 优化drawio 2022-02-21 20:23:15 +08:00
kuaifan
119f61ef67 build 2022-02-21 18:49:38 +08:00
kuaifan
a99588c766 优化drawio路径 2022-02-21 18:11:41 +08:00
kuaifan
6f35fe9936 客户端drawio本地化 2022-02-21 17:46:00 +08:00
kuaifan
3112425e43 build 2022-02-20 21:27:52 +08:00
kuaifan
1ab3aefaa5 perf: 更新流程图表 2022-02-20 17:15:01 +08:00
kuaifan
be08732e6b 修复暗黑模式流程图无法正常浏览的问题 2022-02-19 14:43:11 +08:00
kuaifan
97b58b5f9a build 2022-02-19 14:31:10 +08:00
kuaifan
e4855875cf 优化文件多选部分 2022-02-19 14:29:22 +08:00
kuaifan
7a267cc07b 整理代码 2022-02-19 11:44:03 +08:00
kuaifan
f4f351cf9d Merge branch 'develop' of ssh://git.gezi.vip:6006/gx/dootask
# Conflicts:
#	public/css/app.css
#	public/js/app.js
#	public/js/build/208.js
#	public/js/build/389.js
#	public/js/build/406.js
#	public/js/build/406.js.LICENSE.txt
#	public/js/build/423.js
#	public/js/build/459.js
#	public/js/build/459.js.LICENSE.txt
#	public/js/build/688.js
#	public/js/build/688.js.LICENSE.txt
#	public/js/build/726.js
#	public/js/build/755.js
#	public/js/build/93.js
#	public/js/build/954.js
2022-02-19 09:30:13 +08:00
kuaifan
970b811ab9 no msg 2022-02-19 09:27:30 +08:00
kuaifan
99604dbe35 升级onlyoffice 2022-02-18 19:58:21 +08:00
韦荣超
09b1d89718 build: ccb889233c 2022-02-18 19:20:42 +08:00
韦荣超
9d1a9f3134 build: ccb889233c 2022-02-18 19:20:35 +08:00
韦荣超
ccb889233c fix: 【文件】右键多选所有文件复选框显示,取消所有选中消失 2022-02-18 19:19:05 +08:00
韦荣超
939f2cbf97 build: 4bd8835f23 2022-02-18 18:50:38 +08:00
韦荣超
580e0cb36a build: 4bd8835f23 2022-02-18 18:50:29 +08:00
韦荣超
4bd8835f23 fix: 消息:列表滚动隐藏'置顶'文案 2022-02-18 18:48:43 +08:00
韦荣超
22f32da0c5 build: aa8a094383 2022-02-18 18:42:49 +08:00
韦荣超
aa8a094383 fix: 文件:列表模式右键后已选内容会错乱修复 2022-02-18 18:40:39 +08:00
韦荣超
4a72b7f089 build: a33e4905cf 2022-02-18 17:03:18 +08:00
韦荣超
a33e4905cf fix: 消息:列表滚动右键Y轴值判断错误修复 2022-02-18 17:01:33 +08:00
韦荣超
40bd2f0742 build: 1fd73fe79e 2022-02-18 16:45:55 +08:00
韦荣超
1fd73fe79e fix: 消息:列表滚动在任意位置右键菜单错位问题修复 2022-02-18 16:43:27 +08:00
韦荣超
fd7d3e06f4 build: 7a4d27da69 2022-02-18 16:17:41 +08:00
韦荣超
7a4d27da69 perf: 【文件】多个选择剪切功能与右键剪切重复,数据处理应该合拼;方格列表默认不显示复选框,右键菜单新增一个多选菜单 2022-02-18 16:14:54 +08:00
韦荣超
afbadf7d81 build: 4f2c0e94d9 2022-02-18 14:02:22 +08:00
韦荣超
7b65c64431 build: 4f2c0e94d9 2022-02-18 14:02:00 +08:00
韦荣超
4f2c0e94d9 perf: 优化注册提示 2022-02-18 13:59:40 +08:00
韦荣超
7593d7a3e9 build: 3400d1e803 2022-02-18 11:54:29 +08:00
韦荣超
35ddc4a472 Merge remote-tracking branch 'origin/chao-flow' into develop 2022-02-18 11:52:22 +08:00
韦荣超
3400d1e803 fix: 为引入组件报错 2022-02-18 11:51:11 +08:00
韦荣超
f672428fde build: 1521d1e883 2022-02-18 11:29:33 +08:00
韦荣超
4171d993d0 Merge remote-tracking branch 'origin/chao-flow' into develop
# Conflicts:
#	public/js/app.js
2022-02-18 11:25:49 +08:00
韦荣超
d22310ea31 build: 1521d1e883 2022-02-18 11:23:38 +08:00
韦荣超
1521d1e883 fix: 【文件】流程图只读接入新组件及删除旧组件引入代码 2022-02-18 11:21:56 +08:00
韦荣超
0412615f6e build: 2bd666efe7 2022-02-18 10:11:48 +08:00
韦荣超
2bd666efe7 perf: 消息列表需支持多个置顶 2022-02-18 10:10:17 +08:00
kuaifan
541e1f760b build 2022-02-17 20:39:47 +08:00
kuaifan
f176bed436 隐藏浏览流程图的滚动条(流程图使用拖动浏览) 2022-02-17 19:12:29 +08:00
kuaifan
403545cd9b perf: 没有时间还显示时间倒计时的问题 2022-02-17 19:11:52 +08:00
kuaifan
54e4ed27ae perf: 优化修改任务时间记录 2022-02-17 19:11:23 +08:00
kuaifan
e5ddf5616a perf: 优化文件重命名 2022-02-17 18:48:18 +08:00
kuaifan
967c4f04d9 fix: 修复复制文件内容为空的问题 2022-02-17 18:34:27 +08:00
kuaifan
36d4d445a6 fix: 流程图预览暗黑模式下看不见文字的问题 2022-02-17 17:47:51 +08:00
kuaifan
5ed0ae2fa9 perf: 主任务归档时同步子任务归档 2022-02-17 17:27:11 +08:00
韦荣超
503a719609 build: 2dcbba63cb 2022-02-17 15:22:31 +08:00
韦荣超
2dcbba63cb fix: 【消息】置顶会话在子类tab中排序错误修改 2022-02-17 15:20:47 +08:00
韦荣超
5e86bdfda7 build: 4f74a0440c 2022-02-17 14:43:21 +08:00
韦荣超
4f74a0440c perf: 【注册】校验参数是否合法顺序优化 2022-02-17 14:41:49 +08:00
韦荣超
858709f610 build: 6689d48fad 2022-02-17 10:44:23 +08:00
韦荣超
6689d48fad perf: 【消息】列表置顶会话加背景颜色 2022-02-17 10:42:28 +08:00
韦荣超
8581a7c308 build: 9878efb198 2022-02-17 09:37:08 +08:00
韦荣超
9878efb198 perf: 【消息】列表取消置顶 2022-02-17 09:35:32 +08:00
韦荣超
9855c50367 build: 04c59041e0 2022-02-17 09:19:35 +08:00
韦荣超
04c59041e0 feat: 【消息】列表增加点击右键置顶该聊天功能 2022-02-17 09:17:19 +08:00
kuaifan
7fb1ecc9b0 build 2022-02-15 15:44:43 +08:00
kuaifan
ecd2cdd28e perf: 会员选择输入框不刷新的情况 2022-02-15 15:41:45 +08:00
kuaifan
4b0ad22f8d fix: 取消(完成状态)变为待测试(改变状态),如果有状态负责人应该把状态负责人加上 2022-02-15 15:30:35 +08:00
kuaifan
789558da6c perf: 未完成状态禁止归档 2022-02-15 15:21:11 +08:00
kuaifan
3f96117a57 no message 2022-02-15 15:11:29 +08:00
kuaifan
282d6b5746 升级electron 2022-02-15 15:11:20 +08:00
kuaifan
86d2bb9e2a 升级tinymce 2022-02-15 15:11:00 +08:00
韦荣超
ab4fbf0437 Merge remote-tracking branch 'origin/chao-flow' into develop 2022-02-15 15:09:49 +08:00
韦荣超
ae527a78ef perf: 【文件】流程图新组件添加切换皮肤主题功能 2022-02-15 15:02:32 +08:00
韦荣超
8218868681 Merge remote-tracking branch 'origin/chao-flow' into develop 2022-02-15 14:15:00 +08:00
韦荣超
251f4ca4ac fix: 【文件】流程图新组件添加缺失图片 2022-02-15 14:13:42 +08:00
韦荣超
5d5c8ced24 Merge remote-tracking branch 'origin/chao-flow' into develop 2022-02-15 11:12:20 +08:00
韦荣超
f0ec9b7826 fix: 【文件】流程图新组件复选框在360浏览器中显示异常修改 2022-02-15 11:05:48 +08:00
kuaifan
90b8e785a4 fix: 已完成子任务还出现时间跳动的情况 2022-02-15 10:39:19 +08:00
韦荣超
cb11a71ff5 build: 重新编译 2022-02-15 09:11:14 +08:00
韦荣超
54dd90fc4a Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	public/js/app.js
#	public/js/build/423.js
#	public/js/build/726.js
#	public/js/build/93.js
2022-02-15 09:05:20 +08:00
kuaifan
4bb080bd58 vue data内支持this.$L 2022-02-14 18:59:53 +08:00
韦荣超
5115a048a3 Merge remote-tracking branch 'origin/chao-flow' into develop
# Conflicts:
#	public/css/app.css
#	public/js/app.js
#	public/js/build/423.js
#	public/js/build/93.js
2022-02-14 18:17:09 +08:00
韦荣超
259351cdd2 fix: 【文件】流程图去掉ctr+s提示框 2022-02-14 18:13:16 +08:00
kuaifan
972b8f83bc 添加任务详细描述取消文件上传 2022-02-14 17:37:11 +08:00
kuaifan
9f6b1c1e25 perf: 优化iPad兼容 2022-02-14 17:34:07 +08:00
kuaifan
6229a103aa perf: 优化右下角、登录页主题设置 2022-02-14 17:02:51 +08:00
韦荣超
84116e531b build: 2c1b944b7c 2022-02-14 13:42:45 +08:00
韦荣超
2c1b944b7c fix: 【文件】新版流程图右侧及底部被隐藏问题修改 2022-02-14 13:40:37 +08:00
kuaifan
0e66ca148d build 2022-02-13 14:10:06 +08:00
kuaifan
ce70c1ca3a perf: 单条消息最长2000个字符,超过自动分割发送,总最长20000 2022-02-12 17:01:12 +08:00
韦荣超
56b91a59a2 Merge remote-tracking branch 'origin/develop' into develop 2022-02-12 16:02:04 +08:00
kuaifan
8f1fbcb19e no message 2022-02-12 15:10:30 +08:00
韦荣超
a4799a1bc0 “新版本流程图文件” 2022-02-12 15:04:11 +08:00
韦荣超
cc24ff22e9 perf: 去掉刷新提示及前端报错 2022-02-12 15:02:10 +08:00
kuaifan
72ca335c4c perf: 下载、查看任务文件权限改为所有项目成员 2022-02-12 14:24:36 +08:00
kuaifan
0ef6476e58 perf: 任务详细描述取消文件上传 2022-02-12 14:15:23 +08:00
kuaifan
6ba63d1466 fix: 修复聊天mp4文件无法预览的问题 2022-02-12 14:14:18 +08:00
kuaifan
f9d28e1b6b perf: 优化任务详情拖动发送文件 2022-02-12 13:49:30 +08:00
韦荣超
63534d3eb5 feat: 更新流程图组件 2022-02-12 11:54:56 +08:00
韦荣超
2ab2cf01db “新版本流程图文件” 2022-02-12 11:51:37 +08:00
kuaifan
2fc039dd70 perf: 优化通知 2022-02-12 09:50:02 +08:00
韦荣超
230d1a1f86 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	app/Http/Controllers/Api/FileController.php
#	public/js/build/208.js
#	public/js/build/252.js
#	public/js/build/400.js
#	public/js/build/76.js
2022-02-11 11:01:05 +08:00
kuaifan
f7921bf388 build 2022-02-10 21:18:18 +08:00
kuaifan
feed984ba8 perf: 兼容iPad端 2022-02-10 21:03:36 +08:00
kuaifan
5324861f16 perf: 手动切换账号提示“项目不存在或不在成员列表内”的情况 2022-02-10 20:34:13 +08:00
kuaifan
ae80939d2e perf: 任务详情 2022-02-10 20:33:35 +08:00
kuaifan
c117e4b087 fix: 修复文件上传一直出现loading的情况 2022-02-10 20:33:14 +08:00
kuaifan
96580e2284 perf: 该文件版本已经改变了。该页面将被重新加载 2022-02-10 20:12:17 +08:00
kuaifan
bc3932c8b8 perf: 点击切换语言一级菜单出现的兼容问题 2022-02-10 17:10:18 +08:00
kuaifan
f7cd4f34d3 fix: 已存在的任务新添加负责人不出现在任务群聊里 2022-02-10 17:09:44 +08:00
kuaifan
3a76f51707 fix: 初次安装失败的情况 2022-02-10 16:41:07 +08:00
kuaifan
041ba8f2ed 优化onlyoffice样式 2022-02-09 21:52:04 +08:00
kuaifan
17e5d15b1b build 2022-02-09 21:50:12 +08:00
kuaifan
a6ba59eac0 perf: 上传文件夹 2022-02-09 21:04:18 +08:00
kuaifan
e89ff02b59 fix: 客户端偶尔出现无法打开文件的情况 2022-02-09 21:04:03 +08:00
kuaifan
b28be29dc8 build 2022-02-09 17:29:36 +08:00
kuaifan
9ad85e01de perf: 该文件版本已经改变了。该页面将被重新加载 2022-02-09 17:27:59 +08:00
kuaifan
d49790ba78 perf: 团队管理新增身份筛选项 2022-02-09 17:09:40 +08:00
kuaifan
598f01de95 no message 2022-02-09 16:59:48 +08:00
kuaifan
992e137339 perf: 任务文件支持更多格式上传 2022-02-09 16:59:42 +08:00
kuaifan
124666cca6 优化系统设置路由菜单 2022-02-09 16:13:13 +08:00
kuaifan
848359bf7b feat: 添加项目支持默认工作流 2022-02-09 16:05:25 +08:00
kuaifan
fb24af1900 feat: 新增自定义添加项目时的项目模板 2022-02-09 16:05:02 +08:00
韦荣超
3d6df3cc09 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	app/Http/Controllers/Api/DialogController.php
#	app/Http/Controllers/Api/ProjectController.php
#	app/Models/Project.php
#	electron/package.json
#	package.json
#	public/css/app.css
#	public/js/app.js
#	public/js/build/146.js
#	public/js/build/188.js
#	public/js/build/188.js.LICENSE.txt
#	public/js/build/244.js
#	public/js/build/244.js.LICENSE.txt
#	public/js/build/278.js
#	public/js/build/278.js.LICENSE.txt
#	public/js/build/30.js
#	public/js/build/423.js
#	public/js/build/43.js
#	public/js/build/525.js
#	public/js/build/644.js.LICENSE.txt
#	public/js/build/660.js
#	public/js/build/766.js
#	public/js/build/831.js
#	public/js/build/893.js
#	public/js/build/919.js
#	public/js/build/919.js.LICENSE.txt
#	public/js/build/934.js
#	public/js/build/934.js.LICENSE.txt
#	resources/assets/js/pages/manage/components/DialogList.vue
#	resources/assets/js/pages/manage/components/DialogWrapper.vue
#	resources/assets/js/pages/manage/components/TaskAdd.vue
#	resources/assets/sass/components/report.scss
2022-02-07 10:50:38 +08:00
kuaifan
ea58ed46f2 doc 2022-01-29 16:10:31 +08:00
kuaifan
32817a4275 build 2022-01-29 16:04:28 +08:00
kuaifan
95033cd5b7 perf: 优化项目筛选工作流 2022-01-29 16:02:49 +08:00
kuaifan
9999548bc2 perf: 优化撤回消息 2022-01-29 13:39:16 +08:00
kuaifan
3d04bd4444 build 2022-01-28 18:29:15 +08:00
kuaifan
21e618cca2 优化了一些代码 2022-01-28 18:27:47 +08:00
kuaifan
02eb386155 撤回消息优化 2022-01-28 15:09:57 +08:00
kuaifan
35bd038802 Merge branch 'master' of github.com:kuaifan/dootask into develop
# Conflicts:
#	app/Http/Controllers/Api/DialogController.php
#	app/Http/Controllers/Api/ProjectController.php
#	electron/package.json
#	package.json
#	public/css/app.css
#	public/docs/assets/main.bundle.js
#	public/js/app.js
#	public/js/build/146.js.LICENSE.txt
#	public/js/build/161.js
#	public/js/build/199.js
#	public/js/build/199.js.LICENSE.txt
#	public/js/build/218.js
#	public/js/build/218.js.LICENSE.txt
#	public/js/build/244.js
#	public/js/build/244.js.LICENSE.txt
#	public/js/build/309.js
#	public/js/build/423.js
#	public/js/build/43.js
#	public/js/build/693.js
#	public/js/build/717.js
#	public/js/build/717.js.LICENSE.txt
#	public/js/build/79.js.LICENSE.txt
#	resources/assets/js/pages/manage/components/TaskAdd.vue
2022-01-28 14:39:43 +08:00
kuaifan
1ff59aee56 perf: 优化修改工作流未保存关闭提示 2022-01-28 13:25:51 +08:00
kuaifan
d64fae1832 perf: 查看工作报告图片显示不全的问题 2022-01-28 13:12:11 +08:00
kuaifan
ae169810d0 perf: 优化UserInput组件 2022-01-28 13:04:54 +08:00
kuaifan
3c7619098a build 2022-01-28 01:20:36 +08:00
kuaifan
a881bfd63b fix: 客户端编辑文件不显示协助成员 2022-01-28 01:12:22 +08:00
kuaifan
09d3131d46 fix: 添加任务时选择任务组无效 2022-01-28 00:12:51 +08:00
kuaifan
53550b8975 fix: 工作报告弹窗被遮挡的问题 2022-01-27 23:55:13 +08:00
kuaifan
569164ed56 fix: 客户端任务窗口首次聊天失败的问题 2022-01-27 23:44:06 +08:00
kuaifan
f61d79d53e 客户端office查看修改窗口隐藏自带的标题 2022-01-27 23:28:38 +08:00
kuaifan
bba28d6b57 fix: 文件右键打开失效 2022-01-27 23:17:41 +08:00
Mr.Huan
469f30044a build: 0993d87799 2022-01-27 17:12:58 +08:00
Mr.Huan
0993d87799 fix: 修复工作汇报正文被图片撑开页面的问题 2022-01-27 17:11:48 +08:00
Mr.Huan
09fa33236f build: 4e9abd6512 2022-01-27 16:33:25 +08:00
Mr.Huan
4e9abd6512 perf: 开放文件夹移动功能 2022-01-27 16:31:58 +08:00
Mr.Huan
2996c0b38e build: 36e366abe0 2022-01-27 15:58:26 +08:00
Mr.Huan
36e366abe0 perf: 优化文件模块用户体验 2022-01-27 15:55:44 +08:00
Mr.Huan
f9c6c6c127 build: 34772ef2bf 2022-01-27 14:35:17 +08:00
Mr.Huan
34772ef2bf perf: 文件增加取消选择按钮 2022-01-27 14:33:08 +08:00
Mr.Huan
305af935a7 feat: 文件支持批量移动 2022-01-27 14:24:01 +08:00
kuaifan
33f3c9acbf build 2022-01-27 12:25:10 +08:00
kuaifan
9e560c79ae 优化聊天消息附件支持预览 2022-01-27 12:17:30 +08:00
kuaifan
705d7f3da0 perf: 聊天消息附件支持预览 2022-01-27 00:44:49 +08:00
kuaifan
08372facd7 fix: 移动端无法上传任务文件的问题 2022-01-26 19:11:16 +08:00
kuaifan
886baa427b perf: 仪表盘鼠标滑过时间显示完整时间 2022-01-26 18:12:21 +08:00
kuaifan
8ff94bc138 fix: 任务中没有聊天记录时,发送图片无法成功 2022-01-26 18:05:53 +08:00
Mr.Huan
d0438390cc build: d22266a947 2022-01-26 17:27:42 +08:00
Mr.Huan
d22266a947 perf: 聊天页面图片尺寸缩小至180px 2022-01-26 17:26:29 +08:00
Mr.Huan
e34fb25759 fix: 修复消息撤回文字提示在第一条时会被顶部遮住的问题 2022-01-26 17:23:20 +08:00
Mr.Huan
675955b2e6 build: 4516bce0ee 2022-01-26 17:14:58 +08:00
Mr.Huan
4516bce0ee fix: 修复个人对话为空时无法重复打开该对话的问题 2022-01-26 17:13:20 +08:00
Mr.Huan
4853fbcec3 build: 72e5f9a83e 2022-01-26 15:04:03 +08:00
Mr.Huan
72e5f9a83e fix: 修复消息撤回文字提示在第一条时会被顶部遮住的问题 2022-01-26 15:02:56 +08:00
Mr.Huan
1a1ddc34a2 build: 40ebfb676c 2022-01-26 14:46:37 +08:00
Mr.Huan
40ebfb676c perf: 头像上传图片浏览组件增加空提醒 2022-01-26 14:43:29 +08:00
kuaifan
3bb7e958dc perf: macOS角标首次不显示的问题 2022-01-26 11:46:11 +08:00
Mr.Huan
19dd16fcf0 docs: 补充消息撤回功能API文档 2022-01-26 09:48:35 +08:00
kuaifan
f596749645 Merge branch 'master' of github.com:kuaifan/dootask into develop
# Conflicts:
#	electron/package.json
#	package.json
#	public/css/app.css
#	public/js/app.js
#	public/js/build/120.js
#	public/js/build/120.js.LICENSE.txt
#	public/js/build/146.js.LICENSE.txt
#	public/js/build/161.js
#	public/js/build/284.js
#	public/js/build/309.js
#	public/js/build/400.js
#	public/js/build/644.js
#	public/js/build/644.js.LICENSE.txt
#	public/js/build/79.js.LICENSE.txt
#	resources/assets/js/pages/manage/components/TaskAdd.vue
2022-01-26 09:35:16 +08:00
kuaifan
482813ea88 build 2022-01-26 01:33:53 +08:00
kuaifan
60e47e85a3 perf: 添加任务时如果自己不是任务负责人可选择加入协助人员列表 2022-01-26 01:12:00 +08:00
kuaifan
22c06fee5e 剔除模式代码优化 2022-01-26 00:02:15 +08:00
kuaifan
4a42ce87a2 feat: 工作流程负责人新增剔除模式(改变任务负责人并保留操作状态的人员) 2022-01-25 23:46:47 +08:00
kuaifan
a98e4dbcd4 perf: 文件列表模式右键重命名无效 2022-01-25 23:12:42 +08:00
kuaifan
7baa37ccd1 fix: macOS客户端首次不加载角标的问题 2022-01-25 23:06:08 +08:00
kuaifan
417017add9 优化编辑器皮肤 2022-01-25 23:04:51 +08:00
Mr.Huan
b5e3cc2503 build: 2e11fe2b58 2022-01-25 16:51:32 +08:00
Mr.Huan
2e11fe2b58 feat: 文件网格模式支持批量删除文件 2022-01-25 16:49:54 +08:00
Mr.Huan
70f1258bab feat: 文件表格支持批量删除文件 2022-01-25 16:49:54 +08:00
韦荣超
d91fa33330 fix: 工作流列表接口用作筛选时不用传多余参数 2022-01-25 16:28:15 +08:00
kuaifan
8f0b5dc049 Merge branch 'master' of github.com:kuaifan/dootask into develop
# Conflicts:
#	app/Http/Controllers/Api/DialogController.php
#	electron/package.json
#	package.json
#	public/css/app.css
#	public/js/app.js
#	public/js/build/146.js
#	public/js/build/146.js.LICENSE.txt
#	public/js/build/178.js
#	public/js/build/178.js.LICENSE.txt
#	public/js/build/199.js
#	public/js/build/199.js.LICENSE.txt
#	public/js/build/309.js
#	public/js/build/328.js.LICENSE.txt
#	public/js/build/388.js
#	public/js/build/43.js
#	public/js/build/46.js.LICENSE.txt
#	public/js/build/857.js
#	public/js/build/857.js.LICENSE.txt
#	public/js/build/893.js
2022-01-25 16:12:53 +08:00
kuaifan
7f14a82053 perf: 任务群聊中拖拽文件或者照片时并未有确认窗口 2022-01-25 16:06:26 +08:00
kuaifan
b05db6d458 fix: 主任务负责人无法修改子任务负责人的问题 2022-01-25 15:49:22 +08:00
kuaifan
ea87092d73 fix: 设置主任务时间跟子任务没有交集时子任务时间改为跟主任务一致 2022-01-25 15:28:42 +08:00
kuaifan
f468f7ae27 build 2022-01-25 15:05:46 +08:00
kuaifan
f12c79d292 pref: 待处理时进入动态中点击重置,重置提示中的参数为空,点击确定后,提示流程不存在或者删除 2022-01-25 15:04:03 +08:00
kuaifan
6e76514a24 pref: 添加子任务时输入框为空,回车时提示的图标错误 2022-01-25 15:03:43 +08:00
kuaifan
88f3d3821e fix: 搜索后,点击查看已归档的任务,仍然显示这个搜索框 2022-01-25 15:03:24 +08:00
kuaifan
bed2e20a91 fix: 修复归档任务无法查看日志的问题 2022-01-25 14:50:39 +08:00
kuaifan
c7700bdfef perf: 上传文件夹应该保持目录结构 2022-01-25 14:46:52 +08:00
Mr.Huan
778f6367f9 fix: 动态中点击重置按钮后流程的字体颜色和背景颜色并未正确显示 2022-01-25 11:00:59 +08:00
Mr.Huan
6115eea401 build: ff62ef729a 2022-01-25 09:39:03 +08:00
Mr.Huan
ff62ef729a perf: 支持xmind,rp格式文件上传 2022-01-25 09:37:45 +08:00
kuaifan
d0a432164d build 2022-01-25 02:06:27 +08:00
kuaifan
0f8d9e64ef no message 2022-01-25 01:30:55 +08:00
kuaifan
d1f5096a16 删除luckysheet 2022-01-25 00:32:51 +08:00
kuaifan
003de399dc 支持在线编辑代码 2022-01-25 00:24:47 +08:00
kuaifan
36989ce5ff feat: 新增文本文件编辑功能 2022-01-24 20:33:27 +08:00
Mr.Huan
fa987b4e30 build: b47c494240 2022-01-24 15:50:32 +08:00
Mr.Huan
b47c494240 fix: 修复任务负责人选择弹窗不随页面滚动的问题 2022-01-24 15:35:04 +08:00
kuaifan
1ab8a19f8e fix: 客户端无法下载聊天文件的问题 2022-01-24 15:18:35 +08:00
Mr.Huan
a31134195b build: 5580a4ee3d 2022-01-24 15:08:38 +08:00
Mr.Huan
5580a4ee3d fix: 修复添加任务变更栏目后无效果的问题 2022-01-24 15:07:01 +08:00
kuaifan
870276fa48 feat: 支持文件下载 2022-01-24 14:42:29 +08:00
Mr.Huan
608660a101 build: 25cb8015d0 2022-01-24 11:56:07 +08:00
Mr.Huan
25cb8015d0 perf: 注册页面增加密码格式提醒 2022-01-24 11:54:47 +08:00
Mr.Huan
e686e8f58c build: 14b699b5b1 2022-01-24 10:39:13 +08:00
Mr.Huan
14b699b5b1 perf: 消息撤回后直接删除消息 2022-01-24 10:37:18 +08:00
韦荣超
ce0c86c8e5 build: 11d1f26724 2022-01-24 10:22:45 +08:00
韦荣超
11d1f26724 feat: 加入【项目管理】任务列表流程筛选被回滚功能 2022-01-24 10:20:59 +08:00
Mr.Huan
a8c0978f0d build: 0a0227cca4 2022-01-24 09:31:52 +08:00
Mr.Huan
f643911014 feat: 聊天消息撤回-后端实现 2022-01-24 09:30:10 +08:00
Mr.Huan
0a0227cca4 feat: 聊天消息撤回-前端实现 2022-01-24 09:30:10 +08:00
kuaifan
8bacc3b6ba Merge branch 'master' of github.com:kuaifan/dootask into develop
# Conflicts:
#	electron/package.json
#	package.json
#	public/js/app.js
#	public/js/build/161.js
#	public/js/build/400.js
#	public/js/build/616.js
#	public/js/build/845.js
2022-01-24 01:45:08 +08:00
kuaifan
d516330a41 feat: 聊天粘贴发送文件、图片时预览确认 2022-01-24 01:30:10 +08:00
kuaifan
2d83faf144 build 2022-01-24 00:42:58 +08:00
kuaifan
c3fd6bf88f 优化日志显示 2022-01-24 00:40:14 +08:00
kuaifan
2a646becfd 优化项目简介样式 2022-01-24 00:33:31 +08:00
kuaifan
a825657516 fix: 修改主任务时间时未设置时间的子任务没有同步修改 2022-01-24 00:10:47 +08:00
kuaifan
b71dbe9832 perf: 任务详情窗口截止时间24小时倒计时 2022-01-24 00:05:36 +08:00
kuaifan
f06b4040bc 支持查看已归档任务 2022-01-23 23:55:56 +08:00
kuaifan
563cd4b843 优化已归档任务列表 2022-01-23 23:44:39 +08:00
kuaifan
834dc9bec9 支持查看已归档任务 2022-01-23 23:33:16 +08:00
kuaifan
e6e58a03a6 perf: 修改任务或修改项目后同步对话信息 2022-01-23 17:58:32 +08:00
kuaifan
d9f4adbe26 支持查看归档任务 2022-01-23 17:43:15 +08:00
kuaifan
1777153411 perf: 支持查看已归档任务详情 2022-01-23 17:36:19 +08:00
kuaifan
9b0ca581f1 perf: 消息列表显示任务基本状态 2022-01-23 16:17:11 +08:00
kuaifan
493cf7d46a perf: 优化消息对话排序 2022-01-23 15:01:58 +08:00
kuaifan
004bf36dc1 perf: 隐藏无聊天内容的对话 2022-01-23 14:51:32 +08:00
kuaifan
fcf7fb4b9f perf: 消息页再次点击类型定位到未读消息 2022-01-23 14:48:10 +08:00
kuaifan
130a85a5fc fix: flow文件路径引用错误导致无法编译的问题 2022-01-23 13:16:25 +08:00
Mr.Huan
d46eec568c build: 6529e45868 2022-01-22 15:39:25 +08:00
Mr.Huan
7dc6d0ffb2 build: 6529e45868 2022-01-22 15:28:37 +08:00
Mr.Huan
6529e45868 fix: 修复工作汇报选择接收人组件在调整窗体大小时发生位移的问题 2022-01-22 15:26:42 +08:00
Mr.Huan
3ba6ea9c7e perf: 调整周报签名的生成方法 2022-01-22 15:07:37 +08:00
kuaifan
b6ff6f5453 Merge branch 'master' of github.com:kuaifan/dootask into develop
# Conflicts:
#	public/js/app.js
#	public/js/build/657.js
#	public/js/build/782.js
#	public/js/build/845.js
2022-01-22 14:18:32 +08:00
Mr.Huan
cfa529b742 build: 2eabc76c1a 2022-01-22 13:53:13 +08:00
Mr.Huan
2eabc76c1a fix: 修复周报点击编辑关闭后再点击"新建汇报"按钮导致用户列表无法重新加载的问题 2022-01-22 13:51:03 +08:00
韦荣超
463e67d64c fix:【工作汇报】修复新增今天的周报,提示要覆盖昨天日报的问题 2022-01-22 12:06:21 +08:00
韦荣超
1f96af1024 build: 563aa92958 2022-01-22 10:37:27 +08:00
韦荣超
563aa92958 fix: 【工作汇报】收到的汇报中,未读时发送人删除掉汇报对象,汇报对象的未读数量仍然未减少 2022-01-22 10:35:45 +08:00
kuaifan
f09b864e30 perf: 仅客户端或Chrome浏览器支持主题功能 2022-01-22 09:07:13 +08:00
kuaifan
7ce15d5a71 优化代码 2022-01-21 23:31:01 +08:00
kuaifan
cc767b164e 优化工作报告 2022-01-21 23:16:50 +08:00
kuaifan
230ff75c0b 优化搜索栏前端 2022-01-21 21:56:27 +08:00
kuaifan
da39739fb5 优化工作报告前端 2022-01-21 21:49:27 +08:00
kuaifan
c33be22057 Merge branch 'develop' of ssh://git.gezi.vip:6006/gx/dootask into develop
# Conflicts:
#	resources/assets/js/pages/manage/components/ReportDetail.vue
#	resources/assets/js/pages/manage/components/ReportEdit.vue
2022-01-21 21:04:28 +08:00
kuaifan
902f35d21b 优化前端代码 2022-01-21 21:02:17 +08:00
韦荣超
d8426780d2 build: a1aaf90d2e 2022-01-21 21:01:08 +08:00
韦荣超
a1aaf90d2e fix: 【工作报告】新增报告初始化数据不导入已有时间点数据 2022-01-21 20:59:28 +08:00
韦荣超
53ed4d4072 build: da75554447 2022-01-21 19:59:44 +08:00
韦荣超
da75554447 fix: 【工作报告】详情页面前端报错修改 2022-01-21 19:58:03 +08:00
韦荣超
04998d6a60 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	public/js/app.js
2022-01-21 18:10:53 +08:00
韦荣超
3d88a28465 build: 2dd8f75d52 2022-01-21 18:10:24 +08:00
韦荣超
2dd8f75d52 fix: 【工作报告】填写内容--汇报类型切换提交提示错误问题 2022-01-21 18:08:48 +08:00
kuaifan
7a4bdd0ada no message 2022-01-21 17:59:08 +08:00
韦荣超
0ed0b5ee43 build: 2475ee90ee 2022-01-21 17:41:09 +08:00
韦荣超
2475ee90ee perf: 工作报告优化 2022-01-21 17:39:10 +08:00
kuaifan
511c19d5aa 优化前端代码 2022-01-21 14:49:20 +08:00
韦荣超
c4fed86f1e Merge remote-tracking branch 'origin/develop' into develop 2022-01-21 14:26:32 +08:00
韦荣超
18904ebbaa perf: 规范代码 2022-01-21 14:26:17 +08:00
kuaifan
da3672e6be 优化前端代码 2022-01-21 14:24:31 +08:00
kuaifan
52e9836bbf build 2022-01-21 14:07:14 +08:00
kuaifan
3201d90a53 格式化代码 2022-01-21 14:07:02 +08:00
kuaifan
cd9c1d9660 Merge branch 'master' of github.com:kuaifan/dootask into develop 2022-01-21 13:54:58 +08:00
kuaifan
0f54a1f638 no message 2022-01-21 13:50:41 +08:00
kuaifan
713316f87c 优化暗黑皮肤 2022-01-21 13:50:23 +08:00
kuaifan
88d38ba8d1 fix: 发送图片显示错误 2022-01-21 13:45:33 +08:00
kuaifan
164f3275f4 优化暗黑皮肤 2022-01-21 13:13:07 +08:00
韦荣超
a44d6b8b79 build: 942cf57c36 2022-01-21 12:10:48 +08:00
韦荣超
942cf57c36 perf: 工作报告优化 2022-01-21 12:06:26 +08:00
kuaifan
18bc75242b fix: 项目负责人还原已经回档的自己不是任务负责人的任务时,无法还原,且会提示错误 2022-01-21 11:10:29 +08:00
kuaifan
3b38c8b408 perf: 共享文件删除、移动改为仅限所有者或创建者操作 2022-01-21 10:00:31 +08:00
kuaifan
22e718423e 优化暗黑模式 2022-01-21 01:38:05 +08:00
kuaifan
0dc0e7226e 调整主题色 2022-01-20 22:59:51 +08:00
kuaifan
8069784198 no message 2022-01-20 00:38:30 +08:00
kuaifan
a9e71567fe Merge branch 'master' of github.com:kuaifan/dootask into develop
# Conflicts:
#	electron/package.json
#	package.json
#	public/css/app.css
#	public/js/app.js
#	public/js/build/146.js
#	public/js/build/169.js
#	public/js/build/631.js
#	public/js/build/679.js
#	public/js/build/700.js
#	public/js/build/728.js
#	public/js/build/802.js
#	public/js/build/823.js
#	public/js/build/954.js
#	resources/assets/js/pages/manage.vue
2022-01-20 00:33:05 +08:00
kuaifan
0fae151731 build 2022-01-20 00:20:06 +08:00
kuaifan
3bb0b55955 feat: 添加暗黑模式 2022-01-20 00:19:00 +08:00
kuaifan
071ca80bae fix: 无法读取 /www/.env:没有那个文件或目录 2022-01-18 22:52:43 +08:00
kuaifan
1abd78305f Merge branch 'master' of github.com:kuaifan/dootask into develop
# Conflicts:
#	public/css/app.css
#	public/js/app.js
#	public/js/build/120.js
#	public/js/build/120.js.LICENSE.txt
#	public/js/build/198.js
#	public/js/build/494.js
#	public/js/build/79.js
#	public/js/build/793.js
#	public/js/build/875.js
#	public/js/build/890.js
#	resources/assets/js/components/DrawerOverlay.vue
2022-01-17 17:13:12 +08:00
kuaifan
e1ef9a94af no message 2022-01-17 00:36:19 +08:00
kuaifan
9f62023175 build 2022-01-17 00:12:34 +08:00
kuaifan
c9a6d2f5a8 perf: 优化安装脚本 2022-01-16 22:03:50 +08:00
kuaifan
07fca8b895 fix: 最大只能上传64M的问题 2022-01-16 14:53:35 +08:00
kuaifan
1a65c4a579 修复客户端 2022-01-16 01:11:19 +08:00
kuaifan
7a28a8950c 修复前端编译 2022-01-16 00:43:05 +08:00
kuaifan
89e8b0d8b9 perf: 新增工作流初始化数据 2022-01-16 00:05:49 +08:00
kuaifan
74803df5bd no message 2022-01-15 23:37:29 +08:00
kuaifan
2a8e030fb4 perf: 优化客户端自动更新 2022-01-15 23:36:21 +08:00
kuaifan
c6ebe994cc build 2022-01-15 12:53:21 +08:00
kuaifan
be3677cfa8 优化DrawerOverlay组件支持底部显示动态改变尺寸 2022-01-15 12:16:50 +08:00
kuaifan
0c2e56271b 优化DrawerOverlay组件 2022-01-15 00:06:40 +08:00
kuaifan
b019d40009 fix: websocket获取链接失败 2022-01-14 23:15:59 +08:00
kuaifan
361484be95 build 2022-01-14 18:12:33 +08:00
kuaifan
f0ce6cc28f build 2022-01-14 17:59:07 +08:00
kuaifan
ede298a142 perf: 工作汇报样式 2022-01-14 17:57:51 +08:00
kuaifan
aca1a4d34c 优化ws返回的地址 2022-01-14 16:56:45 +08:00
韦荣超
e63c5c074c build: 0d0b5dd552 2022-01-14 16:46:30 +08:00
韦荣超
0d0b5dd552 fix: 【工作报告】填写'提交'按钮位置被挤压修改 2022-01-14 16:43:13 +08:00
kuaifan
08704e7f60 优化报错 2022-01-14 16:35:00 +08:00
韦荣超
d48ed18102 feat: 【工作报告】功能 2022-01-14 15:55:26 +08:00
kuaifan
3602acd187 build 2022-01-14 14:05:09 +08:00
kuaifan
ad477eb608 优化文案 2022-01-14 14:01:56 +08:00
kuaifan
0a393845e9 优化任务详情窗口聊天 2022-01-14 13:52:10 +08:00
kuaifan
59b98209ac 优化websocket重连后的逻辑 2022-01-14 13:46:22 +08:00
kuaifan
ac26713f86 优化查询代码 2022-01-14 12:19:50 +08:00
kuaifan
9250ef6f65 fix: 查看日历页面部分任务会不见 2022-01-14 10:23:43 +08:00
kuaifan
cabbeb07d0 优化websocket重连后的逻辑 2022-01-14 10:23:23 +08:00
kuaifan
43171645c0 build 2022-01-14 00:04:28 +08:00
kuaifan
79c1484288 新增优化websocket重连后刷新项目、对话、仪表盘数据 2022-01-13 23:59:38 +08:00
kuaifan
e730875db3 perf: 客户端更新提示 2022-01-13 23:55:07 +08:00
kuaifan
18e9fb99b5 更新nginx持续握手时间 2022-01-13 23:53:29 +08:00
kuaifan
fd82b9b555 build 2022-01-13 19:42:41 +08:00
kuaifan
5c3bf0067e 优化日志显示 2022-01-13 19:19:58 +08:00
kuaifan
ac6b90c986 fix: 任务流转会出错 2022-01-13 18:39:57 +08:00
kuaifan
7dcd8b29fc build 2022-01-13 11:50:54 +08:00
kuaifan
d951da2c02 fix: 修复客户端旧版本更新任务出现已完成列表中有状态为“进行中”的情况 2022-01-13 11:48:33 +08:00
kuaifan
605aee35c4 perf: 发送文件ws返回错误地址 2022-01-13 11:42:01 +08:00
kuaifan
dc1f1985e8 访问接口header加上version、platform 2022-01-13 11:22:46 +08:00
kuaifan
cebcc26baf 优化项目日志 2022-01-13 11:11:29 +08:00
kuaifan
75a12fc6f5 优化日志 2022-01-13 09:00:39 +08:00
kuaifan
a3e0d89eb0 修改任务状态判断是否项目负责人直接查表 2022-01-13 00:32:35 +08:00
kuaifan
9594cc674f no message 2022-01-13 00:30:36 +08:00
kuaifan
87cdee4fe8 pref: 优化任务日志的显示 2022-01-13 00:30:36 +08:00
kuaifan
1df55b78c2 perf: 项目页面点击搜索按钮再点击弹出的搜索框窗会隐藏 2022-01-13 00:30:36 +08:00
kuaifan
f7820a23be no message 2022-01-12 16:34:16 +08:00
kuaifan
f80b9cdca5 feat: 工作流程状态支持仅限状态负责人修改 2022-01-12 16:16:04 +08:00
kuaifan
f2180b22c7 build 2022-01-11 23:23:44 +08:00
kuaifan
a745088213 perf: 任务日志刷新 2022-01-11 23:17:59 +08:00
kuaifan
14be7bd2b1 no msg 2022-01-11 22:58:30 +08:00
kuaifan
d453ea66da 优化登录页按钮 2022-01-11 21:31:23 +08:00
kuaifan
e7ae86e261 feat: 任务流转自动负责人支持转让模式 2022-01-11 20:07:58 +08:00
kuaifan
b7417f41c5 perf: action中错误提醒弱化 2022-01-11 17:54:42 +08:00
kuaifan
9e7aa381ed perf: 手机版无法选择日期范围的问题 2022-01-11 17:47:34 +08:00
kuaifan
b02453e9f4 perf: 手机版无法聊天,输入页面跳动 2022-01-11 17:35:53 +08:00
kuaifan
6b7f8fd31c 优化完成任务标记 2022-01-11 14:18:49 +08:00
kuaifan
4652242d6b build 2022-01-11 12:16:01 +08:00
kuaifan
a47ca2f357 perf: 个人设置显示版本号 2022-01-11 12:13:26 +08:00
kuaifan
e5a1e58159 perf: 完成任务暂时继续显示在我的列表 2022-01-11 12:13:06 +08:00
kuaifan
01a1e34e99 no message 2022-01-11 10:59:47 +08:00
kuaifan
e711220a66 优化自定义服务器 2022-01-11 10:31:37 +08:00
kuaifan
1d20f529a0 no message 2022-01-11 00:28:58 +08:00
kuaifan
79a94d25bd perf: 客户端自动下载新版本更新 2022-01-10 19:27:43 +08:00
kuaifan
92793b8ff8 no message 2022-01-10 00:25:36 +08:00
kuaifan
982024f359 子任务时间在主任务时间内 2022-01-09 20:13:59 +08:00
kuaifan
e851bd4d61 子任务时间大于主任务时主任务自动修改 2022-01-09 18:56:30 +08:00
kuaifan
e4b9383e96 perf: 设置子任务时间时,如果主任务没有时间则自动设置 2022-01-09 18:20:36 +08:00
kuaifan
95ac9aac14 添加任务自动上工作流状态 2022-01-09 17:59:08 +08:00
kuaifan
b895eec69c 初步完成工作流 2022-01-09 17:52:46 +08:00
kuaifan
1fe4e80f82 no msg 2022-01-09 12:56:55 +08:00
kuaifan
eb1f5f2632 perf: 任务操作菜单组件化 2022-01-09 00:09:43 +08:00
kuaifan
114b792300 no message 2022-01-08 21:23:47 +08:00
kuaifan
56a352a9d2 perf: 调整仅项目负责人可以删除任务列表 2022-01-08 17:35:25 +08:00
kuaifan
4c4d60dd83 no message 2022-01-08 17:32:03 +08:00
kuaifan
134ab0fe98 no message 2022-01-08 17:27:30 +08:00
kuaifan
bbd394272f feat: 添加工作流 2022-01-08 17:24:48 +08:00
kuaifan
8a2571f514 no msg 2022-01-08 08:56:29 +08:00
kuaifan
bf15be8144 perf: 领取任务需要设置计划时间 2022-01-08 08:51:34 +08:00
kuaifan
bf6f8de7fa no msg 2022-01-08 00:35:01 +08:00
kuaifan
2e75e6ffb6 后台限制任务负责人、协助人员每项最多不能超过10个 2022-01-07 23:36:05 +08:00
kuaifan
b987be54bf 优化前端变量 2022-01-07 23:28:07 +08:00
kuaifan
533e8a3742 build 2022-01-07 19:12:38 +08:00
kuaifan
9d007e64f6 优化部分相同代码 2022-01-07 18:42:04 +08:00
kuaifan
8ec1578f50 左边底部新增添加任务选项 2022-01-07 18:23:30 +08:00
kuaifan
4e8cc36d3a 显示完成时间 2022-01-07 18:09:22 +08:00
kuaifan
5a64cb2323 替换Tooltip改成ETooltip 2022-01-07 18:09:05 +08:00
kuaifan
b2aba82a1b perf: 任务版本如果只有一个负责人时显示负责人的名字 2022-01-07 17:46:03 +08:00
kuaifan
9d9500ba1b fix: 其他人员添加任务会临时出现在自己的列表中 2022-01-07 17:26:22 +08:00
kuaifan
674c5a11c1 客户端pkg包安装前必须关闭已打开的应用 2022-01-07 10:56:07 +08:00
kuaifan
8e2b2947ae 版本号基于master分支生成 2022-01-07 10:16:35 +08:00
kuaifan
0413ca7cba 新增pkg安装包 2022-01-07 10:14:49 +08:00
kuaifan
15cf7800a4 日历选时间快速添加任务 2022-01-07 01:07:41 +08:00
kuaifan
db6114a4ee pref: 日历任务缓存 2022-01-07 00:56:03 +08:00
kuaifan
818495a697 build 2022-01-06 23:25:04 +08:00
kuaifan
7aa41c4050 no msg 2022-01-06 23:22:03 +08:00
kuaifan
1975b8af1d perf: 日历选择时间添加任务 2022-01-06 23:15:04 +08:00
kuaifan
b870f5f4d1 no msg 2022-01-06 22:24:45 +08:00
kuaifan
3ebc720934 no message 2022-01-06 20:24:31 +08:00
kuaifan
748976f393 feat: 创建任务窗口添加小时钟 2022-01-06 20:12:13 +08:00
kuaifan
83e5c7fadb 优化注释 2022-01-06 19:22:29 +08:00
kuaifan
f919a34166 优化任务、列表数量限制 2022-01-06 19:12:49 +08:00
kuaifan
77a9eca634 no msg 2022-01-06 18:56:50 +08:00
kuaifan
72b732a55d perf: 归档任务可以搜索 2022-01-06 18:53:34 +08:00
kuaifan
0236897d1f no msg 2022-01-06 17:17:23 +08:00
kuaifan
46f95e5e13 no msg 2022-01-06 16:57:13 +08:00
kuaifan
24287c0857 fix: 主任务被删除或归档时子任务应该也同步 2022-01-06 16:51:32 +08:00
kuaifan
c930e4dd92 perf: 归档任务列表显示完成时间 2022-01-06 16:30:45 +08:00
kuaifan
7d8856e4bc no msg 2022-01-06 15:41:58 +08:00
kuaifan
6d026bbf42 no message 2022-01-06 15:24:54 +08:00
kuaifan
713770a448 no message 2022-01-06 15:09:41 +08:00
kuaifan
ac873fa757 no msg 2022-01-06 14:40:25 +08:00
kuaifan
6b3513c1c4 feat: 限制个人最多500个未完成任务 2022-01-06 14:40:19 +08:00
kuaifan
840374c48c feat: 限制项目最多100个人参与 2022-01-06 14:39:56 +08:00
kuaifan
7f454f279c perf: 项目版面成员显示 2022-01-06 14:28:23 +08:00
kuaifan
267c09f20c no msg 2022-01-06 13:38:49 +08:00
kuaifan
e44e77a3a6 pref: 子任务默认起始时间与主任务一致 2022-01-06 13:28:47 +08:00
kuaifan
4d9dd13ffb pref: 禁用会员记录禁用时间 2022-01-06 13:28:10 +08:00
kuaifan
369e75cb7e perf: 安装项目时支持自定义端口 2022-01-06 11:33:51 +08:00
kuaifan
0a6fa2431e perf: 任务详情窗口光标在任务描述可使用ctrl+s(command+s)保存 2022-01-06 08:27:32 +08:00
kuaifan
0bc40d1748 perf: 文件快捷键保存 2022-01-06 08:17:37 +08:00
kuaifan
919dc3cdea feat: 思维导图支持无极缩放 2022-01-06 08:08:40 +08:00
kuaifan
7c64b27ef4 feat: 自动归档已完成任务 2022-01-06 01:19:44 +08:00
kuaifan
369577a2c8 no message 2022-01-06 00:32:27 +08:00
kuaifan
fbc25e5134 no message 2022-01-05 23:50:29 +08:00
kuaifan
aba1628d36 no message 2022-01-05 22:51:02 +08:00
kuaifan
90336e1edf 整理$A 2022-01-05 20:19:27 +08:00
kuaifan
dde32fcaee no message 2022-01-05 19:49:47 +08:00
kuaifan
6324e79aba no message 2022-01-05 15:04:55 +08:00
kuaifan
7a22f4b20f no message 2022-01-05 14:56:11 +08:00
kuaifan
aad621bd84 no message 2022-01-05 14:17:45 +08:00
kuaifan
0b4e1f3dee no message 2022-01-05 13:41:05 +08:00
kuaifan
78f88db560 将所有任务都取出全部显示,子任务判断显示主任务 2022-01-05 09:30:00 +08:00
kuaifan
cc1c425ecf no message 2022-01-04 23:42:29 +08:00
kuaifan
186290e355 未完成项目概况 2022-01-04 20:23:01 +08:00
kuaifan
f1f1d784ff 修复我的任务数据 2022-01-01 15:03:33 +08:00
623585344@qq.com
320f183b49 add:新增暗黑皮肤样式 2021-12-31 16:59:42 +08:00
kuaifan
a1c7efeb85 任务数据逻辑 2021-12-31 14:53:57 +08:00
kuaifan
ba5f635687 子任务默认时间与主任务相同 2021-12-31 11:07:44 +08:00
kuaifan
bae5afc0da no message 2021-12-31 09:59:11 +08:00
kuaifan
befd5c3b08 去除taskSubs 2021-12-31 07:48:55 +08:00
kuaifan
0b4e96a90f build 2021-12-30 15:35:16 +08:00
kuaifan
57edf38c1a fix: 任务有负责人后仅限项目或任务负责人修改 2021-12-30 15:32:36 +08:00
kuaifan
502dd1ec1f perf: 未被领取的任务标记完成之前先领取 2021-12-30 14:39:12 +08:00
kuaifan
02361ddfb2 perf: 快速添加任务0天的描述 2021-12-30 14:31:23 +08:00
kuaifan
bcf6cc1019 perf: 客户端标题优化 2021-12-30 14:18:25 +08:00
kuaifan
b97a2e7cf3 build 2021-12-30 11:04:16 +08:00
kuaifan
27158e1ee7 perf: office文件预览 2021-12-30 10:58:51 +08:00
kuaifan
28abad0276 no msg 2021-12-30 10:36:46 +08:00
kuaifan
b59549ebe9 perf: 文件预览不能滚动的问题 2021-12-30 10:34:52 +08:00
kuaifan
b9f788fbe8 build 2021-12-30 00:58:23 +08:00
kuaifan
9d89334cc5 perf: 优化文件权限 2021-12-30 00:52:34 +08:00
kuaifan
6c67ff3fe8 fix: 只读文件也能修改文件 2021-12-29 22:11:18 +08:00
kuaifan
6ef59f703a perf: 初始化数据 2021-12-29 21:40:13 +08:00
623585344@qq.com
c8f11578d6 mod:修改首页内容部分图片自适应 2021-12-29 17:47:45 +08:00
kuaifan
bbd055c798 build 2021-12-29 17:36:33 +08:00
kuaifan
53879fcefb feat: 文件分享查看链接 2021-12-29 17:23:53 +08:00
623585344@qq.com
f33be18be3 fix:修复首页内容部分间距问题 2021-12-29 16:28:30 +08:00
623585344@qq.com
513c7dbd85 add:新增自适应隐藏语言选择框文本 2021-12-29 16:20:59 +08:00
623585344@qq.com
02a9623310 fix:修复英文版本登录按钮无法点击 2021-12-29 14:13:51 +08:00
kuaifan
fd6e7f3096 feat: 新增主动退出共享文件 2021-12-29 14:11:33 +08:00
623585344@qq.com
58c1d52f52 fix:修复首页代码格式化 2021-12-29 13:58:12 +08:00
623585344@qq.com
b71c881b43 add:首页自适应 2021-12-29 13:48:52 +08:00
kuaifan
4d8cf41b7a feat: 共享给所有人 2021-12-29 13:08:30 +08:00
kuaifan
2b0467e00f feat: 文件支持只读、读/写细化设置 2021-12-29 08:38:45 +08:00
kuaifan
cef6646f50 perf: Windows客户端无法关闭的情况 2021-12-28 16:55:29 +08:00
test
ffc2c7dea3 add:首页功能开发 2021-12-28 15:38:40 +08:00
test
d7652a7a32 add:首页功能开发 2021-12-28 15:34:54 +08:00
kuaifan
b695f90ded build 2021-12-28 14:40:59 +08:00
kuaifan
d7f1246c32 perf: 任务等级支持设置0天表示默认不限时 2021-12-28 14:37:02 +08:00
kuaifan
739d1f2455 perf: 客户端子窗口 2021-12-28 14:12:24 +08:00
kuaifan
be7c6e700b fix: 聊天窗口不自动滚动的问题 2021-12-28 12:24:27 +08:00
kuaifan
320dd49a87 perf: 任务窗口 2021-12-28 11:42:59 +08:00
kuaifan
9bfa680fa4 feat: 客户端子窗口数据同步到主窗口 2021-12-28 09:00:44 +08:00
kuaifan
ed32f9994d feat: 任务新窗口打开 2021-12-27 23:20:37 +08:00
kuaifan
c6eb850abe no message 2021-12-27 21:04:35 +08:00
kuaifan
6c458b81b2 fix: 清除缓存导致自定义服务器失败的问题 2021-12-27 14:49:08 +08:00
kuaifan
6741f59aef fix: office无法修改的问题 2021-12-27 14:44:45 +08:00
kuaifan
6a0fd46fc4 perf: 链接说明 2021-12-27 14:37:21 +08:00
kuaifan
b2c8beae71 build 2021-12-27 14:21:05 +08:00
kuaifan
b376327438 style: 任务描述溢出的问题 2021-12-27 13:57:42 +08:00
kuaifan
9eb4fecbe8 feat: 客户端支持自定义服务器 2021-12-27 13:49:04 +08:00
kuaifan
f672f4d1bb style: 优化客户端更新窗口滚动 2021-12-27 12:26:43 +08:00
kuaifan
c8b085e963 perf: 发布窗口小屏幕不支持右键(隐藏右键提示) 2021-12-27 12:20:10 +08:00
kuaifan
8cd3daee9d style: 发布任务出口小屏幕下方按钮不居中 2021-12-27 12:20:10 +08:00
kuaifan
0b03aec038 fix: 邀请加入项目数据库迁移文件 2021-12-27 12:19:03 +08:00
kuaifan
b04fd1a937 fix: 系统设置保存后邀请码消失 2021-12-27 00:07:34 +08:00
kuaifan
bdf4222d70 build 2021-12-26 23:48:14 +08:00
kuaifan
261f7ebbc2 fix: 任务详情窗口不显示子任务 2021-12-26 23:42:38 +08:00
kuaifan
91f25e9ec3 feat: 注册邀请码模式 2021-12-26 23:38:47 +08:00
kuaifan
dd86bb88c6 feat: 通过链接邀请加入项目 2021-12-26 23:38:25 +08:00
kuaifan
0665f2de5f feat: 文件上传进度 2021-12-26 11:51:12 +08:00
kuaifan
364e5df974 fix: 全局loading 2021-12-26 02:22:26 +08:00
kuaifan
d3cdaccbc5 build 2021-12-26 01:52:43 +08:00
kuaifan
75dbc990e4 perf: 安装composer install失败时尝试使用国内源再试一次 2021-12-26 01:44:04 +08:00
kuaifan
5bdd6e15e4 fix: 图片媒体文件无法预览 2021-12-26 01:20:37 +08:00
kuaifan
f7b5a2e971 feat: 新注册自动创建个人项目 2021-12-26 01:11:27 +08:00
kuaifan
a486eefd81 perf: 优化office加载提示 2021-12-26 01:00:49 +08:00
kuaifan
1fae364e7d perf: 按需加载富文本静态资源 2021-12-26 00:43:57 +08:00
kuaifan
76db0c41d3 style: 登录成功加载效果 2021-12-26 00:11:44 +08:00
kuaifan
7318b8917d style: 客户端文件窗口样式 2021-12-25 19:12:55 +08:00
kuaifan
fefcd682b8 build 2021-12-25 16:39:03 +08:00
kuaifan
a32c0b028b perf: 客户端新窗口打开文件 2021-12-25 16:35:47 +08:00
kuaifan
ffe62b8f8e fix: 客户端新窗口 2021-12-25 15:19:03 +08:00
kuaifan
5076374b5d doc: Docker Compose v2.0+ 2021-12-25 14:50:04 +08:00
kuaifan
d368e24f75 perf: 客户端窗口标题 2021-12-25 14:49:44 +08:00
kuaifan
343c5eb587 feat: 客户端新窗口打开文件 2021-12-25 14:48:38 +08:00
kuaifan
f7fc379e56 perf: 领取任务流程 2021-12-25 11:43:29 +08:00
kuaifan
ccf4c4bbb3 fix: window客户端弹出错误 2021-12-25 11:11:57 +08:00
kuaifan
a32995abec perf: 到期时间格式化 2021-12-25 02:18:56 +08:00
kuaifan
abd87f3584 feat: 任务创建窗口自动选择上次添加的项目 2021-12-25 02:12:53 +08:00
kuaifan
bbf7277abc style: iview 2021-12-25 01:44:35 +08:00
kuaifan
b38b335fa1 perf: 小屏幕弹窗底部按钮铺全 2021-12-25 01:29:28 +08:00
kuaifan
cf41e71494 perf: 仪表盘完成任务 2021-12-25 01:06:32 +08:00
kuaifan
e75408d20d style: iview 2021-12-25 00:59:26 +08:00
kuaifan
d79f3c6a80 fix: 任务窗口详情描述内容溢出 2021-12-25 00:16:28 +08:00
kuaifan
1a5e196e4e fix: 任务聊天窗口领取任务按钮错位 2021-12-25 00:11:29 +08:00
kuaifan
138bfc8362 fix: 任务级别设置等级描述 2021-12-25 00:06:44 +08:00
kuaifan
464795779f fix: 手机版隐藏客户端下载按钮 2021-12-24 23:49:33 +08:00
kuaifan
e65c80962d perf: 添加任务窗口 2021-12-24 23:49:09 +08:00
kuaifan
7171d1d6b2 perf: 添加任务弹窗 2021-12-24 12:59:05 +08:00
kuaifan
162c6e95d3 style: 排序箭头颜色 2021-12-24 00:36:29 +08:00
kuaifan
63743656d4 fix: 按到期时间排序没有时间应该排到最后 2021-12-24 00:27:01 +08:00
kuaifan
a3a9032af7 build 2021-12-23 23:36:50 +08:00
kuaifan
194ef2b4ca fix: 分页 #31 2021-12-23 23:34:49 +08:00
kuaifan
89b8342ca0 perf: 客户端关闭 2021-12-23 23:26:33 +08:00
kuaifan
ba32df2fb8 perf: 客户端在项目页面支持快捷键添加任务 2021-12-23 23:22:10 +08:00
kuaifan
08234afe4f fix: 任务负责人无法修改子任务的问题 2021-12-23 23:20:04 +08:00
kuaifan
41eb28992e feat: mac客户端角标提示加上仪表盘内的任务数 2021-12-23 21:50:13 +08:00
kuaifan
20a6da4944 feat: 客户端快捷键关闭侧滑窗 2021-12-23 21:49:27 +08:00
kuaifan
544496a09b feat: 客户端快捷键关闭窗口先关网页内的弹窗 2021-12-23 19:35:35 +08:00
kuaifan
26e7d562aa docs: 添加QQ群号 2021-12-23 16:44:11 +08:00
kuaifan
d1814a4e0f feat: 项目任务支持优先级、到期时间排序 2021-12-23 15:41:48 +08:00
kuaifan
a11adad23f perf: 已完成任务的显示 2021-12-23 15:10:09 +08:00
kuaifan
834c8cc7d5 perf: 任务详细描述、添加任务 2021-12-23 14:55:29 +08:00
kuaifan
1801ea7873 perf: 仪表盘 2021-12-23 12:21:23 +08:00
kuaifan
b208634e40 feat: 仪表盘徽标数 2021-12-23 01:33:14 +08:00
kuaifan
7b3d071fd3 feat: 协助的任务 2021-12-23 01:09:05 +08:00
kuaifan
663a8bb06d perf: 时间范围任务 2021-12-23 01:07:47 +08:00
kuaifan
6c34f083e3 perf: 今天任务、我的任务 2021-12-22 22:39:08 +08:00
kuaifan
6d9237c399 perf: 日历日、周视图选择添加任务会进入all day 2021-12-22 21:37:07 +08:00
kuaifan
8a7186c1b1 feat: 记住最后登录账号 2021-12-22 21:19:22 +08:00
kuaifan
c120686fae perf: 清除缓存 2021-12-22 21:13:25 +08:00
kuaifan
75c83e4117 fix 只有一列的时候默认版面为表格模式 2021-12-22 20:28:40 +08:00
kuaifan
d16b846d4e build 2021-12-22 19:51:54 +08:00
kuaifan
fe44c35406 使用element notification 2021-12-22 17:49:44 +08:00
kuaifan
376bcc4a0b 新增客户端提示升级 2021-12-22 15:37:19 +08:00
kuaifan
26d9e63e83 优化electron脚本命令 2021-12-22 12:13:31 +08:00
kuaifan
a7ac2cee13 no message 2021-12-22 01:42:21 +08:00
kuaifan
7a850704e5 no message 2021-12-22 00:37:17 +08:00
kuaifan
d4cc561d90 add workflows 2021-12-21 23:47:08 +08:00
4428 changed files with 341010 additions and 457112 deletions

View File

@@ -1,13 +1,17 @@
APP_NAME=Dootask
TIMEZONE=PRC
APP_NAME=DooTask
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_DEBUG=false
APP_SCHEME=auto
APP_URL=http://localhost
APP_ID=
APP_IPPR=
APP_PORT=2222
APP_SSL_PORT=
APP_DEV_PORT=
LOG_CHANNEL=stack
LOG_LEVEL=debug

View File

@@ -1,3 +1,5 @@
TIMEZONE=PRC
APP_NAME=Laravel
APP_ENV=local
APP_KEY=

View File

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

View File

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

3
.gitignore vendored
View File

@@ -1,10 +1,12 @@
/node_modules
/public/hot
/public/tmp
/public/summary
/public/uploads/*
/public/.well-known
/public/.user.ini
/storage/*.key
/config/LICENSE
/vendor
/build
/tmp
@@ -19,7 +21,6 @@ Homestead.yaml
npm-debug.log
yarn-error.log
test.*
composer.lock
package-lock.json
laravels-timer-process.pid
.DS_Store

6
.gitmodules vendored Normal file
View File

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

13
.gitpod.yml Normal file
View File

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

22
.prefetch Normal file
View File

@@ -0,0 +1,22 @@
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

2838
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

106
README.md
View File

@@ -2,28 +2,34 @@
English | **[中文文档](./README_CN.md)**
- [Screenshot Preview](README_PREVIEW.md)
- [Screenshot preview](./README_PREVIEW.md)
- [Demo site](http://www.dootask.com/)
**QQ Group**
Group No.: `546574618`
## Setup
> `Docker` & `Docker Compose` must be installed
- `Docker v20.10+` & `Docker Compose v2.0+` must be installed
- System: `Centos/Debian/Ubuntu/macOS/Windows`
- Hardware suggestion: 2 cores and above 4G memory
- Special note: Windows users please use `git bash` or `cmder` to run the command
### Deployment project
### Deployment (Pro Edition)
```bash
# 1、Clone the repository
# Clone projects on github
git clone https://github.com/kuaifan/dootask.git
# or you can use gitee
git clone https://gitee.com/aipaw/dootask.git
git clone -b pro --depth=1 https://github.com/kuaifan/dootask.git
# Or you can use gitee
git clone -b pro --depth=1 https://gitee.com/aipaw/dootask.git
# 2、enter directory
# 2、Enter directory
cd dootask
# 3、Build project
# 3、InstallationCustom port installation, as: ./cmd install --port 80
./cmd install
```
@@ -37,8 +43,18 @@ cd dootask
### Change port
```bash
./cmd php bin/run --port=2222
./cmd up -d
# This method only replaces the HTTP port. To replace the HTTPS port, please read the SSL configuration below
./cmd port 80
```
### Change App Url
```bash
# This URL only affects the email reply.
./cmd url {Your domain url}
# example:
./cmd url https://domain.com
```
### Stop server
@@ -50,21 +66,41 @@ cd dootask
./cmd start
```
### Development compilation
- `NodeJs 20+` must be installed
```bash
# Development
./cmd dev
# Production (This is web client. For App/PC/Mac clients, Please read README-CLIENT.md)
./cmd prod
```
### Shortcuts for running command
```bash
# You can do this using the following command
./cmd artisan "your command" // To run a artisan command
./cmd php "your command" // To run a php command
./cmd nginx "your command" // To run a nginx command
./cmd redis "your command" // To run a redis command
./cmd composer "your command" // To run a composer command
./cmd supervisorctl "your command" // To run a supervisorctl command
./cmd test "your command" // To run a phpunit command
./cmd mysql "your command" // To run a mysql command (backup: Backup database, recovery: Restore database)
./cmd artisan "your command" # To run a artisan command
./cmd php "your command" # To run a php command
./cmd nginx "your command" # To run a nginx command
./cmd redis "your command" # To run a redis command
./cmd composer "your command" # To run a composer command
./cmd supervisorctl "your command" # To run a supervisorctl command
./cmd 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
@@ -72,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、Enter directory and run command
./cmd https
# 2、Running commands in a project (If you unconfigure the NGINX agent, run: ./cmd https close)
./cmd https agent
```
## Upgrade
@@ -81,7 +117,7 @@ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
**Note: Please back up your data before upgrading!**
```bash
# Method 1: enter directory and run command
# Method 1: Running commands in a project
./cmd update
# Or method 2: use this method if method 1 fails
@@ -92,9 +128,31 @@ git pull
./cmd mysql recovery
```
* Please try again if the upgrade fails across a large version.
* If 502 after the upgrade please run `./cmd restart` restart the service.
## Transfer
Follow these steps to complete the project migration after the new project is installed:
1. Backup original database
```bash
# Run command under old project
./cmd mysql backup
```
2. Copy `database backup file` and `public/uploads` directory to the new project.
3. Restore database to new project
```bash
# Run command under new project
./cmd mysql recovery
```
## Uninstall
```bash
# Enter directory and run command
# Running commands in a project
./cmd uninstall
```

25
README_CLIENT.md Normal file
View File

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

View File

@@ -2,28 +2,34 @@
**[English](./README.md)** | 中文文档
- [截图预览](README_PREVIEW.md)
- [截图预览](./README_PREVIEW.md)
- [演示站点](http://www.dootask.com/)
**QQ交流群**
- QQ群号: `546574618`
## 安装程序
> 必须安装 `Docker` 和 `Docker Compose`
- 必须安装`Docker v20.10+``Docker Compose v2.0+`
- 支持环境:`Centos/Debian/Ubuntu/macOS/Windows`
- 硬件建议2核4G以上
- 特别说明Windows 用户请使用 `git bash` 或者 `cmder` 运行命令
### 部署项目
### 部署项目Pro版
```bash
# 1、克隆项目到您的本地或服务器
# 通过github克隆项目
git clone https://github.com/kuaifan/dootask.git
git clone -b pro --depth=1 https://github.com/kuaifan/dootask.git
# 或者你也可以使用gitee
git clone https://gitee.com/aipaw/dootask.git
git clone -b pro --depth=1 https://gitee.com/aipaw/dootask.git
# 2、进入目录
cd dootask
# 3、一键构建项目
# 3、一键安装项目(自定义端口安装,如:./cmd install --port 80
./cmd install
```
@@ -37,8 +43,18 @@ cd dootask
### 更换端口
```bash
./cmd php bin/run --port=2222
./cmd up -d
# 此方法仅更换http端口更换https端口请阅读下面SSL配置
./cmd port 80
```
### 更换URL
```bash
# 此地址仅影响邮件回复功能
./cmd url {域名地址}
# 例如:
./cmd url https://domain.com
```
### 停止服务
@@ -50,21 +66,42 @@ cd dootask
./cmd start
```
### 开发编译
- 请确保你已经安装了 `NodeJs 20+`
```bash
# 开发模式
./cmd dev
# 编译项目这是网页端的App/Pc/Mac客户端请查看 README_CLIENT.md
./cmd prod
```
### 运行命令的快捷方式
```bash
# 你可以使用以下命令来执行
./cmd artisan "your command" // 运行 artisan 命令
./cmd php "your command" // 运行 php 命令
./cmd nginx "your command" // 运行 nginx 命令
./cmd redis "your command" // 运行 redis 命令
./cmd composer "your command" // 运行 composer 命令
./cmd supervisorctl "your command" // 运行 supervisorctl 命令
./cmd test "your command" // 运行 phpunit 命令
./cmd mysql "your command" // 运行 mysql 命令 (backup: 备份数据库recovery: 还原数据库)
./cmd artisan "your command" # 运行 artisan 命令
./cmd php "your command" # 运行 php 命令
./cmd nginx "your command" # 运行 nginx 命令
./cmd redis "your command" # 运行 redis 命令
./cmd composer "your command" # 运行 composer 命令
./cmd supervisorctl "your command" # 运行 supervisorctl 命令
./cmd mysql "your command" # 运行 mysql 命令 (backup: 备份数据库recovery: 还原数据库)
```
### NGINX 代理 SSL
### SSL 配置
#### 方法1自动配置
```bash
# 在项目下运行命令,根据提示执行即可
./cmd https
```
#### 或者方法2Nginx 代理配置
```bash
# 1、Nginx 代理配置添加
@@ -72,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
```
## 升级更新
@@ -81,7 +118,7 @@ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
**注意:在升级之前请备份好你的数据!**
```bash
# 方法1进入项目所在目录,运行以下命令
# 方法1在项目下运行命令
./cmd update
# 或者方法2如果方法1失败请使用此方法
@@ -92,9 +129,31 @@ git pull
./cmd mysql recovery
```
* 跨越大版本升级失败时请重试执行一次。
* 如果升级后出现502请运行 `./cmd restart` 重启服务即可。
## 迁移项目
在新项目安装好之后按照以下步骤完成项目迁移:
1、备份原数据库
```bash
# 在旧的项目下运行命令
./cmd mysql backup
```
2、将`数据库备份文件`及`public/uploads`目录拷贝至新项目
3、还原数据库至新项目
```bash
# 在新的项目下运行命令
./cmd mysql recovery
```
## 卸载项目
```bash
# 进入项目所在目录,运行以下命令
# 在项目下运行命令
./cmd uninstall
```

26
README_PUBLISH.md Normal file
View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,97 @@
<?php
namespace App\Http\Controllers\Api;
use App\Models\UserBot;
use App\Module\Base;
use Request;
/**
* @apiDefine public
*
* 公开
*/
class PublicController extends AbstractController
{
/**
* 签到 - 路由器openwrt功能安装脚本
*
* @apiParam {String} key
*
* @return string
*/
public function checkin__install()
{
$key = trim(Request::input('key'));
//
$setting = Base::setting('checkinSetting');
if ($setting['open'] !== 'open') {
return <<<EOF
#!/bin/sh
echo "function off"
EOF;
}
if ($key != $setting['key']) {
return <<<EOF
#!/bin/sh
echo "key error"
EOF;
}
//
$reportUrl = Base::fillUrl("api/public/checkin/report");
return <<<EOE
#!/bin/sh
echo 'installing...'
cat > /etc/init.d/dootask-checkin-report <<EOF
#!/bin/sh
mac=\\\$(awk 'NR!=1&&\\\$3=="0x2" {print \\\$4}' /proc/net/arp | tr "\\n" ",")
tmp='{"key":"{$setting['key']}","mac":"'\\\${mac}'","time":"'\\\$(date +%s)'"}'
curl -4 -X POST "{$reportUrl}" -H "Content-Type: application/json" -d \\\${tmp}
EOF
chmod +x /etc/init.d/dootask-checkin-report
crontab -l >/tmp/cronbak
sed -i '/\/etc\/init.d\/dootask-checkin-report/d' /tmp/cronbak
sed -i '/^$/d' /tmp/cronbak
echo "* * * * * sh /etc/init.d/dootask-checkin-report" >>/tmp/cronbak
crontab /tmp/cronbak
rm -f /tmp/cronbak
/etc/init.d/cron enable
/etc/init.d/cron restart
echo 'installed'
EOE;
}
/**
* {post} 签到 - 路由器openwrt上报
*
* @apiParam {String} key
* @apiParam {String} mac 使用逗号分割多个
* @apiParam {String} time
*
* @return string
*/
public function checkin__report()
{
$key = trim(Request::input('key'));
$mac = trim(Request::input('mac'));
$time = intval(Request::input('time'));
//
$setting = Base::setting('checkinSetting');
if ($setting['open'] !== 'open') {
return 'function off';
}
if (!in_array('auto', $setting['modes'])) {
return 'mode off';
}
if ($key != $setting['key']) {
return 'key error';
}
if ($error = UserBot::checkinBotCheckin($mac, $time)) {
return $error;
}
return 'success';
}
}

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -2,10 +2,27 @@
namespace App\Http\Controllers;
use App\Module\Base;
use App\Tasks\DeleteTmpTask;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Arr;
use Cache;
use Request;
use Redirect;
use Response;
use App\Module\Doo;
use App\Models\File;
use App\Module\Base;
use App\Tasks\LoopTask;
use App\Module\Extranet;
use App\Tasks\AppPushTask;
use App\Module\RandomColor;
use App\Tasks\JokeSoupTask;
use App\Tasks\DeleteTmpTask;
use App\Tasks\EmailNoticeTask;
use App\Tasks\AutoArchivedTask;
use App\Tasks\DeleteBotMsgTask;
use App\Tasks\CheckinRemindTask;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use App\Tasks\UnclaimedTaskRemindTask;
use LasseRafn\InitialAvatarGenerator\InitialAvatar;
/**
@@ -21,6 +38,9 @@ class IndexController extends InvokeController
if ($action) {
$app .= "__" . $action;
}
if ($app == 'default') {
return '';
}
if (!method_exists($this, $app)) {
$app = method_exists($this, $method) ? $method : 'main';
}
@@ -29,11 +49,79 @@ class IndexController extends InvokeController
/**
* 首页
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @return \Illuminate\Http\Response
*/
public function main()
{
return view('main', ['version' => Base::getVersion()]);
$hotFile = public_path('hot');
$manifestFile = public_path('manifest.json');
if (file_exists($hotFile)) {
$array = Base::json2array(file_get_contents($hotFile));
$style = null;
$script = preg_replace("/^(\/\/(.*?))(:\d+)?\//i", "$1:" . $array['APP_DEV_PORT'] . "/", asset_main("resources/assets/js/app.js"));
} else {
$array = Base::json2array(file_get_contents($manifestFile));
$style = asset_main($array['resources/assets/js/app.js']['css'][0]);
$script = asset_main($array['resources/assets/js/app.js']['file']);
}
return response()->view('main', [
'version' => Base::getVersion(),
'style' => $style,
'script' => $script,
]);
}
/**
* 获取版本号
* @return \Illuminate\Http\RedirectResponse
*/
public function version()
{
return Redirect::to(Base::fillUrl('api/system/version'), 301);
}
/**
* 头像
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response
*/
public function avatar()
{
$segment = Request::segment(2);
if ($segment && preg_match('/.*?\.png$/i', $segment)) {
$name = substr($segment, 0, -4);
} else {
$name = Request::input('name', 'H');
}
$size = Request::input('size', 128);
$color = Request::input('color');
$background = Request::input('background');
//
if (preg_match('/^[\x{4e00}-\x{9fa5}]+$/u', $name)) {
$name = mb_substr($name, mb_strlen($name) - 2);
}
if (empty($color)) {
$color = '#ffffff';
$cacheKey = "avatarBackgroundColor::" . md5($name);
$background = Cache::rememberForever($cacheKey, function() {
return RandomColor::one(['luminosity' => 'dark']);
});
}
//
$avatar = new InitialAvatar();
$content = $avatar->name($name)
->size($size)
->color($color)
->background($background)
->fontSize(0.35)
->autoFont()
->generate()
->stream('png', 100);
//
return response($content)
->header('Pragma', 'public')
->header('Cache-Control', 'max-age=1814400')
->header('Content-type', 'image/png')
->header('Expires', gmdate('D, d M Y H:i:s \G\M\T', time() + 1814400));
}
/**
@@ -55,13 +143,246 @@ class IndexController extends InvokeController
// 限制内网访问
return "Forbidden Access";
}
// 自动归档
Task::deliver(new AutoArchivedTask());
// 邮件通知
Task::deliver(new EmailNoticeTask());
// App推送
Task::deliver(new AppPushTask());
// 删除过期的临时表数据
Task::deliver(new DeleteTmpTask('wg_tmp_msgs', 1));
Task::deliver(new DeleteTmpTask('tmp', 24));
Task::deliver(new DeleteTmpTask('task_worker', 12));
Task::deliver(new DeleteTmpTask('tmp'));
Task::deliver(new DeleteTmpTask('file'));
Task::deliver(new DeleteTmpTask('file_pack'));
// 删除机器人消息
Task::deliver(new DeleteBotMsgTask());
// 周期任务
Task::deliver(new LoopTask());
// 签到提醒
Task::deliver(new CheckinRemindTask());
// 获取笑话/心灵鸡汤
Task::deliver(new JokeSoupTask());
// 未领取任务通知
Task::deliver(new UnclaimedTaskRemindTask());
return "success";
}
/**
* 桌面客户端发布
*/
public function desktop__publish($name = '')
{
$publishVersion = Request::header('publish-version');
$fileNum = Request::get('file_num', 1);
$latestFile = public_path("uploads/desktop/latest");
$latestVersion = file_exists($latestFile) ? trim(file_get_contents($latestFile)) : "0.0.1";
if (strtolower($name) === 'latest') {
$name = $latestVersion;
}
// 上传
if (preg_match("/^\d+\.\d+\.\d+$/", $publishVersion)) {
$uploadSuccessFileNum = (int)Cache::get($publishVersion, 0);
$publishKey = Request::header('publish-key');
if ($publishKey !== env('APP_KEY')) {
return Base::retError("key error");
}
if (version_compare($publishVersion, $latestVersion) > -1) { // 限制上传版本必须 ≥ 当前版本
$publishPath = "uploads/desktop/{$publishVersion}/";
$res = Base::upload([
"file" => Request::file('file'),
"type" => 'desktop',
"path" => $publishPath,
"fileName" => true
]);
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;
}
}
// 列表
if (preg_match("/^\d+\.\d+\.\d+$/", $name)) {
$path = "uploads/desktop/{$name}";
$dirPath = public_path($path);
$lists = Base::readDir($dirPath);
$files = [];
foreach ($lists as $file) {
if (str_ends_with($file, '.yml') || str_ends_with($file, '.yaml') || 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' => $fileSize > 0 ? Base::readableBytes($fileSize) : 0,
'url' => Base::fillUrl($path . $fileName),
];
}
//
$path = "uploads/android";
$dirPath = public_path($path);
$lists = Base::readDir($dirPath);
$apkFile = null;
foreach ($lists as $file) {
if (!str_ends_with($file, '.apk')) {
continue;
}
if ($apkFile && strtotime($apkFile['time']) > filemtime($file)) {
continue;
}
$fileName = Base::leftDelete($file, $dirPath);
$fileSize = filesize($file);
$apkFile = [
'name' => substr($fileName, 1),
'time' => date("Y-m-d H:i:s", filemtime($file)),
'size' => $fileSize > 0 ? Base::readableBytes($fileSize) : 0,
'url' => Base::fillUrl($path . $fileName),
];
}
if ($apkFile) {
$files = array_merge([$apkFile], $files);
}
return view('desktop', ['version' => $name, 'files' => $files]);
}
// 下载
if ($name && file_exists($latestFile)) {
$publishVersion = file_get_contents($latestFile);
if (preg_match("/^\d+\.\d+\.\d+$/", $publishVersion)) {
$filePath = public_path("uploads/desktop/{$publishVersion}/{$name}");
if (file_exists($filePath)) {
return Response::download($filePath);
}
}
}
return abort(404);
}
/**
* Drawio 图标搜索
* @return array|mixed
*/
public function drawio__iconsearch()
{
$query = trim(Request::input('q'));
$page = trim(Request::input('p'));
$size = trim(Request::input('c'));
return Extranet::drawioIconSearch($query, $page, $size);
}
/**
* 预览文件
* @return array|mixed
*/
public function online__preview()
{
$key = trim(Request::input('key'));
//
$data = parse_url($key);
$path = Arr::get($data, 'path');
$file = public_path($path);
// 防止 ../ 穿越获取到系统文件
if (!str_starts_with(realpath($file), public_path())) {
abort(404);
}
//
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');
}
// 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 (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);
}
/**
* 保存配置 (todo 已废弃)
* @return string
*/
public function storage__synch()
{
return '<!-- Deprecated -->';
}
/**
* 提取所有中文
* @return array|string
@@ -123,9 +444,6 @@ class IndexController extends InvokeController
$list = array_merge(Base::readDir(app_path()), Base::readDir(resource_path()));
$array = [];
foreach ($list as $item) {
if (Base::rightExists($item, "language.all.js")) {
continue;
}
if (Base::rightExists($item, ".php") || Base::rightExists($item, ".vue") || Base::rightExists($item, ".js")) {
$content = file_get_contents($item);
preg_match_all("/(['\"])(.*?)[\u{4e00}-\u{9fa5}\u{FE30}-\u{FFA0}]+([\s\S]((?!\n).)*)\\1/u", $content, $matchs);

View File

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

View File

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

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

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

View File

@@ -10,16 +10,21 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
/**
* App\Model\AbstractModel
* App\Models\AbstractModel
*
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
* @method static \Illuminate\Database\Eloquent\Model|object|static|null cancelAppend()
* @method static \Illuminate\Database\Eloquent\Model|object|static|null cancelHidden()
* @method static \Illuminate\Database\Eloquent\Builder|static with($relations)
* @method static \Illuminate\Database\Query\Builder|static select($columns = [])
* @method static \Illuminate\Database\Query\Builder|static whereIn($column, $values, $boolean = 'and', $not = false)
* @method static \Illuminate\Database\Query\Builder|static whereNotIn($column, $values, $boolean = 'and')
* @method int change(array $array)
* @method int remove()
* @mixin \Eloquent
*/
class AbstractModel extends Model
@@ -36,6 +41,42 @@ class AbstractModel extends Model
protected $appendattrs = [];
/**
* 通过模型修改数据
* @param AbstractModel $builder
* @param $array
* @return int
*/
protected function scopeChange($builder, $array)
{
$line = 0;
$rows = $builder->get();
foreach ($rows as $row) {
$row->updateInstance($array);
if ($row->save()) {
$line++;
}
}
return $line;
}
/**
* 通过模型删除数据
* @param AbstractModel $builder
* @return int
*/
protected function scopeRemove($builder)
{
$line = 0;
$rows = $builder->get();
foreach ($rows as $row) {
if ($row->delete()) {
$line++;
}
}
return $line;
}
/**
* 保存数据忽略错误
* @return bool
@@ -44,7 +85,7 @@ class AbstractModel extends Model
{
try {
return $this->save();
} catch (\Exception $e) {
} catch (\Throwable) {
return false;
}
}
@@ -62,6 +103,24 @@ class AbstractModel extends Model
return $this->$key;
}
/**
* 取消附加值
* @return static
*/
protected function scopeCancelAppend()
{
return $this->setAppends([]);
}
/**
* 取消隐藏值
* @return static
*/
protected function scopeCancelHidden()
{
return $this->setHidden([]);
}
/**
* 为数组 / JSON 序列化准备日期。
* @param DateTimeInterface $date
@@ -133,16 +192,23 @@ class AbstractModel extends Model
* @param $where
* @param array $update 存在时更新的内容
* @param array $insert 不存在时插入的内容,如果没有则插入更新内容
* @param bool $isInsert 是否是插入数据
* @return AbstractModel|\Illuminate\Database\Eloquent\Builder|Model|object|static|null
*/
public static function updateInsert($where, $update = [], $insert = [])
public static function updateInsert($where, $update = [], $insert = [], &$isInsert = true)
{
$row = static::where($where)->first();
if (empty($row)) {
$row = new static;
$row->updateInstance(array_merge($where, $insert ?: $update));
$array = array_merge($where, $insert ?: $update);
if (isset($array[$row->primaryKey])) {
unset($array[$row->primaryKey]);
}
$row->updateInstance($array);
$isInsert = true;
} elseif ($update) {
$row->updateInstance($update);
$isInsert = false;
}
if (!$row->save()) {
return null;
@@ -170,10 +236,9 @@ class AbstractModel extends Model
info($eb);
}
if ($e instanceof ApiException) {
throw new ApiException($e->getMessage(), $e->getData(), $e->getCode());
throw new ApiException( $e->getMessage() , $e->getData(), $e->getCode());
} else {
info($e);
throw new ApiException($e->getMessage() ?: '处理错误');
throw new ApiException( $e->getMessage() ?: '处理错误');
}
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Models;
/**
* App\Models\ApproveProcMsg
*
* @property int $id
* @property int|null $proc_inst_id 流程实例ID
* @property int|null $userid 会员ID
* @property int|null $msg_id 消息ID
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg query()
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereMsgId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereProcInstId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcMsg whereUserid($value)
* @mixin \Eloquent
*/
class ApproveProcMsg extends AbstractModel
{
}

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

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

View File

@@ -2,18 +2,20 @@
namespace App\Models;
use App\Exceptions\ApiException;
use Request;
use App\Module\Base;
use App\Tasks\PushTask;
use App\Exceptions\ApiException;
use Illuminate\Support\Facades\DB;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Database\Eloquent\SoftDeletes;
use Request;
/**
* App\Models\File
*
* @property int $id
* @property int|null $pid 上级ID
* @property string|null $pids 上级ID递归
* @property int|null $cid 复制ID
* @property string|null $name 名称
* @property string|null $type 类型
@@ -21,13 +23,14 @@ use Request;
* @property int|null $size 大小(B)
* @property int|null $userid 拥有者ID
* @property int|null $share 是否共享
* @property int|null $pshare 所属分享ID
* @property int|null $created_id 创建者
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @method static \Illuminate\Database\Eloquent\Builder|File newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|File newQuery()
* @method static \Illuminate\Database\Query\Builder|File onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|File onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|File query()
* @method static \Illuminate\Database\Eloquent\Builder|File whereCid($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereCreatedAt($value)
@@ -37,13 +40,15 @@ use Request;
* @method static \Illuminate\Database\Eloquent\Builder|File whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|File wherePid($value)
* @method static \Illuminate\Database\Eloquent\Builder|File wherePids($value)
* @method static \Illuminate\Database\Eloquent\Builder|File wherePshare($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereShare($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereSize($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|File whereUserid($value)
* @method static \Illuminate\Database\Query\Builder|File withTrashed()
* @method static \Illuminate\Database\Query\Builder|File withoutTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|File withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|File withoutTrashed()
* @mixin \Eloquent
*/
class File extends AbstractModel
@@ -51,43 +56,309 @@ class File extends AbstractModel
use SoftDeletes;
/**
* 是否有访问权限(没有时抛出异常)
* @param $userid
* 文件文件
*/
public function exceAllow($userid)
const codeExt = [
'txt',
'htaccess', 'htgroups', 'htpasswd', 'conf', 'bat', 'cmd', 'cpp', 'c', 'cc', 'cxx', 'h', 'hh', 'hpp', 'ino', 'cs', 'css',
'dockerfile', 'go', 'golang', 'html', 'htm', 'xhtml', 'vue', 'we', 'wpy', 'java', 'js', 'jsm', 'jsx', 'json', 'jsp', 'less', 'lua', 'makefile', 'gnumakefile',
'ocamlmakefile', 'make', 'mysql', 'nginx', 'ini', 'cfg', 'prefs', 'm', 'mm', 'pl', 'pm', 'p6', 'pl6', 'pm6', 'pgsql', 'php',
'inc', 'phtml', 'shtml', 'php3', 'php4', 'php5', 'phps', 'phpt', 'aw', 'ctp', 'module', 'ps1', 'py', 'r', 'rb', 'ru', 'gemspec', 'rake', 'guardfile', 'rakefile',
'gemfile', 'rs', 'sass', 'scss', 'sh', 'bash', 'bashrc', 'sql', 'sqlserver', 'swift', 'ts', 'typescript', 'str', 'vbs', 'vb', 'v', 'vh', 'sv', 'svh', 'xml',
'rdf', 'rss', 'wsdl', 'xslt', 'atom', 'mathml', 'mml', 'xul', 'xbl', 'xaml', 'yaml', 'yml',
'asp', 'properties', 'gitignore', 'log', 'bas', 'prg', 'python', 'ftl', 'aspx', 'plist'
];
/**
* office文件
*/
const officeExt = [
'doc', 'docx',
'xls', 'xlsx',
'ppt', 'pptx',
];
/**
* 图片文件
*/
const imageExt = [
'jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp'
];
/**
* 本地媒体文件
*/
const localExt = [
'jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp', 'ico', 'raw',
'tif', 'tiff',
'mp3', 'wav', 'mp4', 'flv',
'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm',
];
/**
* 压缩包下载大小限制
*/
const zipMaxSize = 1024 * 1024 * 1024; // 1G
/**
* 获取文件列表
* @param user $user
* @param int $pid
* @return array
*/
public function getFileList($user, int $pid, $type = "all", $isGetparent = true)
{
if (!$this->chackAllow($userid)) {
throw new ApiException('没有访问权限');
$permission = 1000;
$userids = $user->isTemp() ? [$user->userid] : [0, $user->userid];
$builder = File::wherePid($pid)
->when($type=='dir',function($q){
$q->whereType('folder');
});
if ($pid > 0) {
File::permissionFind($pid, $userids, 0, $permission);
} else {
$builder->whereUserid($user->userid);
}
//
$array = $builder->take(500)->get()->toArray();
foreach ($array as &$item) {
$item['permission'] = $permission;
}
//
if ($pid > 0) {
// 遍历获取父级
if($isGetparent){
while ($pid > 0) {
$file = File::whereId($pid)->first();
if (empty($file)) {
break;
}
$pid = $file->pid;
$temp = $file->toArray();
$temp['permission'] = $file->getPermission($userids);
$array[] = $temp;
}
}
// 去除没有权限的文件
$isUnset = false;
foreach ($array as $index1 => $item1) {
if ($item1['permission'] === -1) {
foreach ($array as $index2 => $item2) {
if ($item2['pid'] === $item1['id']) {
$array[$index2]['pid'] = 0;
}
}
$isUnset = true;
unset($array[$index1]);
}
}
if ($isUnset) {
$array = array_values($array);
}
} else {
// 获取共享相关
DB::statement("SET SQL_MODE=''");
$pre = DB::connection()->getTablePrefix();
$list = File::select(["files.*", DB::raw("MAX({$pre}file_users.permission) as permission")])
->join('file_users', 'files.id', '=', 'file_users.file_id')
->where('files.userid', '!=', $user->userid)
->whereIn('file_users.userid', $userids)
->groupBy('files.id')
->take(100)
->when($type=='dir',function($q){
$q->where('files.type','folder');
})
->get();
if ($list->isNotEmpty()) {
foreach ($list as $file) {
$temp = $file->toArray();
$temp['pid'] = 0;
$array[] = $temp;
}
}
}
// 图片直接返回预览地址
foreach ($array as &$item) {
$item = File::handleImageUrl($item);
}
return $array;
}
/**
* 保存文件内容(上传文件)
* @param user $user
* @param int $pid
* @param string $webkitRelativePath
* @param bool $overwrite
* @return array
*/
public function contentUpload($user, int $pid, $webkitRelativePath, $overwrite = false)
{
$userid = $user->userid;
if ($pid > 0) {
if (File::wherePid($pid)->count() >= 300) {
return Base::retError('每个文件夹里最多只能创建300个文件或文件夹');
}
$row = File::permissionFind($pid, $user, 1);
$userid = $row->userid;
} else {
if (File::whereUserid($user->userid)->wherePid(0)->count() >= 300) {
return Base::retError('每个文件夹里最多只能创建300个文件或文件夹');
}
}
//
$dirs = explode("/", $webkitRelativePath);
$addItem = [];
while (count($dirs) > 1) {
$dirName = array_shift($dirs);
if ($dirName) {
AbstractModel::transaction(function () use ($dirName, $user, $userid, &$pid, &$addItem) {
$dirRow = File::wherePid($pid)->whereType('folder')->whereName($dirName)->lockForUpdate()->first();
if (empty($dirRow)) {
$dirRow = File::createInstance([
'pid' => $pid,
'type' => 'folder',
'name' => $dirName,
'userid' => $userid,
'created_id' => $user->userid,
]);
$dirRow->handleDuplicateName();
if ($dirRow->saveBeforePP()) {
$addItem[] = File::find($dirRow->id);
}
}
if (empty($dirRow)) {
throw new ApiException('创建文件夹失败');
}
$pid = $dirRow->id;
});
foreach ($addItem as $tmpRow) {
$tmpRow->pushMsg('add', $tmpRow);
}
}
}
//
$setting = Base::setting('system');
$path = 'uploads/tmp/' . date("Ym") . '/';
$data = Base::upload([
"file" => Request::file('files'),
"type" => 'more',
"autoThumb" => false,
"path" => $path,
"size" => ($setting['file_upload_limit'] ?: 0) * 1024
]);
if (Base::isError($data)) {
throw new ApiException($data['msg']);
}
$data = $data['data'];
//
$type = match ($data['ext']) {
'text', 'md', 'markdown' => 'document',
'drawio' => 'drawio',
'mind' => 'mind',
'doc', 'docx' => "word",
'xls', 'xlsx' => "excel",
'ppt', 'pptx' => "ppt",
'wps' => "wps",
'jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp', 'ico', 'raw', 'svg' => "picture",
'rar', 'zip', 'jar', '7-zip', 'tar', 'gzip', '7z', 'gz', 'apk', 'dmg' => "archive",
'tif', 'tiff' => "tif",
'dwg', 'dxf' => "cad",
'ofd' => "ofd",
'pdf' => "pdf",
'txt' => "txt",
'htaccess', 'htgroups', 'htpasswd', 'conf', 'bat', 'cmd', 'cpp', 'c', 'cc', 'cxx', 'h', 'hh', 'hpp', 'ino', 'cs', 'css',
'dockerfile', 'go', 'golang', 'html', 'htm', 'xhtml', 'vue', 'we', 'wpy', 'java', 'js', 'jsm', 'jsx', 'json', 'jsp', 'less', 'lua', 'makefile', 'gnumakefile',
'ocamlmakefile', 'make', 'mysql', 'nginx', 'ini', 'cfg', 'prefs', 'm', 'mm', 'pl', 'pm', 'p6', 'pl6', 'pm6', 'pgsql', 'php',
'inc', 'phtml', 'shtml', 'php3', 'php4', 'php5', 'phps', 'phpt', 'aw', 'ctp', 'module', 'ps1', 'py', 'r', 'rb', 'ru', 'gemspec', 'rake', 'guardfile', 'rakefile',
'gemfile', 'rs', 'sass', 'scss', 'sh', 'bash', 'bashrc', 'sql', 'sqlserver', 'swift', 'ts', 'typescript', 'str', 'vbs', 'vb', 'v', 'vh', 'sv', 'svh', 'xml',
'rdf', 'rss', 'wsdl', 'xslt', 'atom', 'mathml', 'mml', 'xul', 'xbl', 'xaml', 'yaml', 'yml',
'asp', 'properties', 'gitignore', 'log', 'bas', 'prg', 'python', 'ftl', 'aspx', 'plist' => "code",
'mp3', 'wav', 'mp4', 'flv',
'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm' => "media",
'xmind' => "xmind",
'rp' => "axure",
default => "",
};
if ($data['ext'] == 'markdown') {
$data['ext'] = 'md';
}
$file = null;
$params = [
'pid' => $pid,
'name' => Base::rightDelete($data['name'], '.' . $data['ext']),
'type' => $type,
'ext' => $data['ext'],
'userid' => $userid,
'created_id' => $user->userid,
];
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 ($overwrite, $addItem, $webkitRelativePath, $type, $user, $data, $file) {
$file->size = $data['size'] * 1024;
$file->saveBeforePP();
//
$data = Base::uploadMove($data, "uploads/file/" . $file->type . "/" . date("Ym") . "/" . $file->id . "/");
$content = [
'from' => '',
'type' => $type,
'ext' => $data['ext'],
'url' => $data['path'],
];
if (isset($data['width'])) {
$content['width'] = $data['width'];
$content['height'] = $data['height'];
}
$content = FileContent::createInstance([
'fid' => $file->id,
'content' => $content,
'text' => '',
'size' => $file->size,
'userid' => $user->userid,
]);
$content->save();
//
$tmpRow = File::find($file->id);
$tmpRow->pushMsg('add', $tmpRow);
//
$data = File::handleImageUrl($tmpRow->toArray());
$data['full_name'] = $webkitRelativePath ?: ($data['name'] . '.' . $data['ext']);
$data['overwrite'] = $overwrite ? 1 : 0;
//
$addItem[] = $data;
return ['data' => $data, 'addItem' => $addItem];
});
}
/**
* 是否有访问权限
* ① 自己的文件夹
* ② 共享所有人的文件夹
* ③ 在指定共享人员内
* @param $userid
* @return bool
* @param array $userids
* @return int -1:没有权限0:访问权限1:读写权限1000:所有者或创建者
*/
public function chackAllow($userid)
public function getPermission(array $userids)
{
if ($userid == $this->userid) {
// ① 自己的文件夹
return true;
if (in_array($this->userid, $userids) || in_array($this->created_id, $userids)) {
// ① 自己的文件夹 或 自己创建的文件夹
return 1000;
}
$row = $this->getShareInfo();
if ($row) {
if ($row->share == 1) {
// ② 共享所有人的文件夹
return true;
} elseif ($row->share == 2) {
// ③ 在指定共享人员内
if (FileUser::whereFileId($row->id)->whereUserid($userid)->exists()) {
return true;
}
$fileUser = FileUser::whereFileId($row->id)->whereIn('userid', $userids)->orderByDesc('permission')->first();
if ($fileUser) {
// ② 在指定共享成员内
return $fileUser->permission;
}
}
return false;
return -1;
}
/**
@@ -96,7 +367,7 @@ class File extends AbstractModel
*/
public function getShareInfo()
{
if ($this->share > 0) {
if ($this->share) {
return $this;
}
$pid = $this->pid;
@@ -105,7 +376,7 @@ class File extends AbstractModel
if (empty($row)) {
break;
}
if ($row->share > 0) {
if ($row->share) {
return $row;
}
$pid = $row->pid;
@@ -115,7 +386,7 @@ class File extends AbstractModel
/**
* 是否处于共享文件夹内(不含自身)
* @return bool
* @return File|false
*/
public function isNnShare()
{
@@ -125,31 +396,119 @@ class File extends AbstractModel
if (empty($row)) {
break;
}
if ($row->share > 0) {
return true;
if ($row->share) {
return $row;
}
$pid = $row->pid;
}
return false;
}
/**
* 目录内是否存在共享文件或文件夹
* @return bool
*/
public function isSubShare()
{
return $this->type == 'folder' && File::where("pids", "like", "%,{$this->id},%")->whereShare(1)->exists();
}
/**
* 设置/关闭 共享(同时遍历取消里面的共享)
* @param $share
* @return bool
*/
public function setShare($share)
public function updataShare($share = null)
{
AbstractModel::transaction(function () use ($share) {
$this->share = $share;
$this->save();
$list = self::wherePid($this->id)->get();
if ($list->isNotEmpty()) {
foreach ($list as $item) {
$item->setShare(0);
if ($share === null) {
$share = FileUser::whereFileId($this->id)->count() == 0 ? 0 : 1;
}
if ($this->share != $share) {
AbstractModel::transaction(function () use ($share) {
$this->share = $share;
$this->pshare = $share ? $this->id : 0;
$this->save();
File::where("pids", "like", "%,{$this->id},%")->update(['pshare' => $share ? $this->id : 0]);
if ($share === 0) {
FileUser::deleteFileAll($this->id, $this->userid);
}
$list = self::wherePid($this->id)->get();
if ($list->isNotEmpty()) {
foreach ($list as $item) {
$item->updataShare(0);
}
}
});
}
return true;
}
/**
* 处理重名
* @return void
*/
public function handleDuplicateName()
{
$builder = self::wherePid($this->pid)->whereUserid($this->userid)->whereExt($this->ext);
$exist = $builder->clone()->whereName($this->name)->exists();
if (!$exist) {
return; // 未重名,不需要处理
}
// 发现重名,自动重命名
$nextNum = 2;
if (preg_match("/(.*?)(\s+\(\d+\))*$/", $this->name)) {
$preName = preg_replace("/(.*?)(\s+\(\d+\))*$/", "$1", $this->name);
$nextNum = $builder->clone()->where("name", "LIKE", "{$preName}%")->count() + 1;
}
$newName = "{$this->name} ({$nextNum})";
if ($builder->clone()->whereName($newName)->exists()) {
$nextNum = rand(100, 9999);
$newName = "{$this->name} ({$nextNum})";
}
$this->name = $newName;
}
/**
* 保存前更新pids/pshare
* @return bool
*/
public function saveBeforePP()
{
$pid = $this->pid;
$pshare = $this->share ? $this->id : 0;
$array = [];
while ($pid > 0) {
$array[] = $pid;
$file = self::select(['id', 'pid', 'share'])->find($pid);
if ($file) {
$pid = $file->pid;
if ($file->share) {
$pshare = $file->id;
}
} else {
$pid = 0;
}
});
}
$opids = $this->pids;
if ($array) {
$array = array_values(array_reverse($array));
$this->pids = ',' . implode(',', $array) . ',';
} else {
$this->pids = '';
}
$this->pshare = $pshare;
if (!$this->save()) {
return false;
}
// 更新子文件(夹)
if ($opids != $this->pids) {
self::wherePid($this->id)->chunkById(100, function ($lists) {
/** @var self $item */
foreach ($lists as $item) {
$item->saveBeforePP();
}
});
}
return true;
}
@@ -162,6 +521,7 @@ class File extends AbstractModel
AbstractModel::transaction(function () {
$this->delete();
$this->pushMsg('delete');
FileUser::deleteFileAll($this->id);
FileContent::whereFid($this->id)->delete();
$list = self::wherePid($this->id)->get();
if ($list->isNotEmpty()) {
@@ -173,6 +533,50 @@ class File extends AbstractModel
return true;
}
/**
* 强制删除文件
* @return true
*/
public function forceDeleteFile()
{
AbstractModel::transaction(function () {
$this->forceDelete();
FileContent::withTrashed()
->whereFid($this->id)
->orderBy('id')
->chunk(500, function ($contents) {
/** @var FileContent $content */
foreach ($contents as $content) {
$content->forceDeleteContent();
}
});
});
return true;
}
/**
* 获取文件分享链接
* @param $userid
* @param $refresh
* @return array
*/
public function getShareLink($userid, $refresh = false)
{
if ($this->type == 'folder') {
throw new ApiException('文件夹不支持分享');
}
return FileLink::generateLink($this->id, $userid, $refresh);
}
/**
* 获取文件名称加后缀
* @return string|null
*/
public function getNameAndExt()
{
return $this->ext ? "{$this->name}.{$this->ext}" : $this->name;
}
/**
* 推送消息
* @param $action
@@ -187,19 +591,7 @@ class File extends AbstractModel
];
}
//
if ($userid === null) {
$userid = [$this->userid];
if ($this->share == 1) {
$builder = WebSocket::select(['userid']);
if ($action == 'content') {
$builder->wherePath('file/content/' . $this->id);
}
$userid = array_merge($userid, $builder->pluck('userid')->toArray());
} elseif ($this->share == 2) {
$userid = array_merge($userid, FileUser::whereFileId($this->id)->pluck('userid')->toArray());
}
$userid = array_values(array_filter(array_unique($userid)));
}
$userid = $this->pushUserid($action, $userid);
if (empty($userid)) {
return;
}
@@ -223,18 +615,340 @@ class File extends AbstractModel
}
/**
* 获取文件并检测权限
* @param $id
* @param null $noExistTis
* 获取推送会员
* @param $action
* @param $userid
* @return array|int[]|mixed|null[]
*/
public function pushUserid($action, $userid = null) {
$wherePath = "/manage/file";
if ($userid === null) {
$array = [$this->userid];
if ($action == 'add' && $this->pid == 0) {
return $array;
}
if ($action == 'content') {
$wherePath = "/single/file/{$this->id}";
} elseif ($this->pid > 0) {
$wherePath = "/manage/file/{$this->pid}";
} else {
$tmpArray = FileUser::whereFileId($this->id)->pluck('userid')->toArray();
if (empty($tmpArray)) {
return $array;
}
if (!in_array(0, $tmpArray)) {
return $tmpArray;
}
}
$tmpArray = WebSocket::wherePath($wherePath)->pluck('userid')->toArray();
if (empty($tmpArray)) {
return $array;
}
$array = array_values(array_filter(array_unique(array_merge($array, $tmpArray))));
} else {
$array = is_array($userid) ? $userid : [$userid];
if (in_array(0, $array)) {
return WebSocket::wherePath($wherePath)->pluck('userid')->toArray();
}
}
return $array;
}
/**
* code获取文件ID、名称
* @param $code
* @return File
*/
public static function allowFind($id, $noExistTis = null)
public static function code2IdName($code) {
$arr = explode(",", base64_decode($code));
if (empty($arr)) {
return null;
}
$fileId = intval($arr[0]);
if (empty($fileId)) {
return null;
}
return File::select(['id', 'name'])->find($fileId);
}
/**
* 处理返回图片地址
* @param array $item
* @return array
*/
public static function handleImageUrl($item)
{
if (in_array($item['ext'], self::imageExt) ) {
$content = Base::json2array(FileContent::whereFid($item['id'])->orderByDesc('id')->value('content'));
if ($content) {
$item['image_url'] = Base::fillUrl($content['url']);
$item['image_width'] = intval($content['width']);
$item['image_height'] = intval($content['height']);
}
}
return $item;
}
/**
* 获取文件并检测权限
* @param int $id
* @param User|array|int $user 要求权限的用户,如:[0, 1]
* @param int $limit 要求权限: 0-访问权限、1-读写权限、1000-所有者或创建者
* @param int $permission
* @return File
*/
public static function permissionFind(int $id, $user, int $limit = 0, int &$permission = -1)
{
$file = File::find($id);
if (empty($file)) {
throw new ApiException($noExistTis ?: '文件不存在或已被删除');
throw new ApiException('文件不存在或已被删除');
}
//
if ($user instanceof User) {
$userids = $user->isTemp() ? [$user->userid] : [0, $user->userid];
} else {
$userids = is_array($user) ? $user : [$user];
}
$permission = $file->getPermission($userids);
if ($permission < $limit) {
$msg = match ($limit) {
1000 => '仅限所有者或创建者操作',
1 => '没有修改写入权限',
default => '没有查看访问权限',
};
throw new ApiException($msg);
}
$file->exceAllow(User::userid());
return $file;
}
/**
* 格式化内容数据
* @param array $data [path, size, ext, name]
* @return array
*/
public static function formatFileData(array $data)
{
$fileName = $data['name'];
$filePath = $data['path'];
$fileSize = $data['size'];
$fileExt = $data['ext'];
$publicPath = public_path($filePath);
//
switch ($fileExt) {
case 'md':
case 'text':
// 文本
$data['content'] = [
'type' => $fileExt,
'content' => file_get_contents($publicPath) ?: 'Content deleted',
];
$data['file_mode'] = $fileExt;
break;
case 'drawio':
// 图表
$data['content'] = [
'xml' => file_get_contents($publicPath)
];
$data['file_mode'] = $fileExt;
break;
case 'mind':
// 思维导图
$data['content'] = Base::json2array(file_get_contents($publicPath));
$data['file_mode'] = $fileExt;
break;
default:
if (in_array($fileExt, self::codeExt) && $fileSize < 2 * 1024 * 1024)
{
// 文本预览限制2M内的文件
$data['content'] = [
'content' => file_get_contents($publicPath) ?: 'Content deleted',
];
$data['file_mode'] = 'code';
}
elseif (in_array($fileExt, File::officeExt))
{
// office预览
$data['content'] = json_decode('{}');
$data['file_mode'] = 'office';
}
else
{
// 其他预览
$name = Base::rightDelete($fileName, ".{$fileExt}") . ".{$fileExt}";
$data['content'] = [
'preview' => true,
'name' => $name,
'key' => urlencode(Base::urlAddparameter($filePath, [
'name' => $name,
'ext' => $fileExt
])),
];
$data['file_mode'] = 'preview';
}
break;
}
return $data;
}
/**
* 移交文件
* @param $originalUserid
* @param $newUserid
* @return void
*/
public static function transfer($originalUserid, $newUserid)
{
if (!self::whereUserid($originalUserid)->exists()) {
return;
}
// 创建一个文件夹存放移交的文件
$name = User::userid2nickname($originalUserid) ?: ('ID:' . $originalUserid);
$file = File::createInstance([
'pid' => 0,
'name' => "{$name}】移交的文件",
'type' => "folder",
'ext' => "",
'userid' => $newUserid,
'created_id' => 0,
]);
$file->handleDuplicateName();
$file->saveBeforePP();
// 移交文件
self::whereUserid($originalUserid)->chunkById(100, function($list) use ($file, $newUserid) {
/** @var self $item */
foreach ($list as $item) {
if ($item->pid === 0) {
$item->pid = $file->id;
}
$item->userid = $newUserid;
$item->saveBeforePP();
}
});
// 移交文件权限
FileUser::whereUserid($originalUserid)->chunkById(100, function ($list) use ($newUserid) {
/** @var FileUser $item */
foreach ($list as $item) {
$row = FileUser::whereFileId($item->file_id)->whereUserid($newUserid)->first();
if ($row) {
// 已存在则删除原数据,判断改变已存在的数据
$row->permission = max($row->permission, $item->permission);
$row->save();
$item->delete();
} else {
// 不存在则改变原数据
$item->userid = $newUserid;
$item->save();
}
}
});
}
/**
* 获取文件树并计算文件总大小
*
* @param int $fileId
* @param User $user
* @param int $permission 0-访问权限、1-读写权限、1000-所有者或创建者
* @param string $path
* @param int $totalSize
* @return object
*/
public static function getFilesTree(int $fileId, User $user, $permission = 1, $path = '', &$totalSize = 0) {
$file = File::permissionFind($fileId, $user, $permission);
$file->path = ltrim($path . '/' . $file->name, '/');
$file->children = [];
if ($file->type == 'folder') {
$files = $file->getFileList($user, $fileId, 'all', false);
foreach ($files as &$childFile) {
$childFile['path'] = $file->path . '/' . $childFile['name'];
if ($childFile['type'] == 'folder') {
$childFile['children'] = self::getFilesTree($childFile['id'], $user, $permission, $file->path, $totalSize);
} else {
$totalSize += $childFile['size'];
}
}
$file->children = $files;
} else {
$totalSize += $file->size;
}
$file->totalSize = $totalSize;
return $file;
}
/**
* 文件夹文件添加到压缩文件
*
* @param \ZipArchive $zip
* @param object $file
* @return void
*/
public static function addFileTreeToZip($zip, $file)
{
if ($file->type != 'folder' && $file->name != '') {
$content = FileContent::whereFid($file->id)->orderByDesc('id')->first();
$content = Base::json2array($content?->content ?: []);
$typeExtensions = [
'word' => 'docx',
'excel' => 'xlsx',
'ppt' => 'pptx',
];
if (array_key_exists($file->type, $typeExtensions)) {
$filePath = empty($content) ? public_path('assets/office/empty.' . $typeExtensions[$file->type]) : public_path($content['url']);
}
//
$relativePath = $file->path . '.' . $file->ext;
if (file_exists($filePath)) {
$zip->addFile($filePath, $relativePath);
} else {
if (empty($content['url'])) {
$zip->addFromString($relativePath, $content['content']);
} else {
$filePath = public_path($content['url']);
$zip->addFile($filePath, $relativePath);
}
}
} else {
if (isset($file->children)) {
foreach ($file->children as $childFile) {
try {
self::addFileTreeToZip($zip, (object)$childFile);
} catch (\Exception $e) {
}
}
}
// 在压缩包中创建文件夹
$zip->addEmptyDir($file->path);
}
}
/**
* 文件推送消息
* @param $action
* @param array|null $data 发送内容
* @param array $userid 会员ID
*/
public static function filePushMsg($action, $data = null, $userid = null)
{
$userid = User::auth()->userid();
if (empty($userid)) {
return;
}
$msg = [
'type' => 'file',
'action' => $action,
'data' => $data,
];
$params = [
'userid' => $userid,
'msg' => $msg
];
Task::deliver(new PushTask($params));
}
}

View File

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

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

@@ -0,0 +1,70 @@
<?php
namespace App\Models;
use App\Module\Base;
/**
* App\Models\FileLink
*
* @property int $id
* @property int|null $file_id 项目ID
* @property int|null $num 累计访问
* @property string|null $code 链接码
* @property int|null $userid 会员ID
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\File|null $file
* @method static \Illuminate\Database\Eloquent\Builder|FileLink newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|FileLink newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|FileLink query()
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereCode($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereFileId($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereNum($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileLink whereUserid($value)
* @mixin \Eloquent
*/
class FileLink extends AbstractModel
{
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function file(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(File::class, 'id', 'file_id');
}
/**
* 生成链接
* @param $fileId
* @param $userid
* @param $refresh
* @return array
*/
public static function generateLink($fileId, $userid, $refresh = false)
{
$fileLink = FileLink::whereFileId($fileId)->whereUserid($userid)->first();
if (empty($fileLink)) {
$fileLink = FileLink::createInstance([
'file_id' => $fileId,
'userid' => $userid,
'code' => base64_encode("{$fileId},{$userid}," . Base::generatePassword()),
]);
$fileLink->save();
} else {
if ($refresh == 'yes') {
$fileLink->code = base64_encode("{$fileId},{$userid}," . Base::generatePassword());
$fileLink->save();
}
}
return [
'id' => $fileId,
'url' => Base::fillUrl('single/file/' . $fileLink->code),
'code' => $fileLink->code,
'num' => $fileLink->num
];
}
}

View File

@@ -4,12 +4,12 @@ namespace App\Models;
/**
* Class FileUser
* App\Models\FileUser
*
* @package App\Models
* @property int $id
* @property int|null $file_id 项目ID
* @property int|null $userid 成员ID
* @property int|null $permission 权限0只读1读写
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|FileUser newModelQuery()
@@ -18,10 +18,41 @@ namespace App\Models;
* @method static \Illuminate\Database\Eloquent\Builder|FileUser whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileUser whereFileId($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileUser whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileUser wherePermission($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileUser whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileUser whereUserid($value)
* @mixin \Eloquent
*/
class FileUser extends AbstractModel
{
/**
* 删除所有共享成员(同时删除成员分享的链接)
* @param $file_id
* @param int $retain_link_userid 保留指定会员的链接
* @return mixed
*/
public static function deleteFileAll($file_id, $retain_link_userid = 0)
{
return AbstractModel::transaction(function() use ($retain_link_userid, $file_id) {
if ($retain_link_userid > 0) {
FileLink::whereFileId($file_id)->where('userid', '!=', $retain_link_userid)->delete();
} else {
FileLink::whereFileId($file_id)->delete();
}
FileUser::whereFileId($file_id)->delete();
});
}
/**
* 删除指定共享成员(同时删除成员分享的链接)
* @param $file_id
* @param $userid
* @return mixed
*/
public static function deleteFileUser($file_id, $userid)
{
return AbstractModel::transaction(function() use ($userid, $file_id) {
FileLink::whereFileId($file_id)->whereUserid($userid)->delete();
return self::whereFileId($file_id)->whereUserid($userid)->delete();
});
}
}

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

@@ -0,0 +1,91 @@
<?php
namespace App\Models;
use Cache;
use App\Module\Base;
use Illuminate\Support\Carbon;
/**
* App\Models\Meeting
*
* @property int $id
* @property string|null $meetingid 会议ID不是数字
* @property string|null $name 会议主题
* @property string|null $channel 频道
* @property int|null $userid 创建人
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property string|null $end_at
* @property Carbon|null $deleted_at
* @method static \Illuminate\Database\Eloquent\Builder|Meeting newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Meeting newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Meeting query()
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereChannel($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereEndAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereMeetingid($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Meeting whereUserid($value)
* @mixin \Eloquent
*/
class Meeting extends AbstractModel
{
const CACHE_KEY = 'meeting_share_link_code';
const CACHE_EXPIRED_TIME = 6; // 小时
/**
* 获取分享链接
* @return mixed
*/
public function getShareLink()
{
$code = base64_encode("{$this->meetingid}" . Base::generatePassword());
Cache::put(self::CACHE_KEY.'_'.$code, [
'id' => $this->id,
'meetingid' => $this->meetingid,
'channel' => $this->channel,
], Carbon::now()->addHours(self::CACHE_EXPIRED_TIME));
return Base::fillUrl("meeting/{$this->meetingid}/".$code);
}
/**
* 获取分享信息
* @return mixed
*/
public static function getShareInfo($code)
{
if(Cache::has(self::CACHE_KEY.'_'.$code)){
return Cache::get(self::CACHE_KEY.'_'.$code);
}
return null;
}
/**
* 保存访客信息
* @return mixed
*/
public static function setTouristInfo($data)
{
Cache::put(Meeting::CACHE_KEY.'_'.$data['uid'], [
'uid' => $data['uid'],
'userimg' => $data['userimg'],
'nickname' => $data['nickname'],
], Carbon::now()->addHours(self::CACHE_EXPIRED_TIME));
}
/**
* 获取访客信息
* @return mixed
*/
public static function getTouristInfo($touristId)
{
if(Cache::has(Meeting::CACHE_KEY.'_'.$touristId)){
return Cache::get(Meeting::CACHE_KEY.'_'.$touristId);
}
return null;
}
}

View File

@@ -3,43 +3,42 @@
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use App\Tasks\PushTask;
use Arr;
use Carbon\Carbon;
use DB;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Database\Eloquent\SoftDeletes;
use Request;
/**
* Class Project
* App\Models\Project
*
* @package App\Models
* @property int $id
* @property string|null $name 名称
* @property string|null $desc 描述、备注
* @property int|null $userid 创建人
* @property int|mixed $dialog_id 聊天会话ID
* @property int|null $personal 是否个人项目
* @property string|null $user_simple 成员总数|1,2,3
* @property int|null $dialog_id 聊天会话ID
* @property string|null $archived_at 归档时间
* @property int|null $archived_userid 归档会员
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read int $owner_userid
* @property-read int $task_complete
* @property-read int $task_my_complete
* @property-read int $task_my_num
* @property-read int $task_my_percent
* @property-read int $task_num
* @property-read int $task_percent
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectColumn[] $projectColumn
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectColumn> $projectColumn
* @property-read int|null $project_column_count
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectLog[] $projectLog
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectLog> $projectLog
* @property-read int|null $project_log_count
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectUser[] $projectUser
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectUser> $projectUser
* @property-read int|null $project_user_count
* @method static \Illuminate\Database\Eloquent\Builder|Project authData($userid = null)
* @method static \Illuminate\Database\Eloquent\Builder|Project allData($userid = null)
* @method static \Illuminate\Database\Eloquent\Builder|Project authData($userid = null, $owner = null)
* @method static \Illuminate\Database\Eloquent\Builder|Project newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Project newQuery()
* @method static \Illuminate\Database\Query\Builder|Project onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|Project onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|Project query()
* @method static \Illuminate\Database\Eloquent\Builder|Project whereArchivedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereArchivedUserid($value)
@@ -49,109 +48,26 @@ use Request;
* @method static \Illuminate\Database\Eloquent\Builder|Project whereDialogId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project wherePersonal($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereUserSimple($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereUserid($value)
* @method static \Illuminate\Database\Query\Builder|Project withTrashed()
* @method static \Illuminate\Database\Query\Builder|Project withoutTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|Project withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|Project withoutTrashed()
* @mixin \Eloquent
*/
class Project extends AbstractModel
{
use SoftDeletes;
const projectSelect = [
'projects.*',
'project_users.owner',
protected $hidden = [
'deleted_at',
];
protected $appends = [
'task_num',
'task_complete',
'task_percent',
'task_my_num',
'task_my_complete',
'task_my_percent',
'owner_userid',
];
/**
* 生成任务数据
*/
private function generateTaskData()
{
if (!isset($this->appendattrs['task_num'])) {
$builder = ProjectTask::whereProjectId($this->id)->whereParentId(0)->whereNull('archived_at');
$this->appendattrs['task_num'] = $builder->count();
$this->appendattrs['task_complete'] = $builder->whereNotNull('complete_at')->count();
$this->appendattrs['task_percent'] = $this->appendattrs['task_num'] ? intval($this->appendattrs['task_complete'] / $this->appendattrs['task_num'] * 100) : 0;
//
$builder = ProjectTask::whereProjectId($this->id)->whereParentId(0)->authData(User::userid())->whereNull('archived_at');
$this->appendattrs['task_my_num'] = $builder->count();
$this->appendattrs['task_my_complete'] = $builder->whereNotNull('complete_at')->count();
$this->appendattrs['task_my_percent'] = $this->appendattrs['task_my_num'] ? intval($this->appendattrs['task_my_complete'] / $this->appendattrs['task_my_num'] * 100) : 0;
}
}
/**
* 任务数量
* @return int
*/
public function getTaskNumAttribute()
{
$this->generateTaskData();
return $this->appendattrs['task_num'];
}
/**
* 任务完成数量
* @return int
*/
public function getTaskCompleteAttribute()
{
$this->generateTaskData();
return $this->appendattrs['task_complete'];
}
/**
* 任务完成率
* @return int
*/
public function getTaskPercentAttribute()
{
$this->generateTaskData();
return $this->appendattrs['task_percent'];
}
/**
* 任务数量(我的)
* @return int
*/
public function getTaskMyNumAttribute()
{
$this->generateTaskData();
return $this->appendattrs['task_my_num'];
}
/**
* 任务完成数量(我的)
* @return int
*/
public function getTaskMyCompleteAttribute()
{
$this->generateTaskData();
return $this->appendattrs['task_my_complete'];
}
/**
* 任务完成率(我的)
* @return int
*/
public function getTaskMyPercentAttribute()
{
$this->generateTaskData();
return $this->appendattrs['task_my_percent'];
}
/**
* 负责人会员ID
* @return int
@@ -159,7 +75,7 @@ class Project extends AbstractModel
public function getOwnerUseridAttribute()
{
if (!isset($this->appendattrs['owner_userid'])) {
$ownerUser = $this->projectUser->where('owner', 1)->first();
$ownerUser = ProjectUser::whereProjectId($this->id)->whereOwner(1)->first();
$this->appendattrs['owner_userid'] = $ownerUser ? $ownerUser->userid : 0;
}
return $this->appendattrs['owner_userid'];
@@ -190,19 +106,73 @@ class Project extends AbstractModel
}
/**
* 查询自己的项目
* 查询所有项目与正常查询多返回owner字段
* @param self $query
* @param null $userid
* @return self
*/
public function scopeAuthData($query, $userid = null)
public function scopeAllData($query, $userid = null)
{
$userid = $userid ?: User::userid();
$query->join('project_users', 'projects.id', '=', 'project_users.project_id')
->where('project_users.userid', $userid);
$query
->select([
'projects.*',
'project_users.owner',
'project_users.top_at',
])
->leftJoin('project_users', function ($leftJoin) use ($userid) {
$leftJoin
->on('project_users.userid', '=', DB::raw($userid))
->on('projects.id', '=', 'project_users.project_id');
});
return $query;
}
/**
* 查询自己负责或参与的项目
* @param self $query
* @param null $userid
* @param null $owner
* @return self
*/
public function scopeAuthData($query, $userid = null, $owner = null)
{
$userid = $userid ?: User::userid();
$query
->select([
'projects.*',
'project_users.owner',
'project_users.top_at',
])
->join('project_users', 'projects.id', '=', 'project_users.project_id')
->where('project_users.userid', $userid);
if ($owner !== null) {
$query->where('project_users.owner', $owner);
}
return $query;
}
/**
* 获取任务统计数据
* @param $userid
* @return array
*/
public function getTaskStatistics($userid)
{
$array = [];
$builder = ProjectTask::whereProjectId($this->id)->whereNull('archived_at');
$array['task_num'] = $builder->count();
$array['task_complete'] = $builder->whereNotNull('complete_at')->count();
$array['task_percent'] = $array['task_num'] ? intval($array['task_complete'] / $array['task_num'] * 100) : 0;
//
$builder = ProjectTask::authData($userid, 1)->where('project_tasks.project_id', $this->id)->whereNull('project_tasks.archived_at');
$array['task_my_num'] = $builder->count();
$array['task_my_complete'] = $builder->whereNotNull('project_tasks.complete_at')->count();
$array['task_my_percent'] = $array['task_my_num'] ? intval($array['task_my_complete'] / $array['task_my_num'] * 100) : 0;
//
return $array;
}
/**
* 加入项目
* @param int $userid 加入的会员ID
@@ -237,9 +207,11 @@ class Project extends AbstractModel
WebSocketDialogUser::updateInsert([
'dialog_id' => $this->dialog_id,
'userid' => $userid,
], [
'important' => 1
]);
}
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->delete();
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->remove();
});
}
@@ -249,7 +221,7 @@ class Project extends AbstractModel
*/
public function relationUserids()
{
return $this->projectUser->pluck('userid')->toArray();
return ProjectUser::whereProjectId($this->id)->orderBy('id')->pluck('userid')->toArray();
}
/**
@@ -277,9 +249,10 @@ class Project extends AbstractModel
if ($archived_at === null) {
// 取消归档
$this->archived_at = null;
$this->archived_userid = User::userid();
$this->addLog("项目取消归档");
$this->pushMsg('add', $this);
ProjectTask::whereProjectId($this->id)->whereArchivedFollow(1)->update([
$this->pushMsg('recovery', $this);
ProjectTask::whereProjectId($this->id)->whereArchivedFollow(1)->change([
'archived_at' => null,
'archived_follow' => 0
]);
@@ -289,7 +262,7 @@ class Project extends AbstractModel
$this->archived_userid = User::userid();
$this->addLog("项目归档");
$this->pushMsg('archived');
ProjectTask::whereProjectId($this->id)->whereArchivedAt(null)->update([
ProjectTask::whereProjectId($this->id)->whereArchivedAt(null)->change([
'archived_at' => $archived_at,
'archived_follow' => 1
]);
@@ -322,18 +295,23 @@ class Project extends AbstractModel
/**
* 添加项目日志
* @param string $detail
* @param array $record
* @param int $userid
* @return ProjectLog
*/
public function addLog($detail, $userid = 0)
public function addLog($detail, $record = [], $userid = 0)
{
$log = ProjectLog::createInstance([
$array = [
'project_id' => $this->id,
'column_id' => 0,
'task_id' => 0,
'userid' => $userid ?: User::userid(),
'detail' => $detail,
]);
];
if ($record) {
$array['record'] = $record;
}
$log = ProjectLog::createInstance($array);
$log->save();
return $log;
}
@@ -341,45 +319,269 @@ class Project extends AbstractModel
/**
* 推送消息
* @param string $action
* @param array $data 发送内容,默认为[id=>项目ID]
* @param array $userid 指定会员,默认为项目所有成员
* @param array|self $data 发送内容,默认为[id=>项目ID]
* @param array $userid 指定会员,默认为项目所有成员
*/
public function pushMsg($action, $data = null, $userid = null)
{
if ($data === null) {
$data = ['id' => $this->id];
} elseif ($data instanceof self) {
$data = $data->toArray();
}
//
$array = [$userid, []];
if ($userid === null) {
$userid = $this->relationUserids();
$array[0] = $this->relationUserids();
} elseif (!is_array($userid)) {
$array[0] = [$userid];
}
//
if (isset($data['owner'])) {
$owners = ProjectUser::whereProjectId($data['id'])->whereOwner(1)->pluck('userid')->toArray();
$array = [array_intersect($array[0], $owners), array_diff($array[0], $owners)];
}
//
foreach ($array as $index => $item) {
if ($index > 0) {
$data['owner'] = 0;
}
$params = [
'ignoreFd' => Request::header('fd'),
'userid' => array_values($item),
'msg' => [
'type' => 'project',
'action' => $action,
'data' => $data,
]
];
$task = new PushTask($params, false);
Task::deliver($task);
}
$params = [
'ignoreFd' => Request::header('fd'),
'userid' => $userid,
'msg' => [
'type' => 'project',
'action' => $action,
'data' => $data,
]
];
$task = new PushTask($params, false);
Task::deliver($task);
}
/**
* 根据用户获取项目信息(用于判断会员是否存在项目内)
* 添加工作流
* @param $flows
* @return mixed
*/
public function addFlow($flows)
{
return AbstractModel::transaction(function() use ($flows) {
$projectFlow = ProjectFlow::whereProjectId($this->id)->first();
if (empty($projectFlow)) {
$projectFlow = ProjectFlow::createInstance([
'project_id' => $this->id,
'name' => 'Default'
]);
if (!$projectFlow->save()) {
throw new ApiException('工作流创建失败');
}
}
//
$ids = [];
$idc = [];
$hasStart = false;
$hasEnd = false;
$upTaskList = [];
foreach ($flows as $item) {
$id = intval($item['id']);
$turns = Base::arrayRetainInt($item['turns'] ?: [], true);
$userids = Base::arrayRetainInt($item['userids'] ?: [], true);
$usertype = trim($item['usertype']);
$userlimit = intval($item['userlimit']);
$columnid = intval($item['columnid']);
if ($usertype == 'replace' && empty($userids)) {
throw new ApiException("状态[{$item['name']}]设置错误,设置流转模式时必须填写状态负责人");
}
if ($usertype == 'merge' && empty($userids)) {
throw new ApiException("状态[{$item['name']}]设置错误,设置剔除模式时必须填写状态负责人");
}
if ($userlimit && empty($userids)) {
throw new ApiException("状态[{$item['name']}]设置错误,设置限制负责人时必须填写状态负责人");
}
$flow = ProjectFlowItem::updateInsert([
'id' => $id,
'project_id' => $this->id,
'flow_id' => $projectFlow->id,
], [
'name' => trim($item['name']),
'status' => trim($item['status']),
'sort' => intval($item['sort']),
'turns' => $turns,
'userids' => $userids,
'usertype' => trim($item['usertype']),
'userlimit' => $userlimit,
'columnid' => $columnid,
], [], $isInsert);
if ($flow) {
$ids[] = $flow->id;
if ($flow->id != $id) {
$idc[$id] = $flow->id;
}
if ($flow->status == 'start') {
$hasStart = true;
}
if ($flow->status == 'end') {
$hasEnd = true;
}
if (!$isInsert) {
$upTaskList[$flow->id] = $flow->status . "|" . $flow->name;
}
}
}
if (!$hasStart) {
throw new ApiException('至少需要1个开始状态');
}
if (!$hasEnd) {
throw new ApiException('至少需要1个结束状态');
}
ProjectFlowItem::whereFlowId($projectFlow->id)->whereNotIn('id', $ids)->chunk(100, function($list) {
foreach ($list as $item) {
$item->deleteFlowItem();
}
});
//
foreach ($upTaskList as $id => $value) {
ProjectTask::whereFlowItemId($id)->change([
'flow_item_name' => $value
]);
}
//
$projectFlow = ProjectFlow::with(['projectFlowItem'])->whereProjectId($this->id)->find($projectFlow->id);
$itemIds = $projectFlow->projectFlowItem->pluck('id')->toArray();
foreach ($projectFlow->projectFlowItem as $item) {
$turns = $item->turns;
foreach ($idc as $oid => $nid) {
if (in_array($oid, $turns)) {
$turns = array_diff($turns, [$oid]);
$turns[] = $nid;
}
}
if (!in_array($item->id, $turns)) {
$turns[] = $item->id;
}
$turns = array_values(array_filter(array_unique(array_intersect($turns, $itemIds))));
sort($turns);
$item->turns = $turns;
ProjectFlowItem::whereId($item->id)->update([ 'turns' => Base::array2json($turns) ]);
}
return $projectFlow;
});
}
/**
* 创建项目
* @param $params
* - name 项目名称
* - desc
* - flow
* - personal
* - columns
* @return array
*/
public static function createProject($params, $userid)
{
$name = trim(Arr::get($params, 'name', ''));
$desc = trim(Arr::get($params, 'desc', ''));
$flow = trim(Arr::get($params, 'flow', 'close'));
$isPersonal = intval(Arr::get($params, 'personal'));
if (mb_strlen($name) < 2) {
return Base::retError('项目名称不可以少于2个字');
} elseif (mb_strlen($name) > 32) {
return Base::retError('项目名称最多只能设置32个字');
}
if (mb_strlen($desc) > 255) {
return Base::retError('项目介绍最多只能设置255个字');
}
// 列表
$columns = explode(",", Arr::get($params, 'columns'));
$insertColumns = [];
$sort = 0;
foreach ($columns AS $column) {
$column = trim($column);
if ($column) {
$insertColumns[] = [
'name' => $column,
'sort' => $sort++,
];
}
}
if (empty($insertColumns)) {
$insertColumns[] = [
'name' => 'Default',
'sort' => 0,
];
}
if (count($insertColumns) > 30) {
return Base::retError('项目列表最多不能超过30个');
}
// 开始创建
$project = Project::createInstance([
'name' => $name,
'desc' => $desc,
'userid' => $userid,
]);
if ($isPersonal) {
if (Project::whereUserid($userid)->wherePersonal(1)->exists()) {
return Base::retError('个人项目已存在,无须重复创建');
}
$project->personal = 1;
}
AbstractModel::transaction(function() use ($flow, $insertColumns, $project) {
$project->save();
ProjectUser::createInstance([
'project_id' => $project->id,
'userid' => $project->userid,
'owner' => 1,
])->save();
foreach ($insertColumns AS $column) {
$column['project_id'] = $project->id;
ProjectColumn::createInstance($column)->save();
}
$dialog = WebSocketDialog::createGroup($project->name, $project->userid, 'project');
if (empty($dialog)) {
throw new ApiException('创建项目聊天室失败');
}
$project->dialog_id = $dialog->id;
$project->save();
//
if ($flow == 'open') {
$project->addFlow(Base::json2array('[{"id":-10,"name":"待处理","status":"start","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-11,"name":"进行中","status":"progress","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-12,"name":"待测试","status":"test","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-13,"name":"已完成","status":"end","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0},{"id":-14,"name":"已取消","status":"end","turns":[-10,-11,-12,-13,-14],"userids":[],"usertype":"add","userlimit":0,"columnid":0}]'));
}
});
//
$data = Project::find($project->id);
$data->addLog("创建项目");
$data->pushMsg('add', $data);
return Base::retSuccess('添加成功', $data);
}
/**
* 获取项目信息(用于判断会员是否存在项目内)
* @param int $project_id
* @param bool $ignoreArchived 排除已归档
* @param null|bool $archived true:仅限未归档, false:仅限已归档, null:不限制
* @param null|bool $mustOwner true:仅限项目负责人, false:仅限非项目负责人, null:不限制
* @return self
*/
public static function userProject($project_id, $ignoreArchived = true)
public static function userProject($project_id, $archived = true, $mustOwner = null)
{
$project = self::select(self::projectSelect)->authData()->where('projects.id', intval($project_id))->first();
$project = self::authData()->where('projects.id', intval($project_id))->first();
if (empty($project)) {
throw new ApiException('项目不存在或不在成员列表内', [ 'project_id' => $project_id ], -4001);
}
if ($ignoreArchived && $project->archived_at != null) {
if ($archived === true && $project->archived_at != null) {
throw new ApiException('项目已归档', [ 'project_id' => $project_id ], -4001);
}
if ($archived === false && $project->archived_at == null) {
throw new ApiException('项目未归档', [ 'project_id' => $project_id ]);
}
if ($mustOwner === true && !$project->owner) {
throw new ApiException('仅限项目负责人操作', [ 'project_id' => $project_id ]);
}
if ($mustOwner === false && $project->owner) {
throw new ApiException('禁止项目负责人操作', [ 'project_id' => $project_id ]);
}
return $project;
}
}

View File

@@ -9,9 +9,8 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Request;
/**
* Class ProjectColumn
* App\Models\ProjectColumn
*
* @package App\Models
* @property int $id
* @property int|null $project_id 项目ID
* @property string|null $name 列表名称
@@ -21,11 +20,11 @@ use Request;
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read \App\Models\Project|null $project
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectTask[] $projectTask
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectTask> $projectTask
* @property-read int|null $project_task_count
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn newQuery()
* @method static \Illuminate\Database\Query\Builder|ProjectColumn onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn query()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn whereColor($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn whereCreatedAt($value)
@@ -35,8 +34,8 @@ use Request;
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn whereProjectId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn whereSort($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn whereUpdatedAt($value)
* @method static \Illuminate\Database\Query\Builder|ProjectColumn withTrashed()
* @method static \Illuminate\Database\Query\Builder|ProjectColumn withoutTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectColumn withoutTrashed()
* @mixin \Eloquent
*/
class ProjectColumn extends AbstractModel
@@ -69,7 +68,9 @@ class ProjectColumn extends AbstractModel
AbstractModel::transaction(function () use ($pushMsg) {
$tasks = ProjectTask::whereColumnId($this->id)->get();
foreach ($tasks as $task) {
$task->deleteTask($pushMsg);
if(!$task->archived_at){
$task->deleteTask($pushMsg);
}
}
$this->delete();
$this->addLog("删除列表:" . $this->name);
@@ -120,7 +121,7 @@ class ProjectColumn extends AbstractModel
$userid = $this->project->relationUserids();
}
$params = [
'ignoreFd' => Request::header('fd'),
'ignoreFd' => $action == 'recovery' ? 0 : Request::header('fd'),
'userid' => $userid,
'msg' => [
'type' => 'projectColumn',

View File

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

View File

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

View File

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

View File

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

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;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,10 +2,12 @@
namespace App\Models;
use App\Module\Base;
use App\Exceptions\ApiException;
/**
* Class ProjectTaskContent
* App\Models\ProjectTaskContent
*
* @package App\Models
* @property int $id
* @property int|null $project_id 项目ID
* @property int|null $task_id 任务ID
@@ -29,4 +31,61 @@ class ProjectTaskContent extends AbstractModel
'created_at',
'updated_at',
];
/**
* 获取内容详情
* @return array
*/
public function getContentInfo()
{
$content = Base::json2array($this->content);
if (isset($content['url'])) {
$filePath = public_path($content['url']);
$array = $this->toArray();
$array['content'] = file_get_contents($filePath) ?: '';
if ($array['content']) {
$replace = Base::fillUrl('uploads/task');
$array['content'] = str_replace('{{RemoteURL}}uploads/task', $replace, $array['content']);
}
return $array;
}
return $this->toArray();
}
/**
* 保存任务详情至文件并返回文件路径
* @param $task_id
* @param $content
* @return string
*/
public static function saveContent($task_id, $content)
{
@ini_set("pcre.backtrack_limit", 999999999);
//
$oldContent = $content;
$path = 'uploads/task/content/' . date("Ym") . '/' . $task_id . '/';
//
preg_match_all("/<img\s+src=\"data:image\/(png|jpg|jpeg|webp);base64,(.*?)\"/s", $content, $matchs);
foreach ($matchs[2] as $key => $text) {
$tmpPath = $path . 'attached/';
Base::makeDir(public_path($tmpPath));
$tmpPath .= md5($text) . "." . $matchs[1][$key];
if (Base::saveContentImage(public_path($tmpPath), base64_decode($text))) {
$paramet = getimagesize(public_path($tmpPath));
$content = str_replace($matchs[0][$key], '<img src="{{RemoteURL}}' . $tmpPath . '" original-width="' . $paramet[0] . '" original-height="' . $paramet[1] . '"', $content);
}
}
$pattern = '/<img(.*?)src=("|\')https*:\/\/(.*?)\/(uploads\/task\/content\/(.*?))\2/is';
$content = preg_replace($pattern, '<img$1src=$2{{RemoteURL}}$4$2', $content);
//
$filePath = $path . md5($content);
$publicPath = public_path($filePath);
Base::makeDir(dirname($publicPath));
$result = file_put_contents($publicPath, $content);
if(!$result && $oldContent){
throw new ApiException("保存任务详情至文件失败,请重试");
}
//
return $filePath;
}
}

View File

@@ -3,23 +3,25 @@
namespace App\Models;
use App\Module\Base;
use Cache;
/**
* Class ProjectTaskFile
* App\Models\ProjectTaskFile
*
* @package App\Models
* @property int $id
* @property int|null $project_id 项目ID
* @property int|null $task_id 任务ID
* @property string|null $name 文件名称
* @property int|null $size 文件大小(B)
* @property string|null $ext 文件格式
* @property string|null $path 文件地址
* @property string|null $thumb 缩略图
* @property string $path 文件地址
* @property string $thumb 缩略图
* @property int|null $userid 上传用户ID
* @property int|null $download 下载次数
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read int $height
* @property-read int $width
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskFile query()
@@ -39,6 +41,11 @@ use App\Module\Base;
*/
class ProjectTaskFile extends AbstractModel
{
protected $appends = [
'width',
'height',
];
/**
* 地址
* @param $value
@@ -58,4 +65,50 @@ class ProjectTaskFile extends AbstractModel
{
return Base::fillUrl($value ?: Base::extIcon($this->ext));
}
/**
* 宽
* @return int
*/
public function getWidthAttribute()
{
$this->generateSizeData();
return $this->appendattrs['width'];
}
/**
* 高
* @return int
*/
public function getHeightAttribute()
{
$this->generateSizeData();
return $this->appendattrs['height'];
}
/**
* 生成尺寸数据
*/
private function generateSizeData()
{
if (!isset($this->appendattrs['width'])) {
$width = -1;
$height = -1;
if (in_array($this->ext, ['jpg', 'jpeg', 'webp', 'gif', 'png'])) {
$path = public_path($this->getRawOriginal('path'));
[$width, $height] = Cache::remember("File::size-" . md5($path), now()->addDays(7), function () use ($path) {
$width = -1;
$height = -1;
if (file_exists($path)) {
$paramet = getimagesize($path);
$width = $paramet[0];
$height = $paramet[1];
}
return [$width, $height];
});
}
$this->appendattrs['width'] = $width;
$this->appendattrs['height'] = $height;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -2,16 +2,14 @@
namespace App\Models;
use App\Module\Base;
/**
* Class ProjectUser
* App\Models\ProjectUser
*
* @package App\Models
* @property int $id
* @property int|null $project_id 项目ID
* @property int|null $userid 成员ID
* @property int|null $owner 是否负责人
* @property string|null $top_at 置顶时间
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Project|null $project
@@ -22,6 +20,7 @@ use App\Module\Base;
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereOwner($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereProjectId($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereTopAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectUser whereUserid($value)
* @mixin \Eloquent
@@ -37,17 +36,59 @@ class ProjectUser extends AbstractModel
return $this->hasOne(Project::class, 'id', 'project_id');
}
/**
* 移交项目身份
* @param $originalUserid
* @param $newUserid
* @return void
*/
public static function transfer($originalUserid, $newUserid)
{
self::whereUserid($originalUserid)->chunkById(100, function ($list) use ($originalUserid, $newUserid) {
/** @var self $item */
foreach ($list as $item) {
$row = self::whereProjectId($item->project_id)->whereUserid($newUserid)->first();
if ($row) {
// 已存在则删除原数据,判断改变已存在的数据
$row->owner = max($row->owner, $item->owner);
$row->save();
$item->delete();
} else {
// 不存在则改变原数据
$item->userid = $newUserid;
$item->save();
}
if ($item->project) {
if ($item->project->personal) {
$name = User::userid2nickname($originalUserid) ?: ('ID:' . $originalUserid);
$item->project->name = "{$name}{$item->project->name}";
$item->project->save();
}
$item->project->addLog("移交项目身份", ['userid' => [$originalUserid, ' => ', $newUserid]]);
$item->project->syncDialogUser();
}
}
});
}
/**
* 退出项目
*/
public function exitProject()
{
$tasks = ProjectTask::whereProjectId($this->project_id)->authData($this->userid)->get();
foreach ($tasks as $task) {
if (ProjectTaskUser::whereTaskId($task->id)->whereUserid($this->userid)->delete()) {
$task->syncDialogUser();
}
}
ProjectTaskUser::whereProjectId($this->project_id)
->whereUserid($this->userid)
->chunk(100, function ($list) {
$tastIds = [];
/** @var ProjectTaskUser $item */
foreach ($list as $item) {
$item->delete();
if (!in_array($item->task_pid, $tastIds)) {
$tastIds[] = $item->task_pid;
$item->projectTask?->syncDialogUser();
}
}
});
$this->delete();
}
}

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

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

View File

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

View File

@@ -2,10 +2,11 @@
namespace App\Models;
use App\Module\Base;
/**
* Class Setting
* App\Models\Setting
*
* @package App\Models
* @property int $id
* @property string|null $name
* @property string|null $desc 参数描述、备注
@@ -25,5 +26,40 @@ namespace App\Models;
*/
class Setting extends AbstractModel
{
/**
* 验证邮箱地址(过滤忽略地址)
* @param $array
* @param \Closure $resultClosure
* @param \Closure|null $emptyClosure
* @return array|mixed
*/
public static function validateAddr($array, $resultClosure, $emptyClosure = null)
{
if (!is_array($array)) {
$array = [$array];
}
$ignoreAddr = Base::settingFind('emailSetting', 'ignore_addr');
$ignoreAddr = explode("\n", $ignoreAddr);
$ignoreArray = ['admin@dootask.com', 'test@dootask.com'];
foreach ($ignoreAddr as $item) {
if (Base::isEmail($item)) {
$ignoreArray[] = trim($item);
}
}
if ($ignoreArray) {
$array = array_diff($array, $ignoreArray);
}
if ($array) {
if ($resultClosure instanceof \Closure) {
foreach ($array as $value) {
$resultClosure($value);
}
}
} else {
if ($emptyClosure instanceof \Closure) {
$emptyClosure();
}
}
return $array;
}
}

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

@@ -0,0 +1,37 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* App\Models\TaskWorker
*
* @property int $id
* @property string|null $args
* @property string|null $error
* @property string|null $start_at 开始时间
* @property string|null $end_at 结束时间
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker query()
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereArgs($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereEndAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereError($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereStartAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker withoutTrashed()
* @mixin \Eloquent
*/
class TaskWorker extends AbstractModel
{
use SoftDeletes;
}

View File

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

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

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

View File

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

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

@@ -0,0 +1,295 @@
<?php
namespace App\Models;
use App\Module\Base;
use App\Module\Doo;
use App\Module\Extranet;
use App\Tasks\JokeSoupTask;
use Cache;
use Carbon\Carbon;
/**
* App\Models\UserBot
*
* @property int $id
* @property int|null $userid 所属人ID
* @property int|null $bot_id 机器人ID
* @property int|null $clear_day 消息自动清理天数
* @property string|null $clear_at 下一次清理时间
* @property string|null $webhook_url 消息webhook地址
* @property int|null $webhook_num 消息webhook请求次数
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|UserBot newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserBot newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserBot query()
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereBotId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereClearAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereClearDay($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereWebhookNum($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereWebhookUrl($value)
* @mixin \Eloquent
*/
class UserBot extends AbstractModel
{
/**
* 系统机器人名称
* @param $name string 邮箱 或 邮箱前缀
* @return string
*/
public static function systemBotName($name)
{
if (str_contains($name, "@")) {
$name = explode("@", $name)[0];
}
return match ($name) {
'system-msg' => '系统消息',
'task-alert' => '任务提醒',
'check-in' => '签到打卡',
'anon-msg' => '匿名消息',
'approval-alert' => '审批',
'ai-openai' => 'ChatGPT',
'ai-claude' => 'Claude',
'ai-wenxin' => '文心一言',
'ai-qianwen' => '通义千问',
'ai-gemini' => 'Gemini',
'bot-manager' => '机器人管理',
'meeting-alert' => '会议通知',
'okr-alert' => 'OKR提醒',
default => '', // 不是系统机器人时返回空(也可以拿来判断是否是系统机器人)
};
}
/**
* 机器人菜单
* @param $email
* @return array|array[]
*/
public static function quickMsgs($email)
{
return match ($email) {
'check-in@bot.system' => [
[
'key' => 'checkin',
'label' => Doo::translate('我要打卡')
], [
'key' => 'it',
'label' => Doo::translate('IT资讯')
], [
'key' => '36ke',
'label' => Doo::translate('36氪')
], [
'key' => '60s',
'label' => Doo::translate('60s读世界')
], [
'key' => 'joke',
'label' => Doo::translate('开心笑话')
], [
'key' => 'soup',
'label' => Doo::translate('心灵鸡汤')
]
],
'anon-msg@bot.system' => [
[
'key' => 'help',
'label' => Doo::translate('使用说明')
], [
'key' => 'privacy',
'label' => Doo::translate('隐私说明')
],
],
'bot-manager@bot.system' => [
[
'key' => '/help',
'label' => Doo::translate('帮助指令')
], [
'key' => '/api',
'label' => Doo::translate('Api接口文档')
], [
'key' => '/list',
'label' => Doo::translate('我的机器人')
],
],
'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 => [],
};
}
/**
* 签到机器人
* @param $command
* @param $userid
* @return string
*/
public static function checkinBotQuickMsg($command, $userid)
{
if (Cache::get("UserBot::checkinBotQuickMsg:{$userid}") === "yes") {
return "操作频繁!";
}
Cache::put("UserBot::checkinBotQuickMsg:{$userid}", "yes", Carbon::now()->addSecond());
//
if ($command === 'checkin') {
$setting = Base::setting('checkinSetting');
if ($setting['open'] !== 'open') {
return '暂未开启签到功能。';
}
if (!in_array('manual', $setting['modes'])) {
return '暂未开放手动签到。';
}
if ($error = UserBot::checkinBotCheckin($userid, Base::time(), true)) {
return $error;
}
return null;
} else {
return Extranet::checkinBotQuickMsg($command);
}
}
/**
* 签到机器人签到
* @param $mac
* @param $time
* @param bool $alreadyTip 签到过是否提示
* @return string|null 返回string表示错误信息返回null表示签到成功
*/
public static function checkinBotCheckin($mac, $time, $alreadyTip = false)
{
$setting = Base::setting('checkinSetting');
$times = $setting['time'] ? Base::json2array($setting['time']) : ['09:00', '18:00'];
$advance = (intval($setting['advance']) ?: 120) * 60;
$delay = (intval($setting['delay']) ?: 120) * 60;
//
$nowDate = date("Y-m-d");
$nowTime = date("H:i:s");
//
$timeStart = strtotime("{$nowDate} {$times[0]}");
$timeEnd = strtotime("{$nowDate} {$times[1]}");
$timeAdvance = max($timeStart - $advance, strtotime($nowDate));
$timeDelay = min($timeEnd + $delay, strtotime("{$nowDate} 23:59:59"));
if (Base::time() < $timeAdvance || $timeDelay < Base::time()) {
return "不在有效时间内,有效时间为:" . date("H:i", $timeAdvance) . "-" . date("H:i", $timeDelay);
}
//
$macs = explode(",", $mac);
$checkins = [];
foreach ($macs as $mac) {
$mac = strtoupper($mac);
$array = [];
if (Base::isMac($mac)) {
if ($UserCheckinMac = UserCheckinMac::whereMac($mac)->first()) {
$array = [
'userid' => $UserCheckinMac->userid,
'mac' => $UserCheckinMac->mac,
'date' => $nowDate,
];
$checkins[] = [
'userid' => $UserCheckinMac->userid,
'remark' => $UserCheckinMac->remark,
];
}
} elseif (Base::isNumber($mac)) {
if ($UserInfo = User::whereUserid($mac)->whereBot(0)->first()) {
$array = [
'userid' => $UserInfo->userid,
'mac' => '00:00:00:00:00:00',
'date' => $nowDate,
];
$checkins[] = [
'userid' => $UserInfo->userid,
'remark' => '手动签到',
];
}
}
if ($array) {
$record = UserCheckinRecord::where($array)->first();
if (empty($record)) {
$record = UserCheckinRecord::createInstance($array);
}
$record->times = Base::array2json(array_merge($record->times, [$nowTime]));
$record->report_time = $time;
$record->save();
}
}
//
if ($checkins && $botUser = User::botGetOrCreate('check-in')) {
$getJokeSoup = function($type) {
$pre = $type == "up" ? "每日开心:" : "心灵鸡汤:";
$key = $type == "up" ? "jokes" : "soups";
$array = Base::json2array(Cache::get(JokeSoupTask::keyName($key)));
if ($array) {
$item = $array[array_rand($array)];
if ($item) {
return $pre . $item;
}
}
return null;
};
$sendMsg = function($type, $checkin) use ($alreadyTip, $getJokeSoup, $botUser, $nowDate) {
$cacheKey = "Checkin::sendMsg-{$nowDate}-{$type}:" . $checkin['userid'];
$typeDesc = $type == "up" ? "上班" : "下班";
if (Cache::get($cacheKey) === "yes") {
if ($alreadyTip && $dialog = WebSocketDialog::checkUserDialog($botUser, $checkin['userid'])) {
$text = "<p>今日已{$typeDesc}打卡,无需重复打卡。</p>";
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, $type != "up");
}
return;
}
Cache::put($cacheKey, "yes", Carbon::now()->addDay());
//
if ($dialog = WebSocketDialog::checkUserDialog($botUser, $checkin['userid'])) {
$hi = date("H:i");
$remark = $checkin['remark'] ? " ({$checkin['remark']})": "";
$text = "<p>{$typeDesc}打卡成功,打卡时间: {$hi}{$remark}</p>";
$suff = $getJokeSoup($type);
if ($suff) {
$text = "{$text}<p>----------</p><p>{$suff}</p>";
}
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, $type != "up");
}
};
if ($timeAdvance <= Base::time() && Base::time() < $timeEnd) {
// 上班打卡通知(从最早打卡时间 到 下班打卡时间)
foreach ($checkins as $checkin) {
$sendMsg('up', $checkin);
}
}
if ($timeEnd <= Base::time() && Base::time() <= $timeDelay) {
// 下班打卡通知(下班打卡时间 到 最晚打卡时间)
foreach ($checkins as $checkin) {
$sendMsg('down', $checkin);
}
}
}
return null;
}
/**
* 隐私机器人
* @param $command
* @return string
*/
public static function anonBotQuickMsg($command)
{
return match ($command) {
"help" => "使用说明:打开你想要发匿名消息的个人对话,点击输入框右边的 ⊕ 号,选择 <u>匿名消息</u> 即可输入你想要发送的匿名消息内容。",
"privacy" => "匿名消息将通过 <u>匿名消息(机器人)</u> 发送给对方,不会记录你的身份信息。",
default => '',
};
}
}

View File

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

View File

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

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

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

View File

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

View File

@@ -0,0 +1,133 @@
<?php
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use Carbon\Carbon;
use Guanguans\Notify\Factory;
use Guanguans\Notify\Messages\EmailMessage;
/**
* App\Models\UserEmailVerification
*
* @property int $id
* @property int|null $userid 用户id
* @property string|null $code 验证参数
* @property string|null $email 电子邮箱
* @property int|null $status 0-未验证1-已验证
* @property int|null $type 邮件类型1-邮箱认证2-修改邮箱
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification query()
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereCode($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereEmail($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereStatus($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserEmailVerification whereUserid($value)
* @mixin \Eloquent
*/
class UserEmailVerification extends AbstractModel
{
/**
* 发验证邮箱
* @param User $user
* @param int $type
* @param null $email
*/
public static function userEmailSend(User $user, $type = 1, $email = null)
{
$email = $type == 1 ? $user->email : $email;
$res = self::whereEmail($email)->where('created_at', '>', Carbon::now()->subMinutes(30))->whereType($type)->first();
if ($res && $type == 1) return;
//删除
self::whereUserid($email)->delete();
$code = $type == 1 ? Base::generatePassword(64) : rand(100000, 999999);
$row = self::createInstance([
'userid' => $user->userid,
'email' => $email,
'code' => $code,
'status' => 0,
'type' => $type
]);
$row->save();
$setting = Base::setting('emailSetting');
try {
if (!Base::isEmail($email)) {
throw new \Exception("User email '{$email}' address error");
}
switch ($type) {
case 2:
$subject = env('APP_NAME') . "修改邮箱验证";
$content = "<p>{$user->nickname} 您好,您正在修改 " . env('APP_NAME') . " 的邮箱验证码如下。请在30分钟内输入验证码</p><p style='color: #0000DD;'><u>$code</u></p><p>如果不是本人操作,您的帐号可能存在风险,请及时修改密码!</p>";
break;
case 3:
$subject = env('APP_NAME') . "注销帐号验证";
$content = "<p>{$user->nickname} 您好,您正在注销 " . env('APP_NAME') . " 的帐号验证码如下。请在30分钟内输入验证码</p><p style='color: #0000DD;'><u>$code</u></p><p>如果不是本人操作,您的帐号可能存在风险,请及时修改密码!</p>";
break;
default:
$url = Base::fillUrl('single/valid/email') . '?code=' . $row->code;
$subject = env('APP_NAME') . "绑定邮箱验证";
$content = "<p>{$user->nickname} 您好,您正在绑定 " . env('APP_NAME') . " 的邮箱请于30分钟之内点击以下链接完成验证 :</p><p style='display: flex; justify-content: center;'><a href='{$url}' target='_blank'>{$url}</a></p>";
break;
}
Factory::mailer()
->setDsn("smtp://{$setting['account']}:{$setting['password']}@{$setting['smtp_server']}:{$setting['port']}?verify_peer=0")
->setMessage(EmailMessage::create()
->from(env('APP_NAME', 'Task') . " <{$setting['account']}>")
->to($email)
->subject($subject)
->html($content))
->send();
} catch (\Throwable $e) {
if (str_contains($e->getMessage(), "Timed Out")) {
throw new ApiException("language.TimedOut");
} elseif ($e->getCode() === 550) {
throw new ApiException('邮件内容被拒绝,请检查邮箱是否开启接收功能');
} else {
throw new ApiException($e->getMessage());
}
}
}
/**
* 校验验证码
* @param $email
* @param $code
* @param int $type
* @return bool
*/
public static function verify($email, $code, $type = 1)
{
if (!$code) {
throw new ApiException('请输入验证码');
}
/** @var UserEmailVerification $emailVerify */
$emailVerify = self::whereEmail($email)->whereType($type)->orderByDesc('id')->first();
if (empty($emailVerify) || $emailVerify->code != $code) {
throw new ApiException('验证码错误');
}
$oldTime = Carbon::parse($emailVerify->created_at)->timestamp;
$time = Base::Time();
// 30分钟失效
if (abs($time - $oldTime) > 1800) {
throw new ApiException('验证码已失效');
}
self::whereEmail($email)->whereCode($code)->whereType($type)->update([
'status' => 1
]);
return true;
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use Carbon\Carbon;
use Guanguans\Notify\Factory;
use Guanguans\Notify\Messages\EmailMessage;
/**
* App\Models\UserTransfer
*
* @property int $id
* @property int|null $original_userid 原作者
* @property int|null $new_userid 交接人
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer query()
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer whereNewUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer whereOriginalUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserTransfer whereUpdatedAt($value)
* @mixin \Eloquent
*/
class UserTransfer extends AbstractModel
{
/**
* 开始移交
* @return void
*/
public function start()
{
// 移交部门
UserDepartment::transfer($this->original_userid, $this->new_userid);
// 移交项目身份
ProjectUser::transfer($this->original_userid, $this->new_userid);
// 移交任务身份
ProjectTaskUser::transfer($this->original_userid, $this->new_userid);
// 移交文件
File::transfer($this->original_userid, $this->new_userid);
// 离职移出群组
WebSocketDialog::select(['web_socket_dialogs.*'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->where('web_socket_dialogs.type', 'group')
->where('u.userid', $this->original_userid)
->orderByDesc('web_socket_dialogs.id')
->chunk(100, function($list) {
/** @var WebSocketDialog $dialog */
foreach ($list as $dialog) {
// 离职员工退出群
$dialog->exitGroup($this->original_userid, 'remove', false, false);
if ($dialog->owner_id === $this->original_userid) {
// 如果是群主则把交接人设为群主
$dialog->owner_id = $this->new_userid;
if ($dialog->save()) {
$dialog->joinGroup($this->new_userid, 0);
$dialog->pushMsg("groupUpdate", [
'id' => $dialog->id,
'owner_id' => $dialog->owner_id,
]);
}
}
}
});
}
}

View File

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

View File

@@ -3,43 +3,66 @@
namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use App\Module\Doo;
use App\Tasks\PushTask;
use Cache;
use Carbon\Carbon;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\DB;
/**
* Class WebSocketDialog
* App\Models\WebSocketDialog
*
* @package App\Models
* @property int $id
* @property string|null $type 对话类型
* @property string|null $group_type 聊天室类型
* @property string|null $name 对话名称
* @property string|null $last_at 最后消息时间
* @property string $avatar 头像(群)
* @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
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\WebSocketDialogUser[] $dialogUser
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\WebSocketDialogUser> $dialogUser
* @property-read int|null $dialog_user_count
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog newQuery()
* @method static \Illuminate\Database\Query\Builder|WebSocketDialog onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog query()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereAvatar($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereGroupType($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereLastAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog 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\Query\Builder|WebSocketDialog withTrashed()
* @method static \Illuminate\Database\Query\Builder|WebSocketDialog withoutTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog withoutTrashed()
* @mixin \Eloquent
*/
class WebSocketDialog extends AbstractModel
{
use SoftDeletes;
/**
* 头像地址
* @param $value
* @return string
*/
public function getAvatarAttribute($value)
{
return $value ? Base::fillUrl($value) : $value;
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
@@ -48,6 +71,385 @@ class WebSocketDialog extends AbstractModel
return $this->hasMany(WebSocketDialogUser::class, 'dialog_id', 'id');
}
/**
* 获取对话列表
* @param int $userid 会员ID
* @param $updated
* @param $deleted
* @return array
*/
public static function getDialogList($userid, $updated = "", $deleted = "")
{
$builder = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->where('u.userid', $userid);
if ($updated) {
$builder->where('u.updated_at', '>', $updated);
}
$list = $builder
->orderByDesc('u.top_at')
->orderByDesc('u.last_at')
->paginate(Base::getPaginate(100, 50));
$list->transform(function (WebSocketDialog $item) use ($userid) {
return $item->formatData($userid);
});
//
$data = $list->toArray();
if ($list->currentPage() === 1) {
$data['deleted_id'] = Deleted::ids('dialog', $userid, $deleted);
}
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
* @param bool $hasData
* @return $this
*/
public function formatData($userid, $hasData = false)
{
$dialogUserFun = function ($key, $default = null) use ($userid) {
$data = Cache::remember("Dialog::formatData-{$this->id}-{$userid}", now()->addSeconds(10), function () use ($userid) {
return WebSocketDialogUser::whereDialogId($this->id)->whereUserid($userid)->first()?->toArray();
});
return $data[$key] ?? $default;
};
//
$time = Carbon::parse($this->user_at ?? $dialogUserFun('updated_at'));
$this->hide = $this->hide ?? $dialogUserFun('hide');
$this->top_at = $this->top_at ?? $dialogUserFun('top_at');
$this->last_at = $this->last_at ?? $dialogUserFun('last_at');
$this->user_at = $time->toDateTimeString('millisecond');
$this->user_ms = $time->valueOf();
//
if (isset($this->search_msg_id)) {
// 最后消息 (搜索预览消息)
$this->last_msg = WebSocketDialogMsg::whereDialogId($this->id)->find($this->search_msg_id);
$this->last_at = $this->last_msg ? Carbon::parse($this->last_msg->created_at)->format('Y-m-d H:i:s') : null;
} else {
// 未读信息
if (Base::judgeClientVersion("0.34.0")) {
$this->generateUnread($userid);
} else {
$this->generateUnread_03398($userid, $hasData);
}
// 未读标记
$this->mark_unread = $this->mark_unread ?? $dialogUserFun('mark_unread');
// 是否免打扰
$this->silence = $this->silence ?? $dialogUserFun('silence');
// 对话人数
$this->people = WebSocketDialogUser::whereDialogId($this->id)->count();
// 有待办
$this->todo_num = WebSocketDialogMsgTodo::whereDialogId($this->id)->whereUserid($userid)->whereDoneAt(null)->count();
// 最后消息
$this->last_msg = WebSocketDialogMsg::whereDialogId($this->id)->orderByDesc('id')->first();
}
// 对方信息
$this->pinyin = Base::cn2pinyin($this->name);
$this->quick_msgs = [];
$this->dialog_user = null;
$this->group_info = null;
$this->bot = 0;
switch ($this->type) {
case "user":
$dialog_user = WebSocketDialogUser::whereDialogId($this->id)->where('userid', '!=', $userid)->first();
if ($dialog_user->userid === 0) {
$dialog_user->userid = $userid;
}
$basic = User::userid2basic($dialog_user->userid);
if ($basic) {
$this->name = $basic->nickname;
$this->email = $basic->email;
$this->userimg = $basic->userimg;
$this->bot = $basic->getBotOwner();
$this->quick_msgs = UserBot::quickMsgs($basic->email);
} else {
$this->name = 'non-existent';
$this->dialog_delete = 1;
}
$this->dialog_user = $dialog_user;
$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) {
$this->name = $this->group_info->name;
} else {
$this->name = '[Delete]';
$this->dialog_delete = 1;
}
break;
case 'task':
$this->group_info = ProjectTask::withTrashed()->select(['id', 'name', 'complete_at', 'archived_at', 'deleted_at'])->whereDialogId($this->id)->first()?->cancelAppend()->cancelHidden();
if ($this->group_info) {
$this->name = $this->group_info->name;
} else {
$this->name = '[Delete]';
$this->dialog_delete = 1;
}
break;
case 'all':
$this->name = Doo::translate('全体成员');
$this->dialog_mute = Base::settingFind('system', 'all_group_mute');
break;
}
break;
}
if ($hasData === true) {
$msgBuilder = WebSocketDialogMsg::whereDialogId($this->id);
$this->has_tag = $msgBuilder->clone()->where('tag', '>', 0)->exists();
$this->has_todo = $msgBuilder->clone()->where('todo', '>', 0)->exists();
$this->has_image = $msgBuilder->clone()->whereMtype('image')->exists();
$this->has_file = $msgBuilder->clone()->whereMtype('file')->exists();
$this->has_link = $msgBuilder->clone()->whereLink(1)->exists();
Cache::forever("Dialog::tag:" . $this->id, Base::array2json([
'has_tag' => $this->has_tag,
'has_todo' => $this->has_todo,
'has_image' => $this->has_image,
'has_file' => $this->has_file,
'has_link' => $this->has_link,
]));
} 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;
}
/**
* 生成未读数据
* @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_03398($userid, $positionData = false)
{
$builder = WebSocketDialogMsgRead::whereDialogId($this->id)->whereUserid($userid)->whereReadAt(null);
$this->unread = $builder->count();
$this->mention = $this->unread > 0 ? $builder->clone()->whereMention(1)->count() : 0;
if ($positionData) {
$array = [];
// @我的消息
if ($this->mention > 0) {
$list = $builder->clone()->whereMention(1)->orderByDesc('msg_id')->take(20)->get();
foreach ($list as $item) {
$array[] = [
'msg_id' => $item->msg_id,
'label' => Doo::translate('@我的消息'),
];
}
}
// 最早一条未读消息
if ($this->unread > 0
&& $first_id = intval($builder->clone()->orderBy('msg_id')->value('msg_id'))) {
$array[] = [
'msg_id' => $first_id,
'label' => '{UNREAD}'
];
}
//
$this->position_msgs = $array;
}
return $this;
}
/**
* 加入聊天室
* @param int|array $userid 加入的会员ID或会员ID组
* @param int $inviter 邀请人
* @param bool|null $important 重要人员(null不修改、bool修改)
* @return bool
*/
public function joinGroup($userid, $inviter, $important = null)
{
AbstractModel::transaction(function () use ($important, $inviter, $userid) {
foreach (is_array($userid) ? $userid : [$userid] as $value) {
if ($value > 0) {
$updateData = [
'inviter' => $inviter,
];
if (is_bool($important)) {
$updateData['important'] = $important ? 1 : 0;
}
$isInsert = false;
WebSocketDialogUser::updateInsert([
'dialog_id' => $this->id,
'userid' => $value,
], $updateData, [], $isInsert);
if ($isInsert) {
WebSocketDialogMsg::sendMsg(null, $this->id, 'notice', [
'notice' => User::userid2nickname($value) . " 已加入群组"
], $inviter, true, true);
}
}
}
});
$this->pushMsg("groupUpdate", [
'id' => $this->id,
'people' => WebSocketDialogUser::whereDialogId($this->id)->count()
]);
return true;
}
/**
* 退出聊天室
* @param int|array $userid 退出的会员ID或会员ID组
* @param string $type exit|remove
* @param bool $checkDelete 是否检查删除
* @param bool $pushMsg 是否推送消息
*/
public function exitGroup($userid, $type = 'exit', $checkDelete = true, $pushMsg = true)
{
$typeDesc = $type === 'remove' ? '移出' : '退出';
AbstractModel::transaction(function () use ($pushMsg, $checkDelete, $typeDesc, $type, $userid) {
$builder = WebSocketDialogUser::whereDialogId($this->id);
if (is_array($userid)) {
$builder->whereIn('userid', $userid);
} else {
$builder->whereUserid($userid);
}
$builder->chunkById(100, function($list) use ($pushMsg, $checkDelete, $typeDesc, $type) {
/** @var WebSocketDialogUser $item */
foreach ($list as $item) {
if ($checkDelete) {
if ($type === 'remove') {
// 移出时:如果是全员群仅允许管理员操作,其他群仅群主或邀请人可以操作
if ($this->group_type === 'all') {
User::auth("admin");
} elseif (!in_array(User::userid(), [$this->owner_id, $item->inviter])) {
throw new ApiException('只有群主或邀请人可以移出成员');
}
}
if ($item->userid == $this->owner_id) {
throw new ApiException('群主不可' . $typeDesc);
}
if ($item->important) {
throw new ApiException('部门成员、项目人员或任务人员不可' . $typeDesc);
}
}
//
$item->delete();
//
if ($pushMsg) {
if ($type === 'remove') {
$notice = User::nickname() . "" . User::userid2nickname($item->userid) . " 移出群组";
} else {
$notice = User::userid2nickname($item->userid) . " 退出群组";
}
WebSocketDialogMsg::sendMsg(null, $this->id, 'notice', [
'notice' => $notice
], User::userid(), true, true);
}
}
});
});
//
$this->pushMsg("groupUpdate", [
'id' => $this->id,
'people' => WebSocketDialogUser::whereDialogId($this->id)->count()
]);
}
/**
* 删除会话
* @return bool
@@ -55,20 +457,148 @@ class WebSocketDialog extends AbstractModel
public function deleteDialog()
{
AbstractModel::transaction(function () {
WebSocketDialogMsgRead::whereDialogId($this->id)->whereNull('read_at')->update([
'read_at' => Carbon::now()
]);
WebSocketDialogMsgRead::whereDialogId($this->id)
->whereNull('read_at')
->chunkById(100, function ($list) {
WebSocketDialogMsgRead::onlyMarkRead($list);
});
$this->delete();
});
$this->pushMsg("groupDelete");
return true;
}
/**
* 还原会话
* @return bool
*/
public function restoreDialog()
{
$this->restore();
$this->pushMsg("groupRestore");
return true;
}
/**
* 检查群组类型
* @param string|array|null $groupType
* @return void
*/
public function checkGroup($groupType = null)
{
if ($this->type !== 'group') {
throw new ApiException('仅限群组操作');
}
if ($groupType) {
$groupTypes = is_array($groupType) ? $groupType : [$groupType];
if (!in_array($this->group_type, $groupTypes)) {
throw new ApiException('操作的群组类型错误');
}
}
}
/**
* 检查禁言
* @param $userid
* @return void
*/
public function checkMute($userid)
{
$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);
}
/**
* 获取群组名称
* @return mixed|string|null
*/
public function getGroupName()
{
if (!isset($this->appendattrs['groupName'])) {
$name = $this->name;
if ($this->type == "group") {
switch ($this->group_type) {
case 'project':
$name = \DB::table('projects')->where('dialog_id', $this->id)->value('name');
break;
case 'task':
$name = \DB::table('project_tasks')->where('dialog_id', $this->id)->value('name');
break;
case 'all':
$name = Doo::translate('全体成员');
break;
}
}
$this->appendattrs['groupName'] = $name;
}
return $this->appendattrs['groupName'];
}
/**
* 推送消息
* @param $action
* @param array $data 发送内容,默认为[id=>会话ID]
* @param array $userid 指定会员,默认为群组所有成员
* @return void
*/
public function pushMsg($action, $data = null, $userid = null)
{
if ($data === null) {
$data = ['id' => $this->id];
}
//
if ($userid === null) {
$userid = $this->dialogUser->pluck('userid')->toArray();
}
//
$params = [
'userid' => $userid,
'msg' => [
'type' => 'dialog',
'mode' => $action,
'data' => $data,
]
];
$task = new PushTask($params, false);
Task::deliver($task);
}
/**
* 获取对话(同时检验对话身份)
* @param $dialog_id
* @param bool|string $checkOwner 是否校验群组身份,'auto'时有群主为true无群主为false
* @return self
*/
public static function checkDialog($dialog_id)
public static function checkDialog($dialog_id, $checkOwner = false)
{
$dialog = WebSocketDialog::find($dialog_id);
if (empty($dialog)) {
@@ -76,7 +606,14 @@ class WebSocketDialog extends AbstractModel
}
//
$userid = User::userid();
if ($dialog->type === 'group' && $dialog->group_type === 'task') {
if ($checkOwner === 'auto') {
$checkOwner = $dialog->owner_id > 0;
}
if ($checkOwner === true && $dialog->owner_id != $userid) {
throw new ApiException('仅限群主操作');
}
//
if ($dialog->group_type === 'task') {
// 任务群对话校验是否在项目内
$project_id = intval(ProjectTask::whereDialogId($dialog->id)->value('project_id'));
if ($project_id > 0) {
@@ -85,49 +622,12 @@ class WebSocketDialog extends AbstractModel
}
}
}
if ($dialog->group_type == 'okr') {
return $dialog;
}
if (!WebSocketDialogUser::whereDialogId($dialog->id)->whereUserid($userid)->exists()) {
throw new ApiException('不在成员列表内');
}
return $dialog;
}
/**
* 格式化对话
* @param WebSocketDialog $dialog
* @param int $userid 会员ID
* @return self|null
*/
public static function formatData(WebSocketDialog $dialog, $userid)
{
if (empty($dialog)) {
return null;
}
// 最后消息
$last_msg = WebSocketDialogMsg::whereDialogId($dialog->id)->orderByDesc('id')->first();
$dialog->last_msg = $last_msg;
// 未读信息
$dialog->unread = WebSocketDialogMsgRead::whereDialogId($dialog->id)->whereUserid($userid)->whereReadAt(null)->count();
// 对话人数
$builder = WebSocketDialogUser::whereDialogId($dialog->id);
$dialog->people = $builder->count();
// 对方信息
$dialog->dialog_user = null;
$dialog->group_info = null;
switch ($dialog->type) {
case "user":
$dialog_user = $builder->where('userid', '!=', $userid)->first();
$dialog->name = User::userid2nickname($dialog_user->userid);
$dialog->dialog_user = $dialog_user;
break;
case "group":
if ($dialog->group_type === 'project') {
$dialog->group_info = Project::withTrashed()->select(['id', 'name'])->whereDialogId($dialog->id)->first();
$dialog->name = $dialog->group_info ? $dialog->group_info->name : '';
} elseif ($dialog->group_type === 'task') {
$dialog->group_info = ProjectTask::withTrashed()->select(['id', 'name'])->whereDialogId($dialog->id)->first();
$dialog->name = $dialog->group_info ? $dialog->group_info->name : '';
}
break;
WebSocketDialogMsgRead::forceRead($dialog_id, $userid);
throw new ApiException('不在成员列表内', ['dialog_id' => $dialog_id], -4003);
}
return $dialog;
}
@@ -135,17 +635,19 @@ class WebSocketDialog extends AbstractModel
/**
* 创建聊天室
* @param string $name 聊天室名称
* @param int|array $userid 加入的会员ID或会员ID组
* @param int|array $userid 加入的会员ID(组)
* @param string $group_type 聊天室类型
* @param int $owner_id 群主会员ID
* @return self|null
*/
public static function createGroup($name, $userid, $group_type = '')
public static function createGroup($name, $userid, $group_type = '', $owner_id = 0)
{
return AbstractModel::transaction(function () use ($userid, $group_type, $name) {
return AbstractModel::transaction(function () use ($owner_id, $userid, $group_type, $name) {
$dialog = self::createInstance([
'type' => 'group',
'name' => $name ?: '',
'group_type' => $group_type,
'owner_id' => $owner_id,
]);
$dialog->save();
foreach (is_array($userid) ? $userid : [$userid] as $value) {
@@ -153,6 +655,8 @@ class WebSocketDialog extends AbstractModel
WebSocketDialogUser::createInstance([
'dialog_id' => $dialog->id,
'userid' => $value,
'important' => !in_array($group_type, ['user', 'all']),
'last_at' => in_array($group_type, ['user', 'department', 'all']) ? Carbon::now() : null,
])->save();
}
}
@@ -160,80 +664,124 @@ class WebSocketDialog extends AbstractModel
});
}
/**
* 加入聊天室
* @param int $dialog_id 会话ID即 聊天室ID
* @param int|array $userid 加入的会员ID或会员ID组
* @return bool
*/
public static function joinGroup($dialog_id, $userid)
{
$dialog = self::whereId($dialog_id)->whereType('group')->first();
if (empty($dialog)) {
return false;
}
AbstractModel::transaction(function () use ($dialog, $userid) {
foreach (is_array($userid) ? $userid : [$userid] as $value) {
if ($value > 0) {
WebSocketDialogUser::createInstance([
'dialog_id' => $dialog->id,
'userid' => $value,
])->save();
}
}
});
return true;
}
/**
* 退出聊天室
* @param int $dialog_id 会话ID即 聊天室ID
* @param int|array $userid 加入的会员ID或会员ID组
* @return bool
*/
public static function exitGroup($dialog_id, $userid)
{
if (is_array($userid)) {
WebSocketDialogUser::whereDialogId($dialog_id)->whereIn('userid', $userid)->delete();
} else {
WebSocketDialogUser::whereDialogId($dialog_id)->whereUserid($userid)->delete();
}
return true;
}
/**
* 获取会员对话(没有自动创建)
* @param int $userid 会员ID
* @param int $userid2 另一个会员ID
* @param User $user 发起会话的会员
* @param int $receiver 另一个会员ID
* @return self|null
*/
public static function checkUserDialog($userid, $userid2)
public static function checkUserDialog($user, $receiver)
{
if ($user->userid == $receiver) {
$receiver = 0;
}
$dialogUser = self::select(['web_socket_dialogs.*'])
->join('web_socket_dialog_users as u1', 'web_socket_dialogs.id', '=', 'u1.dialog_id')
->join('web_socket_dialog_users as u2', 'web_socket_dialogs.id', '=', 'u2.dialog_id')
->where('u1.userid', $userid)
->where('u2.userid', $userid2)
->where('u1.userid', $user->userid)
->where('u2.userid', $receiver)
->where('web_socket_dialogs.type', 'user')
->first();
if ($dialogUser) {
return $dialogUser;
}
return AbstractModel::transaction(function () use ($userid2, $userid) {
if ($receiver > 0 && $user->isTemp() && !User::whereUserid($receiver)->whereBot(1)->exists() ) {
throw new ApiException('无法发起会话,请联系管理员。');
}
return AbstractModel::transaction(function () use ($receiver, $user) {
$dialog = self::createInstance([
'type' => 'user',
]);
$dialog->save();
WebSocketDialogUser::createInstance([
'dialog_id' => $dialog->id,
'userid' => $userid,
'userid' => $user->userid,
])->save();
WebSocketDialogUser::createInstance([
'dialog_id' => $dialog->id,
'userid' => $userid2,
'userid' => $receiver,
])->save();
return $dialog;
});
}
/**
* 发送消息文件
*
* @param User $user 发起会话的会员
* @param array $dialogIds 对话id
* @param file $files 文件对象
* @param string $image64 base64文件
* @param string $fileName 文件名称
* @param int $replyId 恢复id
* @param int $imageAttachment
* @return array
*/
public static function sendMsgFiles($user, $dialogIds, $files, $image64, $fileName, $replyId, $imageAttachment)
{
$filePath = '';
$result = [];
foreach ($dialogIds as $dialog_id) {
$dialog = WebSocketDialog::checkDialog($dialog_id);
//
$action = $replyId > 0 ? "reply-$replyId" : "";
$path = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
if ($image64) {
$data = Base::image64save([
"image64" => $image64,
"path" => $path,
"fileName" => $fileName,
]);
} else if ($filePath) {
Base::makeDir(public_path($path));
copy($filePath, public_path($path) . basename($filePath));
} else {
$setting = Base::setting('system');
$data = Base::upload([
"file" => $files,
"type" => 'more',
"path" => $path,
"fileName" => $fileName,
"size" => ($setting['file_upload_limit'] ?: 0) * 1024
]);
}
//
if (Base::isError($data)) {
throw new ApiException($data['msg']);
} else {
$fileData = $data['data'];
$filePath = $fileData['file'];
$fileName = $fileData['name'];
$fileData['thumb'] = Base::unFillUrl($fileData['thumb']);
$fileData['size'] *= 1024;
//
if ($dialog->type === 'group' && $dialog->group_type === 'task') { // 任务群组保存文件
if ($imageAttachment || !in_array($fileData['ext'], File::imageExt)) { // 如果是图片不保存
$task = ProjectTask::whereDialogId($dialog->id)->first();
if ($task) {
$file = ProjectTaskFile::createInstance([
'project_id' => $task->project_id,
'task_id' => $task->id,
'name' => $fileData['name'],
'size' => $fileData['size'],
'ext' => $fileData['ext'],
'path' => $fileData['path'],
'thumb' => $fileData['thumb'],
'userid' => $user->userid,
]);
$file->save();
}
}
}
//
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'file', $fileData, $user->userid);
if (Base::isSuccess($result)) {
if (isset($task)) {
$result['data']['task_id'] = $task->id;
}
}
}
}
return $result;
}
}

View File

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

View File

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

View File

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

View File

@@ -2,26 +2,65 @@
namespace App\Models;
use Carbon\Carbon;
/**
* Class WebSocketDialogUser
* App\Models\WebSocketDialogUser
*
* @package App\Models
* @property int $id
* @property int|null $dialog_id 对话ID
* @property int|null $userid 会员ID
* @property string|null $top_at 置顶时间
* @property 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 string|null $color 颜色
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\WebSocketDialog|null $webSocketDialog
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser query()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser 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)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereUserid($value)
* @mixin \Eloquent
*/
class WebSocketDialogUser extends AbstractModel
{
protected $dateFormat = 'Y-m-d H:i:s.v';
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function webSocketDialog(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(WebSocketDialog::class, 'id', 'dialog_id');
}
/**
* 更新对话最后消息时间
* @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

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

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

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

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

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

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

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

View File

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

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

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

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

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

View File

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

235
app/Module/Image.php Normal file
View File

@@ -0,0 +1,235 @@
<?php
namespace App\Module;
use Imagick;
/**
* 图片类:缩略图、调整大小、压缩
*/
class Image
{
private Imagick $image;
private string $path;
private int $height;
private int $width;
/**
* @param $imagePath
* @throws \ImagickException
*/
public function __construct($imagePath) {
$this->path = $imagePath;
$this->image = new Imagick($this->path);
$geo = $this->image->getImageGeometry();
$this->height = $geo['height'];
$this->width = $geo['width'];
}
/**
* 获取图片宽度
* @return int
*/
public function getWidth(): int
{
return $this->width;
}
/**
* 获取图片高度
* @return int
*/
public function getHeight(): int
{
return $this->height;
}
/**
* 创建缩略图
* @param int $width
* @param int $height
* @param string $mode = 'percentage|cover|contain' ($width 和 $height 都设置的情况下有效)
* @return Image
* @throws \ImagickException
*/
public function thumb(int $width, int $height, string $mode = 'percentage'): static
{
if ($height === 0 && $width === 0) {
return $this;
}
if ($width && $height) {
if ($mode === 'cover') {
// 铺满背景(超出被裁掉)
$this->image->cropThumbnailImage($width, $height);
} else {
// 等比缩放
if ($this->width >= $this->height) {
$this->image->thumbnailImage($width, 0);
} else {
$this->image->thumbnailImage(0, $height);
}
if ($mode === 'contain') {
// 显示完整的图
$background = new Imagick();
$background->newImage($width, $height, 'none', strtolower(pathinfo($this->path, PATHINFO_EXTENSION)));
$bg_width = $background->getImageWidth();
$bg_height = $background->getImageHeight();
$img_width = $this->image->getImageWidth();
$img_height = $this->image->getImageHeight();
if ($img_width / $bg_width > $img_height / $bg_height) {
$width = $bg_width;
$height = intval($img_height / ($img_width / $width));
} else {
$height = $bg_height;
$width = intval($img_width / ($img_height / $height));
}
$this->image->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1);
$x = intval(($bg_width - $width) / 2);
$y = intval(($bg_height - $height) / 2);
$background->compositeImage($this->image, Imagick::COMPOSITE_DEFAULT, $x, $y);
$this->image->destroy();
$this->image = $background;
}
}
} else {
$this->image->thumbnailImage($width, $height);
}
return $this;
}
/**
* 调整图像大小
* @param int $width
* @param int $height
* @return Image
* @throws \ImagickException
*/
public function resize(int $width, int $height): static
{
if ($height === 0 && $width === 0) {
return $this;
}
$this->image->adaptiveResizeImage($width, $height);
return $this;
}
/**
* 压缩图片
* @param int $quality
* @return $this
* @throws \ImagickException
*/
public function compress(int $quality = 100): static
{
$format = strtolower($this->image->getImageFormat());
switch ($format) {
case 'jpeg':
case 'jpg':
$this->image->setImageCompression(Imagick::COMPRESSION_JPEG);
break;
case 'png':
$this->image->setImageCompression(Imagick::COMPRESSION_ZIP);
break;
case 'gif':
$this->image->setImageCompression(Imagick::COMPRESSION_LZW);
break;
default:
return $this;
}
if ($quality > 1) {
$this->image->setImageCompressionQuality($quality);
} elseif ($quality > 0) {
$this->image->setImageCompressionQuality($quality * 100);
} else {
$this->image->setImageCompressionQuality(100);
}
return $this;
}
/**
* 保存结果到文件
* @param string $savePath Save path
* @return void
* @throws \ImagickException
*/
public function saveTo(string $savePath): void
{
$this->image->writeImage($savePath);
$this->image->destroy();
}
/** ******************************************************************************/
/** ******************************************************************************/
/** ******************************************************************************/
/**
* 生成缩略图
* @param string $imagePath 图片路径
* @param string $savePath 保存路径
* @param int $width 宽度
* @param int $height 高度
* @param string $mode 模式percentage|cover|contain
* @return string|null 成功返回图片后缀,失败返回 false
*/
public static function thumbImage(string $imagePath, string $savePath, int $width, int $height, string $mode = 'percentage'): ?string
{
if (!file_exists($imagePath)) {
return null;
}
try {
$extension = strtolower(pathinfo($imagePath, PATHINFO_EXTENSION));
if (str_contains($savePath, '.{*}')) {
$savePath = str_replace('.{*}', '.' . $extension, $savePath);
}
$image = new Image($imagePath);
$image->thumb($width, $height, $mode);
$image->saveTo($savePath);
return $extension;
} catch (\ImagickException) {
return null;
}
}
/**
* 压缩图片
* @param string $imagePath 图片路径
* @param string|null $savePath 保存路径(默认覆盖原图)
* @param int $quality 压缩质量0-100
* @param float $minSize 最小尺寸单位KB
* @return bool
*/
public static function compressImage(string $imagePath, string $savePath = null, int $quality = 100, float $minSize = 10): bool
{
if (Base::settingFind("system", "image_compress") === 'close') {
return false;
}
if (!file_exists($imagePath)) {
return false;
}
$imageSize = filesize($imagePath);
if ($minSize > 0 && $imageSize < $minSize * 1024) {
return false;
}
if (empty($savePath)) {
$savePath = $imagePath;
}
$tmpPath = $imagePath . '.compress.tmp';
try {
$image = new Image($imagePath);
$image->compress($quality);
$image->saveTo($tmpPath);
if (filesize($tmpPath) >= $imageSize) {
copy($imagePath, $savePath);
unlink($tmpPath);
} else {
rename($tmpPath, $savePath);
}
return true;
} catch (\ImagickException) {
return false;
}
}
}

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

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

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

@@ -0,0 +1,57 @@
<?php
namespace App\Module;
use Carbon\Carbon;
class TimeRange
{
public ?Carbon $updated;
public ?Carbon $deleted;
/**
* @param $data
*/
public function __construct($data)
{
if (is_array($data)) {
$range = $this->format($data['timerange']);
if ($data['updated_at'] || $data['at_after']) {
$range[0] = $data['updated_at'] ?: $data['at_after'];
}
if ($data['deleted_at']) {
$range[1] = $data['deleted_at'];
}
} else {
$range = $this->format($data);
}
//
$updated = Base::isNumber($range[0]) ? intval($range[0]) : trim($range[0]);
$deleted = Base::isNumber($range[1]) ? intval($range[1]) : trim($range[1]);
//
$timezone = config('app.timezone');
$this->updated = $updated ? Carbon::parse($updated)->setTimezone($timezone) : null;
$this->deleted = $deleted ? Carbon::parse($deleted)->setTimezone($timezone) : null;
}
/**
* @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));
}
/**
* @param $data
* @return TimeRange
*/
public static function parse($data): TimeRange
{
return new self($data);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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