Compare commits
486 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
397421010e | ||
|
|
8872b0519d | ||
|
|
83d3b3ffbf | ||
|
|
d1702bd62c | ||
|
|
928235eac8 | ||
|
|
3bdfaab158 | ||
|
|
d7902b4d08 | ||
|
|
3334abfb8f | ||
|
|
3e44e584c0 | ||
|
|
195a305fc3 | ||
|
|
cedffd17b3 | ||
|
|
59b29014d9 | ||
|
|
06db036e4a | ||
|
|
617e0837c9 | ||
|
|
7a275bd802 | ||
|
|
83f58eae68 | ||
|
|
19815fe27d | ||
|
|
0f75556bed | ||
|
|
dc0f925d24 | ||
|
|
c5948c4171 | ||
|
|
d144b06c1f | ||
|
|
92dfea677b | ||
|
|
7b5867e2c0 | ||
|
|
b643fe56d5 | ||
|
|
38fa72e9da | ||
|
|
82fddefc94 | ||
|
|
0c9c9cb90a | ||
|
|
38b50a8a84 | ||
|
|
0f250dbafd | ||
|
|
168650649f | ||
|
|
52babf82ae | ||
|
|
0c8517667f | ||
|
|
77d105cb9f | ||
|
|
8af33ea66a | ||
|
|
a57740e14e | ||
|
|
82230d70a5 | ||
|
|
15f3f9c0e5 | ||
|
|
7fc328492b | ||
|
|
81cedca590 | ||
|
|
df4e00e23f | ||
|
|
ad70f23a05 | ||
|
|
c93beb27fd | ||
|
|
41da2231ed | ||
|
|
9d9213fbdb | ||
|
|
50106d19e8 | ||
|
|
62d1e676bd | ||
|
|
0b7d49785c | ||
|
|
40736c4a05 | ||
|
|
1f0ab02702 | ||
|
|
21aa4f7b2b | ||
|
|
43d0a85061 | ||
|
|
8bdd31ae67 | ||
|
|
b78f93979d | ||
|
|
8d6b4a1d2e | ||
|
|
7630c83ae0 | ||
|
|
c7c47aff5a | ||
|
|
72e475cb84 | ||
|
|
f750a6aec2 | ||
|
|
0dde37e1f1 | ||
|
|
a2066fc137 | ||
|
|
f652f35c3a | ||
|
|
562697da27 | ||
|
|
d23d77ff90 | ||
|
|
119443cc88 | ||
|
|
27ae831799 | ||
|
|
b482947207 | ||
|
|
6ebc89695a | ||
|
|
a65dfec7a8 | ||
|
|
a0cd79e587 | ||
|
|
8fe1e2fee4 | ||
|
|
3cc9f7bc40 | ||
|
|
8d3d5025ed | ||
|
|
a49c0aea47 | ||
|
|
d366cf9885 | ||
|
|
be53afe6b4 | ||
|
|
cdd980112d | ||
|
|
bca284969d | ||
|
|
dd899a3e13 | ||
|
|
d6ca66aa2f | ||
|
|
20ba671cd3 | ||
|
|
672795ac49 | ||
|
|
9716d7fe43 | ||
|
|
193ad8d902 | ||
|
|
a87f903c50 | ||
|
|
82f154a229 | ||
|
|
bee36801ab | ||
|
|
37f379c890 | ||
|
|
88b995ca9c | ||
|
|
919289c5ca | ||
|
|
0535b56766 | ||
|
|
6afd413b87 | ||
|
|
4818409329 | ||
|
|
919b652a06 | ||
|
|
15d3ec9d81 | ||
|
|
e0be6e429e | ||
|
|
8d24be914d | ||
|
|
8bbe9c97e9 | ||
|
|
ccbd904a3f | ||
|
|
4ed3db7e41 | ||
|
|
65ced28004 | ||
|
|
4c282962b3 | ||
|
|
c64c436b9f | ||
|
|
378e270f41 | ||
|
|
7217bd7d1a | ||
|
|
ff2461d89d | ||
|
|
0eb3430c14 | ||
|
|
da7c1e40e3 | ||
|
|
8c9e928ddc | ||
|
|
477aef7db6 | ||
|
|
cc97d9f1ea | ||
|
|
ee6cf05a92 | ||
|
|
575db58476 | ||
|
|
986a2f8cbb | ||
|
|
0b1da914cd | ||
|
|
04acd7c56d | ||
|
|
4430d85242 | ||
|
|
55ade32589 | ||
|
|
0ffbaaaeaa | ||
|
|
62b40ddb84 | ||
|
|
2d5ce87605 | ||
|
|
7ca0bc5960 | ||
|
|
021c09e426 | ||
|
|
75db81f2f9 | ||
|
|
f162617765 | ||
|
|
b7d10a4c58 | ||
|
|
79ca1aea02 | ||
|
|
957201804c | ||
|
|
cf5e126eaa | ||
|
|
69fc0a118b | ||
|
|
4dacc26567 | ||
|
|
7de1ed7d45 | ||
|
|
ab47f01625 | ||
|
|
13c4fa4f1f | ||
|
|
173631f115 | ||
|
|
8462e9c097 | ||
|
|
3c9447e1b6 | ||
|
|
38eaf2eb02 | ||
|
|
c8364ed17b | ||
|
|
ba52738904 | ||
|
|
4061ae4275 | ||
|
|
82afb5b150 | ||
|
|
e1203f0c8d | ||
|
|
e6f6b3fee2 | ||
|
|
e5efcd3d26 | ||
|
|
bf45587c80 | ||
|
|
29a0d22938 | ||
|
|
635cc04c50 | ||
|
|
bc5343652b | ||
|
|
03f140fe3b | ||
|
|
3e4a119f61 | ||
|
|
3b7bcbc14a | ||
|
|
3c49e96e02 | ||
|
|
5be209ab59 | ||
|
|
56ea048ab3 | ||
|
|
9fc0bd0439 | ||
|
|
1c2798cbf4 | ||
|
|
9d8af2eaab | ||
|
|
bba1e0d12f | ||
|
|
c060e60e4a | ||
|
|
1c504bd899 | ||
|
|
b617648bd8 | ||
|
|
e849c7a34f | ||
|
|
f6dd1ce98e | ||
|
|
9c78db8d45 | ||
|
|
5154348cf9 | ||
|
|
4521cea3b4 | ||
|
|
0ff1ac7743 | ||
|
|
277a751ed4 | ||
|
|
96be2a86ca | ||
|
|
f28bff569a | ||
|
|
e34aa77a54 | ||
|
|
e53b65496f | ||
|
|
f6ee630615 | ||
|
|
ec2e1e3152 | ||
|
|
6cffe9baed | ||
|
|
b63df27409 | ||
|
|
617c466ac0 | ||
|
|
ed8e443f3a | ||
|
|
58cb49b125 | ||
|
|
7dd5baa9ec | ||
|
|
bbf9107560 | ||
|
|
be527355ee | ||
|
|
c866500120 | ||
|
|
3e2a40aaa0 | ||
|
|
eef9fa56c6 | ||
|
|
945d84dbc4 | ||
|
|
d353d33107 | ||
|
|
f54bad5d79 | ||
|
|
b605c70e91 | ||
|
|
1752e88c42 | ||
|
|
e2718a39a0 | ||
|
|
25298ac69e | ||
|
|
cf9f389f75 | ||
|
|
567c75830a | ||
|
|
7b1d352c95 | ||
|
|
4fa54381a6 | ||
|
|
9c91f7cf83 | ||
|
|
edd5cd1ca1 | ||
|
|
f2ec6ad05e | ||
|
|
a04ef4ac38 | ||
|
|
43b3d1d379 | ||
|
|
b65fdeacc2 | ||
|
|
622fe1e5d9 | ||
|
|
a6c7c0c7ad | ||
|
|
e5c8748b75 | ||
|
|
f096d71cc1 | ||
|
|
d73a152a36 | ||
|
|
f4e6fd060e | ||
|
|
c78ca1de5d | ||
|
|
2b219c7256 | ||
|
|
6ffa651742 | ||
|
|
cb3b22a4bf | ||
|
|
145bfdb0e9 | ||
|
|
8c7b0c502d | ||
|
|
684bf12a5c | ||
|
|
aaa75aff14 | ||
|
|
f03600bd65 | ||
|
|
1c4c4fe3fb | ||
|
|
5e46b2cd1a | ||
|
|
027db7c0ec | ||
|
|
5bb17ddc6b | ||
|
|
e8edd74bc3 | ||
|
|
ed064a825a | ||
|
|
32c232a0b5 | ||
|
|
c2fd747c45 | ||
|
|
9148853f2c | ||
|
|
23d0f50a3d | ||
|
|
36cdf87bfe | ||
|
|
cfd2e1fd7b | ||
|
|
3cafac99ff | ||
|
|
1dd4e8da71 | ||
|
|
543015a36e | ||
|
|
2efdfc4b1f | ||
|
|
7234d9307e | ||
|
|
769ce1ce7c | ||
|
|
62c1d5783e | ||
|
|
a6bd4a2ffe | ||
|
|
f1a9077b7e | ||
|
|
2c3e80bd8f | ||
|
|
e52d066fb0 | ||
|
|
5279d57018 | ||
|
|
25e5eb4427 | ||
|
|
b01d5ce8c4 | ||
|
|
ff41f5c041 | ||
|
|
dd0770a93f | ||
|
|
9a3e76fff3 | ||
|
|
7c867578ee | ||
|
|
d543c27000 | ||
|
|
a8be330baa | ||
|
|
c128c58110 | ||
|
|
e32a3887cd | ||
|
|
94932c7486 | ||
|
|
a1920745fb | ||
|
|
51e8f9555e | ||
|
|
213ab8418b | ||
|
|
707f1dd6cb | ||
|
|
125ce036cd | ||
|
|
172c562a71 | ||
|
|
80bbe6711c | ||
|
|
3f56c64086 | ||
|
|
e6167119e0 | ||
|
|
368fae5f32 | ||
|
|
6ae46cf7bb | ||
|
|
e97806c85b | ||
|
|
f31e88bed1 | ||
|
|
6bd20038f9 | ||
|
|
30cfb1200d | ||
|
|
154e0039d1 | ||
|
|
a8f3b02ee7 | ||
|
|
b3e83e13bc | ||
|
|
d0a0e77c44 | ||
|
|
a14896307f | ||
|
|
976b300277 | ||
|
|
ccbd873204 | ||
|
|
9c1482f9e9 | ||
|
|
5a7f4efa91 | ||
|
|
f78c4a1fb0 | ||
|
|
db6500369f | ||
|
|
9e4beaa317 | ||
|
|
afd021737a | ||
|
|
3982ed56f7 | ||
|
|
df4a01a7f9 | ||
|
|
a6fac96ec1 | ||
|
|
8ed9186ff4 | ||
|
|
821df75d4b | ||
|
|
0c09a2445c | ||
|
|
e6983e858d | ||
|
|
f8b69df955 | ||
|
|
15370a93c7 | ||
|
|
bc18aeeadc | ||
|
|
a1f143b0aa | ||
|
|
c13fe9d590 | ||
|
|
50203fbcb3 | ||
|
|
ffe7ebf711 | ||
|
|
f0b5e0c3b9 | ||
|
|
501235ef12 | ||
|
|
da0fa31181 | ||
|
|
0272933f70 | ||
|
|
30d88761b4 | ||
|
|
fb286cea3c | ||
|
|
6bcc7b6c49 | ||
|
|
6338a44cc1 | ||
|
|
ae4680f20c | ||
|
|
2841874417 | ||
|
|
b6a4e6b4de | ||
|
|
34cfd1e344 | ||
|
|
b467dc55e5 | ||
|
|
9fd8d44a6e | ||
|
|
64262134c4 | ||
|
|
0019c9ef41 | ||
|
|
2676ebd047 | ||
|
|
97cdd56110 | ||
|
|
d973451bdc | ||
|
|
80313f613e | ||
|
|
5c564524a3 | ||
|
|
e081fbd92b | ||
|
|
0ecc20472a | ||
|
|
b51052f0c6 | ||
|
|
cb106e42ee | ||
|
|
52f9495ff8 | ||
|
|
440b633bad | ||
|
|
a07913181a | ||
|
|
34ffd96c86 | ||
|
|
46a623b430 | ||
|
|
c16e37023c | ||
|
|
1cb0cdf540 | ||
|
|
073d03a882 | ||
|
|
30b9276ab4 | ||
|
|
76c8b4a4c6 | ||
|
|
9ea4781d93 | ||
|
|
07d583f73f | ||
|
|
12c74aef7a | ||
|
|
64b10e3060 | ||
|
|
ab2b29f267 | ||
|
|
be9a968ad9 | ||
|
|
5f87067a75 | ||
|
|
ef273bd9dd | ||
|
|
0737a9fae7 | ||
|
|
727d7e1d81 | ||
|
|
87e8589aea | ||
|
|
b13758d3e9 | ||
|
|
14775e2861 | ||
|
|
94af3822d8 | ||
|
|
07254c9f27 | ||
|
|
a99c2f6944 | ||
|
|
f9540b08cd | ||
|
|
34af77eb6d | ||
|
|
cf3f22776c | ||
|
|
5bebc8b5ee | ||
|
|
8a4b0c57f9 | ||
|
|
1acfd7ee34 | ||
|
|
a29661c54d | ||
|
|
90558d5ece | ||
|
|
e6c7007be5 | ||
|
|
16d0d1687f | ||
|
|
95ab44d118 | ||
|
|
e541757b76 | ||
|
|
f422aea330 | ||
|
|
d5eb3716aa | ||
|
|
7fb854fb48 | ||
|
|
60b5ecdcd7 | ||
|
|
6cce7d31ff | ||
|
|
46f5dd99a6 | ||
|
|
9753dec996 | ||
|
|
53f2e07178 | ||
|
|
3aa2c604d8 | ||
|
|
d8fbf36e00 | ||
|
|
008653e3d9 | ||
|
|
23188777fe | ||
|
|
8eb0a49ee6 | ||
|
|
207f09a4af | ||
|
|
69120c5045 | ||
|
|
b8143d1a9b | ||
|
|
f7eab5893a | ||
|
|
5fc598a220 | ||
|
|
783c21ad18 | ||
|
|
a1ce6e6928 | ||
|
|
8cbae629a5 | ||
|
|
da7e832f21 | ||
|
|
a572ba0523 | ||
|
|
85a20168dc | ||
|
|
25be9c0fef | ||
|
|
a8c890ba51 | ||
|
|
11628b98ca | ||
|
|
4ae6ca945b | ||
|
|
49aa1434aa | ||
|
|
9e92c61fbf | ||
|
|
c84111b6b9 | ||
|
|
3a2fcdd18a | ||
|
|
84a800f69b | ||
|
|
77e08aa048 | ||
|
|
0d6fd903f1 | ||
|
|
bcc74dd927 | ||
|
|
dd0720afa7 | ||
|
|
a06a4095b6 | ||
|
|
29bc009c07 | ||
|
|
520d2a0e20 | ||
|
|
dbeb9dd561 | ||
|
|
5b02d8008f | ||
|
|
a032c6114f | ||
|
|
69ec4966d5 | ||
|
|
87fab80ea3 | ||
|
|
bd2dabe851 | ||
|
|
4a45d69e5b | ||
|
|
e15bea9342 | ||
|
|
7132413837 | ||
|
|
c51116acaa | ||
|
|
002776f15e | ||
|
|
c7f5c62e71 | ||
|
|
3c57cf8d81 | ||
|
|
f29bf3640a | ||
|
|
07663dea6c | ||
|
|
0ddb696e90 | ||
|
|
cc0a6d4706 | ||
|
|
4c0ecc8f07 | ||
|
|
d50c8ce691 | ||
|
|
8aa66661ac | ||
|
|
00a9b3b57b | ||
|
|
3896d08207 | ||
|
|
9b736c99f8 | ||
|
|
129d7e5850 | ||
|
|
c2b26ffe6e | ||
|
|
9b01e076f5 | ||
|
|
88553872fc | ||
|
|
2b8de4c028 | ||
|
|
24c5200a90 | ||
|
|
bca0410a08 | ||
|
|
42234be5cf | ||
|
|
8e108e2d38 | ||
|
|
248b0ce196 | ||
|
|
d25ee3c234 | ||
|
|
8ea1234596 | ||
|
|
32530e5dc9 | ||
|
|
952d060e2f | ||
|
|
712f9e07b7 | ||
|
|
03cd6e79bb | ||
|
|
cbd9e8a33c | ||
|
|
13222fbe9a | ||
|
|
4b89eb88bd | ||
|
|
646a5e3b28 | ||
|
|
08153cd99b | ||
|
|
61ebbac333 | ||
|
|
d63c1f156f | ||
|
|
a4548e2cba | ||
|
|
77a3f2027e | ||
|
|
ecf0c78993 | ||
|
|
1a0c1e3306 | ||
|
|
506207d3ba | ||
|
|
76bf46c152 | ||
|
|
96c64fbb91 | ||
|
|
7fedb7d275 | ||
|
|
c16f316200 | ||
|
|
4c5c071b21 | ||
|
|
df917001d3 | ||
|
|
65e75f974d | ||
|
|
8afc1db72f | ||
|
|
71f13a0b50 | ||
|
|
4f57b195a8 | ||
|
|
aa1ea41c5d | ||
|
|
b45058de72 | ||
|
|
576ab9a268 | ||
|
|
e3312c97a7 | ||
|
|
6bafa0a6dd | ||
|
|
153d26ffcd | ||
|
|
74fecdd941 | ||
|
|
902844e008 | ||
|
|
e78d850138 | ||
|
|
94cefe52dd | ||
|
|
a011f82912 | ||
|
|
a160b2a471 | ||
|
|
396144f3fb | ||
|
|
ff0fadc0c1 | ||
|
|
65ec3a10bf | ||
|
|
01c721c7e0 | ||
|
|
d9aadb4f30 | ||
|
|
964611eba4 | ||
|
|
98d2627036 | ||
|
|
ba64540743 | ||
|
|
62c50bb4e6 | ||
|
|
0d4b005f4e | ||
|
|
61b1206091 | ||
|
|
2d37faea1d | ||
|
|
3f5c85b434 | ||
|
|
d34bff28c5 | ||
|
|
8f622dd6a5 | ||
|
|
7fbd3bc760 |
@@ -17,7 +17,7 @@ LOG_CHANNEL=stack
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST="${APP_IPPR}.5"
|
||||
DB_HOST=mariadb
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=dootask
|
||||
DB_USERNAME=dootask
|
||||
@@ -34,7 +34,7 @@ SESSION_LIFETIME=120
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_HOST="${APP_IPPR}.4"
|
||||
REDIS_HOST=redis
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
|
||||
48
.github/workflows/publish.yml
vendored
48
.github/workflows/publish.yml
vendored
@@ -115,9 +115,50 @@ jobs:
|
||||
})
|
||||
return data.id
|
||||
|
||||
build-client:
|
||||
pack-vendor:
|
||||
needs: [ check-version, create-release ]
|
||||
if: needs.check-version.outputs.should_release == 'true'
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.0'
|
||||
extensions: mbstring, intl, gd, xml, zip, swoole
|
||||
tools: composer:v2
|
||||
|
||||
- name: Install Dependencies
|
||||
run: composer install
|
||||
|
||||
- name: Create Vendor Archive
|
||||
run: tar -czf vendor.tar.gz vendor/
|
||||
|
||||
- name: Upload Vendor Archive
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
const data = await fs.promises.readFile('vendor.tar.gz');
|
||||
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: process.env.RELEASE_ID,
|
||||
name: 'vendor.tar.gz',
|
||||
data: data
|
||||
});
|
||||
|
||||
build-client:
|
||||
needs: [ check-version, create-release, pack-vendor ]
|
||||
if: needs.check-version.outputs.should_release == 'true'
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
@@ -231,7 +272,7 @@ jobs:
|
||||
./cmd electron win
|
||||
|
||||
publish-release:
|
||||
needs: [ check-version, create-release, build-client ]
|
||||
needs: [ check-version, create-release, pack-vendor, build-client ]
|
||||
if: needs.check-version.outputs.should_release == 'true' && github.ref == 'refs/heads/pro'
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -258,4 +299,7 @@ jobs:
|
||||
env:
|
||||
PUBLISH_KEY: ${{ secrets.PUBLISH_KEY }}
|
||||
run: |
|
||||
pushd electron || exit
|
||||
npm install
|
||||
popd || exit
|
||||
node ./electron/build.js published
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -15,6 +15,7 @@
|
||||
.idea
|
||||
.vscode
|
||||
.vagrant
|
||||
.windsurfrules
|
||||
.phpunit.result.cache
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
|
||||
116
.prefetch
116
.prefetch
@@ -1,55 +1,115 @@
|
||||
office/web-apps/apps/api/documents/api.js?hash={version}
|
||||
|
||||
office/{path}/fonts/000
|
||||
office/{path}/fonts/020
|
||||
office/{path}/fonts/020
|
||||
office/{path}/fonts/001
|
||||
office/{path}/fonts/002
|
||||
office/{path}/fonts/020
|
||||
office/{path}/fonts/022
|
||||
office/{path}/fonts/022
|
||||
office/{path}/fonts/022
|
||||
office/{path}/fonts/023
|
||||
office/{path}/fonts/023
|
||||
office/{path}/fonts/023
|
||||
office/{path}/fonts/024
|
||||
office/{path}/fonts/024
|
||||
office/{path}/fonts/024
|
||||
office/{path}/fonts/027
|
||||
office/{path}/fonts/027
|
||||
office/{path}/fonts/028
|
||||
office/{path}/fonts/028
|
||||
office/{path}/fonts/029
|
||||
office/{path}/fonts/029
|
||||
office/{path}/fonts/030
|
||||
office/{path}/fonts/030
|
||||
office/{path}/fonts/036
|
||||
office/{path}/fonts/036
|
||||
office/{path}/fonts/037
|
||||
office/{path}/fonts/037
|
||||
office/{path}/fonts/038
|
||||
office/{path}/fonts/038
|
||||
office/{path}/fonts/039
|
||||
office/{path}/fonts/039
|
||||
office/{path}/fonts/058
|
||||
office/{path}/fonts/058
|
||||
office/{path}/fonts/050
|
||||
office/{path}/fonts/051
|
||||
office/{path}/fonts/052
|
||||
office/{path}/fonts/053
|
||||
office/{path}/fonts/058
|
||||
office/{path}/fonts/059
|
||||
office/{path}/fonts/059
|
||||
office/{path}/fonts/059
|
||||
office/{path}/fonts/060
|
||||
office/{path}/fonts/060
|
||||
office/{path}/fonts/060
|
||||
office/{path}/fonts/061
|
||||
office/{path}/fonts/061
|
||||
office/{path}/fonts/061
|
||||
office/{path}/fonts/062
|
||||
office/{path}/fonts/063
|
||||
office/{path}/fonts/064
|
||||
office/{path}/fonts/065
|
||||
office/{path}/fonts/066
|
||||
office/{path}/fonts/067
|
||||
office/{path}/fonts/068
|
||||
office/{path}/fonts/069
|
||||
office/{path}/fonts/070
|
||||
office/{path}/fonts/071
|
||||
office/{path}/fonts/072
|
||||
office/{path}/fonts/073
|
||||
office/{path}/fonts/074
|
||||
office/{path}/fonts/075
|
||||
office/{path}/fonts/076
|
||||
office/{path}/fonts/077
|
||||
office/{path}/fonts/078
|
||||
office/{path}/fonts/079
|
||||
office/{path}/fonts/080
|
||||
office/{path}/fonts/081
|
||||
office/{path}/fonts/081
|
||||
office/{path}/fonts/081
|
||||
office/{path}/fonts/086
|
||||
office/{path}/fonts/091
|
||||
office/{path}/fonts/092
|
||||
office/{path}/fonts/093
|
||||
office/{path}/fonts/094
|
||||
office/{path}/fonts/095
|
||||
office/{path}/fonts/096
|
||||
office/{path}/fonts/097
|
||||
office/{path}/fonts/098
|
||||
office/{path}/fonts/099
|
||||
office/{path}/fonts/100
|
||||
office/{path}/fonts/101
|
||||
office/{path}/fonts/102
|
||||
office/{path}/fonts/103
|
||||
office/{path}/fonts/131
|
||||
office/{path}/fonts/132
|
||||
office/{path}/fonts/133
|
||||
office/{path}/fonts/134
|
||||
office/{path}/fonts/135
|
||||
office/{path}/fonts/136
|
||||
office/{path}/fonts/137
|
||||
office/{path}/fonts/138
|
||||
office/{path}/fonts/139
|
||||
office/{path}/fonts/140
|
||||
office/{path}/fonts/141
|
||||
office/{path}/fonts/142
|
||||
office/{path}/fonts/143
|
||||
office/{path}/fonts/145
|
||||
office/{path}/fonts/147
|
||||
office/{path}/fonts/152
|
||||
office/{path}/fonts/154
|
||||
office/{path}/fonts/177
|
||||
office/{path}/fonts/178
|
||||
office/{path}/fonts/179
|
||||
office/{path}/fonts/180
|
||||
office/{path}/fonts/181
|
||||
office/{path}/fonts/182
|
||||
office/{path}/fonts/183
|
||||
office/{path}/fonts/184
|
||||
office/{path}/fonts/184
|
||||
office/{path}/fonts/185
|
||||
office/{path}/fonts/186
|
||||
office/{path}/fonts/187
|
||||
office/{path}/fonts/188
|
||||
office/{path}/fonts/189
|
||||
office/{path}/fonts/190
|
||||
office/{path}/fonts/191
|
||||
office/{path}/fonts/192
|
||||
office/{path}/fonts/193
|
||||
office/{path}/fonts/198
|
||||
office/{path}/fonts/199
|
||||
office/{path}/fonts/200
|
||||
office/{path}/fonts/201
|
||||
office/{path}/fonts/202
|
||||
office/{path}/fonts/203
|
||||
office/{path}/fonts/204
|
||||
office/{path}/fonts/205
|
||||
office/{path}/fonts/206
|
||||
office/{path}/fonts/207
|
||||
office/{path}/fonts/208
|
||||
office/{path}/fonts/209
|
||||
office/{path}/fonts/210
|
||||
office/{path}/fonts/211
|
||||
office/{path}/fonts/212
|
||||
office/{path}/fonts/214
|
||||
office/{path}/fonts/215
|
||||
office/{path}/fonts/216
|
||||
office/{path}/fonts/217
|
||||
office/{path}/sdkjs/cell/sdk-all-min.js
|
||||
office/{path}/sdkjs/cell/sdk-all.js
|
||||
office/{path}/sdkjs/common/AllFonts.js
|
||||
@@ -107,4 +167,4 @@ drawio/webapp/styles/grapheditor.css
|
||||
|
||||
minder/css/chunk-vendors.fe9c56c6.css
|
||||
minder/js/app.aa385de3.js
|
||||
minder/js/chunk-vendors.cc7455b8.js
|
||||
minder/js/chunk-vendors.cc7455b8.js
|
||||
|
||||
425
CHANGELOG.md
425
CHANGELOG.md
@@ -2,6 +2,429 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.45.64]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复部分页面出现空白的情况
|
||||
- 修复输入框无法点击添加链接的情况
|
||||
- 修复AI机器人不存在的情况
|
||||
|
||||
### Features
|
||||
|
||||
- 新增转发至AI开启新会话
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化数据结构
|
||||
- 优化图片存储名
|
||||
- 优化消息窗口显示
|
||||
- 优化目录结构
|
||||
- 优化日历
|
||||
- 优化任务时间范围选择
|
||||
|
||||
## [0.45.33]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复未读数错误暴增的情况
|
||||
- 修复地址可能存在localhost的情况
|
||||
- 修复消息编辑和发布时序号对不上
|
||||
- 修复草稿出现上一次内容的情况
|
||||
- 修复本地群消息通知没有会员昵称的问题
|
||||
- 修复了拉人进群无法踢出去的问题
|
||||
- 提及出现白色字的情况
|
||||
|
||||
### Features
|
||||
|
||||
- 添加移动端提示可能要发送的图片
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化消息窗口
|
||||
- 优化消息长按菜单
|
||||
- 优化内置浏览器
|
||||
- 优化App隐私政策提示
|
||||
- 优化对话独立窗口显示
|
||||
- 优化移动端选择交互
|
||||
- 优化移动端选中消息文本
|
||||
- 优化撤回消息逻辑
|
||||
- 优化提及搜索
|
||||
- 优化机器人Webhook消息
|
||||
|
||||
## [0.44.91]
|
||||
|
||||
### Features
|
||||
|
||||
- 添加我的机器人管理
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化初始化逻辑
|
||||
- 优化docker配置
|
||||
|
||||
## [0.44.82]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复搜索结果显示即将到期
|
||||
|
||||
### Features
|
||||
|
||||
- 新增独立窗口打开会话
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化AI支持文件类型
|
||||
|
||||
## [0.44.74]
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化AI解析文件
|
||||
- 优化 WebSocket 消息
|
||||
- 优化数据
|
||||
|
||||
## [0.44.67]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复查看待办图片不符的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化数据
|
||||
- 优化未读消息数
|
||||
- 优化搜索组件
|
||||
- 已归档/已删除任务列表支持按状态检索
|
||||
- 优化消息流效果
|
||||
- 优化AI上下文
|
||||
- 优化工作流获取
|
||||
- 优化转发功能
|
||||
|
||||
## [0.44.53]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 工作流存在已离职人员
|
||||
|
||||
### Features
|
||||
|
||||
- 可点击标注图标查看标注人员
|
||||
- 支持分享工作报告到消息
|
||||
- 支持AI分析工作报告
|
||||
- 支持使用%发送工作报告
|
||||
- 新增自定义撤回及修改消息时限
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化转发消息
|
||||
- 优化工作报告列表
|
||||
- 优化引用消息
|
||||
- 优化全局提示
|
||||
- 优化草稿消息
|
||||
|
||||
## [0.44.19]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 看不到未读消息定位提醒
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化消息定位
|
||||
- 优化消息性能
|
||||
|
||||
## [0.44.15]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 会话内消息搜索布局错位
|
||||
- 流程设置翻译不统一
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化消息定位
|
||||
- 优化MD消息
|
||||
|
||||
## [0.44.3]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 定位签到失败的问题
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化发送语音效果
|
||||
- 录音转文字支持自定义语言
|
||||
- 优化ES模块
|
||||
- 优化emoji表情
|
||||
- 按住Ctrl/Command键可连续选择表情
|
||||
- Md消息支持html代码
|
||||
- 优化脚本
|
||||
- 优化安装命令
|
||||
- 优化ES索引名称
|
||||
|
||||
## [0.43.73]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 全屏预览图片关闭窗口
|
||||
- 点击排序导致任务不显示的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- 新增录音转文字
|
||||
- 优化数据排序
|
||||
|
||||
## [0.43.49]
|
||||
|
||||
### Performance
|
||||
|
||||
- 添加全局搜索功能
|
||||
- 优化消息搜索
|
||||
- 团队管理支持调整部门区域尺寸
|
||||
- 任务详情支持调整聊天区域尺寸
|
||||
- 优化团队部门支持3级部门
|
||||
- 可见群组ID
|
||||
- 支持在团队管理打开群聊
|
||||
- 优化回复消息自动@逻辑
|
||||
- 转发预览隐藏表情回应部分
|
||||
- 优化任务日志
|
||||
- 已删除任务支持按标签搜索
|
||||
- 归档任务支持按标签搜索
|
||||
- 项目面板添加按标签筛选
|
||||
- 优化 AI 提示词
|
||||
- 优化 AI 设置
|
||||
|
||||
## [0.43.18]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 首次跟ai聊天没有记录的问题
|
||||
|
||||
### Performance
|
||||
|
||||
- 工作报告支持查看仅未读
|
||||
- AI 支持引用文件
|
||||
- 优化图文消息
|
||||
- 优化文本信息复制
|
||||
- 优化样式
|
||||
- 无法再AI机器人页面看到模型的问题
|
||||
|
||||
## [0.43.7]
|
||||
|
||||
### Features
|
||||
|
||||
- 添加 Grok AI、Ollama AI
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化MD消息过长处理
|
||||
- 优化AI支持分析指定文件
|
||||
- 支持在AI对话中直接引用任务提问
|
||||
- 优化 AI 参数
|
||||
- 优化 Ollama AI
|
||||
- 优化设置
|
||||
- 优化AI设置
|
||||
- 优化AI消息
|
||||
|
||||
## [0.42.85]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 撤回消息是消息列表不更新的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- 表情回复时更新对话列表
|
||||
- Onlyoffice 支持打开超过100m的文件
|
||||
- 优化点击上传列表效果
|
||||
- AI支持自定义模型列表
|
||||
|
||||
## [0.42.79]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复偶现的是子窗口出现身份丢失的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化查看长消息内容
|
||||
|
||||
## [0.42.74]
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化审批功能
|
||||
- AI机器人支持多会话
|
||||
- AI机器人支持自定义模型
|
||||
|
||||
## [0.42.61]
|
||||
|
||||
### Performance
|
||||
|
||||
- 支持下载聊天引用的文件
|
||||
- 优化翻译消息
|
||||
- 支持显示思考过程
|
||||
|
||||
## [0.42.57]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 跨地区发消息出现消息过期的情况
|
||||
- 多线程下载文件损坏的问题
|
||||
- 修复新建周报或日报唯一标识重复
|
||||
|
||||
### Features
|
||||
|
||||
- 添加 DeepSeek AI
|
||||
- 添加https证书自动更新
|
||||
|
||||
### Performance
|
||||
|
||||
- 支持自定义仪表盘欢迎词
|
||||
- ChatGPT 支持自定义 Base URL
|
||||
- 优化仪表盘任务更新规则
|
||||
|
||||
## [0.42.37]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 部分电脑无法复制的问题
|
||||
- 修复任务可见性 - 任务重覆获取, 子任务负责人看不到任务问题
|
||||
|
||||
### Performance
|
||||
|
||||
- 更新小海豚表情包
|
||||
- 优化任务时间冲突提示
|
||||
- 优化消息
|
||||
- 群聊总人数排除机器人
|
||||
|
||||
## [0.42.26]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 移交账号后工作流的负责人没有更新
|
||||
- 全屏预览时深色皮肤反色的情况
|
||||
|
||||
### Features
|
||||
|
||||
- 替换网页的资源为本地资源
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化任务面板
|
||||
- 优化子任务的可见性
|
||||
- 优化客户端
|
||||
- 优化会议
|
||||
- 优化会员搜索
|
||||
- 优化打开会话
|
||||
- 优化项目面板任务加载
|
||||
- 优化客户端加载
|
||||
|
||||
## [0.42.3]
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化对话阅读状况
|
||||
- 优化表情回复
|
||||
|
||||
## [0.42.0]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 桌面端查看表情图片缩略图显示错误
|
||||
- 项目面板任务不显示的情况
|
||||
- 修复移动任务子任务不跟随的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化桌面端数据处理
|
||||
- 优化资源
|
||||
- 优化数据流
|
||||
|
||||
## [0.41.93]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 复制文件权限判断
|
||||
|
||||
### Performance
|
||||
|
||||
- AI创建任务确认
|
||||
- 优化项目面板
|
||||
|
||||
## [0.41.84]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- @在线状态不正确
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化子任务上下文
|
||||
- 优化子任务时间调整
|
||||
- 优化超长文本信息
|
||||
- 记录版本信息
|
||||
- 支持更多办公文件格式
|
||||
- 请假或外出时取消打卡提醒
|
||||
- 图片容错处理
|
||||
- 优化全局监听事件
|
||||
- 优化数据流消息
|
||||
|
||||
## [0.41.64]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复使用AI创建任务顺序错误的问题
|
||||
|
||||
## [0.41.55]
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化媒体播放
|
||||
- 优化临时会话的消息推送
|
||||
- 优化任务时间显示
|
||||
|
||||
## [0.41.45]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复官网-帮助中心图片替换
|
||||
- 可见非共享文件夹的情况
|
||||
- 审批导致图片显示错误
|
||||
- Win子窗口无法激活的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- Upgrade office
|
||||
- 添加项目任务标签功能
|
||||
- Upgrade drawio
|
||||
- 优化AI创建任务
|
||||
- 优化已读数据
|
||||
- 优化AI群聊
|
||||
- 支持AI在项目群里创建任务
|
||||
- 优化AI上下文
|
||||
- 优化客户端会议打开速度
|
||||
- 支持通过接口发送通知和模板消息
|
||||
- 优化仪表盘任务避免重复统计
|
||||
- 支持自定义AI个人提示词
|
||||
- 优化客户端媒体浏览器
|
||||
- 支持自定义上传图片压缩质量
|
||||
- 优化与离职账号聊天
|
||||
- 优化邮件通知
|
||||
- 优化未设置优先级的显示
|
||||
- 添加任务模板
|
||||
- 项目可自定义任务归档时间
|
||||
- 优化快速添加任务
|
||||
- 支持通过职位名称搜索成员
|
||||
- 会话页面支持查看头像
|
||||
- 优化文件列表
|
||||
- 更新桌面客户端框架
|
||||
- 优化主题变化逻辑
|
||||
- 更新 AI 支持更多模型和支持提示词
|
||||
|
||||
## [0.40.78]
|
||||
|
||||
### Bug Fixes
|
||||
@@ -19,6 +442,7 @@ All notable changes to this project will be documented in this file.
|
||||
- 重复添加任务的情况
|
||||
- 重复添加任务列表的情况
|
||||
- 优化消息样式
|
||||
- 优化表情滚动条
|
||||
- 优化websocket消息
|
||||
- 优化快捷选择
|
||||
- 延期任务支持快选时间
|
||||
@@ -26,6 +450,7 @@ All notable changes to this project will be documented in this file.
|
||||
- 新增文件打包下载权限设置
|
||||
- 升级electron框架
|
||||
- 优化深色主题
|
||||
- 优化深色主题下调整浏览器窗口显示白边的情况
|
||||
- 优化表情包资源
|
||||
- 优化客户端子窗口
|
||||
- 优化项目列表
|
||||
|
||||
10
README.md
10
README.md
@@ -47,16 +47,6 @@ cd dootask
|
||||
./cmd port 80
|
||||
```
|
||||
|
||||
### Change App Url
|
||||
|
||||
```bash
|
||||
# This URL only affects the email reply.
|
||||
./cmd url {Your domain url}
|
||||
|
||||
# example:
|
||||
./cmd url https://domain.com
|
||||
```
|
||||
|
||||
### Stop server
|
||||
|
||||
```bash
|
||||
|
||||
10
README_CN.md
10
README_CN.md
@@ -47,16 +47,6 @@ cd dootask
|
||||
./cmd port 80
|
||||
```
|
||||
|
||||
### 更换URL
|
||||
|
||||
```bash
|
||||
# 此地址仅影响邮件回复功能
|
||||
./cmd url {域名地址}
|
||||
|
||||
# 例如:
|
||||
./cmd url https://domain.com
|
||||
```
|
||||
|
||||
### 停止服务
|
||||
|
||||
```bash
|
||||
|
||||
254
app/Console/Commands/SyncDialogUserMsgToElasticsearch.php
Normal file
254
app/Console/Commands/SyncDialogUserMsgToElasticsearch.php
Normal file
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Models\WebSocketDialogUser;
|
||||
use App\Module\ElasticSearch\ElasticSearchKeyValue;
|
||||
use App\Module\ElasticSearch\ElasticSearchUserMsg;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SyncDialogUserMsgToElasticsearch extends Command
|
||||
{
|
||||
/**
|
||||
* 更新数据
|
||||
* --f: 全量更新 (默认)
|
||||
* --i: 增量更新(从上次更新的最后一个ID接上)
|
||||
*
|
||||
* 清理数据
|
||||
* --c: 清除索引
|
||||
*/
|
||||
|
||||
protected $signature = 'elasticsearch:sync-dialog-user-msg {--f} {--i} {--c} {--batch=500}';
|
||||
protected $description = '同步聊天会话用户和消息到Elasticsearch';
|
||||
protected $es;
|
||||
|
||||
/**
|
||||
* SyncDialogUserMsgToElasticsearch constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
try {
|
||||
$this->es = new ElasticSearchUserMsg();
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Elasticsearch连接失败: ' . $e->getMessage());
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('开始同步聊天数据...');
|
||||
|
||||
// 清除索引
|
||||
if ($this->option('c')) {
|
||||
$this->info('清除索引...');
|
||||
if (!$this->es->indexExists()) {
|
||||
$this->saveLastId(true);
|
||||
$this->info('索引不存在');
|
||||
return 0;
|
||||
}
|
||||
$result = $this->es->deleteIndex();
|
||||
if (isset($result['error'])) {
|
||||
$this->error('删除索引失败: ' . $result['error']);
|
||||
return 1;
|
||||
}
|
||||
$this->saveLastId(true);
|
||||
$this->info('索引删除成功');
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 判断创建索引
|
||||
if (!$this->es->indexExists()) {
|
||||
$this->info('创建索引...');
|
||||
$result = ElasticSearchUserMsg::generateIndex();
|
||||
if (isset($result['error'])) {
|
||||
$this->error('创建索引失败: ' . $result['error']);
|
||||
return 1;
|
||||
}
|
||||
$this->saveLastId(true);
|
||||
$this->info('索引创建成功');
|
||||
}
|
||||
|
||||
// 同步用户-会话数据
|
||||
$this->syncDialogUsers($this->option('batch'));
|
||||
|
||||
// 同步消息数据
|
||||
$this->syncDialogMsgs($this->option('batch'));
|
||||
|
||||
// 完成
|
||||
$this->info("\n同步完成");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存最后一个ID
|
||||
* @param string|true $type
|
||||
* @param integer $lastId
|
||||
*/
|
||||
private function saveLastId($type, $lastId = 0)
|
||||
{
|
||||
if ($type === true) {
|
||||
$setting = [];
|
||||
} else {
|
||||
$setting = ElasticSearchKeyValue::getArray('elasticSearch:sync');
|
||||
$setting[$type] = $lastId;
|
||||
}
|
||||
ElasticSearchKeyValue::save('elasticSearch:sync', $setting);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最后一个ID
|
||||
* @param $type
|
||||
* @return int
|
||||
*/
|
||||
private function getLastId($type)
|
||||
{
|
||||
if ($this->option('i')) {
|
||||
$setting = ElasticSearchKeyValue::getArray('elasticSearch:sync');
|
||||
return intval($setting[$type] ?? 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步用户-会话数据(父文档)
|
||||
* @param $batchSize
|
||||
* @return void
|
||||
*/
|
||||
private function syncDialogUsers($batchSize)
|
||||
{
|
||||
$this->info("\n同步用户数据...");
|
||||
$lastId = $this->getLastId('dialog_user');
|
||||
|
||||
$num = 0;
|
||||
$count = WebSocketDialogUser::where('id', '>', $lastId)->count();
|
||||
|
||||
do {
|
||||
// 获取一批用户-会话关系
|
||||
$dialogUsers = WebSocketDialogUser::where('id', '>', $lastId)
|
||||
->orderBy('id')
|
||||
->limit($batchSize)
|
||||
->get();
|
||||
|
||||
if ($dialogUsers->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
$num += count($dialogUsers);
|
||||
$progress = round($num / $count * 100, 2);
|
||||
$this->info("{$num}/{$count} ({$progress}%) 正在同步用户ID {$lastId} ~ {$dialogUsers->last()->id}");
|
||||
|
||||
// 批量索引数据
|
||||
$params = ['body' => []];
|
||||
foreach ($dialogUsers as $dialogUser) {
|
||||
$params['body'][] = [
|
||||
'index' => [
|
||||
'_index' => ElasticSearchUserMsg::indexName(),
|
||||
'_id' => ElasticSearchUserMsg::generateUserDicId($dialogUser),
|
||||
]
|
||||
];
|
||||
$params['body'][] = ElasticSearchUserMsg::generateUserFormat($dialogUser);
|
||||
}
|
||||
|
||||
if ($params['body']) {
|
||||
$result = $this->es->bulk($params);
|
||||
if (isset($result['errors']) && $result['errors']) {
|
||||
$this->error('批量索引用户数据部分失败');
|
||||
Log::error('Elasticsearch批量索引失败: ' . json_encode($result['items']));
|
||||
}
|
||||
}
|
||||
|
||||
$lastId = $dialogUsers->last()->id;
|
||||
$this->saveLastId('dialog_user', $lastId);
|
||||
} while (count($dialogUsers) == $batchSize);
|
||||
|
||||
$this->info("同步用户数据结束 - 最后ID {$lastId}");
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步消息数据(子文档)
|
||||
*/
|
||||
private function syncDialogMsgs($batchSize)
|
||||
{
|
||||
$this->info("\n同步消息数据...");
|
||||
$lastId = $this->getLastId('dialog_msg');
|
||||
|
||||
$num = 0;
|
||||
$count = WebSocketDialogMsg::where('id', '>', $lastId)->count();
|
||||
|
||||
do {
|
||||
// 获取一批消息
|
||||
$dialogMsgs = WebSocketDialogMsg::where('id', '>', $lastId)
|
||||
->orderBy('id')
|
||||
->limit($batchSize)
|
||||
->get();
|
||||
|
||||
if ($dialogMsgs->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
$num += count($dialogMsgs);
|
||||
$progress = round($num / $count * 100, 2);
|
||||
$this->info("{$num}/{$count} ({$progress}%) 正在同步消息ID {$lastId} ~ {$dialogMsgs->last()->id}");
|
||||
|
||||
// 获取这些消息所属的会话对应的所有用户
|
||||
$dialogIds = $dialogMsgs->pluck('dialog_id')->unique()->toArray();
|
||||
$userDialogMap = [];
|
||||
|
||||
if (!empty($dialogIds)) {
|
||||
$dialogUsers = WebSocketDialogUser::whereIn('dialog_id', $dialogIds)->get();
|
||||
|
||||
foreach ($dialogUsers as $dialogUser) {
|
||||
$userDialogMap[$dialogUser->dialog_id][] = $dialogUser->userid;
|
||||
}
|
||||
}
|
||||
|
||||
// 批量索引消息数据
|
||||
$params = ['body' => []];
|
||||
foreach ($dialogMsgs as $dialogMsg) {
|
||||
// 如果该会话没有用户,跳过
|
||||
if (empty($userDialogMap[$dialogMsg->dialog_id])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 为每个用户-会话关系创建子文档
|
||||
foreach ($userDialogMap[$dialogMsg->dialog_id] as $userid) {
|
||||
$params['body'][] = [
|
||||
'index' => [
|
||||
'_index' => ElasticSearchUserMsg::indexName(),
|
||||
'_id' => ElasticSearchUserMsg::generateMsgDicId($dialogMsg, $userid),
|
||||
'routing' => ElasticSearchUserMsg::generateMsgParentId($dialogMsg, $userid) // 路由到父文档
|
||||
]
|
||||
];
|
||||
|
||||
$params['body'][] = ElasticSearchUserMsg::generateMsgFormat($dialogMsg, $userid);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($params['body'])) {
|
||||
// 分批处理
|
||||
$chunks = array_chunk($params['body'], 1000);
|
||||
foreach ($chunks as $chunk) {
|
||||
$chunkParams = ['body' => $chunk];
|
||||
$result = $this->es->bulk($chunkParams);
|
||||
if (isset($result['errors']) && $result['errors']) {
|
||||
$this->error('批量索引消息数据部分失败');
|
||||
Log::error('Elasticsearch批量索引失败: ' . json_encode($result['items']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$lastId = $dialogMsgs->last()->id;
|
||||
$this->saveLastId('dialog_msg', $lastId);
|
||||
} while (count($dialogMsgs) == $batchSize);
|
||||
|
||||
$this->info("同步消息结束 - 最后ID {$lastId}");
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\WebSocket;
|
||||
use App\Services\RequestContext;
|
||||
use Hhxsv5\LaravelS\Swoole\Events\WorkerStartInterface;
|
||||
use Swoole\Http\Server;
|
||||
|
||||
@@ -25,5 +26,6 @@ class WorkerStartEvent implements WorkerStartInterface
|
||||
private function handleFirstWorkerTasks()
|
||||
{
|
||||
WebSocket::query()->delete();
|
||||
RequestContext::clearBaseUrlCache();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,6 +223,19 @@ class Handler extends ExceptionHandler
|
||||
} catch (\ImagickException) { }
|
||||
}
|
||||
|
||||
// 容错处理
|
||||
$patternFault = '/^(images\/.*\.(png|jpg|jpeg))\/crop\/([^\/]+)$/';
|
||||
$matchesFault = null;
|
||||
if (preg_match($patternFault, $path, $matchesFault)) {
|
||||
$file = public_path($matchesFault[1]);
|
||||
if (!file_exists($file)) {
|
||||
$file = public_path('images/other/imgerr.jpg');
|
||||
}
|
||||
if (file_exists($file)) {
|
||||
return response()->file($file);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ use App\Tasks\PushTask;
|
||||
use App\Module\BillExport;
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\ApproveProcMsg;
|
||||
use App\Models\ApproveProcInstHistory;
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Models\UserDepartment;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
@@ -996,7 +997,7 @@ class ApproveController extends AbstractController
|
||||
if ($thumb && file_exists(public_path($thumb))) {
|
||||
$imageSize = getimagesize(public_path($thumb));
|
||||
$data['thumb'] = [
|
||||
'url' => $thumb,
|
||||
'url' => Base::fillUrl($thumb),
|
||||
'width' => $imageSize[0],
|
||||
'height' => $imageSize[1]
|
||||
];
|
||||
@@ -1146,13 +1147,9 @@ class ApproveController extends AbstractController
|
||||
*/
|
||||
public function user__status()
|
||||
{
|
||||
$data['userid'] = intval(Request::input('userid'));
|
||||
$ret = Ihttp::ihttp_get($this->flow_url . '/api/v1/workflow/process/getUserApprovalStatus?' . http_build_query($data));
|
||||
$procdef = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
if (isset($procdef['status']) && $procdef['status'] == 200) {
|
||||
return Base::retSuccess('success', $procdef['data']["proc_def_name"] ?? '');
|
||||
}
|
||||
return Base::retSuccess('success', '');
|
||||
$userid = intval(Request::input('userid'));
|
||||
$status = ApproveProcInstHistory::getUserApprovalStatus($userid);
|
||||
return Base::retSuccess('success', $status);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -47,8 +47,7 @@ class FileController extends AbstractController
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$data = Request::all();
|
||||
$pid = intval($data['pid']);
|
||||
$pid = intval(Request::input('pid'));
|
||||
//
|
||||
return Base::retSuccess('success', (new File)->getFileList($user, $pid));
|
||||
}
|
||||
@@ -88,6 +87,7 @@ class FileController extends AbstractController
|
||||
}
|
||||
return Base::retError($msg, $data);
|
||||
}
|
||||
$fileLink->increment("num");
|
||||
} else {
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
@@ -107,6 +107,7 @@ class FileController extends AbstractController
|
||||
*
|
||||
* @apiParam {String} [link] 通过分享地址搜索(如:https://t.hitosea.com/single/file/ODcwOCwzOSxpa0JBS2lmVQ==)
|
||||
* @apiParam {String} [key] 关键词
|
||||
* @apiParam {Number} [take] 获取数量(默认:50,最大:100)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -119,7 +120,7 @@ class FileController extends AbstractController
|
||||
$link = trim(Request::input('link'));
|
||||
$key = trim(Request::input('key'));
|
||||
$id = 0;
|
||||
$take = 50;
|
||||
$take = Base::getPaginate(100, 50, 'take');
|
||||
if (preg_match("/\/single\/file\/(.*?)$/i", $link, $match)) {
|
||||
$id = intval(FileLink::whereCode($match[1])->value('file_id'));
|
||||
$take = 1;
|
||||
@@ -302,6 +303,7 @@ class FileController extends AbstractController
|
||||
//
|
||||
$userid = $user->userid;
|
||||
if ($row->pid > 0) {
|
||||
File::permissionFind($row->pid, $user, 1);
|
||||
$userid = intval(File::whereId($row->pid)->value('userid'));
|
||||
}
|
||||
//
|
||||
@@ -553,7 +555,7 @@ class FileController extends AbstractController
|
||||
$tmpPath = "uploads/file/document/" . date("Ym") . "/" . $id . "/attached/";
|
||||
Base::makeDir(public_path($tmpPath));
|
||||
$tmpPath .= md5($text) . "." . $matchs[1][$key];
|
||||
if (Base::saveContentImage(public_path($tmpPath), base64_decode($text), 90)) {
|
||||
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;
|
||||
@@ -664,7 +666,7 @@ class FileController extends AbstractController
|
||||
//
|
||||
if ($status === 2) {
|
||||
$parse = parse_url($url);
|
||||
$from = 'http://' . env('APP_IPPR') . '.3' . $parse['path'] . '?' . $parse['query'];
|
||||
$from = 'http://nginx' . $parse['path'] . '?' . $parse['query'];
|
||||
$path = 'uploads/file/' . $file->type . '/' . date("Ym") . '/' . $file->id . '/' . $key;
|
||||
$save = public_path($path);
|
||||
Base::makeDir(dirname($save));
|
||||
|
||||
@@ -39,6 +39,8 @@ use App\Module\BillMultipleExport;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Models\ProjectTaskFlowChange;
|
||||
use App\Models\ProjectTaskVisibilityUser;
|
||||
use App\Models\ProjectTaskTemplate;
|
||||
use App\Models\ProjectTag;
|
||||
|
||||
/**
|
||||
* @apiDefine project
|
||||
@@ -286,6 +288,8 @@ class ProjectController extends AbstractController
|
||||
* @apiParam {Number} project_id 项目ID
|
||||
* @apiParam {String} name 项目名称
|
||||
* @apiParam {String} [desc] 项目介绍
|
||||
* @apiParam {String} [archive_method] 归档方式
|
||||
* @apiParam {Number} [archive_days] 自动归档天数
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -298,6 +302,8 @@ class ProjectController extends AbstractController
|
||||
$project_id = intval(Request::input('project_id'));
|
||||
$name = trim(Request::input('name', ''));
|
||||
$desc = trim(Request::input('desc', ''));
|
||||
$archive_method = Request::input('archive_method');
|
||||
$archive_days = intval(Request::input('archive_days'));
|
||||
if (mb_strlen($name) < 2) {
|
||||
return Base::retError('项目名称不可以少于2个字');
|
||||
} elseif (mb_strlen($name) > 32) {
|
||||
@@ -306,9 +312,14 @@ class ProjectController extends AbstractController
|
||||
if (mb_strlen($desc) > 255) {
|
||||
return Base::retError('项目介绍最多只能设置255个字');
|
||||
}
|
||||
if ($archive_method == 'custom') {
|
||||
if ($archive_days < 1 || $archive_days > 365) {
|
||||
return Base::retError('自动归档天数设置错误,范围:1-365');
|
||||
}
|
||||
}
|
||||
//
|
||||
$project = Project::userProject($project_id, true, true);
|
||||
AbstractModel::transaction(function () use ($desc, $name, $project) {
|
||||
AbstractModel::transaction(function () use ($archive_days, $archive_method, $desc, $name, $project) {
|
||||
if ($project->name != $name) {
|
||||
$project->addLog("修改项目名称", [
|
||||
'change' => [$project->name, $name]
|
||||
@@ -322,6 +333,18 @@ class ProjectController extends AbstractController
|
||||
$project->desc = $desc;
|
||||
$project->addLog("修改项目介绍");
|
||||
}
|
||||
if ($project->archive_method != $archive_method) {
|
||||
$project->addLog("修改归档方式", [
|
||||
'change' => [$project->archive_method, $archive_method]
|
||||
]);
|
||||
$project->archive_method = $archive_method;
|
||||
}
|
||||
if ($project->archive_method == 'custom') {
|
||||
$project->addLog("修改自动归档天数", [
|
||||
'change' => [$project->archive_days, $archive_days]
|
||||
]);
|
||||
$project->archive_days = $archive_days;
|
||||
}
|
||||
$project->save();
|
||||
});
|
||||
$project->pushMsg('update');
|
||||
@@ -918,7 +941,9 @@ class ProjectController extends AbstractController
|
||||
* @apiName task__lists
|
||||
*
|
||||
* @apiParam {Object} [keys] 搜索条件
|
||||
* - keys.name: ID、任务名称
|
||||
* - keys.name: ID、任务名称、任务描述
|
||||
* - keys.tag: 标签名称
|
||||
* - keys.status: 任务状态 (completed: 已完成、uncompleted: 未完成、flow-xx: 流程状态ID)
|
||||
*
|
||||
* @apiParam {Number} [project_id] 项目ID
|
||||
* @apiParam {Number} [parent_id] 主任务ID(project_id && parent_id ≤ 0 时 仅查询自己参与的任务)
|
||||
@@ -971,7 +996,29 @@ class ProjectController extends AbstractController
|
||||
if (Base::isNumber($keys['name'])) {
|
||||
$builder->where("project_tasks.id", intval($keys['name']));
|
||||
} else {
|
||||
$builder->where("project_tasks.name", "like", "%{$keys['name']}%");
|
||||
$builder->where(function ($query) use ($keys) {
|
||||
$query->where("project_tasks.name", "like", "%{$keys['name']}%");
|
||||
$query->orWhere("project_tasks.desc", "like", "%{$keys['name']}%");
|
||||
});
|
||||
}
|
||||
}
|
||||
if ($keys['tag']) {
|
||||
$builder->whereHas('taskTag', function ($query) use ($keys) {
|
||||
$query->where('project_task_tags.name', $keys['tag']);
|
||||
});
|
||||
}
|
||||
if ($keys['status']) {
|
||||
if ($keys['status'] == 'completed') {
|
||||
$builder->whereNotNull('project_tasks.complete_at');
|
||||
} elseif ($keys['status'] == 'uncompleted') {
|
||||
$builder->whereNull('project_tasks.complete_at');
|
||||
} elseif (str_starts_with($keys['status'], 'flow-')) {
|
||||
$flow = str_replace('flow-', '', $keys['status']);
|
||||
if (Base::isNumber($flow)) {
|
||||
$builder->where('project_tasks.flow_item_id', intval($flow));
|
||||
} elseif ($flow) {
|
||||
$builder->where('project_tasks.flow_item_name', 'like', "%{$flow}%");
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
@@ -1035,20 +1082,20 @@ class ProjectController extends AbstractController
|
||||
$query->where('project_users.owner', 1);
|
||||
$query->where('project_users.userid', $userid);
|
||||
});
|
||||
$builder->leftJoin('project_task_users as project_sub_task_users', function ($query) use($userid) {
|
||||
$query->on('project_sub_task_users.task_pid', '=', 'project_tasks.parent_id');
|
||||
$query->where('project_sub_task_users.userid', $userid);
|
||||
});
|
||||
$builder->leftJoin('project_task_visibility_users', function ($query) use($userid) {
|
||||
$query->on('project_task_visibility_users.task_id', '=', 'project_tasks.id');
|
||||
$query->where('project_task_visibility_users.userid', $userid);
|
||||
});
|
||||
$builder->leftJoin('project_task_visibility_users as project_sub_task_visibility_users', function ($query) use($userid) {
|
||||
$query->on('project_sub_task_visibility_users.task_id', '=', 'project_tasks.parent_id');
|
||||
$query->where('project_sub_task_visibility_users.userid', $userid);
|
||||
});
|
||||
$builder->where(function ($query) use ($userid) {
|
||||
$query->where("project_tasks.visibility", 1);
|
||||
$query->orWhere("project_users.userid", $userid);
|
||||
$query->orWhere("project_task_users.userid", $userid);
|
||||
$query->orWhere("project_task_visibility_users.userid", $userid);
|
||||
$query->orWhere("project_sub_task_users.userid", $userid);
|
||||
$query->orWhere("project_sub_task_visibility_users.userid", $userid);
|
||||
});
|
||||
// 优化子查询汇总
|
||||
$builder->leftJoinSub(function ($query) {
|
||||
@@ -1136,6 +1183,7 @@ class ProjectController extends AbstractController
|
||||
$list = ProjectTask::with(['taskUser'])
|
||||
->select([
|
||||
'projects.name as project_name',
|
||||
'project_tasks.project_id',
|
||||
'project_tasks.id',
|
||||
'project_tasks.name',
|
||||
'project_tasks.start_at',
|
||||
@@ -1216,6 +1264,7 @@ class ProjectController extends AbstractController
|
||||
$headings[] = Doo::translate('父级任务ID');
|
||||
$headings[] = Doo::translate('所属项目');
|
||||
$headings[] = Doo::translate('任务标题');
|
||||
$headings[] = Doo::translate('任务标签');
|
||||
$headings[] = Doo::translate('任务开始时间');
|
||||
$headings[] = Doo::translate('任务结束时间');
|
||||
$headings[] = Doo::translate('完成时间');
|
||||
@@ -1236,7 +1285,7 @@ class ProjectController extends AbstractController
|
||||
'style' => 'font-weight: bold;padding-bottom: 4px;',
|
||||
];
|
||||
//
|
||||
$builder = ProjectTask::select(['project_tasks.*', 'project_task_users.userid as ownerid'])
|
||||
$builder = ProjectTask::with(['taskTag'])->select(['project_tasks.*', 'project_task_users.userid as ownerid'])
|
||||
->join('project_task_users', 'project_tasks.id', '=', 'project_task_users.task_id')
|
||||
->where('project_task_users.owner', 1)
|
||||
->whereIn('project_task_users.userid', $userid)
|
||||
@@ -1318,6 +1367,9 @@ class ProjectController extends AbstractController
|
||||
$task->parent_id ?: '-',
|
||||
Base::filterEmoji($task->project?->name) ?: '-',
|
||||
Base::filterEmoji($task->name),
|
||||
$task->taskTag->map(function ($tag) {
|
||||
return Base::filterEmoji($tag->name);
|
||||
})->join(', ') ?: '-',
|
||||
$task->start_at ?: '-',
|
||||
$task->end_at ?: '-',
|
||||
$task->complete_at ?: '-',
|
||||
@@ -1440,6 +1492,7 @@ class ProjectController extends AbstractController
|
||||
$headings[] = Doo::translate('父级任务ID');
|
||||
$headings[] = Doo::translate('所属项目');
|
||||
$headings[] = Doo::translate('任务标题');
|
||||
$headings[] = Doo::translate('任务标签');
|
||||
$headings[] = Doo::translate('任务开始时间');
|
||||
$headings[] = Doo::translate('任务结束时间');
|
||||
$headings[] = Doo::translate('任务计划用时');
|
||||
@@ -1448,7 +1501,8 @@ class ProjectController extends AbstractController
|
||||
$headings[] = Doo::translate('创建人');
|
||||
$data = [];
|
||||
//
|
||||
ProjectTask::whereNull('complete_at')
|
||||
ProjectTask::with(['taskTag'])
|
||||
->whereNull('complete_at')
|
||||
->whereNotNull('end_at')
|
||||
->where('end_at', '<=', Carbon::now())
|
||||
->orderBy('end_at')
|
||||
@@ -1479,11 +1533,14 @@ class ProjectController extends AbstractController
|
||||
$task->parent_id ?: '-',
|
||||
Base::filterEmoji($task->project?->name) ?: '-',
|
||||
Base::filterEmoji($task->name),
|
||||
$task->taskTag->map(function ($tag) {
|
||||
return Base::filterEmoji($tag->name);
|
||||
})->join(', ') ?: '-',
|
||||
$task->start_at ?: '-',
|
||||
$task->end_at ?: '-',
|
||||
$planTime,
|
||||
$overTime,
|
||||
implode("、", $ownerNames),
|
||||
implode(', ', $ownerNames),
|
||||
Base::filterEmoji(User::userid2nickname($task->userid)) . " (ID: {$task->userid})",
|
||||
];
|
||||
}
|
||||
@@ -1820,7 +1877,7 @@ class ProjectController extends AbstractController
|
||||
}
|
||||
//
|
||||
$filePath = public_path($file->getRawOriginal('path'));
|
||||
return Base::BinaryFileResponse($filePath, $file->name);
|
||||
return Base::DownloadFileResponse($filePath, $file->name);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1972,6 +2029,7 @@ class ProjectController extends AbstractController
|
||||
* @apiParam {String} [content] 任务详情(子任务不支持)
|
||||
* @apiParam {String} [color] 背景色(子任务不支持)
|
||||
* @apiParam {Array} [assist] 修改协助人员(子任务不支持)
|
||||
* @apiParam {Array} [task_tag] 任务标签(子任务不支持)
|
||||
* @apiParam {Number} [visibility] 修改可见性
|
||||
* @apiParam {Array} [visibility_appointor] 修改可见性人员
|
||||
*
|
||||
@@ -2264,8 +2322,8 @@ class ProjectController extends AbstractController
|
||||
* @apiGroup project
|
||||
* @apiName task__flow
|
||||
*
|
||||
* @apiParam {Number} task_id 任务ID
|
||||
* @apiParam {Number} project_id 项目ID - 存在时只返回这个项目的
|
||||
* @apiParam {Number} [task_id] 任务ID
|
||||
* @apiParam {Number} [project_id] 项目ID(存在时只返回这个项目的工作流,主要用于任务移动到其他项目时)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -2366,6 +2424,7 @@ class ProjectController extends AbstractController
|
||||
*/
|
||||
public function task__move()
|
||||
{
|
||||
Base::checkClientVersion('0.42.0');
|
||||
User::auth();
|
||||
//
|
||||
$task_id = intval(Request::input('task_id'));
|
||||
@@ -2403,9 +2462,26 @@ class ProjectController extends AbstractController
|
||||
//
|
||||
$task->moveTask($project_id, $column_id, $flow_item_id, $owner, $assist, $completeAt);
|
||||
//
|
||||
$task = ProjectTask::userTask($task_id);
|
||||
$data = [];
|
||||
$mainTask = ProjectTask::userTask($task_id)?->toArray();
|
||||
if ($mainTask) {
|
||||
$mainTask['column_name'] = ProjectColumn::whereId($mainTask['column_id'])->value('name');
|
||||
$mainTask['project_name'] = Project::whereId($mainTask['project_id'])->value('name');
|
||||
$data[] = $mainTask;
|
||||
//
|
||||
$subTasks = ProjectTask::whereParentId($task_id)->get();
|
||||
foreach ($subTasks as $subTask) {
|
||||
$data[] = [
|
||||
'id' => $subTask->id,
|
||||
'project_id' => $subTask->project_id,
|
||||
'column_id' => $subTask->column_id,
|
||||
'column_name' => $mainTask['column_name'],
|
||||
'project_name' => $mainTask['project_name'],
|
||||
];
|
||||
}
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('移动成功', $task);
|
||||
return Base::retSuccess('移动成功', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2534,7 +2610,7 @@ class ProjectController extends AbstractController
|
||||
$builder->with(['projectTask:id,parent_id,name'])->whereProjectId($project->id)->whereTaskOnly(0);
|
||||
}
|
||||
//
|
||||
$list = $builder->orderByDesc('created_at')->paginate(Base::getPaginate(100, 20));
|
||||
$list = $builder->orderByDesc('created_at')->orderByDesc('id')->paginate(Base::getPaginate(100, 20));
|
||||
$list->transform(function (ProjectLog $log) use ($task_id) {
|
||||
$timestamp = Carbon::parse($log->created_at)->timestamp;
|
||||
if ($task_id === 0) {
|
||||
@@ -2672,4 +2748,291 @@ class ProjectController extends AbstractController
|
||||
$projectPermission = ProjectPermission::updatePermissions($projectId, Base::newArrayRecursive('intval', $permissions));
|
||||
return Base::retSuccess("success", $projectPermission);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/project/task/template_list 47. 任务模板列表
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup project
|
||||
* @apiName task__template_list
|
||||
*
|
||||
* @apiParam {Number} project_id 项目ID
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function task__template_list()
|
||||
{
|
||||
User::auth();
|
||||
//
|
||||
$projectId = intval(Request::input('project_id'));
|
||||
if (!$projectId) {
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
$templates = ProjectTaskTemplate::where('project_id', $projectId)
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
return Base::retSuccess('success', $templates);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/project/task/template_save 48. 保存任务模板
|
||||
*
|
||||
* @apiDescription 需要token身份(限:项目负责人)
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup project
|
||||
* @apiName task__template_save
|
||||
*
|
||||
* @apiParam {Number} project_id 项目ID
|
||||
* @apiParam {Number} [id] 模板ID
|
||||
* @apiParam {String} name 模板名称
|
||||
* @apiParam {String} title 任务标题
|
||||
* @apiParam {String} content 任务内容
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function task__template_save()
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$projectId = intval(Request::input('project_id'));
|
||||
if (!$projectId) {
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
Project::userProject($projectId, true, true);
|
||||
//
|
||||
$id = intval(Request::input('id', 0));
|
||||
$name = trim(Request::input('name', ''));
|
||||
$title = trim(Request::input('title', ''));
|
||||
$content = trim(Request::input('content', ''));
|
||||
if (empty($name)) {
|
||||
return Base::retError('请输入模板名称');
|
||||
}
|
||||
if (empty($title) && empty($content)) {
|
||||
return Base::retError('请输入任务标题或内容');
|
||||
}
|
||||
$data = [
|
||||
'project_id' => $projectId,
|
||||
'name' => $name,
|
||||
'title' => $title,
|
||||
'content' => $content,
|
||||
'userid' => $user->userid
|
||||
];
|
||||
if ($id > 0) {
|
||||
$template = ProjectTaskTemplate::where('id', $id)
|
||||
->where('project_id', $projectId)
|
||||
->first();
|
||||
if (!$template) {
|
||||
return Base::retError('模板不存在或已被删除');
|
||||
}
|
||||
$template->update($data);
|
||||
} else {
|
||||
$templateCount = ProjectTaskTemplate::where('project_id', $projectId)->count();
|
||||
if ($templateCount >= 20) {
|
||||
return Base::retError('每个项目最多添加20个模板');
|
||||
}
|
||||
$template = ProjectTaskTemplate::create($data);
|
||||
}
|
||||
return Base::retSuccess('保存成功', $template);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/project/task/template_delete 49. 删除任务模板
|
||||
*
|
||||
* @apiDescription 需要token身份(限:项目负责人)
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup project
|
||||
* @apiName task__template_delete
|
||||
*
|
||||
* @apiParam {Number} id 模板ID
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function task__template_delete()
|
||||
{
|
||||
User::auth();
|
||||
//
|
||||
$id = intval(Request::input('id'));
|
||||
if (!$id) {
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
$template = ProjectTaskTemplate::find($id);
|
||||
if (!$template) {
|
||||
return Base::retError('模板不存在或已被删除');
|
||||
}
|
||||
Project::userProject($template->project_id, true, true);
|
||||
$template->delete();
|
||||
return Base::retSuccess('删除成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/project/task/template_default 50. 设置(取消)任务模板为默认
|
||||
*
|
||||
* @apiDescription 需要token身份(限:项目负责人)
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup project
|
||||
* @apiName task__template_default
|
||||
*
|
||||
* @apiParam {Number} id 模板ID
|
||||
* @apiParam {Number} project_id 项目ID
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function task__template_default()
|
||||
{
|
||||
User::auth();
|
||||
//
|
||||
$id = intval(Request::input('id'));
|
||||
$projectId = intval(Request::input('project_id'));
|
||||
if (!$id || !$projectId) {
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
Project::userProject($projectId, true, true);
|
||||
//
|
||||
$template = ProjectTaskTemplate::where('id', $id)
|
||||
->where('project_id', $projectId)
|
||||
->first();
|
||||
if (!$template) {
|
||||
return Base::retError('模板不存在或已被删除');
|
||||
}
|
||||
if ($template->is_default) {
|
||||
$template->update(['is_default' => false]);
|
||||
return Base::retSuccess('取消成功');
|
||||
}
|
||||
//
|
||||
ProjectTaskTemplate::where('project_id', $projectId)->update(['is_default' => false]);
|
||||
$template->update(['is_default' => true]);
|
||||
return Base::retSuccess('设置成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/project/tag/save 51. 保存标签
|
||||
*
|
||||
* @apiDescription 需要token身份(修改:项目负责人;添加:项目所有成员)
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup project
|
||||
* @apiName tag__save
|
||||
*
|
||||
* @apiParam {Number} project_id 项目ID
|
||||
* @apiParam {Number} [id] 标签ID
|
||||
* @apiParam {String} name 标签名称
|
||||
* @apiParam {String} desc 标签描述
|
||||
* @apiParam {String} color 标签颜色
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function tag__save()
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$projectId = intval(Request::input('project_id'));
|
||||
if (!$projectId) {
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
//
|
||||
$id = intval(Request::input('id', 0));
|
||||
$name = trim(Request::input('name', ''));
|
||||
$desc = trim(Request::input('desc', ''));
|
||||
$color = trim(Request::input('color', ''));
|
||||
if (empty($name)) {
|
||||
return Base::retError('请输入标签名称');
|
||||
}
|
||||
if (empty($color)) {
|
||||
return Base::retError('请选择标签颜色');
|
||||
}
|
||||
$data = [
|
||||
'project_id' => $projectId,
|
||||
'name' => $name,
|
||||
'desc' => $desc,
|
||||
'color' => $color,
|
||||
'userid' => $user->userid
|
||||
];
|
||||
$project = Project::userProject($projectId, true, $id > 0 ? true : null);
|
||||
if ($id > 0) {
|
||||
$tag = ProjectTag::where('id', $id)
|
||||
->where('project_id', $projectId)
|
||||
->first();
|
||||
if (!$tag) {
|
||||
return Base::retError('标签不存在或已被删除');
|
||||
}
|
||||
$tag->update($data);
|
||||
} else {
|
||||
$tagCount = ProjectTag::where('project_id', $projectId)->count();
|
||||
if ($tagCount >= 20) {
|
||||
return Base::retError('每个项目最多添加20个标签');
|
||||
}
|
||||
$tag = ProjectTag::create($data);
|
||||
$project->addLog("添加标签【" . $tag->name . "】");
|
||||
}
|
||||
return Base::retSuccess('保存成功', $tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/project/tag/delete 52. 删除标签
|
||||
*
|
||||
* @apiDescription 需要token身份(限:项目负责人)
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup project
|
||||
* @apiName tag__delete
|
||||
*
|
||||
* @apiParam {Number} id 标签ID
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function tag__delete()
|
||||
{
|
||||
User::auth();
|
||||
//
|
||||
$id = intval(Request::input('id'));
|
||||
if (!$id) {
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
$tag = ProjectTag::find($id);
|
||||
if (!$tag) {
|
||||
return Base::retError('标签不存在或已被删除');
|
||||
}
|
||||
Project::userProject($tag->project_id, true, true);
|
||||
$tag->delete();
|
||||
return Base::retSuccess('删除成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/project/tag/list 53. 标签列表
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup project
|
||||
* @apiName tag__list
|
||||
*
|
||||
* @apiParam {Number} project_id 项目ID
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function tag__list()
|
||||
{
|
||||
User::auth();
|
||||
//
|
||||
$projectId = intval(Request::input('project_id'));
|
||||
if (!$projectId) {
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
$tags = ProjectTag::where('project_id', $projectId)
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
return Base::retSuccess('success', $tags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ use App\Exceptions\ApiException;
|
||||
use App\Models\AbstractModel;
|
||||
use App\Models\ProjectTask;
|
||||
use App\Models\Report;
|
||||
use App\Models\ReportLink;
|
||||
use App\Models\ReportReceive;
|
||||
use App\Models\User;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Tasks\PushTask;
|
||||
@@ -28,11 +30,13 @@ class ReportController extends AbstractController
|
||||
/**
|
||||
* @api {get} api/report/my 01. 我发送的汇报
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName my
|
||||
*
|
||||
* @apiParam {Object} [keys] 搜索条件
|
||||
* - keys.key: 关键词
|
||||
* - keys.type: 汇报类型,weekly:周报,daily:日报
|
||||
* - keys.created_at: 汇报时间
|
||||
* @apiParam {Number} [page] 当前页,默认:1
|
||||
@@ -49,6 +53,15 @@ class ReportController extends AbstractController
|
||||
$builder = Report::with(['receivesUser'])->whereUserid($user->userid);
|
||||
$keys = Request::input('keys');
|
||||
if (is_array($keys)) {
|
||||
if ($keys['key']) {
|
||||
if (str_contains($keys['key'], '@')) {
|
||||
$builder->whereHas('sendUser', function ($q2) use ($keys) {
|
||||
$q2->where("users.email", "LIKE", "%{$keys['key']}%");
|
||||
});
|
||||
} else {
|
||||
$builder->where("title", "LIKE", "%{$keys['key']}%");
|
||||
}
|
||||
}
|
||||
if (in_array($keys['type'], [Report::WEEKLY, Report::DAILY])) {
|
||||
$builder->whereType($keys['type']);
|
||||
}
|
||||
@@ -64,13 +77,16 @@ class ReportController extends AbstractController
|
||||
/**
|
||||
* @api {get} api/report/receive 02. 我接收的汇报
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName receive
|
||||
*
|
||||
* @apiParam {Object} [keys] 搜索条件
|
||||
* - keys.key: 关键词
|
||||
* - keys.department_id: 部门ID
|
||||
* - keys.type: 汇报类型,weekly:周报,daily:日报
|
||||
* - keys.status: 状态,unread:未读,read:已读
|
||||
* - keys.created_at: 汇报时间
|
||||
* @apiParam {Number} [page] 当前页,默认:1
|
||||
* @apiParam {Number} [pagesize] 每页显示数量,默认:20,最大:50
|
||||
@@ -89,15 +105,29 @@ class ReportController extends AbstractController
|
||||
$keys = Request::input('keys');
|
||||
if (is_array($keys)) {
|
||||
if ($keys['key']) {
|
||||
$builder->where(function($query) use ($keys) {
|
||||
$query->whereHas('sendUser', function ($q2) use ($keys) {
|
||||
if (str_contains($keys['key'], '@')) {
|
||||
$builder->whereHas('sendUser', function ($q2) use ($keys) {
|
||||
$q2->where("users.email", "LIKE", "%{$keys['key']}%");
|
||||
})->orWhere("title", "LIKE", "%{$keys['key']}%");
|
||||
});
|
||||
} elseif (Base::isNumber($keys['key'])) {
|
||||
$builder->where("userid", intval($keys['key']));
|
||||
} else {
|
||||
$builder->where("title", "LIKE", "%{$keys['key']}%");
|
||||
}
|
||||
}
|
||||
if ($keys['department_id']) {
|
||||
$builder->whereHas('sendUser', function ($query) use ($keys) {
|
||||
$query->where("users.department", "LIKE", "%,{$keys['department_id']},%");
|
||||
});
|
||||
}
|
||||
if (in_array($keys['type'], [Report::WEEKLY, Report::DAILY])) {
|
||||
$builder->whereType($keys['type']);
|
||||
}
|
||||
if (in_array($keys['status'], ['unread', 'read'])) {
|
||||
$builder->whereHas("receivesUser", function ($query) use ($user, $keys) {
|
||||
$query->where("report_receives.userid", $user->userid)->where("report_receives.read", $keys['status'] === 'unread' ? 0 : 1);
|
||||
});
|
||||
}
|
||||
if (is_array($keys['created_at'])) {
|
||||
if ($keys['created_at'][0] > 0) $builder->where('created_at', '>=', Base::newCarbon($keys['created_at'][0])->startOfDay());
|
||||
if ($keys['created_at'][1] > 0) $builder->where('created_at', '<=', Base::newCarbon($keys['created_at'][1])->endOfDay());
|
||||
@@ -115,6 +145,7 @@ class ReportController extends AbstractController
|
||||
/**
|
||||
* @api {get} api/report/store 03. 保存并发送工作汇报
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName store
|
||||
@@ -240,6 +271,7 @@ class ReportController extends AbstractController
|
||||
/**
|
||||
* @api {get} api/report/template 04. 生成汇报模板
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName template
|
||||
@@ -411,11 +443,13 @@ class ReportController extends AbstractController
|
||||
/**
|
||||
* @api {get} api/report/detail 05. 报告详情
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName detail
|
||||
*
|
||||
* @apiParam {Number} [id] 报告id
|
||||
* @apiParam {Number} [id] 报告ID
|
||||
* @apiParam {String} [code] 报告分享代码,与ID二选一,优先ID
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -424,30 +458,43 @@ class ReportController extends AbstractController
|
||||
public function detail(): array
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$id = intval(trim(Request::input("id")));
|
||||
if (empty($id))
|
||||
$code = trim(Request::input("code"));
|
||||
//
|
||||
if (empty($id) && empty($code)) {
|
||||
return Base::retError("缺少ID参数");
|
||||
|
||||
$one = Report::getOne($id);
|
||||
$one->type_val = $one->getRawOriginal("type");
|
||||
|
||||
// 标记为已读
|
||||
if (!empty($one->receivesUser)) {
|
||||
foreach ($one->receivesUser as $item) {
|
||||
if ($item->userid === $user->userid && $item->pivot->read === 0) {
|
||||
$one->receivesUser()->updateExistingPivot($user->userid, [
|
||||
"read" => 1,
|
||||
]);
|
||||
}
|
||||
//
|
||||
if (!empty($id)) {
|
||||
$one = Report::getOne($id);
|
||||
$one->type_val = $one->getRawOriginal("type");
|
||||
// 标记为已读
|
||||
if (!empty($one->receivesUser)) {
|
||||
foreach ($one->receivesUser as $item) {
|
||||
if ($item->userid === $user->userid && $item->pivot->read === 0) {
|
||||
$one->receivesUser()->updateExistingPivot($user->userid, [
|
||||
"read" => 1,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$link = ReportLink::whereCode($code)->first();
|
||||
if (empty($link)) {
|
||||
return Base::retError("报告不存在或已被删除");
|
||||
}
|
||||
$one = Report::getOne($link->rid);
|
||||
$one->report_link = $link;
|
||||
$link->increment("num");
|
||||
}
|
||||
|
||||
return Base::retSuccess("success", $one);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/mark 06. 标记已读/未读
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName mark
|
||||
@@ -488,8 +535,71 @@ class ReportController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/last_submitter 07. 获取最后一次提交的接收人
|
||||
* @api {get} api/report/share 07. 分享报告到消息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName share
|
||||
*
|
||||
* @apiParam {Number} id 报告id(组)
|
||||
* @apiParam {Array} dialogids 转发给的对话ID
|
||||
* @apiParam {Array} userids 转发给的成员ID
|
||||
* @apiParam {String} leave_message 转发留言
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function share()
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$id = Request::input('id');
|
||||
$dialogids = Request::input('dialogids');
|
||||
$userids = Request::input('userids');
|
||||
$leave_message = Request::input('leave_message');
|
||||
//
|
||||
if (is_array($id)) {
|
||||
if (count(Base::arrayRetainInt($id)) > 20) {
|
||||
return Base::retError("最多只能操作20条数据");
|
||||
}
|
||||
$builder = Report::whereIn("id", Base::arrayRetainInt($id));
|
||||
} else {
|
||||
$builder = Report::whereId(intval($id));
|
||||
}
|
||||
$reportMsgs = [];
|
||||
$builder ->chunkById(100, function ($list) use (&$reportMsgs, $user) {
|
||||
/** @var Report $item */
|
||||
foreach ($list as $item) {
|
||||
$reportLink = ReportLink::generateLink($item->id, $user->userid);
|
||||
$reportMsgs[] = "<a class=\"mention report\" href=\"{{RemoteURL}}single/report/detail/{$reportLink['code']}\" target=\"_blank\">%{$item->title}</a>";
|
||||
}
|
||||
});
|
||||
if (empty($reportMsgs)) {
|
||||
return Base::retError("报告不存在或已被删除");
|
||||
}
|
||||
$reportTag = count($reportMsgs) > 1 ? 'li' : 'p';
|
||||
$reportAttr = $reportTag === 'li' ? ' data-list="ordered"' : '';
|
||||
$reportMsgs = array_map(function ($item) use ($reportAttr, $reportTag) {
|
||||
return "<{$reportTag}{$reportAttr}>{$item}</{$reportTag}>";
|
||||
}, $reportMsgs);
|
||||
if ($reportTag === 'li') {
|
||||
array_unshift($reportMsgs, "<ol>");
|
||||
$reportMsgs[] = "</ol>";
|
||||
}
|
||||
if ($leave_message) {
|
||||
$reportMsgs[] = "<p>{$leave_message}</p>";
|
||||
}
|
||||
$msgText = implode("", $reportMsgs);
|
||||
//
|
||||
return WebSocketDialogMsg::sendMsgBatch($user, $userids, $dialogids, $msgText);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/last_submitter 08. 获取最后一次提交的接收人
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName last_submitter
|
||||
@@ -505,8 +615,9 @@ class ReportController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/unread 08. 获取未读
|
||||
* @api {get} api/report/unread 09. 获取未读
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName unread
|
||||
@@ -529,8 +640,9 @@ class ReportController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/report/read 09. 标记汇报已读,可批量
|
||||
* @api {get} api/report/read 10. 标记汇报已读,可批量
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup report
|
||||
* @apiName read
|
||||
|
||||
@@ -41,7 +41,7 @@ class SystemController extends AbstractController
|
||||
* @apiParam {String} type
|
||||
* - get: 获取(默认)
|
||||
* - all: 获取所有(需要管理员权限)
|
||||
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'voice2text', 'translation', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'system_alias', 'image_compress', 'image_save_local', 'start_home'])
|
||||
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'voice2text', 'translation', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'system_alias', 'system_welcome', 'image_compress', 'image_quality', 'image_save_local', 'start_home'])
|
||||
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -54,6 +54,7 @@ class SystemController extends AbstractController
|
||||
if (env("SYSTEM_SETTING") == 'disabled') {
|
||||
return Base::retError('当前环境禁止修改');
|
||||
}
|
||||
Base::checkClientVersion('0.41.11');
|
||||
User::auth('admin');
|
||||
$all = Request::input();
|
||||
foreach ($all AS $key => $value) {
|
||||
@@ -70,6 +71,8 @@ class SystemController extends AbstractController
|
||||
'voice2text',
|
||||
'translation',
|
||||
'e2e_message',
|
||||
'msg_rev_limit',
|
||||
'msg_edit_limit',
|
||||
'auto_archived',
|
||||
'archived_day',
|
||||
'task_visible',
|
||||
@@ -79,7 +82,9 @@ class SystemController extends AbstractController
|
||||
'user_private_chat_mute',
|
||||
'user_group_chat_mute',
|
||||
'system_alias',
|
||||
'system_welcome',
|
||||
'image_compress',
|
||||
'image_quality',
|
||||
'image_save_local',
|
||||
'start_home',
|
||||
'file_upload_limit',
|
||||
@@ -97,15 +102,18 @@ class SystemController extends AbstractController
|
||||
return Base::retError('自动归档时间不可大于100天!');
|
||||
}
|
||||
}
|
||||
if ($all['voice2text'] == 'open' && empty(Base::settingFind('aibotSetting', 'openai_key'))) {
|
||||
if ($all['voice2text'] == 'open' && !Setting::AIOpen()) {
|
||||
return Base::retError('开启语音转文字功能需要在应用中开启 ChatGPT AI 机器人。');
|
||||
}
|
||||
if ($all['translation'] == 'open' && empty(Base::settingFind('aibotSetting', 'openai_key'))) {
|
||||
if ($all['translation'] == 'open' && !Setting::AIOpen()) {
|
||||
return Base::retError('开启翻译功能需要在应用中开启 ChatGPT AI 机器人。');
|
||||
}
|
||||
if ($all['system_alias'] == env('APP_NAME')) {
|
||||
$all['system_alias'] = '';
|
||||
}
|
||||
if ($all['system_welcome'] == '欢迎您,{username}') {
|
||||
$all['system_welcome'] = '';
|
||||
}
|
||||
$setting = Base::setting('system', Base::newTrim($all));
|
||||
} else {
|
||||
$setting = Base::setting('system');
|
||||
@@ -129,6 +137,8 @@ class SystemController extends AbstractController
|
||||
$setting['voice2text'] = $setting['voice2text'] ?: 'close';
|
||||
$setting['translation'] = $setting['translation'] ?: 'close';
|
||||
$setting['e2e_message'] = $setting['e2e_message'] ?: 'close';
|
||||
$setting['msg_rev_limit'] = $setting['msg_rev_limit'] ?: '';
|
||||
$setting['msg_edit_limit'] = $setting['msg_edit_limit'] ?: '';
|
||||
$setting['auto_archived'] = $setting['auto_archived'] ?: 'close';
|
||||
$setting['archived_day'] = floatval($setting['archived_day']) ?: 7;
|
||||
$setting['task_visible'] = $setting['task_visible'] ?: 'close';
|
||||
@@ -136,8 +146,6 @@ class SystemController extends AbstractController
|
||||
$setting['all_group_autoin'] = $setting['all_group_autoin'] ?: 'yes';
|
||||
$setting['user_private_chat_mute'] = $setting['user_private_chat_mute'] ?: 'open';
|
||||
$setting['user_group_chat_mute'] = $setting['user_group_chat_mute'] ?: 'open';
|
||||
$setting['image_compress'] = $setting['image_compress'] ?: 'open';
|
||||
$setting['image_save_local'] = $setting['image_save_local'] ?: 'open';
|
||||
$setting['start_home'] = $setting['start_home'] ?: 'close';
|
||||
$setting['file_upload_limit'] = $setting['file_upload_limit'] ?: '';
|
||||
$setting['unclaimed_task_reminder'] = $setting['unclaimed_task_reminder'] ?: 'close';
|
||||
@@ -282,100 +290,46 @@ class SystemController extends AbstractController
|
||||
*
|
||||
* @apiParam {String} type
|
||||
* - get: 获取(默认)
|
||||
* - save: 保存设置(参数:['openai_key', 'openai_agency', 'claude_token', 'claude_agency'])
|
||||
* - save: 保存设置(参数:[...])
|
||||
* @apiParam {String} filter 过滤字段(可选)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function setting__aibot()
|
||||
{
|
||||
$user = User::auth('admin');
|
||||
User::auth('admin');
|
||||
//
|
||||
$type = trim(Request::input('type'));
|
||||
$filter = trim(Request::input('filter'));
|
||||
$setting = Base::setting('aibotSetting');
|
||||
|
||||
$keys = [
|
||||
'openai_key',
|
||||
'openai_agency',
|
||||
'openai_model',
|
||||
'claude_token',
|
||||
'claude_agency',
|
||||
'wenxin_key',
|
||||
'wenxin_secret',
|
||||
'wenxin_model',
|
||||
'qianwen_key',
|
||||
'qianwen_model',
|
||||
'gemini_key',
|
||||
'gemini_model',
|
||||
'gemini_agency',
|
||||
'zhipu_key',
|
||||
'zhipu_model',
|
||||
];
|
||||
|
||||
if ($type == 'save') {
|
||||
if (env("SYSTEM_SETTING") == 'disabled') {
|
||||
return Base::retError('当前环境禁止修改');
|
||||
}
|
||||
Base::checkClientVersion('0.41.11');
|
||||
$all = Request::input();
|
||||
foreach ($all as $key => $value) {
|
||||
if (!in_array($key, $keys)) {
|
||||
unset($all[$key]);
|
||||
}
|
||||
}
|
||||
$backup = $setting;
|
||||
$setting = Base::setting('aibotSetting', Base::newTrim($all));
|
||||
$tempMsg = [
|
||||
'type' => 'content',
|
||||
'content' => '设置成功'
|
||||
];
|
||||
//
|
||||
if ($backup['openai_key'] != $setting['openai_key']) {
|
||||
$botUser = User::botGetOrCreate('ai-openai');
|
||||
if ($botUser && $dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', $tempMsg, $botUser->userid, true, false, true);
|
||||
}
|
||||
}
|
||||
if ($backup['claude_token'] != $setting['claude_token']) {
|
||||
$botUser = User::botGetOrCreate('ai-claude');
|
||||
if ($botUser && $dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', $tempMsg, $botUser->userid, true, false, true);
|
||||
}
|
||||
}
|
||||
if ($backup['wenxin_key'] != $setting['wenxin_key']) {
|
||||
$botUser = User::botGetOrCreate('ai-wenxin');
|
||||
if ($botUser && $dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', $tempMsg, $botUser->userid, true, false, true);
|
||||
}
|
||||
}
|
||||
if ($backup['qianwen_key'] != $setting['qianwen_key']) {
|
||||
$botUser = User::botGetOrCreate('ai-qianwen');
|
||||
if ($botUser && $dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', $tempMsg, $botUser->userid, true, false, true);
|
||||
}
|
||||
}
|
||||
if ($backup['gemini_key'] != $setting['gemini_key']) {
|
||||
$botUser = User::botGetOrCreate('ai-gemini');
|
||||
if ($botUser && $dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', $tempMsg, $botUser->userid, true, false, true);
|
||||
}
|
||||
}
|
||||
if ($backup['zhipu_key'] != $setting['zhipu_key']) {
|
||||
$botUser = User::botGetOrCreate('ai-zhipu');
|
||||
if ($botUser && $dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', $tempMsg, $botUser->userid, true, false, true);
|
||||
if (isset($setting[$key])) {
|
||||
$setting[$key] = $value;
|
||||
}
|
||||
}
|
||||
$setting = Base::setting('aibotSetting', Base::newTrim($setting));
|
||||
}
|
||||
if ($filter) {
|
||||
$setting = array_filter($setting, function($value, $key) use ($filter) {
|
||||
return str_starts_with($key, $filter);
|
||||
}, ARRAY_FILTER_USE_BOTH);
|
||||
}
|
||||
//
|
||||
$setting['openai_model'] = $setting['openai_model'] ?: 'gpt-3.5-turbo';
|
||||
$setting['wenxin_model'] = $setting['wenxin_model'] ?: 'eb-instant';
|
||||
$setting['qianwen_model'] = $setting['qianwen_model'] ?: 'qwen-v1';
|
||||
$setting['gemini_model'] = $setting['gemini_model'] ?: 'gemini-1.0-pro';
|
||||
$setting['zhipu_model'] = $setting['zhipu_model'] ?: 'glm-4';
|
||||
if (env("SYSTEM_SETTING") == 'disabled') {
|
||||
foreach ($keys as $item) {
|
||||
if (strlen($setting[$item]) > 12) {
|
||||
$setting[$item] = substr($setting[$item], 0, 4) . str_repeat('*', strlen($setting[$item]) - 8) . substr($setting[$item], -4);
|
||||
foreach ($setting as $key => $item) {
|
||||
if (empty($item)) {
|
||||
continue;
|
||||
}
|
||||
if (str_ends_with($key, '_key') || str_ends_with($key, '_secret')) {
|
||||
$setting[$key] = substr($item, 0, 4) . str_repeat('*', strlen($item) - 8) . substr($item, -4);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -384,7 +338,66 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/checkin 05. 获取签到设置、保存签到设置(限管理员)
|
||||
* @api {get} api/system/setting/aibot_models 05. 获取AI模型
|
||||
*
|
||||
* @apiDescription 获取所有AI机器人模型设置
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
* @apiName aibot_models
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function setting__aibot_models()
|
||||
{
|
||||
$setting = Base::setting('aibotSetting');
|
||||
$setting = array_filter($setting, function($value, $key) {
|
||||
return str_ends_with($key, '_models') || str_ends_with($key, '_model');
|
||||
}, ARRAY_FILTER_USE_BOTH);
|
||||
return Base::retSuccess('success', $setting ?: json_decode('{}'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/aibot_defmodels 06. 获取AI默认模型
|
||||
*
|
||||
* @apiDescription 获取AI机器人默认模型
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
* @apiName setting__aibot_defmodels
|
||||
*
|
||||
* @apiParam {String} type AI类型
|
||||
* @apiParam {String} [base_url] 基础URL(仅 type=ollama 时有效)
|
||||
* @apiParam {String} [key] Key(仅 type=ollama 时有效)
|
||||
* @apiParam {String} [agency] 使用代理(仅 type=ollama 时有效)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function setting__aibot_defmodels()
|
||||
{
|
||||
$type = trim(Request::input('type'));
|
||||
if ($type == 'ollama') {
|
||||
$baseUrl = trim(Request::input('base_url'));
|
||||
$key = trim(Request::input('key'));
|
||||
$agency = trim(Request::input('agency'));
|
||||
if (empty($baseUrl)) {
|
||||
return Base::retError('请先填写 Base URL');
|
||||
}
|
||||
return Extranet::ollamaModels($baseUrl, $key, $agency);
|
||||
}
|
||||
$models = Setting::AIDefaultModels($type);
|
||||
if (empty($models)) {
|
||||
return Base::retError('未找到默认模型');
|
||||
}
|
||||
return Base::retSuccess('success', [
|
||||
'models' => $models
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/checkin 07. 获取签到设置、保存签到设置(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -490,7 +503,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/apppush 06. 获取APP推送设置、保存APP推送设置(限管理员)
|
||||
* @api {get} api/system/setting/apppush 08. 获取APP推送设置、保存APP推送设置(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -535,7 +548,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/thirdaccess 07. 第三方帐号(限管理员)
|
||||
* @api {get} api/system/setting/thirdaccess 09. 第三方帐号(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -605,7 +618,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/file 08. 文件设置(限管理员)
|
||||
* @api {get} api/system/setting/file 10. 文件设置(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -645,7 +658,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/demo 09. 获取演示帐号
|
||||
* @api {get} api/system/demo 11. 获取演示帐号
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -669,7 +682,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/priority 10. 任务优先级
|
||||
* @api {post} api/system/priority 12. 任务优先级
|
||||
*
|
||||
* @apiDescription 获取任务优先级、保存任务优先级
|
||||
* @apiVersion 1.0.0
|
||||
@@ -718,7 +731,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/column/template 11. 创建项目模板
|
||||
* @api {post} api/system/column/template 13. 创建项目模板
|
||||
*
|
||||
* @apiDescription 获取创建项目模板、保存创建项目模板
|
||||
* @apiVersion 1.0.0
|
||||
@@ -765,7 +778,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/license 12. License
|
||||
* @api {post} api/system/license 14. License
|
||||
*
|
||||
* @apiDescription 获取License信息、保存License(限管理员)
|
||||
* @apiVersion 1.0.0
|
||||
@@ -824,11 +837,17 @@ class SystemController extends AbstractController
|
||||
$data['error'][] = '终端License已过期';
|
||||
}
|
||||
//
|
||||
if ($type === 'error') {
|
||||
$data = [
|
||||
'error' => $data['error']
|
||||
];
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('success', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/info 13. 获取终端详细信息
|
||||
* @api {get} api/system/get/info 15. 获取终端详细信息
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -857,7 +876,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/ip 14. 获取IP地址
|
||||
* @api {get} api/system/get/ip 16. 获取IP地址
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -872,7 +891,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/cnip 15. 是否中国IP地址
|
||||
* @api {get} api/system/get/cnip 17. 是否中国IP地址
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -889,7 +908,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/ipgcj02 16. 获取IP地址经纬度
|
||||
* @api {get} api/system/get/ipgcj02 18. 获取IP地址经纬度
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -906,7 +925,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/ipinfo 17. 获取IP地址详细信息
|
||||
* @api {get} api/system/get/ipinfo 19. 获取IP地址详细信息
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -923,7 +942,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/imgupload 18. 上传图片
|
||||
* @api {post} api/system/imgupload 20. 上传图片
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -932,7 +951,7 @@ class SystemController extends AbstractController
|
||||
*
|
||||
* @apiParam {File} image post-图片对象
|
||||
* @apiParam {String} [image64] post-图片base64(与'image'二选一)
|
||||
* @apiParam {String} filename post-文件名
|
||||
* @apiParam {String} [filename] post-文件名
|
||||
* @apiParam {Number} [width] 压缩图片宽(默认0)
|
||||
* @apiParam {Number} [height] 压缩图片高(默认0)
|
||||
* @apiParam {String} [whcut] 压缩方式(等比缩放)
|
||||
@@ -969,7 +988,7 @@ class SystemController extends AbstractController
|
||||
"path" => $path,
|
||||
"fileName" => $fileName,
|
||||
"scale" => $scale,
|
||||
"quality" => 85
|
||||
"quality" => true
|
||||
]);
|
||||
} else {
|
||||
$data = Base::upload([
|
||||
@@ -978,7 +997,7 @@ class SystemController extends AbstractController
|
||||
"path" => $path,
|
||||
"fileName" => $fileName,
|
||||
"scale" => $scale,
|
||||
"quality" => 100
|
||||
"quality" => true
|
||||
]);
|
||||
}
|
||||
if (Base::isError($data)) {
|
||||
@@ -989,7 +1008,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/imgview 19. 浏览图片空间
|
||||
* @api {get} api/system/get/imgview 21. 浏览图片空间
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1086,16 +1105,16 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/system/fileupload 20. 上传文件
|
||||
* @api {post} api/system/fileupload 22. 上传文件
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
* @apiName fileupload
|
||||
*
|
||||
* @apiParam {String} [image64] 图片base64
|
||||
* @apiParam {String} filename 文件名
|
||||
* @apiParam {String} [files] 文件名
|
||||
* @apiParam {File} files 文件名
|
||||
* @apiParam {String} [image64] 图片base64(与'files'二选一)
|
||||
* @apiParam {String} [filename] 文件名
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -1114,7 +1133,7 @@ class SystemController extends AbstractController
|
||||
"image64" => $image64,
|
||||
"path" => $path,
|
||||
"fileName" => $fileName,
|
||||
"quality" => 85
|
||||
"quality" => true
|
||||
]);
|
||||
} else {
|
||||
$data = Base::upload([
|
||||
@@ -1122,7 +1141,7 @@ class SystemController extends AbstractController
|
||||
"type" => 'file',
|
||||
"path" => $path,
|
||||
"fileName" => $fileName,
|
||||
"quality" => 100
|
||||
"quality" => true
|
||||
]);
|
||||
}
|
||||
//
|
||||
@@ -1130,7 +1149,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/get/updatelog 21. 获取更新日志
|
||||
* @api {get} api/system/get/updatelog 23. 获取更新日志
|
||||
*
|
||||
* @apiDescription 获取更新日志
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1163,7 +1182,7 @@ class SystemController extends AbstractController
|
||||
if ($logResults) {
|
||||
$logVersion = $logResults[0]['title'];
|
||||
$logContent = implode("\n", array_map(function($item) {
|
||||
return "## [{$item['title']}]" . $item['content'];
|
||||
return "## {$item['title']}" . $item['content'];
|
||||
}, $logResults));
|
||||
}
|
||||
return Base::retSuccess('success', [
|
||||
@@ -1173,7 +1192,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/email/check 22. 邮件发送测试(限管理员)
|
||||
* @api {get} api/system/email/check 24. 邮件发送测试(限管理员)
|
||||
*
|
||||
* @apiDescription 测试配置邮箱是否能发送邮件
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1219,7 +1238,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/checkin/export 23. 导出签到数据(限管理员)
|
||||
* @api {get} api/system/checkin/export 25. 导出签到数据(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -1388,7 +1407,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/checkin/down 24. 下载导出的签到数据
|
||||
* @api {get} api/system/checkin/down 26. 下载导出的签到数据
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -1414,7 +1433,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/version 25. 获取版本号
|
||||
* @api {get} api/system/version 27. 获取版本号
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -1451,7 +1470,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/prefetch 26. 预加载的资源
|
||||
* @api {get} api/system/prefetch 28. 预加载的资源
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
@@ -1491,7 +1510,7 @@ class SystemController extends AbstractController
|
||||
}
|
||||
// 添加office资源
|
||||
$officePath = '';
|
||||
$officeApi = 'http://' . env('APP_IPPR') . '.6/web-apps/apps/api/documents/api.js';
|
||||
$officeApi = 'http://office/web-apps/apps/api/documents/api.js';
|
||||
$content = @file_get_contents($officeApi);
|
||||
if ($content) {
|
||||
if (preg_match("/const\s+ver\s*=\s*'\/*([^']+)'/", $content, $matches)) {
|
||||
|
||||
@@ -520,7 +520,8 @@ class UsersController extends AbstractController
|
||||
} else {
|
||||
$builder->where(function($query) use ($keys) {
|
||||
$query->where("nickname", "like", "%{$keys['key']}%")
|
||||
->orWhere("pinyin", "like", "%{$keys['key']}%");
|
||||
->orWhere("pinyin", "like", "%{$keys['key']}%")
|
||||
->orWhere("profession", "like", "%{$keys['key']}%");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -610,7 +611,38 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/basic 11. 获取指定会员基础信息
|
||||
* @api {get} api/users/search/ai 11. 获取AI机器人
|
||||
*
|
||||
* @apiDescription 搜索会员列表
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName search__ai
|
||||
*
|
||||
* @apiParam {String} type AI 类型(比如:openai)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function search__ai()
|
||||
{
|
||||
User::auth();
|
||||
//
|
||||
$type = trim(Request::input('type'));
|
||||
$botName = "ai-{$type}";
|
||||
if (!UserBot::systemBotName($botName)) {
|
||||
return Base::retError('AI机器人不存在');
|
||||
}
|
||||
//
|
||||
$botUser = User::botGetOrCreate($botName);
|
||||
if (empty($botUser)) {
|
||||
return Base::retError('AI机器人不存在');
|
||||
}
|
||||
return Base::retSuccess('success', $botUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/basic 12. 获取指定会员基础信息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -653,7 +685,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/lists 12. 会员列表(限管理员)
|
||||
* @api {get} api/users/lists 13. 会员列表(限管理员)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -802,7 +834,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/operation 13. 操作会员(限管理员)
|
||||
* @api {get} api/users/operation 14. 操作会员(限管理员)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1060,7 +1092,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/email/verification 14. 邮箱验证
|
||||
* @api {get} api/users/email/verification 15. 邮箱验证
|
||||
*
|
||||
* @apiDescription 不需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1108,7 +1140,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/umeng/alias 15. 设置友盟别名
|
||||
* @api {get} api/users/umeng/alias 16. 设置友盟别名
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1119,6 +1151,10 @@ class UsersController extends AbstractController
|
||||
* - update: 更新(默认)
|
||||
* - remove: 删除
|
||||
* @apiParam {String} alias 别名
|
||||
* @apiParam {String} [userAgent] 浏览器信息
|
||||
* @apiParam {String} [deviceModel] 设备型号
|
||||
* @apiParam {String} [isNotified] 是否有通知权限(0不通知、1通知)
|
||||
* @apiParam {Number} [isDebug] 是否调试(0不调试、1调试)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -1127,6 +1163,10 @@ class UsersController extends AbstractController
|
||||
public function umeng__alias()
|
||||
{
|
||||
$data = Request::input();
|
||||
// 判断是否调试
|
||||
if (intval($data['isDebug'])) {
|
||||
return Base::retError('调试模式下不允许使用');
|
||||
}
|
||||
// 表单验证
|
||||
Base::validator($data, [
|
||||
'alias.required' => '别名不能为空',
|
||||
@@ -1150,11 +1190,15 @@ class UsersController extends AbstractController
|
||||
'alias' => $data['alias'],
|
||||
'platform' => Base::platform(),
|
||||
];
|
||||
$version = $data['appVersion'] ? ($data['appVersionName'] . " ({$data['appVersion']})") : '';
|
||||
$isNotified = trim($data['isNotified']) === 'true' || $data['isNotified'] === true ? 1 : intval($data['isNotified']);
|
||||
$row = UmengAlias::where($inArray);
|
||||
if ($row->exists()) {
|
||||
$row->update([
|
||||
'ua' => $data['userAgent'],
|
||||
'device' => $data['deviceModel'],
|
||||
'version' => $version,
|
||||
'is_notified' => $isNotified,
|
||||
'updated_at' => Carbon::now()
|
||||
]);
|
||||
return Base::retSuccess('别名已存在');
|
||||
@@ -1162,6 +1206,8 @@ class UsersController extends AbstractController
|
||||
$row = UmengAlias::createInstance(array_merge($inArray, [
|
||||
'ua' => $data['userAgent'],
|
||||
'device' => $data['deviceModel'],
|
||||
'version' => $version,
|
||||
'is_notified' => $isNotified,
|
||||
]));
|
||||
if ($row->save()) {
|
||||
return Base::retSuccess('添加成功');
|
||||
@@ -1171,7 +1217,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/meeting/open 16. 【会议】创建会议、加入会议
|
||||
* @api {get} api/users/meeting/open 17. 【会议】创建会议、加入会议
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1289,7 +1335,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/meeting/link 17. 【会议】获取分享链接
|
||||
* @api {get} api/users/meeting/link 18. 【会议】获取分享链接
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1318,7 +1364,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/meeting/tourist 18. 【会议】游客信息
|
||||
* @api {get} api/users/meeting/tourist 19. 【会议】游客信息
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1341,7 +1387,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/meeting/invitation 19. 【会议】发送邀请
|
||||
* @api {get} api/users/meeting/invitation 20. 【会议】发送邀请
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1388,7 +1434,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/email/send 20. 发送邮箱验证码
|
||||
* @api {get} api/users/email/send 21. 发送邮箱验证码
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1428,7 +1474,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/email/edit 21. 修改邮箱
|
||||
* @api {get} api/users/email/edit 22. 修改邮箱
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1473,7 +1519,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/delete/account 22. 删除帐号
|
||||
* @api {get} api/users/delete/account 23. 删除帐号
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1535,7 +1581,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/department/list 23. 部门列表(限管理员)
|
||||
* @api {get} api/users/department/list 24. 部门列表(限管理员)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1554,7 +1600,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/department/add 24. 新建、修改部门(限管理员)
|
||||
* @api {get} api/users/department/add 25. 新建、修改部门(限管理员)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1612,19 +1658,22 @@ class UsersController extends AbstractController
|
||||
if (empty($parentDepartment)) {
|
||||
return Base::retError('上级部门不存在或已被删除');
|
||||
}
|
||||
if ($parentDepartment->parent_id > 0) {
|
||||
return Base::retError('上级部门层级错误');
|
||||
if (count($parentDepartment->parents()) > 2) {
|
||||
return Base::retError('部门层级最多只能创建3级');
|
||||
}
|
||||
if (UserDepartment::whereParentId($parent_id)->count() > 20) {
|
||||
if ($id > 0 && UserDepartment::whereParentId($id)->whereId($parent_id)->exists()) {
|
||||
return Base::retError('不能选择自己的子部门作为上级部门');
|
||||
}
|
||||
if (UserDepartment::whereParentId($parent_id)->count() >= 20) {
|
||||
return Base::retError('每个部门最多只能创建20个子部门');
|
||||
}
|
||||
if ($id > 0 && UserDepartment::whereParentId($id)->exists()) {
|
||||
return Base::retError('含有子部门无法修改上级部门');
|
||||
}
|
||||
}
|
||||
if (empty($owner_userid) || !User::whereUserid($owner_userid)->exists()) {
|
||||
return Base::retError('请选择正确的部门负责人');
|
||||
}
|
||||
if (UserDepartment::whereOwnerUserid($owner_userid)->count() >= 10) {
|
||||
return Base::retError('每个用户最多只能负责10个部门');
|
||||
}
|
||||
//
|
||||
$userDepartment->saveDepartment([
|
||||
'name' => $name,
|
||||
@@ -1633,11 +1682,11 @@ class UsersController extends AbstractController
|
||||
], $dialog_useid);
|
||||
Cache::forever("UserDepartment::rand", Base::generatePassword());
|
||||
//
|
||||
return Base::retSuccess($parent_id > 0 ? '保存成功' : '新建成功');
|
||||
return Base::retSuccess($id > 0 ? '保存成功' : '新建成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/department/del 25. 删除部门(限管理员)
|
||||
* @api {get} api/users/department/del 26. 删除部门(限管理员)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1660,6 +1709,9 @@ class UsersController extends AbstractController
|
||||
if (empty($userDepartment)) {
|
||||
return Base::retError('部门不存在或已被删除');
|
||||
}
|
||||
if (UserDepartment::whereParentId($id)->exists()) {
|
||||
return Base::retError('含有子部门无法删除');
|
||||
}
|
||||
$userDepartment->deleteDepartment();
|
||||
Cache::forever("UserDepartment::rand", Base::generatePassword());
|
||||
//
|
||||
@@ -1667,7 +1719,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/checkin/get 26. 获取签到设置
|
||||
* @api {get} api/users/checkin/get 27. 获取签到设置
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1694,7 +1746,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/users/checkin/save 27. 保存签到设置
|
||||
* @api {post} api/users/checkin/save 28. 保存签到设置
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1769,7 +1821,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/checkin/list 28. 获取签到数据
|
||||
* @api {get} api/users/checkin/list 29. 获取签到数据
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1816,7 +1868,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/socket/status 29. 获取socket状态
|
||||
* @api {get} api/users/socket/status 30. 获取socket状态
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1839,7 +1891,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/key/client 30. 客户端KEY
|
||||
* @api {get} api/users/key/client 31. 客户端KEY
|
||||
*
|
||||
* @apiDescription 获取客户端KEY,用于加密数据发送给服务端
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1881,7 +1933,51 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/bot/info 31. 机器人信息
|
||||
* @api {get} api/users/bot/list 32. 机器人列表
|
||||
*
|
||||
* @apiDescription 需要token身份,获取我的机器人列表
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName bot__list
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function bot__list()
|
||||
{
|
||||
// 获取当前认证用户
|
||||
$user = User::auth();
|
||||
|
||||
// 使用连表查询一次性获取所有机器人数据
|
||||
$bots = User::join('user_bots', 'user_bots.bot_id', '=', 'users.userid')
|
||||
->where('user_bots.userid', $user->userid)
|
||||
->select([
|
||||
'users.userid',
|
||||
'users.nickname',
|
||||
'users.userimg',
|
||||
'user_bots.clear_day',
|
||||
'user_bots.webhook_url'
|
||||
])
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
->toArray();
|
||||
foreach ($bots as &$bot) {
|
||||
$bot['id'] = $bot['userid'];
|
||||
$bot['name'] = $bot['nickname'];
|
||||
$bot['avatar'] = $bot['userimg'];
|
||||
$bot['system_name'] = UserBot::systemBotName($bot['name']);
|
||||
unset($bot['userid'], $bot['nickname'], $bot['userimg']);
|
||||
}
|
||||
|
||||
// 返回成功响应,将机器人列表包装在list字段中
|
||||
return Base::retSuccess('success', [
|
||||
'list' => $bots
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/bot/info 33. 机器人信息
|
||||
*
|
||||
* @apiDescription 需要token身份,获取我的机器人信息
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1932,14 +2028,14 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/users/bot/edit 32. 编辑机器人
|
||||
* @api {post} api/users/bot/edit 34. 添加、编辑机器人
|
||||
*
|
||||
* @apiDescription 需要token身份,编辑 我的机器人 或 管理员修改系统机器人 信息
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName bot__edit
|
||||
*
|
||||
* @apiParam {Number} id 机器人ID
|
||||
* @apiParam {Number} [id] 机器人ID(编辑时必填,留空为添加)
|
||||
* @apiParam {String} [name] 机器人名称
|
||||
* @apiParam {String} [avatar] 机器人头像
|
||||
* @apiParam {Number} [clear_day] 清理天数(仅 我的机器人)
|
||||
@@ -1954,10 +2050,19 @@ class UsersController extends AbstractController
|
||||
$user = User::auth();
|
||||
//
|
||||
$botId = intval(Request::input('id'));
|
||||
$botUser = User::whereUserid($botId)->whereBot(1)->first();
|
||||
if (empty($botUser)) {
|
||||
return Base::retError('机器人不存在');
|
||||
if (empty($botId)) {
|
||||
$res = UserBot::newbot($user->userid, trim(Request::input('name')));
|
||||
if (Base::isError($res)) {
|
||||
return $res;
|
||||
}
|
||||
$botUser = $res['data'];
|
||||
} else {
|
||||
$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)) {
|
||||
if (UserBot::systemBotName($botUser->email)) {
|
||||
@@ -2014,11 +2119,61 @@ class UsersController extends AbstractController
|
||||
$data['clear_day'] = $userBot->clear_day;
|
||||
$data['webhook_url'] = $userBot->webhook_url;
|
||||
}
|
||||
return Base::retSuccess('修改成功', $data);
|
||||
return Base::retSuccess($botId ? '修改成功' : '添加成功', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/share/list 33. 获取分享列表
|
||||
* @api {get} api/users/bot/delete 35. 删除机器人
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName bot__delete
|
||||
*
|
||||
* @apiParam {Number} id 机器人ID
|
||||
* @apiParam {String} remark 删除备注
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function bot__delete()
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$botId = intval(Request::input('id'));
|
||||
$remark = trim(Request::input('remark'));
|
||||
//
|
||||
if (empty($remark)) {
|
||||
return Base::retError('请输入删除备注');
|
||||
}
|
||||
if (mb_strlen($remark) > 255) {
|
||||
return Base::retError('删除备注长度限制255个字');
|
||||
}
|
||||
//
|
||||
$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)) {
|
||||
if (UserBot::systemBotName($botUser->email)) {
|
||||
// 系统机器人(仅限管理员)
|
||||
return Base::retError('系统机器人不能删除');
|
||||
} else {
|
||||
// 其他用户的机器人(仅限主人)
|
||||
return Base::retError('不是你的机器人');
|
||||
}
|
||||
}
|
||||
//
|
||||
if (!$botUser->deleteUser($remark)) {
|
||||
return Base::retError('删除失败');
|
||||
}
|
||||
return Base::retSuccess('删除成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/share/list 36. 获取分享列表
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
@@ -2103,7 +2258,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/annual/report 34. 年度报告
|
||||
* @api {get} api/users/annual/report 37. 年度报告
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
|
||||
@@ -23,6 +23,7 @@ use App\Tasks\AutoArchivedTask;
|
||||
use App\Tasks\DeleteBotMsgTask;
|
||||
use App\Tasks\CheckinRemindTask;
|
||||
use App\Tasks\CloseMeetingRoomTask;
|
||||
use App\Tasks\ElasticSearchSyncTask;
|
||||
use App\Tasks\UnclaimedTaskRemindTask;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
use Laravolt\Avatar\Avatar;
|
||||
@@ -258,6 +259,8 @@ class IndexController extends InvokeController
|
||||
Task::deliver(new UnclaimedTaskRemindTask());
|
||||
// 关闭会议室
|
||||
Task::deliver(new CloseMeetingRoomTask());
|
||||
// ElasticSearch 同步
|
||||
Task::deliver(new ElasticSearchSyncTask());
|
||||
|
||||
return "success";
|
||||
}
|
||||
@@ -321,8 +324,7 @@ class IndexController extends InvokeController
|
||||
"file" => Request::file('file'),
|
||||
"type" => 'publish',
|
||||
"path" => $draftPath,
|
||||
"fileName" => true,
|
||||
"quality" => 100
|
||||
"saveName" => true,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -491,7 +493,7 @@ class IndexController extends InvokeController
|
||||
if (in_array($ext, File::localExt)) {
|
||||
$url = Base::fillUrl($path);
|
||||
} else {
|
||||
$url = 'http://' . env('APP_IPPR') . '.3/' . $path;
|
||||
$url = 'http://nginx/' . $path;
|
||||
}
|
||||
$url = Base::urlAddparameter($url, [
|
||||
'fullfilename' => Base::rightDelete($name, '.' . $ext) . '_' . filemtime($file) . '.' . $ext
|
||||
|
||||
@@ -24,6 +24,9 @@ class WebApi
|
||||
RequestContext::set('start_time', microtime(true));
|
||||
RequestContext::set('header_language', $request->header('language'));
|
||||
|
||||
// 更新请求的基本URL
|
||||
RequestContext::updateBaseUrl($request);
|
||||
|
||||
// 加载Doo类
|
||||
Doo::load();
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ class LdapUser extends Model
|
||||
$path = "uploads/user/ldap/";
|
||||
$file = "{$path}{$user->userid}.jpeg";
|
||||
Base::makeDir(public_path($path));
|
||||
if (Base::saveContentImage(public_path($file), $userimg, 90)) {
|
||||
if (Base::saveContentImage(public_path($file), $userimg)) {
|
||||
$user->userimg = $file;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,8 +210,8 @@ class AbstractModel extends Model
|
||||
/**
|
||||
* 数据库更新或插入
|
||||
* @param $where
|
||||
* @param array $update 存在时更新的内容
|
||||
* @param array $insert 不存在时插入的内容,如果没有则插入更新内容
|
||||
* @param array|\Closure $update 存在时更新的内容
|
||||
* @param array|\Closure $insert 不存在时插入的内容,如果没有则插入更新内容
|
||||
* @param bool $isInsert 是否是插入数据
|
||||
* @return AbstractModel|\Illuminate\Database\Eloquent\Builder|Model|object|static|null
|
||||
*/
|
||||
@@ -220,6 +220,12 @@ class AbstractModel extends Model
|
||||
$row = static::where($where)->first();
|
||||
if (empty($row)) {
|
||||
$row = new static;
|
||||
if ($update instanceof \Closure) {
|
||||
$update = $update();
|
||||
}
|
||||
if ($insert instanceof \Closure) {
|
||||
$insert = $insert();
|
||||
}
|
||||
$array = array_merge($where, $insert ?: $update);
|
||||
if (isset($array[$row->primaryKey])) {
|
||||
unset($array[$row->primaryKey]);
|
||||
@@ -227,6 +233,9 @@ class AbstractModel extends Model
|
||||
$row->updateInstance($array);
|
||||
$isInsert = true;
|
||||
} elseif ($update) {
|
||||
if ($update instanceof \Closure) {
|
||||
$update = $update();
|
||||
}
|
||||
$row->updateInstance($update);
|
||||
$isInsert = false;
|
||||
}
|
||||
|
||||
99
app/Models/ApproveProcInstHistory.php
Normal file
99
app/Models/ApproveProcInstHistory.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
use DB;
|
||||
|
||||
/**
|
||||
* App\Models\ApproveProcInstHistory
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $proc_def_id 流程定义ID
|
||||
* @property string|null $proc_def_name 流程定义名
|
||||
* @property string|null $title 标题
|
||||
* @property int|null $department_id 用户部门ID
|
||||
* @property string|null $department 用户部门
|
||||
* @property string|null $company 用户公司
|
||||
* @property string|null $node_id 当前节点
|
||||
* @property string|null $candidate 审批人
|
||||
* @property int|null $task_id 当前任务
|
||||
* @property string|null $start_time 开始时间
|
||||
* @property string|null $end_time 结束时间
|
||||
* @property int|null $duration 持续时间
|
||||
* @property string|null $start_user_id 开始用户ID
|
||||
* @property string|null $start_user_name 开始用户名
|
||||
* @property int|null $is_finished 是否完成
|
||||
* @property string|null $var
|
||||
* @property int $state 当前状态: 0待审批,1审批中,2通过,3拒绝,4撤回
|
||||
* @property string|null $latest_comment
|
||||
* @property string|null $global_comment
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereCandidate($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereCompany($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereDepartment($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereDepartmentId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereDuration($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereEndTime($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereGlobalComment($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereIsFinished($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereLatestComment($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereNodeId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereProcDefId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereProcDefName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereStartTime($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereStartUserId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereStartUserName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereState($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereTaskId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereTitle($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ApproveProcInstHistory whereVar($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ApproveProcInstHistory extends AbstractModel
|
||||
{
|
||||
protected $table = 'approve_proc_inst_history';
|
||||
|
||||
/**
|
||||
* 获取用户审批状态(请假、外出)
|
||||
* @param $userid
|
||||
* @return mixed|null
|
||||
*/
|
||||
public static function getUserApprovalStatus($userid)
|
||||
{
|
||||
if (empty($userid)) {
|
||||
return null;
|
||||
}
|
||||
return Cache::remember('user_is_leave_' . $userid, Carbon::now()->addMinute(), function () use ($userid) {
|
||||
return self::where([
|
||||
['start_user_id', '=', $userid],
|
||||
[DB::raw("JSON_UNQUOTE(JSON_EXTRACT(var, '$.startTime'))"), '<=', Carbon::now()->toDateTimeString()],
|
||||
[DB::raw("JSON_UNQUOTE(JSON_EXTRACT(var, '$.endTime'))"), '>=', Carbon::now()->toDateTimeString()],
|
||||
['state', '=', 2]
|
||||
])->where(function ($query) {
|
||||
$query->where('proc_def_name', 'like', '%请假%')
|
||||
->orWhere('proc_def_name', 'like', '%外出%');
|
||||
})->orderByDesc('id')->value('proc_def_name');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断用户是否请假(包含:请假、外出)
|
||||
* @param $userid
|
||||
* @return bool
|
||||
*/
|
||||
public static function userIsLeave($userid)
|
||||
{
|
||||
return (bool)self::getUserApprovalStatus($userid);
|
||||
}
|
||||
}
|
||||
@@ -79,9 +79,28 @@ class File extends AbstractModel
|
||||
* office文件
|
||||
*/
|
||||
const officeExt = [
|
||||
'doc', 'docx',
|
||||
'xls', 'xlsx',
|
||||
'ppt', 'pptx',
|
||||
// 文本文件
|
||||
'doc', 'docx', // Microsoft Word 文档
|
||||
'dot', 'dotx', // Word 模板
|
||||
'odt', // OpenDocument 文本格式
|
||||
'ott', // OpenDocument 文本模板
|
||||
'rtf', // 富文本格式
|
||||
|
||||
// 电子表格
|
||||
'xls', 'xlsx', // Microsoft Excel 电子表格
|
||||
'xlsm', // Excel 含宏的工作簿
|
||||
'xlt', 'xltx', // Excel 模板
|
||||
'ods', // OpenDocument 电子表格格式
|
||||
'ots', // OpenDocument 电子表格模板
|
||||
'csv', // 逗号分隔值
|
||||
'tsv', // 制表符分隔值
|
||||
|
||||
// 演示文稿
|
||||
'ppt', 'pptx', // Microsoft PowerPoint 演示文稿
|
||||
'pps', 'ppsx', // PowerPoint 幻灯片放映
|
||||
'pot', 'potx', // PowerPoint 模板
|
||||
'odp', // OpenDocument 演示文稿格式
|
||||
'otp', // OpenDocument 演示文稿模板
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -111,6 +130,8 @@ class File extends AbstractModel
|
||||
* 获取文件列表
|
||||
* @param user $user
|
||||
* @param int $pid
|
||||
* @param string $type
|
||||
* @param bool $isGetparent
|
||||
* @return array
|
||||
*/
|
||||
public function getFileList($user, int $pid, $type = "all", $isGetparent = true)
|
||||
@@ -118,7 +139,7 @@ class File extends AbstractModel
|
||||
$permission = 1000;
|
||||
$userids = $user->isTemp() ? [$user->userid] : [0, $user->userid];
|
||||
$builder = File::wherePid($pid)
|
||||
->when($type=='dir',function($q){
|
||||
->when($type == 'dir', function ($q) {
|
||||
$q->whereType('folder');
|
||||
});
|
||||
if ($pid > 0) {
|
||||
@@ -134,7 +155,7 @@ class File extends AbstractModel
|
||||
//
|
||||
if ($pid > 0) {
|
||||
// 遍历获取父级
|
||||
if($isGetparent){
|
||||
if ($isGetparent) {
|
||||
while ($pid > 0) {
|
||||
$file = File::whereId($pid)->first();
|
||||
if (empty($file)) {
|
||||
@@ -172,8 +193,8 @@ class File extends AbstractModel
|
||||
->whereIn('file_users.userid', $userids)
|
||||
->groupBy('files.id')
|
||||
->take(100)
|
||||
->when($type=='dir',function($q){
|
||||
$q->where('files.type','folder');
|
||||
->when($type == 'dir', function ($q) {
|
||||
$q->where('files.type', 'folder');
|
||||
})
|
||||
->get();
|
||||
if ($list->isNotEmpty()) {
|
||||
@@ -251,7 +272,7 @@ class File extends AbstractModel
|
||||
"type" => 'more',
|
||||
"autoThumb" => false,
|
||||
"path" => $path,
|
||||
"quality" => 100
|
||||
"quality" => true
|
||||
]);
|
||||
if (Base::isError($data)) {
|
||||
throw new ApiException($data['msg']);
|
||||
@@ -262,9 +283,9 @@ class File extends AbstractModel
|
||||
'text', 'md', 'markdown' => 'document',
|
||||
'drawio' => 'drawio',
|
||||
'mind' => 'mind',
|
||||
'doc', 'docx' => "word",
|
||||
'xls', 'xlsx' => "excel",
|
||||
'ppt', 'pptx' => "ppt",
|
||||
'doc', 'docx', 'dot', 'dotx', 'odt', 'ott', 'rtf' => "word",
|
||||
'xls', 'xlsx', 'xlsm', 'xlt', 'xltx', 'ods', 'ots', 'csv', 'tsv' => "excel",
|
||||
'ppt', 'pptx', 'pps', 'ppsx', 'pot', 'potx', 'odp', 'otp' => "ppt",
|
||||
'wps' => "wps",
|
||||
'jpg', 'jpeg', 'webp', 'png', 'gif', 'bmp', 'ico', 'raw', 'svg' => "picture",
|
||||
'rar', 'zip', 'jar', '7-zip', 'tar', 'gzip', '7z', 'gz', 'apk', 'dmg' => "archive",
|
||||
@@ -351,7 +372,8 @@ class File extends AbstractModel
|
||||
*/
|
||||
public function getPermission(array $userids)
|
||||
{
|
||||
if (in_array($this->userid, $userids) || in_array($this->created_id, $userids)) {
|
||||
$validUserIds = array_filter($userids);
|
||||
if (in_array($this->userid, $validUserIds) || in_array($this->created_id, $validUserIds)) {
|
||||
// ① 自己的文件夹 或 自己创建的文件夹
|
||||
return 1000;
|
||||
}
|
||||
@@ -703,9 +725,9 @@ class File extends AbstractModel
|
||||
* @param int $permission
|
||||
* @return File
|
||||
*/
|
||||
public static function permissionFind(int $id, $user, int $limit = 0, int &$permission = -1)
|
||||
public static function permissionFind($id, $user, int $limit = 0, int &$permission = -1)
|
||||
{
|
||||
$file = File::find($id);
|
||||
$file = File::find(intval($id));
|
||||
if (empty($file)) {
|
||||
throw new ApiException('文件不存在或已被删除');
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
|
||||
use App\Module\Base;
|
||||
use App\Module\Timer;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
/**
|
||||
* App\Models\FileContent
|
||||
@@ -104,10 +104,10 @@ class FileContent extends AbstractModel
|
||||
|
||||
/**
|
||||
* 获取格式内容(或下载)
|
||||
* @param File $file
|
||||
* @param $file
|
||||
* @param $content
|
||||
* @param $download
|
||||
* @return array|\Symfony\Component\HttpFoundation\BinaryFileResponse
|
||||
* @return array|StreamedResponse
|
||||
*/
|
||||
public static function formatContent($file, $content, $download = false)
|
||||
{
|
||||
@@ -119,7 +119,7 @@ class FileContent extends AbstractModel
|
||||
} else {
|
||||
$filePath = public_path($content['url']);
|
||||
}
|
||||
return Base::BinaryFileResponse($filePath, $name);
|
||||
return Base::DownloadFileResponse($filePath, $name);
|
||||
}
|
||||
if (empty($content)) {
|
||||
$content = match ($file->type) {
|
||||
@@ -148,7 +148,7 @@ class FileContent extends AbstractModel
|
||||
if ($download) {
|
||||
$filePath = public_path($path);
|
||||
if (isset($filePath)) {
|
||||
return Base::BinaryFileResponse($filePath, $name);
|
||||
return Base::DownloadFileResponse($filePath, $name);
|
||||
} else {
|
||||
abort(403, "This file not support download.");
|
||||
}
|
||||
@@ -156,4 +156,28 @@ class FileContent extends AbstractModel
|
||||
}
|
||||
return Base::retSuccess('success', [ 'content' => $content ]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件内容
|
||||
* @param $id
|
||||
* @return self|null
|
||||
*/
|
||||
public static function idOrCodeToContent($id)
|
||||
{
|
||||
$builder = null;
|
||||
if (Base::isNumber($id)) {
|
||||
$builder = FileContent::whereFid($id);
|
||||
} elseif ($id) {
|
||||
$fileLink = FileLink::whereCode($id)->first();
|
||||
if ($fileLink) {
|
||||
$builder = FileContent::whereFid($fileLink->file_id);
|
||||
}
|
||||
}
|
||||
/** @var self $fileContent */
|
||||
$fileContent = $builder?->orderByDesc('id')->first();
|
||||
if ($fileContent) {
|
||||
$fileContent->content = Base::json2array($fileContent->content ?: []);
|
||||
}
|
||||
return $fileContent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ use Request;
|
||||
* @property string|null $desc 描述、备注
|
||||
* @property int|null $userid 创建人
|
||||
* @property int|null $personal 是否个人项目
|
||||
* @property string|null $archive_method 自动归档方式
|
||||
* @property int|null $archive_days 自动归档天数
|
||||
* @property string|null $user_simple 成员总数|1,2,3
|
||||
* @property int|null $dialog_id 聊天会话ID
|
||||
* @property \Illuminate\Support\Carbon|null $archived_at 归档时间
|
||||
@@ -46,6 +48,8 @@ use Request;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project whereArchiveDays($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project whereArchiveMethod($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project whereArchivedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project whereArchivedUserid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Project whereCreatedAt($value)
|
||||
@@ -215,7 +219,12 @@ class Project extends AbstractModel
|
||||
'userid' => $userid,
|
||||
], [
|
||||
'important' => 1
|
||||
]);
|
||||
], function () use ($userid) {
|
||||
return [
|
||||
'important' => 1,
|
||||
'bot' => User::isBot($userid) ? 1 : 0,
|
||||
];
|
||||
});
|
||||
}
|
||||
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->remove();
|
||||
});
|
||||
@@ -411,6 +420,7 @@ class Project extends AbstractModel
|
||||
$hasStart = false;
|
||||
$hasEnd = false;
|
||||
$upTaskList = [];
|
||||
$projectUserids = $this->relationUserids();
|
||||
foreach ($flows as $item) {
|
||||
$id = intval($item['id']);
|
||||
$turns = Base::arrayRetainInt($item['turns'] ?: [], true);
|
||||
@@ -427,6 +437,12 @@ class Project extends AbstractModel
|
||||
if ($userlimit && empty($userids)) {
|
||||
throw new ApiException("状态[{$item['name']}]设置错误,设置限制负责人时必须填写状态负责人");
|
||||
}
|
||||
foreach ($userids as $userid) {
|
||||
if (!in_array($userid, $projectUserids)) {
|
||||
$nickname = User::userid2nickname($userid);
|
||||
throw new ApiException("状态[{$item['name']}]设置错误,状态负责人[{$nickname}]不在项目成员内");
|
||||
}
|
||||
}
|
||||
$flow = ProjectFlowItem::updateInsert([
|
||||
'id' => $id,
|
||||
'project_id' => $this->id,
|
||||
|
||||
65
app/Models/ProjectTag.php
Normal file
65
app/Models/ProjectTag.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/**
|
||||
* App\Models\ProjectTag
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $project_id 项目ID
|
||||
* @property string $name 标签名称
|
||||
* @property string|null $desc 标签描述
|
||||
* @property string|null $color 颜色
|
||||
* @property int $userid 创建人
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\Project $project
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereColor($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereDesc($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereProjectId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTag whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ProjectTag extends AbstractModel
|
||||
{
|
||||
protected $hidden = [
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'project_id',
|
||||
'name',
|
||||
'desc',
|
||||
'color',
|
||||
'userid'
|
||||
];
|
||||
|
||||
/**
|
||||
* 关联项目
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function project()
|
||||
{
|
||||
return $this->belongsTo(Project::class);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ use App\Tasks\PushTask;
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Observers\ProjectTaskObserver;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
use League\HTMLToMarkdown\HtmlConverter;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
@@ -617,7 +618,12 @@ class ProjectTask extends AbstractModel
|
||||
$data['complete_at'] = false;
|
||||
}
|
||||
}
|
||||
if ($newFlowItem->userids) {
|
||||
$flowUserids = $newFlowItem->userids;
|
||||
if ($flowUserids) {
|
||||
// 确认负责人在任务中
|
||||
$flowUserids = ProjectUser::whereProjectId($this->project_id)->whereIn('userid', $flowUserids)->pluck('userid')->toArray();
|
||||
}
|
||||
if ($flowUserids) {
|
||||
// 判断自动添加负责人
|
||||
$flowData['owner'] = $data['owner'] = $this->taskUser->where('owner', 1)->pluck('userid')->toArray();
|
||||
if (in_array($newFlowItem->usertype, ["replace", "merge"])) {
|
||||
@@ -626,14 +632,14 @@ class ProjectTask extends AbstractModel
|
||||
$flowData['assist'] = $data['assist'] = $this->taskUser->where('owner', 0)->pluck('userid')->toArray();
|
||||
$data['assist'] = array_merge($data['assist'], $data['owner']);
|
||||
}
|
||||
$data['owner'] = $newFlowItem->userids;
|
||||
$data['owner'] = $flowUserids;
|
||||
// 判断剔除模式:保留操作状态的人员
|
||||
if ($newFlowItem->usertype == "merge") {
|
||||
$data['owner'][] = User::userid();
|
||||
}
|
||||
} else {
|
||||
// 添加模式
|
||||
$data['owner'] = array_merge($data['owner'], $newFlowItem->userids);
|
||||
$data['owner'] = array_merge($data['owner'], $flowUserids);
|
||||
}
|
||||
$data['owner'] = array_values(array_unique($data['owner']));
|
||||
if (isset($data['assist'])) {
|
||||
@@ -770,6 +776,7 @@ class ProjectTask extends AbstractModel
|
||||
if (Arr::exists($data, 'times')) {
|
||||
$oldAt = [Carbon::parse($this->start_at), Carbon::parse($this->end_at)];
|
||||
$oldStringAt = $this->start_at ? ($oldAt[0]->toDateTimeString() . '~' . $oldAt[1]->toDateTimeString()) : '';
|
||||
$isOverdue = $this->overdue;
|
||||
$clearSubTaskTime = false;
|
||||
$this->start_at = null;
|
||||
$this->end_at = null;
|
||||
@@ -835,7 +842,19 @@ class ProjectTask extends AbstractModel
|
||||
}
|
||||
});
|
||||
}
|
||||
$newStringAt = $this->start_at && !$clearSubTaskTime ? ($this->start_at->toDateTimeString() . '~' . $this->end_at->toDateTimeString()) : '';
|
||||
$existAt = $this->start_at && !$clearSubTaskTime;
|
||||
$newStringAt = $existAt ? ($this->start_at->toDateTimeString() . '~' . $this->end_at->toDateTimeString()) : '';
|
||||
if ($isOverdue) {
|
||||
$this->addLog("{任务}超期未完成", [
|
||||
'cache' => [
|
||||
'task_at' => $oldStringAt,
|
||||
'change_at' => $newStringAt,
|
||||
'over_sec' => ($existAt ? $this->end_at : Carbon::now())->diffInSeconds($oldAt[1]),
|
||||
'owners' => $this->taskUser->where('owner', 1)->pluck('userid')->toArray(),
|
||||
'assists' => $this->taskUser->where('owner', 0)->pluck('userid')->toArray(),
|
||||
]
|
||||
]);
|
||||
}
|
||||
$newDesc = $desc ? "(备注:{$desc})" : "";
|
||||
$this->addLog("修改{任务}时间" . $newDesc, [
|
||||
'change' => [$oldStringAt, $newStringAt]
|
||||
@@ -944,6 +963,60 @@ class ProjectTask extends AbstractModel
|
||||
$this->addLog("修改{任务}详细描述", $logRecord);
|
||||
$updateMarking['is_update_content'] = true;
|
||||
}
|
||||
// 标签
|
||||
if (Arr::exists($data, 'task_tag')) {
|
||||
$oldTags = collect($this->taskTag);
|
||||
$newTags = collect($data['task_tag']);
|
||||
|
||||
// 找出需要删除的标签(在旧数据中存在,但在新数据中不存在)
|
||||
$deletedTags = $oldTags->filter(function ($oldTag) use ($newTags) {
|
||||
return !$newTags->contains('name', $oldTag['name']);
|
||||
});
|
||||
if ($deletedTags->isNotEmpty()) {
|
||||
$this->addLog("删除{任务}标签", [
|
||||
'tags' => $deletedTags->values()->all()
|
||||
]);
|
||||
ProjectTaskTag::whereProjectId($this->project_id)
|
||||
->whereTaskId($this->id)
|
||||
->whereIn('name', $deletedTags->pluck('name'))
|
||||
->delete();
|
||||
}
|
||||
|
||||
// 找出需要新增的标签(在新数据中存在,但在旧数据中不存在)
|
||||
$addedTags = $newTags->filter(function ($newTag) use ($oldTags) {
|
||||
return !$oldTags->contains('name', $newTag['name']);
|
||||
});
|
||||
if ($addedTags->isNotEmpty()) {
|
||||
$this->addLog("新增{任务}标签", [
|
||||
'tags' => $addedTags->values()->all()
|
||||
]);
|
||||
$addedTags->each(function ($tag) {
|
||||
ProjectTaskTag::createInstance([
|
||||
'project_id' => $this->project_id,
|
||||
'task_id' => $this->id,
|
||||
'name' => $tag['name'],
|
||||
'color' => $tag['color'],
|
||||
])->save();
|
||||
});
|
||||
}
|
||||
|
||||
// 找出需要更新的标签(标签名相同,但其他属性可能变化)
|
||||
$updatedTags = $newTags->filter(function ($newTag) use ($oldTags) {
|
||||
$oldTag = $oldTags->firstWhere('name', $newTag['name']);
|
||||
return $oldTag && ($oldTag['color'] !== $newTag['color']);
|
||||
});
|
||||
if ($updatedTags->isNotEmpty()) {
|
||||
$this->addLog("更新{任务}标签", [
|
||||
'tags' => $updatedTags->values()->all()
|
||||
]);
|
||||
$updatedTags->each(function ($tag) {
|
||||
ProjectTaskTag::whereProjectId($this->project_id)
|
||||
->whereTaskId($this->id)
|
||||
->whereName($tag['name'])
|
||||
->update(['color' => $tag['color']]);
|
||||
});
|
||||
}
|
||||
}
|
||||
// 优先级
|
||||
$p = false;
|
||||
$oldPName = $this->p_name;
|
||||
@@ -1126,7 +1199,12 @@ class ProjectTask extends AbstractModel
|
||||
'userid' => $userid,
|
||||
], [
|
||||
'important' => 1
|
||||
]);
|
||||
], function () use ($userid) {
|
||||
return [
|
||||
'important' => 1,
|
||||
'bot' => User::isBot($userid) ? 1 : 0,
|
||||
];
|
||||
});
|
||||
}
|
||||
WebSocketDialogUser::whereDialogId($this->dialog_id)->whereNotIn('userid', $userids)->whereImportant(1)->remove();
|
||||
});
|
||||
@@ -1289,6 +1367,16 @@ class ProjectTask extends AbstractModel
|
||||
if (!$this->hasOwner()) {
|
||||
throw new ApiException('请先领取任务');
|
||||
}
|
||||
if ($this->overdue) {
|
||||
$this->addLog("{任务}超期未完成", [
|
||||
'cache' => [
|
||||
'task_at' => $this->start_at . '~' . $this->end_at,
|
||||
'over_sec' => Carbon::now()->diffInSeconds($this->end_at),
|
||||
'owners' => $this->taskUser->where('owner', 1)->pluck('userid')->toArray(),
|
||||
'assists' => $this->taskUser->where('owner', 0)->pluck('userid')->toArray(),
|
||||
]
|
||||
]);
|
||||
}
|
||||
if (empty($complete_name)) {
|
||||
$complete_name = '已完成';
|
||||
}
|
||||
@@ -1779,6 +1867,11 @@ class ProjectTask extends AbstractModel
|
||||
$taskUser->save();
|
||||
}
|
||||
}
|
||||
// 子任务
|
||||
ProjectTask::whereParentId($this->id)->change([
|
||||
'project_id' => $projectId,
|
||||
'column_id' => $columnId,
|
||||
]);
|
||||
//
|
||||
if ($flowItemId) {
|
||||
$flowItem = projectFlowItem::whereProjectId($projectId)->whereId($flowItemId)->first();
|
||||
@@ -1806,6 +1899,67 @@ class ProjectTask extends AbstractModel
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成AI上下文
|
||||
* @return array
|
||||
*/
|
||||
public function AIContext()
|
||||
{
|
||||
$contexts = [];
|
||||
if ($this->archived_at) {
|
||||
$contexts[] = "任务状态:已归档";
|
||||
$contexts[] = "归档时间:" . $this->archived_at;
|
||||
} elseif ($this->complete_at) {
|
||||
$contexts[] = "任务状态:已完成";
|
||||
$contexts[] = "完成时间:" . $this->complete_at;
|
||||
} elseif ($this->end_at && Carbon::parse($this->end_at)->lt(Carbon::now())) {
|
||||
$contexts[] = "任务状态:已过期";
|
||||
$contexts[] = "任务截止时间:" . $this->end_at;
|
||||
} else {
|
||||
$contexts[] = "任务状态:进行中";
|
||||
if ($this->start_at) {
|
||||
$contexts[] = "任务开始时间:" . $this->start_at;
|
||||
}
|
||||
if ($this->end_at) {
|
||||
$contexts[] = "任务截止时间:" . $this->end_at;
|
||||
}
|
||||
}
|
||||
$contexts[] = "当前系统时间:" . Carbon::now()->toDateTimeString();
|
||||
if ($this->content) {
|
||||
$taskDesc = $this->content?->getContentInfo();
|
||||
if ($taskDesc) {
|
||||
$converter = new HtmlConverter(['strip_tags' => true]);
|
||||
$descContent = Base::cutStr($converter->convert($taskDesc['content']), 2000);
|
||||
$contexts[] = <<<EOF
|
||||
任务描述:
|
||||
```md
|
||||
{$descContent}
|
||||
```
|
||||
EOF;
|
||||
}
|
||||
}
|
||||
$subTask = ProjectTask::select(['id', 'name', 'complete_at', 'end_at'])->whereParentId($this->id)->get();
|
||||
if ($subTask->isNotEmpty()) {
|
||||
$subTaskContent = $subTask->map(function($item) {
|
||||
if ($item->complete_at) {
|
||||
$status = " (已完成)";
|
||||
} elseif ($item->end_at && Carbon::parse($item->end_at)->lt(Carbon::now())) {
|
||||
$status = " (已过期)";
|
||||
} else {
|
||||
$status = " (进行中)";
|
||||
}
|
||||
return " - {$item->name} {$status}";
|
||||
})->join("\n");
|
||||
if ($subTaskContent) {
|
||||
$contexts[] = <<<EOF
|
||||
子任务列表:
|
||||
{$subTaskContent}
|
||||
EOF;
|
||||
}
|
||||
}
|
||||
return $contexts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务
|
||||
* @param $task_id
|
||||
|
||||
@@ -79,7 +79,7 @@ class ProjectTaskContent extends AbstractModel
|
||||
$tmpPath = $path . 'attached/';
|
||||
Base::makeDir(public_path($tmpPath));
|
||||
$tmpPath .= md5($text) . "." . $matchs[1][$key];
|
||||
if (Base::saveContentImage(public_path($tmpPath), base64_decode($text), 90)) {
|
||||
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);
|
||||
}
|
||||
|
||||
77
app/Models/ProjectTaskTemplate.php
Normal file
77
app/Models/ProjectTaskTemplate.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/**
|
||||
* App\Models\ProjectTaskTemplate
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $project_id 项目ID
|
||||
* @property string $name 模板名称
|
||||
* @property string|null $title 任务标题
|
||||
* @property string|null $content 任务内容
|
||||
* @property int $sort 排序
|
||||
* @property int $is_default 是否默认模板
|
||||
* @property int $userid 创建人
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\Project $project
|
||||
* @property-read \App\Models\User $user
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereContent($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereIsDefault($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereProjectId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereSort($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereTitle($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskTemplate whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ProjectTaskTemplate extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'project_id',
|
||||
'name',
|
||||
'title',
|
||||
'content',
|
||||
'sort',
|
||||
'is_default',
|
||||
'userid'
|
||||
];
|
||||
|
||||
/**
|
||||
* 关联项目
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function project()
|
||||
{
|
||||
return $this->belongsTo(Project::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联创建者
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'userid');
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
|
||||
/**
|
||||
* App\Models\ProjectUser
|
||||
*
|
||||
@@ -50,7 +52,9 @@ class ProjectUser extends AbstractModel
|
||||
*/
|
||||
public static function transfer($originalUserid, $newUserid)
|
||||
{
|
||||
self::whereUserid($originalUserid)->chunkById(100, function ($list) use ($originalUserid, $newUserid) {
|
||||
$projectIds = [];
|
||||
// 移交项目身份
|
||||
self::whereUserid($originalUserid)->chunkById(100, function ($list) use ($originalUserid, $newUserid, &$projectIds) {
|
||||
/** @var self $item */
|
||||
foreach ($list as $item) {
|
||||
$row = self::whereProjectId($item->project_id)->whereUserid($newUserid)->first();
|
||||
@@ -72,9 +76,23 @@ class ProjectUser extends AbstractModel
|
||||
}
|
||||
$item->project->addLog("移交项目身份", ['userid' => [$originalUserid, ' => ', $newUserid]]);
|
||||
$item->project->syncDialogUser();
|
||||
$projectIds[] = $item->project_id;
|
||||
}
|
||||
}
|
||||
});
|
||||
// 移交工作流状态负责人
|
||||
if ($projectIds) {
|
||||
ProjectFlowItem::whereIn('project_id', $projectIds)->chunkById(100, function ($list) use ($originalUserid, $newUserid) {
|
||||
/** @var ProjectFlowItem $item */
|
||||
foreach ($list as $item) {
|
||||
if (in_array($originalUserid, $item->userids)) {
|
||||
$userids = array_values(array_diff($item->userids, [$originalUserid]));
|
||||
$item->userids = Base::array2json(array_merge($userids, [$newUserid]));
|
||||
$item->save();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Base;
|
||||
use Carbon\Carbon;
|
||||
use Carbon\Traits\Creator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
@@ -95,6 +96,24 @@ class Report extends AbstractModel
|
||||
return $this->appendattrs['receives'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取汇报内容
|
||||
* @param $id
|
||||
* @return self|null
|
||||
*/
|
||||
public static function idOrCodeToContent($id)
|
||||
{
|
||||
if (Base::isNumber($id)) {
|
||||
return self::find($id);
|
||||
} elseif ($id) {
|
||||
$reportLink = ReportLink::whereCode($id)->first();
|
||||
if ($reportLink) {
|
||||
return self::find($reportLink->rid);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条记录
|
||||
* @param $id
|
||||
@@ -139,12 +158,12 @@ class Report extends AbstractModel
|
||||
// 如果设置了周期偏移量
|
||||
empty( $offset ) || $now_dt->subWeeks( abs( $offset ) );
|
||||
$now_dt->startOfWeek(); // 设置为当周第一天
|
||||
return $now_dt->year . $now_dt->weekOfYear;
|
||||
return now()->year . $now_dt->weekOfYear;
|
||||
},
|
||||
Report::DAILY => function() use ($now_dt, $offset) {
|
||||
// 如果设置了周期偏移量
|
||||
empty( $offset ) || $now_dt->subDays( abs( $offset ) );
|
||||
return $now_dt->format("Ymd");
|
||||
return now()->format("Ymd");
|
||||
},
|
||||
default => "",
|
||||
};
|
||||
|
||||
86
app/Models/ReportLink.php
Normal file
86
app/Models/ReportLink.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Base;
|
||||
|
||||
/**
|
||||
* App\Models\ReportLink
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $rid 报告ID
|
||||
* @property int|null $num 累计访问
|
||||
* @property string|null $code 链接码
|
||||
* @property int|null $userid 会员ID
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\Report|null $report
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereCode($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereNum($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereRid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ReportLink whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ReportLink extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
*/
|
||||
public function report(): \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
{
|
||||
return $this->hasOne(Report::class, 'id', 'report_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成链接
|
||||
* @param $rid
|
||||
* @param $userid
|
||||
* @param $refresh
|
||||
* @return array
|
||||
*/
|
||||
public static function generateLink($rid, $userid, $refresh = false)
|
||||
{
|
||||
$report = Report::find($rid);
|
||||
if (empty($report)) {
|
||||
throw new ApiException('报告不存在或已被删除');
|
||||
}
|
||||
if ($report->userid != $userid) {
|
||||
if (!ReportReceive::whereRid($rid)->whereUserid($userid)->exists()) {
|
||||
throw new ApiException('您没有权限查看该报告');
|
||||
}
|
||||
}
|
||||
$reportLink = ReportLink::whereRid($rid)->whereUserid($userid)->first();
|
||||
if (empty($reportLink)) {
|
||||
$reportLink = ReportLink::createInstance([
|
||||
'rid' => $rid,
|
||||
'userid' => $userid,
|
||||
'code' => base64_encode("{$rid},{$userid}," . Base::generatePassword()),
|
||||
]);
|
||||
$reportLink->save();
|
||||
} else {
|
||||
if ($refresh == 'yes') {
|
||||
$reportLink->code = base64_encode("{$rid},{$userid}," . Base::generatePassword());
|
||||
$reportLink->save();
|
||||
}
|
||||
}
|
||||
return [
|
||||
'id' => $rid,
|
||||
'url' => Base::fillUrl('single/report/detail/' . $reportLink->code),
|
||||
'code' => $reportLink->code,
|
||||
'num' => $reportLink->num
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Module\Timer;
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* App\Models\Setting
|
||||
@@ -11,7 +14,7 @@ use App\Module\Timer;
|
||||
* @property int $id
|
||||
* @property string|null $name
|
||||
* @property string|null $desc 参数描述、备注
|
||||
* @property string|null $setting
|
||||
* @property array $setting
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
@@ -36,7 +39,7 @@ class Setting extends AbstractModel
|
||||
/**
|
||||
* 格式化设置参数
|
||||
* @param $value
|
||||
* @return array|mixed
|
||||
* @return array
|
||||
*/
|
||||
public function getSettingAttribute($value)
|
||||
{
|
||||
@@ -47,18 +50,186 @@ class Setting extends AbstractModel
|
||||
switch ($this->name) {
|
||||
case 'system':
|
||||
$value['system_alias'] = $value['system_alias'] ?: env('APP_NAME');
|
||||
$value['image_compress'] = $value['image_compress'] ?: 'open';
|
||||
$value['image_quality'] = min(100, max(0, intval($value['image_quality']) ?: 90));
|
||||
$value['image_save_local'] = $value['image_save_local'] ?: 'open';
|
||||
if (!is_array($value['task_default_time']) || count($value['task_default_time']) != 2 || !Timer::isTime($value['task_default_time'][0]) || !Timer::isTime($value['task_default_time'][1])) {
|
||||
$value['task_default_time'] = ['09:00', '18:00'];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'fileSetting':
|
||||
$value['permission_pack_type'] = $value['permission_pack_type'] ?: 'all';
|
||||
$value['permission_pack_userids'] = is_array($value['permission_pack_userids']) ? $value['permission_pack_userids'] : [];
|
||||
break;
|
||||
|
||||
case 'aibotSetting':
|
||||
if ($value['claude_token'] && empty($value['claude_key'])) {
|
||||
$value['claude_key'] = $value['claude_token'];
|
||||
}
|
||||
$array = [];
|
||||
$aiList = ['openai', 'claude', 'deepseek', 'gemini', 'grok', 'ollama', 'zhipu', 'qianwen', 'wenxin'];
|
||||
$fieldList = ['key', 'secret', 'models', 'model', 'base_url', 'agency', 'temperature', 'system'];
|
||||
foreach ($aiList as $aiName) {
|
||||
foreach ($fieldList as $fieldName) {
|
||||
$key = $aiName . '_' . $fieldName;
|
||||
$content = $value[$key] ? trim($value[$key]) : '';
|
||||
switch ($fieldName) {
|
||||
case 'models':
|
||||
if ($content) {
|
||||
$content = explode("\n", $content);
|
||||
$content = array_filter($content);
|
||||
}
|
||||
if (empty($content)) {
|
||||
$content = self::AIDefaultModels($aiName);
|
||||
}
|
||||
$content = implode("\n", $content);
|
||||
break;
|
||||
case 'model':
|
||||
$models = Setting::AIModels2Array($array[$key . 's'], true);
|
||||
$content = in_array($content, $models) ? $content : ($models[0] ?? '');
|
||||
break;
|
||||
case 'temperature':
|
||||
if ($content) {
|
||||
$content = floatval(min(1, max(0, floatval($content) ?: 0.7)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
$array[$key] = $content;
|
||||
}
|
||||
}
|
||||
$value = $array;
|
||||
break;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否开启AI
|
||||
* @param $ai
|
||||
* @return bool
|
||||
*/
|
||||
public static function AIOpen($ai = 'openai')
|
||||
{
|
||||
$array = Base::setting('aibotSetting');
|
||||
return !!$array[$ai . '_key'];
|
||||
}
|
||||
|
||||
/**
|
||||
* AI默认模型
|
||||
* @param string $ai
|
||||
* @return array
|
||||
*/
|
||||
public static function AIDefaultModels($ai = 'openai')
|
||||
{
|
||||
return match ($ai) {
|
||||
'openai' => [
|
||||
'gpt-4 | GPT-4',
|
||||
'gpt-4-turbo | GPT-4 Turbo',
|
||||
'gpt-4o | GPT-4o',
|
||||
'gpt-4o-mini | GPT-4o Mini',
|
||||
'o1 | GPT-o1',
|
||||
'o1-mini | GPT-o1 Mini',
|
||||
'o3-mini | GPT-o3 Mini',
|
||||
'gpt-3.5-turbo | GPT-3.5 Turbo',
|
||||
'gpt-3.5-turbo-16k | GPT-3.5 Turbo 16K',
|
||||
'gpt-3.5-turbo-0125 | GPT-3.5 Turbo 0125',
|
||||
'gpt-3.5-turbo-1106 | GPT-3.5 Turbo 1106'
|
||||
],
|
||||
'claude' => [
|
||||
'claude-3-5-sonnet-latest | Claude 3.5 Sonnet',
|
||||
'claude-3-5-sonnet-20241022 | Claude 3.5 Sonnet 20241022',
|
||||
'claude-3-5-haiku-latest | Claude 3.5 Haiku',
|
||||
'claude-3-5-haiku-20241022 | Claude 3.5 Haiku 20241022',
|
||||
'claude-3-opus-latest | Claude 3 Opus',
|
||||
'claude-3-opus-20240229 | Claude 3 Opus 20240229',
|
||||
'claude-3-haiku-20240307 | Claude 3 Haiku 20240307',
|
||||
'claude-2.1 | Claude 2.1',
|
||||
'claude-2.0 | Claude 2.0'
|
||||
],
|
||||
'deepseek' => [
|
||||
'deepseek-chat | DeepSeek V3',
|
||||
'deepseek-reasoner | DeepSeek R1'
|
||||
],
|
||||
'gemini' => [
|
||||
'gemini-2.0-flash | Gemini 2.0 Flash',
|
||||
'gemini-2.0-flash-lite-preview-02-05 | Gemini 2.0 Flash-Lite Preview',
|
||||
'gemini-1.5-flash | Gemini 1.5 Flash',
|
||||
'gemini-1.5-flash-8b | Gemini 1.5 Flash 8B',
|
||||
'gemini-1.5-pro | Gemini 1.5 Pro',
|
||||
'gemini-1.0-pro | Gemini 1.0 Pro'
|
||||
],
|
||||
'grok' => [
|
||||
'grok-2-vision-1212 | Grok 2 Vision 1212',
|
||||
'grok-2-vision | Grok 2 Vision',
|
||||
'grok-2-vision-latest | Grok 2 Vision Latest',
|
||||
'grok-2-1212 | Grok 2 1212',
|
||||
'grok-2 | Grok 2',
|
||||
'grok-2-latest | Grok 2 Latest',
|
||||
'grok-vision-beta | Grok Vision Beta',
|
||||
'grok-beta | Grok Beta',
|
||||
],
|
||||
'zhipu' => [
|
||||
'glm-4 | GLM-4',
|
||||
'glm-4-plus | GLM-4 Plus',
|
||||
'glm-4-air | GLM-4 Air',
|
||||
'glm-4-airx | GLM-4 AirX',
|
||||
'glm-4-long | GLM-4 Long',
|
||||
'glm-4-flash | GLM-4 Flash',
|
||||
'glm-4v | GLM-4V',
|
||||
'glm-4v-plus | GLM-4V Plus',
|
||||
'glm-3-turbo | GLM-3 Turbo'
|
||||
],
|
||||
'qianwen' => [
|
||||
'qwen-max | QWEN Max',
|
||||
'qwen-max-latest | QWEN Max Latest',
|
||||
'qwen-turbo | QWEN Turbo',
|
||||
'qwen-turbo-latest | QWEN Turbo Latest',
|
||||
'qwen-plus | QWEN Plus',
|
||||
'qwen-plus-latest | QWEN Plus Latest',
|
||||
'qwen-long | QWEN Long'
|
||||
],
|
||||
'wenxin' => [
|
||||
'ernie-4.0-8k | Ernie 4.0 8K',
|
||||
'ernie-4.0-8k-latest | Ernie 4.0 8K Latest',
|
||||
'ernie-4.0-turbo-128k | Ernie 4.0 Turbo 128K',
|
||||
'ernie-4.0-turbo-8k | Ernie 4.0 Turbo 8K',
|
||||
'ernie-3.5-128k | Ernie 3.5 128K',
|
||||
'ernie-3.5-8k | Ernie 3.5 8K',
|
||||
'ernie-speed-128k | Ernie Speed 128K',
|
||||
'ernie-speed-8k | Ernie Speed 8K',
|
||||
'ernie-lite-8k | Ernie Lite 8K',
|
||||
'ernie-tiny-8k | Ernie Tiny 8K'
|
||||
],
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* AI模型转数组
|
||||
* @param $models
|
||||
* @param bool $retValue
|
||||
* @return array
|
||||
*/
|
||||
public static function AIModels2Array($models, $retValue = false)
|
||||
{
|
||||
$list = is_array($models) ? $models : explode("\n", $models);
|
||||
$array = [];
|
||||
foreach ($list as $item) {
|
||||
$arr = Base::newTrim(explode('|', $item . '|'));
|
||||
if ($arr[0]) {
|
||||
$array[] = [
|
||||
'value' => $arr[0],
|
||||
'label' => $arr[1] ?: $arr[0]
|
||||
];
|
||||
}
|
||||
}
|
||||
if ($retValue) {
|
||||
return array_column($array, 'value');
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱地址(过滤忽略地址)
|
||||
* @param $array
|
||||
@@ -95,4 +266,36 @@ class Setting extends AbstractModel
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证消息限制
|
||||
* @param $type
|
||||
* @param $msg
|
||||
* @return void
|
||||
*/
|
||||
public static function validateMsgLimit($type, $msg)
|
||||
{
|
||||
$keyName = 'msg_edit_limit';
|
||||
$error = '此消息不可修改';
|
||||
if ($type == 'rev') {
|
||||
$keyName = 'msg_rev_limit';
|
||||
$error = '此消息不可撤回';
|
||||
}
|
||||
$limitNum = intval(Base::settingFind('system', $keyName, 0));
|
||||
if ($limitNum <= 0) {
|
||||
return;
|
||||
}
|
||||
if ($msg instanceof WebSocketDialogMsg) {
|
||||
$dialogMsg = $msg;
|
||||
} else {
|
||||
$dialogMsg = WebSocketDialogMsg::find($msg);
|
||||
}
|
||||
if (!$dialogMsg) {
|
||||
return;
|
||||
}
|
||||
$limitTime = Carbon::parse($dialogMsg->created_at)->addMinutes($limitNum);
|
||||
if ($limitTime->lt(Carbon::now())) {
|
||||
throw new ApiException('已超过' . Doo::translate(Base::forumMinuteDay($limitNum)) . ',' . $error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ use Hedeqiang\UMeng\IOS;
|
||||
* @property string|null $alias 别名
|
||||
* @property string|null $platform 平台类型
|
||||
* @property string|null $device 设备类型
|
||||
* @property string|null $version 应用版本号
|
||||
* @property string|null $ua userAgent
|
||||
* @property int|null $is_notified 通知权限
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
@@ -31,10 +33,12 @@ use Hedeqiang\UMeng\IOS;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereDevice($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereIsNotified($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias wherePlatform($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereUa($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereUserid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|UmengAlias whereVersion($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class UmengAlias extends AbstractModel
|
||||
@@ -189,7 +193,11 @@ class UmengAlias extends AbstractModel
|
||||
$lists = $rows->take(5)->groupBy('platform'); // 每个会员最多推送5个别名
|
||||
foreach ($lists as $platform => $list) {
|
||||
$alias = $list->pluck('alias')->implode(',');
|
||||
self::pushMsgToAlias($alias, $platform, $array);
|
||||
try {
|
||||
self::pushMsgToAlias($alias, $platform, $array);
|
||||
} catch (\Exception $e) {
|
||||
info("[PushMsg] fail: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -222,10 +222,14 @@ class User extends AbstractModel
|
||||
|
||||
/**
|
||||
* 返回是否禁用帐号(离职)
|
||||
* @param bool $incAt 是否包含禁用时间
|
||||
* @return bool
|
||||
*/
|
||||
public function isDisable()
|
||||
public function isDisable($incAt = false)
|
||||
{
|
||||
if ($incAt) {
|
||||
return in_array('disable', $this->identity) || $this->disable_at;
|
||||
}
|
||||
return in_array('disable', $this->identity);
|
||||
}
|
||||
|
||||
@@ -238,6 +242,31 @@ class User extends AbstractModel
|
||||
return in_array('admin', $this->identity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回是否AI机器人
|
||||
* @return bool
|
||||
*/
|
||||
public function isAiBot(&$aiName = '')
|
||||
{
|
||||
if (preg_match('/^ai-(.*?)@bot\.system$/', $this->email, $matches)) {
|
||||
$aiName = $matches[1];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回是否用户机器人
|
||||
* @return bool
|
||||
*/
|
||||
public function isUserBot()
|
||||
{
|
||||
if (preg_match('/^user-(.*?)@bot\.system$/', $this->email)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否管理员
|
||||
*/
|
||||
@@ -589,8 +618,14 @@ class User extends AbstractModel
|
||||
return url("images/avatar/default_openai.png");
|
||||
case 'ai-claude@bot.system':
|
||||
return url("images/avatar/default_claude.png");
|
||||
case 'ai-deepseek@bot.system':
|
||||
return url("images/avatar/default_deepseek.png");
|
||||
case 'ai-gemini@bot.system':
|
||||
return url("images/avatar/default_gemini.png");
|
||||
case 'ai-grok@bot.system':
|
||||
return url("images/avatar/default_grok.png");
|
||||
case 'ai-ollama@bot.system':
|
||||
return url("images/avatar/default_ollama.png");
|
||||
case 'ai-zhipu@bot.system':
|
||||
return url("images/avatar/default_zhipu.png");
|
||||
case 'bot-manager@bot.system':
|
||||
@@ -679,4 +714,21 @@ class User extends AbstractModel
|
||||
}
|
||||
return $botUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否机器人
|
||||
* @param $userid
|
||||
* @return bool|mixed
|
||||
*/
|
||||
public static function isBot($userid)
|
||||
{
|
||||
if (empty($userid)) {
|
||||
return false;
|
||||
}
|
||||
$userid = intval($userid);
|
||||
if (RequestContext::has("isBot_" . $userid)) {
|
||||
return RequestContext::get("isBot_" . $userid);
|
||||
}
|
||||
return (bool)User::find($userid)?->bot;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,10 +73,13 @@ class UserBot extends AbstractModel
|
||||
'approval-alert' => '审批',
|
||||
'ai-openai' => 'ChatGPT',
|
||||
'ai-claude' => 'Claude',
|
||||
'ai-wenxin' => '文心一言',
|
||||
'ai-qianwen' => '通义千问',
|
||||
'ai-deepseek' => 'DeepSeek',
|
||||
'ai-gemini' => 'Gemini',
|
||||
'ai-grok' => 'Grok',
|
||||
'ai-ollama' => 'Ollama',
|
||||
'ai-zhipu' => '智谱清言',
|
||||
'ai-qianwen' => '通义千问',
|
||||
'ai-wenxin' => '文心一言',
|
||||
'bot-manager' => '机器人管理',
|
||||
'meeting-alert' => '会议通知',
|
||||
'okr-alert' => 'OKR提醒',
|
||||
@@ -177,11 +180,35 @@ class UserBot extends AbstractModel
|
||||
];
|
||||
|
||||
default:
|
||||
if (preg_match('/^ai-(.*?)@bot.system$/', $email)) {
|
||||
return [
|
||||
[
|
||||
if (preg_match('/^ai-(.*?)@bot\.system$/', $email, $match)) {
|
||||
if (!Base::judgeClientVersion('0.42.62')) {
|
||||
return [
|
||||
'key' => '%3A.clear',
|
||||
'label' => Doo::translate('清空上下文')
|
||||
];
|
||||
}
|
||||
$aibotSetting = Base::setting('aibotSetting');
|
||||
$aibotModel = $aibotSetting[$match[1] . '_model'];
|
||||
$aibotModels = Setting::AIModels2Array($aibotSetting[$match[1] . '_models']);
|
||||
if (empty($aibotModels)) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
[
|
||||
'key' => '~ai-model-select',
|
||||
'label' => Doo::translate('选择模型'),
|
||||
'config' => [
|
||||
'model' => $aibotModel,
|
||||
'models' => $aibotModels
|
||||
]
|
||||
],
|
||||
[
|
||||
'key' => '~ai-session-create',
|
||||
'label' => Doo::translate('开启新会话'),
|
||||
],
|
||||
[
|
||||
'key' => '~ai-session-history',
|
||||
'label' => Doo::translate('历史会话'),
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -415,4 +442,39 @@ class UserBot extends AbstractModel
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建我的机器人
|
||||
* @param $userid
|
||||
* @param $botName
|
||||
* @return array
|
||||
*/
|
||||
public static function newbot($userid, $botName)
|
||||
{
|
||||
if (User::select(['users.*'])
|
||||
->join('user_bots', 'users.userid', '=', 'user_bots.bot_id')
|
||||
->where('users.bot', 1)
|
||||
->where('user_bots.userid', $userid)
|
||||
->count() >= 50) {
|
||||
return Base::retError("超过最大创建数量。");
|
||||
}
|
||||
if (strlen($botName) < 2 || strlen($botName) > 20) {
|
||||
return Base::retError("机器人名称由2-20个字符组成。");
|
||||
}
|
||||
$data = User::botGetOrCreate("user-" . Base::generatePassword(), [
|
||||
'nickname' => $botName
|
||||
], $userid);
|
||||
if (empty($data)) {
|
||||
return Base::retError("创建失败。");
|
||||
}
|
||||
$dialog = WebSocketDialog::checkUserDialog($data, $userid);
|
||||
if ($dialog) {
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => '/hello',
|
||||
'title' => '创建成功。',
|
||||
'data' => $data,
|
||||
], $data->userid);
|
||||
}
|
||||
return Base::retSuccess("创建成功。", $data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class UserCheckinFace extends AbstractModel
|
||||
$record = base64_encode(file_get_contents($faceFile));
|
||||
}
|
||||
|
||||
$url = 'http://' . env('APP_IPPR') . '.14' . ":7788/user";
|
||||
$url = "http://face:7788/user";
|
||||
$data = [
|
||||
'name' => $nickname,
|
||||
'enrollid' => $userid,
|
||||
@@ -92,7 +92,7 @@ class UserCheckinFace extends AbstractModel
|
||||
}
|
||||
|
||||
public static function deleteDeviceUser($userid) {
|
||||
$url = 'http://' . env('APP_IPPR') . '.14' . ":7788/user/delete";
|
||||
$url = "http://face:7788/user/delete";
|
||||
$data = [
|
||||
'enrollid' => $userid,
|
||||
'backupnum' => 50, // 13 删除整个用户 50 删除图片
|
||||
|
||||
@@ -34,6 +34,21 @@ use App\Exceptions\ApiException;
|
||||
*/
|
||||
class UserDepartment extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* 获取所有父级部门
|
||||
* @return array
|
||||
*/
|
||||
public function parents()
|
||||
{
|
||||
$parents = [];
|
||||
$parent = $this;
|
||||
while ($parent) {
|
||||
$parents[] = $parent;
|
||||
$parent = $parent->parent_id ? self::find($parent->parent_id) : null;
|
||||
}
|
||||
return $parents;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存部门
|
||||
* @param $data
|
||||
@@ -131,9 +146,7 @@ class UserDepartment extends AbstractModel
|
||||
});
|
||||
// 解散群组
|
||||
$dialog = WebSocketDialog::find($this->dialog_id);
|
||||
if ($dialog) {
|
||||
$dialog->deleteDialog();
|
||||
}
|
||||
$dialog?->deleteDialog();
|
||||
//
|
||||
$this->delete();
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ use Illuminate\Support\Facades\DB;
|
||||
* @property int $id
|
||||
* @property string|null $type 对话类型
|
||||
* @property string|null $group_type 聊天室类型
|
||||
* @property int|null $session_id 会话ID
|
||||
* @property string|null $name 对话名称
|
||||
* @property string $avatar 头像(群)
|
||||
* @property int|null $owner_id 群主用户ID
|
||||
@@ -48,6 +49,7 @@ use Illuminate\Support\Facades\DB;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereLinkId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereOwnerId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereSessionId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereTopMsgId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereTopUserid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereType($value)
|
||||
@@ -265,7 +267,9 @@ class WebSocketDialog extends AbstractModel
|
||||
// 未读消息
|
||||
$data = array_merge($data, self::generateUnread($data['id'], $userid));
|
||||
// 对话人数
|
||||
$data['people'] = $data['people'] ?? WebSocketDialogUser::whereDialogId($data['id'])->count();
|
||||
if (!isset($data['people'])) {
|
||||
$data = array_merge($data, self::generatePeople($data['id']));
|
||||
}
|
||||
// 有待办
|
||||
$data['todo_num'] = $data['todo_num'] ?? WebSocketDialogMsgTodo::whereDialogId($data['id'])->whereUserid($userid)->whereDoneAt(null)->count();
|
||||
// 最后消息
|
||||
@@ -291,6 +295,7 @@ class WebSocketDialog extends AbstractModel
|
||||
$data['email'] = $basic->email;
|
||||
$data['userimg'] = $basic->userimg;
|
||||
$data['bot'] = $basic->getBotOwner();
|
||||
$data['is_disable'] = $basic->isDisable(true);
|
||||
$data['quick_msgs'] = UserBot::quickMsgs($basic->email);
|
||||
} else {
|
||||
$data['name'] = 'non-existent';
|
||||
@@ -329,6 +334,9 @@ class WebSocketDialog extends AbstractModel
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (empty($data['pinyin'])) {
|
||||
$data['pinyin'] = Base::cn2pinyin($data['name']);
|
||||
}
|
||||
|
||||
// 已存在的消息类型
|
||||
if ($hasData === true) {
|
||||
@@ -397,6 +405,26 @@ class WebSocketDialog extends AbstractModel
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话人数
|
||||
* @param $dialogId
|
||||
* @return array
|
||||
*/
|
||||
public static function generatePeople($dialogId)
|
||||
{
|
||||
$counts = WebSocketDialogUser::whereDialogId($dialogId)
|
||||
->groupBy('bot')
|
||||
->selectRaw('bot, COUNT(*) as count')
|
||||
->pluck('count', 'bot');
|
||||
$userCount = $counts->get(0, 0); // 非机器人数量
|
||||
$botCount = $counts->get(1, 0); // 机器人数量
|
||||
return [
|
||||
'people' => $userCount + $botCount,
|
||||
'people_user' => $userCount,
|
||||
'people_bot' => $botCount,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 加入聊天室
|
||||
* @param int|array $userid 加入的会员ID或会员ID组
|
||||
@@ -419,7 +447,11 @@ class WebSocketDialog extends AbstractModel
|
||||
WebSocketDialogUser::updateInsert([
|
||||
'dialog_id' => $this->id,
|
||||
'userid' => $value,
|
||||
], $updateData, [], $isInsert);
|
||||
], $updateData, function() use ($value, $updateData) {
|
||||
return array_merge($updateData, [
|
||||
'bot' => User::isBot($value) ? 1 : 0
|
||||
]);
|
||||
}, $isInsert);
|
||||
if ($isInsert) {
|
||||
WebSocketDialogMsg::sendMsg(null, $this->id, 'notice', [
|
||||
'notice' => User::userid2nickname($value) . " 已加入群组"
|
||||
@@ -428,10 +460,9 @@ class WebSocketDialog extends AbstractModel
|
||||
}
|
||||
}
|
||||
});
|
||||
$this->pushMsg("groupUpdate", [
|
||||
'id' => $this->id,
|
||||
'people' => WebSocketDialogUser::whereDialogId($this->id)->count()
|
||||
]);
|
||||
$data = WebSocketDialog::generatePeople($this->id);
|
||||
$data['id'] = $this->id;
|
||||
$this->pushMsg("groupUpdate", $data);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -488,10 +519,9 @@ class WebSocketDialog extends AbstractModel
|
||||
});
|
||||
});
|
||||
//
|
||||
$this->pushMsg("groupUpdate", [
|
||||
'id' => $this->id,
|
||||
'people' => WebSocketDialogUser::whereDialogId($this->id)->count()
|
||||
]);
|
||||
$data = WebSocketDialog::generatePeople($this->id);
|
||||
$data['id'] = $this->id;
|
||||
$this->pushMsg("groupUpdate", $data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -740,6 +770,17 @@ class WebSocketDialog extends AbstractModel
|
||||
'dialog_id' => $dialog->id,
|
||||
'userid' => $receiver,
|
||||
])->save();
|
||||
//
|
||||
if ($user->isAiBot() || User::find($receiver)?->isAiBot()) {
|
||||
$session = WebSocketDialogSession::create([
|
||||
'dialog_id' => $dialog->id,
|
||||
'status' => 1,
|
||||
'title' => '',
|
||||
]);
|
||||
$session->save();
|
||||
$dialog->session_id = $session->id;
|
||||
$dialog->save();
|
||||
}
|
||||
return $dialog;
|
||||
});
|
||||
}
|
||||
@@ -754,6 +795,9 @@ class WebSocketDialog extends AbstractModel
|
||||
*/
|
||||
public static function getUserDialog($userid1, $userid2, $ttl, &$cacheKey = null)
|
||||
{
|
||||
if ($userid1 == $userid2) {
|
||||
$userid2 = 0;
|
||||
}
|
||||
$userids = [$userid1, $userid2];
|
||||
sort($userids);
|
||||
$cacheKey = "Dialog::user:" . implode('-', $userids);
|
||||
@@ -806,7 +850,7 @@ class WebSocketDialog extends AbstractModel
|
||||
"image64" => $image64,
|
||||
"path" => $path,
|
||||
"fileName" => $fileName,
|
||||
"quality" => 85
|
||||
"quality" => true
|
||||
]);
|
||||
} else if ($filePath) {
|
||||
Base::makeDir(public_path($path));
|
||||
@@ -817,7 +861,7 @@ class WebSocketDialog extends AbstractModel
|
||||
"type" => 'more',
|
||||
"path" => $path,
|
||||
"fileName" => $fileName,
|
||||
"quality" => 100,
|
||||
"quality" => true,
|
||||
"convertVideo" => true
|
||||
]);
|
||||
}
|
||||
|
||||
64
app/Models/WebSocketDialogConfig.php
Normal file
64
app/Models/WebSocketDialogConfig.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/**
|
||||
* App\Models\WebSocketDialogConfig
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $dialog_id 对话ID
|
||||
* @property int $userid 用户ID
|
||||
* @property string $type 配置类型
|
||||
* @property string|null $value 配置值
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\WebSocketDialog|null $dialog
|
||||
* @property-read \App\Models\User $user
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig whereDialogId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig whereType($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig whereUserid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogConfig whereValue($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class WebSocketDialogConfig extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* 可以批量赋值的属性
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'dialog_id',
|
||||
'userid',
|
||||
'type',
|
||||
'value',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取关联的对话
|
||||
*/
|
||||
public function dialog()
|
||||
{
|
||||
return $this->belongsTo(WebSocketDialog::class, 'dialog_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取关联的用户
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'userid');
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @property int $id
|
||||
* @property int|null $dialog_id 对话ID
|
||||
* @property string|null $dialog_type 对话类型
|
||||
* @property int|null $session_id 会话ID
|
||||
* @property int|null $userid 发送会员ID
|
||||
* @property string|null $type 消息类型
|
||||
* @property string|null $mtype 消息类型(用于搜索)
|
||||
@@ -30,6 +31,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @property int|null $todo 设为待办会员ID
|
||||
* @property int|null $link 是否存在链接
|
||||
* @property int|null $modify 是否编辑
|
||||
* @property int|null $bot 是否机器人的消息
|
||||
* @property int|null $reply_num 有多少条回复
|
||||
* @property int|null $reply_id 回复ID
|
||||
* @property int|null $forward_id 转发ID
|
||||
@@ -38,6 +40,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||
* @property-read int|mixed $percentage
|
||||
* @property-read \App\Models\User|null $user
|
||||
* @property-read \App\Models\WebSocketDialog|null $webSocketDialog
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
@@ -49,6 +52,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereBot($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDeletedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDialogId($value)
|
||||
@@ -66,6 +70,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereReplyId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereReplyNum($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereSend($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereSessionId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereTag($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereTodo($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereType($value)
|
||||
@@ -96,6 +101,14 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
return $this->hasOne(WebSocketDialog::class, 'id', 'dialog_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
*/
|
||||
public function user(): \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
{
|
||||
return $this->hasOne(User::class, 'userid', 'userid');
|
||||
}
|
||||
|
||||
/**
|
||||
* 阅读占比
|
||||
* @return int|mixed
|
||||
@@ -103,7 +116,11 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
public function getPercentageAttribute()
|
||||
{
|
||||
if (!isset($this->appendattrs['percentage'])) {
|
||||
$this->generatePercentage();
|
||||
if ($this->read > $this->send || empty($this->send)) {
|
||||
$this->appendattrs['percentage'] = 100;
|
||||
} else {
|
||||
$this->appendattrs['percentage'] = intval($this->read / $this->send * 100);
|
||||
}
|
||||
}
|
||||
return $this->appendattrs['percentage'];
|
||||
}
|
||||
@@ -180,22 +197,6 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
return $msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取占比
|
||||
* @param bool|int $increment 是否新增阅读数
|
||||
* @return int
|
||||
*/
|
||||
public function generatePercentage($increment = false) {
|
||||
if ($increment) {
|
||||
$this->increment('read', is_bool($increment) ? 1 : $increment);
|
||||
}
|
||||
if ($this->read > $this->send || empty($this->send)) {
|
||||
return $this->appendattrs['percentage'] = 100;
|
||||
} else {
|
||||
return $this->appendattrs['percentage'] = intval($this->read / $this->send * 100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记已送达 同时 告诉发送人已送达
|
||||
* @param $userid
|
||||
@@ -225,16 +226,17 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
if (!$msgRead->read_at) {
|
||||
$msgRead->read_at = Carbon::now();
|
||||
$msgRead->save();
|
||||
$this->generatePercentage(true);
|
||||
//
|
||||
$row = self::incrementRead($this->id);
|
||||
PushTask::push([
|
||||
'userid' => $this->userid,
|
||||
'userid' => $row->userid,
|
||||
'msg' => [
|
||||
'type' => 'dialog',
|
||||
'mode' => 'readed',
|
||||
'data' => [
|
||||
'id' => $this->id,
|
||||
'read' => $this->read,
|
||||
'percentage' => $this->percentage,
|
||||
'id' => $row->id,
|
||||
'read' => $row->read,
|
||||
'percentage' => $row->percentage,
|
||||
],
|
||||
]
|
||||
]);
|
||||
@@ -243,6 +245,24 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加已读数量
|
||||
* @param $msgId
|
||||
* @return self
|
||||
*/
|
||||
private static function incrementRead($msgId)
|
||||
{
|
||||
return self::transaction(function () use ($msgId) {
|
||||
$model = WebSocketDialogMsg::lockForUpdate()->find($msgId);
|
||||
if (!$model) {
|
||||
throw new \Exception('记录不存在');
|
||||
}
|
||||
|
||||
$model->increment('read');
|
||||
return WebSocketDialogMsg::find($msgId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* emoji回复
|
||||
* @param $symbol
|
||||
@@ -285,7 +305,12 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
];
|
||||
//
|
||||
$dialog = WebSocketDialog::find($this->dialog_id);
|
||||
$dialog?->pushMsg('update', $resData);
|
||||
if ($dialog) {
|
||||
$dialog->pushMsg('update', $resData);
|
||||
WebSocketDialogUser::whereDialogId($dialog->id)->change([
|
||||
'updated_at' => Carbon::now()->toDateTimeString('millisecond'),
|
||||
]);
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('success', $resData);
|
||||
}
|
||||
@@ -514,10 +539,6 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
*/
|
||||
public function withdrawMsg()
|
||||
{
|
||||
$send_dt = Carbon::parse($this->created_at)->addDay();
|
||||
if ($send_dt->lt(Carbon::now())) {
|
||||
throw new ApiException('已超过24小时,此消息不能撤回');
|
||||
}
|
||||
AbstractModel::transaction(function() {
|
||||
$deleteRead = WebSocketDialogMsgRead::whereMsgId($this->id)->whereNull('read_at')->delete(); // 未阅读记录不需要软删除,直接删除即可
|
||||
$this->delete();
|
||||
@@ -574,6 +595,9 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
case 'text':
|
||||
return self::previewTextMsg($data['msg'], $preserveHtml);
|
||||
|
||||
case 'longtext':
|
||||
return $data['msg']['desc'] ? Base::cutStr($data['msg']['desc'], 50) : ("[" . Doo::translate("长文本") . "]");
|
||||
|
||||
case 'vote':
|
||||
$action = Doo::translate("投票");
|
||||
return "[{$action}] " . self::previewTextMsg($data['msg'], $preserveHtml);
|
||||
@@ -610,7 +634,8 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
return "[{$action}] " . self::previewMsg($data['msg']['data']);
|
||||
|
||||
case 'notice':
|
||||
return Base::cutStr(Doo::translate($data['msg']['notice']), 50);
|
||||
$notice = $data['msg']['source'] === 'api' ? $data['msg']['notice'] : Doo::translate($data['msg']['notice']);
|
||||
return Base::cutStr($notice, 50);
|
||||
|
||||
case 'template':
|
||||
return self::previewTemplateMsg($data['msg']);
|
||||
@@ -635,7 +660,12 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
$text = $msgData['text'] ?? '';
|
||||
if (!$text) return '';
|
||||
if ($msgData['type'] === 'md') {
|
||||
$text = preg_replace("/:::\s*reasoning[\s\S]*?:::/", "", $text);
|
||||
if (preg_match('/:::\s*reasoning\s+/', $text)) {
|
||||
return Doo::translate('思考中...');
|
||||
}
|
||||
$text = Base::markdown2html($text);
|
||||
$text = self::previewConvertTaskList($text);
|
||||
}
|
||||
$text = preg_replace("/<img\s+class=\"emoticon\"[^>]*?alt=\"(\S+)\"[^>]*?>/", "[$1]", $text);
|
||||
$text = preg_replace("/<img\s+class=\"emoticon\"[^>]*?>/", "[" . Doo::translate('动画表情') . "]", $text);
|
||||
@@ -650,6 +680,36 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换任务列表
|
||||
* @param $text
|
||||
* @return array|string|string[]|null
|
||||
*/
|
||||
private static function previewConvertTaskList($text) {
|
||||
$pattern = '/:::\s*(create-task-list|create-subtask-list)(.*?):::/s';
|
||||
$replacement = function($matches) {
|
||||
$content = $matches[2];
|
||||
$lines = explode("\n", trim($content));
|
||||
$result = [];
|
||||
$currentTitle = '';
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if (empty($line)) continue;
|
||||
|
||||
if (preg_match('/^title:\s*(.+)$/', $line, $titleMatch)) {
|
||||
$currentTitle = $titleMatch[1];
|
||||
$result[] = $currentTitle;
|
||||
} elseif (preg_match('/^desc:\s*(.+)$/', $line, $descMatch)) {
|
||||
if (!empty($currentTitle)) {
|
||||
$result[] = $descMatch[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
return implode("\n", $result);
|
||||
};
|
||||
return preg_replace_callback($pattern, $replacement, $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览文件消息
|
||||
* @param $msg
|
||||
@@ -679,13 +739,15 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
return $msg['title_raw'];
|
||||
}
|
||||
if ($msg['type'] === 'task_list' && count($msg['list']) === 1) {
|
||||
return Doo::translate($msg['title']) . ": " . Base::cutStr($msg['list'][0]['name'], 50);
|
||||
$title = $msg['source'] === 'api' ? $msg['title'] : Doo::translate($msg['title']);
|
||||
return $title . ": " . Base::cutStr($msg['list'][0]['name'], 50);
|
||||
}
|
||||
if (!empty($msg['title'])) {
|
||||
return Doo::translate($msg['title']);
|
||||
return $msg['source'] === 'api' ? $msg['title'] : Doo::translate($msg['title']);
|
||||
}
|
||||
if ($msg['type'] === 'content' && is_string($msg['content']) && $msg['content'] !== '') {
|
||||
return Base::cutStr(Doo::translate($msg['content']), 50);
|
||||
$content = $msg['source'] === 'api' ? $msg['content'] : Doo::translate($msg['content']);
|
||||
return Base::cutStr($content, 50);
|
||||
}
|
||||
return Doo::translate('未知的消息');
|
||||
}
|
||||
@@ -701,9 +763,15 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
$key = '';
|
||||
switch ($this->type) {
|
||||
case 'text':
|
||||
if (!preg_match("/<span[^>]*?data-quick-key=([\"'])([^\"']+?)\\1[^>]*?>/is", $this->msg['text'])) {
|
||||
$key = strip_tags($this->msg['text']);
|
||||
if (preg_match("/<span[^>]*?data-quick-key=([\"'])([^\"']+?)\\1[^>]*?>/i", $this->msg['text'])) {
|
||||
break;
|
||||
}
|
||||
$key = $this->msg['text'];
|
||||
if ($this->msg['type'] === 'md') {
|
||||
$key = preg_replace("/:::\s*reasoning[\s\S]*?:::/", "", $key);
|
||||
$key = Base::markdown2html($key);
|
||||
}
|
||||
$key = strip_tags($key);
|
||||
break;
|
||||
|
||||
case 'vote':
|
||||
@@ -722,14 +790,24 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
break;
|
||||
}
|
||||
}
|
||||
$key = str_replace([""", "&", "<", ">"], "", $key);
|
||||
$key = str_replace(["\r", "\n", "\t", " "], " ", $key);
|
||||
$key = preg_replace("/^\/[A-Za-z]+/", " ", $key);
|
||||
$key = preg_replace("/\s+/", " ", $key);
|
||||
$this->key = trim($key);
|
||||
$this->key = self::filterEscape($key);
|
||||
$this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤转义
|
||||
* @param $content
|
||||
* @return string
|
||||
*/
|
||||
public static function filterEscape($content)
|
||||
{
|
||||
$content = str_replace([""", "&", "<", ">"], "", $content);
|
||||
$content = str_replace(["\r", "\n", "\t", " "], " ", $content);
|
||||
$content = preg_replace("/^\/[A-Za-z]+/", " ", $content);
|
||||
$content = preg_replace("/\s+/", " ", $content);
|
||||
return trim($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回引用消息(如果是文本只取预览)
|
||||
* @return array|mixed
|
||||
@@ -760,7 +838,7 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
$imagePath = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
|
||||
Base::makeDir(public_path($imagePath));
|
||||
$imagePath .= md5s($base64) . "." . $matchs[1][$key];
|
||||
if (Base::saveContentImage(public_path($imagePath), base64_decode($base64), 90)) {
|
||||
if (Base::saveContentImage(public_path($imagePath), base64_decode($base64))) {
|
||||
$imageSize = getimagesize(public_path($imagePath));
|
||||
if ($extension = Image::thumbImage(public_path($imagePath), public_path($imagePath) . "_thumb.{*}", 320, 0, 80)) {
|
||||
$imagePath .= "_thumb.{$extension}";
|
||||
@@ -817,8 +895,16 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
$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) {
|
||||
$parsed = parse_url($str);
|
||||
if (str_starts_with($parsed['path'], "/uploads/")) {
|
||||
$relativePath = ltrim($parsed['path'], "/");
|
||||
$relativePath = Base::thumbRestore($relativePath);
|
||||
if (file_exists(public_path($relativePath))) {
|
||||
$str = "{{RemoteURL}}{$relativePath}";
|
||||
}
|
||||
}
|
||||
if ($imageSaveLocal === 'close') {
|
||||
$imageSize = getimagesize($str);
|
||||
$imageSize = @getimagesize($str);
|
||||
if ($imageSize === false) {
|
||||
$imageSize = ["auto", "auto"];
|
||||
}
|
||||
@@ -844,7 +930,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 (Base::saveContentImage(public_path($imagePath), $image, 90)) {
|
||||
} else if (Base::saveContentImage(public_path($imagePath), $image)) {
|
||||
$imageSize = getimagesize(public_path($imagePath));
|
||||
if ($extension = Image::thumbImage(public_path($imagePath), public_path($imagePath) . "_thumb.{*}", 320, 0, 80)) {
|
||||
$imagePath .= "_thumb.{$extension}";
|
||||
@@ -853,7 +939,7 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
}
|
||||
}
|
||||
}
|
||||
// @成员、#任务、~文件
|
||||
// @成员、#任务、~文件、%报告
|
||||
preg_match_all("/<span\s+class=\"mention\"(.*?)>.*?<\/span>.*?<\/span>.*?<\/span>/s", $text, $matchs);
|
||||
foreach ($matchs[1] as $key => $str) {
|
||||
preg_match("/data-denotation-char=\"(.*?)\"/", $str, $matchChar);
|
||||
@@ -861,6 +947,7 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
preg_match("/data-value=\"(.*?)\"/s", $str, $matchValye);
|
||||
$keyId = $matchId[1];
|
||||
if ($matchChar[1] === "~") {
|
||||
// 文件特殊处理
|
||||
if (Base::isNumber($keyId)) {
|
||||
$file = File::permissionFind($keyId, User::auth());
|
||||
if ($file->type == 'folder') {
|
||||
@@ -876,6 +963,19 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
throw new ApiException('文件分享错误');
|
||||
}
|
||||
}
|
||||
} elseif ($matchChar[1] === "%") {
|
||||
// 报告特殊处理
|
||||
if (Base::isNumber($keyId)) {
|
||||
$reportLink = ReportLink::generateLink($keyId, User::userid());
|
||||
$keyId = $reportLink['code'];
|
||||
} else {
|
||||
preg_match("/\/single\/report\/detail\/(.*?)$/i", $keyId, $match);
|
||||
if ($match && strlen($match[1]) >= 8) {
|
||||
$keyId = $match[1];
|
||||
} else {
|
||||
throw new ApiException('报告分享错误');
|
||||
}
|
||||
}
|
||||
}
|
||||
$text = str_replace($matchs[0][$key], "[:{$matchChar[1]}:{$keyId}:{$matchValye[1]}:]", $text);
|
||||
}
|
||||
@@ -904,31 +1004,18 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
foreach ($matchs[0] as $key => $str) {
|
||||
$herf = $matchs[2][$key];
|
||||
$title = $matchs[3][$key] ?: $herf;
|
||||
preg_match("/\/single\/file\/(.*?)$/i", strip_tags($title), $match);
|
||||
if ($match && strlen($match[1]) >= 8) {
|
||||
$file = File::select(['files.id', 'files.name', 'files.ext'])->join('file_links as L', 'files.id', '=', 'L.file_id')->where('L.code', $match[1])->first();
|
||||
if ($file && $file->name) {
|
||||
$name = $file->ext ? "{$file->name}.{$file->ext}" : $file->name;
|
||||
$text = str_replace($str, "[:~:{$match[1]}:{$name}:]", $text);
|
||||
continue;
|
||||
}
|
||||
if (self::formatLink($str, strip_tags($title), $text)) {
|
||||
continue;
|
||||
}
|
||||
$herf = base64_encode($herf);
|
||||
$title = base64_encode($title);
|
||||
$text = str_replace($str, "[:LINK:{$herf}:{$title}:]", $text);
|
||||
}
|
||||
// 文件分享链接
|
||||
// 分享链接
|
||||
preg_match_all("/(https?:\/\/)((\w|=|\?|\.|\/|&|-|:|\+|%|;|#|@|,|!)+)/i", $text, $matchs);
|
||||
if ($matchs) {
|
||||
foreach ($matchs[0] as $str) {
|
||||
preg_match("/\/single\/file\/(.*?)$/i", $str, $match);
|
||||
if ($match && strlen($match[1]) >= 8) {
|
||||
$file = File::select(['files.id', 'files.name', 'files.ext'])->join('file_links as L', 'files.id', '=', 'L.file_id')->where('L.code', $match[1])->first();
|
||||
if ($file && $file->name) {
|
||||
$name = $file->ext ? "{$file->name}.{$file->ext}" : $file->name;
|
||||
$text = str_replace($str, "[:~:{$match[1]}:{$name}:]", $text);
|
||||
}
|
||||
}
|
||||
self::formatLink($str, $str, $text);
|
||||
}
|
||||
}
|
||||
// 过滤标签
|
||||
@@ -958,10 +1045,41 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
$text = preg_replace("/\[:@:(.*?):(.*?):\]/i", "<span class=\"mention user\" data-id=\"$1\">@$2</span>", $text);
|
||||
$text = preg_replace("/\[:#:(.*?):(.*?):\]/is", "<span class=\"mention task\" data-id=\"$1\">#$2</span>", $text);
|
||||
$text = preg_replace("/\[:~:(.*?):(.*?):\]/i", "<a class=\"mention file\" href=\"{{RemoteURL}}single/file/$1\" target=\"_blank\">~$2</a>", $text);
|
||||
$text = preg_replace("/\[:%:(.*?):(.*?):\]/i", "<a class=\"mention report\" href=\"{{RemoteURL}}single/report/detail/$1\" target=\"_blank\">%$2</a>", $text);
|
||||
$text = preg_replace("/\[:QUICK:(.*?):(.*?):\]/i", "<span data-quick-key=\"$1\">$2</span>", $text);
|
||||
return preg_replace("/^(<p><\/p>)+|(<p><\/p>)+$/i", "", $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 链接转换处理
|
||||
* @param $search
|
||||
* @param $subject
|
||||
* @param $content
|
||||
* @return bool
|
||||
*/
|
||||
public static function formatLink($search, $subject, &$content)
|
||||
{
|
||||
$ret = false;
|
||||
preg_match("/\/single\/file\/(.*?)$/i", $subject, $match);
|
||||
if ($match && strlen($match[1]) >= 8) {
|
||||
$file = File::select(['files.id', 'files.name', 'files.ext'])->join('file_links as L', 'files.id', '=', 'L.file_id')->where('L.code', $match[1])->first();
|
||||
if ($file && $file->name) {
|
||||
$name = $file->ext ? "{$file->name}.{$file->ext}" : $file->name;
|
||||
$content = str_replace($search, "[:~:{$match[1]}:{$name}:]", $content);
|
||||
$ret = true;
|
||||
}
|
||||
}
|
||||
preg_match("/\/single\/report\/detail\/(.*?)$/i", $subject, $match);
|
||||
if ($match && strlen($match[1]) >= 8) {
|
||||
$report = Report::select(['reports.id', 'reports.title'])->join('report_links as L', 'reports.id', '=', 'L.rid')->where('L.code', $match[1])->first();
|
||||
if ($report && $report->title) {
|
||||
$content = str_replace($search, "[:%:{$match[1]}:{$report->title}:]", $content);
|
||||
$ret = true;
|
||||
}
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息、修改消息
|
||||
* @param string $action 动作
|
||||
@@ -1012,7 +1130,7 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
$fileUrl = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/" . md5s($msg['thumb']) . ".jpg";
|
||||
$filePath = public_path($fileUrl);
|
||||
Base::makeDir(dirname($filePath));
|
||||
if (!Base::saveContentImage($filePath, $thumb, 90)) {
|
||||
if (!Base::saveContentImage($filePath, $thumb)) {
|
||||
throw new ApiException('保存地图快照失败');
|
||||
}
|
||||
$imageSize = getimagesize($filePath);
|
||||
@@ -1071,6 +1189,7 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
}
|
||||
//
|
||||
$updateData = [
|
||||
'type' => $type,
|
||||
'mtype' => $mtype,
|
||||
'link' => $link,
|
||||
'msg' => array_merge($oldMsg, $msg),
|
||||
@@ -1113,12 +1232,14 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
$dialogMsg = self::createInstance([
|
||||
'dialog_id' => $dialog_id,
|
||||
'dialog_type' => $dialog->type,
|
||||
'session_id' => $dialog->session_id,
|
||||
'reply_id' => $reply_id,
|
||||
'forward_id' => $forward_id,
|
||||
'userid' => $sender,
|
||||
'type' => $type,
|
||||
'mtype' => $mtype,
|
||||
'link' => $link,
|
||||
'bot' => User::isBot($sender) ? 1 : 0,
|
||||
'msg' => $msg,
|
||||
'read' => 0,
|
||||
]);
|
||||
@@ -1126,6 +1247,8 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
$dialogMsg->send = 1;
|
||||
$dialogMsg->generateKeyAndSave($search_key);
|
||||
//
|
||||
WebSocketDialogSession::updateTitle($dialogMsg->session_id, $dialogMsg);
|
||||
//
|
||||
if ($dialogMsg->type === 'meeting') {
|
||||
MeetingMsg::createInstance([
|
||||
'meetingid' => $dialogMsg->msg['meetingid'],
|
||||
@@ -1157,6 +1280,55 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量发送消息
|
||||
* @param User $user 发送的会员
|
||||
* @param array $userids 接收的会员ID
|
||||
* @param array $dialogids 接收的会话ID
|
||||
* @param string $msgText 发送的消息
|
||||
* @return array
|
||||
*/
|
||||
public static function sendMsgBatch($user, $userids, $dialogids, $msgText)
|
||||
{
|
||||
return AbstractModel::transaction(function() use ($user, $userids, $dialogids, $msgText) {
|
||||
$msgs = [];
|
||||
$already = [];
|
||||
if ($dialogids) {
|
||||
if (!is_array($dialogids)) {
|
||||
$dialogids = [$dialogids];
|
||||
}
|
||||
foreach ($dialogids as $dialogid) {
|
||||
$res = WebSocketDialogMsg::sendMsg(null, $dialogid, 'text', ['text' => $msgText], $user->userid);
|
||||
if (Base::isSuccess($res)) {
|
||||
$msgs[] = $res['data'];
|
||||
$already[] = $dialogid;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($userids) {
|
||||
if (!is_array($userids)) {
|
||||
$userids = [$userids];
|
||||
}
|
||||
foreach ($userids as $userid) {
|
||||
if (!User::whereUserid($userid)->exists()) {
|
||||
continue;
|
||||
}
|
||||
$dialog = WebSocketDialog::checkUserDialog($user, $userid);
|
||||
if ($dialog && !in_array($dialog->id, $already)) {
|
||||
$res = WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $msgText], $user->userid);
|
||||
if (Base::isSuccess($res)) {
|
||||
$msgs[] = $res['data'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Base::retSuccess('发送成功', [
|
||||
'msgs' => $msgs
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将被@的人加入群
|
||||
* @param WebSocketDialog $dialog 对话
|
||||
|
||||
@@ -91,7 +91,9 @@ class WebSocketDialogMsgRead extends AbstractModel
|
||||
}
|
||||
}
|
||||
foreach ($dialogMsg as $item) {
|
||||
$item['dialogMsg']?->generatePercentage($item['readNum']);
|
||||
if ($item['dialogMsg']) {
|
||||
$item['dialogMsg']->increment('read', $item['readNum']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
99
app/Models/WebSocketDialogSession.php
Normal file
99
app/Models/WebSocketDialogSession.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Module\Base;
|
||||
use App\Module\Extranet;
|
||||
use Swoole\Coroutine;
|
||||
use Cache;
|
||||
|
||||
/**
|
||||
* App\Models\WebSocketDialogSession
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $dialog_id 对话ID
|
||||
* @property string $title 会话标题
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\WebSocketDialog|null $dialog
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereDialogId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereTitle($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogSession whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class WebSocketDialogSession extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* 可以批量赋值的属性
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'dialog_id',
|
||||
'userid',
|
||||
'title',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取关联的对话
|
||||
*/
|
||||
public function dialog()
|
||||
{
|
||||
return $this->belongsTo(WebSocketDialog::class, 'dialog_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $sessionId
|
||||
* @param WebSocketDialogMsg $dialogMsg
|
||||
* @return void
|
||||
*/
|
||||
public static function updateTitle($sessionId, $dialogMsg)
|
||||
{
|
||||
if (!$sessionId) {
|
||||
return;
|
||||
}
|
||||
if ($dialogMsg->type != 'text') {
|
||||
return;
|
||||
}
|
||||
$cacheKey = 'dialog_session_title_' . $sessionId;
|
||||
if (Cache::has($cacheKey)) {
|
||||
return;
|
||||
}
|
||||
$originalTitle = $dialogMsg->key ?: $dialogMsg->msg['text'] ?: 'Untitled';
|
||||
$title = Base::cutStr($originalTitle, 100);
|
||||
if ($title == '...') {
|
||||
return;
|
||||
}
|
||||
$session = self::whereId($sessionId)->first();
|
||||
if (!$session) {
|
||||
return;
|
||||
}
|
||||
$session->title = $title;
|
||||
$session->save();
|
||||
Cache::forever($cacheKey, true);
|
||||
// 通过AI接口更新对话标题
|
||||
go(function () use ($session, $title, $originalTitle) {
|
||||
Coroutine::sleep(0.1);
|
||||
$res = Extranet::openAIGenerateTitle($originalTitle);
|
||||
if (Base::isError($res)) {
|
||||
return;
|
||||
}
|
||||
$newTitle = $res['data'];
|
||||
if ($newTitle && $newTitle != $title) {
|
||||
$session->title = Base::cutStr($newTitle, 100);
|
||||
$session->save();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,13 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* App\Models\WebSocketDialogUser
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $dialog_id 对话ID
|
||||
* @property int|null $userid 会员ID
|
||||
* @property int|null $bot 是否机器人
|
||||
* @property \Illuminate\Support\Carbon|null $top_at 置顶时间
|
||||
* @property \Illuminate\Support\Carbon|null $last_at 最后消息时间
|
||||
* @property int|null $mark_unread 是否标记为未读:0否,1是
|
||||
@@ -30,6 +29,7 @@ use Carbon\Carbon;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereBot($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereColor($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereDialogId($value)
|
||||
|
||||
@@ -9,11 +9,12 @@ use App\Services\RequestContext;
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
use League\CommonMark\Exception\CommonMarkException;
|
||||
use League\HTMLToMarkdown\HtmlConverter;
|
||||
use Overtrue\Pinyin\Pinyin;
|
||||
use Redirect;
|
||||
use Request;
|
||||
use Storage;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
||||
use Symfony\Component\HttpFoundation\File\File;
|
||||
use Validator;
|
||||
@@ -801,16 +802,16 @@ class Base
|
||||
str_starts_with(str_replace(' ', '', $str), "data:image/")
|
||||
) {
|
||||
return $str;
|
||||
} else {
|
||||
if (RequestContext::has('fill_url_remote_url')) {
|
||||
return "{{RemoteURL}}" . $str;
|
||||
}
|
||||
try {
|
||||
return url($str);
|
||||
} catch (\Throwable) {
|
||||
return self::getSchemeAndHost() . "/" . $str;
|
||||
}
|
||||
}
|
||||
if (RequestContext::has('fill_url_remote_url')) {
|
||||
return "{{RemoteURL}}" . $str;
|
||||
}
|
||||
try {
|
||||
$fillUrl = url($str);
|
||||
} catch (\Throwable) {
|
||||
$fillUrl = self::getSchemeAndHost() . "/" . $str;
|
||||
}
|
||||
return RequestContext::replaceBaseUrl($fillUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1294,7 +1295,7 @@ class Base
|
||||
* 获取或设置
|
||||
* @param $setname // 配置名称
|
||||
* @param bool $array // 保存内容
|
||||
* @param false $isUpdate // 保存内容为更新模式,默认否
|
||||
* @param bool $isUpdate // 保存内容为更新模式,默认否
|
||||
* @return array
|
||||
*/
|
||||
public static function setting($setname, $array = false, $isUpdate = false)
|
||||
@@ -1475,14 +1476,36 @@ class Base
|
||||
public static function forumHourDay($hour)
|
||||
{
|
||||
$hour = intval($hour);
|
||||
if ($hour > 24) {
|
||||
if ($hour >= 24) {
|
||||
$day = floor($hour / 24);
|
||||
$hour -= $day * 24;
|
||||
return $day . '天' . $hour . '小时';
|
||||
if ($hour > 0) {
|
||||
return $day . '天' . $hour . '小时';
|
||||
}
|
||||
return $day . '天';
|
||||
}
|
||||
return $hour . '小时';
|
||||
}
|
||||
|
||||
/**
|
||||
* 分钟转天/小时/分钟
|
||||
* @param $minute
|
||||
* @return string
|
||||
*/
|
||||
public static function forumMinuteDay($minute)
|
||||
{
|
||||
$minute = intval($minute);
|
||||
if ($minute >= 60) {
|
||||
$hour = floor($minute / 60);
|
||||
$minute -= $hour * 60;
|
||||
if ($minute > 0) {
|
||||
return Base::forumHourDay($hour) . $minute . '分钟';
|
||||
}
|
||||
return Base::forumHourDay($hour);
|
||||
}
|
||||
return $minute . '分钟';
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Carbon对象
|
||||
* @param $var
|
||||
@@ -1849,18 +1872,18 @@ class Base
|
||||
if (!in_array($extension, ['mp3', 'wav'])) {
|
||||
return Base::retError('语音格式错误');
|
||||
}
|
||||
$fileName = 'record_' . md5($base64) . '.' . $extension;
|
||||
$saveName = 'record_' . md5($base64) . '.' . $extension;
|
||||
$fileDir = $param['path'];
|
||||
$filePath = public_path($fileDir);
|
||||
Base::makeDir($filePath);
|
||||
if (file_put_contents($filePath . $fileName, base64_decode(str_replace($res[1], '', $base64)))) {
|
||||
$fileSize = filesize($filePath . $fileName);
|
||||
if (file_put_contents($filePath . $saveName, base64_decode(str_replace($res[1], '', $base64)))) {
|
||||
$fileSize = filesize($filePath . $saveName);
|
||||
$array = [
|
||||
"name" => $fileName, //原文件名
|
||||
"name" => $saveName, //原文件名
|
||||
"size" => Base::twoFloat($fileSize / 1024, true), //大小KB
|
||||
"file" => $filePath . $fileName, //文件的完整路径 "D:\www....KzZ.jpg"
|
||||
"path" => $fileDir . $fileName, //相对路径 "uploads/pic....KzZ.jpg"
|
||||
"url" => Base::fillUrl($fileDir . $fileName), //完整的URL "https://.....hhsKzZ.jpg"
|
||||
"file" => $filePath . $saveName, //文件的完整路径 "D:\www....KzZ.jpg"
|
||||
"path" => $fileDir . $saveName, //相对路径 "uploads/pic....KzZ.jpg"
|
||||
"url" => Base::fillUrl($fileDir . $saveName), //完整的URL "https://.....hhsKzZ.jpg"
|
||||
"ext" => $extension, //文件后缀名
|
||||
];
|
||||
return Base::retSuccess('success', $array);
|
||||
@@ -1871,8 +1894,23 @@ class Base
|
||||
|
||||
/**
|
||||
* image64图片保存
|
||||
* @param array $param [ image64=带前缀的base64, path=>文件路径, fileName=>文件名称, scale=>[压缩原图宽,高, 压缩方式], autoThumb=>false不要自动生成缩略图, 'quality'=>压缩图片质量(默认:0不压缩) ]
|
||||
* @return array [name=>文件名, size=>文件大小(单位KB),file=>绝对地址, path=>相对地址, url=>全路径地址, ext=>文件后缀名]
|
||||
* @param array $param [
|
||||
image64=带前缀的base64,
|
||||
path=>文件路径,
|
||||
fileName=>文件名称,
|
||||
saveName=>保存文件名称,
|
||||
scale=>[压缩原图宽,高, 压缩方式],
|
||||
autoThumb=>false不要自动生成缩略图,
|
||||
quality=>压缩图片质量(默认:0不压缩)
|
||||
]
|
||||
* @return array [
|
||||
name=>文件名,
|
||||
size=>文件大小(单位KB),
|
||||
file=>绝对地址,
|
||||
path=>相对地址,
|
||||
url=>全路径地址,
|
||||
ext=>文件后缀名
|
||||
]
|
||||
*/
|
||||
public static function image64save($param)
|
||||
{
|
||||
@@ -1883,8 +1921,8 @@ class Base
|
||||
return Base::retError('图片格式错误');
|
||||
}
|
||||
$scaleName = "";
|
||||
if ($param['fileName']) {
|
||||
$fileName = basename($param['fileName']);
|
||||
if ($param['saveName']) {
|
||||
$saveName = basename($param['saveName']);
|
||||
} else {
|
||||
if ($param['scale'] && is_array($param['scale'])) {
|
||||
list($width, $height) = $param['scale'];
|
||||
@@ -1895,21 +1933,21 @@ class Base
|
||||
}
|
||||
}
|
||||
}
|
||||
$fileName = 'paste_' . md5($imgBase64) . '.' . $extension;
|
||||
$saveName = 'paste_' . md5($imgBase64) . '.' . $extension;
|
||||
$scaleName = md5_file($imgBase64) . $scaleName . '.' . $extension;
|
||||
}
|
||||
$fileDir = $param['path'];
|
||||
$filePath = public_path($fileDir);
|
||||
$fileFullPath = $filePath . $fileName;
|
||||
$fileFullPath = $filePath . $saveName;
|
||||
Base::makeDir($filePath);
|
||||
if (file_put_contents($fileFullPath, base64_decode(str_replace($res[1], '', $imgBase64)))) {
|
||||
$fileSize = filesize($fileFullPath);
|
||||
$array = [
|
||||
"name" => $fileName, //原文件名
|
||||
"name" => $param['fileName'] ?: $saveName, //原文件名
|
||||
"size" => Base::twoFloat($fileSize / 1024, true), //大小KB
|
||||
"file" => $fileFullPath, //文件的完整路径 "D:\www....KzZ.jpg"
|
||||
"path" => $fileDir . $fileName, //相对路径 "uploads/pic....KzZ.jpg"
|
||||
"url" => Base::fillUrl($fileDir . $fileName), //完整的URL "https://.....hhsKzZ.jpg"
|
||||
"path" => $fileDir . $saveName, //相对路径 "uploads/pic....KzZ.jpg"
|
||||
"url" => Base::fillUrl($fileDir . $saveName), //完整的URL "https://.....hhsKzZ.jpg"
|
||||
"thumb" => '', //缩略图(预览图) "https://.....hhsKzZ.jpg_thumb.jpg"
|
||||
"width" => -1, //图片宽度
|
||||
"height" => -1, //图片高度
|
||||
@@ -1932,7 +1970,7 @@ class Base
|
||||
// 图片裁剪
|
||||
$cutMode = ($width > 0 && $height > 0) ? 'cover' : 'percentage';
|
||||
$cutMode = $param['scale'][2] ?? $cutMode;
|
||||
Image::thumbImage($array['file'], $array['file'], $width, $height, 90, $cutMode);
|
||||
Image::thumbImage($array['file'], $array['file'], $width, $height, true, $cutMode);
|
||||
// 更新图片尺寸
|
||||
$paramet = getimagesize($array['file']);
|
||||
$array['width'] = $paramet[0];
|
||||
@@ -1940,18 +1978,17 @@ class Base
|
||||
// 重命名
|
||||
if ($scaleName) {
|
||||
$scaleName = str_replace(['{WIDTH}', '{HEIGHT}'], [$array['width'], $array['height']], $scaleName);
|
||||
if (rename($array['file'], Base::rightDelete($array['file'], $fileName) . $scaleName)) {
|
||||
$array['file'] = Base::rightDelete($array['file'], $fileName) . $scaleName;
|
||||
$array['path'] = Base::rightDelete($array['path'], $fileName) . $scaleName;
|
||||
$array['url'] = Base::rightDelete($array['url'], $fileName) . $scaleName;
|
||||
if (rename($array['file'], Base::rightDelete($array['file'], $saveName) . $scaleName)) {
|
||||
$array['file'] = Base::rightDelete($array['file'], $saveName) . $scaleName;
|
||||
$array['path'] = Base::rightDelete($array['path'], $saveName) . $scaleName;
|
||||
$array['url'] = Base::rightDelete($array['url'], $saveName) . $scaleName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 压缩图片
|
||||
$quality = intval($param['quality']);
|
||||
if ($quality > 0) {
|
||||
Image::compressImage($array['file'], $quality);
|
||||
if ($param['quality']) {
|
||||
Image::compressImage($array['file'], $param['quality']);
|
||||
$array['size'] = Base::twoFloat(filesize($array['file']) / 1024, true);
|
||||
}
|
||||
//生成缩略图
|
||||
@@ -1978,6 +2015,7 @@ class Base
|
||||
file=>Request::file,
|
||||
path=>文件路径,
|
||||
fileName=>文件名称,
|
||||
saveName=>保存文件名称,
|
||||
scale=>[压缩原图宽,高, 压缩方式],
|
||||
size=>限制大小KB,
|
||||
autoThumb=>false不要自动生成缩略图,
|
||||
@@ -2018,7 +2056,7 @@ class Base
|
||||
$type = ['mp3', 'wma', 'wav', 'amr'];
|
||||
break;
|
||||
case 'excel':
|
||||
$type = ['xls', 'xlsx'];
|
||||
$type = ['xls', 'xlsx', 'xlsm', 'xlt', 'xltx', 'ods', 'ots', 'csv', 'tsv'];
|
||||
break;
|
||||
case 'app':
|
||||
$type = ['apk'];
|
||||
@@ -2069,10 +2107,10 @@ class Base
|
||||
}
|
||||
}
|
||||
$scaleName = "";
|
||||
if ($param['fileName'] === true) {
|
||||
$fileName = $file->getClientOriginalName();
|
||||
} elseif ($param['fileName']) {
|
||||
$fileName = basename($param['fileName']);
|
||||
if ($param['saveName'] === true) {
|
||||
$saveName = $file->getClientOriginalName();
|
||||
} elseif ($param['saveName']) {
|
||||
$saveName = basename($param['saveName']);
|
||||
} else {
|
||||
if ($param['scale'] && is_array($param['scale'])) {
|
||||
list($width, $height) = $param['scale'];
|
||||
@@ -2083,19 +2121,19 @@ class Base
|
||||
}
|
||||
}
|
||||
}
|
||||
$fileName = md5_file($file);
|
||||
$saveName = md5_file($file);
|
||||
$scaleName = md5_file($file) . $scaleName;
|
||||
if ($extension) {
|
||||
$fileName = $fileName . '.' . $extension;
|
||||
$saveName = $saveName . '.' . $extension;
|
||||
$scaleName = $scaleName . '.' . $extension;
|
||||
}
|
||||
}
|
||||
//
|
||||
$file->move(public_path($param['path']), $fileName);
|
||||
$file->move(public_path($param['path']), $saveName);
|
||||
//
|
||||
$path = $param['path'] . $fileName;
|
||||
$path = $param['path'] . $saveName;
|
||||
$array = [
|
||||
"name" => $file->getClientOriginalName(), //原文件名
|
||||
"name" => $param['fileName'] ?: $file->getClientOriginalName(), //原文件名
|
||||
"size" => Base::twoFloat($fileSize / 1024, true), //大小KB
|
||||
"file" => public_path($path), //文件的完整路径 "D:\www....KzZ.jpg"
|
||||
"path" => $path, //相对路径 "uploads/pic....KzZ.jpg"
|
||||
@@ -2177,7 +2215,7 @@ class Base
|
||||
// 图片裁剪
|
||||
$cutMode = ($width > 0 && $height > 0) ? 'cover' : 'percentage';
|
||||
$cutMode = $param['scale'][2] ?? $cutMode;
|
||||
Image::thumbImage($array['file'], $array['file'], $width, $height, 90, $cutMode);
|
||||
Image::thumbImage($array['file'], $array['file'], $width, $height, true, $cutMode);
|
||||
// 更新图片尺寸
|
||||
$paramet = getimagesize($array['file']);
|
||||
$array['width'] = $paramet[0];
|
||||
@@ -2185,18 +2223,17 @@ class Base
|
||||
// 重命名
|
||||
if ($scaleName) {
|
||||
$scaleName = str_replace(['{WIDTH}', '{HEIGHT}'], [$array['width'], $array['height']], $scaleName);
|
||||
if (rename($array['file'], Base::rightDelete($array['file'], $fileName) . $scaleName)) {
|
||||
$array['file'] = Base::rightDelete($array['file'], $fileName) . $scaleName;
|
||||
$array['path'] = Base::rightDelete($array['path'], $fileName) . $scaleName;
|
||||
$array['url'] = Base::rightDelete($array['url'], $fileName) . $scaleName;
|
||||
if (rename($array['file'], Base::rightDelete($array['file'], $saveName) . $scaleName)) {
|
||||
$array['file'] = Base::rightDelete($array['file'], $saveName) . $scaleName;
|
||||
$array['path'] = Base::rightDelete($array['path'], $saveName) . $scaleName;
|
||||
$array['url'] = Base::rightDelete($array['url'], $saveName) . $scaleName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 压缩图片
|
||||
$quality = intval($param['quality']);
|
||||
if ($quality > 0) {
|
||||
Image::compressImage($array['file'], $quality);
|
||||
if ($param['quality']) {
|
||||
Image::compressImage($array['file'], $param['quality']);
|
||||
$array['size'] = Base::twoFloat(filesize($array['file']) / 1024, true);
|
||||
}
|
||||
// 生成缩略图
|
||||
@@ -2760,12 +2797,12 @@ class Base
|
||||
}
|
||||
|
||||
/**
|
||||
* BinaryFileResponse 下载文件
|
||||
* DownloadFileResponse 下载文件
|
||||
* @param File|\SplFileInfo|string $file 文件对象或路径
|
||||
* @param string|null $name 下载文件名
|
||||
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
|
||||
* @return StreamedResponse
|
||||
*/
|
||||
public static function BinaryFileResponse($file, $name = null)
|
||||
public static function DownloadFileResponse($file, $name = null)
|
||||
{
|
||||
try {
|
||||
// 处理文件对象
|
||||
@@ -2782,6 +2819,12 @@ class Base
|
||||
throw new FileException('File must be readable and exist.');
|
||||
}
|
||||
|
||||
// 获取文件信息
|
||||
$size = $file->getSize();
|
||||
if ($size === false || $size < 0) {
|
||||
throw new FileException('Unable to determine file size.');
|
||||
}
|
||||
|
||||
// 处理文件名
|
||||
if (empty($name)) {
|
||||
$name = basename($file->getPathname());
|
||||
@@ -2793,34 +2836,98 @@ class Base
|
||||
$name = Base::cutStr($name, 180);
|
||||
$name = str_replace(['"', '<', '>', '|', '/', '\\', '?', ':'], '', $name);
|
||||
|
||||
// IE 浏览器特殊处理
|
||||
$encodedName = $name;
|
||||
if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/MSIE|Internet Explorer|Trident/i", $_SERVER['HTTP_USER_AGENT'])) {
|
||||
$encodedName = rawurlencode($name);
|
||||
// 获取MIME类型
|
||||
$mimeType = $file->getMimeType();
|
||||
if (empty($mimeType)) {
|
||||
$mimeType = 'application/octet-stream';
|
||||
}
|
||||
|
||||
// 创建响应对象
|
||||
return new \Symfony\Component\HttpFoundation\BinaryFileResponse($file->getPathname(), 200, [
|
||||
'Content-Type' => $file->getMimeType() ?: 'application/octet-stream',
|
||||
// 处理 Range 请求
|
||||
$start = 0;
|
||||
$end = $size - 1;
|
||||
$length = $size;
|
||||
$isRangeRequest = false;
|
||||
|
||||
if (isset($_SERVER['HTTP_RANGE'])) {
|
||||
$range = str_replace('bytes=', '', $_SERVER['HTTP_RANGE']);
|
||||
if (preg_match('/^(\d+)-(\d*)$/', $range, $matches)) {
|
||||
$start = intval($matches[1]);
|
||||
$end = !empty($matches[2]) ? intval($matches[2]) : $size - 1;
|
||||
|
||||
// 验证范围的有效性
|
||||
if ($start >= 0 && $end < $size && $start <= $end) {
|
||||
$length = $end - $start + 1;
|
||||
$isRangeRequest = true;
|
||||
} else {
|
||||
$start = 0;
|
||||
$end = $size - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置基本响应头
|
||||
$headers = [
|
||||
'Content-Type' => $mimeType,
|
||||
'Content-Disposition' => sprintf(
|
||||
'attachment; filename="%s"; filename*=UTF-8\'\'%s',
|
||||
$encodedName,
|
||||
$name,
|
||||
rawurlencode($name)
|
||||
),
|
||||
// 添加缓存控制和安全相关的头
|
||||
'Cache-Control' => 'private, no-transform, no-store, must-revalidate',
|
||||
'Pragma' => 'public',
|
||||
'Expires' => '0',
|
||||
'Accept-Ranges' => 'bytes', // 支持断点续传
|
||||
'X-Content-Type-Options' => 'nosniff', // 安全相关
|
||||
]);
|
||||
'Accept-Ranges' => 'bytes',
|
||||
'Cache-Control' => 'private, no-transform, no-store, must-revalidate, max-age=0',
|
||||
'Content-Length' => $length,
|
||||
'Last-Modified' => gmdate('D, d M Y H:i:s', $file->getMTime()) . ' GMT',
|
||||
'ETag' => sprintf('"%s"', md5_file($file->getPathname()))
|
||||
];
|
||||
|
||||
if ($isRangeRequest) {
|
||||
$headers['Content-Range'] = "bytes {$start}-{$end}/{$size}";
|
||||
$statusCode = 206;
|
||||
} else {
|
||||
$statusCode = 200;
|
||||
}
|
||||
|
||||
// 创建流式响应
|
||||
return new StreamedResponse(
|
||||
function () use ($file, $start, $length) {
|
||||
$handle = fopen($file->getPathname(), 'rb');
|
||||
if ($handle === false) {
|
||||
throw new FileException('Cannot open file for reading');
|
||||
}
|
||||
|
||||
if (fseek($handle, $start) === -1) {
|
||||
fclose($handle);
|
||||
throw new FileException('Cannot seek to position ' . $start);
|
||||
}
|
||||
|
||||
$remaining = $length;
|
||||
$bufferSize = 8192; // 8KB chunks
|
||||
|
||||
while ($remaining > 0 && !feof($handle)) {
|
||||
$readSize = min($bufferSize, $remaining);
|
||||
$buffer = fread($handle, $readSize);
|
||||
if ($buffer === false) {
|
||||
break;
|
||||
}
|
||||
echo $buffer;
|
||||
flush();
|
||||
$remaining -= strlen($buffer);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
},
|
||||
$statusCode,
|
||||
$headers
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('File download failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'file' => $file->getPathname() ?? null,
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'file' => $file ?? null,
|
||||
'name' => $name ?? null,
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null, // 添加更多调试信息
|
||||
'ip' => request()->ip()
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
|
||||
'ip' => request()->ip(),
|
||||
'range' => $_SERVER['HTTP_RANGE'] ?? null
|
||||
]);
|
||||
abort(403, 'File download failed');
|
||||
}
|
||||
@@ -2830,14 +2937,11 @@ class Base
|
||||
* 保存图片到文件(同时压缩)
|
||||
* @param $path
|
||||
* @param $content
|
||||
* @param int $quality 压缩图片质量(默认:0不压缩)
|
||||
* @return bool
|
||||
*/
|
||||
public static function saveContentImage($path, $content, int $quality = 0) {
|
||||
public static function saveContentImage($path, $content) {
|
||||
if (file_put_contents($path, $content)) {
|
||||
if ($quality > 0) {
|
||||
Image::compressImage($path, $quality);
|
||||
}
|
||||
Image::compressImage($path);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -2889,11 +2993,26 @@ class Base
|
||||
*/
|
||||
public static function markdown2html($markdown)
|
||||
{
|
||||
$converter = new CommonMarkConverter();
|
||||
try {
|
||||
$converter = new CommonMarkConverter();
|
||||
return $converter->convert($markdown);
|
||||
} catch (CommonMarkException $e) {
|
||||
} catch (\League\CommonMark\Exception\CommonMarkException $e) {
|
||||
return $markdown;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* html 转 MD(markdown)
|
||||
* @param $html
|
||||
* @return mixed|string
|
||||
*/
|
||||
public static function html2markdown($html)
|
||||
{
|
||||
try {
|
||||
$converter = new HtmlConverter();
|
||||
return $converter->convert($html);
|
||||
} catch (\Exception) {
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
308
app/Module/ElasticSearch/ElasticSearchBase.php
Normal file
308
app/Module/ElasticSearch/ElasticSearchBase.php
Normal file
@@ -0,0 +1,308 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module\ElasticSearch;
|
||||
|
||||
use Elastic\Elasticsearch\ClientBuilder;
|
||||
use Elastic\Elasticsearch\Exception\MissingParameterException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Elasticsearch基础类
|
||||
*
|
||||
* Class ElasticSearchBase
|
||||
* @package App\Module\ElasticSearch
|
||||
*/
|
||||
class ElasticSearchBase
|
||||
{
|
||||
/**
|
||||
* Elasticsearch客户端实例
|
||||
*
|
||||
* @var \Elastic\Elasticsearch\Client
|
||||
*/
|
||||
protected $client;
|
||||
|
||||
/**
|
||||
* 当前操作的索引名称
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $index;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param null $index 默认索引名称
|
||||
* @throws \Elastic\Elasticsearch\Exception\ConfigException
|
||||
*/
|
||||
public function __construct($index = null)
|
||||
{
|
||||
$host = env('ELASTICSEARCH_HOST', 'es');
|
||||
$port = env('ELASTICSEARCH_PORT', '9200');
|
||||
$scheme = env('ELASTICSEARCH_SCHEME', 'http');
|
||||
$user = env('ELASTICSEARCH_USER', '');
|
||||
$pass = env('ELASTICSEARCH_PASS', '');
|
||||
$verifi = env('ELASTICSEARCH_VERIFI', false);
|
||||
$ca = env('ELASTICSEARCH_CA', '');
|
||||
$key = env('ELASTICSEARCH_KEY', '');
|
||||
$cert = env('ELASTICSEARCH_CERT', '');
|
||||
// 为8.x版本客户端配置连接
|
||||
$config = [
|
||||
'hosts' => ["{$scheme}://{$host}:{$port}"]
|
||||
];
|
||||
|
||||
// 如果设置了用户名和密码
|
||||
if (!empty($user)) {
|
||||
$config['basicAuthentication'] = [$user, $pass];
|
||||
}
|
||||
|
||||
$config['SSLVerification'] = $verifi;
|
||||
if ($verifi) {
|
||||
$config['SSLCert'] = $cert;
|
||||
$config['CABundle'] = $ca;
|
||||
$config['SSLKey'] = $key;
|
||||
}
|
||||
// 8.x版本使用ClientBuilder::fromConfig创建客户端
|
||||
$this->client = ClientBuilder::fromConfig($config);
|
||||
|
||||
if ($index) {
|
||||
$this->index = $index;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置索引名称
|
||||
*
|
||||
* @param string $index
|
||||
* @return $this
|
||||
*/
|
||||
public function setIndex($index)
|
||||
{
|
||||
$this->index = $index;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查索引是否存在
|
||||
*
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function indexExists()
|
||||
{
|
||||
$params = ['index' => $this->index];
|
||||
return $this->client->indices()->exists($params)->asBool();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建索引
|
||||
*
|
||||
* @param array $settings 索引设置
|
||||
* @param array $mappings 字段映射
|
||||
* @return array
|
||||
*/
|
||||
public function createIndex($settings = [], $mappings = [])
|
||||
{
|
||||
$params = [
|
||||
'index' => $this->index
|
||||
];
|
||||
|
||||
$body = [];
|
||||
if (!empty($settings)) {
|
||||
$body['settings'] = $settings;
|
||||
}
|
||||
|
||||
if (!empty($mappings)) {
|
||||
$body['mappings'] = $mappings;
|
||||
}
|
||||
|
||||
if (!empty($body)) {
|
||||
$params['body'] = $body;
|
||||
}
|
||||
|
||||
try {
|
||||
// 在8.x中,索引操作位于indices()命名空间
|
||||
return $this->client->indices()->create($params)->asArray();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('创建Elasticsearch索引失败: ' . $e->getMessage());
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除索引
|
||||
* @return array
|
||||
*/
|
||||
public function deleteIndex()
|
||||
{
|
||||
try {
|
||||
$params = ['index' => $this->index];
|
||||
return $this->client->indices()->delete($params)->asArray();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('删除Elasticsearch索引失败: ' . $e->getMessage());
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量操作(批量添加/更新/删除文档)
|
||||
*
|
||||
* @param array $operations 批量操作的数据
|
||||
* @return array
|
||||
*/
|
||||
public function bulk($operations)
|
||||
{
|
||||
try {
|
||||
// 在8.x中,批量操作API签名相同,但内部实现有所变化
|
||||
return $this->client->bulk($operations)->asArray();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('批量操作失败: ' . $e->getMessage());
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 索引单个文档
|
||||
*
|
||||
* @param array $document 文档数据
|
||||
* @param string $id 文档ID
|
||||
* @param string|null $routing 路由值,用于父子文档
|
||||
* @return array
|
||||
*/
|
||||
public function indexDocument($document, $id, $routing = null)
|
||||
{
|
||||
$params = [
|
||||
'index' => $this->index,
|
||||
'id' => $id,
|
||||
'body' => $document
|
||||
];
|
||||
|
||||
if ($routing) {
|
||||
$params['routing'] = $routing;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->client->index($params)->asArray();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('索引文档失败: ' . $e->getMessage());
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文档
|
||||
*
|
||||
* @param string $id 文档ID
|
||||
* @param string|null $routing 路由值,用于父子文档
|
||||
* @return array
|
||||
*/
|
||||
public function deleteDocument($id, $routing = null)
|
||||
{
|
||||
$params = [
|
||||
'index' => $this->index,
|
||||
'id' => $id
|
||||
];
|
||||
|
||||
if ($routing) {
|
||||
$params['routing'] = $routing;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->client->delete($params)->asArray();
|
||||
} catch (MissingParameterException $e) {
|
||||
// 文档不存在时返回成功
|
||||
return ['result' => 'not_found', 'error' => $e->getMessage()];
|
||||
} catch (\Exception $e) {
|
||||
Log::error('删除文档失败: ' . $e->getMessage());
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新索引
|
||||
* @return array
|
||||
*/
|
||||
public function refreshIndex()
|
||||
{
|
||||
$params = [
|
||||
'index' => $this->index
|
||||
];
|
||||
|
||||
try {
|
||||
return $this->client->indices()->refresh($params)->asArray();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('刷新索引失败: ' . $e->getMessage());
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查索引映射
|
||||
* @return array
|
||||
*/
|
||||
public function checkIndexMapping()
|
||||
{
|
||||
try {
|
||||
return $this->client->indices()->getMapping(['index' => $this->index])->asArray();
|
||||
} catch (\Exception $e) {
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用搜索方法
|
||||
*
|
||||
* @param array $query 搜索查询
|
||||
* @param int $from 起始位置
|
||||
* @param int $size 返回结果数量
|
||||
* @param array $sort 排序规则
|
||||
* @return array
|
||||
*/
|
||||
public function search($query, $from = 0, $size = 10, $sort = [])
|
||||
{
|
||||
$params = [
|
||||
'index' => $this->index,
|
||||
'body' => [
|
||||
'query' => $query,
|
||||
'from' => $from,
|
||||
'size' => $size
|
||||
]
|
||||
];
|
||||
|
||||
if (!empty($sort)) {
|
||||
$params['body']['sort'] = $sort;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->client->search($params)->asArray();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('搜索失败: ' . $e->getMessage());
|
||||
return ['error' => $e->getMessage(), 'hits' => ['total' => ['value' => 0], 'hits' => []]];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 索引名称
|
||||
*/
|
||||
const indexName = 'default';
|
||||
|
||||
/**
|
||||
* 获取索引名称
|
||||
* @param string $index 索引名称
|
||||
* @param string|null $prefix 索引前缀
|
||||
* @param string|null $subfix 索引后缀
|
||||
* @return string
|
||||
*/
|
||||
public static function indexName($index = '', $prefix = '', $subfix = '')
|
||||
{
|
||||
$index = $index ?: static::indexName;
|
||||
$prefix = $prefix ?: env('ES_INDEX_PREFIX', '');
|
||||
$subfix = $subfix ?: env('ES_INDEX_SUFFIX', '');
|
||||
if ($prefix) {
|
||||
$index = rtrim($prefix, '_') . '_' . $index;
|
||||
}
|
||||
if ($subfix) {
|
||||
$index = $index . '_' . ltrim($subfix, '_');
|
||||
}
|
||||
return $index;
|
||||
}
|
||||
}
|
||||
204
app/Module/ElasticSearch/ElasticSearchKeyValue.php
Normal file
204
app/Module/ElasticSearch/ElasticSearchKeyValue.php
Normal file
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module\ElasticSearch;
|
||||
|
||||
use App\Module\Base;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Elasticsearch键值存储
|
||||
*
|
||||
* Class ElasticSearchKeyValue
|
||||
* @package App\Module\ElasticSearch
|
||||
*/
|
||||
class ElasticSearchKeyValue extends ElasticSearchBase
|
||||
{
|
||||
const indexName = 'key_value_store';
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @return ElasticSearchBase
|
||||
* @throws \Elastic\Elasticsearch\Exception\ConfigException
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
return parent::__construct(self::indexName());
|
||||
}
|
||||
|
||||
/** ******************************************************************************************************** */
|
||||
/** *********************************** 键值存储方法 ******************************************************** */
|
||||
/** ******************************************************************************************************** */
|
||||
|
||||
/**
|
||||
* 创建键值存储索引
|
||||
* @return array
|
||||
*/
|
||||
public static function generateIndex()
|
||||
{
|
||||
try {
|
||||
$es = new self();
|
||||
|
||||
// 如果索引已存在,则直接返回
|
||||
if ($es->indexExists()) {
|
||||
return ['acknowledged' => true, 'message' => '索引已存在'];
|
||||
}
|
||||
|
||||
// 定义映射
|
||||
$mappings = [
|
||||
'properties' => [
|
||||
'key' => ['type' => 'keyword'],
|
||||
'value' => ['type' => 'text', 'fields' => ['keyword' => ['type' => 'keyword']]],
|
||||
'created_at' => ['type' => 'integer'],
|
||||
'updated_at' => ['type' => 'integer']
|
||||
]
|
||||
];
|
||||
|
||||
// 索引设置
|
||||
$settings = [
|
||||
'number_of_shards' => 1,
|
||||
'number_of_replicas' => 1,
|
||||
'refresh_interval' => '1s'
|
||||
];
|
||||
|
||||
return $es->createIndex($settings, $mappings);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('创建键值存储索引失败: ' . $e->getMessage());
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存键值对
|
||||
* @param string $key 键名
|
||||
* @param mixed $value 键值
|
||||
* @param string $namespace 命名空间,用于区分不同的键值存储场景
|
||||
* @return array
|
||||
*/
|
||||
public static function save($key, $value, $namespace = 'default')
|
||||
{
|
||||
try {
|
||||
// 确保索引存在
|
||||
self::generateIndex();
|
||||
|
||||
$es = new self();
|
||||
|
||||
// 生成文档ID
|
||||
$docId = "{$namespace}:{$key}";
|
||||
|
||||
// 准备文档数据
|
||||
$document = [
|
||||
'key' => $key,
|
||||
'value' => is_array($value) ? json_encode($value, JSON_UNESCAPED_UNICODE) : $value,
|
||||
'namespace' => $namespace,
|
||||
'created_at' => time(),
|
||||
'updated_at' => time()
|
||||
];
|
||||
|
||||
// 索引文档
|
||||
$result = $es->indexDocument($document, $docId);
|
||||
|
||||
// 刷新索引以确保立即可见
|
||||
$es->refreshIndex();
|
||||
|
||||
return $result;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('保存键值对失败: ' . $e->getMessage());
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取键值
|
||||
* @param string $key 键名
|
||||
* @param mixed $default 默认值,当键不存在时返回
|
||||
* @param string $namespace 命名空间,用于区分不同的键值存储场景
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get($key, $default = null, $namespace = 'default')
|
||||
{
|
||||
try {
|
||||
$es = new self();
|
||||
|
||||
// 如果索引不存在,直接返回默认值
|
||||
if (!$es->indexExists()) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
// 生成文档ID
|
||||
$docId = "{$namespace}:{$key}";
|
||||
|
||||
// 查询参数
|
||||
$params = [
|
||||
'index' => self::indexName(),
|
||||
'id' => $docId
|
||||
];
|
||||
|
||||
try {
|
||||
// 获取文档
|
||||
$response = $es->client->get($params)->asArray();
|
||||
|
||||
// 获取值
|
||||
$value = $response['_source']['value'] ?? $default;
|
||||
|
||||
// 如果值是JSON字符串,尝试解码
|
||||
if (is_string($value) && $decoded = json_decode($value, true)) {
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
} catch (\Exception $e) {
|
||||
// 文档不存在或其他错误,返回默认值
|
||||
return $default;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('获取键值对失败: ' . $e->getMessage());
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取键值,返回数组
|
||||
* @param string $key 键名
|
||||
* @param array $default 默认值,当键不存在时返回
|
||||
* @param string $namespace 命名空间,用于区分不同的键值存储场景
|
||||
* @return array
|
||||
*/
|
||||
public static function getArray($key, $default = [], $namespace = 'default')
|
||||
{
|
||||
return Base::string2array(self::get($key, $default, $namespace));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除键值对
|
||||
* @param string $key 键名
|
||||
* @param string $namespace 命名空间
|
||||
* @return array
|
||||
*/
|
||||
public static function delete($key, $namespace = 'default')
|
||||
{
|
||||
try {
|
||||
$es = new self();
|
||||
|
||||
// 如果索引不存在,直接返回成功
|
||||
if (!$es->indexExists()) {
|
||||
return ['result' => 'not_found'];
|
||||
}
|
||||
|
||||
// 生成文档ID
|
||||
$docId = "{$namespace}:{$key}";
|
||||
|
||||
// 删除文档
|
||||
$result = $es->deleteDocument($docId);
|
||||
|
||||
// 刷新索引以确保立即生效
|
||||
$es->refreshIndex();
|
||||
|
||||
return $result;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('删除键值对失败: ' . $e->getMessage());
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
}
|
||||
375
app/Module/ElasticSearch/ElasticSearchUserMsg.php
Normal file
375
app/Module/ElasticSearch/ElasticSearchUserMsg.php
Normal file
@@ -0,0 +1,375 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module\ElasticSearch;
|
||||
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Models\WebSocketDialogUser;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* 对话系统消息索引
|
||||
*
|
||||
* Class ElasticSearchUserMsg
|
||||
* @package App\Module\ElasticSearch
|
||||
*/
|
||||
class ElasticSearchUserMsg extends ElasticSearchBase
|
||||
{
|
||||
const indexName = 'dialog_user_msg';
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @return ElasticSearchBase
|
||||
* @throws \Elastic\Elasticsearch\Exception\ConfigException
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
return parent::__construct(self::indexName());
|
||||
}
|
||||
|
||||
/** ******************************************************************************************************** */
|
||||
/** *********************************************** 基础 ************************************************** */
|
||||
/** ******************************************************************************************************** */
|
||||
|
||||
/**
|
||||
* 创建聊天系统索引 - 使用父子关系
|
||||
* @return array
|
||||
*/
|
||||
public static function generateIndex()
|
||||
{
|
||||
// 定义映射
|
||||
$mappings = [
|
||||
'properties' => [
|
||||
// 共用字段
|
||||
'dialog_id' => ['type' => 'keyword'],
|
||||
'created_at' => ['type' => 'date'],
|
||||
'updated_at' => ['type' => 'date'],
|
||||
|
||||
// dialog_users 字段
|
||||
'userid' => ['type' => 'keyword'],
|
||||
'top_at' => ['type' => 'date'],
|
||||
'last_at' => ['type' => 'date'],
|
||||
'mark_unread' => ['type' => 'integer'],
|
||||
'silence' => ['type' => 'integer'],
|
||||
'hide' => ['type' => 'integer'],
|
||||
'color' => ['type' => 'keyword'],
|
||||
|
||||
// dialog_msgs 字段
|
||||
'msg_id' => ['type' => 'keyword'],
|
||||
'sender_userid' => ['type' => 'keyword'],
|
||||
'msg_type' => ['type' => 'keyword'],
|
||||
'key' => ['type' => 'text'],
|
||||
'bot' => ['type' => 'integer'],
|
||||
|
||||
// Join字段定义父子关系
|
||||
'relationship' => [
|
||||
'type' => 'join',
|
||||
'relations' => [
|
||||
'dialog_user' => 'dialog_msg' // dialog_user是父文档,dialog_msg是子文档
|
||||
]
|
||||
],
|
||||
]
|
||||
];
|
||||
|
||||
// 索引设置
|
||||
$settings = [
|
||||
'number_of_shards' => 5,
|
||||
'number_of_replicas' => 1,
|
||||
'refresh_interval' => '5s'
|
||||
];
|
||||
|
||||
try {
|
||||
$es = new self();
|
||||
return $es->createIndex($settings, $mappings);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('创建聊天系统索引失败: ' . $e->getMessage());
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建对话系统特定的搜索 - 根据用户ID和消息关键词搜索会话
|
||||
* @param string $userid 用户ID
|
||||
* @param string $keyword 消息关键词
|
||||
* @param int $size 返回结果数量
|
||||
* @return array
|
||||
*/
|
||||
public static function searchByKeyword($userid, $keyword, $size = 20)
|
||||
{
|
||||
// 注意这里的类型名称要与创建索引时的一致
|
||||
$query = [
|
||||
'bool' => [
|
||||
'must' => [
|
||||
[
|
||||
'term' => [
|
||||
'userid' => $userid
|
||||
]
|
||||
],
|
||||
[
|
||||
'has_child' => [
|
||||
'type' => 'dialog_msg',
|
||||
'query' => [
|
||||
'bool' => [
|
||||
'must' => [
|
||||
[
|
||||
'match_phrase' => [
|
||||
'key' => $keyword
|
||||
]
|
||||
],
|
||||
[
|
||||
'term' => [
|
||||
'bot' => 0
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'inner_hits' => [
|
||||
'size' => 1,
|
||||
'sort' => [
|
||||
'msg_id' => 'desc'
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
// 结果集合
|
||||
$searchMap = [];
|
||||
|
||||
try {
|
||||
// 开始搜索
|
||||
$es = new self();
|
||||
$results = $es->search($query, 0, $size, ['last_at' => 'desc']);
|
||||
|
||||
// 处理搜索结果
|
||||
$hits = $results['hits']['hits'] ?? [];
|
||||
|
||||
foreach ($hits as $hit) {
|
||||
if (isset($hit['inner_hits']['dialog_msg']['hits']['hits'][0])) {
|
||||
$msgHit = $hit['inner_hits']['dialog_msg']['hits']['hits'][0];
|
||||
$source = $hit['_source'];
|
||||
$msgSource = $msgHit['_source'];
|
||||
|
||||
$searchMap[] = [
|
||||
'id' => $source['dialog_id'],
|
||||
'top_at' => $source['top_at'],
|
||||
'last_at' => $source['last_at'],
|
||||
'mark_unread' => $source['mark_unread'],
|
||||
'silence' => $source['silence'],
|
||||
'hide' => $source['hide'],
|
||||
'color' => $source['color'],
|
||||
'user_at' => $source['updated_at'],
|
||||
'search_msg_id' => $msgSource['msg_id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('searchByKeyword: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// 返回搜索结果
|
||||
return $searchMap;
|
||||
}
|
||||
|
||||
/** ******************************************************************************************************** */
|
||||
/** *********************************************** 用户 ************************************************** */
|
||||
/** ******************************************************************************************************** */
|
||||
|
||||
/**
|
||||
* 会话用户 - 生成文档ID
|
||||
* @param WebSocketDialogUser $dialogUser
|
||||
* @return string
|
||||
*/
|
||||
public static function generateUserDicId(WebSocketDialogUser $dialogUser)
|
||||
{
|
||||
return "user_{$dialogUser->userid}_dialog_{$dialogUser->dialog_id}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话用户 - 生成文档格式
|
||||
* @param WebSocketDialogUser $dialogUser
|
||||
* @return array
|
||||
*/
|
||||
public static function generateUserFormat(WebSocketDialogUser $dialogUser)
|
||||
{
|
||||
return [
|
||||
'dialog_id' => $dialogUser->dialog_id,
|
||||
'created_at' => $dialogUser->created_at,
|
||||
'updated_at' => $dialogUser->updated_at,
|
||||
|
||||
'userid' => $dialogUser->userid,
|
||||
'top_at' => $dialogUser->top_at,
|
||||
'last_at' => $dialogUser->last_at,
|
||||
'mark_unread' => $dialogUser->mark_unread ? 1 : 0,
|
||||
'silence' => $dialogUser->silence ? 1 : 0,
|
||||
'hide' => $dialogUser->hide ? 1 : 0,
|
||||
'color' => $dialogUser->color,
|
||||
|
||||
'relationship' => [
|
||||
'name' => 'dialog_user'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话用户 - 同步到Elasticsearch
|
||||
* @param WebSocketDialogUser $dialogUser
|
||||
* @return void
|
||||
*/
|
||||
public static function syncUser(WebSocketDialogUser $dialogUser)
|
||||
{
|
||||
try {
|
||||
$es = new self();
|
||||
$es->indexDocument(self::generateUserFormat($dialogUser), self::generateUserDicId($dialogUser));
|
||||
} catch (\Exception $e) {
|
||||
Log::error('syncUser: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话用户 - 从Elasticsearch删除
|
||||
*/
|
||||
public static function deleteUser(WebSocketDialogUser $dialogUser)
|
||||
{
|
||||
try {
|
||||
$es = new self();
|
||||
|
||||
$docId = "user_{$dialogUser->userid}_dialog_{$dialogUser->dialog_id}";
|
||||
|
||||
// 删除用户-会话文档
|
||||
$es->deleteDocument($docId);
|
||||
|
||||
// 注意:这里可能还需要删除所有关联的消息文档
|
||||
// 但由于父子关系,可以通过查询找到所有子文档并删除
|
||||
// 这里为简化,可以选择在后台任务中处理,或者直接依赖ES的级联删除功能
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('deleteUser: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** ******************************************************************************************************** */
|
||||
/** *********************************************** 消息 ************************************************** */
|
||||
/** ******************************************************************************************************** */
|
||||
|
||||
/**
|
||||
* 会话消息 - 生成父文档ID
|
||||
* @param WebSocketDialogMsg $dialogMsg
|
||||
* @param $userid
|
||||
* @return string
|
||||
*/
|
||||
public static function generateMsgParentId(WebSocketDialogMsg $dialogMsg, $userid)
|
||||
{
|
||||
return "user_{$userid}_dialog_{$dialogMsg->dialog_id}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话消息 - 生成文档ID
|
||||
* @param WebSocketDialogMsg $dialogMsg
|
||||
* @param $userid
|
||||
* @return string
|
||||
*/
|
||||
public static function generateMsgDicId(WebSocketDialogMsg $dialogMsg, $userid)
|
||||
{
|
||||
return "msg_{$dialogMsg->id}_user_{$userid}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话消息 - 生成文档格式
|
||||
* @param WebSocketDialogMsg $dialogMsg
|
||||
* @param $userid
|
||||
* @return array
|
||||
*/
|
||||
public static function generateMsgFormat(WebSocketDialogMsg $dialogMsg, $userid)
|
||||
{
|
||||
return [
|
||||
'dialog_id' => $dialogMsg->dialog_id,
|
||||
'created_at' => $dialogMsg->created_at,
|
||||
'updated_at' => $dialogMsg->updated_at,
|
||||
|
||||
'msg_id' => $dialogMsg->id,
|
||||
'sender_userid' => $dialogMsg->userid,
|
||||
'msg_type' => $dialogMsg->type,
|
||||
'key' => $dialogMsg->key,
|
||||
'bot' => $dialogMsg->bot ? 1 : 0,
|
||||
|
||||
'relationship' => [
|
||||
'name' => 'dialog_msg',
|
||||
'parent' => self::generateMsgParentId($dialogMsg, $userid)
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话消息 - 同步到Elasticsearch
|
||||
*/
|
||||
public static function syncMsg(WebSocketDialogMsg $dialogMsg)
|
||||
{
|
||||
try {
|
||||
$es = new self();
|
||||
|
||||
// 获取此会话的所有用户
|
||||
$dialogUsers = WebSocketDialogUser::whereDialogId($dialogMsg->dialog_id)->get();
|
||||
|
||||
if ($dialogUsers->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = ['body' => []];
|
||||
|
||||
foreach ($dialogUsers as $dialogUser) {
|
||||
$params['body'][] = [
|
||||
'index' => [
|
||||
'_index' => self::indexName(),
|
||||
'_id' => self::generateMsgDicId($dialogMsg, $dialogUser->userid),
|
||||
'routing' => self::generateMsgParentId($dialogMsg, $dialogUser->userid)
|
||||
]
|
||||
];
|
||||
$params['body'][] = self::generateMsgFormat($dialogMsg, $dialogUser->userid);
|
||||
}
|
||||
|
||||
if (!empty($params['body'])) {
|
||||
$es->bulk($params);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('syncMsg: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话消息 - 从Elasticsearch删除
|
||||
*/
|
||||
public static function deleteMsg(WebSocketDialogMsg $dialogMsg)
|
||||
{
|
||||
try {
|
||||
$es = new self();
|
||||
|
||||
// 获取此会话的所有用户
|
||||
$dialogUsers = WebSocketDialogUser::whereDialogId($dialogMsg->dialog_id)->get();
|
||||
|
||||
if ($dialogUsers->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = ['body' => []];
|
||||
|
||||
foreach ($dialogUsers as $dialogUser) {
|
||||
$params['body'][] = [
|
||||
'delete' => [
|
||||
'_index' => self::indexName(),
|
||||
'_id' => self::generateMsgDicId($dialogMsg, $dialogUser->userid),
|
||||
'routing' => self::generateMsgParentId($dialogMsg, $dialogUser->userid)
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($params['body'])) {
|
||||
$es->bulk($params);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('deleteMsg: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,10 @@ class Extranet
|
||||
/**
|
||||
* 通过 openAI 语音转文字
|
||||
* @param string $filePath
|
||||
* @param array $extParams
|
||||
* @return array
|
||||
*/
|
||||
public static function openAItranscriptions($filePath)
|
||||
public static function openAItranscriptions($filePath, $extParams = [])
|
||||
{
|
||||
if (!file_exists($filePath)) {
|
||||
return Base::retError("语音文件不存在");
|
||||
@@ -27,32 +28,34 @@ class Extranet
|
||||
if ($systemSetting['voice2text'] !== 'open' || empty($aibotSetting['openai_key'])) {
|
||||
return Base::retError("语音转文字功能未开启");
|
||||
}
|
||||
//
|
||||
$extra = [
|
||||
'Content-Type' => 'multipart/form-data',
|
||||
'Authorization' => 'Bearer ' . $aibotSetting['openai_key'],
|
||||
];
|
||||
if ($aibotSetting['openai_agency']) {
|
||||
$extra['CURLOPT_PROXY'] = $aibotSetting['openai_agency'];
|
||||
if (str_contains($aibotSetting['openai_agency'], 'socks')) {
|
||||
$extra['CURLOPT_PROXYTYPE'] = CURLPROXY_SOCKS5;
|
||||
} else {
|
||||
$extra['CURLOPT_PROXYTYPE'] = CURLPROXY_HTTP;
|
||||
}
|
||||
$extra['CURLOPT_PROXYTYPE'] = str_contains($aibotSetting['openai_agency'], 'socks') ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP;
|
||||
}
|
||||
$res = Ihttp::ihttp_request('https://api.openai.com/v1/audio/transcriptions', [
|
||||
$post = array_merge($extParams, [
|
||||
'file' => new \CURLFile($filePath),
|
||||
'model' => 'whisper-1'
|
||||
], $extra, 15);
|
||||
if (Base::isError($res)) {
|
||||
return Base::retError("语音转文字失败", $res);
|
||||
'model' => 'whisper-1',
|
||||
]);
|
||||
$cacheKey = "openAItranscriptions::" . md5($filePath . '_' . Base::array2json($extra) . '_' . Base::array2json($extParams));
|
||||
$result = Cache::remember($cacheKey, Carbon::now()->addDays(), function() use ($extra, $post) {
|
||||
$res = Ihttp::ihttp_request('https://api.openai.com/v1/audio/transcriptions', $post, $extra, 15);
|
||||
if (Base::isError($res)) {
|
||||
return Base::retError("语音转文字失败", $res);
|
||||
}
|
||||
$resData = Base::json2array($res['data']);
|
||||
if (empty($resData['text'])) {
|
||||
return Base::retError("语音转文字失败", $resData);
|
||||
}
|
||||
return Base::retSuccess("success", $resData['text']);
|
||||
});
|
||||
if (Base::isError($result)) {
|
||||
Cache::forget($cacheKey);
|
||||
}
|
||||
$resData = Base::json2array($res['data']);
|
||||
if (empty($resData['text'])) {
|
||||
return Base::retError("语音转文字失败", $resData);
|
||||
}
|
||||
//
|
||||
return Base::retSuccess("success", $resData['text']);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,18 +77,78 @@ class Extranet
|
||||
];
|
||||
if ($aibotSetting['openai_agency']) {
|
||||
$extra['CURLOPT_PROXY'] = $aibotSetting['openai_agency'];
|
||||
if (str_contains($aibotSetting['openai_agency'], 'socks')) {
|
||||
$extra['CURLOPT_PROXYTYPE'] = CURLPROXY_SOCKS5;
|
||||
} else {
|
||||
$extra['CURLOPT_PROXYTYPE'] = CURLPROXY_HTTP;
|
||||
}
|
||||
$extra['CURLOPT_PROXYTYPE'] = str_contains($aibotSetting['openai_agency'], 'socks') ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP;
|
||||
}
|
||||
$res = Ihttp::ihttp_request('https://api.openai.com/v1/chat/completions', json_encode([
|
||||
"model" => "gpt-3.5-turbo",
|
||||
$post = json_encode([
|
||||
"model" => "gpt-4o-mini",
|
||||
"messages" => [
|
||||
[
|
||||
"role" => "system",
|
||||
"content" => "你是一个专业的翻译器,翻译的结果尽量符合“项目任务管理系统”的使用,并且翻译的结果不用额外添加换行尽量保持原格式,将提供的文本翻译成“{$targetLanguage}”语言。"
|
||||
"content" => <<<EOF
|
||||
你是一名专业翻译人员,请将 <translation_original_text> 标签内的内容翻译为{$targetLanguage}。
|
||||
|
||||
翻译要求:
|
||||
- 翻译结果需符合“项目任务管理系统”的专业术语和使用场景。
|
||||
- 保持原文格式、结构和排版不变。
|
||||
- 语言表达准确、简洁,符合项目管理领域的行业规范。
|
||||
- 注意专业术语的一致性和连贯性。
|
||||
EOF
|
||||
],
|
||||
[
|
||||
"role" => "user",
|
||||
"content" => "<translation_original_text>{$text}</translation_original_text>"
|
||||
]
|
||||
]
|
||||
]);
|
||||
$cacheKey = "openAItranslations::" . md5(Base::array2json($extra) . '_' . Base::array2json($post));
|
||||
$result = Cache::remember($cacheKey, Carbon::now()->addDays(), function() use ($extra, $post) {
|
||||
$res = Ihttp::ihttp_request('https://api.openai.com/v1/chat/completions', $post, $extra, 15);
|
||||
if (Base::isError($res)) {
|
||||
return Base::retError("翻译失败", $res);
|
||||
}
|
||||
$resData = Base::json2array($res['data']);
|
||||
if (empty($resData['choices'])) {
|
||||
return Base::retError("翻译失败", $resData);
|
||||
}
|
||||
$result = $resData['choices'][0]['message']['content'];
|
||||
$result = preg_replace('/^\"|\"$/', '', trim($result));
|
||||
$result = preg_replace('/<\/*translation_original_text>/', '', trim($result));
|
||||
if (empty($result)) {
|
||||
return Base::retError("翻译失败", $result);
|
||||
}
|
||||
return Base::retSuccess("success", $result);
|
||||
});
|
||||
if (Base::isError($result)) {
|
||||
Cache::forget($cacheKey);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 openAI 生成标题
|
||||
* @param $text
|
||||
* @return array
|
||||
*/
|
||||
public static function openAIGenerateTitle($text)
|
||||
{
|
||||
$aibotSetting = Base::setting('aibotSetting');
|
||||
if (empty($aibotSetting['openai_key'])) {
|
||||
return Base::retError("AI接口未配置");
|
||||
}
|
||||
$extra = [
|
||||
'Content-Type' => 'application/json',
|
||||
'Authorization' => 'Bearer ' . $aibotSetting['openai_key'],
|
||||
];
|
||||
if ($aibotSetting['openai_agency']) {
|
||||
$extra['CURLOPT_PROXY'] = $aibotSetting['openai_agency'];
|
||||
$extra['CURLOPT_PROXYTYPE'] = str_contains($aibotSetting['openai_agency'], 'socks') ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP;
|
||||
}
|
||||
$res = Ihttp::ihttp_request('https://api.openai.com/v1/chat/completions', json_encode([
|
||||
"model" => "gpt-4o-mini",
|
||||
"messages" => [
|
||||
[
|
||||
"role" => "system",
|
||||
"content" => "你是一个专业的标题生成器,擅长为对话生成简洁的标题,请将提供的文本生成一个标题。"
|
||||
],
|
||||
[
|
||||
"role" => "user",
|
||||
@@ -94,20 +157,61 @@ class Extranet
|
||||
]
|
||||
]), $extra, 15);
|
||||
if (Base::isError($res)) {
|
||||
return Base::retError("翻译失败", $res);
|
||||
return Base::retError("生成失败", $res);
|
||||
}
|
||||
$resData = Base::json2array($res['data']);
|
||||
if (empty($resData['choices'])) {
|
||||
return Base::retError("翻译失败", $resData);
|
||||
return Base::retError("生成失败", $resData);
|
||||
}
|
||||
$result = $resData['choices'][0]['message']['content'];
|
||||
$result = preg_replace('/^\"|\"$/', '', $result);
|
||||
if (empty($result)) {
|
||||
return Base::retError("翻译失败", $result);
|
||||
return Base::retError("生成失败", $result);
|
||||
}
|
||||
return Base::retSuccess("success", $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 ollama 模型
|
||||
* @param $baseUrl
|
||||
* @param $key
|
||||
* @param $agency
|
||||
* @return array
|
||||
*/
|
||||
public static function ollamaModels($baseUrl, $key = null, $agency = null)
|
||||
{
|
||||
$extra = [
|
||||
'Content-Type' => 'application/json',
|
||||
];
|
||||
if ($key) {
|
||||
$extra['Authorization'] = 'Bearer ' . $key;
|
||||
}
|
||||
if ($agency) {
|
||||
$extra['CURLOPT_PROXY'] = $agency;
|
||||
$extra['CURLOPT_PROXYTYPE'] = str_contains($agency, 'socks') ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP;
|
||||
}
|
||||
$res = Ihttp::ihttp_request(rtrim($baseUrl, '/') . '/api/tags', [], $extra, 15);
|
||||
if (Base::isError($res)) {
|
||||
return Base::retError("获取失败", $res);
|
||||
}
|
||||
$resData = Base::json2array($res['data']);
|
||||
if (empty($resData['models'])) {
|
||||
return Base::retError("获取失败", $resData);
|
||||
}
|
||||
$models = [];
|
||||
foreach ($resData['models'] as $model) {
|
||||
if ($model['name'] !== $model['model']) {
|
||||
$models[] = "{$model['model']} | {$model['name']}";
|
||||
} else {
|
||||
$models[] = $model['model'];
|
||||
}
|
||||
}
|
||||
return Base::retSuccess("success", [
|
||||
'models' => $models,
|
||||
'original' => $resData['models']
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取IP地址经纬度
|
||||
* @param string $ip
|
||||
|
||||
@@ -220,11 +220,11 @@ class Image
|
||||
* @param string $savePath 保存路径
|
||||
* @param int $width 宽度
|
||||
* @param int $height 高度
|
||||
* @param int $quality 压缩质量(0-100), 0 为不压缩
|
||||
* @param int|bool $quality 压缩质量(0-100), 0 为不压缩,true 为从系统设置里面获取
|
||||
* @param string $mode 模式(percentage|cover|contain)
|
||||
* @return string|null 成功返回图片后缀,失败返回 false
|
||||
*/
|
||||
public static function thumbImage(string $imagePath, string $savePath, int $width, int $height, int $quality = 0, string $mode = 'percentage'): ?string
|
||||
public static function thumbImage(string $imagePath, string $savePath, int $width, int $height, int|bool $quality = 0, string $mode = 'percentage'): ?string
|
||||
{
|
||||
if (!file_exists($imagePath)) {
|
||||
return null;
|
||||
@@ -237,7 +237,7 @@ class Image
|
||||
$image = new Image($imagePath);
|
||||
$image->thumb($width, $height, $mode);
|
||||
$image->saveTo($savePath);
|
||||
if ($quality > 0) {
|
||||
if ($quality) {
|
||||
Image::compressImage($savePath, $quality);
|
||||
}
|
||||
if ($savePath != $imagePath && filesize($savePath) >= filesize($imagePath)) {
|
||||
@@ -253,14 +253,18 @@ class Image
|
||||
/**
|
||||
* 压缩图片(如果压缩后的图片比原图还大那就直接使用原图)
|
||||
* @param array|string $path 图片路径(如果是数组,第1个元素为原图路径,第2个元素为保存路径)
|
||||
* @param int $quality 压缩质量(0-100)
|
||||
* @param int|bool $quality 压缩质量(0-100),如果为 true,则从系统设置里面获取
|
||||
* @param float $minSize 最小尺寸,小于这个尺寸不压缩(单位:KB)
|
||||
* @return bool
|
||||
*/
|
||||
public static function compressImage(array|string $path, int $quality = 100, float $minSize = 5): bool
|
||||
public static function compressImage(array|string $path, int|bool $quality = true, float $minSize = 5): bool
|
||||
{
|
||||
if (Base::settingFind("system", "image_compress") === 'close') {
|
||||
return false;
|
||||
if ($quality === true) {
|
||||
$setting = Base::setting("system");
|
||||
if ($setting['image_compress'] === 'close') {
|
||||
return false;
|
||||
}
|
||||
$quality = $setting['image_quality'];
|
||||
}
|
||||
if (is_array($path)) {
|
||||
$imagePath = $path[0];
|
||||
@@ -272,7 +276,7 @@ class Image
|
||||
if (!file_exists($imagePath)) {
|
||||
return false;
|
||||
}
|
||||
$quality = min(max($quality, 1), 100);
|
||||
$quality = min(max(intval($quality), 1), 100);
|
||||
$imageSize = filesize($imagePath);
|
||||
if ($minSize > 0 && $imageSize < $minSize * 1024) {
|
||||
return false;
|
||||
|
||||
136
app/Module/MsgTool.php
Normal file
136
app/Module/MsgTool.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module;
|
||||
|
||||
|
||||
use DOMDocument;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
use League\CommonMark\Exception\CommonMarkException;
|
||||
use League\HTMLToMarkdown\HtmlConverter;
|
||||
|
||||
class MsgTool
|
||||
{
|
||||
/**
|
||||
* 截取文本并保持标签完整性
|
||||
*
|
||||
* @param string $text 要截取的文本
|
||||
* @param int $length 截取长度
|
||||
* @param string $type 文本类型 (htm 或 md)
|
||||
* @return string 处理后的文本
|
||||
*/
|
||||
public static function truncateText($text, $length, $type = 'htm')
|
||||
{
|
||||
if (empty($text) || mb_strlen($text) <= $length) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
$isMd = strtolower($type) === 'md';
|
||||
$placeholders = [];
|
||||
|
||||
// 如果是Markdown,先处理特殊标记及转换为HTML
|
||||
if ($isMd) {
|
||||
// 处理特殊标记
|
||||
$pattern = '/:::\s*reasoning\s+(.*?)\s*:::/s';
|
||||
$counter = 0;
|
||||
$text = preg_replace_callback($pattern, function($matches) use ($type, $length, &$placeholders, &$counter) {
|
||||
// 使用更简短的占位符,避免被markdown解析
|
||||
$placeholder = "@PH::{$counter}::PH@";
|
||||
$placeholders[$placeholder] = "::: reasoning\n" . self::truncateText($matches[1], $length, $type) . "\n:::";
|
||||
$counter++;
|
||||
return $placeholder;
|
||||
}, $text);
|
||||
// 转换为HTML
|
||||
try {
|
||||
$converter = new CommonMarkConverter();
|
||||
$text = $converter->convert($text);
|
||||
} catch (CommonMarkException) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// 创建DOM文档
|
||||
$dom = new DOMDocument('1.0', 'UTF-8');
|
||||
libxml_use_internal_errors(true);
|
||||
$dom->loadHTML(mb_convert_encoding($text, 'HTML-ENTITIES', 'UTF-8'));
|
||||
libxml_clear_errors();
|
||||
|
||||
// 获取body元素
|
||||
$body = $dom->getElementsByTagName('body')->item(0);
|
||||
$truncatedHtml = '';
|
||||
$currentLength = 0;
|
||||
|
||||
// 递归函数来遍历节点并截取内容
|
||||
self::traverseNodes($body, $currentLength, $length, $truncatedHtml);
|
||||
|
||||
// 如果是Markdown,转换回Markdown及还原特殊标记
|
||||
if ($isMd) {
|
||||
// 转换回Markdown
|
||||
try {
|
||||
$converter = new HtmlConverter();
|
||||
$truncatedHtml = $converter->convert($truncatedHtml);
|
||||
} catch (\Exception) {
|
||||
return "";
|
||||
}
|
||||
// 还原特殊标记
|
||||
if (!empty($placeholders)) {
|
||||
$truncatedHtml = preg_replace('/@P?H?:*\s*$/', '', $truncatedHtml);
|
||||
$preCount = substr_count($truncatedHtml, '@PH::');
|
||||
$sufCount = substr_count($truncatedHtml, '::PH@');
|
||||
$diffCount = $preCount - $sufCount;
|
||||
if ($diffCount > 0) {
|
||||
$truncatedHtml .= str_repeat('::PH@', $diffCount);
|
||||
}
|
||||
$truncatedHtml = strtr($truncatedHtml, $placeholders);
|
||||
}
|
||||
}
|
||||
|
||||
return $truncatedHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归遍历节点
|
||||
* @param $node
|
||||
* @param $currentLength
|
||||
* @param $length
|
||||
* @param $truncatedHtml
|
||||
* @return void
|
||||
*/
|
||||
private static function traverseNodes($node, &$currentLength, $length, &$truncatedHtml)
|
||||
{
|
||||
foreach ($node->childNodes as $child) {
|
||||
if ($currentLength >= $length) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($child->nodeType === XML_TEXT_NODE) {
|
||||
$textContent = $child->textContent;
|
||||
$remainingLength = $length - $currentLength;
|
||||
|
||||
if (mb_strlen($textContent) > $remainingLength) {
|
||||
$truncatedHtml .= htmlspecialchars(mb_substr($textContent, 0, $remainingLength) . '...');
|
||||
$currentLength += $remainingLength;
|
||||
} else {
|
||||
$truncatedHtml .= htmlspecialchars($textContent);
|
||||
$currentLength += mb_strlen($textContent);
|
||||
}
|
||||
} elseif ($child->nodeType === XML_ELEMENT_NODE) {
|
||||
$truncatedHtml .= '<' . $child->nodeName;
|
||||
|
||||
// 添加属性
|
||||
if ($child->hasAttributes()) {
|
||||
foreach ($child->attributes as $attr) {
|
||||
$truncatedHtml .= ' ' . $attr->nodeName . '="' . htmlspecialchars($attr->nodeValue) . '"';
|
||||
}
|
||||
}
|
||||
|
||||
$truncatedHtml .= '>';
|
||||
|
||||
self::traverseNodes($child, $currentLength, $length, $truncatedHtml);
|
||||
|
||||
if ($currentLength < $length || $child->firstChild) {
|
||||
$truncatedHtml .= '</' . $child->nodeName . '>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
259
app/Module/TextExtractor.php
Normal file
259
app/Module/TextExtractor.php
Normal file
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module;
|
||||
|
||||
use Exception;
|
||||
use PhpOffice\PhpWord\IOFactory as WordIOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory as SpreadsheetIOFactory;
|
||||
use PhpOffice\PhpPresentation\IOFactory as PresentationIOFactory;
|
||||
use Illuminate\Support\Facades\File as FileFacade;
|
||||
|
||||
|
||||
class TextExtractor
|
||||
{
|
||||
private string $filePath;
|
||||
private string $fileMimeType;
|
||||
private string $fileExtension;
|
||||
|
||||
/**
|
||||
* @param string $filePath
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct(string $filePath)
|
||||
{
|
||||
if (!file_exists($filePath)) {
|
||||
throw new Exception("File does not exist: {$filePath}");
|
||||
}
|
||||
$this->filePath = $filePath;
|
||||
$this->fileMimeType = FileFacade::mimeType($filePath);
|
||||
$this->fileExtension = $this->detectFileType();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件中提取文本
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
public function extractContent(): string
|
||||
{
|
||||
return match ($this->fileExtension) {
|
||||
// Word文档
|
||||
'docx' => $this->parseWordDocument(),
|
||||
|
||||
// Excel文档
|
||||
'xlsx', 'xls', 'csv' => $this->parseSpreadsheet(),
|
||||
|
||||
// PowerPoint文档
|
||||
'ppt', 'pptx' => $this->parsePresentation(),
|
||||
|
||||
// PDF文档
|
||||
'pdf' => $this->parsePdf(),
|
||||
|
||||
// RTF文档
|
||||
'rtf' => $this->parseRtf(),
|
||||
|
||||
// 其他文本文件
|
||||
default => $this->parseOther(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件类型
|
||||
* @return string
|
||||
*/
|
||||
private function detectFileType(): string
|
||||
{
|
||||
return match ($this->fileMimeType) {
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
|
||||
'application/vnd.ms-excel' => 'xls',
|
||||
'text/csv', 'application/csv' => 'csv',
|
||||
'application/vnd.ms-powerpoint' => 'ppt',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
|
||||
'application/pdf' => 'pdf',
|
||||
'application/rtf', 'text/rtf' => 'rtf',
|
||||
default => strtolower(pathinfo($this->filePath, PATHINFO_EXTENSION)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Word documents (.doc, .docx)
|
||||
* @return string
|
||||
*/
|
||||
private function parseWordDocument(): string
|
||||
{
|
||||
$phpWord = WordIOFactory::load($this->filePath);
|
||||
$text = '';
|
||||
|
||||
// Extract text from each section
|
||||
foreach ($phpWord->getSections() as $section) {
|
||||
foreach ($section->getElements() as $element) {
|
||||
if (method_exists($element, 'getText')) {
|
||||
$text .= $element->getText() . "\n";
|
||||
} elseif (method_exists($element, 'getElements')) {
|
||||
foreach ($element->getElements() as $childElement) {
|
||||
if (method_exists($childElement, 'getText')) {
|
||||
$text .= $childElement->getText() . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse spreadsheet files (.xlsx, .xls, .csv)
|
||||
* @return string
|
||||
*/
|
||||
private function parseSpreadsheet(): string
|
||||
{
|
||||
$spreadsheet = SpreadsheetIOFactory::load($this->filePath);
|
||||
$text = '';
|
||||
|
||||
// Extract text from all worksheets
|
||||
foreach ($spreadsheet->getWorksheetIterator() as $worksheet) {
|
||||
$text .= 'Worksheet: ' . $worksheet->getTitle() . "\n";
|
||||
|
||||
foreach ($worksheet->getRowIterator() as $row) {
|
||||
$cellIterator = $row->getCellIterator();
|
||||
$cellIterator->setIterateOnlyExistingCells(false);
|
||||
$rowText = '';
|
||||
|
||||
foreach ($cellIterator as $cell) {
|
||||
$value = $cell->getValue();
|
||||
if (!empty($value)) {
|
||||
$rowText .= $value . "\t";
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty(trim($rowText))) {
|
||||
$text .= trim($rowText) . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
$text .= "\n";
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse presentation files (.ppt, .pptx)
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
private function parsePresentation(): string
|
||||
{
|
||||
$presentation = PresentationIOFactory::load($this->filePath);
|
||||
$text = '';
|
||||
|
||||
// Extract text from all slides
|
||||
foreach ($presentation->getAllSlides() as $slide) {
|
||||
foreach ($slide->getShapeCollection() as $shape) {
|
||||
if ($shape instanceof \PhpOffice\PhpPresentation\Shape\RichText) {
|
||||
foreach ($shape->getParagraphs() as $paragraph) {
|
||||
foreach ($paragraph->getRichTextElements() as $element) {
|
||||
$text .= $element->getText();
|
||||
}
|
||||
$text .= "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
$text .= "\n";
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse PDF files (requires additional library like Smalot\PdfParser)
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
private function parsePdf(): string
|
||||
{
|
||||
// You'll need to install the Smalot PDF Parser: composer require smalot/pdfparser
|
||||
if (!class_exists('\Smalot\PdfParser\Parser')) {
|
||||
throw new \Exception("PDF Parser not available. Install with: composer require smalot/pdfparser");
|
||||
}
|
||||
|
||||
$parser = new \Smalot\PdfParser\Parser();
|
||||
$pdf = $parser->parseFile($this->filePath);
|
||||
return $pdf->getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse RTF files
|
||||
* @return string
|
||||
*/
|
||||
private function parseRtf(): string
|
||||
{
|
||||
// Simple RTF to text conversion
|
||||
$content = file_get_contents($this->filePath);
|
||||
|
||||
// Remove RTF control words and groups
|
||||
$content = preg_replace('/\\\\([a-z]{1,32})(-?[0-9]{1,10})?[ ]?/i', '', $content);
|
||||
$content = preg_replace('/\\\\([^a-z]|[a-z]{33,})/i', '', $content);
|
||||
$content = preg_replace('/\{\*?\\\\[^{}]*\}/', '', $content);
|
||||
$content = preg_replace('/\{[\r\n]*\}/', '', $content);
|
||||
|
||||
// Convert special characters
|
||||
$content = preg_replace('/\\\\\'([0-9a-f]{2})/i', '', $content);
|
||||
|
||||
// Remove remaining curly braces
|
||||
$content = str_replace(['{', '}'], '', $content);
|
||||
|
||||
return $content ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Other(text) files
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
private function parseOther(): string
|
||||
{
|
||||
$isBinary = !str_contains($this->fileMimeType, 'text/')
|
||||
&& !str_contains($this->fileMimeType, 'application/json')
|
||||
&& !str_contains($this->fileMimeType, 'application/xml');
|
||||
|
||||
if ($isBinary) {
|
||||
throw new Exception("Unable to read the text content of this type of file");
|
||||
}
|
||||
|
||||
return file_get_contents($this->filePath);
|
||||
}
|
||||
|
||||
/** ********************************************************************* */
|
||||
/** ********************************************************************* */
|
||||
/** ********************************************************************* */
|
||||
|
||||
/**
|
||||
* 获取文件内容
|
||||
* @param $filePath
|
||||
* @param int $fileMaxSize 最大文件大小,单位字节,默认1024KB
|
||||
* @param int $contentMaxSize 最大内容大小,单位字节,默认300KB
|
||||
* @return array
|
||||
*/
|
||||
public static function extractFile($filePath, int $fileMaxSize = 1024, int $contentMaxSize = 300): array
|
||||
{
|
||||
if (!file_exists($filePath) || !is_file($filePath)) {
|
||||
return Base::retError("Failed to read contents of {$filePath}");
|
||||
}
|
||||
if (filesize($filePath) > $fileMaxSize * 1024) {
|
||||
return Base::retError("File size exceeds " . Base::readableBytes($fileMaxSize * 1024) . ", unable to display content");
|
||||
}
|
||||
try {
|
||||
$extractor = new self($filePath);
|
||||
$content = $extractor->extractContent();
|
||||
if (strlen($content) > $contentMaxSize * 1024) {
|
||||
return Base::retError("Content size exceeds " . Base::readableBytes($contentMaxSize * 1024) . ", unable to display content");
|
||||
}
|
||||
return Base::retSuccess("success", $content);
|
||||
} catch (Exception $e) {
|
||||
return Base::retError($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ class ProjectTaskObserver
|
||||
}
|
||||
$array = [];
|
||||
if (in_array('task', $dataType)) {
|
||||
$array = array_merge($array, ProjectTaskUser::whereTaskId($projectTask->id)->pluck('userid')->toArray());
|
||||
$array = array_merge($array, ProjectTaskUser::whereTaskId($projectTask->id)->orWhere('task_pid' ,$projectTask->id)->pluck('userid')->toArray());
|
||||
}
|
||||
if (in_array('visibility', $dataType)) {
|
||||
$array = array_merge($array, ProjectTaskVisibilityUser::whereTaskId($projectTask->id)->pluck('userid')->toArray());
|
||||
@@ -121,5 +121,6 @@ class ProjectTaskObserver
|
||||
Deleted::forget('projectTask', $projectTask->id, $forgetUserids);
|
||||
break;
|
||||
}
|
||||
ProjectTask::whereParentId($projectTask->id)->change(['visibility' => $projectTask->visibility]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ class ProjectTaskUserObserver
|
||||
public function created(ProjectTaskUser $projectTaskUser)
|
||||
{
|
||||
Deleted::forget('projectTask', $projectTaskUser->task_id, $projectTaskUser->userid);
|
||||
if ($projectTaskUser->task_pid) {
|
||||
Deleted::forget('projectTask', $projectTaskUser->task_pid, $projectTaskUser->userid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
64
app/Observers/WebSocketDialogMsgObserver.php
Normal file
64
app/Observers/WebSocketDialogMsgObserver.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Module\ElasticSearch\ElasticSearchUserMsg;
|
||||
|
||||
class WebSocketDialogMsgObserver
|
||||
{
|
||||
/**
|
||||
* Handle the WebSocketDialogMsg "created" event.
|
||||
*
|
||||
* @param \App\Models\WebSocketDialogMsg $webSocketDialogMsg
|
||||
* @return void
|
||||
*/
|
||||
public function created(WebSocketDialogMsg $webSocketDialogMsg)
|
||||
{
|
||||
ElasticSearchUserMsg::syncMsg($webSocketDialogMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the WebSocketDialogMsg "updated" event.
|
||||
*
|
||||
* @param \App\Models\WebSocketDialogMsg $webSocketDialogMsg
|
||||
* @return void
|
||||
*/
|
||||
public function updated(WebSocketDialogMsg $webSocketDialogMsg)
|
||||
{
|
||||
ElasticSearchUserMsg::syncMsg($webSocketDialogMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the WebSocketDialogMsg "deleted" event.
|
||||
*
|
||||
* @param \App\Models\WebSocketDialogMsg $webSocketDialogMsg
|
||||
* @return void
|
||||
*/
|
||||
public function deleted(WebSocketDialogMsg $webSocketDialogMsg)
|
||||
{
|
||||
ElasticSearchUserMsg::deleteMsg($webSocketDialogMsg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the WebSocketDialogMsg "restored" event.
|
||||
*
|
||||
* @param \App\Models\WebSocketDialogMsg $webSocketDialogMsg
|
||||
* @return void
|
||||
*/
|
||||
public function restored(WebSocketDialogMsg $webSocketDialogMsg)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the WebSocketDialogMsg "force deleted" event.
|
||||
*
|
||||
* @param \App\Models\WebSocketDialogMsg $webSocketDialogMsg
|
||||
* @return void
|
||||
*/
|
||||
public function forceDeleted(WebSocketDialogMsg $webSocketDialogMsg)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Observers;
|
||||
|
||||
use App\Models\Deleted;
|
||||
use App\Models\WebSocketDialogUser;
|
||||
use App\Module\ElasticSearch\ElasticSearchUserMsg;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class WebSocketDialogUserObserver
|
||||
@@ -29,6 +30,7 @@ class WebSocketDialogUserObserver
|
||||
}
|
||||
}
|
||||
Deleted::forget('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
|
||||
ElasticSearchUserMsg::syncUser($webSocketDialogUser);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,7 +41,7 @@ class WebSocketDialogUserObserver
|
||||
*/
|
||||
public function updated(WebSocketDialogUser $webSocketDialogUser)
|
||||
{
|
||||
//
|
||||
ElasticSearchUserMsg::syncUser($webSocketDialogUser);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,6 +53,7 @@ class WebSocketDialogUserObserver
|
||||
public function deleted(WebSocketDialogUser $webSocketDialogUser)
|
||||
{
|
||||
Deleted::record('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
|
||||
ElasticSearchUserMsg::deleteUser($webSocketDialogUser);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,11 +7,13 @@ use App\Models\ProjectTask;
|
||||
use App\Models\ProjectTaskUser;
|
||||
use App\Models\ProjectUser;
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Models\WebSocketDialogUser;
|
||||
use App\Observers\ProjectObserver;
|
||||
use App\Observers\ProjectTaskObserver;
|
||||
use App\Observers\ProjectTaskUserObserver;
|
||||
use App\Observers\ProjectUserObserver;
|
||||
use App\Observers\WebSocketDialogMsgObserver;
|
||||
use App\Observers\WebSocketDialogObserver;
|
||||
use App\Observers\WebSocketDialogUserObserver;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
@@ -43,6 +45,7 @@ class EventServiceProvider extends ServiceProvider
|
||||
ProjectTaskUser::observe(ProjectTaskUserObserver::class);
|
||||
ProjectUser::observe(ProjectUserObserver::class);
|
||||
WebSocketDialog::observe(WebSocketDialogObserver::class);
|
||||
WebSocketDialogMsg::observe(WebSocketDialogMsgObserver::class);
|
||||
WebSocketDialogUser::observe(WebSocketDialogUserObserver::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,4 +139,64 @@ class RequestContext
|
||||
self::$context[$requestId] ??= [];
|
||||
self::$context[$requestId] = array_merge(self::$context[$requestId], $data);
|
||||
}
|
||||
|
||||
/** ***************************************************************************************** */
|
||||
/** ***************************************************************************************** */
|
||||
/** ***************************************************************************************** */
|
||||
|
||||
/**
|
||||
* 更新请求的基本URL
|
||||
*
|
||||
* @param Request $request
|
||||
* @return void
|
||||
*/
|
||||
public static function updateBaseUrl($request)
|
||||
{
|
||||
if ($request->path() !== 'api/system/setting') {
|
||||
return;
|
||||
}
|
||||
$schemeAndHttpHost = $request->getSchemeAndHttpHost();
|
||||
if (str_contains($schemeAndHttpHost, '127.0.0.1') || str_contains($schemeAndHttpHost, 'localhost')) {
|
||||
return;
|
||||
}
|
||||
\Cache::forever('RequestContext::base_url', $schemeAndHttpHost);
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换请求的基本URL
|
||||
*
|
||||
* @param string $url
|
||||
* @return string
|
||||
*/
|
||||
public static function replaceBaseUrl(string $url): string
|
||||
{
|
||||
// 先提取主机部分
|
||||
$pattern = '/^(https?:\/\/[^\/?#:]+(:\d+)?)/i';
|
||||
if (!preg_match($pattern, $url, $matches)) {
|
||||
return $url; // 如果不是有效URL直接返回
|
||||
}
|
||||
|
||||
$schemeAndHttpHost = $matches[1] ?? '';
|
||||
if (!$schemeAndHttpHost) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
// 只检查主机部分是否为本地主机
|
||||
if (str_contains($schemeAndHttpHost, '127.0.0.1') || str_contains($schemeAndHttpHost, 'localhost')) {
|
||||
$baseUrl = \Cache::get('RequestContext::base_url');
|
||||
if ($baseUrl) {
|
||||
return $baseUrl . substr($url, strlen($schemeAndHttpHost));
|
||||
}
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除基本URL缓存
|
||||
*/
|
||||
public static function clearBaseUrlCache(): void
|
||||
{
|
||||
\Cache::forget('RequestContext::base_url');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,10 @@ class WebSocketService implements WebSocketHandlerInterface
|
||||
case 'receipt':
|
||||
return;
|
||||
|
||||
// 握手信息
|
||||
case 'handshake':
|
||||
break;
|
||||
|
||||
// 访问状态
|
||||
case 'path':
|
||||
$row = WebSocket::whereFd($frame->fd)->first();
|
||||
|
||||
@@ -21,25 +21,62 @@ class AutoArchivedTask extends AbstractTask
|
||||
}
|
||||
|
||||
public function start()
|
||||
{
|
||||
$this->systemAutoArchived();
|
||||
$this->projectAutoArchived();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理已完成未归档的任务(系统默认)
|
||||
*/
|
||||
private function systemAutoArchived()
|
||||
{
|
||||
$setting = Base::setting('system');
|
||||
if ($setting['auto_archived'] === 'open') {
|
||||
$archivedDay = floatval($setting['archived_day']);
|
||||
if ($archivedDay > 0) {
|
||||
$archivedDay = min(100, $archivedDay);
|
||||
$archivedTime = Carbon::now()->subDays($archivedDay);
|
||||
//获取已完成未归档的任务
|
||||
$taskLists = ProjectTask::whereNotNull('complete_at')
|
||||
->where('complete_at', '<=', $archivedTime)
|
||||
->where('archived_userid', 0)
|
||||
->whereNull('archived_at')
|
||||
->take(100)
|
||||
->get();
|
||||
/** @var ProjectTask $task */
|
||||
foreach ($taskLists AS $task) {
|
||||
$task->archivedTask(Carbon::now(), true);
|
||||
}
|
||||
}
|
||||
if ($setting['auto_archived'] !== 'open') {
|
||||
return;
|
||||
}
|
||||
$archivedDay = min(365, floatval($setting['archived_day']));
|
||||
if ($archivedDay <= 0) {
|
||||
return;
|
||||
}
|
||||
$taskLists = ProjectTask::select('project_tasks.*')
|
||||
->join('projects', 'projects.id', '=', 'project_tasks.project_id')
|
||||
->whereNotNull('project_tasks.complete_at')
|
||||
->where('project_tasks.complete_at', '<=', Carbon::now()->subDays($archivedDay))
|
||||
->where('project_tasks.archived_userid', 0)
|
||||
->whereNull('project_tasks.archived_at')
|
||||
->where('projects.archive_method', '!=', 'custom')
|
||||
->take(100)
|
||||
->get();
|
||||
/** @var ProjectTask $task */
|
||||
foreach ($taskLists as $task) {
|
||||
$task->archivedTask(Carbon::now(), true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理已完成未归档的任务(项目自定义)
|
||||
*/
|
||||
private function projectAutoArchived()
|
||||
{
|
||||
// 获取设置了自定义归档的项目的任务
|
||||
$prefix = \DB::getTablePrefix();
|
||||
$taskLists = ProjectTask::select('project_tasks.*')
|
||||
->join('projects', 'projects.id', '=', 'project_tasks.project_id')
|
||||
->whereNotNull('project_tasks.complete_at')
|
||||
->where('project_tasks.archived_userid', 0)
|
||||
->whereNull('project_tasks.archived_at')
|
||||
->where('projects.archive_method', 'custom')
|
||||
->whereRaw("DATEDIFF(NOW(), {$prefix}project_tasks.complete_at) >= {$prefix}projects.archive_days")
|
||||
->with(['project' => function ($query) {
|
||||
$query->select('id', 'archive_days');
|
||||
}])
|
||||
->take(100)
|
||||
->get();
|
||||
|
||||
/** @var ProjectTask $task */
|
||||
foreach ($taskLists as $task) {
|
||||
$task->archivedTask(Carbon::now(), true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,23 @@
|
||||
|
||||
namespace App\Tasks;
|
||||
|
||||
use App\Models\FileContent;
|
||||
use App\Models\Project;
|
||||
use App\Models\ProjectTask;
|
||||
use App\Models\Report;
|
||||
use App\Models\User;
|
||||
use App\Models\UserBot;
|
||||
use App\Models\UserDepartment;
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogConfig;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Module\Ihttp;
|
||||
use App\Module\TextExtractor;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use League\HTMLToMarkdown\HtmlConverter;
|
||||
use DB;
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
@@ -22,17 +31,19 @@ use DB;
|
||||
*/
|
||||
class BotReceiveMsgTask extends AbstractTask
|
||||
{
|
||||
protected $userid;
|
||||
protected $msgId;
|
||||
protected $mention;
|
||||
protected $client = [];
|
||||
protected $userid; // 机器人ID
|
||||
protected $msgId; // 消息ID
|
||||
protected $mention; // 是否提及
|
||||
protected $mentionOther; // 是否提及其他人
|
||||
protected $client = []; // 客户端信息(版本、语言、平台)
|
||||
|
||||
public function __construct($userid, $msgId, $mention, $client = [])
|
||||
public function __construct($userid, $msgId, $mentions, $client = [])
|
||||
{
|
||||
parent::__construct(...func_get_args());
|
||||
$this->userid = $userid;
|
||||
$this->msgId = $msgId;
|
||||
$this->mention = $mention;
|
||||
$this->mention = array_intersect([$userid], $mentions) ? 1 : 0; // 是否提及(不含@所有人)
|
||||
$this->mentionOther = array_diff($mentions, [0, $userid]) ? 1 : 0; // 是否提及其他人
|
||||
$this->client = is_array($client) ? $client : [];
|
||||
}
|
||||
|
||||
@@ -42,12 +53,14 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
if (empty($botUser)) {
|
||||
return;
|
||||
}
|
||||
$msg = WebSocketDialogMsg::find($this->msgId);
|
||||
$msg = WebSocketDialogMsg::with(['user'])->find($this->msgId);
|
||||
if (empty($msg)) {
|
||||
return;
|
||||
}
|
||||
$msg->readSuccess($botUser->userid);
|
||||
$this->botManagerReceive($msg, $botUser);
|
||||
if (!$msg->user?->bot) {
|
||||
$this->botReceiveBusiness($msg, $botUser);
|
||||
}
|
||||
}
|
||||
|
||||
public function end()
|
||||
@@ -56,16 +69,15 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
}
|
||||
|
||||
/**
|
||||
* 机器人管理处理消息
|
||||
* 机器人处理消息
|
||||
* @param WebSocketDialogMsg $msg
|
||||
* @param User $botUser
|
||||
* @return void
|
||||
*/
|
||||
private function botManagerReceive(WebSocketDialogMsg $msg, User $botUser)
|
||||
private function botReceiveBusiness(WebSocketDialogMsg $msg, User $botUser)
|
||||
{
|
||||
// 位置消息
|
||||
// 位置消息(仅支持签到机器人)
|
||||
if ($msg->type === 'location') {
|
||||
// 签到机器人
|
||||
if ($botUser->email === 'check-in@bot.system') {
|
||||
$content = UserBot::checkinBotQuickMsg('locat-checkin', $msg->userid, $msg->msg);
|
||||
if ($content) {
|
||||
@@ -78,36 +90,39 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
return;
|
||||
}
|
||||
|
||||
// 文本消息
|
||||
if ($msg->type !== 'text') {
|
||||
// 提取指令
|
||||
try {
|
||||
$command = $this->extractCommand($msg, $botUser, $this->mention);
|
||||
if (empty($command)) {
|
||||
return;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'template', [
|
||||
'type' => 'content',
|
||||
'content' => $e->getMessage() ?: "指令解析失败。",
|
||||
], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
|
||||
return;
|
||||
}
|
||||
$original = $msg->msg['text'];
|
||||
if ($this->mention) {
|
||||
$original = preg_replace("/<span class=\"mention user\" data-id=\"(\d+)\">(.*?)<\/span>/", "", $original);
|
||||
}
|
||||
if (preg_match("/<span[^>]*?data-quick-key=([\"'])([^\"']+?)\\1[^>]*?>(.*?)<\/span>/is", $original, $match)) {
|
||||
$command = $match[2];
|
||||
if (str_starts_with($command, '%3A.')) {
|
||||
$command = ":" . substr($command, 4);
|
||||
}
|
||||
} else {
|
||||
$command = trim(strip_tags($original));
|
||||
}
|
||||
//
|
||||
|
||||
// 查询会话
|
||||
$dialog = WebSocketDialog::find($msg->dialog_id);
|
||||
if (empty($dialog)) {
|
||||
return;
|
||||
}
|
||||
// 推送Webhook
|
||||
if ($command
|
||||
&& !str_starts_with($command, '/')
|
||||
&& ($dialog->type === 'user' || $this->mention)) {
|
||||
$this->botManagerWebhook($command, $msg, $botUser, $dialog);
|
||||
|
||||
// 如果是群聊,未提及丹提及其他人
|
||||
if ($dialog->type === 'group' && !$this->mention && $this->mentionOther) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 推送Webhook
|
||||
$this->botWebhookBusiness($command, $msg, $botUser, $dialog);
|
||||
|
||||
// 仅支持用户会话
|
||||
if ($dialog->type !== 'user') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 签到机器人
|
||||
if ($botUser->email === 'check-in@bot.system') {
|
||||
$content = UserBot::checkinBotQuickMsg($command, $msg->userid);
|
||||
@@ -118,6 +133,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
|
||||
}
|
||||
}
|
||||
|
||||
// 隐私机器人
|
||||
if ($botUser->email === 'anon-msg@bot.system') {
|
||||
$array = UserBot::anonBotQuickMsg($command);
|
||||
@@ -129,8 +145,10 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
], $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()) {
|
||||
@@ -142,7 +160,8 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
|
||||
return;
|
||||
}
|
||||
//
|
||||
|
||||
// 指令处理
|
||||
$array = Base::newTrim(explode(" ", "{$command} "));
|
||||
$type = $array[0];
|
||||
$data = [];
|
||||
@@ -179,7 +198,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
case '/hello':
|
||||
case '/info':
|
||||
$botId = $isManager ? $array[1] : $botUser->userid;
|
||||
$data = $this->botManagerOne($botId, $msg->userid);
|
||||
$data = $this->botOne($botId, $msg->userid);
|
||||
if (!$data) {
|
||||
$content = "机器人不存在。";
|
||||
}
|
||||
@@ -189,32 +208,11 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
* 创建
|
||||
*/
|
||||
case '/newbot':
|
||||
if (User::select(['users.*'])
|
||||
->join('user_bots', 'users.userid', '=', 'user_bots.bot_id')
|
||||
->where('users.bot', 1)
|
||||
->where('user_bots.userid', $msg->userid)
|
||||
->count() >= 50) {
|
||||
$content = "超过最大创建数量。";
|
||||
break;
|
||||
}
|
||||
if (strlen($array[1]) < 2 || strlen($array[1]) > 20) {
|
||||
$content = "机器人名称由2-20个字符组成。";
|
||||
break;
|
||||
}
|
||||
$data = User::botGetOrCreate("user-" . Base::generatePassword(), [
|
||||
'nickname' => $array[1]
|
||||
], $msg->userid);
|
||||
if (empty($data)) {
|
||||
$content = "创建失败。";
|
||||
break;
|
||||
}
|
||||
$dialog = WebSocketDialog::checkUserDialog($data, $msg->userid);
|
||||
if ($dialog) {
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'template', [
|
||||
'type' => '/hello',
|
||||
'title' => '创建成功。',
|
||||
'data' => $data,
|
||||
], $data->userid); // todo 未能在任务end事件来发送任务
|
||||
$res = UserBot::newbot($msg->userid, $array[1]);
|
||||
if (Base::isError($res)) {
|
||||
$content = $res['msg'];
|
||||
} else {
|
||||
$data = $res['data'];
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -228,7 +226,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
$content = "机器人名称由2-20个字符组成。";
|
||||
break;
|
||||
}
|
||||
$data = $this->botManagerOne($botId, $msg->userid);
|
||||
$data = $this->botOne($botId, $msg->userid);
|
||||
if ($data) {
|
||||
$data->nickname = $nameString;
|
||||
$data->az = Base::getFirstCharter($nameString);
|
||||
@@ -245,7 +243,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
*/
|
||||
case '/deletebot':
|
||||
$botId = $isManager ? $array[1] : $botUser->userid;
|
||||
$data = $this->botManagerOne($botId, $msg->userid);
|
||||
$data = $this->botOne($botId, $msg->userid);
|
||||
if ($data) {
|
||||
$data->deleteUser('delete bot');
|
||||
} else {
|
||||
@@ -258,7 +256,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
*/
|
||||
case '/token':
|
||||
$botId = $isManager ? $array[1] : $botUser->userid;
|
||||
$data = $this->botManagerOne($botId, $msg->userid);
|
||||
$data = $this->botOne($botId, $msg->userid);
|
||||
if ($data) {
|
||||
User::generateToken($data);
|
||||
} else {
|
||||
@@ -271,7 +269,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
*/
|
||||
case '/revoke':
|
||||
$botId = $isManager ? $array[1] : $botUser->userid;
|
||||
$data = $this->botManagerOne($botId, $msg->userid);
|
||||
$data = $this->botOne($botId, $msg->userid);
|
||||
if ($data) {
|
||||
$data->encrypt = Base::generatePassword(6);
|
||||
$data->password = Doo::md5s(Base::generatePassword(32), $data->encrypt);
|
||||
@@ -287,7 +285,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
case '/clearday':
|
||||
$botId = $isManager ? $array[1] : $botUser->userid;
|
||||
$clearDay = $isManager ? $array[2] : $array[1];
|
||||
$data = $this->botManagerOne($botId, $msg->userid);
|
||||
$data = $this->botOne($botId, $msg->userid);
|
||||
if ($data) {
|
||||
$userBot = UserBot::whereBotId($botId)->whereUserid($msg->userid)->first();
|
||||
if ($userBot) {
|
||||
@@ -308,7 +306,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
case '/webhook':
|
||||
$botId = $isManager ? $array[1] : $botUser->userid;
|
||||
$webhookUrl = $isManager ? $array[2] : $array[1];
|
||||
$data = $this->botManagerOne($botId, $msg->userid);
|
||||
$data = $this->botOne($botId, $msg->userid);
|
||||
if (strlen($webhookUrl) > 255) {
|
||||
$content = "webhook地址最长仅支持255个字符。";
|
||||
} elseif ($data) {
|
||||
@@ -331,7 +329,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
case '/dialog':
|
||||
$botId = $isManager ? $array[1] : $botUser->userid;
|
||||
$nameKey = $isManager ? $array[2] : $array[1];
|
||||
$data = $this->botManagerOne($botId, $msg->userid);
|
||||
$data = $this->botOne($botId, $msg->userid);
|
||||
if ($data) {
|
||||
$list = DB::table('web_socket_dialog_users as u')
|
||||
->select(['d.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
|
||||
@@ -357,8 +355,8 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
}
|
||||
break;
|
||||
}
|
||||
//
|
||||
|
||||
// 回复消息
|
||||
if ($content) {
|
||||
$msgData = [
|
||||
'type' => 'content',
|
||||
@@ -403,119 +401,94 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
* @param WebSocketDialog $dialog
|
||||
* @return void
|
||||
*/
|
||||
private function botManagerWebhook(string $command, WebSocketDialogMsg $msg, User $botUser, WebSocketDialog $dialog)
|
||||
private function botWebhookBusiness(string $command, WebSocketDialogMsg $msg, User $botUser, WebSocketDialog $dialog)
|
||||
{
|
||||
$serverUrl = 'http://' . env('APP_IPPR') . '.3';
|
||||
$serverUrl = 'http://nginx';
|
||||
$userBot = null;
|
||||
$extras = [];
|
||||
$replyText = null;
|
||||
$errorContent = null;
|
||||
switch ($botUser->email) {
|
||||
// ChatGPT 机器人
|
||||
case 'ai-openai@bot.system':
|
||||
$setting = Base::setting('aibotSetting');
|
||||
$webhookUrl = "{$serverUrl}/ai/openai/send";
|
||||
$extras = [
|
||||
'openai_key' => $setting['openai_key'],
|
||||
'openai_agency' => $setting['openai_agency'],
|
||||
'openai_model' => $setting['openai_model'],
|
||||
'server_url' => $serverUrl,
|
||||
'chunk_size' => 7,
|
||||
];
|
||||
if (empty($extras['openai_key'])) {
|
||||
if ($botUser->isAiBot($type)) {
|
||||
// AI机器人
|
||||
$setting = Base::setting('aibotSetting');
|
||||
$extras = [
|
||||
'model_type' => match ($type) {
|
||||
'qianwen' => 'qwen',
|
||||
default => $type,
|
||||
},
|
||||
'model_name' => $setting[$type . '_model'],
|
||||
'system_message' => $setting[$type . '_system'],
|
||||
'api_key' => $setting[$type . '_key'],
|
||||
'base_url' => $setting[$type . '_base_url'],
|
||||
'agency' => $setting[$type . '_agency'],
|
||||
'server_url' => $serverUrl,
|
||||
];
|
||||
if ($setting[$type . '_temperature']) {
|
||||
$extras['temperature'] = floatval($setting[$type . '_temperature']);
|
||||
}
|
||||
if ($msg->msg['model_name']) {
|
||||
$extras['model_name'] = $msg->msg['model_name'];
|
||||
}
|
||||
if (preg_match("/(.*?)(\s+|\s*[_-]\s*)(think|thinking|reasoning)\s*$/", $extras['model_name'], $match)) {
|
||||
$extras['model_name'] = $match[1];
|
||||
$extras['max_tokens'] = 20000;
|
||||
$extras['thinking'] = 4096;
|
||||
$extras['temperature'] = 1.0;
|
||||
}
|
||||
if ($dialog->session_id) {
|
||||
$extras['context_key'] = 'session_' . $dialog->session_id;
|
||||
}
|
||||
if ($type === 'wenxin') {
|
||||
$extras['api_key'] .= ':' . $setting['wenxin_secret'];
|
||||
}
|
||||
if ($type === 'ollama') {
|
||||
if (empty($extras['base_url'])) {
|
||||
$errorContent = '机器人未启用。';
|
||||
} elseif (in_array($this->client['platform'], ['win', 'mac', 'web'])
|
||||
&& !Base::judgeClientVersion("0.29.11", $this->client['version'])) {
|
||||
$errorContent = '当前客户端版本低(所需版本≥v0.29.11)。';
|
||||
}
|
||||
break;
|
||||
// Claude 机器人
|
||||
case 'ai-claude@bot.system':
|
||||
$setting = Base::setting('aibotSetting');
|
||||
$webhookUrl = "{$serverUrl}/ai/claude/send";
|
||||
$extras = [
|
||||
'claude_token' => $setting['claude_token'],
|
||||
'claude_agency' => $setting['claude_agency'],
|
||||
'server_url' => $serverUrl,
|
||||
];
|
||||
if (empty($extras['claude_token'])) {
|
||||
$errorContent = '机器人未启用。';
|
||||
} elseif (in_array($this->client['platform'], ['win', 'mac', 'web'])
|
||||
&& !Base::judgeClientVersion("0.29.11", $this->client['version'])) {
|
||||
$errorContent = '当前客户端版本低(所需版本≥v0.29.11)。';
|
||||
if (empty($extras['api_key'])) {
|
||||
$extras['api_key'] = Base::strRandom(6);
|
||||
}
|
||||
break;
|
||||
// Wenxin 机器人
|
||||
case 'ai-wenxin@bot.system':
|
||||
$setting = Base::setting('aibotSetting');
|
||||
$webhookUrl = "{$serverUrl}/ai/wenxin/send";
|
||||
$extras = [
|
||||
'wenxin_key' => $setting['wenxin_key'],
|
||||
'wenxin_secret' => $setting['wenxin_secret'],
|
||||
'wenxin_model' => $setting['wenxin_model'],
|
||||
'server_url' => $serverUrl,
|
||||
];
|
||||
if (empty($extras['wenxin_key'])) {
|
||||
$errorContent = '机器人未启用。';
|
||||
} elseif (in_array($this->client['platform'], ['win', 'mac', 'web'])
|
||||
&& !Base::judgeClientVersion("0.29.11", $this->client['version'])) {
|
||||
$errorContent = '当前客户端版本低(所需版本≥v0.29.12)。';
|
||||
}
|
||||
if (empty($extras['api_key'])) {
|
||||
$errorContent = '机器人未启用。';
|
||||
}
|
||||
if (in_array($this->client['platform'], ['win', 'mac', 'web']) && !Base::judgeClientVersion("0.41.11", $this->client['version'])) {
|
||||
$errorContent = '当前客户端版本低(所需版本≥v0.41.11)。';
|
||||
}
|
||||
|
||||
if ($msg->reply_id > 0) {
|
||||
$replyCommand = $this->extractReplyCommand($msg->reply_id, $botUser);
|
||||
if (Base::isError($replyCommand)) {
|
||||
$errorContent = $replyCommand['msg'];
|
||||
} else {
|
||||
$command = <<<EOF
|
||||
<quoted_content>
|
||||
{$replyCommand['data']}
|
||||
</quoted_content>
|
||||
|
||||
The content within the above quoted_content tags is a citation.
|
||||
|
||||
{$command}
|
||||
EOF;
|
||||
}
|
||||
break;
|
||||
// QianWen 机器人
|
||||
case 'ai-qianwen@bot.system':
|
||||
$setting = Base::setting('aibotSetting');
|
||||
$webhookUrl = "{$serverUrl}/ai/qianwen/send";
|
||||
$extras = [
|
||||
'qianwen_key' => $setting['qianwen_key'],
|
||||
'qianwen_model' => $setting['qianwen_model'],
|
||||
'server_url' => $serverUrl,
|
||||
];
|
||||
if (empty($extras['qianwen_key'])) {
|
||||
$errorContent = '机器人未启用。';
|
||||
} elseif (in_array($this->client['platform'], ['win', 'mac', 'web'])
|
||||
&& !Base::judgeClientVersion("0.29.11", $this->client['version'])) {
|
||||
$errorContent = '当前客户端版本低(所需版本≥v0.29.12)。';
|
||||
}
|
||||
$this->AIGenerateSystemMessage($msg->userid, $dialog, $extras);
|
||||
$webhookUrl = "{$serverUrl}/ai/chat";
|
||||
} else {
|
||||
// 用户机器人
|
||||
if ($botUser->isUserBot() && str_starts_with($command, '/')) {
|
||||
// 用户机器人不处理指令类型命令
|
||||
return;
|
||||
}
|
||||
|
||||
if ($msg->reply_id > 0) {
|
||||
$replyCommand = $this->extractReplyCommand($msg->reply_id, $botUser);
|
||||
if (Base::isSuccess($replyCommand)) {
|
||||
$replyText = $replyCommand['data'] ?: '';
|
||||
}
|
||||
break;
|
||||
// Gemini 机器人
|
||||
case 'ai-gemini@bot.system':
|
||||
$setting = Base::setting('aibotSetting');
|
||||
$webhookUrl = "{$serverUrl}/ai/gemini/send";
|
||||
$extras = [
|
||||
'gemini_key' => $setting['gemini_key'],
|
||||
'gemini_model' => $setting['gemini_model'],
|
||||
'gemini_agency' => $setting['gemini_agency'],
|
||||
'gemini_timeout' => 20,
|
||||
'server_url' => $serverUrl,
|
||||
];
|
||||
if (empty($extras['gemini_key'])) {
|
||||
$errorContent = '机器人未启用。';
|
||||
} elseif (in_array($this->client['platform'], ['win', 'mac', 'web'])
|
||||
&& !Base::judgeClientVersion("0.29.11", $this->client['version'])) {
|
||||
$errorContent = '当前客户端版本低(所需版本≥v0.29.12)。';
|
||||
}
|
||||
break;
|
||||
// 智谱清言 机器人
|
||||
case 'ai-zhipu@bot.system':
|
||||
$setting = Base::setting('aibotSetting');
|
||||
$webhookUrl = "{$serverUrl}/ai/zhipu/send";
|
||||
$extras = [
|
||||
'zhipu_key' => $setting['zhipu_key'],
|
||||
'zhipu_model' => $setting['zhipu_model'],
|
||||
'server_url' => $serverUrl,
|
||||
];
|
||||
if (empty($extras['zhipu_key'])) {
|
||||
$errorContent = '机器人未启用。';
|
||||
} elseif (in_array($this->client['platform'], ['win', 'mac', 'web'])
|
||||
&& !Base::judgeClientVersion("0.29.11", $this->client['version'])) {
|
||||
$errorContent = '当前客户端版本低(所需版本≥v0.29.12)。';
|
||||
}
|
||||
break;
|
||||
// 其他机器人
|
||||
default:
|
||||
$userBot = UserBot::whereBotId($botUser->userid)->first();
|
||||
$webhookUrl = $userBot?->webhook_url;
|
||||
break;
|
||||
}
|
||||
$userBot = UserBot::whereBotId($botUser->userid)->first();
|
||||
$webhookUrl = $userBot?->webhook_url;
|
||||
}
|
||||
if ($errorContent) {
|
||||
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'template', [
|
||||
@@ -531,6 +504,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
try {
|
||||
$data = [
|
||||
'text' => $command,
|
||||
'reply_text' => $replyText,
|
||||
'token' => User::generateToken($botUser),
|
||||
'dialog_id' => $dialog->id,
|
||||
'dialog_type' => $dialog->type,
|
||||
@@ -541,7 +515,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
'version' => Base::getVersion(),
|
||||
'extras' => Base::array2json($extras)
|
||||
];
|
||||
$res = Ihttp::ihttp_post($webhookUrl, $data);
|
||||
$res = Ihttp::ihttp_post($webhookUrl, $data, 30);
|
||||
if ($userBot) {
|
||||
$userBot->webhook_num++;
|
||||
$userBot->save();
|
||||
@@ -563,11 +537,12 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取机器人信息
|
||||
* @param $botId
|
||||
* @param $userid
|
||||
* @return User
|
||||
*/
|
||||
private function botManagerOne($botId, $userid)
|
||||
private function botOne($botId, $userid)
|
||||
{
|
||||
$botId = intval($botId);
|
||||
$userid = intval($userid);
|
||||
@@ -587,4 +562,224 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取消息指令(提取消息内容)
|
||||
* @param WebSocketDialogMsg $msg
|
||||
* @param User $botUser
|
||||
* @param bool $mention
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
private function extractCommand(WebSocketDialogMsg $msg, User $botUser, bool $mention = false)
|
||||
{
|
||||
if ($msg->type !== 'text') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$original = $msg->msg['text'] ?: '';
|
||||
if ($mention) {
|
||||
$original = preg_replace("/<span class=\"mention user\" data-id=\"(\d+)\">(.*?)<\/span>/", "", $original);
|
||||
}
|
||||
if (preg_match("/<span[^>]*?data-quick-key=([\"'])([^\"']+?)\\1[^>]*?>(.*?)<\/span>/is", $original, $match)) {
|
||||
$command = $match[2];
|
||||
if (str_starts_with($command, '%3A.')) {
|
||||
$command = ":" . substr($command, 4);
|
||||
}
|
||||
return $command;
|
||||
}
|
||||
|
||||
if ($botUser->isAiBot()) {
|
||||
// AI 机器人
|
||||
$contents = [];
|
||||
if (preg_match_all("/<span class=\"mention task\" data-id=\"(\d+)\">(.*?)<\/span>/", $original, $match)) {
|
||||
// 任务
|
||||
$taskIds = Base::newIntval($match[1]);
|
||||
foreach ($taskIds as $index => $taskId) {
|
||||
$taskInfo = ProjectTask::with(['content'])->whereId($taskId)->first();
|
||||
if (!$taskInfo) {
|
||||
throw new Exception("任务不存在或已被删除");
|
||||
}
|
||||
$taskName = addslashes($taskInfo->name) . " (ID:{$taskId})";
|
||||
$taskContext = implode("\n", $taskInfo->AIContext());
|
||||
$contents[] = "<task_content path=\"{$taskName}\">\n{$taskContext}\n</task_content>";
|
||||
$original = str_replace($match[0][$index], "'{$taskName}' (see below for task_content tag)", $original);
|
||||
}
|
||||
}
|
||||
if (preg_match_all("/<a class=\"mention ([^'\"]*)\" href=\"([^\"']+?)\"[^>]*?>[~%]([^>]*)<\/a>/", $original, $match)) {
|
||||
// 文件、报告
|
||||
$urlPaths = $match[2];
|
||||
foreach ($urlPaths as $index => $urlPath) {
|
||||
$pathTag = null;
|
||||
$pathName = null;
|
||||
$pathContent = null;
|
||||
// 文件
|
||||
if (preg_match("/single\/file\/(.*?)$/", $urlPath, $fileMatch)) {
|
||||
$fileInfo = FileContent::idOrCodeToContent($fileMatch[1]);
|
||||
if (!$fileInfo || !isset($fileInfo->content['url'])) {
|
||||
throw new Exception("文件不存在或已被删除");
|
||||
}
|
||||
$urlPath = public_path($fileInfo->content['url']);
|
||||
if (!file_exists($urlPath)) {
|
||||
throw new Exception("文件不存在或已被删除");
|
||||
}
|
||||
$fileResult = TextExtractor::extractFile($urlPath);
|
||||
if (Base::isError($fileResult)) {
|
||||
throw new Exception("文件读取失败:" . $fileResult['msg']);
|
||||
}
|
||||
$pathTag = "file_content";
|
||||
$pathName = addslashes($match[3][$index]) . " (ID:{$fileInfo->id})";
|
||||
$pathContent = $fileResult['data'];
|
||||
}
|
||||
// 报告
|
||||
elseif (preg_match("/single\/report\/detail\/(.*?)$/", $urlPath, $reportMatch)) {
|
||||
$reportInfo = Report::idOrCodeToContent($reportMatch[1]);
|
||||
if (!$reportInfo) {
|
||||
throw new Exception("报告不存在或已被删除");
|
||||
}
|
||||
$pathTag = "report_content";
|
||||
$pathName = addslashes($match[3][$index]) . " (ID:{$reportInfo->id})";
|
||||
$pathContent = $reportInfo->content;
|
||||
}
|
||||
if ($pathTag) {
|
||||
$contents[] = "<{$pathTag} path=\"{$pathName}\">\n{$pathContent}\n</{$pathTag}>";
|
||||
$original = str_replace($match[0][$index], "'{$pathName}' (see below for {$pathTag} tag)", $original);
|
||||
}
|
||||
}
|
||||
}
|
||||
$original = Base::html2markdown($original);
|
||||
if ($contents) {
|
||||
// 添加tag内容
|
||||
$original .= "\n\n" . implode("\n\n", $contents);
|
||||
}
|
||||
return $original;
|
||||
} elseif ($botUser->isUserBot()) {
|
||||
// 用户机器人
|
||||
return Base::html2markdown($original);
|
||||
} else {
|
||||
// 其他机器人(系统)
|
||||
return trim(strip_tags($original));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取回复消息指令
|
||||
* @param $id
|
||||
* @param User $botUser
|
||||
* @return array
|
||||
*/
|
||||
private function extractReplyCommand($id, User $botUser)
|
||||
{
|
||||
$replyMsg = WebSocketDialogMsg::find($id);
|
||||
$replyCommand = null;
|
||||
if ($replyMsg) {
|
||||
switch ($replyMsg->type) {
|
||||
case 'text':
|
||||
try {
|
||||
$replyCommand = $this->extractCommand($replyMsg, $botUser);
|
||||
} catch (Exception) {
|
||||
return Base::retError('error', "引用消息解析失败。");
|
||||
}
|
||||
break;
|
||||
case 'file':
|
||||
if ($botUser->isAiBot()) {
|
||||
$msgData = Base::json2array($replyMsg->getRawOriginal('msg'));
|
||||
$fileResult = TextExtractor::extractFile(public_path($msgData['path']));
|
||||
if (Base::isError($fileResult)) {
|
||||
return Base::retError('error', $fileResult['msg']);
|
||||
} else {
|
||||
$replyCommand = $fileResult['data'];
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Base::retSuccess('success', $replyCommand);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成AI系统提示词
|
||||
* @param int|null $userid
|
||||
* @param WebSocketDialog $dialog
|
||||
* @param array $extras
|
||||
* @return void
|
||||
*/
|
||||
private function AIGenerateSystemMessage(int|null $userid, WebSocketDialog $dialog, array &$extras)
|
||||
{
|
||||
$system_messages = [];
|
||||
switch ($dialog->type) {
|
||||
case "user":
|
||||
$aiPrompt = WebSocketDialogConfig::where([
|
||||
'dialog_id' => $dialog->id,
|
||||
'userid' => $userid,
|
||||
'type' => 'ai_prompt',
|
||||
])->value('value');
|
||||
if ($aiPrompt) {
|
||||
$extras['system_message'] = $aiPrompt;
|
||||
}
|
||||
break;
|
||||
case "group":
|
||||
switch ($dialog->group_type) {
|
||||
case 'user':
|
||||
break;
|
||||
case 'project':
|
||||
$projectInfo = Project::whereDialogId($dialog->id)->first();
|
||||
if ($projectInfo) {
|
||||
$projectDesc = $projectInfo->desc ?: "-";
|
||||
$projectStatus = $projectInfo->archived_at ? '已归档' : '正在进行中';
|
||||
$system_messages[] = <<<EOF
|
||||
当前我在项目【{$projectInfo->name}】中
|
||||
项目描述:{$projectDesc}
|
||||
项目状态:{$projectStatus}
|
||||
|
||||
如果你判断我想要或需要添加任务,请按照以下格式回复:
|
||||
|
||||
::: create-task-list
|
||||
title: 任务标题1
|
||||
desc: 任务描述1
|
||||
|
||||
title: 任务标题2
|
||||
desc: 任务描述2
|
||||
:::
|
||||
EOF;
|
||||
}
|
||||
break;
|
||||
case 'task':
|
||||
$taskInfo = ProjectTask::with(['content'])->whereDialogId($dialog->id)->first();
|
||||
if ($taskInfo) {
|
||||
$taskContext = implode("\n", $taskInfo->AIContext());
|
||||
$system_messages[] = <<<EOF
|
||||
当前我在任务【{$taskInfo->name}】中
|
||||
当前时间:{$taskInfo->updated_at}
|
||||
任务ID:{$taskInfo->id}
|
||||
{$taskContext}
|
||||
|
||||
如果你判断我想要或需要添加子任务,请按照以下格式回复:
|
||||
|
||||
::: create-subtask-list
|
||||
title: 子任务标题1
|
||||
title: 子任务标题2
|
||||
:::
|
||||
EOF;
|
||||
}
|
||||
break;
|
||||
case 'department':
|
||||
$userDepartment = UserDepartment::whereDialogId($dialog->id)->first();
|
||||
if ($userDepartment) {
|
||||
$system_messages[] = "当前我在【{$userDepartment->name}】的部门群聊中";
|
||||
}
|
||||
break;
|
||||
case 'all':
|
||||
$system_messages[] = "当前我在【全体成员】的群聊中";
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if ($extras['system_message']) {
|
||||
array_unshift($system_messages, $extras['system_message']);
|
||||
}
|
||||
if ($system_messages) {
|
||||
$extras['system_message'] = implode("\n\n====\n\n", Base::newTrim($system_messages));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
namespace App\Tasks;
|
||||
|
||||
use App\Models\ApproveProcInstHistory;
|
||||
use App\Models\User;
|
||||
use App\Models\UserCheckinRecord;
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Module\Extranet;
|
||||
use App\Module\Timer;
|
||||
use Cache;
|
||||
@@ -82,6 +82,9 @@ class CheckinRemindTask extends AbstractTask
|
||||
if (!UserCheckinRecord::whereUserid($user->userid)->where('created_at', '>', Carbon::now()->subDays(3))->exists()) {
|
||||
continue; // 3天内没有打卡
|
||||
}
|
||||
if (ApproveProcInstHistory::userIsLeave($user->userid)) {
|
||||
continue; // 请假、外出
|
||||
}
|
||||
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
|
||||
if ($dialog) {
|
||||
if ($type === 'exceed') {
|
||||
|
||||
39
app/Tasks/ElasticSearchSyncTask.php
Normal file
39
app/Tasks/ElasticSearchSyncTask.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tasks;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/**
|
||||
* 同步聊天数据到Elasticsearch
|
||||
*/
|
||||
class ElasticSearchSyncTask extends AbstractTask
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function start()
|
||||
{
|
||||
// 120分钟执行一次
|
||||
$time = intval(Cache::get("ElasticSearchSyncTask:Time"));
|
||||
if (time() - $time < 120 * 60) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 执行开始,120分钟后缓存标记失效
|
||||
Cache::put("ElasticSearchSyncTask:Time", time(), Carbon::now()->addMinutes(120));
|
||||
|
||||
// 开始执行同步
|
||||
@shell_exec("php /var/www/artisan elasticsearch:sync-dialog-user-msg --i");
|
||||
|
||||
// 执行完成,5分钟后缓存标记失效(5分钟任务可重复执行)
|
||||
Cache::put("ElasticSearchSyncTask:Time", time(), Carbon::now()->addMinutes(5));
|
||||
}
|
||||
|
||||
public function end()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tasks;
|
||||
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
@@ -106,9 +107,9 @@ class EmailNoticeTask extends AbstractTask
|
||||
->groupBy('web_socket_dialog_msg_reads.userid');
|
||||
|
||||
// 分批处理用户的未读消息
|
||||
$query->chunk(self::CHUNK_SIZE, function($users) use ($dialogType, $startTime, $endTime) {
|
||||
$query->chunk(self::CHUNK_SIZE, function($users) use ($dialogType) {
|
||||
foreach ($users as $userData) {
|
||||
$this->sendUserEmail($userData->userid, $dialogType, $startTime, $endTime);
|
||||
$this->sendUserEmail($userData->userid, $dialogType);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -121,7 +122,7 @@ class EmailNoticeTask extends AbstractTask
|
||||
/**
|
||||
* 发送用户的未读消息邮件
|
||||
*/
|
||||
private function sendUserEmail(int $userId, string $dialogType, Carbon $startTime, Carbon $endTime): void
|
||||
private function sendUserEmail(int $userId, string $dialogType): void
|
||||
{
|
||||
// 验证用户
|
||||
$user = User::whereDisableAt(null)->find($userId);
|
||||
@@ -130,7 +131,7 @@ class EmailNoticeTask extends AbstractTask
|
||||
}
|
||||
|
||||
// 获取未读消息
|
||||
$messages = $this->getUnreadMessages($userId, $dialogType, $startTime, $endTime);
|
||||
$messages = $this->getUnreadMessages($userId, $dialogType);
|
||||
if ($messages->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@@ -156,7 +157,7 @@ class EmailNoticeTask extends AbstractTask
|
||||
/**
|
||||
* 获取用户的未读消息
|
||||
*/
|
||||
private function getUnreadMessages($userId, $dialogType, Carbon $startTime, Carbon $endTime)
|
||||
private function getUnreadMessages($userId, $dialogType)
|
||||
{
|
||||
return WebSocketDialogMsg::select([
|
||||
'web_socket_dialog_msgs.*',
|
||||
@@ -171,7 +172,6 @@ class EmailNoticeTask extends AbstractTask
|
||||
'web_socket_dialog_msgs.dialog_type' => $dialogType
|
||||
])
|
||||
->whereNull('r.read_at')
|
||||
->whereBetween('web_socket_dialog_msgs.created_at', [$startTime, $endTime])
|
||||
->whereIn('web_socket_dialog_msgs.type', self::ALLOWED_MSG_TYPES)
|
||||
->orderBy('web_socket_dialog_msgs.created_at')
|
||||
->limit(self::CHUNK_SIZE)
|
||||
|
||||
@@ -119,14 +119,20 @@ class WebSocketDialogMsgTask extends AbstractTask
|
||||
$mention = array_intersect([0, $userid], $mentions) ? 1 : 0;
|
||||
$silence = $mention ? false : $silence;
|
||||
$dot = $msg->type === 'record' ? 1 : 0;
|
||||
WebSocketDialogMsgRead::createInstance([
|
||||
$msgRead = WebSocketDialogMsgRead::createInstance([
|
||||
'dialog_id' => $msg->dialog_id,
|
||||
'msg_id' => $msg->id,
|
||||
'userid' => $userid,
|
||||
'mention' => $mention,
|
||||
'silence' => $silence,
|
||||
'dot' => $dot,
|
||||
])->saveOrIgnore();
|
||||
]);
|
||||
if ($msgRead->saveOrIgnore()) {
|
||||
if ($dialog->session_id && $dialog->session_id != $msg->session_id) {
|
||||
$msgRead->read_at = Carbon::now();
|
||||
$msgRead->save();
|
||||
}
|
||||
}
|
||||
$array[$userid] = [
|
||||
'userid' => $userid,
|
||||
'mention' => $mention,
|
||||
@@ -137,7 +143,7 @@ class WebSocketDialogMsgTask extends AbstractTask
|
||||
// 机器人收到消处理
|
||||
$botUser = User::whereUserid($userid)->whereBot(1)->first();
|
||||
if ($botUser) {
|
||||
$this->endArray[] = new BotReceiveMsgTask($botUser->userid, $msg->id, $mention, $this->client);
|
||||
$this->endArray[] = new BotReceiveMsgTask($botUser->userid, $msg->id, $mentions, $this->client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
54
bin/https
54
bin/https
@@ -142,6 +142,7 @@ install() {
|
||||
if /root/.acme.sh/acme.sh --installcert -d "${domain}" --fullchainpath "${sslPath}/${domain}.crt" --keypath "${sslPath}/${domain}.key" --ecc --force; then
|
||||
success "SSL 证书配置成功"
|
||||
sleep 2
|
||||
cp -r /root/.acme.sh/${domain}_ecc/*.conf ${sslPath}/
|
||||
fi
|
||||
else
|
||||
error "SSL 证书生成失败"
|
||||
@@ -165,5 +166,54 @@ error_page 497 https://\$host\$request_uri;
|
||||
EOF
|
||||
}
|
||||
|
||||
check
|
||||
install
|
||||
UPDATE_LOG="$(dirname "$PWD")/docker/nginx/site/ssl/update.log"
|
||||
SSL_PATH="$(dirname "$PWD")/docker/nginx/site/ssl"
|
||||
upgrade_cert(){
|
||||
curl https://get.acme.sh | sh
|
||||
if [[ 0 -ne $? ]]; then
|
||||
echo "安装证书更新脚本失败"
|
||||
echo $(date)": 安装证书更新脚本失败" >> ${UPDATE_LOG}
|
||||
exit 1
|
||||
fi
|
||||
file=$1
|
||||
domain=$(basename "$file" .key)
|
||||
old_crt_md5=$(md5sum ${SSL_PATH}/${domain}.crt| awk '{print $1}')
|
||||
/root/.acme.sh/acme.sh --renew --standalone -d ${domain} --fullchainpath "${SSL_PATH}/${domain}.crt" --keypath "${SSL_PATH}/${domain}.key" --ecc --force
|
||||
new_crt_md5=$(md5sum ${SSL_PATH}/${domain}.crt| awk '{print $1}')
|
||||
if [ "${old_key_md5}" == "${new_key_md5}" ]; then
|
||||
echo "${domain} 证书更新脚本失败"
|
||||
echo $(date)": ${domain} 证书更新失败" >> ${UPDATE_LOG}
|
||||
echo $(date)": ${old_crt_md5} == ${new_crt_md5}" >> ${UPDATE_LOG}
|
||||
else
|
||||
echo "${domain} 证书更新脚本成功"
|
||||
echo $(date)": ${domain} 证书更新成功" >> ${UPDATE_LOG}
|
||||
fi
|
||||
}
|
||||
|
||||
check_expire(){
|
||||
apk add --no-cache openssl socat
|
||||
find ${SSL_PATH} -type f -name "*.key" | while read -r file; do
|
||||
CERT_PATH=$file
|
||||
expiry_date=$(openssl x509 -enddate -noout -in "$CERT_PATH" | cut -d= -f2)
|
||||
expiry_timestamp=$(date -d "$expiry_date" +%s)
|
||||
current_timestamp=$(date +%s)
|
||||
days_remaining=$(( (expiry_timestamp - current_timestamp) / 86400 ))
|
||||
echo "剩余时间${days_remaining}天" >> ${UPDATE_LOG}
|
||||
if [ "$days_remaining" -lt 30 ]; then
|
||||
upgrade_cert $file
|
||||
fi
|
||||
done
|
||||
}
|
||||
case "${1}" in
|
||||
"install")
|
||||
check
|
||||
install
|
||||
;;
|
||||
"renew")
|
||||
check_expire
|
||||
;;
|
||||
*)
|
||||
echo "test"
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
4
bin/version.js
vendored
4
bin/version.js
vendored
File diff suppressed because one or more lines are too long
122
cmd
122
cmd
@@ -156,9 +156,9 @@ run_electron() {
|
||||
npm install
|
||||
fi
|
||||
if [ ! -d "./electron/node_modules" ]; then
|
||||
pushd electron
|
||||
pushd electron || exit
|
||||
npm install
|
||||
popd
|
||||
popd || exit
|
||||
fi
|
||||
#
|
||||
if [ -d "./electron/dist" ]; then
|
||||
@@ -178,8 +178,9 @@ run_electron() {
|
||||
|
||||
run_exec() {
|
||||
local container=$1
|
||||
local cmd=$2
|
||||
local name=`docker_name $container`
|
||||
shift 1
|
||||
local cmd=$@
|
||||
local name=$(docker_name "$container")
|
||||
if [ -z "$name" ]; then
|
||||
error "没有找到 $container 容器!"
|
||||
exit 1
|
||||
@@ -322,15 +323,26 @@ https_auto() {
|
||||
if [[ "$restart_nginx" == "y" ]]; then
|
||||
$COMPOSE up -d
|
||||
fi
|
||||
docker run -it --rm -v $(pwd):/work nginx:alpine sh "/work/bin/https"
|
||||
docker run -it --rm -v $(pwd):/work nginx:alpine sh /work/bin/https install
|
||||
if [[ 0 -eq $? ]]; then
|
||||
run_exec nginx "nginx -s reload"
|
||||
fi
|
||||
new_job="* 6 * * * docker run -it --rm -v $(pwd):/work nginx:alpine sh /work/bin/https renew"
|
||||
current_crontab=$(crontab -l 2>/dev/null)
|
||||
if ! echo "$current_crontab" | grep -v "https renew"; then
|
||||
echo "任务已存在,无需添加。"
|
||||
else
|
||||
crontab -l |{
|
||||
cat
|
||||
echo "$new_job"
|
||||
} | crontab -
|
||||
echo "任务已添加。"
|
||||
fi
|
||||
}
|
||||
|
||||
env_get() {
|
||||
local key=$1
|
||||
local value=`cat ${cur_path}/.env | grep "^$key=" | awk -F '=' '{print $2}'`
|
||||
local value=`cat ${cur_path}/.env | grep "^$key=" | awk -F '=' '{print $2}' | tr -d '\r\n'`
|
||||
echo "$value"
|
||||
}
|
||||
|
||||
@@ -405,14 +417,53 @@ if [ $# -gt 0 ]; then
|
||||
rm -rf vendor
|
||||
rm -rf composer.lock
|
||||
fi
|
||||
mkdir -p "${cur_path}/docker/log/supervisor"
|
||||
mkdir -p "${cur_path}/docker/mysql/data"
|
||||
chmod -R 775 "${cur_path}/docker/log/supervisor"
|
||||
chmod -R 775 "${cur_path}/docker/mysql/data"
|
||||
# 目录权限
|
||||
volumes=(
|
||||
"docker/log/supervisor"
|
||||
"docker/mysql/data"
|
||||
"docker/office/logs"
|
||||
"docker/office/data"
|
||||
"docker/es/data"
|
||||
)
|
||||
cmda=""
|
||||
cmdb=""
|
||||
for vol in "${volumes[@]}"; do
|
||||
tmp_path="${cur_path}/${vol}"
|
||||
mkdir -p "${tmp_path}"
|
||||
chmod -R 775 "${tmp_path}"
|
||||
rm -f "${tmp_path}/dootask.lock"
|
||||
cmda="${cmda} -v ${tmp_path}:/usr/share/${vol}"
|
||||
cmdb="${cmdb} touch /usr/share/${vol}/dootask.lock &&"
|
||||
done
|
||||
# 目录权限检测
|
||||
remaining=10
|
||||
while true; do
|
||||
((remaining=$remaining-1))
|
||||
writable="yes"
|
||||
docker run --rm ${cmda} nginx:alpine sh -c "${cmdb} touch /usr/share/docker/dootask.lock" &> /dev/null
|
||||
for vol in "${volumes[@]}"; do
|
||||
if [ ! -f "${vol}/dootask.lock" ]; then
|
||||
if [ $remaining -lt 0 ]; then
|
||||
error "目录【${vol}】权限不足!"
|
||||
exit 1
|
||||
else
|
||||
writable="no"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [ "$writable" == "yes" ]; then
|
||||
break
|
||||
else
|
||||
sleep 3
|
||||
fi
|
||||
done
|
||||
# 设置ES索引后缀
|
||||
env_set ES_INDEX_SUFFIX "$(rand_string 6)"
|
||||
# 启动容器
|
||||
[[ "$(arg_get port)" -gt 0 ]] && env_set APP_PORT "$(arg_get port)"
|
||||
$COMPOSE up php -d
|
||||
# 安装composer依赖
|
||||
# 安装PHP依赖
|
||||
run_exec php "composer install"
|
||||
if [ ! -f "${cur_path}/vendor/autoload.php" ]; then
|
||||
run_exec php "composer config repo.packagist composer https://packagist.phpcomposer.com"
|
||||
@@ -424,45 +475,32 @@ if [ $# -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
[[ -z "$(env_get APP_KEY)" ]] && run_exec php "php artisan key:generate"
|
||||
# 设置生产模式
|
||||
switch_debug "false"
|
||||
# 检查数据库
|
||||
remaining=20
|
||||
while [ ! -f "${cur_path}/docker/mysql/data/$(env_get DB_DATABASE)/db.opt" ]; do
|
||||
((remaining=$remaining-1))
|
||||
if [ $remaining -lt 0 ]; then
|
||||
error "数据库初始化失败!"
|
||||
exit 1
|
||||
fi
|
||||
chmod -R 775 "${cur_path}/docker/mysql/data"
|
||||
done
|
||||
# 数据库迁移
|
||||
remaining=20
|
||||
while [ ! -f "${cur_path}/docker/mysql/data/$(env_get DB_DATABASE)/$(env_get DB_PREFIX)migrations.ibd" ]; do
|
||||
((remaining=$remaining-1))
|
||||
if [ $remaining -lt 0 ]; then
|
||||
error "数据库安装失败!"
|
||||
exit 1
|
||||
fi
|
||||
sleep 3
|
||||
run_exec php "php artisan migrate --seed"
|
||||
done
|
||||
# 设置初始化密码
|
||||
res=`run_exec mariadb "sh /etc/mysql/repassword.sh"`
|
||||
run_exec php "php artisan migrate --seed"
|
||||
# 启动其他容器
|
||||
$COMPOSE up -d
|
||||
restart_php
|
||||
success "安装完成"
|
||||
info "地址: http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
|
||||
info "$res"
|
||||
# 设置初始化密码
|
||||
run_exec mariadb "sh /etc/mysql/repassword.sh"
|
||||
elif [[ "$1" == "update" ]]; then
|
||||
shift 1
|
||||
if [[ "$@" != "nobackup" ]]; then
|
||||
run_mysql backup
|
||||
fi
|
||||
if [[ -z "$(arg_get local)" ]]; then
|
||||
git fetch --all
|
||||
git reset --hard origin/$(git branch | sed -n -e 's/^\* \(.*\)/\1/p')
|
||||
current_branch=$(git branch | sed -n -e 's/^\* \(.*\)/\1/p')
|
||||
db_changes=$(git diff --name-only HEAD..origin/$current_branch | grep -E "^database/")
|
||||
if [[ -n "$db_changes" ]]; then
|
||||
info "数据库有迁移变动,执行数据库备份..."
|
||||
run_mysql backup
|
||||
fi
|
||||
git reset --hard origin/$current_branch
|
||||
git pull
|
||||
run_exec php "composer update"
|
||||
else
|
||||
info "执行数据库备份..."
|
||||
run_mysql backup
|
||||
fi
|
||||
run_exec php "php artisan migrate"
|
||||
run_exec nginx "nginx -s reload"
|
||||
@@ -513,7 +551,7 @@ if [ $# -gt 0 ]; then
|
||||
success "修改成功"
|
||||
elif [[ "$1" == "repassword" ]]; then
|
||||
shift 1
|
||||
run_exec mariadb "sh /etc/mysql/repassword.sh \"$@\""
|
||||
run_exec mariadb "sh /etc/mysql/repassword.sh $@"
|
||||
elif [[ "$1" == "serve" ]] || [[ "$1" == "dev" ]] || [[ "$1" == "development" ]]; then
|
||||
shift 1
|
||||
run_compile dev
|
||||
@@ -539,9 +577,9 @@ if [ $# -gt 0 ]; then
|
||||
elif [[ "$1" == "npm" ]]; then
|
||||
shift 1
|
||||
npm $@
|
||||
cd electron
|
||||
pushd electron || exit
|
||||
npm $@
|
||||
cd ..
|
||||
popd || exit
|
||||
docker run --rm -it -v ${cur_path}/resources/mobile:/work -w /work --entrypoint=/bin/bash node:16 -c "npm $@"
|
||||
elif [[ "$1" == "doc" ]]; then
|
||||
shift 1
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
"require": {
|
||||
"php": "^8.0",
|
||||
"ext-curl": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-fileinfo": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-imagick": "*",
|
||||
"ext-json": "*",
|
||||
@@ -18,6 +20,7 @@
|
||||
"ext-simplexml": "*",
|
||||
"ext-zip": "*",
|
||||
"directorytree/ldaprecord-laravel": "^2.7",
|
||||
"elasticsearch/elasticsearch": "^8.17",
|
||||
"fideloper/proxy": "^4.4.1",
|
||||
"firebase/php-jwt": "^6.9",
|
||||
"fruitcake/laravel-cors": "^2.0.4",
|
||||
@@ -28,12 +31,16 @@
|
||||
"laravel/tinker": "^v2.6.1",
|
||||
"laravolt/avatar": "^5.1",
|
||||
"league/commonmark": "^2.5",
|
||||
"league/html-to-markdown": "^5.1",
|
||||
"maatwebsite/excel": "^3.1.31",
|
||||
"madnest/madzipper": "^v1.1.0",
|
||||
"mews/captcha": "^3.2.6",
|
||||
"orangehill/iseed": "^3.0.1",
|
||||
"overtrue/pinyin": "^4.0",
|
||||
"phpoffice/phppresentation": "^1.1",
|
||||
"phpoffice/phpword": "^1.3",
|
||||
"predis/predis": "^1.1.7",
|
||||
"smalot/pdfparser": "^2.11",
|
||||
"symfony/mailer": "^6.0"
|
||||
},
|
||||
"require-dev": {
|
||||
@@ -82,7 +89,10 @@
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
|
||||
1835
composer.lock
generated
1835
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddArchiveFieldsToPreProjectsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('projects', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('projects', 'archive_method')) {
|
||||
$table->after('personal', function ($table) {
|
||||
$table->string('archive_method', 20)->nullable()->default('system')->comment('自动归档方式');
|
||||
$table->integer('archive_days')->nullable()->default(30)->comment('自动归档天数');
|
||||
});
|
||||
$table->index('archive_method');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('projects', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_projects_archive_method');
|
||||
$table->dropColumn([
|
||||
'archive_method',
|
||||
'archive_days'
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateProjectTaskTemplatesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
if (!Schema::hasTable('project_task_templates')) {
|
||||
Schema::create('project_task_templates', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('project_id')->index()->comment('项目ID');
|
||||
$table->string('name', 100)->comment('模板名称');
|
||||
$table->string('title', 255)->nullable()->comment('任务标题');
|
||||
$table->text('content')->nullable()->comment('任务内容');
|
||||
$table->unsignedTinyInteger('sort')->default(0)->comment('排序');
|
||||
$table->boolean('is_default')->default(false)->comment('是否默认模板');
|
||||
$table->unsignedBigInteger('userid')->index()->comment('创建人');
|
||||
$table->timestamps();
|
||||
|
||||
// 外键约束
|
||||
$table->foreign('project_id')->references('id')->on('projects')->onDelete('cascade');
|
||||
$table->foreign('userid')->references('userid')->on('users');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('project_task_templates');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateWebSocketDialogConfigsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
if (!Schema::hasTable('web_socket_dialog_configs')) {
|
||||
Schema::create('web_socket_dialog_configs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->bigInteger('dialog_id')->unsigned()->index()->comment('对话ID');
|
||||
$table->bigInteger('userid')->unsigned()->index()->comment('用户ID');
|
||||
$table->string('type', 50)->default('')->comment('配置类型');
|
||||
$table->text('value')->nullable()->comment('配置值');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('web_socket_dialog_configs');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateProjectTagsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
if (Schema::hasTable('project_tags')) {
|
||||
return;
|
||||
}
|
||||
Schema::create('project_tags', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('project_id')->index()->comment('项目ID');
|
||||
$table->string('name', 50)->comment('标签名称');
|
||||
$table->string('desc', 255)->nullable()->comment('标签描述');
|
||||
$table->string('color', 20)->nullable()->default('')->comment('颜色');
|
||||
$table->unsignedBigInteger('userid')->index()->comment('创建人');
|
||||
$table->timestamps();
|
||||
|
||||
// 外键约束
|
||||
$table->foreign('project_id')->references('id')->on('projects')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('project_tags');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AddFulltextIndexToWebSocketDialogMsgsTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$tableName = 'web_socket_dialog_msgs';
|
||||
$column = 'key'; // 需要添加 FULLTEXT 索引的字段
|
||||
|
||||
// 检查 FULLTEXT 索引是否已经存在
|
||||
if (!$this->fullTextIndexExists($tableName, $column)) {
|
||||
Schema::table($tableName, function (Blueprint $table) use ($column) {
|
||||
$table->fullText($column);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
// 删除 FULLTEXT 索引
|
||||
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
|
||||
$table->dropFullText(['key']);
|
||||
});
|
||||
}
|
||||
|
||||
private function fullTextIndexExists($tableName, $column)
|
||||
{
|
||||
// 获取当前数据库名称
|
||||
$databaseName = env('DB_DATABASE');
|
||||
|
||||
// 查询 information_schema.statistics 表
|
||||
$prefix = DB::getTablePrefix();
|
||||
$indexExists = DB::table(DB::raw('information_schema.statistics'))
|
||||
->where('table_schema', $databaseName)
|
||||
->where('table_name', $prefix . $tableName)
|
||||
->where('index_type', 'FULLTEXT')
|
||||
->get();
|
||||
|
||||
// 检查返回的索引是否包含指定的列
|
||||
foreach ($indexExists as $index) {
|
||||
$indexColumns = explode(',', $index->COLUMN_NAME ?? $index->column_name ?? '');
|
||||
// 如果索引包含指定的列,则返回 true
|
||||
if (in_array($column, $indexColumns)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddBotToWebSocketDialogMsgs extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('web_socket_dialog_msgs', 'bot')) {
|
||||
$table->tinyInteger('bot')->nullable()->default(0)->after('modify')->comment('是否机器人的消息');
|
||||
$table->index('bot');
|
||||
}
|
||||
});
|
||||
|
||||
// 获取表前缀
|
||||
$prefix = DB::getTablePrefix();
|
||||
|
||||
// 使用原生SQL更新数据
|
||||
/** @noinspection SqlNoDataSourceInspection */
|
||||
DB::statement("
|
||||
UPDATE {$prefix}web_socket_dialog_msgs m
|
||||
INNER JOIN {$prefix}users u ON u.userid = m.userid
|
||||
SET m.bot = 1
|
||||
WHERE u.bot = 1
|
||||
");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('web_socket_dialog_msgs', 'bot')) {
|
||||
$table->dropIndex('bot');
|
||||
$table->dropColumn('bot');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddIsNotifiedToUmengAlias extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('umeng_alias', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('umeng_alias', 'is_notified')) {
|
||||
$table->tinyInteger('is_notified')->nullable()->default(0)->after('ua')->comment('通知权限');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('umeng_alias', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('umeng_alias', 'is_notified')) {
|
||||
$table->dropColumn('is_notified');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddVersionToUmengAlias extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('umeng_alias', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('umeng_alias', 'version')) {
|
||||
$table->string('version', 50)->nullable()->default('')->after('device')->comment('应用版本号');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('umeng_alias', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('umeng_alias', 'version')) {
|
||||
$table->dropColumn('version');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UpdateProjectTasksSubtaskProjectIdAndColumnId extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
$prefix = DB::getTablePrefix();
|
||||
$now = Carbon::now();
|
||||
DB::statement("
|
||||
UPDATE {$prefix}project_tasks AS subtask
|
||||
INNER JOIN {$prefix}project_tasks AS parent ON subtask.parent_id = parent.id
|
||||
SET
|
||||
subtask.project_id = parent.project_id,
|
||||
subtask.column_id = parent.column_id,
|
||||
subtask.updated_at = '{$now}'
|
||||
WHERE subtask.parent_id > 0
|
||||
");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
// No need for down operation as this is a data correction
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UpdateProjectTasksSubtaskVisibility extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
$prefix = DB::getTablePrefix();
|
||||
$now = Carbon::now();
|
||||
DB::statement("
|
||||
UPDATE {$prefix}project_tasks AS subtask
|
||||
INNER JOIN {$prefix}project_tasks AS parent ON subtask.parent_id = parent.id
|
||||
SET
|
||||
subtask.visibility = parent.visibility,
|
||||
subtask.updated_at = '{$now}'
|
||||
WHERE subtask.parent_id > 0
|
||||
");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
// No need for down operation as this is a data correction
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddBotToWebSocketDialogUsersTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('web_socket_dialog_users', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('web_socket_dialog_users', 'bot')) {
|
||||
$table->tinyInteger('bot')->nullable()->default(0)->after('userid')->comment('是否机器人');
|
||||
$table->index('bot');
|
||||
}
|
||||
});
|
||||
|
||||
// 获取表前缀
|
||||
$prefix = DB::getTablePrefix();
|
||||
|
||||
// 使用原生SQL更新数据
|
||||
/** @noinspection SqlNoDataSourceInspection */
|
||||
DB::statement("
|
||||
UPDATE {$prefix}web_socket_dialog_users du
|
||||
INNER JOIN {$prefix}users u ON u.userid = du.userid
|
||||
SET du.bot = 1
|
||||
WHERE u.bot = 1
|
||||
");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('web_socket_dialog_users', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('web_socket_dialog_users', 'bot')) {
|
||||
$table->dropIndex('bot');
|
||||
$table->dropColumn('bot');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class WebSocketDialogMsgsAddSessionId extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('web_socket_dialog_msgs', 'session_id')) {
|
||||
$table->bigInteger('session_id')->index()->nullable()->default(0)->after('dialog_type')->comment('会话ID');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
|
||||
$table->dropColumn('session_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class WebSocketDialogsAddSessionId extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('web_socket_dialogs', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('web_socket_dialogs', 'session_id')) {
|
||||
$table->bigInteger('session_id')->index()->nullable()->default(0)->after('group_type')->comment('会话ID');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('web_socket_dialogs', function (Blueprint $table) {
|
||||
$table->dropColumn('session_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Models\WebSocketDialogSession;
|
||||
use App\Module\Base;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateWebSocketDialogSessionsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
if (Schema::hasTable('web_socket_dialog_sessions')) {
|
||||
return;
|
||||
}
|
||||
Schema::create('web_socket_dialog_sessions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->bigInteger('dialog_id')->unsigned()->index()->comment('对话ID');
|
||||
$table->string('title', 255)->default('')->comment('会话标题');
|
||||
$table->timestamps();
|
||||
});
|
||||
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.email'])
|
||||
->join('web_socket_dialog_users as du', 'web_socket_dialogs.id', '=', 'du.dialog_id')
|
||||
->join('users as u', 'du.userid', '=', 'u.userid')
|
||||
->where('u.email', 'like', 'ai-%@bot.system')
|
||||
->where('web_socket_dialogs.type', 'user')
|
||||
->get();
|
||||
foreach ($list as $item) {
|
||||
$title = WebSocketDialogMsg::whereDialogId($item->id)->where('key', '!=', '')->orderBy('id')->value('key');
|
||||
$session = WebSocketDialogSession::createInstance([
|
||||
'dialog_id' => $item->id,
|
||||
'title' => $title ? Base::cutStr($title, 100) : 'Unknown',
|
||||
'created_at' => $item->created_at,
|
||||
]);
|
||||
$session->save();
|
||||
$item->session_id = $session->id;
|
||||
$item->save();
|
||||
WebSocketDialogMsg::whereDialogId($item->id)->update(['session_id' => $session->id]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('web_socket_dialog_sessions');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Module\Base;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class UpdateAiModelsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
$row = Setting::whereName('aibotSetting')->first();
|
||||
if (empty($row)) {
|
||||
return;
|
||||
}
|
||||
$value = Base::json2array($row->getRawOriginal('setting'));
|
||||
foreach ($value as $key => $item) {
|
||||
if (str_ends_with($key, '_models')) {
|
||||
$value[$key] = preg_replace('/\s*:\s*/', ' | ', $item);
|
||||
}
|
||||
}
|
||||
$row->setting = Base::array2json($value);
|
||||
$row->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
$row = Setting::whereName('aibotSetting')->first();
|
||||
if (empty($row)) {
|
||||
return;
|
||||
}
|
||||
$value = Base::json2array($row->getRawOriginal('setting'));
|
||||
foreach ($value as $key => $item) {
|
||||
if (str_ends_with($key, '_models')) {
|
||||
$value[$key] = preg_replace('/\s*\|\s*/', ': ', $item);
|
||||
}
|
||||
}
|
||||
$row->setting = Base::array2json($value);
|
||||
$row->save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Models\WebSocketDialogSession;
|
||||
use App\Module\Base;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class UpdateWebSocketDialogMsgsSessionId extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.email'])
|
||||
->join('web_socket_dialog_users as du', 'web_socket_dialogs.id', '=', 'du.dialog_id')
|
||||
->join('users as u', 'du.userid', '=', 'u.userid')
|
||||
->where('u.email', 'like', 'ai-%@bot.system')
|
||||
->where('web_socket_dialogs.type', 'user')
|
||||
->get();
|
||||
foreach ($list as $item) {
|
||||
$msg = WebSocketDialogMsg::whereDialogId($item->id)->whereSessionId(0)->orderBy('id')->first();
|
||||
if ($msg || empty($item->session_id)) {
|
||||
$title = $msg?->key;
|
||||
$session = WebSocketDialogSession::createInstance([
|
||||
'dialog_id' => $item->id,
|
||||
'title' => $title ? Base::cutStr($title, 100) : 'Unknown',
|
||||
'created_at' => $item->created_at,
|
||||
]);
|
||||
$session->save();
|
||||
if (empty($item->session_id)) {
|
||||
$item->session_id = $session->id;
|
||||
$item->save();
|
||||
}
|
||||
if ($msg) {
|
||||
WebSocketDialogMsg::whereDialogId($item->id)->whereSessionId(0)->update(['session_id' => $session->id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateReportLinksTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('report_links', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('rid')->nullable()->default(0)->index()->comment('报告ID');
|
||||
$table->integer('num')->nullable()->default(0)->comment('累计访问');
|
||||
$table->string('code')->nullable()->default('')->comment('链接码');
|
||||
$table->bigInteger('userid')->nullable()->default(0)->index()->comment('会员ID');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('report_links');
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ services:
|
||||
php:
|
||||
container_name: "dootask-php-${APP_ID}"
|
||||
image: "kuaifan/php:swoole-8.0.rc18"
|
||||
shm_size: "2gb"
|
||||
shm_size: 2G
|
||||
ulimits:
|
||||
core:
|
||||
soft: 0
|
||||
@@ -25,8 +25,10 @@ services:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.2"
|
||||
depends_on:
|
||||
- redis
|
||||
- mariadb
|
||||
mariadb:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
nginx:
|
||||
@@ -41,20 +43,16 @@ services:
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.3"
|
||||
links:
|
||||
- php
|
||||
- office
|
||||
- fileview
|
||||
- drawio-webapp
|
||||
- drawio-export
|
||||
- minder
|
||||
- okr
|
||||
- ai
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
container_name: "dootask-redis-${APP_ID}"
|
||||
image: "redis:alpine"
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.4"
|
||||
@@ -68,12 +66,16 @@ services:
|
||||
- ./docker/mysql/conf.d:/etc/mysql/conf.d
|
||||
- ./docker/mysql/data:/var/lib/mysql
|
||||
environment:
|
||||
TZ: "${TIMEZONE:-PRC}"
|
||||
MYSQL_PREFIX: "${DB_PREFIX}"
|
||||
MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}"
|
||||
MYSQL_DATABASE: "${DB_DATABASE}"
|
||||
MYSQL_USER: "${DB_USERNAME}"
|
||||
MYSQL_PASSWORD: "${DB_PASSWORD}"
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "${DB_USERNAME}", "-p${DB_PASSWORD}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.5"
|
||||
@@ -81,11 +83,13 @@ services:
|
||||
|
||||
office:
|
||||
container_name: "dootask-office-${APP_ID}"
|
||||
image: "onlyoffice/documentserver:8.2.1.1"
|
||||
image: "onlyoffice/documentserver:8.2.2.1"
|
||||
volumes:
|
||||
- ./docker/office/logs:/var/log/onlyoffice
|
||||
- ./docker/office/data:/var/www/onlyoffice/Data
|
||||
- ./docker/office/etc/documentserver/default.json:/etc/onlyoffice/documentserver/default.json
|
||||
- ./docker/office/resources/require.js:/var/www/onlyoffice/documentserver/web-apps/vendor/requirejs/require.js
|
||||
- ./docker/office/resources/common/main/resources/img/header:/var/www/onlyoffice/documentserver/web-apps/apps/common/main/resources/img/header
|
||||
- ./docker/office/resources/documenteditor/main/resources/css/app.css:/var/www/onlyoffice/documentserver/web-apps/apps/documenteditor/main/resources/css/app.css
|
||||
- ./docker/office/resources/documenteditor/mobile/css/526.caf35c11a8d72ca5ac85.css:/var/www/onlyoffice/documentserver/web-apps/apps/documenteditor/mobile/css/526.caf35c11a8d72ca5ac85.css
|
||||
- ./docker/office/resources/presentationeditor/main/resources/css/app.css:/var/www/onlyoffice/documentserver/web-apps/apps/presentationeditor/main/resources/css/app.css
|
||||
@@ -101,7 +105,7 @@ services:
|
||||
|
||||
fileview:
|
||||
container_name: "dootask-fileview-${APP_ID}"
|
||||
image: "kuaifan/fileview:4.2.0-SNAPSHOT-RC25"
|
||||
image: "kuaifan/fileview:4.4.0-3"
|
||||
environment:
|
||||
KK_CONTEXT_PATH: "/fileview"
|
||||
KK_OFFICE_PREVIEW_SWITCH_DISABLED: true
|
||||
@@ -114,7 +118,7 @@ services:
|
||||
|
||||
drawio-webapp:
|
||||
container_name: "dootask-drawio-webapp-${APP_ID}"
|
||||
image: "jgraph/drawio:20.8.20"
|
||||
image: "jgraph/drawio:24.7.17"
|
||||
volumes:
|
||||
- ./docker/drawio/webapp/index.html:/usr/local/tomcat/webapps/draw/index.html
|
||||
- ./docker/drawio/webapp/stencils:/usr/local/tomcat/webapps/draw/stencils
|
||||
@@ -124,8 +128,6 @@ services:
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.8"
|
||||
depends_on:
|
||||
- drawio-export
|
||||
restart: unless-stopped
|
||||
|
||||
drawio-export:
|
||||
@@ -162,13 +164,15 @@ services:
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.11"
|
||||
depends_on:
|
||||
- mariadb
|
||||
restart: unless-stopped
|
||||
|
||||
ai:
|
||||
container_name: "dootask-ai-${APP_ID}"
|
||||
image: "kuaifan/dooai:0.2.1"
|
||||
image: "kuaifan/dootask-ai:0.3.5"
|
||||
environment:
|
||||
REDIS_HOST: "${REDIS_HOST}"
|
||||
REDIS_PORT: "${REDIS_PORT}"
|
||||
TIMEOUT: 600
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.12"
|
||||
@@ -179,7 +183,7 @@ services:
|
||||
image: "kuaifan/doookr:0.4.5"
|
||||
environment:
|
||||
TZ: "${TIMEZONE:-PRC}"
|
||||
DOO_TASK_URL: "http://${APP_IPPR}.3"
|
||||
DOO_TASK_URL: "http://nginx"
|
||||
MYSQL_HOST: "${DB_HOST}"
|
||||
MYSQL_PORT: "${DB_PORT}"
|
||||
MYSQL_DBNAME: "${DB_DATABASE}"
|
||||
@@ -191,8 +195,6 @@ services:
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.13"
|
||||
depends_on:
|
||||
- mariadb
|
||||
restart: unless-stopped
|
||||
|
||||
face:
|
||||
@@ -209,14 +211,27 @@ services:
|
||||
MYSQL_PASSWORD: "${DB_PASSWORD}"
|
||||
MYSQL_DB_NAME: "${DB_DATABASE}"
|
||||
DB_PREFIX: "${DB_PREFIX}"
|
||||
REPORT_API: "http://${APP_IPPR}.3:80/api/public/checkin/report"
|
||||
depends_on:
|
||||
- mariadb
|
||||
REPORT_API: "http://nginx/api/public/checkin/report"
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.14"
|
||||
restart: unless-stopped
|
||||
|
||||
es:
|
||||
container_name: "dootask-es-${APP_ID}"
|
||||
image: "elasticsearch:8.17.2"
|
||||
volumes:
|
||||
- ./docker/es/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
|
||||
- ./docker/es/data:/usr/share/elasticsearch/data
|
||||
environment:
|
||||
discovery.type: single-node
|
||||
xpack.security.enabled: false
|
||||
ES_JAVA_OPTS: "-Xms1g -Xmx1g"
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.15"
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
extnetwork:
|
||||
name: "dootask-networks-${APP_ID}"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Change
|
||||
|
||||
diff https://github.com/jgraph/drawio/tree/1a858166fb4f1330cf23e58941e3fbec1dcd16f8
|
||||
diff https://github.com/jgraph/drawio/tree/acd938b1e42cff3be3b629e6239cdec9a9baddcc
|
||||
|
||||
@@ -8,23 +8,23 @@
|
||||
<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
|
||||
<meta itemprop="description" content="draw.io is a free online diagramming application and flowchart maker . You can use it to create UML, entity relationship,
|
||||
org charts, BPMN and BPM, database schema and networks. Also possible are telecommunication network, workflow, flowcharts, maps overlays and GIS, electronic
|
||||
circuit and social network diagrams.">
|
||||
<meta itemprop="image" content="https://lh4.googleusercontent.com/-cLKEldMbT_E/Tx8qXDuw6eI/AAAAAAAAAAs/Ke0pnlk8Gpg/w500-h344-k/BPMN%2Bdiagram%2Brc2f.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<meta name="msapplication-config" content="images/browserconfig.xml">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="theme-color" content="#d89000">
|
||||
<script type="text/javascript">
|
||||
window.EXPORT_URL = window.location.origin + "/drawio/export/";
|
||||
window.DRAWIO_LIGHTBOX_URL = window.location.origin + "/drawio/webapp";
|
||||
setInterval(function() {window.ICONSEARCH_PATH = window.location.origin + "/drawio/iconsearch";}, 1000)
|
||||
<script id="geBootstrap" type="text/javascript">
|
||||
window.EXPORT_URL = window.location.origin + "/drawio/export/";
|
||||
window.DRAWIO_LIGHTBOX_URL = window.location.origin + "/drawio/webapp";
|
||||
setInterval(function() {window.ICONSEARCH_PATH = window.location.origin + "/drawio/iconsearch";}, 1000)
|
||||
|
||||
/**
|
||||
* URL Parameters and protocol description are here:
|
||||
*
|
||||
* https://desk.draw.io/support/solutions/articles/16000042546-what-url-parameters-are-supported
|
||||
* https://www.drawio.com/doc/faq/supported-url-parameters
|
||||
*
|
||||
* Parameters for developers:
|
||||
*
|
||||
@@ -44,27 +44,27 @@
|
||||
{
|
||||
var result = new Object();
|
||||
var params = window.location.search.slice(1).split('&');
|
||||
|
||||
|
||||
for (var i = 0; i < params.length; i++)
|
||||
{
|
||||
var idx = params[i].indexOf('=');
|
||||
|
||||
|
||||
if (idx > 0)
|
||||
{
|
||||
result[params[i].substring(0, idx)] = params[i].substring(idx + 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
})();
|
||||
|
||||
|
||||
// Forces CDN caches by passing URL parameters via URL hash
|
||||
if (window.location.hash != null && window.location.hash.substring(0, 2) == '#P')
|
||||
{
|
||||
try
|
||||
{
|
||||
urlParams = JSON.parse(decodeURIComponent(window.location.hash.substring(2)));
|
||||
|
||||
|
||||
if (urlParams.hash != null)
|
||||
{
|
||||
window.location.hash = urlParams.hash;
|
||||
@@ -75,9 +75,10 @@
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Global variable for desktop
|
||||
var mxIsElectron = window && window.process && window.process.type;
|
||||
var mxIsElectron = navigator.userAgent != null && navigator.userAgent.toLowerCase().indexOf(' electron/') > -1 &&
|
||||
navigator.userAgent.indexOf(' draw.io/') > -1;
|
||||
|
||||
// Redirects page if required
|
||||
if (urlParams['dev'] != '1')
|
||||
@@ -85,21 +86,21 @@
|
||||
(function()
|
||||
{
|
||||
var proto = window.location.protocol;
|
||||
|
||||
|
||||
if (!mxIsElectron)
|
||||
{
|
||||
var host = window.location.host;
|
||||
|
||||
|
||||
// Redirects apex, drive and rt to www
|
||||
if (host === 'draw.io' || host === 'rt.draw.io' || host === 'drive.draw.io')
|
||||
{
|
||||
host = 'www.draw.io';
|
||||
}
|
||||
|
||||
|
||||
var href = proto + '//' + host + window.location.href.substring(
|
||||
window.location.protocol.length +
|
||||
window.location.host.length + 2);
|
||||
|
||||
|
||||
// Redirects if href changes
|
||||
if (href != window.location.href)
|
||||
{
|
||||
@@ -108,7 +109,7 @@
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds meta tag to the page.
|
||||
*/
|
||||
@@ -117,15 +118,15 @@
|
||||
try
|
||||
{
|
||||
var s = document.createElement('meta');
|
||||
|
||||
if (name != null)
|
||||
|
||||
if (name != null)
|
||||
{
|
||||
s.setAttribute('name', name);
|
||||
}
|
||||
|
||||
s.setAttribute('content', content);
|
||||
|
||||
if (httpEquiv != null)
|
||||
|
||||
if (httpEquiv != null)
|
||||
{
|
||||
s.setAttribute('http-equiv', httpEquiv);
|
||||
}
|
||||
@@ -138,14 +139,14 @@
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Synchronously adds scripts to the page.
|
||||
*/
|
||||
function mxscript(src, onLoad, id, dataAppKey, noWrite, onError)
|
||||
{
|
||||
var defer = onLoad == null && !noWrite;
|
||||
|
||||
|
||||
if ((urlParams['dev'] != '1' && typeof document.createElement('canvas').getContext === "function") ||
|
||||
onLoad != null || noWrite)
|
||||
{
|
||||
@@ -158,16 +159,16 @@
|
||||
{
|
||||
s.setAttribute('id', id);
|
||||
}
|
||||
|
||||
|
||||
if (dataAppKey != null)
|
||||
{
|
||||
s.setAttribute('data-app-key', dataAppKey);
|
||||
}
|
||||
|
||||
|
||||
if (onLoad != null)
|
||||
{
|
||||
var r = false;
|
||||
|
||||
|
||||
s.onload = s.onreadystatechange = function()
|
||||
{
|
||||
if (!r && (!this.readyState || this.readyState == 'complete'))
|
||||
@@ -185,9 +186,9 @@
|
||||
onError('Failed to load ' + src, e);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
var t = document.getElementsByTagName('script')[0];
|
||||
|
||||
|
||||
if (t != null)
|
||||
{
|
||||
t.parentNode.insertBefore(s, t);
|
||||
@@ -209,11 +210,11 @@
|
||||
g.type = 'text/javascript';
|
||||
g.async = true;
|
||||
g.src = src;
|
||||
|
||||
|
||||
var s = document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(g, s);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Adds meta tags with application name (depends on offline URL parameter)
|
||||
*/
|
||||
@@ -225,13 +226,13 @@
|
||||
|
||||
if (mxIsElectron)
|
||||
{
|
||||
// mxmeta(null, 'default-src \'self\' \'unsafe-inline\'; connect-src \'self\' https://*.draw.io https://fonts.googleapis.com https://fonts.gstatic.com; img-src * data:; media-src *; font-src *; style-src-elem \'self\' \'unsafe-inline\' https://fonts.googleapis.com', 'Content-Security-Policy');
|
||||
mxmeta(null, 'default-src \'self\'; script-src \'self\' \'sha256-6g514VrT/cZFZltSaKxIVNFF46+MFaTSDTPB8WfYK+c=\'; connect-src \'self\' https://*.draw.io https://*.diagrams.net https://fonts.googleapis.com https://fonts.gstatic.com; img-src * data:; media-src *; font-src *; frame-src \'none\'; style-src \'self\' \'unsafe-inline\' https://fonts.googleapis.com; base-uri \'none\';child-src \'self\';object-src \'none\';', 'Content-Security-Policy');
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
// Checks for local storage
|
||||
var isLocalStorage = false;
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
isLocalStorage = urlParams['local'] != '1' && typeof(localStorage) != 'undefined';
|
||||
@@ -242,34 +243,30 @@
|
||||
}
|
||||
|
||||
var mxScriptsLoaded = false, mxWinLoaded = false;
|
||||
|
||||
|
||||
function checkAllLoaded()
|
||||
{
|
||||
if (mxScriptsLoaded && mxWinLoaded)
|
||||
{
|
||||
App.main();
|
||||
App.main();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var t0 = new Date();
|
||||
|
||||
// Changes paths for local development environment
|
||||
if (urlParams['dev'] == '1')
|
||||
{
|
||||
// Used to request grapheditor/mxgraph sources in dev mode
|
||||
var mxDevUrl = document.location.protocol + '//devhost.jgraph.com/drawio/src/main';
|
||||
|
||||
var mxDevUrl = '';
|
||||
|
||||
// Used to request draw.io sources in dev mode
|
||||
var drawDevUrl = document.location.protocol + '//devhost.jgraph.com/drawio/src/main/webapp/';
|
||||
var geBasePath = drawDevUrl + '/js/grapheditor';
|
||||
var mxBasePath = mxDevUrl + '/mxgraph';
|
||||
|
||||
var drawDevUrl = '';
|
||||
var geBasePath = 'js/grapheditor';
|
||||
var mxBasePath = 'mxgraph/src';
|
||||
|
||||
if (document.location.protocol == 'file:')
|
||||
{
|
||||
geBasePath = './js/grapheditor';
|
||||
mxBasePath = './mxgraph';
|
||||
drawDevUrl = './';
|
||||
|
||||
// Forces includes for dev environment in node.js
|
||||
mxForceIncludes = true;
|
||||
}
|
||||
@@ -280,19 +277,19 @@
|
||||
mxscript(drawDevUrl + 'js/diagramly/Init.js');
|
||||
mxscript(geBasePath + '/Init.js');
|
||||
mxscript(mxBasePath + '/mxClient.js');
|
||||
|
||||
|
||||
// Adds all JS code that depends on mxClient. This indirection via Devel.js is
|
||||
// required in some browsers to make sure mxClient.js (and the files that it
|
||||
// loads asynchronously) are available when the code loaded in Devel.js runs.
|
||||
mxscript(drawDevUrl + 'js/diagramly/Devel.js');
|
||||
|
||||
|
||||
// Electron
|
||||
if (mxIsElectron)
|
||||
{
|
||||
mxscript('js/diagramly/DesktopLibrary.js');
|
||||
mxscript('js/diagramly/ElectronApp.js');
|
||||
}
|
||||
|
||||
|
||||
mxscript(drawDevUrl + 'js/PostConfig.js');
|
||||
}
|
||||
else
|
||||
@@ -300,18 +297,18 @@
|
||||
(function()
|
||||
{
|
||||
var hostName = window.location.hostname;
|
||||
|
||||
|
||||
// Supported domains are *.draw.io and the packaged version in Quip
|
||||
var supportedDomain = (hostName.substring(hostName.length - 8, hostName.length) === '.draw.io') ||
|
||||
(hostName.substring(hostName.length - 13, hostName.length) === '.diagrams.net');
|
||||
|
||||
|
||||
function loadAppJS()
|
||||
{
|
||||
mxscript('js/app.min.js', function()
|
||||
{
|
||||
mxScriptsLoaded = true;
|
||||
checkAllLoaded();
|
||||
|
||||
|
||||
// Electron
|
||||
if (mxIsElectron)
|
||||
{
|
||||
@@ -332,14 +329,14 @@
|
||||
});
|
||||
});
|
||||
}
|
||||
else if (!supportedDomain)
|
||||
else if (!supportedDomain || navigator.onLine)
|
||||
{
|
||||
mxscript('js/PostConfig.js');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (!supportedDomain || mxIsElectron)
|
||||
|
||||
if (!supportedDomain || mxIsElectron || navigator.onLine)
|
||||
{
|
||||
mxscript('js/PreConfig.js', loadAppJS);
|
||||
}
|
||||
@@ -354,19 +351,17 @@
|
||||
window.onerror = function()
|
||||
{
|
||||
var status = document.getElementById('geStatus');
|
||||
|
||||
|
||||
if (status != null)
|
||||
{
|
||||
status.innerHTML = 'Page could not be loaded. Please try refreshing.';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<link rel="chrome-webstore-item" href="https://chrome.google.com/webstore/detail/plgmlhohecdddhbmmkncjdmlhcmaachm">
|
||||
<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="icon" href="favicon.ico" sizes="any">
|
||||
<link rel="apple-touch-icon" href="images/apple-touch-icon.png">
|
||||
<link rel="stylesheet" type="text/css" href="styles/grapheditor.css">
|
||||
<link rel="stylesheet" media="(forced-colors: active)" href="styles/high-contrast.css" id="high-contrast-stylesheet">
|
||||
<link rel="canonical" href="https://app.diagrams.net">
|
||||
<link rel="manifest" href="images/manifest.json">
|
||||
<link rel="shortcut icon" href="favicon.ico">
|
||||
@@ -378,7 +373,7 @@
|
||||
color:#606060;
|
||||
}
|
||||
.geBlock {
|
||||
display: none;
|
||||
display: none;
|
||||
z-index:-3;
|
||||
margin:100px;
|
||||
margin-top:40px;
|
||||
@@ -443,7 +438,7 @@
|
||||
<div class="geBlock">
|
||||
<h1>Flowchart Maker and Online Diagram Software</h1>
|
||||
<p>
|
||||
draw.io is free online diagram software. You can use it as a flowchart maker, network diagram software, to create UML online, as an ER diagram tool,
|
||||
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™ and Lucidchart™ files .
|
||||
</p>
|
||||
<h2 id="geStatus">Loading...</h2>
|
||||
@@ -452,7 +447,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
<script id="geMain" type="text/javascript">
|
||||
/**
|
||||
* Main
|
||||
*/
|
||||
|
||||
18953
docker/drawio/webapp/js/app.min.js
vendored
18953
docker/drawio/webapp/js/app.min.js
vendored
File diff suppressed because one or more lines are too long
668
docker/drawio/webapp/js/diagramly/ElectronApp.js
vendored
668
docker/drawio/webapp/js/diagramly/ElectronApp.js
vendored
File diff suppressed because it is too large
Load Diff
2
docker/es/config/elasticsearch.yml
Normal file
2
docker/es/config/elasticsearch.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
cluster.name: "docker-cluster"
|
||||
network.host: 0.0.0.0
|
||||
2
docker/es/data/.gitignore
vendored
Executable file
2
docker/es/data/.gitignore
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
3
docker/log/.gitignore
vendored
3
docker/log/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
*/
|
||||
*
|
||||
!.gitignore
|
||||
|
||||
@@ -1,27 +1,99 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# 重置用户密码脚本
|
||||
#
|
||||
# 使用方法:
|
||||
# ./repassword.sh [账号标识符] [自定义密码]
|
||||
#
|
||||
# 参数说明:
|
||||
# [账号标识符]: 可选,可以是用户ID(纯数字)或邮箱地址。不提供时默认为第一个管理员用户
|
||||
# [自定义密码]: 可选,指定要设置的新密码。不提供时会自动生成随机密码
|
||||
#
|
||||
# 使用示例:
|
||||
# ./repassword.sh # 重置第一个管理员用户密码(随机生成)
|
||||
# ./repassword.sh 123 # 重置ID=123的用户密码(随机生成)
|
||||
# ./repassword.sh user@example.com # 重置邮箱为user@example.com的用户密码(随机生成)
|
||||
# ./repassword.sh 123 newpass # 重置ID=123的用户密码为"newpass"
|
||||
# ./repassword.sh user@example.com newpass # 重置邮箱为user@example.com的用户密码为"newpass"
|
||||
#
|
||||
|
||||
new_password=$1
|
||||
account_identifier=$1
|
||||
custom_password=$2
|
||||
|
||||
GreenBG="\033[42;37m"
|
||||
RedBG="\033[41;37m"
|
||||
Font="\033[0m"
|
||||
|
||||
# 生成随机密码
|
||||
new_encrypt=$(date +%s%N | md5sum | awk '{print $1}' | cut -c 1-6)
|
||||
if [ -z "$new_password" ]; then
|
||||
if [ -z "$custom_password" ]; then
|
||||
new_password=$(date +%s%N | md5sum | awk '{print $1}' | cut -c 1-16)
|
||||
else
|
||||
new_password=$custom_password
|
||||
fi
|
||||
md5_password=$(echo -n `echo -n $new_password | md5sum | awk '{print $1}'`$new_encrypt | md5sum | awk '{print $1}')
|
||||
|
||||
content=$(echo "select \`email\` from ${MYSQL_PREFIX}users where \`userid\`=1;" | mysql -u$MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE)
|
||||
account=$(echo "$content" | sed -n '2p')
|
||||
# 构建查询条件
|
||||
if [ -z "$account_identifier" ]; then
|
||||
# 默认查询第一个管理员
|
||||
admin_query=$(echo "SELECT userid FROM ${MYSQL_PREFIX}users WHERE identity LIKE '%,admin,%' ORDER BY userid LIMIT 1;" | mysql -u$MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE)
|
||||
identifier_value=$(echo "$admin_query" | sed -n '2p')
|
||||
|
||||
if [ -z "$identifier_value" ]; then
|
||||
echo "${RedBG}错误:未找到管理员用户!${Font}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
where_field="userid"
|
||||
identifier_type="管理员ID"
|
||||
else
|
||||
# 检查是否为纯数字(ID)
|
||||
# 使用更兼容的 shell 语法检查是否为纯数字
|
||||
case "$account_identifier" in
|
||||
''|*[!0-9]*)
|
||||
# 非纯数字,视为邮箱
|
||||
where_field="email"
|
||||
identifier_type="邮箱"
|
||||
identifier_value="$account_identifier"
|
||||
;;
|
||||
*)
|
||||
# 纯数字,视为ID
|
||||
where_field="userid"
|
||||
identifier_type="ID"
|
||||
identifier_value="$account_identifier"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# 构建 WHERE 条件(为邮箱添加引号)
|
||||
if [ "$where_field" = "email" ]; then
|
||||
where_condition="where $where_field='$identifier_value'"
|
||||
else
|
||||
where_condition="where $where_field=$identifier_value"
|
||||
fi
|
||||
|
||||
# 查询用户信息
|
||||
content=$(echo "select userid,email from ${MYSQL_PREFIX}users $where_condition;" | mysql -u$MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE)
|
||||
|
||||
# 提取用户ID和邮箱
|
||||
user_id=$(echo "$content" | sed -n '2p' | awk '{print $1}')
|
||||
account=$(echo "$content" | sed -n '2p' | awk '{print $2}')
|
||||
|
||||
if [ -z "$account" ]; then
|
||||
echo "错误:账号不存在!"
|
||||
echo "${RedBG}错误:${identifier_type} '${identifier_value}' 的账号不存在!${Font}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 更新密码
|
||||
mysql -u$MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE <<EOF
|
||||
update ${MYSQL_PREFIX}users set \`encrypt\`='${new_encrypt}',\`password\`='${md5_password}' where \`userid\`=1;
|
||||
update ${MYSQL_PREFIX}users set encrypt='${new_encrypt}',password='${md5_password}' $where_condition;
|
||||
EOF
|
||||
|
||||
echo "账号: ${GreenBG}${account}${Font}"
|
||||
# 只在 identifier_type="ID" 时才输出ID
|
||||
if [ "$identifier_type" = "ID" ]; then
|
||||
echo "ID: ${GreenBG}${user_id}${Font}"
|
||||
fi
|
||||
|
||||
# 输出邮箱和密码
|
||||
echo "邮箱: ${GreenBG}${account}${Font}"
|
||||
echo "密码: ${GreenBG}${new_password}${Font}"
|
||||
|
||||
1
docker/nginx/conf.d/.gitignore
vendored
1
docker/nginx/conf.d/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
|
||||
@@ -78,125 +78,7 @@ server {
|
||||
proxy_pass http://service;
|
||||
}
|
||||
|
||||
location /fileview {
|
||||
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://fileview:8012;
|
||||
}
|
||||
|
||||
location /office/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Real-PORT $remote_port;
|
||||
proxy_set_header X-Forwarded-Host $the_host/office;
|
||||
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_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 3600s;
|
||||
proxy_pass http://office/;
|
||||
}
|
||||
|
||||
location /drawio/webapp/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Real-PORT $remote_port;
|
||||
proxy_set_header X-Forwarded-Host $the_host/drawio/webapp;
|
||||
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_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_pass http://drawio-webapp:8080/;
|
||||
}
|
||||
|
||||
location /drawio/export/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Real-PORT $remote_port;
|
||||
proxy_set_header X-Forwarded-Host $the_host/drawio/export;
|
||||
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_set_header Upgrade $http_upgrade;
|
||||
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;
|
||||
}
|
||||
|
||||
# OKR
|
||||
location /apps/okr/ {
|
||||
proxy_pass http://okr:5566/apps/okr/;
|
||||
}
|
||||
|
||||
# AI
|
||||
location /ai/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Scheme $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_pass http://ai:8881/;
|
||||
}
|
||||
include /etc/nginx/conf.d/location/*.conf;
|
||||
}
|
||||
|
||||
include /etc/nginx/conf.d/conf.d/*.conf;
|
||||
|
||||
12
docker/nginx/location/ai.conf
Normal file
12
docker/nginx/location/ai.conf
Normal file
@@ -0,0 +1,12 @@
|
||||
location /ai/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 3600s;
|
||||
proxy_set_header Scheme $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_pass http://ai:5001/;
|
||||
}
|
||||
13
docker/nginx/location/approve.conf
Normal file
13
docker/nginx/location/approve.conf
Normal file
@@ -0,0 +1,13 @@
|
||||
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;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user