Compare commits

..

152 Commits

Author SHA1 Message Date
kuaifan
905662e963 build 2023-05-31 15:26:01 +08:00
kuaifan
ac2dfcfce3 Merge pull request #159 from hitosea/pro-doo
打开审批设置页面路由修改
2023-05-31 15:22:31 +08:00
gwokwong
fd6fef4292 Merge branch 'pro' into pro-doo
* pro:
  fix: 打开审批设置页面 -  路由修改
2023-05-31 15:04:43 +08:00
weifashi
eee7b8f0f1 fix: 打开审批设置页面 - 路由修改 2023-05-31 14:34:50 +08:00
kuaifan
ef88924f36 build 2023-05-31 09:15:38 +08:00
kuaifan
c9378b0eab Merge pull request #157 from hitosea/pro-doo
Pro doo
2023-05-30 19:59:14 +08:00
gwokwong
cde96cb8b4 Merge branch 'pro' into pro-doo
* pro:
  fix: 更改审批流数据表名
2023-05-30 19:12:15 +08:00
weifashi
ffbd9b1f26 fix: 更改审批流数据表名 2023-05-30 18:38:39 +08:00
gwokwong
59524c78e6 调整统一命名 2023-05-30 18:04:33 +08:00
weifashi
1ad0c3a3ec fix: 调整统一命名 2023-05-30 17:58:43 +08:00
gwokwong
e85d4c24d3 build编译文件上传 2023-05-26 14:31:44 +08:00
gwokwong
18280b4ff9 工作流合并 2023-05-26 14:19:36 +08:00
weifashi
e504fb845f build 2023-05-24 11:08:27 +08:00
weifashi
003186edd9 fix: 添加评论功能 - 100% 2023-05-24 11:04:39 +08:00
ganzizi
61e958d757 perf 优化全局评论 2023-05-24 11:01:10 +08:00
ganzizi
2ee349605e feat 全局评论追加 2023-05-24 10:15:08 +08:00
ganzizi
7d200d9a73 feat 全局评论追加 2023-05-24 10:10:56 +08:00
ganzizi
0468178c43 feat 新增全局评论init 2023-05-23 19:12:54 +08:00
ganzizi
6f4a94c8d1 feat 新增全局评论init 2023-05-23 19:07:37 +08:00
weifashi
e83a65ec40 fix:工作流 - 添加上传图片功能 100% 2023-05-22 18:36:25 +08:00
weifashi
53ea6b00ab build - 去除加密 - 测试那边用 2023-05-17 14:54:46 +08:00
weifashi
45865c0c18 build 2023-05-11 17:51:27 +08:00
weifashi
979d8057a4 fix:添加流程 标题改名 2023-05-11 17:48:57 +08:00
weifashi
b81f75b794 build 2023-05-11 17:46:56 +08:00
weifashi
8f7578ab06 fix:假期类型去掉外出 2023-05-11 17:44:59 +08:00
weifashi
dd39a8b63f build 2023-05-11 17:27:59 +08:00
weifashi
7595309dc8 fix:翻译 2023-05-11 17:26:09 +08:00
weifashi
93abe72041 fix: 调整样式 2023-05-11 17:21:33 +08:00
kuaifan
60c513ea3b build 2023-05-10 19:53:58 +08:00
kuaifan
385abae741 fix: iOS16.4之前版本无法进入的问题 2023-05-10 19:48:56 +08:00
kuaifan
a680538f80 build 2023-05-08 23:19:13 +08:00
kuaifan
11571c2045 perf: Windows 10 以下不支持加密 2023-05-08 23:09:59 +08:00
kuaifan
f1d267e2b2 fix: 移动端无法长按录音的问题 2023-05-08 22:57:25 +08:00
kuaifan
003b52fbc9 perf: 优化图形验证码方式 2023-05-08 22:42:11 +08:00
weifs
6e2df6fcfc build:编译资源上传 2023-04-28 10:23:19 +08:00
weifs
cd151667c1 feat:1. 新增部门负责人标识并排第一位 - 深优化
2.  clearHelper.php 文件添加判断是cmd命令运行时才执行
2023-04-28 10:14:10 +08:00
ganzizi
1a3ecbb4e9 perf 新增部门负责人标识并排第一位 2023-04-28 09:28:44 +08:00
ganzizi
0b1f433fe4 feat 新增部门负责人标识并排第一位 2023-04-27 19:19:01 +08:00
kuaifan
7aa44b8623 perf: 兼容iPad键盘 2023-04-26 20:58:56 +08:00
weifs
6e68100f86 build:编译资源上传 2023-04-25 17:35:39 +08:00
weifs
c869e88c40 fix:兼容safari 时间处理 2023-04-25 14:18:55 +08:00
kuaifan
9771e1e741 perf: 优化移动端兼容 2023-04-25 12:03:59 +08:00
weifs
9034a70b8f fix:修复了bug - 成员已加入部门,无法发起请假申请 2023-04-25 10:08:10 +08:00
weifs
e58ebebb80 feat:解决bug - 发起流程提示用户所在部门departmentId不能为空 2023-04-25 09:44:57 +08:00
weifs
edab43504e build:编译资源上传 2023-04-24 17:11:11 +08:00
kuaifan
900b11280c perf: 优化iPad Pro页面布局 2023-04-24 14:48:35 +08:00
kuaifan
0f449f151a 网页端token过期时间调整成30天 2023-04-24 14:18:13 +08:00
kuaifan
8186649a9f fix: 终端用户数提示错误 2023-04-24 13:59:52 +08:00
weifs
364ef34248 feat:更改镜像 2023-04-24 10:51:25 +08:00
ganzizi
c66714d1f5 perf 优化导出 2023-04-21 11:58:24 +08:00
ganzizi
d42fcca81f perf 优化导出 2023-04-21 11:58:24 +08:00
weifs
16d1591675 feat:工作流-翻译 2023-04-21 11:09:22 +08:00
ganzizi
d7b2f93797 perf 优化抄送信息按钮显示 2023-04-21 10:00:34 +08:00
ganzizi
9203046d97 perf 优化发给抄送人信息 2023-04-21 09:25:20 +08:00
ganzizi
b860a749f3 perf 优化发给抄送人信息 2023-04-20 18:32:49 +08:00
ganzizi
6c689fe55f perf 优化发给抄送人信息 2023-04-20 18:31:37 +08:00
weifs
c18fd1ef20 feat:工作流 - 小优化 2023-04-20 18:25:34 +08:00
ganzizi
7e56389c34 perf 优化工作流容器时间 2023-04-20 17:50:23 +08:00
weifs
28771c38e7 feat:更新容器 2023-04-20 16:38:00 +08:00
weifs
de7e74ff19 feat:工作流优化 2023-04-20 16:10:39 +08:00
ganzizi
2cf661b54f feat 导出前端页 2023-04-20 14:55:20 +08:00
weifs
16ea92c970 feat: 工作流 - 前端bug修复,节点不显示提及名称 2023-04-20 14:37:06 +08:00
weifs
3ad55cf6d5 Merge branch 'pro' of github.com:hitosea/dootask into pro
# Conflicts:
#	app/Http/Controllers/Api/WorkflowController.php
2023-04-20 14:27:13 +08:00
weifs
d0951041eb feat:工作流 - 前端100% 2023-04-20 14:24:58 +08:00
ganzizi
150d9f18b4 feat 新增工作流导出 2023-04-20 11:51:39 +08:00
weifs
09f48c32c7 feat:工作流前端进度 - 95% 2023-04-19 17:19:28 +08:00
weifs
596594d8b3 feat:工作流 - 前端进度 90% 2023-04-18 18:57:22 +08:00
ganzizi
59d17aa950 feat 新增查询条及已发起所有记录 2023-04-18 16:10:21 +08:00
weifs
836d4f8209 feat:工作流 - 前端进度 70% 2023-04-18 15:54:36 +08:00
ganzizi
c13aa8d7a4 feat 新增查询条及已发起所有记录 2023-04-18 15:00:21 +08:00
kuaifan
5fb07b7aa7 build 2023-04-18 12:02:39 +08:00
kuaifan
ee12cc21c1 no message 2023-04-18 11:51:02 +08:00
weifs
6abfc7a2e2 feat:工作流 - 前端进度 60% 2023-04-18 10:45:49 +08:00
weifs
18758421fd feat:工作流 - 前端进度 50% 2023-04-17 18:46:57 +08:00
ganzizi
644a986747 feat 新增用户头像 2023-04-17 17:43:54 +08:00
ganzizi
ad164f3539 feat 新增用户头像 2023-04-17 15:20:50 +08:00
ganzizi
d5a7ae5fa3 fix 修复了bug 2023-04-17 14:24:55 +08:00
ganzizi
a845b7b892 fix 修复了bug 2023-04-17 14:16:28 +08:00
ganzizi
a29414a477 fix 修复了bug 2023-04-17 13:48:29 +08:00
kuaifan
1c3ce47dcc build 2023-04-17 12:42:58 +08:00
kuaifan
8f30693473 no message 2023-04-17 12:37:22 +08:00
kuaifan
0f5f7c515a perf: 优化移动端部门管理 2023-04-17 12:36:59 +08:00
kuaifan
8e76493cc6 perf: 移动端显示工作报告 2023-04-17 12:26:00 +08:00
ganzizi
8617c72aab fix 修复了bug 2023-04-17 10:18:28 +08:00
weifs
7f0a150b74 feat:更新前端路由 2023-04-17 10:03:02 +08:00
HEXIANG
a53aaff620 fix:审批流程静态页 2023-04-17 09:27:49 +08:00
ganzizi
cab81c5c04 perf 工作流接口优化 2023-04-13 14:35:54 +08:00
ganzizi
5923b6169a perf 工作流接口优化 2023-04-13 09:43:02 +08:00
weifs
0f5eb829c6 feat:工作流 - 前端设置页面 - 30% 2023-04-13 09:38:50 +08:00
ganzizi
d87c06de9a feat 初步融合工作流 2023-04-12 18:25:11 +08:00
ganzizi
76e8870738 feat 初步融合工作流 2023-04-12 18:22:25 +08:00
kuaifan
247b9ee0c1 下班静默推送 2023-04-12 18:17:11 +08:00
kuaifan
80dd76b078 ws仅加密对话消息 2023-04-12 17:51:45 +08:00
ganzizi
9163cee90e feat 初步融合工作流 2023-04-12 16:58:27 +08:00
weifs
fe46d63031 feat:初步融合工作流 2023-04-12 16:55:00 +08:00
HEXIANG
d14d5a5942 fix:审批流程静态页 2023-04-12 16:41:11 +08:00
ganzizi
7181652f01 feat 初步融合工作流 2023-04-12 16:25:26 +08:00
kuaifan
4024cfb7c7 no message 2023-04-10 12:36:41 +08:00
kuaifan
1cf5a24c26 perf: 导出数据支持搜索离职会员 2023-04-10 10:48:45 +08:00
kuaifan
06795ca32c build 2023-04-07 17:37:32 +08:00
kuaifan
c4fd5ee60a perf: 优化md消息图片预览 2023-04-07 17:02:29 +08:00
kuaifan
e30b66e5c1 no message 2023-04-07 15:22:35 +08:00
kuaifan
9ee8d9b828 build 2023-04-06 13:03:25 +08:00
kuaifan
6107bde666 license 提醒 2023-04-06 12:53:33 +08:00
kuaifan
e69a91feb3 fix: 修改个人头像缓存不更新的情况 2023-04-06 11:15:42 +08:00
kuaifan
174df30978 更新自启动说明 2023-04-06 10:56:49 +08:00
kuaifan
8ffac1376a perf: 对webp文件的支持 2023-04-06 10:34:23 +08:00
kuaifan
433a46bba9 fix: 无法查看已归档任务 2023-04-06 09:31:08 +08:00
kuaifan
ba3d5299a0 build 2023-04-04 21:18:50 +08:00
kuaifan
0aa6911159 perf: 右键或长按消息发送按钮可选无声发送、Markdown格式发送 2023-04-04 21:14:58 +08:00
kuaifan
1cef313689 perf: 系统设置新增图片优化、是否保存网络图片功能 2023-04-04 11:46:44 +08:00
kuaifan
4a198ccd8f no message 2023-04-03 23:49:52 +08:00
kuaifan
11e08fea73 build 2023-04-03 22:54:19 +08:00
kuaifan
a5be461021 perf: 消息api支持markdown 2023-04-03 22:47:23 +08:00
kuaifan
d2e04843a4 no message 2023-04-03 12:57:32 +08:00
kuaifan
d0276e63c6 build 2023-04-03 11:50:09 +08:00
kuaifan
2d4fa5735b no message 2023-04-03 11:32:02 +08:00
kuaifan
1c367eb687 perf: 优化图片消息 2023-04-03 11:01:38 +08:00
kuaifan
56cf534340 build 2023-04-02 23:15:02 +08:00
kuaifan
01fbc8b39d no message 2023-04-02 23:01:51 +08:00
kuaifan
91f3ea7c46 fix: 会议回音 2023-04-02 22:43:18 +08:00
kuaifan
d11a89d02c perf: 优化动态加载静态资源 2023-04-02 22:41:49 +08:00
kuaifan
b580a9f7ad no message 2023-04-02 11:18:09 +08:00
kuaifan
c2db186620 perf: 消息接口支持MARKDOWN 2023-04-02 10:53:21 +08:00
kuaifan
679a0002a7 no message 2023-04-01 23:45:21 +08:00
kuaifan
81fdcce40f no message 2023-04-01 23:09:13 +08:00
kuaifan
8af540f9b2 build 2023-04-01 21:33:14 +08:00
kuaifan
79898d87c0 fix: 文件搜索不到根目录的共享 2023-04-01 16:54:13 +08:00
kuaifan
3c400af559 perf: 优化会话搜索 2023-04-01 16:16:24 +08:00
kuaifan
c8375b846a build 2023-04-01 15:29:37 +08:00
kuaifan
765f74b619 no message 2023-04-01 15:25:11 +08:00
kuaifan
2fcb7fb59b perf: 通过页面修改机器人资料 2023-04-01 15:23:07 +08:00
kuaifan
d828ed83a5 no message 2023-04-01 13:03:16 +08:00
kuaifan
22993283f0 fix: 清除缓存导致获取不到数据的问题 2023-04-01 12:39:03 +08:00
kuaifan
69f0d3b2bc build 2023-03-31 20:31:02 +08:00
kuaifan
ab0a5898cb perf: WebSocket 数据传输加密 2023-03-31 20:24:23 +08:00
kuaifan
60a41cae41 Merge branch 'e2ee' into pro 2023-03-30 16:08:15 +08:00
kuaifan
6c7f7f9543 build 2023-03-30 15:56:17 +08:00
kuaifan
d571d6e121 no message 2023-03-30 15:45:44 +08:00
kuaifan
b7b933c89d fix: 无法查看已归档任务 2023-03-30 15:45:44 +08:00
kuaifan
7e98a78333 feat: 实现非对称加密关键接口 2023-03-30 15:44:51 +08:00
kuaifan
342b9d6fc6 upgrade php container 2023-03-28 00:11:48 +08:00
kuaifan
ef7ed58a22 no message 2023-03-25 23:41:48 +08:00
kuaifan
160c736b38 perf: 自动清空文件回收站 2023-03-25 23:25:50 +08:00
kuaifan
01bb72523e build 2023-03-25 20:39:52 +08:00
kuaifan
ec62d7be5a perf: 优化任务接口数据逻辑 2023-03-25 20:23:25 +08:00
kuaifan
f8cbd31f61 no message 2023-03-25 12:25:22 +08:00
kuaifan
8c04a432a8 no message 2023-03-24 11:04:02 +08:00
kuaifan
3a9001e091 Upgrade Professional Edition 2023-03-24 09:08:53 +08:00
kuaifan
a172909ddf build 2022-06-20 18:41:00 +08:00
kuaifan
54d695f851 perf: 指定mariadb:10.7.3解决部分出现初始化数据库失败的情况 2022-06-20 18:40:09 +08:00
kuaifan
0147e7f1e1 perf: 上传限制改为1G 2022-06-20 18:39:29 +08:00
754 changed files with 58589 additions and 29489 deletions

View File

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

View File

@@ -1,85 +0,0 @@
name: Build Generic
on:
push:
tags:
- 'v*'
jobs:
build-mac:
strategy:
fail-fast: false
matrix:
id: [ "com.dootask.task", "com.hitosea.task" ]
runs-on: macos-latest
environment: build
if: startsWith(github.event.ref, 'refs/tags/v')
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Create changelog text
id: changelog
uses: loopwerk/tag-changelog@v1
with:
token: ${{ secrets.GH_PAT }}
exclude_types: other,chore,build
- name: Use Node.js 16.x
uses: actions/setup-node@v1
with:
node-version: 16.x
- name: Build for MacOS
env:
APPID: ${{ matrix.id }}
PROVIDER: "generic"
APPLEID: ${{ secrets.APPLEID }}
APPLEIDPASS: ${{ secrets.APPLEIDPASS }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
CSC_LINK: ${{ secrets.CSC_LINK }}
RELEASE_BODY: ${{ steps.changelog.outputs.changes }}
run: ./cmd electron build-mac
build-win:
strategy:
fail-fast: false
matrix:
id: [ "com.dootask.task", "com.hitosea.task" ]
runs-on: windows-latest
environment: build
if: startsWith(github.event.ref, 'refs/tags/v')
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Create changelog text
id: changelog
uses: loopwerk/tag-changelog@v1
with:
token: ${{ secrets.GH_PAT }}
exclude_types: other,chore,build
- name: Use Node.js 16.x
uses: actions/setup-node@v1
with:
node-version: 16.x
- name: Build for Windows
shell: powershell
env:
APPID: ${{ matrix.id }}
PROVIDER: "generic"
RELEASE_BODY: ${{ steps.changelog.outputs.changes }}
run: |
npm install
cd electron
npm install
cd ../
mkdir -p ./electron/public
cp ./electron/index.html ./electron/public/index.html
npx mix --production -- --env --electron
node ./electron/build.js build-win

View File

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

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

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

2
.gitignore vendored
View File

@@ -5,10 +5,10 @@
/public/.well-known
/public/.user.ini
/storage/*.key
/config/LICENSE
/vendor
/build
/tmp
/CHANGELOG.md
._*
.env
.idea

2
.gitmodules vendored
View File

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

13
.gitpod.yml Normal file
View File

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

2083
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -15,15 +15,15 @@ Group No.: `546574618`
- System: `Centos/Debian/Ubuntu/macOS`
- Hardware suggestion: 2 cores and above 4G memory
### Deployment project
### Deployment (Pro Edition)
```bash
# 1、Clone the repository
# Clone projects on github
git clone --depth=1 https://github.com/kuaifan/dootask.git
git clone -b pro --depth=1 https://github.com/kuaifan/dootask.git
# Or you can use gitee
git clone --depth=1 https://gitee.com/aipaw/dootask.git
git clone -b pro --depth=1 https://gitee.com/aipaw/dootask.git
# 2、Enter directory
cd dootask
@@ -116,7 +116,8 @@ git pull
./cmd mysql recovery
```
If 502 after the upgrade please run `./cmd restart` restart the service.
* Please try again if the upgrade fails across a large version.
* If 502 after the upgrade please run `./cmd restart` restart the service.
## Transfer

View File

@@ -15,15 +15,15 @@
- 支持环境:`Centos/Debian/Ubuntu/macOS`
- 硬件建议2核4G以上
### 部署项目
### 部署项目Pro版
```bash
# 1、克隆项目到您的本地或服务器
# 通过github克隆项目
git clone --depth=1 https://github.com/kuaifan/dootask.git
git clone -b pro --depth=1 https://github.com/kuaifan/dootask.git
# 或者你也可以使用gitee
git clone --depth=1 https://gitee.com/aipaw/dootask.git
git clone -b pro --depth=1 https://gitee.com/aipaw/dootask.git
# 2、进入目录
cd dootask
@@ -117,7 +117,8 @@ git pull
./cmd mysql recovery
```
如果升级后出现502请运行 `./cmd restart` 重启服务即可
* 跨越大版本升级失败时请重试执行一次
* 如果升级后出现502请运行 `./cmd restart` 重启服务即可。
## 迁移项目

View File

@@ -2,25 +2,23 @@
## 发布前
1. 添加环境变量 `APPLEID``APPLEIDPASS``CSC_LINK`
2. 发布GitHub还需要添加 `GH_PAT`
1. 添加环境变量 `APPLEID``APPLEIDPASS` 用于公证
2. 添加环境变量 `CSC_LINK``CSC_KEY_PASSWORD` 用于签名
3. 添加环境变量 `GH_TOKEN``GH_REPOSITORY` 用于发布到GitHub
4. 添加环境变量 `DP_KEY` 用于发布到私有服务器
## 通过 GitHub Actions 发布
1. 执行 `./cmd prod` 编译
2. 执行 `node ./version.js` 制作版本
3. 执行 `git commit` 相关操作
4. 制作标签
5. 推送标签
1. 执行 `npm run version` 生成版本
2. 执行 `npm run build` 编译前端
3. 执行 `git commit` 提交并推送
4. 添加并推送标签
## 本地发布
1. 执行 `./cmd prod` 编译
2. 执行 `node ./version.js` 制作版本
3. 执行 `git commit` 相关操作
4. 制作标签
5. 执行 `./cmd electron` 相关操作
1. 执行 `npm run version` 生成版本
2. 执行 `npm run build` 编译前端
3. 执行 `./cmd electron` 相关操作
## 编译App

View File

@@ -4,7 +4,7 @@
/**
* A helper file for Laravel, to provide autocomplete information to your IDE
* Generated for Laravel 8.83.16.
* Generated for Laravel 8.83.27.
*
* This file should not be included in your code, only analyzed by your IDE!
*
@@ -2167,6 +2167,17 @@
{
/** @var \Illuminate\Auth\SessionGuard $instance */
return $instance->setRequest($request);
}
/**
* Get the timebox instance used by the guard.
*
* @return \Illuminate\Support\Timebox
* @static
*/
public static function getTimebox()
{
/** @var \Illuminate\Auth\SessionGuard $instance */
return $instance->getTimebox();
}
/**
* Determine if the current user is authenticated. If not, throw an exception.
@@ -9878,12 +9889,12 @@
* Clones a request and overrides some of its parameters.
*
* @return static
* @param array $query The GET parameters
* @param array $request The POST parameters
* @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...)
* @param array $cookies The COOKIE parameters
* @param array $files The FILES parameters
* @param array $server The SERVER parameters
* @param array|null $query The GET parameters
* @param array|null $request The POST parameters
* @param array|null $attributes The request attributes (parameters parsed from the PATH_INFO, ...)
* @param array|null $cookies The COOKIE parameters
* @param array|null $files The FILES parameters
* @param array|null $server The SERVER parameters
* @return static
* @static
*/
@@ -16109,6 +16120,16 @@
{
/** @var \Facade\FlareClient\Flare $instance */
return $instance->filterExceptionsUsing($filterExceptionsCallable);
}
/**
*
*
* @static
*/
public static function filterReportsUsing($filterReportsCallable)
{
/** @var \Facade\FlareClient\Flare $instance */
return $instance->filterReportsUsing($filterReportsCallable);
}
/**
*

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

File diff suppressed because it is too large Load Diff

View File

@@ -3,9 +3,9 @@
namespace App\Http\Controllers\Api;
use App\Models\AbstractModel;
use App\Models\Deleted;
use App\Models\File;
use App\Models\FileContent;
use App\Models\FileLink;
use App\Models\ProjectTask;
use App\Models\ProjectTaskFile;
use App\Models\User;
@@ -15,11 +15,11 @@ use App\Models\WebSocketDialogMsgRead;
use App\Models\WebSocketDialogMsgTodo;
use App\Models\WebSocketDialogUser;
use App\Module\Base;
use App\Module\TimeRange;
use Carbon\Carbon;
use DB;
use Redirect;
use Request;
use Response;
/**
* @apiDefine dialog
@@ -36,7 +36,10 @@ class DialogController extends AbstractController
* @apiGroup dialog
* @apiName lists
*
* @apiParam {String} [at_after] 只读取在这个时间之后更新的对话
* @apiParam {String} [timerange] 时间范围1678248944,1678248944
* - 第一个时间: 读取在这个时间之后更新的数据
* - 第二个时间: 读取在这个时间之后删除的数据ID第1页附加返回数据: deleted_id
*
* @apiParam {Number} [page] 当前页,默认:1
* @apiParam {Number} [pagesize] 每页显示数量,默认:50最大:100
*
@@ -48,11 +51,13 @@ class DialogController extends AbstractController
{
$user = User::auth();
//
$builder = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread', 'u.silence'])
$timerange = TimeRange::parse(Request::input());
//
$builder = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread', 'u.silence', 'u.updated_at as user_at'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->where('u.userid', $user->userid);
if (Request::exists('at_after')) {
$builder->where('web_socket_dialogs.last_at', '>', Carbon::parse(Request::input('at_after')));
if ($timerange->updated) {
$builder->where('u.updated_at', '>', $timerange->updated);
}
$list = $builder
->orderByDesc('u.top_at')
@@ -62,7 +67,12 @@ class DialogController extends AbstractController
return $item->formatData($user->userid);
});
//
return Base::retSuccess('success', $list);
$data = $list->toArray();
if ($list->currentPage() === 1) {
$data['deleted_id'] = Deleted::ids('dialog', $user->userid, $timerange->deleted);
}
//
return Base::retSuccess('success', $data);
}
/**
@@ -88,7 +98,7 @@ class DialogController extends AbstractController
return Base::retError('请输入搜索关键词');
}
// 搜索会话
$dialogs = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread', 'u.silence'])
$dialogs = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread', 'u.silence', 'u.updated_at as user_at'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->where('web_socket_dialogs.name', 'LIKE', "%{$key}%")
->where('u.userid', $user->userid)
@@ -104,7 +114,11 @@ class DialogController extends AbstractController
if (count($list) < 20 && Base::judgeClientVersion("0.21.60")) {
$users = User::select(User::$basicField)
->where(function ($query) use ($key) {
$query->where("email", "like", "%{$key}%")->orWhere("nickname", "like", "%{$key}%");
if (str_contains($key, "@")) {
$query->where("email", "like", "%{$key}%");
} else {
$query->where("nickname", "like", "%{$key}%")->orWhere("pinyin", "like", "%{$key}%");
}
})->orderBy('userid')
->take(20 - count($list))
->get();
@@ -121,7 +135,7 @@ class DialogController extends AbstractController
}
// 搜索消息会话
if (count($list) < 20) {
$msgs = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread', 'u.silence', 'm.id as search_msg_id'])
$msgs = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread', 'u.silence', 'u.updated_at as user_at', 'm.id as search_msg_id'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->join('web_socket_dialog_msgs as m', 'web_socket_dialogs.id', '=', 'm.dialog_id')
->where('u.userid', $user->userid)
@@ -158,7 +172,7 @@ class DialogController extends AbstractController
//
$dialog_id = intval(Request::input('dialog_id'));
//
$item = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread', 'u.silence'])
$item = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread', 'u.silence', 'u.updated_at as user_at'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->where('web_socket_dialogs.id', $dialog_id)
->where('u.userid', $user->userid)
@@ -302,6 +316,9 @@ class DialogController extends AbstractController
if (empty($callUser) || empty($callUser->tel)) {
return Base::retError("对方未设置联系电话");
}
if ($user->isTemp()) {
return Base::retError("无法查看联系电话");
}
//
$add = null;
$res = WebSocketDialogMsg::sendMsg(null, $dialog->id, 'notice', [
@@ -340,7 +357,7 @@ class DialogController extends AbstractController
return Base::retError('错误的会话');
}
//
$dialog = WebSocketDialog::checkUserDialog($user->userid, $userid);
$dialog = WebSocketDialog::checkUserDialog($user, $userid);
if (empty($dialog)) {
return Base::retError('打开会话失败');
}
@@ -357,7 +374,7 @@ class DialogController extends AbstractController
* @apiName msg__list
*
* @apiParam {Number} dialog_id 对话ID
* @apiParam {Number} msg_id 消息ID
* @apiParam {Number} [msg_id] 消息ID
* @apiParam {Number} [position_id] 此消息ID前后的数据
* @apiParam {Number} [prev_id] 此消息ID之前的数据
* @apiParam {Number} [next_id] 此消息ID之后的数据
@@ -459,6 +476,7 @@ class DialogController extends AbstractController
->value('id'));
}
$data['list'] = $list;
$data['time'] = Base::time();
// 记录当前打开的任务对话
if ($dialog->type == 'group' && $dialog->group_type == 'task') {
$user->task_dialog_id = $dialog->id;
@@ -479,7 +497,44 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/one 10. 获取单条消息
* @api {get} api/dialog/msg/search 10. 搜索消息位置
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__search
*
* @apiParam {Number} dialog_id 对话ID
* @apiParam {String} key 搜索关键词
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__search()
{
User::auth();
//
$dialog_id = intval(Request::input('dialog_id'));
$key = trim(Request::input('key'));
//
if (empty($key)) {
return Base::retError('关键词不能为空');
}
//
WebSocketDialog::checkDialog($dialog_id);
//
$data = WebSocketDialogMsg::whereDialogId($dialog_id)
->where('key', 'LIKE', "%{$key}%")
->take(200)
->pluck('id');
return Base::retSuccess('success', [
'data' => $data
]);
}
/**
* @api {get} api/dialog/msg/one 11. 获取单条消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -508,7 +563,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/read 11. 标记已读
* @api {get} api/dialog/msg/read 12. 已读聊天消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -528,50 +583,79 @@ class DialogController extends AbstractController
$id = Request::input('id');
$ids = Base::explodeInt($id);
//
WebSocketDialogMsg::whereIn('id', $ids)->chunkById(20, function($list) use ($user) {
$dialogIds = [];
WebSocketDialogMsg::whereIn('id', $ids)->chunkById(20, function($list) use ($user, &$dialogIds) {
/** @var WebSocketDialogMsg $item */
foreach ($list as $item) {
$item->readSuccess($user->userid);
$dialogIds[$item->dialog_id] = $item->dialog_id;
}
});
return Base::retSuccess('success');
//
$data = [];
$dialogUsers = WebSocketDialogUser::with(['webSocketDialog'])->whereUserid($user->userid)->whereIn('dialog_id', array_values($dialogIds))->get();
foreach ($dialogUsers as $dialogUser) {
if (!$dialogUser->webSocketDialog) {
continue;
}
$dialogUser->updated_at = Carbon::now();
$dialogUser->save();
//
$dialogUser->webSocketDialog->generateUnread($user->userid);
$data[] = [
'id' => $dialogUser->webSocketDialog->id,
'unread' => $dialogUser->webSocketDialog->unread,
'mention' => $dialogUser->webSocketDialog->mention,
'user_at' => Carbon::parse($dialogUser->updated_at)->toDateTimeString('millisecond'),
'user_ms' => Carbon::parse($dialogUser->updated_at)->valueOf()
];
}
return Base::retSuccess('success', $data);
}
/**
* @api {get} api/dialog/msg/unread 12. 获取未读消息数
* @api {get} api/dialog/msg/unread 13. 获取未读消息数
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__unread
*
* @apiParam {Number} [dialog_id] 对话ID,留空获取总未读消息数量
* @apiParam {Number} dialog_id 对话ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
* @apiSuccessExample {json} data:
{
"unread": 43, // 未读消息数
"last_umid": 308 // 最新的一条未读消息ID用于判断是否更新前端的未读数量
"id": 43,
"unread": 308,
"mention": 11,
"user_at": "2020-12-12 00:00:00.000",
"user_ms": 1677558147167,
}
*/
public function msg__unread()
{
$dialog_id = intval(Request::input('dialog_id'));
//
$builder = WebSocketDialogMsgRead::whereUserid(User::userid())->whereReadAt(null);
if ($dialog_id > 0) {
$builder->whereDialogId($dialog_id);
$dialogUser = WebSocketDialogUser::with(['webSocketDialog'])->whereDialogId($dialog_id)->whereUserid(User::userid())->first();
if (empty($dialogUser?->webSocketDialog)) {
return Base::retError('会话不存在');
}
$dialogUser->webSocketDialog->generateUnread($dialogUser->userid);
//
return Base::retSuccess('success', [
'unread' => $builder->count(),
'last_umid' => intval($builder->orderByDesc('msg_id')->value('msg_id')),
'id' => $dialogUser->webSocketDialog->id,
'unread' => $dialogUser->webSocketDialog->unread,
'mention' => $dialogUser->webSocketDialog->mention,
'user_at' => Carbon::parse($dialogUser->updated_at)->toDateTimeString('millisecond'),
'user_ms' => Carbon::parse($dialogUser->updated_at)->valueOf()
]);
}
/**
* @api {post} api/dialog/msg/sendtext 13. 发送消息
* @api {post} api/dialog/msg/sendtext 14. 发送消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -580,7 +664,10 @@ class DialogController extends AbstractController
*
* @apiParam {Number} dialog_id 对话ID
* @apiParam {String} text 消息内容
* @apiParam {Number} [update_id] 更新消息ID优先大于reply_id
* @apiParam {String} [text_type] 消息类型
* - html: HTML默认
* - md: MARKDOWN
* @apiParam {Number} [update_id] 更新消息ID优先大于 reply_id
* @apiParam {Number} [reply_id] 回复ID
* @apiParam {String} [silence] 是否静默发送
* - no: 正常发送(默认)
@@ -592,7 +679,6 @@ class DialogController extends AbstractController
*/
public function msg__sendtext()
{
Base::checkClientVersion('0.19.0');
$user = User::auth();
//
if (!$user->bot) {
@@ -607,11 +693,13 @@ class DialogController extends AbstractController
}
}
//
$dialog_id = Base::getPostInt('dialog_id');
$update_id = Base::getPostInt('update_id');
$reply_id = Base::getPostInt('reply_id');
$text = trim(Base::getPostValue('text'));
$silence = trim(Base::getPostValue('silence')) === 'yes';
$dialog_id = intval(Request::input('dialog_id'));
$update_id = intval(Request::input('update_id'));
$reply_id = intval(Request::input('reply_id'));
$text = trim(Request::input('text'));
$text_type = strtolower(trim(Request::input('text_type')));
$silence = in_array(strtolower(trim(Request::input('silence'))), ['yes', 'true', '1']);
$markdown = in_array($text_type, ['md', 'markdown']);
//
WebSocketDialog::checkDialog($dialog_id);
//
@@ -623,7 +711,9 @@ class DialogController extends AbstractController
$action = "";
}
//
$text = WebSocketDialogMsg::formatMsg($text, $dialog_id);
if (!$markdown) {
$text = WebSocketDialogMsg::formatMsg($text, $dialog_id);
}
$strlen = mb_strlen($text);
$noimglen = mb_strlen(preg_replace("/<img[^>]*?>/i", "", $text));
if ($strlen < 1) {
@@ -643,8 +733,9 @@ class DialogController extends AbstractController
if (empty($size)) {
return Base::retError('消息发送保存失败');
}
$ext = $markdown ? 'md' : 'htm';
$fileData = [
'name' => "LongText-{$strlen}.htm",
'name' => "LongText-{$strlen}.{$ext}",
'size' => $size,
'file' => $file,
'path' => $path,
@@ -652,16 +743,20 @@ class DialogController extends AbstractController
'thumb' => '',
'width' => -1,
'height' => -1,
'ext' => 'htm',
'ext' => $ext,
];
return WebSocketDialogMsg::sendMsg($action, $dialog_id, 'file', $fileData, $user->userid, false, false, $silence);
}
//
return WebSocketDialogMsg::sendMsg($action, $dialog_id, 'text', ['text' => $text], $user->userid, false, false, $silence);
$msgData = ['text' => $text];
if ($markdown) {
$msgData['type'] = 'md';
}
return WebSocketDialogMsg::sendMsg($action, $dialog_id, 'text', $msgData, $user->userid, false, false, $silence);
}
/**
* @api {post} api/dialog/msg/sendrecord 14. 发送语音
* @api {post} api/dialog/msg/sendrecord 15. 发送语音
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -681,15 +776,15 @@ class DialogController extends AbstractController
{
$user = User::auth();
//
$dialog_id = Base::getPostInt('dialog_id');
$reply_id = Base::getPostInt('reply_id');
$dialog_id = intval(Request::input('dialog_id'));
$reply_id = intval(Request::input('reply_id'));
//
WebSocketDialog::checkDialog($dialog_id);
//
$action = $reply_id > 0 ? "reply-$reply_id" : "";
$path = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
$base64 = Base::getPostValue('base64');
$duration = Base::getPostInt('duration');
$base64 = Request::input('base64');
$duration = intval(Request::input('duration'));
if ($duration < 600) {
return Base::retError('说话时间太短');
}
@@ -708,7 +803,7 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/sendfile 15. 文件上传
* @api {post} api/dialog/msg/sendfile 16. 文件上传
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -730,16 +825,16 @@ class DialogController extends AbstractController
{
$user = User::auth();
//
$dialog_id = Base::getPostInt('dialog_id');
$reply_id = Base::getPostInt('reply_id');
$image_attachment = Base::getPostInt('image_attachment');
$dialog_id = intval(Request::input('dialog_id'));
$reply_id = intval(Request::input('reply_id'));
$image_attachment = intval(Request::input('image_attachment'));
//
$dialog = WebSocketDialog::checkDialog($dialog_id);
//
$action = $reply_id > 0 ? "reply-$reply_id" : "";
$path = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
$image64 = Base::getPostValue('image64');
$fileName = Base::getPostValue('filename');
$image64 = Request::input('image64');
$fileName = Request::input('filename');
if ($image64) {
$data = Base::image64save([
"image64" => $image64,
@@ -792,7 +887,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/sendfileid 15. 通过文件ID发送文件
* @api {get} api/dialog/msg/sendfileid 17. 通过文件ID发送文件
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -819,12 +914,11 @@ class DialogController extends AbstractController
return Base::retError("请选择转发对话或成员");
}
//
$file = File::permissionFind($file_id);
$file = File::permissionFind($file_id, $user);
$fileLink = $file->getShareLink($user->userid);
$fileMsg = "<a class=\"mention file\" href=\"{{RemoteURL}}single/file/{$fileLink['code']}\" target=\"_blank\">~{$file->getNameAndExt()}</a>";
//
$sender = $user->userid;
return AbstractModel::transaction(function() use ($sender, $fileMsg, $userids, $dialogids) {
return AbstractModel::transaction(function() use ($user, $fileMsg, $userids, $dialogids) {
$msgs = [];
$already = [];
if ($dialogids) {
@@ -832,7 +926,7 @@ class DialogController extends AbstractController
$dialogids = [$dialogids];
}
foreach ($dialogids as $dialogid) {
$res = WebSocketDialogMsg::sendMsg(null, $dialogid, 'text', ['text' => $fileMsg], $sender);
$res = WebSocketDialogMsg::sendMsg(null, $dialogid, 'text', ['text' => $fileMsg], $user->userid);
if (Base::isSuccess($res)) {
$msgs[] = $res['data'];
$already[] = $dialogid;
@@ -847,9 +941,9 @@ class DialogController extends AbstractController
if (!User::whereUserid($userid)->exists()) {
continue;
}
$dialog = WebSocketDialog::checkUserDialog($sender, $userid);
$dialog = WebSocketDialog::checkUserDialog($user, $userid);
if ($dialog && !in_array($dialog->id, $already)) {
$res = WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $fileMsg], $sender);
$res = WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $fileMsg], $user->userid);
if (Base::isSuccess($res)) {
$msgs[] = $res['data'];
}
@@ -863,7 +957,60 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/readlist 16. 获取消息阅读情况
* @api {post} api/dialog/msg/sendanon 18. 发送匿名消息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__sendanon
*
* @apiParam {Number} userid 对方会员ID
* @apiParam {String} text 消息内容
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__sendanon()
{
User::auth();
//
$userid = intval(Request::input('userid'));
$text = trim(Request::input('text'));
//
$anonMessage = Base::settingFind('system', 'anon_message', 'open');
if ($anonMessage != 'open') {
return Base::retError("匿名消息功能暂停使用");
}
//
$toUser = User::whereUserid($userid)->first();
if (empty($toUser) || $toUser->bot) {
return Base::retError("匿名消息仅允许发送给个人");
}
if ($toUser->isDisable()) {
return Base::retError("对方已离职");
}
$strlen = mb_strlen($text);
if ($strlen < 1) {
return Base::retError('消息内容不能为空');
}
if ($strlen > 2000) {
return Base::retError('消息内容最大不能超过2000字');
}
//
$botUser = User::botGetOrCreate('anon-msg');
if (empty($botUser)) {
return Base::retError('匿名机器人不存在');
}
$dialog = WebSocketDialog::checkUserDialog($botUser, $toUser->userid);
if (empty($dialog)) {
return Base::retError('匿名机器人会话不存在');
}
return WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => "<p>{$text}</p>"], $botUser->userid, true);
}
/**
* @api {get} api/dialog/msg/readlist 19. 获取消息阅读情况
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -892,7 +1039,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/detail 17. 消息详情
* @api {get} api/dialog/msg/detail 20. 消息详情
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -940,7 +1087,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/download 18. 文件下载
* @api {get} api/dialog/msg/download 21. 文件下载
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -977,13 +1124,13 @@ class DialogController extends AbstractController
}
//
$filePath = public_path($array['path']);
return Response::streamDownload(function() use ($filePath) {
return Base::streamDownload(function() use ($filePath) {
echo file_get_contents($filePath);
}, $array['name']);
}
/**
* @api {get} api/dialog/msg/withdraw 19. 聊天消息撤回
* @api {get} api/dialog/msg/withdraw 22. 聊天消息撤回
*
* @apiDescription 消息撤回限制24小时内需要token身份
* @apiVersion 1.0.0
@@ -1004,12 +1151,12 @@ class DialogController extends AbstractController
if (empty($msg)) {
return Base::retError("消息不存在或已被删除");
}
$msg->deleteMsg();
$msg->withdrawMsg();
return Base::retSuccess("success");
}
/**
* @api {get} api/dialog/msg/mark 20. 消息标记操作
* @api {get} api/dialog/msg/mark 23. 消息标记操作
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1018,8 +1165,9 @@ class DialogController extends AbstractController
*
* @apiParam {Number} dialog_id 会话ID
* @apiParam {String} type 类型
* - read
* - unread
* - read: 已读
* - unread: 未读
* @apiParam {Number} [after_msg_id] 仅标记已读指定之后(含)的消息
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -1028,42 +1176,54 @@ class DialogController extends AbstractController
public function msg__mark()
{
$user = User::auth();
$dialogId = intval(Request::input('dialog_id'));
//
$dialog_id = intval(Request::input('dialog_id'));
$type = Request::input('type');
$dialogUser = WebSocketDialogUser::whereUserid($user->userid)->whereDialogId($dialogId)->first();
if (!$dialogUser) {
return Base::retError("会话不存在");
$after_msg_id = intval(Request::input('after_msg_id'));
//
$dialogUser = WebSocketDialogUser::with(['webSocketDialog'])->whereDialogId($dialog_id)->whereUserid($user->userid)->first();
if (empty($dialogUser?->webSocketDialog)) {
return Base::retError('会话不存在');
}
$data = [
'id' => $dialogId,
];
switch ($type) {
case 'read':
$data['unread'] = 0;
WebSocketDialogMsgRead::whereUserid($user->userid)
->whereReadAt(null)
->whereDialogId($dialogId)
->chunkById(100, function ($list) {
WebSocketDialogMsgRead::onlyMarkRead($list);
});
$dialogUser->mark_unread = 0;
$dialogUser->save();
$builder = WebSocketDialogMsgRead::whereDialogId($dialog_id)->whereUserid($user->userid)->whereReadAt(null);
if ($after_msg_id > 0) {
$builder->where('msg_id', '>=', $after_msg_id);
}
$builder->chunkById(100, function ($list) {
WebSocketDialogMsgRead::onlyMarkRead($list);
});
//
$dialogUser->webSocketDialog->generateUnread($user->userid);
$data = [
'id' => $dialogUser->webSocketDialog->id,
'unread' => $dialogUser->webSocketDialog->unread,
'mention' => $dialogUser->webSocketDialog->mention,
'mark_unread' => 0,
];
break;
case 'unread':
$dialogUser->mark_unread = 1;
$dialogUser->save();
$data = [
'id' => $dialogUser->webSocketDialog->id,
'mark_unread' => 1,
];
break;
default:
return Base::retError("参数错误");
}
$data['mark_unread'] = $dialogUser->mark_unread;
return Base::retSuccess("success", $data);
$dialogUser->mark_unread = $data['mark_unread'];
$dialogUser->save();
return Base::retSuccess("success", array_merge($data, [
'user_at' => Carbon::parse($dialogUser->updated_at)->toDateTimeString('millisecond'),
'user_ms' => Carbon::parse($dialogUser->updated_at)->valueOf(),
]));
}
/**
* @api {get} api/dialog/msg/silence 20. 消息免打扰
* @api {get} api/dialog/msg/silence 24. 消息免打扰
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1126,7 +1286,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/forward 21. 转发消息给
* @api {get} api/dialog/msg/forward 25. 转发消息给
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1159,11 +1319,11 @@ class DialogController extends AbstractController
}
WebSocketDialog::checkDialog($msg->dialog_id);
//
return $msg->forwardMsg($dialogids, $userids, $user->userid);
return $msg->forwardMsg($dialogids, $userids, $user);
}
/**
* @api {get} api/dialog/msg/emoji 22. emoji回复
* @api {get} api/dialog/msg/emoji 26. emoji回复
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1198,7 +1358,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/tag 23. 标注/取消标注
* @api {get} api/dialog/msg/tag 27. 标注/取消标注
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1227,7 +1387,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/todo 24. 设待办/取消待办
* @api {get} api/dialog/msg/todo 28. 设待办/取消待办
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1270,7 +1430,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/todolist 25. 获取消息待办情况
* @api {get} api/dialog/msg/todolist 29. 获取消息待办情况
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1300,7 +1460,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/msg/done 26. 完成待办
* @api {get} api/dialog/msg/done 30. 完成待办
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1347,15 +1507,16 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/add 27. 新增群组
* @api {get} api/dialog/group/add 31. 新增群组
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName group__add
*
* @apiParam {String} [avatar] 群头像
* @apiParam {String} [chat_name] 群名称
* @apiParam {Array} userids 群成员,格式: [userid1, userid2, userid3]
* @apiParam {String} chat_name 群名称
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -1365,8 +1526,10 @@ class DialogController extends AbstractController
{
$user = User::auth();
//
$userids = Request::input('userids');
$avatar = Request::input('avatar');
$avatar = $avatar ? Base::unFillUrl(is_array($avatar) ? $avatar[0]['path'] : $avatar) : '';
$chatName = trim(Request::input('chat_name'));
$userids = Request::input('userids');
//
if (!is_array($userids)) {
return Base::retError('请选择群成员');
@@ -1388,10 +1551,17 @@ class DialogController extends AbstractController
}
$chatName = implode(", ", $array);
}
if ($user->isTemp()) {
return Base::retError('无法创建群组');
}
$dialog = WebSocketDialog::createGroup($chatName, $userids, 'user', $user->userid);
if (empty($dialog)) {
return Base::retError('创建群组失败');
}
if ($avatar) {
$dialog->avatar = $avatar;
$dialog->save();
}
$data = WebSocketDialog::find($dialog->id)?->formatData($user->userid);
$userids = array_values(array_diff($userids, [$user->userid]));
$dialog->pushMsg("groupAdd", null, $userids);
@@ -1399,7 +1569,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/edit 28. 修改群组
* @api {get} api/dialog/group/edit 32. 修改群组
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1407,7 +1577,9 @@ class DialogController extends AbstractController
* @apiName group__edit
*
* @apiParam {Number} dialog_id 会话ID
* @apiParam {String} chat_name名称
* @apiParam {String} [avatar] 头像
* @apiParam {String} [chat_name] 群名称
* @apiParam {Number} [admin] 系统管理员操作1只判断是不是系统管理员否则判断是否群管理员
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -1418,27 +1590,47 @@ class DialogController extends AbstractController
$user = User::auth();
//
$dialog_id = intval(Request::input('dialog_id'));
$chatName = trim(Request::input('chat_name'));
$admin = intval(Request::input('admin'));
//
if (mb_strlen($chatName) < 2) {
return Base::retError('群名称至少2个字');
}
if (mb_strlen($chatName) > 100) {
return Base::retError('群名称最长限制100个字');
if ($admin === 1) {
$user->checkAdmin();
$dialog = WebSocketDialog::find($dialog_id);
if (empty($dialog)) {
return Base::retError('对话不存在或已被删除', ['dialog_id' => $dialog_id], -4003);
}
} else {
$dialog = WebSocketDialog::checkDialog($dialog_id, true);
}
//
$dialog = WebSocketDialog::checkDialog($dialog_id, true);
$data = ['id' => $dialog->id];
$array = [];
if (Request::exists('avatar')) {
$avatar = Request::input('avatar');
$avatar = $avatar ? Base::unFillUrl(is_array($avatar) ? $avatar[0]['path'] : $avatar) : '';
$data['avatar'] = Base::fillUrl($array['avatar'] = $avatar);
}
if (Request::exists('chat_name') && $dialog->group_type === 'user') {
$chatName = trim(Request::input('chat_name'));
if (mb_strlen($chatName) < 2) {
return Base::retError('群名称至少2个字');
}
if (mb_strlen($chatName) > 100) {
return Base::retError('群名称最长限制100个字');
}
$data['name'] = $array['name'] = $chatName;
}
//
$dialog->name = $chatName;
$dialog->save();
return Base::retSuccess('修改成功', [
'id' => $dialog->id,
'name' => $dialog->name,
]);
if ($array) {
$dialog->updateInstance($array);
$dialog->save();
WebSocketDialogUser::whereDialogId($dialog->id)->change(['updated_at' => Carbon::now()->toDateTimeString('millisecond')]);
}
//
return Base::retSuccess('修改成功', $data);
}
/**
* @api {get} api/dialog/group/adduser 29. 添加群成员
* @api {get} api/dialog/group/adduser 33. 添加群成员
*
* @apiDescription 需要token身份
* - 有群主时:只有群主可以邀请
@@ -1474,7 +1666,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/deluser 30. 移出(退出)群成员
* @api {get} api/dialog/group/deluser 34. 移出(退出)群成员
*
* @apiDescription 需要token身份
* - 只有群主、邀请人可以踢人
@@ -1518,7 +1710,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/transfer 31. 转让群组
* @api {get} api/dialog/group/transfer 35. 转让群组
*
* @apiDescription 需要token身份
* - 只有群主且是个人类型群可以解散
@@ -1562,7 +1754,7 @@ class DialogController extends AbstractController
}
/**
* @api {get} api/dialog/group/disband 32. 解散群组
* @api {get} api/dialog/group/disband 36. 解散群组
*
* @apiDescription 需要token身份
* - 只有群主且是个人类型群可以解散
@@ -1586,12 +1778,11 @@ class DialogController extends AbstractController
//
$dialog->checkGroup('user');
$dialog->deleteDialog();
$dialog->pushMsg("groupDelete");
return Base::retSuccess('解散成功');
}
/**
* @api {get} api/dialog/group/searchuser 33. 搜索个人群(仅限管理员)
* @api {get} api/dialog/group/searchuser 37. 搜索个人群(仅限管理员)
*
* @apiDescription 需要token身份用于创建部门搜索个人群组
* @apiVersion 1.0.0

View File

@@ -45,9 +45,10 @@ class FileController extends AbstractController
$pid = intval($data['pid']);
//
$permission = 1000;
$userids = $user->isTemp() ? [$user->userid] : [0, $user->userid];
$builder = File::wherePid($pid);
if ($pid > 0) {
File::permissionFind($pid, 0, $permission);
File::permissionFind($pid, $userids, 0, $permission);
} else {
$builder->whereUserid($user->userid);
}
@@ -66,7 +67,7 @@ class FileController extends AbstractController
}
$pid = $file->pid;
$temp = $file->toArray();
$temp['permission'] = $file->getPermission($user->userid);
$temp['permission'] = $file->getPermission($userids);
$array[] = $temp;
}
// 去除没有权限的文件
@@ -92,9 +93,7 @@ class FileController extends AbstractController
$list = File::select(["files.*", DB::raw("MAX({$pre}file_users.permission) as permission")])
->join('file_users', 'files.id', '=', 'file_users.file_id')
->where('files.userid', '!=', $user->userid)
->where(function ($query) use ($user) {
$query->whereIn('file_users.userid', [0, $user->userid]);
})
->whereIn('file_users.userid', $userids)
->groupBy('files.id')
->take(100)
->get();
@@ -135,13 +134,18 @@ class FileController extends AbstractController
//
$permission = 0;
if (Base::isNumber($id)) {
User::auth();
$file = File::permissionFind(intval($id), 0, $permission);
$user = User::auth();
$file = File::permissionFind(intval($id), $user, 0, $permission);
} elseif ($id) {
$fileLink = FileLink::whereCode($id)->first();
$file = $fileLink?->file;
if (empty($file)) {
return Base::retError('链接不存在');
$msg = '文件链接不存在';
$data = File::code2IdName($id);
if ($data) {
$msg = "{$data->name}{$msg}";
}
return Base::retError($msg, $data);
}
} else {
return Base::retError('参数错误');
@@ -160,6 +164,7 @@ class FileController extends AbstractController
* @apiGroup file
* @apiName search
*
* @apiParam {String} [link] 通过分享地址搜索https://t.hitosea.com/single/file/ODcwOCwzOSxpa0JBS2lmVQ==
* @apiParam {String} [key] 关键词
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
@@ -170,28 +175,45 @@ class FileController extends AbstractController
{
$user = User::auth();
//
$link = trim(Request::input('link'));
$key = trim(Request::input('key'));
$id = 0;
$take = 50;
if (preg_match("/\/single\/file\/(.*?)$/i", $link, $match)) {
$id = intval(FileLink::whereCode($match[1])->value('file_id'));
$take = 1;
if (empty($id)) {
return Base::retSuccess('success', []);
}
}
// 搜索自己的
$builder = File::whereUserid($user->userid);
if ($id) {
$builder->where("id", $id);
}
if ($key) {
$builder->where("name", "like", "%{$key}%");
}
$array = $builder->take(50)->get()->toArray();
$array = $builder->take($take)->get()->toArray();
// 搜索共享的
$take = 50 - count($array);
if ($take > 0 && $key) {
$list = File::where("name", "like", "%{$key}%")
->whereIn('pshare', function ($queryA) use ($user) {
$queryA->select('files.id')
->from('files')
->join('file_users', 'files.id', '=', 'file_users.file_id')
->where('files.userid', '!=', $user->userid)
->where(function ($queryB) use ($user) {
$queryB->whereIn('file_users.userid', [0, $user->userid]);
});
})
->take($take)
->get();
$take = $take - count($array);
if ($take > 0 && ($id || $key)) {
$builder = File::whereIn('pshare', function ($queryA) use ($user) {
$queryA->select('files.id')
->from('files')
->join('file_users', 'files.id', '=', 'file_users.file_id')
->where('files.userid', '!=', $user->userid)
->where(function ($queryB) use ($user) {
$queryB->whereIn('file_users.userid', [0, $user->userid]);
});
});
if ($id) {
$builder->where("id", $id);
}
if ($key) {
$builder->where("name", "like", "%{$key}%");
}
$list = $builder->take($take)->get();
if ($list->isNotEmpty()) {
foreach ($list as $file) {
$temp = $file->toArray();
@@ -236,16 +258,24 @@ class FileController extends AbstractController
} elseif (mb_strlen($name) > 32) {
return Base::retError('文件名称最多只能设置32个字');
}
$tmpName = preg_replace("/[\\\\\/:*?\"<>|]/", '', $name);
if ($tmpName != $name) {
return Base::retError("文件名称不能包含这些字符:\/:*?\"<>|");
}
//
if ($id > 0) {
// 修改
$file = File::permissionFind($id, 1);
$file = File::permissionFind($id, $user, 1);
//
$file->name = $name;
$file->handleDuplicateName();
$file->save();
$file->pushMsg('update', $file);
return Base::retSuccess('修改成功', $file);
$data = [
'id' => $file->id,
'name' => $file->name,
];
$file->pushMsg('update', $data);
return Base::retSuccess('修改成功', $data);
} else {
// 添加
if (!in_array($type, [
@@ -282,7 +312,7 @@ class FileController extends AbstractController
if (File::wherePid($pid)->count() >= 300) {
return Base::retError('每个文件夹里最多只能创建300个文件或文件夹');
}
$row = File::permissionFind($pid, 1);
$row = File::permissionFind($pid, $user, 1);
$userid = $row->userid;
} else {
if (File::whereUserid($user->userid)->wherePid(0)->count() >= 300) {
@@ -327,7 +357,7 @@ class FileController extends AbstractController
//
$id = intval(Request::input('id'));
//
$row = File::permissionFind($id);
$row = File::permissionFind($id, $user);
//
$userid = $user->userid;
if ($row->pid > 0) {
@@ -397,14 +427,14 @@ class FileController extends AbstractController
}
$toShareFile = false;
if ($pid > 0) {
$tmpFile = File::permissionFind($pid, 1);
$tmpFile = File::permissionFind($pid, $user, 1);
$toShareFile = $tmpFile->getShareInfo();
}
//
$files = [];
AbstractModel::transaction(function() use ($user, $pid, $ids, $toShareFile, &$files) {
foreach ($ids as $id) {
$file = File::permissionFind($id, 1000);
$file = File::permissionFind($id, $user, 1000);
//
if ($pid > 0) {
if ($toShareFile) {
@@ -458,7 +488,7 @@ class FileController extends AbstractController
*/
public function remove()
{
User::auth();
$user = User::auth();
//
$ids = Request::input('ids');
//
@@ -470,9 +500,9 @@ class FileController extends AbstractController
}
//
$files = [];
AbstractModel::transaction(function() use ($ids, &$files) {
AbstractModel::transaction(function() use ($user, $ids, &$files) {
foreach ($ids as $id) {
$file = File::permissionFind($id, 1000);
$file = File::permissionFind($id, $user, 1000);
$file->deleteFile();
$files[] = $file;
}
@@ -513,13 +543,18 @@ class FileController extends AbstractController
$history_id = intval(Request::input('history_id'));
//
if (Base::isNumber($id)) {
User::auth();
$file = File::permissionFind(intval($id));
$user = User::auth();
$file = File::permissionFind(intval($id), $user);
} elseif ($id) {
$fileLink = FileLink::whereCode($id)->first();
$file = $fileLink?->file;
if (empty($file)) {
return Base::retError('链接不存在');
$msg = '文件链接不存在';
$data = File::code2IdName($id);
if ($data) {
$msg = "{$data->name}{$msg}";
}
return Base::retError($msg, $data);
}
} else {
return Base::retError('参数错误');
@@ -563,21 +598,21 @@ class FileController extends AbstractController
{
$user = User::auth();
//
$id = Base::getPostInt('id');
$content = Base::getPostValue('content');
$id = intval(Request::input('id'));
$content = Request::input('content');
//
$file = File::permissionFind($id, 1);
$file = File::permissionFind($id, $user, 1);
//
$text = '';
if ($file->type == 'document') {
$data = Base::json2array($content);
$isRep = false;
preg_match_all("/<img\s+src=\"data:image\/(png|jpg|jpeg);base64,(.*?)\"/s", $data['content'], $matchs);
preg_match_all("/<img\s+src=\"data:image\/(png|jpg|jpeg|webp);base64,(.*?)\"/s", $data['content'], $matchs);
foreach ($matchs[2] as $key => $text) {
$tmpPath = "uploads/file/document/" . date("Ym") . "/" . $id . "/attached/";
Base::makeDir(public_path($tmpPath));
$tmpPath .= md5($text) . "." . $matchs[1][$key];
if (file_put_contents(public_path($tmpPath), base64_decode($text))) {
if (Base::saveContentImage(public_path($tmpPath), base64_decode($text))) {
$paramet = getimagesize(public_path($tmpPath));
$data['content'] = str_replace($matchs[0][$key], '<img src="' . Base::fillUrl($tmpPath) . '" original-width="' . $paramet[0] . '" original-height="' . $paramet[1] . '"', $data['content']);
$isRep = true;
@@ -659,7 +694,7 @@ class FileController extends AbstractController
$key = Request::input('key');
$url = Request::input('url');
//
$file = File::permissionFind($id, 1);
$file = File::permissionFind($id, $user, 1);
//
if ($status === 2) {
$parse = parse_url($url);
@@ -717,7 +752,7 @@ class FileController extends AbstractController
if (File::wherePid($pid)->count() >= 300) {
return Base::retError('每个文件夹里最多只能创建300个文件或文件夹');
}
$row = File::permissionFind($pid, 1);
$row = File::permissionFind($pid, $user, 1);
$userid = $row->userid;
} else {
if (File::whereUserid($user->userid)->wherePid(0)->count() >= 300) {
@@ -776,7 +811,7 @@ class FileController extends AbstractController
'xls', 'xlsx' => "excel",
'ppt', 'pptx' => "ppt",
'wps' => "wps",
'jpg', 'jpeg', 'png', 'gif', 'bmp', 'ico', 'raw', 'svg' => "picture",
'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",
@@ -863,9 +898,11 @@ class FileController extends AbstractController
*/
public function content__history()
{
$user = User::auth();
//
$id = Request::input('id');
//
$file = File::permissionFind(intval($id));
$file = File::permissionFind(intval($id), $user);
//
$data = FileContent::select(['id', 'size', 'userid', 'created_at'])
->whereFid($file->id)
@@ -896,7 +933,7 @@ class FileController extends AbstractController
$id = intval(Request::input('id'));
$history_id = intval(Request::input('history_id'));
//
$file = File::permissionFind($id);
$file = File::permissionFind($id, $user);
//
$history = FileContent::whereFid($file->id)->whereId($history_id)->first();
if (empty($history)) {
@@ -1060,7 +1097,7 @@ class FileController extends AbstractController
//
$id = intval(Request::input('id'));
//
$file = File::permissionFind($id);
$file = File::permissionFind($id, $user);
//
if ($file->userid == $user->userid) {
return Base::retError('不能退出自己共享的文件');
@@ -1098,7 +1135,7 @@ class FileController extends AbstractController
$id = intval(Request::input('id'));
$refresh = Request::input('refresh', 'no');
//
$file = File::permissionFind($id);
$file = File::permissionFind($id, $user);
$fileLink = $file->getShareLink($user->userid, $refresh == 'yes');
//
return Base::retSuccess('success', $fileLink);

View File

@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api;
use App\Exceptions\ApiException;
use App\Models\AbstractModel;
use App\Models\Deleted;
use App\Models\File;
use App\Models\FileContent;
use App\Models\Project;
@@ -21,6 +22,8 @@ use App\Models\WebSocketDialog;
use App\Module\Base;
use App\Module\BillExport;
use App\Module\BillMultipleExport;
use App\Module\Doo;
use App\Module\TimeRange;
use Carbon\Carbon;
use Illuminate\Support\Arr;
use Madzipper;
@@ -58,6 +61,9 @@ class ProjectController extends AbstractController
* - yes取列表
* @apiParam {Object} [keys] 搜索条件
* - keys.name: 项目名称
* @apiParam {String} [timerange] 时间范围1678248944,1678248944
* - 第一个时间: 读取在这个时间之后更新的数据
* - 第二个时间: 读取在这个时间之后删除的数据ID第1页附加返回数据: deleted_id
*
* @apiParam {Number} [page] 当前页,默认:1
* @apiParam {Number} [pagesize] 每页显示数量,默认:50最大:100
@@ -104,6 +110,8 @@ class ProjectController extends AbstractController
$type = Request::input('type', 'all');
$archived = Request::input('archived', 'no');
$getcolumn = Request::input('getcolumn', 'no');
$keys = Request::input('keys');
$timerange = TimeRange::parse(Request::input('timerange'));
//
if ($all) {
$user->identity('admin');
@@ -128,24 +136,29 @@ class ProjectController extends AbstractController
$builder->whereNull('projects.archived_at');
}
//
$keys = Request::input('keys');
if (is_array($keys) || $timerange->updated) {
$totalAll = $builder->clone()->count();
}
//
if (is_array($keys)) {
$buildClone = $builder->clone();
if ($keys['name']) {
$builder->where("projects.name", "like", "%{$keys['name']}%");
}
}
//
if ($timerange->updated) {
$builder->where('projects.updated_at', '>', $timerange->updated);
}
//
$list = $builder->orderByDesc('projects.id')->paginate(Base::getPaginate(100, 50));
$list->transform(function (Project $project) use ($user) {
return array_merge($project->toArray(), $project->getTaskStatistics($user->userid));
});
//
$data = $list->toArray();
if (isset($buildClone)) {
$data['total_all'] = $buildClone->count();
} else {
$data['total_all'] = $data['total'];
$data['total_all'] = $totalAll ?? $data['total'];
if ($list->currentPage() === 1) {
$data['deleted_id'] = Deleted::ids('project', $user->userid, $timerange->deleted);
}
//
return Base::retSuccess('success', $data);
@@ -503,7 +516,7 @@ class ProjectController extends AbstractController
}
//
AbstractModel::transaction(function() use ($owner_userid, $project) {
ProjectUser::whereProjectId($project->id)->update(['owner' => 0]);
ProjectUser::whereProjectId($project->id)->change(['owner' => 0]);
ProjectUser::updateInsert([
'project_id' => $project->id,
'userid' => $owner_userid,
@@ -564,11 +577,11 @@ class ProjectController extends AbstractController
if (!is_array($item['task'])) continue;
$index = 0;
foreach ($item['task'] as $task_id) {
if (ProjectTask::whereId($task_id)->whereProjectId($project->id)->whereCompleteAt(null)->update([
if (ProjectTask::whereId($task_id)->whereProjectId($project->id)->whereCompleteAt(null)->change([
'column_id' => $item['id'],
'sort' => $index
])) {
ProjectTask::whereParentId($task_id)->whereProjectId($project->id)->update([
ProjectTask::whereParentId($task_id)->whereProjectId($project->id)->change([
'column_id' => $item['id'],
]);
}
@@ -834,27 +847,28 @@ class ProjectController extends AbstractController
* @apiName task__lists
*
* @apiParam {Object} [keys] 搜索条件
* - keys.name: 任务名称
* - keys.name: ID、任务名称
*
* @apiParam {Number} [project_id] 项目ID
* @apiParam {Number} [parent_id] 主任务IDproject_id && parent_id ≤ 0 时 仅查询自己参与的任务)
* - 大于0指定主任务下的子任务
* - 等于-1表示仅主任务
* @apiParam {String} [name] 任务描述关键词
*
* @apiParam {Array} [time] 指定时间范围,如:['2020-12-12', '2020-12-30']
* @apiParam {String} [time_before] 指定时间之前2020-12-30 00:00:00填写此项时 time 参数无效
* @apiParam {String} [complete] 完成状态
* - all所有默认
* - yes已完成
* - no未完成
* @apiParam {String} [timerange] 时间范围1678248944,1678248944
* - 第一个时间: 读取在这个时间之后更新的数据
* - 第二个时间: 读取在这个时间之后删除的数据ID第1页附加返回数据: deleted_id
*
* @apiParam {String} [archived] 归档状态
* - all所有
* - all所有parent_id > 0 时强制 all
* - yes已归档
* - no未归档默认
* @apiParam {String} [deleted] 是否读取已删除
* - all所有
* - yes已删除
* - no未删除默认
* @apiParam {Object} sorts 排序方式
*
* @apiParam {Object} [sorts] 排序方式
* - sorts.complete_at 完成时间asc|desc
* - sorts.archived_at 归档时间asc|desc
* - sorts.end_at 到期时间asc|desc
@@ -865,7 +879,7 @@ class ProjectController extends AbstractController
*/
public function task__lists()
{
User::auth();
$user = User::auth();
//
$builder = ProjectTask::with(['taskUser', 'taskTag']);
//
@@ -873,8 +887,7 @@ class ProjectController extends AbstractController
$project_id = intval(Request::input('project_id'));
$name = Request::input('name');
$time = Request::input('time');
$time_before = Request::input('time_before');
$complete = Request::input('complete', 'all');
$timerange = TimeRange::parse(Request::input('timerange'));
$archived = Request::input('archived', 'no');
$deleted = Request::input('deleted', 'no');
$keys = Request::input('keys');
@@ -883,7 +896,11 @@ class ProjectController extends AbstractController
$sorts = is_array($sorts) ? $sorts : [];
//
if ($keys['name']) {
$builder->where("project_tasks.name", "like", "%{$keys['name']}%");
if (Base::isNumber($keys['name'])) {
$builder->where("project_tasks.id", intval($keys['name']));
} else {
$builder->where("project_tasks.name", "like", "%{$keys['name']}%");
}
}
//
$scopeAll = false;
@@ -892,6 +909,7 @@ class ProjectController extends AbstractController
$isDeleted = str_replace(['all', 'yes', 'no'], [null, false, true], $deleted);
ProjectTask::userTask($parent_id, $isArchived, $isDeleted);
$scopeAll = true;
$archived = 'all';
$builder->where('project_tasks.parent_id', $parent_id);
} elseif ($parent_id === -1) {
$builder->where('project_tasks.parent_id', 0);
@@ -913,18 +931,13 @@ class ProjectController extends AbstractController
});
}
//
if (Base::isDateOrTime($time_before)) {
$builder->whereNotNull('project_tasks.end_at')->where('project_tasks.end_at', '<', Carbon::parse($time_before));
} elseif (is_array($time)) {
if (is_array($time)) {
if (Base::isDateOrTime($time[0]) && Base::isDateOrTime($time[1])) {
$builder->betweenTime(Carbon::parse($time[0])->startOfDay(), Carbon::parse($time[1])->endOfDay());
}
}
//
if ($complete === 'yes') {
$builder->whereNotNull('project_tasks.complete_at');
} elseif ($complete === 'no') {
$builder->whereNull('project_tasks.complete_at');
if ($timerange->updated) {
$builder->where('project_tasks.updated_at', '>', $timerange->updated);
}
//
if ($archived == 'yes') {
@@ -947,7 +960,12 @@ class ProjectController extends AbstractController
//
$list = $builder->orderByDesc('project_tasks.id')->paginate(Base::getPaginate(200, 100));
//
return Base::retSuccess('success', $list);
$data = $list->toArray();
if ($list->currentPage() === 1) {
$data['deleted_id'] = Deleted::ids('projectTask', $user->userid, $timerange->deleted);
}
//
return Base::retSuccess('success', $data);
}
/**
@@ -1113,7 +1131,8 @@ class ProjectController extends AbstractController
'styles' => ["A1:P1" => ["font" => ["bold" => true]]],
'data' => [],
];
$sheets[] = BillExport::create()->setTitle($data['nickname'] ?: $ownerid)->setHeadings($headings)->setData($data['data'])->setStyles($data['styles']);
$title = (count($sheets) + 1) . "." . ($data['nickname'] ?: $ownerid);
$sheets[] = BillExport::create()->setTitle($title)->setHeadings($headings)->setData($data['data'])->setStyles($data['styles']);
}
//
$fileName = User::userid2nickname($userid[0]) ?: $userid[0];
@@ -1153,7 +1172,116 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/task/down 20. 下载导出的任务
* @api {get} api/project/task/exportoverdue 20. 导出超期任务(限管理员)
*
* @apiDescription 导出指定范围任务已完成、未完成、已归档返回下载地址需要token身份
* @apiVersion 1.0.0
* @apiGroup project
* @apiName task__exportoverdue
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function task__exportoverdue()
{
$user = User::auth('admin');
//
$headings = [];
$headings[] = '任务ID';
$headings[] = '父级任务ID';
$headings[] = '所属项目';
$headings[] = '任务标题';
$headings[] = '任务开始时间';
$headings[] = '任务结束时间';
$headings[] = '任务计划用时';
$headings[] = '超时时间';
$headings[] = '负责人';
$headings[] = '创建人';
$data = [];
//
ProjectTask::whereNull('complete_at')
->whereNotNull('end_at')
->where('end_at', '<=', Carbon::now())
->orderBy('end_at')
->chunk(100, function ($tasks) use (&$data) {
/** @var ProjectTask $task */
foreach ($tasks as $task) {
$taskStartTime = Carbon::parse($task->start_at ?: $task->created_at)->timestamp;
$totalTime = time() - $taskStartTime; //开发测试总用时
$planTime = '-';//任务计划用时
$overTime = '-';//超时时间
if ($task->end_at) {
$startTime = Carbon::parse($task->start_at)->timestamp;
$endTime = Carbon::parse($task->end_at)->timestamp;
$planTotalTime = $endTime - $startTime;
$residueTime = $planTotalTime - $totalTime;
if ($residueTime < 0) {
$overTime = Base::timeFormat(abs($residueTime));
}
$planTime = Base::timeDiff($startTime, $endTime);
}
$ownerIds = $task->taskUser->where('owner', 1)->pluck('userid')->toArray();
$ownerNames = [];
foreach ($ownerIds as $ownerId) {
$ownerNames[] = Base::filterEmoji(User::userid2nickname($ownerId)) . " (ID: {$ownerId})";
}
$data[] = [
$task->id,
$task->parent_id ?: '-',
Base::filterEmoji($task->project?->name) ?: '-',
Base::filterEmoji($task->name),
$task->start_at ?: '-',
$task->end_at ?: '-',
$planTime ?: '-',
$overTime,
implode("", $ownerNames),
Base::filterEmoji(User::userid2nickname($task->userid)) . " (ID: {$task->userid})",
];
}
});
if (empty($data)) {
return Base::retError('没有任何数据');
}
//
$sheets = [
BillExport::create()->setTitle("超期任务")->setHeadings($headings)->setData($data)->setStyles(["A1:J1" => ["font" => ["bold" => true]]])
];
//
$fileName = '超期任务_' . Base::time() . '.xls';
$filePath = "temp/task/export/" . date("Ym", Base::time());
$export = new BillMultipleExport($sheets);
$res = $export->store($filePath . "/" . $fileName);
if ($res != 1) {
return Base::retError('导出失败,' . $fileName . '');
}
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xls') . ".zip";
$zipPath = storage_path($zipFile);
if (file_exists($zipPath)) {
Base::deleteDirAndFile($zipPath, true);
}
try {
Madzipper::make($zipPath)->add($xlsPath)->close();
} catch (\Throwable) {
}
//
if (file_exists($zipPath)) {
$base64 = base64_encode(Base::array2string([
'file' => $zipFile,
]));
Session::put('task::export:userid', $user->userid);
return Base::retSuccess('success', [
'size' => Base::twoFloat(filesize($zipPath) / 1024, true),
'url' => Base::fillUrl('api/project/task/down?key=' . urlencode($base64)),
]);
} else {
return Base::retError('打包失败,请稍后再试...');
}
}
/**
* @api {get} api/project/task/down 21. 下载导出的任务
*
* @apiVersion 1.0.0
* @apiGroup project
@@ -1179,7 +1307,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/task/one 21. 获取单个任务信息
* @api {get} api/project/task/one 22. 获取单个任务信息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1213,7 +1341,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/task/content 22. 获取任务详细描述
* @api {get} api/project/task/content 23. 获取任务详细描述
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1241,7 +1369,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/task/files 23. 获取任务文件列表
* @api {get} api/project/task/files 24. 获取任务文件列表
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1266,7 +1394,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/task/filedelete 24. 删除任务文件
* @api {get} api/project/task/filedelete 25. 删除任务文件
*
* @apiDescription 需要token身份项目、任务负责人
* @apiVersion 1.0.0
@@ -1299,7 +1427,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/task/filedetail 25. 获取任务文件详情
* @api {get} api/project/task/filedetail 26. 获取任务文件详情
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1343,7 +1471,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/task/filedown 26. 下载任务文件
* @api {get} api/project/task/filedown 27. 下载任务文件
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1386,13 +1514,13 @@ class ProjectController extends AbstractController
}
//
$filePath = public_path($file->getRawOriginal('path'));
return Response::streamDownload(function() use ($filePath) {
return Base::streamDownload(function() use ($filePath) {
echo file_get_contents($filePath);
}, $file->name);
}
/**
* @api {post} api/project/task/add 27. 添加任务
* @api {post} api/project/task/add 28. 添加任务
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1415,7 +1543,8 @@ class ProjectController extends AbstractController
public function task__add()
{
User::auth();
parse_str(Request::getContent(), $data);
//
$data = Request::input();
$project_id = intval($data['project_id']);
$column_id = $data['column_id'];
// 项目
@@ -1464,7 +1593,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/task/addsub 28. 添加子任务
* @api {get} api/project/task/addsub 29. 添加子任务
*
* @apiDescription 需要token身份项目、任务负责人
* @apiVersion 1.0.0
@@ -1504,7 +1633,7 @@ class ProjectController extends AbstractController
}
/**
* @api {post} api/project/task/update 29. 修改任务、子任务
* @api {post} api/project/task/update 30. 修改任务、子任务
*
* @apiDescription 需要token身份项目、任务负责人
* @apiVersion 1.0.0
@@ -1535,7 +1664,7 @@ class ProjectController extends AbstractController
{
User::auth();
//
parse_str(Request::getContent(), $data);
$data = Request::input();
$task_id = intval($data['task_id']);
//
$task = ProjectTask::userTask($task_id, true, true, 2);
@@ -1551,7 +1680,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/task/dialog 30. 创建/获取聊天室
* @api {get} api/project/task/dialog 31. 创建/获取聊天室
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1600,7 +1729,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/task/archived 31. 归档任务
* @api {get} api/project/task/archived 32. 归档任务
*
* @apiDescription 需要token身份项目、任务负责人
* @apiVersion 1.0.0
@@ -1642,7 +1771,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/task/remove 32. 删除任务
* @api {get} api/project/task/remove 33. 删除任务
*
* @apiDescription 需要token身份项目、任务负责人
* @apiVersion 1.0.0
@@ -1667,7 +1796,7 @@ class ProjectController extends AbstractController
//
$task = ProjectTask::userTask($task_id, null, $type !== 'recovery', true);
if ($type == 'recovery') {
$task->recoveryTask();
$task->restoreTask();
return Base::retSuccess('操作成功', ['id' => $task->id]);
} else {
$task->deleteTask();
@@ -1676,7 +1805,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/task/resetfromlog 33. 根据日志重置任务
* @api {get} api/project/task/resetfromlog 34. 根据日志重置任务
*
* @apiDescription 需要token身份项目、任务负责人
* @apiVersion 1.0.0
@@ -1735,7 +1864,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/task/flow 34. 任务工作流信息
* @api {get} api/project/task/flow 35. 任务工作流信息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1817,7 +1946,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/flow/list 35. 工作流列表
* @api {get} api/project/flow/list 36. 工作流列表
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1843,7 +1972,7 @@ class ProjectController extends AbstractController
}
/**
* @api {post} api/project/flow/save 36. 保存工作流
* @api {post} api/project/flow/save 37. 保存工作流
*
* @apiDescription 需要token身份项目负责人
* @apiVersion 1.0.0
@@ -1861,8 +1990,8 @@ class ProjectController extends AbstractController
{
User::auth();
//
$project_id = intval(Base::getContentValue('project_id'));
$flows = Base::getContentValue('flows');
$project_id = intval(Request::input('project_id'));
$flows = Request::input('flows');
//
if (!is_array($flows)) {
return Base::retError('参数错误');
@@ -1877,7 +2006,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/flow/delete 37. 删除工作流
* @api {get} api/project/flow/delete 38. 删除工作流
*
* @apiDescription 需要token身份项目负责人
* @apiVersion 1.0.0
@@ -1909,7 +2038,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/log/lists 38. 获取项目、任务日志
* @api {get} api/project/log/lists 39. 获取项目、任务日志
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1948,11 +2077,12 @@ class ProjectController extends AbstractController
if ($task_id === 0) {
$log->projectTask?->cancelAppend();
}
$log->detail = Doo::translate($log->detail);
$log->time = [
'ymd' => date(date("Y", $timestamp) == date("Y", Base::time()) ? "m-d" : "Y-m-d", $timestamp),
'hi' => date("h:i", $timestamp) ,
'week' => "" . Base::getTimeWeek($timestamp),
'segment' => Base::getTimeDayeSegment($timestamp),
'week' => Doo::translate("" . Base::getTimeWeek($timestamp)),
'segment' => Doo::translate(Base::getTimeDayeSegment($timestamp)),
];
return $log;
});
@@ -1961,7 +2091,7 @@ class ProjectController extends AbstractController
}
/**
* @api {get} api/project/top 39. 项目置顶
* @api {get} api/project/top 40. 项目置顶
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0

View File

@@ -92,7 +92,7 @@ class PublicController extends AbstractController
if ($key != $setting['key']) {
return 'key error';
}
$times = $setting['time'] ? Base::json2array($setting['time']) : ['00:00', '23:59'];
$times = $setting['time'] ? Base::json2array($setting['time']) : ['09:00', '18:00'];
$advance = (intval($setting['advance']) ?: 120) * 60;
$delay = (intval($setting['delay']) ?: 120) * 60;
//
@@ -129,20 +129,36 @@ class PublicController extends AbstractController
}
//
if ($checkins && $botUser = User::botGetOrCreate('check-in')) {
$sendMsg = function($type, UserCheckinMac $checkin) use ($botUser, $nowDate) {
$getJokeSoup = function($type) {
$pre = $type == "up" ? "每日开心:" : "心灵鸡汤:";
$key = $type == "up" ? "JokeSoupTask:jokes" : "JokeSoupTask:soups";
$array = Base::json2array(Cache::get($key));
if ($array) {
$item = $array[array_rand($array)];
if ($item) {
return $pre . $item;
}
}
return null;
};
$sendMsg = function($type, UserCheckinMac $checkin) use ($getJokeSoup, $botUser, $nowDate) {
$cacheKey = "Checkin::sendMsg-{$nowDate}-{$type}:" . $checkin->userid;
if (Cache::get($cacheKey) === "yes") {
return;
}
Cache::put($cacheKey, "yes", Carbon::now()->addDay());
//
$dialog = WebSocketDialog::checkUserDialog($botUser->userid, $checkin->userid);
$dialog = WebSocketDialog::checkUserDialog($botUser, $checkin->userid);
if ($dialog) {
$hi = date("H:i");
$pre = $type == "up" ? "上班" : "下班";
$remark = $checkin->remark ? " ({$checkin->remark})": "";
$text = "{$pre}打卡成功,打卡时间: {$hi} {$remark}";
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid);
$text = "<p>{$pre}打卡成功,打卡时间: {$hi}{$remark}</p>";
$suff = $getJokeSoup($type);
if ($suff) {
$text = "{$text}<p>----------</p><p>{$suff}</p>";
}
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, $type != "up");
}
};
if ($timeAdvance <= Base::time() && Base::time() < $timeEnd) {

View File

@@ -9,6 +9,7 @@ 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;
@@ -136,14 +137,14 @@ class ReportController extends AbstractController
$user = User::auth();
//
$input = [
"id" => Base::getPostValue("id", 0),
"sign" => Base::getPostValue("sign"),
"title" => Base::getPostValue("title"),
"type" => Base::getPostValue("type"),
"content" => Base::getPostValue("content"),
"receive" => Base::getPostValue("receive"),
"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" => Base::getPostValue("offset", 0),
"offset" => Request::input("offset", 0),
];
$validator = Validator::make($input, [
'id' => 'numeric',
@@ -306,7 +307,7 @@ class ReportController extends AbstractController
if ($complete_task->isNotEmpty()) {
foreach ($complete_task as $task) {
$complete_at = Carbon::parse($task->complete_at);
$pre = $type == Report::WEEKLY ? ('<span>[' . Base::Lang('周' . ['日', '一', '二', '三', '四', '五', '六'][$complete_at->dayOfWeek]) . ']</span>&nbsp;') : '';
$pre = $type == Report::WEEKLY ? ('<span>[' . Doo::translate('周' . ['日', '一', '二', '三', '四', '五', '六'][$complete_at->dayOfWeek]) . ']</span>&nbsp;') : '';
$completeContent .= "<li>{$pre}[{$task->project->name}] {$task->name}</li>";
}
} else {
@@ -327,7 +328,7 @@ class ReportController extends AbstractController
if ($unfinished_task->isNotEmpty()) {
foreach ($unfinished_task as $task) {
empty($task->end_at) || $end_at = Carbon::parse($task->end_at);
$pre = (!empty($end_at) && $end_at->lt($now_dt)) ? '<span style="color:#ff0000;">[' . Base::Lang('超期') . ']</span>&nbsp;' : '';
$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 {
@@ -340,14 +341,19 @@ class ReportController extends AbstractController
} 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" => '<h2>' . Base::Lang('已完成工作') . '</h2><ol>' .
$completeContent . '</ol><h2>' .
Base::Lang('未完成的工作') . '</h2><ol>' .
$unfinishedContent . '</ol>',
"content" => $content,
"complete_task" => $complete_task,
"unfinished_task" => $unfinished_task,
];
@@ -460,17 +466,14 @@ class ReportController extends AbstractController
* @apiGroup report
* @apiName unread
*
* @apiParam {Number} [userid] 用户id
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function unread(): array
{
$userid = intval(trim(Request::input("userid")));
$user = empty($userid) ? User::auth() : User::find($userid);
$user = User::auth();
//
$data = Report::whereHas("Receives", function (Builder $query) use ($user) {
$query->where("userid", $user->userid)->where("read", 0);
})->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20));

View File

@@ -8,10 +8,13 @@ use App\Models\UserCheckinRecord;
use App\Module\Base;
use App\Module\BillExport;
use App\Module\BillMultipleExport;
use Arr;
use App\Module\Doo;
use App\Module\Extranet;
use Carbon\Carbon;
use Guanguans\Notify\Factory;
use Guanguans\Notify\Messages\EmailMessage;
use LdapRecord\Container;
use LdapRecord\LdapRecordException;
use Madzipper;
use Request;
use Response;
@@ -35,7 +38,7 @@ class SystemController extends AbstractController
* @apiParam {String} type
* - get: 获取(默认)
* - all: 获取所有(需要管理员权限)
* - save: 保存设置(参数:['reg', 'reg_invite', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'auto_archived', 'archived_day', 'all_group_mute', 'all_group_autoin', 'start_home', 'home_footer']
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'auto_archived', 'archived_day', 'all_group_mute', 'all_group_autoin', 'image_compress', 'image_save_local', 'start_home', 'home_footer']
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -53,15 +56,19 @@ class SystemController extends AbstractController
foreach ($all AS $key => $value) {
if (!in_array($key, [
'reg',
'reg_identity',
'reg_invite',
'login_code',
'password_policy',
'project_invite',
'chat_information',
'anon_message',
'auto_archived',
'archived_day',
'all_group_mute',
'all_group_autoin',
'image_compress',
'image_save_local',
'start_home',
'home_footer'
])) {
@@ -89,14 +96,18 @@ class SystemController extends AbstractController
}
//
$setting['reg'] = $setting['reg'] ?: 'open';
$setting['reg_identity'] = $setting['reg_identity'] ?: 'normal';
$setting['login_code'] = $setting['login_code'] ?: 'auto';
$setting['password_policy'] = $setting['password_policy'] ?: 'simple';
$setting['project_invite'] = $setting['project_invite'] ?: 'open';
$setting['chat_information'] = $setting['chat_information'] ?: 'optional';
$setting['anon_message'] = $setting['anon_message'] ?: 'open';
$setting['auto_archived'] = $setting['auto_archived'] ?: 'close';
$setting['archived_day'] = floatval($setting['archived_day']) ?: 7;
$setting['all_group_mute'] = $setting['all_group_mute'] ?: 'open';
$setting['all_group_autoin'] = $setting['all_group_autoin'] ?: 'yes';
$setting['image_compress'] = $setting['image_compress'] ?: 'open';
$setting['image_save_local'] = $setting['image_save_local'] ?: 'open';
$setting['start_home'] = $setting['start_home'] ?: 'close';
//
return Base::retSuccess('success', $setting ?: json_decode('{}'));
@@ -197,18 +208,25 @@ class SystemController extends AbstractController
unset($all[$key]);
}
}
if ($all['open'] === 'open' && (!$all['appid'] || !$all['app_certificate'])) {
return Base::retError('请填写完整的参数');
}
$setting = Base::setting('meetingSetting', Base::newTrim($all));
} else {
$setting = Base::setting('meetingSetting');
}
//
$setting['open'] = $setting['open'] ?: 'close';
if (env("SYSTEM_SETTING") == 'disabled') {
$setting['appid'] = substr($setting['appid'], 0, 4) . str_repeat('*', strlen($setting['appid']) - 8) . substr($setting['appid'], -4);
$setting['app_certificate'] = substr($setting['app_certificate'], 0, 4) . str_repeat('*', strlen($setting['app_certificate']) - 8) . substr($setting['app_certificate'], -4);
}
//
return Base::retSuccess('success', $setting ?: json_decode('{}'));
}
/**
* @api {get} api/system/setting/checkin 03. 获取签到设置、保存签到设置(限管理员)
* @api {get} api/system/setting/checkin 04. 获取签到设置、保存签到设置(限管理员)
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -216,7 +234,7 @@ class SystemController extends AbstractController
*
* @apiParam {String} type
* - get: 获取(默认)
* - save: 保存设置(参数:['open', 'time', 'advance', 'delay', 'edit', 'key']
* - save: 保存设置(参数:['open', 'time', 'advance', 'delay', 'remindin', 'remindexceed', 'edit', 'key']
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
@@ -237,6 +255,8 @@ class SystemController extends AbstractController
'time',
'advance',
'delay',
'remindin',
'remindexceed',
'edit',
'key',
])) {
@@ -257,9 +277,11 @@ class SystemController extends AbstractController
}
//
$setting['open'] = $setting['open'] ?: 'close';
$setting['time'] = $setting['time'] ? Base::json2array($setting['time']) : ['00:00', '23:59'];
$setting['time'] = $setting['time'] ? Base::json2array($setting['time']) : ['09:00', '18:00'];
$setting['advance'] = intval($setting['advance']) ?: 120;
$setting['delay'] = intval($setting['delay']) ?: 120;
$setting['remindin'] = intval($setting['remindin']) ?: 5;
$setting['remindexceed'] = intval($setting['remindexceed']) ?: 10;
$setting['edit'] = $setting['edit'] ?: 'close';
$setting['cmd'] = "curl -sSL '" . Base::fillUrl("api/public/checkin/install?key={$setting['key']}") . "' | sh";
//
@@ -267,7 +289,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/setting/apppush 04. 获取APP推送设置、保存APP推送设置限管理员
* @api {get} api/system/setting/apppush 05. 获取APP推送设置、保存APP推送设置限管理员
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -312,7 +334,77 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/demo 05. 获取演示帐号
* @api {get} api/system/setting/thirdaccess 06. 第三方帐号(限管理员)
*
* @apiVersion 1.0.0
* @apiGroup system
* @apiName setting__thirdaccess
*
* @apiParam {String} type
* - get: 获取(默认)
* - save: 保存设置(参数:['ldap_open', 'ldap_host', 'ldap_port', 'ldap_password', 'ldap_user_dn', 'ldap_base_dn', 'ldap_sync_local']
* - testldap: 测试ldap连接
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function setting__thirdaccess()
{
User::auth('admin');
//
$type = trim(Request::input('type'));
if ($type == 'testldap') {
$all = Base::newTrim(Request::input());
$connection = Container::getDefaultConnection();
try {
$connection->setConfiguration([
"hosts" => [$all['ldap_host']],
"port" => intval($all['ldap_port']),
"password" => $all['ldap_password'],
"username" => $all['ldap_user_dn'],
"base_dn" => $all['ldap_base_dn'],
]);
if ($connection->auth()->attempt($all['ldap_user_dn'], $all['ldap_password'])) {
return Base::retSuccess('验证通过');
} else {
return Base::retError('验证失败');
}
} catch (LdapRecordException $e) {
return Base::retError($e->getMessage() ?: "验证失败:未知错误", config("ldap.connections.default"));
}
} elseif ($type == 'save') {
if (env("SYSTEM_SETTING") == 'disabled') {
return Base::retError('当前环境禁止修改');
}
$all = Base::newTrim(Request::input());
foreach ($all as $key => $value) {
if (!in_array($key, [
'ldap_open',
'ldap_host',
'ldap_port',
'ldap_password',
'ldap_user_dn',
'ldap_base_dn',
'ldap_sync_local'
])) {
unset($all[$key]);
}
}
$all['ldap_port'] = intval($all['ldap_port']) ?: 389;
$setting = Base::setting('thirdAccessSetting', Base::newTrim($all));
} else {
$setting = Base::setting('thirdAccessSetting');
}
//
$setting['ldap_open'] = $setting['ldap_open'] ?: 'close';
$setting['ldap_port'] = intval($setting['ldap_port']) ?: 389;
$setting['ldap_sync_local'] = $setting['ldap_sync_local'] ?: 'close';
//
return Base::retSuccess('success', $setting ?: json_decode('{}'));
}
/**
* @api {get} api/system/demo 07. 获取演示帐号
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -336,7 +428,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/priority 06. 任务优先级
* @api {post} api/system/priority 08. 任务优先级
*
* @apiDescription 获取任务优先级、保存任务优先级
* @apiVersion 1.0.0
@@ -357,7 +449,7 @@ class SystemController extends AbstractController
$type = trim(Request::input('type'));
if ($type == 'save') {
User::auth('admin');
$list = Base::getPostValue('list');
$list = Request::input('list');
$array = [];
if (empty($list) || !is_array($list)) {
return Base::retError('参数错误');
@@ -385,7 +477,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/column/template 07. 创建项目模板
* @api {post} api/system/column/template 09. 创建项目模板
*
* @apiDescription 获取创建项目模板、保存创建项目模板
* @apiVersion 1.0.0
@@ -406,7 +498,7 @@ class SystemController extends AbstractController
$type = trim(Request::input('type'));
if ($type == 'save') {
User::auth('admin');
$list = Base::getPostValue('list');
$list = Request::input('list');
$array = [];
if (empty($list) || !is_array($list)) {
return Base::retError('参数错误');
@@ -432,7 +524,70 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/info 08. 获取终端详细信息
* @api {post} api/system/license 10. License
*
* @apiDescription 获取License信息、保存License限管理员
* @apiVersion 1.0.0
* @apiGroup system
* @apiName license
*
* @apiParam {String} type
* - get: 获取
* - save: 保存
* @apiParam {String} license License 原文
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function license()
{
User::auth('admin');
//
$type = trim(Request::input('type'));
if ($type == 'save') {
$license = Request::input('license');
Doo::licenseSave($license);
}
//
$data = [
'license' => Doo::licenseContent(),
'info' => Doo::license(),
'macs' => Doo::macs(),
'doo_sn' => Doo::dooSN(),
'user_count' => User::whereBot(0)->whereNull('disable_at')->count(),
'error' => []
];
if ($data['info']['people'] > 3) {
// 小于3人的License不检查
if ($data['info']['sn'] != $data['doo_sn']) {
$data['error'][] = '终端SN与License不匹配';
}
if ($data['info']['mac']) {
$approved = false;
foreach ($data['info']['mac'] as $mac) {
if (in_array($mac, $data['macs'])) {
$approved = true;
break;
}
}
if (!$approved) {
$data['error'][] = '终端MAC与License不匹配';
}
}
}
if ($data['info']['people'] > 0 && $data['user_count'] > $data['info']['people']) {
$data['error'][] = '终端用户数超过License限制';
}
if ($data['info']['expired_at'] && strtotime($data['info']['expired_at']) <= Base::time()) {
$data['error'][] = '终端License已过期';
}
//
return Base::retSuccess('success', $data);
}
/**
* @api {get} api/system/get/info 11. 获取终端详细信息
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -451,17 +606,17 @@ class SystemController extends AbstractController
}
return Base::retSuccess('success', [
'ip' => Base::getIp(),
'ip-info' => Base::getIpInfo(Base::getIp()),
'ip-gcj02' => Base::getIpGcj02(Base::getIp()),
'ip-info' => Extranet::getIpInfo(Base::getIp()),
'ip-gcj02' => Extranet::getIpGcj02(Base::getIp()),
'ip-iscn' => Base::isCnIp(Base::getIp()),
'header' => Request::header(),
'token' => Base::getToken(),
'token' => Doo::userToken(),
'url' => url('') . Base::getUrl(),
]);
}
/**
* @api {get} api/system/get/ip 09. 获取IP地址
* @api {get} api/system/get/ip 12. 获取IP地址
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -476,7 +631,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/cnip 10. 是否中国IP地址
* @api {get} api/system/get/cnip 13. 是否中国IP地址
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -493,7 +648,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/ipgcj02 11. 获取IP地址经纬度
* @api {get} api/system/get/ipgcj02 14. 获取IP地址经纬度
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -506,11 +661,11 @@ class SystemController extends AbstractController
* @apiSuccess {Object} data 返回数据
*/
public function get__ipgcj02() {
return Base::getIpGcj02(Request::input("ip"));
return Extranet::getIpGcj02(Request::input("ip"));
}
/**
* @api {get} api/system/get/ipinfo 12. 获取IP地址详细信息
* @api {get} api/system/get/ipinfo 15. 获取IP地址详细信息
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -523,11 +678,11 @@ class SystemController extends AbstractController
* @apiSuccess {Object} data 返回数据
*/
public function get__ipinfo() {
return Base::getIpInfo(Request::input("ip"));
return Extranet::getIpInfo(Request::input("ip"));
}
/**
* @api {post} api/system/imgupload 13. 上传图片
* @api {post} api/system/imgupload 16. 上传图片
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -561,8 +716,8 @@ class SystemController extends AbstractController
$scale = [$width, $height, $whcut];
}
$path = "uploads/user/picture/" . User::userid() . "/" . date("Ym") . "/";
$image64 = trim(Base::getPostValue('image64'));
$fileName = trim(Base::getPostValue('filename'));
$image64 = trim(Request::input('image64'));
$fileName = trim(Request::input('filename'));
if ($image64) {
$data = Base::image64save([
"image64" => $image64,
@@ -587,7 +742,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/imgview 14. 浏览图片空间
* @api {get} api/system/get/imgview 17. 浏览图片空间
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -642,7 +797,7 @@ class SystemController extends AbstractController
'path' => $pathTemp,
'url' => Base::fillUrl($pathTemp),
'thumb' => Base::fillUrl('images/other/dir.png'),
'inode' => fileatime($v),
'inode' => filemtime($v),
];
} elseif (!str_ends_with($filename, "_thumb.jpg")) {
$array = [
@@ -651,11 +806,11 @@ class SystemController extends AbstractController
'path' => $pathTemp,
'url' => Base::fillUrl($pathTemp),
'thumb' => $pathTemp,
'inode' => fileatime($v),
'inode' => filemtime($v),
];
//
$extension = pathinfo($dirPath . $filename, PATHINFO_EXTENSION);
if (in_array($extension, array('gif', 'jpg', 'jpeg', 'png', 'bmp'))) {
if (in_array($extension, array('gif', 'jpg', 'jpeg', 'webp', 'png', 'bmp'))) {
if (file_exists($dirPath . $filename . '_thumb.jpg')) {
$array['thumb'] .= '_thumb.jpg';
}
@@ -683,7 +838,7 @@ class SystemController extends AbstractController
}
/**
* @api {post} api/system/fileupload 15. 上传文件
* @api {post} api/system/fileupload 18. 上传文件
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -704,8 +859,8 @@ class SystemController extends AbstractController
return Base::retError('身份失效,等重新登录');
}
$path = "uploads/user/file/" . User::userid() . "/" . date("Ym") . "/";
$image64 = trim(Base::getPostValue('image64'));
$fileName = trim(Base::getPostValue('filename'));
$image64 = trim(Request::input('image64'));
$fileName = trim(Request::input('filename'));
if ($image64) {
$data = Base::image64save([
"image64" => $image64,
@@ -725,7 +880,39 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/get/starthome 16. 启动首页设置信息
* @api {get} api/system/get/showitem 19. 首页显示ITEM
*
* @apiDescription 用于判断首页是否显示pro、github、更新日志...
* @apiVersion 1.0.0
* @apiGroup system
* @apiName get__showitem
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function get__showitem()
{
$logPath = base_path('CHANGELOG.md');
$logContent = "";
$logVersion = "";
if (file_exists($logPath)) {
$logContent = file_get_contents($logPath);
preg_match("/## \[(.*?)\]/", $logContent, $matchs);
if ($matchs) {
$logVersion = $matchs[1] === "Unreleased" ? $matchs[1] : "v{$matchs[1]}";
}
}
return Base::retSuccess('success', [
'pro' => str_contains(Request::getHost(), "dootask.com") || str_contains(Request::getHost(), "127.0.0.1"),
'github' => env('GITHUB_URL') ?: false,
'updateLog' => $logContent ?: false,
'updateVer' => $logVersion,
]);
}
/**
* @api {get} api/system/get/starthome 20. 启动首页设置信息
*
* @apiDescription 用于判断注册是否需要启动首页
* @apiVersion 1.0.0
@@ -745,7 +932,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/email/check 17. 邮件发送测试(限管理员)
* @api {get} api/system/email/check 21. 邮件发送测试(限管理员)
*
* @apiDescription 测试配置邮箱是否能发送邮件
* @apiVersion 1.0.0
@@ -791,7 +978,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/checkin/export 17. 导出签到数据(限管理员)
* @api {get} api/system/checkin/export 22. 导出签到数据(限管理员)
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -850,7 +1037,7 @@ class SystemController extends AbstractController
$sheets = [];
$startD = Carbon::parse($date[0])->startOfDay();
$endD = Carbon::parse($date[1])->endOfDay();
$users = User::whereIn('userid', $userid)->take(20)->get();
$users = User::whereIn('userid', $userid)->take(100)->get();
/** @var User $user */
foreach ($users as $user) {
$recordTimes = UserCheckinRecord::getTimes($user->userid, [$startD, $endD]);
@@ -914,7 +1101,8 @@ class SystemController extends AbstractController
];
$startT += 86400;
}
$sheets[] = BillExport::create()->setTitle($nickname ?: $user->userid)->setHeadings($headings)->setData($datas)->setStyles($styles);
$title = (count($sheets) + 1) . "." . ($nickname ?: $user->userid);
$sheets[] = BillExport::create()->setTitle($title)->setHeadings($headings)->setData($datas)->setStyles($styles);
}
if (empty($sheets)) {
return Base::retError('没有任何数据');
@@ -957,7 +1145,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/checkin/down 17. 下载导出的签到数据
* @api {get} api/system/checkin/down 23. 下载导出的签到数据
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -983,7 +1171,7 @@ class SystemController extends AbstractController
}
/**
* @api {get} api/system/version 18. 获取版本号
* @api {get} api/system/version 24. 获取版本号
*
* @apiVersion 1.0.0
* @apiGroup system
@@ -998,14 +1186,16 @@ class SystemController extends AbstractController
$package = Base::getPackage();
$array = [
'version' => Base::getVersion(),
'publish' => Arr::get($package, 'app.0.publish'),
'publish' => [],
];
if (is_array($package['app'])) {
$i = 0;
foreach ($package['app'] as $item) {
$urls = $item['urls'] && is_array($item['urls']) ? $item['urls'] : $item['url'];
if (is_array($item['publish']) && Base::hostContrast($url, $urls)) {
if (is_array($item['publish']) && ($i === 0 || Base::hostContrast($url, $urls))) {
$array['publish'] = $item['publish'];
}
$i++;
}
}
return $array;

View File

@@ -2,21 +2,25 @@
namespace App\Http\Controllers\Api;
use App\Ldap\LdapUser;
use App\Models\AbstractModel;
use App\Models\Meeting;
use App\Models\Project;
use App\Models\UmengAlias;
use App\Models\User;
use App\Models\UserBot;
use App\Models\UserCheckinMac;
use App\Models\UserCheckinRecord;
use App\Models\UserDelete;
use App\Models\UserDepartment;
use App\Models\UserEmailVerification;
use App\Models\UserTransfer;
use App\Models\WebSocket;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Module\AgoraIO\AgoraTokenGenerator;
use App\Module\Base;
use App\Module\Doo;
use Arr;
use Cache;
use Captcha;
@@ -44,6 +48,7 @@ class UsersController extends AbstractController
* @apiParam {String} email 邮箱
* @apiParam {String} password 密码
* @apiParam {String} [code] 登录验证码
* @apiParam {String} [code_key] 验证码通过key验证
* @apiParam {String} [invite] 注册邀请码
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
@@ -75,10 +80,16 @@ class UsersController extends AbstractController
$needCode = !Base::isError(User::needCode($email));
if ($needCode) {
$code = trim(Request::input('code'));
$codeKey = trim(Request::input('code_key'));
if (empty($code)) {
return Base::retError('请输入验证码', ['code' => 'need']);
}
if (!Captcha::check($code)) {
if ($codeKey) {
$check = Captcha::check_api($code, $codeKey);
} else {
$check = Captcha::check($code);
}
if (!$check) {
return Base::retError('请输入正确的验证码', ['code' => 'need']);
}
}
@@ -89,11 +100,23 @@ class UsersController extends AbstractController
$needData = ['code' => $needCode ? 'need' : 'no'];
return Base::retError($msg, $needData);
};
//
$user = User::whereEmail($email)->first();
$usePassword = true;
if (LdapUser::isOpen()) {
if (empty($user) || $user->isLdap()) {
$user = LdapUser::userLogin($email, $password, $user);
if ($user) {
$user->identity = Base::arrayImplode(array_merge(array_diff($user->identity, ['ldap']), ['ldap']));
$user->save();
}
$usePassword = false;
}
}
if (empty($user)) {
return $retError('帐号或密码错误');
}
if ($user->password != Base::md52($password, $user->encrypt)) {
if ($usePassword && $user->password != Doo::md5s($password, $user->encrypt)) {
return $retError('帐号或密码错误');
}
//
@@ -116,12 +139,13 @@ class UsersController extends AbstractController
];
$user->updateInstance($array);
$user->save();
User::token($user);
User::generateToken($user);
LdapUser::userSync($user, $password);
//
if (!Project::withTrashed()->whereUserid($user->userid)->wherePersonal(1)->exists()) {
Project::createProject([
'name' => Base::Lang('个人项目'),
'desc' => Base::Lang('注册时系统自动创建项目,你可以自由删除。'),
'name' => Doo::translate('个人项目'),
'desc' => Doo::translate('注册时系统自动创建项目,你可以自由删除。'),
'personal' => 1,
], $user->userid);
}
@@ -130,7 +154,58 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/login/needcode 02. 是否需要验证码
* @api {get} api/users/login/qrcode 02. 二维码登录
*
* @apiDescription 通过二维码code登录 (或:是否登录成功)
* @apiVersion 1.0.0
* @apiGroup users
* @apiName login__qrcode
*
* @apiParam {String} type 类型
* - login: 登录用于app登录
* - status: 状态 (默认,用于:网页、客户端获取)
* @apiParam {String} code 二维码 code
*
* @apiSuccess {Number} ret 返回状态码1需要、0不需要
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function login__qrcode()
{
$type = trim(Request::input('type'));
$code = trim(Request::input('code'));
$key = "User::qrcode:" . $code;
//
if (strlen($code) < 32) {
return Base::retError("参数错误");
}
//
if ($type === 'login') {
$user = User::auth();
Cache::put($key, $user->userid, Carbon::now()->addSeconds(30));
return Base::retSuccess("扫码成功");
}
//
$userid = intval(Cache::get($key));
if ($userid > 0 && $user = User::whereUserid($userid)->first()) {
$array = [
'login_num' => $user->login_num + 1,
'last_ip' => Base::getIp(),
'last_at' => Carbon::now(),
'line_ip' => Base::getIp(),
'line_at' => Carbon::now(),
];
$user->updateInstance($array);
$user->save();
User::generateToken($user);
return Base::retSuccess("success", $user);
}
//
return Base::retError("No identity");
}
/**
* @api {get} api/users/login/needcode 03. 是否需要验证码
*
* @apiDescription 用于判断是否需要登录验证码
* @apiVersion 1.0.0
@@ -149,7 +224,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/login/codeimg 03. 验证码图片
* @api {get} api/users/login/codeimg 04. 验证码图片
*
* @apiDescription 用于判断是否需要登录验证码
* @apiVersion 1.0.0
@@ -164,7 +239,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/login/codejson 04. 验证码json
* @api {get} api/users/login/codejson 05. 验证码json
*
* @apiDescription 用于判断是否需要登录验证码
* @apiVersion 1.0.0
@@ -182,7 +257,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/reg/needinvite 05. 是否需要邀请码
* @api {get} api/users/reg/needinvite 06. 是否需要邀请码
*
* @apiDescription 用于判断注册是否需要邀请码
* @apiVersion 1.0.0
@@ -201,7 +276,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/info 06. 获取我的信息
* @api {get} api/users/info 07. 获取我的信息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -232,7 +307,16 @@ class UsersController extends AbstractController
public function info()
{
$user = User::auth();
User::token($user);
//
$refreshToken = false;
if (in_array(Base::platform(), ['ios', 'android'])) {
// 移动端token还剩7天到期时获取新的token
$expiredAt = Doo::userExpiredAt();
if ($expiredAt && Carbon::parse($expiredAt)->isBefore(Carbon::now()->addDays(7))) {
$refreshToken = true;
}
}
User::generateToken($user, $refreshToken);
//
$data = $user->toArray();
$data['nickname_original'] = $user->getRawOriginal('nickname');
@@ -241,7 +325,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/editdata 07. 修改自己的资料
* @api {get} api/users/editdata 08. 修改自己的资料
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -260,14 +344,20 @@ class UsersController extends AbstractController
public function editdata()
{
$user = User::auth();
//
$data = Request::all();
$user->checkSystem(1);
$upLdap = [];
// 头像
if (Arr::exists($data, 'userimg')) {
$userimg = Request::input('userimg');
$user->userimg = $userimg ? Base::unFillUrl(is_array($userimg) ? $userimg[0]['path'] : $userimg) : '';
if (str_contains($user->userimg, 'avatar/')) {
$user->userimg = '';
$userimg = $userimg ? Base::unFillUrl(is_array($userimg) ? $userimg[0]['path'] : $userimg) : '';
if (str_contains($userimg, 'avatar/')) {
$userimg = '';
}
$user->userimg = $userimg;
if (file_exists(public_path($userimg))) {
$upLdap['jpegPhoto'] = file_get_contents(public_path($userimg));
}
}
// 电话
@@ -280,6 +370,7 @@ class UsersController extends AbstractController
return Base::retError('联系电话已存在');
}
$user->tel = $tel;
$upLdap['mobile'] = $tel;
}
// 昵称
if (Arr::exists($data, 'nickname')) {
@@ -292,6 +383,7 @@ class UsersController extends AbstractController
$user->nickname = $nickname;
$user->az = Base::getFirstCharter($nickname);
$user->pinyin = Base::cn2pinyin($nickname);
$upLdap['displayName'] = $nickname;
}
}
// 职位/职称
@@ -303,20 +395,19 @@ class UsersController extends AbstractController
return Base::retError('职位/职称最多只能设置20个字');
} else {
$user->profession = $profession;
$upLdap['employeeType'] = $profession;
}
}
//
$user->save();
User::token($user);
User::generateToken($user);
LdapUser::userUpdate($user->email, $upLdap);
//
if (empty($user->userimg)) {
$user->userimg = $user->getUserimgAttribute(null);
}
return Base::retSuccess('修改成功', $user);
}
/**
* @api {get} api/users/editpass 08. 修改自己的密码
* @api {get} api/users/editpass 09. 修改自己的密码
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -342,21 +433,22 @@ class UsersController extends AbstractController
}
User::passwordPolicy($newpass);
//
$verify = User::whereUserid($user->userid)->wherePassword(Base::md52($oldpass, User::token2encrypt()))->count();
$verify = User::whereUserid($user->userid)->wherePassword(Doo::md5s($oldpass, Doo::userEncrypt()))->count();
if (empty($verify)) {
return Base::retError('请填写正确的旧密码');
}
//
$user->encrypt = Base::generatePassword(6);
$user->password = Base::md52($newpass, $user->encrypt);
$user->password = Doo::md5s($newpass, $user->encrypt);
$user->changepass = 0;
$user->save();
User::token($user);
User::generateToken($user);
LdapUser::userUpdate($user->email, ['userPassword' => $newpass]);
return Base::retSuccess('修改成功', $user);
}
/**
* @api {get} api/users/search 09. 搜索会员列表
* @api {get} api/users/search 10. 搜索会员列表
*
* @apiDescription 搜索会员列表
* @apiVersion 1.0.0
@@ -364,8 +456,8 @@ class UsersController extends AbstractController
* @apiName searchinfo
*
* @apiParam {Object} keys 搜索条件
* - keys.key 昵称、邮箱关键字
* - keys.disable 0-排除禁止默认1-仅禁止2-含禁止
* - keys.key 昵称、拼音、邮箱关键字
* - keys.disable 0-排除离职默认1-仅离职2-含离职
* - keys.bot 0-排除机器人默认1-仅机器人2-含机器人
* - keys.project_id 在指定项目ID
* - keys.no_project_id 不在指定项目ID
@@ -387,7 +479,12 @@ class UsersController extends AbstractController
*/
public function search()
{
$builder = User::select(User::$basicField);
$user = User::auth();
//
$columns = User::$basicField;
$columns[] = 'created_at';
$columns[] = 'identity';
$builder = User::select($columns);
//
$keys = Request::input('keys');
$sorts = Request::input('sorts');
@@ -397,10 +494,14 @@ class UsersController extends AbstractController
$sorts = is_array($sorts) ? $sorts : [];
//
if ($keys['key']) {
$builder->where(function($query) use ($keys) {
$query->where("email", "like", "%{$keys['key']}%")
->orWhere("nickname", "like", "%{$keys['key']}%");
});
if (str_contains($keys['key'], "@")) {
$builder->where("email", "like", "%{$keys['key']}%");
} else {
$builder->where(function($query) use ($keys) {
$query->where("nickname", "like", "%{$keys['key']}%")
->orWhere("pinyin", "like", "%{$keys['key']}%");
});
}
}
if (intval($keys['disable']) == 0) {
$builder->whereNull("disable_at");
@@ -432,6 +533,13 @@ class UsersController extends AbstractController
}
if (in_array($sorts['az'], ['asc', 'desc'])) {
$builder->orderBy('az', $sorts['az']);
} else {
if (intval($keys['disable']) == 2) {
$builder->orderBy('disable_at');
}
if (intval($keys['bot']) == 2) {
$builder->orderBy('bot');
}
}
//
if (Request::exists('page')) {
@@ -440,17 +548,38 @@ class UsersController extends AbstractController
$list = $builder->orderBy('userid')->take(Base::getPaginate(100, 10, 'take'))->get();
}
//
if ($state === 1) {
$list->transform(function (User $userInfo) {
$userInfo->online = $userInfo->getOnlineStatus();
return $userInfo;
$list->transform(function (User $userInfo) use ($user, $state) {
$tags = [];
$dep = $userInfo->getDepartmentName();
$dep = array_filter(explode(",", $dep), function($item) {
return preg_match("/\(M\)$/", $item);
});
}
if ($dep) {
$tags[] = preg_replace("/\(M\)$/", "", trim($dep[0])) . Doo::translate("负责人");
}
if ($user->isAdmin()) {
if ($userInfo->isAdmin()) {
$tags[] = Doo::translate("系统管理员");
}
if ($userInfo->isTemp()) {
$tags[] = Doo::translate("临时帐号");
}
if ($userInfo->userid > 3 && Carbon::parse($userInfo->created_at)->isAfter(Carbon::now()->subDays(30))) {
$tags[] = Doo::translate("新帐号");
}
}
$userInfo->tags = $tags;
//
if ($state === 1) {
$userInfo->online = $userInfo->getOnlineStatus();
}
return $userInfo;
});
return Base::retSuccess('success', $list);
}
/**
* @api {get} api/users/basic 10. 获取指定会员基础信息
* @api {get} api/users/basic 11. 获取指定会员基础信息
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -489,7 +618,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/lists 11. 会员列表(限管理员)
* @api {get} api/users/lists 12. 会员列表(限管理员)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -590,7 +719,16 @@ class UsersController extends AbstractController
$query->where("department", "")->orWhere("department", ",,");
});
} else {
$builder->where("department", "like", "%,{$keys['department']},%");
// 关联user_departments表中owner_userid查询出负责人重新排序部门负责人始终在前面
$builder->where(function($query) use ($keys) {
$query->where("department", "like", "%,{$keys['department']},%");
$query->orWhereIn('userid', function ($query) use ($keys) {
$query->select('owner_userid')->from('user_departments')->where("id", "=", trim($keys['department'], ','));
});
});
$prefix = \DB::getTablePrefix();
$builder->selectRaw("if(EXISTS(select id from {$prefix}user_departments where owner_userid = userid and id={$keys['department']}),1,0) as is_principal");
$builder->orderBy("is_principal","desc");
}
}
if ($getCheckinMac && isset($keys['checkin_mac'])) {
@@ -602,11 +740,14 @@ class UsersController extends AbstractController
$builder->whereNull('disable_at');
$builder->where('bot', 0);
}
$list = $builder->orderByDesc('userid')->paginate(Base::getPaginate(50, 20));
$builder = $keys['department'] == '0' ? $builder->orderByDesc('userid') : $builder;
$list = $builder->paginate(Base::getPaginate(50, 20));
//
if ($getCheckinMac) {
$list->transform(function (User $user) {
$user->checkin_macs = UserCheckinMac::select(['id', 'mac', 'remark'])->whereUserid($user->userid)->orderBy('id')->get();
$list->transform(function (User $user) use ($getCheckinMac) {
if($getCheckinMac){
$user->checkin_macs = UserCheckinMac::select(['id', 'mac', 'remark'])->whereUserid($user->userid)->orderBy('id')->get();
}
return $user;
});
}
@@ -615,7 +756,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/operation 12. 操作会员(限管理员)
* @api {get} api/users/operation 13. 操作会员(限管理员)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -626,6 +767,8 @@ class UsersController extends AbstractController
* @apiParam {String} [type] 操作
* - setadmin 设为管理员
* - clearadmin 取消管理员
* - settemp 设为临时帐号
* - cleartemp 取消临时身份(取消临时帐号)
* - checkin_macs 修改自动签到mac地址需要参数 checkin_macs
* - department 修改部门(需要参数 department
* - setdisable 设为离职(需要参数 disable_time、transfer_userid
@@ -661,6 +804,7 @@ class UsersController extends AbstractController
$userInfo->checkSystem(1);
//
$upArray = [];
$upLdap = [];
$transferUser = null;
switch ($type) {
case 'setadmin':
@@ -672,6 +816,15 @@ class UsersController extends AbstractController
$upArray['identity'] = array_diff($userInfo->identity, ['admin']);
break;
case 'settemp':
$upArray['identity'] = array_diff($userInfo->identity, ['temp']);
$upArray['identity'][] = 'temp';
break;
case 'cleartemp':
$upArray['identity'] = array_diff($userInfo->identity, ['temp']);
break;
case 'checkin_macs':
$list = is_array($data['checkin_macs']) ? $data['checkin_macs'] : [];
$array = [];
@@ -737,10 +890,10 @@ class UsersController extends AbstractController
break;
}
if (isset($upArray['identity'])) {
$upArray['identity'] = "," . implode(",", $upArray['identity']) . ",";
$upArray['identity'] = Base::arrayImplode($upArray['identity']);
}
if (isset($upArray['department'])) {
$upArray['department'] = "," . implode(",", $upArray['department']) . ",";
$upArray['department'] = Base::arrayImplode($upArray['department']);
}
// 邮箱
if (Arr::exists($data, 'email')) {
@@ -748,6 +901,9 @@ class UsersController extends AbstractController
if (User::whereEmail($email)->where('userid', '!=', $userInfo->userid)->exists()) {
return Base::retError('邮箱地址已存在');
}
if ($userInfo->isLdap()) {
return Base::retError('LDAP 用户禁止修改邮箱');
}
$upArray['email'] = $email;
}
// 电话
@@ -757,14 +913,16 @@ class UsersController extends AbstractController
return Base::retError('联系电话已存在');
}
$upArray['tel'] = $tel;
$upLdap['mobile'] = $tel;
}
// 密码
if (Arr::exists($data, 'password')) {
$password = trim($data['password']);
User::passwordPolicy($password);
$upArray['encrypt'] = Base::generatePassword(6);
$upArray['password'] = Base::md52($password, $upArray['encrypt']);
$upArray['password'] = Doo::md5s($password, $upArray['encrypt']);
$upArray['changepass'] = 1;
$upLdap['userPassword'] = $password;
}
// 昵称
if (Arr::exists($data, 'nickname')) {
@@ -777,6 +935,7 @@ class UsersController extends AbstractController
$upArray['nickname'] = $nickname;
$upArray['az'] = Base::getFirstCharter($nickname);
$upArray['pinyin'] = Base::cn2pinyin($nickname);
$upLdap['displayName'] = $nickname;
}
}
// 职位/职称
@@ -788,14 +947,16 @@ class UsersController extends AbstractController
return Base::retError('职位/职称最多只能设置20个字');
} else {
$upArray['profession'] = $profession;
$upLdap['employeeType'] = $profession;
}
}
if ($upArray) {
AbstractModel::transaction(function() use ($user, $type, $upArray, $userInfo, $transferUser) {
AbstractModel::transaction(function() use ($upLdap, $user, $type, $upArray, $userInfo, $transferUser) {
$exitIds = array_diff($userInfo->department, Base::explodeInt($upArray['department']));
$joinIds = array_diff(Base::explodeInt($upArray['department']), $userInfo->department);
$userInfo->updateInstance($upArray);
$userInfo->save();
LdapUser::userUpdate($userInfo->email, $upLdap);
if ($type === 'department') {
$userids = [$userInfo->userid];
// 退出群组
@@ -835,7 +996,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/email/verification 13. 邮箱验证
* @api {get} api/users/email/verification 14. 邮箱验证
*
* @apiDescription 不需要token身份
* @apiVersion 1.0.0
@@ -883,7 +1044,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/umeng/alias 14. 设置友盟别名
* @api {get} api/users/umeng/alias 15. 设置友盟别名
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -905,12 +1066,7 @@ class UsersController extends AbstractController
'alias.between:2,20' => '别名的长度在2-20个字符',
]);
//
$agent = strtolower(Request::server('HTTP_USER_AGENT'));
if (str_contains($agent, 'android')) {
$platform = 'android';
} elseif (str_contains($agent, 'iphone') || str_contains($agent, 'ipad')) {
$platform = 'ios';
} else {
if (!in_array(Base::platform(), ['ios', 'android'])) {
return Base::retError('设备类型错误');
}
//
@@ -918,7 +1074,7 @@ class UsersController extends AbstractController
$inArray = [
'userid' => $user->userid,
'alias' => $data['alias'],
'platform' => $platform,
'platform' => Base::platform(),
];
$row = UmengAlias::where($inArray);
if ($row->exists()) {
@@ -934,7 +1090,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/meeting/open 15. 【会议】创建会议、加入会议
* @api {get} api/users/meeting/open 16. 【会议】创建会议、加入会议
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1008,7 +1164,7 @@ class UsersController extends AbstractController
if (!User::whereUserid($userid)->exists()) {
continue;
}
$dialog = WebSocketDialog::checkUserDialog($user->userid, $userid);
$dialog = WebSocketDialog::checkUserDialog($user, $userid);
if ($dialog) {
$res = WebSocketDialogMsg::sendMsg(null, $dialog->id, 'meeting', $data, $user->userid);
if (Base::isSuccess($res)) {
@@ -1026,7 +1182,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/meeting/invitation 16. 【会议】发送邀请
* @api {get} api/users/meeting/invitation 17. 【会议】发送邀请
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1058,7 +1214,7 @@ class UsersController extends AbstractController
if (!User::whereUserid($userid)->exists()) {
continue;
}
$dialog = WebSocketDialog::checkUserDialog($user->userid, $userid);
$dialog = WebSocketDialog::checkUserDialog($user, $userid);
if ($dialog) {
$res = WebSocketDialogMsg::sendMsg(null, $dialog->id, 'meeting', $data, $user->userid);
if (Base::isSuccess($res)) {
@@ -1072,7 +1228,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/email/send 17. 发送邮箱验证码
* @api {get} api/users/email/send 18. 发送邮箱验证码
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1112,7 +1268,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/email/edit 18. 修改邮箱
* @api {get} api/users/email/edit 19. 修改邮箱
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1132,6 +1288,10 @@ class UsersController extends AbstractController
//
$user->checkSystem();
//
if ($user->isLdap()) {
return Base::retError('LDAP 用户禁止修改邮箱');
}
//
$newEmail = trim(Request::input('newEmail'));
$code = trim(Request::input('code'));
if (!$newEmail) {
@@ -1148,12 +1308,12 @@ class UsersController extends AbstractController
$user->email = $newEmail;
$user->save();
User::token($user);
User::generateToken($user);
return Base::retSuccess('修改成功', $user);
}
/**
* @api {get} api/users/delete/account 19. 删除帐号
* @api {get} api/users/delete/account 20. 删除帐号
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1200,7 +1360,7 @@ class UsersController extends AbstractController
if (!$password) {
return Base::retError('请输入登录密码');
}
if ($user->password != Base::md52($password, $user->encrypt)) {
if ($user->password != Doo::md5s($password, $user->encrypt)) {
return Base::retError('密码错误');
}
}
@@ -1215,7 +1375,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/department/list 20. 部门列表(限管理员)
* @api {get} api/users/department/list 21. 部门列表(限管理员)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1234,7 +1394,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/department/add 21. 新建、修改部门(限管理员)
* @api {get} api/users/department/add 22. 新建、修改部门(限管理员)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1268,6 +1428,12 @@ class UsersController extends AbstractController
if (mb_strlen($name) < 2 || mb_strlen($name) > 20) {
return Base::retError('部门名称长度限制2-20个字');
}
if (preg_match('/[\Q~!@#$%^&*()+-_=.:?<>,\E]/', $name)) {
return Base::retError('部门名称不能包含特殊符号');
}
if (str_contains($name, '(M)')) {
return Base::retError('部门名称不能包含:(M)');
}
//
if ($id > 0) {
$userDepartment = UserDepartment::find($id);
@@ -1305,12 +1471,13 @@ class UsersController extends AbstractController
'parent_id' => $parent_id,
'owner_userid' => $owner_userid,
], $dialog_useid);
Cache::forever("UserDepartment::rand", Base::generatePassword());
//
return Base::retSuccess($parent_id > 0 ? '保存成功' : '新建成功');
}
/**
* @api {get} api/users/department/del 22. 删除部门(限管理员)
* @api {get} api/users/department/del 23. 删除部门(限管理员)
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1334,12 +1501,13 @@ class UsersController extends AbstractController
return Base::retError('部门不存在或已被删除');
}
$userDepartment->deleteDepartment();
Cache::forever("UserDepartment::rand", Base::generatePassword());
//
return Base::retSuccess('删除成功');
}
/**
* @api {get} api/users/checkin/get 22. 获取签到设置
* @api {get} api/users/checkin/get 24. 获取签到设置
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1360,7 +1528,7 @@ class UsersController extends AbstractController
}
/**
* @api {post} api/users/checkin/save 22. 保存签到设置
* @api {post} api/users/checkin/save 25. 保存签到设置
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1385,7 +1553,7 @@ class UsersController extends AbstractController
return Base::retError('未开放修改权限,请联系管理员');
}
//
$list = Base::getPostValue('list');
$list = Request::input('list');
$array = [];
if (empty($list) || !is_array($list)) {
return Base::retError('参数错误');
@@ -1408,7 +1576,7 @@ class UsersController extends AbstractController
}
/**
* @api {get} api/users/checkin/list 22. 获取签到数据
* @api {get} api/users/checkin/list 26. 获取签到数据
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
@@ -1416,6 +1584,7 @@ class UsersController extends AbstractController
* @apiName checkin__list
*
* @apiParam {String} ym 年-月2020-01
* @apiParam {Number} [before] 取月份之前的数据单位月数最大3
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@@ -1429,6 +1598,11 @@ class UsersController extends AbstractController
$start = Carbon::parse(date("Y-m-01 00:00:00", strtotime($ym)));
$end = (clone $start)->addMonth()->subSecond();
//
$before = min(3, intval(Request::input('before')));
if ($before > 0) {
$start = $start->subMonths($before);
}
//
$recordTimes = UserCheckinRecord::getTimes($user->userid, [$start, $end]);
$array = [];
$startT = $start->timestamp;
@@ -1447,4 +1621,177 @@ class UsersController extends AbstractController
//
return Base::retSuccess('success', $array);
}
/**
* @api {get} api/users/socket/status 27. 获取socket状态
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup users
* @apiName socket__status
*
* @apiParam {String} [fd]
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function socket__status()
{
$row = WebSocket::select(['id', 'fd', 'userid', 'updated_at'])->whereFd(Base::headerOrInput('fd'))->first();
if (empty($row)) {
return Base::retError('error');
}
return Base::retSuccess('success', $row);
}
/**
* @api {get} api/users/key/client 28. 客户端KEY
*
* @apiDescription 获取客户端KEY用于加密数据发送给服务端
* @apiVersion 1.0.0
* @apiGroup users
* @apiName key__client
*
* @apiParam {String} [client_id] 客户端ID希望不变的除非清除浏览器缓存或者卸载应用
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function key__client()
{
$clientId = (trim(Request::input('client_id')) ?: Base::generatePassword(6)) . Doo::userId();
//
$cacheKey = "KeyPair::" . $clientId;
if (Cache::has($cacheKey)) {
$cacheData = Base::json2array(Cache::get($cacheKey));
if ($cacheData['private_key']) {
return Base::retSuccess('success', [
'type' => 'pgp',
'id' => $clientId,
'key' => $cacheData['public_key'],
]);
}
}
//
$name = Doo::userEmail() ?: Base::generatePassword(6);
$email = Doo::userEmail() ?: 'aa@bb.cc';
$data = Doo::pgpGenerateKeyPair($name, $email, Base::generatePassword());
Cache::put("KeyPair::" . $clientId, Base::array2json($data), Carbon::now()->addQuarter());
//
return Base::retSuccess('success', [
'type' => 'pgp',
'id' => $clientId,
'key' => $data['public_key'],
]);
}
/**
* @api {get} api/users/bot/info 29. 机器人信息
*
* @apiDescription 需要token身份获取我的机器人信息
* @apiVersion 1.0.0
* @apiGroup users
* @apiName bot__info
*
* @apiParam {Number} id 机器人ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function bot__info()
{
$user = User::auth();
//
$botId = intval(Request::input('id'));
$botUser = User::whereUserid($botId)->whereBot(1)->first();
if (empty($botUser)) {
return Base::retError('机器人不存在');
}
$userBot = UserBot::whereBotId($botUser->userid)->whereUserid($user->userid)->first();
if (empty($userBot)) {
return Base::retError('不是你的机器人');
}
return Base::retSuccess('success', [
'id' => $botUser->userid,
'name' => $botUser->nickname,
'avatar' => $botUser->userimg,
'clear_day' => $userBot->clear_day,
'webhook_url' => $userBot->webhook_url,
]);
}
/**
* @api {post} api/users/bot/edit 30. 编辑机器人
*
* @apiDescription 需要token身份编辑我的机器人信息
* @apiVersion 1.0.0
* @apiGroup users
* @apiName bot__edit
*
* @apiParam {Number} id 机器人ID
* @apiParam {String} [name] 机器人名称
* @apiParam {String} [avatar] 机器人头像
* @apiParam {Number} [clear_day] 清理天数
* @apiParam {String} [webhook_url] Webhook地址
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function bot__edit()
{
$user = User::auth();
//
$botId = intval(Request::input('id'));
$botUser = User::whereUserid($botId)->whereBot(1)->first();
if (empty($botUser)) {
return Base::retError('机器人不存在');
}
$userBot = UserBot::whereBotId($botUser->userid)->whereUserid($user->userid)->first();
if (empty($userBot)) {
return Base::retError('不是你的机器人');
}
//
$data = Request::input();
$upUser = [];
$upBot = [];
//
if (isset($data['name'])) {
$upUser['nickname'] = trim($data['name']);
}
if (isset($data['avatar'])) {
$avatar = $data['avatar'];
$avatar = $avatar ? Base::unFillUrl(is_array($avatar) ? $avatar[0]['path'] : $avatar) : '';
if (str_contains($avatar, 'avatar/')) {
$avatar = '';
}
$upUser['userimg'] = $avatar;
}
if (isset($data['clear_day'])) {
$upBot['clear_day'] = min(max(intval($data['clear_day']), 1), 999);
}
if (isset($data['webhook_url'])) {
$upBot['webhook_url'] = trim($data['webhook_url']);
}
//
if ($upUser) {
$botUser->updateInstance($upUser);
$botUser->save();
}
if ($upBot) {
$userBot->updateInstance($upBot);
$userBot->save();
}
//
return Base::retSuccess('修改成功', [
'id' => $botUser->userid,
'name' => $botUser->nickname,
'avatar' => $botUser->userimg,
'clear_day' => $userBot->clear_day,
'webhook_url' => $userBot->webhook_url,
]);
}
}

View File

@@ -4,12 +4,15 @@ namespace App\Http\Controllers;
use App\Models\File;
use App\Module\Base;
use App\Module\Ihttp;
use App\Module\Extranet;
use App\Module\RandomColor;
use App\Tasks\AppPushTask;
use App\Tasks\AutoArchivedTask;
use App\Tasks\CheckinRemindTask;
use App\Tasks\DeleteBotMsgTask;
use App\Tasks\DeleteTmpTask;
use App\Tasks\EmailNoticeTask;
use App\Tasks\JokeSoupTask;
use App\Tasks\LoopTask;
use Arr;
use Cache;
@@ -49,19 +52,22 @@ class IndexController extends InvokeController
*/
public function main()
{
$hash = 'no';
$path = public_path('js/hash');
$murl = url('manifest.txt');
if (file_exists($path)) {
$hash = trim(file_get_contents(public_path('js/hash')));
if (strlen($hash) > 16) {
$hash = 'long';
}
$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(),
'hash' => $hash
])->header('Link', "<{$murl}>; rel=\"prefetch\"");
'style' => $style,
'script' => $script,
])->header('Link', "<" . url('manifest.txt') . ">; rel=\"prefetch\"");
}
/**
@@ -77,28 +83,28 @@ class IndexController extends InvokeController
}
$array = [
"office/web-apps/apps/api/documents/api.js?hash=" . Base::getVersion(),
"office/7.2.2-56/web-apps/vendor/requirejs/require.js",
"office/7.2.2-56/web-apps/apps/api/documents/api.js",
"office/7.2.2-56/sdkjs/common/AllFonts.js",
"office/7.2.2-56/web-apps/vendor/xregexp/xregexp-all-min.js",
"office/7.2.2-56/web-apps/vendor/sockjs/sockjs.min.js",
"office/7.2.2-56/web-apps/vendor/jszip/jszip.min.js",
"office/7.2.2-56/web-apps/vendor/jszip-utils/jszip-utils.min.js",
"office/7.2.2-56/sdkjs/common/libfont/wasm/fonts.js",
"office/7.2.2-56/sdkjs/common/Charts/ChartStyles.js",
"office/7.2.2-56/sdkjs/slide/themes//themes.js",
"office/7.3.2-8/web-apps/vendor/requirejs/require.js",
"office/7.3.2-8/web-apps/apps/api/documents/api.js",
"office/7.3.2-8/sdkjs/common/AllFonts.js",
"office/7.3.2-8/web-apps/vendor/xregexp/xregexp-all-min.js",
"office/7.3.2-8/web-apps/vendor/sockjs/sockjs.min.js",
"office/7.3.2-8/web-apps/vendor/jszip/jszip.min.js",
"office/7.3.2-8/web-apps/vendor/jszip-utils/jszip-utils.min.js",
"office/7.3.2-8/sdkjs/common/libfont/wasm/fonts.js",
"office/7.3.2-8/sdkjs/common/Charts/ChartStyles.js",
"office/7.3.2-8/sdkjs/slide/themes//themes.js",
"office/7.2.2-56/web-apps/apps/presentationeditor/main/app.js",
"office/7.2.2-56/sdkjs/slide/sdk-all-min.js",
"office/7.2.2-56/sdkjs/slide/sdk-all.js",
"office/7.3.2-8/web-apps/apps/presentationeditor/main/app.js",
"office/7.3.2-8/sdkjs/slide/sdk-all-min.js",
"office/7.3.2-8/sdkjs/slide/sdk-all.js",
"office/7.2.2-56/web-apps/apps/documenteditor/main/app.js",
"office/7.2.2-56/sdkjs/word/sdk-all-min.js",
"office/7.2.2-56/sdkjs/word/sdk-all.js",
"office/7.3.2-8/web-apps/apps/documenteditor/main/app.js",
"office/7.3.2-8/sdkjs/word/sdk-all-min.js",
"office/7.3.2-8/sdkjs/word/sdk-all.js",
"office/7.2.2-56/web-apps/apps/spreadsheeteditor/main/app.js",
"office/7.2.2-56/sdkjs/cell/sdk-all-min.js",
"office/7.2.2-56/sdkjs/cell/sdk-all.js",
"office/7.3.2-8/web-apps/apps/spreadsheeteditor/main/app.js",
"office/7.3.2-8/sdkjs/cell/sdk-all-min.js",
"office/7.3.2-8/sdkjs/cell/sdk-all.js",
];
foreach ($array as &$item) {
$item = url($item);
@@ -187,9 +193,16 @@ class IndexController extends InvokeController
// 删除过期的临时表数据
Task::deliver(new DeleteTmpTask('wg_tmp_msgs', 1));
Task::deliver(new DeleteTmpTask('task_worker', 12));
Task::deliver(new DeleteTmpTask('tmp', 24));
Task::deliver(new DeleteTmpTask('tmp'));
Task::deliver(new DeleteTmpTask('file'));
// 删除机器人消息
Task::deliver(new DeleteBotMsgTask());
// 周期任务
Task::deliver(new LoopTask());
// 签到提醒
Task::deliver(new CheckinRemindTask());
// 获取笑话/心灵鸡汤
Task::deliver(new JokeSoupTask());
return "success";
}
@@ -199,24 +212,28 @@ class IndexController extends InvokeController
*/
public function desktop__publish($name = '')
{
$genericVersion = Request::header('generic-version');
$publishVersion = Request::header('publish-version');
$latestFile = public_path("uploads/desktop/latest");
$latestVersion = file_exists($latestFile) ? trim(file_get_contents($latestFile)) : "0.0.1";
if (strtolower($name) === 'latest') {
$name = $latestVersion;
}
// 上传
if (preg_match("/^\d+\.\d+\.\d+$/", $genericVersion)) {
if (version_compare($genericVersion, $latestVersion) > -1) { // 限制上传版本必须 ≥ 当前版本
$genericPath = "uploads/desktop/{$genericVersion}/";
if (preg_match("/^\d+\.\d+\.\d+$/", $publishVersion)) {
$publishKey = Request::header('publish-key');
if ($publishKey !== env('APP_KEY')) {
return Base::retError("key error");
}
if (version_compare($publishVersion, $latestVersion) > -1) { // 限制上传版本必须 ≥ 当前版本
$publishPath = "uploads/desktop/{$publishVersion}/";
$res = Base::upload([
"file" => Request::file('file'),
"type" => 'desktop',
"path" => $genericPath,
"path" => $publishPath,
"fileName" => true
]);
if (Base::isSuccess($res)) {
file_put_contents($latestFile, $genericVersion);
file_put_contents($latestFile, $publishVersion);
}
return $res;
}
@@ -234,7 +251,7 @@ class IndexController extends InvokeController
$fileName = Base::leftDelete($file, $dirPath);
$files[] = [
'name' => substr($fileName, 1),
'time' => date("Y-m-d H:i:s", fileatime($file)),
'time' => date("Y-m-d H:i:s", filemtime($file)),
'size' => Base::readableBytes(filesize($file)),
'url' => Base::fillUrl($path . $fileName),
];
@@ -248,13 +265,13 @@ class IndexController extends InvokeController
if (!str_ends_with($file, '.apk')) {
continue;
}
if ($apkFile && strtotime($apkFile['time']) > fileatime($file)) {
if ($apkFile && strtotime($apkFile['time']) > filemtime($file)) {
continue;
}
$fileName = Base::leftDelete($file, $dirPath);
$apkFile = [
'name' => substr($fileName, 1),
'time' => date("Y-m-d H:i:s", fileatime($file)),
'time' => date("Y-m-d H:i:s", filemtime($file)),
'size' => Base::readableBytes(filesize($file)),
'url' => Base::fillUrl($path . $fileName),
];
@@ -266,9 +283,9 @@ class IndexController extends InvokeController
}
// 下载
if ($name && file_exists($latestFile)) {
$genericVersion = file_get_contents($latestFile);
if (preg_match("/^\d+\.\d+\.\d+$/", $genericVersion)) {
$filePath = public_path("uploads/desktop/{$genericVersion}/{$name}");
$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);
}
@@ -283,20 +300,10 @@ class IndexController extends InvokeController
*/
public function drawio__iconsearch()
{
$query = Request::input('q');
$page = Request::input('p');
$size = Request::input('c');
$url = "https://app.diagrams.net/iconSearch?q={$query}&p={$page}&c={$size}";
$result = Cache::remember("drawioIconsearch::" . md5($url), now()->addDays(15), function () use ($url) {
return Ihttp::ihttp_get($url);
});
if (Base::isSuccess($result)) {
return $result['data'];
}
return [
'icons' => [],
'total_count' => 0
];
$query = trim(Request::input('q'));
$page = trim(Request::input('p'));
$size = trim(Request::input('c'));
return Extranet::drawioIconSearch($query, $page, $size);
}
/**
@@ -412,9 +419,6 @@ class IndexController extends InvokeController
$list = array_merge(Base::readDir(app_path()), Base::readDir(resource_path()));
$array = [];
foreach ($list as $item) {
if (Base::rightExists($item, "language.all.js")) {
continue;
}
if (Base::rightExists($item, ".php") || Base::rightExists($item, ".vue") || Base::rightExists($item, ".js")) {
$content = file_get_contents($item);
preg_match_all("/(['\"])(.*?)[\u{4e00}-\u{9fa5}\u{FE30}-\u{FFA0}]+([\s\S]((?!\n).)*)\\1/u", $content, $matchs);

View File

@@ -12,53 +12,8 @@ class VerifyCsrfToken extends Middleware
* @var array
*/
protected $except = [
// 上传图片
'api/system/imgupload/',
// 上传文件
'api/system/fileupload/',
// 保存任务优先级
'api/system/priority/',
// 保存创建项目列表模板
'api/system/column/template/',
// 添加任务
'api/project/task/add/',
// 保存工作流
'api/project/flow/save/',
// 修改任务
'api/project/task/update/',
// 聊天发文本
'api/dialog/msg/sendtext/',
// 聊天发语音
'api/dialog/msg/sendrecord/',
// 聊天发文件
'api/dialog/msg/sendfile/',
// 保存文件内容
'api/file/content/save/',
// 保存文件内容office
'api/file/content/office/',
// 保存文件内容(上传)
'api/file/content/upload/',
// 保存汇报
'api/report/store/',
// 签到设置
'api/users/checkin/save/',
// 签到上报
'api/public/checkin/report/',
// 接口部分
'api/*',
// 发布桌面端
'desktop/publish/',

View File

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

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

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

View File

@@ -21,7 +21,10 @@ use Illuminate\Support\Facades\DB;
* @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
@@ -38,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

View File

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

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

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

View File

@@ -29,7 +29,7 @@ use Request;
* @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)
@@ -46,8 +46,8 @@ use Request;
* @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
@@ -81,14 +81,14 @@ class File extends AbstractModel
* 图片文件
*/
const imageExt = [
'jpg', 'jpeg', 'png', 'gif', 'bmp'
'jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp'
];
/**
* 本地媒体文件
*/
const localExt = [
'jpg', 'jpeg', 'png', 'gif', 'bmp', 'ico', 'raw',
'jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp', 'ico', 'raw',
'tif', 'tiff',
'mp3', 'wav', 'mp4', 'flv',
'avi', 'mov', 'wmv', 'mkv', '3gp', 'rm',
@@ -96,21 +96,18 @@ class File extends AbstractModel
/**
* 是否有访问权限
* @param $userid
* @param array $userids
* @return int -1:没有权限0:访问权限1:读写权限1000:所有者或创建者
*/
public function getPermission($userid)
public function getPermission(array $userids)
{
if ($userid == $this->userid || $userid == $this->created_id) {
if (in_array($this->userid, $userids) || in_array($this->created_id, $userids)) {
// ① 自己的文件夹 或 自己创建的文件夹
return 1000;
}
$row = $this->getShareInfo();
if ($row) {
$fileUser = FileUser::whereFileId($row->id)->where(function ($query) use ($userid) {
$query->where('userid', 0);
$query->orWhere('userid', $userid);
})->orderByDesc('permission')->first();
$fileUser = FileUser::whereFileId($row->id)->whereIn('userid', $userids)->orderByDesc('permission')->first();
if ($fileUser) {
// ② 在指定共享成员内
return $fileUser->permission;
@@ -184,6 +181,7 @@ class File extends AbstractModel
if ($this->share != $share) {
AbstractModel::transaction(function () use ($share) {
$this->share = $share;
$this->pshare = $share ? $this->id : 0;
$this->save();
File::where("pids", "like", "%,{$this->id},%")->update(['pshare' => $share ? $this->id : 0]);
if ($share === 0) {
@@ -290,6 +288,27 @@ 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
@@ -390,6 +409,23 @@ class File extends AbstractModel
return $array;
}
/**
* code获取文件ID、名称
* @param $code
* @return File
*/
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);
}
/**
* 处理返回图片地址
@@ -411,19 +447,25 @@ class File extends AbstractModel
/**
* 获取文件并检测权限
* @param $id
* @param int $limit 要求权限: 0-访问权限、1-读写权限、1000-所有者或创建者
* @param $permission
* @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($id, $limit = 0, &$permission = -1)
public static function permissionFind(int $id, $user, int $limit = 0, int &$permission = -1)
{
$file = File::find($id);
if (empty($file)) {
throw new ApiException('文件不存在或已被删除');
}
//
$permission = $file->getPermission(User::userid());
if ($user instanceof User) {
$userids = $user->isTemp() ? [$user->userid] : [0, $user->userid];
} else {
$userids = is_array($user) ? $user : [$user];
}
$permission = $file->getPermission($userids);
if ($permission < $limit) {
$msg = match ($limit) {
1000 => '仅限所有者或创建者操作',

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use App\Module\Base;
use Illuminate\Database\Eloquent\SoftDeletes;
use Response;
/**
* App\Models\FileContent
@@ -21,7 +20,7 @@ use Response;
* @property \Illuminate\Support\Carbon|null $deleted_at
* @method static \Illuminate\Database\Eloquent\Builder|FileContent newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|FileContent newQuery()
* @method static \Illuminate\Database\Query\Builder|FileContent onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|FileContent onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|FileContent query()
* @method static \Illuminate\Database\Eloquent\Builder|FileContent whereContent($value)
* @method static \Illuminate\Database\Eloquent\Builder|FileContent whereCreatedAt($value)
@@ -32,14 +31,30 @@ 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
{
use SoftDeletes;
/**
* 强制删除文件内容
* @return void
*/
public function forceDeleteContent()
{
$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);
}
}
}
/**
* 转预览地址
* @param array $array
@@ -97,7 +112,7 @@ class FileContent extends AbstractModel
} else {
$filePath = public_path($content['url']);
}
return Response::streamDownload(function() use ($filePath) {
return Base::streamDownload(function() use ($filePath) {
echo file_get_contents($filePath);
}, $name);
}
@@ -128,7 +143,7 @@ class FileContent extends AbstractModel
if ($download) {
$filePath = public_path($path);
if (isset($filePath)) {
return Response::streamDownload(function() use ($filePath) {
return Base::streamDownload(function() use ($filePath) {
echo file_get_contents($filePath);
}, $name);
} else {

View File

@@ -51,12 +51,12 @@ class FileLink extends AbstractModel
$fileLink = FileLink::createInstance([
'file_id' => $fileId,
'userid' => $userid,
'code' => Base::generatePassword(64),
'code' => base64_encode("{$fileId},{$userid}," . Base::generatePassword()),
]);
$fileLink->save();
} else {
if ($refresh == 'yes') {
$fileLink->code = Base::generatePassword(64);
$fileLink->code = base64_encode("{$fileId},{$userid}," . Base::generatePassword());
$fileLink->save();
}
}

View File

@@ -28,17 +28,17 @@ use Request;
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read int $owner_userid
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectColumn[] $projectColumn
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectColumn> $projectColumn
* @property-read int|null $project_column_count
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectLog[] $projectLog
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectLog> $projectLog
* @property-read int|null $project_log_count
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectUser[] $projectUser
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectUser> $projectUser
* @property-read int|null $project_user_count
* @method static \Illuminate\Database\Eloquent\Builder|Project allData($userid = null)
* @method static \Illuminate\Database\Eloquent\Builder|Project authData($userid = null, $owner = null)
* @method static \Illuminate\Database\Eloquent\Builder|Project newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|Project newQuery()
* @method static \Illuminate\Database\Query\Builder|Project onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|Project onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|Project query()
* @method static \Illuminate\Database\Eloquent\Builder|Project whereArchivedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|Project whereArchivedUserid($value)
@@ -52,8 +52,8 @@ use Request;
* @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
@@ -211,7 +211,7 @@ class Project extends AbstractModel
'important' => 1
]);
}
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->delete();
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->remove();
});
}
@@ -251,8 +251,8 @@ class Project extends AbstractModel
$this->archived_at = null;
$this->archived_userid = User::userid();
$this->addLog("项目取消归档");
$this->pushMsg('add', $this);
ProjectTask::whereProjectId($this->id)->whereArchivedFollow(1)->update([
$this->pushMsg('recovery', $this);
ProjectTask::whereProjectId($this->id)->whereArchivedFollow(1)->change([
'archived_at' => null,
'archived_follow' => 0
]);
@@ -262,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
]);
@@ -441,7 +441,7 @@ class Project extends AbstractModel
});
//
foreach ($upTaskList as $id => $value) {
ProjectTask::whereFlowItemId($id)->update([
ProjectTask::whereFlowItemId($id)->change([
'flow_item_name' => $value
]);
}

View File

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

View File

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

View File

@@ -81,7 +81,7 @@ class ProjectFlowItem extends AbstractModel
*/
public function deleteFlowItem()
{
ProjectTask::whereFlowItemId($this->id)->update([
ProjectTask::whereFlowItemId($this->id)->change([
'flow_item_id' => 0,
'flow_item_name' => "",
]);

View File

@@ -52,18 +52,18 @@ use Request;
* @property-read bool $today
* @property-read \App\Models\Project|null $project
* @property-read \App\Models\ProjectColumn|null $projectColumn
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectTaskFile[] $taskFile
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectTaskFile> $taskFile
* @property-read int|null $task_file_count
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectTaskTag[] $taskTag
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectTaskTag> $taskTag
* @property-read int|null $task_tag_count
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ProjectTaskUser[] $taskUser
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ProjectTaskUser> $taskUser
* @property-read int|null $task_user_count
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask allData($userid = null)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask authData($userid = null, $owner = null)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask betweenTime($start, $end, $type = 'taskTime')
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask newQuery()
* @method static \Illuminate\Database\Query\Builder|ProjectTask onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask query()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereArchivedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereArchivedFollow($value)
@@ -92,8 +92,8 @@ use Request;
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereStartAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereUserid($value)
* @method static \Illuminate\Database\Query\Builder|ProjectTask withTrashed()
* @method static \Illuminate\Database\Query\Builder|ProjectTask withoutTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask withoutTrashed()
* @mixin \Eloquent
*/
class ProjectTask extends AbstractModel
@@ -530,8 +530,6 @@ class ProjectTask extends AbstractModel
public function updateTask($data, &$updateMarking = [])
{
AbstractModel::transaction(function () use ($data, &$updateMarking) {
// 判断版本
Base::checkClientVersion('0.19.0');
// 主任务
$mainTask = $this->parent_id > 0 ? self::find($this->parent_id) : null;
// 工作流
@@ -628,7 +626,7 @@ class ProjectTask extends AbstractModel
if ($this->complete_at) {
throw new ApiException('任务已完成');
}
$this->completeTask(Carbon::now());
$this->completeTask(Carbon::now(), isset($newFlowItem) ? $newFlowItem->name : null);
} else {
// 标记未完成
if (!$this->complete_at) {
@@ -674,9 +672,6 @@ class ProjectTask extends AbstractModel
'owner' => 1,
]);
$array[] = $uid;
if ($this->parent_id) {
break; // 子任务只能是一个负责人
}
}
if ($array) {
if (count($older) == 0 && count($array) == 1 && $array[0] == User::userid()) {
@@ -764,12 +759,13 @@ class ProjectTask extends AbstractModel
});
}
$newStringAt = $this->start_at ? ($this->start_at->toDateTimeString() . '~' . $this->end_at->toDateTimeString()) : '';
$this->addLog("修改{任务}时间" . ($desc ? "(备注:{$desc}" : ""), [
$newDesc = $desc ? "(备注:{$desc}" : "";
$this->addLog("修改{任务}时间" . $newDesc, [
'change' => [$oldStringAt, $newStringAt]
]);
$this->taskPush(null, 3);
$this->taskPush(null, 3, $newDesc);
}
// 以下顶级任务可修改
// 以下顶级任务可修改
if ($this->parent_id === 0) {
// 重复周期
$loopAt = $this->loop_at;
@@ -1046,7 +1042,7 @@ class ProjectTask extends AbstractModel
'important' => 1
]);
}
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->delete();
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->remove();
});
}
@@ -1179,11 +1175,12 @@ class ProjectTask extends AbstractModel
/**
* 标记已完成、未完成
* @param Carbon|null $complete_at 完成时间
* @param String $complete_name 已完成名称(留空为:已完成)
* @return bool
*/
public function completeTask($complete_at)
public function completeTask($complete_at, $complete_name = null)
{
AbstractModel::transaction(function () use ($complete_at) {
AbstractModel::transaction(function () use ($complete_at, $complete_name) {
$addMsg = $this->parent_id == 0 && $this->dialog_id > 0;
if ($complete_at === null) {
// 标记未完成
@@ -1191,7 +1188,7 @@ class ProjectTask extends AbstractModel
$this->addLog("标记{任务}未完成");
if ($addMsg) {
WebSocketDialogMsg::sendMsg(null, $this->dialog_id, 'notice', [
'notice' => '标记任务未完成'
'notice' => "标记任务未完成"
], 0, true, true);
}
} else {
@@ -1204,11 +1201,14 @@ class ProjectTask extends AbstractModel
if (!$this->hasOwner()) {
throw new ApiException('请先领取任务');
}
if (empty($complete_name)) {
$complete_name = '已完成';
}
$this->complete_at = $complete_at;
$this->addLog("标记{任务}已完成");
$this->addLog("标记{任务}{$complete_name}");
if ($addMsg) {
WebSocketDialogMsg::sendMsg(null, $this->dialog_id, 'notice', [
'notice' => '标记任务已完成'
'notice' => "标记任务{$complete_name}"
], 0, true, true);
}
}
@@ -1255,12 +1255,12 @@ class ProjectTask extends AbstractModel
$this->archived_follow = 0;
$this->addLog($logText, [], $userid);
}
$this->pushMsg('update', [
$this->pushMsg($archived_at === null ? 'recovery' : 'archived', [
'id' => $this->id,
'archived_at' => $this->archived_at,
'archived_userid' => $this->archived_userid,
]);
self::whereParentId($this->id)->update([
self::whereParentId($this->id)->change([
'archived_at' => $this->archived_at,
'archived_userid' => $this->archived_userid,
'archived_follow' => $this->archived_follow,
@@ -1282,7 +1282,7 @@ class ProjectTask extends AbstractModel
$dialog = WebSocketDialog::find($this->dialog_id);
$dialog?->deleteDialog();
}
self::whereParentId($this->id)->delete();
self::whereParentId($this->id)->remove();
$this->deleted_userid = User::userid();
$this->save();
$this->addLog("删除{任务}");
@@ -1299,12 +1299,12 @@ class ProjectTask extends AbstractModel
* @param bool $pushMsg 是否推送
* @return bool
*/
public function recoveryTask($pushMsg = true)
public function restoreTask($pushMsg = true)
{
AbstractModel::transaction(function () {
if ($this->dialog_id) {
$dialog = WebSocketDialog::withTrashed()->find($this->dialog_id);
$dialog?->recoveryDialog();
$dialog?->restoreDialog();
}
self::whereParentId($this->id)->withTrashed()->restore();
$this->addLog("还原{任务}");
@@ -1434,18 +1434,20 @@ class ProjectTask extends AbstractModel
* 任务提醒
* @param $userids
* @param int $type 0-新任务、1-即将超时、2-已超时、3-修改时间
* @param string $suffix 描述后缀
* @return void
*/
public function taskPush($userids, int $type)
public function taskPush($userids, int $type, string $suffix = "")
{
if ($userids === null) {
$userids = $this->taskUser->where('owner', 1)->pluck('userid')->toArray();
$userids = $this->taskUser->pluck('userid')->toArray();
}
if (empty($userids)) {
return;
}
$users = User::whereIn('userid', $userids)->whereNull('disable_at')->get();
if (empty($users)) {
$owners = $this->taskUser->pluck('owner', 'userid')->toArray();
$receivers = User::whereIn('userid', $userids)->whereNull('disable_at')->get();
if (empty($receivers)) {
return;
}
@@ -1454,27 +1456,32 @@ class ProjectTask extends AbstractModel
return;
}
$text = view('push.task', [
'type' => str_replace([0, 1, 2, 3], ['start', 'before', 'after', 'times'], $type),
'task' => $this,
])->render();
$taskHtml = "<span class=\"mention task\" data-id=\"{$this->id}\">#{$this->name}</span>";
$text = match ($type) {
1 => "您的任务 {$taskHtml} 即将超时。",
2 => "您的任务 {$taskHtml} 已经超时。",
3 => "您的任务 {$taskHtml} 时间已修改。",
default => "您有一个新任务 {$taskHtml}",
};
/** @var User $user */
foreach ($users as $user) {
foreach ($receivers as $receiver) {
$data = [
'type' => $type,
'userid' => $user->userid,
'userid' => $receiver->userid,
'task_id' => $this->id,
];
$pushLog = ProjectTaskPushLog::where($data)->exists();
if ($pushLog) {
if (in_array($type, [1, 2]) && ProjectTaskPushLog::where($data)->exists()) {
continue;
}
//
$dialog = WebSocketDialog::checkUserDialog($botUser->userid, $data['userid']);
$replace = $owners[$receiver->userid] ? "您负责的任务" : "您协助的任务";
$dialog = WebSocketDialog::checkUserDialog($botUser, $receiver->userid);
if ($dialog) {
ProjectTaskPushLog::createInstance($data)->save();
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid);
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', [
'text' => str_replace("您的任务", $replace, $text) . $suffix
], $botUser->userid);
}
}
}

View File

@@ -61,12 +61,12 @@ class ProjectTaskContent extends AbstractModel
{
$path = 'uploads/task/content/' . date("Ym") . '/' . $task_id . '/';
//
preg_match_all("/<img\s+src=\"data:image\/(png|jpg|jpeg);base64,(.*?)\"/s", $content, $matchs);
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 (file_put_contents(public_path($tmpPath), base64_decode($text))) {
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);
}

View File

@@ -94,7 +94,7 @@ class ProjectTaskFile extends AbstractModel
if (!isset($this->appendattrs['width'])) {
$width = -1;
$height = -1;
if (in_array($this->ext, ['jpg', 'jpeg', 'gif', 'png'])) {
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;

View File

@@ -19,7 +19,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @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\Query\Builder|ProjectTaskPushLog onlyTrashed()
* @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)
@@ -28,8 +28,8 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @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\Query\Builder|ProjectTaskPushLog withTrashed()
* @method static \Illuminate\Database\Query\Builder|ProjectTaskPushLog withoutTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskPushLog withoutTrashed()
* @mixin \Eloquent
*/
class ProjectTaskPushLog extends AbstractModel

View File

@@ -2,8 +2,6 @@
namespace App\Models;
use App\Module\Base;
/**
* App\Models\ProjectUser
*

View File

@@ -23,10 +23,10 @@ use JetBrains\PhpStorm\Pure;
* @property int $userid
* @property string $content
* @property string $sign 汇报唯一标识
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\ReportReceive[] $Receives
* @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|\App\Models\User[] $receivesUser
* @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()

View File

@@ -17,7 +17,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @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\Query\Builder|TaskWorker onlyTrashed()
* @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)
@@ -27,8 +27,8 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @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\Query\Builder|TaskWorker withTrashed()
* @method static \Illuminate\Database\Query\Builder|TaskWorker withoutTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|TaskWorker withoutTrashed()
* @mixin \Eloquent
*/
class TaskWorker extends AbstractModel

View File

@@ -155,12 +155,12 @@ class UmengAlias extends AbstractModel
$builder->whereUserid($userid);
}
$builder
->orderByDesc('id')
->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->groupBy('platform');
$lists = $rows->take(5)->groupBy('platform'); // 每个会员最多推送5个别名
foreach ($lists as $platform => $list) {
$alias = $list->pluck('alias')->implode(',');
self::pushMsgToAlias($alias, $platform, $array);

View File

@@ -5,6 +5,7 @@ namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use App\Module\Doo;
use Cache;
use Carbon\Carbon;
@@ -81,19 +82,6 @@ class User extends AbstractModel
// 基本信息的字段
public static $basicField = ['userid', 'email', 'nickname', 'profession', 'department', 'userimg', 'bot', 'az', 'pinyin', 'line_at', 'disable_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));
}
}
/**
* 昵称
* @param $value
@@ -149,21 +137,49 @@ class User extends AbstractModel
if (empty($this->department)) {
return "";
}
$list = UserDepartment::select(['id', 'owner_userid', 'name'])->whereIn('id', $this->department)->take(10)->get();
$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)' : '');
$array[] = $item['name'] . ($item['owner_userid'] === $this->userid ? '(M)' : '');
}
return implode(', ', $array);
}
/**
* 判断是否为部门负责人
*/
public function isDepartmentOwner()
{
return UserDepartment::where('owner_userid', $this->userid)->exists();
}
/**
* 获取机器人所有者
* @return int|mixed
*/
public function getBotOwner()
{
if (!$this->bot) {
return 0;
}
$key = "BotOwner::" . $this->userid;
return Cache::remember($key, now()->addMonth(), function() {
return intval(UserBot::whereBotId($this->userid)->value('userid'));
});
}
/**
* 是否在线
* @return bool
*/
public function getOnlineStatus()
{
$online = $this->bot || intval(Cache::get("User::online:" . $this->userid, 0)) > 0;
$online = $this->bot || Cache::get("User::online:" . $this->userid) === "on";
if ($online) {
return true;
}
@@ -171,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');
}
@@ -251,38 +303,40 @@ class User extends AbstractModel
*/
public static function reg($email, $password, $other = [])
{
//邮箱
// 邮箱
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';
$user = self::whereUserid(User::email2userid($email))->first();
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 = User::createInstance($inArray);
$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 (Base::settingFind('system', 'all_group_autoin', 'yes') === 'yes') {
if ($all_group_autoin === 'yes') {
$dialog = WebSocketDialog::whereGroupType('all')->orderByDesc('id')->first();
$dialog?->joinGroup($user->userid, 0);
}
@@ -290,73 +344,6 @@ class User extends AbstractModel
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
@@ -392,8 +379,7 @@ class User extends AbstractModel
{
$user = self::authInfo();
if (!$user) {
$authorization = Base::getToken();
if ($authorization) {
if (Base::headerOrInput('token')) {
throw new ApiException('身份已失效,请重新登录', [], -1);
} else {
throw new ApiException('请登录后继续...', [], -1);
@@ -418,61 +404,49 @@ class User extends AbstractModel
if (isset($_A["__static_auth"])) {
return $_A["__static_auth"];
}
$authorization = Base::getToken();
if ($authorization) {
$authInfo = self::authFind('all', $authorization);
if ($authInfo['userid'] > 0) {
$loginValid = floatval(Base::settingFind('system', 'loginValid')) ?: 720;
$loginValid *= 3600;
if ($authInfo['timestamp'] + $loginValid > time() || $authInfo['timestamp'] === -1) {
$row = self::whereUserid($authInfo['userid'])->whereEmail($authInfo['email'])->whereEncrypt($authInfo['encrypt'])->first();
if ($row) {
if (!$row->bot && $authInfo['timestamp'] === -1) {
return $_A["__static_auth"] = false; // 非机器人token时间不允许-1
}
$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)
{
$time = $userinfo->bot ? -1 : time();
$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;
}
/**
@@ -506,8 +480,7 @@ class User extends AbstractModel
*/
public static function userid2nickname($userid)
{
$basic = self::userid2basic($userid);
return $basic ? $basic->nickname : '';
return self::userid2basic($userid)?->nickname ?: '';
}
/**
@@ -549,14 +522,19 @@ class User extends AbstractModel
return Base::fillUrl($userimg);
}
// 机器人头像
if ($email == 'system-msg@bot.system') {
return url("images/avatar/default_system.png");
} elseif ($email == 'task-alert@bot.system') {
return url("images/avatar/default_task.png");
} elseif ($email == 'check-in@bot.system') {
return url("images/avatar/default_checkin.png");
} elseif ($email == 'bot-manager@bot.system') {
return url("images/avatar/default_bot.png");
switch ($email) {
case 'system-msg@bot.system':
return url("images/avatar/default_system.png");
case 'task-alert@bot.system':
return url("images/avatar/default_task.png");
case 'check-in@bot.system':
return url("images/avatar/default_checkin.png");
case 'anon-msg@bot.system':
return url("images/avatar/default_anon.png");
case 'approval-alert@bot.system':
return url("images/avatar/default_approval.png");
case 'bot-manager@bot.system':
return url("images/avatar/default_bot.png");
}
// 生成文字头像
if (self::$defaultAvatarMode === 'auto') {
@@ -603,19 +581,18 @@ class User extends AbstractModel
* @param $key
* @param $update
* @param $userid
* @return self
* @return self|null
*/
public static function botGetOrCreate($key, $update = [], $userid = 0)
{
$email = "{$key}@bot.system";
$botUser = self::whereEmail($email)->first();
if (empty($botUser)) {
$encrypt = Base::generatePassword(6);
$botUser = self::createInstance([
'bot' => 1,
'encrypt' => $encrypt,
'email' => $email,
'password' => Base::md52(Base::generatePassword(32), $encrypt),
$botUser = Doo::userCreate($email, Base::generatePassword(32));
if (empty($botUser)) {
return null;
}
$botUser->updateInstance([
'created_ip' => Base::getIp(),
]);
$botUser->save();
@@ -626,14 +603,25 @@ class User extends AbstractModel
])->save();
}
//
if ($key === 'system-msg') {
$update['nickname'] = '系统消息';
} elseif ($key === 'task-alert') {
$update['nickname'] = '任务提醒';
} elseif ($key === 'check-in') {
$update['nickname'] = '签到打卡';
} elseif ($key === 'bot-manager') {
$update['nickname'] = '机器人管理';
switch ($key) {
case 'system-msg':
$update['nickname'] = '系统消息';
break;
case 'task-alert':
$update['nickname'] = '任务提醒';
break;
case 'check-in':
$update['nickname'] = '签到打卡';
break;
case 'anon-msg':
$update['nickname'] = '匿名消息';
break;
case 'approval-alert':
$update['nickname'] = '审批';
break;
case 'bot-manager':
$update['nickname'] = '机器人管理';
break;
}
}
if ($update) {

View File

@@ -2,25 +2,126 @@
namespace App\Models;
use App\Module\Doo;
use App\Module\Extranet;
use Cache;
use Carbon\Carbon;
/**
* App\Models\UserBot
*
* @property int $id
* @property int|null $userid 所属人ID
* @property int|null $bot_id 机器人ID
* @property int|null $clear_day 消息自动清理天数
* @property string|null $clear_at 下一次清理时间
* @property string|null $webhook_url 消息webhook地址
* @property int|null $webhook_num 消息webhook请求次数
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @method static \Illuminate\Database\Eloquent\Builder|UserBot newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserBot newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|UserBot query()
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereBotId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereClearAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereClearDay($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereUserid($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereWebhookNum($value)
* @method static \Illuminate\Database\Eloquent\Builder|UserBot whereWebhookUrl($value)
* @mixin \Eloquent
*/
class UserBot extends AbstractModel
{
/**
* 机器人菜单
* @param $email
* @return array|array[]
*/
public static function quickMsgs($email)
{
return match ($email) {
'check-in@bot.system' => [
[
'key' => 'checkin',
'label' => Doo::translate('我要签到')
], [
'key' => 'it',
'label' => Doo::translate('IT资讯')
], [
'key' => '36ke',
'label' => Doo::translate('36氪')
], [
'key' => '60s',
'label' => Doo::translate('60s读世界')
], [
'key' => 'joke',
'label' => Doo::translate('开心笑话')
], [
'key' => 'soup',
'label' => Doo::translate('心灵鸡汤')
]
],
'anon-msg@bot.system' => [
[
'key' => 'help',
'label' => Doo::translate('使用说明')
], [
'key' => 'privacy',
'label' => Doo::translate('隐私说明')
],
],
'bot-manager@bot.system' => [
[
'key' => '/help',
'label' => Doo::translate('帮助指令')
], [
'key' => '/api',
'label' => Doo::translate('Api接口文档')
], [
'key' => '/list',
'label' => Doo::translate('我的机器人')
],
],
default => [],
};
}
/**
* 签到机器人
* @param $command
* @param $userid
* @return string
*/
public static function checkinBotQuickMsg($command, $userid)
{
if (Cache::get("UserBot::checkinBotQuickMsg:{$userid}") === "yes") {
return "操作频繁!";
}
Cache::put("UserBot::checkinBotQuickMsg:{$userid}", "yes", Carbon::now()->addSecond());
//
$text = match ($command) {
"checkin" => "暂未开放手动签到。",
default => Extranet::checkinBotQuickMsg($command),
};
return $text ?: '维护中...';
}
/**
* 隐私机器人
* @param $command
* @return string
*/
public static function anonBotQuickMsg($command)
{
return match ($command) {
"help" => "使用说明:打开你想要发匿名消息的个人对话,点击输入框右边的 ⊕ 号,选择 <u>匿名消息</u> 即可输入你想要发送的匿名消息内容。",
"privacy" => "匿名消息将通过 <u>匿名消息(机器人)</u> 发送给对方,不会记录你的身份信息。",
default => '',
};
}
}

View File

@@ -127,7 +127,6 @@ class UserDepartment extends AbstractModel
$dialog = WebSocketDialog::find($this->dialog_id);
if ($dialog) {
$dialog->deleteDialog();
$dialog->pushMsg("groupDelete");
}
//
$this->delete();

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use App\Module\Doo;
use App\Tasks\PushTask;
use Cache;
use Carbon\Carbon;
@@ -17,17 +18,19 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property string|null $type 对话类型
* @property string|null $group_type 聊天室类型
* @property string|null $name 对话名称
* @property string $avatar 头像(群)
* @property string|null $last_at 最后消息时间
* @property int|null $owner_id 群主用户ID
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\WebSocketDialogUser[] $dialogUser
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\WebSocketDialogUser> $dialogUser
* @property-read int|null $dialog_user_count
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog newQuery()
* @method static \Illuminate\Database\Query\Builder|WebSocketDialog onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog query()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereAvatar($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereGroupType($value)
@@ -37,14 +40,24 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereOwnerId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereUpdatedAt($value)
* @method static \Illuminate\Database\Query\Builder|WebSocketDialog withTrashed()
* @method static \Illuminate\Database\Query\Builder|WebSocketDialog withoutTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog withoutTrashed()
* @mixin \Eloquent
*/
class WebSocketDialog extends AbstractModel
{
use SoftDeletes;
/**
* 头像地址
* @param $value
* @return string
*/
public function getAvatarAttribute($value)
{
return $value ? Base::fillUrl($value) : $value;
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
@@ -68,35 +81,34 @@ class WebSocketDialog extends AbstractModel
return $data[$key] ?? $default;
};
//
$time = Carbon::parse($this->user_at ?? $dialogUserFun('updated_at'));
$this->top_at = $this->top_at ?? $dialogUserFun('top_at');
$this->user_at = $time->toDateTimeString('millisecond');
$this->user_ms = $time->valueOf();
//
if (isset($this->search_msg_id)) {
// 最后消息 (搜索预览消息)
$this->last_msg = WebSocketDialogMsg::whereDialogId($this->id)->find($this->search_msg_id);
$this->last_at = $this->last_msg?->created_at;
} else {
// 最后消息
$this->last_msg = WebSocketDialogMsg::whereDialogId($this->id)->orderByDesc('id')->first();
// 未读信息
$unreadBuilder = WebSocketDialogMsgRead::whereDialogId($this->id)->whereUserid($userid)->whereReadAt(null);
$this->unread = $unreadBuilder->count();
$this->mention = 0;
$this->last_umid = 0;
if ($this->unread > 0) {
$this->mention = $unreadBuilder->clone()->whereMention(1)->count();
$this->last_umid = intval($unreadBuilder->clone()->orderByDesc('msg_id')->value('msg_id'));
}
$this->generateUnread($userid, $hasData);
// 未读标记
$this->mark_unread = $this->mark_unread ?? $dialogUserFun('mark_unread');
// 是否免打扰
$this->silence = $this->silence ?? $dialogUserFun('silence');
// 对话人数
$builder = WebSocketDialogUser::whereDialogId($this->id);
$this->people = $builder->count();
$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->top_at = $this->top_at ?? $dialogUserFun('top_at');
$this->bot = 0;
switch ($this->type) {
case "user":
@@ -107,7 +119,8 @@ class WebSocketDialog extends AbstractModel
$basic = User::userid2basic($dialog_user->userid);
if ($basic) {
$this->name = $basic->nickname;
$this->bot = $basic->bot;
$this->bot = $basic->getBotOwner();
$this->quick_msgs = UserBot::quickMsgs($basic->email);
} else {
$this->name = 'non-existent';
$this->dialog_delete = 1;
@@ -135,7 +148,7 @@ class WebSocketDialog extends AbstractModel
}
break;
case 'all':
$this->name = Base::Lang('全体成员');
$this->name = Doo::translate('全体成员');
$this->all_group_mute = Base::settingFind('system', 'all_group_mute');
break;
}
@@ -148,7 +161,41 @@ class WebSocketDialog extends AbstractModel
$this->has_file = $msgBuilder->clone()->whereMtype('file')->exists();
$this->has_link = $msgBuilder->clone()->whereLink(1)->exists();
}
$this->pinyin = Base::cn2pinyin($this->name);
return $this;
}
/**
* 生成未读数据
* @param $userid
* @param $positionData
* @return $this
*/
public function generateUnread($userid, $positionData = false)
{
$builder = WebSocketDialogMsgRead::whereDialogId($this->id)->whereUserid($userid)->whereReadAt(null);
$this->unread = $builder->count();
$this->mention = $this->unread > 0 ? $builder->clone()->whereMention(1)->count() : 0;
if ($positionData) {
$array = [];
// @我的消息
if ($this->mention > 0
&& $mention_id = intval($builder->clone()->whereMention(1)->orderByDesc('msg_id')->value('msg_id'))) {
$array[] = [
'msg_id' => $mention_id,
'label' => Doo::translate('@我的消息'),
];
}
// 最早一条未读消息
if ($this->unread > 0
&& $first_id = intval($builder->clone()->orderBy('msg_id')->value('msg_id'))) {
$array[] = [
'msg_id' => $first_id,
'label' => '{UNREAD}'
];
}
//
$this->position_msgs = $array;
}
return $this;
}
@@ -211,8 +258,13 @@ class WebSocketDialog extends AbstractModel
/** @var WebSocketDialogUser $item */
foreach ($list as $item) {
if ($checkDelete) {
if ($type === 'remove' && !in_array(User::userid(), [$this->owner_id, $item->inviter])) {
throw new ApiException('只有群主或邀请人可以移出成员');
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);
@@ -258,6 +310,7 @@ class WebSocketDialog extends AbstractModel
});
$this->delete();
});
$this->pushMsg("groupDelete");
return true;
}
@@ -265,9 +318,10 @@ class WebSocketDialog extends AbstractModel
* 还原会话
* @return bool
*/
public function recoveryDialog()
public function restoreDialog()
{
$this->restore();
$this->pushMsg("groupRestore");
return true;
}
@@ -302,7 +356,7 @@ class WebSocketDialog extends AbstractModel
case 'all':
throw new ApiException('当前会话全员禁言');
case 'user':
if (!User::find($userid)?->isAdmin()) {
if (!User::find($userid)?->checkAdmin()) {
throw new ApiException('当前会话禁言');
}
}
@@ -326,7 +380,7 @@ class WebSocketDialog extends AbstractModel
$name = \DB::table('project_tasks')->where('dialog_id', $this->id)->value('name');
break;
case 'all':
$name = Base::Lang('全体成员');
$name = Doo::translate('全体成员');
break;
}
}
@@ -364,6 +418,20 @@ class WebSocketDialog extends AbstractModel
Task::deliver($task);
}
/**
* 更新对话最后消息时间
* @return WebSocketDialogMsg|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Query\Builder|object|null
*/
public function updateMsgLastAt()
{
$lastMsg = WebSocketDialogMsg::whereDialogId($this->id)->orderByDesc('id')->first();
if ($lastMsg) {
$this->last_at = $lastMsg->created_at;
$this->save();
}
return $lastMsg;
}
/**
* 获取对话(同时检验对话身份)
* @param $dialog_id
@@ -434,37 +502,40 @@ class WebSocketDialog extends AbstractModel
/**
* 获取会员对话(没有自动创建)
* @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 ($userid == $userid2) {
$userid2 = 0;
if ($user->userid == $receiver) {
$receiver = 0;
}
$dialogUser = self::select(['web_socket_dialogs.*'])
->join('web_socket_dialog_users as u1', 'web_socket_dialogs.id', '=', 'u1.dialog_id')
->join('web_socket_dialog_users as u2', 'web_socket_dialogs.id', '=', 'u2.dialog_id')
->where('u1.userid', $userid)
->where('u2.userid', $userid2)
->where('u1.userid', $user->userid)
->where('u2.userid', $receiver)
->where('web_socket_dialogs.type', 'user')
->first();
if ($dialogUser) {
return $dialogUser;
}
return AbstractModel::transaction(function () use ($userid2, $userid) {
if ($receiver > 0 && $user->isTemp()) {
throw new ApiException('无法发起会话');
}
return AbstractModel::transaction(function () use ($receiver, $user) {
$dialog = self::createInstance([
'type' => 'user',
]);
$dialog->save();
WebSocketDialogUser::createInstance([
'dialog_id' => $dialog->id,
'userid' => $userid,
'userid' => $user->userid,
])->save();
WebSocketDialogUser::createInstance([
'dialog_id' => $dialog->id,
'userid' => $userid2,
'userid' => $receiver,
])->save();
return $dialog;
});

View File

@@ -2,11 +2,11 @@
namespace App\Models;
use App\Exceptions\ApiException;
use Carbon\Carbon;
use App\Module\Base;
use App\Tasks\PushTask;
use App\Exceptions\ApiException;
use App\Tasks\WebSocketDialogMsgTask;
use Carbon\Carbon;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -38,7 +38,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property-read \App\Models\WebSocketDialog|null $webSocketDialog
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg newQuery()
* @method static \Illuminate\Database\Query\Builder|WebSocketDialogMsg onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg 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)
@@ -60,8 +60,8 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereType($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereUserid($value)
* @method static \Illuminate\Database\Query\Builder|WebSocketDialogMsg withTrashed()
* @method static \Illuminate\Database\Query\Builder|WebSocketDialogMsg withoutTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg withTrashed()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg withoutTrashed()
* @mixin \Eloquent
*/
class WebSocketDialogMsg extends AbstractModel
@@ -125,7 +125,7 @@ class WebSocketDialogMsg extends AbstractModel
}
$value = Base::json2array($value);
if ($this->type === 'file') {
$value['type'] = in_array($value['ext'], ['jpg', 'jpeg', 'png', 'gif']) ? 'img' : 'file';
$value['type'] = in_array($value['ext'], ['jpg', 'jpeg', 'webp', 'png', 'gif']) ? 'img' : 'file';
$value['path'] = Base::fillUrl($value['path']);
$value['thumb'] = Base::fillUrl($value['thumb'] ?: Base::extIcon($value['ext']));
} else if ($this->type === 'record') {
@@ -254,7 +254,7 @@ class WebSocketDialogMsg extends AbstractModel
$dialog = WebSocketDialog::find($this->dialog_id);
$dialog?->pushMsg('update', $resData);
//
return Base::retSuccess('sucess', $resData);
return Base::retSuccess('success', $resData);
}
/**
@@ -367,12 +367,12 @@ class WebSocketDialogMsg extends AbstractModel
* 转发消息
* @param array|int $dialogids
* @param array|int $userids
* @param int $sender 发送的会员ID
* @param User $user 发送的会员
* @return mixed
*/
public function forwardMsg($dialogids, $userids, $sender)
public function forwardMsg($dialogids, $userids, $user)
{
return AbstractModel::transaction(function() use ($dialogids, $sender, $userids) {
return AbstractModel::transaction(function() use ($dialogids, $user, $userids) {
$originalMsg = Base::json2array($this->getRawOriginal('msg'));
$msgs = [];
$already = [];
@@ -381,7 +381,7 @@ class WebSocketDialogMsg extends AbstractModel
$dialogids = [$dialogids];
}
foreach ($dialogids as $dialogid) {
$res = self::sendMsg(null, $dialogid, $this->type, $originalMsg, $sender);
$res = self::sendMsg(null, $dialogid, $this->type, $originalMsg, $user->userid);
if (Base::isSuccess($res)) {
$msgs[] = $res['data'];
$already[] = $dialogid;
@@ -396,9 +396,9 @@ class WebSocketDialogMsg extends AbstractModel
if (!User::whereUserid($userid)->exists()) {
continue;
}
$dialog = WebSocketDialog::checkUserDialog($sender, $userid);
$dialog = WebSocketDialog::checkUserDialog($user, $userid);
if ($dialog && !in_array($dialog->id, $already)) {
$res = self::sendMsg(null, $dialog->id, $this->type, $originalMsg, $sender);
$res = self::sendMsg(null, $dialog->id, $this->type, $originalMsg, $user->userid);
if (Base::isSuccess($res)) {
$msgs[] = $res['data'];
}
@@ -413,9 +413,34 @@ class WebSocketDialogMsg extends AbstractModel
/**
* 删除消息
* @param array|int $ids
* @return void
*/
public function deleteMsg()
public static function deleteMsgs($ids) {
$ids = Base::arrayRetainInt(is_array($ids) ? $ids : [$ids], true);
AbstractModel::transaction(function() use ($ids) {
$dialogIds = WebSocketDialogMsg::select('dialog_id')->whereIn("id", $ids)->distinct()->get()->pluck('dialog_id');
$replyIds = WebSocketDialogMsg::select('reply_id')->whereIn("id", $ids)->distinct()->get()->pluck('reply_id');
//
WebSocketDialogMsgRead::whereIn('msg_id', $ids)->whereNull('read_at')->delete(); // 未阅读记录不需要软删除,直接删除即可
WebSocketDialogMsgTodo::whereIn('msg_id', $ids)->delete();
self::whereIn('id', $ids)->delete();
//
$dialogDatas = WebSocketDialog::whereIn('id', $dialogIds)->get();
foreach ($dialogDatas as $dialogData) {
$dialogData->updateMsgLastAt();
}
foreach ($replyIds as $id) {
self::whereId($id)->update(['reply_num' => self::whereReplyId($id)->count()]);
}
});
}
/**
* 撤回消息
* @return void
*/
public function withdrawMsg()
{
$send_dt = Carbon::parse($this->created_at)->addDay();
if ($send_dt->lt(Carbon::now())) {
@@ -429,16 +454,13 @@ class WebSocketDialogMsg extends AbstractModel
self::whereId($this->reply_id)->decrement('reply_num');
}
//
$last_msg = null;
if ($this->webSocketDialog) {
$last_msg = WebSocketDialogMsg::whereDialogId($this->dialog_id)->orderByDesc('id')->first();
$this->webSocketDialog->last_at = $last_msg->created_at;
$this->webSocketDialog->save();
}
//
$dialog = WebSocketDialog::find($this->dialog_id);
if ($dialog) {
$userids = $dialog->dialogUser->pluck('userid')->toArray();
$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' => [
@@ -447,7 +469,7 @@ class WebSocketDialogMsg extends AbstractModel
'data' => [
'id' => $this->id,
'dialog_id' => $this->dialog_id,
'last_msg' => $last_msg,
'last_msg' => $dialogData->updateMsgLastAt(),
'update_read' => $deleteRead ? 1 : 0
],
]
@@ -558,12 +580,12 @@ class WebSocketDialogMsg extends AbstractModel
// 基础处理
$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|gif);base64,(.*?)\"(.*?)>(<\/img>)*/s", $text, $matchs);
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 (file_put_contents(public_path($imagePath), base64_decode($base64))) {
if (Base::saveContentImage(public_path($imagePath), base64_decode($base64))) {
$imageSize = getimagesize(public_path($imagePath));
if (Base::imgThumb(public_path($imagePath), public_path($imagePath) . "_thumb.jpg", 320, 0)) {
$imagePath .= "_thumb.jpg";
@@ -594,7 +616,7 @@ class WebSocketDialogMsg extends AbstractModel
$imageSize = getimagesize(public_path($imagePath));
// 添加后缀
if ($imageSize && !str_contains($imagePath, '.')) {
preg_match("/^image\/(png|jpg|jpeg|gif)$/", $imageSize['mime'], $matchMine);
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))) {
@@ -617,8 +639,18 @@ class WebSocketDialogMsg extends AbstractModel
}
}
// 其他网络图片
preg_match_all("/<img[^>]*?src=([\"'])(.*?\.(png|jpg|jpeg|gif))\\1[^>]*?>/is", $text, $matchs);
$imageSaveLocal = Base::settingFind("system", "image_save_local");
preg_match_all("/<img[^>]*?src=([\"'])(.*?\.(png|jpg|jpeg|webp|gif))\\1[^>]*?>/is", $text, $matchs);
foreach ($matchs[2] as $key => $str) {
if ($imageSaveLocal === 'close') {
$imageSize = getimagesize($str);
if ($imageSize === false) {
$imageSize = ["auto", "auto"];
}
$imagePath = "base64-" . base64_encode($str);
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:{$imageSize[0]}:{$imageSize[1]}:{$imagePath}::]", $text);
continue;
}
if (str_starts_with($str, "{{RemoteURL}}")) {
$imagePath = Base::leftDelete($str, "{{RemoteURL}}");
$imagePath = Base::rightDelete($imagePath, "_thumb.jpg");
@@ -637,7 +669,7 @@ class WebSocketDialogMsg extends AbstractModel
$image = file_get_contents($str);
if (empty($image)) {
$text = str_replace($matchs[0][$key], "[:IMAGE:browse:90:90:images/other/imgerr.jpg::]", $text);
} else if (file_put_contents(public_path($imagePath), $image)) {
} else if (Base::saveContentImage(public_path($imagePath), $image)) {
$imageSize = getimagesize(public_path($imagePath));
if (Base::imgThumb(public_path($imagePath), public_path($imagePath) . "_thumb.jpg", 320, 0)) {
$imagePath .= "_thumb.jpg";
@@ -655,7 +687,7 @@ class WebSocketDialogMsg extends AbstractModel
$keyId = $matchId[1];
if ($matchChar[1] === "~") {
if (Base::isNumber($keyId)) {
$file = File::permissionFind($keyId);
$file = File::permissionFind($keyId, User::auth());
if ($file->type == 'folder') {
throw new ApiException('文件夹不支持分享');
}
@@ -663,7 +695,7 @@ class WebSocketDialogMsg extends AbstractModel
$keyId = $fileLink['code'];
} else {
preg_match("/\/single\/file\/(.*?)$/i", $keyId, $match);
if ($match && strlen($match[1]) >= 32) {
if ($match && strlen($match[1]) >= 8) {
$keyId = $match[1];
} else {
throw new ApiException('文件分享错误');
@@ -672,13 +704,24 @@ class WebSocketDialogMsg extends AbstractModel
}
$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]) >= 32) {
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;
@@ -691,11 +734,11 @@ class WebSocketDialogMsg extends AbstractModel
$text = str_replace($str, "[:LINK:{$herf}:{$title}:]", $text);
}
// 文件分享链接
preg_match_all("/(https*:\/\/)((\w|=|\?|\.|\/|&|-|:|\+|%|;|#)+)/i", $text, $matchs);
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]) >= 32) {
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;
@@ -707,13 +750,18 @@ class WebSocketDialogMsg extends AbstractModel
// 过滤标签
$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("/\[:IMAGE:(.*?):(.*?):(.*?):(.*?):(.*?):\]/i", "<img class=\"$1\" width=\"$2\" height=\"$3\" src=\"{{RemoteURL}}$4\" alt=\"$5\"/>", $text);
$text = preg_replace("/\[:@:(.*?):(.*?):\]/i", "<span class=\"mention user\" data-id=\"$1\">@$2</span>", $text);
$text = preg_replace("/\[:#:(.*?):(.*?):\]/i", "<span class=\"mention task\" data-id=\"$1\">#$2</span>", $text);
$text = preg_replace("/\[:~:(.*?):(.*?):\]/i", "<a class=\"mention file\" href=\"{{RemoteURL}}single/file/$1\" target=\"_blank\">~$2</a>", $text);
$text = preg_replace_callback("/\[:LINK:(.*?):(.*?):\]/i", function (array $match) {
return "<a href=\"" . base64_decode($match[1]) . "\" target=\"_blank\">" . base64_decode($match[2]) . "</a>";
}, $text);
$text = preg_replace_callback("/\[:IMAGE:(.*?):(.*?):(.*?):(.*?):(.*?):\]/i", function (array $match) {
$wh = $match[2] === 'auto' ? "" : " width=\"{$match[2]}\" height=\"{$match[3]}\"";
$src = str_starts_with($match[4], "base64-") ? base64_decode(substr($match[4], 7)) : "{{RemoteURL}}{$match[4]}";
return "<img class=\"{$match[1]}\"{$wh} src=\"{$src}\" alt=\"{$match[5]}\"/>";
}, $text);
$text = preg_replace("/\[:@:(.*?):(.*?):\]/i", "<span class=\"mention user\" data-id=\"$1\">@$2</span>", $text);
$text = preg_replace("/\[:#:(.*?):(.*?):\]/i", "<span class=\"mention task\" data-id=\"$1\">#$2</span>", $text);
$text = preg_replace("/\[:~:(.*?):(.*?):\]/i", "<a class=\"mention file\" href=\"{{RemoteURL}}single/file/$1\" target=\"_blank\">~$2</a>", $text);
$text = preg_replace("/\[:QUICK:(.*?):(.*?):\]/i", "<span data-quick-key=\"$1\">$2</span>", $text);
return preg_replace("/^(<p><\/p>)+|(<p><\/p>)+$/i", "", $text);
}
@@ -743,8 +791,15 @@ class WebSocketDialogMsg extends AbstractModel
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', 'png', 'gif'])) {
if (in_array($msg['ext'], ['jpg', 'jpeg', 'webp', 'png', 'gif'])) {
$mtype = 'image';
}
}
@@ -813,6 +868,7 @@ class WebSocketDialogMsg extends AbstractModel
$dialogMsg->send = 1;
$dialogMsg->key = $dialogMsg->generateMsgKey();
$dialogMsg->save();
WebSocketDialogUser::whereDialogId($dialog->id)->change(['updated_at' => Carbon::now()->toDateTimeString('millisecond')]);
});
//
$task = new WebSocketDialogMsgTask($dialogMsg->id);

View File

@@ -2,6 +2,8 @@
namespace App\Models;
use Carbon\Carbon;
/**
* App\Models\WebSocketDialogUser
*
@@ -15,6 +17,7 @@ namespace App\Models;
* @property int|null $important 是否不可移出(项目、任务、部门人员)
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\WebSocketDialog|null $webSocketDialog
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser query()
@@ -32,5 +35,13 @@ namespace App\Models;
*/
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');
}
}

View File

@@ -6,6 +6,12 @@
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
// 判断是否通过cmd命令执行的
if (function_exists('info')){
echo "Success \n";
return;
}
$path = dirname(__FILE__). '/';
$lists = scandir($path);
//

View File

@@ -6,11 +6,10 @@ use App\Exceptions\ApiException;
use App\Models\Setting;
use App\Models\Tmp;
use Cache;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
use Overtrue\Pinyin\Pinyin;
use Redirect;
use Request;
use Response;
use Storage;
use Validator;
@@ -76,6 +75,16 @@ class Base
});
}
/**
* 如果header没有则通过input读取
* @param $key
* @return mixed|string
*/
public static function headerOrInput($key)
{
return Base::nullShow(Request::header($key), Request::input($key));
}
/**
* 获取版本号
* @return string
@@ -94,7 +103,7 @@ class Base
{
global $_A;
if (!isset($_A["__static_client_version"])) {
$_A["__static_client_version"] = Request::header('version') ?: '0.0.1';
$_A["__static_client_version"] = self::headerOrInput('version') ?: '0.0.1';
}
return $_A["__static_client_version"];
}
@@ -917,6 +926,27 @@ class Base
return array_values($array);
}
/**
* 数组拼接字符串(前后也加上)
* @param $glue
* @param $pieces
* @param $around
* @return string
*/
public static function arrayImplode($glue = "", $pieces = null, $around = true)
{
if ($pieces == null) {
$pieces = $glue;
$glue = ',';
}
$pieces = array_values(array_filter(array_unique($pieces)));
$string = implode($glue, $pieces);
if ($around && $string) {
$string = ",{$string},";
}
return $string;
}
/**
* 判断是否二维数组
* @param $array
@@ -1280,6 +1310,27 @@ class Base
return $setting;
}
/**
* 时间转毫秒时间戳
* @param $time
* @return float|int
*/
public static function strtotimeM($time)
{
if (str_contains($time, '.')) {
list($t, $m) = explode(".", $time);
if (is_string($t)) {
$t = strtotime($t);
}
$time = $t . str_pad($m, 3, "0", STR_PAD_LEFT);
}
if (is_numeric($time)) {
return (int) str_pad($time, 13, "0");
} else {
return strtotime($time) * 1000;
}
}
/**
* 获取设置值
* @param $setname
@@ -1407,54 +1458,6 @@ class Base
}
}
/**
* 国际化(替换国际语言)
* @param $val
* @return mixed
*/
public static function Lang($val)
{
$repArray = [];
if (is_array($val)) {
if (self::strExists($val[0], '%') && count($val) > 1) {
$repArray = array_slice($val, 1);
}
$val = $val[0];
}
$data = self::langData();
if (isset($data[$val]) && $data[$val] !== null) {
$val = $data[$val];
}
if ($repArray) {
foreach ($repArray as $item) {
$val = self::strReplaceLimit('%', $item, $val, 1);
}
}
return $val;
}
/**
* 加载语言数据
* @param bool $refresh
* @return array
*/
public static function langData($refresh = false)
{
global $_A;
if (!isset($_A["__static_langdata"]) || $refresh === true) {
$_A["__static_langdata"] = [];
$language = trim(Request::header('language'));
$langpath = resource_path('lang/' . $language . '/general.php');
if (file_exists($langpath)) {
$data = include $langpath;
if (is_array($data)) {
$_A["__static_langdata"] = $data;
}
}
}
return $_A["__static_langdata"];
}
/**
* JSON返回
* @param $param
@@ -1462,10 +1465,8 @@ class Base
*/
public static function jsonEcho($param)
{
global $_GPC;
//
$json = json_encode($param);
$callback = $_GPC['callback'];
$callback = Request::input('callback');
if ($callback) {
return $callback . '(' . $json . ')';
} else {
@@ -1482,11 +1483,11 @@ class Base
*/
public static function retSuccess($msg, $data = [], $ret = 1)
{
return array(
return [
'ret' => $ret,
'msg' => self::Lang($msg),
'msg' => Doo::translate($msg),
'data' => $data
);
];
}
/**
@@ -1498,11 +1499,11 @@ class Base
*/
public static function retError($msg, $data = [], $ret = 0)
{
return array(
return [
'ret' => $ret,
'msg' => self::Lang($msg),
'msg' => Doo::translate($msg),
'data' => $data
);
];
}
/**
@@ -1843,111 +1844,6 @@ class Base
return $_A["__static_ip"];
}
/**
* 获取IP地址经纬度
* @param string $ip
* @return array|mixed
*/
public static function getIpGcj02($ip = '')
{
if (empty($ip)) {
$ip = self::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|mixed
*/
public static function getIpGcj02ByBaidu($ip = ''): array
{
if (empty($ip)) {
$ip = self::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|mixed
*/
public static function getIpInfo($ip = '')
{
if (empty($ip)) {
$ip = self::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);
}
/**
* 是否是中国IP-1错误、1是、0否
* @param string $ip
@@ -2037,60 +1933,6 @@ class Base
}
}
/**
* php://input 字符串解析到变量并获取指定值
* @param $key
* @return array
*/
public static function getContentsParse($key)
{
parse_str(Request::getContent(), $input);
if ($key) {
$input = $input[$key] ?? array();
}
return is_array($input) ? $input : array($input);
}
/**
* php://input 字符串解析到变量并获取指定值
* @param $key
* @param null $default
* @return mixed|null
*/
public static function getContentValue($key, $default = null)
{
global $_A;
if (!isset($_A["__static_input_content"])) {
parse_str(Request::getContent(), $input);
$_A["__static_input_content"] = $input;
}
return $_A["__static_input_content"][$key] ?? $default;
}
/**
* @param $key
* @param null $default
* @return array|mixed|string|null
*/
public static function getPostValue($key, $default = null)
{
$value = self::getContentValue($key, $default);
if (!isset($value)) {
$value = Request::post($key, $default);
}
return $value;
}
/**
* @param $key
* @param null $default
* @return int
*/
public static function getPostInt($key, $default = null)
{
return intval(self::getPostValue($key, $default));
}
/**
* 多维 array_values
* @param $array
@@ -2123,29 +1965,6 @@ class Base
return $array;
}
/**
* 获取tonken
* @return string
*/
public static function getToken()
{
global $_A;
if (!isset($_A["__static_token"])) {
$_A["__static_token"] = Base::nullShow(Request::header('token'), Request::input('token'));
}
return $_A["__static_token"];
}
/**
* 设置tonken
* @param $token
*/
public static function setToken($token)
{
global $_A;
$_A["__static_token"] = $token;
}
/**
* 是否微信
* @return bool
@@ -2171,6 +1990,27 @@ class Base
}
}
/**
* 获取平台类型
* @return string
*/
public static function platform()
{
$platform = strtolower(trim(Request::header('platform')));
if (in_array($platform, ['android', 'ios', 'win', 'mac', 'web'])) {
return $platform;
}
$agent = strtolower(Request::server('HTTP_USER_AGENT'));
if (str_contains($agent, 'android')) {
$platform = 'android';
} elseif (str_contains($agent, 'iphone') || str_contains($agent, 'ipad')) {
$platform = 'ios';
} else {
$platform = 'unknown';
}
return $platform;
}
/**
* 返回根据距离sql排序语句
* @param $lat
@@ -2237,7 +2077,7 @@ class Base
/**
* image64图片保存
* @param array $param [ image64=带前缀的base64, path=>文件路径, fileName=>文件名称, scale=>[压缩原图宽,高, 压缩方式], autoThumb=>false不要自动生成缩略图 ]
* @param array $param [ image64=带前缀的base64, path=>文件路径, fileName=>文件名称, scale=>[压缩原图宽,高, 压缩方式], autoThumb=>false不要自动生成缩略图, 'compress'=>是否压缩图片(默认true) ]
* @return array [name=>文件名, size=>文件大小(单位KB),file=>绝对地址, path=>相对地址, url=>全路径地址, ext=>文件后缀名]
*/
public static function image64save($param)
@@ -2245,7 +2085,7 @@ class Base
$imgBase64 = $param['image64'];
if (preg_match('/^(data:\s*image\/(\w+);base64,)/', $imgBase64, $res)) {
$extension = $res[2];
if (!in_array($extension, ['png', 'jpg', 'jpeg', 'gif'])) {
if (!in_array($extension, ['png', 'jpg', 'jpeg', 'webp', 'gif'])) {
return Base::retError('图片格式错误');
}
$scaleName = "";
@@ -2316,6 +2156,11 @@ class Base
}
}
}
// 压缩图片
if ($param['compress'] !== false) {
ImgCompress::compress($array['file']);
$array['size'] = Base::twoFloat(filesize($array['file']) / 1024, true);
}
//生成缩略图
$array['thumb'] = $array['path'];
if ($extension === 'gif' && !isset($param['autoThumb'])) {
@@ -2335,7 +2180,7 @@ class Base
/**
* 上传文件
* @param array $param [ type=[文件类型], file=>Request::file, path=>文件路径, fileName=>文件名称, scale=>[压缩原图宽,高, 压缩方式], size=>限制大小KB, autoThumb=>false不要自动生成缩略图, chmod=>权限(默认0644) ]
* @param array $param [ type=[文件类型], file=>Request::file, path=>文件路径, fileName=>文件名称, scale=>[压缩原图宽,高, 压缩方式], size=>限制大小KB, autoThumb=>false不要自动生成缩略图, chmod=>权限(默认0644), 'compress'=>是否压缩图片(默认true) ]
* @return array [name=>原文件名, size=>文件大小(单位KB),file=>绝对地址, path=>相对地址, url=>全路径地址, ext=>文件后缀名]
*/
public static function upload($param)
@@ -2352,11 +2197,8 @@ class Base
case 'png':
$type = ['png'];
break;
case 'png+jpg':
$type = ['jpg', 'jpeg', 'png'];
break;
case 'image':
$type = ['jpg', 'jpeg', 'gif', 'png'];
$type = ['jpg', 'jpeg', 'webp', 'gif', 'png'];
break;
case 'video':
$type = ['rm', 'rmvb', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4'];
@@ -2374,7 +2216,7 @@ class Base
$type = ['zip'];
break;
case 'file':
$type = ['jpg', 'jpeg', 'png', 'gif', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'esp', 'pdf', 'rar', 'zip', 'gz', 'ai', 'avi', 'bmp', 'cdr', 'eps', 'mov', 'mp3', 'mp4', 'pr', 'psd', 'svg', 'tif'];
$type = ['jpg', 'jpeg', 'webp', 'png', 'gif', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'esp', 'pdf', 'rar', 'zip', 'gz', 'ai', 'avi', 'bmp', 'cdr', 'eps', 'mov', 'mp3', 'mp4', 'pr', 'psd', 'svg', 'tif'];
break;
case 'firmware':
$type = ['img', 'tar', 'bin'];
@@ -2461,7 +2303,7 @@ class Base
}
}
//
if (in_array($extension, ['jpg', 'jpeg', 'gif', 'png'])) {
if (in_array($extension, ['jpg', 'jpeg', 'webp', 'gif', 'png'])) {
//图片尺寸
$paramet = getimagesize($array['file']);
$array['width'] = $paramet[0];
@@ -2510,6 +2352,11 @@ class Base
}
$array['thumb'] = Base::fillUrl($array['thumb']);
}
// 压缩图片
if ($param['compress'] !== false) {
ImgCompress::compress($array['file']);
$array['size'] = Base::twoFloat(filesize($array['file']) / 1024, true);
}
//
return Base::retSuccess('success', $array);
} else {
@@ -2567,7 +2414,7 @@ class Base
$dst_img = $src_img;
}
$st = pathinfo($src_img, PATHINFO_EXTENSION);
if (!in_array(strtolower($st), array('jpg', 'jpeg', 'png', 'gif', 'bmp'))) {
if (!in_array(strtolower($st), array('jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp'))) {
return false;
}
$ot = pathinfo($dst_img, PATHINFO_EXTENSION);
@@ -2661,7 +2508,7 @@ class Base
}
$src = $createfun($src_img);
$dst = imagecreatetruecolor($width ? $width : $dst_w, $height ? $height : $dst_h);
$dst = imagecreatetruecolor($width ?: $dst_w, $height ?: $dst_h);
try {
$white = imagecolorallocate($dst, 255, 255, 255);
imagefill($dst, 0, 0, $white);
@@ -3108,4 +2955,73 @@ class Base
throw new ApiException($validator->errors()->first());
}
}
/**
* 流下载,解决没有后缀无法下载的问题
* @param $callback
* @param $name
* @return mixed
*/
public static function streamDownload($callback, $name = null) {
if ($name && !str_contains($name, '.')) {
$name .= ".";
}
return Response::streamDownload($callback, $name);
}
/**
* 保存图片到文件(同时压缩)
* @param $path
* @param $content
* @param $compress
* @return bool
*/
public static function saveContentImage($path, $content, $compress = true) {
if (file_put_contents($path, $content)) {
if ($compress) {
ImgCompress::compress($path);
}
return true;
}
return false;
}
/**
* 多维数组字母转下划线格式
* @param $array
* @return array
*/
public static function arrayKeyToUnderline($array)
{
$newArray = [];
foreach ($array as $key => $value) {
//如果是数组,递归调用
if (is_array($value)) {
$value = self::arrayKeyToUnderline($value);
}
$newKey = strtolower(preg_replace('/(?<=[a-z])([A-Z])/', '_$1', $key));
$newArray[$newKey] = $value;
}
return $newArray;
}
/**
* 多维数组字母转驼峰格式
*
* @param [type] $array
* @return array
*/
public static function arrayKeyToCamel($array)
{
$newArray = [];
foreach ($array as $key => $value) {
//如果是数组,递归调用
if (is_array($value)) {
$value = self::arrayKeyToCamel($value);
}
$newKey = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $key))));
$newArray[$newKey] = $value;
}
return $newArray;
}
}

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ use App\Models\User;
use App\Models\WebSocket;
use App\Models\WebSocketDialogMsg;
use App\Module\Base;
use App\Module\Doo;
use App\Tasks\LineTask;
use App\Tasks\PushTask;
use Cache;
@@ -39,55 +40,45 @@ class WebSocketService implements WebSocketHandlerInterface
*/
public function onOpen(Server $server, Request $request)
{
global $_A;
$_A = [
'__static_langdata' => [],
];
$fd = $request->fd;
$data = Base::newTrim($request->get);
$action = $data['action'];
$get = Base::newTrim($request->get);
$action = $get['action'];
Cache::forget("User::encrypt:" . $fd);
switch ($action) {
/**
* 网页访问
*/
case 'web':
{
// 判断token参数
$token = $data['token'];
$cacheKey = "ws::token:" . md5($token);
$userid = Cache::remember($cacheKey, now()->addSeconds(1), function () use ($token) {
$authInfo = User::authFind('all', $token);
if ($authInfo['userid'] > 0) {
if (User::whereUserid($authInfo['userid'])->whereEmail($authInfo['email'])->whereEncrypt($authInfo['encrypt'])->exists()) {
return $authInfo['userid'];
}
}
return 0;
});
if (empty($userid)) {
Cache::forget($cacheKey);
Doo::load($get['token'], $get['language']);
//
if (Doo::userId() > 0
&& !Doo::userExpired()
&& $user = User::whereUserid(Doo::userId())->whereEmail(Doo::userEmail())->whereEncrypt(Doo::userEncrypt())->first()) {
// 保存用户
$this->saveUser($fd, $user->userid);
// 发送open事件
$server->push($fd, Base::array2json([
'type' => 'open',
'data' => [
'fd' => $fd,
],
]));
// 通知上线
Task::deliver(new LineTask($user->userid, true));
// 推送离线时收到的消息
Task::deliver(new PushTask("RETRY::" . $user->userid));
} else {
// 用户不存在
$server->push($fd, Base::array2json([
'type' => 'error',
'data' => [
'error' => '成员不存在'
'error' => 'No member'
],
]));
$server->close($fd);
$this->deleteUser($fd);
return;
}
// 保存用户、发送open事件
$this->saveUser($fd, $userid);
$server->push($fd, Base::array2json([
'type' => 'open',
'data' => [
'fd' => $fd,
],
]));
// 通知上线
Task::deliver(new LineTask($userid, true));
// 推送离线时收到的消息
Task::deliver(new PushTask("RETRY::" . $userid));
}
break;
@@ -103,11 +94,6 @@ class WebSocketService implements WebSocketHandlerInterface
*/
public function onMessage(Server $server, Frame $frame)
{
global $_A;
$_A = [
'__static_langdata' => [],
];
//
$msg = Base::json2array($frame->data);
$type = $msg['type']; // 消息类型
$msgId = $msg['msgId']; // 消息ID用于回调
@@ -153,6 +139,16 @@ class WebSocketService implements WebSocketHandlerInterface
}
}
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) {
@@ -191,13 +187,15 @@ class WebSocketService implements WebSocketHandlerInterface
*/
private function saveUser($fd, $userid)
{
Cache::put("User::fd:" . $fd, "on", Carbon::now()->addDay());
Cache::put("User::online:" . $userid, "on", Carbon::now()->addDay());
//
WebSocket::updateInsert([
'key' => md5($fd . '@' . $userid)
], [
'fd' => $fd,
'userid' => $userid,
]);
Cache::put("User::online:" . $userid, time(), Carbon::now()->addSeconds(30));
}
/**
@@ -206,6 +204,8 @@ class WebSocketService implements WebSocketHandlerInterface
*/
private function deleteUser($fd)
{
Cache::forget("User::fd:" . $fd);
//
$array = [];
WebSocket::whereFd($fd)->chunk(10, function($list) use (&$array) {
/** @var WebSocket $item */
@@ -216,6 +216,7 @@ class WebSocketService implements WebSocketHandlerInterface
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;

View File

@@ -3,9 +3,13 @@
namespace App\Tasks;
use App\Models\User;
use App\Models\UserBot;
use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Module\Base;
use App\Module\Doo;
use App\Module\Ihttp;
use Carbon\Carbon;
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
@@ -46,9 +50,7 @@ class BotReceiveMsgTask extends AbstractTask
if ($dialog->type !== 'user') {
return;
}
if ($botUser->email === 'bot-manager@bot.system') {
$this->botManagerReceive($msg);
}
$this->botManagerReceive($msg, $botUser);
}
public function end()
@@ -59,25 +61,65 @@ class BotReceiveMsgTask extends AbstractTask
/**
* 机器人管理处理消息
* @param WebSocketDialogMsg $msg
* @param User $botUser
* @return void
*/
private function botManagerReceive(WebSocketDialogMsg $msg)
private function botManagerReceive(WebSocketDialogMsg $msg, User $botUser)
{
if ($msg->type === 'text') {
$text = trim(strip_tags($msg->msg['text']));
if (empty($text)) {
if ($msg->type !== 'text') {
return;
}
$original = $msg->msg['text'];
if (preg_match("/<span[^>]*?data-quick-key=([\"'])(.*?)\\1[^>]*?>(.*?)<\/span>/is", $original, $match)) {
$command = $match[2];
} else {
$command = trim(strip_tags($original));
}
// 签到机器人
if ($botUser->email === 'check-in@bot.system') {
$text = UserBot::checkinBotQuickMsg($command, $msg->userid);
if ($text) {
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', ['text' => $text], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
}
}
// 隐私机器人
if ($botUser->email === 'anon-msg@bot.system') {
$text = UserBot::anonBotQuickMsg($command);
if ($text) {
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', ['text' => $text], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
}
}
// 管理机器人
if (str_starts_with($command, '/')) {
if ($botUser->email === 'bot-manager@bot.system') {
$isManager = true;
} elseif (UserBot::whereBotId($botUser->userid)->whereUserid($msg->userid)->exists()) {
$isManager = false;
} else {
$text = "非常抱歉,我不是你的机器人,无法完成你的指令。";
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', ['text' => $text], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
return;
}
$array = Base::newTrim(explode(" ", "{$text} "));
//
$array = Base::newTrim(explode(" ", "{$command} "));
$type = $array[0];
$data = [];
$notice = "";
if (!$isManager && in_array($type, ['/list', '/newbot'])) {
return; // 这些操作仅支持【机器人管理】机器人
}
switch ($type) {
/**
* 列表
*/
case '/list':
$data = User::select(['users.*'])
$data = User::select([
'users.*',
'user_bots.clear_day',
'user_bots.clear_at',
'user_bots.webhook_url',
'user_bots.webhook_num'
])
->join('user_bots', 'users.userid', '=', 'user_bots.bot_id')
->where('users.bot', 1)
->where('user_bots.userid', $msg->userid)
@@ -90,6 +132,18 @@ class BotReceiveMsgTask extends AbstractTask
}
break;
/**
* 详情
*/
case '/info':
$botId = $isManager ? $array[1] : $botUser->userid;
$data = $this->botManagerOne($botId, $msg->userid);
if (!$data) {
$type = "notice";
$notice = "机器人不存在。";
}
break;
/**
* 创建
*/
@@ -116,9 +170,9 @@ class BotReceiveMsgTask extends AbstractTask
$notice = "创建失败。";
break;
}
$dialog = WebSocketDialog::checkUserDialog($data->userid, $msg->userid);
$dialog = WebSocketDialog::checkUserDialog($data, $msg->userid);
if ($dialog) {
$text = "好,我是你的机器人:{$data->nickname}, 我的机器人ID是{$data->userid}";
$text = "<p>您好,我是机器人:{$data->nickname}我的机器人ID是{$data->userid}</p><p>你可以发送 <u><b>/help</b></u> 查看我支持什么命令。</p>";
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $data->userid); // todo 未能在任务end事件来发送任务
}
break;
@@ -127,16 +181,18 @@ class BotReceiveMsgTask extends AbstractTask
* 修改名字
*/
case '/setname':
if (strlen($array[2]) < 2 || strlen($array[2]) > 20) {
$botId = $isManager ? $array[1] : $botUser->userid;
$nameString = $isManager ? $array[2] : $array[1];
if (strlen($nameString) < 2 || strlen($nameString) > 20) {
$type = "notice";
$notice = "机器人名称由2-20个字符组成。";
break;
}
$data = $this->botManagerOne($array[1], $msg->userid);
$data = $this->botManagerOne($botId, $msg->userid);
if ($data) {
$data->nickname = $array[2];
$data->az = Base::getFirstCharter($array[2]);
$data->pinyin = Base::cn2pinyin($array[2]);
$data->nickname = $nameString;
$data->az = Base::getFirstCharter($nameString);
$data->pinyin = Base::cn2pinyin($nameString);
$data->save();
} else {
$type = "notice";
@@ -149,7 +205,8 @@ class BotReceiveMsgTask extends AbstractTask
* 删除
*/
case '/deletebot':
$data = $this->botManagerOne($array[1], $msg->userid);
$botId = $isManager ? $array[1] : $botUser->userid;
$data = $this->botManagerOne($botId, $msg->userid);
if ($data) {
$data->deleteUser('delete bot');
} else {
@@ -162,9 +219,10 @@ class BotReceiveMsgTask extends AbstractTask
* 获取Token
*/
case '/token':
$data = $this->botManagerOne($array[1], $msg->userid);
$botId = $isManager ? $array[1] : $botUser->userid;
$data = $this->botManagerOne($botId, $msg->userid);
if ($data) {
User::token($data);
User::generateToken($data);
} else {
$type = "notice";
$notice = "机器人不存在。";
@@ -175,10 +233,11 @@ class BotReceiveMsgTask extends AbstractTask
* 更新Token
*/
case '/revoke':
$data = $this->botManagerOne($array[1], $msg->userid);
$botId = $isManager ? $array[1] : $botUser->userid;
$data = $this->botManagerOne($botId, $msg->userid);
if ($data) {
$data->encrypt = Base::generatePassword(6);
$data->password = Base::md52(Base::generatePassword(32), $data->encrypt);
$data->password = Doo::md5s(Base::generatePassword(32), $data->encrypt);
$data->save();
} else {
$type = "notice";
@@ -186,15 +245,64 @@ class BotReceiveMsgTask extends AbstractTask
}
break;
/**
* 设置保留消息时间
*/
case '/clearday':
$botId = $isManager ? $array[1] : $botUser->userid;
$clearDay = $isManager ? $array[2] : $array[1];
$data = $this->botManagerOne($botId, $msg->userid);
if ($data) {
$userBot = UserBot::whereBotId($botId)->whereUserid($msg->userid)->first();
if ($userBot) {
$userBot->clear_day = min(intval($clearDay) ?: 30, 999);
$userBot->clear_at = Carbon::now()->addDays($userBot->clear_day);
$userBot->save();
}
$data->clear_day = $userBot->clear_day;
$data->clear_at = $userBot->clear_at; // 这两个参数只是作为输出,所以不保存
} else {
$type = "notice";
$notice = "机器人不存在。";
}
break;
/**
* 设置webhook
*/
case '/webhook':
$botId = $isManager ? $array[1] : $botUser->userid;
$webhookUrl = $isManager ? $array[2] : $array[1];
$data = $this->botManagerOne($botId, $msg->userid);
if (strlen($webhookUrl) > 255) {
$type = "notice";
$notice = "webhook地址最长仅支持255个字符。";
} elseif ($data) {
$userBot = UserBot::whereBotId($botId)->whereUserid($msg->userid)->first();
if ($userBot) {
$userBot->webhook_url = $webhookUrl ?: "";
$userBot->webhook_num = 0;
$userBot->save();
}
$data->webhook_url = $userBot->webhook_url ?: '-';
$data->webhook_num = $userBot->webhook_num; // 这两个参数只是作为输出,所以不保存
} else {
$type = "notice";
$notice = "机器人不存在。";
}
break;
/**
* 会话搜索
*/
case '/dialog':
$data = $this->botManagerOne($array[1], $msg->userid);
$botId = $isManager ? $array[1] : $botUser->userid;
$nameKey = $isManager ? $array[2] : $array[1];
$data = $this->botManagerOne($botId, $msg->userid);
if ($data) {
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread', 'u.silence'])
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread', 'u.silence', 'u.updated_at as user_at'])
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
->where('web_socket_dialogs.name', 'LIKE', "%{$array[2]}%")
->where('web_socket_dialogs.name', 'LIKE', "%{$nameKey}%")
->where('u.userid', $data->userid)
->orderByDesc('u.top_at')
->orderByDesc('web_socket_dialogs.last_at')
@@ -207,7 +315,7 @@ class BotReceiveMsgTask extends AbstractTask
$list->transform(function (WebSocketDialog $item) use ($data) {
return $item->formatData($data->userid);
});
$data->list = $list;
$data->list = $list; // 这个参数只是作为输出,所以不保存
}
} else {
$type = "notice";
@@ -220,11 +328,33 @@ class BotReceiveMsgTask extends AbstractTask
'type' => $type,
'data' => $data,
'notice' => $notice,
'manager' => $isManager,
'version' => Base::getVersion()
])->render();
if (!$isManager) {
$text = preg_replace("/\s*\{机器人ID\}/", "", $text);
}
$text = preg_replace("/^\x20+/", "", $text);
$text = preg_replace("/\n\x20+/", "\n", $text);
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', ['text' => $text], $this->userid, false, false, true); // todo 未能在任务end事件来发送任务
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', ['text' => $text], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
return;
}
// 推送Webhook
if ($command) {
$userBot = UserBot::whereBotId($botUser->userid)->first();
if ($userBot && preg_match("/^https*:\/\//", $userBot->webhook_url)) {
Ihttp::ihttp_post($userBot->webhook_url, [
'text' => $command,
'token' => User::generateToken($botUser),
'dialog_id' => $msg->dialog_id,
'msg_id' => $msg->id,
'msg_uid' => $msg->userid,
'bot_uid' => $botUser->userid,
'version' => Base::getVersion(),
], 10);
$userBot->webhook_num++;
$userBot->save();
}
}
}
@@ -238,7 +368,13 @@ class BotReceiveMsgTask extends AbstractTask
$botId = intval($botId);
$userid = intval($userid);
if ($botId > 0) {
return User::select(['users.*'])
return User::select([
'users.*',
'user_bots.clear_day',
'user_bots.clear_at',
'user_bots.webhook_url',
'user_bots.webhook_num'
])
->join('user_bots', 'users.userid', '=', 'user_bots.bot_id')
->where('users.bot', 1)
->where('user_bots.bot_id', $botId)

View File

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

View File

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

View File

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

View File

@@ -90,7 +90,7 @@ class EmailNoticeTask extends AbstractTask
if (empty($data)) {
continue;
}
$user = User::whereNull('disable_at')->find($userid);
$user = User::whereBot(0)->whereNull('disable_at')->find($userid);
if (empty($user)) {
continue;
}

View File

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

View File

@@ -6,6 +6,7 @@ namespace App\Tasks;
use App\Models\WebSocket;
use App\Models\WebSocketTmpMsg;
use App\Module\Base;
use App\Module\Doo;
use Cache;
use Carbon\Carbon;
use Hhxsv5\LaravelS\Swoole\Task\Task;
@@ -111,7 +112,7 @@ class PushTask extends AbstractTask
*/
public static function push(array $lists, $retryOffline = true, $key = null, $delay = 1)
{
if (!is_array($lists) || empty($lists)) {
if (empty($lists)) {
return;
}
if (!Base::isTwoArray($lists)) {
@@ -177,10 +178,12 @@ class PushTask extends AbstractTask
Task::deliver($task);
} else {
try {
$swoole->push($fid, Base::array2json($msg));
$tmpMsgId > 0 && WebSocketTmpMsg::whereId($tmpMsgId)->update(['send' => 1]);
$swoole->push($fid, self::pushMsgFormat($fid, $msg));
if ($tmpMsgId > 0) {
WebSocketTmpMsg::whereId($tmpMsgId)->update(['send' => 1]);
}
} catch (\Throwable) {
// 发送失败
}
}
}
@@ -191,4 +194,25 @@ class PushTask extends AbstractTask
}
}
}
/**
* 格式化推送消息
* @param $fid
* @param $msg
* @return string
*/
private static function pushMsgFormat($fid, $msg)
{
$encrypt = Base::json2array(Cache::get("User::encrypt:" . $fid));
if ($encrypt['type'] == 'pgp') {
if (is_array($msg) && $msg['type'] == 'dialog') {
// 仅加密对话消息
$msg = [
'type' => 'encrypt',
'encrypted' => Doo::pgpEncryptApi($msg, $encrypt['key']),
];
}
}
return Base::array2json($msg);
}
}

View File

@@ -9,6 +9,7 @@ use App\Models\WebSocketDialog;
use App\Models\WebSocketDialogMsg;
use App\Models\WebSocketDialogMsgRead;
use App\Module\Base;
use Carbon\Carbon;
use Hhxsv5\LaravelS\Swoole\Task\Task;
use Request;
@@ -84,7 +85,12 @@ class WebSocketDialogMsgTask extends AbstractTask
if (empty($dialog)) {
return;
}
$silences = $dialog->dialogUser->pluck('silence', 'userid')->toArray();
$updateds = [];
$silences = [];
foreach ($dialog->dialogUser as $dialogUser) {
$updateds[$dialogUser->userid] = $dialogUser->updated_at;
$silences[$dialogUser->userid] = $dialogUser->silence;
}
$userids = array_keys($silences);
// 提及会员
@@ -111,14 +117,17 @@ class WebSocketDialogMsgTask extends AbstractTask
$array = [];
foreach ($userids AS $userid) {
$silence = $this->silence || $silences[$userid];
$updated = $updateds[$userid] ?? $msg->created_at;
if ($userid == $msg->userid) {
$array[$userid] = [
'userid' => $userid,
'silence' => $silence,
'mention' => false,
'silence' => $silence,
'updated' => $updated,
];
} else {
$mention = array_intersect([0, $userid], $mentions) ? 1 : 0;
$silence = $mention ? false : $silence;
WebSocketDialogMsgRead::createInstance([
'dialog_id' => $msg->dialog_id,
'msg_id' => $msg->id,
@@ -128,8 +137,9 @@ class WebSocketDialogMsgTask extends AbstractTask
])->saveOrIgnore();
$array[$userid] = [
'userid' => $userid,
'silence' => $silence,
'mention' => $mention,
'silence' => $silence,
'updated' => $updated,
];
// 机器人收到消处理
$botUser = User::whereUserid($userid)->whereBot(1)->first();
@@ -153,6 +163,8 @@ class WebSocketDialogMsgTask extends AbstractTask
'silence' => $item['silence'] ? 1 : 0,
'data' => array_merge($msg->toArray(), [
'mention' => $item['mention'],
'user_at' => Carbon::parse($item['updated'])->toDateTimeString('millisecond'),
'user_ms' => Carbon::parse($item['updated'])->valueOf(),
]),
]
];
@@ -196,7 +208,10 @@ class WebSocketDialogMsgTask extends AbstractTask
'type' => 'dialog',
'mode' => 'chat',
'silence' => $this->silence ? 1 : 0,
'data' => $msg->toArray(),
'data' => array_merge($msg->toArray(), [
'user_at' => Carbon::parse($msg->created_at)->toDateTimeString('millisecond'),
'user_ms' => Carbon::parse($msg->created_at)->valueOf(),
]),
]
];
}

15
bin/auto Executable file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env bash
if [ ! -f ".env" ]; then
echo -e "配置文件不存在!"
exit 1
fi
debug=`cat .env | grep "^APP_DEBUG=" | awk -F '=' '{print $2}'`
if [ "$debug" = "true" ]; then
echo "[MODE] development"
./bin/inotify ./app
else
echo "[MODE] production"
php bin/laravels start -i
fi

89
bin/run
View File

@@ -1,89 +0,0 @@
#!/usr/bin/env php
<?php
/**
* Class runLoader
*/
class runLoader
{
public function randString($length = 16)
{
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$passwordstr = '';
$max = strlen($chars) - 1;
for ($i = 0; $i < $length; $i++) {
$passwordstr .= $chars[mt_rand(0, $max)];
}
return $passwordstr;
}
function getEnv(string $key)
{
if (empty($key) || !is_string($key)) {
return '';
}
$envPath = realpath(__DIR__ . '/../') . DIRECTORY_SEPARATOR . '.env';
if (!file_exists($envPath)) {
return false;
}
$envContent = file_get_contents($envPath);
preg_match_all("/^" . $key . "\s*=\s*(.*?)$/m", $envContent, $matchs);
return $matchs[1] ?: '';
}
function modifyEnv(array $data)
{
if (empty($data) || !is_array($data)) {
return false;
}
$envPath = realpath(__DIR__ . '/../') . DIRECTORY_SEPARATOR . '.env';
if (!file_exists($envPath)) {
return false;
}
$envContent = file_get_contents($envPath);
foreach ($data as $key => $val) {
$envContent = preg_replace("/^" . $key . "\s*=\s*(.*?)$/m", $key . "=" . $val, $envContent);
}
file_put_contents($envPath, $envContent);
return true;
}
function modifyMode($type)
{
$filePath = realpath(__DIR__ . '/../') . DIRECTORY_SEPARATOR . '/docker/php/php.conf';
if (!file_exists($filePath)) {
return false;
}
$envContent = file_get_contents($filePath);
$envContent = str_replace("#command=php bin/laravels start -i", "command=php bin/laravels start -i", $envContent);
$envContent = str_replace("#command=./bin/inotify ./app", "command=./bin/inotify ./app", $envContent);
if ($type == "dev") {
$envContent = str_replace("command=php bin/laravels start -i", "#command=php bin/laravels start -i", $envContent);
$this->modifyEnv([
'APP_DEBUG' => 'true'
]);
} else {
$envContent = str_replace("command=./bin/inotify ./app", "#command=./bin/inotify ./app", $envContent);
$this->modifyEnv([
'APP_DEBUG' => 'false'
]);
}
file_put_contents($filePath, $envContent);
return true;
}
}
$array = getopt('', ['port:', 'mode:']);
$loader = new runLoader();
if (isset($array['mode'])) {
$loader->modifyMode($array['mode']);
}
$data = [];
if (isset($array['port'])) {
$data['APP_PORT'] = $array['port'];
}
if ($data) {
$loader->modifyEnv($data);
}

View File

@@ -57,4 +57,4 @@ ignore_tags = ""
# sort the tags chronologically
date_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"
sort_commits = "newest"

100
cmd
View File

@@ -41,15 +41,31 @@ rand_string() {
fi
}
supervisorctl_restart() {
local RES=`run_exec php "supervisorctl update $1"`
restart_php() {
local RES=`run_exec php "supervisorctl update php"`
if [ -z "$RES" ]; then
run_exec php "supervisorctl restart $1"
RES=`run_exec php "supervisorctl restart php"`
fi
local IN=`echo $RES | grep "ERROR"`
if [[ "$IN" != "" ]]; then
$COMPOSE stop php
$COMPOSE start php
else
echo -e "$RES"
fi
}
switch_debug() {
local debug="false"
if [[ "$1" == "true" ]] || [[ "$1" == "dev" ]] || [[ "$1" == "open" ]]; then
debug="true"
fi
if [[ "$(env_get APP_DEBUG)" != "$debug" ]]; then
env_set APP_DEBUG "$debug"
restart_php
fi
}
check_docker() {
docker --version &> /dev/null
if [ $? -ne 0 ]; then
@@ -90,15 +106,17 @@ run_compile() {
if [ ! -d "./node_modules" ]; then
npm install
fi
run_exec php "php bin/run --mode=$type"
supervisorctl_restart php
if [ "$type" = "dev" ]; then
echo "<script>window.location.href=window.location.href.replace(/:\d+/, ':' + $(env_get APP_PORT))</script>" > ./index.html
env_set APP_DEV_PORT $(rand 20001 30000)
fi
switch_debug "$type"
#
if [ "$type" = "prod" ]; then
rm -rf "./public/js/build"
npx mix --production
echo "$(rand_string 16)" > ./public/js/hash
npx vite build -- fromcmd
else
npx mix watch --hot
npx vite -- fromcmd
fi
}
@@ -117,18 +135,16 @@ run_electron() {
if [ -d "./electron/dist" ]; then
rm -rf "./electron/dist"
fi
if [ -d "./electron/public" ] && [ "$argv" != "--nobuild" ]; then
if [ -d "./electron/public" ]; then
rm -rf "./electron/public"
fi
mkdir -p ./electron/public
cp ./electron/index.html ./electron/public/index.html
#
if [ "$argv" != "dev" ] && [ "$argv" != "--nobuild" ]; then
npx mix --production -- --env --electron
fi
if [ "$argv" == "dev" ]; then
run_exec php "php bin/run --mode=$argv"
supervisorctl_restart php
switch_debug "$argv"
else
mkdir -p ./electron/public
cp ./electron/index.html ./electron/public/index.html
npx vite build -- fromcmd electronBuild
fi
node ./electron/build.js $argv
}
@@ -243,15 +259,6 @@ arg_get() {
echo $value
}
is_arm() {
local get_arch=`arch`
if [[ $get_arch =~ "aarch" ]] || [[ $get_arch =~ "arm" ]]; then
echo "yes"
else
echo "no"
fi
}
####################################################################################
####################################################################################
####################################################################################
@@ -264,11 +271,6 @@ fi
if [ $# -gt 0 ]; then
if [[ "$1" == "init" ]] || [[ "$1" == "install" ]]; then
shift 1
# 判断架构
if [[ "$(is_arm)" == "yes" ]] && [[ -z "$(arg_get force)" ]]; then
echo -e "${Error} ${RedBG}暂不支持arm架构强制安装请使用./cmd install --force${Font}"
exit 1
fi
# 初始化文件
if [[ -n "$(arg_get relock)" ]]; then
rm -rf node_modules
@@ -295,7 +297,7 @@ if [ $# -gt 0 ]; then
exit 1
fi
[[ -z "$(env_get APP_KEY)" ]] && run_exec php "php artisan key:generate"
run_exec php "php bin/run --mode=prod"
switch_debug "false"
# 检查数据库
remaining=20
while [ ! -f "${cur_path}/docker/mysql/data/$(env_get DB_DATABASE)/db.opt" ]; do
@@ -315,19 +317,21 @@ if [ $# -gt 0 ]; then
# 设置初始化密码
res=`run_exec mariadb "sh /etc/mysql/repassword.sh"`
$COMPOSE up -d
supervisorctl_restart php
restart_php
echo -e "${OK} ${GreenBG} 安装完成 ${Font}"
echo -e "地址: http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
echo -e "$res"
elif [[ "$1" == "update" ]]; then
shift 1
run_mysql backup
git fetch --all
git reset --hard origin/$(git branch | sed -n -e 's/^\* \(.*\)/\1/p')
git pull
run_exec php "composer update"
if [[ -z "$(arg_get local)" ]]; then
git fetch --all
git reset --hard origin/$(git branch | sed -n -e 's/^\* \(.*\)/\1/p')
git pull
run_exec php "composer update"
fi
run_exec php "php artisan migrate"
supervisorctl_restart php
restart_php
$COMPOSE up -d
elif [[ "$1" == "uninstall" ]]; then
shift 1
@@ -343,6 +347,7 @@ if [ $# -gt 0 ]; then
;;
esac
$COMPOSE down
env_set APP_DEBUG "false"
rm -rf "./docker/mysql/data"
rm -rf "./docker/log/supervisor"
find "./storage/logs" -name "*.log" | xargs rm -rf
@@ -361,22 +366,22 @@ if [ $# -gt 0 ]; then
elif [[ "$1" == "url" ]]; then
shift 1
env_set APP_URL "$1"
supervisorctl_restart php
restart_php
echo -e "${OK} ${GreenBG} 修改成功 ${Font}"
elif [[ "$1" == "env" ]]; then
shift 1
if [ -n "$1" ]; then
env_set $1 "$2"
fi
supervisorctl_restart php
restart_php
echo -e "${OK} ${GreenBG} 修改成功 ${Font}"
elif [[ "$1" == "repassword" ]]; then
shift 1
run_exec mariadb "sh /etc/mysql/repassword.sh \"$@\""
elif [[ "$1" == "dev" ]] || [[ "$1" == "development" ]]; then
elif [[ "$1" == "serve" ]] || [[ "$1" == "dev" ]] || [[ "$1" == "development" ]]; then
shift 1
run_compile dev
elif [[ "$1" == "prod" ]] || [[ "$1" == "production" ]]; then
elif [[ "$1" == "build" ]] || [[ "$1" == "prod" ]] || [[ "$1" == "production" ]]; then
shift 1
run_compile prod
elif [[ "$1" == "appbuild" ]] || [[ "$1" == "buildapp" ]]; then
@@ -391,12 +396,8 @@ if [ $# -gt 0 ]; then
docker run -it --rm -v ${cur_path}:/home/node/apidoc kuaifan/apidoc -i app/Http/Controllers/Api -o public/docs
elif [[ "$1" == "debug" ]]; then
shift 1
if [[ "$@" == "close" ]]; then
env_set APP_DEBUG "false"
else
env_set APP_DEBUG "true"
fi
supervisorctl_restart php
switch_debug "$@"
echo "success"
elif [[ "$1" == "https" ]]; then
shift 1
if [[ "$@" == "auto" ]]; then
@@ -404,7 +405,7 @@ if [ $# -gt 0 ]; then
else
env_set APP_SCHEME "true"
fi
supervisorctl_restart php
restart_php
elif [[ "$1" == "artisan" ]]; then
shift 1
e="php artisan $@" && run_exec php "$e"
@@ -439,6 +440,9 @@ if [ $# -gt 0 ]; then
shift 1
run_exec php "php app/Models/clearHelper.php"
run_exec php "php artisan ide-helper:models -W"
elif [[ "$1" == "translate" ]]; then
shift 1
run_exec php "cd /var/www/language && php translate.php"
elif [[ "$1" == "test" ]]; then
shift 1
e="./vendor/bin/phpunit $@" && run_exec php "$e"

View File

@@ -13,7 +13,9 @@
"ext-gd": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-openssl": "*",
"ext-simplexml": "*",
"directorytree/ldaprecord-laravel": "^2.7",
"fideloper/proxy": "^4.4.1",
"fruitcake/laravel-cors": "^2.0.4",
"guanguans/notify": "^1.21.1",
@@ -79,5 +81,7 @@
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
"prefer-stable": true,
"repositories": {
}
}

1250
composer.lock generated

File diff suppressed because it is too large Load Diff

73
config/ldap.php Normal file
View File

@@ -0,0 +1,73 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default LDAP Connection Name
|--------------------------------------------------------------------------
|
| Here you may specify which of the LDAP connections below you wish
| to use as your default connection for all LDAP operations. Of
| course you may add as many connections you'd like below.
|
*/
'default' => env('LDAP_CONNECTION', 'default'),
/*
|--------------------------------------------------------------------------
| LDAP Connections
|--------------------------------------------------------------------------
|
| Below you may configure each LDAP connection your application requires
| access to. Be sure to include a valid base DN - otherwise you may
| not receive any results when performing LDAP search operations.
|
*/
'connections' => [
'default' => [
'hosts' => [env('LDAP_HOST', '127.0.0.1')],
'username' => env('LDAP_USERNAME', 'cn=user,dc=local,dc=com'),
'password' => env('LDAP_PASSWORD', 'secret'),
'port' => env('LDAP_PORT', 389),
'base_dn' => env('LDAP_BASE_DN', 'dc=local,dc=com'),
'timeout' => env('LDAP_TIMEOUT', 5),
'use_ssl' => env('LDAP_SSL', false),
'use_tls' => env('LDAP_TLS', false),
],
],
/*
|--------------------------------------------------------------------------
| LDAP Logging
|--------------------------------------------------------------------------
|
| When LDAP logging is enabled, all LDAP search and authentication
| operations are logged using the default application logging
| driver. This can assist in debugging issues and more.
|
*/
'logging' => (bool) env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| LDAP Cache
|--------------------------------------------------------------------------
|
| LDAP caching enables the ability of caching search results using the
| query builder. This is great for running expensive operations that
| may take many seconds to complete, such as a pagination request.
|
*/
'cache' => [
'enabled' => env('LDAP_CACHE', false),
'driver' => env('CACHE_DRIVER', 'file'),
],
];

View File

@@ -27,7 +27,7 @@ class ProjectTasksAddArchivedFollow extends Migration
// 更新数据
Project::whereNotNull('archived_at')->chunkById(100, function ($lists) {
foreach ($lists as $item) {
ProjectTask::whereProjectId($item->id)->whereArchivedAt(null)->update([
ProjectTask::whereProjectId($item->id)->whereArchivedAt(null)->change([
'archived_at' => $item->archived_at,
'archived_follow' => 1
]);

View File

@@ -18,7 +18,7 @@ class ProjectTasksUpdateSubtaskArchivedDelete extends Migration
->chunkById(100, function ($lists) {
/** @var ProjectTask $task */
foreach ($lists as $task) {
ProjectTask::whereParentId($task->id)->update([
ProjectTask::whereParentId($task->id)->change([
'archived_at' => $task->archived_at,
'archived_userid' => $task->archived_userid,
'archived_follow' => $task->archived_follow,
@@ -32,7 +32,7 @@ class ProjectTasksUpdateSubtaskArchivedDelete extends Migration
->chunkById(100, function ($lists) {
/** @var ProjectTask $task */
foreach ($lists as $task) {
ProjectTask::whereParentId($task->id)->update([
ProjectTask::whereParentId($task->id)->change([
'deleted_at' => $task->deleted_at,
]);
}

View File

@@ -25,7 +25,7 @@ class AddWebSocketDialogUsersInviterImportant extends Migration
\App\Models\WebSocketDialog::whereIn('group_type', ['project', 'task'])->chunkById(100, function ($lists) {
/** @var \App\Models\WebSocketDialog $item */
foreach ($lists as $item) {
\App\Models\WebSocketDialogUser::whereDialogId($item->id)->update([
\App\Models\WebSocketDialogUser::whereDialogId($item->id)->change([
'important' => 1,
]);
}

View File

@@ -14,16 +14,11 @@ class AddUsersBot extends Migration
*/
public function up()
{
$isAdd = false;
Schema::table('users', function (Blueprint $table) use (&$isAdd) {
Schema::table('users', function (Blueprint $table) {
if (!Schema::hasColumn('users', 'bot')) {
$isAdd = true;
$table->tinyInteger('bot')->nullable()->default(0)->after('email_verity')->comment('是否机器人');
}
});
if ($isAdd && User::count() > 0) {
User::botGetOrCreate('bot-manager');
}
}
/**

View File

@@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddUserBotsClear extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$isAdd = false;
Schema::table('user_bots', function (Blueprint $table) use (&$isAdd) {
if (!Schema::hasColumn('user_bots', 'clear_day')) {
$isAdd = true;
$table->smallInteger('clear_day')->nullable()->default(90)->after('bot_id')->comment('消息自动清理天数');
$table->timestamp('clear_at')->nullable()->after('clear_day')->comment('下一次清理时间');
}
});
if ($isAdd) {
\App\Models\UserBot::whereNull('clear_at')->update(['clear_at' => \Carbon\Carbon::now()->addDays(90)]);
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('user_bots', function (Blueprint $table) {
$table->dropColumn("clear_day");
$table->dropColumn("clear_at");
});
}
}

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddUserBotsWebhook extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('user_bots', function (Blueprint $table) {
if (!Schema::hasColumn('user_bots', 'webhook_url')) {
$table->string('webhook_url', 255)->nullable()->default('')->after('clear_at')->comment('消息webhook地址');
$table->integer('webhook_num')->nullable()->default(0)->after('webhook_url')->comment('消息webhook请求次数');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('user_bots', function (Blueprint $table) {
$table->dropColumn("webhook_url");
$table->dropColumn("webhook_num");
});
}
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddWebSocketDialogsAvatar extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('web_socket_dialogs', function (Blueprint $table) {
if (!Schema::hasColumn('web_socket_dialogs', 'avatar')) {
$table->string('avatar', 255)->nullable()->default('')->after('name')->comment('头像(群)');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('web_socket_dialogs', function (Blueprint $table) {
$table->dropColumn("avatar");
});
}
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class UpdateWebSocketDialogUsersUpdatedAt extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('web_socket_dialog_users', function (Blueprint $table) {
$pre = DB::connection()->getTablePrefix();
DB::statement("ALTER TABLE `{$pre}web_socket_dialog_users` MODIFY COLUMN `created_at` timestamp(3) NULL DEFAULT NULL AFTER `important`");
DB::statement("ALTER TABLE `{$pre}web_socket_dialog_users` MODIFY COLUMN `updated_at` timestamp(3) NULL DEFAULT NULL AFTER `created_at`");
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('web_socket_dialog_users', function (Blueprint $table) {
//
});
}
}

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateDeleteds extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('deleteds', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('type', 50)->nullable()->default('')->comment('删除的数据类型project、task、dialog');
$table->bigInteger('did')->nullable()->default(0)->comment('删除的数据ID');
$table->bigInteger('userid')->nullable()->default(0)->comment('关系会员ID');
$table->timestamp('created_at')->nullable();
$table->unique(['type', 'did', 'userid']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('deleted', function (Blueprint $table) {
//
});
}
}

View File

@@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class UpdateTableIndexes extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('web_socket_dialog_msg_reads', function (Blueprint $table) {
$table->index(['userid', 'read_at', 'dialog_id']);
});
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
$table->index('dialog_id');
$table->index('dialog_type');
$table->index('userid');
});
Schema::table('task_workers', function (Blueprint $table) {
$table->index('deleted_at');
});
Schema::table('project_tasks', function (Blueprint $table) {
$table->index(['parent_id', 'deleted_at']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
}
}

View File

@@ -2,9 +2,10 @@
use App\Models\User;
use App\Models\WebSocketDialog;
use Carbon\Carbon;
use Illuminate\Database\Migrations\Migration;
class GenerateWebSocketDialogsAllGroup extends Migration
class GenerateWebSocketDialogsDefaultGroup extends Migration
{
/**
* Run the migrations.
@@ -14,7 +15,16 @@ class GenerateWebSocketDialogsAllGroup extends Migration
public function up()
{
if (WebSocketDialog::count() > 0 && !WebSocketDialog::whereGroupType('all')->exists()) {
$userids = User::whereNull('disable_at')->pluck('userid')->toArray();
$botUser = User::botGetOrCreate('bot-manager');
if ($botUser) {
$dialog = WebSocketDialog::checkUserDialog($botUser, 1);
if ($dialog) {
$dialog->last_at = Carbon::now();
$dialog->save();
}
}
$userids = User::whereBot(0)->whereNull('disable_at')->pluck('userid')->toArray();
WebSocketDialog::createGroup("全体成员 All members", $userids, 'all');
}
}

View File

@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
class UpdateFilePshare extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
\App\Models\File::whereShare(1)->wherePshare(0)->update(['pshare' => DB::raw('id')]);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateApproveProcMsgs extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (!Schema::hasTable('approve_proc_msgs')) {
Schema::create('approve_proc_msgs', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('proc_inst_id')->nullable()->default(0)->comment('流程实例ID');
$table->bigInteger('userid')->nullable()->default(0)->comment('会员ID');
$table->bigInteger('msg_id')->nullable()->default(0)->comment('消息ID');
$table->timestamps();
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('approve_proc_msgs', function (Blueprint $table) {
Schema::dropIfExists('approve_proc_msgs');
});
}
}

View File

@@ -2,7 +2,6 @@
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
class UsersTableSeeder extends Seeder
@@ -71,8 +70,5 @@ class UsersTableSeeder extends Seeder
'updated_at' => seeders_at('2021-07-01 16:58:00'),
),
));
User::botGetOrCreate('bot-manager');
}
}

View File

@@ -4,6 +4,7 @@ namespace Database\Seeders;
use App\Models\User;
use App\Models\WebSocketDialog;
use Carbon\Carbon;
use Illuminate\Database\Seeder;
class WebSocketDialogsTableSeeder extends Seeder
@@ -223,7 +224,16 @@ class WebSocketDialogsTableSeeder extends Seeder
),
));
$userids = User::whereNull('disable_at')->pluck('userid')->toArray();
$botUser = User::botGetOrCreate('bot-manager');
if ($botUser) {
$dialog = WebSocketDialog::checkUserDialog($botUser, 1);
if ($dialog) {
$dialog->last_at = Carbon::now();
$dialog->save();
}
}
$userids = User::whereBot(0)->whereNull('disable_at')->pluck('userid')->toArray();
WebSocketDialog::createGroup("全体成员 All members", $userids, 'all');
}
}

View File

@@ -3,8 +3,12 @@ version: '3'
services:
php:
container_name: "dootask-php-${APP_ID}"
image: "kuaifan/php:swoole-8.0"
shm_size: "1024m"
image: "kuaifan/php:swoole-8.0.rc9"
shm_size: "2gb"
ulimits:
core:
soft: 0
hard: 0
volumes:
- ./docker/crontab/crontab.conf:/etc/supervisor/conf.d/crontab.conf
- ./docker/php/php.conf:/etc/supervisor/conf.d/php.conf
@@ -12,7 +16,6 @@ services:
- ./docker/log/supervisor:/var/log/supervisor
- ./:/var/www
environment:
TZ: "Asia/Shanghai"
LANG: "C.UTF-8"
MODE: "production"
MYSQL_HOST: "${DB_HOST}"
@@ -36,8 +39,6 @@ services:
volumes:
- ./docker/nginx:/etc/nginx/conf.d
- ./public:/var/www/public
environment:
TZ: "Asia/Shanghai"
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.3"
@@ -47,13 +48,12 @@ services:
- fileview
- drawio-webapp
- drawio-export
- minder
restart: unless-stopped
redis:
container_name: "dootask-redis-${APP_ID}"
image: "redis:alpine"
environment:
TZ: "Asia/Shanghai"
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.4"
@@ -62,14 +62,13 @@ services:
mariadb:
container_name: "dootask-mariadb-${APP_ID}"
image: "mariadb:10.7.3"
ports: # mysql ports item
- "33062:3306" # mysql ports value
ports:
- "${DB_OPEN_PORT:-}:3306"
volumes:
- ./docker/mysql/repassword.sh:/etc/mysql/repassword.sh
- ./docker/mysql/conf.d:/etc/mysql/conf.d
- ./docker/mysql/data:/var/lib/mysql
environment:
TZ: "Asia/Shanghai"
MYSQL_PREFIX: "${DB_PREFIX}"
MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}"
MYSQL_DATABASE: "${DB_DATABASE}"
@@ -82,11 +81,11 @@ services:
office:
container_name: "dootask-office-${APP_ID}"
image: "onlyoffice/documentserver:7.2.2.56"
image: "onlyoffice/documentserver:7.3.2.8"
volumes:
- ./docker/office/logs:/var/log/onlyoffice
- ./docker/office/data:/var/www/onlyoffice/Data
- ./docker/office/resources/api/documents/api.js:/var/www/onlyoffice/documentserver/web-apps/apps/api/documents/api.js
- ./docker/office/resources/require.js:/var/www/onlyoffice/documentserver/web-apps/vendor/requirejs/require.js
- ./docker/office/resources/documenteditor/main/resources/css/app.css:/var/www/onlyoffice/documentserver/web-apps/apps/documenteditor/main/resources/css/app.css
- ./docker/office/resources/documenteditor/mobile/css/964.css:/var/www/onlyoffice/documentserver/web-apps/apps/documenteditor/mobile/css/964.css
- ./docker/office/resources/presentationeditor/main/resources/css/app.css:/var/www/onlyoffice/documentserver/web-apps/apps/presentationeditor/main/resources/css/app.css
@@ -94,7 +93,6 @@ services:
- ./docker/office/resources/spreadsheeteditor/main/resources/css/app.css:/var/www/onlyoffice/documentserver/web-apps/apps/spreadsheeteditor/main/resources/css/app.css
- ./docker/office/resources/spreadsheeteditor/mobile/css/887.css:/var/www/onlyoffice/documentserver/web-apps/apps/spreadsheeteditor/mobile/css/887.css
environment:
TZ: "Asia/Shanghai"
JWT_ENABLED: false
networks:
extnetwork:
@@ -103,9 +101,9 @@ services:
fileview:
container_name: "dootask-fileview-${APP_ID}"
image: "kuaifan/fileview:4.1.0-SNAPSHOT-RC18"
image: "kuaifan/fileview:4.1.0-SNAPSHOT-RC20"
platform: linux/amd64
environment:
TZ: "Asia/Shanghai"
KK_CONTEXT_PATH: "/fileview"
KK_OFFICE_PREVIEW_SWITCH_DISABLED: true
KK_FILE_UPLOAD_ENABLED: true
@@ -117,7 +115,7 @@ services:
drawio-webapp:
container_name: "dootask-drawio-webapp-${APP_ID}"
image: "jgraph/drawio:16.6.1"
image: "jgraph/drawio:20.8.20"
volumes:
- ./docker/drawio/webapp/index.html:/usr/local/tomcat/webapps/draw/index.html
- ./docker/drawio/webapp/stencils:/usr/local/tomcat/webapps/draw/stencils
@@ -127,24 +125,47 @@ services:
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.8"
environment:
TZ: "Asia/Shanghai"
depends_on:
- drawio-export
restart: unless-stopped
drawio-export:
container_name: "dootask-drawio-export-${APP_ID}"
image: "jgraph/export-server"
image: "kuaifan/export-server:latest"
platform: linux/amd64
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.9"
environment:
TZ: "Asia/Shanghai"
volumes:
- ./docker/drawio/export/fonts:/usr/share/fonts/drawio
restart: unless-stopped
minder:
container_name: "dootask-minder-${APP_ID}"
image: "kuaifan/minder:0.1.1"
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.10"
restart: unless-stopped
approve:
container_name: "dootask-approve-${APP_ID}"
image: "hitosea2020/go-approve:latest"
environment:
TZ: "Asia/Shanghai"
MYSQL_HOST: "${DB_HOST}"
MYSQL_PORT: "${DB_PORT}"
MYSQL_DBNAME: "${DB_DATABASE}"
MYSQL_USERNAME: "${DB_USERNAME}"
MYSQL_PASSWORD: "${DB_PASSWORD}"
MYSQL_Prefix: "${DB_PREFIX}approve_"
networks:
extnetwork:
ipv4_address: "${APP_IPPR}.11"
depends_on:
- mariadb
restart: unless-stopped
networks:
extnetwork:
name: "dootask-networks-${APP_ID}"

View File

@@ -1,24 +1,3 @@
# Change
## js/diagramly/ElectronApp.js
- 隐藏文件中的无用菜单
## js/app.min.js
- 隐藏帮助菜单
- 取消未保存关闭窗口提示
- `EmbedFile.prototype.getTitle=...``EmbedFile.prototype.getTitle=function(){return this.desc.title||(urlParams.title?decodeURIComponent(urlParams.title):"")}`
- `c.insertTemplateEnabled&&!c.isOffline()&&this.addMenuItems(b,["insertTemplate"],d)``c.insertTemplateEnabled&&this.addMenuItems(b,["insertTemplate"],d)`
- `390:270``390:285`
- `!this.editorUi.isOffline()&&d.length<=c/4?(f=b-Math.ceil((e-c/4)/c),mxUtils.get(ICONSEARCH_PATH``d.length<=c/4?(f=b-Math.ceil((e-c/4)/c),mxUtils.get(ICONSEARCH_PATH`
## index.html
- 隐藏加载中的提示
- 加入
```js
window.EXPORT_URL = window.location.origin + "/drawio/export/";
window.DRAWIO_LIGHTBOX_URL = window.location.origin + "/drawio/webapp";
setInterval(function() {window.ICONSEARCH_PATH = window.location.origin + "/drawio/iconsearch";}, 1000)
```
diff https://github.com/jgraph/drawio/tree/1a858166fb4f1330cf23e58941e3fbec1dcd16f8

View File

@@ -5,10 +5,10 @@
<title>Flowchart Maker &amp; Online Diagram Software</title>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="Description" content="diagrams.net is free online diagram software for making flowcharts, process diagrams, org charts, UML, ER and network diagrams">
<meta name="Keywords" content="diagram, online, flow chart, flowchart maker, uml, erd">
<meta itemprop="name" content="diagrams.net - free flowchart maker and diagrams online">
<meta itemprop="description" content="diagrams.net is a free online diagramming application and flowchart maker . You can use it to create UML, entity relationship,
<meta name="Description" content="draw.io is free online diagram software for making flowcharts, process diagrams, org charts, UML, ER and network diagrams">
<meta name="Keywords" content="drawio, diagram, online, flow chart, flowchart maker, uml, erd">
<meta itemprop="name" content="draw.io - free flowchart maker and diagrams online">
<meta itemprop="description" content="draw.io is a free online diagramming application and flowchart maker . You can use it to create UML, entity relationship,
org charts, BPMN and BPM, database schema and networks. Also possible are telecommunication network, workflow, flowcharts, maps overlays and GIS, electronic
circuit and social network diagrams.">
<meta itemprop="image" content="https://lh4.googleusercontent.com/-cLKEldMbT_E/Tx8qXDuw6eI/AAAAAAAAAAs/Ke0pnlk8Gpg/w500-h344-k/BPMN%2Bdiagram%2Brc2f.png">
@@ -17,12 +17,11 @@
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#d89000">
<script type="text/javascript">
window.SERVER_HOME_URL = window.location.origin + "/"
window.EXPORT_URL = window.SERVER_HOME_URL + "drawio/export/";
window.DRAWIO_LIGHTBOX_URL = window.SERVER_HOME_URL + "drawio/webapp";
setInterval(function() {window.ICONSEARCH_PATH = window.SERVER_HOME_URL + "drawio/iconsearch";}, 1000)
window.EXPORT_URL = window.location.origin + "/drawio/export/";
window.DRAWIO_LIGHTBOX_URL = window.location.origin + "/drawio/webapp";
setInterval(function() {window.ICONSEARCH_PATH = window.location.origin + "/drawio/iconsearch";}, 1000)
/**
/**
* URL Parameters and protocol description are here:
*
* https://desk.draw.io/support/solutions/articles/16000042546-what-url-parameters-are-supported
@@ -48,7 +47,7 @@
for (var i = 0; i < params.length; i++)
{
idx = params[i].indexOf('=');
var idx = params[i].indexOf('=');
if (idx > 0)
{
@@ -143,7 +142,7 @@
/**
* Synchronously adds scripts to the page.
*/
function mxscript(src, onLoad, id, dataAppKey, noWrite)
function mxscript(src, onLoad, id, dataAppKey, noWrite, onError)
{
var defer = onLoad == null && !noWrite;
@@ -179,6 +178,14 @@
};
}
if (onError != null)
{
s.onerror = function(e)
{
onError('Failed to load ' + src, e);
};
}
var t = document.getElementsByTagName('script')[0];
if (t != null)
@@ -267,6 +274,8 @@
mxForceIncludes = true;
}
mxForceIncludes = false;
mxscript(drawDevUrl + 'js/PreConfig.js');
mxscript(drawDevUrl + 'js/diagramly/Init.js');
mxscript(geBasePath + '/Init.js');
@@ -353,11 +362,14 @@
};
</script>
<link rel="chrome-webstore-item" href="https://chrome.google.com/webstore/detail/plgmlhohecdddhbmmkncjdmlhcmaachm">
<link rel="stylesheet" type="text/css" href="js/croppie/croppie.min.css">
<link rel="apple-touch-icon" sizes="180x180" href="images/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="images/favicon-16x16.png">
<link rel="mask-icon" href="images/safari-pinned-tab.svg" color="#d89000">
<link rel="stylesheet" type="text/css" href="styles/grapheditor.css">
<link rel="preconnect" href="https://storage.googleapis.com">
<link rel="canonical" href="https://app.diagrams.net">
<link rel="manifest" href="images/manifest.json">
<link rel="shortcut icon" href="favicon.ico">
<style type="text/css">
body { overflow:hidden; }
div.picker { z-index: 10007; }
@@ -380,8 +392,8 @@
padding-top:0px;
}
.geEditor *:not(.geScrollable)::-webkit-scrollbar {
width:14px;
height:14px;
width:10px;
height:10px;
}
.geEditor ::-webkit-scrollbar-track {
background-clip:padding-box;
@@ -411,9 +423,6 @@
cursor:pointer;
margin:5px;
}
.geDialog h2 {
line-height: 1.2;
}
</style>
<!-- Workaround for binary XHR in IE 9/10, see App.loadUrl -->
<!--[if (IE 9)|(IE 10)]><!-->
@@ -434,7 +443,7 @@
<div class="geBlock">
<h1>Flowchart Maker and Online Diagram Software</h1>
<p>
diagrams.net (formerly draw.io) is free online diagram software. You can use it as a flowchart maker, network diagram software, to create UML online, as an ER diagram tool,
draw.io is free online diagram software. You can use it as a flowchart maker, network diagram software, to create UML online, as an ER diagram tool,
to design database schema, to build BPMN online, as a circuit diagram maker, and more. draw.io can import .vsdx, Gliffy&trade; and Lucidchart&trade; files .
</p>
<h2 id="geStatus">Loading...</h2>
@@ -447,32 +456,17 @@
/**
* Main
*/
if (navigator.userAgent != null && navigator.userAgent.toLowerCase().
indexOf(' electron/') >= 0 && typeof process !== 'undefined' && process.versions.electron < 5)
if (urlParams['dev'] != '1' && typeof document.createElement('canvas').getContext === "function")
{
// Redirects old Electron app to latest version
var div = document.getElementById('geInfo');
if (div != null)
window.addEventListener('load', function()
{
div.innerHTML = '<center><h2>You are using an out of date version of this app.<br>Please download the latest version ' +
'<a href="https://github.com/jgraph/drawio-desktop/releases/latest" target="_blank">here</a>.</h2></center>';
}
mxWinLoaded = true;
checkAllLoaded();
});
}
else
{
if (urlParams['dev'] != '1' && typeof document.createElement('canvas').getContext === "function")
{
window.addEventListener('load', function()
{
mxWinLoaded = true;
checkAllLoaded();
});
}
else
{
App.main();
}
App.main();
}
</script>
</body>

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +1,9 @@
window.PLUGINS_BASE_PATH = '.';
window.TEMPLATE_PATH = 'templates';
window.DRAW_MATH_URL = 'math';
window.DRAW_MATH_URL = 'math/es5';
window.DRAWIO_BASE_URL = '.'; //Prevent access to online website since it is not allowed
FeedbackDialog.feedbackUrl = 'https://log.draw.io/email';
EditorUi.draftSaveDelay = 5000;
//Disables eval for JS (uses shapes-14-6-5.min.js)
mxStencilRegistry.allowEval = false;
@@ -60,10 +60,18 @@ mxStencilRegistry.allowEval = false;
if (file)
{
file.updateFileData();
xml = file.getData();
xml = editorUi.getFileData(true, null, null, null, null, false,
null, null, null, false, true);
title = file.title;
}
var extras = {globalVars: editorUi.editor.graph.getExportVariables()};
if (Graph.translateDiagram)
{
extras.diagramLanguage = Graph.diagramLanguage;
}
new mxElectronRequest('export', {
print: true,
format: 'pdf',
@@ -78,6 +86,7 @@ mxStencilRegistry.allowEval = false;
sheetsAcross: sheetsAcross,
sheetsDown: sheetsDown,
scale: zoom,
extras: JSON.stringify(extras),
fileTitle: title
}).send(function(){}, function(){});
};
@@ -85,13 +94,15 @@ mxStencilRegistry.allowEval = false;
var oldWindowOpen = window.open;
window.open = async function(url)
{
if (url != null && url.startsWith('http'))
// Only open a native electron window when url is empty. We use this in our code in several places.
if (url == null)
{
await requestSync({action: 'openExternal', url: url});
return oldWindowOpen(url);
}
else
{
return oldWindowOpen(url);
// Open external will filter urls based on their protocol
await requestSync({action: 'openExternal', url: url});
}
}
@@ -99,53 +110,93 @@ mxStencilRegistry.allowEval = false;
App.main = async function()
{
// Set AutoSave delay
var draftSaveDelay = mxSettings.getDraftSaveDelay();
if (draftSaveDelay != null)
{
EditorUi.draftSaveDelay = draftSaveDelay * 1000;
EditorUi.enableDrafts = draftSaveDelay > 0;
}
//Load desktop plugins
var plugins = (mxSettings.settings != null) ? mxSettings.getPlugins() : null;
App.initPluginCallback();
if (plugins != null && plugins.length > 0)
{
for (var i = 0; i < plugins.length; i++)
// Workaround for body not defined if plugins are used in dev mode
if (urlParams['dev'] == '1')
{
try
EditorUi.debug('App.main', 'Skipped plugins', plugins);
}
else
{
for (var i = 0; i < plugins.length; i++)
{
if (plugins[i].startsWith('/plugins/'))
try
{
plugins[i] = '.' + plugins[i];
if (plugins[i].indexOf('..') >= 0)
{
continue;
}
else if (plugins[i].startsWith('/plugins/'))
{
plugins[i] = '.' + plugins[i];
}
else if (plugins[i].startsWith('plugins/'))
{
plugins[i] = './' + plugins[i];
}
else
{
let pluginFile = await requestSync({
action: 'getPluginFile',
plugin: plugins[i]
});
if (pluginFile != null)
{
plugins[i] = 'file://' + pluginFile;
}
else
{
continue; //skip not found files
}
}
try
{
mxscript(plugins[i]);
}
catch (e)
{
// ignore
}
}
else if (plugins[i].startsWith('plugins/'))
catch (e)
{
plugins[i] = './' + plugins[i];
// ignore
}
//Support old plugins added using file:// workaround
else if (!plugins[i].startsWith('file://'))
{
let appFolder = await requestSync('getAppDataFolder');
let pluginsFile = await requestSync({
action: 'checkFileExists',
pathParts: [appFolder, '/plugins', plugins[i]]
});
if (pluginsFile.exists)
{
plugins[i] = 'file://' + pluginsFile.path;
}
else
{
continue; //skip not found files
}
}
mxscript(plugins[i]);
}
catch (e)
{
// ignore
}
}
}
//Remove old relaxed CSP and add strict one
// var allMeta = document.getElementsByTagName('meta');
//
// for (var i = 0; i < allMeta.length; i++)
// {
// if (allMeta[i].getAttribute('http-equiv') == 'Content-Security-Policy')
// {
// allMeta[i].parentNode.removeChild(allMeta[i]);
// }
//
// break;
// }
//
// mxmeta(null, 'default-src \'self\'; connect-src \'self\' https://fonts.googleapis.com https://fonts.gstatic.com; img-src * data:; media-src *; font-src *; style-src \'self\' \'unsafe-inline\' https://fonts.googleapis.com', 'Content-Security-Policy');
//Disable web plugins loading
urlParams['plugins'] = '0';
origAppMain.apply(this, arguments);
@@ -208,8 +259,8 @@ mxStencilRegistry.allowEval = false;
// Replaces file menu to replace openFrom menu with open and rename downloadAs to export
this.put('file', new Menu(mxUtils.bind(this, function(menu, parent)
{
this.addMenuItems(menu, ['import'], parent);
this.addSubmenu('exportAs', menu, parent);
this.addMenuItems(menu, ['import'], parent);
this.addSubmenu('exportAs', menu, parent);
menu.addSeparator(parent);
this.addSubmenu('embed', menu, parent);
menu.addSeparator(parent);
@@ -229,7 +280,7 @@ mxStencilRegistry.allowEval = false;
}
}
this.addMenuItems(menu, ['-', 'pageSetup', 'print'], parent);
this.addMenuItems(menu, ['-', 'pageSetup', 'print'], parent);
// LATER: Find API for application.quit
})));
};
@@ -267,25 +318,25 @@ mxStencilRegistry.allowEval = false;
var editorUi = this;
var graph = this.editor.graph;
window.__emt_isModified = function()
electron.registerMsgListener('isModified', () =>
{
if (editorUi.getCurrentFile())
const currentFile = editorUi.getCurrentFile();
let reply = {isModified: false, draftPath: null};
if (currentFile != null)
{
return editorUi.getCurrentFile().isModified()
reply.isModified = currentFile.isModified();
reply.draftPath = EditorUi.enableDrafts && currentFile.fileObject? currentFile.fileObject.draftFileName : null;
}
return false
};
electron.sendMessage('isModified-result', reply);
});
window.__emt_removeDraft = function()
electron.registerMsgListener('removeDraft', () =>
{
var currentFile = editorUi.getCurrentFile();
if (currentFile != null && EditorUi.enableDrafts)
{
currentFile.removeDraft();
}
};
editorUi.getCurrentFile().removeDraft();
electron.sendMessage('draftRemoved', {});
});
// Adds support for libraries
this.actions.addAction('newLibrary...', mxUtils.bind(this, function()
@@ -588,7 +639,19 @@ mxStencilRegistry.allowEval = false;
editorUi.actions.addAction('plugins...', function()
{
editorUi.showDialog(new PluginsDialog(editorUi, function(callback)
var pluginsMap = {};
//Initialize it with plugins in settings
var plugins = (mxSettings.settings != null) ? mxSettings.getPlugins() : null;
if (plugins != null)
{
for (var i = 0; i < plugins.length; i++)
{
pluginsMap[plugins[i]] = true;
}
}
editorUi.showDialog(new PluginsDialog(editorUi, async function(callback)
{
var div = document.createElement('div');
@@ -602,9 +665,13 @@ mxStencilRegistry.allowEval = false;
for (var i = 0; i < App.publicPlugin.length; i++)
{
var p = App.publicPlugin[i];
if (pluginsMap[App.pluginRegistry[p]]) continue;
var option = document.createElement('option');
mxUtils.write(option, App.publicPlugin[i]);
option.value = App.publicPlugin[i];
mxUtils.write(option, p);
option.value = p;
pluginsSelect.appendChild(option);
}
@@ -616,63 +683,89 @@ mxStencilRegistry.allowEval = false;
mxUtils.write(title, mxResources.get('extPlugins') + ': ');
div.appendChild(title);
var extPluginsBtn = mxUtils.button(mxResources.get('selectFile') + '...', async function()
if (await requestSync('isPluginsEnabled'))
{
var lastDir = localStorage.getItem('.lastPluginDir');
var extPluginsBtn = mxUtils.button(mxResources.get('selectFile') + '...', async function()
{
var warningMsgs = mxResources.get('pluginWarning').split('\\n');
var warningMsg = warningMsgs.pop(); //Last line in the message
var paths = await requestSync({
action: 'showOpenDialog',
defaultPath: lastDir || (await requestSync('getDocumentsFolder')),
filters: [
{ name: 'draw.io Plugins', extensions: ['js'] },
{ name: 'All Files', extensions: ['*'] }
],
properties: ['openFile']
if (!warningMsg)
{
warningMsg = warningMsgs.pop();
}
if (!confirm(warningMsg))
{
return;
}
var lastDir = localStorage.getItem('.lastPluginDir');
var paths = await requestSync({
action: 'showOpenDialog',
defaultPath: lastDir || (await requestSync('getDocumentsFolder')),
filters: [
{ name: 'draw.io Plugins', extensions: ['js'] },
{ name: 'All Files', extensions: ['*'] }
],
properties: ['openFile']
});
if (paths !== undefined && paths[0] != null)
{
try
{
let ret = await requestSync({
action: 'installPlugin',
filePath: paths[0]
});
localStorage.setItem('.lastPluginDir', ret.selDir);
callback(ret.pluginName);
editorUi.hideDialog();
}
catch (e)
{
if (e.message == 'fileExists')
{
alert(mxResources.get('fileExists'));
}
else
{
alert('Adding plugin failed.');
}
}
}
});
if (paths !== undefined && paths[0] != null)
{
try
{
let ret = await requestSync({
action: 'installPlugin',
filePath: paths[0]
});
localStorage.setItem('.lastPluginDir', ret.selDir);
callback(ret.pluginName);
editorUi.hideDialog();
}
catch (e)
{
if (e.message == 'fileExists')
{
alert(mxResources.get('fileExists'));
}
else
{
alert('Adding plugin failed.');
}
}
}
});
extPluginsBtn.className = 'geBtn';
div.appendChild(extPluginsBtn);
extPluginsBtn.className = 'geBtn';
div.appendChild(extPluginsBtn);
}
else
{
title = document.createElement('span');
mxUtils.write(title, mxResources.get('pluginsDisabled'));
div.appendChild(title);
}
var dlg = new CustomDialog(editorUi, div, mxUtils.bind(this, function()
{
callback(App.pluginRegistry[pluginsSelect.value]);
var newP = App.pluginRegistry[pluginsSelect.value];
pluginsMap[newP] = true;
callback(newP);
}));
editorUi.showDialog(dlg.container, 300, 125, true, true);
},
async function(plugin)
{
delete pluginsMap[plugin];
await requestSync({
action: 'uninstallPlugin',
plugin: plugin
});
}).container, 360, 225, true, false);
}, true).container, 360, 225, true, false);
});
}
@@ -829,25 +922,24 @@ mxStencilRegistry.allowEval = false;
if (file.fileObject != null)
{
var title = file.fileObject.path;
if (title.length > 100)
{
title = '...' + title.substr(title.length - 97);
}
this.addRecent({id: file.fileObject.path, title: title});
file.addToRecent();
await requestSync({
action: 'watchFile',
path: file.fileObject.path,
listener: mxUtils.bind(this, function(curr, prev)
{
EditorUi.debug('EditorUi.watchFile', [this],
'file', [file], 'stat', [file.stat],
'curr', [curr], 'prev', [prev],
'inConflictState', file.inConflictState,
'unwatchedSaves', file.unwatchedSaves);
//File is changed (not just accessed) && File is not already in a conflict state
if (curr.mtimeMs != prev.mtimeMs && !file.inConflictState)
{
//Ignore our own changes
if (file.unwatchedSaves || (file.state != null && file.stat.mtimeMs == curr.mtimeMs))
if (file.unwatchedSaves || (file.stat != null && file.stat.mtimeMs == curr.mtimeMs))
{
file.unwatchedSaves = false;
return;
@@ -855,26 +947,18 @@ mxStencilRegistry.allowEval = false;
file.inConflictState = true;
this.showError(mxResources.get('externalChanges'),
mxResources.get('fileChangedSyncDialog'),
mxResources.get('synchronize'), mxUtils.bind(this, function()
file.addConflictStatus(null, mxUtils.bind(this, function()
{
file.ui.editor.setStatus(mxUtils.htmlEntities(
mxResources.get('updatingDocument')));
file.synchronizeFile(mxUtils.bind(this, function()
{
if (this.spinner.spin(document.body, mxResources.get('updatingDocument')))
{
file.synchronizeFile(mxUtils.bind(this, function()
{
this.spinner.stop();
}), mxUtils.bind(this, function(err)
{
file.handleFileError(err, true);
}));
}
}), null, null, null,
mxResources.get('cancel'), mxUtils.bind(this, function()
file.handleFileSuccess(false);
}), mxUtils.bind(this, function(err)
{
this.hideDialog();
file.handleFileError(null, false);
}), 340, 150);
file.handleFileError(err, true);
}));
}));
}
})
});
@@ -964,6 +1048,21 @@ mxStencilRegistry.allowEval = false;
}
};
EditorUi.prototype.normalizeFilename = function(title, defaultExtension)
{
var tokens = title.split('.');
var ext = (tokens.length > 1) ? tokens[tokens.length - 1] : '';
defaultExtension = (defaultExtension != null) ? defaultExtension : 'drawio';
if (tokens.length == 1 || mxUtils.indexOf(['xml',
'html', 'drawio', 'png', 'svg'], ext) < 0)
{
tokens.push(defaultExtension);
}
return tokens.join('.');
};
//In order not to repeat the logic for opening a file, we collect files information here and use them in openLocalFile
var origOpenFiles = EditorUi.prototype.openFiles;
var openFilesMap = {};
@@ -1160,7 +1259,7 @@ mxStencilRegistry.allowEval = false;
{
this.ui.readGraphFile(mxUtils.bind(this, function(fileEntry, data, stat, name, isModified)
{
var file = new LocalFile(this, data, '');
var file = new LocalFile(this.ui, data, '');
file.stat = stat;
file.setModified(isModified? true : false);
success(file);
@@ -1276,6 +1375,12 @@ mxStencilRegistry.allowEval = false;
LocalFile.prototype.save = function(revision, success, error, unloading, overwrite)
{
if (!this.isEditable())
{
this.saveAs(this.title, success, error);
return;
}
DrawioFile.prototype.save.apply(this, [revision, mxUtils.bind(this, function()
{
this.saveFile(revision, mxUtils.bind(this, function()
@@ -1307,23 +1412,6 @@ mxStencilRegistry.allowEval = false;
this.editable = editable;
};
LocalFile.prototype.getFilename = function()
{
var filename = this.title;
// Adds default extension
if (filename.length > 0 && (!/(\.xml)$/i.test(filename) && !/(\.html)$/i.test(filename) &&
!/(\.svg)$/i.test(filename) && !/(\.png)$/i.test(filename) && !/(\.drawio)$/i.test(filename)))
{
filename += '.drawio';
}
return filename;
};
// Prototype inheritance needs new functions to be added to subclasses
LocalLibrary.prototype.getFilename = LocalFile.prototype.getFilename;
LocalFile.prototype.saveFile = async function(revision, success, error, unloading, overwrite)
{
//Safeguard in case saveFile is called from online code in the future
@@ -1420,83 +1508,114 @@ mxStencilRegistry.allowEval = false;
}
});
if (this.fileObject == null)
try
{
var lastDir = localStorage.getItem('.lastSaveDir');
var name = this.getFilename();
var ext = null;
if (name != null)
if (this.fileObject == null)
{
var idx = name.lastIndexOf('.');
var lastDir = localStorage.getItem('.lastSaveDir');
var name = this.ui.normalizeFilename(this.getTitle(),
this.constructor == LocalLibrary ? 'xml' : null);
var ext = null;
if (idx > 0)
if (name != null)
{
ext = name.substring(idx + 1);
name = name.substring(0, idx);
var idx = name.lastIndexOf('.');
if (idx > 0)
{
ext = name.substring(idx + 1);
}
}
var path = await requestSync({
action: 'showSaveDialog',
defaultPath: (lastDir || (await requestSync('getDocumentsFolder'))) + '/' + name,
filters: this.ui.createFileSystemFilters(ext)
});
if (path != null)
{
localStorage.setItem('.lastSaveDir', await requestSync({action: 'dirname', path: path}));
this.fileObject = new Object();
this.fileObject.path = path;
this.fileObject.name = path.replace(/^.*[\\\/]/, '');
this.fileObject.type = 'utf-8';
this.title = this.fileObject.name;
this.addToRecent();
fn();
}
else
{
this.ui.spinner.stop();
}
}
var path = await requestSync({
action: 'showSaveDialog',
defaultPath: (lastDir || (await requestSync('getDocumentsFolder'))) + '/' + name,
filters: this.ui.createFileSystemFilters(ext)
});
if (path != null)
{
localStorage.setItem('.lastSaveDir', await requestSync({action: 'dirname', path: path}));
this.fileObject = new Object();
this.fileObject.path = path;
this.fileObject.name = path.replace(/^.*[\\\/]/, '');
this.fileObject.type = 'utf-8';
else
{
fn();
}
else
{
this.ui.spinner.stop();
}
}
else
catch (e)
{
fn();
error(e);
}
}
};
LocalFile.prototype.saveAs = async function(title, success, error)
LocalFile.prototype.addToRecent = function()
{
var lastDir = localStorage.getItem('.lastSaveDir');
var name = this.getFilename();
var ext = null;
if (this.fileObject == null) return;
if (name == '' && this.fileObject != null && this.fileObject.name != null)
var title = this.fileObject.path;
if (title.length > 100)
{
name = this.fileObject.name;
var idx = name.lastIndexOf('.');
if (idx > 0)
{
ext = name.substring(idx + 1);
name = name.substring(0, idx);
}
title = '...' + title.substr(title.length - 97);
}
var path = await requestSync({
action: 'showSaveDialog',
defaultPath: (lastDir || (await requestSync('getDocumentsFolder'))) + '/' + name,
filters: this.ui.createFileSystemFilters(ext)
});
this.ui.addRecent({id: this.fileObject.path, title: title});
};
if (path != null)
{
localStorage.setItem('.lastSaveDir', await requestSync({action: 'dirname', path: path}));
this.fileObject = new Object();
this.fileObject.path = path;
this.fileObject.name = path.replace(/^.*[\\\/]/, '');
this.fileObject.type = 'utf-8';
this.setEditable(true); //In case original file is read only
this.save(false, success, error, null, true);
LocalFile.prototype.saveAs = async function(title, success, error)
{
try
{
var lastDir = localStorage.getItem('.lastSaveDir');
var name = this.ui.normalizeFilename(this.getTitle(),
this.constructor == LocalLibrary ? 'xml' : null);
var ext = null;
if (name != null)
{
var idx = name.lastIndexOf('.');
if (idx > 0)
{
ext = name.substring(idx + 1);
}
}
var path = await requestSync({
action: 'showSaveDialog',
defaultPath: (lastDir || (await requestSync('getDocumentsFolder'))) + '/' + name,
filters: this.ui.createFileSystemFilters(ext)
});
if (path != null)
{
localStorage.setItem('.lastSaveDir', await requestSync({action: 'dirname', path: path}));
this.fileObject = new Object();
this.fileObject.path = path;
this.fileObject.name = path.replace(/^.*[\\\/]/, '');
this.fileObject.type = 'utf-8';
this.title = this.fileObject.name;
this.addToRecent();
this.setEditable(true); //In case original file is read only
this.save(false, success, error, null, true);
}
}
catch (e)
{
error(e);
}
};
@@ -1543,6 +1662,11 @@ mxStencilRegistry.allowEval = false;
}
};
LocalLibrary.prototype.addToRecent = function()
{
// do nothing
};
/**
* Loads the given file handle as a local file.
*/
@@ -1710,155 +1834,16 @@ mxStencilRegistry.allowEval = false;
electron.sendMessage('toggleSpellCheck');
}
var origUpdateHeader = App.prototype.updateHeader;
App.prototype.updateHeader = function()
App.prototype.toggleStoreBkp = function()
{
origUpdateHeader.apply(this, arguments);
if (urlParams['winCtrls'] != '1')
{
return;
}
document.querySelectorAll('.geStatus').forEach(i => i.style.webkitAppRegion = 'no-drag');
var menubarContainer = document.querySelector('.geMenubarContainer');
if (urlParams['sketch'] == '1')
{
menubarContainer = this.titlebar;
}
menubarContainer.style.webkitAppRegion = 'drag';
//Add window control buttons
this.windowControls = document.createElement('div');
this.windowControls.id = 'geWindow-controls';
this.windowControls.innerHTML =
'<div class="button" id="min-button">' +
' <svg width="10" height="1" viewBox="0 0 11 1">' +
' <path d="m11 0v1h-11v-1z" stroke-width=".26208"/>' +
' </svg>' +
'</div>' +
'<div class="button" id="max-button">' +
' <svg width="10" height="10" viewBox="0 0 10 10">' +
' <path d="m10-1.6667e-6v10h-10v-10zm-1.001 1.001h-7.998v7.998h7.998z" stroke-width=".25" />' +
' </svg>' +
'</div>' +
'<div class="button" id="restore-button">' +
' <svg width="10" height="10" viewBox="0 0 11 11">' +
' <path' +
' d="m11 8.7978h-2.2021v2.2022h-8.7979v-8.7978h2.2021v-2.2022h8.7979zm-3.2979-5.5h-6.6012v6.6011h6.6012zm2.1968-2.1968h-6.6012v1.1011h5.5v5.5h1.1011z"' +
' stroke-width=".275" />' +
' </svg>' +
'</div>' +
'<div class="button" id="close-button">' +
' <svg width="10" height="10" viewBox="0 0 12 12">' +
' <path' +
' d="m6.8496 6 5.1504 5.1504-0.84961 0.84961-5.1504-5.1504-5.1504 5.1504-0.84961-0.84961 5.1504-5.1504-5.1504-5.1504 0.84961-0.84961 5.1504 5.1504 5.1504-5.1504 0.84961 0.84961z"' +
' stroke-width=".3" />' +
' </svg>' +
'</div>';
if (uiTheme == 'atlas')
{
this.windowControls.style.top = '9px';
}
else if (urlParams['sketch'] == '1')
{
this.windowControls.style.top = '-1px';
}
menubarContainer.appendChild(this.windowControls);
var handleDarkModeChange = mxUtils.bind(this, function ()
{
if (uiTheme == 'atlas' || Editor.isDarkMode())
{
this.windowControls.style.fill = 'white';
document.querySelectorAll('#geWindow-controls .button').forEach(b => b.className = 'button dark');
}
else
{
this.windowControls.style.fill = '#999';
document.querySelectorAll('#geWindow-controls .button').forEach(b => b.className = 'button white');
}
});
handleDarkModeChange();
this.addListener('darkModeChanged', handleDarkModeChange);
if (this.appIcon != null)
{
this.appIcon.style.webkitAppRegion = 'no-drag';
}
if (this.menubar != null)
{
this.menubar.container.style.webkitAppRegion = 'no-drag';
if (uiTheme == 'atlas')
{
this.menubar.container.style.width = 'fit-content';
}
if (this.menubar.langIcon != null)
{
this.menubar.langIcon.parentNode.removeChild(this.menubar.langIcon);
menubarContainer.appendChild(this.menubar.langIcon);
}
}
window.onbeforeunload = async (event) => {
/* If window is reloaded, remove win event listeners
(DOM element listeners get auto garbage collected but not
Electron win listeners as the win is not dereferenced unless closed) */
await requestSync({action: 'windowAction', method: 'removeAllListeners'});
}
// Make minimise/maximise/restore/close buttons work when they are clicked
document.getElementById('min-button').addEventListener("click", async event => {
await requestSync({action: 'windowAction', method: 'minimize'});
});
document.getElementById('max-button').addEventListener("click", async event => {
await requestSync({action: 'windowAction', method: 'maximize'});
});
document.getElementById('restore-button').addEventListener("click", async event => {
await requestSync({action: 'windowAction', method: 'unmaximize'});
});
document.getElementById('close-button').addEventListener("click", async event => {
await requestSync({action: 'windowAction', method: 'close'});
});
// Toggle maximise/restore buttons when maximisation/unmaximisation occurs
toggleMaxRestoreButtons();
electron.registerMsgListener('maximize', toggleMaxRestoreButtons)
electron.registerMsgListener('unmaximize', toggleMaxRestoreButtons)
electron.registerMsgListener('resize', toggleMaxRestoreButtons)
async function toggleMaxRestoreButtons() {
if (await requestSync({action: 'windowAction', method: 'isMaximized'})) {
document.body.classList.add('geMaximized');
} else {
document.body.classList.remove('geMaximized');
}
}
electron.sendMessage('toggleStoreBkp');
}
var origUpdateDocumentTitle = App.prototype.updateDocumentTitle;
App.prototype.updateDocumentTitle = function()
App.prototype.openDevTools = function()
{
origUpdateDocumentTitle.apply(this, arguments);
electron.sendMessage('openDevTools');
}
if (this.titlebar != null && this.titlebar.firstChild != null)
{
this.titlebar.firstChild.innerHTML = mxUtils.htmlEntities(document.title);
}
};
/**
* Copies the given cells and XML to the clipboard as an embedded image.
*/
@@ -1961,80 +1946,13 @@ mxStencilRegistry.allowEval = false;
}
//Direct export to pdf
EditorUi.prototype.createDownloadRequest = function(filename, format, ignoreSelection, base64, transparent,
currentPage, scale, border, grid, includeXml)
EditorUi.prototype.createDownloadRequest = function(filename, format, ignoreSelection,
base64, transparent, currentPage, scale, border, grid, includeXml, pageRange, w, h)
{
var graph = this.editor.graph;
var bounds = graph.getGraphBounds();
var params = this.downloadRequestBuilder(filename, format, ignoreSelection,
base64, transparent, currentPage, scale, border, grid, includeXml, pageRange, w, h);
// Exports only current page for images that does not contain file data, but for
// the other formats with XML included or pdf with all pages, we need to send the complete data and use
// the from/to URL parameters to specify the page to be exported.
var data = this.getFileData(true, null, null, null, ignoreSelection, currentPage == false? false : format != 'xmlpng');
var range = null;
var allPages = null;
var embed = (includeXml) ? '1' : '0';
if (format == 'pdf' && currentPage == false)
{
allPages = '1';
}
if (format == 'xmlpng')
{
embed = '1';
format = 'png';
// Finds the current page number
if (this.pages != null && this.currentPage != null)
{
for (var i = 0; i < this.pages.length; i++)
{
if (this.pages[i] == this.currentPage)
{
range = i;
break;
}
}
}
}
var bg = graph.background;
if (format == 'png' && transparent)
{
bg = mxConstants.NONE;
}
else if (!transparent && (bg == null || bg == mxConstants.NONE))
{
bg = '#ffffff';
}
var extras = {globalVars: graph.getExportVariables()};
if (grid)
{
extras.grid = {
size: graph.gridSize,
steps: graph.view.gridSteps,
color: graph.view.gridColor
};
}
return new mxElectronRequest('export', {
format: format,
xml: data,
from: range,
bg: (bg != null) ? bg : mxConstants.NONE,
filename: (filename != null) ? filename : null,
allPages: allPages,
base64: base64,
embedXml: embed,
extras: encodeURIComponent(JSON.stringify(extras)),
scale: scale,
border: border
});
return new mxElectronRequest('export', params);
};
var origSetAutosave = Editor.prototype.setAutosave;
@@ -2082,6 +2000,11 @@ mxStencilRegistry.allowEval = false;
{
var extras = {globalVars: graph.getExportVariables()};
if (Graph.translateDiagram)
{
extras.diagramLanguage = Graph.diagramLanguage;
}
editorUi.saveRequest(name, format,
function(newTitle, base64)
{

View File

@@ -151,6 +151,38 @@ server {
proxy_set_header Connection $connection_upgrade;
proxy_pass http://drawio-export:8000/;
}
location /minder/ {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-PORT $remote_port;
proxy_set_header X-Forwarded-Host $the_host;
proxy_set_header X-Forwarded-Proto $the_scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header Server-Protocol $server_protocol;
proxy_set_header Server-Name $server_name;
proxy_set_header Server-Addr $server_addr;
proxy_set_header Server-Port $server_port;
proxy_pass http://minder/;
}
# 审批
location /approve/ {
proxy_pass http://approve/;
}
location /approve/api/ {
auth_request /approveAuth;
proxy_pass http://approve/api/;
}
location /approveAuth {
internal;
proxy_set_header Content-Type "application/json";
proxy_set_header Content-Length $request_length;
proxy_pass http://service/api/approve/verifyToken;
}
}
include /etc/nginx/conf.d/conf.d/*.conf;

File diff suppressed because it is too large Load Diff

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